@oai404iao/pi-subagent 0.2.0 → 0.4.0-alpha.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/README.md +266 -75
- package/agents/worker.md +1 -1
- package/config.example.json +4 -1
- package/config.schema.json +29 -4
- package/package.json +9 -6
- package/src/agent-state.ts +125 -0
- package/src/agent-sync.ts +64 -53
- package/src/agents.ts +3 -22
- package/src/catalog.ts +59 -7
- package/src/completion-mailbox.ts +656 -0
- package/src/config.ts +57 -6
- package/src/coordinator.ts +2476 -205
- package/src/descriptor.ts +118 -12
- package/src/index.ts +177 -27
- package/src/mailbox.ts +451 -0
- package/src/providers.ts +223 -28
- package/src/render.ts +25 -8
- package/src/scheduler.ts +173 -0
- package/src/schemas.ts +114 -11
- package/src/task-path.ts +188 -0
- package/src/types.ts +74 -15
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DelegationDetails,
|
|
3
|
+
SubagentStopReason,
|
|
4
|
+
} from "./types.ts";
|
|
5
|
+
|
|
6
|
+
export type AgentLifecycleState = "open" | "closed";
|
|
7
|
+
export type AgentResidencyState = "resident" | "unloaded";
|
|
8
|
+
|
|
9
|
+
export type AgentTurnState =
|
|
10
|
+
| { state: "none" }
|
|
11
|
+
| { state: "queued"; turnId: string }
|
|
12
|
+
| { state: "running"; turnId: string }
|
|
13
|
+
| { state: "completed"; turnId: string }
|
|
14
|
+
| { state: "errored"; turnId: string }
|
|
15
|
+
| { state: "interrupted"; turnId: string };
|
|
16
|
+
|
|
17
|
+
export interface AgentControlState {
|
|
18
|
+
lifecycle: AgentLifecycleState;
|
|
19
|
+
residency: AgentResidencyState;
|
|
20
|
+
turn: AgentTurnState;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function createAgentControlState(): AgentControlState {
|
|
24
|
+
return {
|
|
25
|
+
lifecycle: "open",
|
|
26
|
+
residency: "resident",
|
|
27
|
+
turn: { state: "none" },
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function queueAgentTurn(state: AgentControlState, turnId: string): void {
|
|
32
|
+
if (state.turn.state === "queued" || state.turn.state === "running") {
|
|
33
|
+
throw new Error(`agent turn ${state.turn.turnId} is already active`);
|
|
34
|
+
}
|
|
35
|
+
state.turn = { state: "queued", turnId };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function startAgentTurn(state: AgentControlState, turnId: string): void {
|
|
39
|
+
if (state.turn.state !== "queued" || state.turn.turnId !== turnId) {
|
|
40
|
+
throw new Error(`agent turn ${turnId} was not queued`);
|
|
41
|
+
}
|
|
42
|
+
state.turn = { state: "running", turnId };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function finishAgentTurn(
|
|
46
|
+
state: AgentControlState,
|
|
47
|
+
turnId: string,
|
|
48
|
+
stopReason: SubagentStopReason,
|
|
49
|
+
): void {
|
|
50
|
+
if (
|
|
51
|
+
(state.turn.state !== "queued" &&
|
|
52
|
+
state.turn.state !== "running" &&
|
|
53
|
+
state.turn.state !== "interrupted") ||
|
|
54
|
+
state.turn.turnId !== turnId
|
|
55
|
+
) {
|
|
56
|
+
throw new Error(`agent turn ${turnId} is not active`);
|
|
57
|
+
}
|
|
58
|
+
switch (stopReason) {
|
|
59
|
+
case "completed":
|
|
60
|
+
state.turn = { state: "completed", turnId };
|
|
61
|
+
return;
|
|
62
|
+
case "aborted":
|
|
63
|
+
state.turn = { state: "interrupted", turnId };
|
|
64
|
+
return;
|
|
65
|
+
case "error":
|
|
66
|
+
case "max-tokens":
|
|
67
|
+
state.turn = { state: "errored", turnId };
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function interruptAgentTurn(state: AgentControlState): void {
|
|
73
|
+
if (state.turn.state === "queued" || state.turn.state === "running") {
|
|
74
|
+
state.turn = { state: "interrupted", turnId: state.turn.turnId };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function setAgentResidency(
|
|
79
|
+
state: AgentControlState,
|
|
80
|
+
residency: AgentResidencyState,
|
|
81
|
+
): void {
|
|
82
|
+
state.residency = residency;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function closeAgent(state: AgentControlState): void {
|
|
86
|
+
state.lifecycle = "closed";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function currentAgentTurnId(state: AgentControlState): string | undefined {
|
|
90
|
+
return state.turn.state === "none" ? undefined : state.turn.turnId;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function delegationStatus(
|
|
94
|
+
state: AgentControlState,
|
|
95
|
+
hasOwnedChildren: boolean,
|
|
96
|
+
): DelegationDetails["status"] {
|
|
97
|
+
switch (state.turn.state) {
|
|
98
|
+
case "none":
|
|
99
|
+
case "queued":
|
|
100
|
+
return "starting";
|
|
101
|
+
case "running":
|
|
102
|
+
return "running";
|
|
103
|
+
case "completed":
|
|
104
|
+
return hasOwnedChildren ? "waiting" : "completed";
|
|
105
|
+
case "errored":
|
|
106
|
+
case "interrupted":
|
|
107
|
+
return hasOwnedChildren ? "waiting" : "failed";
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function catalogStatus(
|
|
112
|
+
state: AgentControlState,
|
|
113
|
+
): "running" | "idle" | "ready" {
|
|
114
|
+
if (state.residency === "unloaded") return "ready";
|
|
115
|
+
switch (state.turn.state) {
|
|
116
|
+
case "queued":
|
|
117
|
+
case "running":
|
|
118
|
+
return "running";
|
|
119
|
+
case "none":
|
|
120
|
+
case "completed":
|
|
121
|
+
case "errored":
|
|
122
|
+
case "interrupted":
|
|
123
|
+
return "idle";
|
|
124
|
+
}
|
|
125
|
+
}
|
package/src/agent-sync.ts
CHANGED
|
@@ -32,6 +32,11 @@ interface AgentManifest {
|
|
|
32
32
|
files: Record<string, string>;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
type ManifestReadResult =
|
|
36
|
+
| { kind: "missing" }
|
|
37
|
+
| { kind: "valid"; manifest: AgentManifest }
|
|
38
|
+
| { kind: "corrupt"; diagnostic: string };
|
|
39
|
+
|
|
35
40
|
interface BundledAgentFile {
|
|
36
41
|
name: string;
|
|
37
42
|
content: Buffer;
|
|
@@ -182,53 +187,35 @@ function bundledAgentFiles(bundledDir: string): BundledAgentFile[] {
|
|
|
182
187
|
return files;
|
|
183
188
|
}
|
|
184
189
|
|
|
185
|
-
function readPreviousManifest(manifestPath: string):
|
|
186
|
-
if (!existsSync(manifestPath)) return
|
|
190
|
+
function readPreviousManifest(manifestPath: string): ManifestReadResult {
|
|
191
|
+
if (!existsSync(manifestPath)) return { kind: "missing" };
|
|
192
|
+
let content: Buffer | undefined;
|
|
187
193
|
try {
|
|
188
|
-
|
|
194
|
+
content = readFileSync(manifestPath);
|
|
195
|
+
const manifest = parseManifest(JSON.parse(content.toString("utf8")), manifestPath);
|
|
196
|
+
return { kind: "valid", manifest };
|
|
189
197
|
} catch (error) {
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* Identify untouched files created by the old opt-out synchronizer without
|
|
202
|
-
* changing the user filesystem. Direct bundled discovery can then use newer
|
|
203
|
-
* package definitions while real user edits continue to override them.
|
|
204
|
-
*/
|
|
205
|
-
export function unmodifiedManagedAgentNames(agentDir: string): Set<string> {
|
|
206
|
-
const manifestPath = join(agentDir, STATE_DIR_NAME, MANIFEST_FILE_NAME);
|
|
207
|
-
if (!existsSync(manifestPath)) return new Set();
|
|
208
|
-
|
|
209
|
-
let manifest: AgentManifest;
|
|
210
|
-
try {
|
|
211
|
-
manifest = parseManifest(JSON.parse(readFileSync(manifestPath, "utf8")), manifestPath);
|
|
212
|
-
} catch {
|
|
213
|
-
// A malformed historical manifest must never cause a default read-only
|
|
214
|
-
// session to hide user files or rewrite state.
|
|
215
|
-
return new Set();
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const unmodified = new Set<string>();
|
|
219
|
-
const userAgentsDir = join(agentDir, "agents");
|
|
220
|
-
for (const [name, expectedHash] of Object.entries(manifest.files)) {
|
|
198
|
+
const fingerprint =
|
|
199
|
+
typeof content === "undefined"
|
|
200
|
+
? randomUUID().slice(0, 12)
|
|
201
|
+
: hash(content).slice(0, 12);
|
|
202
|
+
const corruptPath = `${manifestPath}.corrupt-${fingerprint}`;
|
|
203
|
+
let preservation = `a copy was preserved at ${corruptPath}`;
|
|
221
204
|
try {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
// Missing, unreadable, or replaced paths are user-controlled and
|
|
228
|
-
// therefore remain visible to discovery.
|
|
205
|
+
if (!existsSync(corruptPath)) copyFileSync(manifestPath, corruptPath);
|
|
206
|
+
} catch (backupError) {
|
|
207
|
+
preservation = `the corrupt file could not be copied: ${
|
|
208
|
+
backupError instanceof Error ? backupError.message : String(backupError)
|
|
209
|
+
}`;
|
|
229
210
|
}
|
|
211
|
+
return {
|
|
212
|
+
kind: "corrupt",
|
|
213
|
+
diagnostic:
|
|
214
|
+
`${manifestPath}: invalid manifest; bundled-template initialization was skipped and user/project agents remain available; ${preservation}: ${
|
|
215
|
+
error instanceof Error ? error.message : String(error)
|
|
216
|
+
}`,
|
|
217
|
+
};
|
|
230
218
|
}
|
|
231
|
-
return unmodified;
|
|
232
219
|
}
|
|
233
220
|
|
|
234
221
|
function sleepSync(milliseconds: number): void {
|
|
@@ -445,15 +432,46 @@ function syncBundledAgentsLocked(
|
|
|
445
432
|
manifestPath: string,
|
|
446
433
|
): AgentSyncResult {
|
|
447
434
|
const diagnostics: string[] = [];
|
|
448
|
-
const
|
|
449
|
-
|
|
435
|
+
const manifestRead = readPreviousManifest(manifestPath);
|
|
436
|
+
if (manifestRead.kind === "corrupt") {
|
|
437
|
+
return {
|
|
438
|
+
packageVersion,
|
|
439
|
+
userAgentsDir,
|
|
440
|
+
manifestPath,
|
|
441
|
+
installed: [],
|
|
442
|
+
updated: [],
|
|
443
|
+
removed: [],
|
|
444
|
+
preserved: [],
|
|
445
|
+
backups: [],
|
|
446
|
+
diagnostics: [manifestRead.diagnostic],
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
const previous = manifestRead.kind === "valid" ? manifestRead.manifest : undefined;
|
|
450
|
+
|
|
451
|
+
// The package copies are initialization templates, not a runtime fallback.
|
|
452
|
+
// Once this package version has been initialized, the user directory is
|
|
453
|
+
// authoritative: edits and deletions must survive every same-version start.
|
|
454
|
+
if (previous?.packageVersion === packageVersion) {
|
|
455
|
+
return {
|
|
456
|
+
packageVersion,
|
|
457
|
+
userAgentsDir,
|
|
458
|
+
manifestPath,
|
|
459
|
+
installed: [],
|
|
460
|
+
updated: [],
|
|
461
|
+
removed: [],
|
|
462
|
+
preserved: [],
|
|
463
|
+
backups: [],
|
|
464
|
+
diagnostics,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
450
468
|
const files = bundledAgentFiles(options.bundledDir);
|
|
451
469
|
const currentNames = new Set(files.map((file) => file.name));
|
|
452
470
|
const preserved: string[] = [];
|
|
453
471
|
const actions: PlannedAction[] = [];
|
|
454
472
|
|
|
455
|
-
// Plan the complete
|
|
456
|
-
//
|
|
473
|
+
// Plan the complete first-install or version-change operation before
|
|
474
|
+
// changing any user agent file.
|
|
457
475
|
for (const file of files) {
|
|
458
476
|
const destination = join(userAgentsDir, file.name);
|
|
459
477
|
const destinationState = destinationKind(destination);
|
|
@@ -468,13 +486,6 @@ function syncBundledAgentsLocked(
|
|
|
468
486
|
continue;
|
|
469
487
|
}
|
|
470
488
|
|
|
471
|
-
const previousHash = previous?.files[file.name];
|
|
472
|
-
const bundledChanged = previousHash === undefined || previousHash !== file.hash;
|
|
473
|
-
const refresh = previous === undefined || packageChanged || bundledChanged;
|
|
474
|
-
if (!refresh) {
|
|
475
|
-
preserved.push(file.name);
|
|
476
|
-
continue;
|
|
477
|
-
}
|
|
478
489
|
if (
|
|
479
490
|
destinationState.kind === "file" &&
|
|
480
491
|
sameRegularFile(destination, destinationState.size, file.content)
|
package/src/agents.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
3
|
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
4
4
|
import { CONFIG_DIR_NAME, getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
@@ -13,10 +13,7 @@ export interface AgentDiscoveryOptions {
|
|
|
13
13
|
cwd: string;
|
|
14
14
|
scope: AgentScope;
|
|
15
15
|
projectTrusted: boolean;
|
|
16
|
-
bundledDir: string;
|
|
17
16
|
agentDir?: string;
|
|
18
|
-
includeBundled?: boolean;
|
|
19
|
-
excludeUserAgentNames?: ReadonlySet<string>;
|
|
20
17
|
}
|
|
21
18
|
|
|
22
19
|
export interface AgentDiscoveryResult {
|
|
@@ -94,11 +91,7 @@ function loadAgentFile(filePath: string, source: AgentSource): AgentDefinition {
|
|
|
94
91
|
};
|
|
95
92
|
}
|
|
96
93
|
|
|
97
|
-
function loadDirectory(
|
|
98
|
-
dir: string,
|
|
99
|
-
source: AgentSource,
|
|
100
|
-
excludeNames?: ReadonlySet<string>,
|
|
101
|
-
): { agents: AgentDefinition[]; diagnostics: string[] } {
|
|
94
|
+
function loadDirectory(dir: string, source: AgentSource): { agents: AgentDefinition[]; diagnostics: string[] } {
|
|
102
95
|
if (!isDirectory(dir)) return { agents: [], diagnostics: [] };
|
|
103
96
|
const agents: AgentDefinition[] = [];
|
|
104
97
|
const diagnostics: string[] = [];
|
|
@@ -114,7 +107,6 @@ function loadDirectory(
|
|
|
114
107
|
|
|
115
108
|
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
116
109
|
if (!entry.name.endsWith(".md") || (!entry.isFile() && !entry.isSymbolicLink())) continue;
|
|
117
|
-
if (excludeNames?.has(entry.name)) continue;
|
|
118
110
|
const filePath = join(dir, entry.name);
|
|
119
111
|
try {
|
|
120
112
|
agents.push(loadAgentFile(filePath, source));
|
|
@@ -131,9 +123,6 @@ export function discoverAgents(options: AgentDiscoveryOptions): AgentDiscoveryRe
|
|
|
131
123
|
? findNearestProjectAgentsDir(options.cwd)
|
|
132
124
|
: undefined;
|
|
133
125
|
const sources: Array<{ dir: string; source: AgentSource }> = [];
|
|
134
|
-
if (options.includeBundled !== false) {
|
|
135
|
-
sources.push({ dir: options.bundledDir, source: "bundled" });
|
|
136
|
-
}
|
|
137
126
|
if (options.scope !== "project") {
|
|
138
127
|
sources.push({ dir: join(options.agentDir ?? getAgentDir(), "agents"), source: "user" });
|
|
139
128
|
}
|
|
@@ -148,11 +137,7 @@ export function discoverAgents(options: AgentDiscoveryOptions): AgentDiscoveryRe
|
|
|
148
137
|
|
|
149
138
|
const byName = new Map<string, AgentDefinition>();
|
|
150
139
|
for (const item of sources) {
|
|
151
|
-
const loaded = loadDirectory(
|
|
152
|
-
item.dir,
|
|
153
|
-
item.source,
|
|
154
|
-
item.source === "user" ? options.excludeUserAgentNames : undefined,
|
|
155
|
-
);
|
|
140
|
+
const loaded = loadDirectory(item.dir, item.source);
|
|
156
141
|
diagnostics.push(...loaded.diagnostics);
|
|
157
142
|
for (const agent of loaded.agents) byName.set(agent.name, agent);
|
|
158
143
|
}
|
|
@@ -168,7 +153,3 @@ export function formatAgentCatalog(agents: AgentDefinition[]): string {
|
|
|
168
153
|
if (agents.length === 0) return "(no agents)";
|
|
169
154
|
return agents.map((agent) => `${agent.name} (${agent.source}) — ${agent.description}`).join("\n");
|
|
170
155
|
}
|
|
171
|
-
|
|
172
|
-
export function hasBundledAgents(dir: string): boolean {
|
|
173
|
-
return existsSync(dir) && isDirectory(dir);
|
|
174
|
-
}
|
package/src/catalog.ts
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
import { SessionManager } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { foldDescriptor } from "./descriptor.ts";
|
|
3
|
+
import {
|
|
4
|
+
foldCompletionMailbox,
|
|
5
|
+
unreadCompletionCounts,
|
|
6
|
+
} from "./completion-mailbox.ts";
|
|
7
|
+
import { foldOwnedMailbox } from "./mailbox.ts";
|
|
3
8
|
import type { SessionView } from "./providers.ts";
|
|
4
9
|
import type { CatalogDiagnostic, SubagentDescriptor } from "./types.ts";
|
|
5
10
|
|
|
6
11
|
export interface PersistedDescriptor {
|
|
7
|
-
|
|
12
|
+
agentId: string;
|
|
13
|
+
piSessionId: string;
|
|
8
14
|
sessionFile: string;
|
|
9
15
|
descriptor: SubagentDescriptor;
|
|
16
|
+
pendingMessages: number;
|
|
17
|
+
unreadUpdatesByChild: Map<string, number>;
|
|
10
18
|
}
|
|
11
19
|
|
|
12
20
|
export interface PersistedCatalog {
|
|
@@ -44,7 +52,7 @@ export async function readPersistedCatalog(session: SessionView): Promise<Persis
|
|
|
44
52
|
if (headerParent !== folded.descriptor.parentSessionFile) {
|
|
45
53
|
diagnostics.push({
|
|
46
54
|
kind: "diagnostic",
|
|
47
|
-
|
|
55
|
+
piSessionId: manager.getSessionId(),
|
|
48
56
|
reason: "corrupt",
|
|
49
57
|
sessionFile: info.path,
|
|
50
58
|
...(headerParent ? { parentSessionFile: headerParent } : {}),
|
|
@@ -52,16 +60,57 @@ export async function readPersistedCatalog(session: SessionView): Promise<Persis
|
|
|
52
60
|
});
|
|
53
61
|
return;
|
|
54
62
|
}
|
|
63
|
+
let pendingMessages = 0;
|
|
64
|
+
if (folded.descriptor.runtime.backgroundProtocol === "mailbox-v2") {
|
|
65
|
+
const mailbox = foldOwnedMailbox(manager.getEntries(), {
|
|
66
|
+
parentAgentId: folded.descriptor.parentAgentId,
|
|
67
|
+
agentId: folded.descriptor.agentId,
|
|
68
|
+
});
|
|
69
|
+
if (mailbox.kind === "corrupt") {
|
|
70
|
+
diagnostics.push({
|
|
71
|
+
kind: "diagnostic",
|
|
72
|
+
piSessionId: manager.getSessionId(),
|
|
73
|
+
reason: "corrupt",
|
|
74
|
+
sessionFile: info.path,
|
|
75
|
+
...(headerParent ? { parentSessionFile: headerParent } : {}),
|
|
76
|
+
message: `corrupt subagent mailbox: ${mailbox.message}`,
|
|
77
|
+
});
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
pendingMessages = mailbox.snapshot.pending.length;
|
|
81
|
+
}
|
|
82
|
+
let unreadUpdatesByChild = new Map<string, number>();
|
|
83
|
+
const completions = foldCompletionMailbox(
|
|
84
|
+
manager.getEntries(),
|
|
85
|
+
{ parentAgentId: folded.descriptor.agentId },
|
|
86
|
+
);
|
|
87
|
+
if (completions.kind === "corrupt") {
|
|
88
|
+
diagnostics.push({
|
|
89
|
+
kind: "diagnostic",
|
|
90
|
+
piSessionId: manager.getSessionId(),
|
|
91
|
+
reason: "corrupt",
|
|
92
|
+
sessionFile: info.path,
|
|
93
|
+
...(headerParent ? { parentSessionFile: headerParent } : {}),
|
|
94
|
+
message: `corrupt completion mailbox: ${completions.message}`,
|
|
95
|
+
});
|
|
96
|
+
} else {
|
|
97
|
+
unreadUpdatesByChild = unreadCompletionCounts(
|
|
98
|
+
completions.snapshot,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
55
101
|
descriptors.push({
|
|
56
|
-
|
|
102
|
+
agentId: folded.descriptor.agentId,
|
|
103
|
+
piSessionId: manager.getSessionId(),
|
|
57
104
|
sessionFile: info.path,
|
|
58
105
|
descriptor: folded.descriptor,
|
|
106
|
+
pendingMessages,
|
|
107
|
+
unreadUpdatesByChild,
|
|
59
108
|
});
|
|
60
109
|
} else if (folded.kind === "corrupt") {
|
|
61
110
|
const headerParent = manager.getHeader()?.parentSession;
|
|
62
111
|
diagnostics.push({
|
|
63
112
|
kind: "diagnostic",
|
|
64
|
-
|
|
113
|
+
piSessionId: manager.getSessionId(),
|
|
65
114
|
reason: "corrupt",
|
|
66
115
|
sessionFile: info.path,
|
|
67
116
|
...(headerParent ? { parentSessionFile: headerParent } : {}),
|
|
@@ -71,7 +120,7 @@ export async function readPersistedCatalog(session: SessionView): Promise<Persis
|
|
|
71
120
|
} catch (error) {
|
|
72
121
|
diagnostics.push({
|
|
73
122
|
kind: "diagnostic",
|
|
74
|
-
|
|
123
|
+
piSessionId: info.id,
|
|
75
124
|
reason: "unavailable",
|
|
76
125
|
sessionFile: info.path,
|
|
77
126
|
...(info.parentSessionPath ? { parentSessionFile: info.parentSessionPath } : {}),
|
|
@@ -82,8 +131,11 @@ export async function readPersistedCatalog(session: SessionView): Promise<Persis
|
|
|
82
131
|
|
|
83
132
|
descriptors.sort(
|
|
84
133
|
(left, right) =>
|
|
85
|
-
left.descriptor.createdAt.localeCompare(right.descriptor.createdAt) ||
|
|
134
|
+
left.descriptor.createdAt.localeCompare(right.descriptor.createdAt) ||
|
|
135
|
+
left.agentId.localeCompare(right.agentId),
|
|
136
|
+
);
|
|
137
|
+
diagnostics.sort((left, right) =>
|
|
138
|
+
left.piSessionId.localeCompare(right.piSessionId),
|
|
86
139
|
);
|
|
87
|
-
diagnostics.sort((left, right) => left.id.localeCompare(right.id));
|
|
88
140
|
return { descriptors, diagnostics };
|
|
89
141
|
}
|