@gajae-code/coding-agent 0.15.6 → 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 +59 -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 +13 -0
- package/dist/types/cli.d.ts +7 -0
- package/dist/types/commands/launch.d.ts +10 -0
- package/dist/types/config/autorouting-tier-map.d.ts +24 -0
- package/dist/types/config/file-lock.d.ts +5 -0
- package/dist/types/config/model-registry.d.ts +6 -0
- package/dist/types/config/settings-schema.d.ts +10 -1
- 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/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-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/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/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/print-mode.d.ts +17 -0
- package/dist/types/modes/ui-language.d.ts +7 -1
- package/dist/types/sdk/broker/broker.d.ts +77 -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/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/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/session-runtime.d.ts +52 -0
- 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/router/session-router.d.ts +2 -0
- package/dist/types/sdk/session.d.ts +16 -0
- package/dist/types/sdk/startup-capability.d.ts +2 -1
- package/dist/types/session/agent-session.d.ts +5 -0
- package/dist/types/session/credential-store-errors.d.ts +1 -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/tools/bash.d.ts +4 -0
- package/dist/types/tools/gh.d.ts +8 -0
- package/dist/types/tools/index.d.ts +4 -0
- package/package.json +7 -7
- package/scripts/generate-sdk-operation-inventory.ts +6 -0
- package/scripts/verify-gjc-sdk-canonicalization.ts +35 -1
- 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/root-flags.ts +5 -0
- package/src/cli/update-cli.ts +77 -11
- package/src/commands/launch.ts +86 -18
- package/src/commands/sdk.ts +113 -21
- package/src/config/autorouting-tier-map.ts +12 -0
- package/src/config/file-lock.ts +158 -83
- package/src/config/model-preset-registry.ts +1 -5
- package/src/config/model-profiles.ts +5 -5
- package/src/config/model-registry.ts +23 -0
- package/src/config/model-resolver.ts +5 -0
- package/src/config/settings-schema.ts +8 -1
- package/src/config/settings.ts +5 -0
- package/src/coordinator-mcp/projection-scan.ts +21 -0
- package/src/coordinator-mcp/question-state.ts +9 -11
- package/src/coordinator-mcp/server.ts +94 -20
- package/src/crash/index-store.ts +2 -0
- package/src/crash/record-loader.ts +10 -0
- 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 +79 -6
- package/src/gjc-runtime/session-state-sidecar.ts +929 -23
- 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/internal-urls/docs-index.generated.ts +7 -7
- package/src/internal-urls/issue-pr-protocol.ts +4 -2
- package/src/main.ts +57 -2
- 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 +605 -156
- package/src/modes/components/footer.ts +4 -2
- package/src/modes/components/frictionless-onboarding-selector.ts +48 -2
- 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 +23 -9
- package/src/modes/components/tool-status-header.ts +151 -15
- package/src/modes/controllers/input-controller.ts +28 -0
- package/src/modes/controllers/selector-controller.ts +191 -96
- package/src/modes/print-mode.ts +64 -0
- package/src/modes/ui-language.ts +49 -2
- package/src/prompts/system/master-mode.md +28 -0
- package/src/sdk/broker/broker.ts +1636 -26
- 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 +186 -41
- 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 +35 -4
- package/src/sdk/bus/index.ts +51 -4
- 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 +264 -5
- package/src/sdk/client/client.ts +14 -8
- package/src/sdk/host/control/dispatch.ts +99 -38
- 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/session-runtime.test.ts +210 -6
- package/src/sdk/host/session-runtime.ts +291 -49
- 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/protocol/operation-inventory.generated.json +51 -0
- package/src/sdk/protocol/operation-registry.ts +13 -3
- package/src/sdk/router/session-router.ts +67 -59
- package/src/sdk/session.ts +357 -155
- package/src/sdk/startup-capability.ts +1 -0
- package/src/session/agent-session.ts +640 -103
- package/src/session/blob-store.ts +15 -5
- package/src/session/credential-store-errors.ts +2 -0
- package/src/session/internal/managed-session-storage.ts +153 -78
- package/src/session/session-manager.ts +337 -24
- package/src/session/yield-queue.ts +24 -10
- package/src/setup/frictionless-onboarding.ts +2 -0
- package/src/telemetry/events.ts +1138 -12
- package/src/tools/bash.ts +256 -42
- package/src/tools/gh.ts +104 -48
- package/src/tools/github-cache.ts +2 -2
- package/src/tools/index.ts +4 -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";
|
|
@@ -66,6 +66,7 @@ import {
|
|
|
66
66
|
sanitizeSdkStartupMessage,
|
|
67
67
|
} from "../startup-capability";
|
|
68
68
|
import type { Broker, BrokerCleanupEvidence, BrokerCleanupIdentity, BrokerResponse } from "./broker";
|
|
69
|
+
import { matchesIndexedEndpointFile, readEndpointFile } from "./endpoint-authority";
|
|
69
70
|
import { decodeLifecycleUtf8, parseLifecycleJson } from "./lifecycle-codec";
|
|
70
71
|
import type {
|
|
71
72
|
LifecycleCleanupProof,
|
|
@@ -81,7 +82,12 @@ import {
|
|
|
81
82
|
processIncarnation,
|
|
82
83
|
} from "./process-incarnation";
|
|
83
84
|
import { resolveSdkInternalSpawnCommand, type SdkInternalSpawnCommand } from "./runtime";
|
|
84
|
-
import {
|
|
85
|
+
import {
|
|
86
|
+
type IndexedSession,
|
|
87
|
+
isSessionAuthorityEligible,
|
|
88
|
+
resolveSessionLocator,
|
|
89
|
+
type SessionLocatorV2,
|
|
90
|
+
} from "./session-index";
|
|
85
91
|
import {
|
|
86
92
|
cancellableSleep,
|
|
87
93
|
DEFAULT_READINESS_TIMEOUT_MS,
|
|
@@ -89,6 +95,7 @@ import {
|
|
|
89
95
|
READINESS_TIMEOUT_INVALID_MESSAGE,
|
|
90
96
|
startupQueueWaitMs,
|
|
91
97
|
} from "./startup-budget";
|
|
98
|
+
import { worktreeOccupant } from "./worktree-occupancy";
|
|
92
99
|
|
|
93
100
|
export {
|
|
94
101
|
type ProcessIncarnationCommandRunner,
|
|
@@ -612,7 +619,7 @@ function text(value: unknown): string | undefined {
|
|
|
612
619
|
return typeof value === "string" && value ? value : undefined;
|
|
613
620
|
}
|
|
614
621
|
|
|
615
|
-
function validateBrokerModelPresetSync(agentDir: string, requestedProfile: string): string | BrokerResponse {
|
|
622
|
+
export function validateBrokerModelPresetSync(agentDir: string, requestedProfile: string): string | BrokerResponse {
|
|
616
623
|
const modelsConfigFile = ModelsConfigFile.relocate(path.join(agentDir, "models.yml"));
|
|
617
624
|
modelsConfigFile.invalidate();
|
|
618
625
|
const loaded = modelsConfigFile.tryLoad();
|
|
@@ -733,10 +740,13 @@ function lifecycleWorktreeTarget(input: Input): SessionLifecycleWorktreeTarget |
|
|
|
733
740
|
}
|
|
734
741
|
|
|
735
742
|
type LiveResumeRecord = {
|
|
736
|
-
|
|
743
|
+
sessionId: string;
|
|
744
|
+
locator: SessionLocatorV2;
|
|
737
745
|
endpointGeneration: number;
|
|
738
746
|
pid: number;
|
|
739
747
|
endpointMtimeMs?: number;
|
|
748
|
+
processIncarnation?: string;
|
|
749
|
+
hostIncarnation?: string;
|
|
740
750
|
live: boolean;
|
|
741
751
|
ambiguous: boolean;
|
|
742
752
|
};
|
|
@@ -755,7 +765,7 @@ type ResumeScope = {
|
|
|
755
765
|
};
|
|
756
766
|
function sameResumeLocator(record: LiveResumeRecord, cwd: string, root: string): boolean {
|
|
757
767
|
return (
|
|
758
|
-
resolveEquivalentPath(record.locator.
|
|
768
|
+
resolveEquivalentPath(record.locator.cwd) === resolveEquivalentPath(cwd) &&
|
|
759
769
|
resolveEquivalentPath(record.locator.stateRoot) === resolveEquivalentPath(root)
|
|
760
770
|
);
|
|
761
771
|
}
|
|
@@ -777,10 +787,24 @@ function sameLiveResumeRecord(expected: LiveResumeRecord, current: LiveResumeRec
|
|
|
777
787
|
current.endpointGeneration === expected.endpointGeneration &&
|
|
778
788
|
current.pid === expected.pid &&
|
|
779
789
|
current.endpointMtimeMs === expected.endpointMtimeMs &&
|
|
780
|
-
|
|
790
|
+
(current.hostIncarnation ?? current.processIncarnation) ===
|
|
791
|
+
(expected.hostIncarnation ?? expected.processIncarnation) &&
|
|
792
|
+
sameResumeLocator(current, expected.locator.cwd, expected.locator.stateRoot)
|
|
781
793
|
);
|
|
782
794
|
}
|
|
783
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
|
+
|
|
784
808
|
type ValidatedTranscript = {
|
|
785
809
|
path: string;
|
|
786
810
|
id: string;
|
|
@@ -912,18 +936,36 @@ async function validateLiveResumeScope(
|
|
|
912
936
|
sessionIdentity: session.identity,
|
|
913
937
|
};
|
|
914
938
|
}
|
|
915
|
-
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> {
|
|
916
946
|
if (!scope) return;
|
|
917
947
|
await broker.index.refresh();
|
|
918
|
-
const record = broker.index
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
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);
|
|
923
965
|
await broker.index.append({
|
|
924
966
|
type: "record_reconciled",
|
|
925
967
|
sessionId: id,
|
|
926
|
-
locator
|
|
968
|
+
locator,
|
|
927
969
|
endpointGeneration: record.endpointGeneration,
|
|
928
970
|
pid: record.pid,
|
|
929
971
|
// Reconciliation only re-scopes the locator, so every identity fact the host
|
|
@@ -933,6 +975,8 @@ async function reconcileReadyScope(broker: Broker, id: string, scope: string | u
|
|
|
933
975
|
...(record.hostIncarnation === undefined ? {} : { hostIncarnation: record.hostIncarnation }),
|
|
934
976
|
...(record.lifecycleRequestId === undefined ? {} : { lifecycleRequestId: record.lifecycleRequestId }),
|
|
935
977
|
endpointMtimeMs: record.endpointMtimeMs,
|
|
978
|
+
...(record.endpointFileId === undefined ? {} : { endpointFileId: record.endpointFileId }),
|
|
979
|
+
...(record.masterRole === undefined ? {} : { masterRole: record.masterRole }),
|
|
936
980
|
});
|
|
937
981
|
}
|
|
938
982
|
|
|
@@ -964,6 +1008,66 @@ function command(broker: Broker): LifecycleCommand {
|
|
|
964
1008
|
return lifecycleCommandResolversForTest.get(broker)?.() ?? resolveSdkInternalSpawnCommand("session-host-internal");
|
|
965
1009
|
}
|
|
966
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
|
+
|
|
967
1071
|
const lifecycleMarkerPath = (root: string, id: string) => path.join(root, "sdk", `${id}.lifecycle.json`);
|
|
968
1072
|
const lifecycleReadyPath = (root: string, id: string) => path.join(root, "sdk", `${id}.lifecycle.ready.json`);
|
|
969
1073
|
const lifecycleFailurePath = (root: string, id: string, effectMarker: string) =>
|
|
@@ -973,6 +1077,7 @@ type ReadyAuthority = {
|
|
|
973
1077
|
endpoint: Record<string, unknown>;
|
|
974
1078
|
endpointSource: string;
|
|
975
1079
|
endpointMtimeMs: number;
|
|
1080
|
+
endpointFileId?: string;
|
|
976
1081
|
endpointGeneration: number;
|
|
977
1082
|
};
|
|
978
1083
|
type ReadinessResult =
|
|
@@ -3351,7 +3456,7 @@ async function recordTerminalUncertain(
|
|
|
3351
3456
|
await broker.index.append({
|
|
3352
3457
|
type: "lifecycle_terminal",
|
|
3353
3458
|
sessionId: id,
|
|
3354
|
-
locator: {
|
|
3459
|
+
locator: { cwd: "unknown", worktreeRoot: null, stateRoot: root },
|
|
3355
3460
|
endpointGeneration: 0,
|
|
3356
3461
|
pid,
|
|
3357
3462
|
terminalUncertain: true,
|
|
@@ -3667,7 +3772,10 @@ async function removeExactDeadSessionEndpoint(
|
|
|
3667
3772
|
: finalEndpointPath;
|
|
3668
3773
|
let handle: fs.FileHandle | undefined;
|
|
3669
3774
|
try {
|
|
3670
|
-
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
|
+
);
|
|
3671
3779
|
const metadata = await handle.stat({ bigint: true });
|
|
3672
3780
|
if (!metadata.isFile() || metadata.nlink !== 1n || metadata.size > 4096n) return false;
|
|
3673
3781
|
const bytes = Buffer.alloc(Number(metadata.size) + 1);
|
|
@@ -3847,10 +3955,9 @@ async function currentReadyAuthority(
|
|
|
3847
3955
|
if (!(await hasOwnedReadinessEvidence(broker, root, id, expected))) return undefined;
|
|
3848
3956
|
const endpointPath = path.join(root, "sdk", `${id}.json`);
|
|
3849
3957
|
try {
|
|
3850
|
-
const
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
]);
|
|
3958
|
+
const endpointFile = await readEndpointFile(endpointPath);
|
|
3959
|
+
if (!endpointFile) return undefined;
|
|
3960
|
+
const endpointSource = endpointFile.source;
|
|
3854
3961
|
const endpoint = JSON.parse(endpointSource) as {
|
|
3855
3962
|
sessionId?: unknown;
|
|
3856
3963
|
url?: unknown;
|
|
@@ -3863,14 +3970,24 @@ async function currentReadyAuthority(
|
|
|
3863
3970
|
// native-dead child; do not refuse a native-alive ready host for a stale live bit.
|
|
3864
3971
|
await broker.heartbeatSessions();
|
|
3865
3972
|
await broker.index.refresh();
|
|
3866
|
-
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
|
+
);
|
|
3867
3983
|
if (
|
|
3868
3984
|
!record ||
|
|
3869
3985
|
record.terminal ||
|
|
3870
3986
|
record.terminalUncertain ||
|
|
3871
3987
|
record.pid !== expected.pid ||
|
|
3872
3988
|
resolveEquivalentPath(record.locator.stateRoot) !== resolveEquivalentPath(root) ||
|
|
3873
|
-
record.
|
|
3989
|
+
(record.hostIncarnation ?? record.processIncarnation) !== expected.incarnation ||
|
|
3990
|
+
!matchesIndexedEndpointFile(endpointFile, record) ||
|
|
3874
3991
|
endpoint.pid !== expected.pid ||
|
|
3875
3992
|
endpoint.sessionId !== id ||
|
|
3876
3993
|
typeof endpoint.url !== "string" ||
|
|
@@ -3885,7 +4002,8 @@ async function currentReadyAuthority(
|
|
|
3885
4002
|
return {
|
|
3886
4003
|
endpoint: endpoint as Record<string, unknown>,
|
|
3887
4004
|
endpointSource,
|
|
3888
|
-
endpointMtimeMs:
|
|
4005
|
+
endpointMtimeMs: endpointFile.mtimeMs,
|
|
4006
|
+
...(record.endpointFileId === undefined ? {} : { endpointFileId: record.endpointFileId }),
|
|
3889
4007
|
endpointGeneration: record.endpointGeneration,
|
|
3890
4008
|
};
|
|
3891
4009
|
} catch {
|
|
@@ -3896,7 +4014,8 @@ async function currentReadyAuthority(
|
|
|
3896
4014
|
function sameReadyAuthority(left: ReadyAuthority, right: ReadyAuthority): boolean {
|
|
3897
4015
|
return (
|
|
3898
4016
|
left.endpointSource === right.endpointSource &&
|
|
3899
|
-
left.endpointMtimeMs
|
|
4017
|
+
Math.abs(left.endpointMtimeMs - right.endpointMtimeMs) <= 0.001 &&
|
|
4018
|
+
left.endpointFileId === right.endpointFileId &&
|
|
3900
4019
|
left.endpointGeneration === right.endpointGeneration
|
|
3901
4020
|
);
|
|
3902
4021
|
}
|
|
@@ -4022,6 +4141,15 @@ function worktreeIntent(plan: GjcLaunchWorktreePlan | undefined): LifecycleWorkt
|
|
|
4022
4141
|
};
|
|
4023
4142
|
}
|
|
4024
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
|
+
|
|
4025
4153
|
function preparePlannedWorktree(plan: GjcLaunchWorktreePlan): SessionLifecycleWorktreeReceipt {
|
|
4026
4154
|
const prepared = ensureLaunchWorktree(plan);
|
|
4027
4155
|
if (!prepared.enabled || path.resolve(prepared.worktreePath) !== path.resolve(plan.worktreePath))
|
|
@@ -4453,7 +4581,7 @@ async function validateDeletePath(
|
|
|
4453
4581
|
broker: Broker,
|
|
4454
4582
|
input: Input,
|
|
4455
4583
|
id: string,
|
|
4456
|
-
record: { locator:
|
|
4584
|
+
record: { locator: SessionLocatorV2 } | undefined,
|
|
4457
4585
|
cleanup?: CleanupEvidence,
|
|
4458
4586
|
): Promise<ValidatedDelete | BrokerResponse> {
|
|
4459
4587
|
const sessionPath = text(input.sessionPath);
|
|
@@ -4467,7 +4595,7 @@ async function validateDeletePath(
|
|
|
4467
4595
|
const canonicalRequestedRoot = canonicalExistingPath(requestedRoot);
|
|
4468
4596
|
if (
|
|
4469
4597
|
record &&
|
|
4470
|
-
(canonicalExistingPath(record.locator.
|
|
4598
|
+
(canonicalExistingPath(record.locator.cwd) !== cwd ||
|
|
4471
4599
|
canonicalExistingPath(record.locator.stateRoot) !== canonicalRequestedRoot)
|
|
4472
4600
|
)
|
|
4473
4601
|
return fail("invalid_input", "session.delete locator does not match the indexed session.");
|
|
@@ -4557,7 +4685,7 @@ async function validateDeletePath(
|
|
|
4557
4685
|
}
|
|
4558
4686
|
type CloseAuthority = { endpointGeneration: number; endpointIncarnation: string };
|
|
4559
4687
|
type CloseRecord = {
|
|
4560
|
-
locator:
|
|
4688
|
+
locator: SessionLocatorV2;
|
|
4561
4689
|
endpointGeneration: number;
|
|
4562
4690
|
pid: number;
|
|
4563
4691
|
endpointMtimeMs?: number;
|
|
@@ -4621,7 +4749,7 @@ function sameCloseStoredProcessIdentity(expected: CloseRecord, current: CloseRec
|
|
|
4621
4749
|
typeof expected.lifecycleRequestId === "string" &&
|
|
4622
4750
|
expected.lifecycleRequestId.length > 0 &&
|
|
4623
4751
|
current.lifecycleRequestId === expected.lifecycleRequestId &&
|
|
4624
|
-
path.resolve(current.locator.
|
|
4752
|
+
path.resolve(current.locator.cwd) === path.resolve(expected.locator.cwd) &&
|
|
4625
4753
|
path.resolve(current.locator.stateRoot) === path.resolve(expected.locator.stateRoot)
|
|
4626
4754
|
);
|
|
4627
4755
|
}
|
|
@@ -4639,7 +4767,7 @@ function sameCloseEndpointIdentity(expected: CloseRecord, current: CloseRecord):
|
|
|
4639
4767
|
current.endpointMtimeMs !== undefined &&
|
|
4640
4768
|
expected.endpointMtimeMs !== undefined &&
|
|
4641
4769
|
current.lifecycleRequestId === expected.lifecycleRequestId &&
|
|
4642
|
-
path.resolve(current.locator.
|
|
4770
|
+
path.resolve(current.locator.cwd) === path.resolve(expected.locator.cwd) &&
|
|
4643
4771
|
path.resolve(current.locator.stateRoot) === path.resolve(expected.locator.stateRoot)
|
|
4644
4772
|
);
|
|
4645
4773
|
}
|
|
@@ -4656,7 +4784,7 @@ function sameCloseGeneration(expected: CloseRecord, current: CloseRecord & { liv
|
|
|
4656
4784
|
current.endpointMtimeMs === expected.endpointMtimeMs &&
|
|
4657
4785
|
current.lifecycleRequestId === expected.lifecycleRequestId &&
|
|
4658
4786
|
current.processIncarnation === expected.processIncarnation &&
|
|
4659
|
-
path.resolve(current.locator.
|
|
4787
|
+
path.resolve(current.locator.cwd) === path.resolve(expected.locator.cwd) &&
|
|
4660
4788
|
path.resolve(current.locator.stateRoot) === path.resolve(expected.locator.stateRoot)
|
|
4661
4789
|
);
|
|
4662
4790
|
}
|
|
@@ -4720,13 +4848,15 @@ async function executeLifecycleResponse(
|
|
|
4720
4848
|
if (operation === "session.create" || operation === "session.fork" || operation === "session.resume") {
|
|
4721
4849
|
await broker.index.refresh();
|
|
4722
4850
|
await broker.heartbeatSessions();
|
|
4851
|
+
await broker.index.refresh();
|
|
4723
4852
|
if (operation === "session.resume") {
|
|
4724
4853
|
const requestedSessionId = sessionId(input);
|
|
4725
|
-
const
|
|
4726
|
-
? broker.index.listSessions().sessions
|
|
4727
|
-
:
|
|
4728
|
-
if (
|
|
4854
|
+
const authority = requestedSessionId
|
|
4855
|
+
? liveResumeAuthority(broker.index.listSessions().sessions, requestedSessionId)
|
|
4856
|
+
: { kind: "none" as const };
|
|
4857
|
+
if (authority.kind === "ambiguous")
|
|
4729
4858
|
return fail("endpoint_stale", "Session authority is ambiguous and cannot be resumed safely.");
|
|
4859
|
+
const existing = authority.kind === "live" ? authority.record : undefined;
|
|
4730
4860
|
if (existing?.live) {
|
|
4731
4861
|
const initialScope = await validateLiveResumeScope(broker, input, requestedSessionId!, existing);
|
|
4732
4862
|
if ("ok" in initialScope) return initialScope;
|
|
@@ -4743,16 +4873,17 @@ async function executeLifecycleResponse(
|
|
|
4743
4873
|
"live_session",
|
|
4744
4874
|
"Session is already live but its incarnation-bound endpoint is unavailable.",
|
|
4745
4875
|
);
|
|
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);
|
|
4876
|
+
const finalScope = await validateLiveResumeScope(broker, input, requestedSessionId!, existing);
|
|
4753
4877
|
if ("ok" in finalScope) return finalScope;
|
|
4754
4878
|
if (!sameResumeSessionIdentity(initialScope, finalScope))
|
|
4755
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.");
|
|
4756
4887
|
if (current.endpointMtimeMs === undefined)
|
|
4757
4888
|
return fail("endpoint_stale", "Live session endpoint authority is incomplete.");
|
|
4758
4889
|
return {
|
|
@@ -4827,6 +4958,14 @@ async function executeLifecycleResponse(
|
|
|
4827
4958
|
"incarnation_unavailable",
|
|
4828
4959
|
"OS process incarnation authority is unavailable; refusing to spawn a lifecycle session.",
|
|
4829
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
|
+
}
|
|
4830
4969
|
const effectMarker = randomUUID();
|
|
4831
4970
|
const plannedWorktreeIntent = worktreeIntent(launch.worktreePlan);
|
|
4832
4971
|
const effectIntent: LifecycleEffectIntentWithDeadline = {
|
|
@@ -4933,7 +5072,13 @@ async function executeLifecycleResponse(
|
|
|
4933
5072
|
// by the OS rather than captured (#4712 review).
|
|
4934
5073
|
stdio: "ignore",
|
|
4935
5074
|
env: {
|
|
4936
|
-
|
|
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
|
+
),
|
|
4937
5082
|
GJC_AGENT_DIR: broker.settings.agentDir,
|
|
4938
5083
|
GJC_CODING_AGENT_DIR: broker.settings.agentDir,
|
|
4939
5084
|
GJC_SESSION_ID: launch.id,
|
|
@@ -5073,7 +5218,7 @@ async function executeLifecycleResponse(
|
|
|
5073
5218
|
`Session ${launch.id} did not register an endpoint before the readiness timeout.`,
|
|
5074
5219
|
);
|
|
5075
5220
|
}
|
|
5076
|
-
await reconcileReadyScope(broker, launch.id, launch.cwd);
|
|
5221
|
+
await reconcileReadyScope(broker, launch.id, launch.cwd, launch.root, spawnedAuthority);
|
|
5077
5222
|
const verified = await currentReadyAuthority(broker, launch.id, launch.root, spawnedAuthority);
|
|
5078
5223
|
if (!verified || !sameReadyAuthority(readiness.authority, verified)) {
|
|
5079
5224
|
const exited =
|