@gajae-code/coding-agent 0.7.6 → 0.7.8

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 (111) hide show
  1. package/CHANGELOG.md +26 -1
  2. package/README.md +8 -0
  3. package/dist/types/cli/local-provider-smoke.d.ts +59 -0
  4. package/dist/types/cli/notify-cli.d.ts +1 -1
  5. package/dist/types/cli/update-cli.d.ts +24 -1
  6. package/dist/types/commands/local-provider.d.ts +34 -0
  7. package/dist/types/config/model-profiles.d.ts +2 -2
  8. package/dist/types/config/model-registry.d.ts +8 -3
  9. package/dist/types/config/models-config-schema.d.ts +5 -5
  10. package/dist/types/config/settings-schema.d.ts +123 -9
  11. package/dist/types/config/settings.d.ts +7 -0
  12. package/dist/types/coordinator-mcp/server.d.ts +8 -0
  13. package/dist/types/export/html/template.generated.d.ts +1 -1
  14. package/dist/types/gjc-runtime/launch-tmux.d.ts +2 -2
  15. package/dist/types/gjc-runtime/state-renderer.d.ts +5 -0
  16. package/dist/types/gjc-runtime/team-runtime.d.ts +1 -1
  17. package/dist/types/gjc-runtime/tmux-common.d.ts +12 -0
  18. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +37 -1
  19. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +80 -0
  20. package/dist/types/modes/components/custom-model-preset-wizard.d.ts +1 -1
  21. package/dist/types/modes/components/model-selector.d.ts +2 -0
  22. package/dist/types/modes/components/settings-selector.d.ts +4 -1
  23. package/dist/types/modes/components/status-line/types.d.ts +5 -7
  24. package/dist/types/modes/components/status-line.d.ts +4 -3
  25. package/dist/types/modes/controllers/input-controller.d.ts +1 -0
  26. package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
  27. package/dist/types/modes/prompt-action-autocomplete.d.ts +1 -0
  28. package/dist/types/modes/tmux-scroll.d.ts +18 -0
  29. package/dist/types/modes/utils/terminal-bell.d.ts +3 -0
  30. package/dist/types/notifications/telegram-daemon-cli.d.ts +13 -2
  31. package/dist/types/tools/browser/tab-supervisor.d.ts +31 -0
  32. package/dist/types/tools/computer-gc.d.ts +23 -0
  33. package/dist/types/tools/cron.d.ts +36 -61
  34. package/dist/types/tools/index.d.ts +0 -1
  35. package/dist/types/tools/resource-gc.d.ts +54 -0
  36. package/dist/types/web/search/index.d.ts +1 -0
  37. package/dist/types/web/search/providers/utils.d.ts +11 -4
  38. package/package.json +7 -7
  39. package/src/cli/args.ts +0 -1
  40. package/src/cli/fast-help.ts +0 -1
  41. package/src/cli/local-provider-smoke.ts +632 -0
  42. package/src/cli/notify-cli.ts +4 -2
  43. package/src/cli/update-cli.ts +155 -13
  44. package/src/cli/web-search-cli.ts +5 -0
  45. package/src/cli.ts +18 -0
  46. package/src/commands/launch.ts +46 -1
  47. package/src/commands/local-provider.ts +62 -0
  48. package/src/commands/notify.ts +1 -1
  49. package/src/config/model-profiles.ts +3 -4
  50. package/src/config/model-registry.ts +58 -7
  51. package/src/config/models-config-schema.ts +10 -1
  52. package/src/config/settings-schema.ts +127 -10
  53. package/src/config/settings.ts +11 -0
  54. package/src/coordinator-mcp/server.ts +276 -34
  55. package/src/export/html/template.generated.ts +1 -1
  56. package/src/export/html/template.js +0 -12
  57. package/src/gjc-runtime/launch-tmux.ts +34 -8
  58. package/src/gjc-runtime/launch-worktree.ts +16 -2
  59. package/src/gjc-runtime/state-renderer.ts +13 -0
  60. package/src/gjc-runtime/team-runtime.ts +21 -6
  61. package/src/gjc-runtime/tmux-common.ts +16 -0
  62. package/src/gjc-runtime/tmux-sessions.ts +13 -9
  63. package/src/gjc-runtime/ultragoal-guard.ts +167 -0
  64. package/src/gjc-runtime/ultragoal-runtime.ts +221 -1
  65. package/src/goals/tools/goal-tool.ts +14 -1
  66. package/src/internal-urls/docs-index.generated.ts +7 -7
  67. package/src/modes/components/agent-dashboard.ts +2 -1
  68. package/src/modes/components/custom-model-preset-wizard.ts +30 -189
  69. package/src/modes/components/model-selector.ts +182 -5
  70. package/src/modes/components/session-observer-overlay.ts +3 -3
  71. package/src/modes/components/settings-selector.ts +491 -5
  72. package/src/modes/components/status-line/segments.ts +7 -18
  73. package/src/modes/components/status-line/types.ts +6 -2
  74. package/src/modes/components/status-line.ts +69 -35
  75. package/src/modes/controllers/event-controller.ts +98 -4
  76. package/src/modes/controllers/extension-ui-controller.ts +3 -0
  77. package/src/modes/controllers/input-controller.ts +15 -0
  78. package/src/modes/controllers/selector-controller.ts +18 -4
  79. package/src/modes/prompt-action-autocomplete.ts +8 -0
  80. package/src/modes/tmux-scroll.ts +92 -0
  81. package/src/modes/utils/terminal-bell.ts +48 -0
  82. package/src/notifications/telegram-daemon-cli.ts +112 -9
  83. package/src/notifications/telegram-daemon.ts +4 -0
  84. package/src/prompts/system/system-prompt.md +2 -2
  85. package/src/prompts/tools/cron.md +5 -3
  86. package/src/prompts/tools/read.md +1 -1
  87. package/src/sdk.ts +5 -0
  88. package/src/session/agent-session.ts +118 -5
  89. package/src/slash-commands/builtin-registry.ts +106 -13
  90. package/src/tools/browser/tab-supervisor.ts +86 -2
  91. package/src/tools/computer-gc.ts +66 -0
  92. package/src/tools/computer.ts +2 -0
  93. package/src/tools/cron.ts +75 -112
  94. package/src/tools/index.ts +2 -8
  95. package/src/tools/read.ts +25 -55
  96. package/src/tools/renderers.ts +0 -2
  97. package/src/tools/resource-gc.ts +291 -0
  98. package/src/tools/ultragoal-ask-guard.ts +7 -1
  99. package/src/web/search/index.ts +1 -0
  100. package/src/web/search/providers/utils.ts +22 -5
  101. package/vendor/insane-search/MANIFEST.json +3 -1
  102. package/vendor/insane-search/engine/__init__.py +14 -0
  103. package/vendor/insane-search/engine/content_safety.py +151 -0
  104. package/vendor/insane-search/engine/fetch_chain.py +32 -0
  105. package/vendor/insane-search/engine/tests/test_u8.py +216 -0
  106. package/dist/types/tools/inspect-image-renderer.d.ts +0 -26
  107. package/dist/types/tools/inspect-image.d.ts +0 -31
  108. package/src/prompts/tools/inspect-image-system.md +0 -20
  109. package/src/prompts/tools/inspect-image.md +0 -32
  110. package/src/tools/inspect-image-renderer.ts +0 -103
  111. package/src/tools/inspect-image.ts +0 -172
@@ -274,6 +274,43 @@ export const SETTINGS_SCHEMA = {
274
274
  validate: (value: number) => Number.isFinite(value) && value > 0,
275
275
  },
276
276
 
277
+ "notifications.terminalBell": {
278
+ type: "boolean",
279
+ default: false,
280
+ ui: {
281
+ tab: "interaction",
282
+ label: "Terminal Bell",
283
+ description: "Emit a BEL character for local terminal notifications",
284
+ },
285
+ },
286
+ "notifications.bellOnComplete": {
287
+ type: "boolean",
288
+ default: true,
289
+ ui: {
290
+ tab: "interaction",
291
+ label: "Bell on Completion",
292
+ description: "Ring the terminal bell when an agent turn completes",
293
+ },
294
+ },
295
+ "notifications.bellOnApproval": {
296
+ type: "boolean",
297
+ default: true,
298
+ ui: {
299
+ tab: "interaction",
300
+ label: "Bell on Approval",
301
+ description: "Ring the terminal bell when a plan or approval prompt needs attention",
302
+ },
303
+ },
304
+ "notifications.bellOnAsk": {
305
+ type: "boolean",
306
+ default: true,
307
+ ui: {
308
+ tab: "interaction",
309
+ label: "Bell on Ask",
310
+ description: "Ring the terminal bell when an ask/user-input prompt needs attention",
311
+ },
312
+ },
313
+
277
314
  autoResume: {
278
315
  type: "boolean",
279
316
  default: false,
@@ -1135,6 +1172,16 @@ export const SETTINGS_SCHEMA = {
1135
1172
  default: "on",
1136
1173
  ui: { tab: "interaction", label: "Completion Notification", description: "Notify when the agent completes" },
1137
1174
  },
1175
+ "completion.notifyCommand": {
1176
+ type: "string",
1177
+ default: "",
1178
+ ui: {
1179
+ tab: "interaction",
1180
+ label: "Completion Notification Command",
1181
+ description:
1182
+ "Optional user-level shell command to run when an agent turn completes; receives GJC_NOTIFICATION_* environment variables.",
1183
+ },
1184
+ },
1138
1185
 
1139
1186
  "ask.timeout": {
1140
1187
  type: "number",
@@ -2071,16 +2118,6 @@ export const SETTINGS_SCHEMA = {
2071
2118
  },
2072
2119
  },
2073
2120
 
2074
- "inspect_image.enabled": {
2075
- type: "boolean",
2076
- default: false,
2077
- ui: {
2078
- tab: "tools",
2079
- label: "Inspect Image",
2080
- description: "Enable the inspect_image tool, delegating image understanding to a vision-capable model",
2081
- },
2082
- },
2083
-
2084
2121
  "checkpoint.enabled": {
2085
2122
  type: "boolean",
2086
2123
  default: false,
@@ -2178,6 +2215,17 @@ export const SETTINGS_SCHEMA = {
2178
2215
  },
2179
2216
  },
2180
2217
 
2218
+ "web_search.timeout": {
2219
+ type: "number",
2220
+ default: 300,
2221
+ validate: (value: number) => Number.isFinite(value) && value > 0,
2222
+ ui: {
2223
+ tab: "tools",
2224
+ label: "Web Search Timeout",
2225
+ description: "Hard timeout in seconds for a single web search request (default 300)",
2226
+ },
2227
+ },
2228
+
2181
2229
  "browser.enabled": {
2182
2230
  type: "boolean",
2183
2231
  default: true,
@@ -2208,6 +2256,46 @@ export const SETTINGS_SCHEMA = {
2208
2256
  },
2209
2257
  },
2210
2258
 
2259
+ "browser.gc.enabled": {
2260
+ type: "boolean",
2261
+ default: true,
2262
+ ui: {
2263
+ tab: "tools",
2264
+ label: "Browser Tab GC",
2265
+ description: "Automatically reclaim idle, unheld GJC-managed headless/spawned browser tabs.",
2266
+ },
2267
+ },
2268
+ "browser.gc.idleMs": {
2269
+ type: "number",
2270
+ default: 300_000,
2271
+ validate: (value: number) => Number.isFinite(value) && value >= 0,
2272
+ ui: {
2273
+ tab: "tools",
2274
+ label: "Browser Tab GC Idle (ms)",
2275
+ description: "Evict headless/spawned tabs idle longer than this many milliseconds.",
2276
+ },
2277
+ },
2278
+ "browser.gc.rssLimitMb": {
2279
+ type: "number",
2280
+ default: 1536,
2281
+ validate: (value: number) => Number.isFinite(value) && value > 0,
2282
+ ui: {
2283
+ tab: "tools",
2284
+ label: "Browser Tab GC RSS Limit (MB)",
2285
+ description: "Parent-process RSS (MB) above which idle tabs are opportunistically evicted LRU.",
2286
+ },
2287
+ },
2288
+ "resourceGc.sweepIntervalMs": {
2289
+ type: "number",
2290
+ default: 30_000,
2291
+ validate: (value: number) => Number.isFinite(value) && value > 0,
2292
+ ui: {
2293
+ tab: "tools",
2294
+ label: "Resource GC Sweep Interval (ms)",
2295
+ description: "How often the resource GC sweeps browser tabs and stale screenshot directories.",
2296
+ },
2297
+ },
2298
+
2211
2299
  "computer.enabled": {
2212
2300
  type: "boolean",
2213
2301
  default: false,
@@ -2267,6 +2355,35 @@ export const SETTINGS_SCHEMA = {
2267
2355
  description: "Persist audit records for enabled computer-use actions.",
2268
2356
  },
2269
2357
  },
2358
+ "computer.screenshotGc.enabled": {
2359
+ type: "boolean",
2360
+ default: true,
2361
+ ui: {
2362
+ tab: "tools",
2363
+ label: "Computer Screenshot GC",
2364
+ description: "Delete stale computer-use screenshot fallback directories on disk.",
2365
+ },
2366
+ },
2367
+ "computer.screenshotGc.staleMs": {
2368
+ type: "number",
2369
+ default: 43_200_000,
2370
+ validate: (value: number) => Number.isFinite(value) && value >= 0,
2371
+ ui: {
2372
+ tab: "tools",
2373
+ label: "Computer Screenshot GC Stale Age (ms)",
2374
+ description: "Remove screenshot fallback directories whose mtime is older than this many milliseconds.",
2375
+ },
2376
+ },
2377
+ "computer.screenshotGc.scanIntervalMs": {
2378
+ type: "number",
2379
+ default: 1_800_000,
2380
+ validate: (value: number) => Number.isFinite(value) && value > 0,
2381
+ ui: {
2382
+ tab: "tools",
2383
+ label: "Computer Screenshot GC Scan Interval (ms)",
2384
+ description: "Minimum interval between os.tmpdir scans for stale screenshot directories.",
2385
+ },
2386
+ },
2270
2387
 
2271
2388
  // Tool execution
2272
2389
  "tools.intentTracing": {
@@ -297,6 +297,17 @@ export class Settings {
297
297
  return getDefault(path);
298
298
  }
299
299
 
300
+ /**
301
+ * Get a setting value from the user/global config only.
302
+ *
303
+ * Use for machine-local command hooks and other settings that must not be
304
+ * activated by project-scoped config files.
305
+ */
306
+ getGlobal<P extends SettingPath>(path: P): SettingValue<P> | undefined {
307
+ const value = getByPath(this.#global, path.split("."));
308
+ return value === undefined ? undefined : (value as SettingValue<P>);
309
+ }
310
+
300
311
  /** Check whether a setting is present in loaded settings/overrides rather than coming from schema defaults. */
301
312
  has(path: SettingPath): boolean {
302
313
  return getByPath(this.#merged, path.split(".")) !== undefined;
@@ -119,7 +119,7 @@ interface TurnRecord {
119
119
  target: string | null;
120
120
  tmux_keys_sent?: boolean;
121
121
  prompt_acknowledged?: boolean;
122
- state?: "queued" | "tmux_keys_sent" | "acknowledged" | "unavailable";
122
+ state?: "queued" | "tmux_keys_sent" | "acknowledged" | "unavailable" | "unacknowledged";
123
123
  attempts: Array<{
124
124
  delivered: boolean;
125
125
  created_at: string;
@@ -214,6 +214,9 @@ interface CoordinatorEventInput {
214
214
  }
215
215
 
216
216
  const MISSING_FINAL_RESPONSE_ADVISORY = "completion_missing_final_response";
217
+ const PROMPT_ACK_TIMEOUT_REASON = "runtime_prompt_ack_timeout";
218
+ const DEFAULT_RUNTIME_PROMPT_ACK_TIMEOUT_MS = 10_000;
219
+ const MAX_RUNTIME_PROMPT_ACK_TIMEOUT_MS = 5 * 60 * 1000;
217
220
  const ACTIVE_TURN_STATUSES = new Set<TurnStatus>(["delivering", "active", "waiting_for_answer", "completing"]);
218
221
  const TERMINAL_TURN_STATUSES = new Set<TurnStatus>(["completed", "failed", "cancelled", "superseded"]);
219
222
  const TURN_ID_PATTERN = /^turn-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
@@ -317,8 +320,14 @@ function toolSchema(name: CoordinatorToolName): {
317
320
  properties: {
318
321
  session_id: sessionId,
319
322
  turn_id: { type: "string" },
320
- timeout_ms: { type: "number" },
321
- poll_interval_ms: { type: "number" },
323
+ timeout_ms: {
324
+ type: "number",
325
+ description: "Bounded await timeout in milliseconds, capped at 30 minutes.",
326
+ },
327
+ poll_interval_ms: {
328
+ type: "number",
329
+ description: "Bounded polling interval in milliseconds, capped at 10 seconds.",
330
+ },
322
331
  lines: { type: "number" },
323
332
  },
324
333
  required: ["turn_id"],
@@ -406,7 +415,10 @@ function toolSchema(name: CoordinatorToolName): {
406
415
  after_seq: { type: "number" },
407
416
  session_id: sessionId,
408
417
  event_types: { type: "array", items: { type: "string" } },
409
- timeout_ms: { type: "number" },
418
+ timeout_ms: {
419
+ type: "number",
420
+ description: "Bounded event long-poll timeout in milliseconds, capped at 30 seconds.",
421
+ },
410
422
  limit: { type: "number" },
411
423
  },
412
424
  },
@@ -447,7 +459,8 @@ function toolSchema(name: CoordinatorToolName): {
447
459
  await_completion: { type: "boolean", description: "If true, poll the turn until terminal or timeout." },
448
460
  timeout_ms: {
449
461
  type: "number",
450
- description: "Bounded await timeout; same cap semantics as gjc_coordinator_await_turn.",
462
+ description:
463
+ "Bounded await timeout in milliseconds, capped at 30 minutes like gjc_coordinator_await_turn.",
451
464
  },
452
465
  poll_interval_ms: { type: "number", description: "Bounded await polling interval." },
453
466
  lines: { type: "number", description: "Bounded advisory tail lines returned with await/read payloads." },
@@ -1039,6 +1052,135 @@ async function markTurnTerminalFromSessionState(
1039
1052
  return resolved;
1040
1053
  }
1041
1054
 
1055
+ function runtimeStateAcknowledgesTurn(turn: TurnRecord, sessionState: CoordinatorSessionState | null): boolean {
1056
+ return (
1057
+ sessionState?.source === "agent_session_event" &&
1058
+ sessionState.current_turn_id === turn.turn_id &&
1059
+ (sessionState.state === "running" ||
1060
+ sessionState.state === "needs_user_input" ||
1061
+ sessionState.state === "completed" ||
1062
+ sessionState.state === "errored")
1063
+ );
1064
+ }
1065
+
1066
+ async function markTurnAcknowledgedFromRuntimeState(
1067
+ namespaceDir: string,
1068
+ turn: TurnRecord,
1069
+ sessionState: CoordinatorSessionState,
1070
+ ): Promise<TurnRecord> {
1071
+ if (turn.delivery.prompt_acknowledged === true && turn.delivery.state === "acknowledged") return turn;
1072
+ const timestamp = new Date().toISOString();
1073
+ const acknowledged: TurnRecord = {
1074
+ ...turn,
1075
+ delivery: {
1076
+ ...turn.delivery,
1077
+ delivered: true,
1078
+ prompt_acknowledged: true,
1079
+ state: "acknowledged",
1080
+ attempts: [
1081
+ ...turn.delivery.attempts,
1082
+ {
1083
+ delivered: true,
1084
+ created_at: sessionState.updated_at,
1085
+ reason: "runtime_prompt_acknowledged",
1086
+ channel: "runtime_ack",
1087
+ tmux_keys_sent: turn.delivery.tmux_keys_sent,
1088
+ },
1089
+ ],
1090
+ },
1091
+ updated_at: timestamp,
1092
+ };
1093
+ await writeTurnRecord(namespaceDir, acknowledged);
1094
+ await writeActiveTurn(namespaceDir, acknowledged);
1095
+ return acknowledged;
1096
+ }
1097
+
1098
+ function turnAwaitingRuntimeAckExpired(turn: TurnRecord, nowMs: number, ackTimeoutMs: number): boolean {
1099
+ if (!ACTIVE_TURN_STATUSES.has(turn.status)) return false;
1100
+ if (turn.delivery.tmux_keys_sent !== true) return false;
1101
+ if (turn.delivery.prompt_acknowledged === true) return false;
1102
+ if (turn.delivery.state !== "tmux_keys_sent") return false;
1103
+ const deliveredAt =
1104
+ turn.delivery.attempts.findLast(attempt => attempt.channel === "tmux_keys")?.created_at ?? turn.updated_at;
1105
+ const deliveredMs = Date.parse(deliveredAt);
1106
+ return Number.isFinite(deliveredMs) && nowMs - deliveredMs >= ackTimeoutMs;
1107
+ }
1108
+
1109
+ async function markTurnFailedForUnacknowledgedDelivery(
1110
+ namespaceDir: string,
1111
+ turn: TurnRecord,
1112
+ ackTimeoutMs: number,
1113
+ ): Promise<TurnRecord> {
1114
+ const timestamp = new Date().toISOString();
1115
+ const message = `Tmux key delivery succeeded, but the GJC runtime did not acknowledge the prompt or emit turn_start within ${ackTimeoutMs}ms. The turn never started; stop waiting and inspect/retry the coordinator session.`;
1116
+ const failed: TurnRecord = {
1117
+ ...turn,
1118
+ status: "failed",
1119
+ delivery: {
1120
+ ...turn.delivery,
1121
+ delivered: false,
1122
+ queued: false,
1123
+ prompt_acknowledged: false,
1124
+ state: "unacknowledged",
1125
+ attempts: [
1126
+ ...turn.delivery.attempts,
1127
+ {
1128
+ delivered: false,
1129
+ created_at: timestamp,
1130
+ reason: PROMPT_ACK_TIMEOUT_REASON,
1131
+ channel: "runtime_ack",
1132
+ tmux_keys_sent: true,
1133
+ },
1134
+ ],
1135
+ },
1136
+ final_response: {
1137
+ text: message,
1138
+ format: "markdown",
1139
+ source: "coordinator_delivery_ack_timeout",
1140
+ artifact_path: null,
1141
+ truncated: false,
1142
+ },
1143
+ error: { code: PROMPT_ACK_TIMEOUT_REASON, message, recoverable: true },
1144
+ evidence: [
1145
+ ...turn.evidence,
1146
+ {
1147
+ type: PROMPT_ACK_TIMEOUT_REASON,
1148
+ message,
1149
+ tmux_keys_sent: true,
1150
+ prompt_acknowledged: false,
1151
+ created_at: timestamp,
1152
+ },
1153
+ ],
1154
+ liveness: { checked_at: timestamp, live: turn.liveness.live, reason: PROMPT_ACK_TIMEOUT_REASON },
1155
+ updated_at: timestamp,
1156
+ completed_at: timestamp,
1157
+ };
1158
+ await writeTurnRecord(namespaceDir, failed);
1159
+ await clearActiveTurn(namespaceDir, failed);
1160
+ await writeSessionState(namespaceDir, failed.session_id, "stale", {
1161
+ lastTurnId: failed.turn_id,
1162
+ live: failed.liveness.live,
1163
+ reason: PROMPT_ACK_TIMEOUT_REASON,
1164
+ });
1165
+ return failed;
1166
+ }
1167
+
1168
+ async function reconcileRuntimeAcknowledgement(
1169
+ namespaceDir: string,
1170
+ turn: TurnRecord,
1171
+ sessionState: CoordinatorSessionState | null,
1172
+ ackTimeoutMs: number,
1173
+ options: { failOnTimeout: boolean } = { failOnTimeout: true },
1174
+ ): Promise<TurnRecord> {
1175
+ if (sessionState && runtimeStateAcknowledgesTurn(turn, sessionState)) {
1176
+ return await markTurnAcknowledgedFromRuntimeState(namespaceDir, turn, sessionState);
1177
+ }
1178
+ if (options.failOnTimeout && turnAwaitingRuntimeAckExpired(turn, Date.now(), ackTimeoutMs)) {
1179
+ return await markTurnFailedForUnacknowledgedDelivery(namespaceDir, turn, ackTimeoutMs);
1180
+ }
1181
+ return turn;
1182
+ }
1183
+
1042
1184
  function shellQuote(value: string): string {
1043
1185
  return `'${value.replaceAll("'", "'\\''")}'`;
1044
1186
  }
@@ -1086,16 +1228,33 @@ function asTerminalTurnStatus(status: unknown): TurnStatus | null {
1086
1228
  return null;
1087
1229
  }
1088
1230
 
1089
- function boundedTimeoutMs(value: unknown): number {
1231
+ export const COORDINATOR_AWAIT_TURN_TIMEOUT_MAX_MS = 30 * 60 * 1000;
1232
+ export const COORDINATOR_RUNTIME_PROMPT_ACK_TIMEOUT_MAX_MS = MAX_RUNTIME_PROMPT_ACK_TIMEOUT_MS;
1233
+ export const COORDINATOR_EVENT_WATCH_TIMEOUT_MAX_MS = 30_000;
1234
+ export const COORDINATOR_POLL_INTERVAL_MAX_MS = 10_000;
1235
+
1236
+ function parsePositiveIntegerMs(value: unknown, fallback: number): number {
1090
1237
  const parsed = typeof value === "number" ? value : Number.parseInt(String(value ?? ""), 10);
1091
- if (!Number.isFinite(parsed) || parsed <= 0) return 1000;
1092
- return Math.min(parsed, 30_000);
1238
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
1093
1239
  }
1094
1240
 
1095
- function boundedPollIntervalMs(value: unknown): number {
1096
- const parsed = typeof value === "number" ? value : Number.parseInt(String(value ?? ""), 10);
1097
- if (!Number.isFinite(parsed) || parsed <= 0) return 100;
1098
- return Math.min(Math.max(parsed, 10), 1000);
1241
+ export function boundedAwaitTurnTimeoutMs(value: unknown): number {
1242
+ return Math.min(parsePositiveIntegerMs(value, 1000), COORDINATOR_AWAIT_TURN_TIMEOUT_MAX_MS);
1243
+ }
1244
+
1245
+ export function boundedRuntimePromptAckTimeoutMs(value: unknown): number {
1246
+ return Math.min(
1247
+ parsePositiveIntegerMs(value, DEFAULT_RUNTIME_PROMPT_ACK_TIMEOUT_MS),
1248
+ COORDINATOR_RUNTIME_PROMPT_ACK_TIMEOUT_MAX_MS,
1249
+ );
1250
+ }
1251
+
1252
+ export function boundedEventWatchTimeoutMs(value: unknown): number {
1253
+ return Math.min(parsePositiveIntegerMs(value, 1000), COORDINATOR_EVENT_WATCH_TIMEOUT_MAX_MS);
1254
+ }
1255
+
1256
+ export function boundedPollIntervalMs(value: unknown): number {
1257
+ return Math.min(Math.max(parsePositiveIntegerMs(value, 100), 10), COORDINATOR_POLL_INTERVAL_MAX_MS);
1099
1258
  }
1100
1259
  async function runCommand(command: string[]): Promise<{ exitCode: number; stdout: string; stderr: string }> {
1101
1260
  const proc = Bun.spawn(command, { stdout: "pipe", stderr: "pipe" });
@@ -1387,7 +1546,9 @@ export async function readCoordinatorArtifact(
1387
1546
  }
1388
1547
 
1389
1548
  export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions = {}) {
1390
- const config = buildCoordinatorMcpConfig(options.env ?? process.env);
1549
+ const env = options.env ?? process.env;
1550
+ const config = buildCoordinatorMcpConfig(env);
1551
+ const promptAckTimeoutMs = boundedRuntimePromptAckTimeoutMs(env.GJC_COORDINATOR_MCP_PROMPT_ACK_TIMEOUT_MS);
1391
1552
  const services = options.services ?? {};
1392
1553
  const namespaceDir = coordinatorNamespacePath(config);
1393
1554
  const commandRunner = services.commandRunner ?? runCommand;
@@ -1485,14 +1646,17 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
1485
1646
  await writeTurnRecord(namespaceDir, activeTurn);
1486
1647
  await writeActiveTurn(namespaceDir, activeTurn);
1487
1648
  const sessionState = await readSessionState(namespaceDir, activeTurn.session_id);
1488
- const runtimeStateAlreadySettled =
1489
- sessionState?.current_turn_id === activeTurn.turn_id &&
1490
- (sessionState.state === "completed" || sessionState.state === "errored");
1491
- if (!runtimeStateAlreadySettled) {
1492
- await writeSessionState(namespaceDir, activeTurn.session_id, tmuxKeysSent ? "running" : "stale", {
1649
+ const runtimeStateAlreadyAcknowledged =
1650
+ sessionState !== null && runtimeStateAcknowledgesTurn(activeTurn, sessionState);
1651
+ const resolvedTurn =
1652
+ runtimeStateAlreadyAcknowledged && sessionState
1653
+ ? await markTurnAcknowledgedFromRuntimeState(namespaceDir, activeTurn, sessionState)
1654
+ : activeTurn;
1655
+ if (!runtimeStateAlreadyAcknowledged && !tmuxKeysSent) {
1656
+ await writeSessionState(namespaceDir, activeTurn.session_id, "stale", {
1493
1657
  currentTurnId: activeTurn.turn_id,
1494
1658
  live,
1495
- reason: tmuxKeysSent ? null : "tmux_delivery_unavailable",
1659
+ reason: "tmux_delivery_unavailable",
1496
1660
  });
1497
1661
  }
1498
1662
  await appendCoordinatorEvent(namespaceDir, {
@@ -1505,7 +1669,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
1505
1669
  payloadRef: path.relative(namespaceDir, turnFile(namespaceDir, activeTurn.turn_id)),
1506
1670
  metadata: { target: typeof target === "string" ? target : null, live },
1507
1671
  });
1508
- return activeTurn;
1672
+ return resolvedTurn;
1509
1673
  }
1510
1674
 
1511
1675
  async function promoteNextQueuedTurn(sessionId: string): Promise<TurnRecord | null> {
@@ -1533,33 +1697,67 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
1533
1697
  let resolvedTurn = turn;
1534
1698
  let advisoryStatus: Record<string, unknown> = { live: false };
1535
1699
  let sessionState = await readSessionState(namespaceDir, turn.session_id);
1700
+ resolvedTurn = await reconcileRuntimeAcknowledgement(
1701
+ namespaceDir,
1702
+ resolvedTurn,
1703
+ sessionState,
1704
+ promptAckTimeoutMs,
1705
+ { failOnTimeout: false },
1706
+ );
1707
+ if (resolvedTurn !== turn) sessionState = await readSessionState(namespaceDir, resolvedTurn.session_id);
1536
1708
  if (
1537
1709
  sessionState &&
1538
- ACTIVE_TURN_STATUSES.has(turn.status) &&
1539
- sessionState.current_turn_id === turn.turn_id &&
1710
+ ACTIVE_TURN_STATUSES.has(resolvedTurn.status) &&
1711
+ (sessionState.current_turn_id === resolvedTurn.turn_id ||
1712
+ (sessionState.state === "errored" &&
1713
+ sessionState.source === "agent_session_event" &&
1714
+ sessionState.current_turn_id == null)) &&
1540
1715
  (sessionState.state === "completed" || sessionState.state === "errored")
1541
1716
  ) {
1542
- resolvedTurn = await markTurnTerminalFromSessionState(namespaceDir, turn, sessionState);
1717
+ resolvedTurn = await markTurnTerminalFromSessionState(namespaceDir, resolvedTurn, sessionState);
1543
1718
  sessionState = await readSessionState(namespaceDir, resolvedTurn.session_id);
1544
1719
  } else if (
1545
1720
  sessionState &&
1546
- ACTIVE_TURN_STATUSES.has(turn.status) &&
1547
- sessionState.current_turn_id === turn.turn_id &&
1721
+ ACTIVE_TURN_STATUSES.has(resolvedTurn.status) &&
1722
+ sessionState.current_turn_id === resolvedTurn.turn_id &&
1548
1723
  sessionState.state === "stale" &&
1549
1724
  sessionState.reason === "tmux_delivery_unavailable" &&
1550
- turn.delivery.state === "unavailable" &&
1725
+ resolvedTurn.delivery.state === "unavailable" &&
1551
1726
  session &&
1552
1727
  hasTmuxIdentity(session)
1553
1728
  ) {
1554
- resolvedTurn = await markTurnFailedForUnavailableSession(namespaceDir, turn, "tmux_delivery_unavailable");
1729
+ resolvedTurn = await markTurnFailedForUnavailableSession(
1730
+ namespaceDir,
1731
+ resolvedTurn,
1732
+ "tmux_delivery_unavailable",
1733
+ );
1555
1734
  sessionState = await readSessionState(namespaceDir, resolvedTurn.session_id);
1556
- } else if (!session && ACTIVE_TURN_STATUSES.has(turn.status)) {
1557
- resolvedTurn = await markTurnFailedForUnavailableSession(namespaceDir, turn, "session_record_missing");
1735
+ } else if (!session && ACTIVE_TURN_STATUSES.has(resolvedTurn.status)) {
1736
+ resolvedTurn = await markTurnFailedForUnavailableSession(namespaceDir, resolvedTurn, "session_record_missing");
1558
1737
  sessionState = await readSessionState(namespaceDir, resolvedTurn.session_id);
1559
1738
  } else if (session) {
1560
1739
  advisoryStatus = await inspectTmuxSession(session, boundedLineCount(lines), commandRunner);
1561
- if (ACTIVE_TURN_STATUSES.has(turn.status) && hasTmuxIdentity(session) && advisoryStatus.live === false) {
1562
- resolvedTurn = await markTurnFailedForUnavailableSession(namespaceDir, turn, "tmux_session_missing");
1740
+ if (
1741
+ ACTIVE_TURN_STATUSES.has(resolvedTurn.status) &&
1742
+ hasTmuxIdentity(session) &&
1743
+ advisoryStatus.live === false
1744
+ ) {
1745
+ resolvedTurn = await markTurnFailedForUnavailableSession(
1746
+ namespaceDir,
1747
+ resolvedTurn,
1748
+ "tmux_session_missing",
1749
+ );
1750
+ sessionState = await readSessionState(namespaceDir, resolvedTurn.session_id);
1751
+ }
1752
+ }
1753
+ if (ACTIVE_TURN_STATUSES.has(resolvedTurn.status)) {
1754
+ resolvedTurn = await reconcileRuntimeAcknowledgement(
1755
+ namespaceDir,
1756
+ resolvedTurn,
1757
+ sessionState,
1758
+ promptAckTimeoutMs,
1759
+ );
1760
+ if (!ACTIVE_TURN_STATUSES.has(resolvedTurn.status)) {
1563
1761
  sessionState = await readSessionState(namespaceDir, resolvedTurn.session_id);
1564
1762
  }
1565
1763
  }
@@ -1579,6 +1777,46 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
1579
1777
  };
1580
1778
  }
1581
1779
 
1780
+ async function reconcileActiveTurnAcknowledgements(): Promise<void> {
1781
+ const turns = (await listJsonFiles(turnsDir(namespaceDir)))
1782
+ .map(turn => asRecord(turn) as TurnRecord | null)
1783
+ .filter((turn): turn is TurnRecord => turn !== null && ACTIVE_TURN_STATUSES.has(turn.status));
1784
+ for (const turn of turns) {
1785
+ let sessionState = await readSessionState(namespaceDir, turn.session_id);
1786
+ const resolvedTurn = await reconcileRuntimeAcknowledgement(
1787
+ namespaceDir,
1788
+ turn,
1789
+ sessionState,
1790
+ promptAckTimeoutMs,
1791
+ { failOnTimeout: false },
1792
+ );
1793
+ if (!ACTIVE_TURN_STATUSES.has(resolvedTurn.status)) continue;
1794
+ if (resolvedTurn !== turn) sessionState = await readSessionState(namespaceDir, resolvedTurn.session_id);
1795
+ const session = asRecord(await readJsonFile(sessionFile(resolvedTurn.session_id)));
1796
+ if (
1797
+ sessionState &&
1798
+ sessionState.current_turn_id === resolvedTurn.turn_id &&
1799
+ sessionState.state === "stale" &&
1800
+ sessionState.reason === "tmux_delivery_unavailable" &&
1801
+ resolvedTurn.delivery.state === "unavailable" &&
1802
+ session &&
1803
+ hasTmuxIdentity(session)
1804
+ ) {
1805
+ await markTurnFailedForUnavailableSession(namespaceDir, resolvedTurn, "tmux_delivery_unavailable");
1806
+ continue;
1807
+ }
1808
+ if (!session) {
1809
+ await markTurnFailedForUnavailableSession(namespaceDir, resolvedTurn, "session_record_missing");
1810
+ continue;
1811
+ }
1812
+ if (hasTmuxIdentity(session) && (await hasTmuxSession(session, commandRunner)) === false) {
1813
+ await markTurnFailedForUnavailableSession(namespaceDir, resolvedTurn, "tmux_session_missing");
1814
+ continue;
1815
+ }
1816
+ await reconcileRuntimeAcknowledgement(namespaceDir, resolvedTurn, sessionState, promptAckTimeoutMs);
1817
+ }
1818
+ }
1819
+
1582
1820
  async function callTool(name: string, args: Record<string, unknown> = {}): Promise<Record<string, unknown>> {
1583
1821
  try {
1584
1822
  if (name === "gjc_coordinator_list_sessions") return { ok: true, sessions: await listSessions() };
@@ -1619,6 +1857,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
1619
1857
  };
1620
1858
  }
1621
1859
  if (name === "gjc_coordinator_read_status") {
1860
+ await reconcileActiveTurnAcknowledgements();
1622
1861
  const sessionId = args.session_id;
1623
1862
  if (sessionId) {
1624
1863
  const session = asRecord(await readJsonFile(sessionFile(sessionId)));
@@ -1651,6 +1890,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
1651
1890
  if (name === "gjc_coordinator_read_artifact")
1652
1891
  return await readCoordinatorArtifact(config, { path: args.path });
1653
1892
  if (name === "gjc_coordinator_read_coordination_status") {
1893
+ await reconcileActiveTurnAcknowledgements();
1654
1894
  const sessions = jsonRecords(await listSessions());
1655
1895
  const sessionStates = jsonRecords(await listJsonFiles(path.join(namespaceDir, "session-states")));
1656
1896
  const turns = jsonRecords(await listJsonFiles(turnsDir(namespaceDir)));
@@ -1684,13 +1924,15 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
1684
1924
  };
1685
1925
  }
1686
1926
  if (name === "gjc_coordinator_watch_events") {
1927
+ await reconcileActiveTurnAcknowledgements();
1687
1928
  const limit = boundedEventLimit(args.limit);
1688
- const timeoutMs = boundedTimeoutMs(args.timeout_ms);
1929
+ const timeoutMs = boundedEventWatchTimeoutMs(args.timeout_ms);
1689
1930
  let events = await readCoordinatorEvents(namespaceDir);
1690
1931
  let matched = filterCoordinatorEvents(events, args, limit);
1691
1932
  let timedOut = false;
1692
1933
  if (matched.length === 0 && timeoutMs > 0) {
1693
1934
  await waitForCoordinatorEvents(namespaceDir, timeoutMs);
1935
+ await reconcileActiveTurnAcknowledgements();
1694
1936
  events = await readCoordinatorEvents(namespaceDir);
1695
1937
  matched = filterCoordinatorEvents(events, args, limit);
1696
1938
  timedOut = matched.length === 0;
@@ -1819,7 +2061,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
1819
2061
  };
1820
2062
  if (promptAliasIgnored) base.prompt_alias_ignored = true;
1821
2063
  if (args.await_completion === true && !shouldQueue) {
1822
- const timeoutMs = boundedTimeoutMs(args.timeout_ms);
2064
+ const timeoutMs = boundedAwaitTurnTimeoutMs(args.timeout_ms);
1823
2065
  const pollIntervalMs = boundedPollIntervalMs(args.poll_interval_ms);
1824
2066
  const deadline = Date.now() + timeoutMs;
1825
2067
  let payload = await readTurnPayload(turn.turn_id, sessionId, args.lines);
@@ -1979,7 +2221,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
1979
2221
  return await readTurnPayload(args.turn_id, args.session_id, args.lines);
1980
2222
  }
1981
2223
  if (name === "gjc_coordinator_await_turn") {
1982
- const timeoutMs = boundedTimeoutMs(args.timeout_ms);
2224
+ const timeoutMs = boundedAwaitTurnTimeoutMs(args.timeout_ms);
1983
2225
  const pollIntervalMs = boundedPollIntervalMs(args.poll_interval_ms);
1984
2226
  const deadline = Date.now() + timeoutMs;
1985
2227
  let payload = await readTurnPayload(args.turn_id, args.session_id, args.lines);