@bridge4dev/runner 0.64.1 → 0.65.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/dist/adapters/agent-tasks.d.ts +113 -0
- package/dist/adapters/agent-tasks.js +260 -0
- package/dist/adapters/claude.js +45 -236
- package/dist/adapters/codex-subagents.d.ts +169 -0
- package/dist/adapters/codex-subagents.js +569 -0
- package/dist/adapters/codex.d.ts +4 -0
- package/dist/adapters/codex.js +194 -18
- package/dist/supervisor.d.ts +20 -0
- package/dist/supervisor.js +51 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/adapters/claude.js
CHANGED
|
@@ -10,6 +10,7 @@ import { cageSpawn, noteSessionAgentPid, memoryDeathSentence, releaseSessionScop
|
|
|
10
10
|
import { evaluateToolUse, maskSecrets, maskString, } from '../policy.js';
|
|
11
11
|
import { availableModes, cardDescription, DIRECT_BRANCH_RULE, folderRuleFor, MODE_REFUSED_TEXT, MODE_WITHDRAWN_TEXT, policyContextFor, DEVBRIDGE_MCP_SERVER_NAME, } from './types.js';
|
|
12
12
|
import { percentFromUtilization, RATE_WINDOW_MINUTES, rateWindowKey } from './rate-limits.js';
|
|
13
|
+
import { AgentTaskTray } from './agent-tasks.js';
|
|
13
14
|
import { applyUsagePercentages, lastUsageRows, readUsageRows } from './claude-usage.js';
|
|
14
15
|
import { assertClaudeInstalled, claudeExecutableOption, sessionClaudePath, } from '../agent-binary.js';
|
|
15
16
|
/** Same 2KB the SDK keeps: enough for the CLI's last words, not a log sink. */
|
|
@@ -255,18 +256,6 @@ const EFFORT_LABELS = {
|
|
|
255
256
|
const EFFORT_DESCRIPTIONS = {
|
|
256
257
|
[ULTRACODE]: 'Extra high, plus the agent orchestrates sub-agent workflows on its own',
|
|
257
258
|
};
|
|
258
|
-
/** Longest a task title may be before the tray truncates it. */
|
|
259
|
-
const TASK_TITLE_LIMIT = 120;
|
|
260
|
-
/** How many live tasks travel in one `agent_tasks` event. */
|
|
261
|
-
const TASK_LIST_CAP = 20;
|
|
262
|
-
/**
|
|
263
|
-
* Floor between two `agent_tasks` publications.
|
|
264
|
-
*
|
|
265
|
-
* A twenty-agent fan-out changes membership dozens of times a minute and every
|
|
266
|
-
* running subagent adds a `task_progress` every ~30s. Each event is a row in
|
|
267
|
-
* `DevSessionEvent`, so the tray coalesces rather than narrating.
|
|
268
|
-
*/
|
|
269
|
-
const TASK_PUBLISH_INTERVAL_MS = 1_500;
|
|
270
259
|
/**
|
|
271
260
|
* What the agent calls a finished task, mapped onto the tray's three states.
|
|
272
261
|
*
|
|
@@ -433,29 +422,15 @@ class ClaudeSession {
|
|
|
433
422
|
*/
|
|
434
423
|
liveWireModel;
|
|
435
424
|
/**
|
|
436
|
-
* Everything running beside the conversation
|
|
437
|
-
*
|
|
438
|
-
*
|
|
425
|
+
* Everything running beside the conversation (ticket #113). Membership is
|
|
426
|
+
* owned by `background_tasks_changed`; the details come from the `task_*`
|
|
427
|
+
* bookends. How the set is published – the floor, the de-duplication, the
|
|
428
|
+
* per-turn counters – is the tray's, and shared with the Codex adapter (#382).
|
|
439
429
|
*/
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
* Which turn is being counted (#147).
|
|
445
|
-
*
|
|
446
|
-
* There are no `started` / `done` accumulators any more, and that is the
|
|
447
|
-
* whole fix. Two counters with different lifetimes could disagree, and did:
|
|
448
|
-
* `endTaskTurn` zeroed both while deliberately keeping the ROWS of tasks
|
|
449
|
-
* still running, so a background shell that outlived its turn was counted as
|
|
450
|
-
* finished in the next one without ever having been counted as started —
|
|
451
|
-
* «22 of 21 done». Both numbers are now derived from this map in one pass, so
|
|
452
|
-
* `done <= total` holds by cardinality rather than by clamping.
|
|
453
|
-
*/
|
|
454
|
-
turnEpoch = 0;
|
|
455
|
-
taskPublishTimer = null;
|
|
456
|
-
taskPublishedAt = 0;
|
|
457
|
-
/** Last published snapshot, minus the ages — see `flushTasks` (QA-111 M4). */
|
|
458
|
-
lastTaskFingerprint = '';
|
|
430
|
+
tray = new AgentTaskTray({
|
|
431
|
+
emit: (event) => this.emit(event),
|
|
432
|
+
isStopped: () => this.stopped,
|
|
433
|
+
});
|
|
459
434
|
/**
|
|
460
435
|
* Ambient tasks the SDK marked `skip_transcript`. Remembered by id because
|
|
461
436
|
* the flag arrives on `task_started`, which is usually NOT the first message
|
|
@@ -642,13 +617,6 @@ class ClaudeSession {
|
|
|
642
617
|
};
|
|
643
618
|
this.q = queryFn({ prompt: this.input, options });
|
|
644
619
|
void this.consume();
|
|
645
|
-
// Ticket #113. The task level is per-PROCESS: the SDK emits nothing at
|
|
646
|
-
// startup and tells consumers to reset to the empty set whenever the
|
|
647
|
-
// session's CLI process (re)starts. The dashboard reads the newest
|
|
648
|
-
// `agent_tasks` in the stored feed, so without this a session killed
|
|
649
|
-
// mid-turn — the runner restarted, the machine rebooted — would come back
|
|
650
|
-
// and show the subagents of its previous life as though they were still
|
|
651
|
-
// running, right up until the next membership change.
|
|
652
620
|
// Ticket #119 / QA-114 MAJOR-3: if the protected file could not be written,
|
|
653
621
|
// say so where a person will see it, not only in journald.
|
|
654
622
|
if (this.mcpFallbackNotice) {
|
|
@@ -658,8 +626,14 @@ class ClaudeSession {
|
|
|
658
626
|
this.emit({ type: 'notice', level: 'warn', text: MODE_REFUSED_TEXT });
|
|
659
627
|
this.emit({ type: 'settings', mode: this.mode });
|
|
660
628
|
}
|
|
661
|
-
|
|
662
|
-
|
|
629
|
+
// Ticket #113. The task level is per-PROCESS: the SDK emits nothing at
|
|
630
|
+
// startup and tells consumers to reset to the empty set whenever the
|
|
631
|
+
// session's CLI process (re)starts. The dashboard reads the newest
|
|
632
|
+
// `agent_tasks` in the stored feed, so without this a session killed
|
|
633
|
+
// mid-turn — the runner restarted, the machine rebooted — would come back
|
|
634
|
+
// and show the subagents of its previous life as though they were still
|
|
635
|
+
// running, right up until the next membership change.
|
|
636
|
+
this.tray.announceEmpty();
|
|
663
637
|
// Report what the agent can do right away. `system:init` only arrives with
|
|
664
638
|
// the first turn (verified live), so a free session waiting for its first
|
|
665
639
|
// message would otherwise show no model list at all — while the control
|
|
@@ -1540,7 +1514,7 @@ class ClaudeSession {
|
|
|
1540
1514
|
if (!announced.has(id))
|
|
1541
1515
|
this.skipTaskIds.delete(id);
|
|
1542
1516
|
}
|
|
1543
|
-
|
|
1517
|
+
const liveIds = [];
|
|
1544
1518
|
for (const row of rows) {
|
|
1545
1519
|
const id = typeof row['task_id'] === 'string' ? row['task_id'] : null;
|
|
1546
1520
|
if (!id)
|
|
@@ -1551,16 +1525,17 @@ class ClaudeSession {
|
|
|
1551
1525
|
// and it never left again (QA-111 m4).
|
|
1552
1526
|
if (this.skipTaskIds.has(id))
|
|
1553
1527
|
continue;
|
|
1554
|
-
|
|
1528
|
+
liveIds.push(id);
|
|
1555
1529
|
// A task can reach the live set before its own `task_started` — the
|
|
1556
1530
|
// SDK says the level usually precedes the bookends. Seed it here so
|
|
1557
1531
|
// the tray never has a live id it cannot name.
|
|
1558
|
-
this.
|
|
1532
|
+
this.tray.touch(id, {
|
|
1559
1533
|
kind: typeof row['task_type'] === 'string' ? row['task_type'] : 'task',
|
|
1560
1534
|
title: typeof row['description'] === 'string' ? row['description'] : 'Working…',
|
|
1561
1535
|
});
|
|
1562
1536
|
}
|
|
1563
|
-
this.
|
|
1537
|
+
this.tray.setLive(liveIds);
|
|
1538
|
+
this.tray.publish();
|
|
1564
1539
|
break;
|
|
1565
1540
|
}
|
|
1566
1541
|
case 'task_started': {
|
|
@@ -1572,14 +1547,11 @@ class ClaudeSession {
|
|
|
1572
1547
|
// may already have taken it in before this message arrived.
|
|
1573
1548
|
if (msg['skip_transcript'] === true) {
|
|
1574
1549
|
this.skipTaskIds.add(id);
|
|
1575
|
-
this.
|
|
1576
|
-
|
|
1577
|
-
this.liveTaskIds = this.liveTaskIds.filter((live) => live !== id);
|
|
1578
|
-
if (this.liveTaskIds.length !== before)
|
|
1579
|
-
this.publishTasks();
|
|
1550
|
+
if (this.tray.forget(id))
|
|
1551
|
+
this.tray.publish();
|
|
1580
1552
|
break;
|
|
1581
1553
|
}
|
|
1582
|
-
this.
|
|
1554
|
+
this.tray.touch(id, {
|
|
1583
1555
|
kind: typeof msg['task_type'] === 'string' ? msg['task_type'] : 'task',
|
|
1584
1556
|
...(typeof msg['description'] === 'string' ? { title: msg['description'] } : {}),
|
|
1585
1557
|
...(typeof msg['subagent_type'] === 'string'
|
|
@@ -1589,7 +1561,7 @@ class ClaudeSession {
|
|
|
1589
1561
|
? { workflowName: msg['workflow_name'] }
|
|
1590
1562
|
: {}),
|
|
1591
1563
|
});
|
|
1592
|
-
this.
|
|
1564
|
+
this.tray.publish();
|
|
1593
1565
|
break;
|
|
1594
1566
|
}
|
|
1595
1567
|
case 'task_progress': {
|
|
@@ -1600,7 +1572,7 @@ class ClaudeSession {
|
|
|
1600
1572
|
// denominator (#147).
|
|
1601
1573
|
if (!id || this.skipTaskIds.has(id))
|
|
1602
1574
|
break;
|
|
1603
|
-
this.
|
|
1575
|
+
this.tray.touch(id, {
|
|
1604
1576
|
...(typeof msg['description'] === 'string' ? { title: msg['description'] } : {}),
|
|
1605
1577
|
...(typeof msg['subagent_type'] === 'string'
|
|
1606
1578
|
? { subagentType: msg['subagent_type'] }
|
|
@@ -1608,20 +1580,20 @@ class ClaudeSession {
|
|
|
1608
1580
|
...(typeof msg['summary'] === 'string' ? { summary: msg['summary'] } : {}),
|
|
1609
1581
|
...usagePatch(msg['usage']),
|
|
1610
1582
|
});
|
|
1611
|
-
this.
|
|
1583
|
+
this.tray.publish();
|
|
1612
1584
|
break;
|
|
1613
1585
|
}
|
|
1614
1586
|
case 'task_updated': {
|
|
1615
1587
|
const id = typeof msg['task_id'] === 'string' ? msg['task_id'] : null;
|
|
1616
1588
|
const patch = (msg['patch'] ?? {});
|
|
1617
|
-
if (!id || !this.
|
|
1589
|
+
if (!id || !this.tray.has(id))
|
|
1618
1590
|
break;
|
|
1619
1591
|
const status = typeof patch['status'] === 'string' ? patch['status'] : undefined;
|
|
1620
|
-
this.
|
|
1592
|
+
this.tray.touch(id, {
|
|
1621
1593
|
...(typeof patch['description'] === 'string' ? { title: patch['description'] } : {}),
|
|
1622
1594
|
...(status ? { status: taskStatus(status) } : {}),
|
|
1623
1595
|
});
|
|
1624
|
-
this.
|
|
1596
|
+
this.tray.publish();
|
|
1625
1597
|
break;
|
|
1626
1598
|
}
|
|
1627
1599
|
case 'task_notification': {
|
|
@@ -1638,12 +1610,12 @@ class ClaudeSession {
|
|
|
1638
1610
|
}
|
|
1639
1611
|
break;
|
|
1640
1612
|
}
|
|
1641
|
-
this.
|
|
1613
|
+
this.tray.touch(id, {
|
|
1642
1614
|
status: taskStatus(typeof msg['status'] === 'string' ? msg['status'] : 'completed'),
|
|
1643
1615
|
...(typeof msg['summary'] === 'string' ? { summary: msg['summary'] } : {}),
|
|
1644
1616
|
...usagePatch(msg['usage']),
|
|
1645
1617
|
});
|
|
1646
|
-
this.
|
|
1618
|
+
this.tray.publish();
|
|
1647
1619
|
break;
|
|
1648
1620
|
}
|
|
1649
1621
|
default:
|
|
@@ -1651,181 +1623,21 @@ class ClaudeSession {
|
|
|
1651
1623
|
}
|
|
1652
1624
|
}
|
|
1653
1625
|
/**
|
|
1654
|
-
*
|
|
1655
|
-
*
|
|
1656
|
-
* The counters live here rather than in the individual message handlers
|
|
1657
|
-
* because «started» and «finished» are properties of the transition, not of
|
|
1658
|
-
* whichever message happened to announce it (QA-111 M1). They were counted
|
|
1659
|
-
* per-message, and both orderings the SDK actually produces missed:
|
|
1660
|
-
*
|
|
1661
|
-
* `background_tasks_changed` → `task_started` → `total` stayed 0
|
|
1662
|
-
* `task_updated{completed}` → `task_notification` → `done` stayed 0
|
|
1663
|
-
*
|
|
1664
|
-
* The first of those is the COMMON ordering — the SDK documents the level as
|
|
1665
|
-
* usually preceding the bookends — so «N of M done», the one number the
|
|
1666
|
-
* ticket asked for by name, was never printed at all.
|
|
1667
|
-
*
|
|
1668
|
-
* `startedAt` is stamped once and never moves.
|
|
1669
|
-
*/
|
|
1670
|
-
touchTask(id, patch) {
|
|
1671
|
-
const existing = this.tasks.get(id);
|
|
1672
|
-
if (existing) {
|
|
1673
|
-
const settledAs = existing.status === 'running' ? null : existing.status;
|
|
1674
|
-
Object.assign(existing, patch);
|
|
1675
|
-
if (patch.title)
|
|
1676
|
-
existing.title = truncate(patch.title, TASK_TITLE_LIMIT);
|
|
1677
|
-
if (patch.summary)
|
|
1678
|
-
existing.summary = truncate(patch.summary, TASK_TITLE_LIMIT);
|
|
1679
|
-
// Settling is one-way. A task that has stopped cannot start again under
|
|
1680
|
-
// the same id: the only messages that could say so are a late
|
|
1681
|
-
// `task_updated` carrying a status this runner does not recognise, or a
|
|
1682
|
-
// frame redelivered after a reconnect — neither of which is news that the
|
|
1683
|
-
// work resumed. Letting either through would make the derived `done`
|
|
1684
|
-
// count downwards on screen.
|
|
1685
|
-
if (settledAs && existing.status === 'running')
|
|
1686
|
-
existing.status = settledAs;
|
|
1687
|
-
return;
|
|
1688
|
-
}
|
|
1689
|
-
this.tasks.set(id, {
|
|
1690
|
-
...patch,
|
|
1691
|
-
id,
|
|
1692
|
-
kind: patch.kind ?? 'task',
|
|
1693
|
-
title: truncate(patch.title ?? 'Working…', TASK_TITLE_LIMIT),
|
|
1694
|
-
status: patch.status ?? 'running',
|
|
1695
|
-
turnEpoch: this.turnEpoch,
|
|
1696
|
-
startedAt: Date.now(),
|
|
1697
|
-
// The only free-text field an agent writes with no length of its own:
|
|
1698
|
-
// a subagent's closing `summary` runs to kilobytes, and twenty of them
|
|
1699
|
-
// push the event past the payload cap, which replaces the WHOLE payload
|
|
1700
|
-
// with `{truncated:true}` and blinks the tray out (QA-111 m1).
|
|
1701
|
-
...(patch.summary ? { summary: truncate(patch.summary, TASK_TITLE_LIMIT) } : {}),
|
|
1702
|
-
});
|
|
1703
|
-
}
|
|
1704
|
-
/**
|
|
1705
|
-
* Publish the tray, at most once every `TASK_PUBLISH_INTERVAL_MS`.
|
|
1706
|
-
*
|
|
1707
|
-
* Every publication is a stored row in the session feed, and a wide fan-out
|
|
1708
|
-
* changes membership dozens of times a minute. The trailing timer matters as
|
|
1709
|
-
* much as the floor: the LAST change in a burst is the one that says the work
|
|
1710
|
-
* is over, and dropping it would leave the tray running forever.
|
|
1711
|
-
*/
|
|
1712
|
-
publishTasks() {
|
|
1713
|
-
if (this.stopped)
|
|
1714
|
-
return;
|
|
1715
|
-
const wait = TASK_PUBLISH_INTERVAL_MS - (Date.now() - this.taskPublishedAt);
|
|
1716
|
-
if (wait > 0) {
|
|
1717
|
-
if (!this.taskPublishTimer) {
|
|
1718
|
-
this.taskPublishTimer = setTimeout(() => {
|
|
1719
|
-
this.taskPublishTimer = null;
|
|
1720
|
-
this.flushTasks();
|
|
1721
|
-
}, wait);
|
|
1722
|
-
this.taskPublishTimer.unref();
|
|
1723
|
-
}
|
|
1724
|
-
return;
|
|
1725
|
-
}
|
|
1726
|
-
this.flushTasks();
|
|
1727
|
-
}
|
|
1728
|
-
flushTasks() {
|
|
1729
|
-
if (this.stopped)
|
|
1730
|
-
return;
|
|
1731
|
-
this.taskPublishedAt = Date.now();
|
|
1732
|
-
const now = Date.now();
|
|
1733
|
-
// Only what the agent still calls live. A task that finished keeps its row
|
|
1734
|
-
// in `this.tasks` for the counters, but the tray is about NOW.
|
|
1735
|
-
const tasks = this.liveTaskIds
|
|
1736
|
-
.map((id) => this.tasks.get(id))
|
|
1737
|
-
.filter((t) => Boolean(t))
|
|
1738
|
-
.slice(0, TASK_LIST_CAP)
|
|
1739
|
-
// How long it has been running, measured HERE (QA-111 m2). `startedAt` is
|
|
1740
|
-
// this machine's clock and the browser's is a different one — subtracting
|
|
1741
|
-
// across them put the dev server's clock skew straight into the number,
|
|
1742
|
-
// so a host ten minutes behind showed «10m 03s» on a task one second old.
|
|
1743
|
-
.map(({ turnEpoch: _turnEpoch, ...task }) => ({
|
|
1744
|
-
...task,
|
|
1745
|
-
ageMs: Math.max(0, now - task.startedAt),
|
|
1746
|
-
}));
|
|
1747
|
-
// Both numbers, one pass, one map (#147). `done` counts a SUBSET of what
|
|
1748
|
-
// `total` counts, so `done <= total` is a property of set cardinality and
|
|
1749
|
-
// cannot be broken by a message ordering, a redelivery, a re-title, an
|
|
1750
|
-
// ambient row being deleted or a turn boundary. There is no pair of
|
|
1751
|
-
// counters to keep in agreement, because there is no pair.
|
|
1752
|
-
//
|
|
1753
|
-
// Tasks that outlived an earlier turn keep that turn's epoch: they still
|
|
1754
|
-
// render as live rows — background work outliving its turn is the whole
|
|
1755
|
-
// point of the tray — but they belong to neither number here, exactly as
|
|
1756
|
-
// this file's own doctrine says («done/total are per-turn by definition.
|
|
1757
|
-
// The live set does NOT»).
|
|
1758
|
-
let total = 0;
|
|
1759
|
-
let done = 0;
|
|
1760
|
-
for (const task of this.tasks.values()) {
|
|
1761
|
-
if (task.turnEpoch !== this.turnEpoch)
|
|
1762
|
-
continue;
|
|
1763
|
-
total += 1;
|
|
1764
|
-
if (task.status !== 'running')
|
|
1765
|
-
done += 1;
|
|
1766
|
-
}
|
|
1767
|
-
const payload = { type: 'agent_tasks', tasks, done, total };
|
|
1768
|
-
// A frame that says exactly what the last one said is not worth a row in
|
|
1769
|
-
// the session feed (QA-111 M4). `task_progress` fires every ~30s per
|
|
1770
|
-
// running subagent and usually carries nothing new, and with twenty of
|
|
1771
|
-
// them that alone is the throttle's whole budget. `ageMs` is excluded from
|
|
1772
|
-
// the comparison on purpose — it changes every time by definition, and
|
|
1773
|
-
// including it would make every frame unique and the check pointless.
|
|
1774
|
-
const fingerprint = JSON.stringify({
|
|
1775
|
-
done: payload.done,
|
|
1776
|
-
total: payload.total,
|
|
1777
|
-
tasks: tasks.map(({ ageMs: _ageMs, ...rest }) => rest),
|
|
1778
|
-
});
|
|
1779
|
-
if (fingerprint === this.lastTaskFingerprint)
|
|
1780
|
-
return;
|
|
1781
|
-
this.lastTaskFingerprint = fingerprint;
|
|
1782
|
-
this.emit(payload);
|
|
1783
|
-
}
|
|
1784
|
-
/**
|
|
1785
|
-
* The turn ended. Reset what belongs to the TURN — and only that.
|
|
1786
|
-
*
|
|
1787
|
-
* `done`/`total` are per-turn by definition and go back to zero. The live set
|
|
1788
|
-
* does NOT: background work is precisely the work that outlives the turn that
|
|
1789
|
-
* started it, which is the whole reason the ticket asks for it. Caught on
|
|
1790
|
-
* production during this session's own verification — the agent ended its
|
|
1791
|
-
* turn with `sleep 40` still running in the background and the tray, which
|
|
1792
|
-
* cleared everything here, showed nothing at exactly the moment somebody was
|
|
1793
|
-
* reading the answer and wondering whether the deploy had finished.
|
|
1626
|
+
* The turn ended: the tray resets the turn's counters and says what is still
|
|
1627
|
+
* running (see `AgentTaskTray.endTurn`).
|
|
1794
1628
|
*
|
|
1795
1629
|
* Membership stays owned by `background_tasks_changed`, which reports the set
|
|
1796
|
-
* emptying when it actually empties.
|
|
1797
|
-
*
|
|
1798
|
-
*
|
|
1630
|
+
* emptying when it actually empties.
|
|
1631
|
+
*
|
|
1632
|
+
* `skipTaskIds` is NOT cleared here (#147). An ambient task the SDK asked
|
|
1633
|
+
* consumers to hide can span a turn boundary, and clearing the set let its
|
|
1634
|
+
* id back into the live list on the next `background_tasks_changed` – where
|
|
1635
|
+
* it was drawn as a tray row and counted as work. The id is forgotten when
|
|
1636
|
+
* that task settles instead, which is the moment it stops being able to come
|
|
1637
|
+
* back.
|
|
1799
1638
|
*/
|
|
1800
1639
|
endTaskTurn() {
|
|
1801
|
-
|
|
1802
|
-
clearTimeout(this.taskPublishTimer);
|
|
1803
|
-
this.taskPublishTimer = null;
|
|
1804
|
-
}
|
|
1805
|
-
const live = new Set(this.liveTaskIds);
|
|
1806
|
-
for (const id of [...this.tasks.keys()]) {
|
|
1807
|
-
if (!live.has(id))
|
|
1808
|
-
this.tasks.delete(id);
|
|
1809
|
-
}
|
|
1810
|
-
// `skipTaskIds` is NOT cleared here any more (#147). An ambient task the
|
|
1811
|
-
// SDK asked consumers to hide can span a turn boundary, and clearing the
|
|
1812
|
-
// set let its id back into the live list on the next
|
|
1813
|
-
// `background_tasks_changed` — where it was drawn as a tray row and counted
|
|
1814
|
-
// as work. The id is forgotten when that task settles instead, which is the
|
|
1815
|
-
// moment it stops being able to come back.
|
|
1816
|
-
this.turnEpoch += 1;
|
|
1817
|
-
// Straight through `flushTasks` rather than an empty frame of its own, and
|
|
1818
|
-
// with the de-duplication disarmed for this one frame (plan
|
|
1819
|
-
// `workflow-mode-fixes` S1 p.10). The turn's counters usually changed, but
|
|
1820
|
-
// not always: a task started in an earlier turn and still running, and a
|
|
1821
|
-
// turn that started nothing new, produce exactly the frame the last turn
|
|
1822
|
-
// ended on — and the end of a turn is the moment the API and the tray most
|
|
1823
|
-
// need to hear what is still running, whether or not it is news. The
|
|
1824
|
-
// supervisor de-duplicates the COUNT on its own side; this frame is the
|
|
1825
|
-
// tray's freshness, not the database's.
|
|
1826
|
-
this.taskPublishedAt = 0;
|
|
1827
|
-
this.lastTaskFingerprint = '';
|
|
1828
|
-
this.flushTasks();
|
|
1640
|
+
this.tray.endTurn();
|
|
1829
1641
|
}
|
|
1830
1642
|
/**
|
|
1831
1643
|
* Forget what the LAST turn did, now that a new one is starting (#252).
|
|
@@ -2266,10 +2078,7 @@ class ClaudeSession {
|
|
|
2266
2078
|
return;
|
|
2267
2079
|
this.stopped = true;
|
|
2268
2080
|
// A pending tray publication would fire into a closed output queue.
|
|
2269
|
-
|
|
2270
|
-
clearTimeout(this.taskPublishTimer);
|
|
2271
|
-
this.taskPublishTimer = null;
|
|
2272
|
-
}
|
|
2081
|
+
this.tray.close();
|
|
2273
2082
|
// Before the process goes: withdraw everything a human was still being
|
|
2274
2083
|
// asked, WITH a cause. A pending permission used to gutter out as a plain
|
|
2275
2084
|
// "denied", which put a decision in the audit trail that nobody made.
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import type { AgentTaskTray } from './agent-tasks.js';
|
|
2
|
+
/**
|
|
3
|
+
* The tray's word for a helper. The dashboard draws the robot for any kind with
|
|
4
|
+
* «agent» in it, and this is the word the Claude SDK already uses for the same
|
|
5
|
+
* thing – so the row looks the same whichever CLI started it.
|
|
6
|
+
*/
|
|
7
|
+
export declare const SUBAGENT_TASK_KIND = "local_agent";
|
|
8
|
+
/** How often the set is re-checked against Codex while a helper is running. */
|
|
9
|
+
export declare const SUBAGENT_RECONCILE_MS = 60000;
|
|
10
|
+
/**
|
|
11
|
+
* How long after a helper became `running` the snapshot may believe an `idle`.
|
|
12
|
+
*
|
|
13
|
+
* A thread handed work reports `idle` first and `active` a little later
|
|
14
|
+
* (measured: 400 ms and 1.3 s after a spawn), so a snapshot landing in that gap
|
|
15
|
+
* – a parent that spawns and ends its turn at once – would otherwise settle a
|
|
16
|
+
* helper that has not started yet. A real ending is still caught: by its own
|
|
17
|
+
* events at once, or by the next tick once this has passed.
|
|
18
|
+
*/
|
|
19
|
+
export declare const SUBAGENT_SPAWN_GRACE_MS = 30000;
|
|
20
|
+
type HelperState = 'running' | 'done' | 'failed';
|
|
21
|
+
/** Codex's `CollabAgentStatus` → the tray. Unknown is «no news», never a guess. */
|
|
22
|
+
export declare function collabAgentState(status: unknown): HelperState | null;
|
|
23
|
+
/**
|
|
24
|
+
* Codex's `SubAgentActivityKind` → the tray. Unknown is «no news».
|
|
25
|
+
*
|
|
26
|
+
* `interacted` is deliberately NOT «running»: it is one agent putting words in
|
|
27
|
+
* another's mailbox, and the CLI's own tool description draws the line —
|
|
28
|
+
* `followup_task` «gives an existing agent a new task and triggers a turn»,
|
|
29
|
+
* `send_message` «passes a message to a running agent without triggering a
|
|
30
|
+
* turn». Both produce this one kind. Measured: a helper reported to the session
|
|
31
|
+
* with `interacted` and the session's thread started no turn for the next 120
|
|
32
|
+
* seconds. Read as «working», a message to an agent that has already finished
|
|
33
|
+
* would put a row back in the tray that nothing takes out again until the
|
|
34
|
+
* snapshot's grace is over. A re-tasked agent announces itself the honest way,
|
|
35
|
+
* with its own `turn/started` on its own thread.
|
|
36
|
+
*/
|
|
37
|
+
export declare function activityState(kind: unknown): HelperState | null;
|
|
38
|
+
/** What one `thread/read` said, reduced to what the set needs. */
|
|
39
|
+
export type ThreadReading = {
|
|
40
|
+
kind: 'thread';
|
|
41
|
+
/** `active` · `idle` · `notLoaded` · `systemError`, or null when absent. */
|
|
42
|
+
status: string | null;
|
|
43
|
+
parentThreadId: string | null;
|
|
44
|
+
/** Spawned by an agent's collaboration tools – the only helpers that count. */
|
|
45
|
+
spawned: boolean;
|
|
46
|
+
nickname: string | null;
|
|
47
|
+
role: string | null;
|
|
48
|
+
path: string | null;
|
|
49
|
+
}
|
|
50
|
+
/** The thread does not exist in this app-server any more. */
|
|
51
|
+
| {
|
|
52
|
+
kind: 'gone';
|
|
53
|
+
}
|
|
54
|
+
/** This Codex has no such request – stop asking. */
|
|
55
|
+
| {
|
|
56
|
+
kind: 'unsupported';
|
|
57
|
+
}
|
|
58
|
+
/** No answer worth acting on (a timeout, a transport hiccup). */
|
|
59
|
+
| {
|
|
60
|
+
kind: 'unknown';
|
|
61
|
+
};
|
|
62
|
+
export declare function threadReading(raw: unknown): ThreadReading;
|
|
63
|
+
/**
|
|
64
|
+
* A refused `thread/read` or `thread/loaded/list`, classified by what Codex
|
|
65
|
+
* said. Measured on 0.154.0: an unknown thread is `-32600 thread not loaded:
|
|
66
|
+
* <id>`, an unknown request is `-32600 Invalid request: unknown variant …` –
|
|
67
|
+
* the same code, so the sentence decides.
|
|
68
|
+
*/
|
|
69
|
+
export declare function threadReadingOfError(error: unknown): ThreadReading;
|
|
70
|
+
type Request = (method: string, params: Record<string, unknown>, timeoutMs: number) => Promise<unknown>;
|
|
71
|
+
export interface CodexThreadProbe {
|
|
72
|
+
read(threadId: string): Promise<ThreadReading>;
|
|
73
|
+
/** Loaded thread ids, `unsupported`, or null when there was no answer. */
|
|
74
|
+
loaded(): Promise<string[] | 'unsupported' | null>;
|
|
75
|
+
}
|
|
76
|
+
export declare function threadProbeOver(request: Request): CodexThreadProbe;
|
|
77
|
+
export interface CodexSubagentsOptions {
|
|
78
|
+
tray: AgentTaskTray;
|
|
79
|
+
/** The session's own thread – never counted as its own helper. */
|
|
80
|
+
ownThreadId: () => string | null;
|
|
81
|
+
probe: CodexThreadProbe;
|
|
82
|
+
isStopped: () => boolean;
|
|
83
|
+
reconcileMs?: number;
|
|
84
|
+
spawnGraceMs?: number;
|
|
85
|
+
}
|
|
86
|
+
export declare class CodexSubagents {
|
|
87
|
+
private readonly opts;
|
|
88
|
+
private readonly helpers;
|
|
89
|
+
/** Loaded threads the snapshot already found are not ours to count. */
|
|
90
|
+
private readonly notOurs;
|
|
91
|
+
private timer;
|
|
92
|
+
private reconciling;
|
|
93
|
+
/** A full reconcile asked for while one was running – run once more after it. */
|
|
94
|
+
private reconcileAgain;
|
|
95
|
+
private snapshotSupported;
|
|
96
|
+
private closed;
|
|
97
|
+
private readonly reconcileMs;
|
|
98
|
+
private readonly spawnGraceMs;
|
|
99
|
+
constructor(opts: CodexSubagentsOptions);
|
|
100
|
+
/** One of this session's helpers, at any depth. */
|
|
101
|
+
isHelper(threadId: string): boolean;
|
|
102
|
+
/**
|
|
103
|
+
* A collaboration item, on the session's own thread or on a helper's.
|
|
104
|
+
*
|
|
105
|
+
* A helper's helpers are this session's too: a spawned agent can spawn, and a
|
|
106
|
+
* grandchild at work keeps the session just as busy. An item from any other
|
|
107
|
+
* thread speaks for somebody else and is ignored.
|
|
108
|
+
*/
|
|
109
|
+
onItem(senderThreadId: string, item: Record<string, unknown>): void;
|
|
110
|
+
/**
|
|
111
|
+
* A helper's own turn started or ended.
|
|
112
|
+
*
|
|
113
|
+
* Only for a helper already known: a thread's turn says it is working, not
|
|
114
|
+
* whose it is, and Codex runs threads of its own (review, compaction) that are
|
|
115
|
+
* nobody's helpers.
|
|
116
|
+
*/
|
|
117
|
+
onHelperTurn(threadId: string, phase: 'started' | 'completed', turnStatus?: string): void;
|
|
118
|
+
/** A helper's thread was closed. */
|
|
119
|
+
onThreadClosed(threadId: string): void;
|
|
120
|
+
/**
|
|
121
|
+
* The session's turn ended: reset the turn's counters, say what is still
|
|
122
|
+
* running, then check the set against Codex.
|
|
123
|
+
*
|
|
124
|
+
* The frame goes out synchronously, BEFORE the caller emits `turn_end` – the
|
|
125
|
+
* supervisor decides «is this the person's turn now» on the count it holds at
|
|
126
|
+
* that moment. The snapshot follows in the background; if it changes the
|
|
127
|
+
* set, the supervisor re-reports the resting status with the new count.
|
|
128
|
+
*/
|
|
129
|
+
endTurn(): void;
|
|
130
|
+
close(): void;
|
|
131
|
+
/**
|
|
132
|
+
* Check the set against Codex (R16).
|
|
133
|
+
*
|
|
134
|
+
* `everything` – at the end of a turn: every helper not known to be gone, plus
|
|
135
|
+
* every loaded thread not classified yet. Otherwise (the minute tick) only
|
|
136
|
+
* the ones believed running, which is what can go stale.
|
|
137
|
+
*/
|
|
138
|
+
reconcile(options: {
|
|
139
|
+
everything: boolean;
|
|
140
|
+
}): Promise<void>;
|
|
141
|
+
private gone;
|
|
142
|
+
private reconcileOnce;
|
|
143
|
+
/** What one reading means for a helper already held. */
|
|
144
|
+
private absorb;
|
|
145
|
+
/** The helper's record, created (as finished) if it is new. */
|
|
146
|
+
private remember;
|
|
147
|
+
/**
|
|
148
|
+
* Move one helper to a state, and publish.
|
|
149
|
+
*
|
|
150
|
+
* The session itself is never its own helper: a helper reporting back to its
|
|
151
|
+
* parent names the PARENT as the agent it «interacted» with
|
|
152
|
+
* (`agentThreadId` = the session's thread, `agentPath: "/root"`) – measured
|
|
153
|
+
* on 0.154.0, and counted it would keep the session «busy» for ever.
|
|
154
|
+
*/
|
|
155
|
+
private apply;
|
|
156
|
+
/** New facts about a helper, onto its row if it has one. */
|
|
157
|
+
private learn;
|
|
158
|
+
/**
|
|
159
|
+
* Ask Codex for the helper's name once, right after it appears. The activity
|
|
160
|
+
* item carries only its path (`/root/a`); the name Codex gives it
|
|
161
|
+
* («Heisenberg») is what Codex's own interface calls it.
|
|
162
|
+
*/
|
|
163
|
+
private askName;
|
|
164
|
+
private syncLive;
|
|
165
|
+
private startTimer;
|
|
166
|
+
private stopTimer;
|
|
167
|
+
}
|
|
168
|
+
export {};
|
|
169
|
+
//# sourceMappingURL=codex-subagents.d.ts.map
|