johns-harness 2026.9.34 → 2026.9.36

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 (47) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/auth-profiles-5CHn7vq1.js +6 -0
  3. package/dist/channels/plugins/actions/telegram.js +1 -0
  4. package/dist/config-koj5M_EO.js +6 -0
  5. package/dist/daemon-cli.js +6 -0
  6. package/dist/entry.js +20 -1
  7. package/dist/model-selection-BGlGpPgM.js +6 -0
  8. package/dist/model-selection-BU6wl1le.js +6 -0
  9. package/dist/model-selection-L7RMwsG-.js +6 -0
  10. package/dist/pi-embedded-helpers-CkWXaNFn.js +76 -8
  11. package/dist/pi-embedded-helpers-DLLQ3MsR.js +76 -8
  12. package/dist/plugin-sdk/config-4Fgm-yEH.js +6 -0
  13. package/dist/plugin-sdk/config-Di-lyCdh.js +6 -0
  14. package/dist/plugin-sdk/discord.js +6 -0
  15. package/dist/plugin-sdk/mattermost.js +1 -0
  16. package/dist/plugin-sdk/model-auth-CX9cPHdC.js +6 -0
  17. package/dist/plugin-sdk/model-auth-Ciehz0x5.js +6 -0
  18. package/dist/plugin-sdk/model-auth-DVyo6JSX.js +6 -0
  19. package/dist/plugin-sdk/model-auth-kiHYHGrs.js +6 -0
  20. package/dist/plugin-sdk/model-selection-Dbj4jDYM.js +6 -0
  21. package/dist/plugin-sdk/pi-embedded-helpers-B89Z8pGU.js +76 -8
  22. package/dist/plugin-sdk/pi-embedded-helpers-BDFUaaiY.js +76 -8
  23. package/dist/plugin-sdk/pi-embedded-helpers-BnFhV3or.js +76 -8
  24. package/dist/plugin-sdk/pi-embedded-helpers-CRUVFL1T.js +76 -8
  25. package/dist/plugin-sdk/pi-embedded-helpers-CY7PsNpl.js +76 -8
  26. package/dist/plugin-sdk/pi-embedded-helpers-Dira0ZRW.js +76 -8
  27. package/dist/plugin-sdk/thinking-B-rIrY_n.js +1 -0
  28. package/dist/plugin-sdk/thinking-CopbP_Cn.js +1 -0
  29. package/dist/plugin-sdk/thinking-WQ4Zq8Nl.js +1 -0
  30. package/dist/plugin-sdk/thinking-WXqkGljb.js +1 -0
  31. package/dist/plugin-sdk/thinking-_uXqFaFO.js +1 -0
  32. package/dist/plugin-sdk/thinking-pTdxk2dr.js +1 -0
  33. package/dist/sessions-Cqv9FKMF.js +76 -8
  34. package/dist/sessions-DVRA-cO-.js +76 -8
  35. package/dist/thinking-B5B36ffe.js +1 -0
  36. package/dist/thinking-BYwvlJ3S.js +1 -0
  37. package/dist/thinking-CAzdgmNV.js +1 -0
  38. package/dist/thinking-DykY2Fzj.js +1 -0
  39. package/docs/PATCHES.md +28 -1
  40. package/docs/PROVIDER-CATALOG.md +12 -6
  41. package/node_modules/@mariozechner/pi-ai/dist/models.generated.js +24 -0
  42. package/node_modules/@mariozechner/pi-ai/dist/models.js +1 -0
  43. package/node_modules/@mariozechner/pi-ai/dist/providers/anthropic.js +2 -2
  44. package/package.json +1 -1
  45. package/vendor/patched-deps/@mariozechner/pi-ai/dist/models.generated.js +24 -0
  46. package/vendor/patched-deps/@mariozechner/pi-ai/dist/models.js +1 -0
  47. package/vendor/patched-deps/@mariozechner/pi-ai/dist/providers/anthropic.js +2 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 2026.9.36
4
+
5
+ - Family 82 (daily model integration, 2026-09-29): Claude Sonnet 5.5 (`claude-sonnet-5-5`) ships on `anthropic`. Card data from the official model page checked 2026-09-29: $2/$10 per MTok, cache read $0.20, 5m write $2.50, 1h write $4, 1M native context and 128K max output, adaptive thinking on by default (effort `low` through `max`, default `high`; `between_tools` is the lowest setting), and non-default `temperature`/`top_p`/`top_k` are rejected. `contextWindow` stays the standing 175K Sonnet window (owner caps do not change). Live probe 2026-09-29: the default request and the `thinking: adaptive` + `output_config.effort: xhigh` request both returned 200.
6
+ - Aliases: `sonnet` now resolves to `anthropic/claude-sonnet-5-5`; `sonnet5` keeps Claude Sonnet 5. Everything else is unchanged. A user alias that pins Sonnet 5 under `sonnet` moves forward on the next config load (family 79) and keeps `sonnet5`.
7
+ - Exact replay `scripts/patch-sonnet-5-5-model.py` (29 files); verifier checks 82.1-82.12. Roll back with `npm i -g johns-harness@2026.9.35`.
8
+
9
+ ## 2026.9.35
10
+
11
+ - Family 81 (session store memory, 2026-09-28): the gateway no longer runs out of memory every few days. `sessions.json` stops storing `skillsSnapshot` and `systemPromptReport` in every entry (they are rebuilt in memory on demand); existing stores shrink on the first write after the update (the live main store: 27 MB to 0.3 MB). Writes are compact JSON with one fewer full clone per load: 347 ms to 5 ms per session update on the live store copy.
12
+ - Session maintenance now defaults to `enforce` (prune after 30 days, 500 entries, rotate at 10 MB) and never removes the active session, main sessions, or anything updated in the last 24 hours. Set `session.maintenance.mode: "warn"` to keep the old behavior.
13
+ - The gateway starts with `--max-old-space-size=min(8192, RAM/2)` MB (only when that raises the limit); override with `JOHNNESS_GATEWAY_MAX_OLD_SPACE_MB=<MB>` or disable with `off`. Existing service installs pick it up on the next restart.
14
+ - Exact replay `scripts/patch-session-store-memory-81.py` (11 files); verifier checks 81.1-81.7. Roll back with `npm i -g johns-harness@2026.9.34`.
15
+
3
16
  ## 2026.9.34
4
17
 
5
18
  - Family 80 (daily model integration, 2026-09-24): GPT-6 Sol (`gpt-6-sol`) and GPT-6 Luna (`gpt-6-luna`) ship on `openai` and `openai-codex`, and xAI Grok 4.7 (`grok-4.7`) on `xai`. Card data from the official model pages checked 2026-09-24: GPT-6 Sol $2/$10 per MTok, Luna $0.10/$0.50, both 1.05M native context and 128K max output (272K harness window, like every GPT entry), efforts none through max; Grok 4.7 $2/$6, 500K native context (200K harness window and 64K max output, like Grok 4.6), efforts low through xhigh. All three allow `/think xhigh`. GPT-6 Sol/Luna drop `temperature` unless reasoning effort is `none` (the API rejects it otherwise, verified live).
@@ -5134,6 +5134,12 @@ Object.assign(DEFAULT_MODEL_ALIASES, {
5134
5134
  "nano-banana": "google/gemini-3-pro-image-preview",
5135
5135
  image: "openai-codex/gpt-image-2"
5136
5136
  });
5137
+ // JOHNNESS_PATCH_SONNET_55_MODEL: Claude Sonnet 5.5 is the newest Sonnet; `sonnet` moves to it and
5138
+ // Sonnet 5 keeps the versioned alias sonnet5.
5139
+ Object.assign(DEFAULT_MODEL_ALIASES, {
5140
+ sonnet: "anthropic/claude-sonnet-5-5",
5141
+ sonnet5: "anthropic/claude-sonnet-5"
5142
+ });
5137
5143
  // JOHNNESS_PATCH_OPUS_55_MODEL: Claude Opus 5.5 is the newest Opus; `opus` moves to it, opus-5 stays in the catalog.
5138
5144
  Object.assign(DEFAULT_MODEL_ALIASES, {
5139
5145
  opus: "anthropic/claude-opus-5-5",
@@ -2110,6 +2110,7 @@ const XHIGH_MODEL_REFS = [
2110
2110
  "anthropic/claude-fable-5-1",
2111
2111
  "meta/muse-spark-1.3", // JOHNNESS_PATCH_MUSE_SPARK
2112
2112
  "meta/muse-spark-1.2",
2113
+ "anthropic/claude-sonnet-5-5", // JOHNNESS_PATCH_SONNET_55_MODEL
2113
2114
  "anthropic/claude-opus-5-5", // JOHNNESS_PATCH_OPUS_55_MODEL
2114
2115
  "xai/grok-4.7", // JOHNNESS_PATCH_DAILY_MODELS_80
2115
2116
  "openai/gpt-6-sol",
@@ -1829,6 +1829,12 @@ Object.assign(DEFAULT_MODEL_ALIASES, {
1829
1829
  "nano-banana": "google/gemini-3-pro-image-preview",
1830
1830
  image: "openai-codex/gpt-image-2"
1831
1831
  });
1832
+ // JOHNNESS_PATCH_SONNET_55_MODEL: Claude Sonnet 5.5 is the newest Sonnet; `sonnet` moves to it and
1833
+ // Sonnet 5 keeps the versioned alias sonnet5.
1834
+ Object.assign(DEFAULT_MODEL_ALIASES, {
1835
+ sonnet: "anthropic/claude-sonnet-5-5",
1836
+ sonnet5: "anthropic/claude-sonnet-5"
1837
+ });
1832
1838
  // JOHNNESS_PATCH_OPUS_55_MODEL: Claude Opus 5.5 is the newest Opus; `opus` moves to it, opus-5 stays in the catalog.
1833
1839
  Object.assign(DEFAULT_MODEL_ALIASES, {
1834
1840
  opus: "anthropic/claude-opus-5-5",
@@ -4981,6 +4981,12 @@ Object.assign(DEFAULT_MODEL_ALIASES, {
4981
4981
  "nano-banana": "google/gemini-3-pro-image-preview",
4982
4982
  image: "openai-codex/gpt-image-2"
4983
4983
  });
4984
+ // JOHNNESS_PATCH_SONNET_55_MODEL: Claude Sonnet 5.5 is the newest Sonnet; `sonnet` moves to it and
4985
+ // Sonnet 5 keeps the versioned alias sonnet5.
4986
+ Object.assign(DEFAULT_MODEL_ALIASES, {
4987
+ sonnet: "anthropic/claude-sonnet-5-5",
4988
+ sonnet5: "anthropic/claude-sonnet-5"
4989
+ });
4984
4990
  // JOHNNESS_PATCH_OPUS_55_MODEL: Claude Opus 5.5 is the newest Opus; `opus` moves to it, opus-5 stays in the catalog.
4985
4991
  Object.assign(DEFAULT_MODEL_ALIASES, {
4986
4992
  opus: "anthropic/claude-opus-5-5",
package/dist/entry.js CHANGED
@@ -104,6 +104,7 @@ function applyCliProfileEnv(params) {
104
104
  if (profile === "dev" && !env.JOHNNESS_GATEWAY_PORT?.trim()) env.JOHNNESS_GATEWAY_PORT = "19001";
105
105
  }
106
106
  //#endregion
107
+ import v8 from "node:v8"; // JOHNNESS_PATCH_SESSION_STORE_MEMORY_81
107
108
  //#region src/cli/respawn-policy.ts
108
109
  function shouldSkipRespawnForArgv(argv) {
109
110
  return hasHelpOrVersion(argv);
@@ -326,14 +327,32 @@ if (!isMainModule({
326
327
  for (const arg of process$1.execArgv) if (arg === EXPERIMENTAL_WARNING_FLAG || arg === "--no-warnings") return true;
327
328
  return false;
328
329
  }
330
+ // JOHNNESS_PATCH_SESSION_STORE_MEMORY_81: gateway heap. Node's default old-space limit (~4 GB) was the OOM ceiling. The
331
+ // gateway runs with --max-old-space-size=min(8192, RAM/2) MB, only when that raises the limit.
332
+ // JOHNNESS_GATEWAY_MAX_OLD_SPACE_MB=<MB> sets it explicitly; 0/off disables. A heap flag already
333
+ // in NODE_OPTIONS or execArgv wins. Applied at launch, so existing service installs get it.
334
+ function resolveGatewayHeapArgs() {
335
+ if (!process$1.argv.slice(2).includes("gateway")) return [];
336
+ const nodeOptions = process$1.env.NODE_OPTIONS ?? "";
337
+ if (nodeOptions.includes("--max-old-space-size") || process$1.execArgv.some((arg) => arg.startsWith("--max-old-space-size"))) return [];
338
+ const raw = (process$1.env.JOHNNESS_GATEWAY_MAX_OLD_SPACE_MB ?? "").trim();
339
+ if (/^(0|off|false|no|none)$/i.test(raw)) return [];
340
+ const explicit = /^[1-9][0-9]*$/.test(raw) ? Number(raw) : void 0;
341
+ const mb = explicit ?? Math.min(8192, Math.floor(os.totalmem() / 1048576 / 2));
342
+ const currentMb = v8.getHeapStatistics().heap_size_limit / 1048576;
343
+ if (explicit === void 0 && !(mb > currentMb)) return [];
344
+ return [`--max-old-space-size=${mb}`];
345
+ }
329
346
  function ensureExperimentalWarningSuppressed() {
330
347
  if (shouldSkipRespawnForArgv(process$1.argv)) return false;
331
348
  if (isTruthyEnvValue(process$1.env.JOHNNESS_NO_RESPAWN)) return false;
332
349
  if (isTruthyEnvValue(process$1.env.JOHNNESS_NODE_OPTIONS_READY)) return false;
333
- if (hasExperimentalWarningSuppressed()) return false;
350
+ const gatewayHeapArgs = resolveGatewayHeapArgs();
351
+ if (hasExperimentalWarningSuppressed() && gatewayHeapArgs.length === 0) return false;
334
352
  process$1.env.JOHNNESS_NODE_OPTIONS_READY = "1";
335
353
  const child = spawn(process$1.execPath, [
336
354
  EXPERIMENTAL_WARNING_FLAG,
355
+ ...gatewayHeapArgs,
337
356
  ...process$1.execArgv,
338
357
  ...process$1.argv.slice(1)
339
358
  ], {
@@ -1686,6 +1686,12 @@ Object.assign(DEFAULT_MODEL_ALIASES, {
1686
1686
  "nano-banana": "google/gemini-3-pro-image-preview",
1687
1687
  image: "openai-codex/gpt-image-2"
1688
1688
  });
1689
+ // JOHNNESS_PATCH_SONNET_55_MODEL: Claude Sonnet 5.5 is the newest Sonnet; `sonnet` moves to it and
1690
+ // Sonnet 5 keeps the versioned alias sonnet5.
1691
+ Object.assign(DEFAULT_MODEL_ALIASES, {
1692
+ sonnet: "anthropic/claude-sonnet-5-5",
1693
+ sonnet5: "anthropic/claude-sonnet-5"
1694
+ });
1689
1695
  // JOHNNESS_PATCH_OPUS_55_MODEL: Claude Opus 5.5 is the newest Opus; `opus` moves to it, opus-5 stays in the catalog.
1690
1696
  Object.assign(DEFAULT_MODEL_ALIASES, {
1691
1697
  opus: "anthropic/claude-opus-5-5",
@@ -1661,6 +1661,12 @@ Object.assign(DEFAULT_MODEL_ALIASES, {
1661
1661
  "nano-banana": "google/gemini-3-pro-image-preview",
1662
1662
  image: "openai-codex/gpt-image-2"
1663
1663
  });
1664
+ // JOHNNESS_PATCH_SONNET_55_MODEL: Claude Sonnet 5.5 is the newest Sonnet; `sonnet` moves to it and
1665
+ // Sonnet 5 keeps the versioned alias sonnet5.
1666
+ Object.assign(DEFAULT_MODEL_ALIASES, {
1667
+ sonnet: "anthropic/claude-sonnet-5-5",
1668
+ sonnet5: "anthropic/claude-sonnet-5"
1669
+ });
1664
1670
  // JOHNNESS_PATCH_OPUS_55_MODEL: Claude Opus 5.5 is the newest Opus; `opus` moves to it, opus-5 stays in the catalog.
1665
1671
  Object.assign(DEFAULT_MODEL_ALIASES, {
1666
1672
  opus: "anthropic/claude-opus-5-5",
@@ -1592,6 +1592,12 @@ Object.assign(DEFAULT_MODEL_ALIASES, {
1592
1592
  "nano-banana": "google/gemini-3-pro-image-preview",
1593
1593
  image: "openai-codex/gpt-image-2"
1594
1594
  });
1595
+ // JOHNNESS_PATCH_SONNET_55_MODEL: Claude Sonnet 5.5 is the newest Sonnet; `sonnet` moves to it and
1596
+ // Sonnet 5 keeps the versioned alias sonnet5.
1597
+ Object.assign(DEFAULT_MODEL_ALIASES, {
1598
+ sonnet: "anthropic/claude-sonnet-5-5",
1599
+ sonnet5: "anthropic/claude-sonnet-5"
1600
+ });
1595
1601
  // JOHNNESS_PATCH_OPUS_55_MODEL: Claude Opus 5.5 is the newest Opus; `opus` moves to it, opus-5 stays in the catalog.
1596
1602
  Object.assign(DEFAULT_MODEL_ALIASES, {
1597
1603
  opus: "anthropic/claude-opus-5-5",
@@ -7467,7 +7467,15 @@ const log$2 = createSubsystemLogger("sessions/store");
7467
7467
  const DEFAULT_SESSION_PRUNE_AFTER_MS = 720 * 60 * 60 * 1e3;
7468
7468
  const DEFAULT_SESSION_MAX_ENTRIES = 500;
7469
7469
  const DEFAULT_SESSION_ROTATE_BYTES = 10485760;
7470
- const DEFAULT_SESSION_MAINTENANCE_MODE = "warn";
7470
+ const DEFAULT_SESSION_MAINTENANCE_MODE = "enforce"; // JOHNNESS_PATCH_SESSION_STORE_MEMORY_81: was "warn" (never pruned)
7471
+ const SESSION_MAINTENANCE_PROTECT_RECENT_MS = 24 * 60 * 60 * 1e3;
7472
+ function isProtectedSessionEntry(key, entry, opts = {}) {
7473
+ const active = typeof opts.activeSessionKey === "string" ? opts.activeSessionKey.trim() : "";
7474
+ if (active && key === active) return true;
7475
+ if (key === "main" || key === "global" || key.endsWith(":main")) return true;
7476
+ const updatedAt = entry?.updatedAt;
7477
+ return typeof updatedAt === "number" && Date.now() - updatedAt < SESSION_MAINTENANCE_PROTECT_RECENT_MS;
7478
+ }
7471
7479
  const DEFAULT_SESSION_DISK_BUDGET_HIGH_WATER_RATIO = .8;
7472
7480
  function resolvePruneAfterMs(maintenance) {
7473
7481
  const raw = maintenance?.pruneAfter ?? maintenance?.pruneDays;
@@ -7552,7 +7560,7 @@ function pruneStaleEntries(store, overrideMaxAgeMs, opts = {}) {
7552
7560
  const maxAgeMs = overrideMaxAgeMs ?? resolveMaintenanceConfig().pruneAfterMs;
7553
7561
  const cutoffMs = Date.now() - maxAgeMs;
7554
7562
  let pruned = 0;
7555
- for (const [key, entry] of Object.entries(store)) if (entry?.updatedAt != null && entry.updatedAt < cutoffMs) {
7563
+ for (const [key, entry] of Object.entries(store)) if (entry?.updatedAt != null && entry.updatedAt < cutoffMs && !isProtectedSessionEntry(key, entry, opts)) {
7556
7564
  opts.onPruned?.({
7557
7565
  key,
7558
7566
  entry
@@ -7601,7 +7609,7 @@ function capEntryCount(store, overrideMax, opts = {}) {
7601
7609
  const toRemove = keys.toSorted((a, b) => {
7602
7610
  const aTime = getEntryUpdatedAt(store[a]);
7603
7611
  return getEntryUpdatedAt(store[b]) - aTime;
7604
- }).slice(maxEntries);
7612
+ }).slice(maxEntries).filter((key) => !isProtectedSessionEntry(key, store[key], opts));
7605
7613
  for (const key of toRemove) {
7606
7614
  const entry = store[key];
7607
7615
  if (entry) opts.onCapped?.({
@@ -7753,6 +7761,59 @@ function normalizeSessionStore(store) {
7753
7761
  if (normalized !== entry) store[key] = normalized;
7754
7762
  }
7755
7763
  }
7764
+ // JOHNNESS_PATCH_SESSION_STORE_MEMORY_81: skillsSnapshot and systemPromptReport are rebuilt per process
7765
+ // (ensureSkillSnapshot, resolveCronSkillsSnapshot, resolveContextReport) and were 80%+ of
7766
+ // sessions.json. They live here, keyed by store path + session key + sessionId, never on disk.
7767
+ const SESSION_RUNTIME_FIELD_NAMES = ["skillsSnapshot", "systemPromptReport"];
7768
+ const SESSION_RUNTIME_FIELDS_KEEP_MS = 24 * 60 * 60 * 1e3;
7769
+ const SESSION_RUNTIME_FIELDS = /* @__PURE__ */ new Map();
7770
+ function stripSessionRuntimeFields(store) {
7771
+ let stripped = 0;
7772
+ for (const entry of Object.values(store)) {
7773
+ if (!entry || typeof entry !== "object") continue;
7774
+ for (const field of SESSION_RUNTIME_FIELD_NAMES) if (field in entry) {
7775
+ delete entry[field];
7776
+ stripped++;
7777
+ }
7778
+ }
7779
+ return stripped;
7780
+ }
7781
+ function attachSessionRuntimeFields(storePath, store) {
7782
+ const side = SESSION_RUNTIME_FIELDS.get(storePath);
7783
+ if (!side || side.size === 0) return store;
7784
+ for (const [key, kept] of side) {
7785
+ const entry = store[key];
7786
+ if (!entry || typeof entry !== "object" || entry.sessionId !== kept.sessionId) continue;
7787
+ for (const field of SESSION_RUNTIME_FIELD_NAMES) if (kept[field] !== void 0 && entry[field] === void 0) entry[field] = kept[field];
7788
+ }
7789
+ return store;
7790
+ }
7791
+ function persistableSessionStore(storePath, store) {
7792
+ const side = SESSION_RUNTIME_FIELDS.get(storePath) ?? /* @__PURE__ */ new Map();
7793
+ const cutoff = Date.now() - SESSION_RUNTIME_FIELDS_KEEP_MS;
7794
+ let out = store;
7795
+ for (const [key, entry] of Object.entries(store)) {
7796
+ if (!entry || typeof entry !== "object") continue;
7797
+ if (!SESSION_RUNTIME_FIELD_NAMES.some((field) => entry[field] !== void 0)) {
7798
+ side.delete(key);
7799
+ continue;
7800
+ }
7801
+ if (out === store) out = { ...store };
7802
+ const copy = { ...entry };
7803
+ const kept = { sessionId: entry.sessionId };
7804
+ for (const field of SESSION_RUNTIME_FIELD_NAMES) {
7805
+ if (copy[field] !== void 0) kept[field] = copy[field];
7806
+ delete copy[field];
7807
+ }
7808
+ out[key] = copy;
7809
+ if (typeof entry.updatedAt === "number" && entry.updatedAt < cutoff) side.delete(key);
7810
+ else side.set(key, kept);
7811
+ }
7812
+ for (const key of side.keys()) if (!store[key]) side.delete(key);
7813
+ if (side.size > 0) SESSION_RUNTIME_FIELDS.set(storePath, side);
7814
+ else SESSION_RUNTIME_FIELDS.delete(storePath);
7815
+ return out;
7816
+ }
7756
7817
  function loadSessionStore(storePath, opts = {}) {
7757
7818
  if (!opts.skipCache && isSessionStoreCacheEnabled()) {
7758
7819
  const currentFileStat = getFileStatSnapshot(storePath);
@@ -7762,7 +7823,7 @@ function loadSessionStore(storePath, opts = {}) {
7762
7823
  mtimeMs: currentFileStat?.mtimeMs,
7763
7824
  sizeBytes: currentFileStat?.sizeBytes
7764
7825
  });
7765
- if (cached) return cached;
7826
+ if (cached) return attachSessionRuntimeFields(storePath, cached);
7766
7827
  }
7767
7828
  let store = {};
7768
7829
  let fileStat = getFileStatSnapshot(storePath);
@@ -7793,6 +7854,7 @@ function loadSessionStore(storePath, opts = {}) {
7793
7854
  if (serializedFromDisk !== void 0) setSerializedSessionStore(storePath, serializedFromDisk);
7794
7855
  else setSerializedSessionStore(storePath, void 0);
7795
7856
  applySessionStoreMigrations(store);
7857
+ stripSessionRuntimeFields(store);
7796
7858
  if (!opts.skipCache && isSessionStoreCacheEnabled()) writeSessionStoreCache({
7797
7859
  storePath,
7798
7860
  store,
@@ -7800,7 +7862,8 @@ function loadSessionStore(storePath, opts = {}) {
7800
7862
  sizeBytes: fileStat?.sizeBytes,
7801
7863
  serialized: serializedFromDisk
7802
7864
  });
7803
- return structuredClone(store);
7865
+ // JOHNNESS_PATCH_SESSION_STORE_MEMORY_81: `store` was parsed just now and the cache holds its own clone.
7866
+ return attachSessionRuntimeFields(storePath, store);
7804
7867
  }
7805
7868
  function readSessionUpdatedAt(params) {
7806
7869
  try {
@@ -7874,10 +7937,10 @@ async function saveSessionStoreUnlocked(storePath, store, opts) {
7874
7937
  });
7875
7938
  } else {
7876
7939
  const removedSessionFiles = /* @__PURE__ */ new Map();
7877
- const pruned = pruneStaleEntries(store, maintenance.pruneAfterMs, { onPruned: ({ entry }) => {
7940
+ const pruned = pruneStaleEntries(store, maintenance.pruneAfterMs, { activeSessionKey: opts?.activeSessionKey, onPruned: ({ entry }) => {
7878
7941
  rememberRemovedSessionFile(removedSessionFiles, entry);
7879
7942
  } });
7880
- const capped = capEntryCount(store, maintenance.maxEntries, { onCapped: ({ entry }) => {
7943
+ const capped = capEntryCount(store, maintenance.maxEntries, { activeSessionKey: opts?.activeSessionKey, onCapped: ({ entry }) => {
7881
7944
  rememberRemovedSessionFile(removedSessionFiles, entry);
7882
7945
  } });
7883
7946
  const archivedDirs = /* @__PURE__ */ new Set();
@@ -7922,7 +7985,9 @@ async function saveSessionStoreUnlocked(storePath, store, opts) {
7922
7985
  }
7923
7986
  }
7924
7987
  await fs$1.promises.mkdir(path.dirname(storePath), { recursive: true });
7925
- const json = JSON.stringify(store, null, 2);
7988
+ // JOHNNESS_PATCH_SESSION_STORE_MEMORY_81: persist without runtime fields, compact (was 2-space indent).
7989
+ store = persistableSessionStore(storePath, store);
7990
+ const json = JSON.stringify(store);
7926
7991
  if (getSerializedSessionStore(storePath) === json) {
7927
7992
  updateSessionStoreWriteCaches({
7928
7993
  storePath,
@@ -7974,6 +8039,9 @@ async function saveSessionStoreUnlocked(storePath, store, opts) {
7974
8039
  }
7975
8040
  async function updateSessionStore(storePath, mutator, opts) {
7976
8041
  return await withSessionStoreLock(storePath, async () => {
8042
+ // JOHNNESS_PATCH_SESSION_STORE_MEMORY_81: skipCache stays. This runs under the cross-process file lock and the cache is
8043
+ // validated only by mtime + size, so a same-size write by another process could be missed.
8044
+ // The re-read is ~3 MB now that runtime fields are not persisted.
7977
8045
  const store = loadSessionStore(storePath, { skipCache: true });
7978
8046
  const result = await mutator(store);
7979
8047
  await saveSessionStoreUnlocked(storePath, store, opts);
@@ -7464,7 +7464,15 @@ const log$2 = createSubsystemLogger("sessions/store");
7464
7464
  const DEFAULT_SESSION_PRUNE_AFTER_MS = 720 * 60 * 60 * 1e3;
7465
7465
  const DEFAULT_SESSION_MAX_ENTRIES = 500;
7466
7466
  const DEFAULT_SESSION_ROTATE_BYTES = 10485760;
7467
- const DEFAULT_SESSION_MAINTENANCE_MODE = "warn";
7467
+ const DEFAULT_SESSION_MAINTENANCE_MODE = "enforce"; // JOHNNESS_PATCH_SESSION_STORE_MEMORY_81: was "warn" (never pruned)
7468
+ const SESSION_MAINTENANCE_PROTECT_RECENT_MS = 24 * 60 * 60 * 1e3;
7469
+ function isProtectedSessionEntry(key, entry, opts = {}) {
7470
+ const active = typeof opts.activeSessionKey === "string" ? opts.activeSessionKey.trim() : "";
7471
+ if (active && key === active) return true;
7472
+ if (key === "main" || key === "global" || key.endsWith(":main")) return true;
7473
+ const updatedAt = entry?.updatedAt;
7474
+ return typeof updatedAt === "number" && Date.now() - updatedAt < SESSION_MAINTENANCE_PROTECT_RECENT_MS;
7475
+ }
7468
7476
  const DEFAULT_SESSION_DISK_BUDGET_HIGH_WATER_RATIO = .8;
7469
7477
  function resolvePruneAfterMs(maintenance) {
7470
7478
  const raw = maintenance?.pruneAfter ?? maintenance?.pruneDays;
@@ -7549,7 +7557,7 @@ function pruneStaleEntries(store, overrideMaxAgeMs, opts = {}) {
7549
7557
  const maxAgeMs = overrideMaxAgeMs ?? resolveMaintenanceConfig().pruneAfterMs;
7550
7558
  const cutoffMs = Date.now() - maxAgeMs;
7551
7559
  let pruned = 0;
7552
- for (const [key, entry] of Object.entries(store)) if (entry?.updatedAt != null && entry.updatedAt < cutoffMs) {
7560
+ for (const [key, entry] of Object.entries(store)) if (entry?.updatedAt != null && entry.updatedAt < cutoffMs && !isProtectedSessionEntry(key, entry, opts)) {
7553
7561
  opts.onPruned?.({
7554
7562
  key,
7555
7563
  entry
@@ -7598,7 +7606,7 @@ function capEntryCount(store, overrideMax, opts = {}) {
7598
7606
  const toRemove = keys.toSorted((a, b) => {
7599
7607
  const aTime = getEntryUpdatedAt(store[a]);
7600
7608
  return getEntryUpdatedAt(store[b]) - aTime;
7601
- }).slice(maxEntries);
7609
+ }).slice(maxEntries).filter((key) => !isProtectedSessionEntry(key, store[key], opts));
7602
7610
  for (const key of toRemove) {
7603
7611
  const entry = store[key];
7604
7612
  if (entry) opts.onCapped?.({
@@ -7750,6 +7758,59 @@ function normalizeSessionStore(store) {
7750
7758
  if (normalized !== entry) store[key] = normalized;
7751
7759
  }
7752
7760
  }
7761
+ // JOHNNESS_PATCH_SESSION_STORE_MEMORY_81: skillsSnapshot and systemPromptReport are rebuilt per process
7762
+ // (ensureSkillSnapshot, resolveCronSkillsSnapshot, resolveContextReport) and were 80%+ of
7763
+ // sessions.json. They live here, keyed by store path + session key + sessionId, never on disk.
7764
+ const SESSION_RUNTIME_FIELD_NAMES = ["skillsSnapshot", "systemPromptReport"];
7765
+ const SESSION_RUNTIME_FIELDS_KEEP_MS = 24 * 60 * 60 * 1e3;
7766
+ const SESSION_RUNTIME_FIELDS = /* @__PURE__ */ new Map();
7767
+ function stripSessionRuntimeFields(store) {
7768
+ let stripped = 0;
7769
+ for (const entry of Object.values(store)) {
7770
+ if (!entry || typeof entry !== "object") continue;
7771
+ for (const field of SESSION_RUNTIME_FIELD_NAMES) if (field in entry) {
7772
+ delete entry[field];
7773
+ stripped++;
7774
+ }
7775
+ }
7776
+ return stripped;
7777
+ }
7778
+ function attachSessionRuntimeFields(storePath, store) {
7779
+ const side = SESSION_RUNTIME_FIELDS.get(storePath);
7780
+ if (!side || side.size === 0) return store;
7781
+ for (const [key, kept] of side) {
7782
+ const entry = store[key];
7783
+ if (!entry || typeof entry !== "object" || entry.sessionId !== kept.sessionId) continue;
7784
+ for (const field of SESSION_RUNTIME_FIELD_NAMES) if (kept[field] !== void 0 && entry[field] === void 0) entry[field] = kept[field];
7785
+ }
7786
+ return store;
7787
+ }
7788
+ function persistableSessionStore(storePath, store) {
7789
+ const side = SESSION_RUNTIME_FIELDS.get(storePath) ?? /* @__PURE__ */ new Map();
7790
+ const cutoff = Date.now() - SESSION_RUNTIME_FIELDS_KEEP_MS;
7791
+ let out = store;
7792
+ for (const [key, entry] of Object.entries(store)) {
7793
+ if (!entry || typeof entry !== "object") continue;
7794
+ if (!SESSION_RUNTIME_FIELD_NAMES.some((field) => entry[field] !== void 0)) {
7795
+ side.delete(key);
7796
+ continue;
7797
+ }
7798
+ if (out === store) out = { ...store };
7799
+ const copy = { ...entry };
7800
+ const kept = { sessionId: entry.sessionId };
7801
+ for (const field of SESSION_RUNTIME_FIELD_NAMES) {
7802
+ if (copy[field] !== void 0) kept[field] = copy[field];
7803
+ delete copy[field];
7804
+ }
7805
+ out[key] = copy;
7806
+ if (typeof entry.updatedAt === "number" && entry.updatedAt < cutoff) side.delete(key);
7807
+ else side.set(key, kept);
7808
+ }
7809
+ for (const key of side.keys()) if (!store[key]) side.delete(key);
7810
+ if (side.size > 0) SESSION_RUNTIME_FIELDS.set(storePath, side);
7811
+ else SESSION_RUNTIME_FIELDS.delete(storePath);
7812
+ return out;
7813
+ }
7753
7814
  function loadSessionStore(storePath, opts = {}) {
7754
7815
  if (!opts.skipCache && isSessionStoreCacheEnabled()) {
7755
7816
  const currentFileStat = getFileStatSnapshot(storePath);
@@ -7759,7 +7820,7 @@ function loadSessionStore(storePath, opts = {}) {
7759
7820
  mtimeMs: currentFileStat?.mtimeMs,
7760
7821
  sizeBytes: currentFileStat?.sizeBytes
7761
7822
  });
7762
- if (cached) return cached;
7823
+ if (cached) return attachSessionRuntimeFields(storePath, cached);
7763
7824
  }
7764
7825
  let store = {};
7765
7826
  let fileStat = getFileStatSnapshot(storePath);
@@ -7790,6 +7851,7 @@ function loadSessionStore(storePath, opts = {}) {
7790
7851
  if (serializedFromDisk !== void 0) setSerializedSessionStore(storePath, serializedFromDisk);
7791
7852
  else setSerializedSessionStore(storePath, void 0);
7792
7853
  applySessionStoreMigrations(store);
7854
+ stripSessionRuntimeFields(store);
7793
7855
  if (!opts.skipCache && isSessionStoreCacheEnabled()) writeSessionStoreCache({
7794
7856
  storePath,
7795
7857
  store,
@@ -7797,7 +7859,8 @@ function loadSessionStore(storePath, opts = {}) {
7797
7859
  sizeBytes: fileStat?.sizeBytes,
7798
7860
  serialized: serializedFromDisk
7799
7861
  });
7800
- return structuredClone(store);
7862
+ // JOHNNESS_PATCH_SESSION_STORE_MEMORY_81: `store` was parsed just now and the cache holds its own clone.
7863
+ return attachSessionRuntimeFields(storePath, store);
7801
7864
  }
7802
7865
  function readSessionUpdatedAt(params) {
7803
7866
  try {
@@ -7871,10 +7934,10 @@ async function saveSessionStoreUnlocked(storePath, store, opts) {
7871
7934
  });
7872
7935
  } else {
7873
7936
  const removedSessionFiles = /* @__PURE__ */ new Map();
7874
- const pruned = pruneStaleEntries(store, maintenance.pruneAfterMs, { onPruned: ({ entry }) => {
7937
+ const pruned = pruneStaleEntries(store, maintenance.pruneAfterMs, { activeSessionKey: opts?.activeSessionKey, onPruned: ({ entry }) => {
7875
7938
  rememberRemovedSessionFile(removedSessionFiles, entry);
7876
7939
  } });
7877
- const capped = capEntryCount(store, maintenance.maxEntries, { onCapped: ({ entry }) => {
7940
+ const capped = capEntryCount(store, maintenance.maxEntries, { activeSessionKey: opts?.activeSessionKey, onCapped: ({ entry }) => {
7878
7941
  rememberRemovedSessionFile(removedSessionFiles, entry);
7879
7942
  } });
7880
7943
  const archivedDirs = /* @__PURE__ */ new Set();
@@ -7919,7 +7982,9 @@ async function saveSessionStoreUnlocked(storePath, store, opts) {
7919
7982
  }
7920
7983
  }
7921
7984
  await syncFs.promises.mkdir(path.dirname(storePath), { recursive: true });
7922
- const json = JSON.stringify(store, null, 2);
7985
+ // JOHNNESS_PATCH_SESSION_STORE_MEMORY_81: persist without runtime fields, compact (was 2-space indent).
7986
+ store = persistableSessionStore(storePath, store);
7987
+ const json = JSON.stringify(store);
7923
7988
  if (getSerializedSessionStore(storePath) === json) {
7924
7989
  updateSessionStoreWriteCaches({
7925
7990
  storePath,
@@ -7976,6 +8041,9 @@ async function saveSessionStore(storePath, store, opts) {
7976
8041
  }
7977
8042
  async function updateSessionStore(storePath, mutator, opts) {
7978
8043
  return await withSessionStoreLock(storePath, async () => {
8044
+ // JOHNNESS_PATCH_SESSION_STORE_MEMORY_81: skipCache stays. This runs under the cross-process file lock and the cache is
8045
+ // validated only by mtime + size, so a same-size write by another process could be missed.
8046
+ // The re-read is ~3 MB now that runtime fields are not persisted.
7979
8047
  const store = loadSessionStore(storePath, { skipCache: true });
7980
8048
  const result = await mutator(store);
7981
8049
  await saveSessionStoreUnlocked(storePath, store, opts);
@@ -7079,6 +7079,12 @@ Object.assign(DEFAULT_MODEL_ALIASES, {
7079
7079
  "nano-banana": "google/gemini-3-pro-image-preview",
7080
7080
  image: "openai-codex/gpt-image-2"
7081
7081
  });
7082
+ // JOHNNESS_PATCH_SONNET_55_MODEL: Claude Sonnet 5.5 is the newest Sonnet; `sonnet` moves to it and
7083
+ // Sonnet 5 keeps the versioned alias sonnet5.
7084
+ Object.assign(DEFAULT_MODEL_ALIASES, {
7085
+ sonnet: "anthropic/claude-sonnet-5-5",
7086
+ sonnet5: "anthropic/claude-sonnet-5"
7087
+ });
7082
7088
  // JOHNNESS_PATCH_OPUS_55_MODEL: Claude Opus 5.5 is the newest Opus; `opus` moves to it, opus-5 stays in the catalog.
7083
7089
  Object.assign(DEFAULT_MODEL_ALIASES, {
7084
7090
  opus: "anthropic/claude-opus-5-5",
@@ -8080,6 +8080,12 @@ Object.assign(DEFAULT_MODEL_ALIASES, {
8080
8080
  "nano-banana": "google/gemini-3-pro-image-preview",
8081
8081
  image: "openai-codex/gpt-image-2"
8082
8082
  });
8083
+ // JOHNNESS_PATCH_SONNET_55_MODEL: Claude Sonnet 5.5 is the newest Sonnet; `sonnet` moves to it and
8084
+ // Sonnet 5 keeps the versioned alias sonnet5.
8085
+ Object.assign(DEFAULT_MODEL_ALIASES, {
8086
+ sonnet: "anthropic/claude-sonnet-5-5",
8087
+ sonnet5: "anthropic/claude-sonnet-5"
8088
+ });
8083
8089
  // JOHNNESS_PATCH_OPUS_55_MODEL: Claude Opus 5.5 is the newest Opus; `opus` moves to it, opus-5 stays in the catalog.
8084
8090
  Object.assign(DEFAULT_MODEL_ALIASES, {
8085
8091
  opus: "anthropic/claude-opus-5-5",
@@ -5209,6 +5209,12 @@ Object.assign(DEFAULT_MODEL_ALIASES, {
5209
5209
  "nano-banana": "google/gemini-3-pro-image-preview",
5210
5210
  image: "openai-codex/gpt-image-2"
5211
5211
  });
5212
+ // JOHNNESS_PATCH_SONNET_55_MODEL: Claude Sonnet 5.5 is the newest Sonnet; `sonnet` moves to it and
5213
+ // Sonnet 5 keeps the versioned alias sonnet5.
5214
+ Object.assign(DEFAULT_MODEL_ALIASES, {
5215
+ sonnet: "anthropic/claude-sonnet-5-5",
5216
+ sonnet5: "anthropic/claude-sonnet-5"
5217
+ });
5212
5218
  // JOHNNESS_PATCH_OPUS_55_MODEL: Claude Opus 5.5 is the newest Opus; `opus` moves to it, opus-5 stays in the catalog.
5213
5219
  Object.assign(DEFAULT_MODEL_ALIASES, {
5214
5220
  opus: "anthropic/claude-opus-5-5",
@@ -3135,6 +3135,7 @@ const XHIGH_MODEL_REFS = [
3135
3135
  "anthropic/claude-fable-5-1",
3136
3136
  "meta/muse-spark-1.3", // JOHNNESS_PATCH_MUSE_SPARK
3137
3137
  "meta/muse-spark-1.2",
3138
+ "anthropic/claude-sonnet-5-5", // JOHNNESS_PATCH_SONNET_55_MODEL
3138
3139
  "anthropic/claude-opus-5-5", // JOHNNESS_PATCH_OPUS_55_MODEL
3139
3140
  "xai/grok-4.7", // JOHNNESS_PATCH_DAILY_MODELS_80
3140
3141
  "openai/gpt-6-sol",
@@ -6000,6 +6000,12 @@ Object.assign(DEFAULT_MODEL_ALIASES, {
6000
6000
  "nano-banana": "google/gemini-3-pro-image-preview",
6001
6001
  image: "openai-codex/gpt-image-2"
6002
6002
  });
6003
+ // JOHNNESS_PATCH_SONNET_55_MODEL: Claude Sonnet 5.5 is the newest Sonnet; `sonnet` moves to it and
6004
+ // Sonnet 5 keeps the versioned alias sonnet5.
6005
+ Object.assign(DEFAULT_MODEL_ALIASES, {
6006
+ sonnet: "anthropic/claude-sonnet-5-5",
6007
+ sonnet5: "anthropic/claude-sonnet-5"
6008
+ });
6003
6009
  // JOHNNESS_PATCH_OPUS_55_MODEL: Claude Opus 5.5 is the newest Opus; `opus` moves to it, opus-5 stays in the catalog.
6004
6010
  Object.assign(DEFAULT_MODEL_ALIASES, {
6005
6011
  opus: "anthropic/claude-opus-5-5",
@@ -4969,6 +4969,12 @@ Object.assign(DEFAULT_MODEL_ALIASES, {
4969
4969
  "nano-banana": "google/gemini-3-pro-image-preview",
4970
4970
  image: "openai-codex/gpt-image-2"
4971
4971
  });
4972
+ // JOHNNESS_PATCH_SONNET_55_MODEL: Claude Sonnet 5.5 is the newest Sonnet; `sonnet` moves to it and
4973
+ // Sonnet 5 keeps the versioned alias sonnet5.
4974
+ Object.assign(DEFAULT_MODEL_ALIASES, {
4975
+ sonnet: "anthropic/claude-sonnet-5-5",
4976
+ sonnet5: "anthropic/claude-sonnet-5"
4977
+ });
4972
4978
  // JOHNNESS_PATCH_OPUS_55_MODEL: Claude Opus 5.5 is the newest Opus; `opus` moves to it, opus-5 stays in the catalog.
4973
4979
  Object.assign(DEFAULT_MODEL_ALIASES, {
4974
4980
  opus: "anthropic/claude-opus-5-5",
@@ -4973,6 +4973,12 @@ Object.assign(DEFAULT_MODEL_ALIASES, {
4973
4973
  "nano-banana": "google/gemini-3-pro-image-preview",
4974
4974
  image: "openai-codex/gpt-image-2"
4975
4975
  });
4976
+ // JOHNNESS_PATCH_SONNET_55_MODEL: Claude Sonnet 5.5 is the newest Sonnet; `sonnet` moves to it and
4977
+ // Sonnet 5 keeps the versioned alias sonnet5.
4978
+ Object.assign(DEFAULT_MODEL_ALIASES, {
4979
+ sonnet: "anthropic/claude-sonnet-5-5",
4980
+ sonnet5: "anthropic/claude-sonnet-5"
4981
+ });
4976
4982
  // JOHNNESS_PATCH_OPUS_55_MODEL: Claude Opus 5.5 is the newest Opus; `opus` moves to it, opus-5 stays in the catalog.
4977
4983
  Object.assign(DEFAULT_MODEL_ALIASES, {
4978
4984
  opus: "anthropic/claude-opus-5-5",
@@ -4973,6 +4973,12 @@ Object.assign(DEFAULT_MODEL_ALIASES, {
4973
4973
  "nano-banana": "google/gemini-3-pro-image-preview",
4974
4974
  image: "openai-codex/gpt-image-2"
4975
4975
  });
4976
+ // JOHNNESS_PATCH_SONNET_55_MODEL: Claude Sonnet 5.5 is the newest Sonnet; `sonnet` moves to it and
4977
+ // Sonnet 5 keeps the versioned alias sonnet5.
4978
+ Object.assign(DEFAULT_MODEL_ALIASES, {
4979
+ sonnet: "anthropic/claude-sonnet-5-5",
4980
+ sonnet5: "anthropic/claude-sonnet-5"
4981
+ });
4976
4982
  // JOHNNESS_PATCH_OPUS_55_MODEL: Claude Opus 5.5 is the newest Opus; `opus` moves to it, opus-5 stays in the catalog.
4977
4983
  Object.assign(DEFAULT_MODEL_ALIASES, {
4978
4984
  opus: "anthropic/claude-opus-5-5",
@@ -4782,6 +4782,12 @@ Object.assign(DEFAULT_MODEL_ALIASES, {
4782
4782
  "nano-banana": "google/gemini-3-pro-image-preview",
4783
4783
  image: "openai-codex/gpt-image-2"
4784
4784
  });
4785
+ // JOHNNESS_PATCH_SONNET_55_MODEL: Claude Sonnet 5.5 is the newest Sonnet; `sonnet` moves to it and
4786
+ // Sonnet 5 keeps the versioned alias sonnet5.
4787
+ Object.assign(DEFAULT_MODEL_ALIASES, {
4788
+ sonnet: "anthropic/claude-sonnet-5-5",
4789
+ sonnet5: "anthropic/claude-sonnet-5"
4790
+ });
4785
4791
  // JOHNNESS_PATCH_OPUS_55_MODEL: Claude Opus 5.5 is the newest Opus; `opus` moves to it, opus-5 stays in the catalog.
4786
4792
  Object.assign(DEFAULT_MODEL_ALIASES, {
4787
4793
  opus: "anthropic/claude-opus-5-5",