@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.
Files changed (107) hide show
  1. package/dist/backend/antigravity.js +3 -2
  2. package/dist/backend/antigravity.js.map +1 -1
  3. package/dist/backend/claude-code.d.ts +7 -0
  4. package/dist/backend/claude-code.js +107 -3
  5. package/dist/backend/claude-code.js.map +1 -1
  6. package/dist/backend/cli-env-probe-worker.js +2 -10
  7. package/dist/backend/cli-env-probe-worker.js.map +1 -1
  8. package/dist/backend/cli-env-probe.d.ts +14 -0
  9. package/dist/backend/cli-env-probe.js +13 -0
  10. package/dist/backend/cli-env-probe.js.map +1 -0
  11. package/dist/backend/codex-metadata.d.ts +14 -0
  12. package/dist/backend/codex-metadata.js +70 -0
  13. package/dist/backend/codex-metadata.js.map +1 -0
  14. package/dist/backend/codex.d.ts +1 -2
  15. package/dist/backend/codex.js +112 -65
  16. package/dist/backend/codex.js.map +1 -1
  17. package/dist/backend/effort-metadata.d.ts +32 -0
  18. package/dist/backend/effort-metadata.js +21 -0
  19. package/dist/backend/effort-metadata.js.map +1 -0
  20. package/dist/backend/grok.js +3 -2
  21. package/dist/backend/grok.js.map +1 -1
  22. package/dist/backend/kiro.d.ts +3 -0
  23. package/dist/backend/kiro.js +19 -4
  24. package/dist/backend/kiro.js.map +1 -1
  25. package/dist/backend/muse.js +3 -2
  26. package/dist/backend/muse.js.map +1 -1
  27. package/dist/backend/opencode.d.ts +31 -4
  28. package/dist/backend/opencode.js +66 -23
  29. package/dist/backend/opencode.js.map +1 -1
  30. package/dist/backend/types.d.ts +14 -1
  31. package/dist/backend/types.js.map +1 -1
  32. package/dist/channel/adapters/discord.js +10 -2
  33. package/dist/channel/adapters/discord.js.map +1 -1
  34. package/dist/cli.js +48 -6
  35. package/dist/cli.js.map +1 -1
  36. package/dist/config.js +1 -0
  37. package/dist/config.js.map +1 -1
  38. package/dist/daemon.d.ts +24 -0
  39. package/dist/daemon.js +109 -12
  40. package/dist/daemon.js.map +1 -1
  41. package/dist/doctor.d.ts +3 -1
  42. package/dist/doctor.js +11 -0
  43. package/dist/doctor.js.map +1 -1
  44. package/dist/fleet-context.d.ts +2 -2
  45. package/dist/fleet-control-audit.d.ts +83 -0
  46. package/dist/fleet-control-audit.js +195 -0
  47. package/dist/fleet-control-audit.js.map +1 -0
  48. package/dist/fleet-manager.d.ts +45 -35
  49. package/dist/fleet-manager.js +327 -384
  50. package/dist/fleet-manager.js.map +1 -1
  51. package/dist/full-restart.js +2 -1
  52. package/dist/full-restart.js.map +1 -1
  53. package/dist/general-knowledge/skills/fleet-config/SKILL.md +3 -2
  54. package/dist/host-memory.d.ts +16 -0
  55. package/dist/host-memory.js +32 -0
  56. package/dist/host-memory.js.map +1 -0
  57. package/dist/instance-lifecycle.js +4 -1
  58. package/dist/instance-lifecycle.js.map +1 -1
  59. package/dist/locale.js +62 -16
  60. package/dist/locale.js.map +1 -1
  61. package/dist/log-tail.d.ts +34 -0
  62. package/dist/log-tail.js +66 -0
  63. package/dist/log-tail.js.map +1 -0
  64. package/dist/logger.d.ts +15 -0
  65. package/dist/logger.js +89 -1
  66. package/dist/logger.js.map +1 -1
  67. package/dist/login-controller.d.ts +1 -3
  68. package/dist/login-controller.js +2 -6
  69. package/dist/login-controller.js.map +1 -1
  70. package/dist/login-flows.d.ts +0 -8
  71. package/dist/login-flows.js +1 -11
  72. package/dist/login-flows.js.map +1 -1
  73. package/dist/login-manager.d.ts +5 -20
  74. package/dist/login-manager.js +0 -53
  75. package/dist/login-manager.js.map +1 -1
  76. package/dist/login-window-lock.d.ts +1 -1
  77. package/dist/login-window-lock.js +1 -1
  78. package/dist/memory-pressure.d.ts +41 -0
  79. package/dist/memory-pressure.js +117 -0
  80. package/dist/memory-pressure.js.map +1 -0
  81. package/dist/probe-worker-pool.d.ts +28 -0
  82. package/dist/probe-worker-pool.js +129 -0
  83. package/dist/probe-worker-pool.js.map +1 -0
  84. package/dist/resource-report.d.ts +31 -0
  85. package/dist/resource-report.js +209 -0
  86. package/dist/resource-report.js.map +1 -0
  87. package/dist/rollout-index.d.ts +66 -0
  88. package/dist/rollout-index.js +124 -0
  89. package/dist/rollout-index.js.map +1 -0
  90. package/dist/slash-authz.d.ts +50 -0
  91. package/dist/slash-authz.js +19 -0
  92. package/dist/slash-authz.js.map +1 -0
  93. package/dist/spawn-gate.d.ts +13 -1
  94. package/dist/spawn-gate.js +108 -39
  95. package/dist/spawn-gate.js.map +1 -1
  96. package/dist/storm-window.d.ts +42 -0
  97. package/dist/storm-window.js +84 -1
  98. package/dist/storm-window.js.map +1 -1
  99. package/dist/topic-commands.d.ts +29 -2
  100. package/dist/topic-commands.js +77 -21
  101. package/dist/topic-commands.js.map +1 -1
  102. package/dist/transcript-sources.d.ts +21 -4
  103. package/dist/transcript-sources.js +55 -41
  104. package/dist/transcript-sources.js.map +1 -1
  105. package/dist/types.d.ts +5 -1
  106. package/dist/ui/settings.html +14 -13
  107. 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":"AAAA,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAChD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AA8BjC,MAAM,OAAO,GAAiB,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;AAuBjG,MAAM,CAAC,MAAM,sBAAsB,GAAG,KAAK,CAAC;AAC5C,sGAAsG;AACtG,MAAM,UAAU,GAAG,KAAK,CAAC;AAEzB,MAAM,OAAO,YAAY;IAaM;IAZZ,KAAK,CAAS;IACd,OAAO,CAAS;IAChB,aAAa,CAAS;IACtB,QAAQ,CAAS;IACjB,GAAG,CAAe;IAClB,GAAG,CAAe;IAC3B,IAAI,GAAG,IAAI,GAAG,EAAoB,CAAC;IACnC,KAAK,GAAG,IAAI,GAAG,EAAuB,CAAC;IACvC,OAAO,GAAkB,EAAE,CAAC;IAC5B,OAAO,GAAG,MAAM,CAAC,iBAAiB,CAAC;IACnC,MAAM,GAAG,MAAM,CAAC,iBAAiB,CAAC;IAE1C,YAA6B,IAAY,EAAE,UAA+B,EAAE;QAA/C,SAAI,GAAJ,IAAI,CAAQ;QACvC,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,sBAAsB,CAAC;QACrD,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,EAAE,GAAG,MAAM,CAAC;QAC9C,IAAI,CAAC,aAAa,GAAG,OAAO,CAAC,aAAa,IAAI,MAAM,CAAC;QACrD,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,CAAC,CAAC;QACtC,IAAI,CAAC,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC;QAClC,IAAI,CAAC,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC;IACrC,CAAC;IAED,0FAA0F;IAC1F,IAAI,CAAC,KAAK,GAAG,KAAK;QAChB,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,IAAI,CAAC,KAAK,IAAI,GAAG,GAAG,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,KAAK;YAAE,OAAO,IAAI,CAAC,OAAO,CAAC;QACnE,MAAM,IAAI,GAAG,KAAK,IAAI,GAAG,GAAG,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,aAAa,CAAC;QAC9D,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QACxB,OAAO,IAAI,CAAC,OAAO,CAAC;IACtB,CAAC;IAEO,OAAO,CAAC,GAAW,EAAE,IAAa;QACxC,MAAM,IAAI,GAAG,IAAI,GAAG,EAAoB,CAAC;QACzC,MAAM,KAAK,GAAG,IAAI,GAAG,EAAuB,CAAC;QAC7C,MAAM,IAAI,GAAG,CAAC,GAAW,EAAE,KAAa,EAAQ,EAAE;YAChD,IAAI,OAAe,CAAC;YACpB,IAAI,CAAC;gBAAC,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC;YAAC,CAAC;YAAC,MAAM,CAAC;gBAAC,OAAO;YAAC,CAAC;YAC/D,IAAI,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YAC/B,gGAAgG;YAChG,gGAAgG;YAChG,oGAAoG;YACpG,mGAAmG;YACnG,IAAI,IAAI,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,OAAO,KAAK,OAAO,IAAI,KAAK,CAAC,WAAW,EAAE,CAAC;gBACrE,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,CAAC,CAAC;YACjD,CAAC;YACD,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;YACrB,KAAK,MAAM,OAAO,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;gBACrC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;gBACtC,IAAI,CAAC,IAAI,IAAI,KAAK,IAAI,GAAG,GAAG,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC;oBAAC,KAAK,CAAC,GAAG,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;oBAAC,SAAS;gBAAC,CAAC;gBAClG,IAAI,CAAC;oBACH,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;oBAClC,KAAK,CAAC,GAAG,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,CAAC,OAAO,EAAE,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC;gBAC5E,CAAC;gBAAC,MAAM,CAAC,CAAC,yBAAyB,CAAC,CAAC;YACvC,CAAC;YACD,IAAI,KAAK,GAAG,IAAI,CAAC,QAAQ;gBAAE,KAAK,MAAM,GAAG,IAAI,KAAK,CAAC,OAAO;oBAAE,IAAI,CAAC,GAAG,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC;QACnF,CAAC,CAAC;QACF,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;QACnB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,OAAO,GAAG,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC;QACzE,IAAI,CAAC,OAAO,GAAG,GAAG,CAAC;QACnB,IAAI,IAAI;YAAE,IAAI,CAAC,MAAM,GAAG,GAAG,CAAC;IAC9B,CAAC;IAEO,OAAO,CAAC,GAAW,EAAE,OAAe,EAAE,KAAa,EAAE,GAAW;QACtE,MAAM,KAAK,GAAa,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,WAAW,EAAE,GAAG,GAAG,OAAO,GAAG,UAAU,EAAE,CAAC;QACxG,IAAI,KAAe,CAAC;QACpB,IAAI,CAAC;YAAC,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAAC,CAAC;QAAC,MAAM,CAAC;YAAC,OAAO,KAAK,CAAC;QAAC,CAAC;QAC9D,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;YAC7B,IAAI,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;gBAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBAAC,SAAS;YAAC,CAAC;YACpG,IAAI,KAAK,IAAI,IAAI,CAAC,QAAQ;gBAAE,SAAS;YACrC,IAAI,CAAC;gBAAC,IAAI,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE;oBAAE,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAAC,CAAC;YAAC,MAAM,CAAC,CAAC,yBAAyB,CAAC,CAAC;QAC9G,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;CACF;AAED,MAAM,MAAM,GAAG,IAAI,GAAG,EAAwB,CAAC;AAC/C,IAAI,WAAW,GAAG,sBAAsB,CAAC;AAEzC,wDAAwD;AACxD,MAAM,UAAU,kBAAkB,CAAC,IAAY;IAC7C,IAAI,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC7B,IAAI,CAAC,KAAK,EAAE,CAAC;QAAC,KAAK,GAAG,IAAI,YAAY,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC,CAAC;QAAC,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IAAC,CAAC;IAChG,OAAO,KAAK,CAAC;AACf,CAAC;AAED,uIAAuI;AACvI,MAAM,UAAU,iCAAiC,CAAC,KAAK,GAAG,sBAAsB;IAC9E,MAAM,CAAC,KAAK,EAAE,CAAC;IACf,WAAW,GAAG,KAAK,CAAC;AACtB,CAAC"}
@@ -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"}
@@ -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 lowMemoryBytes;
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
  }
@@ -1,33 +1,40 @@
1
- import { freemem } from "node:os";
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
- lowMemoryBytes;
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.lowMemoryBytes = options.lowMemoryBytes ?? 300 * 1024 * 1024;
18
- options.storm.on("recovery_due", () => this.pump());
19
- options.storm.on("closed", () => this.pump());
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
- // Treat that nested acquisition as re-entrant for the same instance.
27
- if (this.activeInstances.has(task.instanceName))
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.queue.push({ task, run: operation, resolve, reject });
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
- const configured = Math.max(1, Math.min(20, this.options.concurrency()));
47
- const limit = this.options.storm.isActive() ? Math.min(4, configured) : configured;
48
- while (this.active < limit) {
49
- const index = this.queue.findIndex(item => !this.activeDirectories.has(item.task.workingDirectory));
50
- if (index < 0)
51
- return;
52
- if (this.active > 0 && freemem() < this.lowMemoryBytes) {
53
- this.timer = setTimeout(() => { this.timer = null; this.pump(); }, 5_000);
54
- this.timer.unref?.();
55
- return;
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
- const stagger = Math.max(0, Math.min(30_000, this.options.staggerMs()));
58
- const jitter = this.options.storm.isActive() ? Math.floor(this.random() * 500) : 0;
59
- const wait = Math.max(0, this.lastStartedAt + stagger + jitter - Date.now());
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
  }
@@ -1 +1 @@
1
- {"version":3,"file":"spawn-gate.js","sourceRoot":"","sources":["../src/spawn-gate.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"spawn-gate.js","sourceRoot":"","sources":["../src/spawn-gate.ts"],"names":[],"mappings":"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"}
@@ -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>;