@gajae-code/coding-agent 0.11.11 → 0.12.1
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 +131 -4
- package/dist/types/async/job-manager.d.ts +17 -3
- package/dist/types/cli/read-cli.d.ts +1 -0
- 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-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 +83 -1
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/exa/mcp-client.d.ts +10 -1
- package/dist/types/exec/bash-executor.d.ts +45 -12
- package/dist/types/extensibility/extensions/types.d.ts +38 -4
- package/dist/types/extensibility/gjc-plugins/index.d.ts +12 -2
- package/dist/types/extensibility/gjc-plugins/installer.d.ts +63 -7
- package/dist/types/extensibility/gjc-plugins/lifecycle-reconciliation.d.ts +55 -0
- package/dist/types/extensibility/gjc-plugins/lifecycle.d.ts +64 -0
- package/dist/types/extensibility/gjc-plugins/runtime-quarantine.d.ts +67 -0
- package/dist/types/extensibility/gjc-plugins/session-validation.d.ts +3 -1
- package/dist/types/extensibility/gjc-plugins/types.d.ts +124 -0
- 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/launch-tmux.d.ts +1 -0
- package/dist/types/gjc-runtime/memory-guard-owner-claims.d.ts +47 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +70 -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 +27 -1
- package/dist/types/gjc-runtime/team-store.d.ts +12 -0
- package/dist/types/gjc-runtime/team-worker-heartbeat.d.ts +43 -0
- package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +92 -0
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +17 -0
- package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +8 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +31 -80
- 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 +10 -19
- package/dist/types/modes/components/assistant-message.d.ts +3 -1
- package/dist/types/modes/components/gjc-bundle-settings.d.ts +34 -0
- package/dist/types/modes/components/irc-sidebar.d.ts +41 -0
- package/dist/types/modes/components/notifications-settings-editor.d.ts +1 -0
- package/dist/types/modes/components/read-tool-group.d.ts +4 -4
- package/dist/types/modes/components/session-observer-overlay.d.ts +22 -0
- package/dist/types/modes/components/settings-selector.d.ts +11 -0
- package/dist/types/modes/components/status-line/gh.d.ts +5 -0
- package/dist/types/modes/components/tool-execution.d.ts +8 -0
- package/dist/types/modes/components/transcript-viewer-overlay.d.ts +18 -1
- package/dist/types/modes/controllers/event-controller.d.ts +3 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- 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/reminders/star-reminder.d.ts +3 -13
- package/dist/types/runtime-mcp/manager.d.ts +1 -0
- package/dist/types/runtime-mcp/smithery-auth.d.ts +5 -0
- package/dist/types/sdk/acp/adapter.d.ts +3 -1
- package/dist/types/sdk/broker/lifecycle.d.ts +5 -0
- package/dist/types/sdk/broker/transport.d.ts +2 -0
- package/dist/types/sdk/bus/attachment-registry.d.ts +1 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +4 -1
- package/dist/types/sdk/bus/config.d.ts +6 -2
- package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
- package/dist/types/sdk/bus/index.d.ts +44 -1
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +44 -0
- package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +2 -1
- package/dist/types/sdk/bus/reconciliation-store.d.ts +69 -0
- package/dist/types/sdk/bus/rich-draft.d.ts +1 -0
- package/dist/types/sdk/bus/rich-render.d.ts +2 -0
- package/dist/types/sdk/bus/telegram-adoption-intent.d.ts +165 -0
- package/dist/types/sdk/bus/telegram-cli.d.ts +12 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +11 -4
- package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -1
- package/dist/types/sdk/bus/telegram-reference.d.ts +11 -1
- package/dist/types/sdk/bus/threaded-render.d.ts +3 -2
- package/dist/types/sdk/bus/topic-registry.d.ts +11 -1
- package/dist/types/sdk/host/control/operations.d.ts +1 -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/host/query/revision-store.d.ts +2 -0
- package/dist/types/sdk/host/reverse-leases.d.ts +1 -1
- package/dist/types/sdk/prompt-status.d.ts +23 -3
- package/dist/types/sdk/session.d.ts +6 -0
- package/dist/types/session/agent-session.d.ts +82 -12
- package/dist/types/session/artifacts.d.ts +8 -0
- package/dist/types/session/blob-store.d.ts +57 -4
- package/dist/types/session/internal/managed-session-storage.d.ts +8 -1
- package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
- package/dist/types/session/session-manager.d.ts +48 -19
- package/dist/types/session/session-storage.d.ts +4 -0
- package/dist/types/session/streaming-output.d.ts +96 -12
- package/dist/types/setup/credential-import.d.ts +12 -0
- package/dist/types/skill-state/active-state.d.ts +12 -4
- package/dist/types/skill-state/workflow-hud.d.ts +9 -0
- package/dist/types/task/executor.d.ts +3 -2
- package/dist/types/task/index.d.ts +3 -1
- package/dist/types/task/provider-retry-status.d.ts +8 -0
- package/dist/types/task/receipt.d.ts +7 -1
- package/dist/types/task/types.d.ts +23 -0
- package/dist/types/tools/ask-answer-registry.d.ts +2 -2
- package/dist/types/tools/bash-interactive.d.ts +4 -1
- package/dist/types/tools/bash.d.ts +3 -1
- package/dist/types/tools/browser/launch.d.ts +7 -0
- package/dist/types/tools/fetch.d.ts +17 -1
- package/dist/types/tools/image-gen.d.ts +4 -0
- package/dist/types/tools/index.d.ts +4 -0
- package/dist/types/tools/output-meta.d.ts +44 -3
- package/dist/types/tools/path-utils.d.ts +1 -0
- package/dist/types/tools/read-internals.d.ts +12 -0
- package/dist/types/tools/read.d.ts +45 -7
- package/dist/types/tools/resource-gc.d.ts +4 -0
- package/dist/types/tools/review.d.ts +5 -7
- package/dist/types/tools/subagent.d.ts +2 -0
- package/dist/types/tools/tool-result.d.ts +7 -1
- package/dist/types/utils/gh.d.ts +13 -0
- package/dist/types/utils/image-resize.d.ts +1 -0
- package/dist/types/web/search/providers/kimi.d.ts +2 -0
- package/dist/types/web/search/providers/searxng.d.ts +7 -0
- package/dist/types/web/search/providers/xai.d.ts +2 -0
- package/package.json +11 -13
- package/scripts/acp-conformance-agent.ts +218 -0
- package/scripts/benchmark-sticky-viewport-pr1.ts +239 -0
- package/scripts/capture-gjc-bundle-settings.ts +306 -0
- package/scripts/capture-sticky-viewport-showcase.ts +292 -0
- package/scripts/generate-sdk-operation-inventory.ts +5 -0
- package/scripts/resident-memory-bench.ts +1341 -0
- package/scripts/run-acp-conformance.ts +285 -0
- package/scripts/verify-gjc-sdk-canonicalization.ts +122 -18
- package/scripts/verify-sticky-viewport-showcase.ts +611 -0
- package/src/async/job-manager.ts +78 -20
- package/src/capability/index.ts +8 -0
- package/src/cli/fast-help.ts +1 -1
- package/src/cli/plugin-cli.ts +176 -18
- package/src/cli/read-cli.ts +2 -1
- package/src/cli.ts +1 -0
- 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/team.ts +20 -2
- package/src/config/file-lock.ts +11 -2
- 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 +74 -1
- package/src/config/settings.ts +246 -108
- package/src/coordinator-mcp/question-gate-codec.ts +1 -1
- package/src/coordinator-mcp/server.ts +5 -12
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +230 -82
- package/src/defaults/gjc/skills/ralplan/SKILL.md +77 -41
- package/src/defaults/gjc/skills/team/SKILL.md +3 -1
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +62 -39
- package/src/defaults/gjc/skills/ultragoal/validation-batch-contracts.md +12 -0
- package/src/defaults/gjc-defaults.ts +4 -4
- package/src/eval/py/executor.ts +7 -2
- package/src/exa/mcp-client.ts +12 -3
- package/src/exec/bash-executor.ts +144 -27
- package/src/extensibility/extensions/runner.ts +15 -0
- package/src/extensibility/extensions/types.ts +46 -5
- package/src/extensibility/gjc-plugins/constrained-hooks.ts +8 -2
- package/src/extensibility/gjc-plugins/index.ts +19 -2
- package/src/extensibility/gjc-plugins/installer.ts +189 -50
- package/src/extensibility/gjc-plugins/lifecycle-reconciliation.ts +199 -0
- package/src/extensibility/gjc-plugins/lifecycle.ts +626 -0
- package/src/extensibility/gjc-plugins/observability.ts +3 -3
- package/src/extensibility/gjc-plugins/prompt-appendix.ts +5 -1
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +12 -2
- package/src/extensibility/gjc-plugins/runtime-quarantine.ts +121 -0
- package/src/extensibility/gjc-plugins/schema.ts +84 -12
- package/src/extensibility/gjc-plugins/session-validation.ts +9 -3
- package/src/extensibility/gjc-plugins/types.ts +141 -0
- package/src/extensibility/gjc-plugins/validation.ts +9 -0
- package/src/extensibility/shared-events.ts +5 -0
- package/src/gjc-runtime/deep-interview-ambiguity.ts +47 -82
- 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 -943
- package/src/gjc-runtime/gc-render.ts +1 -0
- package/src/gjc-runtime/gc-runtime.ts +11 -1
- package/src/gjc-runtime/launch-tmux.ts +25 -6
- package/src/gjc-runtime/managed-owner-admission.ts +1 -1
- package/src/gjc-runtime/memory-guard-owner-claims.ts +389 -0
- package/src/gjc-runtime/ralplan-runtime.ts +1149 -98
- package/src/gjc-runtime/state-runtime.ts +2 -1
- package/src/gjc-runtime/state-writer.ts +70 -222
- package/src/gjc-runtime/team-launch.ts +42 -5
- package/src/gjc-runtime/team-runtime.ts +1085 -40
- package/src/gjc-runtime/team-store.ts +32 -0
- package/src/gjc-runtime/team-worker-heartbeat.ts +149 -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 +28 -0
- package/src/gjc-runtime/tmux-sessions.ts +58 -13
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +20 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +414 -618
- package/src/gjc-runtime/workflow-manifest.generated.json +105 -449
- package/src/gjc-runtime/workflow-manifest.ts +38 -247
- 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 +21 -22
- 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 +98 -0
- package/src/modes/acp/acp-agent.ts +339 -191
- package/src/modes/acp/acp-event-mapper.ts +14 -7
- package/src/modes/acp/acp-mode.ts +39 -3
- package/src/modes/components/assistant-message.ts +123 -22
- package/src/modes/components/gjc-bundle-settings.ts +508 -0
- package/src/modes/components/irc-sidebar.ts +176 -62
- package/src/modes/components/model-selector.ts +61 -4
- package/src/modes/components/notifications-settings-editor.ts +12 -1
- package/src/modes/components/oauth-selector.ts +96 -28
- package/src/modes/components/read-tool-group.ts +82 -14
- package/src/modes/components/session-observer-overlay.ts +457 -83
- package/src/modes/components/settings-selector.ts +116 -23
- package/src/modes/components/status-line/gh.ts +16 -0
- package/src/modes/components/tool-execution.ts +138 -27
- package/src/modes/components/tool-status-header.ts +3 -14
- package/src/modes/components/transcript-viewer-overlay.ts +99 -24
- 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 +26 -2
- package/src/modes/controllers/input-controller.ts +18 -3
- package/src/modes/controllers/selector-controller.ts +127 -36
- package/src/modes/interactive-mode.ts +62 -3
- package/src/modes/irc-observation-ledger.ts +71 -15
- package/src/modes/runtime-init.ts +22 -2
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/ui-helpers.ts +94 -1
- package/src/prompts/agent-fragments/ralplan-persistence.md +4 -3
- package/src/prompts/agents/architect.md +9 -1
- package/src/prompts/agents/critic.md +9 -0
- package/src/prompts/system/eager-todo.md +8 -4
- package/src/prompts/system/system-prompt.md +11 -5
- package/src/prompts/tools/bash.md +5 -5
- package/src/prompts/tools/read.md +4 -2
- package/src/prompts/tools/search-tool-bm25.md +5 -0
- package/src/prompts/tools/task.md +1 -1
- package/src/reminders/star-reminder.ts +4 -42
- package/src/runtime-mcp/manager.ts +19 -4
- package/src/runtime-mcp/smithery-auth.ts +17 -2
- package/src/runtime-mcp/smithery-connect.ts +11 -1
- package/src/sdk/acp/adapter.ts +22 -6
- package/src/sdk/broker/lifecycle.ts +25 -6
- package/src/sdk/broker/transport.ts +35 -3
- package/src/sdk/bus/attachment-registry.ts +5 -1
- package/src/sdk/bus/chat-daemon-cli.ts +4 -39
- package/src/sdk/bus/chat-daemon-control.ts +6 -3
- package/src/sdk/bus/config.ts +18 -2
- package/src/sdk/bus/control-drain-lease.ts +104 -0
- package/src/sdk/bus/index.ts +984 -146
- package/src/sdk/bus/kind-aware-reconciliation.ts +317 -0
- package/src/sdk/bus/lifecycle-commands.ts +4 -1
- package/src/sdk/bus/lifecycle-control-runtime.ts +403 -88
- package/src/sdk/bus/prompt-reconciliation.ts +2 -1
- package/src/sdk/bus/reconciliation-store.ts +303 -0
- package/src/sdk/bus/rich-draft.ts +1 -1
- package/src/sdk/bus/rich-render.ts +2 -2
- package/src/sdk/bus/telegram-adoption-intent.ts +574 -0
- package/src/sdk/bus/telegram-cli.ts +33 -8
- package/src/sdk/bus/telegram-daemon-cli.ts +1 -0
- package/src/sdk/bus/telegram-daemon-contract.ts +11 -4
- package/src/sdk/bus/telegram-daemon.ts +916 -23
- package/src/sdk/bus/telegram-reference.ts +40 -3
- package/src/sdk/bus/threaded-render.ts +48 -3
- package/src/sdk/bus/topic-registry.ts +20 -0
- package/src/sdk/host/control/dispatch.ts +5 -1
- package/src/sdk/host/control/operations.ts +1 -1
- package/src/sdk/host/host.ts +23 -1
- package/src/sdk/host/query/handlers.ts +55 -0
- package/src/sdk/host/query/revision-store.ts +36 -2
- package/src/sdk/host/reverse-leases.ts +44 -7
- package/src/sdk/mcp/server.ts +46 -18
- package/src/sdk/prompt-status.ts +16 -3
- package/src/sdk/protocol/operation-inventory.generated.json +51 -0
- package/src/sdk/protocol/operation-registry.ts +7 -2
- package/src/sdk/session.ts +101 -10
- package/src/session/agent-session.ts +1286 -220
- package/src/session/artifacts.ts +22 -0
- package/src/session/auth-broker-config.ts +10 -3
- package/src/session/blob-store.ts +915 -24
- package/src/session/internal/managed-session-scope.ts +44 -6
- package/src/session/internal/managed-session-storage.ts +48 -6
- package/src/session/memory-guard-checkpoint-participant.ts +131 -0
- package/src/session/session-manager.ts +2364 -438
- package/src/session/session-storage.ts +9 -1
- package/src/session/streaming-output.ts +500 -111
- package/src/setup/credential-import.ts +75 -13
- package/src/setup/provider-onboarding.ts +19 -4
- package/src/skill-state/active-state.ts +32 -10
- package/src/skill-state/workflow-hud.ts +42 -2
- package/src/skill-state/workflow-mutation-guard.ts +560 -8
- package/src/task/executor.ts +122 -39
- package/src/task/index.ts +23 -14
- package/src/task/provider-retry-status.ts +16 -0
- package/src/task/receipt.ts +23 -7
- package/src/task/render.ts +6 -2
- package/src/task/types.ts +108 -0
- package/src/tools/ask-answer-registry.ts +12 -6
- package/src/tools/ask.ts +169 -118
- package/src/tools/ast-edit.ts +1 -0
- package/src/tools/ast-grep.ts +1 -0
- package/src/tools/bash-interactive.ts +8 -3
- package/src/tools/bash.ts +503 -60
- package/src/tools/browser/launch.ts +41 -6
- package/src/tools/fetch.ts +114 -23
- package/src/tools/find.ts +1 -0
- package/src/tools/image-gen.ts +29 -5
- package/src/tools/index.ts +4 -0
- package/src/tools/output-meta.ts +281 -20
- package/src/tools/path-utils.ts +8 -3
- package/src/tools/read-internals.ts +44 -0
- package/src/tools/read.ts +1560 -310
- package/src/tools/resource-gc.ts +182 -18
- package/src/tools/review.ts +5 -7
- package/src/tools/search.ts +1 -0
- package/src/tools/subagent-render.ts +6 -2
- package/src/tools/subagent.ts +6 -0
- package/src/tools/telegram-send.ts +6 -3
- package/src/tools/tool-result.ts +14 -1
- package/src/utils/gh.ts +39 -0
- package/src/utils/image-resize.ts +3 -0
- package/src/web/search/providers/anthropic.ts +4 -4
- package/src/web/search/providers/codex.ts +1 -1
- package/src/web/search/providers/kimi.ts +15 -5
- package/src/web/search/providers/searxng.ts +47 -4
- package/src/web/search/providers/xai.ts +7 -2
- 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/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +0 -23
- package/src/defaults/gjc/skills/ultragoal/pipeline-validation-contracts.md +0 -30
- 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
- package/src/gjc-runtime/ultragoal-pipeline.ts +0 -306
|
@@ -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",
|
|
@@ -534,6 +551,40 @@ export const GJC_TEAM_API_OPERATIONS = [
|
|
|
534
551
|
"write-task-approval",
|
|
535
552
|
] as const;
|
|
536
553
|
|
|
554
|
+
export type GjcTeamApiOperation = (typeof GJC_TEAM_API_OPERATIONS)[number];
|
|
555
|
+
|
|
556
|
+
export class UnknownGjcTeamApiOperationError extends Error {
|
|
557
|
+
readonly code = "unknown_team_api_operation";
|
|
558
|
+
readonly operation: string;
|
|
559
|
+
readonly suggestions: readonly string[];
|
|
560
|
+
|
|
561
|
+
constructor(operation: string, suggestions: readonly string[]) {
|
|
562
|
+
const guidance =
|
|
563
|
+
suggestions.length > 0
|
|
564
|
+
? `did you mean ${suggestions.join(" or ")}?`
|
|
565
|
+
: "run gjc team api --help for supported operations";
|
|
566
|
+
super(`unknown_team_api_operation:${operation}; ${guidance}`);
|
|
567
|
+
this.name = "UnknownGjcTeamApiOperationError";
|
|
568
|
+
this.operation = operation;
|
|
569
|
+
this.suggestions = suggestions;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function isGjcTeamApiOperation(operation: string): operation is GjcTeamApiOperation {
|
|
574
|
+
return (GJC_TEAM_API_OPERATIONS as readonly string[]).includes(operation);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function unknownGjcTeamApiOperationSuggestions(operation: string): readonly string[] {
|
|
578
|
+
if (operation === "heartbeat") return ["read-worker-heartbeat", "update-worker-heartbeat"];
|
|
579
|
+
if (operation === "get-task") return ["read-task"];
|
|
580
|
+
return [];
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function resolveGjcTeamApiOperation(operation: string): GjcTeamApiOperation {
|
|
584
|
+
if (isGjcTeamApiOperation(operation)) return operation;
|
|
585
|
+
throw new UnknownGjcTeamApiOperationError(operation, unknownGjcTeamApiOperationSuggestions(operation));
|
|
586
|
+
}
|
|
587
|
+
|
|
537
588
|
function currentTimeMs(): number {
|
|
538
589
|
return gjcTeamRuntimeTestSeams?.nowMs?.() ?? Date.now();
|
|
539
590
|
}
|
|
@@ -1218,6 +1269,838 @@ async function readConfigForWorkerIntegration(dir: string): Promise<GjcTeamConfi
|
|
|
1218
1269
|
|
|
1219
1270
|
return config;
|
|
1220
1271
|
}
|
|
1272
|
+
function manifestPath(dir: string): string {
|
|
1273
|
+
return path.join(dir, "manifest.v2.json");
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
function manifestRecordFromConfig(config: GjcTeamConfig): Record<string, unknown> {
|
|
1277
|
+
return {
|
|
1278
|
+
version: 2,
|
|
1279
|
+
team_name: config.team_name,
|
|
1280
|
+
display_name: config.display_name,
|
|
1281
|
+
requested_name: config.requested_name,
|
|
1282
|
+
tmux_session: config.tmux_session,
|
|
1283
|
+
tmux_session_name: config.tmux_session_name,
|
|
1284
|
+
tmux_target: config.tmux_target,
|
|
1285
|
+
worker_command: config.worker_command,
|
|
1286
|
+
worker_cli_plan: config.worker_cli_plan,
|
|
1287
|
+
tmux_command: config.tmux_command,
|
|
1288
|
+
leader: config.leader,
|
|
1289
|
+
workers: config.workers,
|
|
1290
|
+
workspace_mode: config.workspace_mode,
|
|
1291
|
+
dry_run: config.dry_run,
|
|
1292
|
+
created_at: config.created_at,
|
|
1293
|
+
updated_at: config.updated_at,
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
async function syncTeamConfigAndManifest(dir: string, config: GjcTeamConfig): Promise<void> {
|
|
1298
|
+
await writeJsonFile(path.join(dir, "config.json"), config);
|
|
1299
|
+
await writeJsonFile(manifestPath(dir), manifestRecordFromConfig(config));
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
async function readWorkerMemoryGuardLedger(
|
|
1303
|
+
dir: string,
|
|
1304
|
+
workerId: string,
|
|
1305
|
+
platform: string,
|
|
1306
|
+
): Promise<GjcTeamWorkerMemoryGuardLedger> {
|
|
1307
|
+
const ledgerPath = workerMemoryGuardLedgerPath(dir, workerId);
|
|
1308
|
+
const existing = await readJsonFile<unknown>(ledgerPath);
|
|
1309
|
+
if (isCanonicalGjcTeamWorkerMemoryGuardLedger(existing) && existing.worker_id === workerId) return existing;
|
|
1310
|
+
const ledger = createInitialGjcTeamWorkerMemoryGuardLedger({ workerId, platform, now: now() });
|
|
1311
|
+
await writeJsonFile(ledgerPath, ledger);
|
|
1312
|
+
return ledger;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
async function writeWorkerMemoryGuardLedger(
|
|
1316
|
+
dir: string,
|
|
1317
|
+
ledger: GjcTeamWorkerMemoryGuardLedger,
|
|
1318
|
+
): Promise<GjcTeamWorkerMemoryGuardLedger> {
|
|
1319
|
+
await writeJsonFile(workerMemoryGuardLedgerPath(dir, ledger.worker_id), ledger);
|
|
1320
|
+
return ledger;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
async function appendWorkerMemoryGuardAction(input: {
|
|
1324
|
+
dir: string;
|
|
1325
|
+
teamName: string;
|
|
1326
|
+
cwd: string;
|
|
1327
|
+
workerId: string;
|
|
1328
|
+
task?: GjcTeamTask;
|
|
1329
|
+
incidentId: string;
|
|
1330
|
+
action: "advisory" | "replace" | "blocked";
|
|
1331
|
+
result: "noop" | "scheduled" | "succeeded" | "failed" | "blocked";
|
|
1332
|
+
reason: string;
|
|
1333
|
+
}): Promise<void> {
|
|
1334
|
+
const workerPath = path.join(input.dir, "workers", safePathSegment("worker_id", input.workerId));
|
|
1335
|
+
const entries = await readTeamWorkerMemoryGuardLedger(workerPath);
|
|
1336
|
+
await appendTeamWorkerMemoryGuardLedgerEntry(
|
|
1337
|
+
workerPath,
|
|
1338
|
+
{
|
|
1339
|
+
schema_version: 1,
|
|
1340
|
+
recorded_at: now(),
|
|
1341
|
+
incident_id: input.incidentId,
|
|
1342
|
+
team_name: input.teamName,
|
|
1343
|
+
worker_id: input.workerId,
|
|
1344
|
+
task_id: input.task?.id ?? "unclaimed",
|
|
1345
|
+
claim_token: input.task?.claim?.token ?? "unclaimed",
|
|
1346
|
+
attempt: nextTeamWorkerMemoryGuardAttempt(entries, input.incidentId),
|
|
1347
|
+
platform: process.platform,
|
|
1348
|
+
action: input.action,
|
|
1349
|
+
result: input.result,
|
|
1350
|
+
reason: input.reason,
|
|
1351
|
+
},
|
|
1352
|
+
{ cwd: input.cwd },
|
|
1353
|
+
);
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
function normalizeWorkerMemoryGuardPlatform(value: unknown): string {
|
|
1357
|
+
return typeof value === "string" && value.trim() ? value.trim() : process.platform;
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
async function readLinuxProcessStartTime(pid: number): Promise<string | undefined> {
|
|
1361
|
+
if (process.platform !== "linux" || !Number.isSafeInteger(pid) || pid <= 0) return undefined;
|
|
1362
|
+
try {
|
|
1363
|
+
const stat = await Bun.file(`/proc/${pid}/stat`).text();
|
|
1364
|
+
const commandEnd = stat.lastIndexOf(")");
|
|
1365
|
+
if (commandEnd < 0) return undefined;
|
|
1366
|
+
const fields = stat
|
|
1367
|
+
.slice(commandEnd + 2)
|
|
1368
|
+
.trim()
|
|
1369
|
+
.split(/\s+/);
|
|
1370
|
+
const startTime = fields[19];
|
|
1371
|
+
return startTime && /^\d+$/.test(startTime) ? startTime : undefined;
|
|
1372
|
+
} catch {
|
|
1373
|
+
return undefined;
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
function normalizeWorkerMemoryGuardSelectionCandidates(
|
|
1378
|
+
value: unknown,
|
|
1379
|
+
ledgers: Map<string, GjcTeamWorkerMemoryGuardLedger>,
|
|
1380
|
+
): GjcTeamWorkerMemoryGuardSelectionCandidate[] {
|
|
1381
|
+
if (!Array.isArray(value)) return [];
|
|
1382
|
+
const candidates: GjcTeamWorkerMemoryGuardSelectionCandidate[] = [];
|
|
1383
|
+
for (const entry of value) {
|
|
1384
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
|
|
1385
|
+
const record = entry as Record<string, unknown>;
|
|
1386
|
+
const workerId =
|
|
1387
|
+
typeof record.worker_id === "string"
|
|
1388
|
+
? record.worker_id.trim()
|
|
1389
|
+
: typeof record.workerId === "string"
|
|
1390
|
+
? record.workerId.trim()
|
|
1391
|
+
: "";
|
|
1392
|
+
if (!workerId) continue;
|
|
1393
|
+
const ledger = ledgers.get(workerId);
|
|
1394
|
+
const excessBytes = Number(record.excess_bytes ?? record.excessBytes ?? 0);
|
|
1395
|
+
if (!Number.isFinite(excessBytes)) continue;
|
|
1396
|
+
candidates.push({
|
|
1397
|
+
worker_id: workerId,
|
|
1398
|
+
platform:
|
|
1399
|
+
typeof record.platform === "string" && record.platform.trim()
|
|
1400
|
+
? record.platform.trim()
|
|
1401
|
+
: (ledger?.platform ?? process.platform),
|
|
1402
|
+
excess_bytes: excessBytes,
|
|
1403
|
+
retry_count:
|
|
1404
|
+
typeof record.retry_count === "number" && Number.isInteger(record.retry_count)
|
|
1405
|
+
? record.retry_count
|
|
1406
|
+
: (ledger?.retry_count ?? 0),
|
|
1407
|
+
retry_limit:
|
|
1408
|
+
typeof record.retry_limit === "number" && Number.isInteger(record.retry_limit) && record.retry_limit > 0
|
|
1409
|
+
? record.retry_limit
|
|
1410
|
+
: (ledger?.retry_limit ?? 2),
|
|
1411
|
+
blocked: typeof record.blocked === "boolean" ? record.blocked : ledger?.state === "blocked",
|
|
1412
|
+
current_task_id:
|
|
1413
|
+
typeof record.current_task_id === "string"
|
|
1414
|
+
? record.current_task_id
|
|
1415
|
+
: typeof record.currentTaskId === "string"
|
|
1416
|
+
? record.currentTaskId
|
|
1417
|
+
: ledger?.current_task_id,
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
1420
|
+
return candidates;
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
function withMemoryGuardRetry(
|
|
1424
|
+
ledger: GjcTeamWorkerMemoryGuardLedger,
|
|
1425
|
+
input: {
|
|
1426
|
+
platform: string;
|
|
1427
|
+
reason: string;
|
|
1428
|
+
incidentId: string;
|
|
1429
|
+
currentTaskId?: string;
|
|
1430
|
+
pidProbe?: GjcTeamWorkerMemoryGuardLedger["last_pid_probe"];
|
|
1431
|
+
nowIso: string;
|
|
1432
|
+
},
|
|
1433
|
+
): { ledger: GjcTeamWorkerMemoryGuardLedger; finalBlocked: boolean } {
|
|
1434
|
+
const retryCount = Math.min(ledger.retry_count + 1, ledger.retry_limit);
|
|
1435
|
+
const finalBlocked = retryCount >= ledger.retry_limit;
|
|
1436
|
+
return {
|
|
1437
|
+
ledger: {
|
|
1438
|
+
...ledger,
|
|
1439
|
+
platform: input.platform,
|
|
1440
|
+
state: finalBlocked ? "blocked" : "retrying",
|
|
1441
|
+
retry_count: retryCount,
|
|
1442
|
+
current_task_id: input.currentTaskId,
|
|
1443
|
+
last_incident_id: input.incidentId,
|
|
1444
|
+
last_reason: input.reason,
|
|
1445
|
+
last_pid_probe: input.pidProbe,
|
|
1446
|
+
updated_at: input.nowIso,
|
|
1447
|
+
},
|
|
1448
|
+
finalBlocked,
|
|
1449
|
+
};
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
async function finalizeWorkerMemoryGuardBlockedState(input: {
|
|
1453
|
+
teamName: string;
|
|
1454
|
+
dir: string;
|
|
1455
|
+
worker: GjcTeamWorker;
|
|
1456
|
+
task: GjcTeamTask | undefined;
|
|
1457
|
+
taskMutation: GjcTeamTaskMutationCapability;
|
|
1458
|
+
reason: string;
|
|
1459
|
+
cwd: string;
|
|
1460
|
+
env: NodeJS.ProcessEnv;
|
|
1461
|
+
}): Promise<void> {
|
|
1462
|
+
await updateWorkerStatus(
|
|
1463
|
+
workerRuntime,
|
|
1464
|
+
input.teamName,
|
|
1465
|
+
input.worker.id,
|
|
1466
|
+
"blocked",
|
|
1467
|
+
input.cwd,
|
|
1468
|
+
input.env,
|
|
1469
|
+
input.task?.id,
|
|
1470
|
+
input.reason,
|
|
1471
|
+
);
|
|
1472
|
+
await writeLifecycleRecord(workerRuntime, input.dir, input.worker, "failed", { stop_reason: input.reason });
|
|
1473
|
+
const task = input.task;
|
|
1474
|
+
if (!task?.claim) return;
|
|
1475
|
+
await input.taskMutation.transition(task.id, "blocked", task.claim.token, input.worker.id);
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
async function checkpointWorkerForMemoryGuard(
|
|
1479
|
+
worker: GjcTeamWorker,
|
|
1480
|
+
taskId: string | undefined,
|
|
1481
|
+
): Promise<{ ok: true; checkpoint: GjcTeamWorkerMemoryGuardCheckpoint } | { ok: false; reason: string }> {
|
|
1482
|
+
if (!worker.worktree_path) return { ok: false, reason: "worker_worktree_missing" };
|
|
1483
|
+
let classification: GjcWorkerCheckpointClassification;
|
|
1484
|
+
try {
|
|
1485
|
+
classification = classifyWorkerCheckpointStatus(worker.worktree_path);
|
|
1486
|
+
} catch {
|
|
1487
|
+
return { ok: false, reason: "checkpoint_git_error" };
|
|
1488
|
+
}
|
|
1489
|
+
if (
|
|
1490
|
+
classification.kind !== "clean" &&
|
|
1491
|
+
classification.kind !== "eligible" &&
|
|
1492
|
+
classification.kind !== "protected_only"
|
|
1493
|
+
)
|
|
1494
|
+
return { ok: false, reason: `checkpoint_${classification.kind}` };
|
|
1495
|
+
let commit: string | null = null;
|
|
1496
|
+
if (classification.kind === "eligible") {
|
|
1497
|
+
const added = runGitResult(worker.worktree_path, ["add", "--", ...classification.files]);
|
|
1498
|
+
if (!added.ok) return { ok: false, reason: "checkpoint_git_add_failed" };
|
|
1499
|
+
const message = `gjc(team): memory-guard checkpoint ${worker.id} [${taskId ?? "unknown"}]`;
|
|
1500
|
+
const committed = runGitResult(worker.worktree_path, [
|
|
1501
|
+
"commit",
|
|
1502
|
+
"--no-verify",
|
|
1503
|
+
"--only",
|
|
1504
|
+
"-m",
|
|
1505
|
+
message,
|
|
1506
|
+
"--",
|
|
1507
|
+
...classification.files,
|
|
1508
|
+
]);
|
|
1509
|
+
if (!committed.ok) return { ok: false, reason: "checkpoint_git_commit_failed" };
|
|
1510
|
+
commit = resolveHead(worker.worktree_path);
|
|
1511
|
+
}
|
|
1512
|
+
return {
|
|
1513
|
+
ok: true,
|
|
1514
|
+
checkpoint: {
|
|
1515
|
+
kind: classification.kind,
|
|
1516
|
+
files: classification.files,
|
|
1517
|
+
head: resolveHead(worker.worktree_path),
|
|
1518
|
+
commit,
|
|
1519
|
+
recorded_at: now(),
|
|
1520
|
+
},
|
|
1521
|
+
};
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
async function relaunchWorkerPaneForMemoryGuard(input: {
|
|
1525
|
+
config: GjcTeamConfig;
|
|
1526
|
+
worker: GjcTeamWorker;
|
|
1527
|
+
platform: NodeJS.Platform;
|
|
1528
|
+
startupAckPath: string;
|
|
1529
|
+
replacementToken: string;
|
|
1530
|
+
startupAckTimeoutMs: number;
|
|
1531
|
+
env: NodeJS.ProcessEnv;
|
|
1532
|
+
}): Promise<string> {
|
|
1533
|
+
if (input.config.dry_run)
|
|
1534
|
+
return `%memory-guard-${input.worker.id}-${stableHash(`${input.worker.id}:${now()}`).slice(0, 8)}`;
|
|
1535
|
+
const workerCommand = buildWorkerCommand(
|
|
1536
|
+
input.config,
|
|
1537
|
+
input.worker,
|
|
1538
|
+
input.platform,
|
|
1539
|
+
`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}`,
|
|
1540
|
+
input.env,
|
|
1541
|
+
);
|
|
1542
|
+
const workerCwd = input.worker.worktree_path ?? input.config.leader.cwd;
|
|
1543
|
+
const useSendKeysFallback = shouldDispatchWorkerWithSendKeys(input.config.tmux_command, input.platform);
|
|
1544
|
+
const splitTarget =
|
|
1545
|
+
input.worker.pane_id && paneBelongsToTeamTarget(input.config, input.worker.pane_id)
|
|
1546
|
+
? input.worker.pane_id
|
|
1547
|
+
: input.config.tmux_target;
|
|
1548
|
+
const split = Bun.spawnSync(
|
|
1549
|
+
[
|
|
1550
|
+
input.config.tmux_command,
|
|
1551
|
+
"split-window",
|
|
1552
|
+
"-v",
|
|
1553
|
+
"-t",
|
|
1554
|
+
splitTarget,
|
|
1555
|
+
"-d",
|
|
1556
|
+
"-P",
|
|
1557
|
+
"-F",
|
|
1558
|
+
"#{pane_id}",
|
|
1559
|
+
"-c",
|
|
1560
|
+
workerCwd,
|
|
1561
|
+
...(useSendKeysFallback ? [] : [workerCommand]),
|
|
1562
|
+
],
|
|
1563
|
+
{ stdout: "pipe", stderr: "pipe" },
|
|
1564
|
+
);
|
|
1565
|
+
if (split.exitCode !== 0)
|
|
1566
|
+
throw new Error(split.stderr.toString().trim() || `memory_guard_split_failed:${input.worker.id}`);
|
|
1567
|
+
const newPaneId = split.stdout.toString().trim().split(/\r?\n/)[0]?.trim() ?? "";
|
|
1568
|
+
if (!newPaneId.startsWith("%")) throw new Error(`memory_guard_split_missing_pane:${input.worker.id}`);
|
|
1569
|
+
if (useSendKeysFallback) {
|
|
1570
|
+
Bun.spawnSync([input.config.tmux_command, "send-keys", "-l", "-t", newPaneId, workerCommand], {
|
|
1571
|
+
stdout: "ignore",
|
|
1572
|
+
stderr: "ignore",
|
|
1573
|
+
});
|
|
1574
|
+
Bun.spawnSync([input.config.tmux_command, "send-keys", "-t", newPaneId, "Enter"], {
|
|
1575
|
+
stdout: "ignore",
|
|
1576
|
+
stderr: "ignore",
|
|
1577
|
+
});
|
|
1578
|
+
}
|
|
1579
|
+
const startupDeadline = Date.now() + input.startupAckTimeoutMs;
|
|
1580
|
+
while (true) {
|
|
1581
|
+
try {
|
|
1582
|
+
const ack = await Bun.file(input.startupAckPath).json();
|
|
1583
|
+
if (
|
|
1584
|
+
ack &&
|
|
1585
|
+
typeof ack === "object" &&
|
|
1586
|
+
"replacement_token" in ack &&
|
|
1587
|
+
ack.replacement_token === input.replacementToken
|
|
1588
|
+
)
|
|
1589
|
+
break;
|
|
1590
|
+
} catch {
|
|
1591
|
+
// The successor may still be publishing its generation-bound ACK.
|
|
1592
|
+
}
|
|
1593
|
+
if (Date.now() >= startupDeadline) {
|
|
1594
|
+
Bun.spawnSync([input.config.tmux_command, "kill-pane", "-t", newPaneId], {
|
|
1595
|
+
stdout: "ignore",
|
|
1596
|
+
stderr: "ignore",
|
|
1597
|
+
});
|
|
1598
|
+
throw new Error(`memory_guard_successor_startup_timeout:${input.worker.id}`);
|
|
1599
|
+
}
|
|
1600
|
+
await Bun.sleep(50);
|
|
1601
|
+
}
|
|
1602
|
+
const successorPane = probePaneTeamTarget(input.config, newPaneId);
|
|
1603
|
+
if (!successorPane.exists || !successorPane.belongsToTeamTarget) {
|
|
1604
|
+
Bun.spawnSync([input.config.tmux_command, "kill-pane", "-t", newPaneId], {
|
|
1605
|
+
stdout: "ignore",
|
|
1606
|
+
stderr: "ignore",
|
|
1607
|
+
});
|
|
1608
|
+
throw new Error(`memory_guard_successor_pane_unavailable:${input.worker.id}`);
|
|
1609
|
+
}
|
|
1610
|
+
if (input.worker.pane_id) {
|
|
1611
|
+
const oldPane = probePaneTeamTarget(input.config, input.worker.pane_id);
|
|
1612
|
+
if (oldPane.exists && !oldPane.belongsToTeamTarget) {
|
|
1613
|
+
Bun.spawnSync([input.config.tmux_command, "kill-pane", "-t", newPaneId], {
|
|
1614
|
+
stdout: "ignore",
|
|
1615
|
+
stderr: "ignore",
|
|
1616
|
+
});
|
|
1617
|
+
throw new Error(`memory_guard_old_pane_outside_team_target:${input.worker.id}`);
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
Bun.spawnSync([input.config.tmux_command, "select-layout", "-t", input.config.tmux_target, "main-vertical"], {
|
|
1621
|
+
stdout: "ignore",
|
|
1622
|
+
stderr: "ignore",
|
|
1623
|
+
});
|
|
1624
|
+
return newPaneId;
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
async function applyWorkerMemoryGuardUnlocked(input: {
|
|
1628
|
+
teamName: string;
|
|
1629
|
+
workerId?: string;
|
|
1630
|
+
requestedWorkerId: string;
|
|
1631
|
+
reason?: string;
|
|
1632
|
+
incidentId?: string;
|
|
1633
|
+
platform: string;
|
|
1634
|
+
pidProbe?: GjcTeamWorkerMemoryGuardLedger["last_pid_probe"];
|
|
1635
|
+
candidates?: unknown;
|
|
1636
|
+
cwd: string;
|
|
1637
|
+
env: NodeJS.ProcessEnv;
|
|
1638
|
+
allowAutomaticAction?: boolean;
|
|
1639
|
+
replacementToken?: string;
|
|
1640
|
+
dir: string;
|
|
1641
|
+
/**
|
|
1642
|
+
* Acquire the team task-mutation fence only for the blocked-state transition.
|
|
1643
|
+
* Must not be held across the successor startup-ack wait: concurrent
|
|
1644
|
+
* `worker-startup-ack` must publish while replacement is in flight.
|
|
1645
|
+
*/
|
|
1646
|
+
withTaskMutation: <T>(fn: (capability: GjcTeamTaskMutationCapability) => Promise<T>) => Promise<T>;
|
|
1647
|
+
}): Promise<Record<string, unknown>> {
|
|
1648
|
+
const dir = input.dir;
|
|
1649
|
+
const config = await readConfig(dir);
|
|
1650
|
+
const ledgers = new Map<string, GjcTeamWorkerMemoryGuardLedger>();
|
|
1651
|
+
for (const candidate of config.workers)
|
|
1652
|
+
ledgers.set(candidate.id, await readWorkerMemoryGuardLedger(dir, candidate.id, input.platform));
|
|
1653
|
+
let workerId = input.workerId ?? input.requestedWorkerId;
|
|
1654
|
+
const parsedCandidates = normalizeWorkerMemoryGuardSelectionCandidates(input.candidates, ledgers);
|
|
1655
|
+
if (!input.workerId && Array.isArray(input.candidates)) {
|
|
1656
|
+
const selected = selectGjcTeamWorkerMemoryGuardCandidate(parsedCandidates);
|
|
1657
|
+
if (!selected)
|
|
1658
|
+
return {
|
|
1659
|
+
ok: true,
|
|
1660
|
+
result: "advisory",
|
|
1661
|
+
lifecycle_mutated: false,
|
|
1662
|
+
reason: "no_eligible_worker_memory_guard_candidate",
|
|
1663
|
+
};
|
|
1664
|
+
workerId = selected.worker_id;
|
|
1665
|
+
}
|
|
1666
|
+
const worker = findKnownWorker(config, workerId);
|
|
1667
|
+
const authorityInventory = await readGjcContinuationAuthorityInventory(dir);
|
|
1668
|
+
const authority = authorityInventory.valid
|
|
1669
|
+
? selectGjcContinuationWorkerAuthority(authorityInventory, worker.id)
|
|
1670
|
+
: { valid: false as const, taskCount: 0, claimCount: 0 };
|
|
1671
|
+
const task = authority.valid ? authority.task : undefined;
|
|
1672
|
+
const leaseExpiresAt = authority.valid ? Date.parse(authority.claim.leased_until) : Number.NaN;
|
|
1673
|
+
let ledger = ledgers.get(worker.id) ?? (await readWorkerMemoryGuardLedger(dir, worker.id, input.platform));
|
|
1674
|
+
const nowIso = now();
|
|
1675
|
+
const currentTaskId = task?.id;
|
|
1676
|
+
const incidentId = input.incidentId ?? stableHash(`${worker.id}:${currentTaskId ?? "none"}:${nowIso}`).slice(0, 16);
|
|
1677
|
+
const baseReason = input.reason?.trim() || "memory_guard_requested";
|
|
1678
|
+
if (!authority.valid || !Number.isFinite(leaseExpiresAt) || leaseExpiresAt <= currentTimeMs()) {
|
|
1679
|
+
const noClaimReason =
|
|
1680
|
+
process.platform !== "linux" || input.platform !== "linux"
|
|
1681
|
+
? `unsupported_platform:${input.platform}:host:${process.platform}:${baseReason}`
|
|
1682
|
+
: "worker_has_no_exact_active_claim";
|
|
1683
|
+
ledger = {
|
|
1684
|
+
...ledger,
|
|
1685
|
+
platform: input.platform,
|
|
1686
|
+
state: "advisory",
|
|
1687
|
+
current_task_id: undefined,
|
|
1688
|
+
last_incident_id: incidentId,
|
|
1689
|
+
last_reason: noClaimReason,
|
|
1690
|
+
last_pid_probe: input.pidProbe,
|
|
1691
|
+
updated_at: nowIso,
|
|
1692
|
+
};
|
|
1693
|
+
await writeWorkerMemoryGuardLedger(dir, ledger);
|
|
1694
|
+
await appendWorkerMemoryGuardAction({
|
|
1695
|
+
dir,
|
|
1696
|
+
teamName: input.teamName,
|
|
1697
|
+
cwd: input.cwd,
|
|
1698
|
+
workerId: worker.id,
|
|
1699
|
+
task,
|
|
1700
|
+
incidentId,
|
|
1701
|
+
action: "advisory",
|
|
1702
|
+
result: "noop",
|
|
1703
|
+
reason: noClaimReason,
|
|
1704
|
+
});
|
|
1705
|
+
return {
|
|
1706
|
+
ok: true,
|
|
1707
|
+
result: "advisory",
|
|
1708
|
+
lifecycle_mutated: false,
|
|
1709
|
+
ledger,
|
|
1710
|
+
reason: noClaimReason,
|
|
1711
|
+
};
|
|
1712
|
+
}
|
|
1713
|
+
if (ledger.state === "blocked" || ledger.retry_count >= ledger.retry_limit) {
|
|
1714
|
+
await appendWorkerMemoryGuardAction({
|
|
1715
|
+
dir,
|
|
1716
|
+
teamName: input.teamName,
|
|
1717
|
+
cwd: input.cwd,
|
|
1718
|
+
workerId: worker.id,
|
|
1719
|
+
task,
|
|
1720
|
+
incidentId,
|
|
1721
|
+
action: "blocked",
|
|
1722
|
+
result: "blocked",
|
|
1723
|
+
reason: ledger.last_reason ?? "retry_limit",
|
|
1724
|
+
});
|
|
1725
|
+
return { ok: true, result: "blocked", lifecycle_mutated: false, ledger };
|
|
1726
|
+
}
|
|
1727
|
+
if (input.allowAutomaticAction !== undefined)
|
|
1728
|
+
ledger = {
|
|
1729
|
+
...ledger,
|
|
1730
|
+
automatic_action_allowed: input.allowAutomaticAction,
|
|
1731
|
+
updated_at: nowIso,
|
|
1732
|
+
};
|
|
1733
|
+
if (process.platform !== "linux" || input.platform !== "linux") {
|
|
1734
|
+
ledger = {
|
|
1735
|
+
...ledger,
|
|
1736
|
+
platform: input.platform,
|
|
1737
|
+
state: "advisory",
|
|
1738
|
+
current_task_id: currentTaskId,
|
|
1739
|
+
last_incident_id: incidentId,
|
|
1740
|
+
last_reason: `unsupported_platform:${input.platform}:host:${process.platform}:${baseReason}`,
|
|
1741
|
+
last_pid_probe: input.pidProbe,
|
|
1742
|
+
updated_at: nowIso,
|
|
1743
|
+
};
|
|
1744
|
+
await writeWorkerMemoryGuardLedger(dir, ledger);
|
|
1745
|
+
await appendWorkerMemoryGuardAction({
|
|
1746
|
+
dir,
|
|
1747
|
+
teamName: input.teamName,
|
|
1748
|
+
cwd: input.cwd,
|
|
1749
|
+
workerId: worker.id,
|
|
1750
|
+
task,
|
|
1751
|
+
incidentId,
|
|
1752
|
+
action: "advisory",
|
|
1753
|
+
result: "noop",
|
|
1754
|
+
reason: ledger.last_reason ?? "unsupported_platform",
|
|
1755
|
+
});
|
|
1756
|
+
return { ok: true, result: "advisory", lifecycle_mutated: false, ledger };
|
|
1757
|
+
}
|
|
1758
|
+
if (!ledger.automatic_action_allowed) {
|
|
1759
|
+
ledger = {
|
|
1760
|
+
...ledger,
|
|
1761
|
+
platform: input.platform,
|
|
1762
|
+
state: "advisory",
|
|
1763
|
+
current_task_id: currentTaskId,
|
|
1764
|
+
last_incident_id: incidentId,
|
|
1765
|
+
last_reason: "automatic_linux_action_disabled",
|
|
1766
|
+
last_pid_probe: input.pidProbe,
|
|
1767
|
+
updated_at: nowIso,
|
|
1768
|
+
};
|
|
1769
|
+
await writeWorkerMemoryGuardLedger(dir, ledger);
|
|
1770
|
+
await appendWorkerMemoryGuardAction({
|
|
1771
|
+
dir,
|
|
1772
|
+
teamName: input.teamName,
|
|
1773
|
+
cwd: input.cwd,
|
|
1774
|
+
workerId: worker.id,
|
|
1775
|
+
task,
|
|
1776
|
+
incidentId,
|
|
1777
|
+
action: "advisory",
|
|
1778
|
+
result: "noop",
|
|
1779
|
+
reason: "automatic_linux_action_disabled",
|
|
1780
|
+
});
|
|
1781
|
+
return { ok: true, result: "advisory", lifecycle_mutated: false, ledger };
|
|
1782
|
+
}
|
|
1783
|
+
const controllerPath = path.join(dir, "memory-guard-controller.json");
|
|
1784
|
+
const controllerProcessStart = await readLinuxProcessStartTime(process.pid);
|
|
1785
|
+
const controllerId = `pid:${process.pid}:start:${controllerProcessStart ?? "unknown"}`;
|
|
1786
|
+
const controllerNow = currentTimeMs();
|
|
1787
|
+
const controllerCooldownMs = parseDurationEnv(input.env, "GJC_TEAM_MEMORY_GUARD_ACTION_COOLDOWN_MS", 120_000);
|
|
1788
|
+
const existingController = await readJsonFile<{
|
|
1789
|
+
controller_id?: string;
|
|
1790
|
+
cooldown_until?: string;
|
|
1791
|
+
}>(controllerPath);
|
|
1792
|
+
const existingCooldownUntil = Date.parse(existingController?.cooldown_until ?? "");
|
|
1793
|
+
if (
|
|
1794
|
+
existingController?.controller_id &&
|
|
1795
|
+
existingController.controller_id !== controllerId &&
|
|
1796
|
+
Number.isFinite(existingCooldownUntil) &&
|
|
1797
|
+
existingCooldownUntil > controllerNow
|
|
1798
|
+
)
|
|
1799
|
+
return {
|
|
1800
|
+
ok: true,
|
|
1801
|
+
result: "advisory",
|
|
1802
|
+
lifecycle_mutated: false,
|
|
1803
|
+
reason: "team_memory_guard_controller_active",
|
|
1804
|
+
};
|
|
1805
|
+
await writeJsonFile(controllerPath, {
|
|
1806
|
+
schema_version: 1,
|
|
1807
|
+
controller_id: controllerId,
|
|
1808
|
+
worker_id: worker.id,
|
|
1809
|
+
incident_id: incidentId,
|
|
1810
|
+
reserved_at: new Date(controllerNow).toISOString(),
|
|
1811
|
+
cooldown_until: new Date(controllerNow + controllerCooldownMs).toISOString(),
|
|
1812
|
+
});
|
|
1813
|
+
const checkpoint = await checkpointWorkerForMemoryGuard(worker, currentTaskId);
|
|
1814
|
+
if (!checkpoint.ok) {
|
|
1815
|
+
const retried = withMemoryGuardRetry(ledger, {
|
|
1816
|
+
platform: input.platform,
|
|
1817
|
+
reason: checkpoint.reason,
|
|
1818
|
+
incidentId,
|
|
1819
|
+
currentTaskId,
|
|
1820
|
+
pidProbe: input.pidProbe,
|
|
1821
|
+
nowIso,
|
|
1822
|
+
});
|
|
1823
|
+
ledger = retried.ledger;
|
|
1824
|
+
await writeWorkerMemoryGuardLedger(dir, ledger);
|
|
1825
|
+
if (retried.finalBlocked)
|
|
1826
|
+
await input.withTaskMutation(taskMutation =>
|
|
1827
|
+
finalizeWorkerMemoryGuardBlockedState({
|
|
1828
|
+
teamName: input.teamName,
|
|
1829
|
+
dir,
|
|
1830
|
+
worker,
|
|
1831
|
+
task,
|
|
1832
|
+
taskMutation,
|
|
1833
|
+
reason: checkpoint.reason,
|
|
1834
|
+
cwd: input.cwd,
|
|
1835
|
+
env: input.env,
|
|
1836
|
+
}),
|
|
1837
|
+
);
|
|
1838
|
+
await appendWorkerMemoryGuardAction({
|
|
1839
|
+
dir,
|
|
1840
|
+
teamName: input.teamName,
|
|
1841
|
+
cwd: input.cwd,
|
|
1842
|
+
workerId: worker.id,
|
|
1843
|
+
task,
|
|
1844
|
+
incidentId,
|
|
1845
|
+
action: retried.finalBlocked ? "blocked" : "replace",
|
|
1846
|
+
result: retried.finalBlocked ? "blocked" : "failed",
|
|
1847
|
+
reason: checkpoint.reason,
|
|
1848
|
+
});
|
|
1849
|
+
return {
|
|
1850
|
+
ok: true,
|
|
1851
|
+
result: retried.finalBlocked ? "blocked" : "retrying",
|
|
1852
|
+
lifecycle_mutated: retried.finalBlocked,
|
|
1853
|
+
ledger,
|
|
1854
|
+
};
|
|
1855
|
+
}
|
|
1856
|
+
const refreshedInventory = await readGjcContinuationAuthorityInventory(dir);
|
|
1857
|
+
const refreshedAuthority = refreshedInventory.valid
|
|
1858
|
+
? selectGjcContinuationWorkerAuthority(refreshedInventory, worker.id)
|
|
1859
|
+
: { valid: false as const, taskCount: 0, claimCount: 0 };
|
|
1860
|
+
if (
|
|
1861
|
+
!refreshedAuthority.valid ||
|
|
1862
|
+
refreshedAuthority.task.id !== task?.id ||
|
|
1863
|
+
refreshedAuthority.claim.token !== task?.claim?.token ||
|
|
1864
|
+
refreshedAuthority.claim.leased_until !== task?.claim?.leased_until ||
|
|
1865
|
+
Date.parse(refreshedAuthority.claim.leased_until) <= currentTimeMs()
|
|
1866
|
+
)
|
|
1867
|
+
return {
|
|
1868
|
+
ok: true,
|
|
1869
|
+
result: "advisory",
|
|
1870
|
+
lifecycle_mutated: false,
|
|
1871
|
+
reason: "worker_claim_authority_changed",
|
|
1872
|
+
};
|
|
1873
|
+
const startupAckPath = path.join(dir, "workers", safePathSegment("worker_id", worker.id), "startup-ack.json");
|
|
1874
|
+
const previousStartupAck = (await Bun.file(startupAckPath).exists())
|
|
1875
|
+
? await Bun.file(startupAckPath).text()
|
|
1876
|
+
: undefined;
|
|
1877
|
+
const lifecyclePath = workerLifecyclePath(dir, worker.id);
|
|
1878
|
+
const previousLifecycle = (await Bun.file(lifecyclePath).exists())
|
|
1879
|
+
? await Bun.file(lifecyclePath).text()
|
|
1880
|
+
: undefined;
|
|
1881
|
+
const restorePredecessorStartupState = async (): Promise<void> => {
|
|
1882
|
+
if (previousLifecycle === undefined) await fs.rm(lifecyclePath, { force: true });
|
|
1883
|
+
else await Bun.write(lifecyclePath, previousLifecycle);
|
|
1884
|
+
if (previousStartupAck === undefined) await fs.rm(startupAckPath, { force: true });
|
|
1885
|
+
else await Bun.write(startupAckPath, previousStartupAck);
|
|
1886
|
+
};
|
|
1887
|
+
await fs.rm(startupAckPath, { force: true });
|
|
1888
|
+
const replacementToken = input.replacementToken ?? randomUUID();
|
|
1889
|
+
const startupAckTimeoutMs = parseDurationEnv(input.env, "GJC_TEAM_MEMORY_GUARD_STARTUP_TIMEOUT_MS", 120_000);
|
|
1890
|
+
let newPaneId: string;
|
|
1891
|
+
try {
|
|
1892
|
+
newPaneId = await relaunchWorkerPaneForMemoryGuard({
|
|
1893
|
+
config,
|
|
1894
|
+
worker,
|
|
1895
|
+
platform: process.platform,
|
|
1896
|
+
startupAckPath,
|
|
1897
|
+
replacementToken,
|
|
1898
|
+
startupAckTimeoutMs,
|
|
1899
|
+
env: input.env,
|
|
1900
|
+
});
|
|
1901
|
+
} catch (error) {
|
|
1902
|
+
await restorePredecessorStartupState();
|
|
1903
|
+
const reason = error instanceof Error && error.message ? `relaunch_failed:${error.message}` : "relaunch_failed";
|
|
1904
|
+
const retried = withMemoryGuardRetry(ledger, {
|
|
1905
|
+
platform: input.platform,
|
|
1906
|
+
reason,
|
|
1907
|
+
incidentId,
|
|
1908
|
+
currentTaskId,
|
|
1909
|
+
pidProbe: input.pidProbe,
|
|
1910
|
+
nowIso,
|
|
1911
|
+
});
|
|
1912
|
+
ledger = retried.ledger;
|
|
1913
|
+
await writeWorkerMemoryGuardLedger(dir, ledger);
|
|
1914
|
+
if (retried.finalBlocked)
|
|
1915
|
+
await input.withTaskMutation(taskMutation =>
|
|
1916
|
+
finalizeWorkerMemoryGuardBlockedState({
|
|
1917
|
+
teamName: input.teamName,
|
|
1918
|
+
dir,
|
|
1919
|
+
worker,
|
|
1920
|
+
task,
|
|
1921
|
+
taskMutation,
|
|
1922
|
+
reason,
|
|
1923
|
+
cwd: input.cwd,
|
|
1924
|
+
env: input.env,
|
|
1925
|
+
}),
|
|
1926
|
+
);
|
|
1927
|
+
await appendWorkerMemoryGuardAction({
|
|
1928
|
+
dir,
|
|
1929
|
+
teamName: input.teamName,
|
|
1930
|
+
cwd: input.cwd,
|
|
1931
|
+
workerId: worker.id,
|
|
1932
|
+
task,
|
|
1933
|
+
incidentId,
|
|
1934
|
+
action: retried.finalBlocked ? "blocked" : "replace",
|
|
1935
|
+
result: retried.finalBlocked ? "blocked" : "failed",
|
|
1936
|
+
reason,
|
|
1937
|
+
});
|
|
1938
|
+
return {
|
|
1939
|
+
ok: true,
|
|
1940
|
+
result: retried.finalBlocked ? "blocked" : "retrying",
|
|
1941
|
+
lifecycle_mutated: retried.finalBlocked,
|
|
1942
|
+
ledger,
|
|
1943
|
+
};
|
|
1944
|
+
}
|
|
1945
|
+
const postAckInventory = await readGjcContinuationAuthorityInventory(dir);
|
|
1946
|
+
const postAckAuthority = postAckInventory.valid
|
|
1947
|
+
? selectGjcContinuationWorkerAuthority(postAckInventory, worker.id)
|
|
1948
|
+
: { valid: false as const, taskCount: 0, claimCount: 0 };
|
|
1949
|
+
if (
|
|
1950
|
+
!postAckAuthority.valid ||
|
|
1951
|
+
postAckAuthority.task.id !== task?.id ||
|
|
1952
|
+
postAckAuthority.claim.token !== task?.claim?.token ||
|
|
1953
|
+
postAckAuthority.claim.leased_until !== task?.claim?.leased_until ||
|
|
1954
|
+
Date.parse(postAckAuthority.claim.leased_until) <= currentTimeMs()
|
|
1955
|
+
) {
|
|
1956
|
+
Bun.spawnSync([config.tmux_command, "kill-pane", "-t", newPaneId], {
|
|
1957
|
+
stdout: "ignore",
|
|
1958
|
+
stderr: "ignore",
|
|
1959
|
+
});
|
|
1960
|
+
await restorePredecessorStartupState();
|
|
1961
|
+
return {
|
|
1962
|
+
ok: true,
|
|
1963
|
+
result: "advisory",
|
|
1964
|
+
lifecycle_mutated: false,
|
|
1965
|
+
reason: "worker_claim_authority_changed_after_startup",
|
|
1966
|
+
};
|
|
1967
|
+
}
|
|
1968
|
+
const successorPane = probePaneTeamTarget(config, newPaneId);
|
|
1969
|
+
if (!successorPane.exists || !successorPane.belongsToTeamTarget || !successorPane.pid) {
|
|
1970
|
+
Bun.spawnSync([config.tmux_command, "kill-pane", "-t", newPaneId], {
|
|
1971
|
+
stdout: "ignore",
|
|
1972
|
+
stderr: "ignore",
|
|
1973
|
+
});
|
|
1974
|
+
await restorePredecessorStartupState();
|
|
1975
|
+
return {
|
|
1976
|
+
ok: true,
|
|
1977
|
+
result: "advisory",
|
|
1978
|
+
lifecycle_mutated: false,
|
|
1979
|
+
reason: "successor_pane_unavailable_after_startup",
|
|
1980
|
+
};
|
|
1981
|
+
}
|
|
1982
|
+
const heartbeatPath = path.join(dir, "workers", safePathSegment("worker_id", worker.id), "heartbeat.json");
|
|
1983
|
+
const previousHeartbeat = (await Bun.file(heartbeatPath).exists())
|
|
1984
|
+
? await Bun.file(heartbeatPath).text()
|
|
1985
|
+
: undefined;
|
|
1986
|
+
const successorHeartbeat: WorkerHeartbeatFile = {
|
|
1987
|
+
pid: successorPane.pid,
|
|
1988
|
+
last_turn_at: now(),
|
|
1989
|
+
turn_count: 0,
|
|
1990
|
+
alive: true,
|
|
1991
|
+
process_start_time: await readLinuxProcessStartTime(successorPane.pid),
|
|
1992
|
+
};
|
|
1993
|
+
const nextConfig: GjcTeamConfig = {
|
|
1994
|
+
...config,
|
|
1995
|
+
workers: config.workers.map(candidate =>
|
|
1996
|
+
candidate.id === worker.id
|
|
1997
|
+
? { ...candidate, pane_id: newPaneId, last_heartbeat: nowIso, status: "idle" }
|
|
1998
|
+
: candidate,
|
|
1999
|
+
),
|
|
2000
|
+
updated_at: nowIso,
|
|
2001
|
+
};
|
|
2002
|
+
const rollbackReplacement = async (): Promise<void> => {
|
|
2003
|
+
Bun.spawnSync([config.tmux_command, "kill-pane", "-t", newPaneId], {
|
|
2004
|
+
stdout: "ignore",
|
|
2005
|
+
stderr: "ignore",
|
|
2006
|
+
});
|
|
2007
|
+
if (previousHeartbeat === undefined) await fs.rm(heartbeatPath, { force: true });
|
|
2008
|
+
else await Bun.write(heartbeatPath, previousHeartbeat);
|
|
2009
|
+
await syncTeamConfigAndManifest(dir, config);
|
|
2010
|
+
await restorePredecessorStartupState();
|
|
2011
|
+
};
|
|
2012
|
+
try {
|
|
2013
|
+
await writeJsonFile(heartbeatPath, successorHeartbeat);
|
|
2014
|
+
await syncTeamConfigAndManifest(dir, nextConfig);
|
|
2015
|
+
await writeLifecycleRecord(workerRuntime, dir, { ...worker, pane_id: newPaneId }, "ready", {
|
|
2016
|
+
pane_id: newPaneId,
|
|
2017
|
+
started_at: nowIso,
|
|
2018
|
+
stop_reason: undefined,
|
|
2019
|
+
stopped_at: undefined,
|
|
2020
|
+
});
|
|
2021
|
+
const cutoverPane = probePaneTeamTarget(config, newPaneId);
|
|
2022
|
+
if (!cutoverPane.exists || !cutoverPane.belongsToTeamTarget || cutoverPane.pid !== successorHeartbeat.pid)
|
|
2023
|
+
throw new Error(`memory_guard_successor_pane_changed:${worker.id}`);
|
|
2024
|
+
if (worker.pane_id && !config.dry_run) {
|
|
2025
|
+
const killed = Bun.spawnSync([config.tmux_command, "kill-pane", "-t", worker.pane_id], {
|
|
2026
|
+
stdout: "ignore",
|
|
2027
|
+
stderr: "pipe",
|
|
2028
|
+
});
|
|
2029
|
+
if (killed.exitCode !== 0 && probePaneTeamTarget(config, worker.pane_id).exists)
|
|
2030
|
+
throw new Error(killed.stderr.toString().trim() || `memory_guard_kill_failed:${worker.id}`);
|
|
2031
|
+
}
|
|
2032
|
+
} catch (error) {
|
|
2033
|
+
await rollbackReplacement();
|
|
2034
|
+
throw new Error(
|
|
2035
|
+
error instanceof Error && error.message
|
|
2036
|
+
? `memory_guard_replacement_commit_failed:${error.message}`
|
|
2037
|
+
: "memory_guard_replacement_commit_failed",
|
|
2038
|
+
);
|
|
2039
|
+
}
|
|
2040
|
+
ledger = {
|
|
2041
|
+
...ledger,
|
|
2042
|
+
platform: input.platform,
|
|
2043
|
+
state: "replaced",
|
|
2044
|
+
retry_count: 0,
|
|
2045
|
+
current_task_id: currentTaskId,
|
|
2046
|
+
last_incident_id: incidentId,
|
|
2047
|
+
last_reason: baseReason,
|
|
2048
|
+
last_pid_probe: input.pidProbe,
|
|
2049
|
+
last_checkpoint: checkpoint.checkpoint,
|
|
2050
|
+
last_replacement: {
|
|
2051
|
+
old_pane_id: worker.pane_id,
|
|
2052
|
+
new_pane_id: newPaneId,
|
|
2053
|
+
recorded_at: nowIso,
|
|
2054
|
+
},
|
|
2055
|
+
updated_at: nowIso,
|
|
2056
|
+
};
|
|
2057
|
+
await writeWorkerMemoryGuardLedger(dir, ledger);
|
|
2058
|
+
await appendWorkerMemoryGuardAction({
|
|
2059
|
+
dir,
|
|
2060
|
+
teamName: input.teamName,
|
|
2061
|
+
cwd: input.cwd,
|
|
2062
|
+
workerId: worker.id,
|
|
2063
|
+
task,
|
|
2064
|
+
incidentId,
|
|
2065
|
+
action: "replace",
|
|
2066
|
+
result: "succeeded",
|
|
2067
|
+
reason: baseReason,
|
|
2068
|
+
});
|
|
2069
|
+
await appendEvent(dir, {
|
|
2070
|
+
type: "worker_memory_guard_replaced",
|
|
2071
|
+
worker: worker.id,
|
|
2072
|
+
task_id: currentTaskId,
|
|
2073
|
+
message: `Replaced ${worker.id} after memory-guard checkpoint`,
|
|
2074
|
+
data: {
|
|
2075
|
+
incident_id: incidentId,
|
|
2076
|
+
checkpoint_kind: checkpoint.checkpoint.kind,
|
|
2077
|
+
checkpoint_commit: checkpoint.checkpoint.commit,
|
|
2078
|
+
old_pane_id: worker.pane_id,
|
|
2079
|
+
new_pane_id: newPaneId,
|
|
2080
|
+
},
|
|
2081
|
+
});
|
|
2082
|
+
return {
|
|
2083
|
+
ok: true,
|
|
2084
|
+
result: "replaced",
|
|
2085
|
+
lifecycle_mutated: true,
|
|
2086
|
+
ledger,
|
|
2087
|
+
checkpoint: checkpoint.checkpoint,
|
|
2088
|
+
};
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
async function applyWorkerMemoryGuard(
|
|
2092
|
+
input: Omit<Parameters<typeof applyWorkerMemoryGuardUnlocked>[0], "dir" | "withTaskMutation">,
|
|
2093
|
+
): Promise<Record<string, unknown>> {
|
|
2094
|
+
const dir = await findTeamDir(input.teamName, input.cwd, input.env);
|
|
2095
|
+
// Do not hold withGjcTeamTaskMutation across relaunchWorkerPaneForMemoryGuard's
|
|
2096
|
+
// startup-ack poll (default 120s). That fence would serialize concurrent
|
|
2097
|
+
// worker-startup-ack publication and hang selector-replacement under CI load.
|
|
2098
|
+
return applyWorkerMemoryGuardUnlocked({
|
|
2099
|
+
...input,
|
|
2100
|
+
dir,
|
|
2101
|
+
withTaskMutation: fn => withGjcTeamTaskMutation(taskStore(dir), fn),
|
|
2102
|
+
});
|
|
2103
|
+
}
|
|
1221
2104
|
async function readPhase(dir: string): Promise<GjcTeamPhase> {
|
|
1222
2105
|
try {
|
|
1223
2106
|
const phase = await readJsonFile<{ current_phase?: GjcTeamPhase }>(path.join(dir, "phase.json"));
|
|
@@ -1652,7 +2535,11 @@ function tagTmuxSessionAsGjcLeader(tmuxCommand: string, sessionName: string): bo
|
|
|
1652
2535
|
return result.exitCode === 0;
|
|
1653
2536
|
}
|
|
1654
2537
|
|
|
1655
|
-
function readCurrentTmuxLeaderContext(
|
|
2538
|
+
function readCurrentTmuxLeaderContext(
|
|
2539
|
+
tmuxCommand: string,
|
|
2540
|
+
env: NodeJS.ProcessEnv,
|
|
2541
|
+
adoptUnmanagedSession = true,
|
|
2542
|
+
): GjcTmuxLeaderContext {
|
|
1656
2543
|
if (Bun.which(tmuxCommand) === null)
|
|
1657
2544
|
throw new Error(buildTeamTmuxLeaderRequirementMessage(`tmux_not_installed:${tmuxCommand}`));
|
|
1658
2545
|
// Prefer the explicit GJC-managed session name propagated by `gjc --tmux`
|
|
@@ -1685,7 +2572,7 @@ function readCurrentTmuxLeaderContext(tmuxCommand: string, env: NodeJS.ProcessEn
|
|
|
1685
2572
|
const [sessionName = "", windowIndex = ""] = sessionAndWindow.split(":");
|
|
1686
2573
|
if (!sessionName || !windowIndex || !leaderPaneId.startsWith("%"))
|
|
1687
2574
|
throw new Error(buildTeamTmuxLeaderRequirementMessage(`invalid_tmux_context:${result.stdout.toString().trim()}`));
|
|
1688
|
-
if (readGjcTmuxProfileValue(tmuxCommand, sessionName) !== GJC_TMUX_PROFILE_VALUE) {
|
|
2575
|
+
if (adoptUnmanagedSession && readGjcTmuxProfileValue(tmuxCommand, sessionName) !== GJC_TMUX_PROFILE_VALUE) {
|
|
1689
2576
|
// Adopt any real tmux leader as a GJC team leader — including a session
|
|
1690
2577
|
// the user created outside `gjc --tmux` — by writing GJC's @gjc-profile
|
|
1691
2578
|
// ownership tag and reading it back. A provider that round-trips tmux
|
|
@@ -1708,6 +2595,20 @@ function readCurrentTmuxLeaderContext(tmuxCommand: string, env: NodeJS.ProcessEn
|
|
|
1708
2595
|
target: `${sessionName}:${windowIndex}`,
|
|
1709
2596
|
};
|
|
1710
2597
|
}
|
|
2598
|
+
/**
|
|
2599
|
+
* Check whether the current process can launch a team without changing tmux state.
|
|
2600
|
+
* Unlike the launch path, this never adopts or tags an unmanaged tmux session.
|
|
2601
|
+
*/
|
|
2602
|
+
export function probeGjcTeamAvailability(
|
|
2603
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
2604
|
+
): { available: true } | { available: false; reason: string } {
|
|
2605
|
+
try {
|
|
2606
|
+
readCurrentTmuxLeaderContext(resolveGjcTmuxCommand(env), env, false);
|
|
2607
|
+
return { available: true };
|
|
2608
|
+
} catch (error) {
|
|
2609
|
+
return { available: false, reason: error instanceof Error ? error.message : String(error) };
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
1711
2612
|
function isBunVirtualPath(candidate: string | undefined): boolean {
|
|
1712
2613
|
const normalized = candidate?.trim().replace(/\\/g, "/").toLowerCase();
|
|
1713
2614
|
return (
|
|
@@ -1886,7 +2787,7 @@ async function startTmuxSession(
|
|
|
1886
2787
|
const splitDirection: string = worker.index === 1 ? "-h" : "-v";
|
|
1887
2788
|
const splitTarget: string =
|
|
1888
2789
|
worker.index === 1 ? config.tmux_target : (rightStackRootPaneId ?? config.tmux_target);
|
|
1889
|
-
const workerCommand = buildWorkerCommand(config, worker);
|
|
2790
|
+
const workerCommand = buildWorkerCommand(config, worker, process.platform, undefined, env);
|
|
1890
2791
|
const workerCwd = worker.worktree_path ?? config.leader.cwd;
|
|
1891
2792
|
const useSendKeysFallback = shouldDispatchWorkerWithSendKeys(config.tmux_command);
|
|
1892
2793
|
const splitArgs = [
|
|
@@ -1998,15 +2899,27 @@ async function startTmuxSession(
|
|
|
1998
2899
|
throw error;
|
|
1999
2900
|
}
|
|
2000
2901
|
}
|
|
2902
|
+
function probePaneTeamTarget(
|
|
2903
|
+
config: GjcTeamConfig,
|
|
2904
|
+
paneId: string,
|
|
2905
|
+
): { exists: boolean; belongsToTeamTarget: boolean; pid?: number } {
|
|
2906
|
+
if (paneId === config.leader.pane_id) return { exists: true, belongsToTeamTarget: false };
|
|
2907
|
+
const result = Bun.spawnSync(
|
|
2908
|
+
[config.tmux_command, "display-message", "-p", "-t", paneId, "#S:#I #{pane_id} #{pane_pid}"],
|
|
2909
|
+
{ stdout: "pipe", stderr: "ignore" },
|
|
2910
|
+
);
|
|
2911
|
+
if (result.exitCode !== 0) return { exists: false, belongsToTeamTarget: false };
|
|
2912
|
+
const [target = "", detectedPaneId = "", rawPid = ""] = result.stdout.toString().trim().split(/\s+/);
|
|
2913
|
+
const pid = Number(rawPid);
|
|
2914
|
+
return {
|
|
2915
|
+
exists: true,
|
|
2916
|
+
belongsToTeamTarget: target === config.tmux_target && detectedPaneId === paneId,
|
|
2917
|
+
pid: Number.isInteger(pid) && pid > 0 ? pid : undefined,
|
|
2918
|
+
};
|
|
2919
|
+
}
|
|
2920
|
+
|
|
2001
2921
|
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;
|
|
2922
|
+
return probePaneTeamTarget(config, paneId).belongsToTeamTarget;
|
|
2010
2923
|
}
|
|
2011
2924
|
function killWorkerPanes(config: GjcTeamConfig): void {
|
|
2012
2925
|
for (const worker of config.workers)
|
|
@@ -2147,13 +3060,24 @@ const UNMERGED_GIT_STATUS_CODES = new Set(["DD", "AU", "UD", "UA", "DU", "AA", "
|
|
|
2147
3060
|
// into the leader branch on projects that do not gitignore `.gjc/_session-*/`.
|
|
2148
3061
|
const PROTECTED_WORKER_CHECKPOINT_PREFIXES = [".gjc/_session-*/"];
|
|
2149
3062
|
|
|
2150
|
-
function
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
3063
|
+
function parsePorcelainStatus(stdout: string): { files: string[]; statusCodes: string[] } {
|
|
3064
|
+
const records = stdout.split("\0");
|
|
3065
|
+
const files: string[] = [];
|
|
3066
|
+
const statusCodes: string[] = [];
|
|
3067
|
+
for (let index = 0; index < records.length; index++) {
|
|
3068
|
+
const record = records[index];
|
|
3069
|
+
if (!record) continue;
|
|
3070
|
+
const statusCode = record.slice(0, 2);
|
|
3071
|
+
const file = record.slice(3);
|
|
3072
|
+
if (!file) continue;
|
|
3073
|
+
statusCodes.push(statusCode);
|
|
3074
|
+
files.push(file);
|
|
3075
|
+
if (statusCode.includes("R") || statusCode.includes("C")) {
|
|
3076
|
+
const source = records[++index];
|
|
3077
|
+
if (source) files.push(source);
|
|
3078
|
+
}
|
|
3079
|
+
}
|
|
3080
|
+
return { files: [...new Set(files)], statusCodes };
|
|
2157
3081
|
}
|
|
2158
3082
|
|
|
2159
3083
|
function normalizeGitStatusPath(filePath: string): string {
|
|
@@ -2180,7 +3104,7 @@ export function classifyGjcTeamCheckpointFiles(files: string[]): {
|
|
|
2180
3104
|
}
|
|
2181
3105
|
|
|
2182
3106
|
export function classifyWorkerCheckpointStatus(cwd: string): GjcWorkerCheckpointClassification {
|
|
2183
|
-
const status = runGitResult(cwd, ["status", "--porcelain", "-uall"]);
|
|
3107
|
+
const status = runGitResult(cwd, ["status", "--porcelain=v1", "-z", "-uall"]);
|
|
2184
3108
|
if (!status.ok) {
|
|
2185
3109
|
return {
|
|
2186
3110
|
kind: "git_error",
|
|
@@ -2189,11 +3113,9 @@ export function classifyWorkerCheckpointStatus(cwd: string): GjcWorkerCheckpoint
|
|
|
2189
3113
|
};
|
|
2190
3114
|
}
|
|
2191
3115
|
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)));
|
|
3116
|
+
const parsed = parsePorcelainStatus(status.stdout);
|
|
3117
|
+
const files = parsed.files;
|
|
3118
|
+
const hasUnmergedStatus = parsed.statusCodes.some(code => UNMERGED_GIT_STATUS_CODES.has(code));
|
|
2197
3119
|
const conflictFiles = listConflictFiles(cwd);
|
|
2198
3120
|
if (hasUnmergedStatus || conflictFiles.length > 0) {
|
|
2199
3121
|
return {
|
|
@@ -2210,7 +3132,7 @@ export async function classifyWorkerCheckpointStatusAsync(
|
|
|
2210
3132
|
cwd: string,
|
|
2211
3133
|
signal?: AbortSignal,
|
|
2212
3134
|
): Promise<GjcWorkerCheckpointClassification> {
|
|
2213
|
-
const status = await runGitResultAsync(cwd, ["status", "--porcelain", "-uall"], signal);
|
|
3135
|
+
const status = await runGitResultAsync(cwd, ["status", "--porcelain=v1", "-z", "-uall"], signal);
|
|
2214
3136
|
if (!status.ok) {
|
|
2215
3137
|
return {
|
|
2216
3138
|
kind: "git_error",
|
|
@@ -2219,11 +3141,9 @@ export async function classifyWorkerCheckpointStatusAsync(
|
|
|
2219
3141
|
};
|
|
2220
3142
|
}
|
|
2221
3143
|
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)));
|
|
3144
|
+
const parsed = parsePorcelainStatus(status.stdout);
|
|
3145
|
+
const files = parsed.files;
|
|
3146
|
+
const hasUnmergedStatus = parsed.statusCodes.some(code => UNMERGED_GIT_STATUS_CODES.has(code));
|
|
2227
3147
|
const conflictFiles = await listConflictFilesAsync(cwd, signal);
|
|
2228
3148
|
if (hasUnmergedStatus || conflictFiles.length > 0) {
|
|
2229
3149
|
return {
|
|
@@ -3387,8 +4307,7 @@ async function writeGjcWorkerStartupAck(
|
|
|
3387
4307
|
env: NodeJS.ProcessEnv,
|
|
3388
4308
|
input: Record<string, unknown>,
|
|
3389
4309
|
): Promise<Record<string, unknown>> {
|
|
3390
|
-
|
|
3391
|
-
return withGjcTeamMutationFence(dir, () => writeWorkerStartupAck(workerRuntime, teamName, worker, cwd, env, input));
|
|
4310
|
+
return writeWorkerStartupAck(workerRuntime, teamName, worker, cwd, env, input);
|
|
3392
4311
|
}
|
|
3393
4312
|
function parseDurationEnv(env: NodeJS.ProcessEnv, name: string, fallbackMs: number): number {
|
|
3394
4313
|
const raw = env[name]?.trim();
|
|
@@ -3397,11 +4316,13 @@ function parseDurationEnv(env: NodeJS.ProcessEnv, name: string, fallbackMs: numb
|
|
|
3397
4316
|
return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallbackMs;
|
|
3398
4317
|
}
|
|
3399
4318
|
|
|
3400
|
-
|
|
4319
|
+
/** Positive stale windows are clamped so a worker can publish strictly before expiry. */
|
|
4320
|
+
export function parseHeartbeatStaleMs(env: NodeJS.ProcessEnv): number {
|
|
3401
4321
|
const raw = env.GJC_TEAM_HEARTBEAT_STALE_MS?.trim();
|
|
3402
4322
|
if (!raw) return 120_000;
|
|
3403
4323
|
const parsed = Number(raw);
|
|
3404
|
-
|
|
4324
|
+
if (!Number.isFinite(parsed)) return 120_000;
|
|
4325
|
+
return parsed > 0 ? Math.max(3, parsed) : parsed;
|
|
3405
4326
|
}
|
|
3406
4327
|
async function writeLifecycleNudge(
|
|
3407
4328
|
dir: string,
|
|
@@ -3901,6 +4822,41 @@ export async function updateGjcWorkerHeartbeat(
|
|
|
3901
4822
|
updateWorkerHeartbeat(workerRuntime, teamName, worker, heartbeat, cwd, env),
|
|
3902
4823
|
);
|
|
3903
4824
|
}
|
|
4825
|
+
/**
|
|
4826
|
+
* Refresh liveness without discarding metadata written by other heartbeat
|
|
4827
|
+
* producers. The read/merge/write sequence shares the team mutation fence with
|
|
4828
|
+
* CLI heartbeat updates, so a runtime tick cannot overwrite a newer turn count.
|
|
4829
|
+
*/
|
|
4830
|
+
export async function refreshGjcWorkerHeartbeat(
|
|
4831
|
+
teamName: string,
|
|
4832
|
+
worker: string,
|
|
4833
|
+
pid: number,
|
|
4834
|
+
cwd = process.cwd(),
|
|
4835
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
4836
|
+
): Promise<WorkerHeartbeatFile> {
|
|
4837
|
+
const dir = await findTeamDir(teamName, cwd, env);
|
|
4838
|
+
return withGjcTeamMutationFence(dir, async () => {
|
|
4839
|
+
const current = await readWorkerHeartbeat(workerRuntime, teamName, worker, cwd, env);
|
|
4840
|
+
const processStartTime = await readLinuxProcessStartTime(pid);
|
|
4841
|
+
const turnCount = current?.turn_count;
|
|
4842
|
+
return updateWorkerHeartbeat(
|
|
4843
|
+
workerRuntime,
|
|
4844
|
+
teamName,
|
|
4845
|
+
worker,
|
|
4846
|
+
{
|
|
4847
|
+
...current,
|
|
4848
|
+
pid,
|
|
4849
|
+
last_turn_at: now(),
|
|
4850
|
+
turn_count: Number.isSafeInteger(turnCount) && (turnCount ?? -1) >= 0 ? (turnCount ?? 0) : 0,
|
|
4851
|
+
alive: true,
|
|
4852
|
+
// Never carry another process's incarnation across a pid change.
|
|
4853
|
+
process_start_time: processStartTime ?? (current?.pid === pid ? current.process_start_time : undefined),
|
|
4854
|
+
},
|
|
4855
|
+
cwd,
|
|
4856
|
+
env,
|
|
4857
|
+
);
|
|
4858
|
+
});
|
|
4859
|
+
}
|
|
3904
4860
|
export async function writeGjcWorkerInbox(
|
|
3905
4861
|
teamName: string,
|
|
3906
4862
|
worker: string,
|
|
@@ -4046,12 +5002,13 @@ export async function executeGjcTeamApiOperation(
|
|
|
4046
5002
|
cwd = process.cwd(),
|
|
4047
5003
|
env: NodeJS.ProcessEnv = process.env,
|
|
4048
5004
|
): Promise<unknown> {
|
|
5005
|
+
const resolvedOperation = resolveGjcTeamApiOperation(operation);
|
|
4049
5006
|
const teamName = String(input.team_name ?? input.teamName ?? "").trim();
|
|
4050
5007
|
if (!teamName) throw new Error("missing_team_name");
|
|
4051
5008
|
const workerInput = input.worker ?? input.worker_id ?? input.workerId;
|
|
4052
5009
|
const worker = String(workerInput ?? "worker-1");
|
|
4053
5010
|
const explicitWorker = workerInput == null ? undefined : String(workerInput);
|
|
4054
|
-
switch (
|
|
5011
|
+
switch (resolvedOperation) {
|
|
4055
5012
|
case "list-tasks":
|
|
4056
5013
|
return { tasks: await listGjcTeamTasks(teamName, cwd, env) };
|
|
4057
5014
|
case "read-task":
|
|
@@ -4248,19 +5205,106 @@ export async function executeGjcTeamApiOperation(
|
|
|
4248
5205
|
return readGjcWorkerHeartbeat(teamName, worker, cwd, env);
|
|
4249
5206
|
case "recover-stale-claims":
|
|
4250
5207
|
return recoverGjcTeamStaleClaims(teamName, cwd, env);
|
|
4251
|
-
case "update-worker-heartbeat":
|
|
5208
|
+
case "update-worker-heartbeat": {
|
|
5209
|
+
const pid = Number(input.pid ?? 0);
|
|
4252
5210
|
return updateGjcWorkerHeartbeat(
|
|
4253
5211
|
teamName,
|
|
4254
5212
|
worker,
|
|
4255
5213
|
{
|
|
4256
|
-
pid
|
|
5214
|
+
pid,
|
|
4257
5215
|
last_turn_at: now(),
|
|
4258
5216
|
turn_count: Number(input.turn_count ?? 0),
|
|
4259
5217
|
alive: Boolean(input.alive ?? true),
|
|
5218
|
+
process_start_time: await readLinuxProcessStartTime(pid),
|
|
4260
5219
|
},
|
|
4261
5220
|
cwd,
|
|
4262
5221
|
env,
|
|
4263
5222
|
);
|
|
5223
|
+
}
|
|
5224
|
+
case "read-worker-memory-guard": {
|
|
5225
|
+
const dir = await findTeamDir(teamName, cwd, env);
|
|
5226
|
+
const config = await readConfig(dir);
|
|
5227
|
+
assertKnownWorker(config, worker);
|
|
5228
|
+
return readWorkerMemoryGuardLedger(dir, worker, normalizeWorkerMemoryGuardPlatform(input.platform));
|
|
5229
|
+
}
|
|
5230
|
+
case "update-worker-memory-guard": {
|
|
5231
|
+
const dir = await findTeamDir(teamName, cwd, env);
|
|
5232
|
+
return withGjcTeamMutationFence(dir, async () => {
|
|
5233
|
+
const config = await readConfig(dir);
|
|
5234
|
+
assertKnownWorker(config, worker);
|
|
5235
|
+
const platform = normalizeWorkerMemoryGuardPlatform(input.platform);
|
|
5236
|
+
const tasks = await readTasks(dir);
|
|
5237
|
+
const currentTaskIdInput = input.current_task_id ?? input.currentTaskId;
|
|
5238
|
+
if (typeof currentTaskIdInput === "string" && !currentTaskIdInput.trim())
|
|
5239
|
+
throw new Error("invalid_worker_memory_guard_task_id");
|
|
5240
|
+
const currentTaskId =
|
|
5241
|
+
typeof currentTaskIdInput === "string"
|
|
5242
|
+
? currentTaskIdInput.trim()
|
|
5243
|
+
: findGjcTeamClaimedTaskForWorker(tasks, worker)?.id;
|
|
5244
|
+
const existing = await readWorkerMemoryGuardLedger(dir, worker, platform);
|
|
5245
|
+
const retryLimitInput = Number(input.retry_limit ?? input.retryLimit ?? existing.retry_limit);
|
|
5246
|
+
if (!Number.isInteger(retryLimitInput) || retryLimitInput <= 0)
|
|
5247
|
+
throw new Error(`invalid_worker_memory_guard_retry_limit:${retryLimitInput}`);
|
|
5248
|
+
const pidProbe = normalizeGjcTeamWorkerMemoryGuardPidProbe(input.pid_probe ?? input.pidProbe);
|
|
5249
|
+
const stateInput = typeof input.state === "string" ? input.state.trim() : existing.state;
|
|
5250
|
+
if (!["idle", "advisory", "retrying", "checkpointed", "replaced", "blocked"].includes(stateInput))
|
|
5251
|
+
throw new Error(`invalid_worker_memory_guard_state:${stateInput}`);
|
|
5252
|
+
const updated: GjcTeamWorkerMemoryGuardLedger = {
|
|
5253
|
+
...existing,
|
|
5254
|
+
platform,
|
|
5255
|
+
state: stateInput as GjcTeamWorkerMemoryGuardLedger["state"],
|
|
5256
|
+
automatic_action_allowed:
|
|
5257
|
+
typeof input.automatic_action_allowed === "boolean"
|
|
5258
|
+
? input.automatic_action_allowed
|
|
5259
|
+
: typeof input.automaticActionAllowed === "boolean"
|
|
5260
|
+
? input.automaticActionAllowed
|
|
5261
|
+
: existing.automatic_action_allowed,
|
|
5262
|
+
retry_limit: retryLimitInput,
|
|
5263
|
+
current_task_id: currentTaskId,
|
|
5264
|
+
last_incident_id:
|
|
5265
|
+
typeof input.incident_id === "string"
|
|
5266
|
+
? input.incident_id
|
|
5267
|
+
: typeof input.incidentId === "string"
|
|
5268
|
+
? input.incidentId
|
|
5269
|
+
: existing.last_incident_id,
|
|
5270
|
+
last_reason:
|
|
5271
|
+
typeof input.reason === "string" && input.reason.trim() ? input.reason.trim() : existing.last_reason,
|
|
5272
|
+
last_pid_probe: pidProbe ?? existing.last_pid_probe,
|
|
5273
|
+
updated_at: now(),
|
|
5274
|
+
};
|
|
5275
|
+
return writeWorkerMemoryGuardLedger(dir, updated);
|
|
5276
|
+
});
|
|
5277
|
+
}
|
|
5278
|
+
case "apply-worker-memory-guard":
|
|
5279
|
+
return applyWorkerMemoryGuard({
|
|
5280
|
+
teamName,
|
|
5281
|
+
workerId: explicitWorker,
|
|
5282
|
+
requestedWorkerId: worker,
|
|
5283
|
+
reason: typeof input.reason === "string" ? input.reason : undefined,
|
|
5284
|
+
incidentId:
|
|
5285
|
+
typeof input.incident_id === "string"
|
|
5286
|
+
? input.incident_id
|
|
5287
|
+
: typeof input.incidentId === "string"
|
|
5288
|
+
? input.incidentId
|
|
5289
|
+
: undefined,
|
|
5290
|
+
platform: normalizeWorkerMemoryGuardPlatform(input.platform),
|
|
5291
|
+
pidProbe: normalizeGjcTeamWorkerMemoryGuardPidProbe(input.pid_probe ?? input.pidProbe),
|
|
5292
|
+
candidates: input.candidates,
|
|
5293
|
+
cwd,
|
|
5294
|
+
env,
|
|
5295
|
+
allowAutomaticAction:
|
|
5296
|
+
typeof input.automatic_action_allowed === "boolean"
|
|
5297
|
+
? input.automatic_action_allowed
|
|
5298
|
+
: typeof input.automaticActionAllowed === "boolean"
|
|
5299
|
+
? input.automaticActionAllowed
|
|
5300
|
+
: undefined,
|
|
5301
|
+
replacementToken:
|
|
5302
|
+
typeof input.replacement_token === "string"
|
|
5303
|
+
? input.replacement_token
|
|
5304
|
+
: typeof input.replacementToken === "string"
|
|
5305
|
+
? input.replacementToken
|
|
5306
|
+
: undefined,
|
|
5307
|
+
});
|
|
4264
5308
|
case "write-worker-inbox":
|
|
4265
5309
|
return writeGjcWorkerInbox(teamName, worker, String(input.content ?? ""), cwd, env);
|
|
4266
5310
|
case "write-worker-identity":
|
|
@@ -4310,7 +5354,8 @@ export async function executeGjcTeamApiOperation(
|
|
|
4310
5354
|
case "read-shutdown-ack":
|
|
4311
5355
|
return readGjcShutdownAck(teamName, worker, cwd, env);
|
|
4312
5356
|
default:
|
|
4313
|
-
|
|
5357
|
+
resolvedOperation satisfies never;
|
|
5358
|
+
throw new UnknownGjcTeamApiOperationError(operation, []);
|
|
4314
5359
|
}
|
|
4315
5360
|
}
|
|
4316
5361
|
|