@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.
- package/CHANGELOG.md +26 -1
- package/README.md +8 -0
- package/dist/types/cli/local-provider-smoke.d.ts +59 -0
- package/dist/types/cli/notify-cli.d.ts +1 -1
- package/dist/types/cli/update-cli.d.ts +24 -1
- package/dist/types/commands/local-provider.d.ts +34 -0
- package/dist/types/config/model-profiles.d.ts +2 -2
- package/dist/types/config/model-registry.d.ts +8 -3
- package/dist/types/config/models-config-schema.d.ts +5 -5
- package/dist/types/config/settings-schema.d.ts +123 -9
- package/dist/types/config/settings.d.ts +7 -0
- package/dist/types/coordinator-mcp/server.d.ts +8 -0
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/gjc-runtime/launch-tmux.d.ts +2 -2
- package/dist/types/gjc-runtime/state-renderer.d.ts +5 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +1 -1
- package/dist/types/gjc-runtime/tmux-common.d.ts +12 -0
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +37 -1
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +80 -0
- package/dist/types/modes/components/custom-model-preset-wizard.d.ts +1 -1
- package/dist/types/modes/components/model-selector.d.ts +2 -0
- package/dist/types/modes/components/settings-selector.d.ts +4 -1
- package/dist/types/modes/components/status-line/types.d.ts +5 -7
- package/dist/types/modes/components/status-line.d.ts +4 -3
- package/dist/types/modes/controllers/input-controller.d.ts +1 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
- package/dist/types/modes/prompt-action-autocomplete.d.ts +1 -0
- package/dist/types/modes/tmux-scroll.d.ts +18 -0
- package/dist/types/modes/utils/terminal-bell.d.ts +3 -0
- package/dist/types/notifications/telegram-daemon-cli.d.ts +13 -2
- package/dist/types/tools/browser/tab-supervisor.d.ts +31 -0
- package/dist/types/tools/computer-gc.d.ts +23 -0
- package/dist/types/tools/cron.d.ts +36 -61
- package/dist/types/tools/index.d.ts +0 -1
- package/dist/types/tools/resource-gc.d.ts +54 -0
- package/dist/types/web/search/index.d.ts +1 -0
- package/dist/types/web/search/providers/utils.d.ts +11 -4
- package/package.json +7 -7
- package/src/cli/args.ts +0 -1
- package/src/cli/fast-help.ts +0 -1
- package/src/cli/local-provider-smoke.ts +632 -0
- package/src/cli/notify-cli.ts +4 -2
- package/src/cli/update-cli.ts +155 -13
- package/src/cli/web-search-cli.ts +5 -0
- package/src/cli.ts +18 -0
- package/src/commands/launch.ts +46 -1
- package/src/commands/local-provider.ts +62 -0
- package/src/commands/notify.ts +1 -1
- package/src/config/model-profiles.ts +3 -4
- package/src/config/model-registry.ts +58 -7
- package/src/config/models-config-schema.ts +10 -1
- package/src/config/settings-schema.ts +127 -10
- package/src/config/settings.ts +11 -0
- package/src/coordinator-mcp/server.ts +276 -34
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +0 -12
- package/src/gjc-runtime/launch-tmux.ts +34 -8
- package/src/gjc-runtime/launch-worktree.ts +16 -2
- package/src/gjc-runtime/state-renderer.ts +13 -0
- package/src/gjc-runtime/team-runtime.ts +21 -6
- package/src/gjc-runtime/tmux-common.ts +16 -0
- package/src/gjc-runtime/tmux-sessions.ts +13 -9
- package/src/gjc-runtime/ultragoal-guard.ts +167 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +221 -1
- package/src/goals/tools/goal-tool.ts +14 -1
- package/src/internal-urls/docs-index.generated.ts +7 -7
- package/src/modes/components/agent-dashboard.ts +2 -1
- package/src/modes/components/custom-model-preset-wizard.ts +30 -189
- package/src/modes/components/model-selector.ts +182 -5
- package/src/modes/components/session-observer-overlay.ts +3 -3
- package/src/modes/components/settings-selector.ts +491 -5
- package/src/modes/components/status-line/segments.ts +7 -18
- package/src/modes/components/status-line/types.ts +6 -2
- package/src/modes/components/status-line.ts +69 -35
- package/src/modes/controllers/event-controller.ts +98 -4
- package/src/modes/controllers/extension-ui-controller.ts +3 -0
- package/src/modes/controllers/input-controller.ts +15 -0
- package/src/modes/controllers/selector-controller.ts +18 -4
- package/src/modes/prompt-action-autocomplete.ts +8 -0
- package/src/modes/tmux-scroll.ts +92 -0
- package/src/modes/utils/terminal-bell.ts +48 -0
- package/src/notifications/telegram-daemon-cli.ts +112 -9
- package/src/notifications/telegram-daemon.ts +4 -0
- package/src/prompts/system/system-prompt.md +2 -2
- package/src/prompts/tools/cron.md +5 -3
- package/src/prompts/tools/read.md +1 -1
- package/src/sdk.ts +5 -0
- package/src/session/agent-session.ts +118 -5
- package/src/slash-commands/builtin-registry.ts +106 -13
- package/src/tools/browser/tab-supervisor.ts +86 -2
- package/src/tools/computer-gc.ts +66 -0
- package/src/tools/computer.ts +2 -0
- package/src/tools/cron.ts +75 -112
- package/src/tools/index.ts +2 -8
- package/src/tools/read.ts +25 -55
- package/src/tools/renderers.ts +0 -2
- package/src/tools/resource-gc.ts +291 -0
- package/src/tools/ultragoal-ask-guard.ts +7 -1
- package/src/web/search/index.ts +1 -0
- package/src/web/search/providers/utils.ts +22 -5
- package/vendor/insane-search/MANIFEST.json +3 -1
- package/vendor/insane-search/engine/__init__.py +14 -0
- package/vendor/insane-search/engine/content_safety.py +151 -0
- package/vendor/insane-search/engine/fetch_chain.py +32 -0
- package/vendor/insane-search/engine/tests/test_u8.py +216 -0
- package/dist/types/tools/inspect-image-renderer.d.ts +0 -26
- package/dist/types/tools/inspect-image.d.ts +0 -31
- package/src/prompts/tools/inspect-image-system.md +0 -20
- package/src/prompts/tools/inspect-image.md +0 -32
- package/src/tools/inspect-image-renderer.ts +0 -103
- 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": {
|
package/src/config/settings.ts
CHANGED
|
@@ -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: {
|
|
321
|
-
|
|
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: {
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
1092
|
-
return Math.min(parsed, 30_000);
|
|
1238
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
1093
1239
|
}
|
|
1094
1240
|
|
|
1095
|
-
function
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
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
|
|
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
|
|
1489
|
-
sessionState
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
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:
|
|
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
|
|
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(
|
|
1539
|
-
sessionState.current_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,
|
|
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(
|
|
1547
|
-
sessionState.current_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
|
-
|
|
1725
|
+
resolvedTurn.delivery.state === "unavailable" &&
|
|
1551
1726
|
session &&
|
|
1552
1727
|
hasTmuxIdentity(session)
|
|
1553
1728
|
) {
|
|
1554
|
-
resolvedTurn = await markTurnFailedForUnavailableSession(
|
|
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(
|
|
1557
|
-
resolvedTurn = await markTurnFailedForUnavailableSession(namespaceDir,
|
|
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 (
|
|
1562
|
-
resolvedTurn
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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);
|