@gajae-code/coding-agent 0.12.10 → 0.12.12
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 +47 -2
- package/dist/types/cli/notify-cli.d.ts +42 -1
- package/dist/types/cli/update-cli.d.ts +9 -3
- package/dist/types/commands/notify.d.ts +6 -0
- package/dist/types/config/file-lock.d.ts +3 -0
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/coordinator-mcp/question-state.d.ts +3 -1
- package/dist/types/extensibility/extensions/index.d.ts +1 -0
- package/dist/types/extensibility/extensions/ouroboros-ooo-bridge.d.ts +13 -0
- package/dist/types/extensibility/extensions/prefix-command-bridge.d.ts +0 -1
- package/dist/types/extensibility/extensions/types.d.ts +2 -0
- package/dist/types/main.d.ts +6 -2
- package/dist/types/modes/components/session-selector.d.ts +1 -1
- package/dist/types/modes/utils/host-status.d.ts +23 -0
- package/dist/types/runtime-mcp/types.d.ts +2 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +12 -0
- package/dist/types/sdk/bus/chat-daemon-cli.d.ts +3 -0
- package/dist/types/sdk/bus/chat-daemon-command-channel.d.ts +244 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +13 -1
- package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +7 -0
- package/dist/types/sdk/bus/control-plane-frames.d.ts +43 -0
- package/dist/types/sdk/bus/conversation-store.d.ts +18 -0
- package/dist/types/sdk/bus/existing-thread-readiness.d.ts +71 -0
- package/dist/types/sdk/bus/notification-service.d.ts +10 -0
- package/dist/types/sdk/bus/slack-daemon.d.ts +27 -0
- package/dist/types/sdk/bus/slack-live-provider.d.ts +11 -0
- package/dist/types/sdk/bus/slack-provider.d.ts +14 -0
- package/dist/types/sdk/bus/slack-thread-binding.d.ts +140 -0
- package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +2 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +25 -7
- package/dist/types/sdk/bus/telegram-daemon.d.ts +104 -16
- package/dist/types/sdk/bus/telegram-reference.d.ts +10 -1
- package/dist/types/sdk/bus/topic-registry.d.ts +130 -85
- package/dist/types/sdk/host/host.d.ts +52 -0
- package/dist/types/sdk/lifecycle-session.d.ts +6 -0
- package/dist/types/sdk/session-activation.d.ts +41 -0
- package/dist/types/sdk/session.d.ts +18 -0
- package/dist/types/sdk/startup-capability.d.ts +8 -1
- package/dist/types/session/agent-session.d.ts +7 -0
- package/dist/types/session/artifacts.d.ts +2 -2
- package/dist/types/session/internal/managed-session-storage.d.ts +12 -2
- package/dist/types/session/session-manager.d.ts +40 -5
- package/dist/types/session/session-storage.d.ts +1 -0
- package/dist/types/tools/computer.d.ts +35 -2
- package/dist/types/tools/index.d.ts +8 -0
- package/dist/types/tools/todo-write.d.ts +2 -0
- package/dist/types/utils/npm-registry.d.ts +71 -0
- package/examples/extensions/README.md +38 -4
- package/examples/extensions/ooo-bridge.ts +15 -0
- package/package.json +8 -8
- package/scripts/generate-sdk-operation-inventory.ts +4 -0
- package/src/async/job-manager.ts +9 -2
- package/src/cli/notify-cli.ts +226 -2
- package/src/cli/update-cli.ts +90 -23
- package/src/commands/notify.ts +27 -3
- package/src/commands/sdk.ts +1 -0
- package/src/config/file-lock-gc.ts +18 -0
- package/src/config/file-lock.ts +61 -16
- package/src/config/model-profiles.ts +31 -15
- package/src/coordinator/contract.ts +1 -0
- package/src/coordinator-mcp/question-state.ts +2 -0
- package/src/coordinator-mcp/server.ts +358 -7
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -0
- package/src/extensibility/extensions/index.ts +1 -0
- package/src/extensibility/extensions/ouroboros-ooo-bridge.ts +235 -0
- package/src/extensibility/extensions/prefix-command-bridge.ts +4 -9
- package/src/extensibility/extensions/runner.ts +15 -2
- package/src/extensibility/extensions/types.ts +2 -0
- package/src/gjc-runtime/launch-tmux.ts +53 -1
- package/src/internal-urls/docs-index.generated.ts +13 -12
- package/src/main.ts +203 -67
- package/src/modes/DESIGN.md +8 -5
- package/src/modes/components/footer.ts +10 -18
- package/src/modes/components/session-selector.ts +26 -3
- package/src/modes/components/status-line/gh.ts +23 -2
- package/src/modes/controllers/event-controller.ts +3 -0
- package/src/modes/controllers/extension-ui-controller.ts +2 -0
- package/src/modes/controllers/input-controller.ts +11 -0
- package/src/modes/controllers/selector-controller.ts +8 -2
- package/src/modes/interactive-mode.ts +7 -1
- package/src/modes/utils/host-status.ts +38 -0
- package/src/prompts/tools/computer.md +6 -1
- package/src/runtime-mcp/types.ts +2 -0
- package/src/sdk/broker/lifecycle.ts +60 -3
- package/src/sdk/bus/chat-daemon-cli.ts +42 -0
- package/src/sdk/bus/chat-daemon-command-channel.ts +921 -0
- package/src/sdk/bus/chat-daemon-control.ts +18 -3
- package/src/sdk/bus/chat-daemon-runtime.ts +257 -21
- package/src/sdk/bus/control-plane-frames.ts +51 -0
- package/src/sdk/bus/conversation-store.ts +29 -1
- package/src/sdk/bus/existing-thread-readiness.ts +155 -0
- package/src/sdk/bus/index.ts +70 -0
- package/src/sdk/bus/notification-service.ts +28 -1
- package/src/sdk/bus/rich-render.ts +3 -2
- package/src/sdk/bus/slack-daemon.ts +99 -4
- package/src/sdk/bus/slack-live-provider.ts +32 -2
- package/src/sdk/bus/slack-provider.ts +11 -0
- package/src/sdk/bus/slack-thread-binding.ts +641 -0
- package/src/sdk/bus/telegram-daemon-cli.ts +11 -0
- package/src/sdk/bus/telegram-daemon-contract.ts +25 -7
- package/src/sdk/bus/telegram-daemon.ts +1961 -497
- package/src/sdk/bus/telegram-reference.ts +42 -6
- package/src/sdk/bus/topic-registry.ts +674 -181
- package/src/sdk/host/host.ts +180 -1
- package/src/sdk/lifecycle-session.ts +8 -2
- package/src/sdk/protocol/operation-inventory.generated.json +22 -0
- package/src/sdk/session-activation.ts +127 -0
- package/src/sdk/session.ts +139 -28
- package/src/sdk/startup-capability.ts +10 -1
- package/src/session/agent-session.ts +331 -30
- package/src/session/artifacts.ts +56 -13
- package/src/session/internal/managed-session-storage.ts +440 -243
- package/src/session/session-manager.ts +288 -44
- package/src/session/session-storage.ts +3 -0
- package/src/setup/provider-presets.json +1 -1
- package/src/task/index.ts +92 -44
- package/src/tools/computer.ts +322 -45
- package/src/tools/index.ts +8 -0
- package/src/tools/todo-write.ts +29 -11
- package/src/utils/npm-registry.ts +660 -0
package/src/sdk/host/host.ts
CHANGED
|
@@ -4,6 +4,42 @@ import type { BrokerIndexWriter, HostEndpointAdapters, SdkFrame } from "./types"
|
|
|
4
4
|
|
|
5
5
|
export type SdkRequestObserver = (kind: "control" | "query", connectionId: string, frame: SdkFrame) => void;
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* When a session publishes its replayable readiness signal.
|
|
9
|
+
*
|
|
10
|
+
* `immediate` is the stock contract: `start()` publishes `session_ready` at
|
|
11
|
+
* once, so a chat daemon that attaches (or replays late) surfaces the session
|
|
12
|
+
* and creates its stock root. `deferred` prepares the session instead: the
|
|
13
|
+
* session id, endpoint, and broker registration become discoverable authority,
|
|
14
|
+
* but no readiness exists for any consumer to act on until `activate()` runs.
|
|
15
|
+
* That ordering is what lets an operator adopt an existing chat root before the
|
|
16
|
+
* daemon would otherwise have published one of its own.
|
|
17
|
+
*/
|
|
18
|
+
export type SessionReadinessMode = "immediate" | "deferred";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Replayable signal that a deferred session is fully initialized and holding
|
|
22
|
+
* endpoint authority while its readiness stays withheld.
|
|
23
|
+
*
|
|
24
|
+
* It is deliberately not `session_ready`: a broker lifecycle wait can
|
|
25
|
+
* authenticate it as the child's semantic completion receipt, while every
|
|
26
|
+
* readiness consumer — chat daemons above all — keeps ignoring it and publishes
|
|
27
|
+
* no root until the session is explicitly activated.
|
|
28
|
+
*/
|
|
29
|
+
export const SESSION_PREPARED_EVENT = "session_prepared";
|
|
30
|
+
|
|
31
|
+
/** Every terminal answer an activation attempt can produce. */
|
|
32
|
+
export type SessionActivationOutcome =
|
|
33
|
+
| "activated"
|
|
34
|
+
| "already"
|
|
35
|
+
| "not_prepared"
|
|
36
|
+
| "generation_changed"
|
|
37
|
+
| "not_authorized"
|
|
38
|
+
| "authority_unavailable";
|
|
39
|
+
|
|
40
|
+
/** Proves that a prepared session may publish readiness at this exact generation. */
|
|
41
|
+
export type SessionActivationGate = (input: { sessionId: string; generation: number }) => boolean | Promise<boolean>;
|
|
42
|
+
|
|
7
43
|
export interface SessionSdkHostOptions extends HostEndpointAdapters {
|
|
8
44
|
control?: (connectionId: string, frame: SdkFrame) => unknown | Promise<unknown>;
|
|
9
45
|
query?: (connectionId: string, frame: SdkFrame) => unknown | Promise<unknown>;
|
|
@@ -23,12 +59,32 @@ export interface SessionSdkHostOptions extends HostEndpointAdapters {
|
|
|
23
59
|
onReverseCancel?: (requestId: string, reason: "provider_disconnected" | "lease_released") => void;
|
|
24
60
|
/** Best-effort capabilities mirrored from the native transport for out-of-band consumers. */
|
|
25
61
|
connectionCapabilities?: (connectionId: string) => ReadonlySet<string> | undefined;
|
|
62
|
+
/** Readiness publication mode; defaults to the stock immediate contract. */
|
|
63
|
+
readiness?: SessionReadinessMode;
|
|
64
|
+
/**
|
|
65
|
+
* Authorization for a deferred activation. It is consulted on every attempt
|
|
66
|
+
* that would publish readiness and never on an idempotent replay, and a gate
|
|
67
|
+
* that fails is never read as authorization.
|
|
68
|
+
*/
|
|
69
|
+
activationGate?: SessionActivationGate;
|
|
26
70
|
}
|
|
27
71
|
|
|
28
72
|
const TOOL_ACTIVITY_CAPABILITY = "tool_activity_v2";
|
|
29
73
|
const CAP_GATED_FRAME_KINDS = new Set(["tool_activity", "reasoning_summary"]);
|
|
30
74
|
const EMPTY_CAPABILITIES: ReadonlySet<string> = new Set();
|
|
31
75
|
|
|
76
|
+
/** Safe, identifier-free explanations for every refused activation status. */
|
|
77
|
+
const ACTIVATION_MESSAGES: Record<
|
|
78
|
+
Exclude<SessionActivationOutcome, "activated" | "already"> | "session_mismatch",
|
|
79
|
+
string
|
|
80
|
+
> = {
|
|
81
|
+
not_prepared: "The session is not prepared for activation.",
|
|
82
|
+
generation_changed: "The session endpoint generation changed before activation.",
|
|
83
|
+
not_authorized: "Session activation is not authorized at this endpoint generation.",
|
|
84
|
+
authority_unavailable: "Session activation authority could not be read.",
|
|
85
|
+
session_mismatch: "The activation request addresses a different session.",
|
|
86
|
+
};
|
|
87
|
+
|
|
32
88
|
/** SDK hosting is independent of notification configuration. Only root sessions host an endpoint. */
|
|
33
89
|
export function shouldHostSdk(_settings: unknown, isTopLevel: boolean, env: NodeJS.ProcessEnv = process.env): boolean {
|
|
34
90
|
return isTopLevel && env.GJC_SDK_DISABLE !== "1";
|
|
@@ -117,9 +173,14 @@ export class SessionSdkHost {
|
|
|
117
173
|
readonly reverse: ReverseLeaseRuntime;
|
|
118
174
|
readonly #options: SessionSdkHostOptions;
|
|
119
175
|
#started = false;
|
|
176
|
+
#stopping = false;
|
|
120
177
|
#stopPromise?: Promise<"stopped">;
|
|
121
178
|
#unsubscribe?: () => void;
|
|
122
179
|
#registration?: { writer: BrokerIndexWriter; generation: number };
|
|
180
|
+
/** The generation whose readiness signal has already been published. */
|
|
181
|
+
#readyGeneration?: number;
|
|
182
|
+
/** Serializes activation attempts so a concurrent pair cannot both publish. */
|
|
183
|
+
#activation: Promise<SessionActivationOutcome> = Promise.resolve("not_prepared");
|
|
123
184
|
|
|
124
185
|
constructor(options: SessionSdkHostOptions) {
|
|
125
186
|
this.#options = options;
|
|
@@ -137,6 +198,14 @@ export class SessionSdkHost {
|
|
|
137
198
|
get generation(): number {
|
|
138
199
|
return this.events.generation;
|
|
139
200
|
}
|
|
201
|
+
/** True while the session holds endpoint authority but has published no readiness. */
|
|
202
|
+
get prepared(): boolean {
|
|
203
|
+
return this.#started && this.#readyGeneration !== this.events.generation;
|
|
204
|
+
}
|
|
205
|
+
/** True once readiness for the current generation has been published. */
|
|
206
|
+
get ready(): boolean {
|
|
207
|
+
return this.#started && this.#readyGeneration === this.events.generation;
|
|
208
|
+
}
|
|
140
209
|
/** Current installed definitions for a live provider capability. */
|
|
141
210
|
getProviderDefinitions(capability: string): unknown | undefined {
|
|
142
211
|
return this.reverse.getInstalledDefinitions(capability);
|
|
@@ -154,7 +223,13 @@ export class SessionSdkHost {
|
|
|
154
223
|
async start(): Promise<"started" | "already"> {
|
|
155
224
|
if (this.#started) return "already";
|
|
156
225
|
this.events.restart();
|
|
157
|
-
this.
|
|
226
|
+
if (this.#options.readiness !== "deferred") this.#publishReadiness();
|
|
227
|
+
else
|
|
228
|
+
this.emitEvent({
|
|
229
|
+
name: SESSION_PREPARED_EVENT,
|
|
230
|
+
sessionId: this.#options.sessionId,
|
|
231
|
+
generation: this.events.generation,
|
|
232
|
+
});
|
|
158
233
|
const disposer = this.#options.onFrame((connectionId, frame) => {
|
|
159
234
|
void this.#onFrame(connectionId, frame);
|
|
160
235
|
});
|
|
@@ -169,6 +244,61 @@ export class SessionSdkHost {
|
|
|
169
244
|
return "started";
|
|
170
245
|
}
|
|
171
246
|
|
|
247
|
+
#publishReadiness(): void {
|
|
248
|
+
this.emitEvent({ name: "session_ready", sessionId: this.#options.sessionId, generation: this.events.generation });
|
|
249
|
+
this.#readyGeneration = this.events.generation;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Publish the readiness signal a prepared session withheld.
|
|
254
|
+
*
|
|
255
|
+
* The attempt is refused unless the session is still started at exactly
|
|
256
|
+
* `expectedGeneration` (when supplied) and the activation gate authorizes it
|
|
257
|
+
* at that same generation. Authority is re-proved after the gate resolves,
|
|
258
|
+
* because a stop or an endpoint roll can land while it is in flight, and an
|
|
259
|
+
* exact retry after a successful activation is answered `already` instead of
|
|
260
|
+
* publishing a second readiness signal.
|
|
261
|
+
*/
|
|
262
|
+
activate(expectedGeneration?: number): Promise<SessionActivationOutcome> {
|
|
263
|
+
const attempt = this.#activation.then(
|
|
264
|
+
() => this.#activateOnce(expectedGeneration),
|
|
265
|
+
() => this.#activateOnce(expectedGeneration),
|
|
266
|
+
);
|
|
267
|
+
this.#activation = attempt.then(
|
|
268
|
+
outcome => outcome,
|
|
269
|
+
() => "authority_unavailable" as const,
|
|
270
|
+
);
|
|
271
|
+
return attempt;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async #activateOnce(expectedGeneration?: number): Promise<SessionActivationOutcome> {
|
|
275
|
+
if (!this.#started) return "not_prepared";
|
|
276
|
+
const generation = this.events.generation;
|
|
277
|
+
if (expectedGeneration !== undefined && expectedGeneration !== generation) return "generation_changed";
|
|
278
|
+
if (this.#readyGeneration === generation) return "already";
|
|
279
|
+
const gate = this.#options.activationGate;
|
|
280
|
+
if (gate) {
|
|
281
|
+
let authorized: boolean;
|
|
282
|
+
try {
|
|
283
|
+
authorized = await gate({ sessionId: this.#options.sessionId, generation });
|
|
284
|
+
} catch {
|
|
285
|
+
// An unreadable authority is never an authorization.
|
|
286
|
+
return "authority_unavailable";
|
|
287
|
+
}
|
|
288
|
+
if (!authorized) return "not_authorized";
|
|
289
|
+
// The gate is asynchronous, so the session may have stopped or rolled
|
|
290
|
+
// while it ran; nothing published below may rest on the earlier proof.
|
|
291
|
+
// `#started` stays true across the unregister await inside stop(), so an
|
|
292
|
+
// activation gate that resolves during shutdown would otherwise pass this
|
|
293
|
+
// re-check and publish readiness after teardown began.
|
|
294
|
+
if (!this.#started || this.#stopping) return "not_prepared";
|
|
295
|
+
if (this.events.generation !== generation) return "generation_changed";
|
|
296
|
+
if (this.#readyGeneration === generation) return "already";
|
|
297
|
+
}
|
|
298
|
+
this.#publishReadiness();
|
|
299
|
+
return "activated";
|
|
300
|
+
}
|
|
301
|
+
|
|
172
302
|
async stop(): Promise<"stopped" | "already"> {
|
|
173
303
|
if (this.#stopPromise) return this.#stopPromise;
|
|
174
304
|
if (!this.#started) return "already";
|
|
@@ -182,6 +312,9 @@ export class SessionSdkHost {
|
|
|
182
312
|
}
|
|
183
313
|
|
|
184
314
|
async #stopStartedHost(): Promise<"stopped"> {
|
|
315
|
+
// Fence before the first await: everything after this point is teardown,
|
|
316
|
+
// and no in-flight activation may publish readiness across it.
|
|
317
|
+
this.#stopping = true;
|
|
185
318
|
this.#unsubscribe?.();
|
|
186
319
|
this.#unsubscribe = undefined;
|
|
187
320
|
if (this.#registration?.writer.unregister)
|
|
@@ -191,6 +324,7 @@ export class SessionSdkHost {
|
|
|
191
324
|
endpointGeneration: this.events.generation,
|
|
192
325
|
});
|
|
193
326
|
this.#started = false;
|
|
327
|
+
this.#stopping = false;
|
|
194
328
|
return "stopped";
|
|
195
329
|
}
|
|
196
330
|
|
|
@@ -226,6 +360,22 @@ export class SessionSdkHost {
|
|
|
226
360
|
switch (frame.type) {
|
|
227
361
|
case "control_request": {
|
|
228
362
|
this.#observeRequest("control", connectionId, frame);
|
|
363
|
+
// Deferred readiness withholds `session_ready`, but the control
|
|
364
|
+
// dispatcher was still reachable, so the pre-activation interval made
|
|
365
|
+
// activation optional for input admission. `session_activate` is a
|
|
366
|
+
// separate frame type and is unaffected by this gate.
|
|
367
|
+
if (this.prepared) {
|
|
368
|
+
await this.#send(connectionId, {
|
|
369
|
+
type: "control_response",
|
|
370
|
+
id: requiredString(frame, "id"),
|
|
371
|
+
ok: false,
|
|
372
|
+
error: {
|
|
373
|
+
code: "not_activated",
|
|
374
|
+
message: "The session is prepared and must be activated before it accepts controls.",
|
|
375
|
+
},
|
|
376
|
+
});
|
|
377
|
+
break;
|
|
378
|
+
}
|
|
229
379
|
const result = await this.#options.control?.(connectionId, frame);
|
|
230
380
|
if (result !== undefined) {
|
|
231
381
|
const response = { type: "control_response", ...(result as SdkFrame) };
|
|
@@ -241,6 +391,35 @@ export class SessionSdkHost {
|
|
|
241
391
|
}
|
|
242
392
|
break;
|
|
243
393
|
}
|
|
394
|
+
case "session_activate": {
|
|
395
|
+
const id = requiredString(frame, "id");
|
|
396
|
+
const requestedSession = optionalString(frame, "sessionId");
|
|
397
|
+
const requestedGeneration = frame.endpointGeneration;
|
|
398
|
+
if (
|
|
399
|
+
requestedGeneration !== undefined &&
|
|
400
|
+
(typeof requestedGeneration !== "number" ||
|
|
401
|
+
!Number.isSafeInteger(requestedGeneration) ||
|
|
402
|
+
requestedGeneration <= 0)
|
|
403
|
+
)
|
|
404
|
+
throw invalidFrame("endpointGeneration must be a positive integer.");
|
|
405
|
+
// A request addressed to another session is refused before any
|
|
406
|
+
// activation authority is consulted.
|
|
407
|
+
const status: SessionActivationOutcome | "session_mismatch" =
|
|
408
|
+
requestedSession !== undefined && requestedSession !== this.#options.sessionId
|
|
409
|
+
? "session_mismatch"
|
|
410
|
+
: await this.activate(requestedGeneration as number | undefined);
|
|
411
|
+
const ok = status === "activated" || status === "already";
|
|
412
|
+
await this.#send(connectionId, {
|
|
413
|
+
type: "session_activate_result",
|
|
414
|
+
id,
|
|
415
|
+
ok,
|
|
416
|
+
status,
|
|
417
|
+
sessionId: this.#options.sessionId,
|
|
418
|
+
generation: this.events.generation,
|
|
419
|
+
...(ok ? {} : { error: { code: status, message: ACTIVATION_MESSAGES[status] } }),
|
|
420
|
+
});
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
244
423
|
case "event_replay": {
|
|
245
424
|
const id = requiredString(frame, "id");
|
|
246
425
|
const rawGeneration =
|
|
@@ -24,6 +24,12 @@ export type CreateLifecycleAgentSessionOptions = CreateAgentSessionOptions & {
|
|
|
24
24
|
* the manager's short default ceiling.
|
|
25
25
|
*/
|
|
26
26
|
mcpStartupTimeoutMs?: number;
|
|
27
|
+
/**
|
|
28
|
+
* The broker-issued readiness intent from this session's launch request.
|
|
29
|
+
* `deferred` prepares the session: it holds endpoint authority and publishes
|
|
30
|
+
* a prepared signal instead of readiness until it is explicitly activated.
|
|
31
|
+
*/
|
|
32
|
+
readiness?: "immediate" | "deferred";
|
|
27
33
|
};
|
|
28
34
|
|
|
29
35
|
/** Internal lifecycle-only session construction with an owner-bound SDK startup result. */
|
|
@@ -31,9 +37,9 @@ export async function createLifecycleAgentSession(
|
|
|
31
37
|
options: CreateLifecycleAgentSessionOptions = {},
|
|
32
38
|
): Promise<CreateLifecycleAgentSessionResult> {
|
|
33
39
|
const rollback = new SdkStartupRollbackTracker();
|
|
34
|
-
const capability = new SdkStartupCapability(rollback);
|
|
40
|
+
const capability = new SdkStartupCapability(rollback, options.readiness ?? "immediate");
|
|
35
41
|
try {
|
|
36
|
-
const { mcpStartupTimeoutMs, ...sessionOptions } = options;
|
|
42
|
+
const { mcpStartupTimeoutMs, readiness: _readiness, ...sessionOptions } = options;
|
|
37
43
|
const internalOptions = {
|
|
38
44
|
...sessionOptions,
|
|
39
45
|
[lifecycleStartupCapabilityOption]: capability,
|
|
@@ -2329,6 +2329,17 @@
|
|
|
2329
2329
|
"testIds": "not_applicable"
|
|
2330
2330
|
}
|
|
2331
2331
|
},
|
|
2332
|
+
{
|
|
2333
|
+
"sourceId": "agent_session:extendStartupTurnBarrier",
|
|
2334
|
+
"sourceFile": "packages/coding-agent/src/session/agent-session.ts",
|
|
2335
|
+
"sourceKind": "agent_session",
|
|
2336
|
+
"decision": "exclude",
|
|
2337
|
+
"rationale": "internal CLI startup-readiness fence composition, not a user-facing SDK control seam",
|
|
2338
|
+
"exclusionMetadata": {
|
|
2339
|
+
"adapterMappings": "not_applicable",
|
|
2340
|
+
"testIds": "not_applicable"
|
|
2341
|
+
}
|
|
2342
|
+
},
|
|
2332
2343
|
{
|
|
2333
2344
|
"sourceId": "agent_session:constructor",
|
|
2334
2345
|
"sourceFile": "packages/coding-agent/src/session/agent-session.ts",
|
|
@@ -2560,6 +2571,17 @@
|
|
|
2560
2571
|
"testIds": "not_applicable"
|
|
2561
2572
|
}
|
|
2562
2573
|
},
|
|
2574
|
+
{
|
|
2575
|
+
"sourceId": "agent_session:registerToolSessionTransitionCleanup",
|
|
2576
|
+
"sourceFile": "packages/coding-agent/src/session/agent-session.ts",
|
|
2577
|
+
"sourceKind": "agent_session",
|
|
2578
|
+
"decision": "exclude",
|
|
2579
|
+
"rationale": "internal tool transition cleanup registration for shared artifact-manager ownership, not a user-facing SDK control seam",
|
|
2580
|
+
"exclusionMetadata": {
|
|
2581
|
+
"adapterMappings": "not_applicable",
|
|
2582
|
+
"testIds": "not_applicable"
|
|
2583
|
+
}
|
|
2584
|
+
},
|
|
2563
2585
|
{
|
|
2564
2586
|
"sourceId": "agent_session:getAsyncJobSnapshot",
|
|
2565
2587
|
"sourceFile": "packages/coding-agent/src/session/agent-session.ts",
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The activation exchange a prepared session answers, and nothing else.
|
|
3
|
+
*
|
|
4
|
+
* A prepared session holds discoverable endpoint authority while its readiness
|
|
5
|
+
* stays withheld; activation is the one request that asks it to publish that
|
|
6
|
+
* readiness. Every caller shares this exchange whatever proved the endpoint
|
|
7
|
+
* authority beforehand: the `gjc notify activate-thread` CLI resolves it from
|
|
8
|
+
* the local session index, and the Coordinator resolves it from the broker's
|
|
9
|
+
* incarnation-bound binding.
|
|
10
|
+
*
|
|
11
|
+
* It lives outside `sdk/bus` on purpose. The existing-thread module reaches the
|
|
12
|
+
* chat-daemon and settings graph to prove a Slack binding, and the Coordinator
|
|
13
|
+
* MCP entrypoint may not reach that graph at all, so the exchange the machine
|
|
14
|
+
* entrypoint needs carries no notification, daemon, or settings dependency.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { SdkClientError } from "./client/client";
|
|
18
|
+
|
|
19
|
+
export type SessionActivationErrorCode =
|
|
20
|
+
| "session_not_live"
|
|
21
|
+
| "not_prepared"
|
|
22
|
+
| "not_bound"
|
|
23
|
+
| "activation_unavailable"
|
|
24
|
+
| "activation_outcome_unknown";
|
|
25
|
+
|
|
26
|
+
/** A fail-closed rejection while activating a prepared session. */
|
|
27
|
+
export class SessionActivationError extends Error {
|
|
28
|
+
constructor(
|
|
29
|
+
readonly code: SessionActivationErrorCode,
|
|
30
|
+
message: string,
|
|
31
|
+
) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.name = "SessionActivationError";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Safe confirmation of an activation. It carries identifiers only. */
|
|
38
|
+
export interface ActivatedPreparedSession {
|
|
39
|
+
sessionId: string;
|
|
40
|
+
endpointGeneration: number;
|
|
41
|
+
status: "activated" | "already";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The narrow transport surface activation needs from an SDK connection. */
|
|
45
|
+
export interface PreparedSessionActivationClient {
|
|
46
|
+
request(frame: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
47
|
+
close(): Promise<void>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Ask one already-connected session to publish the readiness it withheld.
|
|
52
|
+
*
|
|
53
|
+
* The frame names the exact session and endpoint generation, and the answer is
|
|
54
|
+
* accepted only when it proves both, so a replacement session can never be
|
|
55
|
+
* activated in place of the requested one. Activation is idempotent: an exact
|
|
56
|
+
* retry answers `already` without a second readiness signal.
|
|
57
|
+
*/
|
|
58
|
+
export async function requestPreparedSessionActivation(
|
|
59
|
+
client: PreparedSessionActivationClient,
|
|
60
|
+
sessionId: string,
|
|
61
|
+
endpointGeneration: number,
|
|
62
|
+
): Promise<ActivatedPreparedSession> {
|
|
63
|
+
try {
|
|
64
|
+
const answer = await client.request({
|
|
65
|
+
type: "session_activate",
|
|
66
|
+
sessionId,
|
|
67
|
+
endpointGeneration,
|
|
68
|
+
});
|
|
69
|
+
return interpretActivationAnswer(answer, sessionId, endpointGeneration);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
throw activationFailure(error);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function interpretActivationAnswer(
|
|
76
|
+
answer: Record<string, unknown>,
|
|
77
|
+
sessionId: string,
|
|
78
|
+
endpointGeneration: number,
|
|
79
|
+
): ActivatedPreparedSession {
|
|
80
|
+
const status = answer.status;
|
|
81
|
+
if (
|
|
82
|
+
answer.type !== "session_activate_result" ||
|
|
83
|
+
answer.ok !== true ||
|
|
84
|
+
answer.sessionId !== sessionId ||
|
|
85
|
+
answer.generation !== endpointGeneration ||
|
|
86
|
+
(status !== "activated" && status !== "already")
|
|
87
|
+
)
|
|
88
|
+
throw new SessionActivationError(
|
|
89
|
+
"activation_outcome_unknown",
|
|
90
|
+
"The session answered an activation that does not match the request; rerun it to observe the settled state.",
|
|
91
|
+
);
|
|
92
|
+
return { sessionId, endpointGeneration, status };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Translate a failed activation by what it proves.
|
|
97
|
+
*
|
|
98
|
+
* A refused status is definitive: the session decided and published nothing. A
|
|
99
|
+
* transport failure after the request was sent is not, because the session may
|
|
100
|
+
* have activated before the answer was lost; an exact retry then answers
|
|
101
|
+
* `already` rather than publishing a second readiness signal.
|
|
102
|
+
*/
|
|
103
|
+
function activationFailure(error: unknown): SessionActivationError {
|
|
104
|
+
if (error instanceof SessionActivationError) return error;
|
|
105
|
+
const code = error instanceof SdkClientError ? error.code : undefined;
|
|
106
|
+
if (code === "not_authorized")
|
|
107
|
+
return new SessionActivationError(
|
|
108
|
+
"not_bound",
|
|
109
|
+
"The session is prepared for an existing thread but no binding has been applied yet.",
|
|
110
|
+
);
|
|
111
|
+
if (code === "not_prepared")
|
|
112
|
+
return new SessionActivationError("not_prepared", "The session is not prepared for activation.");
|
|
113
|
+
if (code === "generation_changed" || code === "session_mismatch")
|
|
114
|
+
return new SessionActivationError(
|
|
115
|
+
"session_not_live",
|
|
116
|
+
"The session endpoint generation changed before activation; rerun it against the current session.",
|
|
117
|
+
);
|
|
118
|
+
if (code === "authority_unavailable")
|
|
119
|
+
return new SessionActivationError(
|
|
120
|
+
"activation_unavailable",
|
|
121
|
+
"The session could not read its activation authority.",
|
|
122
|
+
);
|
|
123
|
+
return new SessionActivationError(
|
|
124
|
+
"activation_outcome_unknown",
|
|
125
|
+
"The session did not answer the activation; rerun it to observe the settled state.",
|
|
126
|
+
);
|
|
127
|
+
}
|