@llblab/pi-actors 0.40.1 → 0.41.1

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,111 @@ 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) => {
35
+ const recency = String(right.updatedAt ?? "").localeCompare(String(left.updatedAt ?? ""));
36
+ return recency || right.run.localeCompare(left.run);
37
+ });
64
38
  }
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;
39
+ catch (error) {
40
+ if (error.code === "ENOENT")
41
+ return [];
42
+ return [];
71
43
  }
72
- return {
73
- notify: `Actor inspector ${state.visible ? "shown" : "hidden"}`,
74
- type: "info",
75
- update: true,
76
- };
77
44
  }
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;
45
+ function ownedSessionPath(stateDir, sessionFile) {
46
+ if (path.isAbsolute(sessionFile))
47
+ return undefined;
48
+ const sessionsRoot = path.resolve(stateDir, "sessions");
49
+ const resolved = path.resolve(stateDir, sessionFile);
50
+ if (!resolved.startsWith(`${sessionsRoot}${path.sep}`))
51
+ return undefined;
52
+ try {
53
+ const canonicalRoot = fs.realpathSync(sessionsRoot);
54
+ const canonicalFile = fs.realpathSync(resolved);
55
+ return canonicalFile.startsWith(`${canonicalRoot}${path.sep}`)
56
+ ? canonicalFile
57
+ : undefined;
118
58
  }
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
- };
59
+ catch {
60
+ return undefined;
125
61
  }
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
62
  }
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
- };
63
+ export function readActorInspectorTurns(stateDir) {
64
+ const evidence = readJsonFileResilient(path.join(stateDir, "review-evidence.json"), {}).value;
65
+ const commands = Array.isArray(evidence.commands)
66
+ ? evidence.commands
67
+ : [];
68
+ const recordedFiles = new Set(commands.flatMap((command) => (Array.isArray(command.session_files) ? command.session_files : []).filter((file) => typeof file === "string")));
69
+ let discoveredCommands = [];
70
+ try {
71
+ const sessionsDir = path.join(stateDir, "sessions");
72
+ discoveredCommands = fs
73
+ .readdirSync(sessionsDir, { withFileTypes: true })
74
+ .filter((entry) => entry.isDirectory())
75
+ .flatMap((entry) => {
76
+ const files = fs
77
+ .readdirSync(path.join(sessionsDir, entry.name), {
78
+ withFileTypes: true,
79
+ })
80
+ .filter((file) => file.isFile() && file.name.endsWith(".jsonl"))
81
+ .map((file) => path.join("sessions", entry.name, file.name))
82
+ .filter((file) => !recordedFiles.has(file));
83
+ return files.length > 0
84
+ ? [{ id: entry.name, session_files: files, stage: "subagent" }]
85
+ : [];
86
+ });
145
87
  }
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;
88
+ catch {
89
+ discoveredCommands = [];
90
+ }
91
+ return [...commands, ...discoveredCommands].flatMap((command) => (Array.isArray(command.session_files) ? command.session_files : [])
92
+ .filter((file) => typeof file === "string")
93
+ .flatMap((file) => {
94
+ const sessionPath = ownedSessionPath(stateDir, file);
95
+ if (!sessionPath)
96
+ return [];
97
+ const session = SessionEvidence.readSessionEvidence(sessionPath);
98
+ return session.turns.map((turn) => ({
99
+ ...turn,
100
+ commandId: String(command.id ?? "unknown"),
101
+ diagnostics: session.diagnostics.map((item) => `${item.line === undefined ? "" : `line ${item.line}: `}${item.message}`),
102
+ ...(typeof command.prompt_bytes === "number"
103
+ ? { promptBytes: command.prompt_bytes }
104
+ : {}),
105
+ ...(typeof command.prompt_file === "string"
106
+ ? { promptFile: command.prompt_file }
107
+ : {}),
108
+ ...(command.recipe_context !== undefined
109
+ ? { recipeContext: command.recipe_context }
110
+ : {}),
111
+ sessionFile: file,
112
+ sessionTruncated: session.truncated,
113
+ ...(typeof command.stage === "string" ? { stage: command.stage } : {}),
114
+ }));
115
+ })).map((turn, index) => ({ ...turn, index: index + 1 }));
169
116
  }
170
117
  function asRecord(value) {
171
118
  return value && typeof value === "object" && !Array.isArray(value)
@@ -178,7 +125,8 @@ function readJsonLines(file) {
178
125
  function previewValue(value, maxLength = Limits.INSPECTOR_BODY_PREVIEW_CHARS) {
179
126
  if (value === undefined)
180
127
  return undefined;
181
- const text = typeof value === "string" ? value : JSON.stringify(value);
128
+ const redacted = SessionEvidence.redactSessionEvidenceValue(value);
129
+ const text = typeof redacted === "string" ? redacted : JSON.stringify(redacted);
182
130
  const compact = text.replaceAll(/\s+/g, " ").trim();
183
131
  if (!compact)
184
132
  return undefined;
@@ -393,6 +341,8 @@ export function readActorInspectorPreviews(stateRoot = Paths.getRunStateRoot(),
393
341
  const stateDir = path.join(stateRoot, entry.name);
394
342
  if (!matchesOwner(stateDir, options.ownerId))
395
343
  return [];
344
+ if (options.run && entry.name !== options.run)
345
+ return [];
396
346
  return [
397
347
  ...readRoomPreviews(entry.name, stateDir),
398
348
  ...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;
@@ -3,6 +3,7 @@
3
3
  * Zones: async runtime, ambient UI, diagnostics
4
4
  * Owns ambient summaries, terminal events, and run outbox delivery for detached command-template runs
5
5
  */
6
+ import { type FSWatcher } from "node:fs";
6
7
  import * as AsyncRuns from "./async-runs.ts";
7
8
  export type RunObservedStatus = "running" | "done" | "failed" | "exited" | "cancelled" | "killed";
8
9
  export type RunOutboxDelivery = "log" | "notify" | "followup";
@@ -58,9 +59,19 @@ export interface RunUiNotificationSink {
58
59
  }): void;
59
60
  }
60
61
  export declare function createRunUiObservationState(): RunUiObservationState;
61
- export declare function readRunUiSnapshot(state: RunUiObservationState, ownerId: string): RunUiSnapshot;
62
+ export declare function readRunUiSnapshot(state: RunUiObservationState, ownerId: string, options?: {
63
+ includeOutbox?: boolean;
64
+ stateRoot?: string;
65
+ }): RunUiSnapshot;
62
66
  export declare function pruneRunUiObservationState(state: RunUiObservationState, snapshot: Pick<RunUiSnapshot, "summary" | "transitions">): void;
63
- export declare function deliverRunTransitionNotifications(transitions: RunTransition[], sink: RunUiNotificationSink): void;
67
+ export declare function deliverRunTransitionNotifications(transitions: RunTransition[], sink: RunUiNotificationSink, inFlight?: Set<string>): void;
68
+ export declare function reconcileRunTerminalNotifications(input: {
69
+ inFlight?: Set<string>;
70
+ ownerId: string;
71
+ sink: RunUiNotificationSink;
72
+ state: RunUiObservationState;
73
+ stateRoot?: string;
74
+ }): RunUiSnapshot;
64
75
  export declare function deliverRunOutboxNotifications(events: RunOutboxEvent[], sink: RunUiNotificationSink): void;
65
76
  export interface RunRetirementCandidate {
66
77
  activeSubagents: number;
@@ -76,14 +87,38 @@ export interface RunRetirementExecution {
76
87
  run: string;
77
88
  stateDir: string;
78
89
  }
90
+ export type RunStateWatcherDiagnosticCode = "attach_failed" | "error" | "removed" | "rearmed";
91
+ export interface RunStateWatcherDiagnostic {
92
+ code: RunStateWatcherDiagnosticCode;
93
+ id: number;
94
+ message: string;
95
+ path: string;
96
+ scope: "root" | "run";
97
+ ts: string;
98
+ }
79
99
  export interface RunStateWatcher {
80
100
  close(): void;
101
+ getDiagnostics(): RunStateWatcherDiagnostic[];
81
102
  refresh(): void;
82
103
  }
83
104
  export declare function createRunStateWatcher(input: {
84
- stateRoot?: string;
105
+ exists?: (path: string) => boolean;
106
+ listDirectories?: (path: string) => string[];
85
107
  onChange: () => void;
108
+ stateRoot?: string;
109
+ watchPath?: (path: string, onChange: () => void) => FSWatcher;
86
110
  }): RunStateWatcher;
111
+ export interface RunTerminalReconciliationLoop {
112
+ close(): void;
113
+ reconcileNow(): void;
114
+ start(): void;
115
+ }
116
+ export declare function createRunTerminalReconciliationLoop(input: {
117
+ intervalMs?: number;
118
+ onError?: (error: unknown) => void;
119
+ reconcile: () => void;
120
+ refreshWatcher: () => void;
121
+ }): RunTerminalReconciliationLoop;
87
122
  export interface RunRetirementExecutorOptions {
88
123
  attempted?: Set<string>;
89
124
  cancelRun: (candidate: RunRetirementCandidate) => Record<string, unknown>;