@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.
- package/dist/capacity/account-window.d.ts +64 -0
- package/dist/capacity/account-window.d.ts.map +1 -0
- package/dist/capacity/account-window.js +66 -0
- package/dist/capacity/account-window.js.map +1 -0
- package/dist/capacity/fleet-pause-verbs.d.ts +38 -0
- package/dist/capacity/fleet-pause-verbs.d.ts.map +1 -0
- package/dist/capacity/fleet-pause-verbs.js +26 -0
- package/dist/capacity/fleet-pause-verbs.js.map +1 -0
- package/dist/capacity/index.d.ts +15 -0
- package/dist/capacity/index.d.ts.map +1 -0
- package/dist/capacity/index.js +15 -0
- package/dist/capacity/index.js.map +1 -0
- package/dist/capacity/resume-controller.d.ts +150 -0
- package/dist/capacity/resume-controller.d.ts.map +1 -0
- package/dist/capacity/resume-controller.js +229 -0
- package/dist/capacity/resume-controller.js.map +1 -0
- package/dist/capacity/resume-policy.d.ts +101 -0
- package/dist/capacity/resume-policy.d.ts.map +1 -0
- package/dist/capacity/resume-policy.js +80 -0
- package/dist/capacity/resume-policy.js.map +1 -0
- package/dist/engines/account-identity.d.ts +54 -0
- package/dist/engines/account-identity.d.ts.map +1 -0
- package/dist/engines/account-identity.js +93 -0
- package/dist/engines/account-identity.js.map +1 -0
- package/dist/engines/account-probe.d.ts +124 -0
- package/dist/engines/account-probe.d.ts.map +1 -0
- package/dist/engines/account-probe.js +134 -0
- package/dist/engines/account-probe.js.map +1 -0
- package/dist/engines/claude-code-chat.d.ts +15 -1
- package/dist/engines/claude-code-chat.d.ts.map +1 -1
- package/dist/engines/claude-code-chat.js +118 -1
- package/dist/engines/claude-code-chat.js.map +1 -1
- package/dist/engines/codex-chat.d.ts.map +1 -1
- package/dist/engines/codex-chat.js +23 -0
- package/dist/engines/codex-chat.js.map +1 -1
- package/dist/engines/gemini.d.ts.map +1 -1
- package/dist/engines/gemini.js +20 -0
- package/dist/engines/gemini.js.map +1 -1
- package/dist/engines/types.d.ts +43 -0
- package/dist/engines/types.d.ts.map +1 -1
- package/dist/task-runner.d.ts.map +1 -1
- package/dist/task-runner.js +17 -0
- package/dist/task-runner.js.map +1 -1
- package/dist/watch.d.ts +11 -1
- package/dist/watch.d.ts.map +1 -1
- package/dist/watch.js +25 -3
- package/dist/watch.js.map +1 -1
- 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
|