@llblab/pi-actors 0.40.1 → 0.41.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.
@@ -3,6 +3,7 @@
3
3
  * Zones: terminal run inspection, room/direct message previews, no-dependency UI formatting
4
4
  * Owns read-only previews and TUI-ready summaries; actor routing stays outside.
5
5
  */
6
+ import * as SessionEvidence from "./session-evidence.ts";
6
7
  export interface ActorInspectorPreview {
7
8
  body_preview?: string;
8
9
  branch?: string;
@@ -45,47 +46,30 @@ export interface ActorInspectorPreviewReadOptions {
45
46
  ownerId?: string;
46
47
  branch?: string;
47
48
  currentRunOnly?: boolean;
49
+ run?: string;
48
50
  channels?: ActorInspectorPreview["channel"][];
49
51
  mention?: string;
50
52
  readKeys?: Iterable<string>;
51
53
  roomLimitPerRun?: number;
52
54
  unreadOnly?: boolean;
53
55
  }
54
- export interface ActorInspectorControllerState {
55
- branch?: string;
56
- channels?: ActorInspectorPreview["channel"][];
57
- mention?: string;
58
- readKeys: Set<string>;
59
- roomLimitPerRun: number;
60
- rows: number;
61
- selectedSequence?: number;
62
- unreadOnly: boolean;
63
- visible: boolean;
56
+ export interface ActorInspectorRunItem {
57
+ run: string;
58
+ status: string;
59
+ updatedAt?: string;
64
60
  }
65
- export interface ActorInspectorCommandResult {
66
- notify: string;
67
- type: "info" | "warning";
68
- update: boolean;
61
+ export declare function readActorInspectorRuns(stateRoot: string, ownerId: string): ActorInspectorRunItem[];
62
+ export interface ActorInspectorTurnItem extends SessionEvidence.SessionEvidenceTurn {
63
+ commandId: string;
64
+ diagnostics: string[];
65
+ promptBytes?: number;
66
+ promptFile?: string;
67
+ recipeContext?: unknown;
68
+ sessionFile: string;
69
+ sessionTruncated: boolean;
70
+ stage?: string;
69
71
  }
70
- export declare const DEFAULT_ACTOR_INSPECTOR_ROWS = 12;
71
- export declare const ACTOR_INSPECTOR_COMMAND_DESCRIPTIONS: {
72
- filter: string;
73
- inspect: string;
74
- toggle: string;
75
- };
76
- export declare function createActorInspectorControllerState(): ActorInspectorControllerState;
77
- export declare function getActorInspectorPreviewOptions(state: ActorInspectorControllerState, ownerId: string): ActorInspectorPreviewReadOptions;
78
- export declare function handleActorInspectorToggle(state: ActorInspectorControllerState, args: unknown): ActorInspectorCommandResult;
79
- export declare function handleActorInspectorFilter(state: ActorInspectorControllerState, args: unknown): ActorInspectorCommandResult;
80
- export declare function handleActorInspectorInspect(state: ActorInspectorControllerState, args: unknown, previews: ActorInspectorPreview[]): ActorInspectorCommandResult;
81
- export declare function selectActorInspectorSequence(state: ActorInspectorControllerState, sequence: number, previews: ActorInspectorPreview[]): ActorInspectorCommandResult;
82
- export declare function renderActorInspectorPanel(input: {
83
- stateRoot: string;
84
- state: ActorInspectorControllerState;
85
- ownerId: string;
86
- width: number;
87
- style: ActorInspectorWidgetStyle;
88
- }): string[];
72
+ export declare function readActorInspectorTurns(stateDir: string): ActorInspectorTurnItem[];
89
73
  export declare function inspectorPreviewReadKey(preview: ActorInspectorPreview): string;
90
74
  export declare function readActorInspectorPreviews(stateRoot?: string, limit?: number, options?: ActorInspectorPreviewReadOptions): ActorInspectorPreview[];
91
75
  export declare function readActorInspectorRoster(stateRoot: string | undefined, run: string, room?: string): ActorInspectorRosterMember[];
@@ -8,164 +8,108 @@ import * as path from "node:path";
8
8
  import { visibleWidth } from "@earendil-works/pi-tui";
9
9
  import * as Limits from "./limits.js";
10
10
  import * as Paths from "./paths.js";
11
+ import * as SessionEvidence from "./session-evidence.js";
11
12
  import { readJsonFileResilient, readJsonlFileResilient, } from "./state-readers.js";
12
- export const DEFAULT_ACTOR_INSPECTOR_ROWS = 12;
13
- export const ACTOR_INSPECTOR_COMMAND_DESCRIPTIONS = {
14
- filter: "Filter actor inspector rows: all, room, direct, broadcast, unread, branch <name>, mention <text>",
15
- inspect: "Inspect actor message by visible number",
16
- toggle: "Toggle actor inspector widget; optional row count",
17
- };
18
- export function createActorInspectorControllerState() {
19
- return {
20
- readKeys: new Set(),
21
- roomLimitPerRun: DEFAULT_ACTOR_INSPECTOR_ROWS,
22
- rows: DEFAULT_ACTOR_INSPECTOR_ROWS,
23
- unreadOnly: false,
24
- visible: false,
25
- };
26
- }
27
- export function getActorInspectorPreviewOptions(state, ownerId) {
28
- return {
29
- channels: state.channels,
30
- currentRunOnly: true,
31
- branch: state.branch,
32
- mention: state.mention,
33
- ownerId,
34
- readKeys: state.readKeys,
35
- roomLimitPerRun: state.roomLimitPerRun,
36
- unreadOnly: state.unreadOnly,
37
- };
38
- }
39
- export function handleActorInspectorToggle(state, args) {
40
- const raw = Array.isArray(args) ? args[0] : String(args ?? "");
41
- if (String(raw).trim()) {
42
- const rows = Number.parseInt(String(raw), 10);
43
- if (!Number.isFinite(rows) || rows <= 0) {
44
- return {
45
- notify: "Usage: /actors-inspector-toggle [rows] where rows > 0",
46
- type: "warning",
47
- update: false,
48
- };
49
- }
50
- state.rows = rows;
51
- state.roomLimitPerRun = rows;
52
- state.selectedSequence = undefined;
53
- state.visible = true;
54
- return {
55
- notify: `Actor inspector rows ${rows}`,
56
- type: "info",
57
- update: true,
58
- };
59
- }
60
- if (state.selectedSequence !== undefined) {
61
- state.selectedSequence = undefined;
62
- state.visible = true;
63
- return { notify: "Actor inspector table", type: "info", update: true };
13
+ export function readActorInspectorRuns(stateRoot, ownerId) {
14
+ try {
15
+ return fs
16
+ .readdirSync(stateRoot, { withFileTypes: true })
17
+ .filter((entry) => entry.isDirectory())
18
+ .flatMap((entry) => {
19
+ const stateDir = path.join(stateRoot, entry.name);
20
+ if (!matchesOwner(stateDir, ownerId))
21
+ return [];
22
+ const progress = readJsonFileResilient(path.join(stateDir, "progress.json"), {}).value;
23
+ const result = readJsonFileResilient(path.join(stateDir, "result.json"), {}).value;
24
+ return [{
25
+ run: entry.name,
26
+ status: String(progress.phase ?? (Object.keys(result).length ? "terminal" : "unknown")),
27
+ ...(typeof progress.updatedAt === "string"
28
+ ? { updatedAt: progress.updatedAt }
29
+ : typeof result.completedAt === "string"
30
+ ? { updatedAt: result.completedAt }
31
+ : {}),
32
+ }];
33
+ })
34
+ .sort((left, right) => String(left.updatedAt ?? "").localeCompare(String(right.updatedAt ?? "")));
64
35
  }
65
- if (state.visible)
66
- state.visible = false;
67
- else {
68
- state.rows = DEFAULT_ACTOR_INSPECTOR_ROWS;
69
- state.roomLimitPerRun = DEFAULT_ACTOR_INSPECTOR_ROWS;
70
- state.visible = true;
36
+ catch (error) {
37
+ if (error.code === "ENOENT")
38
+ return [];
39
+ return [];
71
40
  }
72
- return {
73
- notify: `Actor inspector ${state.visible ? "shown" : "hidden"}`,
74
- type: "info",
75
- update: true,
76
- };
77
41
  }
78
- export function handleActorInspectorFilter(state, args) {
79
- const parts = Array.isArray(args)
80
- ? args.map(String)
81
- : String(args ?? "").split(/\s+/);
82
- const mode = (parts[0] ?? "").trim().toLowerCase();
83
- if (!mode || mode === "all" || mode === "clear") {
84
- state.channels = undefined;
85
- state.mention = undefined;
86
- state.branch = undefined;
87
- state.unreadOnly = false;
88
- }
89
- else if (mode === "room" || mode === "direct" || mode === "broadcast") {
90
- state.channels = [mode];
91
- state.mention = undefined;
92
- }
93
- else if (mode === "unread") {
94
- state.unreadOnly = true;
95
- }
96
- else if (mode === "branch" || mode === "current-branch") {
97
- const branch = parts.slice(1).join(" ").trim();
98
- if (!branch) {
99
- return {
100
- notify: `Usage: /actors-inspector-filter ${mode} <branch-name>`,
101
- type: "warning",
102
- update: false,
103
- };
104
- }
105
- state.branch = branch;
106
- }
107
- else if (mode === "mention") {
108
- const mention = parts.slice(1).join(" ").trim();
109
- if (!mention) {
110
- return {
111
- notify: "Usage: /actors-inspector-filter mention <text>",
112
- type: "warning",
113
- update: false,
114
- };
115
- }
116
- state.channels = undefined;
117
- state.mention = mention;
42
+ function ownedSessionPath(stateDir, sessionFile) {
43
+ if (path.isAbsolute(sessionFile))
44
+ return undefined;
45
+ const sessionsRoot = path.resolve(stateDir, "sessions");
46
+ const resolved = path.resolve(stateDir, sessionFile);
47
+ if (!resolved.startsWith(`${sessionsRoot}${path.sep}`))
48
+ return undefined;
49
+ try {
50
+ const canonicalRoot = fs.realpathSync(sessionsRoot);
51
+ const canonicalFile = fs.realpathSync(resolved);
52
+ return canonicalFile.startsWith(`${canonicalRoot}${path.sep}`)
53
+ ? canonicalFile
54
+ : undefined;
118
55
  }
119
- else {
120
- return {
121
- notify: "Usage: /actors-inspector-filter all|room|direct|broadcast|unread|branch <name>|mention <text>",
122
- type: "warning",
123
- update: false,
124
- };
56
+ catch {
57
+ return undefined;
125
58
  }
126
- state.selectedSequence = undefined;
127
- state.visible = true;
128
- return {
129
- notify: `Actor inspector filter ${mode || "all"}`,
130
- type: "info",
131
- update: true,
132
- };
133
59
  }
134
- export function handleActorInspectorInspect(state, args, previews) {
135
- const raw = Array.isArray(args) ? args[0] : String(args ?? "");
136
- return selectActorInspectorSequence(state, Number.parseInt(String(raw), 10), previews);
137
- }
138
- export function selectActorInspectorSequence(state, sequence, previews) {
139
- if (!Number.isFinite(sequence) || sequence <= 0) {
140
- return {
141
- notify: "Usage: /actors-inspect <number>",
142
- type: "warning",
143
- update: false,
144
- };
60
+ export function readActorInspectorTurns(stateDir) {
61
+ const evidence = readJsonFileResilient(path.join(stateDir, "review-evidence.json"), {}).value;
62
+ const commands = Array.isArray(evidence.commands)
63
+ ? evidence.commands
64
+ : [];
65
+ const recordedFiles = new Set(commands.flatMap((command) => (Array.isArray(command.session_files) ? command.session_files : []).filter((file) => typeof file === "string")));
66
+ let discoveredCommands = [];
67
+ try {
68
+ const sessionsDir = path.join(stateDir, "sessions");
69
+ discoveredCommands = fs
70
+ .readdirSync(sessionsDir, { withFileTypes: true })
71
+ .filter((entry) => entry.isDirectory())
72
+ .flatMap((entry) => {
73
+ const files = fs
74
+ .readdirSync(path.join(sessionsDir, entry.name), {
75
+ withFileTypes: true,
76
+ })
77
+ .filter((file) => file.isFile() && file.name.endsWith(".jsonl"))
78
+ .map((file) => path.join("sessions", entry.name, file.name))
79
+ .filter((file) => !recordedFiles.has(file));
80
+ return files.length > 0
81
+ ? [{ id: entry.name, session_files: files, stage: "subagent" }]
82
+ : [];
83
+ });
145
84
  }
146
- const preview = previews.find((item) => item.sequence === sequence);
147
- if (preview)
148
- state.readKeys.add(inspectorPreviewReadKey(preview));
149
- state.selectedSequence = sequence;
150
- state.visible = true;
151
- return {
152
- notify: `Actor inspect item ${sequence}`,
153
- type: "info",
154
- update: true,
155
- };
156
- }
157
- export function renderActorInspectorPanel(input) {
158
- const previews = readActorInspectorPreviews(input.stateRoot, input.state.rows, getActorInspectorPreviewOptions(input.state, input.ownerId));
159
- const rows = (input.state.selectedSequence !== undefined
160
- ? renderInspectorItemView(previews, input.width, input.style, {
161
- sequence: input.state.selectedSequence,
162
- })
163
- : renderInspectorWidget(previews, input.width, input.style)) ?? [];
164
- const run = previews[0]?.run;
165
- const roster = input.state.selectedSequence === undefined && run
166
- ? renderInspectorRosterPanel(readActorInspectorRoster(input.stateRoot, run), input.width, input.style)
167
- : undefined;
168
- return roster ? [...roster, ...rows] : rows;
85
+ catch {
86
+ discoveredCommands = [];
87
+ }
88
+ return [...commands, ...discoveredCommands].flatMap((command) => (Array.isArray(command.session_files) ? command.session_files : [])
89
+ .filter((file) => typeof file === "string")
90
+ .flatMap((file) => {
91
+ const sessionPath = ownedSessionPath(stateDir, file);
92
+ if (!sessionPath)
93
+ return [];
94
+ const session = SessionEvidence.readSessionEvidence(sessionPath);
95
+ return session.turns.map((turn) => ({
96
+ ...turn,
97
+ commandId: String(command.id ?? "unknown"),
98
+ diagnostics: session.diagnostics.map((item) => `${item.line === undefined ? "" : `line ${item.line}: `}${item.message}`),
99
+ ...(typeof command.prompt_bytes === "number"
100
+ ? { promptBytes: command.prompt_bytes }
101
+ : {}),
102
+ ...(typeof command.prompt_file === "string"
103
+ ? { promptFile: command.prompt_file }
104
+ : {}),
105
+ ...(command.recipe_context !== undefined
106
+ ? { recipeContext: command.recipe_context }
107
+ : {}),
108
+ sessionFile: file,
109
+ sessionTruncated: session.truncated,
110
+ ...(typeof command.stage === "string" ? { stage: command.stage } : {}),
111
+ }));
112
+ })).map((turn, index) => ({ ...turn, index: index + 1 }));
169
113
  }
170
114
  function asRecord(value) {
171
115
  return value && typeof value === "object" && !Array.isArray(value)
@@ -178,7 +122,8 @@ function readJsonLines(file) {
178
122
  function previewValue(value, maxLength = Limits.INSPECTOR_BODY_PREVIEW_CHARS) {
179
123
  if (value === undefined)
180
124
  return undefined;
181
- const text = typeof value === "string" ? value : JSON.stringify(value);
125
+ const redacted = SessionEvidence.redactSessionEvidenceValue(value);
126
+ const text = typeof redacted === "string" ? redacted : JSON.stringify(redacted);
182
127
  const compact = text.replaceAll(/\s+/g, " ").trim();
183
128
  if (!compact)
184
129
  return undefined;
@@ -393,6 +338,8 @@ export function readActorInspectorPreviews(stateRoot = Paths.getRunStateRoot(),
393
338
  const stateDir = path.join(stateRoot, entry.name);
394
339
  if (!matchesOwner(stateDir, options.ownerId))
395
340
  return [];
341
+ if (options.run && entry.name !== options.run)
342
+ return [];
396
343
  return [
397
344
  ...readRoomPreviews(entry.name, stateDir),
398
345
  ...readInboxPreviews(entry.name, stateDir),
@@ -9,3 +9,6 @@ export declare const COMPACT_PREVIEW_CHARS = 160;
9
9
  export declare const INSPECTOR_BODY_PREVIEW_CHARS = 320;
10
10
  export declare const ROOM_MESSAGE_PREVIEW_CHARS = 320;
11
11
  export declare const DOCTOR_ACTION_PREVIEW_CHARS = 72;
12
+ export declare const SESSION_EVIDENCE_MAX_TURNS = 100;
13
+ export declare const SESSION_EVIDENCE_TEXT_CHARS = 4000;
14
+ export declare const SESSION_EVIDENCE_MAX_TOOL_CALLS = 100;
@@ -9,3 +9,6 @@ export const COMPACT_PREVIEW_CHARS = 160;
9
9
  export const INSPECTOR_BODY_PREVIEW_CHARS = 320;
10
10
  export const ROOM_MESSAGE_PREVIEW_CHARS = 320;
11
11
  export const DOCTOR_ACTION_PREVIEW_CHARS = 72;
12
+ export const SESSION_EVIDENCE_MAX_TURNS = 100;
13
+ export const SESSION_EVIDENCE_TEXT_CHARS = 4_000;
14
+ export const SESSION_EVIDENCE_MAX_TOOL_CALLS = 100;
@@ -14,6 +14,11 @@ export declare function markRecipeContextRecords(records: TemplateRecipeContextR
14
14
  export declare function formatRecipeContextJsonl(records: TemplateRecipeContextRecord[], context?: CommandTemplateActorRecipeContext): string;
15
15
  export declare function buildRecipeContextPromptBlock(records: TemplateRecipeContextRecord[], context?: CommandTemplateActorRecipeContext): string;
16
16
  export declare function appendRecipeContextToPiArgs(command: string, args: string[], records: TemplateRecipeContextRecord[] | undefined, context?: CommandTemplateActorRecipeContext): string[];
17
+ export interface ManagedPiSessionArgs {
18
+ args: string[];
19
+ sessionDir?: string;
20
+ }
21
+ export declare function attachPiSessionDir(command: string, args: string[], sessionDir: string): ManagedPiSessionArgs;
17
22
  export interface MaterializedPiPrintPromptArgs {
18
23
  args: string[];
19
24
  promptBytes?: number;
@@ -134,6 +134,21 @@ export function appendRecipeContextToPiArgs(command, args, records, context) {
134
134
  next[promptIndex] = `${next[promptIndex]}\n\n${block}`;
135
135
  return next;
136
136
  }
137
+ const PI_SESSION_POLICY_FLAGS = new Set([
138
+ "--fork",
139
+ "--no-session",
140
+ "--session",
141
+ "--session-dir",
142
+ "--session-id",
143
+ ]);
144
+ export function attachPiSessionDir(command, args, sessionDir) {
145
+ if (!isPiCommand(command) || findPiPrintPromptIndex(args) === undefined) {
146
+ return { args };
147
+ }
148
+ if (args.some((arg) => PI_SESSION_POLICY_FLAGS.has(arg)))
149
+ return { args };
150
+ return { args: ["--session-dir", sessionDir, ...args], sessionDir };
151
+ }
137
152
  export function materializePiPrintPromptArg(command, args, promptFile) {
138
153
  if (!isPiCommand(command))
139
154
  return { args };
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Persisted Pi session evidence reader.
3
+ * Zones: subagent turns, active session branches, bounded/redacted previews
4
+ * Owns read-only normalization of session JSONL into inspector-ready turns.
5
+ */
6
+ import { type StateReadDiagnostic } from "./state-readers.ts";
7
+ export interface SessionEvidenceToolCall {
8
+ arguments?: unknown;
9
+ id: string;
10
+ name: string;
11
+ result?: unknown;
12
+ resultError?: boolean;
13
+ }
14
+ export interface SessionEvidenceTurn {
15
+ assistantEntryId?: string;
16
+ assistantText?: string;
17
+ error?: string;
18
+ index: number;
19
+ model?: string;
20
+ provider?: string;
21
+ stopReason?: string;
22
+ thinking?: string;
23
+ timestamp?: string;
24
+ toolCalls: SessionEvidenceToolCall[];
25
+ unmatchedToolResults: number;
26
+ usage?: unknown;
27
+ userEntryId?: string;
28
+ userText?: string;
29
+ }
30
+ export interface SessionEvidence {
31
+ activeLeafId?: string;
32
+ diagnostics: StateReadDiagnostic[];
33
+ path: string;
34
+ session?: Record<string, unknown>;
35
+ totalTurns: number;
36
+ truncated: boolean;
37
+ turns: SessionEvidenceTurn[];
38
+ }
39
+ export interface SessionEvidenceReadOptions {
40
+ maxTextChars?: number;
41
+ maxToolCalls?: number;
42
+ maxTurns?: number;
43
+ }
44
+ export declare function redactSessionEvidenceValue(value: unknown, maxTextChars?: number, seen?: WeakSet<object>): unknown;
45
+ export declare function readSessionEvidence(path: string, options?: SessionEvidenceReadOptions): SessionEvidence;
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Persisted Pi session evidence reader.
3
+ * Zones: subagent turns, active session branches, bounded/redacted previews
4
+ * Owns read-only normalization of session JSONL into inspector-ready turns.
5
+ */
6
+ import * as Limits from "./limits.js";
7
+ import { readJsonlFileResilient, } from "./state-readers.js";
8
+ const SENSITIVE_KEY = /(?:^|[_-])(?:api[-_]?key|authorization|cookie|credential|password|private[-_]?key|secret|secret[-_]?access[-_]?key|token)$|^(?:access|refresh|auth|api)Token$|^(?:clientSecret|privateKey|secretAccessKey)$/i;
9
+ const SENSITIVE_TEXT = /(bearer\s+)[A-Za-z0-9._~+/=-]+|["']?\b(api[-_]?key|authorization|clientSecret|cookie|password|private[-_]?key|privateKey|secret|secretAccessKey|token)["']?(\s*[:=]\s*)["']?([^\s,;"'}]+)/gi;
10
+ function asRecord(value) {
11
+ return value && typeof value === "object" && !Array.isArray(value)
12
+ ? value
13
+ : {};
14
+ }
15
+ function boundedText(value, maxChars) {
16
+ if (typeof value !== "string")
17
+ return undefined;
18
+ if (/^\s*[\[{]/.test(value)) {
19
+ try {
20
+ const structured = JSON.stringify(redactSessionEvidenceValue(JSON.parse(value), maxChars));
21
+ return structured.length > maxChars
22
+ ? `${structured.slice(0, Math.max(0, maxChars - 1))}…`
23
+ : structured;
24
+ }
25
+ catch {
26
+ // Fall through to bounded text redaction.
27
+ }
28
+ }
29
+ const redacted = value.replaceAll(SENSITIVE_TEXT, (match, bearer, key, separator) => bearer ? `${bearer}[REDACTED]` : `${key}${separator}[REDACTED]`);
30
+ return redacted.length > maxChars
31
+ ? `${redacted.slice(0, Math.max(0, maxChars - 1))}…`
32
+ : redacted;
33
+ }
34
+ export function redactSessionEvidenceValue(value, maxTextChars = Limits.SESSION_EVIDENCE_TEXT_CHARS, seen = new WeakSet()) {
35
+ if (typeof value === "string")
36
+ return boundedText(value, maxTextChars);
37
+ if (!value || typeof value !== "object")
38
+ return value;
39
+ if (seen.has(value))
40
+ return "[CIRCULAR]";
41
+ seen.add(value);
42
+ if (Array.isArray(value)) {
43
+ return value.map((item) => redactSessionEvidenceValue(item, maxTextChars, seen));
44
+ }
45
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
46
+ key,
47
+ SENSITIVE_KEY.test(key)
48
+ ? "[REDACTED]"
49
+ : redactSessionEvidenceValue(item, maxTextChars, seen),
50
+ ]));
51
+ }
52
+ function contentBlocks(message) {
53
+ return Array.isArray(message.content)
54
+ ? message.content.map(asRecord)
55
+ : typeof message.content === "string"
56
+ ? [{ type: "text", text: message.content }]
57
+ : [];
58
+ }
59
+ function contentText(message, type, maxChars) {
60
+ const text = contentBlocks(message)
61
+ .filter((block) => block.type === type && typeof block[type] === "string")
62
+ .map((block) => String(block[type]))
63
+ .join("\n");
64
+ return boundedText(text, maxChars);
65
+ }
66
+ function activeBranch(entries, path, diagnostics) {
67
+ const treeEntries = entries.filter((entry) => entry.type !== "session" && typeof entry.id === "string");
68
+ const leaf = treeEntries.at(-1);
69
+ if (!leaf || typeof leaf.id !== "string")
70
+ return [];
71
+ const byId = new Map(treeEntries.map((entry) => [String(entry.id), entry]));
72
+ const branch = [];
73
+ const visited = new Set();
74
+ let current = leaf;
75
+ while (current && typeof current.id === "string") {
76
+ if (visited.has(current.id)) {
77
+ diagnostics.push({ message: `session entry cycle at ${current.id}`, path });
78
+ break;
79
+ }
80
+ visited.add(current.id);
81
+ branch.push(current);
82
+ if (current.parentId === null || current.parentId === undefined)
83
+ break;
84
+ if (typeof current.parentId !== "string" || !byId.has(current.parentId)) {
85
+ diagnostics.push({
86
+ message: `missing parent ${String(current.parentId)} for ${current.id}`,
87
+ path,
88
+ });
89
+ break;
90
+ }
91
+ current = byId.get(current.parentId);
92
+ }
93
+ return branch.reverse();
94
+ }
95
+ function toolCalls(message, maxTextChars, maxToolCalls) {
96
+ return contentBlocks(message)
97
+ .filter((block) => block.type === "toolCall" &&
98
+ typeof block.id === "string" &&
99
+ typeof block.name === "string")
100
+ .slice(0, maxToolCalls)
101
+ .map((block) => ({
102
+ arguments: redactSessionEvidenceValue(block.arguments, maxTextChars),
103
+ id: String(block.id),
104
+ name: String(block.name),
105
+ }));
106
+ }
107
+ export function readSessionEvidence(path, options = {}) {
108
+ const maxTextChars = Math.max(1, options.maxTextChars ?? Limits.SESSION_EVIDENCE_TEXT_CHARS);
109
+ const maxToolCalls = Math.max(1, options.maxToolCalls ?? Limits.SESSION_EVIDENCE_MAX_TOOL_CALLS);
110
+ const maxTurns = Math.max(1, options.maxTurns ?? Limits.SESSION_EVIDENCE_MAX_TURNS);
111
+ const read = readJsonlFileResilient(path);
112
+ const diagnostics = [...read.diagnostics];
113
+ const header = read.records.find((entry) => entry.type === "session");
114
+ const branch = activeBranch(read.records, path, diagnostics);
115
+ const turns = [];
116
+ let pendingUser;
117
+ let currentTurn;
118
+ for (const entry of branch) {
119
+ if (entry.type !== "message")
120
+ continue;
121
+ const message = asRecord(entry.message);
122
+ const role = message.role;
123
+ if (role === "user") {
124
+ pendingUser = {
125
+ id: String(entry.id),
126
+ text: contentText(message, "text", maxTextChars),
127
+ };
128
+ currentTurn = undefined;
129
+ continue;
130
+ }
131
+ if (role === "assistant") {
132
+ currentTurn = {
133
+ ...(typeof entry.id === "string" ? { assistantEntryId: entry.id } : {}),
134
+ ...(contentText(message, "text", maxTextChars)
135
+ ? { assistantText: contentText(message, "text", maxTextChars) }
136
+ : {}),
137
+ ...(typeof message.errorMessage === "string"
138
+ ? { error: boundedText(message.errorMessage, maxTextChars) }
139
+ : {}),
140
+ index: turns.length + 1,
141
+ ...(typeof message.model === "string" ? { model: message.model } : {}),
142
+ ...(typeof message.provider === "string"
143
+ ? { provider: message.provider }
144
+ : {}),
145
+ ...(typeof message.stopReason === "string"
146
+ ? { stopReason: message.stopReason }
147
+ : {}),
148
+ ...(contentText(message, "thinking", maxTextChars)
149
+ ? { thinking: contentText(message, "thinking", maxTextChars) }
150
+ : {}),
151
+ ...(typeof entry.timestamp === "string"
152
+ ? { timestamp: entry.timestamp }
153
+ : {}),
154
+ toolCalls: toolCalls(message, maxTextChars, maxToolCalls),
155
+ unmatchedToolResults: 0,
156
+ ...(message.usage !== undefined
157
+ ? { usage: redactSessionEvidenceValue(message.usage, maxTextChars) }
158
+ : {}),
159
+ ...(pendingUser
160
+ ? {
161
+ userEntryId: pendingUser.id,
162
+ ...(pendingUser.text ? { userText: pendingUser.text } : {}),
163
+ }
164
+ : {}),
165
+ };
166
+ pendingUser = undefined;
167
+ turns.push(currentTurn);
168
+ continue;
169
+ }
170
+ if (role === "toolResult" && currentTurn) {
171
+ const callId = typeof message.toolCallId === "string" ? message.toolCallId : "";
172
+ const call = currentTurn.toolCalls.find((item) => item.id === callId);
173
+ if (!call) {
174
+ currentTurn.unmatchedToolResults += 1;
175
+ continue;
176
+ }
177
+ call.result = redactSessionEvidenceValue(message.content, maxTextChars);
178
+ call.resultError = message.isError === true;
179
+ }
180
+ }
181
+ if (pendingUser) {
182
+ turns.push({
183
+ index: turns.length + 1,
184
+ toolCalls: [],
185
+ unmatchedToolResults: 0,
186
+ userEntryId: pendingUser.id,
187
+ ...(pendingUser.text ? { userText: pendingUser.text } : {}),
188
+ });
189
+ }
190
+ const firstVisibleIndex = Math.max(0, turns.length - maxTurns);
191
+ const visibleTurns = turns.slice(firstVisibleIndex).map((turn, index) => ({
192
+ ...turn,
193
+ index: firstVisibleIndex + index + 1,
194
+ }));
195
+ return {
196
+ ...(branch.at(-1)?.id ? { activeLeafId: String(branch.at(-1)?.id) } : {}),
197
+ diagnostics,
198
+ path,
199
+ ...(header ? { session: asRecord(header) } : {}),
200
+ totalTurns: turns.length,
201
+ truncated: turns.length > visibleTurns.length,
202
+ turns: visibleTurns,
203
+ };
204
+ }
@@ -3,6 +3,9 @@
3
3
  * Zones: compact text summaries, verbose JSON switching, next-action rendering
4
4
  * Owns model-facing response helpers shared by public tool execution paths
5
5
  */
6
+ export declare function withLeadingBlankLine(text: string): string;
7
+ export declare function spaceToolResult<T>(result: T): T;
8
+ export declare function spaceToolError(error: unknown): unknown;
6
9
  export declare function asRecord(value: unknown): Record<string, unknown>;
7
10
  export declare function jsonText(value: unknown): string;
8
11
  export declare function compactPreview(value: unknown, maxLength?: number): string | undefined;