pi-jar 0.3.1 → 0.3.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/package.json +1 -1
- package/src/delegate.ts +59 -21
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-jar",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "A warm, role-aware Pi terminal workspace with switchable profiles, animated UI, plan and goal workflows, tasks, change review, model roles, background shells and subagents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/delegate.ts
CHANGED
|
@@ -1361,7 +1361,24 @@ export function registerDelegate(pi: ExtensionAPI, roles: ModelRoleManager, regi
|
|
|
1361
1361
|
const value = options.getMaxSubagents?.() ?? MAX_DELEGATES;
|
|
1362
1362
|
return SUBAGENT_LIMIT_CHOICES.includes(value as never) ? value : MAX_DELEGATES;
|
|
1363
1363
|
};
|
|
1364
|
-
|
|
1364
|
+
let sessionGeneration = 0;
|
|
1365
|
+
let sessionActive = true;
|
|
1366
|
+
const cancelUpdates = new Set<() => void>();
|
|
1367
|
+
const resetSession = () => {
|
|
1368
|
+
sessionGeneration++;
|
|
1369
|
+
controllerContext = undefined;
|
|
1370
|
+
clearTimeout(wakeTimer);
|
|
1371
|
+
wakeTimer = undefined;
|
|
1372
|
+
watched.clear();
|
|
1373
|
+
pendingEvents.clear();
|
|
1374
|
+
pendingControls.clear();
|
|
1375
|
+
moderatorActive = false;
|
|
1376
|
+
moderatorInterrupted = false;
|
|
1377
|
+
for (const cancel of cancelUpdates) cancel();
|
|
1378
|
+
cancelUpdates.clear();
|
|
1379
|
+
};
|
|
1380
|
+
pi.on?.("session_shutdown", () => { sessionActive = false; resetSession(); });
|
|
1381
|
+
pi.on?.("session_start", (_event, ctx) => { resetSession(); sessionActive = true; controllerContext = ctx; });
|
|
1365
1382
|
const roleCandidates = (role: string) => {
|
|
1366
1383
|
const manager = roles as ModelRoleManager & { resolveCandidates?: (name: string) => ReturnType<ModelRoleManager["resolveCandidates"]> };
|
|
1367
1384
|
if (typeof manager.resolveCandidates === "function") return manager.resolveCandidates(role);
|
|
@@ -1451,10 +1468,10 @@ export function registerDelegate(pi: ExtensionAPI, roles: ModelRoleManager, regi
|
|
|
1451
1468
|
};
|
|
1452
1469
|
};
|
|
1453
1470
|
const scheduleWake = () => {
|
|
1454
|
-
if (moderatorActive || moderatorInterrupted || wakeTimer || !pendingEvents.size) return;
|
|
1471
|
+
if (!sessionActive || moderatorActive || moderatorInterrupted || wakeTimer || !pendingEvents.size) return;
|
|
1455
1472
|
wakeTimer = setTimeout(() => {
|
|
1456
1473
|
wakeTimer = undefined;
|
|
1457
|
-
if (moderatorActive || moderatorInterrupted) return;
|
|
1474
|
+
if (!sessionActive || moderatorActive || moderatorInterrupted) return;
|
|
1458
1475
|
const message = takeEvents();
|
|
1459
1476
|
if (!message) return;
|
|
1460
1477
|
try { pi.sendMessage?.(message, { triggerTurn: true }); }
|
|
@@ -1462,7 +1479,11 @@ export function registerDelegate(pi: ExtensionAPI, roles: ModelRoleManager, regi
|
|
|
1462
1479
|
}, NOTIFY_COALESCE_MS);
|
|
1463
1480
|
wakeTimer.unref?.();
|
|
1464
1481
|
};
|
|
1465
|
-
const queueEvent = (id: string, event: SubagentEvent) => {
|
|
1482
|
+
const queueEvent = (id: string, event: SubagentEvent) => {
|
|
1483
|
+
if (!sessionActive || registry.get(event.record.key) !== event.record) return;
|
|
1484
|
+
pendingEvents.set(id, event);
|
|
1485
|
+
scheduleWake();
|
|
1486
|
+
};
|
|
1466
1487
|
const boundary = (event: { outcome?: string; context?: { canContinue?: boolean } }) => {
|
|
1467
1488
|
if (event.outcome !== "completed" || event.context?.canContinue === false) return;
|
|
1468
1489
|
const message = takeEvents();
|
|
@@ -1506,6 +1527,9 @@ export function registerDelegate(pi: ExtensionAPI, roles: ModelRoleManager, regi
|
|
|
1506
1527
|
// Internal council launches use session context, not tool-only nested execution APIs.
|
|
1507
1528
|
async execute(_id, params, signal, onUpdate, ctx: ExtensionContext) {
|
|
1508
1529
|
controllerContext = ctx;
|
|
1530
|
+
const generation = sessionGeneration;
|
|
1531
|
+
const currentSession = () => sessionActive && generation === sessionGeneration;
|
|
1532
|
+
const parentCwd = ctx.cwd;
|
|
1509
1533
|
const requested = params.tasks;
|
|
1510
1534
|
const parentToolNames = (): string[] | undefined => {
|
|
1511
1535
|
try {
|
|
@@ -1595,6 +1619,7 @@ export function registerDelegate(pi: ExtensionAPI, roles: ModelRoleManager, regi
|
|
|
1595
1619
|
}
|
|
1596
1620
|
}
|
|
1597
1621
|
}
|
|
1622
|
+
if (!currentSession()) return { content: [{ type: "text", text: "Subagent launch cancelled: session changed." }], details: { runs: [], write: false }, isError: true };
|
|
1598
1623
|
const worktreeTasks = requested.filter((item) => (item.mode ?? params.mode ?? (params.write === true ? "direct" : "scout")) === "worktree").length;
|
|
1599
1624
|
const recoveryBlock = worktreeTasks ? worktreeAdmission(registry, worktreeTasks) : undefined;
|
|
1600
1625
|
if (recoveryBlock) return { content: [{ type: "text", text: recoveryBlock }], details: { runs: [], write: false }, isError: true };
|
|
@@ -1641,9 +1666,10 @@ export function registerDelegate(pi: ExtensionAPI, roles: ModelRoleManager, regi
|
|
|
1641
1666
|
catch (error) { console.error("pi-jar: direct subagent edits will not join /diff", error); }
|
|
1642
1667
|
}
|
|
1643
1668
|
const edited = (path: string) => {
|
|
1669
|
+
if (!currentSession()) return;
|
|
1644
1670
|
const tracker = options.changes?.();
|
|
1645
1671
|
if (!baselines || !tracker) return;
|
|
1646
|
-
const absolute = isAbsolute(path) ? resolve(path) : resolve(
|
|
1672
|
+
const absolute = isAbsolute(path) ? resolve(path) : resolve(parentCwd, path);
|
|
1647
1673
|
const baseline = readChildBaseline(baselines, absolute);
|
|
1648
1674
|
if (baseline !== undefined && tracker.adopt(absolute, baseline)) options.changed?.();
|
|
1649
1675
|
};
|
|
@@ -1651,19 +1677,20 @@ export function registerDelegate(pi: ExtensionAPI, roles: ModelRoleManager, regi
|
|
|
1651
1677
|
/** Last footer state per record: child streams fire many updates, the status changes rarely. */
|
|
1652
1678
|
const published = new Map<string, string>();
|
|
1653
1679
|
const publish = () => {
|
|
1654
|
-
if (!
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1680
|
+
if (!currentSession()) return;
|
|
1681
|
+
try {
|
|
1682
|
+
if (!ctx.hasUI) return;
|
|
1683
|
+
for (const { key, run } of records) {
|
|
1684
|
+
const state = run.state === "done" || run.state === "failed" || run.state === "stopped" ? ""
|
|
1685
|
+
: run.state === "queued" || run.state === "paused" ? "waiting" : run.state === "idle" || run.state === "hibernated" ? "idle" : "working";
|
|
1686
|
+
if (published.get(key) === state) continue;
|
|
1687
|
+
published.set(key, state);
|
|
1661
1688
|
ctx.ui.setStatus(`${ROLE_PREFIX}${key}`, !state ? undefined : JSON.stringify({
|
|
1662
1689
|
name: run.name, label: run.name.slice(0, 12), state,
|
|
1663
1690
|
task: cleanText(run.task, 60), expiresAt: Date.now() + 24 * 60 * 60_000
|
|
1664
1691
|
}));
|
|
1665
|
-
}
|
|
1666
|
-
}
|
|
1692
|
+
}
|
|
1693
|
+
} catch { /* status is decoration; even hasUI can throw on an expired context */ }
|
|
1667
1694
|
};
|
|
1668
1695
|
const details = (includeOutput = true) => ({
|
|
1669
1696
|
batch: id,
|
|
@@ -1673,18 +1700,29 @@ export function registerDelegate(pi: ExtensionAPI, roles: ModelRoleManager, regi
|
|
|
1673
1700
|
let updateTimer: ReturnType<typeof setTimeout> | undefined;
|
|
1674
1701
|
let lastUpdateAt = 0;
|
|
1675
1702
|
let toolReturned = false;
|
|
1676
|
-
const
|
|
1703
|
+
const cancelUpdate = () => {
|
|
1704
|
+
if (updateTimer) clearTimeout(updateTimer);
|
|
1677
1705
|
updateTimer = undefined;
|
|
1706
|
+
cancelUpdates.delete(cancelUpdate);
|
|
1707
|
+
};
|
|
1708
|
+
const emitUpdate = () => {
|
|
1709
|
+
cancelUpdate();
|
|
1710
|
+
if (!currentSession()) return;
|
|
1678
1711
|
lastUpdateAt = Date.now();
|
|
1679
1712
|
publish();
|
|
1680
1713
|
onUpdate?.({ content: [{ type: "text", text: runs.map((run) => `${run.name}: ${run.state}${run.activity ? " · " + run.activity : ""}`).join("\n") }], details: details(false) });
|
|
1681
1714
|
};
|
|
1682
1715
|
const update = () => {
|
|
1716
|
+
if (!currentSession()) return;
|
|
1683
1717
|
registry.notify();
|
|
1684
1718
|
if (toolReturned || !onUpdate) { publish(); return; }
|
|
1685
1719
|
const wait = Math.max(0, LIVE_UPDATE_MS - (Date.now() - lastUpdateAt));
|
|
1686
1720
|
if (wait === 0) { if (updateTimer) clearTimeout(updateTimer); emitUpdate(); return; }
|
|
1687
|
-
if (!updateTimer) {
|
|
1721
|
+
if (!updateTimer) {
|
|
1722
|
+
cancelUpdates.add(cancelUpdate);
|
|
1723
|
+
updateTimer = setTimeout(emitUpdate, wait);
|
|
1724
|
+
updateTimer.unref?.();
|
|
1725
|
+
}
|
|
1688
1726
|
};
|
|
1689
1727
|
const flushUpdate = () => {
|
|
1690
1728
|
if (updateTimer) { clearTimeout(updateTimer); updateTimer = undefined; }
|
|
@@ -1736,7 +1774,7 @@ export function registerDelegate(pi: ExtensionAPI, roles: ModelRoleManager, regi
|
|
|
1736
1774
|
failBeforeStart(run, "fork/worktree mode requires a persisted parent Pi session");
|
|
1737
1775
|
return failedHandle();
|
|
1738
1776
|
}
|
|
1739
|
-
let cwd =
|
|
1777
|
+
let cwd = parentCwd;
|
|
1740
1778
|
let session: DelegateSessionOptions | undefined;
|
|
1741
1779
|
const env: Record<string, string> = { [SUBAGENT_KEY_ENV]: key, [SUBAGENT_NAME_ENV]: run.name };
|
|
1742
1780
|
const effectiveTools = toolsByRun.get(run.index) ?? (run.mode === "worktree" ? WORKTREE_TOOLS : run.mode === "direct" ? undefined : READ_ONLY_TOOLS);
|
|
@@ -1759,7 +1797,7 @@ export function registerDelegate(pi: ExtensionAPI, roles: ModelRoleManager, regi
|
|
|
1759
1797
|
}
|
|
1760
1798
|
if (run.mode === "worktree") {
|
|
1761
1799
|
try {
|
|
1762
|
-
const worktree = await createDelegateWorktree(
|
|
1800
|
+
const worktree = await createDelegateWorktree(parentCwd, controllers[index]!.signal);
|
|
1763
1801
|
worktrees.set(run.index, worktree);
|
|
1764
1802
|
run.workspace = worktree.root;
|
|
1765
1803
|
cwd = worktree.cwd;
|
|
@@ -1802,7 +1840,7 @@ export function registerDelegate(pi: ExtensionAPI, roles: ModelRoleManager, regi
|
|
|
1802
1840
|
void handle.closed.then(async () => {
|
|
1803
1841
|
// The agent ended for good (hibernation never resolves closed): drop its private session and
|
|
1804
1842
|
// discussion identity. An unreconciled worktree outlives it as lightweight recovery state.
|
|
1805
|
-
retireDiscussion(key);
|
|
1843
|
+
if (currentSession()) retireDiscussion(key);
|
|
1806
1844
|
await cleanupPrivate(run.index, dropsWorktree(index) || run.mode !== "worktree");
|
|
1807
1845
|
if (worktrees.has(run.index) && !stopRequested.has(index)) releaseForRecovery(run);
|
|
1808
1846
|
update();
|
|
@@ -1852,8 +1890,8 @@ export function registerDelegate(pi: ExtensionAPI, roles: ModelRoleManager, regi
|
|
|
1852
1890
|
catch (error) { run.error = "could not inspect isolated changes: " + cleanText(String(error), 240); }
|
|
1853
1891
|
if (changed.length && !run.error) {
|
|
1854
1892
|
try {
|
|
1855
|
-
run.appliedFiles = await applyDelegateWorktree(worktree,
|
|
1856
|
-
if (run.appliedFiles.length) {
|
|
1893
|
+
run.appliedFiles = await applyDelegateWorktree(worktree, parentCwd, changed, options.changes?.(), controllers[index]!.signal);
|
|
1894
|
+
if (run.appliedFiles.length && currentSession()) {
|
|
1857
1895
|
try { options.changed?.(); } catch (error) { console.error("pi-jar: could not refresh parent change UI", error); }
|
|
1858
1896
|
}
|
|
1859
1897
|
await disposeDelegateWorktree(worktree);
|