flavor-code 1.4.0-beta.3 → 1.4.0-beta.5

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 (37) hide show
  1. package/dist/agent/loop.d.ts +4 -2
  2. package/dist/{app-H7RYDMD3.js → app-SXNJDSI2.js} +3 -3
  3. package/dist/{chunk-NIKCPCJG.js → chunk-J7TLT6LR.js} +1098 -366
  4. package/dist/{chunk-3BHKW5A7.js → chunk-JZVC4477.js} +8 -2
  5. package/dist/{chunk-KBHWS7M6.js → chunk-KQMKY7C5.js} +4 -0
  6. package/dist/chunk-N5MXHJKD.js +107 -0
  7. package/dist/cli-main.js +20 -13
  8. package/dist/cli.js +51 -10
  9. package/dist/config/schema.d.ts +4 -0
  10. package/dist/context/manager.d.ts +9 -0
  11. package/dist/desktop/main.js +1442 -625
  12. package/dist/desktop-renderer/assets/index-nR67Z0XT.js +154 -0
  13. package/dist/desktop-renderer/assets/{interactive-terminal-j52Ty3QO.js → interactive-terminal-D2J4feTJ.js} +1 -1
  14. package/dist/desktop-renderer/index.html +1 -1
  15. package/dist/goal/classifier.d.ts +2 -0
  16. package/dist/goal/orchestrator.d.ts +23 -2
  17. package/dist/goal/planner.d.ts +2 -0
  18. package/dist/goal/store.d.ts +2 -0
  19. package/dist/goal/types.d.ts +14 -0
  20. package/dist/harness/journal.d.ts +2 -0
  21. package/dist/jobs/registry.d.ts +6 -0
  22. package/dist/{load-SLSL3IWR.js → load-F4AYLCF6.js} +1 -1
  23. package/dist/loop/orchestrator.d.ts +20 -0
  24. package/dist/loop/types.d.ts +7 -0
  25. package/dist/loop/verifier.d.ts +2 -0
  26. package/dist/production.d.ts +17 -0
  27. package/dist/sdk/index.js +4 -4
  28. package/dist/session/tree.d.ts +2 -0
  29. package/dist/ui/session.d.ts +11 -0
  30. package/dist/ui/transcript.d.ts +9 -0
  31. package/dist/utils/abort.d.ts +16 -0
  32. package/dist/utils/heap.d.ts +58 -0
  33. package/dist/utils/memory-restart.d.ts +42 -3
  34. package/dist/utils/rotation-census.d.ts +36 -0
  35. package/package.json +1 -1
  36. package/dist/chunk-CIVPKHF4.js +0 -53
  37. package/dist/desktop-renderer/assets/index-CYd5JkTR.js +0 -154
@@ -1,8 +1,8 @@
1
1
  // src/desktop/main.ts
2
2
  import { appendFileSync } from "fs";
3
3
  import { createHash as createHash28, randomUUID as randomUUID29 } from "crypto";
4
- import { readFile as readFile40, stat as stat19, writeFile as writeFile19 } from "fs/promises";
5
- import { basename as basename12, dirname as dirname23, join as join50, resolve as resolve43 } from "path";
4
+ import { readFile as readFile41, stat as stat20, writeFile as writeFile19 } from "fs/promises";
5
+ import { basename as basename12, dirname as dirname23, join as join51, resolve as resolve43 } from "path";
6
6
  import { fileURLToPath as fileURLToPath3 } from "url";
7
7
  import { app, BrowserWindow as BrowserWindow2, clipboard, dialog, ipcMain, Menu, nativeImage, Notification, shell } from "electron";
8
8
 
@@ -303,6 +303,10 @@ var FlavorConfigSchema = z2.object({
303
303
  maxTokens: z2.number().int().positive().max(Number.MAX_SAFE_INTEGER).default(5e5),
304
304
  isolation: z2.literal("auto").default("auto")
305
305
  }).prefault({}),
306
+ goal: z2.object({
307
+ maxRounds: z2.number().int().positive().max(1e4).default(100),
308
+ maxStallStreak: z2.number().int().positive().max(100).default(3)
309
+ }).prefault({}),
306
310
  context: z2.object({
307
311
  windowTokens: z2.number().int().positive().default(2e5),
308
312
  reservedOutputTokens: z2.number().int().nonnegative().default(2e4),
@@ -4225,11 +4229,11 @@ ${summarizeReport(report2)}`).join("\n\n")
4225
4229
  }
4226
4230
 
4227
4231
  // src/production.ts
4228
- import { readFile as readFile28 } from "fs/promises";
4229
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
4232
+ import { readFile as readFile29 } from "fs/promises";
4233
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync3, renameSync as renameSync2, unlinkSync as unlinkSync3, writeFileSync as writeFileSync3 } from "fs";
4230
4234
  import { createHash as createHash21, randomUUID as randomUUID25 } from "crypto";
4231
4235
  import { homedir as homedir3, release as osRelease, tmpdir as tmpdir4, version as osVersion } from "os";
4232
- import { basename as basename9, dirname as dirname22, join as join37, relative as relative25, resolve as resolve36, sep as sep25 } from "path";
4236
+ import { basename as basename9, dirname as dirname22, join as join38, relative as relative25, resolve as resolve36, sep as sep25 } from "path";
4233
4237
 
4234
4238
  // src/agent/subagents.ts
4235
4239
  import { z as z10 } from "zod";
@@ -5572,6 +5576,18 @@ var ContextManager = class _ContextManager {
5572
5576
  estimatedTokens() {
5573
5577
  return estimateMessageTokens(this.messagesForModel());
5574
5578
  }
5579
+ /** Retained sizes for the rotation census: what this context holds right now. */
5580
+ census() {
5581
+ const chars = (messages) => messages.reduce((sum, entry2) => sum + modelContentText(entry2.content).length, 0);
5582
+ return {
5583
+ messages: this.#messages.length,
5584
+ messageChars: chars(this.#messages),
5585
+ visibilityEntries: this.#visibilityLog.length,
5586
+ visibilityChars: this.#visibilityLog.reduce((sum, record3) => sum + record3.content.length, 0),
5587
+ transientEntries: this.#activeTransientSystem.size,
5588
+ transientChars: chars([...this.#activeTransientSystem.values()])
5589
+ };
5590
+ }
5575
5591
  needsCompaction() {
5576
5592
  if (modelVisibleText(this.messagesForModel()).length >= this.#compactAtChars) return true;
5577
5593
  return calculateContextPressure(this.#currentTokenUsage(), this.#compaction).shouldAutoCompact;
@@ -5612,10 +5628,6 @@ var ContextManager = class _ContextManager {
5612
5628
  }
5613
5629
  return microcompact.changed;
5614
5630
  }
5615
- if (this.#consecutiveAutoCompactFailures >= 3) {
5616
- rollbackMicrocompact();
5617
- return false;
5618
- }
5619
5631
  try {
5620
5632
  const compacted = await this.#compactConversation(signal);
5621
5633
  if (compacted) {
@@ -6079,10 +6091,10 @@ function within(root, candidate) {
6079
6091
 
6080
6092
  // src/context/summarizer.ts
6081
6093
  async function summarizeWithModel(options) {
6082
- const maxAttempts = options.maxPromptTooLongAttempts ?? 3;
6083
- if (!Number.isInteger(maxAttempts) || maxAttempts <= 0) throw new Error("maxPromptTooLongAttempts must be positive");
6084
6094
  options.signal?.throwIfAborted();
6085
6095
  let groups = groupMessagesByApiRound(options.messages);
6096
+ const maxAttempts = options.maxPromptTooLongAttempts ?? Math.max(1, Math.ceil(Math.log2(Math.max(1, groups.length))) + 1);
6097
+ if (!Number.isInteger(maxAttempts) || maxAttempts <= 0) throw new Error("maxPromptTooLongAttempts must be positive");
6086
6098
  let lastOverflow;
6087
6099
  let progress = 20;
6088
6100
  reportProgress(options.onProgress, progress);
@@ -6123,7 +6135,7 @@ async function summarizeWithModel(options) {
6123
6135
  if (terminalError.code !== "context_overflow") throw terminalError;
6124
6136
  lastOverflow = terminalError;
6125
6137
  if (attempt >= maxAttempts || groups.length <= 1) throw terminalError;
6126
- groups = groups.slice(1);
6138
+ groups = groups.slice(Math.max(1, Math.floor(groups.length / 2)));
6127
6139
  }
6128
6140
  throw lastOverflow ?? new Error("Compact summary failed");
6129
6141
  }
@@ -6140,8 +6152,75 @@ function reportProgress(callback, percentage) {
6140
6152
  }
6141
6153
  }
6142
6154
 
6143
- // src/agent/loop.ts
6155
+ // src/utils/heap.ts
6144
6156
  import { getHeapStatistics } from "v8";
6157
+ import { totalmem } from "os";
6158
+ var MEMORY_PRESSURE_HEAP_RATIO = 0.8;
6159
+ var MEMORY_ROTATION_HEAP_RATIO = 0.6;
6160
+ var MEMORY_PRESSURE_RSS_RATIO = 0.75;
6161
+ var MEMORY_ROTATION_RSS_RATIO = 0.6;
6162
+ var ROTATION_PRECHECK_RATIO = 0.45;
6163
+ function readHeap() {
6164
+ const heapLimit = getHeapStatistics().heap_size_limit;
6165
+ const heapUsed = process.memoryUsage().heapUsed;
6166
+ return { heapUsed, heapLimit, ratio: heapLimit > 0 ? heapUsed / heapLimit : 0 };
6167
+ }
6168
+ function forceFullGc() {
6169
+ const gc = globalThis.gc;
6170
+ if (typeof gc !== "function") return false;
6171
+ gc();
6172
+ gc();
6173
+ return true;
6174
+ }
6175
+ function ratioOf(heapLimit, heapUsed) {
6176
+ return heapLimit > 0 ? heapUsed / heapLimit : 0;
6177
+ }
6178
+ function effectiveMemoryLimit() {
6179
+ const physical = totalmem();
6180
+ const constrained = process.constrainedMemory?.() ?? 0;
6181
+ return constrained > 0 ? Math.min(physical, constrained) : physical;
6182
+ }
6183
+ function withRss(reading, gcVerified) {
6184
+ const usage = process.memoryUsage();
6185
+ const memoryLimit = effectiveMemoryLimit();
6186
+ return {
6187
+ ...reading,
6188
+ gcVerified,
6189
+ rss: usage.rss,
6190
+ memoryLimit,
6191
+ rssRatio: ratioOf(memoryLimit, usage.rss),
6192
+ external: usage.external,
6193
+ arrayBuffers: usage.arrayBuffers
6194
+ };
6195
+ }
6196
+ function verifiedHeapPressure(threshold) {
6197
+ const raw = readHeap();
6198
+ const before = withRss(raw, false);
6199
+ const rssThreshold = threshold >= MEMORY_PRESSURE_HEAP_RATIO ? MEMORY_PRESSURE_RSS_RATIO : threshold;
6200
+ if ((raw.heapLimit <= 0 || raw.ratio < threshold) && (before.rssRatio ?? 0) < rssThreshold) return void 0;
6201
+ if (!forceFullGc()) return before;
6202
+ const heapUsed = process.memoryUsage().heapUsed;
6203
+ const ratio = ratioOf(raw.heapLimit, heapUsed);
6204
+ const verified = withRss({ heapUsed, heapLimit: raw.heapLimit, ratio, verifiedRatio: ratio }, true);
6205
+ if (ratio < threshold && (verified.rssRatio ?? 0) < rssThreshold) return void 0;
6206
+ return verified;
6207
+ }
6208
+ function heapRotationNeeded() {
6209
+ const raw = readHeap();
6210
+ const before = withRss(raw, false);
6211
+ if ((raw.heapLimit <= 0 || raw.ratio < ROTATION_PRECHECK_RATIO) && (before.rssRatio ?? 0) < ROTATION_PRECHECK_RATIO) return void 0;
6212
+ if (!forceFullGc()) {
6213
+ return raw.ratio >= MEMORY_PRESSURE_HEAP_RATIO || (before.rssRatio ?? 0) >= MEMORY_PRESSURE_RSS_RATIO ? before : void 0;
6214
+ }
6215
+ const heapUsed = process.memoryUsage().heapUsed;
6216
+ const ratio = ratioOf(raw.heapLimit, heapUsed);
6217
+ const verified = withRss({ heapUsed, heapLimit: raw.heapLimit, ratio, verifiedRatio: ratio }, true);
6218
+ if (ratio < MEMORY_ROTATION_HEAP_RATIO && (verified.rssRatio ?? 0) < MEMORY_ROTATION_RSS_RATIO) return void 0;
6219
+ return verified;
6220
+ }
6221
+ function heapSnapshotWarranted(_reading) {
6222
+ return process.env.FLAVOR_HEAP_SNAPSHOT_ON_ROTATE === "1";
6223
+ }
6145
6224
 
6146
6225
  // src/models/structured.ts
6147
6226
  import { z as z12 } from "zod";
@@ -6560,7 +6639,6 @@ function envMaxIterations() {
6560
6639
  if (!Number.isInteger(parsed) || parsed <= 0) return void 0;
6561
6640
  return parsed;
6562
6641
  }
6563
- var MEMORY_PRESSURE_HEAP_RATIO = 0.8;
6564
6642
  function memoryPressureError(heapUsed, heapLimit) {
6565
6643
  if (heapLimit <= 0 || heapUsed < heapLimit * MEMORY_PRESSURE_HEAP_RATIO) return void 0;
6566
6644
  const mb = (bytes) => Math.round(bytes / 1048576);
@@ -6570,7 +6648,12 @@ function memoryPressureError(heapUsed, heapLimit) {
6570
6648
  };
6571
6649
  }
6572
6650
  function currentMemoryPressure() {
6573
- return memoryPressureError(process.memoryUsage().heapUsed, getHeapStatistics().heap_size_limit);
6651
+ const reading = verifiedHeapPressure(MEMORY_PRESSURE_HEAP_RATIO);
6652
+ if (reading === void 0) return void 0;
6653
+ return memoryPressureError(reading.heapUsed, reading.heapLimit) ?? {
6654
+ code: "memory_pressure",
6655
+ message: `Process RSS reached ${Math.round((reading.rssRatio ?? 0) * 100)}% of the available memory limit; stopping before system OOM`
6656
+ };
6574
6657
  }
6575
6658
  var AgentLoop = class {
6576
6659
  #options;
@@ -8393,6 +8476,10 @@ var HarnessJournal = class {
8393
8476
  get records() {
8394
8477
  return this.#records.map((record3) => structuredClone(record3));
8395
8478
  }
8479
+ /** Retained record count for the rotation census. */
8480
+ get recordCount() {
8481
+ return this.#records.length;
8482
+ }
8396
8483
  admitQueue(kind, payload) {
8397
8484
  QueueKindSchema.parse(kind);
8398
8485
  const id = randomUUID8();
@@ -8669,19 +8756,38 @@ function jsonSafeRecord(value) {
8669
8756
  return JSON.parse(json);
8670
8757
  }
8671
8758
  function hashJson(value) {
8672
- return createHash6("sha256").update(stableJson(value)).digest("hex");
8673
- }
8674
- function recordHash(record3) {
8675
- const { hash: _hash, ...unsigned } = record3;
8676
- return hashJson(unsigned);
8759
+ const hash = createHash6("sha256");
8760
+ stableHashInto(hash, value);
8761
+ return hash.digest("hex");
8677
8762
  }
8678
- function stableJson(value) {
8679
- if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
8763
+ function stableHashInto(hash, value) {
8764
+ if (Array.isArray(value)) {
8765
+ hash.update("[");
8766
+ for (let index = 0; index < value.length; index += 1) {
8767
+ if (index > 0) hash.update(",");
8768
+ stableHashInto(hash, value[index]);
8769
+ }
8770
+ hash.update("]");
8771
+ return;
8772
+ }
8680
8773
  if (typeof value === "object" && value !== null) {
8681
8774
  const object = value;
8682
- return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${stableJson(object[key])}`).join(",")}}`;
8775
+ hash.update("{");
8776
+ const keys = Object.keys(object).sort();
8777
+ for (let index = 0; index < keys.length; index += 1) {
8778
+ const key = keys[index];
8779
+ if (index > 0) hash.update(",");
8780
+ hash.update(`${JSON.stringify(key)}:`);
8781
+ stableHashInto(hash, object[key]);
8782
+ }
8783
+ hash.update("}");
8784
+ return;
8683
8785
  }
8684
- return JSON.stringify(value) ?? String(value);
8786
+ hash.update(JSON.stringify(value) ?? String(value));
8787
+ }
8788
+ function recordHash(record3) {
8789
+ const { hash: _hash, ...unsigned } = record3;
8790
+ return hashJson(unsigned);
8685
8791
  }
8686
8792
  function assertSessionId(value) {
8687
8793
  if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(value)) throw new Error("Invalid session id");
@@ -11387,6 +11493,81 @@ async function initializeFlavor(cwd) {
11387
11493
  return { path, created: existing === void 0, content, facts };
11388
11494
  }
11389
11495
 
11496
+ // src/utils/memory-restart.ts
11497
+ import { join as join14 } from "path";
11498
+ var MEMORY_RESTART_EXIT_CODE = 75;
11499
+ var MEMORY_RESTART_MARKER_FILE = "memory-restart.json";
11500
+ var MEMORY_RESTART_WINDOW_MS = 60 * 60 * 1e3;
11501
+ var MEMORY_RESTART_MIN_GAP_MS = 5 * 60 * 1e3;
11502
+ var MEMORY_RESTART_CONTINUATION_TTL_MS = 24 * 60 * 60 * 1e3;
11503
+ var rotationActive = false;
11504
+ function markMemoryRotation() {
11505
+ rotationActive = true;
11506
+ }
11507
+ function memoryRotationActive() {
11508
+ return rotationActive;
11509
+ }
11510
+ function clearMemoryRotation() {
11511
+ rotationActive = false;
11512
+ }
11513
+ function memoryRestartMarkerPath(workspace) {
11514
+ return join14(workspace, ".flavor", "tmp", MEMORY_RESTART_MARKER_FILE);
11515
+ }
11516
+ function nextMemoryRestartMarker(previous, sessionId, now, continuation) {
11517
+ const previousWindowStart = previous?.windowStartedAt ?? previous?.requestedAt;
11518
+ const reusable = previous !== void 0 && previous.sessionId === sessionId && previousWindowStart !== void 0 && now.getTime() - Date.parse(previousWindowStart) < MEMORY_RESTART_WINDOW_MS && Number.isInteger(previous.attempts) && previous.attempts > 0;
11519
+ return {
11520
+ sessionId,
11521
+ windowStartedAt: reusable ? previousWindowStart : now.toISOString(),
11522
+ requestedAt: now.toISOString(),
11523
+ attempts: reusable ? previous.attempts + 1 : 1,
11524
+ ...continuation === void 0 ? {} : { continuation }
11525
+ };
11526
+ }
11527
+ function consumedMemoryRestartMarker(marker) {
11528
+ return {
11529
+ sessionId: marker.sessionId,
11530
+ ...marker.windowStartedAt === void 0 ? {} : { windowStartedAt: marker.windowStartedAt },
11531
+ requestedAt: marker.requestedAt,
11532
+ attempts: marker.attempts
11533
+ };
11534
+ }
11535
+ function rotationCooldownActive(marker, sessionId, now) {
11536
+ return marker !== void 0 && marker.sessionId === sessionId && now.getTime() - Date.parse(marker.requestedAt) < MEMORY_RESTART_MIN_GAP_MS;
11537
+ }
11538
+ function pendingContinuation(marker, sessionId, now) {
11539
+ if (marker === void 0 || marker.sessionId !== sessionId) return void 0;
11540
+ if (now.getTime() - Date.parse(marker.requestedAt) > MEMORY_RESTART_CONTINUATION_TTL_MS) return void 0;
11541
+ const continuation = marker.continuation;
11542
+ if (continuation === void 0) return void 0;
11543
+ if (continuation.kind !== "loop" && continuation.kind !== "goal") return void 0;
11544
+ if (typeof continuation.id !== "string" || continuation.id.length === 0) return void 0;
11545
+ return continuation;
11546
+ }
11547
+ function parseMemoryRestartMarker(json) {
11548
+ try {
11549
+ const value = JSON.parse(json);
11550
+ if (typeof value.sessionId !== "string" || typeof value.requestedAt !== "string" || typeof value.attempts !== "number") {
11551
+ return void 0;
11552
+ }
11553
+ const continuation = value.continuation;
11554
+ if (continuation !== void 0 && (typeof continuation !== "object" || continuation === null || continuation.kind !== "loop" && continuation.kind !== "goal" || typeof continuation.id !== "string")) {
11555
+ return void 0;
11556
+ }
11557
+ const windowStartedAt = value.windowStartedAt;
11558
+ if (windowStartedAt !== void 0 && typeof windowStartedAt !== "string") return void 0;
11559
+ return {
11560
+ sessionId: value.sessionId,
11561
+ ...windowStartedAt === void 0 ? {} : { windowStartedAt },
11562
+ requestedAt: value.requestedAt,
11563
+ attempts: value.attempts,
11564
+ ...continuation === void 0 ? {} : { continuation }
11565
+ };
11566
+ } catch {
11567
+ return void 0;
11568
+ }
11569
+ }
11570
+
11390
11571
  // src/skills/builtin-loop.ts
11391
11572
  function buildLoopCyclePrompt(input) {
11392
11573
  return [
@@ -11467,6 +11648,14 @@ var LoopStateSchema = z20.object({
11467
11648
  approvals: z20.array(LoopApprovalSchema)
11468
11649
  }).strict(),
11469
11650
  cycles: z20.array(LoopCycleEvidenceSchema),
11651
+ /** Completed worker awaiting host verification; avoids replaying tool work after rotation. */
11652
+ pendingCycle: z20.object({
11653
+ cycle: z20.number().int().positive(),
11654
+ startedAt: z20.string().datetime({ offset: true }),
11655
+ inputTokens: z20.number().int().nonnegative(),
11656
+ outputTokens: z20.number().int().nonnegative(),
11657
+ workerText: z20.string().max(16e3)
11658
+ }).strict().nullable().optional(),
11470
11659
  terminalReason: z20.string().optional()
11471
11660
  }).strict();
11472
11661
  var LoopEventSchema = z20.object({
@@ -11516,7 +11705,7 @@ function extendBudget(input, dimensions, approvedAt) {
11516
11705
  return LoopStateSchema.parse({
11517
11706
  ...state,
11518
11707
  updatedAt: approvedAt,
11519
- budget: { ...state.budget, cycleCheckpoint, tokenCheckpoint, approvals }
11708
+ budget: { ...state.budget, cycleCheckpoint, tokenCheckpoint, approvals: approvals.slice(-64) }
11520
11709
  });
11521
11710
  }
11522
11711
  function rejectBudget(input, terminalReason) {
@@ -11555,15 +11744,45 @@ var LoopOrchestrator = class {
11555
11744
  tokenCheckpoint: this.#options.config.maxTokens,
11556
11745
  approvals: []
11557
11746
  },
11558
- cycles: []
11747
+ cycles: [],
11748
+ pendingCycle: null
11559
11749
  });
11560
11750
  await this.#options.persistence.create(state);
11751
+ yield* this.#execute(state, request.goal, request.signal);
11752
+ }
11753
+ /**
11754
+ * Continue a loop whose process rotated onto a fresh heap. The persisted
11755
+ * state decides where to pick up; an interrupted cycle simply runs again.
11756
+ */
11757
+ async *resume(request) {
11758
+ const persistence = this.#options.persistence;
11759
+ if (persistence.load === void 0) throw new Error("Loop persistence does not support resume");
11760
+ let state;
11761
+ try {
11762
+ state = await persistence.load(request.loopId);
11763
+ } catch (error) {
11764
+ yield { type: "loop-terminal", loopId: request.loopId, status: "failed", reason: message(error) };
11765
+ return;
11766
+ }
11767
+ if (state.status !== "running") {
11768
+ const status = state.status;
11769
+ yield { type: "loop-terminal", loopId: state.loopId, status, reason: state.terminalReason ?? "The loop is not running." };
11770
+ return;
11771
+ }
11772
+ yield* this.#execute(state, state.goal, request.signal);
11773
+ }
11774
+ async *#execute(initialState, goal, signal) {
11775
+ const now = this.#options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
11776
+ const loopId = initialState.loopId;
11777
+ let state = initialState;
11561
11778
  try {
11779
+ await this.#options.onMemoryCheckpoint?.(loopId);
11780
+ if (memoryRotationActive()) return;
11562
11781
  const workspaceResolution = await this.#options.prepareWorkspace({
11563
11782
  root: this.#options.workspace,
11564
11783
  loopId,
11565
- goal: request.goal,
11566
- signal: request.signal
11784
+ goal,
11785
+ signal
11567
11786
  });
11568
11787
  if (workspaceResolution.kind === "needs_human") {
11569
11788
  state = await this.#terminal(state, "needs_human", workspaceResolution.reason, now());
@@ -11581,67 +11800,90 @@ var LoopOrchestrator = class {
11581
11800
  cycleCheckpoint: state.budget.cycleCheckpoint,
11582
11801
  tokenCheckpoint: state.budget.tokenCheckpoint
11583
11802
  };
11584
- let previousVerification = plan.commands.length === 0 ? {
11803
+ const lastCompletedCycle = state.cycles.at(-1);
11804
+ let previousVerification = lastCompletedCycle?.verification ?? (plan.commands.length === 0 ? {
11585
11805
  passed: false,
11586
11806
  commands: [],
11587
11807
  summary: `${plan.needsHumanReason ?? "No deterministic verification command was found."} Inspect the project and establish a meaningful project-native deterministic check; never add a trivial pass-through verifier.`
11588
- } : void 0;
11589
- let previousFailureSignature;
11590
- let repeatedFailures = 0;
11808
+ } : void 0);
11809
+ let previousFailureSignature = lastCompletedCycle === void 0 ? void 0 : `${lastCompletedCycle.workspaceFingerprint}
11810
+ ${lastCompletedCycle.verification.summary}`;
11811
+ let repeatedFailures = previousFailureSignature === void 0 ? 0 : countTrailingFailureSignature(state.cycles, previousFailureSignature);
11591
11812
  while (state.status === "running") {
11592
- request.signal.throwIfAborted();
11593
- const cycle = state.budget.cyclesUsed + 1;
11594
- const startedAt = now();
11595
- await this.#options.persistence.append({ version: 1, type: "cycle_started", timestamp: startedAt, loopId, payload: { cycle } });
11596
- yield { type: "loop-cycle-start", loopId, cycle };
11597
- const prompt = buildLoopCyclePrompt({
11598
- goal: request.goal,
11599
- cycle,
11600
- ...previousVerification === void 0 ? {} : {
11601
- memory: `Previous host verification: ${previousVerification.summary}`,
11602
- verification: previousVerification
11603
- }
11604
- });
11605
- let cycleInputTokens = 0;
11606
- let cycleOutputTokens = 0;
11607
- let workerError;
11608
- let workerText = "";
11609
- for await (const event of this.#options.runWorker({
11610
- goal: request.goal,
11611
- cycle,
11612
- workspace: executionWorkspace.root,
11613
- prompt,
11614
- signal: request.signal
11615
- })) {
11616
- if (event.type === "usage") {
11617
- cycleInputTokens += event.inputTokens;
11618
- cycleOutputTokens += event.outputTokens;
11619
- }
11620
- if (event.type === "error") workerError = event.error.message;
11621
- if (event.type === "text") workerText += event.text;
11622
- if (this.#options.hallucinationGuard !== void 0) {
11623
- if (event.type === "tool-start") {
11624
- this.#options.hallucinationGuard.recordToolCall(event.name, event.input, event.id);
11625
- } else if (event.type === "tool-end") {
11626
- this.#options.hallucinationGuard.recordToolResult(event.name, event.result, event.id);
11813
+ signal.throwIfAborted();
11814
+ await this.#options.onCycleBoundary?.(loopId);
11815
+ if (memoryRotationActive()) return;
11816
+ const pendingCycle = state.pendingCycle;
11817
+ const cycle = pendingCycle?.cycle ?? state.budget.cyclesUsed + 1;
11818
+ const startedAt = pendingCycle?.startedAt ?? now();
11819
+ let cycleInputTokens = pendingCycle?.inputTokens ?? 0;
11820
+ let cycleOutputTokens = pendingCycle?.outputTokens ?? 0;
11821
+ let workerText = pendingCycle?.workerText ?? "";
11822
+ if (pendingCycle === void 0 || pendingCycle === null) {
11823
+ await this.#options.persistence.append({ version: 1, type: "cycle_started", timestamp: startedAt, loopId, payload: { cycle } });
11824
+ yield { type: "loop-cycle-start", loopId, cycle };
11825
+ const prompt = buildLoopCyclePrompt({
11826
+ goal,
11827
+ cycle,
11828
+ ...previousVerification === void 0 ? {} : {
11829
+ memory: `Previous host verification: ${previousVerification.summary}`,
11830
+ verification: previousVerification
11831
+ }
11832
+ });
11833
+ let workerError;
11834
+ let workerMemoryPressure = false;
11835
+ let workerIterationLimit = false;
11836
+ for await (const event of this.#options.runWorker({
11837
+ goal,
11838
+ cycle,
11839
+ workspace: executionWorkspace.root,
11840
+ prompt,
11841
+ signal
11842
+ })) {
11843
+ if (event.type === "usage") {
11844
+ cycleInputTokens += event.inputTokens;
11845
+ cycleOutputTokens += event.outputTokens;
11846
+ }
11847
+ if (event.type === "error") {
11848
+ workerError = event.error;
11849
+ if (event.error.code === "memory_pressure") workerMemoryPressure = true;
11850
+ if (event.error.code === "iteration_limit") workerIterationLimit = true;
11627
11851
  }
11852
+ if (event.type === "text") workerText = `${workerText}${event.text}`.slice(-16e3);
11853
+ if (this.#options.hallucinationGuard !== void 0) {
11854
+ if (event.type === "tool-start") {
11855
+ this.#options.hallucinationGuard.recordToolCall(event.name, event.input, event.id);
11856
+ } else if (event.type === "tool-end") {
11857
+ this.#options.hallucinationGuard.recordToolResult(event.name, event.result, event.id);
11858
+ }
11859
+ }
11860
+ yield { type: "worker-event", event };
11861
+ }
11862
+ if (workerMemoryPressure) return;
11863
+ if (workerError !== void 0 && !workerIterationLimit && !isResumableWorkerError(workerError)) {
11864
+ const status = workerError.code === "cancelled" ? "cancelled" : "failed";
11865
+ state = LoopStateSchema.parse({
11866
+ ...state,
11867
+ budget: {
11868
+ ...state.budget,
11869
+ cyclesUsed: cycle,
11870
+ inputTokens: state.budget.inputTokens + cycleInputTokens,
11871
+ outputTokens: state.budget.outputTokens + cycleOutputTokens
11872
+ }
11873
+ });
11874
+ state = await this.#terminal(state, status, workerError.message, now());
11875
+ yield { type: "loop-terminal", loopId, status, reason: workerError.message };
11876
+ return;
11628
11877
  }
11629
- yield { type: "worker-event", event };
11630
- }
11631
- if (workerError !== void 0) {
11632
11878
  state = LoopStateSchema.parse({
11633
11879
  ...state,
11634
- budget: {
11635
- ...state.budget,
11636
- cyclesUsed: cycle,
11637
- inputTokens: state.budget.inputTokens + cycleInputTokens,
11638
- outputTokens: state.budget.outputTokens + cycleOutputTokens
11639
- }
11880
+ pendingCycle: { cycle, startedAt, inputTokens: cycleInputTokens, outputTokens: cycleOutputTokens, workerText },
11881
+ updatedAt: now()
11640
11882
  });
11641
- state = await this.#terminal(state, "failed", workerError, now());
11642
- yield { type: "loop-terminal", loopId, status: "failed", reason: workerError };
11643
- return;
11883
+ await this.#options.persistence.save(state);
11644
11884
  }
11885
+ await this.#options.onMemoryCheckpoint?.(loopId);
11886
+ if (memoryRotationActive()) return;
11645
11887
  if (plan.commands.length === 0) {
11646
11888
  plan = await this.#options.inferVerification(executionWorkspace.root);
11647
11889
  if (plan.commands.length > 0) {
@@ -11661,8 +11903,22 @@ var LoopOrchestrator = class {
11661
11903
  passed: false,
11662
11904
  commands: [],
11663
11905
  summary: plan.needsHumanReason ?? "No deterministic verification command was found after the discovery cycle."
11664
- } : await this.#options.runVerifier(plan, executionWorkspace.root, request.signal);
11906
+ } : await this.#options.runVerifier(plan, executionWorkspace.root, signal);
11907
+ await this.#options.onMemoryCheckpoint?.(loopId);
11908
+ if (memoryRotationActive()) return;
11665
11909
  const fingerprint2 = await this.#options.fingerprint(executionWorkspace.root);
11910
+ let guardWarnings = [];
11911
+ if (evidence.passed && this.#options.hallucinationGuard !== void 0) {
11912
+ try {
11913
+ await this.#options.onMemoryCheckpoint?.(loopId);
11914
+ if (memoryRotationActive()) return;
11915
+ const report = await this.#options.hallucinationGuard.evaluate(goal, workerText);
11916
+ await this.#options.onMemoryCheckpoint?.(loopId);
11917
+ if (memoryRotationActive()) return;
11918
+ guardWarnings = [...report.blockingReasons, ...report.warnings];
11919
+ } catch {
11920
+ }
11921
+ }
11666
11922
  const completedAt = now();
11667
11923
  state = LoopStateSchema.parse({
11668
11924
  ...state,
@@ -11681,7 +11937,8 @@ var LoopOrchestrator = class {
11681
11937
  outputTokens: cycleOutputTokens,
11682
11938
  workspaceFingerprint: fingerprint2,
11683
11939
  verification: evidence
11684
- }]
11940
+ }].slice(-64),
11941
+ pendingCycle: null
11685
11942
  });
11686
11943
  await this.#options.persistence.save(state);
11687
11944
  await this.#options.persistence.append({
@@ -11699,20 +11956,6 @@ var LoopOrchestrator = class {
11699
11956
  return;
11700
11957
  }
11701
11958
  if (evidence.passed) {
11702
- let guardWarnings = [];
11703
- if (this.#options.hallucinationGuard !== void 0) {
11704
- try {
11705
- const report = await this.#options.hallucinationGuard.evaluate(request.goal, workerText);
11706
- if (!report.passed) {
11707
- const guardReason = report.blockingReasons.join("; ") || "Hallucination guard blocked completion.";
11708
- state = await this.#terminal(state, "failed", guardReason, now());
11709
- yield { type: "loop-terminal", loopId, status: "failed", reason: guardReason };
11710
- return;
11711
- }
11712
- guardWarnings = report.warnings;
11713
- } catch {
11714
- }
11715
- }
11716
11959
  const reason = [evidence.summary, ...guardWarnings].filter(Boolean).join("; ");
11717
11960
  state = await this.#terminal(state, "succeeded", reason, now());
11718
11961
  yield { type: "loop-terminal", loopId, status: "succeeded", reason };
@@ -11723,8 +11966,8 @@ ${evidence.summary}`;
11723
11966
  repeatedFailures = failureSignature === previousFailureSignature ? repeatedFailures + 1 : 1;
11724
11967
  previousFailureSignature = failureSignature;
11725
11968
  previousVerification = evidence;
11726
- if (repeatedFailures >= 3) {
11727
- const reason = "The same verification failure repeated three times without material workspace progress.";
11969
+ if (this.#options.maxNoProgressCycles !== void 0 && repeatedFailures >= this.#options.maxNoProgressCycles) {
11970
+ const reason = `The same verification failure repeated ${this.#options.maxNoProgressCycles} times without material workspace progress.`;
11728
11971
  state = await this.#terminal(state, "no_progress", reason, now());
11729
11972
  yield { type: "loop-terminal", loopId, status: "no_progress", reason };
11730
11973
  return;
@@ -11738,7 +11981,7 @@ ${evidence.summary}`;
11738
11981
  cycleCheckpoint: state.budget.cycleCheckpoint,
11739
11982
  tokenCheckpoint: state.budget.tokenCheckpoint
11740
11983
  };
11741
- const confirmation = await this.#options.confirmBudget(state, decision.dimensions, request.signal);
11984
+ const confirmation = await this.#options.confirmBudget(state, decision.dimensions, signal);
11742
11985
  if (confirmation === "unavailable") {
11743
11986
  const reason = "Loop budget extension requires user confirmation.";
11744
11987
  state = await this.#terminal(state, "needs_human", reason, now());
@@ -11765,8 +12008,9 @@ ${evidence.summary}`;
11765
12008
  }
11766
12009
  }
11767
12010
  } catch (error) {
11768
- const status = request.signal.aborted ? "cancelled" : "failed";
11769
- const reason = request.signal.aborted ? "Loop cancelled by user." : message(error);
12011
+ if (memoryRotationActive()) return;
12012
+ const status = signal.aborted ? "cancelled" : "failed";
12013
+ const reason = signal.aborted ? "Loop cancelled by user." : message(error);
11770
12014
  state = await this.#terminal(state, status, reason, now());
11771
12015
  yield { type: "loop-terminal", loopId, status, reason };
11772
12016
  }
@@ -11784,11 +12028,24 @@ ${evidence.summary}`;
11784
12028
  return state;
11785
12029
  }
11786
12030
  };
12031
+ function isResumableWorkerError(error) {
12032
+ return error.code !== "authentication" && error.code !== "model_not_found" && error.code !== "cancelled" && error.code !== "memory_pressure";
12033
+ }
12034
+ function countTrailingFailureSignature(cycles, signature) {
12035
+ let count = 0;
12036
+ for (let index = cycles.length - 1; index >= 0; index -= 1) {
12037
+ const cycle = cycles[index];
12038
+ if (`${cycle.workspaceFingerprint}
12039
+ ${cycle.verification.summary}` !== signature) break;
12040
+ count += 1;
12041
+ }
12042
+ return count;
12043
+ }
11787
12044
 
11788
12045
  // src/goal/orchestrator.ts
11789
12046
  import { createHash as createHash11 } from "crypto";
11790
12047
  import { writeFile as writeFile9, mkdir as mkdir10 } from "fs/promises";
11791
- import { join as join15 } from "path";
12048
+ import { join as join16 } from "path";
11792
12049
 
11793
12050
  // src/goal/types.ts
11794
12051
  import { z as z21 } from "zod";
@@ -11851,6 +12108,11 @@ var GoalStateSchema = z21.object({
11851
12108
  planPath: z21.string().nullable(),
11852
12109
  verifyRounds: z21.number().int().min(0),
11853
12110
  workerRounds: z21.number().int().min(0),
12111
+ /** Worker output checkpoint used to resume verification without rerunning tools. */
12112
+ pendingVerification: z21.object({
12113
+ round: z21.number().int().positive(),
12114
+ finalResponse: z21.string().max(16e3)
12115
+ }).strict().nullable().optional(),
11854
12116
  lastGaps: z21.array(GapSchema),
11855
12117
  gapFingerprint: z21.string(),
11856
12118
  stallStreak: z21.number().int().min(0),
@@ -11896,6 +12158,7 @@ ${objective}`
11896
12158
  ].join("\n");
11897
12159
  }
11898
12160
  async function runPlanner(options) {
12161
+ await options.onProgress?.();
11899
12162
  const { adapter, model } = options.registry.get(options.modelId);
11900
12163
  const prompt = buildPlannerPrompt(options.objective);
11901
12164
  const messages = [{ role: "user", content: prompt }];
@@ -11907,6 +12170,7 @@ async function runPlanner(options) {
11907
12170
  tools: [],
11908
12171
  ...options.signal === void 0 ? {} : { signal: options.signal }
11909
12172
  })) {
12173
+ await options.onProgress?.();
11910
12174
  if (event.type === "text") text += event.text;
11911
12175
  else if (event.type === "error") throw event.error;
11912
12176
  else if (event.type === "done") {
@@ -11953,7 +12217,7 @@ function extractJson(text) {
11953
12217
  // src/goal/classifier.ts
11954
12218
  import { createHash as createHash10 } from "crypto";
11955
12219
  import { readFile as readFile10 } from "fs/promises";
11956
- import { join as join14 } from "path";
12220
+ import { join as join15 } from "path";
11957
12221
  function buildSkepticPrompt(evidence, plan, index) {
11958
12222
  const criteriaList = plan.criteria.map((c) => ` ${c.id}. [${c.type}] ${c.description}`).join("\n");
11959
12223
  return [
@@ -12013,7 +12277,7 @@ async function runClassifier(evidence, plan, options) {
12013
12277
  const skepticResults = await Promise.all(
12014
12278
  Array.from(
12015
12279
  { length: n },
12016
- (_, i) => runOneSkeptic(evidence, plan, i + 1, adapter, model, options.signal)
12280
+ (_, i) => runOneSkeptic(evidence, plan, i + 1, adapter, model, options.signal, options.onProgress)
12017
12281
  )
12018
12282
  );
12019
12283
  const verdicts = [];
@@ -12030,7 +12294,8 @@ async function runClassifier(evidence, plan, options) {
12030
12294
  }
12031
12295
  return aggregate(verdicts);
12032
12296
  }
12033
- async function runOneSkeptic(evidence, plan, index, adapter, model, signal) {
12297
+ async function runOneSkeptic(evidence, plan, index, adapter, model, signal, onProgress) {
12298
+ await onProgress?.();
12034
12299
  const prompt = buildSkepticPrompt(evidence, plan, index);
12035
12300
  const messages = [{ role: "user", content: prompt }];
12036
12301
  let text = "";
@@ -12040,6 +12305,7 @@ async function runOneSkeptic(evidence, plan, index, adapter, model, signal) {
12040
12305
  tools: [],
12041
12306
  ...signal === void 0 ? {} : { signal }
12042
12307
  })) {
12308
+ await onProgress?.();
12043
12309
  if (event.type === "text") text += event.text;
12044
12310
  else if (event.type === "error") throw event.error;
12045
12311
  else if (event.type === "done") break;
@@ -12130,7 +12396,7 @@ function computeFingerprint(gaps) {
12130
12396
  }
12131
12397
  async function collectEvidence(workspace, objective, finalResponse, priorGaps, contractHash2 = createHash10("sha256").update(objective).digest("hex"), hostVerification) {
12132
12398
  const workspaceState = await collectWorkspaceState(workspace);
12133
- const planPath2 = join14(workspace, ".flavor", "goal-plan.md");
12399
+ const planPath2 = join15(workspace, ".flavor", "goal-plan.md");
12134
12400
  let planFile = null;
12135
12401
  try {
12136
12402
  planFile = await readFile10(planPath2, "utf8");
@@ -12194,7 +12460,7 @@ var GoalOrchestrator = class {
12194
12460
  async *run(request) {
12195
12461
  const timestamp2 = () => this.#options.now?.() ?? (/* @__PURE__ */ new Date()).toISOString();
12196
12462
  const createdAt = timestamp2();
12197
- let state = {
12463
+ const state = {
12198
12464
  id: this.#options.idFactory?.() ?? `goal-${Date.now().toString(36)}`,
12199
12465
  objective: request.goal,
12200
12466
  phase: "planning",
@@ -12203,6 +12469,7 @@ var GoalOrchestrator = class {
12203
12469
  planPath: null,
12204
12470
  verifyRounds: 0,
12205
12471
  workerRounds: 0,
12472
+ pendingVerification: null,
12206
12473
  lastGaps: [],
12207
12474
  gapFingerprint: "",
12208
12475
  stallStreak: 0,
@@ -12211,91 +12478,164 @@ var GoalOrchestrator = class {
12211
12478
  createdAt,
12212
12479
  updatedAt: createdAt
12213
12480
  };
12481
+ yield* this.#execute(state, request.signal);
12482
+ }
12483
+ /**
12484
+ * Continue a goal whose process rotated onto a fresh heap. The persisted
12485
+ * state decides where to pick up; an interrupted round simply runs again.
12486
+ */
12487
+ async *resume(request) {
12488
+ const persistence = this.#options.persistence;
12489
+ if (persistence === void 0 || persistence.load === void 0) {
12490
+ throw new Error("Goal persistence does not support resume");
12491
+ }
12492
+ let state;
12493
+ try {
12494
+ state = await persistence.load(request.goalId);
12495
+ } catch (error) {
12496
+ yield { type: "goal-failed", reason: message(error) };
12497
+ return;
12498
+ }
12499
+ if (state.status !== "active" && state.status !== "not_achieved") {
12500
+ yield { type: "goal-failed", reason: `Goal "${state.id}" is ${state.status}; it cannot be resumed.` };
12501
+ return;
12502
+ }
12503
+ yield { type: "goal-resumed", goalId: state.id, round: resumeStartRound(state) };
12504
+ yield* this.#execute(state, request.signal);
12505
+ }
12506
+ async *#execute(initialState, signal) {
12507
+ const timestamp2 = () => this.#options.now?.() ?? (/* @__PURE__ */ new Date()).toISOString();
12508
+ let state = initialState;
12214
12509
  const persistState = async (patch) => {
12215
12510
  state = { ...state, ...patch, updatedAt: timestamp2() };
12216
12511
  await this.#options.persistence?.save(state);
12217
12512
  };
12513
+ const memoryCheckpoint = async () => {
12514
+ await this.#options.onMemoryCheckpoint?.(state.id);
12515
+ if (memoryRotationActive()) throw new Error("Heap rotation requested");
12516
+ };
12218
12517
  await persistState();
12219
12518
  const workspace = this.#options.workspace;
12220
12519
  let plan;
12221
12520
  let planPath2;
12222
- try {
12223
- request.signal.throwIfAborted();
12224
- plan = await runPlanner({
12225
- registry: this.#options.registry,
12226
- modelId: this.#options.plannerModelId,
12227
- objective: request.goal,
12228
- signal: request.signal
12229
- });
12230
- planPath2 = await writePlanFile(workspace, plan);
12231
- await persistState({ plan, planPath: planPath2, contractHash: contractHash(request.goal, plan) });
12232
- yield { type: "goal-plan-created", plan, planPath: planPath2 };
12233
- } catch (error) {
12234
- const reason = `Goal planning failed: ${message(error)}`;
12235
- await persistState({ phase: "complete", status: "failed" });
12236
- yield { type: "goal-plan-failed", reason };
12237
- yield { type: "goal-failed", reason };
12238
- return;
12239
- }
12240
- let priorGaps = [];
12241
- let priorFingerprint = "";
12242
- let stallStreak = 0;
12243
- for (let round = 1; round <= this.#options.maxRounds; round++) {
12244
- request.signal.throwIfAborted();
12245
- const workerPrompt = buildWorkerPrompt(request.goal, plan, priorGaps, round);
12246
- await persistState({
12247
- phase: "executing",
12248
- status: "active",
12249
- workerRounds: round,
12250
- lastGaps: priorGaps,
12251
- gapFingerprint: priorFingerprint,
12252
- stallStreak
12253
- });
12254
- yield { type: "goal-worker-start", round };
12255
- let finalResponse = "";
12256
- let workerError;
12257
- try {
12258
- for await (const event of this.#options.runWorker({
12259
- goal: request.goal,
12260
- round,
12261
- workspace,
12262
- prompt: workerPrompt,
12263
- priorGaps: formatPriorGaps(priorGaps),
12264
- signal: request.signal
12265
- })) {
12266
- yield { type: "goal-worker-event", round, event };
12267
- if (event.type === "text") finalResponse += event.text;
12268
- if (event.type === "error") {
12269
- workerError = event.error.message;
12270
- break;
12521
+ if (state.plan === null) {
12522
+ let planningAttempt = 0;
12523
+ while (true) {
12524
+ try {
12525
+ signal.throwIfAborted();
12526
+ await memoryCheckpoint();
12527
+ plan = await runPlanner({
12528
+ registry: this.#options.registry,
12529
+ modelId: this.#options.plannerModelId,
12530
+ objective: state.objective,
12531
+ signal,
12532
+ onProgress: memoryCheckpoint
12533
+ });
12534
+ planPath2 = await writePlanFile(workspace, plan);
12535
+ await persistState({ phase: "executing", plan, planPath: planPath2, contractHash: contractHash(state.objective, plan) });
12536
+ yield { type: "goal-plan-created", plan, planPath: planPath2 };
12537
+ break;
12538
+ } catch (error) {
12539
+ if (memoryRotationActive()) return;
12540
+ const normalized = normalizeProviderError(error);
12541
+ if (!isResumableStageError(normalized)) {
12542
+ const reason = `Goal planning failed: ${normalized.message}`;
12543
+ await persistState({ phase: "complete", status: "failed" });
12544
+ yield { type: "goal-plan-failed", reason };
12545
+ yield { type: "goal-failed", reason };
12546
+ return;
12271
12547
  }
12548
+ planningAttempt += 1;
12549
+ const delayMs = stageRetryDelay(this.#options, planningAttempt);
12550
+ yield { type: "goal-stage-retry", stage: "planning", attempt: planningAttempt, reason: normalized.message, delayMs };
12551
+ await waitForStageRetry(delayMs, signal);
12272
12552
  }
12273
- } catch (error) {
12274
- workerError = message(error);
12275
12553
  }
12276
- if (workerError !== void 0) {
12277
- const reason = `Worker error in round ${round}: ${workerError}`;
12278
- await persistState({ phase: "complete", status: "failed" });
12279
- yield { type: "goal-failed", reason };
12280
- return;
12554
+ } else {
12555
+ plan = state.plan;
12556
+ planPath2 = state.planPath ?? await writePlanFile(workspace, plan);
12557
+ await persistState({ phase: "executing", status: "active", ...state.planPath === null ? { planPath: planPath2 } : {} });
12558
+ }
12559
+ let priorGaps = state.lastGaps;
12560
+ let priorFingerprint = state.gapFingerprint;
12561
+ let stallStreak = state.stallStreak;
12562
+ const startRound = resumeStartRound(state);
12563
+ for (let round = startRound; this.#options.maxRounds === void 0 || round <= this.#options.maxRounds; round++) {
12564
+ signal.throwIfAborted();
12565
+ await this.#options.onRoundBoundary?.(state.id);
12566
+ if (memoryRotationActive()) return;
12567
+ const pendingVerification = state.pendingVerification?.round === round ? state.pendingVerification : void 0;
12568
+ let finalResponse = pendingVerification?.finalResponse ?? "";
12569
+ if (pendingVerification === void 0) {
12570
+ const workerPrompt = buildWorkerPrompt(state.objective, plan, priorGaps, round);
12571
+ await persistState({
12572
+ phase: "executing",
12573
+ status: "active",
12574
+ workerRounds: round,
12575
+ pendingVerification: null,
12576
+ lastGaps: priorGaps,
12577
+ gapFingerprint: priorFingerprint,
12578
+ stallStreak
12579
+ });
12580
+ yield { type: "goal-worker-start", round };
12581
+ let workerError;
12582
+ let workerMemoryPressure = false;
12583
+ let workerIterationLimit = false;
12584
+ try {
12585
+ for await (const event of this.#options.runWorker({
12586
+ goal: state.objective,
12587
+ round,
12588
+ workspace,
12589
+ prompt: workerPrompt,
12590
+ priorGaps: formatPriorGaps(priorGaps),
12591
+ signal
12592
+ })) {
12593
+ yield { type: "goal-worker-event", round, event };
12594
+ if (event.type === "text") finalResponse = `${finalResponse}${event.text}`.slice(-16e3);
12595
+ if (event.type === "error") {
12596
+ workerError = event.error;
12597
+ if (event.error.code === "memory_pressure") workerMemoryPressure = true;
12598
+ if (event.error.code === "iteration_limit") workerIterationLimit = true;
12599
+ break;
12600
+ }
12601
+ }
12602
+ } catch (error) {
12603
+ if (memoryRotationActive()) return;
12604
+ workerError = { code: "unknown", message: message(error) };
12605
+ }
12606
+ if (workerMemoryPressure) return;
12607
+ if (workerError !== void 0 && !workerIterationLimit && !isResumableWorkerError2(workerError)) {
12608
+ const reason = `Worker error in round ${round}: ${workerError.message}`;
12609
+ await persistState({ phase: "complete", status: "failed" });
12610
+ yield { type: "goal-failed", reason };
12611
+ return;
12612
+ }
12613
+ await persistState({
12614
+ phase: "verifying",
12615
+ status: "active",
12616
+ pendingVerification: { round, finalResponse }
12617
+ });
12281
12618
  }
12282
- await persistState({ phase: "verifying", status: "active", verifyRounds: round });
12283
12619
  yield { type: "goal-verification-start", round };
12284
- const hostVerification = await this.#options.verifyHost?.(request.signal);
12620
+ await this.#options.onMemoryCheckpoint?.(state.id);
12621
+ if (memoryRotationActive()) return;
12622
+ const hostVerification = await this.#options.verifyHost?.(signal);
12623
+ await this.#options.onMemoryCheckpoint?.(state.id);
12624
+ if (memoryRotationActive()) return;
12285
12625
  const evidence = await collectEvidence(
12286
12626
  workspace,
12287
- request.goal,
12627
+ state.objective,
12288
12628
  finalResponse.slice(-4e3),
12289
12629
  // Last 4000 chars of response
12290
12630
  formatPriorGaps(priorGaps),
12291
12631
  state.contractHash,
12292
12632
  hostVerification
12293
12633
  );
12294
- await persistState({ evidenceRounds: [...state.evidenceRounds, {
12634
+ await persistState({ evidenceRounds: [...state.evidenceRounds.filter((item) => item.round !== round), {
12295
12635
  round,
12296
12636
  workspaceDiffHash: evidence.workspaceDiffHash,
12297
12637
  ...hostVerification === void 0 ? {} : { hostVerification }
12298
- }] });
12638
+ }].slice(-64) });
12299
12639
  let outcome;
12300
12640
  if (plan.kind === "code-change" && hostVerification !== void 0 && !hostVerification.passed) {
12301
12641
  if (hostVerification.commands.length === 0) {
@@ -12309,20 +12649,34 @@ var GoalOrchestrator = class {
12309
12649
  fingerprint: createHash11("sha256").update(hostVerification.summary).digest("hex").slice(0, 16)
12310
12650
  };
12311
12651
  }
12312
- } else try {
12313
- outcome = await runClassifier(evidence, plan, {
12314
- registry: this.#options.registry,
12315
- modelId: this.#options.classifierModelId,
12316
- skepticCount: this.#options.skepticCount,
12317
- workspace,
12318
- signal: request.signal
12319
- });
12320
- } catch (error) {
12321
- outcome = {
12322
- type: "blocked",
12323
- reason: `Classifier infrastructure error: ${message(error)}`
12324
- };
12652
+ } else {
12653
+ let classifierAttempt = 0;
12654
+ while (true) {
12655
+ try {
12656
+ outcome = await runClassifier(evidence, plan, {
12657
+ registry: this.#options.registry,
12658
+ modelId: this.#options.classifierModelId,
12659
+ skepticCount: this.#options.skepticCount,
12660
+ workspace,
12661
+ signal,
12662
+ onProgress: memoryCheckpoint
12663
+ });
12664
+ break;
12665
+ } catch (error) {
12666
+ if (memoryRotationActive()) return;
12667
+ const normalized = normalizeProviderError(error);
12668
+ if (!isResumableStageError(normalized)) {
12669
+ outcome = { type: "blocked", reason: `Classifier infrastructure error: ${normalized.message}` };
12670
+ break;
12671
+ }
12672
+ classifierAttempt += 1;
12673
+ const delayMs = stageRetryDelay(this.#options, classifierAttempt);
12674
+ yield { type: "goal-stage-retry", stage: "classification", attempt: classifierAttempt, reason: normalized.message, delayMs };
12675
+ await waitForStageRetry(delayMs, signal);
12676
+ }
12677
+ }
12325
12678
  }
12679
+ await persistState({ verifyRounds: round, pendingVerification: null });
12326
12680
  yield {
12327
12681
  type: "goal-verdict",
12328
12682
  round,
@@ -12342,7 +12696,7 @@ var GoalOrchestrator = class {
12342
12696
  }
12343
12697
  if (outcome.fingerprint === priorFingerprint) {
12344
12698
  stallStreak++;
12345
- if (stallStreak >= this.#options.maxStallStreak) {
12699
+ if (this.#options.maxStallStreak !== void 0 && stallStreak >= this.#options.maxStallStreak) {
12346
12700
  await persistState({
12347
12701
  phase: "complete",
12348
12702
  status: "failed",
@@ -12382,9 +12736,40 @@ var GoalOrchestrator = class {
12382
12736
  function contractHash(objective, plan) {
12383
12737
  return createHash11("sha256").update(JSON.stringify({ objective, plan })).digest("hex");
12384
12738
  }
12739
+ function resumeStartRound(state) {
12740
+ if (state.pendingVerification !== void 0 && state.pendingVerification !== null) {
12741
+ return state.pendingVerification.round;
12742
+ }
12743
+ return state.verifyRounds >= state.workerRounds ? state.workerRounds + 1 : Math.max(state.workerRounds, 1);
12744
+ }
12745
+ function isResumableWorkerError2(error) {
12746
+ return error.code !== "authentication" && error.code !== "model_not_found" && error.code !== "cancelled" && error.code !== "memory_pressure";
12747
+ }
12748
+ function isResumableStageError(error) {
12749
+ return error.code !== "authentication" && error.code !== "model_not_found" && error.code !== "cancelled" && error.code !== "memory_pressure";
12750
+ }
12751
+ function stageRetryDelay(options, attempt) {
12752
+ const delayMs = options.stageRetryDelayMs?.(attempt) ?? Math.min(6e4, 1e3 * 2 ** Math.min(attempt - 1, 6));
12753
+ return Number.isSafeInteger(delayMs) && delayMs >= 0 ? delayMs : 1e3;
12754
+ }
12755
+ function waitForStageRetry(delayMs, signal) {
12756
+ signal.throwIfAborted();
12757
+ if (delayMs === 0) return Promise.resolve();
12758
+ return new Promise((resolvePromise, reject) => {
12759
+ const onAbort = () => {
12760
+ clearTimeout(timer);
12761
+ reject(signal.reason instanceof Error ? signal.reason : new Error("Goal cancelled"));
12762
+ };
12763
+ const timer = setTimeout(() => {
12764
+ signal.removeEventListener("abort", onAbort);
12765
+ resolvePromise();
12766
+ }, delayMs);
12767
+ signal.addEventListener("abort", onAbort, { once: true });
12768
+ });
12769
+ }
12385
12770
  async function writePlanFile(workspace, plan) {
12386
- const dir = join15(workspace, ".flavor");
12387
- const planPath2 = join15(dir, "goal-plan.md");
12771
+ const dir = join16(workspace, ".flavor");
12772
+ const planPath2 = join16(dir, "goal-plan.md");
12388
12773
  await mkdir10(dir, { recursive: true });
12389
12774
  const criteriaText = plan.criteria.map((c) => `- [${c.type}] AC-${c.id}: ${c.description}`).join("\n");
12390
12775
  const content = [
@@ -12461,8 +12846,8 @@ function formatPriorGaps(gaps) {
12461
12846
  // src/goal/store.ts
12462
12847
  import { constants as constants4 } from "fs";
12463
12848
  import { randomUUID as randomUUID11 } from "crypto";
12464
- import { lstat as lstat5, mkdir as mkdir11, open as open5, rename as rename4, rm as rm6 } from "fs/promises";
12465
- import { dirname as dirname9, join as join16, relative as relative9, resolve as resolve12, sep as sep9 } from "path";
12849
+ import { lstat as lstat5, mkdir as mkdir11, open as open5, readFile as readFile11, rename as rename4, rm as rm6, stat as stat6 } from "fs/promises";
12850
+ import { dirname as dirname9, join as join17, relative as relative9, resolve as resolve12, sep as sep9 } from "path";
12466
12851
  var DEFAULT_MAX_BYTES = 1024 * 1024;
12467
12852
  var GoalStore = class {
12468
12853
  #workspace;
@@ -12470,7 +12855,7 @@ var GoalStore = class {
12470
12855
  #maxBytes;
12471
12856
  constructor(options) {
12472
12857
  this.#workspace = resolve12(options.workspace);
12473
- this.#root = join16(this.#workspace, ".flavor", "goals");
12858
+ this.#root = join17(this.#workspace, ".flavor", "goals");
12474
12859
  this.#maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
12475
12860
  }
12476
12861
  async save(input) {
@@ -12478,13 +12863,13 @@ var GoalStore = class {
12478
12863
  await assertNoSymlink(this.#workspace, dirname9(this.#root));
12479
12864
  await mkdir11(this.#root, { recursive: true, mode: 448 });
12480
12865
  await assertNoSymlink(this.#workspace, this.#root);
12481
- const target = join16(this.#root, `${state.id}.json`);
12866
+ const target = join17(this.#root, `${state.id}.json`);
12482
12867
  const body = `${JSON.stringify(state, null, 2)}
12483
12868
  `;
12484
12869
  if (Buffer.byteLength(body) > this.#maxBytes) {
12485
12870
  throw new Error(`Goal state exceeds maximum size of ${this.#maxBytes} bytes`);
12486
12871
  }
12487
- const temporary = join16(this.#root, `.${state.id}.${process.pid}.${randomUUID11()}.tmp`);
12872
+ const temporary = join17(this.#root, `.${state.id}.${process.pid}.${randomUUID11()}.tmp`);
12488
12873
  let handle;
12489
12874
  try {
12490
12875
  handle = await open5(temporary, constants4.O_CREAT | constants4.O_EXCL | constants4.O_WRONLY, 384);
@@ -12498,6 +12883,28 @@ var GoalStore = class {
12498
12883
  await rm6(temporary, { force: true }).catch(() => void 0);
12499
12884
  }
12500
12885
  }
12886
+ /** Reload a persisted goal so a rotated process can resume it. */
12887
+ async load(goalId) {
12888
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(goalId)) {
12889
+ throw new Error(`Goal "${goalId}" was not found`);
12890
+ }
12891
+ await assertNoSymlink(this.#workspace, dirname9(this.#root));
12892
+ await assertNoSymlink(this.#workspace, this.#root);
12893
+ const target = join17(this.#root, `${goalId}.json`);
12894
+ try {
12895
+ const metadata = await stat6(target);
12896
+ if (!metadata.isFile() || metadata.size > this.#maxBytes) {
12897
+ throw new Error("Goal state is not a bounded regular file");
12898
+ }
12899
+ const raw = await readFile11(target, "utf8");
12900
+ return GoalStateSchema.parse(JSON.parse(raw));
12901
+ } catch (error) {
12902
+ if (isCode4(error, "ENOENT")) throw new Error(`Goal "${goalId}" was not found`);
12903
+ const quarantine = `${target}.corrupt-${Date.now()}-${randomUUID11().slice(0, 8)}`;
12904
+ await rename4(target, quarantine).catch(() => void 0);
12905
+ throw new Error(`Goal "${goalId}" is corrupt and was quarantined`);
12906
+ }
12907
+ }
12501
12908
  };
12502
12909
  async function assertNoSymlink(root, target) {
12503
12910
  let cursor = resolve12(target);
@@ -12524,8 +12931,8 @@ function isCode4(error, code) {
12524
12931
  }
12525
12932
 
12526
12933
  // src/loop/isolation.ts
12527
- import { appendFile as appendFile2, mkdir as mkdir12, readFile as readFile11 } from "fs/promises";
12528
- import { join as join17, resolve as resolve13 } from "path";
12934
+ import { appendFile as appendFile2, mkdir as mkdir12, readFile as readFile12 } from "fs/promises";
12935
+ import { join as join18, resolve as resolve13 } from "path";
12529
12936
  var CODE_CHANGE = /\b(?:fix|implement|add|update|change|modify|refactor|build|create|remove|delete|write|migrate)\b|修复|实现|添加|更新|修改|重构|创建|删除|迁移/i;
12530
12937
  var READ_ONLY = /\b(?:review|analy[sz]e|inspect|explain|research|investigate|audit|summari[sz]e|report)\b|调研|分析|审查|检查|解释|研究|总结|报告/i;
12531
12938
  function inferGoalIntent(goal) {
@@ -12548,8 +12955,8 @@ async function prepareLoopWorkspace(options) {
12548
12955
  return { kind: "needs_human", reason: "The workspace has uncommitted changes that a new worktree would omit." };
12549
12956
  }
12550
12957
  options.signal?.throwIfAborted();
12551
- const worktrees = join17(root, ".worktrees");
12552
- const path = join17(worktrees, `loop-${options.loopId}`);
12958
+ const worktrees = join18(root, ".worktrees");
12959
+ const path = join18(worktrees, `loop-${options.loopId}`);
12553
12960
  const branch = `loop/${options.loopId}`;
12554
12961
  try {
12555
12962
  await ensureWorktreeDirectoryIgnored(root);
@@ -12579,12 +12986,12 @@ async function ensureWorktreeDirectoryIgnored(root) {
12579
12986
  const commonDirectory = await git(root, ["rev-parse", "--git-common-dir"]);
12580
12987
  if (commonDirectory.code !== 0) throw new Error(commonDirectory.stderr.trim() || "Git common directory is unavailable");
12581
12988
  const gitDirectory = resolve13(root, commonDirectory.stdout.trim());
12582
- const infoDirectory = join17(gitDirectory, "info");
12583
- const excludePath = join17(infoDirectory, "exclude");
12989
+ const infoDirectory = join18(gitDirectory, "info");
12990
+ const excludePath = join18(infoDirectory, "exclude");
12584
12991
  await mkdir12(infoDirectory, { recursive: true });
12585
12992
  let existing = "";
12586
12993
  try {
12587
- existing = await readFile11(excludePath, "utf8");
12994
+ existing = await readFile12(excludePath, "utf8");
12588
12995
  } catch (error) {
12589
12996
  if (!(typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT")) throw error;
12590
12997
  }
@@ -12595,8 +13002,8 @@ async function ensureWorktreeDirectoryIgnored(root) {
12595
13002
 
12596
13003
  // src/loop/store.ts
12597
13004
  import { constants as constants5 } from "fs";
12598
- import { lstat as lstat6, mkdir as mkdir13, open as open6, readFile as readFile12, realpath as realpath8, rename as rename5, rm as rm7, stat as stat6 } from "fs/promises";
12599
- import { dirname as dirname10, join as join18, relative as relative10, resolve as resolve14, sep as sep10 } from "path";
13005
+ import { lstat as lstat6, mkdir as mkdir13, open as open6, readFile as readFile13, realpath as realpath8, rename as rename5, rm as rm7, stat as stat7 } from "fs/promises";
13006
+ import { dirname as dirname10, join as join19, relative as relative10, resolve as resolve14, sep as sep10 } from "path";
12600
13007
  import { randomUUID as randomUUID12 } from "crypto";
12601
13008
  var LOOP_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
12602
13009
  var DEFAULT_MAX_BYTES2 = 5 * 1024 * 1024;
@@ -12607,7 +13014,7 @@ var LoopStore = class {
12607
13014
  #appendTails = /* @__PURE__ */ new Map();
12608
13015
  constructor(options) {
12609
13016
  this.#workspace = resolve14(options.workspace);
12610
- this.#root = join18(this.#workspace, ".flavor", "loops");
13017
+ this.#root = join19(this.#workspace, ".flavor", "loops");
12611
13018
  this.#maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES2;
12612
13019
  }
12613
13020
  async create(state) {
@@ -12624,11 +13031,11 @@ var LoopStore = class {
12624
13031
  const state = LoopStateSchema.parse(input);
12625
13032
  await this.#assertWorkspace(state.workspace);
12626
13033
  const directory = await this.#prepare(state.loopId);
12627
- const target = join18(directory, "state.json");
13034
+ const target = join19(directory, "state.json");
12628
13035
  const body = `${JSON.stringify(state, null, 2)}
12629
13036
  `;
12630
13037
  if (Buffer.byteLength(body) > this.#maxBytes) throw new Error(`Loop state exceeds maximum size of ${this.#maxBytes} bytes`);
12631
- const temporary = join18(directory, `.state.${process.pid}.${randomUUID12()}.tmp`);
13038
+ const temporary = join19(directory, `.state.${process.pid}.${randomUUID12()}.tmp`);
12632
13039
  let handle;
12633
13040
  try {
12634
13041
  handle = await open6(temporary, constants5.O_CREAT | constants5.O_EXCL | constants5.O_WRONLY, 384);
@@ -12644,11 +13051,11 @@ var LoopStore = class {
12644
13051
  }
12645
13052
  async load(loopId) {
12646
13053
  const directory = await this.#safeExisting(loopId);
12647
- const target = join18(directory, "state.json");
13054
+ const target = join19(directory, "state.json");
12648
13055
  try {
12649
- const metadata = await stat6(target);
13056
+ const metadata = await stat7(target);
12650
13057
  if (!metadata.isFile() || metadata.size > this.#maxBytes) throw new Error("Loop state is not a bounded regular file");
12651
- const raw = await readFile12(target, "utf8");
13058
+ const raw = await readFile13(target, "utf8");
12652
13059
  const state = LoopStateSchema.parse(JSON.parse(raw));
12653
13060
  await this.#assertWorkspace(state.workspace);
12654
13061
  return state;
@@ -12664,7 +13071,7 @@ var LoopStore = class {
12664
13071
  const previous = this.#appendTails.get(event.loopId) ?? Promise.resolve();
12665
13072
  const next = previous.catch(() => void 0).then(async () => {
12666
13073
  const directory = await this.#prepare(event.loopId);
12667
- const path = join18(directory, "events.jsonl");
13074
+ const path = join19(directory, "events.jsonl");
12668
13075
  const handle = await open6(path, constants5.O_CREAT | constants5.O_APPEND | constants5.O_WRONLY, 384);
12669
13076
  try {
12670
13077
  await handle.writeFile(`${JSON.stringify(event)}
@@ -12681,7 +13088,7 @@ var LoopStore = class {
12681
13088
  await assertNoSymlink2(this.#workspace, dirname10(this.#root));
12682
13089
  await mkdir13(this.#root, { recursive: true, mode: 448 });
12683
13090
  await assertNoSymlink2(this.#workspace, this.#root);
12684
- const directory = join18(this.#root, loopId);
13091
+ const directory = join19(this.#root, loopId);
12685
13092
  await mkdir13(directory, { recursive: true, mode: 448 });
12686
13093
  await assertNoSymlink2(this.#workspace, directory);
12687
13094
  return directory;
@@ -12689,7 +13096,7 @@ var LoopStore = class {
12689
13096
  async #safeExisting(loopId) {
12690
13097
  validateId(loopId);
12691
13098
  await assertNoSymlink2(this.#workspace, this.#root);
12692
- const directory = join18(this.#root, loopId);
13099
+ const directory = join19(this.#root, loopId);
12693
13100
  await assertNoSymlink2(this.#workspace, directory);
12694
13101
  return directory;
12695
13102
  }
@@ -12737,13 +13144,13 @@ function isCode5(error, code) {
12737
13144
  }
12738
13145
 
12739
13146
  // src/loop/verifier.ts
12740
- import { readFile as readFile13 } from "fs/promises";
12741
- import { join as join20 } from "path";
13147
+ import { readFile as readFile14 } from "fs/promises";
13148
+ import { join as join21 } from "path";
12742
13149
 
12743
13150
  // src/tools/shell.ts
12744
13151
  import { spawn as spawn4 } from "child_process";
12745
13152
  import { existsSync as existsSync4 } from "fs";
12746
- import { basename as basename5, delimiter as delimiter2, isAbsolute as isAbsolute9, join as join19, relative as relative11, resolve as resolve15, sep as sep11 } from "path";
13153
+ import { basename as basename5, delimiter as delimiter2, isAbsolute as isAbsolute9, join as join20, relative as relative11, resolve as resolve15, sep as sep11 } from "path";
12747
13154
  import { z as z23 } from "zod";
12748
13155
 
12749
13156
  // src/tools/jobs.ts
@@ -13406,7 +13813,7 @@ function decodeOutput(buffer) {
13406
13813
  return new TextDecoder(encoding).decode(buffer);
13407
13814
  }
13408
13815
  function shellEnvironment(root, environment) {
13409
- const localBin = join19(root, "node_modules", ".bin");
13816
+ const localBin = join20(root, "node_modules", ".bin");
13410
13817
  if (!existsSync4(localBin)) return environment;
13411
13818
  const pathKey = Object.keys(environment).find((key) => key.toLowerCase() === "path") ?? "PATH";
13412
13819
  const current = environment[pathKey] ?? "";
@@ -13506,16 +13913,17 @@ function incompleteUtf8SuffixLength(buffer) {
13506
13913
 
13507
13914
  // src/loop/verifier.ts
13508
13915
  var SCRIPT_PRIORITY = ["test", "typecheck", "lint", "build", "smoke:install"];
13916
+ var MAX_PERSISTED_VERIFICATION_STREAM_BYTES = 1024;
13509
13917
  async function inferVerificationPlan(workspace) {
13510
13918
  const commands = [];
13511
13919
  const seen = /* @__PURE__ */ new Set();
13512
- const packageJson = await optionalJson(join20(workspace, "package.json"));
13920
+ const packageJson = await optionalJson(join21(workspace, "package.json"));
13513
13921
  const scripts = isRecord3(packageJson?.scripts) ? packageJson.scripts : {};
13514
13922
  for (const name of SCRIPT_PRIORITY) {
13515
13923
  if (typeof scripts[name] !== "string" || isUnconditionalPassThrough(scripts[name])) continue;
13516
13924
  add(commands, seen, npmScript(name));
13517
13925
  }
13518
- const flavor = await optionalText(join20(workspace, "FLAVOR.md"));
13926
+ const flavor = await optionalText(join21(workspace, "FLAVOR.md"));
13519
13927
  if (flavor !== void 0) {
13520
13928
  for (const match of flavor.matchAll(/^- `([^`]+)`\s*$/gm)) {
13521
13929
  const parsed = parseTrustedCommand(match[1] ?? "");
@@ -13541,9 +13949,9 @@ async function runVerificationPlan(plan, workspace, signal, executionEnvironment
13541
13949
  command: item.command,
13542
13950
  args: item.args,
13543
13951
  exitCode: result.exitCode,
13544
- stdout: result.stdout,
13545
- stderr: result.stderr,
13546
- truncated: result.truncated
13952
+ stdout: boundedEvidenceTail(result.stdout),
13953
+ stderr: boundedEvidenceTail(result.stderr),
13954
+ truncated: result.truncated || Buffer.byteLength(result.stdout, "utf8") > MAX_PERSISTED_VERIFICATION_STREAM_BYTES || Buffer.byteLength(result.stderr, "utf8") > MAX_PERSISTED_VERIFICATION_STREAM_BYTES
13547
13955
  });
13548
13956
  if (result.exitCode !== 0) {
13549
13957
  return { passed: false, commands, summary: `${item.label} failed with exit code ${String(result.exitCode)}.` };
@@ -13551,6 +13959,14 @@ async function runVerificationPlan(plan, workspace, signal, executionEnvironment
13551
13959
  }
13552
13960
  return { passed: true, commands, summary: `Passed ${commands.length} deterministic verification command(s).` };
13553
13961
  }
13962
+ function boundedEvidenceTail(value) {
13963
+ const encoded = Buffer.from(value, "utf8");
13964
+ if (encoded.byteLength <= MAX_PERSISTED_VERIFICATION_STREAM_BYTES) return value;
13965
+ const prefix = Buffer.from("\u2026\n", "utf8");
13966
+ let start = encoded.byteLength - (MAX_PERSISTED_VERIFICATION_STREAM_BYTES - prefix.byteLength);
13967
+ while (start < encoded.byteLength && (encoded[start] & 192) === 128) start += 1;
13968
+ return `${prefix.toString("utf8")}${encoded.subarray(start).toString("utf8")}`;
13969
+ }
13554
13970
  function npmScript(name) {
13555
13971
  return name === "test" ? { label: name, command: "npm", args: ["test"] } : { label: name, command: "npm", args: ["run", name] };
13556
13972
  }
@@ -13582,7 +13998,7 @@ function isUnconditionalPassThrough(script) {
13582
13998
  }
13583
13999
  async function optionalText(path) {
13584
14000
  try {
13585
- return await readFile13(path, "utf8");
14001
+ return await readFile14(path, "utf8");
13586
14002
  } catch (error) {
13587
14003
  if (isCode6(error, "ENOENT")) return void 0;
13588
14004
  throw error;
@@ -13603,7 +14019,7 @@ function isCode6(error, code) {
13603
14019
 
13604
14020
  // src/models/anthropic.ts
13605
14021
  import Anthropic from "@anthropic-ai/sdk";
13606
- import { readFile as readFile14 } from "fs/promises";
14022
+ import { readFile as readFile15 } from "fs/promises";
13607
14023
 
13608
14024
  // src/utils/json.ts
13609
14025
  function safeParseJson(input) {
@@ -13652,11 +14068,11 @@ function isEnvTruthy(value) {
13652
14068
 
13653
14069
  // src/utils/log.ts
13654
14070
  import { appendFile as appendFile3, mkdir as mkdir14 } from "fs/promises";
13655
- import { dirname as dirname11, join as join21, resolve as resolve16 } from "path";
14071
+ import { dirname as dirname11, join as join22, resolve as resolve16 } from "path";
13656
14072
  var pending = Promise.resolve();
13657
14073
  var usagePending = Promise.resolve();
13658
14074
  function usageLogPath() {
13659
- return process.env.FLAVOR_USAGE_FILE ?? join21(process.cwd(), ".flavor", "usage.jsonl");
14075
+ return process.env.FLAVOR_USAGE_FILE ?? join22(process.cwd(), ".flavor", "usage.jsonl");
13660
14076
  }
13661
14077
  var usageSessionId = "unknown";
13662
14078
  var truncateOnNextAppend = false;
@@ -13685,7 +14101,7 @@ var AuditLogger = class {
13685
14101
  #path;
13686
14102
  #closed = false;
13687
14103
  constructor(workspace) {
13688
- this.#path = join21(resolve16(workspace), ".flavor", "audit.jsonl");
14104
+ this.#path = join22(resolve16(workspace), ".flavor", "audit.jsonl");
13689
14105
  }
13690
14106
  get path() {
13691
14107
  return this.#path;
@@ -13709,6 +14125,35 @@ var AuditLogger = class {
13709
14125
  }
13710
14126
  };
13711
14127
 
14128
+ // src/utils/abort.ts
14129
+ function waitWithSignal(operation, signal) {
14130
+ if (signal === void 0) return operation;
14131
+ return new Promise((resolve44, reject) => {
14132
+ const abort2 = () => reject(signal.reason);
14133
+ if (signal.aborted) abort2();
14134
+ else signal.addEventListener("abort", abort2, { once: true });
14135
+ operation.then(resolve44, reject).finally(() => signal.removeEventListener("abort", abort2));
14136
+ });
14137
+ }
14138
+ function createScopedAbortSignal(parent) {
14139
+ const controller2 = new AbortController();
14140
+ let disposed = false;
14141
+ const abort2 = () => {
14142
+ if (!controller2.signal.aborted) controller2.abort(parent?.reason);
14143
+ };
14144
+ if (parent?.aborted) abort2();
14145
+ else parent?.addEventListener("abort", abort2, { once: true });
14146
+ return {
14147
+ signal: controller2.signal,
14148
+ dispose: () => {
14149
+ if (disposed) return;
14150
+ disposed = true;
14151
+ parent?.removeEventListener("abort", abort2);
14152
+ abort2();
14153
+ }
14154
+ };
14155
+ }
14156
+
13712
14157
  // src/models/anthropic.ts
13713
14158
  var CLAUDE_CLIENT_HEADERS = {
13714
14159
  "User-Agent": "claude-cli/2.1.0 (external, cli)",
@@ -13831,6 +14276,7 @@ var AnthropicModelAdapter = class {
13831
14276
  void appendUsageLog(line);
13832
14277
  }
13833
14278
  async *stream(request) {
14279
+ const requestAbort = createScopedAbortSignal(request.signal);
13834
14280
  let inputTokens = 0;
13835
14281
  let outputTokens = 0;
13836
14282
  let hasUsage = false;
@@ -13905,7 +14351,7 @@ var AnthropicModelAdapter = class {
13905
14351
  source: {
13906
14352
  type: "base64",
13907
14353
  media_type: block.mediaType,
13908
- data: (await readFile14(block.source.path)).toString("base64")
14354
+ data: (await readFile15(block.source.path)).toString("base64")
13909
14355
  }
13910
14356
  }));
13911
14357
  if (message2.cacheBreakpoint && Array.isArray(content) && content.length > 0) {
@@ -13954,13 +14400,13 @@ var AnthropicModelAdapter = class {
13954
14400
  };
13955
14401
  let stream;
13956
14402
  try {
13957
- stream = await this.client.messages.create(body, { signal: request.signal });
14403
+ stream = await this.client.messages.create(body, { signal: requestAbort.signal });
13958
14404
  } catch (error) {
13959
14405
  if (this.thinkingBudget > 0 && !this.#thinkingRejected && isThinkingParamRejected(error)) {
13960
14406
  this.#thinkingRejected = true;
13961
14407
  delete body.thinking;
13962
14408
  stripThinkingBlocks(body.messages);
13963
- stream = await this.client.messages.create(body, { signal: request.signal });
14409
+ stream = await this.client.messages.create(body, { signal: requestAbort.signal });
13964
14410
  } else {
13965
14411
  throw error;
13966
14412
  }
@@ -14072,6 +14518,8 @@ var AnthropicModelAdapter = class {
14072
14518
  };
14073
14519
  }
14074
14520
  yield { type: "error", error: normalizeProviderError(error) };
14521
+ } finally {
14522
+ requestAbort.dispose();
14075
14523
  }
14076
14524
  }
14077
14525
  };
@@ -14125,7 +14573,7 @@ function resolveCacheProfile(input) {
14125
14573
 
14126
14574
  // src/models/openai.ts
14127
14575
  import OpenAI from "openai";
14128
- import { readFile as readFile15 } from "fs/promises";
14576
+ import { readFile as readFile16 } from "fs/promises";
14129
14577
  async function toInput(message2) {
14130
14578
  if (message2.role === "tool") {
14131
14579
  if (!message2.toolCallId) throw new Error("Tool messages require toolCallId");
@@ -14137,7 +14585,7 @@ async function toInput(message2) {
14137
14585
  }
14138
14586
  const content = typeof message2.content === "string" ? message2.content : await Promise.all(message2.content.map(async (block) => block.type === "text" ? { type: "input_text", text: block.text } : {
14139
14587
  type: "input_image",
14140
- image_url: `data:${block.mediaType};base64,${(await readFile15(block.source.path)).toString("base64")}`,
14588
+ image_url: `data:${block.mediaType};base64,${(await readFile16(block.source.path)).toString("base64")}`,
14141
14589
  detail: "auto"
14142
14590
  }));
14143
14591
  return [
@@ -14216,6 +14664,7 @@ var OpenAIModelAdapter = class {
14216
14664
  void appendUsageLog(line);
14217
14665
  }
14218
14666
  async *stream(request) {
14667
+ const requestAbort = createScopedAbortSignal(request.signal);
14219
14668
  const callIds = /* @__PURE__ */ new Map();
14220
14669
  const pendingCalls = /* @__PURE__ */ new Map();
14221
14670
  const emittedCalls = /* @__PURE__ */ new Set();
@@ -14232,7 +14681,7 @@ var OpenAIModelAdapter = class {
14232
14681
  strict: tool.strict ?? true
14233
14682
  }))
14234
14683
  };
14235
- const stream = this.client.responses.stream(body, { signal: request.signal });
14684
+ const stream = this.client.responses.stream(body, { signal: requestAbort.signal });
14236
14685
  for await (const event of stream) {
14237
14686
  if (event.type === "response.output_item.added" && event.item?.type === "function_call" && event.output_index !== void 0 && event.item.call_id) {
14238
14687
  callIds.set(event.output_index, event.item.call_id);
@@ -14326,6 +14775,8 @@ var OpenAIModelAdapter = class {
14326
14775
  }
14327
14776
  } catch (error) {
14328
14777
  yield { type: "error", error: normalizeProviderError(error) };
14778
+ } finally {
14779
+ requestAbort.dispose();
14329
14780
  }
14330
14781
  }
14331
14782
  };
@@ -14559,7 +15010,7 @@ import {
14559
15010
 
14560
15011
  // src/utils/version.ts
14561
15012
  import { readFileSync as readFileSync2 } from "fs";
14562
- import { dirname as dirname12, join as join22 } from "path";
15013
+ import { dirname as dirname12, join as join23 } from "path";
14563
15014
  import { fileURLToPath } from "url";
14564
15015
  var cached;
14565
15016
  function packageVersion() {
@@ -14567,7 +15018,7 @@ function packageVersion() {
14567
15018
  let directory = dirname12(fileURLToPath(import.meta.url));
14568
15019
  for (; ; ) {
14569
15020
  try {
14570
- const manifest = JSON.parse(readFileSync2(join22(directory, "package.json"), "utf8"));
15021
+ const manifest = JSON.parse(readFileSync2(join23(directory, "package.json"), "utf8"));
14571
15022
  if (typeof manifest.version === "string" && manifest.version.length > 0) {
14572
15023
  cached = manifest.version;
14573
15024
  return cached;
@@ -14994,8 +15445,8 @@ function isStoredToken(value) {
14994
15445
  }
14995
15446
 
14996
15447
  // src/permissions/policy.ts
14997
- import { readFile as readFile16 } from "fs/promises";
14998
- import { join as join23, resolve as resolve18 } from "path";
15448
+ import { readFile as readFile17 } from "fs/promises";
15449
+ import { join as join24, resolve as resolve18 } from "path";
14999
15450
  import { z as z25 } from "zod";
15000
15451
  var DecisionSchema2 = z25.enum(["allow", "ask", "deny"]);
15001
15452
  var PrefixSchema = z25.array(z25.string().min(1).max(1024)).min(1).max(64);
@@ -15032,15 +15483,15 @@ var CompiledPermissionPolicy = class {
15032
15483
  async function loadPermissionPolicy(options) {
15033
15484
  const sources = [
15034
15485
  ...options.managedPath === void 0 ? [] : [{ tier: "managed", path: resolve18(options.managedPath) }],
15035
- { tier: "user", path: join23(resolve18(options.home), ".flavor-code", "permissions.json") },
15036
- { tier: "project", path: join23(resolve18(options.workspace), ".flavor", "permissions.json") },
15037
- { tier: "local", path: join23(resolve18(options.workspace), ".flavor", "permissions.local.json") }
15486
+ { tier: "user", path: join24(resolve18(options.home), ".flavor-code", "permissions.json") },
15487
+ { tier: "project", path: join24(resolve18(options.workspace), ".flavor", "permissions.json") },
15488
+ { tier: "local", path: join24(resolve18(options.workspace), ".flavor", "permissions.local.json") }
15038
15489
  ];
15039
15490
  const rules = [];
15040
15491
  for (const source of sources) {
15041
15492
  let raw;
15042
15493
  try {
15043
- raw = await readFile16(source.path, "utf8");
15494
+ raw = await readFile17(source.path, "utf8");
15044
15495
  } catch (error) {
15045
15496
  if (isCode7(error, "ENOENT")) continue;
15046
15497
  throw error;
@@ -15806,8 +16257,8 @@ function createSkillResourceTool(registry) {
15806
16257
 
15807
16258
  // src/session/store.ts
15808
16259
  import { constants as constants7 } from "fs";
15809
- import { lstat as lstat8, mkdir as mkdir15, open as open7, readFile as readFile17, readdir as readdir6, realpath as realpath10, rename as rename6, rm as rm8, stat as stat7 } from "fs/promises";
15810
- import { basename as basename6, dirname as dirname14, join as join24, relative as relative13, resolve as resolve20, sep as sep13 } from "path";
16260
+ import { lstat as lstat8, mkdir as mkdir15, open as open7, readFile as readFile18, readdir as readdir6, realpath as realpath10, rename as rename6, rm as rm8, stat as stat8 } from "fs/promises";
16261
+ import { basename as basename6, dirname as dirname14, join as join25, relative as relative13, resolve as resolve20, sep as sep13 } from "path";
15811
16262
  import { randomUUID as randomUUID13 } from "crypto";
15812
16263
  import { z as z27 } from "zod";
15813
16264
 
@@ -15821,6 +16272,11 @@ function appendThinkingText(previous, delta) {
15821
16272
  // src/ui/transcript.ts
15822
16273
  var MAX_TRANSCRIPT_TURNS = 200;
15823
16274
  var MAX_TRANSCRIPT_CHARS = 2e6;
16275
+ var MAX_ACTIVE_TRANSCRIPT_BLOCKS = 160;
16276
+ var MAX_ACTIVE_ASSISTANT_CHARS = 64e3;
16277
+ var MAX_TRANSCRIPT_BLOCK_TEXT_CHARS = 16e3;
16278
+ var MAX_TRANSCRIPT_TOOL_VALUE_CHARS = 16e3;
16279
+ var MAX_TRANSCRIPT_PRESENTATION_CHARS = 32e3;
15824
16280
  function createTranscriptState() {
15825
16281
  return { completed: [], nextId: 1 };
15826
16282
  }
@@ -16181,11 +16637,9 @@ function applyTaskSnapshot(turn, snapshot4, includeTerminal = true) {
16181
16637
  else blocks[index] = block;
16182
16638
  }
16183
16639
  return {
16184
- ...turn,
16640
+ ...withBlocks(turn, blocks),
16185
16641
  taskSnapshot: snapshot4,
16186
- suppressedTaskIds: [...suppressed],
16187
- blocks,
16188
- statusLines: blocks.filter((block) => block.kind === "status").map((block) => block.text)
16642
+ suppressedTaskIds: [...suppressed]
16189
16643
  };
16190
16644
  }
16191
16645
  function terminalTaskIds(snapshot4) {
@@ -16199,13 +16653,10 @@ function upsertStatus(state, block) {
16199
16653
  if (state.active === void 0) return state;
16200
16654
  const blocks = [...state.active.blocks];
16201
16655
  const index = blocks.findIndex((item) => item.kind === "status" && item.id === block.id);
16202
- if (index < 0) blocks.push(block);
16203
- else blocks[index] = block;
16204
- return { ...state, active: {
16205
- ...state.active,
16206
- statusLines: blocks.filter((item) => item.kind === "status").map((item) => item.text),
16207
- blocks
16208
- } };
16656
+ const bounded2 = boundedTranscriptBlock(block);
16657
+ if (index < 0) blocks.push(bounded2);
16658
+ else blocks[index] = bounded2;
16659
+ return { ...state, active: withBlocks(state.active, blocks) };
16209
16660
  }
16210
16661
  function finishActive(state) {
16211
16662
  if (state.active === void 0) return state;
@@ -16229,16 +16680,12 @@ function attachThinking(turn, text) {
16229
16680
  ...block,
16230
16681
  thinkingText: appendThinkingText(block.thinkingText, text)
16231
16682
  };
16232
- return { ...turn, blocks };
16683
+ return withBlocks(turn, blocks);
16233
16684
  }
16234
16685
  function withoutModelActivity(turn, id) {
16235
16686
  const blocks = turn.blocks.filter((block) => block.kind !== "status" || block.activity !== "model" || id !== void 0 && block.id !== id);
16236
16687
  if (blocks.length === turn.blocks.length) return turn;
16237
- return {
16238
- ...turn,
16239
- blocks,
16240
- statusLines: blocks.filter((block) => block.kind === "status").map((block) => block.text)
16241
- };
16688
+ return withBlocks(turn, blocks);
16242
16689
  }
16243
16690
  function restoreTranscriptState(state) {
16244
16691
  const completed = state.completed.map(cloneTurn);
@@ -16340,15 +16787,18 @@ function cloneTurn(turn) {
16340
16787
  function upsertTurnStatus(turn, block) {
16341
16788
  const blocks = [...turn.blocks];
16342
16789
  const index = blocks.findIndex((item) => item.kind === "status" && item.id === block.id);
16343
- if (index < 0) blocks.push(block);
16344
- else blocks[index] = block;
16790
+ const bounded2 = boundedTranscriptBlock(block);
16791
+ if (index < 0) blocks.push(bounded2);
16792
+ else blocks[index] = bounded2;
16345
16793
  return withBlocks(turn, blocks);
16346
16794
  }
16347
16795
  function withBlocks(turn, blocks) {
16796
+ const bounded2 = blocks.length <= MAX_ACTIVE_TRANSCRIPT_BLOCKS ? blocks : blocks.slice(-MAX_ACTIVE_TRANSCRIPT_BLOCKS);
16348
16797
  return {
16349
16798
  ...turn,
16350
- blocks,
16351
- statusLines: blocks.filter((block) => block.kind === "status").map((block) => block.text)
16799
+ assistantText: tailText(turn.assistantText, MAX_ACTIVE_ASSISTANT_CHARS),
16800
+ blocks: bounded2,
16801
+ statusLines: bounded2.filter((block) => block.kind === "status").map((block) => block.text)
16352
16802
  };
16353
16803
  }
16354
16804
  function appendLine(text, line) {
@@ -16362,19 +16812,95 @@ function stripRetryBlocks(turn) {
16362
16812
  if (block.id.startsWith("structured-retry:")) return false;
16363
16813
  return true;
16364
16814
  });
16365
- return {
16366
- ...turn,
16367
- blocks,
16368
- statusLines: blocks.filter((item) => item.kind === "status").map((item) => item.text)
16369
- };
16815
+ return withBlocks(turn, blocks);
16370
16816
  }
16371
16817
  function addText(turn, text, onNewLine = false) {
16372
- const assistantText = onNewLine ? appendLine(turn.assistantText, text) : turn.assistantText + text;
16818
+ const assistantText = tailText(
16819
+ onNewLine ? appendLine(turn.assistantText, text) : turn.assistantText + text,
16820
+ MAX_ACTIVE_ASSISTANT_CHARS
16821
+ );
16373
16822
  const blocks = [...turn.blocks];
16374
16823
  const last = blocks[blocks.length - 1];
16375
- if (!onNewLine && last?.kind === "text") blocks[blocks.length - 1] = { kind: "text", text: last.text + text };
16376
- else blocks.push({ kind: "text", text });
16377
- return { ...turn, assistantText, blocks };
16824
+ if (!onNewLine && last?.kind === "text") {
16825
+ blocks[blocks.length - 1] = { kind: "text", text: tailText(last.text + text, MAX_TRANSCRIPT_BLOCK_TEXT_CHARS) };
16826
+ } else {
16827
+ blocks.push({ kind: "text", text: tailText(text, MAX_TRANSCRIPT_BLOCK_TEXT_CHARS) });
16828
+ }
16829
+ return withBlocks({ ...turn, assistantText }, blocks);
16830
+ }
16831
+ function boundedTranscriptBlock(block) {
16832
+ const presentation = block.presentation === void 0 ? void 0 : boundedTranscriptPresentation(block.presentation);
16833
+ const tool = block.tool === void 0 ? void 0 : {
16834
+ name: block.tool.name,
16835
+ input: boundedTranscriptValue(block.tool.input),
16836
+ ...block.tool.result === void 0 ? {} : {
16837
+ result: {
16838
+ ok: block.tool.result.ok,
16839
+ ...block.tool.result.output === void 0 ? {} : { output: boundedTranscriptValue(block.tool.result.output) },
16840
+ ...block.tool.result.content === void 0 ? {} : {
16841
+ content: tailText(block.tool.result.content, MAX_TRANSCRIPT_TOOL_VALUE_CHARS)
16842
+ },
16843
+ ...block.tool.result.presentation === void 0 ? {} : {
16844
+ presentation: boundedTranscriptPresentation(block.tool.result.presentation)
16845
+ },
16846
+ ...block.tool.result.additionalContext === void 0 ? {} : {
16847
+ additionalContext: block.tool.result.additionalContext.slice(-20).map((item) => tailText(item, MAX_TRANSCRIPT_TOOL_VALUE_CHARS))
16848
+ },
16849
+ ...block.tool.result.error === void 0 ? {} : { error: {
16850
+ code: block.tool.result.error.code,
16851
+ message: tailText(block.tool.result.error.message, MAX_TRANSCRIPT_BLOCK_TEXT_CHARS)
16852
+ } }
16853
+ }
16854
+ }
16855
+ };
16856
+ return {
16857
+ ...block,
16858
+ text: tailText(block.text, MAX_TRANSCRIPT_BLOCK_TEXT_CHARS),
16859
+ ...block.hint === void 0 ? {} : { hint: tailText(block.hint, MAX_TRANSCRIPT_BLOCK_TEXT_CHARS) },
16860
+ ...block.details === void 0 ? {} : { details: tailText(block.details, MAX_TRANSCRIPT_BLOCK_TEXT_CHARS) },
16861
+ ...presentation === void 0 ? {} : { presentation },
16862
+ ...tool === void 0 ? {} : { tool }
16863
+ };
16864
+ }
16865
+ function boundedTranscriptPresentation(presentation) {
16866
+ return jsonSize(presentation) <= MAX_TRANSCRIPT_PRESENTATION_CHARS ? presentation : {
16867
+ kind: "generic",
16868
+ title: "Tool output",
16869
+ summary: "Large presentation omitted from the live transcript; the model-facing result remains available in context."
16870
+ };
16871
+ }
16872
+ function boundedTranscriptValue(value) {
16873
+ const chars = jsonSize(value);
16874
+ if (chars <= MAX_TRANSCRIPT_TOOL_VALUE_CHARS) return value;
16875
+ const record3 = typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
16876
+ const summary2 = { truncated: true, originalChars: chars };
16877
+ for (const key of ["path", "command", "id", "name", "exitCode", "bytes", "replacements"]) {
16878
+ const item = record3?.[key];
16879
+ if (typeof item === "string") summary2[key] = tailText(item, 1e3);
16880
+ else if (typeof item === "number" || typeof item === "boolean" || item === null) summary2[key] = item;
16881
+ }
16882
+ return summary2;
16883
+ }
16884
+ function jsonSize(value) {
16885
+ try {
16886
+ return JSON.stringify(value)?.length ?? 0;
16887
+ } catch {
16888
+ return Number.POSITIVE_INFINITY;
16889
+ }
16890
+ }
16891
+ function tailText(text, limit) {
16892
+ if (text.length <= limit) return text;
16893
+ const marker = `\u2026 ${text.length - limit} earlier characters omitted \u2026
16894
+ `;
16895
+ return marker.length >= limit ? text.slice(-limit) : marker + text.slice(-(limit - marker.length));
16896
+ }
16897
+ function transcriptCensus(state) {
16898
+ const turns = [...state.completed, ...state.active === void 0 ? [] : [state.active]];
16899
+ return {
16900
+ turns: turns.length,
16901
+ blocks: turns.reduce((total, turn) => total + turn.blocks.length, 0),
16902
+ chars: jsonSize(state)
16903
+ };
16378
16904
  }
16379
16905
 
16380
16906
  // src/session/store.ts
@@ -16503,7 +17029,7 @@ var SessionStore = class {
16503
17029
  #maxSessions;
16504
17030
  constructor(options) {
16505
17031
  this.#workspace = resolve20(options.workspace);
16506
- this.#sessions = join24(this.#workspace, ".flavor", "sessions");
17032
+ this.#sessions = join25(this.#workspace, ".flavor", "sessions");
16507
17033
  this.#maxBytes = options.maxBytes ?? DEFAULT_MAX_SESSION_BYTES;
16508
17034
  if (!Number.isSafeInteger(this.#maxBytes) || this.#maxBytes < 256) throw new Error("maxBytes must be an integer of at least 256");
16509
17035
  this.#maxSessions = options.maxSessions ?? 50;
@@ -16535,7 +17061,7 @@ var SessionStore = class {
16535
17061
  const body = `${lines.join("\n")}
16536
17062
  `;
16537
17063
  if (Buffer.byteLength(body) > this.#maxBytes) throw new Error(`Session exceeds maximum size of ${this.#maxBytes} bytes`);
16538
- const temporary = join24(this.#sessions, `.${document.sessionId}.${process.pid}.${randomUUID13()}.tmp`);
17064
+ const temporary = join25(this.#sessions, `.${document.sessionId}.${process.pid}.${randomUUID13()}.tmp`);
16539
17065
  let handle;
16540
17066
  try {
16541
17067
  handle = await open7(temporary, constants7.O_CREAT | constants7.O_EXCL | constants7.O_WRONLY, 384);
@@ -16554,7 +17080,7 @@ var SessionStore = class {
16554
17080
  async acquireLease(sessionId) {
16555
17081
  SessionIdSchema.parse(sessionId);
16556
17082
  await this.#prepareDirectory();
16557
- const path = join24(this.#sessions, `.${sessionId}.lock`);
17083
+ const path = join25(this.#sessions, `.${sessionId}.lock`);
16558
17084
  const token = JSON.stringify({ pid: process.pid, nonce: randomUUID13(), createdAt: (/* @__PURE__ */ new Date()).toISOString() });
16559
17085
  for (let attempt = 0; attempt < 3; attempt += 1) {
16560
17086
  let handle;
@@ -16572,7 +17098,7 @@ var SessionStore = class {
16572
17098
  release: async () => {
16573
17099
  if (released) return;
16574
17100
  try {
16575
- if (await readFile17(path, "utf8") === token) await rm8(path, { force: true });
17101
+ if (await readFile18(path, "utf8") === token) await rm8(path, { force: true });
16576
17102
  } catch (error) {
16577
17103
  if (!isCode8(error, "ENOENT")) throw error;
16578
17104
  }
@@ -16688,14 +17214,14 @@ var SessionStore = class {
16688
17214
  if (isCode8(error, "ENOENT")) return;
16689
17215
  throw error;
16690
17216
  }
16691
- const owner2 = await reclaimStaleSessionLock(join24(this.#sessions, `.${sessionId}.lock`));
17217
+ const owner2 = await reclaimStaleSessionLock(join25(this.#sessions, `.${sessionId}.lock`));
16692
17218
  if (owner2 !== void 0) throw new Error(`Session "${sessionId}" is active in process ${owner2} and cannot be deleted`);
16693
17219
  const path = this.#path(sessionId);
16694
17220
  try {
16695
17221
  const metadata = await lstat8(path);
16696
17222
  if (!metadata.isFile()) throw new Error("Session path is not a regular file");
16697
17223
  await rm8(path);
16698
- await rm8(join24(this.#sessions, `${sessionId}.events.jsonl`), { force: true }).catch(() => void 0);
17224
+ await rm8(join25(this.#sessions, `${sessionId}.events.jsonl`), { force: true }).catch(() => void 0);
16699
17225
  } catch (error) {
16700
17226
  if (!isCode8(error, "ENOENT")) throw error;
16701
17227
  }
@@ -16711,14 +17237,14 @@ var SessionStore = class {
16711
17237
  if (entries.length <= this.#maxSessions) return;
16712
17238
  let retained = 0;
16713
17239
  for (const entry2 of entries) {
16714
- const owner2 = await reclaimStaleSessionLock(join24(this.#sessions, `.${entry2.sessionId}.lock`));
17240
+ const owner2 = await reclaimStaleSessionLock(join25(this.#sessions, `.${entry2.sessionId}.lock`));
16715
17241
  if (owner2 !== void 0) continue;
16716
17242
  if (retained < this.#maxSessions) {
16717
17243
  retained += 1;
16718
17244
  continue;
16719
17245
  }
16720
17246
  await rm8(this.#path(entry2.sessionId), { force: true }).catch(() => void 0);
16721
- await rm8(join24(this.#sessions, `${entry2.sessionId}.events.jsonl`), { force: true }).catch(() => void 0);
17247
+ await rm8(join25(this.#sessions, `${entry2.sessionId}.events.jsonl`), { force: true }).catch(() => void 0);
16722
17248
  }
16723
17249
  }
16724
17250
  async #assertWorkspace(stored) {
@@ -16766,14 +17292,14 @@ function storedSessionVersion(raw) {
16766
17292
  }
16767
17293
  async function reclaimStaleSessionLock(path) {
16768
17294
  try {
16769
- const token = await readFile17(path, "utf8");
16770
- const metadata = await stat7(path);
17295
+ const token = await readFile18(path, "utf8");
17296
+ const metadata = await stat8(path);
16771
17297
  const owner2 = parseSessionLockOwner(token);
16772
17298
  if (owner2 !== void 0 && isProcessAlive2(owner2)) return owner2;
16773
17299
  if (owner2 === void 0 && Date.now() - metadata.mtimeMs <= INVALID_SESSION_LOCK_STALE_MS) {
16774
17300
  throw new Error(`Session writer lock ${path} is incomplete or invalid; retry after the owning process exits`);
16775
17301
  }
16776
- if (await readFile17(path, "utf8") !== token) return void 0;
17302
+ if (await readFile18(path, "utf8") !== token) return void 0;
16777
17303
  const stale = `${path}.stale-${process.pid}-${randomUUID13()}`;
16778
17304
  try {
16779
17305
  await rename6(path, stale);
@@ -16806,10 +17332,10 @@ function isProcessAlive2(pid) {
16806
17332
  }
16807
17333
  }
16808
17334
  async function boundedRead(path, maxBytes) {
16809
- const metadata = await stat7(path);
17335
+ const metadata = await stat8(path);
16810
17336
  if (!metadata.isFile()) throw new Error("Session path is not a regular file");
16811
17337
  if (metadata.size > maxBytes) throw new Error(`Session exceeds maximum read size of ${maxBytes} bytes`);
16812
- const value = await readFile17(path);
17338
+ const value = await readFile18(path);
16813
17339
  if (value.byteLength > maxBytes) throw new Error(`Session exceeds maximum read size of ${maxBytes} bytes`);
16814
17340
  return value.toString("utf8");
16815
17341
  }
@@ -17001,14 +17527,14 @@ function sanitize2(input, seen = /* @__PURE__ */ new WeakSet()) {
17001
17527
 
17002
17528
  // src/session/tree.ts
17003
17529
  import { randomUUID as randomUUID15 } from "crypto";
17004
- import { mkdir as mkdir17, readFile as readFile19, rename as rename8, writeFile as writeFile11 } from "fs/promises";
17005
- import { dirname as dirname16, join as join26, resolve as resolve22 } from "path";
17530
+ import { mkdir as mkdir17, readFile as readFile20, rename as rename8, writeFile as writeFile11 } from "fs/promises";
17531
+ import { dirname as dirname16, join as join27, resolve as resolve22 } from "path";
17006
17532
 
17007
17533
  // src/session/checkpoint.ts
17008
17534
  import { createHash as createHash15, randomUUID as randomUUID14 } from "crypto";
17009
17535
  import { execFile as execFile2 } from "child_process";
17010
- import { lstat as lstat9, mkdir as mkdir16, open as open8, readFile as readFile18, readdir as readdir7, rename as rename7, rm as rm9, writeFile as writeFile10 } from "fs/promises";
17011
- import { dirname as dirname15, join as join25, relative as relative14, resolve as resolve21, sep as sep14 } from "path";
17536
+ import { lstat as lstat9, mkdir as mkdir16, open as open8, readFile as readFile19, readdir as readdir7, rename as rename7, rm as rm9, writeFile as writeFile10 } from "fs/promises";
17537
+ import { dirname as dirname15, join as join26, relative as relative14, resolve as resolve21, sep as sep14 } from "path";
17012
17538
  var EXCLUDED = /* @__PURE__ */ new Set([".git", ".flavor", "node_modules", "dist", "build", "release", ".worktrees"]);
17013
17539
  var MAX_FILE_BYTES = 16 * 1024 * 1024;
17014
17540
  var MAX_TOTAL_BYTES = 128 * 1024 * 1024;
@@ -17021,9 +17547,9 @@ var WorkspaceCheckpointStore = class {
17021
17547
  #maxTotalBytes;
17022
17548
  constructor(options) {
17023
17549
  this.#workspace = resolve21(options.workspace);
17024
- this.#root = join25(this.#workspace, ".flavor", "checkpoints");
17025
- this.#objects = join25(this.#root, "objects");
17026
- this.#manifests = join25(this.#root, "manifests");
17550
+ this.#root = join26(this.#workspace, ".flavor", "checkpoints");
17551
+ this.#objects = join26(this.#root, "objects");
17552
+ this.#manifests = join26(this.#root, "manifests");
17027
17553
  this.#maxFileBytes = options.maxFileBytes ?? MAX_FILE_BYTES;
17028
17554
  this.#maxTotalBytes = options.maxTotalBytes ?? MAX_TOTAL_BYTES;
17029
17555
  }
@@ -17041,9 +17567,9 @@ var WorkspaceCheckpointStore = class {
17041
17567
  if (info.size > this.#maxFileBytes) throw new Error(`Checkpoint file exceeds size limit: ${path}`);
17042
17568
  total += info.size;
17043
17569
  if (total > this.#maxTotalBytes) throw new Error("Checkpoint exceeds total size limit");
17044
- const content = await readFile18(absolute);
17570
+ const content = await readFile19(absolute);
17045
17571
  const digest = createHash15("sha256").update(content).digest("hex");
17046
- const objectPath = join25(this.#objects, digest);
17572
+ const objectPath = join26(this.#objects, digest);
17047
17573
  try {
17048
17574
  const handle = await open8(objectPath, "wx");
17049
17575
  try {
@@ -17063,12 +17589,12 @@ var WorkspaceCheckpointStore = class {
17063
17589
  ...label === void 0 || label.trim().length === 0 ? {} : { label: label.trim() },
17064
17590
  files
17065
17591
  };
17066
- await atomicJson(join25(this.#manifests, `${checkpoint.id}.json`), checkpoint);
17592
+ await atomicJson(join26(this.#manifests, `${checkpoint.id}.json`), checkpoint);
17067
17593
  return checkpoint;
17068
17594
  }
17069
17595
  async load(id) {
17070
17596
  if (!/^checkpoint-[A-Za-z0-9-]+$/.test(id)) throw new Error("Invalid checkpoint id");
17071
- const value = JSON.parse(await readFile18(join25(this.#manifests, `${id}.json`), "utf8"));
17597
+ const value = JSON.parse(await readFile19(join26(this.#manifests, `${id}.json`), "utf8"));
17072
17598
  if (value.version !== 1 || value.id !== id || !Array.isArray(value.files)) throw new Error("Invalid checkpoint manifest");
17073
17599
  for (const file of value.files) {
17074
17600
  contained(this.#workspace, file.path);
@@ -17080,7 +17606,7 @@ var WorkspaceCheckpointStore = class {
17080
17606
  const checkpoint = await this.load(id);
17081
17607
  const objects = /* @__PURE__ */ new Map();
17082
17608
  for (const file of checkpoint.files) {
17083
- const object = await readFile18(join25(this.#objects, file.digest));
17609
+ const object = await readFile19(join26(this.#objects, file.digest));
17084
17610
  if (object.length !== file.size || createHash15("sha256").update(object).digest("hex") !== file.digest) {
17085
17611
  throw new Error(`Checkpoint object is corrupt: ${file.digest}`);
17086
17612
  }
@@ -17103,7 +17629,7 @@ var WorkspaceCheckpointStore = class {
17103
17629
  }
17104
17630
  };
17105
17631
  async function discoverFiles(workspace) {
17106
- if (await exists2(join25(workspace, ".git"))) {
17632
+ if (await exists2(join26(workspace, ".git"))) {
17107
17633
  const listed = await gitFiles(workspace);
17108
17634
  return listed.filter((path) => !EXCLUDED.has(path.split("/")[0] ?? "")).sort();
17109
17635
  }
@@ -17112,7 +17638,7 @@ async function discoverFiles(workspace) {
17112
17638
  const entries = await readdir7(directory, { withFileTypes: true });
17113
17639
  for (const entry2 of entries) {
17114
17640
  if (EXCLUDED.has(entry2.name)) continue;
17115
- const absolute = join25(directory, entry2.name);
17641
+ const absolute = join26(directory, entry2.name);
17116
17642
  const path = normalizeRelative(relative14(workspace, absolute));
17117
17643
  if (entry2.isSymbolicLink()) throw new Error(`Checkpoint refuses symbolic link: ${path}`);
17118
17644
  if (entry2.isDirectory()) await walk(absolute);
@@ -17187,17 +17713,17 @@ var SessionHistory = class _SessionHistory {
17187
17713
  #leafId;
17188
17714
  #unrevert;
17189
17715
  constructor(options, document) {
17190
- this.#path = join26(resolve22(options.workspace), ".flavor", "session-trees", options.sessionId, "tree.json");
17716
+ this.#path = join27(resolve22(options.workspace), ".flavor", "session-trees", options.sessionId, "tree.json");
17191
17717
  this.#checkpoints = new WorkspaceCheckpointStore({ workspace: options.workspace });
17192
17718
  this.#restoreContext = options.restoreContext;
17193
17719
  this.#nodes = boundedNodes(document.nodes);
17194
17720
  this.#leafId = document.leafId !== null && this.#nodes.some((node) => node.id === document.leafId) ? document.leafId : this.#nodes.at(-1)?.id ?? null;
17195
17721
  }
17196
17722
  static async open(options) {
17197
- const path = join26(resolve22(options.workspace), ".flavor", "session-trees", options.sessionId, "tree.json");
17723
+ const path = join27(resolve22(options.workspace), ".flavor", "session-trees", options.sessionId, "tree.json");
17198
17724
  let document = { version: 1, sessionId: options.sessionId, leafId: null, nodes: [] };
17199
17725
  try {
17200
- const parsed = JSON.parse(await readFile19(path, "utf8"));
17726
+ const parsed = JSON.parse(await readFile20(path, "utf8"));
17201
17727
  validate(parsed, options.sessionId);
17202
17728
  document = parsed;
17203
17729
  } catch (error) {
@@ -17210,6 +17736,10 @@ var SessionHistory = class _SessionHistory {
17210
17736
  get leafId() {
17211
17737
  return this.#leafId;
17212
17738
  }
17739
+ /** Cheap retained-node count for the rotation census (no cloning). */
17740
+ get nodeCount() {
17741
+ return this.#nodes.length;
17742
+ }
17213
17743
  tree() {
17214
17744
  return this.#nodes.map((node) => structuredClone(node));
17215
17745
  }
@@ -17324,35 +17854,70 @@ function isCode10(error, code) {
17324
17854
  return typeof error === "object" && error !== null && "code" in error && error.code === code;
17325
17855
  }
17326
17856
 
17327
- // src/utils/memory-restart.ts
17328
- import { join as join27 } from "path";
17329
- var MEMORY_RESTART_EXIT_CODE = 75;
17330
- var MEMORY_RESTART_MARKER_FILE = "memory-restart.json";
17331
- var MEMORY_RESTART_WINDOW_MS = 30 * 60 * 1e3;
17332
- function memoryRestartMarkerPath(workspace) {
17333
- return join27(workspace, ".flavor", "tmp", MEMORY_RESTART_MARKER_FILE);
17857
+ // src/utils/rotation-census.ts
17858
+ import { mkdirSync as mkdirSync2, readdirSync, unlinkSync as unlinkSync2, writeFileSync } from "fs";
17859
+ import { join as join28 } from "path";
17860
+ import { writeHeapSnapshot } from "v8";
17861
+ var CENSUS_PREFIX = "rotation-census-";
17862
+ var SNAPSHOT_PREFIX = "heap-";
17863
+ var KEEP_CENSUS_FILES = 5;
17864
+ var KEEP_SNAPSHOT_FILES = 2;
17865
+ function censusHeapBlock(reading) {
17866
+ const usage = process.memoryUsage();
17867
+ const mb = (bytes) => Math.round(bytes / 1048576);
17868
+ return {
17869
+ heapUsedMb: mb(reading.heapUsed),
17870
+ heapLimitMb: mb(reading.heapLimit),
17871
+ rawRatio: Number(reading.ratio.toFixed(3)),
17872
+ ...reading.verifiedRatio === void 0 ? {} : { verifiedRatio: Number(reading.verifiedRatio.toFixed(3)) },
17873
+ gcVerified: reading.gcVerified,
17874
+ rssMb: mb(usage.rss),
17875
+ externalMb: mb(usage.external),
17876
+ arrayBuffersMb: mb(usage.arrayBuffers),
17877
+ ...reading.memoryLimit === void 0 ? {} : { memoryLimitMb: mb(reading.memoryLimit) },
17878
+ ...reading.rssRatio === void 0 ? {} : { rssRatio: Number(reading.rssRatio.toFixed(3)) }
17879
+ };
17334
17880
  }
17335
- function nextMemoryRestartMarker(previous, sessionId, now) {
17336
- const reusable = previous !== void 0 && previous.sessionId === sessionId && now.getTime() - Date.parse(previous.requestedAt) < MEMORY_RESTART_WINDOW_MS && Number.isInteger(previous.attempts) && previous.attempts > 0;
17337
- return { sessionId, requestedAt: now.toISOString(), attempts: reusable ? previous.attempts + 1 : 1 };
17881
+ function writeRotationCensus(workspace, census) {
17882
+ try {
17883
+ const directory = join28(workspace, ".flavor", "tmp");
17884
+ mkdirSync2(directory, { recursive: true });
17885
+ const path = join28(directory, `${CENSUS_PREFIX}${Date.now()}.json`);
17886
+ writeFileSync(path, `${JSON.stringify(census, null, 2)}
17887
+ `, { encoding: "utf8", mode: 384 });
17888
+ prune(directory, CENSUS_PREFIX, ".json", KEEP_CENSUS_FILES);
17889
+ return path;
17890
+ } catch {
17891
+ return void 0;
17892
+ }
17338
17893
  }
17339
- function parseMemoryRestartMarker(json) {
17894
+ function writeRotationHeapSnapshot(workspace) {
17340
17895
  try {
17341
- const value = JSON.parse(json);
17342
- if (typeof value.sessionId !== "string" || typeof value.requestedAt !== "string" || typeof value.attempts !== "number") {
17343
- return void 0;
17344
- }
17345
- return { sessionId: value.sessionId, requestedAt: value.requestedAt, attempts: value.attempts };
17896
+ const directory = join28(workspace, ".flavor", "tmp");
17897
+ mkdirSync2(directory, { recursive: true });
17898
+ const path = join28(directory, `${SNAPSHOT_PREFIX}${Date.now()}.heapsnapshot`);
17899
+ writeHeapSnapshot(path);
17900
+ prune(directory, SNAPSHOT_PREFIX, ".heapsnapshot", KEEP_SNAPSHOT_FILES);
17901
+ return path;
17346
17902
  } catch {
17347
17903
  return void 0;
17348
17904
  }
17349
17905
  }
17906
+ function prune(directory, prefix, suffix, keep) {
17907
+ const stale = readdirSync(directory).filter((name) => name.startsWith(prefix) && name.endsWith(suffix)).sort().slice(0, -keep);
17908
+ for (const name of stale) {
17909
+ try {
17910
+ unlinkSync2(join28(directory, name));
17911
+ } catch {
17912
+ }
17913
+ }
17914
+ }
17350
17915
 
17351
17916
  // src/sleep/organizer.ts
17352
17917
  import { constants as constants8 } from "fs";
17353
17918
  import { randomUUID as randomUUID16 } from "crypto";
17354
17919
  import { lstat as lstat10, mkdir as mkdir18, open as open9, readdir as readdir8, realpath as realpath11, rename as rename9, rm as rm10 } from "fs/promises";
17355
- import { isAbsolute as isAbsolute11, join as join28, relative as relative15, resolve as resolve23, sep as sep15 } from "path";
17920
+ import { isAbsolute as isAbsolute11, join as join29, relative as relative15, resolve as resolve23, sep as sep15 } from "path";
17356
17921
  import { z as z28 } from "zod";
17357
17922
  var DateKeySchema = z28.string().regex(/^\d{4}-\d{2}-\d{2}$/);
17358
17923
  var ReviewSchema = z28.object({
@@ -17407,7 +17972,7 @@ var ProjectSleepOrganizer = class {
17407
17972
  #generate;
17408
17973
  constructor(options) {
17409
17974
  this.#workspace = resolve23(options.workspace);
17410
- this.#sleepDirectory = join28(this.#workspace, ".flavor", "sleep");
17975
+ this.#sleepDirectory = join29(this.#workspace, ".flavor", "sleep");
17411
17976
  this.#sessions = options.sessions ?? new SessionStore({ workspace: this.#workspace });
17412
17977
  this.#generate = options.generate;
17413
17978
  }
@@ -17416,7 +17981,7 @@ var ProjectSleepOrganizer = class {
17416
17981
  signal.throwIfAborted();
17417
17982
  await this.#prepareDirectory();
17418
17983
  if (await this.#hasReport(date)) return { status: "exists", date };
17419
- const lockPath = join28(this.#sleepDirectory, `.${date}.lock`);
17984
+ const lockPath = join29(this.#sleepDirectory, `.${date}.lock`);
17420
17985
  let lock;
17421
17986
  try {
17422
17987
  lock = await open9(lockPath, constants8.O_CREAT | constants8.O_EXCL | constants8.O_WRONLY, 384);
@@ -17451,7 +18016,7 @@ Previous output was invalid. Repair it and return strict JSON only.`;
17451
18016
  }
17452
18017
  if (review === void 0) throw lastError;
17453
18018
  const filename2 = `${date}-${filenameSummary(review.title)}.md`;
17454
- const path = join28(this.#sleepDirectory, filename2);
18019
+ const path = join29(this.#sleepDirectory, filename2);
17455
18020
  const markdown = renderSleepReport(date, review, documents);
17456
18021
  await this.#atomicWrite(path, markdown);
17457
18022
  return { status: "written", date, path, sessionCount: documents.length };
@@ -17493,7 +18058,7 @@ Previous output was invalid. Repair it and return strict JSON only.`;
17493
18058
  });
17494
18059
  }
17495
18060
  async #prepareDirectory() {
17496
- const flavorDirectory = join28(this.#workspace, ".flavor");
18061
+ const flavorDirectory = join29(this.#workspace, ".flavor");
17497
18062
  await assertNotSymlink(flavorDirectory);
17498
18063
  await mkdir18(flavorDirectory, { recursive: true, mode: 448 });
17499
18064
  await assertNotSymlink(this.#sleepDirectory);
@@ -17507,7 +18072,7 @@ Previous output was invalid. Repair it and return strict JSON only.`;
17507
18072
  return (await readdir8(this.#sleepDirectory, { withFileTypes: true })).some((entry2) => entry2.isFile() && entry2.name.startsWith(prefix) && entry2.name.endsWith(".md"));
17508
18073
  }
17509
18074
  async #atomicWrite(path, content) {
17510
- const temporary = join28(this.#sleepDirectory, `.${randomUUID16()}.tmp`);
18075
+ const temporary = join29(this.#sleepDirectory, `.${randomUUID16()}.tmp`);
17511
18076
  let handle;
17512
18077
  try {
17513
18078
  handle = await open9(temporary, constants8.O_CREAT | constants8.O_EXCL | constants8.O_WRONLY, 384);
@@ -17881,7 +18446,7 @@ function computeSessionStats(documents) {
17881
18446
 
17882
18447
  // src/tools/files.ts
17883
18448
  import { constants as constants9 } from "fs";
17884
- import { mkdir as mkdir19, open as open10, readFile as readFile20, realpath as realpath12, rename as rename10, rm as rm11, stat as stat8 } from "fs/promises";
18449
+ import { mkdir as mkdir19, open as open10, readFile as readFile21, realpath as realpath12, rename as rename10, rm as rm11, stat as stat9 } from "fs/promises";
17885
18450
  import { dirname as dirname17, isAbsolute as isAbsolute12, relative as relative16, resolve as resolve24, sep as sep16 } from "path";
17886
18451
  import { randomUUID as randomUUID17 } from "crypto";
17887
18452
  import { z as z29 } from "zod";
@@ -18034,7 +18599,7 @@ function createReadTool(workspace, options = {}) {
18034
18599
  execute: async (input, signal) => {
18035
18600
  abortIfNeeded(signal);
18036
18601
  const path = await guard.existing(input.path);
18037
- const info = await stat8(path);
18602
+ const info = await stat9(path);
18038
18603
  const maxBytes = input.maxBytes ?? MAX_READ_BYTES;
18039
18604
  if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0 || maxBytes > MAX_READ_BYTES) {
18040
18605
  throw new Error(`maxBytes must be a positive integer no greater than ${MAX_READ_BYTES}`);
@@ -18243,7 +18808,7 @@ async function atomicWrite(path, content, signal, expectedVersion) {
18243
18808
  const temporary = `${path}.${randomUUID17()}.tmp`;
18244
18809
  let mode;
18245
18810
  try {
18246
- mode = (await stat8(path)).mode & 511;
18811
+ mode = (await stat9(path)).mode & 511;
18247
18812
  } catch (error) {
18248
18813
  if (!isMissing2(error)) throw error;
18249
18814
  }
@@ -18266,7 +18831,7 @@ async function atomicWrite(path, content, signal, expectedVersion) {
18266
18831
  }
18267
18832
  }
18268
18833
  async function readText(path) {
18269
- const contents = await readFile20(path);
18834
+ const contents = await readFile21(path);
18270
18835
  if (isBinary(contents)) throw new Error("Cannot edit binary file as text");
18271
18836
  return decodeFileText(contents);
18272
18837
  }
@@ -18292,7 +18857,7 @@ async function assertMutationVersion(path, expected, observations) {
18292
18857
  }
18293
18858
  async function fileVersion(path) {
18294
18859
  try {
18295
- return versionOfStat(await stat8(path));
18860
+ return versionOfStat(await stat9(path));
18296
18861
  } catch (error) {
18297
18862
  if (isMissing2(error)) return ABSENT_VERSION;
18298
18863
  throw error;
@@ -18303,7 +18868,7 @@ function versionOfStat(info) {
18303
18868
  }
18304
18869
  async function readOptionalPresentationText(path) {
18305
18870
  try {
18306
- const contents = await readFile20(path);
18871
+ const contents = await readFile21(path);
18307
18872
  return isBinary(contents) ? { exists: true } : { exists: true, text: contents.toString("utf8") };
18308
18873
  } catch (error) {
18309
18874
  return isMissing2(error) ? { exists: false } : { exists: true };
@@ -18614,7 +19179,7 @@ function matchesLines(source, start, expected) {
18614
19179
 
18615
19180
  // src/tools/search.ts
18616
19181
  import { spawn as spawn5 } from "child_process";
18617
- import { open as open11, opendir, stat as stat9 } from "fs/promises";
19182
+ import { open as open11, opendir, stat as stat10 } from "fs/promises";
18618
19183
  import { isAbsolute as isAbsolute13, relative as relative17, resolve as resolve25, sep as sep17 } from "path";
18619
19184
  import { rgPath as bundledRgPath } from "@vscode/ripgrep";
18620
19185
  import createIgnore from "ignore";
@@ -18962,7 +19527,7 @@ async function nodeFiles(root, start, signal, accept, limit, resources, ignoreBu
18962
19527
  }
18963
19528
  ignores.length = ruleCount;
18964
19529
  }
18965
- if ((await stat9(start)).isFile()) {
19530
+ if ((await stat10(start)).isFile()) {
18966
19531
  const path = normalizedRelative(root, start);
18967
19532
  if (!isGitMetadataPath(root, start) && (includeIgnored || !ignored(path, false, ignores)) && accept(path)) output.push(path);
18968
19533
  } else {
@@ -18997,7 +19562,7 @@ async function collectIgnoreLayers(root, start, signal, resources, ignoreBudget)
18997
19562
  if (!ignored(path, true, layers)) await walk(absolute);
18998
19563
  }
18999
19564
  }
19000
- if (!(await stat9(start)).isFile()) await walk(start);
19565
+ if (!(await stat10(start)).isFile()) await walk(start);
19001
19566
  return layers;
19002
19567
  }
19003
19568
  async function loadIgnoreFiles(directory, base, rules, budget, signal) {
@@ -19116,7 +19681,7 @@ function scope(root, path = ".") {
19116
19681
  async function resolveGrepPath(root, path = ".") {
19117
19682
  const candidate = scope(root, path);
19118
19683
  try {
19119
- const info = await stat9(candidate);
19684
+ const info = await stat10(candidate);
19120
19685
  if (!info.isFile() && !info.isDirectory()) throw new Error("Search path must be a file or directory");
19121
19686
  } catch (error) {
19122
19687
  if (error.code === "ENOENT") {
@@ -19320,19 +19885,6 @@ import { isIP } from "net";
19320
19885
  import { request as httpRequest } from "http";
19321
19886
  import { request as httpsRequest } from "https";
19322
19887
  import { z as z31 } from "zod";
19323
-
19324
- // src/utils/abort.ts
19325
- function waitWithSignal(operation, signal) {
19326
- if (signal === void 0) return operation;
19327
- return new Promise((resolve44, reject) => {
19328
- const abort2 = () => reject(signal.reason);
19329
- if (signal.aborted) abort2();
19330
- else signal.addEventListener("abort", abort2, { once: true });
19331
- operation.then(resolve44, reject).finally(() => signal.removeEventListener("abort", abort2));
19332
- });
19333
- }
19334
-
19335
- // src/tools/web.ts
19336
19888
  var WebFetchInput = z31.object({
19337
19889
  url: z31.string().url().max(8192),
19338
19890
  timeoutMs: z31.coerce.number().int().positive().max(12e4).optional(),
@@ -19657,7 +20209,7 @@ function charset(contentType) {
19657
20209
  // src/tools/lsp.ts
19658
20210
  import { spawn as spawn6 } from "child_process";
19659
20211
  import { existsSync as existsSync5 } from "fs";
19660
- import { access as access2, constants as constants10, readFile as readFile21 } from "fs/promises";
20212
+ import { access as access2, constants as constants10, readFile as readFile22 } from "fs/promises";
19661
20213
  import { basename as basename7, dirname as dirname18, extname as extname3, isAbsolute as isAbsolute14, relative as relative18, resolve as resolve26, sep as sep18 } from "path";
19662
20214
  import { fileURLToPath as fileURLToPath2, pathToFileURL as pathToFileURL3 } from "url";
19663
20215
  import { z as z32 } from "zod";
@@ -19851,7 +20403,7 @@ var LspConnection = class {
19851
20403
  const url = new URL(uri);
19852
20404
  if (url.protocol !== "file:") return;
19853
20405
  const filePath = fileURLToPath2(url);
19854
- const text = await readFile21(filePath, "utf8");
20406
+ const text = await readFile22(filePath, "utf8");
19855
20407
  const current = this.#documents.get(uri);
19856
20408
  if (current === void 0) {
19857
20409
  this.#documents.set(uri, { text, version: 1 });
@@ -20217,19 +20769,19 @@ async function changeSummary(workspace) {
20217
20769
  };
20218
20770
  }
20219
20771
  async function stagedDiff(workspace, maxChars = 96e3) {
20220
- const stat20 = await git2(workspace, ["diff", "--staged", "--stat"]);
20772
+ const stat21 = await git2(workspace, ["diff", "--staged", "--stat"]);
20221
20773
  const diff = await git2(workspace, ["diff", "--staged", "--no-ext-diff", "--no-color"]);
20222
20774
  if (!diff.ok) throw gitFailure(diff, "diff --staged");
20223
20775
  const text = diff.stdout;
20224
20776
  return {
20225
20777
  diff: text.length > maxChars ? `${text.slice(0, maxChars)}
20226
20778
  \u2026[diff truncated]` : text,
20227
- stat: stat20.stdout.trim(),
20779
+ stat: stat21.stdout.trim(),
20228
20780
  truncated: text.length > maxChars
20229
20781
  };
20230
20782
  }
20231
20783
  async function uncommittedDiff(workspace, maxChars = 96e3) {
20232
- const stat20 = await git2(workspace, ["diff", "HEAD", "--stat"]);
20784
+ const stat21 = await git2(workspace, ["diff", "HEAD", "--stat"]);
20233
20785
  const diff = await git2(workspace, ["diff", "HEAD", "--no-ext-diff", "--no-color"]);
20234
20786
  if (!diff.ok && !diff.stderr.includes("unknown revision")) throw gitFailure(diff, "diff HEAD");
20235
20787
  const summary2 = await changeSummary(workspace);
@@ -20237,7 +20789,7 @@ async function uncommittedDiff(workspace, maxChars = 96e3) {
20237
20789
  return {
20238
20790
  diff: text.length > maxChars ? `${text.slice(0, maxChars)}
20239
20791
  \u2026[diff truncated]` : text,
20240
- stat: stat20.stdout.trim(),
20792
+ stat: stat21.stdout.trim(),
20241
20793
  untracked: summary2.untrackedFiles
20242
20794
  };
20243
20795
  }
@@ -20534,17 +21086,17 @@ ${explanation}` : "";
20534
21086
  }
20535
21087
 
20536
21088
  // src/desktop/astgraph-service.ts
20537
- import { stat as stat10 } from "fs/promises";
21089
+ import { stat as stat11 } from "fs/promises";
20538
21090
  import { createRequire as nodeCreateRequire } from "module";
20539
- import { join as join29, resolve as resolve27 } from "path";
21091
+ import { join as join30, resolve as resolve27 } from "path";
20540
21092
  var FIELDS = "id, kind, name, qualified_name, file_path, language, start_line, end_line, signature";
20541
21093
  var DesktopAstGraphService = class {
20542
21094
  #path;
20543
21095
  constructor(workspace) {
20544
- this.#path = join29(resolve27(workspace), ".flavor", "astgraph", "index.db");
21096
+ this.#path = join30(resolve27(workspace), ".flavor", "astgraph", "index.db");
20545
21097
  }
20546
21098
  async status() {
20547
- if ((await stat10(this.#path).catch(() => void 0))?.isFile() !== true) {
21099
+ if ((await stat11(this.#path).catch(() => void 0))?.isFile() !== true) {
20548
21100
  return { available: false, path: this.#path, files: 0, nodes: 0, edges: 0 };
20549
21101
  }
20550
21102
  return this.#read((db) => {
@@ -20824,8 +21376,8 @@ function createTodoWriteTool() {
20824
21376
 
20825
21377
  // src/tools/managed.ts
20826
21378
  import { randomUUID as randomUUID18 } from "crypto";
20827
- import { link, lstat as lstat11, mkdir as mkdir20, readFile as readFile22, readdir as readdir9, unlink as unlink3, writeFile as writeFile12 } from "fs/promises";
20828
- import { dirname as dirname19, join as join30, resolve as resolve29 } from "path";
21379
+ import { link, lstat as lstat11, mkdir as mkdir20, readFile as readFile23, readdir as readdir9, unlink as unlink3, writeFile as writeFile12 } from "fs/promises";
21380
+ import { dirname as dirname19, join as join31, resolve as resolve29 } from "path";
20829
21381
  import { z as z37 } from "zod";
20830
21382
  var MANAGED_TOOL_VERSION = 1;
20831
21383
  var MAX_RECORD_BYTES = 256 * 1024;
@@ -20868,8 +21420,8 @@ var ManagedToolStore = class {
20868
21420
  #diagnostics = [];
20869
21421
  constructor(options) {
20870
21422
  this.#workspace = resolve29(options.workspace);
20871
- this.#projectRoot = join30(this.#workspace, ".flavor", "tools");
20872
- this.#globalRoot = join30(resolve29(options.home), ".flavor-code", "tools");
21423
+ this.#projectRoot = join31(this.#workspace, ".flavor", "tools");
21424
+ this.#globalRoot = join31(resolve29(options.home), ".flavor-code", "tools");
20873
21425
  this.#now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
20874
21426
  }
20875
21427
  get diagnostics() {
@@ -20916,7 +21468,7 @@ var ManagedToolStore = class {
20916
21468
  version: MANAGED_TOOL_VERSION,
20917
21469
  createdAt: this.#now()
20918
21470
  });
20919
- const path = join30(this.directory(input.scope), filename(input.name));
21471
+ const path = join31(this.directory(input.scope), filename(input.name));
20920
21472
  const definition = compileDefinition(record3, input.scope, this.#workspace);
20921
21473
  await writeExclusive(path, `${JSON.stringify(record3, void 0, 2)}
20922
21474
  `);
@@ -20941,7 +21493,7 @@ var ManagedToolStore = class {
20941
21493
  if (!parsed.success) return [];
20942
21494
  const matches = this.#matching(parsed.data.name).filter((entry2) => parsed.data.scope === void 0 || entry2.scope === parsed.data.scope);
20943
21495
  if (matches.length > 0) return matches.map((entry2) => entry2.path);
20944
- return [join30(this.directory(parsed.data.scope ?? "project"), filename(parsed.data.name))];
21496
+ return [join31(this.directory(parsed.data.scope ?? "project"), filename(parsed.data.name))];
20945
21497
  }
20946
21498
  async #loadDirectory(scope2) {
20947
21499
  const root = this.directory(scope2);
@@ -20954,12 +21506,12 @@ var ManagedToolStore = class {
20954
21506
  return;
20955
21507
  }
20956
21508
  for (const name of names) {
20957
- const path = join30(root, name);
21509
+ const path = join31(root, name);
20958
21510
  try {
20959
21511
  const info = await lstat11(path);
20960
21512
  if (!info.isFile() || info.isSymbolicLink()) throw new Error("Managed tool record must be a regular file");
20961
21513
  if (info.size > MAX_RECORD_BYTES) throw new Error(`Managed tool record exceeds ${MAX_RECORD_BYTES} bytes`);
20962
- const record3 = ManagedToolRecordSchema.parse(JSON.parse(await readFile22(path, "utf8")));
21514
+ const record3 = ManagedToolRecordSchema.parse(JSON.parse(await readFile23(path, "utf8")));
20963
21515
  if (record3.scope !== scope2) throw new Error(`Managed tool record scope must match its ${scope2} directory`);
20964
21516
  const key = scopedKey(scope2, record3.name);
20965
21517
  if (this.#entries.has(key)) throw new Error(`Duplicate managed tool name in ${scope2} scope: ${record3.name}`);
@@ -21845,6 +22397,11 @@ var HELP = [
21845
22397
  "/co-work <pal> <goal> | status [id] | cancel <id> [reason]"
21846
22398
  ].join("\n");
21847
22399
  var MAX_STOP_DENIALS = 2;
22400
+ var RESUMABLE_TURN_BOUNDARIES = /* @__PURE__ */ new Set([
22401
+ "iteration_limit",
22402
+ "output_limit",
22403
+ "context_overflow"
22404
+ ]);
21848
22405
  var FlavorSession = class {
21849
22406
  #services;
21850
22407
  #active;
@@ -21868,6 +22425,7 @@ var FlavorSession = class {
21868
22425
  #pendingCoWorkStarts = [];
21869
22426
  #activeSteeringDurableIds = /* @__PURE__ */ new Set();
21870
22427
  #durableResumeStarted = false;
22428
+ #continuationResumeStarted = false;
21871
22429
  #permissionBaseline;
21872
22430
  #activeSubmission;
21873
22431
  constructor(services) {
@@ -21888,6 +22446,10 @@ var FlavorSession = class {
21888
22446
  queueSnapshot() {
21889
22447
  return this.#queue.snapshot();
21890
22448
  }
22449
+ /** True when start() admitted a persisted /loop or /goal continuation. */
22450
+ get rotationContinuationResumed() {
22451
+ return this.#continuationResumeStarted;
22452
+ }
21891
22453
  clearQueue() {
21892
22454
  const snapshot4 = this.#queue.clear();
21893
22455
  for (const kind of ["steer", "followUp"]) {
@@ -22037,6 +22599,7 @@ var FlavorSession = class {
22037
22599
  if (decision.additionalContext !== void 0) await this.#services.addContext?.(decision.additionalContext);
22038
22600
  this.#started = true;
22039
22601
  this.#resumeDurableQueue();
22602
+ this.#resumeRotationContinuation();
22040
22603
  });
22041
22604
  return this.#startPromise;
22042
22605
  }
@@ -22070,6 +22633,7 @@ var FlavorSession = class {
22070
22633
  if (!initial) this.#outputQueuedSubmission(submission);
22071
22634
  initial = false;
22072
22635
  await this.#runSubmission(submission);
22636
+ if (this.#services.rotationPending?.() === true) return;
22073
22637
  pending2.push(...this.#drainMessages("steer"), ...this.#drainMessages("followUp"));
22074
22638
  }
22075
22639
  }
@@ -22093,6 +22657,7 @@ var FlavorSession = class {
22093
22657
  this.#activeSubmission = submission;
22094
22658
  this.#interrupted = false;
22095
22659
  let outcome = "completed";
22660
+ let resumableBoundary;
22096
22661
  let assistantSummary = "";
22097
22662
  let deliverables = [];
22098
22663
  try {
@@ -22119,7 +22684,8 @@ var FlavorSession = class {
22119
22684
  skillNames,
22120
22685
  this.#services.managedToolCommands().map(({ name }) => name)
22121
22686
  );
22122
- if (command !== null) await this.#dispatch(command, controller2.signal);
22687
+ if (submission.continuation !== void 0) await this.#runContinuation(submission.continuation, controller2.signal);
22688
+ else if (command !== null) await this.#dispatch(command, controller2.signal);
22123
22689
  else for await (const event of this.#services.run(prompt, controller2.signal, {
22124
22690
  getSteeringMessages: () => this.#drainMessages("steer", true).map((item) => item.text),
22125
22691
  ...decision.additionalContext === void 0 ? {} : { additionalContext: decision.additionalContext },
@@ -22128,7 +22694,10 @@ var FlavorSession = class {
22128
22694
  this.#services.output(event);
22129
22695
  if (event.type === "text") assistantSummary = `${assistantSummary}${event.text}`.slice(-2e3);
22130
22696
  if (event.type === "deliverables") deliverables = event.files.slice(0, 100);
22131
- if (event.type === "error") outcome = "failed";
22697
+ if (event.type === "error") {
22698
+ outcome = "failed";
22699
+ if (RESUMABLE_TURN_BOUNDARIES.has(event.error.code)) resumableBoundary = event.error.code;
22700
+ }
22132
22701
  }
22133
22702
  if (controller2.signal.aborted) outcome = "cancelled";
22134
22703
  } catch (error) {
@@ -22169,6 +22738,13 @@ var FlavorSession = class {
22169
22738
  }
22170
22739
  }
22171
22740
  try {
22741
+ if (resumableBoundary !== void 0 && !this.#closed && !controller2.signal.aborted) {
22742
+ const attempt = (submission.iterationContinuation ?? 0) + 1;
22743
+ const text = `[automatic continuation after ${resumableBoundary} boundary; segment ${attempt}]
22744
+ Continue the unfinished task from the current conversation and workspace state. Do not repeat completed operations. Inspect existing results, finish the remaining work, and verify it before stopping.`;
22745
+ this.#enqueueMessage("followUp", { text, displayText: text, iterationContinuation: attempt });
22746
+ this.#notice(`Agent ${resumableBoundary} boundary reached; durable continuation ${attempt} queued.`);
22747
+ }
22172
22748
  if (stopDecision?.decision === "deny" && typeof stopDecision.reason === "string" && stopDecision.reason.length > 0 && !this.#closed && outcome !== "cancelled" && outcome !== "denied") {
22173
22749
  if (this.#stopDenials < MAX_STOP_DENIALS) {
22174
22750
  this.#stopDenials += 1;
@@ -22365,7 +22941,7 @@ Address this now (for example run the required verification or fix), then finish
22365
22941
  });
22366
22942
  }
22367
22943
  #resumeDurableQueue() {
22368
- if (this.#durableResumeStarted || this.#closed || this.active || this.#pendingSubmissions > 0) return;
22944
+ if (this.#durableResumeStarted || this.#closed || this.active || this.#pendingSubmissions > 0 || this.#services.rotationPending?.() === true) return;
22369
22945
  if (!this.#queue.hasPending) return;
22370
22946
  this.#durableResumeStarted = true;
22371
22947
  const submissions = [...this.#drainMessages("steer"), ...this.#drainMessages("followUp")];
@@ -22377,6 +22953,33 @@ Address this now (for example run the required verification or fix), then finish
22377
22953
  });
22378
22954
  }
22379
22955
  }
22956
+ /**
22957
+ * After a heap rotation the relaunched process inherits the same session and
22958
+ * a marker continuation naming the /loop or /goal that was in flight. Resume
22959
+ * it once here so a multi-day autonomous run survives the rotation instead of
22960
+ * silently stopping at the boundary.
22961
+ */
22962
+ #resumeRotationContinuation() {
22963
+ if (this.#continuationResumeStarted || this.#closed) return;
22964
+ const continuation = this.#services.continuation?.();
22965
+ if (continuation === void 0) return;
22966
+ const supported = continuation.kind === "loop" ? this.#services.resumeLoop !== void 0 : this.#services.resumeGoal !== void 0;
22967
+ if (!supported) return;
22968
+ this.#continuationResumeStarted = true;
22969
+ const displayText = `Resuming the interrupted ${continuation.kind} ${continuation.id} after a heap rotation.`;
22970
+ this.#notice(displayText);
22971
+ this.#enqueueNormalizedSubmission({ text: displayText, displayText, continuation }).catch((error) => {
22972
+ this.#services.output({ type: "error", error: { code: "unknown", message: message(error) } });
22973
+ });
22974
+ }
22975
+ async #runContinuation(continuation, signal) {
22976
+ const stream = continuation.kind === "loop" ? this.#services.resumeLoop?.(continuation.id, signal) : this.#services.resumeGoal?.(continuation.id, signal);
22977
+ if (stream === void 0) {
22978
+ this.#notice(`The runtime cannot resume the interrupted ${continuation.kind} ${continuation.id}; start it again with /${continuation.kind}.`);
22979
+ return;
22980
+ }
22981
+ for await (const event of stream) this.#services.output(event);
22982
+ }
22380
22983
  #ackDurable(submission) {
22381
22984
  if (submission.durableId === void 0) return;
22382
22985
  this.#services.durableQueue?.ack(submission.durableId);
@@ -22489,6 +23092,7 @@ function recoveredSubmission(value) {
22489
23092
  if (typeof input.text !== "string" || input.text.trim().length === 0) return void 0;
22490
23093
  if (typeof input.displayText !== "string") return void 0;
22491
23094
  if (input.controlOnly !== void 0 && typeof input.controlOnly !== "boolean") return void 0;
23095
+ if (input.iterationContinuation !== void 0 && (!Number.isSafeInteger(input.iterationContinuation) || input.iterationContinuation < 1)) return void 0;
22492
23096
  if (input.coWorkPlanningKey !== void 0 && typeof input.coWorkPlanningKey !== "string") return void 0;
22493
23097
  if (input.remoteOrigin !== void 0) {
22494
23098
  const origin = input.remoteOrigin;
@@ -22534,9 +23138,9 @@ function requiredPals(service) {
22534
23138
 
22535
23139
  // src/doctor.ts
22536
23140
  import { constants as constants11 } from "fs";
22537
- import { access as access3, lstat as lstat12, readFile as readFile23, readdir as readdir10, stat as stat11 } from "fs/promises";
23141
+ import { access as access3, lstat as lstat12, readFile as readFile24, readdir as readdir10, stat as stat12 } from "fs/promises";
22538
23142
  import { homedir } from "os";
22539
- import { isAbsolute as isAbsolute16, join as join31, relative as relative20, resolve as resolve30, sep as sep20 } from "path";
23143
+ import { isAbsolute as isAbsolute16, join as join32, relative as relative20, resolve as resolve30, sep as sep20 } from "path";
22540
23144
  import { rgPath as bundledRgPath2 } from "@vscode/ripgrep";
22541
23145
 
22542
23146
  // src/utils/semver.ts
@@ -22583,7 +23187,7 @@ async function runDoctor(options = {}, dependencies2 = {}) {
22583
23187
  const checks = [];
22584
23188
  checks.push(runtimeCheck(nodeVersion, platform, arch));
22585
23189
  checks.push(await directoryCheck("workspace", workspace, false));
22586
- checks.push(await directoryCheck("state directory", join31(home, ".flavor-code"), true, home));
23190
+ checks.push(await directoryCheck("state directory", join32(home, ".flavor-code"), true, home));
22587
23191
  let loaded;
22588
23192
  try {
22589
23193
  loaded = await (dependencies2.loadConfig ?? loadConfig)({
@@ -22609,8 +23213,8 @@ async function runDoctor(options = {}, dependencies2 = {}) {
22609
23213
  checks.push({ name: "shell", status: "fail", message: errorMessage3(error) });
22610
23214
  }
22611
23215
  checks.push(await pluginCheck([
22612
- join31(home, ".flavor-code", "plugins"),
22613
- join31(workspace, ".flavor", "plugins")
23216
+ join32(home, ".flavor-code", "plugins"),
23217
+ join32(workspace, ".flavor", "plugins")
22614
23218
  ]));
22615
23219
  try {
22616
23220
  const latest = await (dependencies2.fetchLatestVersion ?? fetchLatestVersion)({
@@ -22658,7 +23262,7 @@ function runtimeCheck(version, platform, arch) {
22658
23262
  }
22659
23263
  async function directoryCheck(name, path, mayBeCreated, parent = path) {
22660
23264
  try {
22661
- const info = await stat11(path);
23265
+ const info = await stat12(path);
22662
23266
  if (!info.isDirectory()) return { name, status: "fail", message: `${path} is not a directory` };
22663
23267
  await access3(path, constants11.R_OK | constants11.W_OK);
22664
23268
  return { name, status: "pass", message: `${path} is readable and writable` };
@@ -22727,7 +23331,7 @@ async function pluginCheck(directories) {
22727
23331
  try {
22728
23332
  const rootInfo = await lstat12(root);
22729
23333
  if (rootInfo.isSymbolicLink()) throw new Error("plugin directory must not be a symbolic link");
22730
- const manifest = PluginManifestSchema.parse(JSON.parse(await readFile23(join31(root, "flavor-plugin.json"), "utf8")));
23334
+ const manifest = PluginManifestSchema.parse(JSON.parse(await readFile24(join32(root, "flavor-plugin.json"), "utf8")));
22731
23335
  const main = resolve30(root, manifest.main);
22732
23336
  const delta = relative20(root, main);
22733
23337
  if (delta === ".." || delta.startsWith(`..${sep20}`) || isAbsolute16(delta)) throw new Error("plugin entry escapes its root");
@@ -22794,8 +23398,8 @@ var LANG_LABELS = /* @__PURE__ */ new Map([
22794
23398
  ]);
22795
23399
 
22796
23400
  // src/utils/crash-guard.ts
22797
- import { mkdirSync as mkdirSync2, writeFileSync } from "fs";
22798
- import { join as join32 } from "path";
23401
+ import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync2 } from "fs";
23402
+ import { join as join33 } from "path";
22799
23403
  var installed = false;
22800
23404
  var crashing = false;
22801
23405
  var crashCleanups = /* @__PURE__ */ new Set();
@@ -22826,7 +23430,7 @@ function installCrashGuard(options = {}) {
22826
23430
  installed = true;
22827
23431
  const crashLogDir = () => {
22828
23432
  try {
22829
- return join32((options.workspace ?? process.cwd)(), ".flavor");
23433
+ return join33((options.workspace ?? process.cwd)(), ".flavor");
22830
23434
  } catch {
22831
23435
  return process.cwd();
22832
23436
  }
@@ -22834,9 +23438,9 @@ function installCrashGuard(options = {}) {
22834
23438
  const writeCrashLog = (kind, detail) => {
22835
23439
  try {
22836
23440
  const dir = crashLogDir();
22837
- mkdirSync2(dir, { recursive: true });
22838
- const path = join32(dir, `crash-${(/* @__PURE__ */ new Date()).toISOString().replace(/[^0-9]/g, "")}-${process.pid}.log`);
22839
- writeFileSync(path, [
23441
+ mkdirSync3(dir, { recursive: true });
23442
+ const path = join33(dir, `crash-${(/* @__PURE__ */ new Date()).toISOString().replace(/[^0-9]/g, "")}-${process.pid}.log`);
23443
+ writeFileSync2(path, [
22840
23444
  `kind: ${kind}`,
22841
23445
  `version: ${process.version}`,
22842
23446
  `platform: ${process.platform}`,
@@ -23606,8 +24210,8 @@ ${total}`;
23606
24210
 
23607
24211
  // src/memory/store.ts
23608
24212
  import { createHash as createHash17 } from "crypto";
23609
- import { readFile as readFile24, rm as rm12 } from "fs/promises";
23610
- import { resolve as resolve31, join as join33, relative as relative21, sep as sep21 } from "path";
24213
+ import { readFile as readFile25, rm as rm12 } from "fs/promises";
24214
+ import { resolve as resolve31, join as join34, relative as relative21, sep as sep21 } from "path";
23611
24215
 
23612
24216
  // src/memory/similarity.ts
23613
24217
  function normalizeForSimilarity(value) {
@@ -23710,9 +24314,9 @@ var MemoryStore = class {
23710
24314
  if (!Number.isSafeInteger(options.maxEntries) || options.maxEntries < 1) throw new Error("maxEntries must be an integer of at least 1");
23711
24315
  if (!Number.isSafeInteger(options.maxEntryChars) || options.maxEntryChars < 1) throw new Error("maxEntryChars must be an integer of at least 1");
23712
24316
  this.workspace = resolve31(options.workspace);
23713
- this.#memoryRoot = join33(this.workspace, ".flavor", "memory");
23714
- this.path = join33(this.#memoryRoot, "MEMORY.md");
23715
- this.#behaviorPath = join33(this.#memoryRoot, "behavior.json");
24317
+ this.#memoryRoot = join34(this.workspace, ".flavor", "memory");
24318
+ this.path = join34(this.#memoryRoot, "MEMORY.md");
24319
+ this.#behaviorPath = join34(this.#memoryRoot, "behavior.json");
23716
24320
  this.#maxEntries = options.maxEntries;
23717
24321
  this.#maxEntryChars = options.maxEntryChars;
23718
24322
  }
@@ -23930,7 +24534,7 @@ var MemoryStore = class {
23930
24534
  });
23931
24535
  }
23932
24536
  async #readTaskItem(reference) {
23933
- const document = decodeTask(await readFile24(this.#resolveContentPath(reference.contentPath), "utf8"));
24537
+ const document = decodeTask(await readFile25(this.#resolveContentPath(reference.contentPath), "utf8"));
23934
24538
  return document.items.find((item) => item.id === reference.id)?.content ?? reference.summary;
23935
24539
  }
23936
24540
  #resolveContentPath(contentPath) {
@@ -24570,8 +25174,8 @@ function createExecutionEnvironment(workspace, config) {
24570
25174
  }
24571
25175
 
24572
25176
  // src/ide/client.ts
24573
- import { readFile as readFile25, readdir as readdir11, stat as stat12 } from "fs/promises";
24574
- import { basename as basename8, join as join34, relative as relative23, resolve as resolve33, sep as sep23 } from "path";
25177
+ import { readFile as readFile26, readdir as readdir11, stat as stat13 } from "fs/promises";
25178
+ import { basename as basename8, join as join35, relative as relative23, resolve as resolve33, sep as sep23 } from "path";
24575
25179
  var FlavorIdeClient = class {
24576
25180
  #workspace;
24577
25181
  #home;
@@ -24667,7 +25271,7 @@ var FlavorIdeClient = class {
24667
25271
  };
24668
25272
  if (await this.#healthy(configured)) return configured;
24669
25273
  }
24670
- const directory = join34(this.#home, ".flavor-code", "ide");
25274
+ const directory = join35(this.#home, ".flavor-code", "ide");
24671
25275
  let entries;
24672
25276
  try {
24673
25277
  entries = (await readdir11(directory)).filter((name) => name.endsWith(".lock"));
@@ -24675,9 +25279,9 @@ var FlavorIdeClient = class {
24675
25279
  return void 0;
24676
25280
  }
24677
25281
  const candidates = (await Promise.all(entries.map(async (name) => {
24678
- const path = join34(directory, name);
25282
+ const path = join35(directory, name);
24679
25283
  try {
24680
- const [contents, info] = await Promise.all([readFile25(path, "utf8"), stat12(path)]);
25284
+ const [contents, info] = await Promise.all([readFile26(path, "utf8"), stat13(path)]);
24681
25285
  const lock = parseLockfile(contents);
24682
25286
  return lock === void 0 ? void 0 : { lock, modified: info.mtimeMs };
24683
25287
  } catch {
@@ -24796,6 +25400,7 @@ function relativeDisplay(workspace, filePath) {
24796
25400
 
24797
25401
  // src/jobs/registry.ts
24798
25402
  import { randomUUID as randomUUID19 } from "crypto";
25403
+ var OUTPUT_SLICE_SLACK_CHARS = 64 * 1024;
24799
25404
  var JobRegistry = class {
24800
25405
  #jobs = /* @__PURE__ */ new Map();
24801
25406
  #maxOutputChars;
@@ -24824,7 +25429,8 @@ var JobRegistry = class {
24824
25429
  updatedAt: now,
24825
25430
  outputChars: 0,
24826
25431
  truncated: false,
24827
- output: "",
25432
+ chunks: [],
25433
+ windowChars: 0,
24828
25434
  outputStart: 0,
24829
25435
  settle,
24830
25436
  done
@@ -24835,11 +25441,16 @@ var JobRegistry = class {
24835
25441
  id,
24836
25442
  append: (text) => {
24837
25443
  if (record3.state !== "running" || text === "") return;
24838
- record3.output += text;
25444
+ record3.chunks.push(text);
25445
+ record3.windowChars += text.length;
24839
25446
  record3.outputChars += text.length;
24840
- if (record3.output.length > this.#maxOutputChars) {
24841
- const removed = record3.output.length - this.#maxOutputChars;
24842
- record3.output = record3.output.slice(removed);
25447
+ if (record3.outputChars > this.#maxOutputChars) record3.truncated = true;
25448
+ if (record3.windowChars > this.#maxOutputChars + OUTPUT_SLICE_SLACK_CHARS) {
25449
+ const joined = record3.chunks.join("");
25450
+ const kept = joined.slice(joined.length - this.#maxOutputChars);
25451
+ const removed = joined.length - kept.length;
25452
+ record3.chunks = [kept];
25453
+ record3.windowChars = kept.length;
24843
25454
  record3.outputStart += removed;
24844
25455
  record3.truncated = true;
24845
25456
  }
@@ -24860,8 +25471,20 @@ var JobRegistry = class {
24860
25471
  }
24861
25472
  read(id, owner2, cursor = 0) {
24862
25473
  const job = this.#owned(id, owner2);
24863
- const effective = Math.max(cursor, job.outputStart);
24864
- return { ...snapshot2(job), output: job.output.slice(effective - job.outputStart), cursor: job.outputStart + job.output.length };
25474
+ const window = job.chunks.join("");
25475
+ const dropped = Math.max(0, window.length - this.#maxOutputChars);
25476
+ const effective = Math.max(cursor, job.outputStart + dropped);
25477
+ return { ...snapshot2(job), output: window.slice(effective - job.outputStart), cursor: job.outputStart + window.length };
25478
+ }
25479
+ /** Retained sizes for the rotation census. */
25480
+ census() {
25481
+ let windowChars = 0;
25482
+ let running = 0;
25483
+ for (const job of this.#jobs.values()) {
25484
+ windowChars += job.windowChars;
25485
+ if (job.state === "running") running += 1;
25486
+ }
25487
+ return { jobs: this.#jobs.size, running, windowChars };
24865
25488
  }
24866
25489
  async wait(id, owner2, signal) {
24867
25490
  signal?.throwIfAborted();
@@ -25198,7 +25821,7 @@ function palSocketAddress(options) {
25198
25821
  import { createHash as createHash20 } from "crypto";
25199
25822
  import { Socket as Socket2 } from "net";
25200
25823
  import { tmpdir as tmpdir3 } from "os";
25201
- import { join as join36 } from "path";
25824
+ import { join as join37 } from "path";
25202
25825
  import { spawn as spawn8 } from "child_process";
25203
25826
 
25204
25827
  // src/pals/broker.ts
@@ -25209,18 +25832,18 @@ import { dirname as dirname21 } from "path";
25209
25832
 
25210
25833
  // src/pals/auth.ts
25211
25834
  import { randomBytes as randomBytes3, randomUUID as randomUUID21 } from "crypto";
25212
- import { chmod as chmod2, link as link2, mkdir as mkdir21, open as open12, readFile as readFile26, unlink as unlink4 } from "fs/promises";
25835
+ import { chmod as chmod2, link as link2, mkdir as mkdir21, open as open12, readFile as readFile27, unlink as unlink4 } from "fs/promises";
25213
25836
  import { homedir as homedir2 } from "os";
25214
- import { join as join35 } from "path";
25837
+ import { join as join36 } from "path";
25215
25838
  var PAL_AUTH_TOKEN_BYTES = 32;
25216
25839
  var TOKEN_PATTERN = /^[a-f0-9]{64}$/;
25217
25840
  async function loadOrCreatePalAuthToken(options = {}) {
25218
- const directory = join35(options.home ?? homedir2(), ".flavor-code", "pals");
25219
- const tokenPath = join35(directory, "auth-token");
25841
+ const directory = join36(options.home ?? homedir2(), ".flavor-code", "pals");
25842
+ const tokenPath = join36(directory, "auth-token");
25220
25843
  await mkdir21(directory, { recursive: true, mode: 448 });
25221
25844
  await chmod2(directory, 448);
25222
25845
  const token = (options.random ?? randomBytes3)(PAL_AUTH_TOKEN_BYTES).toString("hex");
25223
- const temporaryPath = join35(directory, `.auth-token-${process.pid}-${randomUUID21()}.tmp`);
25846
+ const temporaryPath = join36(directory, `.auth-token-${process.pid}-${randomUUID21()}.tmp`);
25224
25847
  let handle;
25225
25848
  try {
25226
25849
  handle = await open12(temporaryPath, "wx", 384);
@@ -25243,7 +25866,7 @@ async function loadOrCreatePalAuthToken(options = {}) {
25243
25866
  if (error.code !== "ENOENT") throw error;
25244
25867
  });
25245
25868
  }
25246
- const stored = (await readFile26(tokenPath, "utf8")).trim();
25869
+ const stored = (await readFile27(tokenPath, "utf8")).trim();
25247
25870
  if (!TOKEN_PATTERN.test(stored)) throw new Error("Pals authentication token store is invalid");
25248
25871
  await chmod2(tokenPath, 384);
25249
25872
  return stored;
@@ -25255,7 +25878,7 @@ function validatePalAuthToken(token) {
25255
25878
 
25256
25879
  // src/pals/lifecycle.ts
25257
25880
  import { randomUUID as randomUUID22 } from "crypto";
25258
- import { mkdir as mkdir22, open as open13, readFile as readFile27, rename as rename11, stat as stat13, unlink as unlink5 } from "fs/promises";
25881
+ import { mkdir as mkdir22, open as open13, readFile as readFile28, rename as rename11, stat as stat14, unlink as unlink5 } from "fs/promises";
25259
25882
  import { dirname as dirname20 } from "path";
25260
25883
  function defaultProcessAlive(pid) {
25261
25884
  if (!Number.isSafeInteger(pid) || pid <= 0) return true;
@@ -25287,7 +25910,7 @@ async function acquirePalFileLock(options) {
25287
25910
  handle = await open13(options.path, "wx", 384);
25288
25911
  } catch (error) {
25289
25912
  if (error.code !== "EEXIST") throw error;
25290
- const [contents, info] = await Promise.all([readFile27(options.path, "utf8").catch(() => ""), stat13(options.path).catch(() => void 0)]);
25913
+ const [contents, info] = await Promise.all([readFile28(options.path, "utf8").catch(() => ""), stat14(options.path).catch(() => void 0)]);
25291
25914
  if (await options.endpointLive()) return void 0;
25292
25915
  const existing = parseMetadata(contents);
25293
25916
  if (existing !== void 0 && (options.processAlive ?? defaultProcessAlive)(existing.pid)) return void 0;
@@ -25317,7 +25940,7 @@ async function acquirePalFileLock(options) {
25317
25940
  await handle.close();
25318
25941
  }
25319
25942
  const release = async () => {
25320
- const current = parseMetadata(await readFile27(options.path, "utf8").catch(() => ""));
25943
+ const current = parseMetadata(await readFile28(options.path, "utf8").catch(() => ""));
25321
25944
  if (current?.nonce !== nonce) return;
25322
25945
  await unlink5(options.path).catch((error) => {
25323
25946
  if (error.code !== "ENOENT") throw error;
@@ -25350,7 +25973,7 @@ function canConnect(address) {
25350
25973
  }
25351
25974
  function startupLockPath(address) {
25352
25975
  const digest = createHash20("sha256").update(address, "utf8").digest("hex").slice(0, 24);
25353
- return join36(tmpdir3(), `flavor-code-pals-${digest}.startup.lock`);
25976
+ return join37(tmpdir3(), `flavor-code-pals-${digest}.startup.lock`);
25354
25977
  }
25355
25978
  async function spawnProductionBroker(address) {
25356
25979
  const entry2 = process.argv[1];
@@ -26293,8 +26916,8 @@ async function createProductionRuntime(options) {
26293
26916
  const registeredProviders = registration.registered;
26294
26917
  let effectiveLlm = registration.effectiveLlm;
26295
26918
  const pluginHost = new PluginHost({
26296
- globalPluginDirs: [join37(home, ".flavor-code", "plugins")],
26297
- projectPluginDirs: [join37(workspace, ".flavor", "plugins")],
26919
+ globalPluginDirs: [join38(home, ".flavor-code", "plugins")],
26920
+ projectPluginDirs: [join38(workspace, ".flavor", "plugins")],
26298
26921
  config,
26299
26922
  // Existing project plugins (including astgraph and superharness) use Node.js
26300
26923
  // built-ins. The Worker/vm sandbox deliberately blocks those imports, so it
@@ -26403,7 +27026,7 @@ async function createProductionRuntime(options) {
26403
27026
  const address = palSocketAddress({
26404
27027
  platform: process.platform,
26405
27028
  userScope: home,
26406
- runtimeDir: join37(tmpdir4(), `flavor-code-pals-${createHash21("sha256").update(home).digest("hex").slice(0, 16)}`)
27029
+ runtimeDir: join38(tmpdir4(), `flavor-code-pals-${createHash21("sha256").update(home).digest("hex").slice(0, 16)}`)
26407
27030
  });
26408
27031
  const baseCollaborationClient = collaboration.client ?? collaboration.createClient?.({
26409
27032
  instanceId: collaboration.instanceId,
@@ -26514,8 +27137,8 @@ async function createProductionRuntime(options) {
26514
27137
  diagnostics.push(`MCP servers could not start: ${message(error)}`);
26515
27138
  });
26516
27139
  const skills = new SkillRegistry({
26517
- globalRoots: [join37(home, ".flavor-code", "skills")],
26518
- projectRoots: [join37(workspace, ".flavor", "skills"), ...pluginSkillRoots],
27140
+ globalRoots: [join38(home, ".flavor-code", "skills")],
27141
+ projectRoots: [join38(workspace, ".flavor", "skills"), ...pluginSkillRoots],
26519
27142
  authorizeResource: async () => true,
26520
27143
  disabledNames: config.skills.disabled
26521
27144
  });
@@ -26523,7 +27146,7 @@ async function createProductionRuntime(options) {
26523
27146
  skillsReady.catch(() => void 0);
26524
27147
  tools.push(createSkillTool(skills), createSkillResourceTool(skills));
26525
27148
  if (options.extraTools !== void 0) tools.push(...options.extraTools);
26526
- const flavor = await optionalText2(join37(workspace, "FLAVOR.md"));
27149
+ const flavor = await optionalText2(join38(workspace, "FLAVOR.md"));
26527
27150
  const instructionBaseline = await workspaceInstructions.baseline();
26528
27151
  let memoryContext;
26529
27152
  if (memoryStore !== void 0) {
@@ -27108,7 +27731,7 @@ async function createProductionRuntime(options) {
27108
27731
  evolveService.toolDefinition(),
27109
27732
  ...mcpTools
27110
27733
  ];
27111
- const loopFlavor = await optionalText2(join37(input.workspace, "FLAVOR.md"));
27734
+ const loopFlavor = await optionalText2(join38(input.workspace, "FLAVOR.md"));
27112
27735
  const loopInstructionBaseline = await loopInstructions.baseline();
27113
27736
  const loopEnvironment = createPromptEnvironment({
27114
27737
  now: /* @__PURE__ */ new Date(),
@@ -27216,12 +27839,89 @@ async function createProductionRuntime(options) {
27216
27839
  await loopExecutionEnvironment?.dispose();
27217
27840
  }
27218
27841
  };
27842
+ let activeLongTask;
27843
+ const readMemoryRestartMarker = () => {
27844
+ try {
27845
+ return parseMemoryRestartMarker(readFileSync3(memoryRestartMarkerPath(workspace), "utf8"));
27846
+ } catch {
27847
+ return void 0;
27848
+ }
27849
+ };
27850
+ const currentPressureReading = () => verifiedHeapPressure(MEMORY_PRESSURE_HEAP_RATIO) ?? { ...readHeap(), gcVerified: false };
27851
+ const collectRotationHolders = () => {
27852
+ const holders = {};
27853
+ try {
27854
+ const context = harness.main.context.census();
27855
+ holders.context = { entries: context.messages, chars: context.messageChars };
27856
+ holders.contextVisibility = { entries: context.visibilityEntries, chars: context.visibilityChars };
27857
+ holders.contextTransient = { entries: context.transientEntries, chars: context.transientChars };
27858
+ } catch {
27859
+ }
27860
+ try {
27861
+ holders.journal = { entries: harnessJournal.recordCount };
27862
+ } catch {
27863
+ }
27864
+ try {
27865
+ const jobCensus = jobs.census();
27866
+ holders.jobs = { entries: jobCensus.jobs, chars: jobCensus.windowChars };
27867
+ } catch {
27868
+ }
27869
+ try {
27870
+ holders.sessionTree = { entries: sessionHistory.nodeCount };
27871
+ } catch {
27872
+ }
27873
+ try {
27874
+ const transcript = transcriptCensus(timelineState);
27875
+ holders.transcript = { entries: transcript.blocks, chars: transcript.chars };
27876
+ holders.transcriptTurns = { entries: transcript.turns };
27877
+ } catch {
27878
+ }
27879
+ return holders;
27880
+ };
27881
+ const rotateForHeap = (continuation, reading) => {
27882
+ requestMemoryRestart(workspace, sessionId, emitOutput, {
27883
+ ...continuation === void 0 ? {} : { continuation },
27884
+ ...reading === void 0 ? {} : { reading },
27885
+ holders: collectRotationHolders()
27886
+ }, options.onMemoryRestartRequested);
27887
+ };
27888
+ const maybeRotateAtBoundary = (continuation) => {
27889
+ activeLongTask = continuation;
27890
+ const pressure = verifiedHeapPressure(MEMORY_PRESSURE_HEAP_RATIO);
27891
+ if (pressure !== void 0) {
27892
+ rotateForHeap(continuation, pressure);
27893
+ return;
27894
+ }
27895
+ const reading = heapRotationNeeded();
27896
+ if (reading === void 0) return;
27897
+ if (rotationCooldownActive(readMemoryRestartMarker(), sessionId, /* @__PURE__ */ new Date())) return;
27898
+ rotateForHeap(continuation, reading);
27899
+ };
27900
+ const maybeRotateAtTurnBoundary = () => {
27901
+ const reading = heapRotationNeeded();
27902
+ if (reading === void 0) return;
27903
+ if (rotationCooldownActive(readMemoryRestartMarker(), sessionId, /* @__PURE__ */ new Date())) return;
27904
+ rotateForHeap(void 0, reading);
27905
+ };
27906
+ const consumePendingContinuation = () => {
27907
+ const marker = readMemoryRestartMarker();
27908
+ const continuation = pendingContinuation(marker, sessionId, /* @__PURE__ */ new Date());
27909
+ if (continuation === void 0 || marker === void 0) return void 0;
27910
+ try {
27911
+ const markerPath = memoryRestartMarkerPath(workspace);
27912
+ writeMarkerAtomically(markerPath, consumedMemoryRestartMarker(marker));
27913
+ } catch {
27914
+ }
27915
+ return continuation;
27916
+ };
27219
27917
  const loopStore = new LoopStore({ workspace });
27220
27918
  const loopOrchestrator = new LoopOrchestrator({
27221
27919
  workspace,
27222
27920
  config: config.loop,
27223
27921
  persistence: loopStore,
27224
27922
  hallucinationGuard,
27923
+ onCycleBoundary: (loopId) => maybeRotateAtBoundary({ kind: "loop", id: loopId }),
27924
+ onMemoryCheckpoint: (loopId) => maybeRotateAtBoundary({ kind: "loop", id: loopId }),
27225
27925
  prepareWorkspace: (input) => prepareLoopWorkspace(input),
27226
27926
  inferVerification: inferVerificationPlan,
27227
27927
  runWorker: ({ workspace: executionWorkspace, prompt, signal }) => runLoopWorker({ workspace: executionWorkspace, prompt, signal }),
@@ -27233,21 +27933,9 @@ async function createProductionRuntime(options) {
27233
27933
  await verifierEnvironment?.dispose();
27234
27934
  }
27235
27935
  },
27236
- confirmBudget: async (state, dimensions, signal) => {
27237
- if (options.approvalPolicy === "deny") return "unavailable";
27238
- const reached = dimensions.map((dimension) => dimension === "cycles" ? `${state.budget.cyclesUsed} cycles` : `${state.budget.inputTokens + state.budget.outputTokens} tokens`).join(" and ");
27239
- const next = dimensions.map((dimension) => dimension === "cycles" ? `${state.budget.cycleCheckpoint + state.config.cycleStep} cycles` : `${state.budget.tokenCheckpoint + state.config.tokenStep} tokens`).join(" and ");
27240
- const latestVerification = state.cycles.at(-1)?.verification.summary ?? "No host verification evidence yet.";
27241
- const answers = await questions.ask([{
27242
- header: "Loop budget",
27243
- question: `Loop ${state.loopId} reached ${reached}. Latest verification: ${latestVerification} Continue until the next checkpoint (${next})?`,
27244
- options: [
27245
- { label: "Continue", description: "Extend only the reached budget tranche and keep looping." },
27246
- { label: "Stop", description: "End this loop as budget exhausted." }
27247
- ]
27248
- }], signal);
27249
- return answers[0] === "Continue" ? "approved" : "rejected";
27250
- },
27936
+ // Cycle/token values are telemetry checkpoint cadence, not an unattended
27937
+ // execution cap. The user can still cancel explicitly at any time.
27938
+ confirmBudget: async () => "approved",
27251
27939
  fingerprint: workspaceFingerprint,
27252
27940
  idFactory: () => `loop-${(/* @__PURE__ */ new Date()).toISOString().replace(/[^0-9]/g, "").slice(0, 17)}-${randomUUID25().slice(0, 8)}`
27253
27941
  });
@@ -27258,9 +27946,12 @@ async function createProductionRuntime(options) {
27258
27946
  plannerModelId: mainModel,
27259
27947
  classifierModelId: mainModel,
27260
27948
  skepticCount: 3,
27261
- maxRounds: 5,
27262
- maxStallStreak: 2,
27949
+ // Production goals run until verified, explicitly cancelled, or blocked
27950
+ // by a non-recoverable external condition. Numeric config values remain
27951
+ // accepted for compatibility but no longer terminate autonomous work.
27263
27952
  persistence: goalStore,
27953
+ onRoundBoundary: (goalId) => maybeRotateAtBoundary({ kind: "goal", id: goalId }),
27954
+ onMemoryCheckpoint: (goalId) => maybeRotateAtBoundary({ kind: "goal", id: goalId }),
27264
27955
  verifyHost: async (signal) => runVerificationPlan(
27265
27956
  await inferVerificationPlan(workspace),
27266
27957
  workspace,
@@ -27355,30 +28046,50 @@ async function createProductionRuntime(options) {
27355
28046
  contextEpoch: harness.main.context.snapshot().epoch?.id ?? "legacy"
27356
28047
  });
27357
28048
  const turnId = harnessJournal.startTurn(turnConfig, { prompt, initialUserMessage: runOptions?.initialUserMessage });
27358
- return monitorMemoryRestart(durableTurn(persistAfter(runMain(
27359
- harness,
27360
- skills,
27361
- prompt,
27362
- signal,
27363
- selectedModels.mainError,
27364
- memoryStore === void 0 || !memoryHasRoutableEntries && userMemoryContext === void 0 ? void 0 : {
27365
- store: memoryStore,
27366
- taskId: memoryLifecycle.taskId ?? sessionId,
27367
- topK: config.memory.retrievalTopK,
27368
- maxChars: config.memory.maxPromptChars
27369
- },
27370
- runOptions?.getSteeringMessages,
27371
- runOptions?.initialUserMessage,
27372
- runOptions?.additionalContext,
27373
- ide
27374
- ), persist), harnessJournal, turnId, turnConfig), () => requestMemoryRestart(workspace, sessionId, emitOutput));
28049
+ return monitorTurnMemory(
28050
+ durableTurn(persistAfter(runMain(
28051
+ harness,
28052
+ skills,
28053
+ prompt,
28054
+ signal,
28055
+ selectedModels.mainError,
28056
+ memoryStore === void 0 || !memoryHasRoutableEntries && userMemoryContext === void 0 ? void 0 : {
28057
+ store: memoryStore,
28058
+ taskId: memoryLifecycle.taskId ?? sessionId,
28059
+ topK: config.memory.retrievalTopK,
28060
+ maxChars: config.memory.maxPromptChars
28061
+ },
28062
+ runOptions?.getSteeringMessages,
28063
+ runOptions?.initialUserMessage,
28064
+ runOptions?.additionalContext,
28065
+ ide
28066
+ ), persist), harnessJournal, turnId, turnConfig),
28067
+ () => rotateForHeap(void 0, currentPressureReading()),
28068
+ () => maybeRotateAtTurnBoundary()
28069
+ );
27375
28070
  },
27376
28071
  runSkill: (skill, prompt, signal) => persistAfter(
27377
28072
  runExplicitSkill(harness, skills, skill, prompt, signal, selectedModels.mainError),
27378
28073
  persist
27379
28074
  ),
27380
- runLoop: (goal, signal) => runLoopSession(loopOrchestrator, hooks, goal, signal),
27381
- runGoal: (goal, signal) => persistEach(runGoalSession(goalOrchestrator, goal, signal), persist),
28075
+ runLoop: (goal, signal) => monitorLongTaskRestart(
28076
+ runLoopSession(loopOrchestrator, hooks, goal, signal),
28077
+ () => rotateForHeap(activeLongTask, currentPressureReading())
28078
+ ),
28079
+ runGoal: (goal, signal) => monitorLongTaskRestart(
28080
+ persistEach(runGoalSession(goalOrchestrator, goal, signal), persist),
28081
+ () => rotateForHeap(activeLongTask, currentPressureReading())
28082
+ ),
28083
+ resumeLoop: (loopId, signal) => monitorLongTaskRestart(
28084
+ runResumeLoopSession(loopOrchestrator, hooks, loopId, signal),
28085
+ () => rotateForHeap(activeLongTask ?? { kind: "loop", id: loopId }, currentPressureReading())
28086
+ ),
28087
+ resumeGoal: (goalId, signal) => monitorLongTaskRestart(
28088
+ persistEach(runResumeGoalSession(goalOrchestrator, goalId, signal), persist),
28089
+ () => rotateForHeap(activeLongTask ?? { kind: "goal", id: goalId }, currentPressureReading())
28090
+ ),
28091
+ continuation: () => consumePendingContinuation(),
28092
+ rotationPending: () => memoryRotationActive(),
27382
28093
  mcp: async (command, signal) => {
27383
28094
  signal.throwIfAborted();
27384
28095
  const manager = await mcpReady;
@@ -27472,7 +28183,7 @@ async function createProductionRuntime(options) {
27472
28183
  const root = resolve36(workspace);
27473
28184
  const abs = resolve36(root, path);
27474
28185
  if (abs !== root && !abs.startsWith(root + sep25)) throw new Error(`path outside workspace: ${path}`);
27475
- return readFile28(abs, "utf8");
28186
+ return readFile29(abs, "utf8");
27476
28187
  },
27477
28188
  history: async (path, limit) => fileHistory(workspace, path, limit),
27478
28189
  questions,
@@ -27484,7 +28195,7 @@ async function createProductionRuntime(options) {
27484
28195
  }, query, focus, signal),
27485
28196
  audit: async (toolFilter) => {
27486
28197
  try {
27487
- const raw = await readFile28(auditLogger.path, "utf8");
28198
+ const raw = await readFile29(auditLogger.path, "utf8");
27488
28199
  const lines = raw.trim().split("\n").filter((line) => line.length > 0);
27489
28200
  const entries = lines.map((line) => {
27490
28201
  try {
@@ -27524,7 +28235,7 @@ ${summary2}`;
27524
28235
  doctor: async () => formatDoctorReport(await runDoctor({ workspace, home, environment })),
27525
28236
  usage: async () => {
27526
28237
  try {
27527
- const raw = await readFile28(usageLogPath(), "utf8");
28238
+ const raw = await readFile29(usageLogPath(), "utf8");
27528
28239
  return formatUsageSummary(summarizeUsage(parseUsageEntries(raw)));
27529
28240
  } catch (error) {
27530
28241
  if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
@@ -27677,7 +28388,7 @@ ${renderMemoryDocument(entries)}`;
27677
28388
  return `Provider "${providerName}" is missing authorizationUrl, tokenUrl, or clientId.`;
27678
28389
  }
27679
28390
  try {
27680
- const tokenStore = createFileTokenStore(join37(home, ".flavor-code", "auth.json"));
28391
+ const tokenStore = createFileTokenStore(join38(home, ".flavor-code", "auth.json"));
27681
28392
  const credentialId = oauthCredentialId(oauthConfig.tokenUrl, oauthConfig.clientId);
27682
28393
  await migrateLegacyOAuthToken(tokenStore, providerName, credentialId);
27683
28394
  const oauth = new OAuthCallbackAuthProvider({
@@ -27724,7 +28435,7 @@ ${renderMemoryDocument(entries)}`;
27724
28435
  }
27725
28436
  },
27726
28437
  async logout() {
27727
- const tokenStore = createFileTokenStore(join37(home, ".flavor-code", "auth.json"));
28438
+ const tokenStore = createFileTokenStore(join38(home, ".flavor-code", "auth.json"));
27728
28439
  const tokens2 = await tokenStore.load();
27729
28440
  const hadCredentials = Object.keys(tokens2).length > 0 || effectiveLlm !== void 0;
27730
28441
  if (!hadCredentials) {
@@ -27917,10 +28628,16 @@ function formatMcpReconnect(server) {
27917
28628
  return `Failed to reconnect MCP server "${server.name}": ${server.error ?? "unknown error"}`;
27918
28629
  }
27919
28630
  async function* runLoopSession(orchestrator, hooks, goal, signal) {
28631
+ yield* loopSessionEvents(orchestrator.run({ goal, signal }), hooks);
28632
+ }
28633
+ async function* runResumeLoopSession(orchestrator, hooks, loopId, signal) {
28634
+ yield* loopSessionEvents(orchestrator.resume({ loopId, signal }), hooks);
28635
+ }
28636
+ async function* loopSessionEvents(events, hooks) {
27920
28637
  let totalInputTokens = 0;
27921
28638
  let totalOutputTokens = 0;
27922
28639
  let latestVerification;
27923
- for await (const event of orchestrator.run({ goal, signal })) {
28640
+ for await (const event of events) {
27924
28641
  if (event.type === "worker-event") {
27925
28642
  if (event.event.type === "usage") {
27926
28643
  totalInputTokens += event.event.inputTokens;
@@ -27930,6 +28647,8 @@ async function* runLoopSession(orchestrator, hooks, goal, signal) {
27930
28647
  totalInputTokens,
27931
28648
  totalOutputTokens
27932
28649
  };
28650
+ } else if (event.event.type === "error" && isResumableLongTaskError(event.event.error.code)) {
28651
+ yield { type: "warning", message: `Loop worker segment ended at ${event.event.error.code}; checkpointing and verifying before continuing.` };
27933
28652
  } else if (event.event.type !== "done") yield event.event;
27934
28653
  continue;
27935
28654
  }
@@ -27984,7 +28703,7 @@ function loopProgressEvent(event) {
27984
28703
  loopId: event.loopId,
27985
28704
  phase: "budget",
27986
28705
  state: "info",
27987
- message: `Confirmation required for ${event.dimensions.join(" and ")} budget.`
28706
+ message: `${event.dimensions.join(" and ")} checkpoint reached; continuing automatically.`
27988
28707
  };
27989
28708
  }
27990
28709
  return {
@@ -28001,6 +28720,9 @@ function terminalProgressState(status) {
28001
28720
  if (status === "needs_human") return "info";
28002
28721
  return "failed";
28003
28722
  }
28723
+ function isResumableLongTaskError(code) {
28724
+ return code !== "authentication" && code !== "model_not_found" && code !== "cancelled" && code !== "memory_pressure";
28725
+ }
28004
28726
  async function workspaceFingerprint(workspace) {
28005
28727
  const hash = createHash21("sha256");
28006
28728
  const diff = await execFileNoThrow(
@@ -28024,7 +28746,7 @@ async function workspaceFingerprint(workspace) {
28024
28746
  hash.update(name);
28025
28747
  if (remaining <= 0) continue;
28026
28748
  try {
28027
- const content = await readFile28(path);
28749
+ const content = await readFile29(path);
28028
28750
  const slice = content.subarray(0, remaining);
28029
28751
  hash.update(slice);
28030
28752
  remaining -= slice.length;
@@ -28136,7 +28858,16 @@ async function* durableTurn(source, journal, turnId, config) {
28136
28858
  }
28137
28859
  }
28138
28860
  }
28139
- async function* monitorMemoryRestart(source, onRestartNeeded) {
28861
+ async function* monitorTurnMemory(source, onPressure, onBoundary) {
28862
+ let pressure = false;
28863
+ for await (const event of source) {
28864
+ if (event.type === "error" && event.error.code === "memory_pressure") pressure = true;
28865
+ yield event;
28866
+ }
28867
+ if (pressure) onPressure();
28868
+ else onBoundary();
28869
+ }
28870
+ async function* monitorLongTaskRestart(source, onRestartNeeded) {
28140
28871
  let pressure = false;
28141
28872
  for await (const event of source) {
28142
28873
  if (event.type === "error" && event.error.code === "memory_pressure") pressure = true;
@@ -28144,7 +28875,10 @@ async function* monitorMemoryRestart(source, onRestartNeeded) {
28144
28875
  }
28145
28876
  if (pressure) onRestartNeeded();
28146
28877
  }
28147
- function requestMemoryRestart(workspace, sessionId, emitOutput) {
28878
+ function requestMemoryRestart(workspace, sessionId, emitOutput, rotation, onRestartRequested) {
28879
+ if (memoryRotationActive()) return;
28880
+ markMemoryRotation();
28881
+ const continuation = rotation?.continuation;
28148
28882
  try {
28149
28883
  const markerPath = memoryRestartMarkerPath(workspace);
28150
28884
  let previous;
@@ -28152,14 +28886,43 @@ function requestMemoryRestart(workspace, sessionId, emitOutput) {
28152
28886
  previous = parseMemoryRestartMarker(readFileSync3(markerPath, "utf8"));
28153
28887
  } catch {
28154
28888
  }
28155
- mkdirSync3(dirname22(markerPath), { recursive: true });
28156
- writeFileSync2(markerPath, `${JSON.stringify(nextMemoryRestartMarker(previous, sessionId, /* @__PURE__ */ new Date()))}
28157
- `, "utf8");
28889
+ writeMarkerAtomically(markerPath, nextMemoryRestartMarker(previous, sessionId, /* @__PURE__ */ new Date(), continuation));
28158
28890
  } catch {
28159
28891
  }
28160
- process.exitCode = MEMORY_RESTART_EXIT_CODE;
28161
- emitOutput({ type: "notice", message: "Memory watermark reached; the session is saved and Flavor is restarting with a fresh heap." });
28162
- emitOutput({ type: "exit" });
28892
+ if (rotation?.reading !== void 0) {
28893
+ const census = {
28894
+ at: (/* @__PURE__ */ new Date()).toISOString(),
28895
+ reason: continuation === void 0 ? "memory-pressure" : `heap-rotation:${continuation.kind}`,
28896
+ sessionId,
28897
+ heap: censusHeapBlock(rotation.reading),
28898
+ holders: rotation.holders ?? {}
28899
+ };
28900
+ writeRotationCensus(workspace, census);
28901
+ if (heapSnapshotWarranted(rotation.reading)) writeRotationHeapSnapshot(workspace);
28902
+ }
28903
+ emitOutput({
28904
+ type: "notice",
28905
+ message: continuation === void 0 ? "Memory watermark reached; the session is saved and Flavor is restarting with a fresh heap." : `Memory watermark reached; the session and the running ${continuation.kind} are saved, and Flavor is restarting with a fresh heap to continue it.`
28906
+ });
28907
+ if (onRestartRequested !== void 0) onRestartRequested(sessionId);
28908
+ else {
28909
+ process.exitCode = MEMORY_RESTART_EXIT_CODE;
28910
+ emitOutput({ type: "exit" });
28911
+ }
28912
+ }
28913
+ function writeMarkerAtomically(path, marker) {
28914
+ mkdirSync4(dirname22(path), { recursive: true });
28915
+ const temporary = `${path}.${process.pid}.${randomUUID25()}.tmp`;
28916
+ try {
28917
+ writeFileSync3(temporary, `${JSON.stringify(marker)}
28918
+ `, { encoding: "utf8", mode: 384 });
28919
+ renameSync2(temporary, path);
28920
+ } finally {
28921
+ try {
28922
+ unlinkSync3(temporary);
28923
+ } catch {
28924
+ }
28925
+ }
28163
28926
  }
28164
28927
  async function runGitCommit(deps, hint, signal) {
28165
28928
  if (!await isGitRepository(deps.workspace)) return "Not a git repository \u2014 /commit needs git.";
@@ -28254,7 +29017,7 @@ async function registerConfiguredAdapters(providers, registry, environment, diag
28254
29017
  const configured = { ...providers };
28255
29018
  if (configured.openai === void 0 && environment.OPENAI_API_KEY) configured.openai = { type: "openai", apiKey: environment.OPENAI_API_KEY };
28256
29019
  if (configured.anthropic === void 0 && environment.ANTHROPIC_API_KEY) configured.anthropic = { type: "anthropic", apiKey: environment.ANTHROPIC_API_KEY };
28257
- const oauthTokenStore = createFileTokenStore(join37(home, ".flavor-code", "auth.json"));
29020
+ const oauthTokenStore = createFileTokenStore(join38(home, ".flavor-code", "auth.json"));
28258
29021
  const registered = [];
28259
29022
  let effectiveLlm;
28260
29023
  for (const [name, provider] of Object.entries(configured)) {
@@ -28523,7 +29286,7 @@ async function generateSleepReview(registry, modelId, prompt, signal) {
28523
29286
  }
28524
29287
  async function optionalText2(path) {
28525
29288
  try {
28526
- return await readFile28(path, "utf8");
29289
+ return await readFile29(path, "utf8");
28527
29290
  } catch (error) {
28528
29291
  if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") return void 0;
28529
29292
  throw error;
@@ -28608,7 +29371,24 @@ function attachCleanupError(primary, cleanup) {
28608
29371
  }
28609
29372
  }
28610
29373
  async function* runGoalSession(orchestrator, goal, signal) {
28611
- for await (const event of orchestrator.run({ goal, signal })) {
29374
+ yield* goalSessionEvents(orchestrator.run({ goal, signal }));
29375
+ }
29376
+ async function* runResumeGoalSession(orchestrator, goalId, signal) {
29377
+ yield* goalSessionEvents(orchestrator.resume({ goalId, signal }));
29378
+ }
29379
+ async function* goalSessionEvents(events) {
29380
+ for await (const event of events) {
29381
+ if (event.type === "goal-resumed") {
29382
+ yield { type: "notice", message: `Resuming goal ${event.goalId} at round ${event.round} after a heap rotation.` };
29383
+ continue;
29384
+ }
29385
+ if (event.type === "goal-stage-retry") {
29386
+ yield {
29387
+ type: "warning",
29388
+ message: `Goal ${event.stage} attempt ${event.attempt} hit a recoverable boundary (${event.reason}); retrying in ${event.delayMs}ms.`
29389
+ };
29390
+ continue;
29391
+ }
28612
29392
  if (event.type === "goal-plan-created") {
28613
29393
  yield { type: "notice", message: `Goal plan created (${event.plan.kind}) with ${event.plan.criteria.length} acceptance criteria.` };
28614
29394
  yield { type: "notice", message: `Plan file: ${event.planPath}` };
@@ -28635,6 +29415,8 @@ async function* runGoalSession(orchestrator, goal, signal) {
28635
29415
  totalInputTokens: event.event.usage.inputTokens,
28636
29416
  totalOutputTokens: event.event.usage.outputTokens
28637
29417
  };
29418
+ } else if (event.event.type === "error" && isResumableLongTaskError(event.event.error.code)) {
29419
+ yield { type: "warning", message: `Goal worker segment ended at ${event.event.error.code}; checkpointing and verifying before continuing.` };
28638
29420
  } else {
28639
29421
  yield event.event;
28640
29422
  }
@@ -28685,13 +29467,13 @@ function createMemoryTaskId() {
28685
29467
 
28686
29468
  // src/desktop/runtime-controller.ts
28687
29469
  import { createHash as createHash27 } from "crypto";
28688
- import { mkdir as mkdir27, readFile as readFile36, readdir as readdir13, rm as rm15, stat as stat16, writeFile as writeFile17 } from "fs/promises";
29470
+ import { mkdir as mkdir27, readFile as readFile37, readdir as readdir13, rm as rm15, stat as stat17, writeFile as writeFile17 } from "fs/promises";
28689
29471
  import { homedir as homedir5 } from "os";
28690
- import { basename as basename10, join as join47, resolve as resolve39 } from "path";
29472
+ import { basename as basename10, join as join48, resolve as resolve39 } from "path";
28691
29473
 
28692
29474
  // src/d2c/workflow.ts
28693
29475
  import { createHash as createHash22 } from "crypto";
28694
- import { join as join38 } from "path";
29476
+ import { join as join39 } from "path";
28695
29477
  import { z as z42 } from "zod";
28696
29478
 
28697
29479
  // src/d2c/workflow-shared.ts
@@ -28837,7 +29619,7 @@ function reviewsFor(report, previous) {
28837
29619
  function implementationDirectory(report) {
28838
29620
  const source = report.implementation.source;
28839
29621
  if (!/^https?:\/\//i.test(source)) return source;
28840
- return join38("src", "d2c-output", report.task);
29622
+ return join39("src", "d2c-output", report.task);
28841
29623
  }
28842
29624
  function createWorkflow(report, framework) {
28843
29625
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -28961,7 +29743,7 @@ function applyQualityIssueDecision(workflow, issueId2, decision) {
28961
29743
  };
28962
29744
  }
28963
29745
  function workflowPath(workspace, task) {
28964
- return join38(taskDir(workspace, task), "workflow.json");
29746
+ return join39(taskDir(workspace, task), "workflow.json");
28965
29747
  }
28966
29748
  async function readWorkflow(workspace, task) {
28967
29749
  return (await readRecoverableFile(workflowPath(workspace, task), decodeWorkflow))?.value;
@@ -29214,8 +29996,8 @@ function confirmApiMapping(mapping, operationKey, operations) {
29214
29996
  }
29215
29997
 
29216
29998
  // src/d2c/modules.ts
29217
- import { readFile as readFile29 } from "fs/promises";
29218
- import { isAbsolute as isAbsolute18, join as join39, normalize as normalize2, posix, sep as sep26, win32 as win322 } from "path";
29999
+ import { readFile as readFile30 } from "fs/promises";
30000
+ import { isAbsolute as isAbsolute18, join as join40, normalize as normalize2, posix, sep as sep26, win32 as win322 } from "path";
29219
30001
  import { z as z43 } from "zod";
29220
30002
  var ModuleSchema = z43.object({
29221
30003
  id: z43.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/),
@@ -29234,13 +30016,13 @@ function safeSource(path) {
29234
30016
  }
29235
30017
  function d2cOutputDirectory(workspace, task) {
29236
30018
  if (!D2C_TASK_PATTERN.test(task)) throw new Error(`Invalid D2C task: ${task}`);
29237
- return join39(workspace, "src", "d2c-output", task);
30019
+ return join40(workspace, "src", "d2c-output", task);
29238
30020
  }
29239
30021
  async function readD2cModules(workspace, task) {
29240
30022
  const project = d2cOutputDirectory(workspace, task);
29241
30023
  let raw;
29242
30024
  try {
29243
- raw = await readFile29(join39(project, "d2c.modules.json"), "utf8");
30025
+ raw = await readFile30(join40(project, "d2c.modules.json"), "utf8");
29244
30026
  } catch {
29245
30027
  return [{ id: "page", label: task, sourceFiles: ["src/App.vue", "src/App.jsx"], keywords: [task] }];
29246
30028
  }
@@ -29260,8 +30042,8 @@ async function readD2cModules(workspace, task) {
29260
30042
  }
29261
30043
 
29262
30044
  // src/d2c/integration.ts
29263
- import { mkdir as mkdir24, readFile as readFile30, writeFile as writeFile13 } from "fs/promises";
29264
- import { join as join40 } from "path";
30045
+ import { mkdir as mkdir24, readFile as readFile31, writeFile as writeFile13 } from "fs/promises";
30046
+ import { join as join41 } from "path";
29265
30047
  function record2(value) {
29266
30048
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
29267
30049
  }
@@ -29397,19 +30179,19 @@ async def reset_acceptance(request: Request):
29397
30179
  }
29398
30180
  async function generateIntegrationArtifacts(projectDir, document, mappings, options = {}) {
29399
30181
  if (mappings.some((mapping) => mapping.status === "needs-confirmation")) throw new Error("Confirm every uncertain OpenAPI mapping before generating integration code");
29400
- const packagePath = join40(projectDir, "package.json");
30182
+ const packagePath = join41(projectDir, "package.json");
29401
30183
  let pkg;
29402
30184
  try {
29403
- pkg = JSON.parse(await readFile30(packagePath, "utf8"));
30185
+ pkg = JSON.parse(await readFile31(packagePath, "utf8"));
29404
30186
  } catch {
29405
30187
  throw new Error(`D2C integration requires a valid package.json at ${packagePath}`);
29406
30188
  }
29407
30189
  const scripts = options.pythonServer ? { ...record2(pkg.scripts) ?? {} } : { ...record2(pkg.scripts) ?? {}, mock: "node mock/server.mjs" };
29408
30190
  const dependencies2 = options.pythonServer ? { ...record2(pkg.dependencies) ?? {}, axios: "^1.7.9" } : { ...record2(pkg.dependencies) ?? {}, axios: "^1.7.9", express: "^5.2.1" };
29409
30191
  const selected = [...new Set(mappings.map((mapping) => mapping.operationKey))].map((key) => document.operations.find((operation) => operation.id === key)).filter((operation) => operation !== void 0);
29410
- const apiDir = join40(projectDir, "src", "api");
29411
- const mockDir = join40(projectDir, "mock");
29412
- const serverDir = join40(projectDir, "server");
30192
+ const apiDir = join41(projectDir, "src", "api");
30193
+ const mockDir = join41(projectDir, "mock");
30194
+ const serverDir = join41(projectDir, "server");
29413
30195
  await Promise.all([
29414
30196
  mkdir24(apiDir, { recursive: true }),
29415
30197
  ...options.pythonServer ? [] : [mkdir24(mockDir, { recursive: true })],
@@ -29419,23 +30201,23 @@ async function generateIntegrationArtifacts(projectDir, document, mappings, opti
29419
30201
  if (options.pythonServer) files.push("server/main.py", "server/requirements.txt", "server/README.md", "server/flavor-runtime.json");
29420
30202
  else files.push("mock/server.mjs");
29421
30203
  await Promise.all([
29422
- writeFile13(join40(apiDir, "http.js"), `import axios from "axios";
30204
+ writeFile13(join41(apiDir, "http.js"), `import axios from "axios";
29423
30205
 
29424
30206
  export const http = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || ${JSON.stringify(document.baseUrl ?? "")}, timeout: 15000 });
29425
30207
 
29426
30208
  http.interceptors.response.use((response) => response, (error) => Promise.reject({ name: "ApiError", status: error.response?.status, message: error.response?.data?.message || error.message, cause: error }));
29427
30209
  `),
29428
- writeFile13(join40(apiDir, "d2c-api.js"), apiModule(selected)),
29429
- writeFile13(join40(apiDir, "d2c-bindings.json"), `${JSON.stringify({ schema: 1, generatedAt: (/* @__PURE__ */ new Date()).toISOString(), mappings }, null, 2)}
30210
+ writeFile13(join41(apiDir, "d2c-api.js"), apiModule(selected)),
30211
+ writeFile13(join41(apiDir, "d2c-bindings.json"), `${JSON.stringify({ schema: 1, generatedAt: (/* @__PURE__ */ new Date()).toISOString(), mappings }, null, 2)}
29430
30212
  `),
29431
- ...options.pythonServer ? [] : [writeFile13(join40(mockDir, "server.mjs"), mockModule({ ...document, operations: selected }))],
30213
+ ...options.pythonServer ? [] : [writeFile13(join41(mockDir, "server.mjs"), mockModule({ ...document, operations: selected }))],
29432
30214
  writeFile13(packagePath, `${JSON.stringify({ ...pkg, scripts, dependencies: dependencies2 }, null, 2)}
29433
30215
  `),
29434
30216
  ...options.pythonServer ? [
29435
- writeFile13(join40(serverDir, "main.py"), pythonServerModule({ ...document, operations: selected })),
29436
- writeFile13(join40(serverDir, "requirements.txt"), "fastapi>=0.116,<1\nuvicorn[standard]>=0.35,<1\nsqlalchemy>=2.0,<3\n"),
29437
- writeFile13(join40(serverDir, "README.md"), "# Python API service\n\nThis is the real integration service, not a mock. Development defaults to SQLite through `DATABASE_URL=sqlite:///./data/app.db`. Set the same environment variable to a SQLAlchemy MySQL or PostgreSQL URL for deployment; keep schema changes in migrations instead of database-specific SQL. Flavor Code starts the service automatically for acceptance.\n"),
29438
- writeFile13(join40(serverDir, "flavor-runtime.json"), `${JSON.stringify({
30217
+ writeFile13(join41(serverDir, "main.py"), pythonServerModule({ ...document, operations: selected })),
30218
+ writeFile13(join41(serverDir, "requirements.txt"), "fastapi>=0.116,<1\nuvicorn[standard]>=0.35,<1\nsqlalchemy>=2.0,<3\n"),
30219
+ writeFile13(join41(serverDir, "README.md"), "# Python API service\n\nThis is the real integration service, not a mock. Development defaults to SQLite through `DATABASE_URL=sqlite:///./data/app.db`. Set the same environment variable to a SQLAlchemy MySQL or PostgreSQL URL for deployment; keep schema changes in migrations instead of database-specific SQL. Flavor Code starts the service automatically for acceptance.\n"),
30220
+ writeFile13(join41(serverDir, "flavor-runtime.json"), `${JSON.stringify({
29439
30221
  schema: 1,
29440
30222
  kind: "python-fastapi",
29441
30223
  cwd: "server",
@@ -29454,9 +30236,9 @@ http.interceptors.response.use((response) => response, (error) => Promise.reject
29454
30236
 
29455
30237
  // src/d2c/mock-runner.ts
29456
30238
  import { spawn as spawn9 } from "child_process";
29457
- import { access as access4, readFile as readFile31 } from "fs/promises";
30239
+ import { access as access4, readFile as readFile32 } from "fs/promises";
29458
30240
  import { createServer as createServer5 } from "net";
29459
- import { join as join41 } from "path";
30241
+ import { join as join42 } from "path";
29460
30242
  async function exists3(path) {
29461
30243
  try {
29462
30244
  await access4(path);
@@ -29564,7 +30346,7 @@ ${output.trim()}` : ""}`);
29564
30346
  }
29565
30347
  }
29566
30348
  async function runPythonBackend(projectDir, options) {
29567
- const serverDir = join41(projectDir, "server");
30349
+ const serverDir = join42(projectDir, "server");
29568
30350
  if (options.installDependencies !== false) await installPython(serverDir, options.signal);
29569
30351
  const selectedPort = await port();
29570
30352
  const url = `http://127.0.0.1:${selectedPort}`;
@@ -29610,20 +30392,20 @@ ${managed.output().slice(-4e3)}`);
29610
30392
  };
29611
30393
  }
29612
30394
  async function runD2cMockServer(projectDir, options = {}) {
29613
- const runtimePath = join41(projectDir, "server", "flavor-runtime.json");
30395
+ const runtimePath = join42(projectDir, "server", "flavor-runtime.json");
29614
30396
  if (await exists3(runtimePath)) {
29615
30397
  let runtime;
29616
30398
  try {
29617
- runtime = JSON.parse(await readFile31(runtimePath, "utf8"));
30399
+ runtime = JSON.parse(await readFile32(runtimePath, "utf8"));
29618
30400
  } catch {
29619
30401
  throw new Error(`E2E backend runtime manifest is invalid at ${runtimePath}`);
29620
30402
  }
29621
30403
  if (runtime.schema !== 1 || runtime.kind !== "python-fastapi") throw new Error("Unsupported E2E backend runtime manifest");
29622
30404
  return runPythonBackend(projectDir, options);
29623
30405
  }
29624
- const serverPath = join41(projectDir, "mock", "server.mjs");
30406
+ const serverPath = join42(projectDir, "mock", "server.mjs");
29625
30407
  if (!await exists3(serverPath)) throw new Error(`D2C mock server is not generated at ${serverPath}`);
29626
- if (options.installDependencies === false && !await exists3(join41(projectDir, "node_modules", "express", "package.json"))) {
30408
+ if (options.installDependencies === false && !await exists3(join42(projectDir, "node_modules", "express", "package.json"))) {
29627
30409
  throw new Error("D2C mock server dependencies are not installed");
29628
30410
  }
29629
30411
  if (options.installDependencies !== false) await install(projectDir, options.signal);
@@ -29665,13 +30447,13 @@ ${managed.output().slice(-4e3)}`);
29665
30447
 
29666
30448
  // src/d2c/product.ts
29667
30449
  import { createHash as createHash24, randomUUID as randomUUID27 } from "crypto";
29668
- import { copyFile as copyFile2, mkdir as mkdir25, readFile as readFile33, rename as rename13, stat as stat14, unlink as unlink7, writeFile as writeFile15 } from "fs/promises";
29669
- import { join as join42 } from "path";
30450
+ import { copyFile as copyFile2, mkdir as mkdir25, readFile as readFile34, rename as rename13, stat as stat15, unlink as unlink7, writeFile as writeFile15 } from "fs/promises";
30451
+ import { join as join43 } from "path";
29670
30452
  import { z as z44 } from "zod";
29671
30453
 
29672
30454
  // src/e2e/prd-governance.ts
29673
30455
  import { createHash as createHash23, randomUUID as randomUUID26 } from "crypto";
29674
- import { readFile as readFile32, rename as rename12, rm as rm13, writeFile as writeFile14 } from "fs/promises";
30456
+ import { readFile as readFile33, rename as rename12, rm as rm13, writeFile as writeFile14 } from "fs/promises";
29675
30457
  function hashPrd(markdown) {
29676
30458
  return createHash23("sha256").update(markdown, "utf8").digest("hex");
29677
30459
  }
@@ -29748,14 +30530,14 @@ function approvePrd(markdown, now = /* @__PURE__ */ new Date()) {
29748
30530
  return { hash: hashPrd(markdown), approvedAt: now.toISOString(), criteria };
29749
30531
  }
29750
30532
  async function assertApprovedPrd(path, approved) {
29751
- const current = await readFile32(path, "utf8");
30533
+ const current = await readFile33(path, "utf8");
29752
30534
  if (hashPrd(current) !== approved.hash) {
29753
30535
  throw new Error("PRD_LOCK_VIOLATION: confirmed PRD content changed after approval");
29754
30536
  }
29755
30537
  return approved;
29756
30538
  }
29757
30539
  async function updatePrdSectionFile(path, input) {
29758
- const current = await readFile32(path, "utf8");
30540
+ const current = await readFile33(path, "utf8");
29759
30541
  if (hashPrd(current) !== input.expectedHash) {
29760
30542
  throw new Error("PRD_EDIT_CONFLICT: the PRD changed; refresh before saving this section");
29761
30543
  }
@@ -29821,13 +30603,13 @@ var PlanSchema2 = z44.object({
29821
30603
  }).strict();
29822
30604
  function d2cProductDirectory(workspace, task) {
29823
30605
  if (!D2C_TASK_PATTERN.test(task)) throw new Error("Invalid D2C task name");
29824
- return join42(taskDir(workspace, task), "product");
30606
+ return join43(taskDir(workspace, task), "product");
29825
30607
  }
29826
30608
  function d2cProductPrototypeDirectory(workspace, task) {
29827
- return join42(d2cProductDirectory(workspace, task), "prototype");
30609
+ return join43(d2cProductDirectory(workspace, task), "prototype");
29828
30610
  }
29829
30611
  function planPath(workspace, task) {
29830
- return join42(taskDir(workspace, task), "product-plan.json");
30612
+ return join43(taskDir(workspace, task), "product-plan.json");
29831
30613
  }
29832
30614
  function resolveD2cProductTechnology(requirement, fallbackFramework = "vue") {
29833
30615
  const frontendCandidates = [
@@ -29915,18 +30697,18 @@ function buildD2cProductOpenApi(task, modules) {
29915
30697
  `;
29916
30698
  }
29917
30699
  async function fileTimestamp(path) {
29918
- const info = await stat14(path).catch(() => void 0);
30700
+ const info = await stat15(path).catch(() => void 0);
29919
30701
  return info?.isFile() ? info.mtime.toISOString() : void 0;
29920
30702
  }
29921
30703
  async function fileHash(path) {
29922
- const buffer = await readFile33(path).catch((error) => {
30704
+ const buffer = await readFile34(path).catch((error) => {
29923
30705
  if (error.code === "ENOENT") return void 0;
29924
30706
  throw error;
29925
30707
  });
29926
30708
  return buffer === void 0 ? void 0 : createHash24("sha256").update(buffer).digest("hex");
29927
30709
  }
29928
30710
  async function validateAndNormalizePrototypeManifest(path) {
29929
- const raw = await readFile33(path, "utf8").catch((error) => {
30711
+ const raw = await readFile34(path, "utf8").catch((error) => {
29930
30712
  if (error.code === "ENOENT") return void 0;
29931
30713
  throw error;
29932
30714
  });
@@ -30002,7 +30784,7 @@ async function createD2cProductPlan(workspace, input, now = () => /* @__PURE__ *
30002
30784
  });
30003
30785
  }
30004
30786
  async function readD2cProductPlan(workspace, task) {
30005
- const raw = await readFile33(planPath(workspace, task), "utf8").catch((error) => {
30787
+ const raw = await readFile34(planPath(workspace, task), "utf8").catch((error) => {
30006
30788
  if (error.code === "ENOENT") return void 0;
30007
30789
  throw error;
30008
30790
  });
@@ -30010,16 +30792,16 @@ async function readD2cProductPlan(workspace, task) {
30010
30792
  }
30011
30793
  async function discoverD2cProductArtifacts(workspace, plan) {
30012
30794
  const product = d2cProductDirectory(workspace, plan.task);
30013
- const prdUpdatedAt = await fileTimestamp(join42(product, "prd.md"));
30014
- const prdHash = await fileHash(join42(product, "prd.md"));
30015
- const prototypeUpdatedAt = await fileTimestamp(join42(product, "prototype", "index.html"));
30016
- const prototypeHash = await fileHash(join42(product, "prototype", "index.html"));
30017
- const expectedManifest = join42(product, "prototype", "interaction-manifest.json");
30795
+ const prdUpdatedAt = await fileTimestamp(join43(product, "prd.md"));
30796
+ const prdHash = await fileHash(join43(product, "prd.md"));
30797
+ const prototypeUpdatedAt = await fileTimestamp(join43(product, "prototype", "index.html"));
30798
+ const prototypeHash = await fileHash(join43(product, "prototype", "index.html"));
30799
+ const expectedManifest = join43(product, "prototype", "interaction-manifest.json");
30018
30800
  let manifestUpdatedAt = await fileTimestamp(expectedManifest);
30019
30801
  if (manifestUpdatedAt === void 0) {
30020
- const misplacedManifest = join42(product, "interaction-manifest.json");
30802
+ const misplacedManifest = join43(product, "interaction-manifest.json");
30021
30803
  if (await fileTimestamp(misplacedManifest) !== void 0) {
30022
- await mkdir25(join42(product, "prototype"), { recursive: true });
30804
+ await mkdir25(join43(product, "prototype"), { recursive: true });
30023
30805
  await copyFile2(misplacedManifest, expectedManifest);
30024
30806
  manifestUpdatedAt = await fileTimestamp(expectedManifest);
30025
30807
  }
@@ -30052,8 +30834,8 @@ async function readD2cProductPlanView(workspace, task) {
30052
30834
  if (stored === void 0) return void 0;
30053
30835
  const plan = await discoverD2cProductArtifacts(workspace, stored);
30054
30836
  if (plan.revision !== stored.revision) await writeD2cProductPlan(workspace, plan);
30055
- const prdMarkdown = plan.prd === void 0 ? void 0 : await readFile33(join42(taskDir(workspace, task), plan.prd.path), "utf8");
30056
- const manifestPath = join42(d2cProductDirectory(workspace, task), "prototype", "interaction-manifest.json");
30837
+ const prdMarkdown = plan.prd === void 0 ? void 0 : await readFile34(join43(taskDir(workspace, task), plan.prd.path), "utf8");
30838
+ const manifestPath = join43(d2cProductDirectory(workspace, task), "prototype", "interaction-manifest.json");
30057
30839
  const validationError = plan.phase === "design-generating" && await fileTimestamp(manifestPath) !== void 0 ? await validateAndNormalizePrototypeManifest(manifestPath) : void 0;
30058
30840
  return {
30059
30841
  plan,
@@ -30143,9 +30925,9 @@ ${prdMarkdown.slice(0, 2e4)}`
30143
30925
 
30144
30926
  // src/e2e/acceptance-baseline.ts
30145
30927
  import { createHash as createHash25 } from "crypto";
30146
- import { readFile as readFile34 } from "fs/promises";
30928
+ import { readFile as readFile35 } from "fs/promises";
30147
30929
  async function fileArtifact(path) {
30148
- const content = await readFile34(path);
30930
+ const content = await readFile35(path);
30149
30931
  return { path, hash: createHash25("sha256").update(content).digest("hex"), bytes: content.byteLength };
30150
30932
  }
30151
30933
  async function verifyAcceptanceArtifact(path, expected, label) {
@@ -30212,7 +30994,7 @@ function createAcceptanceEvidence(coverage, run, now = /* @__PURE__ */ new Date(
30212
30994
 
30213
30995
  // src/e2e/delivery-run.ts
30214
30996
  import { createHash as createHash26 } from "crypto";
30215
- import { join as join43 } from "path";
30997
+ import { join as join44 } from "path";
30216
30998
  import { z as z45 } from "zod";
30217
30999
  var DELIVERY_NODE_IDS = ["requirement", "prd", "design", "d2c", "api", "acceptance", "delivery"];
30218
31000
  var ArtifactSchema = z45.object({
@@ -30329,7 +31111,7 @@ function completeDeliveryNode(run, node, outputs, now = /* @__PURE__ */ new Date
30329
31111
  return { ...run, nodes, updatedAt: timestamp2 };
30330
31112
  }
30331
31113
  function deliveryRunPath(workspace, task) {
30332
- return join43(taskDir(workspace, task), "delivery-run.json");
31114
+ return join44(taskDir(workspace, task), "delivery-run.json");
30333
31115
  }
30334
31116
  function decodeDeliveryRun(raw) {
30335
31117
  return DeliveryRunSchema.parse(JSON.parse(raw));
@@ -30370,9 +31152,9 @@ async function updateDeliveryRun(workspace, task, expectedRevision, mutation) {
30370
31152
  }
30371
31153
 
30372
31154
  // src/skills/manager.ts
30373
- import { lstat as lstat14, mkdir as mkdir26, readFile as readFile35, readdir as readdir12, realpath as realpath13, rm as rm14, writeFile as writeFile16 } from "fs/promises";
31155
+ import { lstat as lstat14, mkdir as mkdir26, readFile as readFile36, readdir as readdir12, realpath as realpath13, rm as rm14, writeFile as writeFile16 } from "fs/promises";
30374
31156
  import { homedir as homedir4 } from "os";
30375
- import { isAbsolute as isAbsolute19, join as join44, relative as relative26, resolve as resolve37, sep as sep27 } from "path";
31157
+ import { isAbsolute as isAbsolute19, join as join45, relative as relative26, resolve as resolve37, sep as sep27 } from "path";
30376
31158
  import { stringify } from "yaml";
30377
31159
  var SkillManager = class {
30378
31160
  #workspace;
@@ -30395,7 +31177,7 @@ var SkillManager = class {
30395
31177
  async create(draft) {
30396
31178
  const value = parseDraft(draft);
30397
31179
  const projectRoot = this.#projectRoot();
30398
- const directory = join44(projectRoot, value.name);
31180
+ const directory = join45(projectRoot, value.name);
30399
31181
  await mkdir26(projectRoot, { recursive: true, mode: 448 });
30400
31182
  try {
30401
31183
  await mkdir26(directory, { mode: 448 });
@@ -30404,7 +31186,7 @@ var SkillManager = class {
30404
31186
  throw error;
30405
31187
  }
30406
31188
  try {
30407
- await writeFile16(join44(directory, "SKILL.md"), renderSkill(value), { encoding: "utf8", flag: "wx", mode: 384 });
31189
+ await writeFile16(join45(directory, "SKILL.md"), renderSkill(value), { encoding: "utf8", flag: "wx", mode: 384 });
30408
31190
  await this.#persistEnabled(value.name, true);
30409
31191
  return this.get(value.name);
30410
31192
  } catch (error) {
@@ -30422,7 +31204,7 @@ var SkillManager = class {
30422
31204
  throw error;
30423
31205
  });
30424
31206
  if (!info.isDirectory() || info.isSymbolicLink()) throw new Error(`Project skill path is not a real directory: ${validName}`);
30425
- const skillFile = join44(directory, "SKILL.md");
31207
+ const skillFile = join45(directory, "SKILL.md");
30426
31208
  const fileInfo = await lstat14(skillFile);
30427
31209
  if (!fileInfo.isFile() || fileInfo.isSymbolicLink()) throw new Error(`Project skill file is not a real file: ${validName}`);
30428
31210
  await writeFile16(skillFile, renderSkill(value), { encoding: "utf8", mode: 384 });
@@ -30454,12 +31236,12 @@ var SkillManager = class {
30454
31236
  async #registry() {
30455
31237
  const loaded = await loadConfig({ cwd: this.#workspace, home: this.#home });
30456
31238
  const [globalPluginRoots, projectPluginRoots] = await Promise.all([
30457
- discoverPluginSkillRoots(join44(this.#home, ".flavor-code", "plugins")),
30458
- discoverPluginSkillRoots(join44(this.#workspace, ".flavor", "plugins"))
31239
+ discoverPluginSkillRoots(join45(this.#home, ".flavor-code", "plugins")),
31240
+ discoverPluginSkillRoots(join45(this.#workspace, ".flavor", "plugins"))
30459
31241
  ]);
30460
31242
  return {
30461
31243
  registry: new SkillRegistry({
30462
- globalRoots: [join44(this.#home, ".flavor-code", "skills"), ...globalPluginRoots],
31244
+ globalRoots: [join45(this.#home, ".flavor-code", "skills"), ...globalPluginRoots],
30463
31245
  projectRoots: [this.#projectRoot(), ...projectPluginRoots]
30464
31246
  }),
30465
31247
  disabled: new Set(loaded.config.skills.disabled)
@@ -30475,10 +31257,10 @@ var SkillManager = class {
30475
31257
  };
30476
31258
  }
30477
31259
  #projectRoot() {
30478
- return join44(this.#workspace, ".flavor", "skills");
31260
+ return join45(this.#workspace, ".flavor", "skills");
30479
31261
  }
30480
31262
  #projectSkillDirectory(name) {
30481
- return join44(this.#projectRoot(), name);
31263
+ return join45(this.#projectRoot(), name);
30482
31264
  }
30483
31265
  };
30484
31266
  function parseName(value) {
@@ -30519,9 +31301,9 @@ async function discoverPluginSkillRoots(directory) {
30519
31301
  const roots = [];
30520
31302
  for (const entry2 of entries) {
30521
31303
  if (!entry2.isDirectory() || entry2.isSymbolicLink()) continue;
30522
- const pluginRoot = await realpath13(join44(directory, entry2.name));
31304
+ const pluginRoot = await realpath13(join45(directory, entry2.name));
30523
31305
  try {
30524
- const manifest = PluginManifestSchema.parse(JSON.parse(await readFile35(join44(pluginRoot, "flavor-plugin.json"), "utf8")));
31306
+ const manifest = PluginManifestSchema.parse(JSON.parse(await readFile36(join45(pluginRoot, "flavor-plugin.json"), "utf8")));
30525
31307
  for (const contribution of manifest.contributes.skillRoots) {
30526
31308
  const candidate = resolve37(pluginRoot, contribution.path);
30527
31309
  const info = await lstat14(candidate);
@@ -30596,11 +31378,11 @@ async function createProjectMemoryManager(options) {
30596
31378
  }
30597
31379
 
30598
31380
  // src/mcp/config-manager.ts
30599
- import { join as join45 } from "path";
31381
+ import { join as join46 } from "path";
30600
31382
  var ProjectMcpConfigManager = class {
30601
31383
  path;
30602
31384
  constructor(workspace) {
30603
- this.path = join45(workspace, ".flavor", "flavor.json");
31385
+ this.path = join46(workspace, ".flavor", "flavor.json");
30604
31386
  }
30605
31387
  async list() {
30606
31388
  const document = await readRecoverableFile(this.path, (raw) => parseDocument2(this.path, raw));
@@ -30697,7 +31479,7 @@ function isRecord7(value) {
30697
31479
  }
30698
31480
 
30699
31481
  // src/desktop/model-config.ts
30700
- import { join as join46 } from "path";
31482
+ import { join as join47 } from "path";
30701
31483
  var DEFAULT_DESKTOP_MODELS = [
30702
31484
  {
30703
31485
  id: "anthropic:deepseek-v4-pro",
@@ -30718,7 +31500,7 @@ var DEFAULT_DESKTOP_MODELS = [
30718
31500
  ];
30719
31501
  async function loadDesktopModels(workspace, home) {
30720
31502
  try {
30721
- const tokens2 = await createFileTokenStore(join46(home, ".flavor-code", "auth.json")).load();
31503
+ const tokens2 = await createFileTokenStore(join47(home, ".flavor-code", "auth.json")).load();
30722
31504
  const pkceModels = [];
30723
31505
  const knownPkce = /* @__PURE__ */ new Set();
30724
31506
  for (const token of Object.values(tokens2)) {
@@ -30786,7 +31568,7 @@ function protocolLabel(type) {
30786
31568
 
30787
31569
  // src/desktop/d2c-product-preview.ts
30788
31570
  import { createReadStream as createReadStream2 } from "fs";
30789
- import { realpath as realpath14, stat as stat15 } from "fs/promises";
31571
+ import { realpath as realpath14, stat as stat16 } from "fs/promises";
30790
31572
  import { createServer as createServer6 } from "http";
30791
31573
  import { extname as extname4, isAbsolute as isAbsolute20, relative as relative27, resolve as resolve38, sep as sep28 } from "path";
30792
31574
  var MIME_TYPES = {
@@ -30811,7 +31593,7 @@ function isInside(root, candidate) {
30811
31593
  }
30812
31594
  async function startD2cProductPreview(rootDirectory) {
30813
31595
  const root = await realpath14(resolve38(rootDirectory));
30814
- const rootInfo = await stat15(root);
31596
+ const rootInfo = await stat16(root);
30815
31597
  if (!rootInfo.isDirectory()) throw new Error("D2C product prototype directory is unavailable");
30816
31598
  const server = createServer6(async (request, response2) => {
30817
31599
  response2.setHeader("cache-control", "no-store");
@@ -30826,7 +31608,7 @@ async function startD2cProductPreview(rootDirectory) {
30826
31608
  if (!isInside(root, unresolved)) return sendNotFound(response2);
30827
31609
  const target = await realpath14(unresolved).catch(() => void 0);
30828
31610
  if (target === void 0 || !isInside(root, target)) return sendNotFound(response2);
30829
- const info = await stat15(target);
31611
+ const info = await stat16(target);
30830
31612
  if (!info.isFile()) return sendNotFound(response2);
30831
31613
  response2.writeHead(200, {
30832
31614
  "content-length": info.size,
@@ -30863,33 +31645,33 @@ function pngDataUrl(png) {
30863
31645
  }
30864
31646
  async function detectD2cFramework(workspace, task) {
30865
31647
  try {
30866
- const pkg = JSON.parse(await readFile36(join47(d2cOutputDirectory(workspace, task), "package.json"), "utf8"));
31648
+ const pkg = JSON.parse(await readFile37(join48(d2cOutputDirectory(workspace, task), "package.json"), "utf8"));
30867
31649
  return pkg.dependencies?.react === void 0 ? "vue" : "react";
30868
31650
  } catch {
30869
31651
  return "vue";
30870
31652
  }
30871
31653
  }
30872
31654
  function integrationDirectory(workspace, task) {
30873
- return join47(workspace, ".flavor", "d2c", task, "integration");
31655
+ return join48(workspace, ".flavor", "d2c", task, "integration");
30874
31656
  }
30875
31657
  var D2C_BACKEND_SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".cfg", ".env", ".ini", ".js", ".json", ".lock", ".mjs", ".py", ".toml", ".txt", ".yaml", ".yml"]);
30876
31658
  var D2C_BACKEND_IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([".git", ".venv", "__pycache__", "data", "node_modules", "venv"]);
30877
31659
  async function d2cBackendSourceFingerprint(project) {
30878
- const runtimeManifest = join47(project, "server", "flavor-runtime.json");
30879
- const realBackend = (await stat16(runtimeManifest).catch(() => void 0))?.isFile() === true;
30880
- const root = realBackend ? join47(project, "server") : join47(project, "mock");
30881
- if ((await stat16(root).catch(() => void 0))?.isDirectory() !== true) return void 0;
31660
+ const runtimeManifest = join48(project, "server", "flavor-runtime.json");
31661
+ const realBackend = (await stat17(runtimeManifest).catch(() => void 0))?.isFile() === true;
31662
+ const root = realBackend ? join48(project, "server") : join48(project, "mock");
31663
+ if ((await stat17(root).catch(() => void 0))?.isDirectory() !== true) return void 0;
30882
31664
  const files = [];
30883
31665
  const visit = async (directory) => {
30884
31666
  const entries = await readdir13(directory, { withFileTypes: true });
30885
31667
  for (const entry2 of entries) {
30886
31668
  if (entry2.isDirectory()) {
30887
- if (!D2C_BACKEND_IGNORED_DIRECTORIES.has(entry2.name)) await visit(join47(directory, entry2.name));
31669
+ if (!D2C_BACKEND_IGNORED_DIRECTORIES.has(entry2.name)) await visit(join48(directory, entry2.name));
30888
31670
  continue;
30889
31671
  }
30890
31672
  if (!entry2.isFile()) continue;
30891
31673
  const extension2 = entry2.name.includes(".") ? `.${entry2.name.split(".").pop().toLowerCase()}` : "";
30892
- if (D2C_BACKEND_SOURCE_EXTENSIONS.has(extension2)) files.push(join47(directory, entry2.name));
31674
+ if (D2C_BACKEND_SOURCE_EXTENSIONS.has(extension2)) files.push(join48(directory, entry2.name));
30893
31675
  }
30894
31676
  };
30895
31677
  await visit(root);
@@ -30897,7 +31679,7 @@ async function d2cBackendSourceFingerprint(project) {
30897
31679
  for (const path of files.sort()) {
30898
31680
  hash.update(path.slice(root.length));
30899
31681
  hash.update("\0");
30900
- hash.update(await readFile36(path));
31682
+ hash.update(await readFile37(path));
30901
31683
  hash.update("\0");
30902
31684
  }
30903
31685
  return hash.digest("hex");
@@ -30933,6 +31715,7 @@ var DesktopRuntimeController = class {
30933
31715
  #busy = false;
30934
31716
  #tasks;
30935
31717
  #disposeJobSubscription;
31718
+ #memoryRestartPromise;
30936
31719
  #d2cMocks = /* @__PURE__ */ new Map();
30937
31720
  #d2cPreviews = /* @__PURE__ */ new Map();
30938
31721
  #d2cProductPreviews = /* @__PURE__ */ new Map();
@@ -31059,6 +31842,7 @@ var DesktopRuntimeController = class {
31059
31842
  this.#captureSessionOutput(event);
31060
31843
  this.#emit({ type: "session-output", sessionId: outputSessionId, event });
31061
31844
  },
31845
+ onMemoryRestartRequested: (sessionId) => this.#scheduleMemoryRestart(sessionId),
31062
31846
  onApprovalChange: () => {
31063
31847
  if (this.#runtime !== void 0) this.#publishSnapshot();
31064
31848
  },
@@ -31443,8 +32227,8 @@ var DesktopRuntimeController = class {
31443
32227
  if (workflow === void 0 || !reviewProgress(workflow).complete || workflow.stage === "visual-review") {
31444
32228
  throw new Error("Complete visual review before importing OpenAPI");
31445
32229
  }
31446
- const moduleManifest = join47(d2cOutputDirectory(workspace, task), "d2c.modules.json");
31447
- const moduleManifestInfo = await stat16(moduleManifest).catch(() => void 0);
32230
+ const moduleManifest = join48(d2cOutputDirectory(workspace, task), "d2c.modules.json");
32231
+ const moduleManifestInfo = await stat17(moduleManifest).catch(() => void 0);
31448
32232
  if (moduleManifestInfo === void 0 || !moduleManifestInfo.isFile()) {
31449
32233
  throw new Error("D2C implementation must write d2c.modules.json before API mapping; ask the agent to split the project into modules first");
31450
32234
  }
@@ -31456,8 +32240,8 @@ var DesktopRuntimeController = class {
31456
32240
  await mkdir27(dir, { recursive: true });
31457
32241
  const hash = createHash27("sha256").update(raw).digest("hex");
31458
32242
  await Promise.all([
31459
- writeFile17(join47(dir, "swagger.json"), raw),
31460
- writeFile17(join47(dir, "openapi.normalized.json"), `${JSON.stringify(document, null, 2)}
32243
+ writeFile17(join48(dir, "swagger.json"), raw),
32244
+ writeFile17(join48(dir, "openapi.normalized.json"), `${JSON.stringify(document, null, 2)}
31461
32245
  `)
31462
32246
  ]);
31463
32247
  const next = {
@@ -31481,9 +32265,9 @@ var DesktopRuntimeController = class {
31481
32265
  const workspace = this.#workspace;
31482
32266
  if (workspace === void 0) throw new Error("Open a project before importing OpenAPI");
31483
32267
  await this.#assertProductPrdLocked(workspace, task);
31484
- const info = await stat16(sourcePath);
32268
+ const info = await stat17(sourcePath);
31485
32269
  if (!info.isFile() || info.size > 8 * 1024 * 1024) throw new Error("OpenAPI JSON must be a file no larger than 8 MiB");
31486
- return this.#storeD2cOpenApi(workspace, task, basename10(sourcePath), await readFile36(sourcePath, "utf8"), false);
32270
+ return this.#storeD2cOpenApi(workspace, task, basename10(sourcePath), await readFile37(sourcePath, "utf8"), false);
31487
32271
  }
31488
32272
  async getD2cIntegration(task) {
31489
32273
  const workspace = this.#workspace;
@@ -31492,8 +32276,8 @@ var DesktopRuntimeController = class {
31492
32276
  let workflow = await readWorkflow(workspace, task);
31493
32277
  if (workflow?.openapi === void 0 && workflow !== void 0 && reviewProgress(workflow).complete && await readD2cProductPlanView(workspace, task) !== void 0) {
31494
32278
  const modules = await readD2cModules(workspace, task);
31495
- const productOpenApiPath = join47(taskDir(workspace, task), "product", "openapi.json");
31496
- const authored = await readFile36(productOpenApiPath, "utf8").catch(() => void 0);
32279
+ const productOpenApiPath = join48(taskDir(workspace, task), "product", "openapi.json");
32280
+ const authored = await readFile37(productOpenApiPath, "utf8").catch(() => void 0);
31497
32281
  const generated = authored === void 0;
31498
32282
  const prepared = await this.#storeD2cOpenApi(
31499
32283
  workspace,
@@ -31505,7 +32289,7 @@ var DesktopRuntimeController = class {
31505
32289
  return prepared;
31506
32290
  }
31507
32291
  if (workflow?.openapi === void 0 || workflow.mappings === void 0) return void 0;
31508
- const document = JSON.parse(await readFile36(join47(integrationDirectory(workspace, task), "openapi.normalized.json"), "utf8"));
32292
+ const document = JSON.parse(await readFile37(join48(integrationDirectory(workspace, task), "openapi.normalized.json"), "utf8"));
31509
32293
  return { workflow, document, mappings: workflow.mappings };
31510
32294
  }
31511
32295
  async confirmD2cMapping(task, moduleId, operationKey) {
@@ -31572,7 +32356,7 @@ var DesktopRuntimeController = class {
31572
32356
  ].join("\n");
31573
32357
  const delivery = await readDeliveryRun(workspace, task);
31574
32358
  if (delivery?.nodes.api.status === "running") {
31575
- const apiArtifacts = await Promise.all(generated.files.map(async (file) => artifactRef(file, await readFile36(join47(project, file)))));
32359
+ const apiArtifacts = await Promise.all(generated.files.map(async (file) => artifactRef(file, await readFile37(join48(project, file)))));
31576
32360
  const completed = await updateDeliveryRun(
31577
32361
  workspace,
31578
32362
  task,
@@ -31599,8 +32383,8 @@ var DesktopRuntimeController = class {
31599
32383
  try {
31600
32384
  const fingerprint2 = await d2cBackendSourceFingerprint(project);
31601
32385
  if (fingerprint2 !== void 0) this.#d2cMockSourceFingerprints.set(task, fingerprint2);
31602
- const environmentPath = join47(project, ".env.local");
31603
- const existingEnvironment = await readFile36(environmentPath, "utf8").catch(() => "");
32386
+ const environmentPath = join48(project, ".env.local");
32387
+ const existingEnvironment = await readFile37(environmentPath, "utf8").catch(() => "");
31604
32388
  const environment = `${existingEnvironment.split(/\r?\n/).filter((line) => !/^\s*VITE_API_BASE_URL\s*=/.test(line)).join("\n").trim()}
31605
32389
  VITE_API_BASE_URL=${running.url}
31606
32390
  `.replace(/^\n/, "");
@@ -31680,8 +32464,8 @@ VITE_API_BASE_URL=${running.url}
31680
32464
  const readyPreview = this.#d2cPreviews.get(task);
31681
32465
  const readyMock = this.#d2cMocks.get(task);
31682
32466
  if (readyPreview === void 0 || readyMock === void 0) throw new Error("D2C preview or backend service is unavailable after synchronization");
31683
- const manifestPath = join47(taskDir(workspace, task), "design", "interaction-manifest.json");
31684
- const manifest = parseInteractionManifest(await readFile36(manifestPath, "utf8"));
32467
+ const manifestPath = join48(taskDir(workspace, task), "design", "interaction-manifest.json");
32468
+ const manifest = parseInteractionManifest(await readFile37(manifestPath, "utf8"));
31685
32469
  const baseline = await this.#verifyAcceptanceBaseline(workspace, task);
31686
32470
  const coverage = baseline === void 0 ? void 0 : verifyRequirementCoverage(baseline.approvedPrd.criteria, manifest);
31687
32471
  let executableManifest = manifest;
@@ -31692,7 +32476,7 @@ VITE_API_BASE_URL=${running.url}
31692
32476
  await mkdir27(planDir, { recursive: true });
31693
32477
  try {
31694
32478
  const observations = await this.#observeD2cPages(manifest, readyPreview.url);
31695
- const prd = await readFile36(join47(taskDir(workspace, task), "product", "prd.md"), "utf8").catch(() => void 0);
32479
+ const prd = await readFile37(join48(taskDir(workspace, task), "product", "prd.md"), "utf8").catch(() => void 0);
31696
32480
  const apiContext = JSON.stringify({ openapi: workflow.openapi, mappings: workflow.mappings }, null, 2);
31697
32481
  const plan = await this.#d2cJudge.planInteractions({ task, seed: manifest, observations, ...prd === void 0 ? {} : { prd }, apiContext });
31698
32482
  executableManifest = mergeInteractionManifests(manifest, plan.manifest);
@@ -31703,9 +32487,9 @@ VITE_API_BASE_URL=${running.url}
31703
32487
  plannedScenarios: executableManifest.pages.reduce((total, page) => total + page.scenarios.length, 0),
31704
32488
  pageAnalyses: plan.pageAnalyses
31705
32489
  };
31706
- await writeFile17(join47(planDir, "autonomous-interaction-plan.json"), `${JSON.stringify({ ...plan, manifest: executableManifest }, null, 2)}
32490
+ await writeFile17(join48(planDir, "autonomous-interaction-plan.json"), `${JSON.stringify({ ...plan, manifest: executableManifest }, null, 2)}
31707
32491
  `, { encoding: "utf8", mode: 384 });
31708
- await rm15(join47(planDir, "autonomous-interaction-diagnostic.json"), { force: true });
32492
+ await rm15(join48(planDir, "autonomous-interaction-diagnostic.json"), { force: true });
31709
32493
  } catch (cause) {
31710
32494
  const detail = message(cause);
31711
32495
  review = {
@@ -31713,8 +32497,8 @@ VITE_API_BASE_URL=${running.url}
31713
32497
  plannedScenarios: manifest.pages.reduce((total, page) => total + page.scenarios.length, 0),
31714
32498
  warning: `\u591A\u6A21\u6001\u81EA\u4E3B\u89C4\u5212\u5931\u8D25\uFF0C\u5DF2\u7EE7\u7EED\u6267\u884C\u5DF2\u786E\u8BA4\u7684\u8BBE\u8BA1\u5951\u7EA6\uFF1A${detail}`
31715
32499
  };
31716
- await rm15(join47(planDir, "autonomous-interaction-plan.json"), { force: true });
31717
- await writeFile17(join47(planDir, "autonomous-interaction-diagnostic.json"), `${JSON.stringify({
32500
+ await rm15(join48(planDir, "autonomous-interaction-plan.json"), { force: true });
32501
+ await writeFile17(join48(planDir, "autonomous-interaction-diagnostic.json"), `${JSON.stringify({
31718
32502
  schema: 1,
31719
32503
  stage: "planning",
31720
32504
  occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -31728,13 +32512,13 @@ VITE_API_BASE_URL=${running.url}
31728
32512
  let result = { ...rawResult, evidenceMode: review.mode };
31729
32513
  if (coverage !== void 0) {
31730
32514
  const evidence = createAcceptanceEvidence(coverage, result);
31731
- await writeFile17(join47(taskDir(workspace, task), "acceptance-evidence.json"), `${JSON.stringify({ ...evidence, baseline }, null, 2)}
32515
+ await writeFile17(join48(taskDir(workspace, task), "acceptance-evidence.json"), `${JSON.stringify({ ...evidence, baseline }, null, 2)}
31732
32516
  `, { encoding: "utf8", mode: 384 });
31733
32517
  if (!evidence.passed) result = { ...result, passed: false, failures: Math.max(1, result.failures) };
31734
32518
  }
31735
32519
  const dir = integrationDirectory(workspace, task);
31736
32520
  await mkdir27(dir, { recursive: true });
31737
- await writeFile17(join47(dir, "interaction-results.json"), `${JSON.stringify(result, null, 2)}
32521
+ await writeFile17(join48(dir, "interaction-results.json"), `${JSON.stringify(result, null, 2)}
31738
32522
  `);
31739
32523
  const next = await updateWorkflow(workspace, task, workflow.revision, (current) => applyInteractionRun(current, result));
31740
32524
  return { workflow: next, result, review };
@@ -31747,7 +32531,7 @@ VITE_API_BASE_URL=${running.url}
31747
32531
  if (mock === void 0 || !mock.exited() || result.failures === 0) return result;
31748
32532
  const output = mock.output().trim();
31749
32533
  try {
31750
- const dir = join47(d2cOutputDirectory(workspace, task), "mock-server.log");
32534
+ const dir = join48(d2cOutputDirectory(workspace, task), "mock-server.log");
31751
32535
  await writeFile17(dir, `${output}
31752
32536
  `);
31753
32537
  } catch {
@@ -31842,7 +32626,7 @@ VITE_API_BASE_URL=${running.url}
31842
32626
  if (current === void 0) throw new Error("D2C product plan is unavailable");
31843
32627
  if (current.plan.approvedPrd !== void 0) throw new Error("The approved PRD is immutable and cannot be edited");
31844
32628
  if (current.plan.phase !== "prd-review") throw new Error("PRD sections can only be edited during PRD review");
31845
- const path = join47(taskDir(workspace, task), "product", "prd.md");
32629
+ const path = join48(taskDir(workspace, task), "product", "prd.md");
31846
32630
  const updated = await updatePrdSectionFile(path, { sectionId, body, expectedHash });
31847
32631
  const now = (/* @__PURE__ */ new Date()).toISOString();
31848
32632
  await writeD2cProductPlan(workspace, {
@@ -31862,33 +32646,33 @@ VITE_API_BASE_URL=${running.url}
31862
32646
  async #assertProductPrdLocked(workspace, task) {
31863
32647
  const current = await readD2cProductPlanView(workspace, task);
31864
32648
  if (current?.plan.approvedPrd === void 0) return;
31865
- await assertApprovedPrd(join47(taskDir(workspace, task), "product", "prd.md"), current.plan.approvedPrd);
32649
+ await assertApprovedPrd(join48(taskDir(workspace, task), "product", "prd.md"), current.plan.approvedPrd);
31866
32650
  }
31867
32651
  async #acceptancePaths(workspace, task) {
31868
- const product = join47(taskDir(workspace, task), "product");
31869
- const openapi = join47(product, "openapi.json");
32652
+ const product = join48(taskDir(workspace, task), "product");
32653
+ const openapi = join48(product, "openapi.json");
31870
32654
  return {
31871
- prd: join47(product, "prd.md"),
31872
- prototype: join47(product, "prototype", "index.html"),
31873
- interaction: join47(product, "prototype", "interaction-manifest.json"),
31874
- ...await stat16(openapi).catch(() => void 0) === void 0 ? {} : { openapi }
32655
+ prd: join48(product, "prd.md"),
32656
+ prototype: join48(product, "prototype", "index.html"),
32657
+ interaction: join48(product, "prototype", "interaction-manifest.json"),
32658
+ ...await stat17(openapi).catch(() => void 0) === void 0 ? {} : { openapi }
31875
32659
  };
31876
32660
  }
31877
32661
  async #verifyAcceptanceBaseline(workspace, task) {
31878
32662
  const product = await readD2cProductPlanView(workspace, task);
31879
32663
  if (product?.plan.approvedPrd === void 0) return void 0;
31880
32664
  await this.#assertProductPrdLocked(workspace, task);
31881
- const baseline = JSON.parse(await readFile36(join47(taskDir(workspace, task), "acceptance-baseline.json"), "utf8"));
32665
+ const baseline = JSON.parse(await readFile37(join48(taskDir(workspace, task), "acceptance-baseline.json"), "utf8"));
31882
32666
  await verifyAcceptanceBaseline(await this.#acceptancePaths(workspace, task), baseline);
31883
- const design = join47(taskDir(workspace, task), "design");
32667
+ const design = join48(taskDir(workspace, task), "design");
31884
32668
  await Promise.all([
31885
- verifyAcceptanceArtifact(join47(design, "index.html"), baseline.artifacts.prototype, "runtime prototype"),
31886
- verifyAcceptanceArtifact(join47(design, "interaction-manifest.json"), baseline.artifacts.interaction, "runtime interaction contract")
32669
+ verifyAcceptanceArtifact(join48(design, "index.html"), baseline.artifacts.prototype, "runtime prototype"),
32670
+ verifyAcceptanceArtifact(join48(design, "interaction-manifest.json"), baseline.artifacts.interaction, "runtime interaction contract")
31887
32671
  ]);
31888
32672
  const workflow = await readWorkflow(workspace, task);
31889
32673
  if (workflow?.openapi !== void 0) {
31890
32674
  await verifyAcceptanceArtifact(
31891
- join47(taskDir(workspace, task), "integration", "swagger.json"),
32675
+ join48(taskDir(workspace, task), "integration", "swagger.json"),
31892
32676
  { hash: workflow.openapi.hash },
31893
32677
  "runtime OpenAPI contract"
31894
32678
  );
@@ -31909,14 +32693,14 @@ VITE_API_BASE_URL=${running.url}
31909
32693
  throw new Error("D2C PRD is empty and cannot be accepted");
31910
32694
  }
31911
32695
  if (stage === "design" && accepted) {
31912
- const manifest = join47(d2cProductPrototypeDirectory(workspace, task), "interaction-manifest.json");
31913
- const normalizedManifest = parseInteractionManifest(await readFile36(manifest, "utf8"));
32696
+ const manifest = join48(d2cProductPrototypeDirectory(workspace, task), "interaction-manifest.json");
32697
+ const normalizedManifest = parseInteractionManifest(await readFile37(manifest, "utf8"));
31914
32698
  if (current.plan.approvedPrd === void 0) throw new Error("Approve the PRD before confirming the design");
31915
32699
  verifyRequirementCoverage(current.plan.approvedPrd.criteria, normalizedManifest);
31916
32700
  await writeFile17(manifest, `${JSON.stringify(normalizedManifest, null, 2)}
31917
32701
  `, { encoding: "utf8", mode: 384 });
31918
32702
  const baseline = await captureAcceptanceBaseline(await this.#acceptancePaths(workspace, task), current.plan.approvedPrd);
31919
- await writeFile17(join47(taskDir(workspace, task), "acceptance-baseline.json"), `${JSON.stringify(baseline, null, 2)}
32703
+ await writeFile17(join48(taskDir(workspace, task), "acceptance-baseline.json"), `${JSON.stringify(baseline, null, 2)}
31920
32704
  `, { encoding: "utf8", mode: 384 });
31921
32705
  }
31922
32706
  const plan = await writeD2cProductPlan(
@@ -31950,8 +32734,8 @@ VITE_API_BASE_URL=${running.url}
31950
32734
  const imported = await this.importD2cDesign(task, d2cProductPrototypeDirectory(workspace, task));
31951
32735
  const delivery = await readDeliveryRun(workspace, task);
31952
32736
  if (delivery !== void 0) {
31953
- const prototype = await readFile36(join47(d2cProductPrototypeDirectory(workspace, task), "index.html"), "utf8");
31954
- const interaction = await readFile36(join47(d2cProductPrototypeDirectory(workspace, task), "interaction-manifest.json"), "utf8");
32737
+ const prototype = await readFile37(join48(d2cProductPrototypeDirectory(workspace, task), "index.html"), "utf8");
32738
+ const interaction = await readFile37(join48(d2cProductPrototypeDirectory(workspace, task), "interaction-manifest.json"), "utf8");
31955
32739
  const completed = await updateDeliveryRun(workspace, task, delivery.revision, (run) => completeDeliveryNode(run, "design", [
31956
32740
  artifactRef("product/prototype/index.html", prototype),
31957
32741
  artifactRef("product/prototype/interaction-manifest.json", interaction)
@@ -32035,10 +32819,10 @@ VITE_API_BASE_URL=${running.url}
32035
32819
  const next = await updateWorkflow(workspace, task, workflow.revision, (current) => applyQualityJudgment(current, judgment));
32036
32820
  const qualityContent = `${JSON.stringify(judgment, null, 2)}
32037
32821
  `;
32038
- await writeFile17(join47(taskDir(workspace, task), "quality-judge.json"), qualityContent, { encoding: "utf8", mode: 384 });
32822
+ await writeFile17(join48(taskDir(workspace, task), "quality-judge.json"), qualityContent, { encoding: "utf8", mode: 384 });
32039
32823
  const delivery = await readDeliveryRun(workspace, task);
32040
32824
  if (next.stage === "completed" && delivery?.nodes.acceptance.status === "running") {
32041
- const interactionContent = await readFile36(join47(integrationDirectory(workspace, task), "interaction-results.json"), "utf8");
32825
+ const interactionContent = await readFile37(join48(integrationDirectory(workspace, task), "interaction-results.json"), "utf8");
32042
32826
  const accepted = await updateDeliveryRun(workspace, task, delivery.revision, (run) => completeDeliveryNode(run, "acceptance", [
32043
32827
  artifactRef("integration/interaction-results.json", interactionContent),
32044
32828
  artifactRef("quality-judge.json", qualityContent)
@@ -32120,6 +32904,39 @@ VITE_API_BASE_URL=${running.url}
32120
32904
  await runtime.session.close();
32121
32905
  await runtime.dispose();
32122
32906
  }
32907
+ /**
32908
+ * Electron owns the process, so a heap rotation rebuilds only the production
32909
+ * runtime. Disposing every holder and forcing GC gives the resumed /loop or
32910
+ * /goal a clean live set without terminating the desktop application.
32911
+ */
32912
+ #scheduleMemoryRestart(sessionId) {
32913
+ if (this.#memoryRestartPromise !== void 0) return;
32914
+ const previous = this.#runtime;
32915
+ this.#memoryRestartPromise = (async () => {
32916
+ await previous?.session.whenIdle?.().catch(() => void 0);
32917
+ if (previous === void 0 || this.#runtime !== previous) return;
32918
+ await this.#disposeRuntime();
32919
+ clearMemoryRotation();
32920
+ const gc = globalThis.gc;
32921
+ if (typeof gc === "function") {
32922
+ gc();
32923
+ gc();
32924
+ }
32925
+ await this.startSession(sessionId);
32926
+ const resumed = this.#runtime;
32927
+ if (resumed !== void 0) {
32928
+ this.#busy = true;
32929
+ this.#publishSnapshot();
32930
+ await resumed.session.whenIdle?.();
32931
+ }
32932
+ })().catch((error) => {
32933
+ this.#emit({ type: "runtime-error", sessionId, message: `Memory restart failed: ${message(error)}` });
32934
+ }).finally(() => {
32935
+ this.#memoryRestartPromise = void 0;
32936
+ this.#busy = false;
32937
+ this.#publishSnapshot();
32938
+ });
32939
+ }
32123
32940
  async #stopAllD2cMocks() {
32124
32941
  const running = [...this.#d2cMocks.values()];
32125
32942
  this.#d2cMocks.clear();
@@ -32237,7 +33054,7 @@ async function loginShellEnvironment(environment, platform = process.platform, d
32237
33054
  }
32238
33055
 
32239
33056
  // src/desktop/git-manager.ts
32240
- import { readFile as readFile37, rm as rm16 } from "fs/promises";
33057
+ import { readFile as readFile38, rm as rm16 } from "fs/promises";
32241
33058
  import { isAbsolute as isAbsolute21, relative as relative28, resolve as resolve40 } from "path";
32242
33059
  function failure(action, stderr, stdout) {
32243
33060
  return new Error(`${action}\u5931\u8D25\uFF1A${(stderr.trim() || stdout.trim() || "\u672A\u77E5 Git \u9519\u8BEF").split("\n")[0]}`);
@@ -32269,7 +33086,7 @@ async function desktopGitDiff(workspace, path, staged = false) {
32269
33086
  if (file === void 0) throw new Error("\u8BE5\u6587\u4EF6\u5DF2\u4E0D\u5728\u53D8\u66F4\u5217\u8868\u4E2D");
32270
33087
  if (file.untracked) {
32271
33088
  const target = safeWorkspacePath(workspace, path);
32272
- const value = await readFile37(target, "utf8");
33089
+ const value = await readFile38(target, "utf8");
32273
33090
  return value.length > 12e4 ? `${value.slice(0, 12e4)}
32274
33091
  \u2026[\u5185\u5BB9\u5DF2\u622A\u65AD]` : value;
32275
33092
  }
@@ -32331,13 +33148,13 @@ async function desktopLastTurnDiff(workspace, sessionId, paths = []) {
32331
33148
  return path.replaceAll("\\", "/");
32332
33149
  }));
32333
33150
  try {
32334
- const tree = JSON.parse(await readFile37(resolve40(workspace, ".flavor", "session-trees", sessionId, "tree.json"), "utf8"));
33151
+ const tree = JSON.parse(await readFile38(resolve40(workspace, ".flavor", "session-trees", sessionId, "tree.json"), "utf8"));
32335
33152
  const leaf = tree.nodes?.find((node) => node.id === tree.leafId);
32336
33153
  const parent = leaf?.parentId == null ? void 0 : tree.nodes?.find((node) => node.id === leaf.parentId);
32337
33154
  if (leaf === void 0 || parent === void 0) return desktopGitReviewDiff(workspace, { scope: "last-turn", paths });
32338
33155
  const load = async (id) => {
32339
33156
  if (!/^checkpoint-[A-Za-z0-9-]+$/.test(id)) throw new Error("Invalid checkpoint id");
32340
- return JSON.parse(await readFile37(resolve40(workspace, ".flavor", "checkpoints", "manifests", `${id}.json`), "utf8"));
33157
+ return JSON.parse(await readFile38(resolve40(workspace, ".flavor", "checkpoints", "manifests", `${id}.json`), "utf8"));
32341
33158
  };
32342
33159
  const [before, after] = await Promise.all([load(parent.checkpointId), load(leaf.checkpointId)]);
32343
33160
  const oldFiles = new Map(before.files.map((file) => [file.path, file.digest]));
@@ -32346,7 +33163,7 @@ async function desktopLastTurnDiff(workspace, sessionId, paths = []) {
32346
33163
  const object = async (digest) => {
32347
33164
  if (digest === void 0) return "";
32348
33165
  if (!/^[a-f0-9]{64}$/.test(digest)) throw new Error("Invalid checkpoint object");
32349
- const value = await readFile37(resolve40(workspace, ".flavor", "checkpoints", "objects", digest));
33166
+ const value = await readFile38(resolve40(workspace, ".flavor", "checkpoints", "objects", digest));
32350
33167
  return value.includes(0) ? "[binary content]" : value.toString("utf8");
32351
33168
  };
32352
33169
  const chunks = await Promise.all(changed.map(async (path) => wholeFileDiff(path, await object(oldFiles.get(path)), await object(newFiles.get(path)))));
@@ -32385,8 +33202,8 @@ function safeGitTarget(value) {
32385
33202
 
32386
33203
  // src/desktop/worktree-manager.ts
32387
33204
  import { randomUUID as randomUUID28 } from "crypto";
32388
- import { mkdir as mkdir28, readFile as readFile38, readdir as readdir14, rm as rm17, stat as stat17, writeFile as writeFile18 } from "fs/promises";
32389
- import { basename as basename11, isAbsolute as isAbsolute22, join as join48, relative as relative29, resolve as resolve41, sep as sep29 } from "path";
33205
+ import { mkdir as mkdir28, readFile as readFile39, readdir as readdir14, rm as rm17, stat as stat18, writeFile as writeFile18 } from "fs/promises";
33206
+ import { basename as basename11, isAbsolute as isAbsolute22, join as join49, relative as relative29, resolve as resolve41, sep as sep29 } from "path";
32390
33207
  var DesktopWorktreeManager = class {
32391
33208
  #repository;
32392
33209
  #storage;
@@ -32432,8 +33249,8 @@ var DesktopWorktreeManager = class {
32432
33249
  }
32433
33250
  const values = await Promise.all(names.filter((name) => name.endsWith(".json")).map(async (name) => {
32434
33251
  try {
32435
- const stored = validateStored(JSON.parse(await readFile38(join48(this.#storage, name), "utf8")), this.#repository, this.#storage);
32436
- if ((await stat17(stored.path).catch(() => void 0))?.isDirectory() !== true) return void 0;
33252
+ const stored = validateStored(JSON.parse(await readFile39(join49(this.#storage, name), "utf8")), this.#repository, this.#storage);
33253
+ if ((await stat18(stored.path).catch(() => void 0))?.isDirectory() !== true) return void 0;
32437
33254
  const status = await git2(stored.path, ["status", "--porcelain"]);
32438
33255
  const merged = await git2(this.#repository, ["merge-base", "--is-ancestor", stored.branch, "HEAD"]);
32439
33256
  const { version: _version, ...item } = stored;
@@ -32466,7 +33283,7 @@ var DesktopWorktreeManager = class {
32466
33283
  }
32467
33284
  async #load(id) {
32468
33285
  const path = this.#metadataPath(id);
32469
- return validateStored(JSON.parse(await readFile38(path, "utf8")), this.#repository, this.#storage);
33286
+ return validateStored(JSON.parse(await readFile39(path, "utf8")), this.#repository, this.#storage);
32470
33287
  }
32471
33288
  #metadataPath(id) {
32472
33289
  if (!/^worktree-[a-z0-9._-]{1,80}$/.test(id)) throw new Error("Invalid worktree id");
@@ -32494,9 +33311,9 @@ function validateStored(value, repository, storage) {
32494
33311
  }
32495
33312
 
32496
33313
  // src/desktop/workbench-service.ts
32497
- import { readFile as readFile39, readdir as readdir15, stat as stat18 } from "fs/promises";
33314
+ import { readFile as readFile40, readdir as readdir15, stat as stat19 } from "fs/promises";
32498
33315
  import { homedir as homedir6 } from "os";
32499
- import { join as join49, resolve as resolve42 } from "path";
33316
+ import { join as join50, resolve as resolve42 } from "path";
32500
33317
  var MAX_TEXT = 128e3;
32501
33318
  var MAX_ITEMS = 200;
32502
33319
  var DesktopWorkbenchService = class {
@@ -32529,11 +33346,11 @@ var DesktopWorkbenchService = class {
32529
33346
  return url.toString().replace(/\/$/, input.trim().endsWith("/") ? "/" : "");
32530
33347
  }
32531
33348
  async #goals() {
32532
- const root = join49(this.#workspace, ".flavor", "goals");
33349
+ const root = join50(this.#workspace, ".flavor", "goals");
32533
33350
  const names = await readdir15(root).catch((error) => error.code === "ENOENT" ? [] : Promise.reject(error));
32534
33351
  const values = await Promise.all(names.filter((name) => /^goal-[A-Za-z0-9._-]+\.json$/.test(name)).slice(0, MAX_ITEMS).map(async (name) => {
32535
33352
  try {
32536
- return GoalStateSchema.parse(JSON.parse(await boundedRead2(join49(root, name))));
33353
+ return GoalStateSchema.parse(JSON.parse(await boundedRead2(join50(root, name))));
32537
33354
  } catch {
32538
33355
  return void 0;
32539
33356
  }
@@ -32543,7 +33360,7 @@ var DesktopWorkbenchService = class {
32543
33360
  async #instructions() {
32544
33361
  const names = ["FLAVOR.md", "AGENTS.md", "CLAUDE.md", "AGENTS.local.md", "CLAUDE.local.md"];
32545
33362
  const items = await Promise.all(names.map(async (name) => {
32546
- const path = join49(this.#workspace, name);
33363
+ const path = join50(this.#workspace, name);
32547
33364
  try {
32548
33365
  return { name, path, content: redactErrorText(await boundedRead2(path)) };
32549
33366
  } catch (error) {
@@ -32556,9 +33373,9 @@ var DesktopWorkbenchService = class {
32556
33373
  async #permissionFiles() {
32557
33374
  const candidates = [
32558
33375
  ...process.env.FLAVOR_MANAGED_PERMISSIONS ? [{ tier: "managed", path: resolve42(process.env.FLAVOR_MANAGED_PERMISSIONS), content: "" }] : [],
32559
- { tier: "user", path: join49(this.#home, ".flavor-code", "permissions.json"), content: "" },
32560
- { tier: "project", path: join49(this.#workspace, ".flavor", "permissions.json"), content: "" },
32561
- { tier: "local", path: join49(this.#workspace, ".flavor", "permissions.local.json"), content: "" }
33376
+ { tier: "user", path: join50(this.#home, ".flavor-code", "permissions.json"), content: "" },
33377
+ { tier: "project", path: join50(this.#workspace, ".flavor", "permissions.json"), content: "" },
33378
+ { tier: "local", path: join50(this.#workspace, ".flavor", "permissions.local.json"), content: "" }
32562
33379
  ];
32563
33380
  const values = await Promise.all(candidates.map(async (item) => {
32564
33381
  try {
@@ -32573,7 +33390,7 @@ var DesktopWorkbenchService = class {
32573
33390
  async #audit() {
32574
33391
  let raw;
32575
33392
  try {
32576
- raw = await boundedRead2(join49(this.#workspace, ".flavor", "audit.jsonl"));
33393
+ raw = await boundedRead2(join50(this.#workspace, ".flavor", "audit.jsonl"));
32577
33394
  } catch (error) {
32578
33395
  if (error.code === "ENOENT") return [];
32579
33396
  throw error;
@@ -32588,7 +33405,7 @@ var DesktopWorkbenchService = class {
32588
33405
  }).reverse();
32589
33406
  }
32590
33407
  async #context(sessionId) {
32591
- const usage = await readJsonLines2(join49(this.#workspace, ".flavor", "usage.jsonl"));
33408
+ const usage = await readJsonLines2(join50(this.#workspace, ".flavor", "usage.jsonl"));
32592
33409
  if (sessionId === void 0) return { visibility: [], usage };
32593
33410
  try {
32594
33411
  const document = await new SessionStore({ workspace: this.#workspace }).load(sessionId);
@@ -32599,10 +33416,10 @@ var DesktopWorkbenchService = class {
32599
33416
  }
32600
33417
  };
32601
33418
  async function boundedRead2(path) {
32602
- const info = await stat18(path);
33419
+ const info = await stat19(path);
32603
33420
  if (!info.isFile()) throw new Error(`Expected a file: ${path}`);
32604
33421
  if (info.size > MAX_TEXT) throw new Error(`Workbench file exceeds ${MAX_TEXT} bytes: ${path}`);
32605
- return readFile39(path, "utf8");
33422
+ return readFile40(path, "utf8");
32606
33423
  }
32607
33424
  async function readJsonLines2(path) {
32608
33425
  try {
@@ -32635,7 +33452,7 @@ var appMenu;
32635
33452
  var quitting = false;
32636
33453
  function logStartup(step, detail) {
32637
33454
  try {
32638
- const logPath = join50(dirname23(process.execPath), "flavor-code-startup.log");
33455
+ const logPath = join51(dirname23(process.execPath), "flavor-code-startup.log");
32639
33456
  const ts = (/* @__PURE__ */ new Date()).toISOString();
32640
33457
  appendFileSync(logPath, `[${ts}] ${step}${detail ? ` | ${detail}` : ""}
32641
33458
  `);
@@ -32660,7 +33477,7 @@ var d2cEmbedded = createEmbeddedD2cAutomation(() => {
32660
33477
  };
32661
33478
  });
32662
33479
  var d2cJudgeClient = createD2cJudgeClient();
32663
- var judgeStore = () => createD2cJudgeConfigStore(join50(app.getPath("userData"), "d2c-judge.json"));
33480
+ var judgeStore = () => createD2cJudgeConfigStore(join51(app.getPath("userData"), "d2c-judge.json"));
32664
33481
  function judgeImage(png) {
32665
33482
  const image = nativeImage.createFromBuffer(png);
32666
33483
  const size = image.getSize();
@@ -32686,7 +33503,7 @@ function sessionMetaKey(workspace, sessionId) {
32686
33503
  }
32687
33504
  function worktreeManager(workspace) {
32688
33505
  const key = createHash28("sha256").update(resolve43(workspace)).digest("hex").slice(0, 20);
32689
- return new DesktopWorktreeManager({ repository: workspace, storage: join50(app.getPath("userData"), "task-worktrees", key) });
33506
+ return new DesktopWorktreeManager({ repository: workspace, storage: join51(app.getPath("userData"), "task-worktrees", key) });
32690
33507
  }
32691
33508
  function activeWorkingDirectory() {
32692
33509
  if (activeWorkspace === void 0) throw new Error("\u8BF7\u5148\u6253\u5F00\u9879\u76EE");
@@ -32831,7 +33648,7 @@ function createDesktopController(workspace, owner2) {
32831
33648
  alias: `desktop-${basename12(workspace ?? runtimeOptions.workspace ?? "flavor")}`.slice(0, 64),
32832
33649
  optional: true,
32833
33650
  startBroker: (address) => {
32834
- const entry2 = app.isPackaged ? join50(process.resourcesPath, "app.asar.unpacked", "dist", "desktop", "pals-broker-entry.js") : join50(moduleDirectory, "pals-broker-entry.js");
33651
+ const entry2 = app.isPackaged ? join51(process.resourcesPath, "app.asar.unpacked", "dist", "desktop", "pals-broker-entry.js") : join51(moduleDirectory, "pals-broker-entry.js");
32835
33652
  return spawnPalBrokerProcess({
32836
33653
  address,
32837
33654
  executable: process.execPath,
@@ -32853,11 +33670,11 @@ function createDesktopController(workspace, owner2) {
32853
33670
  var detachedController = createDesktopController();
32854
33671
  var controller = detachedController;
32855
33672
  function statePath() {
32856
- return join50(app.getPath("userData"), "desktop-state.json");
33673
+ return join51(app.getPath("userData"), "desktop-state.json");
32857
33674
  }
32858
33675
  async function loadPersistedProjects() {
32859
33676
  try {
32860
- const raw = await readFile40(statePath(), "utf8");
33677
+ const raw = await readFile41(statePath(), "utf8");
32861
33678
  if (raw.length > 1e5) return { projects: [] };
32862
33679
  const parsed = JSON.parse(raw);
32863
33680
  Object.assign(workbench, {
@@ -32898,7 +33715,7 @@ function validRecord(value) {
32898
33715
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
32899
33716
  }
32900
33717
  async function assertDirectory(path) {
32901
- if (!(await stat19(path)).isDirectory()) throw new Error("The selected project is not a directory");
33718
+ if (!(await stat20(path)).isDirectory()) throw new Error("The selected project is not a directory");
32902
33719
  }
32903
33720
  async function openWorkspace(path) {
32904
33721
  const workspace = resolve43(path);
@@ -33029,7 +33846,7 @@ async function chooseAndOpenWorkspace() {
33029
33846
  var cachedAppIconDataUrl;
33030
33847
  function appIconDataUrl() {
33031
33848
  if (cachedAppIconDataUrl !== void 0) return cachedAppIconDataUrl;
33032
- const icon = nativeImage.createFromPath(join50(app.getAppPath(), "assets", "icon.png"));
33849
+ const icon = nativeImage.createFromPath(join51(app.getAppPath(), "assets", "icon.png"));
33033
33850
  if (icon.isEmpty()) return void 0;
33034
33851
  cachedAppIconDataUrl = icon.toDataURL();
33035
33852
  return cachedAppIconDataUrl;
@@ -33447,7 +34264,7 @@ function applicationMenu() {
33447
34264
  ]);
33448
34265
  }
33449
34266
  async function createWindow() {
33450
- const rendererPath = join50(app.getAppPath(), "dist", "desktop-renderer", "index.html");
34267
+ const rendererPath = join51(app.getAppPath(), "dist", "desktop-renderer", "index.html");
33451
34268
  mainWindow = new BrowserWindow2({
33452
34269
  title: "Flavor Code",
33453
34270
  width: 1280,
@@ -33458,14 +34275,14 @@ async function createWindow() {
33458
34275
  show: false,
33459
34276
  ...desktopWindowChrome(),
33460
34277
  webPreferences: {
33461
- preload: join50(moduleDirectory, "preload.cjs"),
34278
+ preload: join51(moduleDirectory, "preload.cjs"),
33462
34279
  contextIsolation: true,
33463
34280
  nodeIntegration: false,
33464
34281
  sandbox: false,
33465
34282
  webSecurity: true
33466
34283
  }
33467
34284
  });
33468
- const iconPath = join50(app.getAppPath(), "assets", "icon.png");
34285
+ const iconPath = join51(app.getAppPath(), "assets", "icon.png");
33469
34286
  const icon = nativeImage.createFromPath(iconPath);
33470
34287
  if (!icon.isEmpty()) {
33471
34288
  mainWindow.setIcon(icon);