parallelclaw 1.0.0 → 1.1.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 +31 -0
- package/ingest.js +2 -0
- package/lib/cli/index.js +29 -1
- package/lib/tasks.js +174 -18
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
Notable changes to parallelclaw (formerly memex-mvp). Older history lives in the git log.
|
|
4
4
|
|
|
5
|
+
## 1.1.0 — agent coordination: hardened task ledger + executor + result-routing
|
|
6
|
+
|
|
7
|
+
The first release where the **coordination layer works end-to-end**: one of your
|
|
8
|
+
agents delegates a task, an always-on agent runs it, and the answer comes back to
|
|
9
|
+
where you asked — riding the existing sync mesh, no new transport.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- **`task-claim <id>`** — atomically claim a submitted task for execution (exit 0
|
|
13
|
+
claimed / 3 not-claimable / 1 error; `--json`). The always-on executor uses it
|
|
14
|
+
so two pollers never double-run one task. See `docs/design/executor-prompt.md`
|
|
15
|
+
for the ready-to-paste cron-prompt.
|
|
16
|
+
- **`task-results [--ack] [--json] [--all]`** — results of tasks you delegated,
|
|
17
|
+
to deliver to your surface. The SessionStart hook now injects a "📥 N results
|
|
18
|
+
from tasks you delegated" block automatically (Claude Code requester); a
|
|
19
|
+
Telegram requester's cron DMs them and acks after sending.
|
|
20
|
+
|
|
21
|
+
### Changed / fixed (ledger correctness — required before unattended execution)
|
|
22
|
+
- **Skew-proof status (#1):** current task state is decided by a monotonic
|
|
23
|
+
per-task `seq` (then lifecycle-rank → ts → rowid), not wall-clock ts. Clock
|
|
24
|
+
skew across nodes can no longer regress a `done` back to `working`.
|
|
25
|
+
- **Claim/lock (#3):** optimistic claim resolves concurrent take attempts to one
|
|
26
|
+
deterministic winner once synced.
|
|
27
|
+
- **#2** a `to:'any'` task lands in every node's inbox (first to claim wins).
|
|
28
|
+
**#4** unique per-event `msg_id` (no same-ms silent drop). **#5** lifecycle
|
|
29
|
+
guard (no un-completing a terminal, no regress to submitted). **#6** `done`/
|
|
30
|
+
`failed` must carry a result.
|
|
31
|
+
|
|
32
|
+
Deferred: #7 `listTasks` scan-bound + index (perf), #8 excluding `agent-task`
|
|
33
|
+
rows from default search/overview. NL delegate tool (2c) and the Mac headless
|
|
34
|
+
`claude -p` runner (2b) are the next slices.
|
|
35
|
+
|
|
5
36
|
## 1.0.0 — rebrand: memex → ParallelClaw
|
|
6
37
|
|
|
7
38
|
The product is renamed **ParallelClaw** and repositioned from "AI memory" to a
|
package/ingest.js
CHANGED
|
@@ -156,7 +156,9 @@ if (subcommand && subcommand !== '--help' && subcommand.startsWith('-') === fals
|
|
|
156
156
|
// Foreman tracer (v0) — agent-to-agent task ledger (tasks = agent-task messages)
|
|
157
157
|
'task-delegate': async () => (await import('./lib/tasks.js')).cmdTaskDelegate(),
|
|
158
158
|
'task-list': async () => (await import('./lib/tasks.js')).cmdTaskList(),
|
|
159
|
+
'task-claim': async () => (await import('./lib/tasks.js')).cmdTaskClaim(),
|
|
159
160
|
'task-update': async () => (await import('./lib/tasks.js')).cmdTaskUpdate(),
|
|
161
|
+
'task-results': async () => (await import('./lib/tasks.js')).cmdTaskResults(),
|
|
160
162
|
serve: cmdServe, // explicit foreground; same as no-arg
|
|
161
163
|
// All scan / export modes fall through to module-level logic at EOF.
|
|
162
164
|
// cmdServe is a no-op marker so the dispatch doesn't error.
|
package/lib/cli/index.js
CHANGED
|
@@ -799,6 +799,14 @@ async function cmdContext(args) {
|
|
|
799
799
|
pendingTg = listPending();
|
|
800
800
|
} catch (_) { /* ignore */ }
|
|
801
801
|
|
|
802
|
+
// Result-routing (Phase 2d): answers to tasks THIS node delegated that are now
|
|
803
|
+
// terminal and not yet surfaced here — "answer where you asked".
|
|
804
|
+
let taskResults = [];
|
|
805
|
+
try {
|
|
806
|
+
const { listTaskResults } = await import('../tasks.js');
|
|
807
|
+
taskResults = listTaskResults({ undeliveredOnly: true });
|
|
808
|
+
} catch (_) { /* ignore */ }
|
|
809
|
+
|
|
802
810
|
if (opts.json) {
|
|
803
811
|
console.log(JSON.stringify({
|
|
804
812
|
pwd, project, freshness_days: freshnessDays,
|
|
@@ -806,6 +814,7 @@ async function cmdContext(args) {
|
|
|
806
814
|
fuzzy_matches: filteredFuzzy.length,
|
|
807
815
|
conversations: all,
|
|
808
816
|
token_budget: tokenBudget,
|
|
817
|
+
task_results: taskResults,
|
|
809
818
|
telegram_pending: pendingTg.map((e) => ({
|
|
810
819
|
chat_title: e.chat_title,
|
|
811
820
|
chat_type: e.chat_type,
|
|
@@ -846,7 +855,21 @@ async function cmdContext(args) {
|
|
|
846
855
|
lines.push('');
|
|
847
856
|
}
|
|
848
857
|
|
|
849
|
-
if (
|
|
858
|
+
if (taskResults.length > 0) {
|
|
859
|
+
lines.push(`### 📥 ${taskResults.length} result${taskResults.length === 1 ? '' : 's'} from tasks you delegated`);
|
|
860
|
+
lines.push('');
|
|
861
|
+
for (const t of taskResults.slice(0, 10)) {
|
|
862
|
+
const icon = t.status === 'done' ? '✓' : '✗';
|
|
863
|
+
lines.push(`- ${icon} **${t.id}** → ${t.to} _(${t.status})_ — ${String(t.prompt || '').slice(0, 70)}`);
|
|
864
|
+
lines.push(` - result: ${String(t.result || '').slice(0, 220)}`);
|
|
865
|
+
}
|
|
866
|
+
if (taskResults.length > 10) lines.push(`- _… and ${taskResults.length - 10} more (run \`memex-sync task-results\`)_`);
|
|
867
|
+
lines.push('');
|
|
868
|
+
lines.push(`**INSTRUCTION FOR THE AGENT:** these are answers to tasks you handed to another of your agents. Surface them to the user briefly, then continue.`);
|
|
869
|
+
lines.push('');
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
if (all.length === 0 && pendingTg.length === 0 && taskResults.length === 0) {
|
|
850
873
|
lines.push(`_No recent activity in memex for this project (last ${freshnessDays} days)._`);
|
|
851
874
|
lines.push('');
|
|
852
875
|
lines.push(`Path searched: \`${pwd}\``);
|
|
@@ -895,6 +918,11 @@ async function cmdContext(args) {
|
|
|
895
918
|
out += '\n\n_[context truncated — exceeds token budget]_\n';
|
|
896
919
|
}
|
|
897
920
|
|
|
921
|
+
// Mark surfaced results delivered so they don't re-show every session. Skip on
|
|
922
|
+
// --no-ack (preview/testing) — json/help paths returned earlier and never ack.
|
|
923
|
+
if (taskResults.length > 0 && !(opts.noAck || opts['no-ack'])) {
|
|
924
|
+
try { const { markResultsDelivered } = await import('../tasks.js'); markResultsDelivered(taskResults.map((t) => t.id)); } catch (_) { /* re-show next time */ }
|
|
925
|
+
}
|
|
898
926
|
process.stdout.write(out);
|
|
899
927
|
}
|
|
900
928
|
|
package/lib/tasks.js
CHANGED
|
@@ -24,13 +24,31 @@
|
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
import { homedir } from 'node:os';
|
|
27
|
-
import { join } from 'node:path';
|
|
27
|
+
import { join, dirname } from 'node:path';
|
|
28
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
28
29
|
import Database from 'better-sqlite3';
|
|
29
30
|
|
|
30
31
|
import { getOrigin } from './config.js';
|
|
31
32
|
|
|
32
33
|
const SOURCE = 'agent-task';
|
|
33
34
|
const STATUSES = ['submitted', 'working', 'done', 'failed'];
|
|
35
|
+
const TERMINAL = new Set(['done', 'failed']);
|
|
36
|
+
|
|
37
|
+
// Pick the CURRENT event of a task WITHOUT trusting wall-clock ts — clock skew
|
|
38
|
+
// across nodes regressed state on the live mesh (a 'done' on a fast clock could
|
|
39
|
+
// sort before a later 'working'). Order by the monotonic per-task `seq`, then a
|
|
40
|
+
// lifecycle rank (terminal wins a seq-tie, so a concurrent 'working' can never
|
|
41
|
+
// un-complete a 'done'), then ts, then rowid — a total, deterministic order
|
|
42
|
+
// every node computes identically once synced.
|
|
43
|
+
const STATUS_RANK = { submitted: 0, working: 1, failed: 2, done: 3 };
|
|
44
|
+
function cmpEvent(a, b) {
|
|
45
|
+
const sa = a.seq ?? 0, sb = b.seq ?? 0;
|
|
46
|
+
if (sa !== sb) return sa - sb;
|
|
47
|
+
const ra = STATUS_RANK[a.status] ?? -1, rb = STATUS_RANK[b.status] ?? -1;
|
|
48
|
+
if (ra !== rb) return ra - rb;
|
|
49
|
+
if ((a.ts || 0) !== (b.ts || 0)) return (a.ts || 0) - (b.ts || 0);
|
|
50
|
+
return (a.rowid || 0) - (b.rowid || 0);
|
|
51
|
+
}
|
|
34
52
|
|
|
35
53
|
function dbPath() {
|
|
36
54
|
const dir = process.env.MEMEX_DIR || join(homedir(), '.memex');
|
|
@@ -45,9 +63,12 @@ function openDb() {
|
|
|
45
63
|
}
|
|
46
64
|
|
|
47
65
|
/** Append one event row for a task. Returns the row's msg_id. */
|
|
48
|
-
function writeEvent(db, { taskId, status, envelope, text, origin, now }) {
|
|
66
|
+
function writeEvent(db, { taskId, status, seq, envelope, text, origin, now }) {
|
|
49
67
|
const ts = Math.floor(now / 1000);
|
|
50
|
-
|
|
68
|
+
// `seq` makes the id unique per event (fixes the same-status-same-ms silent
|
|
69
|
+
// INSERT-OR-IGNORE drop); `origin` disambiguates concurrent cross-node writes
|
|
70
|
+
// at the same seq. Computed once at write time → stable across sync.
|
|
71
|
+
const msgId = `${taskId}.${seq}.${status}.${origin}.${now}`;
|
|
51
72
|
db.prepare(
|
|
52
73
|
`INSERT OR IGNORE INTO messages
|
|
53
74
|
(source, conversation_id, msg_id, role, sender, text, ts, metadata, origin)
|
|
@@ -70,9 +91,9 @@ export function createTask({ prompt, to = 'any', kind = 'general', content = nul
|
|
|
70
91
|
const id = `t${now.toString(36)}${Math.random().toString(36).slice(2, 6)}`;
|
|
71
92
|
try {
|
|
72
93
|
writeEvent(db, {
|
|
73
|
-
taskId: id, status: 'submitted', origin: from, now,
|
|
94
|
+
taskId: id, status: 'submitted', seq: 1, origin: from, now,
|
|
74
95
|
text: String(prompt),
|
|
75
|
-
envelope: { task_id: id, status: 'submitted', from, to, kind,
|
|
96
|
+
envelope: { task_id: id, status: 'submitted', seq: 1, from, to, kind,
|
|
76
97
|
prompt: String(prompt), content: content || null, result: null },
|
|
77
98
|
});
|
|
78
99
|
} finally { if (ownDb) db.close(); }
|
|
@@ -91,26 +112,65 @@ export function updateTask(id, status, { result = null, db = null, now = Date.no
|
|
|
91
112
|
try {
|
|
92
113
|
const prev = latestEvent(db, id);
|
|
93
114
|
if (!prev) throw new Error(`updateTask: no task "${id}"`);
|
|
94
|
-
const
|
|
115
|
+
const prevStatus = prev.envelope.status;
|
|
116
|
+
// #5 lifecycle guard: never un-complete a terminal task, never regress to submitted.
|
|
117
|
+
if (TERMINAL.has(prevStatus)) throw new Error(`updateTask: task "${id}" is already ${prevStatus} (terminal) — refusing to set ${status}`);
|
|
118
|
+
if (status === 'submitted') throw new Error(`updateTask: cannot move task "${id}" back to submitted`);
|
|
119
|
+
// #6 a terminal state must say why/what — no silent empty done/failed.
|
|
120
|
+
const carried = result ?? prev.envelope.result ?? null;
|
|
121
|
+
if (TERMINAL.has(status) && !(carried && String(carried).trim())) {
|
|
122
|
+
throw new Error(`updateTask: '${status}' requires a result — pass { result: "<output or reason>" }`);
|
|
123
|
+
}
|
|
124
|
+
const seq = (prev.envelope.seq ?? 0) + 1;
|
|
125
|
+
const env = { ...prev.envelope, status, seq, result: carried };
|
|
95
126
|
writeEvent(db, {
|
|
96
|
-
taskId: id, status, origin: getOrigin(), now,
|
|
97
|
-
text: status
|
|
127
|
+
taskId: id, status, seq, origin: getOrigin(), now,
|
|
128
|
+
text: TERMINAL.has(status) ? (result || carried || '') : (env.prompt || ''),
|
|
98
129
|
envelope: env,
|
|
99
130
|
});
|
|
100
131
|
return env;
|
|
101
132
|
} finally { if (ownDb) db.close(); }
|
|
102
133
|
}
|
|
103
134
|
|
|
104
|
-
/**
|
|
135
|
+
/**
|
|
136
|
+
* Atomically CLAIM a submitted task for execution — prevents double-execution
|
|
137
|
+
* when several executors/cron-ticks poll the same inbox (#3). Optimistic: write
|
|
138
|
+
* a 'working' event stamped with my origin, then re-read — I hold the claim only
|
|
139
|
+
* if the current event is MY working (cmpEvent picks one deterministic winner
|
|
140
|
+
* across nodes once synced). Returns the working envelope on success, or null if
|
|
141
|
+
* the task wasn't claimable (already taken / terminal) or another node won.
|
|
142
|
+
* opts: { db?, now? }.
|
|
143
|
+
*/
|
|
144
|
+
export function claimTask(id, { db = null, now = Date.now() } = {}) {
|
|
145
|
+
const ownDb = !db;
|
|
146
|
+
db = db || openDb();
|
|
147
|
+
try {
|
|
148
|
+
const me = getOrigin();
|
|
149
|
+
const prev = latestEvent(db, id);
|
|
150
|
+
if (!prev) throw new Error(`claimTask: no task "${id}"`);
|
|
151
|
+
if (prev.envelope.status !== 'submitted') return null; // already working/terminal — not claimable
|
|
152
|
+
const seq = (prev.envelope.seq ?? 0) + 1;
|
|
153
|
+
const env = { ...prev.envelope, status: 'working', seq, result: prev.envelope.result ?? null };
|
|
154
|
+
writeEvent(db, { taskId: id, status: 'working', seq, origin: me, now, text: env.prompt || '', envelope: env });
|
|
155
|
+
const after = latestEvent(db, id);
|
|
156
|
+
if (after && after.envelope.status === 'working' && after.origin === me) return after.envelope;
|
|
157
|
+
return null; // another executor's working won the race
|
|
158
|
+
} finally { if (ownDb) db.close(); }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Current event for one task id, or null. Returns { envelope, ts, origin }. */
|
|
105
162
|
function latestEvent(db, id) {
|
|
106
163
|
const rows = db.prepare(
|
|
107
|
-
`SELECT metadata, ts, origin FROM messages
|
|
108
|
-
WHERE source = ? AND conversation_id = ? ORDER BY ts
|
|
164
|
+
`SELECT id AS rowid, metadata, ts, origin FROM messages
|
|
165
|
+
WHERE source = ? AND conversation_id = ? ORDER BY ts ASC, id ASC`
|
|
109
166
|
).all(SOURCE, `task-${id}`);
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
167
|
+
let best = null;
|
|
168
|
+
for (const r of rows) {
|
|
169
|
+
let env; try { env = JSON.parse(r.metadata || '{}'); } catch (_) { continue; }
|
|
170
|
+
const cand = { envelope: env, ts: r.ts, origin: r.origin, status: env.status, seq: env.seq, rowid: r.rowid };
|
|
171
|
+
if (!best || cmpEvent(cand, best) > 0) best = cand;
|
|
172
|
+
}
|
|
173
|
+
return best ? { envelope: best.envelope, ts: best.ts, origin: best.origin } : null;
|
|
114
174
|
}
|
|
115
175
|
|
|
116
176
|
/**
|
|
@@ -128,17 +188,20 @@ export function listTasks({ forOrigin = null, status = null, mine = false, inbox
|
|
|
128
188
|
// All events; reduce to latest per task in JS (low volume; robust to
|
|
129
189
|
// cross-node id ordering — we key on logical ts, not local rowid).
|
|
130
190
|
const rows = db.prepare(
|
|
131
|
-
`SELECT conversation_id, metadata, ts FROM messages
|
|
191
|
+
`SELECT id AS rowid, conversation_id, metadata, ts FROM messages
|
|
132
192
|
WHERE source = ? ORDER BY ts ASC, id ASC`
|
|
133
193
|
).all(SOURCE);
|
|
134
194
|
const latest = new Map();
|
|
135
195
|
for (const r of rows) {
|
|
136
196
|
let env; try { env = JSON.parse(r.metadata || '{}'); } catch (_) { continue; }
|
|
137
197
|
if (!env.task_id) continue;
|
|
138
|
-
|
|
198
|
+
const cand = { ...env, ts: r.ts, rowid: r.rowid };
|
|
199
|
+
const cur = latest.get(env.task_id);
|
|
200
|
+
if (!cur || cmpEvent(cand, cur) > 0) latest.set(env.task_id, cand); // skew-proof: seq, not rowid
|
|
139
201
|
}
|
|
140
202
|
let out = [...latest.values()];
|
|
141
|
-
|
|
203
|
+
// #2 'any' is a broadcast: it lands in EVERY node's inbox (claim resolves the winner).
|
|
204
|
+
if (inbox) out = out.filter((t) => (t.to === me || t.to === 'any') && t.status === 'submitted');
|
|
142
205
|
if (mine) out = out.filter((t) => t.from === me);
|
|
143
206
|
if (forOrigin) out = out.filter((t) => t.to === forOrigin || t.from === forOrigin);
|
|
144
207
|
if (status) out = out.filter((t) => t.status === status);
|
|
@@ -150,6 +213,48 @@ export function listTasks({ forOrigin = null, status = null, mine = false, inbox
|
|
|
150
213
|
} finally { if (ownDb) db.close(); }
|
|
151
214
|
}
|
|
152
215
|
|
|
216
|
+
// ── Result-routing (Phase 2d) ────────────────────────────────────────────────
|
|
217
|
+
// "Answer where you asked": a requester surfaces the results of tasks IT
|
|
218
|
+
// delegated once they're terminal. "Delivered" is per-node UX state (which
|
|
219
|
+
// results this surface already showed) — kept LOCAL in a small JSON file, NOT
|
|
220
|
+
// in the synced ledger (delivery is surface-specific, not task state).
|
|
221
|
+
|
|
222
|
+
function seenPath() {
|
|
223
|
+
const dir = process.env.MEMEX_DIR || join(homedir(), '.memex');
|
|
224
|
+
return join(dir, 'task-results-seen.json');
|
|
225
|
+
}
|
|
226
|
+
function readDelivered() {
|
|
227
|
+
try { return new Set(JSON.parse(readFileSync(seenPath(), 'utf-8')).delivered || []); }
|
|
228
|
+
catch (_) { return new Set(); }
|
|
229
|
+
}
|
|
230
|
+
/** Mark task ids as delivered to this node's surface (idempotent). */
|
|
231
|
+
export function markResultsDelivered(ids = []) {
|
|
232
|
+
if (!ids || !ids.length) return;
|
|
233
|
+
const set = readDelivered();
|
|
234
|
+
for (const id of ids) set.add(id);
|
|
235
|
+
try {
|
|
236
|
+
const p = seenPath();
|
|
237
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
238
|
+
writeFileSync(p, JSON.stringify({ delivered: [...set] }));
|
|
239
|
+
} catch (_) { /* best-effort; re-show next time rather than crash */ }
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Results to route back to a requester surface: tasks THIS origin delegated
|
|
244
|
+
* (from === forOrigin) that are now terminal (done/failed), newest-first.
|
|
245
|
+
* undeliveredOnly drops ids already shown on this node. opts: { forOrigin, undeliveredOnly, db? }.
|
|
246
|
+
*/
|
|
247
|
+
export function listTaskResults({ forOrigin = null, undeliveredOnly = false, db = null } = {}) {
|
|
248
|
+
const ownDb = !db;
|
|
249
|
+
db = db || openDb();
|
|
250
|
+
try {
|
|
251
|
+
const me = forOrigin || getOrigin();
|
|
252
|
+
let out = listTasks({ db, limit: 1000 }).filter((t) => t.from === me && TERMINAL.has(t.status));
|
|
253
|
+
if (undeliveredOnly) { const seen = readDelivered(); out = out.filter((t) => !seen.has(t.id)); }
|
|
254
|
+
return out;
|
|
255
|
+
} finally { if (ownDb) db.close(); }
|
|
256
|
+
}
|
|
257
|
+
|
|
153
258
|
// ── CLI ──────────────────────────────────────────────────────────────────────
|
|
154
259
|
|
|
155
260
|
function parseFlags(argv) {
|
|
@@ -194,6 +299,32 @@ export function cmdTaskUpdate() {
|
|
|
194
299
|
process.exit(0);
|
|
195
300
|
}
|
|
196
301
|
|
|
302
|
+
export function cmdTaskClaim() {
|
|
303
|
+
const args = parseFlags(process.argv.slice(3));
|
|
304
|
+
const id = (args._ || [])[0];
|
|
305
|
+
if (!id) {
|
|
306
|
+
console.error('usage: task-claim <id> [--json] (atomically take a submitted task for execution)');
|
|
307
|
+
process.exit(2);
|
|
308
|
+
}
|
|
309
|
+
let env;
|
|
310
|
+
try { env = claimTask(id); }
|
|
311
|
+
catch (e) { console.error(`✗ ${e.message}`); process.exit(1); }
|
|
312
|
+
if (!env) {
|
|
313
|
+
// exit 3 = "not claimable" (already taken / terminal / lost the race) — the
|
|
314
|
+
// executor loop should treat this as "skip", NOT an error.
|
|
315
|
+
if ('--json' in args) console.log(JSON.stringify({ claimed: false, id }));
|
|
316
|
+
else console.log(`· task ${id} not claimable (already taken or terminal) — skip`);
|
|
317
|
+
process.exit(3);
|
|
318
|
+
}
|
|
319
|
+
if ('--json' in args) { console.log(JSON.stringify({ claimed: true, id, ...env })); process.exit(0); }
|
|
320
|
+
console.log(`✓ claimed ${id} → working (by ${getOrigin()})`);
|
|
321
|
+
console.log(` from ${env.from} · kind: ${env.kind || 'general'}`);
|
|
322
|
+
console.log(` prompt: ${env.prompt || ''}`);
|
|
323
|
+
if (env.content) console.log(` content: ${env.content}`);
|
|
324
|
+
console.log(` on finish: task-update ${id} done --result "<output>" (or: failed --result "<why>")`);
|
|
325
|
+
process.exit(0);
|
|
326
|
+
}
|
|
327
|
+
|
|
197
328
|
export function cmdTaskList() {
|
|
198
329
|
const args = parseFlags(process.argv.slice(3));
|
|
199
330
|
const tasks = listTasks({
|
|
@@ -213,3 +344,28 @@ export function cmdTaskList() {
|
|
|
213
344
|
}
|
|
214
345
|
process.exit(0);
|
|
215
346
|
}
|
|
347
|
+
|
|
348
|
+
// task-results [--ack] [--json] [--all]
|
|
349
|
+
// Results of tasks I delegated, to deliver to my surface (Phase 2d). The
|
|
350
|
+
// requester surface calls this: Claude Code via the SessionStart hook (auto),
|
|
351
|
+
// an OpenClaw/Telegram requester via a cron-prompt that DMs them. --ack marks
|
|
352
|
+
// them delivered so they don't re-show; --all ignores the delivered set.
|
|
353
|
+
export function cmdTaskResults() {
|
|
354
|
+
const args = parseFlags(process.argv.slice(3));
|
|
355
|
+
const results = listTaskResults({ undeliveredOnly: !('--all' in args) });
|
|
356
|
+
if ('--json' in args) {
|
|
357
|
+
console.log(JSON.stringify(results));
|
|
358
|
+
if ('--ack' in args) markResultsDelivered(results.map((r) => r.id));
|
|
359
|
+
process.exit(0);
|
|
360
|
+
}
|
|
361
|
+
if (!results.length) { console.log('No new task results.'); process.exit(0); }
|
|
362
|
+
console.log(`📥 ${results.length} result(s) from tasks you delegated:`);
|
|
363
|
+
for (const t of results) {
|
|
364
|
+
const icon = t.status === 'done' ? '✓' : '✗';
|
|
365
|
+
console.log(`${icon} ${t.id} → ${t.to} [${t.status}]`);
|
|
366
|
+
console.log(` task: ${String(t.prompt || '').slice(0, 80)}`);
|
|
367
|
+
console.log(` result: ${String(t.result || '').slice(0, 200)}`);
|
|
368
|
+
}
|
|
369
|
+
if ('--ack' in args) { markResultsDelivered(results.map((r) => r.id)); console.log(`(${results.length} marked delivered)`); }
|
|
370
|
+
process.exit(0);
|
|
371
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "parallelclaw",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.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",
|