@the-open-engine/zeroshot 6.9.1 → 6.10.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/cli/commands/inspect.js +1 -0
- package/cli/index.js +1 -1
- package/lib/agent-cli-provider/adapters/claude.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/claude.js +15 -2
- package/lib/agent-cli-provider/adapters/claude.js.map +1 -1
- package/lib/agent-cli-provider/adapters/codex.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/codex.js +32 -2
- package/lib/agent-cli-provider/adapters/codex.js.map +1 -1
- package/lib/agent-cli-provider/adapters/common.js +1 -1
- package/lib/agent-cli-provider/adapters/common.js.map +1 -1
- package/lib/agent-cli-provider/adapters/index.d.ts +1 -0
- package/lib/agent-cli-provider/adapters/index.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/index.js +8 -0
- package/lib/agent-cli-provider/adapters/index.js.map +1 -1
- package/lib/agent-cli-provider/contract-options.d.ts.map +1 -1
- package/lib/agent-cli-provider/contract-options.js +1 -0
- package/lib/agent-cli-provider/contract-options.js.map +1 -1
- package/lib/agent-cli-provider/index.d.ts +1 -1
- package/lib/agent-cli-provider/index.d.ts.map +1 -1
- package/lib/agent-cli-provider/index.js +2 -1
- package/lib/agent-cli-provider/index.js.map +1 -1
- package/lib/agent-cli-provider/provider-registry.d.ts +9 -0
- package/lib/agent-cli-provider/provider-registry.d.ts.map +1 -1
- package/lib/agent-cli-provider/provider-registry.js +3 -0
- package/lib/agent-cli-provider/provider-registry.js.map +1 -1
- package/lib/agent-cli-provider/types.d.ts +4 -0
- package/lib/agent-cli-provider/types.d.ts.map +1 -1
- package/lib/agent-cli-provider/types.js.map +1 -1
- package/package.json +1 -1
- package/src/agent/agent-context-builder.js +90 -4
- package/src/agent/agent-context-sources.js +20 -2
- package/src/agent/agent-lifecycle.js +30 -2
- package/src/agent/agent-task-executor.js +31 -1
- package/src/agent/guidance-queue.js +29 -7
- package/src/agent/provider-session.js +277 -0
- package/src/agent-cli-provider/adapters/claude.ts +18 -3
- package/src/agent-cli-provider/adapters/codex.ts +47 -3
- package/src/agent-cli-provider/adapters/common.ts +1 -1
- package/src/agent-cli-provider/adapters/index.ts +10 -0
- package/src/agent-cli-provider/contract-options.ts +1 -0
- package/src/agent-cli-provider/index.ts +1 -0
- package/src/agent-cli-provider/provider-registry.ts +13 -1
- package/src/agent-cli-provider/types.ts +4 -0
- package/src/agent-wrapper.js +44 -8
- package/src/ledger-sequence.js +63 -0
- package/src/ledger.js +114 -29
- package/src/orchestrator.js +40 -2
- package/task-lib/attachable-watcher.js +27 -5
- package/task-lib/commands/resume.js +29 -6
- package/task-lib/commands/status.js +3 -0
- package/task-lib/provider-helper-runtime.js +1 -0
- package/task-lib/provider-session-capture.js +138 -0
- package/task-lib/runner.js +10 -2
- package/task-lib/store.js +58 -6
- package/task-lib/watcher.js +27 -5
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const MAX_SQLITE_ROWID = 9223372036854775807n;
|
|
2
|
+
const CANONICAL_SEQUENCE = /^(0|[1-9][0-9]*)$/;
|
|
3
|
+
|
|
4
|
+
function canonicalMessageSequence(value, name = 'message sequence') {
|
|
5
|
+
let sequence;
|
|
6
|
+
if (typeof value === 'string' && CANONICAL_SEQUENCE.test(value)) {
|
|
7
|
+
sequence = value;
|
|
8
|
+
} else if (Number.isSafeInteger(value) && value >= 0) {
|
|
9
|
+
// Accept legacy JSON state while normalizing every new boundary to a
|
|
10
|
+
// JSON-safe decimal string.
|
|
11
|
+
sequence = String(value);
|
|
12
|
+
} else {
|
|
13
|
+
throw new TypeError(`${name} must be a canonical non-negative decimal string`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (BigInt(sequence) > MAX_SQLITE_ROWID) {
|
|
17
|
+
throw new RangeError(`${name} exceeds the SQLite rowid range`);
|
|
18
|
+
}
|
|
19
|
+
return sequence;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function messageSequenceFromSql(value, name = 'message sequence') {
|
|
23
|
+
if (typeof value === 'bigint') {
|
|
24
|
+
if (value < 0n || value > MAX_SQLITE_ROWID) {
|
|
25
|
+
throw new RangeError(`${name} is outside the SQLite rowid range`);
|
|
26
|
+
}
|
|
27
|
+
return value.toString();
|
|
28
|
+
}
|
|
29
|
+
return canonicalMessageSequence(value, name);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function messageSequenceToSql(value, name = 'message sequence') {
|
|
33
|
+
return BigInt(canonicalMessageSequence(value, name));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function compareMessageSequences(left, right) {
|
|
37
|
+
const leftValue = messageSequenceToSql(left, 'left message sequence');
|
|
38
|
+
const rightValue = messageSequenceToSql(right, 'right message sequence');
|
|
39
|
+
if (leftValue < rightValue) {
|
|
40
|
+
return -1;
|
|
41
|
+
}
|
|
42
|
+
if (leftValue > rightValue) {
|
|
43
|
+
return 1;
|
|
44
|
+
}
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function tryCanonicalMessageSequence(value) {
|
|
49
|
+
try {
|
|
50
|
+
return canonicalMessageSequence(value);
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = {
|
|
57
|
+
MAX_SQLITE_ROWID,
|
|
58
|
+
canonicalMessageSequence,
|
|
59
|
+
compareMessageSequences,
|
|
60
|
+
messageSequenceFromSql,
|
|
61
|
+
messageSequenceToSql,
|
|
62
|
+
tryCanonicalMessageSequence,
|
|
63
|
+
};
|
package/src/ledger.js
CHANGED
|
@@ -16,6 +16,9 @@ const {
|
|
|
16
16
|
USER_GUIDANCE_AGENT,
|
|
17
17
|
USER_GUIDANCE_CLUSTER,
|
|
18
18
|
} = require('./guidance-topics');
|
|
19
|
+
const { messageSequenceFromSql, messageSequenceToSql } = require('./ledger-sequence');
|
|
20
|
+
|
|
21
|
+
const MESSAGE_SEQUENCE_SELECT = 'CAST(rowid AS TEXT) AS sequence';
|
|
19
22
|
|
|
20
23
|
class Ledger extends EventEmitter {
|
|
21
24
|
constructor(dbPath = ':memory:', options = {}) {
|
|
@@ -91,17 +94,23 @@ class Ledger extends EventEmitter {
|
|
|
91
94
|
}
|
|
92
95
|
|
|
93
96
|
_prepareStatements() {
|
|
94
|
-
|
|
95
|
-
insert: this.db.prepare(`
|
|
97
|
+
const insert = this.db.prepare(`
|
|
96
98
|
INSERT INTO messages (id, timestamp, topic, sender, receiver, content_text, content_data, metadata, cluster_id)
|
|
97
99
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
98
|
-
`)
|
|
100
|
+
`);
|
|
101
|
+
insert.safeIntegers(true);
|
|
99
102
|
|
|
100
|
-
|
|
103
|
+
this.stmts = {
|
|
104
|
+
insert,
|
|
105
|
+
|
|
106
|
+
queryBase: `SELECT ${MESSAGE_SEQUENCE_SELECT}, * FROM messages WHERE cluster_id = ?`,
|
|
101
107
|
|
|
102
108
|
count: this.db.prepare(`SELECT COUNT(*) as count FROM messages WHERE cluster_id = ?`),
|
|
103
109
|
|
|
104
|
-
getAll: this.db.prepare(
|
|
110
|
+
getAll: this.db.prepare(
|
|
111
|
+
`SELECT ${MESSAGE_SEQUENCE_SELECT}, * FROM messages WHERE cluster_id = ?` +
|
|
112
|
+
` ORDER BY timestamp ASC, rowid ASC`
|
|
113
|
+
),
|
|
105
114
|
};
|
|
106
115
|
}
|
|
107
116
|
|
|
@@ -145,7 +154,7 @@ class Ledger extends EventEmitter {
|
|
|
145
154
|
};
|
|
146
155
|
|
|
147
156
|
try {
|
|
148
|
-
this.stmts.insert.run(
|
|
157
|
+
const insertResult = this.stmts.insert.run(
|
|
149
158
|
record.id,
|
|
150
159
|
record.timestamp,
|
|
151
160
|
record.topic,
|
|
@@ -156,6 +165,7 @@ class Ledger extends EventEmitter {
|
|
|
156
165
|
record.metadata,
|
|
157
166
|
record.cluster_id
|
|
158
167
|
);
|
|
168
|
+
record.sequence = messageSequenceFromSql(insertResult.lastInsertRowid);
|
|
159
169
|
|
|
160
170
|
// Invalidate cache
|
|
161
171
|
this.cache.clear();
|
|
@@ -219,7 +229,7 @@ class Ledger extends EventEmitter {
|
|
|
219
229
|
cluster_id: message.cluster_id,
|
|
220
230
|
};
|
|
221
231
|
|
|
222
|
-
this.stmts.insert.run(
|
|
232
|
+
const insertResult = this.stmts.insert.run(
|
|
223
233
|
record.id,
|
|
224
234
|
record.timestamp,
|
|
225
235
|
record.topic,
|
|
@@ -230,6 +240,7 @@ class Ledger extends EventEmitter {
|
|
|
230
240
|
record.metadata,
|
|
231
241
|
record.cluster_id
|
|
232
242
|
);
|
|
243
|
+
record.sequence = messageSequenceFromSql(insertResult.lastInsertRowid);
|
|
233
244
|
|
|
234
245
|
results.push(this._deserializeMessage(record));
|
|
235
246
|
}
|
|
@@ -265,7 +276,19 @@ class Ledger extends EventEmitter {
|
|
|
265
276
|
* @returns {Array} Matching messages
|
|
266
277
|
*/
|
|
267
278
|
query(criteria) {
|
|
268
|
-
const {
|
|
279
|
+
const {
|
|
280
|
+
cluster_id,
|
|
281
|
+
topic,
|
|
282
|
+
sender,
|
|
283
|
+
receiver,
|
|
284
|
+
since,
|
|
285
|
+
after,
|
|
286
|
+
until,
|
|
287
|
+
afterId,
|
|
288
|
+
throughId,
|
|
289
|
+
limit,
|
|
290
|
+
offset,
|
|
291
|
+
} = criteria;
|
|
269
292
|
|
|
270
293
|
if (!cluster_id) {
|
|
271
294
|
throw new Error('cluster_id is required for queries');
|
|
@@ -295,18 +318,44 @@ class Ledger extends EventEmitter {
|
|
|
295
318
|
params.push(typeof since === 'number' ? since : new Date(since).getTime());
|
|
296
319
|
}
|
|
297
320
|
|
|
321
|
+
if (after !== undefined && after !== null) {
|
|
322
|
+
if (!Number.isInteger(after) || after < 0) {
|
|
323
|
+
throw new Error('after must be a non-negative durable ledger cursor');
|
|
324
|
+
}
|
|
325
|
+
conditions.push('timestamp > ?');
|
|
326
|
+
params.push(after);
|
|
327
|
+
}
|
|
328
|
+
|
|
298
329
|
if (until) {
|
|
299
330
|
conditions.push('timestamp <= ?');
|
|
300
331
|
params.push(typeof until === 'number' ? until : new Date(until).getTime());
|
|
301
332
|
}
|
|
302
333
|
|
|
334
|
+
if (afterId !== undefined && afterId !== null) {
|
|
335
|
+
conditions.push('rowid > ?');
|
|
336
|
+
params.push(messageSequenceToSql(afterId, 'afterId'));
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (throughId !== undefined && throughId !== null) {
|
|
340
|
+
conditions.push('rowid <= ?');
|
|
341
|
+
params.push(messageSequenceToSql(throughId, 'throughId'));
|
|
342
|
+
}
|
|
343
|
+
|
|
303
344
|
// Defend against prototype pollution affecting default query ordering.
|
|
304
345
|
// Only treat `criteria.order` as set if it's an own property.
|
|
305
346
|
const orderValue = Object.prototype.hasOwnProperty.call(criteria, 'order')
|
|
306
347
|
? criteria.order
|
|
307
348
|
: undefined;
|
|
308
349
|
const direction = String(orderValue ?? 'asc').toLowerCase() === 'desc' ? 'DESC' : 'ASC';
|
|
309
|
-
|
|
350
|
+
const sequenceBounded =
|
|
351
|
+
(afterId !== undefined && afterId !== null) ||
|
|
352
|
+
(throughId !== undefined && throughId !== null);
|
|
353
|
+
const orderClause = sequenceBounded
|
|
354
|
+
? `rowid ${direction}`
|
|
355
|
+
: `timestamp ${direction}, rowid ${direction}`;
|
|
356
|
+
let sql =
|
|
357
|
+
`SELECT ${MESSAGE_SEQUENCE_SELECT}, * FROM messages WHERE ${conditions.join(' AND ')}` +
|
|
358
|
+
` ORDER BY ${orderClause}`;
|
|
310
359
|
|
|
311
360
|
if (limit) {
|
|
312
361
|
sql += ` LIMIT ?`;
|
|
@@ -325,11 +374,12 @@ class Ledger extends EventEmitter {
|
|
|
325
374
|
|
|
326
375
|
/**
|
|
327
376
|
* Query guidance mailbox for cluster-wide + agent-specific guidance
|
|
328
|
-
* @param {Object} criteria -
|
|
329
|
-
* @returns {Array} Guidance messages ordered by
|
|
377
|
+
* @param {Object} criteria - Timestamp compatibility filters plus afterId/throughId sequences
|
|
378
|
+
* @returns {Array} Guidance messages ordered by durable message sequence ASC
|
|
330
379
|
*/
|
|
331
380
|
queryGuidanceMailbox(criteria) {
|
|
332
|
-
const { cluster_id, target_agent_id, lastDeliveredAt, limit } =
|
|
381
|
+
const { cluster_id, target_agent_id, lastDeliveredAt, afterId, throughId, limit } =
|
|
382
|
+
criteria || {};
|
|
333
383
|
|
|
334
384
|
if (!cluster_id) {
|
|
335
385
|
throw new Error('cluster_id is required for guidance mailbox queries');
|
|
@@ -351,7 +401,8 @@ class Ledger extends EventEmitter {
|
|
|
351
401
|
}
|
|
352
402
|
|
|
353
403
|
const params = [cluster_id, USER_GUIDANCE_CLUSTER];
|
|
354
|
-
let sql =
|
|
404
|
+
let sql =
|
|
405
|
+
`SELECT ${MESSAGE_SEQUENCE_SELECT}, * FROM messages ` + 'WHERE cluster_id = ? AND (topic = ?';
|
|
355
406
|
|
|
356
407
|
if (target_agent_id) {
|
|
357
408
|
params.push(USER_GUIDANCE_AGENT, target_agent_id);
|
|
@@ -365,7 +416,20 @@ class Ledger extends EventEmitter {
|
|
|
365
416
|
sql += ' AND timestamp > ?';
|
|
366
417
|
}
|
|
367
418
|
|
|
368
|
-
|
|
419
|
+
if (afterId !== undefined && afterId !== null) {
|
|
420
|
+
params.push(messageSequenceToSql(afterId, 'afterId'));
|
|
421
|
+
sql += ' AND rowid > ?';
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (throughId !== undefined && throughId !== null) {
|
|
425
|
+
params.push(messageSequenceToSql(throughId, 'throughId'));
|
|
426
|
+
sql += ' AND rowid <= ?';
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
sql +=
|
|
430
|
+
(afterId !== undefined && afterId !== null) || (throughId !== undefined && throughId !== null)
|
|
431
|
+
? ' ORDER BY rowid ASC'
|
|
432
|
+
: ' ORDER BY timestamp ASC, rowid ASC';
|
|
369
433
|
|
|
370
434
|
if (limit) {
|
|
371
435
|
params.push(limit);
|
|
@@ -383,7 +447,8 @@ class Ledger extends EventEmitter {
|
|
|
383
447
|
* @returns {Object|null} Last matching message
|
|
384
448
|
*/
|
|
385
449
|
findLast(criteria) {
|
|
386
|
-
const { cluster_id, topic, sender, receiver, since, until } =
|
|
450
|
+
const { cluster_id, topic, sender, receiver, since, until, throughId, orderBySequence } =
|
|
451
|
+
criteria;
|
|
387
452
|
|
|
388
453
|
if (!cluster_id) {
|
|
389
454
|
throw new Error('cluster_id is required for queries');
|
|
@@ -418,7 +483,16 @@ class Ledger extends EventEmitter {
|
|
|
418
483
|
params.push(typeof until === 'number' ? until : new Date(until).getTime());
|
|
419
484
|
}
|
|
420
485
|
|
|
421
|
-
|
|
486
|
+
if (throughId !== undefined && throughId !== null) {
|
|
487
|
+
conditions.push('rowid <= ?');
|
|
488
|
+
params.push(messageSequenceToSql(throughId, 'throughId'));
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const orderClause =
|
|
492
|
+
orderBySequence || throughId !== undefined ? 'rowid DESC' : 'timestamp DESC, rowid DESC';
|
|
493
|
+
const sql =
|
|
494
|
+
`SELECT ${MESSAGE_SEQUENCE_SELECT}, * FROM messages WHERE ${conditions.join(' AND ')}` +
|
|
495
|
+
` ORDER BY ${orderClause} LIMIT 1`;
|
|
422
496
|
|
|
423
497
|
const stmt = this.db.prepare(sql);
|
|
424
498
|
const row = stmt.get(...params);
|
|
@@ -495,7 +569,9 @@ class Ledger extends EventEmitter {
|
|
|
495
569
|
*/
|
|
496
570
|
_computeTokensByRole(cluster_id) {
|
|
497
571
|
// Query all TOKEN_USAGE messages for this cluster
|
|
498
|
-
const sql =
|
|
572
|
+
const sql =
|
|
573
|
+
`SELECT ${MESSAGE_SEQUENCE_SELECT}, * FROM messages WHERE cluster_id = ?` +
|
|
574
|
+
` AND topic = 'TOKEN_USAGE' ORDER BY timestamp ASC, rowid ASC`;
|
|
499
575
|
const stmt = this.db.prepare(sql);
|
|
500
576
|
const rows = stmt.all(cluster_id);
|
|
501
577
|
|
|
@@ -589,7 +665,7 @@ class Ledger extends EventEmitter {
|
|
|
589
665
|
* @returns {Function} Stop polling function
|
|
590
666
|
*/
|
|
591
667
|
pollForMessages(clusterId, callback, intervalMs = 500, initialCount = 300) {
|
|
592
|
-
let
|
|
668
|
+
let lastSequence = '0';
|
|
593
669
|
let lastMessageIds = new Set();
|
|
594
670
|
let isFirstPoll = true;
|
|
595
671
|
|
|
@@ -601,23 +677,29 @@ class Ledger extends EventEmitter {
|
|
|
601
677
|
// First poll: get last N messages by count
|
|
602
678
|
if (clusterId) {
|
|
603
679
|
sql =
|
|
604
|
-
|
|
680
|
+
`SELECT * FROM (SELECT ${MESSAGE_SEQUENCE_SELECT}, * FROM messages ` +
|
|
681
|
+
'WHERE cluster_id = ? ORDER BY rowid DESC LIMIT ?) ' +
|
|
682
|
+
'ORDER BY CAST(sequence AS INTEGER) ASC';
|
|
605
683
|
params = [clusterId, initialCount];
|
|
606
684
|
} else {
|
|
607
685
|
sql =
|
|
608
|
-
|
|
686
|
+
`SELECT * FROM (SELECT ${MESSAGE_SEQUENCE_SELECT}, * FROM messages ` +
|
|
687
|
+
'ORDER BY rowid DESC LIMIT ?) ORDER BY CAST(sequence AS INTEGER) ASC';
|
|
609
688
|
params = [initialCount];
|
|
610
689
|
}
|
|
611
690
|
isFirstPoll = false;
|
|
612
691
|
} else {
|
|
613
|
-
// Subsequent polls: get messages
|
|
692
|
+
// Subsequent polls: get messages after the exact durable sequence.
|
|
614
693
|
if (clusterId) {
|
|
615
694
|
sql =
|
|
616
|
-
|
|
617
|
-
|
|
695
|
+
`SELECT ${MESSAGE_SEQUENCE_SELECT}, * FROM messages ` +
|
|
696
|
+
'WHERE cluster_id = ? AND rowid > ? ORDER BY rowid ASC';
|
|
697
|
+
params = [clusterId, messageSequenceToSql(lastSequence)];
|
|
618
698
|
} else {
|
|
619
|
-
sql =
|
|
620
|
-
|
|
699
|
+
sql =
|
|
700
|
+
`SELECT ${MESSAGE_SEQUENCE_SELECT}, * FROM messages ` +
|
|
701
|
+
'WHERE rowid > ? ORDER BY rowid ASC';
|
|
702
|
+
params = [messageSequenceToSql(lastSequence)];
|
|
621
703
|
}
|
|
622
704
|
}
|
|
623
705
|
|
|
@@ -632,10 +714,9 @@ class Ledger extends EventEmitter {
|
|
|
632
714
|
const message = this._deserializeMessage(row);
|
|
633
715
|
callback(message);
|
|
634
716
|
|
|
635
|
-
//
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
}
|
|
717
|
+
// Rows are delivered in SQLite rowid order, so the final row is the
|
|
718
|
+
// exact database-serialized high-water mark.
|
|
719
|
+
lastSequence = messageSequenceFromSql(row.sequence);
|
|
639
720
|
}
|
|
640
721
|
|
|
641
722
|
// Prune old message IDs to prevent memory leak
|
|
@@ -679,6 +760,10 @@ class Ledger extends EventEmitter {
|
|
|
679
760
|
_deserializeMessage(row) {
|
|
680
761
|
const message = {
|
|
681
762
|
id: row.id,
|
|
763
|
+
sequence:
|
|
764
|
+
row.sequence === undefined
|
|
765
|
+
? undefined
|
|
766
|
+
: messageSequenceFromSql(row.sequence, 'stored message sequence'),
|
|
682
767
|
timestamp: row.timestamp,
|
|
683
768
|
topic: row.topic,
|
|
684
769
|
sender: row.sender,
|
package/src/orchestrator.js
CHANGED
|
@@ -52,6 +52,10 @@ const { isProcessRunning } = require('../lib/process-liveness');
|
|
|
52
52
|
const { getProvider } = require('./providers');
|
|
53
53
|
const StateSnapshotter = require('./state-snapshotter');
|
|
54
54
|
const { resolveClusterRequiredQualityGates } = require('./quality-gates');
|
|
55
|
+
const {
|
|
56
|
+
normalizeProviderSession,
|
|
57
|
+
restoreAgentProviderSession,
|
|
58
|
+
} = require('./agent/provider-session');
|
|
55
59
|
const {
|
|
56
60
|
commandProofsToQualityGates,
|
|
57
61
|
mergeCommandProofs,
|
|
@@ -231,6 +235,19 @@ function buildPrOptions(options, requiredQualityGates) {
|
|
|
231
235
|
};
|
|
232
236
|
}
|
|
233
237
|
|
|
238
|
+
function serializeWorktree(worktree) {
|
|
239
|
+
if (!worktree) {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
enabled: true,
|
|
244
|
+
path: worktree.path,
|
|
245
|
+
branch: worktree.branch,
|
|
246
|
+
repoRoot: worktree.repoRoot,
|
|
247
|
+
workDir: worktree.workDir,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
234
251
|
class Orchestrator {
|
|
235
252
|
constructor(options = {}) {
|
|
236
253
|
this.clusters = new Map(); // cluster_id -> cluster object
|
|
@@ -645,6 +662,15 @@ class Orchestrator {
|
|
|
645
662
|
};
|
|
646
663
|
}
|
|
647
664
|
|
|
665
|
+
if (clusterData.worktree?.enabled) {
|
|
666
|
+
agentOptions.worktree = {
|
|
667
|
+
enabled: true,
|
|
668
|
+
path: clusterData.worktree.path,
|
|
669
|
+
branch: clusterData.worktree.branch,
|
|
670
|
+
repoRoot: clusterData.worktree.repoRoot,
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
|
|
648
674
|
return agentOptions;
|
|
649
675
|
}
|
|
650
676
|
|
|
@@ -656,7 +682,7 @@ class Orchestrator {
|
|
|
656
682
|
return new AgentWrapper(agentConfig, messageBus, clusterContext, agentOptions);
|
|
657
683
|
}
|
|
658
684
|
|
|
659
|
-
_restoreAgentState(agent, agentConfig, clusterData) {
|
|
685
|
+
_restoreAgentState(agent, agentConfig, clusterData, messageBus) {
|
|
660
686
|
if (!clusterData.agentStates) return;
|
|
661
687
|
|
|
662
688
|
const savedState = clusterData.agentStates.find((state) => state.id === agentConfig.id);
|
|
@@ -670,6 +696,13 @@ class Orchestrator {
|
|
|
670
696
|
agent.currentTask = null;
|
|
671
697
|
agent.currentTaskId = savedState.currentTaskId || null;
|
|
672
698
|
agent.processPid = savedState.processPid || null;
|
|
699
|
+
agent.providerSession = restoreAgentProviderSession({
|
|
700
|
+
agent,
|
|
701
|
+
savedState,
|
|
702
|
+
messageBus,
|
|
703
|
+
clusterId: clusterData.id,
|
|
704
|
+
});
|
|
705
|
+
agent.lastGuidanceAppliedId = agent.providerSession?.guidanceSequence ?? null;
|
|
673
706
|
}
|
|
674
707
|
|
|
675
708
|
_rebuildClusterAgents(clusterContext, messageBus, isolation, isolationManager) {
|
|
@@ -699,7 +732,7 @@ class Orchestrator {
|
|
|
699
732
|
isolationManager
|
|
700
733
|
);
|
|
701
734
|
const agent = this._instantiateAgent(agentConfig, messageBus, clusterContext, agentOptions);
|
|
702
|
-
this._restoreAgentState(agent, agentConfig, clusterData);
|
|
735
|
+
this._restoreAgentState(agent, agentConfig, clusterData, messageBus);
|
|
703
736
|
agents.push(agent);
|
|
704
737
|
}
|
|
705
738
|
|
|
@@ -879,6 +912,7 @@ class Orchestrator {
|
|
|
879
912
|
workDir: cluster.isolation.workDir, // Required for resume
|
|
880
913
|
}
|
|
881
914
|
: null,
|
|
915
|
+
worktree: serializeWorktree(cluster.worktree),
|
|
882
916
|
// Persist agent runtime states for accurate status display from other processes
|
|
883
917
|
agentStates: cluster.agents
|
|
884
918
|
? cluster.agents.map((a) => ({
|
|
@@ -888,6 +922,8 @@ class Orchestrator {
|
|
|
888
922
|
currentTask: a.currentTask ? true : false,
|
|
889
923
|
currentTaskId: a.currentTaskId,
|
|
890
924
|
processPid: a.processPid,
|
|
925
|
+
providerSession: normalizeProviderSession(a.providerSession),
|
|
926
|
+
lastGuidanceAppliedId: a.lastGuidanceAppliedId ?? null,
|
|
891
927
|
}))
|
|
892
928
|
: null,
|
|
893
929
|
setupLogPath: cluster.setupLogPath || null,
|
|
@@ -1585,6 +1621,8 @@ class Orchestrator {
|
|
|
1585
1621
|
[
|
|
1586
1622
|
'TASK_STARTED',
|
|
1587
1623
|
'TASK_COMPLETED',
|
|
1624
|
+
'TASK_FAILED',
|
|
1625
|
+
'RETRY_SCHEDULED',
|
|
1588
1626
|
'PROCESS_SPAWNED',
|
|
1589
1627
|
'TASK_ID_ASSIGNED',
|
|
1590
1628
|
'STARTED',
|
|
@@ -7,13 +7,14 @@
|
|
|
7
7
|
|
|
8
8
|
import { appendFileSync, unlinkSync } from 'fs';
|
|
9
9
|
import { unlink } from 'fs/promises';
|
|
10
|
-
import { updateTask } from './store.js';
|
|
10
|
+
import { getTask, updateTask } from './store.js';
|
|
11
11
|
import {
|
|
12
12
|
detectProviderFatalError,
|
|
13
13
|
detectProviderStreamingModeError,
|
|
14
14
|
recoverProviderStructuredOutput,
|
|
15
15
|
supportsProviderStructuredOutputRecovery,
|
|
16
16
|
} from './provider-helper-runtime.js';
|
|
17
|
+
import { createProviderSessionCapture } from './provider-session-capture.js';
|
|
17
18
|
import { createRequire } from 'module';
|
|
18
19
|
|
|
19
20
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -95,6 +96,17 @@ function log(msg) {
|
|
|
95
96
|
|
|
96
97
|
const providerName = normalizeProviderName(config.provider || 'claude');
|
|
97
98
|
const enableRecovery = supportsProviderStructuredOutputRecovery(providerName);
|
|
99
|
+
const storedTask = getTask(taskId);
|
|
100
|
+
const providerSessionCapture = createProviderSessionCapture({
|
|
101
|
+
providerName,
|
|
102
|
+
taskId,
|
|
103
|
+
updateTask,
|
|
104
|
+
log,
|
|
105
|
+
requestedSessionId: storedTask?.requestedResumeSessionId || null,
|
|
106
|
+
initialSessionId: storedTask?.sessionId || null,
|
|
107
|
+
initialSessionIdConflict: storedTask?.sessionIdConflict === true,
|
|
108
|
+
});
|
|
109
|
+
const maybeCaptureProviderSession = providerSessionCapture.captureLine;
|
|
98
110
|
|
|
99
111
|
const env = { ...process.env, ...(commandSpec.env || {}) };
|
|
100
112
|
const command = commandSpec.binary;
|
|
@@ -167,6 +179,7 @@ function maybeCaptureStructuredOutput(line) {
|
|
|
167
179
|
function handleSilentJsonLines(lines, timestamp) {
|
|
168
180
|
for (const line of lines) {
|
|
169
181
|
if (!line.trim()) continue;
|
|
182
|
+
maybeCaptureProviderSession(line);
|
|
170
183
|
maybeHandleFatalError(line, timestamp);
|
|
171
184
|
if (captureStreamingError(line, timestamp)) {
|
|
172
185
|
continue;
|
|
@@ -177,6 +190,7 @@ function handleSilentJsonLines(lines, timestamp) {
|
|
|
177
190
|
|
|
178
191
|
function handleStreamingLines(lines, timestamp) {
|
|
179
192
|
for (const line of lines) {
|
|
193
|
+
maybeCaptureProviderSession(line);
|
|
180
194
|
maybeHandleFatalError(line, timestamp);
|
|
181
195
|
if (captureStreamingError(line, timestamp)) {
|
|
182
196
|
continue;
|
|
@@ -190,6 +204,7 @@ function flushOutputBuffer(timestamp) {
|
|
|
190
204
|
return;
|
|
191
205
|
}
|
|
192
206
|
|
|
207
|
+
maybeCaptureProviderSession(outputBuffer);
|
|
193
208
|
if (!enableRecovery) {
|
|
194
209
|
if (!silentJsonMode) {
|
|
195
210
|
log(`[${timestamp}]${outputBuffer}\n`);
|
|
@@ -305,18 +320,25 @@ server.on('exit', async ({ exitCode, signal }) => {
|
|
|
305
320
|
log(finalResultJson + '\n');
|
|
306
321
|
}
|
|
307
322
|
|
|
308
|
-
|
|
323
|
+
const sessionIdentityError = providerSessionCapture.getCompletionError();
|
|
324
|
+
const resolvedCode =
|
|
325
|
+
fatalError || sessionIdentityError ? 1 : recovered?.payload ? 0 : code;
|
|
326
|
+
const status = resolvedCode === 0 ? 'completed' : 'failed';
|
|
327
|
+
|
|
328
|
+
writeCompletionFooter(resolvedCode, signal);
|
|
309
329
|
await cleanupCommandSpec();
|
|
310
330
|
|
|
311
|
-
const resolvedCode = fatalError ? 1 : recovered?.payload ? 0 : code;
|
|
312
|
-
const status = resolvedCode === 0 ? 'completed' : 'failed';
|
|
313
331
|
try {
|
|
314
332
|
await updateTask(taskId, {
|
|
315
333
|
status,
|
|
316
334
|
pid: null,
|
|
317
335
|
processGroupId: null,
|
|
318
336
|
exitCode: resolvedCode,
|
|
319
|
-
|
|
337
|
+
...providerSessionCapture.getCompletionUpdate(resolvedCode),
|
|
338
|
+
error:
|
|
339
|
+
fatalError ||
|
|
340
|
+
sessionIdentityError ||
|
|
341
|
+
(resolvedCode !== 0 && signal ? `Killed by ${signal}` : null),
|
|
320
342
|
socketPath: null,
|
|
321
343
|
});
|
|
322
344
|
} catch (updateError) {
|
|
@@ -1,7 +1,35 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
+
import { createRequire } from 'module';
|
|
2
3
|
import { getTask } from '../store.js';
|
|
3
4
|
import { spawnTask } from '../runner.js';
|
|
4
5
|
|
|
6
|
+
const require = createRequire(import.meta.url);
|
|
7
|
+
const { providerSupportsCapability } = require('../../lib/provider-names.js');
|
|
8
|
+
|
|
9
|
+
export function buildResumeTaskOptions(task) {
|
|
10
|
+
if (!providerSupportsCapability(task.provider, 'sessionResume')) {
|
|
11
|
+
throw new Error(`Provider ${task.provider} does not support safe session resume.`);
|
|
12
|
+
}
|
|
13
|
+
if (
|
|
14
|
+
task.requestedResumeSessionId &&
|
|
15
|
+
(task.status !== 'completed' || task.resumeIdentityVerified !== true)
|
|
16
|
+
) {
|
|
17
|
+
throw new Error(
|
|
18
|
+
`Task ${task.id} did not durably verify its requested provider session; refusing resume.`
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
if (!task.sessionId) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
`Task ${task.id} has no captured provider session ID; refusing cwd-wide continuation.`
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
cwd: task.cwd,
|
|
28
|
+
resume: task.sessionId,
|
|
29
|
+
provider: task.provider,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
5
33
|
export async function resumeTask(taskId, newPrompt) {
|
|
6
34
|
const task = getTask(taskId);
|
|
7
35
|
|
|
@@ -23,12 +51,7 @@ export async function resumeTask(taskId, newPrompt) {
|
|
|
23
51
|
console.log(chalk.dim(`Original prompt: ${task.prompt}`));
|
|
24
52
|
console.log(chalk.dim(`Resume prompt: ${prompt}`));
|
|
25
53
|
|
|
26
|
-
const newTask = await spawnTask(prompt,
|
|
27
|
-
cwd: task.cwd,
|
|
28
|
-
continue: true, // Use --continue to load most recent session in that directory
|
|
29
|
-
sessionId: task.sessionId,
|
|
30
|
-
provider: task.provider,
|
|
31
|
-
});
|
|
54
|
+
const newTask = await spawnTask(prompt, buildResumeTaskOptions(task));
|
|
32
55
|
|
|
33
56
|
console.log(chalk.green(`\n✓ Resumed as new task: ${chalk.cyan(newTask.id)}`));
|
|
34
57
|
console.log(chalk.dim(` PID: ${newTask.pid}`));
|
|
@@ -41,6 +41,9 @@ export function showStatus(taskId) {
|
|
|
41
41
|
console.log(`${chalk.dim('PID:')} ${task.pid || 'N/A'}`);
|
|
42
42
|
console.log(`${chalk.dim('Exit Code:')} ${task.exitCode ?? 'N/A'}`);
|
|
43
43
|
console.log(`${chalk.dim('Session:')} ${task.sessionId || 'N/A'}`);
|
|
44
|
+
if (task.requestedResumeSessionId) {
|
|
45
|
+
console.log(`${chalk.dim('Requested:')} ${task.requestedResumeSessionId}`);
|
|
46
|
+
}
|
|
44
47
|
console.log(`${chalk.dim('Log File:')} ${task.logFile}`);
|
|
45
48
|
|
|
46
49
|
console.log(`\n${chalk.dim('Prompt:')}`);
|