@tokenfactory/acc-runner 0.40.4 → 0.40.5
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,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CC-3E FAILOVER-E2E — companion contention resolution: most-recent-heartbeat
|
|
3
|
+
* wins.
|
|
4
|
+
*
|
|
5
|
+
* When an operator has TWO companions registered for the same conversation
|
|
6
|
+
* (e.g. a laptop that went to sleep and a desktop that just came online, or a
|
|
7
|
+
* duplicate launch), only one should serve. Per-turn contention is already safe
|
|
8
|
+
* — `acc.claim_chat_turn` takes a per-conversation advisory lock, so exactly one
|
|
9
|
+
* companion wins each individual turn (see companion.ts §advisory lock). But the
|
|
10
|
+
* lock's winner is arbitrary; a stale companion whose host is asleep can still
|
|
11
|
+
* grab the lock first and then requeue on lease-expiry, adding a wasted hop per
|
|
12
|
+
* turn. This module is the PURE tiebreak the pump consults before racing for the
|
|
13
|
+
* lock: prefer the companion whose heartbeat is freshest (and drop ones whose
|
|
14
|
+
* heartbeat has gone stale entirely).
|
|
15
|
+
*
|
|
16
|
+
* The heartbeat-recency comparator mirrors the reviewer-routing "freshest wins"
|
|
17
|
+
* rule (api/_lib/reviewer/routing.ts) but for chat companions, which had no such
|
|
18
|
+
* tiebreak. Pure — the clock is passed in — so every ordering + staleness branch
|
|
19
|
+
* is unit-testable without real timers.
|
|
20
|
+
*/
|
|
21
|
+
import type { RunnerCapacityClass } from "./engines/types.js";
|
|
22
|
+
/** A companion's last-known presence, as gathered from `identity:<user>` beats. */
|
|
23
|
+
export interface CompanionPresence {
|
|
24
|
+
runnerId: string;
|
|
25
|
+
/** Epoch ms of this companion's most recent heartbeat. */
|
|
26
|
+
lastHeartbeatMs: number;
|
|
27
|
+
/** Last-reported host capacity. */
|
|
28
|
+
capacity: RunnerCapacityClass;
|
|
29
|
+
/** Engines still claimable on this companion at report time. */
|
|
30
|
+
availableEngineIds: string[];
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Default staleness window: a companion whose last heartbeat is older than this
|
|
34
|
+
* is considered gone. Generous relative to the 4 s default beat cadence so a
|
|
35
|
+
* single missed beat (GC pause, brief network blip) does not evict a live host,
|
|
36
|
+
* but tight enough that a slept/crashed host stops winning within seconds.
|
|
37
|
+
*/
|
|
38
|
+
export declare const DEFAULT_HEARTBEAT_STALE_MS = 15000;
|
|
39
|
+
export interface CompanionSelectOptions {
|
|
40
|
+
/** Current time (epoch ms). */
|
|
41
|
+
now: number;
|
|
42
|
+
/** Heartbeats older than this are dropped as gone. Default 15s. */
|
|
43
|
+
staleMs?: number;
|
|
44
|
+
/**
|
|
45
|
+
* Require the winner to still advertise claimable engines. Default true — a
|
|
46
|
+
* companion reporting an empty engine set has nothing to serve and should not
|
|
47
|
+
* win even if its heartbeat is the freshest.
|
|
48
|
+
*/
|
|
49
|
+
requireEngines?: boolean;
|
|
50
|
+
}
|
|
51
|
+
/** True when a heartbeat is within the staleness window (i.e. the host is live). */
|
|
52
|
+
export declare function isHeartbeatLive(presence: CompanionPresence, now: number, staleMs?: number): boolean;
|
|
53
|
+
/** The eligible companions (live + non-empty engines when required), unranked. */
|
|
54
|
+
export declare function eligibleCompanions(companions: readonly CompanionPresence[], opts: CompanionSelectOptions): CompanionPresence[];
|
|
55
|
+
/**
|
|
56
|
+
* Pick the companion that should serve: the live one with the FRESHEST heartbeat.
|
|
57
|
+
* Ties (identical heartbeat ms — e.g. a replayed beat) break on `runnerId`
|
|
58
|
+
* lexicographically so the choice is deterministic. Returns null when no
|
|
59
|
+
* companion is eligible (all stale / no engines).
|
|
60
|
+
*/
|
|
61
|
+
export declare function pickFreshestCompanion(companions: readonly CompanionPresence[], opts: CompanionSelectOptions): CompanionPresence | null;
|
|
62
|
+
/**
|
|
63
|
+
* Should THIS companion defer (yield) to a fresher peer? True when a different,
|
|
64
|
+
* eligible companion has a strictly newer heartbeat. Used by the pump to avoid
|
|
65
|
+
* racing for the claim lock when a fresher peer is present — the stale host backs
|
|
66
|
+
* off instead of winning the lock and then requeueing on lease-expiry.
|
|
67
|
+
*/
|
|
68
|
+
export declare function shouldDeferToPeer(self: CompanionPresence, peers: readonly CompanionPresence[], opts: CompanionSelectOptions): boolean;
|
|
69
|
+
/** Parse an ISO-8601 heartbeat timestamp to epoch ms (NaN-safe → 0). */
|
|
70
|
+
export declare function heartbeatMsFromIso(ts: string | null | undefined): number;
|
|
71
|
+
//# sourceMappingURL=companion-select.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"companion-select.d.ts","sourceRoot":"","sources":["../src/companion-select.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAE9D,mFAAmF;AACnF,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,0DAA0D;IAC1D,eAAe,EAAE,MAAM,CAAC;IACxB,mCAAmC;IACnC,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,gEAAgE;IAChE,kBAAkB,EAAE,MAAM,EAAE,CAAC;CAC9B;AAED;;;;;GAKG;AACH,eAAO,MAAM,0BAA0B,QAAS,CAAC;AAEjD,MAAM,WAAW,sBAAsB;IACrC,+BAA+B;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,mEAAmE;IACnE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,oFAAoF;AACpF,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,iBAAiB,EAC3B,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,MAAmC,GAC3C,OAAO,CAET;AAED,kFAAkF;AAClF,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,SAAS,iBAAiB,EAAE,EACxC,IAAI,EAAE,sBAAsB,GAC3B,iBAAiB,EAAE,CAQrB;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,UAAU,EAAE,SAAS,iBAAiB,EAAE,EACxC,IAAI,EAAE,sBAAsB,GAC3B,iBAAiB,GAAG,IAAI,CAS1B;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,iBAAiB,EACvB,KAAK,EAAE,SAAS,iBAAiB,EAAE,EACnC,IAAI,EAAE,sBAAsB,GAC3B,OAAO,CAGT;AAED,wEAAwE;AACxE,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAIxE"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default staleness window: a companion whose last heartbeat is older than this
|
|
3
|
+
* is considered gone. Generous relative to the 4 s default beat cadence so a
|
|
4
|
+
* single missed beat (GC pause, brief network blip) does not evict a live host,
|
|
5
|
+
* but tight enough that a slept/crashed host stops winning within seconds.
|
|
6
|
+
*/
|
|
7
|
+
export const DEFAULT_HEARTBEAT_STALE_MS = 15_000;
|
|
8
|
+
/** True when a heartbeat is within the staleness window (i.e. the host is live). */
|
|
9
|
+
export function isHeartbeatLive(presence, now, staleMs = DEFAULT_HEARTBEAT_STALE_MS) {
|
|
10
|
+
return now - presence.lastHeartbeatMs <= Math.max(0, staleMs);
|
|
11
|
+
}
|
|
12
|
+
/** The eligible companions (live + non-empty engines when required), unranked. */
|
|
13
|
+
export function eligibleCompanions(companions, opts) {
|
|
14
|
+
const staleMs = opts.staleMs ?? DEFAULT_HEARTBEAT_STALE_MS;
|
|
15
|
+
const requireEngines = opts.requireEngines ?? true;
|
|
16
|
+
return companions.filter((c) => {
|
|
17
|
+
if (!isHeartbeatLive(c, opts.now, staleMs))
|
|
18
|
+
return false;
|
|
19
|
+
if (requireEngines && c.availableEngineIds.length === 0)
|
|
20
|
+
return false;
|
|
21
|
+
return true;
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Pick the companion that should serve: the live one with the FRESHEST heartbeat.
|
|
26
|
+
* Ties (identical heartbeat ms — e.g. a replayed beat) break on `runnerId`
|
|
27
|
+
* lexicographically so the choice is deterministic. Returns null when no
|
|
28
|
+
* companion is eligible (all stale / no engines).
|
|
29
|
+
*/
|
|
30
|
+
export function pickFreshestCompanion(companions, opts) {
|
|
31
|
+
const eligible = eligibleCompanions(companions, opts);
|
|
32
|
+
if (eligible.length === 0)
|
|
33
|
+
return null;
|
|
34
|
+
return eligible.reduce((best, c) => {
|
|
35
|
+
if (c.lastHeartbeatMs !== best.lastHeartbeatMs) {
|
|
36
|
+
return c.lastHeartbeatMs > best.lastHeartbeatMs ? c : best;
|
|
37
|
+
}
|
|
38
|
+
return c.runnerId < best.runnerId ? c : best;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Should THIS companion defer (yield) to a fresher peer? True when a different,
|
|
43
|
+
* eligible companion has a strictly newer heartbeat. Used by the pump to avoid
|
|
44
|
+
* racing for the claim lock when a fresher peer is present — the stale host backs
|
|
45
|
+
* off instead of winning the lock and then requeueing on lease-expiry.
|
|
46
|
+
*/
|
|
47
|
+
export function shouldDeferToPeer(self, peers, opts) {
|
|
48
|
+
const winner = pickFreshestCompanion([self, ...peers], opts);
|
|
49
|
+
return !!winner && winner.runnerId !== self.runnerId;
|
|
50
|
+
}
|
|
51
|
+
/** Parse an ISO-8601 heartbeat timestamp to epoch ms (NaN-safe → 0). */
|
|
52
|
+
export function heartbeatMsFromIso(ts) {
|
|
53
|
+
if (!ts)
|
|
54
|
+
return 0;
|
|
55
|
+
const n = Date.parse(ts);
|
|
56
|
+
return Number.isFinite(n) ? n : 0;
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=companion-select.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"companion-select.js","sourceRoot":"","sources":["../src/companion-select.ts"],"names":[],"mappings":"AAiCA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,MAAM,CAAC;AAejD,oFAAoF;AACpF,MAAM,UAAU,eAAe,CAC7B,QAA2B,EAC3B,GAAW,EACX,UAAkB,0BAA0B;IAE5C,OAAO,GAAG,GAAG,QAAQ,CAAC,eAAe,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;AAChE,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,kBAAkB,CAChC,UAAwC,EACxC,IAA4B;IAE5B,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,0BAA0B,CAAC;IAC3D,MAAM,cAAc,GAAG,IAAI,CAAC,cAAc,IAAI,IAAI,CAAC;IACnD,OAAO,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE;QAC7B,IAAI,CAAC,eAAe,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,EAAE,OAAO,CAAC;YAAE,OAAO,KAAK,CAAC;QACzD,IAAI,cAAc,IAAI,CAAC,CAAC,kBAAkB,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,KAAK,CAAC;QACtE,OAAO,IAAI,CAAC;IACd,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,qBAAqB,CACnC,UAAwC,EACxC,IAA4B;IAE5B,MAAM,QAAQ,GAAG,kBAAkB,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;IACtD,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACvC,OAAO,QAAQ,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE;QACjC,IAAI,CAAC,CAAC,eAAe,KAAK,IAAI,CAAC,eAAe,EAAE,CAAC;YAC/C,OAAO,CAAC,CAAC,eAAe,GAAG,IAAI,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QAC7D,CAAC;QACD,OAAO,CAAC,CAAC,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAC/C,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAC/B,IAAuB,EACvB,KAAmC,EACnC,IAA4B;IAE5B,MAAM,MAAM,GAAG,qBAAqB,CAAC,CAAC,IAAI,EAAE,GAAG,KAAK,CAAC,EAAE,IAAI,CAAC,CAAC;IAC7D,OAAO,CAAC,CAAC,MAAM,IAAI,MAAM,CAAC,QAAQ,KAAK,IAAI,CAAC,QAAQ,CAAC;AACvD,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,kBAAkB,CAAC,EAA6B;IAC9D,IAAI,CAAC,EAAE;QAAE,OAAO,CAAC,CAAC;IAClB,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IACzB,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACpC,CAAC"}
|