fix(cli): settle signal-terminated shell commands as 128 + signum - #12698
Conversation
| @@ -443,13 +444,7 @@ export const make = Effect.gen(function* () { | |||
| isRunning: Effect.map(Deferred.isDone(signal), (done) => !done), | |||
| exitCode: Effect.flatMap(Deferred.await(signal), ([code, signal]) => { | |||
| if (Predicate.isNotNull(code)) return Effect.succeed(ExitCode(code)) | |||
There was a problem hiding this comment.
SUGGESTION: This null check is now duplicated inside settle, so the settle([code, signal]) branch below can never observe a non-null code.
Since settle already handles the numeric case and returns an Effect, the whole callback could collapse to exitCode: Effect.flatMap(Deferred.await(signal), settle). That removes the dead branch and shrinks the diff against upstream in this shared file to a single changed line, which matches the stated intent of the PR. Your call.
Reply with @kilocode-bot fix it to have Kilo Code address this issue.
| command, | ||
| ), | ||
| ) | ||
| return settle([code, signal]) // kilocode_change - signal termination settles as 128 + signum |
There was a problem hiding this comment.
SUGGESTION: Settling immediately on signal termination also changes process-group cleanup, and the release handler above isn't updated for it.
The acquireRelease release path only group-kills when the leader exited with a non-null, non-zero code (if (code !== 0 && Predicate.isNotNull(code)) return yield* Effect.ignore(kill(killGroup))). For a signal death code stays null, so no killGroup runs. Previously the shell tool never reached scope close promptly: it waited out its timeout branch and then called handle.kill({ forceKillAfter }), which does killGroup and reaped descendants of the shell. Now the scope closes as soon as the leader dies, so background descendants of a signal-killed shell (e.g. sleep 300 & kill -SEGV $$) survive with their pipes still open.
Extending the release condition to also group-kill when the leader died from a signal would preserve the old cleanup guarantee. Not a blocker for the hang fix itself.
Reply with @kilocode-bot fix it to have Kilo Code address this issue.
| // bash tool waiting on a numeric code that never arrived. | ||
| export const settle = ([code, signal]: readonly [code: number | null, signal: NodeJS.Signals | null]) => { | ||
| if (Predicate.isNotNull(code)) return Effect.succeed(ExitCode(code)) | ||
| if (Predicate.isNotNull(signal)) return Effect.succeed(ExitCode(128 + constants.signals[signal])) |
There was a problem hiding this comment.
SUGGESTION: constants.signals is typed as carrying every NodeJS.Signals key, but at runtime it only contains the signals the current platform defines (SIGINFO is absent on Linux, and Windows exposes a small subset). An unmapped name evaluates to 128 + undefined, i.e. ExitCode(NaN), which then flows into metadata.exit and out over the SDK as a non-numeric exit code — a quieter failure mode than the error this replaces.
Node normally only reports signal names that exist on the platform, so this is defensive rather than a live bug, but a guarded lookup (check the value is a number, otherwise fall back to 128) would keep the result numeric in all cases. No suggestion block here because ?? on the non-nullable typed lookup would itself trip the typechecker.
Reply with @kilocode-bot fix it to have Kilo Code address this issue.
Code Review SummaryStatus: No Issues Found | Recommendation: Merge Incremental review of
Files Reviewed (3 files)
Previous Review Summary (commit 1d360c8)Current summary above is authoritative. Previous snapshots are kept for context only. Previous review (commit 1d360c8)Status: 3 Issues Found | Recommendation: Address before merge Overview
Issue Details (click to expand)SUGGESTION
Files Reviewed (5 files)
The core approach looks right: mapping signal termination to Reviewed by claude-opus-5 · Input: 26 · Output: 3.9K · Cached: 506.8K Review guidance: REVIEW.md from base branch |
1d360c8 to
4d093bb
Compare
Port from Kilo-Org/kilocode#12698: report signal-terminated commands with a human-readable note instead of a bare numeric exit code. Kilo's fix settles a signal-killed process as the conventional 128+signum exit code so its bash tool stops hanging. Hermes already produces numeric codes for signal deaths (subprocess -signum, or the shell's 128+signum), but the model saw a bare exit_code=-9 or 137 and burned turns mis-diagnosing (137 = OOM kill being the most common). This adapts the idea to Hermes' existing exit-code semantics tier: - _interpret_signal_exit(): maps negative codes (definite signal death) and the 128+signum band (hedged with 'usually') to a note naming the signal and its likely cause, wired into _interpret_exit_code() ahead of the per-command semantics table. - Curated signal table (SIGKILL/SIGSEGV/SIGTERM/SIGABRT/...) so ambiguous application exit codes are never mislabeled; uncurated 128+N codes stay silent, SIGINT is excluded (executor's interrupt-marker path owns rc=130). - Notes surface via the existing exit_code_meaning result field. E2E verified against real SIGSEGV/SIGKILL processes.
Port from Kilo-Org/kilocode#12698: report signal-terminated commands with a human-readable note instead of a bare numeric exit code. Kilo's fix settles a signal-killed process as the conventional 128+signum exit code so its bash tool stops hanging. Hermes already produces numeric codes for signal deaths (subprocess -signum, or the shell's 128+signum), but the model saw a bare exit_code=-9 or 137 and burned turns mis-diagnosing (137 = OOM kill being the most common). This adapts the idea to Hermes' existing exit-code semantics tier: - _interpret_signal_exit(): maps negative codes (definite signal death) and the 128+signum band (hedged with 'usually') to a note naming the signal and its likely cause, wired into _interpret_exit_code() ahead of the per-command semantics table. - Curated signal table (SIGKILL/SIGSEGV/SIGTERM/SIGABRT/...) so ambiguous application exit codes are never mislabeled; uncurated 128+N codes stay silent, SIGINT is excluded (executor's interrupt-marker path owns rc=130). - Notes surface via the existing exit_code_meaning result field. E2E verified against real SIGSEGV/SIGKILL processes.
…ite (#8) * fix(config): parse list/mapping literals in hermes config set Fold the list/mapping parser INSIDE the existing string-typed-value coercion guard (the `not isinstance(_default_value_for_key(key), str)` block from e4ea0a0) instead of running it unconditionally, so a genuinely string-typed setting whose value merely starts with '[' or '{' is left untouched while non-string keys get JSON/YAML flow literals parsed to real lists/dicts. Update website/docs/user-guide/configuring-models.md: the `config set only writes scalar values` note is no longer accurate; document the list/mapping support with a quoted example. Fixes NousResearch#40545 NousResearch#50168 * fix(config): extend structured-value parsing to multi-line YAML blocks with a conservative trigger Consolidation follow-up on top of NousResearch#59182's cherry-picked base: - Add _looks_structured_value(): triggers a yaml.safe_load structured parse only when the value starts with '[' / '{' or spans multiple lines with YAML list-item ('- x') or mapping-entry ('key: v') shaped lines. Deliberately avoids the over-broad leading '-' trigger from NousResearch#88066 so '-5' and '--flag' stay strings. - Stays folded INSIDE the string-typed-key guard: keys whose DEFAULT_CONFIG type is str (e.g. approvals.mode) are never coerced. - Tests: multi-line YAML list/dict, string-typed key given '[x]' and '-5' stays string, dash-prefixed scalars stay strings, plain multi-line prose stays a string, load_config round-trip. Sabotage-verified: 7 of the suite's tests fail on main without the fix. * Port from can1357/oh-my-pi#7553: allow quoted shell metacharacters in allowlist matching command_allowlist glob rules (e.g. 'cargo *') rejected any command whose quoted arguments contained shell metacharacters — a cargo benchmark regex filter like '^layer3/write/(a|b)$' disqualified the whole command even though those characters are literal to the shell. _has_allowlist_shell_operator is now quote-aware: - metacharacters inside single/double quotes or behind a backslash are treated as literal arguments; - $ and backtick inside DOUBLE quotes still disqualify (expansion is active there); - quoted/escaped control characters still disqualify when the command carries a -c/-e/--command/--eval-style option that hands the payload to another interpreter (sh -c '...', git -c alias.x='!...' x); - unterminated quotes disqualify (shape can't be reasoned about). Compound commands (unquoted ; & | < > backtick $( newline) are rejected exactly as before. hermes_cli/approvals_suggest.derive_glob picks up the same semantics via its existing import. * feat(terminal): interpret signal-termination exit codes for the model Port from Kilo-Org/kilocode#12698: report signal-terminated commands with a human-readable note instead of a bare numeric exit code. Kilo's fix settles a signal-killed process as the conventional 128+signum exit code so its bash tool stops hanging. Hermes already produces numeric codes for signal deaths (subprocess -signum, or the shell's 128+signum), but the model saw a bare exit_code=-9 or 137 and burned turns mis-diagnosing (137 = OOM kill being the most common). This adapts the idea to Hermes' existing exit-code semantics tier: - _interpret_signal_exit(): maps negative codes (definite signal death) and the 128+signum band (hedged with 'usually') to a note naming the signal and its likely cause, wired into _interpret_exit_code() ahead of the per-command semantics table. - Curated signal table (SIGKILL/SIGSEGV/SIGTERM/SIGABRT/...) so ambiguous application exit codes are never mislabeled; uncurated 128+N codes stay silent, SIGINT is excluded (executor's interrupt-marker path owns rc=130). - Notes surface via the existing exit_code_meaning result field. E2E verified against real SIGSEGV/SIGKILL processes. * Port from MoonshotAI/kimi-code#2596/NousResearch#2600: surface MCP tool-result _meta to the model, minus protocol-reserved keys MCP tool results carry a server _meta mapping (exposed as .meta by the Python SDK) alongside structuredContent. Servers return namespaced machine-readable contracts there (validated payloads, browser-handoff URLs); Hermes previously dropped the field entirely, so that data was invisible to the agent. Now _meta is included in the JSON tool output, after filtering protocol-reserved keys per the MCP spec's key-name rules: a prefix is reserved when a modelcontextprotocol or mcp label is followed by at least one more label (modelcontextprotocol.io/..., tools.mcp.com/...). Vendor namespaces with a trailing reserved word (com.example.mcp/...) and unprefixed keys pass through. Non-serializable metadata drops the extras rather than failing the call. * fix(telegram): rebind TypeHandler in the deferred SDK import `check_telegram_requirements()` re-imports python-telegram-bot after a lazy install and rebinds the module-level aliases that the top-level `except ImportError` block set to `typing.Any`. TypeHandler was left out of all three places: the `global` declaration, the `from telegram.ext import (...)` list, and the assignments. So whenever the top-level import fails and the deferred path runs, every other alias is restored and TELEGRAM_AVAILABLE flips to True, while TypeHandler stays `Any`. Handler registration then raises `TypeError: Any cannot be instantiated` and the gateway reports: [Telegram] Failed to connect to Telegram: Any cannot be instantiated Gateway started with no connected platforms The 22.6 -> 22.8 pin bump named in NousResearch#85272 is the trigger rather than the defect: it makes the top-level import fail, which is what routes the module through the deferred path where the omission has always been. * Port from aaif-goose/goose#10746: strip invisible Unicode TAG chars from MCP content Unicode TAG characters (U+E0000-U+E007F) render as nothing in terminals and chat UIs but are fully visible to LLM tokenizers, making them an ASCII-smuggling prompt-injection channel for untrusted MCP servers. - tools/ansi_strip.py: new strip_unicode_tags() with fast path; unlike goose we preserve valid emoji tag sequences (U+1F3F4 base + tag spec + U+E007F cancel), so regional flags survive. - tools/mcp_tool.py: applied at every MCP text ingestion point — tool result text blocks, embedded resource text, read_resource contents, get_prompt message content, and tool descriptions entering the schema. - tests/tools/test_unicode_tag_strip.py: smuggled-instruction vectors, goose's test vector, emoji-tag-sequence preservation, ZWJ untouched. * feat(business-os): implement Waves 1-33 strategic enhancements and full E2E QA suite - Add Waves 1-33 capabilities across finance, governance, GTM, SEO, and top-tier web/graphic design. - Implement Design System Tokens, WebGL Shaders, Motion Architecture, and Skeleton Shimmer Loaders. - Add WCAG 2.1 AAA Accessible Keyboard Focus Rings, Adaptive Breakpoints, and Fluid Typography Scalers. - Fix pre-existing unit test timing issues and model catalog mocking in test_models, test_objective_worker, and authority_integrity. - Create comprehensive E2E functional test suite (test_full_qa_e2e_workflow.py) verified across 381 unit & integration tests. * chore(contributors): add contributor mapping for paul.lesyuk@gmail.com --------- Co-authored-by: Sam Liu <sam7894604@gmail.com> Co-authored-by: Teknium <127238744+teknium1@users.noreply.github.com> Co-authored-by: Pavel Lesyuk <paul.lesyuk@gmail.com>
When a command run through the Bash tool dies from a signal, the spawned process produces no numeric exit code (Node reports
code: null, signal: 'SIGSEGV'). This is the common case, not an edge case:bash -cexecs a single simple command, so any segfaulting binary kills the spawned process by signal.The spawner's
exitCodeeffect failed in that situation, and the shell tool'sEffect.raceAllonly settles on the first success, so the call kept waiting for the abort or timeout branches. The tool looked hung until the full Bash timeout elapsed, and other consumers ofexitCode(AppProcess, command timeout handling, project git helpers) surfaced an opaqueUnknown: ChildProcess.exitCodeerror instead of a status.Report signal termination the way every POSIX shell does, as
128 + signum(139 for SIGSEGV, 143 for SIGTERM). The mapping lives in a Kilo-owned helper (packages/core/src/kilocode/exit-code.ts) and the shared spawner keeps only twokilocode_changemarked lines, so allexitCodeconsumers settle with a meaningful code at once. Regression coverage sits in Kilo-owned test files: a mapping and live-spawner test inpackages/core, and an end-to-end Bash tool test that terminates the shell with SIGSEGV and asserts the call settles promptly with exit 139 instead of waiting out its configured timeout.Supersedes #12679, which mapped the failure to exit code 1 inside the shell tool only. That left the other
exitCodeconsumers hanging and discarded the signal information that a 128 + signum code preserves.Fixes #12677