@gajae-code/coding-agent 0.12.7 → 0.12.8
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 +19 -0
- package/dist/types/commands/notify.d.ts +4 -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/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/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/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 +31 -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 +11 -1
- package/dist/types/session/internal/native-publish-outcome.d.ts +1 -0
- 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/package.json +10 -8
- package/scripts/build-sdk-package-smoke.ts +51 -0
- package/scripts/capture-notifications-settings-showcase.ts +1 -1
- package/src/cli/notify-cli.ts +204 -94
- package/src/commands/notify.ts +19 -8
- package/src/config/file-lock.ts +6 -0
- package/src/config/model-discovery-manager.ts +50 -8
- package/src/config/model-registry.ts +862 -75
- 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/gjc-runtime/session-state-sidecar.ts +12 -2
- package/src/internal-urls/docs-index.generated.ts +6 -6
- package/src/main.ts +43 -7
- package/src/modes/DESIGN.md +11 -11
- 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 +464 -50
- 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/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 +431 -130
- 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 +10 -7
- package/src/session/agent-session.ts +44 -24
- package/src/session/internal/managed-session-scope.ts +981 -450
- package/src/session/internal/managed-session-storage.ts +382 -45
- package/src/session/internal/native-publish-outcome.ts +3 -1
- 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 +82 -4
- package/src/task/index.ts +9 -3
- package/src/tools/ask.ts +1 -1
- package/src/tools/bash.ts +1 -1
- package/src/tools/telegram-send.ts +4 -2
|
@@ -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";
|
|
@@ -184,6 +184,7 @@ import {
|
|
|
184
184
|
|
|
185
185
|
type AsyncResultEntry = {
|
|
186
186
|
jobId: string;
|
|
187
|
+
generation: string;
|
|
187
188
|
result: string;
|
|
188
189
|
job: AsyncJob | undefined;
|
|
189
190
|
durationMs: number | undefined;
|
|
@@ -1502,13 +1503,14 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1502
1503
|
? new AsyncJobManager({
|
|
1503
1504
|
maxRunningJobs: asyncMaxJobs,
|
|
1504
1505
|
onJobComplete: async (jobId, result, job) => {
|
|
1505
|
-
if (!session
|
|
1506
|
+
if (!session) return;
|
|
1506
1507
|
const formattedResult = await formatAsyncResultForFollowUp(result);
|
|
1507
|
-
if (asyncJobManager!.isDeliverySuppressed(jobId)) return;
|
|
1508
|
+
if (asyncJobManager!.isDeliverySuppressed(jobId, job?.generation)) return;
|
|
1508
1509
|
|
|
1509
1510
|
const durationMs = job ? jobElapsedMs(job) : undefined;
|
|
1510
1511
|
session.yieldQueue.enqueue<AsyncResultEntry>("async-result", {
|
|
1511
1512
|
jobId,
|
|
1513
|
+
generation: job?.generation ?? "",
|
|
1512
1514
|
result: formattedResult,
|
|
1513
1515
|
job,
|
|
1514
1516
|
durationMs,
|
|
@@ -1907,7 +1909,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1907
1909
|
const spawnProvenance = process.env[SPAWN_PROVENANCE_ENV];
|
|
1908
1910
|
const spawnedByGjc = typeof spawnProvenance === "string" && spawnProvenance.trim().length > 0;
|
|
1909
1911
|
delete process.env[SPAWN_PROVENANCE_ENV];
|
|
1910
|
-
const notificationHostEligible =
|
|
1912
|
+
const notificationHostEligible = isGenericNotificationHostEligible({
|
|
1911
1913
|
env: process.env,
|
|
1912
1914
|
hostModeSupported: options.notificationHostModeSupported ?? true,
|
|
1913
1915
|
taskDepth,
|
|
@@ -1919,10 +1921,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1919
1921
|
const notificationSessionController = new NotificationSessionController({
|
|
1920
1922
|
eligible: notificationHostEligible,
|
|
1921
1923
|
getConfig: () => getNotificationConfig(settings),
|
|
1924
|
+
spawnedByGjc,
|
|
1922
1925
|
});
|
|
1923
1926
|
if (
|
|
1924
1927
|
lifecycleStartupCapability ||
|
|
1925
|
-
|
|
1928
|
+
shouldRegisterGenericNotificationsExtension({
|
|
1926
1929
|
env: process.env,
|
|
1927
1930
|
cfg: notificationCfg,
|
|
1928
1931
|
taskDepth,
|
|
@@ -2797,7 +2800,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2797
2800
|
hasSession = true;
|
|
2798
2801
|
if (asyncJobManager) {
|
|
2799
2802
|
session.yieldQueue.register<AsyncResultEntry>("async-result", {
|
|
2800
|
-
isStale: entry => asyncJobManager.isDeliverySuppressed(entry.jobId),
|
|
2803
|
+
isStale: entry => asyncJobManager.isDeliverySuppressed(entry.jobId, entry.generation),
|
|
2801
2804
|
build: buildAsyncResultBatchMessage,
|
|
2802
2805
|
});
|
|
2803
2806
|
}
|
|
@@ -1869,13 +1869,6 @@ export class AgentSession {
|
|
|
1869
1869
|
this.#retryReplayEpoch++;
|
|
1870
1870
|
this.#retryReplayUnsafeEpoch = undefined;
|
|
1871
1871
|
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
1872
|
}
|
|
1880
1873
|
|
|
1881
1874
|
#markRetryReplayUnsafe(): void {
|
|
@@ -1885,6 +1878,14 @@ export class AgentSession {
|
|
|
1885
1878
|
get #hasCleanRetryReplaySafety(): boolean {
|
|
1886
1879
|
return this.#retryReplayEpoch > 0 && this.#retryReplayUnsafeEpoch !== this.#retryReplayEpoch;
|
|
1887
1880
|
}
|
|
1881
|
+
#bindAttemptScope(scope: AttemptScope | undefined): void {
|
|
1882
|
+
if (!scope) return;
|
|
1883
|
+
this.#attemptRecordStore.register(scope);
|
|
1884
|
+
this.#attemptRecordStore.establishClean(scope);
|
|
1885
|
+
}
|
|
1886
|
+
#isRetryScopeClean(scope: AttemptScope | undefined): boolean {
|
|
1887
|
+
return scope !== undefined && this.#attemptRecordStore.isClean(scope);
|
|
1888
|
+
}
|
|
1888
1889
|
#prePromptContextCheckPromise: Promise<void> | undefined = undefined;
|
|
1889
1890
|
/** Display-only context snapshot; pre-prompt compaction estimates deliberately remain uncached. */
|
|
1890
1891
|
#contextUsageCache: { key: string; value: ContextUsage } | undefined;
|
|
@@ -1898,6 +1899,8 @@ export class AgentSession {
|
|
|
1898
1899
|
// Handoff state
|
|
1899
1900
|
#handoffAbortController: AbortController | undefined = undefined;
|
|
1900
1901
|
#skipPostTurnMaintenanceAssistantTimestamp: number | undefined = undefined;
|
|
1902
|
+
/** Scope cleanliness captured after message_end extension delivery and before terminal agent_end retirement. */
|
|
1903
|
+
#assistantAttemptScopes = new WeakMap<AssistantMessage, { scope?: AttemptScope; wasClean: boolean }>();
|
|
1901
1904
|
|
|
1902
1905
|
// Retry state
|
|
1903
1906
|
#retryAbortController: AbortController | undefined = undefined;
|
|
@@ -2606,6 +2609,7 @@ export class AgentSession {
|
|
|
2606
2609
|
if (this.#extensionRunner && typeof this.#extensionRunner.setAttemptRecordStore === "function") {
|
|
2607
2610
|
this.#extensionRunner.setAttemptRecordStore(this.#attemptRecordStore);
|
|
2608
2611
|
}
|
|
2612
|
+
this.agent.setMainAttemptScopeObserver(scope => this.#bindAttemptScope(scope));
|
|
2609
2613
|
this.#skills = config.skills ?? [];
|
|
2610
2614
|
this.#skillWarnings = config.skillWarnings ?? [];
|
|
2611
2615
|
this.#customCommands = config.customCommands ?? [];
|
|
@@ -3739,11 +3743,7 @@ export class AgentSession {
|
|
|
3739
3743
|
/** Internal handler for agent events - shared by subscribe and reconnect */
|
|
3740
3744
|
#handleAgentEvent = async (event: AgentEvent, activePromptHandle?: string): Promise<void> => {
|
|
3741
3745
|
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();
|
|
3746
|
+
|
|
3747
3747
|
if (
|
|
3748
3748
|
event.type === "tool_execution_start" ||
|
|
3749
3749
|
event.type === "tool_execution_update" ||
|
|
@@ -3947,6 +3947,12 @@ export class AgentSession {
|
|
|
3947
3947
|
}
|
|
3948
3948
|
|
|
3949
3949
|
await this.#emitSessionEvent(displayEvent);
|
|
3950
|
+
if (event.type === "message_end" && event.message.role === "assistant") {
|
|
3951
|
+
this.#assistantAttemptScopes.set(event.message, {
|
|
3952
|
+
scope: attemptScope,
|
|
3953
|
+
wasClean: this.#isRetryScopeClean(attemptScope),
|
|
3954
|
+
});
|
|
3955
|
+
}
|
|
3950
3956
|
if (
|
|
3951
3957
|
displayEvent !== event &&
|
|
3952
3958
|
displayEvent.type === "message_end" &&
|
|
@@ -4321,7 +4327,14 @@ export class AgentSession {
|
|
|
4321
4327
|
if (this.#isRetryableError(msg)) {
|
|
4322
4328
|
const transportFailure = (msg as AssistantMessage & { transportFailure?: TransportFailureFacts })
|
|
4323
4329
|
.transportFailure;
|
|
4324
|
-
const
|
|
4330
|
+
const messageScope = this.#assistantAttemptScopes.get(msg);
|
|
4331
|
+
const didRetry = await this.#handleRetryableError(
|
|
4332
|
+
msg,
|
|
4333
|
+
false,
|
|
4334
|
+
transportFailure,
|
|
4335
|
+
messageScope?.scope ?? event.scope,
|
|
4336
|
+
messageScope?.wasClean ?? false,
|
|
4337
|
+
);
|
|
4325
4338
|
if (didRetry) return; // Retry was initiated, don't proceed to compaction
|
|
4326
4339
|
}
|
|
4327
4340
|
if (this.#retryAttempt > 0) {
|
|
@@ -5838,6 +5851,7 @@ export class AgentSession {
|
|
|
5838
5851
|
this.#abortActiveMidRunBarriers();
|
|
5839
5852
|
this.abortCompaction();
|
|
5840
5853
|
this.agent.abort();
|
|
5854
|
+
this.agent.setMainAttemptScopeObserver(undefined);
|
|
5841
5855
|
// Disconnect the Agent event bridge NOW — before the maintenance join and the
|
|
5842
5856
|
// bounded idle / forceAbort below — so no agent_end emitted during teardown
|
|
5843
5857
|
// (including the one forceAbort emits) can re-enter #handleAgentEvent and start
|
|
@@ -14573,6 +14587,8 @@ export class AgentSession {
|
|
|
14573
14587
|
outcome.failure.message,
|
|
14574
14588
|
true,
|
|
14575
14589
|
outcome.failure.transportFailure,
|
|
14590
|
+
outcome.scope,
|
|
14591
|
+
this.#isRetryScopeClean(outcome.scope),
|
|
14576
14592
|
) as Promise<ManagedAttemptDecision>;
|
|
14577
14593
|
}
|
|
14578
14594
|
|
|
@@ -14733,6 +14749,8 @@ export class AgentSession {
|
|
|
14733
14749
|
message: AssistantMessage,
|
|
14734
14750
|
managedOutcome = false,
|
|
14735
14751
|
transportFailure?: TransportFailureFacts,
|
|
14752
|
+
scope?: AttemptScope,
|
|
14753
|
+
scopeWasClean = this.#isRetryScopeClean(scope),
|
|
14736
14754
|
): Promise<boolean | ManagedAttemptDecision> {
|
|
14737
14755
|
const controller = this.#defaultFallbackChain();
|
|
14738
14756
|
const managedFallback = controller.chain.entries.length > 1;
|
|
@@ -14747,12 +14765,16 @@ export class AgentSession {
|
|
|
14747
14765
|
if (!managedFallback && !retrySettings.enabled) return false;
|
|
14748
14766
|
const classification = this.#classifyErrorForRetry(message);
|
|
14749
14767
|
const firstEventTimeout = classification === "first_event_timeout";
|
|
14750
|
-
if (!managedFallback && firstEventTimeout) {
|
|
14751
|
-
this.#firstEventTimeoutRetryStartedAt
|
|
14768
|
+
if (!managedFallback && firstEventTimeout && this.#retryAttempt === 0) {
|
|
14769
|
+
this.#firstEventTimeoutRetryStartedAt = Date.now();
|
|
14752
14770
|
}
|
|
14753
|
-
|
|
14754
|
-
|
|
14755
|
-
|
|
14771
|
+
const requiresScopedFirstEventTimeout = managedFallback || !legacyRetryConfigured;
|
|
14772
|
+
if (
|
|
14773
|
+
firstEventTimeout &&
|
|
14774
|
+
(assistantMessageHasVisibleOrToolContent(message) ||
|
|
14775
|
+
(this.#retryAttempt > 0 && !this.#hasCleanRetryReplaySafety) ||
|
|
14776
|
+
(requiresScopedFirstEventTimeout && (!this.#isTypedFirstEventTimeout(message) || !scope || !scopeWasClean)))
|
|
14777
|
+
) {
|
|
14756
14778
|
return managedOutcome
|
|
14757
14779
|
? {
|
|
14758
14780
|
type: "terminal",
|
|
@@ -14780,14 +14802,12 @@ export class AgentSession {
|
|
|
14780
14802
|
// before the failure. This bypasses #hasCleanRetryReplaySafety because the
|
|
14781
14803
|
// content-free check is the replay-safety guarantee for credential rotation.
|
|
14782
14804
|
const canReplayRotatedCredential = credentialRotated;
|
|
14783
|
-
// Bare defaults
|
|
14784
|
-
//
|
|
14785
|
-
//
|
|
14805
|
+
// Bare defaults retain their narrow watchdog and Codex admissions. A
|
|
14806
|
+
// first-event timeout adds the typed, content-free, current-clean-scope
|
|
14807
|
+
// requirement above; other transient watchdogs preserve legacy behavior.
|
|
14786
14808
|
if (!managedFallback && !legacyRetryConfigured && !canReplayRotatedCredential) {
|
|
14787
14809
|
const bareDefaultCodexOverload = isBareDefaultCodexOverload(message);
|
|
14788
14810
|
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
14811
|
const canReplayCodexOverload = bareDefaultCodexOverload;
|
|
14792
14812
|
if (
|
|
14793
14813
|
(!canReplayCodexOverload &&
|
|
@@ -14796,7 +14816,7 @@ export class AgentSession {
|
|
|
14796
14816
|
(hasBareDefaultRetryDisqualifyingFacts(message) ||
|
|
14797
14817
|
(classification !== "transient" && classification !== "first_event_timeout") ||
|
|
14798
14818
|
!BARE_DEFAULT_WATCHDOG_ERROR.test(message.errorMessage ?? ""))) ||
|
|
14799
|
-
(!canReplayCodexOverload && !this.#hasCleanRetryReplaySafety)
|
|
14819
|
+
(!canReplayCodexOverload && !firstEventTimeout && !this.#hasCleanRetryReplaySafety)
|
|
14800
14820
|
) {
|
|
14801
14821
|
return false;
|
|
14802
14822
|
}
|