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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trantor",
3
- "version": "0.18.54",
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": {
@@ -48,8 +48,7 @@ export function paneAlive(ctx, pane) {
48
48
  }
49
49
 
50
50
  export function workspacePane(ctx, workspace, preferredCwd) {
51
- const parsed = parseJsonOutput(herdrCall(ctx, ["pane", "list"]).stdout);
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
- const ids = readRows(ctx).filter(row => row.project === ctx.project && row.kind === "herdrws").map(row => row.handle);
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");
@@ -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
  }
@@ -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
- return message?.kind === "receipt" || Number(message?.re) > 0 || text.startsWith(RECEIPT_MARKER);
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trantor",
3
- "version": "0.18.54",
3
+ "version": "0.18.55",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "trantor": "bin/cli.mjs"