@esso0428/pi-subagents 0.15.1 → 0.15.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 +5 -0
- package/dist/agent-manager.d.ts +8 -0
- package/dist/agent-manager.js +64 -0
- package/dist/index.js +45 -7
- package/package.json +1 -1
- package/src/agent-manager.ts +72 -0
- package/src/index.ts +5 -0
- package/test/agent-manager-history.test.ts +84 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.15.2] - 2026-09-10
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Completed subagent records survive session reloads.** On `session_start`, the extension now reconstructs validated terminal records persisted as `subagents:record` entries. Restored records retain their result and status for `get_subagent_result`, but never pretend to have a live child session; running and queued records are ignored, and duplicate IDs use the newest branch entry.
|
|
14
|
+
|
|
10
15
|
## [0.15.1] - 2026-09-10
|
|
11
16
|
|
|
12
17
|
### Added
|
package/dist/agent-manager.d.ts
CHANGED
|
@@ -121,6 +121,14 @@ export declare class AgentManager {
|
|
|
121
121
|
*/
|
|
122
122
|
steer(id: string, message: string): boolean;
|
|
123
123
|
getRecord(id: string): AgentRecord | undefined;
|
|
124
|
+
/**
|
|
125
|
+
* Restore terminal records persisted in a parent session.
|
|
126
|
+
*
|
|
127
|
+
* Restored records deliberately have no live session, promise, or abort
|
|
128
|
+
* controller. Invalid data is ignored because session entries are persisted
|
|
129
|
+
* extension data and may have been written by an older version.
|
|
130
|
+
*/
|
|
131
|
+
restoreCompleted(records: readonly unknown[]): void;
|
|
124
132
|
listAgents(): AgentRecord[];
|
|
125
133
|
abort(id: string): boolean;
|
|
126
134
|
/** Dispose a record's session and remove it from the map. */
|
package/dist/agent-manager.js
CHANGED
|
@@ -16,6 +16,13 @@ const usage_js_1 = require("./usage.js");
|
|
|
16
16
|
const worktree_js_1 = require("./worktree.js");
|
|
17
17
|
/** Default max concurrent background agents. */
|
|
18
18
|
const DEFAULT_MAX_CONCURRENT = 4;
|
|
19
|
+
const TERMINAL_STATUSES = new Set([
|
|
20
|
+
"completed",
|
|
21
|
+
"steered",
|
|
22
|
+
"aborted",
|
|
23
|
+
"stopped",
|
|
24
|
+
"error",
|
|
25
|
+
]);
|
|
19
26
|
/**
|
|
20
27
|
* Validate a caller-supplied SpawnOptions.cwd. `undefined`/`null` mean "unset"
|
|
21
28
|
* (parent cwd). Anything else must be an absolute path to an existing
|
|
@@ -39,6 +46,29 @@ function assertValidSpawnCwd(cwd) {
|
|
|
39
46
|
throw new Error(`SpawnOptions.cwd is not a directory: "${cwd}"`);
|
|
40
47
|
}
|
|
41
48
|
}
|
|
49
|
+
const RESTORABLE_STATUSES = new Set([
|
|
50
|
+
"completed",
|
|
51
|
+
"steered",
|
|
52
|
+
"aborted",
|
|
53
|
+
"stopped",
|
|
54
|
+
"error",
|
|
55
|
+
]);
|
|
56
|
+
function isRestorableRecord(value) {
|
|
57
|
+
if (!value || typeof value !== "object")
|
|
58
|
+
return false;
|
|
59
|
+
const record = value;
|
|
60
|
+
return (typeof record.id === "string" &&
|
|
61
|
+
record.id.length > 0 &&
|
|
62
|
+
typeof record.type === "string" &&
|
|
63
|
+
typeof record.description === "string" &&
|
|
64
|
+
RESTORABLE_STATUSES.has(record.status) &&
|
|
65
|
+
typeof record.startedAt === "number" &&
|
|
66
|
+
Number.isFinite(record.startedAt) &&
|
|
67
|
+
typeof record.completedAt === "number" &&
|
|
68
|
+
Number.isFinite(record.completedAt) &&
|
|
69
|
+
(record.result === undefined || typeof record.result === "string") &&
|
|
70
|
+
(record.error === undefined || typeof record.error === "string"));
|
|
71
|
+
}
|
|
42
72
|
class AgentManager {
|
|
43
73
|
agents = new Map();
|
|
44
74
|
cleanupInterval;
|
|
@@ -425,6 +455,40 @@ class AgentManager {
|
|
|
425
455
|
getRecord(id) {
|
|
426
456
|
return this.agents.get(id);
|
|
427
457
|
}
|
|
458
|
+
/**
|
|
459
|
+
* Restore terminal records persisted in a parent session.
|
|
460
|
+
*
|
|
461
|
+
* Restored records deliberately have no live session, promise, or abort
|
|
462
|
+
* controller. Invalid data is ignored because session entries are persisted
|
|
463
|
+
* extension data and may have been written by an older version.
|
|
464
|
+
*/
|
|
465
|
+
restoreCompleted(records) {
|
|
466
|
+
const restoredIds = new Set();
|
|
467
|
+
// getBranch() is chronological; newest persisted state wins on duplicate IDs.
|
|
468
|
+
for (const value of [...records].reverse()) {
|
|
469
|
+
if (!isRestorableRecord(value))
|
|
470
|
+
continue;
|
|
471
|
+
if (restoredIds.has(value.id) || this.agents.has(value.id))
|
|
472
|
+
continue;
|
|
473
|
+
restoredIds.add(value.id);
|
|
474
|
+
this.agents.set(value.id, {
|
|
475
|
+
id: value.id,
|
|
476
|
+
type: value.type,
|
|
477
|
+
description: value.description,
|
|
478
|
+
status: value.status,
|
|
479
|
+
result: value.result,
|
|
480
|
+
error: value.error,
|
|
481
|
+
toolUses: 0,
|
|
482
|
+
startedAt: value.startedAt,
|
|
483
|
+
completedAt: value.completedAt,
|
|
484
|
+
lifetimeUsage: { input: 0, output: 0, cacheWrite: 0 },
|
|
485
|
+
compactionCount: 0,
|
|
486
|
+
// Historical records have no inline tool surface and should remain
|
|
487
|
+
// visible in the background widget.
|
|
488
|
+
isBackground: true,
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
}
|
|
428
492
|
listAgents() {
|
|
429
493
|
return [...this.agents.values()].sort((a, b) => b.startedAt - a.startedAt);
|
|
430
494
|
}
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,39 @@
|
|
|
10
10
|
* Commands:
|
|
11
11
|
* /agents — Interactive agent management menu
|
|
12
12
|
*/
|
|
13
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
16
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
17
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
18
|
+
}
|
|
19
|
+
Object.defineProperty(o, k2, desc);
|
|
20
|
+
}) : (function(o, m, k, k2) {
|
|
21
|
+
if (k2 === undefined) k2 = k;
|
|
22
|
+
o[k2] = m[k];
|
|
23
|
+
}));
|
|
24
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
25
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
26
|
+
}) : function(o, v) {
|
|
27
|
+
o["default"] = v;
|
|
28
|
+
});
|
|
29
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
30
|
+
var ownKeys = function(o) {
|
|
31
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
32
|
+
var ar = [];
|
|
33
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
34
|
+
return ar;
|
|
35
|
+
};
|
|
36
|
+
return ownKeys(o);
|
|
37
|
+
};
|
|
38
|
+
return function (mod) {
|
|
39
|
+
if (mod && mod.__esModule) return mod;
|
|
40
|
+
var result = {};
|
|
41
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
42
|
+
__setModuleDefault(result, mod);
|
|
43
|
+
return result;
|
|
44
|
+
};
|
|
45
|
+
})();
|
|
13
46
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
47
|
exports.renderRunningAgentStatus = renderRunningAgentStatus;
|
|
15
48
|
exports.default = default_1;
|
|
@@ -512,6 +545,11 @@ function default_1(pi) {
|
|
|
512
545
|
pi.on("session_start", async (_event, ctx) => {
|
|
513
546
|
currentCtx = ctx;
|
|
514
547
|
manager.clearCompleted(true);
|
|
548
|
+
const historicalRecords = ctx.sessionManager
|
|
549
|
+
.getBranch()
|
|
550
|
+
.filter((entry) => entry?.type === "custom" && entry.customType === "subagents:record")
|
|
551
|
+
.map((entry) => entry.data);
|
|
552
|
+
manager.restoreCompleted(historicalRecords);
|
|
515
553
|
// Guard mirrors the `!scheduler.isActive()` pattern below: session_start
|
|
516
554
|
// fires once per activation, but a double-bind must not leak listeners.
|
|
517
555
|
if (!rpcHandle) {
|
|
@@ -1611,7 +1649,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1611
1649
|
ctx.ui.notify(`Agent is ${record.status === "queued" ? "queued" : "expired"} — no session available.`, "info");
|
|
1612
1650
|
return;
|
|
1613
1651
|
}
|
|
1614
|
-
const { ConversationViewer, VIEWPORT_HEIGHT_PCT } = await
|
|
1652
|
+
const { ConversationViewer, VIEWPORT_HEIGHT_PCT } = await Promise.resolve().then(() => __importStar(require("./ui/conversation-viewer.js")));
|
|
1615
1653
|
const session = record.session;
|
|
1616
1654
|
const activity = agentActivity.get(record.id);
|
|
1617
1655
|
await ctx.ui.custom((tui, theme, keybindings, done) => {
|
|
@@ -1660,7 +1698,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1660
1698
|
const content = (0, node_fs_1.readFileSync)(file.path, "utf-8");
|
|
1661
1699
|
const edited = await ctx.ui.editor(`Edit ${name}`, content);
|
|
1662
1700
|
if (edited !== undefined && edited !== content) {
|
|
1663
|
-
const { writeFileSync } = await
|
|
1701
|
+
const { writeFileSync } = await Promise.resolve().then(() => __importStar(require("node:fs")));
|
|
1664
1702
|
writeFileSync(file.path, edited, "utf-8");
|
|
1665
1703
|
reloadCustomAgents();
|
|
1666
1704
|
ctx.ui.notify(`Updated ${file.path}`, "info");
|
|
@@ -1748,7 +1786,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1748
1786
|
if (cfg.isolation)
|
|
1749
1787
|
fmFields.push(`isolation: ${cfg.isolation}`);
|
|
1750
1788
|
const content = `---\n${fmFields.join("\n")}\n---\n\n${cfg.systemPrompt}\n`;
|
|
1751
|
-
const { writeFileSync } = await
|
|
1789
|
+
const { writeFileSync } = await Promise.resolve().then(() => __importStar(require("node:fs")));
|
|
1752
1790
|
writeFileSync(targetPath, content, "utf-8");
|
|
1753
1791
|
reloadCustomAgents();
|
|
1754
1792
|
ctx.ui.notify(`Ejected ${name} to ${targetPath}`, "info");
|
|
@@ -1764,7 +1802,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1764
1802
|
return;
|
|
1765
1803
|
}
|
|
1766
1804
|
const updated = content.replace(/^---\n/, "---\nenabled: false\n");
|
|
1767
|
-
const { writeFileSync } = await
|
|
1805
|
+
const { writeFileSync } = await Promise.resolve().then(() => __importStar(require("node:fs")));
|
|
1768
1806
|
writeFileSync(file.path, updated, "utf-8");
|
|
1769
1807
|
reloadCustomAgents();
|
|
1770
1808
|
ctx.ui.notify(`Disabled ${name} (${file.path})`, "info");
|
|
@@ -1780,7 +1818,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1780
1818
|
const targetDir = location.startsWith("Project") ? projectAgentsDir() : personalAgentsDir();
|
|
1781
1819
|
(0, node_fs_1.mkdirSync)(targetDir, { recursive: true });
|
|
1782
1820
|
const targetPath = (0, node_path_1.join)(targetDir, `${name}.md`);
|
|
1783
|
-
const { writeFileSync } = await
|
|
1821
|
+
const { writeFileSync } = await Promise.resolve().then(() => __importStar(require("node:fs")));
|
|
1784
1822
|
writeFileSync(targetPath, "---\nenabled: false\n---\n", "utf-8");
|
|
1785
1823
|
reloadCustomAgents();
|
|
1786
1824
|
ctx.ui.notify(`Disabled ${name} (${targetPath})`, "info");
|
|
@@ -1792,7 +1830,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1792
1830
|
return;
|
|
1793
1831
|
const content = (0, node_fs_1.readFileSync)(file.path, "utf-8");
|
|
1794
1832
|
const updated = content.replace(/^(---\n)enabled: false\n/, "$1");
|
|
1795
|
-
const { writeFileSync } = await
|
|
1833
|
+
const { writeFileSync } = await Promise.resolve().then(() => __importStar(require("node:fs")));
|
|
1796
1834
|
// If the file was just a stub ("---\n---\n"), delete it to restore the built-in default
|
|
1797
1835
|
if (updated.trim() === "---\n---" || updated.trim() === "---\n---\n") {
|
|
1798
1836
|
(0, node_fs_1.unlinkSync)(file.path);
|
|
@@ -1975,7 +2013,7 @@ ${systemPrompt}
|
|
|
1975
2013
|
if (!overwrite)
|
|
1976
2014
|
return;
|
|
1977
2015
|
}
|
|
1978
|
-
const { writeFileSync } = await
|
|
2016
|
+
const { writeFileSync } = await Promise.resolve().then(() => __importStar(require("node:fs")));
|
|
1979
2017
|
writeFileSync(targetPath, content, "utf-8");
|
|
1980
2018
|
reloadCustomAgents();
|
|
1981
2019
|
ctx.ui.notify(`Created ${targetPath}`, "info");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@esso0428/pi-subagents",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.2",
|
|
4
4
|
"description": "A pi extension that brings smart Claude Code-style autonomous sub-agents to pi, with npm:pi-subagents-style JSON agent overrides.",
|
|
5
5
|
"author": "ESSO0428",
|
|
6
6
|
"repository": {
|
package/src/agent-manager.ts
CHANGED
|
@@ -23,6 +23,13 @@ export type CompactionInfo = { reason: "manual" | "threshold" | "overflow"; toke
|
|
|
23
23
|
|
|
24
24
|
/** Default max concurrent background agents. */
|
|
25
25
|
const DEFAULT_MAX_CONCURRENT = 4;
|
|
26
|
+
const TERMINAL_STATUSES = new Set<AgentRecord["status"]>([
|
|
27
|
+
"completed",
|
|
28
|
+
"steered",
|
|
29
|
+
"aborted",
|
|
30
|
+
"stopped",
|
|
31
|
+
"error",
|
|
32
|
+
]);
|
|
26
33
|
|
|
27
34
|
/**
|
|
28
35
|
* Validate a caller-supplied SpawnOptions.cwd. `undefined`/`null` mean "unset"
|
|
@@ -97,6 +104,37 @@ interface SpawnOptions {
|
|
|
97
104
|
onCompaction?: (info: CompactionInfo) => void;
|
|
98
105
|
}
|
|
99
106
|
|
|
107
|
+
const RESTORABLE_STATUSES = new Set<AgentRecord["status"]>([
|
|
108
|
+
"completed",
|
|
109
|
+
"steered",
|
|
110
|
+
"aborted",
|
|
111
|
+
"stopped",
|
|
112
|
+
"error",
|
|
113
|
+
]);
|
|
114
|
+
|
|
115
|
+
type PersistedAgentRecord = Pick<
|
|
116
|
+
AgentRecord,
|
|
117
|
+
"id" | "type" | "description" | "status" | "result" | "error" | "startedAt" | "completedAt"
|
|
118
|
+
>;
|
|
119
|
+
|
|
120
|
+
function isRestorableRecord(value: unknown): value is PersistedAgentRecord {
|
|
121
|
+
if (!value || typeof value !== "object") return false;
|
|
122
|
+
const record = value as Record<string, unknown>;
|
|
123
|
+
return (
|
|
124
|
+
typeof record.id === "string" &&
|
|
125
|
+
record.id.length > 0 &&
|
|
126
|
+
typeof record.type === "string" &&
|
|
127
|
+
typeof record.description === "string" &&
|
|
128
|
+
RESTORABLE_STATUSES.has(record.status as AgentRecord["status"]) &&
|
|
129
|
+
typeof record.startedAt === "number" &&
|
|
130
|
+
Number.isFinite(record.startedAt) &&
|
|
131
|
+
typeof record.completedAt === "number" &&
|
|
132
|
+
Number.isFinite(record.completedAt) &&
|
|
133
|
+
(record.result === undefined || typeof record.result === "string") &&
|
|
134
|
+
(record.error === undefined || typeof record.error === "string")
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
100
138
|
export class AgentManager {
|
|
101
139
|
private agents = new Map<string, AgentRecord>();
|
|
102
140
|
private cleanupInterval: ReturnType<typeof setInterval>;
|
|
@@ -510,6 +548,40 @@ export class AgentManager {
|
|
|
510
548
|
return this.agents.get(id);
|
|
511
549
|
}
|
|
512
550
|
|
|
551
|
+
/**
|
|
552
|
+
* Restore terminal records persisted in a parent session.
|
|
553
|
+
*
|
|
554
|
+
* Restored records deliberately have no live session, promise, or abort
|
|
555
|
+
* controller. Invalid data is ignored because session entries are persisted
|
|
556
|
+
* extension data and may have been written by an older version.
|
|
557
|
+
*/
|
|
558
|
+
restoreCompleted(records: readonly unknown[]): void {
|
|
559
|
+
const restoredIds = new Set<string>();
|
|
560
|
+
// getBranch() is chronological; newest persisted state wins on duplicate IDs.
|
|
561
|
+
for (const value of [...records].reverse()) {
|
|
562
|
+
if (!isRestorableRecord(value)) continue;
|
|
563
|
+
if (restoredIds.has(value.id) || this.agents.has(value.id)) continue;
|
|
564
|
+
restoredIds.add(value.id);
|
|
565
|
+
|
|
566
|
+
this.agents.set(value.id, {
|
|
567
|
+
id: value.id,
|
|
568
|
+
type: value.type,
|
|
569
|
+
description: value.description,
|
|
570
|
+
status: value.status,
|
|
571
|
+
result: value.result,
|
|
572
|
+
error: value.error,
|
|
573
|
+
toolUses: 0,
|
|
574
|
+
startedAt: value.startedAt,
|
|
575
|
+
completedAt: value.completedAt,
|
|
576
|
+
lifetimeUsage: { input: 0, output: 0, cacheWrite: 0 },
|
|
577
|
+
compactionCount: 0,
|
|
578
|
+
// Historical records have no inline tool surface and should remain
|
|
579
|
+
// visible in the background widget.
|
|
580
|
+
isBackground: true,
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
513
585
|
listAgents(): AgentRecord[] {
|
|
514
586
|
return [...this.agents.values()].sort(
|
|
515
587
|
(a, b) => b.startedAt - a.startedAt,
|
package/src/index.ts
CHANGED
|
@@ -571,6 +571,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
571
571
|
pi.on("session_start", async (_event, ctx) => {
|
|
572
572
|
currentCtx = ctx;
|
|
573
573
|
manager.clearCompleted(true);
|
|
574
|
+
const historicalRecords = ctx.sessionManager
|
|
575
|
+
.getBranch()
|
|
576
|
+
.filter((entry: any) => entry?.type === "custom" && entry.customType === "subagents:record")
|
|
577
|
+
.map((entry: any) => entry.data);
|
|
578
|
+
manager.restoreCompleted(historicalRecords);
|
|
574
579
|
// Guard mirrors the `!scheduler.isActive()` pattern below: session_start
|
|
575
580
|
// fires once per activation, but a double-bind must not leak listeners.
|
|
576
581
|
if (!rpcHandle) {
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { AgentManager } from "../src/agent-manager.js";
|
|
3
|
+
|
|
4
|
+
type PersistedRecord = {
|
|
5
|
+
id: string;
|
|
6
|
+
type: string;
|
|
7
|
+
description: string;
|
|
8
|
+
status: string;
|
|
9
|
+
result?: unknown;
|
|
10
|
+
error?: unknown;
|
|
11
|
+
startedAt: number;
|
|
12
|
+
completedAt: number;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function persisted(overrides: Partial<PersistedRecord> = {}): PersistedRecord {
|
|
16
|
+
return {
|
|
17
|
+
id: "agent-1",
|
|
18
|
+
type: "general-purpose",
|
|
19
|
+
description: "history test",
|
|
20
|
+
status: "completed",
|
|
21
|
+
result: "restored result",
|
|
22
|
+
startedAt: 100,
|
|
23
|
+
completedAt: 200,
|
|
24
|
+
...overrides,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe("AgentManager.restoreCompleted", () => {
|
|
29
|
+
let manager: AgentManager | undefined;
|
|
30
|
+
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
manager?.dispose();
|
|
33
|
+
manager = undefined;
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("restores terminal records without creating live runtime state", () => {
|
|
37
|
+
manager = new AgentManager();
|
|
38
|
+
|
|
39
|
+
manager.restoreCompleted([
|
|
40
|
+
persisted(),
|
|
41
|
+
persisted({ id: "agent-error", status: "error", error: "failed" }),
|
|
42
|
+
persisted({ id: "agent-running", status: "running" }),
|
|
43
|
+
persisted({ id: "agent-queued", status: "queued" }),
|
|
44
|
+
persisted({ id: "agent-invalid", completedAt: Number.NaN }),
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
expect(manager.listAgents().map((record) => record.id).sort()).toEqual([
|
|
48
|
+
"agent-1",
|
|
49
|
+
"agent-error",
|
|
50
|
+
]);
|
|
51
|
+
const restored = manager.getRecord("agent-1");
|
|
52
|
+
expect(restored).toMatchObject({
|
|
53
|
+
status: "completed",
|
|
54
|
+
result: "restored result",
|
|
55
|
+
toolUses: 0,
|
|
56
|
+
lifetimeUsage: { input: 0, output: 0, cacheWrite: 0 },
|
|
57
|
+
compactionCount: 0,
|
|
58
|
+
isBackground: true,
|
|
59
|
+
});
|
|
60
|
+
expect(restored?.session).toBeUndefined();
|
|
61
|
+
expect(restored?.promise).toBeUndefined();
|
|
62
|
+
expect(restored?.abortController).toBeUndefined();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("uses the newest branch entry when an id appears more than once", () => {
|
|
66
|
+
manager = new AgentManager();
|
|
67
|
+
|
|
68
|
+
manager.restoreCompleted([
|
|
69
|
+
persisted({ result: "old", completedAt: 200 }),
|
|
70
|
+
persisted({ result: "new", completedAt: 300 }),
|
|
71
|
+
]);
|
|
72
|
+
|
|
73
|
+
expect(manager.getRecord("agent-1")?.result).toBe("new");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("does not overwrite a record already held by the manager", () => {
|
|
77
|
+
manager = new AgentManager();
|
|
78
|
+
|
|
79
|
+
manager.restoreCompleted([persisted({ result: "first" })]);
|
|
80
|
+
manager.restoreCompleted([persisted({ result: "second" })]);
|
|
81
|
+
|
|
82
|
+
expect(manager.getRecord("agent-1")?.result).toBe("first");
|
|
83
|
+
});
|
|
84
|
+
});
|