parallelclaw 1.2.1 → 1.2.2
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 +19 -0
- package/lib/executor.js +3 -1
- package/lib/tasks.js +21 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
Notable changes to parallelclaw (formerly memex-mvp). Older history lives in the git log.
|
|
4
4
|
|
|
5
|
+
## 1.2.2 — durable execution: claim lease + auto-requeue for dead executors
|
|
6
|
+
|
|
7
|
+
Found live: a research task outran Kimi's 240-second cron tick. The executor had
|
|
8
|
+
already written `working` (the claim), but its agentTurn was killed before it
|
|
9
|
+
could write a terminal event → the task was stranded in `working` forever. Fixes:
|
|
10
|
+
- **Claim lease + lazy auto-requeue (#3b).** A `working` claim is leased (default
|
|
11
|
+
15 min). A stale `working` (claimer died — cron timeout, crash, laptop sleep)
|
|
12
|
+
becomes claimable again and reappears in `task-list --inbox`, so the next
|
|
13
|
+
poller retries it. No task is lost to a dead executor, and no sweeper is needed.
|
|
14
|
+
`claimTask`/`listTasks` gain `now`/`leaseMs` knobs; the Mac executor passes them.
|
|
15
|
+
- **Executor prompt + docs:** read the FULL prompt from the `task-claim` output
|
|
16
|
+
(or `task-claim <id> --json`), never the 80-char `task-list` display line; on a
|
|
17
|
+
too-big task write `failed` with partial progress instead of spinning until the
|
|
18
|
+
run is killed or closing `done` with a placeholder (which would block requeue —
|
|
19
|
+
the exact thing that lost the first research delegation).
|
|
20
|
+
|
|
21
|
+
Tests: +2 (stale working is re-claimable and back in inbox; terminal never
|
|
22
|
+
requeues). Full npm test green.
|
|
23
|
+
|
|
5
24
|
## 1.2.1 — patch: make the Mac executor's `claude -p` actually complete
|
|
6
25
|
|
|
7
26
|
The Phase 2b headless executor invoked `claude -p` but every run hung until the
|
package/lib/executor.js
CHANGED
|
@@ -104,7 +104,9 @@ export async function runInboxOnce({
|
|
|
104
104
|
const me = getOrigin();
|
|
105
105
|
const exec = runner || claudeRunner;
|
|
106
106
|
const acted = [];
|
|
107
|
-
|
|
107
|
+
// Passing `now` keeps the lease/stale-working check on the same clock as claimTask
|
|
108
|
+
// below, and lets the executor pick up tasks abandoned by a dead claimer (#3b).
|
|
109
|
+
let inbox = listTasks({ inbox: true, db, limit: 50, now });
|
|
108
110
|
inbox = inbox.filter((t) => t.to === me || (includeBroadcast && t.to === 'any'));
|
|
109
111
|
for (const t of inbox) {
|
|
110
112
|
if (!allowKinds.includes(t.kind)) {
|
package/lib/tasks.js
CHANGED
|
@@ -34,6 +34,17 @@ const SOURCE = 'agent-task';
|
|
|
34
34
|
const STATUSES = ['submitted', 'working', 'done', 'failed'];
|
|
35
35
|
const TERMINAL = new Set(['done', 'failed']);
|
|
36
36
|
|
|
37
|
+
// A claimed ('working') task whose claimer dies (cron agentTurn timeout, crash,
|
|
38
|
+
// laptop sleep) would otherwise be stranded in 'working' forever. We LEASE the
|
|
39
|
+
// claim: a 'working' event older than LEASE_MS is treated as abandoned and
|
|
40
|
+
// becomes re-claimable by the next poller (lazy requeue — no sweeper, no extra
|
|
41
|
+
// event type). Generous so modest cross-node clock skew can't expire a LIVE
|
|
42
|
+
// claim. Tunable per call via opts.leaseMs.
|
|
43
|
+
const LEASE_MS = 15 * 60 * 1000;
|
|
44
|
+
function isStaleWorking(status, eventTsSec, nowMs, leaseMs) {
|
|
45
|
+
return status === 'working' && (nowMs - (eventTsSec || 0) * 1000) > leaseMs;
|
|
46
|
+
}
|
|
47
|
+
|
|
37
48
|
// Pick the CURRENT event of a task WITHOUT trusting wall-clock ts — clock skew
|
|
38
49
|
// across nodes regressed state on the live mesh (a 'done' on a fast clock could
|
|
39
50
|
// sort before a later 'working'). Order by the monotonic per-task `seq`, then a
|
|
@@ -141,14 +152,18 @@ export function updateTask(id, status, { result = null, db = null, now = Date.no
|
|
|
141
152
|
* the task wasn't claimable (already taken / terminal) or another node won.
|
|
142
153
|
* opts: { db?, now? }.
|
|
143
154
|
*/
|
|
144
|
-
export function claimTask(id, { db = null, now = Date.now() } = {}) {
|
|
155
|
+
export function claimTask(id, { db = null, now = Date.now(), leaseMs = LEASE_MS } = {}) {
|
|
145
156
|
const ownDb = !db;
|
|
146
157
|
db = db || openDb();
|
|
147
158
|
try {
|
|
148
159
|
const me = getOrigin();
|
|
149
160
|
const prev = latestEvent(db, id);
|
|
150
161
|
if (!prev) throw new Error(`claimTask: no task "${id}"`);
|
|
151
|
-
|
|
162
|
+
const status = prev.envelope.status;
|
|
163
|
+
// Claimable if fresh-submitted OR a STALE 'working' (its claimer died — the
|
|
164
|
+
// lease expired). A fresh 'working' or a terminal state is not claimable.
|
|
165
|
+
const reclaimStale = isStaleWorking(status, prev.ts, now, leaseMs);
|
|
166
|
+
if (status !== 'submitted' && !reclaimStale) return null;
|
|
152
167
|
const seq = (prev.envelope.seq ?? 0) + 1;
|
|
153
168
|
const env = { ...prev.envelope, status: 'working', seq, result: prev.envelope.result ?? null };
|
|
154
169
|
writeEvent(db, { taskId: id, status: 'working', seq, origin: me, now, text: env.prompt || '', envelope: env });
|
|
@@ -180,7 +195,7 @@ function latestEvent(db, id) {
|
|
|
180
195
|
* inbox — tasks addressed to me AND currently 'submitted' (ready to take)
|
|
181
196
|
* Returns [{ id, from, to, kind, status, prompt, result, ts }] newest-first.
|
|
182
197
|
*/
|
|
183
|
-
export function listTasks({ forOrigin = null, status = null, mine = false, inbox = false, limit = 50, db = null } = {}) {
|
|
198
|
+
export function listTasks({ forOrigin = null, status = null, mine = false, inbox = false, limit = 50, db = null, now = Date.now(), leaseMs = LEASE_MS } = {}) {
|
|
184
199
|
const ownDb = !db;
|
|
185
200
|
db = db || openDb();
|
|
186
201
|
try {
|
|
@@ -201,7 +216,9 @@ export function listTasks({ forOrigin = null, status = null, mine = false, inbox
|
|
|
201
216
|
}
|
|
202
217
|
let out = [...latest.values()];
|
|
203
218
|
// #2 'any' is a broadcast: it lands in EVERY node's inbox (claim resolves the winner).
|
|
204
|
-
|
|
219
|
+
// inbox = ready to run: 'submitted', OR a stale 'working' whose claimer died.
|
|
220
|
+
if (inbox) out = out.filter((t) => (t.to === me || t.to === 'any')
|
|
221
|
+
&& (t.status === 'submitted' || isStaleWorking(t.status, t.ts, now, leaseMs)));
|
|
205
222
|
if (mine) out = out.filter((t) => t.from === me);
|
|
206
223
|
if (forOrigin) out = out.filter((t) => t.to === forOrigin || t.from === forOrigin);
|
|
207
224
|
if (status) out = out.filter((t) => t.status === status);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "parallelclaw",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.2",
|
|
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",
|