trantor 0.18.54 → 0.18.55
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/herdr.mjs +42 -3
- package/bin/crew/open.mjs +3 -0
- package/bin/crew/state.mjs +3 -0
- package/bin/crew-runner.mjs +14 -2
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trantor",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.55",
|
|
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/herdr.mjs
CHANGED
|
@@ -48,8 +48,7 @@ export function paneAlive(ctx, pane) {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
export function workspacePane(ctx, workspace, preferredCwd) {
|
|
51
|
-
const
|
|
52
|
-
const panes = Array.isArray(parsed) ? parsed : parsed?.panes || parsed?.result?.panes || [];
|
|
51
|
+
const panes = listPanes(ctx);
|
|
53
52
|
const matches = panes.filter(pane => (pane.workspace_id || pane.workspace || "") === workspace);
|
|
54
53
|
const preferred = matches.filter(pane => (pane.cwd || "") === preferredCwd);
|
|
55
54
|
const ordered = [...preferred, ...matches.filter(pane => !preferred.includes(pane))];
|
|
@@ -86,13 +85,47 @@ function runSeat(ctx, pane, agent, command) {
|
|
|
86
85
|
}
|
|
87
86
|
|
|
88
87
|
function trackedWorkspace(ctx) {
|
|
89
|
-
|
|
88
|
+
// #7285: duplicate rows of ONE workspace must dedupe — a takeover used to append a second row of
|
|
89
|
+
// the live workspace, and slice(0,-1) then handed that live workspace to the stale list.
|
|
90
|
+
const ids = [];
|
|
91
|
+
for (const id of readRows(ctx).filter(row => row.project === ctx.project && row.kind === "herdrws").map(row => row.handle)) {
|
|
92
|
+
const at = ids.indexOf(id);
|
|
93
|
+
if (at >= 0) ids.splice(at, 1);
|
|
94
|
+
ids.push(id);
|
|
95
|
+
}
|
|
90
96
|
return { reuse: ids.at(-1) || "", stale: ids.slice(0, -1) };
|
|
91
97
|
}
|
|
92
98
|
|
|
99
|
+
function listPanes(ctx) {
|
|
100
|
+
const parsed = parseJsonOutput(herdrCall(ctx, ["pane", "list"]).stdout);
|
|
101
|
+
return Array.isArray(parsed) ? parsed : parsed?.panes || parsed?.result?.panes || [];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// #7285: the workspaces hosting THIS project's tracked orchestrator pane. Herdr pane ids embed
|
|
105
|
+
// their workspace (#7247 forensics: 'w2:p4Y' lived in w2), so the handle prefix names the home
|
|
106
|
+
// without a round trip; when herdr answers, the pane list is a second opinion (stub ids carry no
|
|
107
|
+
// prefix). Closing such a workspace kills the orchestrator's shell.
|
|
108
|
+
function orchPaneWorkspaces(ctx) {
|
|
109
|
+
const handles = readRows(ctx).filter(row => row.project === ctx.project && row.kind === "orch").map(row => row.handle);
|
|
110
|
+
if (!handles.length) return new Set();
|
|
111
|
+
const spaces = new Set(handles.map(handle => handle.split(":")[0]).filter(Boolean));
|
|
112
|
+
if (ctx.dry) return spaces;
|
|
113
|
+
const tracked = new Set(handles);
|
|
114
|
+
for (const pane of listPanes(ctx)) {
|
|
115
|
+
if (!tracked.has(pane.pane_id || pane.id || "")) continue;
|
|
116
|
+
const space = pane.workspace_id || pane.workspace || "";
|
|
117
|
+
if (space) spaces.add(space);
|
|
118
|
+
}
|
|
119
|
+
return spaces;
|
|
120
|
+
}
|
|
121
|
+
|
|
93
122
|
function prepareWorkspace(ctx, prune) {
|
|
94
123
|
const tracked = trackedWorkspace(ctx);
|
|
124
|
+
const orchSpaces = orchPaneWorkspaces(ctx);
|
|
95
125
|
for (const id of tracked.stale) {
|
|
126
|
+
// #7285: never close reuse itself, nor a workspace hosting the tracked orch pane — `up` once
|
|
127
|
+
// killed its own orchestrator that way. The row stays: the workspace is still live.
|
|
128
|
+
if (id === tracked.reuse || orchSpaces.has(id)) continue;
|
|
96
129
|
console.log(` → closing stale stacked crew workspace for ${ctx.project} (${id})`);
|
|
97
130
|
closeWorkspace(ctx, id);
|
|
98
131
|
dropState(ctx, ctx.project, "herdrws", "", id);
|
|
@@ -108,6 +141,12 @@ function prepareWorkspace(ctx, prune) {
|
|
|
108
141
|
console.log(` → adopting existing untracked crew workspace for ${ctx.project} (${id})`);
|
|
109
142
|
reuse = id;
|
|
110
143
|
recordState(ctx, ctx.project, "herdrws", "__ws__", id);
|
|
144
|
+
} else if (orchSpaces.has(id)) {
|
|
145
|
+
// #7285: a labeled workspace hosting the tracked orch pane is the orchestrator's home, not a
|
|
146
|
+
// stray — adopt it, never close it (the #7285 rule extends past the stale loop).
|
|
147
|
+
console.log(` → adopting the crew workspace hosting the tracked orchestrator for ${ctx.project} (${id})`);
|
|
148
|
+
reuse = id;
|
|
149
|
+
recordState(ctx, ctx.project, "herdrws", "__ws__", id);
|
|
111
150
|
} else {
|
|
112
151
|
console.log(` → closing stray crew workspace for ${ctx.project} (${id})`);
|
|
113
152
|
closeWorkspace(ctx, id);
|
package/bin/crew/open.mjs
CHANGED
|
@@ -210,6 +210,9 @@ export function openOrchestrator(ctx, args) {
|
|
|
210
210
|
const prior = tracked(ctx, live);
|
|
211
211
|
if (reattach(ctx, prior.workspace, prior.pane, id)) return 0;
|
|
212
212
|
const chosen = chooseWorkspace(ctx, live, prior.workspace);
|
|
213
|
+
// #7285: ONE herdrws row per project — re-record exactly like the orch row below, never append.
|
|
214
|
+
// The appended duplicate made the next `up` close the "stale" copy of the live workspace.
|
|
215
|
+
dropState(ctx, ctx.project, "herdrws");
|
|
213
216
|
recordState(ctx, ctx.project, "herdrws", "__ws__", chosen.workspace);
|
|
214
217
|
const pane = hostPane(ctx, chosen, id);
|
|
215
218
|
dropState(ctx, ctx.project, "orch");
|
package/bin/crew/state.mjs
CHANGED
|
@@ -27,6 +27,9 @@ export function writeRows(ctx, rows) {
|
|
|
27
27
|
export function recordState(ctx, project, kind, agent, handle) {
|
|
28
28
|
if (ctx.dry) return;
|
|
29
29
|
const rows = readRows(ctx);
|
|
30
|
+
// #7285: an exact duplicate carries no new fact, and a doubled herdrws row is how `up` came to
|
|
31
|
+
// see the live workspace as its own stale copy. One row per fact — refuse the push.
|
|
32
|
+
if (rows.some(row => row.project === project && row.kind === kind && row.agent === agent && row.handle === handle)) return;
|
|
30
33
|
rows.push({ project, kind, agent, handle });
|
|
31
34
|
writeRows(ctx, rows);
|
|
32
35
|
}
|
package/bin/crew-runner.mjs
CHANGED
|
@@ -498,7 +498,10 @@ async function notifyAssigners(pairs, text) {
|
|
|
498
498
|
// right one instead of guessing from timing.
|
|
499
499
|
const payload = { from: SESSION, to: f, text: text.slice(0, 280), project: PROJ, kind: "receipt" };
|
|
500
500
|
if (id) payload.re = id;
|
|
501
|
-
await api("/send", payload).catch(() => {});
|
|
501
|
+
const ack = await api("/send", payload).catch(() => ({}));
|
|
502
|
+
// #7288: remember the id, so a reply threaded onto THIS outcome can later be told apart from a
|
|
503
|
+
// work order that merely rides `re`.
|
|
504
|
+
if (Number(ack?.id) > 0) sentOutcomes.add(Number(ack.id));
|
|
502
505
|
}
|
|
503
506
|
if (seen.size) log(`reported outcome to ${[...seen].join(", ")}`);
|
|
504
507
|
}
|
|
@@ -908,11 +911,20 @@ function composedTurn({ base = "", wakeText = "", ctxText = "", againText = "",
|
|
|
908
911
|
const RECEIPT_MARKER = "✅ done on";
|
|
909
912
|
const CARD_REF_RE = /#\d{1,7}(?!\d)/;
|
|
910
913
|
|
|
914
|
+
// Outcome ids this seat SENT (notifyAssigners records them): a reply threaded onto one of these is
|
|
915
|
+
// about our own outcome, not a new contract.
|
|
916
|
+
const sentOutcomes = new Set();
|
|
917
|
+
|
|
911
918
|
// Runner-authored metadata is bus state, not work. Typed messages are authoritative; `re` and the
|
|
912
919
|
// stable text marker keep a mixed-version crew safe while older runners are still on the bus.
|
|
913
920
|
function isReceipt(message) {
|
|
914
921
|
const text = String(message?.text || "").trimStart();
|
|
915
|
-
|
|
922
|
+
if (message?.kind === "receipt" || text.startsWith(RECEIPT_MARKER)) return true;
|
|
923
|
+
// #7288: `re` alone is NOT a receipt — senders are told to thread EVERY reply, so a corrected
|
|
924
|
+
// contract riding `re` must wake. A reply is a receipt only when it threads an outcome THIS seat
|
|
925
|
+
// sent and its own text carries no work (an ack/closure, #7079).
|
|
926
|
+
if (!(Number(message?.re) > 0)) return false;
|
|
927
|
+
return sentOutcomes.has(Number(message.re)) && !carriesWork(text) && !isContract(message);
|
|
916
928
|
}
|
|
917
929
|
|
|
918
930
|
function isStatusBroadcast(message) {
|