@tokenfactory/acc-runner 0.40.11 → 0.40.13

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 (48) hide show
  1. package/dist/capacity/account-window.d.ts +64 -0
  2. package/dist/capacity/account-window.d.ts.map +1 -0
  3. package/dist/capacity/account-window.js +66 -0
  4. package/dist/capacity/account-window.js.map +1 -0
  5. package/dist/capacity/fleet-pause-verbs.d.ts +38 -0
  6. package/dist/capacity/fleet-pause-verbs.d.ts.map +1 -0
  7. package/dist/capacity/fleet-pause-verbs.js +26 -0
  8. package/dist/capacity/fleet-pause-verbs.js.map +1 -0
  9. package/dist/capacity/index.d.ts +15 -0
  10. package/dist/capacity/index.d.ts.map +1 -0
  11. package/dist/capacity/index.js +15 -0
  12. package/dist/capacity/index.js.map +1 -0
  13. package/dist/capacity/resume-controller.d.ts +150 -0
  14. package/dist/capacity/resume-controller.d.ts.map +1 -0
  15. package/dist/capacity/resume-controller.js +229 -0
  16. package/dist/capacity/resume-controller.js.map +1 -0
  17. package/dist/capacity/resume-policy.d.ts +101 -0
  18. package/dist/capacity/resume-policy.d.ts.map +1 -0
  19. package/dist/capacity/resume-policy.js +80 -0
  20. package/dist/capacity/resume-policy.js.map +1 -0
  21. package/dist/engines/account-identity.d.ts +54 -0
  22. package/dist/engines/account-identity.d.ts.map +1 -0
  23. package/dist/engines/account-identity.js +93 -0
  24. package/dist/engines/account-identity.js.map +1 -0
  25. package/dist/engines/account-probe.d.ts +124 -0
  26. package/dist/engines/account-probe.d.ts.map +1 -0
  27. package/dist/engines/account-probe.js +134 -0
  28. package/dist/engines/account-probe.js.map +1 -0
  29. package/dist/engines/claude-code-chat.d.ts +15 -1
  30. package/dist/engines/claude-code-chat.d.ts.map +1 -1
  31. package/dist/engines/claude-code-chat.js +118 -1
  32. package/dist/engines/claude-code-chat.js.map +1 -1
  33. package/dist/engines/codex-chat.d.ts.map +1 -1
  34. package/dist/engines/codex-chat.js +23 -0
  35. package/dist/engines/codex-chat.js.map +1 -1
  36. package/dist/engines/gemini.d.ts.map +1 -1
  37. package/dist/engines/gemini.js +20 -0
  38. package/dist/engines/gemini.js.map +1 -1
  39. package/dist/engines/types.d.ts +43 -0
  40. package/dist/engines/types.d.ts.map +1 -1
  41. package/dist/task-runner.d.ts.map +1 -1
  42. package/dist/task-runner.js +17 -0
  43. package/dist/task-runner.js.map +1 -1
  44. package/dist/watch.d.ts +11 -1
  45. package/dist/watch.d.ts.map +1 -1
  46. package/dist/watch.js +25 -3
  47. package/dist/watch.js.map +1 -1
  48. package/package.json +1 -1
@@ -0,0 +1,64 @@
1
+ /**
2
+ * AS-3 (RESUME): runner-side per-account capacity WINDOW MEMORY.
3
+ *
4
+ * A durable, account-keyed memory of "is this login's subscription window
5
+ * capped, and until when?" keyed on the engine's `synthetic_account`. It is the
6
+ * piece that lets the resume policy RE-PAUSE a returning capped account FROM
7
+ * MEMORY without burning a request (operator scenario: swap back to a
8
+ * still-capped `ajay` → re-pause immediately rather than claim a task just to
9
+ * watch it 429 again).
10
+ *
11
+ * Two states only — `open | capped` — mirroring the server account-window fold
12
+ * (api/_lib/account-capacity/account-window.ts). The shape is DUPLICATED on
13
+ * purpose: the runner is a standalone published package and MUST NOT import from
14
+ * api/_lib (the cost-pricing.ts / capacity-plugin.ts precedent). Kept in sync by
15
+ * hand. // keep in sync
16
+ *
17
+ * The fold applies the CC-3A hysteresis rule verbatim: a capped window reopens
18
+ * ONLY on a tick whose clock has passed a KNOWN reset instant — never on a
19
+ * null-reset guess (an indefinite cap stays capped until a probe/account-change
20
+ * says otherwise), so a window can't flap open on a missing reset time.
21
+ */
22
+ /** Whether an account's subscription window is serving or capped. */
23
+ export type AccountWindowState = "open" | "capped";
24
+ /** One account's remembered capacity window. */
25
+ export interface AccountWindow {
26
+ state: AccountWindowState;
27
+ /**
28
+ * Absolute reset instant (epoch ms) for a capped window with a KNOWN reset;
29
+ * `null` means capped indefinitely (no parseable reset) — treated as the
30
+ * longest possible wait, so extend-only never shortens it and a tick never
31
+ * reopens it on a guess.
32
+ */
33
+ resetAtMs: number | null;
34
+ /** Epoch ms this window was last folded (audit / staleness). */
35
+ updatedAtMs: number;
36
+ }
37
+ /** An event folded into a window. */
38
+ export type AccountWindowEvent =
39
+ /** The account hit a subscription/usage cap; `resetAtMs` is its stated reset (null = indefinite). */
40
+ {
41
+ kind: "cap_hit";
42
+ resetAtMs: number | null;
43
+ atMs: number;
44
+ }
45
+ /** A clock tick — reopens a capped window iff its known reset has elapsed. */
46
+ | {
47
+ kind: "tick";
48
+ atMs: number;
49
+ };
50
+ /**
51
+ * Fold an event into a window. Pure. Returns a fresh window (never mutates).
52
+ * - cap_hit: caps the window; extend-only on the reset instant.
53
+ * - tick: reopens a capped window ONLY when a known reset has elapsed.
54
+ */
55
+ export declare function foldAccountWindow(prev: AccountWindow | null, event: AccountWindowEvent): AccountWindow;
56
+ /**
57
+ * True when a window is capped AND that cap has NOT yet elapsed — the condition
58
+ * for re-pausing a returning account from memory. An indefinite cap (null reset)
59
+ * is always still-capped; a finite cap is still-capped until `nowMs` reaches it.
60
+ * A missing window (never seen) is NOT still-capped (honest unknown → let the
61
+ * probe decide).
62
+ */
63
+ export declare function isStillCapped(window: AccountWindow | null, nowMs: number): boolean;
64
+ //# sourceMappingURL=account-window.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"account-window.d.ts","sourceRoot":"","sources":["../../src/capacity/account-window.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,qEAAqE;AACrE,MAAM,MAAM,kBAAkB,GAAG,MAAM,GAAG,QAAQ,CAAC;AAEnD,gDAAgD;AAChD,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,kBAAkB,CAAC;IAC1B;;;;;OAKG;IACH,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,gEAAgE;IAChE,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,qCAAqC;AACrC,MAAM,MAAM,kBAAkB;AAC5B,qGAAqG;AACnG;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE;AAC7D,8EAA8E;GAC5E;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAYnC;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,aAAa,GAAG,IAAI,EAC1B,KAAK,EAAE,kBAAkB,GACxB,aAAa,CAiBf;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAGlF"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * AS-3 (RESUME): runner-side per-account capacity WINDOW MEMORY.
3
+ *
4
+ * A durable, account-keyed memory of "is this login's subscription window
5
+ * capped, and until when?" keyed on the engine's `synthetic_account`. It is the
6
+ * piece that lets the resume policy RE-PAUSE a returning capped account FROM
7
+ * MEMORY without burning a request (operator scenario: swap back to a
8
+ * still-capped `ajay` → re-pause immediately rather than claim a task just to
9
+ * watch it 429 again).
10
+ *
11
+ * Two states only — `open | capped` — mirroring the server account-window fold
12
+ * (api/_lib/account-capacity/account-window.ts). The shape is DUPLICATED on
13
+ * purpose: the runner is a standalone published package and MUST NOT import from
14
+ * api/_lib (the cost-pricing.ts / capacity-plugin.ts precedent). Kept in sync by
15
+ * hand. // keep in sync
16
+ *
17
+ * The fold applies the CC-3A hysteresis rule verbatim: a capped window reopens
18
+ * ONLY on a tick whose clock has passed a KNOWN reset instant — never on a
19
+ * null-reset guess (an indefinite cap stays capped until a probe/account-change
20
+ * says otherwise), so a window can't flap open on a missing reset time.
21
+ */
22
+ /**
23
+ * Extend-only reset merge: a straggler cap can only push a window's reset LATER,
24
+ * never earlier (mirrors watch.ts enterCapacityPause's extend-only rule). `null`
25
+ * (indefinite) is the longest wait, so it dominates any finite reset.
26
+ */
27
+ function mergeResetLater(prev, next) {
28
+ if (prev === null || next === null)
29
+ return null;
30
+ return Math.max(prev, next);
31
+ }
32
+ /**
33
+ * Fold an event into a window. Pure. Returns a fresh window (never mutates).
34
+ * - cap_hit: caps the window; extend-only on the reset instant.
35
+ * - tick: reopens a capped window ONLY when a known reset has elapsed.
36
+ */
37
+ export function foldAccountWindow(prev, event) {
38
+ if (event.kind === "cap_hit") {
39
+ const resetAtMs = prev && prev.state === "capped"
40
+ ? mergeResetLater(prev.resetAtMs, event.resetAtMs)
41
+ : event.resetAtMs;
42
+ return { state: "capped", resetAtMs, updatedAtMs: event.atMs };
43
+ }
44
+ // tick
45
+ if (!prev || prev.state !== "capped") {
46
+ return prev ?? { state: "open", resetAtMs: null, updatedAtMs: event.atMs };
47
+ }
48
+ // Reopen only on a KNOWN, elapsed reset — never on a null-reset guess.
49
+ if (prev.resetAtMs !== null && event.atMs >= prev.resetAtMs) {
50
+ return { state: "open", resetAtMs: null, updatedAtMs: event.atMs };
51
+ }
52
+ return { ...prev, updatedAtMs: event.atMs };
53
+ }
54
+ /**
55
+ * True when a window is capped AND that cap has NOT yet elapsed — the condition
56
+ * for re-pausing a returning account from memory. An indefinite cap (null reset)
57
+ * is always still-capped; a finite cap is still-capped until `nowMs` reaches it.
58
+ * A missing window (never seen) is NOT still-capped (honest unknown → let the
59
+ * probe decide).
60
+ */
61
+ export function isStillCapped(window, nowMs) {
62
+ if (!window || window.state !== "capped")
63
+ return false;
64
+ return window.resetAtMs === null || nowMs < window.resetAtMs;
65
+ }
66
+ //# sourceMappingURL=account-window.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"account-window.js","sourceRoot":"","sources":["../../src/capacity/account-window.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AA0BH;;;;GAIG;AACH,SAAS,eAAe,CAAC,IAAmB,EAAE,IAAmB;IAC/D,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAChD,OAAO,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;AAC9B,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAC/B,IAA0B,EAC1B,KAAyB;IAEzB,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC7B,MAAM,SAAS,GACb,IAAI,IAAI,IAAI,CAAC,KAAK,KAAK,QAAQ;YAC7B,CAAC,CAAC,eAAe,CAAC,IAAI,CAAC,SAAS,EAAE,KAAK,CAAC,SAAS,CAAC;YAClD,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC;QACtB,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC;IACjE,CAAC;IACD,OAAO;IACP,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;QACrC,OAAO,IAAI,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,WAAW,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7E,CAAC;IACD,uEAAuE;IACvE,IAAI,IAAI,CAAC,SAAS,KAAK,IAAI,IAAI,KAAK,CAAC,IAAI,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;QAC5D,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,WAAW,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC;IACrE,CAAC;IACD,OAAO,EAAE,GAAG,IAAI,EAAE,WAAW,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC;AAC9C,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAAC,MAA4B,EAAE,KAAa;IACvE,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IACvD,OAAO,MAAM,CAAC,SAAS,KAAK,IAAI,IAAI,KAAK,GAAG,MAAM,CAAC,SAAS,CAAC;AAC/D,CAAC"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * AS-3 (RESUME): the activity-event verbs that drive the server-side FLEET
3
+ * PAUSE picture.
4
+ *
5
+ * "fleet_pause" is NOT a table — the server DERIVES the live paused-fleet set
6
+ * from `activity_events` (api/_lib/fleet-pause.ts): a runner counts as paused
7
+ * while its LATEST capacity event is a `runner.capacity_paused` whose
8
+ * `resume_at` is absent or still future, and it CLEARS the moment a
9
+ * `runner.capacity_resume` lands. So the resume path "clears the server-side
10
+ * fleet_pause row" by emitting `runner.capacity_resume` for this runner.
11
+ *
12
+ * These strings are DUPLICATED from api/_lib/fleet-pause.ts on purpose (the
13
+ * runner is a standalone published package and must not import from api/_lib —
14
+ * the capacity-plugin.ts precedent). Kept in sync by hand. // keep in sync
15
+ *
16
+ * NOTE (drift the wiring must fix): watch.ts today emits `runner.paused_capacity`
17
+ * / `runner.resume_capacity`, which the loader does NOT read — so the current
18
+ * fixed-timer pause is invisible to fleet_pause. The AS-3 controller emits these
19
+ * canonical verbs through its `setServerPause` / `clearServerPause` seams so the
20
+ * server sees the pause AND its clear.
21
+ */
22
+ /** Verb a runner emits when it enters a capacity pause. Payload: `{ runner_id, resume_at?: ISO|null, reason?: string }`. */
23
+ export declare const RUNNER_CAPACITY_PAUSED_VERB = "runner.capacity_paused";
24
+ /** Verb a runner emits to clear its server-side fleet pause. Payload: `{ runner_id }`. */
25
+ export declare const RUNNER_CAPACITY_RESUME_VERB = "runner.capacity_resume";
26
+ /** Shape of the `runner.capacity_paused` payload the wiring should emit. */
27
+ export interface FleetPausePayload {
28
+ runner_id: string;
29
+ /** ISO reset instant, or null for an indefinite pause. */
30
+ resume_at: string | null;
31
+ /** Non-secret cause. */
32
+ reason?: string;
33
+ }
34
+ /** Shape of the `runner.capacity_resume` payload the wiring should emit. */
35
+ export interface FleetResumePayload {
36
+ runner_id: string;
37
+ }
38
+ //# sourceMappingURL=fleet-pause-verbs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fleet-pause-verbs.d.ts","sourceRoot":"","sources":["../../src/capacity/fleet-pause-verbs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,4HAA4H;AAC5H,eAAO,MAAM,2BAA2B,2BAA2B,CAAC;AAEpE,0FAA0F;AAC1F,eAAO,MAAM,2BAA2B,2BAA2B,CAAC;AAEpE,4EAA4E;AAC5E,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,0DAA0D;IAC1D,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,wBAAwB;IACxB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,4EAA4E;AAC5E,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAC;CACnB"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * AS-3 (RESUME): the activity-event verbs that drive the server-side FLEET
3
+ * PAUSE picture.
4
+ *
5
+ * "fleet_pause" is NOT a table — the server DERIVES the live paused-fleet set
6
+ * from `activity_events` (api/_lib/fleet-pause.ts): a runner counts as paused
7
+ * while its LATEST capacity event is a `runner.capacity_paused` whose
8
+ * `resume_at` is absent or still future, and it CLEARS the moment a
9
+ * `runner.capacity_resume` lands. So the resume path "clears the server-side
10
+ * fleet_pause row" by emitting `runner.capacity_resume` for this runner.
11
+ *
12
+ * These strings are DUPLICATED from api/_lib/fleet-pause.ts on purpose (the
13
+ * runner is a standalone published package and must not import from api/_lib —
14
+ * the capacity-plugin.ts precedent). Kept in sync by hand. // keep in sync
15
+ *
16
+ * NOTE (drift the wiring must fix): watch.ts today emits `runner.paused_capacity`
17
+ * / `runner.resume_capacity`, which the loader does NOT read — so the current
18
+ * fixed-timer pause is invisible to fleet_pause. The AS-3 controller emits these
19
+ * canonical verbs through its `setServerPause` / `clearServerPause` seams so the
20
+ * server sees the pause AND its clear.
21
+ */
22
+ /** Verb a runner emits when it enters a capacity pause. Payload: `{ runner_id, resume_at?: ISO|null, reason?: string }`. */
23
+ export const RUNNER_CAPACITY_PAUSED_VERB = "runner.capacity_paused";
24
+ /** Verb a runner emits to clear its server-side fleet pause. Payload: `{ runner_id }`. */
25
+ export const RUNNER_CAPACITY_RESUME_VERB = "runner.capacity_resume";
26
+ //# sourceMappingURL=fleet-pause-verbs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fleet-pause-verbs.js","sourceRoot":"","sources":["../../src/capacity/fleet-pause-verbs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,4HAA4H;AAC5H,MAAM,CAAC,MAAM,2BAA2B,GAAG,wBAAwB,CAAC;AAEpE,0FAA0F;AAC1F,MAAM,CAAC,MAAM,2BAA2B,GAAG,wBAAwB,CAAC"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * AS-3 (RESUME): the runner capacity RESUME surface.
3
+ *
4
+ * Public entry point for the pause-until-(reset OR account-change OR ping-ok)
5
+ * engine: the stateful `ResumeController`, the pure `evaluateResume` decision
6
+ * core, the per-account window memory, and the fleet-pause verbs the wiring
7
+ * emits to set/clear the server-side pause. Wiring these onto watch.ts's pause
8
+ * path and the companion turn loop is a follow-up (those files are outside this
9
+ * track's scope); this barrel is what that follow-up imports.
10
+ */
11
+ export { ResumeController, type ResumeControllerDeps, type ResumeTransition, } from "./resume-controller.js";
12
+ export { evaluateResume, type CapacityMode, type ResumeDecision, type ResumeEvalInput, type ResumeReason, } from "./resume-policy.js";
13
+ export { foldAccountWindow, isStillCapped, type AccountWindow, type AccountWindowEvent, type AccountWindowState, } from "./account-window.js";
14
+ export { RUNNER_CAPACITY_PAUSED_VERB, RUNNER_CAPACITY_RESUME_VERB, type FleetPausePayload, type FleetResumePayload, } from "./fleet-pause-verbs.js";
15
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/capacity/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EACL,gBAAgB,EAChB,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,GACtB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACL,cAAc,EACd,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,KAAK,YAAY,GAClB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,iBAAiB,EACjB,aAAa,EACb,KAAK,aAAa,EAClB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,GACxB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,2BAA2B,EAC3B,2BAA2B,EAC3B,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,GACxB,MAAM,wBAAwB,CAAC"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * AS-3 (RESUME): the runner capacity RESUME surface.
3
+ *
4
+ * Public entry point for the pause-until-(reset OR account-change OR ping-ok)
5
+ * engine: the stateful `ResumeController`, the pure `evaluateResume` decision
6
+ * core, the per-account window memory, and the fleet-pause verbs the wiring
7
+ * emits to set/clear the server-side pause. Wiring these onto watch.ts's pause
8
+ * path and the companion turn loop is a follow-up (those files are outside this
9
+ * track's scope); this barrel is what that follow-up imports.
10
+ */
11
+ export { ResumeController, } from "./resume-controller.js";
12
+ export { evaluateResume, } from "./resume-policy.js";
13
+ export { foldAccountWindow, isStillCapped, } from "./account-window.js";
14
+ export { RUNNER_CAPACITY_PAUSED_VERB, RUNNER_CAPACITY_RESUME_VERB, } from "./fleet-pause-verbs.js";
15
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/capacity/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EACL,gBAAgB,GAGjB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACL,cAAc,GAKf,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,iBAAiB,EACjB,aAAa,GAId,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,2BAA2B,EAC3B,2BAA2B,GAG5B,MAAM,wBAAwB,CAAC"}
@@ -0,0 +1,150 @@
1
+ /**
2
+ * AS-3 (RESUME): the capacity RESUME CONTROLLER.
3
+ *
4
+ * The stateful governor that replaces watch.ts's fixed pause-until-reset
5
+ * `setTimeout` with pause-until-(reset OR account-change OR ping-ok), and adds
6
+ * the symmetric re-pause-from-memory for a returning still-capped account.
7
+ * It owns the per-account window memory + the pure resume policy
8
+ * (resume-policy.ts) and drives them off:
9
+ *
10
+ * - `reportCapacity()` — a task/review hit a subscription cap (watch.ts's
11
+ * `enterCapacityPause` trigger). Folds a cap onto the account and, if it is
12
+ * the current account, ENTERS the pause.
13
+ * - `observeAccount()` — the account probe (AS-1 `AccountProbeScheduler` →
14
+ * AS-2 broadcaster) saw the logged-in account. The fast path for a login
15
+ * swap: fires an immediate re-evaluation so a swap converges within one
16
+ * probe cycle rather than waiting on the stale account's reset.
17
+ * - an internal POLL (armed only while paused, at `pollIntervalMs`) that
18
+ * re-checks the reset instant + fires the cheap probe ping.
19
+ *
20
+ * On RESUME it does the three things the plan pins to the SAME TICK: clears the
21
+ * server-side fleet pause (`clearServerPause`), restarts task+review claim
22
+ * scans (`restartClaims`), and kicks the companion turn loop (`kickCompanion`)
23
+ * so queued chat turns drain — then leaves the pause. In-flight sessions are
24
+ * never touched: the controller only gates CLAIMING, it has no abort seam.
25
+ *
26
+ * FULLY injectable (clock / timer / probe / every side-effect seam) and every
27
+ * action is serialized onto one tail promise so overlapping triggers can't race
28
+ * — so the whole lifecycle is exercised with a fake clock, no wall-clock sleep,
29
+ * no real binary, no network.
30
+ */
31
+ import type { ProbeTimer } from "../engines/account-probe.js";
32
+ import { type AccountWindow } from "./account-window.js";
33
+ import { type CapacityMode, type ResumeReason } from "./resume-policy.js";
34
+ /** A pause/resume transition, handed to the audit sink. */
35
+ export interface ResumeTransition {
36
+ from: CapacityMode;
37
+ to: CapacityMode;
38
+ /** Why the transition fired. */
39
+ reason: ResumeReason | "cap-hit" | "returning-capped-account";
40
+ /** The account the runner is anchored to after the transition. */
41
+ account: string | null;
42
+ /** Which driver produced it. */
43
+ trigger: "cap-report" | "observe-account" | "poll";
44
+ at: number;
45
+ }
46
+ export interface ResumeControllerDeps {
47
+ /** Clock (test seam). Default `Date.now`. */
48
+ now?: () => number;
49
+ /** Timer seam (test seam). Default `setTimeout`/`clearTimeout`. */
50
+ timer?: ProbeTimer;
51
+ /** While-paused re-check cadence (ms). Default 60s. */
52
+ pollIntervalMs?: number;
53
+ /** Min gap between account-change adoptions (flap protection). Default 60s. */
54
+ adoptionDebounceMs?: number;
55
+ /**
56
+ * Cheap serve-check for the observed account while paused:
57
+ * true = can serve now, false = still capped, null = can't tell.
58
+ * Omit to disable ping-based resume (reset + account-change still apply).
59
+ */
60
+ probePing?: () => Promise<boolean | null>;
61
+ /** Clear the server-side fleet pause (emit `runner.capacity_resume`). */
62
+ clearServerPause: () => void | Promise<void>;
63
+ /** Set the server-side fleet pause (emit `runner.capacity_paused`). Optional. */
64
+ setServerPause?: (ctx: {
65
+ account: string;
66
+ resetAtMs: number | null;
67
+ }) => void | Promise<void>;
68
+ /** Restart task + review claim scans (watch.ts: pollOnce + pump + pumpReviews). */
69
+ restartClaims: () => void | Promise<void>;
70
+ /** Stop claiming tasks + reviews (watch.ts: pausedCapacity = true). Optional. */
71
+ stopClaims?: () => void | Promise<void>;
72
+ /** Kick the companion turn loop so queued chat turns drain. Optional. */
73
+ kickCompanion?: () => void | Promise<void>;
74
+ /** Freeze the companion turn loop on (re-)pause. Optional. */
75
+ freezeCompanion?: () => void | Promise<void>;
76
+ /** Best-effort audit/log sink. */
77
+ onLog?: (line: string) => void;
78
+ /** Transition audit hook. */
79
+ onTransition?: (t: ResumeTransition) => void;
80
+ }
81
+ /**
82
+ * Drives the pause-until-(reset OR account-change OR ping-ok) lifecycle. Start
83
+ * it once the runner knows its account (`start`), feed it cap reports and
84
+ * account observations, and it clears/sets the server pause and restarts claims
85
+ * through the injected seams at the right moment.
86
+ */
87
+ export declare class ResumeController {
88
+ private readonly deps;
89
+ private readonly now;
90
+ private readonly timer;
91
+ private readonly pollIntervalMs;
92
+ private readonly adoptionDebounceMs;
93
+ private started;
94
+ private stopped;
95
+ private mode;
96
+ /** The account the current state is anchored to (capped account while paused). */
97
+ private anchorAccountKey;
98
+ /** The account the probe last observed. */
99
+ private observedAccountKey;
100
+ /** Per-account remembered capacity windows. */
101
+ private readonly windows;
102
+ /** Epoch ms of the last account-change adoption (null = none yet). */
103
+ private lastAdoptionAtMs;
104
+ private pollHandle;
105
+ /** Serial action chain so overlapping triggers never race. */
106
+ private tail;
107
+ constructor(deps: ResumeControllerDeps);
108
+ /** Current mode. */
109
+ currentMode(): CapacityMode;
110
+ /** The remembered window for an account (test/inspection seam). */
111
+ windowFor(accountKey: string): AccountWindow | null;
112
+ /** Note the runner's starting account. Idempotent. */
113
+ start(accountKey: string | null): void;
114
+ /**
115
+ * A task/review reported a subscription cap on `accountKey`. Folds the cap
116
+ * into that account's window and, when it is the current account and we are
117
+ * still claiming, enters the pause. Extend-only: a straggler cap on the
118
+ * already-paused account only pushes its reset later.
119
+ */
120
+ reportCapacity(input: {
121
+ accountKey: string;
122
+ resetAtMs: number | null;
123
+ detail?: string;
124
+ }): void;
125
+ /**
126
+ * The account probe observed the logged-in account. The fast path for a login
127
+ * swap — fires an immediate re-evaluation so a swap converges within one probe
128
+ * cycle. A same-account observation is a cheap no-op re-check.
129
+ */
130
+ observeAccount(accountKey: string | null): void;
131
+ /** Cancel the poll timer; in-flight actions still settle. */
132
+ stop(): void;
133
+ /** Resolve once every queued action has settled (test seam). */
134
+ settle(): Promise<void>;
135
+ private enqueue;
136
+ private armPoll;
137
+ private disarmPoll;
138
+ private onPollFire;
139
+ /** Evaluate the resume policy and act on it. */
140
+ private evaluate;
141
+ /** Enter (or re-enter) the capacity pause on `account`. */
142
+ private enterPause;
143
+ /**
144
+ * Leave the pause: clear the server fleet pause, restart claim scans, and kick
145
+ * the companion turn loop — all in this one tick (queued chat turns then drain
146
+ * on their own). In-flight sessions are never touched.
147
+ */
148
+ private doResume;
149
+ }
150
+ //# sourceMappingURL=resume-controller.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resume-controller.d.ts","sourceRoot":"","sources":["../../src/capacity/resume-controller.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,229 @@
1
+ import { foldAccountWindow } from "./account-window.js";
2
+ import { evaluateResume, } from "./resume-policy.js";
3
+ const DEFAULT_POLL_INTERVAL_MS = 60_000; // one probe cycle
4
+ const DEFAULT_ADOPTION_DEBOUNCE_MS = 60_000; // CC-3A cooldownMinDwellMs
5
+ const DEFAULT_TIMER = {
6
+ setTimer: (fn, ms) => setTimeout(fn, ms),
7
+ clearTimer: (h) => clearTimeout(h),
8
+ };
9
+ /**
10
+ * Drives the pause-until-(reset OR account-change OR ping-ok) lifecycle. Start
11
+ * it once the runner knows its account (`start`), feed it cap reports and
12
+ * account observations, and it clears/sets the server pause and restarts claims
13
+ * through the injected seams at the right moment.
14
+ */
15
+ export class ResumeController {
16
+ deps;
17
+ now;
18
+ timer;
19
+ pollIntervalMs;
20
+ adoptionDebounceMs;
21
+ started = false;
22
+ stopped = false;
23
+ mode = "active";
24
+ /** The account the current state is anchored to (capped account while paused). */
25
+ anchorAccountKey = null;
26
+ /** The account the probe last observed. */
27
+ observedAccountKey = null;
28
+ /** Per-account remembered capacity windows. */
29
+ windows = new Map();
30
+ /** Epoch ms of the last account-change adoption (null = none yet). */
31
+ lastAdoptionAtMs = null;
32
+ pollHandle = null;
33
+ /** Serial action chain so overlapping triggers never race. */
34
+ tail = Promise.resolve();
35
+ constructor(deps) {
36
+ this.deps = deps;
37
+ this.now = deps.now ?? Date.now;
38
+ this.timer = deps.timer ?? DEFAULT_TIMER;
39
+ this.pollIntervalMs = Math.max(1, deps.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS);
40
+ this.adoptionDebounceMs = Math.max(0, deps.adoptionDebounceMs ?? DEFAULT_ADOPTION_DEBOUNCE_MS);
41
+ }
42
+ /** Current mode. */
43
+ currentMode() {
44
+ return this.mode;
45
+ }
46
+ /** The remembered window for an account (test/inspection seam). */
47
+ windowFor(accountKey) {
48
+ return this.windows.get(accountKey) ?? null;
49
+ }
50
+ /** Note the runner's starting account. Idempotent. */
51
+ start(accountKey) {
52
+ if (this.started || this.stopped)
53
+ return;
54
+ this.started = true;
55
+ this.anchorAccountKey = accountKey;
56
+ this.observedAccountKey = accountKey;
57
+ }
58
+ /**
59
+ * A task/review reported a subscription cap on `accountKey`. Folds the cap
60
+ * into that account's window and, when it is the current account and we are
61
+ * still claiming, enters the pause. Extend-only: a straggler cap on the
62
+ * already-paused account only pushes its reset later.
63
+ */
64
+ reportCapacity(input) {
65
+ if (this.stopped)
66
+ return;
67
+ if (!this.started)
68
+ this.start(input.accountKey);
69
+ const at = this.now();
70
+ this.windows.set(input.accountKey, foldAccountWindow(this.windows.get(input.accountKey) ?? null, {
71
+ kind: "cap_hit",
72
+ resetAtMs: input.resetAtMs,
73
+ atMs: at,
74
+ }));
75
+ // Only the CURRENT account's cap pauses the runner.
76
+ if (input.accountKey !== (this.observedAccountKey ?? this.anchorAccountKey))
77
+ return;
78
+ if (this.mode === "paused") {
79
+ // Already paused: the window was extended above; nothing else to do.
80
+ return;
81
+ }
82
+ this.enqueue(() => this.enterPause(input.accountKey, input.resetAtMs, "cap-hit", "cap-report"));
83
+ }
84
+ /**
85
+ * The account probe observed the logged-in account. The fast path for a login
86
+ * swap — fires an immediate re-evaluation so a swap converges within one probe
87
+ * cycle. A same-account observation is a cheap no-op re-check.
88
+ */
89
+ observeAccount(accountKey) {
90
+ if (this.stopped || !this.started)
91
+ return;
92
+ this.observedAccountKey = accountKey;
93
+ this.enqueue(() => this.evaluate("observe-account"));
94
+ }
95
+ /** Cancel the poll timer; in-flight actions still settle. */
96
+ stop() {
97
+ this.stopped = true;
98
+ this.disarmPoll();
99
+ }
100
+ /** Resolve once every queued action has settled (test seam). */
101
+ async settle() {
102
+ await this.tail;
103
+ }
104
+ enqueue(fn) {
105
+ this.tail = this.tail.then(fn).catch((err) => {
106
+ this.deps.onLog?.(`[acc-runner] resume-controller action failed: ${err?.message ?? String(err)}`);
107
+ });
108
+ }
109
+ armPoll() {
110
+ this.disarmPoll();
111
+ if (this.stopped)
112
+ return;
113
+ this.pollHandle = this.timer.setTimer(() => this.onPollFire(), this.pollIntervalMs);
114
+ }
115
+ disarmPoll() {
116
+ if (this.pollHandle !== null) {
117
+ this.timer.clearTimer(this.pollHandle);
118
+ this.pollHandle = null;
119
+ }
120
+ }
121
+ onPollFire() {
122
+ if (this.stopped || this.mode !== "paused")
123
+ return;
124
+ // Re-arm BEFORE the async evaluation so the cadence is decoupled from probe
125
+ // latency and stays deterministic under a fake clock.
126
+ this.armPoll();
127
+ this.enqueue(() => this.evaluate("poll"));
128
+ }
129
+ /** Evaluate the resume policy and act on it. */
130
+ async evaluate(trigger) {
131
+ if (this.stopped)
132
+ return;
133
+ let pingOk = null;
134
+ if (this.mode === "paused" && this.deps.probePing) {
135
+ // Only ping when the same account is still observed — a swap resolves via
136
+ // the account-change branch and needs no request.
137
+ const swapped = this.observedAccountKey !== null && this.observedAccountKey !== this.anchorAccountKey;
138
+ if (!swapped) {
139
+ try {
140
+ pingOk = await this.deps.probePing();
141
+ }
142
+ catch (err) {
143
+ this.deps.onLog?.(`[acc-runner] resume probe ping failed: ${err?.message ?? String(err)}`);
144
+ }
145
+ }
146
+ if (this.stopped)
147
+ return;
148
+ }
149
+ const decision = evaluateResume({
150
+ mode: this.mode,
151
+ nowMs: this.now(),
152
+ observedAccountKey: this.observedAccountKey,
153
+ anchorAccountKey: this.anchorAccountKey,
154
+ windows: this.windows,
155
+ pingOk,
156
+ lastAdoptionAtMs: this.lastAdoptionAtMs,
157
+ adoptionDebounceMs: this.adoptionDebounceMs,
158
+ });
159
+ switch (decision.action) {
160
+ case "stay":
161
+ return;
162
+ case "defer":
163
+ this.deps.onLog?.(`[acc-runner] account change to ${decision.account} deferred (adoption debounce)`);
164
+ return;
165
+ case "resume":
166
+ await this.doResume(decision.reason, decision.account, trigger);
167
+ return;
168
+ case "pause":
169
+ await this.enterPause(decision.account, decision.resetAtMs, "returning-capped-account", trigger);
170
+ return;
171
+ }
172
+ }
173
+ /** Enter (or re-enter) the capacity pause on `account`. */
174
+ async enterPause(account, resetAtMs, reason, trigger) {
175
+ if (this.stopped)
176
+ return;
177
+ const from = this.mode;
178
+ const at = this.now();
179
+ // Ensure the account's window reflects this cap.
180
+ this.windows.set(account, foldAccountWindow(this.windows.get(account) ?? null, {
181
+ kind: "cap_hit",
182
+ resetAtMs,
183
+ atMs: at,
184
+ }));
185
+ this.mode = "paused";
186
+ this.anchorAccountKey = account;
187
+ // An account-change re-pause counts as an adoption (flap protection); a
188
+ // plain cap-hit does not, so the first swap after it is never debounced.
189
+ if (reason === "returning-capped-account")
190
+ this.lastAdoptionAtMs = at;
191
+ this.armPoll();
192
+ if (this.deps.setServerPause) {
193
+ await this.deps.setServerPause({ account, resetAtMs });
194
+ }
195
+ if (this.deps.stopClaims)
196
+ await this.deps.stopClaims();
197
+ if (this.deps.freezeCompanion)
198
+ await this.deps.freezeCompanion();
199
+ this.deps.onTransition?.({ from, to: "paused", reason, account, trigger, at });
200
+ this.deps.onLog?.(`[acc-runner] capacity PAUSE (${reason}) on ${account}` +
201
+ (resetAtMs !== null ? ` until ${new Date(resetAtMs).toISOString()}` : " (indefinite)"));
202
+ }
203
+ /**
204
+ * Leave the pause: clear the server fleet pause, restart claim scans, and kick
205
+ * the companion turn loop — all in this one tick (queued chat turns then drain
206
+ * on their own). In-flight sessions are never touched.
207
+ */
208
+ async doResume(reason, account, trigger) {
209
+ if (this.stopped)
210
+ return;
211
+ const from = this.mode;
212
+ const at = this.now();
213
+ this.mode = "active";
214
+ this.anchorAccountKey = account;
215
+ this.lastAdoptionAtMs = at;
216
+ // We are now serving on this account, so its window is open in memory — a
217
+ // later swap back to it resumes (not re-pauses) until it caps again.
218
+ this.windows.set(account, { state: "open", resetAtMs: null, updatedAtMs: at });
219
+ this.disarmPoll();
220
+ // Same tick: clear server pause → restart task+review claims → drain companion.
221
+ await this.deps.clearServerPause();
222
+ await this.deps.restartClaims();
223
+ if (this.deps.kickCompanion)
224
+ await this.deps.kickCompanion();
225
+ this.deps.onTransition?.({ from, to: "active", reason, account, trigger, at });
226
+ this.deps.onLog?.(`[acc-runner] capacity RESUME (${reason}) on ${account}`);
227
+ }
228
+ }
229
+ //# sourceMappingURL=resume-controller.js.map