parallelclaw 1.2.2 → 1.3.0

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/CHANGELOG.md CHANGED
@@ -2,6 +2,27 @@
2
2
 
3
3
  Notable changes to parallelclaw (formerly memex-mvp). Older history lives in the git log.
4
4
 
5
+ ## 1.3.0 — instant delegation: push-on-write (no waiting for the sync schedule)
6
+
7
+ Delegations, claims, and results now fly to the mesh the moment they're written,
8
+ instead of waiting up to a full sync interval. Every task write on an initiating
9
+ node fires a detached `sync-run --all` immediately — fire-and-forget,
10
+ best-effort, a no-op when sync isn't configured (or `PARALLELCLAW_NO_PUSH=1`).
11
+ Wired into `task-delegate` / `task-claim` / `task-update`, the
12
+ `parallelclaw_delegate` MCP tool, and the Mac headless executor's results.
13
+ Live-verified: a delegation pushed to the hub in ~1s (last-sync jumped from 82m
14
+ ago to 0m ago) with no manual sync. +1 guard test.
15
+
16
+ This removes the **requester→hub** leg of the latency. Two legs remain
17
+ schedule-bound (tuned by interval, not code): the **hub→executor** forward (the
18
+ hub syncs the executor on its own timer) and the **executor's inbox poll**. Drop
19
+ those to ~30–60s for near-instant end-to-end; true sub-second needs hub
20
+ push-routing + an event-driven executor (next).
21
+
22
+ Also: **`task-list --inbox --quiet`** — a no-output exit-code gate (0 = has work,
23
+ 1 = empty) so a 1-min executor cron can pre-check with a free shell call and only
24
+ spin up the (token-costly) LLM agentTurn when a task is actually waiting.
25
+
5
26
  ## 1.2.2 — durable execution: claim lease + auto-requeue for dead executors
6
27
 
7
28
  Found live: a research task outran Kimi's 240-second cron tick. The executor had
package/lib/executor.js CHANGED
@@ -25,7 +25,7 @@ import { writeFileSync, existsSync, mkdirSync, unlinkSync } from 'node:fs';
25
25
  import { execFile, execSync } from 'node:child_process';
26
26
 
27
27
  import { getOrigin } from './config.js';
28
- import { listTasks, claimTask, updateTask } from './tasks.js';
28
+ import { listTasks, claimTask, updateTask, triggerSyncPush } from './tasks.js';
29
29
 
30
30
  const HOME = homedir();
31
31
  const MEMEX_DIR = process.env.MEMEX_DIR || join(HOME, '.memex');
@@ -128,6 +128,9 @@ export async function runInboxOnce({
128
128
  acted.push({ id: t.id, status: 'failed' });
129
129
  }
130
130
  }
131
+ // Fly results back to the requester now (claimTask/updateTask here are lib-level
132
+ // and don't push on their own). One push covers the whole drain pass.
133
+ if (acted.some((a) => a.status === 'done' || a.status === 'failed')) triggerSyncPush();
131
134
  return acted;
132
135
  }
133
136
 
package/lib/tasks.js CHANGED
@@ -27,8 +27,11 @@ import { homedir } from 'node:os';
27
27
  import { join, dirname } from 'node:path';
28
28
  import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
29
29
  import Database from 'better-sqlite3';
30
+ import { spawn } from 'node:child_process';
31
+ import { fileURLToPath } from 'node:url';
30
32
 
31
33
  import { getOrigin } from './config.js';
34
+ import { syncExperimentEnabled } from './sync/config.js';
32
35
 
33
36
  const SOURCE = 'agent-task';
34
37
  const STATUSES = ['submitted', 'working', 'done', 'failed'];
@@ -73,6 +76,26 @@ function openDb() {
73
76
  return db;
74
77
  }
75
78
 
79
+ const INGEST_PATH = join(dirname(fileURLToPath(import.meta.url)), '..', 'ingest.js');
80
+
81
+ /**
82
+ * Push the just-written task event toward the mesh NOW — so a delegation, claim,
83
+ * or result propagates in seconds instead of waiting for the next scheduled sync.
84
+ * Fire-and-forget: detached, best-effort, never blocks or throws. No-op when sync
85
+ * isn't configured (or when disabled via PARALLELCLAW_NO_PUSH=1). Returns the
86
+ * spawned child (or null if skipped) — for tests.
87
+ */
88
+ export function triggerSyncPush() {
89
+ try {
90
+ if (process.env.PARALLELCLAW_NO_PUSH === '1') return null;
91
+ if (!syncExperimentEnabled()) return null; // no mesh configured → nothing to push to
92
+ const child = spawn(process.execPath, [INGEST_PATH, 'sync-run', '--all'],
93
+ { detached: true, stdio: 'ignore', env: process.env });
94
+ child.unref();
95
+ return child;
96
+ } catch (_) { return null; }
97
+ }
98
+
76
99
  /** Append one event row for a task. Returns the row's msg_id. */
77
100
  function writeEvent(db, { taskId, status, seq, envelope, text, origin, now }) {
78
101
  const ts = Math.floor(now / 1000);
@@ -299,6 +322,7 @@ export function cmdTaskDelegate() {
299
322
  });
300
323
  console.log(`✓ delegated task ${id} → ${to} (from ${getOrigin()})`);
301
324
  console.log(` track: task-list --mine`);
325
+ triggerSyncPush(); // fly it to the mesh now, don't wait for the schedule
302
326
  process.exit(0);
303
327
  }
304
328
 
@@ -313,6 +337,7 @@ export function cmdTaskUpdate() {
313
337
  const env = updateTask(id, status, { result: typeof args['--result'] === 'string' ? args['--result'] : null });
314
338
  console.log(`✓ task ${id} → ${env.status}${env.result ? ' (result attached)' : ''}`);
315
339
  } catch (e) { console.error(`✗ ${e.message}`); process.exit(1); }
340
+ triggerSyncPush(); // status transition flies back to the requester now
316
341
  process.exit(0);
317
342
  }
318
343
 
@@ -333,6 +358,7 @@ export function cmdTaskClaim() {
333
358
  else console.log(`· task ${id} not claimable (already taken or terminal) — skip`);
334
359
  process.exit(3);
335
360
  }
361
+ triggerSyncPush(); // propagate the claim immediately (shrinks the double-claim window)
336
362
  if ('--json' in args) { console.log(JSON.stringify({ claimed: true, id, ...env })); process.exit(0); }
337
363
  console.log(`✓ claimed ${id} → working (by ${getOrigin()})`);
338
364
  console.log(` from ${env.from} · kind: ${env.kind || 'general'}`);
@@ -351,6 +377,10 @@ export function cmdTaskList() {
351
377
  inbox: '--inbox' in args,
352
378
  limit: parseInt(args['--limit'] || '', 10) || 50,
353
379
  });
380
+ // Cheap exit-code gate for cron polling: no output, exit 0 if there ARE tasks,
381
+ // 1 if none. Lets a scheduler do `task-list --inbox --quiet && run-executor`
382
+ // so the (token-costly) LLM agentTurn only fires when there's actual work.
383
+ if ('--quiet' in args) process.exit(tasks.length ? 0 : 1);
354
384
  if (!tasks.length) { console.log('No tasks.'); process.exit(0); }
355
385
  const icon = { submitted: '○', working: '◐', done: '✓', failed: '✗' };
356
386
  for (const t of tasks) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "parallelclaw",
3
- "version": "1.2.2",
3
+ "version": "1.3.0",
4
4
  "description": "Local-first personal AI ops layer. Shared verbatim memory across your agents (Claude Code, Cursor, OpenClaw, Hermes, Obsidian, Telegram) in one SQLite + FTS5 corpus — plus a coordination layer where any of your agents can delegate tasks to any other. Searchable from any MCP-compatible client.",
5
5
  "type": "module",
6
6
  "main": "server.js",
package/server.js CHANGED
@@ -44,7 +44,7 @@ import {
44
44
  KNOWN_SOURCES,
45
45
  CONFIG_PATH,
46
46
  } from './lib/config.js';
47
- import { createTask, listTasks, listTaskResults } from './lib/tasks.js';
47
+ import { createTask, listTasks, listTaskResults, triggerSyncPush } from './lib/tasks.js';
48
48
  import {
49
49
  canonicalize as canonicalizeUrl,
50
50
  extractDomain,
@@ -3632,6 +3632,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
3632
3632
  const kind = (typeof args.kind === 'string' && args.kind.trim()) ? args.kind.trim() : 'general';
3633
3633
  const content = typeof args.content === 'string' ? args.content : null;
3634
3634
  const { id } = createTask({ prompt, to, kind, content });
3635
+ triggerSyncPush(); // deliver the delegation to the mesh now, not on the next schedule
3635
3636
  return textResult(
3636
3637
  `✓ Delegated task \`${id}\` → **${to}** (from ${getOrigin()}).\n` +
3637
3638
  `Runs when ${to === 'any' ? 'an always-on agent' : to} is available; the result returns to you ` +