@gajae-code/coding-agent 0.11.10 → 0.12.0

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 (249) hide show
  1. package/CHANGELOG.md +86 -2
  2. package/dist/types/cli/read-cli.d.ts +1 -0
  3. package/dist/types/cli.d.ts +8 -1
  4. package/dist/types/commands/deep-interview.d.ts +9 -0
  5. package/dist/types/commands/harness.d.ts +22 -0
  6. package/dist/types/commands/read.d.ts +6 -0
  7. package/dist/types/config/model-profile-activation.d.ts +1 -0
  8. package/dist/types/config/model-profile-contract.d.ts +2 -0
  9. package/dist/types/config/model-registry.d.ts +4 -0
  10. package/dist/types/config/provider-auth-health.d.ts +14 -0
  11. package/dist/types/config/provider-ranking.d.ts +59 -0
  12. package/dist/types/config/settings-schema.d.ts +86 -3
  13. package/dist/types/config/settings.d.ts +3 -0
  14. package/dist/types/extensibility/extensions/types.d.ts +43 -2
  15. package/dist/types/extensibility/shared-events.d.ts +6 -0
  16. package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +5 -12
  17. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +17 -32
  18. package/dist/types/gjc-runtime/deep-interview-stage.d.ts +37 -0
  19. package/dist/types/gjc-runtime/deep-interview-state.d.ts +4 -135
  20. package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
  21. package/dist/types/gjc-runtime/linux-proc.d.ts +22 -14
  22. package/dist/types/gjc-runtime/memory-guard-owner-claims.d.ts +47 -0
  23. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +112 -1
  24. package/dist/types/gjc-runtime/state-runtime.d.ts +3 -1
  25. package/dist/types/gjc-runtime/state-writer.d.ts +5 -46
  26. package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
  27. package/dist/types/gjc-runtime/team-runtime.d.ts +2 -1
  28. package/dist/types/gjc-runtime/team-store.d.ts +12 -0
  29. package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +92 -0
  30. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +7 -0
  31. package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +8 -0
  32. package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -5
  33. package/dist/types/internal-urls/local-root-gc.d.ts +2 -0
  34. package/dist/types/main.d.ts +1 -1
  35. package/dist/types/memories/index.d.ts +2 -0
  36. package/dist/types/modes/acp/acp-agent.d.ts +22 -9
  37. package/dist/types/modes/components/assistant-message.d.ts +3 -1
  38. package/dist/types/modes/components/irc-sidebar.d.ts +41 -0
  39. package/dist/types/modes/components/queue-pane.d.ts +8 -0
  40. package/dist/types/modes/components/queued-message-selector.d.ts +6 -0
  41. package/dist/types/modes/components/read-tool-group.d.ts +4 -4
  42. package/dist/types/modes/components/session-observer-overlay.d.ts +4 -0
  43. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  44. package/dist/types/modes/components/tool-execution.d.ts +8 -0
  45. package/dist/types/modes/components/welcome.d.ts +1 -0
  46. package/dist/types/modes/controllers/event-controller.d.ts +3 -0
  47. package/dist/types/modes/interactive-mode.d.ts +7 -1
  48. package/dist/types/modes/irc-observation-ledger.d.ts +17 -1
  49. package/dist/types/modes/types.d.ts +2 -0
  50. package/dist/types/modes/utils/ui-helpers.d.ts +16 -0
  51. package/dist/types/runtime/memory-domain.d.ts +9 -0
  52. package/dist/types/runtime/memory-guard-contract.d.ts +71 -0
  53. package/dist/types/runtime/memory-guard.d.ts +39 -0
  54. package/dist/types/runtime/memory-limit.d.ts +11 -0
  55. package/dist/types/runtime-mcp/manager.d.ts +8 -0
  56. package/dist/types/runtime-mcp/smithery-auth.d.ts +5 -0
  57. package/dist/types/sdk/acp/final-text.d.ts +19 -0
  58. package/dist/types/sdk/acp/mcp.d.ts +23 -0
  59. package/dist/types/sdk/broker/lifecycle.d.ts +7 -0
  60. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
  61. package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
  62. package/dist/types/sdk/bus/index.d.ts +18 -2
  63. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +37 -0
  64. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +2 -1
  65. package/dist/types/sdk/bus/rate-limit-pool.d.ts +2 -0
  66. package/dist/types/sdk/bus/reconciliation-store.d.ts +65 -0
  67. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -4
  68. package/dist/types/sdk/bus/telegram-daemon.d.ts +23 -3
  69. package/dist/types/sdk/host/control/operations.d.ts +2 -1
  70. package/dist/types/sdk/host/host.d.ts +2 -0
  71. package/dist/types/sdk/host/query/handlers.d.ts +5 -0
  72. package/dist/types/sdk/lifecycle-session.d.ts +10 -1
  73. package/dist/types/sdk/startup-capability.d.ts +6 -0
  74. package/dist/types/session/agent-session.d.ts +51 -19
  75. package/dist/types/session/fallback-chain-controller.d.ts +18 -2
  76. package/dist/types/session/internal/managed-session-scope.d.ts +27 -0
  77. package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
  78. package/dist/types/session/session-manager.d.ts +59 -13
  79. package/dist/types/session/streaming-output.d.ts +60 -5
  80. package/dist/types/skill-state/active-state.d.ts +0 -4
  81. package/dist/types/skill-state/workflow-hud.d.ts +3 -0
  82. package/dist/types/task/executor.d.ts +6 -0
  83. package/dist/types/task/types.d.ts +36 -0
  84. package/dist/types/task/ultragoal-redteam-activation.d.ts +33 -0
  85. package/dist/types/tools/browser/launch.d.ts +7 -0
  86. package/dist/types/tools/fetch.d.ts +17 -1
  87. package/dist/types/tools/output-meta.d.ts +21 -1
  88. package/dist/types/tools/read-internals.d.ts +12 -0
  89. package/dist/types/tools/read.d.ts +45 -1
  90. package/dist/types/tools/resource-gc.d.ts +23 -0
  91. package/dist/types/tools/tool-result.d.ts +7 -1
  92. package/package.json +9 -13
  93. package/scripts/benchmark-sticky-viewport-pr1.ts +239 -0
  94. package/scripts/capture-sticky-viewport-showcase.ts +292 -0
  95. package/scripts/generate-sdk-operation-inventory.ts +4 -0
  96. package/scripts/run-test-manifest.ts +6 -4
  97. package/scripts/verify-gjc-sdk-canonicalization.ts +122 -18
  98. package/scripts/verify-sticky-viewport-showcase.ts +611 -0
  99. package/src/capability/index.ts +8 -0
  100. package/src/cli/args.ts +14 -8
  101. package/src/cli/fast-help.ts +1 -1
  102. package/src/cli/read-cli.ts +2 -1
  103. package/src/cli.ts +49 -2
  104. package/src/commands/deep-interview.ts +29 -9
  105. package/src/commands/harness.ts +35 -12
  106. package/src/commands/read.ts +11 -2
  107. package/src/commands/sdk.ts +91 -7
  108. package/src/commands/team.ts +1 -1
  109. package/src/config/model-profile-activation.ts +1 -0
  110. package/src/config/model-profile-contract.ts +13 -0
  111. package/src/config/model-profiles.ts +24 -0
  112. package/src/config/model-registry.ts +21 -2
  113. package/src/config/provider-auth-health.ts +42 -0
  114. package/src/config/provider-ranking.ts +119 -0
  115. package/src/config/settings-schema.ts +84 -3
  116. package/src/config/settings.ts +254 -108
  117. package/src/defaults/gjc/skills/deep-interview/SKILL.md +229 -82
  118. package/src/defaults/gjc/skills/ralplan/SKILL.md +78 -26
  119. package/src/defaults/gjc/skills/team/SKILL.md +1 -1
  120. package/src/defaults/gjc/skills/ultragoal/SKILL.md +26 -16
  121. package/src/eval/py/executor.ts +7 -2
  122. package/src/extensibility/extensions/runner.ts +4 -0
  123. package/src/extensibility/extensions/types.ts +42 -4
  124. package/src/extensibility/shared-events.ts +5 -0
  125. package/src/gjc-runtime/deep-interview-ambiguity.ts +47 -68
  126. package/src/gjc-runtime/deep-interview-recorder.ts +227 -310
  127. package/src/gjc-runtime/deep-interview-runtime.ts +109 -270
  128. package/src/gjc-runtime/deep-interview-stage.ts +1016 -0
  129. package/src/gjc-runtime/deep-interview-state.ts +12 -921
  130. package/src/gjc-runtime/gc-render.ts +1 -0
  131. package/src/gjc-runtime/gc-runtime.ts +11 -1
  132. package/src/gjc-runtime/linux-proc.ts +70 -36
  133. package/src/gjc-runtime/memory-guard-owner-claims.ts +389 -0
  134. package/src/gjc-runtime/ralplan-runtime.ts +979 -71
  135. package/src/gjc-runtime/state-runtime.ts +50 -12
  136. package/src/gjc-runtime/state-writer.ts +70 -222
  137. package/src/gjc-runtime/team-launch.ts +34 -5
  138. package/src/gjc-runtime/team-runtime.ts +970 -33
  139. package/src/gjc-runtime/team-store.ts +32 -0
  140. package/src/gjc-runtime/team-worker-memory-guard.ts +363 -0
  141. package/src/gjc-runtime/team-workers.ts +1 -0
  142. package/src/gjc-runtime/tmux-owner-isolation.ts +18 -0
  143. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +20 -0
  144. package/src/gjc-runtime/ultragoal-runtime.ts +13 -7
  145. package/src/gjc-runtime/workflow-manifest.generated.json +97 -350
  146. package/src/gjc-runtime/workflow-manifest.ts +34 -179
  147. package/src/hooks/native-skill-hook.ts +17 -4
  148. package/src/hooks/skill-state.ts +1 -23
  149. package/src/internal-urls/docs-index.generated.ts +10 -11
  150. package/src/internal-urls/local-protocol.ts +23 -4
  151. package/src/internal-urls/local-root-gc.ts +127 -0
  152. package/src/main.ts +9 -5
  153. package/src/memories/index.ts +10 -0
  154. package/src/modes/DESIGN.md +47 -0
  155. package/src/modes/acp/acp-agent.ts +597 -67
  156. package/src/modes/acp/acp-event-mapper.ts +106 -12
  157. package/src/modes/acp/acp-mode.ts +39 -3
  158. package/src/modes/components/assistant-message.ts +123 -22
  159. package/src/modes/components/irc-sidebar.ts +176 -62
  160. package/src/modes/components/model-selector.ts +76 -5
  161. package/src/modes/components/notifications-settings-editor.ts +1 -1
  162. package/src/modes/components/oauth-selector.ts +42 -12
  163. package/src/modes/components/queue-pane.ts +55 -9
  164. package/src/modes/components/queued-message-selector.ts +53 -10
  165. package/src/modes/components/read-tool-group.ts +82 -14
  166. package/src/modes/components/session-observer-overlay.ts +187 -32
  167. package/src/modes/components/settings-selector.ts +60 -18
  168. package/src/modes/components/tool-execution.ts +138 -27
  169. package/src/modes/components/tool-status-header.ts +1 -4
  170. package/src/modes/components/welcome.ts +18 -3
  171. package/src/modes/controllers/command-controller.ts +4 -0
  172. package/src/modes/controllers/event-controller.ts +109 -27
  173. package/src/modes/controllers/extension-ui-controller.ts +23 -12
  174. package/src/modes/controllers/input-controller.ts +62 -6
  175. package/src/modes/controllers/selector-controller.ts +83 -37
  176. package/src/modes/interactive-mode.ts +127 -29
  177. package/src/modes/irc-observation-ledger.ts +71 -15
  178. package/src/modes/runtime-init.ts +20 -2
  179. package/src/modes/types.ts +2 -0
  180. package/src/modes/utils/ui-helpers.ts +94 -1
  181. package/src/prompts/agents/architect.md +9 -1
  182. package/src/prompts/agents/critic.md +9 -0
  183. package/src/prompts/agents/executor.md +1 -1
  184. package/src/prompts/system/system-prompt.md +0 -1
  185. package/src/prompts/tools/read.md +4 -2
  186. package/src/runtime/memory-domain.ts +66 -0
  187. package/src/runtime/memory-guard-contract.ts +73 -0
  188. package/src/runtime/memory-guard.ts +292 -0
  189. package/src/runtime/memory-limit.ts +50 -0
  190. package/src/runtime-mcp/manager.ts +26 -6
  191. package/src/runtime-mcp/smithery-auth.ts +17 -2
  192. package/src/runtime-mcp/smithery-connect.ts +11 -1
  193. package/src/sdk/acp/adapter.ts +8 -1
  194. package/src/sdk/acp/final-text.ts +40 -0
  195. package/src/sdk/acp/mcp.ts +26 -0
  196. package/src/sdk/broker/lifecycle.ts +107 -7
  197. package/src/sdk/bus/chat-daemon-cli.ts +2 -2
  198. package/src/sdk/bus/chat-daemon-control.ts +4 -3
  199. package/src/sdk/bus/config.ts +1 -1
  200. package/src/sdk/bus/control-drain-lease.ts +104 -0
  201. package/src/sdk/bus/index.ts +503 -60
  202. package/src/sdk/bus/kind-aware-reconciliation.ts +242 -0
  203. package/src/sdk/bus/prompt-reconciliation.ts +2 -1
  204. package/src/sdk/bus/rate-limit-pool.ts +8 -0
  205. package/src/sdk/bus/reconciliation-store.ts +224 -0
  206. package/src/sdk/bus/telegram-daemon-contract.ts +6 -4
  207. package/src/sdk/bus/telegram-daemon.ts +531 -53
  208. package/src/sdk/host/control/dispatch.ts +7 -1
  209. package/src/sdk/host/control/operations.ts +2 -1
  210. package/src/sdk/host/host.ts +25 -3
  211. package/src/sdk/host/query/handlers.ts +55 -0
  212. package/src/sdk/lifecycle-session.ts +19 -3
  213. package/src/sdk/mcp/server.ts +46 -18
  214. package/src/sdk/protocol/operation-inventory.generated.json +76 -0
  215. package/src/sdk/protocol/operation-registry.ts +8 -2
  216. package/src/sdk/session.ts +25 -2
  217. package/src/sdk/startup-capability.ts +6 -0
  218. package/src/session/agent-session.ts +720 -190
  219. package/src/session/fallback-chain-controller.ts +47 -8
  220. package/src/session/internal/managed-session-scope.ts +40 -10
  221. package/src/session/internal/native-publish-outcome.ts +8 -3
  222. package/src/session/memory-guard-checkpoint-participant.ts +131 -0
  223. package/src/session/session-manager.ts +1372 -42
  224. package/src/session/streaming-output.ts +262 -43
  225. package/src/setup/provider-onboarding.ts +19 -4
  226. package/src/skill-state/active-state.ts +0 -10
  227. package/src/skill-state/workflow-hud.ts +25 -2
  228. package/src/skill-state/workflow-mutation-guard.ts +65 -7
  229. package/src/task/executor.ts +11 -3
  230. package/src/task/index.ts +2 -0
  231. package/src/task/types.ts +22 -11
  232. package/src/task/ultragoal-redteam-activation.ts +75 -0
  233. package/src/tools/ask.ts +89 -69
  234. package/src/tools/browser/launch.ts +41 -6
  235. package/src/tools/fetch.ts +114 -23
  236. package/src/tools/output-meta.ts +176 -9
  237. package/src/tools/path-utils.ts +5 -1
  238. package/src/tools/read-internals.ts +44 -0
  239. package/src/tools/read.ts +1542 -310
  240. package/src/tools/resource-gc.ts +665 -125
  241. package/src/tools/tool-result.ts +14 -1
  242. package/src/web/search/providers/codex.ts +1 -1
  243. package/src/workflow/workflow-intent-diff.ts +4 -1
  244. package/dist/types/gjc-runtime/deep-interview-draft.d.ts +0 -8
  245. package/dist/types/gjc-runtime/deep-interview-payload.d.ts +0 -41
  246. package/dist/types/gjc-runtime/deep-interview-repair.d.ts +0 -17
  247. package/src/gjc-runtime/deep-interview-draft.ts +0 -843
  248. package/src/gjc-runtime/deep-interview-payload.ts +0 -262
  249. package/src/gjc-runtime/deep-interview-repair.ts +0 -1674
package/src/task/types.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { ThinkingLevel } from "@gajae-code/agent-core";
2
2
  import type { Usage } from "@gajae-code/ai";
3
- import { $pickenv } from "@gajae-code/utils";
3
+ import { $env } from "@gajae-code/utils";
4
4
  import * as z from "zod/v4";
5
5
  import { isValidTaskId, TASK_ID_DESCRIPTION } from "./id";
6
6
  import type { TaskResultReceipt } from "./receipt";
@@ -14,23 +14,28 @@ export type AgentSource = "bundled" | "user" | "project";
14
14
  export type ForkContextPolicy = "forbidden" | "allowed";
15
15
  export type ForkContextMode = "none" | "receipt" | "last-turn" | "bounded" | "full";
16
16
 
17
- const parseNumber = (value: string | undefined, defaultValue: number): number => {
18
- if (value) {
19
- try {
20
- const number = Number.parseInt(value, 10);
21
- if (!Number.isNaN(number) && number > 0) {
22
- return number;
23
- }
24
- } catch {}
17
+ const parsePositiveIntegerEnvironment = (keys: string[], defaultValue: number): number => {
18
+ for (const key of keys) {
19
+ const value = $env[key];
20
+ if (!value || value.trim().length === 0) continue;
21
+ if (!/^\d+$/.test(value)) return defaultValue;
22
+ const number = Number(value);
23
+ return Number.isSafeInteger(number) && number > 0 ? number : defaultValue;
25
24
  }
26
25
  return defaultValue;
27
26
  };
28
27
 
29
28
  /** Maximum output bytes per agent */
30
- export const MAX_OUTPUT_BYTES = parseNumber($pickenv("GJC_TASK_MAX_OUTPUT_BYTES", "PI_TASK_MAX_OUTPUT_BYTES"), 500_000);
29
+ export const MAX_OUTPUT_BYTES = parsePositiveIntegerEnvironment(
30
+ ["GJC_TASK_MAX_OUTPUT_BYTES", "PI_TASK_MAX_OUTPUT_BYTES"],
31
+ 500_000,
32
+ );
31
33
 
32
34
  /** Maximum output lines per agent */
33
- export const MAX_OUTPUT_LINES = parseNumber($pickenv("GJC_TASK_MAX_OUTPUT_LINES", "PI_TASK_MAX_OUTPUT_LINES"), 5000);
35
+ export const MAX_OUTPUT_LINES = parsePositiveIntegerEnvironment(
36
+ ["GJC_TASK_MAX_OUTPUT_LINES", "PI_TASK_MAX_OUTPUT_LINES"],
37
+ 5000,
38
+ );
34
39
 
35
40
  /** EventBus channel for raw subagent events */
36
41
  export const TASK_SUBAGENT_EVENT_CHANNEL = "task:subagent:event";
@@ -92,6 +97,12 @@ const createTaskItemSchema = (_contextEnabled: boolean) =>
92
97
  id: z.string().max(48).refine(isValidTaskId, TASK_ID_DESCRIPTION).describe("filesystem-safe task identifier"),
93
98
  description: z.string().describe("ui label, not seen by subagent"),
94
99
  assignment: z.string().describe(assignmentDescription),
100
+ executionMode: z
101
+ .enum(["default", "ultragoal-red-team"])
102
+ .optional()
103
+ .describe(
104
+ "typed executor mode: default keeps ordinary executor behavior; ultragoal-red-team injects the Ultragoal QA/red-team prompt fragment. Prefer this over free-form assignment text (#2698).",
105
+ ),
95
106
  inheritContext: z
96
107
  .enum(["none", "receipt", "last-turn", "bounded", "full"])
97
108
  .optional()
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Decide whether an executor assignment should inject the ultragoal red-team
3
+ * prompt fragment.
4
+ *
5
+ * Preference order (parse, don't re-validate later):
6
+ * 1. Explicit typed `executionMode` on the task / ExecutorOptions — authoritative.
7
+ * 2. Assignment text heuristics — only explicit Ultragoal QA/red-team phrasing.
8
+ *
9
+ * Bare mentions of `executorQa` (docs, quality-gate field names, negative
10
+ * instructions) must not flip the mode (#2698 / #2456).
11
+ *
12
+ * Mirrors `executor.md`:
13
+ * "activates only when the assignment explicitly labels Executor as Ultragoal
14
+ * completion QA/red-team or asks for `executorQa` red-team evidence."
15
+ */
16
+
17
+ export const EXECUTOR_EXECUTION_MODES = ["default", "ultragoal-red-team"] as const;
18
+ export type ExecutorExecutionMode = (typeof EXECUTOR_EXECUTION_MODES)[number];
19
+
20
+ const ULTRAGOAL_COMPLETION_QA = /\bultragoal\s+completion\s+(?:qa|red[-\s]?team)\b/i;
21
+
22
+ /** `executorQa` within a short window of red-team / matrix / evidence framing. */
23
+ const EXECUTOR_QA_EVIDENCE =
24
+ /\bexecutorQa\b[\s\S]{0,120}\b(?:red[-\s]?team|matrix|evidence)\b|\b(?:red[-\s]?team|matrix|evidence)\b[\s\S]{0,120}\bexecutorQa\b/i;
25
+
26
+ /** Common Ultragoal skill spawn phrasing: "executor QA/red-team lane". */
27
+ const EXECUTOR_QA_LANE = /\bexecutor\b[\s\S]{0,48}\b(?:qa|red[-\s]?team)\s+lane\b/i;
28
+
29
+ export function isExecutorExecutionMode(value: unknown): value is ExecutorExecutionMode {
30
+ return value === "default" || value === "ultragoal-red-team";
31
+ }
32
+
33
+ /**
34
+ * Parse a caller-supplied mode. Unknown values fail closed to `undefined`
35
+ * so the assignment heuristic (or default off) still applies — never invent on.
36
+ */
37
+ export function parseExecutorExecutionMode(value: unknown): ExecutorExecutionMode | undefined {
38
+ if (typeof value !== "string") return undefined;
39
+ const normalized = value.trim().toLowerCase().replaceAll("_", "-");
40
+ if (normalized === "default" || normalized === "ordinary" || normalized === "implement") {
41
+ return "default";
42
+ }
43
+ if (
44
+ normalized === "ultragoal-red-team" ||
45
+ normalized === "ultragoal-redteam" ||
46
+ normalized === "red-team" ||
47
+ normalized === "redteam" ||
48
+ normalized === "executor-qa"
49
+ ) {
50
+ return "ultragoal-red-team";
51
+ }
52
+ return undefined;
53
+ }
54
+
55
+ export function assignmentRequestsUltragoalRedTeam(assignment: string | undefined): boolean {
56
+ const text = assignment?.trim() ?? "";
57
+ if (text.length === 0) return false;
58
+ return ULTRAGOAL_COMPLETION_QA.test(text) || EXECUTOR_QA_EVIDENCE.test(text) || EXECUTOR_QA_LANE.test(text);
59
+ }
60
+
61
+ export interface ResolveUltragoalRedTeamArgs {
62
+ readonly executionMode?: unknown;
63
+ readonly assignment?: string;
64
+ }
65
+
66
+ /**
67
+ * Final activation decision for the executor prompt fragment.
68
+ * Typed mode wins; assignment text is a compatibility fallback only.
69
+ */
70
+ export function resolveUltragoalRedTeamActivation(args: ResolveUltragoalRedTeamArgs): boolean {
71
+ const mode = parseExecutorExecutionMode(args.executionMode);
72
+ if (mode === "ultragoal-red-team") return true;
73
+ if (mode === "default") return false;
74
+ return assignmentRequestsUltragoalRedTeam(args.assignment);
75
+ }
package/src/tools/ask.ts CHANGED
@@ -351,15 +351,23 @@ function parseEncodedContainer(value: unknown): unknown {
351
351
  function isRoundZeroRecoveryCandidate(value: unknown): boolean {
352
352
  const root = parseEncodedContainer(value);
353
353
  if (typeof root !== "object" || root === null || !Object.hasOwn(root, "questions")) return false;
354
- const questionsValue = parseEncodedContainer((root as Record<string, unknown>).questions);
354
+ const rawQuestions = (root as Record<string, unknown>).questions;
355
+ const questionsValue = parseEncodedContainer(rawQuestions);
355
356
  if (!Array.isArray(questionsValue)) return questionsValue === null;
357
+ const rootEncoded = typeof value === "string" || typeof rawQuestions === "string";
356
358
  return questionsValue.some(rawQuestion => {
357
359
  const question = parseEncodedContainer(rawQuestion);
358
360
  if (typeof question !== "object" || question === null || !Object.hasOwn(question, "deepInterview")) return false;
359
- const deepInterview = parseEncodedContainer((question as Record<string, unknown>).deepInterview);
361
+ const rawDeepInterview = (question as Record<string, unknown>).deepInterview;
362
+ const deepInterview = parseEncodedContainer(rawDeepInterview);
360
363
  if (typeof deepInterview !== "object" || deepInterview === null) return false;
361
364
  const metadata = deepInterview as Record<string, unknown>;
362
- return Object.hasOwn(metadata, "intent_contract") || Object.hasOwn(metadata, "intent_review");
365
+ const hasContract = Object.hasOwn(metadata, "intent_contract");
366
+ const hasReview = Object.hasOwn(metadata, "intent_review");
367
+ // A JSON-string container is terminal only for the retired contract+review
368
+ // pair; canonical single-sided asks stay eligible for generic JSON coercion.
369
+ const encoded = rootEncoded || typeof rawQuestion === "string" || typeof rawDeepInterview === "string";
370
+ return encoded ? hasContract && hasReview : hasContract || hasReview;
363
371
  });
364
372
  }
365
373
 
@@ -1252,6 +1260,14 @@ export class AskTool implements AgentTool<AskParametersSchema, AskToolDetails> {
1252
1260
  receipt: AskRemoteReceipt;
1253
1261
  settlement?: AskSettlement;
1254
1262
  };
1263
+ const ignoreRemoteAnswerFailure = (error: unknown): Promise<never> => {
1264
+ if (!(error instanceof Error && error.name === "AbortError")) {
1265
+ logger.warn("Ask remote answer source failed", {
1266
+ error: error instanceof Error ? error.message : String(error),
1267
+ });
1268
+ }
1269
+ return new Promise<never>(() => {});
1270
+ };
1255
1271
  const settleActiveRemote = async (settlement: AskSettlement): Promise<void> => {
1256
1272
  const receipt = activeRemoteReceipt;
1257
1273
  activeRemoteReceipt = undefined;
@@ -1304,60 +1320,62 @@ export class AskTool implements AgentTool<AskParametersSchema, AskToolDetails> {
1304
1320
  remoteController.signal,
1305
1321
  )
1306
1322
  : source.awaitAnswer(prompt, options, remoteController.signal)
1307
- ).then((answer): RemoteRaceResult | Promise<RemoteRaceResult> => {
1308
- if (answer === undefined) return new Promise<never>(() => {});
1309
- const receipt = typeof answer === "string" ? legacyAskReceipt(answer) : answer;
1310
- if (generation !== remoteGeneration) {
1311
- return receipt
1312
- .settle({ kind: "resolve_without_commit", reason: "aborted" })
1313
- .then(() => new Promise<never>(() => {}));
1314
- }
1315
- const remoteValue = receipt.interaction.kind === "value" ? receipt.interaction.value : undefined;
1316
- const value = remoteValue ?? REMOTE_NAVIGATION_FORWARD;
1317
- const selectedValue =
1318
- remoteValue === undefined
1319
- ? value
1320
- : (options.find(
1321
- option =>
1322
- option === remoteValue ||
1323
- option === `${theme.checkbox.checked} ${remoteValue}` ||
1324
- option === `${theme.checkbox.unchecked} ${remoteValue}`,
1325
- ) ?? value);
1326
- const normalizedRemoteValue = remoteValue?.startsWith(`${theme.checkbox.checked} `)
1327
- ? remoteValue.slice(`${theme.checkbox.checked} `.length)
1328
- : remoteValue?.startsWith(`${theme.checkbox.unchecked} `)
1329
- ? remoteValue.slice(`${theme.checkbox.unchecked} `.length)
1330
- : remoteValue;
1331
- const semanticRemoteValue = normalizedRemoteValue?.replace(/^\s*\d+[.)]\s+/, "");
1332
- const transitionReason =
1333
- semanticRemoteValue === OTHER_OPTION
1334
- ? "other_transition"
1335
- : semanticRemoteValue === ASK_CLARIFICATION_OPTION
1336
- ? "clarification_transition"
1337
- : undefined;
1338
- if (transitionReason) {
1339
- return {
1340
- winner: "remote" as const,
1341
- value: selectedValue,
1342
- receipt,
1343
- settlement: { kind: "resolve_without_commit", reason: transitionReason },
1344
- };
1345
- }
1346
- if (
1347
- remoteValue !== undefined &&
1348
- activeRemoteRequest?.interaction === "selector" &&
1349
- activeRemoteRequest.controls.length > 0 &&
1350
- activeRemoteRequest.options.includes(remoteValue)
1351
- ) {
1352
- return {
1353
- winner: "remote" as const,
1354
- value: selectedValue,
1355
- receipt,
1356
- settlement: { kind: "resolve_without_commit", reason: "toggle" },
1357
- };
1358
- }
1359
- return { winner: "remote" as const, value: selectedValue, receipt };
1360
- });
1323
+ )
1324
+ .then((answer): RemoteRaceResult | Promise<RemoteRaceResult> => {
1325
+ if (answer === undefined) return new Promise<never>(() => {});
1326
+ const receipt = typeof answer === "string" ? legacyAskReceipt(answer) : answer;
1327
+ if (generation !== remoteGeneration) {
1328
+ return receipt
1329
+ .settle({ kind: "resolve_without_commit", reason: "aborted" })
1330
+ .then(() => new Promise<never>(() => {}));
1331
+ }
1332
+ const remoteValue = receipt.interaction.kind === "value" ? receipt.interaction.value : undefined;
1333
+ const value = remoteValue ?? REMOTE_NAVIGATION_FORWARD;
1334
+ const selectedValue =
1335
+ remoteValue === undefined
1336
+ ? value
1337
+ : (options.find(
1338
+ option =>
1339
+ option === remoteValue ||
1340
+ option === `${theme.checkbox.checked} ${remoteValue}` ||
1341
+ option === `${theme.checkbox.unchecked} ${remoteValue}`,
1342
+ ) ?? value);
1343
+ const normalizedRemoteValue = remoteValue?.startsWith(`${theme.checkbox.checked} `)
1344
+ ? remoteValue.slice(`${theme.checkbox.checked} `.length)
1345
+ : remoteValue?.startsWith(`${theme.checkbox.unchecked} `)
1346
+ ? remoteValue.slice(`${theme.checkbox.unchecked} `.length)
1347
+ : remoteValue;
1348
+ const semanticRemoteValue = normalizedRemoteValue?.replace(/^\s*\d+[.)]\s+/, "");
1349
+ const transitionReason =
1350
+ semanticRemoteValue === OTHER_OPTION
1351
+ ? "other_transition"
1352
+ : semanticRemoteValue === ASK_CLARIFICATION_OPTION
1353
+ ? "clarification_transition"
1354
+ : undefined;
1355
+ if (transitionReason) {
1356
+ return {
1357
+ winner: "remote" as const,
1358
+ value: selectedValue,
1359
+ receipt,
1360
+ settlement: { kind: "resolve_without_commit", reason: transitionReason },
1361
+ };
1362
+ }
1363
+ if (
1364
+ remoteValue !== undefined &&
1365
+ activeRemoteRequest?.interaction === "selector" &&
1366
+ activeRemoteRequest.controls.length > 0 &&
1367
+ activeRemoteRequest.options.includes(remoteValue)
1368
+ ) {
1369
+ return {
1370
+ winner: "remote" as const,
1371
+ value: selectedValue,
1372
+ receipt,
1373
+ settlement: { kind: "resolve_without_commit", reason: "toggle" },
1374
+ };
1375
+ }
1376
+ return { winner: "remote" as const, value: selectedValue, receipt };
1377
+ })
1378
+ .catch(ignoreRemoteAnswerFailure);
1361
1379
 
1362
1380
  const local = extensionUi
1363
1381
  .select(prompt, options, { ...dialogOptions, signal: localController.signal })
@@ -1419,18 +1437,20 @@ export class AskTool implements AgentTool<AskParametersSchema, AskToolDetails> {
1419
1437
  remoteController.signal,
1420
1438
  )
1421
1439
  : source.awaitAnswer(title, [], remoteController.signal)
1422
- ).then((answer): RemoteRaceResult | Promise<RemoteRaceResult> => {
1423
- if (answer === undefined) return new Promise<never>(() => {});
1424
- const receipt = typeof answer === "string" ? legacyAskReceipt(answer) : answer;
1425
- if (generation !== remoteGeneration) {
1426
- return receipt
1427
- .settle({ kind: "resolve_without_commit", reason: "aborted" })
1428
- .then(() => new Promise<never>(() => {}));
1429
- }
1430
- const value =
1431
- receipt.interaction.kind === "control" ? REMOTE_NAVIGATION_FORWARD : receipt.interaction.value;
1432
- return { winner: "remote" as const, value, receipt };
1433
- });
1440
+ )
1441
+ .then((answer): RemoteRaceResult | Promise<RemoteRaceResult> => {
1442
+ if (answer === undefined) return new Promise<never>(() => {});
1443
+ const receipt = typeof answer === "string" ? legacyAskReceipt(answer) : answer;
1444
+ if (generation !== remoteGeneration) {
1445
+ return receipt
1446
+ .settle({ kind: "resolve_without_commit", reason: "aborted" })
1447
+ .then(() => new Promise<never>(() => {}));
1448
+ }
1449
+ const value =
1450
+ receipt.interaction.kind === "control" ? REMOTE_NAVIGATION_FORWARD : receipt.interaction.value;
1451
+ return { winner: "remote" as const, value, receipt };
1452
+ })
1453
+ .catch(ignoreRemoteAnswerFailure);
1434
1454
  const local = extensionUi
1435
1455
  .editor(title, prefill, { ...(dialogOptions ?? {}), signal: localController.signal }, editorOptions)
1436
1456
  .then(answer => {
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
4
  import { getPuppeteerDir } from "@gajae-code/utils/dirs";
5
+ import { $credentialEnv } from "@gajae-code/utils/env";
5
6
  import * as logger from "@gajae-code/utils/logger";
6
7
  import { $which } from "@gajae-code/utils/which";
7
8
  import * as browsers from "@puppeteer/browsers";
@@ -80,6 +81,42 @@ export async function loadPuppeteerInWorker(safeDir: string): Promise<typeof Pup
80
81
  }
81
82
  }
82
83
 
84
+ /**
85
+ * Browser launch overrides resolved from trusted environment sources only.
86
+ *
87
+ * These choose the browser binary and route/verify its traffic, so whatever can
88
+ * set them controls what runs and who sees the session. `$env` merges the
89
+ * caller's `cwd/.env` into `process.env`, so reading them directly would let
90
+ * repository content pick the executable, point every request at an attacker's
91
+ * proxy, or disable certificate validation. Resolve them the same way provider
92
+ * credentials are: launching shell plus GJC/user-owned `.env` files, never the
93
+ * project `.env`.
94
+ */
95
+ function trustedBrowserEnv(name: string): string | undefined {
96
+ return $credentialEnv(name);
97
+ }
98
+
99
+ /** Boolean-like trusted browser env knob (`true`/`1`/`yes`/`on`). */
100
+ function browserLaunchFlagEnabled(name: string): boolean {
101
+ const value = trustedBrowserEnv(name)?.toLowerCase();
102
+ return value === "true" || value === "1" || value === "yes" || value === "on";
103
+ }
104
+
105
+ /** Test seam: the browser launch overrides as resolved from trusted env. */
106
+ export function resolveBrowserEnvOverridesForTest(): {
107
+ executablePath: string | undefined;
108
+ proxy: string | undefined;
109
+ proxyBypassLoopback: boolean;
110
+ ignoreCertErrors: boolean;
111
+ } {
112
+ return {
113
+ executablePath: trustedBrowserEnv("PUPPETEER_EXECUTABLE_PATH"),
114
+ proxy: trustedBrowserEnv("PUPPETEER_PROXY"),
115
+ proxyBypassLoopback: browserLaunchFlagEnabled("PUPPETEER_PROXY_BYPASS_LOOPBACK"),
116
+ ignoreCertErrors: browserLaunchFlagEnabled("PUPPETEER_PROXY_IGNORE_CERT_ERRORS"),
117
+ };
118
+ }
119
+
83
120
  /**
84
121
  * Lazily download Chromium on first browser launch via @puppeteer/browsers.
85
122
  * Skipped when a system Chromium (NixOS) or PUPPETEER_EXECUTABLE_PATH is set.
@@ -89,7 +126,7 @@ let chromiumExecutablePromise: Promise<string | undefined> | undefined;
89
126
  async function ensureChromiumExecutable(): Promise<string | undefined> {
90
127
  const sysChrome = resolveSystemChromium();
91
128
  if (sysChrome) return sysChrome;
92
- const envPath = process.env.PUPPETEER_EXECUTABLE_PATH;
129
+ const envPath = trustedBrowserEnv("PUPPETEER_EXECUTABLE_PATH");
93
130
  if (envPath) return envPath;
94
131
  if (chromiumExecutablePromise) return chromiumExecutablePromise;
95
132
 
@@ -272,13 +309,12 @@ export async function launchHeadlessBrowser(opts: LaunchHeadlessOptions): Promis
272
309
  // Isolated copy only (caller guarantees). Never point this at a live profile.
273
310
  launchArgs.push(`--user-data-dir=${opts.profileWarmupDir}`);
274
311
  }
275
- const proxy = process.env.PUPPETEER_PROXY;
312
+ const proxy = trustedBrowserEnv("PUPPETEER_PROXY");
276
313
  if (proxy) {
277
314
  launchArgs.push(`--proxy-server=${proxy}`);
278
315
  // Chrome (since v72) bypasses proxies for localhost by default. When PUPPETEER_PROXY_BYPASS_LOOPBACK
279
316
  // is true, add <-loopback> so traffic to localhost reaches the proxy (e.g. for mitmdump/auth capture).
280
- const bypassLoopback = process.env.PUPPETEER_PROXY_BYPASS_LOOPBACK?.toLowerCase();
281
- if (bypassLoopback === "true" || bypassLoopback === "1" || bypassLoopback === "yes" || bypassLoopback === "on") {
317
+ if (browserLaunchFlagEnabled("PUPPETEER_PROXY_BYPASS_LOOPBACK")) {
282
318
  launchArgs.push("--proxy-bypass-list=<-loopback>");
283
319
  }
284
320
  }
@@ -287,8 +323,7 @@ export async function launchHeadlessBrowser(opts: LaunchHeadlessOptions): Promis
287
323
  if (opts.geo?.locale) {
288
324
  launchArgs.push(`--lang=${opts.geo.locale}`);
289
325
  }
290
- const ignoreCert = process.env.PUPPETEER_PROXY_IGNORE_CERT_ERRORS?.toLowerCase();
291
- if (ignoreCert === "true" || ignoreCert === "1" || ignoreCert === "yes" || ignoreCert === "on") {
326
+ if (browserLaunchFlagEnabled("PUPPETEER_PROXY_IGNORE_CERT_ERRORS")) {
292
327
  launchArgs.push("--ignore-certificate-errors");
293
328
  }
294
329
  const launchEnv = opts.geo?.timezone ? { ...process.env, TZ: opts.geo.timezone } : undefined;
@@ -10,7 +10,7 @@ import type { RenderResultOptions } from "../extensibility/custom-tools/types";
10
10
  import { type Theme, theme } from "../modes/theme/theme";
11
11
  import type { ToolSession } from "../sdk";
12
12
  import type { AgentStorage } from "../session/agent-storage";
13
- import { DEFAULT_MAX_BYTES, truncateHead } from "../session/streaming-output";
13
+ import { DEFAULT_MAX_BYTES, type TruncationDirection, truncateContent } from "../session/streaming-output";
14
14
  import { renderStatusLine } from "../tui";
15
15
  import { CachedOutputBlock } from "../tui/output-block";
16
16
  import { formatDimensionNote, resizeImage } from "../utils/image-resize";
@@ -1146,7 +1146,12 @@ interface ReadUrlCacheEntry {
1146
1146
  artifactId?: string;
1147
1147
  details: ReadUrlToolDetails;
1148
1148
  image?: FetchImagePayload;
1149
+ /** Complete URL output, including its preamble and body. */
1149
1150
  output: string;
1151
+ /** UTF-16 code-unit offset of the preamble in `output`. */
1152
+ preambleChars: number;
1153
+ /** Offset of the preamble after `wrapUntrustedContent(output)` is persisted. */
1154
+ wrappedPreambleChars?: number;
1150
1155
  }
1151
1156
 
1152
1157
  const readUrlCache = new Map<string, ReadUrlCacheEntry>();
@@ -1178,7 +1183,13 @@ async function materializeReadUrlCacheEntry(
1178
1183
  if (entry.artifactId) {
1179
1184
  const artifactOutput = await readArtifactOutput(session, entry.artifactId);
1180
1185
  if (artifactOutput !== null) {
1181
- return { ...entry, output: artifactOutput };
1186
+ // Artifacts retain their trust-boundary wrapper. Keep the raw preamble
1187
+ // coordinate alongside the escaped coordinate for live/rehydrated parity.
1188
+ return {
1189
+ ...entry,
1190
+ output: artifactOutput,
1191
+ ...(entry.wrappedPreambleChars !== undefined ? { wrappedPreambleChars: entry.wrappedPreambleChars } : {}),
1192
+ };
1182
1193
  }
1183
1194
  }
1184
1195
  return null;
@@ -1218,8 +1229,8 @@ async function buildReadUrlCacheEntry(
1218
1229
 
1219
1230
  const storage = session.settings.getStorage();
1220
1231
  const result = await renderUrl(url, effectiveTimeout, raw, session.settings, signal, storage);
1221
- const output = buildUrlReadOutput(result, result.content);
1222
- const artifactId = options?.ensureArtifact ? await persistReadUrlArtifact(session, output) : undefined;
1232
+ const built = buildUrlReadOutput(result, result.content);
1233
+ const artifactId = options?.ensureArtifact ? await persistReadUrlArtifact(session, built.output) : undefined;
1223
1234
 
1224
1235
  return {
1225
1236
  artifactId,
@@ -1233,7 +1244,9 @@ async function buildReadUrlCacheEntry(
1233
1244
  notes: result.notes,
1234
1245
  },
1235
1246
  image: result.image,
1236
- output,
1247
+ output: built.output,
1248
+ preambleChars: built.preambleChars,
1249
+ wrappedPreambleChars: built.wrappedPreambleChars,
1237
1250
  };
1238
1251
  }
1239
1252
 
@@ -1263,60 +1276,138 @@ export async function loadReadUrlCacheEntry(
1263
1276
 
1264
1277
  const UNTRUSTED_CONTENT_OPEN = "<untrusted-content>";
1265
1278
  const UNTRUSTED_CONTENT_CLOSE = "</untrusted-content>";
1279
+ export const WRAP_PREFIX_CHARS = `${UNTRUSTED_CONTENT_OPEN}\n`.length;
1280
+ const WRAP_SUFFIX = `\n${UNTRUSTED_CONTENT_CLOSE}`;
1281
+
1282
+ export function escapeUntrustedContent(content: string): string {
1283
+ return content.replace(/<\/untrusted-content>/gi, "&lt;/untrusted-content>");
1284
+ }
1266
1285
 
1267
1286
  export function wrapUntrustedContent(content: string): string {
1268
- return `${UNTRUSTED_CONTENT_OPEN}\n${content.replace(/<\/untrusted-content>/gi, "&lt;/untrusted-content>")}\n${UNTRUSTED_CONTENT_CLOSE}`;
1287
+ return `${UNTRUSTED_CONTENT_OPEN}\n${escapeUntrustedContent(content)}${WRAP_SUFFIX}`;
1288
+ }
1289
+
1290
+ function isWrappedReadUrlOutput(output: string): boolean {
1291
+ return output.startsWith(`${UNTRUSTED_CONTENT_OPEN}\n`) && output.endsWith(WRAP_SUFFIX);
1292
+ }
1293
+
1294
+ /**
1295
+ * Remove only the outer URL-output wrapper before line pagination. The content
1296
+ * remains escaped exactly as persisted; it is deliberately never unescaped.
1297
+ */
1298
+ export function prepareReadUrlSelectorInput(
1299
+ output: string,
1300
+ preambleChars: number,
1301
+ wrappedPreambleChars?: number,
1302
+ ): { text: string; preambleChars: number } {
1303
+ if (!isWrappedReadUrlOutput(output)) return { text: output, preambleChars };
1304
+ const bodyEnd = output.length - WRAP_SUFFIX.length;
1305
+ return {
1306
+ text: output.slice(WRAP_PREFIX_CHARS, Math.max(WRAP_PREFIX_CHARS, bodyEnd)),
1307
+ preambleChars: Math.max(0, (wrappedPreambleChars ?? preambleChars) - WRAP_PREFIX_CHARS),
1308
+ };
1309
+ }
1310
+ interface BuiltUrlReadOutput {
1311
+ output: string;
1312
+ preambleChars: number;
1313
+ wrappedPreambleChars: number;
1269
1314
  }
1270
1315
 
1271
- function buildUrlReadOutput(result: FetchRenderResult, content: string): string {
1272
- let output = "";
1273
- output += `URL: ${result.finalUrl}\n`;
1274
- output += `Content-Type: ${result.contentType}\n`;
1275
- output += `Method: ${result.method}\n`;
1316
+ function buildReadUrlPreamble(
1317
+ result: Pick<FetchRenderResult, "finalUrl" | "contentType" | "method" | "notes">,
1318
+ ): string {
1319
+ let preamble = "";
1320
+ preamble += `URL: ${result.finalUrl}\n`;
1321
+ preamble += `Content-Type: ${result.contentType}\n`;
1322
+ preamble += `Method: ${result.method}\n`;
1276
1323
  if (result.notes.length > 0) {
1277
- output += `Notes: ${result.notes.join("; ")}\n`;
1324
+ preamble += `Notes: ${result.notes.join("; ")}\n`;
1278
1325
  }
1279
- output += `\n---\n\n`;
1280
- output += content;
1281
- return output;
1326
+ preamble += "\n---\n\n";
1327
+ return preamble;
1328
+ }
1329
+
1330
+ function buildUrlReadOutput(result: FetchRenderResult, content: string): BuiltUrlReadOutput {
1331
+ const preamble = buildReadUrlPreamble(result);
1332
+ const preambleChars = preamble.length;
1333
+ const output = `${preamble}${content}`;
1334
+ return {
1335
+ output,
1336
+ preambleChars,
1337
+ wrappedPreambleChars: WRAP_PREFIX_CHARS + escapeUntrustedContent(preamble).length,
1338
+ };
1282
1339
  }
1283
1340
 
1284
1341
  export async function executeReadUrl(
1285
1342
  session: ToolSession,
1286
1343
  params: { path: string; raw?: boolean },
1287
1344
  signal?: AbortSignal,
1345
+ direction: TruncationDirection = "head",
1288
1346
  ): Promise<AgentToolResult<ReadUrlToolDetails>> {
1289
1347
  let cacheEntry = await loadReadUrlCacheEntry(session, params, signal, { preferCached: true });
1290
- const truncation = truncateHead(cacheEntry.output, {
1348
+ // `head` intentionally retains the historical whole-output accounting so the
1349
+ // existing URL golden remains byte-identical. Non-head directions account for
1350
+ // and truncate only the body; preamble bytes/chars are never part of that cap.
1351
+ const effectiveDirection =
1352
+ direction !== "head" && cacheEntry.wrappedPreambleChars === undefined ? "head" : direction;
1353
+ const isWrappedOutput = isWrappedReadUrlOutput(cacheEntry.output);
1354
+ const preambleChars = isWrappedOutput
1355
+ ? (cacheEntry.wrappedPreambleChars ??
1356
+ WRAP_PREFIX_CHARS + escapeUntrustedContent(buildReadUrlPreamble(cacheEntry.details)).length)
1357
+ : cacheEntry.preambleChars;
1358
+ const wrappedPrefix = isWrappedOutput ? WRAP_PREFIX_CHARS : 0;
1359
+ const wrappedSuffix = isWrappedOutput ? `\n${UNTRUSTED_CONTENT_CLOSE}` : "";
1360
+ const bodyEnd = cacheEntry.output.length - wrappedSuffix.length;
1361
+ const unwrappedOutput = isWrappedOutput
1362
+ ? cacheEntry.output.slice(wrappedPrefix, Math.max(wrappedPrefix, bodyEnd))
1363
+ : cacheEntry.output;
1364
+ const truncationInput =
1365
+ effectiveDirection === "head"
1366
+ ? unwrappedOutput
1367
+ : cacheEntry.output.slice(preambleChars, Math.max(preambleChars, bodyEnd));
1368
+ const truncation = truncateContent(truncationInput, {
1291
1369
  maxBytes: DEFAULT_MAX_BYTES,
1292
1370
  maxLines: FETCH_DEFAULT_MAX_LINES,
1371
+ direction: effectiveDirection,
1293
1372
  });
1294
1373
  const needsArtifact = truncation.truncated;
1295
1374
  if (needsArtifact && !cacheEntry.artifactId) {
1296
1375
  cacheEntry = await ensureReadUrlCacheArtifact(session, cacheEntry);
1297
1376
  cacheReadUrlEntry(session, params.path, params.raw ?? false, cacheEntry);
1298
1377
  }
1299
- const output = needsArtifact ? truncation.content : cacheEntry.output;
1378
+ const output =
1379
+ effectiveDirection !== "head"
1380
+ ? needsArtifact
1381
+ ? `${cacheEntry.output.slice(0, preambleChars)}${truncation.content}${wrappedSuffix}`
1382
+ : cacheEntry.output
1383
+ : truncation.content;
1300
1384
  const details: ReadUrlToolDetails = {
1301
1385
  ...cacheEntry.details,
1302
1386
  truncated: Boolean(cacheEntry.details.truncated || needsArtifact),
1303
1387
  };
1304
1388
 
1305
- const contentBlocks: Array<TextContent | ImageContent> = [{ type: "text", text: wrapUntrustedContent(output) }];
1389
+ const contentBlocks: Array<TextContent | ImageContent> = [
1390
+ { type: "text", text: isWrappedOutput && effectiveDirection !== "head" ? output : wrapUntrustedContent(output) },
1391
+ ];
1306
1392
  if (cacheEntry.image) {
1307
1393
  contentBlocks.push({ type: "image", data: cacheEntry.image.data, mimeType: cacheEntry.image.mimeType });
1308
1394
  }
1309
1395
 
1310
1396
  const resultBuilder = toolResult(details).content(contentBlocks).sourceUrl(details.finalUrl);
1311
1397
  if (needsArtifact) {
1312
- resultBuilder.truncation(truncation, { direction: "head", artifactId: cacheEntry.artifactId });
1398
+ resultBuilder.truncation(truncation, {
1399
+ direction: effectiveDirection === "both" ? "middle" : effectiveDirection === "last" ? "tail" : "head",
1400
+ artifactId: cacheEntry.artifactId,
1401
+ ...(effectiveDirection !== "head" ? { maxBytes: DEFAULT_MAX_BYTES } : {}),
1402
+ });
1313
1403
  } else if (cacheEntry.details.truncated) {
1314
- const outputLines = cacheEntry.output.split("\n").length;
1315
- const outputBytes = Buffer.byteLength(cacheEntry.output, "utf-8");
1404
+ const reportedText = effectiveDirection === "head" ? cacheEntry.output : truncationInput;
1405
+ const outputLines = reportedText.split("\n").length;
1406
+ const outputBytes = Buffer.byteLength(reportedText, "utf-8");
1316
1407
  const totalBytes = Math.max(outputBytes + 1, MAX_OUTPUT_CHARS + 1);
1317
1408
  const totalLines = outputLines + 1;
1318
- resultBuilder.truncationFromText(cacheEntry.output, {
1319
- direction: "tail",
1409
+ resultBuilder.truncationFromText(reportedText, {
1410
+ direction: effectiveDirection === "both" ? "middle" : effectiveDirection === "last" ? "tail" : "head",
1320
1411
  totalLines,
1321
1412
  totalBytes,
1322
1413
  maxBytes: MAX_OUTPUT_CHARS,