@gajae-code/coding-agent 0.15.4 → 0.15.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.
Files changed (174) hide show
  1. package/CHANGELOG.md +46 -2
  2. package/dist/types/async/job-manager.d.ts +83 -0
  3. package/dist/types/cli/auth-gateway-cli.d.ts +17 -0
  4. package/dist/types/cli/model-presets-cli.d.ts +9 -0
  5. package/dist/types/cli/update-cli.d.ts +27 -5
  6. package/dist/types/commands/auth-gateway.d.ts +3 -0
  7. package/dist/types/commands/gc.d.ts +13 -0
  8. package/dist/types/commands/model-presets.d.ts +21 -0
  9. package/dist/types/config/autorouting-tier-map.d.ts +99 -0
  10. package/dist/types/config/file-lock.d.ts +49 -2
  11. package/dist/types/config/internal/model-preset-registry-test-state.d.ts +4 -0
  12. package/dist/types/config/internal/model-preset-registry-test-support.d.ts +3 -0
  13. package/dist/types/config/keybindings.d.ts +3 -2
  14. package/dist/types/config/model-preset-registry.d.ts +368 -0
  15. package/dist/types/config/model-profile-activation.d.ts +39 -0
  16. package/dist/types/config/model-profiles.d.ts +5 -3
  17. package/dist/types/config/model-registry.d.ts +11 -5
  18. package/dist/types/config/settings-schema.d.ts +9 -0
  19. package/dist/types/coordinator-mcp/model-preset.d.ts +7 -5
  20. package/dist/types/coordinator-mcp/projection-scan.d.ts +46 -0
  21. package/dist/types/extensibility/extensions/types.d.ts +4 -4
  22. package/dist/types/extensibility/shared-events.d.ts +2 -0
  23. package/dist/types/gjc-runtime/empty-delete-gc.d.ts +58 -0
  24. package/dist/types/gjc-runtime/gc-runtime.d.ts +2 -0
  25. package/dist/types/gjc-runtime/session-state-lock.d.ts +35 -6
  26. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +6 -0
  27. package/dist/types/index.d.ts +1 -0
  28. package/dist/types/main.d.ts +3 -1
  29. package/dist/types/modes/components/jobs-overlay-model.d.ts +4 -2
  30. package/dist/types/modes/jobs-observer.d.ts +36 -0
  31. package/dist/types/sdk/broker/broker.d.ts +3 -0
  32. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +3 -0
  33. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +4 -0
  34. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +8 -1
  35. package/dist/types/sdk/cli/session-cli.d.ts +42 -1
  36. package/dist/types/sdk/client/discovery.d.ts +1 -0
  37. package/dist/types/sdk/host/host.d.ts +2 -0
  38. package/dist/types/sdk/host/model-pin.d.ts +7 -2
  39. package/dist/types/sdk/host/sdk-run-capability.d.ts +1 -0
  40. package/dist/types/sdk/host/session-runtime.d.ts +19 -1
  41. package/dist/types/sdk/prompt-deadline-manager.d.ts +11 -1
  42. package/dist/types/sdk/protocol/adapter-validation.d.ts +2 -0
  43. package/dist/types/sdk/router/session-router.d.ts +9 -0
  44. package/dist/types/session/account-inventory.d.ts +1 -0
  45. package/dist/types/session/agent-session.d.ts +19 -5
  46. package/dist/types/session/fold-coordinator.d.ts +201 -0
  47. package/dist/types/session/internal/managed-session-storage.d.ts +4 -3
  48. package/dist/types/session/sdk-run-capability-internal.d.ts +7 -0
  49. package/dist/types/session/sdk-run-capability.d.ts +2 -0
  50. package/dist/types/telemetry/control.d.ts +11 -0
  51. package/dist/types/telemetry/events.d.ts +20 -0
  52. package/dist/types/telemetry/index.d.ts +3 -0
  53. package/dist/types/telemetry/transport.d.ts +22 -0
  54. package/dist/types/tools/bash-interactive.d.ts +23 -0
  55. package/dist/types/tools/index.d.ts +6 -5
  56. package/dist/types/tools/path-utils.d.ts +5 -6
  57. package/dist/types/tools/tmux-self-injection-guard.d.ts +15 -0
  58. package/dist/types/web/search/providers/base.d.ts +9 -4
  59. package/dist/types/web/search/providers/xai.d.ts +2 -1
  60. package/dist/types/web/search/types.d.ts +14 -0
  61. package/package.json +10 -7
  62. package/scripts/generate-hotkeys-docs.ts +4 -0
  63. package/scripts/generate-sdk-operation-inventory.ts +3 -3
  64. package/src/async/job-manager.ts +671 -39
  65. package/src/cli/auth-broker-cli.ts +40 -12
  66. package/src/cli/auth-gateway-cli.ts +273 -97
  67. package/src/cli/config-cli.ts +2 -2
  68. package/src/cli/fast-help.ts +3 -0
  69. package/src/cli/model-presets-cli.ts +100 -0
  70. package/src/cli/update-cli.ts +142 -6
  71. package/src/cli.ts +6 -2
  72. package/src/commands/auth-gateway.ts +12 -8
  73. package/src/commands/gc.ts +19 -0
  74. package/src/commands/model-presets.ts +24 -0
  75. package/src/commands/sdk.ts +5 -0
  76. package/src/config/autorouting-tier-map.ts +33 -0
  77. package/src/config/file-lock-gc.ts +9 -4
  78. package/src/config/file-lock.ts +775 -59
  79. package/src/config/internal/model-preset-registry-test-state.ts +28 -0
  80. package/src/config/internal/model-preset-registry-test-support.ts +11 -0
  81. package/src/config/keybindings.ts +6 -2
  82. package/src/config/model-preset-registry.ts +2799 -0
  83. package/src/config/model-profile-activation.ts +115 -28
  84. package/src/config/model-profile-contract.ts +4 -1
  85. package/src/config/model-profiles.ts +56 -14
  86. package/src/config/model-registry.ts +1028 -123
  87. package/src/config/settings-schema.ts +11 -0
  88. package/src/config/settings.ts +6 -1
  89. package/src/coordinator-mcp/model-preset.ts +32 -17
  90. package/src/coordinator-mcp/policy.ts +1 -1
  91. package/src/coordinator-mcp/projection-scan.ts +571 -0
  92. package/src/coordinator-mcp/server.ts +19 -24
  93. package/src/coordinator-mcp/session-reaper.ts +9 -3
  94. package/src/extensibility/extensions/loader.ts +0 -1
  95. package/src/extensibility/extensions/types.ts +4 -2
  96. package/src/extensibility/gjc-plugins/runtime-adapters.ts +1 -0
  97. package/src/extensibility/shared-events.ts +2 -0
  98. package/src/gjc-runtime/empty-delete-gc.ts +301 -0
  99. package/src/gjc-runtime/gc-render.ts +29 -0
  100. package/src/gjc-runtime/gc-runtime.ts +174 -11
  101. package/src/gjc-runtime/launch-worktree.ts +44 -7
  102. package/src/gjc-runtime/ralplan-runtime.ts +14 -1
  103. package/src/gjc-runtime/session-state-lock.ts +1242 -141
  104. package/src/gjc-runtime/session-state-sidecar.ts +76 -3
  105. package/src/gjc-runtime/tmux-sessions.ts +145 -48
  106. package/src/index.ts +1 -0
  107. package/src/internal-urls/docs-index.generated.ts +14 -13
  108. package/src/lsp/config.ts +4 -3
  109. package/src/lsp/path-trust.ts +12 -6
  110. package/src/main.ts +9 -2
  111. package/src/modes/acp/acp-agent.ts +11 -5
  112. package/src/modes/components/jobs-overlay-model.ts +99 -16
  113. package/src/modes/components/model-selector.ts +158 -18
  114. package/src/modes/components/status-line/segments.ts +6 -1
  115. package/src/modes/controllers/extension-ui-controller.ts +1 -1
  116. package/src/modes/controllers/input-controller.ts +23 -6
  117. package/src/modes/interactive-mode.ts +14 -1
  118. package/src/modes/jobs-observer.ts +149 -16
  119. package/src/modes/prompt-action-autocomplete.ts +26 -47
  120. package/src/modes/runtime-init.ts +1 -1
  121. package/src/modes/utils/injected-user-submission.ts +15 -1
  122. package/src/prompts/tools/bash.md +2 -2
  123. package/src/prompts/tools/fold-receipt.md +3 -0
  124. package/src/prompts/tools/job.md +1 -1
  125. package/src/sdk/acp/adapter.ts +17 -2
  126. package/src/sdk/broker/broker.ts +48 -21
  127. package/src/sdk/broker/lifecycle.ts +32 -6
  128. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  129. package/src/sdk/bus/chat-daemon-runtime.ts +11 -1
  130. package/src/sdk/bus/kind-aware-reconciliation.ts +46 -8
  131. package/src/sdk/bus/prompt-reconciliation.ts +33 -6
  132. package/src/sdk/bus/slack-conversation.ts +5 -1
  133. package/src/sdk/bus/telegram-daemon-contract.ts +8 -1
  134. package/src/sdk/cli/session-cli.ts +161 -5
  135. package/src/sdk/client/discovery.ts +3 -1
  136. package/src/sdk/host/control/dispatch.ts +3 -0
  137. package/src/sdk/host/host.ts +7 -0
  138. package/src/sdk/host/model-pin.ts +185 -25
  139. package/src/sdk/host/sdk-run-capability.ts +1 -0
  140. package/src/sdk/host/session-runtime.test.ts +1240 -107
  141. package/src/sdk/host/session-runtime.ts +1029 -115
  142. package/src/sdk/prompt-deadline-manager.ts +16 -1
  143. package/src/sdk/protocol/adapter-validation.ts +27 -0
  144. package/src/sdk/protocol/operation-inventory.generated.json +2 -2
  145. package/src/sdk/protocol/operation-registry.ts +4 -1
  146. package/src/sdk/router/session-router.ts +297 -111
  147. package/src/sdk/session.ts +83 -12
  148. package/src/session/account-inventory.ts +27 -11
  149. package/src/session/agent-session.ts +887 -95
  150. package/src/session/fold-coordinator.ts +433 -0
  151. package/src/session/internal/managed-session-storage.ts +64 -26
  152. package/src/session/sdk-run-capability-internal.ts +15 -0
  153. package/src/session/sdk-run-capability.ts +2 -0
  154. package/src/session/session-manager.ts +29 -10
  155. package/src/setup/provider-presets.json +1 -1
  156. package/src/slash-commands/helpers/credential-switch.ts +6 -1
  157. package/src/task/executor.ts +19 -3
  158. package/src/telemetry/control.ts +26 -0
  159. package/src/telemetry/events.ts +142 -0
  160. package/src/telemetry/index.ts +3 -0
  161. package/src/telemetry/transport.ts +87 -0
  162. package/src/tools/bash-interactive.ts +182 -85
  163. package/src/tools/bash.ts +786 -67
  164. package/src/tools/index.ts +7 -5
  165. package/src/tools/path-utils.ts +77 -29
  166. package/src/tools/tmux-self-injection-guard.ts +359 -0
  167. package/src/tools/tool-catalog.generated.ts +1 -1
  168. package/src/web/search/provider.ts +120 -11
  169. package/src/web/search/providers/anthropic.ts +89 -17
  170. package/src/web/search/providers/base.ts +9 -4
  171. package/src/web/search/providers/gemini.ts +87 -15
  172. package/src/web/search/providers/openai-compatible.ts +23 -8
  173. package/src/web/search/providers/xai.ts +98 -12
  174. package/src/web/search/types.ts +16 -0
@@ -46,6 +46,8 @@ export interface BrokerSettings {
46
46
  heartbeatTtlMs?: number;
47
47
  /** Broker-owned migration policy. Client lifecycle frames cannot select it. */
48
48
  resolveDirectoryMigration?: (_cwd: string) => Promise<DirectoryMigrationPolicy>;
49
+ /** Host model resolver override for lifecycle tests and embedders. */
50
+ resolveModelPin?: SdkHostModelResolver;
49
51
  }
50
52
 
51
53
  type ResolvedBrokerSettings = {
@@ -863,7 +865,8 @@ export class Broker {
863
865
  #completion!: Promise<void>;
864
866
  #resolveCompletion!: () => void;
865
867
  #rejectCompletion!: (error: unknown) => void;
866
- readonly #resolveModelPin: SdkHostModelResolver;
868
+ #resolveModelPin: SdkHostModelResolver;
869
+ #ownsResolveModelPin: boolean;
867
870
  constructor(settings: BrokerSettings) {
868
871
  this.settings = {
869
872
  agentDir: settings.agentDir,
@@ -874,7 +877,8 @@ export class Broker {
874
877
  };
875
878
  this.index = new SessionIndex(settings.agentDir);
876
879
  this.ledger = new LifecycleLedger(settings.agentDir);
877
- this.#resolveModelPin = createDefaultSdkHostModelResolver(this.settings.agentDir);
880
+ this.#ownsResolveModelPin = settings.resolveModelPin === undefined;
881
+ this.#resolveModelPin = settings.resolveModelPin ?? createDefaultSdkHostModelResolver(this.settings.agentDir);
878
882
  this.#lock = path.join(settings.agentDir, "sdk", "broker.lock");
879
883
  const completion = Promise.withResolvers<void>();
880
884
  this.#completion = completion.promise;
@@ -1023,6 +1027,8 @@ export class Broker {
1023
1027
  this.#resolveCompletion = completion.resolve;
1024
1028
  this.#rejectCompletion = completion.reject;
1025
1029
  this.#completionTask = null;
1030
+ if (this.#ownsResolveModelPin)
1031
+ this.#resolveModelPin = createDefaultSdkHostModelResolver(this.settings.agentDir);
1026
1032
  // A drained queue refuses every later startup by design, so a restarted broker
1027
1033
  // needs a new one or it would admit nothing for the rest of the process.
1028
1034
  this.#startupAdmissions = new StartupAdmissionQueue(sdkHostStartupConcurrency());
@@ -1285,25 +1291,35 @@ export class Broker {
1285
1291
  if (this.#heartbeatTimer) clearInterval(this.#heartbeatTimer);
1286
1292
  this.#heartbeatTimer = null;
1287
1293
  this.#completionTask = (async () => {
1288
- await this.#transport?.stop();
1289
- this.#transport = null;
1290
- if (mode === "lost-root")
1291
- await Promise.race([Promise.allSettled(this.#admitted), Bun.sleep(BROKER_SETTLEMENT_MS)]);
1292
- else await Promise.allSettled(this.#admitted);
1293
- this.#publication?.close();
1294
- this.#publication = null;
1295
- if (mode === "owned-root" && this.discovery?.ownerId === this.#owner) {
1296
- try {
1297
- const disk = JSON.parse(await fs.readFile(brokerDiscoveryPath(this.settings.agentDir), "utf8")) as {
1298
- ownerId?: string;
1299
- };
1300
- if (disk.ownerId === this.#owner) await fs.unlink(brokerDiscoveryPath(this.settings.agentDir));
1301
- } catch (e) {
1302
- if ((e as NodeJS.ErrnoException).code !== "ENOENT") throw e;
1294
+ try {
1295
+ await this.#transport?.stop();
1296
+ this.#transport = null;
1297
+ if (mode === "lost-root")
1298
+ await Promise.race([Promise.allSettled(this.#admitted), Bun.sleep(BROKER_SETTLEMENT_MS)]);
1299
+ else await Promise.allSettled(this.#admitted);
1300
+ this.#publication?.close();
1301
+ this.#publication = null;
1302
+ if (mode === "owned-root" && this.discovery?.ownerId === this.#owner) {
1303
+ try {
1304
+ const disk = JSON.parse(await fs.readFile(brokerDiscoveryPath(this.settings.agentDir), "utf8")) as {
1305
+ ownerId?: string;
1306
+ };
1307
+ if (disk.ownerId === this.#owner) await fs.unlink(brokerDiscoveryPath(this.settings.agentDir));
1308
+ } catch (e) {
1309
+ if ((e as NodeJS.ErrnoException).code !== "ENOENT") throw e;
1310
+ }
1311
+ await this.#releaseOwnedLock();
1312
+ }
1313
+ } finally {
1314
+ const disposeModelPin = this.#ownsResolveModelPin ? this.#resolveModelPin.dispose?.() : undefined;
1315
+ if (mode === "lost-root" && disposeModelPin !== undefined) {
1316
+ void disposeModelPin.catch(() => undefined);
1317
+ await Promise.race([disposeModelPin, Bun.sleep(BROKER_SETTLEMENT_MS)]);
1318
+ } else {
1319
+ await disposeModelPin;
1303
1320
  }
1304
- await this.#releaseOwnedLock();
1321
+ this.discovery = null;
1305
1322
  }
1306
- this.discovery = null;
1307
1323
  })();
1308
1324
  void this.#completionTask.then(this.#resolveCompletion, this.#rejectCompletion);
1309
1325
  return this.#completionTask;
@@ -1417,14 +1433,25 @@ export class Broker {
1417
1433
 
1418
1434
  try {
1419
1435
  const endpointPath = path.join(record.locator.stateRoot, "sdk", `${record.sessionId}.json`);
1420
- const [source, metadata] = await Promise.all([fs.readFile(endpointPath, "utf8"), fs.stat(endpointPath)]);
1436
+ const before = await fs.lstat(endpointPath, { bigint: true });
1437
+ if (!before.isFile()) return error("endpoint_stale", "session endpoint is not a regular file");
1438
+ const source = await fs.readFile(endpointPath, "utf8");
1439
+ const metadata = await fs.lstat(endpointPath, { bigint: true });
1440
+ if (
1441
+ before.dev !== metadata.dev ||
1442
+ before.ino !== metadata.ino ||
1443
+ before.size !== metadata.size ||
1444
+ before.mtimeNs !== metadata.mtimeNs ||
1445
+ before.ctimeNs !== metadata.ctimeNs
1446
+ )
1447
+ return error("endpoint_stale", "session endpoint changed during read");
1421
1448
  const endpoint = JSON.parse(source) as Record<string, unknown>;
1422
1449
  if (
1423
1450
  endpoint.sessionId !== record.sessionId ||
1424
1451
  endpoint.pid !== record.pid ||
1425
1452
  endpoint.stale === true ||
1426
1453
  record.endpointMtimeMs === undefined ||
1427
- Math.abs(metadata.mtimeMs - record.endpointMtimeMs) > 0.001
1454
+ Math.abs(Number(metadata.mtimeNs) / 1_000_000 - record.endpointMtimeMs) > 0.001
1428
1455
  )
1429
1456
  return error("endpoint_stale", "session endpoint is stale");
1430
1457
  await this.index.refresh();
@@ -14,7 +14,10 @@ function nativeLifecycle(): typeof import("@gajae-code/natives") {
14
14
  }
15
15
 
16
16
  import { $credentialEnv, logger, resolveEquivalentPath } from "@gajae-code/utils";
17
-
17
+ import {
18
+ loadAcceptedModelPresetRegistry,
19
+ loadAcceptedModelPresetRegistryAsync,
20
+ } from "../../config/model-preset-registry";
18
21
  import {
19
22
  isModelProfileError,
20
23
  type ModelProfileErrorDetails,
@@ -609,13 +612,36 @@ function text(value: unknown): string | undefined {
609
612
  return typeof value === "string" && value ? value : undefined;
610
613
  }
611
614
 
612
- function validateBrokerModelPreset(agentDir: string, requestedProfile: string): string | BrokerResponse {
615
+ function validateBrokerModelPresetSync(agentDir: string, requestedProfile: string): string | BrokerResponse {
616
+ const modelsConfigFile = ModelsConfigFile.relocate(path.join(agentDir, "models.yml"));
617
+ modelsConfigFile.invalidate();
618
+ const loaded = modelsConfigFile.tryLoad();
619
+ const accepted = loadAcceptedModelPresetRegistry(agentDir);
620
+ const profiles = mergeModelProfiles(loaded.status === "ok" ? loaded.value.profiles : undefined, accepted.profiles);
621
+ try {
622
+ return validateModelProfileName(
623
+ requestedProfile,
624
+ profiles,
625
+ accepted.error ?? (loaded.status === "error" ? loaded.error : undefined),
626
+ );
627
+ } catch (error) {
628
+ if (isModelProfileError(error)) return fail(error.code, error.message, undefined, error.details);
629
+ throw error;
630
+ }
631
+ }
632
+
633
+ async function validateBrokerModelPreset(agentDir: string, requestedProfile: string): Promise<string | BrokerResponse> {
613
634
  const modelsConfigFile = ModelsConfigFile.relocate(path.join(agentDir, "models.yml"));
614
635
  modelsConfigFile.invalidate();
615
636
  const loaded = modelsConfigFile.tryLoad();
616
- const profiles = mergeModelProfiles(loaded.status === "ok" ? loaded.value.profiles : undefined);
637
+ const accepted = await loadAcceptedModelPresetRegistryAsync(agentDir);
638
+ const profiles = mergeModelProfiles(loaded.status === "ok" ? loaded.value.profiles : undefined, accepted.profiles);
617
639
  try {
618
- return validateModelProfileName(requestedProfile, profiles, loaded.status === "error" ? loaded.error : undefined);
640
+ return validateModelProfileName(
641
+ requestedProfile,
642
+ profiles,
643
+ accepted.error ?? (loaded.status === "error" ? loaded.error : undefined),
644
+ );
619
645
  } catch (error) {
620
646
  if (isModelProfileError(error)) return fail(error.code, error.message, undefined, error.details);
621
647
  throw error;
@@ -623,7 +649,7 @@ function validateBrokerModelPreset(agentDir: string, requestedProfile: string):
623
649
  }
624
650
 
625
651
  export function validateBrokerModelPresetForTest(agentDir: string, requestedProfile: string): string | BrokerResponse {
626
- return validateBrokerModelPreset(agentDir, requestedProfile);
652
+ return validateBrokerModelPresetSync(agentDir, requestedProfile);
627
653
  }
628
654
 
629
655
  function readinessTimeout(input: Input): number | BrokerResponse {
@@ -4030,7 +4056,7 @@ async function launchInput(
4030
4056
  if (input.modelPreset !== undefined && (typeof input.modelPreset !== "string" || input.modelPreset.length === 0))
4031
4057
  return fail("invalid_input", "modelPreset must be a non-empty exact profile ID.");
4032
4058
  if (modelPreset !== undefined) {
4033
- const validatedModelPreset = validateBrokerModelPreset(broker.settings.agentDir, modelPreset);
4059
+ const validatedModelPreset = await validateBrokerModelPreset(broker.settings.agentDir, modelPreset);
4034
4060
  if (typeof validatedModelPreset !== "string") return validatedModelPreset;
4035
4061
  modelPreset = validatedModelPreset;
4036
4062
  }
@@ -132,8 +132,8 @@ export type ChatDaemonAction = "stop" | "reload";
132
132
  * would retain the old hot polling loop, so replacement is required.
133
133
  */
134
134
  export const CHAT_DAEMON_GENERATIONS: Readonly<Record<ChatDaemonKind, number>> = {
135
- discord: 69,
136
- slack: 72,
135
+ discord: 71,
136
+ slack: 74,
137
137
  };
138
138
 
139
139
  export function chatDaemonGeneration(kind: ChatDaemonKind): number {
@@ -114,6 +114,10 @@ function readGeneration(value: unknown): number | undefined {
114
114
  return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : undefined;
115
115
  }
116
116
 
117
+ function readSequence(value: unknown): number | undefined {
118
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : undefined;
119
+ }
120
+
117
121
  /**
118
122
  * What one representation of a frame states about an identity it owns.
119
123
  *
@@ -138,7 +142,7 @@ const ABSENT_IDENTITY: IdentityClaim<never> = { state: "absent" };
138
142
  */
139
143
  function identityClaim<T>(
140
144
  frame: Record<string, unknown> | undefined,
141
- key: "sessionId" | "generation" | "name" | "kind",
145
+ key: "sessionId" | "generation" | "name" | "kind" | "seq",
142
146
  read: (value: unknown) => T | undefined,
143
147
  ): IdentityClaim<T> {
144
148
  if (!frame || !Object.hasOwn(frame, key)) return ABSENT_IDENTITY;
@@ -232,11 +236,17 @@ function correlateFrame(frame: Record<string, unknown>): CorrelatedFrame | undef
232
236
  identityClaim(payload, "generation", readGeneration),
233
237
  );
234
238
  if (!generation.ok) return undefined;
239
+ const seq = reconcileIdentity(
240
+ identityClaim(frame, "seq", readSequence),
241
+ identityClaim(payload, "seq", readSequence),
242
+ );
243
+ if (!seq.ok) return undefined;
235
244
  return {
236
245
  body,
237
246
  name: envelopeName.value ?? bodyName,
238
247
  sessionId: sessionId.value,
239
248
  generation: generation.value,
249
+ seq: seq.value,
240
250
  };
241
251
  }
242
252
 
@@ -21,6 +21,8 @@ import type {
21
21
  ReconciliationStore,
22
22
  } from "./reconciliation-store";
23
23
 
24
+ const EMPTY_PROMPT_FAILURE = { code: "prompt_failed", message: "Prompt submission failed." } as const;
25
+
24
26
  export type { ReconciliationKind };
25
27
 
26
28
  export interface KindCorrelation extends PromptCorrelation {
@@ -51,8 +53,13 @@ export interface KindAwareReconciliation {
51
53
  kind: ReconciliationKind,
52
54
  correlation: PromptCorrelation | undefined,
53
55
  frame:
54
- | { type: "agent_start" | "agent_end"; content?: TurnResultContent }
55
- | { type: "agent_failed"; error: unknown; content?: TurnResultContent },
56
+ | {
57
+ type: "agent_start" | "agent_end";
58
+ content?: TurnResultContent;
59
+ hasActivity?: boolean;
60
+ outcome?: SdkPromptTerminalOutcome;
61
+ }
62
+ | { type: "agent_failed"; error: unknown; content?: TurnResultContent; hasActivity?: boolean },
56
63
  ): Promise<void>;
57
64
  claimPendingOutcome(
58
65
  kind: ReconciliationKind,
@@ -340,8 +347,13 @@ export function createKindAwareReconciliation(
340
347
  kind: ReconciliationKind,
341
348
  correlation: PromptCorrelation | undefined,
342
349
  frame:
343
- | { type: "agent_start" | "agent_end"; content?: TurnResultContent }
344
- | { type: "agent_failed"; error: unknown; content?: TurnResultContent },
350
+ | {
351
+ type: "agent_start" | "agent_end";
352
+ content?: TurnResultContent;
353
+ hasActivity?: boolean;
354
+ outcome?: SdkPromptTerminalOutcome;
355
+ }
356
+ | { type: "agent_failed"; error: unknown; content?: TurnResultContent; hasActivity?: boolean },
345
357
  ) => {
346
358
  if (!correlation) return;
347
359
  await queueMutation(candidate => {
@@ -355,7 +367,13 @@ export function createKindAwareReconciliation(
355
367
  // as-is (cleanupRecords is intentionally not re-run). First reason wins: a
356
368
  // late generic frame never overwrites a specific one. Persisted so the reason
357
369
  // survives reconnect/restart reconciliation.
358
- if (frame.type === "agent_failed" && record.error === undefined) {
370
+ if (frame.type === "agent_failed") {
371
+ const failure = sanitizePromptFailure(frame.error);
372
+ if (
373
+ record.error !== undefined &&
374
+ !(record.error.code === "agent_failed" && failure.code !== "agent_failed")
375
+ )
376
+ return { value: undefined, changed: false };
359
377
  record.error = sanitizePromptFailure(frame.error);
360
378
  return { value: undefined, changed: true };
361
379
  }
@@ -373,7 +391,9 @@ export function createKindAwareReconciliation(
373
391
  if (frame.type === "agent_failed") {
374
392
  // agent_failed is additive diagnostics; agent_end remains the sole
375
393
  // terminal lifecycle boundary for the correlated invocation.
376
- record.error ??= sanitizePromptFailure(frame.error);
394
+ const failure = sanitizePromptFailure(frame.error);
395
+ if (record.error === undefined || (record.error.code === "agent_failed" && failure.code !== "agent_failed"))
396
+ record.error = failure;
377
397
  delete record.deadlineRecoveryPending;
378
398
  delete record.deadlineMaxAt;
379
399
  return { value: undefined, changed: true };
@@ -404,7 +424,17 @@ export function createKindAwareReconciliation(
404
424
  record.receiptState = record.pendingReceiptState ?? "missing";
405
425
  record.pendingReceiptState = undefined;
406
426
  } else {
407
- record.status = record.error === undefined ? "terminal_ok" : "failed";
427
+ if (
428
+ kind === "prompt" &&
429
+ record.error === undefined &&
430
+ frame.type === "agent_end" &&
431
+ frame.outcome?.kind !== "stopped" &&
432
+ !frame.content?.text.trim() &&
433
+ !frame.hasActivity
434
+ ) {
435
+ record.status = "failed";
436
+ record.error = EMPTY_PROMPT_FAILURE;
437
+ } else record.status = record.error === undefined ? "terminal_ok" : "failed";
408
438
  record.receiptState = frame.content?.text?.trim() ? "present" : "missing";
409
439
  }
410
440
  cleanupRecords(candidate);
@@ -445,6 +475,7 @@ export function createKindAwareReconciliation(
445
475
  : undefined;
446
476
  const content =
447
477
  typeof arg4 === "function" ? arg6 : typeof arg5 === "object" && arg5 !== null && "code" in arg5 ? arg6 : arg5;
478
+ const sanitizedContent = sanitizeTurnResultContent(content);
448
479
  await queueMutation(candidate => {
449
480
  if (isCurrent !== undefined && !isCurrent()) return { value: undefined, changed: false };
450
481
  const record = candidate.get(keyOf(kind, correlation));
@@ -455,8 +486,15 @@ export function createKindAwareReconciliation(
455
486
  if (finalOutcome?.kind === "failed") {
456
487
  record.status = "failed";
457
488
  record.error = recordError ?? { code: finalOutcome.code, message: finalOutcome.message };
489
+ } else if (
490
+ kind === "prompt" &&
491
+ ((finalOutcome === undefined && !sanitizedContent?.text.trim()) ||
492
+ (finalOutcome?.kind !== "stopped" && content !== undefined && !sanitizedContent?.text.trim()))
493
+ ) {
494
+ record.status = "failed";
495
+ record.error = EMPTY_PROMPT_FAILURE;
458
496
  } else record.status = "terminal_ok";
459
- record.content = sanitizeTurnResultContent(content);
497
+ record.content = sanitizedContent;
460
498
  record.outcome = finalOutcome;
461
499
  record.receiptState = record.pendingReceiptState ?? "unknown";
462
500
  record.pendingOutcome = undefined;
@@ -23,11 +23,13 @@
23
23
  */
24
24
 
25
25
  import { PROMPT_FAILURE_CODE_MAX, sanitizePromptFailure } from "../prompt-failure";
26
+ import type { SdkPromptTerminalOutcome } from "../prompt-status";
26
27
  import type { ReceiptState } from "../receipt-state";
27
28
 
28
29
  export { PROMPT_FAILURE_CODE_MAX, sanitizePromptFailure };
29
30
  export const PROMPT_RECONCILIATION_ACTIVE_CAPACITY = 128;
30
31
  export const PROMPT_RECONCILIATION_TERMINAL_CAPACITY = 256;
32
+ const EMPTY_PROMPT_FAILURE = { code: "prompt_failed", message: "Prompt submission failed." } as const;
31
33
 
32
34
  export type PromptReconciliationStatus = "accepted" | "in_flight" | "terminal_ok" | "failed";
33
35
 
@@ -101,8 +103,13 @@ export interface PromptReconciliation {
101
103
  correlation: PromptCorrelation | undefined,
102
104
  frame:
103
105
  | { type: "agent_start" }
104
- | { type: "agent_end"; finalText?: string }
105
- | { type: "agent_failed"; error: unknown; finalText?: string },
106
+ | {
107
+ type: "agent_end";
108
+ finalText?: string;
109
+ hasActivity?: boolean;
110
+ outcome?: SdkPromptTerminalOutcome;
111
+ }
112
+ | { type: "agent_failed"; error: unknown; finalText?: string; hasActivity?: boolean },
106
113
  ): void;
107
114
  lookup(selector: { commandId?: string; turnId?: string; clientRef?: string }): TurnPromptReconciliation;
108
115
  cleanup(): void;
@@ -190,8 +197,13 @@ export function createPromptReconciliation(options: { now?: () => number } = {})
190
197
  correlation: PromptCorrelation | undefined,
191
198
  frame:
192
199
  | { type: "agent_start" }
193
- | { type: "agent_end"; finalText?: string }
194
- | { type: "agent_failed"; error: unknown; finalText?: string },
200
+ | {
201
+ type: "agent_end";
202
+ finalText?: string;
203
+ hasActivity?: boolean;
204
+ outcome?: SdkPromptTerminalOutcome;
205
+ }
206
+ | { type: "agent_failed"; error: unknown; finalText?: string; hasActivity?: boolean },
195
207
  ) => {
196
208
  if (!correlation) return;
197
209
  const record = records.get(keyOf(correlation));
@@ -202,8 +214,11 @@ export function createPromptReconciliation(options: { now?: () => number } = {})
202
214
  // the settled record instead of being dropped; it must not resurrect it,
203
215
  // so status, terminalAt, retention order, and clientRefIndex stay as-is.
204
216
  // First reason wins: a late generic frame never overwrites a specific one.
205
- if (frame.type === "agent_failed" && record.error === undefined)
206
- record.error = sanitizePromptFailure(frame.error);
217
+ if (frame.type === "agent_failed") {
218
+ const failure = sanitizePromptFailure(frame.error);
219
+ if (record.error === undefined || (record.error.code === "agent_failed" && failure.code !== "agent_failed"))
220
+ record.error = failure;
221
+ }
207
222
  return;
208
223
  }
209
224
  if (frame.type === "agent_start") {
@@ -218,6 +233,18 @@ export function createPromptReconciliation(options: { now?: () => number } = {})
218
233
  if (frame.type === "agent_failed") {
219
234
  record.status = "failed";
220
235
  record.error = sanitizePromptFailure(frame.error);
236
+ } else if (record.error !== undefined) {
237
+ // Preserve an actionable provider failure already recorded before a late
238
+ // empty agent_end; the empty frame cannot downgrade or replace its cause.
239
+ record.status = "failed";
240
+ } else if (frame.type === "agent_end" && frame.outcome?.kind === "stopped") {
241
+ record.status = "terminal_ok";
242
+ } else if (!frame.finalText?.trim() && !frame.hasActivity) {
243
+ // A prompt cannot claim successful completion without assistant content.
244
+ // Keep the terminal transition first-wins, but fail closed instead of
245
+ // exposing an empty zero-token turn as terminal_ok.
246
+ record.status = "failed";
247
+ record.error = EMPTY_PROMPT_FAILURE;
221
248
  } else {
222
249
  record.status = "terminal_ok";
223
250
  }
@@ -74,5 +74,9 @@ export function normalizeSlackConversation(record: SlackConversation): SlackConv
74
74
  }
75
75
 
76
76
  export function acceptsSlackInbound(record: SlackConversation, rootTs: string, endpointGeneration: number): boolean {
77
- return record.state === "active" && record.rootTs === rootTs && record.endpointGeneration === endpointGeneration;
77
+ return (
78
+ (record.state === "active" || record.state === "resumed_root") &&
79
+ record.rootTs === rootTs &&
80
+ record.endpointGeneration === endpointGeneration
81
+ );
78
82
  }
@@ -283,8 +283,15 @@ export const SDK_LIFECYCLE_ROUTER_PROTOCOL_VERSION = 1;
283
283
  * Generation 177 consumes an exact same-window lean settlement receipt only
284
284
  * after its autonomous ask lead-in publishes successfully, preventing idle
285
285
  * replay without changing the serving protocol.
286
+ * Generation 178: idle publication waits for positioned identity delivery to
287
+ * cross the native writer barrier before the independent broadcast lane.
288
+ * Generation 179 tolerates a bounded consecutive run of refused notification
289
+ * publications in the router instead of cancelling the subscription on the
290
+ * first refusal, so an owner still running the generation-178 router keeps
291
+ * killing a session's mirroring after one transient rejection and must be
292
+ * replaced across this upgrade.
286
293
  */
287
- export const DAEMON_GENERATION = 178;
294
+ export const DAEMON_GENERATION = 179;
288
295
 
289
296
  /**
290
297
  * Serving-compatibility boundary for daemon lifecycle requests. Epoch 7