@gajae-code/coding-agent 0.12.7 → 0.12.10
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 +48 -0
- package/dist/types/cli/notify-cli.d.ts +1 -0
- package/dist/types/commands/notify.d.ts +4 -0
- package/dist/types/commands/sdk.d.ts +23 -0
- package/dist/types/config/file-lock.d.ts +3 -0
- package/dist/types/config/model-discovery-manager.d.ts +6 -1
- package/dist/types/config/model-registry.d.ts +2 -0
- package/dist/types/config/resolve-config-value.d.ts +2 -1
- package/dist/types/config/settings-schema.d.ts +17 -1
- package/dist/types/discovery/agents-md.d.ts +11 -1
- package/dist/types/main.d.ts +1 -0
- package/dist/types/modes/components/hook-selector.d.ts +20 -0
- package/dist/types/modes/components/notifications-settings-editor.d.ts +53 -4
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +4 -2
- package/dist/types/modes/types.d.ts +6 -0
- package/dist/types/sdk/broker/broker.d.ts +5 -1
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +1 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +11 -1
- package/dist/types/sdk/bus/config.d.ts +75 -113
- package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
- package/dist/types/sdk/bus/discord-live-provider.d.ts +8 -2
- package/dist/types/sdk/bus/discord-provider.d.ts +19 -0
- package/dist/types/sdk/bus/index.d.ts +1 -1
- package/dist/types/sdk/bus/notification-orchestration.d.ts +127 -18
- package/dist/types/sdk/bus/notification-service.d.ts +54 -4
- package/dist/types/sdk/bus/session-control.d.ts +8 -3
- package/dist/types/sdk/bus/slack-live-provider.d.ts +9 -2
- package/dist/types/sdk/bus/slack-provider.d.ts +22 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +8 -1
- package/dist/types/sdk/client/discovery.d.ts +2 -1
- package/dist/types/sdk/host/query/handlers.d.ts +2 -0
- package/dist/types/sdk/index.d.ts +1 -0
- package/dist/types/sdk/providers.d.ts +22 -0
- package/dist/types/sdk/session-directory.d.ts +1 -1
- package/dist/types/session/internal/managed-session-scope.d.ts +17 -1
- package/dist/types/session/internal/managed-session-storage.d.ts +19 -1
- package/dist/types/session/internal/native-publish-outcome.d.ts +3 -2
- package/dist/types/session/internal/session-open-errors.d.ts +5 -0
- package/dist/types/session/session-manager.d.ts +3 -1
- package/dist/types/session/session-storage.d.ts +2 -0
- package/dist/types/system-prompt.d.ts +12 -1
- package/dist/types/tools/composer-bash-policy.d.ts +9 -1
- package/package.json +10 -8
- package/scripts/build-sdk-package-smoke.ts +51 -0
- package/scripts/capture-notifications-settings-showcase.ts +1 -1
- package/scripts/verify-gjc-sdk-canonicalization.ts +14 -1
- package/src/cli/notify-cli.ts +215 -99
- package/src/commands/notify.ts +19 -8
- package/src/commands/sdk.ts +54 -0
- package/src/config/file-lock.ts +6 -0
- package/src/config/model-discovery-manager.ts +50 -8
- package/src/config/model-profiles.ts +8 -0
- package/src/config/model-registry.ts +866 -76
- package/src/config/resolve-config-value.ts +10 -8
- package/src/config/settings-schema.ts +25 -15
- package/src/config/settings.ts +15 -15
- package/src/discovery/agents-md.ts +76 -19
- package/src/gjc-runtime/psmux-detect.ts +12 -16
- package/src/gjc-runtime/session-state-sidecar.ts +12 -2
- package/src/gjc-runtime/tmux-sessions.ts +8 -1
- package/src/internal-urls/docs-index.generated.ts +10 -9
- package/src/main.ts +43 -7
- package/src/modes/DESIGN.md +11 -11
- package/src/modes/components/extensions/inspector-panel.ts +9 -5
- package/src/modes/components/hook-selector.ts +155 -44
- package/src/modes/components/model-selector.ts +242 -49
- package/src/modes/components/notifications-settings-editor.ts +631 -85
- package/src/modes/controllers/extension-ui-controller.ts +114 -15
- package/src/modes/controllers/selector-controller.ts +525 -93
- package/src/modes/print-mode.ts +3 -0
- package/src/modes/types.ts +6 -0
- package/src/prompts/system/system-prompt.md +1 -0
- package/src/sdk/broker/broker.ts +54 -10
- package/src/sdk/broker/lifecycle-ledger.ts +34 -1
- package/src/sdk/broker/lifecycle.ts +47 -50
- package/src/sdk/bus/chat-daemon-cli.ts +12 -5
- package/src/sdk/bus/chat-daemon-control.ts +35 -6
- package/src/sdk/bus/chat-daemon-runtime.ts +16 -2
- package/src/sdk/bus/config.ts +534 -286
- package/src/sdk/bus/daemon-paths.ts +2 -0
- package/src/sdk/bus/discord-live-provider.ts +57 -2
- package/src/sdk/bus/discord-provider.ts +21 -0
- package/src/sdk/bus/index.ts +97 -35
- package/src/sdk/bus/notification-orchestration.ts +371 -68
- package/src/sdk/bus/notification-service.ts +571 -137
- package/src/sdk/bus/session-control.ts +119 -44
- package/src/sdk/bus/slack-live-provider.ts +78 -2
- package/src/sdk/bus/slack-provider.ts +24 -0
- package/src/sdk/bus/telegram-daemon-cli.ts +9 -2
- package/src/sdk/bus/telegram-daemon-contract.ts +8 -1
- package/src/sdk/bus/telegram-daemon-control.ts +3 -3
- package/src/sdk/bus/telegram-daemon.ts +23 -15
- package/src/sdk/client/discovery.ts +10 -4
- package/src/sdk/host/query/handlers.ts +20 -1
- package/src/sdk/index.ts +1 -0
- package/src/sdk/protocol/operation-inventory.generated.json +18 -0
- package/src/sdk/protocol/operation-registry.ts +5 -2
- package/src/sdk/providers.ts +51 -0
- package/src/sdk/session-directory.ts +1 -0
- package/src/sdk/session.ts +34 -14
- package/src/session/agent-session.ts +109 -37
- package/src/session/internal/managed-session-scope.ts +981 -450
- package/src/session/internal/managed-session-storage.ts +418 -47
- package/src/session/internal/native-publish-outcome.ts +15 -3
- package/src/session/internal/session-open-errors.ts +11 -0
- package/src/session/session-manager.ts +27 -3
- package/src/session/session-storage.ts +72 -5
- package/src/slash-commands/builtin-registry.ts +117 -15
- package/src/system-prompt.ts +28 -15
- package/src/task/index.ts +9 -3
- package/src/tools/ask.ts +1 -1
- package/src/tools/bash.ts +4 -2
- package/src/tools/composer-bash-policy.ts +21 -5
- package/src/tools/telegram-send.ts +4 -2
- package/src/utils/command-args.ts +19 -30
- package/src/utils/lang-from-path.ts +2 -4
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { PROMPT_CLIENT_REF_MAX_LENGTH } from "../../prompt-status.js";
|
|
2
|
+
import type { ActiveProviderDescriptor } from "../../providers.js";
|
|
3
|
+
import { ActiveProviderResolutionError } from "../../providers.js";
|
|
2
4
|
import {
|
|
3
5
|
assertCursorSelector,
|
|
4
6
|
type CursorEnvelope,
|
|
@@ -22,6 +24,7 @@ export interface SessionSurface {
|
|
|
22
24
|
getUsage(): unknown | Promise<unknown>;
|
|
23
25
|
getModels(): unknown | Promise<unknown>;
|
|
24
26
|
getSkillState(): unknown | Promise<unknown>;
|
|
27
|
+
getActiveProviders?(): ActiveProviderDescriptor[] | Promise<ActiveProviderDescriptor[]>;
|
|
25
28
|
/** Q12 rows preserve workflow gate fields and include stable durable gate metadata. */
|
|
26
29
|
getGates(): unknown | Promise<unknown>;
|
|
27
30
|
getConfigItems(): unknown | Promise<unknown>;
|
|
@@ -102,6 +105,7 @@ const sources: Record<string, { resource: string; method: keyof SessionSurface;
|
|
|
102
105
|
Q21: { resource: "queue", method: "getQueueMessages", mvcc: true },
|
|
103
106
|
Q22: { resource: "extensions", method: "getExtensions", mvcc: true },
|
|
104
107
|
Q25: { resource: "jobs", method: "getJobs", mvcc: false },
|
|
108
|
+
Q29: { resource: "activeProviders", method: "getActiveProviders", mvcc: false },
|
|
105
109
|
Q27: { resource: "modelProfiles", method: "getModelProfiles", mvcc: true },
|
|
106
110
|
};
|
|
107
111
|
const names = [
|
|
@@ -133,6 +137,7 @@ const names = [
|
|
|
133
137
|
"turn.prompt_status",
|
|
134
138
|
"models.profiles.list",
|
|
135
139
|
"skill.invoke_status",
|
|
140
|
+
"providers.list/active",
|
|
136
141
|
];
|
|
137
142
|
|
|
138
143
|
export class QueryHandlers {
|
|
@@ -168,6 +173,15 @@ export class QueryHandlers {
|
|
|
168
173
|
return this.#error(request, "invalid_request", false, "models.profiles.list does not accept input fields.");
|
|
169
174
|
if (query === "Q27" && typeof this.surface.getModelProfiles !== "function")
|
|
170
175
|
return this.#error(request, "unavailable", false, "models.profiles.list is unavailable for this session.");
|
|
176
|
+
if (query === "Q29" && request.input && Object.keys(request.input).length > 0)
|
|
177
|
+
return this.#error(
|
|
178
|
+
request,
|
|
179
|
+
"invalid_request",
|
|
180
|
+
false,
|
|
181
|
+
"providers.list/active does not accept input fields.",
|
|
182
|
+
);
|
|
183
|
+
if (query === "Q29" && typeof this.surface.getActiveProviders !== "function")
|
|
184
|
+
return this.#error(request, "unavailable", false, "providers.list/active is unavailable for this session.");
|
|
171
185
|
const source = sources[query];
|
|
172
186
|
if (!source) return this.#error(request, "invalid_request");
|
|
173
187
|
return await this.#pageSource(request, query, source);
|
|
@@ -257,7 +271,12 @@ export class QueryHandlers {
|
|
|
257
271
|
source.resource === "transcript" ? { highWatermark: cursor.highWatermark } : {},
|
|
258
272
|
);
|
|
259
273
|
} else {
|
|
260
|
-
|
|
274
|
+
try {
|
|
275
|
+
snapshot = await (this.surface[source.method] as () => unknown)();
|
|
276
|
+
} catch (error) {
|
|
277
|
+
if (queryId === "Q29") throw new ActiveProviderResolutionError();
|
|
278
|
+
throw error;
|
|
279
|
+
}
|
|
261
280
|
revision = await this.revisions.createRevision(source.resource, resourceId, snapshot);
|
|
262
281
|
}
|
|
263
282
|
if (snapshot === undefined) return this.#error(request, "resource_gone");
|
package/src/sdk/index.ts
CHANGED
|
@@ -1583,6 +1583,24 @@
|
|
|
1583
1583
|
"packages/coding-agent/test/sdk-operation-inventory.test.ts"
|
|
1584
1584
|
]
|
|
1585
1585
|
},
|
|
1586
|
+
{
|
|
1587
|
+
"sourceId": "registry:Q29",
|
|
1588
|
+
"sourceFile": "packages/coding-agent/src/sdk/protocol/operation-registry.ts",
|
|
1589
|
+
"sourceKind": "registry",
|
|
1590
|
+
"decision": "include",
|
|
1591
|
+
"sdkId": "providers.list/active",
|
|
1592
|
+
"adapterMappings": {
|
|
1593
|
+
"telegram": "prohibited",
|
|
1594
|
+
"discord": "prohibited",
|
|
1595
|
+
"slack": "prohibited",
|
|
1596
|
+
"mcp": "generic_safe",
|
|
1597
|
+
"acp": "generic_safe",
|
|
1598
|
+
"daemonCli": "generic_safe"
|
|
1599
|
+
},
|
|
1600
|
+
"testIds": [
|
|
1601
|
+
"packages/coding-agent/test/sdk-operation-inventory.test.ts"
|
|
1602
|
+
]
|
|
1603
|
+
},
|
|
1586
1604
|
{
|
|
1587
1605
|
"sourceId": "registry:R01",
|
|
1588
1606
|
"sourceFile": "packages/coding-agent/src/sdk/protocol/operation-registry.ts",
|
|
@@ -164,6 +164,7 @@ const queries = [
|
|
|
164
164
|
"skill.invoke_status",
|
|
165
165
|
"Read the authoritative reconciliation status of a skill.invoke by command/turn IDs or clientRef.",
|
|
166
166
|
],
|
|
167
|
+
["providers.list/active", "List active providers."],
|
|
167
168
|
] as const;
|
|
168
169
|
|
|
169
170
|
const reverse = [
|
|
@@ -243,7 +244,7 @@ function queryContinuityClass(id: string): QueryContinuityClass {
|
|
|
243
244
|
}
|
|
244
245
|
|
|
245
246
|
function queryDisposition(id: string): Record<Adapter, AdapterDisposition> {
|
|
246
|
-
if (["Q23", "Q24", "Q25", "Q26", "Q27", "Q28"].includes(id))
|
|
247
|
+
if (["Q23", "Q24", "Q25", "Q26", "Q27", "Q28", "Q29"].includes(id))
|
|
247
248
|
return dispositions({ telegram: "prohibited", discord: "prohibited", slack: "prohibited" });
|
|
248
249
|
return dispositions();
|
|
249
250
|
}
|
|
@@ -306,7 +307,9 @@ export const OPERATIONS: readonly Operation[] = [
|
|
|
306
307
|
errorCodes:
|
|
307
308
|
id === "Q27"
|
|
308
309
|
? ["invalid_request", "resource_gone", "model_profile_registry_error"]
|
|
309
|
-
:
|
|
310
|
+
: id === "Q29"
|
|
311
|
+
? ["invalid_request", "resource_gone", "internal"]
|
|
312
|
+
: ["invalid_request", "resource_gone"],
|
|
310
313
|
continuityClass: queryContinuityClass(id),
|
|
311
314
|
adapterDispositions: queryDisposition(id),
|
|
312
315
|
testIds: ["packages/coding-agent/test/sdk-operation-inventory.test.ts"],
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export type ActiveProviderConnectionKind = "credential" | "credentialless";
|
|
2
|
+
|
|
3
|
+
export interface ActiveProviderDescriptor {
|
|
4
|
+
provider: string;
|
|
5
|
+
connectionKind: ActiveProviderConnectionKind;
|
|
6
|
+
}
|
|
7
|
+
function compareProviderIdsByUtf8(left: string, right: string): number {
|
|
8
|
+
const leftBytes = new TextEncoder().encode(left);
|
|
9
|
+
const rightBytes = new TextEncoder().encode(right);
|
|
10
|
+
const length = Math.min(leftBytes.length, rightBytes.length);
|
|
11
|
+
for (let index = 0; index < length; index += 1) {
|
|
12
|
+
const difference = leftBytes[index]! - rightBytes[index]!;
|
|
13
|
+
if (difference !== 0) return difference;
|
|
14
|
+
}
|
|
15
|
+
return leftBytes.length - rightBytes.length;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Project active-provider inputs to the exact public DTO shape.
|
|
20
|
+
*
|
|
21
|
+
* Connection credentials take precedence when the same provider is reported
|
|
22
|
+
* more than once. Provider IDs are sorted by their UTF-8 bytes without
|
|
23
|
+
* normalization.
|
|
24
|
+
*/
|
|
25
|
+
export function projectActiveProviderDescriptors(
|
|
26
|
+
descriptors: readonly { provider: string; connectionKind: unknown }[],
|
|
27
|
+
): ActiveProviderDescriptor[] {
|
|
28
|
+
const active = new Map<string, ActiveProviderConnectionKind>();
|
|
29
|
+
for (const descriptor of descriptors) {
|
|
30
|
+
if (descriptor.connectionKind !== "credential" && descriptor.connectionKind !== "credentialless") {
|
|
31
|
+
throw new Error("Invalid active provider connection kind.");
|
|
32
|
+
}
|
|
33
|
+
if (active.get(descriptor.provider) === "credential") continue;
|
|
34
|
+
active.set(descriptor.provider, descriptor.connectionKind);
|
|
35
|
+
}
|
|
36
|
+
return [...active.entries()]
|
|
37
|
+
.sort(([left], [right]) => compareProviderIdsByUtf8(left, right))
|
|
38
|
+
.map(([provider, connectionKind]) => ({ provider, connectionKind }));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const ACTIVE_PROVIDER_RESOLUTION_ERROR_CODE = "internal" as const;
|
|
42
|
+
export const ACTIVE_PROVIDER_RESOLUTION_ERROR_MESSAGE = "Unable to resolve active providers.";
|
|
43
|
+
|
|
44
|
+
export class ActiveProviderResolutionError extends Error {
|
|
45
|
+
readonly code = ACTIVE_PROVIDER_RESOLUTION_ERROR_CODE;
|
|
46
|
+
|
|
47
|
+
constructor() {
|
|
48
|
+
super(ACTIVE_PROVIDER_RESOLUTION_ERROR_MESSAGE);
|
|
49
|
+
this.name = "ActiveProviderResolutionError";
|
|
50
|
+
}
|
|
51
|
+
}
|
package/src/sdk/session.ts
CHANGED
|
@@ -108,10 +108,10 @@ import { MCPManager } from "../runtime-mcp";
|
|
|
108
108
|
import { createNotificationsExtension } from "../sdk/bus";
|
|
109
109
|
import {
|
|
110
110
|
getNotificationConfig,
|
|
111
|
-
|
|
111
|
+
isGenericNotificationHostEligible,
|
|
112
112
|
type NotificationConfig,
|
|
113
113
|
SPAWN_PROVENANCE_ENV,
|
|
114
|
-
|
|
114
|
+
shouldRegisterGenericNotificationsExtension,
|
|
115
115
|
} from "../sdk/bus/config";
|
|
116
116
|
import { NotificationSessionController } from "../sdk/bus/session-control";
|
|
117
117
|
import { shouldHostSdk } from "../sdk/host";
|
|
@@ -136,6 +136,7 @@ import {
|
|
|
136
136
|
buildSystemPrompt as buildSystemPromptInternal,
|
|
137
137
|
buildSystemPromptToolMetadata,
|
|
138
138
|
loadProjectContextFiles as loadContextFilesInternal,
|
|
139
|
+
loadProjectContextFilesResult as loadContextFilesResultInternal,
|
|
139
140
|
} from "../system-prompt";
|
|
140
141
|
import { AgentOutputManager } from "../task/output-manager";
|
|
141
142
|
import { parseThinkingLevel, resolveThinkingLevelForModel, toReasoningEffort } from "../thinking";
|
|
@@ -184,6 +185,7 @@ import {
|
|
|
184
185
|
|
|
185
186
|
type AsyncResultEntry = {
|
|
186
187
|
jobId: string;
|
|
188
|
+
generation: string;
|
|
187
189
|
result: string;
|
|
188
190
|
job: AsyncJob | undefined;
|
|
189
191
|
durationMs: number | undefined;
|
|
@@ -1165,10 +1167,10 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1165
1167
|
// Independent discoveries that depend only on cwd/agentDir — kicked off in parallel and awaited
|
|
1166
1168
|
// at their respective consumer sites. Their work can overlap with model resolution, secret loading,
|
|
1167
1169
|
// session-context build, tool creation, MCP discovery, and extension discovery.
|
|
1168
|
-
const
|
|
1169
|
-
? Promise.resolve(options.contextFiles)
|
|
1170
|
-
: logger.time("discoverContextFiles",
|
|
1171
|
-
|
|
1170
|
+
const contextFilesResultPromise = options.contextFiles
|
|
1171
|
+
? Promise.resolve({ contextFiles: options.contextFiles, warnings: [] })
|
|
1172
|
+
: logger.time("discoverContextFiles", loadContextFilesResultInternal, { cwd });
|
|
1173
|
+
contextFilesResultPromise.catch(() => {});
|
|
1172
1174
|
const promptTemplatesPromise = options.promptTemplates
|
|
1173
1175
|
? Promise.resolve(options.promptTemplates)
|
|
1174
1176
|
: logger.time("discoverPromptTemplates", discoverPromptTemplates, cwd, agentDir);
|
|
@@ -1464,10 +1466,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1464
1466
|
}
|
|
1465
1467
|
return result;
|
|
1466
1468
|
};
|
|
1467
|
-
const [
|
|
1468
|
-
|
|
1469
|
+
const [contextFilesResult, resolvedWorkspaceTree] = await Promise.all([
|
|
1470
|
+
contextFilesResultPromise,
|
|
1469
1471
|
raceWithDeadline("buildWorkspaceTree", workspaceTreePromise),
|
|
1470
1472
|
]);
|
|
1473
|
+
const contextFiles = contextFilesResult.contextFiles;
|
|
1474
|
+
const discoveredContextFileWarnings = contextFilesResult.warnings;
|
|
1471
1475
|
|
|
1472
1476
|
const backgroundJobsEnabled = isBackgroundJobSupportEnabled(settings);
|
|
1473
1477
|
const asyncMaxJobs = Math.min(100, Math.max(1, settings.get("async.maxJobs") ?? 100));
|
|
@@ -1502,13 +1506,14 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1502
1506
|
? new AsyncJobManager({
|
|
1503
1507
|
maxRunningJobs: asyncMaxJobs,
|
|
1504
1508
|
onJobComplete: async (jobId, result, job) => {
|
|
1505
|
-
if (!session
|
|
1509
|
+
if (!session) return;
|
|
1506
1510
|
const formattedResult = await formatAsyncResultForFollowUp(result);
|
|
1507
|
-
if (asyncJobManager!.isDeliverySuppressed(jobId)) return;
|
|
1511
|
+
if (asyncJobManager!.isDeliverySuppressed(jobId, job?.generation)) return;
|
|
1508
1512
|
|
|
1509
1513
|
const durationMs = job ? jobElapsedMs(job) : undefined;
|
|
1510
1514
|
session.yieldQueue.enqueue<AsyncResultEntry>("async-result", {
|
|
1511
1515
|
jobId,
|
|
1516
|
+
generation: job?.generation ?? "",
|
|
1512
1517
|
result: formattedResult,
|
|
1513
1518
|
job,
|
|
1514
1519
|
durationMs,
|
|
@@ -1907,7 +1912,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1907
1912
|
const spawnProvenance = process.env[SPAWN_PROVENANCE_ENV];
|
|
1908
1913
|
const spawnedByGjc = typeof spawnProvenance === "string" && spawnProvenance.trim().length > 0;
|
|
1909
1914
|
delete process.env[SPAWN_PROVENANCE_ENV];
|
|
1910
|
-
const notificationHostEligible =
|
|
1915
|
+
const notificationHostEligible = isGenericNotificationHostEligible({
|
|
1911
1916
|
env: process.env,
|
|
1912
1917
|
hostModeSupported: options.notificationHostModeSupported ?? true,
|
|
1913
1918
|
taskDepth,
|
|
@@ -1919,10 +1924,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1919
1924
|
const notificationSessionController = new NotificationSessionController({
|
|
1920
1925
|
eligible: notificationHostEligible,
|
|
1921
1926
|
getConfig: () => getNotificationConfig(settings),
|
|
1927
|
+
spawnedByGjc,
|
|
1922
1928
|
});
|
|
1923
1929
|
if (
|
|
1924
1930
|
lifecycleStartupCapability ||
|
|
1925
|
-
|
|
1931
|
+
shouldRegisterGenericNotificationsExtension({
|
|
1926
1932
|
env: process.env,
|
|
1927
1933
|
cfg: notificationCfg,
|
|
1928
1934
|
taskDepth,
|
|
@@ -2230,6 +2236,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2230
2236
|
settings.get("tools.intentTracing"),
|
|
2231
2237
|
options.hasUI ?? false,
|
|
2232
2238
|
);
|
|
2239
|
+
const emittedContextFileWarnings = new Set(discoveredContextFileWarnings);
|
|
2240
|
+
const contextFileWarnings = [...discoveredContextFileWarnings];
|
|
2241
|
+
for (const warning of discoveredContextFileWarnings) {
|
|
2242
|
+
logger.warn("Context file discovery warning", { warning });
|
|
2243
|
+
}
|
|
2233
2244
|
const intentField = intentTracingEnabled ? INTENT_FIELD : undefined;
|
|
2234
2245
|
const rebuildSystemPrompt = async (
|
|
2235
2246
|
toolNames: string[],
|
|
@@ -2308,14 +2319,22 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2308
2319
|
subagent: options.parentTaskPrefix !== undefined,
|
|
2309
2320
|
});
|
|
2310
2321
|
|
|
2322
|
+
for (const warning of defaultPrompt.warnings) {
|
|
2323
|
+
if (emittedContextFileWarnings.has(warning)) continue;
|
|
2324
|
+
emittedContextFileWarnings.add(warning);
|
|
2325
|
+
contextFileWarnings.push(warning);
|
|
2326
|
+
if (hasSession) session.configWarnings.push(warning);
|
|
2327
|
+
logger.warn("Context file discovery warning", { warning });
|
|
2328
|
+
}
|
|
2311
2329
|
if (options.systemPrompt === undefined) {
|
|
2312
2330
|
return defaultPrompt;
|
|
2313
2331
|
}
|
|
2314
2332
|
if (Array.isArray(options.systemPrompt)) {
|
|
2315
|
-
return { systemPrompt: options.systemPrompt };
|
|
2333
|
+
return { systemPrompt: options.systemPrompt, warnings: defaultPrompt.warnings };
|
|
2316
2334
|
}
|
|
2317
2335
|
return {
|
|
2318
2336
|
systemPrompt: options.systemPrompt(defaultPrompt.systemPrompt),
|
|
2337
|
+
warnings: defaultPrompt.warnings,
|
|
2319
2338
|
};
|
|
2320
2339
|
};
|
|
2321
2340
|
|
|
@@ -2794,10 +2813,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2794
2813
|
forkContextSeed: options.forkContextSeed,
|
|
2795
2814
|
providerSessionState: options.providerSessionState,
|
|
2796
2815
|
});
|
|
2816
|
+
session.configWarnings.push(...contextFileWarnings);
|
|
2797
2817
|
hasSession = true;
|
|
2798
2818
|
if (asyncJobManager) {
|
|
2799
2819
|
session.yieldQueue.register<AsyncResultEntry>("async-result", {
|
|
2800
|
-
isStale: entry => asyncJobManager.isDeliverySuppressed(entry.jobId),
|
|
2820
|
+
isStale: entry => asyncJobManager.isDeliverySuppressed(entry.jobId, entry.generation),
|
|
2801
2821
|
build: buildAsyncResultBatchMessage,
|
|
2802
2822
|
});
|
|
2803
2823
|
}
|
|
@@ -109,6 +109,7 @@ import {
|
|
|
109
109
|
streamSimple,
|
|
110
110
|
} from "@gajae-code/ai";
|
|
111
111
|
import {
|
|
112
|
+
type AuthDisposition,
|
|
112
113
|
beginAttempt,
|
|
113
114
|
classifyFallbackTrigger,
|
|
114
115
|
type FallbackAttemptToken,
|
|
@@ -1869,13 +1870,6 @@ export class AgentSession {
|
|
|
1869
1870
|
this.#retryReplayEpoch++;
|
|
1870
1871
|
this.#retryReplayUnsafeEpoch = undefined;
|
|
1871
1872
|
this.#firstEventTimeoutRetryStartedAt = Date.now();
|
|
1872
|
-
if (
|
|
1873
|
-
this.#extensionRunner?.hasHandlers("context") ||
|
|
1874
|
-
this.#extensionRunner?.hasHandlers("before_provider_request") ||
|
|
1875
|
-
this.#extensionRunner?.hasHandlers("after_provider_response")
|
|
1876
|
-
) {
|
|
1877
|
-
this.#markRetryReplayUnsafe();
|
|
1878
|
-
}
|
|
1879
1873
|
}
|
|
1880
1874
|
|
|
1881
1875
|
#markRetryReplayUnsafe(): void {
|
|
@@ -1885,6 +1879,14 @@ export class AgentSession {
|
|
|
1885
1879
|
get #hasCleanRetryReplaySafety(): boolean {
|
|
1886
1880
|
return this.#retryReplayEpoch > 0 && this.#retryReplayUnsafeEpoch !== this.#retryReplayEpoch;
|
|
1887
1881
|
}
|
|
1882
|
+
#bindAttemptScope(scope: AttemptScope | undefined): void {
|
|
1883
|
+
if (!scope) return;
|
|
1884
|
+
this.#attemptRecordStore.register(scope);
|
|
1885
|
+
this.#attemptRecordStore.establishClean(scope);
|
|
1886
|
+
}
|
|
1887
|
+
#isRetryScopeClean(scope: AttemptScope | undefined): boolean {
|
|
1888
|
+
return scope !== undefined && this.#attemptRecordStore.isClean(scope);
|
|
1889
|
+
}
|
|
1888
1890
|
#prePromptContextCheckPromise: Promise<void> | undefined = undefined;
|
|
1889
1891
|
/** Display-only context snapshot; pre-prompt compaction estimates deliberately remain uncached. */
|
|
1890
1892
|
#contextUsageCache: { key: string; value: ContextUsage } | undefined;
|
|
@@ -1898,6 +1900,8 @@ export class AgentSession {
|
|
|
1898
1900
|
// Handoff state
|
|
1899
1901
|
#handoffAbortController: AbortController | undefined = undefined;
|
|
1900
1902
|
#skipPostTurnMaintenanceAssistantTimestamp: number | undefined = undefined;
|
|
1903
|
+
/** Scope cleanliness captured after message_end extension delivery and before terminal agent_end retirement. */
|
|
1904
|
+
#assistantAttemptScopes = new WeakMap<AssistantMessage, { scope?: AttemptScope; wasClean: boolean }>();
|
|
1901
1905
|
|
|
1902
1906
|
// Retry state
|
|
1903
1907
|
#retryAbortController: AbortController | undefined = undefined;
|
|
@@ -2606,6 +2610,7 @@ export class AgentSession {
|
|
|
2606
2610
|
if (this.#extensionRunner && typeof this.#extensionRunner.setAttemptRecordStore === "function") {
|
|
2607
2611
|
this.#extensionRunner.setAttemptRecordStore(this.#attemptRecordStore);
|
|
2608
2612
|
}
|
|
2613
|
+
this.agent.setMainAttemptScopeObserver(scope => this.#bindAttemptScope(scope));
|
|
2609
2614
|
this.#skills = config.skills ?? [];
|
|
2610
2615
|
this.#skillWarnings = config.skillWarnings ?? [];
|
|
2611
2616
|
this.#customCommands = config.customCommands ?? [];
|
|
@@ -3739,11 +3744,7 @@ export class AgentSession {
|
|
|
3739
3744
|
/** Internal handler for agent events - shared by subscribe and reconnect */
|
|
3740
3745
|
#handleAgentEvent = async (event: AgentEvent, activePromptHandle?: string): Promise<void> => {
|
|
3741
3746
|
const attemptScope = (event as AgentEvent & { scope?: AttemptScope }).scope;
|
|
3742
|
-
|
|
3743
|
-
this.#attemptRecordStore.register(attemptScope);
|
|
3744
|
-
this.#attemptRecordStore.establishClean(attemptScope);
|
|
3745
|
-
}
|
|
3746
|
-
if (this.#extensionRunner?.hasHandlers(event.type)) this.#markRetryReplayUnsafe();
|
|
3747
|
+
|
|
3747
3748
|
if (
|
|
3748
3749
|
event.type === "tool_execution_start" ||
|
|
3749
3750
|
event.type === "tool_execution_update" ||
|
|
@@ -3947,6 +3948,12 @@ export class AgentSession {
|
|
|
3947
3948
|
}
|
|
3948
3949
|
|
|
3949
3950
|
await this.#emitSessionEvent(displayEvent);
|
|
3951
|
+
if (event.type === "message_end" && event.message.role === "assistant") {
|
|
3952
|
+
this.#assistantAttemptScopes.set(event.message, {
|
|
3953
|
+
scope: attemptScope,
|
|
3954
|
+
wasClean: this.#isRetryScopeClean(attemptScope),
|
|
3955
|
+
});
|
|
3956
|
+
}
|
|
3950
3957
|
if (
|
|
3951
3958
|
displayEvent !== event &&
|
|
3952
3959
|
displayEvent.type === "message_end" &&
|
|
@@ -4321,7 +4328,14 @@ export class AgentSession {
|
|
|
4321
4328
|
if (this.#isRetryableError(msg)) {
|
|
4322
4329
|
const transportFailure = (msg as AssistantMessage & { transportFailure?: TransportFailureFacts })
|
|
4323
4330
|
.transportFailure;
|
|
4324
|
-
const
|
|
4331
|
+
const messageScope = this.#assistantAttemptScopes.get(msg);
|
|
4332
|
+
const didRetry = await this.#handleRetryableError(
|
|
4333
|
+
msg,
|
|
4334
|
+
false,
|
|
4335
|
+
transportFailure,
|
|
4336
|
+
messageScope?.scope ?? event.scope,
|
|
4337
|
+
messageScope?.wasClean ?? false,
|
|
4338
|
+
);
|
|
4325
4339
|
if (didRetry) return; // Retry was initiated, don't proceed to compaction
|
|
4326
4340
|
}
|
|
4327
4341
|
if (this.#retryAttempt > 0) {
|
|
@@ -5838,6 +5852,7 @@ export class AgentSession {
|
|
|
5838
5852
|
this.#abortActiveMidRunBarriers();
|
|
5839
5853
|
this.abortCompaction();
|
|
5840
5854
|
this.agent.abort();
|
|
5855
|
+
this.agent.setMainAttemptScopeObserver(undefined);
|
|
5841
5856
|
// Disconnect the Agent event bridge NOW — before the maintenance join and the
|
|
5842
5857
|
// bounded idle / forceAbort below — so no agent_end emitted during teardown
|
|
5843
5858
|
// (including the one forceAbort emits) can re-enter #handleAgentEvent and start
|
|
@@ -14214,6 +14229,12 @@ export class AgentSession {
|
|
|
14214
14229
|
);
|
|
14215
14230
|
}
|
|
14216
14231
|
const trigger = classifyFallbackTrigger(transportFailure ?? { status: message.errorStatus });
|
|
14232
|
+
// A plain `forbidden` is terminal. Without this, the blanket
|
|
14233
|
+
// `transportFailure` admission below re-admits a committed forbidden
|
|
14234
|
+
// failure into retry policy: credential mutation is vetoed downstream, but
|
|
14235
|
+
// the chain would still retry and advance models on a request the caller is
|
|
14236
|
+
// simply not authorized to make.
|
|
14237
|
+
if (trigger.class === "auth" && trigger.authDisposition === "forbidden") return false;
|
|
14217
14238
|
if (transportFailure) return true;
|
|
14218
14239
|
if (
|
|
14219
14240
|
trigger.class === "rate_limit" ||
|
|
@@ -14573,6 +14594,8 @@ export class AgentSession {
|
|
|
14573
14594
|
outcome.failure.message,
|
|
14574
14595
|
true,
|
|
14575
14596
|
outcome.failure.transportFailure,
|
|
14597
|
+
outcome.scope,
|
|
14598
|
+
this.#isRetryScopeClean(outcome.scope),
|
|
14576
14599
|
) as Promise<ManagedAttemptDecision>;
|
|
14577
14600
|
}
|
|
14578
14601
|
|
|
@@ -14600,7 +14623,10 @@ export class AgentSession {
|
|
|
14600
14623
|
message: AssistantMessage,
|
|
14601
14624
|
allowLegacyUsageLimit: boolean,
|
|
14602
14625
|
transportFailure?: TransportFailureFacts,
|
|
14603
|
-
|
|
14626
|
+
// `authDisposition` is carried through from `classifyFallbackTrigger`, whose
|
|
14627
|
+
// result is returned verbatim below. Narrowing it away here would silently
|
|
14628
|
+
// disable the terminal-403 guard in `#markFailedCredential`.
|
|
14629
|
+
): { class: FallbackTriggerClass; retryAfterMs?: number; authDisposition?: AuthDisposition } | undefined {
|
|
14604
14630
|
if (classifyContextOverflow(message, transportFailure, this.model?.contextWindow ?? 0)) return undefined;
|
|
14605
14631
|
const transport = classifyFallbackTrigger(transportFailure ?? { status: message.errorStatus });
|
|
14606
14632
|
if (transport.class !== "other") return transport;
|
|
@@ -14707,25 +14733,60 @@ export class AgentSession {
|
|
|
14707
14733
|
return `Model fallback chain exhausted; models tried: ${tried}; models skipped: ${skipped}`;
|
|
14708
14734
|
}
|
|
14709
14735
|
|
|
14710
|
-
|
|
14736
|
+
/**
|
|
14737
|
+
* Marks the credential that just failed and reports whether the session
|
|
14738
|
+
* actually moved to a DIFFERENT stored credential.
|
|
14739
|
+
*
|
|
14740
|
+
* Three invariants this enforces, in order:
|
|
14741
|
+
*
|
|
14742
|
+
* 1. **Pin guard, first and for every trigger class.** A pinned credential
|
|
14743
|
+
* (`--api-key` or `--credential`) must never be mutated or rotated away
|
|
14744
|
+
* from. Both overrides are consulted: they live in separate maps, so
|
|
14745
|
+
* checking only `hasRuntimeApiKey` would silently rotate a `--credential`
|
|
14746
|
+
* pin. Previously the guard existed only on the quota path, so the `auth`
|
|
14747
|
+
* path invalidated pinned credentials outright.
|
|
14748
|
+
* 2. **A terminal `forbidden` never mutates credential state.** Rotation
|
|
14749
|
+
* would hide an authorization defect and cycle through healthy rows.
|
|
14750
|
+
* 3. **Distinct-row proof in BOTH branches.** `invalidateCredentialMatching`
|
|
14751
|
+
* reports "I matched and blocked a row", which is not the same as "the
|
|
14752
|
+
* session now uses a different credential" — with a single-row pool it is
|
|
14753
|
+
* true while nothing rotated. Both branches therefore re-resolve and
|
|
14754
|
+
* require the active key to have actually changed.
|
|
14755
|
+
*/
|
|
14756
|
+
async #markFailedCredential(trigger: {
|
|
14757
|
+
class: FallbackTriggerClass;
|
|
14758
|
+
retryAfterMs?: number;
|
|
14759
|
+
authDisposition?: AuthDisposition;
|
|
14760
|
+
}): Promise<boolean> {
|
|
14711
14761
|
if (!this.model || (trigger.class !== "auth" && trigger.class !== "quota" && trigger.class !== "rate_limit")) {
|
|
14712
14762
|
return false;
|
|
14713
14763
|
}
|
|
14764
|
+
// (2) Terminal forbidden: no credential state may change.
|
|
14765
|
+
if (trigger.class === "auth" && trigger.authDisposition === "forbidden") return false;
|
|
14766
|
+
|
|
14714
14767
|
const authStorage = this.#modelRegistry.authStorage;
|
|
14768
|
+
const provider = this.model.provider;
|
|
14769
|
+
// (1) Pin guard, before any mutation and for every branch.
|
|
14770
|
+
if (authStorage.hasRuntimeApiKey(provider) || authStorage.hasRuntimeCredentialSelector(provider)) return false;
|
|
14771
|
+
|
|
14715
14772
|
const providerAffinitySessionId = this.agent.providerSessionId ?? this.agent.sessionId ?? this.sessionId;
|
|
14773
|
+
const activeApiKey = await this.#modelRegistry.getApiKey(this.model, providerAffinitySessionId);
|
|
14774
|
+
|
|
14775
|
+
let mutated: boolean;
|
|
14716
14776
|
if (trigger.class === "auth") {
|
|
14717
|
-
|
|
14718
|
-
|
|
14719
|
-
return authStorage.invalidateCredentialMatching(this.model.provider, apiKey, {
|
|
14777
|
+
if (!isAuthenticated(activeApiKey)) return false;
|
|
14778
|
+
mutated = await authStorage.invalidateCredentialMatching(provider, activeApiKey, {
|
|
14720
14779
|
sessionId: providerAffinitySessionId,
|
|
14721
14780
|
});
|
|
14781
|
+
} else {
|
|
14782
|
+
mutated = await authStorage.markUsageLimitReached(provider, providerAffinitySessionId, {
|
|
14783
|
+
retryAfterMs: trigger.retryAfterMs,
|
|
14784
|
+
});
|
|
14722
14785
|
}
|
|
14723
|
-
if (
|
|
14724
|
-
|
|
14725
|
-
|
|
14726
|
-
|
|
14727
|
-
});
|
|
14728
|
-
return rotated && (await this.#modelRegistry.getApiKey(this.model, providerAffinitySessionId)) !== activeApiKey;
|
|
14786
|
+
if (!mutated) return false;
|
|
14787
|
+
|
|
14788
|
+
// (3) Distinct-row proof.
|
|
14789
|
+
return (await this.#modelRegistry.getApiKey(this.model, providerAffinitySessionId)) !== activeApiKey;
|
|
14729
14790
|
}
|
|
14730
14791
|
|
|
14731
14792
|
/** Handle retryable errors with exponential backoff. */
|
|
@@ -14733,6 +14794,8 @@ export class AgentSession {
|
|
|
14733
14794
|
message: AssistantMessage,
|
|
14734
14795
|
managedOutcome = false,
|
|
14735
14796
|
transportFailure?: TransportFailureFacts,
|
|
14797
|
+
scope?: AttemptScope,
|
|
14798
|
+
scopeWasClean = this.#isRetryScopeClean(scope),
|
|
14736
14799
|
): Promise<boolean | ManagedAttemptDecision> {
|
|
14737
14800
|
const controller = this.#defaultFallbackChain();
|
|
14738
14801
|
const managedFallback = controller.chain.entries.length > 1;
|
|
@@ -14747,12 +14810,16 @@ export class AgentSession {
|
|
|
14747
14810
|
if (!managedFallback && !retrySettings.enabled) return false;
|
|
14748
14811
|
const classification = this.#classifyErrorForRetry(message);
|
|
14749
14812
|
const firstEventTimeout = classification === "first_event_timeout";
|
|
14750
|
-
if (!managedFallback && firstEventTimeout) {
|
|
14751
|
-
this.#firstEventTimeoutRetryStartedAt
|
|
14813
|
+
if (!managedFallback && firstEventTimeout && this.#retryAttempt === 0) {
|
|
14814
|
+
this.#firstEventTimeoutRetryStartedAt = Date.now();
|
|
14752
14815
|
}
|
|
14753
|
-
|
|
14754
|
-
|
|
14755
|
-
|
|
14816
|
+
const requiresScopedFirstEventTimeout = managedFallback || !legacyRetryConfigured;
|
|
14817
|
+
if (
|
|
14818
|
+
firstEventTimeout &&
|
|
14819
|
+
(assistantMessageHasVisibleOrToolContent(message) ||
|
|
14820
|
+
(this.#retryAttempt > 0 && !this.#hasCleanRetryReplaySafety) ||
|
|
14821
|
+
(requiresScopedFirstEventTimeout && (!this.#isTypedFirstEventTimeout(message) || !scope || !scopeWasClean)))
|
|
14822
|
+
) {
|
|
14756
14823
|
return managedOutcome
|
|
14757
14824
|
? {
|
|
14758
14825
|
type: "terminal",
|
|
@@ -14780,14 +14847,12 @@ export class AgentSession {
|
|
|
14780
14847
|
// before the failure. This bypasses #hasCleanRetryReplaySafety because the
|
|
14781
14848
|
// content-free check is the replay-safety guarantee for credential rotation.
|
|
14782
14849
|
const canReplayRotatedCredential = credentialRotated;
|
|
14783
|
-
// Bare defaults
|
|
14784
|
-
//
|
|
14785
|
-
//
|
|
14850
|
+
// Bare defaults retain their narrow watchdog and Codex admissions. A
|
|
14851
|
+
// first-event timeout adds the typed, content-free, current-clean-scope
|
|
14852
|
+
// requirement above; other transient watchdogs preserve legacy behavior.
|
|
14786
14853
|
if (!managedFallback && !legacyRetryConfigured && !canReplayRotatedCredential) {
|
|
14787
14854
|
const bareDefaultCodexOverload = isBareDefaultCodexOverload(message);
|
|
14788
14855
|
const bareDefaultWrappedFirstEventTimeout = isBareDefaultWrappedFirstEventTimeout(message);
|
|
14789
|
-
// Content-free Codex overload is replay-safe by the same reasoning as
|
|
14790
|
-
// credential rotation: no partial output means no observable state.
|
|
14791
14856
|
const canReplayCodexOverload = bareDefaultCodexOverload;
|
|
14792
14857
|
if (
|
|
14793
14858
|
(!canReplayCodexOverload &&
|
|
@@ -14796,7 +14861,7 @@ export class AgentSession {
|
|
|
14796
14861
|
(hasBareDefaultRetryDisqualifyingFacts(message) ||
|
|
14797
14862
|
(classification !== "transient" && classification !== "first_event_timeout") ||
|
|
14798
14863
|
!BARE_DEFAULT_WATCHDOG_ERROR.test(message.errorMessage ?? ""))) ||
|
|
14799
|
-
(!canReplayCodexOverload && !this.#hasCleanRetryReplaySafety)
|
|
14864
|
+
(!canReplayCodexOverload && !firstEventTimeout && !this.#hasCleanRetryReplaySafety)
|
|
14800
14865
|
) {
|
|
14801
14866
|
return false;
|
|
14802
14867
|
}
|
|
@@ -14816,8 +14881,15 @@ export class AgentSession {
|
|
|
14816
14881
|
credentialRotated = await this.#markFailedCredential(trigger);
|
|
14817
14882
|
}
|
|
14818
14883
|
if (credentialRotated) {
|
|
14819
|
-
if
|
|
14820
|
-
|
|
14884
|
+
// A rotation only becomes a same-model retry if the controller can
|
|
14885
|
+
// actually be rewound. `restorePreviousEntryForRetry()` refuses once an
|
|
14886
|
+
// entry's restore budget is consumed or attempts are exhausted; ignoring
|
|
14887
|
+
// that would force `outcome = "retry"` while `activeIndex` stays on the
|
|
14888
|
+
// next entry and `this.model` stays on the previous one — splitting
|
|
14889
|
+
// attempt attribution, exhaustion, and sticky selection across two models.
|
|
14890
|
+
if (!managedFallback || controller.restorePreviousEntryForRetry()) {
|
|
14891
|
+
outcome = "retry";
|
|
14892
|
+
}
|
|
14821
14893
|
}
|
|
14822
14894
|
if (outcome === "exhausted") {
|
|
14823
14895
|
if (managedFallback) {
|