trantor 0.18.115 → 0.18.116
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 +4 -0
- package/bin/crew/state.mjs +30 -3
- package/bin/crew/swap.mjs +17 -10
- package/hub/routes/admin.mjs +6 -4
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trantor",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.116",
|
|
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
|
@@ -3,6 +3,7 @@ import { setTimeout } from "node:timers/promises";
|
|
|
3
3
|
import { loadOrCreate } from "../../lib/identity.mjs";
|
|
4
4
|
import { ensureEnrolled } from "../../lib/enroll.mjs";
|
|
5
5
|
import { sfetchJson } from "../../lib/signed-fetch.mjs";
|
|
6
|
+
import { personaName } from "../../lib/persona.mjs";
|
|
6
7
|
|
|
7
8
|
export async function unregisterSwapStage(hub, project, session) {
|
|
8
9
|
const identity = loadOrCreate(session, "agent");
|
|
@@ -33,5 +34,8 @@ export async function awaitSwapRelease(env = process.env) {
|
|
|
33
34
|
}
|
|
34
35
|
const handoff = JSON.parse(readFileSync(`${stage}.release`, "utf8"));
|
|
35
36
|
if (handoff.agent !== env.CREW_SWAP_AGENT) throw new Error("swap release names a different seat");
|
|
37
|
+
const persona = personaName(handoff.persona);
|
|
38
|
+
if (env.CREW_PERSONA && env.CREW_PERSONA !== persona) throw new Error("swap release names a different persona");
|
|
39
|
+
env.CREW_PERSONA = persona;
|
|
36
40
|
await unregisterSwapStage(env.RELAY_URL, env.RELAY_PROJECT, session);
|
|
37
41
|
}
|
package/bin/crew/state.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { dirname, join } from "node:path";
|
|
|
3
3
|
import { seatProcesses, signalProcesses } from "./processes.mjs";
|
|
4
4
|
import { call, listPids, run } from "./core.mjs";
|
|
5
5
|
import { hostId } from "../../lib/project.mjs";
|
|
6
|
+
import { signedPost } from "../../hooks/lib/api.mjs";
|
|
6
7
|
import { statePathFor as dockerStatePathFor, sweep as dockerSweep } from "../../lib/docker-janitor.mjs";
|
|
7
8
|
|
|
8
9
|
export function parseRow(line) {
|
|
@@ -122,15 +123,22 @@ function closeTmux(ctx, row, perSeat, seen) {
|
|
|
122
123
|
run(ctx, "tmux", ["kill-session", "-t", session], { rendered: `tmux kill-session -t '${session}' 2>/dev/null` });
|
|
123
124
|
}
|
|
124
125
|
|
|
125
|
-
|
|
126
|
+
async function unregisterSeat(ctx, project, persona) {
|
|
127
|
+
const path = project === ctx.project ? `${ctx.hub}/unregister` : "/unregister";
|
|
128
|
+
const response = await signedPost(path, { session: `${persona}:${project}`, project }, { project });
|
|
129
|
+
if (!response.ok || !response.json?.ok) throw new Error(response.json?.error || response.reason || `hub HTTP ${response.status}`);
|
|
130
|
+
return response.json;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function down(ctx, args, adapters, { unregister = unregisterSeat } = {}) {
|
|
126
134
|
const options = parseDownArgs(ctx, args);
|
|
127
135
|
if (options.help) return 0;
|
|
128
136
|
if (options.error) return 1;
|
|
129
137
|
const rows = readRows(ctx);
|
|
130
|
-
if (!rows.length) { console.log("no tracked crew windows"); return 0; }
|
|
138
|
+
if (!rows.length && !options.agents.length) { console.log("no tracked crew windows"); return 0; }
|
|
131
139
|
const scoped = rows.filter(row => (options.all || row.project === ctx.project) && (!options.agents.length || options.agents.includes(row.persona)));
|
|
132
140
|
const scope = options.all ? "all projects" : `project \"${ctx.project}\"`;
|
|
133
|
-
if (!scoped.length) { console.log(`nothing to tear down for ${scope}`); return 0; }
|
|
141
|
+
if (!scoped.length && !options.agents.length) { console.log(`nothing to tear down for ${scope}`); return 0; }
|
|
134
142
|
console.log(`— tearing down (${scope}):`);
|
|
135
143
|
for (const row of scoped) if (!["attach", "cmuxws", "herdrws", "orch"].includes(row.kind)) console.log(` • ${row.project || "<legacy>"} · ${row.persona} (${row.kind})`);
|
|
136
144
|
if (options.all && !options.yes) {
|
|
@@ -144,6 +152,21 @@ export async function down(ctx, args, adapters) {
|
|
|
144
152
|
const kept = rows.filter(row => !removed.has(`${row.project}|${row.kind}|${row.persona}|${row.handle}`) || (orchProjects.has(row.project) && ["herdrws", "cmuxws", "orch"].includes(row.kind)));
|
|
145
153
|
writeRows(ctx, kept);
|
|
146
154
|
const seatRows = scoped.filter(row => !["attach", "cmuxws", "herdrws", "orch"].includes(row.kind));
|
|
155
|
+
const staleSeats = options.agents.filter(persona => !scoped.some(row => row.persona === persona))
|
|
156
|
+
.map(persona => ({ project: ctx.project, persona, stale: true }));
|
|
157
|
+
const seats = new Map([...seatRows, ...staleSeats].map(row => [`${row.project}/${row.persona}`, row]));
|
|
158
|
+
const failures = [];
|
|
159
|
+
for (const row of seats.values()) {
|
|
160
|
+
if (!row.project) continue;
|
|
161
|
+
if (ctx.dry) { console.log(`[dry] unregister ${row.persona}:${row.project}`); continue; }
|
|
162
|
+
try {
|
|
163
|
+
const result = await unregister(ctx, row.project, row.persona);
|
|
164
|
+
if (row.stale) console.log(result.removed ? ` removed a stale seat row: ${row.persona}:${row.project}` : ` no hub seat row: ${row.persona}:${row.project}`);
|
|
165
|
+
} catch (error) {
|
|
166
|
+
failures.push(`${row.persona}:${row.project}: ${error.message}`);
|
|
167
|
+
console.error(` ${row.persona}:${row.project}: hub row removal failed: ${error.message}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
147
170
|
if (!ctx.dry) {
|
|
148
171
|
const logDir = join(dirname(ctx.statePath), "logs");
|
|
149
172
|
mkdirSync(logDir, { recursive: true });
|
|
@@ -152,6 +175,10 @@ export async function down(ctx, args, adapters) {
|
|
|
152
175
|
`${JSON.stringify({ ts: Date.now(), agent: row.persona, project: row.project, stopped: true })}\n`);
|
|
153
176
|
}
|
|
154
177
|
}
|
|
178
|
+
if (failures.length) {
|
|
179
|
+
console.log(`— hub row removal incomplete; retry Stop: ${failures.join("; ")}`);
|
|
180
|
+
return 1;
|
|
181
|
+
}
|
|
155
182
|
if (options.agents.length) {
|
|
156
183
|
const stopped = [...new Set(seatRows.map(row => row.persona))];
|
|
157
184
|
const remaining = [...new Set(kept.filter(row => row.project === ctx.project &&
|
package/bin/crew/swap.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { unregisterSwapStage } from "./stage.mjs";
|
|
2
|
-
import { updatePersona } from "../../lib/persona.mjs";
|
|
3
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { allocatePersona, updatePersona } from "../../lib/persona.mjs";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
6
|
import { setTimeout } from "node:timers/promises";
|
|
@@ -124,12 +124,15 @@ export function swapRuntime(ctx, adapters, spawn, { now = Date.now, pause = setT
|
|
|
124
124
|
transfer: body => hubRequest(ctx, "/contracts/transfer", body),
|
|
125
125
|
async stop(agent) {
|
|
126
126
|
process.kill(replacementPid, 0);
|
|
127
|
-
await down(ctx, [agent], adapters);
|
|
127
|
+
if (await down(ctx, [agent], adapters)) throw new Error("old seat stopped locally but its hub row could not be removed");
|
|
128
128
|
signalProcesses(pids, "SIGKILL");
|
|
129
129
|
},
|
|
130
130
|
async cancel(stage) {
|
|
131
|
-
await down(ctx, [stage.label], adapters)
|
|
132
|
-
|
|
131
|
+
const code = await down(ctx, [stage.label], adapters, { unregister: async () => {
|
|
132
|
+
await unregisterSwapStage(ctx.hub, ctx.project, `${stage.label}:${ctx.project}`);
|
|
133
|
+
return { removed: true };
|
|
134
|
+
} });
|
|
135
|
+
if (code) throw new Error("replacement hub row could not be removed");
|
|
133
136
|
},
|
|
134
137
|
release(stage, persona, cursor) {
|
|
135
138
|
writeRows(ctx, readRows(ctx).map(row => row.project === ctx.project && row.persona === stage.label ? { ...row, persona } : row));
|
|
@@ -145,24 +148,28 @@ export async function performSwap(ctx, options, runtime) {
|
|
|
145
148
|
let stage, frozen = false, transferred = false;
|
|
146
149
|
try {
|
|
147
150
|
const seat = await runtime.validate(options);
|
|
151
|
+
const occupied = [options.from, seat.agent, ...readRows(ctx).filter(row => row.project === ctx.project).map(row => row.persona)];
|
|
152
|
+
const { persona } = allocatePersona({ dir: join(ctx.home, ".agent-bus"), project: ctx.project, agent: seat.agent, model: seat.model, occupied });
|
|
153
|
+
result.to = persona;
|
|
148
154
|
const label = `${options.agent}-swap-${randomUUID().slice(0, 8)}`;
|
|
149
155
|
mkdirSync(ctx.seatDir, { recursive: true });
|
|
150
|
-
stage = { label, path: join(ctx.seatDir, label), persona
|
|
156
|
+
stage = { label, path: join(ctx.seatDir, label), persona, agent: seat.agent, model: seat.model };
|
|
151
157
|
await runtime.start(seat, stage);
|
|
152
158
|
const readySession = await runtime.ready(stage);
|
|
153
159
|
frozen = true;
|
|
154
160
|
await runtime.freeze(options.from);
|
|
155
161
|
const pending = readPending(ctx, options.from);
|
|
156
|
-
const target =
|
|
157
|
-
const from = `${options.from}:${ctx.project}`, to =
|
|
162
|
+
const target = readPending(ctx, persona);
|
|
163
|
+
const from = `${options.from}:${ctx.project}`, to = `${persona}:${ctx.project}`;
|
|
158
164
|
const handoff = await runtime.transfer({ from, to, project: ctx.project, readySession, _op: `swap-${stage.label}`, heldAsk: pending.ask || 0, pendingIds: pending.wake.map(m => m.id) });
|
|
159
165
|
transferred = true;
|
|
160
166
|
result.moved = [...new Set([...pending.wake.map(m => m.id), ...handoff.moved])];
|
|
161
167
|
const wake = new Map([...target.wake, ...pending.wake, ...handoff.messages].map(m => [m.id, { ...m, to }]));
|
|
162
|
-
savePending(ctx,
|
|
168
|
+
savePending(ctx, persona, { ...target, wake: [...wake.values()], bcast: [...new Map([...target.bcast, ...pending.bcast].map(m => [m.id, m])).values()], seen: [...new Set([...target.seen, ...pending.seen])], ask: handoff.messages.some(m => m.re === pending.ask) ? 0 : pending.ask || target.ask, askTo: pending.askTo || target.askTo });
|
|
163
169
|
await runtime.stop(options.from);
|
|
164
170
|
frozen = false;
|
|
165
|
-
await runtime.release(stage,
|
|
171
|
+
await runtime.release(stage, persona, handoff.cursor);
|
|
172
|
+
if (existsSync(pendingPath(ctx, options.from))) unlinkSync(pendingPath(ctx, options.from));
|
|
166
173
|
return { ...result, ok: true };
|
|
167
174
|
} catch (error) {
|
|
168
175
|
if (!transferred) {
|
package/hub/routes/admin.mjs
CHANGED
|
@@ -39,14 +39,16 @@ export async function routeAdmin({ req, res, q, P, auth, ctx }) {
|
|
|
39
39
|
if (req.method === "POST" && P === "/unregister") {
|
|
40
40
|
const b = await body(req);
|
|
41
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
42
|
const peer = state.peers[b.session];
|
|
46
43
|
if (peer && peer.project !== canon(b.project)) return json(res, 403, { error: "peer belongs to a different project" });
|
|
44
|
+
const self = auth?.identity?.name === b.session && scopeAllows(auth.identity, b.project, "write");
|
|
45
|
+
const owner = scopeAllows(auth?.identity, b.project, "owner") && (!peer || peer.kind === "agent");
|
|
46
|
+
if (AUTH_MODE !== "off" && !self && !owner) {
|
|
47
|
+
return json(res, 403, { error: "only the session itself or a project owner removing an agent can unregister its peer" });
|
|
48
|
+
}
|
|
47
49
|
delete state.peers[b.session];
|
|
48
50
|
markDirty();
|
|
49
|
-
return json(res, 200, { ok: true });
|
|
51
|
+
return json(res, 200, { ok: true, removed: !!peer });
|
|
50
52
|
}
|
|
51
53
|
if (req.method === "POST" && P === "/register") {
|
|
52
54
|
const b = await body(req);
|