@gajae-code/coding-agent 0.11.9 → 0.11.11

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 (150) hide show
  1. package/CHANGELOG.md +56 -1
  2. package/dist/types/cli.d.ts +8 -1
  3. package/dist/types/config/model-profile-activation.d.ts +1 -0
  4. package/dist/types/config/model-profile-contract.d.ts +2 -0
  5. package/dist/types/config/model-registry.d.ts +2 -0
  6. package/dist/types/config/settings-schema.d.ts +57 -2
  7. package/dist/types/config/settings.d.ts +1 -0
  8. package/dist/types/extensibility/extensions/types.d.ts +21 -0
  9. package/dist/types/gjc-runtime/linux-proc.d.ts +22 -14
  10. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +60 -0
  11. package/dist/types/gjc-runtime/state-runtime.d.ts +3 -1
  12. package/dist/types/gjc-runtime/state-writer.d.ts +1 -0
  13. package/dist/types/modes/acp/acp-agent.d.ts +14 -9
  14. package/dist/types/modes/components/queue-pane.d.ts +8 -0
  15. package/dist/types/modes/components/queued-message-selector.d.ts +6 -0
  16. package/dist/types/modes/components/welcome.d.ts +1 -0
  17. package/dist/types/modes/controllers/selector-controller.d.ts +16 -8
  18. package/dist/types/modes/interactive-mode.d.ts +5 -1
  19. package/dist/types/runtime/memory-domain.d.ts +9 -0
  20. package/dist/types/runtime/memory-guard-contract.d.ts +71 -0
  21. package/dist/types/runtime/memory-guard.d.ts +39 -0
  22. package/dist/types/runtime/memory-limit.d.ts +11 -0
  23. package/dist/types/runtime-mcp/manager.d.ts +8 -0
  24. package/dist/types/sdk/acp/final-text.d.ts +19 -0
  25. package/dist/types/sdk/acp/mcp.d.ts +23 -0
  26. package/dist/types/sdk/broker/lifecycle.d.ts +2 -0
  27. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
  28. package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
  29. package/dist/types/sdk/bus/index.d.ts +11 -2
  30. package/dist/types/sdk/bus/operator-runtime.d.ts +1 -0
  31. package/dist/types/sdk/bus/rate-limit-pool.d.ts +2 -0
  32. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +14 -7
  33. package/dist/types/sdk/bus/telegram-daemon.d.ts +97 -8
  34. package/dist/types/sdk/host/control/operations.d.ts +1 -0
  35. package/dist/types/sdk/lifecycle-session.d.ts +10 -1
  36. package/dist/types/sdk/session.d.ts +7 -0
  37. package/dist/types/sdk/startup-capability.d.ts +6 -0
  38. package/dist/types/session/agent-session.d.ts +11 -11
  39. package/dist/types/session/fallback-chain-controller.d.ts +18 -2
  40. package/dist/types/session/internal/managed-session-scope.d.ts +28 -1
  41. package/dist/types/session/session-manager.d.ts +34 -0
  42. package/dist/types/task/executor.d.ts +6 -0
  43. package/dist/types/task/provider-retry-status.d.ts +13 -0
  44. package/dist/types/task/types.d.ts +39 -0
  45. package/dist/types/task/ultragoal-redteam-activation.d.ts +33 -0
  46. package/dist/types/tools/index.d.ts +3 -1
  47. package/dist/types/tools/resource-gc.d.ts +19 -0
  48. package/package.json +8 -8
  49. package/scripts/generate-sdk-operation-inventory.ts +3 -0
  50. package/scripts/run-test-manifest.ts +6 -4
  51. package/src/cli/args.ts +14 -8
  52. package/src/cli/fast-help.ts +1 -1
  53. package/src/cli/notify-cli.ts +1 -0
  54. package/src/cli.ts +48 -2
  55. package/src/commands/sdk.ts +91 -7
  56. package/src/config/model-profile-activation.ts +1 -0
  57. package/src/config/model-profile-contract.ts +13 -0
  58. package/src/config/model-profiles.ts +31 -7
  59. package/src/config/model-registry.ts +4 -2
  60. package/src/config/settings-schema.ts +61 -2
  61. package/src/config/settings.ts +8 -0
  62. package/src/defaults/gjc/skills/ralplan/SKILL.md +21 -2
  63. package/src/defaults/gjc/skills/ultragoal/SKILL.md +1 -1
  64. package/src/eval/js/tool-bridge.ts +1 -1
  65. package/src/extensibility/extensions/runner.ts +4 -0
  66. package/src/extensibility/extensions/types.ts +14 -0
  67. package/src/gjc-runtime/deep-interview-ambiguity.ts +16 -2
  68. package/src/gjc-runtime/deep-interview-draft.ts +2 -1
  69. package/src/gjc-runtime/deep-interview-recorder.ts +16 -1
  70. package/src/gjc-runtime/deep-interview-state.ts +25 -3
  71. package/src/gjc-runtime/linux-proc.ts +70 -36
  72. package/src/gjc-runtime/ralplan-runtime.ts +252 -1
  73. package/src/gjc-runtime/state-runtime.ts +48 -11
  74. package/src/gjc-runtime/state-writer.ts +1 -1
  75. package/src/hooks/skill-state.ts +23 -1
  76. package/src/internal-urls/docs-index.generated.ts +7 -7
  77. package/src/internal-urls/local-protocol.ts +23 -4
  78. package/src/modes/acp/acp-agent.ts +554 -60
  79. package/src/modes/acp/acp-event-mapper.ts +93 -6
  80. package/src/modes/components/model-selector.ts +15 -1
  81. package/src/modes/components/notifications-settings-editor.ts +1 -1
  82. package/src/modes/components/queue-pane.ts +55 -9
  83. package/src/modes/components/queued-message-selector.ts +53 -10
  84. package/src/modes/components/tool-status-header.ts +1 -4
  85. package/src/modes/components/welcome.ts +18 -3
  86. package/src/modes/controllers/command-controller.ts +4 -0
  87. package/src/modes/controllers/extension-ui-controller.ts +4 -10
  88. package/src/modes/controllers/input-controller.ts +44 -3
  89. package/src/modes/controllers/selector-controller.ts +38 -8
  90. package/src/modes/interactive-mode.ts +65 -26
  91. package/src/modes/runtime-init.ts +1 -0
  92. package/src/prompts/agent-fragments/restricted-bash.md +1 -1
  93. package/src/prompts/agents/architect.md +8 -1
  94. package/src/prompts/agents/critic.md +8 -1
  95. package/src/prompts/agents/executor.md +1 -1
  96. package/src/prompts/agents/planner.md +8 -1
  97. package/src/runtime/memory-domain.ts +66 -0
  98. package/src/runtime/memory-guard-contract.ts +73 -0
  99. package/src/runtime/memory-guard.ts +292 -0
  100. package/src/runtime/memory-limit.ts +50 -0
  101. package/src/runtime-mcp/manager.ts +26 -6
  102. package/src/sdk/acp/adapter.ts +8 -1
  103. package/src/sdk/acp/final-text.ts +40 -0
  104. package/src/sdk/acp/mcp.ts +26 -0
  105. package/src/sdk/broker/identity.ts +22 -12
  106. package/src/sdk/broker/lifecycle.ts +82 -1
  107. package/src/sdk/bus/chat-daemon-cli.ts +2 -2
  108. package/src/sdk/bus/chat-daemon-control.ts +4 -3
  109. package/src/sdk/bus/config.ts +1 -1
  110. package/src/sdk/bus/daemon-paths.ts +2 -0
  111. package/src/sdk/bus/index.ts +222 -27
  112. package/src/sdk/bus/notification-service.ts +19 -5
  113. package/src/sdk/bus/operator-runtime.ts +31 -5
  114. package/src/sdk/bus/rate-limit-pool.ts +8 -0
  115. package/src/sdk/bus/telegram-cli.ts +6 -13
  116. package/src/sdk/bus/telegram-daemon-cli.ts +8 -3
  117. package/src/sdk/bus/telegram-daemon-contract.ts +15 -7
  118. package/src/sdk/bus/telegram-daemon-control.ts +14 -5
  119. package/src/sdk/bus/telegram-daemon.ts +1191 -164
  120. package/src/sdk/host/control/dispatch.ts +2 -0
  121. package/src/sdk/host/control/operations.ts +1 -0
  122. package/src/sdk/host/host.ts +2 -2
  123. package/src/sdk/lifecycle-session.ts +19 -3
  124. package/src/sdk/protocol/operation-inventory.generated.json +58 -0
  125. package/src/sdk/protocol/operation-registry.ts +1 -0
  126. package/src/sdk/session.ts +24 -1
  127. package/src/sdk/startup-capability.ts +6 -0
  128. package/src/session/agent-session.ts +290 -132
  129. package/src/session/fallback-chain-controller.ts +47 -8
  130. package/src/session/internal/managed-session-scope.ts +173 -127
  131. package/src/session/internal/native-publish-outcome.ts +8 -3
  132. package/src/session/session-manager.ts +608 -25
  133. package/src/skill-state/workflow-mutation-guard.ts +41 -7
  134. package/src/task/executor.ts +152 -42
  135. package/src/task/index.ts +2 -0
  136. package/src/task/provider-retry-status.ts +88 -0
  137. package/src/task/render.ts +45 -26
  138. package/src/task/types.ts +25 -11
  139. package/src/task/ultragoal-redteam-activation.ts +75 -0
  140. package/src/tools/bash-allowed-prefixes.ts +45 -1
  141. package/src/tools/index.ts +3 -1
  142. package/src/tools/resource-gc.ts +496 -120
  143. package/src/tools/subagent-render.ts +74 -16
  144. package/src/tools/subagent.ts +2 -0
  145. package/src/workflow/workflow-intent-diff.ts +4 -1
  146. package/vendor/insane-search/MANIFEST.json +4 -1
  147. package/vendor/insane-search/engine/bias_check.py +5 -0
  148. package/vendor/insane-search/engine/fetch_chain.py +67 -1
  149. package/vendor/insane-search/engine/transport.py +13 -2
  150. package/vendor/insane-search/engine/validators.py +4 -1
@@ -35,17 +35,28 @@ function planningPhaseBlockMessage(skill: CanonicalGjcWorkflowSkill): string {
35
35
  }
36
36
 
37
37
  const BLOCKED_TOOL_NAMES = new Set(["edit", "write", "ast_edit", "bash"]);
38
+ /**
39
+ * Only `/dev/null` is exempt. `/dev/stdout`, `/dev/stderr`, and `/dev/fd/<n>` are descriptor
40
+ * aliases: `exec 1<>src/product.ts; printf x >/dev/stdout` reaches a real repository file
41
+ * through a rebound descriptor, so they must stay blocked.
42
+ */
43
+ const DEVICE_SINK_PATHS = new Set(["/dev/null"]);
44
+ /** Bash write forms the plain `>`/`>>` scanner misses: clobber, dup-to-path, and read-write open. */
45
+ const BASH_EXTENDED_WRITE_RE = /(?:>\||<>|>&(?![\s;&|]*(?:\d+|-)(?:[\s;&|]|$)))\s*([^\s;&|]+)/g;
46
+ /** Descriptor rebinding is not statically resolvable; any `exec` redirection forces a fail-closed verdict. */
47
+ const BASH_EXEC_REDIRECT_RE = /(?:^|[;&|\n])\s*(?:sudo\s+)?exec\b[^;&|\n]*[<>]/i;
48
+
38
49
  const ARCHIVE_OR_SQLITE_BASE_RE = /^(.+?\.(?:tar\.gz|sqlite3|sqlite|db3|zip|tgz|tar|db))(?:$|:)/i;
39
50
  const INTERNAL_SCHEME_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
40
51
  const VIM_FILE_SWITCH_RE = /^\s*:(?:e|e!|edit|edit!)(?:\s+([^<\r\n]+))?(?:<CR>|\r|\n|$)/i;
41
52
  const BASH_MUTATION_COMMAND_RE =
42
- /(?:^|[;&|\n])\s*(?:\w+=[^\s]+\s+)*(?:sudo\s+)?(?:tee|touch|rm|mkdir|cp|mv|install|truncate)\b([^;&|\n]*)|(?:^|[^<>])(?:>>?|\d>>?)\s*([^\s;&|]+)/gi;
53
+ /(?:^|[;&|\n])\s*(?:\w+=[^\s]+\s+)*(?:sudo\s+)?(?:[^\s;&|]*\/)?(?:tee|touch|rm|mkdir|cp|mv|install|truncate)\b([^;&|\n]*)|(?:^|[^<>])(?:>>?|\d>>?)\s*([^\s;&|]+)/gi;
43
54
  const BASH_IN_PLACE_MUTATION_COMMAND_RE = /(?:^|[;&|\n])\s*(?:\w+=[^\s]+\s+)*(?:sudo\s+)?(?:sed|perl)\b([^;&|\n]*)/gi;
44
55
  const BASH_OPAQUE_INTERPRETER_WRITE_RE =
45
56
  /(?:^|[;&|\n])\s*(?:\w+=[^\s]+\s+)*(?:sudo\s+)?(?:python3?|node|ruby)\b[^;&|\n]*(?:-c|-e)\b[^;&|\n]*(?:open\s*\(|writeFile(?:Sync)?\s*\(|\.write\s*\()/i;
46
57
  const BASH_HEREDOC_OPAQUE_INTERPRETER_WRITE_RE =
47
58
  /(?:^|[;&|\n])\s*(?:\w+=[^\s]+\s+)*(?:sudo\s+)?(?:python3?|node|ruby)\b[^;&|\n]*(?:<<[-]?\s*['"]?\w+['"]?)[\s\S]*(?:open\s*\(|writeFile(?:Sync)?\s*\(|\.write\s*\()/i;
48
- const BASH_DD_OUTPUT_RE = /(?:^|[;&|\n])\s*(?:\w+=[^\s]+\s+)*(?:sudo\s+)?dd\b([^;&|\n]*)/gi;
59
+ const BASH_DD_OUTPUT_RE = /(?:^|[;&|\n])\s*(?:\w+=[^\s]+\s+)*(?:sudo\s+)?(?:[^\s;&|]*\/)?dd\b([^;&|\n]*)/gi;
49
60
 
50
61
  type ToolWithEditMode = AgentTool & {
51
62
  mode?: unknown;
@@ -371,6 +382,10 @@ function cleanShellWord(value: string): string {
371
382
  return value.replace(/^['"]|['"]$/g, "");
372
383
  }
373
384
 
385
+ function isDeviceSinkPath(value: string): boolean {
386
+ return DEVICE_SINK_PATHS.has(cleanShellWord(value));
387
+ }
388
+
374
389
  function extractBashTargets(args: unknown): ExtractedTargets {
375
390
  const record = getRecord(args);
376
391
  const command = safeString(record?.command);
@@ -382,11 +397,25 @@ function extractBashTargets(args: unknown): ExtractedTargets {
382
397
  if (BASH_OPAQUE_INTERPRETER_WRITE_RE.test(command) || BASH_HEREDOC_OPAQUE_INTERPRETER_WRITE_RE.test(command)) {
383
398
  targets.unknown = true;
384
399
  }
400
+ // `exec 1<>file` rebinds a descriptor, so a later `>/dev/null` may not reach the device at all.
401
+ if (BASH_EXEC_REDIRECT_RE.test(command)) {
402
+ targets.unknown = true;
403
+ }
404
+ for (const match of command.matchAll(BASH_EXTENDED_WRITE_RE)) {
405
+ const cleaned = cleanShellWord(match[1] ?? "");
406
+ if (!cleaned) targets.unknown = true;
407
+ else if (!isDeviceSinkPath(cleaned)) addPath(targets, cleaned);
408
+ }
385
409
  for (const match of command.matchAll(BASH_DD_OUTPUT_RE)) {
386
410
  const parts = shellWords(match[1] ?? "").map(cleanShellWord);
387
- const output = parts.find(part => part.startsWith("of="));
388
- if (output) addPath(targets, output.slice(3));
389
- else targets.unknown = true;
411
+ // Every `of=` counts: GNU dd honors the last one, so `of=/dev/null of=real.ts` writes real.ts.
412
+ const outputs = parts.filter(part => part.startsWith("of="));
413
+ if (outputs.length === 0) targets.unknown = true;
414
+ for (const output of outputs) {
415
+ const outputPath = cleanShellWord(output.slice(3));
416
+ if (!outputPath) targets.unknown = true;
417
+ else if (!isDeviceSinkPath(outputPath)) addPath(targets, outputPath);
418
+ }
390
419
  }
391
420
  for (const match of command.matchAll(BASH_IN_PLACE_MUTATION_COMMAND_RE)) {
392
421
  const parts = shellWords(match[1] ?? "").map(cleanShellWord);
@@ -399,12 +428,17 @@ function extractBashTargets(args: unknown): ExtractedTargets {
399
428
  for (const match of command.matchAll(BASH_MUTATION_COMMAND_RE)) {
400
429
  const redirected = match[2]?.trim();
401
430
  if (redirected) {
402
- addPath(targets, cleanShellWord(redirected));
431
+ const cleaned = cleanShellWord(redirected);
432
+ // A capture that dequotes to nothing (e.g. `>" src.ts"`) is a truncated parse, not a safe no-op.
433
+ if (!cleaned) targets.unknown = true;
434
+ else if (!isDeviceSinkPath(cleaned)) addPath(targets, cleaned);
403
435
  continue;
404
436
  }
405
437
  const parts = shellWords(match[1] ?? "");
406
438
  const commandName = match[0]
407
- ?.match(/(?:^|[;&|\n])\s*(?:\w+=[^\s]+\s+)*(?:sudo\s+)?(tee|touch|rm|mkdir|cp|mv|install|truncate)\b/i)?.[1]
439
+ ?.match(
440
+ /(?:^|[;&|\n])\s*(?:\w+=[^\s]+\s+)*(?:sudo\s+)?(?:[^\s;&|]*\/)?(tee|touch|rm|mkdir|cp|mv|install|truncate)\b/i,
441
+ )?.[1]
408
442
  ?.toLowerCase();
409
443
  const targetParts =
410
444
  commandName === "cp" || commandName === "mv" || commandName === "install" || commandName === "truncate"
@@ -15,7 +15,7 @@ import type {
15
15
  } from "@gajae-code/agent-core";
16
16
  import { recordHandoff, resolveTelemetry } from "@gajae-code/agent-core";
17
17
  import { estimateMessageTokensHeuristic } from "@gajae-code/agent-core/compaction";
18
- import type { Message, Model, ServiceTier } from "@gajae-code/ai";
18
+ import type { AssistantMessage, Message, Model, ServiceTier } from "@gajae-code/ai";
19
19
  import { type JsonSchemaValidationIssue, validateJsonSchemaValue } from "@gajae-code/ai/utils/schema";
20
20
  import { logger, prompt, untilAborted } from "@gajae-code/utils";
21
21
  import { AsyncJobManager } from "../async";
@@ -50,6 +50,7 @@ import type { EventBus } from "../utils/event-bus";
50
50
  import { buildNamedToolChoiceResult } from "../utils/tool-choice";
51
51
  import type { WorkspaceTree } from "../workspace-tree";
52
52
  import { validateAllocatedTaskId } from "./id";
53
+ import { classifyProviderRetry, providerNameFromModel } from "./provider-retry-status";
53
54
  import { subprocessToolRegistry } from "./subprocess-tool-registry";
54
55
  import { persistTaskTokenLog, taskTokenLogFromUsage } from "./token-log";
55
56
  import {
@@ -66,6 +67,7 @@ import {
66
67
  TASK_SUBAGENT_PROGRESS_CHANNEL,
67
68
  type TaskToolDetails,
68
69
  } from "./types";
70
+ import { type ExecutorExecutionMode, resolveUltragoalRedTeamActivation } from "./ultragoal-redteam-activation";
69
71
 
70
72
  /** Agent event types to forward for progress tracking. */
71
73
  const agentEventTypes = new Set<AgentEvent["type"]>([
@@ -81,6 +83,21 @@ const agentEventTypes = new Set<AgentEvent["type"]>([
81
83
  "tool_execution_end",
82
84
  ]);
83
85
 
86
+ const providerStreamingUpdateTypes = new Set<string>([
87
+ "text_start",
88
+ "text_delta",
89
+ "text_end",
90
+ "thinking_start",
91
+ "thinking_delta",
92
+ "thinking_end",
93
+ "reasoning_summary_start",
94
+ "reasoning_summary_delta",
95
+ "reasoning_summary_end",
96
+ "toolcall_start",
97
+ "toolcall_delta",
98
+ "toolcall_end",
99
+ ]);
100
+
84
101
  const isAgentEvent = (event: AgentSessionEvent): event is AgentEvent =>
85
102
  agentEventTypes.has(event.type as AgentEvent["type"]);
86
103
 
@@ -174,6 +191,11 @@ export interface ExecutorOptions {
174
191
  agent: AgentDefinition;
175
192
  task: string;
176
193
  assignment?: string;
194
+ /**
195
+ * Typed executor execution mode. When set, overrides assignment-text heuristics
196
+ * for ultragoal red-team prompt injection (#2698 / #2456).
197
+ */
198
+ executionMode?: ExecutorExecutionMode;
177
199
  context?: string;
178
200
  description?: string;
179
201
  index: number;
@@ -845,7 +867,13 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
845
867
  let modelSubstitutionWarning: ModelSubstitutionWarning | undefined;
846
868
  let resolvedModelString: string | undefined;
847
869
  let lastAssistantModelString: string | undefined;
870
+ let activeProviderModelString: string | undefined;
848
871
  let effectiveThinkingLevelForWarning: ThinkingLevel | undefined;
872
+ let lastProviderProgressAtMs: number | undefined;
873
+ let sessionEventOrdinal = 0;
874
+ let retryStartOrdinal = 0;
875
+ const seenAssistantMessages = new WeakSet<AgentMessage>();
876
+ const seenAssistantMessageIdentities = new Set<string>();
849
877
 
850
878
  // Accumulate usage incrementally from message_end events (no memory for streaming events)
851
879
  const accumulatedUsage = {
@@ -927,7 +955,8 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
927
955
 
928
956
  const emitProgressNow = () => {
929
957
  progress.durationMs = Date.now() - startTime;
930
- onProgress?.({ ...progress });
958
+ const progressSnapshot = structuredClone(progress);
959
+ onProgress?.(progressSnapshot);
931
960
  if (options.eventBus) {
932
961
  options.eventBus.emit(TASK_SUBAGENT_PROGRESS_CHANNEL, {
933
962
  index,
@@ -935,7 +964,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
935
964
  agentSource: agent.source,
936
965
  task,
937
966
  assignment,
938
- progress: { ...progress },
967
+ progress: progressSnapshot,
939
968
  sessionFile: null,
940
969
  });
941
970
  }
@@ -990,6 +1019,79 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
990
1019
  : undefined;
991
1020
  };
992
1021
 
1022
+ const getAssistantMessageIdentity = (message: AgentMessage): string | undefined => {
1023
+ if (message.role !== "assistant") return undefined;
1024
+ const model = getMessageModelString(message);
1025
+ const timestamp = "timestamp" in message ? message.timestamp : undefined;
1026
+ if (!model || typeof timestamp !== "number") return undefined;
1027
+ const responseId = "responseId" in message && typeof message.responseId === "string" ? message.responseId : "";
1028
+ return JSON.stringify([model, timestamp, responseId]);
1029
+ };
1030
+
1031
+ const rememberAssistantMessage = (message: AgentMessage): void => {
1032
+ seenAssistantMessages.add(message);
1033
+ const identity = getAssistantMessageIdentity(message);
1034
+ if (identity) seenAssistantMessageIdentities.add(identity);
1035
+ };
1036
+
1037
+ const isProviderAssistantMessage = (message: AgentMessage): message is AssistantMessage =>
1038
+ message.role === "assistant" &&
1039
+ "stopReason" in message &&
1040
+ Array.isArray(message.content) &&
1041
+ getMessageModelString(message) !== undefined;
1042
+
1043
+ const isSuccessfulProviderAssistantMessage = (message: AgentMessage): message is AssistantMessage =>
1044
+ isProviderAssistantMessage(message) &&
1045
+ message.stopReason !== "error" &&
1046
+ message.stopReason !== "aborted" &&
1047
+ message.errorMessage === undefined &&
1048
+ message.errorKind === undefined &&
1049
+ message.transportFailure === undefined;
1050
+
1051
+ const hasMatchingActiveProviderModel = (message: AgentMessage): message is AssistantMessage =>
1052
+ isProviderAssistantMessage(message) &&
1053
+ activeProviderModelString !== undefined &&
1054
+ getMessageModelString(message) === activeProviderModelString;
1055
+
1056
+ const isProviderStreamingUpdate = (event: Extract<AgentEvent, { type: "message_update" }>): boolean => {
1057
+ const update = event.assistantMessageEvent;
1058
+ if (!update || typeof update !== "object" || !providerStreamingUpdateTypes.has(update.type)) return false;
1059
+ if (!("partial" in update) || getMessageModelString(update.partial) !== activeProviderModelString) return false;
1060
+ switch (update.type) {
1061
+ case "text_delta":
1062
+ case "thinking_delta":
1063
+ case "reasoning_summary_delta":
1064
+ case "toolcall_delta":
1065
+ return typeof update.delta === "string";
1066
+ default:
1067
+ return typeof update.contentIndex === "number";
1068
+ }
1069
+ };
1070
+
1071
+ const getProviderTextDelta = (event: Extract<AgentEvent, { type: "message_update" }>): string | undefined => {
1072
+ const update = (event as { assistantMessageEvent?: unknown }).assistantMessageEvent;
1073
+ if (!update || typeof update !== "object") return undefined;
1074
+ const record = update as { type?: unknown; delta?: unknown };
1075
+ return record.type === "text_delta" && typeof record.delta === "string" ? record.delta : undefined;
1076
+ };
1077
+
1078
+ const isProviderBackedRecoveryEvent = (
1079
+ event: Extract<AgentEvent, { type: "message_start" | "message_update" }>,
1080
+ eventOrdinal: number,
1081
+ ): boolean => {
1082
+ const messageIdentity = getAssistantMessageIdentity(event.message);
1083
+ if (
1084
+ !progress.retryState ||
1085
+ eventOrdinal <= retryStartOrdinal ||
1086
+ seenAssistantMessages.has(event.message) ||
1087
+ (messageIdentity !== undefined && seenAssistantMessageIdentities.has(messageIdentity))
1088
+ )
1089
+ return false;
1090
+ if (!hasMatchingActiveProviderModel(event.message)) return false;
1091
+ if (event.type === "message_start") return isSuccessfulProviderAssistantMessage(event.message);
1092
+ return isProviderStreamingUpdate(event);
1093
+ };
1094
+
993
1095
  const updateRecentOutputLines = () => {
994
1096
  const lines = recentOutputTail.split("\n").filter(line => line.trim());
995
1097
  progress.recentOutput = lines.slice(-8).reverse();
@@ -1004,21 +1106,6 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1004
1106
  updateRecentOutputLines();
1005
1107
  };
1006
1108
 
1007
- const replaceRecentOutputFromContent = (content: unknown[]) => {
1008
- recentOutputTail = "";
1009
- for (const block of content) {
1010
- if (!block || typeof block !== "object") continue;
1011
- const record = block as { type?: unknown; text?: unknown };
1012
- if (record.type !== "text" || typeof record.text !== "string") continue;
1013
- if (!record.text) continue;
1014
- recentOutputTail += record.text;
1015
- if (recentOutputTail.length > RECENT_OUTPUT_TAIL_BYTES) {
1016
- recentOutputTail = recentOutputTail.slice(-RECENT_OUTPUT_TAIL_BYTES);
1017
- }
1018
- }
1019
- updateRecentOutputLines();
1020
- };
1021
-
1022
1109
  const resetRecentOutput = () => {
1023
1110
  recentOutputTail = "";
1024
1111
  progress.recentOutput = [];
@@ -1038,20 +1125,32 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1038
1125
  });
1039
1126
  };
1040
1127
 
1041
- const processEvent = (event: AgentEvent) => {
1128
+ const processEvent = (event: AgentEvent, eventOrdinal: number) => {
1042
1129
  if (resolved) return;
1043
1130
 
1044
1131
  forwardSubagentEvent(event);
1045
1132
 
1046
1133
  const now = Date.now();
1047
1134
  let flushProgress = false;
1135
+ // A retry is recovered at the first assistant provider event, before the
1136
+ // session emits auto_retry_end after message completion.
1048
1137
 
1049
1138
  switch (event.type) {
1050
- case "message_start":
1051
- if (event.message?.role === "assistant") {
1139
+ case "message_start": {
1140
+ if (event.message.role !== "assistant") break;
1141
+ const retryWasActive = Boolean(progress.retryState);
1142
+ const recoversRetry = isProviderBackedRecoveryEvent(event, eventOrdinal);
1143
+ rememberAssistantMessage(event.message);
1144
+ if (recoversRetry || !retryWasActive) {
1145
+ lastProviderProgressAtMs = now;
1052
1146
  resetRecentOutput();
1053
1147
  }
1148
+ if (recoversRetry) {
1149
+ progress.retryState = undefined;
1150
+ flushProgress = true;
1151
+ }
1054
1152
  break;
1153
+ }
1055
1154
 
1056
1155
  case "tool_execution_start": {
1057
1156
  progress.toolCount++;
@@ -1163,24 +1262,17 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1163
1262
  }
1164
1263
 
1165
1264
  case "message_update": {
1166
- if (event.message?.role !== "assistant") break;
1167
- const assistantEvent = (
1168
- event as AgentEvent & {
1169
- assistantMessageEvent?: { type?: string; delta?: string };
1170
- }
1171
- ).assistantMessageEvent;
1172
- if (assistantEvent?.type === "text_delta" && typeof assistantEvent.delta === "string") {
1173
- appendRecentOutputTail(assistantEvent.delta);
1174
- break;
1175
- }
1176
- if (assistantEvent && assistantEvent.type !== "text_delta") {
1177
- break;
1178
- }
1179
- const updateContent =
1180
- getMessageContent(event.message) || (event as AgentEvent & { content?: unknown }).content;
1181
- if (updateContent && Array.isArray(updateContent)) {
1182
- replaceRecentOutputFromContent(updateContent);
1265
+ if (event.message.role !== "assistant") break;
1266
+ const retryWasActive = Boolean(progress.retryState);
1267
+ const recoversRetry = isProviderBackedRecoveryEvent(event, eventOrdinal);
1268
+ if (!retryWasActive) rememberAssistantMessage(event.message);
1269
+ if (recoversRetry || !retryWasActive) lastProviderProgressAtMs = now;
1270
+ if (recoversRetry) {
1271
+ progress.retryState = undefined;
1272
+ flushProgress = true;
1183
1273
  }
1274
+ const textDelta = getProviderTextDelta(event);
1275
+ if (textDelta !== undefined) appendRecentOutputTail(textDelta);
1184
1276
  break;
1185
1277
  }
1186
1278
 
@@ -1188,6 +1280,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1188
1280
  // Extract text from assistant and toolResult messages (not user prompts)
1189
1281
  const role = event.message?.role;
1190
1282
  if (role === "assistant") {
1283
+ if (isSuccessfulProviderAssistantMessage(event.message)) lastProviderProgressAtMs = now;
1191
1284
  const messageContent =
1192
1285
  getMessageContent(event.message) || (event as AgentEvent & { content?: unknown }).content;
1193
1286
  if (messageContent && Array.isArray(messageContent)) {
@@ -1200,6 +1293,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1200
1293
  const assistantModel = getMessageModelString(event.message);
1201
1294
  if (assistantModel) {
1202
1295
  lastAssistantModelString = assistantModel;
1296
+ activeProviderModelString = assistantModel;
1203
1297
  if (resolvedModelString && assistantModel !== resolvedModelString && !modelSubstitutionWarning) {
1204
1298
  modelSubstitutionWarning = {
1205
1299
  requested: resolvedModelString,
@@ -1367,6 +1461,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1367
1461
  );
1368
1462
  if (model) {
1369
1463
  resolvedModelString = formatModelString(model);
1464
+ activeProviderModelString = resolvedModelString;
1370
1465
  }
1371
1466
  if (authFallbackUsed && model && requestedModel) {
1372
1467
  modelSubstitutionWarning = {
@@ -1504,6 +1599,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1504
1599
  ? { requestedModel, reason: modelSubstitutionWarning.reason }
1505
1600
  : undefined,
1506
1601
  toolNames,
1602
+ alwaysActiveToolNames: ircEnabled ? ["irc"] : undefined,
1507
1603
  outputSchema,
1508
1604
  requireYieldTool: true,
1509
1605
  contextFiles: options.contextFiles,
@@ -1513,9 +1609,11 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1513
1609
  systemPrompt: defaultPrompt => {
1514
1610
  const subagentPrompt = prompt.render(subagentSystemPromptTemplate, {
1515
1611
  agent: prompt.render(agent.systemPrompt, {
1516
- ultragoalRedTeam: /ultragoal\s+completion\s+(?:qa|red-team)|executorQa/i.test(
1517
- options.assignment ?? task,
1518
- ),
1612
+ // Typed executionMode wins; assignment text is compatibility-only (#2698 / #2456).
1613
+ ultragoalRedTeam: resolveUltragoalRedTeamActivation({
1614
+ executionMode: options.executionMode,
1615
+ assignment: options.assignment ?? task,
1616
+ }),
1519
1617
  }),
1520
1618
  context: options.context?.trim() ?? "",
1521
1619
  worktree: worktree ?? "",
@@ -1737,11 +1835,22 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1737
1835
 
1738
1836
  const MAX_YIELD_RETRIES = 3;
1739
1837
  unsubscribe = session.subscribe(event => {
1838
+ sessionEventOrdinal += 1;
1839
+ const eventOrdinal = sessionEventOrdinal;
1740
1840
  if (event.type === "auto_retry_start") {
1841
+ retryStartOrdinal = eventOrdinal;
1842
+ for (const message of session.messages) {
1843
+ if (message.role === "assistant") rememberAssistantMessage(message);
1844
+ }
1741
1845
  progress.retryState = {
1742
1846
  attempt: event.attempt,
1743
1847
  maxAttempts: event.maxAttempts,
1744
1848
  unbounded: event.unbounded,
1849
+ kind: classifyProviderRetry(event.errorMessage),
1850
+ provider: providerNameFromModel(
1851
+ activeProviderModelString ?? lastAssistantModelString ?? resolvedModelString,
1852
+ ),
1853
+ lastProviderProgressAtMs,
1745
1854
  delayMs: event.delayMs,
1746
1855
  errorMessage: event.errorMessage,
1747
1856
  startedAtMs: Date.now(),
@@ -1763,12 +1872,13 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1763
1872
  return;
1764
1873
  }
1765
1874
  if (event.type === "model_fallback_switched") {
1875
+ activeProviderModelString = event.to;
1766
1876
  forwardSubagentEvent(event);
1767
1877
  return;
1768
1878
  }
1769
1879
  if (isAgentEvent(event)) {
1770
1880
  try {
1771
- processEvent(event);
1881
+ processEvent(event, eventOrdinal);
1772
1882
  } catch (err) {
1773
1883
  logger.error("Subagent event processing failed", {
1774
1884
  error: err instanceof Error ? err.message : String(err),
package/src/task/index.ts CHANGED
@@ -1446,6 +1446,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1446
1446
  agent: effectiveAgent,
1447
1447
  task: renderTaskAssignment(task.assignment, simpleMode),
1448
1448
  assignment: task.assignment.trim(),
1449
+ executionMode: task.executionMode,
1449
1450
  context: sharedContext,
1450
1451
  description: task.description,
1451
1452
  index,
@@ -1518,6 +1519,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1518
1519
  agent: effectiveAgent,
1519
1520
  task: renderTaskAssignment(task.assignment, simpleMode),
1520
1521
  assignment: task.assignment.trim(),
1522
+ executionMode: task.executionMode,
1521
1523
  context: sharedContext,
1522
1524
  description: task.description,
1523
1525
  index,
@@ -0,0 +1,88 @@
1
+ import type { AgentProgress, TaskToolDetails } from "./types";
2
+
3
+ export type ProviderRetryKind = NonNullable<AgentProgress["retryState"]>["kind"];
4
+
5
+ const FIRST_EVENT_TIMEOUT_PATTERN = /stream timed out while waiting for the first event/i;
6
+ const IDLE_STREAM_STALL_PATTERN = /stream stalled while waiting for the next event/i;
7
+
8
+ export function classifyProviderRetry(errorMessage: string): ProviderRetryKind {
9
+ if (FIRST_EVENT_TIMEOUT_PATTERN.test(errorMessage)) return "first_event_timeout";
10
+ if (IDLE_STREAM_STALL_PATTERN.test(errorMessage)) return "idle_stream_stall";
11
+ return "provider_error";
12
+ }
13
+
14
+ export function providerNameFromModel(model: string | undefined): string | undefined {
15
+ const provider = model?.split("/", 1)[0]?.trim();
16
+ return provider || undefined;
17
+ }
18
+
19
+ export function providerRetryPhaseLabel(kind: ProviderRetryKind): string {
20
+ switch (kind) {
21
+ case "first_event_timeout":
22
+ return "first event timeout";
23
+ case "idle_stream_stall":
24
+ return "stream stalled";
25
+ case "provider_error":
26
+ return "provider error";
27
+ }
28
+ }
29
+
30
+ export function providerProgressAgeLabel(retryState: NonNullable<AgentProgress["retryState"]>, nowMs: number): string {
31
+ if (retryState.lastProviderProgressAtMs === undefined) return "no provider events yet";
32
+ const ageSeconds = Math.max(0, Math.floor((nowMs - retryState.lastProviderProgressAtMs) / 1000));
33
+ return `last provider progress ${ageSeconds}s ago`;
34
+ }
35
+
36
+ function isRecord(value: unknown): value is Record<string, unknown> {
37
+ return Boolean(value) && typeof value === "object";
38
+ }
39
+
40
+ function isTaskToolDetails(value: unknown): value is TaskToolDetails {
41
+ return (
42
+ isRecord(value) && Array.isArray(value.results) && (value.progress === undefined || Array.isArray(value.progress))
43
+ );
44
+ }
45
+
46
+ function isAgentProgress(value: unknown): value is AgentProgress {
47
+ return isRecord(value) && typeof value.status === "string";
48
+ }
49
+
50
+ export function hasActiveProviderRetryInTaskDetails(details: TaskToolDetails, seen = new WeakSet<object>()): boolean {
51
+ if (!isTaskToolDetails(details) || seen.has(details)) return false;
52
+ seen.add(details);
53
+ for (const progress of details.progress ?? []) {
54
+ if (isAgentProgress(progress) && hasActiveProviderRetryInProgress(progress, seen)) return true;
55
+ }
56
+ return false;
57
+ }
58
+
59
+ export function hasActiveProviderRetryInProgress(progress: AgentProgress, seen = new WeakSet<object>()): boolean {
60
+ if (!isAgentProgress(progress) || seen.has(progress)) return false;
61
+ seen.add(progress);
62
+ if (progress.status === "running" && progress.retryState) return true;
63
+
64
+ const nestedTaskData = isRecord(progress.extractedToolData) ? progress.extractedToolData.task : undefined;
65
+ if (Array.isArray(nestedTaskData)) {
66
+ for (const nestedDetails of nestedTaskData) {
67
+ if (hasActiveProviderRetryInTaskDetails(nestedDetails as TaskToolDetails, seen)) return true;
68
+ }
69
+ }
70
+ return hasActiveProviderRetryInTaskDetails(progress.inflightTaskDetails as TaskToolDetails, seen);
71
+ }
72
+
73
+ export interface ProviderDegradationGroup {
74
+ provider: string;
75
+ count: number;
76
+ }
77
+
78
+ export function collectProviderDegradationGroups(progress: readonly AgentProgress[]): ProviderDegradationGroup[] {
79
+ const counts = new Map<string, number>();
80
+ for (const item of progress) {
81
+ if (!isAgentProgress(item) || item.status !== "running" || !item.retryState) continue;
82
+ const provider = item.retryState.provider ?? "provider";
83
+ counts.set(provider, (counts.get(provider) ?? 0) + 1);
84
+ }
85
+ return Array.from(counts, ([provider, count]) => ({ provider, count }))
86
+ .filter(group => group.count > 1)
87
+ .sort((a, b) => b.count - a.count || a.provider.localeCompare(b.provider));
88
+ }