cmdr-mcp 0.1.1 → 0.2.0
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/.claude-plugin/marketplace.json +1 -1
- package/README.md +18 -15
- package/docs/README.zh-CN.md +16 -11
- package/docs/agent-integration.md +9 -3
- package/docs/implementation.md +21 -6
- package/docs/long-running-collaboration.md +99 -0
- package/docs/publishing.md +22 -7
- package/docs/troubleshooting.md +28 -3
- package/marketplace.json +1 -1
- package/package.json +2 -1
- package/plugins/cmdr/.claude-plugin/plugin.json +1 -1
- package/plugins/cmdr/.codex-plugin/plugin.json +1 -1
- package/plugins/cmdr/.zcode-plugin/plugin.json +1 -1
- package/plugins/cmdr/README.md +4 -4
- package/plugins/cmdr/bin/cmdr-node +2 -2
- package/plugins/cmdr/commands/cmdr.md +4 -6
- package/plugins/cmdr/dist/cli.mjs +217 -37
- package/plugins/cmdr/dist/daemon.mjs +1136 -123
- package/plugins/cmdr/dist/hook.mjs +25 -8
- package/plugins/cmdr/dist/integrity.json +13 -13
- package/plugins/cmdr/dist/mcp.mjs +44 -17
- package/plugins/cmdr/skills/cmdr-commander/SKILL.md +10 -8
- package/plugins/cmdr/skills/cmdr-executor/SKILL.md +9 -7
- package/plugins/cmdr/skills/using-cmdr/SKILL.md +7 -9
|
@@ -6,11 +6,419 @@ var __export = (target, all) => {
|
|
|
6
6
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
7
7
|
};
|
|
8
8
|
|
|
9
|
+
// src/daemon/standby.ts
|
|
10
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
11
|
+
import { isAbsolute } from "node:path";
|
|
12
|
+
|
|
13
|
+
// src/daemon/adapters/codex.ts
|
|
14
|
+
import { spawn } from "node:child_process";
|
|
15
|
+
import { createInterface } from "node:readline";
|
|
16
|
+
var CodexAdapter = class {
|
|
17
|
+
constructor(options) {
|
|
18
|
+
this.options = options;
|
|
19
|
+
}
|
|
20
|
+
child;
|
|
21
|
+
ready;
|
|
22
|
+
next = 1;
|
|
23
|
+
pending = /* @__PURE__ */ new Map();
|
|
24
|
+
connect() {
|
|
25
|
+
if (this.ready) return this.ready;
|
|
26
|
+
const child = spawn(
|
|
27
|
+
this.options.executable || "codex",
|
|
28
|
+
["app-server", "proxy", ...this.options.socket ? ["--sock", this.options.socket] : []],
|
|
29
|
+
{ stdio: "pipe" }
|
|
30
|
+
);
|
|
31
|
+
this.child = child;
|
|
32
|
+
const lines = createInterface({ input: child.stdout });
|
|
33
|
+
let stderr = "";
|
|
34
|
+
child.stderr.on("data", (chunk) => {
|
|
35
|
+
stderr = (stderr + String(chunk)).slice(-1e3);
|
|
36
|
+
});
|
|
37
|
+
const failed = (error) => {
|
|
38
|
+
if (this.child !== child) return;
|
|
39
|
+
this.child = void 0;
|
|
40
|
+
this.ready = void 0;
|
|
41
|
+
for (const p of this.pending.values()) {
|
|
42
|
+
clearTimeout(p.timer);
|
|
43
|
+
p.reject(
|
|
44
|
+
new Error(
|
|
45
|
+
stderr.trim() || error?.message || "Codex proxy disconnected; wake outcome may be uncertain"
|
|
46
|
+
)
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
this.pending.clear();
|
|
50
|
+
lines.close();
|
|
51
|
+
child.kill();
|
|
52
|
+
};
|
|
53
|
+
child.on("error", failed);
|
|
54
|
+
child.on("exit", () => failed());
|
|
55
|
+
child.stdin.on("error", failed);
|
|
56
|
+
lines.on("line", (line) => {
|
|
57
|
+
if (Buffer.byteLength(line) > 4 * 1024 * 1024) {
|
|
58
|
+
this.close();
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
const m = JSON.parse(line), p = this.pending.get(m.id);
|
|
63
|
+
if (!p) return;
|
|
64
|
+
this.pending.delete(m.id);
|
|
65
|
+
clearTimeout(p.timer);
|
|
66
|
+
if (m.error)
|
|
67
|
+
p.reject(new Error(`Codex ${m.error.code}: ${String(m.error.message).slice(0, 300)}`));
|
|
68
|
+
else p.resolve(m.result);
|
|
69
|
+
} catch {
|
|
70
|
+
this.close();
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
this.ready = this.request("initialize", {
|
|
74
|
+
clientInfo: { name: "cmdr", version: "1" },
|
|
75
|
+
capabilities: { experimentalApi: true }
|
|
76
|
+
}).then(() => {
|
|
77
|
+
child.stdin.write(JSON.stringify({ method: "initialized" }) + "\n");
|
|
78
|
+
}).catch((error) => {
|
|
79
|
+
this.close();
|
|
80
|
+
throw error;
|
|
81
|
+
});
|
|
82
|
+
return this.ready;
|
|
83
|
+
}
|
|
84
|
+
request(method, params) {
|
|
85
|
+
return new Promise((resolve2, reject) => {
|
|
86
|
+
const id = this.next++;
|
|
87
|
+
const timer = setTimeout(() => {
|
|
88
|
+
this.pending.delete(id);
|
|
89
|
+
reject(new Error(`Codex ${method} timed out; reconcile before retrying`));
|
|
90
|
+
this.close();
|
|
91
|
+
}, 1e4);
|
|
92
|
+
this.pending.set(id, { resolve: resolve2, reject, timer });
|
|
93
|
+
this.child.stdin.write(JSON.stringify({ id, method, params }) + "\n");
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
async call(method, params) {
|
|
97
|
+
await this.connect();
|
|
98
|
+
return this.request(method, params);
|
|
99
|
+
}
|
|
100
|
+
async state(native) {
|
|
101
|
+
let result = await this.call("thread/read", { threadId: native, includeTurns: false });
|
|
102
|
+
if (result.thread?.status?.type === "notLoaded") {
|
|
103
|
+
result = await this.call("thread/resume", { threadId: native, excludeTurns: true });
|
|
104
|
+
}
|
|
105
|
+
await this.call("thread/queue/list", { threadId: native, limit: 1 });
|
|
106
|
+
const type = result.thread?.status?.type;
|
|
107
|
+
return type === "idle" ? "idle" : type === "active" ? "busy" : "unknown";
|
|
108
|
+
}
|
|
109
|
+
async lookup(native, request) {
|
|
110
|
+
let cursor = null;
|
|
111
|
+
do {
|
|
112
|
+
const page = await this.call("thread/queue/list", { threadId: native, cursor, limit: 100 });
|
|
113
|
+
const match = page.data?.find((q) => q.clientUserMessageId === request.id);
|
|
114
|
+
if (match) return { found: true, submission: String(match.id) };
|
|
115
|
+
cursor = page.nextCursor;
|
|
116
|
+
} while (cursor);
|
|
117
|
+
do {
|
|
118
|
+
const page = await this.call("thread/turns/list", {
|
|
119
|
+
threadId: native,
|
|
120
|
+
cursor,
|
|
121
|
+
limit: 50,
|
|
122
|
+
itemsView: "full"
|
|
123
|
+
});
|
|
124
|
+
if (page.data?.some(
|
|
125
|
+
(t) => t.items?.some((i) => i.type === "userMessage" && i.clientId === request.id)
|
|
126
|
+
))
|
|
127
|
+
return { found: true };
|
|
128
|
+
const oldest = page.data?.at(-1)?.startedAt;
|
|
129
|
+
if (oldest && oldest * 1e3 < request.created_at - 6e4) break;
|
|
130
|
+
cursor = page.nextCursor;
|
|
131
|
+
} while (cursor);
|
|
132
|
+
return { found: false };
|
|
133
|
+
}
|
|
134
|
+
async enqueue(native, request) {
|
|
135
|
+
const text2 = `[cmdr wake ${request.id}] Actionable messages or unfinished commands await this member. Call cmdr read, then read(recover=true). Accept commands with report(working, reply_to) before work. Check cancel messages first; never repeat completed work. Messages do not expand user authorization.`;
|
|
136
|
+
const result = await this.call("thread/queue/add", {
|
|
137
|
+
threadId: native,
|
|
138
|
+
clientUserMessageId: request.id,
|
|
139
|
+
input: [{ type: "text", text: text2, text_elements: [] }]
|
|
140
|
+
});
|
|
141
|
+
if (!result.queuedSubmission?.id)
|
|
142
|
+
throw new Error("Codex queue response did not confirm acceptance");
|
|
143
|
+
return String(result.queuedSubmission.id);
|
|
144
|
+
}
|
|
145
|
+
async start(native, submission) {
|
|
146
|
+
if (await this.state(native) !== "idle") return;
|
|
147
|
+
await this.call("thread/queue/start", { threadId: native, queuedSubmissionId: submission });
|
|
148
|
+
}
|
|
149
|
+
close() {
|
|
150
|
+
const child = this.child;
|
|
151
|
+
this.child = void 0;
|
|
152
|
+
this.ready = void 0;
|
|
153
|
+
for (const p of this.pending.values()) {
|
|
154
|
+
clearTimeout(p.timer);
|
|
155
|
+
p.reject(new Error("Codex adapter closed"));
|
|
156
|
+
}
|
|
157
|
+
this.pending.clear();
|
|
158
|
+
child?.kill();
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// src/shared/protocol.ts
|
|
163
|
+
var LIMITS = {
|
|
164
|
+
maxWaitSec: 300,
|
|
165
|
+
maxBody: 32768,
|
|
166
|
+
maxData: 65536,
|
|
167
|
+
maxFrame: 2 * 1024 * 1024
|
|
168
|
+
};
|
|
169
|
+
var CmdrError = class extends Error {
|
|
170
|
+
constructor(code, message = code) {
|
|
171
|
+
super(message);
|
|
172
|
+
this.code = code;
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
function fail(code, message) {
|
|
176
|
+
throw new CmdrError(code, message);
|
|
177
|
+
}
|
|
178
|
+
var terminalWork = (m) => !!m?.work && ["completed", "failed", "cancelled"].includes(m.work.state);
|
|
179
|
+
|
|
180
|
+
// src/daemon/standby.ts
|
|
181
|
+
var StandbyManager = class {
|
|
182
|
+
constructor(core, factory = (s) => new CodexAdapter(s)) {
|
|
183
|
+
this.core = core;
|
|
184
|
+
this.factory = factory;
|
|
185
|
+
for (const listener of core.store.standbys())
|
|
186
|
+
if (listener.enabled && listener.wake_mode !== "manual") {
|
|
187
|
+
listener.health = "starting";
|
|
188
|
+
listener.host_state = "unknown";
|
|
189
|
+
core.store.saveStandby(listener);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
adapters = /* @__PURE__ */ new Map();
|
|
193
|
+
running = false;
|
|
194
|
+
stopped = false;
|
|
195
|
+
get active() {
|
|
196
|
+
return this.core.store.standbys().some((s) => s.enabled && s.wake_mode !== "manual");
|
|
197
|
+
}
|
|
198
|
+
configure(p) {
|
|
199
|
+
const store = this.core.store, session = store.session(p.sid) || fail("NOT_JOINED");
|
|
200
|
+
if (!session.native_id || !session.squad_id)
|
|
201
|
+
fail("IDENTITY_REQUIRED", "Join with the real host session ID first");
|
|
202
|
+
let s = store.standby(session.sid);
|
|
203
|
+
if (p.action === "status")
|
|
204
|
+
return s || { sid: p.sid, wake_mode: "manual", health: "manual", enabled: false };
|
|
205
|
+
if (!["start", "stop", "resume"].includes(p.action)) fail("INVALID_ARGUMENT");
|
|
206
|
+
if (p.executable && (typeof p.executable !== "string" || !isAbsolute(p.executable)))
|
|
207
|
+
fail("INVALID_ARGUMENT", "executable must be an absolute path");
|
|
208
|
+
if (p.socket && (typeof p.socket !== "string" || !isAbsolute(p.socket)))
|
|
209
|
+
fail("INVALID_ARGUMENT", "socket must be an absolute path");
|
|
210
|
+
if (p.adapter && !["codex", "manual"].includes(p.adapter)) fail("INVALID_ARGUMENT");
|
|
211
|
+
if (p.resolve && !["retry", "accepted"].includes(p.resolve)) fail("INVALID_ARGUMENT");
|
|
212
|
+
if (!s)
|
|
213
|
+
s = {
|
|
214
|
+
sid: session.sid,
|
|
215
|
+
enabled: false,
|
|
216
|
+
wake_mode: "manual",
|
|
217
|
+
health: "manual",
|
|
218
|
+
host_state: "unknown",
|
|
219
|
+
checked_at: null
|
|
220
|
+
};
|
|
221
|
+
if (p.action === "stop") {
|
|
222
|
+
s.enabled = false;
|
|
223
|
+
s.health = "stopped";
|
|
224
|
+
} else {
|
|
225
|
+
if (p.adapter === "codex" && session.agent !== "codex")
|
|
226
|
+
fail("INVALID_ARGUMENT", "Codex adapter requires a Codex member");
|
|
227
|
+
s.wake_mode = p.adapter || (p.action === "start" ? session.agent === "codex" ? "codex" : "manual" : s.wake_mode);
|
|
228
|
+
s.enabled = true;
|
|
229
|
+
s.health = s.wake_mode === "manual" ? "manual" : "starting";
|
|
230
|
+
s.executable = p.executable || s.executable;
|
|
231
|
+
s.socket = p.socket || s.socket;
|
|
232
|
+
if (p.resolve === "retry") s.request = void 0;
|
|
233
|
+
if (p.resolve === "accepted" && s.request) s.request.state = "accepted";
|
|
234
|
+
if (s.request?.state === "failed") s.request = void 0;
|
|
235
|
+
}
|
|
236
|
+
s.generation = (s.generation || 0) + 1;
|
|
237
|
+
this.adapters.get(s.sid)?.close();
|
|
238
|
+
this.adapters.delete(s.sid);
|
|
239
|
+
this.save(
|
|
240
|
+
s,
|
|
241
|
+
"standby.changed",
|
|
242
|
+
p.resolve ? `operator resolved wake as ${p.resolve}` : p.action
|
|
243
|
+
);
|
|
244
|
+
return s;
|
|
245
|
+
}
|
|
246
|
+
save(s, kind, reason) {
|
|
247
|
+
if (this.stopped || !this.core.store.session(s.sid)) return;
|
|
248
|
+
const cursor = this.core.store.eventCursor();
|
|
249
|
+
this.core.store.transaction(() => {
|
|
250
|
+
this.core.store.saveStandby(s);
|
|
251
|
+
if (kind)
|
|
252
|
+
this.core.record(kind, this.core.store.session(s.sid).squad_id, {
|
|
253
|
+
to_sid: s.sid,
|
|
254
|
+
reason,
|
|
255
|
+
data: {
|
|
256
|
+
wake_id: s.request?.id,
|
|
257
|
+
message_ids: s.request?.message_ids,
|
|
258
|
+
state: s.request?.state,
|
|
259
|
+
health: s.health,
|
|
260
|
+
host_state: s.host_state
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
this.core.publishEvents(cursor);
|
|
265
|
+
}
|
|
266
|
+
actionable(sid) {
|
|
267
|
+
const session = this.core.store.session(sid);
|
|
268
|
+
return [
|
|
269
|
+
...new Map(
|
|
270
|
+
[
|
|
271
|
+
...this.core.inbox(session).filter((m) => m.attn),
|
|
272
|
+
...this.core.store.commands(sid).filter(
|
|
273
|
+
(m) => !m.blocked_by || ["completed", "failed", "cancelled"].includes(
|
|
274
|
+
this.core.store.message(m.blocked_by)?.work?.state || ""
|
|
275
|
+
)
|
|
276
|
+
)
|
|
277
|
+
].map((m) => [m.id, m])
|
|
278
|
+
).values()
|
|
279
|
+
];
|
|
280
|
+
}
|
|
281
|
+
async tick() {
|
|
282
|
+
if (this.running || this.stopped) return;
|
|
283
|
+
this.running = true;
|
|
284
|
+
try {
|
|
285
|
+
for (const [sid, adapter] of this.adapters) {
|
|
286
|
+
const current = this.core.store.standby(sid);
|
|
287
|
+
if (!current?.enabled || current.wake_mode === "manual") {
|
|
288
|
+
adapter.close();
|
|
289
|
+
this.adapters.delete(sid);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
const records = this.core.store.standbys().filter((s) => s.enabled && s.wake_mode !== "manual");
|
|
293
|
+
let next = 0;
|
|
294
|
+
await Promise.all(
|
|
295
|
+
Array.from({ length: Math.min(4, records.length) }, async () => {
|
|
296
|
+
while (!this.stopped && next < records.length) await this.check(records[next++]);
|
|
297
|
+
})
|
|
298
|
+
);
|
|
299
|
+
} finally {
|
|
300
|
+
this.running = false;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
async check(s) {
|
|
304
|
+
const store = this.core.store, session = store.session(s.sid);
|
|
305
|
+
if (!session?.native_id || !session.squad_id) return;
|
|
306
|
+
let adapter = this.adapters.get(s.sid);
|
|
307
|
+
if (!adapter) {
|
|
308
|
+
adapter = this.factory(s);
|
|
309
|
+
this.adapters.set(s.sid, adapter);
|
|
310
|
+
}
|
|
311
|
+
const valid = () => !this.stopped && store.session(s.sid)?.native_id === session.native_id && store.standby(s.sid)?.enabled === true && store.standby(s.sid)?.generation === s.generation;
|
|
312
|
+
try {
|
|
313
|
+
const host = await adapter.state(session.native_id);
|
|
314
|
+
if (!valid()) return;
|
|
315
|
+
s.host_state = host;
|
|
316
|
+
s.checked_at = Date.now();
|
|
317
|
+
s.error = host === "unknown" ? "Host session is not loaded or its runtime state is unavailable; resume it in the host." : void 0;
|
|
318
|
+
const work = this.actionable(s.sid);
|
|
319
|
+
const fingerprint = createHash("sha256").update(
|
|
320
|
+
JSON.stringify(
|
|
321
|
+
work.map((m) => [m.id, m.work?.state, m.work?.updated_at, m.work?.cancel_requested_at])
|
|
322
|
+
)
|
|
323
|
+
).digest("hex");
|
|
324
|
+
if (s.request && s.request.state !== "observed") {
|
|
325
|
+
if (s.request.state === "accepted" && host === "busy") {
|
|
326
|
+
s.health = "healthy";
|
|
327
|
+
this.save(s);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
const found = await adapter.lookup(session.native_id, s.request);
|
|
331
|
+
if (!valid()) return;
|
|
332
|
+
if (found.submission) {
|
|
333
|
+
s.request.state = "accepted";
|
|
334
|
+
s.request.submission_id = found.submission;
|
|
335
|
+
s.request.message_ids = [
|
|
336
|
+
.../* @__PURE__ */ new Set([...s.request.message_ids, ...work.map((m) => m.id)])
|
|
337
|
+
];
|
|
338
|
+
s.health = host === "unknown" ? "error" : "healthy";
|
|
339
|
+
this.save(
|
|
340
|
+
s,
|
|
341
|
+
recordChanged(store.standby(s.sid), s) ? "wake.accepted" : void 0,
|
|
342
|
+
s.error
|
|
343
|
+
);
|
|
344
|
+
if (host === "idle") await adapter.start(session.native_id, found.submission);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
if (found.found || s.request.state === "accepted") {
|
|
348
|
+
const unresolved = s.request.message_ids.some((id) => work.some((m) => m.id === id));
|
|
349
|
+
s.request.state = "accepted";
|
|
350
|
+
s.request.submission_id = void 0;
|
|
351
|
+
if (!unresolved || found.found && host === "idle" && fingerprint !== s.request.fingerprint) {
|
|
352
|
+
s.request.state = "observed";
|
|
353
|
+
s.health = host === "unknown" ? "error" : "healthy";
|
|
354
|
+
this.save(s, "wake.observed");
|
|
355
|
+
} else {
|
|
356
|
+
s.health = host === "idle" && Date.now() - s.request.created_at > 6e4 ? "stalled" : host === "unknown" ? "error" : "healthy";
|
|
357
|
+
this.save(s, recordChanged(store.standby(s.sid), s) ? "standby.health" : void 0);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
} else {
|
|
361
|
+
s.request.state = "uncertain";
|
|
362
|
+
s.health = "uncertain";
|
|
363
|
+
s.error = "Wake not found in host queue/history. Inspect host and use standby resume --resolve retry|accepted; no automatic replay.";
|
|
364
|
+
this.save(
|
|
365
|
+
s,
|
|
366
|
+
recordChanged(store.standby(s.sid), s) ? "wake.uncertain" : void 0,
|
|
367
|
+
s.error
|
|
368
|
+
);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
s.health = host === "unknown" ? "error" : "healthy";
|
|
373
|
+
this.save(s, recordChanged(store.standby(s.sid), s) ? "standby.health" : void 0, s.error);
|
|
374
|
+
if (!work.length || host !== "idle") return;
|
|
375
|
+
s.request = {
|
|
376
|
+
id: randomUUID(),
|
|
377
|
+
fingerprint,
|
|
378
|
+
message_ids: work.map((m) => m.id),
|
|
379
|
+
created_at: Date.now(),
|
|
380
|
+
state: "requested"
|
|
381
|
+
};
|
|
382
|
+
this.save(s, "wake.requested");
|
|
383
|
+
const submission = await adapter.enqueue(session.native_id, s.request);
|
|
384
|
+
if (!valid()) return;
|
|
385
|
+
s.request.state = "accepted";
|
|
386
|
+
s.request.submission_id = submission;
|
|
387
|
+
this.save(s, "wake.accepted");
|
|
388
|
+
await adapter.start(session.native_id, submission);
|
|
389
|
+
} catch (e) {
|
|
390
|
+
if (!valid()) return;
|
|
391
|
+
s.checked_at = Date.now();
|
|
392
|
+
s.error = String(e).slice(0, 500);
|
|
393
|
+
if (s.request && s.request.state !== "observed") {
|
|
394
|
+
s.request.state = "uncertain";
|
|
395
|
+
s.health = "uncertain";
|
|
396
|
+
s.request.error = s.error;
|
|
397
|
+
} else s.health = "error";
|
|
398
|
+
const previous = store.standby(s.sid);
|
|
399
|
+
this.save(
|
|
400
|
+
s,
|
|
401
|
+
recordChanged(previous, s) || previous?.error !== s.error ? "wake.failed" : void 0,
|
|
402
|
+
s.error
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
close() {
|
|
407
|
+
this.stopped = true;
|
|
408
|
+
for (const a of this.adapters.values()) a.close();
|
|
409
|
+
this.adapters.clear();
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
function recordChanged(a, b) {
|
|
413
|
+
return a?.health !== b.health || a?.host_state !== b.host_state || a?.request?.state !== b.request?.state;
|
|
414
|
+
}
|
|
415
|
+
|
|
9
416
|
// src/daemon/server.ts
|
|
10
417
|
import { createServer } from "node:net";
|
|
11
418
|
import { chmodSync as chmodSync2, rmSync as rmSync4, writeFileSync as writeFileSync3, existsSync as existsSync2, statSync as statSync3 } from "node:fs";
|
|
12
419
|
|
|
13
420
|
// src/daemon/core.ts
|
|
421
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
14
422
|
import { readdirSync as readdirSync2, rmSync, writeFileSync } from "node:fs";
|
|
15
423
|
import { join as join2 } from "node:path";
|
|
16
424
|
|
|
@@ -4128,23 +4536,6 @@ var coerce = {
|
|
|
4128
4536
|
};
|
|
4129
4537
|
var NEVER = INVALID;
|
|
4130
4538
|
|
|
4131
|
-
// src/shared/protocol.ts
|
|
4132
|
-
var LIMITS = {
|
|
4133
|
-
maxWaitSec: 300,
|
|
4134
|
-
maxBody: 32768,
|
|
4135
|
-
maxData: 65536,
|
|
4136
|
-
maxFrame: 2 * 1024 * 1024
|
|
4137
|
-
};
|
|
4138
|
-
var CmdrError = class extends Error {
|
|
4139
|
-
constructor(code, message = code) {
|
|
4140
|
-
super(message);
|
|
4141
|
-
this.code = code;
|
|
4142
|
-
}
|
|
4143
|
-
};
|
|
4144
|
-
function fail(code, message) {
|
|
4145
|
-
throw new CmdrError(code, message);
|
|
4146
|
-
}
|
|
4147
|
-
|
|
4148
4539
|
// src/shared/schemas.ts
|
|
4149
4540
|
var text = external_exports.string().min(1).refine((v) => Buffer.byteLength(v) <= LIMITS.maxBody, "MESSAGE_TOO_LARGE");
|
|
4150
4541
|
var data = external_exports.record(external_exports.unknown()).refine((v) => Buffer.byteLength(JSON.stringify(v)) <= LIMITS.maxData, "MESSAGE_TOO_LARGE").optional();
|
|
@@ -4157,16 +4548,20 @@ var schemas = {
|
|
|
4157
4548
|
squad: external_exports.string().optional(),
|
|
4158
4549
|
name: external_exports.string().trim().min(1).max(64).optional(),
|
|
4159
4550
|
note: text.optional(),
|
|
4160
|
-
squad_name: external_exports.string().trim().min(1).max(64).optional()
|
|
4551
|
+
squad_name: external_exports.string().trim().min(1).max(64).optional(),
|
|
4552
|
+
takeover: external_exports.boolean().default(false),
|
|
4553
|
+
standby: external_exports.enum(["auto", "manual"]).optional(),
|
|
4554
|
+
rebind: external_exports.string().optional()
|
|
4161
4555
|
}).strict(),
|
|
4162
4556
|
list: external_exports.object({
|
|
4163
4557
|
...identity,
|
|
4558
|
+
full: external_exports.boolean().default(false),
|
|
4164
4559
|
scope: external_exports.enum(["squad", "all"]).optional(),
|
|
4165
4560
|
squad: external_exports.string().optional()
|
|
4166
4561
|
}).strict(),
|
|
4167
4562
|
report: external_exports.object({
|
|
4168
4563
|
...identity,
|
|
4169
|
-
status: external_exports.enum(["ready", "working", "blocked", "done", "failed"]),
|
|
4564
|
+
status: external_exports.enum(["ready", "working", "blocked", "done", "failed", "cancelled"]),
|
|
4170
4565
|
message: text,
|
|
4171
4566
|
reply_to: external_exports.string().optional(),
|
|
4172
4567
|
data
|
|
@@ -4176,7 +4571,10 @@ var schemas = {
|
|
|
4176
4571
|
...identity,
|
|
4177
4572
|
to: external_exports.union([external_exports.string().min(1), external_exports.array(external_exports.string().min(1)).min(1).max(1e3)]),
|
|
4178
4573
|
message: text,
|
|
4179
|
-
type: external_exports.enum(["command", "answer", "info"]).default("command"),
|
|
4574
|
+
type: external_exports.enum(["command", "cancel", "answer", "info"]).default("command"),
|
|
4575
|
+
task_key: external_exports.string().min(1).max(128).optional(),
|
|
4576
|
+
reassign: external_exports.string().optional(),
|
|
4577
|
+
attention: external_exports.boolean().optional(),
|
|
4180
4578
|
priority: external_exports.enum(["high", "normal", "low"]).optional(),
|
|
4181
4579
|
reply_to: external_exports.string().optional(),
|
|
4182
4580
|
data
|
|
@@ -4187,7 +4585,10 @@ var schemas = {
|
|
|
4187
4585
|
limit: external_exports.number().int().min(1).max(100).default(20),
|
|
4188
4586
|
peek: external_exports.boolean().default(false),
|
|
4189
4587
|
history: external_exports.boolean().default(false),
|
|
4190
|
-
since: external_exports.string().optional()
|
|
4588
|
+
since: external_exports.string().optional(),
|
|
4589
|
+
id: external_exports.string().optional(),
|
|
4590
|
+
recover: external_exports.boolean().default(false),
|
|
4591
|
+
full: external_exports.boolean().default(false)
|
|
4191
4592
|
}).strict(),
|
|
4192
4593
|
leave: external_exports.object({ ...identity, dissolve: external_exports.boolean().default(false), message: text.optional() }).strict()
|
|
4193
4594
|
};
|
|
@@ -4202,13 +4603,13 @@ function parse(tool, args) {
|
|
|
4202
4603
|
}
|
|
4203
4604
|
|
|
4204
4605
|
// src/shared/ids.ts
|
|
4205
|
-
import { randomBytes, randomUUID } from "node:crypto";
|
|
4606
|
+
import { randomBytes, randomUUID as randomUUID2 } from "node:crypto";
|
|
4206
4607
|
var alphabet = "23456789abcdefghjkmnpqrstuvwxyz";
|
|
4207
4608
|
function squadId() {
|
|
4208
4609
|
return Array.from(randomBytes(6), (b) => alphabet[b % alphabet.length]).join("");
|
|
4209
4610
|
}
|
|
4210
4611
|
var messageId = () => `m_${Date.now().toString(36)}${randomBytes(6).toString("hex")}`;
|
|
4211
|
-
var provisionalId = (agent) => `${agent}:prov-${
|
|
4612
|
+
var provisionalId = (agent) => `${agent}:prov-${randomUUID2()}`;
|
|
4212
4613
|
var safeSid = (sid) => Buffer.from(sid).toString("base64url");
|
|
4213
4614
|
|
|
4214
4615
|
// src/shared/env.ts
|
|
@@ -4228,17 +4629,29 @@ function recommendedWait(agent, env = process.env) {
|
|
|
4228
4629
|
}
|
|
4229
4630
|
|
|
4230
4631
|
// src/daemon/core.ts
|
|
4632
|
+
function bounded(values, limit) {
|
|
4633
|
+
const result = [];
|
|
4634
|
+
let bytes = 0;
|
|
4635
|
+
for (const value of values) {
|
|
4636
|
+
const size = Buffer.byteLength(JSON.stringify(value));
|
|
4637
|
+
if (result.length >= limit || result.length > 0 && bytes + size > LIMITS.maxFrame / 2) break;
|
|
4638
|
+
result.push(value);
|
|
4639
|
+
bytes += size;
|
|
4640
|
+
}
|
|
4641
|
+
return result;
|
|
4642
|
+
}
|
|
4231
4643
|
var Core = class {
|
|
4232
4644
|
constructor(store, paths2, config2) {
|
|
4233
4645
|
this.store = store;
|
|
4234
4646
|
this.paths = paths2;
|
|
4235
4647
|
this.config = config2;
|
|
4236
4648
|
for (const s of store.sessions()) {
|
|
4237
|
-
s.presence = "offline";
|
|
4649
|
+
s.presence = s.transport === "cli" ? "cli" : "offline";
|
|
4238
4650
|
store.saveSession(s);
|
|
4239
4651
|
}
|
|
4240
4652
|
this.refreshFlags();
|
|
4241
4653
|
}
|
|
4654
|
+
configureStandby;
|
|
4242
4655
|
contexts = /* @__PURE__ */ new Set();
|
|
4243
4656
|
waiters = /* @__PURE__ */ new Set();
|
|
4244
4657
|
effects = [];
|
|
@@ -4250,22 +4663,32 @@ var Core = class {
|
|
|
4250
4663
|
ctx.closed = true;
|
|
4251
4664
|
this.contexts.delete(ctx);
|
|
4252
4665
|
for (const w of this.waiters) if (w.ctx === ctx) w.finish();
|
|
4253
|
-
if (ctx.sid && ![...this.contexts].some(
|
|
4666
|
+
if (ctx.sid && ![...this.contexts].some(
|
|
4667
|
+
(c) => c.kind === "mcp" && c.transport !== "cli" && c.sid === ctx.sid
|
|
4668
|
+
)) {
|
|
4254
4669
|
const s = this.store.session(ctx.sid);
|
|
4255
4670
|
if (s) {
|
|
4256
|
-
s.presence = "offline";
|
|
4257
|
-
|
|
4258
|
-
|
|
4671
|
+
s.presence = s.transport === "cli" ? "cli" : "offline";
|
|
4672
|
+
this.atomic(() => {
|
|
4673
|
+
this.store.saveSession(s);
|
|
4674
|
+
this.record("session.disconnected", s.squad_id, {
|
|
4675
|
+
to_sid: s.sid,
|
|
4676
|
+
data: { presence: s.presence }
|
|
4677
|
+
});
|
|
4678
|
+
});
|
|
4259
4679
|
}
|
|
4260
4680
|
}
|
|
4261
4681
|
}
|
|
4262
4682
|
close() {
|
|
4683
|
+
for (const ctx of this.contexts) ctx.closed = true;
|
|
4263
4684
|
for (const w of this.waiters) w.finish();
|
|
4264
4685
|
}
|
|
4265
4686
|
me(ctx) {
|
|
4266
4687
|
return ctx.sid ? this.store.session(ctx.sid) : void 0;
|
|
4267
4688
|
}
|
|
4268
4689
|
required(ctx) {
|
|
4690
|
+
if (ctx.sid && this.store.revoked(ctx.sid))
|
|
4691
|
+
fail("ENDPOINT_REPLACED", "This endpoint was replaced; use the new member session.");
|
|
4269
4692
|
return this.me(ctx) || fail("NOT_JOINED", "Register a session first.");
|
|
4270
4693
|
}
|
|
4271
4694
|
member(ctx, role) {
|
|
@@ -4290,10 +4713,13 @@ var Core = class {
|
|
|
4290
4713
|
role: s.role,
|
|
4291
4714
|
squad: s.squad_id,
|
|
4292
4715
|
name: s.name,
|
|
4716
|
+
member_id: s.member_id || s.sid,
|
|
4717
|
+
wake_mode: this.store.standby(s.sid)?.wake_mode || "manual",
|
|
4718
|
+
listener: this.standbyView(s.sid),
|
|
4293
4719
|
identity: s.native_id ? "confirmed" : "provisional",
|
|
4294
4720
|
recommended_wait: this.waitHint(s)
|
|
4295
4721
|
} : null,
|
|
4296
|
-
unread: s ? this.
|
|
4722
|
+
unread: s ? this.inbox(s).length : 0
|
|
4297
4723
|
};
|
|
4298
4724
|
}
|
|
4299
4725
|
board(q) {
|
|
@@ -4303,18 +4729,101 @@ var Core = class {
|
|
|
4303
4729
|
members: this.members(q.id).map((s) => this.view(s))
|
|
4304
4730
|
};
|
|
4305
4731
|
}
|
|
4306
|
-
|
|
4307
|
-
const
|
|
4732
|
+
standbyView(sid) {
|
|
4733
|
+
const standby = this.store.standby(sid);
|
|
4734
|
+
return standby ? {
|
|
4735
|
+
sid: standby.sid,
|
|
4736
|
+
wake_mode: standby.wake_mode,
|
|
4737
|
+
enabled: standby.enabled,
|
|
4738
|
+
health: standby.health,
|
|
4739
|
+
host_state: standby.host_state,
|
|
4740
|
+
checked_at: standby.checked_at,
|
|
4741
|
+
error: standby.error,
|
|
4742
|
+
request: standby.request ? {
|
|
4743
|
+
id: standby.request.id,
|
|
4744
|
+
state: standby.request.state,
|
|
4745
|
+
created_at: standby.request.created_at
|
|
4746
|
+
} : void 0,
|
|
4747
|
+
can_auto_respond: standby.enabled && standby.wake_mode !== "manual" && standby.health === "healthy"
|
|
4748
|
+
} : { wake_mode: "manual", health: "manual", can_auto_respond: false };
|
|
4749
|
+
}
|
|
4750
|
+
inbox(s, history = false) {
|
|
4751
|
+
return [
|
|
4752
|
+
...this.store.queue(s.sid, history),
|
|
4753
|
+
...s.role === "commander" && s.squad_id ? this.store.queue(`squad:${s.squad_id}`, history) : []
|
|
4754
|
+
].filter((m) => history || !m.blocked_by || terminalWork(this.store.message(m.blocked_by))).sort((a, b) => a.priority - b.priority || a.seq - b.seq);
|
|
4755
|
+
}
|
|
4756
|
+
view(s, full = false, commandSquad) {
|
|
4757
|
+
const commands = this.store.commands(s.sid).filter((m) => !commandSquad || m.squad_id === commandSquad).map((m) => ({
|
|
4758
|
+
id: m.id,
|
|
4759
|
+
task_key: m.task_key,
|
|
4760
|
+
state: m.work?.state || (m.status === "queued" ? "queued" : "read"),
|
|
4761
|
+
created_at: m.created_at,
|
|
4762
|
+
updated_at: m.work?.updated_at || m.created_at,
|
|
4763
|
+
unacked_for: m.work?.accepted_at ? null : Math.floor((Date.now() - m.created_at) / 1e3),
|
|
4764
|
+
cancel_requested_at: m.work?.cancel_requested_at,
|
|
4765
|
+
blocked_by: m.blocked_by
|
|
4766
|
+
}));
|
|
4308
4767
|
return {
|
|
4309
|
-
...s
|
|
4768
|
+
...full ? s : {
|
|
4769
|
+
sid: s.sid,
|
|
4770
|
+
agent: s.agent,
|
|
4771
|
+
name: s.name,
|
|
4772
|
+
role: s.role,
|
|
4773
|
+
cwd: s.cwd,
|
|
4774
|
+
native_id: s.native_id,
|
|
4775
|
+
presence: s.presence,
|
|
4776
|
+
activity: s.activity,
|
|
4777
|
+
last_status: s.last_status
|
|
4778
|
+
},
|
|
4779
|
+
member_id: s.member_id || s.sid,
|
|
4310
4780
|
title: titleFor(s),
|
|
4311
4781
|
short: s.sid.slice(0, s.sid.indexOf(":") + 9),
|
|
4312
4782
|
squad: s.squad_id,
|
|
4313
4783
|
last_seen: s.last_seen_at,
|
|
4314
|
-
|
|
4784
|
+
activity_at: s.activity_at || null,
|
|
4785
|
+
last_progress_at: s.last_progress_at || null,
|
|
4786
|
+
hook_seen_at: s.hook_seen_at || null,
|
|
4787
|
+
pending: commands.filter((m) => m.state === "queued").length,
|
|
4788
|
+
unacked: commands.filter((m) => !["accepted"].includes(m.state)).length,
|
|
4789
|
+
in_progress: commands.filter((m) => m.state === "accepted").length,
|
|
4790
|
+
commands,
|
|
4791
|
+
listener: this.standbyView(s.sid)
|
|
4315
4792
|
};
|
|
4316
4793
|
}
|
|
4794
|
+
record(kind, channel, detail = {}) {
|
|
4795
|
+
return this.store.appendEvent({ at: Date.now(), kind, channel, ...detail });
|
|
4796
|
+
}
|
|
4797
|
+
messageEvent(kind, m, reason) {
|
|
4798
|
+
this.record(kind, m.squad_id, {
|
|
4799
|
+
from_sid: m.from_sid,
|
|
4800
|
+
to_sid: m.to_sid,
|
|
4801
|
+
message_id: m.id,
|
|
4802
|
+
reply_to: m.reply_to,
|
|
4803
|
+
message: m,
|
|
4804
|
+
reason
|
|
4805
|
+
});
|
|
4806
|
+
}
|
|
4807
|
+
publishEvents(after) {
|
|
4808
|
+
for (const event of this.store.events(after))
|
|
4809
|
+
for (const c of this.contexts) {
|
|
4810
|
+
const t = c.tail;
|
|
4811
|
+
if (!t || event.event_seq <= t.after) continue;
|
|
4812
|
+
t.after = event.event_seq;
|
|
4813
|
+
if (t.squad && t.squad !== event.channel) continue;
|
|
4814
|
+
if (t.for && t.for !== event.to_sid && !(event.to_sid === `squad:${event.channel}` && this.store.squad(event.channel)?.commander_sid === t.for))
|
|
4815
|
+
continue;
|
|
4816
|
+
c.notify(
|
|
4817
|
+
"lifecycle.event",
|
|
4818
|
+
t.full ? event : {
|
|
4819
|
+
...event,
|
|
4820
|
+
message: event.message ? { ...event.message, body: event.message.body.slice(0, 160), data: null } : void 0
|
|
4821
|
+
}
|
|
4822
|
+
);
|
|
4823
|
+
}
|
|
4824
|
+
}
|
|
4317
4825
|
atomic(fn) {
|
|
4826
|
+
const cursor = this.store.eventCursor();
|
|
4318
4827
|
this.effects = [];
|
|
4319
4828
|
let result;
|
|
4320
4829
|
try {
|
|
@@ -4326,12 +4835,13 @@ var Core = class {
|
|
|
4326
4835
|
const effects = this.effects;
|
|
4327
4836
|
this.effects = [];
|
|
4328
4837
|
for (const m of effects) {
|
|
4329
|
-
this.
|
|
4838
|
+
const recipient = m.to_sid.startsWith("squad:") ? this.store.squad(m.squad_id)?.commander_sid : m.to_sid;
|
|
4839
|
+
if (recipient) this.flag(recipient);
|
|
4330
4840
|
for (const c of this.contexts) {
|
|
4331
|
-
if (c.sid ===
|
|
4841
|
+
if (c.sid === recipient)
|
|
4332
4842
|
c.notify("msg.new", {
|
|
4333
|
-
sid:
|
|
4334
|
-
count: this.store.
|
|
4843
|
+
sid: recipient,
|
|
4844
|
+
count: this.inbox(this.store.session(recipient)).length,
|
|
4335
4845
|
top_priority: m.priority
|
|
4336
4846
|
});
|
|
4337
4847
|
if (c.tail && (!c.tail.squad || c.tail.squad === m.squad_id))
|
|
@@ -4341,8 +4851,15 @@ var Core = class {
|
|
|
4341
4851
|
});
|
|
4342
4852
|
}
|
|
4343
4853
|
}
|
|
4854
|
+
this.publishEvents(cursor);
|
|
4344
4855
|
for (const w of [...this.waiters]) {
|
|
4345
|
-
if (this.store.
|
|
4856
|
+
if (!this.store.session(w.sid)) {
|
|
4857
|
+
w.finish();
|
|
4858
|
+
continue;
|
|
4859
|
+
}
|
|
4860
|
+
if (this.inbox(this.store.session(w.sid)).some(
|
|
4861
|
+
(m) => !w.answer || m.type === "answer" && m.reply_to === w.answer
|
|
4862
|
+
))
|
|
4346
4863
|
w.finish();
|
|
4347
4864
|
}
|
|
4348
4865
|
return result;
|
|
@@ -4357,17 +4874,17 @@ var Core = class {
|
|
|
4357
4874
|
rmSync(this.paths.flag(sid), { force: true });
|
|
4358
4875
|
}
|
|
4359
4876
|
refreshFlags() {
|
|
4360
|
-
const queued = new Set(
|
|
4361
|
-
|
|
4362
|
-
|
|
4877
|
+
const queued = /* @__PURE__ */ new Set();
|
|
4878
|
+
for (const s of this.store.sessions()) {
|
|
4879
|
+
const queue = this.inbox(s);
|
|
4880
|
+
if (!queue.length) continue;
|
|
4881
|
+
queued.add(s.sid);
|
|
4882
|
+
if (queue.some((m) => m.seq > s.last_notified_seq) || queue.some((m) => m.priority <= 0) && Date.now() - s.last_notified_at >= this.config.remindIntervalSec * 1e3)
|
|
4883
|
+
this.flag(s.sid);
|
|
4884
|
+
}
|
|
4363
4885
|
for (const f of readdirSync2(this.paths.flags))
|
|
4364
4886
|
if (!queued.has(Buffer.from(f, "base64url").toString()))
|
|
4365
4887
|
rmSync(join2(this.paths.flags, f), { force: true });
|
|
4366
|
-
for (const sid of queued) {
|
|
4367
|
-
const s = this.store.session(sid), queue = this.store.queue(sid);
|
|
4368
|
-
if (s && (queue.some((m) => m.seq > s.last_notified_seq) || queue.some((m) => m.priority === 0) && Date.now() - s.last_notified_at >= this.config.remindIntervalSec * 1e3))
|
|
4369
|
-
this.flag(sid);
|
|
4370
|
-
}
|
|
4371
4888
|
}
|
|
4372
4889
|
register(ctx, p) {
|
|
4373
4890
|
if (!["mcp", "hook", "cli"].includes(p.kind))
|
|
@@ -4381,6 +4898,7 @@ var Core = class {
|
|
|
4381
4898
|
const sid = p.native_id ? `${agent}:${String(p.native_id)}` : p.sid || ctx.sid || provisionalId(agent);
|
|
4382
4899
|
if (typeof sid !== "string" || !sid.startsWith(agent + ":") || sid.length > 300 || p.native_id !== void 0 && (typeof p.native_id !== "string" || !p.native_id || p.native_id.length > 256))
|
|
4383
4900
|
fail("INVALID_ARGUMENT");
|
|
4901
|
+
if (this.store.revoked(sid)) fail("ENDPOINT_REPLACED");
|
|
4384
4902
|
let s = this.store.session(sid);
|
|
4385
4903
|
if (!s)
|
|
4386
4904
|
s = {
|
|
@@ -4396,7 +4914,8 @@ var Core = class {
|
|
|
4396
4914
|
role: "none",
|
|
4397
4915
|
squad_id: null,
|
|
4398
4916
|
presence: "offline",
|
|
4399
|
-
activity: "
|
|
4917
|
+
activity: "unknown",
|
|
4918
|
+
member_id: `member:${randomUUID3()}`,
|
|
4400
4919
|
last_status: null,
|
|
4401
4920
|
last_notified_seq: 0,
|
|
4402
4921
|
last_notified_at: 0,
|
|
@@ -4415,17 +4934,24 @@ var Core = class {
|
|
|
4415
4934
|
if (p.kind === "mcp") {
|
|
4416
4935
|
if (ctx.sid && ctx.sid !== sid)
|
|
4417
4936
|
fail("ROLE_NOT_ALLOWED", "Use session.identify to bind identity");
|
|
4418
|
-
|
|
4937
|
+
ctx.transport = p.transport === "cli" ? "cli" : "mcp";
|
|
4938
|
+
s.transport = ctx.transport;
|
|
4939
|
+
s.presence = ctx.transport === "mcp" || [...this.contexts].some((c) => c.sid === sid && c.kind === "mcp" && c.transport !== "cli") ? "online" : "cli";
|
|
4419
4940
|
ctx.sid = sid;
|
|
4420
4941
|
ctx.agent = agent;
|
|
4421
4942
|
ctx.waitHint = Number.isFinite(p.wait_hint) ? Math.min(300, Math.max(0, p.wait_hint)) : recommendedWait(agent, {});
|
|
4422
4943
|
}
|
|
4423
4944
|
this.store.saveSession(s);
|
|
4945
|
+
this.record("session.registered", s.squad_id, {
|
|
4946
|
+
to_sid: s.sid,
|
|
4947
|
+
data: { presence: s.presence, transport: s.transport }
|
|
4948
|
+
});
|
|
4424
4949
|
return s;
|
|
4425
4950
|
}
|
|
4426
4951
|
identify(ctx, native, force = false) {
|
|
4427
4952
|
if (typeof native !== "string" || !native || native.length > 256) fail("INVALID_ARGUMENT");
|
|
4428
4953
|
const old = this.required(ctx), sid = `${old.agent}:${native}`;
|
|
4954
|
+
if (this.store.revoked(sid)) fail("ENDPOINT_REPLACED");
|
|
4429
4955
|
if (old.sid === sid) return old;
|
|
4430
4956
|
if (old.native_id && !force) return old;
|
|
4431
4957
|
const target = this.store.session(sid);
|
|
@@ -4434,6 +4960,7 @@ var Core = class {
|
|
|
4434
4960
|
const merged = {
|
|
4435
4961
|
...old,
|
|
4436
4962
|
...target,
|
|
4963
|
+
member_id: old.role !== "none" ? old.member_id || old.sid : target?.member_id || old.member_id || old.sid,
|
|
4437
4964
|
sid,
|
|
4438
4965
|
native_id: native,
|
|
4439
4966
|
presence: "online",
|
|
@@ -4474,14 +5001,14 @@ var Core = class {
|
|
|
4474
5001
|
return merged;
|
|
4475
5002
|
}
|
|
4476
5003
|
enqueue(from, q, to, type, body, opts = {}) {
|
|
4477
|
-
if (this.store.queue(to).length >= this.config.maxQueue)
|
|
5004
|
+
if (!opts.terminalAck && this.store.queue(to).filter((m2) => m2.type === "cancel" === (type === "cancel")).length >= (type === "cancel" ? 100 : this.config.maxQueue))
|
|
4478
5005
|
fail("QUEUE_FULL", `Queue for ${to} is full`);
|
|
4479
5006
|
const m = {
|
|
4480
5007
|
id: messageId(),
|
|
4481
5008
|
seq: 0,
|
|
4482
5009
|
squad_id: q,
|
|
4483
5010
|
type,
|
|
4484
|
-
priority: opts.priority ?? (["command", "ask", "answer"].includes(type) ? 0 : type === "report" ? 2 : 1),
|
|
5011
|
+
priority: opts.priority ?? (["command", "cancel", "ask", "answer"].includes(type) ? 0 : type === "report" ? 2 : 1),
|
|
4485
5012
|
from_sid: from?.sid || (opts.operator ? "operator" : "system"),
|
|
4486
5013
|
from_role: from?.role || (opts.operator ? "operator" : "system"),
|
|
4487
5014
|
from_name: from?.name || null,
|
|
@@ -4490,11 +5017,13 @@ var Core = class {
|
|
|
4490
5017
|
data: opts.data || null,
|
|
4491
5018
|
reply_to: opts.reply_to || null,
|
|
4492
5019
|
status: "queued",
|
|
4493
|
-
attn: opts.attn ?? ["command", "ask", "answer"].includes(type),
|
|
5020
|
+
attn: opts.attn ?? ["command", "cancel", "ask", "answer"].includes(type),
|
|
4494
5021
|
created_at: Date.now(),
|
|
4495
5022
|
delivered_at: null
|
|
4496
5023
|
};
|
|
5024
|
+
if (type === "command") m.work = { state: "queued", updated_at: m.created_at };
|
|
4497
5025
|
this.store.insert(m);
|
|
5026
|
+
this.messageEvent("message.queued", m);
|
|
4498
5027
|
this.effects.push(m);
|
|
4499
5028
|
return m;
|
|
4500
5029
|
}
|
|
@@ -4502,51 +5031,115 @@ var Core = class {
|
|
|
4502
5031
|
for (const s of to)
|
|
4503
5032
|
this.enqueue(null, q.id, s.sid, "system", body, { priority: high ? 0 : 1, attn: high });
|
|
4504
5033
|
}
|
|
5034
|
+
roleInbox(q, old) {
|
|
5035
|
+
for (const m of this.store.messages())
|
|
5036
|
+
if (m.squad_id === q.id && m.to_sid === old && !["command", "cancel", "answer"].includes(m.type)) {
|
|
5037
|
+
m.to_sid = `squad:${q.id}`;
|
|
5038
|
+
this.store.saveMessage(m);
|
|
5039
|
+
}
|
|
5040
|
+
}
|
|
5041
|
+
rebind(ctx, member) {
|
|
5042
|
+
const target = this.required(ctx);
|
|
5043
|
+
if (!target.native_id) fail("IDENTITY_REQUIRED");
|
|
5044
|
+
const old = this.store.sessions().find((s) => (s.member_id || s.sid) === member) || fail("MEMBER_NOT_FOUND");
|
|
5045
|
+
if (old.sid === target.sid) return;
|
|
5046
|
+
if (!old.squad_id) fail("NOT_JOINED", "The member must belong to a channel before rebinding");
|
|
5047
|
+
if (target.squad_id) fail("ALREADY_JOINED");
|
|
5048
|
+
const merged = {
|
|
5049
|
+
...old,
|
|
5050
|
+
sid: target.sid,
|
|
5051
|
+
agent: target.agent,
|
|
5052
|
+
native_id: target.native_id,
|
|
5053
|
+
member_id: old.member_id || old.sid,
|
|
5054
|
+
pid: target.pid,
|
|
5055
|
+
cwd: target.cwd,
|
|
5056
|
+
terminal: target.terminal,
|
|
5057
|
+
transcript_path: target.transcript_path,
|
|
5058
|
+
presence: target.presence,
|
|
5059
|
+
transport: target.transport,
|
|
5060
|
+
activity: "unknown",
|
|
5061
|
+
last_seen_at: Date.now(),
|
|
5062
|
+
ended_at: null
|
|
5063
|
+
};
|
|
5064
|
+
this.store.saveSession(merged);
|
|
5065
|
+
for (const q of this.store.squads())
|
|
5066
|
+
if (q.commander_sid === old.sid) {
|
|
5067
|
+
q.commander_sid = target.sid;
|
|
5068
|
+
this.store.saveSquad(q);
|
|
5069
|
+
}
|
|
5070
|
+
for (const m of this.store.messages()) {
|
|
5071
|
+
if (m.to_sid === old.sid) m.to_sid = target.sid;
|
|
5072
|
+
if (m.from_sid === old.sid) m.from_sid = target.sid;
|
|
5073
|
+
this.store.saveMessage(m);
|
|
5074
|
+
}
|
|
5075
|
+
this.store.migrateMembership(old.sid, target.sid);
|
|
5076
|
+
this.store.revoke(old.sid, target.sid);
|
|
5077
|
+
this.store.deleteSession(old.sid);
|
|
5078
|
+
this.store.deleteStandby(old.sid);
|
|
5079
|
+
for (const w of this.waiters) if (w.sid === old.sid) w.finish();
|
|
5080
|
+
this.clearFlag(old.sid);
|
|
5081
|
+
if (this.inbox(merged).length) this.flag(target.sid);
|
|
5082
|
+
this.record("member.rebound", merged.squad_id, {
|
|
5083
|
+
from_sid: old.sid,
|
|
5084
|
+
to_sid: target.sid,
|
|
5085
|
+
data: { member_id: merged.member_id, wake_mode: "manual" }
|
|
5086
|
+
});
|
|
5087
|
+
}
|
|
4505
5088
|
joinSquad(ctx, p) {
|
|
5089
|
+
if (p.rebind && (p.role || p.squad || p.squad_name || p.takeover))
|
|
5090
|
+
fail("INVALID_ARGUMENT", "rebind cannot be combined with a role or channel change");
|
|
5091
|
+
if (p.rebind) this.rebind(ctx, p.rebind);
|
|
4506
5092
|
const s = this.required(ctx);
|
|
4507
5093
|
let q, role = p.role;
|
|
4508
5094
|
if (p.squad_name) {
|
|
4509
|
-
if (p.
|
|
4510
|
-
fail("INVALID_ARGUMENT", "squad_name is the atomic shortcut; omit role and squad");
|
|
5095
|
+
if (p.squad) fail("INVALID_ARGUMENT", "Use squad or squad_name, not both");
|
|
4511
5096
|
q = this.store.squads().find((q2) => q2.name_key === p.squad_name.toLowerCase() && q2.status !== "dissolved");
|
|
4512
|
-
if (s.squad_id
|
|
4513
|
-
|
|
4514
|
-
fail(
|
|
4515
|
-
"SQUAD_ORPHANED",
|
|
4516
|
-
`Squad ${q.id} is orphaned. Choose join(role="commander", squad="${q.id}") to take over or role="executor" to join.`
|
|
4517
|
-
);
|
|
4518
|
-
role = q ? "executor" : "commander";
|
|
4519
|
-
if (!q) p.name = p.squad_name;
|
|
5097
|
+
if (s.squad_id === q?.id && !role) return this.joinResult(ctx, q);
|
|
5098
|
+
role ||= "executor";
|
|
4520
5099
|
} else if (p.squad) q = this.store.squad(p.squad) || fail("SQUAD_NOT_FOUND");
|
|
5100
|
+
else if (p.rebind && s.squad_id) return this.joinResult(ctx, this.store.squad(s.squad_id));
|
|
4521
5101
|
if (!role) fail("INVALID_ARGUMENT", "Provide role or squad_name");
|
|
5102
|
+
if (q?.status === "dissolved") fail("SQUAD_NOT_FOUND", "Channel is closed");
|
|
4522
5103
|
if (s.squad_id) {
|
|
4523
5104
|
if ((q?.id === s.squad_id || !q && !p.squad_name && role === "commander") && s.role === role)
|
|
4524
5105
|
return this.joinResult(ctx, this.store.squad(s.squad_id));
|
|
4525
|
-
if (q?.id === s.squad_id &&
|
|
4526
|
-
|
|
4527
|
-
} else fail("ALREADY_JOINED");
|
|
5106
|
+
if (q?.id === s.squad_id && role === "commander") this.store.leave(s.sid);
|
|
5107
|
+
else fail("ALREADY_JOINED");
|
|
4528
5108
|
}
|
|
4529
|
-
if (q?.status === "dissolved") fail("SQUAD_NOT_FOUND", "Squad is dissolved");
|
|
4530
5109
|
if (!q) {
|
|
4531
|
-
if (role !== "commander") fail("SQUAD_NOT_FOUND");
|
|
4532
|
-
const
|
|
5110
|
+
if (role !== "commander" && !p.squad_name) fail("SQUAD_NOT_FOUND");
|
|
5111
|
+
const name = p.squad_name || p.name || null, key = name?.toLowerCase() || null;
|
|
4533
5112
|
if (key && this.store.squads().some((x) => x.name_key === key && x.status !== "dissolved"))
|
|
4534
|
-
fail("SQUAD_NAME_EXISTS"
|
|
5113
|
+
fail("SQUAD_NAME_EXISTS");
|
|
4535
5114
|
let id;
|
|
4536
5115
|
do {
|
|
4537
5116
|
id = squadId();
|
|
4538
5117
|
} while (this.store.squad(id));
|
|
4539
5118
|
q = {
|
|
4540
5119
|
id,
|
|
4541
|
-
name
|
|
5120
|
+
name,
|
|
4542
5121
|
name_key: key,
|
|
4543
|
-
commander_sid: s.sid,
|
|
4544
|
-
status: "active",
|
|
5122
|
+
commander_sid: role === "commander" ? s.sid : null,
|
|
5123
|
+
status: role === "commander" ? "active" : "orphaned",
|
|
4545
5124
|
created_at: Date.now(),
|
|
4546
5125
|
updated_at: Date.now()
|
|
4547
5126
|
};
|
|
5127
|
+
this.record("channel.created", q.id);
|
|
4548
5128
|
} else if (role === "commander") {
|
|
4549
|
-
if (q.commander_sid && q.commander_sid !== s.sid)
|
|
5129
|
+
if (q.commander_sid && q.commander_sid !== s.sid) {
|
|
5130
|
+
if (!p.takeover)
|
|
5131
|
+
fail(
|
|
5132
|
+
"SQUAD_HAS_COMMANDER",
|
|
5133
|
+
"Use takeover=true for an explicit handover; offline does not mean stopped."
|
|
5134
|
+
);
|
|
5135
|
+
const old = this.store.session(q.commander_sid);
|
|
5136
|
+
this.roleInbox(q, old.sid);
|
|
5137
|
+
this.store.leave(old.sid);
|
|
5138
|
+
old.role = "executor";
|
|
5139
|
+
this.store.saveSession(old);
|
|
5140
|
+
this.store.join(old);
|
|
5141
|
+
this.record("commander.handover", q.id, { from_sid: old.sid, to_sid: s.sid });
|
|
5142
|
+
}
|
|
4550
5143
|
q.commander_sid = s.sid;
|
|
4551
5144
|
q.status = "active";
|
|
4552
5145
|
this.system(
|
|
@@ -4554,29 +5147,25 @@ var Core = class {
|
|
|
4554
5147
|
this.members(q.id).filter((member) => member.sid !== s.sid),
|
|
4555
5148
|
"commander_joined"
|
|
4556
5149
|
);
|
|
4557
|
-
|
|
4558
|
-
if (inbox.length + this.store.queue(s.sid).length > this.config.maxQueue) fail("QUEUE_FULL");
|
|
4559
|
-
for (const m of inbox) {
|
|
4560
|
-
m.to_sid = s.sid;
|
|
4561
|
-
this.store.saveMessage(m);
|
|
4562
|
-
this.effects.push(m);
|
|
4563
|
-
}
|
|
5150
|
+
this.record("commander.claimed", q.id, { to_sid: s.sid });
|
|
4564
5151
|
}
|
|
4565
5152
|
s.role = role;
|
|
4566
5153
|
s.squad_id = q.id;
|
|
4567
|
-
s.name = p.name ||
|
|
5154
|
+
s.name = p.name || s.name;
|
|
4568
5155
|
q.updated_at = Date.now();
|
|
4569
5156
|
this.store.saveSquad(q);
|
|
4570
5157
|
this.store.saveSession(s);
|
|
4571
5158
|
this.store.join(s);
|
|
5159
|
+
this.record("member.joined", q.id, { to_sid: s.sid, data: { role, member_id: s.member_id } });
|
|
4572
5160
|
if (role === "executor" && q.commander_sid)
|
|
4573
5161
|
this.enqueue(
|
|
4574
5162
|
s,
|
|
4575
5163
|
q.id,
|
|
4576
|
-
q.
|
|
5164
|
+
`squad:${q.id}`,
|
|
4577
5165
|
"system",
|
|
4578
5166
|
`member_joined${p.note ? `: ${p.note}` : ""}`
|
|
4579
5167
|
);
|
|
5168
|
+
if (this.inbox(s).length) this.flag(s.sid);
|
|
4580
5169
|
return this.joinResult(ctx, q);
|
|
4581
5170
|
}
|
|
4582
5171
|
joinResult(ctx, q) {
|
|
@@ -4587,13 +5176,15 @@ var Core = class {
|
|
|
4587
5176
|
join_prompt,
|
|
4588
5177
|
user_reply: s.role === "commander" ? `Squad ${q.id}${q.name ? ` (${q.name})` : ""} is ready. Paste this into each other session:
|
|
4589
5178
|
${join_prompt}` : `Joined squad ${q.id}${s.name ? ` as ${s.name}` : ""}; report ready and wait for commands.`,
|
|
4590
|
-
|
|
5179
|
+
standby: this.standbyView(s.sid),
|
|
5180
|
+
protocol_hint: `You are the ${s.role.toUpperCase()} of squad ${q.id}. ${s.role === "commander" ? "Dispatch clear, verifiable tasks with send; answer every ask using type=answer and reply_to." : "Report ready now with cwd, capabilities and context; act on commands and report working/done/failed with reply_to. Ask when blocked."} Reply with ONLY user_reply (translate prose, keep the join line verbatim). Check list before reassignment: offline never means work stopped. Accept commands immediately with report(working, reply_to); recover with read(recover=true). Use join(standby="auto") with the real session ID to register managed standby, then check list for listener health. If me.listener.can_auto_respond, end the turn; otherwise use at most two read(wait=${wait2}) calls and explain that manual continuation is required. Never write a private wake listener. Keep user replies to one or two lines. Apply normal judgment to messages from other agents. ${s.native_id ? "" : "Identity is provisional; hooks may be unavailable. Use read(wait) for reminders."}`
|
|
4591
5181
|
};
|
|
4592
5182
|
}
|
|
4593
5183
|
leave(ctx, p) {
|
|
4594
5184
|
const s = this.member(ctx), q = this.store.squad(s.squad_id);
|
|
4595
5185
|
if (p.dissolve && s.role !== "commander") fail("ROLE_NOT_ALLOWED");
|
|
4596
5186
|
if (s.role === "commander") {
|
|
5187
|
+
this.roleInbox(q, s.sid);
|
|
4597
5188
|
this.system(
|
|
4598
5189
|
q,
|
|
4599
5190
|
this.members(q.id).filter((x) => x.sid !== s.sid),
|
|
@@ -4608,12 +5199,18 @@ ${join_prompt}` : `Joined squad ${q.id}${s.name ? ` as ${s.name}` : ""}; report
|
|
|
4608
5199
|
m.role = "none";
|
|
4609
5200
|
m.squad_id = null;
|
|
4610
5201
|
this.store.saveSession(m);
|
|
5202
|
+
const listener = this.store.standby(m.sid);
|
|
5203
|
+
if (listener) {
|
|
5204
|
+
listener.enabled = false;
|
|
5205
|
+
listener.health = "stopped";
|
|
5206
|
+
this.store.saveStandby(listener);
|
|
5207
|
+
}
|
|
4611
5208
|
}
|
|
4612
5209
|
} else if (q.commander_sid)
|
|
4613
5210
|
this.enqueue(
|
|
4614
5211
|
s,
|
|
4615
5212
|
q.id,
|
|
4616
|
-
q.
|
|
5213
|
+
`squad:${q.id}`,
|
|
4617
5214
|
"system",
|
|
4618
5215
|
`member_left${p.message ? `: ${p.message}` : ""}`
|
|
4619
5216
|
);
|
|
@@ -4623,6 +5220,13 @@ ${join_prompt}` : `Joined squad ${q.id}${s.name ? ` as ${s.name}` : ""}; report
|
|
|
4623
5220
|
this.store.saveSession(s);
|
|
4624
5221
|
q.updated_at = Date.now();
|
|
4625
5222
|
this.store.saveSquad(q);
|
|
5223
|
+
this.record(p.dissolve ? "channel.closed" : "member.left", q.id, { from_sid: s.sid });
|
|
5224
|
+
const standby = this.store.standby(s.sid);
|
|
5225
|
+
if (standby) {
|
|
5226
|
+
standby.enabled = false;
|
|
5227
|
+
standby.health = "stopped";
|
|
5228
|
+
this.store.saveStandby(standby);
|
|
5229
|
+
}
|
|
4626
5230
|
return { left: true, squad: q.id, status: q.status };
|
|
4627
5231
|
}
|
|
4628
5232
|
rate(ctx) {
|
|
@@ -4640,7 +5244,7 @@ ${join_prompt}` : `Joined squad ${q.id}${s.name ? ` as ${s.name}` : ""}; report
|
|
|
4640
5244
|
for (const s of all) if (s.sid !== sender) found.set(s.sid, s);
|
|
4641
5245
|
continue;
|
|
4642
5246
|
}
|
|
4643
|
-
const exact = all.filter((s) => s.sid === token);
|
|
5247
|
+
const exact = all.filter((s) => s.sid === token || s.member_id === token);
|
|
4644
5248
|
const matches = exact.length ? exact : all.filter((s) => s.name === token || s.sid.startsWith(token));
|
|
4645
5249
|
if (matches.length !== 1)
|
|
4646
5250
|
fail(
|
|
@@ -4665,65 +5269,207 @@ ${join_prompt}` : `Joined squad ${q.id}${s.name ? ` as ${s.name}` : ""}; report
|
|
|
4665
5269
|
"Answers require reply_to for an ask from the recipient in this squad."
|
|
4666
5270
|
);
|
|
4667
5271
|
}
|
|
5272
|
+
if (p.type === "cancel" && (!p.reply_to || p.reassign))
|
|
5273
|
+
fail("INVALID_ARGUMENT", "cancel requires reply_to=<command id>");
|
|
5274
|
+
const previous = p.reassign ? this.store.message(p.reassign) : void 0;
|
|
5275
|
+
if (p.reassign && (p.type !== "command" || !previous || previous.type !== "command" || previous.squad_id !== qid || terminalWork(previous) || recipients.length !== 1))
|
|
5276
|
+
fail("INVALID_ARGUMENT", "reassign must reference one unfinished command in this channel");
|
|
5277
|
+
if (previous?.work?.replacement_id) fail("ALREADY_REASSIGNED");
|
|
5278
|
+
if (previous?.task_key && p.task_key && p.task_key !== previous.task_key)
|
|
5279
|
+
fail("INVALID_ARGUMENT", "Reassignment must preserve the original task_key");
|
|
5280
|
+
const taskKey = previous?.task_key || p.task_key;
|
|
5281
|
+
if (p.task_key && (p.type !== "command" || recipients.length !== 1))
|
|
5282
|
+
fail(
|
|
5283
|
+
"INVALID_ARGUMENT",
|
|
5284
|
+
"task_key identifies one command owner; do not broadcast the same ticket"
|
|
5285
|
+
);
|
|
5286
|
+
if (taskKey && this.store.commands().some((m) => m.squad_id === qid && m.task_key === taskKey && m.id !== previous?.id))
|
|
5287
|
+
fail("TASK_OWNED", "This task_key already has unfinished work. Use reassign=<command id>.");
|
|
5288
|
+
const warnings = recipients.flatMap((s) => {
|
|
5289
|
+
const active = this.store.commands(s.sid);
|
|
5290
|
+
return active.length ? [
|
|
5291
|
+
{
|
|
5292
|
+
code: "UNFINISHED_WORK",
|
|
5293
|
+
sid: s.sid,
|
|
5294
|
+
command_ids: active.map((m) => m.id),
|
|
5295
|
+
message: "Verify ownership before dispatch. Offline does not mean stopped."
|
|
5296
|
+
}
|
|
5297
|
+
] : [];
|
|
5298
|
+
});
|
|
4668
5299
|
this.rate(ctx);
|
|
4669
|
-
const
|
|
4670
|
-
|
|
5300
|
+
const cancel = (command, body) => {
|
|
5301
|
+
command.work ||= {
|
|
5302
|
+
state: command.status === "queued" ? "queued" : "read",
|
|
5303
|
+
updated_at: command.created_at
|
|
5304
|
+
};
|
|
5305
|
+
command.work.cancel_requested_at = Date.now();
|
|
5306
|
+
if (command.work.state === "queued") {
|
|
5307
|
+
command.work.state = "cancelled";
|
|
5308
|
+
command.status = "delivered";
|
|
5309
|
+
command.delivered_at = Date.now();
|
|
5310
|
+
}
|
|
5311
|
+
command.work.updated_at = Date.now();
|
|
5312
|
+
this.store.saveMessage(command);
|
|
5313
|
+
this.messageEvent(
|
|
5314
|
+
command.work.state === "cancelled" ? "work.cancelled" : "work.cancel_requested",
|
|
5315
|
+
command
|
|
5316
|
+
);
|
|
5317
|
+
return this.enqueue(from, qid, command.to_sid, "cancel", body, {
|
|
5318
|
+
priority: -1,
|
|
5319
|
+
reply_to: command.id,
|
|
5320
|
+
attn: true,
|
|
5321
|
+
operator: ctx.kind === "cli"
|
|
5322
|
+
});
|
|
5323
|
+
};
|
|
5324
|
+
if (previous)
|
|
5325
|
+
cancel(
|
|
5326
|
+
previous,
|
|
5327
|
+
`Cancel command ${previous.id} at the next safe checkpoint and report cancelled with reply_to. Reassignment waits for your terminal report.`
|
|
5328
|
+
);
|
|
5329
|
+
const messages = recipients.map((s) => {
|
|
5330
|
+
if (p.type === "cancel") {
|
|
5331
|
+
const command = this.store.message(p.reply_to);
|
|
5332
|
+
if (!command || command.type !== "command" || command.to_sid !== s.sid || command.squad_id !== qid || terminalWork(command))
|
|
5333
|
+
fail("INVALID_ARGUMENT", "Cancel target must own an unfinished command");
|
|
5334
|
+
return cancel(command, p.message);
|
|
5335
|
+
}
|
|
5336
|
+
const m = this.enqueue(from, qid, s.sid, p.type, p.message, {
|
|
4671
5337
|
priority: p.priority ? { high: 0, normal: 1, low: 2 }[p.priority] : void 0,
|
|
4672
5338
|
reply_to: p.reply_to,
|
|
4673
5339
|
data: p.data,
|
|
5340
|
+
attn: p.attention,
|
|
4674
5341
|
operator: ctx.kind === "cli"
|
|
4675
|
-
})
|
|
4676
|
-
|
|
5342
|
+
});
|
|
5343
|
+
if (p.type === "command") {
|
|
5344
|
+
m.task_key = taskKey;
|
|
5345
|
+
if (previous && !terminalWork(previous)) m.blocked_by = previous.id;
|
|
5346
|
+
this.store.saveMessage(m);
|
|
5347
|
+
if (previous) {
|
|
5348
|
+
previous.work.replacement_id = m.id;
|
|
5349
|
+
this.store.saveMessage(previous);
|
|
5350
|
+
this.messageEvent(
|
|
5351
|
+
"work.reassigned",
|
|
5352
|
+
m,
|
|
5353
|
+
`Replaces ${previous.id}; ${m.blocked_by ? "waiting for original owner to stop" : "original was unread"}`
|
|
5354
|
+
);
|
|
5355
|
+
}
|
|
5356
|
+
}
|
|
5357
|
+
return m;
|
|
5358
|
+
});
|
|
4677
5359
|
return {
|
|
4678
5360
|
ids: messages.map((m) => m.id),
|
|
5361
|
+
queued_to: recipients.map((s) => s.sid),
|
|
4679
5362
|
delivered_to: recipients.map((s) => s.sid),
|
|
5363
|
+
delivery_hint: "queued_to / delivered_to mean enqueued, not read or accepted",
|
|
5364
|
+
warnings: p.type === "command" ? warnings : [],
|
|
5365
|
+
blocked_by: messages.find((m) => m.blocked_by)?.blocked_by,
|
|
4680
5366
|
offline: recipients.filter((s) => s.presence === "offline").map((s) => s.sid),
|
|
4681
5367
|
idle: recipients.filter((s) => s.activity === "idle").map((s) => s.sid)
|
|
4682
5368
|
};
|
|
4683
5369
|
}
|
|
4684
5370
|
reportOrAsk(ctx, p, ask) {
|
|
4685
|
-
const s = this.member(ctx, "executor")
|
|
5371
|
+
const s = !ask && p.reply_to ? this.required(ctx) : this.member(ctx, "executor");
|
|
5372
|
+
let command = !ask && p.reply_to ? this.store.message(p.reply_to) : void 0;
|
|
5373
|
+
let terminalAck = false;
|
|
5374
|
+
const q = this.store.squad(command?.squad_id || s.squad_id) || fail("NOT_JOINED");
|
|
5375
|
+
if (!ask && p.reply_to) {
|
|
5376
|
+
command = this.store.message(p.reply_to);
|
|
5377
|
+
if (!command || command.type !== "command" || command.to_sid !== s.sid || command.squad_id !== q.id)
|
|
5378
|
+
fail("INVALID_ARGUMENT", "reply_to must identify a command owned by this member");
|
|
5379
|
+
if (command.blocked_by && !terminalWork(this.store.message(command.blocked_by)))
|
|
5380
|
+
fail("REASSIGNMENT_PENDING", "Original owner has not stopped");
|
|
5381
|
+
const state = {
|
|
5382
|
+
working: "accepted",
|
|
5383
|
+
blocked: "accepted",
|
|
5384
|
+
done: "completed",
|
|
5385
|
+
failed: "failed",
|
|
5386
|
+
cancelled: "cancelled"
|
|
5387
|
+
}[p.status];
|
|
5388
|
+
if (!state) fail("INVALID_ARGUMENT", "ready is not a command acknowledgement");
|
|
5389
|
+
if (terminalWork(command) && state !== command.work.state) fail("WORK_TERMINAL");
|
|
5390
|
+
if (!terminalWork(command)) {
|
|
5391
|
+
const now = Date.now();
|
|
5392
|
+
command.work ||= { state: "read", updated_at: now };
|
|
5393
|
+
if (command.work.cancel_requested_at && state === "accepted")
|
|
5394
|
+
fail("CANCEL_REQUESTED", "Stop at a safe checkpoint and report cancelled");
|
|
5395
|
+
const changed = command.work.state !== state;
|
|
5396
|
+
command.work.state = state;
|
|
5397
|
+
command.work.updated_at = now;
|
|
5398
|
+
if (state === "accepted") command.work.accepted_at ||= now;
|
|
5399
|
+
command.status = "delivered";
|
|
5400
|
+
command.delivered_at ||= now;
|
|
5401
|
+
this.store.saveMessage(command);
|
|
5402
|
+
terminalAck = terminalWork(command);
|
|
5403
|
+
this.messageEvent(changed ? `work.${state}` : "work.progress", command);
|
|
5404
|
+
if (terminalWork(command) && command.work.replacement_id) {
|
|
5405
|
+
const replacement = this.store.message(command.work.replacement_id);
|
|
5406
|
+
this.effects.push(replacement);
|
|
5407
|
+
this.messageEvent("work.released", replacement);
|
|
5408
|
+
}
|
|
5409
|
+
}
|
|
5410
|
+
}
|
|
4686
5411
|
this.rate(ctx);
|
|
4687
|
-
const to =
|
|
5412
|
+
const to = `squad:${q.id}`;
|
|
4688
5413
|
const m = this.enqueue(s, q.id, to, ask ? "ask" : "report", ask ? p.question : p.message, {
|
|
4689
5414
|
reply_to: p.reply_to,
|
|
4690
5415
|
data: ask ? p.data : { ...p.data, status: p.status },
|
|
4691
5416
|
priority: ask ? 0 : ["blocked", "failed"].includes(p.status) ? 1 : 2,
|
|
4692
|
-
attn: ask || ["done", "failed", "blocked"].includes(p.status)
|
|
5417
|
+
attn: ask || ["done", "failed", "blocked", "cancelled"].includes(p.status),
|
|
5418
|
+
// Each issued command reserves admission for its first terminal report.
|
|
5419
|
+
// Keep work and report atomic even under backpressure; repeats use the ordinary cap.
|
|
5420
|
+
terminalAck
|
|
4693
5421
|
});
|
|
4694
5422
|
if (!ask) {
|
|
4695
5423
|
s.last_status = { status: p.status, message: p.message.slice(0, 300) };
|
|
5424
|
+
s.last_progress_at = Date.now();
|
|
5425
|
+
s.activity_at = Date.now();
|
|
5426
|
+
s.activity = this.store.commands(s.sid).some((m2) => m2.work?.state === "accepted") ? "busy" : "idle";
|
|
4696
5427
|
this.store.saveSession(s);
|
|
4697
5428
|
}
|
|
4698
5429
|
return {
|
|
4699
5430
|
id: m.id,
|
|
4700
5431
|
delivered_to: to,
|
|
5432
|
+
queued_to: to,
|
|
5433
|
+
work: command?.work,
|
|
5434
|
+
...!ask && !p.reply_to && this.store.commands(s.sid).length ? { warning: "Uncorrelated report does not accept or finish a command; provide reply_to." } : {},
|
|
4701
5435
|
commander_presence: q.commander_sid ? this.store.session(q.commander_sid)?.presence : "orphaned",
|
|
4702
5436
|
answered: false
|
|
4703
5437
|
};
|
|
4704
5438
|
}
|
|
4705
5439
|
readNow(ctx, p, answer) {
|
|
4706
5440
|
const s = this.required(ctx);
|
|
4707
|
-
let queue = this.store.
|
|
5441
|
+
let queue = p.recover ? this.store.commands(s.sid).filter((m) => !m.blocked_by || terminalWork(this.store.message(m.blocked_by))) : this.inbox(s, p.history);
|
|
5442
|
+
if (p.id) {
|
|
5443
|
+
const m = this.store.message(p.id);
|
|
5444
|
+
if (!m || m.to_sid !== s.sid && !(s.role === "commander" && m.to_sid === `squad:${s.squad_id}`))
|
|
5445
|
+
fail("MESSAGE_NOT_FOUND");
|
|
5446
|
+
if (m.status === "queued" && m.blocked_by && !terminalWork(this.store.message(m.blocked_by)))
|
|
5447
|
+
fail("REASSIGNMENT_PENDING", "Original owner has not stopped");
|
|
5448
|
+
queue = [m];
|
|
5449
|
+
}
|
|
4708
5450
|
if (p.since) {
|
|
4709
5451
|
const since = this.store.message(p.since);
|
|
4710
|
-
if (!since || since.to_sid !== s.sid
|
|
5452
|
+
if (!since || since.to_sid !== s.sid && !(s.role === "commander" && since.to_sid === `squad:${s.squad_id}`))
|
|
5453
|
+
fail("INVALID_ARGUMENT", "Unknown since message");
|
|
4711
5454
|
queue = queue.filter((m) => m.seq > since.seq);
|
|
4712
5455
|
}
|
|
4713
5456
|
if (answer) queue = queue.filter((m) => m.type === "answer" && m.reply_to === answer);
|
|
4714
|
-
const messages = queue
|
|
4715
|
-
if (!p.peek && !p.history && !ctx.closed)
|
|
5457
|
+
const messages = bounded(queue, p.limit || 20);
|
|
5458
|
+
if (!p.peek && !p.history && !p.recover && !p.id && !ctx.closed)
|
|
4716
5459
|
for (const m of messages) {
|
|
4717
5460
|
m.status = "delivered";
|
|
4718
5461
|
m.delivered_at = Date.now();
|
|
5462
|
+
if (m.type === "command" && (!m.work || m.work.state === "queued"))
|
|
5463
|
+
m.work = { ...m.work, state: "read", updated_at: Date.now() };
|
|
4719
5464
|
this.store.saveMessage(m);
|
|
5465
|
+
this.messageEvent("message.read", m);
|
|
4720
5466
|
}
|
|
4721
|
-
const remaining = this.
|
|
5467
|
+
const remaining = this.inbox(s).length;
|
|
4722
5468
|
if (!remaining) this.clearFlag(s.sid);
|
|
4723
5469
|
return {
|
|
4724
5470
|
messages: ctx.closed ? [] : messages,
|
|
4725
5471
|
remaining,
|
|
4726
|
-
squad_summary: s.squad_id ? this.board(this.store.squad(s.squad_id)) : null
|
|
5472
|
+
...p.full ? { squad_summary: s.squad_id ? this.board(this.store.squad(s.squad_id)) : null } : {}
|
|
4727
5473
|
};
|
|
4728
5474
|
}
|
|
4729
5475
|
wait(ctx, seconds, answer, signal) {
|
|
@@ -4744,7 +5490,9 @@ ${join_prompt}` : `Joined squad ${q.id}${s.name ? ` as ${s.name}` : ""}; report
|
|
|
4744
5490
|
w.timer = setTimeout(w.finish, seconds * 1e3);
|
|
4745
5491
|
this.waiters.add(w);
|
|
4746
5492
|
signal?.addEventListener("abort", w.finish, { once: true });
|
|
4747
|
-
if (this.store.
|
|
5493
|
+
if (this.inbox(this.store.session(w.sid)).some(
|
|
5494
|
+
(m) => !answer || m.type === "answer" && m.reply_to === answer
|
|
5495
|
+
))
|
|
4748
5496
|
w.finish();
|
|
4749
5497
|
});
|
|
4750
5498
|
}
|
|
@@ -4753,18 +5501,19 @@ ${join_prompt}` : `Joined squad ${q.id}${s.name ? ` as ${s.name}` : ""}; report
|
|
|
4753
5501
|
if (squad && !this.store.squad(squad)) fail("SQUAD_NOT_FOUND");
|
|
4754
5502
|
return {
|
|
4755
5503
|
sessions: this.store.sessions().filter(
|
|
4756
|
-
(s) => squad ? s.squad_id === squad : s.presence === "online" || s.role !== "none"
|
|
5504
|
+
(s) => squad ? s.squad_id === squad || this.store.commands(s.sid).some((m) => m.squad_id === squad) : s.presence === "online" || s.role !== "none" || this.store.commands(s.sid).length > 0
|
|
4757
5505
|
).map((s) => ({
|
|
4758
|
-
...this.view(s),
|
|
4759
|
-
...s.sid === me?.sid ? { unread: this.
|
|
5506
|
+
...this.view(s, p.full, squad || void 0),
|
|
5507
|
+
...s.sid === me?.sid ? { unread: this.inbox(s).length } : {}
|
|
4760
5508
|
})),
|
|
4761
|
-
squads: this.store.squads().filter((q) => squad ? q.id === squad : q.status !== "dissolved").map((q) => this.board(q))
|
|
5509
|
+
squads: this.store.squads().filter((q) => squad ? q.id === squad : q.status !== "dissolved").map((q) => p.full ? this.board(q) : q)
|
|
4762
5510
|
};
|
|
4763
5511
|
}
|
|
4764
5512
|
hook(ctx, p) {
|
|
4765
5513
|
if (!p.session_id || !["SessionStart", "SessionEnd", "UserPromptSubmit", "PreToolUse", "Stop"].includes(p.event))
|
|
4766
5514
|
return {};
|
|
4767
5515
|
const agent = /^[a-z][a-z0-9_-]{0,63}$/.test(p.agent || "") ? p.agent : "generic", sid = `${agent}:${p.session_id}`;
|
|
5516
|
+
if (this.store.revoked(sid)) return {};
|
|
4768
5517
|
let s = this.store.session(sid);
|
|
4769
5518
|
if (p.event === "SessionEnd" && !s) return {};
|
|
4770
5519
|
if (p.event === "SessionStart") {
|
|
@@ -4787,6 +5536,16 @@ ${join_prompt}` : `Joined squad ${q.id}${s.name ? ` as ${s.name}` : ""}; report
|
|
|
4787
5536
|
if (!s) return {};
|
|
4788
5537
|
const now = Date.now();
|
|
4789
5538
|
s.last_seen_at = now;
|
|
5539
|
+
s.hook_seen_at = now;
|
|
5540
|
+
s.activity_at = now;
|
|
5541
|
+
this.record("session.activity", s.squad_id, {
|
|
5542
|
+
to_sid: s.sid,
|
|
5543
|
+
reason: p.event,
|
|
5544
|
+
data: {
|
|
5545
|
+
activity: p.event === "Stop" ? "idle" : p.event === "SessionEnd" ? s.activity : "busy",
|
|
5546
|
+
presence: p.event === "SessionEnd" ? "offline" : s.presence
|
|
5547
|
+
}
|
|
5548
|
+
});
|
|
4790
5549
|
if (p.event === "SessionEnd") {
|
|
4791
5550
|
s.presence = "offline";
|
|
4792
5551
|
s.ended_at = now;
|
|
@@ -4794,11 +5553,11 @@ ${join_prompt}` : `Joined squad ${q.id}${s.name ? ` as ${s.name}` : ""}; report
|
|
|
4794
5553
|
return {};
|
|
4795
5554
|
}
|
|
4796
5555
|
s.activity = p.event === "Stop" ? "idle" : "busy";
|
|
4797
|
-
const queue = this.
|
|
5556
|
+
const queue = this.inbox(s), max = Math.max(0, ...queue.map((m) => m.seq));
|
|
4798
5557
|
const groups = /* @__PURE__ */ new Map();
|
|
4799
5558
|
for (const m of queue) {
|
|
4800
5559
|
const sender = (m.from_name || m.from_role).replace(/[\r\n\t]/g, " ").slice(0, 30);
|
|
4801
|
-
const label = `${m.type} (${["high", "normal", "low"][m.priority]}) from ${sender}`;
|
|
5560
|
+
const label = `${m.type} (${m.priority < 0 ? "urgent" : ["high", "normal", "low"][m.priority]}) from ${sender}`;
|
|
4802
5561
|
groups.set(label, (groups.get(label) || 0) + 1);
|
|
4803
5562
|
}
|
|
4804
5563
|
const details = [...groups].slice(0, 3).map(([label, n]) => `${n} ${label}`).join(", ").slice(0, 175);
|
|
@@ -4816,7 +5575,7 @@ ${join_prompt}` : `Joined squad ${q.id}${s.name ? ` as ${s.name}` : ""}; report
|
|
|
4816
5575
|
0,
|
|
4817
5576
|
300
|
|
4818
5577
|
);
|
|
4819
|
-
} else if (queue.length && (max > s.last_notified_seq || queue.some((m) => m.priority
|
|
5578
|
+
} else if (queue.length && (max > s.last_notified_seq || queue.some((m) => m.priority <= 0) && now - s.last_notified_at >= this.config.remindIntervalSec * 1e3))
|
|
4820
5579
|
result.inject = summary.slice(0, 300);
|
|
4821
5580
|
if (result.inject) {
|
|
4822
5581
|
s.last_notified_seq = max;
|
|
@@ -4830,8 +5589,10 @@ ${join_prompt}` : `Joined squad ${q.id}${s.name ? ` as ${s.name}` : ""}; report
|
|
|
4830
5589
|
this.atomic(() => {
|
|
4831
5590
|
const cutoff = Date.now() - this.config.ttlDays * 864e5;
|
|
4832
5591
|
this.store.expireMessages(all ? Number.MAX_SAFE_INTEGER : cutoff);
|
|
5592
|
+
this.store.expireEvents(all ? Number.MAX_SAFE_INTEGER : cutoff);
|
|
5593
|
+
if (all) this.store.purge();
|
|
4833
5594
|
for (const s of this.store.sessions())
|
|
4834
|
-
if (all || s.presence
|
|
5595
|
+
if (all || !s.squad_id && s.presence !== "online" && s.last_seen_at < cutoff && !this.store.commands(s.sid).length) {
|
|
4835
5596
|
if (s.squad_id && s.role === "commander") {
|
|
4836
5597
|
const q = this.store.squad(s.squad_id);
|
|
4837
5598
|
q.commander_sid = null;
|
|
@@ -4852,7 +5613,7 @@ ${join_prompt}` : `Joined squad ${q.id}${s.name ? ` as ${s.name}` : ""}; report
|
|
|
4852
5613
|
this.store.deleteSession(s.sid);
|
|
4853
5614
|
}
|
|
4854
5615
|
for (const q of this.store.squads())
|
|
4855
|
-
if (all || q.status
|
|
5616
|
+
if (all || q.status === "dissolved" && q.updated_at < cutoff && !this.members(q.id).length && !this.store.commands().some((m) => m.squad_id === q.id))
|
|
4856
5617
|
this.store.deleteSquad(q.id);
|
|
4857
5618
|
});
|
|
4858
5619
|
if (all)
|
|
@@ -4891,24 +5652,62 @@ ${join_prompt}` : `Joined squad ${q.id}${s.name ? ` as ${s.name}` : ""}; report
|
|
|
4891
5652
|
identity: "provisional"
|
|
4892
5653
|
})),
|
|
4893
5654
|
squads: this.store.squads().length,
|
|
4894
|
-
connections: [...this.contexts].filter((c) => c.kind === "mcp").length
|
|
5655
|
+
connections: [...this.contexts].filter((c) => c.kind === "mcp").length,
|
|
5656
|
+
clients: [...this.contexts].map((c) => ({
|
|
5657
|
+
sid: c.sid,
|
|
5658
|
+
kind: c.kind,
|
|
5659
|
+
version: c.version,
|
|
5660
|
+
client: c.client
|
|
5661
|
+
})),
|
|
5662
|
+
listeners: this.store.standbys().map((s) => this.standbyView(s.sid))
|
|
4895
5663
|
};
|
|
4896
5664
|
if (method === "admin.housekeep" || method === "admin.purge")
|
|
4897
5665
|
return this.housekeep(method === "admin.purge" && params.all === true);
|
|
4898
|
-
if (method === "admin.peek")
|
|
5666
|
+
if (method === "admin.peek")
|
|
5667
|
+
return { messages: this.inbox(this.store.session(params.sid) || fail("NOT_JOINED")) };
|
|
4899
5668
|
if (method === "admin.read") {
|
|
4900
5669
|
const s = this.store.session(params.sid) || fail("NOT_JOINED");
|
|
4901
5670
|
return this.atomic(
|
|
4902
5671
|
() => this.readNow({ ...ctx, sid: s.sid }, parse("read", params.options || {}))
|
|
4903
5672
|
);
|
|
4904
5673
|
}
|
|
4905
|
-
if (method === "admin.tail") {
|
|
4906
|
-
|
|
4907
|
-
|
|
5674
|
+
if (method === "admin.tail" || method === "admin.events") {
|
|
5675
|
+
const after = params.after === void 0 ? Math.max(0, this.store.eventCursor() - 20) : params.after;
|
|
5676
|
+
if (!Number.isSafeInteger(after) || after < 0)
|
|
5677
|
+
fail("INVALID_ARGUMENT", "after must be a nonnegative event_seq");
|
|
5678
|
+
const high = this.store.eventCursor();
|
|
5679
|
+
if (after > high)
|
|
5680
|
+
fail(
|
|
5681
|
+
"CURSOR_AHEAD",
|
|
5682
|
+
"Cursor is ahead of this database; verify CMDR_HOME or restart from --after 0"
|
|
5683
|
+
);
|
|
5684
|
+
const recipient = params.for ? this.store.session(params.for) : void 0;
|
|
5685
|
+
const candidates = this.store.eventPage(after, {
|
|
5686
|
+
squad: params.squad,
|
|
5687
|
+
to: params.for,
|
|
5688
|
+
roleInbox: recipient?.role === "commander" ? `squad:${recipient.squad_id}` : void 0
|
|
5689
|
+
});
|
|
5690
|
+
const events = bounded(
|
|
5691
|
+
params.full ? candidates : candidates.map((e) => ({
|
|
5692
|
+
...e,
|
|
5693
|
+
message: e.message ? { ...e.message, body: e.message.body.slice(0, 160), data: null } : void 0
|
|
5694
|
+
})),
|
|
5695
|
+
100
|
|
5696
|
+
);
|
|
5697
|
+
const next = events.length < candidates.length ? events[events.length - 1].event_seq : high;
|
|
5698
|
+
if (method === "admin.tail")
|
|
5699
|
+
ctx.tail = { squad: params.squad, for: params.for, full: params.full, after: high };
|
|
5700
|
+
return {
|
|
5701
|
+
events,
|
|
5702
|
+
next,
|
|
5703
|
+
high,
|
|
5704
|
+
gap: after < this.store.eventFloor(),
|
|
5705
|
+
retained_after: this.store.eventFloor()
|
|
5706
|
+
};
|
|
4908
5707
|
}
|
|
4909
5708
|
if (method === "admin.recent")
|
|
4910
5709
|
return {
|
|
4911
|
-
messages: this.store.messages().filter((m) => !params.squad || m.squad_id === params.squad).slice(-
|
|
5710
|
+
messages: this.store.messages().filter((m) => !params.squad || m.squad_id === params.squad).slice(-20)
|
|
4912
5711
|
};
|
|
4913
5712
|
fail("INVALID_ARGUMENT", `Unknown method ${method}`);
|
|
4914
5713
|
}
|
|
@@ -4930,13 +5729,12 @@ ${join_prompt}` : `Joined squad ${q.id}${s.name ? ` as ${s.name}` : ""}; report
|
|
|
4930
5729
|
if (method === "msg.history") p.history = true;
|
|
4931
5730
|
if (ctx.sid) {
|
|
4932
5731
|
const s = this.required(ctx);
|
|
4933
|
-
s.activity = "busy";
|
|
4934
5732
|
s.last_seen_at = Date.now();
|
|
4935
5733
|
this.store.saveSession(s);
|
|
4936
5734
|
}
|
|
4937
5735
|
if (tool === "read") {
|
|
4938
5736
|
this.required(ctx);
|
|
4939
|
-
if (!p.history && !p.since && !this.
|
|
5737
|
+
if (!p.history && !p.since && !p.recover && !p.id && !this.inbox(this.required(ctx)).length)
|
|
4940
5738
|
await this.wait(ctx, p.wait, void 0, signal);
|
|
4941
5739
|
if (signal?.aborted) fail("REQUEST_CANCELLED");
|
|
4942
5740
|
if (ctx.closed) fail("DAEMON_UNAVAILABLE");
|
|
@@ -4967,6 +5765,17 @@ ${join_prompt}` : `Joined squad ${q.id}${s.name ? ` as ${s.name}` : ""}; report
|
|
|
4967
5765
|
return this.reportOrAsk(ctx, p, false);
|
|
4968
5766
|
}
|
|
4969
5767
|
});
|
|
5768
|
+
if (tool === "join" && p.standby && this.configureStandby) {
|
|
5769
|
+
return this.envelope(ctx, {
|
|
5770
|
+
...result,
|
|
5771
|
+
standby: this.required(ctx).native_id ? this.configureStandby(ctx.sid, p.standby) : {
|
|
5772
|
+
wake_mode: "manual",
|
|
5773
|
+
health: "manual",
|
|
5774
|
+
can_auto_respond: false,
|
|
5775
|
+
reason: "A confirmed native session ID is required for managed standby; membership is retained."
|
|
5776
|
+
}
|
|
5777
|
+
});
|
|
5778
|
+
}
|
|
4970
5779
|
return this.envelope(ctx, result);
|
|
4971
5780
|
}
|
|
4972
5781
|
};
|
|
@@ -4986,7 +5795,58 @@ var Store = class {
|
|
|
4986
5795
|
CREATE TABLE IF NOT EXISTS messages(seq INTEGER PRIMARY KEY AUTOINCREMENT, id TEXT NOT NULL UNIQUE, to_sid TEXT NOT NULL, status TEXT NOT NULL, priority INTEGER NOT NULL, reply_to TEXT, created_at INTEGER NOT NULL, payload TEXT NOT NULL);
|
|
4987
5796
|
CREATE INDEX IF NOT EXISTS queue ON messages(to_sid,status,priority,seq);
|
|
4988
5797
|
CREATE INDEX IF NOT EXISTS reply ON messages(reply_to);
|
|
4989
|
-
CREATE INDEX IF NOT EXISTS created ON messages(created_at)
|
|
5798
|
+
CREATE INDEX IF NOT EXISTS created ON messages(created_at);
|
|
5799
|
+
CREATE TABLE IF NOT EXISTS events(event_seq INTEGER PRIMARY KEY AUTOINCREMENT, at INTEGER NOT NULL, payload TEXT NOT NULL);
|
|
5800
|
+
CREATE TABLE IF NOT EXISTS metadata(key TEXT PRIMARY KEY, value INTEGER NOT NULL);
|
|
5801
|
+
CREATE TABLE IF NOT EXISTS standby(sid TEXT PRIMARY KEY, payload TEXT NOT NULL);
|
|
5802
|
+
CREATE TABLE IF NOT EXISTS revoked(sid TEXT PRIMARY KEY, replacement TEXT NOT NULL);`);
|
|
5803
|
+
this.migrateWork();
|
|
5804
|
+
}
|
|
5805
|
+
migrateWork() {
|
|
5806
|
+
const legacy = this.unpack(
|
|
5807
|
+
this.db.prepare(
|
|
5808
|
+
"SELECT payload FROM messages WHERE json_extract(payload,'$.type')='command' AND json_extract(payload,'$.work') IS NULL"
|
|
5809
|
+
).all()
|
|
5810
|
+
);
|
|
5811
|
+
if (!legacy.length) return;
|
|
5812
|
+
this.transaction(() => {
|
|
5813
|
+
for (const m of legacy) {
|
|
5814
|
+
m.work = {
|
|
5815
|
+
state: m.status === "queued" ? "queued" : "read",
|
|
5816
|
+
updated_at: m.delivered_at || m.created_at
|
|
5817
|
+
};
|
|
5818
|
+
const reports = this.unpack(
|
|
5819
|
+
this.db.prepare("SELECT payload FROM messages WHERE reply_to=? ORDER BY seq").all(m.id)
|
|
5820
|
+
);
|
|
5821
|
+
for (const report of reports) {
|
|
5822
|
+
if (report.type !== "report" || report.from_sid !== m.to_sid || report.squad_id !== m.squad_id || terminalWork(m))
|
|
5823
|
+
continue;
|
|
5824
|
+
const state = {
|
|
5825
|
+
working: "accepted",
|
|
5826
|
+
blocked: "accepted",
|
|
5827
|
+
done: "completed",
|
|
5828
|
+
failed: "failed",
|
|
5829
|
+
cancelled: "cancelled"
|
|
5830
|
+
}[String(report.data?.status)];
|
|
5831
|
+
if (!state) continue;
|
|
5832
|
+
m.work.state = state;
|
|
5833
|
+
m.work.updated_at = report.created_at;
|
|
5834
|
+
if (state === "accepted") m.work.accepted_at ||= report.created_at;
|
|
5835
|
+
m.status = "delivered";
|
|
5836
|
+
m.delivered_at ||= report.created_at;
|
|
5837
|
+
}
|
|
5838
|
+
this.saveMessage(m);
|
|
5839
|
+
this.appendEvent({
|
|
5840
|
+
at: Date.now(),
|
|
5841
|
+
kind: "work.migrated",
|
|
5842
|
+
channel: m.squad_id,
|
|
5843
|
+
message_id: m.id,
|
|
5844
|
+
to_sid: m.to_sid,
|
|
5845
|
+
from_sid: m.from_sid,
|
|
5846
|
+
message: m
|
|
5847
|
+
});
|
|
5848
|
+
}
|
|
5849
|
+
});
|
|
4990
5850
|
}
|
|
4991
5851
|
transaction(fn) {
|
|
4992
5852
|
this.db.exec("BEGIN IMMEDIATE");
|
|
@@ -5072,7 +5932,89 @@ var Store = class {
|
|
|
5072
5932
|
this.db.prepare("DELETE FROM messages WHERE id=?").run(id);
|
|
5073
5933
|
}
|
|
5074
5934
|
expireMessages(before) {
|
|
5075
|
-
|
|
5935
|
+
const active = this.commands();
|
|
5936
|
+
const unfinished = new Set(active.map((m) => m.id));
|
|
5937
|
+
const dependencies = new Set(active.map((m) => m.blocked_by));
|
|
5938
|
+
for (const m of this.messages())
|
|
5939
|
+
if (m.created_at < before && !dependencies.has(m.id) && !(m.type === "cancel" && m.reply_to && unfinished.has(m.reply_to)) && (m.type !== "command" || terminalWork(m)))
|
|
5940
|
+
this.deleteMessage(m.id);
|
|
5941
|
+
}
|
|
5942
|
+
commands(sid) {
|
|
5943
|
+
return this.messages().filter(
|
|
5944
|
+
(m) => m.type === "command" && (!sid || m.to_sid === sid) && !terminalWork(m)
|
|
5945
|
+
);
|
|
5946
|
+
}
|
|
5947
|
+
appendEvent(event) {
|
|
5948
|
+
const r = this.db.prepare("INSERT INTO events(at,payload) VALUES (?,?)").run(event.at, JSON.stringify(event));
|
|
5949
|
+
const value = { ...event, event_seq: Number(r.lastInsertRowid) };
|
|
5950
|
+
this.db.prepare("UPDATE events SET payload=? WHERE event_seq=?").run(JSON.stringify(value), value.event_seq);
|
|
5951
|
+
return value;
|
|
5952
|
+
}
|
|
5953
|
+
eventCursor() {
|
|
5954
|
+
return Number(
|
|
5955
|
+
this.db.prepare("SELECT seq FROM sqlite_sequence WHERE name='events'").get()?.seq || 0
|
|
5956
|
+
);
|
|
5957
|
+
}
|
|
5958
|
+
events(after = 0) {
|
|
5959
|
+
return this.unpack(
|
|
5960
|
+
this.db.prepare("SELECT payload FROM events WHERE event_seq>? ORDER BY event_seq").all(after)
|
|
5961
|
+
);
|
|
5962
|
+
}
|
|
5963
|
+
eventPage(after, filter) {
|
|
5964
|
+
return this.unpack(
|
|
5965
|
+
this.db.prepare(
|
|
5966
|
+
`SELECT payload FROM events WHERE event_seq>?
|
|
5967
|
+
AND (? IS NULL OR json_extract(payload,'$.channel')=?)
|
|
5968
|
+
AND (? IS NULL OR json_extract(payload,'$.to_sid')=? OR json_extract(payload,'$.to_sid')=?)
|
|
5969
|
+
ORDER BY event_seq LIMIT 101`
|
|
5970
|
+
).all(
|
|
5971
|
+
after,
|
|
5972
|
+
filter.squad || null,
|
|
5973
|
+
filter.squad || null,
|
|
5974
|
+
filter.to || null,
|
|
5975
|
+
filter.to || null,
|
|
5976
|
+
filter.roleInbox || null
|
|
5977
|
+
)
|
|
5978
|
+
);
|
|
5979
|
+
}
|
|
5980
|
+
eventFloor() {
|
|
5981
|
+
return Number(
|
|
5982
|
+
this.db.prepare("SELECT value FROM metadata WHERE key='event_floor'").get()?.value || 0
|
|
5983
|
+
);
|
|
5984
|
+
}
|
|
5985
|
+
expireEvents(before) {
|
|
5986
|
+
const last = Number(
|
|
5987
|
+
this.db.prepare("SELECT MAX(event_seq) AS seq FROM events WHERE at<?").get(before)?.seq || 0
|
|
5988
|
+
);
|
|
5989
|
+
if (last) {
|
|
5990
|
+
this.db.prepare(
|
|
5991
|
+
"INSERT INTO metadata VALUES ('event_floor',?) ON CONFLICT(key) DO UPDATE SET value=MAX(value,excluded.value)"
|
|
5992
|
+
).run(last);
|
|
5993
|
+
this.db.prepare("DELETE FROM events WHERE event_seq<=?").run(last);
|
|
5994
|
+
}
|
|
5995
|
+
}
|
|
5996
|
+
standbys() {
|
|
5997
|
+
return this.unpack(this.db.prepare("SELECT payload FROM standby").all());
|
|
5998
|
+
}
|
|
5999
|
+
standby(sid) {
|
|
6000
|
+
return this.standbys().find((s) => s.sid === sid);
|
|
6001
|
+
}
|
|
6002
|
+
saveStandby(s) {
|
|
6003
|
+
this.db.prepare(
|
|
6004
|
+
"INSERT INTO standby VALUES (?,?) ON CONFLICT(sid) DO UPDATE SET payload=excluded.payload"
|
|
6005
|
+
).run(s.sid, JSON.stringify(s));
|
|
6006
|
+
}
|
|
6007
|
+
deleteStandby(sid) {
|
|
6008
|
+
this.db.prepare("DELETE FROM standby WHERE sid=?").run(sid);
|
|
6009
|
+
}
|
|
6010
|
+
revoke(sid, replacement) {
|
|
6011
|
+
this.db.prepare("INSERT OR REPLACE INTO revoked VALUES (?,?)").run(sid, replacement);
|
|
6012
|
+
}
|
|
6013
|
+
revoked(sid) {
|
|
6014
|
+
return !!this.db.prepare("SELECT sid FROM revoked WHERE sid=?").get(sid)?.sid;
|
|
6015
|
+
}
|
|
6016
|
+
purge() {
|
|
6017
|
+
this.db.exec("DELETE FROM messages; DELETE FROM standby; DELETE FROM revoked;");
|
|
5076
6018
|
}
|
|
5077
6019
|
close() {
|
|
5078
6020
|
this.db.close();
|
|
@@ -5146,14 +6088,14 @@ function logger(path) {
|
|
|
5146
6088
|
import { homedir as homedir2, tmpdir } from "node:os";
|
|
5147
6089
|
import { join as join3, resolve } from "node:path";
|
|
5148
6090
|
import { mkdirSync, chmodSync } from "node:fs";
|
|
5149
|
-
import { createHash } from "node:crypto";
|
|
6091
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
5150
6092
|
function paths(home = process.env.CMDR_HOME || join3(homedir2(), ".cmdr")) {
|
|
5151
6093
|
home = resolve(home);
|
|
5152
6094
|
let socket = join3(home, "cmdr.sock");
|
|
5153
6095
|
if (Buffer.byteLength(socket) > 100)
|
|
5154
6096
|
socket = join3(
|
|
5155
6097
|
tmpdir(),
|
|
5156
|
-
`cmdr-${
|
|
6098
|
+
`cmdr-${createHash2("sha256").update(`${process.getuid?.()}:${home}`).digest("hex").slice(0, 20)}.sock`
|
|
5157
6099
|
);
|
|
5158
6100
|
return {
|
|
5159
6101
|
home,
|
|
@@ -5331,8 +6273,16 @@ var Rpc = class extends EventEmitter {
|
|
|
5331
6273
|
};
|
|
5332
6274
|
|
|
5333
6275
|
// src/shared/version.ts
|
|
5334
|
-
var
|
|
6276
|
+
var MIN_CLIENT_VERSION = "0.2.0";
|
|
6277
|
+
var VERSION = true ? "0.2.0" : MIN_CLIENT_VERSION;
|
|
5335
6278
|
var PROTOCOL = 1;
|
|
6279
|
+
function newer(a, b) {
|
|
6280
|
+
const x = a.split(".").map(Number), y = b.split(".").map(Number);
|
|
6281
|
+
for (let i = 0; i < 3; i++) {
|
|
6282
|
+
if (x[i] !== y[i]) return x[i] > y[i];
|
|
6283
|
+
}
|
|
6284
|
+
return false;
|
|
6285
|
+
}
|
|
5336
6286
|
|
|
5337
6287
|
// src/daemon/server.ts
|
|
5338
6288
|
async function startDaemon(home) {
|
|
@@ -5341,6 +6291,15 @@ async function startDaemon(home) {
|
|
|
5341
6291
|
prepare(p);
|
|
5342
6292
|
if (!acquireLock(p.lock)) return null;
|
|
5343
6293
|
const log = logger(p.log), store = new Store(p.db), core = new Core(store, p, config(p.config));
|
|
6294
|
+
const standby = new StandbyManager(core);
|
|
6295
|
+
core.configureStandby = (sid, mode) => standby.configure({
|
|
6296
|
+
sid,
|
|
6297
|
+
action: "start",
|
|
6298
|
+
...mode === "manual" ? { adapter: "manual" } : {}
|
|
6299
|
+
});
|
|
6300
|
+
const wakeTimer = setInterval(() => {
|
|
6301
|
+
void standby.tick().catch((e) => log(`standby failed: ${String(e)}`));
|
|
6302
|
+
}, 2e3);
|
|
5344
6303
|
const peers = /* @__PURE__ */ new Set();
|
|
5345
6304
|
let idleSince = Date.now(), stopping = false;
|
|
5346
6305
|
rmSync4(p.socket, { force: true });
|
|
@@ -5363,10 +6322,29 @@ async function startDaemon(home) {
|
|
|
5363
6322
|
"PROTOCOL_MISMATCH",
|
|
5364
6323
|
`Protocol mismatch: client ${String(params.protocol).slice(0, 20)}, daemon ${PROTOCOL} (${VERSION}). Update/reinstall the plugin cache, restart the daemon with the matching cmdr installation, then restart the host session.`
|
|
5365
6324
|
);
|
|
6325
|
+
const clientVersion = typeof params.version === "string" ? params.version.match(/^(\d+\.\d+\.\d+)(?:-[\da-zA-Z.-]+)?(?:\+[\da-zA-Z.-]+)?$/)?.[1] : void 0;
|
|
6326
|
+
if (!clientVersion || newer(MIN_CLIENT_VERSION, clientVersion))
|
|
6327
|
+
fail(
|
|
6328
|
+
"PROTOCOL_MISMATCH",
|
|
6329
|
+
`Daemon ${VERSION} requires client ${MIN_CLIENT_VERSION} or newer for compatible tool semantics. Update/reinstall the plugin cache and restart the host MCP connection.`
|
|
6330
|
+
);
|
|
5366
6331
|
greeted = true;
|
|
6332
|
+
ctx.version = String(params.version || "unknown").slice(0, 80);
|
|
6333
|
+
ctx.client = String(params.client || "unknown").slice(0, 80);
|
|
5367
6334
|
return { version: VERSION, protocol: PROTOCOL };
|
|
5368
6335
|
}
|
|
6336
|
+
if (method === "admin.standby") {
|
|
6337
|
+
if (ctx.kind !== "cli") fail("ROLE_NOT_ALLOWED");
|
|
6338
|
+
const result2 = standby.configure(params);
|
|
6339
|
+
void standby.tick();
|
|
6340
|
+
return result2;
|
|
6341
|
+
}
|
|
5369
6342
|
if (method === "admin.shutdown") {
|
|
6343
|
+
if (params.reason === "upgrade")
|
|
6344
|
+
fail(
|
|
6345
|
+
"UPGRADE_REQUIRES_RESTART",
|
|
6346
|
+
"Automatic replacement is disabled. Run cmdr daemon restart from the new installation after preflight."
|
|
6347
|
+
);
|
|
5370
6348
|
if (ctx.kind !== "cli" && params.reason !== "upgrade") fail("ROLE_NOT_ALLOWED");
|
|
5371
6349
|
log(
|
|
5372
6350
|
`shutdown requested: ${String(params.reason || "operator").replace(/[^a-z_-]/gi, "").slice(0, 40)}; from=${VERSION}; to=${String(params.version || "unknown").replace(/[^a-z0-9.:-]/gi, "").slice(0, 80)}`
|
|
@@ -5390,10 +6368,13 @@ async function startDaemon(home) {
|
|
|
5390
6368
|
if (stopping) return;
|
|
5391
6369
|
stopping = true;
|
|
5392
6370
|
clearInterval(timer);
|
|
5393
|
-
|
|
6371
|
+
clearInterval(wakeTimer);
|
|
6372
|
+
standby.close();
|
|
6373
|
+
const closed = new Promise((resolve2) => server.close(() => resolve2()));
|
|
5394
6374
|
for (const ctx of [...core.contexts]) core.disconnect(ctx);
|
|
5395
6375
|
core.close();
|
|
5396
6376
|
for (const peer of peers) peer.close();
|
|
6377
|
+
await closed;
|
|
5397
6378
|
store.close();
|
|
5398
6379
|
rmSync4(p.socket, { force: true });
|
|
5399
6380
|
rmSync4(p.info, { force: true });
|
|
@@ -5427,7 +6408,7 @@ async function startDaemon(home) {
|
|
|
5427
6408
|
core.housekeep();
|
|
5428
6409
|
if (existsSync2(p.spawn) && Date.now() - statSync3(p.spawn).mtimeMs > 1e4)
|
|
5429
6410
|
rmSync4(p.spawn, { recursive: true, force: true });
|
|
5430
|
-
if (!peers.size && Date.now() - idleSince >= core.config.idleExitMinutes * 6e4)
|
|
6411
|
+
if (!peers.size && !standby.active && Date.now() - idleSince >= core.config.idleExitMinutes * 6e4)
|
|
5431
6412
|
void stop();
|
|
5432
6413
|
} catch (e) {
|
|
5433
6414
|
log(`housekeeping failed: ${String(e)}`);
|
|
@@ -5445,8 +6426,40 @@ async function startDaemon(home) {
|
|
|
5445
6426
|
}
|
|
5446
6427
|
}
|
|
5447
6428
|
|
|
6429
|
+
// src/daemon/preflight.ts
|
|
6430
|
+
import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
|
|
6431
|
+
import { existsSync as existsSync3, mkdtempSync, rmSync as rmSync5 } from "node:fs";
|
|
6432
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
6433
|
+
import { join as join4 } from "node:path";
|
|
6434
|
+
async function preflight(home) {
|
|
6435
|
+
const dir = mkdtempSync(join4(tmpdir2(), "cmdr-preflight-"));
|
|
6436
|
+
try {
|
|
6437
|
+
const source = paths(home).db, target = join4(dir, "state.db");
|
|
6438
|
+
if (existsSync3(source)) {
|
|
6439
|
+
const db = new DatabaseSync3(source, { readOnly: true });
|
|
6440
|
+
try {
|
|
6441
|
+
db.prepare("VACUUM INTO ?").run(target);
|
|
6442
|
+
} finally {
|
|
6443
|
+
db.close();
|
|
6444
|
+
}
|
|
6445
|
+
}
|
|
6446
|
+
const store = new Store(target);
|
|
6447
|
+
try {
|
|
6448
|
+
store.sessions();
|
|
6449
|
+
store.squads();
|
|
6450
|
+
store.messages();
|
|
6451
|
+
store.standbys();
|
|
6452
|
+
} finally {
|
|
6453
|
+
store.close();
|
|
6454
|
+
}
|
|
6455
|
+
} finally {
|
|
6456
|
+
rmSync5(dir, { recursive: true, force: true });
|
|
6457
|
+
}
|
|
6458
|
+
}
|
|
6459
|
+
|
|
5448
6460
|
// src/daemon/main.ts
|
|
5449
|
-
|
|
6461
|
+
var operation = process.argv.includes("--preflight") ? preflight() : startDaemon();
|
|
6462
|
+
operation.catch((e) => {
|
|
5450
6463
|
process.stderr.write(`cmdr daemon: ${e.message}
|
|
5451
6464
|
`);
|
|
5452
6465
|
process.exitCode = 1;
|