@rubytech/create-maxy-code 0.1.459 → 0.1.461

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 (189) hide show
  1. package/dist/__tests__/cron-heartbeat-registration.test.js +57 -28
  2. package/dist/__tests__/launchd-plist.test.js +26 -0
  3. package/dist/cron-registration.js +30 -8
  4. package/dist/index.js +50 -9
  5. package/dist/launchd-plist.js +1 -1
  6. package/dist/uninstall.js +9 -3
  7. package/package.json +1 -1
  8. package/payload/platform/lib/storage-broker/dist/__tests__/cf-exec.test.js +116 -6
  9. package/payload/platform/lib/storage-broker/dist/__tests__/cf-exec.test.js.map +1 -1
  10. package/payload/platform/lib/storage-broker/dist/__tests__/object-limits.test.js +20 -0
  11. package/payload/platform/lib/storage-broker/dist/__tests__/object-limits.test.js.map +1 -1
  12. package/payload/platform/lib/storage-broker/dist/__tests__/resource-limits.test.d.ts +2 -0
  13. package/payload/platform/lib/storage-broker/dist/__tests__/resource-limits.test.d.ts.map +1 -0
  14. package/payload/platform/lib/storage-broker/dist/__tests__/resource-limits.test.js +41 -0
  15. package/payload/platform/lib/storage-broker/dist/__tests__/resource-limits.test.js.map +1 -0
  16. package/payload/platform/lib/storage-broker/dist/cf-exec.d.ts +1 -0
  17. package/payload/platform/lib/storage-broker/dist/cf-exec.d.ts.map +1 -1
  18. package/payload/platform/lib/storage-broker/dist/cf-exec.js +116 -48
  19. package/payload/platform/lib/storage-broker/dist/cf-exec.js.map +1 -1
  20. package/payload/platform/lib/storage-broker/dist/index.d.ts +1 -0
  21. package/payload/platform/lib/storage-broker/dist/index.d.ts.map +1 -1
  22. package/payload/platform/lib/storage-broker/dist/index.js +1 -0
  23. package/payload/platform/lib/storage-broker/dist/index.js.map +1 -1
  24. package/payload/platform/lib/storage-broker/dist/object-limits.d.ts +27 -0
  25. package/payload/platform/lib/storage-broker/dist/object-limits.d.ts.map +1 -1
  26. package/payload/platform/lib/storage-broker/dist/object-limits.js +27 -7
  27. package/payload/platform/lib/storage-broker/dist/object-limits.js.map +1 -1
  28. package/payload/platform/lib/storage-broker/dist/resource-limits.d.ts +102 -0
  29. package/payload/platform/lib/storage-broker/dist/resource-limits.d.ts.map +1 -0
  30. package/payload/platform/lib/storage-broker/dist/resource-limits.js +105 -0
  31. package/payload/platform/lib/storage-broker/dist/resource-limits.js.map +1 -0
  32. package/payload/platform/lib/storage-broker/src/__tests__/cf-exec.test.ts +130 -7
  33. package/payload/platform/lib/storage-broker/src/__tests__/object-limits.test.ts +30 -0
  34. package/payload/platform/lib/storage-broker/src/__tests__/resource-limits.test.ts +49 -0
  35. package/payload/platform/lib/storage-broker/src/cf-exec.ts +129 -56
  36. package/payload/platform/lib/storage-broker/src/index.ts +1 -0
  37. package/payload/platform/lib/storage-broker/src/object-limits.ts +26 -6
  38. package/payload/platform/lib/storage-broker/src/resource-limits.ts +105 -0
  39. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +5 -4
  40. package/payload/platform/plugins/cloudflare/PLUGIN.md +3 -1
  41. package/payload/platform/plugins/cloudflare/bin/portal-enrol.mjs +96 -0
  42. package/payload/platform/plugins/cloudflare/mcp/__tests__/auth-route.test.ts +220 -0
  43. package/payload/platform/plugins/cloudflare/mcp/__tests__/authorize.test.ts +201 -0
  44. package/payload/platform/plugins/cloudflare/mcp/__tests__/file-routes.test.ts +178 -0
  45. package/payload/platform/plugins/cloudflare/mcp/__tests__/harness.test.ts +32 -0
  46. package/payload/platform/plugins/cloudflare/mcp/__tests__/passcode.test.ts +204 -0
  47. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-enrol.test.ts +207 -0
  48. package/payload/platform/plugins/cloudflare/mcp/__tests__/ratelimit.test.ts +77 -0
  49. package/payload/platform/plugins/cloudflare/mcp/__tests__/skill-contract.test.ts +46 -0
  50. package/payload/platform/plugins/cloudflare/mcp/__tests__/template-config.test.ts +55 -0
  51. package/payload/platform/plugins/cloudflare/mcp/__tests__/upload-route.test.ts +255 -0
  52. package/payload/platform/plugins/cloudflare/mcp/package.json +5 -2
  53. package/payload/platform/plugins/cloudflare/references/r2-object-storage.md +46 -0
  54. package/payload/platform/plugins/cloudflare/skills/data-portal/SKILL.md +125 -0
  55. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/authorize.ts +32 -0
  56. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/log.ts +12 -0
  57. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/passcode.mjs +193 -0
  58. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/ratelimit.ts +55 -0
  59. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/session.ts +85 -0
  60. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/types.ts +44 -0
  61. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/auth.ts +119 -0
  62. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/delete.ts +89 -0
  63. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/download.ts +70 -0
  64. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/files.ts +41 -0
  65. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/upload.ts +151 -0
  66. package/payload/platform/plugins/cloudflare/skills/data-portal/template/public/index.html +42 -0
  67. package/payload/platform/plugins/cloudflare/skills/data-portal/template/public/portal.css +113 -0
  68. package/payload/platform/plugins/cloudflare/skills/data-portal/template/public/portal.js +155 -0
  69. package/payload/platform/plugins/cloudflare/skills/data-portal/template/schema.sql +73 -0
  70. package/payload/platform/plugins/cloudflare/skills/data-portal/template/wrangler.toml +21 -0
  71. package/payload/platform/plugins/cloudflare/skills/site-deploy/SKILL.md +8 -0
  72. package/payload/platform/plugins/docs/references/outlook-guide.md +3 -3
  73. package/payload/platform/plugins/docs/references/voice-mirror-guide.md +1 -0
  74. package/payload/platform/plugins/filesystem/PLUGIN.md +0 -1
  75. package/payload/platform/plugins/filesystem/mcp/dist/lib/scope.d.ts +14 -10
  76. package/payload/platform/plugins/filesystem/mcp/dist/lib/scope.d.ts.map +1 -1
  77. package/payload/platform/plugins/filesystem/mcp/dist/lib/scope.js +14 -10
  78. package/payload/platform/plugins/filesystem/mcp/dist/lib/scope.js.map +1 -1
  79. package/payload/platform/plugins/outlook/.claude-plugin/plugin.json +1 -1
  80. package/payload/platform/plugins/outlook/PLUGIN.md +3 -5
  81. package/payload/platform/plugins/outlook/mcp/dist/__tests__/detach.test.d.ts +2 -0
  82. package/payload/platform/plugins/outlook/mcp/dist/__tests__/detach.test.d.ts.map +1 -0
  83. package/payload/platform/plugins/outlook/mcp/dist/__tests__/detach.test.js +75 -0
  84. package/payload/platform/plugins/outlook/mcp/dist/__tests__/detach.test.js.map +1 -0
  85. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-removal.test.d.ts +2 -0
  86. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-removal.test.d.ts.map +1 -0
  87. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-removal.test.js +187 -0
  88. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-removal.test.js.map +1 -0
  89. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit.test.js +1 -1
  90. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit.test.js.map +1 -1
  91. package/payload/platform/plugins/outlook/mcp/dist/index.js +3 -2
  92. package/payload/platform/plugins/outlook/mcp/dist/index.js.map +1 -1
  93. package/payload/platform/plugins/outlook/mcp/dist/lib/attachment-list.d.ts +43 -0
  94. package/payload/platform/plugins/outlook/mcp/dist/lib/attachment-list.d.ts.map +1 -0
  95. package/payload/platform/plugins/outlook/mcp/dist/lib/attachment-list.js +65 -0
  96. package/payload/platform/plugins/outlook/mcp/dist/lib/attachment-list.js.map +1 -0
  97. package/payload/platform/plugins/outlook/mcp/dist/lib/detach.d.ts +65 -0
  98. package/payload/platform/plugins/outlook/mcp/dist/lib/detach.d.ts.map +1 -0
  99. package/payload/platform/plugins/outlook/mcp/dist/lib/detach.js +99 -0
  100. package/payload/platform/plugins/outlook/mcp/dist/lib/detach.js.map +1 -0
  101. package/payload/platform/plugins/outlook/mcp/dist/lib/outbound-attachments.d.ts +6 -1
  102. package/payload/platform/plugins/outlook/mcp/dist/lib/outbound-attachments.d.ts.map +1 -1
  103. package/payload/platform/plugins/outlook/mcp/dist/lib/outbound-attachments.js.map +1 -1
  104. package/payload/platform/plugins/outlook/mcp/dist/tools/draft-edit.d.ts +42 -22
  105. package/payload/platform/plugins/outlook/mcp/dist/tools/draft-edit.d.ts.map +1 -1
  106. package/payload/platform/plugins/outlook/mcp/dist/tools/draft-edit.js +59 -23
  107. package/payload/platform/plugins/outlook/mcp/dist/tools/draft-edit.js.map +1 -1
  108. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-attachment.d.ts +15 -16
  109. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-attachment.d.ts.map +1 -1
  110. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-attachment.js +8 -41
  111. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-attachment.js.map +1 -1
  112. package/payload/platform/plugins/outlook/references/auth.md +1 -1
  113. package/payload/platform/plugins/outlook/references/graph-surfaces.md +7 -3
  114. package/payload/platform/plugins/outlook/skills/outlook/SKILL.md +1 -1
  115. package/payload/platform/plugins/storage-broker/PLUGIN.md +1 -1
  116. package/payload/platform/plugins/voice-mirror/PLUGIN.md +19 -7
  117. package/payload/platform/plugins/voice-mirror/mcp/dist/index.d.ts.map +1 -1
  118. package/payload/platform/plugins/voice-mirror/mcp/dist/index.js +35 -16
  119. package/payload/platform/plugins/voice-mirror/mcp/dist/index.js.map +1 -1
  120. package/payload/platform/plugins/voice-mirror/mcp/dist/lib/admin-user.d.ts +31 -0
  121. package/payload/platform/plugins/voice-mirror/mcp/dist/lib/admin-user.d.ts.map +1 -0
  122. package/payload/platform/plugins/voice-mirror/mcp/dist/lib/admin-user.js +87 -0
  123. package/payload/platform/plugins/voice-mirror/mcp/dist/lib/admin-user.js.map +1 -0
  124. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-distil-profile.d.ts.map +1 -1
  125. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-distil-profile.js +111 -12
  126. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-distil-profile.js.map +1 -1
  127. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-ingest-session-text.d.ts.map +1 -1
  128. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-ingest-session-text.js +2 -1
  129. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-ingest-session-text.js.map +1 -1
  130. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-record-feedback.d.ts.map +1 -1
  131. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-record-feedback.js +2 -1
  132. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-record-feedback.js.map +1 -1
  133. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-retrieve-conditioning.d.ts.map +1 -1
  134. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-retrieve-conditioning.js +54 -21
  135. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-retrieve-conditioning.js.map +1 -1
  136. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-tag-content.d.ts +2 -2
  137. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-tag-content.d.ts.map +1 -1
  138. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-tag-content.js +12 -3
  139. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-tag-content.js.map +1 -1
  140. package/payload/platform/plugins/voice-mirror/mcp/scripts/smoke.mjs +444 -0
  141. package/payload/platform/plugins/voice-mirror/skills/voice-mirror/SKILL.md +18 -2
  142. package/payload/platform/scripts/__tests__/logs-rotate.test.sh +256 -0
  143. package/payload/platform/scripts/__tests__/resume-tunnel.test.sh +79 -0
  144. package/payload/platform/scripts/logs-rotate.sh +204 -0
  145. package/payload/platform/scripts/resume-tunnel.sh +27 -0
  146. package/payload/platform/scripts/voice-author-key-audit.sh +155 -0
  147. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
  148. package/payload/platform/services/claude-session-manager/dist/http-server.js +41 -5
  149. package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
  150. package/payload/platform/services/claude-session-manager/dist/index.d.ts +1 -0
  151. package/payload/platform/services/claude-session-manager/dist/index.d.ts.map +1 -1
  152. package/payload/platform/services/claude-session-manager/dist/index.js +22 -0
  153. package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
  154. package/payload/platform/services/claude-session-manager/dist/install-start-counter.d.ts +2 -0
  155. package/payload/platform/services/claude-session-manager/dist/install-start-counter.d.ts.map +1 -0
  156. package/payload/platform/services/claude-session-manager/dist/install-start-counter.js +12 -0
  157. package/payload/platform/services/claude-session-manager/dist/install-start-counter.js.map +1 -0
  158. package/payload/platform/services/claude-session-manager/dist/mcp-descriptor-resolve.d.ts +17 -0
  159. package/payload/platform/services/claude-session-manager/dist/mcp-descriptor-resolve.d.ts.map +1 -0
  160. package/payload/platform/services/claude-session-manager/dist/mcp-descriptor-resolve.js +88 -0
  161. package/payload/platform/services/claude-session-manager/dist/mcp-descriptor-resolve.js.map +1 -0
  162. package/payload/platform/services/claude-session-manager/dist/pty-census.d.ts +122 -9
  163. package/payload/platform/services/claude-session-manager/dist/pty-census.d.ts.map +1 -1
  164. package/payload/platform/services/claude-session-manager/dist/pty-census.js +214 -29
  165. package/payload/platform/services/claude-session-manager/dist/pty-census.js.map +1 -1
  166. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +0 -1
  167. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
  168. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +36 -24
  169. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
  170. package/payload/platform/services/claude-session-manager/dist/rc-daemon.d.ts.map +1 -1
  171. package/payload/platform/services/claude-session-manager/dist/rc-daemon.js +13 -1
  172. package/payload/platform/services/claude-session-manager/dist/rc-daemon.js.map +1 -1
  173. package/payload/platform/services/claude-session-manager/dist/session-memory-cap.d.ts.map +1 -1
  174. package/payload/platform/services/claude-session-manager/dist/session-memory-cap.js +4 -2
  175. package/payload/platform/services/claude-session-manager/dist/session-memory-cap.js.map +1 -1
  176. package/payload/platform/services/claude-session-manager/dist/slice-memory-policy.d.ts +150 -0
  177. package/payload/platform/services/claude-session-manager/dist/slice-memory-policy.d.ts.map +1 -0
  178. package/payload/platform/services/claude-session-manager/dist/slice-memory-policy.js +333 -0
  179. package/payload/platform/services/claude-session-manager/dist/slice-memory-policy.js.map +1 -0
  180. package/payload/platform/services/claude-session-manager/dist/start-counter.d.ts +10 -0
  181. package/payload/platform/services/claude-session-manager/dist/start-counter.d.ts.map +1 -0
  182. package/payload/platform/services/claude-session-manager/dist/start-counter.js +55 -0
  183. package/payload/platform/services/claude-session-manager/dist/start-counter.js.map +1 -0
  184. package/payload/server/{chunk-Q6W4U6HL.js → chunk-JXWFVE5X.js} +83 -39
  185. package/payload/server/server.js +268 -73
  186. package/payload/server/{src-3I2RYZFB.js → src-4F37OHLK.js} +11 -1
  187. package/payload/platform/plugins/outlook/mcp/dist/__tests__/pkce-flow.test.d.ts +0 -2
  188. package/payload/platform/plugins/outlook/mcp/dist/__tests__/pkce-flow.test.js +0 -215
  189. package/payload/platform/plugins/outlook/mcp/dist/__tests__/pkce-flow.test.js.map +0 -1
@@ -0,0 +1,150 @@
1
+ import { type PtyCensusSnapshot, type Logger } from './pty-census.js';
2
+ import { type SystemctlRunner } from './systemd-scope.js';
3
+ import type { FsWatcher, SessionRow } from './fs-watcher.js';
4
+ /** Reclaim an idle operator session when free RAM falls below this. Sits one
5
+ * step above Task 600's MIN_ADMISSION_MB (4 GB) so reclaim precedes refusal:
6
+ * recovering RAM is preferable to refusing work. A judgement call, not a
7
+ * measurement. */
8
+ export declare const RECLAIM_AT_KB: number;
9
+ /** Free RAM the slice's MemoryHigh leaves for everything else. Also absorbs
10
+ * the baseline's own imprecision (see resolveNonSessionBaselineKb). */
11
+ export declare const THROTTLE_MARGIN_KB: number;
12
+ /** Re-apply MemoryHigh only on a delta larger than this. Chosen to exceed the
13
+ * baseline's imprecision so measurement noise cannot trigger a write. */
14
+ export declare const HYSTERESIS_KB: number;
15
+ /** Floor for the slice bound on a small host. Mirrors SLICE_MIN_HIGH_BYTES in
16
+ * packages/create-maxy-code/src/port-resolution.ts. */
17
+ export declare const SLICE_MIN_HIGH_KB: number;
18
+ /** How long an operator session's transcript must have been static before it
19
+ * is a reclaim candidate. Far below the 8.7 h floor of the measured idle
20
+ * population, and long enough not to take a session an operator is mid-thought
21
+ * on. A judgement call, not a measurement. */
22
+ export declare const IDLE_TTL_MS: number;
23
+ /** RAM held by everything that is neither a session nor free: the co-resident
24
+ * Neo4j JVMs, Ollama, the brand servers, edge/manager, the OS.
25
+ *
26
+ * TAKES THE SLICE'S anon, NOT ITS memory.current, and the distinction is the
27
+ * whole correctness of this function. memory.current = anon + file + slab + …,
28
+ * where `file` is page cache the kernel hands back under pressure — and
29
+ * MemAvailable ALREADY counts that cache as available. Subtracting
30
+ * memory.current from a MemAvailable-derived residual therefore subtracts the
31
+ * slice's cache twice, UNDER-stating the baseline and so OVER-stating the
32
+ * bound (high = total - baseline - margin). That is the unsafe direction: a
33
+ * weaker throttle. Worse, it ratchets — slice cache growth raises sliceKb
34
+ * while leaving sysAvailKb untouched, so the bound climbs monotonically back
35
+ * toward the ~96%-of-RAM value this task exists to remove. anon has no such
36
+ * overlap with MemAvailable, so none of that happens.
37
+ *
38
+ * Measured on the laptop, 2026-07-17 (slice: current 7.94 GB, anon 5.61 GB,
39
+ * file 1.16 GB, slab 0.99 GB; box: total 62.5 GiB, avail 38.3 GiB):
40
+ * from anon -> 18.97 GiB
41
+ * from memory.current -> 16.8 GiB (2.2 GB low — exactly file + slab)
42
+ * The task measured Neo4j's four JVMs plus Ollama at ~19 GB independently, so
43
+ * the anon form reproduces a figure derived a different way. That agreement is
44
+ * the evidence for this formula; the memory.current form disagreed with it by
45
+ * the precise size of the double-count.
46
+ *
47
+ * Stable as sessions come and go: session anon growth consumes sysAvailKb and
48
+ * raises sliceAnonKb by the same amount, so the baseline holds. Cache growth
49
+ * moves neither. Only a real baseline change (a brand added, Ollama loading a
50
+ * model) moves it — which is what should move it.
51
+ *
52
+ * Still an estimate: slab is charged to the slice but counted here as
53
+ * non-session, because MemAvailable counts only the reclaimable part of it.
54
+ * That over-states the baseline slightly, making the bound SMALLER — this time
55
+ * genuinely the safe direction, and THROTTLE_MARGIN_KB absorbs it. The reclaim
56
+ * trigger reads sysAvailKb alone and needs none of this arithmetic. */
57
+ export declare function resolveNonSessionBaselineKb(sysTotalKb: number, sysAvailKb: number, sliceAnonKb: number): number;
58
+ /** The slice's soft ceiling: total RAM, less what non-session processes hold,
59
+ * less a margin to leave free. Soft throughout — MemoryHigh throttles and
60
+ * reclaims, never kills.
61
+ *
62
+ * BE HONEST ABOUT WHAT THIS IS. On the non-floored branch it reduces:
63
+ *
64
+ * high = T - max(0, T - A - anon) - M = A + anon - M
65
+ *
66
+ * sysTotalKb cancels. The bound is therefore NOT "a share of total RAM" — it
67
+ * is "what the slice's unreclaimable footprint is now, plus what is genuinely
68
+ * free, less the margin", and it is identical on a 62 GB box and a 200 GB box
69
+ * with the same A and anon. That is correct (A already encodes T, and the
70
+ * question is how far the slice may grow from where it is), but the
71
+ * total-minus-baseline SHAPE reads as though total RAM drives the answer, and
72
+ * it does not. The two-function form is kept because the baseline is a real
73
+ * quantity worth logging on its own — it is what the operator needs to see to
74
+ * understand WHY the bound is what it is — not because total RAM is an input
75
+ * in any meaningful sense.
76
+ *
77
+ * Units: MemoryHigh acts on memory.current, while the baseline is derived from
78
+ * anon. Deliberate, not an oversight — the bound must be reachable in
79
+ * memory.current terms, but the HEADROOM it encodes is an anon question, since
80
+ * only anon growth consumes MemAvailable.
81
+ *
82
+ * What the kernel reclaims when the bound is crossed, exactly. The excess is
83
+ * (anon + file + slab) - (A + anon - M) = (file + slab) - (A - M)
84
+ * and the reclaimable cache on hand is (file + slab). So WHILE A >= M the
85
+ * excess is always fully coverable from cache: the slice's cache is reclaimed,
86
+ * which is what MemoryHigh is for, and no session is throttled. Below A = M
87
+ * the bound falls under anon and the kernel does throttle the cohort's anon —
88
+ * correct, and the designed third tier (recover at 8 GB free, refuse at 4 GB,
89
+ * throttle here), but not free, so the qualifier matters. */
90
+ export declare function resolveSliceMemoryHighKb(sysTotalKb: number, nonSessionKb: number, marginKb: number): number;
91
+ /** True when the bound is worth writing: never applied, or moved by more than
92
+ * HYSTERESIS_KB. The baseline is stable but not noise-free, and every
93
+ * co-resident brand's daemon derives the SAME box-wide value, so without a
94
+ * band four daemons would rewrite the same drop-in on every tick. */
95
+ export declare function shouldApplyMemoryHigh(desiredKb: number, currentKb: number | null): boolean;
96
+ /** The idle-oldest operator session, or null.
97
+ *
98
+ * There is NO transcript check, and the inversion is the point. reaper.ts:63
99
+ * refuses to reap anything with a transcript on disk, because for a specialist
100
+ * a transcript means real work was done. Here the transcript is the
101
+ * PRECONDITION that makes stopping safe: it is the durable artifact, and
102
+ * `--resume <sessionId>` reconstitutes the session from it. Stopping an idle
103
+ * operator session is not lossy — it trades RAM for resume latency.
104
+ *
105
+ * "Idle" here means status:'idle' — NOT PRODUCING OUTPUT. It does NOT mean
106
+ * detached, and these must not be conflated: an operator may be attached and
107
+ * watching a session this returns. Under pressure that session can still be
108
+ * taken. No attachment signal is consulted; the one that exists (Task 1705's
109
+ * stream-lifecycle) lives in the UI server process, not this one. */
110
+ export declare function selectVictim(rows: readonly SessionRow[], nowMs: number, idleTtlMs: number): SessionRow | null;
111
+ export interface ReclaimLine {
112
+ sessionId: string;
113
+ /** The scope unit stopped, or 'none' when no tracker resolved. */
114
+ unit: string;
115
+ idleMs: number;
116
+ sysAvailKb: number;
117
+ thresholdKb: number;
118
+ /** Null renders as `unknown`, never 0 — see formatPtyCensus. */
119
+ sliceKb: number | null;
120
+ }
121
+ /** Pure — no I/O. Carries what happened AND the numbers that caused it, so an
122
+ * auto-stop is as legible as an operator stop. */
123
+ export declare function formatReclaim(a: ReclaimLine): string;
124
+ /** Pure — no I/O. Pressure with no reclaimable session is a real state and must
125
+ * not be silent; mirrors `[pty-cap] ... evicted=none`. */
126
+ export declare function formatPressureNoVictim(sysAvailKb: number, thresholdKb: number): string;
127
+ /** Pure — no I/O. Names the baseline so an operator can see WHY the bound is
128
+ * what it is, not merely that it changed. */
129
+ export declare function formatSliceHighApplied(baselineKb: number, highKb: number, previousKb: number | null): string;
130
+ export interface SliceMemoryPolicy {
131
+ start(): void;
132
+ stop(): void;
133
+ /** Run one pass synchronously. Exposed for tests. */
134
+ tickOnce(): void;
135
+ }
136
+ export interface SliceMemoryPolicyDeps {
137
+ watcher: FsWatcher;
138
+ intervalMs: number;
139
+ killGraceMs: number;
140
+ logger: Logger;
141
+ /** Injectable clock for tests. Defaults to Date.now. */
142
+ now?: () => number;
143
+ /** Injectable measurement for tests. Defaults to the census collector, whose
144
+ * reads this deliberately reuses rather than duplicating. */
145
+ collect?: () => PtyCensusSnapshot;
146
+ /** Injectable systemctl for tests. Defaults to spawnSync. */
147
+ runSystemctl?: SystemctlRunner;
148
+ }
149
+ export declare function createSliceMemoryPolicy(deps: SliceMemoryPolicyDeps): SliceMemoryPolicy;
150
+ //# sourceMappingURL=slice-memory-policy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slice-memory-policy.d.ts","sourceRoot":"","sources":["../src/slice-memory-policy.ts"],"names":[],"mappings":"AAuCA,OAAO,EAAoB,KAAK,iBAAiB,EAAE,KAAK,MAAM,EAAE,MAAM,iBAAiB,CAAA;AACvF,OAAO,EAIL,KAAK,eAAe,EACrB,MAAM,oBAAoB,CAAA;AAE3B,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAE5D;;;mBAGmB;AACnB,eAAO,MAAM,aAAa,QAAkB,CAAA;AAE5C;wEACwE;AACxE,eAAO,MAAM,kBAAkB,QAAkB,CAAA;AAEjD;0EAC0E;AAC1E,eAAO,MAAM,aAAa,QAAkB,CAAA;AAE5C;wDACwD;AACxD,eAAO,MAAM,iBAAiB,QAAkB,CAAA;AAEhD;;;+CAG+C;AAC/C,eAAO,MAAM,WAAW,QAAiB,CAAA;AAEzC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;wEAiCwE;AACxE,wBAAgB,2BAA2B,CACzC,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,GAClB,MAAM,CAER;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;8DA+B8D;AAC9D,wBAAgB,wBAAwB,CACtC,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,GACf,MAAM,CAER;AAED;;;sEAGsE;AACtE,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAG1F;AAED;;;;;;;;;;;;;sEAasE;AACtE,wBAAgB,YAAY,CAC1B,IAAI,EAAE,SAAS,UAAU,EAAE,EAC3B,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,GAChB,UAAU,GAAG,IAAI,CAUnB;AAMD,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,MAAM,CAAA;IACjB,kEAAkE;IAClE,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,MAAM,CAAA;IAClB,WAAW,EAAE,MAAM,CAAA;IACnB,gEAAgE;IAChE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;CACvB;AAED;mDACmD;AACnD,wBAAgB,aAAa,CAAC,CAAC,EAAE,WAAW,GAAG,MAAM,CAMpD;AAED;2DAC2D;AAC3D,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAEtF;AAED;8CAC8C;AAC9C,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,GAAG,IAAI,GACxB,MAAM,CAKR;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,IAAI,IAAI,CAAA;IACb,IAAI,IAAI,IAAI,CAAA;IACZ,qDAAqD;IACrD,QAAQ,IAAI,IAAI,CAAA;CACjB;AAED,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,SAAS,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,MAAM,CAAA;IACd,wDAAwD;IACxD,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;IAClB;kEAC8D;IAC9D,OAAO,CAAC,EAAE,MAAM,iBAAiB,CAAA;IACjC,6DAA6D;IAC7D,YAAY,CAAC,EAAE,eAAe,CAAA;CAC/B;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,qBAAqB,GAAG,iBAAiB,CAoKtF"}
@@ -0,0 +1,333 @@
1
+ // Task 1709 — operator sessions have no idle-stop path, and the slice
2
+ // MemoryHigh cannot fire before the box thrashes.
3
+ //
4
+ // Two gaps, one consequence. reaper.ts:59 returns false unless agent !== null
5
+ // and :63 returns false if a transcript exists, so it reaps only specialists
6
+ // that never wrote one; the specialist LRU (pty-spawner.ts:1224) is likewise
7
+ // specialist-only and checkSlotsAndMaybeEvict returns early for any
8
+ // non-specialist spawn. An operator session is reaped by nothing and evicted
9
+ // by nothing. Meanwhile the installer sized the slice's MemoryHigh at total
10
+ // RAM minus a FIXED 2.7 GiB floor — a floor scoped to ONE brand's "neo4j +
11
+ // brand server + edge/manager/chrome". Four co-resident brands run four Neo4j
12
+ // JVMs plus Ollama, so the real non-session baseline is ~19-21 GB and the
13
+ // bound landed at ~96% of total RAM, where the session cohort can never reach
14
+ // it.
15
+ //
16
+ // The floor could not have been a better constant: the installer does not
17
+ // configure Neo4j's heap, and Neo4j's own default sizing scales with host RAM.
18
+ // The baseline is a function of host RAM AND co-resident brand count, so it is
19
+ // knowable only by measurement. Hence: measure, do not guess.
20
+ //
21
+ // THE TRIGGER IS MemAvailable, NOT THE SLICE. /proc/meminfo's MemAvailable is
22
+ // already box-wide and already correct (Task 1707 certifies sysAvailMb /
23
+ // sysTotalMb; Task 600's admission gate already trusts it). Because the signal
24
+ // is box-wide, the aggregate bound holds across all four co-resident brands
25
+ // with NO cross-daemon coordination and NO shared state — brand isolation is
26
+ // untouched. The failure the aggregate bound must avoid is a per-daemon COUNT
27
+ // cap (RC_DAEMON_CAPACITY=10) being structurally unable to express a shared RAM
28
+ // constraint; a box-wide RAM signal does express it.
29
+ //
30
+ // Ordering against the existing gates, deliberate:
31
+ // sysAvail < RECLAIM_AT (8G) -> reclaim idle-oldest operator session
32
+ // memAvail < MIN_ADMISSION_MB (4G) -> refuse new specialists (Task 600)
33
+ // slice > MemoryHigh -> kernel throttles the cohort
34
+ // Recover, then refuse, then throttle.
35
+ //
36
+ // Shape mirrors pty-census.ts / session-cap-audit.ts: PURE derivations + PURE
37
+ // formatters (no I/O, unit-tested) + an impure collector + a start/stop factory
38
+ // whose setInterval is unref'd and cleared on shutdown.
39
+ import { collectPtyCensus } from './pty-census.js';
40
+ import { CLAUDE_PTYS_SLICE, scopeUnitName, defaultSystemctlRunner, } from './systemd-scope.js';
41
+ import { stopSession, getPtyTrackerForTests } from './pty-spawner.js';
42
+ /** Reclaim an idle operator session when free RAM falls below this. Sits one
43
+ * step above Task 600's MIN_ADMISSION_MB (4 GB) so reclaim precedes refusal:
44
+ * recovering RAM is preferable to refusing work. A judgement call, not a
45
+ * measurement. */
46
+ export const RECLAIM_AT_KB = 8 * 1024 * 1024;
47
+ /** Free RAM the slice's MemoryHigh leaves for everything else. Also absorbs
48
+ * the baseline's own imprecision (see resolveNonSessionBaselineKb). */
49
+ export const THROTTLE_MARGIN_KB = 4 * 1024 * 1024;
50
+ /** Re-apply MemoryHigh only on a delta larger than this. Chosen to exceed the
51
+ * baseline's imprecision so measurement noise cannot trigger a write. */
52
+ export const HYSTERESIS_KB = 2 * 1024 * 1024;
53
+ /** Floor for the slice bound on a small host. Mirrors SLICE_MIN_HIGH_BYTES in
54
+ * packages/create-maxy-code/src/port-resolution.ts. */
55
+ export const SLICE_MIN_HIGH_KB = 2 * 1024 * 1024;
56
+ /** How long an operator session's transcript must have been static before it
57
+ * is a reclaim candidate. Far below the 8.7 h floor of the measured idle
58
+ * population, and long enough not to take a session an operator is mid-thought
59
+ * on. A judgement call, not a measurement. */
60
+ export const IDLE_TTL_MS = 30 * 60 * 1000;
61
+ /** RAM held by everything that is neither a session nor free: the co-resident
62
+ * Neo4j JVMs, Ollama, the brand servers, edge/manager, the OS.
63
+ *
64
+ * TAKES THE SLICE'S anon, NOT ITS memory.current, and the distinction is the
65
+ * whole correctness of this function. memory.current = anon + file + slab + …,
66
+ * where `file` is page cache the kernel hands back under pressure — and
67
+ * MemAvailable ALREADY counts that cache as available. Subtracting
68
+ * memory.current from a MemAvailable-derived residual therefore subtracts the
69
+ * slice's cache twice, UNDER-stating the baseline and so OVER-stating the
70
+ * bound (high = total - baseline - margin). That is the unsafe direction: a
71
+ * weaker throttle. Worse, it ratchets — slice cache growth raises sliceKb
72
+ * while leaving sysAvailKb untouched, so the bound climbs monotonically back
73
+ * toward the ~96%-of-RAM value this task exists to remove. anon has no such
74
+ * overlap with MemAvailable, so none of that happens.
75
+ *
76
+ * Measured on the laptop, 2026-07-17 (slice: current 7.94 GB, anon 5.61 GB,
77
+ * file 1.16 GB, slab 0.99 GB; box: total 62.5 GiB, avail 38.3 GiB):
78
+ * from anon -> 18.97 GiB
79
+ * from memory.current -> 16.8 GiB (2.2 GB low — exactly file + slab)
80
+ * The task measured Neo4j's four JVMs plus Ollama at ~19 GB independently, so
81
+ * the anon form reproduces a figure derived a different way. That agreement is
82
+ * the evidence for this formula; the memory.current form disagreed with it by
83
+ * the precise size of the double-count.
84
+ *
85
+ * Stable as sessions come and go: session anon growth consumes sysAvailKb and
86
+ * raises sliceAnonKb by the same amount, so the baseline holds. Cache growth
87
+ * moves neither. Only a real baseline change (a brand added, Ollama loading a
88
+ * model) moves it — which is what should move it.
89
+ *
90
+ * Still an estimate: slab is charged to the slice but counted here as
91
+ * non-session, because MemAvailable counts only the reclaimable part of it.
92
+ * That over-states the baseline slightly, making the bound SMALLER — this time
93
+ * genuinely the safe direction, and THROTTLE_MARGIN_KB absorbs it. The reclaim
94
+ * trigger reads sysAvailKb alone and needs none of this arithmetic. */
95
+ export function resolveNonSessionBaselineKb(sysTotalKb, sysAvailKb, sliceAnonKb) {
96
+ return Math.max(0, sysTotalKb - sysAvailKb - sliceAnonKb);
97
+ }
98
+ /** The slice's soft ceiling: total RAM, less what non-session processes hold,
99
+ * less a margin to leave free. Soft throughout — MemoryHigh throttles and
100
+ * reclaims, never kills.
101
+ *
102
+ * BE HONEST ABOUT WHAT THIS IS. On the non-floored branch it reduces:
103
+ *
104
+ * high = T - max(0, T - A - anon) - M = A + anon - M
105
+ *
106
+ * sysTotalKb cancels. The bound is therefore NOT "a share of total RAM" — it
107
+ * is "what the slice's unreclaimable footprint is now, plus what is genuinely
108
+ * free, less the margin", and it is identical on a 62 GB box and a 200 GB box
109
+ * with the same A and anon. That is correct (A already encodes T, and the
110
+ * question is how far the slice may grow from where it is), but the
111
+ * total-minus-baseline SHAPE reads as though total RAM drives the answer, and
112
+ * it does not. The two-function form is kept because the baseline is a real
113
+ * quantity worth logging on its own — it is what the operator needs to see to
114
+ * understand WHY the bound is what it is — not because total RAM is an input
115
+ * in any meaningful sense.
116
+ *
117
+ * Units: MemoryHigh acts on memory.current, while the baseline is derived from
118
+ * anon. Deliberate, not an oversight — the bound must be reachable in
119
+ * memory.current terms, but the HEADROOM it encodes is an anon question, since
120
+ * only anon growth consumes MemAvailable.
121
+ *
122
+ * What the kernel reclaims when the bound is crossed, exactly. The excess is
123
+ * (anon + file + slab) - (A + anon - M) = (file + slab) - (A - M)
124
+ * and the reclaimable cache on hand is (file + slab). So WHILE A >= M the
125
+ * excess is always fully coverable from cache: the slice's cache is reclaimed,
126
+ * which is what MemoryHigh is for, and no session is throttled. Below A = M
127
+ * the bound falls under anon and the kernel does throttle the cohort's anon —
128
+ * correct, and the designed third tier (recover at 8 GB free, refuse at 4 GB,
129
+ * throttle here), but not free, so the qualifier matters. */
130
+ export function resolveSliceMemoryHighKb(sysTotalKb, nonSessionKb, marginKb) {
131
+ return Math.max(SLICE_MIN_HIGH_KB, sysTotalKb - nonSessionKb - marginKb);
132
+ }
133
+ /** True when the bound is worth writing: never applied, or moved by more than
134
+ * HYSTERESIS_KB. The baseline is stable but not noise-free, and every
135
+ * co-resident brand's daemon derives the SAME box-wide value, so without a
136
+ * band four daemons would rewrite the same drop-in on every tick. */
137
+ export function shouldApplyMemoryHigh(desiredKb, currentKb) {
138
+ if (currentKb === null)
139
+ return true;
140
+ return Math.abs(desiredKb - currentKb) > HYSTERESIS_KB;
141
+ }
142
+ /** The idle-oldest operator session, or null.
143
+ *
144
+ * There is NO transcript check, and the inversion is the point. reaper.ts:63
145
+ * refuses to reap anything with a transcript on disk, because for a specialist
146
+ * a transcript means real work was done. Here the transcript is the
147
+ * PRECONDITION that makes stopping safe: it is the durable artifact, and
148
+ * `--resume <sessionId>` reconstitutes the session from it. Stopping an idle
149
+ * operator session is not lossy — it trades RAM for resume latency.
150
+ *
151
+ * "Idle" here means status:'idle' — NOT PRODUCING OUTPUT. It does NOT mean
152
+ * detached, and these must not be conflated: an operator may be attached and
153
+ * watching a session this returns. Under pressure that session can still be
154
+ * taken. No attachment signal is consulted; the one that exists (Task 1705's
155
+ * stream-lifecycle) lives in the UI server process, not this one. */
156
+ export function selectVictim(rows, nowMs, idleTtlMs) {
157
+ const candidates = rows.filter((r) => r.agent === null &&
158
+ r.state === 'live' &&
159
+ r.status === 'idle' &&
160
+ nowMs - r.updatedAt > idleTtlMs);
161
+ if (candidates.length === 0)
162
+ return null;
163
+ return candidates.reduce((oldest, r) => (r.updatedAt < oldest.updatedAt ? r : oldest));
164
+ }
165
+ const TAG = '[session-reclaim]';
166
+ const kbToMb = (kb) => Math.round(kb / 1024);
167
+ /** Pure — no I/O. Carries what happened AND the numbers that caused it, so an
168
+ * auto-stop is as legible as an operator stop. */
169
+ export function formatReclaim(a) {
170
+ return (`${TAG} reclaimed sessionId=${a.sessionId} unit=${a.unit} idle-ms=${a.idleMs} ` +
171
+ `memAvailMb=${kbToMb(a.sysAvailKb)} thresholdMb=${kbToMb(a.thresholdKb)} ` +
172
+ `sliceMb=${a.sliceKb === null ? 'unknown' : kbToMb(a.sliceKb)} reason=memory-pressure`);
173
+ }
174
+ /** Pure — no I/O. Pressure with no reclaimable session is a real state and must
175
+ * not be silent; mirrors `[pty-cap] ... evicted=none`. */
176
+ export function formatPressureNoVictim(sysAvailKb, thresholdKb) {
177
+ return `${TAG} pressure memAvailMb=${kbToMb(sysAvailKb)} thresholdMb=${kbToMb(thresholdKb)} victim=none`;
178
+ }
179
+ /** Pure — no I/O. Names the baseline so an operator can see WHY the bound is
180
+ * what it is, not merely that it changed. */
181
+ export function formatSliceHighApplied(baselineKb, highKb, previousKb) {
182
+ return (`${TAG} slice-high-applied baselineMb=${kbToMb(baselineKb)} highMb=${kbToMb(highKb)} ` +
183
+ `previousMb=${previousKb === null ? 'none' : kbToMb(previousKb)}`);
184
+ }
185
+ export function createSliceMemoryPolicy(deps) {
186
+ const now = deps.now ?? (() => Date.now());
187
+ const collect = deps.collect ?? (() => collectPtyCensus());
188
+ const runSystemctl = deps.runSystemctl ?? defaultSystemctlRunner;
189
+ let timer = null;
190
+ function sizeBound(snap) {
191
+ // sliceAnonKb null: no live scope resolved the slice path, or the memory
192
+ // cgroup controller is disabled — where MemoryHigh is a silent no-op
193
+ // regardless. Skip sizing; reclaim below still runs on sysAvailKb alone.
194
+ if (snap.sliceAnonKb === null)
195
+ return;
196
+ const baselineKb = resolveNonSessionBaselineKb(snap.sysTotalKb, snap.sysAvailKb, snap.sliceAnonKb);
197
+ const desiredKb = resolveSliceMemoryHighKb(snap.sysTotalKb, baselineKb, THROTTLE_MARGIN_KB);
198
+ // Compare against the bound ACTUALLY IN FORCE (read from the slice's
199
+ // memory.high), never against what this process remembers writing. Four
200
+ // co-resident brands write this one file, each on its own tick phase and
201
+ // from its own sysAvailKb sample, so they derive different values and
202
+ // last-writer-wins. A process trusting its own memory would believe a value
203
+ // a sibling has already replaced, and would then decline to re-apply
204
+ // because its own delta looked small. Reading reality also makes
205
+ // previousMb= truthful: it is what this write replaced, not what this
206
+ // process hoped was there.
207
+ if (!shouldApplyMemoryHigh(desiredKb, snap.sliceHighKb))
208
+ return;
209
+ const r = runSystemctl([
210
+ '--user',
211
+ 'set-property',
212
+ CLAUDE_PTYS_SLICE,
213
+ `MemoryHigh=${kbToMb(desiredKb)}M`,
214
+ ]);
215
+ if (r.exitCode !== 0) {
216
+ // Never discard the error body — it is the only account of why the bound
217
+ // this task exists to fix is not in force.
218
+ deps.logger(`${TAG} op=slice-high-failed exitCode=${r.exitCode} stderr=${r.stderr.trim()}`);
219
+ return;
220
+ }
221
+ deps.logger(formatSliceHighApplied(baselineKb, desiredKb, snap.sliceHighKb));
222
+ }
223
+ function reclaim(snap) {
224
+ if (snap.sysAvailKb >= RECLAIM_AT_KB)
225
+ return;
226
+ const victim = selectVictim(deps.watcher.all(), now(), IDLE_TTL_MS);
227
+ if (!victim) {
228
+ deps.logger(formatPressureNoVictim(snap.sysAvailKb, RECLAIM_AT_KB));
229
+ return;
230
+ }
231
+ // The scope token can come from either of two places, and passing only the
232
+ // tracker is how a reclaim silently does nothing. A session spawned by THIS
233
+ // manager is in the module-scoped livePtys map. A session that survived a
234
+ // manager restart is not — it was reattached to its SessionRow by the
235
+ // boot-seed, and its token lives on the row. stopSession takes the row
236
+ // token as a fallback, but ONLY if the caller supplies it; without it, a
237
+ // tracker miss returns no-store-entry immediately, issuing no systemctl
238
+ // call and logging nothing at all.
239
+ const tracker = getPtyTrackerForTests(victim.sessionId);
240
+ const token = tracker ? tracker.scopeUnitToken : victim.scopeUnitToken;
241
+ const unit = token === null ? 'none' : scopeUnitName(token);
242
+ const idleMs = now() - victim.updatedAt;
243
+ // Same stop path as the reaper, operator stop and the specialist LRU:
244
+ // systemctl stop of the scope unit, with in-cgroup SIGTERM->SIGKILL
245
+ // escalation bounded by TimeoutStopSec. Async by design — the tick stays
246
+ // synchronous and the fs-watcher drops the row when the PID file goes — so
247
+ // the outcome is reported when it lands rather than awaited here.
248
+ void stopSession({
249
+ killGraceMs: deps.killGraceMs,
250
+ logger: deps.logger,
251
+ runSystemctl: deps.runSystemctl,
252
+ rowScopeUnitToken: victim.scopeUnitToken,
253
+ rowPid: victim.pid,
254
+ }, victim.sessionId)
255
+ .then((outcome) => {
256
+ // A reclaim that stopped nothing is not a reclaim. no-store-entry means
257
+ // neither a tracker nor a row token resolved, so no signal was ever
258
+ // sent and the RAM this line claims to have recovered is still held.
259
+ // Logging `reclaimed` for it would be a success claim for a no-op —
260
+ // the same class of error as reading "returned" as "succeeded".
261
+ if (outcome.path === 'no-store-entry') {
262
+ deps.logger(`${TAG} op=reclaim-no-op sessionId=${victim.sessionId} reason=no-scope-resolved ` +
263
+ `memAvailMb=${kbToMb(snap.sysAvailKb)} note=ram-still-held`);
264
+ return;
265
+ }
266
+ deps.logger(formatReclaim({
267
+ sessionId: victim.sessionId,
268
+ unit,
269
+ idleMs,
270
+ sysAvailKb: snap.sysAvailKb,
271
+ thresholdKb: RECLAIM_AT_KB,
272
+ sliceKb: snap.sliceKb,
273
+ }));
274
+ })
275
+ .catch((err) => {
276
+ deps.logger(`${TAG} op=reclaim-failed sessionId=${victim.sessionId} ` +
277
+ `message=${err instanceof Error ? err.message : String(err)}`);
278
+ });
279
+ }
280
+ function tickOnce() {
281
+ const snap = collect();
282
+ // A TICK THAT CANNOT MEASURE MUST NOT ACT.
283
+ //
284
+ // readMeminfoKb yields 0 for a missing field, and its two fallbacks are
285
+ // INDEPENDENT — MemTotal parsing while MemAvailable does not gives a
286
+ // positive sysTotalKb beside a sysAvailKb of 0. Both fields must therefore
287
+ // be checked, not just the one:
288
+ //
289
+ // * no /proc at all (darwin, any non-Linux): both read 0.
290
+ // * /proc/meminfo unreadable: both read 0.
291
+ // * MemAvailable absent (kernels < 3.14, some container /proc
292
+ // presentations) while MemTotal is present: sysAvailKb alone reads 0.
293
+ //
294
+ // In every case 0 means UNMEASURED, never "no free RAM" — but the pressure
295
+ // test reads `0 < RECLAIM_AT_KB` as true, so an unguarded tick would stop
296
+ // every idle operator session, once a minute, on a box under no pressure
297
+ // whatsoever. A real Linux host reports a positive MemTotal always, and a
298
+ // positive MemAvailable even under genuine pressure (it is an estimate that
299
+ // includes reclaimable cache, and reaches exactly 0 only when the box is
300
+ // already dead), so this costs no true positives.
301
+ if (snap.sysTotalKb <= 0 || snap.sysAvailKb <= 0) {
302
+ deps.logger(`${TAG} op=tick-unmeasurable sysTotalKb=${snap.sysTotalKb} sysAvailKb=${snap.sysAvailKb} action=none`);
303
+ return;
304
+ }
305
+ sizeBound(snap);
306
+ reclaim(snap);
307
+ }
308
+ return {
309
+ tickOnce,
310
+ start() {
311
+ if (timer)
312
+ return;
313
+ timer = setInterval(() => {
314
+ try {
315
+ tickOnce();
316
+ }
317
+ catch (err) {
318
+ deps.logger(`${TAG} op=tick-failed message=${err instanceof Error ? err.message : String(err)}`);
319
+ }
320
+ }, deps.intervalMs);
321
+ if (typeof timer.unref === 'function')
322
+ timer.unref();
323
+ deps.logger(`${TAG} op=policy-started intervalMs=${deps.intervalMs} reclaimAtMb=${kbToMb(RECLAIM_AT_KB)} idleTtlMs=${IDLE_TTL_MS}`);
324
+ },
325
+ stop() {
326
+ if (timer) {
327
+ clearInterval(timer);
328
+ timer = null;
329
+ }
330
+ },
331
+ };
332
+ }
333
+ //# sourceMappingURL=slice-memory-policy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slice-memory-policy.js","sourceRoot":"","sources":["../src/slice-memory-policy.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ export interface StartCounterDeps {
2
+ accountId?: string;
3
+ tmpDir?: string;
4
+ existsSync?: (p: string) => boolean;
5
+ readFileSync?: (p: string, enc: 'utf8') => string;
6
+ writeFileSync?: (p: string, d: string, enc: 'utf8') => void;
7
+ log?: (line: string) => void;
8
+ }
9
+ export declare function bumpStartCounter(deps?: StartCounterDeps): void;
10
+ //# sourceMappingURL=start-counter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"start-counter.d.ts","sourceRoot":"","sources":["../src/start-counter.ts"],"names":[],"mappings":"AAuBA,MAAM,WAAW,gBAAgB;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,UAAU,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,OAAO,CAAA;IACnC,YAAY,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,MAAM,CAAA;IACjD,aAAa,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;IAC3D,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;CAC7B;AAED,wBAAgB,gBAAgB,CAAC,IAAI,GAAE,gBAAqB,GAAG,IAAI,CAiClE"}
@@ -0,0 +1,55 @@
1
+ // Task 1706 — the manager's boot boundary.
2
+ //
3
+ // server.log is appended to by TWO systemd units: the UI process and this one.
4
+ // The UI stamps a counted start line at every boot (platform/ui/server-init.cjs:79)
5
+ // — 151 of them in sitedesk-code's server.log. This process stamped none, yet it
6
+ // is the one that emits [wa-stream], [claude-session-manager], [webchat-session]
7
+ // and most of the file's volume. So a question like "how many streams are armed
8
+ // right now?" could not be scoped to the live process from the log alone, and
9
+ // Task 1705's investigation had to recover the process start time from `ps` —
10
+ // after first publishing a confidently-wrong answer from unscoped counts.
11
+ //
12
+ // The counter lives in /tmp keyed on ACCOUNT_ID, exactly as server-init.cjs does:
13
+ // /tmp clears on reboot, so a clean reboot starts fresh while a restart loop
14
+ // keeps the same /tmp and shows count>1. The key is DISTINCT from the UI's
15
+ // (`server-init-` vs `session-manager-`) — a shared path would make two
16
+ // co-resident processes clobber each other's counts.
17
+ //
18
+ // The line is emitted via console.error so the Task 666 timestamp sink stamps it;
19
+ // install-log-sink.js must be imported before this runs.
20
+ import { existsSync as fsExistsSync, readFileSync as fsReadFileSync, writeFileSync as fsWriteFileSync } from 'node:fs';
21
+ import { join } from 'node:path';
22
+ export function bumpStartCounter(deps = {}) {
23
+ const accountId = deps.accountId ?? process.env.ACCOUNT_ID ?? 'default';
24
+ const tmpDir = deps.tmpDir ?? '/tmp';
25
+ const existsSync = deps.existsSync ?? fsExistsSync;
26
+ const readFileSync = deps.readFileSync ?? fsReadFileSync;
27
+ const writeFileSync = deps.writeFileSync ?? fsWriteFileSync;
28
+ const log = deps.log ?? ((line) => console.error(line));
29
+ const counterPath = join(tmpDir, `session-manager-${accountId}-restart.count`);
30
+ let prior = 0;
31
+ if (existsSync(counterPath)) {
32
+ try {
33
+ const parsed = Number.parseInt(readFileSync(counterPath, 'utf8').trim(), 10);
34
+ if (Number.isFinite(parsed) && parsed >= 0)
35
+ prior = parsed;
36
+ }
37
+ catch {
38
+ // Unreadable counter — treat as 0. The start line still emits, which is
39
+ // the load-bearing half: a boot boundary with a wrong count still scopes
40
+ // a query, but no boundary at all sends the reader back to `ps`.
41
+ }
42
+ }
43
+ const next = prior + 1;
44
+ try {
45
+ writeFileSync(counterPath, String(next), 'utf8');
46
+ }
47
+ catch (err) {
48
+ // Counter persistence is best-effort; never let it swallow the start line.
49
+ // The error body is named, never discarded — an unwritable /tmp is exactly
50
+ // the condition that would silently reset every count to 1 forever.
51
+ log(`[claude-session-manager] start-counter-write-failed path=${counterPath} err=${err instanceof Error ? err.message : String(err)}`);
52
+ }
53
+ log(`[claude-session-manager] start count=${next} account=${accountId} counter-path=${counterPath}`);
54
+ }
55
+ //# sourceMappingURL=start-counter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"start-counter.js","sourceRoot":"","sources":["../src/start-counter.ts"],"names":[],"mappings":"AAAA,2CAA2C;AAC3C,EAAE;AACF,+EAA+E;AAC/E,oFAAoF;AACpF,iFAAiF;AACjF,iFAAiF;AACjF,gFAAgF;AAChF,8EAA8E;AAC9E,8EAA8E;AAC9E,0EAA0E;AAC1E,EAAE;AACF,kFAAkF;AAClF,6EAA6E;AAC7E,2EAA2E;AAC3E,wEAAwE;AACxE,qDAAqD;AACrD,EAAE;AACF,kFAAkF;AAClF,yDAAyD;AAEzD,OAAO,EAAE,UAAU,IAAI,YAAY,EAAE,YAAY,IAAI,cAAc,EAAE,aAAa,IAAI,eAAe,EAAE,MAAM,SAAS,CAAA;AACtH,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAWhC,MAAM,UAAU,gBAAgB,CAAC,OAAyB,EAAE;IAC1D,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,SAAS,CAAA;IACvE,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,MAAM,CAAA;IACpC,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,IAAI,YAAY,CAAA;IAClD,MAAM,YAAY,GAAG,IAAI,CAAC,YAAY,IAAK,cAAqD,CAAA;IAChG,MAAM,aAAa,GAAG,IAAI,CAAC,aAAa,IAAK,eAA+D,CAAA;IAC5G,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC,IAAY,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAA;IAE/D,MAAM,WAAW,GAAG,IAAI,CAAC,MAAM,EAAE,mBAAmB,SAAS,gBAAgB,CAAC,CAAA;IAE9E,IAAI,KAAK,GAAG,CAAC,CAAA;IACb,IAAI,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;QAC5B,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC,CAAA;YAC5E,IAAI,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,MAAM,IAAI,CAAC;gBAAE,KAAK,GAAG,MAAM,CAAA;QAC5D,CAAC;QAAC,MAAM,CAAC;YACP,wEAAwE;YACxE,yEAAyE;YACzE,iEAAiE;QACnE,CAAC;IACH,CAAC;IAED,MAAM,IAAI,GAAG,KAAK,GAAG,CAAC,CAAA;IACtB,IAAI,CAAC;QACH,aAAa,CAAC,WAAW,EAAE,MAAM,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,CAAA;IAClD,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,2EAA2E;QAC3E,2EAA2E;QAC3E,oEAAoE;QACpE,GAAG,CAAC,4DAA4D,WAAW,QAAQ,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;IACxI,CAAC;IAED,GAAG,CAAC,wCAAwC,IAAI,YAAY,SAAS,iBAAiB,WAAW,EAAE,CAAC,CAAA;AACtG,CAAC"}