@gajae-code/coding-agent 0.15.5 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +85 -1
- package/dist/types/async/job-manager.d.ts +83 -0
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/auth-gateway-cli.d.ts +17 -0
- package/dist/types/cli/model-presets-cli.d.ts +9 -0
- package/dist/types/cli/root-flags.d.ts +14 -0
- package/dist/types/cli/update-cli.d.ts +15 -0
- package/dist/types/cli.d.ts +7 -0
- package/dist/types/commands/auth-gateway.d.ts +3 -0
- package/dist/types/commands/gc.d.ts +13 -0
- package/dist/types/commands/launch.d.ts +10 -0
- package/dist/types/commands/model-presets.d.ts +21 -0
- package/dist/types/config/autorouting-tier-map.d.ts +120 -0
- package/dist/types/config/file-lock.d.ts +54 -2
- package/dist/types/config/internal/model-preset-registry-test-state.d.ts +4 -0
- package/dist/types/config/internal/model-preset-registry-test-support.d.ts +3 -0
- package/dist/types/config/keybindings.d.ts +3 -2
- package/dist/types/config/model-preset-registry.d.ts +368 -0
- package/dist/types/config/model-profile-activation.d.ts +39 -0
- package/dist/types/config/model-profiles.d.ts +5 -3
- package/dist/types/config/model-registry.d.ts +17 -5
- package/dist/types/config/settings-schema.d.ts +19 -1
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/coordinator-mcp/model-preset.d.ts +7 -5
- package/dist/types/coordinator-mcp/projection-scan.d.ts +54 -0
- package/dist/types/crash/record-loader.d.ts +15 -0
- package/dist/types/extensibility/extensions/types.d.ts +2 -5
- package/dist/types/gjc-runtime/empty-delete-gc.d.ts +58 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +2 -0
- package/dist/types/gjc-runtime/launch-worktree-reservation.d.ts +21 -0
- package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +2 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +35 -6
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +72 -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/index.d.ts +1 -0
- package/dist/types/main.d.ts +3 -1
- 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/components/frictionless-onboarding-selector.d.ts +10 -0
- package/dist/types/modes/components/jobs-overlay-model.d.ts +4 -2
- package/dist/types/modes/components/oauth-selector.d.ts +1 -0
- 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/jobs-observer.d.ts +36 -0
- package/dist/types/modes/print-mode.d.ts +17 -0
- package/dist/types/modes/ui-language.d.ts +7 -1
- package/dist/types/sdk/broker/broker.d.ts +80 -1
- package/dist/types/sdk/broker/endpoint-authority.d.ts +24 -0
- package/dist/types/sdk/broker/index.d.ts +3 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +26 -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/state-version.d.ts +4 -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/index.d.ts +6 -0
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +3 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +4 -0
- 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 +4 -3
- package/dist/types/sdk/cli/session-cli.d.ts +18 -2
- package/dist/types/sdk/client/client.d.ts +3 -2
- package/dist/types/sdk/client/discovery.d.ts +1 -0
- 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/host.d.ts +10 -0
- package/dist/types/sdk/host/model-pin.d.ts +7 -2
- package/dist/types/sdk/host/sdk-run-capability.d.ts +1 -0
- package/dist/types/sdk/host/session-runtime.d.ts +67 -1
- 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 +2 -0
- package/dist/types/sdk/prompt-deadline-manager.d.ts +11 -1
- package/dist/types/sdk/protocol/adapter-validation.d.ts +2 -0
- package/dist/types/sdk/router/session-router.d.ts +11 -0
- package/dist/types/sdk/session.d.ts +16 -0
- package/dist/types/sdk/startup-capability.d.ts +2 -1
- package/dist/types/session/account-inventory.d.ts +1 -0
- package/dist/types/session/agent-session.d.ts +24 -7
- package/dist/types/session/credential-store-errors.d.ts +1 -0
- package/dist/types/session/fold-coordinator.d.ts +201 -0
- package/dist/types/session/sdk-run-capability-internal.d.ts +7 -0
- package/dist/types/session/sdk-run-capability.d.ts +2 -0
- package/dist/types/session/session-manager.d.ts +65 -0
- package/dist/types/session/yield-queue.d.ts +5 -4
- package/dist/types/setup/frictionless-onboarding.d.ts +1 -0
- package/dist/types/telemetry/control.d.ts +11 -0
- package/dist/types/telemetry/events.d.ts +20 -0
- package/dist/types/telemetry/index.d.ts +3 -0
- package/dist/types/telemetry/transport.d.ts +22 -0
- package/dist/types/tools/bash-interactive.d.ts +23 -0
- package/dist/types/tools/bash.d.ts +4 -0
- package/dist/types/tools/gh.d.ts +8 -0
- package/dist/types/tools/index.d.ts +10 -5
- package/dist/types/web/search/providers/base.d.ts +9 -4
- package/dist/types/web/search/providers/xai.d.ts +2 -1
- package/dist/types/web/search/types.d.ts +14 -0
- package/package.json +10 -7
- package/scripts/generate-hotkeys-docs.ts +4 -0
- package/scripts/generate-sdk-operation-inventory.ts +9 -3
- package/scripts/verify-gjc-sdk-canonicalization.ts +35 -1
- package/src/async/job-manager.ts +671 -39
- package/src/cli/accounts-cli.ts +9 -5
- package/src/cli/args.ts +17 -0
- package/src/cli/auth-broker-cli.ts +40 -12
- package/src/cli/auth-gateway-cli.ts +273 -97
- package/src/cli/config-cli.ts +2 -2
- package/src/cli/fast-help.ts +4 -0
- package/src/cli/model-presets-cli.ts +100 -0
- package/src/cli/root-flags.ts +5 -0
- package/src/cli/update-cli.ts +117 -16
- package/src/cli.ts +6 -2
- package/src/commands/auth-gateway.ts +12 -8
- package/src/commands/gc.ts +19 -0
- package/src/commands/launch.ts +86 -18
- package/src/commands/model-presets.ts +24 -0
- package/src/commands/sdk.ts +113 -21
- package/src/config/autorouting-tier-map.ts +44 -0
- package/src/config/file-lock-gc.ts +9 -4
- package/src/config/file-lock.ts +890 -99
- package/src/config/internal/model-preset-registry-test-state.ts +28 -0
- package/src/config/internal/model-preset-registry-test-support.ts +11 -0
- package/src/config/keybindings.ts +6 -2
- package/src/config/model-preset-registry.ts +2795 -0
- package/src/config/model-profile-activation.ts +115 -28
- package/src/config/model-profile-contract.ts +4 -1
- package/src/config/model-profiles.ts +56 -14
- package/src/config/model-registry.ts +1044 -122
- package/src/config/model-resolver.ts +5 -0
- package/src/config/settings-schema.ts +19 -1
- package/src/config/settings.ts +11 -1
- package/src/coordinator-mcp/model-preset.ts +32 -17
- package/src/coordinator-mcp/projection-scan.ts +592 -0
- package/src/coordinator-mcp/question-state.ts +9 -11
- package/src/coordinator-mcp/server.ts +111 -42
- package/src/coordinator-mcp/session-reaper.ts +9 -3
- package/src/crash/index-store.ts +2 -0
- package/src/crash/record-loader.ts +10 -0
- package/src/extensibility/extensions/loader.ts +0 -1
- package/src/extensibility/extensions/types.ts +2 -3
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +1 -0
- package/src/gjc-runtime/empty-delete-gc.ts +301 -0
- package/src/gjc-runtime/gc-render.ts +29 -0
- package/src/gjc-runtime/gc-runtime.ts +174 -11
- package/src/gjc-runtime/launch-worktree-reservation.ts +242 -0
- package/src/gjc-runtime/managed-owner-supervisor.ts +63 -51
- package/src/gjc-runtime/session-state-lock.ts +1321 -147
- package/src/gjc-runtime/session-state-sidecar.ts +1004 -25
- package/src/gjc-runtime/tmux-owner-isolation-cli.ts +4 -7
- package/src/gjc-runtime/tmux-owner-isolation.ts +2 -2
- package/src/gjc-runtime/tmux-sessions.ts +252 -13
- package/src/hooks/skill-state.ts +6 -0
- package/src/index.ts +1 -0
- package/src/internal-urls/docs-index.generated.ts +14 -13
- package/src/internal-urls/issue-pr-protocol.ts +4 -2
- package/src/lsp/config.ts +4 -3
- package/src/lsp/path-trust.ts +12 -6
- package/src/main.ts +66 -4
- 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 +616 -161
- package/src/modes/components/footer.ts +4 -2
- package/src/modes/components/frictionless-onboarding-selector.ts +48 -2
- package/src/modes/components/jobs-overlay-model.ts +99 -16
- package/src/modes/components/model-selector.ts +158 -18
- package/src/modes/components/oauth-selector.ts +6 -1
- 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 +29 -10
- package/src/modes/components/tool-status-header.ts +151 -15
- package/src/modes/controllers/extension-ui-controller.ts +1 -1
- package/src/modes/controllers/input-controller.ts +51 -6
- package/src/modes/controllers/selector-controller.ts +191 -96
- package/src/modes/interactive-mode.ts +14 -1
- package/src/modes/jobs-observer.ts +149 -16
- package/src/modes/print-mode.ts +64 -0
- package/src/modes/runtime-init.ts +1 -1
- package/src/modes/ui-language.ts +49 -2
- package/src/modes/utils/injected-user-submission.ts +15 -1
- package/src/prompts/system/master-mode.md +28 -0
- package/src/prompts/tools/bash.md +2 -2
- package/src/prompts/tools/fold-receipt.md +3 -0
- package/src/prompts/tools/job.md +1 -1
- package/src/sdk/acp/adapter.ts +17 -2
- package/src/sdk/broker/broker.ts +1671 -34
- package/src/sdk/broker/endpoint-authority.ts +86 -0
- package/src/sdk/broker/ensure.ts +5 -0
- package/src/sdk/broker/index.ts +3 -0
- package/src/sdk/broker/lifecycle.ts +217 -46
- 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 +548 -0
- package/src/sdk/broker/state-version.ts +26 -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 +46 -5
- package/src/sdk/bus/index.ts +51 -4
- package/src/sdk/bus/kind-aware-reconciliation.ts +36 -6
- package/src/sdk/bus/prompt-reconciliation.ts +28 -4
- package/src/sdk/bus/slack-conversation.ts +5 -1
- 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 +11 -8
- package/src/sdk/cli/session-cli.ts +276 -6
- package/src/sdk/client/client.ts +14 -8
- package/src/sdk/client/discovery.ts +3 -1
- package/src/sdk/host/control/dispatch.ts +101 -37
- 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/host.ts +27 -2
- package/src/sdk/host/model-pin.ts +185 -25
- package/src/sdk/host/sdk-run-capability.ts +1 -0
- package/src/sdk/host/session-runtime.test.ts +581 -43
- package/src/sdk/host/session-runtime.ts +655 -101
- package/src/sdk/lifecycle/broker-client.ts +28 -0
- package/src/sdk/lifecycle/service.ts +279 -20
- package/src/sdk/lifecycle-session.ts +10 -2
- package/src/sdk/prompt-deadline-manager.ts +16 -1
- package/src/sdk/protocol/adapter-validation.ts +27 -0
- package/src/sdk/protocol/operation-inventory.generated.json +53 -2
- package/src/sdk/protocol/operation-registry.ts +17 -4
- package/src/sdk/router/session-router.ts +294 -154
- package/src/sdk/session.ts +440 -167
- package/src/sdk/startup-capability.ts +1 -0
- package/src/session/account-inventory.ts +27 -11
- package/src/session/agent-session.ts +1292 -180
- package/src/session/blob-store.ts +15 -5
- package/src/session/credential-store-errors.ts +2 -0
- package/src/session/fold-coordinator.ts +433 -0
- package/src/session/internal/managed-session-storage.ts +153 -78
- package/src/session/sdk-run-capability-internal.ts +15 -0
- package/src/session/sdk-run-capability.ts +2 -0
- package/src/session/session-manager.ts +342 -28
- package/src/session/yield-queue.ts +24 -10
- package/src/setup/frictionless-onboarding.ts +2 -0
- package/src/setup/provider-presets.json +1 -1
- package/src/slash-commands/helpers/credential-switch.ts +6 -1
- package/src/task/executor.ts +19 -3
- package/src/telemetry/control.ts +26 -0
- package/src/telemetry/events.ts +1268 -0
- package/src/telemetry/index.ts +3 -0
- package/src/telemetry/transport.ts +87 -0
- package/src/tools/bash-interactive.ts +182 -85
- package/src/tools/bash.ts +1028 -109
- package/src/tools/gh.ts +104 -48
- package/src/tools/github-cache.ts +2 -2
- package/src/tools/index.ts +11 -5
- package/src/tools/tool-catalog.generated.ts +1 -1
- package/src/web/search/provider.ts +120 -11
- package/src/web/search/providers/anthropic.ts +89 -17
- package/src/web/search/providers/base.ts +9 -4
- package/src/web/search/providers/gemini.ts +87 -15
- package/src/web/search/providers/openai-compatible.ts +23 -8
- package/src/web/search/providers/xai.ts +98 -12
- package/src/web/search/types.ts +16 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import * as fsSync from "node:fs";
|
|
2
|
+
import * as fs from "node:fs/promises";
|
|
3
|
+
|
|
4
|
+
const MAX_ENDPOINT_BYTES = 4_096n;
|
|
5
|
+
|
|
6
|
+
export type EndpointFileRead = {
|
|
7
|
+
source: string;
|
|
8
|
+
dev: bigint;
|
|
9
|
+
ino: bigint;
|
|
10
|
+
nlink: bigint;
|
|
11
|
+
size: bigint;
|
|
12
|
+
mtimeNs: bigint;
|
|
13
|
+
mtimeMs: number;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type EndpointAuthorityFilesystem = Pick<typeof fs, "open">;
|
|
17
|
+
|
|
18
|
+
export type IndexedEndpointAuthority = {
|
|
19
|
+
endpointMtimeMs?: number;
|
|
20
|
+
endpointFileId?: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function readFlags(): number {
|
|
24
|
+
return fsSync.constants.O_RDONLY | (fsSync.constants.O_NOFOLLOW ?? 0) | (fsSync.constants.O_NONBLOCK ?? 0);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Reads one endpoint from the descriptor opened for its pathname. The opened
|
|
29
|
+
* object must remain a small, singly-linked regular file with stable identity
|
|
30
|
+
* and metadata for the whole read; a pathname replacement cannot affect the
|
|
31
|
+
* bytes returned by this function.
|
|
32
|
+
*/
|
|
33
|
+
export async function readEndpointFile(
|
|
34
|
+
filePath: string,
|
|
35
|
+
filesystem: EndpointAuthorityFilesystem = fs,
|
|
36
|
+
): Promise<EndpointFileRead | undefined> {
|
|
37
|
+
let handle: fs.FileHandle | undefined;
|
|
38
|
+
try {
|
|
39
|
+
handle = await filesystem.open(filePath, readFlags());
|
|
40
|
+
const before = await handle.stat({ bigint: true });
|
|
41
|
+
if (!before.isFile() || before.nlink !== 1n || before.size > MAX_ENDPOINT_BYTES) return undefined;
|
|
42
|
+
if (before.size > BigInt(Number.MAX_SAFE_INTEGER)) return undefined;
|
|
43
|
+
const bytes = Buffer.alloc(Number(before.size));
|
|
44
|
+
let offset = 0;
|
|
45
|
+
while (offset < bytes.length) {
|
|
46
|
+
const { bytesRead } = await handle.read(bytes, offset, bytes.length - offset, offset);
|
|
47
|
+
if (bytesRead <= 0) return undefined;
|
|
48
|
+
offset += bytesRead;
|
|
49
|
+
}
|
|
50
|
+
const after = await handle.stat({ bigint: true });
|
|
51
|
+
if (
|
|
52
|
+
before.dev !== after.dev ||
|
|
53
|
+
before.ino !== after.ino ||
|
|
54
|
+
before.nlink !== after.nlink ||
|
|
55
|
+
before.size !== after.size ||
|
|
56
|
+
before.mtimeNs !== after.mtimeNs
|
|
57
|
+
)
|
|
58
|
+
return undefined;
|
|
59
|
+
return {
|
|
60
|
+
source: bytes.toString("utf8"),
|
|
61
|
+
dev: before.dev,
|
|
62
|
+
ino: before.ino,
|
|
63
|
+
nlink: before.nlink,
|
|
64
|
+
size: before.size,
|
|
65
|
+
mtimeNs: before.mtimeNs,
|
|
66
|
+
mtimeMs: Number(before.mtimeNs) / 1_000_000,
|
|
67
|
+
};
|
|
68
|
+
} catch {
|
|
69
|
+
return undefined;
|
|
70
|
+
} finally {
|
|
71
|
+
await handle?.close().catch(() => undefined);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Match a descriptor-bound endpoint read to the index representation. */
|
|
76
|
+
export function matchesIndexedEndpointFile(
|
|
77
|
+
file: Pick<EndpointFileRead, "dev" | "ino" | "mtimeMs">,
|
|
78
|
+
authority: IndexedEndpointAuthority,
|
|
79
|
+
): boolean {
|
|
80
|
+
return (
|
|
81
|
+
authority.endpointMtimeMs !== undefined &&
|
|
82
|
+
Number.isFinite(authority.endpointMtimeMs) &&
|
|
83
|
+
Math.abs(file.mtimeMs - authority.endpointMtimeMs) <= 0.001 &&
|
|
84
|
+
(authority.endpointFileId === undefined || authority.endpointFileId === `${file.dev}:${file.ino}`)
|
|
85
|
+
);
|
|
86
|
+
}
|
package/src/sdk/broker/ensure.ts
CHANGED
|
@@ -232,6 +232,11 @@ function registerBrokerOwner(
|
|
|
232
232
|
function brokerSpawnEnvironment(command: SdkInternalSpawnCommand, override?: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
|
233
233
|
const environment = { ...(override ?? command.env) };
|
|
234
234
|
delete environment.BUN_OPTIONS;
|
|
235
|
+
// The master capability is a transient in-memory dispatch input. A broker
|
|
236
|
+
// cold-started from the master's own Bash environment would otherwise inherit
|
|
237
|
+
// it and pass it on to every substrate child it later launches, so it is
|
|
238
|
+
// stripped at the lifecycle boundary exactly as lifecycle children strip it.
|
|
239
|
+
delete environment.GJC_MASTER_CAPABILITY;
|
|
235
240
|
if (command.kind === "bun-source") {
|
|
236
241
|
delete environment.PI_COMPILED;
|
|
237
242
|
delete environment.GJC_COMPILED;
|
package/src/sdk/broker/index.ts
CHANGED
|
@@ -4,6 +4,9 @@ export * from "./ensure";
|
|
|
4
4
|
export * from "./identity";
|
|
5
5
|
export * from "./lifecycle";
|
|
6
6
|
export * from "./lifecycle-ledger";
|
|
7
|
+
export * from "./master-capability";
|
|
7
8
|
export * from "./session-index";
|
|
9
|
+
export * from "./spawn-authority";
|
|
10
|
+
export * from "./spawn-substrate";
|
|
8
11
|
export * from "./startup-budget";
|
|
9
12
|
export * from "./transport";
|
|
@@ -14,7 +14,10 @@ function nativeLifecycle(): typeof import("@gajae-code/natives") {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
import { $credentialEnv, logger, resolveEquivalentPath } from "@gajae-code/utils";
|
|
17
|
-
|
|
17
|
+
import {
|
|
18
|
+
loadAcceptedModelPresetRegistry,
|
|
19
|
+
loadAcceptedModelPresetRegistryAsync,
|
|
20
|
+
} from "../../config/model-preset-registry";
|
|
18
21
|
import {
|
|
19
22
|
isModelProfileError,
|
|
20
23
|
type ModelProfileErrorDetails,
|
|
@@ -63,6 +66,7 @@ import {
|
|
|
63
66
|
sanitizeSdkStartupMessage,
|
|
64
67
|
} from "../startup-capability";
|
|
65
68
|
import type { Broker, BrokerCleanupEvidence, BrokerCleanupIdentity, BrokerResponse } from "./broker";
|
|
69
|
+
import { matchesIndexedEndpointFile, readEndpointFile } from "./endpoint-authority";
|
|
66
70
|
import { decodeLifecycleUtf8, parseLifecycleJson } from "./lifecycle-codec";
|
|
67
71
|
import type {
|
|
68
72
|
LifecycleCleanupProof,
|
|
@@ -78,7 +82,12 @@ import {
|
|
|
78
82
|
processIncarnation,
|
|
79
83
|
} from "./process-incarnation";
|
|
80
84
|
import { resolveSdkInternalSpawnCommand, type SdkInternalSpawnCommand } from "./runtime";
|
|
81
|
-
import {
|
|
85
|
+
import {
|
|
86
|
+
type IndexedSession,
|
|
87
|
+
isSessionAuthorityEligible,
|
|
88
|
+
resolveSessionLocator,
|
|
89
|
+
type SessionLocatorV2,
|
|
90
|
+
} from "./session-index";
|
|
82
91
|
import {
|
|
83
92
|
cancellableSleep,
|
|
84
93
|
DEFAULT_READINESS_TIMEOUT_MS,
|
|
@@ -86,6 +95,7 @@ import {
|
|
|
86
95
|
READINESS_TIMEOUT_INVALID_MESSAGE,
|
|
87
96
|
startupQueueWaitMs,
|
|
88
97
|
} from "./startup-budget";
|
|
98
|
+
import { worktreeOccupant } from "./worktree-occupancy";
|
|
89
99
|
|
|
90
100
|
export {
|
|
91
101
|
type ProcessIncarnationCommandRunner,
|
|
@@ -609,13 +619,36 @@ function text(value: unknown): string | undefined {
|
|
|
609
619
|
return typeof value === "string" && value ? value : undefined;
|
|
610
620
|
}
|
|
611
621
|
|
|
612
|
-
function
|
|
622
|
+
export function validateBrokerModelPresetSync(agentDir: string, requestedProfile: string): string | BrokerResponse {
|
|
613
623
|
const modelsConfigFile = ModelsConfigFile.relocate(path.join(agentDir, "models.yml"));
|
|
614
624
|
modelsConfigFile.invalidate();
|
|
615
625
|
const loaded = modelsConfigFile.tryLoad();
|
|
616
|
-
const
|
|
626
|
+
const accepted = loadAcceptedModelPresetRegistry(agentDir);
|
|
627
|
+
const profiles = mergeModelProfiles(loaded.status === "ok" ? loaded.value.profiles : undefined, accepted.profiles);
|
|
617
628
|
try {
|
|
618
|
-
return validateModelProfileName(
|
|
629
|
+
return validateModelProfileName(
|
|
630
|
+
requestedProfile,
|
|
631
|
+
profiles,
|
|
632
|
+
accepted.error ?? (loaded.status === "error" ? loaded.error : undefined),
|
|
633
|
+
);
|
|
634
|
+
} catch (error) {
|
|
635
|
+
if (isModelProfileError(error)) return fail(error.code, error.message, undefined, error.details);
|
|
636
|
+
throw error;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
async function validateBrokerModelPreset(agentDir: string, requestedProfile: string): Promise<string | BrokerResponse> {
|
|
641
|
+
const modelsConfigFile = ModelsConfigFile.relocate(path.join(agentDir, "models.yml"));
|
|
642
|
+
modelsConfigFile.invalidate();
|
|
643
|
+
const loaded = modelsConfigFile.tryLoad();
|
|
644
|
+
const accepted = await loadAcceptedModelPresetRegistryAsync(agentDir);
|
|
645
|
+
const profiles = mergeModelProfiles(loaded.status === "ok" ? loaded.value.profiles : undefined, accepted.profiles);
|
|
646
|
+
try {
|
|
647
|
+
return validateModelProfileName(
|
|
648
|
+
requestedProfile,
|
|
649
|
+
profiles,
|
|
650
|
+
accepted.error ?? (loaded.status === "error" ? loaded.error : undefined),
|
|
651
|
+
);
|
|
619
652
|
} catch (error) {
|
|
620
653
|
if (isModelProfileError(error)) return fail(error.code, error.message, undefined, error.details);
|
|
621
654
|
throw error;
|
|
@@ -623,7 +656,7 @@ function validateBrokerModelPreset(agentDir: string, requestedProfile: string):
|
|
|
623
656
|
}
|
|
624
657
|
|
|
625
658
|
export function validateBrokerModelPresetForTest(agentDir: string, requestedProfile: string): string | BrokerResponse {
|
|
626
|
-
return
|
|
659
|
+
return validateBrokerModelPresetSync(agentDir, requestedProfile);
|
|
627
660
|
}
|
|
628
661
|
|
|
629
662
|
function readinessTimeout(input: Input): number | BrokerResponse {
|
|
@@ -707,10 +740,13 @@ function lifecycleWorktreeTarget(input: Input): SessionLifecycleWorktreeTarget |
|
|
|
707
740
|
}
|
|
708
741
|
|
|
709
742
|
type LiveResumeRecord = {
|
|
710
|
-
|
|
743
|
+
sessionId: string;
|
|
744
|
+
locator: SessionLocatorV2;
|
|
711
745
|
endpointGeneration: number;
|
|
712
746
|
pid: number;
|
|
713
747
|
endpointMtimeMs?: number;
|
|
748
|
+
processIncarnation?: string;
|
|
749
|
+
hostIncarnation?: string;
|
|
714
750
|
live: boolean;
|
|
715
751
|
ambiguous: boolean;
|
|
716
752
|
};
|
|
@@ -729,7 +765,7 @@ type ResumeScope = {
|
|
|
729
765
|
};
|
|
730
766
|
function sameResumeLocator(record: LiveResumeRecord, cwd: string, root: string): boolean {
|
|
731
767
|
return (
|
|
732
|
-
resolveEquivalentPath(record.locator.
|
|
768
|
+
resolveEquivalentPath(record.locator.cwd) === resolveEquivalentPath(cwd) &&
|
|
733
769
|
resolveEquivalentPath(record.locator.stateRoot) === resolveEquivalentPath(root)
|
|
734
770
|
);
|
|
735
771
|
}
|
|
@@ -751,10 +787,24 @@ function sameLiveResumeRecord(expected: LiveResumeRecord, current: LiveResumeRec
|
|
|
751
787
|
current.endpointGeneration === expected.endpointGeneration &&
|
|
752
788
|
current.pid === expected.pid &&
|
|
753
789
|
current.endpointMtimeMs === expected.endpointMtimeMs &&
|
|
754
|
-
|
|
790
|
+
(current.hostIncarnation ?? current.processIncarnation) ===
|
|
791
|
+
(expected.hostIncarnation ?? expected.processIncarnation) &&
|
|
792
|
+
sameResumeLocator(current, expected.locator.cwd, expected.locator.stateRoot)
|
|
755
793
|
);
|
|
756
794
|
}
|
|
757
795
|
|
|
796
|
+
function liveResumeAuthority(
|
|
797
|
+
sessions: readonly LiveResumeRecord[],
|
|
798
|
+
sessionId: string,
|
|
799
|
+
): { kind: "none" } | { kind: "ambiguous" } | { kind: "live"; record: LiveResumeRecord } {
|
|
800
|
+
const indexedCandidates = sessions.filter(session => session.sessionId === sessionId);
|
|
801
|
+
if (indexedCandidates.some(session => !isSessionAuthorityEligible(session))) return { kind: "ambiguous" };
|
|
802
|
+
const liveCandidates = indexedCandidates.filter(session => session.live);
|
|
803
|
+
if (liveCandidates.length > 1) return { kind: "ambiguous" };
|
|
804
|
+
const record = liveCandidates[0];
|
|
805
|
+
return record ? { kind: "live", record } : { kind: "none" };
|
|
806
|
+
}
|
|
807
|
+
|
|
758
808
|
type ValidatedTranscript = {
|
|
759
809
|
path: string;
|
|
760
810
|
id: string;
|
|
@@ -886,18 +936,36 @@ async function validateLiveResumeScope(
|
|
|
886
936
|
sessionIdentity: session.identity,
|
|
887
937
|
};
|
|
888
938
|
}
|
|
889
|
-
async function reconcileReadyScope(
|
|
939
|
+
async function reconcileReadyScope(
|
|
940
|
+
broker: Broker,
|
|
941
|
+
id: string,
|
|
942
|
+
scope: string | undefined,
|
|
943
|
+
root: string,
|
|
944
|
+
expected: EffectMarker,
|
|
945
|
+
): Promise<void> {
|
|
890
946
|
if (!scope) return;
|
|
891
947
|
await broker.index.refresh();
|
|
892
|
-
const record = broker.index
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
948
|
+
const record = broker.index
|
|
949
|
+
.listSessions()
|
|
950
|
+
.sessions.find(
|
|
951
|
+
session =>
|
|
952
|
+
session.sessionId === id &&
|
|
953
|
+
resolveEquivalentPath(session.locator.stateRoot) === resolveEquivalentPath(root) &&
|
|
954
|
+
session.pid === expected.pid &&
|
|
955
|
+
(session.hostIncarnation ?? session.processIncarnation) === expected.incarnation &&
|
|
956
|
+
session.lifecycleRequestId === expected.effectMarker,
|
|
957
|
+
);
|
|
958
|
+
if (!record) return;
|
|
959
|
+
const cwd = canonicalExistingPath(scope);
|
|
960
|
+
if (record.locator.cwd === cwd) return;
|
|
961
|
+
// Locator cwd is canonical everywhere. Reconcile only a pre-existing host row
|
|
962
|
+
// whose canonical registration race observed a different path; it never retains
|
|
963
|
+
// a caller's lexical spelling for ACP or any other scope consumer.
|
|
964
|
+
const locator = await resolveSessionLocator(scope, record.locator.stateRoot);
|
|
897
965
|
await broker.index.append({
|
|
898
966
|
type: "record_reconciled",
|
|
899
967
|
sessionId: id,
|
|
900
|
-
locator
|
|
968
|
+
locator,
|
|
901
969
|
endpointGeneration: record.endpointGeneration,
|
|
902
970
|
pid: record.pid,
|
|
903
971
|
// Reconciliation only re-scopes the locator, so every identity fact the host
|
|
@@ -907,6 +975,8 @@ async function reconcileReadyScope(broker: Broker, id: string, scope: string | u
|
|
|
907
975
|
...(record.hostIncarnation === undefined ? {} : { hostIncarnation: record.hostIncarnation }),
|
|
908
976
|
...(record.lifecycleRequestId === undefined ? {} : { lifecycleRequestId: record.lifecycleRequestId }),
|
|
909
977
|
endpointMtimeMs: record.endpointMtimeMs,
|
|
978
|
+
...(record.endpointFileId === undefined ? {} : { endpointFileId: record.endpointFileId }),
|
|
979
|
+
...(record.masterRole === undefined ? {} : { masterRole: record.masterRole }),
|
|
910
980
|
});
|
|
911
981
|
}
|
|
912
982
|
|
|
@@ -938,6 +1008,66 @@ function command(broker: Broker): LifecycleCommand {
|
|
|
938
1008
|
return lifecycleCommandResolversForTest.get(broker)?.() ?? resolveSdkInternalSpawnCommand("session-host-internal");
|
|
939
1009
|
}
|
|
940
1010
|
|
|
1011
|
+
/**
|
|
1012
|
+
* Reuses the ordinary lifecycle host bootstrap without performing its direct
|
|
1013
|
+
* process spawn. Broker-owned substrate providers receive this exact argv and
|
|
1014
|
+
* split inherited/child-specific environment contract as their only launch
|
|
1015
|
+
* authority for session.spawn.
|
|
1016
|
+
*/
|
|
1017
|
+
export type SpawnChildHostLaunch = {
|
|
1018
|
+
childId: string;
|
|
1019
|
+
cwd: string;
|
|
1020
|
+
stateRoot: string;
|
|
1021
|
+
argv: readonly string[];
|
|
1022
|
+
inheritedEnv: Readonly<Record<string, string>>;
|
|
1023
|
+
env: Readonly<Record<string, string>>;
|
|
1024
|
+
effectMarker: string;
|
|
1025
|
+
};
|
|
1026
|
+
|
|
1027
|
+
export function prepareSpawnChildHostLaunch(
|
|
1028
|
+
broker: Broker,
|
|
1029
|
+
input: { cwd: string; modelId?: string; modelPreset?: string; childId?: string; receivedAt?: number },
|
|
1030
|
+
): SpawnChildHostLaunch {
|
|
1031
|
+
const cwd = path.resolve(input.cwd);
|
|
1032
|
+
const childId = input.childId ?? randomUUID();
|
|
1033
|
+
const stateRoot = defaultStateRoot(cwd);
|
|
1034
|
+
const effectMarker = randomUUID();
|
|
1035
|
+
const deadlines = deriveLifecycleDeadlines(input.receivedAt ?? Date.now(), DEFAULT_READINESS_TIMEOUT_MS);
|
|
1036
|
+
const request: SessionLifecycleLaunchRequest = {
|
|
1037
|
+
operation: "session.create",
|
|
1038
|
+
sessionId: childId,
|
|
1039
|
+
cwd,
|
|
1040
|
+
stateRoot,
|
|
1041
|
+
effectMarker,
|
|
1042
|
+
...deadlines,
|
|
1043
|
+
...(input.modelId === undefined ? {} : { modelId: input.modelId }),
|
|
1044
|
+
...(input.modelPreset === undefined ? {} : { modelPreset: input.modelPreset }),
|
|
1045
|
+
};
|
|
1046
|
+
const cmd = command(broker);
|
|
1047
|
+
const inherited = "kind" in cmd ? cmd.env : process.env;
|
|
1048
|
+
const inheritedEnv = Object.fromEntries(
|
|
1049
|
+
Object.entries(inherited).filter(
|
|
1050
|
+
(entry): entry is [string, string] => entry[0] !== "GJC_MASTER_CAPABILITY" && typeof entry[1] === "string",
|
|
1051
|
+
),
|
|
1052
|
+
);
|
|
1053
|
+
return {
|
|
1054
|
+
childId,
|
|
1055
|
+
cwd,
|
|
1056
|
+
stateRoot,
|
|
1057
|
+
argv: [cmd.file, ...cmd.args],
|
|
1058
|
+
inheritedEnv,
|
|
1059
|
+
env: {
|
|
1060
|
+
GJC_AGENT_DIR: broker.settings.agentDir,
|
|
1061
|
+
GJC_CODING_AGENT_DIR: broker.settings.agentDir,
|
|
1062
|
+
GJC_SESSION_ID: childId,
|
|
1063
|
+
GJC_STATE_ROOT: stateRoot,
|
|
1064
|
+
GJC_LIFECYCLE_REQUEST_ID: effectMarker,
|
|
1065
|
+
GJC_SDK_LIFECYCLE_REQUEST: JSON.stringify(request),
|
|
1066
|
+
},
|
|
1067
|
+
effectMarker,
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
|
|
941
1071
|
const lifecycleMarkerPath = (root: string, id: string) => path.join(root, "sdk", `${id}.lifecycle.json`);
|
|
942
1072
|
const lifecycleReadyPath = (root: string, id: string) => path.join(root, "sdk", `${id}.lifecycle.ready.json`);
|
|
943
1073
|
const lifecycleFailurePath = (root: string, id: string, effectMarker: string) =>
|
|
@@ -947,6 +1077,7 @@ type ReadyAuthority = {
|
|
|
947
1077
|
endpoint: Record<string, unknown>;
|
|
948
1078
|
endpointSource: string;
|
|
949
1079
|
endpointMtimeMs: number;
|
|
1080
|
+
endpointFileId?: string;
|
|
950
1081
|
endpointGeneration: number;
|
|
951
1082
|
};
|
|
952
1083
|
type ReadinessResult =
|
|
@@ -3325,7 +3456,7 @@ async function recordTerminalUncertain(
|
|
|
3325
3456
|
await broker.index.append({
|
|
3326
3457
|
type: "lifecycle_terminal",
|
|
3327
3458
|
sessionId: id,
|
|
3328
|
-
locator: {
|
|
3459
|
+
locator: { cwd: "unknown", worktreeRoot: null, stateRoot: root },
|
|
3329
3460
|
endpointGeneration: 0,
|
|
3330
3461
|
pid,
|
|
3331
3462
|
terminalUncertain: true,
|
|
@@ -3641,7 +3772,10 @@ async function removeExactDeadSessionEndpoint(
|
|
|
3641
3772
|
: finalEndpointPath;
|
|
3642
3773
|
let handle: fs.FileHandle | undefined;
|
|
3643
3774
|
try {
|
|
3644
|
-
handle = await fs.open(
|
|
3775
|
+
handle = await fs.open(
|
|
3776
|
+
endpointSource,
|
|
3777
|
+
fsSync.constants.O_RDONLY | (fsSync.constants.O_NOFOLLOW ?? 0) | (fsSync.constants.O_NONBLOCK ?? 0),
|
|
3778
|
+
);
|
|
3645
3779
|
const metadata = await handle.stat({ bigint: true });
|
|
3646
3780
|
if (!metadata.isFile() || metadata.nlink !== 1n || metadata.size > 4096n) return false;
|
|
3647
3781
|
const bytes = Buffer.alloc(Number(metadata.size) + 1);
|
|
@@ -3821,10 +3955,9 @@ async function currentReadyAuthority(
|
|
|
3821
3955
|
if (!(await hasOwnedReadinessEvidence(broker, root, id, expected))) return undefined;
|
|
3822
3956
|
const endpointPath = path.join(root, "sdk", `${id}.json`);
|
|
3823
3957
|
try {
|
|
3824
|
-
const
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
]);
|
|
3958
|
+
const endpointFile = await readEndpointFile(endpointPath);
|
|
3959
|
+
if (!endpointFile) return undefined;
|
|
3960
|
+
const endpointSource = endpointFile.source;
|
|
3828
3961
|
const endpoint = JSON.parse(endpointSource) as {
|
|
3829
3962
|
sessionId?: unknown;
|
|
3830
3963
|
url?: unknown;
|
|
@@ -3837,14 +3970,24 @@ async function currentReadyAuthority(
|
|
|
3837
3970
|
// native-dead child; do not refuse a native-alive ready host for a stale live bit.
|
|
3838
3971
|
await broker.heartbeatSessions();
|
|
3839
3972
|
await broker.index.refresh();
|
|
3840
|
-
const record = broker.index
|
|
3973
|
+
const record = broker.index
|
|
3974
|
+
.listSessions()
|
|
3975
|
+
.sessions.find(
|
|
3976
|
+
session =>
|
|
3977
|
+
session.sessionId === id &&
|
|
3978
|
+
session.pid === expected.pid &&
|
|
3979
|
+
resolveEquivalentPath(session.locator.stateRoot) === resolveEquivalentPath(root) &&
|
|
3980
|
+
(session.hostIncarnation ?? session.processIncarnation) === expected.incarnation &&
|
|
3981
|
+
session.lifecycleRequestId === expected.effectMarker,
|
|
3982
|
+
);
|
|
3841
3983
|
if (
|
|
3842
3984
|
!record ||
|
|
3843
3985
|
record.terminal ||
|
|
3844
3986
|
record.terminalUncertain ||
|
|
3845
3987
|
record.pid !== expected.pid ||
|
|
3846
3988
|
resolveEquivalentPath(record.locator.stateRoot) !== resolveEquivalentPath(root) ||
|
|
3847
|
-
record.
|
|
3989
|
+
(record.hostIncarnation ?? record.processIncarnation) !== expected.incarnation ||
|
|
3990
|
+
!matchesIndexedEndpointFile(endpointFile, record) ||
|
|
3848
3991
|
endpoint.pid !== expected.pid ||
|
|
3849
3992
|
endpoint.sessionId !== id ||
|
|
3850
3993
|
typeof endpoint.url !== "string" ||
|
|
@@ -3859,7 +4002,8 @@ async function currentReadyAuthority(
|
|
|
3859
4002
|
return {
|
|
3860
4003
|
endpoint: endpoint as Record<string, unknown>,
|
|
3861
4004
|
endpointSource,
|
|
3862
|
-
endpointMtimeMs:
|
|
4005
|
+
endpointMtimeMs: endpointFile.mtimeMs,
|
|
4006
|
+
...(record.endpointFileId === undefined ? {} : { endpointFileId: record.endpointFileId }),
|
|
3863
4007
|
endpointGeneration: record.endpointGeneration,
|
|
3864
4008
|
};
|
|
3865
4009
|
} catch {
|
|
@@ -3870,7 +4014,8 @@ async function currentReadyAuthority(
|
|
|
3870
4014
|
function sameReadyAuthority(left: ReadyAuthority, right: ReadyAuthority): boolean {
|
|
3871
4015
|
return (
|
|
3872
4016
|
left.endpointSource === right.endpointSource &&
|
|
3873
|
-
left.endpointMtimeMs
|
|
4017
|
+
Math.abs(left.endpointMtimeMs - right.endpointMtimeMs) <= 0.001 &&
|
|
4018
|
+
left.endpointFileId === right.endpointFileId &&
|
|
3874
4019
|
left.endpointGeneration === right.endpointGeneration
|
|
3875
4020
|
);
|
|
3876
4021
|
}
|
|
@@ -3996,6 +4141,15 @@ function worktreeIntent(plan: GjcLaunchWorktreePlan | undefined): LifecycleWorkt
|
|
|
3996
4141
|
};
|
|
3997
4142
|
}
|
|
3998
4143
|
|
|
4144
|
+
/** Test seam for the worktree occupancy boundary. */
|
|
4145
|
+
export function worktreeOccupantForTest(
|
|
4146
|
+
sessions: IndexedSession[],
|
|
4147
|
+
worktreePath: string,
|
|
4148
|
+
observe: (pid: number, expectedIncarnation: string | undefined) => ProcessObservation = observeProcess,
|
|
4149
|
+
): string | null {
|
|
4150
|
+
return worktreeOccupant(sessions, worktreePath, observe);
|
|
4151
|
+
}
|
|
4152
|
+
|
|
3999
4153
|
function preparePlannedWorktree(plan: GjcLaunchWorktreePlan): SessionLifecycleWorktreeReceipt {
|
|
4000
4154
|
const prepared = ensureLaunchWorktree(plan);
|
|
4001
4155
|
if (!prepared.enabled || path.resolve(prepared.worktreePath) !== path.resolve(plan.worktreePath))
|
|
@@ -4030,7 +4184,7 @@ async function launchInput(
|
|
|
4030
4184
|
if (input.modelPreset !== undefined && (typeof input.modelPreset !== "string" || input.modelPreset.length === 0))
|
|
4031
4185
|
return fail("invalid_input", "modelPreset must be a non-empty exact profile ID.");
|
|
4032
4186
|
if (modelPreset !== undefined) {
|
|
4033
|
-
const validatedModelPreset = validateBrokerModelPreset(broker.settings.agentDir, modelPreset);
|
|
4187
|
+
const validatedModelPreset = await validateBrokerModelPreset(broker.settings.agentDir, modelPreset);
|
|
4034
4188
|
if (typeof validatedModelPreset !== "string") return validatedModelPreset;
|
|
4035
4189
|
modelPreset = validatedModelPreset;
|
|
4036
4190
|
}
|
|
@@ -4427,7 +4581,7 @@ async function validateDeletePath(
|
|
|
4427
4581
|
broker: Broker,
|
|
4428
4582
|
input: Input,
|
|
4429
4583
|
id: string,
|
|
4430
|
-
record: { locator:
|
|
4584
|
+
record: { locator: SessionLocatorV2 } | undefined,
|
|
4431
4585
|
cleanup?: CleanupEvidence,
|
|
4432
4586
|
): Promise<ValidatedDelete | BrokerResponse> {
|
|
4433
4587
|
const sessionPath = text(input.sessionPath);
|
|
@@ -4441,7 +4595,7 @@ async function validateDeletePath(
|
|
|
4441
4595
|
const canonicalRequestedRoot = canonicalExistingPath(requestedRoot);
|
|
4442
4596
|
if (
|
|
4443
4597
|
record &&
|
|
4444
|
-
(canonicalExistingPath(record.locator.
|
|
4598
|
+
(canonicalExistingPath(record.locator.cwd) !== cwd ||
|
|
4445
4599
|
canonicalExistingPath(record.locator.stateRoot) !== canonicalRequestedRoot)
|
|
4446
4600
|
)
|
|
4447
4601
|
return fail("invalid_input", "session.delete locator does not match the indexed session.");
|
|
@@ -4531,7 +4685,7 @@ async function validateDeletePath(
|
|
|
4531
4685
|
}
|
|
4532
4686
|
type CloseAuthority = { endpointGeneration: number; endpointIncarnation: string };
|
|
4533
4687
|
type CloseRecord = {
|
|
4534
|
-
locator:
|
|
4688
|
+
locator: SessionLocatorV2;
|
|
4535
4689
|
endpointGeneration: number;
|
|
4536
4690
|
pid: number;
|
|
4537
4691
|
endpointMtimeMs?: number;
|
|
@@ -4595,7 +4749,7 @@ function sameCloseStoredProcessIdentity(expected: CloseRecord, current: CloseRec
|
|
|
4595
4749
|
typeof expected.lifecycleRequestId === "string" &&
|
|
4596
4750
|
expected.lifecycleRequestId.length > 0 &&
|
|
4597
4751
|
current.lifecycleRequestId === expected.lifecycleRequestId &&
|
|
4598
|
-
path.resolve(current.locator.
|
|
4752
|
+
path.resolve(current.locator.cwd) === path.resolve(expected.locator.cwd) &&
|
|
4599
4753
|
path.resolve(current.locator.stateRoot) === path.resolve(expected.locator.stateRoot)
|
|
4600
4754
|
);
|
|
4601
4755
|
}
|
|
@@ -4613,7 +4767,7 @@ function sameCloseEndpointIdentity(expected: CloseRecord, current: CloseRecord):
|
|
|
4613
4767
|
current.endpointMtimeMs !== undefined &&
|
|
4614
4768
|
expected.endpointMtimeMs !== undefined &&
|
|
4615
4769
|
current.lifecycleRequestId === expected.lifecycleRequestId &&
|
|
4616
|
-
path.resolve(current.locator.
|
|
4770
|
+
path.resolve(current.locator.cwd) === path.resolve(expected.locator.cwd) &&
|
|
4617
4771
|
path.resolve(current.locator.stateRoot) === path.resolve(expected.locator.stateRoot)
|
|
4618
4772
|
);
|
|
4619
4773
|
}
|
|
@@ -4630,7 +4784,7 @@ function sameCloseGeneration(expected: CloseRecord, current: CloseRecord & { liv
|
|
|
4630
4784
|
current.endpointMtimeMs === expected.endpointMtimeMs &&
|
|
4631
4785
|
current.lifecycleRequestId === expected.lifecycleRequestId &&
|
|
4632
4786
|
current.processIncarnation === expected.processIncarnation &&
|
|
4633
|
-
path.resolve(current.locator.
|
|
4787
|
+
path.resolve(current.locator.cwd) === path.resolve(expected.locator.cwd) &&
|
|
4634
4788
|
path.resolve(current.locator.stateRoot) === path.resolve(expected.locator.stateRoot)
|
|
4635
4789
|
);
|
|
4636
4790
|
}
|
|
@@ -4694,13 +4848,15 @@ async function executeLifecycleResponse(
|
|
|
4694
4848
|
if (operation === "session.create" || operation === "session.fork" || operation === "session.resume") {
|
|
4695
4849
|
await broker.index.refresh();
|
|
4696
4850
|
await broker.heartbeatSessions();
|
|
4851
|
+
await broker.index.refresh();
|
|
4697
4852
|
if (operation === "session.resume") {
|
|
4698
4853
|
const requestedSessionId = sessionId(input);
|
|
4699
|
-
const
|
|
4700
|
-
? broker.index.listSessions().sessions
|
|
4701
|
-
:
|
|
4702
|
-
if (
|
|
4854
|
+
const authority = requestedSessionId
|
|
4855
|
+
? liveResumeAuthority(broker.index.listSessions().sessions, requestedSessionId)
|
|
4856
|
+
: { kind: "none" as const };
|
|
4857
|
+
if (authority.kind === "ambiguous")
|
|
4703
4858
|
return fail("endpoint_stale", "Session authority is ambiguous and cannot be resumed safely.");
|
|
4859
|
+
const existing = authority.kind === "live" ? authority.record : undefined;
|
|
4704
4860
|
if (existing?.live) {
|
|
4705
4861
|
const initialScope = await validateLiveResumeScope(broker, input, requestedSessionId!, existing);
|
|
4706
4862
|
if ("ok" in initialScope) return initialScope;
|
|
@@ -4717,16 +4873,17 @@ async function executeLifecycleResponse(
|
|
|
4717
4873
|
"live_session",
|
|
4718
4874
|
"Session is already live but its incarnation-bound endpoint is unavailable.",
|
|
4719
4875
|
);
|
|
4720
|
-
await broker
|
|
4721
|
-
const current = broker.index
|
|
4722
|
-
.listSessions()
|
|
4723
|
-
.sessions.find(session => session.sessionId === requestedSessionId);
|
|
4724
|
-
if (!current || !sameLiveResumeRecord(existing, current))
|
|
4725
|
-
return fail("endpoint_stale", "Live session changed while its resume authority was being verified.");
|
|
4726
|
-
const finalScope = await validateLiveResumeScope(broker, input, requestedSessionId!, current);
|
|
4876
|
+
const finalScope = await validateLiveResumeScope(broker, input, requestedSessionId!, existing);
|
|
4727
4877
|
if ("ok" in finalScope) return finalScope;
|
|
4728
4878
|
if (!sameResumeSessionIdentity(initialScope, finalScope))
|
|
4729
4879
|
return fail("endpoint_stale", "Saved session changed while its resume authority was being verified.");
|
|
4880
|
+
await broker.index.refresh();
|
|
4881
|
+
const finalAuthority = liveResumeAuthority(broker.index.listSessions().sessions, requestedSessionId!);
|
|
4882
|
+
if (finalAuthority.kind === "ambiguous")
|
|
4883
|
+
return fail("endpoint_stale", "Session authority became ambiguous while it was being verified.");
|
|
4884
|
+
const current = finalAuthority.kind === "live" ? finalAuthority.record : undefined;
|
|
4885
|
+
if (!current || !sameLiveResumeRecord(existing, current))
|
|
4886
|
+
return fail("endpoint_stale", "Live session changed while its resume authority was being verified.");
|
|
4730
4887
|
if (current.endpointMtimeMs === undefined)
|
|
4731
4888
|
return fail("endpoint_stale", "Live session endpoint authority is incomplete.");
|
|
4732
4889
|
return {
|
|
@@ -4801,6 +4958,14 @@ async function executeLifecycleResponse(
|
|
|
4801
4958
|
"incarnation_unavailable",
|
|
4802
4959
|
"OS process incarnation authority is unavailable; refusing to spawn a lifecycle session.",
|
|
4803
4960
|
);
|
|
4961
|
+
if (launch.worktreePlan) {
|
|
4962
|
+
const occupant = worktreeOccupant(broker.index.listSessions().sessions, launch.worktreePlan.worktreePath);
|
|
4963
|
+
if (occupant && occupant !== launch.id)
|
|
4964
|
+
return fail(
|
|
4965
|
+
"worktree_in_use",
|
|
4966
|
+
`The requested worktree is already held by session ${occupant}. Choose another worktree name or stop that session.`,
|
|
4967
|
+
);
|
|
4968
|
+
}
|
|
4804
4969
|
const effectMarker = randomUUID();
|
|
4805
4970
|
const plannedWorktreeIntent = worktreeIntent(launch.worktreePlan);
|
|
4806
4971
|
const effectIntent: LifecycleEffectIntentWithDeadline = {
|
|
@@ -4907,7 +5072,13 @@ async function executeLifecycleResponse(
|
|
|
4907
5072
|
// by the OS rather than captured (#4712 review).
|
|
4908
5073
|
stdio: "ignore",
|
|
4909
5074
|
env: {
|
|
4910
|
-
|
|
5075
|
+
// Master capability is process-local to direct Bash children and must
|
|
5076
|
+
// never cross a broker lifecycle launch boundary.
|
|
5077
|
+
...Object.fromEntries(
|
|
5078
|
+
Object.entries("kind" in cmd ? cmd.env : process.env).filter(
|
|
5079
|
+
([key]) => key !== "GJC_MASTER_CAPABILITY",
|
|
5080
|
+
),
|
|
5081
|
+
),
|
|
4911
5082
|
GJC_AGENT_DIR: broker.settings.agentDir,
|
|
4912
5083
|
GJC_CODING_AGENT_DIR: broker.settings.agentDir,
|
|
4913
5084
|
GJC_SESSION_ID: launch.id,
|
|
@@ -5047,7 +5218,7 @@ async function executeLifecycleResponse(
|
|
|
5047
5218
|
`Session ${launch.id} did not register an endpoint before the readiness timeout.`,
|
|
5048
5219
|
);
|
|
5049
5220
|
}
|
|
5050
|
-
await reconcileReadyScope(broker, launch.id, launch.cwd);
|
|
5221
|
+
await reconcileReadyScope(broker, launch.id, launch.cwd, launch.root, spawnedAuthority);
|
|
5051
5222
|
const verified = await currentReadyAuthority(broker, launch.id, launch.root, spawnedAuthority);
|
|
5052
5223
|
if (!verified || !sameReadyAuthority(readiness.authority, verified)) {
|
|
5053
5224
|
const exited =
|