oh-my-knowledge 0.51.2 → 0.52.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.
Files changed (49) hide show
  1. package/README.md +27 -6
  2. package/README.zh.md +27 -6
  3. package/dist/assets/agent-skills/omk/SKILL.md +8 -8
  4. package/dist/observability/codex-conversation-index.d.ts +88 -0
  5. package/dist/observability/codex-conversation-index.js +566 -0
  6. package/dist/observability/codex-protocol.d.ts +12 -0
  7. package/dist/observability/codex-protocol.js +78 -0
  8. package/dist/observability/codex-tool-status.d.ts +16 -0
  9. package/dist/observability/codex-tool-status.js +114 -0
  10. package/dist/observability/codex-trace-adapter.js +342 -119
  11. package/dist/observability/conversation-catalog.d.ts +38 -0
  12. package/dist/observability/conversation-catalog.js +573 -0
  13. package/dist/observability/conversation-index-process.d.ts +1 -0
  14. package/dist/observability/conversation-index-process.js +31 -0
  15. package/dist/observability/conversation-view-model.d.ts +5 -0
  16. package/dist/observability/conversation-view-model.js +100 -0
  17. package/dist/observability/experience.d.ts +3 -1
  18. package/dist/observability/experience.js +248 -7
  19. package/dist/observability/inbox.js +40 -2
  20. package/dist/observability/knowledge-debugger.d.ts +4 -0
  21. package/dist/observability/knowledge-debugger.js +364 -0
  22. package/dist/observability/polling-subscription-hub.d.ts +27 -0
  23. package/dist/observability/polling-subscription-hub.js +149 -0
  24. package/dist/observability/source-record-archive.d.ts +13 -0
  25. package/dist/observability/source-record-archive.js +324 -0
  26. package/dist/observability/task-window.d.ts +8 -0
  27. package/dist/observability/task-window.js +36 -0
  28. package/dist/observability/trace-ir.d.ts +66 -1
  29. package/dist/observability/trace-source.d.ts +1 -0
  30. package/dist/observability/trace-source.js +12 -5
  31. package/dist/observability/turn-index.d.ts +3 -0
  32. package/dist/observability/turn-index.js +251 -0
  33. package/dist/renderer/conversation-renderer.d.ts +9 -0
  34. package/dist/renderer/conversation-renderer.js +410 -0
  35. package/dist/renderer/icons.js +1 -0
  36. package/dist/renderer/inline-markdown.d.ts +6 -0
  37. package/dist/renderer/inline-markdown.js +158 -0
  38. package/dist/renderer/knowledge-debugger-renderer.d.ts +9 -0
  39. package/dist/renderer/knowledge-debugger-renderer.js +2280 -0
  40. package/dist/renderer/observation-inbox-renderer.js +9 -0
  41. package/dist/renderer/skill-list-renderer.js +2 -1
  42. package/dist/renderer/trajectory-live.d.ts +42 -0
  43. package/dist/renderer/trajectory-live.js +258 -0
  44. package/dist/renderer/trajectory-routing.d.ts +60 -0
  45. package/dist/renderer/trajectory-routing.js +351 -0
  46. package/dist/server/report-server.d.ts +4 -1
  47. package/dist/server/report-server.js +274 -6
  48. package/dist/types/observability.d.ts +225 -1
  49. package/package.json +8 -5
@@ -0,0 +1,364 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { resolveTaskWindow } from './task-window.js';
3
+ const AGENTS_CONTEXT_RE = /^# AGENTS\.md instructions for ([^\n]+)\n/gim;
4
+ const SKILL_PATH_RE = /((?:~|\.{0,2}|\/)?[^\s"'`]*\/skills\/(?:\.system\/)?([^/\s"'`]+)\/SKILL\.md)\b/i;
5
+ const MUTATING_TOOL_RE = /(?:^|[._-])(write|edit|delete|remove|move|rename|apply[_-]?patch)(?:$|[._-])/i;
6
+ const READ_ONLY_TOOL_RE = /(?:^|[._-])(read|search|query|fetch|get|list|find|glob|grep|view|open)(?:$|[._-])/i;
7
+ const READ_ONLY_SHELL_RE = /^\s*(?:cat|head|tail|sed\s+-n|rg|grep|find|ls|pwd|wc|stat|which|git\s+(?:status|diff|show|log|branch|rev-parse)|gh\s+(?:issue|pr|run|release|repo)\s+(?:view|list|status|checks?))\b/i;
8
+ export function buildKnowledgeDebuggerViewModel(session, targetTurnId, ingestion, sourceRecords = {
9
+ status: 'unavailable',
10
+ recordCount: 0,
11
+ records: [],
12
+ omittedRecordCount: 0,
13
+ byteCount: 0,
14
+ truncated: false,
15
+ reason: 'no_record_ranges',
16
+ }) {
17
+ const taskWindow = resolveTaskWindow(session, targetTurnId);
18
+ const normalizedEvents = taskWindow.events;
19
+ const timeline = taskWindow.semanticEvents.filter((event) => event.runtimeKind !== 'usage');
20
+ const knowledgeEvidence = projectKnowledgeEvidence(timeline);
21
+ const steps = buildTaskReplaySteps(session, timeline, knowledgeEvidence);
22
+ const notices = buildIntegrityNotices(taskWindow.scope, timeline, steps, ingestion);
23
+ const userEvents = timeline.filter((event) => event.kind === 'user_message');
24
+ const assistantEvents = timeline.filter((event) => event.kind === 'assistant_message');
25
+ const observedModels = timeline.reduce((models, event) => {
26
+ const eventModel = event.model?.trim();
27
+ if (eventModel && !models.includes(eventModel))
28
+ models.push(eventModel);
29
+ return models;
30
+ }, []);
31
+ return {
32
+ session,
33
+ taskScope: taskWindow.scope,
34
+ summary: {
35
+ userGoal: eventText(userEvents[0]),
36
+ finalResponse: eventText(assistantEvents.at(-1)),
37
+ observedStartTimestamp: timeline.find((event) => (event.timestamp && event.runtimeKind !== 'session_context'))?.timestamp
38
+ ?? timeline.find((event) => event.timestamp)?.timestamp,
39
+ observedEndTimestamp: [...timeline].reverse().find((event) => event.timestamp)?.timestamp,
40
+ toolCallCount: steps.filter((step) => step.stepKind === 'tool_exchange').length,
41
+ toolFailureCount: steps.filter((step) => step.stepKind === 'tool_exchange' && step.toolStatus === 'failure').length,
42
+ hasUserCorrection: steps.some((step) => step.stepKind === 'user_correction'),
43
+ observedModels,
44
+ },
45
+ steps,
46
+ normalizedEvents,
47
+ sourceRecords,
48
+ knowledgeEvidence,
49
+ integrity: {
50
+ status: notices.length > 0 ? 'partial' : 'complete',
51
+ notices,
52
+ },
53
+ };
54
+ }
55
+ export function buildTaskReplaySteps(session, timeline, knowledgeEvidence) {
56
+ const sorted = [...timeline].sort((a, b) => a.order - b.order);
57
+ const resultsByCall = new Map();
58
+ const matchedResultIds = new Set();
59
+ const knowledgeIdsByEvent = knowledgeEvidence.reduce((map, evidence) => {
60
+ for (const ref of evidence.evidenceRefs) {
61
+ map.set(ref.id, [...(map.get(ref.id) ?? []), evidence.id]);
62
+ }
63
+ return map;
64
+ }, new Map());
65
+ const correctionIds = correctionEventIds(session, sorted);
66
+ for (const event of sorted) {
67
+ if (event.kind !== 'tool_result')
68
+ continue;
69
+ const key = toolCorrelationKey(event);
70
+ resultsByCall.set(key, [...(resultsByCall.get(key) ?? []), event]);
71
+ }
72
+ const steps = [];
73
+ let sawUserRequest = false;
74
+ for (const event of sorted) {
75
+ if (event.kind === 'tool_result' && matchedResultIds.has(event.id))
76
+ continue;
77
+ if (event.kind === 'tool_use') {
78
+ const result = resultsByCall.get(toolCorrelationKey(event))?.find((item) => !matchedResultIds.has(item.id));
79
+ if (result)
80
+ matchedResultIds.add(result.id);
81
+ const events = result ? [event, result] : [event];
82
+ steps.push({
83
+ id: `step:${event.id}`,
84
+ order: event.order,
85
+ stepKind: 'tool_exchange',
86
+ timestamp: event.timestamp,
87
+ title: event.toolName ?? event.label ?? 'Tool call',
88
+ events,
89
+ toolStatus: result?.toolStatus ?? (result?.isError ? 'failure' : event.toolStatus ?? 'unknown'),
90
+ knowledgeEvidenceIds: knowledgeIdsForEvents(events, knowledgeIdsByEvent),
91
+ });
92
+ continue;
93
+ }
94
+ const stepKind = stepKindForEvent(event, sawUserRequest, correctionIds);
95
+ if (event.kind === 'user_message' && !sawUserRequest)
96
+ sawUserRequest = true;
97
+ steps.push({
98
+ id: `step:${event.id}`,
99
+ order: event.order,
100
+ stepKind,
101
+ timestamp: event.timestamp,
102
+ title: event.toolName ?? event.label ?? event.kind,
103
+ events: [event],
104
+ ...(event.kind === 'tool_result'
105
+ ? { toolStatus: event.toolStatus ?? (event.isError ? 'failure' : 'unknown') }
106
+ : {}),
107
+ knowledgeEvidenceIds: knowledgeIdsForEvents([event], knowledgeIdsByEvent),
108
+ });
109
+ }
110
+ return steps;
111
+ }
112
+ export function projectKnowledgeEvidence(timeline) {
113
+ const candidates = [];
114
+ const toolCalls = new Map();
115
+ for (const event of timeline) {
116
+ if (event.kind === 'tool_use') {
117
+ toolCalls.set(toolCorrelationKey(event), event);
118
+ continue;
119
+ }
120
+ if (event.kind === 'runtime_context') {
121
+ const text = event.fullText ?? event.snippet ?? '';
122
+ const agentsMatches = [...text.matchAll(AGENTS_CONTEXT_RE)];
123
+ for (const [index, agentsMatch] of agentsMatches.entries()) {
124
+ if (!agentsMatch[1])
125
+ continue;
126
+ const sectionStart = agentsMatch.index ?? 0;
127
+ const sectionEnd = agentsMatches[index + 1]?.index ?? text.length;
128
+ const section = text.slice(sectionStart, sectionEnd).trim();
129
+ candidates.push({
130
+ knowledgeKind: 'project_instruction',
131
+ accessKind: 'injected',
132
+ label: 'AGENTS.md',
133
+ sourceLocator: `runtime-context:${agentsMatch[1].trim()}`,
134
+ contentHash: contentHash(section),
135
+ firstSeen: event.timestamp,
136
+ lastSeen: event.timestamp,
137
+ evidenceRef: evidenceRef(event),
138
+ });
139
+ }
140
+ continue;
141
+ }
142
+ if (event.kind === 'skill_context') {
143
+ const text = event.fullText ?? event.snippet ?? '';
144
+ candidates.push({
145
+ knowledgeKind: 'skill',
146
+ accessKind: 'injected',
147
+ label: event.label || 'Skill context',
148
+ contentHash: text ? contentHash(text) : undefined,
149
+ firstSeen: event.timestamp,
150
+ lastSeen: event.timestamp,
151
+ evidenceRef: evidenceRef(event),
152
+ });
153
+ continue;
154
+ }
155
+ if (event.kind !== 'tool_result')
156
+ continue;
157
+ const call = toolCalls.get(toolCorrelationKey(event));
158
+ if (!call)
159
+ continue;
160
+ const output = event.fullText ?? event.snippet ?? '';
161
+ if (!output.trim())
162
+ continue;
163
+ const callText = call.fullText ?? call.snippet ?? '';
164
+ const skillPath = SKILL_PATH_RE.exec(callText);
165
+ if (skillPath?.[1] && skillPath[2]) {
166
+ candidates.push({
167
+ knowledgeKind: 'skill',
168
+ accessKind: 'read',
169
+ label: skillPath[2],
170
+ sourceLocator: skillPath[1],
171
+ contentHash: contentHash(output),
172
+ firstSeen: event.timestamp ?? call.timestamp,
173
+ lastSeen: event.timestamp ?? call.timestamp,
174
+ evidenceRef: evidenceRef(event),
175
+ });
176
+ continue;
177
+ }
178
+ const toolName = call.toolName ?? call.label ?? 'tool';
179
+ if (!isKnowledgeReturningCall(toolName, callText))
180
+ continue;
181
+ candidates.push({
182
+ knowledgeKind: 'runtime_evidence',
183
+ accessKind: 'returned',
184
+ label: toolName,
185
+ sourceLocator: sourceLocator(callText),
186
+ contentHash: contentHash(output),
187
+ firstSeen: event.timestamp ?? call.timestamp,
188
+ lastSeen: event.timestamp ?? call.timestamp,
189
+ evidenceRef: evidenceRef(event),
190
+ });
191
+ }
192
+ return aggregateCandidates(candidates);
193
+ }
194
+ function isKnowledgeReturningCall(toolName, callText) {
195
+ if (MUTATING_TOOL_RE.test(toolName))
196
+ return false;
197
+ if (READ_ONLY_TOOL_RE.test(toolName))
198
+ return true;
199
+ const command = sourceLocator(callText);
200
+ return command ? READ_ONLY_SHELL_RE.test(command) : false;
201
+ }
202
+ function buildIntegrityNotices(taskScope, timeline, steps, ingestion) {
203
+ const notices = [];
204
+ if (taskScope.basis === 'unresolved') {
205
+ notices.push({ code: 'task_boundary_unavailable', count: 1 });
206
+ }
207
+ const omittedSemanticEvents = taskScope.normalizedEventCount - taskScope.semanticEventCount;
208
+ if (omittedSemanticEvents > 0) {
209
+ notices.push({ code: 'timeline_truncated', count: omittedSemanticEvents });
210
+ }
211
+ if (ingestion?.malformedRecordCount)
212
+ notices.push({ code: 'malformed_records', count: ingestion.malformedRecordCount });
213
+ if (ingestion?.ignoredValueCount)
214
+ notices.push({ code: 'ignored_values', count: ingestion.ignoredValueCount });
215
+ if (ingestion?.unknownEventCount)
216
+ notices.push({ code: 'unknown_events', count: ingestion.unknownEventCount });
217
+ const unmatchedCalls = steps.filter((step) => step.stepKind === 'tool_exchange' && step.events.length === 1).length;
218
+ if (unmatchedCalls > 0)
219
+ notices.push({ code: 'unmatched_tool_calls', count: unmatchedCalls });
220
+ const unmatchedResults = steps.filter((step) => step.stepKind === 'unmatched_tool_result').length;
221
+ if (unmatchedResults > 0)
222
+ notices.push({ code: 'unmatched_tool_results', count: unmatchedResults });
223
+ const missingTimestamps = timeline.filter((event) => !event.timestamp).length;
224
+ if (missingTimestamps > 0)
225
+ notices.push({ code: 'missing_timestamps', count: missingTimestamps });
226
+ return notices;
227
+ }
228
+ function correctionEventIds(session, timeline) {
229
+ const ids = new Set();
230
+ for (const episode of session.sessionStory?.episodes ?? []) {
231
+ for (const signal of episode.feedbackSignals) {
232
+ if (signal.type === 'correction' || signal.type === 'frustration' || signal.type === 'interruption') {
233
+ ids.add(signal.evidenceRef.id);
234
+ }
235
+ }
236
+ }
237
+ const userCorrectionCount = session.indicators?.userCorrectionCount ?? 0;
238
+ if (ids.size > 0 || userCorrectionCount === 0)
239
+ return ids;
240
+ const lastAssistantOrder = Math.max(-1, ...timeline.filter((event) => event.kind === 'assistant_message').map((event) => event.order));
241
+ const correctionCandidates = timeline
242
+ .filter((event) => event.kind === 'user_message' && event.order > lastAssistantOrder)
243
+ .slice(-userCorrectionCount);
244
+ for (const event of correctionCandidates)
245
+ ids.add(event.id);
246
+ return ids;
247
+ }
248
+ function stepKindForEvent(event, sawUserRequest, correctionIds) {
249
+ if (event.kind === 'user_message') {
250
+ if (correctionIds.has(event.id))
251
+ return 'user_correction';
252
+ return sawUserRequest ? 'user_message' : 'user_request';
253
+ }
254
+ if (event.kind === 'assistant_message')
255
+ return 'assistant_message';
256
+ if (event.kind === 'model_activity')
257
+ return 'model_activity';
258
+ if (event.kind === 'lifecycle')
259
+ return 'lifecycle';
260
+ if (event.kind === 'runtime_context')
261
+ return 'runtime_context';
262
+ if (event.kind === 'skill_context')
263
+ return 'skill_context';
264
+ if (event.kind === 'tool_result')
265
+ return 'unmatched_tool_result';
266
+ if (event.kind === 'observation')
267
+ return 'observation';
268
+ return 'system_event';
269
+ }
270
+ function knowledgeIdsForEvents(events, knowledgeIdsByEvent) {
271
+ return [...new Set(events.flatMap((event) => knowledgeIdsByEvent.get(event.id) ?? []))];
272
+ }
273
+ function aggregateCandidates(candidates) {
274
+ const aggregated = new Map();
275
+ for (const candidate of candidates) {
276
+ const identity = [
277
+ candidate.knowledgeKind,
278
+ candidate.accessKind,
279
+ candidate.sourceLocator ?? candidate.label,
280
+ candidate.contentHash ?? '',
281
+ ].join('\u0000');
282
+ const id = `knowledge:${createHash('sha256').update(identity).digest('hex').slice(0, 20)}`;
283
+ const current = aggregated.get(id);
284
+ if (current) {
285
+ current.accessCount += 1;
286
+ current.evidenceRefs.push(candidate.evidenceRef);
287
+ if (candidate.firstSeen && (!current.firstSeen || candidate.firstSeen < current.firstSeen)) {
288
+ current.firstSeen = candidate.firstSeen;
289
+ }
290
+ if (candidate.lastSeen && (!current.lastSeen || candidate.lastSeen > current.lastSeen)) {
291
+ current.lastSeen = candidate.lastSeen;
292
+ }
293
+ continue;
294
+ }
295
+ aggregated.set(id, {
296
+ id,
297
+ knowledgeKind: candidate.knowledgeKind,
298
+ accessKind: candidate.accessKind,
299
+ label: candidate.label,
300
+ sourceLocator: candidate.sourceLocator,
301
+ contentHash: candidate.contentHash,
302
+ firstSeen: candidate.firstSeen,
303
+ lastSeen: candidate.lastSeen,
304
+ accessCount: 1,
305
+ evidenceRefs: [candidate.evidenceRef],
306
+ });
307
+ }
308
+ return [...aggregated.values()].sort((a, b) => {
309
+ const aTime = a.firstSeen ?? '';
310
+ const bTime = b.firstSeen ?? '';
311
+ return aTime.localeCompare(bTime) || a.label.localeCompare(b.label);
312
+ });
313
+ }
314
+ function toolCorrelationKey(event) {
315
+ return event.callInstanceId ?? event.toolUseId ?? event.id;
316
+ }
317
+ function sourceLocator(inputText) {
318
+ let parsed;
319
+ try {
320
+ parsed = JSON.parse(inputText);
321
+ }
322
+ catch {
323
+ return undefined;
324
+ }
325
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
326
+ return undefined;
327
+ const input = parsed;
328
+ for (const key of ['file_path', 'path', 'url', 'query', 'command', 'cmd']) {
329
+ const value = input[key];
330
+ if (typeof value === 'string' && value.trim())
331
+ return value.trim().slice(0, 500);
332
+ }
333
+ return undefined;
334
+ }
335
+ function eventText(event) {
336
+ const text = event?.fullText ?? event?.snippet;
337
+ return text?.trim() || undefined;
338
+ }
339
+ function contentHash(value) {
340
+ return createHash('sha256').update(value).digest('hex');
341
+ }
342
+ function evidenceRef(event) {
343
+ return {
344
+ id: event.id,
345
+ kind: event.kind,
346
+ traceId: event.traceId,
347
+ sourceTrace: event.sourceTrace,
348
+ sessionId: event.sessionId,
349
+ traceRole: event.traceRole,
350
+ traceLabel: event.traceLabel,
351
+ messageIndex: event.messageIndex,
352
+ logicalMessageIndex: event.logicalMessageIndex,
353
+ sourceLineIndex: event.sourceLineIndex,
354
+ messageUuid: event.messageUuid,
355
+ sourceType: event.sourceType,
356
+ callInstanceId: event.callInstanceId,
357
+ toolUseId: event.toolUseId,
358
+ timestamp: event.timestamp,
359
+ role: event.role,
360
+ runtimeKind: event.runtimeKind,
361
+ label: event.label,
362
+ snippet: event.snippet,
363
+ };
364
+ }
@@ -0,0 +1,27 @@
1
+ export interface PollingSnapshot<T> {
2
+ revision: string;
3
+ terminal: boolean;
4
+ value: T;
5
+ }
6
+ export type PollingSnapshotLoader<T> = (previous: PollingSnapshot<T> | undefined) => Promise<PollingSnapshot<T>>;
7
+ export interface PollingSnapshotObserver<T> {
8
+ next(snapshot: PollingSnapshot<T>): void;
9
+ complete?(): void;
10
+ error?(cause: unknown): void;
11
+ }
12
+ export interface PollingSubscriptionOptions {
13
+ signal?: AbortSignal;
14
+ }
15
+ /**
16
+ * Shares one sequential poll loop between all subscribers of the same key.
17
+ * The hub owns scheduling and cleanup; source adapters only produce snapshots.
18
+ */
19
+ export declare class PollingSubscriptionHub<T> {
20
+ private readonly intervalMs;
21
+ private readonly entries;
22
+ constructor(intervalMs?: number);
23
+ subscribe(key: string, loader: PollingSnapshotLoader<T>, observer: PollingSnapshotObserver<T>, options?: PollingSubscriptionOptions): Promise<() => void>;
24
+ close(): void;
25
+ private poll;
26
+ private dispose;
27
+ }
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Shares one sequential poll loop between all subscribers of the same key.
3
+ * The hub owns scheduling and cleanup; source adapters only produce snapshots.
4
+ */
5
+ export class PollingSubscriptionHub {
6
+ intervalMs;
7
+ entries = new Map();
8
+ constructor(intervalMs = 750) {
9
+ this.intervalMs = intervalMs;
10
+ }
11
+ async subscribe(key, loader, observer, options = {}) {
12
+ let entry = this.entries.get(key);
13
+ if (!entry) {
14
+ let resolveReady;
15
+ let rejectReady;
16
+ const ready = new Promise((resolve, reject) => {
17
+ resolveReady = resolve;
18
+ rejectReady = reject;
19
+ });
20
+ entry = {
21
+ active: true,
22
+ observers: new Set(),
23
+ loader,
24
+ ready,
25
+ resolveReady,
26
+ rejectReady,
27
+ };
28
+ this.entries.set(key, entry);
29
+ void this.poll(key, entry, true);
30
+ }
31
+ entry.observers.add(observer);
32
+ if (entry.latest)
33
+ notifyNext(observer, entry.latest);
34
+ let subscribed = true;
35
+ let rejectAborted;
36
+ const aborted = options.signal
37
+ ? new Promise((_resolve, reject) => {
38
+ rejectAborted = reject;
39
+ })
40
+ : undefined;
41
+ const unsubscribe = () => {
42
+ if (!subscribed)
43
+ return;
44
+ subscribed = false;
45
+ options.signal?.removeEventListener('abort', abort);
46
+ entry?.observers.delete(observer);
47
+ if (entry && entry.observers.size === 0)
48
+ this.dispose(key, entry);
49
+ };
50
+ const abort = () => {
51
+ unsubscribe();
52
+ rejectAborted?.(subscriptionAbortError());
53
+ };
54
+ if (options.signal) {
55
+ options.signal.addEventListener('abort', abort, { once: true });
56
+ if (options.signal.aborted)
57
+ abort();
58
+ }
59
+ try {
60
+ await (aborted ? Promise.race([entry.ready, aborted]) : entry.ready);
61
+ }
62
+ catch (cause) {
63
+ unsubscribe();
64
+ throw cause;
65
+ }
66
+ return unsubscribe;
67
+ }
68
+ close() {
69
+ for (const [key, entry] of this.entries)
70
+ this.dispose(key, entry);
71
+ }
72
+ async poll(key, entry, initial) {
73
+ try {
74
+ const snapshot = await entry.loader(entry.latest);
75
+ if (!entry.active)
76
+ return;
77
+ const changed = snapshot.revision !== entry.latest?.revision;
78
+ entry.latest = snapshot;
79
+ if (changed) {
80
+ for (const observer of entry.observers)
81
+ notifyNext(observer, snapshot);
82
+ }
83
+ if (initial)
84
+ entry.resolveReady();
85
+ if (snapshot.terminal) {
86
+ for (const observer of entry.observers)
87
+ notifyComplete(observer);
88
+ this.dispose(key, entry);
89
+ return;
90
+ }
91
+ entry.timer = setTimeout(() => {
92
+ entry.timer = undefined;
93
+ void this.poll(key, entry, false);
94
+ }, this.intervalMs);
95
+ entry.timer.unref?.();
96
+ }
97
+ catch (cause) {
98
+ if (!entry.active)
99
+ return;
100
+ if (initial)
101
+ entry.rejectReady(cause);
102
+ else
103
+ for (const observer of entry.observers)
104
+ notifyError(observer, cause);
105
+ this.dispose(key, entry);
106
+ }
107
+ }
108
+ dispose(key, entry) {
109
+ if (!entry.active)
110
+ return;
111
+ entry.active = false;
112
+ entry.rejectReady(subscriptionAbortError());
113
+ if (entry.timer)
114
+ clearTimeout(entry.timer);
115
+ entry.timer = undefined;
116
+ entry.observers.clear();
117
+ if (this.entries.get(key) === entry)
118
+ this.entries.delete(key);
119
+ }
120
+ }
121
+ function subscriptionAbortError() {
122
+ const error = new Error('Polling subscription closed before the initial snapshot was ready');
123
+ error.name = 'AbortError';
124
+ return error;
125
+ }
126
+ function notifyNext(observer, snapshot) {
127
+ try {
128
+ observer.next(snapshot);
129
+ }
130
+ catch {
131
+ // One consumer must not stop updates for other subscribers.
132
+ }
133
+ }
134
+ function notifyComplete(observer) {
135
+ try {
136
+ observer.complete?.();
137
+ }
138
+ catch {
139
+ // One consumer must not stop completion for other subscribers.
140
+ }
141
+ }
142
+ function notifyError(observer, cause) {
143
+ try {
144
+ observer.error?.(cause);
145
+ }
146
+ catch {
147
+ // One consumer must not stop error delivery for other subscribers.
148
+ }
149
+ }
@@ -0,0 +1,13 @@
1
+ import type { ObservationInboxReport, ObservationSourceRecord, ObservationSourceRecordArchiveRef, ObservationSourceRecordArchiveView } from '../types/index.js';
2
+ /**
3
+ * Persist bounded source records beside a report. Absolute source paths are
4
+ * consumed only during trusted ingest; Studio later reads relative sidecars.
5
+ */
6
+ export declare function writeObservationSourceRecordArchives(report: ObservationInboxReport, outDir: string, reportPath: string): ObservationSourceRecordArchiveRef[];
7
+ export declare function loadObservationSourceRecordArchive(ref: ObservationSourceRecordArchiveRef | undefined, observationsDir: string): ObservationSourceRecordArchiveView;
8
+ /**
9
+ * Build the page-safe archive metadata without reading the potentially large
10
+ * sidecar. The source-record endpoint resolves the same ref on demand.
11
+ */
12
+ export declare function summarizeObservationSourceRecordArchive(ref: ObservationSourceRecordArchiveRef | undefined): ObservationSourceRecordArchiveView;
13
+ export declare function observationSourceRecordFromLine(line: string, sourceIndex: number, traceId: string, sourceTrace: string): ObservationSourceRecord;