@gajae-code/coding-agent 0.13.0 → 0.13.2

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 (119) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/types/config/file-lock.d.ts +2 -0
  3. package/dist/types/config/keybindings.d.ts +2 -2
  4. package/dist/types/config/model-registry.d.ts +4 -0
  5. package/dist/types/config/models-config-schema.d.ts +6 -0
  6. package/dist/types/config/settings-schema.d.ts +17 -5
  7. package/dist/types/config/settings.d.ts +10 -1
  8. package/dist/types/edit/streaming.d.ts +5 -5
  9. package/dist/types/eval/py/kernel.d.ts +2 -2
  10. package/dist/types/eval/py/runtime.d.ts +5 -1
  11. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +3 -0
  12. package/dist/types/gjc-runtime/tmux-sessions.d.ts +3 -1
  13. package/dist/types/modes/components/model-selector.d.ts +3 -2
  14. package/dist/types/runtime/process-lifecycle.d.ts +12 -31
  15. package/dist/types/sdk/broker/identity.d.ts +2 -1
  16. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +17 -1
  17. package/dist/types/sdk/broker/lifecycle.d.ts +11 -6
  18. package/dist/types/sdk/broker/startup-failure.d.ts +32 -0
  19. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
  20. package/dist/types/sdk/bus/conversation-store.d.ts +1 -0
  21. package/dist/types/sdk/bus/index.d.ts +4 -0
  22. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +10 -1
  23. package/dist/types/sdk/bus/reconciliation-store.d.ts +2 -0
  24. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +13 -2
  25. package/dist/types/sdk/bus/telegram-daemon.d.ts +17 -1
  26. package/dist/types/sdk/host/query/cursor.d.ts +4 -0
  27. package/dist/types/sdk/host/query/handlers.d.ts +6 -0
  28. package/dist/types/sdk/host/session-runtime.d.ts +7 -6
  29. package/dist/types/sdk/host/websocket-transport.d.ts +1 -1
  30. package/dist/types/sdk/index.d.ts +1 -0
  31. package/dist/types/sdk/turn-result.d.ts +37 -0
  32. package/dist/types/session/agent-session.d.ts +8 -11
  33. package/dist/types/session/blob-store.d.ts +6 -3
  34. package/dist/types/session/internal/managed-session-storage.d.ts +13 -0
  35. package/dist/types/slash-commands/helpers/fast-status-report.d.ts +12 -3
  36. package/dist/types/task/executor.d.ts +4 -7
  37. package/dist/types/tools/index.d.ts +1 -1
  38. package/dist/types/tools/read.d.ts +1 -1
  39. package/dist/types/tools/search.d.ts +1 -1
  40. package/dist/types/utils/edit-mode.d.ts +69 -0
  41. package/package.json +8 -8
  42. package/scripts/generate-hotkeys-docs.ts +8 -6
  43. package/src/commands/sdk.ts +23 -2
  44. package/src/config/file-lock.ts +25 -5
  45. package/src/config/keybindings.ts +4 -2
  46. package/src/config/model-registry.ts +20 -0
  47. package/src/config/models-config-schema.ts +1 -0
  48. package/src/config/settings-schema.ts +29 -22
  49. package/src/config/settings.ts +42 -10
  50. package/src/coordinator-mcp/server.ts +87 -106
  51. package/src/edit/index.ts +5 -19
  52. package/src/edit/renderer.ts +3 -2
  53. package/src/edit/streaming.ts +28 -15
  54. package/src/eval/py/executor.ts +108 -10
  55. package/src/eval/py/kernel.ts +86 -12
  56. package/src/eval/py/runtime.ts +200 -16
  57. package/src/exec/bash-executor.ts +36 -37
  58. package/src/gjc-runtime/gc-runtime.ts +25 -3
  59. package/src/gjc-runtime/managed-owner-supervisor.ts +27 -1
  60. package/src/gjc-runtime/session-state-sidecar.ts +110 -4
  61. package/src/gjc-runtime/tmux-sessions.ts +34 -5
  62. package/src/internal-urls/docs-index.generated.ts +9 -9
  63. package/src/modes/acp/acp-agent.ts +10 -2
  64. package/src/modes/components/model-selector.ts +28 -8
  65. package/src/modes/controllers/plan-mode-controller.ts +3 -3
  66. package/src/modes/controllers/selector-controller.ts +4 -2
  67. package/src/modes/interactive-mode.ts +4 -0
  68. package/src/runtime/process-lifecycle.ts +126 -27
  69. package/src/runtime-mcp/transports/stdio.ts +11 -6
  70. package/src/sdk/acp/adapter.ts +18 -10
  71. package/src/sdk/broker/broker.ts +55 -4
  72. package/src/sdk/broker/ensure.ts +10 -7
  73. package/src/sdk/broker/identity.ts +10 -0
  74. package/src/sdk/broker/lifecycle-ledger.ts +56 -6
  75. package/src/sdk/broker/lifecycle.ts +53 -10
  76. package/src/sdk/broker/process-incarnation.ts +6 -1
  77. package/src/sdk/broker/session-index.ts +28 -11
  78. package/src/sdk/broker/startup-failure.ts +101 -0
  79. package/src/sdk/broker/transport.ts +1 -0
  80. package/src/sdk/bus/chat-daemon-control.ts +4 -3
  81. package/src/sdk/bus/chat-daemon-runtime.ts +8 -1
  82. package/src/sdk/bus/conversation-store.ts +171 -50
  83. package/src/sdk/bus/index.ts +101 -47
  84. package/src/sdk/bus/kind-aware-reconciliation.ts +52 -2
  85. package/src/sdk/bus/notification-service.ts +3 -3
  86. package/src/sdk/bus/reconciliation-store.ts +16 -0
  87. package/src/sdk/bus/telegram-daemon-contract.ts +13 -2
  88. package/src/sdk/bus/telegram-daemon.ts +151 -41
  89. package/src/sdk/bus/topic-registry.ts +38 -9
  90. package/src/sdk/cli/session-cli.ts +20 -7
  91. package/src/sdk/client/discovery.ts +23 -2
  92. package/src/sdk/host/query/cursor.ts +4 -0
  93. package/src/sdk/host/query/handlers.ts +99 -100
  94. package/src/sdk/host/sdk-surface-parity.test.ts +1 -1
  95. package/src/sdk/host/session-runtime.test.ts +98 -1
  96. package/src/sdk/host/session-runtime.ts +57 -15
  97. package/src/sdk/host/websocket-transport.lifecycle.test.ts +192 -2
  98. package/src/sdk/host/websocket-transport.ts +54 -30
  99. package/src/sdk/index.ts +1 -0
  100. package/src/sdk/mcp/server.ts +19 -6
  101. package/src/sdk/protocol/operation-inventory.generated.json +1 -1
  102. package/src/sdk/protocol/operation-registry.ts +8 -12
  103. package/src/sdk/session.ts +5 -14
  104. package/src/sdk/turn-result.ts +53 -0
  105. package/src/session/agent-session.ts +112 -69
  106. package/src/session/blob-store.ts +91 -21
  107. package/src/session/internal/managed-session-storage.ts +136 -21
  108. package/src/session/session-manager.ts +408 -81
  109. package/src/session/session-storage.ts +68 -10
  110. package/src/slash-commands/builtin-registry.ts +43 -5
  111. package/src/slash-commands/helpers/fast-status-report.ts +24 -8
  112. package/src/task/executor.ts +25 -8
  113. package/src/task/index.ts +5 -4
  114. package/src/tools/bash.ts +11 -9
  115. package/src/tools/index.ts +1 -1
  116. package/src/tools/read.ts +12 -11
  117. package/src/tools/search.ts +7 -7
  118. package/src/utils/edit-mode.ts +180 -9
  119. package/src/web/search/providers/insane.ts +55 -1
@@ -3,11 +3,13 @@
3
3
  * Preserves Q26 admit/first-terminal/capacity/TTL semantics; indexes and caps are per-kind.
4
4
  */
5
5
  import type { PromptReconciliationStatus, SdkPromptTerminalOutcome, TurnPromptReconciliation } from "../prompt-status";
6
+ import { type TurnResultContent, type TurnResultPage } from "../turn-result";
6
7
  import { type PromptCorrelation } from "./prompt-reconciliation";
7
8
  import type { ReconciliationKind, ReconciliationStore } from "./reconciliation-store";
8
9
  export type { ReconciliationKind };
9
10
  export interface KindCorrelation extends PromptCorrelation {
10
11
  kind: ReconciliationKind;
12
+ content?: TurnResultContent;
11
13
  }
12
14
  export interface KindAwareReconciliation {
13
15
  admit(kind: ReconciliationKind, clientRef?: string): void;
@@ -17,21 +19,28 @@ export interface KindAwareReconciliation {
17
19
  }): Promise<void>;
18
20
  noteTransition(kind: ReconciliationKind, correlation: PromptCorrelation | undefined, frame: {
19
21
  type: "agent_start" | "agent_end";
22
+ content?: TurnResultContent;
20
23
  } | {
21
24
  type: "agent_failed";
22
25
  error: unknown;
26
+ content?: TurnResultContent;
23
27
  }): Promise<void>;
24
28
  claimPendingOutcome(correlation: PromptCorrelation, outcome: SdkPromptTerminalOutcome): Promise<SdkPromptTerminalOutcome>;
25
29
  finalizePromptOutcome(correlation: PromptCorrelation, outcome?: SdkPromptTerminalOutcome, recordError?: {
26
30
  code: string;
27
31
  message: string;
28
- }): Promise<void>;
32
+ }, content?: unknown): Promise<void>;
29
33
  peekPendingOutcome(correlation: PromptCorrelation): SdkPromptTerminalOutcome | undefined;
30
34
  lookup(kind: ReconciliationKind, selector: {
31
35
  commandId?: string;
32
36
  turnId?: string;
33
37
  clientRef?: string;
34
38
  }): TurnPromptReconciliation;
39
+ lookupResult(kind: ReconciliationKind, selector: {
40
+ commandId?: string;
41
+ turnId?: string;
42
+ clientRef?: string;
43
+ }): TurnResultPage;
35
44
  cleanup(): void;
36
45
  activeCount(kind: ReconciliationKind): number;
37
46
  /** Hydrate from durable store (call once at session host start). */
@@ -1,4 +1,5 @@
1
1
  import type { PromptReconciliationStatus, SdkPromptTerminalOutcome } from "../prompt-status";
2
+ import { type TurnResultContent } from "../turn-result";
2
3
  import type { PromptCorrelation } from "./prompt-reconciliation";
3
4
  export declare const RECONCILIATION_STORE_VERSION = 1;
4
5
  export declare const RECONCILIATION_SESSION_ID_PATTERN: RegExp;
@@ -19,6 +20,7 @@ export interface DurableReconciliationRecord extends PromptCorrelation {
19
20
  pendingOutcome?: SdkPromptTerminalOutcome;
20
21
  /** Skill-only safe token; never skill args bodies. */
21
22
  skillName?: string;
23
+ content?: TurnResultContent;
22
24
  }
23
25
  export interface ReconciliationStoreDocument {
24
26
  version: typeof RECONCILIATION_STORE_VERSION;
@@ -93,9 +93,20 @@ export declare const NOTIFICATION_PROTOCOL_VERSION = 3;
93
93
  * the exact grace deadline when a failed archive publication rolls back.
94
94
  * Generation 59 publishes the owner's attached OPEN-socket count in the
95
95
  * heartbeat sidecar so `gjc notify health` can report a live daemon that is
96
- * attached to no registered endpoint (#4128).
96
+ * attached to no registered endpoint (#4128). Generation 60 contains transient
97
+ * heartbeat-sidecar publication failures: a Windows EPERM/EACCES/EBUSY on the
98
+ * sidecar rename is retried bounded under the ownership-lock fence and an
99
+ * exhausted publication keeps the daemon alive as `publish_failed` instead of
100
+ * escaping as an uncaught exception, while only a proven state/lock mismatch
101
+ * (`not_owner`) stops the owner (#4200). Generation 61 keeps authenticated
102
+ * same-session transports attached while predecessor archival converges and
103
+ * settles non-forum close responses. Generation 62 rejects replay-gap
104
+ * authority claims that do not match the requested cursor, answer bounds,
105
+ * and retained replay suffix.
106
+ * Generation 63 contains transient steady-heartbeat state and ownership-lock
107
+ * read failures and cleans every staged sidecar before retrying (#4200).
97
108
  */
98
- export declare const DAEMON_GENERATION = 59;
109
+ export declare const DAEMON_GENERATION = 63;
99
110
  /**
100
111
  * Serving-compatibility boundary for daemon lifecycle requests. Epoch 5
101
112
  * requires the complete generation-36 topic authority contract, so older
@@ -296,6 +296,20 @@ export declare function readOwnerFreshnessSnapshot(input: {
296
296
  settings: Settings;
297
297
  fs?: TelegramDaemonFs;
298
298
  }): Promise<OwnerFreshnessSnapshot>;
299
+ /** Outcome of a steady heartbeat sidecar renewal. */
300
+ export type OwnerHeartbeatSidecarRenewal =
301
+ /** The sidecar was published under a still-matching ownership lock. */
302
+ "renewed"
303
+ /** This process no longer matches the persisted state/lock; stop renewing. */
304
+ | "not_owner"
305
+ /**
306
+ * Ownership still held, but the sidecar publication failed (e.g. a transient
307
+ * Windows EPERM/EBUSY while another process holds the destination). The
308
+ * daemon must stay alive and publish again on the next cycle (#4200); a
309
+ * persistently failing publication eventually stales the heartbeat for
310
+ * outside observers, whose takeover then surfaces here as `not_owner`.
311
+ */
312
+ | "publish_failed";
299
313
  /** Marker-free steady heartbeat renewal. The final lock reread fences a stale writer. */
300
314
  export declare function renewOwnerHeartbeatSidecar(input: {
301
315
  settings: Settings;
@@ -311,7 +325,7 @@ export declare function renewOwnerHeartbeatSidecar(input: {
311
325
  * rather than falsely claiming zero attachments.
312
326
  */
313
327
  attachedEndpoints?: number;
314
- }): Promise<boolean>;
328
+ }): Promise<OwnerHeartbeatSidecarRenewal>;
315
329
  export interface NotificationRootRegistration {
316
330
  root?: string;
317
331
  managed?: boolean;
@@ -946,6 +960,8 @@ interface SessionSocket {
946
960
  pingTimer: ReturnType<typeof setInterval> | undefined;
947
961
  /** Correlation id for the startup replay barrier. */
948
962
  replayId: string;
963
+ /** Sequence cursor sent by the current startup replay request. */
964
+ replaySinceSeq: number;
949
965
  /** Queues live frames until startup replay is applied. */
950
966
  replayPending: boolean;
951
967
  replayQueue: Record<string, unknown>[];
@@ -28,6 +28,10 @@ export interface CursorSelector {
28
28
  resourceKind?: string;
29
29
  resourceId?: string;
30
30
  itemId?: string;
31
+ kind?: string;
32
+ clientRef?: string;
33
+ commandId?: string;
34
+ turnId?: string;
31
35
  }
32
36
  export interface CursorPosition {
33
37
  offset?: number;
@@ -44,6 +44,12 @@ export interface SessionSurface {
44
44
  turnId?: string;
45
45
  clientRef?: string;
46
46
  }): unknown | Promise<unknown>;
47
+ getTurnResult?(selector: {
48
+ kind: "prompt" | "skill";
49
+ clientRef?: string;
50
+ commandId?: string;
51
+ turnId?: string;
52
+ }): unknown | Promise<unknown>;
47
53
  /** Q27 effective model-profile catalog from the live session registry. */
48
54
  getModelProfiles?(): unknown[] | Promise<unknown[]>;
49
55
  /**
@@ -92,6 +92,11 @@ export interface InvocationReconciliation {
92
92
  turnId?: string;
93
93
  clientRef?: string;
94
94
  }): unknown;
95
+ lookupResult(kind: InvocationKind, selector: {
96
+ commandId?: string;
97
+ turnId?: string;
98
+ clientRef?: string;
99
+ }): unknown;
95
100
  hydrate(): Promise<void>;
96
101
  }
97
102
  export declare function createInvocationReconciliation(options?: {
@@ -112,12 +117,8 @@ export interface SdkSurfaceFactoryOptions {
112
117
  configOverrides?: ReadonlyMap<string, unknown>;
113
118
  /** Session settings; used for model-usage preferences in profile-limit resolution. */
114
119
  settings?: Settings;
115
- promptStatusLookup?: (selector: {
116
- commandId?: string;
117
- turnId?: string;
118
- clientRef?: string;
119
- }) => unknown;
120
- skillStatusLookup?: (selector: {
120
+ turnResultLookup?: (selector: {
121
+ kind: "prompt" | "skill";
121
122
  commandId?: string;
122
123
  turnId?: string;
123
124
  clientRef?: string;
@@ -1,7 +1,7 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import type { SessionSdkTransport } from "./session-runtime";
3
3
  export interface SdkWebSocketTransportDependencies {
4
- readonly filesystem?: Pick<typeof fs, "mkdir" | "writeFile" | "chmod" | "rm">;
4
+ readonly filesystem?: Pick<typeof fs, "mkdir" | "writeFile" | "chmod" | "rename" | "readFile" | "rm">;
5
5
  readonly serve?: typeof Bun.serve;
6
6
  }
7
7
  export type SdkTransportLifecycleErrorCode = "endpoint_write_failed" | "endpoint_chmod_failed" | "endpoint_remove_failed" | "server_stop_failed";
@@ -8,3 +8,4 @@ export * from "./prompt-status";
8
8
  export type { ActiveProviderConnectionKind, ActiveProviderDescriptor } from "./providers";
9
9
  export * from "./session";
10
10
  export * from "./session-directory";
11
+ export * from "./turn-result";
@@ -0,0 +1,37 @@
1
+ /** Canonical durable turn-result DTOs. */
2
+ export declare const TURN_RESULT_VERSION: 1;
3
+ export declare const TURN_RESULT_CONTENT_MAX_BYTES = 16384;
4
+ export declare const TURN_RESULT_ERROR_MESSAGE_MAX_LENGTH = 512;
5
+ export type TurnResultSelector = {
6
+ kind: "prompt" | "skill";
7
+ clientRef: string;
8
+ } | {
9
+ kind: "prompt" | "skill";
10
+ commandId: string;
11
+ turnId: string;
12
+ };
13
+ export type TurnResultStatus = "unknown" | "accepted" | "in_flight" | "terminal_ok" | "failed";
14
+ export interface TurnResultContent {
15
+ version: typeof TURN_RESULT_VERSION;
16
+ type: "text";
17
+ text: string;
18
+ byteLength: number;
19
+ truncated: boolean;
20
+ }
21
+ export interface TurnResultError {
22
+ code: string;
23
+ message: string;
24
+ }
25
+ export interface TurnResultPage {
26
+ status: TurnResultStatus;
27
+ kind?: "prompt" | "skill";
28
+ commandId?: string;
29
+ turnId?: string;
30
+ clientRef?: string;
31
+ acceptedAt?: number;
32
+ startedAt?: number;
33
+ terminalAt?: number;
34
+ content?: TurnResultContent;
35
+ error?: TurnResultError;
36
+ }
37
+ export declare function sanitizeTurnResultContent(text: unknown): TurnResultContent | undefined;
@@ -629,7 +629,7 @@ export declare class AgentSession {
629
629
  /** Suspend provider state without closing it while a temporary model is active. */
630
630
  beginTemporaryProviderSessionScope(reason: TemporaryModelReason): TemporaryProviderSessionScope;
631
631
  /** Restore a temporary scope, unwinding any auto-owned scopes above it. */
632
- restoreTemporaryProviderSessionScope(token: TemporaryProviderSessionScope): boolean;
632
+ restoreTemporaryProviderSessionScope(token: TemporaryProviderSessionScope): Promise<boolean>;
633
633
  /** Promote a temporary scope. The suspended provider state is permanently closed. */
634
634
  commitTemporaryProviderSessionScope(token: TemporaryProviderSessionScope): boolean;
635
635
  buildForkContextSeed(options: ForkContextSeedOptions): Promise<ForkContextSeed>;
@@ -1251,19 +1251,16 @@ export declare class AgentSession {
1251
1251
  */
1252
1252
  isFastModeEnabled(): boolean;
1253
1253
  /**
1254
- * True when the configured `serviceTier` resolves to `"priority"` for the
1255
- * given model `provider`. Returns false for scoped tiers that don't match
1256
- * (e.g. `"openai-only"` on an anthropic provider) and when `provider` is
1257
- * undefined. This is the canonical provider-aware fast-mode predicate.
1254
+ * True when the configured tier is realized as a fast-mode field on the
1255
+ * provider's wire protocol. Providers that silently drop unscoped priority
1256
+ * intent return false so UI indicators match the request that is sent.
1258
1257
  */
1259
- isFastForProvider(provider?: string): boolean;
1258
+ isFastForProvider(provider?: string, supportsServiceTier?: boolean): boolean;
1260
1259
  /**
1261
- * Provider-aware fast-mode predicate for task-tool subagent roles, evaluated
1262
- * against the effective subagent tier (`task.serviceTier`) rather than the
1263
- * main session tier. Use this for `task.agentModelOverrides` role rows so the
1264
- * ⚡ glyph reflects the tier the subagent actually runs under.
1260
+ * Wire-effective fast-mode predicate for task-tool subagent roles, evaluated
1261
+ * against `task.serviceTier` rather than the main session tier.
1265
1262
  */
1266
- isFastForSubagentProvider(provider?: string): boolean;
1263
+ isFastForSubagentProvider(provider?: string, supportsServiceTier?: boolean): boolean;
1267
1264
  /**
1268
1265
  * True when the configured `serviceTier` resolves to `"priority"` for the
1269
1266
  * *currently selected model's provider* AND fast mode was not auto-disabled
@@ -27,7 +27,7 @@ export declare class ResidentCacheTrustError extends Error {
27
27
  readonly path: string;
28
28
  constructor(reason: string, path: string, options?: ErrorOptions);
29
29
  }
30
- export declare function disposeVerifiedResidentCacheInstanceDir(instanceDir: string): void;
30
+ export declare function disposeVerifiedResidentCacheInstanceDir(instanceDir: string, parentDescriptor?: number): void;
31
31
  /**
32
32
  * Best-effort cleanup of abandoned resident-cache instances. The root is
33
33
  * re-verified before scanning, work is bounded, and all failures fail closed.
@@ -84,6 +84,7 @@ export declare class BlobStore {
84
84
  }
85
85
  interface EphemeralBlobStoreOptions {
86
86
  readonly adoptVerifiedDir?: boolean;
87
+ readonly disposalParentDescriptor?: number;
87
88
  }
88
89
  /** Resident-cache directories still tracked by this process. Test seam. */
89
90
  export declare function trackedResidentCacheDirsForTest(): string[];
@@ -213,8 +214,10 @@ export interface CanonicalBlobEntry {
213
214
  readonly path: string;
214
215
  readonly bytes: number;
215
216
  readonly mtimeMs: number;
216
- readonly dev: number;
217
- readonly ino: number;
217
+ readonly mtimeNs: bigint;
218
+ readonly dev: bigint;
219
+ readonly ino: bigint;
220
+ readonly nlink: bigint;
218
221
  }
219
222
  /** Longest possible `blob:sha256:<hash>` reference, used to bound chunked scans. */
220
223
  export declare const BLOB_REFERENCE_MAX_LENGTH: number;
@@ -87,6 +87,10 @@ export interface ManagedBoundedAppendExpectation {
87
87
  readonly ctimeNs: string;
88
88
  readonly sha256: string;
89
89
  }
90
+ export interface ScrubbedProtocolRemnantReapResult {
91
+ readonly reaped: number;
92
+ readonly failures: number;
93
+ }
90
94
  /**
91
95
  * Best-effort removal of scrubbed write-protocol remnants from one managed
92
96
  * directory. Only zero-length, single-link, non-symlink regular files whose
@@ -98,6 +102,15 @@ export interface ManagedBoundedAppendExpectation {
98
102
  * have reconciled any candidate.
99
103
  */
100
104
  export declare function reapScrubbedProtocolRemnantsSync(directory: string, minAgeMs?: number): number;
105
+ /**
106
+ * Async twin of {@link reapScrubbedProtocolRemnantsSync} with the same safety
107
+ * filters (terminal remnant prefix, zero-length, single-link, non-symlink,
108
+ * older than the age gate), yielding between bounded batches. Long-lived
109
+ * processes reap per-session descendant directories through this path so a
110
+ * legacy oversized directory cannot starve timers or sibling subagents while
111
+ * it is being drained (issue #4394).
112
+ */
113
+ export declare function reapScrubbedProtocolRemnants(directory: string, minAgeMs?: number): Promise<ScrubbedProtocolRemnantReapResult>;
101
114
  export declare function validateNativeSecurityResult(value: unknown, operation: "apply" | "verify", kind: "directory" | "file"): NativeOwnerOnlySecurityResult;
102
115
  export interface ManagedLockRetirementTestEvent {
103
116
  readonly path: string;
@@ -1,4 +1,4 @@
1
- import type { Model } from "@gajae-code/ai/core";
1
+ import { type Model } from "@gajae-code/ai/core";
2
2
  /**
3
3
  * A single line in the `/fast status` report: a labelled model and whether fast
4
4
  * mode is effective for it. The `fast` flag is resolved by the caller
@@ -36,7 +36,7 @@ export declare function formatFastStatusReport(args: FormatFastStatusReportArgs)
36
36
  export interface FastStatusSessionLike {
37
37
  readonly model?: Model;
38
38
  /** Fast predicate against the main session tier (current model + `modelRoles`). */
39
- isFastForProvider(provider?: string): boolean;
39
+ isFastForProvider(provider?: string, supportsServiceTier?: boolean): boolean;
40
40
  /**
41
41
  * Current-model EFFECTIVE fast state (intent minus any provider auto-disable).
42
42
  * Used for the current-model row so it matches what the next request does.
@@ -44,10 +44,19 @@ export interface FastStatusSessionLike {
44
44
  */
45
45
  isFastModeActive?(): boolean;
46
46
  /** Fast predicate against the effective subagent tier (`task.agentModelOverrides` roles). */
47
- isFastForSubagentProvider(provider?: string): boolean;
47
+ isFastForSubagentProvider(provider?: string, supportsServiceTier?: boolean): boolean;
48
48
  resolveRoleModelWithThinking(role: string): {
49
49
  model?: Model;
50
50
  };
51
+ /** Runtime fallback position for the configured DEFAULT chain. */
52
+ getDefaultFallbackRuntimeState?(): {
53
+ chain: {
54
+ entries: readonly string[];
55
+ };
56
+ controller: {
57
+ activeIndex: number;
58
+ };
59
+ };
51
60
  }
52
61
  /** A role to enumerate in the report, with the tier source its subagent runs under. */
53
62
  export interface FastStatusRoleTarget {
@@ -4,7 +4,7 @@
4
4
  * Runs each subagent on the main thread and forwards AgentEvents for progress tracking.
5
5
  */
6
6
  import type { AgentTelemetryConfig, ThinkingLevel } from "@gajae-code/agent-core";
7
- import type { Model, ServiceTier } from "@gajae-code/ai/core";
7
+ import { type Model, type ServiceTier } from "@gajae-code/ai/core";
8
8
  import { ModelRegistry } from "../config/model-registry";
9
9
  import type { PromptTemplate } from "../config/prompt-templates";
10
10
  import { Settings } from "../config/settings";
@@ -82,14 +82,11 @@ export interface ExecutorOptions {
82
82
  /** Parent session's registry; shared by child sessions for IRC routing and roster visibility. */
83
83
  agentRegistry?: AgentRegistry;
84
84
  /**
85
- * Live service-tier intent of the parent session (`AgentSession.serviceTier`),
86
- * used as the inherited tier when `task.serviceTier === "inherit"`. Passing the
87
- * live value (not the stale settings snapshot) lets a runtime `/fast on` reach
88
- * subagents, and a main-model fast-mode auto-disable does not clobber it.
85
+ * Parent service-tier intent captured when the child is spawned. Used when
86
+ * `task.serviceTier === "inherit"` so request serialization and reporting use
87
+ * the same immutable child settings snapshot.
89
88
  */
90
89
  inheritedServiceTier?: ServiceTier;
91
- /** Resolve whether the effective subagent tier grants fast mode for the selected provider. */
92
- isFastForSubagentProvider?: (provider?: string) => boolean;
93
90
  /** Override local:// protocol options so subagent shares parent's local:// root */
94
91
  localProtocolOptions?: LocalProtocolOptions;
95
92
  /**
@@ -237,7 +237,7 @@ export interface ToolSession {
237
237
  /** Live service-tier intent of the parent session, inherited by `inherit` subagents. */
238
238
  serviceTier?: ServiceTier;
239
239
  /** Whether the effective subagent tier grants fast mode for a resolved provider. */
240
- isFastForSubagentProvider?: (provider?: string) => boolean;
240
+ isFastForSubagentProvider?: (provider?: string, supportsServiceTier?: boolean) => boolean;
241
241
  /** Plan mode state (if active) */
242
242
  getPlanModeState?: () => PlanModeState | undefined;
243
243
  /** Goal mode state (if active or paused) */
@@ -90,7 +90,7 @@ export declare class ReadTool implements AgentTool<typeof readSchema, ReadToolDe
90
90
  readonly name = "read";
91
91
  readonly label = "Read";
92
92
  readonly loadMode = "essential";
93
- readonly description: string;
93
+ get description(): string;
94
94
  readonly parameters: z.ZodObject<{
95
95
  path: z.ZodString;
96
96
  truncation: z.ZodOptional<z.ZodEnum<{
@@ -58,7 +58,7 @@ export declare class SearchTool implements AgentTool<typeof searchSchema, Search
58
58
  readonly label = "Search";
59
59
  readonly loadMode = "discoverable";
60
60
  readonly summary = "Search file contents using ripgrep (fast text search)";
61
- readonly description: string;
61
+ get description(): string;
62
62
  readonly parameters: z.ZodObject<{
63
63
  pattern: z.ZodString;
64
64
  paths: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -1,13 +1,82 @@
1
1
  export type EditMode = "replace" | "patch" | "hashline" | "vim" | "apply_patch";
2
+ /** Setting value for `edit.mode`: an executable mode or `auto` (model-family routing). */
3
+ export type EditModeSetting = "auto" | EditMode;
4
+ /** Fallback executable mode used when automatic routing cannot identify the model. */
2
5
  export declare const DEFAULT_EDIT_MODE: EditMode;
6
+ /** Default `edit.mode` setting value. */
7
+ export declare const DEFAULT_EDIT_MODE_SETTING: EditModeSetting;
3
8
  export declare const EDIT_MODES: EditMode[];
9
+ export declare const EDIT_MODE_SETTINGS: EditModeSetting[];
4
10
  export declare function normalizeEditMode(mode?: string | null): EditMode | undefined;
11
+ export type ModelEditFamily = "gpt" | "codex" | "claude" | "deepseek" | "qwen" | "minimax" | "glm" | "kimi" | "unknown";
12
+ /**
13
+ * Detect the model family from a model ID, ignoring the provider prefix so
14
+ * equivalent models served through different providers route identically
15
+ * (`openai/gpt-5.4`, `openrouter/openai/gpt-5.4`, `company-gateway/gpt-5.4`).
16
+ *
17
+ * Matching only examines the final path segment. A direct family prefix or a
18
+ * known provider-qualified family name is accepted, while arbitrary token
19
+ * substrings return `unknown` instead of guessing.
20
+ */
21
+ export declare function detectModelEditFamily(modelId: string | undefined): ModelEditFamily;
22
+ /** Built-in mode for a detected family; `undefined` for `unknown`. */
23
+ export declare function builtinEditModeForFamily(family: ModelEditFamily): EditMode | undefined;
24
+ export type EditModeSource = "environment" | "model-override" | "setting" | "catalog" | "builtin-family" | "fallback";
25
+ export type ResolvedEditModeDetails = {
26
+ mode: EditMode;
27
+ source: EditModeSource;
28
+ modelId?: string;
29
+ family?: ModelEditFamily;
30
+ matchedRule?: string;
31
+ };
32
+ /** Raw `edit.modelVariants` match: pattern plus the configured (unvalidated) value. */
33
+ export type EditVariantMatch = {
34
+ pattern: string;
35
+ value: string;
36
+ };
5
37
  export interface EditModeSettingsLike {
6
38
  get(key: "edit.mode"): unknown;
39
+ /** Legacy accessor: first *valid* matching `edit.modelVariants` value. */
7
40
  getEditVariantForModel?(model: string | undefined): EditMode | null;
41
+ /**
42
+ * Discriminated accessor: first matching `edit.modelVariants` rule with its
43
+ * raw value. Lets the resolver fail closed on invalid matched values
44
+ * instead of silently falling through to another mode.
45
+ */
46
+ matchEditVariantForModel?(model: string | undefined): EditVariantMatch | null;
8
47
  }
9
48
  export interface EditModeSessionLike {
10
49
  settings: EditModeSettingsLike;
11
50
  getActiveModelString?: () => string | undefined;
51
+ /**
52
+ * Optional model-catalog edit recommendation; beats the built-in family
53
+ * mapping when present.
54
+ *
55
+ * Extension point: production sessions do not wire this yet — the model
56
+ * catalog does not expose an edit-protocol recommendation (`applyPatchToolType`
57
+ * is the OpenAI wire representation of an already-selected `apply_patch`
58
+ * mode, not a mode selector). Catalog metadata generation is deliberately
59
+ * deferred until the family-based path is proven for arbitrary custom-provider
60
+ * IDs; when it lands, sessions can expose this hook without a resolver change.
61
+ */
62
+ getCatalogEditMode?: () => EditMode | undefined;
12
63
  }
64
+ /**
65
+ * Read the forced edit mode from `GJC_EDIT_VARIANT`/`PI_EDIT_VARIANT`.
66
+ * The environment force is the emergency kill switch and beats every other
67
+ * source. Invalid values fail fast; empty and `auto` mean "not forced".
68
+ */
69
+ export declare function resolveForcedEnvEditMode(): EditMode | undefined;
70
+ /**
71
+ * Resolve the active edit mode with provenance.
72
+ *
73
+ * Precedence:
74
+ * 1. `GJC_EDIT_VARIANT`/`PI_EDIT_VARIANT` environment force (invalid → throw).
75
+ * 2. Matching user `edit.modelVariants` rule (matched invalid → throw).
76
+ * 3. Explicit `edit.mode` setting when it is not `auto`.
77
+ * 4. Model-catalog edit recommendation, when the session exposes one.
78
+ * 5. Built-in model-family mapping.
79
+ * 6. `hashline` fallback.
80
+ */
81
+ export declare function resolveEditModeDetails(session: EditModeSessionLike): ResolvedEditModeDetails;
13
82
  export declare function resolveEditMode(session: EditModeSessionLike): EditMode;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/coding-agent",
4
- "version": "0.13.0",
4
+ "version": "0.13.2",
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",
@@ -54,13 +54,13 @@
54
54
  "@agentclientprotocol/sdk": "1.3.0",
55
55
  "@babel/parser": "^7.29.3",
56
56
  "@mozilla/readability": "^0.6.0",
57
- "@gajae-code/stats": "0.13.0",
58
- "@gajae-code/agent-core": "0.13.0",
59
- "@gajae-code/ai": "0.13.0",
60
- "@gajae-code/bridge-client": "0.13.0",
61
- "@gajae-code/natives": "0.13.0",
62
- "@gajae-code/tui": "0.13.0",
63
- "@gajae-code/utils": "0.13.0",
57
+ "@gajae-code/stats": "0.13.2",
58
+ "@gajae-code/agent-core": "0.13.2",
59
+ "@gajae-code/ai": "0.13.2",
60
+ "@gajae-code/bridge-client": "0.13.2",
61
+ "@gajae-code/natives": "0.13.2",
62
+ "@gajae-code/tui": "0.13.2",
63
+ "@gajae-code/utils": "0.13.2",
64
64
  "@puppeteer/browsers": "^2.13.0",
65
65
  "@types/turndown": "5.0.6",
66
66
  "@xterm/headless": "^6.0.0",
@@ -19,16 +19,18 @@ function defaultKeys(
19
19
  bindingId: keyof typeof KEYBINDINGS,
20
20
  ): string {
21
21
  const defaultsByPlatform = DOCUMENTATION_PLATFORMS.map(platform => {
22
+ let keys: string | readonly string[];
22
23
  switch (bindingId) {
23
24
  case "app.message.queue":
24
- return defaultMessageQueueKeysForPlatform(platform);
25
+ keys = defaultMessageQueueKeysForPlatform(platform);
26
+ break;
25
27
  case "app.clipboard.pasteImage":
26
- return defaultClipboardPasteImageKeysForPlatform(platform);
27
- default: {
28
- const keys = value.defaultKeys;
29
- return (Array.isArray(keys) ? keys : [keys]).join(", ") || "_(none)_";
30
- }
28
+ keys = defaultClipboardPasteImageKeysForPlatform(platform);
29
+ break;
30
+ default:
31
+ keys = value.defaultKeys;
31
32
  }
33
+ return (typeof keys === "string" ? keys : keys.join(", ")) || "_(none)_";
32
34
  });
33
35
  const groups = new Map<string, string[]>();
34
36
  for (const [index, keys] of defaultsByPlatform.entries()) {
@@ -24,6 +24,7 @@ import {
24
24
  writeSessionLifecycleReady,
25
25
  } from "../sdk/broker/lifecycle";
26
26
  import { processIncarnation } from "../sdk/broker/process-incarnation";
27
+ import { writeBrokerStartupFailureMarker } from "../sdk/broker/startup-failure";
27
28
  import { runSdkSessionCli } from "../sdk/cli";
28
29
  import { runSdkGuidesCli } from "../sdk/guides/cli";
29
30
  import { type CreateLifecycleAgentSessionResult, createLifecycleAgentSession } from "../sdk/lifecycle-session";
@@ -913,8 +914,28 @@ export default class Sdk extends Command {
913
914
  return policy === "disabled" ? "disabled" : "copy-retain";
914
915
  },
915
916
  });
916
- await broker.start();
917
- if (!broker.ownsDiscovery) return;
917
+ try {
918
+ await broker.start();
919
+ } catch (error) {
920
+ // Surface why this detached broker failed before discovery: the caller
921
+ // spawns with stdio ignored, so the durable marker is the only channel.
922
+ await writeBrokerStartupFailureMarker(agentDir, {
923
+ reason: error instanceof Error ? error.message : String(error),
924
+ exitCode: 1,
925
+ signal: null,
926
+ });
927
+ throw error;
928
+ }
929
+ if (!broker.ownsDiscovery) {
930
+ // Another broker owns discovery; this process exits cleanly (code 0) as
931
+ // the race loser. Record why so the caller can diagnose a missing winner.
932
+ await writeBrokerStartupFailureMarker(agentDir, {
933
+ reason: "Another broker owns the lock/discovery; this broker exited as the race loser.",
934
+ exitCode: 0,
935
+ signal: null,
936
+ });
937
+ return;
938
+ }
918
939
  // A live broker must not keep advertising sessions whose host process is
919
940
  // gone; the sweep is the broker-side half of the host reaping bound.
920
941
  const stopSweep = startBrokerDeadRegistrationSweep(broker);