@llblab/pi-actors 0.42.3 → 0.43.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/AGENTS.md +121 -175
- package/CHANGELOG.md +12 -0
- package/README.md +113 -276
- package/dist/fixtures/protocol/control-endpoint.json +6 -0
- package/dist/fixtures/protocol/control-record.json +9 -0
- package/dist/fixtures/protocol/recipe-summary.json +4 -12
- package/dist/fixtures/protocol/trace-event.json +9 -0
- package/dist/lib/async-runs.d.ts +14 -38
- package/dist/lib/async-runs.js +158 -108
- package/dist/lib/control.d.ts +12 -0
- package/dist/lib/control.js +84 -0
- package/dist/lib/execution-sessions.d.ts +17 -0
- package/dist/lib/execution-sessions.js +85 -0
- package/dist/lib/file-state.d.ts +1 -0
- package/dist/lib/file-state.js +17 -5
- package/dist/lib/inspector-actions.d.ts +2 -2
- package/dist/lib/inspector-actions.js +2 -2
- package/dist/lib/inspector-command.js +3 -3
- package/dist/lib/inspector-overlay.d.ts +52 -70
- package/dist/lib/inspector-overlay.js +532 -905
- package/dist/lib/inspector.d.ts +3 -71
- package/dist/lib/inspector.js +19 -665
- package/dist/lib/limits.d.ts +4 -2
- package/dist/lib/limits.js +4 -2
- package/dist/lib/observability.d.ts +16 -16
- package/dist/lib/observability.js +43 -82
- package/dist/lib/prompts.d.ts +1 -1
- package/dist/lib/prompts.js +2 -2
- package/dist/lib/recipe-control.d.ts +7 -0
- package/dist/lib/recipe-control.js +39 -0
- package/dist/lib/recipes-discovery.js +2 -0
- package/dist/lib/recipes-references.d.ts +1 -14
- package/dist/lib/recipes-references.js +6 -21
- package/dist/lib/review-projection.js +1 -5
- package/dist/lib/run-ui-runtime.js +2 -2
- package/dist/lib/runs-control-delivery.d.ts +21 -0
- package/dist/lib/runs-control-delivery.js +127 -0
- package/dist/lib/runs-controls.d.ts +35 -0
- package/dist/lib/runs-controls.js +144 -0
- package/dist/lib/runs-retention.d.ts +7 -0
- package/dist/lib/runs-retention.js +27 -3
- package/dist/lib/runs-start.js +4 -2
- package/dist/lib/runs-status.js +11 -6
- package/dist/lib/runs-trace.d.ts +24 -0
- package/dist/lib/runs-trace.js +98 -0
- package/dist/lib/runtime-notifier.d.ts +1 -1
- package/dist/lib/runtime-notifier.js +1 -1
- package/dist/lib/tools-inspect.d.ts +3 -3
- package/dist/lib/tools-inspect.js +203 -708
- package/dist/lib/tools-local.js +2 -10
- package/dist/lib/tools-message.d.ts +7 -7
- package/dist/lib/tools-message.js +95 -396
- package/dist/lib/tools-response.d.ts +1 -4
- package/dist/lib/tools-response.js +5 -39
- package/dist/lib/tools-spawn.js +16 -28
- package/dist/lib/tools.js +1 -2
- package/dist/lib/trace-projection.d.ts +22 -0
- package/dist/lib/trace-projection.js +165 -0
- package/dist/recipes/draft-review.json +0 -10
- package/dist/recipes/lens-swarm.json +0 -14
- package/dist/recipes/music-player.json +10 -19
- package/dist/recipes/pipeline-architect-coordinator.json +0 -11
- package/dist/recipes/pipeline-artifact-bundle.json +1 -22
- package/dist/recipes/pipeline-artifact-report.json +1 -18
- package/dist/recipes/pipeline-artifact-write.json +1 -18
- package/dist/recipes/pipeline-async-run-ops.json +0 -12
- package/dist/recipes/pipeline-checkpoint-continuation.json +0 -14
- package/dist/recipes/pipeline-development-tasking.json +0 -12
- package/dist/recipes/pipeline-docs-maintenance.json +0 -12
- package/dist/recipes/pipeline-media-library.json +0 -12
- package/dist/recipes/pipeline-quorum-review.json +0 -12
- package/dist/recipes/pipeline-release-readiness.json +0 -12
- package/dist/recipes/pipeline-release-summary.json +0 -12
- package/dist/recipes/pipeline-repo-health.json +0 -12
- package/dist/recipes/pipeline-research-synthesis.json +0 -11
- package/dist/recipes/pipeline-review-readiness.json +0 -12
- package/dist/recipes/resource-locker.json +27 -0
- package/dist/recipes/subagent-artifact.json +0 -9
- package/dist/recipes/subagent-checkpoint.json +0 -10
- package/dist/recipes/subagent-conflict-report.json +0 -11
- package/dist/recipes/subagent-contradiction-map.json +0 -11
- package/dist/recipes/subagent-critic.json +0 -11
- package/dist/recipes/subagent-evidence-map.json +0 -11
- package/dist/recipes/subagent-followup.json +0 -10
- package/dist/recipes/subagent-judge.json +0 -11
- package/dist/recipes/subagent-merge.json +0 -11
- package/dist/recipes/subagent-normalize.json +0 -11
- package/dist/recipes/subagent-plan.json +0 -11
- package/dist/recipes/subagent-preflight.json +0 -11
- package/dist/recipes/subagent-prompt.json +0 -10
- package/dist/recipes/subagent-quorum.json +0 -10
- package/dist/recipes/subagent-review-coordinator.json +0 -14
- package/dist/recipes/subagent-review.json +0 -11
- package/dist/recipes/subagent-task-card.json +0 -11
- package/dist/recipes/subagent-tools.json +0 -10
- package/dist/recipes/subagent-verify.json +0 -11
- package/dist/recipes/subagents-prompts.json +0 -10
- package/dist/recipes/tool-review.json +0 -10
- package/dist/scripts/async-runner.mjs +25 -25
- package/dist/scripts/conformance.mjs +4 -2
- package/dist/scripts/locker.mjs +200 -66
- package/dist/scripts/music-player.mjs +159 -150
- package/dist/scripts/recipe-utils.mjs +6 -96
- package/dist/scripts/release-gates.mjs +60 -0
- package/dist/scripts/validate-recipe.mjs +3 -53
- package/dist/skills/actors/SKILL.md +53 -266
- package/dist/skills/swarm/SKILL.md +11 -33
- package/docs/0.43-baseline.md +44 -0
- package/docs/README.md +3 -3
- package/docs/actor-inspector.md +26 -64
- package/docs/actors-deep-reference.md +92 -50
- package/docs/async-runs.md +81 -328
- package/docs/command-templates.md +2 -2
- package/docs/component-recipes.md +30 -133
- package/docs/recipe-library.md +57 -182
- package/docs/task-first-recipes.md +10 -12
- package/docs/template-recipes.md +76 -289
- package/docs/tool-registry.md +41 -161
- package/fixtures/protocol/control-endpoint.json +6 -0
- package/fixtures/protocol/control-record.json +9 -0
- package/fixtures/protocol/recipe-summary.json +4 -12
- package/fixtures/protocol/trace-event.json +9 -0
- package/lib/async-runs.ts +202 -201
- package/lib/control.ts +102 -0
- package/lib/execution-sessions.ts +111 -0
- package/lib/file-state.ts +17 -4
- package/lib/inspector-actions.ts +2 -2
- package/lib/inspector-command.ts +3 -3
- package/lib/inspector-overlay.ts +577 -1121
- package/lib/inspector.ts +46 -979
- package/lib/limits.ts +4 -2
- package/lib/observability.ts +60 -101
- package/lib/prompts.ts +2 -2
- package/lib/recipe-control.ts +45 -0
- package/lib/recipes-discovery.ts +2 -0
- package/lib/recipes-references.ts +9 -45
- package/lib/review-projection.ts +1 -5
- package/lib/run-ui-runtime.ts +2 -2
- package/lib/runs-control-delivery.ts +181 -0
- package/lib/runs-controls.ts +204 -0
- package/lib/runs-retention.ts +38 -3
- package/lib/runs-start.ts +4 -2
- package/lib/runs-status.ts +11 -6
- package/lib/runs-trace.ts +132 -0
- package/lib/runtime-notifier.ts +1 -1
- package/lib/tools-inspect.ts +240 -901
- package/lib/tools-local.ts +2 -12
- package/lib/tools-message.ts +112 -519
- package/lib/tools-response.ts +5 -52
- package/lib/tools-spawn.ts +16 -32
- package/lib/tools.ts +1 -2
- package/lib/trace-projection.ts +221 -0
- package/package.json +2 -1
- package/recipes/draft-review.json +0 -10
- package/recipes/lens-swarm.json +0 -14
- package/recipes/music-player.json +10 -19
- package/recipes/pipeline-architect-coordinator.json +0 -11
- package/recipes/pipeline-artifact-bundle.json +1 -22
- package/recipes/pipeline-artifact-report.json +1 -18
- package/recipes/pipeline-artifact-write.json +1 -18
- package/recipes/pipeline-async-run-ops.json +0 -12
- package/recipes/pipeline-checkpoint-continuation.json +0 -14
- package/recipes/pipeline-development-tasking.json +0 -12
- package/recipes/pipeline-docs-maintenance.json +0 -12
- package/recipes/pipeline-media-library.json +0 -12
- package/recipes/pipeline-quorum-review.json +0 -12
- package/recipes/pipeline-release-readiness.json +0 -12
- package/recipes/pipeline-release-summary.json +0 -12
- package/recipes/pipeline-repo-health.json +0 -12
- package/recipes/pipeline-research-synthesis.json +0 -11
- package/recipes/pipeline-review-readiness.json +0 -12
- package/recipes/resource-locker.json +27 -0
- package/recipes/subagent-artifact.json +0 -9
- package/recipes/subagent-checkpoint.json +0 -10
- package/recipes/subagent-conflict-report.json +0 -11
- package/recipes/subagent-contradiction-map.json +0 -11
- package/recipes/subagent-critic.json +0 -11
- package/recipes/subagent-evidence-map.json +0 -11
- package/recipes/subagent-followup.json +0 -10
- package/recipes/subagent-judge.json +0 -11
- package/recipes/subagent-merge.json +0 -11
- package/recipes/subagent-normalize.json +0 -11
- package/recipes/subagent-plan.json +0 -11
- package/recipes/subagent-preflight.json +0 -11
- package/recipes/subagent-prompt.json +0 -10
- package/recipes/subagent-quorum.json +0 -10
- package/recipes/subagent-review-coordinator.json +0 -14
- package/recipes/subagent-review.json +0 -11
- package/recipes/subagent-task-card.json +0 -11
- package/recipes/subagent-tools.json +0 -10
- package/recipes/subagent-verify.json +0 -11
- package/recipes/subagents-prompts.json +0 -10
- package/recipes/tool-review.json +0 -10
- package/scripts/async-runner.mjs +25 -25
- package/scripts/conformance.mjs +4 -2
- package/scripts/locker.mjs +200 -66
- package/scripts/music-player.mjs +159 -150
- package/scripts/recipe-utils.mjs +6 -96
- package/scripts/release-gates.mjs +60 -0
- package/scripts/validate-recipe.mjs +3 -53
- package/skills/actors/SKILL.md +53 -266
- package/skills/swarm/SKILL.md +11 -33
- package/dist/fixtures/protocol/actor-message-branch.json +0 -13
- package/dist/fixtures/protocol/mailbox-contract.json +0 -15
- package/dist/fixtures/protocol/room-message.json +0 -11
- package/dist/fixtures/protocol/room-roster.json +0 -11
- package/dist/fixtures/protocol/run-inbox-message.json +0 -9
- package/dist/fixtures/protocol/run-outbox-event.json +0 -9
- package/dist/lib/mailbox-loop.d.ts +0 -41
- package/dist/lib/mailbox-loop.js +0 -60
- package/dist/lib/messages.d.ts +0 -25
- package/dist/lib/messages.js +0 -122
- package/dist/lib/rooms.d.ts +0 -104
- package/dist/lib/rooms.js +0 -647
- package/dist/lib/runs-mailbox.d.ts +0 -25
- package/dist/lib/runs-mailbox.js +0 -146
- package/dist/lib/runs-messages.d.ts +0 -15
- package/dist/lib/runs-messages.js +0 -179
- package/dist/lib/runs-outbox.d.ts +0 -41
- package/dist/lib/runs-outbox.js +0 -87
- package/dist/lib/tools-mailbox.d.ts +0 -8
- package/dist/lib/tools-mailbox.js +0 -48
- package/dist/recipes/actor-worker.json +0 -39
- package/dist/recipes/coordinator-locker.json +0 -45
- package/dist/recipes/locker.json +0 -45
- package/dist/recipes/pipeline-room-swarm.json +0 -50
- package/dist/recipes/subagent-message.json +0 -32
- package/dist/recipes/utility-actor-message.json +0 -23
- package/dist/scripts/actor-worker.mjs +0 -214
- package/dist/scripts/coordinator.mjs +0 -799
- package/docs/actor-messages.md +0 -225
- package/fixtures/protocol/actor-message-branch.json +0 -13
- package/fixtures/protocol/mailbox-contract.json +0 -15
- package/fixtures/protocol/room-message.json +0 -11
- package/fixtures/protocol/room-roster.json +0 -11
- package/fixtures/protocol/run-inbox-message.json +0 -9
- package/fixtures/protocol/run-outbox-event.json +0 -9
- package/lib/mailbox-loop.ts +0 -144
- package/lib/messages.ts +0 -151
- package/lib/rooms.ts +0 -939
- package/lib/runs-mailbox.ts +0 -208
- package/lib/runs-messages.ts +0 -252
- package/lib/runs-outbox.ts +0 -144
- package/lib/tools-mailbox.ts +0 -56
- package/recipes/actor-worker.json +0 -39
- package/recipes/coordinator-locker.json +0 -45
- package/recipes/locker.json +0 -45
- package/recipes/pipeline-room-swarm.json +0 -50
- package/recipes/subagent-message.json +0 -32
- package/recipes/utility-actor-message.json +0 -23
- package/scripts/actor-worker.mjs +0 -214
- package/scripts/coordinator.mjs +0 -799
package/lib/inspector.ts
CHANGED
|
@@ -1,76 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Inspector
|
|
3
|
-
* Zones:
|
|
4
|
-
* Owns
|
|
2
|
+
* Actor Inspector evidence readers.
|
|
3
|
+
* Zones: owned actor-instance inventory and captured Recipe projection
|
|
4
|
+
* Owns actor-instance TUI evidence; Trace, Control, and execution parsing stay in their domains.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import * as fs from "node:fs";
|
|
8
8
|
import * as path from "node:path";
|
|
9
9
|
|
|
10
|
-
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
11
|
-
|
|
12
|
-
import type { ActorMessage } from "./messages.ts";
|
|
13
|
-
import * as Limits from "./limits.ts";
|
|
14
|
-
import * as Paths from "./paths.ts";
|
|
15
10
|
import * as SessionEvidence from "./session-evidence.ts";
|
|
16
|
-
import {
|
|
17
|
-
readJsonFileResilient,
|
|
18
|
-
readJsonlFileResilient,
|
|
19
|
-
} from "./state-readers.ts";
|
|
20
|
-
|
|
21
|
-
export interface ActorInspectorPreview {
|
|
22
|
-
body_preview?: string;
|
|
23
|
-
branch?: string;
|
|
24
|
-
channel: "broadcast" | "direct" | "room";
|
|
25
|
-
event_id?: string;
|
|
26
|
-
from?: string;
|
|
27
|
-
from_display?: string;
|
|
28
|
-
inbox_status?: string;
|
|
29
|
-
message_id?: string;
|
|
30
|
-
needs_response?: boolean;
|
|
31
|
-
run: string;
|
|
32
|
-
sequence?: number;
|
|
33
|
-
summary?: string;
|
|
34
|
-
stripe?: boolean;
|
|
35
|
-
timestamp: string;
|
|
36
|
-
to: string;
|
|
37
|
-
type: string;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export interface ActorInspectorWidgetStyle {
|
|
41
|
-
actor?: (text: string) => string;
|
|
42
|
-
muted?: (text: string) => string;
|
|
43
|
-
preview?: (text: string) => string;
|
|
44
|
-
stripe?: (text: string) => string;
|
|
45
|
-
stripeAlt?: (text: string) => string;
|
|
46
|
-
target?: (text: string) => string;
|
|
47
|
-
type?: (text: string) => string;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export interface ActorInspectorRenderOptions {}
|
|
51
|
-
|
|
52
|
-
export interface ActorInspectorItemViewOptions {
|
|
53
|
-
sequence: number;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export interface ActorInspectorRosterMember {
|
|
57
|
-
address: string;
|
|
58
|
-
display?: string;
|
|
59
|
-
role?: string;
|
|
60
|
-
status?: string;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export interface ActorInspectorPreviewReadOptions {
|
|
64
|
-
ownerId?: string;
|
|
65
|
-
branch?: string;
|
|
66
|
-
currentRunOnly?: boolean;
|
|
67
|
-
run?: string;
|
|
68
|
-
channels?: ActorInspectorPreview["channel"][];
|
|
69
|
-
mention?: string;
|
|
70
|
-
readKeys?: Iterable<string>;
|
|
71
|
-
roomLimitPerRun?: number;
|
|
72
|
-
unreadOnly?: boolean;
|
|
73
|
-
}
|
|
11
|
+
import { readJsonFileResilient } from "./state-readers.ts";
|
|
74
12
|
|
|
75
13
|
export interface ActorInspectorRunItem {
|
|
76
14
|
run: string;
|
|
@@ -79,6 +17,14 @@ export interface ActorInspectorRunItem {
|
|
|
79
17
|
updatedAt?: string;
|
|
80
18
|
}
|
|
81
19
|
|
|
20
|
+
function matchesOwner(stateDir: string, ownerId: string): boolean {
|
|
21
|
+
const meta = readJsonFileResilient<Record<string, unknown>>(
|
|
22
|
+
path.join(stateDir, "run.json"),
|
|
23
|
+
{},
|
|
24
|
+
).value;
|
|
25
|
+
return meta.ownerId === ownerId;
|
|
26
|
+
}
|
|
27
|
+
|
|
82
28
|
export function readActorInspectorRuns(
|
|
83
29
|
stateRoot: string,
|
|
84
30
|
ownerId: string,
|
|
@@ -107,7 +53,10 @@ export function readActorInspectorRuns(
|
|
|
107
53
|
...(typeof runMeta.run_instance_id === "string"
|
|
108
54
|
? { runInstanceId: runMeta.run_instance_id }
|
|
109
55
|
: {}),
|
|
110
|
-
status: String(
|
|
56
|
+
status: String(
|
|
57
|
+
progress.phase ??
|
|
58
|
+
(Object.keys(result).length ? "terminal" : "unknown"),
|
|
59
|
+
),
|
|
111
60
|
...(typeof progress.updatedAt === "string"
|
|
112
61
|
? { updatedAt: progress.updatedAt }
|
|
113
62
|
: typeof result.completedAt === "string"
|
|
@@ -115,14 +64,11 @@ export function readActorInspectorRuns(
|
|
|
115
64
|
: {}),
|
|
116
65
|
}];
|
|
117
66
|
})
|
|
118
|
-
.sort((left, right) =>
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
});
|
|
124
|
-
} catch (error) {
|
|
125
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
|
|
67
|
+
.sort((left, right) =>
|
|
68
|
+
String(right.updatedAt ?? "").localeCompare(String(left.updatedAt ?? "")) ||
|
|
69
|
+
right.run.localeCompare(left.run),
|
|
70
|
+
);
|
|
71
|
+
} catch {
|
|
126
72
|
return [];
|
|
127
73
|
}
|
|
128
74
|
}
|
|
@@ -144,33 +90,35 @@ export function readActorInspectorRecipe(
|
|
|
144
90
|
);
|
|
145
91
|
const meta = result.value;
|
|
146
92
|
const contexts = Array.isArray(meta.recipe_context_records)
|
|
147
|
-
? meta.recipe_context_records
|
|
148
|
-
|
|
93
|
+
? meta.recipe_context_records.filter(
|
|
94
|
+
(entry): entry is Record<string, unknown> =>
|
|
149
95
|
Boolean(entry && typeof entry === "object" && !Array.isArray(entry)),
|
|
150
|
-
|
|
96
|
+
)
|
|
151
97
|
: [];
|
|
152
98
|
const primary = contexts.find((entry) => entry.depth === 0) ?? contexts[0];
|
|
153
|
-
const definition =
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
99
|
+
const definition =
|
|
100
|
+
primary?.recipe &&
|
|
101
|
+
typeof primary.recipe === "object" &&
|
|
102
|
+
!Array.isArray(primary.recipe)
|
|
103
|
+
? (primary.recipe as Record<string, unknown>)
|
|
104
|
+
: undefined;
|
|
159
105
|
const redactedRecord = (value: Record<string, unknown>): Record<string, unknown> =>
|
|
160
|
-
|
|
161
|
-
Object.
|
|
162
|
-
|
|
106
|
+
SessionEvidence.redactSessionEvidenceValue(
|
|
107
|
+
Object.fromEntries(
|
|
108
|
+
Object.entries(value).filter(([, entry]) => entry !== undefined),
|
|
109
|
+
),
|
|
110
|
+
) as Record<string, unknown>;
|
|
163
111
|
return {
|
|
164
|
-
composition: contexts.map((entry) =>
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
112
|
+
composition: contexts.map((entry) =>
|
|
113
|
+
redactedRecord({
|
|
114
|
+
...(typeof entry.alias === "string" ? { alias: entry.alias } : {}),
|
|
115
|
+
depth: entry.depth,
|
|
116
|
+
file: entry.file,
|
|
117
|
+
import_path: entry.import_path,
|
|
118
|
+
name: entry.name,
|
|
119
|
+
}),
|
|
120
|
+
),
|
|
121
|
+
...(definition ? { definition: redactedRecord(definition) } : {}),
|
|
174
122
|
diagnostics: result.diagnostics.map((diagnostic) => diagnostic.message),
|
|
175
123
|
identity: redactedRecord({
|
|
176
124
|
run: meta.run,
|
|
@@ -183,891 +131,10 @@ export function readActorInspectorRecipe(
|
|
|
183
131
|
template: meta.template,
|
|
184
132
|
values: meta.values,
|
|
185
133
|
model_policy: meta.model_policy,
|
|
186
|
-
|
|
134
|
+
control: meta.control,
|
|
187
135
|
artifacts: meta.artifacts,
|
|
188
136
|
notification_policy: meta.notification_policy,
|
|
189
137
|
retire_when: meta.retire_when,
|
|
190
138
|
}),
|
|
191
139
|
};
|
|
192
140
|
}
|
|
193
|
-
|
|
194
|
-
export interface ActorInspectorTurnItem extends SessionEvidence.SessionEvidenceTurn {
|
|
195
|
-
commandId: string;
|
|
196
|
-
diagnostics: string[];
|
|
197
|
-
promptBytes?: number;
|
|
198
|
-
promptFile?: string;
|
|
199
|
-
recipeContext?: unknown;
|
|
200
|
-
sessionFile: string;
|
|
201
|
-
sessionTruncated: boolean;
|
|
202
|
-
stage?: string;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function ownedSessionPath(
|
|
206
|
-
stateDir: string,
|
|
207
|
-
sessionFile: string,
|
|
208
|
-
): string | undefined {
|
|
209
|
-
if (path.isAbsolute(sessionFile)) return undefined;
|
|
210
|
-
const sessionsRoot = path.resolve(stateDir, "sessions");
|
|
211
|
-
const resolved = path.resolve(stateDir, sessionFile);
|
|
212
|
-
if (!resolved.startsWith(`${sessionsRoot}${path.sep}`)) return undefined;
|
|
213
|
-
try {
|
|
214
|
-
const canonicalRoot = fs.realpathSync(sessionsRoot);
|
|
215
|
-
const canonicalFile = fs.realpathSync(resolved);
|
|
216
|
-
return canonicalFile.startsWith(`${canonicalRoot}${path.sep}`)
|
|
217
|
-
? canonicalFile
|
|
218
|
-
: undefined;
|
|
219
|
-
} catch {
|
|
220
|
-
return undefined;
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
export function readActorInspectorTurns(stateDir: string): ActorInspectorTurnItem[] {
|
|
225
|
-
const evidence = readJsonFileResilient<Record<string, unknown>>(
|
|
226
|
-
path.join(stateDir, "review-evidence.json"),
|
|
227
|
-
{},
|
|
228
|
-
).value;
|
|
229
|
-
const commands = Array.isArray(evidence.commands)
|
|
230
|
-
? (evidence.commands as Array<Record<string, unknown>>)
|
|
231
|
-
: [];
|
|
232
|
-
const recordedFiles = new Set(
|
|
233
|
-
commands.flatMap((command) =>
|
|
234
|
-
(Array.isArray(command.session_files) ? command.session_files : []).filter(
|
|
235
|
-
(file): file is string => typeof file === "string",
|
|
236
|
-
),
|
|
237
|
-
),
|
|
238
|
-
);
|
|
239
|
-
let discoveredCommands: Array<Record<string, unknown>> = [];
|
|
240
|
-
try {
|
|
241
|
-
const sessionsDir = path.join(stateDir, "sessions");
|
|
242
|
-
discoveredCommands = fs
|
|
243
|
-
.readdirSync(sessionsDir, { withFileTypes: true })
|
|
244
|
-
.filter((entry) => entry.isDirectory())
|
|
245
|
-
.flatMap((entry) => {
|
|
246
|
-
const files = fs
|
|
247
|
-
.readdirSync(path.join(sessionsDir, entry.name), {
|
|
248
|
-
withFileTypes: true,
|
|
249
|
-
})
|
|
250
|
-
.filter((file) => file.isFile() && file.name.endsWith(".jsonl"))
|
|
251
|
-
.map((file) => path.posix.join("sessions", entry.name, file.name))
|
|
252
|
-
.filter((file) => !recordedFiles.has(file));
|
|
253
|
-
return files.length > 0
|
|
254
|
-
? [{ id: entry.name, session_files: files, stage: "subagent" }]
|
|
255
|
-
: [];
|
|
256
|
-
});
|
|
257
|
-
} catch {
|
|
258
|
-
discoveredCommands = [];
|
|
259
|
-
}
|
|
260
|
-
return [...commands, ...discoveredCommands].flatMap((command) =>
|
|
261
|
-
(Array.isArray(command.session_files) ? command.session_files : [])
|
|
262
|
-
.filter((file): file is string => typeof file === "string")
|
|
263
|
-
.flatMap((file) => {
|
|
264
|
-
const sessionPath = ownedSessionPath(stateDir, file);
|
|
265
|
-
if (!sessionPath) return [];
|
|
266
|
-
const session = SessionEvidence.readSessionEvidence(sessionPath);
|
|
267
|
-
return session.turns.map((turn) => ({
|
|
268
|
-
...turn,
|
|
269
|
-
commandId: String(command.id ?? "unknown"),
|
|
270
|
-
diagnostics: session.diagnostics.map((item) =>
|
|
271
|
-
`${item.line === undefined ? "" : `line ${item.line}: `}${item.message}`,
|
|
272
|
-
),
|
|
273
|
-
...(typeof command.prompt_bytes === "number"
|
|
274
|
-
? { promptBytes: command.prompt_bytes }
|
|
275
|
-
: {}),
|
|
276
|
-
...(typeof command.prompt_file === "string"
|
|
277
|
-
? { promptFile: command.prompt_file }
|
|
278
|
-
: {}),
|
|
279
|
-
...(command.recipe_context !== undefined
|
|
280
|
-
? { recipeContext: command.recipe_context }
|
|
281
|
-
: {}),
|
|
282
|
-
sessionFile: file,
|
|
283
|
-
sessionTruncated: session.truncated,
|
|
284
|
-
...(typeof command.stage === "string" ? { stage: command.stage } : {}),
|
|
285
|
-
}));
|
|
286
|
-
}),
|
|
287
|
-
).map((turn, index) => ({ ...turn, index: index + 1 }));
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
function asRecord(value: unknown): Record<string, unknown> {
|
|
291
|
-
return value && typeof value === "object" && !Array.isArray(value)
|
|
292
|
-
? (value as Record<string, unknown>)
|
|
293
|
-
: {};
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
function readJsonLines(file: string): Record<string, unknown>[] {
|
|
297
|
-
return readJsonlFileResilient<Record<string, unknown>>(file).records;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
function previewValue(
|
|
301
|
-
value: unknown,
|
|
302
|
-
maxLength = Limits.INSPECTOR_BODY_PREVIEW_CHARS,
|
|
303
|
-
): string | undefined {
|
|
304
|
-
if (value === undefined) return undefined;
|
|
305
|
-
const redacted = SessionEvidence.redactSessionEvidenceValue(value);
|
|
306
|
-
const text =
|
|
307
|
-
typeof redacted === "string" ? redacted : JSON.stringify(redacted);
|
|
308
|
-
const compact = text.replaceAll(/\s+/g, " ").trim();
|
|
309
|
-
if (!compact) return undefined;
|
|
310
|
-
return compact.length > maxLength
|
|
311
|
-
? `${compact.slice(0, Math.max(0, maxLength - 1))}…`
|
|
312
|
-
: compact;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
function channelFor(
|
|
316
|
-
message: Pick<ActorMessage, "to">,
|
|
317
|
-
): ActorInspectorPreview["channel"] {
|
|
318
|
-
if (message.to.startsWith("room:")) return "room";
|
|
319
|
-
if (message.to === "coordinator" || message.to.startsWith("session:"))
|
|
320
|
-
return "broadcast";
|
|
321
|
-
return "direct";
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
function previewFromMessage(
|
|
325
|
-
run: string,
|
|
326
|
-
message: Record<string, unknown>,
|
|
327
|
-
timestamp: string,
|
|
328
|
-
displayNames: Record<string, string> = {},
|
|
329
|
-
): ActorInspectorPreview | undefined {
|
|
330
|
-
const to = typeof message.to === "string" ? message.to : undefined;
|
|
331
|
-
const type = typeof message.type === "string" ? message.type : undefined;
|
|
332
|
-
if (!to || !type) return undefined;
|
|
333
|
-
const from = typeof message.from === "string" ? message.from : undefined;
|
|
334
|
-
const summary =
|
|
335
|
-
typeof message.summary === "string" ? message.summary : undefined;
|
|
336
|
-
const body = asRecord(message.body);
|
|
337
|
-
const metadata = asRecord(message.metadata);
|
|
338
|
-
const eventId =
|
|
339
|
-
typeof message.id === "string" && message.id.trim()
|
|
340
|
-
? message.id.trim()
|
|
341
|
-
: undefined;
|
|
342
|
-
const display = from
|
|
343
|
-
? typeof body.display === "string" && body.display.trim()
|
|
344
|
-
? body.display.trim()
|
|
345
|
-
: displayNames[from]
|
|
346
|
-
: undefined;
|
|
347
|
-
return {
|
|
348
|
-
...(previewValue(message.body)
|
|
349
|
-
? { body_preview: previewValue(message.body) }
|
|
350
|
-
: {}),
|
|
351
|
-
channel: channelFor({ to }),
|
|
352
|
-
...(eventId ? { event_id: eventId } : {}),
|
|
353
|
-
...(from ? { from } : {}),
|
|
354
|
-
...(display ? { from_display: display } : {}),
|
|
355
|
-
...(metadata.requires_response === true ? { needs_response: true } : {}),
|
|
356
|
-
run,
|
|
357
|
-
...(summary ? { summary } : {}),
|
|
358
|
-
timestamp,
|
|
359
|
-
to,
|
|
360
|
-
type,
|
|
361
|
-
};
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
function readRoomRosterRecords(
|
|
365
|
-
stateDir: string,
|
|
366
|
-
room: string,
|
|
367
|
-
): Record<string, Record<string, unknown>> {
|
|
368
|
-
return readJsonFileResilient<Record<string, Record<string, unknown>>>(
|
|
369
|
-
path.join(stateDir, "rooms", room, "roster.json"),
|
|
370
|
-
{},
|
|
371
|
-
).value;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
function memberDisplay(
|
|
375
|
-
_address: string,
|
|
376
|
-
member: Record<string, unknown>,
|
|
377
|
-
): string | undefined {
|
|
378
|
-
const display =
|
|
379
|
-
typeof member.display === "string" ? member.display.trim() : "";
|
|
380
|
-
return display || undefined;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
function readRoomDisplayNames(
|
|
384
|
-
stateDir: string,
|
|
385
|
-
room: string,
|
|
386
|
-
): Record<string, string> {
|
|
387
|
-
const roster = readRoomRosterRecords(stateDir, room);
|
|
388
|
-
return Object.fromEntries(
|
|
389
|
-
Object.entries(roster).flatMap(([address, member]) => {
|
|
390
|
-
const display = memberDisplay(address, member);
|
|
391
|
-
return display ? [[address, display]] : [];
|
|
392
|
-
}),
|
|
393
|
-
);
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
function readRoomPreviews(
|
|
397
|
-
run: string,
|
|
398
|
-
stateDir: string,
|
|
399
|
-
): ActorInspectorPreview[] {
|
|
400
|
-
const roomsDir = path.join(stateDir, "rooms");
|
|
401
|
-
try {
|
|
402
|
-
const previews = fs
|
|
403
|
-
.readdirSync(roomsDir, { withFileTypes: true })
|
|
404
|
-
.filter((entry) => entry.isDirectory())
|
|
405
|
-
.flatMap((entry) => {
|
|
406
|
-
const displayNames = readRoomDisplayNames(stateDir, entry.name);
|
|
407
|
-
return readJsonLines(path.join(roomsDir, entry.name, "messages.jsonl"))
|
|
408
|
-
.map((message) =>
|
|
409
|
-
previewFromMessage(
|
|
410
|
-
run,
|
|
411
|
-
message,
|
|
412
|
-
String(message.received_at ?? message.timestamp ?? ""),
|
|
413
|
-
displayNames,
|
|
414
|
-
),
|
|
415
|
-
)
|
|
416
|
-
.filter((preview): preview is ActorInspectorPreview =>
|
|
417
|
-
Boolean(preview),
|
|
418
|
-
);
|
|
419
|
-
});
|
|
420
|
-
return previews;
|
|
421
|
-
} catch (error) {
|
|
422
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
|
|
423
|
-
return [];
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
function readInboxPreviews(
|
|
428
|
-
run: string,
|
|
429
|
-
stateDir: string,
|
|
430
|
-
): ActorInspectorPreview[] {
|
|
431
|
-
return readJsonLines(path.join(stateDir, "inbox.jsonl"))
|
|
432
|
-
.map((message) =>
|
|
433
|
-
previewFromMessage(
|
|
434
|
-
run,
|
|
435
|
-
message,
|
|
436
|
-
String(message.received_at ?? message.timestamp ?? ""),
|
|
437
|
-
),
|
|
438
|
-
)
|
|
439
|
-
.filter((preview): preview is ActorInspectorPreview => Boolean(preview));
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
function readBranchInboxPreviews(
|
|
443
|
-
run: string,
|
|
444
|
-
stateDir: string,
|
|
445
|
-
): ActorInspectorPreview[] {
|
|
446
|
-
const branchesDir = path.join(stateDir, "branches");
|
|
447
|
-
try {
|
|
448
|
-
return fs
|
|
449
|
-
.readdirSync(branchesDir, { withFileTypes: true })
|
|
450
|
-
.filter((entry) => entry.isDirectory())
|
|
451
|
-
.flatMap((entry) =>
|
|
452
|
-
readJsonLines(path.join(branchesDir, entry.name, "inbox.jsonl"))
|
|
453
|
-
.map((message): ActorInspectorPreview | undefined => {
|
|
454
|
-
const preview = previewFromMessage(
|
|
455
|
-
run,
|
|
456
|
-
message,
|
|
457
|
-
String(
|
|
458
|
-
message.queued_at ??
|
|
459
|
-
message.received_at ??
|
|
460
|
-
message.timestamp ??
|
|
461
|
-
"",
|
|
462
|
-
),
|
|
463
|
-
);
|
|
464
|
-
if (!preview) return undefined;
|
|
465
|
-
return {
|
|
466
|
-
...preview,
|
|
467
|
-
branch: entry.name,
|
|
468
|
-
...(typeof message.id === "string"
|
|
469
|
-
? { message_id: message.id }
|
|
470
|
-
: {}),
|
|
471
|
-
...(typeof message.status === "string"
|
|
472
|
-
? { inbox_status: message.status }
|
|
473
|
-
: {}),
|
|
474
|
-
};
|
|
475
|
-
})
|
|
476
|
-
.filter((preview): preview is ActorInspectorPreview =>
|
|
477
|
-
Boolean(preview),
|
|
478
|
-
),
|
|
479
|
-
);
|
|
480
|
-
} catch (error) {
|
|
481
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
|
|
482
|
-
return [];
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
function readOutboxPreviews(
|
|
487
|
-
run: string,
|
|
488
|
-
stateDir: string,
|
|
489
|
-
): ActorInspectorPreview[] {
|
|
490
|
-
return readJsonLines(path.join(stateDir, "outbox.jsonl"))
|
|
491
|
-
.map((event) => {
|
|
492
|
-
const message = asRecord(event.message ?? event);
|
|
493
|
-
return previewFromMessage(
|
|
494
|
-
run,
|
|
495
|
-
message,
|
|
496
|
-
String(event.timestamp ?? event.created_at ?? event.emitted_at ?? ""),
|
|
497
|
-
);
|
|
498
|
-
})
|
|
499
|
-
.filter((preview): preview is ActorInspectorPreview => Boolean(preview));
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
function getRunOwnerId(stateDir: string): string | undefined {
|
|
503
|
-
const meta = readJsonFileResilient<Record<string, unknown> | undefined>(
|
|
504
|
-
path.join(stateDir, "run.json"),
|
|
505
|
-
undefined,
|
|
506
|
-
).value;
|
|
507
|
-
return typeof meta?.ownerId === "string" ? meta.ownerId : undefined;
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
function matchesOwner(stateDir: string, ownerId: string | undefined): boolean {
|
|
511
|
-
return ownerId === undefined || getRunOwnerId(stateDir) === ownerId;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
export function inspectorPreviewReadKey(
|
|
515
|
-
preview: ActorInspectorPreview,
|
|
516
|
-
): string {
|
|
517
|
-
return [
|
|
518
|
-
preview.channel,
|
|
519
|
-
preview.run,
|
|
520
|
-
preview.branch ?? "",
|
|
521
|
-
preview.event_id ?? preview.message_id ?? preview.timestamp,
|
|
522
|
-
preview.from ?? "",
|
|
523
|
-
preview.to,
|
|
524
|
-
preview.type,
|
|
525
|
-
].join("|");
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
function isUnreadPreview(
|
|
529
|
-
preview: ActorInspectorPreview,
|
|
530
|
-
readKeys?: Set<string>,
|
|
531
|
-
): boolean {
|
|
532
|
-
if (readKeys?.has(inspectorPreviewReadKey(preview))) return false;
|
|
533
|
-
return (
|
|
534
|
-
preview.inbox_status === "queued" ||
|
|
535
|
-
(preview.inbox_status === undefined && preview.branch !== undefined)
|
|
536
|
-
);
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
function matchesBranchFilter(
|
|
540
|
-
preview: ActorInspectorPreview,
|
|
541
|
-
branch: string | undefined,
|
|
542
|
-
): boolean {
|
|
543
|
-
const name = branch?.trim();
|
|
544
|
-
if (!name) return true;
|
|
545
|
-
if (preview.branch !== undefined) return preview.branch === name;
|
|
546
|
-
const address = `branch:${preview.run}/${name}`;
|
|
547
|
-
return preview.from === address || preview.to === address;
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
function matchesPreviewFilter(
|
|
551
|
-
preview: ActorInspectorPreview,
|
|
552
|
-
options: ActorInspectorPreviewReadOptions,
|
|
553
|
-
): boolean {
|
|
554
|
-
if (options.channels?.length && !options.channels.includes(preview.channel)) {
|
|
555
|
-
return false;
|
|
556
|
-
}
|
|
557
|
-
const readKeys = options.readKeys ? new Set(options.readKeys) : undefined;
|
|
558
|
-
if (options.unreadOnly && !isUnreadPreview(preview, readKeys)) return false;
|
|
559
|
-
if (!matchesBranchFilter(preview, options.branch)) return false;
|
|
560
|
-
const mention = options.mention?.trim().toLowerCase();
|
|
561
|
-
if (!mention) return true;
|
|
562
|
-
return [
|
|
563
|
-
preview.branch,
|
|
564
|
-
preview.from,
|
|
565
|
-
preview.from_display,
|
|
566
|
-
preview.inbox_status,
|
|
567
|
-
preview.to,
|
|
568
|
-
preview.type,
|
|
569
|
-
preview.summary,
|
|
570
|
-
preview.body_preview,
|
|
571
|
-
].some((value) => value?.toLowerCase().includes(mention));
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
function limitRoomPreviewsPerRun(
|
|
575
|
-
previews: ActorInspectorPreview[],
|
|
576
|
-
limitPerRun?: number,
|
|
577
|
-
): ActorInspectorPreview[] {
|
|
578
|
-
if (!Number.isFinite(limitPerRun)) return previews;
|
|
579
|
-
const limit = Math.max(0, Number(limitPerRun));
|
|
580
|
-
const remainingByRun = new Map<string, number>();
|
|
581
|
-
const keep = new Set<ActorInspectorPreview>();
|
|
582
|
-
for (let index = previews.length - 1; index >= 0; index -= 1) {
|
|
583
|
-
const preview = previews[index];
|
|
584
|
-
if (preview.channel !== "room") {
|
|
585
|
-
keep.add(preview);
|
|
586
|
-
continue;
|
|
587
|
-
}
|
|
588
|
-
const remaining = remainingByRun.get(preview.run) ?? limit;
|
|
589
|
-
if (remaining <= 0) {
|
|
590
|
-
remainingByRun.set(preview.run, 0);
|
|
591
|
-
continue;
|
|
592
|
-
}
|
|
593
|
-
keep.add(preview);
|
|
594
|
-
remainingByRun.set(preview.run, remaining - 1);
|
|
595
|
-
}
|
|
596
|
-
return previews.filter((preview) => keep.has(preview));
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
export function readActorInspectorPreviews(
|
|
600
|
-
stateRoot = Paths.getRunStateRoot(),
|
|
601
|
-
limit = 8,
|
|
602
|
-
options: ActorInspectorPreviewReadOptions = {},
|
|
603
|
-
): ActorInspectorPreview[] {
|
|
604
|
-
try {
|
|
605
|
-
const previews = fs
|
|
606
|
-
.readdirSync(stateRoot, { withFileTypes: true })
|
|
607
|
-
.filter((entry) => entry.isDirectory())
|
|
608
|
-
.flatMap((entry) => {
|
|
609
|
-
const stateDir = path.join(stateRoot, entry.name);
|
|
610
|
-
if (!matchesOwner(stateDir, options.ownerId)) return [];
|
|
611
|
-
if (options.run && entry.name !== options.run) return [];
|
|
612
|
-
return [
|
|
613
|
-
...readRoomPreviews(entry.name, stateDir),
|
|
614
|
-
...readInboxPreviews(entry.name, stateDir),
|
|
615
|
-
...readBranchInboxPreviews(entry.name, stateDir),
|
|
616
|
-
...readOutboxPreviews(entry.name, stateDir),
|
|
617
|
-
];
|
|
618
|
-
})
|
|
619
|
-
.filter((preview) => preview.timestamp)
|
|
620
|
-
.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
|
|
621
|
-
const currentRun = options.currentRunOnly
|
|
622
|
-
? previews.at(-1)?.run
|
|
623
|
-
: undefined;
|
|
624
|
-
const sequenced = previews
|
|
625
|
-
.filter((preview) => !currentRun || preview.run === currentRun)
|
|
626
|
-
.filter((preview) => matchesPreviewFilter(preview, options))
|
|
627
|
-
.map((preview, index) => ({
|
|
628
|
-
...preview,
|
|
629
|
-
sequence: index + 1,
|
|
630
|
-
stripe: index % 2 === 1,
|
|
631
|
-
}));
|
|
632
|
-
return limitRoomPreviewsPerRun(sequenced, options.roomLimitPerRun).slice(
|
|
633
|
-
-Math.max(1, limit),
|
|
634
|
-
);
|
|
635
|
-
} catch (error) {
|
|
636
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
|
|
637
|
-
return [];
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
function shorten(
|
|
642
|
-
value: string | undefined,
|
|
643
|
-
maxLength: number,
|
|
644
|
-
options: { preserveSpaces?: boolean } = { preserveSpaces: true },
|
|
645
|
-
): string {
|
|
646
|
-
if (!value) return "-";
|
|
647
|
-
const compact =
|
|
648
|
-
options.preserveSpaces === false
|
|
649
|
-
? value.replaceAll(/\s+/g, "_")
|
|
650
|
-
: value.replaceAll(/\s+/g, " ").trim();
|
|
651
|
-
if (maxLength <= 1) return compact.slice(0, Math.max(0, maxLength));
|
|
652
|
-
return compact.length > maxLength
|
|
653
|
-
? `${compact.slice(0, Math.max(0, maxLength - 1))}…`
|
|
654
|
-
: compact;
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
function actorName(address: string | undefined): string {
|
|
658
|
-
if (!address) return "unknown";
|
|
659
|
-
const branch = /^branch:[^/]+\/(.+)$/.exec(address);
|
|
660
|
-
if (branch) return branch[1] || address;
|
|
661
|
-
const run = /^run:(.+)$/.exec(address);
|
|
662
|
-
if (run) return run[1] || address;
|
|
663
|
-
return address;
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
function roomName(address: string): string | undefined {
|
|
667
|
-
const room = /^room:([^/]+)(?:\/(main))?$/.exec(address);
|
|
668
|
-
return room ? room[1] : undefined;
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
function routeActorText(preview: ActorInspectorPreview): string {
|
|
672
|
-
return preview.from_display || actorName(preview.from);
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
function routeText(preview: ActorInspectorPreview): string {
|
|
676
|
-
const actor = routeActorText(preview);
|
|
677
|
-
if (preview.channel === "room") return `${actor} # all`;
|
|
678
|
-
if (preview.channel === "broadcast") return `${actor} ⇢ ${preview.to}`;
|
|
679
|
-
return `${actor} → ${actorName(preview.to)}`;
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
function attentionMarker(preview: ActorInspectorPreview): string {
|
|
683
|
-
return preview.needs_response ? "! " : "";
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
function style(
|
|
687
|
-
styleFn: ((text: string) => string) | undefined,
|
|
688
|
-
text: string,
|
|
689
|
-
): string {
|
|
690
|
-
return styleFn ? styleFn(text) : text;
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
function previewText(preview: ActorInspectorPreview): string {
|
|
694
|
-
return preview.summary || preview.body_preview || "-";
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
function propertyValue(value: unknown): string {
|
|
698
|
-
if (value === undefined) return "";
|
|
699
|
-
if (typeof value === "string") return value;
|
|
700
|
-
if (typeof value === "number" || typeof value === "boolean")
|
|
701
|
-
return String(value);
|
|
702
|
-
return JSON.stringify(value);
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
function displayWidth(value: string): number {
|
|
706
|
-
return visibleWidth(value);
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
const lineSegmenter = new Intl.Segmenter();
|
|
710
|
-
|
|
711
|
-
function boundedLine(value: string, width: number): string {
|
|
712
|
-
if (width <= 0) return "";
|
|
713
|
-
if (visibleWidth(value) <= width) return value;
|
|
714
|
-
const ellipsis = "…";
|
|
715
|
-
const ellipsisWidth = visibleWidth(ellipsis);
|
|
716
|
-
if (width <= ellipsisWidth) return ellipsis.slice(0, width);
|
|
717
|
-
let output = "";
|
|
718
|
-
let used = 0;
|
|
719
|
-
const maxTextWidth = width - ellipsisWidth;
|
|
720
|
-
for (const { segment } of lineSegmenter.segment(value)) {
|
|
721
|
-
const segmentWidth = visibleWidth(segment);
|
|
722
|
-
if (used + segmentWidth > maxTextWidth) break;
|
|
723
|
-
output += segment;
|
|
724
|
-
used += segmentWidth;
|
|
725
|
-
}
|
|
726
|
-
return `${output}${ellipsis}`;
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
function padLine(
|
|
730
|
-
plain: string,
|
|
731
|
-
rendered: string,
|
|
732
|
-
width: number,
|
|
733
|
-
styles: ActorInspectorWidgetStyle,
|
|
734
|
-
): string {
|
|
735
|
-
const boundedPlain = boundedLine(plain, width);
|
|
736
|
-
const visible =
|
|
737
|
-
boundedPlain === plain ? rendered : style(styles.preview, boundedPlain);
|
|
738
|
-
const padding = Math.max(0, width - visibleWidth(boundedPlain));
|
|
739
|
-
return `${visible}${" ".repeat(padding)}`;
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
function renderCompactInspectorEntry(
|
|
743
|
-
preview: ActorInspectorPreview,
|
|
744
|
-
width: number,
|
|
745
|
-
sequenceWidth: number,
|
|
746
|
-
routeWidth: number,
|
|
747
|
-
typeWidth: number,
|
|
748
|
-
styles: ActorInspectorWidgetStyle,
|
|
749
|
-
stripe: boolean,
|
|
750
|
-
): string[] {
|
|
751
|
-
const separator = " ";
|
|
752
|
-
const prefix = " ";
|
|
753
|
-
const suffix = " ";
|
|
754
|
-
const contentWidth = Math.max(8, width - prefix.length - suffix.length);
|
|
755
|
-
const sequence = String(preview.sequence ?? 0).padStart(sequenceWidth, " ");
|
|
756
|
-
const sequencePrefix = `${sequence}${separator}`;
|
|
757
|
-
const route = `${attentionMarker(preview)}${routeText(preview)}`;
|
|
758
|
-
const routePadding = " ".repeat(
|
|
759
|
-
Math.max(0, routeWidth - displayWidth(route)),
|
|
760
|
-
);
|
|
761
|
-
const typePadding = " ".repeat(
|
|
762
|
-
Math.max(0, typeWidth - displayWidth(preview.type)),
|
|
763
|
-
);
|
|
764
|
-
const headline = previewText(preview);
|
|
765
|
-
const lead = `${sequencePrefix}${route}${routePadding}${separator}${preview.type}${typePadding}${separator}`;
|
|
766
|
-
const visibleHeadline = boundedLine(
|
|
767
|
-
headline,
|
|
768
|
-
Math.max(0, contentWidth - displayWidth(lead)),
|
|
769
|
-
);
|
|
770
|
-
const plain = `${lead}${visibleHeadline}`;
|
|
771
|
-
const rendered = [
|
|
772
|
-
style(styles.muted, sequencePrefix),
|
|
773
|
-
style(styles.target, route),
|
|
774
|
-
routePadding,
|
|
775
|
-
separator,
|
|
776
|
-
style(styles.type, preview.type),
|
|
777
|
-
typePadding,
|
|
778
|
-
separator,
|
|
779
|
-
style(styles.preview, visibleHeadline),
|
|
780
|
-
].join("");
|
|
781
|
-
const line = `${prefix}${padLine(plain, rendered, contentWidth, styles)}${suffix}`;
|
|
782
|
-
if (stripe && styles.stripe) return [styles.stripe(line)];
|
|
783
|
-
if (!stripe && styles.stripeAlt) return [styles.stripeAlt(line)];
|
|
784
|
-
return [line];
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
function renderInspectorEntry(
|
|
788
|
-
preview: ActorInspectorPreview,
|
|
789
|
-
width: number,
|
|
790
|
-
sequenceWidth: number,
|
|
791
|
-
routeWidth: number,
|
|
792
|
-
typeWidth: number,
|
|
793
|
-
summaryWidth: number,
|
|
794
|
-
styles: ActorInspectorWidgetStyle,
|
|
795
|
-
stripe: boolean,
|
|
796
|
-
): string[] {
|
|
797
|
-
const separator = " ";
|
|
798
|
-
const prefix = " ";
|
|
799
|
-
const suffix = " ";
|
|
800
|
-
const contentWidth = Math.max(8, width - prefix.length - suffix.length);
|
|
801
|
-
const sequence = String(preview.sequence ?? 0).padStart(sequenceWidth, " ");
|
|
802
|
-
const sequencePrefix = `${sequence}${separator}`;
|
|
803
|
-
const route = `${attentionMarker(preview)}${routeText(preview)}`;
|
|
804
|
-
const type = preview.type;
|
|
805
|
-
const summary = preview.summary?.trim() ?? "";
|
|
806
|
-
const body =
|
|
807
|
-
preview.body_preview?.trim() || (!summary ? previewText(preview) : "-");
|
|
808
|
-
const visibleRoute = boundedLine(route, routeWidth);
|
|
809
|
-
const visibleType = boundedLine(type, typeWidth);
|
|
810
|
-
const boundedRoutePadding = " ".repeat(
|
|
811
|
-
Math.max(0, routeWidth - displayWidth(visibleRoute)),
|
|
812
|
-
);
|
|
813
|
-
const boundedTypePadding = " ".repeat(
|
|
814
|
-
Math.max(0, typeWidth - displayWidth(visibleType)),
|
|
815
|
-
);
|
|
816
|
-
const leadParts = [
|
|
817
|
-
style(styles.muted, sequencePrefix),
|
|
818
|
-
style(styles.target, visibleRoute),
|
|
819
|
-
boundedRoutePadding,
|
|
820
|
-
separator,
|
|
821
|
-
style(styles.type, visibleType),
|
|
822
|
-
boundedTypePadding,
|
|
823
|
-
separator,
|
|
824
|
-
];
|
|
825
|
-
let lead = `${sequencePrefix}${visibleRoute}${boundedRoutePadding}${separator}${visibleType}${boundedTypePadding}${separator}`;
|
|
826
|
-
if (summary) {
|
|
827
|
-
const visibleSummary = boundedLine(summary, summaryWidth);
|
|
828
|
-
const summaryPadding = " ".repeat(
|
|
829
|
-
Math.max(0, summaryWidth - displayWidth(visibleSummary)),
|
|
830
|
-
);
|
|
831
|
-
lead += `${visibleSummary}${summaryPadding}${separator}`;
|
|
832
|
-
leadParts.push(
|
|
833
|
-
style(styles.preview, visibleSummary),
|
|
834
|
-
summaryPadding,
|
|
835
|
-
separator,
|
|
836
|
-
);
|
|
837
|
-
}
|
|
838
|
-
const visibleBody = boundedLine(
|
|
839
|
-
body,
|
|
840
|
-
Math.max(0, contentWidth - displayWidth(lead)),
|
|
841
|
-
);
|
|
842
|
-
const plain = `${lead}${visibleBody}`;
|
|
843
|
-
const rendered = `${leadParts.join("")}${style(styles.preview, visibleBody)}`;
|
|
844
|
-
const line = `${prefix}${padLine(plain, rendered, contentWidth, styles)}${suffix}`;
|
|
845
|
-
if (stripe && styles.stripe) return [styles.stripe(line)];
|
|
846
|
-
if (!stripe && styles.stripeAlt) return [styles.stripeAlt(line)];
|
|
847
|
-
return [line];
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
export function readActorInspectorRoster(
|
|
851
|
-
stateRoot = Paths.getRunStateRoot(),
|
|
852
|
-
run: string,
|
|
853
|
-
room = "main",
|
|
854
|
-
): ActorInspectorRosterMember[] {
|
|
855
|
-
const stateDir = path.join(stateRoot, run);
|
|
856
|
-
const roster = readRoomRosterRecords(stateDir, room);
|
|
857
|
-
return Object.entries(roster).map(([address, member]) => ({
|
|
858
|
-
address,
|
|
859
|
-
...(memberDisplay(address, member)
|
|
860
|
-
? { display: memberDisplay(address, member) }
|
|
861
|
-
: {}),
|
|
862
|
-
...(typeof member.role === "string" ? { role: member.role } : {}),
|
|
863
|
-
...(typeof member.status === "string" ? { status: member.status } : {}),
|
|
864
|
-
}));
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
function rosterRoleText(role: string | undefined): string | undefined {
|
|
868
|
-
const roleLabel = role?.split(";")[0] ?? "";
|
|
869
|
-
const cleaned = roleLabel
|
|
870
|
-
.replaceAll(/\s*\([^)]*\)\s*$/g, "")
|
|
871
|
-
.trim()
|
|
872
|
-
.toLowerCase();
|
|
873
|
-
if (!cleaned || cleaned === "actor") return undefined;
|
|
874
|
-
return cleaned.replaceAll(/\s+/g, "-");
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
function rosterMemberText(member: ActorInspectorRosterMember): string {
|
|
878
|
-
const name = member.display || actorName(member.address);
|
|
879
|
-
const role = rosterRoleText(member.role);
|
|
880
|
-
if (role === "run") return `run/${name}`;
|
|
881
|
-
return role ? `${name}/${role}` : name;
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
function isRosterMemberActive(member: ActorInspectorRosterMember): boolean {
|
|
885
|
-
const status = member.status?.trim().toLowerCase();
|
|
886
|
-
return (
|
|
887
|
-
!status ||
|
|
888
|
-
status === "present" ||
|
|
889
|
-
status === "active" ||
|
|
890
|
-
status === "running"
|
|
891
|
-
);
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
export function renderInspectorRosterLine(
|
|
895
|
-
members: ActorInspectorRosterMember[],
|
|
896
|
-
width = 80,
|
|
897
|
-
styles: ActorInspectorWidgetStyle = {},
|
|
898
|
-
): string | undefined {
|
|
899
|
-
return renderInspectorRosterPanel(members, width, styles)?.[0];
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
export function renderInspectorRosterPanel(
|
|
903
|
-
members: ActorInspectorRosterMember[],
|
|
904
|
-
width = 80,
|
|
905
|
-
styles: ActorInspectorWidgetStyle = {},
|
|
906
|
-
): string[] | undefined {
|
|
907
|
-
if (members.length === 0) return undefined;
|
|
908
|
-
const safeWidth = Math.max(1, width);
|
|
909
|
-
const innerWidth = Math.max(1, safeWidth - 2);
|
|
910
|
-
const prefix = `roster ${members.length}: `;
|
|
911
|
-
const tokens = members.map((member) => ({
|
|
912
|
-
active: isRosterMemberActive(member),
|
|
913
|
-
text: rosterMemberText(member),
|
|
914
|
-
}));
|
|
915
|
-
const lines: string[] = [];
|
|
916
|
-
let plain = prefix;
|
|
917
|
-
let rendered = style(styles.muted, prefix);
|
|
918
|
-
const flush = () => {
|
|
919
|
-
const visible = boundedLine(plain, innerWidth);
|
|
920
|
-
const line = visible === plain ? rendered : style(styles.muted, visible);
|
|
921
|
-
lines.push(
|
|
922
|
-
` ${line}${" ".repeat(Math.max(0, innerWidth - displayWidth(visible)))} `,
|
|
923
|
-
);
|
|
924
|
-
};
|
|
925
|
-
for (const token of tokens) {
|
|
926
|
-
const separator = plain === prefix ? "" : ", ";
|
|
927
|
-
const nextPlain = `${plain}${separator}${token.text}`;
|
|
928
|
-
const renderedToken = style(
|
|
929
|
-
token.active ? styles.target : styles.muted,
|
|
930
|
-
token.text,
|
|
931
|
-
);
|
|
932
|
-
if (plain !== prefix && displayWidth(nextPlain) > innerWidth) {
|
|
933
|
-
flush();
|
|
934
|
-
plain = ` ${token.text}`;
|
|
935
|
-
rendered = `${style(styles.muted, " ")}${renderedToken}`;
|
|
936
|
-
continue;
|
|
937
|
-
}
|
|
938
|
-
plain = nextPlain;
|
|
939
|
-
rendered = `${rendered}${style(styles.muted, separator)}${renderedToken}`;
|
|
940
|
-
}
|
|
941
|
-
flush();
|
|
942
|
-
return lines;
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
export function renderInspectorItemView(
|
|
946
|
-
previews: ActorInspectorPreview[],
|
|
947
|
-
width = 80,
|
|
948
|
-
styles: ActorInspectorWidgetStyle = {},
|
|
949
|
-
options: ActorInspectorItemViewOptions,
|
|
950
|
-
): string[] | undefined {
|
|
951
|
-
const preview = previews.find((item) => item.sequence === options.sequence);
|
|
952
|
-
if (!preview) return undefined;
|
|
953
|
-
const safeWidth = Math.max(1, width);
|
|
954
|
-
const prefix = " ";
|
|
955
|
-
const suffix = " ";
|
|
956
|
-
const contentWidth = Math.max(8, safeWidth - prefix.length - suffix.length);
|
|
957
|
-
const rawEntries: Array<readonly [string, unknown]> = [
|
|
958
|
-
["channel", preview.channel],
|
|
959
|
-
["run", preview.run],
|
|
960
|
-
["event_id", preview.event_id],
|
|
961
|
-
["from", preview.from],
|
|
962
|
-
["from_display", preview.from_display],
|
|
963
|
-
["to", preview.to],
|
|
964
|
-
["type", preview.type],
|
|
965
|
-
["summary", preview.summary],
|
|
966
|
-
["body", preview.body_preview],
|
|
967
|
-
["message_id", preview.message_id],
|
|
968
|
-
["needs_response", preview.needs_response],
|
|
969
|
-
["timestamp", preview.timestamp],
|
|
970
|
-
["stripe", preview.stripe],
|
|
971
|
-
];
|
|
972
|
-
const entries = rawEntries
|
|
973
|
-
.filter((entry) => entry[1] !== undefined)
|
|
974
|
-
.map(([key, value]) => [key, propertyValue(value)] as const);
|
|
975
|
-
const keyWidth = Math.max(1, ...entries.map(([key]) => displayWidth(key)));
|
|
976
|
-
const sequenceText = String(preview.sequence ?? options.sequence);
|
|
977
|
-
const sequencePadding = " ".repeat(
|
|
978
|
-
Math.max(0, keyWidth - displayWidth(sequenceText)),
|
|
979
|
-
);
|
|
980
|
-
const headerSeparator = " ";
|
|
981
|
-
const route = routeText(preview);
|
|
982
|
-
const visibleRoute = boundedLine(
|
|
983
|
-
route,
|
|
984
|
-
Math.max(0, contentWidth - keyWidth - headerSeparator.length),
|
|
985
|
-
);
|
|
986
|
-
const headerPlain = `${sequenceText}${sequencePadding}${headerSeparator}${visibleRoute}`;
|
|
987
|
-
const header = `${style(styles.muted, sequenceText)}${sequencePadding}${headerSeparator}${style(styles.target, visibleRoute)}`;
|
|
988
|
-
const headerPadding = Math.max(0, contentWidth - visibleWidth(headerPlain));
|
|
989
|
-
const lines = [`${prefix}${header}${" ".repeat(headerPadding)}${suffix}`, ""];
|
|
990
|
-
for (const [key, value] of entries) {
|
|
991
|
-
const keyPadding = " ".repeat(Math.max(0, keyWidth - displayWidth(key)));
|
|
992
|
-
const separator = " ";
|
|
993
|
-
const valueWidth = Math.max(0, contentWidth - keyWidth - separator.length);
|
|
994
|
-
const visibleValue = boundedLine(value, valueWidth);
|
|
995
|
-
const plain = `${key}${keyPadding}${separator}${visibleValue}`;
|
|
996
|
-
const rendered = `${style(styles.muted, key)}${keyPadding}${separator}${style(styles.preview, visibleValue)}`;
|
|
997
|
-
const padding = Math.max(0, contentWidth - visibleWidth(plain));
|
|
998
|
-
lines.push(`${prefix}${rendered}${" ".repeat(padding)}${suffix}`);
|
|
999
|
-
}
|
|
1000
|
-
return lines;
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
export function renderInspectorWidget(
|
|
1004
|
-
previews: ActorInspectorPreview[],
|
|
1005
|
-
width = 80,
|
|
1006
|
-
styles: ActorInspectorWidgetStyle = {},
|
|
1007
|
-
options: ActorInspectorRenderOptions = {},
|
|
1008
|
-
): string[] | undefined {
|
|
1009
|
-
if (previews.length === 0) return undefined;
|
|
1010
|
-
const safeWidth = Math.max(1, width);
|
|
1011
|
-
void options;
|
|
1012
|
-
const visible = previews.map((preview, index) => ({
|
|
1013
|
-
preview: { ...preview, sequence: preview.sequence ?? index + 1 },
|
|
1014
|
-
stripe: preview.stripe ?? index % 2 === 1,
|
|
1015
|
-
}));
|
|
1016
|
-
const sequenceWidth = Math.max(
|
|
1017
|
-
1,
|
|
1018
|
-
...visible.map(({ preview }) => String(preview.sequence ?? 0).length),
|
|
1019
|
-
);
|
|
1020
|
-
const lines: string[] = [];
|
|
1021
|
-
const separatorWidth = 2;
|
|
1022
|
-
const sequencePrefixWidth = sequenceWidth + separatorWidth;
|
|
1023
|
-
const fixedSeparatorsWidth = separatorWidth * 3;
|
|
1024
|
-
const availableForColumns = Math.max(
|
|
1025
|
-
0,
|
|
1026
|
-
safeWidth - 1 - sequencePrefixWidth - fixedSeparatorsWidth,
|
|
1027
|
-
);
|
|
1028
|
-
const naturalRouteWidth = Math.max(
|
|
1029
|
-
...visible.map(({ preview }) =>
|
|
1030
|
-
displayWidth(`${attentionMarker(preview)}${routeText(preview)}`),
|
|
1031
|
-
),
|
|
1032
|
-
);
|
|
1033
|
-
const naturalTypeWidth = Math.max(
|
|
1034
|
-
...visible.map(({ preview }) => displayWidth(preview.type)),
|
|
1035
|
-
);
|
|
1036
|
-
const routeWidth = Math.min(
|
|
1037
|
-
naturalRouteWidth,
|
|
1038
|
-
Math.max(4, Math.floor(availableForColumns * 0.35)),
|
|
1039
|
-
);
|
|
1040
|
-
const typeWidth = Math.min(
|
|
1041
|
-
naturalTypeWidth,
|
|
1042
|
-
Math.max(4, Math.floor(availableForColumns * 0.25)),
|
|
1043
|
-
);
|
|
1044
|
-
const messageWidth = Math.max(
|
|
1045
|
-
0,
|
|
1046
|
-
availableForColumns - routeWidth - typeWidth,
|
|
1047
|
-
);
|
|
1048
|
-
const summaryWidths = visible
|
|
1049
|
-
.map(({ preview }) => preview.summary?.trim())
|
|
1050
|
-
.filter((summary): summary is string => Boolean(summary))
|
|
1051
|
-
.map((summary) => displayWidth(summary));
|
|
1052
|
-
const summaryWidth = summaryWidths.length
|
|
1053
|
-
? Math.min(
|
|
1054
|
-
Math.max(...summaryWidths),
|
|
1055
|
-
Math.max(1, Math.floor(messageWidth * 0.5)),
|
|
1056
|
-
)
|
|
1057
|
-
: 0;
|
|
1058
|
-
for (const { preview, stripe } of visible) {
|
|
1059
|
-
lines.push(
|
|
1060
|
-
...renderInspectorEntry(
|
|
1061
|
-
preview,
|
|
1062
|
-
safeWidth,
|
|
1063
|
-
sequenceWidth,
|
|
1064
|
-
routeWidth,
|
|
1065
|
-
typeWidth,
|
|
1066
|
-
summaryWidth,
|
|
1067
|
-
styles,
|
|
1068
|
-
stripe,
|
|
1069
|
-
),
|
|
1070
|
-
);
|
|
1071
|
-
}
|
|
1072
|
-
return lines;
|
|
1073
|
-
}
|