@gajae-code/coding-agent 0.16.4 → 0.16.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 (198) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/types/capability/types.d.ts +4 -0
  3. package/dist/types/cli/customize-doctor.d.ts +22 -0
  4. package/dist/types/config/file-lock.d.ts +31 -3
  5. package/dist/types/coordinator-mcp/question-state.d.ts +7 -0
  6. package/dist/types/coordinator-mcp/server.d.ts +7 -0
  7. package/dist/types/discovery/helpers.d.ts +33 -0
  8. package/dist/types/exec/bash-executor.d.ts +17 -1
  9. package/dist/types/exec/bash-shell-guardian.d.ts +15 -0
  10. package/dist/types/exec/bash-shell-runtime-entry.d.ts +2 -0
  11. package/dist/types/exec/bash-shell-signals.d.ts +2 -0
  12. package/dist/types/exec/bash-shell-supervisor-entry.d.ts +1 -0
  13. package/dist/types/exec/bash-shell-supervisor.d.ts +10 -0
  14. package/dist/types/exec/bash-shell-worker-entry.d.ts +2 -0
  15. package/dist/types/exec/bash-shell-worker-protocol.d.ts +63 -0
  16. package/dist/types/exec/bash-shell-worker.d.ts +1 -0
  17. package/dist/types/exec/isolated-shell.d.ts +19 -0
  18. package/dist/types/extensibility/extensions/function-hooks-internal.d.ts +9 -0
  19. package/dist/types/extensibility/extensions/function-hooks.d.ts +169 -0
  20. package/dist/types/extensibility/extensions/index.d.ts +1 -0
  21. package/dist/types/extensibility/extensions/runner.d.ts +46 -6
  22. package/dist/types/extensibility/extensions/types.d.ts +16 -0
  23. package/dist/types/extensibility/extensions/wrapper.d.ts +3 -1
  24. package/dist/types/extensibility/gjc-plugins/constrained-hooks.d.ts +35 -2
  25. package/dist/types/extensibility/gjc-plugins/types.d.ts +25 -0
  26. package/dist/types/extensibility/hooks/types.d.ts +4 -0
  27. package/dist/types/extensibility/runtime-skill-discovery.d.ts +4 -1
  28. package/dist/types/extensibility/skill-management.d.ts +9 -4
  29. package/dist/types/extensibility/skills.d.ts +2 -0
  30. package/dist/types/gjc-runtime/empty-delete-gc.d.ts +4 -1
  31. package/dist/types/gjc-runtime/linux-proc.d.ts +2 -0
  32. package/dist/types/gjc-runtime/session-state-lock.d.ts +12 -0
  33. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +41 -3
  34. package/dist/types/gjc-runtime/ultragoal-succession.d.ts +251 -0
  35. package/dist/types/modes/components/bash-execution.d.ts +1 -0
  36. package/dist/types/modes/components/execution-shared.d.ts +1 -0
  37. package/dist/types/modes/interactive-mode.d.ts +2 -0
  38. package/dist/types/modes/types.d.ts +6 -0
  39. package/dist/types/sdk/broker/ensure.d.ts +32 -0
  40. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +0 -1
  41. package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
  42. package/dist/types/sdk/broker/startup-failure.d.ts +4 -1
  43. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -0
  44. package/dist/types/sdk/bus/chat-effect-journal.d.ts +17 -2
  45. package/dist/types/sdk/bus/conversation-store.d.ts +11 -0
  46. package/dist/types/sdk/bus/lifecycle-commands.d.ts +8 -8
  47. package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
  48. package/dist/types/sdk/bus/topic-registry.d.ts +8 -0
  49. package/dist/types/sdk/client/client.d.ts +6 -0
  50. package/dist/types/sdk/host/host.d.ts +3 -1
  51. package/dist/types/sdk/host/session-runtime.d.ts +4 -2
  52. package/dist/types/sdk/lifecycle/service.d.ts +1 -0
  53. package/dist/types/sdk/router/session-router.d.ts +10 -2
  54. package/dist/types/sdk/session.d.ts +5 -1
  55. package/dist/types/session/agent-session.d.ts +2 -0
  56. package/dist/types/session/fallback-chain-controller.d.ts +15 -0
  57. package/dist/types/session/internal/managed-session-storage.d.ts +7 -0
  58. package/dist/types/session/messages.d.ts +1 -0
  59. package/dist/types/session/session-manager.d.ts +3 -1
  60. package/dist/types/setup/paseo/announce.d.ts +4 -3
  61. package/dist/types/skill-state/canonical-skills.d.ts +4 -0
  62. package/dist/types/system-prompt.d.ts +8 -0
  63. package/dist/types/task/executor.d.ts +3 -0
  64. package/dist/types/tools/bash.d.ts +1 -0
  65. package/dist/types/tools/github-cache.d.ts +2 -1
  66. package/dist/types/tools/index.d.ts +2 -0
  67. package/package.json +9 -7
  68. package/scripts/generate-sdk-operation-inventory.ts +2 -0
  69. package/src/capability/index.ts +14 -2
  70. package/src/capability/types.ts +4 -0
  71. package/src/cli/customize-doctor.ts +155 -12
  72. package/src/cli.ts +22 -0
  73. package/src/commands/sdk.ts +74 -31
  74. package/src/config/file-lock-gc.ts +40 -2
  75. package/src/config/file-lock.ts +198 -14
  76. package/src/config/model-preset-registry.ts +115 -16
  77. package/src/config/model-registry.ts +16 -12
  78. package/src/coordinator-mcp/question-state.ts +33 -2
  79. package/src/coordinator-mcp/server.ts +477 -101
  80. package/src/crash/sanitize.ts +1 -1
  81. package/src/defaults/gjc/skills/ultragoal/SKILL.md +22 -0
  82. package/src/discovery/builtin.ts +50 -62
  83. package/src/discovery/helpers.ts +297 -32
  84. package/src/exec/bash-executor.ts +330 -50
  85. package/src/exec/bash-shell-guardian.ts +388 -0
  86. package/src/exec/bash-shell-runtime-entry.ts +5 -0
  87. package/src/exec/bash-shell-signals.ts +29 -0
  88. package/src/exec/bash-shell-supervisor-entry.ts +3 -0
  89. package/src/exec/bash-shell-supervisor.ts +359 -0
  90. package/src/exec/bash-shell-worker-entry.ts +5 -0
  91. package/src/exec/bash-shell-worker-protocol.ts +36 -0
  92. package/src/exec/bash-shell-worker.ts +116 -0
  93. package/src/exec/isolated-shell.ts +393 -0
  94. package/src/extensibility/extensions/function-hooks-internal.ts +73 -0
  95. package/src/extensibility/extensions/function-hooks.ts +959 -0
  96. package/src/extensibility/extensions/index.ts +22 -0
  97. package/src/extensibility/extensions/loader.ts +53 -2
  98. package/src/extensibility/extensions/runner.ts +895 -264
  99. package/src/extensibility/extensions/types.ts +42 -0
  100. package/src/extensibility/extensions/wrapper.ts +56 -15
  101. package/src/extensibility/gjc-plugins/compiler.ts +20 -2
  102. package/src/extensibility/gjc-plugins/constrained-hooks.ts +321 -23
  103. package/src/extensibility/gjc-plugins/registry.ts +113 -1
  104. package/src/extensibility/gjc-plugins/runtime-quarantine.ts +20 -2
  105. package/src/extensibility/gjc-plugins/schema.ts +121 -4
  106. package/src/extensibility/gjc-plugins/types.ts +25 -0
  107. package/src/extensibility/hooks/loader.ts +133 -16
  108. package/src/extensibility/hooks/types.ts +28 -0
  109. package/src/extensibility/runtime-skill-discovery.ts +80 -47
  110. package/src/extensibility/skill-management.ts +200 -52
  111. package/src/extensibility/skills.ts +29 -9
  112. package/src/gjc-runtime/empty-delete-gc.ts +94 -4
  113. package/src/gjc-runtime/gc-runtime.ts +29 -0
  114. package/src/gjc-runtime/linux-proc.ts +14 -0
  115. package/src/gjc-runtime/session-state-lock.ts +51 -8
  116. package/src/gjc-runtime/session-state-sidecar.ts +27 -11
  117. package/src/gjc-runtime/state-writer.ts +30 -29
  118. package/src/gjc-runtime/ultragoal-runtime.ts +189 -47
  119. package/src/gjc-runtime/ultragoal-succession.ts +1667 -0
  120. package/src/gjc-runtime/workflow-manifest.generated.json +52 -1
  121. package/src/gjc-runtime/workflow-manifest.ts +16 -0
  122. package/src/harness-control-plane/session-lease.ts +2 -2
  123. package/src/internal-urls/docs-index.generated.ts +6 -6
  124. package/src/lsp/config.ts +10 -3
  125. package/src/lsp/index.ts +1 -1
  126. package/src/lsp/path-trust.ts +69 -14
  127. package/src/main.ts +6 -1
  128. package/src/modes/acp/acp-agent.ts +16 -3
  129. package/src/modes/components/bash-execution.ts +4 -1
  130. package/src/modes/components/execution-shared.ts +4 -1
  131. package/src/modes/components/session-observer-overlay.ts +1 -1
  132. package/src/modes/components/tool-status-header.ts +3 -1
  133. package/src/modes/controllers/command-controller.ts +1 -0
  134. package/src/modes/controllers/event-controller.ts +3 -2
  135. package/src/modes/controllers/extension-ui-controller.ts +1 -0
  136. package/src/modes/controllers/input-controller.ts +6 -3
  137. package/src/modes/interactive-mode.ts +6 -5
  138. package/src/modes/print-mode.ts +5 -1
  139. package/src/modes/prompt-action-autocomplete.ts +54 -27
  140. package/src/modes/runtime-init.ts +2 -0
  141. package/src/modes/types.ts +16 -3
  142. package/src/modes/utils/ui-helpers.ts +1 -0
  143. package/src/prompts/tools/subagent.md +1 -0
  144. package/src/runtime-mcp/manager.ts +20 -2
  145. package/src/runtime-mcp/pool.test.ts +20 -11
  146. package/src/sdk/broker/broker.ts +119 -28
  147. package/src/sdk/broker/ensure.ts +248 -41
  148. package/src/sdk/broker/lifecycle-ledger.ts +7 -17
  149. package/src/sdk/broker/lifecycle.ts +56 -6
  150. package/src/sdk/broker/startup-failure.ts +15 -6
  151. package/src/sdk/bus/chat-daemon-control.ts +3 -1
  152. package/src/sdk/bus/chat-daemon-runtime.ts +35 -5
  153. package/src/sdk/bus/chat-effect-journal.ts +132 -7
  154. package/src/sdk/bus/conversation-store.ts +29 -0
  155. package/src/sdk/bus/lifecycle-commands.ts +17 -11
  156. package/src/sdk/bus/slack-conversation.ts +1 -0
  157. package/src/sdk/bus/slack-daemon.ts +387 -48
  158. package/src/sdk/bus/telegram-daemon.ts +16 -2
  159. package/src/sdk/bus/topic-registry.ts +19 -0
  160. package/src/sdk/cli/master-cli.ts +1 -1
  161. package/src/sdk/cli/session-cli.ts +2 -2
  162. package/src/sdk/client/client.ts +66 -17
  163. package/src/sdk/host/host.ts +29 -9
  164. package/src/sdk/host/query/revision-store.ts +19 -13
  165. package/src/sdk/host/reverse-leases.ts +7 -4
  166. package/src/sdk/host/session-runtime.test.ts +50 -8
  167. package/src/sdk/host/session-runtime.ts +158 -51
  168. package/src/sdk/lifecycle/service.ts +4 -0
  169. package/src/sdk/protocol/operation-inventory.generated.json +11 -0
  170. package/src/sdk/router/session-router.ts +89 -38
  171. package/src/sdk/session.ts +155 -26
  172. package/src/sdk/transport/relay.ts +35 -13
  173. package/src/session/agent-session.ts +117 -52
  174. package/src/session/fallback-chain-controller.ts +38 -0
  175. package/src/session/internal/managed-session-storage.ts +19 -5
  176. package/src/session/messages.ts +5 -0
  177. package/src/session/session-manager.ts +29 -5
  178. package/src/session-import/redact.ts +5 -1
  179. package/src/setup/paseo/announce.ts +49 -43
  180. package/src/setup/paseo/setup-deps.ts +1 -1
  181. package/src/skill-state/canonical-skills.ts +12 -0
  182. package/src/skills/skills.test.ts +41 -21
  183. package/src/system-prompt.ts +25 -3
  184. package/src/task/executor.ts +5 -0
  185. package/src/task/index.ts +4 -0
  186. package/src/tools/bash.ts +28 -11
  187. package/src/tools/gh.ts +26 -3
  188. package/src/tools/github-cache.ts +68 -16
  189. package/src/tools/index.ts +2 -0
  190. package/src/tools/job.ts +13 -2
  191. package/src/tools/read.ts +14 -2
  192. package/src/tools/render-utils.ts +1 -3
  193. package/src/tools/resource-gc.ts +28 -8
  194. package/src/tools/skill-discovery.ts +5 -0
  195. package/src/tools/skill.ts +15 -0
  196. package/src/tools/subagent.ts +3 -2
  197. package/src/tools/tool-catalog.generated.ts +2 -2
  198. package/src/tools/ultragoal-ask-guard.ts +2 -2
@@ -439,6 +439,7 @@ import {
439
439
  type ConfiguredFallbackChain,
440
440
  cappedExponentialWithFullJitter,
441
441
  compactionRetryDelay,
442
+ describeCompactionCandidateFailures,
442
443
  effectiveFallbackDelay,
443
444
  FallbackChainController,
444
445
  type FallbackChainRuntimeState,
@@ -2791,7 +2792,7 @@ export class AgentSession {
2791
2792
  this.#activeAttemptScope = undefined;
2792
2793
  this.#activeLogicalRunId = undefined;
2793
2794
  }
2794
- #acceptSdkAttemptRun(handle: AttemptRunHandle, sdkRunToken?: string): void {
2795
+ #acceptSdkAttemptRun(handle: AttemptRunHandle, sdkRunToken?: string, sdkRunTokens?: string[]): void {
2795
2796
  const predecessorScope = this.#activeAttemptScope;
2796
2797
  const predecessorSdkRunToken =
2797
2798
  predecessorScope === undefined ? undefined : this.#sdkRunTokensByAttemptScope.get(predecessorScope);
@@ -2804,6 +2805,11 @@ export class AgentSession {
2804
2805
  if (sdkRunToken !== undefined) {
2805
2806
  this.#activeSdkRunToken = sdkRunToken;
2806
2807
  this.#sdkRunTokensByAttemptScope.set(handle.scope, sdkRunToken);
2808
+ const inheritedCohort =
2809
+ predecessorScope !== undefined && predecessorSdkRunToken === sdkRunToken
2810
+ ? this.#sdkRunCohortsByAttemptScope.get(predecessorScope)
2811
+ : undefined;
2812
+ this.#sdkRunCohortsByAttemptScope.set(handle.scope, sdkRunTokens ?? inheritedCohort ?? [sdkRunToken]);
2807
2813
  }
2808
2814
  if (carryRecoverySkip) this.#skipPostPromptRecoveryWaitByAttemptScope.add(handle.scope);
2809
2815
  }
@@ -3108,6 +3114,7 @@ export class AgentSession {
3108
3114
  #sdkRunTokensByQueuedMessage = new WeakMap<AgentMessage, string>();
3109
3115
  #skipPostPromptRecoveryWaitByAttemptScope = new WeakSet<AttemptScope>();
3110
3116
  #sdkRunTokensByAttemptScope = new WeakMap<AttemptScope, string>();
3117
+ #sdkRunCohortsByAttemptScope = new WeakMap<AttemptScope, string[]>();
3111
3118
  #activeSdkRunToken: string | undefined;
3112
3119
  #activeAttemptScope: AttemptScope | undefined;
3113
3120
  #attemptAuthority!: AttemptScopeAuthority;
@@ -4578,7 +4585,7 @@ export class AgentSession {
4578
4585
  this.#transformContext = config.transformContext ?? (messages => messages);
4579
4586
  const configuredOnPayload = config.onPayload;
4580
4587
  this.#onPayload = configuredOnPayload
4581
- ? (payload, model, scope) => configuredOnPayload(payload, model, scope)
4588
+ ? (payload, model, scope, signal) => configuredOnPayload(payload, model, scope, signal)
4582
4589
  : undefined;
4583
4590
  this.rawSseDebugBuffer = config.rawSseDebugBuffer ?? new RawSseDebugBuffer();
4584
4591
  // Avoid wrapping in an `async` closure when no user callback is configured: the
@@ -4587,9 +4594,9 @@ export class AgentSession {
4587
4594
  // shows up as ~3.5% self time in streaming profiles.
4588
4595
  const configuredOnResponse = config.onResponse;
4589
4596
  this.#onResponse = configuredOnResponse
4590
- ? async (response, model, scope) => {
4597
+ ? async (response, model, scope, signal) => {
4591
4598
  this.rawSseDebugBuffer.recordResponse(response, model);
4592
- await configuredOnResponse(response, model, scope);
4599
+ await configuredOnResponse(response, model, scope, signal);
4593
4600
  }
4594
4601
  : (response, model, _scope) => {
4595
4602
  this.rawSseDebugBuffer.recordResponse(response, model);
@@ -6925,12 +6932,15 @@ export class AgentSession {
6925
6932
  assistantMsg.stopReason !== "aborted" &&
6926
6933
  this.#retryAttempt > 0
6927
6934
  ) {
6935
+ // Clear ownership before notifying observers: agent_end may run while
6936
+ // the success event is awaiting its subscribers.
6937
+ const attempt = this.#retryAttempt;
6938
+ this.#retryAttempt = 0;
6928
6939
  await this.#emitSessionEvent({
6929
6940
  type: "auto_retry_end",
6930
6941
  success: true,
6931
- attempt: this.#retryAttempt,
6942
+ attempt,
6932
6943
  });
6933
- this.#retryAttempt = 0;
6934
6944
  // Settle the retry gate here, colocated with the success event, rather
6935
6945
  // than relying on the generic #resolveRetry() at the end of the
6936
6946
  // agent_end branch. That tail resolver is bypassed by every early
@@ -7530,15 +7540,19 @@ export class AgentSession {
7530
7540
  const inheritedSdkRunToken = options?.continueQueuedOnly
7531
7541
  ? undefined
7532
7542
  : (options?.sdkRunToken ?? scheduledSdkRunToken);
7533
- const consumedSdkRunToken = acceptance.consumedQueuedMessages
7543
+ const consumedSdkRunTokens = acceptance.consumedQueuedMessages
7534
7544
  .map(message => this.#sdkRunTokensByQueuedMessage.get(message))
7535
- .find((token): token is string => token !== undefined);
7536
- const sdkRunToken = consumedSdkRunToken ?? inheritedSdkRunToken;
7545
+ .filter((token): token is string => token !== undefined);
7546
+ const sdkRunToken = consumedSdkRunTokens[0] ?? inheritedSdkRunToken;
7537
7547
  this.#fireQueuedPromotionHooks(acceptance.consumedQueuedMessages, {
7538
7548
  startsOwnRun: startsOwn,
7539
7549
  });
7540
7550
  if (startsOwn) this.#activeSdkRunToken = sdkRunToken;
7541
- this.#acceptSdkAttemptRun(handle, sdkRunToken);
7551
+ this.#acceptSdkAttemptRun(
7552
+ handle,
7553
+ sdkRunToken,
7554
+ consumedSdkRunTokens.length > 0 ? consumedSdkRunTokens : undefined,
7555
+ );
7542
7556
  options?.onRunAccepted?.(handle);
7543
7557
  // Keep the queued token available through the acceptance callback;
7544
7558
  // SDK follow-up owners bind it to the new attempt scope there.
@@ -8674,6 +8688,9 @@ export class AgentSession {
8674
8688
  {
8675
8689
  type: "agent_start",
8676
8690
  ...(sdkRunToken ? { sdkRunToken } : {}),
8691
+ ...(deliveryScope
8692
+ ? { sdkRunTokens: this.#sdkRunCohortsByAttemptScope.get(deliveryScope as AttemptScope) }
8693
+ : {}),
8677
8694
  },
8678
8695
  undefined,
8679
8696
  deliveryScope,
@@ -9549,6 +9566,11 @@ export class AgentSession {
9549
9566
  });
9550
9567
  }
9551
9568
 
9569
+ /** Test seam: await all currently admitted coordinator sidecar writes. */
9570
+ async awaitCoordinatorRuntimeStatePersistenceForTests(): Promise<void> {
9571
+ await this.#coordinatorPersistQueue;
9572
+ await this.#drainUnbarrieredCoordinatorPersists();
9573
+ }
9552
9574
  queueCoordinatorRuntimeStatePersistForTests(event: AgentSessionEvent, gate: Promise<void>): Promise<void> {
9553
9575
  this.#agentEventAdmission.set(event, {
9554
9576
  persistGeneration: this.#coordinatorPersistGeneration,
@@ -10275,11 +10297,12 @@ export class AgentSession {
10275
10297
  let wrappersByVersion = this.#guardedToolWrapperCache.get(tool);
10276
10298
  const cached = wrappersByVersion?.get(cacheKey);
10277
10299
  if (cached) return cached as T;
10278
- const wrapped = this.#wrapToolForCwdTransitionFence(
10300
+ const innerTool = tool instanceof ExtensionToolWrapper ? tool.getInnerTool() : tool;
10301
+ const guarded = this.#wrapToolForCwdTransitionFence(
10279
10302
  this.#wrapToolForWorkflowMutationGuard(
10280
10303
  this.#wrapToolForAcpPermission(
10281
10304
  guardToolForUltragoalAsk(
10282
- tool,
10305
+ innerTool,
10283
10306
  () => this.sessionManager.getCwd(),
10284
10307
  () => ({
10285
10308
  activeSkillState: this.getActiveSkillState(),
@@ -10290,6 +10313,7 @@ export class AgentSession {
10290
10313
  ),
10291
10314
  ),
10292
10315
  );
10316
+ const wrapped = (this.#extensionRunner ? new ExtensionToolWrapper(guarded, this.#extensionRunner) : guarded) as T;
10293
10317
  // The object published into `agent.state.tools` — and therefore the object the
10294
10318
  // agent loop actually dispatches to — is this guard wrapper, not the registry
10295
10319
  // entry. A wrapper built from a proven built-in inherits that proof; one built
@@ -10970,10 +10994,10 @@ export class AgentSession {
10970
10994
  preparedOptions.onPayload = sessionOnPayload;
10971
10995
  } else {
10972
10996
  const requestOnPayload = options.onPayload;
10973
- preparedOptions.onPayload = async (payload, model, callbackScope) => {
10974
- const sessionPayload = await sessionOnPayload(payload, model, callbackScope);
10997
+ preparedOptions.onPayload = async (payload, model, callbackScope, signal) => {
10998
+ const sessionPayload = await sessionOnPayload(payload, model, callbackScope, signal);
10975
10999
  const sessionResolvedPayload = sessionPayload ?? payload;
10976
- const requestPayload = await requestOnPayload(sessionResolvedPayload, model, callbackScope);
11000
+ const requestPayload = await requestOnPayload(sessionResolvedPayload, model, callbackScope, signal);
10977
11001
  return requestPayload ?? sessionResolvedPayload;
10978
11002
  };
10979
11003
  }
@@ -10984,9 +11008,9 @@ export class AgentSession {
10984
11008
  preparedOptions.onResponse = sessionOnResponse;
10985
11009
  } else {
10986
11010
  const requestOnResponse = options.onResponse;
10987
- preparedOptions.onResponse = async (response, model, callbackScope) => {
10988
- await sessionOnResponse(response, model, callbackScope);
10989
- await requestOnResponse(response, model, callbackScope);
11011
+ preparedOptions.onResponse = async (response, model, callbackScope, signal) => {
11012
+ await sessionOnResponse(response, model, callbackScope, signal);
11013
+ await requestOnResponse(response, model, callbackScope, signal);
10990
11014
  };
10991
11015
  }
10992
11016
  }
@@ -11867,6 +11891,11 @@ export class AgentSession {
11867
11891
  const releaseStartupPromptWaiter = this.#reserveStartupPromptWaiter();
11868
11892
  try {
11869
11893
  await this.#promptInternal(text, options, releaseStartupPromptWaiter);
11894
+ // Agent-core converts listener failures into an aborted response. Keep a
11895
+ // rejected persistence fence typed at the public prompt boundary instead
11896
+ // of reporting the misleading provider-level "Request was aborted".
11897
+ const appendRejection = this.sessionManager.getAppendIdentityMismatch();
11898
+ if (appendRejection) throw appendRejection;
11870
11899
  } finally {
11871
11900
  releaseStartupPromptWaiter();
11872
11901
  }
@@ -12458,6 +12487,14 @@ export class AgentSession {
12458
12487
  hasPendingNextTurnMessages = pendingNextTurnMessageCount > 0;
12459
12488
  const promptAttribution: "user" | "agent" | undefined =
12460
12489
  "attribution" in message ? message.attribution : undefined;
12490
+ let effectivePrompt = expandedText;
12491
+ const messageImages =
12492
+ "content" in message && Array.isArray(message.content)
12493
+ ? message.content.filter((content): content is ImageContent => content.type === "image")
12494
+ : undefined;
12495
+ let effectiveImages =
12496
+ options?.images ?? (messageImages && messageImages.length > 0 ? messageImages : undefined);
12497
+ let promptMessage = message;
12461
12498
  let phaseACompleted = false;
12462
12499
  let fileMentionMessages: AgentMessage[] = [];
12463
12500
  let beforeAgentStartResultMessages: BeforeAgentStartInternalMessage[] = [];
@@ -12478,7 +12515,49 @@ export class AgentSession {
12478
12515
  if (!phaseACompleted) {
12479
12516
  phaseACompleted = true;
12480
12517
  // Phase A (one-time side-effectful products; runs once).
12481
- const fileMentions = extractFileMentions(expandedText);
12518
+ const beforeAgentStartSystemPrompt = await this.#buildSystemPromptForAgentStart(effectivePrompt);
12519
+ hindsightRecall = this.getHindsightSessionState()?.getRecallSnippetForInjection();
12520
+ planReferenceMessage = await this.#buildPlanReferenceMessage();
12521
+ // Emit before_agent_start extension event. Race hook completion with
12522
+ // prompt cancellation so a wedged hook cannot retain SDK prompt authority.
12523
+ if (this.#extensionRunner?.hasHandlers("before_agent_start")) this.#markRetryReplayUnsafe();
12524
+ if (this.#extensionRunner) {
12525
+ const result = await this.#awaitPromptPreflight(
12526
+ generation,
12527
+ preflightSignal,
12528
+ this.#extensionRunner.emitBeforeAgentStart(
12529
+ effectivePrompt,
12530
+ effectiveImages,
12531
+ beforeAgentStartSystemPrompt,
12532
+ preflightSignal,
12533
+ ),
12534
+ );
12535
+ if (result?.messages) beforeAgentStartResultMessages = [...result.messages];
12536
+ if (result?.prompt !== undefined) effectivePrompt = result.prompt;
12537
+ if (result && Object.hasOwn(result, "images")) effectiveImages = result.images;
12538
+ if (result?.prompt !== undefined || (result && Object.hasOwn(result, "images"))) {
12539
+ if (
12540
+ promptMessage.role === "user" ||
12541
+ promptMessage.role === "developer" ||
12542
+ promptMessage.role === "custom"
12543
+ ) {
12544
+ promptMessage = {
12545
+ ...promptMessage,
12546
+ content: [{ type: "text", text: effectivePrompt }, ...(effectiveImages ?? [])],
12547
+ };
12548
+ }
12549
+ }
12550
+ if (result?.systemPrompt !== undefined) {
12551
+ this.agent.setSystemPrompt(result.systemPrompt);
12552
+ } else if (result?.prompt !== undefined) {
12553
+ this.agent.setSystemPrompt(await this.#buildSystemPromptForAgentStart(effectivePrompt));
12554
+ } else {
12555
+ this.agent.setSystemPrompt(beforeAgentStartSystemPrompt);
12556
+ }
12557
+ } else {
12558
+ this.agent.setSystemPrompt(beforeAgentStartSystemPrompt);
12559
+ }
12560
+ const fileMentions = extractFileMentions(effectivePrompt);
12482
12561
  if (fileMentions.length > 0) {
12483
12562
  const cwd = this.sessionManager.getCwd();
12484
12563
  // Collect resolved paths already shown (read or mentioned) in the
@@ -12506,31 +12585,6 @@ export class AgentSession {
12506
12585
  recentlyShownPaths,
12507
12586
  });
12508
12587
  }
12509
- const beforeAgentStartSystemPrompt = await this.#buildSystemPromptForAgentStart(expandedText);
12510
- hindsightRecall = this.getHindsightSessionState()?.getRecallSnippetForInjection();
12511
- planReferenceMessage = await this.#buildPlanReferenceMessage();
12512
- // Emit before_agent_start extension event. Race hook completion with
12513
- // prompt cancellation so a wedged hook cannot retain SDK prompt authority.
12514
- if (this.#extensionRunner?.hasHandlers("before_agent_start")) this.#markRetryReplayUnsafe();
12515
- if (this.#extensionRunner) {
12516
- const result = await this.#awaitPromptPreflight(
12517
- generation,
12518
- preflightSignal,
12519
- this.#extensionRunner.emitBeforeAgentStart(
12520
- expandedText,
12521
- options?.images,
12522
- beforeAgentStartSystemPrompt,
12523
- ),
12524
- );
12525
- if (result?.messages) beforeAgentStartResultMessages = [...result.messages];
12526
- if (result?.systemPrompt !== undefined) {
12527
- this.agent.setSystemPrompt(result.systemPrompt);
12528
- } else {
12529
- this.agent.setSystemPrompt(beforeAgentStartSystemPrompt);
12530
- }
12531
- } else {
12532
- this.agent.setSystemPrompt(beforeAgentStartSystemPrompt);
12533
- }
12534
12588
  // Invoke first-party internal before-agent-start contributors. These
12535
12589
  // run alongside the extension runner (not via user-loaded hooks) and
12536
12590
  // append through the same custom-message attribution path. Errors are nonfatal.
@@ -12541,8 +12595,8 @@ export class AgentSession {
12541
12595
  generation,
12542
12596
  preflightSignal,
12543
12597
  contributor({
12544
- prompt: expandedText,
12545
- images: options?.images,
12598
+ prompt: effectivePrompt,
12599
+ images: effectiveImages,
12546
12600
  sessionId: this.sessionId,
12547
12601
  }),
12548
12602
  );
@@ -12600,7 +12654,7 @@ export class AgentSession {
12600
12654
  }
12601
12655
 
12602
12656
  const promptIndex = messages.length;
12603
- messages.push(message);
12657
+ messages.push(promptMessage);
12604
12658
 
12605
12659
  // Re-present captured pending next-turn messages (never drained here).
12606
12660
  // Reclassify deferred envelopes before injecting them into the new
@@ -19435,10 +19489,10 @@ export class AgentSession {
19435
19489
  isError: message.isError,
19436
19490
  content: this.#normalizeProviderReplayValue(message.content),
19437
19491
  };
19438
- case "bashExecution":
19492
+ case "pythonExecution":
19439
19493
  return {
19440
19494
  role: message.role,
19441
- command: message.command,
19495
+ code: message.code,
19442
19496
  output: message.output,
19443
19497
  exitCode: message.exitCode,
19444
19498
  cancelled: message.cancelled,
@@ -19456,12 +19510,13 @@ export class AgentSession {
19456
19510
  : undefined,
19457
19511
  excludeFromContext: message.excludeFromContext,
19458
19512
  };
19459
- case "pythonExecution":
19513
+ case "bashExecution":
19460
19514
  return {
19461
19515
  role: message.role,
19462
- code: message.code,
19516
+ command: message.command,
19463
19517
  output: message.output,
19464
19518
  exitCode: message.exitCode,
19519
+ signal: message.signal,
19465
19520
  cancelled: message.cancelled,
19466
19521
  meta: message.meta
19467
19522
  ? {
@@ -20160,6 +20215,10 @@ export class AgentSession {
20160
20215
  const telemetry = resolveTelemetry(this.agent.telemetry, this.sessionId);
20161
20216
  let compactResult: CompactionResult | undefined;
20162
20217
  let lastError: unknown;
20218
+ // Every candidate that was tried and failed, in order. Only the last failure
20219
+ // used to reach the user, which named whichever same-provider fallback the
20220
+ // chain ended on and hid that the session model itself had already failed.
20221
+ const candidateFailures: Array<{ model: string; message: string }> = [];
20163
20222
 
20164
20223
  for (const candidate of candidates) {
20165
20224
  const apiKey = await this.#modelRegistry.getApiKey(candidate, this.credentialSessionId);
@@ -20193,6 +20252,7 @@ export class AgentSession {
20193
20252
  const message = error instanceof Error ? error.message : String(error);
20194
20253
  if (this.#isCompactionAuthFailure(error)) {
20195
20254
  lastError = this.#buildCompactionAuthError();
20255
+ candidateFailures.push({ model: `${candidate.provider}/${candidate.id}`, message });
20196
20256
  break;
20197
20257
  }
20198
20258
  const retryAfterMs = this.#parseRetryAfterMsFromError(message);
@@ -20205,6 +20265,7 @@ export class AgentSession {
20205
20265
  isUsageLimitError(message));
20206
20266
  if (!shouldRetry) {
20207
20267
  lastError = error;
20268
+ candidateFailures.push({ model: `${candidate.provider}/${candidate.id}`, message });
20208
20269
  break;
20209
20270
  }
20210
20271
 
@@ -20229,6 +20290,7 @@ export class AgentSession {
20229
20290
  model: `${candidate.provider}/${candidate.id}`,
20230
20291
  });
20231
20292
  lastError = error;
20293
+ candidateFailures.push({ model: `${candidate.provider}/${candidate.id}`, message });
20232
20294
  break; // Exit retry loop, continue to next candidate
20233
20295
  }
20234
20296
  // No more candidates - we have to wait
@@ -20254,7 +20316,9 @@ export class AgentSession {
20254
20316
 
20255
20317
  if (!compactResult) {
20256
20318
  if (lastError) {
20257
- throw lastError;
20319
+ throw candidateFailures.length > 1
20320
+ ? describeCompactionCandidateFailures(candidateFailures, lastError)
20321
+ : lastError;
20258
20322
  }
20259
20323
  throw new Error("Compaction failed: no available model");
20260
20324
  }
@@ -22329,6 +22393,7 @@ export class AgentSession {
22329
22393
  command,
22330
22394
  output: result.output,
22331
22395
  exitCode: result.exitCode,
22396
+ signal: result.signal,
22332
22397
  cancelled: result.cancelled,
22333
22398
  truncated: result.truncated,
22334
22399
  meta,
@@ -1,3 +1,4 @@
1
+ import { cleanReason } from "@gajae-code/ai/auth-broker/redact";
1
2
  import type { FallbackTriggerClass } from "@gajae-code/ai/utils/fallback-transport";
2
3
 
3
4
  /** Immutable configured fallback intent. Transient attempt state never belongs here. */
@@ -265,6 +266,43 @@ export function compactionRetryDelay(
265
266
  return Math.max(0, bounded);
266
267
  }
267
268
 
269
+ export interface CompactionCandidateFailure {
270
+ readonly model: string;
271
+ readonly message: string;
272
+ }
273
+
274
+ function sanitizeCompactionCandidateFailureMessage(message: string): string {
275
+ const cleaned = cleanReason(message) ?? "Compaction candidate failed.";
276
+ return cleaned.replace(/\b[a-z][a-z0-9+.-]*:\/\/[^\s<>'"]+/gi, "[redacted URL]");
277
+ }
278
+
279
+ /**
280
+ * Error for an auto-compaction run whose every candidate failed.
281
+ *
282
+ * The candidate chain starts at the session model and ends on a same-provider
283
+ * largest-context fallback, so surfacing only the final error named a model
284
+ * the user never chose and hid that their own model had already failed the
285
+ * same way. The message leads with the first failure (the one the user can act
286
+ * on), then lists every candidate that was tried with its own error. `cause`
287
+ * is the final error so callers that classify by type keep the same object.
288
+ */
289
+ export function describeCompactionCandidateFailures(
290
+ failures: readonly CompactionCandidateFailure[],
291
+ lastError: unknown,
292
+ ): Error {
293
+ if (failures.length === 0)
294
+ return new Error("Compaction failed: no candidate failure details available.", { cause: lastError });
295
+ const first = failures[0]!;
296
+ const firstMessage = sanitizeCompactionCandidateFailureMessage(first.message);
297
+ const lines = failures.map(
298
+ failure => ` ${failure.model}: ${sanitizeCompactionCandidateFailureMessage(failure.message)}`,
299
+ );
300
+ return new Error(
301
+ `${firstMessage} (${first.model}); ${failures.length} compaction candidates failed:\n${lines.join("\n")}`,
302
+ { cause: lastError },
303
+ );
304
+ }
305
+
268
306
  /** Retry-After is intentionally uncapped. */
269
307
  export function effectiveFallbackDelay(
270
308
  baseDelayMs: number,
@@ -52,6 +52,20 @@ const LOCK_LEASE_MS = 60_000;
52
52
  const LOCK_HEARTBEAT_MS = 10_000;
53
53
  const LOCK_WAIT_MS = 5_000;
54
54
 
55
+ /** A pre-mutation append fence rejection: no bytes from this append were written. */
56
+ export class ManagedAppendIdentityMismatchError extends Error {
57
+ readonly code = "managed_append_identity_mismatch";
58
+ readonly relativePath: string;
59
+ readonly operatorMessage =
60
+ "Session was resumed by another process; this resume did not run. Retry, or resume a different session.";
61
+
62
+ constructor(relativePath: string) {
63
+ super("managed_append_identity_mismatch");
64
+ this.name = "ManagedAppendIdentityMismatchError";
65
+ this.relativePath = relativePath;
66
+ }
67
+ }
68
+
55
69
  export class ManagedPublishError extends Error {
56
70
  readonly classification:
57
71
  | "destination_conflict"
@@ -1806,7 +1820,7 @@ export class ManagedSessionDescendantStore {
1806
1820
  current.size.toString() === expected.size &&
1807
1821
  current.sha256 === expected.sha256;
1808
1822
  if (!identityMatches) {
1809
- throw new Error("managed_append_identity_mismatch");
1823
+ throw new ManagedAppendIdentityMismatchError(relativePath);
1810
1824
  }
1811
1825
  // mtime/ctime drift on the same file object is harmless. The append
1812
1826
  // helper performs its own descriptor-bound mutation check.
@@ -1820,7 +1834,7 @@ export class ManagedSessionDescendantStore {
1820
1834
  return managedAppendReceiptFromIdentity(appended);
1821
1835
  }
1822
1836
  const current = this.captureBoundedAppendExpectation(relativePath);
1823
- if (!current) throw new Error("managed_append_identity_mismatch");
1837
+ if (!current) throw new ManagedAppendIdentityMismatchError(relativePath);
1824
1838
  if (
1825
1839
  current.dev !== expected.dev ||
1826
1840
  current.ino !== expected.ino ||
@@ -1828,7 +1842,7 @@ export class ManagedSessionDescendantStore {
1828
1842
  current.size !== expected.size ||
1829
1843
  current.sha256 !== expected.sha256
1830
1844
  )
1831
- throw new Error("managed_append_identity_mismatch");
1845
+ throw new ManagedAppendIdentityMismatchError(relativePath);
1832
1846
  const descriptorMatches =
1833
1847
  current.dev === expected.dev &&
1834
1848
  current.ino === expected.ino &&
@@ -1862,7 +1876,7 @@ export class ManagedSessionDescendantStore {
1862
1876
  expected: ManagedFileIdentity,
1863
1877
  ): ManagedAppendReceipt {
1864
1878
  const bounded = this.captureBoundedAppendExpectation(relativePath);
1865
- if (!bounded) throw new Error("managed_append_identity_mismatch");
1879
+ if (!bounded) throw new ManagedAppendIdentityMismatchError(relativePath);
1866
1880
  const descriptorMatches =
1867
1881
  bounded.dev === expected.dev.toString() &&
1868
1882
  bounded.ino === expected.ino.toString() &&
@@ -1882,7 +1896,7 @@ export class ManagedSessionDescendantStore {
1882
1896
  bounded.size !== String(expected.size) ||
1883
1897
  bounded.sha256 !== expected.sha256
1884
1898
  )
1885
- throw new Error("managed_append_identity_mismatch");
1899
+ throw new ManagedAppendIdentityMismatchError(relativePath);
1886
1900
  }
1887
1901
  return this.appendExpectedSync(relativePath, bytes, bounded);
1888
1902
  }
@@ -212,6 +212,7 @@ export interface BashExecutionMessage {
212
212
  command: string;
213
213
  output: string;
214
214
  exitCode: number | undefined;
215
+ signal?: string;
215
216
  cancelled: boolean;
216
217
  truncated: boolean;
217
218
  meta?: OutputMeta;
@@ -313,6 +314,10 @@ export function bashExecutionToText(msg: BashExecutionMessage): string {
313
314
  }
314
315
  if (msg.cancelled) {
315
316
  text += "\n\n(command cancelled)";
317
+ } else if (msg.signal) {
318
+ text += `\n\nCommand terminated by ${msg.signal}${
319
+ msg.exitCode !== null && msg.exitCode !== undefined ? ` (exit code ${msg.exitCode})` : ""
320
+ }`;
316
321
  } else if (msg.exitCode !== null && msg.exitCode !== undefined && msg.exitCode !== 0) {
317
322
  text += `\n\nCommand exited with code ${msg.exitCode}`;
318
323
  }
@@ -112,6 +112,7 @@ import {
112
112
  captureManagedFileNoFollow,
113
113
  fsyncManagedArtifactTree,
114
114
  MANAGED_ARTIFACT_MAX_FILE_BYTES,
115
+ ManagedAppendIdentityMismatchError,
115
116
  type ManagedAppendReceipt,
116
117
  type ManagedBoundedAppendExpectation,
117
118
  ManagedCommittedMutationError,
@@ -11873,6 +11874,10 @@ export class SessionManager {
11873
11874
  return false;
11874
11875
  }
11875
11876
 
11877
+ /** Ownership loss remains observable even when an agent event listener aborts the turn. */
11878
+ getAppendIdentityMismatch(): ManagedAppendIdentityMismatchError | undefined {
11879
+ return this.#persistError instanceof ManagedAppendIdentityMismatchError ? this.#persistError : undefined;
11880
+ }
11876
11881
  #recordPersistError(err: unknown): Error {
11877
11882
  const normalized = toError(err);
11878
11883
  if (!this.#persistError) this.#persistError = normalized;
@@ -16255,7 +16260,11 @@ export class SessionManager {
16255
16260
  }
16256
16261
  }
16257
16262
  const outcome = await this.closeStrict();
16258
- return priorFlushError && outcome.kind === "closed" ? { kind: "close_unknown", error: priorFlushError } : outcome;
16263
+ return priorFlushError &&
16264
+ !(priorFlushError instanceof ManagedAppendIdentityMismatchError) &&
16265
+ outcome.kind === "closed"
16266
+ ? { kind: "close_unknown", error: priorFlushError }
16267
+ : outcome;
16259
16268
  }
16260
16269
 
16261
16270
  /**
@@ -16280,6 +16289,21 @@ export class SessionManager {
16280
16289
  });
16281
16290
  }
16282
16291
  if (preparedCleanupError) return { kind: "close_failed_retryable", error: preparedCleanupError };
16292
+ await this.#persistChain;
16293
+ // Managed appends are synchronous and reject identity drift before writing.
16294
+ // Disown the stale transcript, not an uncertain OS writer: never rewrite a
16295
+ // successor owned by another process while disposing this rejected resume.
16296
+ if (
16297
+ this.destination.kind === "managed" &&
16298
+ !this.#persistWriter &&
16299
+ this.#persistError instanceof ManagedAppendIdentityMismatchError
16300
+ ) {
16301
+ this.#needsFullRewriteOnNextPersist = false;
16302
+ this.#strictResumeMutationPending = false;
16303
+ this.#managedPersistExpectedIdentity = undefined;
16304
+ this.#persistError = undefined;
16305
+ this.#persistErrorReported = false;
16306
+ }
16283
16307
  let priorPersistError = this.#persistError;
16284
16308
  if (this.#needsFullRewriteOnNextPersist && (!this.#readOnlyResume || this.#strictResumeMutationPending)) {
16285
16309
  await this.#persistChain.catch(() => {});
@@ -17379,10 +17403,7 @@ export class SessionManager {
17379
17403
  receipt = store.appendExpectedIdentitySync(relativePath, bytes, this.#managedPersistExpectedIdentity);
17380
17404
  } catch (err) {
17381
17405
  const predecessorMissing = store.descriptorExpected(relativePath) === null;
17382
- if (
17383
- !isEnoent(err) &&
17384
- (!(err instanceof Error) || err.message !== "managed_append_identity_mismatch" || !predecessorMissing)
17385
- )
17406
+ if (!isEnoent(err) && (!(err instanceof ManagedAppendIdentityMismatchError) || !predecessorMissing))
17386
17407
  throw err;
17387
17408
  // Appending only the new records would create a truncated transcript.
17388
17409
  // Recreate the missing file from the complete resident entry set instead.
@@ -17724,6 +17745,9 @@ export class SessionManager {
17724
17745
  if (entry.type === "label") this.#labelRevision++;
17725
17746
  if (sidecarAppendCharge > 0) activeRuntime?.accountant.release(sidecarAppendCharge);
17726
17747
  if (sidecarTailCharge > 0) activeRuntime?.tailCache.release(sidecarTailCharge);
17748
+ // The pre-write fence is an expected ownership loss, not an uncertain
17749
+ // append. Preserve its type after rolling back the rejected entry.
17750
+ if (error instanceof ManagedAppendIdentityMismatchError) throw error;
17727
17751
  throw new SessionAppendPersistenceError(
17728
17752
  priorPersistenceError ? "prior_failure" : "current_append",
17729
17753
  residentEntry.id,
@@ -59,7 +59,11 @@ const REDACTION_RULES: readonly RedactionRule[] = [
59
59
  // Long hex secrets (64+ hex chars: webhook secrets, signing keys).
60
60
  { id: "hex-secret", pattern: new RegExp(`\\b${HEX}{64,}\\b`, "g") },
61
61
  // Basic-auth credentials embedded in URLs (https://user:pass@host).
62
- { id: "url-credential", pattern: /([a-z][a-z0-9+.-]*:\/\/)[^/\s:@]{1,256}:[^/\s@]{1,256}@/gi },
62
+ // The scheme repetition is bounded. Unbounded `[a-z0-9+.-]*` in front of the
63
+ // literal `://` re-tries every prefix of a long alphabetic run before failing,
64
+ // which is quadratic in the input length: 200 KB of ordinary prose costs ~10s.
65
+ // IANA's longest registered scheme is well under 16 characters.
66
+ { id: "url-credential", pattern: /([a-z][a-z0-9+.-]{0,15}:\/\/)[^/\s:@]{1,256}:[^/\s@]{1,256}@/gi },
63
67
  // Sensitive env/KEY assignments: OPENAI_API_KEY=..., token: ..., password = ...
64
68
  // The name prefix is optional so a bare sensitive name (`password: …`) also matches.
65
69
  {