trantor 0.18.114 → 0.18.115
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/plugin.json +1 -1
- package/bin/crew/stage.mjs +9 -0
- package/bin/crew/swap.mjs +24 -8
- package/hub/routes/admin.mjs +12 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trantor",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.115",
|
|
4
4
|
"description": "Trantor — the hub-world for AI agent crews: live message bus, presence, project Kanban/flow board + crew orchestration for independent AI coding agents (Claude, Codex, Gemini, Kimi, DeepSeek)",
|
|
5
5
|
"mcpServers": {
|
|
6
6
|
"relay": {
|
package/bin/crew/stage.mjs
CHANGED
|
@@ -4,6 +4,14 @@ import { loadOrCreate } from "../../lib/identity.mjs";
|
|
|
4
4
|
import { ensureEnrolled } from "../../lib/enroll.mjs";
|
|
5
5
|
import { sfetchJson } from "../../lib/signed-fetch.mjs";
|
|
6
6
|
|
|
7
|
+
export async function unregisterSwapStage(hub, project, session) {
|
|
8
|
+
const identity = loadOrCreate(session, "agent");
|
|
9
|
+
const response = await sfetchJson(`${hub}/unregister`, {
|
|
10
|
+
identity, payload: { session, project }, signal: AbortSignal.timeout(8000),
|
|
11
|
+
});
|
|
12
|
+
if (!response.ok) throw new Error(`replacement deregistration failed: ${response.status}`);
|
|
13
|
+
}
|
|
14
|
+
|
|
7
15
|
// A replacement registers before it touches the old runner's worktree or queue.
|
|
8
16
|
export async function awaitSwapRelease(env = process.env) {
|
|
9
17
|
if (!env.CREW_SWAP_STAGE) return;
|
|
@@ -25,4 +33,5 @@ export async function awaitSwapRelease(env = process.env) {
|
|
|
25
33
|
}
|
|
26
34
|
const handoff = JSON.parse(readFileSync(`${stage}.release`, "utf8"));
|
|
27
35
|
if (handoff.agent !== env.CREW_SWAP_AGENT) throw new Error("swap release names a different seat");
|
|
36
|
+
await unregisterSwapStage(env.RELAY_URL, env.RELAY_PROJECT, session);
|
|
28
37
|
}
|
package/bin/crew/swap.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { unregisterSwapStage } from "./stage.mjs";
|
|
1
2
|
import { updatePersona } from "../../lib/persona.mjs";
|
|
2
3
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
3
4
|
import { join } from "node:path";
|
|
@@ -85,7 +86,7 @@ async function hubRequest(ctx, path, body) {
|
|
|
85
86
|
return response.json;
|
|
86
87
|
}
|
|
87
88
|
|
|
88
|
-
export function swapRuntime(ctx, adapters, spawn) {
|
|
89
|
+
export function swapRuntime(ctx, adapters, spawn, { now = Date.now, pause = setTimeout, requestTimeoutMs = 8000 } = {}) {
|
|
89
90
|
let pids = [];
|
|
90
91
|
let replacementPid;
|
|
91
92
|
return {
|
|
@@ -97,14 +98,23 @@ export function swapRuntime(ctx, adapters, spawn) {
|
|
|
97
98
|
spawn(staged, { ...seat, persona: stage.label });
|
|
98
99
|
},
|
|
99
100
|
async ready(stage) {
|
|
100
|
-
const deadline =
|
|
101
|
+
const deadline = now() + 30000;
|
|
101
102
|
while (!existsSync(`${stage.path}.ready`)) {
|
|
102
|
-
if (
|
|
103
|
-
await
|
|
103
|
+
if (now() > deadline) throw new Error("replacement did not join the bus within 30s; old seat unchanged");
|
|
104
|
+
await pause(100);
|
|
104
105
|
}
|
|
105
106
|
const ready = JSON.parse(readFileSync(`${stage.path}.ready`, "utf8"));
|
|
106
|
-
|
|
107
|
-
|
|
107
|
+
let failure = "status=0 reason=readiness deadline elapsed timedOut=false";
|
|
108
|
+
let visible = false;
|
|
109
|
+
while (now() < deadline) {
|
|
110
|
+
const response = await signedGet(`${ctx.hub}/peers?project=${encodeURIComponent(ctx.project)}`, {
|
|
111
|
+
project: ctx.project, timeoutMs: Math.min(requestTimeoutMs, deadline - now()),
|
|
112
|
+
});
|
|
113
|
+
if (response.ok && response.json?.peers?.some(p => p.session === ready.session)) { visible = true; break; }
|
|
114
|
+
failure = `status=${response.status} reason=${response.reason || response.json?.error || "replacement absent from roster"} timedOut=${response.timedOut === true}`;
|
|
115
|
+
await pause(Math.min(100, Math.max(0, deadline - now())));
|
|
116
|
+
}
|
|
117
|
+
if (!visible) throw new Error(`replacement registration is not visible on the bus: ${failure}`);
|
|
108
118
|
process.kill(ready.pid, 0);
|
|
109
119
|
replacementPid = ready.pid;
|
|
110
120
|
return ready.session;
|
|
@@ -117,7 +127,10 @@ export function swapRuntime(ctx, adapters, spawn) {
|
|
|
117
127
|
await down(ctx, [agent], adapters);
|
|
118
128
|
signalProcesses(pids, "SIGKILL");
|
|
119
129
|
},
|
|
120
|
-
async cancel(stage) {
|
|
130
|
+
async cancel(stage) {
|
|
131
|
+
await down(ctx, [stage.label], adapters);
|
|
132
|
+
await unregisterSwapStage(ctx.hub, ctx.project, `${stage.label}:${ctx.project}`);
|
|
133
|
+
},
|
|
121
134
|
release(stage, persona, cursor) {
|
|
122
135
|
writeRows(ctx, readRows(ctx).map(row => row.project === ctx.project && row.persona === stage.label ? { ...row, persona } : row));
|
|
123
136
|
writeFileSync(runnerRecord(ctx, ctx.project, persona), JSON.stringify({ label: stage.label, dir: ctx.dir }));
|
|
@@ -154,7 +167,10 @@ export async function performSwap(ctx, options, runtime) {
|
|
|
154
167
|
} catch (error) {
|
|
155
168
|
if (!transferred) {
|
|
156
169
|
if (frozen) await runtime.resume();
|
|
157
|
-
if (stage)
|
|
170
|
+
if (stage) {
|
|
171
|
+
try { await runtime.cancel(stage); }
|
|
172
|
+
catch (cleanupError) { error.message += `; replacement cleanup failed: ${cleanupError.message}`; }
|
|
173
|
+
}
|
|
158
174
|
}
|
|
159
175
|
result.reason = error.message;
|
|
160
176
|
if (transferred) result.reason += "; handoff recorded on hub; old seat held for recovery";
|
package/hub/routes/admin.mjs
CHANGED
|
@@ -36,6 +36,18 @@ export async function routeAdmin({ req, res, q, P, auth, ctx }) {
|
|
|
36
36
|
}
|
|
37
37
|
return json(res, 200, keyUsageSnapshot(state, now(), ONLINE_MS));
|
|
38
38
|
}
|
|
39
|
+
if (req.method === "POST" && P === "/unregister") {
|
|
40
|
+
const b = await body(req);
|
|
41
|
+
if (!b.session || !b.project) return json(res, 400, { error: "session and project required" });
|
|
42
|
+
if (AUTH_MODE !== "off" && (auth?.identity?.name !== b.session || !scopeAllows(auth.identity, b.project, "write"))) {
|
|
43
|
+
return json(res, 403, { error: "only the session owner can unregister its peer" });
|
|
44
|
+
}
|
|
45
|
+
const peer = state.peers[b.session];
|
|
46
|
+
if (peer && peer.project !== canon(b.project)) return json(res, 403, { error: "peer belongs to a different project" });
|
|
47
|
+
delete state.peers[b.session];
|
|
48
|
+
markDirty();
|
|
49
|
+
return json(res, 200, { ok: true });
|
|
50
|
+
}
|
|
39
51
|
if (req.method === "POST" && P === "/register") {
|
|
40
52
|
const b = await body(req);
|
|
41
53
|
const cpg = crossProjectGuard(auth, P, b);
|