oh-my-knowledge 0.52.3 → 0.54.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 (106) hide show
  1. package/README.md +21 -1
  2. package/README.zh.md +21 -1
  3. package/dist/assets/agent-skills/omk/SKILL.md +6 -0
  4. package/dist/assets/agent-skills/omk/references/commands.md +1 -1
  5. package/dist/authoring/evolver.js +1 -1
  6. package/dist/authoring/generator.js +1 -1
  7. package/dist/cli/commands/eval/index.js +7 -6
  8. package/dist/cli/commands/install.js +5 -1
  9. package/dist/cli/lib/generation-failure-hint.js +6 -8
  10. package/dist/cli/lib/runtime-defaults.d.ts +2 -0
  11. package/dist/cli/lib/runtime-defaults.js +7 -4
  12. package/dist/dsh-plugin/cordis.patch.yml +3 -0
  13. package/dist/dsh-plugin/host-executor.d.ts +93 -0
  14. package/dist/dsh-plugin/host-executor.js +232 -0
  15. package/dist/dsh-plugin/index.d.ts +30 -0
  16. package/dist/dsh-plugin/index.js +275 -0
  17. package/dist/dsh-plugin/observe.d.ts +47 -0
  18. package/dist/dsh-plugin/observe.js +359 -0
  19. package/dist/dsh-plugin/protocol.d.ts +21 -0
  20. package/dist/dsh-plugin/protocol.js +229 -0
  21. package/dist/dsh-plugin/trace-adapter.d.ts +48 -0
  22. package/dist/dsh-plugin/trace-adapter.js +590 -0
  23. package/dist/eval-core/comparability.js +3 -0
  24. package/dist/eval-core/evaluation-execution.js +5 -13
  25. package/dist/eval-core/evaluation-reporting.d.ts +7 -4
  26. package/dist/eval-core/evaluation-reporting.js +39 -18
  27. package/dist/eval-core/judge-independence.d.ts +1 -1
  28. package/dist/eval-core/judge-independence.js +1 -1
  29. package/dist/eval-core/report-document.js +6 -0
  30. package/dist/eval-core/resume-compatibility.d.ts +1 -0
  31. package/dist/eval-core/resume-compatibility.js +5 -4
  32. package/dist/eval-workflows/batch-evaluation-workflow.js +1 -1
  33. package/dist/eval-workflows/evaluation-pipeline/preflight-warnings.js +4 -2
  34. package/dist/eval-workflows/evaluation-pipeline.d.ts +3 -1
  35. package/dist/eval-workflows/evaluation-pipeline.js +7 -3
  36. package/dist/eval-workflows/run-evaluation.d.ts +4 -2
  37. package/dist/eval-workflows/run-evaluation.js +7 -3
  38. package/dist/executors/{anthropic-api.d.ts → anthropic/api.d.ts} +1 -1
  39. package/dist/executors/{anthropic-api.js → anthropic/api.js} +4 -2
  40. package/dist/executors/{claude-cli.d.ts → anthropic/claude/cli.d.ts} +1 -1
  41. package/dist/executors/{claude-cli.js → anthropic/claude/cli.js} +5 -3
  42. package/dist/executors/anthropic/claude/protocol.d.ts +87 -0
  43. package/dist/executors/{claude-protocol.js → anthropic/claude/protocol.js} +6 -6
  44. package/dist/executors/{claude-sdk.d.ts → anthropic/claude/sdk.d.ts} +2 -2
  45. package/dist/executors/{claude-sdk.js → anthropic/claude/sdk.js} +8 -5
  46. package/dist/executors/anthropic/claude/trace.d.ts +9 -0
  47. package/dist/executors/{claude-sdk-trace.js → anthropic/claude/trace.js} +5 -5
  48. package/dist/executors/{capabilities.d.ts → core/capabilities.d.ts} +3 -5
  49. package/dist/executors/{capabilities.js → core/capabilities.js} +4 -11
  50. package/dist/executors/core/http.d.ts +6 -0
  51. package/dist/executors/core/http.js +19 -0
  52. package/dist/executors/core/limits.d.ts +2 -0
  53. package/dist/executors/core/limits.js +2 -0
  54. package/dist/executors/core/optional-dependencies.d.ts +7 -0
  55. package/dist/executors/core/optional-dependencies.js +35 -0
  56. package/dist/executors/core/registry.d.ts +145 -0
  57. package/dist/executors/core/registry.js +127 -0
  58. package/dist/executors/core/runtime-fingerprint.d.ts +13 -0
  59. package/dist/executors/{runtime-fingerprint.js → core/runtime-fingerprint.js} +119 -59
  60. package/dist/executors/core/runtime.d.ts +12 -0
  61. package/dist/executors/core/runtime.js +61 -0
  62. package/dist/executors/core/subprocess.d.ts +44 -0
  63. package/dist/executors/{shared.js → core/subprocess.js} +20 -156
  64. package/dist/executors/index.d.ts +4 -4
  65. package/dist/executors/index.js +26 -15
  66. package/dist/executors/{openai-api.d.ts → openai/api.d.ts} +1 -1
  67. package/dist/executors/{openai-api.js → openai/api.js} +4 -2
  68. package/dist/executors/{codex-cli.d.ts → openai/codex/cli.d.ts} +3 -3
  69. package/dist/executors/{codex-cli.js → openai/codex/cli.js} +5 -3
  70. package/dist/executors/openai/codex/protocol.d.ts +72 -0
  71. package/dist/executors/{codex-protocol.js → openai/codex/protocol.js} +33 -2
  72. package/dist/executors/{codex-sdk.d.ts → openai/codex/sdk.d.ts} +18 -4
  73. package/dist/executors/{codex-sdk.js → openai/codex/sdk.js} +7 -4
  74. package/dist/executors/{codex-cli-trace.d.ts → openai/codex/trace.d.ts} +2 -2
  75. package/dist/executors/{codex-cli-trace.js → openai/codex/trace.js} +4 -4
  76. package/dist/executors/{script.d.ts → script/index.d.ts} +1 -1
  77. package/dist/executors/{script.js → script/index.js} +6 -4
  78. package/dist/grading/judge.d.ts +1 -1
  79. package/dist/grading/judge.js +1 -1
  80. package/dist/observability/conversation-catalog.js +1 -1
  81. package/dist/observability/experience.js +4 -0
  82. package/dist/observability/inbox.d.ts +4 -1
  83. package/dist/observability/inbox.js +7 -2
  84. package/dist/observability/trace-ir.d.ts +6 -3
  85. package/dist/observability/turn-index.js +4 -0
  86. package/dist/renderer/conversation-renderer.js +20 -6
  87. package/dist/renderer/html-renderer.js +30 -11
  88. package/dist/renderer/knowledge-debugger-renderer.js +6 -1
  89. package/dist/renderer/observation-inbox-renderer.js +2 -2
  90. package/dist/renderer/trajectory-live.d.ts +1 -0
  91. package/dist/renderer/trajectory-live.js +5 -2
  92. package/dist/shared/trace-source-kind.js +1 -0
  93. package/dist/types/executor.d.ts +13 -2
  94. package/dist/types/judge.d.ts +2 -2
  95. package/dist/types/observability.d.ts +1 -1
  96. package/dist/types/trace.d.ts +1 -1
  97. package/package.json +25 -5
  98. package/dist/executors/claude-protocol.d.ts +0 -28
  99. package/dist/executors/claude-sdk-trace.d.ts +0 -9
  100. package/dist/executors/codex-protocol.d.ts +0 -24
  101. package/dist/executors/gemini.d.ts +0 -2
  102. package/dist/executors/gemini.js +0 -156
  103. package/dist/executors/runtime-fingerprint.d.ts +0 -6
  104. package/dist/executors/shared.d.ts +0 -226
  105. /package/dist/executors/{script-command.d.ts → script/command.d.ts} +0 -0
  106. /package/dist/executors/{script-command.js → script/command.js} +0 -0
@@ -0,0 +1,275 @@
1
+ import { dirname, join, resolve } from 'node:path';
2
+ import { computeVerdict } from '../eval-core/verdict.js';
3
+ import { configVariantsToSpecs, loadEvalConfig } from '../inputs/eval-config.js';
4
+ import { runEvaluation, runMultiple } from '../eval-workflows/run-evaluation.js';
5
+ import { createDshHostExecutor, } from './host-executor.js';
6
+ import { createDshConversationCatalog, dshTraceIngestionSummary, listDshObserveCandidates, readDshObservedGroup, } from './observe.js';
7
+ export const name = 'omk-dsh-plugin';
8
+ export const inject = ['agentPresets', 'agents', 'commands', 'tools'];
9
+ const HELP = [
10
+ 'OMK 已接入当前 DeepSeek Harness。',
11
+ '',
12
+ '受控评测:/omk eval <eval.yaml>',
13
+ '查看最近任务:/omk observe',
14
+ '打开任务轨迹:/omk observe <session-id>',
15
+ '',
16
+ '文档:https://oh-my-knowledge.pages.dev/zh/reference/executors',
17
+ ].join('\n');
18
+ const USAGE = '用法:/omk eval <eval.yaml> 或 /omk observe [session-id];运行 /omk 查看帮助。';
19
+ function unquote(value) {
20
+ if ((value.startsWith('"') && value.endsWith('"'))
21
+ || (value.startsWith("'") && value.endsWith("'"))) {
22
+ return value.slice(1, -1);
23
+ }
24
+ return value;
25
+ }
26
+ function parseCommand(rawInput) {
27
+ const normalized = rawInput.trim();
28
+ if (!normalized || normalized === 'help')
29
+ return { commandKind: 'help' };
30
+ const evalMatch = /^\s*eval\s+(.+?)\s*$/u.exec(rawInput);
31
+ const evalPath = evalMatch?.[1]?.trim();
32
+ if (evalPath)
33
+ return { commandKind: 'eval', path: unquote(evalPath) };
34
+ const observeMatch = /^\s*observe(?:\s+(.+?))?\s*$/u.exec(rawInput);
35
+ if (!observeMatch)
36
+ return undefined;
37
+ const sessionId = observeMatch[1]?.trim();
38
+ return {
39
+ commandKind: 'observe',
40
+ ...(sessionId ? { sessionId: unquote(sessionId) } : {}),
41
+ };
42
+ }
43
+ function projectRoot(configPath) {
44
+ return dirname(configPath);
45
+ }
46
+ function modelFor(agent, configured) {
47
+ const model = configured?.trim() || agent.options.model?.trim();
48
+ if (!model) {
49
+ throw new Error('当前 DSH session 没有可继承的模型,且 eval.yaml 未配置 model。');
50
+ }
51
+ return model;
52
+ }
53
+ function normalizeJudgeModels(configured, model) {
54
+ if (!configured || configured.length === 0)
55
+ return [{ executor: 'dsh-host', model }];
56
+ return configured.map((judge) => {
57
+ if (judge.executor === 'dsh-host') {
58
+ throw new Error('dsh-host 是 OMK 内部执行器标识;评委要复用当前 DSH 时,请使用 executor: dsh 或省略 judgeModels。');
59
+ }
60
+ return {
61
+ ...judge,
62
+ executor: judge.executor === 'dsh' ? 'dsh-host' : judge.executor,
63
+ };
64
+ });
65
+ }
66
+ async function executeEvalCommand(ctx, invocation, requestedPath) {
67
+ const cwd = invocation.agent.session.header.cwd ?? process.cwd();
68
+ const configPath = resolve(cwd, requestedPath);
69
+ const config = loadEvalConfig(configPath);
70
+ if (config.executor) {
71
+ return {
72
+ kind: 'error',
73
+ text: 'DSH 内运行 OMK 时,被测执行器固定为当前 DSH;请删除 eval.yaml 中的顶层 executor。',
74
+ };
75
+ }
76
+ if (config.effort) {
77
+ return {
78
+ kind: 'error',
79
+ text: 'DSH 的 reasoning effort 是 provider-owned 枚举,当前无法与 OMK 五档无损映射;请删除 eval.yaml 中的 effort,并在 DSH profile 中固定模型推理配置。',
80
+ };
81
+ }
82
+ const model = modelFor(invocation.agent, config.model);
83
+ const executor = createDshHostExecutor(ctx, {
84
+ parentAgent: invocation.agent,
85
+ signal: invocation.signal,
86
+ });
87
+ const judgeModels = normalizeJudgeModels(config.judgeModels, model);
88
+ const currentSessionProvesConnectivity = config.model === undefined
89
+ && ((config.noJudge ?? false) || judgeModels.every((judge) => (judge.executor === 'dsh-host' && judge.model === model)));
90
+ const root = projectRoot(configPath);
91
+ const outputDir = join(root, '.omk', 'reports');
92
+ const options = {
93
+ samplesPath: config.samples,
94
+ skillDir: join(root, 'skills'),
95
+ variantSpecs: configVariantsToSpecs(config.variants),
96
+ model,
97
+ outputDir,
98
+ noJudge: config.noJudge ?? false,
99
+ concurrency: config.concurrency ?? 1,
100
+ timeoutMs: config.timeoutMs,
101
+ noCache: config.noCache ?? false,
102
+ executorName: 'dsh-host',
103
+ executorOverrides: { 'dsh-host': executor },
104
+ judgeModels,
105
+ skipConnectivity: currentSessionProvesConnectivity,
106
+ skipDoctor: config.skipDoctor ?? false,
107
+ lang: 'zh',
108
+ mcpConfig: config.mcpConfig,
109
+ bootstrap: config.bootstrap ?? true,
110
+ bootstrapSamples: config.bootstrapSamples,
111
+ holdoutRatio: config.holdoutRatio,
112
+ judgeRepeat: config.judgeRepeat,
113
+ lengthDebias: config.lengthDebias,
114
+ budget: config.budget,
115
+ strictBaseline: config.strictBaseline,
116
+ noDiagnostic: config.noDiagnostic,
117
+ };
118
+ const result = config.repeat && config.repeat > 1
119
+ ? await runMultiple({ ...options, repeat: config.repeat })
120
+ : await runEvaluation(options);
121
+ const report = result.report;
122
+ const goldMessages = [];
123
+ if (config.goldDir) {
124
+ const { attachGoldAgreementToReport } = await import('../grading/gold-cli.js');
125
+ const gold = attachGoldAgreementToReport({
126
+ report,
127
+ goldDir: config.goldDir,
128
+ outputDir,
129
+ samples: config.bootstrapSamples,
130
+ });
131
+ if (gold.result && gold.gold) {
132
+ const alpha = Number.isFinite(gold.result.agreement.alpha)
133
+ ? gold.result.agreement.alpha.toFixed(3)
134
+ : '不可计算';
135
+ goldMessages.push(`Gold 一致性:Krippendorff α=${alpha},N=${gold.result.agreement.sampleCount},标注者=${gold.gold.metadata.annotator}`);
136
+ if (gold.result.contaminationWarning) {
137
+ goldMessages.push(`⚠ Gold 污染提示:${gold.result.contaminationWarning}`);
138
+ }
139
+ }
140
+ else {
141
+ goldMessages.push(`⚠ Gold 数据未加载:${gold.loadIssues.join(';') || config.goldDir}`);
142
+ }
143
+ }
144
+ const verdict = computeVerdict(report);
145
+ return {
146
+ kind: 'success',
147
+ text: [
148
+ `OMK 评测完成:${verdict.level}`,
149
+ verdict.headline,
150
+ `报告:${report.id}`,
151
+ ...(result.filePath ? [`文件:${result.filePath}`] : []),
152
+ ...goldMessages,
153
+ ].join('\n'),
154
+ };
155
+ }
156
+ function persistenceFor(ctx) {
157
+ const persistence = ctx.get?.('sessionPersistence');
158
+ if (!persistence) {
159
+ throw new Error('当前 DSH profile 没有启用 sessionPersistence;/omk eval 仍可使用,但任务轨迹需要配置 JSONL 或 SQLite persistence backend。');
160
+ }
161
+ return persistence;
162
+ }
163
+ async function studioUrl(invocation, state) {
164
+ if (state.serverUrl)
165
+ return state.serverUrl;
166
+ const cwd = invocation.agent.session.header.cwd ?? process.cwd();
167
+ const omkDir = join(cwd, '.omk');
168
+ const { createReportServer } = await import('../server/report-server.js');
169
+ state.server = createReportServer({
170
+ port: 0,
171
+ reportsDir: join(omkDir, 'reports'),
172
+ analysesDir: join(omkDir, 'observe-health'),
173
+ doctorsDir: join(omkDir, 'doctors'),
174
+ observationsDir: join(omkDir, 'observe-inbox'),
175
+ jobsDir: join(omkDir, 'jobs'),
176
+ managedDir: join(omkDir, 'managed'),
177
+ conversationCatalog: state.catalog,
178
+ });
179
+ state.serverUrl = await state.server.start();
180
+ return state.serverUrl;
181
+ }
182
+ async function executeObserveCommand(ctx, invocation, sessionId, state) {
183
+ const persistence = persistenceFor(ctx);
184
+ if (!sessionId) {
185
+ const candidates = await listDshObserveCandidates(persistence, {
186
+ excludeSessionId: String(invocation.agent.session.id),
187
+ signal: invocation.signal,
188
+ });
189
+ if (candidates.length === 0) {
190
+ return {
191
+ kind: 'success',
192
+ text: '没有找到可观测的已结束 DSH session。完成一个任务后再运行 /omk observe。',
193
+ };
194
+ }
195
+ return {
196
+ kind: 'success',
197
+ text: [
198
+ '最近可观测的 DSH session:',
199
+ ...candidates.map((candidate) => (`- ${candidate.sessionId} ${candidate.status} ${candidate.createdAt}${candidate.cwd ? ` ${candidate.cwd}` : ''}`)),
200
+ '',
201
+ '查看轨迹:/omk observe <session-id>',
202
+ ].join('\n'),
203
+ };
204
+ }
205
+ const group = await readDshObservedGroup(persistence, sessionId, invocation.signal);
206
+ const selected = group.traces.find((trace) => trace.session.runId === sessionId);
207
+ if (!selected)
208
+ throw new Error(`DSH session 一致快照缺少目标 session:${sessionId}。`);
209
+ const incomplete = group.traces.filter((trace) => !trace.integrity.complete);
210
+ if (incomplete.length > 0) {
211
+ const details = incomplete.map((trace) => {
212
+ const integrity = trace.integrity;
213
+ const reasons = [
214
+ integrity.openTurnCount > 0 ? `未闭合 turn=${integrity.openTurnCount}` : '',
215
+ integrity.openStepCount > 0 ? `未闭合 step=${integrity.openStepCount}` : '',
216
+ integrity.unmatchedToolCallCount > 0 ? `缺失 tool result=${integrity.unmatchedToolCallCount}` : '',
217
+ integrity.unmatchedToolResultCount > 0 ? `孤立 tool result=${integrity.unmatchedToolResultCount}` : '',
218
+ integrity.status === 'unknown' ? '终态未知' : '',
219
+ ].filter(Boolean).join(';');
220
+ return `${trace.session.runId}(${reasons || '完整性检查未通过'})`;
221
+ }).join(';');
222
+ throw new Error(`DSH session group 无法形成完整任务轨迹:${details}。`);
223
+ }
224
+ const cwd = invocation.agent.session.header.cwd ?? process.cwd();
225
+ const observationsDir = join(cwd, '.omk', 'observe-inbox');
226
+ const { buildObservationInboxReportFromTraceSessions, saveObservationInboxReport, } = await import('../observability/inbox.js');
227
+ const { loadObservationReviewState } = await import('../observability/review-state.js');
228
+ const { buildObserveDiagnosticsFromReport } = await import('../diagnosis/observe-producer.js');
229
+ const report = buildObservationInboxReportFromTraceSessions(`dsh:${group.rootSessionId}`, group.traces.map((trace) => trace.session), dshTraceIngestionSummary(group), { reviewState: loadObservationReviewState(observationsDir) });
230
+ report.diagnostics = buildObserveDiagnosticsFromReport(report);
231
+ const inboxPath = saveObservationInboxReport(report, observationsDir);
232
+ const target = state.catalog.upsert(group);
233
+ const baseUrl = await studioUrl(invocation, state);
234
+ const trajectoryUrl = `${baseUrl}/conversations/${encodeURIComponent(target.threadId)}/tasks/${encodeURIComponent(target.turnId)}`;
235
+ return {
236
+ kind: 'success',
237
+ text: [
238
+ `已只读摄取 DSH session:${sessionId}`,
239
+ `任务状态:${selected.integrity.status}`,
240
+ `任务轨迹:${trajectoryUrl}`,
241
+ `观测收件箱:${inboxPath}`,
242
+ ].join('\n'),
243
+ };
244
+ }
245
+ /** Register `/omk eval` and `/omk observe` in every DSH command-capable surface. */
246
+ export function apply(ctx) {
247
+ const observeState = { catalog: createDshConversationCatalog() };
248
+ const disposeCommand = ctx.commands.register({
249
+ name: 'omk',
250
+ description: '在当前 DeepSeek Harness 中运行 OMK 对照评测或查看任务轨迹',
251
+ input: { hint: 'eval <eval.yaml> | observe [session-id]' },
252
+ async handler(invocation) {
253
+ const command = parseCommand(invocation.rawInput);
254
+ if (!command)
255
+ return { kind: 'error', text: USAGE };
256
+ if (command.commandKind === 'help')
257
+ return { kind: 'success', text: HELP };
258
+ try {
259
+ return command.commandKind === 'eval'
260
+ ? await executeEvalCommand(ctx, invocation, command.path)
261
+ : await executeObserveCommand(ctx, invocation, command.sessionId, observeState);
262
+ }
263
+ catch (error) {
264
+ return {
265
+ kind: 'error',
266
+ text: error instanceof Error ? error.message : String(error),
267
+ };
268
+ }
269
+ },
270
+ });
271
+ return () => {
272
+ disposeCommand();
273
+ void observeState.server?.stop().catch(() => undefined);
274
+ };
275
+ }
@@ -0,0 +1,47 @@
1
+ import type { TraceIngestionSummary } from '../types/index.js';
2
+ import type { ConversationCatalog } from '../observability/conversation-catalog.js';
3
+ import { type DshSessionEventLike, type DshSessionHeaderLike, type DshTraceAdapterResult } from './trace-adapter.js';
4
+ export interface DshPersistenceSnapshotLike {
5
+ readonly header: DshSessionHeaderLike;
6
+ readonly revision: unknown;
7
+ }
8
+ export interface DshSessionInspectionLike {
9
+ readonly meta: DshSessionHeaderLike;
10
+ readonly events: readonly DshSessionEventLike[];
11
+ }
12
+ export interface DshSessionPersistenceLike {
13
+ listSnapshots(signal?: AbortSignal): Promise<readonly DshPersistenceSnapshotLike[]>;
14
+ inspect(id: string, signal?: AbortSignal): Promise<DshSessionInspectionLike>;
15
+ }
16
+ export interface DshObserveCandidate {
17
+ sessionId: string;
18
+ createdAt: string;
19
+ cwd?: string;
20
+ status: 'completed' | 'failed' | 'aborted' | 'interrupted';
21
+ }
22
+ export interface DshObservedGroup {
23
+ rootSessionId: string;
24
+ selectedSessionId: string;
25
+ revision: string;
26
+ traces: DshTraceAdapterResult[];
27
+ inspections: DshSessionInspectionLike[];
28
+ }
29
+ /** Count every valid DSH logical record as parsed, including chunks consolidated into messages. */
30
+ export declare function dshTraceIngestionSummary(group: DshObservedGroup): TraceIngestionSummary;
31
+ /** Read one root+descendant group through the logical persistence seam without mutating it. */
32
+ export declare function readDshObservedGroup(persistence: DshSessionPersistenceLike, selectedSessionId: string, signal?: AbortSignal): Promise<DshObservedGroup>;
33
+ /** List recent terminal root sessions; the command's own live session stays out of the default result. */
34
+ export declare function listDshObserveCandidates(persistence: DshSessionPersistenceLike, options?: {
35
+ excludeSessionId?: string;
36
+ limit?: number;
37
+ signal?: AbortSignal;
38
+ }): Promise<DshObserveCandidate[]>;
39
+ export interface DshCatalogTarget {
40
+ threadId: string;
41
+ turnId: string;
42
+ }
43
+ export interface MutableDshConversationCatalog extends ConversationCatalog {
44
+ upsert(group: DshObservedGroup): DshCatalogTarget;
45
+ }
46
+ /** Static snapshot catalog used by Studio; importing another session updates it without adding a parallel UI. */
47
+ export declare function createDshConversationCatalog(): MutableDshConversationCatalog;
@@ -0,0 +1,359 @@
1
+ import { durationMsBetween } from '../shared/time.js';
2
+ import { projectTraceSessionTimeline, } from '../observability/experience.js';
3
+ import { reconstructExperienceTurns } from '../observability/turn-index.js';
4
+ import { observationSourceRecordFromLine } from '../observability/source-record-archive.js';
5
+ import { adaptDshSession, } from './trace-adapter.js';
6
+ const MAX_STABLE_READ_ATTEMPTS = 3;
7
+ const DEFAULT_LIST_LIMIT = 8;
8
+ const LIST_INSPECTION_LIMIT = 24;
9
+ const MAX_SOURCE_RECORD_BYTES = 16 * 1024 * 1024;
10
+ /** Count every valid DSH logical record as parsed, including chunks consolidated into messages. */
11
+ export function dshTraceIngestionSummary(group) {
12
+ const sourceRecordCount = group.inspections.reduce((sum, item) => sum + item.events.length + 1, 0);
13
+ return {
14
+ fileCount: group.traces.length,
15
+ sourceRecordCount,
16
+ parsedRecordCount: sourceRecordCount,
17
+ malformedRecordCount: 0,
18
+ ignoredValueCount: 0,
19
+ unknownEventCount: group.traces.reduce((sum, item) => sum + item.integrity.unknownEventCount, 0),
20
+ filteredSessionCount: 0,
21
+ };
22
+ }
23
+ function snapshotMap(snapshots) {
24
+ return new Map(snapshots.map((snapshot) => [String(snapshot.header.id), snapshot]));
25
+ }
26
+ function rootSessionId(sessionId, snapshots) {
27
+ const seen = new Set();
28
+ let current = sessionId;
29
+ while (true) {
30
+ if (seen.has(current))
31
+ throw new Error(`DSH session lineage 存在循环:${sessionId}。`);
32
+ seen.add(current);
33
+ const currentHeader = snapshots.get(current)?.header;
34
+ if (currentHeader?.origin !== 'subagent')
35
+ return current;
36
+ const parent = currentHeader.parentSession;
37
+ if (!parent)
38
+ return current;
39
+ if (!snapshots.has(String(parent))) {
40
+ throw new Error(`DSH session lineage 缺少 parent session:${String(parent)}。`);
41
+ }
42
+ current = String(parent);
43
+ }
44
+ }
45
+ function groupSessionIds(rootId, snapshots) {
46
+ const ids = new Set([rootId]);
47
+ let changed = true;
48
+ while (changed) {
49
+ changed = false;
50
+ for (const [id, snapshot] of snapshots) {
51
+ const parent = snapshot.header.parentSession ? String(snapshot.header.parentSession) : undefined;
52
+ if (snapshot.header.origin !== 'subagent' || !parent || !ids.has(parent) || ids.has(id))
53
+ continue;
54
+ ids.add(id);
55
+ changed = true;
56
+ }
57
+ }
58
+ return Array.from(ids).sort((left, right) => {
59
+ if (left === rootId)
60
+ return -1;
61
+ if (right === rootId)
62
+ return 1;
63
+ const depthDiff = (snapshots.get(left)?.header.delegationDepth ?? 0)
64
+ - (snapshots.get(right)?.header.delegationDepth ?? 0);
65
+ return depthDiff || left.localeCompare(right);
66
+ });
67
+ }
68
+ function sameRevision(left, right) {
69
+ return Object.is(left, right);
70
+ }
71
+ function stableGroup(before, after, rootId) {
72
+ const beforeIds = groupSessionIds(rootId, before);
73
+ const afterIds = groupSessionIds(rootId, after);
74
+ if (beforeIds.length !== afterIds.length
75
+ || beforeIds.some((id, index) => id !== afterIds[index]))
76
+ return false;
77
+ return beforeIds.every((id) => {
78
+ const left = before.get(id);
79
+ const right = after.get(id);
80
+ return left !== undefined && right !== undefined && sameRevision(left.revision, right.revision);
81
+ });
82
+ }
83
+ function revisionLabel(snapshots, ids) {
84
+ return ids.map((id) => `${id}:${String(snapshots.get(id)?.revision ?? 'missing')}`).join('|');
85
+ }
86
+ /** Read one root+descendant group through the logical persistence seam without mutating it. */
87
+ export async function readDshObservedGroup(persistence, selectedSessionId, signal) {
88
+ for (let attempt = 1; attempt <= MAX_STABLE_READ_ATTEMPTS; attempt += 1) {
89
+ signal?.throwIfAborted();
90
+ const before = snapshotMap(await persistence.listSnapshots(signal));
91
+ if (!before.has(selectedSessionId))
92
+ throw new Error(`DSH session 不存在:${selectedSessionId}。`);
93
+ const rootId = rootSessionId(selectedSessionId, before);
94
+ const ids = groupSessionIds(rootId, before);
95
+ const inspections = await Promise.all(ids.map((id) => persistence.inspect(id, signal)));
96
+ signal?.throwIfAborted();
97
+ const after = snapshotMap(await persistence.listSnapshots(signal));
98
+ if (!stableGroup(before, after, rootId))
99
+ continue;
100
+ const traces = inspections.map((inspection) => adaptDshSession(inspection.meta, inspection.events, {
101
+ rootRunId: rootId,
102
+ role: String(inspection.meta.id) === rootId ? 'main' : 'subagent',
103
+ groupPath: `dsh:${rootId}`,
104
+ }));
105
+ return {
106
+ rootSessionId: rootId,
107
+ selectedSessionId,
108
+ revision: revisionLabel(after, ids),
109
+ traces,
110
+ inspections,
111
+ };
112
+ }
113
+ throw new Error(`DSH session 在读取期间持续变化,${MAX_STABLE_READ_ATTEMPTS} 次一致快照均失败:${selectedSessionId}。`);
114
+ }
115
+ async function inspectCandidate(persistence, snapshot, signal) {
116
+ const sessionId = String(snapshot.header.id);
117
+ try {
118
+ const group = await readDshObservedGroup(persistence, sessionId, signal);
119
+ const trace = group.traces.find((item) => item.session.runId === sessionId);
120
+ if (!trace)
121
+ return undefined;
122
+ if (group.traces.some((item) => !item.integrity.complete))
123
+ return undefined;
124
+ const status = trace.integrity.status;
125
+ if (status !== 'completed' && status !== 'failed' && status !== 'aborted' && status !== 'interrupted') {
126
+ return undefined;
127
+ }
128
+ return {
129
+ sessionId,
130
+ createdAt: new Date(snapshot.header.createdAt).toISOString(),
131
+ cwd: snapshot.header.cwd,
132
+ status,
133
+ };
134
+ }
135
+ catch {
136
+ return undefined;
137
+ }
138
+ }
139
+ /** List recent terminal root sessions; the command's own live session stays out of the default result. */
140
+ export async function listDshObserveCandidates(persistence, options = {}) {
141
+ const limit = Math.max(1, options.limit ?? DEFAULT_LIST_LIMIT);
142
+ const snapshots = [...await persistence.listSnapshots(options.signal)]
143
+ .filter((snapshot) => snapshot.header.origin !== 'subagent'
144
+ && String(snapshot.header.id) !== options.excludeSessionId)
145
+ .sort((left, right) => right.header.createdAt - left.header.createdAt)
146
+ .slice(0, Math.max(limit, LIST_INSPECTION_LIMIT));
147
+ const candidates = [];
148
+ for (const snapshot of snapshots) {
149
+ options.signal?.throwIfAborted();
150
+ const candidate = await inspectCandidate(persistence, snapshot, options.signal);
151
+ if (candidate)
152
+ candidates.push(candidate);
153
+ if (candidates.length >= limit)
154
+ break;
155
+ }
156
+ return candidates;
157
+ }
158
+ function mergedTimeline(group) {
159
+ const ranked = group.traces.flatMap((trace, traceRank) => (projectTraceSessionTimeline(trace.session).map((event) => ({ event, traceRank }))));
160
+ ranked.sort((left, right) => {
161
+ if (left.event.timestamp && right.event.timestamp && left.event.timestamp !== right.event.timestamp) {
162
+ return left.event.timestamp.localeCompare(right.event.timestamp);
163
+ }
164
+ return left.traceRank - right.traceRank || left.event.order - right.event.order;
165
+ });
166
+ return ranked.map(({ event }, index) => ({ ...event, order: index * 10 }));
167
+ }
168
+ function attachDescendantsToRootTurns(turns, timeline, rootTraceId) {
169
+ const rootTurns = turns.filter((turn) => turn.traceId === rootTraceId);
170
+ const childEvents = timeline.filter((event) => event.traceId !== rootTraceId);
171
+ if (rootTurns.length === 0 || childEvents.length === 0)
172
+ return turns;
173
+ const eventById = new Map(timeline.map((event) => [event.id, event]));
174
+ return turns.map((turn) => {
175
+ if (turn.traceId !== rootTraceId)
176
+ return turn;
177
+ const related = childEvents.filter((event) => {
178
+ if (rootTurns.length === 1)
179
+ return true;
180
+ if (!event.timestamp || !turn.startTimestamp)
181
+ return false;
182
+ return event.timestamp >= turn.startTimestamp
183
+ && (!turn.endTimestamp || event.timestamp <= turn.endTimestamp);
184
+ });
185
+ if (related.length === 0)
186
+ return turn;
187
+ const eventIds = Array.from(new Set([...turn.eventIds, ...related.map((event) => event.id)]))
188
+ .sort((left, right) => (eventById.get(left)?.order ?? 0) - (eventById.get(right)?.order ?? 0));
189
+ return {
190
+ ...turn,
191
+ eventIds,
192
+ toolCallCount: eventIds.filter((id) => eventById.get(id)?.kind === 'tool_use').length,
193
+ toolFailureCount: eventIds.filter((id) => {
194
+ const event = eventById.get(id);
195
+ return event?.kind === 'tool_result' && (event.toolStatus === 'failure' || event.isError);
196
+ }).length,
197
+ };
198
+ });
199
+ }
200
+ function sourceRecords(group) {
201
+ let byteCount = 0;
202
+ let omittedRecordCount = 0;
203
+ const records = [];
204
+ group.traces.forEach((trace, traceIndex) => {
205
+ const inspection = group.inspections[traceIndex];
206
+ if (!inspection)
207
+ return;
208
+ const values = [{ type: 'session/header', ...inspection.meta }, ...inspection.events];
209
+ values.forEach((value, sourceIndex) => {
210
+ const raw = JSON.stringify(value);
211
+ const event = sourceIndex === 0 ? undefined : inspection.events[sourceIndex - 1];
212
+ const base = observationSourceRecordFromLine(raw, sourceIndex, trace.session.traceId, trace.session.sourcePath);
213
+ if (byteCount + base.byteCount > MAX_SOURCE_RECORD_BYTES) {
214
+ omittedRecordCount += 1;
215
+ return;
216
+ }
217
+ records.push({
218
+ ...base,
219
+ sourceType: sourceIndex === 0 ? 'session/header' : event?.type ?? 'unknown',
220
+ sourceEventId: event ? `${String(inspection.meta.id)}:${event.seq}` : String(inspection.meta.id),
221
+ timestamp: sourceIndex === 0
222
+ ? new Date(inspection.meta.createdAt).toISOString()
223
+ : event ? new Date(event.time).toISOString() : undefined,
224
+ });
225
+ byteCount += base.byteCount;
226
+ });
227
+ });
228
+ return {
229
+ status: omittedRecordCount > 0 ? 'partial' : 'available',
230
+ recordCount: records.length,
231
+ records,
232
+ omittedRecordCount,
233
+ byteCount,
234
+ truncated: omittedRecordCount > 0,
235
+ ...(omittedRecordCount > 0 ? { reason: 'archive_limit' } : {}),
236
+ };
237
+ }
238
+ function taskItem(threadId, turn) {
239
+ return {
240
+ turnId: turn.turnId,
241
+ sourceTurnId: turn.sourceTurnId,
242
+ trajectoryHref: `/conversations/${encodeURIComponent(threadId)}/tasks/${encodeURIComponent(turn.turnId)}`,
243
+ title: turn.title,
244
+ startTimestamp: turn.startTimestamp,
245
+ endTimestamp: turn.endTimestamp,
246
+ durationMs: durationMsBetween(turn.startTimestamp, turn.endTimestamp),
247
+ status: turn.status,
248
+ eventCount: turn.eventIds.length,
249
+ toolCallCount: turn.toolCallCount,
250
+ toolFailureCount: turn.toolFailureCount,
251
+ relatedSkillNames: [],
252
+ };
253
+ }
254
+ function catalogEntry(group) {
255
+ const rootTrace = group.traces.find((trace) => trace.session.runId === group.rootSessionId)
256
+ ?? group.traces[0];
257
+ if (!rootTrace)
258
+ throw new Error(`DSH session group 为空:${group.rootSessionId}。`);
259
+ const timeline = mergedTimeline(group);
260
+ const turns = attachDescendantsToRootTurns(reconstructExperienceTurns(timeline), timeline, rootTrace.session.traceId);
261
+ const rootTimeline = timeline.filter((event) => event.traceId === rootTrace.session.traceId);
262
+ const firstUser = rootTimeline.find((event) => event.kind === 'user_message');
263
+ const title = firstUser?.fullText?.trim() || firstUser?.snippet?.trim() || `DSH session ${group.rootSessionId}`;
264
+ const tasks = turns.map((turn) => taskItem(group.rootSessionId, turn));
265
+ const startTimestamp = rootTrace.session.startTimestamp;
266
+ const endTimestamp = rootTrace.session.endTimestamp;
267
+ return {
268
+ group,
269
+ turns,
270
+ timeline,
271
+ records: sourceRecords(group),
272
+ conversation: {
273
+ threadId: group.rootSessionId,
274
+ sourceThreadId: group.rootSessionId,
275
+ sourceKind: 'dsh',
276
+ title: title.replace(/\s+/g, ' ').slice(0, 160),
277
+ preview: title.replace(/\s+/g, ' ').slice(0, 240),
278
+ cwd: rootTrace.session.cwd,
279
+ model: rootTrace.session.sourceMetadata?.model,
280
+ childThreadCount: Math.max(0, group.traces.length - 1),
281
+ startTimestamp,
282
+ endTimestamp,
283
+ durationMs: durationMsBetween(startTimestamp, endTimestamp),
284
+ turnCount: tasks.length,
285
+ toolCallCount: tasks.reduce((sum, task) => sum + task.toolCallCount, 0),
286
+ toolFailureCount: tasks.reduce((sum, task) => sum + task.toolFailureCount, 0),
287
+ relatedSkillNames: [],
288
+ tasks,
289
+ },
290
+ };
291
+ }
292
+ /** Static snapshot catalog used by Studio; importing another session updates it without adding a parallel UI. */
293
+ export function createDshConversationCatalog() {
294
+ const entries = new Map();
295
+ return {
296
+ upsert(group) {
297
+ const entry = catalogEntry(group);
298
+ entries.set(group.rootSessionId, entry);
299
+ const selectedTrace = group.traces.find((trace) => trace.session.runId === group.selectedSessionId);
300
+ const selectedTurn = [...entry.turns].reverse().find((turn) => turn.traceId === selectedTrace?.session.traceId)
301
+ ?? entry.turns.at(-1);
302
+ if (!selectedTurn)
303
+ throw new Error(`DSH session 没有可展示的任务:${group.selectedSessionId}。`);
304
+ return { threadId: group.rootSessionId, turnId: selectedTurn.turnId };
305
+ },
306
+ async listConversations() {
307
+ const conversations = Array.from(entries.values())
308
+ .map((entry) => entry.conversation)
309
+ .sort((left, right) => (right.endTimestamp ?? '').localeCompare(left.endTimestamp ?? ''));
310
+ return {
311
+ conversations,
312
+ totalTurnCount: conversations.reduce((sum, item) => sum + (item.turnCount ?? 0), 0),
313
+ totalToolCallCount: conversations.reduce((sum, item) => sum + (item.toolCallCount ?? 0), 0),
314
+ totalToolFailureCount: conversations.reduce((sum, item) => sum + (item.toolFailureCount ?? 0), 0),
315
+ indexedConversationCount: conversations.length,
316
+ unarchivedConversationCount: conversations.length,
317
+ archivedConversationCount: 0,
318
+ workspaceCount: new Set(conversations.flatMap((item) => item.cwd ? [item.cwd] : [])).size,
319
+ };
320
+ },
321
+ async getConversation(threadId) {
322
+ return entries.get(threadId)?.conversation;
323
+ },
324
+ async loadTaskTrajectory(threadId, turnId) {
325
+ const entry = entries.get(threadId);
326
+ const turn = entry?.turns.find((candidate) => candidate.turnId === turnId);
327
+ if (!entry || !turn)
328
+ return undefined;
329
+ const rootTrace = entry.group.traces.find((trace) => trace.session.runId === entry.group.rootSessionId)
330
+ ?? entry.group.traces[0];
331
+ if (!rootTrace)
332
+ return undefined;
333
+ return {
334
+ revision: entry.group.revision,
335
+ status: turn.status,
336
+ liveObservable: false,
337
+ session: {
338
+ id: `dsh:${threadId}`,
339
+ threadId,
340
+ sourceThreadId: entry.group.rootSessionId,
341
+ sessionId: entry.group.selectedSessionId,
342
+ sourceTrace: rootTrace.session.sourcePath,
343
+ sourceKind: 'dsh',
344
+ entrypoint: 'dsh',
345
+ sourceMetadata: rootTrace.session.sourceMetadata,
346
+ cwd: rootTrace.session.cwd,
347
+ startTimestamp: rootTrace.session.startTimestamp,
348
+ endTimestamp: rootTrace.session.endTimestamp,
349
+ attributedEventIds: [],
350
+ turns: entry.turns,
351
+ fullSessionTimeline: entry.timeline,
352
+ indicators: { userCorrectionCount: 0 },
353
+ },
354
+ ingestion: dshTraceIngestionSummary(entry.group),
355
+ sourceRecords: entry.records,
356
+ };
357
+ },
358
+ };
359
+ }