trantor 0.18.48 → 0.18.49

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.48",
3
+ "version": "0.18.49",
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": {
@@ -28,7 +28,7 @@ import {
28
28
  senderProjectOf, isLinkedProject,
29
29
  } from "../lib/turn-policy.mjs";
30
30
  import {
31
- auditDutyNudges, dutyNudgeDirective, observedDutyNudgeIds, planDutyNudges,
31
+ auditDutyNudges, claudeTranscriptDir, dutyNudgeDirective, observedDutyNudgeIds, planDutyNudges,
32
32
  } from "../lib/duty-nudges.mjs";
33
33
 
34
34
  const AGENT = process.argv[2];
@@ -341,7 +341,19 @@ const ERRF = join(homedir(), ".agent-bus", `err-${AGENT}-${PROJ}.txt`);
341
341
  const DUTY_NUDGES = process.env.RUNNER_DUTY_NUDGES === "1";
342
342
  const DUTY_NUDGE_STATE = process.env.RUNNER_DUTY_NUDGE_STATE
343
343
  || join(homedir(), ".agent-bus", "duty-nudged.json");
344
- const TRANSCRIPT_DIR = join(homedir(), ".claude", "projects", TURN_DIR.replace(/[^a-zA-Z0-9]/g, "-"));
344
+ const TRANSCRIPT_DIR = claudeTranscriptDir(TURN_DIR, homedir());
345
+
346
+ function startDutyNudgeWatcher(plan, sinceMs) {
347
+ if (!DUTY_NUDGES || !plan.items.length) return () => {};
348
+ const stopPath = join(homedir(), ".agent-bus", `duty-nudge-watch-${process.pid}-${TURN + 1}.stop`);
349
+ try { unlinkSync(stopPath); } catch {}
350
+ const child = spawn(process.execPath, [
351
+ join(import.meta.dirname, "duty-nudge-watch.mjs"), TRANSCRIPT_DIR, DUTY_NUDGE_STATE,
352
+ String(sinceMs), JSON.stringify(plan), stopPath,
353
+ ], { detached: true, stdio: "ignore" });
354
+ child.unref();
355
+ return () => { try { writeFileSync(stopPath, ""); } catch {} };
356
+ }
345
357
 
346
358
  // ---- undelivered wake messages (the runner owns delivery, not the hub) ----
347
359
  // The hub hands a message out exactly ONCE: the poll cursor advances the instant we read it, and
@@ -1094,7 +1106,10 @@ function askedExcerpt(message) {
1094
1106
  tailText: "\nAct on what's addressed to you, then end your turn.\n\n",
1095
1107
  rulesText: RULES, lessons,
1096
1108
  });
1097
- const ec = await runTurn(prompt, fresh, deliveryFails ? `${trigger} (redelivery)` : trigger);
1109
+ const stopDutyNudgeWatcher = startDutyNudgeWatcher(dutyPlan, tStart);
1110
+ let ec;
1111
+ try { ec = await runTurn(prompt, fresh, deliveryFails ? `${trigger} (redelivery)` : trigger); }
1112
+ finally { stopDutyNudgeWatcher(); }
1098
1113
  const secs = Math.round((Date.now() - tStart) / 1000);
1099
1114
  let skippedNudges = [];
1100
1115
  if (!ec && dutyPlan.items.length) {
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, unlinkSync } from "node:fs";
3
+ import { observedDutyNudgeIds, recordDutyNudges } from "../lib/duty-nudges.mjs";
4
+
5
+ const [transcriptDir, statePath, sinceText, planText, stopPath] = process.argv.slice(2);
6
+ const plan = JSON.parse(planText || "{}");
7
+ const sinceMs = Number(sinceText);
8
+ const deadline = Date.now() + 30 * 60 * 1000;
9
+ const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
10
+ let complete = false;
11
+ const recordedIds = new Set();
12
+
13
+ while (Date.now() < deadline) {
14
+ if (!complete) {
15
+ const observedIds = observedDutyNudgeIds(transcriptDir, sinceMs);
16
+ const newIds = new Set([...observedIds].filter(id => !recordedIds.has(id)));
17
+ await recordDutyNudges({ plan, observedIds: newIds, statePath });
18
+ for (const id of newIds) recordedIds.add(id);
19
+ complete = plan.items.every(item => recordedIds.has(item.id));
20
+ }
21
+ if (existsSync(stopPath)) break;
22
+ await sleep(100);
23
+ }
24
+
25
+ try { unlinkSync(stopPath); } catch {}
@@ -1,5 +1,6 @@
1
1
  import {
2
- existsSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync,
2
+ closeSync, existsSync, openSync, readFileSync, readdirSync, renameSync, statSync,
3
+ unlinkSync, writeFileSync,
3
4
  } from "node:fs";
4
5
  import { join } from "node:path";
5
6
 
@@ -62,6 +63,10 @@ export function dutyNudgeDirective(plan) {
62
63
  return `\nMECHANICAL DUTY NUDGE REQUIREMENT (runner-enforced):\n${targets}\nEvery id above is NEW and has no verified socket nudge in ~/.agent-bus/duty-nudged.json. Use ListAgents to resolve each local session and call SendMessage for EVERY listed id before ending this turn. A prior nudge to the same target does not cover a new id; the metronome rule applies only to the SAME id. The runner verifies actual SendMessage tool calls, records successful ids, and reports any omitted ids through /duty/failure. Your only discretion is the content-free nudge wording.\n`;
63
64
  }
64
65
 
66
+ export function claudeTranscriptDir(turnDir, homeDir) {
67
+ return join(homeDir, ".claude", "projects", turnDir.replace(/[^a-zA-Z0-9]/g, "-"));
68
+ }
69
+
65
70
  function toolUses(value, found) {
66
71
  if (!(value instanceof Object)) return;
67
72
  if (value.type === "tool_use" && value.name === "SendMessage") found.push(value);
@@ -87,7 +92,9 @@ export function observedDutyNudgeIds(transcriptDir, sinceMs) {
87
92
  toolUses(row, uses);
88
93
  for (const use of uses) {
89
94
  const input = use.input || {};
90
- for (const id of idsIn(input.message || input.content)) ids.add(id);
95
+ const text = String(input.message || input.content || "");
96
+ if (!text.startsWith("Trantor delivery nudge from the duty seat:")) continue;
97
+ for (const id of idsIn(text)) ids.add(id);
91
98
  }
92
99
  }
93
100
  } catch {}
@@ -105,18 +112,41 @@ function writeDutyNudgeState(path, state) {
105
112
  renameSync(temporary, path);
106
113
  }
107
114
 
108
- export async function auditDutyNudges({ plan, observedIds, statePath, reportFailure, now = Date.now() }) {
109
- const state = readDutyNudgeState(statePath);
110
- for (const item of plan.items) {
111
- if (observedIds.has(item.id)) {
115
+ async function withStateLock(path, update) {
116
+ const lockPath = `${path}.lock`;
117
+ let lock = null;
118
+ for (let attempt = 0; attempt < 100 && lock === null; attempt++) {
119
+ try { lock = openSync(lockPath, "wx", 0o600); }
120
+ catch { await new Promise(resolve => setTimeout(resolve, 10)); }
121
+ }
122
+ if (lock === null) throw new Error(`could not lock ${path}`);
123
+ try {
124
+ const state = readDutyNudgeState(path);
125
+ await update(state);
126
+ writeDutyNudgeState(path, state);
127
+ } finally {
128
+ closeSync(lock);
129
+ try { unlinkSync(lockPath); } catch {}
130
+ }
131
+ }
132
+
133
+ export async function recordDutyNudges({ plan, observedIds, statePath, now = Date.now() }) {
134
+ const nudged = plan.items.filter(item => observedIds.has(item.id));
135
+ if (!nudged.length) return [];
136
+ await withStateLock(statePath, state => {
137
+ for (const item of nudged) {
112
138
  state.nudged[item.id] = { recipient: item.recipient, project: item.project, nudgedAt: now };
113
139
  }
114
- }
115
- writeDutyNudgeState(statePath, state);
140
+ });
141
+ return nudged;
142
+ }
143
+
144
+ export async function auditDutyNudges({ plan, observedIds, statePath, reportFailure, now = Date.now() }) {
145
+ const nudged = await recordDutyNudges({ plan, observedIds, statePath, now });
116
146
  const missing = plan.targets.map(target => ({
117
147
  ...target,
118
148
  ids: target.ids.filter(id => !observedIds.has(id)),
119
149
  })).filter(target => target.ids.length);
120
150
  for (const target of missing) await reportFailure(target);
121
- return { missing, nudged: plan.items.filter(item => observedIds.has(item.id)) };
151
+ return { missing, nudged };
122
152
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trantor",
3
- "version": "0.18.48",
3
+ "version": "0.18.49",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "trantor": "bin/cli.mjs"