@gajae-code/coding-agent 0.18.5 → 0.18.6
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 +101 -0
- package/dist/types/cli-commands.d.ts +10 -0
- package/dist/types/cli-ordinary.d.ts +13 -0
- package/dist/types/cli.d.ts +12 -3
- package/dist/types/commands/contribution-prep.d.ts +0 -3
- package/dist/types/commands/sdk.d.ts +3 -0
- package/dist/types/commands/update.d.ts +1 -0
- package/dist/types/config/model-profile-activation.d.ts +19 -0
- package/dist/types/coordinator-mcp/session-reaper.d.ts +5 -0
- package/dist/types/edit/modes/replace.d.ts +3 -3
- package/dist/types/edit/streaming.d.ts +2 -2
- package/dist/types/exec/bash-shell-supervisor.d.ts +12 -0
- package/dist/types/sdk/broker/discovery.d.ts +8 -0
- package/dist/types/sdk/broker/ensure.d.ts +1 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +4 -0
- package/dist/types/sdk/broker/runtime.d.ts +1 -1
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +1 -0
- package/dist/types/sdk/cli/diagnostics-cli.d.ts +18 -0
- package/dist/types/sdk/diagnostics/observe-broker.d.ts +185 -0
- package/dist/types/sdk/host/control/operations.d.ts +1 -1
- package/dist/types/sdk/index.d.ts +1 -0
- package/dist/types/sdk/router/session-router.d.ts +2 -0
- package/dist/types/sdk/session.d.ts +13 -0
- package/dist/types/session/agent-session.d.ts +5 -1
- package/dist/types/tools/ask-contract.d.ts +75 -254
- package/dist/types/tools/bash.d.ts +1 -1
- package/package.json +7 -7
- package/scripts/build-sdk-package-smoke.ts +5 -0
- package/src/cli/public-command-entry.ts +5 -0
- package/src/cli/ssh-cli.ts +6 -3
- package/src/cli/web-search-cli.ts +9 -3
- package/src/cli-commands.ts +14 -0
- package/src/cli-ordinary.ts +197 -0
- package/src/cli.ts +25 -182
- package/src/commands/contribution-prep.ts +1 -3
- package/src/commands/sdk.ts +59 -0
- package/src/commands/update.ts +15 -1
- package/src/config/file-lock.ts +91 -2
- package/src/config/model-profile-activation.ts +70 -4
- package/src/config/model-profile-contract.ts +4 -1
- package/src/config/model-profiles.ts +23 -31
- package/src/config/model-resolver.ts +9 -1
- package/src/coordinator-mcp/event-webhook.ts +19 -14
- package/src/coordinator-mcp/policy.ts +14 -4
- package/src/coordinator-mcp/server.ts +142 -7
- package/src/coordinator-mcp/session-reaper.ts +9 -1
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
- package/src/edit/modes/replace.ts +6 -6
- package/src/edit/streaming.ts +15 -15
- package/src/exec/bash-shell-supervisor.ts +60 -3
- package/src/internal-urls/docs-index.generated.ts +10 -9
- package/src/memories/index.ts +8 -2
- package/src/modes/acp/acp-agent.ts +649 -367
- package/src/modes/components/tool-execution.ts +1 -1
- package/src/prompts/tools/ask.md +2 -0
- package/src/sdk/broker/broker.ts +46 -0
- package/src/sdk/broker/daemon-entry.ts +67 -6
- package/src/sdk/broker/discovery.ts +8 -0
- package/src/sdk/broker/ensure.ts +156 -20
- package/src/sdk/broker/lifecycle-ledger.ts +110 -12
- package/src/sdk/broker/lifecycle.ts +225 -341
- package/src/sdk/broker/runtime.ts +5 -1
- package/src/sdk/broker/transport.ts +5 -1
- package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
- package/src/sdk/bus/telegram-daemon-control.ts +15 -12
- package/src/sdk/bus/telegram-daemon.ts +34 -1
- package/src/sdk/cli/diagnostics-cli.ts +247 -0
- package/src/sdk/diagnostics/observe-broker.ts +654 -0
- package/src/sdk/host/control/dispatch.ts +52 -4
- package/src/sdk/host/control/operations.ts +1 -1
- package/src/sdk/host/session-runtime.test.ts +40 -2
- package/src/sdk/host/session-runtime.ts +13 -5
- package/src/sdk/index.ts +1 -0
- package/src/sdk/lifecycle-session.ts +1 -0
- package/src/sdk/protocol/operation-registry.ts +1 -1
- package/src/sdk/router/session-router.ts +7 -1
- package/src/sdk/session.ts +37 -8
- package/src/session/agent-session.ts +114 -39
- package/src/session/blob-store.ts +78 -33
- package/src/session/internal/managed-session-storage.ts +96 -3
- package/src/setup/hermes/templates/operator-instructions.v1.md +19 -1
- package/src/setup/hermes-setup.ts +4 -1
- package/src/slash-commands/builtin-registry.ts +8 -5
- package/src/tools/ask-contract.test.ts +76 -1
- package/src/tools/ask-contract.ts +57 -6
- package/src/tools/ask.ts +16 -7
- package/src/tools/bash.ts +38 -29
- package/src/tools/descriptors.test.ts +29 -0
- package/src/tools/tool-catalog.generated.ts +7 -4
- package/src/web/search/index.ts +2 -2
|
@@ -93,6 +93,7 @@ const IDEMPOTENCY_TTL_MS = 15 * 60 * 1_000;
|
|
|
93
93
|
const MAX_IDEMPOTENCY_ENTRIES = 256;
|
|
94
94
|
|
|
95
95
|
const sessionChains = new WeakMap<ControlSurface, Promise<void>>();
|
|
96
|
+
const pendingReplacementBarriers = new WeakMap<ControlSurface, Promise<void>>();
|
|
96
97
|
interface IdempotencyEntry {
|
|
97
98
|
hash: string;
|
|
98
99
|
expiresAt: number;
|
|
@@ -189,6 +190,19 @@ function invalidInput(message: string): never {
|
|
|
189
190
|
throw new TypedControlError("invalid_input", message);
|
|
190
191
|
}
|
|
191
192
|
|
|
193
|
+
const EXPECTED_SDK_RUN_TOKEN_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}:[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$/;
|
|
194
|
+
|
|
195
|
+
function enqueueReplacementBarrier(surface: ControlSurface): () => void {
|
|
196
|
+
const previous = pendingReplacementBarriers.get(surface) ?? Promise.resolve();
|
|
197
|
+
const { promise: current, resolve: resolveCurrent } = Promise.withResolvers<void>();
|
|
198
|
+
const barrier = previous.then(() => current);
|
|
199
|
+
pendingReplacementBarriers.set(surface, barrier);
|
|
200
|
+
return () => {
|
|
201
|
+
resolveCurrent();
|
|
202
|
+
if (pendingReplacementBarriers.get(surface) === barrier) pendingReplacementBarriers.delete(surface);
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
192
206
|
/**
|
|
193
207
|
* C04 `turn.abort` dispatch.
|
|
194
208
|
*
|
|
@@ -244,7 +258,11 @@ function invoke(
|
|
|
244
258
|
case "turn.prompt":
|
|
245
259
|
return surface.prompt(text(input), input.images, input.clientRef as string | undefined);
|
|
246
260
|
case "turn.steer":
|
|
247
|
-
return surface.steer(
|
|
261
|
+
return surface.steer(
|
|
262
|
+
text(input),
|
|
263
|
+
typeof input.clientRef === "string" ? input.clientRef : undefined,
|
|
264
|
+
Object.hasOwn(input, "expectedSdkRunToken") ? (input.expectedSdkRunToken as string) : undefined,
|
|
265
|
+
);
|
|
248
266
|
case "turn.follow_up":
|
|
249
267
|
return surface.followUp(text(input));
|
|
250
268
|
case "turn.abort":
|
|
@@ -486,6 +504,15 @@ export function dispatchControl(
|
|
|
486
504
|
);
|
|
487
505
|
if (!isInput(request.input))
|
|
488
506
|
return Promise.resolve(failure(request.id, "invalid_input", "Control input must be an object."));
|
|
507
|
+
if (
|
|
508
|
+
row.sdkId === "turn.steer" &&
|
|
509
|
+
Object.hasOwn(request.input, "expectedSdkRunToken") &&
|
|
510
|
+
(typeof request.input.expectedSdkRunToken !== "string" ||
|
|
511
|
+
!EXPECTED_SDK_RUN_TOKEN_PATTERN.test(request.input.expectedSdkRunToken))
|
|
512
|
+
)
|
|
513
|
+
return Promise.resolve(
|
|
514
|
+
failure(request.id, "invalid_input", "expectedSdkRunToken must be a commandId:turnId pair."),
|
|
515
|
+
);
|
|
489
516
|
if (
|
|
490
517
|
row.sdkId === "turn.abort" &&
|
|
491
518
|
((brokerAbortFieldPresent && !brokerAbortAuthorized) ||
|
|
@@ -538,10 +565,31 @@ export function dispatchControl(
|
|
|
538
565
|
const work = () => execute(surface, row, dispatchRequest);
|
|
539
566
|
if (row.sdkId === "turn.abort_and_prompt") {
|
|
540
567
|
const cancellable = surface as PreflightCancellableSurface;
|
|
541
|
-
|
|
542
|
-
|
|
568
|
+
const releaseBarrier = enqueueReplacementBarrier(surface);
|
|
569
|
+
try {
|
|
570
|
+
if (Object.hasOwn(cancellable, "cancelPendingPreflights")) cancellable.cancelPendingPreflights?.();
|
|
571
|
+
return serialize(surface, async () => {
|
|
572
|
+
try {
|
|
573
|
+
return await work();
|
|
574
|
+
} finally {
|
|
575
|
+
releaseBarrier();
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
} catch (error) {
|
|
579
|
+
releaseBarrier();
|
|
580
|
+
throw error;
|
|
581
|
+
}
|
|
543
582
|
}
|
|
544
583
|
if (row.idempotency === "idempotent" && dispatchRequest.idempotencyKey)
|
|
545
584
|
return idempotent(surface, row, dispatchRequest, work);
|
|
546
|
-
|
|
585
|
+
if (row.sdkId === "turn.steer") {
|
|
586
|
+
const replacementBarrier = pendingReplacementBarriers.get(surface);
|
|
587
|
+
return replacementBarrier ? replacementBarrier.then(work) : work();
|
|
588
|
+
}
|
|
589
|
+
// Active feedback must reach the worker even while an earlier ordered control
|
|
590
|
+
// waits for that worker. Correlated steer admission remains deduplicated by
|
|
591
|
+
// the runtime reconciliation store, independently of this dispatch chain.
|
|
592
|
+
return row.idempotency === "ordered" && row.sdkId !== "retry.now" && row.sdkId !== "turn.steer"
|
|
593
|
+
? serialize(surface, work)
|
|
594
|
+
: work();
|
|
547
595
|
}
|
|
@@ -22,7 +22,7 @@ export type ControlInput = Record<string, unknown>;
|
|
|
22
22
|
*/
|
|
23
23
|
export interface ControlSurface {
|
|
24
24
|
prompt(text: string, images?: ControlValue, clientRef?: string): Promise<ControlValue> | ControlValue;
|
|
25
|
-
steer(text: string, clientRef?: string): Promise<ControlValue> | ControlValue;
|
|
25
|
+
steer(text: string, clientRef?: string, expectedSdkRunToken?: string): Promise<ControlValue> | ControlValue;
|
|
26
26
|
followUp(text: string): Promise<ControlValue> | ControlValue;
|
|
27
27
|
abort(): Promise<ControlValue> | ControlValue;
|
|
28
28
|
/** Terminal abort: stop the current root turn (and optionally exact owned work). */
|
|
@@ -290,7 +290,7 @@ test("session.last_assistant returns the latest projected readable text past non
|
|
|
290
290
|
});
|
|
291
291
|
});
|
|
292
292
|
|
|
293
|
-
test("session.last_assistant returns
|
|
293
|
+
test("session.last_assistant returns null when the projected transcript has no readable assistant text", async () => {
|
|
294
294
|
const sessionId = "last-assistant-empty";
|
|
295
295
|
const ctx = extensionContext(sessionId, "/tmp", {
|
|
296
296
|
transcript: [
|
|
@@ -311,7 +311,7 @@ test("session.last_assistant returns resource_gone when the projected transcript
|
|
|
311
311
|
],
|
|
312
312
|
});
|
|
313
313
|
|
|
314
|
-
expect(await queryLastAssistant(ctx, sessionId)).toMatchObject({ ok:
|
|
314
|
+
expect(await queryLastAssistant(ctx, sessionId)).toMatchObject({ ok: true, page: { items: [null] } });
|
|
315
315
|
});
|
|
316
316
|
|
|
317
317
|
test("native prompt reconciliation fails closed for an explicitly empty assistant result", () => {
|
|
@@ -4142,6 +4142,7 @@ describe("SessionSdkSessionRuntime", () => {
|
|
|
4142
4142
|
interface PreflightHooks {
|
|
4143
4143
|
onPreflightAccepted?: () => void;
|
|
4144
4144
|
onPreflightAcceptCommit?: () => void | Promise<void>;
|
|
4145
|
+
expectedSdkRunToken?: string;
|
|
4145
4146
|
}
|
|
4146
4147
|
|
|
4147
4148
|
interface ResponseFrame {
|
|
@@ -4482,6 +4483,43 @@ function neverSettlingPromise(): Promise<void> {
|
|
|
4482
4483
|
return Promise.withResolvers<void>().promise;
|
|
4483
4484
|
}
|
|
4484
4485
|
|
|
4486
|
+
test("SDK turn.steer preserves its expected run token and propagates a stale-run rejection", async () => {
|
|
4487
|
+
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-turn-steer-run-token-"));
|
|
4488
|
+
const staleToken = "ended-command:ended-turn";
|
|
4489
|
+
const clientRef = "stale-steer-ref";
|
|
4490
|
+
const calls: Array<{ content: unknown; deliverAs?: string; expectedSdkRunToken?: string }> = [];
|
|
4491
|
+
let harness: InvocationHarness | undefined;
|
|
4492
|
+
try {
|
|
4493
|
+
harness = await invocationHarness("steer-run-token", cwd, {
|
|
4494
|
+
sendUserMessage: async (content, options) => {
|
|
4495
|
+
calls.push({
|
|
4496
|
+
content,
|
|
4497
|
+
deliverAs: options?.deliverAs,
|
|
4498
|
+
expectedSdkRunToken: options?.expectedSdkRunToken,
|
|
4499
|
+
});
|
|
4500
|
+
if (options?.expectedSdkRunToken === staleToken)
|
|
4501
|
+
throw Object.assign(new Error("The expected SDK run is not active."), { code: "turn_not_active" });
|
|
4502
|
+
return "completed";
|
|
4503
|
+
},
|
|
4504
|
+
});
|
|
4505
|
+
|
|
4506
|
+
const rejected = await harness.control("turn.steer", {
|
|
4507
|
+
text: "stale steer",
|
|
4508
|
+
expectedSdkRunToken: staleToken,
|
|
4509
|
+
clientRef,
|
|
4510
|
+
});
|
|
4511
|
+
expect(rejected).toMatchObject({ ok: false, error: { code: "turn_not_active" } });
|
|
4512
|
+
expect(calls).toEqual([{ content: "stale steer", deliverAs: "steer", expectedSdkRunToken: staleToken }]);
|
|
4513
|
+
expect(await harness.query("turn.steer_status", { clientRef })).toMatchObject({
|
|
4514
|
+
ok: true,
|
|
4515
|
+
result: { status: "rejected" },
|
|
4516
|
+
});
|
|
4517
|
+
} finally {
|
|
4518
|
+
if (harness) await harness.stop();
|
|
4519
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4520
|
+
}
|
|
4521
|
+
});
|
|
4522
|
+
|
|
4485
4523
|
describe("post-acceptance invocation terminalization", () => {
|
|
4486
4524
|
test.each([
|
|
4487
4525
|
{ status: 402, code: "provider_http_402" },
|
|
@@ -2059,7 +2059,7 @@ function createQuerySurface(
|
|
|
2059
2059
|
: entry.content?.flatMap(block => (block.type === "text" ? [block.text] : [])).join("\n");
|
|
2060
2060
|
if (text !== undefined && text.trim().length > 0) return text;
|
|
2061
2061
|
}
|
|
2062
|
-
return
|
|
2062
|
+
return null;
|
|
2063
2063
|
};
|
|
2064
2064
|
const getProfileCredentialSessionId = () => ctx.credentialSessionId ?? id;
|
|
2065
2065
|
const profileSettings = (options.settings ?? ctx.settings) as Pick<Settings, "get"> | undefined;
|
|
@@ -2914,6 +2914,7 @@ function createControlSurface(
|
|
|
2914
2914
|
};
|
|
2915
2915
|
type InternalSendOptions = NonNullable<Parameters<ExtensionAPI["sendUserMessage"]>[1]> & {
|
|
2916
2916
|
sdkRunCapability?: SdkRunCapability;
|
|
2917
|
+
expectedSdkRunToken?: string;
|
|
2917
2918
|
};
|
|
2918
2919
|
type InternalSdkApi = Omit<ExtensionAPI, "sendUserMessage"> & {
|
|
2919
2920
|
sendUserMessage: (
|
|
@@ -4243,23 +4244,30 @@ function createControlSurface(
|
|
|
4243
4244
|
),
|
|
4244
4245
|
);
|
|
4245
4246
|
},
|
|
4246
|
-
steer: async (text, clientRef) => {
|
|
4247
|
+
steer: async (text, clientRef, expectedSdkRunToken) => {
|
|
4247
4248
|
const invalid = validateRequiredPromptText("turn.steer", { text });
|
|
4248
4249
|
if (invalid) throw Object.assign(new Error(invalid.message), { code: invalid.code });
|
|
4249
4250
|
const retainedClientRef = normalizeClientRef(clientRef);
|
|
4251
|
+
const sendSteer = () =>
|
|
4252
|
+
sendSdkUserMessage(text, {
|
|
4253
|
+
deliverAs: "steer",
|
|
4254
|
+
...(expectedSdkRunToken === undefined ? {} : { expectedSdkRunToken }),
|
|
4255
|
+
});
|
|
4250
4256
|
if (retainedClientRef === undefined) {
|
|
4251
4257
|
const correlation = newCorrelation();
|
|
4252
|
-
await
|
|
4258
|
+
await sendSteer();
|
|
4253
4259
|
return { accepted: true, ...correlation };
|
|
4254
4260
|
}
|
|
4255
4261
|
const durable = steerReconciliation;
|
|
4256
4262
|
const reservation = await durable.reserveSteer(retainedClientRef, text);
|
|
4257
4263
|
if (reservation.replay) return { accepted: reservation.result.status === "accepted", ...reservation.result };
|
|
4258
4264
|
try {
|
|
4259
|
-
await
|
|
4265
|
+
await sendSteer();
|
|
4260
4266
|
return { accepted: true, ...(await durable.settleSteer(retainedClientRef, "accepted")) };
|
|
4261
4267
|
} catch (error) {
|
|
4262
|
-
|
|
4268
|
+
const settled = await durable.settleSteer(retainedClientRef, "rejected", error);
|
|
4269
|
+
if (expectedSdkRunToken !== undefined) throw error;
|
|
4270
|
+
return { accepted: false, ...settled };
|
|
4263
4271
|
}
|
|
4264
4272
|
},
|
|
4265
4273
|
followUp: async text => {
|
package/src/sdk/index.ts
CHANGED
|
@@ -21,6 +21,7 @@ export type {
|
|
|
21
21
|
TrackedSendUserMessageOptions,
|
|
22
22
|
} from "../session/agent-session";
|
|
23
23
|
export * as bus from "./bus";
|
|
24
|
+
export * from "./diagnostics/observe-broker";
|
|
24
25
|
export * as host from "./host";
|
|
25
26
|
export * as lifecycle from "./lifecycle";
|
|
26
27
|
export * as mcp from "./mcp";
|
|
@@ -123,6 +123,7 @@ export async function createLifecycleAgentSession(
|
|
|
123
123
|
// inside the broker's readiness window is what kills the child at the
|
|
124
124
|
// cutoff; the host resumes it once readiness is published.
|
|
125
125
|
deferMemoryBackendStartup: true,
|
|
126
|
+
deferOptionalModelRefresh: true,
|
|
126
127
|
[lifecycleStartupCapabilityOption]: capability,
|
|
127
128
|
...(mcpStartupTimeoutMs !== undefined ? { [lifecycleMcpStartupTimeoutOption]: mcpStartupTimeoutMs } : {}),
|
|
128
129
|
} as CreateAgentSessionOptions & {
|
|
@@ -267,7 +267,7 @@ function controlErrors(id: string): string[] {
|
|
|
267
267
|
// refuses a session with no selected model before admission. The code is
|
|
268
268
|
// appended: ACP machine-only mapping reads errorCodes[0].
|
|
269
269
|
C01: ["client_ref_conflict", "reconciliation_capacity", "reconciliation_persist_failed", "model_not_selected"],
|
|
270
|
-
C02: ["client_ref_conflict", "reconciliation_capacity", "reconciliation_persist_failed"],
|
|
270
|
+
C02: ["turn_not_active", "client_ref_conflict", "reconciliation_capacity", "reconciliation_persist_failed"],
|
|
271
271
|
C09: ["client_ref_conflict", "reconciliation_capacity", "reconciliation_persist_failed"],
|
|
272
272
|
C06: ["action_claimed"],
|
|
273
273
|
C07: ["action_claimed", "terminal_uncertain"],
|
|
@@ -287,6 +287,8 @@ export interface SessionRouterOptions {
|
|
|
287
287
|
agentDir: string;
|
|
288
288
|
/** Limits attachment to exact ids selected by a Broker-scoped operation. */
|
|
289
289
|
sessionIds?: readonly string[];
|
|
290
|
+
/** Dynamically limits attachment to sessions currently owned by the caller. */
|
|
291
|
+
attachFilter?: (sessionId: string) => boolean;
|
|
290
292
|
/** Marks default SDK clients as notification-only observers; demanding by default. */
|
|
291
293
|
observer?: boolean;
|
|
292
294
|
deps?: SessionRouterDeps;
|
|
@@ -615,6 +617,7 @@ type AdoptedSession = {
|
|
|
615
617
|
export class SessionRouter {
|
|
616
618
|
readonly #agentDir: string;
|
|
617
619
|
readonly #sessionIds: ReadonlySet<string> | undefined;
|
|
620
|
+
readonly #attachFilter: ((sessionId: string) => boolean) | undefined;
|
|
618
621
|
readonly #observer: boolean;
|
|
619
622
|
readonly #deps: SessionRouterDeps;
|
|
620
623
|
readonly #correlateFrame: SessionRouterFrameCorrelator;
|
|
@@ -657,6 +660,7 @@ export class SessionRouter {
|
|
|
657
660
|
constructor(options: SessionRouterOptions) {
|
|
658
661
|
this.#agentDir = options.agentDir;
|
|
659
662
|
this.#sessionIds = options.sessionIds === undefined ? undefined : new Set(options.sessionIds);
|
|
663
|
+
this.#attachFilter = options.attachFilter;
|
|
660
664
|
this.#observer = options.observer === true;
|
|
661
665
|
this.#deps = options.deps ?? {};
|
|
662
666
|
this.#correlateFrame = options.correlateFrame ?? fallbackCorrelation;
|
|
@@ -797,6 +801,7 @@ export class SessionRouter {
|
|
|
797
801
|
if (
|
|
798
802
|
sessionId !== fallback.sessionId ||
|
|
799
803
|
(this.#sessionIds !== undefined && !this.#sessionIds.has(sessionId ?? "")) ||
|
|
804
|
+
(this.#attachFilter !== undefined && !this.#attachFilter(sessionId ?? "")) ||
|
|
800
805
|
endpointGeneration === undefined ||
|
|
801
806
|
pid === undefined ||
|
|
802
807
|
endpointMtimeMs === undefined ||
|
|
@@ -1294,7 +1299,8 @@ export class SessionRouter {
|
|
|
1294
1299
|
session.live &&
|
|
1295
1300
|
isSessionAuthorityEligible(session) &&
|
|
1296
1301
|
!session.terminalUncertain &&
|
|
1297
|
-
(this.#sessionIds === undefined || this.#sessionIds.has(session.sessionId))
|
|
1302
|
+
(this.#sessionIds === undefined || this.#sessionIds.has(session.sessionId)) &&
|
|
1303
|
+
(this.#attachFilter === undefined || this.#attachFilter(session.sessionId)),
|
|
1298
1304
|
);
|
|
1299
1305
|
const liveIds = new Set(live.map(session => session.sessionId));
|
|
1300
1306
|
const attachedIds = new Set<string>();
|
package/src/sdk/session.ts
CHANGED
|
@@ -59,7 +59,11 @@ import { loadCapability, reset as resetCapabilities } from "../capability";
|
|
|
59
59
|
import { type Rule, ruleCapability, setActiveRules } from "../capability/rule";
|
|
60
60
|
import type { SourceMeta } from "../capability/types";
|
|
61
61
|
import { AUTOROUTING_INACTIVE_WARNING } from "../config/autorouting-contract";
|
|
62
|
-
import {
|
|
62
|
+
import {
|
|
63
|
+
isSessionCredentialPinBlocking,
|
|
64
|
+
ModelProfileCredentialError,
|
|
65
|
+
resolveMissingSessionModelRecovery,
|
|
66
|
+
} from "../config/model-profile-activation";
|
|
63
67
|
import { resolveModelProfileName } from "../config/model-profile-contract";
|
|
64
68
|
import { resolveProfileBindings } from "../config/model-profiles";
|
|
65
69
|
import { kNoAuth, ModelRegistry } from "../config/model-registry";
|
|
@@ -577,6 +581,12 @@ export interface CreateAgentSessionOptions {
|
|
|
577
581
|
* @internal CLI-only ordering guard; SDK callers retain immediate startup by default.
|
|
578
582
|
*/
|
|
579
583
|
deferMemoryBackendStartup?: boolean;
|
|
584
|
+
/**
|
|
585
|
+
* Skip optional model-catalog discovery during lifecycle startup. ACP hosts must
|
|
586
|
+
* publish their endpoint before unrelated provider credential refreshes begin.
|
|
587
|
+
* @internal lifecycle-only startup guard.
|
|
588
|
+
*/
|
|
589
|
+
deferOptionalModelRefresh?: boolean;
|
|
580
590
|
|
|
581
591
|
/** Enable LSP integration (tool, formatting, diagnostics, warmup). Default: true */
|
|
582
592
|
enableLsp?: boolean;
|
|
@@ -695,6 +705,22 @@ export interface CreateAgentSessionResult {
|
|
|
695
705
|
gjcRuntimeSnapshot?: GjcRuntimeSnapshotProvider;
|
|
696
706
|
}
|
|
697
707
|
|
|
708
|
+
/**
|
|
709
|
+
* Start optional provider/model discovery only when it is safe to add work to
|
|
710
|
+
* the caller's startup graph. Lifecycle hosts deliberately skip this during
|
|
711
|
+
* construction: discovery preflights every configured provider and may refresh
|
|
712
|
+
* an unrelated OAuth credential while several hosts are opening the same DB.
|
|
713
|
+
*/
|
|
714
|
+
export function startOptionalModelRefresh(
|
|
715
|
+
modelRegistry: Pick<ModelRegistry, "refreshInBackground">,
|
|
716
|
+
credentialSessionId: string | undefined,
|
|
717
|
+
deferred: boolean,
|
|
718
|
+
): boolean {
|
|
719
|
+
if (deferred) return false;
|
|
720
|
+
modelRegistry.refreshInBackground("online-if-uncached", credentialSessionId);
|
|
721
|
+
return true;
|
|
722
|
+
}
|
|
723
|
+
|
|
698
724
|
export interface DeferredMcpConfigStartupResult {
|
|
699
725
|
loadedToolCount: number;
|
|
700
726
|
hasErrors: boolean;
|
|
@@ -1970,11 +1996,15 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1970
1996
|
options.preferredCredentialSelector.selector,
|
|
1971
1997
|
);
|
|
1972
1998
|
}
|
|
1999
|
+
// A runtime key or this registry's literal models.yml apiKey outranks an
|
|
2000
|
+
// unavailable pin; without one the provider stays blocked (never retargeted).
|
|
2001
|
+
const isCredentialPinBlocking = (provider: string): boolean =>
|
|
2002
|
+
isSessionCredentialPinBlocking(modelRegistry, provider, credentialSessionId);
|
|
1973
2003
|
const modelApiKeyAvailability = new Map<string, boolean>();
|
|
1974
2004
|
const getModelAvailabilityKey = (candidate: Model): string =>
|
|
1975
2005
|
`${candidate.provider}\u0000${candidate.baseUrl ?? ""}`;
|
|
1976
2006
|
const hasModelApiKey = async (candidate: Model): Promise<boolean> => {
|
|
1977
|
-
if (
|
|
2007
|
+
if (isCredentialPinBlocking(candidate.provider)) return false;
|
|
1978
2008
|
const availabilityKey = getModelAvailabilityKey(candidate);
|
|
1979
2009
|
const cached = modelApiKeyAvailability.get(availabilityKey);
|
|
1980
2010
|
if (cached !== undefined) {
|
|
@@ -2000,6 +2030,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2000
2030
|
const key = await modelRegistry
|
|
2001
2031
|
.getApiKey(candidate, credentialSessionId, { credentialSelector })
|
|
2002
2032
|
.catch(error => {
|
|
2033
|
+
if (isCredentialPinBlocking(candidate.provider)) return undefined;
|
|
2003
2034
|
if (credentialSelector) {
|
|
2004
2035
|
logger.debug("Credential selector did not match model availability candidate", {
|
|
2005
2036
|
provider: candidate.provider,
|
|
@@ -2148,7 +2179,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2148
2179
|
settings,
|
|
2149
2180
|
);
|
|
2150
2181
|
await refreshMissingQualifiedModelProviders(startupModelSelectors, modelRegistry, credentialSessionId);
|
|
2151
|
-
modelRegistry
|
|
2182
|
+
startOptionalModelRefresh(modelRegistry, credentialSessionId, options.deferOptionalModelRefresh === true);
|
|
2152
2183
|
}
|
|
2153
2184
|
|
|
2154
2185
|
const hasExplicitModel = options.model !== undefined || options.modelPattern !== undefined;
|
|
@@ -2241,8 +2272,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2241
2272
|
{
|
|
2242
2273
|
managedFallback: defaultModelEntries.length > 1,
|
|
2243
2274
|
canonicalSessionId: providerSessionId,
|
|
2244
|
-
isCredentialUnavailable:
|
|
2245
|
-
authStorage.hasSessionCredentialUnavailable(provider, credentialSessionId),
|
|
2275
|
+
isCredentialUnavailable: isCredentialPinBlocking,
|
|
2246
2276
|
...(persistedProfileOwnsDefault ? { aliasIntent: "preset-equivalent" as const } : {}),
|
|
2247
2277
|
},
|
|
2248
2278
|
);
|
|
@@ -2297,7 +2327,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2297
2327
|
!hasExplicitModel &&
|
|
2298
2328
|
!model &&
|
|
2299
2329
|
defaultRoleSpec.model &&
|
|
2300
|
-
!
|
|
2330
|
+
!isCredentialPinBlocking(defaultRoleSpec.model.provider) &&
|
|
2301
2331
|
(!preferredCredentialProvider || defaultRoleSpec.model.provider === preferredCredentialProvider)
|
|
2302
2332
|
) {
|
|
2303
2333
|
const settingsDefaultModel = defaultRoleSpec.model;
|
|
@@ -4236,8 +4266,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
4236
4266
|
{
|
|
4237
4267
|
managedFallback: defaultModelEntries.length > 1,
|
|
4238
4268
|
canonicalSessionId: providerSessionId,
|
|
4239
|
-
isCredentialUnavailable:
|
|
4240
|
-
authStorage.hasSessionCredentialUnavailable(provider, credentialSessionId),
|
|
4269
|
+
isCredentialUnavailable: isCredentialPinBlocking,
|
|
4241
4270
|
...(persistedProfileOwnsDefault ? { aliasIntent: "preset-equivalent" as const } : {}),
|
|
4242
4271
|
},
|
|
4243
4272
|
);
|