flavor-code 1.4.2 → 1.4.3

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 (81) hide show
  1. package/README.md +53 -1
  2. package/README.zh-CN.md +53 -1
  3. package/dist/agent/loop.d.ts +1 -0
  4. package/dist/{app-4CUS3GEJ.js → app-TVY2HM5L.js} +891 -604
  5. package/dist/apply-WOW3QU5I.js +51 -0
  6. package/dist/broker-cli-X5H46FZV.js +15 -0
  7. package/dist/check-CSKTV4O2.js +16 -0
  8. package/dist/chunk-3563SW4X.js +10513 -0
  9. package/dist/chunk-36IDJRLQ.js +630 -0
  10. package/dist/chunk-3UX2CGDW.js +1955 -0
  11. package/dist/chunk-4TMLXZVZ.js +75 -0
  12. package/dist/{chunk-7YFHRORG.js → chunk-5BDKRA6U.js} +412 -172
  13. package/dist/chunk-6F66UXBI.js +48 -0
  14. package/dist/{chunk-KMBDK4SN.js → chunk-6URQ7FIF.js} +24 -88
  15. package/dist/chunk-7Y64WNBB.js +8930 -0
  16. package/dist/chunk-AA4QNKTF.js +501 -0
  17. package/dist/chunk-BDX777JR.js +11 -0
  18. package/dist/chunk-CVP5OUHD.js +103 -0
  19. package/dist/chunk-EWD7FWTH.js +14547 -0
  20. package/dist/{chunk-N5MXHJKD.js → chunk-GTFP2R5I.js} +1 -1
  21. package/dist/{chunk-W2TIY6JY.js → chunk-IKGT4MDT.js} +2028 -516
  22. package/dist/chunk-IQEEFDEE.js +389 -0
  23. package/dist/chunk-IU3UBETC.js +30 -0
  24. package/dist/chunk-JQM2L2YM.js +1003 -0
  25. package/dist/chunk-KRWWCVXA.js +74 -0
  26. package/dist/chunk-OKHV7CTB.js +482 -0
  27. package/dist/chunk-ONRY3C2L.js +196 -0
  28. package/dist/chunk-PULWAH6E.js +474 -0
  29. package/dist/chunk-PZ5NKRVA.js +85 -0
  30. package/dist/chunk-QACFKPZP.js +43704 -0
  31. package/dist/chunk-RBAZLFKB.js +1022 -0
  32. package/dist/chunk-RV3EXUXM.js +30 -0
  33. package/dist/chunk-TIEOVQW6.js +61 -0
  34. package/dist/chunk-WLZUAVVU.js +15 -0
  35. package/dist/chunk-X442CJJH.js +114 -0
  36. package/dist/chunk-X6U6BO5D.js +2130 -0
  37. package/dist/{chunk-RAYH6K2H.js → chunk-Y7EH4NVO.js} +1 -1
  38. package/dist/{chunk-S32JRDZK.js → chunk-ZKKVSR37.js} +6 -1
  39. package/dist/{claude-ink-VQU3C5UL.js → claude-ink-F554I3GJ.js} +9 -4
  40. package/dist/cli-MAWCV2YR.js +163 -0
  41. package/dist/cli-main.js +3805 -473
  42. package/dist/cli.js +50 -4
  43. package/dist/config/load.d.ts +8 -0
  44. package/dist/config/protected-file.d.ts +2 -0
  45. package/dist/config/schema.d.ts +14 -0
  46. package/dist/context/global-instructions.d.ts +12 -0
  47. package/dist/context/manager.d.ts +4 -0
  48. package/dist/desktop/main.js +4943 -1562
  49. package/dist/desktop/preload.cjs +30 -0
  50. package/dist/desktop-renderer/assets/index-BWklSoOO.css +1 -0
  51. package/dist/desktop-renderer/assets/index-CjC0Ibiq.js +153 -0
  52. package/dist/desktop-renderer/assets/{interactive-terminal-ouQQ3brz.js → interactive-terminal-IjIT3Xoa.js} +1 -1
  53. package/dist/desktop-renderer/index.html +2 -2
  54. package/dist/doctor-SEL32R7M.js +20 -0
  55. package/dist/harness/local.d.ts +1 -0
  56. package/dist/load-RHWXNUCS.js +25 -0
  57. package/dist/log-ATFIPRNU.js +19 -0
  58. package/dist/manager-IYBNNKUP.js +191 -0
  59. package/dist/models/anthropic.d.ts +3 -0
  60. package/dist/node-LOIXT44J.js +879 -0
  61. package/dist/plugin-worker-entry.js +1 -1
  62. package/dist/production-TQROFEPV.js +46 -0
  63. package/dist/production.d.ts +28 -1
  64. package/dist/project-3QXE22TG.js +11 -0
  65. package/dist/react-M6JGXSJZ.js +7 -0
  66. package/dist/recorder-FAGJJH3N.js +11 -0
  67. package/dist/sdk/index.js +30 -8
  68. package/dist/server-2KLKPHPL.js +11 -0
  69. package/dist/session/store.d.ts +1 -0
  70. package/dist/store-MZZERW7K.js +18 -0
  71. package/dist/ui/commands.d.ts +11 -3
  72. package/dist/ui/session.d.ts +2 -1
  73. package/dist/usage-summary-G5FBOJLK.js +13 -0
  74. package/dist/write-stream-CGD57URC.js +77 -0
  75. package/package.json +1 -1
  76. package//346/212/200/346/234/257/346/226/271/346/241/210/346/212/245/345/221/212.md +649 -1
  77. package/dist/chunk-B23V72YR.js +0 -170
  78. package/dist/chunk-QKND3FX7.js +0 -26103
  79. package/dist/desktop-renderer/assets/index-Bk6FYhyh.css +0 -1
  80. package/dist/desktop-renderer/assets/index-CPXrzFgv.js +0 -154
  81. package/dist/load-UPOG3XQR.js +0 -19
@@ -0,0 +1,2130 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire as createFlavorRequire } from "node:module"; const require = createFlavorRequire(import.meta.url);
3
+ import {
4
+ message
5
+ } from "./chunk-BDX777JR.js";
6
+ import {
7
+ PermissionModeSchema
8
+ } from "./chunk-ONRY3C2L.js";
9
+ import {
10
+ external_exports
11
+ } from "./chunk-EWD7FWTH.js";
12
+
13
+ // src/session/store.ts
14
+ import { constants } from "fs";
15
+ import { lstat, mkdir, open, readFile, readdir, realpath, rename, rm, stat } from "fs/promises";
16
+ import { basename, dirname, join, relative, resolve, sep } from "path";
17
+ import { randomUUID } from "crypto";
18
+
19
+ // src/agent/planner.ts
20
+ var TaskNodeSchema = external_exports.object({
21
+ id: external_exports.string().trim().min(1),
22
+ description: external_exports.string().trim().min(1),
23
+ dependencies: external_exports.array(external_exports.string().trim().min(1)),
24
+ expectedOutputs: external_exports.array(external_exports.string().trim().min(1)),
25
+ verification: external_exports.array(external_exports.string().trim().min(1)),
26
+ files: external_exports.array(external_exports.string().trim().min(1)).describe(
27
+ "Workspace files this task owns for writing. Tasks whose files overlap are never run in parallel, so declare every file this task may create or modify."
28
+ ).optional()
29
+ }).strict();
30
+ var TaskGraphSchema = external_exports.object({
31
+ nodes: external_exports.array(TaskNodeSchema)
32
+ }).strict().superRefine((graph, context) => {
33
+ const ids = /* @__PURE__ */ new Set();
34
+ for (const [index, task] of graph.nodes.entries()) {
35
+ if (ids.has(task.id)) {
36
+ context.addIssue({ code: "custom", path: ["nodes", index, "id"], message: `Duplicate task id: ${task.id}` });
37
+ }
38
+ ids.add(task.id);
39
+ }
40
+ for (const [index, task] of graph.nodes.entries()) {
41
+ const dependencies = /* @__PURE__ */ new Set();
42
+ for (const dependency of task.dependencies) {
43
+ if (!ids.has(dependency)) {
44
+ context.addIssue({ code: "custom", path: ["nodes", index, "dependencies"], message: `Unknown dependency: ${dependency}` });
45
+ }
46
+ if (dependencies.has(dependency)) {
47
+ context.addIssue({ code: "custom", path: ["nodes", index, "dependencies"], message: `Duplicate dependency: ${dependency}` });
48
+ }
49
+ dependencies.add(dependency);
50
+ }
51
+ }
52
+ if (ids.size === graph.nodes.length && hasCycle(graph.nodes)) {
53
+ context.addIssue({ code: "custom", path: ["nodes"], message: "Task graph contains a cycle" });
54
+ }
55
+ });
56
+ var TaskPlanner = class {
57
+ #hooks;
58
+ constructor(options) {
59
+ this.#hooks = options.hooks;
60
+ }
61
+ async plan(input, signal) {
62
+ let outcome = "failed";
63
+ let graph;
64
+ let primaryError;
65
+ let hasPrimaryError = false;
66
+ try {
67
+ const before = await this.#hooks.emit({
68
+ version: 1,
69
+ type: "BeforePlan",
70
+ payload: { graph: input }
71
+ }, signal);
72
+ if (before.decision === "deny") throw new Error(before.reason ?? "Planning denied by hook");
73
+ signal?.throwIfAborted();
74
+ graph = TaskGraphSchema.parse(before.updatedInput === void 0 ? input : updatedGraph(before.updatedInput));
75
+ outcome = "completed";
76
+ } catch (error) {
77
+ outcome = signal?.aborted ? "cancelled" : "failed";
78
+ primaryError = error;
79
+ hasPrimaryError = true;
80
+ }
81
+ try {
82
+ await this.#hooks.emit({
83
+ version: 1,
84
+ type: "AfterPlan",
85
+ payload: { outcome }
86
+ });
87
+ } catch (afterPlanError) {
88
+ if (!hasPrimaryError) throw afterPlanError;
89
+ attachAfterPlanError(primaryError, afterPlanError);
90
+ }
91
+ if (hasPrimaryError) throw primaryError;
92
+ return graph;
93
+ }
94
+ };
95
+ function attachAfterPlanError(primary, afterPlanError) {
96
+ try {
97
+ if (typeof primary !== "object" && typeof primary !== "function" || primary === null || !Object.isExtensible(primary)) return;
98
+ Object.defineProperty(primary, "afterPlanError", {
99
+ value: afterPlanError,
100
+ configurable: true
101
+ });
102
+ if (!("cause" in primary)) {
103
+ Object.defineProperty(primary, "cause", {
104
+ value: afterPlanError,
105
+ configurable: true
106
+ });
107
+ }
108
+ } catch {
109
+ }
110
+ }
111
+ function updatedGraph(input) {
112
+ if (typeof input !== "object" || input === null || !("graph" in input)) {
113
+ throw new Error("BeforePlan updatedInput must contain graph");
114
+ }
115
+ return input.graph;
116
+ }
117
+ function hasCycle(nodes) {
118
+ const indegree = new Map(nodes.map((task) => [task.id, task.dependencies.length]));
119
+ const dependents = /* @__PURE__ */ new Map();
120
+ for (const task of nodes) {
121
+ for (const dependency of task.dependencies) {
122
+ const list = dependents.get(dependency) ?? [];
123
+ list.push(task.id);
124
+ dependents.set(dependency, list);
125
+ }
126
+ }
127
+ const ready = nodes.filter((task) => indegree.get(task.id) === 0).map((task) => task.id);
128
+ let visited = 0;
129
+ for (let cursor = 0; cursor < ready.length; cursor += 1) {
130
+ const id = ready[cursor];
131
+ visited += 1;
132
+ for (const dependent of dependents.get(id) ?? []) {
133
+ const next = (indegree.get(dependent) ?? 0) - 1;
134
+ indegree.set(dependent, next);
135
+ if (next === 0) ready.push(dependent);
136
+ }
137
+ }
138
+ return visited !== nodes.length;
139
+ }
140
+
141
+ // src/models/types.ts
142
+ function modelContentText(content) {
143
+ if (typeof content === "string") return content;
144
+ return content.filter((block) => block.type === "text").map((block) => block.text).join("\n");
145
+ }
146
+ function modelContentTranscriptText(content) {
147
+ if (typeof content === "string") return content;
148
+ let image = 0;
149
+ return content.map((block) => block.type === "text" ? block.text : `[Image #${++image}]`).filter(Boolean).join("\n");
150
+ }
151
+ function cloneModelContent(content) {
152
+ if (typeof content === "string") return content;
153
+ return content.map((block) => block.type === "text" ? { ...block } : { ...block, source: { ...block.source } });
154
+ }
155
+ function normalizeProviderError(error) {
156
+ const value = typeof error === "object" && error !== null ? error : { message: String(error) };
157
+ const status = typeof value.status === "number" ? value.status : void 0;
158
+ const providerCode = [value.code, value.type].filter((part) => typeof part === "string").join(" ").toLowerCase();
159
+ const message2 = typeof value.message === "string" ? value.message : "Unknown provider error";
160
+ const searchable = `${providerCode} ${message2}`.toLowerCase();
161
+ let code = "unknown";
162
+ if (value.name === "AbortError" || searchable.includes("abort") || searchable.includes("cancel")) {
163
+ code = "cancelled";
164
+ } else if (status === 401 || status === 403 || /auth|api.?key|unauthorized/.test(searchable)) {
165
+ code = "authentication";
166
+ } else if (status === 429 || /rate.?limit|overloaded/.test(searchable)) {
167
+ code = "rate_limit";
168
+ } else if (/context[_\s-]?(?:length|window|overflow)(?:[_\s-]?(?:exceeded|limit))?/.test(searchable) || /maximum context length|too many tokens|prompt.*(?:too )?long/.test(searchable)) {
169
+ code = "context_overflow";
170
+ } else if (status === 404 || /model.*not.?found|not.?found.*model/.test(searchable)) {
171
+ code = "model_not_found";
172
+ } else if (status === 400 || status === 422 || /invalid schema|schema.*(?:invalid|not (?:a )?valid)/.test(searchable)) {
173
+ code = "invalid_request";
174
+ } else if (status !== void 0 && status >= 500 && status <= 599) {
175
+ code = "network";
176
+ } else if (status === void 0 && /econn|enet|socket|network|fetch failed|connection|timeout/.test(searchable)) {
177
+ code = "network";
178
+ }
179
+ return { code, message: message2 };
180
+ }
181
+
182
+ // src/tools/task-output.ts
183
+ var CommandRunSchema = external_exports.object({
184
+ command: external_exports.string().trim().min(1),
185
+ exitCode: external_exports.coerce.number().int().nullable(),
186
+ summary: external_exports.string().trim().min(1)
187
+ });
188
+ var VerificationItemSchema = external_exports.object({
189
+ name: external_exports.string().trim().min(1),
190
+ passed: external_exports.boolean(),
191
+ details: external_exports.string().trim().min(1)
192
+ });
193
+ var TaskOutputInput = external_exports.object({
194
+ summary: external_exports.string().trim().min(1),
195
+ filesChanged: external_exports.array(external_exports.string().trim().min(1)),
196
+ commandsRun: external_exports.array(CommandRunSchema),
197
+ verification: external_exports.array(VerificationItemSchema),
198
+ artifacts: external_exports.array(external_exports.string().trim().min(1)),
199
+ risks: external_exports.array(external_exports.string().trim().min(1)),
200
+ suggestedNextSteps: external_exports.array(external_exports.string().trim().min(1))
201
+ });
202
+ var TaskOutputResultSchema = TaskOutputInput.extend({
203
+ taskCompleted: external_exports.literal(true)
204
+ }).strict();
205
+ function createTaskOutputTool() {
206
+ return {
207
+ name: "TaskOutput",
208
+ description: "Produce a structured result that summarises task completion. Use this when a subagent has finished its work to report what was accomplished, what files were changed, which commands were run, verification results, and any risks or next steps. The main agent can also use this to produce a structured completion summary.",
209
+ inputSchema: TaskOutputInput,
210
+ paths: () => [],
211
+ summarize: (input) => input.summary,
212
+ execute: async (input) => TaskOutputResultSchema.parse({
213
+ taskCompleted: true,
214
+ summary: input.summary,
215
+ filesChanged: input.filesChanged,
216
+ commandsRun: input.commandsRun,
217
+ verification: input.verification,
218
+ artifacts: input.artifacts,
219
+ risks: input.risks,
220
+ suggestedNextSteps: input.suggestedNextSteps
221
+ })
222
+ };
223
+ }
224
+
225
+ // src/utils/async.ts
226
+ function withTimeout(promise, ms) {
227
+ return new Promise((resolve2, reject) => {
228
+ let settled = false;
229
+ const timer = setTimeout(() => {
230
+ if (settled) return;
231
+ settled = true;
232
+ resolve2({ timedOut: true, value: void 0 });
233
+ }, ms);
234
+ timer.unref?.();
235
+ promise.then(
236
+ (value) => {
237
+ if (!settled) {
238
+ settled = true;
239
+ clearTimeout(timer);
240
+ resolve2({ timedOut: false, value });
241
+ }
242
+ },
243
+ (error) => {
244
+ if (!settled) {
245
+ settled = true;
246
+ clearTimeout(timer);
247
+ reject(error);
248
+ }
249
+ }
250
+ );
251
+ });
252
+ }
253
+ function awaitWithSignal(promise, signal) {
254
+ signal.throwIfAborted();
255
+ return new Promise((resolve2, reject) => {
256
+ const onAbort = () => reject(signal.reason);
257
+ signal.addEventListener("abort", onAbort, { once: true });
258
+ promise.then(
259
+ (value) => {
260
+ signal.removeEventListener("abort", onAbort);
261
+ resolve2(value);
262
+ },
263
+ (error) => {
264
+ signal.removeEventListener("abort", onAbort);
265
+ reject(error);
266
+ }
267
+ );
268
+ });
269
+ }
270
+
271
+ // src/agent/subagents.ts
272
+ var SubagentResultSchema = external_exports.object({
273
+ taskId: external_exports.string().min(1),
274
+ status: external_exports.enum(["completed", "failed", "blocked"]),
275
+ summary: external_exports.string(),
276
+ filesChanged: external_exports.array(external_exports.string()),
277
+ commandsRun: external_exports.array(external_exports.object({
278
+ command: external_exports.string(),
279
+ exitCode: external_exports.number().int().nullable(),
280
+ summary: external_exports.string()
281
+ }).strict()),
282
+ verification: external_exports.array(external_exports.object({
283
+ name: external_exports.string(),
284
+ passed: external_exports.boolean(),
285
+ details: external_exports.string()
286
+ }).strict()),
287
+ artifacts: external_exports.array(external_exports.string()),
288
+ risks: external_exports.array(external_exports.string()),
289
+ suggestedNextSteps: external_exports.array(external_exports.string())
290
+ }).strict();
291
+ function subagentResultFromTaskOutput(taskId, output) {
292
+ const parsed = TaskOutputResultSchema.safeParse(output);
293
+ if (!parsed.success) return void 0;
294
+ const { taskCompleted: _taskCompleted, ...result } = parsed.data;
295
+ return SubagentResultSchema.parse({ taskId, status: "completed", ...result });
296
+ }
297
+ function parseFinalSubagentMessage(messages) {
298
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
299
+ const candidate = messages[index];
300
+ if (candidate.role !== "assistant" || candidate.toolCalls !== void 0 && candidate.toolCalls.length > 0) continue;
301
+ const content = modelContentText(candidate.content);
302
+ try {
303
+ return JSON.parse(content.trim());
304
+ } catch {
305
+ return content;
306
+ }
307
+ }
308
+ return "";
309
+ }
310
+ var SubagentScheduler = class {
311
+ #options;
312
+ constructor(options) {
313
+ const maxSubagents = options.maxSubagents ?? 3;
314
+ if (!Number.isInteger(maxSubagents) || maxSubagents < 1 || maxSubagents > 16) {
315
+ throw new Error("maxSubagents must be an integer from 1 to 16");
316
+ }
317
+ this.#options = { ...options, maxSubagents };
318
+ }
319
+ async run(input, externalSignal) {
320
+ const graph = TaskGraphSchema.parse(input);
321
+ const controller = new AbortController();
322
+ const signal = externalSignal === void 0 ? controller.signal : AbortSignal.any([controller.signal, externalSignal]);
323
+ const states = new Map(graph.nodes.map((task) => [task.id, "pending"]));
324
+ const results = /* @__PURE__ */ new Map();
325
+ const running = /* @__PURE__ */ new Map();
326
+ const runningFiles = /* @__PURE__ */ new Map();
327
+ const started = /* @__PURE__ */ new Set();
328
+ try {
329
+ while (statesHaveWork(states)) {
330
+ signal.throwIfAborted();
331
+ await this.#blockDescendants(graph, states, results);
332
+ for (const task of graph.nodes) {
333
+ if (running.size >= this.#options.maxSubagents) break;
334
+ if (states.get(task.id) !== "pending" || !isReady(task, states)) continue;
335
+ if (conflictsWithRunning(task, runningFiles)) continue;
336
+ states.set(task.id, "running");
337
+ if (task.files !== void 0) runningFiles.set(task.id, task.files.map(normalizeOwnedFile));
338
+ const execution = this.#runTask(task, signal);
339
+ running.set(task.id, execution);
340
+ started.add(execution);
341
+ }
342
+ if (running.size === 0) {
343
+ await this.#blockDescendants(graph, states, results);
344
+ if (statesHaveWork(states)) throw new Error("Scheduler reached an invalid state with no runnable tasks");
345
+ break;
346
+ }
347
+ const completion = await Promise.race(running.values());
348
+ signal.throwIfAborted();
349
+ running.delete(completion.id);
350
+ runningFiles.delete(completion.id);
351
+ results.set(completion.id, completion.result);
352
+ states.set(completion.id, completion.result.status);
353
+ await this.#options.onResult?.(completion.result);
354
+ }
355
+ return orderedOutcome(graph, states, results);
356
+ } finally {
357
+ controller.abort(new Error("Subagent scheduler stopped"));
358
+ await Promise.allSettled(started);
359
+ }
360
+ }
361
+ async #blockDescendants(graph, states, results) {
362
+ let changed = true;
363
+ while (changed) {
364
+ changed = false;
365
+ for (const task of graph.nodes) {
366
+ if (states.get(task.id) !== "pending") continue;
367
+ if (!task.dependencies.some((id) => ["failed", "blocked"].includes(states.get(id) ?? "pending"))) continue;
368
+ states.set(task.id, "blocked");
369
+ const result = syntheticResult(task, "blocked", "Blocked by a failed dependency");
370
+ results.set(task.id, result);
371
+ await this.#options.onResult?.(result);
372
+ changed = true;
373
+ }
374
+ }
375
+ }
376
+ async #runTask(task, signal) {
377
+ let failure;
378
+ let completion;
379
+ try {
380
+ const start = await this.#options.hooks.emit({
381
+ version: 1,
382
+ type: "SubagentStart",
383
+ payload: { taskId: task.id }
384
+ }, signal);
385
+ if (start.decision === "deny") throw new Error(start.reason ?? `Subagent ${task.id} denied by hook`);
386
+ for (const attempt of [1, 2]) {
387
+ signal.throwIfAborted();
388
+ const raw = await awaitWithSignal(this.#options.execute(task, { attempt, signal }), signal);
389
+ const parsed = SubagentResultSchema.safeParse(raw);
390
+ if (parsed.success && parsed.data.taskId === task.id) {
391
+ completion = { id: task.id, result: parsed.data };
392
+ break;
393
+ }
394
+ failure = parsed.success ? new Error(`Subagent result taskId ${parsed.data.taskId} does not match ${task.id}`) : parsed.error;
395
+ }
396
+ completion ??= {
397
+ id: task.id,
398
+ result: syntheticResult(task, "failed", `Invalid structured result after repair: ${message(failure)}`)
399
+ };
400
+ } catch (error) {
401
+ failure = error;
402
+ if (!signal.aborted) completion = { id: task.id, result: syntheticResult(task, "failed", message(error)) };
403
+ } finally {
404
+ try {
405
+ await this.#options.hooks.emit({
406
+ version: 1,
407
+ type: "SubagentStop",
408
+ payload: {
409
+ taskId: task.id,
410
+ status: signal.aborted ? "cancelled" : completion?.result.status ?? "failed",
411
+ ...failure === void 0 ? {} : { error: message(failure) }
412
+ }
413
+ });
414
+ } catch (stopError) {
415
+ if (!signal.aborted) {
416
+ failure = stopError;
417
+ completion = { id: task.id, result: syntheticResult(task, "failed", message(stopError)) };
418
+ }
419
+ }
420
+ }
421
+ if (signal.aborted) throw signal.reason;
422
+ return completion ?? { id: task.id, result: syntheticResult(task, "failed", message(failure)) };
423
+ }
424
+ };
425
+ function isReady(task, states) {
426
+ return task.dependencies.every((dependency) => states.get(dependency) === "completed");
427
+ }
428
+ function normalizeOwnedFile(value) {
429
+ return value.replace(/\\/g, "/");
430
+ }
431
+ function ownedFilesConflict(left, right) {
432
+ for (const one of left) {
433
+ for (const other of right) {
434
+ if (one === other) return true;
435
+ if (other.startsWith(`${one}/`) || one.startsWith(`${other}/`)) return true;
436
+ }
437
+ }
438
+ return false;
439
+ }
440
+ function conflictsWithRunning(task, runningFiles) {
441
+ if (task.files === void 0 || task.files.length === 0) return false;
442
+ const owned = task.files.map(normalizeOwnedFile);
443
+ for (const files of runningFiles.values()) {
444
+ if (ownedFilesConflict(owned, files)) return true;
445
+ }
446
+ return false;
447
+ }
448
+ function statesHaveWork(states) {
449
+ return [...states.values()].some((state) => state === "pending" || state === "running");
450
+ }
451
+ function syntheticResult(task, status, summary) {
452
+ return {
453
+ taskId: task.id,
454
+ status,
455
+ summary,
456
+ filesChanged: [],
457
+ commandsRun: [],
458
+ verification: [],
459
+ artifacts: [],
460
+ risks: [summary],
461
+ suggestedNextSteps: []
462
+ };
463
+ }
464
+ function orderedOutcome(graph, states, results) {
465
+ const orderedStates = {};
466
+ const orderedResults = {};
467
+ for (const task of graph.nodes) {
468
+ orderedStates[task.id] = states.get(task.id);
469
+ const result = results.get(task.id);
470
+ if (result !== void 0) orderedResults[task.id] = result;
471
+ }
472
+ return { states: orderedStates, results: orderedResults };
473
+ }
474
+
475
+ // src/agent/task-plan.ts
476
+ var PlanTaskStatusSchema = external_exports.enum([
477
+ "pending",
478
+ "in_progress",
479
+ "completed",
480
+ "failed",
481
+ "blocked",
482
+ "cancelled"
483
+ ]);
484
+ var PlanTaskSchema = external_exports.object({
485
+ id: external_exports.string().trim().min(1),
486
+ subject: external_exports.string().trim().min(1),
487
+ activeForm: external_exports.string().trim().min(1),
488
+ status: PlanTaskStatusSchema,
489
+ dependencies: external_exports.array(external_exports.string().trim().min(1)),
490
+ result: external_exports.string().optional()
491
+ }).strict();
492
+ var TaskPlanSchema = external_exports.object({
493
+ tasks: external_exports.array(PlanTaskSchema)
494
+ }).strict().superRefine((plan, context) => {
495
+ const ids = /* @__PURE__ */ new Set();
496
+ let activeTasks = 0;
497
+ for (const [index, task] of plan.tasks.entries()) {
498
+ if (ids.has(task.id)) {
499
+ context.addIssue({ code: "custom", path: ["tasks", index, "id"], message: `Duplicate task id: ${task.id}` });
500
+ }
501
+ ids.add(task.id);
502
+ if (task.status === "in_progress") activeTasks += 1;
503
+ }
504
+ if (activeTasks > 1) {
505
+ context.addIssue({ code: "custom", path: ["tasks"], message: "Only one task may be in progress" });
506
+ }
507
+ for (const [index, task] of plan.tasks.entries()) {
508
+ const dependencies = /* @__PURE__ */ new Set();
509
+ for (const dependency of task.dependencies) {
510
+ if (!ids.has(dependency)) {
511
+ context.addIssue({ code: "custom", path: ["tasks", index, "dependencies"], message: `Unknown dependency: ${dependency}` });
512
+ }
513
+ if (dependencies.has(dependency)) {
514
+ context.addIssue({ code: "custom", path: ["tasks", index, "dependencies"], message: `Duplicate dependency: ${dependency}` });
515
+ }
516
+ dependencies.add(dependency);
517
+ }
518
+ if (task.status === "completed" || task.status === "in_progress") {
519
+ const incomplete = task.dependencies.find((dependency) => plan.tasks.find((candidate) => candidate.id === dependency)?.status !== "completed");
520
+ if (incomplete !== void 0) {
521
+ context.addIssue({
522
+ code: "custom",
523
+ path: ["tasks", index, "status"],
524
+ message: `Task ${task.id} has an incomplete dependency: ${incomplete}`
525
+ });
526
+ }
527
+ }
528
+ }
529
+ if (ids.size === plan.tasks.length && hasCycle2(plan.tasks)) {
530
+ context.addIssue({ code: "custom", path: ["tasks"], message: "Task plan contains a cycle" });
531
+ }
532
+ });
533
+ var TaskUpdateInputSchema = external_exports.object({
534
+ taskId: external_exports.string().trim().min(1),
535
+ status: PlanTaskStatusSchema,
536
+ result: external_exports.string().optional()
537
+ }).strict();
538
+ function updatePlanTask(plan, input) {
539
+ const update = TaskUpdateInputSchema.parse(input);
540
+ const current = TaskPlanSchema.parse(plan);
541
+ const index = current.tasks.findIndex((task) => task.id === update.taskId);
542
+ if (index < 0) throw new Error(`Unknown task: ${update.taskId}`);
543
+ const previousStatus = current.tasks[index].status;
544
+ if (!isValidTransition(previousStatus, update.status)) {
545
+ throw new Error(`Invalid task transition for ${update.taskId}: ${previousStatus} -> ${update.status}`);
546
+ }
547
+ if (update.status === "completed" || update.status === "in_progress") {
548
+ const incomplete = current.tasks[index].dependencies.find((dependency) => current.tasks.find((task) => task.id === dependency)?.status !== "completed");
549
+ if (incomplete !== void 0) throw new Error(`Task ${update.taskId} has an incomplete dependency: ${incomplete}`);
550
+ }
551
+ return TaskPlanSchema.parse({
552
+ tasks: current.tasks.map((task, currentIndex) => currentIndex === index ? {
553
+ ...task,
554
+ status: update.status,
555
+ ...update.result === void 0 ? {} : { result: update.result }
556
+ } : { ...task })
557
+ });
558
+ }
559
+ function activateNextReadyTask(plan) {
560
+ const current = TaskPlanSchema.parse(plan);
561
+ if (current.tasks.some((task) => task.status === "in_progress")) return current;
562
+ const next = current.tasks.find((task) => task.status === "pending" && task.dependencies.every((dependency) => current.tasks.find((candidate) => candidate.id === dependency)?.status === "completed"));
563
+ if (next === void 0) return current;
564
+ return updatePlanTask(current, { taskId: next.id, status: "in_progress" });
565
+ }
566
+ function isValidTransition(from, to) {
567
+ if (from === to) return true;
568
+ if (from === "pending") return to === "in_progress" || to === "blocked" || to === "cancelled";
569
+ if (from === "in_progress") {
570
+ return to === "completed" || to === "failed" || to === "blocked" || to === "cancelled";
571
+ }
572
+ if (from === "blocked" || from === "failed") return to === "pending" || to === "in_progress" || to === "cancelled";
573
+ return false;
574
+ }
575
+ function normalizeAbandonedPlan(plan) {
576
+ const current = TaskPlanSchema.parse(plan);
577
+ return TaskPlanSchema.parse({
578
+ tasks: current.tasks.map((task) => task.status === "in_progress" ? {
579
+ ...task,
580
+ status: "cancelled",
581
+ result: "Execution was abandoned"
582
+ } : { ...task })
583
+ });
584
+ }
585
+ function hasCycle2(tasks) {
586
+ const indegree = new Map(tasks.map((task) => [task.id, task.dependencies.length]));
587
+ const dependents = /* @__PURE__ */ new Map();
588
+ for (const task of tasks) {
589
+ for (const dependency of task.dependencies) {
590
+ const items = dependents.get(dependency) ?? [];
591
+ items.push(task.id);
592
+ dependents.set(dependency, items);
593
+ }
594
+ }
595
+ const ready = tasks.filter((task) => indegree.get(task.id) === 0).map((task) => task.id);
596
+ let visited = 0;
597
+ for (let index = 0; index < ready.length; index += 1) {
598
+ const id = ready[index];
599
+ visited += 1;
600
+ for (const dependent of dependents.get(id) ?? []) {
601
+ const next = (indegree.get(dependent) ?? 0) - 1;
602
+ indegree.set(dependent, next);
603
+ if (next === 0) ready.push(dependent);
604
+ }
605
+ }
606
+ return visited !== tasks.length;
607
+ }
608
+
609
+ // src/ui/char-width.ts
610
+ function charWidth(codePoint) {
611
+ if (codePoint === 8203 || // ZERO WIDTH SPACE
612
+ codePoint === 8204 || // ZERO WIDTH NON-JOINER
613
+ codePoint === 8205 || // ZERO WIDTH JOINER
614
+ codePoint === 65279 || // BOM / ZERO WIDTH NO-BREAK SPACE
615
+ codePoint >= 768 && codePoint <= 879 || // Combining Diacritical Marks
616
+ codePoint >= 1155 && codePoint <= 1161 || // Cyrillic combining marks
617
+ codePoint >= 1425 && codePoint <= 1469 || // Hebrew combining marks
618
+ codePoint >= 1552 && codePoint <= 1562 || // Arabic combining marks
619
+ codePoint >= 1611 && codePoint <= 1631 || // Arabic combining marks
620
+ codePoint >= 1648 && codePoint <= 1648 || // Arabic combining marks
621
+ codePoint >= 1750 && codePoint <= 1756 || // Arabic combining marks
622
+ codePoint >= 1759 && codePoint <= 1764 || // Arabic combining marks
623
+ codePoint >= 1767 && codePoint <= 1768 || // Arabic combining marks
624
+ codePoint >= 1770 && codePoint <= 1773 || // Arabic combining marks
625
+ codePoint >= 1809 && codePoint <= 1809 || // Syriac combining marks
626
+ codePoint >= 1840 && codePoint <= 1866 || // Syriac combining marks
627
+ codePoint >= 1958 && codePoint <= 1968 || // Thaana combining marks
628
+ codePoint >= 2304 && codePoint <= 2306 || // Devanagari combining marks
629
+ codePoint >= 2362 && codePoint <= 2362 || // Devanagari combining marks
630
+ codePoint >= 2364 && codePoint <= 2364 || // Devanagari combining marks
631
+ codePoint >= 2369 && codePoint <= 2376 || // Devanagari combining marks
632
+ codePoint >= 2381 && codePoint <= 2381 || // Devanagari combining marks
633
+ codePoint >= 2385 && codePoint <= 2391 || // Devanagari combining marks
634
+ codePoint >= 2402 && codePoint <= 2403 || // Devanagari combining marks
635
+ codePoint >= 2433 && codePoint <= 2433 || // Bengali combining marks
636
+ codePoint >= 2492 && codePoint <= 2492 || // Bengali combining marks
637
+ codePoint >= 2497 && codePoint <= 2500 || // Bengali combining marks
638
+ codePoint >= 2509 && codePoint <= 2509 || // Bengali combining marks
639
+ codePoint >= 2530 && codePoint <= 2531 || // Bengali combining marks
640
+ codePoint >= 2561 && codePoint <= 2562 || // Gurmukhi combining marks
641
+ codePoint >= 2620 && codePoint <= 2620 || // Gurmukhi combining marks
642
+ codePoint >= 2625 && codePoint <= 2626 || // Gurmukhi combining marks
643
+ codePoint >= 2631 && codePoint <= 2632 || // Gurmukhi combining marks
644
+ codePoint >= 2635 && codePoint <= 2637 || // Gurmukhi combining marks
645
+ codePoint >= 2672 && codePoint <= 2673 || // Gurmukhi combining marks
646
+ codePoint >= 2689 && codePoint <= 2690 || // Gujarati combining marks
647
+ codePoint >= 2748 && codePoint <= 2748 || // Gujarati combining marks
648
+ codePoint >= 2753 && codePoint <= 2757 || // Gujarati combining marks
649
+ codePoint >= 2759 && codePoint <= 2760 || // Gujarati combining marks
650
+ codePoint >= 2765 && codePoint <= 2765 || // Gujarati combining marks
651
+ codePoint >= 2786 && codePoint <= 2787 || // Gujarati combining marks
652
+ codePoint >= 2817 && codePoint <= 2817 || // Oriya combining marks
653
+ codePoint >= 2876 && codePoint <= 2876 || // Oriya combining marks
654
+ codePoint >= 2879 && codePoint <= 2879 || // Oriya combining marks
655
+ codePoint >= 2881 && codePoint <= 2883 || // Oriya combining marks
656
+ codePoint >= 2893 && codePoint <= 2893 || // Oriya combining marks
657
+ codePoint >= 2902 && codePoint <= 2902 || // Oriya combining marks
658
+ codePoint >= 2946 && codePoint <= 2946 || // Tamil combining marks
659
+ codePoint >= 3008 && codePoint <= 3008 || // Tamil combining marks
660
+ codePoint >= 3021 && codePoint <= 3021 || // Tamil combining marks
661
+ codePoint >= 3134 && codePoint <= 3136 || // Telugu combining marks
662
+ codePoint >= 3142 && codePoint <= 3144 || // Telugu combining marks
663
+ codePoint >= 3146 && codePoint <= 3149 || // Telugu combining marks
664
+ codePoint >= 3157 && codePoint <= 3158 || // Telugu combining marks
665
+ codePoint >= 3260 && codePoint <= 3260 || // Kannada combining marks
666
+ codePoint >= 3263 && codePoint <= 3263 || // Kannada combining marks
667
+ codePoint >= 3270 && codePoint <= 3270 || // Kannada combining marks
668
+ codePoint >= 3276 && codePoint <= 3277 || // Kannada combining marks
669
+ codePoint >= 3298 && codePoint <= 3299 || // Kannada combining marks
670
+ codePoint >= 3393 && codePoint <= 3395 || // Malayalam combining marks
671
+ codePoint >= 3405 && codePoint <= 3405 || // Malayalam combining marks
672
+ codePoint >= 3530 && codePoint <= 3530 || // Sinhala combining marks
673
+ codePoint >= 3538 && codePoint <= 3540 || // Sinhala combining marks
674
+ codePoint >= 3542 && codePoint <= 3542 || // Sinhala combining marks
675
+ codePoint >= 3633 && codePoint <= 3633 || // Thai combining marks
676
+ codePoint >= 3636 && codePoint <= 3642 || // Thai combining marks
677
+ codePoint >= 3655 && codePoint <= 3662 || // Thai combining marks
678
+ codePoint >= 3761 && codePoint <= 3761 || // Lao combining marks
679
+ codePoint >= 3764 && codePoint <= 3769 || // Lao combining marks
680
+ codePoint >= 3771 && codePoint <= 3772 || // Lao combining marks
681
+ codePoint >= 3784 && codePoint <= 3789 || // Lao combining marks
682
+ codePoint >= 3864 && codePoint <= 3865 || // Tibetan combining marks
683
+ codePoint >= 3893 && codePoint <= 3893 || // Tibetan combining marks
684
+ codePoint >= 3895 && codePoint <= 3895 || // Tibetan combining marks
685
+ codePoint >= 3897 && codePoint <= 3897 || // Tibetan combining marks
686
+ codePoint >= 3953 && codePoint <= 3966 || // Tibetan combining marks
687
+ codePoint >= 3968 && codePoint <= 3972 || // Tibetan combining marks
688
+ codePoint >= 3974 && codePoint <= 3975 || // Tibetan combining marks
689
+ codePoint >= 3984 && codePoint <= 3991 || // Tibetan combining marks
690
+ codePoint >= 3993 && codePoint <= 4028 || // Tibetan combining marks
691
+ codePoint >= 4038 && codePoint <= 4038 || // Tibetan combining marks
692
+ codePoint >= 4146 && codePoint <= 4151 || // Myanmar combining marks
693
+ codePoint >= 4153 && codePoint <= 4154 || // Myanmar combining marks
694
+ codePoint >= 4157 && codePoint <= 4158 || // Myanmar combining marks
695
+ codePoint >= 4184 && codePoint <= 4185 || // Myanmar combining marks
696
+ codePoint >= 4190 && codePoint <= 4192 || // Myanmar combining marks
697
+ codePoint >= 4209 && codePoint <= 4212 || // Myanmar combining marks
698
+ codePoint >= 4226 && codePoint <= 4226 || // Myanmar combining marks
699
+ codePoint >= 4229 && codePoint <= 4230 || // Myanmar combining marks
700
+ codePoint >= 4237 && codePoint <= 4237 || // Myanmar combining marks
701
+ codePoint >= 4957 && codePoint <= 4959 || // Ethiopic combining marks
702
+ codePoint >= 5906 && codePoint <= 5908 || // Tagalog combining marks
703
+ codePoint >= 5938 && codePoint <= 5940 || // Hanunoo combining marks
704
+ codePoint >= 5970 && codePoint <= 5971 || // Buhid combining marks
705
+ codePoint >= 6002 && codePoint <= 6003 || // Tagbanwa combining marks
706
+ codePoint >= 6068 && codePoint <= 6069 || // Khmer combining marks
707
+ codePoint >= 6071 && codePoint <= 6077 || // Khmer combining marks
708
+ codePoint >= 6086 && codePoint <= 6086 || // Khmer combining marks
709
+ codePoint >= 6089 && codePoint <= 6099 || // Khmer combining marks
710
+ codePoint >= 6109 && codePoint <= 6109 || // Khmer combining marks
711
+ codePoint >= 6155 && codePoint <= 6157 || // Mongolian combining marks
712
+ codePoint >= 6313 && codePoint <= 6313 || // Mongolian combining marks
713
+ codePoint >= 6432 && codePoint <= 6434 || // Limbu combining marks
714
+ codePoint >= 6439 && codePoint <= 6440 || // Limbu combining marks
715
+ codePoint >= 6450 && codePoint <= 6450 || // Limbu combining marks
716
+ codePoint >= 6457 && codePoint <= 6459 || // Limbu combining marks
717
+ codePoint >= 6679 && codePoint <= 6680 || // Buginese combining marks
718
+ codePoint >= 6912 && codePoint <= 6915 || // Balinese combining marks
719
+ codePoint >= 6964 && codePoint <= 6964 || // Balinese combining marks
720
+ codePoint >= 6966 && codePoint <= 6970 || // Balinese combining marks
721
+ codePoint >= 6972 && codePoint <= 6972 || // Balinese combining marks
722
+ codePoint >= 6978 && codePoint <= 6978 || // Balinese combining marks
723
+ codePoint >= 7019 && codePoint <= 7027 || // Balinese combining marks
724
+ codePoint >= 7616 && codePoint <= 7679 || // Combining Diacritical Marks Supplement
725
+ codePoint >= 8400 && codePoint <= 8432 || // Combining Diacritical Marks for Symbols
726
+ codePoint >= 11503 && codePoint <= 11505 || // Coptic combining marks
727
+ codePoint >= 11647 && codePoint <= 11647 || // Tifinagh combining marks
728
+ codePoint >= 11744 && codePoint <= 11775 || // Cyrillic Extended-A combining marks
729
+ codePoint >= 42607 && codePoint <= 42610 || // Cyrillic Extended-B combining marks
730
+ codePoint >= 42612 && codePoint <= 42621 || // Cyrillic Extended-B combining marks
731
+ codePoint >= 42654 && codePoint <= 42655 || // Cyrillic Extended-B combining marks
732
+ codePoint >= 42736 && codePoint <= 42737 || // Bamum combining marks
733
+ codePoint >= 43010 && codePoint <= 43010 || // Syloti Nagri combining marks
734
+ codePoint >= 43014 && codePoint <= 43014 || // Syloti Nagri combining marks
735
+ codePoint >= 43019 && codePoint <= 43019 || // Syloti Nagri combining marks
736
+ codePoint >= 43045 && codePoint <= 43046 || // Syloti Nagri combining marks
737
+ codePoint >= 43204 && codePoint <= 43204 || // Saurashtra combining marks
738
+ codePoint >= 43232 && codePoint <= 43249 || // Devanagari Extended combining marks
739
+ codePoint >= 43302 && codePoint <= 43309 || // Kayah Li combining marks
740
+ codePoint >= 43335 && codePoint <= 43345 || // Rejang combining marks
741
+ codePoint >= 43392 && codePoint <= 43394 || // Javanese combining marks
742
+ codePoint >= 43443 && codePoint <= 43443 || // Javanese combining marks
743
+ codePoint >= 43446 && codePoint <= 43449 || // Javanese combining marks
744
+ codePoint >= 43452 && codePoint <= 43452 || // Javanese combining marks
745
+ codePoint >= 43561 && codePoint <= 43566 || // Cham combining marks
746
+ codePoint >= 43569 && codePoint <= 43570 || // Cham combining marks
747
+ codePoint >= 43573 && codePoint <= 43574 || // Cham combining marks
748
+ codePoint >= 43587 && codePoint <= 43587 || // Cham combining marks
749
+ codePoint >= 43596 && codePoint <= 43596 || // Cham combining marks
750
+ codePoint >= 43696 && codePoint <= 43696 || // Tai Viet combining marks
751
+ codePoint >= 43698 && codePoint <= 43700 || // Tai Viet combining marks
752
+ codePoint >= 43703 && codePoint <= 43704 || // Tai Viet combining marks
753
+ codePoint >= 43710 && codePoint <= 43711 || // Tai Viet combining marks
754
+ codePoint >= 43713 && codePoint <= 43713 || // Tai Viet combining marks
755
+ codePoint >= 44005 && codePoint <= 44005 || // Meetei Mayek combining marks
756
+ codePoint >= 44008 && codePoint <= 44008 || // Meetei Mayek combining marks
757
+ codePoint >= 44013 && codePoint <= 44013 || // Meetei Mayek combining marks
758
+ codePoint >= 64286 && codePoint <= 64286 || // Hebrew combining marks
759
+ codePoint >= 65024 && codePoint <= 65039 || // Variation Selectors
760
+ codePoint >= 65056 && codePoint <= 65071 || // Combining Half Marks
761
+ codePoint >= 917760 && codePoint <= 917999) {
762
+ return 0;
763
+ }
764
+ if (codePoint >= 4352 && codePoint <= 4447 || // Hangul Jamo
765
+ codePoint >= 9001 && codePoint <= 9002 || // Left/Right-pointing angle bracket
766
+ codePoint >= 11904 && codePoint <= 12350 || // CJK Radicals Supplement, Kangxi Radicals, CJK Symbols
767
+ codePoint >= 12352 && codePoint <= 13247 || // Hiragana, Katakana, Bopomofo, Hangul Compatibility, Kanbun, CJK Strokes, Enclosed CJK
768
+ codePoint >= 13312 && codePoint <= 19903 || // CJK Unified Ideographs Extension A
769
+ codePoint >= 19968 && codePoint <= 42191 || // CJK Unified Ideographs, Yi Syllables, Yi Radicals
770
+ codePoint >= 43360 && codePoint <= 43388 || // Hangul Jamo Extended-A
771
+ codePoint >= 44032 && codePoint <= 55203 || // Hangul Syllables
772
+ codePoint >= 63744 && codePoint <= 64255 || // CJK Compatibility Ideographs
773
+ codePoint >= 65040 && codePoint <= 65049 || // Vertical forms
774
+ codePoint >= 65072 && codePoint <= 65135 || // CJK Compatibility Forms, Small Form Variants
775
+ codePoint >= 65281 && codePoint <= 65376 || // Fullwidth Forms
776
+ codePoint >= 65504 && codePoint <= 65510 || // Fullwidth Signs
777
+ codePoint >= 110592 && codePoint <= 111359 || // Kana Supplement, Kana Extended-A, Small Kana Extension
778
+ codePoint >= 126980 && codePoint <= 126980 || // Mahjong tile
779
+ codePoint >= 127183 && codePoint <= 127183 || // Playing card
780
+ codePoint >= 127232 && codePoint <= 129535 || // Enclosed Alphanumeric Supplement, Emoticons, Transport, etc.
781
+ codePoint >= 129536 && codePoint <= 129647 || // Chess Symbols
782
+ codePoint >= 129648 && codePoint <= 129791 || // Symbols and Pictographs Extended-A
783
+ codePoint >= 131072 && codePoint <= 196605 || // CJK Unified Ideographs Extension B–G, CJK Compatibility Supplement
784
+ codePoint >= 196608 && codePoint <= 262141) {
785
+ return 2;
786
+ }
787
+ return 1;
788
+ }
789
+
790
+ // src/ui/thinking-line.ts
791
+ var THINKING_SCROLL_CELLS_PER_SEC = 16;
792
+ var THINKING_EASE = 0.18;
793
+ var THINKING_MAX_STEP_MS = 250;
794
+ var THINKING_MAX_STORED_CHARS = 4e3;
795
+ function normalizeThinkingText(text) {
796
+ return text.replace(/\s+/gu, " ").trim();
797
+ }
798
+ function thinkingDisplayWidth(text) {
799
+ let width = 0;
800
+ for (const character of text) width += charWidth(character.codePointAt(0) ?? 32);
801
+ return width;
802
+ }
803
+ function sliceThinkingByWidth(text, startCell, cellCount) {
804
+ let out = "";
805
+ let cell = 0;
806
+ for (const character of text) {
807
+ const w = charWidth(character.codePointAt(0) ?? 32);
808
+ if (cell + w > startCell && cell < startCell + cellCount) out += character;
809
+ cell += w;
810
+ if (cell >= startCell + cellCount) break;
811
+ }
812
+ return out;
813
+ }
814
+ function thinkingWindow(text, previousStart, options) {
815
+ const normalized = normalizeThinkingText(text);
816
+ const width = Math.max(1, Math.floor(options.width));
817
+ const total = thinkingDisplayWidth(normalized);
818
+ if (total <= width) return { text: normalized, start: 0 };
819
+ const maxStart = total - width;
820
+ const from = Math.min(Math.max(0, previousStart), maxStart);
821
+ const cellsPerSecond = options.cellsPerSecond ?? THINKING_SCROLL_CELLS_PER_SEC;
822
+ const ease = options.ease ?? THINKING_EASE;
823
+ const dt = Math.max(0, Math.min(options.dtMs, THINKING_MAX_STEP_MS));
824
+ const lag = maxStart - from;
825
+ const step = Math.max(cellsPerSecond * dt / 1e3, lag * ease);
826
+ const start = Math.min(maxStart, Math.floor(from + step));
827
+ return { text: sliceThinkingByWidth(normalized, start, width), start };
828
+ }
829
+ function appendThinkingText(previous, delta) {
830
+ const next = (previous ?? "") + delta;
831
+ return next.length > THINKING_MAX_STORED_CHARS ? next.slice(-THINKING_MAX_STORED_CHARS) : next;
832
+ }
833
+
834
+ // src/ui/transcript.ts
835
+ var MAX_TRANSCRIPT_TURNS = 200;
836
+ var MAX_TRANSCRIPT_CHARS = 2e6;
837
+ var MAX_ACTIVE_TRANSCRIPT_BLOCKS = 160;
838
+ var MAX_ACTIVE_ASSISTANT_CHARS = 64e3;
839
+ var MAX_TRANSCRIPT_BLOCK_TEXT_CHARS = 16e3;
840
+ var MAX_TRANSCRIPT_TOOL_VALUE_CHARS = 16e3;
841
+ var MAX_TRANSCRIPT_PRESENTATION_CHARS = 32e3;
842
+ function createTranscriptState() {
843
+ return { completed: [], nextId: 1 };
844
+ }
845
+ function transcriptReducer(state, action) {
846
+ if (action.type === "hydrate") return hydrateHistory(action.messages, action.compact);
847
+ if (action.type === "restore") return restoreTranscriptState(action.state);
848
+ if (action.type === "clear") return createTranscriptState();
849
+ if (action.type === "submit") {
850
+ const active = {
851
+ id: state.nextId,
852
+ submittedAt: (/* @__PURE__ */ new Date()).toISOString(),
853
+ prompt: action.prompt,
854
+ assistantText: "",
855
+ statusLines: [],
856
+ blocks: [],
857
+ ...state.taskSnapshot === void 0 ? {} : { taskSnapshot: state.taskSnapshot },
858
+ ...state.taskSnapshot === void 0 ? {} : { suppressedTaskIds: terminalTaskIds(state.taskSnapshot) }
859
+ };
860
+ return {
861
+ ...state,
862
+ active: state.taskSnapshot === void 0 ? active : applyTaskSnapshot(active, state.taskSnapshot, false),
863
+ nextId: state.nextId + 1
864
+ };
865
+ }
866
+ if (action.type === "finish") return finishActive(state);
867
+ if (action.type === "submit-error") {
868
+ if (state.active === void 0) return state;
869
+ return finishActive({
870
+ ...state,
871
+ active: addText(withoutModelActivity(state.active), `\u25C6 ${action.message}`, true)
872
+ });
873
+ }
874
+ const event = action.event;
875
+ if (event.type === "clear") return createTranscriptState();
876
+ if (event.type === "queued-prompt") {
877
+ return transcriptReducer(state.active === void 0 ? state : finishActive(state), {
878
+ type: "submit",
879
+ prompt: event.prompt
880
+ });
881
+ }
882
+ if (event.type === "queued-remote-prompt") {
883
+ return createPalTurn(state.active === void 0 ? state : finishActive(state), {
884
+ alias: event.senderAlias,
885
+ instanceId: event.senderId,
886
+ prompt: event.prompt,
887
+ ...event.context === void 0 ? {} : { context: event.context }
888
+ });
889
+ }
890
+ if (event.type === "tasks-cleared") {
891
+ const { taskSnapshot: _taskSnapshot, ...rest } = state;
892
+ return rest;
893
+ }
894
+ if (event.type === "tasks") {
895
+ const active = state.active === void 0 ? void 0 : applyTaskSnapshot(state.active, event.snapshot);
896
+ return {
897
+ ...state,
898
+ taskSnapshot: event.snapshot,
899
+ ...active === void 0 ? {} : { active }
900
+ };
901
+ }
902
+ if (event.type === "pal-task") return applyPalTaskEvent(state, event);
903
+ if (event.type === "cowork-event") return applyCoWorkEvent(state, event);
904
+ if (event.type === "exit" || state.active === void 0) return state;
905
+ if (event.type === "model-start") return upsertStatus({
906
+ ...state,
907
+ active: withoutModelActivity(state.active)
908
+ }, {
909
+ kind: "status",
910
+ id: `model:${event.id}`,
911
+ state: "running",
912
+ text: "Flavoring",
913
+ activity: "model",
914
+ startedAt: Date.now()
915
+ });
916
+ if (event.type === "model-end") return {
917
+ ...state,
918
+ active: withoutModelActivity(state.active, `model:${event.id}`)
919
+ };
920
+ if (event.type === "done") {
921
+ const usage = event.usage;
922
+ const cacheRead = usage.cacheReadTokens;
923
+ const hasCache = cacheRead !== void 0;
924
+ const percent = !hasCache || usage.inputTokens <= 0 ? 0 : Math.round(cacheRead / usage.inputTokens * 100);
925
+ const withUsage = upsertStatus({ ...state, active: withoutModelActivity(state.active) }, {
926
+ kind: "status",
927
+ id: `usage:${state.active.id}`,
928
+ state: "info",
929
+ text: hasCache ? `\xB7 ${usage.inputTokens} in \xB7 ${percent}% cached \xB7 ${usage.outputTokens} out` : `\xB7 ${usage.inputTokens} in \xB7 ${usage.outputTokens} out`,
930
+ ...hasCache ? { hint: `cache ${cacheRead}/${usage.inputTokens}` } : {}
931
+ });
932
+ return finishActive(withUsage);
933
+ }
934
+ if (event.type === "text") {
935
+ return { ...state, active: addStreamingText(state.active, event.text) };
936
+ }
937
+ if (event.type === "thinking") {
938
+ return { ...state, active: attachThinking(state.active, event.text) };
939
+ }
940
+ if (event.type === "tool-start") return upsertStatus({ ...state, active: withoutModelActivity(state.active) }, {
941
+ kind: "status",
942
+ id: `tool:${event.id}`,
943
+ state: "running",
944
+ text: `${event.name}${event.label ? ` ${event.label}` : ""}`,
945
+ ...event.hint === void 0 ? {} : { hint: event.hint },
946
+ ...event.presentation === void 0 ? {} : { presentation: event.presentation },
947
+ tool: { name: event.name, input: event.input }
948
+ });
949
+ if (event.type === "tool-end") {
950
+ const cancelled = !event.result.ok && event.result.error?.code === "cancelled";
951
+ const previous = state.active.blocks.find((block) => block.kind === "status" && block.id === `tool:${event.id}`);
952
+ const input = previous?.kind === "status" && previous.tool !== void 0 ? previous.tool.input : null;
953
+ const presentation = event.result.ok ? event.result.presentation ?? (previous?.kind === "status" ? previous.presentation : void 0) : void 0;
954
+ return upsertStatus(state, {
955
+ kind: "status",
956
+ id: `tool:${event.id}`,
957
+ state: event.result.ok ? "completed" : cancelled ? "cancelled" : "failed",
958
+ text: `${event.result.ok ? "\u2713" : "\xD7"} ${event.name}${event.label ? ` ${event.label}` : ""}`,
959
+ ...event.hint === void 0 ? {} : { hint: event.hint },
960
+ ...presentation === void 0 ? {} : { presentation },
961
+ tool: { name: event.name, input, result: event.result }
962
+ });
963
+ }
964
+ if (event.type === "deliverables") return upsertStatus(state, {
965
+ kind: "status",
966
+ id: `deliverables:${state.active.id}`,
967
+ state: "completed",
968
+ text: `Changed ${event.files.length} file${event.files.length === 1 ? "" : "s"}`,
969
+ details: event.files.map((file) => `${file.operation} ${file.path} (+${file.added} -${file.removed})`).join("\n"),
970
+ presentation: { kind: "changeset", files: event.files }
971
+ });
972
+ if (event.type === "notice") return upsertStatus(state, {
973
+ kind: "status",
974
+ id: `notice:${state.active.blocks.length}`,
975
+ state: "info",
976
+ text: `\xB7 ${event.message}`
977
+ });
978
+ if (event.type === "compact-progress") {
979
+ const progress = Math.max(0, Math.min(100, Math.floor(event.progress / 10) * 10));
980
+ return upsertStatus(state, {
981
+ kind: "status",
982
+ id: "compact:progress",
983
+ state: progress === 100 ? "completed" : "running",
984
+ text: "Compacting context",
985
+ progress
986
+ });
987
+ }
988
+ if (event.type === "error") {
989
+ const withoutActivity = withoutModelActivity(state.active);
990
+ const active = event.error.code === "cancelled" ? stripRetryBlocks(withoutActivity) : withoutActivity;
991
+ return { ...state, active: addText(active, `\u25C6 ${event.error.code}: ${event.error.message}`, true) };
992
+ }
993
+ if (event.type === "usage") return state;
994
+ if (event.type === "model-retry") return upsertStatus({ ...state, active: withoutModelActivity(state.active) }, {
995
+ kind: "status",
996
+ id: "model-retry",
997
+ state: "info",
998
+ tone: "retry",
999
+ text: `\u21BB Retrying model call \xB7 attempt ${event.attempt}/${event.maxAttempts} in ${event.delayMs / 1e3}s`
1000
+ });
1001
+ if (event.type === "structured-output-retry") return upsertStatus(state, {
1002
+ kind: "status",
1003
+ id: `structured-retry:${event.tool}`,
1004
+ state: "info",
1005
+ tone: "retry",
1006
+ text: `\u21BB Repairing ${event.tool} arguments \xB7 attempt ${event.attempt}/${event.maxAttempts} in ${event.delayMs / 1e3}s`
1007
+ });
1008
+ if (event.type === "loop-progress") return upsertStatus(state, {
1009
+ kind: "status",
1010
+ id: `loop:${event.loopId}`,
1011
+ state: event.state,
1012
+ text: event.message
1013
+ });
1014
+ if (event.type === "compacted") return upsertStatus(state, {
1015
+ kind: "status",
1016
+ id: `compact:${state.active.blocks.length}`,
1017
+ state: "info",
1018
+ text: "\xB7 Context compacted."
1019
+ });
1020
+ if (event.type === "warning") return upsertStatus(state, {
1021
+ kind: "status",
1022
+ id: `warn:${state.active.blocks.length}`,
1023
+ state: "info",
1024
+ tone: "warning",
1025
+ text: `\u26A0 ${event.message}`
1026
+ });
1027
+ if (event.type === "limit_reached") {
1028
+ const suffix = event.extended ? " \u2014 auto-extended" : "";
1029
+ return upsertStatus(state, {
1030
+ kind: "status",
1031
+ id: `limit:${state.active.blocks.length}`,
1032
+ state: event.extended ? "info" : "failed",
1033
+ text: `\u25C6 Iteration limit ${event.maxIterations} reached at round ${event.iteration}${suffix}`
1034
+ });
1035
+ }
1036
+ return state;
1037
+ }
1038
+ var MAX_PEER_PRESENTATION_CHARS = 240;
1039
+ function boundedPeerText(value, fallback) {
1040
+ const text = typeof value === "string" && value.length > 0 ? value : fallback;
1041
+ return text.length <= MAX_PEER_PRESENTATION_CHARS ? text : `${text.slice(0, MAX_PEER_PRESENTATION_CHARS - 1)}\u2026`;
1042
+ }
1043
+ function palLabel(alias, instanceId) {
1044
+ return `PAL \xB7 ${boundedPeerText(alias, "unknown")} (${instanceId.slice(0, 8)})`;
1045
+ }
1046
+ function createPalTurn(state, input) {
1047
+ const active = {
1048
+ id: state.nextId,
1049
+ submittedAt: (/* @__PURE__ */ new Date()).toISOString(),
1050
+ prompt: boundedPeerText(input.prompt, "Peer collaboration update"),
1051
+ assistantText: "",
1052
+ statusLines: [],
1053
+ blocks: [],
1054
+ source: {
1055
+ kind: "pal",
1056
+ alias: boundedPeerText(input.alias, "unknown"),
1057
+ instanceId: input.instanceId,
1058
+ ...input.context === void 0 ? {} : { context: input.context }
1059
+ },
1060
+ ...state.taskSnapshot === void 0 ? {} : { taskSnapshot: state.taskSnapshot }
1061
+ };
1062
+ return { ...state, active, nextId: state.nextId + 1 };
1063
+ }
1064
+ function applyPalTaskEvent(state, event) {
1065
+ if (state.active === void 0) {
1066
+ return createPalTurn(state, {
1067
+ alias: event.senderAlias,
1068
+ instanceId: event.senderId,
1069
+ prompt: event.goal
1070
+ });
1071
+ }
1072
+ return upsertStatus(state, {
1073
+ kind: "status",
1074
+ id: `pal-task:${event.taskId}`,
1075
+ state: "info",
1076
+ text: `${palLabel(event.senderAlias, event.senderId)} \xB7 task received`,
1077
+ details: boundedPeerText(event.goal, "Peer task")
1078
+ });
1079
+ }
1080
+ function applyCoWorkEvent(initial, event) {
1081
+ const snapshot = typeof event.snapshot === "object" && event.snapshot !== null ? event.snapshot : {};
1082
+ const goal = boundedPeerText(snapshot.goal, "Co-work update");
1083
+ const context = event.action === "START" ? "CO-WORK EXECUTION" : "CO-WORK PLANNING";
1084
+ const wasIdle = initial.active === void 0;
1085
+ let state = wasIdle ? createPalTurn(initial, { alias: event.senderAlias, instanceId: event.senderId, prompt: goal, context }) : initial;
1086
+ if (event.action === "START" && state.active?.source?.kind === "pal") {
1087
+ state = { ...state, active: { ...state.active, source: { ...state.active.source, context: "CO-WORK EXECUTION" } } };
1088
+ }
1089
+ const integration = typeof snapshot.integration === "object" && snapshot.integration !== null ? snapshot.integration : void 0;
1090
+ const failedAssertion = Array.isArray(snapshot.completionAssertions) ? snapshot.completionAssertions.find((assertion) => typeof assertion === "object" && assertion !== null && assertion.passed === false) : void 0;
1091
+ const passed = integration?.passed;
1092
+ const details = event.action === "END" || event.action === "FAIL" ? boundedPeerText(integration?.evidence ?? failedAssertion?.detail, goal) : goal;
1093
+ const stateByAction = event.action === "PROPOSE" || event.action === "START" ? "running" : event.action === "END" ? passed === false ? "failed" : "completed" : event.action === "FAIL" ? "failed" : event.action === "CANCEL" ? "cancelled" : "info";
1094
+ const textByAction = event.action === "PROPOSE" ? "Co-work proposal received" : event.action === "PLAN" ? "Co-work plan updated" : event.action === "START" ? "Co-work execution started" : event.action === "END" ? passed === false ? "\xD7 Co-work ended \xB7 integration failed" : "\u2713 Co-work ended" : event.action === "FAIL" ? "\xD7 Co-work failed" : "\xD7 Co-work cancelled";
1095
+ state = upsertStatus(state, {
1096
+ kind: "status",
1097
+ id: `cowork:${event.coWorkId}:${event.action}`,
1098
+ state: stateByAction,
1099
+ text: `${palLabel(event.senderAlias, event.senderId)} \xB7 ${textByAction}`,
1100
+ details
1101
+ });
1102
+ if ((event.action === "END" || event.action === "FAIL" || event.action === "CANCEL") && (wasIdle || state.active?.source?.kind === "pal")) {
1103
+ return finishActive(state);
1104
+ }
1105
+ return state;
1106
+ }
1107
+ function hydrateHistory(messages, compact) {
1108
+ const completed = [];
1109
+ if (compact !== void 0) completed.push(compactionTurn(compact));
1110
+ let turn;
1111
+ for (const message2 of messages) {
1112
+ if (message2.role === "user") {
1113
+ if (turn !== void 0) completed.push(turn);
1114
+ turn = {
1115
+ id: completed.length + 1,
1116
+ prompt: modelContentTranscriptText(message2.content),
1117
+ assistantText: "",
1118
+ statusLines: [],
1119
+ blocks: []
1120
+ };
1121
+ } else if (message2.role === "assistant" && turn !== void 0) {
1122
+ const content = modelContentText(message2.content);
1123
+ if (content.length > 0) turn = addText(turn, content);
1124
+ for (const call of message2.toolCalls ?? []) {
1125
+ turn = upsertTurnStatus(turn, {
1126
+ kind: "status",
1127
+ id: `tool:${call.id}`,
1128
+ state: "running",
1129
+ text: call.name,
1130
+ tool: { name: call.name, input: call.input }
1131
+ });
1132
+ }
1133
+ } else if (message2.role === "tool" && turn !== void 0) {
1134
+ turn = applyLegacyToolResult(turn, message2);
1135
+ }
1136
+ }
1137
+ if (turn !== void 0) completed.push(turn);
1138
+ return { completed: boundedCompleted(completed), nextId: completed.length + 1 };
1139
+ }
1140
+ function transcriptFromLegacyConversation(messages, compact) {
1141
+ return hydrateHistory(messages, compact);
1142
+ }
1143
+ function applyTaskSnapshot(turn, snapshot, includeTerminal = true) {
1144
+ const taskBlocks = [];
1145
+ const previous = new Map(turn.blocks.filter((block) => block.kind === "status").map((block) => [block.id, block]));
1146
+ const now = Date.now();
1147
+ const suppressed = new Set(turn.suppressedTaskIds ?? []);
1148
+ for (const task of snapshot.plan?.tasks ?? []) {
1149
+ const id = `task:${task.id}`;
1150
+ const terminal = ["completed", "failed", "blocked", "cancelled"].includes(task.status);
1151
+ if (!terminal) suppressed.delete(id);
1152
+ if ((!includeTerminal || suppressed.has(id)) && terminal) continue;
1153
+ const state = task.status === "in_progress" ? "running" : task.status === "completed" ? "completed" : task.status === "cancelled" ? "cancelled" : task.status === "failed" || task.status === "blocked" ? "failed" : "info";
1154
+ const suffix = task.status === "completed" ? "done" : task.status.replace("_", " ");
1155
+ const prior = previous.get(id);
1156
+ const startedAt = state === "running" ? prior?.startedAt ?? now : prior?.startedAt;
1157
+ const elapsedMs = state !== "running" && startedAt !== void 0 ? prior?.elapsedMs ?? Math.max(0, now - startedAt) : prior?.elapsedMs;
1158
+ taskBlocks.push({
1159
+ kind: "status",
1160
+ id,
1161
+ state,
1162
+ text: `${state === "completed" ? "\u2713" : state === "failed" || state === "cancelled" ? "\xD7" : "\xB7"} ${task.subject} \xB7 ${suffix}`,
1163
+ task: { subject: task.subject, activeForm: task.activeForm, role: "main" },
1164
+ ...startedAt === void 0 ? {} : { startedAt },
1165
+ ...elapsedMs === void 0 ? {} : { elapsedMs }
1166
+ });
1167
+ }
1168
+ for (const node of snapshot.subagents.graph?.nodes ?? []) {
1169
+ const status = snapshot.subagents.states[node.id] ?? "pending";
1170
+ const id = `subagent:${node.id}`;
1171
+ const terminal = ["completed", "failed", "blocked", "cancelled"].includes(status);
1172
+ if (!terminal) suppressed.delete(id);
1173
+ if ((!includeTerminal || suppressed.has(id)) && terminal) continue;
1174
+ const state = status === "running" ? "running" : status === "completed" ? "completed" : status === "cancelled" ? "cancelled" : status === "failed" || status === "blocked" ? "failed" : "info";
1175
+ const prior = previous.get(id);
1176
+ const snapshotStartedAt = snapshot.subagents.startedAt?.[node.id];
1177
+ const snapshotElapsedMs = snapshot.subagents.elapsedMs?.[node.id];
1178
+ const startedAt = state === "running" ? snapshotStartedAt ?? prior?.startedAt ?? now : prior?.startedAt ?? snapshotStartedAt;
1179
+ const elapsedMs = state !== "running" && startedAt !== void 0 ? prior?.elapsedMs ?? snapshotElapsedMs ?? Math.max(0, now - startedAt) : prior?.elapsedMs;
1180
+ taskBlocks.push({
1181
+ kind: "status",
1182
+ id,
1183
+ state,
1184
+ text: `${state === "completed" ? "\u2713" : state === "failed" || state === "cancelled" ? "\xD7" : "\xB7"} subagent: ${node.description} \xB7 ${status}`,
1185
+ task: { subject: node.description, activeForm: node.description, role: "subagent" },
1186
+ ...startedAt === void 0 ? {} : { startedAt },
1187
+ ...elapsedMs === void 0 ? {} : { elapsedMs }
1188
+ });
1189
+ }
1190
+ const ids = new Set(taskBlocks.map((block) => block.id));
1191
+ const blocks = turn.blocks.filter((block) => {
1192
+ if (block.kind !== "status") return true;
1193
+ if (!block.id.startsWith("task:") && !block.id.startsWith("subagent:")) return true;
1194
+ return ids.has(block.id);
1195
+ });
1196
+ for (const block of taskBlocks) {
1197
+ const index = blocks.findIndex((current) => current.kind === "status" && current.id === block.id);
1198
+ if (index < 0) blocks.push(block);
1199
+ else blocks[index] = block;
1200
+ }
1201
+ return {
1202
+ ...withBlocks(turn, blocks),
1203
+ taskSnapshot: snapshot,
1204
+ suppressedTaskIds: [...suppressed]
1205
+ };
1206
+ }
1207
+ function terminalTaskIds(snapshot) {
1208
+ const ids = (snapshot.plan?.tasks ?? []).filter((task) => ["completed", "failed", "blocked", "cancelled"].includes(task.status)).map((task) => `task:${task.id}`);
1209
+ for (const [id, status] of Object.entries(snapshot.subagents.states)) {
1210
+ if (["completed", "failed", "blocked", "cancelled"].includes(status)) ids.push(`subagent:${id}`);
1211
+ }
1212
+ return ids;
1213
+ }
1214
+ function upsertStatus(state, block) {
1215
+ if (state.active === void 0) return state;
1216
+ const blocks = [...state.active.blocks];
1217
+ const index = blocks.findIndex((item) => item.kind === "status" && item.id === block.id);
1218
+ const bounded = boundedTranscriptBlock(block);
1219
+ if (index < 0) blocks.push(bounded);
1220
+ else blocks[index] = bounded;
1221
+ return { ...state, active: withBlocks(state.active, blocks) };
1222
+ }
1223
+ function finishActive(state) {
1224
+ if (state.active === void 0) return state;
1225
+ const active = withoutModelActivity(state.active);
1226
+ return {
1227
+ completed: boundedCompleted([...state.completed, {
1228
+ ...active,
1229
+ completedAt: active.completedAt ?? (/* @__PURE__ */ new Date()).toISOString()
1230
+ }]),
1231
+ nextId: state.nextId,
1232
+ ...state.taskSnapshot === void 0 ? {} : { taskSnapshot: state.taskSnapshot }
1233
+ };
1234
+ }
1235
+ function attachThinking(turn, text) {
1236
+ const blocks = [...turn.blocks];
1237
+ const index = blocks.findIndex((block2) => block2.kind === "status" && block2.activity === "model" && block2.state === "running");
1238
+ if (index < 0) return turn;
1239
+ const block = blocks[index];
1240
+ if (block === void 0 || block.kind !== "status") return turn;
1241
+ blocks[index] = {
1242
+ ...block,
1243
+ thinkingText: appendThinkingText(block.thinkingText, text)
1244
+ };
1245
+ return withBlocks(turn, blocks);
1246
+ }
1247
+ function withoutModelActivity(turn, id) {
1248
+ const blocks = turn.blocks.filter((block) => block.kind !== "status" || block.activity !== "model" || id !== void 0 && block.id !== id);
1249
+ if (blocks.length === turn.blocks.length) return turn;
1250
+ return withBlocks(turn, blocks);
1251
+ }
1252
+ function restoreTranscriptState(state) {
1253
+ const completed = state.completed.map(cloneTurn);
1254
+ if (state.active !== void 0) completed.push(normalizeInterruptedTurn(cloneTurn(state.active)));
1255
+ const nextId = Math.max(state.nextId, ...completed.map((turn) => turn.id + 1), 1);
1256
+ return {
1257
+ completed: boundedCompleted(completed),
1258
+ nextId,
1259
+ ...state.taskSnapshot === void 0 ? {} : { taskSnapshot: state.taskSnapshot }
1260
+ };
1261
+ }
1262
+ function boundedCompleted(input) {
1263
+ const recent = input.slice(-MAX_TRANSCRIPT_TURNS);
1264
+ let chars = 0;
1265
+ let start = recent.length;
1266
+ for (let index = recent.length - 1; index >= 0; index -= 1) {
1267
+ const turn = recent[index];
1268
+ const size = JSON.stringify(turn).length;
1269
+ if (start < recent.length && chars + size > MAX_TRANSCRIPT_CHARS) break;
1270
+ chars += size;
1271
+ start = index;
1272
+ }
1273
+ return recent.slice(start);
1274
+ }
1275
+ function compactionTurn(compact) {
1276
+ const text = `Original execution steps before ${compact.compactedAt} are unavailable.`;
1277
+ return {
1278
+ id: 1,
1279
+ kind: "compaction",
1280
+ prompt: "Earlier execution history was compacted",
1281
+ assistantText: "",
1282
+ statusLines: [text],
1283
+ blocks: [{
1284
+ kind: "status",
1285
+ id: `compact-boundary:${compact.compactedAt}`,
1286
+ state: "info",
1287
+ tone: "warning",
1288
+ text,
1289
+ details: compact.summary
1290
+ }]
1291
+ };
1292
+ }
1293
+ function applyLegacyToolResult(turn, message2) {
1294
+ const id = message2.toolCallId;
1295
+ const index = id === void 0 ? -1 : turn.blocks.findIndex((block2) => block2.kind === "status" && block2.id === `tool:${id}`);
1296
+ if (index < 0) {
1297
+ return upsertTurnStatus(turn, {
1298
+ kind: "status",
1299
+ id: `orphan-tool:${id ?? turn.blocks.length}`,
1300
+ state: "info",
1301
+ tone: "warning",
1302
+ text: "Orphaned historical tool result",
1303
+ details: modelContentText(message2.content)
1304
+ });
1305
+ }
1306
+ const previous = turn.blocks[index];
1307
+ if (previous.kind !== "status" || previous.tool === void 0) return turn;
1308
+ const result = parseLegacyToolResult(modelContentText(message2.content));
1309
+ const block = {
1310
+ ...previous,
1311
+ state: result.ok ? "completed" : "failed",
1312
+ text: `${result.ok ? "\u2713" : "\xD7"} ${previous.tool.name}`,
1313
+ tool: { ...previous.tool, result }
1314
+ };
1315
+ const blocks = [...turn.blocks];
1316
+ blocks[index] = block;
1317
+ return withBlocks(turn, blocks);
1318
+ }
1319
+ function parseLegacyToolResult(content) {
1320
+ let value = content;
1321
+ try {
1322
+ value = JSON.parse(content);
1323
+ } catch {
1324
+ }
1325
+ if (typeof value === "object" && value !== null && "error" in value) {
1326
+ const candidate = value.error;
1327
+ if (typeof candidate === "object" && candidate !== null) {
1328
+ const error = candidate;
1329
+ return { ok: false, error: {
1330
+ code: typeof error.code === "string" ? error.code : "unknown",
1331
+ message: typeof error.message === "string" ? error.message : content
1332
+ } };
1333
+ }
1334
+ }
1335
+ return { ok: true, output: value };
1336
+ }
1337
+ function normalizeInterruptedTurn(turn) {
1338
+ const blocks = turn.blocks.flatMap((block) => {
1339
+ if (block.kind !== "status") return [block];
1340
+ if (block.activity === "model") return [];
1341
+ if (block.state !== "running") return [block];
1342
+ return [{ ...block, state: "cancelled", text: `\xD7 ${block.text.replace(/^[✓×·]\s*/, "")}` }];
1343
+ });
1344
+ return withBlocks(turn, blocks);
1345
+ }
1346
+ function cloneTurn(turn) {
1347
+ return { ...turn, blocks: turn.blocks.map((block) => ({ ...block })), statusLines: [...turn.statusLines] };
1348
+ }
1349
+ function upsertTurnStatus(turn, block) {
1350
+ const blocks = [...turn.blocks];
1351
+ const index = blocks.findIndex((item) => item.kind === "status" && item.id === block.id);
1352
+ const bounded = boundedTranscriptBlock(block);
1353
+ if (index < 0) blocks.push(bounded);
1354
+ else blocks[index] = bounded;
1355
+ return withBlocks(turn, blocks);
1356
+ }
1357
+ function withBlocks(turn, blocks) {
1358
+ const bounded = blocks.length <= MAX_ACTIVE_TRANSCRIPT_BLOCKS ? blocks : blocks.slice(-MAX_ACTIVE_TRANSCRIPT_BLOCKS);
1359
+ return {
1360
+ ...turn,
1361
+ assistantText: tailText(turn.assistantText, MAX_ACTIVE_ASSISTANT_CHARS),
1362
+ blocks: bounded,
1363
+ statusLines: bounded.filter((block) => block.kind === "status").map((block) => block.text)
1364
+ };
1365
+ }
1366
+ function appendLine(text, line) {
1367
+ return text.length === 0 || text.endsWith("\n") ? text + line : `${text}
1368
+ ${line}`;
1369
+ }
1370
+ function stripRetryBlocks(turn) {
1371
+ const blocks = turn.blocks.filter((block) => {
1372
+ if (block.kind !== "status") return true;
1373
+ if (block.id === "model-retry") return false;
1374
+ if (block.id.startsWith("structured-retry:")) return false;
1375
+ return true;
1376
+ });
1377
+ return withBlocks(turn, blocks);
1378
+ }
1379
+ function addText(turn, text, onNewLine = false) {
1380
+ const assistantText = tailText(
1381
+ onNewLine ? appendLine(turn.assistantText, text) : turn.assistantText + text,
1382
+ MAX_ACTIVE_ASSISTANT_CHARS
1383
+ );
1384
+ const blocks = [...turn.blocks];
1385
+ const last = blocks[blocks.length - 1];
1386
+ if (!onNewLine && last?.kind === "text") {
1387
+ blocks[blocks.length - 1] = { kind: "text", text: tailText(last.text + text, MAX_TRANSCRIPT_BLOCK_TEXT_CHARS) };
1388
+ } else {
1389
+ blocks.push({ kind: "text", text: tailText(text, MAX_TRANSCRIPT_BLOCK_TEXT_CHARS) });
1390
+ }
1391
+ return withBlocks({ ...turn, assistantText }, blocks);
1392
+ }
1393
+ function addStreamingText(turn, text) {
1394
+ const activity = turn.blocks.find((block) => block.kind === "status" && block.activity === "model" && block.state === "running");
1395
+ if (activity === void 0) return addText(turn, text);
1396
+ const withoutActivity = withBlocks(turn, turn.blocks.filter((block) => block !== activity));
1397
+ const withText = addText(withoutActivity, text);
1398
+ const { thinkingText: _thinkingText, ...writingActivity } = activity;
1399
+ return withBlocks(withText, [...withText.blocks, {
1400
+ ...writingActivity,
1401
+ text: "Writing response"
1402
+ }]);
1403
+ }
1404
+ function boundedTranscriptBlock(block) {
1405
+ const presentation = block.presentation === void 0 ? void 0 : boundedTranscriptPresentation(block.presentation);
1406
+ const tool = block.tool === void 0 ? void 0 : {
1407
+ name: block.tool.name,
1408
+ input: boundedTranscriptValue(block.tool.input),
1409
+ ...block.tool.result === void 0 ? {} : {
1410
+ result: {
1411
+ ok: block.tool.result.ok,
1412
+ ...block.tool.result.output === void 0 ? {} : { output: boundedTranscriptValue(block.tool.result.output) },
1413
+ ...block.tool.result.content === void 0 ? {} : {
1414
+ content: tailText(block.tool.result.content, MAX_TRANSCRIPT_TOOL_VALUE_CHARS)
1415
+ },
1416
+ ...block.tool.result.presentation === void 0 ? {} : {
1417
+ presentation: boundedTranscriptPresentation(block.tool.result.presentation)
1418
+ },
1419
+ ...block.tool.result.additionalContext === void 0 ? {} : {
1420
+ additionalContext: block.tool.result.additionalContext.slice(-20).map((item) => tailText(item, MAX_TRANSCRIPT_TOOL_VALUE_CHARS))
1421
+ },
1422
+ ...block.tool.result.error === void 0 ? {} : { error: {
1423
+ code: block.tool.result.error.code,
1424
+ message: tailText(block.tool.result.error.message, MAX_TRANSCRIPT_BLOCK_TEXT_CHARS)
1425
+ } }
1426
+ }
1427
+ }
1428
+ };
1429
+ return {
1430
+ ...block,
1431
+ text: tailText(block.text, MAX_TRANSCRIPT_BLOCK_TEXT_CHARS),
1432
+ ...block.hint === void 0 ? {} : { hint: tailText(block.hint, MAX_TRANSCRIPT_BLOCK_TEXT_CHARS) },
1433
+ ...block.details === void 0 ? {} : { details: tailText(block.details, MAX_TRANSCRIPT_BLOCK_TEXT_CHARS) },
1434
+ ...presentation === void 0 ? {} : { presentation },
1435
+ ...tool === void 0 ? {} : { tool }
1436
+ };
1437
+ }
1438
+ function boundedTranscriptPresentation(presentation) {
1439
+ return jsonSize(presentation) <= MAX_TRANSCRIPT_PRESENTATION_CHARS ? presentation : {
1440
+ kind: "generic",
1441
+ title: "Tool output",
1442
+ summary: "Large presentation omitted from the live transcript; the model-facing result remains available in context."
1443
+ };
1444
+ }
1445
+ function boundedTranscriptValue(value) {
1446
+ const chars = jsonSize(value);
1447
+ if (chars <= MAX_TRANSCRIPT_TOOL_VALUE_CHARS) return value;
1448
+ const record = typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
1449
+ const summary = { truncated: true, originalChars: chars };
1450
+ for (const key of ["path", "command", "id", "name", "exitCode", "bytes", "replacements"]) {
1451
+ const item = record?.[key];
1452
+ if (typeof item === "string") summary[key] = tailText(item, 1e3);
1453
+ else if (typeof item === "number" || typeof item === "boolean" || item === null) summary[key] = item;
1454
+ }
1455
+ return summary;
1456
+ }
1457
+ function jsonSize(value) {
1458
+ try {
1459
+ return JSON.stringify(value)?.length ?? 0;
1460
+ } catch {
1461
+ return Number.POSITIVE_INFINITY;
1462
+ }
1463
+ }
1464
+ function tailText(text, limit) {
1465
+ if (text.length <= limit) return text;
1466
+ const marker = `\u2026 ${text.length - limit} earlier characters omitted \u2026
1467
+ `;
1468
+ return marker.length >= limit ? text.slice(-limit) : marker + text.slice(-(limit - marker.length));
1469
+ }
1470
+ function transcriptCensus(state) {
1471
+ const turns = [...state.completed, ...state.active === void 0 ? [] : [state.active]];
1472
+ return {
1473
+ turns: turns.length,
1474
+ blocks: turns.reduce((total, turn) => total + turn.blocks.length, 0),
1475
+ chars: jsonSize(state)
1476
+ };
1477
+ }
1478
+
1479
+ // src/session/store.ts
1480
+ var SESSION_VERSION = 4;
1481
+ var DEFAULT_MAX_SESSION_BYTES = 5 * 1024 * 1024;
1482
+ var SessionIdSchema = external_exports.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/, "Invalid session id");
1483
+ var IsoDateSchema = external_exports.string().datetime({ offset: true });
1484
+ var ToolCallSchema = external_exports.object({
1485
+ id: external_exports.string().min(1).max(256),
1486
+ name: external_exports.string().min(1).max(256),
1487
+ input: external_exports.unknown()
1488
+ }).strict();
1489
+ var TextContentBlockSchema = external_exports.object({
1490
+ type: external_exports.literal("text"),
1491
+ text: external_exports.string().max(DEFAULT_MAX_SESSION_BYTES)
1492
+ }).strict();
1493
+ var ImageContentBlockSchema = external_exports.object({
1494
+ type: external_exports.literal("image"),
1495
+ source: external_exports.object({ type: external_exports.literal("file"), path: external_exports.string().min(1).max(32768) }).strict(),
1496
+ mediaType: external_exports.enum(["image/png", "image/jpeg", "image/webp"]),
1497
+ sha256: external_exports.string().regex(/^[a-f0-9]{64}$/),
1498
+ bytes: external_exports.number().int().positive().max(20 * 1024 * 1024),
1499
+ name: external_exports.string().min(1).max(255).optional()
1500
+ }).strict();
1501
+ var UserContentSchema = external_exports.union([
1502
+ external_exports.string().max(DEFAULT_MAX_SESSION_BYTES),
1503
+ external_exports.array(external_exports.discriminatedUnion("type", [TextContentBlockSchema, ImageContentBlockSchema])).min(1).max(100)
1504
+ ]);
1505
+ var MessageMetadataShape = {
1506
+ toolCallId: external_exports.string().min(1).max(256).optional(),
1507
+ toolCalls: external_exports.array(ToolCallSchema).max(1e3).optional()
1508
+ };
1509
+ var MessageSchema = external_exports.discriminatedUnion("role", [
1510
+ external_exports.object({ role: external_exports.literal("user"), content: UserContentSchema, ...MessageMetadataShape }).strict(),
1511
+ external_exports.object({ role: external_exports.literal("system"), content: external_exports.string().max(DEFAULT_MAX_SESSION_BYTES), ...MessageMetadataShape }).strict(),
1512
+ external_exports.object({ role: external_exports.literal("assistant"), content: external_exports.string().max(DEFAULT_MAX_SESSION_BYTES), ...MessageMetadataShape }).strict(),
1513
+ external_exports.object({ role: external_exports.literal("tool"), content: external_exports.string().max(DEFAULT_MAX_SESSION_BYTES), ...MessageMetadataShape }).strict()
1514
+ ]);
1515
+ var LegacySummarySchema = external_exports.object({ role: external_exports.literal("system"), content: external_exports.string().max(DEFAULT_MAX_SESSION_BYTES) }).strict();
1516
+ var CompactBoundarySchema = external_exports.object({
1517
+ summary: external_exports.string().min(1).max(DEFAULT_MAX_SESSION_BYTES),
1518
+ compactedAt: IsoDateSchema
1519
+ }).strict();
1520
+ var StateSchema = external_exports.enum(["pending", "running", "completed", "failed", "blocked", "cancelled"]);
1521
+ var MemoryLifecycleSchema = external_exports.object({
1522
+ status: external_exports.enum(["active", "completed", "abandoned"]),
1523
+ taskId: external_exports.string().regex(/^memory-[A-Za-z0-9._-]{1,120}$/).optional(),
1524
+ messageStart: external_exports.number().int().min(0).max(5e4).optional(),
1525
+ finalizedAt: IsoDateSchema.optional(),
1526
+ transcriptHash: external_exports.string().regex(/^[a-f0-9]{64}$/).optional()
1527
+ }).strict();
1528
+ var SessionBaseSchema = external_exports.object({
1529
+ sessionId: SessionIdSchema,
1530
+ createdAt: IsoDateSchema,
1531
+ updatedAt: IsoDateSchema,
1532
+ workspace: external_exports.object({ path: external_exports.string().min(1).max(32768) }).strict(),
1533
+ tasks: external_exports.object({
1534
+ plan: TaskPlanSchema.optional(),
1535
+ graph: TaskGraphSchema.optional(),
1536
+ states: external_exports.record(external_exports.string(), StateSchema),
1537
+ results: external_exports.record(external_exports.string(), SubagentResultSchema)
1538
+ }).strict(),
1539
+ models: external_exports.object({ main: external_exports.string().min(1).max(1024), subagent: external_exports.string().min(1).max(1024) }).strict(),
1540
+ permissionMode: PermissionModeSchema,
1541
+ memory: MemoryLifecycleSchema.optional()
1542
+ }).strict();
1543
+ var SessionDocumentV1Schema = SessionBaseSchema.extend({
1544
+ version: external_exports.literal(1),
1545
+ conversation: external_exports.object({
1546
+ summary: LegacySummarySchema.optional(),
1547
+ messages: external_exports.array(MessageSchema).max(5e4)
1548
+ }).strict()
1549
+ }).strict();
1550
+ var ConversationSchema = external_exports.object({
1551
+ compact: CompactBoundarySchema.optional(),
1552
+ messages: external_exports.array(MessageSchema).max(5e4)
1553
+ }).strict();
1554
+ var SessionDocumentV2Schema = SessionBaseSchema.extend({
1555
+ version: external_exports.literal(2),
1556
+ conversation: ConversationSchema
1557
+ }).strict();
1558
+ var TranscriptStateSchema = external_exports.custom(isTranscriptState, "Invalid transcript state");
1559
+ var TimelineSchema = external_exports.object({
1560
+ version: external_exports.literal(1),
1561
+ state: TranscriptStateSchema
1562
+ }).strict();
1563
+ var ContextEpochSchema = external_exports.object({
1564
+ version: external_exports.literal(1),
1565
+ id: external_exports.string().uuid(),
1566
+ startedAt: IsoDateSchema,
1567
+ stableMessages: external_exports.array(external_exports.object({
1568
+ role: external_exports.literal("system"),
1569
+ content: external_exports.string().max(DEFAULT_MAX_SESSION_BYTES),
1570
+ cacheBreakpoint: external_exports.boolean().optional()
1571
+ }).strict()).max(1e3),
1572
+ sources: external_exports.record(external_exports.string().min(1).max(128), external_exports.string().max(DEFAULT_MAX_SESSION_BYTES)),
1573
+ pinnedSources: external_exports.record(external_exports.string().min(1).max(128), external_exports.string().max(DEFAULT_MAX_SESSION_BYTES)).optional(),
1574
+ stableSourceHash: external_exports.string().regex(/^[a-f0-9]{64}$/)
1575
+ }).strict();
1576
+ var ContextVisibilityRecordSchema = external_exports.object({
1577
+ id: external_exports.string().uuid(),
1578
+ role: external_exports.literal("system"),
1579
+ content: external_exports.string().max(DEFAULT_MAX_SESSION_BYTES),
1580
+ admittedAt: IsoDateSchema,
1581
+ scope: external_exports.literal("run")
1582
+ }).strict();
1583
+ var SessionDocumentV3Schema = SessionBaseSchema.extend({
1584
+ version: external_exports.literal(3),
1585
+ conversation: ConversationSchema,
1586
+ timeline: TimelineSchema
1587
+ }).strict();
1588
+ var SessionDocumentSchema = SessionBaseSchema.extend({
1589
+ version: external_exports.literal(SESSION_VERSION),
1590
+ conversation: external_exports.object({
1591
+ compact: CompactBoundarySchema.optional(),
1592
+ epoch: ContextEpochSchema.optional(),
1593
+ visibilityLog: external_exports.array(ContextVisibilityRecordSchema).max(1e4).optional(),
1594
+ messages: external_exports.array(MessageSchema).max(5e4)
1595
+ }).strict(),
1596
+ timeline: TimelineSchema
1597
+ }).strict();
1598
+ var INVALID_SESSION_LOCK_STALE_MS = 3e4;
1599
+ var SessionStore = class {
1600
+ #workspace;
1601
+ #sessions;
1602
+ #maxBytes;
1603
+ #maxSessions;
1604
+ constructor(options) {
1605
+ this.#workspace = resolve(options.workspace);
1606
+ this.#sessions = join(this.#workspace, ".flavor", "sessions");
1607
+ this.#maxBytes = options.maxBytes ?? DEFAULT_MAX_SESSION_BYTES;
1608
+ if (!Number.isSafeInteger(this.#maxBytes) || this.#maxBytes < 256) throw new Error("maxBytes must be an integer of at least 256");
1609
+ this.#maxSessions = options.maxSessions ?? 50;
1610
+ if (!Number.isSafeInteger(this.#maxSessions) || this.#maxSessions < 1) throw new Error("maxSessions must be an integer of at least 1");
1611
+ }
1612
+ async save(input) {
1613
+ const document = SessionDocumentSchema.parse(sanitize(input));
1614
+ await this.#assertWorkspace(document.workspace.path);
1615
+ await this.#prepareDirectory();
1616
+ const target = this.#path(document.sessionId);
1617
+ const { conversation: { messages, compact, epoch, visibilityLog }, timeline, ...meta } = document;
1618
+ const metaLine = JSON.stringify({
1619
+ __meta: true,
1620
+ ...meta,
1621
+ ...compact === void 0 ? {} : { compact },
1622
+ ...epoch === void 0 ? {} : { epoch },
1623
+ ...visibilityLog === void 0 ? {} : { visibilityLog },
1624
+ timeline: {
1625
+ version: timeline.version,
1626
+ nextId: timeline.state.nextId,
1627
+ ...timeline.state.taskSnapshot === void 0 ? {} : { taskSnapshot: timeline.state.taskSnapshot }
1628
+ }
1629
+ });
1630
+ const timelineLines = [
1631
+ ...timeline.state.completed.map((turn) => JSON.stringify({ __timeline: true, turn })),
1632
+ ...timeline.state.active === void 0 ? [] : [JSON.stringify({ __timeline: true, active: timeline.state.active })]
1633
+ ];
1634
+ const lines = [metaLine, ...messages.map((message2) => JSON.stringify(message2)), ...timelineLines];
1635
+ const body = `${lines.join("\n")}
1636
+ `;
1637
+ if (Buffer.byteLength(body) > this.#maxBytes) throw new Error(`Session exceeds maximum size of ${this.#maxBytes} bytes`);
1638
+ const temporary = join(this.#sessions, `.${document.sessionId}.${process.pid}.${randomUUID()}.tmp`);
1639
+ let handle;
1640
+ try {
1641
+ handle = await open(temporary, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, 384);
1642
+ await handle.writeFile(body, "utf8");
1643
+ await handle.sync();
1644
+ await handle.close();
1645
+ handle = void 0;
1646
+ await rename(temporary, target);
1647
+ } finally {
1648
+ await handle?.close().catch(() => void 0);
1649
+ await rm(temporary, { force: true }).catch(() => void 0);
1650
+ }
1651
+ await this.#prune().catch(() => void 0);
1652
+ }
1653
+ /** Hold this lease for the full lifetime of a writable session runtime. */
1654
+ async acquireLease(sessionId) {
1655
+ SessionIdSchema.parse(sessionId);
1656
+ await this.#prepareDirectory();
1657
+ const path = join(this.#sessions, `.${sessionId}.lock`);
1658
+ const token = JSON.stringify({ pid: process.pid, nonce: randomUUID(), createdAt: (/* @__PURE__ */ new Date()).toISOString() });
1659
+ for (let attempt = 0; attempt < 3; attempt += 1) {
1660
+ let handle;
1661
+ let created = false;
1662
+ try {
1663
+ handle = await open(path, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, 384);
1664
+ created = true;
1665
+ await handle.writeFile(token, "utf8");
1666
+ await handle.sync();
1667
+ await handle.close();
1668
+ handle = void 0;
1669
+ let released = false;
1670
+ return {
1671
+ sessionId,
1672
+ release: async () => {
1673
+ if (released) return;
1674
+ try {
1675
+ if (await readFile(path, "utf8") === token) await rm(path, { force: true });
1676
+ } catch (error) {
1677
+ if (!isCode(error, "ENOENT")) throw error;
1678
+ }
1679
+ released = true;
1680
+ }
1681
+ };
1682
+ } catch (error) {
1683
+ await handle?.close().catch(() => void 0);
1684
+ if (created) await rm(path, { force: true }).catch(() => void 0);
1685
+ if (!isCode(error, "EEXIST")) throw error;
1686
+ const owner = await reclaimStaleSessionLock(path);
1687
+ if (owner !== void 0) {
1688
+ throw new Error(`Session "${sessionId}" is already active in process ${owner}. Close that session before resuming it here.`);
1689
+ }
1690
+ }
1691
+ }
1692
+ throw new Error(`Session "${sessionId}" could not acquire its writer lease because the lock kept changing`);
1693
+ }
1694
+ async load(sessionId) {
1695
+ if (sessionId === void 0) {
1696
+ const [latest] = await this.list();
1697
+ if (latest === void 0) throw new Error("No saved sessions exist in this workspace");
1698
+ sessionId = latest.sessionId;
1699
+ }
1700
+ SessionIdSchema.parse(sessionId);
1701
+ await this.#assertSafeExistingDirectory();
1702
+ const path = this.#path(sessionId);
1703
+ let raw;
1704
+ try {
1705
+ raw = await boundedRead(path, this.#maxBytes);
1706
+ } catch (error) {
1707
+ if (isCode(error, "ENOENT")) throw new Error(`Session "${sessionId}" was not found in this workspace`);
1708
+ throw error;
1709
+ }
1710
+ let parsed;
1711
+ try {
1712
+ parsed = this.#parseSession(raw);
1713
+ } catch (error) {
1714
+ const quarantine = await this.#quarantine(path);
1715
+ throw new Error(`Session "${sessionId}" is corrupt or incompatible and was quarantined as ${basename(quarantine)}: ${message(error)}`);
1716
+ }
1717
+ await this.#assertWorkspace(parsed.workspace.path);
1718
+ const storedVersion = storedSessionVersion(raw);
1719
+ if (storedVersion < SESSION_VERSION) await this.#backupLegacy(path, raw, storedVersion);
1720
+ return normalizeAbandonedTasks(parsed);
1721
+ }
1722
+ #parseSession(raw) {
1723
+ const trimmed = raw.trim();
1724
+ if (trimmed.length === 0) throw new Error("Empty session file");
1725
+ const firstLine = trimmed.split("\n", 1)[0] ?? "";
1726
+ let candidate;
1727
+ const first = JSON.parse(firstLine);
1728
+ if (first.__meta === true) {
1729
+ const meta = { ...first };
1730
+ const records = trimmed.split("\n").slice(1).filter((line) => line.length > 0).map((line) => JSON.parse(line));
1731
+ const messages = records.filter((record) => !isTimelineRecord(record));
1732
+ const timelineRecords = records.filter(isTimelineRecord);
1733
+ const summary = meta.summary;
1734
+ const compact = meta.compact;
1735
+ const epoch = meta.epoch;
1736
+ const visibilityLog = meta.visibilityLog;
1737
+ const timeline = meta.timeline;
1738
+ delete meta.__meta;
1739
+ delete meta.summary;
1740
+ delete meta.compact;
1741
+ delete meta.epoch;
1742
+ delete meta.visibilityLog;
1743
+ delete meta.timeline;
1744
+ candidate = {
1745
+ ...meta,
1746
+ conversation: {
1747
+ ...meta.version === 1 && summary !== void 0 ? { summary } : {},
1748
+ ...(meta.version === 2 || meta.version === 3 || meta.version === SESSION_VERSION) && compact !== void 0 ? { compact } : {},
1749
+ ...meta.version === SESSION_VERSION && epoch !== void 0 ? { epoch } : {},
1750
+ ...meta.version === SESSION_VERSION && visibilityLog !== void 0 ? { visibilityLog } : {},
1751
+ messages
1752
+ },
1753
+ ...meta.version === 3 || meta.version === SESSION_VERSION ? {
1754
+ timeline: timelineFromRecords(timeline, timelineRecords)
1755
+ } : {}
1756
+ };
1757
+ } else {
1758
+ candidate = JSON.parse(trimmed);
1759
+ }
1760
+ return parseAndMigrateSession(candidate);
1761
+ }
1762
+ async list() {
1763
+ try {
1764
+ await this.#assertSafeExistingDirectory();
1765
+ } catch (error) {
1766
+ if (isCode(error, "ENOENT")) return [];
1767
+ throw error;
1768
+ }
1769
+ const names = (await readdir(this.#sessions)).filter((name) => name.endsWith(".jsonl") && !name.endsWith(".events.jsonl")).sort();
1770
+ const entries = [];
1771
+ for (const name of names) {
1772
+ const id = name.slice(0, -6);
1773
+ if (!SessionIdSchema.safeParse(id).success) continue;
1774
+ try {
1775
+ const document = await this.load(id);
1776
+ entries.push({ sessionId: document.sessionId, createdAt: document.createdAt, updatedAt: document.updatedAt, mainModel: document.models.main });
1777
+ } catch (error) {
1778
+ if (!/corrupt|quarantined/i.test(message(error))) throw error;
1779
+ }
1780
+ }
1781
+ return entries.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt) || a.sessionId.localeCompare(b.sessionId));
1782
+ }
1783
+ async delete(sessionId) {
1784
+ SessionIdSchema.parse(sessionId);
1785
+ try {
1786
+ await this.#assertSafeExistingDirectory();
1787
+ } catch (error) {
1788
+ if (isCode(error, "ENOENT")) return;
1789
+ throw error;
1790
+ }
1791
+ const owner = await reclaimStaleSessionLock(join(this.#sessions, `.${sessionId}.lock`));
1792
+ if (owner !== void 0) throw new Error(`Session "${sessionId}" is active in process ${owner} and cannot be deleted`);
1793
+ const path = this.#path(sessionId);
1794
+ try {
1795
+ const metadata = await lstat(path);
1796
+ if (!metadata.isFile()) throw new Error("Session path is not a regular file");
1797
+ await rm(path);
1798
+ await rm(join(this.#sessions, `${sessionId}.events.jsonl`), { force: true }).catch(() => void 0);
1799
+ } catch (error) {
1800
+ if (!isCode(error, "ENOENT")) throw error;
1801
+ }
1802
+ }
1803
+ #path(sessionId) {
1804
+ SessionIdSchema.parse(sessionId);
1805
+ const path = resolve(this.#sessions, `${sessionId}.jsonl`);
1806
+ if (!isWithin(this.#sessions, path)) throw new Error("Invalid session id");
1807
+ return path;
1808
+ }
1809
+ async #prune() {
1810
+ const entries = await this.list();
1811
+ if (entries.length <= this.#maxSessions) return;
1812
+ let retained = 0;
1813
+ for (const entry of entries) {
1814
+ const owner = await reclaimStaleSessionLock(join(this.#sessions, `.${entry.sessionId}.lock`));
1815
+ if (owner !== void 0) continue;
1816
+ if (retained < this.#maxSessions) {
1817
+ retained += 1;
1818
+ continue;
1819
+ }
1820
+ await rm(this.#path(entry.sessionId), { force: true }).catch(() => void 0);
1821
+ await rm(join(this.#sessions, `${entry.sessionId}.events.jsonl`), { force: true }).catch(() => void 0);
1822
+ }
1823
+ }
1824
+ async #assertWorkspace(stored) {
1825
+ const expected = await canonical(this.#workspace);
1826
+ const actual = await canonical(stored);
1827
+ if (process.platform === "win32" ? expected.toLowerCase() !== actual.toLowerCase() : expected !== actual) {
1828
+ throw new Error(`Session belongs to a different workspace: ${stored}. Resume it from that workspace.`);
1829
+ }
1830
+ }
1831
+ async #prepareDirectory() {
1832
+ await assertNoSymlink(this.#workspace, dirname(this.#sessions));
1833
+ await mkdir(this.#sessions, { recursive: true, mode: 448 });
1834
+ await this.#assertSafeExistingDirectory();
1835
+ }
1836
+ async #assertSafeExistingDirectory() {
1837
+ await assertNoSymlink(this.#workspace, this.#sessions);
1838
+ const canonicalRoot = await realpath(this.#workspace);
1839
+ const canonicalSessions = await realpath(this.#sessions);
1840
+ if (!isWithin(canonicalRoot, canonicalSessions)) throw new Error("Session directory escapes the workspace");
1841
+ }
1842
+ async #quarantine(path) {
1843
+ const target = `${path}.corrupt-${Date.now()}-${randomUUID().slice(0, 8)}`;
1844
+ await rename(path, target);
1845
+ return target;
1846
+ }
1847
+ async #backupLegacy(path, raw, version) {
1848
+ const target = `${path}.v${version}.bak`;
1849
+ let handle;
1850
+ try {
1851
+ handle = await open(target, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, 384);
1852
+ await handle.writeFile(raw, "utf8");
1853
+ await handle.sync();
1854
+ } catch (error) {
1855
+ if (!isCode(error, "EEXIST")) throw error;
1856
+ } finally {
1857
+ await handle?.close().catch(() => void 0);
1858
+ }
1859
+ }
1860
+ };
1861
+ function storedSessionVersion(raw) {
1862
+ const trimmed = raw.trim();
1863
+ const first = JSON.parse(trimmed.split("\n", 1)[0] ?? "{}");
1864
+ const candidate = first.__meta === true ? first.version : JSON.parse(trimmed).version;
1865
+ return typeof candidate === "number" && Number.isInteger(candidate) ? candidate : 0;
1866
+ }
1867
+ async function reclaimStaleSessionLock(path) {
1868
+ try {
1869
+ const token = await readFile(path, "utf8");
1870
+ const metadata = await stat(path);
1871
+ const owner = parseSessionLockOwner(token);
1872
+ if (owner !== void 0 && isProcessAlive(owner)) return owner;
1873
+ if (owner === void 0 && Date.now() - metadata.mtimeMs <= INVALID_SESSION_LOCK_STALE_MS) {
1874
+ throw new Error(`Session writer lock ${path} is incomplete or invalid; retry after the owning process exits`);
1875
+ }
1876
+ if (await readFile(path, "utf8") !== token) return void 0;
1877
+ const stale = `${path}.stale-${process.pid}-${randomUUID()}`;
1878
+ try {
1879
+ await rename(path, stale);
1880
+ await rm(stale, { force: true });
1881
+ } catch (error) {
1882
+ if (!isCode(error, "ENOENT")) throw error;
1883
+ }
1884
+ return void 0;
1885
+ } catch (error) {
1886
+ if (isCode(error, "ENOENT")) return void 0;
1887
+ throw error;
1888
+ }
1889
+ }
1890
+ function parseSessionLockOwner(token) {
1891
+ try {
1892
+ const value = JSON.parse(token);
1893
+ if (typeof value !== "object" || value === null || !("pid" in value)) return void 0;
1894
+ const pid = value.pid;
1895
+ return typeof pid === "number" && Number.isSafeInteger(pid) && pid > 0 ? pid : void 0;
1896
+ } catch {
1897
+ return void 0;
1898
+ }
1899
+ }
1900
+ function isProcessAlive(pid) {
1901
+ try {
1902
+ process.kill(pid, 0);
1903
+ return true;
1904
+ } catch (error) {
1905
+ return isCode(error, "EPERM");
1906
+ }
1907
+ }
1908
+ async function boundedRead(path, maxBytes) {
1909
+ const metadata = await stat(path);
1910
+ if (!metadata.isFile()) throw new Error("Session path is not a regular file");
1911
+ if (metadata.size > maxBytes) throw new Error(`Session exceeds maximum read size of ${maxBytes} bytes`);
1912
+ const value = await readFile(path);
1913
+ if (value.byteLength > maxBytes) throw new Error(`Session exceeds maximum read size of ${maxBytes} bytes`);
1914
+ return value.toString("utf8");
1915
+ }
1916
+ function normalizeAbandonedTasks(document) {
1917
+ const plan = document.tasks.plan === void 0 ? void 0 : normalizeAbandonedPlan(document.tasks.plan);
1918
+ const states = { ...document.tasks.states };
1919
+ const results = { ...document.tasks.results };
1920
+ const nodes = new Map(document.tasks.graph?.nodes.map((node) => [node.id, node]) ?? []);
1921
+ for (const [id, state] of Object.entries(states)) {
1922
+ if (state !== "running") continue;
1923
+ const node = nodes.get(id);
1924
+ const failedDependency = node?.dependencies.some((dependency) => ["failed", "blocked"].includes(states[dependency] ?? "pending")) ?? false;
1925
+ states[id] = failedDependency ? "failed" : "pending";
1926
+ if (failedDependency && results[id] === void 0) results[id] = {
1927
+ taskId: id,
1928
+ status: "failed",
1929
+ summary: "Recovered as failed because a dependency did not complete",
1930
+ filesChanged: [],
1931
+ commandsRun: [],
1932
+ verification: [],
1933
+ artifacts: [],
1934
+ risks: ["Execution was abandoned"],
1935
+ suggestedNextSteps: []
1936
+ };
1937
+ }
1938
+ const restoredTimeline = restoreTranscriptState(document.timeline.state);
1939
+ const priorSnapshot = restoredTimeline.taskSnapshot;
1940
+ const { taskSnapshot: _discardedTaskSnapshot, ...timelineWithoutTaskSnapshot } = restoredTimeline;
1941
+ const hasTaskSnapshot = plan !== void 0 || document.tasks.graph !== void 0 || Object.keys(states).length > 0;
1942
+ const timelineState = {
1943
+ ...timelineWithoutTaskSnapshot,
1944
+ ...hasTaskSnapshot ? {
1945
+ taskSnapshot: {
1946
+ ...plan === void 0 ? {} : { plan },
1947
+ subagents: {
1948
+ ...document.tasks.graph === void 0 ? {} : { graph: document.tasks.graph },
1949
+ states: { ...states },
1950
+ ...priorSnapshot?.subagents.elapsedMs === void 0 ? {} : { elapsedMs: priorSnapshot.subagents.elapsedMs }
1951
+ }
1952
+ }
1953
+ } : {}
1954
+ };
1955
+ return {
1956
+ ...document,
1957
+ timeline: { ...document.timeline, state: timelineState },
1958
+ tasks: {
1959
+ ...document.tasks,
1960
+ ...plan === void 0 ? {} : { plan },
1961
+ states,
1962
+ results
1963
+ }
1964
+ };
1965
+ }
1966
+ function parseAndMigrateSession(input) {
1967
+ if (typeof input !== "object" || input === null || !("version" in input)) throw new Error("Session version is missing");
1968
+ if (input.version === SESSION_VERSION) return SessionDocumentSchema.parse(input);
1969
+ if (input.version === 3) {
1970
+ const legacy2 = SessionDocumentV3Schema.parse(input);
1971
+ return SessionDocumentSchema.parse({ ...legacy2, version: SESSION_VERSION });
1972
+ }
1973
+ if (input.version === 2) {
1974
+ const legacy2 = SessionDocumentV2Schema.parse(input);
1975
+ return SessionDocumentSchema.parse({
1976
+ ...legacy2,
1977
+ version: SESSION_VERSION,
1978
+ timeline: {
1979
+ version: 1,
1980
+ state: transcriptFromLegacyConversation(legacy2.conversation.messages.filter((message2) => message2.role !== "system"), legacy2.conversation.compact)
1981
+ }
1982
+ });
1983
+ }
1984
+ if (input.version !== 1) throw new Error(`Unsupported session version: ${String(input.version)}`);
1985
+ const legacy = SessionDocumentV1Schema.parse(input);
1986
+ const { conversation, ...metadata } = legacy;
1987
+ const prefix = "Conversation summary\n";
1988
+ const summary = conversation.summary?.content.startsWith(prefix) ? conversation.summary.content.slice(prefix.length).trim() : void 0;
1989
+ const compact = summary === void 0 || summary.length === 0 ? void 0 : { summary, compactedAt: legacy.updatedAt };
1990
+ return SessionDocumentSchema.parse({
1991
+ ...metadata,
1992
+ version: SESSION_VERSION,
1993
+ conversation: {
1994
+ ...compact === void 0 ? {} : { compact },
1995
+ messages: conversation.messages
1996
+ },
1997
+ timeline: {
1998
+ version: 1,
1999
+ state: transcriptFromLegacyConversation(conversation.messages.filter((message2) => message2.role !== "system"), compact)
2000
+ }
2001
+ });
2002
+ }
2003
+ function isTimelineRecord(value) {
2004
+ return typeof value === "object" && value !== null && value.__timeline === true;
2005
+ }
2006
+ function timelineFromRecords(header, records) {
2007
+ const value = typeof header === "object" && header !== null ? header : {};
2008
+ const completed = records.filter((record) => "turn" in record).map((record) => record.turn);
2009
+ const activeRecords = records.filter((record) => "active" in record);
2010
+ if (activeRecords.length > 1) throw new Error("Session contains multiple active timeline turns");
2011
+ return {
2012
+ version: value.version,
2013
+ state: {
2014
+ completed,
2015
+ ...activeRecords.length === 0 ? {} : { active: activeRecords[0].active },
2016
+ nextId: value.nextId,
2017
+ ...value.taskSnapshot === void 0 ? {} : { taskSnapshot: value.taskSnapshot }
2018
+ }
2019
+ };
2020
+ }
2021
+ function isTranscriptState(value) {
2022
+ if (!isRecord(value) || !Array.isArray(value.completed) || !value.completed.every(isTranscriptTurn)) return false;
2023
+ if (!Number.isSafeInteger(value.nextId) || value.nextId < 1) return false;
2024
+ if (value.active !== void 0 && !isTranscriptTurn(value.active)) return false;
2025
+ return value.taskSnapshot === void 0 || isRecord(value.taskSnapshot);
2026
+ }
2027
+ function isTranscriptTurn(value) {
2028
+ if (!isRecord(value)) return false;
2029
+ if (!Number.isSafeInteger(value.id) || value.id < 1) return false;
2030
+ if (value.kind !== void 0 && value.kind !== "compaction") return false;
2031
+ if (value.submittedAt !== void 0 && typeof value.submittedAt !== "string") return false;
2032
+ if (value.completedAt !== void 0 && typeof value.completedAt !== "string") return false;
2033
+ if (typeof value.prompt !== "string" || typeof value.assistantText !== "string") return false;
2034
+ if (!Array.isArray(value.statusLines) || !value.statusLines.every((line) => typeof line === "string")) return false;
2035
+ if (!Array.isArray(value.blocks) || !value.blocks.every(isTranscriptBlock)) return false;
2036
+ return true;
2037
+ }
2038
+ function isTranscriptBlock(value) {
2039
+ if (!isRecord(value)) return false;
2040
+ if (value.kind === "text") return typeof value.text === "string";
2041
+ if (value.kind !== "status") return false;
2042
+ if (typeof value.id !== "string" || typeof value.text !== "string") return false;
2043
+ if (!["running", "completed", "failed", "cancelled", "info"].includes(String(value.state))) return false;
2044
+ if (value.tool !== void 0) {
2045
+ if (!isRecord(value.tool) || typeof value.tool.name !== "string" || !("input" in value.tool)) return false;
2046
+ if (value.tool.result !== void 0 && !isRecord(value.tool.result)) return false;
2047
+ }
2048
+ return value.details === void 0 || typeof value.details === "string";
2049
+ }
2050
+ function isRecord(value) {
2051
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2052
+ }
2053
+ async function assertNoSymlink(root, target) {
2054
+ let cursor = resolve(target);
2055
+ const stop = resolve(root);
2056
+ if (!isWithin(stop, cursor)) throw new Error("Session path escapes the workspace");
2057
+ const chain = [];
2058
+ while (cursor !== stop) {
2059
+ chain.push(cursor);
2060
+ cursor = dirname(cursor);
2061
+ }
2062
+ for (const path of chain.reverse()) {
2063
+ try {
2064
+ if ((await lstat(path)).isSymbolicLink()) throw new Error(`Session path contains a symbolic link: ${path}`);
2065
+ } catch (error) {
2066
+ if (!isCode(error, "ENOENT")) throw error;
2067
+ }
2068
+ }
2069
+ }
2070
+ function isWithin(root, path) {
2071
+ const value = relative(resolve(root), resolve(path));
2072
+ return value === "" || !value.startsWith(`..${sep}`) && value !== ".." && !value.includes(":");
2073
+ }
2074
+ async function canonical(path) {
2075
+ try {
2076
+ return await realpath(resolve(path));
2077
+ } catch {
2078
+ return resolve(path);
2079
+ }
2080
+ }
2081
+ function isCode(error, code) {
2082
+ return typeof error === "object" && error !== null && "code" in error && error.code === code;
2083
+ }
2084
+ function sanitize(input, seen = /* @__PURE__ */ new WeakSet()) {
2085
+ if (typeof input === "string") {
2086
+ if (/\b(?:authorization|api[_ -]?key|provider[_ -]?key|access[_ -]?token|secret)\s*[:=]/i.test(input)) return "[redacted]";
2087
+ return input.replace(/\bsk-[A-Za-z0-9_-]+\b/g, "[redacted]");
2088
+ }
2089
+ if (Array.isArray(input)) return input.map((item) => sanitize(item, seen));
2090
+ if (typeof input !== "object" || input === null) return input;
2091
+ if (seen.has(input)) return "[redacted circular value]";
2092
+ seen.add(input);
2093
+ const output = {};
2094
+ for (const [key, value] of Object.entries(input)) {
2095
+ if (/^(?:authorization|proxy-authorization|api[_-]?key|provider[_-]?key|access[_-]?token|secret)$/i.test(key)) continue;
2096
+ output[key] = sanitize(value, seen);
2097
+ }
2098
+ seen.delete(input);
2099
+ return output;
2100
+ }
2101
+
2102
+ export {
2103
+ TaskGraphSchema,
2104
+ TaskPlanner,
2105
+ modelContentText,
2106
+ modelContentTranscriptText,
2107
+ cloneModelContent,
2108
+ normalizeProviderError,
2109
+ createTaskOutputTool,
2110
+ withTimeout,
2111
+ awaitWithSignal,
2112
+ SubagentResultSchema,
2113
+ subagentResultFromTaskOutput,
2114
+ parseFinalSubagentMessage,
2115
+ SubagentScheduler,
2116
+ TaskPlanSchema,
2117
+ TaskUpdateInputSchema,
2118
+ updatePlanTask,
2119
+ activateNextReadyTask,
2120
+ charWidth,
2121
+ thinkingWindow,
2122
+ createTranscriptState,
2123
+ transcriptReducer,
2124
+ restoreTranscriptState,
2125
+ transcriptCensus,
2126
+ SESSION_VERSION,
2127
+ DEFAULT_MAX_SESSION_BYTES,
2128
+ SessionDocumentSchema,
2129
+ SessionStore
2130
+ };