@gajae-code/coding-agent 0.15.6 → 0.16.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 +99 -0
- package/dist/types/async/job-manager.d.ts +32 -1
- package/dist/types/browser-backend.d.ts +12 -0
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/root-flags.d.ts +14 -0
- package/dist/types/cli/update-cli.d.ts +14 -0
- package/dist/types/cli.d.ts +7 -0
- package/dist/types/commands/launch.d.ts +10 -0
- package/dist/types/commands/sdk.d.ts +2 -0
- package/dist/types/config/autorouting-tier-map.d.ts +24 -0
- package/dist/types/config/billing-path.d.ts +25 -0
- package/dist/types/config/config-schema-version.d.ts +13 -1
- package/dist/types/config/file-lock.d.ts +5 -0
- package/dist/types/config/model-registry.d.ts +22 -0
- package/dist/types/config/settings-schema.d.ts +27 -8
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/coordinator-mcp/projection-scan.d.ts +8 -0
- package/dist/types/crash/record-loader.d.ts +15 -0
- package/dist/types/extensibility/extensions/types.d.ts +8 -3
- package/dist/types/gjc-runtime/launch-worktree-reservation.d.ts +21 -0
- package/dist/types/gjc-runtime/launch-worktree.d.ts +25 -1
- package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +2 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +22 -0
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +66 -1
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +1 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +27 -0
- package/dist/types/main.d.ts +1 -0
- package/dist/types/master-mode/context.d.ts +21 -0
- package/dist/types/master-mode/first-request.d.ts +34 -0
- package/dist/types/master-mode/snapshot.d.ts +32 -0
- package/dist/types/modes/acp/acp-agent.d.ts +21 -21
- package/dist/types/modes/components/frictionless-onboarding-selector.d.ts +10 -0
- package/dist/types/modes/components/model-selector.d.ts +4 -0
- package/dist/types/modes/components/oauth-selector.d.ts +1 -0
- package/dist/types/modes/components/session-selector.d.ts +1 -1
- package/dist/types/modes/components/sessions-dashboard.d.ts +2 -1
- package/dist/types/modes/components/status-line/model-name.d.ts +18 -0
- package/dist/types/modes/components/status-line/priority-row.d.ts +34 -0
- package/dist/types/modes/components/status-line/segments.d.ts +10 -0
- package/dist/types/modes/interactive-mode.d.ts +16 -2
- package/dist/types/modes/print-mode.d.ts +17 -0
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/modes/ui-language.d.ts +7 -1
- package/dist/types/sdk/broker/broker.d.ts +79 -2
- package/dist/types/sdk/broker/endpoint-authority.d.ts +24 -0
- package/dist/types/sdk/broker/ensure.d.ts +3 -0
- package/dist/types/sdk/broker/index.d.ts +3 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +6 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +40 -0
- package/dist/types/sdk/broker/master-capability.d.ts +10 -0
- package/dist/types/sdk/broker/session-index.d.ts +40 -11
- package/dist/types/sdk/broker/session-scope.d.ts +72 -0
- package/dist/types/sdk/broker/spawn-authority.d.ts +201 -0
- package/dist/types/sdk/broker/spawn-substrate.d.ts +30 -0
- package/dist/types/sdk/broker/startup-budget.d.ts +52 -0
- package/dist/types/sdk/broker/state-version.d.ts +21 -1
- package/dist/types/sdk/broker/worktree-occupancy.d.ts +28 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -0
- package/dist/types/sdk/bus/discord-daemon.d.ts +1 -1
- package/dist/types/sdk/bus/index.d.ts +9 -1
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +1 -1
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +1 -1
- package/dist/types/sdk/bus/slack-conversation.d.ts +1 -1
- package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
- package/dist/types/sdk/cli/index.d.ts +1 -0
- package/dist/types/sdk/cli/master-cli.d.ts +63 -0
- package/dist/types/sdk/cli/rows.d.ts +12 -5
- package/dist/types/sdk/cli/session-cli.d.ts +20 -3
- package/dist/types/sdk/client/client.d.ts +3 -2
- package/dist/types/sdk/host/control/dispatch.d.ts +10 -0
- package/dist/types/sdk/host/control/index.d.ts +1 -1
- package/dist/types/sdk/host/control/operations.d.ts +2 -0
- package/dist/types/sdk/host/control/runtime-gate.d.ts +21 -7
- package/dist/types/sdk/host/events.d.ts +1 -0
- package/dist/types/sdk/host/host.d.ts +10 -0
- package/dist/types/sdk/host/query/cursor.d.ts +4 -2
- package/dist/types/sdk/host/query/handlers.d.ts +1 -0
- package/dist/types/sdk/host/query/revision-store.d.ts +1 -0
- package/dist/types/sdk/host/session-runtime.d.ts +70 -17
- package/dist/types/sdk/lifecycle/broker-client.d.ts +6 -0
- package/dist/types/sdk/lifecycle/service.d.ts +15 -3
- package/dist/types/sdk/lifecycle-session.d.ts +3 -1
- package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -5
- package/dist/types/sdk/receipt-state.d.ts +2 -0
- package/dist/types/sdk/router/session-router.d.ts +6 -0
- package/dist/types/sdk/session.d.ts +16 -0
- package/dist/types/sdk/startup-capability.d.ts +2 -1
- package/dist/types/sdk/turn-result.d.ts +6 -0
- package/dist/types/session/agent-session.d.ts +52 -7
- package/dist/types/session/credential-store-errors.d.ts +1 -0
- package/dist/types/session/fold-coordinator.d.ts +29 -2
- package/dist/types/session/session-manager.d.ts +79 -2
- package/dist/types/session/yield-queue.d.ts +5 -4
- package/dist/types/setup/frictionless-onboarding.d.ts +1 -0
- package/dist/types/tools/ask.d.ts +1 -0
- package/dist/types/tools/bash.d.ts +16 -0
- package/dist/types/tools/browser/launch.d.ts +10 -0
- package/dist/types/tools/browser/registry.d.ts +8 -0
- package/dist/types/tools/gh.d.ts +8 -0
- package/dist/types/tools/index.d.ts +10 -10
- package/dist/types/tools/job.d.ts +3 -1
- package/dist/types/utils/external-editor.d.ts +7 -0
- package/examples/extensions/README.md +1 -1
- package/examples/extensions/pirate.ts +7 -4
- package/package.json +7 -7
- package/scripts/generate-sdk-operation-inventory.ts +13 -1
- package/scripts/verify-gjc-sdk-canonicalization.ts +35 -1
- package/src/async/job-manager.ts +107 -9
- package/src/browser-backend.ts +29 -0
- package/src/cli/accounts-cli.ts +9 -5
- package/src/cli/args.ts +17 -0
- package/src/cli/fast-help.ts +1 -0
- package/src/cli/plugin-cli.ts +29 -0
- package/src/cli/root-flags.ts +5 -0
- package/src/cli/update-cli.ts +140 -23
- package/src/commands/launch.ts +86 -18
- package/src/commands/sdk.ts +135 -28
- package/src/config/autorouting-tier-map.ts +12 -0
- package/src/config/billing-path.ts +42 -0
- package/src/config/config-schema-version.ts +18 -1
- package/src/config/file-lock.ts +158 -83
- package/src/config/model-preset-registry.ts +178 -23
- package/src/config/model-profiles.ts +5 -5
- package/src/config/model-registry.ts +291 -42
- package/src/config/model-resolver.ts +5 -0
- package/src/config/settings-schema.ts +27 -8
- package/src/config/settings.ts +51 -15
- package/src/coordinator-mcp/projection-scan.ts +27 -6
- package/src/coordinator-mcp/question-state.ts +9 -11
- package/src/coordinator-mcp/server.ts +115 -21
- package/src/crash/index-store.ts +2 -0
- package/src/crash/record-loader.ts +10 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
- package/src/extensibility/extensions/runner.ts +27 -0
- package/src/extensibility/extensions/types.ts +8 -3
- package/src/extensibility/gjc-plugins/installer.ts +51 -11
- package/src/extensibility/runtime-skill-discovery.ts +110 -3
- package/src/extensibility/skills.ts +22 -12
- package/src/gjc-runtime/launch-worktree-reservation.ts +242 -0
- package/src/gjc-runtime/launch-worktree.ts +285 -4
- package/src/gjc-runtime/managed-owner-supervisor.ts +63 -51
- package/src/gjc-runtime/session-state-lock.ts +213 -14
- package/src/gjc-runtime/session-state-sidecar.ts +938 -39
- package/src/gjc-runtime/tmux-owner-isolation-cli.ts +4 -7
- package/src/gjc-runtime/tmux-owner-isolation.ts +18 -3
- package/src/gjc-runtime/tmux-sessions.ts +252 -13
- package/src/hooks/skill-state.ts +6 -0
- package/src/internal-urls/docs-index.generated.ts +15 -15
- package/src/internal-urls/issue-pr-protocol.ts +4 -2
- package/src/main.ts +61 -3
- package/src/master-mode/context.ts +50 -0
- package/src/master-mode/first-request.ts +65 -0
- package/src/master-mode/snapshot.ts +205 -0
- package/src/modes/acp/acp-agent.ts +610 -161
- package/src/modes/components/footer.ts +4 -2
- package/src/modes/components/frictionless-onboarding-selector.ts +48 -2
- package/src/modes/components/model-selector.ts +76 -8
- package/src/modes/components/oauth-selector.ts +6 -1
- package/src/modes/components/session-selector.ts +23 -15
- package/src/modes/components/sessions-dashboard.ts +8 -5
- package/src/modes/components/settings-selector.ts +1 -0
- package/src/modes/components/status-line/model-name.ts +53 -0
- package/src/modes/components/status-line/priority-row.ts +107 -0
- package/src/modes/components/status-line/segments.ts +23 -9
- package/src/modes/components/tool-status-header.ts +151 -15
- package/src/modes/controllers/command-controller.ts +1 -1
- package/src/modes/controllers/event-controller.ts +30 -4
- package/src/modes/controllers/extension-ui-controller.ts +13 -13
- package/src/modes/controllers/input-controller.ts +30 -2
- package/src/modes/controllers/selector-controller.ts +200 -98
- package/src/modes/interactive-mode.ts +36 -12
- package/src/modes/print-mode.ts +64 -0
- package/src/modes/prompt-action-autocomplete.ts +30 -4
- package/src/modes/runtime-init.ts +12 -9
- package/src/modes/theme/theme.ts +5 -0
- package/src/modes/types.ts +2 -0
- package/src/modes/ui-language.ts +49 -2
- package/src/modes/utils/ui-helpers.ts +22 -3
- package/src/prompts/agent-fragments/browser-aside.md +22 -0
- package/src/prompts/system/master-mode.md +28 -0
- package/src/prompts/tools/ask.md +1 -1
- package/src/prompts/tools/bash.md +1 -0
- package/src/prompts/tools/job.md +1 -1
- package/src/sdk/acp/adapter.ts +5 -0
- package/src/sdk/broker/broker.ts +1646 -27
- package/src/sdk/broker/endpoint-authority.ts +86 -0
- package/src/sdk/broker/ensure.ts +81 -3
- package/src/sdk/broker/index.ts +3 -0
- package/src/sdk/broker/lifecycle-ledger.ts +6 -0
- package/src/sdk/broker/lifecycle.ts +440 -75
- package/src/sdk/broker/master-capability.ts +232 -0
- package/src/sdk/broker/session-index.ts +121 -17
- package/src/sdk/broker/session-scope.ts +272 -0
- package/src/sdk/broker/spawn-authority.ts +1031 -0
- package/src/sdk/broker/spawn-substrate.ts +591 -0
- package/src/sdk/broker/startup-budget.ts +144 -2
- package/src/sdk/broker/state-version.ts +43 -14
- package/src/sdk/broker/transport.ts +1 -0
- package/src/sdk/broker/worktree-occupancy.ts +90 -0
- package/src/sdk/bus/chat-daemon-control.ts +5 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +136 -18
- package/src/sdk/bus/discord-daemon.ts +50 -10
- package/src/sdk/bus/index.ts +84 -7
- package/src/sdk/bus/kind-aware-reconciliation.ts +296 -48
- package/src/sdk/bus/lifecycle-commands.ts +4 -0
- package/src/sdk/bus/notification-service.ts +22 -5
- package/src/sdk/bus/prompt-reconciliation.ts +7 -2
- package/src/sdk/bus/reconciliation-store.ts +55 -31
- package/src/sdk/bus/slack-conversation.ts +1 -1
- package/src/sdk/bus/slack-daemon.ts +72 -38
- package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
- package/src/sdk/cli/index.ts +1 -0
- package/src/sdk/cli/master-cli.ts +259 -0
- package/src/sdk/cli/rows.ts +41 -15
- package/src/sdk/cli/session-cli.ts +297 -26
- package/src/sdk/client/client.ts +14 -8
- package/src/sdk/host/control/dispatch.ts +101 -40
- package/src/sdk/host/control/index.ts +2 -0
- package/src/sdk/host/control/operations.ts +2 -0
- package/src/sdk/host/control/runtime-gate.ts +203 -11
- package/src/sdk/host/events.ts +1 -0
- package/src/sdk/host/host.ts +29 -2
- package/src/sdk/host/query/cursor.ts +52 -7
- package/src/sdk/host/query/handlers.ts +33 -12
- package/src/sdk/host/query/revision-store.ts +12 -0
- package/src/sdk/host/session-runtime.test.ts +673 -14
- package/src/sdk/host/session-runtime.ts +674 -103
- package/src/sdk/host/surface-policy.ts +3 -3
- package/src/sdk/lifecycle/broker-client.ts +28 -0
- package/src/sdk/lifecycle/service.ts +279 -20
- package/src/sdk/lifecycle-session.ts +17 -4
- package/src/sdk/prompt-deadline-manager.ts +2 -1
- package/src/sdk/protocol/operation-inventory.generated.json +105 -3
- package/src/sdk/protocol/operation-registry.ts +14 -4
- package/src/sdk/receipt-state.ts +10 -0
- package/src/sdk/router/session-router.ts +219 -118
- package/src/sdk/session.ts +416 -195
- package/src/sdk/startup-capability.ts +1 -0
- package/src/sdk/turn-result.ts +22 -0
- package/src/session/agent-session.ts +1628 -318
- package/src/session/blob-store.ts +15 -5
- package/src/session/credential-store-errors.ts +2 -0
- package/src/session/fold-coordinator.ts +40 -3
- package/src/session/internal/managed-session-storage.ts +153 -78
- package/src/session/session-manager.ts +850 -97
- package/src/session/yield-queue.ts +24 -10
- package/src/setup/frictionless-onboarding.ts +2 -0
- package/src/slash-commands/builtin-registry.ts +41 -0
- package/src/task/executor.ts +9 -4
- package/src/telemetry/events.ts +1138 -12
- package/src/tools/ask-contract.test.ts +64 -0
- package/src/tools/ask-contract.ts +36 -1
- package/src/tools/ask.ts +10 -8
- package/src/tools/bash-interactive.ts +11 -2
- package/src/tools/bash.ts +412 -75
- package/src/tools/browser/launch.ts +37 -0
- package/src/tools/browser/registry.ts +99 -9
- package/src/tools/cron.ts +6 -4
- package/src/tools/descriptors.test.ts +15 -0
- package/src/tools/descriptors.ts +5 -1
- package/src/tools/gh.ts +104 -48
- package/src/tools/github-cache.ts +2 -2
- package/src/tools/index.ts +10 -6
- package/src/tools/job.ts +4 -1
- package/src/tools/monitor.ts +12 -9
- package/src/tools/resolve.ts +30 -10
- package/src/tools/tool-catalog.generated.ts +2 -2
- package/src/utils/external-editor.ts +72 -1
|
@@ -26,10 +26,13 @@ import {
|
|
|
26
26
|
import { mergeModelProfiles } from "../../config/model-profiles";
|
|
27
27
|
import { ModelsConfigFile } from "../../config/model-registry";
|
|
28
28
|
import {
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
DependencyPreparationTimeoutError,
|
|
30
|
+
ensureLaunchWorktreeCancellable,
|
|
31
|
+
ensureReusableNodeModulesCancellable,
|
|
31
32
|
type GjcLaunchWorktreePlan,
|
|
32
33
|
planLaunchWorktree,
|
|
34
|
+
removeOwnedLaunchWorktree,
|
|
35
|
+
WorktreePreparationTimeoutError,
|
|
33
36
|
} from "../../gjc-runtime/launch-worktree";
|
|
34
37
|
import { probeLinuxProcPidSync } from "../../gjc-runtime/linux-proc";
|
|
35
38
|
import {
|
|
@@ -66,6 +69,7 @@ import {
|
|
|
66
69
|
sanitizeSdkStartupMessage,
|
|
67
70
|
} from "../startup-capability";
|
|
68
71
|
import type { Broker, BrokerCleanupEvidence, BrokerCleanupIdentity, BrokerResponse } from "./broker";
|
|
72
|
+
import { matchesIndexedEndpointFile, readEndpointFile } from "./endpoint-authority";
|
|
69
73
|
import { decodeLifecycleUtf8, parseLifecycleJson } from "./lifecycle-codec";
|
|
70
74
|
import type {
|
|
71
75
|
LifecycleCleanupProof,
|
|
@@ -81,14 +85,23 @@ import {
|
|
|
81
85
|
processIncarnation,
|
|
82
86
|
} from "./process-incarnation";
|
|
83
87
|
import { resolveSdkInternalSpawnCommand, type SdkInternalSpawnCommand } from "./runtime";
|
|
84
|
-
import {
|
|
88
|
+
import {
|
|
89
|
+
type IndexedSession,
|
|
90
|
+
isSessionAuthorityEligible,
|
|
91
|
+
resolveSessionLocator,
|
|
92
|
+
type SessionLocatorV2,
|
|
93
|
+
} from "./session-index";
|
|
85
94
|
import {
|
|
86
95
|
cancellableSleep,
|
|
87
96
|
DEFAULT_READINESS_TIMEOUT_MS,
|
|
97
|
+
deriveLifecycleOuterDeadlines,
|
|
88
98
|
isValidReadinessTimeoutMs,
|
|
99
|
+
PREPARATION_TIMEOUT_INVALID_MESSAGE,
|
|
89
100
|
READINESS_TIMEOUT_INVALID_MESSAGE,
|
|
101
|
+
readPreparationTimeouts,
|
|
90
102
|
startupQueueWaitMs,
|
|
91
103
|
} from "./startup-budget";
|
|
104
|
+
import { worktreeOccupant } from "./worktree-occupancy";
|
|
92
105
|
|
|
93
106
|
export {
|
|
94
107
|
type ProcessIncarnationCommandRunner,
|
|
@@ -194,6 +207,17 @@ const lifecycleCommandResolversForTest = new WeakMap<Broker, LifecycleCommandRes
|
|
|
194
207
|
const lifecycleCleanupHooksForTest = new WeakMap<Broker, () => void>();
|
|
195
208
|
const startupAdmittedInputs = new WeakSet<Input>();
|
|
196
209
|
const startupLaunchInputs = new WeakMap<Input, SessionLaunch>();
|
|
210
|
+
type EnsureLaunchWorktreeForTest = (
|
|
211
|
+
plan: GjcLaunchWorktreePlan,
|
|
212
|
+
opts: { signal: AbortSignal; deadlineAt: number },
|
|
213
|
+
) => Promise<SessionLifecycleWorktreeReceipt & { createdBranch: boolean }>;
|
|
214
|
+
type EnsureReusableNodeModulesForTest = (
|
|
215
|
+
sourceRoot: string,
|
|
216
|
+
worktreePath: string,
|
|
217
|
+
opts: { signal: AbortSignal; deadlineAt: number },
|
|
218
|
+
) => Promise<"symlink" | "present" | "missing">;
|
|
219
|
+
let ensureLaunchWorktreeForTest: EnsureLaunchWorktreeForTest | undefined;
|
|
220
|
+
let ensureReusableNodeModulesForTest: EnsureReusableNodeModulesForTest | undefined;
|
|
197
221
|
|
|
198
222
|
/** Test-only hook for simulating a crash immediately after one exact lifecycle detach. */
|
|
199
223
|
export function setLifecycleCleanupHookForTest(broker: Broker, hook: (() => void) | undefined): void {
|
|
@@ -213,6 +237,20 @@ export function setLifecycleTimingForTest(broker: Broker, timing: LifecycleTimin
|
|
|
213
237
|
if (timing) lifecycleTimingsForTest.set(broker, timing);
|
|
214
238
|
else lifecycleTimingsForTest.delete(broker);
|
|
215
239
|
}
|
|
240
|
+
export function setEnsureLaunchWorktreeForTest(
|
|
241
|
+
_broker: Broker | undefined,
|
|
242
|
+
fn?: EnsureLaunchWorktreeForTest | undefined,
|
|
243
|
+
): void {
|
|
244
|
+
ensureLaunchWorktreeForTest = typeof _broker === "function" ? (_broker as EnsureLaunchWorktreeForTest) : fn;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function setEnsureReusableNodeModulesForTest(
|
|
248
|
+
_broker: Broker | undefined,
|
|
249
|
+
fn?: EnsureReusableNodeModulesForTest | undefined,
|
|
250
|
+
): void {
|
|
251
|
+
ensureReusableNodeModulesForTest =
|
|
252
|
+
typeof _broker === "function" ? (_broker as EnsureReusableNodeModulesForTest) : fn;
|
|
253
|
+
}
|
|
216
254
|
|
|
217
255
|
let lifecycleHostPlatformForTest: NodeJS.Platform | undefined;
|
|
218
256
|
|
|
@@ -293,6 +331,7 @@ export interface SessionLifecycleWorktreeReceipt {
|
|
|
293
331
|
cwd: string;
|
|
294
332
|
created: boolean;
|
|
295
333
|
reused: boolean;
|
|
334
|
+
createdBranch: boolean;
|
|
296
335
|
branch?: string;
|
|
297
336
|
}
|
|
298
337
|
|
|
@@ -612,7 +651,7 @@ function text(value: unknown): string | undefined {
|
|
|
612
651
|
return typeof value === "string" && value ? value : undefined;
|
|
613
652
|
}
|
|
614
653
|
|
|
615
|
-
function validateBrokerModelPresetSync(agentDir: string, requestedProfile: string): string | BrokerResponse {
|
|
654
|
+
export function validateBrokerModelPresetSync(agentDir: string, requestedProfile: string): string | BrokerResponse {
|
|
616
655
|
const modelsConfigFile = ModelsConfigFile.relocate(path.join(agentDir, "models.yml"));
|
|
617
656
|
modelsConfigFile.invalidate();
|
|
618
657
|
const loaded = modelsConfigFile.tryLoad();
|
|
@@ -727,16 +766,19 @@ function isLifecycleWorktreeTarget(value: unknown): value is SessionLifecycleWor
|
|
|
727
766
|
|
|
728
767
|
function lifecycleWorktreeTarget(input: Input): SessionLifecycleWorktreeTarget | null | undefined {
|
|
729
768
|
const target = input.target as Record<string, unknown> | undefined;
|
|
730
|
-
const worktree = target?.worktree;
|
|
769
|
+
const worktree = target?.worktree ?? input.worktree;
|
|
731
770
|
if (worktree === undefined) return undefined;
|
|
732
771
|
return isLifecycleWorktreeTarget(worktree) ? worktree : null;
|
|
733
772
|
}
|
|
734
773
|
|
|
735
774
|
type LiveResumeRecord = {
|
|
736
|
-
|
|
775
|
+
sessionId: string;
|
|
776
|
+
locator: SessionLocatorV2;
|
|
737
777
|
endpointGeneration: number;
|
|
738
778
|
pid: number;
|
|
739
779
|
endpointMtimeMs?: number;
|
|
780
|
+
processIncarnation?: string;
|
|
781
|
+
hostIncarnation?: string;
|
|
740
782
|
live: boolean;
|
|
741
783
|
ambiguous: boolean;
|
|
742
784
|
};
|
|
@@ -755,7 +797,7 @@ type ResumeScope = {
|
|
|
755
797
|
};
|
|
756
798
|
function sameResumeLocator(record: LiveResumeRecord, cwd: string, root: string): boolean {
|
|
757
799
|
return (
|
|
758
|
-
resolveEquivalentPath(record.locator.
|
|
800
|
+
resolveEquivalentPath(record.locator.cwd) === resolveEquivalentPath(cwd) &&
|
|
759
801
|
resolveEquivalentPath(record.locator.stateRoot) === resolveEquivalentPath(root)
|
|
760
802
|
);
|
|
761
803
|
}
|
|
@@ -777,10 +819,24 @@ function sameLiveResumeRecord(expected: LiveResumeRecord, current: LiveResumeRec
|
|
|
777
819
|
current.endpointGeneration === expected.endpointGeneration &&
|
|
778
820
|
current.pid === expected.pid &&
|
|
779
821
|
current.endpointMtimeMs === expected.endpointMtimeMs &&
|
|
780
|
-
|
|
822
|
+
(current.hostIncarnation ?? current.processIncarnation) ===
|
|
823
|
+
(expected.hostIncarnation ?? expected.processIncarnation) &&
|
|
824
|
+
sameResumeLocator(current, expected.locator.cwd, expected.locator.stateRoot)
|
|
781
825
|
);
|
|
782
826
|
}
|
|
783
827
|
|
|
828
|
+
function liveResumeAuthority(
|
|
829
|
+
sessions: readonly LiveResumeRecord[],
|
|
830
|
+
sessionId: string,
|
|
831
|
+
): { kind: "none" } | { kind: "ambiguous" } | { kind: "live"; record: LiveResumeRecord } {
|
|
832
|
+
const indexedCandidates = sessions.filter(session => session.sessionId === sessionId);
|
|
833
|
+
if (indexedCandidates.some(session => !isSessionAuthorityEligible(session))) return { kind: "ambiguous" };
|
|
834
|
+
const liveCandidates = indexedCandidates.filter(session => session.live);
|
|
835
|
+
if (liveCandidates.length > 1) return { kind: "ambiguous" };
|
|
836
|
+
const record = liveCandidates[0];
|
|
837
|
+
return record ? { kind: "live", record } : { kind: "none" };
|
|
838
|
+
}
|
|
839
|
+
|
|
784
840
|
type ValidatedTranscript = {
|
|
785
841
|
path: string;
|
|
786
842
|
id: string;
|
|
@@ -912,18 +968,36 @@ async function validateLiveResumeScope(
|
|
|
912
968
|
sessionIdentity: session.identity,
|
|
913
969
|
};
|
|
914
970
|
}
|
|
915
|
-
async function reconcileReadyScope(
|
|
971
|
+
async function reconcileReadyScope(
|
|
972
|
+
broker: Broker,
|
|
973
|
+
id: string,
|
|
974
|
+
scope: string | undefined,
|
|
975
|
+
root: string,
|
|
976
|
+
expected: EffectMarker,
|
|
977
|
+
): Promise<void> {
|
|
916
978
|
if (!scope) return;
|
|
917
979
|
await broker.index.refresh();
|
|
918
|
-
const record = broker.index
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
980
|
+
const record = broker.index
|
|
981
|
+
.listSessions()
|
|
982
|
+
.sessions.find(
|
|
983
|
+
session =>
|
|
984
|
+
session.sessionId === id &&
|
|
985
|
+
resolveEquivalentPath(session.locator.stateRoot) === resolveEquivalentPath(root) &&
|
|
986
|
+
session.pid === expected.pid &&
|
|
987
|
+
(session.hostIncarnation ?? session.processIncarnation) === expected.incarnation &&
|
|
988
|
+
session.lifecycleRequestId === expected.effectMarker,
|
|
989
|
+
);
|
|
990
|
+
if (!record) return;
|
|
991
|
+
const cwd = canonicalExistingPath(scope);
|
|
992
|
+
if (record.locator.cwd === cwd) return;
|
|
993
|
+
// Locator cwd is canonical everywhere. Reconcile only a pre-existing host row
|
|
994
|
+
// whose canonical registration race observed a different path; it never retains
|
|
995
|
+
// a caller's lexical spelling for ACP or any other scope consumer.
|
|
996
|
+
const locator = await resolveSessionLocator(scope, record.locator.stateRoot);
|
|
923
997
|
await broker.index.append({
|
|
924
998
|
type: "record_reconciled",
|
|
925
999
|
sessionId: id,
|
|
926
|
-
locator
|
|
1000
|
+
locator,
|
|
927
1001
|
endpointGeneration: record.endpointGeneration,
|
|
928
1002
|
pid: record.pid,
|
|
929
1003
|
// Reconciliation only re-scopes the locator, so every identity fact the host
|
|
@@ -933,6 +1007,8 @@ async function reconcileReadyScope(broker: Broker, id: string, scope: string | u
|
|
|
933
1007
|
...(record.hostIncarnation === undefined ? {} : { hostIncarnation: record.hostIncarnation }),
|
|
934
1008
|
...(record.lifecycleRequestId === undefined ? {} : { lifecycleRequestId: record.lifecycleRequestId }),
|
|
935
1009
|
endpointMtimeMs: record.endpointMtimeMs,
|
|
1010
|
+
...(record.endpointFileId === undefined ? {} : { endpointFileId: record.endpointFileId }),
|
|
1011
|
+
...(record.masterRole === undefined ? {} : { masterRole: record.masterRole }),
|
|
936
1012
|
});
|
|
937
1013
|
}
|
|
938
1014
|
|
|
@@ -964,6 +1040,66 @@ function command(broker: Broker): LifecycleCommand {
|
|
|
964
1040
|
return lifecycleCommandResolversForTest.get(broker)?.() ?? resolveSdkInternalSpawnCommand("session-host-internal");
|
|
965
1041
|
}
|
|
966
1042
|
|
|
1043
|
+
/**
|
|
1044
|
+
* Reuses the ordinary lifecycle host bootstrap without performing its direct
|
|
1045
|
+
* process spawn. Broker-owned substrate providers receive this exact argv and
|
|
1046
|
+
* split inherited/child-specific environment contract as their only launch
|
|
1047
|
+
* authority for session.spawn.
|
|
1048
|
+
*/
|
|
1049
|
+
export type SpawnChildHostLaunch = {
|
|
1050
|
+
childId: string;
|
|
1051
|
+
cwd: string;
|
|
1052
|
+
stateRoot: string;
|
|
1053
|
+
argv: readonly string[];
|
|
1054
|
+
inheritedEnv: Readonly<Record<string, string>>;
|
|
1055
|
+
env: Readonly<Record<string, string>>;
|
|
1056
|
+
effectMarker: string;
|
|
1057
|
+
};
|
|
1058
|
+
|
|
1059
|
+
export function prepareSpawnChildHostLaunch(
|
|
1060
|
+
broker: Broker,
|
|
1061
|
+
input: { cwd: string; modelId?: string; modelPreset?: string; childId?: string; receivedAt?: number },
|
|
1062
|
+
): SpawnChildHostLaunch {
|
|
1063
|
+
const cwd = path.resolve(input.cwd);
|
|
1064
|
+
const childId = input.childId ?? randomUUID();
|
|
1065
|
+
const stateRoot = defaultStateRoot(cwd);
|
|
1066
|
+
const effectMarker = randomUUID();
|
|
1067
|
+
const deadlines = deriveLifecycleDeadlines(input.receivedAt ?? Date.now(), DEFAULT_READINESS_TIMEOUT_MS);
|
|
1068
|
+
const request: SessionLifecycleLaunchRequest = {
|
|
1069
|
+
operation: "session.create",
|
|
1070
|
+
sessionId: childId,
|
|
1071
|
+
cwd,
|
|
1072
|
+
stateRoot,
|
|
1073
|
+
effectMarker,
|
|
1074
|
+
...deadlines,
|
|
1075
|
+
...(input.modelId === undefined ? {} : { modelId: input.modelId }),
|
|
1076
|
+
...(input.modelPreset === undefined ? {} : { modelPreset: input.modelPreset }),
|
|
1077
|
+
};
|
|
1078
|
+
const cmd = command(broker);
|
|
1079
|
+
const inherited = "kind" in cmd ? cmd.env : process.env;
|
|
1080
|
+
const inheritedEnv = Object.fromEntries(
|
|
1081
|
+
Object.entries(inherited).filter(
|
|
1082
|
+
(entry): entry is [string, string] => entry[0] !== "GJC_MASTER_CAPABILITY" && typeof entry[1] === "string",
|
|
1083
|
+
),
|
|
1084
|
+
);
|
|
1085
|
+
return {
|
|
1086
|
+
childId,
|
|
1087
|
+
cwd,
|
|
1088
|
+
stateRoot,
|
|
1089
|
+
argv: [cmd.file, ...cmd.args],
|
|
1090
|
+
inheritedEnv,
|
|
1091
|
+
env: {
|
|
1092
|
+
GJC_AGENT_DIR: broker.settings.agentDir,
|
|
1093
|
+
GJC_CODING_AGENT_DIR: broker.settings.agentDir,
|
|
1094
|
+
GJC_SESSION_ID: childId,
|
|
1095
|
+
GJC_STATE_ROOT: stateRoot,
|
|
1096
|
+
GJC_LIFECYCLE_REQUEST_ID: effectMarker,
|
|
1097
|
+
GJC_SDK_LIFECYCLE_REQUEST: JSON.stringify(request),
|
|
1098
|
+
},
|
|
1099
|
+
effectMarker,
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
|
|
967
1103
|
const lifecycleMarkerPath = (root: string, id: string) => path.join(root, "sdk", `${id}.lifecycle.json`);
|
|
968
1104
|
const lifecycleReadyPath = (root: string, id: string) => path.join(root, "sdk", `${id}.lifecycle.ready.json`);
|
|
969
1105
|
const lifecycleFailurePath = (root: string, id: string, effectMarker: string) =>
|
|
@@ -973,6 +1109,7 @@ type ReadyAuthority = {
|
|
|
973
1109
|
endpoint: Record<string, unknown>;
|
|
974
1110
|
endpointSource: string;
|
|
975
1111
|
endpointMtimeMs: number;
|
|
1112
|
+
endpointFileId?: string;
|
|
976
1113
|
endpointGeneration: number;
|
|
977
1114
|
};
|
|
978
1115
|
type ReadinessResult =
|
|
@@ -3351,7 +3488,7 @@ async function recordTerminalUncertain(
|
|
|
3351
3488
|
await broker.index.append({
|
|
3352
3489
|
type: "lifecycle_terminal",
|
|
3353
3490
|
sessionId: id,
|
|
3354
|
-
locator: {
|
|
3491
|
+
locator: { cwd: "unknown", worktreeRoot: null, stateRoot: root },
|
|
3355
3492
|
endpointGeneration: 0,
|
|
3356
3493
|
pid,
|
|
3357
3494
|
terminalUncertain: true,
|
|
@@ -3667,7 +3804,10 @@ async function removeExactDeadSessionEndpoint(
|
|
|
3667
3804
|
: finalEndpointPath;
|
|
3668
3805
|
let handle: fs.FileHandle | undefined;
|
|
3669
3806
|
try {
|
|
3670
|
-
handle = await fs.open(
|
|
3807
|
+
handle = await fs.open(
|
|
3808
|
+
endpointSource,
|
|
3809
|
+
fsSync.constants.O_RDONLY | (fsSync.constants.O_NOFOLLOW ?? 0) | (fsSync.constants.O_NONBLOCK ?? 0),
|
|
3810
|
+
);
|
|
3671
3811
|
const metadata = await handle.stat({ bigint: true });
|
|
3672
3812
|
if (!metadata.isFile() || metadata.nlink !== 1n || metadata.size > 4096n) return false;
|
|
3673
3813
|
const bytes = Buffer.alloc(Number(metadata.size) + 1);
|
|
@@ -3847,10 +3987,9 @@ async function currentReadyAuthority(
|
|
|
3847
3987
|
if (!(await hasOwnedReadinessEvidence(broker, root, id, expected))) return undefined;
|
|
3848
3988
|
const endpointPath = path.join(root, "sdk", `${id}.json`);
|
|
3849
3989
|
try {
|
|
3850
|
-
const
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
]);
|
|
3990
|
+
const endpointFile = await readEndpointFile(endpointPath);
|
|
3991
|
+
if (!endpointFile) return undefined;
|
|
3992
|
+
const endpointSource = endpointFile.source;
|
|
3854
3993
|
const endpoint = JSON.parse(endpointSource) as {
|
|
3855
3994
|
sessionId?: unknown;
|
|
3856
3995
|
url?: unknown;
|
|
@@ -3863,14 +4002,24 @@ async function currentReadyAuthority(
|
|
|
3863
4002
|
// native-dead child; do not refuse a native-alive ready host for a stale live bit.
|
|
3864
4003
|
await broker.heartbeatSessions();
|
|
3865
4004
|
await broker.index.refresh();
|
|
3866
|
-
const record = broker.index
|
|
4005
|
+
const record = broker.index
|
|
4006
|
+
.listSessions()
|
|
4007
|
+
.sessions.find(
|
|
4008
|
+
session =>
|
|
4009
|
+
session.sessionId === id &&
|
|
4010
|
+
session.pid === expected.pid &&
|
|
4011
|
+
resolveEquivalentPath(session.locator.stateRoot) === resolveEquivalentPath(root) &&
|
|
4012
|
+
(session.hostIncarnation ?? session.processIncarnation) === expected.incarnation &&
|
|
4013
|
+
session.lifecycleRequestId === expected.effectMarker,
|
|
4014
|
+
);
|
|
3867
4015
|
if (
|
|
3868
4016
|
!record ||
|
|
3869
4017
|
record.terminal ||
|
|
3870
4018
|
record.terminalUncertain ||
|
|
3871
4019
|
record.pid !== expected.pid ||
|
|
3872
4020
|
resolveEquivalentPath(record.locator.stateRoot) !== resolveEquivalentPath(root) ||
|
|
3873
|
-
record.
|
|
4021
|
+
(record.hostIncarnation ?? record.processIncarnation) !== expected.incarnation ||
|
|
4022
|
+
!matchesIndexedEndpointFile(endpointFile, record) ||
|
|
3874
4023
|
endpoint.pid !== expected.pid ||
|
|
3875
4024
|
endpoint.sessionId !== id ||
|
|
3876
4025
|
typeof endpoint.url !== "string" ||
|
|
@@ -3885,7 +4034,8 @@ async function currentReadyAuthority(
|
|
|
3885
4034
|
return {
|
|
3886
4035
|
endpoint: endpoint as Record<string, unknown>,
|
|
3887
4036
|
endpointSource,
|
|
3888
|
-
endpointMtimeMs:
|
|
4037
|
+
endpointMtimeMs: endpointFile.mtimeMs,
|
|
4038
|
+
...(record.endpointFileId === undefined ? {} : { endpointFileId: record.endpointFileId }),
|
|
3889
4039
|
endpointGeneration: record.endpointGeneration,
|
|
3890
4040
|
};
|
|
3891
4041
|
} catch {
|
|
@@ -3896,7 +4046,8 @@ async function currentReadyAuthority(
|
|
|
3896
4046
|
function sameReadyAuthority(left: ReadyAuthority, right: ReadyAuthority): boolean {
|
|
3897
4047
|
return (
|
|
3898
4048
|
left.endpointSource === right.endpointSource &&
|
|
3899
|
-
left.endpointMtimeMs
|
|
4049
|
+
Math.abs(left.endpointMtimeMs - right.endpointMtimeMs) <= 0.001 &&
|
|
4050
|
+
left.endpointFileId === right.endpointFileId &&
|
|
3900
4051
|
left.endpointGeneration === right.endpointGeneration
|
|
3901
4052
|
);
|
|
3902
4053
|
}
|
|
@@ -4022,8 +4173,31 @@ function worktreeIntent(plan: GjcLaunchWorktreePlan | undefined): LifecycleWorkt
|
|
|
4022
4173
|
};
|
|
4023
4174
|
}
|
|
4024
4175
|
|
|
4025
|
-
|
|
4026
|
-
|
|
4176
|
+
/** Test seam for the worktree occupancy boundary. */
|
|
4177
|
+
export function worktreeOccupantForTest(
|
|
4178
|
+
sessions: IndexedSession[],
|
|
4179
|
+
worktreePath: string,
|
|
4180
|
+
observe: (pid: number, expectedIncarnation: string | undefined) => ProcessObservation = observeProcess,
|
|
4181
|
+
): string | null {
|
|
4182
|
+
return worktreeOccupant(sessions, worktreePath, observe);
|
|
4183
|
+
}
|
|
4184
|
+
|
|
4185
|
+
async function preparePlannedWorktree(
|
|
4186
|
+
plan: GjcLaunchWorktreePlan,
|
|
4187
|
+
opts: { signal: AbortSignal; deadlineAt: number; now: () => number },
|
|
4188
|
+
): Promise<SessionLifecycleWorktreeReceipt> {
|
|
4189
|
+
const hooked = ensureLaunchWorktreeForTest;
|
|
4190
|
+
if (hooked) {
|
|
4191
|
+
const receipt = await hooked(plan, { signal: opts.signal, deadlineAt: opts.deadlineAt });
|
|
4192
|
+
if (!receipt.enabled || path.resolve(receipt.cwd) !== path.resolve(plan.worktreePath))
|
|
4193
|
+
throw new Error("Lifecycle worktree preparation did not preserve the durable worktree identity.");
|
|
4194
|
+
return receipt;
|
|
4195
|
+
}
|
|
4196
|
+
const prepared = await ensureLaunchWorktreeCancellable(plan, {
|
|
4197
|
+
signal: opts.signal,
|
|
4198
|
+
deadlineAt: opts.deadlineAt,
|
|
4199
|
+
now: opts.now,
|
|
4200
|
+
});
|
|
4027
4201
|
if (!prepared.enabled || path.resolve(prepared.worktreePath) !== path.resolve(plan.worktreePath))
|
|
4028
4202
|
throw new Error("Lifecycle worktree preparation did not preserve the durable worktree identity.");
|
|
4029
4203
|
return {
|
|
@@ -4031,9 +4205,67 @@ function preparePlannedWorktree(plan: GjcLaunchWorktreePlan): SessionLifecycleWo
|
|
|
4031
4205
|
cwd: path.resolve(prepared.worktreePath),
|
|
4032
4206
|
created: prepared.created,
|
|
4033
4207
|
reused: prepared.reused,
|
|
4208
|
+
createdBranch: prepared.createdBranch,
|
|
4034
4209
|
...(prepared.branchName ? { branch: prepared.branchName } : {}),
|
|
4035
4210
|
};
|
|
4036
4211
|
}
|
|
4212
|
+
|
|
4213
|
+
function mapPreparationFailure(error: unknown): BrokerResponse {
|
|
4214
|
+
const message = sanitizeSdkStartupMessage(error);
|
|
4215
|
+
if (
|
|
4216
|
+
error instanceof WorktreePreparationTimeoutError ||
|
|
4217
|
+
(error as { code?: string }).code === "worktree_preparation_timeout"
|
|
4218
|
+
) {
|
|
4219
|
+
return fail("worktree_preparation_timeout", message);
|
|
4220
|
+
}
|
|
4221
|
+
if (
|
|
4222
|
+
error instanceof DependencyPreparationTimeoutError ||
|
|
4223
|
+
(error as { code?: string }).code === "dependency_preparation_timeout"
|
|
4224
|
+
) {
|
|
4225
|
+
return fail("dependency_preparation_timeout", message);
|
|
4226
|
+
}
|
|
4227
|
+
return fail("spawn_failed", `Unable to prepare lifecycle worktree: ${message}`);
|
|
4228
|
+
}
|
|
4229
|
+
|
|
4230
|
+
function abortWhenDue(
|
|
4231
|
+
controller: AbortController,
|
|
4232
|
+
deadlineAt: number,
|
|
4233
|
+
now: () => number,
|
|
4234
|
+
): ReturnType<typeof setInterval> {
|
|
4235
|
+
const tick = (): void => {
|
|
4236
|
+
if (now() >= deadlineAt && !controller.signal.aborted) controller.abort();
|
|
4237
|
+
};
|
|
4238
|
+
tick();
|
|
4239
|
+
return setInterval(tick, 10);
|
|
4240
|
+
}
|
|
4241
|
+
|
|
4242
|
+
function durableWorktreeEffects(
|
|
4243
|
+
receipt: SessionLifecycleWorktreeReceipt,
|
|
4244
|
+
timings: { worktreePreparationMs?: number; dependencyPreparationMs?: number; spawnAuthorizedAtOffsetMs?: number },
|
|
4245
|
+
): LifecycleDurableEffectsReceipt {
|
|
4246
|
+
const worktree = {
|
|
4247
|
+
cwdDigest: createHash("sha256").update(receipt.cwd, "utf8").digest("hex"),
|
|
4248
|
+
created: receipt.created,
|
|
4249
|
+
reused: receipt.reused,
|
|
4250
|
+
createdBranch: receipt.createdBranch,
|
|
4251
|
+
...(receipt.branch ? { branchDigest: createHash("sha256").update(receipt.branch, "utf8").digest("hex") } : {}),
|
|
4252
|
+
};
|
|
4253
|
+
return {
|
|
4254
|
+
worktree,
|
|
4255
|
+
timings,
|
|
4256
|
+
digest: createHash("sha256").update(canonicalJson({ worktree, timings })).digest("hex"),
|
|
4257
|
+
};
|
|
4258
|
+
}
|
|
4259
|
+
|
|
4260
|
+
function deadlineFieldsPresent(input: Input): boolean {
|
|
4261
|
+
return [
|
|
4262
|
+
input.receivedAt,
|
|
4263
|
+
input.requestedReadinessTimeoutMs,
|
|
4264
|
+
input.semanticReadyDeadlineAt,
|
|
4265
|
+
input.terminationStartDeadlineAt,
|
|
4266
|
+
input.lifecycleCleanupDeadlineAt,
|
|
4267
|
+
].some(value => value !== undefined);
|
|
4268
|
+
}
|
|
4037
4269
|
async function launchInput(
|
|
4038
4270
|
broker: Broker,
|
|
4039
4271
|
operation: "session.create" | "session.fork" | "session.resume",
|
|
@@ -4453,7 +4685,7 @@ async function validateDeletePath(
|
|
|
4453
4685
|
broker: Broker,
|
|
4454
4686
|
input: Input,
|
|
4455
4687
|
id: string,
|
|
4456
|
-
record: { locator:
|
|
4688
|
+
record: { locator: SessionLocatorV2 } | undefined,
|
|
4457
4689
|
cleanup?: CleanupEvidence,
|
|
4458
4690
|
): Promise<ValidatedDelete | BrokerResponse> {
|
|
4459
4691
|
const sessionPath = text(input.sessionPath);
|
|
@@ -4467,7 +4699,7 @@ async function validateDeletePath(
|
|
|
4467
4699
|
const canonicalRequestedRoot = canonicalExistingPath(requestedRoot);
|
|
4468
4700
|
if (
|
|
4469
4701
|
record &&
|
|
4470
|
-
(canonicalExistingPath(record.locator.
|
|
4702
|
+
(canonicalExistingPath(record.locator.cwd) !== cwd ||
|
|
4471
4703
|
canonicalExistingPath(record.locator.stateRoot) !== canonicalRequestedRoot)
|
|
4472
4704
|
)
|
|
4473
4705
|
return fail("invalid_input", "session.delete locator does not match the indexed session.");
|
|
@@ -4557,7 +4789,7 @@ async function validateDeletePath(
|
|
|
4557
4789
|
}
|
|
4558
4790
|
type CloseAuthority = { endpointGeneration: number; endpointIncarnation: string };
|
|
4559
4791
|
type CloseRecord = {
|
|
4560
|
-
locator:
|
|
4792
|
+
locator: SessionLocatorV2;
|
|
4561
4793
|
endpointGeneration: number;
|
|
4562
4794
|
pid: number;
|
|
4563
4795
|
endpointMtimeMs?: number;
|
|
@@ -4621,7 +4853,7 @@ function sameCloseStoredProcessIdentity(expected: CloseRecord, current: CloseRec
|
|
|
4621
4853
|
typeof expected.lifecycleRequestId === "string" &&
|
|
4622
4854
|
expected.lifecycleRequestId.length > 0 &&
|
|
4623
4855
|
current.lifecycleRequestId === expected.lifecycleRequestId &&
|
|
4624
|
-
path.resolve(current.locator.
|
|
4856
|
+
path.resolve(current.locator.cwd) === path.resolve(expected.locator.cwd) &&
|
|
4625
4857
|
path.resolve(current.locator.stateRoot) === path.resolve(expected.locator.stateRoot)
|
|
4626
4858
|
);
|
|
4627
4859
|
}
|
|
@@ -4639,7 +4871,7 @@ function sameCloseEndpointIdentity(expected: CloseRecord, current: CloseRecord):
|
|
|
4639
4871
|
current.endpointMtimeMs !== undefined &&
|
|
4640
4872
|
expected.endpointMtimeMs !== undefined &&
|
|
4641
4873
|
current.lifecycleRequestId === expected.lifecycleRequestId &&
|
|
4642
|
-
path.resolve(current.locator.
|
|
4874
|
+
path.resolve(current.locator.cwd) === path.resolve(expected.locator.cwd) &&
|
|
4643
4875
|
path.resolve(current.locator.stateRoot) === path.resolve(expected.locator.stateRoot)
|
|
4644
4876
|
);
|
|
4645
4877
|
}
|
|
@@ -4656,7 +4888,7 @@ function sameCloseGeneration(expected: CloseRecord, current: CloseRecord & { liv
|
|
|
4656
4888
|
current.endpointMtimeMs === expected.endpointMtimeMs &&
|
|
4657
4889
|
current.lifecycleRequestId === expected.lifecycleRequestId &&
|
|
4658
4890
|
current.processIncarnation === expected.processIncarnation &&
|
|
4659
|
-
path.resolve(current.locator.
|
|
4891
|
+
path.resolve(current.locator.cwd) === path.resolve(expected.locator.cwd) &&
|
|
4660
4892
|
path.resolve(current.locator.stateRoot) === path.resolve(expected.locator.stateRoot)
|
|
4661
4893
|
);
|
|
4662
4894
|
}
|
|
@@ -4720,13 +4952,15 @@ async function executeLifecycleResponse(
|
|
|
4720
4952
|
if (operation === "session.create" || operation === "session.fork" || operation === "session.resume") {
|
|
4721
4953
|
await broker.index.refresh();
|
|
4722
4954
|
await broker.heartbeatSessions();
|
|
4955
|
+
await broker.index.refresh();
|
|
4723
4956
|
if (operation === "session.resume") {
|
|
4724
4957
|
const requestedSessionId = sessionId(input);
|
|
4725
|
-
const
|
|
4726
|
-
? broker.index.listSessions().sessions
|
|
4727
|
-
:
|
|
4728
|
-
if (
|
|
4958
|
+
const authority = requestedSessionId
|
|
4959
|
+
? liveResumeAuthority(broker.index.listSessions().sessions, requestedSessionId)
|
|
4960
|
+
: { kind: "none" as const };
|
|
4961
|
+
if (authority.kind === "ambiguous")
|
|
4729
4962
|
return fail("endpoint_stale", "Session authority is ambiguous and cannot be resumed safely.");
|
|
4963
|
+
const existing = authority.kind === "live" ? authority.record : undefined;
|
|
4730
4964
|
if (existing?.live) {
|
|
4731
4965
|
const initialScope = await validateLiveResumeScope(broker, input, requestedSessionId!, existing);
|
|
4732
4966
|
if ("ok" in initialScope) return initialScope;
|
|
@@ -4743,16 +4977,17 @@ async function executeLifecycleResponse(
|
|
|
4743
4977
|
"live_session",
|
|
4744
4978
|
"Session is already live but its incarnation-bound endpoint is unavailable.",
|
|
4745
4979
|
);
|
|
4746
|
-
await broker
|
|
4747
|
-
const current = broker.index
|
|
4748
|
-
.listSessions()
|
|
4749
|
-
.sessions.find(session => session.sessionId === requestedSessionId);
|
|
4750
|
-
if (!current || !sameLiveResumeRecord(existing, current))
|
|
4751
|
-
return fail("endpoint_stale", "Live session changed while its resume authority was being verified.");
|
|
4752
|
-
const finalScope = await validateLiveResumeScope(broker, input, requestedSessionId!, current);
|
|
4980
|
+
const finalScope = await validateLiveResumeScope(broker, input, requestedSessionId!, existing);
|
|
4753
4981
|
if ("ok" in finalScope) return finalScope;
|
|
4754
4982
|
if (!sameResumeSessionIdentity(initialScope, finalScope))
|
|
4755
4983
|
return fail("endpoint_stale", "Saved session changed while its resume authority was being verified.");
|
|
4984
|
+
await broker.index.refresh();
|
|
4985
|
+
const finalAuthority = liveResumeAuthority(broker.index.listSessions().sessions, requestedSessionId!);
|
|
4986
|
+
if (finalAuthority.kind === "ambiguous")
|
|
4987
|
+
return fail("endpoint_stale", "Session authority became ambiguous while it was being verified.");
|
|
4988
|
+
const current = finalAuthority.kind === "live" ? finalAuthority.record : undefined;
|
|
4989
|
+
if (!current || !sameLiveResumeRecord(existing, current))
|
|
4990
|
+
return fail("endpoint_stale", "Live session changed while its resume authority was being verified.");
|
|
4756
4991
|
if (current.endpointMtimeMs === undefined)
|
|
4757
4992
|
return fail("endpoint_stale", "Live session endpoint authority is incomplete.");
|
|
4758
4993
|
return {
|
|
@@ -4792,8 +5027,16 @@ async function executeLifecycleResponse(
|
|
|
4792
5027
|
const queueWaitMs = startupQueueWaitMs(requestedReadinessTimeoutMs);
|
|
4793
5028
|
const launch = await launchInput(broker, operation, input);
|
|
4794
5029
|
if ("ok" in launch) return launch;
|
|
5030
|
+
if (launch.worktreePlan && deadlineFieldsPresent(input)) {
|
|
5031
|
+
return fail(
|
|
5032
|
+
"invalid_input",
|
|
5033
|
+
"Lifecycle worktree launches cannot carry a caller-supplied child deadline tuple.",
|
|
5034
|
+
);
|
|
5035
|
+
}
|
|
4795
5036
|
const admitted = await broker.runStartup(queueWaitMs, timing, async admittedAt => {
|
|
4796
|
-
const admittedInput =
|
|
5037
|
+
const admittedInput = launch.worktreePlan
|
|
5038
|
+
? { ...input, admittedAt, requestedReadinessTimeoutMs }
|
|
5039
|
+
: { ...input, ...deriveLifecycleDeadlines(admittedAt, requestedReadinessTimeoutMs) };
|
|
4797
5040
|
startupAdmittedInputs.add(admittedInput);
|
|
4798
5041
|
startupLaunchInputs.set(admittedInput, launch);
|
|
4799
5042
|
try {
|
|
@@ -4813,27 +5056,73 @@ async function executeLifecycleResponse(
|
|
|
4813
5056
|
return fail("startup_admission_timeout", failure.message);
|
|
4814
5057
|
}
|
|
4815
5058
|
|
|
4816
|
-
const deadlines = lifecycleDeadlines(input, input.receivedAt as number);
|
|
4817
|
-
|
|
4818
|
-
if ("ok" in deadlines) return deadlines;
|
|
4819
|
-
const lifecycleDeadline = deadlines.lifecycleCleanupDeadlineAt;
|
|
4820
|
-
const readinessDeadline = deadlines.semanticReadyDeadlineAt;
|
|
4821
|
-
const terminationStartDeadline = deadlines.terminationStartDeadlineAt;
|
|
4822
|
-
|
|
4823
5059
|
const launch = startupLaunchInputs.get(input) ?? (await launchInput(broker, operation, input));
|
|
4824
5060
|
if ("ok" in launch) return launch;
|
|
5061
|
+
if (launch.worktreePlan && deadlineFieldsPresent(input) && input.admittedAt === undefined) {
|
|
5062
|
+
return fail(
|
|
5063
|
+
"invalid_input",
|
|
5064
|
+
"Lifecycle worktree launches cannot carry a caller-supplied child deadline tuple.",
|
|
5065
|
+
);
|
|
5066
|
+
}
|
|
5067
|
+
|
|
5068
|
+
let childDeadlines: LifecycleDeadlines | undefined;
|
|
5069
|
+
let lifecycleDeadline: number;
|
|
5070
|
+
let readinessDeadline: number;
|
|
5071
|
+
let terminationStartDeadline: number;
|
|
5072
|
+
let outerCleanupDeadlineAt: number | undefined;
|
|
5073
|
+
if (launch.worktreePlan) {
|
|
5074
|
+
const prepTimeouts = readPreparationTimeouts(input);
|
|
5075
|
+
if (!prepTimeouts.ok) return fail("invalid_input", PREPARATION_TIMEOUT_INVALID_MESSAGE);
|
|
5076
|
+
const admittedAt =
|
|
5077
|
+
typeof input.admittedAt === "number" && Number.isSafeInteger(input.admittedAt)
|
|
5078
|
+
? input.admittedAt
|
|
5079
|
+
: timing.now();
|
|
5080
|
+
const requestedReadinessTimeoutMs =
|
|
5081
|
+
typeof input.requestedReadinessTimeoutMs === "number" &&
|
|
5082
|
+
isValidReadinessTimeoutMs(input.requestedReadinessTimeoutMs)
|
|
5083
|
+
? input.requestedReadinessTimeoutMs
|
|
5084
|
+
: readinessTimeout(input);
|
|
5085
|
+
if (typeof requestedReadinessTimeoutMs !== "number") return requestedReadinessTimeoutMs;
|
|
5086
|
+
const outer = deriveLifecycleOuterDeadlines({
|
|
5087
|
+
admittedAt,
|
|
5088
|
+
worktreePrepTimeoutMs: prepTimeouts.worktreePrepTimeoutMs,
|
|
5089
|
+
dependencyPrepTimeoutMs: prepTimeouts.dependencyPrepTimeoutMs,
|
|
5090
|
+
requestedReadinessTimeoutMs,
|
|
5091
|
+
});
|
|
5092
|
+
outerCleanupDeadlineAt = outer.lifecycleCleanupDeadlineAt;
|
|
5093
|
+
lifecycleDeadline = outer.lifecycleCleanupDeadlineAt;
|
|
5094
|
+
readinessDeadline = outer.lifecycleCleanupDeadlineAt;
|
|
5095
|
+
terminationStartDeadline = outer.lifecycleCleanupDeadlineAt;
|
|
5096
|
+
} else {
|
|
5097
|
+
const deadlines = lifecycleDeadlines(input, input.receivedAt as number);
|
|
5098
|
+
if ("ok" in deadlines) return deadlines;
|
|
5099
|
+
childDeadlines = deadlines;
|
|
5100
|
+
lifecycleDeadline = deadlines.lifecycleCleanupDeadlineAt;
|
|
5101
|
+
readinessDeadline = deadlines.semanticReadyDeadlineAt;
|
|
5102
|
+
terminationStartDeadline = deadlines.terminationStartDeadlineAt;
|
|
5103
|
+
}
|
|
5104
|
+
|
|
4825
5105
|
if (!hasProcessIncarnationAuthority())
|
|
4826
5106
|
return fail(
|
|
4827
5107
|
"incarnation_unavailable",
|
|
4828
5108
|
"OS process incarnation authority is unavailable; refusing to spawn a lifecycle session.",
|
|
4829
5109
|
);
|
|
5110
|
+
if (launch.worktreePlan) {
|
|
5111
|
+
const occupant = worktreeOccupant(broker.index.listSessions().sessions, launch.worktreePlan.worktreePath);
|
|
5112
|
+
if (occupant && occupant !== launch.id)
|
|
5113
|
+
return fail(
|
|
5114
|
+
"worktree_in_use",
|
|
5115
|
+
`The requested worktree is already held by session ${occupant}. Choose another worktree name or stop that session.`,
|
|
5116
|
+
);
|
|
5117
|
+
}
|
|
4830
5118
|
const effectMarker = randomUUID();
|
|
4831
5119
|
const plannedWorktreeIntent = worktreeIntent(launch.worktreePlan);
|
|
4832
5120
|
const effectIntent: LifecycleEffectIntentWithDeadline = {
|
|
4833
5121
|
sessionId: launch.id,
|
|
4834
5122
|
stateRoot: launch.root,
|
|
4835
5123
|
childOwnershipEstablished: false,
|
|
4836
|
-
lifecycleCleanupDeadlineAt:
|
|
5124
|
+
lifecycleCleanupDeadlineAt:
|
|
5125
|
+
outerCleanupDeadlineAt ?? (childDeadlines as LifecycleDeadlines).lifecycleCleanupDeadlineAt,
|
|
4837
5126
|
...(plannedWorktreeIntent ? { worktree: plannedWorktreeIntent } : {}),
|
|
4838
5127
|
};
|
|
4839
5128
|
|
|
@@ -4850,39 +5139,109 @@ async function executeLifecycleResponse(
|
|
|
4850
5139
|
let worktreeReceipt: SessionLifecycleWorktreeReceipt | undefined;
|
|
4851
5140
|
try {
|
|
4852
5141
|
if (launch.worktreePlan) {
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
|
|
4867
|
-
|
|
5142
|
+
const prepTimeouts = readPreparationTimeouts(input);
|
|
5143
|
+
if (!prepTimeouts.ok) return fail("invalid_input", PREPARATION_TIMEOUT_INVALID_MESSAGE);
|
|
5144
|
+
const admittedAt =
|
|
5145
|
+
typeof input.admittedAt === "number" && Number.isSafeInteger(input.admittedAt)
|
|
5146
|
+
? input.admittedAt
|
|
5147
|
+
: timing.now();
|
|
5148
|
+
const requestedReadinessTimeoutMs =
|
|
5149
|
+
typeof input.requestedReadinessTimeoutMs === "number" &&
|
|
5150
|
+
isValidReadinessTimeoutMs(input.requestedReadinessTimeoutMs)
|
|
5151
|
+
? input.requestedReadinessTimeoutMs
|
|
5152
|
+
: readinessTimeout(input);
|
|
5153
|
+
if (typeof requestedReadinessTimeoutMs !== "number") return requestedReadinessTimeoutMs;
|
|
5154
|
+
const outer = deriveLifecycleOuterDeadlines({
|
|
5155
|
+
admittedAt,
|
|
5156
|
+
worktreePrepTimeoutMs: prepTimeouts.worktreePrepTimeoutMs,
|
|
5157
|
+
dependencyPrepTimeoutMs: prepTimeouts.dependencyPrepTimeoutMs,
|
|
5158
|
+
requestedReadinessTimeoutMs,
|
|
5159
|
+
});
|
|
5160
|
+
const controller = new AbortController();
|
|
5161
|
+
const worktreeStartedAt = timing.now();
|
|
5162
|
+
const worktreeWatch = abortWhenDue(controller, outer.worktreePreparationDeadlineAt, () => timing.now());
|
|
5163
|
+
try {
|
|
5164
|
+
worktreeReceipt = await preparePlannedWorktree(launch.worktreePlan, {
|
|
5165
|
+
signal: controller.signal,
|
|
5166
|
+
deadlineAt: outer.worktreePreparationDeadlineAt,
|
|
5167
|
+
now: () => timing.now(),
|
|
5168
|
+
});
|
|
5169
|
+
} finally {
|
|
5170
|
+
clearInterval(worktreeWatch);
|
|
5171
|
+
}
|
|
5172
|
+
const worktreeDoneAt = timing.now();
|
|
5173
|
+
const worktreePreparationMs = Math.max(0, worktreeDoneAt - worktreeStartedAt);
|
|
5174
|
+
await broker.ledger.transition(identity, "effect_started", {
|
|
5175
|
+
durableEffects: durableWorktreeEffects(worktreeReceipt, { worktreePreparationMs }),
|
|
5176
|
+
});
|
|
5177
|
+
const remainingDepBudget = Math.min(
|
|
5178
|
+
prepTimeouts.dependencyPrepTimeoutMs,
|
|
5179
|
+
Math.max(0, outer.lifecycleCleanupDeadlineAt - worktreeDoneAt - requestedReadinessTimeoutMs),
|
|
5180
|
+
);
|
|
5181
|
+
const dependencyDeadlineAt = worktreeDoneAt + remainingDepBudget;
|
|
5182
|
+
if (timing.now() >= dependencyDeadlineAt) throw new DependencyPreparationTimeoutError();
|
|
5183
|
+
const depWatch = abortWhenDue(controller, dependencyDeadlineAt, () => timing.now());
|
|
5184
|
+
const depHook = ensureReusableNodeModulesForTest;
|
|
5185
|
+
const dependencyStartedAt = timing.now();
|
|
5186
|
+
try {
|
|
5187
|
+
if (depHook) {
|
|
5188
|
+
await depHook(launch.worktreePlan.repoRoot, launch.worktreePlan.worktreePath, {
|
|
5189
|
+
signal: controller.signal,
|
|
5190
|
+
deadlineAt: dependencyDeadlineAt,
|
|
5191
|
+
});
|
|
5192
|
+
} else {
|
|
5193
|
+
await ensureReusableNodeModulesCancellable(
|
|
5194
|
+
launch.worktreePlan.repoRoot,
|
|
5195
|
+
launch.worktreePlan.worktreePath,
|
|
5196
|
+
{
|
|
5197
|
+
signal: controller.signal,
|
|
5198
|
+
deadlineAt: dependencyDeadlineAt,
|
|
5199
|
+
now: () => timing.now(),
|
|
5200
|
+
},
|
|
5201
|
+
);
|
|
5202
|
+
}
|
|
5203
|
+
} finally {
|
|
5204
|
+
clearInterval(depWatch);
|
|
5205
|
+
}
|
|
5206
|
+
const dependencyPreparationMs = Math.max(0, timing.now() - dependencyStartedAt);
|
|
5207
|
+
await broker.ledger.transition(identity, "effect_started", {
|
|
5208
|
+
durableEffects: durableWorktreeEffects(worktreeReceipt, {
|
|
5209
|
+
worktreePreparationMs,
|
|
5210
|
+
dependencyPreparationMs,
|
|
5211
|
+
spawnAuthorizedAtOffsetMs: Math.max(0, timing.now() - admittedAt),
|
|
5212
|
+
}),
|
|
5213
|
+
});
|
|
5214
|
+
const prepSucceededAt = timing.now();
|
|
5215
|
+
childDeadlines = deriveLifecycleDeadlines(prepSucceededAt, requestedReadinessTimeoutMs);
|
|
5216
|
+
readinessDeadline = childDeadlines.semanticReadyDeadlineAt;
|
|
5217
|
+
terminationStartDeadline = childDeadlines.terminationStartDeadlineAt;
|
|
5218
|
+
lifecycleDeadline = childDeadlines.lifecycleCleanupDeadlineAt;
|
|
4868
5219
|
}
|
|
4869
5220
|
await reapDeadLifecycleMarkers(launch.root);
|
|
4870
5221
|
} catch (error) {
|
|
4871
|
-
|
|
4872
|
-
|
|
4873
|
-
|
|
4874
|
-
);
|
|
5222
|
+
if (launch.worktreePlan && worktreeReceipt?.created && !worktreeReceipt.reused) {
|
|
5223
|
+
removeOwnedLaunchWorktree(launch.worktreePlan, worktreeReceipt);
|
|
5224
|
+
}
|
|
5225
|
+
return mapPreparationFailure(error);
|
|
4875
5226
|
}
|
|
4876
|
-
if (timing.now() >= readinessDeadline)
|
|
5227
|
+
if (!launch.worktreePlan && timing.now() >= readinessDeadline)
|
|
4877
5228
|
return fail(
|
|
4878
5229
|
"readiness_timeout",
|
|
4879
5230
|
"Lifecycle preparation exhausted the semantic readiness deadline before spawning.",
|
|
4880
5231
|
);
|
|
5232
|
+
if (launch.worktreePlan && !childDeadlines)
|
|
5233
|
+
return fail(
|
|
5234
|
+
worktreeReceipt ? "dependency_preparation_timeout" : "worktree_preparation_timeout",
|
|
5235
|
+
worktreeReceipt
|
|
5236
|
+
? "Dependency preparation exceeded its deadline before the session host was spawned."
|
|
5237
|
+
: "Worktree preparation exceeded its deadline before the session host was spawned.",
|
|
5238
|
+
);
|
|
4881
5239
|
if (!hasProcessIncarnationAuthority())
|
|
4882
5240
|
return fail(
|
|
4883
5241
|
"incarnation_unavailable",
|
|
4884
5242
|
"OS process incarnation authority is unavailable; refusing to spawn a lifecycle session.",
|
|
4885
5243
|
);
|
|
5244
|
+
const deadlines = childDeadlines as LifecycleDeadlines;
|
|
4886
5245
|
|
|
4887
5246
|
const coordinatorSidecarTarget =
|
|
4888
5247
|
typeof launch.coordinatorStateDir === "string" && typeof launch.coordinatorSidecarKeyId === "string"
|
|
@@ -4933,7 +5292,13 @@ async function executeLifecycleResponse(
|
|
|
4933
5292
|
// by the OS rather than captured (#4712 review).
|
|
4934
5293
|
stdio: "ignore",
|
|
4935
5294
|
env: {
|
|
4936
|
-
|
|
5295
|
+
// Master capability is process-local to direct Bash children and must
|
|
5296
|
+
// never cross a broker lifecycle launch boundary.
|
|
5297
|
+
...Object.fromEntries(
|
|
5298
|
+
Object.entries("kind" in cmd ? cmd.env : process.env).filter(
|
|
5299
|
+
([key]) => key !== "GJC_MASTER_CAPABILITY",
|
|
5300
|
+
),
|
|
5301
|
+
),
|
|
4937
5302
|
GJC_AGENT_DIR: broker.settings.agentDir,
|
|
4938
5303
|
GJC_CODING_AGENT_DIR: broker.settings.agentDir,
|
|
4939
5304
|
GJC_SESSION_ID: launch.id,
|
|
@@ -5073,7 +5438,7 @@ async function executeLifecycleResponse(
|
|
|
5073
5438
|
`Session ${launch.id} did not register an endpoint before the readiness timeout.`,
|
|
5074
5439
|
);
|
|
5075
5440
|
}
|
|
5076
|
-
await reconcileReadyScope(broker, launch.id, launch.cwd);
|
|
5441
|
+
await reconcileReadyScope(broker, launch.id, launch.cwd, launch.root, spawnedAuthority);
|
|
5077
5442
|
const verified = await currentReadyAuthority(broker, launch.id, launch.root, spawnedAuthority);
|
|
5078
5443
|
if (!verified || !sameReadyAuthority(readiness.authority, verified)) {
|
|
5079
5444
|
const exited =
|