@gajae-code/coding-agent 0.11.10 → 0.12.0
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 +86 -2
- package/dist/types/cli/read-cli.d.ts +1 -0
- package/dist/types/cli.d.ts +8 -1
- package/dist/types/commands/deep-interview.d.ts +9 -0
- package/dist/types/commands/harness.d.ts +22 -0
- package/dist/types/commands/read.d.ts +6 -0
- package/dist/types/config/model-profile-activation.d.ts +1 -0
- package/dist/types/config/model-profile-contract.d.ts +2 -0
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/provider-auth-health.d.ts +14 -0
- package/dist/types/config/provider-ranking.d.ts +59 -0
- package/dist/types/config/settings-schema.d.ts +86 -3
- package/dist/types/config/settings.d.ts +3 -0
- package/dist/types/extensibility/extensions/types.d.ts +43 -2
- package/dist/types/extensibility/shared-events.d.ts +6 -0
- package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +5 -12
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +17 -32
- package/dist/types/gjc-runtime/deep-interview-stage.d.ts +37 -0
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +4 -135
- package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
- package/dist/types/gjc-runtime/linux-proc.d.ts +22 -14
- package/dist/types/gjc-runtime/memory-guard-owner-claims.d.ts +47 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +112 -1
- package/dist/types/gjc-runtime/state-runtime.d.ts +3 -1
- package/dist/types/gjc-runtime/state-writer.d.ts +5 -46
- package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
- package/dist/types/gjc-runtime/team-runtime.d.ts +2 -1
- package/dist/types/gjc-runtime/team-store.d.ts +12 -0
- package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +92 -0
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +7 -0
- package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +8 -0
- package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -5
- package/dist/types/internal-urls/local-root-gc.d.ts +2 -0
- package/dist/types/main.d.ts +1 -1
- package/dist/types/memories/index.d.ts +2 -0
- package/dist/types/modes/acp/acp-agent.d.ts +22 -9
- package/dist/types/modes/components/assistant-message.d.ts +3 -1
- package/dist/types/modes/components/irc-sidebar.d.ts +41 -0
- package/dist/types/modes/components/queue-pane.d.ts +8 -0
- package/dist/types/modes/components/queued-message-selector.d.ts +6 -0
- package/dist/types/modes/components/read-tool-group.d.ts +4 -4
- package/dist/types/modes/components/session-observer-overlay.d.ts +4 -0
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/modes/components/tool-execution.d.ts +8 -0
- package/dist/types/modes/components/welcome.d.ts +1 -0
- package/dist/types/modes/controllers/event-controller.d.ts +3 -0
- package/dist/types/modes/interactive-mode.d.ts +7 -1
- package/dist/types/modes/irc-observation-ledger.d.ts +17 -1
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +16 -0
- package/dist/types/runtime/memory-domain.d.ts +9 -0
- package/dist/types/runtime/memory-guard-contract.d.ts +71 -0
- package/dist/types/runtime/memory-guard.d.ts +39 -0
- package/dist/types/runtime/memory-limit.d.ts +11 -0
- package/dist/types/runtime-mcp/manager.d.ts +8 -0
- package/dist/types/runtime-mcp/smithery-auth.d.ts +5 -0
- package/dist/types/sdk/acp/final-text.d.ts +19 -0
- package/dist/types/sdk/acp/mcp.d.ts +23 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +7 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
- package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
- package/dist/types/sdk/bus/index.d.ts +18 -2
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +37 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +2 -1
- package/dist/types/sdk/bus/rate-limit-pool.d.ts +2 -0
- package/dist/types/sdk/bus/reconciliation-store.d.ts +65 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -4
- package/dist/types/sdk/bus/telegram-daemon.d.ts +23 -3
- package/dist/types/sdk/host/control/operations.d.ts +2 -1
- package/dist/types/sdk/host/host.d.ts +2 -0
- package/dist/types/sdk/host/query/handlers.d.ts +5 -0
- package/dist/types/sdk/lifecycle-session.d.ts +10 -1
- package/dist/types/sdk/startup-capability.d.ts +6 -0
- package/dist/types/session/agent-session.d.ts +51 -19
- package/dist/types/session/fallback-chain-controller.d.ts +18 -2
- package/dist/types/session/internal/managed-session-scope.d.ts +27 -0
- package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
- package/dist/types/session/session-manager.d.ts +59 -13
- package/dist/types/session/streaming-output.d.ts +60 -5
- package/dist/types/skill-state/active-state.d.ts +0 -4
- package/dist/types/skill-state/workflow-hud.d.ts +3 -0
- package/dist/types/task/executor.d.ts +6 -0
- package/dist/types/task/types.d.ts +36 -0
- package/dist/types/task/ultragoal-redteam-activation.d.ts +33 -0
- package/dist/types/tools/browser/launch.d.ts +7 -0
- package/dist/types/tools/fetch.d.ts +17 -1
- package/dist/types/tools/output-meta.d.ts +21 -1
- package/dist/types/tools/read-internals.d.ts +12 -0
- package/dist/types/tools/read.d.ts +45 -1
- package/dist/types/tools/resource-gc.d.ts +23 -0
- package/dist/types/tools/tool-result.d.ts +7 -1
- package/package.json +9 -13
- package/scripts/benchmark-sticky-viewport-pr1.ts +239 -0
- package/scripts/capture-sticky-viewport-showcase.ts +292 -0
- package/scripts/generate-sdk-operation-inventory.ts +4 -0
- package/scripts/run-test-manifest.ts +6 -4
- package/scripts/verify-gjc-sdk-canonicalization.ts +122 -18
- package/scripts/verify-sticky-viewport-showcase.ts +611 -0
- package/src/capability/index.ts +8 -0
- package/src/cli/args.ts +14 -8
- package/src/cli/fast-help.ts +1 -1
- package/src/cli/read-cli.ts +2 -1
- package/src/cli.ts +49 -2
- package/src/commands/deep-interview.ts +29 -9
- package/src/commands/harness.ts +35 -12
- package/src/commands/read.ts +11 -2
- package/src/commands/sdk.ts +91 -7
- package/src/commands/team.ts +1 -1
- package/src/config/model-profile-activation.ts +1 -0
- package/src/config/model-profile-contract.ts +13 -0
- package/src/config/model-profiles.ts +24 -0
- package/src/config/model-registry.ts +21 -2
- package/src/config/provider-auth-health.ts +42 -0
- package/src/config/provider-ranking.ts +119 -0
- package/src/config/settings-schema.ts +84 -3
- package/src/config/settings.ts +254 -108
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +229 -82
- package/src/defaults/gjc/skills/ralplan/SKILL.md +78 -26
- package/src/defaults/gjc/skills/team/SKILL.md +1 -1
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +26 -16
- package/src/eval/py/executor.ts +7 -2
- package/src/extensibility/extensions/runner.ts +4 -0
- package/src/extensibility/extensions/types.ts +42 -4
- package/src/extensibility/shared-events.ts +5 -0
- package/src/gjc-runtime/deep-interview-ambiguity.ts +47 -68
- package/src/gjc-runtime/deep-interview-recorder.ts +227 -310
- package/src/gjc-runtime/deep-interview-runtime.ts +109 -270
- package/src/gjc-runtime/deep-interview-stage.ts +1016 -0
- package/src/gjc-runtime/deep-interview-state.ts +12 -921
- package/src/gjc-runtime/gc-render.ts +1 -0
- package/src/gjc-runtime/gc-runtime.ts +11 -1
- package/src/gjc-runtime/linux-proc.ts +70 -36
- package/src/gjc-runtime/memory-guard-owner-claims.ts +389 -0
- package/src/gjc-runtime/ralplan-runtime.ts +979 -71
- package/src/gjc-runtime/state-runtime.ts +50 -12
- package/src/gjc-runtime/state-writer.ts +70 -222
- package/src/gjc-runtime/team-launch.ts +34 -5
- package/src/gjc-runtime/team-runtime.ts +970 -33
- package/src/gjc-runtime/team-store.ts +32 -0
- package/src/gjc-runtime/team-worker-memory-guard.ts +363 -0
- package/src/gjc-runtime/team-workers.ts +1 -0
- package/src/gjc-runtime/tmux-owner-isolation.ts +18 -0
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +20 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +13 -7
- package/src/gjc-runtime/workflow-manifest.generated.json +97 -350
- package/src/gjc-runtime/workflow-manifest.ts +34 -179
- package/src/hooks/native-skill-hook.ts +17 -4
- package/src/hooks/skill-state.ts +1 -23
- package/src/internal-urls/docs-index.generated.ts +10 -11
- package/src/internal-urls/local-protocol.ts +23 -4
- package/src/internal-urls/local-root-gc.ts +127 -0
- package/src/main.ts +9 -5
- package/src/memories/index.ts +10 -0
- package/src/modes/DESIGN.md +47 -0
- package/src/modes/acp/acp-agent.ts +597 -67
- package/src/modes/acp/acp-event-mapper.ts +106 -12
- package/src/modes/acp/acp-mode.ts +39 -3
- package/src/modes/components/assistant-message.ts +123 -22
- package/src/modes/components/irc-sidebar.ts +176 -62
- package/src/modes/components/model-selector.ts +76 -5
- package/src/modes/components/notifications-settings-editor.ts +1 -1
- package/src/modes/components/oauth-selector.ts +42 -12
- package/src/modes/components/queue-pane.ts +55 -9
- package/src/modes/components/queued-message-selector.ts +53 -10
- package/src/modes/components/read-tool-group.ts +82 -14
- package/src/modes/components/session-observer-overlay.ts +187 -32
- package/src/modes/components/settings-selector.ts +60 -18
- package/src/modes/components/tool-execution.ts +138 -27
- package/src/modes/components/tool-status-header.ts +1 -4
- package/src/modes/components/welcome.ts +18 -3
- package/src/modes/controllers/command-controller.ts +4 -0
- package/src/modes/controllers/event-controller.ts +109 -27
- package/src/modes/controllers/extension-ui-controller.ts +23 -12
- package/src/modes/controllers/input-controller.ts +62 -6
- package/src/modes/controllers/selector-controller.ts +83 -37
- package/src/modes/interactive-mode.ts +127 -29
- package/src/modes/irc-observation-ledger.ts +71 -15
- package/src/modes/runtime-init.ts +20 -2
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/ui-helpers.ts +94 -1
- package/src/prompts/agents/architect.md +9 -1
- package/src/prompts/agents/critic.md +9 -0
- package/src/prompts/agents/executor.md +1 -1
- package/src/prompts/system/system-prompt.md +0 -1
- package/src/prompts/tools/read.md +4 -2
- package/src/runtime/memory-domain.ts +66 -0
- package/src/runtime/memory-guard-contract.ts +73 -0
- package/src/runtime/memory-guard.ts +292 -0
- package/src/runtime/memory-limit.ts +50 -0
- package/src/runtime-mcp/manager.ts +26 -6
- package/src/runtime-mcp/smithery-auth.ts +17 -2
- package/src/runtime-mcp/smithery-connect.ts +11 -1
- package/src/sdk/acp/adapter.ts +8 -1
- package/src/sdk/acp/final-text.ts +40 -0
- package/src/sdk/acp/mcp.ts +26 -0
- package/src/sdk/broker/lifecycle.ts +107 -7
- package/src/sdk/bus/chat-daemon-cli.ts +2 -2
- package/src/sdk/bus/chat-daemon-control.ts +4 -3
- package/src/sdk/bus/config.ts +1 -1
- package/src/sdk/bus/control-drain-lease.ts +104 -0
- package/src/sdk/bus/index.ts +503 -60
- package/src/sdk/bus/kind-aware-reconciliation.ts +242 -0
- package/src/sdk/bus/prompt-reconciliation.ts +2 -1
- package/src/sdk/bus/rate-limit-pool.ts +8 -0
- package/src/sdk/bus/reconciliation-store.ts +224 -0
- package/src/sdk/bus/telegram-daemon-contract.ts +6 -4
- package/src/sdk/bus/telegram-daemon.ts +531 -53
- package/src/sdk/host/control/dispatch.ts +7 -1
- package/src/sdk/host/control/operations.ts +2 -1
- package/src/sdk/host/host.ts +25 -3
- package/src/sdk/host/query/handlers.ts +55 -0
- package/src/sdk/lifecycle-session.ts +19 -3
- package/src/sdk/mcp/server.ts +46 -18
- package/src/sdk/protocol/operation-inventory.generated.json +76 -0
- package/src/sdk/protocol/operation-registry.ts +8 -2
- package/src/sdk/session.ts +25 -2
- package/src/sdk/startup-capability.ts +6 -0
- package/src/session/agent-session.ts +720 -190
- package/src/session/fallback-chain-controller.ts +47 -8
- package/src/session/internal/managed-session-scope.ts +40 -10
- package/src/session/internal/native-publish-outcome.ts +8 -3
- package/src/session/memory-guard-checkpoint-participant.ts +131 -0
- package/src/session/session-manager.ts +1372 -42
- package/src/session/streaming-output.ts +262 -43
- package/src/setup/provider-onboarding.ts +19 -4
- package/src/skill-state/active-state.ts +0 -10
- package/src/skill-state/workflow-hud.ts +25 -2
- package/src/skill-state/workflow-mutation-guard.ts +65 -7
- package/src/task/executor.ts +11 -3
- package/src/task/index.ts +2 -0
- package/src/task/types.ts +22 -11
- package/src/task/ultragoal-redteam-activation.ts +75 -0
- package/src/tools/ask.ts +89 -69
- package/src/tools/browser/launch.ts +41 -6
- package/src/tools/fetch.ts +114 -23
- package/src/tools/output-meta.ts +176 -9
- package/src/tools/path-utils.ts +5 -1
- package/src/tools/read-internals.ts +44 -0
- package/src/tools/read.ts +1542 -310
- package/src/tools/resource-gc.ts +665 -125
- package/src/tools/tool-result.ts +14 -1
- package/src/web/search/providers/codex.ts +1 -1
- package/src/workflow/workflow-intent-diff.ts +4 -1
- package/dist/types/gjc-runtime/deep-interview-draft.d.ts +0 -8
- package/dist/types/gjc-runtime/deep-interview-payload.d.ts +0 -41
- package/dist/types/gjc-runtime/deep-interview-repair.d.ts +0 -17
- package/src/gjc-runtime/deep-interview-draft.ts +0 -843
- package/src/gjc-runtime/deep-interview-payload.ts +0 -262
- package/src/gjc-runtime/deep-interview-repair.ts +0 -1674
|
@@ -43,6 +43,7 @@ import type {
|
|
|
43
43
|
GjcTeamTaskStatus,
|
|
44
44
|
} from "./team-store";
|
|
45
45
|
import {
|
|
46
|
+
findGjcTeamClaimedTaskForWorker,
|
|
46
47
|
GjcTeamTaskStore,
|
|
47
48
|
isCanonicalPersistedGjcTeamTask,
|
|
48
49
|
isCanonicalPersistedGjcTeamTaskClaim,
|
|
@@ -52,7 +53,19 @@ import {
|
|
|
52
53
|
withGjcTeamMutationFence,
|
|
53
54
|
withGjcTeamTaskMutation,
|
|
54
55
|
} from "./team-store";
|
|
55
|
-
|
|
56
|
+
import {
|
|
57
|
+
appendTeamWorkerMemoryGuardLedgerEntry,
|
|
58
|
+
createInitialGjcTeamWorkerMemoryGuardLedger,
|
|
59
|
+
type GjcTeamWorkerMemoryGuardCheckpoint,
|
|
60
|
+
type GjcTeamWorkerMemoryGuardLedger,
|
|
61
|
+
type GjcTeamWorkerMemoryGuardSelectionCandidate,
|
|
62
|
+
isCanonicalGjcTeamWorkerMemoryGuardLedger,
|
|
63
|
+
nextTeamWorkerMemoryGuardAttempt,
|
|
64
|
+
normalizeGjcTeamWorkerMemoryGuardPidProbe,
|
|
65
|
+
readTeamWorkerMemoryGuardLedger,
|
|
66
|
+
selectGjcTeamWorkerMemoryGuardCandidate,
|
|
67
|
+
workerMemoryGuardLedgerPath,
|
|
68
|
+
} from "./team-worker-memory-guard";
|
|
56
69
|
import {
|
|
57
70
|
GJC_TEAM_CONTINUATION_PROMPT,
|
|
58
71
|
type GjcTeamWorkerOrchestrationRuntime,
|
|
@@ -70,10 +83,10 @@ import {
|
|
|
70
83
|
updateGjcWorkerHeartbeat as updateWorkerHeartbeat,
|
|
71
84
|
updateGjcWorkerStatus as updateWorkerStatus,
|
|
72
85
|
writeWorkerLifecycleForConfig as writeLifecycleForConfig,
|
|
86
|
+
writeWorkerLifecycleRecord as writeLifecycleRecord,
|
|
73
87
|
writeGjcShutdownRequest as writeShutdownRequest,
|
|
74
88
|
writeGjcWorkerStartupAck as writeWorkerStartupAck,
|
|
75
89
|
} from "./team-workers";
|
|
76
|
-
|
|
77
90
|
import {
|
|
78
91
|
buildGjcTmuxExactOptionTarget,
|
|
79
92
|
buildGjcTmuxUntaggedSessionHint,
|
|
@@ -435,6 +448,7 @@ export interface WorkerHeartbeatFile {
|
|
|
435
448
|
last_turn_at: string;
|
|
436
449
|
turn_count: number;
|
|
437
450
|
alive: boolean;
|
|
451
|
+
process_start_time?: string;
|
|
438
452
|
}
|
|
439
453
|
interface GitResult {
|
|
440
454
|
ok: boolean;
|
|
@@ -520,6 +534,9 @@ export const GJC_TEAM_API_OPERATIONS = [
|
|
|
520
534
|
"read-worker-heartbeat",
|
|
521
535
|
"recover-stale-claims",
|
|
522
536
|
"update-worker-heartbeat",
|
|
537
|
+
"read-worker-memory-guard",
|
|
538
|
+
"update-worker-memory-guard",
|
|
539
|
+
"apply-worker-memory-guard",
|
|
523
540
|
"write-worker-inbox",
|
|
524
541
|
"write-worker-identity",
|
|
525
542
|
"append-event",
|
|
@@ -1218,6 +1235,821 @@ async function readConfigForWorkerIntegration(dir: string): Promise<GjcTeamConfi
|
|
|
1218
1235
|
|
|
1219
1236
|
return config;
|
|
1220
1237
|
}
|
|
1238
|
+
function manifestPath(dir: string): string {
|
|
1239
|
+
return path.join(dir, "manifest.v2.json");
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
function manifestRecordFromConfig(config: GjcTeamConfig): Record<string, unknown> {
|
|
1243
|
+
return {
|
|
1244
|
+
version: 2,
|
|
1245
|
+
team_name: config.team_name,
|
|
1246
|
+
display_name: config.display_name,
|
|
1247
|
+
requested_name: config.requested_name,
|
|
1248
|
+
tmux_session: config.tmux_session,
|
|
1249
|
+
tmux_session_name: config.tmux_session_name,
|
|
1250
|
+
tmux_target: config.tmux_target,
|
|
1251
|
+
worker_command: config.worker_command,
|
|
1252
|
+
worker_cli_plan: config.worker_cli_plan,
|
|
1253
|
+
tmux_command: config.tmux_command,
|
|
1254
|
+
leader: config.leader,
|
|
1255
|
+
workers: config.workers,
|
|
1256
|
+
workspace_mode: config.workspace_mode,
|
|
1257
|
+
dry_run: config.dry_run,
|
|
1258
|
+
created_at: config.created_at,
|
|
1259
|
+
updated_at: config.updated_at,
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
async function syncTeamConfigAndManifest(dir: string, config: GjcTeamConfig): Promise<void> {
|
|
1264
|
+
await writeJsonFile(path.join(dir, "config.json"), config);
|
|
1265
|
+
await writeJsonFile(manifestPath(dir), manifestRecordFromConfig(config));
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
async function readWorkerMemoryGuardLedger(
|
|
1269
|
+
dir: string,
|
|
1270
|
+
workerId: string,
|
|
1271
|
+
platform: string,
|
|
1272
|
+
): Promise<GjcTeamWorkerMemoryGuardLedger> {
|
|
1273
|
+
const ledgerPath = workerMemoryGuardLedgerPath(dir, workerId);
|
|
1274
|
+
const existing = await readJsonFile<unknown>(ledgerPath);
|
|
1275
|
+
if (isCanonicalGjcTeamWorkerMemoryGuardLedger(existing) && existing.worker_id === workerId) return existing;
|
|
1276
|
+
const ledger = createInitialGjcTeamWorkerMemoryGuardLedger({ workerId, platform, now: now() });
|
|
1277
|
+
await writeJsonFile(ledgerPath, ledger);
|
|
1278
|
+
return ledger;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
async function writeWorkerMemoryGuardLedger(
|
|
1282
|
+
dir: string,
|
|
1283
|
+
ledger: GjcTeamWorkerMemoryGuardLedger,
|
|
1284
|
+
): Promise<GjcTeamWorkerMemoryGuardLedger> {
|
|
1285
|
+
await writeJsonFile(workerMemoryGuardLedgerPath(dir, ledger.worker_id), ledger);
|
|
1286
|
+
return ledger;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
async function appendWorkerMemoryGuardAction(input: {
|
|
1290
|
+
dir: string;
|
|
1291
|
+
teamName: string;
|
|
1292
|
+
cwd: string;
|
|
1293
|
+
workerId: string;
|
|
1294
|
+
task?: GjcTeamTask;
|
|
1295
|
+
incidentId: string;
|
|
1296
|
+
action: "advisory" | "replace" | "blocked";
|
|
1297
|
+
result: "noop" | "scheduled" | "succeeded" | "failed" | "blocked";
|
|
1298
|
+
reason: string;
|
|
1299
|
+
}): Promise<void> {
|
|
1300
|
+
const workerPath = path.join(input.dir, "workers", safePathSegment("worker_id", input.workerId));
|
|
1301
|
+
const entries = await readTeamWorkerMemoryGuardLedger(workerPath);
|
|
1302
|
+
await appendTeamWorkerMemoryGuardLedgerEntry(
|
|
1303
|
+
workerPath,
|
|
1304
|
+
{
|
|
1305
|
+
schema_version: 1,
|
|
1306
|
+
recorded_at: now(),
|
|
1307
|
+
incident_id: input.incidentId,
|
|
1308
|
+
team_name: input.teamName,
|
|
1309
|
+
worker_id: input.workerId,
|
|
1310
|
+
task_id: input.task?.id ?? "unclaimed",
|
|
1311
|
+
claim_token: input.task?.claim?.token ?? "unclaimed",
|
|
1312
|
+
attempt: nextTeamWorkerMemoryGuardAttempt(entries, input.incidentId),
|
|
1313
|
+
platform: process.platform,
|
|
1314
|
+
action: input.action,
|
|
1315
|
+
result: input.result,
|
|
1316
|
+
reason: input.reason,
|
|
1317
|
+
},
|
|
1318
|
+
{ cwd: input.cwd },
|
|
1319
|
+
);
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
function normalizeWorkerMemoryGuardPlatform(value: unknown): string {
|
|
1323
|
+
return typeof value === "string" && value.trim() ? value.trim() : process.platform;
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
async function readLinuxProcessStartTime(pid: number): Promise<string | undefined> {
|
|
1327
|
+
if (process.platform !== "linux" || !Number.isSafeInteger(pid) || pid <= 0) return undefined;
|
|
1328
|
+
try {
|
|
1329
|
+
const stat = await Bun.file(`/proc/${pid}/stat`).text();
|
|
1330
|
+
const commandEnd = stat.lastIndexOf(")");
|
|
1331
|
+
if (commandEnd < 0) return undefined;
|
|
1332
|
+
const fields = stat
|
|
1333
|
+
.slice(commandEnd + 2)
|
|
1334
|
+
.trim()
|
|
1335
|
+
.split(/\s+/);
|
|
1336
|
+
const startTime = fields[19];
|
|
1337
|
+
return startTime && /^\d+$/.test(startTime) ? startTime : undefined;
|
|
1338
|
+
} catch {
|
|
1339
|
+
return undefined;
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
function normalizeWorkerMemoryGuardSelectionCandidates(
|
|
1344
|
+
value: unknown,
|
|
1345
|
+
ledgers: Map<string, GjcTeamWorkerMemoryGuardLedger>,
|
|
1346
|
+
): GjcTeamWorkerMemoryGuardSelectionCandidate[] {
|
|
1347
|
+
if (!Array.isArray(value)) return [];
|
|
1348
|
+
const candidates: GjcTeamWorkerMemoryGuardSelectionCandidate[] = [];
|
|
1349
|
+
for (const entry of value) {
|
|
1350
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
|
|
1351
|
+
const record = entry as Record<string, unknown>;
|
|
1352
|
+
const workerId =
|
|
1353
|
+
typeof record.worker_id === "string"
|
|
1354
|
+
? record.worker_id.trim()
|
|
1355
|
+
: typeof record.workerId === "string"
|
|
1356
|
+
? record.workerId.trim()
|
|
1357
|
+
: "";
|
|
1358
|
+
if (!workerId) continue;
|
|
1359
|
+
const ledger = ledgers.get(workerId);
|
|
1360
|
+
const excessBytes = Number(record.excess_bytes ?? record.excessBytes ?? 0);
|
|
1361
|
+
if (!Number.isFinite(excessBytes)) continue;
|
|
1362
|
+
candidates.push({
|
|
1363
|
+
worker_id: workerId,
|
|
1364
|
+
platform:
|
|
1365
|
+
typeof record.platform === "string" && record.platform.trim()
|
|
1366
|
+
? record.platform.trim()
|
|
1367
|
+
: (ledger?.platform ?? process.platform),
|
|
1368
|
+
excess_bytes: excessBytes,
|
|
1369
|
+
retry_count:
|
|
1370
|
+
typeof record.retry_count === "number" && Number.isInteger(record.retry_count)
|
|
1371
|
+
? record.retry_count
|
|
1372
|
+
: (ledger?.retry_count ?? 0),
|
|
1373
|
+
retry_limit:
|
|
1374
|
+
typeof record.retry_limit === "number" && Number.isInteger(record.retry_limit) && record.retry_limit > 0
|
|
1375
|
+
? record.retry_limit
|
|
1376
|
+
: (ledger?.retry_limit ?? 2),
|
|
1377
|
+
blocked: typeof record.blocked === "boolean" ? record.blocked : ledger?.state === "blocked",
|
|
1378
|
+
current_task_id:
|
|
1379
|
+
typeof record.current_task_id === "string"
|
|
1380
|
+
? record.current_task_id
|
|
1381
|
+
: typeof record.currentTaskId === "string"
|
|
1382
|
+
? record.currentTaskId
|
|
1383
|
+
: ledger?.current_task_id,
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
return candidates;
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
function withMemoryGuardRetry(
|
|
1390
|
+
ledger: GjcTeamWorkerMemoryGuardLedger,
|
|
1391
|
+
input: {
|
|
1392
|
+
platform: string;
|
|
1393
|
+
reason: string;
|
|
1394
|
+
incidentId: string;
|
|
1395
|
+
currentTaskId?: string;
|
|
1396
|
+
pidProbe?: GjcTeamWorkerMemoryGuardLedger["last_pid_probe"];
|
|
1397
|
+
nowIso: string;
|
|
1398
|
+
},
|
|
1399
|
+
): { ledger: GjcTeamWorkerMemoryGuardLedger; finalBlocked: boolean } {
|
|
1400
|
+
const retryCount = Math.min(ledger.retry_count + 1, ledger.retry_limit);
|
|
1401
|
+
const finalBlocked = retryCount >= ledger.retry_limit;
|
|
1402
|
+
return {
|
|
1403
|
+
ledger: {
|
|
1404
|
+
...ledger,
|
|
1405
|
+
platform: input.platform,
|
|
1406
|
+
state: finalBlocked ? "blocked" : "retrying",
|
|
1407
|
+
retry_count: retryCount,
|
|
1408
|
+
current_task_id: input.currentTaskId,
|
|
1409
|
+
last_incident_id: input.incidentId,
|
|
1410
|
+
last_reason: input.reason,
|
|
1411
|
+
last_pid_probe: input.pidProbe,
|
|
1412
|
+
updated_at: input.nowIso,
|
|
1413
|
+
},
|
|
1414
|
+
finalBlocked,
|
|
1415
|
+
};
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
async function finalizeWorkerMemoryGuardBlockedState(input: {
|
|
1419
|
+
teamName: string;
|
|
1420
|
+
dir: string;
|
|
1421
|
+
worker: GjcTeamWorker;
|
|
1422
|
+
task: GjcTeamTask | undefined;
|
|
1423
|
+
taskMutation: GjcTeamTaskMutationCapability;
|
|
1424
|
+
reason: string;
|
|
1425
|
+
cwd: string;
|
|
1426
|
+
env: NodeJS.ProcessEnv;
|
|
1427
|
+
}): Promise<void> {
|
|
1428
|
+
await updateWorkerStatus(
|
|
1429
|
+
workerRuntime,
|
|
1430
|
+
input.teamName,
|
|
1431
|
+
input.worker.id,
|
|
1432
|
+
"blocked",
|
|
1433
|
+
input.cwd,
|
|
1434
|
+
input.env,
|
|
1435
|
+
input.task?.id,
|
|
1436
|
+
input.reason,
|
|
1437
|
+
);
|
|
1438
|
+
await writeLifecycleRecord(workerRuntime, input.dir, input.worker, "failed", { stop_reason: input.reason });
|
|
1439
|
+
const task = input.task;
|
|
1440
|
+
if (!task?.claim) return;
|
|
1441
|
+
await input.taskMutation.transition(task.id, "blocked", task.claim.token, input.worker.id);
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
async function checkpointWorkerForMemoryGuard(
|
|
1445
|
+
worker: GjcTeamWorker,
|
|
1446
|
+
taskId: string | undefined,
|
|
1447
|
+
): Promise<{ ok: true; checkpoint: GjcTeamWorkerMemoryGuardCheckpoint } | { ok: false; reason: string }> {
|
|
1448
|
+
if (!worker.worktree_path) return { ok: false, reason: "worker_worktree_missing" };
|
|
1449
|
+
let classification: GjcWorkerCheckpointClassification;
|
|
1450
|
+
try {
|
|
1451
|
+
classification = classifyWorkerCheckpointStatus(worker.worktree_path);
|
|
1452
|
+
} catch {
|
|
1453
|
+
return { ok: false, reason: "checkpoint_git_error" };
|
|
1454
|
+
}
|
|
1455
|
+
if (
|
|
1456
|
+
classification.kind !== "clean" &&
|
|
1457
|
+
classification.kind !== "eligible" &&
|
|
1458
|
+
classification.kind !== "protected_only"
|
|
1459
|
+
)
|
|
1460
|
+
return { ok: false, reason: `checkpoint_${classification.kind}` };
|
|
1461
|
+
let commit: string | null = null;
|
|
1462
|
+
if (classification.kind === "eligible") {
|
|
1463
|
+
const added = runGitResult(worker.worktree_path, ["add", "--", ...classification.files]);
|
|
1464
|
+
if (!added.ok) return { ok: false, reason: "checkpoint_git_add_failed" };
|
|
1465
|
+
const message = `gjc(team): memory-guard checkpoint ${worker.id} [${taskId ?? "unknown"}]`;
|
|
1466
|
+
const committed = runGitResult(worker.worktree_path, [
|
|
1467
|
+
"commit",
|
|
1468
|
+
"--no-verify",
|
|
1469
|
+
"--only",
|
|
1470
|
+
"-m",
|
|
1471
|
+
message,
|
|
1472
|
+
"--",
|
|
1473
|
+
...classification.files,
|
|
1474
|
+
]);
|
|
1475
|
+
if (!committed.ok) return { ok: false, reason: "checkpoint_git_commit_failed" };
|
|
1476
|
+
commit = resolveHead(worker.worktree_path);
|
|
1477
|
+
}
|
|
1478
|
+
return {
|
|
1479
|
+
ok: true,
|
|
1480
|
+
checkpoint: {
|
|
1481
|
+
kind: classification.kind,
|
|
1482
|
+
files: classification.files,
|
|
1483
|
+
head: resolveHead(worker.worktree_path),
|
|
1484
|
+
commit,
|
|
1485
|
+
recorded_at: now(),
|
|
1486
|
+
},
|
|
1487
|
+
};
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
async function relaunchWorkerPaneForMemoryGuard(input: {
|
|
1491
|
+
config: GjcTeamConfig;
|
|
1492
|
+
worker: GjcTeamWorker;
|
|
1493
|
+
platform: NodeJS.Platform;
|
|
1494
|
+
startupAckPath: string;
|
|
1495
|
+
replacementToken: string;
|
|
1496
|
+
startupAckTimeoutMs: number;
|
|
1497
|
+
}): Promise<string> {
|
|
1498
|
+
if (input.config.dry_run)
|
|
1499
|
+
return `%memory-guard-${input.worker.id}-${stableHash(`${input.worker.id}:${now()}`).slice(0, 8)}`;
|
|
1500
|
+
const workerCommand = buildWorkerCommand(
|
|
1501
|
+
input.config,
|
|
1502
|
+
input.worker,
|
|
1503
|
+
input.platform,
|
|
1504
|
+
`Send startup ACK before resuming: gjc team api worker-startup-ack --input '{"team_name":"${input.config.team_name}","worker_id":"${input.worker.id}","protocol_version":"1","replacement_token":"${input.replacementToken}"}' --json. ${GJC_TEAM_CONTINUATION_PROMPT}`,
|
|
1505
|
+
);
|
|
1506
|
+
const workerCwd = input.worker.worktree_path ?? input.config.leader.cwd;
|
|
1507
|
+
const useSendKeysFallback = shouldDispatchWorkerWithSendKeys(input.config.tmux_command, input.platform);
|
|
1508
|
+
const splitTarget =
|
|
1509
|
+
input.worker.pane_id && paneBelongsToTeamTarget(input.config, input.worker.pane_id)
|
|
1510
|
+
? input.worker.pane_id
|
|
1511
|
+
: input.config.tmux_target;
|
|
1512
|
+
const split = Bun.spawnSync(
|
|
1513
|
+
[
|
|
1514
|
+
input.config.tmux_command,
|
|
1515
|
+
"split-window",
|
|
1516
|
+
"-v",
|
|
1517
|
+
"-t",
|
|
1518
|
+
splitTarget,
|
|
1519
|
+
"-d",
|
|
1520
|
+
"-P",
|
|
1521
|
+
"-F",
|
|
1522
|
+
"#{pane_id}",
|
|
1523
|
+
"-c",
|
|
1524
|
+
workerCwd,
|
|
1525
|
+
...(useSendKeysFallback ? [] : [workerCommand]),
|
|
1526
|
+
],
|
|
1527
|
+
{ stdout: "pipe", stderr: "pipe" },
|
|
1528
|
+
);
|
|
1529
|
+
if (split.exitCode !== 0)
|
|
1530
|
+
throw new Error(split.stderr.toString().trim() || `memory_guard_split_failed:${input.worker.id}`);
|
|
1531
|
+
const newPaneId = split.stdout.toString().trim().split(/\r?\n/)[0]?.trim() ?? "";
|
|
1532
|
+
if (!newPaneId.startsWith("%")) throw new Error(`memory_guard_split_missing_pane:${input.worker.id}`);
|
|
1533
|
+
if (useSendKeysFallback) {
|
|
1534
|
+
Bun.spawnSync([input.config.tmux_command, "send-keys", "-l", "-t", newPaneId, workerCommand], {
|
|
1535
|
+
stdout: "ignore",
|
|
1536
|
+
stderr: "ignore",
|
|
1537
|
+
});
|
|
1538
|
+
Bun.spawnSync([input.config.tmux_command, "send-keys", "-t", newPaneId, "Enter"], {
|
|
1539
|
+
stdout: "ignore",
|
|
1540
|
+
stderr: "ignore",
|
|
1541
|
+
});
|
|
1542
|
+
}
|
|
1543
|
+
const startupDeadline = Date.now() + input.startupAckTimeoutMs;
|
|
1544
|
+
while (true) {
|
|
1545
|
+
try {
|
|
1546
|
+
const ack = await Bun.file(input.startupAckPath).json();
|
|
1547
|
+
if (
|
|
1548
|
+
ack &&
|
|
1549
|
+
typeof ack === "object" &&
|
|
1550
|
+
"replacement_token" in ack &&
|
|
1551
|
+
ack.replacement_token === input.replacementToken
|
|
1552
|
+
)
|
|
1553
|
+
break;
|
|
1554
|
+
} catch {
|
|
1555
|
+
// The successor may still be publishing its generation-bound ACK.
|
|
1556
|
+
}
|
|
1557
|
+
if (Date.now() >= startupDeadline) {
|
|
1558
|
+
Bun.spawnSync([input.config.tmux_command, "kill-pane", "-t", newPaneId], {
|
|
1559
|
+
stdout: "ignore",
|
|
1560
|
+
stderr: "ignore",
|
|
1561
|
+
});
|
|
1562
|
+
throw new Error(`memory_guard_successor_startup_timeout:${input.worker.id}`);
|
|
1563
|
+
}
|
|
1564
|
+
await Bun.sleep(50);
|
|
1565
|
+
}
|
|
1566
|
+
const successorPane = probePaneTeamTarget(input.config, newPaneId);
|
|
1567
|
+
if (!successorPane.exists || !successorPane.belongsToTeamTarget) {
|
|
1568
|
+
Bun.spawnSync([input.config.tmux_command, "kill-pane", "-t", newPaneId], {
|
|
1569
|
+
stdout: "ignore",
|
|
1570
|
+
stderr: "ignore",
|
|
1571
|
+
});
|
|
1572
|
+
throw new Error(`memory_guard_successor_pane_unavailable:${input.worker.id}`);
|
|
1573
|
+
}
|
|
1574
|
+
if (input.worker.pane_id) {
|
|
1575
|
+
const oldPane = probePaneTeamTarget(input.config, input.worker.pane_id);
|
|
1576
|
+
if (oldPane.exists && !oldPane.belongsToTeamTarget) {
|
|
1577
|
+
Bun.spawnSync([input.config.tmux_command, "kill-pane", "-t", newPaneId], {
|
|
1578
|
+
stdout: "ignore",
|
|
1579
|
+
stderr: "ignore",
|
|
1580
|
+
});
|
|
1581
|
+
throw new Error(`memory_guard_old_pane_outside_team_target:${input.worker.id}`);
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
Bun.spawnSync([input.config.tmux_command, "select-layout", "-t", input.config.tmux_target, "main-vertical"], {
|
|
1585
|
+
stdout: "ignore",
|
|
1586
|
+
stderr: "ignore",
|
|
1587
|
+
});
|
|
1588
|
+
return newPaneId;
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
async function applyWorkerMemoryGuardUnlocked(input: {
|
|
1592
|
+
teamName: string;
|
|
1593
|
+
workerId?: string;
|
|
1594
|
+
requestedWorkerId: string;
|
|
1595
|
+
reason?: string;
|
|
1596
|
+
incidentId?: string;
|
|
1597
|
+
platform: string;
|
|
1598
|
+
pidProbe?: GjcTeamWorkerMemoryGuardLedger["last_pid_probe"];
|
|
1599
|
+
candidates?: unknown;
|
|
1600
|
+
cwd: string;
|
|
1601
|
+
env: NodeJS.ProcessEnv;
|
|
1602
|
+
allowAutomaticAction?: boolean;
|
|
1603
|
+
replacementToken?: string;
|
|
1604
|
+
dir: string;
|
|
1605
|
+
taskMutation: GjcTeamTaskMutationCapability;
|
|
1606
|
+
}): Promise<Record<string, unknown>> {
|
|
1607
|
+
const dir = input.dir;
|
|
1608
|
+
const config = await readConfig(dir);
|
|
1609
|
+
const ledgers = new Map<string, GjcTeamWorkerMemoryGuardLedger>();
|
|
1610
|
+
for (const candidate of config.workers)
|
|
1611
|
+
ledgers.set(candidate.id, await readWorkerMemoryGuardLedger(dir, candidate.id, input.platform));
|
|
1612
|
+
let workerId = input.workerId ?? input.requestedWorkerId;
|
|
1613
|
+
const parsedCandidates = normalizeWorkerMemoryGuardSelectionCandidates(input.candidates, ledgers);
|
|
1614
|
+
if (!input.workerId && Array.isArray(input.candidates)) {
|
|
1615
|
+
const selected = selectGjcTeamWorkerMemoryGuardCandidate(parsedCandidates);
|
|
1616
|
+
if (!selected)
|
|
1617
|
+
return {
|
|
1618
|
+
ok: true,
|
|
1619
|
+
result: "advisory",
|
|
1620
|
+
lifecycle_mutated: false,
|
|
1621
|
+
reason: "no_eligible_worker_memory_guard_candidate",
|
|
1622
|
+
};
|
|
1623
|
+
workerId = selected.worker_id;
|
|
1624
|
+
}
|
|
1625
|
+
const worker = findKnownWorker(config, workerId);
|
|
1626
|
+
const authorityInventory = await readGjcContinuationAuthorityInventory(dir);
|
|
1627
|
+
const authority = authorityInventory.valid
|
|
1628
|
+
? selectGjcContinuationWorkerAuthority(authorityInventory, worker.id)
|
|
1629
|
+
: { valid: false as const, taskCount: 0, claimCount: 0 };
|
|
1630
|
+
const task = authority.valid ? authority.task : undefined;
|
|
1631
|
+
const leaseExpiresAt = authority.valid ? Date.parse(authority.claim.leased_until) : Number.NaN;
|
|
1632
|
+
let ledger = ledgers.get(worker.id) ?? (await readWorkerMemoryGuardLedger(dir, worker.id, input.platform));
|
|
1633
|
+
const nowIso = now();
|
|
1634
|
+
const currentTaskId = task?.id;
|
|
1635
|
+
const incidentId = input.incidentId ?? stableHash(`${worker.id}:${currentTaskId ?? "none"}:${nowIso}`).slice(0, 16);
|
|
1636
|
+
const baseReason = input.reason?.trim() || "memory_guard_requested";
|
|
1637
|
+
if (!authority.valid || !Number.isFinite(leaseExpiresAt) || leaseExpiresAt <= currentTimeMs()) {
|
|
1638
|
+
const noClaimReason =
|
|
1639
|
+
process.platform !== "linux" || input.platform !== "linux"
|
|
1640
|
+
? `unsupported_platform:${input.platform}:host:${process.platform}:${baseReason}`
|
|
1641
|
+
: "worker_has_no_exact_active_claim";
|
|
1642
|
+
ledger = {
|
|
1643
|
+
...ledger,
|
|
1644
|
+
platform: input.platform,
|
|
1645
|
+
state: "advisory",
|
|
1646
|
+
current_task_id: undefined,
|
|
1647
|
+
last_incident_id: incidentId,
|
|
1648
|
+
last_reason: noClaimReason,
|
|
1649
|
+
last_pid_probe: input.pidProbe,
|
|
1650
|
+
updated_at: nowIso,
|
|
1651
|
+
};
|
|
1652
|
+
await writeWorkerMemoryGuardLedger(dir, ledger);
|
|
1653
|
+
await appendWorkerMemoryGuardAction({
|
|
1654
|
+
dir,
|
|
1655
|
+
teamName: input.teamName,
|
|
1656
|
+
cwd: input.cwd,
|
|
1657
|
+
workerId: worker.id,
|
|
1658
|
+
task,
|
|
1659
|
+
incidentId,
|
|
1660
|
+
action: "advisory",
|
|
1661
|
+
result: "noop",
|
|
1662
|
+
reason: noClaimReason,
|
|
1663
|
+
});
|
|
1664
|
+
return {
|
|
1665
|
+
ok: true,
|
|
1666
|
+
result: "advisory",
|
|
1667
|
+
lifecycle_mutated: false,
|
|
1668
|
+
ledger,
|
|
1669
|
+
reason: noClaimReason,
|
|
1670
|
+
};
|
|
1671
|
+
}
|
|
1672
|
+
if (ledger.state === "blocked" || ledger.retry_count >= ledger.retry_limit) {
|
|
1673
|
+
await appendWorkerMemoryGuardAction({
|
|
1674
|
+
dir,
|
|
1675
|
+
teamName: input.teamName,
|
|
1676
|
+
cwd: input.cwd,
|
|
1677
|
+
workerId: worker.id,
|
|
1678
|
+
task,
|
|
1679
|
+
incidentId,
|
|
1680
|
+
action: "blocked",
|
|
1681
|
+
result: "blocked",
|
|
1682
|
+
reason: ledger.last_reason ?? "retry_limit",
|
|
1683
|
+
});
|
|
1684
|
+
return { ok: true, result: "blocked", lifecycle_mutated: false, ledger };
|
|
1685
|
+
}
|
|
1686
|
+
if (input.allowAutomaticAction !== undefined)
|
|
1687
|
+
ledger = {
|
|
1688
|
+
...ledger,
|
|
1689
|
+
automatic_action_allowed: input.allowAutomaticAction,
|
|
1690
|
+
updated_at: nowIso,
|
|
1691
|
+
};
|
|
1692
|
+
if (process.platform !== "linux" || input.platform !== "linux") {
|
|
1693
|
+
ledger = {
|
|
1694
|
+
...ledger,
|
|
1695
|
+
platform: input.platform,
|
|
1696
|
+
state: "advisory",
|
|
1697
|
+
current_task_id: currentTaskId,
|
|
1698
|
+
last_incident_id: incidentId,
|
|
1699
|
+
last_reason: `unsupported_platform:${input.platform}:host:${process.platform}:${baseReason}`,
|
|
1700
|
+
last_pid_probe: input.pidProbe,
|
|
1701
|
+
updated_at: nowIso,
|
|
1702
|
+
};
|
|
1703
|
+
await writeWorkerMemoryGuardLedger(dir, ledger);
|
|
1704
|
+
await appendWorkerMemoryGuardAction({
|
|
1705
|
+
dir,
|
|
1706
|
+
teamName: input.teamName,
|
|
1707
|
+
cwd: input.cwd,
|
|
1708
|
+
workerId: worker.id,
|
|
1709
|
+
task,
|
|
1710
|
+
incidentId,
|
|
1711
|
+
action: "advisory",
|
|
1712
|
+
result: "noop",
|
|
1713
|
+
reason: ledger.last_reason ?? "unsupported_platform",
|
|
1714
|
+
});
|
|
1715
|
+
return { ok: true, result: "advisory", lifecycle_mutated: false, ledger };
|
|
1716
|
+
}
|
|
1717
|
+
if (!ledger.automatic_action_allowed) {
|
|
1718
|
+
ledger = {
|
|
1719
|
+
...ledger,
|
|
1720
|
+
platform: input.platform,
|
|
1721
|
+
state: "advisory",
|
|
1722
|
+
current_task_id: currentTaskId,
|
|
1723
|
+
last_incident_id: incidentId,
|
|
1724
|
+
last_reason: "automatic_linux_action_disabled",
|
|
1725
|
+
last_pid_probe: input.pidProbe,
|
|
1726
|
+
updated_at: nowIso,
|
|
1727
|
+
};
|
|
1728
|
+
await writeWorkerMemoryGuardLedger(dir, ledger);
|
|
1729
|
+
await appendWorkerMemoryGuardAction({
|
|
1730
|
+
dir,
|
|
1731
|
+
teamName: input.teamName,
|
|
1732
|
+
cwd: input.cwd,
|
|
1733
|
+
workerId: worker.id,
|
|
1734
|
+
task,
|
|
1735
|
+
incidentId,
|
|
1736
|
+
action: "advisory",
|
|
1737
|
+
result: "noop",
|
|
1738
|
+
reason: "automatic_linux_action_disabled",
|
|
1739
|
+
});
|
|
1740
|
+
return { ok: true, result: "advisory", lifecycle_mutated: false, ledger };
|
|
1741
|
+
}
|
|
1742
|
+
const controllerPath = path.join(dir, "memory-guard-controller.json");
|
|
1743
|
+
const controllerProcessStart = await readLinuxProcessStartTime(process.pid);
|
|
1744
|
+
const controllerId = `pid:${process.pid}:start:${controllerProcessStart ?? "unknown"}`;
|
|
1745
|
+
const controllerNow = currentTimeMs();
|
|
1746
|
+
const controllerCooldownMs = parseDurationEnv(input.env, "GJC_TEAM_MEMORY_GUARD_ACTION_COOLDOWN_MS", 120_000);
|
|
1747
|
+
const existingController = await readJsonFile<{
|
|
1748
|
+
controller_id?: string;
|
|
1749
|
+
cooldown_until?: string;
|
|
1750
|
+
}>(controllerPath);
|
|
1751
|
+
const existingCooldownUntil = Date.parse(existingController?.cooldown_until ?? "");
|
|
1752
|
+
if (
|
|
1753
|
+
existingController?.controller_id &&
|
|
1754
|
+
existingController.controller_id !== controllerId &&
|
|
1755
|
+
Number.isFinite(existingCooldownUntil) &&
|
|
1756
|
+
existingCooldownUntil > controllerNow
|
|
1757
|
+
)
|
|
1758
|
+
return {
|
|
1759
|
+
ok: true,
|
|
1760
|
+
result: "advisory",
|
|
1761
|
+
lifecycle_mutated: false,
|
|
1762
|
+
reason: "team_memory_guard_controller_active",
|
|
1763
|
+
};
|
|
1764
|
+
await writeJsonFile(controllerPath, {
|
|
1765
|
+
schema_version: 1,
|
|
1766
|
+
controller_id: controllerId,
|
|
1767
|
+
worker_id: worker.id,
|
|
1768
|
+
incident_id: incidentId,
|
|
1769
|
+
reserved_at: new Date(controllerNow).toISOString(),
|
|
1770
|
+
cooldown_until: new Date(controllerNow + controllerCooldownMs).toISOString(),
|
|
1771
|
+
});
|
|
1772
|
+
const checkpoint = await checkpointWorkerForMemoryGuard(worker, currentTaskId);
|
|
1773
|
+
if (!checkpoint.ok) {
|
|
1774
|
+
const retried = withMemoryGuardRetry(ledger, {
|
|
1775
|
+
platform: input.platform,
|
|
1776
|
+
reason: checkpoint.reason,
|
|
1777
|
+
incidentId,
|
|
1778
|
+
currentTaskId,
|
|
1779
|
+
pidProbe: input.pidProbe,
|
|
1780
|
+
nowIso,
|
|
1781
|
+
});
|
|
1782
|
+
ledger = retried.ledger;
|
|
1783
|
+
await writeWorkerMemoryGuardLedger(dir, ledger);
|
|
1784
|
+
if (retried.finalBlocked)
|
|
1785
|
+
await finalizeWorkerMemoryGuardBlockedState({
|
|
1786
|
+
teamName: input.teamName,
|
|
1787
|
+
dir,
|
|
1788
|
+
worker,
|
|
1789
|
+
task,
|
|
1790
|
+
taskMutation: input.taskMutation,
|
|
1791
|
+
reason: checkpoint.reason,
|
|
1792
|
+
cwd: input.cwd,
|
|
1793
|
+
env: input.env,
|
|
1794
|
+
});
|
|
1795
|
+
await appendWorkerMemoryGuardAction({
|
|
1796
|
+
dir,
|
|
1797
|
+
teamName: input.teamName,
|
|
1798
|
+
cwd: input.cwd,
|
|
1799
|
+
workerId: worker.id,
|
|
1800
|
+
task,
|
|
1801
|
+
incidentId,
|
|
1802
|
+
action: retried.finalBlocked ? "blocked" : "replace",
|
|
1803
|
+
result: retried.finalBlocked ? "blocked" : "failed",
|
|
1804
|
+
reason: checkpoint.reason,
|
|
1805
|
+
});
|
|
1806
|
+
return {
|
|
1807
|
+
ok: true,
|
|
1808
|
+
result: retried.finalBlocked ? "blocked" : "retrying",
|
|
1809
|
+
lifecycle_mutated: retried.finalBlocked,
|
|
1810
|
+
ledger,
|
|
1811
|
+
};
|
|
1812
|
+
}
|
|
1813
|
+
const refreshedInventory = await readGjcContinuationAuthorityInventory(dir);
|
|
1814
|
+
const refreshedAuthority = refreshedInventory.valid
|
|
1815
|
+
? selectGjcContinuationWorkerAuthority(refreshedInventory, worker.id)
|
|
1816
|
+
: { valid: false as const, taskCount: 0, claimCount: 0 };
|
|
1817
|
+
if (
|
|
1818
|
+
!refreshedAuthority.valid ||
|
|
1819
|
+
refreshedAuthority.task.id !== task?.id ||
|
|
1820
|
+
refreshedAuthority.claim.token !== task?.claim?.token ||
|
|
1821
|
+
refreshedAuthority.claim.leased_until !== task?.claim?.leased_until ||
|
|
1822
|
+
Date.parse(refreshedAuthority.claim.leased_until) <= currentTimeMs()
|
|
1823
|
+
)
|
|
1824
|
+
return {
|
|
1825
|
+
ok: true,
|
|
1826
|
+
result: "advisory",
|
|
1827
|
+
lifecycle_mutated: false,
|
|
1828
|
+
reason: "worker_claim_authority_changed",
|
|
1829
|
+
};
|
|
1830
|
+
const startupAckPath = path.join(dir, "workers", safePathSegment("worker_id", worker.id), "startup-ack.json");
|
|
1831
|
+
const previousStartupAck = (await Bun.file(startupAckPath).exists())
|
|
1832
|
+
? await Bun.file(startupAckPath).text()
|
|
1833
|
+
: undefined;
|
|
1834
|
+
const lifecyclePath = workerLifecyclePath(dir, worker.id);
|
|
1835
|
+
const previousLifecycle = (await Bun.file(lifecyclePath).exists())
|
|
1836
|
+
? await Bun.file(lifecyclePath).text()
|
|
1837
|
+
: undefined;
|
|
1838
|
+
const restorePredecessorStartupState = async (): Promise<void> => {
|
|
1839
|
+
if (previousLifecycle === undefined) await fs.rm(lifecyclePath, { force: true });
|
|
1840
|
+
else await Bun.write(lifecyclePath, previousLifecycle);
|
|
1841
|
+
if (previousStartupAck === undefined) await fs.rm(startupAckPath, { force: true });
|
|
1842
|
+
else await Bun.write(startupAckPath, previousStartupAck);
|
|
1843
|
+
};
|
|
1844
|
+
await fs.rm(startupAckPath, { force: true });
|
|
1845
|
+
const replacementToken = input.replacementToken ?? randomUUID();
|
|
1846
|
+
const startupAckTimeoutMs = parseDurationEnv(input.env, "GJC_TEAM_MEMORY_GUARD_STARTUP_TIMEOUT_MS", 120_000);
|
|
1847
|
+
let newPaneId: string;
|
|
1848
|
+
try {
|
|
1849
|
+
newPaneId = await relaunchWorkerPaneForMemoryGuard({
|
|
1850
|
+
config,
|
|
1851
|
+
worker,
|
|
1852
|
+
platform: process.platform,
|
|
1853
|
+
startupAckPath,
|
|
1854
|
+
replacementToken,
|
|
1855
|
+
startupAckTimeoutMs,
|
|
1856
|
+
});
|
|
1857
|
+
} catch (error) {
|
|
1858
|
+
await restorePredecessorStartupState();
|
|
1859
|
+
const reason = error instanceof Error && error.message ? `relaunch_failed:${error.message}` : "relaunch_failed";
|
|
1860
|
+
const retried = withMemoryGuardRetry(ledger, {
|
|
1861
|
+
platform: input.platform,
|
|
1862
|
+
reason,
|
|
1863
|
+
incidentId,
|
|
1864
|
+
currentTaskId,
|
|
1865
|
+
pidProbe: input.pidProbe,
|
|
1866
|
+
nowIso,
|
|
1867
|
+
});
|
|
1868
|
+
ledger = retried.ledger;
|
|
1869
|
+
await writeWorkerMemoryGuardLedger(dir, ledger);
|
|
1870
|
+
if (retried.finalBlocked)
|
|
1871
|
+
await finalizeWorkerMemoryGuardBlockedState({
|
|
1872
|
+
teamName: input.teamName,
|
|
1873
|
+
dir,
|
|
1874
|
+
worker,
|
|
1875
|
+
task,
|
|
1876
|
+
taskMutation: input.taskMutation,
|
|
1877
|
+
reason,
|
|
1878
|
+
cwd: input.cwd,
|
|
1879
|
+
env: input.env,
|
|
1880
|
+
});
|
|
1881
|
+
await appendWorkerMemoryGuardAction({
|
|
1882
|
+
dir,
|
|
1883
|
+
teamName: input.teamName,
|
|
1884
|
+
cwd: input.cwd,
|
|
1885
|
+
workerId: worker.id,
|
|
1886
|
+
task,
|
|
1887
|
+
incidentId,
|
|
1888
|
+
action: retried.finalBlocked ? "blocked" : "replace",
|
|
1889
|
+
result: retried.finalBlocked ? "blocked" : "failed",
|
|
1890
|
+
reason,
|
|
1891
|
+
});
|
|
1892
|
+
return {
|
|
1893
|
+
ok: true,
|
|
1894
|
+
result: retried.finalBlocked ? "blocked" : "retrying",
|
|
1895
|
+
lifecycle_mutated: retried.finalBlocked,
|
|
1896
|
+
ledger,
|
|
1897
|
+
};
|
|
1898
|
+
}
|
|
1899
|
+
const postAckInventory = await readGjcContinuationAuthorityInventory(dir);
|
|
1900
|
+
const postAckAuthority = postAckInventory.valid
|
|
1901
|
+
? selectGjcContinuationWorkerAuthority(postAckInventory, worker.id)
|
|
1902
|
+
: { valid: false as const, taskCount: 0, claimCount: 0 };
|
|
1903
|
+
if (
|
|
1904
|
+
!postAckAuthority.valid ||
|
|
1905
|
+
postAckAuthority.task.id !== task?.id ||
|
|
1906
|
+
postAckAuthority.claim.token !== task?.claim?.token ||
|
|
1907
|
+
postAckAuthority.claim.leased_until !== task?.claim?.leased_until ||
|
|
1908
|
+
Date.parse(postAckAuthority.claim.leased_until) <= currentTimeMs()
|
|
1909
|
+
) {
|
|
1910
|
+
Bun.spawnSync([config.tmux_command, "kill-pane", "-t", newPaneId], {
|
|
1911
|
+
stdout: "ignore",
|
|
1912
|
+
stderr: "ignore",
|
|
1913
|
+
});
|
|
1914
|
+
await restorePredecessorStartupState();
|
|
1915
|
+
return {
|
|
1916
|
+
ok: true,
|
|
1917
|
+
result: "advisory",
|
|
1918
|
+
lifecycle_mutated: false,
|
|
1919
|
+
reason: "worker_claim_authority_changed_after_startup",
|
|
1920
|
+
};
|
|
1921
|
+
}
|
|
1922
|
+
const successorPane = probePaneTeamTarget(config, newPaneId);
|
|
1923
|
+
if (!successorPane.exists || !successorPane.belongsToTeamTarget || !successorPane.pid) {
|
|
1924
|
+
Bun.spawnSync([config.tmux_command, "kill-pane", "-t", newPaneId], {
|
|
1925
|
+
stdout: "ignore",
|
|
1926
|
+
stderr: "ignore",
|
|
1927
|
+
});
|
|
1928
|
+
await restorePredecessorStartupState();
|
|
1929
|
+
return {
|
|
1930
|
+
ok: true,
|
|
1931
|
+
result: "advisory",
|
|
1932
|
+
lifecycle_mutated: false,
|
|
1933
|
+
reason: "successor_pane_unavailable_after_startup",
|
|
1934
|
+
};
|
|
1935
|
+
}
|
|
1936
|
+
const heartbeatPath = path.join(dir, "workers", safePathSegment("worker_id", worker.id), "heartbeat.json");
|
|
1937
|
+
const previousHeartbeat = (await Bun.file(heartbeatPath).exists())
|
|
1938
|
+
? await Bun.file(heartbeatPath).text()
|
|
1939
|
+
: undefined;
|
|
1940
|
+
const successorHeartbeat: WorkerHeartbeatFile = {
|
|
1941
|
+
pid: successorPane.pid,
|
|
1942
|
+
last_turn_at: now(),
|
|
1943
|
+
turn_count: 0,
|
|
1944
|
+
alive: true,
|
|
1945
|
+
process_start_time: await readLinuxProcessStartTime(successorPane.pid),
|
|
1946
|
+
};
|
|
1947
|
+
const nextConfig: GjcTeamConfig = {
|
|
1948
|
+
...config,
|
|
1949
|
+
workers: config.workers.map(candidate =>
|
|
1950
|
+
candidate.id === worker.id
|
|
1951
|
+
? { ...candidate, pane_id: newPaneId, last_heartbeat: nowIso, status: "idle" }
|
|
1952
|
+
: candidate,
|
|
1953
|
+
),
|
|
1954
|
+
updated_at: nowIso,
|
|
1955
|
+
};
|
|
1956
|
+
const rollbackReplacement = async (): Promise<void> => {
|
|
1957
|
+
Bun.spawnSync([config.tmux_command, "kill-pane", "-t", newPaneId], {
|
|
1958
|
+
stdout: "ignore",
|
|
1959
|
+
stderr: "ignore",
|
|
1960
|
+
});
|
|
1961
|
+
if (previousHeartbeat === undefined) await fs.rm(heartbeatPath, { force: true });
|
|
1962
|
+
else await Bun.write(heartbeatPath, previousHeartbeat);
|
|
1963
|
+
await syncTeamConfigAndManifest(dir, config);
|
|
1964
|
+
await restorePredecessorStartupState();
|
|
1965
|
+
};
|
|
1966
|
+
try {
|
|
1967
|
+
await writeJsonFile(heartbeatPath, successorHeartbeat);
|
|
1968
|
+
await syncTeamConfigAndManifest(dir, nextConfig);
|
|
1969
|
+
await writeLifecycleRecord(workerRuntime, dir, { ...worker, pane_id: newPaneId }, "ready", {
|
|
1970
|
+
pane_id: newPaneId,
|
|
1971
|
+
started_at: nowIso,
|
|
1972
|
+
stop_reason: undefined,
|
|
1973
|
+
stopped_at: undefined,
|
|
1974
|
+
});
|
|
1975
|
+
const cutoverPane = probePaneTeamTarget(config, newPaneId);
|
|
1976
|
+
if (!cutoverPane.exists || !cutoverPane.belongsToTeamTarget || cutoverPane.pid !== successorHeartbeat.pid)
|
|
1977
|
+
throw new Error(`memory_guard_successor_pane_changed:${worker.id}`);
|
|
1978
|
+
if (worker.pane_id && !config.dry_run) {
|
|
1979
|
+
const killed = Bun.spawnSync([config.tmux_command, "kill-pane", "-t", worker.pane_id], {
|
|
1980
|
+
stdout: "ignore",
|
|
1981
|
+
stderr: "pipe",
|
|
1982
|
+
});
|
|
1983
|
+
if (killed.exitCode !== 0 && probePaneTeamTarget(config, worker.pane_id).exists)
|
|
1984
|
+
throw new Error(killed.stderr.toString().trim() || `memory_guard_kill_failed:${worker.id}`);
|
|
1985
|
+
}
|
|
1986
|
+
} catch (error) {
|
|
1987
|
+
await rollbackReplacement();
|
|
1988
|
+
throw new Error(
|
|
1989
|
+
error instanceof Error && error.message
|
|
1990
|
+
? `memory_guard_replacement_commit_failed:${error.message}`
|
|
1991
|
+
: "memory_guard_replacement_commit_failed",
|
|
1992
|
+
);
|
|
1993
|
+
}
|
|
1994
|
+
ledger = {
|
|
1995
|
+
...ledger,
|
|
1996
|
+
platform: input.platform,
|
|
1997
|
+
state: "replaced",
|
|
1998
|
+
retry_count: 0,
|
|
1999
|
+
current_task_id: currentTaskId,
|
|
2000
|
+
last_incident_id: incidentId,
|
|
2001
|
+
last_reason: baseReason,
|
|
2002
|
+
last_pid_probe: input.pidProbe,
|
|
2003
|
+
last_checkpoint: checkpoint.checkpoint,
|
|
2004
|
+
last_replacement: {
|
|
2005
|
+
old_pane_id: worker.pane_id,
|
|
2006
|
+
new_pane_id: newPaneId,
|
|
2007
|
+
recorded_at: nowIso,
|
|
2008
|
+
},
|
|
2009
|
+
updated_at: nowIso,
|
|
2010
|
+
};
|
|
2011
|
+
await writeWorkerMemoryGuardLedger(dir, ledger);
|
|
2012
|
+
await appendWorkerMemoryGuardAction({
|
|
2013
|
+
dir,
|
|
2014
|
+
teamName: input.teamName,
|
|
2015
|
+
cwd: input.cwd,
|
|
2016
|
+
workerId: worker.id,
|
|
2017
|
+
task,
|
|
2018
|
+
incidentId,
|
|
2019
|
+
action: "replace",
|
|
2020
|
+
result: "succeeded",
|
|
2021
|
+
reason: baseReason,
|
|
2022
|
+
});
|
|
2023
|
+
await appendEvent(dir, {
|
|
2024
|
+
type: "worker_memory_guard_replaced",
|
|
2025
|
+
worker: worker.id,
|
|
2026
|
+
task_id: currentTaskId,
|
|
2027
|
+
message: `Replaced ${worker.id} after memory-guard checkpoint`,
|
|
2028
|
+
data: {
|
|
2029
|
+
incident_id: incidentId,
|
|
2030
|
+
checkpoint_kind: checkpoint.checkpoint.kind,
|
|
2031
|
+
checkpoint_commit: checkpoint.checkpoint.commit,
|
|
2032
|
+
old_pane_id: worker.pane_id,
|
|
2033
|
+
new_pane_id: newPaneId,
|
|
2034
|
+
},
|
|
2035
|
+
});
|
|
2036
|
+
return {
|
|
2037
|
+
ok: true,
|
|
2038
|
+
result: "replaced",
|
|
2039
|
+
lifecycle_mutated: true,
|
|
2040
|
+
ledger,
|
|
2041
|
+
checkpoint: checkpoint.checkpoint,
|
|
2042
|
+
};
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
async function applyWorkerMemoryGuard(
|
|
2046
|
+
input: Omit<Parameters<typeof applyWorkerMemoryGuardUnlocked>[0], "dir" | "taskMutation">,
|
|
2047
|
+
): Promise<Record<string, unknown>> {
|
|
2048
|
+
const dir = await findTeamDir(input.teamName, input.cwd, input.env);
|
|
2049
|
+
return withGjcTeamTaskMutation(taskStore(dir), taskMutation =>
|
|
2050
|
+
applyWorkerMemoryGuardUnlocked({ ...input, dir, taskMutation }),
|
|
2051
|
+
);
|
|
2052
|
+
}
|
|
1221
2053
|
async function readPhase(dir: string): Promise<GjcTeamPhase> {
|
|
1222
2054
|
try {
|
|
1223
2055
|
const phase = await readJsonFile<{ current_phase?: GjcTeamPhase }>(path.join(dir, "phase.json"));
|
|
@@ -1998,15 +2830,27 @@ async function startTmuxSession(
|
|
|
1998
2830
|
throw error;
|
|
1999
2831
|
}
|
|
2000
2832
|
}
|
|
2833
|
+
function probePaneTeamTarget(
|
|
2834
|
+
config: GjcTeamConfig,
|
|
2835
|
+
paneId: string,
|
|
2836
|
+
): { exists: boolean; belongsToTeamTarget: boolean; pid?: number } {
|
|
2837
|
+
if (paneId === config.leader.pane_id) return { exists: true, belongsToTeamTarget: false };
|
|
2838
|
+
const result = Bun.spawnSync(
|
|
2839
|
+
[config.tmux_command, "display-message", "-p", "-t", paneId, "#S:#I #{pane_id} #{pane_pid}"],
|
|
2840
|
+
{ stdout: "pipe", stderr: "ignore" },
|
|
2841
|
+
);
|
|
2842
|
+
if (result.exitCode !== 0) return { exists: false, belongsToTeamTarget: false };
|
|
2843
|
+
const [target = "", detectedPaneId = "", rawPid = ""] = result.stdout.toString().trim().split(/\s+/);
|
|
2844
|
+
const pid = Number(rawPid);
|
|
2845
|
+
return {
|
|
2846
|
+
exists: true,
|
|
2847
|
+
belongsToTeamTarget: target === config.tmux_target && detectedPaneId === paneId,
|
|
2848
|
+
pid: Number.isInteger(pid) && pid > 0 ? pid : undefined,
|
|
2849
|
+
};
|
|
2850
|
+
}
|
|
2851
|
+
|
|
2001
2852
|
function paneBelongsToTeamTarget(config: GjcTeamConfig, paneId: string): boolean {
|
|
2002
|
-
|
|
2003
|
-
const result = Bun.spawnSync([config.tmux_command, "display-message", "-p", "-t", paneId, "#S:#I #{pane_id}"], {
|
|
2004
|
-
stdout: "pipe",
|
|
2005
|
-
stderr: "ignore",
|
|
2006
|
-
});
|
|
2007
|
-
if (result.exitCode !== 0) return false;
|
|
2008
|
-
const [target = "", detectedPaneId = ""] = result.stdout.toString().trim().split(/\s+/);
|
|
2009
|
-
return target === config.tmux_target && detectedPaneId === paneId;
|
|
2853
|
+
return probePaneTeamTarget(config, paneId).belongsToTeamTarget;
|
|
2010
2854
|
}
|
|
2011
2855
|
function killWorkerPanes(config: GjcTeamConfig): void {
|
|
2012
2856
|
for (const worker of config.workers)
|
|
@@ -2147,13 +2991,24 @@ const UNMERGED_GIT_STATUS_CODES = new Set(["DD", "AU", "UD", "UA", "DU", "AA", "
|
|
|
2147
2991
|
// into the leader branch on projects that do not gitignore `.gjc/_session-*/`.
|
|
2148
2992
|
const PROTECTED_WORKER_CHECKPOINT_PREFIXES = [".gjc/_session-*/"];
|
|
2149
2993
|
|
|
2150
|
-
function
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2994
|
+
function parsePorcelainStatus(stdout: string): { files: string[]; statusCodes: string[] } {
|
|
2995
|
+
const records = stdout.split("\0");
|
|
2996
|
+
const files: string[] = [];
|
|
2997
|
+
const statusCodes: string[] = [];
|
|
2998
|
+
for (let index = 0; index < records.length; index++) {
|
|
2999
|
+
const record = records[index];
|
|
3000
|
+
if (!record) continue;
|
|
3001
|
+
const statusCode = record.slice(0, 2);
|
|
3002
|
+
const file = record.slice(3);
|
|
3003
|
+
if (!file) continue;
|
|
3004
|
+
statusCodes.push(statusCode);
|
|
3005
|
+
files.push(file);
|
|
3006
|
+
if (statusCode.includes("R") || statusCode.includes("C")) {
|
|
3007
|
+
const source = records[++index];
|
|
3008
|
+
if (source) files.push(source);
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
return { files: [...new Set(files)], statusCodes };
|
|
2157
3012
|
}
|
|
2158
3013
|
|
|
2159
3014
|
function normalizeGitStatusPath(filePath: string): string {
|
|
@@ -2180,7 +3035,7 @@ export function classifyGjcTeamCheckpointFiles(files: string[]): {
|
|
|
2180
3035
|
}
|
|
2181
3036
|
|
|
2182
3037
|
export function classifyWorkerCheckpointStatus(cwd: string): GjcWorkerCheckpointClassification {
|
|
2183
|
-
const status = runGitResult(cwd, ["status", "--porcelain", "-uall"]);
|
|
3038
|
+
const status = runGitResult(cwd, ["status", "--porcelain=v1", "-z", "-uall"]);
|
|
2184
3039
|
if (!status.ok) {
|
|
2185
3040
|
return {
|
|
2186
3041
|
kind: "git_error",
|
|
@@ -2189,11 +3044,9 @@ export function classifyWorkerCheckpointStatus(cwd: string): GjcWorkerCheckpoint
|
|
|
2189
3044
|
};
|
|
2190
3045
|
}
|
|
2191
3046
|
if (!status.stdout.trim()) return { kind: "clean", files: [] };
|
|
2192
|
-
const
|
|
2193
|
-
const
|
|
2194
|
-
|
|
2195
|
-
.filter(Boolean)
|
|
2196
|
-
.some(line => UNMERGED_GIT_STATUS_CODES.has(line.slice(0, 2)));
|
|
3047
|
+
const parsed = parsePorcelainStatus(status.stdout);
|
|
3048
|
+
const files = parsed.files;
|
|
3049
|
+
const hasUnmergedStatus = parsed.statusCodes.some(code => UNMERGED_GIT_STATUS_CODES.has(code));
|
|
2197
3050
|
const conflictFiles = listConflictFiles(cwd);
|
|
2198
3051
|
if (hasUnmergedStatus || conflictFiles.length > 0) {
|
|
2199
3052
|
return {
|
|
@@ -2210,7 +3063,7 @@ export async function classifyWorkerCheckpointStatusAsync(
|
|
|
2210
3063
|
cwd: string,
|
|
2211
3064
|
signal?: AbortSignal,
|
|
2212
3065
|
): Promise<GjcWorkerCheckpointClassification> {
|
|
2213
|
-
const status = await runGitResultAsync(cwd, ["status", "--porcelain", "-uall"], signal);
|
|
3066
|
+
const status = await runGitResultAsync(cwd, ["status", "--porcelain=v1", "-z", "-uall"], signal);
|
|
2214
3067
|
if (!status.ok) {
|
|
2215
3068
|
return {
|
|
2216
3069
|
kind: "git_error",
|
|
@@ -2219,11 +3072,9 @@ export async function classifyWorkerCheckpointStatusAsync(
|
|
|
2219
3072
|
};
|
|
2220
3073
|
}
|
|
2221
3074
|
if (!status.stdout.trim()) return { kind: "clean", files: [] };
|
|
2222
|
-
const
|
|
2223
|
-
const
|
|
2224
|
-
|
|
2225
|
-
.filter(Boolean)
|
|
2226
|
-
.some(line => UNMERGED_GIT_STATUS_CODES.has(line.slice(0, 2)));
|
|
3075
|
+
const parsed = parsePorcelainStatus(status.stdout);
|
|
3076
|
+
const files = parsed.files;
|
|
3077
|
+
const hasUnmergedStatus = parsed.statusCodes.some(code => UNMERGED_GIT_STATUS_CODES.has(code));
|
|
2227
3078
|
const conflictFiles = await listConflictFilesAsync(cwd, signal);
|
|
2228
3079
|
if (hasUnmergedStatus || conflictFiles.length > 0) {
|
|
2229
3080
|
return {
|
|
@@ -3387,8 +4238,7 @@ async function writeGjcWorkerStartupAck(
|
|
|
3387
4238
|
env: NodeJS.ProcessEnv,
|
|
3388
4239
|
input: Record<string, unknown>,
|
|
3389
4240
|
): Promise<Record<string, unknown>> {
|
|
3390
|
-
|
|
3391
|
-
return withGjcTeamMutationFence(dir, () => writeWorkerStartupAck(workerRuntime, teamName, worker, cwd, env, input));
|
|
4241
|
+
return writeWorkerStartupAck(workerRuntime, teamName, worker, cwd, env, input);
|
|
3392
4242
|
}
|
|
3393
4243
|
function parseDurationEnv(env: NodeJS.ProcessEnv, name: string, fallbackMs: number): number {
|
|
3394
4244
|
const raw = env[name]?.trim();
|
|
@@ -4248,19 +5098,106 @@ export async function executeGjcTeamApiOperation(
|
|
|
4248
5098
|
return readGjcWorkerHeartbeat(teamName, worker, cwd, env);
|
|
4249
5099
|
case "recover-stale-claims":
|
|
4250
5100
|
return recoverGjcTeamStaleClaims(teamName, cwd, env);
|
|
4251
|
-
case "update-worker-heartbeat":
|
|
5101
|
+
case "update-worker-heartbeat": {
|
|
5102
|
+
const pid = Number(input.pid ?? 0);
|
|
4252
5103
|
return updateGjcWorkerHeartbeat(
|
|
4253
5104
|
teamName,
|
|
4254
5105
|
worker,
|
|
4255
5106
|
{
|
|
4256
|
-
pid
|
|
5107
|
+
pid,
|
|
4257
5108
|
last_turn_at: now(),
|
|
4258
5109
|
turn_count: Number(input.turn_count ?? 0),
|
|
4259
5110
|
alive: Boolean(input.alive ?? true),
|
|
5111
|
+
process_start_time: await readLinuxProcessStartTime(pid),
|
|
4260
5112
|
},
|
|
4261
5113
|
cwd,
|
|
4262
5114
|
env,
|
|
4263
5115
|
);
|
|
5116
|
+
}
|
|
5117
|
+
case "read-worker-memory-guard": {
|
|
5118
|
+
const dir = await findTeamDir(teamName, cwd, env);
|
|
5119
|
+
const config = await readConfig(dir);
|
|
5120
|
+
assertKnownWorker(config, worker);
|
|
5121
|
+
return readWorkerMemoryGuardLedger(dir, worker, normalizeWorkerMemoryGuardPlatform(input.platform));
|
|
5122
|
+
}
|
|
5123
|
+
case "update-worker-memory-guard": {
|
|
5124
|
+
const dir = await findTeamDir(teamName, cwd, env);
|
|
5125
|
+
return withGjcTeamMutationFence(dir, async () => {
|
|
5126
|
+
const config = await readConfig(dir);
|
|
5127
|
+
assertKnownWorker(config, worker);
|
|
5128
|
+
const platform = normalizeWorkerMemoryGuardPlatform(input.platform);
|
|
5129
|
+
const tasks = await readTasks(dir);
|
|
5130
|
+
const currentTaskIdInput = input.current_task_id ?? input.currentTaskId;
|
|
5131
|
+
if (typeof currentTaskIdInput === "string" && !currentTaskIdInput.trim())
|
|
5132
|
+
throw new Error("invalid_worker_memory_guard_task_id");
|
|
5133
|
+
const currentTaskId =
|
|
5134
|
+
typeof currentTaskIdInput === "string"
|
|
5135
|
+
? currentTaskIdInput.trim()
|
|
5136
|
+
: findGjcTeamClaimedTaskForWorker(tasks, worker)?.id;
|
|
5137
|
+
const existing = await readWorkerMemoryGuardLedger(dir, worker, platform);
|
|
5138
|
+
const retryLimitInput = Number(input.retry_limit ?? input.retryLimit ?? existing.retry_limit);
|
|
5139
|
+
if (!Number.isInteger(retryLimitInput) || retryLimitInput <= 0)
|
|
5140
|
+
throw new Error(`invalid_worker_memory_guard_retry_limit:${retryLimitInput}`);
|
|
5141
|
+
const pidProbe = normalizeGjcTeamWorkerMemoryGuardPidProbe(input.pid_probe ?? input.pidProbe);
|
|
5142
|
+
const stateInput = typeof input.state === "string" ? input.state.trim() : existing.state;
|
|
5143
|
+
if (!["idle", "advisory", "retrying", "checkpointed", "replaced", "blocked"].includes(stateInput))
|
|
5144
|
+
throw new Error(`invalid_worker_memory_guard_state:${stateInput}`);
|
|
5145
|
+
const updated: GjcTeamWorkerMemoryGuardLedger = {
|
|
5146
|
+
...existing,
|
|
5147
|
+
platform,
|
|
5148
|
+
state: stateInput as GjcTeamWorkerMemoryGuardLedger["state"],
|
|
5149
|
+
automatic_action_allowed:
|
|
5150
|
+
typeof input.automatic_action_allowed === "boolean"
|
|
5151
|
+
? input.automatic_action_allowed
|
|
5152
|
+
: typeof input.automaticActionAllowed === "boolean"
|
|
5153
|
+
? input.automaticActionAllowed
|
|
5154
|
+
: existing.automatic_action_allowed,
|
|
5155
|
+
retry_limit: retryLimitInput,
|
|
5156
|
+
current_task_id: currentTaskId,
|
|
5157
|
+
last_incident_id:
|
|
5158
|
+
typeof input.incident_id === "string"
|
|
5159
|
+
? input.incident_id
|
|
5160
|
+
: typeof input.incidentId === "string"
|
|
5161
|
+
? input.incidentId
|
|
5162
|
+
: existing.last_incident_id,
|
|
5163
|
+
last_reason:
|
|
5164
|
+
typeof input.reason === "string" && input.reason.trim() ? input.reason.trim() : existing.last_reason,
|
|
5165
|
+
last_pid_probe: pidProbe ?? existing.last_pid_probe,
|
|
5166
|
+
updated_at: now(),
|
|
5167
|
+
};
|
|
5168
|
+
return writeWorkerMemoryGuardLedger(dir, updated);
|
|
5169
|
+
});
|
|
5170
|
+
}
|
|
5171
|
+
case "apply-worker-memory-guard":
|
|
5172
|
+
return applyWorkerMemoryGuard({
|
|
5173
|
+
teamName,
|
|
5174
|
+
workerId: explicitWorker,
|
|
5175
|
+
requestedWorkerId: worker,
|
|
5176
|
+
reason: typeof input.reason === "string" ? input.reason : undefined,
|
|
5177
|
+
incidentId:
|
|
5178
|
+
typeof input.incident_id === "string"
|
|
5179
|
+
? input.incident_id
|
|
5180
|
+
: typeof input.incidentId === "string"
|
|
5181
|
+
? input.incidentId
|
|
5182
|
+
: undefined,
|
|
5183
|
+
platform: normalizeWorkerMemoryGuardPlatform(input.platform),
|
|
5184
|
+
pidProbe: normalizeGjcTeamWorkerMemoryGuardPidProbe(input.pid_probe ?? input.pidProbe),
|
|
5185
|
+
candidates: input.candidates,
|
|
5186
|
+
cwd,
|
|
5187
|
+
env,
|
|
5188
|
+
allowAutomaticAction:
|
|
5189
|
+
typeof input.automatic_action_allowed === "boolean"
|
|
5190
|
+
? input.automatic_action_allowed
|
|
5191
|
+
: typeof input.automaticActionAllowed === "boolean"
|
|
5192
|
+
? input.automaticActionAllowed
|
|
5193
|
+
: undefined,
|
|
5194
|
+
replacementToken:
|
|
5195
|
+
typeof input.replacement_token === "string"
|
|
5196
|
+
? input.replacement_token
|
|
5197
|
+
: typeof input.replacementToken === "string"
|
|
5198
|
+
? input.replacementToken
|
|
5199
|
+
: undefined,
|
|
5200
|
+
});
|
|
4264
5201
|
case "write-worker-inbox":
|
|
4265
5202
|
return writeGjcWorkerInbox(teamName, worker, String(input.content ?? ""), cwd, env);
|
|
4266
5203
|
case "write-worker-identity":
|