@rubytech/create-maxy-code 0.1.460 → 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.
- package/dist/__tests__/cron-heartbeat-registration.test.js +57 -28
- package/dist/__tests__/launchd-plist.test.js +26 -0
- package/dist/cron-registration.js +30 -8
- package/dist/index.js +50 -9
- package/dist/launchd-plist.js +1 -1
- package/dist/uninstall.js +9 -3
- package/package.json +1 -1
- package/payload/platform/plugins/cloudflare/bin/portal-enrol.mjs +96 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/auth-route.test.ts +54 -40
- package/payload/platform/plugins/cloudflare/mcp/__tests__/passcode.test.ts +169 -39
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-enrol.test.ts +207 -0
- package/payload/platform/plugins/cloudflare/mcp/package.json +1 -1
- package/payload/platform/plugins/cloudflare/skills/data-portal/SKILL.md +21 -19
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/passcode.mjs +193 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/auth.ts +18 -12
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/schema.sql +14 -5
- package/payload/platform/plugins/voice-mirror/PLUGIN.md +1 -1
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-distil-profile.d.ts.map +1 -1
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-distil-profile.js +98 -9
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-distil-profile.js.map +1 -1
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-retrieve-conditioning.d.ts.map +1 -1
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-retrieve-conditioning.js +54 -21
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-retrieve-conditioning.js.map +1 -1
- package/payload/platform/plugins/voice-mirror/mcp/scripts/smoke.mjs +110 -0
- package/payload/platform/plugins/voice-mirror/skills/voice-mirror/SKILL.md +6 -3
- package/payload/platform/scripts/__tests__/logs-rotate.test.sh +256 -0
- package/payload/platform/scripts/__tests__/resume-tunnel.test.sh +79 -0
- package/payload/platform/scripts/logs-rotate.sh +204 -0
- package/payload/platform/scripts/resume-tunnel.sh +27 -0
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.js +41 -5
- package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/index.d.ts +1 -0
- package/payload/platform/services/claude-session-manager/dist/index.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/index.js +22 -0
- package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/install-start-counter.d.ts +2 -0
- package/payload/platform/services/claude-session-manager/dist/install-start-counter.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/install-start-counter.js +12 -0
- package/payload/platform/services/claude-session-manager/dist/install-start-counter.js.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/pty-census.d.ts +122 -9
- package/payload/platform/services/claude-session-manager/dist/pty-census.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-census.js +214 -29
- package/payload/platform/services/claude-session-manager/dist/pty-census.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +2 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/rc-daemon.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/rc-daemon.js +13 -1
- package/payload/platform/services/claude-session-manager/dist/rc-daemon.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/session-memory-cap.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/session-memory-cap.js +4 -2
- package/payload/platform/services/claude-session-manager/dist/session-memory-cap.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/slice-memory-policy.d.ts +150 -0
- package/payload/platform/services/claude-session-manager/dist/slice-memory-policy.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/slice-memory-policy.js +333 -0
- package/payload/platform/services/claude-session-manager/dist/slice-memory-policy.js.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/start-counter.d.ts +10 -0
- package/payload/platform/services/claude-session-manager/dist/start-counter.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/start-counter.js +55 -0
- package/payload/platform/services/claude-session-manager/dist/start-counter.js.map +1 -0
- package/payload/server/server.js +55 -35
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/passcode.ts +0 -73
|
@@ -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"}
|