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

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 (33) hide show
  1. package/dist/agent/loop.d.ts +4 -2
  2. package/dist/{app-H7RYDMD3.js → app-UWIEIWBQ.js} +3 -3
  3. package/dist/{chunk-3BHKW5A7.js → chunk-4RR5VVX4.js} +8 -2
  4. package/dist/{chunk-NIKCPCJG.js → chunk-DCNUBQCH.js} +826 -273
  5. package/dist/{chunk-KBHWS7M6.js → chunk-KQMKY7C5.js} +4 -0
  6. package/dist/chunk-LKQTBL3R.js +107 -0
  7. package/dist/cli-main.js +20 -13
  8. package/dist/cli.js +24 -5
  9. package/dist/config/schema.d.ts +4 -0
  10. package/dist/context/manager.d.ts +9 -0
  11. package/dist/desktop/main.js +1170 -532
  12. package/dist/desktop-renderer/assets/{index-CYd5JkTR.js → index-C0t1nGih.js} +2 -2
  13. package/dist/desktop-renderer/assets/{interactive-terminal-j52Ty3QO.js → interactive-terminal-BJqe96mZ.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 +18 -0
  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 +8 -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 +18 -0
  24. package/dist/loop/types.d.ts +7 -0
  25. package/dist/production.d.ts +17 -0
  26. package/dist/sdk/index.js +4 -4
  27. package/dist/session/tree.d.ts +2 -0
  28. package/dist/ui/session.d.ts +9 -0
  29. package/dist/utils/heap.d.ts +58 -0
  30. package/dist/utils/memory-restart.d.ts +38 -3
  31. package/dist/utils/rotation-census.d.ts +36 -0
  32. package/package.json +1 -1
  33. package/dist/chunk-CIVPKHF4.js +0 -53
@@ -2,10 +2,15 @@
2
2
  import { createRequire } from "node:module"; const require = createRequire(import.meta.url);
3
3
  import {
4
4
  MEMORY_RESTART_EXIT_CODE,
5
+ consumedMemoryRestartMarker,
6
+ markMemoryRotation,
5
7
  memoryRestartMarkerPath,
8
+ memoryRotationActive,
6
9
  nextMemoryRestartMarker,
7
- parseMemoryRestartMarker
8
- } from "./chunk-CIVPKHF4.js";
10
+ parseMemoryRestartMarker,
11
+ pendingContinuation,
12
+ rotationCooldownActive
13
+ } from "./chunk-LKQTBL3R.js";
9
14
  import {
10
15
  PERMISSION_MODES,
11
16
  PermissionModeSchema,
@@ -18,7 +23,7 @@ import {
18
23
  redactConfig,
19
24
  setProjectMcpServerDisabled,
20
25
  updateProtectedFile
21
- } from "./chunk-KBHWS7M6.js";
26
+ } from "./chunk-KQMKY7C5.js";
22
27
  import {
23
28
  AuditLogger,
24
29
  appendUsageLog,
@@ -2061,6 +2066,10 @@ var SessionHistory = class _SessionHistory {
2061
2066
  get leafId() {
2062
2067
  return this.#leafId;
2063
2068
  }
2069
+ /** Cheap retained-node count for the rotation census (no cloning). */
2070
+ get nodeCount() {
2071
+ return this.#nodes.length;
2072
+ }
2064
2073
  tree() {
2065
2074
  return this.#nodes.map((node) => structuredClone(node));
2066
2075
  }
@@ -3635,6 +3644,7 @@ var FlavorSession = class {
3635
3644
  #pendingCoWorkStarts = [];
3636
3645
  #activeSteeringDurableIds = /* @__PURE__ */ new Set();
3637
3646
  #durableResumeStarted = false;
3647
+ #continuationResumeStarted = false;
3638
3648
  #permissionBaseline;
3639
3649
  #activeSubmission;
3640
3650
  constructor(services) {
@@ -3655,6 +3665,10 @@ var FlavorSession = class {
3655
3665
  queueSnapshot() {
3656
3666
  return this.#queue.snapshot();
3657
3667
  }
3668
+ /** True when start() admitted a persisted /loop or /goal continuation. */
3669
+ get rotationContinuationResumed() {
3670
+ return this.#continuationResumeStarted;
3671
+ }
3658
3672
  clearQueue() {
3659
3673
  const snapshot4 = this.#queue.clear();
3660
3674
  for (const kind of ["steer", "followUp"]) {
@@ -3804,6 +3818,7 @@ var FlavorSession = class {
3804
3818
  if (decision.additionalContext !== void 0) await this.#services.addContext?.(decision.additionalContext);
3805
3819
  this.#started = true;
3806
3820
  this.#resumeDurableQueue();
3821
+ this.#resumeRotationContinuation();
3807
3822
  });
3808
3823
  return this.#startPromise;
3809
3824
  }
@@ -3886,7 +3901,8 @@ var FlavorSession = class {
3886
3901
  skillNames,
3887
3902
  this.#services.managedToolCommands().map(({ name }) => name)
3888
3903
  );
3889
- if (command !== null) await this.#dispatch(command, controller.signal);
3904
+ if (submission.continuation !== void 0) await this.#runContinuation(submission.continuation, controller.signal);
3905
+ else if (command !== null) await this.#dispatch(command, controller.signal);
3890
3906
  else for await (const event of this.#services.run(prompt, controller.signal, {
3891
3907
  getSteeringMessages: () => this.#drainMessages("steer", true).map((item) => item.text),
3892
3908
  ...decision.additionalContext === void 0 ? {} : { additionalContext: decision.additionalContext },
@@ -4144,6 +4160,33 @@ Address this now (for example run the required verification or fix), then finish
4144
4160
  });
4145
4161
  }
4146
4162
  }
4163
+ /**
4164
+ * After a heap rotation the relaunched process inherits the same session and
4165
+ * a marker continuation naming the /loop or /goal that was in flight. Resume
4166
+ * it once here so a multi-day autonomous run survives the rotation instead of
4167
+ * silently stopping at the boundary.
4168
+ */
4169
+ #resumeRotationContinuation() {
4170
+ if (this.#continuationResumeStarted || this.#closed) return;
4171
+ const continuation = this.#services.continuation?.();
4172
+ if (continuation === void 0) return;
4173
+ const supported = continuation.kind === "loop" ? this.#services.resumeLoop !== void 0 : this.#services.resumeGoal !== void 0;
4174
+ if (!supported) return;
4175
+ this.#continuationResumeStarted = true;
4176
+ const displayText = `Resuming the interrupted ${continuation.kind} ${continuation.id} after a heap rotation.`;
4177
+ this.#notice(displayText);
4178
+ this.#enqueueNormalizedSubmission({ text: displayText, displayText, continuation }).catch((error) => {
4179
+ this.#services.output({ type: "error", error: { code: "unknown", message: message(error) } });
4180
+ });
4181
+ }
4182
+ async #runContinuation(continuation, signal) {
4183
+ const stream = continuation.kind === "loop" ? this.#services.resumeLoop?.(continuation.id, signal) : this.#services.resumeGoal?.(continuation.id, signal);
4184
+ if (stream === void 0) {
4185
+ this.#notice(`The runtime cannot resume the interrupted ${continuation.kind} ${continuation.id}; start it again with /${continuation.kind}.`);
4186
+ return;
4187
+ }
4188
+ for await (const event of stream) this.#services.output(event);
4189
+ }
4147
4190
  #ackDurable(submission) {
4148
4191
  if (submission.durableId === void 0) return;
4149
4192
  this.#services.durableQueue?.ack(submission.durableId);
@@ -5973,8 +6016,8 @@ var PalBrokerServer = class {
5973
6016
  });
5974
6017
  if (this.endpointLock === void 0) throw new Error(`Pals endpoint '${this.address}' is active or owned`);
5975
6018
  try {
5976
- const stat11 = await lstat4(this.address);
5977
- if (!stat11.isSocket()) throw new Error(`Refusing to unlink '${this.address}' because it is not a socket`);
6019
+ const stat12 = await lstat4(this.address);
6020
+ if (!stat12.isSocket()) throw new Error(`Refusing to unlink '${this.address}' because it is not a socket`);
5978
6021
  await unlink3(this.address);
5979
6022
  } catch (error) {
5980
6023
  if (error.code !== "ENOENT") throw error;
@@ -5987,8 +6030,8 @@ var PalBrokerServer = class {
5987
6030
  }
5988
6031
  async unlinkKnownSocket() {
5989
6032
  try {
5990
- const stat11 = await lstat4(this.address);
5991
- if (stat11.isSocket()) await unlink3(this.address);
6033
+ const stat12 = await lstat4(this.address);
6034
+ if (stat12.isSocket()) await unlink3(this.address);
5992
6035
  } catch (error) {
5993
6036
  if (error.code !== "ENOENT") throw error;
5994
6037
  }
@@ -6715,11 +6758,11 @@ async function ensureOnce(options) {
6715
6758
  }
6716
6759
 
6717
6760
  // src/production.ts
6718
- import { readFile as readFile23 } from "fs/promises";
6719
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
6761
+ import { readFile as readFile24 } from "fs/promises";
6762
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync3, renameSync as renameSync2, unlinkSync as unlinkSync3, writeFileSync as writeFileSync3 } from "fs";
6720
6763
  import { createHash as createHash18, randomUUID as randomUUID20 } from "crypto";
6721
6764
  import { homedir as homedir3, release as osRelease, tmpdir as tmpdir4, version as osVersion } from "os";
6722
- import { basename as basename7, dirname as dirname17, join as join29, relative as relative22, resolve as resolve31, sep as sep22 } from "path";
6765
+ import { basename as basename7, dirname as dirname17, join as join30, relative as relative22, resolve as resolve31, sep as sep22 } from "path";
6723
6766
 
6724
6767
  // src/agent/subagents.ts
6725
6768
  import { z as z9 } from "zod";
@@ -7677,6 +7720,18 @@ var ContextManager = class _ContextManager {
7677
7720
  estimatedTokens() {
7678
7721
  return estimateMessageTokens(this.messagesForModel());
7679
7722
  }
7723
+ /** Retained sizes for the rotation census: what this context holds right now. */
7724
+ census() {
7725
+ const chars = (messages) => messages.reduce((sum, entry2) => sum + modelContentText(entry2.content).length, 0);
7726
+ return {
7727
+ messages: this.#messages.length,
7728
+ messageChars: chars(this.#messages),
7729
+ visibilityEntries: this.#visibilityLog.length,
7730
+ visibilityChars: this.#visibilityLog.reduce((sum, record) => sum + record.content.length, 0),
7731
+ transientEntries: this.#activeTransientSystem.size,
7732
+ transientChars: chars([...this.#activeTransientSystem.values()])
7733
+ };
7734
+ }
7680
7735
  needsCompaction() {
7681
7736
  if (modelVisibleText(this.messagesForModel()).length >= this.#compactAtChars) return true;
7682
7737
  return calculateContextPressure(this.#currentTokenUsage(), this.#compaction).shouldAutoCompact;
@@ -8245,8 +8300,75 @@ function reportProgress(callback, percentage) {
8245
8300
  }
8246
8301
  }
8247
8302
 
8248
- // src/agent/loop.ts
8303
+ // src/utils/heap.ts
8249
8304
  import { getHeapStatistics } from "v8";
8305
+ import { totalmem } from "os";
8306
+ var MEMORY_PRESSURE_HEAP_RATIO = 0.8;
8307
+ var MEMORY_ROTATION_HEAP_RATIO = 0.6;
8308
+ var MEMORY_PRESSURE_RSS_RATIO = 0.75;
8309
+ var MEMORY_ROTATION_RSS_RATIO = 0.6;
8310
+ var ROTATION_PRECHECK_RATIO = 0.45;
8311
+ function readHeap() {
8312
+ const heapLimit = getHeapStatistics().heap_size_limit;
8313
+ const heapUsed = process.memoryUsage().heapUsed;
8314
+ return { heapUsed, heapLimit, ratio: heapLimit > 0 ? heapUsed / heapLimit : 0 };
8315
+ }
8316
+ function forceFullGc() {
8317
+ const gc = globalThis.gc;
8318
+ if (typeof gc !== "function") return false;
8319
+ gc();
8320
+ gc();
8321
+ return true;
8322
+ }
8323
+ function ratioOf(heapLimit, heapUsed) {
8324
+ return heapLimit > 0 ? heapUsed / heapLimit : 0;
8325
+ }
8326
+ function effectiveMemoryLimit() {
8327
+ const physical = totalmem();
8328
+ const constrained = process.constrainedMemory?.() ?? 0;
8329
+ return constrained > 0 ? Math.min(physical, constrained) : physical;
8330
+ }
8331
+ function withRss(reading, gcVerified) {
8332
+ const usage = process.memoryUsage();
8333
+ const memoryLimit = effectiveMemoryLimit();
8334
+ return {
8335
+ ...reading,
8336
+ gcVerified,
8337
+ rss: usage.rss,
8338
+ memoryLimit,
8339
+ rssRatio: ratioOf(memoryLimit, usage.rss),
8340
+ external: usage.external,
8341
+ arrayBuffers: usage.arrayBuffers
8342
+ };
8343
+ }
8344
+ function verifiedHeapPressure(threshold) {
8345
+ const raw = readHeap();
8346
+ const before = withRss(raw, false);
8347
+ const rssThreshold = threshold >= MEMORY_PRESSURE_HEAP_RATIO ? MEMORY_PRESSURE_RSS_RATIO : threshold;
8348
+ if ((raw.heapLimit <= 0 || raw.ratio < threshold) && (before.rssRatio ?? 0) < rssThreshold) return void 0;
8349
+ if (!forceFullGc()) return before;
8350
+ const heapUsed = process.memoryUsage().heapUsed;
8351
+ const ratio = ratioOf(raw.heapLimit, heapUsed);
8352
+ const verified = withRss({ heapUsed, heapLimit: raw.heapLimit, ratio, verifiedRatio: ratio }, true);
8353
+ if (ratio < threshold && (verified.rssRatio ?? 0) < rssThreshold) return void 0;
8354
+ return verified;
8355
+ }
8356
+ function heapRotationNeeded() {
8357
+ const raw = readHeap();
8358
+ const before = withRss(raw, false);
8359
+ if ((raw.heapLimit <= 0 || raw.ratio < ROTATION_PRECHECK_RATIO) && (before.rssRatio ?? 0) < ROTATION_PRECHECK_RATIO) return void 0;
8360
+ if (!forceFullGc()) {
8361
+ return raw.ratio >= MEMORY_PRESSURE_HEAP_RATIO || (before.rssRatio ?? 0) >= MEMORY_PRESSURE_RSS_RATIO ? before : void 0;
8362
+ }
8363
+ const heapUsed = process.memoryUsage().heapUsed;
8364
+ const ratio = ratioOf(raw.heapLimit, heapUsed);
8365
+ const verified = withRss({ heapUsed, heapLimit: raw.heapLimit, ratio, verifiedRatio: ratio }, true);
8366
+ if (ratio < MEMORY_ROTATION_HEAP_RATIO && (verified.rssRatio ?? 0) < MEMORY_ROTATION_RSS_RATIO) return void 0;
8367
+ return verified;
8368
+ }
8369
+ function heapSnapshotWarranted(_reading) {
8370
+ return process.env.FLAVOR_HEAP_SNAPSHOT_ON_ROTATE === "1";
8371
+ }
8250
8372
 
8251
8373
  // src/models/structured.ts
8252
8374
  import { z as z11 } from "zod";
@@ -8665,7 +8787,6 @@ function envMaxIterations() {
8665
8787
  if (!Number.isInteger(parsed) || parsed <= 0) return void 0;
8666
8788
  return parsed;
8667
8789
  }
8668
- var MEMORY_PRESSURE_HEAP_RATIO = 0.8;
8669
8790
  function memoryPressureError(heapUsed, heapLimit) {
8670
8791
  if (heapLimit <= 0 || heapUsed < heapLimit * MEMORY_PRESSURE_HEAP_RATIO) return void 0;
8671
8792
  const mb = (bytes) => Math.round(bytes / 1048576);
@@ -8675,7 +8796,12 @@ function memoryPressureError(heapUsed, heapLimit) {
8675
8796
  };
8676
8797
  }
8677
8798
  function currentMemoryPressure() {
8678
- return memoryPressureError(process.memoryUsage().heapUsed, getHeapStatistics().heap_size_limit);
8799
+ const reading = verifiedHeapPressure(MEMORY_PRESSURE_HEAP_RATIO);
8800
+ if (reading === void 0) return void 0;
8801
+ return memoryPressureError(reading.heapUsed, reading.heapLimit) ?? {
8802
+ code: "memory_pressure",
8803
+ message: `Process RSS reached ${Math.round((reading.rssRatio ?? 0) * 100)}% of the available memory limit; stopping before system OOM`
8804
+ };
8679
8805
  }
8680
8806
  var AgentLoop = class {
8681
8807
  #options;
@@ -10053,6 +10179,10 @@ var HarnessJournal = class {
10053
10179
  get records() {
10054
10180
  return this.#records.map((record) => structuredClone(record));
10055
10181
  }
10182
+ /** Retained record count for the rotation census. */
10183
+ get recordCount() {
10184
+ return this.#records.length;
10185
+ }
10056
10186
  admitQueue(kind, payload) {
10057
10187
  QueueKindSchema.parse(kind);
10058
10188
  const id = randomUUID8();
@@ -10329,19 +10459,38 @@ function jsonSafeRecord(value) {
10329
10459
  return JSON.parse(json);
10330
10460
  }
10331
10461
  function hashJson(value) {
10332
- return createHash6("sha256").update(stableJson(value)).digest("hex");
10333
- }
10334
- function recordHash(record) {
10335
- const { hash: _hash, ...unsigned } = record;
10336
- return hashJson(unsigned);
10462
+ const hash = createHash6("sha256");
10463
+ stableHashInto(hash, value);
10464
+ return hash.digest("hex");
10337
10465
  }
10338
- function stableJson(value) {
10339
- if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
10466
+ function stableHashInto(hash, value) {
10467
+ if (Array.isArray(value)) {
10468
+ hash.update("[");
10469
+ for (let index = 0; index < value.length; index += 1) {
10470
+ if (index > 0) hash.update(",");
10471
+ stableHashInto(hash, value[index]);
10472
+ }
10473
+ hash.update("]");
10474
+ return;
10475
+ }
10340
10476
  if (typeof value === "object" && value !== null) {
10341
10477
  const object = value;
10342
- return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${stableJson(object[key])}`).join(",")}}`;
10478
+ hash.update("{");
10479
+ const keys = Object.keys(object).sort();
10480
+ for (let index = 0; index < keys.length; index += 1) {
10481
+ const key = keys[index];
10482
+ if (index > 0) hash.update(",");
10483
+ hash.update(`${JSON.stringify(key)}:`);
10484
+ stableHashInto(hash, object[key]);
10485
+ }
10486
+ hash.update("}");
10487
+ return;
10343
10488
  }
10344
- return JSON.stringify(value) ?? String(value);
10489
+ hash.update(JSON.stringify(value) ?? String(value));
10490
+ }
10491
+ function recordHash(record) {
10492
+ const { hash: _hash, ...unsigned } = record;
10493
+ return hashJson(unsigned);
10345
10494
  }
10346
10495
  function assertSessionId(value) {
10347
10496
  if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(value)) throw new Error("Invalid session id");
@@ -12457,6 +12606,14 @@ var LoopStateSchema = z17.object({
12457
12606
  approvals: z17.array(LoopApprovalSchema)
12458
12607
  }).strict(),
12459
12608
  cycles: z17.array(LoopCycleEvidenceSchema),
12609
+ /** Completed worker awaiting host verification; avoids replaying tool work after rotation. */
12610
+ pendingCycle: z17.object({
12611
+ cycle: z17.number().int().positive(),
12612
+ startedAt: z17.string().datetime({ offset: true }),
12613
+ inputTokens: z17.number().int().nonnegative(),
12614
+ outputTokens: z17.number().int().nonnegative(),
12615
+ workerText: z17.string().max(16e3)
12616
+ }).strict().nullable().optional(),
12460
12617
  terminalReason: z17.string().optional()
12461
12618
  }).strict();
12462
12619
  var LoopEventSchema = z17.object({
@@ -12545,15 +12702,45 @@ var LoopOrchestrator = class {
12545
12702
  tokenCheckpoint: this.#options.config.maxTokens,
12546
12703
  approvals: []
12547
12704
  },
12548
- cycles: []
12705
+ cycles: [],
12706
+ pendingCycle: null
12549
12707
  });
12550
12708
  await this.#options.persistence.create(state);
12709
+ yield* this.#execute(state, request.goal, request.signal);
12710
+ }
12711
+ /**
12712
+ * Continue a loop whose process rotated onto a fresh heap. The persisted
12713
+ * state decides where to pick up; an interrupted cycle simply runs again.
12714
+ */
12715
+ async *resume(request) {
12716
+ const persistence = this.#options.persistence;
12717
+ if (persistence.load === void 0) throw new Error("Loop persistence does not support resume");
12718
+ let state;
12551
12719
  try {
12720
+ state = await persistence.load(request.loopId);
12721
+ } catch (error) {
12722
+ yield { type: "loop-terminal", loopId: request.loopId, status: "failed", reason: message(error) };
12723
+ return;
12724
+ }
12725
+ if (state.status !== "running") {
12726
+ const status = state.status;
12727
+ yield { type: "loop-terminal", loopId: state.loopId, status, reason: state.terminalReason ?? "The loop is not running." };
12728
+ return;
12729
+ }
12730
+ yield* this.#execute(state, state.goal, request.signal);
12731
+ }
12732
+ async *#execute(initialState, goal, signal) {
12733
+ const now = this.#options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
12734
+ const loopId = initialState.loopId;
12735
+ let state = initialState;
12736
+ try {
12737
+ await this.#options.onMemoryCheckpoint?.(loopId);
12738
+ if (memoryRotationActive()) return;
12552
12739
  const workspaceResolution = await this.#options.prepareWorkspace({
12553
12740
  root: this.#options.workspace,
12554
12741
  loopId,
12555
- goal: request.goal,
12556
- signal: request.signal
12742
+ goal,
12743
+ signal
12557
12744
  });
12558
12745
  if (workspaceResolution.kind === "needs_human") {
12559
12746
  state = await this.#terminal(state, "needs_human", workspaceResolution.reason, now());
@@ -12571,67 +12758,87 @@ var LoopOrchestrator = class {
12571
12758
  cycleCheckpoint: state.budget.cycleCheckpoint,
12572
12759
  tokenCheckpoint: state.budget.tokenCheckpoint
12573
12760
  };
12574
- let previousVerification = plan.commands.length === 0 ? {
12761
+ const lastCompletedCycle = state.cycles.at(-1);
12762
+ let previousVerification = lastCompletedCycle?.verification ?? (plan.commands.length === 0 ? {
12575
12763
  passed: false,
12576
12764
  commands: [],
12577
12765
  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.`
12578
- } : void 0;
12579
- let previousFailureSignature;
12580
- let repeatedFailures = 0;
12766
+ } : void 0);
12767
+ let previousFailureSignature = lastCompletedCycle === void 0 ? void 0 : `${lastCompletedCycle.workspaceFingerprint}
12768
+ ${lastCompletedCycle.verification.summary}`;
12769
+ let repeatedFailures = previousFailureSignature === void 0 ? 0 : countTrailingFailureSignature(state.cycles, previousFailureSignature);
12581
12770
  while (state.status === "running") {
12582
- request.signal.throwIfAborted();
12583
- const cycle = state.budget.cyclesUsed + 1;
12584
- const startedAt = now();
12585
- await this.#options.persistence.append({ version: 1, type: "cycle_started", timestamp: startedAt, loopId, payload: { cycle } });
12586
- yield { type: "loop-cycle-start", loopId, cycle };
12587
- const prompt = buildLoopCyclePrompt({
12588
- goal: request.goal,
12589
- cycle,
12590
- ...previousVerification === void 0 ? {} : {
12591
- memory: `Previous host verification: ${previousVerification.summary}`,
12592
- verification: previousVerification
12593
- }
12594
- });
12595
- let cycleInputTokens = 0;
12596
- let cycleOutputTokens = 0;
12597
- let workerError;
12598
- let workerText = "";
12599
- for await (const event of this.#options.runWorker({
12600
- goal: request.goal,
12601
- cycle,
12602
- workspace: executionWorkspace.root,
12603
- prompt,
12604
- signal: request.signal
12605
- })) {
12606
- if (event.type === "usage") {
12607
- cycleInputTokens += event.inputTokens;
12608
- cycleOutputTokens += event.outputTokens;
12609
- }
12610
- if (event.type === "error") workerError = event.error.message;
12611
- if (event.type === "text") workerText += event.text;
12612
- if (this.#options.hallucinationGuard !== void 0) {
12613
- if (event.type === "tool-start") {
12614
- this.#options.hallucinationGuard.recordToolCall(event.name, event.input, event.id);
12615
- } else if (event.type === "tool-end") {
12616
- this.#options.hallucinationGuard.recordToolResult(event.name, event.result, event.id);
12771
+ signal.throwIfAborted();
12772
+ await this.#options.onCycleBoundary?.(loopId);
12773
+ if (memoryRotationActive()) return;
12774
+ const pendingCycle = state.pendingCycle;
12775
+ const cycle = pendingCycle?.cycle ?? state.budget.cyclesUsed + 1;
12776
+ const startedAt = pendingCycle?.startedAt ?? now();
12777
+ let cycleInputTokens = pendingCycle?.inputTokens ?? 0;
12778
+ let cycleOutputTokens = pendingCycle?.outputTokens ?? 0;
12779
+ let workerText = pendingCycle?.workerText ?? "";
12780
+ if (pendingCycle === void 0 || pendingCycle === null) {
12781
+ await this.#options.persistence.append({ version: 1, type: "cycle_started", timestamp: startedAt, loopId, payload: { cycle } });
12782
+ yield { type: "loop-cycle-start", loopId, cycle };
12783
+ const prompt = buildLoopCyclePrompt({
12784
+ goal,
12785
+ cycle,
12786
+ ...previousVerification === void 0 ? {} : {
12787
+ memory: `Previous host verification: ${previousVerification.summary}`,
12788
+ verification: previousVerification
12789
+ }
12790
+ });
12791
+ let workerError;
12792
+ let workerMemoryPressure = false;
12793
+ for await (const event of this.#options.runWorker({
12794
+ goal,
12795
+ cycle,
12796
+ workspace: executionWorkspace.root,
12797
+ prompt,
12798
+ signal
12799
+ })) {
12800
+ if (event.type === "usage") {
12801
+ cycleInputTokens += event.inputTokens;
12802
+ cycleOutputTokens += event.outputTokens;
12803
+ }
12804
+ if (event.type === "error") {
12805
+ workerError = event.error.message;
12806
+ if (event.error.code === "memory_pressure") workerMemoryPressure = true;
12807
+ }
12808
+ if (event.type === "text") workerText = `${workerText}${event.text}`.slice(-16e3);
12809
+ if (this.#options.hallucinationGuard !== void 0) {
12810
+ if (event.type === "tool-start") {
12811
+ this.#options.hallucinationGuard.recordToolCall(event.name, event.input, event.id);
12812
+ } else if (event.type === "tool-end") {
12813
+ this.#options.hallucinationGuard.recordToolResult(event.name, event.result, event.id);
12814
+ }
12617
12815
  }
12816
+ yield { type: "worker-event", event };
12817
+ }
12818
+ if (workerMemoryPressure) return;
12819
+ if (workerError !== void 0) {
12820
+ state = LoopStateSchema.parse({
12821
+ ...state,
12822
+ budget: {
12823
+ ...state.budget,
12824
+ cyclesUsed: cycle,
12825
+ inputTokens: state.budget.inputTokens + cycleInputTokens,
12826
+ outputTokens: state.budget.outputTokens + cycleOutputTokens
12827
+ }
12828
+ });
12829
+ state = await this.#terminal(state, "failed", workerError, now());
12830
+ yield { type: "loop-terminal", loopId, status: "failed", reason: workerError };
12831
+ return;
12618
12832
  }
12619
- yield { type: "worker-event", event };
12620
- }
12621
- if (workerError !== void 0) {
12622
12833
  state = LoopStateSchema.parse({
12623
12834
  ...state,
12624
- budget: {
12625
- ...state.budget,
12626
- cyclesUsed: cycle,
12627
- inputTokens: state.budget.inputTokens + cycleInputTokens,
12628
- outputTokens: state.budget.outputTokens + cycleOutputTokens
12629
- }
12835
+ pendingCycle: { cycle, startedAt, inputTokens: cycleInputTokens, outputTokens: cycleOutputTokens, workerText },
12836
+ updatedAt: now()
12630
12837
  });
12631
- state = await this.#terminal(state, "failed", workerError, now());
12632
- yield { type: "loop-terminal", loopId, status: "failed", reason: workerError };
12633
- return;
12838
+ await this.#options.persistence.save(state);
12634
12839
  }
12840
+ await this.#options.onMemoryCheckpoint?.(loopId);
12841
+ if (memoryRotationActive()) return;
12635
12842
  if (plan.commands.length === 0) {
12636
12843
  plan = await this.#options.inferVerification(executionWorkspace.root);
12637
12844
  if (plan.commands.length > 0) {
@@ -12651,8 +12858,28 @@ var LoopOrchestrator = class {
12651
12858
  passed: false,
12652
12859
  commands: [],
12653
12860
  summary: plan.needsHumanReason ?? "No deterministic verification command was found after the discovery cycle."
12654
- } : await this.#options.runVerifier(plan, executionWorkspace.root, request.signal);
12861
+ } : await this.#options.runVerifier(plan, executionWorkspace.root, signal);
12862
+ await this.#options.onMemoryCheckpoint?.(loopId);
12863
+ if (memoryRotationActive()) return;
12655
12864
  const fingerprint2 = await this.#options.fingerprint(executionWorkspace.root);
12865
+ let guardWarnings = [];
12866
+ if (evidence.passed && this.#options.hallucinationGuard !== void 0) {
12867
+ try {
12868
+ await this.#options.onMemoryCheckpoint?.(loopId);
12869
+ if (memoryRotationActive()) return;
12870
+ const report = await this.#options.hallucinationGuard.evaluate(goal, workerText);
12871
+ await this.#options.onMemoryCheckpoint?.(loopId);
12872
+ if (memoryRotationActive()) return;
12873
+ if (!report.passed) {
12874
+ const guardReason = report.blockingReasons.join("; ") || "Hallucination guard blocked completion.";
12875
+ state = await this.#terminal(state, "failed", guardReason, now());
12876
+ yield { type: "loop-terminal", loopId, status: "failed", reason: guardReason };
12877
+ return;
12878
+ }
12879
+ guardWarnings = report.warnings;
12880
+ } catch {
12881
+ }
12882
+ }
12656
12883
  const completedAt = now();
12657
12884
  state = LoopStateSchema.parse({
12658
12885
  ...state,
@@ -12671,7 +12898,8 @@ var LoopOrchestrator = class {
12671
12898
  outputTokens: cycleOutputTokens,
12672
12899
  workspaceFingerprint: fingerprint2,
12673
12900
  verification: evidence
12674
- }]
12901
+ }],
12902
+ pendingCycle: null
12675
12903
  });
12676
12904
  await this.#options.persistence.save(state);
12677
12905
  await this.#options.persistence.append({
@@ -12689,20 +12917,6 @@ var LoopOrchestrator = class {
12689
12917
  return;
12690
12918
  }
12691
12919
  if (evidence.passed) {
12692
- let guardWarnings = [];
12693
- if (this.#options.hallucinationGuard !== void 0) {
12694
- try {
12695
- const report = await this.#options.hallucinationGuard.evaluate(request.goal, workerText);
12696
- if (!report.passed) {
12697
- const guardReason = report.blockingReasons.join("; ") || "Hallucination guard blocked completion.";
12698
- state = await this.#terminal(state, "failed", guardReason, now());
12699
- yield { type: "loop-terminal", loopId, status: "failed", reason: guardReason };
12700
- return;
12701
- }
12702
- guardWarnings = report.warnings;
12703
- } catch {
12704
- }
12705
- }
12706
12920
  const reason = [evidence.summary, ...guardWarnings].filter(Boolean).join("; ");
12707
12921
  state = await this.#terminal(state, "succeeded", reason, now());
12708
12922
  yield { type: "loop-terminal", loopId, status: "succeeded", reason };
@@ -12728,7 +12942,7 @@ ${evidence.summary}`;
12728
12942
  cycleCheckpoint: state.budget.cycleCheckpoint,
12729
12943
  tokenCheckpoint: state.budget.tokenCheckpoint
12730
12944
  };
12731
- const confirmation = await this.#options.confirmBudget(state, decision.dimensions, request.signal);
12945
+ const confirmation = await this.#options.confirmBudget(state, decision.dimensions, signal);
12732
12946
  if (confirmation === "unavailable") {
12733
12947
  const reason = "Loop budget extension requires user confirmation.";
12734
12948
  state = await this.#terminal(state, "needs_human", reason, now());
@@ -12755,8 +12969,9 @@ ${evidence.summary}`;
12755
12969
  }
12756
12970
  }
12757
12971
  } catch (error) {
12758
- const status = request.signal.aborted ? "cancelled" : "failed";
12759
- const reason = request.signal.aborted ? "Loop cancelled by user." : message(error);
12972
+ if (memoryRotationActive()) return;
12973
+ const status = signal.aborted ? "cancelled" : "failed";
12974
+ const reason = signal.aborted ? "Loop cancelled by user." : message(error);
12760
12975
  state = await this.#terminal(state, status, reason, now());
12761
12976
  yield { type: "loop-terminal", loopId, status, reason };
12762
12977
  }
@@ -12774,6 +12989,16 @@ ${evidence.summary}`;
12774
12989
  return state;
12775
12990
  }
12776
12991
  };
12992
+ function countTrailingFailureSignature(cycles, signature) {
12993
+ let count = 0;
12994
+ for (let index = cycles.length - 1; index >= 0; index -= 1) {
12995
+ const cycle = cycles[index];
12996
+ if (`${cycle.workspaceFingerprint}
12997
+ ${cycle.verification.summary}` !== signature) break;
12998
+ count += 1;
12999
+ }
13000
+ return count;
13001
+ }
12777
13002
 
12778
13003
  // src/goal/orchestrator.ts
12779
13004
  import { createHash as createHash11 } from "crypto";
@@ -12841,6 +13066,11 @@ var GoalStateSchema = z18.object({
12841
13066
  planPath: z18.string().nullable(),
12842
13067
  verifyRounds: z18.number().int().min(0),
12843
13068
  workerRounds: z18.number().int().min(0),
13069
+ /** Worker output checkpoint used to resume verification without rerunning tools. */
13070
+ pendingVerification: z18.object({
13071
+ round: z18.number().int().positive(),
13072
+ finalResponse: z18.string().max(16e3)
13073
+ }).strict().nullable().optional(),
12844
13074
  lastGaps: z18.array(GapSchema),
12845
13075
  gapFingerprint: z18.string(),
12846
13076
  stallStreak: z18.number().int().min(0),
@@ -12886,6 +13116,7 @@ ${objective}`
12886
13116
  ].join("\n");
12887
13117
  }
12888
13118
  async function runPlanner(options) {
13119
+ await options.onProgress?.();
12889
13120
  const { adapter, model } = options.registry.get(options.modelId);
12890
13121
  const prompt = buildPlannerPrompt(options.objective);
12891
13122
  const messages = [{ role: "user", content: prompt }];
@@ -12897,6 +13128,7 @@ async function runPlanner(options) {
12897
13128
  tools: [],
12898
13129
  ...options.signal === void 0 ? {} : { signal: options.signal }
12899
13130
  })) {
13131
+ await options.onProgress?.();
12900
13132
  if (event.type === "text") text += event.text;
12901
13133
  else if (event.type === "error") throw event.error;
12902
13134
  else if (event.type === "done") {
@@ -13003,7 +13235,7 @@ async function runClassifier(evidence, plan, options) {
13003
13235
  const skepticResults = await Promise.all(
13004
13236
  Array.from(
13005
13237
  { length: n },
13006
- (_, i) => runOneSkeptic(evidence, plan, i + 1, adapter, model, options.signal)
13238
+ (_, i) => runOneSkeptic(evidence, plan, i + 1, adapter, model, options.signal, options.onProgress)
13007
13239
  )
13008
13240
  );
13009
13241
  const verdicts = [];
@@ -13020,7 +13252,8 @@ async function runClassifier(evidence, plan, options) {
13020
13252
  }
13021
13253
  return aggregate(verdicts);
13022
13254
  }
13023
- async function runOneSkeptic(evidence, plan, index, adapter, model, signal) {
13255
+ async function runOneSkeptic(evidence, plan, index, adapter, model, signal, onProgress) {
13256
+ await onProgress?.();
13024
13257
  const prompt = buildSkepticPrompt(evidence, plan, index);
13025
13258
  const messages = [{ role: "user", content: prompt }];
13026
13259
  let text = "";
@@ -13030,6 +13263,7 @@ async function runOneSkeptic(evidence, plan, index, adapter, model, signal) {
13030
13263
  tools: [],
13031
13264
  ...signal === void 0 ? {} : { signal }
13032
13265
  })) {
13266
+ await onProgress?.();
13033
13267
  if (event.type === "text") text += event.text;
13034
13268
  else if (event.type === "error") throw event.error;
13035
13269
  else if (event.type === "done") break;
@@ -13184,7 +13418,7 @@ var GoalOrchestrator = class {
13184
13418
  async *run(request) {
13185
13419
  const timestamp2 = () => this.#options.now?.() ?? (/* @__PURE__ */ new Date()).toISOString();
13186
13420
  const createdAt = timestamp2();
13187
- let state = {
13421
+ const state = {
13188
13422
  id: this.#options.idFactory?.() ?? `goal-${Date.now().toString(36)}`,
13189
13423
  objective: request.goal,
13190
13424
  phase: "planning",
@@ -13193,6 +13427,7 @@ var GoalOrchestrator = class {
13193
13427
  planPath: null,
13194
13428
  verifyRounds: 0,
13195
13429
  workerRounds: 0,
13430
+ pendingVerification: null,
13196
13431
  lastGaps: [],
13197
13432
  gapFingerprint: "",
13198
13433
  stallStreak: 0,
@@ -13201,87 +13436,147 @@ var GoalOrchestrator = class {
13201
13436
  createdAt,
13202
13437
  updatedAt: createdAt
13203
13438
  };
13439
+ yield* this.#execute(state, request.signal);
13440
+ }
13441
+ /**
13442
+ * Continue a goal whose process rotated onto a fresh heap. The persisted
13443
+ * state decides where to pick up; an interrupted round simply runs again.
13444
+ */
13445
+ async *resume(request) {
13446
+ const persistence = this.#options.persistence;
13447
+ if (persistence === void 0 || persistence.load === void 0) {
13448
+ throw new Error("Goal persistence does not support resume");
13449
+ }
13450
+ let state;
13451
+ try {
13452
+ state = await persistence.load(request.goalId);
13453
+ } catch (error) {
13454
+ yield { type: "goal-failed", reason: message(error) };
13455
+ return;
13456
+ }
13457
+ if (state.status !== "active" && state.status !== "not_achieved") {
13458
+ yield { type: "goal-failed", reason: `Goal "${state.id}" is ${state.status}; it cannot be resumed.` };
13459
+ return;
13460
+ }
13461
+ yield { type: "goal-resumed", goalId: state.id, round: resumeStartRound(state) };
13462
+ yield* this.#execute(state, request.signal);
13463
+ }
13464
+ async *#execute(initialState, signal) {
13465
+ const timestamp2 = () => this.#options.now?.() ?? (/* @__PURE__ */ new Date()).toISOString();
13466
+ let state = initialState;
13204
13467
  const persistState = async (patch) => {
13205
13468
  state = { ...state, ...patch, updatedAt: timestamp2() };
13206
13469
  await this.#options.persistence?.save(state);
13207
13470
  };
13471
+ const memoryCheckpoint = async () => {
13472
+ await this.#options.onMemoryCheckpoint?.(state.id);
13473
+ if (memoryRotationActive()) throw new Error("Heap rotation requested");
13474
+ };
13208
13475
  await persistState();
13209
13476
  const workspace = this.#options.workspace;
13210
13477
  let plan;
13211
13478
  let planPath;
13212
- try {
13213
- request.signal.throwIfAborted();
13214
- plan = await runPlanner({
13215
- registry: this.#options.registry,
13216
- modelId: this.#options.plannerModelId,
13217
- objective: request.goal,
13218
- signal: request.signal
13219
- });
13220
- planPath = await writePlanFile(workspace, plan);
13221
- await persistState({ plan, planPath, contractHash: contractHash(request.goal, plan) });
13222
- yield { type: "goal-plan-created", plan, planPath };
13223
- } catch (error) {
13224
- const reason = `Goal planning failed: ${message(error)}`;
13225
- await persistState({ phase: "complete", status: "failed" });
13226
- yield { type: "goal-plan-failed", reason };
13227
- yield { type: "goal-failed", reason };
13228
- return;
13229
- }
13230
- let priorGaps = [];
13231
- let priorFingerprint = "";
13232
- let stallStreak = 0;
13233
- for (let round = 1; round <= this.#options.maxRounds; round++) {
13234
- request.signal.throwIfAborted();
13235
- const workerPrompt = buildWorkerPrompt(request.goal, plan, priorGaps, round);
13236
- await persistState({
13237
- phase: "executing",
13238
- status: "active",
13239
- workerRounds: round,
13240
- lastGaps: priorGaps,
13241
- gapFingerprint: priorFingerprint,
13242
- stallStreak
13243
- });
13244
- yield { type: "goal-worker-start", round };
13245
- let finalResponse = "";
13246
- let workerError;
13479
+ if (state.plan === null) {
13247
13480
  try {
13248
- for await (const event of this.#options.runWorker({
13249
- goal: request.goal,
13250
- round,
13251
- workspace,
13252
- prompt: workerPrompt,
13253
- priorGaps: formatPriorGaps(priorGaps),
13254
- signal: request.signal
13255
- })) {
13256
- yield { type: "goal-worker-event", round, event };
13257
- if (event.type === "text") finalResponse += event.text;
13258
- if (event.type === "error") {
13259
- workerError = event.error.message;
13260
- break;
13261
- }
13262
- }
13481
+ signal.throwIfAborted();
13482
+ await memoryCheckpoint();
13483
+ plan = await runPlanner({
13484
+ registry: this.#options.registry,
13485
+ modelId: this.#options.plannerModelId,
13486
+ objective: state.objective,
13487
+ signal,
13488
+ onProgress: memoryCheckpoint
13489
+ });
13490
+ planPath = await writePlanFile(workspace, plan);
13491
+ await persistState({ phase: "executing", plan, planPath, contractHash: contractHash(state.objective, plan) });
13492
+ yield { type: "goal-plan-created", plan, planPath };
13263
13493
  } catch (error) {
13264
- workerError = message(error);
13265
- }
13266
- if (workerError !== void 0) {
13267
- const reason = `Worker error in round ${round}: ${workerError}`;
13494
+ if (memoryRotationActive()) return;
13495
+ const reason = `Goal planning failed: ${message(error)}`;
13268
13496
  await persistState({ phase: "complete", status: "failed" });
13497
+ yield { type: "goal-plan-failed", reason };
13269
13498
  yield { type: "goal-failed", reason };
13270
13499
  return;
13271
13500
  }
13272
- await persistState({ phase: "verifying", status: "active", verifyRounds: round });
13501
+ } else {
13502
+ plan = state.plan;
13503
+ planPath = state.planPath ?? await writePlanFile(workspace, plan);
13504
+ await persistState({ phase: "executing", status: "active", ...state.planPath === null ? { planPath } : {} });
13505
+ }
13506
+ let priorGaps = state.lastGaps;
13507
+ let priorFingerprint = state.gapFingerprint;
13508
+ let stallStreak = state.stallStreak;
13509
+ const startRound = resumeStartRound(state);
13510
+ for (let round = startRound; round <= this.#options.maxRounds; round++) {
13511
+ signal.throwIfAborted();
13512
+ await this.#options.onRoundBoundary?.(state.id);
13513
+ if (memoryRotationActive()) return;
13514
+ const pendingVerification = state.pendingVerification?.round === round ? state.pendingVerification : void 0;
13515
+ let finalResponse = pendingVerification?.finalResponse ?? "";
13516
+ if (pendingVerification === void 0) {
13517
+ const workerPrompt = buildWorkerPrompt(state.objective, plan, priorGaps, round);
13518
+ await persistState({
13519
+ phase: "executing",
13520
+ status: "active",
13521
+ workerRounds: round,
13522
+ pendingVerification: null,
13523
+ lastGaps: priorGaps,
13524
+ gapFingerprint: priorFingerprint,
13525
+ stallStreak
13526
+ });
13527
+ yield { type: "goal-worker-start", round };
13528
+ let workerError;
13529
+ let workerMemoryPressure = false;
13530
+ try {
13531
+ for await (const event of this.#options.runWorker({
13532
+ goal: state.objective,
13533
+ round,
13534
+ workspace,
13535
+ prompt: workerPrompt,
13536
+ priorGaps: formatPriorGaps(priorGaps),
13537
+ signal
13538
+ })) {
13539
+ yield { type: "goal-worker-event", round, event };
13540
+ if (event.type === "text") finalResponse = `${finalResponse}${event.text}`.slice(-16e3);
13541
+ if (event.type === "error") {
13542
+ workerError = event.error.message;
13543
+ if (event.error.code === "memory_pressure") workerMemoryPressure = true;
13544
+ break;
13545
+ }
13546
+ }
13547
+ } catch (error) {
13548
+ if (memoryRotationActive()) return;
13549
+ workerError = message(error);
13550
+ }
13551
+ if (workerMemoryPressure) return;
13552
+ if (workerError !== void 0) {
13553
+ const reason = `Worker error in round ${round}: ${workerError}`;
13554
+ await persistState({ phase: "complete", status: "failed" });
13555
+ yield { type: "goal-failed", reason };
13556
+ return;
13557
+ }
13558
+ await persistState({
13559
+ phase: "verifying",
13560
+ status: "active",
13561
+ pendingVerification: { round, finalResponse }
13562
+ });
13563
+ }
13273
13564
  yield { type: "goal-verification-start", round };
13274
- const hostVerification = await this.#options.verifyHost?.(request.signal);
13565
+ await this.#options.onMemoryCheckpoint?.(state.id);
13566
+ if (memoryRotationActive()) return;
13567
+ const hostVerification = await this.#options.verifyHost?.(signal);
13568
+ await this.#options.onMemoryCheckpoint?.(state.id);
13569
+ if (memoryRotationActive()) return;
13275
13570
  const evidence = await collectEvidence(
13276
13571
  workspace,
13277
- request.goal,
13572
+ state.objective,
13278
13573
  finalResponse.slice(-4e3),
13279
13574
  // Last 4000 chars of response
13280
13575
  formatPriorGaps(priorGaps),
13281
13576
  state.contractHash,
13282
13577
  hostVerification
13283
13578
  );
13284
- await persistState({ evidenceRounds: [...state.evidenceRounds, {
13579
+ await persistState({ evidenceRounds: [...state.evidenceRounds.filter((item) => item.round !== round), {
13285
13580
  round,
13286
13581
  workspaceDiffHash: evidence.workspaceDiffHash,
13287
13582
  ...hostVerification === void 0 ? {} : { hostVerification }
@@ -13305,14 +13600,17 @@ var GoalOrchestrator = class {
13305
13600
  modelId: this.#options.classifierModelId,
13306
13601
  skepticCount: this.#options.skepticCount,
13307
13602
  workspace,
13308
- signal: request.signal
13603
+ signal,
13604
+ onProgress: memoryCheckpoint
13309
13605
  });
13310
13606
  } catch (error) {
13607
+ if (memoryRotationActive()) return;
13311
13608
  outcome = {
13312
13609
  type: "blocked",
13313
13610
  reason: `Classifier infrastructure error: ${message(error)}`
13314
13611
  };
13315
13612
  }
13613
+ await persistState({ verifyRounds: round, pendingVerification: null });
13316
13614
  yield {
13317
13615
  type: "goal-verdict",
13318
13616
  round,
@@ -13372,6 +13670,12 @@ var GoalOrchestrator = class {
13372
13670
  function contractHash(objective, plan) {
13373
13671
  return createHash11("sha256").update(JSON.stringify({ objective, plan })).digest("hex");
13374
13672
  }
13673
+ function resumeStartRound(state) {
13674
+ if (state.pendingVerification !== void 0 && state.pendingVerification !== null) {
13675
+ return state.pendingVerification.round;
13676
+ }
13677
+ return state.verifyRounds >= state.workerRounds ? state.workerRounds + 1 : Math.max(state.workerRounds, 1);
13678
+ }
13375
13679
  async function writePlanFile(workspace, plan) {
13376
13680
  const dir = join17(workspace, ".flavor");
13377
13681
  const planPath = join17(dir, "goal-plan.md");
@@ -13451,7 +13755,7 @@ function formatPriorGaps(gaps) {
13451
13755
  // src/goal/store.ts
13452
13756
  import { constants as constants4 } from "fs";
13453
13757
  import { randomUUID as randomUUID11 } from "crypto";
13454
- import { lstat as lstat7, mkdir as mkdir11, open as open6, rename as rename4, rm as rm4 } from "fs/promises";
13758
+ import { lstat as lstat7, mkdir as mkdir11, open as open6, readFile as readFile11, rename as rename4, rm as rm4, stat as stat6 } from "fs/promises";
13455
13759
  import { dirname as dirname10, join as join18, relative as relative11, resolve as resolve14, sep as sep11 } from "path";
13456
13760
  var DEFAULT_MAX_BYTES = 1024 * 1024;
13457
13761
  var GoalStore = class {
@@ -13488,6 +13792,28 @@ var GoalStore = class {
13488
13792
  await rm4(temporary, { force: true }).catch(() => void 0);
13489
13793
  }
13490
13794
  }
13795
+ /** Reload a persisted goal so a rotated process can resume it. */
13796
+ async load(goalId) {
13797
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(goalId)) {
13798
+ throw new Error(`Goal "${goalId}" was not found`);
13799
+ }
13800
+ await assertNoSymlink(this.#workspace, dirname10(this.#root));
13801
+ await assertNoSymlink(this.#workspace, this.#root);
13802
+ const target = join18(this.#root, `${goalId}.json`);
13803
+ try {
13804
+ const metadata = await stat6(target);
13805
+ if (!metadata.isFile() || metadata.size > this.#maxBytes) {
13806
+ throw new Error("Goal state is not a bounded regular file");
13807
+ }
13808
+ const raw = await readFile11(target, "utf8");
13809
+ return GoalStateSchema.parse(JSON.parse(raw));
13810
+ } catch (error) {
13811
+ if (isCode3(error, "ENOENT")) throw new Error(`Goal "${goalId}" was not found`);
13812
+ const quarantine = `${target}.corrupt-${Date.now()}-${randomUUID11().slice(0, 8)}`;
13813
+ await rename4(target, quarantine).catch(() => void 0);
13814
+ throw new Error(`Goal "${goalId}" is corrupt and was quarantined`);
13815
+ }
13816
+ }
13491
13817
  };
13492
13818
  async function assertNoSymlink(root, target) {
13493
13819
  let cursor = resolve14(target);
@@ -13514,7 +13840,7 @@ function isCode3(error, code) {
13514
13840
  }
13515
13841
 
13516
13842
  // src/loop/isolation.ts
13517
- import { appendFile as appendFile2, mkdir as mkdir12, readFile as readFile11 } from "fs/promises";
13843
+ import { appendFile as appendFile2, mkdir as mkdir12, readFile as readFile12 } from "fs/promises";
13518
13844
  import { join as join19, resolve as resolve15 } from "path";
13519
13845
  var CODE_CHANGE = /\b(?:fix|implement|add|update|change|modify|refactor|build|create|remove|delete|write|migrate)\b|修复|实现|添加|更新|修改|重构|创建|删除|迁移/i;
13520
13846
  var READ_ONLY = /\b(?:review|analy[sz]e|inspect|explain|research|investigate|audit|summari[sz]e|report)\b|调研|分析|审查|检查|解释|研究|总结|报告/i;
@@ -13574,7 +13900,7 @@ async function ensureWorktreeDirectoryIgnored(root) {
13574
13900
  await mkdir12(infoDirectory, { recursive: true });
13575
13901
  let existing = "";
13576
13902
  try {
13577
- existing = await readFile11(excludePath, "utf8");
13903
+ existing = await readFile12(excludePath, "utf8");
13578
13904
  } catch (error) {
13579
13905
  if (!(typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT")) throw error;
13580
13906
  }
@@ -13585,7 +13911,7 @@ async function ensureWorktreeDirectoryIgnored(root) {
13585
13911
 
13586
13912
  // src/loop/store.ts
13587
13913
  import { constants as constants5 } from "fs";
13588
- import { lstat as lstat8, mkdir as mkdir13, open as open7, readFile as readFile12, realpath as realpath5, rename as rename5, rm as rm5, stat as stat6 } from "fs/promises";
13914
+ import { lstat as lstat8, mkdir as mkdir13, open as open7, readFile as readFile13, realpath as realpath5, rename as rename5, rm as rm5, stat as stat7 } from "fs/promises";
13589
13915
  import { dirname as dirname11, join as join20, relative as relative12, resolve as resolve16, sep as sep12 } from "path";
13590
13916
  import { randomUUID as randomUUID12 } from "crypto";
13591
13917
  var LOOP_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
@@ -13636,9 +13962,9 @@ var LoopStore = class {
13636
13962
  const directory = await this.#safeExisting(loopId);
13637
13963
  const target = join20(directory, "state.json");
13638
13964
  try {
13639
- const metadata = await stat6(target);
13965
+ const metadata = await stat7(target);
13640
13966
  if (!metadata.isFile() || metadata.size > this.#maxBytes) throw new Error("Loop state is not a bounded regular file");
13641
- const raw = await readFile12(target, "utf8");
13967
+ const raw = await readFile13(target, "utf8");
13642
13968
  const state = LoopStateSchema.parse(JSON.parse(raw));
13643
13969
  await this.#assertWorkspace(state.workspace);
13644
13970
  return state;
@@ -13727,7 +14053,7 @@ function isCode4(error, code) {
13727
14053
  }
13728
14054
 
13729
14055
  // src/loop/verifier.ts
13730
- import { readFile as readFile13 } from "fs/promises";
14056
+ import { readFile as readFile14 } from "fs/promises";
13731
14057
  import { join as join21 } from "path";
13732
14058
  var SCRIPT_PRIORITY = ["test", "typecheck", "lint", "build", "smoke:install"];
13733
14059
  async function inferVerificationPlan(workspace) {
@@ -13806,7 +14132,7 @@ function isUnconditionalPassThrough(script) {
13806
14132
  }
13807
14133
  async function optionalText(path) {
13808
14134
  try {
13809
- return await readFile13(path, "utf8");
14135
+ return await readFile14(path, "utf8");
13810
14136
  } catch (error) {
13811
14137
  if (isCode5(error, "ENOENT")) return void 0;
13812
14138
  throw error;
@@ -13827,7 +14153,7 @@ function isCode5(error, code) {
13827
14153
 
13828
14154
  // src/models/anthropic.ts
13829
14155
  import Anthropic from "@anthropic-ai/sdk";
13830
- import { readFile as readFile14 } from "fs/promises";
14156
+ import { readFile as readFile15 } from "fs/promises";
13831
14157
 
13832
14158
  // src/utils/json.ts
13833
14159
  function safeParseJson(input) {
@@ -14064,7 +14390,7 @@ var AnthropicModelAdapter = class {
14064
14390
  source: {
14065
14391
  type: "base64",
14066
14392
  media_type: block.mediaType,
14067
- data: (await readFile14(block.source.path)).toString("base64")
14393
+ data: (await readFile15(block.source.path)).toString("base64")
14068
14394
  }
14069
14395
  }));
14070
14396
  if (message2.cacheBreakpoint && Array.isArray(content) && content.length > 0) {
@@ -14284,7 +14610,7 @@ function resolveCacheProfile(input) {
14284
14610
 
14285
14611
  // src/models/openai.ts
14286
14612
  import OpenAI from "openai";
14287
- import { readFile as readFile15 } from "fs/promises";
14613
+ import { readFile as readFile16 } from "fs/promises";
14288
14614
  async function toInput(message2) {
14289
14615
  if (message2.role === "tool") {
14290
14616
  if (!message2.toolCallId) throw new Error("Tool messages require toolCallId");
@@ -14296,7 +14622,7 @@ async function toInput(message2) {
14296
14622
  }
14297
14623
  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 } : {
14298
14624
  type: "input_image",
14299
- image_url: `data:${block.mediaType};base64,${(await readFile15(block.source.path)).toString("base64")}`,
14625
+ image_url: `data:${block.mediaType};base64,${(await readFile16(block.source.path)).toString("base64")}`,
14300
14626
  detail: "auto"
14301
14627
  }));
14302
14628
  return [
@@ -15127,7 +15453,7 @@ function isStoredToken(value) {
15127
15453
  }
15128
15454
 
15129
15455
  // src/permissions/policy.ts
15130
- import { readFile as readFile16 } from "fs/promises";
15456
+ import { readFile as readFile17 } from "fs/promises";
15131
15457
  import { join as join22, resolve as resolve18 } from "path";
15132
15458
  import { z as z20 } from "zod";
15133
15459
  var DecisionSchema2 = z20.enum(["allow", "ask", "deny"]);
@@ -15173,7 +15499,7 @@ async function loadPermissionPolicy(options) {
15173
15499
  for (const source of sources) {
15174
15500
  let raw;
15175
15501
  try {
15176
- raw = await readFile16(source.path, "utf8");
15502
+ raw = await readFile17(source.path, "utf8");
15177
15503
  } catch (error) {
15178
15504
  if (isCode6(error, "ENOENT")) continue;
15179
15505
  throw error;
@@ -15939,7 +16265,7 @@ function createSkillResourceTool(registry) {
15939
16265
 
15940
16266
  // src/session/store.ts
15941
16267
  import { constants as constants7 } from "fs";
15942
- import { lstat as lstat10, mkdir as mkdir14, open as open8, readFile as readFile17, readdir as readdir7, realpath as realpath7, rename as rename6, rm as rm6, stat as stat7 } from "fs/promises";
16268
+ import { lstat as lstat10, mkdir as mkdir14, open as open8, readFile as readFile18, readdir as readdir7, realpath as realpath7, rename as rename6, rm as rm6, stat as stat8 } from "fs/promises";
15943
16269
  import { basename as basename4, dirname as dirname13, join as join23, relative as relative14, resolve as resolve20, sep as sep14 } from "path";
15944
16270
  import { randomUUID as randomUUID13 } from "crypto";
15945
16271
  import { z as z22 } from "zod";
@@ -16137,7 +16463,7 @@ var SessionStore = class {
16137
16463
  release: async () => {
16138
16464
  if (released) return;
16139
16465
  try {
16140
- if (await readFile17(path, "utf8") === token) await rm6(path, { force: true });
16466
+ if (await readFile18(path, "utf8") === token) await rm6(path, { force: true });
16141
16467
  } catch (error) {
16142
16468
  if (!isCode7(error, "ENOENT")) throw error;
16143
16469
  }
@@ -16331,14 +16657,14 @@ function storedSessionVersion(raw) {
16331
16657
  }
16332
16658
  async function reclaimStaleSessionLock(path) {
16333
16659
  try {
16334
- const token = await readFile17(path, "utf8");
16335
- const metadata = await stat7(path);
16660
+ const token = await readFile18(path, "utf8");
16661
+ const metadata = await stat8(path);
16336
16662
  const owner2 = parseSessionLockOwner(token);
16337
16663
  if (owner2 !== void 0 && isProcessAlive(owner2)) return owner2;
16338
16664
  if (owner2 === void 0 && Date.now() - metadata.mtimeMs <= INVALID_SESSION_LOCK_STALE_MS) {
16339
16665
  throw new Error(`Session writer lock ${path} is incomplete or invalid; retry after the owning process exits`);
16340
16666
  }
16341
- if (await readFile17(path, "utf8") !== token) return void 0;
16667
+ if (await readFile18(path, "utf8") !== token) return void 0;
16342
16668
  const stale = `${path}.stale-${process.pid}-${randomUUID13()}`;
16343
16669
  try {
16344
16670
  await rename6(path, stale);
@@ -16371,10 +16697,10 @@ function isProcessAlive(pid) {
16371
16697
  }
16372
16698
  }
16373
16699
  async function boundedRead(path, maxBytes) {
16374
- const metadata = await stat7(path);
16700
+ const metadata = await stat8(path);
16375
16701
  if (!metadata.isFile()) throw new Error("Session path is not a regular file");
16376
16702
  if (metadata.size > maxBytes) throw new Error(`Session exceeds maximum read size of ${maxBytes} bytes`);
16377
- const value = await readFile17(path);
16703
+ const value = await readFile18(path);
16378
16704
  if (value.byteLength > maxBytes) throw new Error(`Session exceeds maximum read size of ${maxBytes} bytes`);
16379
16705
  return value.toString("utf8");
16380
16706
  }
@@ -16564,11 +16890,70 @@ function sanitize2(input, seen = /* @__PURE__ */ new WeakSet()) {
16564
16890
  return output;
16565
16891
  }
16566
16892
 
16893
+ // src/utils/rotation-census.ts
16894
+ import { mkdirSync as mkdirSync3, readdirSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
16895
+ import { join as join24 } from "path";
16896
+ import { writeHeapSnapshot } from "v8";
16897
+ var CENSUS_PREFIX = "rotation-census-";
16898
+ var SNAPSHOT_PREFIX = "heap-";
16899
+ var KEEP_CENSUS_FILES = 5;
16900
+ var KEEP_SNAPSHOT_FILES = 2;
16901
+ function censusHeapBlock(reading) {
16902
+ const usage = process.memoryUsage();
16903
+ const mb = (bytes) => Math.round(bytes / 1048576);
16904
+ return {
16905
+ heapUsedMb: mb(reading.heapUsed),
16906
+ heapLimitMb: mb(reading.heapLimit),
16907
+ rawRatio: Number(reading.ratio.toFixed(3)),
16908
+ ...reading.verifiedRatio === void 0 ? {} : { verifiedRatio: Number(reading.verifiedRatio.toFixed(3)) },
16909
+ gcVerified: reading.gcVerified,
16910
+ rssMb: mb(usage.rss),
16911
+ externalMb: mb(usage.external),
16912
+ arrayBuffersMb: mb(usage.arrayBuffers),
16913
+ ...reading.memoryLimit === void 0 ? {} : { memoryLimitMb: mb(reading.memoryLimit) },
16914
+ ...reading.rssRatio === void 0 ? {} : { rssRatio: Number(reading.rssRatio.toFixed(3)) }
16915
+ };
16916
+ }
16917
+ function writeRotationCensus(workspace, census) {
16918
+ try {
16919
+ const directory = join24(workspace, ".flavor", "tmp");
16920
+ mkdirSync3(directory, { recursive: true });
16921
+ const path = join24(directory, `${CENSUS_PREFIX}${Date.now()}.json`);
16922
+ writeFileSync2(path, `${JSON.stringify(census, null, 2)}
16923
+ `, { encoding: "utf8", mode: 384 });
16924
+ prune(directory, CENSUS_PREFIX, ".json", KEEP_CENSUS_FILES);
16925
+ return path;
16926
+ } catch {
16927
+ return void 0;
16928
+ }
16929
+ }
16930
+ function writeRotationHeapSnapshot(workspace) {
16931
+ try {
16932
+ const directory = join24(workspace, ".flavor", "tmp");
16933
+ mkdirSync3(directory, { recursive: true });
16934
+ const path = join24(directory, `${SNAPSHOT_PREFIX}${Date.now()}.heapsnapshot`);
16935
+ writeHeapSnapshot(path);
16936
+ prune(directory, SNAPSHOT_PREFIX, ".heapsnapshot", KEEP_SNAPSHOT_FILES);
16937
+ return path;
16938
+ } catch {
16939
+ return void 0;
16940
+ }
16941
+ }
16942
+ function prune(directory, prefix, suffix, keep) {
16943
+ const stale = readdirSync(directory).filter((name) => name.startsWith(prefix) && name.endsWith(suffix)).sort().slice(0, -keep);
16944
+ for (const name of stale) {
16945
+ try {
16946
+ unlinkSync2(join24(directory, name));
16947
+ } catch {
16948
+ }
16949
+ }
16950
+ }
16951
+
16567
16952
  // src/sleep/organizer.ts
16568
16953
  import { constants as constants8 } from "fs";
16569
16954
  import { randomUUID as randomUUID14 } from "crypto";
16570
16955
  import { lstat as lstat11, mkdir as mkdir15, open as open9, readdir as readdir8, realpath as realpath8, rename as rename7, rm as rm7 } from "fs/promises";
16571
- import { isAbsolute as isAbsolute10, join as join24, relative as relative15, resolve as resolve21, sep as sep15 } from "path";
16956
+ import { isAbsolute as isAbsolute10, join as join25, relative as relative15, resolve as resolve21, sep as sep15 } from "path";
16572
16957
  import { z as z23 } from "zod";
16573
16958
  var DateKeySchema = z23.string().regex(/^\d{4}-\d{2}-\d{2}$/);
16574
16959
  var ReviewSchema = z23.object({
@@ -16623,7 +17008,7 @@ var ProjectSleepOrganizer = class {
16623
17008
  #generate;
16624
17009
  constructor(options) {
16625
17010
  this.#workspace = resolve21(options.workspace);
16626
- this.#sleepDirectory = join24(this.#workspace, ".flavor", "sleep");
17011
+ this.#sleepDirectory = join25(this.#workspace, ".flavor", "sleep");
16627
17012
  this.#sessions = options.sessions ?? new SessionStore({ workspace: this.#workspace });
16628
17013
  this.#generate = options.generate;
16629
17014
  }
@@ -16632,7 +17017,7 @@ var ProjectSleepOrganizer = class {
16632
17017
  signal.throwIfAborted();
16633
17018
  await this.#prepareDirectory();
16634
17019
  if (await this.#hasReport(date)) return { status: "exists", date };
16635
- const lockPath = join24(this.#sleepDirectory, `.${date}.lock`);
17020
+ const lockPath = join25(this.#sleepDirectory, `.${date}.lock`);
16636
17021
  let lock;
16637
17022
  try {
16638
17023
  lock = await open9(lockPath, constants8.O_CREAT | constants8.O_EXCL | constants8.O_WRONLY, 384);
@@ -16667,7 +17052,7 @@ Previous output was invalid. Repair it and return strict JSON only.`;
16667
17052
  }
16668
17053
  if (review === void 0) throw lastError;
16669
17054
  const filename2 = `${date}-${filenameSummary(review.title)}.md`;
16670
- const path = join24(this.#sleepDirectory, filename2);
17055
+ const path = join25(this.#sleepDirectory, filename2);
16671
17056
  const markdown = renderSleepReport(date, review, documents);
16672
17057
  await this.#atomicWrite(path, markdown);
16673
17058
  return { status: "written", date, path, sessionCount: documents.length };
@@ -16709,7 +17094,7 @@ Previous output was invalid. Repair it and return strict JSON only.`;
16709
17094
  });
16710
17095
  }
16711
17096
  async #prepareDirectory() {
16712
- const flavorDirectory = join24(this.#workspace, ".flavor");
17097
+ const flavorDirectory = join25(this.#workspace, ".flavor");
16713
17098
  await assertNotSymlink(flavorDirectory);
16714
17099
  await mkdir15(flavorDirectory, { recursive: true, mode: 448 });
16715
17100
  await assertNotSymlink(this.#sleepDirectory);
@@ -16723,7 +17108,7 @@ Previous output was invalid. Repair it and return strict JSON only.`;
16723
17108
  return (await readdir8(this.#sleepDirectory, { withFileTypes: true })).some((entry2) => entry2.isFile() && entry2.name.startsWith(prefix) && entry2.name.endsWith(".md"));
16724
17109
  }
16725
17110
  async #atomicWrite(path, content) {
16726
- const temporary = join24(this.#sleepDirectory, `.${randomUUID14()}.tmp`);
17111
+ const temporary = join25(this.#sleepDirectory, `.${randomUUID14()}.tmp`);
16727
17112
  let handle;
16728
17113
  try {
16729
17114
  handle = await open9(temporary, constants8.O_CREAT | constants8.O_EXCL | constants8.O_WRONLY, 384);
@@ -17097,7 +17482,7 @@ function computeSessionStats(documents) {
17097
17482
 
17098
17483
  // src/tools/files.ts
17099
17484
  import { constants as constants9 } from "fs";
17100
- import { mkdir as mkdir16, open as open10, readFile as readFile18, realpath as realpath9, rename as rename8, rm as rm8, stat as stat8 } from "fs/promises";
17485
+ import { mkdir as mkdir16, open as open10, readFile as readFile19, realpath as realpath9, rename as rename8, rm as rm8, stat as stat9 } from "fs/promises";
17101
17486
  import { dirname as dirname14, isAbsolute as isAbsolute11, relative as relative16, resolve as resolve22, sep as sep16 } from "path";
17102
17487
  import { randomUUID as randomUUID15 } from "crypto";
17103
17488
  import { z as z24 } from "zod";
@@ -17250,7 +17635,7 @@ function createReadTool(workspace, options = {}) {
17250
17635
  execute: async (input, signal) => {
17251
17636
  abortIfNeeded(signal);
17252
17637
  const path = await guard.existing(input.path);
17253
- const info = await stat8(path);
17638
+ const info = await stat9(path);
17254
17639
  const maxBytes = input.maxBytes ?? MAX_READ_BYTES;
17255
17640
  if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0 || maxBytes > MAX_READ_BYTES) {
17256
17641
  throw new Error(`maxBytes must be a positive integer no greater than ${MAX_READ_BYTES}`);
@@ -17459,7 +17844,7 @@ async function atomicWrite(path, content, signal, expectedVersion) {
17459
17844
  const temporary = `${path}.${randomUUID15()}.tmp`;
17460
17845
  let mode;
17461
17846
  try {
17462
- mode = (await stat8(path)).mode & 511;
17847
+ mode = (await stat9(path)).mode & 511;
17463
17848
  } catch (error) {
17464
17849
  if (!isMissing3(error)) throw error;
17465
17850
  }
@@ -17482,7 +17867,7 @@ async function atomicWrite(path, content, signal, expectedVersion) {
17482
17867
  }
17483
17868
  }
17484
17869
  async function readText(path) {
17485
- const contents = await readFile18(path);
17870
+ const contents = await readFile19(path);
17486
17871
  if (isBinary(contents)) throw new Error("Cannot edit binary file as text");
17487
17872
  return decodeFileText(contents);
17488
17873
  }
@@ -17508,7 +17893,7 @@ async function assertMutationVersion(path, expected, observations) {
17508
17893
  }
17509
17894
  async function fileVersion(path) {
17510
17895
  try {
17511
- return versionOfStat(await stat8(path));
17896
+ return versionOfStat(await stat9(path));
17512
17897
  } catch (error) {
17513
17898
  if (isMissing3(error)) return ABSENT_VERSION;
17514
17899
  throw error;
@@ -17519,7 +17904,7 @@ function versionOfStat(info) {
17519
17904
  }
17520
17905
  async function readOptionalPresentationText(path) {
17521
17906
  try {
17522
- const contents = await readFile18(path);
17907
+ const contents = await readFile19(path);
17523
17908
  return isBinary(contents) ? { exists: true } : { exists: true, text: contents.toString("utf8") };
17524
17909
  } catch (error) {
17525
17910
  return isMissing3(error) ? { exists: false } : { exists: true };
@@ -18171,7 +18556,7 @@ function charset(contentType) {
18171
18556
  // src/tools/lsp.ts
18172
18557
  import { spawn as spawn8 } from "child_process";
18173
18558
  import { existsSync as existsSync5 } from "fs";
18174
- import { access as access2, constants as constants10, readFile as readFile19 } from "fs/promises";
18559
+ import { access as access2, constants as constants10, readFile as readFile20 } from "fs/promises";
18175
18560
  import { basename as basename5, dirname as dirname15, extname as extname2, isAbsolute as isAbsolute12, relative as relative17, resolve as resolve23, sep as sep17 } from "path";
18176
18561
  import { fileURLToPath as fileURLToPath2, pathToFileURL as pathToFileURL2 } from "url";
18177
18562
  import { z as z26 } from "zod";
@@ -18365,7 +18750,7 @@ var LspConnection = class {
18365
18750
  const url = new URL(uri);
18366
18751
  if (url.protocol !== "file:") return;
18367
18752
  const filePath = fileURLToPath2(url);
18368
- const text = await readFile19(filePath, "utf8");
18753
+ const text = await readFile20(filePath, "utf8");
18369
18754
  const current = this.#documents.get(uri);
18370
18755
  if (current === void 0) {
18371
18756
  this.#documents.set(uri, { text, version: 1 });
@@ -18731,19 +19116,19 @@ async function changeSummary(workspace) {
18731
19116
  };
18732
19117
  }
18733
19118
  async function stagedDiff(workspace, maxChars = 96e3) {
18734
- const stat11 = await git2(workspace, ["diff", "--staged", "--stat"]);
19119
+ const stat12 = await git2(workspace, ["diff", "--staged", "--stat"]);
18735
19120
  const diff = await git2(workspace, ["diff", "--staged", "--no-ext-diff", "--no-color"]);
18736
19121
  if (!diff.ok) throw gitFailure(diff, "diff --staged");
18737
19122
  const text = diff.stdout;
18738
19123
  return {
18739
19124
  diff: text.length > maxChars ? `${text.slice(0, maxChars)}
18740
19125
  \u2026[diff truncated]` : text,
18741
- stat: stat11.stdout.trim(),
19126
+ stat: stat12.stdout.trim(),
18742
19127
  truncated: text.length > maxChars
18743
19128
  };
18744
19129
  }
18745
19130
  async function uncommittedDiff(workspace, maxChars = 96e3) {
18746
- const stat11 = await git2(workspace, ["diff", "HEAD", "--stat"]);
19131
+ const stat12 = await git2(workspace, ["diff", "HEAD", "--stat"]);
18747
19132
  const diff = await git2(workspace, ["diff", "HEAD", "--no-ext-diff", "--no-color"]);
18748
19133
  if (!diff.ok && !diff.stderr.includes("unknown revision")) throw gitFailure(diff, "diff HEAD");
18749
19134
  const summary2 = await changeSummary(workspace);
@@ -18751,7 +19136,7 @@ async function uncommittedDiff(workspace, maxChars = 96e3) {
18751
19136
  return {
18752
19137
  diff: text.length > maxChars ? `${text.slice(0, maxChars)}
18753
19138
  \u2026[diff truncated]` : text,
18754
- stat: stat11.stdout.trim(),
19139
+ stat: stat12.stdout.trim(),
18755
19140
  untracked: summary2.untrackedFiles
18756
19141
  };
18757
19142
  }
@@ -19048,17 +19433,17 @@ ${explanation}` : "";
19048
19433
  }
19049
19434
 
19050
19435
  // src/desktop/astgraph-service.ts
19051
- import { stat as stat9 } from "fs/promises";
19436
+ import { stat as stat10 } from "fs/promises";
19052
19437
  import { createRequire as nodeCreateRequire } from "module";
19053
- import { join as join25, resolve as resolve24 } from "path";
19438
+ import { join as join26, resolve as resolve24 } from "path";
19054
19439
  var FIELDS = "id, kind, name, qualified_name, file_path, language, start_line, end_line, signature";
19055
19440
  var DesktopAstGraphService = class {
19056
19441
  #path;
19057
19442
  constructor(workspace) {
19058
- this.#path = join25(resolve24(workspace), ".flavor", "astgraph", "index.db");
19443
+ this.#path = join26(resolve24(workspace), ".flavor", "astgraph", "index.db");
19059
19444
  }
19060
19445
  async status() {
19061
- if ((await stat9(this.#path).catch(() => void 0))?.isFile() !== true) {
19446
+ if ((await stat10(this.#path).catch(() => void 0))?.isFile() !== true) {
19062
19447
  return { available: false, path: this.#path, files: 0, nodes: 0, edges: 0 };
19063
19448
  }
19064
19449
  return this.#read((db) => {
@@ -19338,8 +19723,8 @@ function createTodoWriteTool() {
19338
19723
 
19339
19724
  // src/tools/managed.ts
19340
19725
  import { randomUUID as randomUUID16 } from "crypto";
19341
- import { link as link2, lstat as lstat12, mkdir as mkdir17, readFile as readFile20, readdir as readdir9, unlink as unlink5, writeFile as writeFile10 } from "fs/promises";
19342
- import { dirname as dirname16, join as join26, resolve as resolve26 } from "path";
19726
+ import { link as link2, lstat as lstat12, mkdir as mkdir17, readFile as readFile21, readdir as readdir9, unlink as unlink5, writeFile as writeFile10 } from "fs/promises";
19727
+ import { dirname as dirname16, join as join27, resolve as resolve26 } from "path";
19343
19728
  import { z as z31 } from "zod";
19344
19729
  var MANAGED_TOOL_VERSION = 1;
19345
19730
  var MAX_RECORD_BYTES = 256 * 1024;
@@ -19382,8 +19767,8 @@ var ManagedToolStore = class {
19382
19767
  #diagnostics = [];
19383
19768
  constructor(options) {
19384
19769
  this.#workspace = resolve26(options.workspace);
19385
- this.#projectRoot = join26(this.#workspace, ".flavor", "tools");
19386
- this.#globalRoot = join26(resolve26(options.home), ".flavor-code", "tools");
19770
+ this.#projectRoot = join27(this.#workspace, ".flavor", "tools");
19771
+ this.#globalRoot = join27(resolve26(options.home), ".flavor-code", "tools");
19387
19772
  this.#now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
19388
19773
  }
19389
19774
  get diagnostics() {
@@ -19430,7 +19815,7 @@ var ManagedToolStore = class {
19430
19815
  version: MANAGED_TOOL_VERSION,
19431
19816
  createdAt: this.#now()
19432
19817
  });
19433
- const path = join26(this.directory(input.scope), filename(input.name));
19818
+ const path = join27(this.directory(input.scope), filename(input.name));
19434
19819
  const definition = compileDefinition(record, input.scope, this.#workspace);
19435
19820
  await writeExclusive(path, `${JSON.stringify(record, void 0, 2)}
19436
19821
  `);
@@ -19455,7 +19840,7 @@ var ManagedToolStore = class {
19455
19840
  if (!parsed.success) return [];
19456
19841
  const matches = this.#matching(parsed.data.name).filter((entry2) => parsed.data.scope === void 0 || entry2.scope === parsed.data.scope);
19457
19842
  if (matches.length > 0) return matches.map((entry2) => entry2.path);
19458
- return [join26(this.directory(parsed.data.scope ?? "project"), filename(parsed.data.name))];
19843
+ return [join27(this.directory(parsed.data.scope ?? "project"), filename(parsed.data.name))];
19459
19844
  }
19460
19845
  async #loadDirectory(scope2) {
19461
19846
  const root = this.directory(scope2);
@@ -19468,12 +19853,12 @@ var ManagedToolStore = class {
19468
19853
  return;
19469
19854
  }
19470
19855
  for (const name of names) {
19471
- const path = join26(root, name);
19856
+ const path = join27(root, name);
19472
19857
  try {
19473
19858
  const info = await lstat12(path);
19474
19859
  if (!info.isFile() || info.isSymbolicLink()) throw new Error("Managed tool record must be a regular file");
19475
19860
  if (info.size > MAX_RECORD_BYTES) throw new Error(`Managed tool record exceeds ${MAX_RECORD_BYTES} bytes`);
19476
- const record = ManagedToolRecordSchema.parse(JSON.parse(await readFile20(path, "utf8")));
19861
+ const record = ManagedToolRecordSchema.parse(JSON.parse(await readFile21(path, "utf8")));
19477
19862
  if (record.scope !== scope2) throw new Error(`Managed tool record scope must match its ${scope2} directory`);
19478
19863
  const key = scopedKey(scope2, record.name);
19479
19864
  if (this.#entries.has(key)) throw new Error(`Duplicate managed tool name in ${scope2} scope: ${record.name}`);
@@ -20386,8 +20771,8 @@ ${total}`;
20386
20771
 
20387
20772
  // src/memory/store.ts
20388
20773
  import { createHash as createHash16 } from "crypto";
20389
- import { readFile as readFile21, rm as rm9 } from "fs/promises";
20390
- import { resolve as resolve27, join as join27, relative as relative19, sep as sep19 } from "path";
20774
+ import { readFile as readFile22, rm as rm9 } from "fs/promises";
20775
+ import { resolve as resolve27, join as join28, relative as relative19, sep as sep19 } from "path";
20391
20776
 
20392
20777
  // src/memory/similarity.ts
20393
20778
  function normalizeForSimilarity(value) {
@@ -20490,9 +20875,9 @@ var MemoryStore = class {
20490
20875
  if (!Number.isSafeInteger(options.maxEntries) || options.maxEntries < 1) throw new Error("maxEntries must be an integer of at least 1");
20491
20876
  if (!Number.isSafeInteger(options.maxEntryChars) || options.maxEntryChars < 1) throw new Error("maxEntryChars must be an integer of at least 1");
20492
20877
  this.workspace = resolve27(options.workspace);
20493
- this.#memoryRoot = join27(this.workspace, ".flavor", "memory");
20494
- this.path = join27(this.#memoryRoot, "MEMORY.md");
20495
- this.#behaviorPath = join27(this.#memoryRoot, "behavior.json");
20878
+ this.#memoryRoot = join28(this.workspace, ".flavor", "memory");
20879
+ this.path = join28(this.#memoryRoot, "MEMORY.md");
20880
+ this.#behaviorPath = join28(this.#memoryRoot, "behavior.json");
20496
20881
  this.#maxEntries = options.maxEntries;
20497
20882
  this.#maxEntryChars = options.maxEntryChars;
20498
20883
  }
@@ -20710,7 +21095,7 @@ var MemoryStore = class {
20710
21095
  });
20711
21096
  }
20712
21097
  async #readTaskItem(reference) {
20713
- const document = decodeTask(await readFile21(this.#resolveContentPath(reference.contentPath), "utf8"));
21098
+ const document = decodeTask(await readFile22(this.#resolveContentPath(reference.contentPath), "utf8"));
20714
21099
  return document.items.find((item) => item.id === reference.id)?.content ?? reference.summary;
20715
21100
  }
20716
21101
  #resolveContentPath(contentPath) {
@@ -21186,8 +21571,8 @@ function createExecutionEnvironment(workspace, config) {
21186
21571
  }
21187
21572
 
21188
21573
  // src/ide/client.ts
21189
- import { readFile as readFile22, readdir as readdir10, stat as stat10 } from "fs/promises";
21190
- import { basename as basename6, join as join28, relative as relative20, resolve as resolve28, sep as sep20 } from "path";
21574
+ import { readFile as readFile23, readdir as readdir10, stat as stat11 } from "fs/promises";
21575
+ import { basename as basename6, join as join29, relative as relative20, resolve as resolve28, sep as sep20 } from "path";
21191
21576
  var FlavorIdeClient = class {
21192
21577
  #workspace;
21193
21578
  #home;
@@ -21283,7 +21668,7 @@ var FlavorIdeClient = class {
21283
21668
  };
21284
21669
  if (await this.#healthy(configured)) return configured;
21285
21670
  }
21286
- const directory = join28(this.#home, ".flavor-code", "ide");
21671
+ const directory = join29(this.#home, ".flavor-code", "ide");
21287
21672
  let entries;
21288
21673
  try {
21289
21674
  entries = (await readdir10(directory)).filter((name) => name.endsWith(".lock"));
@@ -21291,9 +21676,9 @@ var FlavorIdeClient = class {
21291
21676
  return void 0;
21292
21677
  }
21293
21678
  const candidates = (await Promise.all(entries.map(async (name) => {
21294
- const path = join28(directory, name);
21679
+ const path = join29(directory, name);
21295
21680
  try {
21296
- const [contents, info] = await Promise.all([readFile22(path, "utf8"), stat10(path)]);
21681
+ const [contents, info] = await Promise.all([readFile23(path, "utf8"), stat11(path)]);
21297
21682
  const lock = parseLockfile(contents);
21298
21683
  return lock === void 0 ? void 0 : { lock, modified: info.mtimeMs };
21299
21684
  } catch {
@@ -21412,6 +21797,7 @@ function relativeDisplay(workspace, filePath) {
21412
21797
 
21413
21798
  // src/jobs/registry.ts
21414
21799
  import { randomUUID as randomUUID17 } from "crypto";
21800
+ var OUTPUT_SLICE_SLACK_CHARS = 64 * 1024;
21415
21801
  var JobRegistry = class {
21416
21802
  #jobs = /* @__PURE__ */ new Map();
21417
21803
  #maxOutputChars;
@@ -21440,7 +21826,8 @@ var JobRegistry = class {
21440
21826
  updatedAt: now,
21441
21827
  outputChars: 0,
21442
21828
  truncated: false,
21443
- output: "",
21829
+ chunks: [],
21830
+ windowChars: 0,
21444
21831
  outputStart: 0,
21445
21832
  settle,
21446
21833
  done
@@ -21451,11 +21838,16 @@ var JobRegistry = class {
21451
21838
  id,
21452
21839
  append: (text) => {
21453
21840
  if (record.state !== "running" || text === "") return;
21454
- record.output += text;
21841
+ record.chunks.push(text);
21842
+ record.windowChars += text.length;
21455
21843
  record.outputChars += text.length;
21456
- if (record.output.length > this.#maxOutputChars) {
21457
- const removed = record.output.length - this.#maxOutputChars;
21458
- record.output = record.output.slice(removed);
21844
+ if (record.outputChars > this.#maxOutputChars) record.truncated = true;
21845
+ if (record.windowChars > this.#maxOutputChars + OUTPUT_SLICE_SLACK_CHARS) {
21846
+ const joined = record.chunks.join("");
21847
+ const kept = joined.slice(joined.length - this.#maxOutputChars);
21848
+ const removed = joined.length - kept.length;
21849
+ record.chunks = [kept];
21850
+ record.windowChars = kept.length;
21459
21851
  record.outputStart += removed;
21460
21852
  record.truncated = true;
21461
21853
  }
@@ -21476,8 +21868,20 @@ var JobRegistry = class {
21476
21868
  }
21477
21869
  read(id, owner2, cursor = 0) {
21478
21870
  const job = this.#owned(id, owner2);
21479
- const effective = Math.max(cursor, job.outputStart);
21480
- return { ...snapshot2(job), output: job.output.slice(effective - job.outputStart), cursor: job.outputStart + job.output.length };
21871
+ const window = job.chunks.join("");
21872
+ const dropped = Math.max(0, window.length - this.#maxOutputChars);
21873
+ const effective = Math.max(cursor, job.outputStart + dropped);
21874
+ return { ...snapshot2(job), output: window.slice(effective - job.outputStart), cursor: job.outputStart + window.length };
21875
+ }
21876
+ /** Retained sizes for the rotation census. */
21877
+ census() {
21878
+ let windowChars = 0;
21879
+ let running = 0;
21880
+ for (const job of this.#jobs.values()) {
21881
+ windowChars += job.windowChars;
21882
+ if (job.state === "running") running += 1;
21883
+ }
21884
+ return { jobs: this.#jobs.size, running, windowChars };
21481
21885
  }
21482
21886
  async wait(id, owner2, signal) {
21483
21887
  signal?.throwIfAborted();
@@ -22705,8 +23109,8 @@ async function createProductionRuntime(options) {
22705
23109
  const registeredProviders = registration.registered;
22706
23110
  let effectiveLlm = registration.effectiveLlm;
22707
23111
  const pluginHost = new PluginHost({
22708
- globalPluginDirs: [join29(home, ".flavor-code", "plugins")],
22709
- projectPluginDirs: [join29(workspace, ".flavor", "plugins")],
23112
+ globalPluginDirs: [join30(home, ".flavor-code", "plugins")],
23113
+ projectPluginDirs: [join30(workspace, ".flavor", "plugins")],
22710
23114
  config,
22711
23115
  // Existing project plugins (including astgraph and superharness) use Node.js
22712
23116
  // built-ins. The Worker/vm sandbox deliberately blocks those imports, so it
@@ -22815,7 +23219,7 @@ async function createProductionRuntime(options) {
22815
23219
  const address = palSocketAddress({
22816
23220
  platform: process.platform,
22817
23221
  userScope: home,
22818
- runtimeDir: join29(tmpdir4(), `flavor-code-pals-${createHash18("sha256").update(home).digest("hex").slice(0, 16)}`)
23222
+ runtimeDir: join30(tmpdir4(), `flavor-code-pals-${createHash18("sha256").update(home).digest("hex").slice(0, 16)}`)
22819
23223
  });
22820
23224
  const baseCollaborationClient = collaboration.client ?? collaboration.createClient?.({
22821
23225
  instanceId: collaboration.instanceId,
@@ -22926,8 +23330,8 @@ async function createProductionRuntime(options) {
22926
23330
  diagnostics.push(`MCP servers could not start: ${message(error)}`);
22927
23331
  });
22928
23332
  const skills = new SkillRegistry({
22929
- globalRoots: [join29(home, ".flavor-code", "skills")],
22930
- projectRoots: [join29(workspace, ".flavor", "skills"), ...pluginSkillRoots],
23333
+ globalRoots: [join30(home, ".flavor-code", "skills")],
23334
+ projectRoots: [join30(workspace, ".flavor", "skills"), ...pluginSkillRoots],
22931
23335
  authorizeResource: async () => true,
22932
23336
  disabledNames: config.skills.disabled
22933
23337
  });
@@ -22935,7 +23339,7 @@ async function createProductionRuntime(options) {
22935
23339
  skillsReady.catch(() => void 0);
22936
23340
  tools.push(createSkillTool(skills), createSkillResourceTool(skills));
22937
23341
  if (options.extraTools !== void 0) tools.push(...options.extraTools);
22938
- const flavor = await optionalText2(join29(workspace, "FLAVOR.md"));
23342
+ const flavor = await optionalText2(join30(workspace, "FLAVOR.md"));
22939
23343
  const instructionBaseline = await workspaceInstructions.baseline();
22940
23344
  let memoryContext;
22941
23345
  if (memoryStore !== void 0) {
@@ -23520,7 +23924,7 @@ async function createProductionRuntime(options) {
23520
23924
  evolveService.toolDefinition(),
23521
23925
  ...mcpTools
23522
23926
  ];
23523
- const loopFlavor = await optionalText2(join29(input.workspace, "FLAVOR.md"));
23927
+ const loopFlavor = await optionalText2(join30(input.workspace, "FLAVOR.md"));
23524
23928
  const loopInstructionBaseline = await loopInstructions.baseline();
23525
23929
  const loopEnvironment = createPromptEnvironment({
23526
23930
  now: /* @__PURE__ */ new Date(),
@@ -23628,12 +24032,83 @@ async function createProductionRuntime(options) {
23628
24032
  await loopExecutionEnvironment?.dispose();
23629
24033
  }
23630
24034
  };
24035
+ let activeLongTask;
24036
+ const readMemoryRestartMarker = () => {
24037
+ try {
24038
+ return parseMemoryRestartMarker(readFileSync3(memoryRestartMarkerPath(workspace), "utf8"));
24039
+ } catch {
24040
+ return void 0;
24041
+ }
24042
+ };
24043
+ const currentPressureReading = () => verifiedHeapPressure(MEMORY_PRESSURE_HEAP_RATIO) ?? { ...readHeap(), gcVerified: false };
24044
+ const collectRotationHolders = () => {
24045
+ const holders = {};
24046
+ try {
24047
+ const context = harness.main.context.census();
24048
+ holders.context = { entries: context.messages, chars: context.messageChars };
24049
+ holders.contextVisibility = { entries: context.visibilityEntries, chars: context.visibilityChars };
24050
+ holders.contextTransient = { entries: context.transientEntries, chars: context.transientChars };
24051
+ } catch {
24052
+ }
24053
+ try {
24054
+ holders.journal = { entries: harnessJournal.recordCount };
24055
+ } catch {
24056
+ }
24057
+ try {
24058
+ const jobCensus = jobs.census();
24059
+ holders.jobs = { entries: jobCensus.jobs, chars: jobCensus.windowChars };
24060
+ } catch {
24061
+ }
24062
+ try {
24063
+ holders.sessionTree = { entries: sessionHistory.nodeCount };
24064
+ } catch {
24065
+ }
24066
+ return holders;
24067
+ };
24068
+ const rotateForHeap = (continuation, reading) => {
24069
+ requestMemoryRestart(workspace, sessionId, emitOutput, {
24070
+ ...continuation === void 0 ? {} : { continuation },
24071
+ ...reading === void 0 ? {} : { reading },
24072
+ holders: collectRotationHolders()
24073
+ }, options.onMemoryRestartRequested);
24074
+ };
24075
+ const maybeRotateAtBoundary = (continuation) => {
24076
+ activeLongTask = continuation;
24077
+ const pressure = verifiedHeapPressure(MEMORY_PRESSURE_HEAP_RATIO);
24078
+ if (pressure !== void 0) {
24079
+ rotateForHeap(continuation, pressure);
24080
+ return;
24081
+ }
24082
+ const reading = heapRotationNeeded();
24083
+ if (reading === void 0) return;
24084
+ if (rotationCooldownActive(readMemoryRestartMarker(), sessionId, /* @__PURE__ */ new Date())) return;
24085
+ rotateForHeap(continuation, reading);
24086
+ };
24087
+ const maybeRotateAtTurnBoundary = () => {
24088
+ const reading = heapRotationNeeded();
24089
+ if (reading === void 0) return;
24090
+ if (rotationCooldownActive(readMemoryRestartMarker(), sessionId, /* @__PURE__ */ new Date())) return;
24091
+ rotateForHeap(void 0, reading);
24092
+ };
24093
+ const consumePendingContinuation = () => {
24094
+ const marker = readMemoryRestartMarker();
24095
+ const continuation = pendingContinuation(marker, sessionId, /* @__PURE__ */ new Date());
24096
+ if (continuation === void 0 || marker === void 0) return void 0;
24097
+ try {
24098
+ const markerPath = memoryRestartMarkerPath(workspace);
24099
+ writeMarkerAtomically(markerPath, consumedMemoryRestartMarker(marker));
24100
+ } catch {
24101
+ }
24102
+ return continuation;
24103
+ };
23631
24104
  const loopStore = new LoopStore({ workspace });
23632
24105
  const loopOrchestrator = new LoopOrchestrator({
23633
24106
  workspace,
23634
24107
  config: config.loop,
23635
24108
  persistence: loopStore,
23636
24109
  hallucinationGuard,
24110
+ onCycleBoundary: (loopId) => maybeRotateAtBoundary({ kind: "loop", id: loopId }),
24111
+ onMemoryCheckpoint: (loopId) => maybeRotateAtBoundary({ kind: "loop", id: loopId }),
23637
24112
  prepareWorkspace: (input) => prepareLoopWorkspace(input),
23638
24113
  inferVerification: inferVerificationPlan,
23639
24114
  runWorker: ({ workspace: executionWorkspace, prompt, signal }) => runLoopWorker({ workspace: executionWorkspace, prompt, signal }),
@@ -23670,9 +24145,11 @@ async function createProductionRuntime(options) {
23670
24145
  plannerModelId: mainModel,
23671
24146
  classifierModelId: mainModel,
23672
24147
  skepticCount: 3,
23673
- maxRounds: 5,
23674
- maxStallStreak: 2,
24148
+ maxRounds: config.goal.maxRounds,
24149
+ maxStallStreak: config.goal.maxStallStreak,
23675
24150
  persistence: goalStore,
24151
+ onRoundBoundary: (goalId) => maybeRotateAtBoundary({ kind: "goal", id: goalId }),
24152
+ onMemoryCheckpoint: (goalId) => maybeRotateAtBoundary({ kind: "goal", id: goalId }),
23676
24153
  verifyHost: async (signal) => runVerificationPlan(
23677
24154
  await inferVerificationPlan(workspace),
23678
24155
  workspace,
@@ -23767,30 +24244,49 @@ async function createProductionRuntime(options) {
23767
24244
  contextEpoch: harness.main.context.snapshot().epoch?.id ?? "legacy"
23768
24245
  });
23769
24246
  const turnId = harnessJournal.startTurn(turnConfig, { prompt, initialUserMessage: runOptions?.initialUserMessage });
23770
- return monitorMemoryRestart(durableTurn(persistAfter(runMain(
23771
- harness,
23772
- skills,
23773
- prompt,
23774
- signal,
23775
- selectedModels.mainError,
23776
- memoryStore === void 0 || !memoryHasRoutableEntries && userMemoryContext === void 0 ? void 0 : {
23777
- store: memoryStore,
23778
- taskId: memoryLifecycle.taskId ?? sessionId,
23779
- topK: config.memory.retrievalTopK,
23780
- maxChars: config.memory.maxPromptChars
23781
- },
23782
- runOptions?.getSteeringMessages,
23783
- runOptions?.initialUserMessage,
23784
- runOptions?.additionalContext,
23785
- ide
23786
- ), persist), harnessJournal, turnId, turnConfig), () => requestMemoryRestart(workspace, sessionId, emitOutput));
24247
+ return monitorTurnMemory(
24248
+ durableTurn(persistAfter(runMain(
24249
+ harness,
24250
+ skills,
24251
+ prompt,
24252
+ signal,
24253
+ selectedModels.mainError,
24254
+ memoryStore === void 0 || !memoryHasRoutableEntries && userMemoryContext === void 0 ? void 0 : {
24255
+ store: memoryStore,
24256
+ taskId: memoryLifecycle.taskId ?? sessionId,
24257
+ topK: config.memory.retrievalTopK,
24258
+ maxChars: config.memory.maxPromptChars
24259
+ },
24260
+ runOptions?.getSteeringMessages,
24261
+ runOptions?.initialUserMessage,
24262
+ runOptions?.additionalContext,
24263
+ ide
24264
+ ), persist), harnessJournal, turnId, turnConfig),
24265
+ () => rotateForHeap(void 0, currentPressureReading()),
24266
+ () => maybeRotateAtTurnBoundary()
24267
+ );
23787
24268
  },
23788
24269
  runSkill: (skill, prompt, signal) => persistAfter(
23789
24270
  runExplicitSkill(harness, skills, skill, prompt, signal, selectedModels.mainError),
23790
24271
  persist
23791
24272
  ),
23792
- runLoop: (goal, signal) => runLoopSession(loopOrchestrator, hooks, goal, signal),
23793
- runGoal: (goal, signal) => persistEach(runGoalSession(goalOrchestrator, goal, signal), persist),
24273
+ runLoop: (goal, signal) => monitorLongTaskRestart(
24274
+ runLoopSession(loopOrchestrator, hooks, goal, signal),
24275
+ () => rotateForHeap(activeLongTask, currentPressureReading())
24276
+ ),
24277
+ runGoal: (goal, signal) => monitorLongTaskRestart(
24278
+ persistEach(runGoalSession(goalOrchestrator, goal, signal), persist),
24279
+ () => rotateForHeap(activeLongTask, currentPressureReading())
24280
+ ),
24281
+ resumeLoop: (loopId, signal) => monitorLongTaskRestart(
24282
+ runResumeLoopSession(loopOrchestrator, hooks, loopId, signal),
24283
+ () => rotateForHeap(activeLongTask ?? { kind: "loop", id: loopId }, currentPressureReading())
24284
+ ),
24285
+ resumeGoal: (goalId, signal) => monitorLongTaskRestart(
24286
+ persistEach(runResumeGoalSession(goalOrchestrator, goalId, signal), persist),
24287
+ () => rotateForHeap(activeLongTask ?? { kind: "goal", id: goalId }, currentPressureReading())
24288
+ ),
24289
+ continuation: () => consumePendingContinuation(),
23794
24290
  mcp: async (command, signal) => {
23795
24291
  signal.throwIfAborted();
23796
24292
  const manager = await mcpReady;
@@ -23884,7 +24380,7 @@ async function createProductionRuntime(options) {
23884
24380
  const root = resolve31(workspace);
23885
24381
  const abs = resolve31(root, path);
23886
24382
  if (abs !== root && !abs.startsWith(root + sep22)) throw new Error(`path outside workspace: ${path}`);
23887
- return readFile23(abs, "utf8");
24383
+ return readFile24(abs, "utf8");
23888
24384
  },
23889
24385
  history: async (path, limit) => fileHistory(workspace, path, limit),
23890
24386
  questions,
@@ -23896,7 +24392,7 @@ async function createProductionRuntime(options) {
23896
24392
  }, query, focus, signal),
23897
24393
  audit: async (toolFilter) => {
23898
24394
  try {
23899
- const raw = await readFile23(auditLogger.path, "utf8");
24395
+ const raw = await readFile24(auditLogger.path, "utf8");
23900
24396
  const lines = raw.trim().split("\n").filter((line) => line.length > 0);
23901
24397
  const entries = lines.map((line) => {
23902
24398
  try {
@@ -23936,7 +24432,7 @@ ${summary2}`;
23936
24432
  doctor: async () => formatDoctorReport(await runDoctor({ workspace, home, environment })),
23937
24433
  usage: async () => {
23938
24434
  try {
23939
- const raw = await readFile23(usageLogPath(), "utf8");
24435
+ const raw = await readFile24(usageLogPath(), "utf8");
23940
24436
  return formatUsageSummary(summarizeUsage(parseUsageEntries(raw)));
23941
24437
  } catch (error) {
23942
24438
  if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
@@ -24089,7 +24585,7 @@ ${renderMemoryDocument(entries)}`;
24089
24585
  return `Provider "${providerName}" is missing authorizationUrl, tokenUrl, or clientId.`;
24090
24586
  }
24091
24587
  try {
24092
- const tokenStore = createFileTokenStore(join29(home, ".flavor-code", "auth.json"));
24588
+ const tokenStore = createFileTokenStore(join30(home, ".flavor-code", "auth.json"));
24093
24589
  const credentialId = oauthCredentialId(oauthConfig.tokenUrl, oauthConfig.clientId);
24094
24590
  await migrateLegacyOAuthToken(tokenStore, providerName, credentialId);
24095
24591
  const oauth = new OAuthCallbackAuthProvider({
@@ -24136,7 +24632,7 @@ ${renderMemoryDocument(entries)}`;
24136
24632
  }
24137
24633
  },
24138
24634
  async logout() {
24139
- const tokenStore = createFileTokenStore(join29(home, ".flavor-code", "auth.json"));
24635
+ const tokenStore = createFileTokenStore(join30(home, ".flavor-code", "auth.json"));
24140
24636
  const tokens = await tokenStore.load();
24141
24637
  const hadCredentials = Object.keys(tokens).length > 0 || effectiveLlm !== void 0;
24142
24638
  if (!hadCredentials) {
@@ -24329,10 +24825,16 @@ function formatMcpReconnect(server) {
24329
24825
  return `Failed to reconnect MCP server "${server.name}": ${server.error ?? "unknown error"}`;
24330
24826
  }
24331
24827
  async function* runLoopSession(orchestrator, hooks, goal, signal) {
24828
+ yield* loopSessionEvents(orchestrator.run({ goal, signal }), hooks);
24829
+ }
24830
+ async function* runResumeLoopSession(orchestrator, hooks, loopId, signal) {
24831
+ yield* loopSessionEvents(orchestrator.resume({ loopId, signal }), hooks);
24832
+ }
24833
+ async function* loopSessionEvents(events, hooks) {
24332
24834
  let totalInputTokens = 0;
24333
24835
  let totalOutputTokens = 0;
24334
24836
  let latestVerification;
24335
- for await (const event of orchestrator.run({ goal, signal })) {
24837
+ for await (const event of events) {
24336
24838
  if (event.type === "worker-event") {
24337
24839
  if (event.event.type === "usage") {
24338
24840
  totalInputTokens += event.event.inputTokens;
@@ -24436,7 +24938,7 @@ async function workspaceFingerprint(workspace) {
24436
24938
  hash.update(name);
24437
24939
  if (remaining <= 0) continue;
24438
24940
  try {
24439
- const content = await readFile23(path);
24941
+ const content = await readFile24(path);
24440
24942
  const slice = content.subarray(0, remaining);
24441
24943
  hash.update(slice);
24442
24944
  remaining -= slice.length;
@@ -24548,7 +25050,16 @@ async function* durableTurn(source, journal, turnId, config) {
24548
25050
  }
24549
25051
  }
24550
25052
  }
24551
- async function* monitorMemoryRestart(source, onRestartNeeded) {
25053
+ async function* monitorTurnMemory(source, onPressure, onBoundary) {
25054
+ let pressure = false;
25055
+ for await (const event of source) {
25056
+ if (event.type === "error" && event.error.code === "memory_pressure") pressure = true;
25057
+ yield event;
25058
+ }
25059
+ if (pressure) onPressure();
25060
+ else onBoundary();
25061
+ }
25062
+ async function* monitorLongTaskRestart(source, onRestartNeeded) {
24552
25063
  let pressure = false;
24553
25064
  for await (const event of source) {
24554
25065
  if (event.type === "error" && event.error.code === "memory_pressure") pressure = true;
@@ -24556,7 +25067,10 @@ async function* monitorMemoryRestart(source, onRestartNeeded) {
24556
25067
  }
24557
25068
  if (pressure) onRestartNeeded();
24558
25069
  }
24559
- function requestMemoryRestart(workspace, sessionId, emitOutput) {
25070
+ function requestMemoryRestart(workspace, sessionId, emitOutput, rotation, onRestartRequested) {
25071
+ if (memoryRotationActive()) return;
25072
+ markMemoryRotation();
25073
+ const continuation = rotation?.continuation;
24560
25074
  try {
24561
25075
  const markerPath = memoryRestartMarkerPath(workspace);
24562
25076
  let previous;
@@ -24564,14 +25078,43 @@ function requestMemoryRestart(workspace, sessionId, emitOutput) {
24564
25078
  previous = parseMemoryRestartMarker(readFileSync3(markerPath, "utf8"));
24565
25079
  } catch {
24566
25080
  }
24567
- mkdirSync3(dirname17(markerPath), { recursive: true });
24568
- writeFileSync2(markerPath, `${JSON.stringify(nextMemoryRestartMarker(previous, sessionId, /* @__PURE__ */ new Date()))}
24569
- `, "utf8");
25081
+ writeMarkerAtomically(markerPath, nextMemoryRestartMarker(previous, sessionId, /* @__PURE__ */ new Date(), continuation));
24570
25082
  } catch {
24571
25083
  }
24572
- process.exitCode = MEMORY_RESTART_EXIT_CODE;
24573
- emitOutput({ type: "notice", message: "Memory watermark reached; the session is saved and Flavor is restarting with a fresh heap." });
24574
- emitOutput({ type: "exit" });
25084
+ if (rotation?.reading !== void 0) {
25085
+ const census = {
25086
+ at: (/* @__PURE__ */ new Date()).toISOString(),
25087
+ reason: continuation === void 0 ? "memory-pressure" : `heap-rotation:${continuation.kind}`,
25088
+ sessionId,
25089
+ heap: censusHeapBlock(rotation.reading),
25090
+ holders: rotation.holders ?? {}
25091
+ };
25092
+ writeRotationCensus(workspace, census);
25093
+ if (heapSnapshotWarranted(rotation.reading)) writeRotationHeapSnapshot(workspace);
25094
+ }
25095
+ emitOutput({
25096
+ type: "notice",
25097
+ 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.`
25098
+ });
25099
+ if (onRestartRequested !== void 0) onRestartRequested(sessionId);
25100
+ else {
25101
+ process.exitCode = MEMORY_RESTART_EXIT_CODE;
25102
+ emitOutput({ type: "exit" });
25103
+ }
25104
+ }
25105
+ function writeMarkerAtomically(path, marker) {
25106
+ mkdirSync4(dirname17(path), { recursive: true });
25107
+ const temporary = `${path}.${process.pid}.${randomUUID20()}.tmp`;
25108
+ try {
25109
+ writeFileSync3(temporary, `${JSON.stringify(marker)}
25110
+ `, { encoding: "utf8", mode: 384 });
25111
+ renameSync2(temporary, path);
25112
+ } finally {
25113
+ try {
25114
+ unlinkSync3(temporary);
25115
+ } catch {
25116
+ }
25117
+ }
24575
25118
  }
24576
25119
  async function runGitCommit(deps, hint, signal) {
24577
25120
  if (!await isGitRepository(deps.workspace)) return "Not a git repository \u2014 /commit needs git.";
@@ -24666,7 +25209,7 @@ async function registerConfiguredAdapters(providers, registry, environment, diag
24666
25209
  const configured = { ...providers };
24667
25210
  if (configured.openai === void 0 && environment.OPENAI_API_KEY) configured.openai = { type: "openai", apiKey: environment.OPENAI_API_KEY };
24668
25211
  if (configured.anthropic === void 0 && environment.ANTHROPIC_API_KEY) configured.anthropic = { type: "anthropic", apiKey: environment.ANTHROPIC_API_KEY };
24669
- const oauthTokenStore = createFileTokenStore(join29(home, ".flavor-code", "auth.json"));
25212
+ const oauthTokenStore = createFileTokenStore(join30(home, ".flavor-code", "auth.json"));
24670
25213
  const registered = [];
24671
25214
  let effectiveLlm;
24672
25215
  for (const [name, provider] of Object.entries(configured)) {
@@ -24935,7 +25478,7 @@ async function generateSleepReview(registry, modelId, prompt, signal) {
24935
25478
  }
24936
25479
  async function optionalText2(path) {
24937
25480
  try {
24938
- return await readFile23(path, "utf8");
25481
+ return await readFile24(path, "utf8");
24939
25482
  } catch (error) {
24940
25483
  if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") return void 0;
24941
25484
  throw error;
@@ -25020,7 +25563,17 @@ function attachCleanupError(primary, cleanup) {
25020
25563
  }
25021
25564
  }
25022
25565
  async function* runGoalSession(orchestrator, goal, signal) {
25023
- for await (const event of orchestrator.run({ goal, signal })) {
25566
+ yield* goalSessionEvents(orchestrator.run({ goal, signal }));
25567
+ }
25568
+ async function* runResumeGoalSession(orchestrator, goalId, signal) {
25569
+ yield* goalSessionEvents(orchestrator.resume({ goalId, signal }));
25570
+ }
25571
+ async function* goalSessionEvents(events) {
25572
+ for await (const event of events) {
25573
+ if (event.type === "goal-resumed") {
25574
+ yield { type: "notice", message: `Resuming goal ${event.goalId} at round ${event.round} after a heap rotation.` };
25575
+ continue;
25576
+ }
25024
25577
  if (event.type === "goal-plan-created") {
25025
25578
  yield { type: "notice", message: `Goal plan created (${event.plan.kind}) with ${event.plan.criteria.length} acceptance criteria.` };
25026
25579
  yield { type: "notice", message: `Plan file: ${event.planPath}` };