@narumitw/pi-inspect 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +110 -2
- package/dist/app.css +1 -0
- package/dist/app.js +128 -0
- package/dist/index.html +1 -0
- package/docs/REVIEW.md +189 -0
- package/docs/VERIFICATION.md +186 -0
- package/docs/viewer.md +74 -0
- package/package.json +67 -4
- package/src/ancestry.ts +32 -0
- package/src/collector.ts +143 -0
- package/src/context.ts +115 -0
- package/src/correlation.ts +27 -0
- package/src/entry-index.ts +17 -0
- package/src/entry-summary.ts +34 -0
- package/src/extension.ts +259 -0
- package/src/feed.ts +145 -0
- package/src/identity.ts +112 -0
- package/src/index.ts +1 -0
- package/src/model.ts +117 -0
- package/src/native-projection.ts +28 -0
- package/src/privacy.ts +136 -0
- package/src/projection.ts +374 -0
- package/src/server.ts +202 -0
- package/src/system-message.ts +117 -0
- package/src/web/api.ts +137 -0
- package/src/web/app.tsx +570 -0
- package/src/web/captured-executions.tsx +177 -0
- package/src/web/components.css +261 -0
- package/src/web/components.tsx +241 -0
- package/src/web/composition.css +554 -0
- package/src/web/context-composition.tsx +329 -0
- package/src/web/context-inventory.tsx +65 -0
- package/src/web/entry-calls.ts +14 -0
- package/src/web/format.ts +94 -0
- package/src/web/hierarchy.ts +117 -0
- package/src/web/history.ts +56 -0
- package/src/web/inline-entry.tsx +124 -0
- package/src/web/inspector.tsx +386 -0
- package/src/web/json-tree.tsx +74 -0
- package/src/web/overview.tsx +67 -0
- package/src/web/panels.css +267 -0
- package/src/web/panes.tsx +123 -0
- package/src/web/recorded-content.tsx +49 -0
- package/src/web/search.ts +16 -0
- package/src/web/session-filters.tsx +100 -0
- package/src/web/session.css +114 -0
- package/src/web/style.css +332 -0
- package/src/web/styles.d.ts +1 -0
- package/src/web/timeline.tsx +33 -0
- package/src/web/timing.ts +26 -0
- package/src/web/trace.css +411 -0
- package/src/web/trace.tsx +313 -0
- package/src/web/virtual-context.ts +138 -0
package/src/privacy.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { stripVTControlCharacters } from "node:util";
|
|
2
|
+
import type { Capture, Json } from "./model.js";
|
|
3
|
+
|
|
4
|
+
const secretKey =
|
|
5
|
+
/^(authorization|proxy.authorization|cookie|set.cookie|password|passwd|secret|client[_-]?secret|secret[_-]?key|private[_-]?key|token|api[_-]?key|access[_-]?token|refresh[_-]?token|session[_-]?token|credentials|env|headers)$|(?:^|[_-])(?:api[_-]?key|(?:access|refresh|session)[_-]?token|token|client[_-]?secret|secret[_-]?key|private[_-]?key|password|passwd|secret)$/i;
|
|
6
|
+
|
|
7
|
+
// Display-only: original session entries and event objects are never modified.
|
|
8
|
+
export function displayText(text: string): string {
|
|
9
|
+
return (
|
|
10
|
+
stripVTControlCharacters(text)
|
|
11
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: Display sanitation intentionally removes C0/C1 controls, preserving tab and newline.
|
|
12
|
+
.replace(/[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g, "")
|
|
13
|
+
.replace(/\bBearer\s+[A-Za-z0-9._~+/-]+/gi, "Bearer [redacted]")
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function sessionName(raw: unknown): { name: string; nameTruncated?: true } {
|
|
18
|
+
const clean = typeof raw === "string" ? displayText(raw) : "[unavailable: non-string session name]";
|
|
19
|
+
return { name: clean.slice(0, 512), nameTruncated: clean.length > 512 ? true : undefined };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function readSessionName(manager: { getSessionName(): string | undefined }): {
|
|
23
|
+
name: string;
|
|
24
|
+
nameTruncated?: true;
|
|
25
|
+
} {
|
|
26
|
+
try {
|
|
27
|
+
return sessionName(manager.getSessionName() ?? "Current session");
|
|
28
|
+
} catch {
|
|
29
|
+
return { name: "[unavailable: invalid stored session name]" };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function readSessionId(manager: { getSessionId(): string }): string {
|
|
34
|
+
try {
|
|
35
|
+
const id = manager.getSessionId();
|
|
36
|
+
if (typeof id !== "string" || !id.length || id.length > 512)
|
|
37
|
+
return "[unavailable: invalid or over-budget session ID]";
|
|
38
|
+
return displayText(id) || "[unavailable: blank session ID]";
|
|
39
|
+
} catch {
|
|
40
|
+
return "[unavailable: invalid session ID]";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Bound traversal before serialization: huge strings, arrays, cycles and deep objects are safe.
|
|
45
|
+
export function capture(input: unknown, maxChars = 32768): Capture {
|
|
46
|
+
let budget = maxChars;
|
|
47
|
+
let nodes = 0;
|
|
48
|
+
let truncated = false;
|
|
49
|
+
const seen = new WeakSet<object>();
|
|
50
|
+
function text(value: string): string {
|
|
51
|
+
const clean = displayText(value);
|
|
52
|
+
const length = Math.min(clean.length, Math.max(0, budget));
|
|
53
|
+
budget -= length;
|
|
54
|
+
if (length < clean.length) truncated = true;
|
|
55
|
+
return clean.slice(0, length);
|
|
56
|
+
}
|
|
57
|
+
function visit(value: unknown, depth: number): Json {
|
|
58
|
+
if (++nodes > 2048 || depth > 12 || budget <= 0) {
|
|
59
|
+
truncated = true;
|
|
60
|
+
return "[truncated]";
|
|
61
|
+
}
|
|
62
|
+
budget -= 8;
|
|
63
|
+
if (value === null || value === undefined) return null;
|
|
64
|
+
if (typeof value === "string") return text(value);
|
|
65
|
+
if (typeof value === "boolean") return value;
|
|
66
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : String(value);
|
|
67
|
+
if (typeof value !== "object") {
|
|
68
|
+
truncated = true;
|
|
69
|
+
return `[unsupported ${typeof value}]`;
|
|
70
|
+
}
|
|
71
|
+
if (seen.has(value)) {
|
|
72
|
+
truncated = true;
|
|
73
|
+
return "[circular]";
|
|
74
|
+
}
|
|
75
|
+
seen.add(value);
|
|
76
|
+
if (Array.isArray(value)) {
|
|
77
|
+
const result: Json[] = [];
|
|
78
|
+
for (let index = 0; index < value.length; index++) {
|
|
79
|
+
if (budget <= 0 || nodes >= 2048) {
|
|
80
|
+
truncated = true;
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
84
|
+
if (descriptor?.get || descriptor?.set) {
|
|
85
|
+
truncated = true;
|
|
86
|
+
result.push("[accessor omitted]");
|
|
87
|
+
} else result.push(visit(descriptor?.value, depth + 1));
|
|
88
|
+
}
|
|
89
|
+
seen.delete(value);
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
const record = value as Record<string, unknown>;
|
|
93
|
+
const type = Object.getOwnPropertyDescriptor(record, "type")?.value;
|
|
94
|
+
const mimeType = Object.getOwnPropertyDescriptor(record, "mimeType")?.value;
|
|
95
|
+
const data = Object.getOwnPropertyDescriptor(record, "data")?.value;
|
|
96
|
+
const raster =
|
|
97
|
+
type === "image" &&
|
|
98
|
+
typeof mimeType === "string" &&
|
|
99
|
+
/^(image\/(png|jpeg|gif|webp))$/.test(mimeType) &&
|
|
100
|
+
typeof data === "string" &&
|
|
101
|
+
data.length <= Math.min(16384, Math.max(0, budget / 2)) &&
|
|
102
|
+
/^[A-Za-z0-9+/]*={0,2}$/.test(data);
|
|
103
|
+
const result: Record<string, Json> = Object.create(null);
|
|
104
|
+
for (const key in record) {
|
|
105
|
+
if (!Object.hasOwn(record, key)) continue;
|
|
106
|
+
if (budget <= 0 || nodes >= 2048) {
|
|
107
|
+
truncated = true;
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
// Image bytes and opaque provider replay signatures are not useful as raw JSON.
|
|
111
|
+
const cleanKey = displayText(key);
|
|
112
|
+
const name = text(cleanKey.slice(0, 256));
|
|
113
|
+
if (cleanKey.length > 256) truncated = true;
|
|
114
|
+
const descriptor = Object.getOwnPropertyDescriptor(record, key);
|
|
115
|
+
if (descriptor?.get || descriptor?.set) {
|
|
116
|
+
truncated = true;
|
|
117
|
+
result[name] = "[accessor omitted]";
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const classifiedKey = cleanKey.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/([A-Z])([A-Z][a-z])/g, "$1_$2");
|
|
121
|
+
result[name] =
|
|
122
|
+
secretKey.test(classifiedKey) || /(?:^|[_-])secret[_-]?access[_-]?key$/i.test(classifiedKey)
|
|
123
|
+
? "[redacted]"
|
|
124
|
+
: key === "data" && type === "image"
|
|
125
|
+
? raster
|
|
126
|
+
? text(data as string)
|
|
127
|
+
: "[opaque or oversized data omitted]"
|
|
128
|
+
: /^(thinkingSignature|thoughtSignature|textSignature)$/.test(key)
|
|
129
|
+
? "[opaque data omitted]"
|
|
130
|
+
: visit(descriptor?.value, depth + 1);
|
|
131
|
+
}
|
|
132
|
+
seen.delete(value);
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
return { value: visit(input, 0), truncated };
|
|
136
|
+
}
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getCurrentSystemMessage,
|
|
3
|
+
getCurrentSystemPrompt,
|
|
4
|
+
getCurrentTools,
|
|
5
|
+
getInitialSystemMessage,
|
|
6
|
+
} from "@earendil-works/pi-ai";
|
|
7
|
+
import {
|
|
8
|
+
buildSessionProjection,
|
|
9
|
+
type ExtensionContext,
|
|
10
|
+
parseSkillBlock,
|
|
11
|
+
type SessionEntry,
|
|
12
|
+
type ToolInfo,
|
|
13
|
+
} from "@earendil-works/pi-coding-agent";
|
|
14
|
+
|
|
15
|
+
type ReadonlySessionManager = ExtensionContext["sessionManager"];
|
|
16
|
+
|
|
17
|
+
import { ancestry } from "./ancestry.js";
|
|
18
|
+
import type { Collector } from "./collector.js";
|
|
19
|
+
import { captureContext, sessionContext } from "./context.js";
|
|
20
|
+
import { correlatedCalls } from "./correlation.js";
|
|
21
|
+
import { EntryIndex } from "./entry-index.js";
|
|
22
|
+
import { readableSummary } from "./entry-summary.js";
|
|
23
|
+
import { identityIssue, recordedLeaf } from "./identity.js";
|
|
24
|
+
import type { BranchView, ContextComposition, DetailView, EntrySummary, SkillView, Snapshot } from "./model.js";
|
|
25
|
+
import { nativeProjection } from "./native-projection.js";
|
|
26
|
+
import { capture, displayText, readSessionId, readSessionName } from "./privacy.js";
|
|
27
|
+
import { systemReplayIssue } from "./system-message.js";
|
|
28
|
+
|
|
29
|
+
export function summarize(entry: SessionEntry, label?: string): EntrySummary {
|
|
30
|
+
const issue = identityIssue(entry);
|
|
31
|
+
if (issue) throw new Error(issue);
|
|
32
|
+
const message = entry.type === "message" ? entry.message : undefined;
|
|
33
|
+
const kind = displayText(message ? message.role : entry.type).slice(0, 512);
|
|
34
|
+
const value = message && "content" in message ? message.content : entry;
|
|
35
|
+
const preview = capture(value, 180).value;
|
|
36
|
+
const name =
|
|
37
|
+
entry.type === "custom" || entry.type === "custom_message"
|
|
38
|
+
? entry.customType
|
|
39
|
+
: entry.type === "model_change"
|
|
40
|
+
? entry.modelId
|
|
41
|
+
: entry.type === "thinking_level_change"
|
|
42
|
+
? entry.thinkingLevel
|
|
43
|
+
: message?.role === "assistant"
|
|
44
|
+
? message.model
|
|
45
|
+
: message?.role === "toolResult"
|
|
46
|
+
? message.toolName
|
|
47
|
+
: undefined;
|
|
48
|
+
const displayName = typeof name === "string" ? displayText(name) : undefined;
|
|
49
|
+
return {
|
|
50
|
+
id: entry.id,
|
|
51
|
+
parentId: entry.parentId,
|
|
52
|
+
...readableSummary(entry),
|
|
53
|
+
internal: ["custom", "label", "session_info", "usage"].includes(entry.type) ? true : undefined,
|
|
54
|
+
kind,
|
|
55
|
+
timestamp: entry.timestamp,
|
|
56
|
+
name: displayName?.slice(0, 512),
|
|
57
|
+
nameTruncated: displayName !== undefined && displayName.length > 512 ? true : undefined,
|
|
58
|
+
tokens:
|
|
59
|
+
message?.role === "assistant" && Number.isFinite(message.usage?.totalTokens)
|
|
60
|
+
? message.usage?.totalTokens
|
|
61
|
+
: undefined,
|
|
62
|
+
status:
|
|
63
|
+
message?.role === "toolResult" && typeof message.isError === "boolean"
|
|
64
|
+
? message.isError
|
|
65
|
+
? "error"
|
|
66
|
+
: "success"
|
|
67
|
+
: message?.role === "assistant" && message.stopReason === "error"
|
|
68
|
+
? "error"
|
|
69
|
+
: message?.role === "assistant" && message.stopReason === "aborted"
|
|
70
|
+
? "cancelled"
|
|
71
|
+
: undefined,
|
|
72
|
+
toolCallId: message?.role === "toolResult" ? message.toolCallId : undefined,
|
|
73
|
+
label: displayText(
|
|
74
|
+
typeof label === "string" ? label : typeof preview === "string" ? preview : JSON.stringify(preview),
|
|
75
|
+
).slice(0, 180),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function snapshot(
|
|
80
|
+
manager: ReadonlySessionManager,
|
|
81
|
+
collector: Collector,
|
|
82
|
+
generation: string,
|
|
83
|
+
revision: number,
|
|
84
|
+
prompt: string,
|
|
85
|
+
tools: ToolInfo[],
|
|
86
|
+
active: string[],
|
|
87
|
+
skills: SkillView[],
|
|
88
|
+
index = new EntryIndex(manager.getEntries()),
|
|
89
|
+
observedContext?: ContextComposition,
|
|
90
|
+
): Snapshot {
|
|
91
|
+
const entries = index.entries;
|
|
92
|
+
const owning = new Map<string, { id: string; ids: Set<string> }>();
|
|
93
|
+
const invalidEntries: NonNullable<Snapshot["invalidEntries"]> = [];
|
|
94
|
+
let invalidEntryCount = 0;
|
|
95
|
+
const nodes: EntrySummary[] = [];
|
|
96
|
+
const leafId = recordedLeaf(manager, index.duplicates);
|
|
97
|
+
const reserved = new Set(leafId ? ancestry(manager, leafId, index).path.map((entry) => entry.id) : []);
|
|
98
|
+
let remaining = 10000 - reserved.size;
|
|
99
|
+
const included = entries.flatMap((entry, ordinal) =>
|
|
100
|
+
reserved.has(entry.id) || remaining-- > 0 ? [{ entry, ordinal }] : [],
|
|
101
|
+
);
|
|
102
|
+
for (const { ordinal, entry } of included) {
|
|
103
|
+
const reason = index.duplicates.has(entry.id) ? "duplicate entry id" : identityIssue(entry);
|
|
104
|
+
if (reason) {
|
|
105
|
+
invalidEntryCount++;
|
|
106
|
+
if (invalidEntries.length < 20) invalidEntries.push({ index: ordinal, reason, raw: capture(entry, 2048) });
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const source =
|
|
110
|
+
entry.type === "message" && entry.message.role === "assistant"
|
|
111
|
+
? {
|
|
112
|
+
id: entry.id,
|
|
113
|
+
ids: new Set(
|
|
114
|
+
(Array.isArray(entry.message.content) ? entry.message.content : [])
|
|
115
|
+
.filter((block) => block.type === "toolCall")
|
|
116
|
+
.map((block) => block.id),
|
|
117
|
+
),
|
|
118
|
+
}
|
|
119
|
+
: entry.parentId
|
|
120
|
+
? owning.get(entry.parentId)
|
|
121
|
+
: undefined;
|
|
122
|
+
if (source) owning.set(entry.id, source);
|
|
123
|
+
const label = manager.getLabel(entry.id);
|
|
124
|
+
if (label !== undefined && typeof label !== "string") {
|
|
125
|
+
invalidEntryCount++;
|
|
126
|
+
if (invalidEntries.length < 20)
|
|
127
|
+
invalidEntries.push({
|
|
128
|
+
index: ordinal,
|
|
129
|
+
reason: "non-string resolved label",
|
|
130
|
+
raw: capture({ entry, label }, 2048),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
const node = summarize(entry, label);
|
|
134
|
+
if (node.toolCallId && source?.ids.has(node.toolCallId)) node.toolAnchor = source.id;
|
|
135
|
+
nodes.push(node);
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
protocol: 1,
|
|
139
|
+
generation,
|
|
140
|
+
revision,
|
|
141
|
+
sessionId: readSessionId(manager),
|
|
142
|
+
...readSessionName(manager),
|
|
143
|
+
leafId,
|
|
144
|
+
totalEntries: entries.length,
|
|
145
|
+
nodes,
|
|
146
|
+
incomplete: entries.length > 10000 || tools.length > 256 || skills.length > 256 || invalidEntryCount > 0,
|
|
147
|
+
invalidEntryCount,
|
|
148
|
+
invalidEntries,
|
|
149
|
+
context: observedContext ?? sessionContext(manager, index),
|
|
150
|
+
currentPrompt: capture(prompt, 65536),
|
|
151
|
+
tools: tools.slice(0, 256).map((t) => ({
|
|
152
|
+
name: displayText(t.name),
|
|
153
|
+
description: displayText(t.description).slice(0, 512),
|
|
154
|
+
exposure: t.exposure,
|
|
155
|
+
active: active.includes(t.name),
|
|
156
|
+
callable:
|
|
157
|
+
t.exposure === "codemode" || t.exposure === "deferred" || (t.exposure === "direct" && active.includes(t.name)),
|
|
158
|
+
namespace: t.namespace ? displayText(t.namespace.name) : undefined,
|
|
159
|
+
schema: capture(t.parameters, 2048),
|
|
160
|
+
})),
|
|
161
|
+
skills: skills.slice(0, 256).map((s) => ({
|
|
162
|
+
name: displayText(s.name),
|
|
163
|
+
path: displayText(s.path),
|
|
164
|
+
description: displayText(s.description).slice(0, 512),
|
|
165
|
+
})),
|
|
166
|
+
calls: collector.list(),
|
|
167
|
+
droppedCalls: collector.dropped,
|
|
168
|
+
invalidCallEvents: collector.invalidEvents,
|
|
169
|
+
captureStartedAt: collector.startedAt,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function branch(
|
|
174
|
+
manager: ReadonlySessionManager,
|
|
175
|
+
leafId: string,
|
|
176
|
+
offset: number,
|
|
177
|
+
skills: SkillView[],
|
|
178
|
+
index = new EntryIndex(manager.getEntries()),
|
|
179
|
+
): BranchView {
|
|
180
|
+
if (!index.byId.has(leafId)) throw new Error("Unknown entry");
|
|
181
|
+
const walked = ancestry(manager, leafId, index);
|
|
182
|
+
const { path } = walked;
|
|
183
|
+
const projection = walked.issue ? undefined : buildSessionProjection(path, leafId);
|
|
184
|
+
const target = index.get(leafId);
|
|
185
|
+
const previousView = !walked.issue && target?.parentId ? nativeProjection(path, target.parentId) : undefined;
|
|
186
|
+
const previous = previousView?.projection?.messages ?? [];
|
|
187
|
+
const issue = walked.issue ?? (projection && systemReplayIssue(projection.messages)) ?? systemReplayIssue(previous);
|
|
188
|
+
if (issue || !projection) {
|
|
189
|
+
const unavailable = capture(`[unavailable: ${issue}]`);
|
|
190
|
+
return {
|
|
191
|
+
leafId,
|
|
192
|
+
ancestryIssue: issue,
|
|
193
|
+
entries: path
|
|
194
|
+
.slice(offset, offset + 50)
|
|
195
|
+
.filter((entry) => !identityIssue(entry))
|
|
196
|
+
.map((entry) => summarize(entry, manager.getLabel(entry.id))),
|
|
197
|
+
total: path.length,
|
|
198
|
+
offset,
|
|
199
|
+
prompt: unavailable,
|
|
200
|
+
previousPrompt: unavailable,
|
|
201
|
+
sections: unavailable,
|
|
202
|
+
declaredTools: unavailable,
|
|
203
|
+
promptUpdates: unavailable,
|
|
204
|
+
projection: unavailable,
|
|
205
|
+
context: {
|
|
206
|
+
source: "session-derived",
|
|
207
|
+
leafId,
|
|
208
|
+
totalMessages: 0,
|
|
209
|
+
messages: [],
|
|
210
|
+
segments: [],
|
|
211
|
+
incomplete: true,
|
|
212
|
+
unavailable: issue,
|
|
213
|
+
},
|
|
214
|
+
skillEvidence: unavailable,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
const historicalSystem = getCurrentSystemMessage(projection.messages);
|
|
218
|
+
const evidence: { name: string; state: string; entryId: string }[] = [];
|
|
219
|
+
const callPaths = new Map<string, string>();
|
|
220
|
+
const byPath = new Map(skills.map((skill) => [skill.path, skill]));
|
|
221
|
+
let scanned = 0;
|
|
222
|
+
let evidenceIncomplete = false;
|
|
223
|
+
function spend(): boolean {
|
|
224
|
+
if (++scanned <= 2048 && evidence.length < 256) return true;
|
|
225
|
+
evidenceIncomplete = true;
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
evidenceLoop: for (const entry of path) {
|
|
229
|
+
if (identityIssue(entry)) continue;
|
|
230
|
+
if (entry.type !== "message") continue;
|
|
231
|
+
const m = entry.message;
|
|
232
|
+
if (m.role === "assistant") {
|
|
233
|
+
callPaths.clear();
|
|
234
|
+
const duplicateIds = new Set<string>();
|
|
235
|
+
const encountered = new Set<string>();
|
|
236
|
+
for (const block of Array.isArray(m.content) ? m.content : []) {
|
|
237
|
+
if (!spend()) break evidenceLoop;
|
|
238
|
+
if (block.type === "toolCall") {
|
|
239
|
+
if (encountered.has(block.id)) duplicateIds.add(block.id);
|
|
240
|
+
encountered.add(block.id);
|
|
241
|
+
}
|
|
242
|
+
if (
|
|
243
|
+
block.type === "toolCall" &&
|
|
244
|
+
block.name === "read" &&
|
|
245
|
+
typeof block.arguments?.path === "string" &&
|
|
246
|
+
block.arguments.path.length <= 8192
|
|
247
|
+
) {
|
|
248
|
+
callPaths.set(block.id, block.arguments.path);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
for (const id of duplicateIds) callPaths.delete(id);
|
|
252
|
+
}
|
|
253
|
+
if (m.role === "user" && typeof m.content === "string") {
|
|
254
|
+
if (!spend()) break;
|
|
255
|
+
if (m.content.length > 8192) {
|
|
256
|
+
evidenceIncomplete = true;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
const invoked = parseSkillBlock(m.content);
|
|
260
|
+
if (invoked) evidence.push({ name: invoked.name, state: "explicitly invoked", entryId: entry.id });
|
|
261
|
+
}
|
|
262
|
+
if (m.role === "toolResult" && m.isError === false) {
|
|
263
|
+
if (!spend()) break;
|
|
264
|
+
const path = callPaths.get(m.toolCallId);
|
|
265
|
+
const skill = path ? byPath.get(path) : undefined;
|
|
266
|
+
if (skill) evidence.push({ name: skill.name, state: "successfully read", entryId: entry.id });
|
|
267
|
+
// Public bounded nested metadata provides names/arguments/status, never child results.
|
|
268
|
+
for (const call of Array.isArray(m.nestedCalls?.calls) ? m.nestedCalls.calls : []) {
|
|
269
|
+
if (!spend()) break evidenceLoop;
|
|
270
|
+
if (!call || typeof call !== "object" || call.name !== "read" || call.status !== "ok") continue;
|
|
271
|
+
const nestedPath = call.arguments?.path;
|
|
272
|
+
const skill = typeof nestedPath === "string" && nestedPath.length <= 8192 ? byPath.get(nestedPath) : undefined;
|
|
273
|
+
if (skill) {
|
|
274
|
+
evidence.push({ name: skill.name, state: "successfully read (nested metadata)", entryId: entry.id });
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
const capturedEvidence = capture(evidence);
|
|
280
|
+
capturedEvidence.truncated ||= evidenceIncomplete;
|
|
281
|
+
return {
|
|
282
|
+
leafId,
|
|
283
|
+
entries: path
|
|
284
|
+
.slice(offset, offset + 50)
|
|
285
|
+
.filter((entry) => !identityIssue(entry))
|
|
286
|
+
.map((e) => summarize(e, manager.getLabel(e.id))),
|
|
287
|
+
total: path.length,
|
|
288
|
+
offset,
|
|
289
|
+
prompt: capture(
|
|
290
|
+
historicalSystem ? getCurrentSystemPrompt(projection.messages) : "[unavailable: no stored system prompt]",
|
|
291
|
+
65536,
|
|
292
|
+
),
|
|
293
|
+
previousPrompt: capture(
|
|
294
|
+
previousView?.issue
|
|
295
|
+
? `[unavailable: ${previousView.issue}]`
|
|
296
|
+
: getCurrentSystemMessage(previous)
|
|
297
|
+
? getCurrentSystemPrompt(previous)
|
|
298
|
+
: "[unavailable: no stored system prompt]",
|
|
299
|
+
65536,
|
|
300
|
+
),
|
|
301
|
+
sections: capture(historicalSystem?.sections),
|
|
302
|
+
declaredTools: capture(
|
|
303
|
+
getInitialSystemMessage(projection.messages)
|
|
304
|
+
? getCurrentTools(projection.messages).map((t) => ({
|
|
305
|
+
name: t.name,
|
|
306
|
+
description: t.description,
|
|
307
|
+
parameters: t.parameters,
|
|
308
|
+
}))
|
|
309
|
+
: "[unavailable: no initial system/tool checkpoint]",
|
|
310
|
+
),
|
|
311
|
+
promptUpdates: capture(
|
|
312
|
+
path
|
|
313
|
+
.filter((e) => e.type === "message" && e.message.role === "system")
|
|
314
|
+
.map((e) => ({ id: e.id, message: e.type === "message" ? e.message : null })),
|
|
315
|
+
),
|
|
316
|
+
projection: capture(
|
|
317
|
+
projection.entries.map((e) => ({ id: e.sourceEntry.id, messages: e.messages })),
|
|
318
|
+
65536,
|
|
319
|
+
),
|
|
320
|
+
context: captureContext(projection.messages, "session-derived", leafId),
|
|
321
|
+
skillEvidence: capturedEvidence,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export function detail(
|
|
326
|
+
manager: ReadonlySessionManager,
|
|
327
|
+
id: string,
|
|
328
|
+
leafId: string,
|
|
329
|
+
collector: Collector,
|
|
330
|
+
index = new EntryIndex(manager.getEntries()),
|
|
331
|
+
): DetailView {
|
|
332
|
+
if (index.duplicates.has(id) || index.duplicates.has(leafId)) throw new Error("Ambiguous duplicate entry id");
|
|
333
|
+
const entry = index.get(id);
|
|
334
|
+
if (!entry || !index.get(leafId)) throw new Error("Unknown entry");
|
|
335
|
+
const selected = ancestry(manager, id, index);
|
|
336
|
+
const leaf = id === leafId ? selected : ancestry(manager, leafId, index);
|
|
337
|
+
const issue = selected.issue ?? leaf.issue;
|
|
338
|
+
if (issue)
|
|
339
|
+
return {
|
|
340
|
+
ancestryIssue: issue,
|
|
341
|
+
raw: capture(entry, 65536),
|
|
342
|
+
projected: capture(`[unavailable: ${issue}]`),
|
|
343
|
+
calls: [],
|
|
344
|
+
};
|
|
345
|
+
const projected = buildSessionProjection(leaf.path, leafId).entries.find((e) => e.sourceEntry.id === id);
|
|
346
|
+
const ids = new Set<string>();
|
|
347
|
+
if (entry.type === "message") {
|
|
348
|
+
const m = entry.message;
|
|
349
|
+
if (m.role === "assistant" && !identityIssue(entry))
|
|
350
|
+
for (const b of Array.isArray(m.content) ? m.content : []) if (b.type === "toolCall") ids.add(b.id);
|
|
351
|
+
if (m.role === "toolResult") ids.add(m.toolCallId);
|
|
352
|
+
}
|
|
353
|
+
const toolAnchor =
|
|
354
|
+
entry.type === "message" && entry.message.role === "assistant"
|
|
355
|
+
? entry.id
|
|
356
|
+
: selected.path
|
|
357
|
+
.slice()
|
|
358
|
+
.reverse()
|
|
359
|
+
.find(
|
|
360
|
+
(candidate) =>
|
|
361
|
+
candidate.type === "message" &&
|
|
362
|
+
!identityIssue(candidate) &&
|
|
363
|
+
candidate.message.role === "assistant" &&
|
|
364
|
+
(Array.isArray(candidate.message.content) ? candidate.message.content : []).some(
|
|
365
|
+
(block) => block.type === "toolCall" && ids.has(block.id),
|
|
366
|
+
),
|
|
367
|
+
)?.id;
|
|
368
|
+
return {
|
|
369
|
+
toolAnchor,
|
|
370
|
+
raw: capture(entry, 65536),
|
|
371
|
+
projected: capture(projected?.messages),
|
|
372
|
+
calls: correlatedCalls(ids, toolAnchor, collector.list()),
|
|
373
|
+
};
|
|
374
|
+
}
|