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.
- package/.claude-plugin/plugin.json +1 -1
- package/bin/crew-runner.mjs +18 -3
- package/bin/duty-nudge-watch.mjs +25 -0
- package/lib/duty-nudges.mjs +39 -9
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trantor",
|
|
3
|
-
"version": "0.18.
|
|
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": {
|
package/bin/crew-runner.mjs
CHANGED
|
@@ -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 =
|
|
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
|
|
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 {}
|
package/lib/duty-nudges.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
|
-
existsSync, readFileSync, readdirSync, renameSync, statSync,
|
|
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
|
-
|
|
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
|
-
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
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
|
|
151
|
+
return { missing, nudged };
|
|
122
152
|
}
|