@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,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which session currently holds a GJC-managed worktree.
|
|
3
|
+
*
|
|
4
|
+
* Both launch paths need this answer and must not disagree about it: the broker
|
|
5
|
+
* refuses a lifecycle launch into an occupied worktree, and the interactive
|
|
6
|
+
* `gjc --worktree` launch refuses for the same reason. Keeping one predicate —
|
|
7
|
+
* and one definition of "still running" — here prevents the two from drifting
|
|
8
|
+
* into different notions of liveness.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type * as NativeBindings from "@gajae-code/natives";
|
|
12
|
+
import { resolveEquivalentPath } from "@gajae-code/utils";
|
|
13
|
+
import { probeLinuxProcPidSync } from "../../gjc-runtime/linux-proc";
|
|
14
|
+
import { processIncarnation } from "./process-incarnation";
|
|
15
|
+
import type { IndexedSession } from "./session-index";
|
|
16
|
+
|
|
17
|
+
function nativeLifecycle(): typeof NativeBindings {
|
|
18
|
+
return require("@gajae-code/natives") as typeof NativeBindings;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type ProcessObservation = "alive" | "exited" | "uncertain";
|
|
22
|
+
|
|
23
|
+
/** Only ESRCH or a changed, readable incarnation proves the owned process exited. */
|
|
24
|
+
export function observeProcess(
|
|
25
|
+
pid: number,
|
|
26
|
+
expectedIncarnation: string | undefined,
|
|
27
|
+
readIncarnation: (pid: number) => string | undefined = processIncarnation,
|
|
28
|
+
): ProcessObservation {
|
|
29
|
+
if (process.platform === "win32") {
|
|
30
|
+
try {
|
|
31
|
+
const reference = nativeLifecycle().Process.fromPid(pid);
|
|
32
|
+
if (reference?.status() !== "running") return "exited";
|
|
33
|
+
} catch {
|
|
34
|
+
return "uncertain";
|
|
35
|
+
}
|
|
36
|
+
if (!expectedIncarnation) return "uncertain";
|
|
37
|
+
const actualIncarnation = readIncarnation(pid);
|
|
38
|
+
if (!actualIncarnation) return "uncertain";
|
|
39
|
+
return actualIncarnation === expectedIncarnation ? "alive" : "exited";
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
process.kill(pid, 0);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
return (error as NodeJS.ErrnoException).code === "ESRCH" ? "exited" : "uncertain";
|
|
45
|
+
}
|
|
46
|
+
if (process.platform === "linux") {
|
|
47
|
+
const probe = probeLinuxProcPidSync(pid);
|
|
48
|
+
if (probe.kind === "live" && probe.state === "Z" && expectedIncarnation === `linux:${probe.startTime}`)
|
|
49
|
+
return "exited";
|
|
50
|
+
}
|
|
51
|
+
if (!expectedIncarnation) return "uncertain";
|
|
52
|
+
const actualIncarnation = readIncarnation(pid);
|
|
53
|
+
if (!actualIncarnation) return "uncertain";
|
|
54
|
+
return actualIncarnation === expectedIncarnation ? "alive" : "exited";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The id of a session still occupying `worktreePath`, or null when it is free.
|
|
59
|
+
*
|
|
60
|
+
* `ensureLaunchWorktree` reuses an existing worktree without asking whether
|
|
61
|
+
* anyone is in it, and an unnamed launch derives its directory deterministically
|
|
62
|
+
* from the repository's current branch. Two concurrent sessions in one
|
|
63
|
+
* repository therefore land in the same checkout and overwrite each other's
|
|
64
|
+
* files with no error.
|
|
65
|
+
*
|
|
66
|
+
* Only a process observed as definitively exited releases the worktree.
|
|
67
|
+
* `uncertain` counts as occupied: refusing a launch is recoverable by picking
|
|
68
|
+
* another worktree name, whereas two live sessions sharing a checkout corrupts
|
|
69
|
+
* work already done.
|
|
70
|
+
*/
|
|
71
|
+
export function worktreeOccupant(
|
|
72
|
+
sessions: readonly IndexedSession[],
|
|
73
|
+
worktreePath: string,
|
|
74
|
+
observe: (pid: number, expectedIncarnation: string | undefined) => ProcessObservation = observeProcess,
|
|
75
|
+
): string | null {
|
|
76
|
+
// Session locators retain the canonical Git worktree root separately from the
|
|
77
|
+
// session cwd. A host may start below that root, and a launch plan may arrive
|
|
78
|
+
// through a symlink, so compare the root identity rather than the cwd spelling.
|
|
79
|
+
const target = resolveEquivalentPath(worktreePath);
|
|
80
|
+
for (const session of sessions) {
|
|
81
|
+
const sessionWorktreeRoot = session.locator.worktreeRoot;
|
|
82
|
+
if (session.terminal || typeof sessionWorktreeRoot !== "string" || sessionWorktreeRoot.length === 0) continue;
|
|
83
|
+
if (resolveEquivalentPath(sessionWorktreeRoot) !== target) continue;
|
|
84
|
+
// `live` is heartbeat-derived and can be stale. Only positive process-exit
|
|
85
|
+
// evidence releases a matching retained session's worktree.
|
|
86
|
+
if (observe(session.pid, session.hostIncarnation ?? session.processIncarnation) === "exited") continue;
|
|
87
|
+
return session.sessionId;
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
@@ -130,10 +130,13 @@ export type ChatDaemonAction = "stop" | "reload";
|
|
|
130
130
|
* session-index lock every 2s; staleness retirement moved to a 30s sweep and
|
|
131
131
|
* lease heartbeats no longer force an authority reconcile. A pre-upgrade owner
|
|
132
132
|
* would retain the old hot polling loop, so replacement is required.
|
|
133
|
+
* Discord generation 72 / Slack generation 75 serialize same-generation
|
|
134
|
+
* successor attachment admission behind predecessor provider retirement, so a
|
|
135
|
+
* pre-upgrade owner cannot retain the queue race fixed by #5120.
|
|
133
136
|
*/
|
|
134
137
|
export const CHAT_DAEMON_GENERATIONS: Readonly<Record<ChatDaemonKind, number>> = {
|
|
135
|
-
discord:
|
|
136
|
-
slack:
|
|
138
|
+
discord: 72,
|
|
139
|
+
slack: 75,
|
|
137
140
|
};
|
|
138
141
|
|
|
139
142
|
export function chatDaemonGeneration(kind: ChatDaemonKind): number {
|
|
@@ -258,6 +258,7 @@ export class ChatDaemonRuntime {
|
|
|
258
258
|
readonly #router: SessionRouter;
|
|
259
259
|
readonly #attachments = new Map<string, SessionAttachment>();
|
|
260
260
|
readonly #cleanupWork = new Map<string, Promise<void>>();
|
|
261
|
+
readonly #retirementWork = new Map<string, Promise<void>>();
|
|
261
262
|
readonly #attachmentBarriers = new Map<string, Promise<void>>();
|
|
262
263
|
#discord: DiscordNotificationDaemon | undefined;
|
|
263
264
|
#slack: SlackNotificationDaemon | undefined;
|
|
@@ -281,12 +282,18 @@ export class ChatDaemonRuntime {
|
|
|
281
282
|
}
|
|
282
283
|
|
|
283
284
|
async start(): Promise<void> {
|
|
284
|
-
if (this.#cleanupWork.size > 0) {
|
|
285
|
+
if (this.#cleanupWork.size > 0 || this.#retirementWork.size > 0) {
|
|
286
|
+
const retirements = [...this.#retirementWork.entries()];
|
|
285
287
|
const settled = await Promise.race([
|
|
286
|
-
Promise.
|
|
288
|
+
Promise.all([
|
|
289
|
+
Promise.allSettled([...this.#cleanupWork.values()]),
|
|
290
|
+
Promise.all(retirements.map(([, retirement]) => retirement)),
|
|
291
|
+
]).then(() => true),
|
|
287
292
|
Bun.sleep(5_000).then(() => false),
|
|
288
293
|
]);
|
|
289
294
|
if (!settled) throw new Error("Prior provider cleanup did not settle before chat daemon restart.");
|
|
295
|
+
for (const [sessionId, retirement] of retirements)
|
|
296
|
+
if (this.#retirementWork.get(sessionId) === retirement) this.#retirementWork.delete(sessionId);
|
|
290
297
|
}
|
|
291
298
|
this.#attachments.clear();
|
|
292
299
|
const retainedProvider =
|
|
@@ -428,6 +435,12 @@ export class ChatDaemonRuntime {
|
|
|
428
435
|
const discord = this.#discord;
|
|
429
436
|
const slack = this.#slack;
|
|
430
437
|
const barrier = (async () => {
|
|
438
|
+
const retirement = this.#retirementWork.get(attachment.sessionId);
|
|
439
|
+
if (retirement) {
|
|
440
|
+
await retirement;
|
|
441
|
+
if (this.#retirementWork.get(attachment.sessionId) === retirement)
|
|
442
|
+
this.#retirementWork.delete(attachment.sessionId);
|
|
443
|
+
}
|
|
431
444
|
await this.#cleanupWork.get(attachment.sessionId)?.catch(() => undefined);
|
|
432
445
|
await discord?.recoverCleanup(attachment.sessionId, attachment.generation);
|
|
433
446
|
await slack?.recoverCleanup(attachment.sessionId, attachment.generation);
|
|
@@ -451,13 +464,31 @@ export class ChatDaemonRuntime {
|
|
|
451
464
|
): Promise<void> {
|
|
452
465
|
if (this.#attachments.get(attachment.sessionId) !== attachment) return;
|
|
453
466
|
if (reason === "replaced_same_generation") {
|
|
454
|
-
|
|
455
|
-
|
|
467
|
+
const retirement = this.#trackRetirement(attachment);
|
|
468
|
+
this.#attachments.delete(attachment.sessionId);
|
|
469
|
+
await retirement;
|
|
470
|
+
return;
|
|
456
471
|
}
|
|
457
472
|
this.#attachments.delete(attachment.sessionId);
|
|
458
473
|
if (reason === "removed") await this.#trackCleanup(attachment);
|
|
459
474
|
}
|
|
460
475
|
|
|
476
|
+
#trackRetirement(attachment: SessionAttachment): Promise<void> {
|
|
477
|
+
const sessionId = attachment.sessionId;
|
|
478
|
+
const discord = this.#discord;
|
|
479
|
+
const slack = this.#slack;
|
|
480
|
+
const previous = this.#retirementWork.get(sessionId);
|
|
481
|
+
const work = (previous ?? Promise.resolve()).then(async () => {
|
|
482
|
+
// Capture the predecessor providers before stop() can clear the runtime
|
|
483
|
+
// fields. The successor barrier owns this promise until every retirement
|
|
484
|
+
// waiter settles, even when takeover overlaps worker shutdown.
|
|
485
|
+
await discord?.retireAttachment(sessionId, attachment.generation);
|
|
486
|
+
await slack?.retireAttachment(sessionId, attachment.generation);
|
|
487
|
+
});
|
|
488
|
+
this.#retirementWork.set(sessionId, work);
|
|
489
|
+
return work;
|
|
490
|
+
}
|
|
491
|
+
|
|
461
492
|
async #handleFrame(attachment: SessionAttachment, correlated: CorrelatedFrame): Promise<void> {
|
|
462
493
|
if (this.#attachments.get(attachment.sessionId) !== attachment) return;
|
|
463
494
|
await this.#attachmentBarriers.get(attachment.sessionId);
|
package/src/sdk/bus/index.ts
CHANGED
|
@@ -90,14 +90,17 @@ import { acpFinalTextFromMessage } from "../acp/final-text";
|
|
|
90
90
|
import { ensureBroker } from "../broker/ensure";
|
|
91
91
|
import { publishSessionHostRuntimeEvidence, type SessionHostRuntimePublication } from "../broker/lifecycle";
|
|
92
92
|
import { processIncarnation } from "../broker/process-incarnation";
|
|
93
|
-
import { SessionIndex } from "../broker/session-index";
|
|
93
|
+
import { resolveSessionLocator, SessionIndex } from "../broker/session-index";
|
|
94
94
|
import {
|
|
95
95
|
CAP_GATED_FRAME_KINDS,
|
|
96
96
|
createSdkSurfaceFactory,
|
|
97
|
+
masterAttestationForEffectiveHost,
|
|
98
|
+
reattestMasterSessionIdentity,
|
|
97
99
|
type SessionSdkHost,
|
|
98
100
|
SessionSdkSessionRuntime,
|
|
99
101
|
shouldHostSdk,
|
|
100
102
|
TOOL_ACTIVITY_CAPABILITY,
|
|
103
|
+
verifyMasterCapabilityFrame,
|
|
101
104
|
} from "../host";
|
|
102
105
|
import { type AbortScope, type ControlSurface, dispatchControl, TypedControlError } from "../host/control";
|
|
103
106
|
import { BROKER_RUNTIME_CLOSE_CAPABILITY_FIELD } from "../host/control/runtime-gate";
|
|
@@ -4043,6 +4046,12 @@ export function createNotificationsExtension(
|
|
|
4043
4046
|
controller?: NotificationSessionController;
|
|
4044
4047
|
/** Whether this host mode can own the root SDK endpoint. Default: true. */
|
|
4045
4048
|
sdkHostModeSupported?: boolean;
|
|
4049
|
+
/** In-memory master capability for private broker verification only. */
|
|
4050
|
+
masterCapability?: string;
|
|
4051
|
+
/** Opaque direct-role epoch this effective host may adopt. */
|
|
4052
|
+
masterAttestationEpoch?: string;
|
|
4053
|
+
/** Stable master lineage identity retained across session switches/branches. */
|
|
4054
|
+
masterOwnerSessionId?: string;
|
|
4046
4055
|
onSdkRequest?: (kind: "control" | "query", connectionId: string, frame: Record<string, unknown>) => void;
|
|
4047
4056
|
runBtwTurn?: (question: string, signal: AbortSignal) => Promise<{ replyText: string }>;
|
|
4048
4057
|
/** Observes settlement of optional session-branch startup after reconciliation completes. */
|
|
@@ -4103,6 +4112,7 @@ export function createNotificationsExtension(
|
|
|
4103
4112
|
}
|
|
4104
4113
|
| undefined;
|
|
4105
4114
|
let extensionShuttingDown = false;
|
|
4115
|
+
const consumedMasterNonces = new Map<string, number>();
|
|
4106
4116
|
|
|
4107
4117
|
async function ensureTelegramOwner(settings: Settings): Promise<"ready" | "blocked_identity"> {
|
|
4108
4118
|
if (!telegramTopicsEnabled()) return "blocked_identity";
|
|
@@ -4489,12 +4499,14 @@ export function createNotificationsExtension(
|
|
|
4489
4499
|
|
|
4490
4500
|
async function startSession(ctx: ExtensionContext): Promise<SessionStartResult> {
|
|
4491
4501
|
const id = sessionId(ctx);
|
|
4492
|
-
const lifecycleRequestId = safeLifecycleRequestId(process.env.GJC_LIFECYCLE_REQUEST_ID);
|
|
4493
4502
|
const { settings, cfg, settingsAvailable } = resolveSettings(options.settings);
|
|
4494
4503
|
const notificationsEnabledForSession = controller.query(ctx).genericSessionEnabled;
|
|
4495
4504
|
const sdkEnabledForSession =
|
|
4496
4505
|
(options.sdkHostModeSupported ?? true) && shouldHostSdk(settings, isNotificationEligibleContext(ctx));
|
|
4497
4506
|
const lifecycleRequired = lifecycleStartupCapability !== undefined;
|
|
4507
|
+
const lifecycleRequestId = lifecycleRequired
|
|
4508
|
+
? safeLifecycleRequestId(lifecycleStartupCapability.lifecycleRequestId)
|
|
4509
|
+
: safeLifecycleRequestId(process.env.GJC_LIFECYCLE_REQUEST_ID);
|
|
4498
4510
|
/** The broker-issued readiness intent for this exact lifecycle-managed session. */
|
|
4499
4511
|
const lifecycleReadiness = lifecycleStartupCapability?.readiness ?? "immediate";
|
|
4500
4512
|
const failLifecycleStartup = (
|
|
@@ -6588,6 +6600,7 @@ export function createNotificationsExtension(
|
|
|
6588
6600
|
}
|
|
6589
6601
|
|
|
6590
6602
|
sdkRuntime = new SessionSdkSessionRuntime({
|
|
6603
|
+
masterOwnerSessionId: options.masterOwnerSessionId,
|
|
6591
6604
|
transport: {
|
|
6592
6605
|
sessionId: id,
|
|
6593
6606
|
stateRoot,
|
|
@@ -6607,6 +6620,13 @@ export function createNotificationsExtension(
|
|
|
6607
6620
|
...(activationGate ? { activationGate } : {}),
|
|
6608
6621
|
...(settings ? { settings } : {}),
|
|
6609
6622
|
...(configOverrides ? { configOverrides } : {}),
|
|
6623
|
+
masterCapabilityVerify: frame =>
|
|
6624
|
+
verifyMasterCapabilityFrame({
|
|
6625
|
+
frame,
|
|
6626
|
+
expectedCapability: options.masterCapability,
|
|
6627
|
+
expectedEpoch: options.masterAttestationEpoch,
|
|
6628
|
+
replay: consumedMasterNonces,
|
|
6629
|
+
}),
|
|
6610
6630
|
connectionCapabilities: connectionId => hostCapCache.get(connectionId),
|
|
6611
6631
|
installProviderDefinitions,
|
|
6612
6632
|
onProviderDefinitionsRemoved: removeProviderDefinitions,
|
|
@@ -7751,6 +7771,8 @@ export function createNotificationsExtension(
|
|
|
7751
7771
|
}
|
|
7752
7772
|
const agentDir = lifecycleAgentDir ?? settings?.getAgentDir?.();
|
|
7753
7773
|
if (lifecycleRequired && !agentDir) throw new Error("Lifecycle SDK host requires an agent directory.");
|
|
7774
|
+
if (lifecycleRequired && !lifecycleRequestId)
|
|
7775
|
+
throw new Error("Lifecycle SDK host requires a capability-bound request identity.");
|
|
7754
7776
|
|
|
7755
7777
|
if (agentDir) {
|
|
7756
7778
|
try {
|
|
@@ -7758,13 +7780,36 @@ export function createNotificationsExtension(
|
|
|
7758
7780
|
throwIfLifecycleStopped();
|
|
7759
7781
|
const index = await new SessionIndex(agentDir).open();
|
|
7760
7782
|
throwIfLifecycleStopped();
|
|
7761
|
-
const locator =
|
|
7762
|
-
const
|
|
7783
|
+
const locator = await resolveSessionLocator(ctx.cwd, endpointStateRoot);
|
|
7784
|
+
const endpointPath = path.join(endpointStateRoot, "sdk", `${id}.json`);
|
|
7785
|
+
const endpointMtimeMs = fs.statSync(endpointPath).mtimeMs;
|
|
7786
|
+
const endpointIdentity = fs.statSync(endpointPath, { bigint: true });
|
|
7787
|
+
const endpointFileId = `${endpointIdentity.dev}:${endpointIdentity.ino}`;
|
|
7763
7788
|
const hostProcessIncarnation = processIncarnation(process.pid);
|
|
7789
|
+
const direct = await reattestMasterSessionIdentity({
|
|
7790
|
+
index,
|
|
7791
|
+
locator,
|
|
7792
|
+
masterCapability: options.masterCapability,
|
|
7793
|
+
attestationEpoch: options.masterAttestationEpoch,
|
|
7794
|
+
ownerSessionId: options.masterOwnerSessionId,
|
|
7795
|
+
sessionId: id,
|
|
7796
|
+
pid: process.pid,
|
|
7797
|
+
processIncarnation: hostProcessIncarnation,
|
|
7798
|
+
});
|
|
7799
|
+
throwIfLifecycleStopped();
|
|
7764
7800
|
await host.registerWithBroker({
|
|
7765
7801
|
// The endpoint is written before registration. Its exact mtime
|
|
7766
7802
|
// binds this index generation to that discovery record.
|
|
7767
7803
|
register: async input => {
|
|
7804
|
+
const masterRole = masterAttestationForEffectiveHost({
|
|
7805
|
+
masterCapability: options.masterCapability,
|
|
7806
|
+
attestationEpoch: options.masterAttestationEpoch,
|
|
7807
|
+
ownerSessionId: options.masterOwnerSessionId,
|
|
7808
|
+
sessionId: input.sessionId,
|
|
7809
|
+
pid: process.pid,
|
|
7810
|
+
processIncarnation: hostProcessIncarnation,
|
|
7811
|
+
direct,
|
|
7812
|
+
});
|
|
7768
7813
|
await index.append({
|
|
7769
7814
|
type: "host_registered",
|
|
7770
7815
|
...input,
|
|
@@ -7776,6 +7821,8 @@ export function createNotificationsExtension(
|
|
|
7776
7821
|
// teardown identity provable after that workspace is gone.
|
|
7777
7822
|
...(hostProcessIncarnation ? { processIncarnation: hostProcessIncarnation } : {}),
|
|
7778
7823
|
endpointMtimeMs,
|
|
7824
|
+
endpointFileId,
|
|
7825
|
+
...(masterRole ? { masterRole } : {}),
|
|
7779
7826
|
...(lifecycleRequestId ? { lifecycleRequestId } : {}),
|
|
7780
7827
|
});
|
|
7781
7828
|
},
|
|
@@ -291,7 +291,7 @@ export const SDK_LIFECYCLE_ROUTER_PROTOCOL_VERSION = 1;
|
|
|
291
291
|
* killing a session's mirroring after one transient rejection and must be
|
|
292
292
|
* replaced across this upgrade.
|
|
293
293
|
*/
|
|
294
|
-
export const DAEMON_GENERATION =
|
|
294
|
+
export const DAEMON_GENERATION = 180;
|
|
295
295
|
|
|
296
296
|
/**
|
|
297
297
|
* Serving-compatibility boundary for daemon lifecycle requests. Epoch 7
|
package/src/sdk/cli/index.ts
CHANGED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local interactive-master CLI surface.
|
|
3
|
+
*
|
|
4
|
+
* `gjc sdk spawn` is legal only inside a live master session: the master's Bash
|
|
5
|
+
* tool threads the transient capability only through a strict direct spawn
|
|
6
|
+
* command, and the Broker still verifies it against the live effective host
|
|
7
|
+
* before any effect.
|
|
8
|
+
* No output path may echo the task or capability.
|
|
9
|
+
*/
|
|
10
|
+
import { randomUUID } from "node:crypto";
|
|
11
|
+
import * as path from "node:path";
|
|
12
|
+
import { getAgentDir } from "@gajae-code/utils";
|
|
13
|
+
import { type IndexedSession, isSessionAuthorityEligible, SessionIndex } from "../broker/session-index";
|
|
14
|
+
import { SdkClientError } from "../client";
|
|
15
|
+
import { dispatchSpawnGlobal } from "../lifecycle/broker-client";
|
|
16
|
+
|
|
17
|
+
const MASTER_CAPABILITY_ENV = "GJC_MASTER_CAPABILITY";
|
|
18
|
+
const MASTER_SESSION_ENV = "GJC_SESSION_ID";
|
|
19
|
+
const SPAWN_TIMEOUT_MS = 120_000;
|
|
20
|
+
|
|
21
|
+
export class SdkMasterCliError extends Error {
|
|
22
|
+
constructor(
|
|
23
|
+
readonly code: string,
|
|
24
|
+
message: string,
|
|
25
|
+
readonly exitCode: 1 | 2 = 1,
|
|
26
|
+
) {
|
|
27
|
+
super(message);
|
|
28
|
+
this.name = "SdkMasterCliError";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface SdkSpawnArgs {
|
|
33
|
+
cwd?: string;
|
|
34
|
+
prompt?: string;
|
|
35
|
+
model?: string;
|
|
36
|
+
profile?: string;
|
|
37
|
+
agentDir?: string;
|
|
38
|
+
idempotencyKey?: string;
|
|
39
|
+
json?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The allowlisted spawn result projection rendered to the operator. */
|
|
43
|
+
export interface SdkSpawnRendered {
|
|
44
|
+
code: string;
|
|
45
|
+
claimId?: string;
|
|
46
|
+
sessionId?: string;
|
|
47
|
+
substrateKind?: string;
|
|
48
|
+
seed?: { phase?: string; clientRef?: string; commandId?: string; turnId?: string; status?: string };
|
|
49
|
+
idempotencyKey?: string;
|
|
50
|
+
error?: { code: string; message: string };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface SdkSpawnDependencies {
|
|
54
|
+
env: Record<string, string | undefined>;
|
|
55
|
+
dispatch: (agentDir: string, input: Record<string, unknown>, idempotencyKey: string) => Promise<unknown>;
|
|
56
|
+
resolveAttestationEpoch: (agentDir: string, ownerSessionId: string) => Promise<string | undefined>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function brokerSpawnDispatch(
|
|
60
|
+
agentDir: string,
|
|
61
|
+
input: Record<string, unknown>,
|
|
62
|
+
idempotencyKey: string,
|
|
63
|
+
): Promise<unknown> {
|
|
64
|
+
return await dispatchSpawnGlobal(agentDir, input, idempotencyKey, SPAWN_TIMEOUT_MS);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Select the newest retained master attestation for an owner session.
|
|
69
|
+
*
|
|
70
|
+
* `listSessionIdentities()` can retain both the direct-role row and one or
|
|
71
|
+
* more endpoint generations. Its iteration order is a projection detail, not
|
|
72
|
+
* authority ordering, so selecting the first matching row can resurrect an
|
|
73
|
+
* older epoch after a master relaunch. The broker's monotonically increasing
|
|
74
|
+
* `indexSeq` is the authoritative retained ordering.
|
|
75
|
+
*/
|
|
76
|
+
export function selectNewestMasterAttestationEpoch(
|
|
77
|
+
rows: readonly IndexedSession[],
|
|
78
|
+
ownerSessionId: string,
|
|
79
|
+
): string | undefined {
|
|
80
|
+
const processIdentity = (row: IndexedSession): string | undefined => row.hostIncarnation ?? row.processIncarnation;
|
|
81
|
+
const matchesAttestation = (
|
|
82
|
+
left: NonNullable<IndexedSession["masterRole"]>,
|
|
83
|
+
right: NonNullable<IndexedSession["masterRole"]>,
|
|
84
|
+
) =>
|
|
85
|
+
left.version === right.version &&
|
|
86
|
+
left.ownerSessionId === right.ownerSessionId &&
|
|
87
|
+
left.launchPid === right.launchPid &&
|
|
88
|
+
left.launchProcessIncarnation === right.launchProcessIncarnation &&
|
|
89
|
+
left.role === right.role &&
|
|
90
|
+
left.attestationEpoch === right.attestationEpoch;
|
|
91
|
+
const hasDirectAttestation = (effective: IndexedSession): boolean =>
|
|
92
|
+
rows.some(
|
|
93
|
+
row =>
|
|
94
|
+
row.sessionId === effective.sessionId &&
|
|
95
|
+
row.endpointGeneration === 0 &&
|
|
96
|
+
row.pid === effective.masterRole?.launchPid &&
|
|
97
|
+
processIdentity(row) === effective.masterRole?.launchProcessIncarnation &&
|
|
98
|
+
row.masterRole !== undefined &&
|
|
99
|
+
effective.masterRole !== undefined &&
|
|
100
|
+
matchesAttestation(row.masterRole, effective.masterRole),
|
|
101
|
+
);
|
|
102
|
+
let newest: { indexSeq: number; epoch: string } | undefined;
|
|
103
|
+
for (const row of rows) {
|
|
104
|
+
const attestation = row.masterRole;
|
|
105
|
+
if (
|
|
106
|
+
!Number.isSafeInteger(row.indexSeq) ||
|
|
107
|
+
attestation === undefined ||
|
|
108
|
+
attestation.version !== 2 ||
|
|
109
|
+
attestation.role !== "master" ||
|
|
110
|
+
attestation.ownerSessionId !== ownerSessionId ||
|
|
111
|
+
row.endpointGeneration <= 0 ||
|
|
112
|
+
row.live !== true ||
|
|
113
|
+
row.terminal ||
|
|
114
|
+
row.terminalUncertain ||
|
|
115
|
+
!isSessionAuthorityEligible(row) ||
|
|
116
|
+
attestation.launchPid !== row.pid ||
|
|
117
|
+
attestation.launchProcessIncarnation !== processIdentity(row) ||
|
|
118
|
+
!hasDirectAttestation(row) ||
|
|
119
|
+
typeof attestation.attestationEpoch !== "string" ||
|
|
120
|
+
attestation.attestationEpoch.length === 0 ||
|
|
121
|
+
attestation.attestationEpoch.length > 512 ||
|
|
122
|
+
!/^[A-Za-z0-9_-]+$/u.test(attestation.attestationEpoch)
|
|
123
|
+
)
|
|
124
|
+
continue;
|
|
125
|
+
if (newest === undefined || row.indexSeq > newest.indexSeq)
|
|
126
|
+
newest = { indexSeq: row.indexSeq, epoch: attestation.attestationEpoch };
|
|
127
|
+
}
|
|
128
|
+
return newest?.epoch;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function indexAttestationEpoch(agentDir: string, ownerSessionId: string): Promise<string | undefined> {
|
|
132
|
+
const index = await new SessionIndex(agentDir).open();
|
|
133
|
+
return selectNewestMasterAttestationEpoch(index.listSessionIdentities(), ownerSessionId);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function record(value: unknown): Record<string, unknown> | undefined {
|
|
137
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
138
|
+
? (value as Record<string, unknown>)
|
|
139
|
+
: undefined;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function opaque(value: unknown): string | undefined {
|
|
143
|
+
return typeof value === "string" && value.length > 0 && value.length <= 512 ? value : undefined;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Projects an arbitrary Broker response onto the allowlisted render shape. */
|
|
147
|
+
export function safeSpawnRender(response: unknown): { rendered: SdkSpawnRendered; exitCode: 0 | 1 } {
|
|
148
|
+
const outer = record(response);
|
|
149
|
+
if (outer?.ok === true) {
|
|
150
|
+
const result = record(outer.result);
|
|
151
|
+
const seed = record(result?.seed);
|
|
152
|
+
return {
|
|
153
|
+
rendered: {
|
|
154
|
+
code: opaque(result?.code) ?? "spawn_accepted",
|
|
155
|
+
...(opaque(result?.claimId) === undefined ? {} : { claimId: opaque(result?.claimId) }),
|
|
156
|
+
...(opaque(result?.sessionId) === undefined ? {} : { sessionId: opaque(result?.sessionId) }),
|
|
157
|
+
...(opaque(result?.substrateKind) === undefined ? {} : { substrateKind: opaque(result?.substrateKind) }),
|
|
158
|
+
...(seed === undefined
|
|
159
|
+
? {}
|
|
160
|
+
: {
|
|
161
|
+
seed: {
|
|
162
|
+
...(opaque(seed.phase) === undefined ? {} : { phase: opaque(seed.phase) }),
|
|
163
|
+
...(opaque(seed.clientRef) === undefined ? {} : { clientRef: opaque(seed.clientRef) }),
|
|
164
|
+
...(opaque(seed.commandId) === undefined ? {} : { commandId: opaque(seed.commandId) }),
|
|
165
|
+
...(opaque(seed.turnId) === undefined ? {} : { turnId: opaque(seed.turnId) }),
|
|
166
|
+
...(opaque(seed.status) === undefined ? {} : { status: opaque(seed.status) }),
|
|
167
|
+
},
|
|
168
|
+
}),
|
|
169
|
+
},
|
|
170
|
+
exitCode: 0,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
const error = record(outer?.error);
|
|
174
|
+
const code = opaque(error?.code) ?? "spawn_failed";
|
|
175
|
+
const retryIdempotencyKey = code === "uncertain_after_send" ? opaque(error?.idempotencyKey) : undefined;
|
|
176
|
+
// Error messages are Broker-typed and never quote request input; an uncertain
|
|
177
|
+
// post-send result may additionally expose its non-secret replay key.
|
|
178
|
+
const message =
|
|
179
|
+
typeof error?.message === "string" && error.message.length <= 512 ? error.message : "session.spawn failed";
|
|
180
|
+
return {
|
|
181
|
+
rendered: {
|
|
182
|
+
code,
|
|
183
|
+
...(retryIdempotencyKey === undefined ? {} : { idempotencyKey: retryIdempotencyKey }),
|
|
184
|
+
error: { code, message },
|
|
185
|
+
},
|
|
186
|
+
exitCode: 1,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function renderSpawnTable(rendered: SdkSpawnRendered): string {
|
|
191
|
+
const lines = [`Result: ${rendered.code}`];
|
|
192
|
+
if (rendered.claimId) lines.push(`Claim: ${rendered.claimId}`);
|
|
193
|
+
if (rendered.sessionId) lines.push(`Child session: ${rendered.sessionId}`);
|
|
194
|
+
if (rendered.substrateKind) lines.push(`Substrate: ${rendered.substrateKind}`);
|
|
195
|
+
if (rendered.seed?.phase) lines.push(`Seed phase: ${rendered.seed.phase}`);
|
|
196
|
+
if (rendered.seed?.status) lines.push(`Seed status: ${rendered.seed.status}`);
|
|
197
|
+
if (rendered.error) lines.push(`Error: ${rendered.error.code}: ${rendered.error.message}`);
|
|
198
|
+
if (rendered.idempotencyKey) lines.push(`Retry idempotency key: ${rendered.idempotencyKey}`);
|
|
199
|
+
|
|
200
|
+
return lines.join("\n");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Runs one local master spawn. A caller-provided idempotency key replays the
|
|
205
|
+
* matching durable claim; otherwise each invocation receives a fresh key.
|
|
206
|
+
*/
|
|
207
|
+
export async function runSdkSpawn(
|
|
208
|
+
args: SdkSpawnArgs,
|
|
209
|
+
dependencies: Partial<SdkSpawnDependencies> = {},
|
|
210
|
+
): Promise<{ rendered: SdkSpawnRendered; exitCode: 0 | 1 }> {
|
|
211
|
+
const deps: SdkSpawnDependencies = {
|
|
212
|
+
env: dependencies.env ?? process.env,
|
|
213
|
+
dispatch: dependencies.dispatch ?? brokerSpawnDispatch,
|
|
214
|
+
resolveAttestationEpoch: dependencies.resolveAttestationEpoch ?? indexAttestationEpoch,
|
|
215
|
+
};
|
|
216
|
+
if (!args.cwd) throw new SdkMasterCliError("invalid_input", "sdk spawn requires --cwd <dir>.", 2);
|
|
217
|
+
if (!args.prompt) throw new SdkMasterCliError("invalid_input", "sdk spawn requires --prompt <task>.", 2);
|
|
218
|
+
const capability = deps.env[MASTER_CAPABILITY_ENV];
|
|
219
|
+
const ownerSessionId = deps.env[MASTER_SESSION_ENV];
|
|
220
|
+
if (!capability || !ownerSessionId)
|
|
221
|
+
throw new SdkMasterCliError(
|
|
222
|
+
"master_context_required",
|
|
223
|
+
"sdk spawn is available only inside a live interactive master session.",
|
|
224
|
+
1,
|
|
225
|
+
);
|
|
226
|
+
const agentDir = args.agentDir ?? getAgentDir();
|
|
227
|
+
const attestationEpoch = await deps.resolveAttestationEpoch(agentDir, ownerSessionId);
|
|
228
|
+
if (!attestationEpoch)
|
|
229
|
+
throw new SdkMasterCliError(
|
|
230
|
+
"master_context_required",
|
|
231
|
+
"No master role attestation exists for this session; relaunch with gjc --master.",
|
|
232
|
+
1,
|
|
233
|
+
);
|
|
234
|
+
const idempotencyKey = args.idempotencyKey ?? randomUUID();
|
|
235
|
+
try {
|
|
236
|
+
const response = await deps.dispatch(
|
|
237
|
+
agentDir,
|
|
238
|
+
{
|
|
239
|
+
task: args.prompt,
|
|
240
|
+
masterCapability: capability,
|
|
241
|
+
ownerSessionId,
|
|
242
|
+
attestationEpoch,
|
|
243
|
+
cwd: args.cwd === undefined ? undefined : path.resolve(args.cwd),
|
|
244
|
+
...(args.model === undefined ? {} : { modelId: args.model }),
|
|
245
|
+
...(args.profile === undefined ? {} : { modelPreset: args.profile }),
|
|
246
|
+
},
|
|
247
|
+
idempotencyKey,
|
|
248
|
+
);
|
|
249
|
+
return safeSpawnRender(response);
|
|
250
|
+
} catch (error) {
|
|
251
|
+
if (error instanceof SdkClientError && error.code === "uncertain_after_send") {
|
|
252
|
+
return safeSpawnRender({
|
|
253
|
+
ok: false,
|
|
254
|
+
error: { code: error.code, message: error.message, idempotencyKey },
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
throw error;
|
|
258
|
+
}
|
|
259
|
+
}
|
package/src/sdk/cli/rows.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Versioned DTO rows for `gjc sdk session` semantic verbs (row DTO
|
|
2
|
+
* Versioned DTO rows for `gjc sdk session` semantic verbs (row DTO v2, DR-12).
|
|
3
3
|
*
|
|
4
4
|
* Every semantic verb renders its output through these rows so machine
|
|
5
5
|
* consumers get one deterministic, versioned envelope per verb. Credentials
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* rendering.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
export const SESSION_ROWS_VERSION =
|
|
11
|
+
export const SESSION_ROWS_VERSION = 2;
|
|
12
12
|
|
|
13
13
|
export type SdkSessionActivityState = "active" | "idle";
|
|
14
14
|
|
|
@@ -19,7 +19,7 @@ export interface SdkSessionActivityV1 {
|
|
|
19
19
|
|
|
20
20
|
export interface SdkSessionRowV1 {
|
|
21
21
|
sessionId: string;
|
|
22
|
-
locator: {
|
|
22
|
+
locator: { cwd: string; worktreeRoot: string | null; stateRoot: string };
|
|
23
23
|
endpointGeneration: number;
|
|
24
24
|
/** Process-bound incarnation of the hosting process (C1); absent for legacy-provenance rows. */
|
|
25
25
|
hostIncarnation?: string;
|
|
@@ -98,12 +98,15 @@ function optionalFiniteNumber(value: unknown): number | undefined {
|
|
|
98
98
|
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
function locatorOf(row: Record<string, unknown>): {
|
|
101
|
+
function locatorOf(row: Record<string, unknown>): { cwd: string; worktreeRoot: string | null; stateRoot: string } {
|
|
102
102
|
const locator = isRecord(row.locator) ? row.locator : {};
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
103
|
+
const cwd = optionalString(locator.cwd);
|
|
104
|
+
const stateRoot = optionalString(locator.stateRoot);
|
|
105
|
+
const worktreeRoot = locator.worktreeRoot;
|
|
106
|
+
if (!cwd || !stateRoot || (worktreeRoot !== null && optionalString(worktreeRoot) === undefined)) {
|
|
107
|
+
throw new Error("session row has an invalid locator v2");
|
|
108
|
+
}
|
|
109
|
+
return { cwd, worktreeRoot: worktreeRoot === null ? null : optionalString(worktreeRoot)!, stateRoot };
|
|
107
110
|
}
|
|
108
111
|
|
|
109
112
|
function activityOf(row: Record<string, unknown>): SdkSessionActivityV1 | undefined {
|