oh-my-knowledge 0.53.0 → 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.
- package/README.md +9 -1
- package/README.zh.md +9 -1
- package/dist/assets/agent-skills/omk/SKILL.md +2 -0
- package/dist/dsh-plugin/index.d.ts +4 -2
- package/dist/dsh-plugin/index.js +134 -15
- package/dist/dsh-plugin/observe.d.ts +47 -0
- package/dist/dsh-plugin/observe.js +359 -0
- package/dist/dsh-plugin/trace-adapter.d.ts +48 -0
- package/dist/dsh-plugin/trace-adapter.js +590 -0
- package/dist/observability/conversation-catalog.js +1 -1
- package/dist/observability/experience.js +4 -0
- package/dist/observability/inbox.d.ts +4 -1
- package/dist/observability/inbox.js +7 -2
- package/dist/observability/trace-ir.d.ts +6 -3
- package/dist/observability/turn-index.js +4 -0
- package/dist/renderer/conversation-renderer.js +20 -6
- package/dist/renderer/knowledge-debugger-renderer.js +6 -1
- package/dist/renderer/observation-inbox-renderer.js +2 -2
- package/dist/renderer/trajectory-live.d.ts +1 -0
- package/dist/renderer/trajectory-live.js +5 -2
- package/dist/shared/trace-source-kind.js +1 -0
- package/dist/types/observability.d.ts +1 -1
- package/dist/types/trace.d.ts +1 -1
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -16,6 +16,8 @@ Observe real-world performance, measure version differences, and determine wheth
|
|
|
16
16
|
|
|
17
17
|
**Same model. Same evaluation samples. Only the knowledge artifact changes.**
|
|
18
18
|
|
|
19
|
+
**DeepSeek Harness users:** install OMK as a native bundle, reuse the current profile for controlled evaluations, and open persisted DSH task trajectories in Studio. [Set up the DSH host plugin →](docs/reference/executors.md#deepseek-harness-prefer-the-host-plugin)
|
|
20
|
+
|
|
19
21
|

|
|
20
22
|
|
|
21
23
|
📖 **Full documentation: [oh-my-knowledge.pages.dev](https://oh-my-knowledge.pages.dev)** (searchable, English / 简体中文)
|
|
@@ -186,7 +188,13 @@ dsh plugin --profile web add oh-my-knowledge
|
|
|
186
188
|
dsh --profile web
|
|
187
189
|
```
|
|
188
190
|
|
|
189
|
-
|
|
191
|
+
Inside DSH:
|
|
192
|
+
|
|
193
|
+
- `/omk eval eval.yaml` runs every sample in an isolated DSH session while OMK owns the report and statistics;
|
|
194
|
+
- `/omk observe` lists recent terminal sessions;
|
|
195
|
+
- `/omk observe <session-id>` reads a consistent snapshot and returns its Studio Task Trajectory URL.
|
|
196
|
+
|
|
197
|
+
Observe uses the profile's `sessionPersistence` directly, so users do not export or locate JSONL / SQLite files. The first version is offline-only and does not live-follow a session that is still being written. See the [executor guide](docs/reference/executors.md#deepseek-harness-prefer-the-host-plugin) and [observe guide](docs/guides/observe-production.md#inspect-a-task-inside-deepseek-harness).
|
|
190
198
|
|
|
191
199
|
## Documentation
|
|
192
200
|
|
package/README.zh.md
CHANGED
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
|
|
17
17
|
**相同模型,相同评测用例,只改变知识载体。**
|
|
18
18
|
|
|
19
|
+
**DeepSeek Harness 用户:** OMK 可作为原生 bundle 安装,复用当前 profile 做受控评测,并在 Studio 打开已持久化的 DSH 任务轨迹。[接入 DSH 宿主插件 →](docs/zh/reference/executors.md#deepseek-harness优先使用宿主插件)
|
|
20
|
+
|
|
19
21
|

|
|
20
22
|
|
|
21
23
|
📖 **完整文档:[oh-my-knowledge.pages.dev/zh](https://oh-my-knowledge.pages.dev/zh/)**(可搜索,可切换英文)
|
|
@@ -186,7 +188,13 @@ dsh plugin --profile web add oh-my-knowledge
|
|
|
186
188
|
dsh --profile web
|
|
187
189
|
```
|
|
188
190
|
|
|
189
|
-
进入 DSH
|
|
191
|
+
进入 DSH 后:
|
|
192
|
+
|
|
193
|
+
- `/omk eval eval.yaml`:每条用例使用独立 DSH session,报告仍由 OMK 生成;
|
|
194
|
+
- `/omk observe`:列出最近已结束的 session;
|
|
195
|
+
- `/omk observe <session-id>`:只读摄取一致快照,并返回 Studio 任务轨迹链接。
|
|
196
|
+
|
|
197
|
+
observe 直接使用 profile 的 `sessionPersistence`,无需导出或定位 JSONL/SQLite 文件;首版不实时跟随正在写入的 session。详见[执行器文档](docs/zh/reference/executors.md#deepseek-harness优先使用宿主插件)与[观测指南](docs/zh/guides/observe-production.md#在-deepseek-harness-中查看任务轨迹)。
|
|
190
198
|
|
|
191
199
|
## 文档
|
|
192
200
|
|
|
@@ -31,6 +31,8 @@ Codex 是 omk 的一等 runtime。运行在 Codex 任务中时,`omk eval` / `d
|
|
|
31
31
|
|
|
32
32
|
如果当前 DSH profile 已安装 `oh-my-knowledge` bundle,使用 `/omk eval <eval.yaml>`。该路径直接复用现有 DSH 的模型、凭证、工具与 sandbox,并为每条用例创建隔离 session;不要要求用户另起 DSH runtime。
|
|
33
33
|
|
|
34
|
+
查看真实 DSH 任务轨迹时,先运行 `/omk observe` 列出最近已结束的 session,再运行 `/omk observe <session-id>`。OMK 通过当前 profile 的 `sessionPersistence` 只读摄取一致快照,并返回 Studio 任务轨迹链接;不要求用户导出或定位 JSONL/SQLite 文件。首版不实时跟随正在写入的 session,也不默认选择发起 observe 命令的当前 session。
|
|
35
|
+
|
|
34
36
|
## 第二步:理解用户意图
|
|
35
37
|
|
|
36
38
|
根据用户的描述,匹配对应的操作:
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type DshAgentLike, type DshHostContextLike } from './host-executor.js';
|
|
2
|
+
import { type DshSessionPersistenceLike } from './observe.js';
|
|
2
3
|
interface DshCommandInvocationLike {
|
|
3
4
|
readonly agent: DshAgentLike;
|
|
4
5
|
readonly rawInput: string;
|
|
@@ -10,6 +11,7 @@ type DshCommandResultLike = Readonly<Record<'kind', 'success'> & {
|
|
|
10
11
|
text: string;
|
|
11
12
|
}>;
|
|
12
13
|
interface DshPluginContextLike extends DshHostContextLike {
|
|
14
|
+
get?(name: 'sessionPersistence'): DshSessionPersistenceLike | undefined;
|
|
13
15
|
readonly commands: {
|
|
14
16
|
register(definition: {
|
|
15
17
|
readonly name: string;
|
|
@@ -23,6 +25,6 @@ interface DshPluginContextLike extends DshHostContextLike {
|
|
|
23
25
|
}
|
|
24
26
|
export declare const name = "omk-dsh-plugin";
|
|
25
27
|
export declare const inject: string[];
|
|
26
|
-
/** Register `/omk eval
|
|
27
|
-
export declare function apply(ctx: DshPluginContextLike): void;
|
|
28
|
+
/** Register `/omk eval` and `/omk observe` in every DSH command-capable surface. */
|
|
29
|
+
export declare function apply(ctx: DshPluginContextLike): () => void;
|
|
28
30
|
export {};
|
package/dist/dsh-plugin/index.js
CHANGED
|
@@ -3,22 +3,43 @@ import { computeVerdict } from '../eval-core/verdict.js';
|
|
|
3
3
|
import { configVariantsToSpecs, loadEvalConfig } from '../inputs/eval-config.js';
|
|
4
4
|
import { runEvaluation, runMultiple } from '../eval-workflows/run-evaluation.js';
|
|
5
5
|
import { createDshHostExecutor, } from './host-executor.js';
|
|
6
|
+
import { createDshConversationCatalog, dshTraceIngestionSummary, listDshObserveCandidates, readDshObservedGroup, } from './observe.js';
|
|
6
7
|
export const name = 'omk-dsh-plugin';
|
|
7
8
|
export const inject = ['agentPresets', 'agents', 'commands', 'tools'];
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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) {
|
|
16
20
|
if ((value.startsWith('"') && value.endsWith('"'))
|
|
17
21
|
|| (value.startsWith("'") && value.endsWith("'"))) {
|
|
18
22
|
return value.slice(1, -1);
|
|
19
23
|
}
|
|
20
24
|
return value;
|
|
21
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
|
+
}
|
|
22
43
|
function projectRoot(configPath) {
|
|
23
44
|
return dirname(configPath);
|
|
24
45
|
}
|
|
@@ -132,18 +153,112 @@ async function executeEvalCommand(ctx, invocation, requestedPath) {
|
|
|
132
153
|
].join('\n'),
|
|
133
154
|
};
|
|
134
155
|
}
|
|
135
|
-
|
|
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. */
|
|
136
246
|
export function apply(ctx) {
|
|
137
|
-
|
|
247
|
+
const observeState = { catalog: createDshConversationCatalog() };
|
|
248
|
+
const disposeCommand = ctx.commands.register({
|
|
138
249
|
name: 'omk',
|
|
139
|
-
description: '在当前 DeepSeek Harness 中运行 OMK
|
|
140
|
-
input: { hint: 'eval <eval.yaml>' },
|
|
250
|
+
description: '在当前 DeepSeek Harness 中运行 OMK 对照评测或查看任务轨迹',
|
|
251
|
+
input: { hint: 'eval <eval.yaml> | observe [session-id]' },
|
|
141
252
|
async handler(invocation) {
|
|
142
|
-
const
|
|
143
|
-
if (!
|
|
253
|
+
const command = parseCommand(invocation.rawInput);
|
|
254
|
+
if (!command)
|
|
144
255
|
return { kind: 'error', text: USAGE };
|
|
256
|
+
if (command.commandKind === 'help')
|
|
257
|
+
return { kind: 'success', text: HELP };
|
|
145
258
|
try {
|
|
146
|
-
return
|
|
259
|
+
return command.commandKind === 'eval'
|
|
260
|
+
? await executeEvalCommand(ctx, invocation, command.path)
|
|
261
|
+
: await executeObserveCommand(ctx, invocation, command.sessionId, observeState);
|
|
147
262
|
}
|
|
148
263
|
catch (error) {
|
|
149
264
|
return {
|
|
@@ -153,4 +268,8 @@ export function apply(ctx) {
|
|
|
153
268
|
}
|
|
154
269
|
},
|
|
155
270
|
});
|
|
271
|
+
return () => {
|
|
272
|
+
disposeCommand();
|
|
273
|
+
void observeState.server?.stop().catch(() => undefined);
|
|
274
|
+
};
|
|
156
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
|
+
}
|