@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.
Files changed (95) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/types/commands/notify.d.ts +4 -0
  3. package/dist/types/config/file-lock.d.ts +3 -0
  4. package/dist/types/config/model-discovery-manager.d.ts +6 -1
  5. package/dist/types/config/model-registry.d.ts +2 -0
  6. package/dist/types/config/resolve-config-value.d.ts +2 -1
  7. package/dist/types/config/settings-schema.d.ts +17 -1
  8. package/dist/types/main.d.ts +1 -0
  9. package/dist/types/modes/components/hook-selector.d.ts +20 -0
  10. package/dist/types/modes/components/notifications-settings-editor.d.ts +53 -4
  11. package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
  12. package/dist/types/modes/controllers/selector-controller.d.ts +4 -2
  13. package/dist/types/sdk/broker/broker.d.ts +5 -1
  14. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +1 -0
  15. package/dist/types/sdk/bus/chat-daemon-control.d.ts +11 -1
  16. package/dist/types/sdk/bus/config.d.ts +75 -113
  17. package/dist/types/sdk/bus/discord-live-provider.d.ts +8 -2
  18. package/dist/types/sdk/bus/discord-provider.d.ts +19 -0
  19. package/dist/types/sdk/bus/index.d.ts +1 -1
  20. package/dist/types/sdk/bus/notification-orchestration.d.ts +127 -18
  21. package/dist/types/sdk/bus/notification-service.d.ts +31 -4
  22. package/dist/types/sdk/bus/session-control.d.ts +8 -3
  23. package/dist/types/sdk/bus/slack-live-provider.d.ts +9 -2
  24. package/dist/types/sdk/bus/slack-provider.d.ts +22 -0
  25. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +8 -1
  26. package/dist/types/sdk/client/discovery.d.ts +2 -1
  27. package/dist/types/sdk/host/query/handlers.d.ts +2 -0
  28. package/dist/types/sdk/index.d.ts +1 -0
  29. package/dist/types/sdk/providers.d.ts +22 -0
  30. package/dist/types/sdk/session-directory.d.ts +1 -1
  31. package/dist/types/session/internal/managed-session-scope.d.ts +17 -1
  32. package/dist/types/session/internal/managed-session-storage.d.ts +11 -1
  33. package/dist/types/session/internal/native-publish-outcome.d.ts +1 -0
  34. package/dist/types/session/internal/session-open-errors.d.ts +5 -0
  35. package/dist/types/session/session-manager.d.ts +3 -1
  36. package/dist/types/session/session-storage.d.ts +2 -0
  37. package/package.json +10 -8
  38. package/scripts/build-sdk-package-smoke.ts +51 -0
  39. package/scripts/capture-notifications-settings-showcase.ts +1 -1
  40. package/src/cli/notify-cli.ts +204 -94
  41. package/src/commands/notify.ts +19 -8
  42. package/src/config/file-lock.ts +6 -0
  43. package/src/config/model-discovery-manager.ts +50 -8
  44. package/src/config/model-registry.ts +862 -75
  45. package/src/config/resolve-config-value.ts +10 -8
  46. package/src/config/settings-schema.ts +25 -15
  47. package/src/config/settings.ts +15 -15
  48. package/src/gjc-runtime/session-state-sidecar.ts +12 -2
  49. package/src/internal-urls/docs-index.generated.ts +6 -6
  50. package/src/main.ts +43 -7
  51. package/src/modes/DESIGN.md +11 -11
  52. package/src/modes/components/hook-selector.ts +155 -44
  53. package/src/modes/components/model-selector.ts +242 -49
  54. package/src/modes/components/notifications-settings-editor.ts +631 -85
  55. package/src/modes/controllers/extension-ui-controller.ts +114 -15
  56. package/src/modes/controllers/selector-controller.ts +464 -50
  57. package/src/sdk/broker/broker.ts +54 -10
  58. package/src/sdk/broker/lifecycle-ledger.ts +34 -1
  59. package/src/sdk/broker/lifecycle.ts +47 -50
  60. package/src/sdk/bus/chat-daemon-cli.ts +12 -5
  61. package/src/sdk/bus/chat-daemon-control.ts +35 -6
  62. package/src/sdk/bus/chat-daemon-runtime.ts +16 -2
  63. package/src/sdk/bus/config.ts +534 -286
  64. package/src/sdk/bus/discord-live-provider.ts +57 -2
  65. package/src/sdk/bus/discord-provider.ts +21 -0
  66. package/src/sdk/bus/index.ts +97 -35
  67. package/src/sdk/bus/notification-orchestration.ts +371 -68
  68. package/src/sdk/bus/notification-service.ts +431 -130
  69. package/src/sdk/bus/session-control.ts +119 -44
  70. package/src/sdk/bus/slack-live-provider.ts +78 -2
  71. package/src/sdk/bus/slack-provider.ts +24 -0
  72. package/src/sdk/bus/telegram-daemon-cli.ts +9 -2
  73. package/src/sdk/bus/telegram-daemon-contract.ts +8 -1
  74. package/src/sdk/bus/telegram-daemon-control.ts +3 -3
  75. package/src/sdk/bus/telegram-daemon.ts +23 -15
  76. package/src/sdk/client/discovery.ts +10 -4
  77. package/src/sdk/host/query/handlers.ts +20 -1
  78. package/src/sdk/index.ts +1 -0
  79. package/src/sdk/protocol/operation-inventory.generated.json +18 -0
  80. package/src/sdk/protocol/operation-registry.ts +5 -2
  81. package/src/sdk/providers.ts +51 -0
  82. package/src/sdk/session-directory.ts +1 -0
  83. package/src/sdk/session.ts +10 -7
  84. package/src/session/agent-session.ts +44 -24
  85. package/src/session/internal/managed-session-scope.ts +981 -450
  86. package/src/session/internal/managed-session-storage.ts +382 -45
  87. package/src/session/internal/native-publish-outcome.ts +3 -1
  88. package/src/session/internal/session-open-errors.ts +11 -0
  89. package/src/session/session-manager.ts +27 -3
  90. package/src/session/session-storage.ts +72 -5
  91. package/src/slash-commands/builtin-registry.ts +82 -4
  92. package/src/task/index.ts +9 -3
  93. package/src/tools/ask.ts +1 -1
  94. package/src/tools/bash.ts +1 -1
  95. 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
- snapshot = await (this.surface[source.method] as () => unknown)();
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
@@ -22,5 +22,6 @@ export type {
22
22
  Q10ThinkingMode,
23
23
  } from "./models";
24
24
  export * from "./prompt-status";
25
+ export type { ActiveProviderConnectionKind, ActiveProviderDescriptor } from "./providers";
25
26
  export * from "./session";
26
27
  export * from "./session-directory";
@@ -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
- : ["invalid_request", "resource_gone"],
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
+ }
@@ -39,6 +39,7 @@ export type ResolveManagedSessionScopeResult =
39
39
  | "sessions_root_unavailable"
40
40
  | "binding_conflict"
41
41
  | "binding_invalid"
42
+ | "migration_busy"
42
43
  | "atomic_unavailable"
43
44
  | "durability_not_provable"
44
45
  | "durability_failed"
@@ -108,10 +108,10 @@ import { MCPManager } from "../runtime-mcp";
108
108
  import { createNotificationsExtension } from "../sdk/bus";
109
109
  import {
110
110
  getNotificationConfig,
111
- isNotificationHostEligible,
111
+ isGenericNotificationHostEligible,
112
112
  type NotificationConfig,
113
113
  SPAWN_PROVENANCE_ENV,
114
- shouldRegisterNotificationsExtension,
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 || asyncJobManager!.isDeliverySuppressed(jobId)) return;
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 = isNotificationHostEligible({
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
- shouldRegisterNotificationsExtension({
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
- if ((event.type === "agent_start" || event.type === "turn_start") && attemptScope) {
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 didRetry = await this.#handleRetryableError(msg, false, transportFailure);
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 ??= Date.now();
14768
+ if (!managedFallback && firstEventTimeout && this.#retryAttempt === 0) {
14769
+ this.#firstEventTimeoutRetryStartedAt = Date.now();
14752
14770
  }
14753
- // First-event timeouts are replay-safe only before any model progress,
14754
- // abort, or extension/provider lifecycle participation.
14755
- if (firstEventTimeout && (!this.#hasCleanRetryReplaySafety || assistantMessageHasVisibleOrToolContent(message))) {
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 admit only clean, side-effect-free canonical stream watchdog failures,
14784
- // content-free quota/rate-limit credential rotations, and the explicit Codex
14785
- // capacity-overload event.
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
  }