let-them-talk 5.5.2 → 5.5.4

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/README.md CHANGED
@@ -310,7 +310,7 @@ The verify suite doesn't claim to cover every provider or runtime matrix, and do
310
310
  - **Rate-limited** API endpoints on non-localhost requests.
311
311
  - **No telemetry, no cloud.** Everything runs locally.
312
312
  - **Obsidian-quality rich rendering** — GFM tables, fenced code with syntax highlighting (highlight.js), Obsidian-style callouts (`> [!NOTE]`, `> [!WARNING]`, `> [!SUMMARY]-` collapsible), Mermaid diagrams, KaTeX math, clickable image lightbox, copy-code buttons. Every shipping lib is bundled locally under `vendor/` so the dashboard works offline.
313
- - **0 known vulnerabilities** in the shipped tarball as of v5.5.2.
313
+ - **0 known vulnerabilities** in the shipped tarball as of v5.5.4.
314
314
  - **Sensitive-path blocks** on file-share: `.env`, `.pem`, `.key`, `.lan-token`, `mcp.json`, and the agent-bridge data directory cannot be shared.
315
315
  - See [`SECURITY.md`](SECURITY.md) for the disclosure policy.
316
316
 
package/USAGE.md CHANGED
@@ -1,6 +1,6 @@
1
1
  <!-- Generated from ../USAGE.md by scripts/sync-packaged-docs.js for published package consumers. -->
2
2
 
3
- # Let Them Talk Usage Guide v5.5.2
3
+ # Let Them Talk Usage Guide v5.5.4
4
4
 
5
5
  This guide is the short operator view of the current runtime. For normative architecture details, use the docs under `docs/architecture/`.
6
6
 
package/cli.js CHANGED
@@ -9,7 +9,7 @@ const { createCanonicalState } = require('./state/canonical');
9
9
 
10
10
  function printUsage() {
11
11
  console.log(`
12
- Let Them Talk — Agent Bridge v5.5.2
12
+ Let Them Talk — Agent Bridge v5.5.4
13
13
  MCP message broker for inter-agent communication
14
14
  Supports: Claude Code, Gemini CLI, Codex CLI, Ollama
15
15
 
@@ -30,6 +30,8 @@ function printUsage() {
30
30
  node .agent-bridge/launch.js reset Clear all conversation data
31
31
  node .agent-bridge/launch.js migrate Backfill canonical event stream from legacy projections
32
32
  node .agent-bridge/launch.js migrate --dry-run Preview what migrate would do
33
+ node .agent-bridge/launch.js repair Repair corrupted canonical event log (drops orphan redactions)
34
+ node .agent-bridge/launch.js repair --dry-run Preview what repair would do
33
35
 
34
36
  Or via npx (re-downloads each time):
35
37
  npx let-them-talk dashboard
@@ -879,6 +881,15 @@ function cliStatus() {
879
881
  console.log('');
880
882
  }
881
883
 
884
+ function cliRepairEvents() {
885
+ const { repairBranch: _ } = require('./scripts/repair-canonical-events');
886
+ // Delegate to the script's main — it reads process.argv and handles --dry-run
887
+ const scriptPath = path.join(__dirname, 'scripts', 'repair-canonical-events.js');
888
+ const args = process.argv.slice(3);
889
+ const child = require('child_process').spawnSync(process.execPath, [scriptPath, ...args], { stdio: 'inherit' });
890
+ if (child.status !== 0) process.exit(child.status || 1);
891
+ }
892
+
882
893
  function cliMigrate() {
883
894
  const args = process.argv.slice(3);
884
895
  const dryRun = args.includes('--dry-run') || args.includes('-n');
@@ -1143,6 +1154,10 @@ function runCli() {
1143
1154
  case 'migrate-legacy':
1144
1155
  cliMigrate();
1145
1156
  break;
1157
+ case 'repair':
1158
+ case 'repair-events':
1159
+ cliRepairEvents();
1160
+ break;
1146
1161
  case 'msg':
1147
1162
  case 'message':
1148
1163
  case 'send':
package/dashboard.js CHANGED
@@ -3445,7 +3445,7 @@ server.listen(PORT, LAN_MODE ? '0.0.0.0' : '127.0.0.1', () => {
3445
3445
  const dataDir = resolveDataDir();
3446
3446
  const lanIP = getLanIP();
3447
3447
  console.log('');
3448
- console.log(' Let Them Talk - Agent Bridge Dashboard v5.5.2');
3448
+ console.log(' Let Them Talk - Agent Bridge Dashboard v5.5.4');
3449
3449
  console.log(' ============================================');
3450
3450
  console.log(' Dashboard: http://localhost:' + PORT);
3451
3451
  if (LAN_MODE && lanIP) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "let-them-talk",
3
- "version": "5.5.2",
3
+ "version": "5.5.4",
4
4
  "description": "MCP message broker + web dashboard for inter-agent communication. Let AI CLI agents talk to each other.",
5
5
  "main": "server.js",
6
6
  "bin": {
@@ -0,0 +1,195 @@
1
+ #!/usr/bin/env node
2
+ // Repair a corrupted canonical event log in-place.
3
+ //
4
+ // Problem this fixes:
5
+ // Pre-v5.5.3 Clear Messages would emit a message.redacted event for every
6
+ // message currently in the projection, even if that message had no
7
+ // corresponding message.sent event in the canonical log (e.g. legacy
8
+ // projection-only messages left over from a partial migration, or an
9
+ // earlier clear cycle). On the next rebuild/replay the redacted event
10
+ // fails with "cannot apply message.redacted because message X does not
11
+ // exist", which blocks further Clear Messages on that branch.
12
+ //
13
+ // v5.5.3 fixed the root cause in clearMessages so new redactions are
14
+ // gated on the presence of a message.sent ancestor. This script cleans
15
+ // up the orphan redactions that are already in the event log from
16
+ // previous versions so the branch can replay again.
17
+ //
18
+ // What it does:
19
+ // - For every branch under .agent-bridge/runtime/branches/<branch>/:
20
+ // - Reads events.jsonl
21
+ // - Collects the set of message IDs that have a message.sent event
22
+ // - Drops message.redacted and message.corrected events whose
23
+ // payload.message_id is not in that set (orphans)
24
+ // - Backs up the original events.jsonl to events.jsonl.pre-repair-<ts>
25
+ // - Writes the cleaned stream back
26
+ // - Deletes the branch's projection files (messages.jsonl, history.jsonl,
27
+ // dashboard-query-projection.json, events.head.json) so the runtime
28
+ // rebuilds them cleanly on the next read.
29
+ //
30
+ // Usage:
31
+ // node agent-bridge/scripts/repair-canonical-events.js [project-path]
32
+ // node agent-bridge/scripts/repair-canonical-events.js --dry-run [project-path]
33
+ //
34
+ // project-path defaults to the current working directory.
35
+
36
+ const fs = require('fs');
37
+ const path = require('path');
38
+ const { resolveDataDir } = require('../data-dir');
39
+
40
+ function readJsonl(file) {
41
+ if (!fs.existsSync(file)) return [];
42
+ return fs
43
+ .readFileSync(file, 'utf8')
44
+ .split(/\r?\n/)
45
+ .filter((line) => line.trim())
46
+ .map((line) => {
47
+ try { return JSON.parse(line); } catch { return null; }
48
+ })
49
+ .filter(Boolean);
50
+ }
51
+
52
+ function repairBranch(branchDir, opts) {
53
+ const eventsFile = path.join(branchDir, 'events.jsonl');
54
+ if (!fs.existsSync(eventsFile)) return { skipped: true, reason: 'no events.jsonl' };
55
+
56
+ const events = readJsonl(eventsFile);
57
+ if (events.length === 0) return { skipped: true, reason: 'empty events.jsonl' };
58
+
59
+ const sentIds = new Set();
60
+ for (const ev of events) {
61
+ if (ev && ev.type === 'message.sent' && ev.payload && ev.payload.message && typeof ev.payload.message.id === 'string') {
62
+ sentIds.add(ev.payload.message.id);
63
+ }
64
+ }
65
+
66
+ const kept = [];
67
+ const orphans = [];
68
+ const duplicateRedactions = [];
69
+ const seenRedactedIds = new Set();
70
+ for (const ev of events) {
71
+ if (ev && (ev.type === 'message.redacted' || ev.type === 'message.corrected')) {
72
+ const msgId = ev.payload && ev.payload.message_id;
73
+ // Orphan: no corresponding message.sent ancestor.
74
+ if (msgId && !sentIds.has(msgId)) {
75
+ orphans.push(ev);
76
+ continue;
77
+ }
78
+ // Duplicate redaction: same message_id already redacted earlier in the
79
+ // stream. Keep the first, drop subsequent ones. Pre-v5.5.4 the replay
80
+ // threw on the second redaction; now it tolerates duplicates, but we
81
+ // still want to prune the log so it's clean.
82
+ if (ev.type === 'message.redacted' && msgId) {
83
+ if (seenRedactedIds.has(msgId)) {
84
+ duplicateRedactions.push(ev);
85
+ continue;
86
+ }
87
+ seenRedactedIds.add(msgId);
88
+ }
89
+ }
90
+ kept.push(ev);
91
+ }
92
+
93
+ const result = {
94
+ branch: path.basename(branchDir),
95
+ total_events: events.length,
96
+ message_sent_events: sentIds.size,
97
+ orphan_redacted: orphans.filter((o) => o.type === 'message.redacted').length,
98
+ orphan_corrected: orphans.filter((o) => o.type === 'message.corrected').length,
99
+ duplicate_redacted: duplicateRedactions.length,
100
+ kept_events: kept.length,
101
+ };
102
+
103
+ if (opts.dryRun) {
104
+ result.dryRun = true;
105
+ return result;
106
+ }
107
+
108
+ if (orphans.length === 0 && duplicateRedactions.length === 0) {
109
+ result.skipped = true;
110
+ result.reason = 'no orphan or duplicate events';
111
+ return result;
112
+ }
113
+
114
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
115
+ const backupFile = path.join(branchDir, `events.jsonl.pre-repair-${stamp}`);
116
+ fs.copyFileSync(eventsFile, backupFile);
117
+ result.backup = backupFile;
118
+
119
+ fs.writeFileSync(eventsFile, kept.map((e) => JSON.stringify(e)).join('\n') + '\n');
120
+
121
+ // Drop cached projections + head pointer so the runtime rebuilds next read
122
+ for (const name of ['messages.jsonl', 'history.jsonl', 'dashboard-query-projection.json', 'events.head.json']) {
123
+ const p = path.join(branchDir, name);
124
+ if (fs.existsSync(p)) fs.unlinkSync(p);
125
+ }
126
+ result.projections_cleared = true;
127
+
128
+ return result;
129
+ }
130
+
131
+ function main(argv) {
132
+ const args = argv.slice(2);
133
+ const dryRun = args.includes('--dry-run') || args.includes('-n');
134
+ const projectArg = args.filter((a) => !a.startsWith('-'))[0] || process.cwd();
135
+
136
+ const dataDir = resolveDataDir({ cwd: projectArg });
137
+ const branchesDir = path.join(dataDir, 'runtime', 'branches');
138
+
139
+ console.log('');
140
+ console.log(' Canonical event log repair');
141
+ console.log(' ==========================');
142
+ console.log(' Project: ' + projectArg);
143
+ console.log(' Runtime: ' + branchesDir);
144
+ console.log(dryRun ? ' Mode: DRY RUN (no changes written)' : ' Mode: apply');
145
+ console.log('');
146
+
147
+ if (!fs.existsSync(branchesDir)) {
148
+ console.log(' [info] No canonical runtime at this path. Nothing to repair.');
149
+ return;
150
+ }
151
+
152
+ const branches = fs.readdirSync(branchesDir).filter((name) => {
153
+ const p = path.join(branchesDir, name);
154
+ return fs.statSync(p).isDirectory();
155
+ });
156
+
157
+ if (branches.length === 0) {
158
+ console.log(' [info] No branches present. Nothing to repair.');
159
+ return;
160
+ }
161
+
162
+ for (const name of branches) {
163
+ const r = repairBranch(path.join(branchesDir, name), { dryRun });
164
+ console.log(` Branch "${name}":`);
165
+ if (r.skipped) {
166
+ console.log(' skipped — ' + r.reason);
167
+ } else {
168
+ console.log(` ${r.total_events} total events, ${r.message_sent_events} sent messages`);
169
+ console.log(` orphan redactions: ${r.orphan_redacted}, orphan corrections: ${r.orphan_corrected}, duplicate redactions: ${r.duplicate_redacted}`);
170
+ if (!dryRun) {
171
+ console.log(' [ok] rewrote events.jsonl (' + r.kept_events + ' events kept)');
172
+ console.log(' [ok] backup: ' + r.backup);
173
+ console.log(' [ok] projections cleared — will rebuild on next read');
174
+ } else {
175
+ const drops = r.orphan_redacted + r.orphan_corrected + r.duplicate_redacted;
176
+ console.log(' [dry-run] would drop ' + drops + ' bad event(s) and back up the original');
177
+ }
178
+ }
179
+ }
180
+
181
+ console.log('');
182
+ if (!dryRun) {
183
+ console.log(' Done. Clear Messages should now work on the repaired branches.');
184
+ } else {
185
+ console.log(' Re-run without --dry-run to apply.');
186
+ }
187
+ console.log('');
188
+ }
189
+
190
+ if (require.main === module) {
191
+ try { main(process.argv); }
192
+ catch (e) { console.error(' [error] ' + (e && e.stack ? e.stack : e)); process.exit(1); }
193
+ }
194
+
195
+ module.exports = { repairBranch };
package/server.js CHANGED
@@ -3708,7 +3708,7 @@ function buildListenGroupResponse(batch, consumed, agentName, listenStart) {
3708
3708
  return new Date(a.timestamp) - new Date(b.timestamp);
3709
3709
  });
3710
3710
 
3711
- // LEAN RESPONSE (v5.5.2+): the agent already has every prior message in its
3711
+ // LEAN RESPONSE (v5.5.4+): the agent already has every prior message in its
3712
3712
  // own LLM context, plus the full rule set from get_guide() + AGENTS.md. We
3713
3713
  // only send the NEW messages + the managed-mode signals needed for
3714
3714
  // turn-taking. No repeated reminders, no agent rosters, no "next_action"
@@ -8136,7 +8136,7 @@ function toolToggleRule(ruleId) {
8136
8136
  // --- MCP Server setup ---
8137
8137
 
8138
8138
  const server = new Server(
8139
- { name: 'agent-bridge', version: '5.5.2' },
8139
+ { name: 'agent-bridge', version: '5.5.4' },
8140
8140
  { capabilities: { tools: {} } }
8141
8141
  );
8142
8142
 
@@ -9265,7 +9265,7 @@ async function main() {
9265
9265
  try {
9266
9266
  const transport = new StdioServerTransport();
9267
9267
  await server.connect(transport);
9268
- console.error('Agent Bridge MCP server v5.5.2 running (65 tools)');
9268
+ console.error('Agent Bridge MCP server v5.5.4 running (65 tools)');
9269
9269
  } catch (e) {
9270
9270
  console.error('ERROR: MCP server failed to start: ' + e.message);
9271
9271
  console.error('Fix: Run "npx let-them-talk doctor" to check your setup.');
@@ -2770,8 +2770,22 @@ function createCanonicalState(options = {}) {
2770
2770
  const redactedAt = params.redactedAt || new Date().toISOString();
2771
2771
  const clearedMessageIds = [];
2772
2772
 
2773
+ // Only redact messages that actually have a canonical message.sent event
2774
+ // in this branch's log. Redacting a message that was never "sent" in
2775
+ // canonical terms (e.g. a legacy projection-only message left over from
2776
+ // pre-canonical clear cycles) would create an orphan redaction that
2777
+ // breaks future replay — the rebuild fails with "cannot apply
2778
+ // message.redacted because message X does not exist".
2779
+ const canonicalSentIds = new Set();
2780
+ for (const event of readCanonicalMessageEvents(branch)) {
2781
+ if (event && event.type === 'message.sent' && event.payload && event.payload.message && typeof event.payload.message.id === 'string') {
2782
+ canonicalSentIds.add(event.payload.message.id);
2783
+ }
2784
+ }
2785
+
2773
2786
  for (const message of Array.isArray(currentMessages) ? currentMessages : []) {
2774
2787
  if (!message || typeof message.id !== 'string' || !message.id) continue;
2788
+ if (!canonicalSentIds.has(message.id)) continue; // skip projection-only / orphan
2775
2789
  appendCanonicalMessageRedactedEvent({
2776
2790
  branch,
2777
2791
  actorAgent,
package/state/messages.js CHANGED
@@ -397,18 +397,15 @@ function createMessagesState(options = {}) {
397
397
  case 'message.redacted': {
398
398
  const redaction = validateCanonicalMessageRedactionPayload(event);
399
399
  const record = findProjectedMessageRecord(projection, redaction.messageId);
400
- if (!record) {
401
- throw createCanonicalReplayError(
402
- CANONICAL_REPLAY_ERROR_CODES.INVALID_SEQUENCE,
403
- `Canonical message replay cannot apply ${describeReplayEvent(event)} because message ${redaction.messageId} does not exist in the current branch projection.`,
404
- {
405
- event_type: event.type,
406
- seq: event.seq,
407
- branch_id: event.branch_id,
408
- message_id: redaction.messageId,
409
- }
410
- );
411
- }
400
+ // Redaction is idempotent: if the message is already gone from the
401
+ // projection (e.g. a prior message.redacted for the same id already
402
+ // ran during this replay), the second redaction is a no-op rather
403
+ // than a fatal error. The canonical log can legitimately carry
404
+ // multiple redaction events per id — operators running Clear
405
+ // Messages repeatedly on the same branch would produce that shape,
406
+ // and aborting the whole replay over it is worse than ignoring the
407
+ // duplicate.
408
+ if (!record) return projection;
412
409
 
413
410
  if (record.messageIndex >= 0) {
414
411
  record.conversation.messages.splice(record.messageIndex, 1);