zidane 5.0.6 → 5.1.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 (74) hide show
  1. package/README.md +37 -1
  2. package/dist/{agent-JhicgLOV.d.ts → agent-B0vrSTQ9.d.ts} +162 -4
  3. package/dist/agent-B0vrSTQ9.d.ts.map +1 -0
  4. package/dist/chat.d.ts +526 -81
  5. package/dist/chat.d.ts.map +1 -1
  6. package/dist/chat.js +2 -2
  7. package/dist/contexts/docker.d.ts +7 -0
  8. package/dist/contexts/docker.d.ts.map +1 -0
  9. package/dist/{contexts-3Arvn7yR.js → contexts/docker.js} +2 -128
  10. package/dist/contexts/docker.js.map +1 -0
  11. package/dist/contexts-BwiHIr2w.js +129 -0
  12. package/dist/contexts-BwiHIr2w.js.map +1 -0
  13. package/dist/contexts.d.ts +3 -2
  14. package/dist/contexts.js +2 -2
  15. package/dist/index-CFxhms_B.d.ts +1303 -0
  16. package/dist/index-CFxhms_B.d.ts.map +1 -0
  17. package/dist/index-DYcymRtr.d.ts +26 -0
  18. package/dist/index-DYcymRtr.d.ts.map +1 -0
  19. package/dist/{index-t_W9i7Ql.d.ts → index-X6Q9PN_A.d.ts} +3 -3
  20. package/dist/{index-t_W9i7Ql.d.ts.map → index-X6Q9PN_A.d.ts.map} +1 -1
  21. package/dist/index.d.ts +6 -5
  22. package/dist/index.js +10 -9
  23. package/dist/index.js.map +1 -1
  24. package/dist/login-BiuHyuEh.js +1276 -0
  25. package/dist/login-BiuHyuEh.js.map +1 -0
  26. package/dist/{mcp-Dw-fRPVk.js → mcp-BgwK6ySj.js} +184 -9
  27. package/dist/mcp-BgwK6ySj.js.map +1 -0
  28. package/dist/mcp.d.ts +2 -2
  29. package/dist/mcp.js +1 -1
  30. package/dist/{messages-xaYMMFlb.js → messages-BAFLvH_z.js} +1 -1
  31. package/dist/{messages-xaYMMFlb.js.map → messages-BAFLvH_z.js.map} +1 -1
  32. package/dist/{presets-BRFH2qsQ.js → presets-CI8_fyvX.js} +2 -2
  33. package/dist/{presets-BRFH2qsQ.js.map → presets-CI8_fyvX.js.map} +1 -1
  34. package/dist/presets.d.ts +2 -2
  35. package/dist/presets.js +1 -1
  36. package/dist/{providers-BCbdv99U.js → providers-C6-vhaVu.js} +2 -2
  37. package/dist/{providers-BCbdv99U.js.map → providers-C6-vhaVu.js.map} +1 -1
  38. package/dist/providers.d.ts +1 -1
  39. package/dist/providers.js +2 -2
  40. package/dist/session/sqlite.d.ts +1 -1
  41. package/dist/{session-791hhrFa.js → session-pS4Vt4dl.js} +1 -1
  42. package/dist/{session-791hhrFa.js.map → session-pS4Vt4dl.js.map} +1 -1
  43. package/dist/session.d.ts +1 -1
  44. package/dist/session.js +2 -2
  45. package/dist/skills.d.ts +2 -2
  46. package/dist/{stats-DZIsGqzu.js → stats-DvCtBRwK.js} +1 -1
  47. package/dist/{stats-DZIsGqzu.js.map → stats-DvCtBRwK.js.map} +1 -1
  48. package/dist/{theme-C3JHZ5y9.d.ts → theme-CcGLMJrn.d.ts} +732 -20
  49. package/dist/theme-CcGLMJrn.d.ts.map +1 -0
  50. package/dist/{tools-CLazLRb4.js → tools-d1yeA6xK.js} +399 -13
  51. package/dist/tools-d1yeA6xK.js.map +1 -0
  52. package/dist/tools.d.ts +2 -2
  53. package/dist/tools.js +1 -1
  54. package/dist/tui.d.ts +421 -39
  55. package/dist/tui.d.ts.map +1 -1
  56. package/dist/tui.js +5755 -2367
  57. package/dist/tui.js.map +1 -1
  58. package/dist/{turn-operations-BfEh-GER.js → turn-operations-BzOIM6Of.js} +1984 -108
  59. package/dist/turn-operations-BzOIM6Of.js.map +1 -0
  60. package/dist/types-Bx_F8jet.js.map +1 -1
  61. package/dist/{index-CXVvqTQj.d.ts → types-OtrV6LJT.d.ts} +2 -27
  62. package/dist/types-OtrV6LJT.d.ts.map +1 -0
  63. package/dist/types.d.ts +4 -3
  64. package/dist/types.js +1 -1
  65. package/package.json +5 -1
  66. package/dist/agent-JhicgLOV.d.ts.map +0 -1
  67. package/dist/contexts-3Arvn7yR.js.map +0 -1
  68. package/dist/index-2yLUyTbc.d.ts +0 -430
  69. package/dist/index-2yLUyTbc.d.ts.map +0 -1
  70. package/dist/index-CXVvqTQj.d.ts.map +0 -1
  71. package/dist/mcp-Dw-fRPVk.js.map +0 -1
  72. package/dist/theme-C3JHZ5y9.d.ts.map +0 -1
  73. package/dist/tools-CLazLRb4.js.map +0 -1
  74. package/dist/turn-operations-BfEh-GER.js.map +0 -1
@@ -0,0 +1,1276 @@
1
+ import { C as getReadState } from "./tools-d1yeA6xK.js";
2
+ import { t as toolOutputByteLength } from "./types-Bx_F8jet.js";
3
+ import { a as createTolerantClient, o as sseToJsonFetchIfNeeded, s as McpOAuthProvider } from "./mcp-BgwK6ySj.js";
4
+ import { createServer } from "node:http";
5
+ //#region src/compact/errors.ts
6
+ /**
7
+ * Typed errors thrown by the compaction helper.
8
+ *
9
+ * Lives in its own file so both the runner and the pure messages module
10
+ * can import without circular dependencies.
11
+ */
12
+ /**
13
+ * Raised when the caller's inputs make compaction meaningless before any
14
+ * API call is attempted. Common cases:
15
+ * - empty `turns`
16
+ * - `keepTurns >= turns.length` (no older content to summarize)
17
+ * - `'from'` / `'up_to'` anchor id not found in `turns`
18
+ * - the resolved `toSummarize` slice has no text-bearing content
19
+ *
20
+ * Synchronous — thrown from `compactConversation()` before the provider
21
+ * call so the caller can recover without a network round-trip.
22
+ */
23
+ var CompactInvalidInputError = class extends Error {
24
+ constructor(message) {
25
+ super(message);
26
+ this.name = "CompactInvalidInputError";
27
+ }
28
+ };
29
+ /**
30
+ * Raised when the provider rejects the compaction request with
31
+ * `prompt_too_long` (or an equivalent) and the head-truncation retry
32
+ * budget has been exhausted. Callers can inspect `ptlRetries` to log
33
+ * how far the retry loop got before giving up.
34
+ */
35
+ var CompactPromptTooLongError = class extends Error {
36
+ ptlRetries;
37
+ constructor(message, ptlRetries) {
38
+ super(message);
39
+ this.ptlRetries = ptlRetries;
40
+ this.name = "CompactPromptTooLongError";
41
+ }
42
+ };
43
+ //#endregion
44
+ //#region src/compact/messages.ts
45
+ /**
46
+ * Partition `turns` into `(toSummarize, preserved)` according to `scope`.
47
+ *
48
+ * Throws {@link CompactInvalidInputError} on degenerate inputs so the
49
+ * caller doesn't pay for a doomed provider call:
50
+ * - empty `turns`
51
+ * - `scope: 'tail'` with `keepTurns >= turns.length` (nothing to summarize)
52
+ * - `scope: { from | up_to }` with an anchor id that isn't in `turns`
53
+ * - the resulting `toSummarize` slice contains no text-bearing content
54
+ * (only system turns or empty content)
55
+ *
56
+ * Pure. Returns references to the original `SessionTurn` objects — the
57
+ * caller can compare by identity (=== Object.is) to confirm.
58
+ */
59
+ function sliceForCompaction(turns, scope, keepTurns) {
60
+ if (turns.length === 0) throw new CompactInvalidInputError("No turns to compact.");
61
+ let toSummarize;
62
+ let preserved;
63
+ if (scope === "full") {
64
+ toSummarize = turns;
65
+ preserved = [];
66
+ } else if (scope === "tail") {
67
+ const keep = Math.max(0, keepTurns);
68
+ if (keep >= turns.length) throw new CompactInvalidInputError(`Nothing to compact: keepTurns (${keep}) covers the entire conversation (${turns.length} turns).`);
69
+ const safeCut = findSafeRoundBoundary(turns, turns.length - keep);
70
+ toSummarize = turns.slice(0, safeCut);
71
+ preserved = turns.slice(safeCut);
72
+ } else if (scope.kind === "from") {
73
+ const idx = turns.findIndex((t) => t.id === scope.turnId);
74
+ if (idx < 0) throw new CompactInvalidInputError(`Anchor turn not found: "${scope.turnId}".`);
75
+ const safeIdx = findSafeRoundBoundary(turns, idx);
76
+ preserved = turns.slice(0, safeIdx);
77
+ toSummarize = turns.slice(safeIdx);
78
+ } else {
79
+ const idx = turns.findIndex((t) => t.id === scope.turnId);
80
+ if (idx < 0) throw new CompactInvalidInputError(`Anchor turn not found: "${scope.turnId}".`);
81
+ const safeCut = findSafeRoundBoundary(turns, idx + 1);
82
+ toSummarize = turns.slice(0, safeCut);
83
+ preserved = turns.slice(safeCut);
84
+ }
85
+ if (toSummarize.length === 0) throw new CompactInvalidInputError("Compaction scope resolved to zero turns.");
86
+ if (!hasTextBearingContent(toSummarize)) throw new CompactInvalidInputError("Compaction scope contains no text-bearing turns to summarize.");
87
+ return {
88
+ toSummarize,
89
+ preserved
90
+ };
91
+ }
92
+ /**
93
+ * Replace every image block in `turns` with a `[image]` text marker.
94
+ *
95
+ * Covers two shapes:
96
+ * - Top-level `{ type: 'image', ... }` content blocks on user turns.
97
+ * - Image entries inside `tool_result.output` array form (multimodal
98
+ * tool results — e.g. an MCP browser screenshot).
99
+ *
100
+ * Unconditional by design: even on vision-capable models, the summary
101
+ * call doesn't benefit from raw image bytes (the model can't refer to
102
+ * them after the summary lands), and stripping uniformly avoids
103
+ * `prompt_too_long` on image-heavy sessions.
104
+ *
105
+ * Returns a fresh array; input turns / blocks are never mutated.
106
+ */
107
+ function stripImagesFromTurns(turns) {
108
+ return turns.map((turn) => stripImagesFromTurn(turn));
109
+ }
110
+ function stripImagesFromTurn(turn) {
111
+ let touched = false;
112
+ const nextContent = [];
113
+ for (const block of turn.content) {
114
+ if (block.type === "image") {
115
+ touched = true;
116
+ nextContent.push({
117
+ type: "text",
118
+ text: "[image]"
119
+ });
120
+ continue;
121
+ }
122
+ if (block.type === "tool_result" && Array.isArray(block.output)) {
123
+ const flat = stripImagesFromToolResult(block.output);
124
+ if (flat) {
125
+ touched = true;
126
+ nextContent.push({
127
+ ...block,
128
+ output: flat
129
+ });
130
+ continue;
131
+ }
132
+ }
133
+ nextContent.push(block);
134
+ }
135
+ return touched ? {
136
+ ...turn,
137
+ content: nextContent
138
+ } : turn;
139
+ }
140
+ /**
141
+ * Return a fresh `ToolResultContent[]` with images flattened to `[image]`
142
+ * text placeholders, or `null` when no image blocks were present (caller
143
+ * keeps the original input). Returning a new mutable array — never the
144
+ * input — keeps the `tool_result.output` slot's mutable type contract
145
+ * satisfied without leaky `as`-casts at the call site.
146
+ */
147
+ function stripImagesFromToolResult(parts) {
148
+ let touched = false;
149
+ const out = [];
150
+ for (const part of parts) if (part.type === "image") {
151
+ touched = true;
152
+ out.push({
153
+ type: "text",
154
+ text: "[image]"
155
+ });
156
+ } else out.push(part);
157
+ return touched ? out : null;
158
+ }
159
+ /**
160
+ * Drop the oldest "round" from `turns` and return a fresh array. Used by
161
+ * the PTL retry path to shrink the prompt one round at a time.
162
+ *
163
+ * A round is a contiguous `[user, assistant?, tool_results?]` group. The
164
+ * function walks forward from index 0, advances through the user turn
165
+ * and any trailing assistant + tool-result turns belonging to the same
166
+ * exchange, and returns the remainder.
167
+ *
168
+ * Adjacency-safe: when the oldest user turn carries `tool_result` blocks
169
+ * answering an assistant turn ahead of it (rare — happens during
170
+ * resume), the function keeps walking until the next clean boundary so
171
+ * the resulting array still respects every provider's `tool_use ↔
172
+ * tool_result` adjacency rule.
173
+ *
174
+ * Returns `turns` unchanged when only one round (or less) remains — the
175
+ * caller is expected to interpret that as "cannot shrink further" and
176
+ * give up the retry loop.
177
+ */
178
+ function truncateHeadForPtlRetry(turns) {
179
+ if (turns.length <= 1) return turns.slice();
180
+ const firstUserIdx = turns.findIndex((t) => t.role === "user");
181
+ if (firstUserIdx < 0) return turns.slice();
182
+ let cursor = firstUserIdx + 1;
183
+ while (cursor < turns.length) {
184
+ const turn = turns[cursor];
185
+ if (turn.role === "assistant") {
186
+ cursor++;
187
+ continue;
188
+ }
189
+ if (turn.role === "user" && isToolResultsOnlyTurn(turn)) {
190
+ cursor++;
191
+ continue;
192
+ }
193
+ break;
194
+ }
195
+ if (cursor >= turns.length) return turns.slice();
196
+ return turns.slice(cursor);
197
+ }
198
+ function isToolResultsOnlyTurn(turn) {
199
+ if (turn.content.length === 0) return false;
200
+ return turn.content.every((block) => block.type === "tool_result");
201
+ }
202
+ /**
203
+ * Walk `proposedCut` backward to the nearest position where splitting at
204
+ * that index produces a round-boundary-clean partition — i.e. neither
205
+ * half breaks the `tool_use ↔ tool_result` adjacency rule that every
206
+ * provider (most strictly Anthropic) enforces.
207
+ *
208
+ * The hazardous case: the proposed cut lands BETWEEN an assistant turn
209
+ * carrying `tool_call` blocks and the user turn carrying the matching
210
+ * `tool_result` blocks. Then `toSummarize` ends with an orphan
211
+ * `tool_use` (provider 400 on the summarization request) AND `preserved`
212
+ * starts with an orphan `tool_result` (provider 400 on the next live
213
+ * agent run against the wire-level cutoff output).
214
+ *
215
+ * Algorithm: keep walking `cut` backward as long as `turns[cut - 1]`
216
+ * is an assistant turn with at least one `tool_call` block. The walk
217
+ * stops when:
218
+ * - we reach `cut = 0` (slice would be empty; caller's existing
219
+ * "scope resolved to zero turns" guard handles it), or
220
+ * - the trailing turn is user-role (clean — model emits no pending
221
+ * tool_use from user turns), or
222
+ * - the trailing turn is assistant text without tool_use (clean —
223
+ * text-only response is a complete round).
224
+ *
225
+ * Returning the adjusted cut over-preserves the tail relative to the
226
+ * caller's request — `keepTurns` is interpreted as the MINIMUM number
227
+ * of turns kept verbatim, not the exact count.
228
+ */
229
+ function findSafeRoundBoundary(turns, proposedCut) {
230
+ let cut = Math.max(0, Math.min(turns.length, proposedCut));
231
+ while (cut > 0 && hasPendingToolUse(turns[cut - 1])) cut--;
232
+ return cut;
233
+ }
234
+ /** Does this turn end with any unanswered `tool_use` blocks? */
235
+ function hasPendingToolUse(turn) {
236
+ if (turn.role !== "assistant") return false;
237
+ for (const block of turn.content) if (block.type === "tool_call") return true;
238
+ return false;
239
+ }
240
+ function hasTextBearingContent(turns) {
241
+ for (const turn of turns) {
242
+ if (turn.role === "system") continue;
243
+ for (const block of turn.content) {
244
+ if (block.type === "text" && block.text.trim().length > 0) return true;
245
+ if (block.type === "tool_call" || block.type === "tool_result") return true;
246
+ }
247
+ }
248
+ return false;
249
+ }
250
+ /**
251
+ * Maximum length of an anchor turn's textual preview, in characters. Long
252
+ * enough to give the model recognizable context (the first paragraph of
253
+ * a typical user message), short enough that it doesn't blow the
254
+ * cache-stability invariant for the prompt prefix.
255
+ */
256
+ const ANCHOR_PREVIEW_MAX_CHARS = 200;
257
+ /**
258
+ * Extract the first ~200 chars of text-bearing content from a turn — the
259
+ * preview surfaced in `from` / `up_to` direction prompts so the model
260
+ * knows where the slice begins.
261
+ */
262
+ function anchorPreviewFor(turn) {
263
+ for (const block of turn.content) if (block.type === "text" && block.text.trim().length > 0) {
264
+ const flat = block.text.replace(/\s+/g, " ").trim();
265
+ return flat.length > 200 ? `${flat.slice(0, 199)}…` : flat;
266
+ }
267
+ return "(no preview available)";
268
+ }
269
+ /**
270
+ * Build a synthetic `SessionTurn` carrying a single `compact-summary`
271
+ * block, ready to append to a session.
272
+ *
273
+ * The turn's role is `'user'` so it sits at a conversational boundary
274
+ * the way the model expects. The caller is responsible for
275
+ * `session.appendTurns([turn])`. The id is freshly generated via
276
+ * `crypto.randomUUID()` so collisions are statistically impossible.
277
+ *
278
+ * Typical use after running `compactConversation`:
279
+ *
280
+ * ```ts
281
+ * const result = await compactConversation({ provider, turns })
282
+ * const turn = summaryToTurn({
283
+ * summary: result.summary,
284
+ * replacesTurnIds: result.summarizedTurnIds,
285
+ * model: result.model,
286
+ * usage: result.usage,
287
+ * })
288
+ * await session.appendTurns([turn])
289
+ * ```
290
+ */
291
+ function summaryToTurn(input) {
292
+ const compactedAt = input.compactedAt ?? Date.now();
293
+ return {
294
+ id: crypto.randomUUID(),
295
+ role: "user",
296
+ content: [{
297
+ type: "compact-summary",
298
+ replacesTurnIds: input.replacesTurnIds,
299
+ summary: input.summary,
300
+ model: input.model,
301
+ usage: input.usage,
302
+ compactedAt
303
+ }],
304
+ createdAt: compactedAt
305
+ };
306
+ }
307
+ //#endregion
308
+ //#region src/compact/prompt.ts
309
+ /**
310
+ * No-tools guard. The runner sends `tools: []` to the provider already,
311
+ * but some models still hallucinate tool-call intent on a long
312
+ * conversation. The prose guard is cheap insurance.
313
+ */
314
+ const NO_TOOLS_PREAMBLE = `CRITICAL: Respond with TEXT ONLY. Do NOT call any tools.
315
+
316
+ - Do NOT use Read, Bash, Grep, Glob, Edit, Write, or ANY other tool.
317
+ - You already have all the context you need in the conversation above.
318
+ - Tool calls will be REJECTED and will waste your only turn — you will fail the task.
319
+ - Your entire response must be plain text: an <analysis> block followed by a <summary> block.`;
320
+ /**
321
+ * Body shared by every direction. Lays out the 9-section scaffold,
322
+ * mirrors Claude Code's `BASE_COMPACT_PROMPT` so a model already trained
323
+ * on the layout produces the same shape.
324
+ */
325
+ const BASE_INSTRUCTIONS = `Your task is to create a detailed summary of the conversation so far, paying close attention to the user's explicit requests and your previous actions.
326
+
327
+ This summary should be thorough in capturing technical details, code patterns, and architectural decisions that would be essential for continuing development work without losing context.
328
+
329
+ Before providing your final summary, wrap your analysis in <analysis> tags to organize your thoughts and ensure you've covered all necessary points. In your analysis process:
330
+
331
+ 1. Chronologically analyze each message and section of the conversation. For each section thoroughly identify:
332
+ - The user's explicit requests and intents
333
+ - Your approach to addressing the user's requests
334
+ - Key decisions, technical concepts and code patterns
335
+ - Specific details like file names, full code snippets, function signatures, file edits
336
+ - Errors that you ran into and how you fixed them
337
+ - Pay special attention to specific user feedback that you received, especially if the user told you to do something differently.
338
+ 2. Double-check for technical accuracy and completeness.
339
+
340
+ Your summary, wrapped in <summary> tags, must include the following sections:
341
+
342
+ 1. Primary Request and Intent
343
+ 2. Key Technical Concepts
344
+ 3. Files and Code Sections (with paths; include code snippets only when load-bearing)
345
+ 4. Errors and fixes
346
+ 5. Problem Solving
347
+ 6. All user messages (list ALL non-tool user messages, verbatim)
348
+ 7. Pending Tasks
349
+ 8. Current Work
350
+ 9. Optional Next Step (include direct quotes from the most recent conversation when relevant)`;
351
+ /** Trailer prompting the model to begin. Same on every direction. */
352
+ const TRAILER = "Provide your <analysis> and <summary> now.";
353
+ const FULL_BLURB = `## Scope
354
+ The conversation above is being summarized in full. Capture every section the user might need to resume from a fresh context.`;
355
+ const TAIL_BLURB = `## Scope
356
+ Summarize the conversation above. The most recent turns will be preserved verbatim alongside your summary, so prioritize older context that would otherwise be lost.`;
357
+ const FROM_BLURB = `## Scope
358
+ Summarize the conversation FROM the marked anchor onward (the recent portion). Everything before the anchor will be preserved verbatim.
359
+
360
+ Anchor turn (preview):
361
+ %ANCHOR_PREVIEW%`;
362
+ const UP_TO_BLURB = `## Scope
363
+ Summarize the conversation UP TO the marked anchor (the older portion). Everything from the anchor onward will be preserved verbatim — your summary's job is to compress the prior context the user can no longer scroll back to.
364
+
365
+ Anchor turn (preview):
366
+ %ANCHOR_PREVIEW%`;
367
+ /** Compose the full prompt with a custom direction-blurb. Internal helper. */
368
+ function compose(blurb) {
369
+ return [
370
+ NO_TOOLS_PREAMBLE,
371
+ BASE_INSTRUCTIONS,
372
+ blurb,
373
+ TRAILER
374
+ ].join("\n\n");
375
+ }
376
+ function buildFullCompactPrompt() {
377
+ return compose(FULL_BLURB);
378
+ }
379
+ function buildTailCompactPrompt() {
380
+ return compose(TAIL_BLURB);
381
+ }
382
+ function buildFromCompactPrompt(anchorPreview) {
383
+ return compose(FROM_BLURB.replace("%ANCHOR_PREVIEW%", anchorPreview));
384
+ }
385
+ function buildUpToCompactPrompt(anchorPreview) {
386
+ return compose(UP_TO_BLURB.replace("%ANCHOR_PREVIEW%", anchorPreview));
387
+ }
388
+ /**
389
+ * Default public builder. Dispatches by direction to the four named
390
+ * builders. Throws when `from` / `up_to` are passed without an
391
+ * `anchorPreview` — those scopes only make sense with an anchor and a
392
+ * silent fallback would produce a prompt that doesn't tell the model
393
+ * where the slice begins.
394
+ */
395
+ const buildCompactPrompt = (opts) => {
396
+ switch (opts.direction) {
397
+ case "full": return buildFullCompactPrompt();
398
+ case "tail": return buildTailCompactPrompt();
399
+ case "from": {
400
+ const preview = opts.anchorPreview ?? "";
401
+ if (preview.length === 0) throw new Error("buildCompactPrompt: `anchorPreview` is required for direction \"from\".");
402
+ return buildFromCompactPrompt(preview);
403
+ }
404
+ case "up_to": {
405
+ const preview = opts.anchorPreview ?? "";
406
+ if (preview.length === 0) throw new Error("buildCompactPrompt: `anchorPreview` is required for direction \"up_to\".");
407
+ return buildUpToCompactPrompt(preview);
408
+ }
409
+ }
410
+ };
411
+ //#endregion
412
+ //#region src/compact/utils.ts
413
+ /**
414
+ * Shared utilities for the compact module — extracted so the runner
415
+ * (`compact.ts`) and the restoration helper (`restore.ts`) don't carry
416
+ * redundant copies of the same low-level math.
417
+ *
418
+ * Kept zero-dependency by design: no Node/Bun imports, no zidane types
419
+ * either. Both `utf8ByteLength` and `estimateTokens` are total
420
+ * functions of a single `string` argument, so they're trivially
421
+ * portable to a worker/browser context if the compact module ever
422
+ * needs to render outside Node.
423
+ */
424
+ /**
425
+ * UTF-8 byte length, matching `Buffer.byteLength(text, 'utf-8')` but
426
+ * without pulling `node:buffer` for a one-liner. Stable for surrogate
427
+ * pairs — a high+low surrogate pair counts as a single 4-byte sequence
428
+ * (not 2 × 3-byte). Cheap to call in hot loops.
429
+ *
430
+ * Pure. Identical bytes for identical input across every JS runtime.
431
+ */
432
+ function utf8ByteLength(text) {
433
+ let bytes = 0;
434
+ for (let i = 0; i < text.length; i++) {
435
+ const code = text.charCodeAt(i);
436
+ if (code < 128) bytes += 1;
437
+ else if (code < 2048) bytes += 2;
438
+ else if (code >= 55296 && code <= 56319) {
439
+ bytes += 4;
440
+ i++;
441
+ } else bytes += 3;
442
+ }
443
+ return bytes;
444
+ }
445
+ /** Same constant Claude Code's `CONTEXT_MANAGEMENT.md` documents. */
446
+ const BYTES_PER_TOKEN = 4;
447
+ /**
448
+ * Approximate token count for `text`. Mirrors Claude Code's
449
+ * `BYTES_PER_TOKEN = 4` heuristic — acceptable for budget arithmetic
450
+ * (sizing summarization scopes, enforcing per-file caps in restoration).
451
+ *
452
+ * Accurate to ~10% on English + code. Use a real tokenizer when exact
453
+ * counts matter (cost reporting, hard caps); this is for budget gates
454
+ * where being off by 10% is fine.
455
+ *
456
+ * Pure. Exported so callers (TUI, SDK consumers) can do their own
457
+ * pre-budgeting against the same heuristic the harness uses internally.
458
+ */
459
+ function estimateTokens(text) {
460
+ return Math.ceil(text.length / 4);
461
+ }
462
+ //#endregion
463
+ //#region src/compact/compact.ts
464
+ /** Default `keepTurns` for `scope: 'tail'`. Matches `AgentBehavior.compactKeepTurns`. */
465
+ const DEFAULT_KEEP_TURNS = 4;
466
+ /** Default max output tokens for the summary call. */
467
+ const DEFAULT_MAX_OUTPUT_TOKENS = 2e4;
468
+ /** Default PTL retry budget. */
469
+ const DEFAULT_MAX_PTL_RETRIES = 3;
470
+ /** Maximum transient-error retries before giving up. Independent of PTL. */
471
+ const TRANSIENT_RETRY_BUDGET = 2;
472
+ async function compactConversation(opts) {
473
+ const slice = sliceForCompaction(opts.turns, opts.scope ?? "tail", opts.keepTurns ?? DEFAULT_KEEP_TURNS);
474
+ const direction = scopeToDirection(opts.scope ?? "tail");
475
+ const anchorPreview = direction === "from" || direction === "up_to" ? anchorPreviewFor(anchorTurnFor(slice, opts.scope)) : void 0;
476
+ const systemPrompt = (opts.prompt ?? buildCompactPrompt)({
477
+ direction,
478
+ ...anchorPreview !== void 0 ? { anchorPreview } : {}
479
+ });
480
+ const model = opts.model ?? opts.provider.meta.defaultModel;
481
+ const maxOutputTokens = opts.maxOutputTokens ?? DEFAULT_MAX_OUTPUT_TOKENS;
482
+ const maxPtlRetries = Math.max(0, opts.maxPtlRetries ?? DEFAULT_MAX_PTL_RETRIES);
483
+ let workingTurns = stripImagesFromTurns(slice.toSummarize);
484
+ let ptlRetries = 0;
485
+ let transientRetries = 0;
486
+ let attempt = 0;
487
+ while (true) {
488
+ attempt++;
489
+ const kind = ptlRetries > 0 ? "ptl-retry" : transientRetries > 0 ? "transient-retry" : "initial";
490
+ opts.onAttempt?.({
491
+ attempt,
492
+ kind
493
+ });
494
+ try {
495
+ const messages = ensureEndsWithUserMessage(turnsToMessages(workingTurns), opts.provider);
496
+ const { summary, usage } = await runOnce({
497
+ provider: opts.provider,
498
+ model,
499
+ system: systemPrompt,
500
+ messages,
501
+ maxTokens: maxOutputTokens,
502
+ ...opts.thinking !== void 0 ? { thinking: opts.thinking } : {},
503
+ ...opts.signal !== void 0 ? { signal: opts.signal } : {}
504
+ });
505
+ const workingIds = new Set(workingTurns.map((t) => t.id));
506
+ const droppedDueToPtl = [];
507
+ if (ptlRetries > 0) {
508
+ for (const t of slice.toSummarize) if (!workingIds.has(t.id)) droppedDueToPtl.push(t.id);
509
+ }
510
+ return {
511
+ summary,
512
+ usage: {
513
+ ...usage,
514
+ modelId: usage.modelId ?? model
515
+ },
516
+ model,
517
+ ptlRetries,
518
+ summarizedTurnIds: workingTurns.map((t) => t.id),
519
+ droppedDueToPtl,
520
+ preservedTurns: slice.preserved,
521
+ beforeBytes: bytesIn(workingTurns),
522
+ afterBytes: utf8ByteLength(summary)
523
+ };
524
+ } catch (err) {
525
+ if (isAbortError(err, opts.signal)) throw err;
526
+ if (isPromptTooLongError(err)) {
527
+ if (ptlRetries >= maxPtlRetries) throw new CompactPromptTooLongError(`Compaction failed: prompt_too_long after ${ptlRetries} retries.`, ptlRetries);
528
+ const truncated = truncateHeadForPtlRetry(workingTurns);
529
+ if (truncated.length === workingTurns.length) throw new CompactPromptTooLongError(`Compaction failed: prompt_too_long and conversation cannot be shrunk further.`, ptlRetries);
530
+ workingTurns = truncated;
531
+ ptlRetries++;
532
+ continue;
533
+ }
534
+ if (isTransientError(err) && transientRetries < TRANSIENT_RETRY_BUDGET) {
535
+ transientRetries++;
536
+ continue;
537
+ }
538
+ throw err;
539
+ }
540
+ }
541
+ }
542
+ /**
543
+ * Single provider call. Strips the `<analysis>` block and returns the
544
+ * post-trim summary text along with the usage report.
545
+ *
546
+ * Why not just use `result.text`: some providers stream deltas without
547
+ * emitting a concatenated `text` field at the end (or set it to an
548
+ * empty string when the stream finished via tool-call). Accumulating
549
+ * deltas in `text` and falling back to `result.text` mirrors
550
+ * `generateSessionTitle` and handles every adapter shape.
551
+ */
552
+ async function runOnce(opts) {
553
+ let streamed = "";
554
+ const result = await opts.provider.stream({
555
+ model: opts.model,
556
+ system: opts.system,
557
+ tools: [],
558
+ messages: opts.messages,
559
+ maxTokens: opts.maxTokens,
560
+ ...opts.thinking !== void 0 ? { thinking: opts.thinking } : {},
561
+ ...opts.signal !== void 0 ? { signal: opts.signal } : {}
562
+ }, { onText: (delta) => {
563
+ streamed += delta;
564
+ } });
565
+ let summary = stripAnalysisBlock(streamed.length > 0 ? streamed : result.text).trim();
566
+ if (summary.length === 0) {
567
+ const thinking = collectThinkingText(result.assistantMessage);
568
+ if (thinking.length > 0) summary = stripAnalysisBlock(thinking).trim();
569
+ }
570
+ if (summary.length === 0) throw new Error("Compaction failed: provider returned no summary text.");
571
+ return {
572
+ summary,
573
+ usage: result.usage
574
+ };
575
+ }
576
+ /**
577
+ * Concatenate every `thinking`-block's text from an assistant message.
578
+ *
579
+ * Reasoning models route their output through provider-specific
580
+ * "thinking" channels (Anthropic native thinking, OpenAI o-series
581
+ * reasoning, OpenAI-compat `reasoning_content`, OpenRouter
582
+ * `reasoning_details`). All of them surface as `{ type: 'thinking', text }`
583
+ * blocks on the normalized `SessionMessage.content` by the time the
584
+ * provider's `stream()` returns. Walking the blocks is provider-agnostic
585
+ * — works for every adapter without bespoke per-provider handling.
586
+ *
587
+ * Returns the empty string when no thinking blocks are present, so the
588
+ * caller's "did we get anything?" check stays a single `.length === 0`.
589
+ */
590
+ function collectThinkingText(message) {
591
+ const parts = [];
592
+ for (const block of message.content) if (block.type === "thinking" && typeof block.text === "string" && block.text.length > 0) parts.push(block.text);
593
+ return parts.join("\n");
594
+ }
595
+ /**
596
+ * Extract the summary text from the provider's response, peeling off
597
+ * any envelope the model wrapped it in.
598
+ *
599
+ * The compact prompt asks for `<analysis>...</analysis><summary>...</summary>`
600
+ * but real models drift from the format. This function tries four
601
+ * extraction paths in order of strictness:
602
+ *
603
+ * 1. **Strict path** — strip `<analysis>...</analysis>` blocks and
604
+ * extract the `<summary>...</summary>` envelope. Matches the
605
+ * prompt-following ideal.
606
+ * 2. **Loose path** — same strip, but accept whatever's outside the
607
+ * `<analysis>` tags as the summary (no `<summary>` envelope
608
+ * required). Handles models that drop the wrapper but keep the
609
+ * analysis.
610
+ * 3. **Analysis-as-summary fallback** — when the entire response is a
611
+ * single `<analysis>` block (model conflated the two concepts),
612
+ * return the analysis content. Better than failing the compaction
613
+ * and forcing the user to retry.
614
+ * 4. **Raw passthrough** — no recognized envelope. Return the text
615
+ * as-is and let `runOnce`'s empty-check decide whether to throw.
616
+ *
617
+ * Matches Claude Code's `formatCompactSummary` for path (1) + (2) and
618
+ * adds (3) as a graceful-degradation layer we ran into in the wild
619
+ * (smaller / non-Anthropic models sometimes produce analysis-only).
620
+ */
621
+ function stripAnalysisBlock(text) {
622
+ const analysisStripped = text.replace(/<analysis>[\s\S]*?<\/analysis>/g, "");
623
+ const summaryMatch = analysisStripped.match(/<summary>([\s\S]*?)<\/summary>/);
624
+ if (summaryMatch) return summaryMatch[1];
625
+ if (analysisStripped.trim().length > 0) return analysisStripped;
626
+ const analysisMatch = text.match(/<analysis>([\s\S]*?)<\/analysis>/);
627
+ if (analysisMatch) return analysisMatch[1];
628
+ return text;
629
+ }
630
+ /**
631
+ * Convert turns into the wire-level `SessionMessage[]` shape. Drops
632
+ * system turns (rare; they'd confuse a summary call), and inlines any
633
+ * pre-existing `compact-summary` markers as plain text so the
634
+ * provider's wire converter doesn't have to know about zidane's
635
+ * internal block type.
636
+ *
637
+ * Inlining (instead of dropping) is intentional: a session already
638
+ * compacted once still contains the prior summary as load-bearing
639
+ * context. Surfacing it as `[Previous compaction summary]\n…` lets the
640
+ * new summarization integrate it instead of forgetting it.
641
+ */
642
+ function turnsToMessages(turns) {
643
+ const out = [];
644
+ for (const turn of turns) {
645
+ if (turn.role === "system") continue;
646
+ const content = [];
647
+ for (const block of turn.content) {
648
+ if (block.type === "compact-summary") {
649
+ content.push({
650
+ type: "text",
651
+ text: `[Previous compaction summary]\n${block.summary}`
652
+ });
653
+ continue;
654
+ }
655
+ content.push(block);
656
+ }
657
+ if (content.length === 0) continue;
658
+ out.push({
659
+ role: turn.role,
660
+ content
661
+ });
662
+ }
663
+ return out;
664
+ }
665
+ /**
666
+ * Directive appended as the trailing user message when the conversation
667
+ * to summarize ends with an assistant turn. Mirrors the system-prompt
668
+ * TRAILER text so the model gets the same imperative cue from both
669
+ * surfaces — system instruction + final user message.
670
+ */
671
+ const SUMMARY_USER_DIRECTIVE = "Provide your <analysis> and <summary> now.";
672
+ /**
673
+ * Guarantee the message list ends with a `role: 'user'` message.
674
+ *
675
+ * Why: Anthropic's API (and several OpenAI-compat reasoning models)
676
+ * rejects requests whose final message is `role: 'assistant'` with
677
+ * "This model does not support assistant message prefill. The
678
+ * conversation must end with a user message." Compaction is the
679
+ * common trigger — it runs against the session's existing history,
680
+ * which most often ends with an assistant turn (the agent just
681
+ * responded). Auto-compact fires immediately after `agent.run()`
682
+ * returns, which makes the assistant-tail case the rule, not the
683
+ * exception.
684
+ *
685
+ * Empty input passes through unchanged so the surrounding code can
686
+ * keep its own "nothing to summarize" guard upstream; we don't
687
+ * synthesize a user message into thin air. Otherwise: append a user
688
+ * message containing the same directive the system prompt's TRAILER
689
+ * carries, constructed via `provider.userMessage()` so each provider's
690
+ * native message shape is honored.
691
+ */
692
+ function ensureEndsWithUserMessage(messages, provider) {
693
+ if (messages.length === 0) return messages;
694
+ if (messages[messages.length - 1].role === "user") return messages;
695
+ return [...messages, provider.userMessage(SUMMARY_USER_DIRECTIVE)];
696
+ }
697
+ function scopeToDirection(scope) {
698
+ if (scope === "full" || scope === "tail") return scope;
699
+ return scope.kind;
700
+ }
701
+ function anchorTurnFor(slice, scope) {
702
+ if (typeof scope === "string") throw new Error("anchorTurnFor: scope must be object form");
703
+ if (scope.kind === "from") return slice.toSummarize[0];
704
+ return slice.toSummarize[slice.toSummarize.length - 1];
705
+ }
706
+ function bytesIn(turns) {
707
+ let total = 0;
708
+ for (const turn of turns) for (const block of turn.content) if (block.type === "text") total += utf8ByteLength(block.text);
709
+ else if (block.type === "tool_result") total += toolOutputByteLength(block.output);
710
+ else if (block.type === "tool_call") total += utf8ByteLength(JSON.stringify(block.input));
711
+ else if (block.type === "thinking") total += utf8ByteLength(block.text);
712
+ return total;
713
+ }
714
+ /**
715
+ * Provider-agnostic predicate for the "prompt is too long" rejection.
716
+ * Inspects error code, type, status, and message substring — every
717
+ * provider names this case differently but the message is recognizable.
718
+ */
719
+ function isPromptTooLongError(err) {
720
+ if (!err || typeof err !== "object") return false;
721
+ const e = err;
722
+ if (typeof e.code === "string" && /prompt[_ ]too[_ ]long/i.test(e.code)) return true;
723
+ if (typeof e.status === "number" && (e.status === 413 || e.status === 400)) {
724
+ const message = typeof e.message === "string" ? e.message : "";
725
+ if (/prompt[_ ]too[_ ]long|context[_ ]length|maximum[_ ]context|too many tokens/i.test(message)) return true;
726
+ }
727
+ if (typeof e.message === "string" && /prompt[_ ]too[_ ]long|context[_ ]length[_ ]exceeded|context window/i.test(e.message)) return true;
728
+ const nested = e.error ?? {};
729
+ if (typeof nested.type === "string" && nested.type === "invalid_request_error") {
730
+ const message = typeof nested.message === "string" ? nested.message : "";
731
+ if (/prompt[_ ]too[_ ]long|too long|context window/i.test(message)) return true;
732
+ }
733
+ return false;
734
+ }
735
+ /**
736
+ * Transient network / 5xx errors worth retrying once or twice.
737
+ */
738
+ function isTransientError(err) {
739
+ if (!err || typeof err !== "object") return false;
740
+ const e = err;
741
+ if (typeof e.status === "number" && e.status >= 500 && e.status < 600) return true;
742
+ if (typeof e.code === "string" && /ECONNRESET|ETIMEDOUT|ENETUNREACH|EAI_AGAIN|fetch failed/i.test(e.code)) return true;
743
+ if (typeof e.message === "string" && /socket hang up|fetch failed|network error|terminated|ECONNRESET|read ETIMEDOUT/i.test(e.message)) return true;
744
+ return false;
745
+ }
746
+ function isAbortError(err, signal) {
747
+ if (signal?.aborted) return true;
748
+ if (!err || typeof err !== "object") return false;
749
+ const e = err;
750
+ if (e.name === "AbortError") return true;
751
+ if (typeof e.message === "string" && /aborted/i.test(e.message)) return true;
752
+ return false;
753
+ }
754
+ //#endregion
755
+ //#region src/compact/restore.ts
756
+ const DEFAULT_FILE_TOKEN_BUDGET = 5e4;
757
+ const DEFAULT_FILE_TOKEN_PER_FILE_CAP = 5e3;
758
+ const DEFAULT_MAX_FILES_TO_RESTORE = 5;
759
+ const DEFAULT_SKILL_TOKEN_BUDGET = 25e3;
760
+ const DEFAULT_SKILL_TOKEN_PER_SKILL_CAP = 5e3;
761
+ /** Default canonical tool names — `rewriteMessagesToWire` handles aliasing. */
762
+ const DEFAULT_READ_FILE_TOOL_NAME = "read_file";
763
+ const DEFAULT_SKILLS_USE_TOOL_NAME = "skills_use";
764
+ /**
765
+ * Convert a raw read-state map into a deduped, path-ranked list ready for
766
+ * restoration. Multiple entries for the same path (different
767
+ * `(offset, limit, maxBytes)` slices) collapse to one — keeping the most
768
+ * recent `mtimeMs`.
769
+ *
770
+ * Filters out entries whose key doesn't share the given `cwd` prefix:
771
+ * those came from a different execution context and can't be read back
772
+ * through this agent's handle.
773
+ *
774
+ * Pure. Most callers want {@link selectFilesFromSession}, which wraps
775
+ * `getReadState(session)` + this function in one call so the host
776
+ * doesn't have to reach into the tools layer.
777
+ */
778
+ function selectFilesFromReadState(readState, cwd) {
779
+ const prefix = `${cwd}::`;
780
+ const byPath = /* @__PURE__ */ new Map();
781
+ for (const [key, entry] of readState) {
782
+ if (!key.startsWith(prefix)) continue;
783
+ const path = key.slice(prefix.length);
784
+ if (path.length === 0) continue;
785
+ const prior = byPath.get(path) ?? -Infinity;
786
+ if (entry.mtimeMs > prior) byPath.set(path, entry.mtimeMs);
787
+ }
788
+ return Array.from(byPath, ([path, mtimeMs]) => ({
789
+ path,
790
+ mtimeMs
791
+ })).sort((a, b) => b.mtimeMs - a.mtimeMs);
792
+ }
793
+ /**
794
+ * Session-aware convenience: extract recently-read files directly from
795
+ * a {@link Session} via its per-session read-state map.
796
+ *
797
+ * Hosts (TUI / SDK consumers) typically have a `Session` and a `cwd`
798
+ * (the active agent's `handle.cwd`) on hand — this wrapper saves them
799
+ * from reaching into `src/tools/read-state.ts` directly. Returns an
800
+ * empty list when no read state has been recorded yet (fresh session,
801
+ * or `behavior.dedupReads === false`).
802
+ *
803
+ * Equivalent to:
804
+ *
805
+ * ```ts
806
+ * const state = getReadState(session)
807
+ * return state ? selectFilesFromReadState(state, cwd) : []
808
+ * ```
809
+ */
810
+ function selectFilesFromSession(session, cwd) {
811
+ const state = getReadState(session);
812
+ return state ? selectFilesFromReadState(state, cwd) : [];
813
+ }
814
+ /**
815
+ * Pick the top `maxFiles` from `files` (descending by `mtimeMs`),
816
+ * dropping any whose path appears in `excludePaths`.
817
+ *
818
+ * Stable for equal mtimes — files with the same timestamp retain their
819
+ * input order. Pure.
820
+ */
821
+ function selectRecentFiles(files, opts) {
822
+ const excluded = new Set(opts.excludePaths ?? []);
823
+ return files.filter((f) => !excluded.has(f.path)).slice().sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, Math.max(0, opts.maxFiles));
824
+ }
825
+ /**
826
+ * Format a file's contents to match `read_file`'s output shape — 1-indexed
827
+ * line numbers separated by tabs, identical to what the model has seen
828
+ * from real `read_file` calls. Applies the per-file token cap by truncating
829
+ * at the nearest line boundary; appends a footer pointing at the next
830
+ * offset so the model can re-read the rest if needed.
831
+ */
832
+ function formatFileForRestoration(content, perFileTokenCap) {
833
+ const totalBytes = utf8ByteLength(content);
834
+ const allLines = content.split("\n");
835
+ const totalLines = allLines.length;
836
+ const numbered = [];
837
+ let runningChars = 0;
838
+ const charCap = Math.max(1, perFileTokenCap) * 4;
839
+ let truncatedAt = -1;
840
+ let midLineCut = false;
841
+ for (let i = 0; i < allLines.length; i++) {
842
+ const numberedLine = `${i + 1}\t${allLines[i]}`;
843
+ const lineCharCost = numberedLine.length + (i < allLines.length - 1 ? 1 : 0);
844
+ if (runningChars + lineCharCost > charCap) {
845
+ if (numbered.length === 0) {
846
+ const cutTo = Math.max(1, charCap - `${i + 1}\t`.length);
847
+ numbered.push(`${i + 1}\t${allLines[i].slice(0, cutTo)}`);
848
+ midLineCut = true;
849
+ }
850
+ truncatedAt = i;
851
+ break;
852
+ }
853
+ numbered.push(numberedLine);
854
+ runningChars += lineCharCost;
855
+ }
856
+ const body = numbered.join("\n");
857
+ if (truncatedAt < 0) return {
858
+ body,
859
+ truncated: false,
860
+ estimatedTokens: estimateTokens(body)
861
+ };
862
+ const truncated = body + `\n\n…truncated at ${midLineCut ? `line ${truncatedAt + 1} (mid-line)` : `line ${truncatedAt}`} (post-compact restoration cap: ${perFileTokenCap} tokens). File has ${totalLines} lines, ${totalBytes} bytes total — ${midLineCut ? `mid-line cut prevents a precise offset — re-read with offset=${truncatedAt + 1} and a larger maxBytes` : `re-read with offset=${truncatedAt + 1} to continue`}.`;
863
+ return {
864
+ body: truncated,
865
+ truncated: true,
866
+ estimatedTokens: estimateTokens(truncated)
867
+ };
868
+ }
869
+ /**
870
+ * Format a skill's instructions for restoration. Skills are plain markdown
871
+ * — no line numbering, no wrapping XML envelope (the model already
872
+ * understands the format from real `skills_use` calls). Truncation cuts
873
+ * at a line boundary when possible; appends a marker so the model knows
874
+ * the body is incomplete.
875
+ */
876
+ function formatSkillForRestoration(instructions, perSkillTokenCap) {
877
+ const charCap = Math.max(1, perSkillTokenCap) * 4;
878
+ if (instructions.length <= charCap) return {
879
+ body: instructions,
880
+ truncated: false,
881
+ estimatedTokens: estimateTokens(instructions)
882
+ };
883
+ const lastNewline = instructions.slice(0, charCap).lastIndexOf("\n");
884
+ const cutoff = lastNewline > 0 ? lastNewline : charCap;
885
+ const truncatedBody = `${instructions.slice(0, cutoff).trimEnd()}\n\n…[truncated post-compact at ${perSkillTokenCap} tokens; full skill body lives at the skill's location]`;
886
+ return {
887
+ body: truncatedBody,
888
+ truncated: true,
889
+ estimatedTokens: estimateTokens(truncatedBody)
890
+ };
891
+ }
892
+ /** UTF-8 byte length, matching `Buffer.byteLength(text, 'utf-8')`. */
893
+ /**
894
+ * Build the synthetic turns to append after a `compact-summary` marker.
895
+ *
896
+ * Returns a `PostCompactAttachments` envelope; the caller is responsible
897
+ * for `session.appendTurns([summaryTurn, ...result.turns])`. The two
898
+ * synthetic turns are always emitted together (or both omitted when
899
+ * nothing was restored) so the `tool_use ↔ tool_result` adjacency
900
+ * invariant holds regardless of caller code path.
901
+ *
902
+ * Failure isolation: a single file's read failure never blocks the rest
903
+ * of restoration — that file is skipped silently and processing
904
+ * continues. The returned `restoredFiles` count reflects what actually
905
+ * landed in the synthesized turns.
906
+ */
907
+ async function buildPostCompactAttachments(opts) {
908
+ const fileTokenBudget = opts.fileTokenBudget ?? DEFAULT_FILE_TOKEN_BUDGET;
909
+ const fileTokenPerFileCap = opts.fileTokenPerFileCap ?? DEFAULT_FILE_TOKEN_PER_FILE_CAP;
910
+ const maxFilesToRestore = opts.maxFilesToRestore ?? DEFAULT_MAX_FILES_TO_RESTORE;
911
+ const skillTokenBudget = opts.skillTokenBudget ?? DEFAULT_SKILL_TOKEN_BUDGET;
912
+ const skillTokenPerSkillCap = opts.skillTokenPerSkillCap ?? DEFAULT_SKILL_TOKEN_PER_SKILL_CAP;
913
+ const readFileToolName = opts.readFileToolName ?? DEFAULT_READ_FILE_TOOL_NAME;
914
+ const skillsUseToolName = opts.skillsUseToolName ?? DEFAULT_SKILLS_USE_TOOL_NAME;
915
+ const candidateFiles = opts.recentFiles && opts.recentFiles.length > 0 ? selectRecentFiles(opts.recentFiles, {
916
+ maxFiles: maxFilesToRestore,
917
+ ...opts.excludePaths ? { excludePaths: opts.excludePaths } : {}
918
+ }) : [];
919
+ const candidateSkills = opts.activeSkills ?? [];
920
+ const fileCalls = [];
921
+ let fileBudgetUsed = 0;
922
+ if (candidateFiles.length > 0 && opts.execution && opts.handle) for (let i = 0; i < candidateFiles.length; i++) {
923
+ const file = candidateFiles[i];
924
+ let content;
925
+ try {
926
+ content = await opts.execution.readFile(opts.handle, file.path);
927
+ } catch {
928
+ continue;
929
+ }
930
+ const formatted = formatFileForRestoration(content, fileTokenPerFileCap);
931
+ if (fileBudgetUsed + formatted.estimatedTokens > fileTokenBudget) break;
932
+ fileBudgetUsed += formatted.estimatedTokens;
933
+ fileCalls.push({
934
+ callId: `compact-restore-file-${i}`,
935
+ path: file.path,
936
+ body: formatted.body,
937
+ estimatedTokens: formatted.estimatedTokens
938
+ });
939
+ }
940
+ const skillCalls = [];
941
+ let skillBudgetUsed = 0;
942
+ for (let i = 0; i < candidateSkills.length; i++) {
943
+ const active = candidateSkills[i];
944
+ const instructions = active.skill.instructions ?? "";
945
+ if (instructions.trim().length === 0) continue;
946
+ const formatted = formatSkillForRestoration(instructions, skillTokenPerSkillCap);
947
+ if (skillBudgetUsed + formatted.estimatedTokens > skillTokenBudget) break;
948
+ skillBudgetUsed += formatted.estimatedTokens;
949
+ skillCalls.push({
950
+ callId: `compact-restore-skill-${i}`,
951
+ name: active.skill.name,
952
+ body: formatted.body,
953
+ estimatedTokens: formatted.estimatedTokens
954
+ });
955
+ }
956
+ if (fileCalls.length === 0 && skillCalls.length === 0) return {
957
+ turns: [],
958
+ restoredFiles: 0,
959
+ restoredSkills: 0,
960
+ estimatedTokens: 0
961
+ };
962
+ const assistantBlocks = [];
963
+ const userBlocks = [];
964
+ for (const fc of fileCalls) {
965
+ assistantBlocks.push({
966
+ type: "tool_call",
967
+ id: fc.callId,
968
+ name: readFileToolName,
969
+ input: { path: fc.path }
970
+ });
971
+ userBlocks.push({
972
+ type: "tool_result",
973
+ callId: fc.callId,
974
+ output: fc.body
975
+ });
976
+ }
977
+ for (const sc of skillCalls) {
978
+ assistantBlocks.push({
979
+ type: "tool_call",
980
+ id: sc.callId,
981
+ name: skillsUseToolName,
982
+ input: { name: sc.name }
983
+ });
984
+ userBlocks.push({
985
+ type: "tool_result",
986
+ callId: sc.callId,
987
+ output: sc.body
988
+ });
989
+ }
990
+ const now = Date.now();
991
+ const tag = opts.runId ? { runId: opts.runId } : {};
992
+ return {
993
+ turns: [{
994
+ id: crypto.randomUUID(),
995
+ role: "assistant",
996
+ content: assistantBlocks,
997
+ createdAt: now,
998
+ ...tag
999
+ }, {
1000
+ id: crypto.randomUUID(),
1001
+ role: "user",
1002
+ content: userBlocks,
1003
+ createdAt: now + 1,
1004
+ ...tag
1005
+ }],
1006
+ restoredFiles: fileCalls.length,
1007
+ restoredSkills: skillCalls.length,
1008
+ estimatedTokens: fileBudgetUsed + skillBudgetUsed
1009
+ };
1010
+ }
1011
+ //#endregion
1012
+ //#region src/mcp/oauth-callback.ts
1013
+ const DEFAULT_PATH = "/callback";
1014
+ const DEFAULT_HOST = "127.0.0.1";
1015
+ const SUCCESS_HTML = `<!doctype html>
1016
+ <html><head><meta charset="utf-8"><title>Logged in</title>
1017
+ <style>body{font:14px/1.5 -apple-system,system-ui,sans-serif;margin:6rem auto;max-width:28rem;text-align:center;color:#1d1d1f}.ok{color:#1f8a4c;font-weight:600}</style>
1018
+ </head><body>
1019
+ <p class="ok">Logged in.</p>
1020
+ <p>You can close this tab and return to the terminal.</p>
1021
+ </body></html>`;
1022
+ function errorHtml(message) {
1023
+ return `<!doctype html>
1024
+ <html><head><meta charset="utf-8"><title>Login failed</title>
1025
+ <style>body{font:14px/1.5 -apple-system,system-ui,sans-serif;margin:6rem auto;max-width:28rem;text-align:center;color:#1d1d1f}.err{color:#c43c2c;font-weight:600}</style>
1026
+ </head><body>
1027
+ <p class="err">Login failed.</p>
1028
+ <p>${message.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")}</p>
1029
+ <p>You can close this tab and return to the terminal.</p>
1030
+ </body></html>`;
1031
+ }
1032
+ /**
1033
+ * Start a one-shot OAuth callback server. The returned handle's `redirectUri`
1034
+ * should be passed to the authorization server as the `redirect_uri` query
1035
+ * parameter; `promise` resolves once the user finishes the browser flow.
1036
+ *
1037
+ * Always `await handle.close()` in a `finally` block — even on success, the
1038
+ * server stays open until told to shut down (so it can serve the
1039
+ * "you can close this tab" page).
1040
+ */
1041
+ async function startOAuthCallback(opts = {}) {
1042
+ const path = opts.path ?? DEFAULT_PATH;
1043
+ const host = opts.host ?? DEFAULT_HOST;
1044
+ const port = opts.port ?? 0;
1045
+ if (!path.startsWith("/")) throw new Error(`OAuth callback path must start with "/" (got: ${JSON.stringify(path)})`);
1046
+ if (opts.signal?.aborted) throw new Error("OAuth callback aborted");
1047
+ let resolveResult;
1048
+ let rejectResult;
1049
+ const promise = new Promise((resolve, reject) => {
1050
+ resolveResult = resolve;
1051
+ rejectResult = reject;
1052
+ });
1053
+ promise.catch(() => {});
1054
+ let settled = false;
1055
+ const resolveOnce = (value) => {
1056
+ if (settled) return;
1057
+ settled = true;
1058
+ resolveResult(value);
1059
+ };
1060
+ const rejectOnce = (error) => {
1061
+ if (settled) return;
1062
+ settled = true;
1063
+ rejectResult(error);
1064
+ };
1065
+ const server = createServer((req, res) => {
1066
+ const url = new URL(req.url ?? "/", `http://${host}`);
1067
+ if (url.pathname !== path) {
1068
+ res.writeHead(404, { "content-type": "text/plain" });
1069
+ res.end("Not Found");
1070
+ return;
1071
+ }
1072
+ const htmlHeaders = {
1073
+ "content-type": "text/html; charset=utf-8",
1074
+ "cache-control": "no-store"
1075
+ };
1076
+ const error = url.searchParams.get("error");
1077
+ if (error) {
1078
+ const desc = url.searchParams.get("error_description") ?? error;
1079
+ res.writeHead(400, htmlHeaders);
1080
+ res.end(errorHtml(desc));
1081
+ rejectOnce(/* @__PURE__ */ new Error(`OAuth authorization failed: ${desc}`));
1082
+ return;
1083
+ }
1084
+ const code = url.searchParams.get("code");
1085
+ if (!code) {
1086
+ res.writeHead(400, { "content-type": "text/plain" });
1087
+ res.end("Missing \"code\" query parameter.");
1088
+ return;
1089
+ }
1090
+ const state = url.searchParams.get("state") ?? void 0;
1091
+ res.writeHead(200, htmlHeaders);
1092
+ res.end(SUCCESS_HTML);
1093
+ resolveOnce({
1094
+ code,
1095
+ state
1096
+ });
1097
+ });
1098
+ server.on("error", (err) => {
1099
+ rejectOnce(err instanceof Error ? err : new Error(String(err)));
1100
+ });
1101
+ await new Promise((resolve, reject) => {
1102
+ server.once("error", reject);
1103
+ server.listen(port, host, () => {
1104
+ server.off("error", reject);
1105
+ resolve();
1106
+ });
1107
+ });
1108
+ const addr = server.address();
1109
+ if (!addr || typeof addr === "string") {
1110
+ server.close();
1111
+ throw new Error("OAuth callback server did not bind to a TCP port");
1112
+ }
1113
+ let closing;
1114
+ let closeServer;
1115
+ const onAbort = () => {
1116
+ rejectOnce(/* @__PURE__ */ new Error("OAuth callback aborted"));
1117
+ closeServer();
1118
+ };
1119
+ closeServer = () => {
1120
+ if (closing) return closing;
1121
+ closing = new Promise((resolve) => {
1122
+ server.closeAllConnections?.();
1123
+ server.close(() => {
1124
+ opts.signal?.removeEventListener("abort", onAbort);
1125
+ if (opts.signal?.aborted) rejectOnce(/* @__PURE__ */ new Error("OAuth callback aborted"));
1126
+ else rejectOnce(/* @__PURE__ */ new Error("OAuth callback server closed"));
1127
+ resolve();
1128
+ });
1129
+ });
1130
+ return closing;
1131
+ };
1132
+ opts.signal?.addEventListener("abort", onAbort, { once: true });
1133
+ return {
1134
+ redirectUri: `http://${host}:${addr.port}${path}`,
1135
+ promise,
1136
+ close: closeServer
1137
+ };
1138
+ }
1139
+ //#endregion
1140
+ //#region src/mcp/login.ts
1141
+ const DEFAULT_LOGIN_TIMEOUT_MS = 5 * 6e4;
1142
+ /**
1143
+ * Run the full interactive OAuth flow for `config`. Only supports `sse` and
1144
+ * `streamable-http` transports — `stdio` MCP servers don't speak OAuth.
1145
+ *
1146
+ * Throws on:
1147
+ * - Wrong transport.
1148
+ * - Abort signal.
1149
+ * - Browser-side error (user denied, server rejected, etc.).
1150
+ * - Code exchange failure.
1151
+ * - Post-exchange connect failure.
1152
+ *
1153
+ * Always closes the loopback callback server before returning, success or
1154
+ * failure.
1155
+ */
1156
+ async function loginMcpServer(config, options) {
1157
+ if (config.transport !== "sse" && config.transport !== "streamable-http") throw new Error(`MCP OAuth: cannot login for transport "${config.transport}" — only sse / streamable-http are supported`);
1158
+ if (!config.url) throw new Error(`MCP OAuth: server "${config.name}" is missing a url`);
1159
+ const hooks = options.hooks;
1160
+ let callback;
1161
+ try {
1162
+ callback = await startOAuthCallback({
1163
+ signal: options.signal,
1164
+ path: options.callbackPath
1165
+ });
1166
+ const handle = callback;
1167
+ const provider = new McpOAuthProvider({
1168
+ name: config.name,
1169
+ store: options.store,
1170
+ redirectUri: handle.redirectUri,
1171
+ clientName: options.clientName,
1172
+ scope: options.scope,
1173
+ onAuthorizationUrl: async (url) => {
1174
+ await hooks?.callHook("mcp:auth:url", {
1175
+ name: config.name,
1176
+ url: url.toString()
1177
+ });
1178
+ await options.onAuthorizationUrl?.(url);
1179
+ }
1180
+ });
1181
+ const transport = await createInteractiveTransport(config, provider);
1182
+ const client = await createTolerantClient({
1183
+ name: "zidane",
1184
+ version: "1.0.0"
1185
+ });
1186
+ let needsAuth = false;
1187
+ try {
1188
+ await client.connect(transport);
1189
+ } catch (err) {
1190
+ if (!isUnauthorizedError(err)) {
1191
+ await client.close().catch(() => {});
1192
+ throw err;
1193
+ }
1194
+ needsAuth = true;
1195
+ }
1196
+ if (needsAuth) {
1197
+ const { code } = await raceLoginCallback(handle, options.timeoutMs ?? DEFAULT_LOGIN_TIMEOUT_MS, options.signal);
1198
+ await transport.finishAuth(code);
1199
+ await transport.close().catch(() => {});
1200
+ const freshTransport = await createInteractiveTransport(config, provider);
1201
+ await client.connect(freshTransport);
1202
+ }
1203
+ const { tools } = await client.listTools();
1204
+ await client.close();
1205
+ const tokens = provider.tokens();
1206
+ if (!tokens) throw new Error(`MCP OAuth: login for "${config.name}" returned no tokens (server may have rejected the exchange silently)`);
1207
+ await hooks?.callHook("mcp:auth:success", { name: config.name });
1208
+ return {
1209
+ tokens,
1210
+ tools
1211
+ };
1212
+ } catch (err) {
1213
+ const error = err instanceof Error ? err : new Error(String(err));
1214
+ await hooks?.callHook("mcp:auth:error", {
1215
+ name: config.name,
1216
+ error
1217
+ });
1218
+ throw error;
1219
+ } finally {
1220
+ await callback?.close();
1221
+ }
1222
+ }
1223
+ /**
1224
+ * Build an `sse` / `streamable-http` transport pre-wired with `authProvider`.
1225
+ * Mirrors the bootstrap-side `createTransport` shape but inlined here so the
1226
+ * login flow doesn't depend on the bootstrap module's private export.
1227
+ */
1228
+ async function createInteractiveTransport(config, authProvider) {
1229
+ if (config.transport === "sse") {
1230
+ const { SSEClientTransport } = await import("@modelcontextprotocol/sdk/client/sse.js");
1231
+ return new SSEClientTransport(new URL(config.url), {
1232
+ requestInit: config.headers ? { headers: config.headers } : void 0,
1233
+ authProvider
1234
+ });
1235
+ }
1236
+ const { StreamableHTTPClientTransport } = await import("@modelcontextprotocol/sdk/client/streamableHttp.js");
1237
+ return new StreamableHTTPClientTransport(new URL(config.url), {
1238
+ requestInit: config.headers ? { headers: config.headers } : void 0,
1239
+ fetch: sseToJsonFetchIfNeeded(),
1240
+ authProvider
1241
+ });
1242
+ }
1243
+ function isUnauthorizedError(err) {
1244
+ if (!err || typeof err !== "object") return false;
1245
+ const e = err;
1246
+ if (e.name === "UnauthorizedError" || e.constructor?.name === "UnauthorizedError") return true;
1247
+ return typeof e.message === "string" && e.message.toLowerCase().startsWith("unauthorized");
1248
+ }
1249
+ /**
1250
+ * Wait on the callback handle, with a hard timeout AND honor the external
1251
+ * abort signal. Unlike `callback.promise` alone, this provides a deterministic
1252
+ * failure mode for "user opened the browser then walked away" — the modal
1253
+ * doesn't hang forever waiting for a redirect that may never come.
1254
+ */
1255
+ async function raceLoginCallback(handle, timeoutMs, signal) {
1256
+ if (signal?.aborted) throw new Error("OAuth login aborted");
1257
+ let timer;
1258
+ let onAbort;
1259
+ try {
1260
+ return await new Promise((resolve, reject) => {
1261
+ timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`OAuth login timed out after ${timeoutMs}ms`)), timeoutMs);
1262
+ if (signal) {
1263
+ onAbort = () => reject(/* @__PURE__ */ new Error("OAuth login aborted"));
1264
+ signal.addEventListener("abort", onAbort, { once: true });
1265
+ }
1266
+ handle.promise.then(resolve, reject);
1267
+ });
1268
+ } finally {
1269
+ if (timer) clearTimeout(timer);
1270
+ if (signal && onAbort) signal.removeEventListener("abort", onAbort);
1271
+ }
1272
+ }
1273
+ //#endregion
1274
+ export { summaryToTurn as C, CompactPromptTooLongError as E, stripImagesFromTurns as S, CompactInvalidInputError as T, buildTailCompactPrompt as _, selectFilesFromSession as a, anchorPreviewFor as b, BYTES_PER_TOKEN as c, BASE_INSTRUCTIONS as d, NO_TOOLS_PREAMBLE as f, buildFullCompactPrompt as g, buildFromCompactPrompt as h, selectFilesFromReadState as i, estimateTokens as l, buildCompactPrompt as m, startOAuthCallback as n, selectRecentFiles as o, TRAILER as p, buildPostCompactAttachments as r, compactConversation as s, loginMcpServer as t, utf8ByteLength as u, buildUpToCompactPrompt as v, truncateHeadForPtlRetry as w, sliceForCompaction as x, ANCHOR_PREVIEW_MAX_CHARS as y };
1275
+
1276
+ //# sourceMappingURL=login-BiuHyuEh.js.map