@khorsheed/dsh-context-lens 0.0.0-stage → 0.1.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/CHANGELOG.md +16 -0
- package/README.en.md +53 -0
- package/README.i18n.yaml +6 -0
- package/README.md +52 -2
- package/cordis.patch.yml +13 -0
- package/lib/client.js +26953 -0
- package/lib/index.js +3943 -0
- package/lib/typert.host.d.ts +3 -0
- package/lib/typert.host.js +1569 -0
- package/lib/typert.remote-client.d.ts +40 -0
- package/lib/typert.remote-client.js +1202 -0
- package/lib/types/changes.d.ts +72 -0
- package/lib/types/changes.js +328 -0
- package/lib/types/client/AgentTabs.d.ts +34 -0
- package/lib/types/client/AgentTabs.js +89 -0
- package/lib/types/client/ChangeList.d.ts +28 -0
- package/lib/types/client/ChangeList.js +109 -0
- package/lib/types/client/ConfigFacts.d.ts +21 -0
- package/lib/types/client/ConfigFacts.js +30 -0
- package/lib/types/client/HeaderUtility.d.ts +12 -0
- package/lib/types/client/HeaderUtility.js +8 -0
- package/lib/types/client/InspectorPanel.d.ts +5 -0
- package/lib/types/client/InspectorPanel.js +486 -0
- package/lib/types/client/RawView.d.ts +18 -0
- package/lib/types/client/RawView.js +8 -0
- package/lib/types/client/RequestFacts.d.ts +19 -0
- package/lib/types/client/RequestFacts.js +39 -0
- package/lib/types/client/RoomDecisionsView.d.ts +24 -0
- package/lib/types/client/RoomDecisionsView.js +52 -0
- package/lib/types/client/SchemaFields.d.ts +13 -0
- package/lib/types/client/SchemaFields.js +96 -0
- package/lib/types/client/SegCtl.d.ts +32 -0
- package/lib/types/client/SegCtl.js +12 -0
- package/lib/types/client/SegmentBlocks.d.ts +28 -0
- package/lib/types/client/SegmentBlocks.js +225 -0
- package/lib/types/client/TimingView.d.ts +17 -0
- package/lib/types/client/TimingView.js +98 -0
- package/lib/types/client/TokenView.d.ts +21 -0
- package/lib/types/client/TokenView.js +22 -0
- package/lib/types/client/ToolCatalog.d.ts +9 -0
- package/lib/types/client/ToolCatalog.js +51 -0
- package/lib/types/client/Trend.d.ts +25 -0
- package/lib/types/client/Trend.js +100 -0
- package/lib/types/client/TurnAction.d.ts +14 -0
- package/lib/types/client/TurnAction.js +31 -0
- package/lib/types/client/TurnMenu.d.ts +36 -0
- package/lib/types/client/TurnMenu.js +50 -0
- package/lib/types/client/aggregate.d.ts +141 -0
- package/lib/types/client/aggregate.js +213 -0
- package/lib/types/client/contract.d.ts +58 -0
- package/lib/types/client/contract.js +1 -0
- package/lib/types/client/definition.d.ts +47 -0
- package/lib/types/client/definition.js +25 -0
- package/lib/types/client/icons-local.d.ts +19 -0
- package/lib/types/client/icons-local.js +3 -0
- package/lib/types/client/icons.d.ts +18 -0
- package/lib/types/client/icons.js +2 -0
- package/lib/types/client/index.d.ts +42 -0
- package/lib/types/client/index.js +139 -0
- package/lib/types/client/locales.d.ts +365 -0
- package/lib/types/client/locales.js +705 -0
- package/lib/types/client/preset-visibility.d.ts +99 -0
- package/lib/types/client/preset-visibility.js +108 -0
- package/lib/types/client/scroll-sync.d.ts +54 -0
- package/lib/types/client/scroll-sync.js +118 -0
- package/lib/types/client/timing.d.ts +29 -0
- package/lib/types/client/timing.js +117 -0
- package/lib/types/client/wire.d.ts +13 -0
- package/lib/types/client/wire.js +11 -0
- package/lib/types/index.d.ts +32 -0
- package/lib/types/index.js +25 -0
- package/lib/types/log-lane.d.ts +198 -0
- package/lib/types/log-lane.js +841 -0
- package/lib/types/messages.d.ts +96 -0
- package/lib/types/messages.js +254 -0
- package/lib/types/prefix-diff.d.ts +4 -0
- package/lib/types/prefix-diff.js +20 -0
- package/lib/types/remote.d.ts +45 -0
- package/lib/types/remote.js +99 -0
- package/lib/types/room.d.ts +66 -0
- package/lib/types/room.js +101 -0
- package/lib/types/segmenter.d.ts +100 -0
- package/lib/types/segmenter.js +184 -0
- package/lib/types/service.d.ts +145 -0
- package/lib/types/service.js +493 -0
- package/lib/types/store.d.ts +424 -0
- package/lib/types/store.js +1920 -0
- package/lib/types/tool-summary.d.ts +6 -0
- package/lib/types/tool-summary.js +7 -0
- package/lib/types/types.d.ts +703 -0
- package/lib/types/types.js +18 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +133 -3
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
import { InspectorStore, scopeAuthorized } from "./store.js";
|
|
2
|
+
import { recordAssembly } from "./segmenter.js";
|
|
3
|
+
import { fromRequestMessage, serializeMessage } from "./messages.js";
|
|
4
|
+
import { messageOfRow, meterOf, replacementFact, replacementFactsOf } from "./changes.js";
|
|
5
|
+
import { foldLogRows, IN_PROCESS_PROVIDERS, isSessionLogReader, openCursor, reconstructSession, } from "./log-lane.js";
|
|
6
|
+
/** The capture store's query face (Remote delegates here). */
|
|
7
|
+
export class InspectorService {
|
|
8
|
+
ctx;
|
|
9
|
+
config;
|
|
10
|
+
store;
|
|
11
|
+
disposers;
|
|
12
|
+
/** childId → open read handle (persistent: reads are offset-incremental). */
|
|
13
|
+
handles = new Map();
|
|
14
|
+
cursors = new Map();
|
|
15
|
+
/** In-flight refresh per childId — concurrent panel queries share one read. */
|
|
16
|
+
refreshing = new Map();
|
|
17
|
+
/** sessionId → the metering event (`compaction/summary` | `compaction/prune`) just seen; the host appends its replacement next. */
|
|
18
|
+
meters = new Map();
|
|
19
|
+
now;
|
|
20
|
+
logWarn;
|
|
21
|
+
persistenceOverride;
|
|
22
|
+
constructor(ctx, config = {}, deps = {}) {
|
|
23
|
+
this.ctx = ctx;
|
|
24
|
+
this.config = config;
|
|
25
|
+
this.now = deps.now ?? Date.now;
|
|
26
|
+
this.logWarn = deps.logger?.warn.bind(deps.logger) ?? ((message) => ctx.logger.warn(message));
|
|
27
|
+
this.persistenceOverride = deps.persistence;
|
|
28
|
+
const storeConfig = {
|
|
29
|
+
...config.maxTurnsPerSession === undefined ? {} : { maxTurnsPerSession: config.maxTurnsPerSession },
|
|
30
|
+
...config.maxBackfillRequestsPerSession === undefined ? {} : { maxBackfillRequestsPerSession: config.maxBackfillRequestsPerSession },
|
|
31
|
+
...config.maxStoredCharsTotal === undefined ? {} : { maxStoredCharsTotal: config.maxStoredCharsTotal },
|
|
32
|
+
};
|
|
33
|
+
this.store = new InspectorStore(storeConfig, {
|
|
34
|
+
now: this.now,
|
|
35
|
+
onTurnEvicted: (_sessionId, turn) => this.dropEvictedChildState(turn),
|
|
36
|
+
...deps.generation === undefined ? {} : { generation: deps.generation },
|
|
37
|
+
});
|
|
38
|
+
this.disposers = [
|
|
39
|
+
ctx.on('llm/stream', (options, next) => this.onStream(options, next)),
|
|
40
|
+
ctx.on('system-prompt/assemble', (_assembly, _context, next) => this.onAssemble(next)),
|
|
41
|
+
ctx.on('session/event', (session, event) => this.onSessionEvent(session, event)),
|
|
42
|
+
];
|
|
43
|
+
}
|
|
44
|
+
// ---------- lane 1: llm/stream (in-process, full fidelity) ----------
|
|
45
|
+
/**
|
|
46
|
+
* The waterfall tee: snapshot the (possibly deep-frozen) request at dispatch,
|
|
47
|
+
* then forward every chunk unchanged while observing. A capture-side failure
|
|
48
|
+
* at setup degrades to the bare downstream stream; mid-stream failures only
|
|
49
|
+
* lose the remaining observations.
|
|
50
|
+
*/
|
|
51
|
+
onStream(options, next) {
|
|
52
|
+
let request;
|
|
53
|
+
try {
|
|
54
|
+
request = this.store.beginRequest(snapshotOptions(options), this.now());
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
this.logWarn(`context-lens: capture setup failed, forwarding uncaptured (${String(error)})`);
|
|
58
|
+
return next();
|
|
59
|
+
}
|
|
60
|
+
if (request === undefined)
|
|
61
|
+
return next();
|
|
62
|
+
return this.tee(request, next);
|
|
63
|
+
}
|
|
64
|
+
tee(request, next) {
|
|
65
|
+
const { store, now } = this;
|
|
66
|
+
const warn = this.logWarn;
|
|
67
|
+
return (async function* () {
|
|
68
|
+
let thrown;
|
|
69
|
+
try {
|
|
70
|
+
for await (const chunk of next()) {
|
|
71
|
+
try {
|
|
72
|
+
store.observeChunk(request, chunk, now());
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
warn(`context-lens: chunk observation dropped (${String(error)})`);
|
|
76
|
+
}
|
|
77
|
+
yield chunk;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
thrown = error;
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
try {
|
|
86
|
+
store.settleRequest(request, now(), thrown);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
warn(`context-lens: request settlement dropped (${String(error)})`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
})();
|
|
93
|
+
}
|
|
94
|
+
// ---------- lane 2: system-prompt/assemble (read-only attribution feed) ----------
|
|
95
|
+
/**
|
|
96
|
+
* Record the post-waterfall assembly for section attribution. The returned
|
|
97
|
+
* value passes through unchanged — this listener observes, it never rewrites
|
|
98
|
+
* a prompt.
|
|
99
|
+
*/
|
|
100
|
+
async onAssemble(next) {
|
|
101
|
+
const result = await next();
|
|
102
|
+
try {
|
|
103
|
+
recordAssembly(this.store.assemblyRing, result, this.now());
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
this.logWarn(`context-lens: assembly record dropped (${String(error)})`);
|
|
107
|
+
}
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
// ---------- lane 3: session/event (turn brackets + tool pairing + catalog) ----------
|
|
111
|
+
onSessionEvent(session, event) {
|
|
112
|
+
try {
|
|
113
|
+
this.dispatchSessionEvent(session, event);
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
this.logWarn(`context-lens: session event "${String(event.type)}" dropped (${String(error)})`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
dispatchSessionEvent(session, event) {
|
|
120
|
+
const sessionId = session.id;
|
|
121
|
+
const at = event.time;
|
|
122
|
+
const eventType = event.type;
|
|
123
|
+
this.noteReplacement(session, event);
|
|
124
|
+
this.store.noteLogActivity(sessionId);
|
|
125
|
+
if (eventType.startsWith('compaction/'))
|
|
126
|
+
this.store.noteCompaction(sessionId, eventType, event.data);
|
|
127
|
+
if (eventType.startsWith('room/')) {
|
|
128
|
+
this.store.noteRoomEvent(sessionId, eventType, event.data, event.seq, at);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (event.type === 'tool/result' && event.surfaceOp !== 'append') {
|
|
132
|
+
const original = session.eventAt(event.surfaceOp.startSeq);
|
|
133
|
+
if (original?.type === 'tool/result') {
|
|
134
|
+
this.store.notePruning(sessionId, event.data.message.id, serializeMessage(fromRequestMessage(original.data.message)).length, serializeMessage(fromRequestMessage(event.data.message)).length);
|
|
135
|
+
}
|
|
136
|
+
// A replacement updates context, not the original execution's result.
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
switch (event.type) {
|
|
140
|
+
case 'turn/start':
|
|
141
|
+
this.store.turnStart(sessionId, event.data.turn, at, event.seq);
|
|
142
|
+
return;
|
|
143
|
+
case 'turn/end':
|
|
144
|
+
this.store.turnEnd(sessionId, event.data.turn, event.data.reason, at);
|
|
145
|
+
return;
|
|
146
|
+
case 'user/message': {
|
|
147
|
+
const message = fromRequestMessage(event.data);
|
|
148
|
+
if (message.compactionId !== undefined && event.surfaceOp !== 'append')
|
|
149
|
+
this.store.noteCompaction(sessionId, 'compaction/applied', { compactionId: message.compactionId });
|
|
150
|
+
if (message.id)
|
|
151
|
+
this.store.noteMessageTurn(sessionId, message.id);
|
|
152
|
+
this.store.noteUserMessage(sessionId, message.sourceKind, serializeMessage(message));
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
case 'assistant/message':
|
|
156
|
+
this.store.noteMessageTurn(sessionId, event.data.message.id, event.data.turn);
|
|
157
|
+
this.store.noteAssistantMessage(sessionId, event.data.message.id);
|
|
158
|
+
return;
|
|
159
|
+
case 'tool/call':
|
|
160
|
+
this.store.toolCall(sessionId, event.data.callId, event.data.name, event.data.arguments, at);
|
|
161
|
+
return;
|
|
162
|
+
case 'tool/result': {
|
|
163
|
+
this.store.noteMessageTurn(sessionId, event.data.message.id, event.data.turn);
|
|
164
|
+
const message = fromRequestMessage(event.data.message);
|
|
165
|
+
this.store.toolResult(sessionId, event.data.message.toolCallId, event.data.message.isError === true, serializeMessage(message), at);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
default:
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
// `subagent/catalog` belongs to the subagent package's SessionEventMap
|
|
172
|
+
// merge — read structurally so this plugin neither depends on that package
|
|
173
|
+
// nor breaks when the composition lacks it.
|
|
174
|
+
const type = event.type;
|
|
175
|
+
if (type === 'subagent/descriptor') {
|
|
176
|
+
const provider = event.data['provider'];
|
|
177
|
+
if (typeof provider === 'string') {
|
|
178
|
+
this.store.noteChildProvider(sessionId, provider);
|
|
179
|
+
if (!IN_PROCESS_PROVIDERS.has(provider))
|
|
180
|
+
this.store.markLogDriven(sessionId);
|
|
181
|
+
}
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (type === 'subagent/catalog') {
|
|
185
|
+
const data = event.data;
|
|
186
|
+
const childId = data['childId'];
|
|
187
|
+
if (typeof childId === 'string' && childId.length > 0) {
|
|
188
|
+
const label = data['label'];
|
|
189
|
+
this.store.subagentCatalog(sessionId, childId, typeof label === 'string' && label.length > 0 ? label : undefined, at);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// ---------- lane 4: child logs (fresh-process subagent fallback) ----------
|
|
194
|
+
/** The probed `sessionPersistence` service, when the composition mounts one. */
|
|
195
|
+
reader() {
|
|
196
|
+
if (this.persistenceOverride !== undefined)
|
|
197
|
+
return this.persistenceOverride;
|
|
198
|
+
try {
|
|
199
|
+
const candidate = this.ctx.get('sessionPersistence');
|
|
200
|
+
return isSessionLogReader(candidate) ? candidate : undefined;
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Refresh the log-sourced child scopes of one session (query-time,
|
|
208
|
+
* incremental). A scope the query's {@link ScopeFilter} excludes is skipped
|
|
209
|
+
* outright — its log is not opened on an unauthorized query's behalf.
|
|
210
|
+
*/
|
|
211
|
+
async refreshChildren(rootSessionId, filter) {
|
|
212
|
+
const reader = this.reader();
|
|
213
|
+
if (reader === undefined)
|
|
214
|
+
return;
|
|
215
|
+
const scopes = this.store.childScopes(rootSessionId);
|
|
216
|
+
await Promise.all(scopes.map(scope => scopeAuthorized(scope.scopeId, filter)
|
|
217
|
+
? this.refreshChild(rootSessionId, scope.scopeId, reader, scope.live)
|
|
218
|
+
: Promise.resolve()));
|
|
219
|
+
}
|
|
220
|
+
refreshChild(rootSessionId, childId, reader, live) {
|
|
221
|
+
if (live)
|
|
222
|
+
return Promise.resolve();
|
|
223
|
+
const pending = this.refreshing.get(childId);
|
|
224
|
+
if (pending !== undefined)
|
|
225
|
+
return pending;
|
|
226
|
+
const job = this.doRefreshChild(rootSessionId, childId, reader)
|
|
227
|
+
.finally(() => {
|
|
228
|
+
this.refreshing.delete(childId);
|
|
229
|
+
});
|
|
230
|
+
this.refreshing.set(childId, job);
|
|
231
|
+
return job;
|
|
232
|
+
}
|
|
233
|
+
async doRefreshChild(rootSessionId, childId, reader) {
|
|
234
|
+
let cursor = this.cursors.get(childId);
|
|
235
|
+
if (cursor?.broken !== undefined)
|
|
236
|
+
return;
|
|
237
|
+
if (cursor === undefined) {
|
|
238
|
+
cursor = openCursor();
|
|
239
|
+
this.cursors.set(childId, cursor);
|
|
240
|
+
}
|
|
241
|
+
try {
|
|
242
|
+
let handle = this.handles.get(childId);
|
|
243
|
+
if (handle === undefined) {
|
|
244
|
+
handle = await reader.open(childId, 'read');
|
|
245
|
+
this.handles.set(childId, handle);
|
|
246
|
+
}
|
|
247
|
+
const { events } = await handle.read(cursor.consumed);
|
|
248
|
+
const descriptor = events.find(row => row.type === 'subagent/descriptor');
|
|
249
|
+
const provider = descriptor?.data?.['provider'];
|
|
250
|
+
if (typeof provider === 'string')
|
|
251
|
+
this.store.noteChildProvider(childId, provider);
|
|
252
|
+
for (const request of foldLogRows(cursor, events)) {
|
|
253
|
+
this.store.appendLogRequest(rootSessionId, childId, request);
|
|
254
|
+
}
|
|
255
|
+
this.store.refreshExecutionResults(rootSessionId, childId);
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
// A not-yet-materialized child log is transient (the child process flushes
|
|
259
|
+
// on its own schedule) — retry on the next refresh. Anything else is a
|
|
260
|
+
// durable unreadability (format drift, unknown event vocabulary): stop
|
|
261
|
+
// hammering it and keep the scope's honest empty state.
|
|
262
|
+
if (error instanceof Error && error.name === 'SessionPersistenceNotFoundError')
|
|
263
|
+
return;
|
|
264
|
+
cursor.broken = error instanceof Error ? error.message : String(error);
|
|
265
|
+
this.logWarn(`context-lens: child log "${childId}" unreadable (${cursor.broken})`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
/** Turn eviction drops the child cursors/handles whose scopes lived on it. */
|
|
269
|
+
dropEvictedChildState(turn) {
|
|
270
|
+
for (const scopeId of turn.scopes.keys()) {
|
|
271
|
+
if (scopeId === 'main')
|
|
272
|
+
continue;
|
|
273
|
+
this.cursors.delete(scopeId);
|
|
274
|
+
const handle = this.handles.get(scopeId);
|
|
275
|
+
if (handle !== undefined) {
|
|
276
|
+
this.handles.delete(scopeId);
|
|
277
|
+
void handle.close().catch(() => undefined);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
// ---------- lane 5: main-session history backfill ----------
|
|
282
|
+
/**
|
|
283
|
+
* Reconstruct the session's pre-mount history from its own log, once per
|
|
284
|
+
* session (the store's watermark holds). The read is one full pass at first
|
|
285
|
+
* query; the store keeps the newest turns that fit the budget behind the
|
|
286
|
+
* live ones. A not-yet-materialized log is transient (retry next query); any
|
|
287
|
+
* other read failure marks the session broken and stops hammering it.
|
|
288
|
+
*/
|
|
289
|
+
/** Pair a surface replacement with what it shadowed, typed by the metering event right before it. */
|
|
290
|
+
noteReplacement(session, row) {
|
|
291
|
+
const meter = this.meters.get(session.id);
|
|
292
|
+
this.meters.delete(session.id);
|
|
293
|
+
const next = meterOf(row);
|
|
294
|
+
if (next !== undefined)
|
|
295
|
+
this.meters.set(session.id, next);
|
|
296
|
+
const fact = replacementFact(row, seq => {
|
|
297
|
+
const shadowed = session.eventAt(seq);
|
|
298
|
+
return shadowed === undefined ? undefined : messageOfRow(shadowed)?.id;
|
|
299
|
+
}, meter);
|
|
300
|
+
if (fact !== undefined)
|
|
301
|
+
this.store.noteReplacement(session.id, fact);
|
|
302
|
+
}
|
|
303
|
+
async backfillMainSession(sessionId) {
|
|
304
|
+
const reader = this.reader();
|
|
305
|
+
if (reader === undefined)
|
|
306
|
+
return;
|
|
307
|
+
// Retryable until complete: a partial fill re-reads on later queries
|
|
308
|
+
// (eviction may have freed room); the read is one zstd decode.
|
|
309
|
+
const state = this.store.backfillState(sessionId);
|
|
310
|
+
if (state === 'broken' || (state === 'complete' && !this.store.needsRequestNumbers(sessionId) && !this.store.needsLogRead(sessionId)))
|
|
311
|
+
return;
|
|
312
|
+
const jobKey = `main:${sessionId}`;
|
|
313
|
+
const pending = this.refreshing.get(jobKey);
|
|
314
|
+
if (pending !== undefined)
|
|
315
|
+
return pending;
|
|
316
|
+
const job = this.doBackfillMainSession(sessionId, reader)
|
|
317
|
+
.finally(() => {
|
|
318
|
+
this.refreshing.delete(jobKey);
|
|
319
|
+
});
|
|
320
|
+
this.refreshing.set(jobKey, job);
|
|
321
|
+
return job;
|
|
322
|
+
}
|
|
323
|
+
async doBackfillMainSession(sessionId, reader) {
|
|
324
|
+
try {
|
|
325
|
+
const handle = await reader.open(sessionId, 'read');
|
|
326
|
+
try {
|
|
327
|
+
const { events } = await handle.read(0);
|
|
328
|
+
// Materialize only the window's requests: every request carries its
|
|
329
|
+
// whole context, so a long log rebuilt at once grows quadratically.
|
|
330
|
+
const hosts = events.filter(row => row.type === 'turn/start')
|
|
331
|
+
.map(row => row.data?.turn).filter((turn) => typeof turn === 'number');
|
|
332
|
+
// An external agent's own session: its requests reach only this log.
|
|
333
|
+
const provider = events.find(row => row.type === 'subagent/descriptor')?.data?.['provider'];
|
|
334
|
+
if (typeof provider === 'string' && !IN_PROCESS_PROVIDERS.has(provider))
|
|
335
|
+
this.store.markLogDriven(sessionId);
|
|
336
|
+
const keep = this.store.windowHosts(sessionId, hosts);
|
|
337
|
+
this.store.insertLogTurns(sessionId, reconstructSession(events, host => host === undefined || keep.has(host)));
|
|
338
|
+
for (const fact of replacementFactsOf(events))
|
|
339
|
+
this.store.noteReplacement(sessionId, fact);
|
|
340
|
+
for (const row of events) {
|
|
341
|
+
if (row.type.startsWith('room/'))
|
|
342
|
+
this.store.noteRoomEvent(sessionId, row.type, row.data, row.seq, row.time);
|
|
343
|
+
}
|
|
344
|
+
await this.resolveParent(sessionId, events, reader);
|
|
345
|
+
}
|
|
346
|
+
finally {
|
|
347
|
+
void handle.close().catch(() => undefined);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
catch (error) {
|
|
351
|
+
if (error instanceof Error && error.name === 'SessionPersistenceNotFoundError')
|
|
352
|
+
return;
|
|
353
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
354
|
+
this.store.markBackfillBroken(sessionId, reason);
|
|
355
|
+
this.logWarn(`context-lens: session "${sessionId}" log unreadable, history backfill skipped (${reason})`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* A session opened on its own whose log header names a `parentSession`:
|
|
360
|
+
* read the parent's log once for its title, the turn whose catalog row
|
|
361
|
+
* started this child, and that row's label. Best effort — an unreadable
|
|
362
|
+
* parent leaves just its id.
|
|
363
|
+
*/
|
|
364
|
+
async resolveParent(sessionId, events, reader) {
|
|
365
|
+
const header = events[0];
|
|
366
|
+
const parentId = header?.type === 'session' ? header.parentSession : undefined;
|
|
367
|
+
if (typeof parentId !== 'string' || parentId.length === 0 || this.store.hasParent(sessionId))
|
|
368
|
+
return;
|
|
369
|
+
const parent = { sessionId: parentId };
|
|
370
|
+
try {
|
|
371
|
+
const handle = await reader.open(parentId, 'read');
|
|
372
|
+
try {
|
|
373
|
+
const { events: rows } = await handle.read(0);
|
|
374
|
+
let turn;
|
|
375
|
+
for (const row of rows) {
|
|
376
|
+
const data = (row.data ?? {});
|
|
377
|
+
if (row.type === 'turn/start' && typeof data['turn'] === 'number')
|
|
378
|
+
turn = data['turn'];
|
|
379
|
+
if (row.type === 'session/title' && typeof data['title'] === 'string')
|
|
380
|
+
parent.title = data['title'];
|
|
381
|
+
if (row.type === 'subagent/catalog' && data['childId'] === sessionId) {
|
|
382
|
+
if (turn !== undefined)
|
|
383
|
+
parent.hostTurn = turn;
|
|
384
|
+
if (typeof data['label'] === 'string')
|
|
385
|
+
parent.label = data['label'];
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
finally {
|
|
390
|
+
void handle.close().catch(() => undefined);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
catch {
|
|
394
|
+
// The id alone still says whose child this is.
|
|
395
|
+
}
|
|
396
|
+
this.store.noteParent(sessionId, parent);
|
|
397
|
+
}
|
|
398
|
+
// ---------- queries (the Remote face delegates here) ----------
|
|
399
|
+
/**
|
|
400
|
+
* Light turn list of one session plus its backfill watermark. The optional
|
|
401
|
+
* {@link ScopeFilter} restricts rows and aggregates to the authorized
|
|
402
|
+
* scopes; absent is the unfiltered local semantics.
|
|
403
|
+
*/
|
|
404
|
+
async listTurns(sessionId, filter) {
|
|
405
|
+
await this.backfillMainSession(sessionId);
|
|
406
|
+
await this.refreshChildren(sessionId, filter);
|
|
407
|
+
return this.store.listTurns(sessionId, filter);
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Move the session's backfill window to the turns ending at `end` (the
|
|
411
|
+
* latest turns when null) and answer the refreshed turn list. Live turns
|
|
412
|
+
* stay; the window refills from the log in the same call.
|
|
413
|
+
*/
|
|
414
|
+
async loadTurns(sessionId, end, filter) {
|
|
415
|
+
const pending = this.refreshing.get(`main:${sessionId}`);
|
|
416
|
+
if (pending !== undefined)
|
|
417
|
+
await pending;
|
|
418
|
+
this.store.setWindow(sessionId, end ?? undefined);
|
|
419
|
+
return this.listTurns(sessionId, filter);
|
|
420
|
+
}
|
|
421
|
+
/** One turn's scope/request overview, without segments. */
|
|
422
|
+
async getTurn(sessionId, turnSeq, filter) {
|
|
423
|
+
await this.backfillMainSession(sessionId);
|
|
424
|
+
await this.refreshChildren(sessionId, filter);
|
|
425
|
+
return this.store.getTurn(sessionId, turnSeq, filter);
|
|
426
|
+
}
|
|
427
|
+
/** One request's full four-segment breakdown (a {@link ScopedRefusal} when the filter excludes its scope). */
|
|
428
|
+
async getRequest(sessionId, seq, filter) {
|
|
429
|
+
await this.backfillMainSession(sessionId);
|
|
430
|
+
await this.refreshChildren(sessionId, filter);
|
|
431
|
+
const request = this.store.getRequest(sessionId, seq, filter);
|
|
432
|
+
const tools = request !== undefined && 'prefix' in request ? request.prefix?.toolChanges : undefined;
|
|
433
|
+
if (tools === undefined || tools.length === 0)
|
|
434
|
+
return request;
|
|
435
|
+
return { ...request, prefix: { ...request.prefix, toolChanges: tools.map(tool => {
|
|
436
|
+
const owner = this.toolOwner(tool.name);
|
|
437
|
+
return owner === undefined ? tool : { ...tool, owner };
|
|
438
|
+
}) } };
|
|
439
|
+
}
|
|
440
|
+
/** The declaring package from a registered tool's host-side origin tag (capability-catalog convention); absent when untagged. */
|
|
441
|
+
toolOwner(name) {
|
|
442
|
+
try {
|
|
443
|
+
const registry = this.ctx.get('tools');
|
|
444
|
+
const definition = registry?.get?.(name);
|
|
445
|
+
if (typeof definition !== 'object' || definition === null)
|
|
446
|
+
return undefined;
|
|
447
|
+
const origin = definition[Symbol.for('dsh.tool.origin')];
|
|
448
|
+
return typeof origin?.owner === 'string' ? origin.owner : undefined;
|
|
449
|
+
}
|
|
450
|
+
catch {
|
|
451
|
+
return undefined;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
/** Read the session map through the same backfill and scope-filtered path. */
|
|
455
|
+
async getSessionTrend(sessionId, filter) {
|
|
456
|
+
await this.listTurns(sessionId, filter);
|
|
457
|
+
return this.store.getSessionTrend(sessionId, filter);
|
|
458
|
+
}
|
|
459
|
+
/** One turn's whole-turn timing aggregation. */
|
|
460
|
+
async getTurnTiming(sessionId, turnSeq, filter) {
|
|
461
|
+
await this.backfillMainSession(sessionId);
|
|
462
|
+
await this.refreshChildren(sessionId, filter);
|
|
463
|
+
return this.store.getTurnTiming(sessionId, turnSeq, filter);
|
|
464
|
+
}
|
|
465
|
+
/** Release every session buffer, listener, and open child-log handle (plugin dispose). */
|
|
466
|
+
dispose() {
|
|
467
|
+
for (const dispose of this.disposers.splice(0))
|
|
468
|
+
dispose();
|
|
469
|
+
this.store.dispose();
|
|
470
|
+
for (const handle of this.handles.values()) {
|
|
471
|
+
void handle.close().catch(() => undefined);
|
|
472
|
+
}
|
|
473
|
+
this.handles.clear();
|
|
474
|
+
this.cursors.clear();
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
/** The dispatch-time snapshot of one GenerateOptions (never mutates the frozen input). */
|
|
478
|
+
function snapshotOptions(options) {
|
|
479
|
+
return {
|
|
480
|
+
...options.sessionId === undefined ? {} : { sessionId: options.sessionId },
|
|
481
|
+
...options.purpose === undefined ? {} : { purpose: options.purpose },
|
|
482
|
+
config: {
|
|
483
|
+
provider: options.provider,
|
|
484
|
+
model: options.model,
|
|
485
|
+
...options.reasoningEffort === undefined ? {} : { reasoningEffort: options.reasoningEffort },
|
|
486
|
+
...options.temperature === undefined ? {} : { temperature: options.temperature },
|
|
487
|
+
...options.maxTokens === undefined ? {} : { maxTokens: options.maxTokens },
|
|
488
|
+
},
|
|
489
|
+
messages: options.messages.map(fromRequestMessage),
|
|
490
|
+
...options.system === undefined ? {} : { systemSlot: options.system },
|
|
491
|
+
...options.tools === undefined ? {} : { tools: options.tools },
|
|
492
|
+
};
|
|
493
|
+
}
|