flavor-code 1.3.13 → 1.3.16

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.
@@ -1,5 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from "node:module"; const require = createRequire(import.meta.url);
3
+ import {
4
+ MEMORY_RESTART_EXIT_CODE,
5
+ memoryRestartMarkerPath,
6
+ nextMemoryRestartMarker,
7
+ parseMemoryRestartMarker
8
+ } from "./chunk-CIVPKHF4.js";
3
9
  import {
4
10
  PERMISSION_MODES,
5
11
  PermissionModeSchema,
@@ -12,7 +18,7 @@ import {
12
18
  redactConfig,
13
19
  setProjectMcpServerDisabled,
14
20
  updateProtectedFile
15
- } from "./chunk-JFKRX2KG.js";
21
+ } from "./chunk-HJOCNKPN.js";
16
22
  import {
17
23
  AuditLogger,
18
24
  appendUsageLog,
@@ -5326,9 +5332,10 @@ async function ensureOnce(options) {
5326
5332
 
5327
5333
  // src/production.ts
5328
5334
  import { readFile as readFile22 } from "fs/promises";
5335
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync } from "fs";
5329
5336
  import { createHash as createHash18, randomUUID as randomUUID20 } from "crypto";
5330
5337
  import { homedir as homedir2, release as osRelease, tmpdir as tmpdir4, version as osVersion } from "os";
5331
- import { basename as basename6, join as join26, relative as relative20, resolve as resolve28, sep as sep20 } from "path";
5338
+ import { basename as basename6, dirname as dirname19, join as join26, relative as relative20, resolve as resolve28, sep as sep20 } from "path";
5332
5339
 
5333
5340
  // src/agent/subagents.ts
5334
5341
  import { z as z5 } from "zod";
@@ -5877,7 +5884,9 @@ import { createHash as createHash4, randomUUID as randomUUID6 } from "crypto";
5877
5884
  var DEFAULT_COMPACTION_POLICY = {
5878
5885
  windowTokens: 2e5,
5879
5886
  reservedOutputTokens: 2e4,
5880
- autoCompactBufferTokens: 13e3,
5887
+ // 85% of the effective window (180_000 - 27_000 = 153_000): compaction used
5888
+ // to trigger at ~92.8%, which let sessions ride the threshold into heap OOM.
5889
+ autoCompactBufferTokens: 27e3,
5881
5890
  warningBufferTokens: 2e4,
5882
5891
  blockingBufferTokens: 3e3,
5883
5892
  microcompactKeepRecentToolResults: 5,
@@ -6087,6 +6096,13 @@ var ContextManager = class _ContextManager {
6087
6096
  #epoch;
6088
6097
  #visibilityLog = [];
6089
6098
  #activeTransientSystem = /* @__PURE__ */ new Map();
6099
+ /**
6100
+ * The visibility audit log persists every transient system admission and is
6101
+ * serialized in full on every session save. Bound both dimensions or a long
6102
+ * subagent-driven run grows it into hundreds of megabytes (heap OOM).
6103
+ */
6104
+ static VISIBILITY_LOG_MAX_ENTRIES = 1e3;
6105
+ static VISIBILITY_RECORD_CONTENT_LIMIT = 2048;
6090
6106
  constructor(options) {
6091
6107
  if (options.compactAtChars !== void 0 && options.compactAtChars <= 0) throw new Error("compactAtChars must be positive");
6092
6108
  if (options.toolOutputChars <= 0) throw new Error("toolOutputChars must be positive");
@@ -6182,11 +6198,14 @@ var ContextManager = class _ContextManager {
6182
6198
  const record = {
6183
6199
  id,
6184
6200
  role: "system",
6185
- content,
6201
+ content: limitVisibilityContent(content),
6186
6202
  admittedAt: (/* @__PURE__ */ new Date()).toISOString(),
6187
6203
  scope: "run"
6188
6204
  };
6189
6205
  this.#visibilityLog.push(record);
6206
+ if (this.#visibilityLog.length > _ContextManager.VISIBILITY_LOG_MAX_ENTRIES) {
6207
+ this.#visibilityLog = this.#visibilityLog.slice(-_ContextManager.VISIBILITY_LOG_MAX_ENTRIES);
6208
+ }
6190
6209
  this.#activeTransientSystem.set(id, { role: "system", content });
6191
6210
  return id;
6192
6211
  }
@@ -6210,8 +6229,9 @@ var ContextManager = class _ContextManager {
6210
6229
  const currentStableHash = currentStable === void 0 ? this.#epoch.stableSourceHash : hashMessages(currentStable);
6211
6230
  if (currentStable !== void 0 && currentStableHash !== this.#epoch.stableSourceHash) {
6212
6231
  const value = currentStable.map((message2) => message2.content).join("\n\n");
6232
+ const previous = this.#epoch.sources["system-baseline"] ?? this.#epoch.stableMessages.map((message2) => message2.content).join("\n\n");
6213
6233
  if (this.#epoch.sources["system-baseline"] !== value) {
6214
- updates.push(contextUpdateMessage("system-baseline", value));
6234
+ updates.push(contextUpdateMessage("system-baseline", deltaSourceValue(previous, value)));
6215
6235
  this.#epoch.sources["system-baseline"] = value;
6216
6236
  }
6217
6237
  } else if (currentStable !== void 0 && this.#epoch.sources["system-baseline"] !== void 0) {
@@ -6222,7 +6242,7 @@ var ContextManager = class _ContextManager {
6222
6242
  for (const key of DYNAMIC_SOURCE_ORDER) {
6223
6243
  const value = next[key];
6224
6244
  if (value === void 0 || value === this.#epoch.sources[key]) continue;
6225
- updates.push(contextUpdateMessage(key, displaySourceValue(key, value)));
6245
+ updates.push(contextUpdateMessage(key, deltaSourceValue(this.#epoch.sources[key], displaySourceValue(key, value))));
6226
6246
  this.#epoch.sources[key] = value;
6227
6247
  }
6228
6248
  for (const key of DYNAMIC_SOURCE_ORDER) {
@@ -6249,7 +6269,7 @@ var ContextManager = class _ContextManager {
6249
6269
  const legacySummary = snapshot4.summary?.role === "system" && snapshot4.summary.content.startsWith("Conversation summary\n") ? snapshot4.summary.content.slice("Conversation summary\n".length) : void 0;
6250
6270
  this.#compact = snapshot4.compact === void 0 ? legacySummary === void 0 ? void 0 : { summary: legacySummary, compactedAt: (/* @__PURE__ */ new Date(0)).toISOString() } : { ...snapshot4.compact };
6251
6271
  if (snapshot4.epoch !== void 0) this.#epoch = cloneEpoch(snapshot4.epoch);
6252
- this.#visibilityLog = snapshot4.visibilityLog?.map((item) => ({ ...item })) ?? [];
6272
+ this.#visibilityLog = boundVisibilityLog(snapshot4.visibilityLog);
6253
6273
  this.#activeTransientSystem.clear();
6254
6274
  this.#messages = messages.map((message2) => message2.role === "tool" ? { ...message2, content: truncateToolOutput(message2.content, this.#toolOutputChars) } : cloneMessage2(message2));
6255
6275
  this.#lastRecordedInputTokens = void 0;
@@ -6293,6 +6313,10 @@ var ContextManager = class _ContextManager {
6293
6313
  async prepareForModelCall(signal = new AbortController().signal) {
6294
6314
  signal.throwIfAborted();
6295
6315
  this.refreshContextSources();
6316
+ if (this.#dropStaleContextUpdates()) {
6317
+ this.#lastRecordedInputTokens = void 0;
6318
+ this.#estimatedTokensAtLastRecordedUsage = void 0;
6319
+ }
6296
6320
  if (!this.needsCompaction()) return false;
6297
6321
  const originalMessages = this.#messages;
6298
6322
  const originalRecordedUsage = this.#lastRecordedInputTokens;
@@ -6392,6 +6416,26 @@ var ContextManager = class _ContextManager {
6392
6416
  this.#reportCompactProgress(100);
6393
6417
  return true;
6394
6418
  }
6419
+ #dropStaleContextUpdates() {
6420
+ const latestBySource = /* @__PURE__ */ new Map();
6421
+ for (let index = this.#messages.length - 1; index >= 0; index -= 1) {
6422
+ const source = contextUpdateSource(this.#messages[index]);
6423
+ if (source === void 0 || latestBySource.has(source)) continue;
6424
+ latestBySource.set(source, index);
6425
+ }
6426
+ const next = [];
6427
+ let changed = false;
6428
+ this.#messages.forEach((message2, index) => {
6429
+ const source = contextUpdateSource(message2);
6430
+ if (source !== void 0 && latestBySource.get(source) !== index) {
6431
+ changed = true;
6432
+ return;
6433
+ }
6434
+ next.push(message2);
6435
+ });
6436
+ if (changed) this.#messages = next;
6437
+ return changed;
6438
+ }
6395
6439
  #reportCompactProgress(percentage) {
6396
6440
  const normalized = Math.max(0, Math.min(100, Math.floor(percentage / 10) * 10));
6397
6441
  if (normalized === this.#lastCompactProgress) return;
@@ -6503,6 +6547,33 @@ function contextUpdateMessage(source, content) {
6503
6547
  return { role: "system", content: `Context update [${source}]
6504
6548
  ${content}` };
6505
6549
  }
6550
+ var CONTEXT_UPDATE_PREFIX = "Context update [";
6551
+ function contextUpdateSource(message2) {
6552
+ if (message2.role !== "system" || typeof message2.content !== "string") return void 0;
6553
+ if (!message2.content.startsWith(CONTEXT_UPDATE_PREFIX)) return void 0;
6554
+ const end = message2.content.indexOf("]\n", CONTEXT_UPDATE_PREFIX.length);
6555
+ if (end < 0) return void 0;
6556
+ return message2.content.slice(CONTEXT_UPDATE_PREFIX.length, end);
6557
+ }
6558
+ function deltaSourceValue(previous, display) {
6559
+ if (previous === void 0 || !display.startsWith(previous)) return display;
6560
+ return display.slice(previous.length).replace(/^\s+/, "") || display;
6561
+ }
6562
+ function limitVisibilityContent(content) {
6563
+ const limit = ContextManager.VISIBILITY_RECORD_CONTENT_LIMIT;
6564
+ if (content.length <= limit) return content;
6565
+ return `${content.slice(0, limit)}
6566
+ ...[truncated; original length: ${content.length} characters]...`;
6567
+ }
6568
+ function boundVisibilityLog(records) {
6569
+ if (records === void 0) return [];
6570
+ const limit = ContextManager.VISIBILITY_RECORD_CONTENT_LIMIT;
6571
+ const bounded2 = records.map((item) => ({
6572
+ ...item,
6573
+ ...item.content.length <= limit ? {} : { content: limitVisibilityContent(item.content) }
6574
+ }));
6575
+ return bounded2.length > ContextManager.VISIBILITY_LOG_MAX_ENTRIES ? bounded2.slice(-ContextManager.VISIBILITY_LOG_MAX_ENTRIES) : bounded2;
6576
+ }
6506
6577
  function cloneEpoch(epoch) {
6507
6578
  return {
6508
6579
  ...epoch,
@@ -6797,6 +6868,9 @@ function reportProgress(callback, percentage) {
6797
6868
  }
6798
6869
  }
6799
6870
 
6871
+ // src/agent/loop.ts
6872
+ import { getHeapStatistics } from "v8";
6873
+
6800
6874
  // src/models/structured.ts
6801
6875
  import { z as z7 } from "zod";
6802
6876
  var DEFAULT_RETRY_DELAYS = [1e3, 2e3, 4e3];
@@ -7214,6 +7288,18 @@ function envMaxIterations() {
7214
7288
  if (!Number.isInteger(parsed) || parsed <= 0) return void 0;
7215
7289
  return parsed;
7216
7290
  }
7291
+ var MEMORY_PRESSURE_HEAP_RATIO = 0.8;
7292
+ function memoryPressureError(heapUsed, heapLimit) {
7293
+ if (heapLimit <= 0 || heapUsed < heapLimit * MEMORY_PRESSURE_HEAP_RATIO) return void 0;
7294
+ const mb = (bytes) => Math.round(bytes / 1048576);
7295
+ return {
7296
+ code: "memory_pressure",
7297
+ message: `Heap usage reached ${Math.round(heapUsed / heapLimit * 100)}% of the V8 limit (${mb(heapUsed)}MB of ${mb(heapLimit)}MB); stopping to avoid a crash. The session is saved and will resume after restart.`
7298
+ };
7299
+ }
7300
+ function currentMemoryPressure() {
7301
+ return memoryPressureError(process.memoryUsage().heapUsed, getHeapStatistics().heap_size_limit);
7302
+ }
7217
7303
  var AgentLoop = class {
7218
7304
  #options;
7219
7305
  #iterationLimitMode = "standard";
@@ -7284,6 +7370,11 @@ var AgentLoop = class {
7284
7370
  yield { type: "error", error: { code: "cancelled", message: abortMessage(request.signal) } };
7285
7371
  return;
7286
7372
  }
7373
+ const pressure = currentMemoryPressure();
7374
+ if (pressure !== void 0) {
7375
+ yield { type: "error", error: pressure };
7376
+ return;
7377
+ }
7287
7378
  const softLimit = Math.floor(maxIterations * this.#options.softLimitFactor);
7288
7379
  if (!warned && iteration >= softLimit) {
7289
7380
  warned = true;
@@ -12679,7 +12770,8 @@ var BoundedOutput = class {
12679
12770
  #headLimit;
12680
12771
  #tailLimit;
12681
12772
  #head = Buffer.alloc(0);
12682
- #tail = Buffer.alloc(0);
12773
+ #tailChunks = [];
12774
+ #tailBytes = 0;
12683
12775
  #complete = Buffer.alloc(0);
12684
12776
  #bytes = 0;
12685
12777
  constructor(limit) {
@@ -12699,15 +12791,24 @@ var BoundedOutput = class {
12699
12791
  const count = Math.min(this.#headLimit - this.#head.length, chunk.length);
12700
12792
  this.#head = Buffer.concat([this.#head, chunk.subarray(0, count)]);
12701
12793
  }
12702
- if (this.#tailLimit > 0) this.#tail = Buffer.concat([this.#tail, chunk]).subarray(-this.#tailLimit);
12794
+ if (this.#tailLimit > 0) {
12795
+ this.#tailChunks.push(chunk);
12796
+ this.#tailBytes += chunk.length;
12797
+ if (this.#tailBytes >= this.#tailLimit * 2) {
12798
+ const merged = Buffer.concat(this.#tailChunks).subarray(-this.#tailLimit);
12799
+ this.#tailChunks = [merged];
12800
+ this.#tailBytes = merged.length;
12801
+ }
12802
+ }
12703
12803
  }
12704
12804
  metadata() {
12705
12805
  return { truncated: this.truncated, originalBytes: this.#bytes, limitBytes: this.#limit };
12706
12806
  }
12707
12807
  text() {
12708
12808
  if (!this.truncated) return decodeOutput(this.#complete);
12809
+ const tail = Buffer.concat(this.#tailChunks).subarray(-this.#tailLimit);
12709
12810
  const encoding = classifyUtf8(this.#complete) === "invalid" && process.platform === "win32" ? "gb18030" : "utf-8";
12710
- return `${decodePrefix(this.#head, encoding)}${ELLIPSIS}${decodeSuffix(this.#tail, encoding)}`;
12811
+ return `${decodePrefix(this.#head, encoding)}${ELLIPSIS}${decodeSuffix(tail, encoding)}`;
12711
12812
  }
12712
12813
  };
12713
12814
  var AdaptiveOutputDecoder = class {
@@ -21421,9 +21522,28 @@ async function createProductionRuntime(options) {
21421
21522
  const recovered = options.resumeSession === void 0 ? void 0 : await sessionStore.load(options.resumeSession === true ? void 0 : options.resumeSession);
21422
21523
  let timelineState = recovered === void 0 ? createTranscriptState() : restoreTranscriptState(recovered.timeline.state);
21423
21524
  const restoredTranscript = restoreTranscriptState(timelineState);
21525
+ const pendingTimelineStreams = [];
21526
+ const flushTimelineStreams = () => {
21527
+ for (const pending of pendingTimelineStreams.splice(0)) {
21528
+ timelineState = transcriptReducer(timelineState, {
21529
+ type: "session",
21530
+ event: { type: pending.type, text: pending.chunks.join("") }
21531
+ });
21532
+ }
21533
+ };
21534
+ const recordTimelineEvent = (event) => {
21535
+ if (event.type === "text" || event.type === "thinking") {
21536
+ const last = pendingTimelineStreams.at(-1);
21537
+ if (last?.type === event.type) last.chunks.push(event.text);
21538
+ else pendingTimelineStreams.push({ type: event.type, chunks: [event.text] });
21539
+ return;
21540
+ }
21541
+ flushTimelineStreams();
21542
+ timelineState = transcriptReducer(timelineState, { type: "session", event });
21543
+ };
21424
21544
  let ideSessionId;
21425
21545
  const emitOutput = (event) => {
21426
- timelineState = transcriptReducer(timelineState, { type: "session", event });
21546
+ recordTimelineEvent(event);
21427
21547
  options.output(event);
21428
21548
  if (ideSessionId !== void 0) ide.publishEvent(ideSessionId, event);
21429
21549
  };
@@ -21840,27 +21960,30 @@ async function createProductionRuntime(options) {
21840
21960
  };
21841
21961
  }
21842
21962
  let persistTail = Promise.resolve();
21843
- const sessionDocument = () => ({
21844
- version: SESSION_VERSION,
21845
- sessionId,
21846
- createdAt,
21847
- updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
21848
- workspace: { path: workspace },
21849
- conversation: storedConversation(harness.main.context.snapshot()),
21850
- tasks: {
21851
- ...taskPlan === void 0 ? {} : { plan: taskPlan },
21852
- ...taskGraph === void 0 ? {} : { graph: taskGraph },
21853
- states: { ...taskStates },
21854
- results: { ...taskResults }
21855
- },
21856
- // Persist the resolved decision, not the harness snapshot: after logout the
21857
- // harness may still hold the old login model, and resuming from that stale
21858
- // value is what made the welcome card show a previous service.
21859
- models: { main: mainModel, subagent: childModel },
21860
- permissionMode: harness.permissionMode,
21861
- memory: memoryLifecycle,
21862
- timeline: { version: 1, state: timelineState }
21863
- });
21963
+ const sessionDocument = () => {
21964
+ flushTimelineStreams();
21965
+ return {
21966
+ version: SESSION_VERSION,
21967
+ sessionId,
21968
+ createdAt,
21969
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
21970
+ workspace: { path: workspace },
21971
+ conversation: storedConversation(harness.main.context.snapshot()),
21972
+ tasks: {
21973
+ ...taskPlan === void 0 ? {} : { plan: taskPlan },
21974
+ ...taskGraph === void 0 ? {} : { graph: taskGraph },
21975
+ states: { ...taskStates },
21976
+ results: { ...taskResults }
21977
+ },
21978
+ // Persist the resolved decision, not the harness snapshot: after logout the
21979
+ // harness may still hold the old login model, and resuming from that stale
21980
+ // value is what made the welcome card show a previous service.
21981
+ models: { main: mainModel, subagent: childModel },
21982
+ permissionMode: harness.permissionMode,
21983
+ memory: memoryLifecycle,
21984
+ timeline: { version: 1, state: timelineState }
21985
+ };
21986
+ };
21864
21987
  let persistFailed = false;
21865
21988
  const persist = () => {
21866
21989
  persistTail = persistTail.catch(() => void 0).then(
@@ -22612,7 +22735,7 @@ async function createProductionRuntime(options) {
22612
22735
  contextEpoch: harness.main.context.snapshot().epoch?.id ?? "legacy"
22613
22736
  });
22614
22737
  const turnId = harnessJournal.startTurn(turnConfig, { prompt, initialUserMessage: runOptions?.initialUserMessage });
22615
- return durableTurn(persistAndCheckpointAfter(runMain(
22738
+ return monitorMemoryRestart(durableTurn(persistAndCheckpointAfter(runMain(
22616
22739
  harness,
22617
22740
  skills,
22618
22741
  prompt,
@@ -22632,7 +22755,7 @@ async function createProductionRuntime(options) {
22632
22755
  prompt,
22633
22756
  context: harness.main.context.snapshot(),
22634
22757
  label: `turn: ${prompt.slice(0, 80)}`
22635
- })), harnessJournal, turnId, turnConfig);
22758
+ })), harnessJournal, turnId, turnConfig), () => requestMemoryRestart(workspace, sessionId, emitOutput));
22636
22759
  },
22637
22760
  runSkill: (skill, prompt, signal) => persistAfter(
22638
22761
  runExplicitSkill(harness, skills, skill, prompt, signal, selectedModels.mainError),
@@ -23381,6 +23504,31 @@ async function* durableTurn(source, journal, turnId, config) {
23381
23504
  }
23382
23505
  }
23383
23506
  }
23507
+ async function* monitorMemoryRestart(source, onRestartNeeded) {
23508
+ let pressure = false;
23509
+ for await (const event of source) {
23510
+ if (event.type === "error" && event.error.code === "memory_pressure") pressure = true;
23511
+ yield event;
23512
+ }
23513
+ if (pressure) onRestartNeeded();
23514
+ }
23515
+ function requestMemoryRestart(workspace, sessionId, emitOutput) {
23516
+ try {
23517
+ const markerPath = memoryRestartMarkerPath(workspace);
23518
+ let previous;
23519
+ try {
23520
+ previous = parseMemoryRestartMarker(readFileSync3(markerPath, "utf8"));
23521
+ } catch {
23522
+ }
23523
+ mkdirSync3(dirname19(markerPath), { recursive: true });
23524
+ writeFileSync(markerPath, `${JSON.stringify(nextMemoryRestartMarker(previous, sessionId, /* @__PURE__ */ new Date()))}
23525
+ `, "utf8");
23526
+ } catch {
23527
+ }
23528
+ process.exitCode = MEMORY_RESTART_EXIT_CODE;
23529
+ emitOutput({ type: "notice", message: "Memory watermark reached; the session is saved and Flavor is restarting with a fresh heap." });
23530
+ emitOutput({ type: "exit" });
23531
+ }
23384
23532
  async function runGitCommit(deps, hint, signal) {
23385
23533
  if (!await isGitRepository(deps.workspace)) return "Not a git repository \u2014 /commit needs git.";
23386
23534
  let summary2 = await changeSummary(deps.workspace);