@songsid/agend 2.1.10-beta.6 → 2.1.11-beta.1
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/backend/antigravity.js +3 -2
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/claude-code.d.ts +7 -0
- package/dist/backend/claude-code.js +107 -3
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/cli-env-probe-worker.js +2 -10
- package/dist/backend/cli-env-probe-worker.js.map +1 -1
- package/dist/backend/cli-env-probe.d.ts +14 -0
- package/dist/backend/cli-env-probe.js +13 -0
- package/dist/backend/cli-env-probe.js.map +1 -0
- package/dist/backend/codex-metadata.d.ts +14 -0
- package/dist/backend/codex-metadata.js +70 -0
- package/dist/backend/codex-metadata.js.map +1 -0
- package/dist/backend/codex.d.ts +1 -2
- package/dist/backend/codex.js +112 -65
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/effort-metadata.d.ts +32 -0
- package/dist/backend/effort-metadata.js +21 -0
- package/dist/backend/effort-metadata.js.map +1 -0
- package/dist/backend/grok.js +3 -2
- package/dist/backend/grok.js.map +1 -1
- package/dist/backend/kiro.d.ts +3 -0
- package/dist/backend/kiro.js +19 -4
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/muse.js +3 -2
- package/dist/backend/muse.js.map +1 -1
- package/dist/backend/opencode.d.ts +31 -4
- package/dist/backend/opencode.js +66 -23
- package/dist/backend/opencode.js.map +1 -1
- package/dist/backend/types.d.ts +14 -1
- package/dist/backend/types.js.map +1 -1
- package/dist/channel/adapters/discord.js +10 -2
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/cli.js +48 -6
- package/dist/cli.js.map +1 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/daemon.d.ts +24 -0
- package/dist/daemon.js +109 -12
- package/dist/daemon.js.map +1 -1
- package/dist/doctor.d.ts +3 -1
- package/dist/doctor.js +11 -0
- package/dist/doctor.js.map +1 -1
- package/dist/fleet-context.d.ts +2 -2
- package/dist/fleet-control-audit.d.ts +83 -0
- package/dist/fleet-control-audit.js +195 -0
- package/dist/fleet-control-audit.js.map +1 -0
- package/dist/fleet-manager.d.ts +45 -35
- package/dist/fleet-manager.js +327 -384
- package/dist/fleet-manager.js.map +1 -1
- package/dist/full-restart.js +2 -1
- package/dist/full-restart.js.map +1 -1
- package/dist/general-knowledge/skills/fleet-config/SKILL.md +3 -2
- package/dist/host-memory.d.ts +16 -0
- package/dist/host-memory.js +32 -0
- package/dist/host-memory.js.map +1 -0
- package/dist/instance-lifecycle.js +4 -1
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/locale.js +62 -16
- package/dist/locale.js.map +1 -1
- package/dist/log-tail.d.ts +34 -0
- package/dist/log-tail.js +66 -0
- package/dist/log-tail.js.map +1 -0
- package/dist/logger.d.ts +15 -0
- package/dist/logger.js +89 -1
- package/dist/logger.js.map +1 -1
- package/dist/login-controller.d.ts +1 -3
- package/dist/login-controller.js +2 -6
- package/dist/login-controller.js.map +1 -1
- package/dist/login-flows.d.ts +0 -8
- package/dist/login-flows.js +1 -11
- package/dist/login-flows.js.map +1 -1
- package/dist/login-manager.d.ts +5 -20
- package/dist/login-manager.js +0 -53
- package/dist/login-manager.js.map +1 -1
- package/dist/login-window-lock.d.ts +1 -1
- package/dist/login-window-lock.js +1 -1
- package/dist/memory-pressure.d.ts +41 -0
- package/dist/memory-pressure.js +117 -0
- package/dist/memory-pressure.js.map +1 -0
- package/dist/probe-worker-pool.d.ts +28 -0
- package/dist/probe-worker-pool.js +129 -0
- package/dist/probe-worker-pool.js.map +1 -0
- package/dist/resource-report.d.ts +31 -0
- package/dist/resource-report.js +209 -0
- package/dist/resource-report.js.map +1 -0
- package/dist/rollout-index.d.ts +66 -0
- package/dist/rollout-index.js +124 -0
- package/dist/rollout-index.js.map +1 -0
- package/dist/slash-authz.d.ts +50 -0
- package/dist/slash-authz.js +19 -0
- package/dist/slash-authz.js.map +1 -0
- package/dist/spawn-gate.d.ts +13 -1
- package/dist/spawn-gate.js +108 -39
- package/dist/spawn-gate.js.map +1 -1
- package/dist/storm-window.d.ts +42 -0
- package/dist/storm-window.js +84 -1
- package/dist/storm-window.js.map +1 -1
- package/dist/topic-commands.d.ts +29 -2
- package/dist/topic-commands.js +77 -21
- package/dist/topic-commands.js.map +1 -1
- package/dist/transcript-sources.d.ts +21 -4
- package/dist/transcript-sources.js +55 -41
- package/dist/transcript-sources.js.map +1 -1
- package/dist/types.d.ts +5 -1
- package/dist/ui/settings.html +14 -13
- package/package.json +1 -1
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One cached listing of Codex's date-sharded rollout files, shared by every instance
|
|
3
|
+
* that follows the same sessions directory (#1161, D2).
|
|
4
|
+
*
|
|
5
|
+
* Every Codex instance's transcript source used to re-walk, stat and sort the WHOLE
|
|
6
|
+
* tree on every 2 s poll: N instances × (every directory + every rollout file), and the
|
|
7
|
+
* tree only ever grows. The listing is the same for all of them, so it is built once
|
|
8
|
+
* and shared; what is built is kept up to date incrementally:
|
|
9
|
+
*
|
|
10
|
+
* - a listing is reused for `ttlMs` (the pollers' own interval is 2 s, so a new rollout
|
|
11
|
+
* is noticed within a second of the next poll);
|
|
12
|
+
* - a directory whose mtime has not moved is not read again (creating a rollout bumps
|
|
13
|
+
* its directory's mtime; appending to one does not);
|
|
14
|
+
* - a rollout that has been quiet for `quietMs` is not stat'ed again until the next
|
|
15
|
+
* full refresh (a file's mtime only matters for ordering, and a quiet file is not
|
|
16
|
+
* moving), which bounds how stale an old, resumed rollout's position can be by `fullRefreshMs`;
|
|
17
|
+
* - a full refresh re-reads and re-stats everything, so none of the shortcuts can go
|
|
18
|
+
* wrong for longer than `fullRefreshMs`.
|
|
19
|
+
*
|
|
20
|
+
* Returns newest-first, the order the sources always had.
|
|
21
|
+
*/
|
|
22
|
+
export interface RolloutFile {
|
|
23
|
+
path: string;
|
|
24
|
+
mtimeMs: number;
|
|
25
|
+
size: number;
|
|
26
|
+
}
|
|
27
|
+
export interface RolloutFsOps {
|
|
28
|
+
readdir(dir: string): string[];
|
|
29
|
+
stat(path: string): {
|
|
30
|
+
mtimeMs: number;
|
|
31
|
+
size: number;
|
|
32
|
+
isDirectory(): boolean;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export interface RolloutIndexOptions {
|
|
36
|
+
ttlMs?: number;
|
|
37
|
+
quietMs?: number;
|
|
38
|
+
fullRefreshMs?: number;
|
|
39
|
+
maxDepth?: number;
|
|
40
|
+
ops?: RolloutFsOps;
|
|
41
|
+
now?: () => number;
|
|
42
|
+
}
|
|
43
|
+
export declare const DEFAULT_ROLLOUT_TTL_MS = 1000;
|
|
44
|
+
export declare class RolloutIndex {
|
|
45
|
+
private readonly root;
|
|
46
|
+
private readonly ttlMs;
|
|
47
|
+
private readonly quietMs;
|
|
48
|
+
private readonly fullRefreshMs;
|
|
49
|
+
private readonly maxDepth;
|
|
50
|
+
private readonly ops;
|
|
51
|
+
private readonly now;
|
|
52
|
+
private dirs;
|
|
53
|
+
private files;
|
|
54
|
+
private listing;
|
|
55
|
+
private builtAt;
|
|
56
|
+
private fullAt;
|
|
57
|
+
constructor(root: string, options?: RolloutIndexOptions);
|
|
58
|
+
/** Newest rollout first. `fresh` bypasses the shared listing (checkpoints, baselines). */
|
|
59
|
+
list(fresh?: boolean): RolloutFile[];
|
|
60
|
+
private rebuild;
|
|
61
|
+
private readDir;
|
|
62
|
+
}
|
|
63
|
+
/** The listing every source following `root` shares. */
|
|
64
|
+
export declare function sharedRolloutIndex(root: string): RolloutIndex;
|
|
65
|
+
/** Test seam: drop every shared listing and set the TTL new ones get (0 = always rebuild: a test edits its own tree between polls). */
|
|
66
|
+
export declare function resetSharedRolloutIndexesForTests(ttlMs?: number): void;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
const realOps = { readdir: dir => readdirSync(dir), stat: path => statSync(path) };
|
|
4
|
+
export const DEFAULT_ROLLOUT_TTL_MS = 1_000;
|
|
5
|
+
/** A directory read this soon after its last change is read once more before its mtime is trusted. */
|
|
6
|
+
const HOT_DIR_MS = 2_000;
|
|
7
|
+
export class RolloutIndex {
|
|
8
|
+
root;
|
|
9
|
+
ttlMs;
|
|
10
|
+
quietMs;
|
|
11
|
+
fullRefreshMs;
|
|
12
|
+
maxDepth;
|
|
13
|
+
ops;
|
|
14
|
+
now;
|
|
15
|
+
dirs = new Map();
|
|
16
|
+
files = new Map();
|
|
17
|
+
listing = [];
|
|
18
|
+
builtAt = Number.NEGATIVE_INFINITY;
|
|
19
|
+
fullAt = Number.NEGATIVE_INFINITY;
|
|
20
|
+
constructor(root, options = {}) {
|
|
21
|
+
this.root = root;
|
|
22
|
+
this.ttlMs = options.ttlMs ?? DEFAULT_ROLLOUT_TTL_MS;
|
|
23
|
+
this.quietMs = options.quietMs ?? 10 * 60_000;
|
|
24
|
+
this.fullRefreshMs = options.fullRefreshMs ?? 60_000;
|
|
25
|
+
this.maxDepth = options.maxDepth ?? 4;
|
|
26
|
+
this.ops = options.ops ?? realOps;
|
|
27
|
+
this.now = options.now ?? Date.now;
|
|
28
|
+
}
|
|
29
|
+
/** Newest rollout first. `fresh` bypasses the shared listing (checkpoints, baselines). */
|
|
30
|
+
list(fresh = false) {
|
|
31
|
+
const now = this.now();
|
|
32
|
+
if (!fresh && now - this.builtAt < this.ttlMs)
|
|
33
|
+
return this.listing;
|
|
34
|
+
const full = fresh || now - this.fullAt >= this.fullRefreshMs;
|
|
35
|
+
this.rebuild(now, full);
|
|
36
|
+
return this.listing;
|
|
37
|
+
}
|
|
38
|
+
rebuild(now, full) {
|
|
39
|
+
const dirs = new Map();
|
|
40
|
+
const files = new Map();
|
|
41
|
+
const walk = (dir, depth) => {
|
|
42
|
+
let mtimeMs;
|
|
43
|
+
try {
|
|
44
|
+
mtimeMs = this.ops.stat(dir).mtimeMs;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
let entry = this.dirs.get(dir);
|
|
50
|
+
// A directory whose last read was taken right after a change is read once more: file times have
|
|
51
|
+
// coarse granularity, so a rollout created in the same tick as that change shares its mtime and
|
|
52
|
+
// would otherwise stay unseen until the next full refresh. (Judged by when it was LAST READ, not by
|
|
53
|
+
// how hot it is now: the next refresh comes a poll interval later, when it has long since cooled.)
|
|
54
|
+
if (full || !entry || entry.mtimeMs !== mtimeMs || entry.hotWhenRead) {
|
|
55
|
+
entry = this.readDir(dir, mtimeMs, depth, now);
|
|
56
|
+
}
|
|
57
|
+
dirs.set(dir, entry);
|
|
58
|
+
for (const rollout of entry.rollouts) {
|
|
59
|
+
const known = this.files.get(rollout);
|
|
60
|
+
if (!full && known && now - known.mtimeMs > this.quietMs) {
|
|
61
|
+
files.set(rollout, known);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const st = this.ops.stat(rollout);
|
|
66
|
+
files.set(rollout, { path: rollout, mtimeMs: st.mtimeMs, size: st.size });
|
|
67
|
+
}
|
|
68
|
+
catch { /* raced with deletion */ }
|
|
69
|
+
}
|
|
70
|
+
if (depth < this.maxDepth)
|
|
71
|
+
for (const sub of entry.subdirs)
|
|
72
|
+
walk(sub, depth + 1);
|
|
73
|
+
};
|
|
74
|
+
walk(this.root, 0);
|
|
75
|
+
this.dirs = dirs;
|
|
76
|
+
this.files = files;
|
|
77
|
+
this.listing = [...files.values()].sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
78
|
+
this.builtAt = now;
|
|
79
|
+
if (full)
|
|
80
|
+
this.fullAt = now;
|
|
81
|
+
}
|
|
82
|
+
readDir(dir, mtimeMs, depth, now) {
|
|
83
|
+
const entry = { mtimeMs, subdirs: [], rollouts: [], hotWhenRead: now - mtimeMs < HOT_DIR_MS };
|
|
84
|
+
let names;
|
|
85
|
+
try {
|
|
86
|
+
names = this.ops.readdir(dir);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return entry;
|
|
90
|
+
}
|
|
91
|
+
for (const name of names) {
|
|
92
|
+
const path = join(dir, name);
|
|
93
|
+
if (name.startsWith("rollout-") && name.endsWith(".jsonl")) {
|
|
94
|
+
entry.rollouts.push(path);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (depth >= this.maxDepth)
|
|
98
|
+
continue;
|
|
99
|
+
try {
|
|
100
|
+
if (this.ops.stat(path).isDirectory())
|
|
101
|
+
entry.subdirs.push(path);
|
|
102
|
+
}
|
|
103
|
+
catch { /* raced with deletion */ }
|
|
104
|
+
}
|
|
105
|
+
return entry;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const shared = new Map();
|
|
109
|
+
let sharedTtlMs = DEFAULT_ROLLOUT_TTL_MS;
|
|
110
|
+
/** The listing every source following `root` shares. */
|
|
111
|
+
export function sharedRolloutIndex(root) {
|
|
112
|
+
let index = shared.get(root);
|
|
113
|
+
if (!index) {
|
|
114
|
+
index = new RolloutIndex(root, { ttlMs: sharedTtlMs });
|
|
115
|
+
shared.set(root, index);
|
|
116
|
+
}
|
|
117
|
+
return index;
|
|
118
|
+
}
|
|
119
|
+
/** Test seam: drop every shared listing and set the TTL new ones get (0 = always rebuild: a test edits its own tree between polls). */
|
|
120
|
+
export function resetSharedRolloutIndexesForTests(ttlMs = DEFAULT_ROLLOUT_TTL_MS) {
|
|
121
|
+
shared.clear();
|
|
122
|
+
sharedTtlMs = ttlMs;
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=rollout-index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rollout-index.js","sourceRoot":"","sources":["../src/rollout-index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who may use a Discord slash command, decided once at the door.
|
|
3
|
+
*
|
|
4
|
+
* AgEnD registers its slash commands GLOBALLY: every guild the bot is in, and every DM with it, shows the
|
|
5
|
+
* same menu — Discord has no per-channel visibility to lean on, and the lock emoji in a description is only
|
|
6
|
+
* a label. So the handler is the only place authority can be enforced, and for a long time it was not: a
|
|
7
|
+
* slash command skipped the allowlist that guards the same words typed as a message, never looked at which
|
|
8
|
+
* guild it came from, and (apart from /start) never looked at whether it came from a DM. Anyone who shared
|
|
9
|
+
* any server with the bot could `/steer`, `/compact` or `/cancel` a fleet instance, or run `/sysinfo`.
|
|
10
|
+
*
|
|
11
|
+
* This is the pure decision. The caller supplies the facts (it owns the routing table, the access managers
|
|
12
|
+
* and the adapter's config), so the rule is testable without a Discord client and cannot drift between the
|
|
13
|
+
* two places that dispatch slash commands.
|
|
14
|
+
*
|
|
15
|
+
* What it settles, in order:
|
|
16
|
+
* 1. a DM is refused — nothing in AgEnD is addressed to a DM channel, and `guildId` is the only thing that
|
|
17
|
+
* tells a DM from a guild;
|
|
18
|
+
* 2. the guild must be the adapter's own, unless the channel is a registered ClassicBot channel (those are
|
|
19
|
+
* open by design, in any guild the operator allowed) or the command is `/start` (which validates its own
|
|
20
|
+
* guild against `allowed_guilds`);
|
|
21
|
+
* 3. whoever speaks must be someone the text path would also hear: a fleet channel applies the access policy
|
|
22
|
+
* of the adapter that owns its instance, anywhere else the invoking adapter's. ClassicBot channels stay
|
|
23
|
+
* open, exactly as they are for typed messages.
|
|
24
|
+
*
|
|
25
|
+
* Which admin level a command needs stays with the command: this only decides whether the caller may speak.
|
|
26
|
+
*/
|
|
27
|
+
export type SlashScope = "fleet" | "classic" | "none";
|
|
28
|
+
export type SlashSpeaker =
|
|
29
|
+
/** The caller is an explicit fleet admin, or the access policy of the world that governs this channel admits them. */
|
|
30
|
+
"allowed" | "denied"
|
|
31
|
+
/** A fleet channel whose owning adapter is not running: its policy cannot be applied, so nothing is let through. */
|
|
32
|
+
| "owner-not-running";
|
|
33
|
+
export interface SlashFacts {
|
|
34
|
+
command: string;
|
|
35
|
+
/** Discord's guild id for the interaction; absent in a DM. */
|
|
36
|
+
guildId?: string;
|
|
37
|
+
/** The guild this adapter's fleet lives in (`group_id`); empty/absent when the adapter has none. */
|
|
38
|
+
primaryGuildId?: string;
|
|
39
|
+
/** What the channel the command was typed in is. */
|
|
40
|
+
scope: SlashScope;
|
|
41
|
+
speaker: SlashSpeaker;
|
|
42
|
+
}
|
|
43
|
+
export type SlashDenial = "dm" | "wrong-guild" | "not-allowed" | "owner-not-running";
|
|
44
|
+
export type SlashDecision = {
|
|
45
|
+
allow: true;
|
|
46
|
+
} | {
|
|
47
|
+
allow: false;
|
|
48
|
+
reason: SlashDenial;
|
|
49
|
+
};
|
|
50
|
+
export declare function decideSlash(f: SlashFacts): SlashDecision;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function decideSlash(f) {
|
|
2
|
+
if (!f.guildId)
|
|
3
|
+
return { allow: false, reason: "dm" };
|
|
4
|
+
const inOwnGuild = !!f.primaryGuildId && f.guildId === f.primaryGuildId;
|
|
5
|
+
if (!inOwnGuild && f.scope !== "classic" && f.command !== "start") {
|
|
6
|
+
return { allow: false, reason: "wrong-guild" };
|
|
7
|
+
}
|
|
8
|
+
// Typed messages in a ClassicBot channel are open to everyone there, and so are its slash commands.
|
|
9
|
+
if (f.scope === "classic")
|
|
10
|
+
return { allow: true };
|
|
11
|
+
// `/start` in a channel nobody has registered is how a ClassicBot channel comes to exist; its own guild
|
|
12
|
+
// check (allowed_guilds) is the gate, as it always was.
|
|
13
|
+
if (f.scope === "none" && f.command === "start")
|
|
14
|
+
return { allow: true };
|
|
15
|
+
if (f.speaker === "owner-not-running")
|
|
16
|
+
return { allow: false, reason: "owner-not-running" };
|
|
17
|
+
return f.speaker === "allowed" ? { allow: true } : { allow: false, reason: "not-allowed" };
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=slash-authz.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slash-authz.js","sourceRoot":"","sources":["../src/slash-authz.ts"],"names":[],"mappings":"AAkDA,MAAM,UAAU,WAAW,CAAC,CAAa;IACvC,IAAI,CAAC,CAAC,CAAC,OAAO;QAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;IAEtD,MAAM,UAAU,GAAG,CAAC,CAAC,CAAC,CAAC,cAAc,IAAI,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,cAAc,CAAC;IACxE,IAAI,CAAC,UAAU,IAAI,CAAC,CAAC,KAAK,KAAK,SAAS,IAAI,CAAC,CAAC,OAAO,KAAK,OAAO,EAAE,CAAC;QAClE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,CAAC;IACjD,CAAC;IAED,oGAAoG;IACpG,IAAI,CAAC,CAAC,KAAK,KAAK,SAAS;QAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IAClD,wGAAwG;IACxG,wDAAwD;IACxD,IAAI,CAAC,CAAC,KAAK,KAAK,MAAM,IAAI,CAAC,CAAC,OAAO,KAAK,OAAO;QAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IAExE,IAAI,CAAC,CAAC,OAAO,KAAK,mBAAmB;QAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,mBAAmB,EAAE,CAAC;IAC5F,OAAO,CAAC,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,CAAC;AAC7F,CAAC"}
|
package/dist/spawn-gate.d.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import { MemoryPressure } from "./memory-pressure.js";
|
|
1
2
|
import type { StormWindow } from "./storm-window.js";
|
|
2
3
|
export interface SpawnTask {
|
|
3
4
|
instanceName: string;
|
|
4
5
|
workingDirectory: string;
|
|
5
6
|
reason: "startup" | "wake" | "recovery" | "restart";
|
|
7
|
+
/** Outer fleet operations reserve a slot before their Daemon.trySpawn call. */
|
|
8
|
+
stage?: "lifecycle";
|
|
6
9
|
}
|
|
7
10
|
export interface SpawnGateOptions {
|
|
8
11
|
storm: StormWindow;
|
|
@@ -10,21 +13,30 @@ export interface SpawnGateOptions {
|
|
|
10
13
|
staggerMs: () => number;
|
|
11
14
|
random?: () => number;
|
|
12
15
|
lowMemoryBytes?: number;
|
|
16
|
+
memoryPressure?: MemoryPressure;
|
|
13
17
|
}
|
|
14
18
|
/** Persistent fleet-wide concurrency/workdir gate shared by startup and recovery. */
|
|
15
19
|
export declare class SpawnGate {
|
|
16
20
|
private readonly options;
|
|
17
21
|
private queue;
|
|
22
|
+
private nestedQueue;
|
|
18
23
|
private active;
|
|
24
|
+
private physicalActive;
|
|
19
25
|
private activeDirectories;
|
|
20
26
|
private activeInstances;
|
|
21
27
|
private lastStartedAt;
|
|
22
28
|
private timer;
|
|
29
|
+
private capacityWait;
|
|
23
30
|
private stopped;
|
|
31
|
+
private pumping;
|
|
32
|
+
private pressureRetryMs;
|
|
33
|
+
private pressureHeld;
|
|
24
34
|
private readonly random;
|
|
25
|
-
private readonly
|
|
35
|
+
private readonly memoryPressure;
|
|
36
|
+
private readonly onStormReady;
|
|
26
37
|
constructor(options: SpawnGateOptions);
|
|
27
38
|
run<T>(task: SpawnTask, operation: () => Promise<T>): Promise<T>;
|
|
28
39
|
shutdown(): void;
|
|
40
|
+
private wait;
|
|
29
41
|
private pump;
|
|
30
42
|
}
|
package/dist/spawn-gate.js
CHANGED
|
@@ -1,33 +1,40 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { MemoryPressure } from "./memory-pressure.js";
|
|
2
2
|
/** Persistent fleet-wide concurrency/workdir gate shared by startup and recovery. */
|
|
3
3
|
export class SpawnGate {
|
|
4
4
|
options;
|
|
5
5
|
queue = [];
|
|
6
|
+
nestedQueue = [];
|
|
6
7
|
active = 0;
|
|
8
|
+
physicalActive = 0;
|
|
7
9
|
activeDirectories = new Set();
|
|
8
10
|
activeInstances = new Set();
|
|
9
11
|
lastStartedAt = 0;
|
|
10
12
|
timer = null;
|
|
13
|
+
capacityWait = false;
|
|
11
14
|
stopped = false;
|
|
15
|
+
pumping = false;
|
|
16
|
+
pressureRetryMs = 5_000;
|
|
17
|
+
pressureHeld = false;
|
|
12
18
|
random;
|
|
13
|
-
|
|
19
|
+
memoryPressure;
|
|
20
|
+
onStormReady = () => this.pump();
|
|
14
21
|
constructor(options) {
|
|
15
22
|
this.options = options;
|
|
16
23
|
this.random = options.random ?? Math.random;
|
|
17
|
-
this.
|
|
18
|
-
options.storm.on("recovery_due",
|
|
19
|
-
options.storm.on("closed",
|
|
24
|
+
this.memoryPressure = options.memoryPressure ?? new MemoryPressure({ criticalBytes: options.lowMemoryBytes });
|
|
25
|
+
options.storm.on("recovery_due", this.onStormReady);
|
|
26
|
+
options.storm.on("closed", this.onStormReady);
|
|
20
27
|
}
|
|
21
28
|
run(task, operation) {
|
|
22
29
|
if (this.stopped)
|
|
23
30
|
return Promise.reject(new Error("Spawn gate is shutting down"));
|
|
24
31
|
// startInstancesWithConcurrency/restartSingleInstance gate the whole
|
|
25
32
|
// lifecycle operation, whose Daemon.trySpawn reaches this same choke point.
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
return operation();
|
|
33
|
+
// Keep the outer concurrency/workdir reservation, but recheck pressure
|
|
34
|
+
// before the nested physical spawn. Reacquiring its own slot would deadlock.
|
|
29
35
|
return new Promise((resolve, reject) => {
|
|
30
|
-
this.
|
|
36
|
+
const queue = this.activeInstances.has(task.instanceName) ? this.nestedQueue : this.queue;
|
|
37
|
+
queue.push({ task, run: operation, resolve, reject });
|
|
31
38
|
this.pump();
|
|
32
39
|
});
|
|
33
40
|
}
|
|
@@ -36,43 +43,105 @@ export class SpawnGate {
|
|
|
36
43
|
if (this.timer)
|
|
37
44
|
clearTimeout(this.timer);
|
|
38
45
|
this.timer = null;
|
|
46
|
+
this.options.storm.off("recovery_due", this.onStormReady);
|
|
47
|
+
this.options.storm.off("closed", this.onStormReady);
|
|
39
48
|
const err = new Error("Spawn gate shut down before task started");
|
|
40
49
|
for (const item of this.queue.splice(0))
|
|
41
50
|
item.reject(err);
|
|
51
|
+
for (const item of this.nestedQueue.splice(0))
|
|
52
|
+
item.reject(err);
|
|
53
|
+
}
|
|
54
|
+
wait(ms, capacityWait = false) {
|
|
55
|
+
this.capacityWait = capacityWait;
|
|
56
|
+
this.timer = setTimeout(() => { this.timer = null; this.capacityWait = false; this.pump(); }, ms);
|
|
57
|
+
this.timer.unref?.();
|
|
42
58
|
}
|
|
43
59
|
pump() {
|
|
44
|
-
if (this.stopped || this.timer || this.options.storm.isSpawnBlocked())
|
|
60
|
+
if (this.stopped || this.pumping || this.timer || this.options.storm.isSpawnBlocked())
|
|
45
61
|
return;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
this.
|
|
55
|
-
|
|
62
|
+
this.pumping = true;
|
|
63
|
+
try {
|
|
64
|
+
while (!this.stopped) {
|
|
65
|
+
const index = this.queue.findIndex(item => !this.activeDirectories.has(item.task.workingDirectory));
|
|
66
|
+
if (this.nestedQueue.length === 0 && index < 0)
|
|
67
|
+
return;
|
|
68
|
+
// Explicit zero preserves the existing deterministic test/embedding opt-out.
|
|
69
|
+
let pressure = this.options.lowMemoryBytes === 0 ? "normal" : this.memoryPressure.sample().level;
|
|
70
|
+
if (this.stopped)
|
|
71
|
+
return;
|
|
72
|
+
if (pressure === "critical") {
|
|
73
|
+
this.pressureHeld = true;
|
|
74
|
+
this.wait(this.pressureRetryMs);
|
|
75
|
+
this.pressureRetryMs = Math.min(60_000, this.pressureRetryMs * 2);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
// A recovered host still uses slow admission while a held task waits
|
|
79
|
+
// for capacity. Start its full recovery window only when it can run.
|
|
80
|
+
if (this.pressureHeld && pressure === "normal")
|
|
81
|
+
pressure = "elevated";
|
|
82
|
+
this.pressureRetryMs = 5_000;
|
|
83
|
+
const configured = Math.max(1, Math.min(20, this.options.concurrency()));
|
|
84
|
+
const limit = Math.min(pressure === "normal" ? configured : 1, this.options.storm.isActive() ? 4 : configured);
|
|
85
|
+
const nested = this.nestedQueue.length > 0;
|
|
86
|
+
const physical = (nested ? this.nestedQueue[0] : this.queue[index]).task.stage !== "lifecycle";
|
|
87
|
+
if (pressure !== "normal" && physical && this.physicalActive > 0) {
|
|
88
|
+
this.wait(5_000, true);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (!nested && this.active >= limit) {
|
|
92
|
+
// Recovery must be able to raise the limit without waiting for a
|
|
93
|
+
// long-running outer lifecycle callback to release its slot.
|
|
94
|
+
if (pressure !== "normal")
|
|
95
|
+
this.wait(5_000, true);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const stagger = Math.max(0, Math.min(30_000, this.options.staggerMs()), pressure === "normal" ? 0 : 5_000);
|
|
99
|
+
const jitter = this.options.storm.isActive() ? Math.floor(this.random() * 500) : 0;
|
|
100
|
+
// A healthy nested acquisition already owns its outer admission slot.
|
|
101
|
+
const delay = nested && pressure === "normal" ? 0 : Math.max(0, this.lastStartedAt + stagger + jitter - Date.now());
|
|
102
|
+
if (delay > 0) {
|
|
103
|
+
this.wait(delay);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const [item] = nested ? this.nestedQueue.splice(0, 1) : this.queue.splice(index, 1);
|
|
107
|
+
if (this.pressureHeld) {
|
|
108
|
+
this.memoryPressure.startRecoveryWindow();
|
|
109
|
+
this.pressureHeld = false;
|
|
110
|
+
}
|
|
111
|
+
if (!nested) {
|
|
112
|
+
this.active++;
|
|
113
|
+
this.activeDirectories.add(item.task.workingDirectory);
|
|
114
|
+
this.activeInstances.add(item.task.instanceName);
|
|
115
|
+
}
|
|
116
|
+
if (physical)
|
|
117
|
+
this.physicalActive++;
|
|
118
|
+
this.lastStartedAt = Date.now();
|
|
119
|
+
let operation;
|
|
120
|
+
try {
|
|
121
|
+
operation = item.run();
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
operation = Promise.reject(err);
|
|
125
|
+
}
|
|
126
|
+
void operation.then(item.resolve, item.reject).finally(() => {
|
|
127
|
+
if (physical)
|
|
128
|
+
this.physicalActive--;
|
|
129
|
+
if (!nested) {
|
|
130
|
+
this.active--;
|
|
131
|
+
this.activeDirectories.delete(item.task.workingDirectory);
|
|
132
|
+
this.activeInstances.delete(item.task.instanceName);
|
|
133
|
+
}
|
|
134
|
+
if (this.capacityWait && this.timer) {
|
|
135
|
+
clearTimeout(this.timer);
|
|
136
|
+
this.timer = null;
|
|
137
|
+
this.capacityWait = false;
|
|
138
|
+
}
|
|
139
|
+
this.pump();
|
|
140
|
+
});
|
|
56
141
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (wait > 0) {
|
|
61
|
-
this.timer = setTimeout(() => { this.timer = null; this.pump(); }, wait);
|
|
62
|
-
this.timer.unref?.();
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
const [item] = this.queue.splice(index, 1);
|
|
66
|
-
this.active++;
|
|
67
|
-
this.activeDirectories.add(item.task.workingDirectory);
|
|
68
|
-
this.activeInstances.add(item.task.instanceName);
|
|
69
|
-
this.lastStartedAt = Date.now();
|
|
70
|
-
void item.run().then(item.resolve, item.reject).finally(() => {
|
|
71
|
-
this.active--;
|
|
72
|
-
this.activeDirectories.delete(item.task.workingDirectory);
|
|
73
|
-
this.activeInstances.delete(item.task.instanceName);
|
|
74
|
-
this.pump();
|
|
75
|
-
});
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
this.pumping = false;
|
|
76
145
|
}
|
|
77
146
|
}
|
|
78
147
|
}
|
package/dist/spawn-gate.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"spawn-gate.js","sourceRoot":"","sources":["../src/spawn-gate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"spawn-gate.js","sourceRoot":"","sources":["../src/spawn-gate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AA2BtD,qFAAqF;AACrF,MAAM,OAAO,SAAS;IAkBS;IAjBrB,KAAK,GAAiB,EAAE,CAAC;IACzB,WAAW,GAAiB,EAAE,CAAC;IAC/B,MAAM,GAAG,CAAC,CAAC;IACX,cAAc,GAAG,CAAC,CAAC;IACnB,iBAAiB,GAAG,IAAI,GAAG,EAAU,CAAC;IACtC,eAAe,GAAG,IAAI,GAAG,EAAU,CAAC;IACpC,aAAa,GAAG,CAAC,CAAC;IAClB,KAAK,GAAyC,IAAI,CAAC;IACnD,YAAY,GAAG,KAAK,CAAC;IACrB,OAAO,GAAG,KAAK,CAAC;IAChB,OAAO,GAAG,KAAK,CAAC;IAChB,eAAe,GAAG,KAAK,CAAC;IACxB,YAAY,GAAG,KAAK,CAAC;IACZ,MAAM,CAAe;IACrB,cAAc,CAAiB;IAC/B,YAAY,GAAG,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;IAElD,YAA6B,OAAyB;QAAzB,YAAO,GAAP,OAAO,CAAkB;QACpD,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,IAAI,CAAC,MAAM,CAAC;QAC5C,IAAI,CAAC,cAAc,GAAG,OAAO,CAAC,cAAc,IAAI,IAAI,cAAc,CAAC,EAAE,aAAa,EAAE,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC;QAC9G,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,cAAc,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;QACpD,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,QAAQ,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;IAChD,CAAC;IAED,GAAG,CAAI,IAAe,EAAE,SAA2B;QACjD,IAAI,IAAI,CAAC,OAAO;YAAE,OAAO,OAAO,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,6BAA6B,CAAC,CAAC,CAAC;QAClF,qEAAqE;QACrE,4EAA4E;QAC5E,uEAAuE;QACvE,6EAA6E;QAC7E,OAAO,IAAI,OAAO,CAAI,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACxC,MAAM,KAAK,GAAG,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC;YAC1F,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAgB,CAAC,CAAC;YACpE,IAAI,CAAC,IAAI,EAAE,CAAC;QACd,CAAC,CAAC,CAAC;IACL,CAAC;IAED,QAAQ;QACN,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QACpB,IAAI,IAAI,CAAC,KAAK;YAAE,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACzC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QAClB,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,cAAc,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;QAC1D,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;QACpD,MAAM,GAAG,GAAG,IAAI,KAAK,CAAC,0CAA0C,CAAC,CAAC;QAClE,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;YAAE,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAC1D,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC;YAAE,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAClE,CAAC;IAEO,IAAI,CAAC,EAAU,EAAE,YAAY,GAAG,KAAK;QAC3C,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;QACjC,IAAI,CAAC,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC,IAAI,CAAC,YAAY,GAAG,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAClG,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,EAAE,CAAC;IACvB,CAAC;IAEO,IAAI;QACV,IAAI,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,cAAc,EAAE;YAAE,OAAO;QAC9F,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QACpB,IAAI,CAAC;YACH,OAAO,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;gBACrB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC,CAAC;gBACpG,IAAI,IAAI,CAAC,WAAW,CAAC,MAAM,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC;oBAAE,OAAO;gBACvD,6EAA6E;gBAC7E,IAAI,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,cAAc,KAAK,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC;gBACjG,IAAI,IAAI,CAAC,OAAO;oBAAE,OAAO;gBACzB,IAAI,QAAQ,KAAK,UAAU,EAAE,CAAC;oBAC5B,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC;oBACzB,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;oBAChC,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,eAAe,GAAG,CAAC,CAAC,CAAC;oBAClE,OAAO;gBACT,CAAC;gBACD,qEAAqE;gBACrE,qEAAqE;gBACrE,IAAI,IAAI,CAAC,YAAY,IAAI,QAAQ,KAAK,QAAQ;oBAAE,QAAQ,GAAG,UAAU,CAAC;gBACtE,IAAI,CAAC,eAAe,GAAG,KAAK,CAAC;gBAC7B,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;gBACzE,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC;gBAC/G,MAAM,MAAM,GAAG,IAAI,CAAC,WAAW,CAAC,MAAM,GAAG,CAAC,CAAC;gBAC3C,MAAM,QAAQ,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,KAAK,WAAW,CAAC;gBAC/F,IAAI,QAAQ,KAAK,QAAQ,IAAI,QAAQ,IAAI,IAAI,CAAC,cAAc,GAAG,CAAC,EAAE,CAAC;oBACjE,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;oBACvB,OAAO;gBACT,CAAC;gBACD,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,MAAM,IAAI,KAAK,EAAE,CAAC;oBACpC,iEAAiE;oBACjE,6DAA6D;oBAC7D,IAAI,QAAQ,KAAK,QAAQ;wBAAE,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;oBAClD,OAAO;gBACT,CAAC;gBACD,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,CAAC,EAAE,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;gBAC3G,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;gBACnF,sEAAsE;gBACtE,MAAM,KAAK,GAAG,MAAM,IAAI,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,aAAa,GAAG,OAAO,GAAG,MAAM,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;gBACpH,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;oBAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;oBAAC,OAAO;gBAAC,CAAC;gBAC5C,MAAM,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;gBACpF,IAAI,IAAI,CAAC,YAAY,EAAE,CAAC;oBACtB,IAAI,CAAC,cAAc,CAAC,mBAAmB,EAAE,CAAC;oBAC1C,IAAI,CAAC,YAAY,GAAG,KAAK,CAAC;gBAC5B,CAAC;gBACD,IAAI,CAAC,MAAM,EAAE,CAAC;oBACZ,IAAI,CAAC,MAAM,EAAE,CAAC;oBACd,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;oBACvD,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;gBACnD,CAAC;gBACD,IAAI,QAAQ;oBAAE,IAAI,CAAC,cAAc,EAAE,CAAC;gBACpC,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;gBAChC,IAAI,SAA2B,CAAC;gBAChC,IAAI,CAAC;oBAAC,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;gBAAC,CAAC;gBAAC,OAAO,GAAG,EAAE,CAAC;oBAAC,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;gBAAC,CAAC;gBAChF,KAAK,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE;oBAC1D,IAAI,QAAQ;wBAAE,IAAI,CAAC,cAAc,EAAE,CAAC;oBACpC,IAAI,CAAC,MAAM,EAAE,CAAC;wBACZ,IAAI,CAAC,MAAM,EAAE,CAAC;wBACd,IAAI,CAAC,iBAAiB,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;wBAC1D,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;oBACtD,CAAC;oBACD,IAAI,IAAI,CAAC,YAAY,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;wBACpC,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;wBACzB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;wBAClB,IAAI,CAAC,YAAY,GAAG,KAAK,CAAC;oBAC5B,CAAC;oBACD,IAAI,CAAC,IAAI,EAAE,CAAC;gBACd,CAAC,CAAC,CAAC;YACL,CAAC;QACH,CAAC;gBAAS,CAAC;YACT,IAAI,CAAC,OAAO,GAAG,KAAK,CAAC;QACvB,CAAC;IACH,CAAC;CACF"}
|
package/dist/storm-window.d.ts
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
2
|
export type StormPhase = "closed" | "backing_off" | "recovering";
|
|
3
|
+
/**
|
|
4
|
+
* What opened the window. `server`: the tmux server died or was replaced (every
|
|
5
|
+
* window is gone, spawning is held for a backoff). `window_loss`: the server is
|
|
6
|
+
* fine but several instances lost their windows within seconds of each other
|
|
7
|
+
* (#1127) — a host or tmux-level event, not N independent crashes; nothing is
|
|
8
|
+
* held, but the recoveries run at the storm rate and as ONE incident.
|
|
9
|
+
*/
|
|
10
|
+
export type StormKind = "server" | "window_loss";
|
|
3
11
|
export interface StormSnapshot {
|
|
4
12
|
phase: StormPhase;
|
|
13
|
+
kind: StormKind;
|
|
5
14
|
generation: number;
|
|
6
15
|
crashLevel: number;
|
|
7
16
|
crashCount: number;
|
|
@@ -19,6 +28,9 @@ export interface StormWindowOptions {
|
|
|
19
28
|
backoffsMs?: readonly number[];
|
|
20
29
|
stableResetMs?: number;
|
|
21
30
|
recoveryTimeoutMs?: number;
|
|
31
|
+
/** Distinct instances losing their window inside `windowLossWindowMs` that count as one event (#1127). */
|
|
32
|
+
windowLossThreshold?: number;
|
|
33
|
+
windowLossWindowMs?: number;
|
|
22
34
|
}
|
|
23
35
|
/**
|
|
24
36
|
* Fleet-wide tmux-server storm state.
|
|
@@ -35,7 +47,17 @@ export declare class StormWindow extends EventEmitter {
|
|
|
35
47
|
private readonly backoffsMs;
|
|
36
48
|
private readonly stableResetMs;
|
|
37
49
|
private readonly recoveryTimeoutMs;
|
|
50
|
+
private readonly windowLossThreshold;
|
|
51
|
+
/** The span in which `windowLossThreshold` distinct losses make one event. */
|
|
52
|
+
readonly windowLossWindowMs: number;
|
|
38
53
|
private phase;
|
|
54
|
+
private kind;
|
|
55
|
+
private windowLosses;
|
|
56
|
+
/**
|
|
57
|
+
* Instances whose last respawn finished outside a storm. A fresh loss withdraws
|
|
58
|
+
* the entry (noteWindowLoss), so it only ever speaks for the loss it followed.
|
|
59
|
+
*/
|
|
60
|
+
private windowLossRecovered;
|
|
39
61
|
private generation;
|
|
40
62
|
private crashLevel;
|
|
41
63
|
private crashCount;
|
|
@@ -69,6 +91,26 @@ export declare class StormWindow extends EventEmitter {
|
|
|
69
91
|
observeServerAlive(pid: number | null): boolean;
|
|
70
92
|
addAffected(name: string): void;
|
|
71
93
|
markRecovered(name: string): void;
|
|
94
|
+
/**
|
|
95
|
+
* An instance confirmed that its tmux window is gone while the server is
|
|
96
|
+
* alive. One such loss is an ordinary crash; several inside the window are one
|
|
97
|
+
* infrastructure event, and N independent respawn loops would stampede (#1127).
|
|
98
|
+
* Returns true when THIS call opens the window. Once a window is open, a loss
|
|
99
|
+
* simply joins it.
|
|
100
|
+
*/
|
|
101
|
+
noteWindowLoss(name: string): boolean;
|
|
102
|
+
private openWindowLossBurst;
|
|
103
|
+
/**
|
|
104
|
+
* A respawn finished outside a storm. Remember it, so that an instance that
|
|
105
|
+
* recovered before a burst was recognised is not counted as still down when
|
|
106
|
+
* the next loss opens one (inside an open window the caller marks recovery).
|
|
107
|
+
*/
|
|
108
|
+
noteWindowRecovered(name: string): void;
|
|
109
|
+
/**
|
|
110
|
+
* A live pane inside an open window-loss burst is recovery evidence on its own
|
|
111
|
+
* (a server storm is different: a pane alive then says nothing about the CLI).
|
|
112
|
+
*/
|
|
113
|
+
noteWindowAlive(name: string): void;
|
|
72
114
|
shouldSuppress(kind: string): boolean;
|
|
73
115
|
waitForSpawnAllowed(): Promise<void>;
|
|
74
116
|
waitForDeliveryAllowed(name: string): Promise<void>;
|