@gajae-code/coding-agent 0.18.1 → 0.18.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/dist/types/cli/stats-cli.d.ts +0 -5
  3. package/dist/types/config/autorouting-tier-map.d.ts +29 -2
  4. package/dist/types/config/model-registry.d.ts +16 -1
  5. package/dist/types/exa/mcp-client.d.ts +1 -5
  6. package/dist/types/exec/bash-shell-guardian.d.ts +26 -0
  7. package/dist/types/hashline/parser.d.ts +28 -5
  8. package/dist/types/modes/acp/acp-agent.d.ts +6 -1
  9. package/dist/types/sdk/broker/lifecycle.d.ts +6 -1
  10. package/dist/types/sdk/broker/recovery-backoff.d.ts +62 -0
  11. package/dist/types/sdk/broker/runtime.d.ts +20 -0
  12. package/dist/types/sdk/broker/startup-budget.d.ts +5 -1
  13. package/dist/types/sdk/bus/index.d.ts +1 -0
  14. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
  15. package/dist/types/sdk/host/session-runtime.d.ts +7 -0
  16. package/dist/types/sdk/prompt-watchdog.d.ts +1 -1
  17. package/dist/types/tools/bash.d.ts +1 -0
  18. package/package.json +7 -7
  19. package/src/cli/stats-cli.ts +4 -36
  20. package/src/cli-main.ts +4 -8
  21. package/src/cli.ts +4 -8
  22. package/src/commands/commit.ts +2 -2
  23. package/src/commands/sdk.ts +1 -2
  24. package/src/config/autorouting-tier-map.ts +20 -2
  25. package/src/config/model-preset-registry.ts +17 -2
  26. package/src/config/model-profile-activation.ts +3 -1
  27. package/src/config/model-profiles.ts +11 -3
  28. package/src/config/model-registry.ts +45 -17
  29. package/src/exa/mcp-client.ts +41 -8
  30. package/src/exec/bash-shell-guardian.ts +184 -34
  31. package/src/hashline/execute.ts +51 -3
  32. package/src/hashline/parser.ts +63 -16
  33. package/src/internal-urls/docs-index.generated.ts +22 -22
  34. package/src/modes/acp/acp-agent.ts +39 -4
  35. package/src/modes/controllers/ssh-command-controller.ts +3 -1
  36. package/src/sdk/broker/lifecycle.ts +408 -218
  37. package/src/sdk/broker/recovery-backoff.ts +135 -0
  38. package/src/sdk/broker/runtime.ts +83 -0
  39. package/src/sdk/broker/startup-budget.ts +6 -1
  40. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  41. package/src/sdk/bus/index.ts +305 -19
  42. package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
  43. package/src/sdk/host/session-runtime.ts +99 -3
  44. package/src/sdk/prompt-watchdog.ts +17 -4
  45. package/src/session/agent-session.ts +14 -2
  46. package/src/session/internal/managed-session-storage.ts +23 -0
  47. package/src/tools/bash.ts +39 -0
  48. package/dist/types/cli/grep-cli.d.ts +0 -17
  49. package/dist/types/cli/shell-cli.d.ts +0 -8
  50. package/dist/types/commands/grep.d.ts +0 -42
  51. package/dist/types/commands/shell.d.ts +0 -21
  52. package/src/cli/grep-cli.ts +0 -160
  53. package/src/cli/shell-cli.ts +0 -176
  54. package/src/commands/grep.ts +0 -48
  55. package/src/commands/shell.ts +0 -29
package/CHANGELOG.md CHANGED
@@ -2,6 +2,66 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.18.5] - 2026-09-30
6
+
7
+ ## [0.18.4] - 2026-09-30
8
+
9
+ ## [0.18.3] - 2026-09-30
10
+
11
+ ## [0.18.2] - 2026-09-30
12
+
13
+ ### Added
14
+
15
+ - Added the additive `codex-sol61` built-in model profile for GPT-6.1 Sol.
16
+
17
+ ### Changed
18
+
19
+ - Built-in `claude-opus` and `claude-fable` executor roles and the `opus-codex` planner role now use `anthropic/claude-sonnet-5-5` instead of `anthropic/claude-sonnet-5` (#6111).
20
+
21
+ ### Fixed
22
+
23
+ - Bare line-number and hash-only hashline anchors now fail with actionable, copy-ready full-anchor suggestions; edits remain unapplied until a full anchor is supplied.
24
+
25
+ - On macOS, the shell runtime no longer exits with code 70 when spawning fast-exiting child processes or entitled/setuid children (like `/bin/ps`, `/usr/bin/top`, `sudo`) that cannot be queried for unique identity. When a child is confirmed absent or terminated before observation, the process identity is recorded like on Linux. Only genuine integrity failures (ledger write failures, HMAC errors) trigger exit 70 on a live child (#6085).
26
+
27
+ - Exa MCP tool-call responses are formatted in bounded time. `formatGenericResponse` re-indents the entire formatted subtree at every object level, so cost grew super-linearly with nesting depth: 500/1000/2000/4000/8000 levels cost 60ms/227ms/730ms/5.9s/94s, and 20000 levels exhausted the call stack. The payload is whatever the remote MCP server returned, and the 16 MiB content cap does not bound depth — 8000 levels of `{"a":` is under 47 KB. Depth is now limited to 64, matching the session-import walker, and output for payloads within that limit is unchanged.
28
+
29
+ - Preserve the one-line hashline retry hint when copied text contains literal `..`, while keeping ambiguous range edits rejected without modifying the file (#6112).
30
+
31
+ - Nested `gjc` commands run through the Bash tool inside a managed-owner (tmux-supervised) session no longer fail with `managed_owner_admission_metadata_invalid`: the Bash boundary now scrubs the whole managed-owner env family, including the tmux owner server key and `GJC_TMUX_LAUNCHED`, instead of leaving a partial owner context behind (#6140).
32
+
33
+ - Keep the ACP prompt watchdog on its awaiting-model bound until a streamed tool call actually begins executing.
34
+
35
+ - Sessions without explicit `retry.*` settings now retry a content-free "socket connection was closed unexpectedly" (`ECONNRESET`) failure when the attempt is replay-safe, bounded by `retry.maxRetries`. Previously the bare-default gate only admitted stream-timeout watchdog errors, so a transient connection reset before any response ended the turn even though it was classified as transient.
36
+
37
+ - Fixed sessions intermittently hanging after a tool call on long contexts (seen with OpenGateway `-ultrafast` models). The CLI no longer routes `fetch()` through Bun's HTTP/2 client, which could stall a pooled connection on large request bodies until the process restarted.
38
+
39
+ - A long-running session whose `gjc` binary was replaced on disk no longer respawns the shared SDK broker over and over. Broker recovery now stops and asks for a session restart when this process's runtime image was replaced (same path, new file identity) or removed, and repeated recovery failures back off exponentially up to a cap instead of retrying every 30–60 seconds. Other clients sharing the broker are no longer disrupted by the churn (#6040).
40
+
41
+ - Model registry refreshes no longer rescan and re-lowercase the whole model catalog for every registry-profile selector; selector matching uses a prebuilt index.
42
+
43
+ - Validating cached model-preset registry documents is about a third faster: canonical JSON serialization no longer calls `JSON.stringify` for strings that need no escaping and checks surrogates with the native well-formedness test.
44
+
45
+ - A session sharing its managed scope with another `gjc` process (for example a master session and the SDK session it spawned) no longer stops persisting with `managed_replace_receipt_cleanup_pending:quarantine_collision` when both processes try to retire the same replacement cleanup receipt at the same time. The process that loses the race to claim the retirement slot now leaves the receipt for a later reconciliation, and its own session write continues.
46
+
47
+ - Keep SDK prompts alive when transient directed delivery back-pressure (`writer_backlog_full`) drops a correlated progress frame. Previously the prompt was abandoned while the run kept executing, so the final `agent_end` was dropped and ACP clients only settled the prompt when the prompt watchdog expired. Terminal frames now retry briefly under back-pressure so the prompt settles normally.
48
+ - Release the prompt submission and work lease when terminal delivery still fails after the retry window, while preserving the `delivery_failed` terminal outcome.
49
+
50
+ - Keep SDK and ACP prompts alive when a streaming message snapshot exceeds the directed frame limit, and report a correlated delivery failure to the requester only when the undeliverable frame is the run's own terminal, so a prompt is never reported failed while its execution is still running.
51
+ - Preserve assistant text shape and terminal truncation metadata when bounding oversized correlated frames.
52
+ - Preserve as much aggregate assistant message_end text as fits, including ordered text blocks.
53
+
54
+ - Give broker-admitted lifecycle launches without a worktree a fresh child readiness budget after bounded pre-spawn bookkeeping, while preserving caller-supplied exact deadlines.
55
+ - Bound broker-derived non-worktree pre-spawn preparation by the unused admission window (10s without queueing at default readiness), so fresh readiness still finishes inside the unchanged caller deadline.
56
+
57
+ - Exit lifecycle session hosts cleanly after publishing a complete rollback receipt at the readiness cutoff.
58
+
59
+ - Prove broker-owned session hosts have exited at readiness cutoff before returning retryable startup failures, including hosts blocked during extension initialization.
60
+
61
+ - `gjc stats --json` now writes only the JSON document to stdout, so `gjc stats --json | jq ...` works. The `Synced N new entries ...` summary goes to stderr in JSON mode, alongside the sync progress. The dashboard and `--summary` output are unchanged.
62
+
63
+ - Interactive `/ssh add ... --port` rejects a port with trailing or non-digit characters (for example `22oops` or `2222.5`) instead of saving the digits before them, matching the ACP `/ssh add` parser.
64
+
5
65
  ## [0.18.1] - 2026-09-29
6
66
 
7
67
  ### Fixed
@@ -8,10 +8,5 @@ export interface StatsCommandArgs {
8
8
  json: boolean;
9
9
  summary: boolean;
10
10
  }
11
- /**
12
- * Parse stats subcommand arguments.
13
- * Returns undefined if not a stats command.
14
- */
15
- export declare function parseStatsArgs(args: string[]): StatsCommandArgs | undefined;
16
11
  export declare function runStatsCommand(cmd: StatsCommandArgs): Promise<void>;
17
12
  export declare function printStatsHelp(): void;
@@ -33,14 +33,18 @@ export declare const CURATED_TIER_LABELS: {
33
33
  readonly tier: "fast";
34
34
  readonly rank: 1;
35
35
  }];
36
- readonly "anthropic/claude-sonnet-5": readonly [{
36
+ readonly "anthropic/claude-sonnet-5-5": readonly [{
37
37
  readonly tier: "balanced";
38
38
  readonly rank: 1;
39
39
  }];
40
- readonly "anthropic/claude-sonnet-4-6": readonly [{
40
+ readonly "anthropic/claude-sonnet-5": readonly [{
41
41
  readonly tier: "balanced";
42
42
  readonly rank: 2;
43
43
  }];
44
+ readonly "anthropic/claude-sonnet-4-6": readonly [{
45
+ readonly tier: "balanced";
46
+ readonly rank: 3;
47
+ }];
44
48
  readonly "anthropic/claude-opus-5-5": readonly [{
45
49
  readonly tier: "strong";
46
50
  readonly effort: "high";
@@ -70,6 +74,11 @@ export declare const CURATED_TIER_LABELS: {
70
74
  readonly effort: "high";
71
75
  readonly rank: 1;
72
76
  }];
77
+ readonly "openai-codex/gpt-6.1-sol": readonly [{
78
+ readonly tier: "strong";
79
+ readonly effort: "high";
80
+ readonly rank: 2;
81
+ }];
73
82
  readonly "google/gemini-3.5-flash-lite": readonly [{
74
83
  readonly tier: "fast";
75
84
  readonly rank: 1;
@@ -123,6 +132,24 @@ export declare const CURATED_TIER_LABELS: {
123
132
  };
124
133
  /** Generated from packages/ai/src/models.json at feature land: 3936 in-scope baseline skips. */
125
134
  export declare const TIER_MAP_SKIP_LIST: {
135
+ readonly "amazon-bedrock/anthropic.claude-sonnet-5-5": {
136
+ readonly rationale: "Sonnet 5.5 catalog addition (#6111); not yet curated";
137
+ };
138
+ readonly "amazon-bedrock/au.anthropic.claude-sonnet-5-5": {
139
+ readonly rationale: "Sonnet 5.5 catalog addition (#6111); not yet curated";
140
+ };
141
+ readonly "amazon-bedrock/eu.anthropic.claude-sonnet-5-5": {
142
+ readonly rationale: "Sonnet 5.5 catalog addition (#6111); not yet curated";
143
+ };
144
+ readonly "amazon-bedrock/global.anthropic.claude-sonnet-5-5": {
145
+ readonly rationale: "Sonnet 5.5 catalog addition (#6111); not yet curated";
146
+ };
147
+ readonly "amazon-bedrock/jp.anthropic.claude-sonnet-5-5": {
148
+ readonly rationale: "Sonnet 5.5 catalog addition (#6111); not yet curated";
149
+ };
150
+ readonly "amazon-bedrock/us.anthropic.claude-sonnet-5-5": {
151
+ readonly rationale: "Sonnet 5.5 catalog addition (#6111); not yet curated";
152
+ };
126
153
  readonly "kiro/claude-opus-5-5": {
127
154
  readonly rationale: "post-rebase catalog addition from dev; not yet curated";
128
155
  };
@@ -327,7 +327,22 @@ interface ProviderOverride {
327
327
  cacheRetention?: CacheRetention;
328
328
  isOAuth?: boolean;
329
329
  }
330
- export declare function registrySelectorResolvesToModel(selector: string, models: readonly Model<Api>[]): boolean;
330
+ /**
331
+ * Lowercased lookup keys for a model catalog, so selector resolution is a set
332
+ * lookup instead of a scan that re-lowercases every model id per selector.
333
+ */
334
+ export interface RegistrySelectorIndex {
335
+ /** `id` and `provider/id`. */
336
+ exact: ReadonlySet<string>;
337
+ /** `id` alone. */
338
+ ids: ReadonlySet<string>;
339
+ /** Model ids per provider, for parsed `provider/id` selectors (nested, so no delimiter can collide). */
340
+ providerIds: ReadonlyMap<string, ReadonlySet<string>>;
341
+ /** Every `/`-delimited suffix of each id (`a/b/c` → `b/c`, `c`). */
342
+ idSuffixes: ReadonlySet<string>;
343
+ }
344
+ export declare function createRegistrySelectorIndex(models: readonly Model<Api>[]): RegistrySelectorIndex;
345
+ export declare function registrySelectorResolvesToModel(selector: string, index: RegistrySelectorIndex): boolean;
331
346
  /**
332
347
  * Merge a freshly discovered model with the matching bundled/configured entry
333
348
  * (or a runtime provider override when no bundled entry exists).
@@ -22,11 +22,7 @@ export declare function callExaTool(toolName: string, args: Record<string, unkno
22
22
  export declare function callWebsetsTool(apiKey: string, toolName: string, args: Record<string, unknown>): Promise<unknown>;
23
23
  /** Format search results for LLM */
24
24
  export declare function formatSearchResults(data: ExaSearchResponse): string;
25
- /**
26
- * Format a non-search MCP response as human-readable text.
27
- * Handles objects, arrays, primitives, and common MCP response shapes.
28
- */
29
- export declare function formatGenericResponse(data: unknown): string;
25
+ export declare function formatGenericResponse(data: unknown, depth?: number): string;
30
26
  /** Check if result is a search response */
31
27
  export declare function isSearchResponse(data: unknown): data is ExaSearchResponse;
32
28
  /** Fetch and cache MCP tool schema */
@@ -11,5 +11,31 @@ export declare function extendOwnedDarwinAncestry<T extends {
11
11
  uniqueId: bigint;
12
12
  parentUniqueId: bigint;
13
13
  }>(knownUniqueIds: Set<bigint>, candidates: ReadonlyMap<number, T>): number[];
14
+ export declare function authenticateOwnershipRecord(line: string, ledgerToken: string): {
15
+ processRef?: NativeProcess;
16
+ darwinUniqueId?: bigint;
17
+ pending?: true;
18
+ } | undefined;
19
+ export type DarwinAncestryTracker = {
20
+ seed(uniqueId: bigint): void;
21
+ track(processRef: NativeProcess, uniqueId?: bigint): boolean;
22
+ trackGuardian(processRef: NativeProcess, uniqueId?: bigint): boolean;
23
+ poll(): boolean;
24
+ close(): void;
25
+ };
26
+ export type DarwinIdentity = {
27
+ uniqueId: bigint;
28
+ parentUniqueId: bigint;
29
+ };
30
+ /** Test seams for the Darwin tracker; production uses libproc and the natives binding. */
31
+ export type DarwinAncestryTrackerDeps = {
32
+ uniqueIdentity?: (pid: number) => DarwinIdentity | undefined;
33
+ fromPid?: (pid: number) => NativeProcess | null | undefined;
34
+ observe?: (pid: number) => {
35
+ status: "present" | "absent" | "unknown";
36
+ incarnation?: string;
37
+ };
38
+ };
39
+ export declare function createDarwinAncestryTracker(owned: Map<string, NativeProcess>, deps?: DarwinAncestryTrackerDeps): DarwinAncestryTracker | undefined;
14
40
  export declare function runBashShellGuardian(): Promise<void>;
15
41
  export {};
@@ -1,19 +1,41 @@
1
+ import { HL_OP_INSERT_AFTER, HL_OP_INSERT_BEFORE, HL_OP_REPLACE } from "./hash";
1
2
  import type { HashlineCursor, HashlineEdit } from "./types";
3
+ type HashlineOpSigil = typeof HL_OP_INSERT_BEFORE | typeof HL_OP_INSERT_AFTER | typeof HL_OP_REPLACE;
2
4
  /**
3
- * An op referenced lines by number alone, without the content hash. The edit
4
- * is never applied on a line number alone; the executor answers with the
5
- * current full anchors for {@link lines} so the model can retry without an
6
- * extra read.
5
+ * An op referenced lines by number alone. The edit is never applied on a line
6
+ * number alone; the executor answers with the current full anchors so the model
7
+ * can retry without another read.
7
8
  */
8
9
  export declare class HashlineMissingHashError extends Error {
9
10
  readonly lines: {
10
11
  start: number;
11
12
  end: number;
12
13
  };
14
+ readonly opSigil: HashlineOpSigil;
15
+ readonly rangeRaw?: {
16
+ start: string;
17
+ end: string;
18
+ } | undefined;
13
19
  constructor(message: string, lines: {
14
20
  start: number;
15
21
  end: number;
16
- });
22
+ }, opSigil: HashlineOpSigil, rangeRaw?: {
23
+ start: string;
24
+ end: string;
25
+ } | undefined);
26
+ }
27
+ export declare class HashlineMissingLineError extends Error {
28
+ readonly hash: string;
29
+ readonly lineNum: number | undefined;
30
+ readonly opSigil: HashlineOpSigil;
31
+ readonly rangeRaw?: {
32
+ start: string;
33
+ end: string;
34
+ } | undefined;
35
+ constructor(hash: string, lineNum: number | undefined, opSigil: HashlineOpSigil, rangeRaw?: {
36
+ start: string;
37
+ end: string;
38
+ } | undefined);
17
39
  }
18
40
  export declare function cloneCursor(cursor: HashlineCursor): HashlineCursor;
19
41
  export declare function parseHashline(diff: string): HashlineEdit[];
@@ -21,3 +43,4 @@ export declare function parseHashlineWithWarnings(diff: string): {
21
43
  edits: HashlineEdit[];
22
44
  warnings: string[];
23
45
  };
46
+ export {};
@@ -46,7 +46,12 @@ export declare function paginateAcpSessions(listed: unknown[], cwd: string | und
46
46
  export declare function acpAvailableCommandsFromSkills(query: unknown): AvailableCommand[];
47
47
  type SdkPromptFailedOutcome = Extract<SdkPromptTerminalOutcome, {
48
48
  kind: "failed";
49
- }>;
49
+ }> & {
50
+ deliveryFailure?: {
51
+ cause: string;
52
+ frameBytes: number;
53
+ };
54
+ };
50
55
  /**
51
56
  * A prompt rejection that still carries the terminal's structured failure classification.
52
57
  * The ACP client sees only the code and message, but the first-turn retry gate must decide on
@@ -4,7 +4,7 @@ import type { SessionWorkLease } from "../../session/session-work-lease";
4
4
  import type { SessionLifecycleMcpServer } from "../acp/mcp";
5
5
  import { type SdkStartupFailure, type SdkStartupRollbackResult } from "../startup-capability";
6
6
  import type { Broker, BrokerCleanupEvidence, BrokerResponse } from "./broker";
7
- import type { LifecycleDurableEffectsReceipt, LifecycleStartupFailureReceipt } from "./lifecycle-ledger";
7
+ import type { LifecycleDurableEffectsReceipt, LifecycleEffectIntent, LifecycleStartupFailureReceipt } from "./lifecycle-ledger";
8
8
  import { type ProcessIncarnationCommandRunner, type ProcessIncarnationOptions, parseDarwinProcessIncarnation, processIncarnation } from "./process-incarnation";
9
9
  import { type SdkInternalSpawnCommand } from "./runtime";
10
10
  import { type IndexedSession } from "./session-index";
@@ -52,6 +52,10 @@ export declare function setLifecycleHostPlatformForTest(platform: NodeJS.Platfor
52
52
  export declare function readyThenExitToleranceEnabledForTest(): boolean;
53
53
  export declare function hasValidLifecycleDeadlines(value: LifecycleDeadlines, now?: number): boolean;
54
54
  type Input = Record<string, unknown>;
55
+ type LifecycleEffectIntentWithDeadline = LifecycleEffectIntent & {
56
+ lifecycleCleanupDeadlineAt?: number;
57
+ admissionCleanupDeadlineAt?: number;
58
+ };
55
59
  export declare const isCanonicalSessionId: (value: string) => boolean;
56
60
  export interface SessionLifecycleWorktreeTarget {
57
61
  enabled: true;
@@ -260,6 +264,7 @@ export interface LifecycleExecutionOutcome {
260
264
  startupFailure?: LifecycleStartupFailureReceipt;
261
265
  deferredArtifactCleanup?: () => Promise<void>;
262
266
  }
267
+ export declare function classifyLateAdmissionSpawnFailure(response: BrokerResponse, effectIntent: LifecycleEffectIntentWithDeadline | undefined, now: number, messageSource: BrokerResponse): BrokerResponse;
263
268
  /** Returns the response together with every durable lifecycle fact needed for truthful replay. */
264
269
  export declare function executeLifecycle(broker: Broker, operation: string, input: Input, identity: string, cleanup?: CleanupEvidence): Promise<LifecycleExecutionOutcome>;
265
270
  /**
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Bounded exponential backoff tracker for broker recovery failures per agent dir.
3
+ * Tracks repeated recovery failures and implements exponential backoff with a cap
4
+ * to prevent repeated spawns from destabilizing the system.
5
+ */
6
+ /** Configuration for recovery backoff behavior. */
7
+ export interface RecoveryBackoffConfig {
8
+ /** Initial backoff delay in milliseconds (default: 1000ms). */
9
+ initialDelayMs: number;
10
+ /** Maximum backoff delay in milliseconds (default: 30000ms = 30s). */
11
+ maxDelayMs: number;
12
+ /** Backoff multiplier applied each retry (default: 2). */
13
+ multiplier: number;
14
+ /** Maximum number of recovery attempts per session (default: 5). */
15
+ maxAttempts: number;
16
+ }
17
+ /** State for a single agent dir's recovery backoff. */
18
+ export interface RecoveryBackoffState {
19
+ /** Timestamp of the last recovery failure. */
20
+ lastFailureAt: number;
21
+ /** Current backoff delay in milliseconds. */
22
+ currentDelayMs: number;
23
+ /** Number of consecutive failures. */
24
+ failureCount: number;
25
+ /** Whether recovery has been permanently stopped due to max attempts. */
26
+ stopped: boolean;
27
+ }
28
+ /** Manages per-agent-dir recovery backoff state. */
29
+ export declare class RecoveryBackoffTracker {
30
+ #private;
31
+ constructor(config?: Partial<RecoveryBackoffConfig>);
32
+ /** Test hook: inject a custom clock. */
33
+ setClockForTest(clock: {
34
+ now(): number;
35
+ }): void;
36
+ /**
37
+ * Check if recovery should proceed now, considering backoff.
38
+ * Returns true if sufficient time has elapsed since the last failure.
39
+ */
40
+ canAttemptRecovery(agentDir: string): boolean;
41
+ /**
42
+ * Record a recovery failure and update backoff state.
43
+ * Returns true if recovery should continue to be attempted, false if max attempts reached.
44
+ */
45
+ recordFailure(agentDir: string): boolean;
46
+ /**
47
+ * Record a successful recovery and reset backoff state.
48
+ */
49
+ recordSuccess(agentDir: string): void;
50
+ /**
51
+ * Get the current backoff state for an agent dir (for testing/diagnostics).
52
+ */
53
+ getState(agentDir: string): RecoveryBackoffState | undefined;
54
+ /**
55
+ * Get milliseconds until the next recovery attempt can proceed.
56
+ */
57
+ getBackoffWaitMs(agentDir: string): number;
58
+ /**
59
+ * Reset all backoff state (for testing).
60
+ */
61
+ resetForTest(): void;
62
+ }
@@ -40,6 +40,14 @@ export interface SdkInternalRuntimeDescriptorTestOptions {
40
40
  * must not hide behind it.
41
41
  */
42
42
  export declare function sdkInternalRuntimeImage(): string | undefined;
43
+ /** Identity of a runtime image: enough to detect replacement at the same path. */
44
+ export interface SdkInternalRuntimeImageIdentity {
45
+ path: string;
46
+ dev: number;
47
+ ino: number;
48
+ mtimeMs: number;
49
+ size: number;
50
+ }
43
51
  /**
44
52
  * Whether a published runtime image is still a regular file on disk. A resident broker keeps
45
53
  * answering requests after the executable it was started from is deleted (a
@@ -50,6 +58,18 @@ export declare function sdkInternalRuntimeImage(): string | undefined;
50
58
  * its bound: a false retirement kills a healthy broker.
51
59
  */
52
60
  export declare function isSdkInternalRuntimeImagePresent(file: string): Promise<boolean>;
61
+ /**
62
+ * Capture the runtime image identity (dev, ino, mtimeMs, size) at startup.
63
+ * Used to detect if the binary at the same path has been replaced.
64
+ */
65
+ export declare function captureRuntimeImageIdentity(file: string): Promise<SdkInternalRuntimeImageIdentity | undefined>;
66
+ /**
67
+ * Check if the runtime image has been replaced since startup.
68
+ * Returns `true` if the path is gone, its dev/ino differ from the startup identity,
69
+ * or its size or modification time changed (an in-place rewrite keeps the inode).
70
+ * Returns `false` (inconclusive) if the file exists and matches, or on any error.
71
+ */
72
+ export declare function isSdkInternalRuntimeImageReplaced(startupIdentity: SdkInternalRuntimeImageIdentity | undefined): Promise<boolean>;
53
73
  /** Resolve the production descriptor from the statically imported marker and current Bun runtime evidence. */
54
74
  export declare function resolveSdkInternalSpawnCommand(action: SdkInternalAction): SdkInternalSpawnCommand;
55
75
  /** Test hook: injects runtime evidence without weakening the production marker authority. */
@@ -8,6 +8,8 @@ export declare const MIN_PREPARATION_TIMEOUT_MS = 1000;
8
8
  export declare const MAX_PREPARATION_TIMEOUT_MS = 120000;
9
9
  /** Bounded workspace install window; independent of child semantic readiness. */
10
10
  export declare const DEFAULT_DEPENDENCY_PREPARATION_TIMEOUT_MS = 30000;
11
+ /** Non-worktree broker pre-spawn cap, also limited by the admission window remaining since receipt. */
12
+ export declare const DEFAULT_BROKER_PRESPAWN_PREPARATION_TIMEOUT_MS = 30000;
11
13
  export declare function isValidReadinessTimeoutMs(value: unknown): value is number;
12
14
  export declare function isValidPreparationTimeoutMs(value: unknown): value is number;
13
15
  export declare const READINESS_TIMEOUT_INVALID_MESSAGE = "readinessTimeoutMs must be an integer between 4000 and 60000.";
@@ -51,7 +53,9 @@ export declare function lifecycleStartupBudgetMs(requestedReadinessTimeoutMs: nu
51
53
  *
52
54
  * Worktree launches add independent preparation budgets so git add / install cannot
53
55
  * cut the caller while the broker still owns the request. One-arg
54
- * {@link lifecycleStartupBudgetMs} stays queue+readiness for no-worktree callers.
56
+ * {@link lifecycleStartupBudgetMs} stays queue+readiness for no-worktree callers:
57
+ * broker-derived pre-spawn work uses only the unspent admission window from receipt,
58
+ * and the fresh readiness window ends within that unchanged caller budget.
55
59
  */
56
60
  export declare function lifecycleRequestTimeoutMs(operation: string, input: Record<string, unknown>): number | undefined;
57
61
  /**
@@ -602,6 +602,7 @@ export declare function shouldAwaitNotificationStartup(event: {
602
602
  origin: string;
603
603
  };
604
604
  }): boolean;
605
+ export declare function boundedCorrelatedAgentEndFrame(frame: Record<string, unknown>): Record<string, unknown>;
605
606
  export declare function createNotificationsExtension(api: ExtensionAPI, options?: {
606
607
  settings?: Settings;
607
608
  ensureTelegramDaemon?: (input: {
@@ -292,7 +292,7 @@ export declare const SDK_LIFECYCLE_ROUTER_PROTOCOL_VERSION = 1;
292
292
  * Generation 193 gates streamed content on negotiated observer capabilities,
293
293
  * so existing owners are replaced before exposing the new observer path.
294
294
  */
295
- export declare const DAEMON_GENERATION = 197;
295
+ export declare const DAEMON_GENERATION = 199;
296
296
  /**
297
297
  * Serving-compatibility boundary for daemon lifecycle requests. Epoch 7
298
298
  * requires durable publication receipts and acknowledged attachment dispatch,
@@ -3,6 +3,7 @@ import { type Settings } from "../../config/settings";
3
3
  import type { ExtensionAPI, ExtensionContext } from "../../extensibility/extensions";
4
4
  import type { AgentEndEvent } from "../../extensibility/shared-events";
5
5
  import { ensureBroker } from "../broker/ensure";
6
+ import { captureRuntimeImageIdentity } from "../broker/runtime";
6
7
  import { type MasterRoleAttestationV2, SessionIndex, type SessionLocatorV2 } from "../broker/session-index";
7
8
  import type { SdkPromptTerminalOutcome } from "../prompt-status";
8
9
  import { type ReceiptState } from "../receipt-state";
@@ -286,6 +287,12 @@ export interface CreateSdkSessionRuntimeOptions {
286
287
  onFailureDiagnosticKeyCountForTests?: (count: number) => void;
287
288
  /** Test-only observation after a resolved invocation completion is reconciled. */
288
289
  onInvocationCompletionReconciledForTests?: (kind: InvocationKind, correlation: InvocationCorrelation) => void;
290
+ /** Test hook: provides a function to inject a controlled clock for broker recovery backoff assertions. */
291
+ setRecoveryBackoffClockForTest?: (inject: (clock: {
292
+ now(): number;
293
+ }) => void) => void;
294
+ /** Test hook: inject mock runtime image identity and replacement detection for broker recovery tests. */
295
+ setRuntimeImageIdentityForTest?: (register: (captureFn: () => Awaited<ReturnType<typeof captureRuntimeImageIdentity>> | undefined, isReplacedFn: () => Promise<boolean>) => void) => void;
289
296
  }
290
297
  export interface InvocationCorrelation {
291
298
  commandId: string;
@@ -65,7 +65,7 @@ export declare const ACP_PROMPT_INFERENCE_TIMEOUT_MS: number;
65
65
  export declare class PromptActivity {
66
66
  #private;
67
67
  /** Folds one inbound frame's lifecycle event into the in-flight tool and model state. */
68
- observe(eventType: string | undefined, toolCallId: string | undefined, messageRole: string | undefined): void;
68
+ observe(eventType: string | undefined, toolCallId: string | undefined, messageRole: string | undefined, messageHasToolCall?: boolean): void;
69
69
  /** True while at least one started tool call has not reported an end. */
70
70
  get running(): boolean;
71
71
  /** True while a dispatched model call has not produced the first frame of its response. */
@@ -10,6 +10,7 @@ import type { ToolSession } from ".";
10
10
  import { type OutputMeta } from "./output-meta";
11
11
  export { STEER_FOLD_GRACE_MS, steerFoldReasonLine } from "./steer-fold";
12
12
  export declare const BASH_DEFAULT_PREVIEW_LINES = 10;
13
+ export declare const MANAGED_OWNER_BASH_ENV: readonly ["GJC_TMUX_OWNER_STATE_DIR", "GJC_TMUX_OWNER_GENERATION", "GJC_MANAGED_OWNER_RUN_ID", "GJC_MANAGED_OWNER_INCARNATION", "GJC_MANAGED_OWNER_CHILD_TOKEN", "GJC_MANAGED_OWNER_PREDECESSOR_TOKEN", "GJC_MANAGED_OWNER_PREDECESSOR_GENERATION", "GJC_MANAGED_OWNER_PREDECESSOR_RUN_ID", "GJC_MANAGED_OWNER_PREDECESSOR_INCARNATION", "GJC_MANAGED_OWNER_TRANSCRIPT_PATH", "GJC_TMUX_OWNER_SERVER_KEY", "GJC_TMUX_LAUNCHED"];
13
14
  export type BashOriginalArtifactSaveResult = BashArtifactSaveResult;
14
15
  /**
15
16
  * Lift a leading `cd <dir> &&` into the tool cwd only when `<dir>` is exactly
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/coding-agent",
4
- "version": "0.18.1",
4
+ "version": "0.18.5",
5
5
  "description": "Gajae Code CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://gajae-code.com",
7
7
  "author": "Yeachan-Heo and Gajae Code Contributors",
@@ -57,12 +57,12 @@
57
57
  "@agentclientprotocol/sdk": "1.3.0",
58
58
  "@babel/parser": "^7.29.3",
59
59
  "@mozilla/readability": "^0.6.0",
60
- "@gajae-code/stats": "0.18.1",
61
- "@gajae-code/agent-core": "0.18.1",
62
- "@gajae-code/ai": "0.18.1",
63
- "@gajae-code/natives": "0.18.1",
64
- "@gajae-code/tui": "0.18.1",
65
- "@gajae-code/utils": "0.18.1",
60
+ "@gajae-code/stats": "0.18.5",
61
+ "@gajae-code/agent-core": "0.18.5",
62
+ "@gajae-code/ai": "0.18.5",
63
+ "@gajae-code/natives": "0.18.5",
64
+ "@gajae-code/tui": "0.18.5",
65
+ "@gajae-code/utils": "0.18.5",
66
66
  "@puppeteer/browsers": "^2.13.0",
67
67
  "@types/turndown": "5.0.6",
68
68
  "@xterm/headless": "^6.0.0",
@@ -70,41 +70,6 @@ export interface StatsCommandArgs {
70
70
  summary: boolean;
71
71
  }
72
72
 
73
- // =============================================================================
74
- // Argument Parser
75
- // =============================================================================
76
-
77
- /**
78
- * Parse stats subcommand arguments.
79
- * Returns undefined if not a stats command.
80
- */
81
- export function parseStatsArgs(args: string[]): StatsCommandArgs | undefined {
82
- if (args.length === 0 || args[0] !== "stats") {
83
- return undefined;
84
- }
85
-
86
- const result: StatsCommandArgs = {
87
- port: 3847,
88
- json: false,
89
- summary: false,
90
- };
91
-
92
- for (let i = 1; i < args.length; i++) {
93
- const arg = args[i];
94
- if (arg === "--json" || arg === "-j") {
95
- result.json = true;
96
- } else if (arg === "--summary" || arg === "-s") {
97
- result.summary = true;
98
- } else if ((arg === "--port" || arg === "-p") && i + 1 < args.length) {
99
- result.port = parseInt(args[++i], 10);
100
- } else if (arg.startsWith("--port=")) {
101
- result.port = parseInt(arg.split("=")[1], 10);
102
- }
103
- }
104
-
105
- return result;
106
- }
107
-
108
73
  function formatCost(n: number): string {
109
74
  if (n < 0.01) return `$${n.toFixed(4)}`;
110
75
  if (n < 1) return `$${n.toFixed(3)}`;
@@ -131,7 +96,10 @@ export async function runStatsCommand(cmd: StatsCommandArgs): Promise<void> {
131
96
  const { processed, files } = await syncAllSessions({ onProgress: progress.onProgress });
132
97
  progress.finish();
133
98
  const total = await getTotalMessageCount();
134
- console.log(`Synced ${processed} new entries from ${files} files (${total} total)\n`);
99
+ const synced = `Synced ${processed} new entries from ${files} files (${total} total)\n`;
100
+ // `--json` stdout must be exactly the JSON document so it can be piped into a parser.
101
+ if (cmd.json) process.stderr.write(`${synced}\n`);
102
+ else console.log(synced);
135
103
 
136
104
  if (cmd.json) {
137
105
  const stats = await getDashboardStats();
package/src/cli-main.ts CHANGED
@@ -95,14 +95,10 @@ async function showHelp(config: CliConfig): Promise<void> {
95
95
  }
96
96
 
97
97
  async function installRuntimeGlobals(): Promise<void> {
98
- const { installH2Fetch } = await import("@gajae-code/ai/utils/h2-fetch");
99
- // Activate HTTP/2 for all `fetch()` calls (provider streams, OAuth, model
100
- // discovery, web tools). Bun's HTTP/2 client is gated on a startup flag we
101
- // can't toggle from JS, so we patch globalThis.fetch to pass
102
- // `protocol: "http2"` per request, with transparent HTTP/1.1 fallback on
103
- // `HTTP2Unsupported`. See @gajae-code/ai/utils/h2-fetch for details.
104
- installH2Fetch();
105
-
98
+ // fetch() deliberately stays on HTTP/1.1. Bun's HTTP/2 client (per-request
99
+ // `protocol: "http2"`) wedges a pooled connection while uploading large
100
+ // request bodies (~300KB): every later request on it waits forever for
101
+ // response headers. Do not re-enable HTTP/2 without re-running that probe.
106
102
  const { warnIfMacOSNoFileLimitTooLow } = await import("./cli/nofile-limit");
107
103
  warnIfMacOSNoFileLimitTooLow();
108
104
 
package/src/cli.ts CHANGED
@@ -37,14 +37,10 @@ export const commands: CommandEntry[] = [
37
37
  ];
38
38
 
39
39
  async function installRuntimeGlobals(writeNoFileWarning?: (text: string) => void): Promise<void> {
40
- const { installH2Fetch } = await import("@gajae-code/ai/utils/h2-fetch");
41
- // Activate HTTP/2 for all `fetch()` calls (provider streams, OAuth, model
42
- // discovery, web tools). Bun's HTTP/2 client is gated on a startup flag we
43
- // can't toggle from JS, so we patch globalThis.fetch to pass
44
- // `protocol: "http2"` per request, with transparent HTTP/1.1 fallback on
45
- // `HTTP2Unsupported`. See @gajae-code/ai/utils/h2-fetch for details.
46
- installH2Fetch();
47
-
40
+ // fetch() deliberately stays on HTTP/1.1. Bun's HTTP/2 client (per-request
41
+ // `protocol: "http2"`) wedges a pooled connection while uploading large
42
+ // request bodies (~300KB): every later request on it waits forever for
43
+ // response headers. Do not re-enable HTTP/2 without re-running that probe.
48
44
  const { warnIfMacOSNoFileLimitTooLow } = await import("./cli/nofile-limit");
49
45
  warnIfMacOSNoFileLimitTooLow({ writeStderr: writeNoFileWarning });
50
46
 
@@ -32,8 +32,8 @@ export default class Commit extends Command {
32
32
  };
33
33
 
34
34
  await initTheme();
35
- // The agentic commit flow opens HTTP/2 keep-alive sockets to the model
36
- // provider (via `installH2Fetch`) and spins up an AgentSession with
35
+ // The agentic commit flow opens keep-alive sockets to the model
36
+ // provider and spins up an AgentSession with
37
37
  // background async-job + extension machinery. `session.dispose()` releases
38
38
  // what it can, but Bun's fetch keeps idle connections warm and a few
39
39
  // timers (Settings autosave, OAuth refresh) stay armed long enough to
@@ -642,8 +642,7 @@ export async function runSessionHost(
642
642
  },
643
643
  absent.result,
644
644
  );
645
-
646
- throw new Error("SDK startup did not complete before readiness cutoff.");
645
+ return;
647
646
  }
648
647
 
649
648
  const rollback = new SdkStartupRollbackTracker();