@tokenfactory/acc-runner 0.40.23 → 0.40.24

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.
@@ -0,0 +1,14 @@
1
+ /**
2
+ * CX-4 (WARM-SESSIONS): barrel export for the warm-session layer.
3
+ *
4
+ * A pre-spawned, resumable engine session per active conversation so chat turns
5
+ * reuse it (fast first token) instead of paying process spawn every prompt.
6
+ * These are injectable PRIMITIVES; a follow-up wires the pool into the companion
7
+ * (a `claude --session-id` warm factory + `capacityHeld()` folded into the
8
+ * chat-lane claim budget), exactly as the AS-3 resume primitives shipped ahead
9
+ * of their watch/companion wiring. Intentionally `dead-until-wired`.
10
+ */
11
+ export * from "./types.js";
12
+ export * from "./latency.js";
13
+ export * from "./pool.js";
14
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/warm-session/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,WAAW,CAAC"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * CX-4 (WARM-SESSIONS): barrel export for the warm-session layer.
3
+ *
4
+ * A pre-spawned, resumable engine session per active conversation so chat turns
5
+ * reuse it (fast first token) instead of paying process spawn every prompt.
6
+ * These are injectable PRIMITIVES; a follow-up wires the pool into the companion
7
+ * (a `claude --session-id` warm factory + `capacityHeld()` folded into the
8
+ * chat-lane claim budget), exactly as the AS-3 resume primitives shipped ahead
9
+ * of their watch/companion wiring. Intentionally `dead-until-wired`.
10
+ */
11
+ export * from "./types.js";
12
+ export * from "./latency.js";
13
+ export * from "./pool.js";
14
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/warm-session/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,WAAW,CAAC"}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * CX-4 (WARM-SESSIONS): the first-token latency recorder.
3
+ *
4
+ * The default `OpsMetricsSink` the pool reports into. It keeps a bounded ring of
5
+ * recent first-token samples PER MODE (warm/cold) and computes p50/p95 with the
6
+ * nearest-rank method, so Pulse can render "warm p50 <= 1.5s / cold <= 4s" from
7
+ * one snapshot. No-token turns (an empty/failed reply, `firstTokenMs === null`)
8
+ * are counted but EXCLUDED from the percentile so a broken turn never flatters
9
+ * or poisons the latency picture. A follow-up wires an adapter over this that
10
+ * forwards each sample to the durable ops-metrics topic; this recorder is the
11
+ * pure, deterministic core the benchmark asserts against.
12
+ *
13
+ * STAGING: intentionally `dead-until-wired` — the ops-metrics/Pulse adapter over
14
+ * this recorder lands in a follow-up (see the task's Follow-ups).
15
+ */
16
+ import { type FirstTokenMode, type FirstTokenSample, type OpsMetricsSink } from "./types.js";
17
+ /** Percentile view of one mode's first-token samples (ms); null when empty. */
18
+ export interface FirstTokenPercentiles {
19
+ /** Total samples recorded for the mode (includes no-token turns). */
20
+ count: number;
21
+ /** Samples that produced a visible token (the percentile population). */
22
+ tokenCount: number;
23
+ p50: number | null;
24
+ p95: number | null;
25
+ min: number | null;
26
+ max: number | null;
27
+ /** Simple arithmetic mean of the token-bearing samples (ms). */
28
+ mean: number | null;
29
+ }
30
+ /** A full warm-vs-cold snapshot plus the pass/fail against the plan targets. */
31
+ export interface FirstTokenStats {
32
+ warm: FirstTokenPercentiles;
33
+ cold: FirstTokenPercentiles;
34
+ /** True when warm has ≥1 token sample and its p50 ≤ the warm target. */
35
+ warmP50WithinTarget: boolean;
36
+ /** True when cold has ≥1 token sample and its p95 ≤ the cold target. */
37
+ coldP95WithinTarget: boolean;
38
+ targets: {
39
+ warmP50Ms: number;
40
+ coldP95Ms: number;
41
+ };
42
+ }
43
+ /**
44
+ * Nearest-rank percentile over an ASCENDING-sorted array. `p` is 0..100. Returns
45
+ * null for an empty array. index = clamp(ceil(p/100 * n) - 1, 0, n-1).
46
+ */
47
+ export declare function percentile(sortedAsc: readonly number[], p: number): number | null;
48
+ export interface FirstTokenLatencyRecorderOptions {
49
+ /** Max samples retained per mode (ring buffer); older samples are evicted. */
50
+ maxSamplesPerMode?: number;
51
+ /** Warm p50 target (ms) — defaults to the plan's 1.5s. */
52
+ warmP50TargetMs?: number;
53
+ /** Cold p95 target (ms) — defaults to the plan's 4s. */
54
+ coldP95TargetMs?: number;
55
+ }
56
+ /** In-memory, bounded first-token recorder implementing `OpsMetricsSink`. */
57
+ export declare class FirstTokenLatencyRecorder implements OpsMetricsSink {
58
+ private readonly byMode;
59
+ private readonly maxSamples;
60
+ private readonly warmP50TargetMs;
61
+ private readonly coldP95TargetMs;
62
+ constructor(opts?: FirstTokenLatencyRecorderOptions);
63
+ recordFirstToken(sample: FirstTokenSample): void;
64
+ /** All retained samples for a mode (stable copy). */
65
+ samples(mode: FirstTokenMode): readonly FirstTokenSample[];
66
+ private percentilesFor;
67
+ /** Current warm/cold percentiles plus pass/fail against the targets. */
68
+ snapshot(): FirstTokenStats;
69
+ /** Drop all retained samples (test/rig reset). */
70
+ reset(): void;
71
+ }
72
+ //# sourceMappingURL=latency.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"latency.d.ts","sourceRoot":"","sources":["../../src/warm-session/latency.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAGL,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACpB,MAAM,YAAY,CAAC;AAEpB,+EAA+E;AAC/E,MAAM,WAAW,qBAAqB;IACpC,qEAAqE;IACrE,KAAK,EAAE,MAAM,CAAC;IACd,yEAAyE;IACzE,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,gEAAgE;IAChE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB;AAED,gFAAgF;AAChF,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,qBAAqB,CAAC;IAC5B,IAAI,EAAE,qBAAqB,CAAC;IAC5B,wEAAwE;IACxE,mBAAmB,EAAE,OAAO,CAAC;IAC7B,wEAAwE;IACxE,mBAAmB,EAAE,OAAO,CAAC;IAC7B,OAAO,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;CACnD;AAYD;;;GAGG;AACH,wBAAgB,UAAU,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAMjF;AAED,MAAM,WAAW,gCAAgC;IAC/C,8EAA8E;IAC9E,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,0DAA0D;IAC1D,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,wDAAwD;IACxD,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAID,6EAA6E;AAC7E,qBAAa,yBAA0B,YAAW,cAAc;IAC9D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAGrB;IACF,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;gBAE7B,IAAI,GAAE,gCAAqC;IAMvD,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,IAAI;IAOhD,qDAAqD;IACrD,OAAO,CAAC,IAAI,EAAE,cAAc,GAAG,SAAS,gBAAgB,EAAE;IAI1D,OAAO,CAAC,cAAc;IAoBtB,wEAAwE;IACxE,QAAQ,IAAI,eAAe;IAY3B,kDAAkD;IAClD,KAAK,IAAI,IAAI;CAId"}
@@ -0,0 +1,102 @@
1
+ /**
2
+ * CX-4 (WARM-SESSIONS): the first-token latency recorder.
3
+ *
4
+ * The default `OpsMetricsSink` the pool reports into. It keeps a bounded ring of
5
+ * recent first-token samples PER MODE (warm/cold) and computes p50/p95 with the
6
+ * nearest-rank method, so Pulse can render "warm p50 <= 1.5s / cold <= 4s" from
7
+ * one snapshot. No-token turns (an empty/failed reply, `firstTokenMs === null`)
8
+ * are counted but EXCLUDED from the percentile so a broken turn never flatters
9
+ * or poisons the latency picture. A follow-up wires an adapter over this that
10
+ * forwards each sample to the durable ops-metrics topic; this recorder is the
11
+ * pure, deterministic core the benchmark asserts against.
12
+ *
13
+ * STAGING: intentionally `dead-until-wired` — the ops-metrics/Pulse adapter over
14
+ * this recorder lands in a follow-up (see the task's Follow-ups).
15
+ */
16
+ import { COLD_FIRST_TOKEN_TARGET_MS, WARM_FIRST_TOKEN_P50_TARGET_MS, } from "./types.js";
17
+ const EMPTY_PERCENTILES = {
18
+ count: 0,
19
+ tokenCount: 0,
20
+ p50: null,
21
+ p95: null,
22
+ min: null,
23
+ max: null,
24
+ mean: null,
25
+ };
26
+ /**
27
+ * Nearest-rank percentile over an ASCENDING-sorted array. `p` is 0..100. Returns
28
+ * null for an empty array. index = clamp(ceil(p/100 * n) - 1, 0, n-1).
29
+ */
30
+ export function percentile(sortedAsc, p) {
31
+ const n = sortedAsc.length;
32
+ if (n === 0)
33
+ return null;
34
+ const rank = Math.ceil((p / 100) * n);
35
+ const idx = Math.min(n - 1, Math.max(0, rank - 1));
36
+ return sortedAsc[idx] ?? null;
37
+ }
38
+ const DEFAULT_MAX_SAMPLES = 1_000;
39
+ /** In-memory, bounded first-token recorder implementing `OpsMetricsSink`. */
40
+ export class FirstTokenLatencyRecorder {
41
+ byMode = {
42
+ warm: [],
43
+ cold: [],
44
+ };
45
+ maxSamples;
46
+ warmP50TargetMs;
47
+ coldP95TargetMs;
48
+ constructor(opts = {}) {
49
+ this.maxSamples = Math.max(1, Math.floor(opts.maxSamplesPerMode ?? DEFAULT_MAX_SAMPLES));
50
+ this.warmP50TargetMs = opts.warmP50TargetMs ?? WARM_FIRST_TOKEN_P50_TARGET_MS;
51
+ this.coldP95TargetMs = opts.coldP95TargetMs ?? COLD_FIRST_TOKEN_TARGET_MS;
52
+ }
53
+ recordFirstToken(sample) {
54
+ const bucket = this.byMode[sample.mode];
55
+ bucket.push(sample);
56
+ // Ring: drop the oldest once past the cap so retention stays bounded.
57
+ if (bucket.length > this.maxSamples)
58
+ bucket.splice(0, bucket.length - this.maxSamples);
59
+ }
60
+ /** All retained samples for a mode (stable copy). */
61
+ samples(mode) {
62
+ return [...this.byMode[mode]];
63
+ }
64
+ percentilesFor(mode) {
65
+ const all = this.byMode[mode];
66
+ if (all.length === 0)
67
+ return { ...EMPTY_PERCENTILES };
68
+ const tokenMs = all
69
+ .filter((s) => s.sawToken && typeof s.firstTokenMs === "number")
70
+ .map((s) => s.firstTokenMs)
71
+ .sort((a, b) => a - b);
72
+ const tokenCount = tokenMs.length;
73
+ const sum = tokenMs.reduce((acc, v) => acc + v, 0);
74
+ return {
75
+ count: all.length,
76
+ tokenCount,
77
+ p50: percentile(tokenMs, 50),
78
+ p95: percentile(tokenMs, 95),
79
+ min: tokenCount > 0 ? tokenMs[0] : null,
80
+ max: tokenCount > 0 ? tokenMs[tokenCount - 1] : null,
81
+ mean: tokenCount > 0 ? sum / tokenCount : null,
82
+ };
83
+ }
84
+ /** Current warm/cold percentiles plus pass/fail against the targets. */
85
+ snapshot() {
86
+ const warm = this.percentilesFor("warm");
87
+ const cold = this.percentilesFor("cold");
88
+ return {
89
+ warm,
90
+ cold,
91
+ warmP50WithinTarget: warm.p50 !== null && warm.p50 <= this.warmP50TargetMs,
92
+ coldP95WithinTarget: cold.p95 !== null && cold.p95 <= this.coldP95TargetMs,
93
+ targets: { warmP50Ms: this.warmP50TargetMs, coldP95Ms: this.coldP95TargetMs },
94
+ };
95
+ }
96
+ /** Drop all retained samples (test/rig reset). */
97
+ reset() {
98
+ this.byMode.warm.length = 0;
99
+ this.byMode.cold.length = 0;
100
+ }
101
+ }
102
+ //# sourceMappingURL=latency.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"latency.js","sourceRoot":"","sources":["../../src/warm-session/latency.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,153 @@
1
+ /**
2
+ * CX-4 (WARM-SESSIONS): the warm-session pool.
3
+ *
4
+ * Holds at most one pre-spawned, resumable engine session per ACTIVE
5
+ * conversation and routes each turn through the fastest available path:
6
+ *
7
+ * - WARM HIT — a `ready`, alive session exists for the conversation → resume
8
+ * it (fast first token). The session is marked `in_use` for the turn and
9
+ * returned to `ready` on a CLEAN finish so the NEXT turn is warm too.
10
+ * - COLD MISS — no warm session (never warmed, still warming, evicted, or
11
+ * expired) → fall through to the injected `coldStream`, which is the
12
+ * UNCHANGED per-turn spawn (an engine's `streamTurn`). A background
13
+ * `prewarm` is kicked so the conversation's NEXT turn is warm. The cold path
14
+ * is byte-for-byte the pre-warm behaviour, so cold latency is a strict
15
+ * regression baseline.
16
+ *
17
+ * Every turn emits ONE first-token latency sample (tagged warm/cold) to the
18
+ * injected `OpsMetricsSink`. Idle sessions are swept after a TTL with clean
19
+ * teardown, and — critically — an IDLE warm session holds ZERO capacity:
20
+ * `capacityHeld()` counts only `in_use` sessions, so a warm-but-idle session is
21
+ * never mistaken for an in-flight turn by the chat-lane claim budget.
22
+ *
23
+ * Fully injectable (factory / cold path / clock / metrics / logger), so the
24
+ * whole lifecycle — warm/cold routing, latency tagging, idle sweep, teardown,
25
+ * capacity accounting — is exercised with a fake clock and scripted engine, no
26
+ * `claude` binary and no network.
27
+ *
28
+ * STAGING: intentionally `dead-until-wired` — the companion folds `capacityHeld()`
29
+ * into its chat-lane claim budget and routes turns through `runTurn` in a
30
+ * follow-up (see the task's Follow-ups).
31
+ */
32
+ import type { EngineStreamChunk, EngineStreamTurnOptions } from "../engines/types.js";
33
+ import { type FirstTokenMode, type OpsMetricsSink, type WarmSessionFactory, type WarmSpawnArgs, type WarmTeardownReason } from "./types.js";
34
+ /** The unchanged cold path: an engine's per-turn `streamTurn`. */
35
+ export type ColdStreamFn = (opts: EngineStreamTurnOptions) => AsyncIterable<EngineStreamChunk>;
36
+ export interface WarmSessionPoolOptions {
37
+ /** Pre-spawns resumable sessions (the ONE engine seam). */
38
+ factory: WarmSessionFactory;
39
+ /** The unchanged per-turn spawn used when no warm session is ready. */
40
+ coldStream: ColdStreamFn;
41
+ /** Where first-token latency samples are reported. */
42
+ metrics: OpsMetricsSink;
43
+ /** Idle-expiry TTL (ms); a `ready` session idle longer than this is torn down. */
44
+ idleTtlMs?: number;
45
+ /** Max concurrent warm sessions; a cold miss past this does not prewarm. */
46
+ maxSessions?: number;
47
+ /** Idle-sweep cadence for `start()` (ms). */
48
+ sweepIntervalMs?: number;
49
+ /** Clock (test seam). */
50
+ now?: () => number;
51
+ /** Best-effort log sink. */
52
+ onLog?: (line: string) => void;
53
+ }
54
+ /** The result of routing one turn: the chosen mode + the measured stream. */
55
+ export interface WarmTurnHandle {
56
+ /** Whether the turn is being served warm (reused session) or cold (fresh spawn). */
57
+ mode: FirstTokenMode;
58
+ /** The warm session id when `mode === "warm"`, else null. */
59
+ sessionId: string | null;
60
+ /**
61
+ * The turn's delta stream, instrumented for first-token latency. Iterating to
62
+ * completion (or breaking / throwing early) records exactly ONE latency sample
63
+ * and releases the warm session. The consumer drives it exactly as it would a
64
+ * bare `streamTurn`.
65
+ */
66
+ stream: AsyncGenerator<EngineStreamChunk, void, void>;
67
+ }
68
+ /** Observable pool sizes (for logging / the doctor surface). */
69
+ export interface WarmPoolStats {
70
+ size: number;
71
+ ready: number;
72
+ warming: number;
73
+ inUse: number;
74
+ /** Capacity held right now — equals `inUse` (idle warm sessions hold none). */
75
+ capacityHeld: number;
76
+ }
77
+ export declare class WarmSessionPool {
78
+ private readonly opts;
79
+ private readonly entries;
80
+ private readonly idleTtlMs;
81
+ private readonly maxSessions;
82
+ private readonly sweepIntervalMs;
83
+ private readonly now;
84
+ private timer;
85
+ private stopped;
86
+ constructor(opts: WarmSessionPoolOptions);
87
+ /** Number of conversations with a warm-session slot (any phase). */
88
+ size(): number;
89
+ /** Conversations with a `ready`, reusable warm session. */
90
+ readyConversations(): string[];
91
+ /**
92
+ * Capacity held by the pool RIGHT NOW: the count of sessions with a turn
93
+ * actively streaming (`in_use`). An idle/warming warm session holds ZERO —
94
+ * this is the "idle != tokens" invariant the chat-lane claim budget relies on,
95
+ * so a wired companion adds THIS (not the warm-session count) to its in-flight
96
+ * accounting. A pre-warmed but idle conversation must never shrink the budget.
97
+ */
98
+ capacityHeld(): number;
99
+ stats(): WarmPoolStats;
100
+ /**
101
+ * Pre-spawn a resumable session for a conversation ahead of its next turn.
102
+ * Idempotent + deduped: a no-op when a `ready`/`warming`/`in_use` slot already
103
+ * exists, and concurrent calls share ONE warm-up. Returns true when a session
104
+ * is (or is already) warming/ready, false when the pool declined (stopped or
105
+ * at the `maxSessions` cap). Never throws — a failed warm logs and resolves
106
+ * false so the cold path still serves the turn.
107
+ */
108
+ prewarm(conversationId: string, args?: Omit<WarmSpawnArgs, "conversationId">): Promise<boolean>;
109
+ /** Run the factory warm-up, transitioning the entry to `ready` or evicting it. */
110
+ private doWarm;
111
+ /**
112
+ * Route one turn. Returns the chosen `mode`, the warm `sessionId` (or null),
113
+ * and an instrumented `stream` the caller drives exactly like a bare
114
+ * `streamTurn`. Consuming the stream to its end — or breaking/throwing early —
115
+ * records ONE first-token latency sample and releases the warm session.
116
+ */
117
+ runTurn(opts: EngineStreamTurnOptions): WarmTurnHandle;
118
+ /**
119
+ * Wrap a delta stream to (a) time the first VISIBLE token, (b) emit one latency
120
+ * sample on completion, and (c) release the warm session — back to `ready` on a
121
+ * clean finish, or torn down + evicted on abort/error (a resumed session left
122
+ * mid-turn is unsafe to reuse). A cold turn has no session to release.
123
+ */
124
+ private instrument;
125
+ /**
126
+ * Release a warm session after its turn. Clean finish → back to `ready` (the
127
+ * next turn reuses it, `lastUsedAt` reset so the idle clock restarts). Fault /
128
+ * abort → teardown + evict so a corrupt resumed session is never reused. If the
129
+ * pool stopped mid-turn, the session is torn down regardless.
130
+ */
131
+ private release;
132
+ /**
133
+ * Tear down every `ready` session idle longer than the TTL. `in_use` sessions
134
+ * (a turn is streaming) and `warming` slots are never expired. Returns the
135
+ * count torn down. Safe to call ad hoc or from the sweep interval.
136
+ */
137
+ sweepIdle(nowMs?: number): Promise<number>;
138
+ /**
139
+ * Explicitly evict one conversation's session (e.g. the conversation went
140
+ * inactive, or a dead handle was detected). No-op when absent or in_use.
141
+ */
142
+ evict(conversationId: string, reason?: WarmTeardownReason): Promise<boolean>;
143
+ /** Start the periodic idle sweep. Idempotent. */
144
+ start(): void;
145
+ /**
146
+ * Stop the pool: cancel the sweep and tear down EVERY session (including any
147
+ * in-flight one) so a shutdown leaves no orphaned process. Returns after all
148
+ * teardowns settle. Idempotent.
149
+ */
150
+ stop(): Promise<void>;
151
+ private log;
152
+ }
153
+ //# sourceMappingURL=pool.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pool.d.ts","sourceRoot":"","sources":["../../src/warm-session/pool.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,335 @@
1
+ import { DEFAULT_MAX_WARM_SESSIONS, DEFAULT_WARM_IDLE_TTL_MS, DEFAULT_WARM_SWEEP_INTERVAL_MS, } from "./types.js";
2
+ export class WarmSessionPool {
3
+ opts;
4
+ entries = new Map();
5
+ idleTtlMs;
6
+ maxSessions;
7
+ sweepIntervalMs;
8
+ now;
9
+ timer = null;
10
+ stopped = false;
11
+ constructor(opts) {
12
+ this.opts = opts;
13
+ this.idleTtlMs = Math.max(1_000, Math.floor(opts.idleTtlMs ?? DEFAULT_WARM_IDLE_TTL_MS));
14
+ this.maxSessions = Math.max(1, Math.floor(opts.maxSessions ?? DEFAULT_MAX_WARM_SESSIONS));
15
+ this.sweepIntervalMs = Math.max(1_000, Math.floor(opts.sweepIntervalMs ?? DEFAULT_WARM_SWEEP_INTERVAL_MS));
16
+ this.now = opts.now ?? Date.now;
17
+ }
18
+ /* ----------------------------- observability ---------------------------- */
19
+ /** Number of conversations with a warm-session slot (any phase). */
20
+ size() {
21
+ return this.entries.size;
22
+ }
23
+ /** Conversations with a `ready`, reusable warm session. */
24
+ readyConversations() {
25
+ return [...this.entries.values()].filter((e) => e.phase === "ready").map((e) => e.conversationId);
26
+ }
27
+ /**
28
+ * Capacity held by the pool RIGHT NOW: the count of sessions with a turn
29
+ * actively streaming (`in_use`). An idle/warming warm session holds ZERO —
30
+ * this is the "idle != tokens" invariant the chat-lane claim budget relies on,
31
+ * so a wired companion adds THIS (not the warm-session count) to its in-flight
32
+ * accounting. A pre-warmed but idle conversation must never shrink the budget.
33
+ */
34
+ capacityHeld() {
35
+ let n = 0;
36
+ for (const e of this.entries.values())
37
+ if (e.phase === "in_use")
38
+ n += 1;
39
+ return n;
40
+ }
41
+ stats() {
42
+ let ready = 0;
43
+ let warming = 0;
44
+ let inUse = 0;
45
+ for (const e of this.entries.values()) {
46
+ if (e.phase === "ready")
47
+ ready += 1;
48
+ else if (e.phase === "warming")
49
+ warming += 1;
50
+ else if (e.phase === "in_use")
51
+ inUse += 1;
52
+ }
53
+ return { size: this.entries.size, ready, warming, inUse, capacityHeld: inUse };
54
+ }
55
+ /* ------------------------------- warming -------------------------------- */
56
+ /**
57
+ * Pre-spawn a resumable session for a conversation ahead of its next turn.
58
+ * Idempotent + deduped: a no-op when a `ready`/`warming`/`in_use` slot already
59
+ * exists, and concurrent calls share ONE warm-up. Returns true when a session
60
+ * is (or is already) warming/ready, false when the pool declined (stopped or
61
+ * at the `maxSessions` cap). Never throws — a failed warm logs and resolves
62
+ * false so the cold path still serves the turn.
63
+ */
64
+ async prewarm(conversationId, args = {}) {
65
+ if (this.stopped)
66
+ return false;
67
+ const existing = this.entries.get(conversationId);
68
+ if (existing) {
69
+ // Already warming or holding a session — dedupe onto it.
70
+ if (existing.phase === "warming" && existing.warming) {
71
+ return (await existing.warming) !== null;
72
+ }
73
+ return existing.phase === "ready" || existing.phase === "in_use";
74
+ }
75
+ if (this.entries.size >= this.maxSessions) {
76
+ this.log(`prewarm(${conversationId}) declined: at maxSessions=${this.maxSessions}`);
77
+ return false;
78
+ }
79
+ const entry = {
80
+ conversationId,
81
+ phase: "warming",
82
+ handle: null,
83
+ warming: null,
84
+ warmedAt: 0,
85
+ lastUsedAt: this.now(),
86
+ };
87
+ this.entries.set(conversationId, entry);
88
+ const warming = this.doWarm(entry, args);
89
+ entry.warming = warming;
90
+ const handle = await warming;
91
+ return handle !== null;
92
+ }
93
+ /** Run the factory warm-up, transitioning the entry to `ready` or evicting it. */
94
+ async doWarm(entry, args) {
95
+ try {
96
+ const handle = await this.opts.factory.warm({ conversationId: entry.conversationId, ...args });
97
+ // The pool may have been stopped, or the slot evicted/superseded, while
98
+ // the warm-up was in flight — if so, tear the fresh handle down at once so
99
+ // it does not leak (its slot is gone).
100
+ if (this.stopped || this.entries.get(entry.conversationId) !== entry) {
101
+ await safeTeardown(handle, "replaced", this.opts.onLog);
102
+ return null;
103
+ }
104
+ entry.handle = handle;
105
+ entry.phase = "ready";
106
+ entry.warmedAt = this.now();
107
+ entry.lastUsedAt = this.now();
108
+ entry.warming = null;
109
+ this.log(`warmed ${entry.conversationId} (session ${handle.sessionId})`);
110
+ return handle;
111
+ }
112
+ catch (err) {
113
+ // Warm-up failed — drop the slot so the cold path serves and a later turn
114
+ // may retry warming.
115
+ if (this.entries.get(entry.conversationId) === entry)
116
+ this.entries.delete(entry.conversationId);
117
+ this.log(`warm(${entry.conversationId}) failed: ${errText(err)}`);
118
+ return null;
119
+ }
120
+ }
121
+ /* ------------------------------- driving -------------------------------- */
122
+ /**
123
+ * Route one turn. Returns the chosen `mode`, the warm `sessionId` (or null),
124
+ * and an instrumented `stream` the caller drives exactly like a bare
125
+ * `streamTurn`. Consuming the stream to its end — or breaking/throwing early —
126
+ * records ONE first-token latency sample and releases the warm session.
127
+ */
128
+ runTurn(opts) {
129
+ const conversationId = opts.conversationId;
130
+ const entry = this.entries.get(conversationId);
131
+ const warmUsable = !this.stopped &&
132
+ entry !== undefined &&
133
+ entry.phase === "ready" &&
134
+ entry.handle !== null &&
135
+ isHandleAlive(entry.handle);
136
+ if (warmUsable && entry && entry.handle) {
137
+ const handle = entry.handle;
138
+ entry.phase = "in_use";
139
+ const inner = handle.resumeStream(opts);
140
+ return {
141
+ mode: "warm",
142
+ sessionId: handle.sessionId,
143
+ stream: this.instrument("warm", opts, inner, entry, handle),
144
+ };
145
+ }
146
+ // Cold miss. If a stale/dead handle is sitting `ready`, evict it first so it
147
+ // does not leak and the conversation re-warms clean.
148
+ if (entry && entry.phase === "ready" && entry.handle && !isHandleAlive(entry.handle)) {
149
+ void this.evict(conversationId, "evicted_dead");
150
+ }
151
+ // Kick a background prewarm so the NEXT turn on this conversation is warm.
152
+ // Fire-and-forget and self-guarded (prewarm never rejects).
153
+ if (!this.stopped && !this.entries.has(conversationId))
154
+ void this.prewarm(conversationId, spawnArgsFrom(opts));
155
+ const inner = this.opts.coldStream(opts);
156
+ return {
157
+ mode: "cold",
158
+ sessionId: null,
159
+ stream: this.instrument("cold", opts, inner, null, null),
160
+ };
161
+ }
162
+ /**
163
+ * Wrap a delta stream to (a) time the first VISIBLE token, (b) emit one latency
164
+ * sample on completion, and (c) release the warm session — back to `ready` on a
165
+ * clean finish, or torn down + evicted on abort/error (a resumed session left
166
+ * mid-turn is unsafe to reuse). A cold turn has no session to release.
167
+ */
168
+ async *instrument(mode, opts, inner, entry, handle) {
169
+ const startedAt = this.now();
170
+ let firstTokenMs = null;
171
+ let sawToken = false;
172
+ let faulted = false;
173
+ try {
174
+ for await (const chunk of inner) {
175
+ if (!sawToken && chunk.delta && chunk.delta.length > 0) {
176
+ sawToken = true;
177
+ firstTokenMs = this.now() - startedAt;
178
+ }
179
+ yield chunk;
180
+ }
181
+ }
182
+ catch (err) {
183
+ faulted = true;
184
+ throw err;
185
+ }
186
+ finally {
187
+ // An abort counts as a fault for reuse purposes even without a throw.
188
+ if (opts.signal?.aborted)
189
+ faulted = true;
190
+ const totalMs = this.now() - startedAt;
191
+ this.opts.metrics.recordFirstToken({
192
+ conversationId: opts.conversationId,
193
+ turnId: opts.turnId,
194
+ mode,
195
+ firstTokenMs: sawToken ? firstTokenMs : null,
196
+ totalMs,
197
+ sawToken,
198
+ at: this.now(),
199
+ });
200
+ if (entry && handle)
201
+ await this.release(entry, handle, faulted);
202
+ }
203
+ }
204
+ /**
205
+ * Release a warm session after its turn. Clean finish → back to `ready` (the
206
+ * next turn reuses it, `lastUsedAt` reset so the idle clock restarts). Fault /
207
+ * abort → teardown + evict so a corrupt resumed session is never reused. If the
208
+ * pool stopped mid-turn, the session is torn down regardless.
209
+ */
210
+ async release(entry, handle, faulted) {
211
+ // The slot may have been superseded/evicted while the turn ran.
212
+ const current = this.entries.get(entry.conversationId);
213
+ if (current !== entry) {
214
+ await safeTeardown(handle, "replaced", this.opts.onLog);
215
+ return;
216
+ }
217
+ if (faulted || this.stopped || !isHandleAlive(handle)) {
218
+ const reason = this.stopped
219
+ ? "pool_shutdown"
220
+ : faulted
221
+ ? "evicted_after_turn_fault"
222
+ : "evicted_dead";
223
+ this.entries.delete(entry.conversationId);
224
+ entry.phase = "torn_down";
225
+ await safeTeardown(handle, reason, this.opts.onLog);
226
+ return;
227
+ }
228
+ entry.phase = "ready";
229
+ entry.lastUsedAt = this.now();
230
+ }
231
+ /* ------------------------------ expiry/stop ----------------------------- */
232
+ /**
233
+ * Tear down every `ready` session idle longer than the TTL. `in_use` sessions
234
+ * (a turn is streaming) and `warming` slots are never expired. Returns the
235
+ * count torn down. Safe to call ad hoc or from the sweep interval.
236
+ */
237
+ async sweepIdle(nowMs = this.now()) {
238
+ const expired = [];
239
+ for (const entry of this.entries.values()) {
240
+ if (entry.phase !== "ready" || entry.handle === null)
241
+ continue;
242
+ if (nowMs - entry.lastUsedAt >= this.idleTtlMs) {
243
+ expired.push({ conversationId: entry.conversationId, handle: entry.handle });
244
+ entry.phase = "torn_down";
245
+ this.entries.delete(entry.conversationId);
246
+ }
247
+ }
248
+ for (const { conversationId, handle } of expired) {
249
+ this.log(`idle-expiry teardown ${conversationId} (session ${handle.sessionId})`);
250
+ await safeTeardown(handle, "idle_expiry", this.opts.onLog);
251
+ }
252
+ return expired.length;
253
+ }
254
+ /**
255
+ * Explicitly evict one conversation's session (e.g. the conversation went
256
+ * inactive, or a dead handle was detected). No-op when absent or in_use.
257
+ */
258
+ async evict(conversationId, reason = "capacity_reclaim") {
259
+ const entry = this.entries.get(conversationId);
260
+ if (!entry || entry.phase === "in_use" || entry.handle === null)
261
+ return false;
262
+ this.entries.delete(conversationId);
263
+ entry.phase = "torn_down";
264
+ await safeTeardown(entry.handle, reason, this.opts.onLog);
265
+ return true;
266
+ }
267
+ /** Start the periodic idle sweep. Idempotent. */
268
+ start() {
269
+ if (this.timer || this.stopped)
270
+ return;
271
+ this.timer = setInterval(() => {
272
+ void this.sweepIdle();
273
+ }, this.sweepIntervalMs);
274
+ this.timer.unref?.();
275
+ }
276
+ /**
277
+ * Stop the pool: cancel the sweep and tear down EVERY session (including any
278
+ * in-flight one) so a shutdown leaves no orphaned process. Returns after all
279
+ * teardowns settle. Idempotent.
280
+ */
281
+ async stop() {
282
+ this.stopped = true;
283
+ if (this.timer) {
284
+ clearInterval(this.timer);
285
+ this.timer = null;
286
+ }
287
+ const handles = [];
288
+ for (const entry of this.entries.values()) {
289
+ if (entry.handle)
290
+ handles.push({ conversationId: entry.conversationId, handle: entry.handle });
291
+ entry.phase = "torn_down";
292
+ }
293
+ this.entries.clear();
294
+ await Promise.all(handles.map(({ handle }) => safeTeardown(handle, "pool_shutdown", this.opts.onLog)));
295
+ }
296
+ log(line) {
297
+ this.opts.onLog?.(`[acc-runner] warm-session: ${line}`);
298
+ }
299
+ }
300
+ /* ------------------------------- helpers ---------------------------------- */
301
+ /** A handle with no `isAlive` is assumed alive; a throwing probe is treated dead. */
302
+ function isHandleAlive(handle) {
303
+ if (!handle.isAlive)
304
+ return true;
305
+ try {
306
+ return handle.isAlive();
307
+ }
308
+ catch {
309
+ return false;
310
+ }
311
+ }
312
+ /** Teardown that never rejects (a throwing teardown must not crash the pool). */
313
+ async function safeTeardown(handle, reason, onLog) {
314
+ try {
315
+ await handle.teardown(reason);
316
+ }
317
+ catch (err) {
318
+ onLog?.(`[acc-runner] warm-session: teardown(${reason}) failed: ${errText(err)}`);
319
+ }
320
+ }
321
+ /** Carry the turn's cwd/model into the background prewarm for the next turn. */
322
+ function spawnArgsFrom(opts) {
323
+ const out = {};
324
+ if (opts.cwd !== undefined)
325
+ out.cwd = opts.cwd;
326
+ if (opts.modelId !== undefined)
327
+ out.modelId = opts.modelId;
328
+ return out;
329
+ }
330
+ /** Best-effort, log-safe text of a thrown value. */
331
+ function errText(err) {
332
+ const e = err;
333
+ return (e?.message ?? String(err ?? "")).slice(0, 300);
334
+ }
335
+ //# sourceMappingURL=pool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pool.js","sourceRoot":"","sources":["../../src/warm-session/pool.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,124 @@
1
+ /**
2
+ * CX-4 (WARM-SESSIONS): the warm-session type surface.
3
+ *
4
+ * Per CHAT_UX_ENHANCEMENTS_PLAN.md §1: a chat turn's first-token latency is
5
+ * dominated by engine PROCESS SPAWN (node boot + acc-mcp handshake + auth
6
+ * resolution), not by the transport. The warm-session layer keeps a pre-spawned,
7
+ * resumable engine session per ACTIVE conversation on the serving runner so a
8
+ * turn REUSES it (fast first token) instead of paying spawn on every prompt.
9
+ *
10
+ * This module is deliberately self-contained and fully injectable: it names two
11
+ * seams — a `WarmSessionFactory` that pre-spawns/holds a resumable session, and
12
+ * a `coldStream` fallback that is the UNCHANGED per-turn spawn (an engine's
13
+ * `streamTurn`) — plus an `OpsMetricsSink` for first-token latency. The pool
14
+ * neither imports the companion nor a `claude` binary, so it is exercised
15
+ * deterministically (tests/unit/warm-session/*) and wired into the companion by
16
+ * a follow-up, mirroring how the AS-3 resume primitives shipped ahead of their
17
+ * watch/companion wiring.
18
+ *
19
+ * The `EngineStreamChunk` / `EngineStreamTurnOptions` shapes are reused verbatim
20
+ * from engines/types.ts so a warm turn is byte-identical to a cold one on the
21
+ * wire — a warm session is a latency optimisation, never a protocol change.
22
+ *
23
+ * STAGING: these exports are intentionally `dead-until-wired` — the companion +
24
+ * real-engine resume adapter that consume them land in a follow-up (see the
25
+ * task's Follow-ups), mirroring how the AS-3 resume primitives shipped first.
26
+ */
27
+ import type { EngineStreamChunk, EngineStreamTurnOptions } from "../engines/types.js";
28
+ /** Whether a turn's first token came from a reused warm session or a cold spawn. */
29
+ export type FirstTokenMode = "warm" | "cold";
30
+ /**
31
+ * Lifecycle phase of a warm session entry in the pool.
32
+ * warming — factory.warm() is in flight (the session is being pre-spawned).
33
+ * ready — pre-spawned and idle, holding NO capacity, awaiting a turn.
34
+ * in_use — a turn is actively streaming on it (this is the ONLY phase that
35
+ * counts toward capacity — idle warm sessions never do).
36
+ * torn_down — teardown() has settled; the entry is being/has been evicted.
37
+ */
38
+ export type WarmSessionPhase = "warming" | "ready" | "in_use" | "torn_down";
39
+ /**
40
+ * Why a warm session was torn down. Carried into `WarmSessionHandle.teardown` so
41
+ * a wired factory can log/observe the disposal cause without the pool leaking a
42
+ * string convention.
43
+ */
44
+ export type WarmTeardownReason = "idle_expiry" | "pool_shutdown" | "evicted_after_turn_fault" | "evicted_dead" | "replaced" | "capacity_reclaim";
45
+ /**
46
+ * A pre-spawned, resumable engine session bound to ONE conversation. Produced by
47
+ * `WarmSessionFactory.warm`. `resumeStream` feeds a turn's prompt into the
48
+ * already-warm session and yields the same `EngineStreamChunk` deltas a cold
49
+ * `streamTurn` would — the first delta arrives fast because node/acc-mcp/auth
50
+ * were paid at warm time. `teardown` disposes the underlying process/handle
51
+ * cleanly (SIGTERM + reap) so an expired or faulted session leaves NO leak.
52
+ */
53
+ export interface WarmSessionHandle {
54
+ /** The conversation this session is pinned to. */
55
+ readonly conversationId: string;
56
+ /** Opaque resumable-session id (engine's `--session-id`/resume token). */
57
+ readonly sessionId: string;
58
+ /** Run one turn by resuming this warm session; yields incremental deltas. */
59
+ resumeStream(opts: EngineStreamTurnOptions): AsyncIterable<EngineStreamChunk>;
60
+ /**
61
+ * Cheap liveness check for an IDLE session (default: assume alive). A wired
62
+ * factory backing the session with a long-lived child reports `false` once the
63
+ * child has exited so the pool evicts the dead handle instead of resuming into
64
+ * an error. Never throws.
65
+ */
66
+ isAlive?(): boolean;
67
+ /** Dispose the session cleanly (idempotent). Never rejects. */
68
+ teardown(reason: WarmTeardownReason): Promise<void>;
69
+ }
70
+ /** Arguments to pre-spawn a warm session for a conversation. */
71
+ export interface WarmSpawnArgs {
72
+ conversationId: string;
73
+ /** Repo checkout the eventual turns run in (forwarded to the engine). */
74
+ cwd?: string;
75
+ /** Model to pin for the session; omitted/blank uses the operator default. */
76
+ modelId?: string | null;
77
+ /** Abort signal for the warm-up itself (pool shutdown mid-warm). */
78
+ signal?: AbortSignal;
79
+ }
80
+ /**
81
+ * Pre-spawns and hands back a resumable engine session. The ONE seam a follow-up
82
+ * wires to a real chat engine (a `claude --session-id` pre-spawn, or a long-lived
83
+ * stdin-streaming child). Kept as an interface so the pool has zero engine
84
+ * knowledge and the conformance tests inject a scripted fake.
85
+ */
86
+ export interface WarmSessionFactory {
87
+ warm(args: WarmSpawnArgs): Promise<WarmSessionHandle>;
88
+ }
89
+ /**
90
+ * One first-token latency observation for a single turn. `firstTokenMs` is the
91
+ * ms from turn start to the first VISIBLE delta; it is `null` when the turn
92
+ * produced no visible token (an empty/failed reply) so a no-token turn never
93
+ * pollutes the p50/p95. `totalMs` is turn start → stream end regardless.
94
+ */
95
+ export interface FirstTokenSample {
96
+ conversationId: string;
97
+ turnId: string;
98
+ mode: FirstTokenMode;
99
+ firstTokenMs: number | null;
100
+ totalMs: number;
101
+ sawToken: boolean;
102
+ /** Wall-clock stamp (via the injected clock) for ordering/retention. */
103
+ at: number;
104
+ }
105
+ /**
106
+ * Where the pool reports per-turn first-token latency. A follow-up wires this to
107
+ * the ops-metrics topic / activity events so Pulse can render warm/cold p50/p95;
108
+ * `FirstTokenLatencyRecorder` (latency.ts) is the default in-memory sink that
109
+ * also computes the percentiles.
110
+ */
111
+ export interface OpsMetricsSink {
112
+ recordFirstToken(sample: FirstTokenSample): void;
113
+ }
114
+ /** Warm p50 first-token target on the rig (ms). */
115
+ export declare const WARM_FIRST_TOKEN_P50_TARGET_MS = 1500;
116
+ /** Cold first-token target on the rig (ms) — the unchanged spawn path ceiling. */
117
+ export declare const COLD_FIRST_TOKEN_TARGET_MS = 4000;
118
+ /** Default idle-expiry TTL for a warm session (~10 min per the plan). */
119
+ export declare const DEFAULT_WARM_IDLE_TTL_MS = 600000;
120
+ /** Default max warm sessions the pool holds concurrently. */
121
+ export declare const DEFAULT_MAX_WARM_SESSIONS = 32;
122
+ /** Default idle-sweep cadence. */
123
+ export declare const DEFAULT_WARM_SWEEP_INTERVAL_MS = 60000;
124
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/warm-session/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AAEtF,oFAAoF;AACpF,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,MAAM,CAAC;AAE7C;;;;;;;GAOG;AACH,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,OAAO,GAAG,QAAQ,GAAG,WAAW,CAAC;AAE5E;;;;GAIG;AACH,MAAM,MAAM,kBAAkB,GAC1B,aAAa,GACb,eAAe,GACf,0BAA0B,GAC1B,cAAc,GACd,UAAU,GACV,kBAAkB,CAAC;AAEvB;;;;;;;GAOG;AACH,MAAM,WAAW,iBAAiB;IAChC,kDAAkD;IAClD,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,0EAA0E;IAC1E,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,6EAA6E;IAC7E,YAAY,CAAC,IAAI,EAAE,uBAAuB,GAAG,aAAa,CAAC,iBAAiB,CAAC,CAAC;IAC9E;;;;;OAKG;IACH,OAAO,CAAC,IAAI,OAAO,CAAC;IACpB,+DAA+D;IAC/D,QAAQ,CAAC,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACrD;AAED,gEAAgE;AAChE,MAAM,WAAW,aAAa;IAC5B,cAAc,EAAE,MAAM,CAAC;IACvB,yEAAyE;IACzE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,6EAA6E;IAC7E,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,oEAAoE;IACpE,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED;;;;;GAKG;AACH,MAAM,WAAW,kBAAkB;IACjC,IAAI,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;CACvD;AAED;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,cAAc,CAAC;IACrB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,OAAO,CAAC;IAClB,wEAAwE;IACxE,EAAE,EAAE,MAAM,CAAC;CACZ;AAED;;;;;GAKG;AACH,MAAM,WAAW,cAAc;IAC7B,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,IAAI,CAAC;CAClD;AAED,mDAAmD;AACnD,eAAO,MAAM,8BAA8B,OAAQ,CAAC;AACpD,kFAAkF;AAClF,eAAO,MAAM,0BAA0B,OAAQ,CAAC;AAChD,yEAAyE;AACzE,eAAO,MAAM,wBAAwB,SAAU,CAAC;AAChD,6DAA6D;AAC7D,eAAO,MAAM,yBAAyB,KAAK,CAAC;AAC5C,kCAAkC;AAClC,eAAO,MAAM,8BAA8B,QAAS,CAAC"}
@@ -0,0 +1,11 @@
1
+ /** Warm p50 first-token target on the rig (ms). */
2
+ export const WARM_FIRST_TOKEN_P50_TARGET_MS = 1_500;
3
+ /** Cold first-token target on the rig (ms) — the unchanged spawn path ceiling. */
4
+ export const COLD_FIRST_TOKEN_TARGET_MS = 4_000;
5
+ /** Default idle-expiry TTL for a warm session (~10 min per the plan). */
6
+ export const DEFAULT_WARM_IDLE_TTL_MS = 600_000;
7
+ /** Default max warm sessions the pool holds concurrently. */
8
+ export const DEFAULT_MAX_WARM_SESSIONS = 32;
9
+ /** Default idle-sweep cadence. */
10
+ export const DEFAULT_WARM_SWEEP_INTERVAL_MS = 60_000;
11
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/warm-session/types.ts"],"names":[],"mappings":"AAgIA,mDAAmD;AACnD,MAAM,CAAC,MAAM,8BAA8B,GAAG,KAAK,CAAC;AACpD,kFAAkF;AAClF,MAAM,CAAC,MAAM,0BAA0B,GAAG,KAAK,CAAC;AAChD,yEAAyE;AACzE,MAAM,CAAC,MAAM,wBAAwB,GAAG,OAAO,CAAC;AAChD,6DAA6D;AAC7D,MAAM,CAAC,MAAM,yBAAyB,GAAG,EAAE,CAAC;AAC5C,kCAAkC;AAClC,MAAM,CAAC,MAAM,8BAA8B,GAAG,MAAM,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenfactory/acc-runner",
3
- "version": "0.40.23",
3
+ "version": "0.40.24",
4
4
  "description": "Agent Control Center local runner. Spawns Claude Code sessions assigned via ACC.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,