johns-harness 2026.9.26 → 2026.9.27

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 2026.9.27
4
+
5
+ - Family 74: the local active-session pressure guard compacts on exactly one trigger, estimated active-context tokens against the active model's declared context window (`ceil(0.7 x window)`, floored at `max(compaction.reserveTokensFloor, 50000)`; undeclared windows fall back to the runtime default, `DEFAULT_CONTEXT_TOKENS`). The fixed 1 MB transcript-bytes, 220-message and 3 GiB heap backstops are removed as triggers, so a session with 221 small messages and 528 KB no longer compacts early while a large context still compacts on real token pressure. The 5-minute cooldown, the 24-message floor and the failed-attempt growth gate remain as anti-thrash throttles, and the emergency compaction path is untouched. `contextRatio`, `minIntervalMs` and `minMessagesAfterCompaction` are the config surface; legacy `transcriptBytes`/`messageCount`/`heapUsedBytes` keys are accepted and ignored.
6
+ - Roll back with `npm i -g johns-harness@2026.9.26`.
7
+
3
8
  ## 2026.9.26
4
9
 
5
10
  - Family 73: the `content-story`, `dashboard` and `cold-outreach` skills are removed from the bundled set on request, leaving eight generic bundled skills (`code-ops`, `web-research`, `plan-archival`, `todos`, `arch-viz`, `gcp-logs`, `activity-logging`, `outlook`). They load by description match exactly as before, workspace copies override them, and `enabled: false` and `allowBundled` still apply. Nothing else changes from 2026.9.25.
package/README.md CHANGED
@@ -85,8 +85,8 @@ node johnness.mjs --help
85
85
  ### Verify
86
86
 
87
87
  ```bash
88
- npm test # 914 tests against the installed dependency tree
89
- bash scripts/verify-patches.sh # 455 checks across every patch family
88
+ npm test # 915 tests against the installed dependency tree
89
+ bash scripts/verify-patches.sh # 459 checks across every patch family
90
90
  ```
91
91
 
92
92
  ## Launch a swarm with one command
@@ -132,7 +132,7 @@ Coding agents today run as isolated processes. Two agents on the same repository
132
132
  - **Memory built for production.** Tiered recall that traces from consolidated reflections down to the verbatim record, surviving compaction and shared across the swarm. Installed and enabled by default.
133
133
  - **Production authority per commit.** Promotion is granted to an exact SHA, never to a branch name, so one worker's authorization never covers another worker's commits.
134
134
  - **An append-only action log.** Agents record every consequential action to `ACTION_LOG.md` and read it before acting.
135
- - **A fixed runtime surface.** The core does not grow on a schedule. New mechanisms enter only to solve a demonstrated production failure, one narrow, tested, reversible patch at a time: 73 patch families, each with a regression test and a rollback path.
135
+ - **A fixed runtime surface.** The core does not grow on a schedule. New mechanisms enter only to solve a demonstrated production failure, one narrow, tested, reversible patch at a time: 74 patch families, each with a regression test and a rollback path.
136
136
  - **Reliability with a definition.** Bounded retries, explicit failure states, exact-once continuation, context-pressure recovery, durable delivery, and supervised gateway recovery.
137
137
  - **Coding first, phone optional.** Direct the work at a high level from a chat channel while agents plan, implement, test, coordinate, and stage releases unattended.
138
138
  - **Local voice.** Optional native Whisper transcription of inbound audio, entirely on your machine. Off by default. [Setup](docs/NATIVE-WHISPER-MUSE.md).
@@ -199,7 +199,7 @@ The log is a [`RULES.md`](RULES.md) workspace convention enforced through the ac
199
199
 
200
200
  ## Patch families
201
201
 
202
- The runtime carries 73 patch families plus the 65.1 safe-update amendment. Each is a production fix applied at the source with a regression test, a durable patch marker, and a rollback path. A verifier runs 455 checks against the package tree and installed dependencies, and 914 tests run against the installed dependency tree.
202
+ The runtime carries 74 patch families plus the 65.1 safe-update amendment. Each is a production fix applied at the source with a regression test, a durable patch marker, and a rollback path. A verifier runs 459 checks against the package tree and installed dependencies, and 915 tests run against the installed dependency tree.
203
203
 
204
204
  Every family follows the same six steps: reproduce the failure, trace the exact runtime path, make the smallest source-level change that restores the invariant, add a regression test and a patch marker, run the verifier, and retain rollback artifacts. The full index: [`docs/PATCHES.md`](docs/PATCHES.md).
205
205
 
@@ -218,7 +218,7 @@ Every family follows the same six steps: reproduce the failure, trace the exact
218
218
  ## Documentation
219
219
 
220
220
  - [`docs/index.md`](docs/index.md): runtime docs, from gateway and channels to plugins, nodes, and CLI reference.
221
- - [`docs/PATCHES.md`](docs/PATCHES.md): the 73 patch families, one record each.
221
+ - [`docs/PATCHES.md`](docs/PATCHES.md): the 74 patch families, one record each.
222
222
  - [`docs/NATIVE-WHISPER-MUSE.md`](docs/NATIVE-WHISPER-MUSE.md): local Whisper transcription setup.
223
223
  - [`RULES.md`](RULES.md): the default operating rules, including Rule Zero on production authorization.
224
224
 
@@ -7894,9 +7894,7 @@ function resolveTranscriptPolicy(params) {
7894
7894
 
7895
7895
  //#region johnness local active-session pressure guard
7896
7896
  const LOCAL_PRESSURE_GUARD_DEFAULTS = {
7897
- transcriptBytes: 1048576,
7898
- messageCount: 220,
7899
- heapUsedBytes: 3221225472,
7897
+ contextRatio: 0.7,
7900
7898
  minIntervalMs: 300000,
7901
7899
  minMessagesAfterCompaction: 24
7902
7900
  };
@@ -7907,14 +7905,24 @@ function localPressureGuardNumber(value, fallback) {
7907
7905
  function localPressureGuardSettings(cfg) {
7908
7906
  const raw = cfg?.agents?.defaults?.activeSessionPressure ?? cfg?.session?.activePressure ?? {};
7909
7907
  if (raw?.enabled === false) return null;
7908
+ /* JOHNNESS_PATCH_CONTEXT_WINDOW_COMPACTION: legacy transcriptBytes/messageCount/heapUsedBytes keys are accepted without error and ignored. Config surface: enabled, contextRatio, minIntervalMs, minMessagesAfterCompaction. */
7909
+ const contextRatio = localPressureGuardNumber(raw.contextRatio, LOCAL_PRESSURE_GUARD_DEFAULTS.contextRatio);
7910
7910
  return {
7911
- transcriptBytes: localPressureGuardNumber(raw.transcriptBytes ?? raw.maxTranscriptBytes, LOCAL_PRESSURE_GUARD_DEFAULTS.transcriptBytes),
7912
- messageCount: localPressureGuardNumber(raw.messageCount ?? raw.maxMessages, LOCAL_PRESSURE_GUARD_DEFAULTS.messageCount),
7913
- heapUsedBytes: localPressureGuardNumber(raw.heapUsedBytes ?? raw.maxHeapUsedBytes, LOCAL_PRESSURE_GUARD_DEFAULTS.heapUsedBytes),
7911
+ contextRatio: contextRatio > 0 ? contextRatio : LOCAL_PRESSURE_GUARD_DEFAULTS.contextRatio,
7914
7912
  minIntervalMs: localPressureGuardNumber(raw.minIntervalMs, LOCAL_PRESSURE_GUARD_DEFAULTS.minIntervalMs),
7915
7913
  minMessagesAfterCompaction: localPressureGuardNumber(raw.minMessagesAfterCompaction, LOCAL_PRESSURE_GUARD_DEFAULTS.minMessagesAfterCompaction)
7916
7914
  };
7917
7915
  }
7916
+ /* JOHNNESS_PATCH_CONTEXT_WINDOW_COMPACTION: single pressure trigger — estimated active-context tokens vs the active model's declared context window; the window falls back to DEFAULT_CONTEXT_TOKENS when undeclared. See docs/PATCHES.md family 74. */
7917
+ function localPressureGuardContextWindowTokens(model) {
7918
+ const declared = typeof model?.contextWindow === "number" && Number.isFinite(model.contextWindow) ? Math.floor(model.contextWindow) : 0;
7919
+ return declared > 0 ? declared : DEFAULT_CONTEXT_TOKENS;
7920
+ }
7921
+ function localPressureGuardContextThreshold(settings, contextWindowTokens, cfg) {
7922
+ const reserveTokensFloor = localPressureGuardNumber(cfg?.agents?.defaults?.compaction?.reserveTokensFloor, 2e4);
7923
+ const floor = Math.max(reserveTokensFloor, 50000);
7924
+ return Math.max(Math.ceil(settings.contextRatio * contextWindowTokens), floor);
7925
+ }
7918
7926
  /* JOHNNESS_PATCH_APPEND_ONLY_PRESSURE_BASELINE: derive a restart-safe byte baseline from the append-only JSONL so transcriptBytes pressure measures NEW growth since the last successful compaction rather than total (never-shrinking) file size. See docs/PATCHES.md family 22. Scans getEntries() newest->oldest for the newest SUCCESSFUL johnness.active-pressure-refresh marker (failed !== true) and returns its recorded post-compaction size (postCompactionBytes, else transcriptBytes). Returns 0 for legacy sessions with no such marker, so a first over-limit session still compacts once. */
7919
7927
  function computeActivePressureBaselineBytes(sessionManager) {
7920
7928
  try {
@@ -8072,12 +8080,12 @@ function compactionRotationNumber(value, fallback) {
8072
8080
  return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
8073
8081
  }
8074
8082
  function compactionRotationSettings(cfg) {
8075
- const pressure = localPressureGuardSettings(cfg) ?? { transcriptBytes: 1048576 };
8076
8083
  const raw = cfg?.agents?.defaults?.activeSessionPressure ?? cfg?.session?.activePressure ?? {};
8077
8084
  if (raw?.rotateAfterCompaction === false) return null;
8078
- const threshold = compactionRotationNumber(pressure?.transcriptBytes, 1048576);
8085
+ /* JOHNNESS_PATCH_CONTEXT_WINDOW_COMPACTION: the legacy transcriptBytes threshold no longer exists; the rotation size budget keeps its historical 1 MiB-derived default so default rotation behavior is unchanged. */
8086
+ const threshold = 1048576;
8079
8087
  const configuredMax = compactionRotationNumber(raw.rotationMaxBytes, 0);
8080
- const maxBytes = configuredMax > 0 ? configuredMax : threshold > 0 ? Math.max(131072, Math.min(524288, Math.floor(threshold * .45))) : 524288;
8088
+ const maxBytes = configuredMax > 0 ? configuredMax : Math.max(131072, Math.min(524288, Math.floor(threshold * .45)));
8081
8089
  return {
8082
8090
  maxBytes,
8083
8091
  maxMessages: Math.max(4, Math.floor(compactionRotationNumber(raw.rotationMaxMessages, 80))),
@@ -8213,18 +8221,20 @@ async function maybeRefreshActiveSessionForPressure(params) {
8213
8221
  const heapUsedBytes = typeof process?.memoryUsage === "function" ? process.memoryUsage().heapUsed : 0;
8214
8222
  const __hasCompactedContext = Array.isArray(params.session.messages) && params.session.messages.some((message) => message?.role === "compactionSummary");
8215
8223
  const __alreadyRotated = params.sessionManager.getEntries?.().some((entry) => entry?.type === "custom" && entry?.customType === "johnness.compaction-session-rotation");
8216
- if (settings.transcriptBytes > 0 && transcriptBytes >= settings.transcriptBytes && __hasCompactedContext && !__alreadyRotated) {
8224
+ /* JOHNNESS_PATCH_CONTEXT_WINDOW_COMPACTION: the existing-compacted-lineage rotation follows the same single context-token trigger; the byte gate is gone (family 74). */
8225
+ const __cwcWindowTokens = localPressureGuardContextWindowTokens(params.session?.model);
8226
+ const __cwcThresholdTokens = localPressureGuardContextThreshold(settings, __cwcWindowTokens, params.config);
8227
+ const __cwcEstimatedTokens = estimateMessagesTokens(messages);
8228
+ if (__cwcEstimatedTokens >= __cwcThresholdTokens && __hasCompactedContext && !__alreadyRotated) {
8217
8229
  const __existingRotation = await rotateCompactedSessionLineage({ config: params.config, session: params.session, sessionManager: params.sessionManager, sessionFile: params.sessionFile, sessionKey: params.sessionKey, sessionId: params.sessionId, reason: "existing-compacted-lineage" });
8218
8230
  if (__existingRotation.rotated) return { refreshed: true, rotated: true, reasons: ["existing-compacted-lineage"], transcriptBytes: __existingRotation.transcriptBytes, messageCount: __existingRotation.messageCount, heapUsedBytes, sessionId: __existingRotation.sessionId, sessionFile: __existingRotation.sessionFile, oldSessionId: __existingRotation.oldSessionId, oldSessionFile: __existingRotation.oldSessionFile };
8219
8231
  }
8220
8232
  const reasons = [];
8221
- /* JOHNNESS_PATCH_APPEND_ONLY_PRESSURE_BASELINE: compare NEW growth since the last successful compaction (restart-safe baseline from the append-only JSONL), not the total never-shrinking file size (family 22). */
8233
+ /* JOHNNESS_PATCH_CONTEXT_WINDOW_COMPACTION: single trigger — estimated active-context tokens vs the context-window threshold (family 74). Legacy byte/message/heap heuristics no longer gate; the family 22 append-only baseline below is computed and recorded for observability only. */
8222
8234
  const __aopBaselineBytes = computeActivePressureBaselineBytes(params.sessionManager);
8223
8235
  const __aopEffectiveBytes = effectiveTranscriptPressureBytes(transcriptBytes, __aopBaselineBytes);
8224
- if (settings.transcriptBytes > 0 && __aopEffectiveBytes >= settings.transcriptBytes) reasons.push("transcriptBytes=" + __aopEffectiveBytes + "/" + settings.transcriptBytes + (__aopBaselineBytes > 0 ? " (new since baseline=" + __aopBaselineBytes + "; raw=" + transcriptBytes + ")" : ""));
8225
- if (settings.messageCount > 0 && messageCount >= settings.messageCount) reasons.push("messages=" + messageCount + "/" + settings.messageCount);
8226
- if (settings.heapUsedBytes > 0 && heapUsedBytes >= settings.heapUsedBytes) reasons.push("heapUsed=" + heapUsedBytes + "/" + settings.heapUsedBytes);
8227
- if (reasons.length === 0) return { refreshed: false, reason: "below threshold", transcriptBytes, messageCount, heapUsedBytes };
8236
+ if (__cwcEstimatedTokens >= __cwcThresholdTokens) reasons.push("contextTokens=" + __cwcEstimatedTokens + "/" + __cwcThresholdTokens);
8237
+ if (reasons.length === 0) return { refreshed: false, reason: "below threshold", contextTokens: __cwcEstimatedTokens, contextThreshold: __cwcThresholdTokens, baselineBytes: __aopBaselineBytes, effectiveTranscriptBytes: __aopEffectiveBytes, transcriptBytes, messageCount, heapUsedBytes };
8228
8238
  let last = 0;
8229
8239
  let lastFailed = false;
8230
8240
  let lastBytes = 0;
@@ -16150,13 +16160,14 @@ async function maybeCompactActiveRunForPressure(params) {
16150
16160
  } catch {}
16151
16161
  const heapUsedBytes = typeof process?.memoryUsage === "function" ? process.memoryUsage().heapUsed : 0;
16152
16162
  const reasons = [];
16153
- /* JOHNNESS_PATCH_APPEND_ONLY_PRESSURE_BASELINE: compare NEW growth since the last successful compaction (restart-safe baseline from the append-only JSONL), not the total never-shrinking file size (family 22). */
16163
+ /* JOHNNESS_PATCH_CONTEXT_WINDOW_COMPACTION: single trigger — estimated active-context tokens vs the context-window threshold (family 74). Legacy byte/message/heap heuristics no longer gate; the family 22 append-only baseline below is computed and recorded for observability only. */
16154
16164
  const __aopBaselineBytes = computeActivePressureBaselineBytes(params.sessionManager);
16155
16165
  const __aopEffectiveBytes = effectiveTranscriptPressureBytes(transcriptBytes, __aopBaselineBytes);
16156
- if (settings.transcriptBytes > 0 && __aopEffectiveBytes >= settings.transcriptBytes) reasons.push("transcriptBytes=" + __aopEffectiveBytes + "/" + settings.transcriptBytes + (__aopBaselineBytes > 0 ? " (new since baseline=" + __aopBaselineBytes + "; raw=" + transcriptBytes + ")" : ""));
16157
- if (settings.messageCount > 0 && messageCount >= settings.messageCount) reasons.push("messages=" + messageCount + "/" + settings.messageCount);
16158
- if (settings.heapUsedBytes > 0 && heapUsedBytes >= settings.heapUsedBytes) reasons.push("heapUsed=" + heapUsedBytes + "/" + settings.heapUsedBytes);
16159
- if (reasons.length === 0) return { refreshed: false, reason: "below threshold", transcriptBytes, messageCount, heapUsedBytes };
16166
+ const __cwcWindowTokens = localPressureGuardContextWindowTokens(params.session?.model);
16167
+ const __cwcThresholdTokens = localPressureGuardContextThreshold(settings, __cwcWindowTokens, params.config);
16168
+ const __cwcEstimatedTokens = estimateMessagesTokens(messages);
16169
+ if (__cwcEstimatedTokens >= __cwcThresholdTokens) reasons.push("contextTokens=" + __cwcEstimatedTokens + "/" + __cwcThresholdTokens);
16170
+ if (reasons.length === 0) return { refreshed: false, reason: "below threshold", contextTokens: __cwcEstimatedTokens, contextThreshold: __cwcThresholdTokens, baselineBytes: __aopBaselineBytes, effectiveTranscriptBytes: __aopEffectiveBytes, transcriptBytes, messageCount, heapUsedBytes };
16160
16171
  let last = 0;
16161
16172
  let lastFailed = false;
16162
16173
  let lastBytes = 0;
@@ -96130,9 +96130,7 @@ function buildEmbeddedExtensionFactories(params) {
96130
96130
 
96131
96131
  //#region johnness local active-session pressure guard
96132
96132
  const LOCAL_PRESSURE_GUARD_DEFAULTS = {
96133
- transcriptBytes: 1048576,
96134
- messageCount: 220,
96135
- heapUsedBytes: 3221225472,
96133
+ contextRatio: 0.7,
96136
96134
  minIntervalMs: 300000,
96137
96135
  minMessagesAfterCompaction: 24
96138
96136
  };
@@ -96143,14 +96141,24 @@ function localPressureGuardNumber(value, fallback) {
96143
96141
  function localPressureGuardSettings(cfg) {
96144
96142
  const raw = cfg?.agents?.defaults?.activeSessionPressure ?? cfg?.session?.activePressure ?? {};
96145
96143
  if (raw?.enabled === false) return null;
96144
+ /* JOHNNESS_PATCH_CONTEXT_WINDOW_COMPACTION: legacy transcriptBytes/messageCount/heapUsedBytes keys are accepted without error and ignored. Config surface: enabled, contextRatio, minIntervalMs, minMessagesAfterCompaction. */
96145
+ const contextRatio = localPressureGuardNumber(raw.contextRatio, LOCAL_PRESSURE_GUARD_DEFAULTS.contextRatio);
96146
96146
  return {
96147
- transcriptBytes: localPressureGuardNumber(raw.transcriptBytes ?? raw.maxTranscriptBytes, LOCAL_PRESSURE_GUARD_DEFAULTS.transcriptBytes),
96148
- messageCount: localPressureGuardNumber(raw.messageCount ?? raw.maxMessages, LOCAL_PRESSURE_GUARD_DEFAULTS.messageCount),
96149
- heapUsedBytes: localPressureGuardNumber(raw.heapUsedBytes ?? raw.maxHeapUsedBytes, LOCAL_PRESSURE_GUARD_DEFAULTS.heapUsedBytes),
96147
+ contextRatio: contextRatio > 0 ? contextRatio : LOCAL_PRESSURE_GUARD_DEFAULTS.contextRatio,
96150
96148
  minIntervalMs: localPressureGuardNumber(raw.minIntervalMs, LOCAL_PRESSURE_GUARD_DEFAULTS.minIntervalMs),
96151
96149
  minMessagesAfterCompaction: localPressureGuardNumber(raw.minMessagesAfterCompaction, LOCAL_PRESSURE_GUARD_DEFAULTS.minMessagesAfterCompaction)
96152
96150
  };
96153
96151
  }
96152
+ /* JOHNNESS_PATCH_CONTEXT_WINDOW_COMPACTION: single pressure trigger — estimated active-context tokens vs the active model's declared context window; the window falls back to DEFAULT_CONTEXT_TOKENS when undeclared. See docs/PATCHES.md family 74. */
96153
+ function localPressureGuardContextWindowTokens(model) {
96154
+ const declared = typeof model?.contextWindow === "number" && Number.isFinite(model.contextWindow) ? Math.floor(model.contextWindow) : 0;
96155
+ return declared > 0 ? declared : DEFAULT_CONTEXT_TOKENS;
96156
+ }
96157
+ function localPressureGuardContextThreshold(settings, contextWindowTokens, cfg) {
96158
+ const reserveTokensFloor = localPressureGuardNumber(cfg?.agents?.defaults?.compaction?.reserveTokensFloor, 2e4);
96159
+ const floor = Math.max(reserveTokensFloor, 50000);
96160
+ return Math.max(Math.ceil(settings.contextRatio * contextWindowTokens), floor);
96161
+ }
96154
96162
  /* JOHNNESS_PATCH_APPEND_ONLY_PRESSURE_BASELINE: derive a restart-safe byte baseline from the append-only JSONL so transcriptBytes pressure measures NEW growth since the last successful compaction rather than total (never-shrinking) file size. See docs/PATCHES.md family 22. Scans getEntries() newest->oldest for the newest SUCCESSFUL johnness.active-pressure-refresh marker (failed !== true) and returns its recorded post-compaction size (postCompactionBytes, else transcriptBytes). Returns 0 for legacy sessions with no such marker, so a first over-limit session still compacts once. */
96155
96163
  function computeActivePressureBaselineBytes(sessionManager) {
96156
96164
  try {
@@ -96308,12 +96316,12 @@ function compactionRotationNumber(value, fallback) {
96308
96316
  return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
96309
96317
  }
96310
96318
  function compactionRotationSettings(cfg) {
96311
- const pressure = localPressureGuardSettings(cfg) ?? { transcriptBytes: 1048576 };
96312
96319
  const raw = cfg?.agents?.defaults?.activeSessionPressure ?? cfg?.session?.activePressure ?? {};
96313
96320
  if (raw?.rotateAfterCompaction === false) return null;
96314
- const threshold = compactionRotationNumber(pressure?.transcriptBytes, 1048576);
96321
+ /* JOHNNESS_PATCH_CONTEXT_WINDOW_COMPACTION: the legacy transcriptBytes threshold no longer exists; the rotation size budget keeps its historical 1 MiB-derived default so default rotation behavior is unchanged. */
96322
+ const threshold = 1048576;
96315
96323
  const configuredMax = compactionRotationNumber(raw.rotationMaxBytes, 0);
96316
- const maxBytes = configuredMax > 0 ? configuredMax : threshold > 0 ? Math.max(131072, Math.min(524288, Math.floor(threshold * .45))) : 524288;
96324
+ const maxBytes = configuredMax > 0 ? configuredMax : Math.max(131072, Math.min(524288, Math.floor(threshold * .45)));
96317
96325
  return {
96318
96326
  maxBytes,
96319
96327
  maxMessages: Math.max(4, Math.floor(compactionRotationNumber(raw.rotationMaxMessages, 80))),
@@ -96449,18 +96457,20 @@ async function maybeRefreshActiveSessionForPressure(params) {
96449
96457
  const heapUsedBytes = typeof process?.memoryUsage === "function" ? process.memoryUsage().heapUsed : 0;
96450
96458
  const __hasCompactedContext = Array.isArray(params.session.messages) && params.session.messages.some((message) => message?.role === "compactionSummary");
96451
96459
  const __alreadyRotated = params.sessionManager.getEntries?.().some((entry) => entry?.type === "custom" && entry?.customType === "johnness.compaction-session-rotation");
96452
- if (settings.transcriptBytes > 0 && transcriptBytes >= settings.transcriptBytes && __hasCompactedContext && !__alreadyRotated) {
96460
+ /* JOHNNESS_PATCH_CONTEXT_WINDOW_COMPACTION: the existing-compacted-lineage rotation follows the same single context-token trigger; the byte gate is gone (family 74). */
96461
+ const __cwcWindowTokens = localPressureGuardContextWindowTokens(params.session?.model);
96462
+ const __cwcThresholdTokens = localPressureGuardContextThreshold(settings, __cwcWindowTokens, params.config);
96463
+ const __cwcEstimatedTokens = estimateMessagesTokens(messages);
96464
+ if (__cwcEstimatedTokens >= __cwcThresholdTokens && __hasCompactedContext && !__alreadyRotated) {
96453
96465
  const __existingRotation = await rotateCompactedSessionLineage({ config: params.config, session: params.session, sessionManager: params.sessionManager, sessionFile: params.sessionFile, sessionKey: params.sessionKey, sessionId: params.sessionId, reason: "existing-compacted-lineage" });
96454
96466
  if (__existingRotation.rotated) return { refreshed: true, rotated: true, reasons: ["existing-compacted-lineage"], transcriptBytes: __existingRotation.transcriptBytes, messageCount: __existingRotation.messageCount, heapUsedBytes, sessionId: __existingRotation.sessionId, sessionFile: __existingRotation.sessionFile, oldSessionId: __existingRotation.oldSessionId, oldSessionFile: __existingRotation.oldSessionFile };
96455
96467
  }
96456
96468
  const reasons = [];
96457
- /* JOHNNESS_PATCH_APPEND_ONLY_PRESSURE_BASELINE: compare NEW growth since the last successful compaction (restart-safe baseline from the append-only JSONL), not the total never-shrinking file size (family 22). */
96469
+ /* JOHNNESS_PATCH_CONTEXT_WINDOW_COMPACTION: single trigger — estimated active-context tokens vs the context-window threshold (family 74). Legacy byte/message/heap heuristics no longer gate; the family 22 append-only baseline below is computed and recorded for observability only. */
96458
96470
  const __aopBaselineBytes = computeActivePressureBaselineBytes(params.sessionManager);
96459
96471
  const __aopEffectiveBytes = effectiveTranscriptPressureBytes(transcriptBytes, __aopBaselineBytes);
96460
- if (settings.transcriptBytes > 0 && __aopEffectiveBytes >= settings.transcriptBytes) reasons.push("transcriptBytes=" + __aopEffectiveBytes + "/" + settings.transcriptBytes + (__aopBaselineBytes > 0 ? " (new since baseline=" + __aopBaselineBytes + "; raw=" + transcriptBytes + ")" : ""));
96461
- if (settings.messageCount > 0 && messageCount >= settings.messageCount) reasons.push("messages=" + messageCount + "/" + settings.messageCount);
96462
- if (settings.heapUsedBytes > 0 && heapUsedBytes >= settings.heapUsedBytes) reasons.push("heapUsed=" + heapUsedBytes + "/" + settings.heapUsedBytes);
96463
- if (reasons.length === 0) return { refreshed: false, reason: "below threshold", transcriptBytes, messageCount, heapUsedBytes };
96472
+ if (__cwcEstimatedTokens >= __cwcThresholdTokens) reasons.push("contextTokens=" + __cwcEstimatedTokens + "/" + __cwcThresholdTokens);
96473
+ if (reasons.length === 0) return { refreshed: false, reason: "below threshold", contextTokens: __cwcEstimatedTokens, contextThreshold: __cwcThresholdTokens, baselineBytes: __aopBaselineBytes, effectiveTranscriptBytes: __aopEffectiveBytes, transcriptBytes, messageCount, heapUsedBytes };
96464
96474
  let last = 0;
96465
96475
  let lastFailed = false;
96466
96476
  let lastBytes = 0;
@@ -101878,13 +101888,14 @@ async function maybeCompactActiveRunForPressure(params) {
101878
101888
  } catch {}
101879
101889
  const heapUsedBytes = typeof process?.memoryUsage === "function" ? process.memoryUsage().heapUsed : 0;
101880
101890
  const reasons = [];
101881
- /* JOHNNESS_PATCH_APPEND_ONLY_PRESSURE_BASELINE: compare NEW growth since the last successful compaction (restart-safe baseline from the append-only JSONL), not the total never-shrinking file size (family 22). */
101891
+ /* JOHNNESS_PATCH_CONTEXT_WINDOW_COMPACTION: single trigger — estimated active-context tokens vs the context-window threshold (family 74). Legacy byte/message/heap heuristics no longer gate; the family 22 append-only baseline below is computed and recorded for observability only. */
101882
101892
  const __aopBaselineBytes = computeActivePressureBaselineBytes(params.sessionManager);
101883
101893
  const __aopEffectiveBytes = effectiveTranscriptPressureBytes(transcriptBytes, __aopBaselineBytes);
101884
- if (settings.transcriptBytes > 0 && __aopEffectiveBytes >= settings.transcriptBytes) reasons.push("transcriptBytes=" + __aopEffectiveBytes + "/" + settings.transcriptBytes + (__aopBaselineBytes > 0 ? " (new since baseline=" + __aopBaselineBytes + "; raw=" + transcriptBytes + ")" : ""));
101885
- if (settings.messageCount > 0 && messageCount >= settings.messageCount) reasons.push("messages=" + messageCount + "/" + settings.messageCount);
101886
- if (settings.heapUsedBytes > 0 && heapUsedBytes >= settings.heapUsedBytes) reasons.push("heapUsed=" + heapUsedBytes + "/" + settings.heapUsedBytes);
101887
- if (reasons.length === 0) return { refreshed: false, reason: "below threshold", transcriptBytes, messageCount, heapUsedBytes };
101894
+ const __cwcWindowTokens = localPressureGuardContextWindowTokens(params.session?.model);
101895
+ const __cwcThresholdTokens = localPressureGuardContextThreshold(settings, __cwcWindowTokens, params.config);
101896
+ const __cwcEstimatedTokens = estimateMessagesTokens(messages);
101897
+ if (__cwcEstimatedTokens >= __cwcThresholdTokens) reasons.push("contextTokens=" + __cwcEstimatedTokens + "/" + __cwcThresholdTokens);
101898
+ if (reasons.length === 0) return { refreshed: false, reason: "below threshold", contextTokens: __cwcEstimatedTokens, contextThreshold: __cwcThresholdTokens, baselineBytes: __aopBaselineBytes, effectiveTranscriptBytes: __aopEffectiveBytes, transcriptBytes, messageCount, heapUsedBytes };
101888
101899
  let last = 0;
101889
101900
  let lastFailed = false;
101890
101901
  let lastBytes = 0;
@@ -96120,9 +96120,7 @@ function buildEmbeddedExtensionFactories(params) {
96120
96120
 
96121
96121
  //#region johnness local active-session pressure guard
96122
96122
  const LOCAL_PRESSURE_GUARD_DEFAULTS = {
96123
- transcriptBytes: 1048576,
96124
- messageCount: 220,
96125
- heapUsedBytes: 3221225472,
96123
+ contextRatio: 0.7,
96126
96124
  minIntervalMs: 300000,
96127
96125
  minMessagesAfterCompaction: 24
96128
96126
  };
@@ -96133,14 +96131,24 @@ function localPressureGuardNumber(value, fallback) {
96133
96131
  function localPressureGuardSettings(cfg) {
96134
96132
  const raw = cfg?.agents?.defaults?.activeSessionPressure ?? cfg?.session?.activePressure ?? {};
96135
96133
  if (raw?.enabled === false) return null;
96134
+ /* JOHNNESS_PATCH_CONTEXT_WINDOW_COMPACTION: legacy transcriptBytes/messageCount/heapUsedBytes keys are accepted without error and ignored. Config surface: enabled, contextRatio, minIntervalMs, minMessagesAfterCompaction. */
96135
+ const contextRatio = localPressureGuardNumber(raw.contextRatio, LOCAL_PRESSURE_GUARD_DEFAULTS.contextRatio);
96136
96136
  return {
96137
- transcriptBytes: localPressureGuardNumber(raw.transcriptBytes ?? raw.maxTranscriptBytes, LOCAL_PRESSURE_GUARD_DEFAULTS.transcriptBytes),
96138
- messageCount: localPressureGuardNumber(raw.messageCount ?? raw.maxMessages, LOCAL_PRESSURE_GUARD_DEFAULTS.messageCount),
96139
- heapUsedBytes: localPressureGuardNumber(raw.heapUsedBytes ?? raw.maxHeapUsedBytes, LOCAL_PRESSURE_GUARD_DEFAULTS.heapUsedBytes),
96137
+ contextRatio: contextRatio > 0 ? contextRatio : LOCAL_PRESSURE_GUARD_DEFAULTS.contextRatio,
96140
96138
  minIntervalMs: localPressureGuardNumber(raw.minIntervalMs, LOCAL_PRESSURE_GUARD_DEFAULTS.minIntervalMs),
96141
96139
  minMessagesAfterCompaction: localPressureGuardNumber(raw.minMessagesAfterCompaction, LOCAL_PRESSURE_GUARD_DEFAULTS.minMessagesAfterCompaction)
96142
96140
  };
96143
96141
  }
96142
+ /* JOHNNESS_PATCH_CONTEXT_WINDOW_COMPACTION: single pressure trigger — estimated active-context tokens vs the active model's declared context window; the window falls back to DEFAULT_CONTEXT_TOKENS when undeclared. See docs/PATCHES.md family 74. */
96143
+ function localPressureGuardContextWindowTokens(model) {
96144
+ const declared = typeof model?.contextWindow === "number" && Number.isFinite(model.contextWindow) ? Math.floor(model.contextWindow) : 0;
96145
+ return declared > 0 ? declared : DEFAULT_CONTEXT_TOKENS;
96146
+ }
96147
+ function localPressureGuardContextThreshold(settings, contextWindowTokens, cfg) {
96148
+ const reserveTokensFloor = localPressureGuardNumber(cfg?.agents?.defaults?.compaction?.reserveTokensFloor, 2e4);
96149
+ const floor = Math.max(reserveTokensFloor, 50000);
96150
+ return Math.max(Math.ceil(settings.contextRatio * contextWindowTokens), floor);
96151
+ }
96144
96152
  /* JOHNNESS_PATCH_APPEND_ONLY_PRESSURE_BASELINE: derive a restart-safe byte baseline from the append-only JSONL so transcriptBytes pressure measures NEW growth since the last successful compaction rather than total (never-shrinking) file size. See docs/PATCHES.md family 22. Scans getEntries() newest->oldest for the newest SUCCESSFUL johnness.active-pressure-refresh marker (failed !== true) and returns its recorded post-compaction size (postCompactionBytes, else transcriptBytes). Returns 0 for legacy sessions with no such marker, so a first over-limit session still compacts once. */
96145
96153
  function computeActivePressureBaselineBytes(sessionManager) {
96146
96154
  try {
@@ -96298,12 +96306,12 @@ function compactionRotationNumber(value, fallback) {
96298
96306
  return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
96299
96307
  }
96300
96308
  function compactionRotationSettings(cfg) {
96301
- const pressure = localPressureGuardSettings(cfg) ?? { transcriptBytes: 1048576 };
96302
96309
  const raw = cfg?.agents?.defaults?.activeSessionPressure ?? cfg?.session?.activePressure ?? {};
96303
96310
  if (raw?.rotateAfterCompaction === false) return null;
96304
- const threshold = compactionRotationNumber(pressure?.transcriptBytes, 1048576);
96311
+ /* JOHNNESS_PATCH_CONTEXT_WINDOW_COMPACTION: the legacy transcriptBytes threshold no longer exists; the rotation size budget keeps its historical 1 MiB-derived default so default rotation behavior is unchanged. */
96312
+ const threshold = 1048576;
96305
96313
  const configuredMax = compactionRotationNumber(raw.rotationMaxBytes, 0);
96306
- const maxBytes = configuredMax > 0 ? configuredMax : threshold > 0 ? Math.max(131072, Math.min(524288, Math.floor(threshold * .45))) : 524288;
96314
+ const maxBytes = configuredMax > 0 ? configuredMax : Math.max(131072, Math.min(524288, Math.floor(threshold * .45)));
96307
96315
  return {
96308
96316
  maxBytes,
96309
96317
  maxMessages: Math.max(4, Math.floor(compactionRotationNumber(raw.rotationMaxMessages, 80))),
@@ -96439,18 +96447,20 @@ async function maybeRefreshActiveSessionForPressure(params) {
96439
96447
  const heapUsedBytes = typeof process?.memoryUsage === "function" ? process.memoryUsage().heapUsed : 0;
96440
96448
  const __hasCompactedContext = Array.isArray(params.session.messages) && params.session.messages.some((message) => message?.role === "compactionSummary");
96441
96449
  const __alreadyRotated = params.sessionManager.getEntries?.().some((entry) => entry?.type === "custom" && entry?.customType === "johnness.compaction-session-rotation");
96442
- if (settings.transcriptBytes > 0 && transcriptBytes >= settings.transcriptBytes && __hasCompactedContext && !__alreadyRotated) {
96450
+ /* JOHNNESS_PATCH_CONTEXT_WINDOW_COMPACTION: the existing-compacted-lineage rotation follows the same single context-token trigger; the byte gate is gone (family 74). */
96451
+ const __cwcWindowTokens = localPressureGuardContextWindowTokens(params.session?.model);
96452
+ const __cwcThresholdTokens = localPressureGuardContextThreshold(settings, __cwcWindowTokens, params.config);
96453
+ const __cwcEstimatedTokens = estimateMessagesTokens(messages);
96454
+ if (__cwcEstimatedTokens >= __cwcThresholdTokens && __hasCompactedContext && !__alreadyRotated) {
96443
96455
  const __existingRotation = await rotateCompactedSessionLineage({ config: params.config, session: params.session, sessionManager: params.sessionManager, sessionFile: params.sessionFile, sessionKey: params.sessionKey, sessionId: params.sessionId, reason: "existing-compacted-lineage" });
96444
96456
  if (__existingRotation.rotated) return { refreshed: true, rotated: true, reasons: ["existing-compacted-lineage"], transcriptBytes: __existingRotation.transcriptBytes, messageCount: __existingRotation.messageCount, heapUsedBytes, sessionId: __existingRotation.sessionId, sessionFile: __existingRotation.sessionFile, oldSessionId: __existingRotation.oldSessionId, oldSessionFile: __existingRotation.oldSessionFile };
96445
96457
  }
96446
96458
  const reasons = [];
96447
- /* JOHNNESS_PATCH_APPEND_ONLY_PRESSURE_BASELINE: compare NEW growth since the last successful compaction (restart-safe baseline from the append-only JSONL), not the total never-shrinking file size (family 22). */
96459
+ /* JOHNNESS_PATCH_CONTEXT_WINDOW_COMPACTION: single trigger — estimated active-context tokens vs the context-window threshold (family 74). Legacy byte/message/heap heuristics no longer gate; the family 22 append-only baseline below is computed and recorded for observability only. */
96448
96460
  const __aopBaselineBytes = computeActivePressureBaselineBytes(params.sessionManager);
96449
96461
  const __aopEffectiveBytes = effectiveTranscriptPressureBytes(transcriptBytes, __aopBaselineBytes);
96450
- if (settings.transcriptBytes > 0 && __aopEffectiveBytes >= settings.transcriptBytes) reasons.push("transcriptBytes=" + __aopEffectiveBytes + "/" + settings.transcriptBytes + (__aopBaselineBytes > 0 ? " (new since baseline=" + __aopBaselineBytes + "; raw=" + transcriptBytes + ")" : ""));
96451
- if (settings.messageCount > 0 && messageCount >= settings.messageCount) reasons.push("messages=" + messageCount + "/" + settings.messageCount);
96452
- if (settings.heapUsedBytes > 0 && heapUsedBytes >= settings.heapUsedBytes) reasons.push("heapUsed=" + heapUsedBytes + "/" + settings.heapUsedBytes);
96453
- if (reasons.length === 0) return { refreshed: false, reason: "below threshold", transcriptBytes, messageCount, heapUsedBytes };
96462
+ if (__cwcEstimatedTokens >= __cwcThresholdTokens) reasons.push("contextTokens=" + __cwcEstimatedTokens + "/" + __cwcThresholdTokens);
96463
+ if (reasons.length === 0) return { refreshed: false, reason: "below threshold", contextTokens: __cwcEstimatedTokens, contextThreshold: __cwcThresholdTokens, baselineBytes: __aopBaselineBytes, effectiveTranscriptBytes: __aopEffectiveBytes, transcriptBytes, messageCount, heapUsedBytes };
96454
96464
  let last = 0;
96455
96465
  let lastFailed = false;
96456
96466
  let lastBytes = 0;
@@ -101868,13 +101878,14 @@ async function maybeCompactActiveRunForPressure(params) {
101868
101878
  } catch {}
101869
101879
  const heapUsedBytes = typeof process?.memoryUsage === "function" ? process.memoryUsage().heapUsed : 0;
101870
101880
  const reasons = [];
101871
- /* JOHNNESS_PATCH_APPEND_ONLY_PRESSURE_BASELINE: compare NEW growth since the last successful compaction (restart-safe baseline from the append-only JSONL), not the total never-shrinking file size (family 22). */
101881
+ /* JOHNNESS_PATCH_CONTEXT_WINDOW_COMPACTION: single trigger — estimated active-context tokens vs the context-window threshold (family 74). Legacy byte/message/heap heuristics no longer gate; the family 22 append-only baseline below is computed and recorded for observability only. */
101872
101882
  const __aopBaselineBytes = computeActivePressureBaselineBytes(params.sessionManager);
101873
101883
  const __aopEffectiveBytes = effectiveTranscriptPressureBytes(transcriptBytes, __aopBaselineBytes);
101874
- if (settings.transcriptBytes > 0 && __aopEffectiveBytes >= settings.transcriptBytes) reasons.push("transcriptBytes=" + __aopEffectiveBytes + "/" + settings.transcriptBytes + (__aopBaselineBytes > 0 ? " (new since baseline=" + __aopBaselineBytes + "; raw=" + transcriptBytes + ")" : ""));
101875
- if (settings.messageCount > 0 && messageCount >= settings.messageCount) reasons.push("messages=" + messageCount + "/" + settings.messageCount);
101876
- if (settings.heapUsedBytes > 0 && heapUsedBytes >= settings.heapUsedBytes) reasons.push("heapUsed=" + heapUsedBytes + "/" + settings.heapUsedBytes);
101877
- if (reasons.length === 0) return { refreshed: false, reason: "below threshold", transcriptBytes, messageCount, heapUsedBytes };
101884
+ const __cwcWindowTokens = localPressureGuardContextWindowTokens(params.session?.model);
101885
+ const __cwcThresholdTokens = localPressureGuardContextThreshold(settings, __cwcWindowTokens, params.config);
101886
+ const __cwcEstimatedTokens = estimateMessagesTokens(messages);
101887
+ if (__cwcEstimatedTokens >= __cwcThresholdTokens) reasons.push("contextTokens=" + __cwcEstimatedTokens + "/" + __cwcThresholdTokens);
101888
+ if (reasons.length === 0) return { refreshed: false, reason: "below threshold", contextTokens: __cwcEstimatedTokens, contextThreshold: __cwcThresholdTokens, baselineBytes: __aopBaselineBytes, effectiveTranscriptBytes: __aopEffectiveBytes, transcriptBytes, messageCount, heapUsedBytes };
101878
101889
  let last = 0;
101879
101890
  let lastFailed = false;
101880
101891
  let lastBytes = 0;
@@ -74132,9 +74132,7 @@ function buildEmbeddedExtensionFactories(params) {
74132
74132
 
74133
74133
  //#region johnness local active-session pressure guard
74134
74134
  const LOCAL_PRESSURE_GUARD_DEFAULTS = {
74135
- transcriptBytes: 1048576,
74136
- messageCount: 220,
74137
- heapUsedBytes: 3221225472,
74135
+ contextRatio: 0.7,
74138
74136
  minIntervalMs: 300000,
74139
74137
  minMessagesAfterCompaction: 24
74140
74138
  };
@@ -74145,14 +74143,24 @@ function localPressureGuardNumber(value, fallback) {
74145
74143
  function localPressureGuardSettings(cfg) {
74146
74144
  const raw = cfg?.agents?.defaults?.activeSessionPressure ?? cfg?.session?.activePressure ?? {};
74147
74145
  if (raw?.enabled === false) return null;
74146
+ /* JOHNNESS_PATCH_CONTEXT_WINDOW_COMPACTION: legacy transcriptBytes/messageCount/heapUsedBytes keys are accepted without error and ignored. Config surface: enabled, contextRatio, minIntervalMs, minMessagesAfterCompaction. */
74147
+ const contextRatio = localPressureGuardNumber(raw.contextRatio, LOCAL_PRESSURE_GUARD_DEFAULTS.contextRatio);
74148
74148
  return {
74149
- transcriptBytes: localPressureGuardNumber(raw.transcriptBytes ?? raw.maxTranscriptBytes, LOCAL_PRESSURE_GUARD_DEFAULTS.transcriptBytes),
74150
- messageCount: localPressureGuardNumber(raw.messageCount ?? raw.maxMessages, LOCAL_PRESSURE_GUARD_DEFAULTS.messageCount),
74151
- heapUsedBytes: localPressureGuardNumber(raw.heapUsedBytes ?? raw.maxHeapUsedBytes, LOCAL_PRESSURE_GUARD_DEFAULTS.heapUsedBytes),
74149
+ contextRatio: contextRatio > 0 ? contextRatio : LOCAL_PRESSURE_GUARD_DEFAULTS.contextRatio,
74152
74150
  minIntervalMs: localPressureGuardNumber(raw.minIntervalMs, LOCAL_PRESSURE_GUARD_DEFAULTS.minIntervalMs),
74153
74151
  minMessagesAfterCompaction: localPressureGuardNumber(raw.minMessagesAfterCompaction, LOCAL_PRESSURE_GUARD_DEFAULTS.minMessagesAfterCompaction)
74154
74152
  };
74155
74153
  }
74154
+ /* JOHNNESS_PATCH_CONTEXT_WINDOW_COMPACTION: single pressure trigger — estimated active-context tokens vs the active model's declared context window; the window falls back to DEFAULT_CONTEXT_TOKENS when undeclared. See docs/PATCHES.md family 74. */
74155
+ function localPressureGuardContextWindowTokens(model) {
74156
+ const declared = typeof model?.contextWindow === "number" && Number.isFinite(model.contextWindow) ? Math.floor(model.contextWindow) : 0;
74157
+ return declared > 0 ? declared : DEFAULT_CONTEXT_TOKENS;
74158
+ }
74159
+ function localPressureGuardContextThreshold(settings, contextWindowTokens, cfg) {
74160
+ const reserveTokensFloor = localPressureGuardNumber(cfg?.agents?.defaults?.compaction?.reserveTokensFloor, 2e4);
74161
+ const floor = Math.max(reserveTokensFloor, 50000);
74162
+ return Math.max(Math.ceil(settings.contextRatio * contextWindowTokens), floor);
74163
+ }
74156
74164
  /* JOHNNESS_PATCH_APPEND_ONLY_PRESSURE_BASELINE: derive a restart-safe byte baseline from the append-only JSONL so transcriptBytes pressure measures NEW growth since the last successful compaction rather than total (never-shrinking) file size. See docs/PATCHES.md family 22. Scans getEntries() newest->oldest for the newest SUCCESSFUL johnness.active-pressure-refresh marker (failed !== true) and returns its recorded post-compaction size (postCompactionBytes, else transcriptBytes). Returns 0 for legacy sessions with no such marker, so a first over-limit session still compacts once. */
74157
74165
  function computeActivePressureBaselineBytes(sessionManager) {
74158
74166
  try {
@@ -74310,12 +74318,12 @@ function compactionRotationNumber(value, fallback) {
74310
74318
  return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
74311
74319
  }
74312
74320
  function compactionRotationSettings(cfg) {
74313
- const pressure = localPressureGuardSettings(cfg) ?? { transcriptBytes: 1048576 };
74314
74321
  const raw = cfg?.agents?.defaults?.activeSessionPressure ?? cfg?.session?.activePressure ?? {};
74315
74322
  if (raw?.rotateAfterCompaction === false) return null;
74316
- const threshold = compactionRotationNumber(pressure?.transcriptBytes, 1048576);
74323
+ /* JOHNNESS_PATCH_CONTEXT_WINDOW_COMPACTION: the legacy transcriptBytes threshold no longer exists; the rotation size budget keeps its historical 1 MiB-derived default so default rotation behavior is unchanged. */
74324
+ const threshold = 1048576;
74317
74325
  const configuredMax = compactionRotationNumber(raw.rotationMaxBytes, 0);
74318
- const maxBytes = configuredMax > 0 ? configuredMax : threshold > 0 ? Math.max(131072, Math.min(524288, Math.floor(threshold * .45))) : 524288;
74326
+ const maxBytes = configuredMax > 0 ? configuredMax : Math.max(131072, Math.min(524288, Math.floor(threshold * .45)));
74319
74327
  return {
74320
74328
  maxBytes,
74321
74329
  maxMessages: Math.max(4, Math.floor(compactionRotationNumber(raw.rotationMaxMessages, 80))),
@@ -74451,18 +74459,20 @@ async function maybeRefreshActiveSessionForPressure(params) {
74451
74459
  const heapUsedBytes = typeof process?.memoryUsage === "function" ? process.memoryUsage().heapUsed : 0;
74452
74460
  const __hasCompactedContext = Array.isArray(params.session.messages) && params.session.messages.some((message) => message?.role === "compactionSummary");
74453
74461
  const __alreadyRotated = params.sessionManager.getEntries?.().some((entry) => entry?.type === "custom" && entry?.customType === "johnness.compaction-session-rotation");
74454
- if (settings.transcriptBytes > 0 && transcriptBytes >= settings.transcriptBytes && __hasCompactedContext && !__alreadyRotated) {
74462
+ /* JOHNNESS_PATCH_CONTEXT_WINDOW_COMPACTION: the existing-compacted-lineage rotation follows the same single context-token trigger; the byte gate is gone (family 74). */
74463
+ const __cwcWindowTokens = localPressureGuardContextWindowTokens(params.session?.model);
74464
+ const __cwcThresholdTokens = localPressureGuardContextThreshold(settings, __cwcWindowTokens, params.config);
74465
+ const __cwcEstimatedTokens = estimateMessagesTokens(messages);
74466
+ if (__cwcEstimatedTokens >= __cwcThresholdTokens && __hasCompactedContext && !__alreadyRotated) {
74455
74467
  const __existingRotation = await rotateCompactedSessionLineage({ config: params.config, session: params.session, sessionManager: params.sessionManager, sessionFile: params.sessionFile, sessionKey: params.sessionKey, sessionId: params.sessionId, reason: "existing-compacted-lineage" });
74456
74468
  if (__existingRotation.rotated) return { refreshed: true, rotated: true, reasons: ["existing-compacted-lineage"], transcriptBytes: __existingRotation.transcriptBytes, messageCount: __existingRotation.messageCount, heapUsedBytes, sessionId: __existingRotation.sessionId, sessionFile: __existingRotation.sessionFile, oldSessionId: __existingRotation.oldSessionId, oldSessionFile: __existingRotation.oldSessionFile };
74457
74469
  }
74458
74470
  const reasons = [];
74459
- /* JOHNNESS_PATCH_APPEND_ONLY_PRESSURE_BASELINE: compare NEW growth since the last successful compaction (restart-safe baseline from the append-only JSONL), not the total never-shrinking file size (family 22). */
74471
+ /* JOHNNESS_PATCH_CONTEXT_WINDOW_COMPACTION: single trigger — estimated active-context tokens vs the context-window threshold (family 74). Legacy byte/message/heap heuristics no longer gate; the family 22 append-only baseline below is computed and recorded for observability only. */
74460
74472
  const __aopBaselineBytes = computeActivePressureBaselineBytes(params.sessionManager);
74461
74473
  const __aopEffectiveBytes = effectiveTranscriptPressureBytes(transcriptBytes, __aopBaselineBytes);
74462
- if (settings.transcriptBytes > 0 && __aopEffectiveBytes >= settings.transcriptBytes) reasons.push("transcriptBytes=" + __aopEffectiveBytes + "/" + settings.transcriptBytes + (__aopBaselineBytes > 0 ? " (new since baseline=" + __aopBaselineBytes + "; raw=" + transcriptBytes + ")" : ""));
74463
- if (settings.messageCount > 0 && messageCount >= settings.messageCount) reasons.push("messages=" + messageCount + "/" + settings.messageCount);
74464
- if (settings.heapUsedBytes > 0 && heapUsedBytes >= settings.heapUsedBytes) reasons.push("heapUsed=" + heapUsedBytes + "/" + settings.heapUsedBytes);
74465
- if (reasons.length === 0) return { refreshed: false, reason: "below threshold", transcriptBytes, messageCount, heapUsedBytes };
74474
+ if (__cwcEstimatedTokens >= __cwcThresholdTokens) reasons.push("contextTokens=" + __cwcEstimatedTokens + "/" + __cwcThresholdTokens);
74475
+ if (reasons.length === 0) return { refreshed: false, reason: "below threshold", contextTokens: __cwcEstimatedTokens, contextThreshold: __cwcThresholdTokens, baselineBytes: __aopBaselineBytes, effectiveTranscriptBytes: __aopEffectiveBytes, transcriptBytes, messageCount, heapUsedBytes };
74466
74476
  let last = 0;
74467
74477
  let lastFailed = false;
74468
74478
  let lastBytes = 0;
@@ -79883,13 +79893,14 @@ async function maybeCompactActiveRunForPressure(params) {
79883
79893
  } catch {}
79884
79894
  const heapUsedBytes = typeof process?.memoryUsage === "function" ? process.memoryUsage().heapUsed : 0;
79885
79895
  const reasons = [];
79886
- /* JOHNNESS_PATCH_APPEND_ONLY_PRESSURE_BASELINE: compare NEW growth since the last successful compaction (restart-safe baseline from the append-only JSONL), not the total never-shrinking file size (family 22). */
79896
+ /* JOHNNESS_PATCH_CONTEXT_WINDOW_COMPACTION: single trigger — estimated active-context tokens vs the context-window threshold (family 74). Legacy byte/message/heap heuristics no longer gate; the family 22 append-only baseline below is computed and recorded for observability only. */
79887
79897
  const __aopBaselineBytes = computeActivePressureBaselineBytes(params.sessionManager);
79888
79898
  const __aopEffectiveBytes = effectiveTranscriptPressureBytes(transcriptBytes, __aopBaselineBytes);
79889
- if (settings.transcriptBytes > 0 && __aopEffectiveBytes >= settings.transcriptBytes) reasons.push("transcriptBytes=" + __aopEffectiveBytes + "/" + settings.transcriptBytes + (__aopBaselineBytes > 0 ? " (new since baseline=" + __aopBaselineBytes + "; raw=" + transcriptBytes + ")" : ""));
79890
- if (settings.messageCount > 0 && messageCount >= settings.messageCount) reasons.push("messages=" + messageCount + "/" + settings.messageCount);
79891
- if (settings.heapUsedBytes > 0 && heapUsedBytes >= settings.heapUsedBytes) reasons.push("heapUsed=" + heapUsedBytes + "/" + settings.heapUsedBytes);
79892
- if (reasons.length === 0) return { refreshed: false, reason: "below threshold", transcriptBytes, messageCount, heapUsedBytes };
79899
+ const __cwcWindowTokens = localPressureGuardContextWindowTokens(params.session?.model);
79900
+ const __cwcThresholdTokens = localPressureGuardContextThreshold(settings, __cwcWindowTokens, params.config);
79901
+ const __cwcEstimatedTokens = estimateMessagesTokens(messages);
79902
+ if (__cwcEstimatedTokens >= __cwcThresholdTokens) reasons.push("contextTokens=" + __cwcEstimatedTokens + "/" + __cwcThresholdTokens);
79903
+ if (reasons.length === 0) return { refreshed: false, reason: "below threshold", contextTokens: __cwcEstimatedTokens, contextThreshold: __cwcThresholdTokens, baselineBytes: __aopBaselineBytes, effectiveTranscriptBytes: __aopEffectiveBytes, transcriptBytes, messageCount, heapUsedBytes };
79893
79904
  let last = 0;
79894
79905
  let lastFailed = false;
79895
79906
  let lastBytes = 0;
package/docs/PATCHES.md CHANGED
@@ -17,7 +17,7 @@ they predate ~2026-06-06).
17
17
 
18
18
  ## Index
19
19
 
20
- Family 01 is retired. Families 02 through 41, 43 through 72, including the 65.1 amendment, are live on `main`; the `latest` and `extended` release lines converged at 2026.9.24. Family 42 is omitted; native Codex OAuth ownership is Family 58.
20
+ Family 01 is retired. Families 02 through 41, 43 through 74, including the 65.1 amendment, are live on `main`; the `latest` and `extended` release lines converged at 2026.9.24. Family 42 is omitted; native Codex OAuth ownership is Family 58.
21
21
 
22
22
  1. **01, announce queue TTL, retired:** Removed the five-minute age filter that could discard delayed sub-agent completion announcements. [Details](#01--announce-ttl-retired-2026-07-30)
23
23
  2. **02, failover rotation:** Returns model selection to the highest-priority profile after its cooldown ends. [Details](#02--failover-rotation-undocumented)
@@ -99,6 +99,7 @@ Family 01 is retired. Families 02 through 41, 43 through 72, including the 65.1
99
99
 
100
100
  72. **72, bundled generic skills:** Eight workspace skills (code operations, research, todos, planning, logs, and more) ship as generic bundled skills that load by description match, with no owner names or personal paths. [Details](#family-72-bundled-generic-skills)
101
101
  73. **73, prune bundled skills:** `content-story`, `dashboard` and `cold-outreach` are removed from the Family 72 bundled set on request after `2026.9.25`; the remaining eight ship unchanged. [Details](#family-73-prune-bundled-skills)
102
+ 74. **74, context-window compaction:** The local active-session pressure guard becomes single-trigger and model-aware: estimated active-context tokens against 70% of the active model's declared context window, floored at `max(compaction.reserveTokensFloor, 50000)`; the 1 MB byte, 220-message and 3 GiB heap triggers are removed. [Details](#family-74-context-window-compaction)
102
103
 
103
104
  ---
104
105
 
@@ -729,6 +730,7 @@ already-compacting / compaction-disabled / cooldown are no-ops.
729
730
  ---
730
731
 
731
732
  ## 22 - append-only-pressure-baseline (2026-07-29)
733
+ **Superseded by Family 74:** the append-only baseline is still computed and recorded on the `active-pressure-refresh` marker (see below), but nothing gates on it any more; the pressure trigger is now estimated context tokens against the active model's declared context window.
732
734
  **What:** The active-pressure `transcriptBytes` trigger now measures **NEW transcript
733
735
  growth since the last successful compaction** instead of the total, never-shrinking
734
736
  append-only file size. This removes the false-retrigger loop where a session that has
@@ -2669,3 +2671,17 @@ Three of the Family 72 bundled skills are removed from the bundled set on reques
2669
2671
  The counters move with the set: the verifier emits one check per remaining skill through the same 72.1 loop (455 checks in total), `tests/bundled-skills.test.mjs` asserts exactly eight bundled skills, `scripts/check-packed-artifact.mjs` fails closed unless a tarball carries all eight with frontmatter, and `README.md` and the pinned copy in `patches/59-migrate-cli.patch` carry the new counts together so check 59c stays green. `RULES.md` section 5 drops the three rows.
2670
2672
 
2671
2673
  Roll back with `npm i -g johns-harness@2026.9.25`.
2674
+
2675
+ ## Family 74: context-window compaction
2676
+
2677
+ **Release:** `johns-harness@2026.9.27`. Published as `latest` and the `extended` dist-tag moves to it.
2678
+
2679
+ A live gateway session compacted at `messages=221/220` while its transcript held only 528 KB of a 1,048,576-byte budget. The guard was firing on a fixed message count, which is the wrong unit: 221 small messages can sit comfortably inside a large context window, and a few large tool outputs can overflow a small one. Family 74 makes the local active-session pressure guard single-trigger and model-aware.
2680
+
2681
+ The only trigger is estimated active-context tokens (`estimateMessagesTokens`, the estimator the compaction path already uses) against `max(ceil(contextRatio x activeModel.contextWindow), max(compaction.reserveTokensFloor, 50000))`, with `contextRatio` defaulting to 0.7. A model that declares no `contextWindow` falls back to the runtime default. Both the startup rotation gate and the in-loop refresh push only `contextTokens=<n>/<threshold>`.
2682
+
2683
+ The `transcriptBytes` (1 MB), `messageCount` (220) and `heapUsedBytes` (3 GiB) triggers are removed by design; the legacy config keys are accepted without error and ignored. The 5-minute cooldown, the 24-message post-compaction floor and the failed-attempt growth gate stay as anti-thrash throttles, and the emergency compaction path (Family 13) is untouched, so a genuine overflow is still caught. The Family 22 append-only baseline is still computed and recorded on the `active-pressure-refresh` marker for observability, but nothing gates on it. The config surface is `enabled`, `contextRatio`, `minIntervalMs`, `minMessagesAfterCompaction`.
2684
+
2685
+ Verifier checks 74.1 through 74.4 prove the marker, the absence of the byte/message/heap triggers, the threshold math and the config parsing in all four pressure bundles; `scripts/patch-context-window-compaction.py` applies the same change to any install root (both naming eras), is idempotent with `--check`, and fails closed on anchor drift. `patches/provider-catalog/changes.json` pins the four pressure bundles by hash, so `scripts/patch-provider-catalog.py` maps the Family 74 rewrite back to its pre-image in `family67_surface`, exactly as it already does for Family 68.
2686
+
2687
+ Roll back with `npm i -g johns-harness@2026.9.26`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "johns-harness",
3
- "version": "2026.9.26",
3
+ "version": "2026.9.27",
4
4
  "description": "John's Harness: a production agent harness that runs autonomous coding agents as one cooperative swarm.",
5
5
  "keywords": [
6
6
  "ai-agents",