@xiaolei.shawn/mcp-server 0.2.1 → 0.3.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/dist/__tests__/ingest.test.js +40 -1
- package/dist/__tests__/local-analysis.test.d.ts +1 -0
- package/dist/__tests__/local-analysis.test.js +118 -0
- package/dist/adapters/codex.js +29 -7
- package/dist/dashboard.js +125 -13
- package/dist/index.js +0 -0
- package/dist/ingest.d.ts +2 -0
- package/dist/ingest.js +67 -2
- package/dist/local-analysis.d.ts +91 -0
- package/dist/local-analysis.js +517 -0
- package/package.json +14 -15
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Ingest and merge logic tests.
|
|
3
3
|
* Run from mcp-server: pnpm run build && pnpm test
|
|
4
4
|
*/
|
|
5
|
-
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
5
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import { tmpdir } from "node:os";
|
|
8
8
|
import { describe, it, before, after } from "node:test";
|
|
@@ -102,4 +102,43 @@ describe("ingest", () => {
|
|
|
102
102
|
assert.strictEqual(result.adapter, "codex_jsonl");
|
|
103
103
|
assert.ok(result.inserted > 0);
|
|
104
104
|
});
|
|
105
|
+
it("merge raw log from different day: time window filters out all raw events", () => {
|
|
106
|
+
const sessionId = "sess_merge_target_time_window";
|
|
107
|
+
const sessionStartTs = "2026-03-02T20:55:12.151Z";
|
|
108
|
+
const sessionEndTs = "2026-03-02T20:57:42.004Z";
|
|
109
|
+
const sessionLines = [
|
|
110
|
+
JSON.stringify({
|
|
111
|
+
id: `${sessionId}:1:aa`,
|
|
112
|
+
session_id: sessionId,
|
|
113
|
+
seq: 1,
|
|
114
|
+
ts: sessionStartTs,
|
|
115
|
+
kind: "session_start",
|
|
116
|
+
actor: { type: "agent" },
|
|
117
|
+
payload: { goal: "Test" },
|
|
118
|
+
schema_version: 1,
|
|
119
|
+
}),
|
|
120
|
+
JSON.stringify({
|
|
121
|
+
id: `${sessionId}:2:bb`,
|
|
122
|
+
session_id: sessionId,
|
|
123
|
+
seq: 2,
|
|
124
|
+
ts: sessionEndTs,
|
|
125
|
+
kind: "session_end",
|
|
126
|
+
actor: { type: "agent" },
|
|
127
|
+
payload: { outcome: "completed" },
|
|
128
|
+
schema_version: 1,
|
|
129
|
+
}),
|
|
130
|
+
].join("\n") + "\n";
|
|
131
|
+
writeFileSync(join(sessionsDir, `${sessionId}.jsonl`), sessionLines, "utf-8");
|
|
132
|
+
const raw = readFileSync(fixturePath("codex_sample.jsonl"), "utf-8");
|
|
133
|
+
const result = ingestRawContent(raw, {
|
|
134
|
+
adapter: "codex_jsonl",
|
|
135
|
+
merge_session_id: sessionId,
|
|
136
|
+
});
|
|
137
|
+
assert.strictEqual(result.session_id, sessionId);
|
|
138
|
+
assert.strictEqual(result.merge_strategy, "explicit_merge");
|
|
139
|
+
assert.strictEqual(result.inserted, 0, "no raw events fall in Mar 2 window");
|
|
140
|
+
assert.ok(result.filtered_out_by_time_window !== undefined && result.filtered_out_by_time_window > 0, "raw events (Feb 24) were filtered out by time window");
|
|
141
|
+
const eventsAfter = readSessionEvents(sessionId);
|
|
142
|
+
assert.strictEqual(eventsAfter.length, 2, "session still has only session_start and session_end");
|
|
143
|
+
});
|
|
105
144
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import assert from "node:assert";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import { deriveIntentTokenBreakdown, generateFollowupArtifacts } from "../local-analysis.js";
|
|
4
|
+
function baseEvent(overrides) {
|
|
5
|
+
return {
|
|
6
|
+
id: "e-1",
|
|
7
|
+
session_id: "sess-test",
|
|
8
|
+
seq: 1,
|
|
9
|
+
ts: "2026-03-03T00:00:00.000Z",
|
|
10
|
+
kind: "intent",
|
|
11
|
+
actor: { type: "agent" },
|
|
12
|
+
payload: {},
|
|
13
|
+
schema_version: 1,
|
|
14
|
+
...overrides,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
describe("local-analysis", () => {
|
|
18
|
+
it("generates per-intent artifacts with deterministic template", () => {
|
|
19
|
+
const events = [
|
|
20
|
+
baseEvent({
|
|
21
|
+
id: "i1",
|
|
22
|
+
seq: 1,
|
|
23
|
+
kind: "intent",
|
|
24
|
+
scope: { intent_id: "intent_a" },
|
|
25
|
+
payload: { intent_id: "intent_a", title: "Implement feature A" },
|
|
26
|
+
}),
|
|
27
|
+
baseEvent({
|
|
28
|
+
id: "t1",
|
|
29
|
+
seq: 2,
|
|
30
|
+
kind: "tool_call",
|
|
31
|
+
scope: { intent_id: "intent_a" },
|
|
32
|
+
payload: { category: "tool", action: "read_file", target: "src/a.ts" },
|
|
33
|
+
}),
|
|
34
|
+
baseEvent({
|
|
35
|
+
id: "t2",
|
|
36
|
+
seq: 3,
|
|
37
|
+
kind: "tool_call",
|
|
38
|
+
scope: { intent_id: "intent_a" },
|
|
39
|
+
payload: { category: "tool", action: "read_file", target: "src/a.ts" },
|
|
40
|
+
}),
|
|
41
|
+
baseEvent({
|
|
42
|
+
id: "t3",
|
|
43
|
+
seq: 4,
|
|
44
|
+
kind: "tool_call",
|
|
45
|
+
scope: { intent_id: "intent_a" },
|
|
46
|
+
payload: { category: "tool", action: "read_file", target: "src/a.ts" },
|
|
47
|
+
}),
|
|
48
|
+
baseEvent({
|
|
49
|
+
id: "v1",
|
|
50
|
+
seq: 5,
|
|
51
|
+
kind: "verification",
|
|
52
|
+
scope: { intent_id: "intent_a" },
|
|
53
|
+
payload: { type: "test", result: "fail" },
|
|
54
|
+
}),
|
|
55
|
+
baseEvent({
|
|
56
|
+
id: "r1",
|
|
57
|
+
seq: 6,
|
|
58
|
+
kind: "risk_signal",
|
|
59
|
+
scope: { intent_id: "intent_a", file: "src/a.ts" },
|
|
60
|
+
payload: { level: "high", reasons: ["regression risk"] },
|
|
61
|
+
}),
|
|
62
|
+
];
|
|
63
|
+
const result = generateFollowupArtifacts(events, {
|
|
64
|
+
mode: "per_intent",
|
|
65
|
+
strictness: "soft",
|
|
66
|
+
focus: "risk",
|
|
67
|
+
});
|
|
68
|
+
assert.equal(result.artifacts.length, 1);
|
|
69
|
+
assert.equal(result.artifacts[0].intent_id, "intent_a");
|
|
70
|
+
assert.equal(result.artifacts[0].rule_template_id, "high_risk_guardrail");
|
|
71
|
+
assert.ok(result.artifacts[0].value_claims.risk_mitigation.length > 0);
|
|
72
|
+
});
|
|
73
|
+
it("derives token breakdown context/output split", () => {
|
|
74
|
+
const events = [
|
|
75
|
+
baseEvent({
|
|
76
|
+
id: "i1",
|
|
77
|
+
seq: 1,
|
|
78
|
+
kind: "intent",
|
|
79
|
+
scope: { intent_id: "intent_a" },
|
|
80
|
+
payload: { intent_id: "intent_a", title: "Intent A" },
|
|
81
|
+
}),
|
|
82
|
+
baseEvent({
|
|
83
|
+
id: "ctx1",
|
|
84
|
+
seq: 2,
|
|
85
|
+
kind: "tool_call",
|
|
86
|
+
scope: { intent_id: "intent_a" },
|
|
87
|
+
payload: { category: "search", action: "search_docs", target: "api docs" },
|
|
88
|
+
}),
|
|
89
|
+
baseEvent({
|
|
90
|
+
id: "tok1",
|
|
91
|
+
seq: 3,
|
|
92
|
+
kind: "token_usage_checkpoint",
|
|
93
|
+
scope: { intent_id: "intent_a" },
|
|
94
|
+
payload: { usage: { prompt_tokens: 100, completion_tokens: 50, total_tokens: 150 } },
|
|
95
|
+
}),
|
|
96
|
+
baseEvent({
|
|
97
|
+
id: "out1",
|
|
98
|
+
seq: 4,
|
|
99
|
+
kind: "file_op",
|
|
100
|
+
scope: { intent_id: "intent_a", file: "src/a.ts" },
|
|
101
|
+
payload: { category: "file", action: "edit", target: "src/a.ts" },
|
|
102
|
+
}),
|
|
103
|
+
baseEvent({
|
|
104
|
+
id: "tok2",
|
|
105
|
+
seq: 5,
|
|
106
|
+
kind: "token_usage_checkpoint",
|
|
107
|
+
scope: { intent_id: "intent_a" },
|
|
108
|
+
payload: { usage: { total_tokens: 90 } },
|
|
109
|
+
}),
|
|
110
|
+
];
|
|
111
|
+
const result = deriveIntentTokenBreakdown(events);
|
|
112
|
+
assert.equal(result.intent_breakdown.length, 1);
|
|
113
|
+
assert.equal(result.totals.total_tokens, 240);
|
|
114
|
+
assert.equal(result.intent_breakdown[0].context_tokens +
|
|
115
|
+
result.intent_breakdown[0].output_tokens +
|
|
116
|
+
result.intent_breakdown[0].unknown_tokens, 240);
|
|
117
|
+
});
|
|
118
|
+
});
|
package/dist/adapters/codex.js
CHANGED
|
@@ -4,11 +4,24 @@ function toObject(value) {
|
|
|
4
4
|
: {};
|
|
5
5
|
}
|
|
6
6
|
function toIso(ts, fallback) {
|
|
7
|
+
if (ts == null)
|
|
8
|
+
return fallback;
|
|
9
|
+
if (typeof ts === "number" && Number.isFinite(ts)) {
|
|
10
|
+
const parsed = new Date(ts);
|
|
11
|
+
return Number.isNaN(parsed.getTime()) ? fallback : parsed.toISOString();
|
|
12
|
+
}
|
|
7
13
|
if (typeof ts !== "string" || ts.trim() === "")
|
|
8
14
|
return fallback;
|
|
9
15
|
const parsed = new Date(ts);
|
|
10
16
|
return Number.isNaN(parsed.getTime()) ? fallback : parsed.toISOString();
|
|
11
17
|
}
|
|
18
|
+
/** Prefer top-level timestamp, then payload.timestamp / payload.created_at. */
|
|
19
|
+
function getRecordTimestamp(record) {
|
|
20
|
+
if (record.timestamp !== undefined && record.timestamp !== null)
|
|
21
|
+
return record.timestamp;
|
|
22
|
+
const payload = toObject(record.payload);
|
|
23
|
+
return payload.timestamp ?? payload.created_at;
|
|
24
|
+
}
|
|
12
25
|
function short(value, max = 800) {
|
|
13
26
|
if (typeof value !== "string")
|
|
14
27
|
return undefined;
|
|
@@ -67,7 +80,7 @@ function parseLines(content) {
|
|
|
67
80
|
function mapResponseItem(record, intentId, now) {
|
|
68
81
|
const payload = toObject(record.payload);
|
|
69
82
|
const itemType = short(payload.type) ?? "unknown";
|
|
70
|
-
const ts = toIso(record
|
|
83
|
+
const ts = toIso(getRecordTimestamp(record), now);
|
|
71
84
|
if (itemType === "function_call" || itemType === "custom_tool_call" || itemType === "web_search_call") {
|
|
72
85
|
const action = short(payload.name) ?? short(toObject(payload.action).type) ?? itemType;
|
|
73
86
|
return [
|
|
@@ -189,7 +202,7 @@ export const codexJsonlAdapter = {
|
|
|
189
202
|
}
|
|
190
203
|
const meta = toObject(sessionMeta.payload);
|
|
191
204
|
const sessionId = short(meta.id) ?? `codex_${Date.now()}`;
|
|
192
|
-
const start = toIso(
|
|
205
|
+
const start = toIso(getRecordTimestamp(sessionMeta) ?? meta.timestamp, now);
|
|
193
206
|
let intentCounter = 0;
|
|
194
207
|
let activeIntentId;
|
|
195
208
|
const events = [];
|
|
@@ -216,7 +229,7 @@ export const codexJsonlAdapter = {
|
|
|
216
229
|
activeIntentId = `intent_${sessionId}_${intentCounter}`;
|
|
217
230
|
events.push({
|
|
218
231
|
kind: "intent",
|
|
219
|
-
ts: toIso(record
|
|
232
|
+
ts: toIso(getRecordTimestamp(record), now),
|
|
220
233
|
actor: { type: "user", id: "codex-user" },
|
|
221
234
|
scope: { intent_id: activeIntentId },
|
|
222
235
|
payload: {
|
|
@@ -235,7 +248,7 @@ export const codexJsonlAdapter = {
|
|
|
235
248
|
const usage = normalizeTokenUsage(p.info);
|
|
236
249
|
events.push({
|
|
237
250
|
kind: "token_usage_checkpoint",
|
|
238
|
-
ts: toIso(record
|
|
251
|
+
ts: toIso(getRecordTimestamp(record), now),
|
|
239
252
|
actor: { type: "system", id: "codex" },
|
|
240
253
|
scope: activeIntentId ? { intent_id: activeIntentId, module: "llm" } : { module: "llm" },
|
|
241
254
|
payload: {
|
|
@@ -253,7 +266,7 @@ export const codexJsonlAdapter = {
|
|
|
253
266
|
if (reasoning) {
|
|
254
267
|
events.push({
|
|
255
268
|
kind: "artifact_created",
|
|
256
|
-
ts: toIso(record
|
|
269
|
+
ts: toIso(getRecordTimestamp(record), now),
|
|
257
270
|
actor: { type: "agent", id: "codex" },
|
|
258
271
|
scope: activeIntentId
|
|
259
272
|
? { intent_id: activeIntentId, module: "reasoning" }
|
|
@@ -274,7 +287,7 @@ export const codexJsonlAdapter = {
|
|
|
274
287
|
if (assistantMessage) {
|
|
275
288
|
events.push({
|
|
276
289
|
kind: "artifact_created",
|
|
277
|
-
ts: toIso(record
|
|
290
|
+
ts: toIso(getRecordTimestamp(record), now),
|
|
278
291
|
actor: { type: "agent", id: "codex" },
|
|
279
292
|
scope: activeIntentId
|
|
280
293
|
? { intent_id: activeIntentId, module: "assistant_output" }
|
|
@@ -295,7 +308,16 @@ export const codexJsonlAdapter = {
|
|
|
295
308
|
events.push(...mapResponseItem(record, activeIntentId, now));
|
|
296
309
|
}
|
|
297
310
|
}
|
|
298
|
-
const
|
|
311
|
+
const lastRecordTs = records.reduce((acc, r) => {
|
|
312
|
+
const t = getRecordTimestamp(r);
|
|
313
|
+
const ms = typeof t === "number" && Number.isFinite(t) ? t : (typeof t === "string" ? new Date(t).getTime() : NaN);
|
|
314
|
+
if (Number.isNaN(ms))
|
|
315
|
+
return acc;
|
|
316
|
+
return acc == null ? ms : Math.max(acc, ms);
|
|
317
|
+
}, null);
|
|
318
|
+
const endTs = lastRecordTs != null
|
|
319
|
+
? new Date(lastRecordTs).toISOString()
|
|
320
|
+
: toIso(records[records.length - 1]?.timestamp, now);
|
|
299
321
|
events.push({
|
|
300
322
|
kind: "session_end",
|
|
301
323
|
ts: endTs,
|
package/dist/dashboard.js
CHANGED
|
@@ -2,10 +2,12 @@ import { createServer } from "node:http";
|
|
|
2
2
|
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { extname, join, normalize, resolve } from "node:path";
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { EVENT_SCHEMA_VERSION } from "./event-envelope.js";
|
|
5
6
|
import { getDashboardHost, getDashboardPort, getDashboardWebappDir, getSessionsDir, isDashboardEnabled, } from "./config.js";
|
|
6
7
|
import { exportSessionJson } from "./store.js";
|
|
7
8
|
import { handleGatewayAct, handleGatewayBeginRun, handleGatewayEndRun } from "./tools.js";
|
|
8
9
|
import { ingestRawContent } from "./ingest.js";
|
|
10
|
+
import { deriveIntentTokenBreakdown, generateFollowupArtifacts, } from "./local-analysis.js";
|
|
9
11
|
const importSets = new Map();
|
|
10
12
|
function json(res, status, payload) {
|
|
11
13
|
res.writeHead(status, {
|
|
@@ -170,6 +172,66 @@ function summarizeSessionPayload(payload) {
|
|
|
170
172
|
event_count: payload.events.length,
|
|
171
173
|
};
|
|
172
174
|
}
|
|
175
|
+
/** Merge multiple session payloads into one: combined events sorted by ts, single session_id, re-sequenced. */
|
|
176
|
+
function mergeSessionPayloads(payloads) {
|
|
177
|
+
if (payloads.length === 0)
|
|
178
|
+
throw new Error("No payloads to merge.");
|
|
179
|
+
if (payloads.length === 1)
|
|
180
|
+
return payloads[0];
|
|
181
|
+
const mergedSessionId = `merged_${Date.now()}_${randomUUID().slice(0, 8)}`;
|
|
182
|
+
const expectedTotal = payloads.reduce((sum, p) => sum + p.events.length, 0);
|
|
183
|
+
const allEvents = [];
|
|
184
|
+
for (const payload of payloads) {
|
|
185
|
+
for (const e of payload.events) {
|
|
186
|
+
allEvents.push({
|
|
187
|
+
id: e.id,
|
|
188
|
+
session_id: mergedSessionId,
|
|
189
|
+
seq: e.seq,
|
|
190
|
+
ts: e.ts,
|
|
191
|
+
kind: e.kind,
|
|
192
|
+
actor: { ...e.actor },
|
|
193
|
+
scope: e.scope ? { ...e.scope } : undefined,
|
|
194
|
+
payload: typeof e.payload === "object" && e.payload !== null ? { ...e.payload } : e.payload,
|
|
195
|
+
derived: e.derived,
|
|
196
|
+
confidence: e.confidence,
|
|
197
|
+
visibility: e.visibility,
|
|
198
|
+
schema_version: e.schema_version ?? EVENT_SCHEMA_VERSION,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (allEvents.length !== expectedTotal) {
|
|
203
|
+
throw new Error(`Merge event count mismatch: collected ${allEvents.length}, expected ${expectedTotal} from ${payloads.length} payload(s).`);
|
|
204
|
+
}
|
|
205
|
+
allEvents.sort((a, b) => {
|
|
206
|
+
const tsCmp = a.ts.localeCompare(b.ts);
|
|
207
|
+
if (tsCmp !== 0)
|
|
208
|
+
return tsCmp;
|
|
209
|
+
return (a.seq ?? 0) - (b.seq ?? 0);
|
|
210
|
+
});
|
|
211
|
+
let seq = 0;
|
|
212
|
+
const mergedEvents = allEvents.map((e) => {
|
|
213
|
+
seq += 1;
|
|
214
|
+
return {
|
|
215
|
+
...e,
|
|
216
|
+
id: `${mergedSessionId}:${seq}:${randomUUID().slice(0, 8)}`,
|
|
217
|
+
seq,
|
|
218
|
+
schema_version: e.schema_version ?? EVENT_SCHEMA_VERSION,
|
|
219
|
+
};
|
|
220
|
+
});
|
|
221
|
+
const first = payloads[0];
|
|
222
|
+
const last = payloads[payloads.length - 1];
|
|
223
|
+
const firstStart = mergedEvents.find((e) => e.kind === "session_start");
|
|
224
|
+
const lastEnd = [...mergedEvents].reverse().find((e) => e.kind === "session_end");
|
|
225
|
+
const startPayload = (firstStart?.payload ?? {});
|
|
226
|
+
return {
|
|
227
|
+
session_id: mergedSessionId,
|
|
228
|
+
goal: typeof startPayload.goal === "string" ? startPayload.goal : first.goal ?? "Merged session",
|
|
229
|
+
user_prompt: first.user_prompt,
|
|
230
|
+
started_at: firstStart?.ts ?? first.started_at ?? mergedEvents[0]?.ts,
|
|
231
|
+
ended_at: lastEnd?.ts ?? last.ended_at,
|
|
232
|
+
events: mergedEvents,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
173
235
|
function buildFollowupFromEvents(events, focus) {
|
|
174
236
|
const risks = events.filter((event) => event.kind === "risk_signal");
|
|
175
237
|
const fails = events.filter((event) => event.kind === "verification" && event.payload?.result === "fail");
|
|
@@ -475,7 +537,7 @@ async function handleApi(req, res, pathname) {
|
|
|
475
537
|
json(res, 400, { error: "Missing `files` array. Provide one or more MCP canonical session logs." });
|
|
476
538
|
return true;
|
|
477
539
|
}
|
|
478
|
-
const
|
|
540
|
+
const acceptedPayloads = [];
|
|
479
541
|
const rejected_files = [];
|
|
480
542
|
for (const item of filesRaw) {
|
|
481
543
|
const file = item;
|
|
@@ -486,13 +548,7 @@ async function handleApi(req, res, pathname) {
|
|
|
486
548
|
}
|
|
487
549
|
try {
|
|
488
550
|
const payload = parseSessionContent(file.content);
|
|
489
|
-
|
|
490
|
-
const summary = summarizeSessionPayload(payload);
|
|
491
|
-
accepted.push({
|
|
492
|
-
...summary,
|
|
493
|
-
size_bytes: Buffer.byteLength(file.content, "utf-8"),
|
|
494
|
-
updated_at: new Date().toISOString(),
|
|
495
|
-
});
|
|
551
|
+
acceptedPayloads.push(payload);
|
|
496
552
|
}
|
|
497
553
|
catch (error) {
|
|
498
554
|
rejected_files.push({
|
|
@@ -503,24 +559,43 @@ async function handleApi(req, res, pathname) {
|
|
|
503
559
|
});
|
|
504
560
|
}
|
|
505
561
|
}
|
|
506
|
-
if (
|
|
562
|
+
if (acceptedPayloads.length === 0) {
|
|
507
563
|
json(res, 400, {
|
|
508
564
|
error: "No canonical MCP session logs were accepted.",
|
|
509
565
|
rejected_files,
|
|
510
566
|
});
|
|
511
567
|
return true;
|
|
512
568
|
}
|
|
569
|
+
const mergedPayload = acceptedPayloads.length === 1
|
|
570
|
+
? acceptedPayloads[0]
|
|
571
|
+
: mergeSessionPayloads(acceptedPayloads);
|
|
572
|
+
writeCanonicalSession(mergedPayload);
|
|
573
|
+
const summary = summarizeSessionPayload(mergedPayload);
|
|
574
|
+
const accepted = [
|
|
575
|
+
{
|
|
576
|
+
...summary,
|
|
577
|
+
size_bytes: mergedPayload.events.reduce((acc, e) => acc + Buffer.byteLength(JSON.stringify(e), "utf-8"), 0),
|
|
578
|
+
updated_at: new Date().toISOString(),
|
|
579
|
+
},
|
|
580
|
+
];
|
|
513
581
|
const import_set_id = `iset_${Date.now()}_${randomUUID().slice(0, 8)}`;
|
|
514
582
|
importSets.set(import_set_id, {
|
|
515
583
|
import_set_id,
|
|
516
584
|
created_at: new Date().toISOString(),
|
|
517
|
-
session_ids: [
|
|
585
|
+
session_ids: [mergedPayload.session_id],
|
|
518
586
|
});
|
|
587
|
+
const multipleRejected = filesRaw.length > 1 && rejected_files.length > 0;
|
|
519
588
|
json(res, 200, {
|
|
520
589
|
import_set_id,
|
|
521
590
|
sessions: accepted,
|
|
522
591
|
rejected_files,
|
|
523
|
-
|
|
592
|
+
accepted_file_count: acceptedPayloads.length,
|
|
593
|
+
total_event_count: mergedPayload.events.length,
|
|
594
|
+
guidance: multipleRejected
|
|
595
|
+
? `${acceptedPayloads.length} of ${filesRaw.length} files were valid. Fix or remove rejected files to merge all session logs.`
|
|
596
|
+
: acceptedPayloads.length > 1
|
|
597
|
+
? "Multiple session logs were merged into one. Raw logs will merge into this combined session."
|
|
598
|
+
: "Import only relevant or consecutive sessions from the same thread/conversation to avoid noisy insights.",
|
|
524
599
|
});
|
|
525
600
|
}
|
|
526
601
|
catch (error) {
|
|
@@ -584,7 +659,15 @@ async function handleApi(req, res, pathname) {
|
|
|
584
659
|
try {
|
|
585
660
|
const body = await readJsonBody(req);
|
|
586
661
|
const scope = typeof body.scope === "string" ? body.scope : "session";
|
|
587
|
-
const focus =
|
|
662
|
+
const focus = body.focus === "risk" ||
|
|
663
|
+
body.focus === "verification_gap" ||
|
|
664
|
+
body.focus === "hotspot" ||
|
|
665
|
+
body.focus === "rework_pattern" ||
|
|
666
|
+
body.focus === "efficiency"
|
|
667
|
+
? body.focus
|
|
668
|
+
: "risk";
|
|
669
|
+
const mode = body.mode === "session" ? "session" : "per_intent";
|
|
670
|
+
const strictness = body.strictness === "advisory" || body.strictness === "hard" ? body.strictness : "soft";
|
|
588
671
|
const importSetId = typeof body.import_set_id === "string" ? body.import_set_id : "";
|
|
589
672
|
const sessionId = typeof body.session_id === "string" ? body.session_id : "";
|
|
590
673
|
const summaries = listSessionFiles();
|
|
@@ -609,10 +692,16 @@ async function handleApi(req, res, pathname) {
|
|
|
609
692
|
json(res, 404, { error: "No events found for requested follow-up scope." });
|
|
610
693
|
return true;
|
|
611
694
|
}
|
|
612
|
-
const generated =
|
|
695
|
+
const generated = generateFollowupArtifacts(collectedEvents, {
|
|
696
|
+
focus,
|
|
697
|
+
mode,
|
|
698
|
+
strictness,
|
|
699
|
+
});
|
|
613
700
|
json(res, 200, {
|
|
614
701
|
scope,
|
|
615
702
|
focus,
|
|
703
|
+
mode,
|
|
704
|
+
strictness,
|
|
616
705
|
session_ids: scopedSessionIds,
|
|
617
706
|
generated_at: new Date().toISOString(),
|
|
618
707
|
...generated,
|
|
@@ -656,6 +745,29 @@ async function handleApi(req, res, pathname) {
|
|
|
656
745
|
}
|
|
657
746
|
return true;
|
|
658
747
|
}
|
|
748
|
+
if (key.endsWith("/token-breakdown")) {
|
|
749
|
+
const rawKey = key.slice(0, -"/token-breakdown".length);
|
|
750
|
+
const summary = listSessionFiles().find((item) => item.key === rawKey || item.session_id === rawKey);
|
|
751
|
+
if (!summary) {
|
|
752
|
+
json(res, 404, { error: "Session not found." });
|
|
753
|
+
return true;
|
|
754
|
+
}
|
|
755
|
+
try {
|
|
756
|
+
const payload = readSessionFile(summary.absolute_path);
|
|
757
|
+
const breakdown = deriveIntentTokenBreakdown(payload.events);
|
|
758
|
+
json(res, 200, {
|
|
759
|
+
session_id: summary.session_id,
|
|
760
|
+
generated_at: new Date().toISOString(),
|
|
761
|
+
...breakdown,
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
catch (error) {
|
|
765
|
+
json(res, 500, {
|
|
766
|
+
error: error instanceof Error ? error.message : "Failed to derive token breakdown.",
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
return true;
|
|
770
|
+
}
|
|
659
771
|
const summary = listSessionFiles().find((item) => item.key === key || item.session_id === key);
|
|
660
772
|
if (!summary) {
|
|
661
773
|
json(res, 404, { error: "Session not found." });
|
package/dist/index.js
CHANGED
|
File without changes
|
package/dist/ingest.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export interface IngestResult {
|
|
|
8
8
|
adapter: string;
|
|
9
9
|
inserted: number;
|
|
10
10
|
skipped_duplicates: number;
|
|
11
|
+
/** When merging into an existing session, number of raw events dropped by the time-window filter. */
|
|
12
|
+
filtered_out_by_time_window?: number;
|
|
11
13
|
session_path: string;
|
|
12
14
|
raw_path: string;
|
|
13
15
|
merge_strategy: "explicit_merge" | "adapted_session_id" | "fingerprint_match" | "new_session";
|
package/dist/ingest.js
CHANGED
|
@@ -89,6 +89,59 @@ function toMs(raw) {
|
|
|
89
89
|
const ms = new Date(raw).getTime();
|
|
90
90
|
return Number.isNaN(ms) ? undefined : ms;
|
|
91
91
|
}
|
|
92
|
+
/** Padding (ms) around session time range when merging. Use 0 so only events strictly inside the session window are kept; raw adapters often synthesize timestamps at "now" which would otherwise fall in session_end+padding. */
|
|
93
|
+
const MERGE_TIME_PADDING_MS = 0;
|
|
94
|
+
/**
|
|
95
|
+
* Session time range from existing canonical events.
|
|
96
|
+
* Uses session_start / session_end ts when present, else first/last event ts.
|
|
97
|
+
*/
|
|
98
|
+
function getSessionTimeRange(existing) {
|
|
99
|
+
if (existing.length === 0) {
|
|
100
|
+
const now = Date.now();
|
|
101
|
+
return { startMs: now, endMs: now };
|
|
102
|
+
}
|
|
103
|
+
let sessionStartMs;
|
|
104
|
+
let sessionEndMs;
|
|
105
|
+
for (const e of existing) {
|
|
106
|
+
const ms = toMs(e.ts);
|
|
107
|
+
if (ms == null)
|
|
108
|
+
continue;
|
|
109
|
+
if (e.kind === "session_start")
|
|
110
|
+
sessionStartMs = ms;
|
|
111
|
+
if (e.kind === "session_end")
|
|
112
|
+
sessionEndMs = ms;
|
|
113
|
+
}
|
|
114
|
+
const firstMs = toMs(existing[0]?.ts);
|
|
115
|
+
const lastMs = toMs(existing[existing.length - 1]?.ts);
|
|
116
|
+
return {
|
|
117
|
+
startMs: sessionStartMs ?? firstMs ?? Date.now(),
|
|
118
|
+
endMs: sessionEndMs ?? lastMs ?? Date.now(),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* When merging raw into an existing session, keep only events that fall inside the
|
|
123
|
+
* session time window (with padding) and drop session_start/session_end from raw.
|
|
124
|
+
* Events without a parseable ts are excluded. Events on a different calendar day (UTC)
|
|
125
|
+
* from the session range are always excluded to avoid timezone/clock skew.
|
|
126
|
+
*/
|
|
127
|
+
function filterAdaptedEventsForMerge(events, range, paddingMs, fallbackTs) {
|
|
128
|
+
const minMs = range.startMs - paddingMs;
|
|
129
|
+
const maxMs = range.endMs + paddingMs;
|
|
130
|
+
const startDay = new Date(range.startMs).toISOString().slice(0, 10);
|
|
131
|
+
const endDay = new Date(range.endMs).toISOString().slice(0, 10);
|
|
132
|
+
return events.filter((event) => {
|
|
133
|
+
if (event.kind === "session_start" || event.kind === "session_end")
|
|
134
|
+
return false;
|
|
135
|
+
const ts = toIso(event.ts, fallbackTs);
|
|
136
|
+
const ms = toMs(ts);
|
|
137
|
+
if (ms == null)
|
|
138
|
+
return false;
|
|
139
|
+
const eventDay = new Date(ms).toISOString().slice(0, 10);
|
|
140
|
+
if (eventDay < startDay || eventDay > endDay)
|
|
141
|
+
return false;
|
|
142
|
+
return ms >= minMs && ms <= maxMs;
|
|
143
|
+
});
|
|
144
|
+
}
|
|
92
145
|
function normalizeFingerprint(value) {
|
|
93
146
|
if (!value)
|
|
94
147
|
return "";
|
|
@@ -362,13 +415,21 @@ export function ingestRawContent(raw, options = {}) {
|
|
|
362
415
|
const doDedupe = options.dedupe ?? true;
|
|
363
416
|
const fallbackTs = new Date().toISOString();
|
|
364
417
|
const isMerge = existing.length > 0;
|
|
418
|
+
const isExplicitMerge = isMerge && Boolean(options.merge_session_id);
|
|
365
419
|
const existingSemanticKeys = new Set(existing.map((e) => semanticDedupeKey(e)));
|
|
366
420
|
const existingExactKeys = new Set(existing.map((e) => dedupeKey(e)));
|
|
421
|
+
const range = getSessionTimeRange(existing);
|
|
422
|
+
const eventsToProcess = isExplicitMerge
|
|
423
|
+
? filterAdaptedEventsForMerge(adapted.events, range, MERGE_TIME_PADDING_MS, fallbackTs)
|
|
424
|
+
: adapted.events;
|
|
425
|
+
const filteredOutByTimeWindow = isExplicitMerge && adapted.events.length > eventsToProcess.length
|
|
426
|
+
? adapted.events.length - eventsToProcess.length
|
|
427
|
+
: undefined;
|
|
367
428
|
let seq = existing.length > 0 ? Math.max(...existing.map((e) => e.seq)) : 0;
|
|
368
429
|
let inserted = 0;
|
|
369
430
|
let skipped = 0;
|
|
370
431
|
const toInsert = [];
|
|
371
|
-
for (const event of
|
|
432
|
+
for (const event of eventsToProcess) {
|
|
372
433
|
const candidate = buildCanonicalEvent(sessionId, seq + 1, event, fallbackTs);
|
|
373
434
|
if (doDedupe) {
|
|
374
435
|
if (isMerge && existingSemanticKeys.has(semanticDedupeKey(candidate))) {
|
|
@@ -402,7 +463,7 @@ export function ingestRawContent(raw, options = {}) {
|
|
|
402
463
|
sessionPath = writeEventsAppend(sessionId, toInsert);
|
|
403
464
|
}
|
|
404
465
|
const rawPath = writeRawSidecar(sessionId, adapted.source, raw);
|
|
405
|
-
|
|
466
|
+
const result = {
|
|
406
467
|
session_id: sessionId,
|
|
407
468
|
adapter: adapted.source,
|
|
408
469
|
inserted,
|
|
@@ -412,6 +473,10 @@ export function ingestRawContent(raw, options = {}) {
|
|
|
412
473
|
merge_strategy: sessionSelection.strategy,
|
|
413
474
|
merge_confidence: sessionSelection.fingerprint_confidence,
|
|
414
475
|
};
|
|
476
|
+
if (filteredOutByTimeWindow !== undefined) {
|
|
477
|
+
result.filtered_out_by_time_window = filteredOutByTimeWindow;
|
|
478
|
+
}
|
|
479
|
+
return result;
|
|
415
480
|
}
|
|
416
481
|
export function ingestRawFile(filePath, options = {}) {
|
|
417
482
|
const raw = readFileSync(filePath, "utf-8");
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { CanonicalEvent } from "./event-envelope.js";
|
|
2
|
+
export type FollowupMode = "per_intent" | "session";
|
|
3
|
+
export type FollowupStrictness = "soft" | "advisory" | "hard";
|
|
4
|
+
export type FollowupFocus = "risk" | "verification_gap" | "hotspot" | "rework_pattern" | "efficiency";
|
|
5
|
+
export type RuleTemplateId = "high_risk_guardrail" | "verification_loop_control" | "context_retrieval_limit" | "file_check_dedup" | "hotspot_change_gate";
|
|
6
|
+
export interface ValueClaims {
|
|
7
|
+
risk_mitigation: string[];
|
|
8
|
+
efficiency_improvement: string[];
|
|
9
|
+
quality_standardization: string[];
|
|
10
|
+
}
|
|
11
|
+
export interface EvidenceRef {
|
|
12
|
+
event_id: string;
|
|
13
|
+
reason: string;
|
|
14
|
+
file?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface IntentRuleFeatures {
|
|
17
|
+
intent_id: string;
|
|
18
|
+
intent_title: string;
|
|
19
|
+
risk_score: number;
|
|
20
|
+
efficiency_waste_score: number;
|
|
21
|
+
stability_score: number;
|
|
22
|
+
high_risk_signals: number;
|
|
23
|
+
verification_fail_count: number;
|
|
24
|
+
verification_unknown_count: number;
|
|
25
|
+
hotspot_score: number;
|
|
26
|
+
repeated_call_count: number;
|
|
27
|
+
repeated_file_check_count: number;
|
|
28
|
+
file_ops_count: number;
|
|
29
|
+
token_total: number;
|
|
30
|
+
}
|
|
31
|
+
export interface FollowupArtifact {
|
|
32
|
+
intent_id: string;
|
|
33
|
+
intent_title: string;
|
|
34
|
+
confidence: number;
|
|
35
|
+
rule_template_id: RuleTemplateId;
|
|
36
|
+
features: IntentRuleFeatures;
|
|
37
|
+
value_claims: ValueClaims;
|
|
38
|
+
evidence_refs: EvidenceRef[];
|
|
39
|
+
rule_spec: Record<string, unknown>;
|
|
40
|
+
skill_draft: string;
|
|
41
|
+
insufficient_evidence?: boolean;
|
|
42
|
+
}
|
|
43
|
+
export interface FollowupSummary {
|
|
44
|
+
intent_count: number;
|
|
45
|
+
high_risk_intents: number;
|
|
46
|
+
high_efficiency_waste_intents: number;
|
|
47
|
+
avg_confidence: number;
|
|
48
|
+
}
|
|
49
|
+
export interface FollowupGenerationResult {
|
|
50
|
+
artifacts: FollowupArtifact[];
|
|
51
|
+
summary: FollowupSummary;
|
|
52
|
+
insufficient_evidence: boolean;
|
|
53
|
+
}
|
|
54
|
+
export interface TokenSplitAttribution {
|
|
55
|
+
checkpoint_event_id: string;
|
|
56
|
+
total_tokens: number;
|
|
57
|
+
context_tokens: number;
|
|
58
|
+
output_tokens: number;
|
|
59
|
+
unknown_tokens: number;
|
|
60
|
+
neighbor_event_ids: string[];
|
|
61
|
+
}
|
|
62
|
+
export interface IntentTokenBreakdown {
|
|
63
|
+
intent_id: string;
|
|
64
|
+
intent_title: string;
|
|
65
|
+
total_tokens: number;
|
|
66
|
+
context_tokens: number;
|
|
67
|
+
output_tokens: number;
|
|
68
|
+
unknown_tokens: number;
|
|
69
|
+
estimated_cost_usd?: number;
|
|
70
|
+
event_window: {
|
|
71
|
+
start_seq: number;
|
|
72
|
+
end_seq: number;
|
|
73
|
+
};
|
|
74
|
+
supporting_events: TokenSplitAttribution[];
|
|
75
|
+
}
|
|
76
|
+
export interface TokenBreakdownResult {
|
|
77
|
+
intent_breakdown: IntentTokenBreakdown[];
|
|
78
|
+
totals: {
|
|
79
|
+
total_tokens: number;
|
|
80
|
+
context_tokens: number;
|
|
81
|
+
output_tokens: number;
|
|
82
|
+
unknown_tokens: number;
|
|
83
|
+
estimated_cost_usd?: number;
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
export declare function generateFollowupArtifacts(events: CanonicalEvent[], input?: {
|
|
87
|
+
mode?: FollowupMode;
|
|
88
|
+
strictness?: FollowupStrictness;
|
|
89
|
+
focus?: FollowupFocus;
|
|
90
|
+
}): FollowupGenerationResult;
|
|
91
|
+
export declare function deriveIntentTokenBreakdown(events: CanonicalEvent[]): TokenBreakdownResult;
|
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
const MAX_REPEATED_CHECKS_SOFT = 3;
|
|
2
|
+
const MAX_REPEATED_CALLS_SOFT = 4;
|
|
3
|
+
const TOKEN_NEIGHBORHOOD_RADIUS_SEQ = 5;
|
|
4
|
+
const MIN_RULE_CONFIDENCE = 0.45;
|
|
5
|
+
function toNumber(value) {
|
|
6
|
+
if (typeof value === "number")
|
|
7
|
+
return Number.isFinite(value) ? value : 0;
|
|
8
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
9
|
+
const parsed = Number(value);
|
|
10
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
11
|
+
}
|
|
12
|
+
return 0;
|
|
13
|
+
}
|
|
14
|
+
function toStringValue(value) {
|
|
15
|
+
return typeof value === "string" && value.trim() !== "" ? value : undefined;
|
|
16
|
+
}
|
|
17
|
+
function payload(event) {
|
|
18
|
+
return event.payload ?? {};
|
|
19
|
+
}
|
|
20
|
+
function getIntentId(event) {
|
|
21
|
+
const p = payload(event);
|
|
22
|
+
const payloadIntent = toStringValue(p.intent_id);
|
|
23
|
+
return event.scope?.intent_id ?? payloadIntent;
|
|
24
|
+
}
|
|
25
|
+
function getIntentTitle(event) {
|
|
26
|
+
const p = payload(event);
|
|
27
|
+
return toStringValue(p.title) ?? toStringValue(p.description);
|
|
28
|
+
}
|
|
29
|
+
function getFileTarget(event) {
|
|
30
|
+
if (event.kind !== "file_op")
|
|
31
|
+
return undefined;
|
|
32
|
+
const p = payload(event);
|
|
33
|
+
return toStringValue(p.target) ?? event.scope?.file;
|
|
34
|
+
}
|
|
35
|
+
function getTokenUsage(event) {
|
|
36
|
+
const p = payload(event);
|
|
37
|
+
const direct = p.usage;
|
|
38
|
+
const details = p.details;
|
|
39
|
+
const nested = details && typeof details === "object" ? details.llm_usage : undefined;
|
|
40
|
+
const usage = direct && typeof direct === "object"
|
|
41
|
+
? direct
|
|
42
|
+
: nested && typeof nested === "object"
|
|
43
|
+
? nested
|
|
44
|
+
: null;
|
|
45
|
+
if (!usage)
|
|
46
|
+
return null;
|
|
47
|
+
const prompt = toNumber(usage.prompt_tokens);
|
|
48
|
+
const completion = toNumber(usage.completion_tokens);
|
|
49
|
+
const totalRaw = toNumber(usage.total_tokens);
|
|
50
|
+
const total = totalRaw > 0 ? totalRaw : prompt + completion;
|
|
51
|
+
if (total <= 0)
|
|
52
|
+
return null;
|
|
53
|
+
const cost = toNumber(usage.estimated_cost_usd);
|
|
54
|
+
return {
|
|
55
|
+
total_tokens: total,
|
|
56
|
+
estimated_cost_usd: cost > 0 ? cost : undefined,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function percentile(values, p) {
|
|
60
|
+
if (values.length === 0)
|
|
61
|
+
return 0;
|
|
62
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
63
|
+
const idx = Math.max(0, Math.min(sorted.length - 1, Math.floor((sorted.length - 1) * p)));
|
|
64
|
+
return sorted[idx];
|
|
65
|
+
}
|
|
66
|
+
function buildIntentBuckets(eventsRaw) {
|
|
67
|
+
const events = [...eventsRaw].sort((a, b) => (a.seq === b.seq ? a.ts.localeCompare(b.ts) : a.seq - b.seq));
|
|
68
|
+
const boundaryEvents = events.filter((event) => event.kind === "intent");
|
|
69
|
+
const boundaries = boundaryEvents.map((event) => ({
|
|
70
|
+
intent_id: getIntentId(event) ?? `intent_${event.seq}`,
|
|
71
|
+
intent_title: getIntentTitle(event) ?? `Intent ${event.seq}`,
|
|
72
|
+
seq: event.seq,
|
|
73
|
+
}));
|
|
74
|
+
const boundaryById = new Map(boundaries.map((item) => [item.intent_id, item.intent_title]));
|
|
75
|
+
const fallbackId = "intent_fallback";
|
|
76
|
+
const byIntent = new Map();
|
|
77
|
+
function inferIntent(event) {
|
|
78
|
+
const explicit = getIntentId(event);
|
|
79
|
+
if (explicit)
|
|
80
|
+
return explicit;
|
|
81
|
+
let latest;
|
|
82
|
+
for (const boundary of boundaries) {
|
|
83
|
+
if (boundary.seq <= event.seq)
|
|
84
|
+
latest = boundary.intent_id;
|
|
85
|
+
else
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
return latest ?? fallbackId;
|
|
89
|
+
}
|
|
90
|
+
for (const event of events) {
|
|
91
|
+
if (event.kind === "session_start" || event.kind === "session_end")
|
|
92
|
+
continue;
|
|
93
|
+
const intentId = inferIntent(event);
|
|
94
|
+
const list = byIntent.get(intentId) ?? [];
|
|
95
|
+
list.push(event);
|
|
96
|
+
byIntent.set(intentId, list);
|
|
97
|
+
}
|
|
98
|
+
const orderedIds = [...new Set([...boundaries.map((item) => item.intent_id), ...byIntent.keys()])];
|
|
99
|
+
return orderedIds.map((intentId) => {
|
|
100
|
+
const intentEvents = byIntent.get(intentId) ?? [];
|
|
101
|
+
const startSeq = intentEvents[0]?.seq ?? 0;
|
|
102
|
+
const endSeq = intentEvents[intentEvents.length - 1]?.seq ?? startSeq;
|
|
103
|
+
return {
|
|
104
|
+
intent_id: intentId,
|
|
105
|
+
intent_title: boundaryById.get(intentId) ?? (intentId === fallbackId ? "Fallback intent" : intentId),
|
|
106
|
+
start_seq: startSeq,
|
|
107
|
+
end_seq: endSeq,
|
|
108
|
+
events: intentEvents,
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
function isFileCheckEvent(event) {
|
|
113
|
+
if (event.kind !== "tool_call")
|
|
114
|
+
return false;
|
|
115
|
+
const p = payload(event);
|
|
116
|
+
const action = (toStringValue(p.action) ?? "").toLowerCase();
|
|
117
|
+
const target = (toStringValue(p.target) ?? "").toLowerCase();
|
|
118
|
+
if (!action && !target)
|
|
119
|
+
return false;
|
|
120
|
+
const looksCheck = /(read|cat|open|view|inspect|grep|rg|ls|stat)/.test(action);
|
|
121
|
+
const looksFile = /\/|\\|\.ts$|\.tsx$|\.js$|\.json$|\.md$|\.yml$|\.yaml$/.test(target);
|
|
122
|
+
return looksCheck || looksFile;
|
|
123
|
+
}
|
|
124
|
+
function isContextEvent(event) {
|
|
125
|
+
if (event.kind === "tool_call") {
|
|
126
|
+
const category = toStringValue(payload(event).category);
|
|
127
|
+
return category === "search" || category === "tool" || category === "execution";
|
|
128
|
+
}
|
|
129
|
+
if (event.kind === "artifact_created") {
|
|
130
|
+
const module = (event.scope?.module ?? "").toLowerCase();
|
|
131
|
+
const artifactType = (toStringValue(payload(event).artifact_type) ?? "").toLowerCase();
|
|
132
|
+
return module === "reasoning" || artifactType === "reasoning";
|
|
133
|
+
}
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
function isOutputEvent(event) {
|
|
137
|
+
if (event.kind === "file_op" || event.kind === "diff_summary")
|
|
138
|
+
return true;
|
|
139
|
+
if (event.kind === "artifact_created") {
|
|
140
|
+
const artifactType = (toStringValue(payload(event).artifact_type) ?? "").toLowerCase();
|
|
141
|
+
return ["file", "patch", "report", "test", "build", "migration", "pr"].includes(artifactType);
|
|
142
|
+
}
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
function deriveIntentFeatures(bucket, repeatedCallThreshold, repeatedCheckThreshold) {
|
|
146
|
+
const evidence = [];
|
|
147
|
+
let highRiskSignals = 0;
|
|
148
|
+
let verificationFailCount = 0;
|
|
149
|
+
let verificationUnknownCount = 0;
|
|
150
|
+
let hotspotScore = 0;
|
|
151
|
+
let fileOpsCount = 0;
|
|
152
|
+
let tokenTotal = 0;
|
|
153
|
+
let tokenCost = 0;
|
|
154
|
+
const callKeyCount = new Map();
|
|
155
|
+
const fileCheckCount = new Map();
|
|
156
|
+
const editedFiles = new Set();
|
|
157
|
+
let diffSummaryCount = 0;
|
|
158
|
+
let outputArtifacts = 0;
|
|
159
|
+
for (const event of bucket.events) {
|
|
160
|
+
const p = payload(event);
|
|
161
|
+
if (event.kind === "risk_signal") {
|
|
162
|
+
const level = toStringValue(p.level) ?? "low";
|
|
163
|
+
if (level === "high")
|
|
164
|
+
highRiskSignals += 3;
|
|
165
|
+
else if (level === "medium")
|
|
166
|
+
highRiskSignals += 2;
|
|
167
|
+
else
|
|
168
|
+
highRiskSignals += 1;
|
|
169
|
+
evidence.push({ event_id: event.id, reason: `risk_signal:${level}`, file: event.scope?.file });
|
|
170
|
+
}
|
|
171
|
+
if (event.kind === "assumption") {
|
|
172
|
+
const risk = toStringValue(p.risk);
|
|
173
|
+
if (risk === "high")
|
|
174
|
+
highRiskSignals += 2;
|
|
175
|
+
else if (risk === "medium")
|
|
176
|
+
highRiskSignals += 1;
|
|
177
|
+
}
|
|
178
|
+
if (event.kind === "verification") {
|
|
179
|
+
const result = toStringValue(p.result);
|
|
180
|
+
if (result === "fail") {
|
|
181
|
+
verificationFailCount += 1;
|
|
182
|
+
evidence.push({ event_id: event.id, reason: "verification:fail", file: event.scope?.file });
|
|
183
|
+
}
|
|
184
|
+
else if (result === "unknown") {
|
|
185
|
+
verificationUnknownCount += 1;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (event.kind === "hotspot") {
|
|
189
|
+
const score = toNumber(p.score);
|
|
190
|
+
hotspotScore += score;
|
|
191
|
+
evidence.push({ event_id: event.id, reason: `hotspot:${score}`, file: toStringValue(p.file) ?? event.scope?.file });
|
|
192
|
+
}
|
|
193
|
+
if (event.kind === "tool_call") {
|
|
194
|
+
const key = `${toStringValue(p.action) ?? "unknown"}::${toStringValue(p.target) ?? ""}`;
|
|
195
|
+
callKeyCount.set(key, (callKeyCount.get(key) ?? 0) + 1);
|
|
196
|
+
if (isFileCheckEvent(event)) {
|
|
197
|
+
const target = toStringValue(p.target) ?? toStringValue(event.scope?.file) ?? "unknown";
|
|
198
|
+
fileCheckCount.set(target, (fileCheckCount.get(target) ?? 0) + 1);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (event.kind === "file_op") {
|
|
202
|
+
fileOpsCount += 1;
|
|
203
|
+
const action = toStringValue(p.action);
|
|
204
|
+
if (action === "create" || action === "edit" || action === "delete") {
|
|
205
|
+
const target = getFileTarget(event);
|
|
206
|
+
if (target)
|
|
207
|
+
editedFiles.add(target);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (event.kind === "diff_summary")
|
|
211
|
+
diffSummaryCount += 1;
|
|
212
|
+
if (event.kind === "artifact_created" && isOutputEvent(event))
|
|
213
|
+
outputArtifacts += 1;
|
|
214
|
+
const usage = getTokenUsage(event);
|
|
215
|
+
if (usage) {
|
|
216
|
+
tokenTotal += usage.total_tokens;
|
|
217
|
+
tokenCost += usage.estimated_cost_usd ?? 0;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const repeatedCallCount = [...callKeyCount.values()].reduce((sum, count) => {
|
|
221
|
+
return sum + (count > repeatedCallThreshold ? count - repeatedCallThreshold : 0);
|
|
222
|
+
}, 0);
|
|
223
|
+
const repeatedFileCheckCount = [...fileCheckCount.entries()].reduce((sum, [target, count]) => {
|
|
224
|
+
if (count <= repeatedCheckThreshold)
|
|
225
|
+
return sum;
|
|
226
|
+
if (editedFiles.has(target))
|
|
227
|
+
return sum;
|
|
228
|
+
return sum + (count - repeatedCheckThreshold);
|
|
229
|
+
}, 0);
|
|
230
|
+
const deliverableDelta = fileOpsCount + diffSummaryCount + outputArtifacts;
|
|
231
|
+
const lowDeliverableWithHighToken = tokenTotal > 0 && tokenTotal > 2000 && deliverableDelta <= 1 ? 1 : 0;
|
|
232
|
+
const verificationLoop = verificationFailCount + verificationUnknownCount >= 3 ? 1 : 0;
|
|
233
|
+
const riskScoreRaw = highRiskSignals * 12 + verificationFailCount * 16 + Math.min(20, hotspotScore);
|
|
234
|
+
const efficiencyScoreRaw = repeatedCallCount * 10 + repeatedFileCheckCount * 12 + verificationLoop * 10 + lowDeliverableWithHighToken * 14;
|
|
235
|
+
const stabilityRaw = 100 - (verificationFailCount * 12 + verificationUnknownCount * 5 + repeatedCallCount * 2);
|
|
236
|
+
const riskScore = Math.max(0, Math.min(100, Math.round(riskScoreRaw)));
|
|
237
|
+
const efficiencyWasteScore = Math.max(0, Math.min(100, Math.round(efficiencyScoreRaw)));
|
|
238
|
+
const stabilityScore = Math.max(0, Math.min(100, Math.round(stabilityRaw)));
|
|
239
|
+
let template = "context_retrieval_limit";
|
|
240
|
+
if (riskScore >= 65 || (highRiskSignals >= 3 && verificationFailCount > 0))
|
|
241
|
+
template = "high_risk_guardrail";
|
|
242
|
+
else if (verificationLoop)
|
|
243
|
+
template = "verification_loop_control";
|
|
244
|
+
else if (repeatedFileCheckCount > 0)
|
|
245
|
+
template = "file_check_dedup";
|
|
246
|
+
else if (hotspotScore >= 8)
|
|
247
|
+
template = "hotspot_change_gate";
|
|
248
|
+
if (evidence.length === 0) {
|
|
249
|
+
const fallbackEvent = bucket.events.find((event) => event.kind === "tool_call" || event.kind === "file_op");
|
|
250
|
+
if (fallbackEvent)
|
|
251
|
+
evidence.push({ event_id: fallbackEvent.id, reason: "behavioral_pattern", file: fallbackEvent.scope?.file });
|
|
252
|
+
}
|
|
253
|
+
return {
|
|
254
|
+
features: {
|
|
255
|
+
intent_id: bucket.intent_id,
|
|
256
|
+
intent_title: bucket.intent_title,
|
|
257
|
+
risk_score: riskScore,
|
|
258
|
+
efficiency_waste_score: efficiencyWasteScore,
|
|
259
|
+
stability_score: stabilityScore,
|
|
260
|
+
high_risk_signals: highRiskSignals,
|
|
261
|
+
verification_fail_count: verificationFailCount,
|
|
262
|
+
verification_unknown_count: verificationUnknownCount,
|
|
263
|
+
hotspot_score: Number(hotspotScore.toFixed(2)),
|
|
264
|
+
repeated_call_count: repeatedCallCount,
|
|
265
|
+
repeated_file_check_count: repeatedFileCheckCount,
|
|
266
|
+
file_ops_count: fileOpsCount,
|
|
267
|
+
token_total: tokenTotal,
|
|
268
|
+
},
|
|
269
|
+
template,
|
|
270
|
+
evidence: evidence.slice(0, 12),
|
|
271
|
+
thresholdHints: {
|
|
272
|
+
repeated_call_threshold: repeatedCallThreshold,
|
|
273
|
+
repeated_check_threshold: repeatedCheckThreshold,
|
|
274
|
+
verification_loop_threshold: 3,
|
|
275
|
+
low_delta_token_threshold: 2000,
|
|
276
|
+
estimated_cost_usd: Number(tokenCost.toFixed(6)),
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
function valueClaimsForTemplate(template, features) {
|
|
281
|
+
const risk_mitigation = [];
|
|
282
|
+
const efficiency_improvement = [];
|
|
283
|
+
const quality_standardization = [];
|
|
284
|
+
if (template === "high_risk_guardrail" || features.verification_fail_count > 0 || features.high_risk_signals > 0) {
|
|
285
|
+
risk_mitigation.push("Mitigates high-risk regressions by enforcing stronger verification gates.");
|
|
286
|
+
}
|
|
287
|
+
if (template === "file_check_dedup" || template === "context_retrieval_limit" || features.repeated_call_count > 0) {
|
|
288
|
+
efficiency_improvement.push("Reduces unnecessary repeated calls/checks by applying bounded retrieval guardrails.");
|
|
289
|
+
}
|
|
290
|
+
quality_standardization.push("Standardizes intent execution with explicit evidence-based guardrails.");
|
|
291
|
+
return { risk_mitigation, efficiency_improvement, quality_standardization };
|
|
292
|
+
}
|
|
293
|
+
function buildRuleSpec(template, features, strictness, focus, thresholdHints, evidence) {
|
|
294
|
+
const softGuardrailAction = strictness === "soft"
|
|
295
|
+
? "warn_and_recommend"
|
|
296
|
+
: strictness === "advisory"
|
|
297
|
+
? "recommend_only"
|
|
298
|
+
: "block_until_ack";
|
|
299
|
+
return {
|
|
300
|
+
version: 1,
|
|
301
|
+
template_id: template,
|
|
302
|
+
focus,
|
|
303
|
+
strictness,
|
|
304
|
+
intent_id: features.intent_id,
|
|
305
|
+
intent_title: features.intent_title,
|
|
306
|
+
scores: {
|
|
307
|
+
risk_score: features.risk_score,
|
|
308
|
+
efficiency_waste_score: features.efficiency_waste_score,
|
|
309
|
+
stability_score: features.stability_score,
|
|
310
|
+
},
|
|
311
|
+
thresholds: thresholdHints,
|
|
312
|
+
soft_guardrails: [
|
|
313
|
+
{
|
|
314
|
+
trigger: "repeated_file_check_without_edit",
|
|
315
|
+
threshold: thresholdHints.repeated_check_threshold,
|
|
316
|
+
action: softGuardrailAction,
|
|
317
|
+
recommendation: "Consolidate file inspection and defer duplicate checks until meaningful edits occur.",
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
trigger: "repeated_tool_call_same_target",
|
|
321
|
+
threshold: thresholdHints.repeated_call_threshold,
|
|
322
|
+
action: softGuardrailAction,
|
|
323
|
+
recommendation: "Cache previously gathered context and avoid repeated identical retrieval calls.",
|
|
324
|
+
},
|
|
325
|
+
],
|
|
326
|
+
expected_outcomes: {
|
|
327
|
+
risk_reduction: features.risk_score >= 50 || features.verification_fail_count > 0,
|
|
328
|
+
efficiency_gain: features.efficiency_waste_score >= 35,
|
|
329
|
+
reduced_unnecessary_checks: features.repeated_file_check_count > 0 || features.repeated_call_count > 0,
|
|
330
|
+
},
|
|
331
|
+
evidence_refs: evidence,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
function buildSkillDraft(features, template, claims, thresholdHints, evidence) {
|
|
335
|
+
return [
|
|
336
|
+
`# Skill: ${features.intent_title} Guardrail`,
|
|
337
|
+
"",
|
|
338
|
+
"## Purpose",
|
|
339
|
+
"Apply deterministic checks that reduce wasteful context gathering and mitigate risk in similar tasks.",
|
|
340
|
+
"",
|
|
341
|
+
"## Template",
|
|
342
|
+
`- ${template}`,
|
|
343
|
+
"",
|
|
344
|
+
"## Value",
|
|
345
|
+
...(claims.risk_mitigation.length > 0 ? claims.risk_mitigation.map((item) => `- ${item}`) : []),
|
|
346
|
+
...(claims.efficiency_improvement.length > 0 ? claims.efficiency_improvement.map((item) => `- ${item}`) : []),
|
|
347
|
+
...(claims.quality_standardization.length > 0
|
|
348
|
+
? claims.quality_standardization.map((item) => `- ${item}`)
|
|
349
|
+
: []),
|
|
350
|
+
"",
|
|
351
|
+
"## Thresholds",
|
|
352
|
+
`- Repeated file checks threshold: ${thresholdHints.repeated_check_threshold}`,
|
|
353
|
+
`- Repeated call threshold: ${thresholdHints.repeated_call_threshold}`,
|
|
354
|
+
`- Verification loop threshold: ${thresholdHints.verification_loop_threshold}`,
|
|
355
|
+
"",
|
|
356
|
+
"## Workflow",
|
|
357
|
+
"1. Gather context once and cache references.",
|
|
358
|
+
"2. Avoid repeated retrieval calls for the same target unless state changed.",
|
|
359
|
+
"3. Prioritize edits and run targeted checks only when needed.",
|
|
360
|
+
"4. Escalate to full verification for high-risk or failing intents.",
|
|
361
|
+
"",
|
|
362
|
+
"## Evidence anchors",
|
|
363
|
+
...evidence.map((item) => `- ${item.event_id}${item.file ? ` (${item.file})` : ""}: ${item.reason}`),
|
|
364
|
+
"",
|
|
365
|
+
].join("\n");
|
|
366
|
+
}
|
|
367
|
+
function computeConfidence(features, evidenceCount) {
|
|
368
|
+
const strongSignals = (features.risk_score >= 55 ? 1 : 0) +
|
|
369
|
+
(features.efficiency_waste_score >= 45 ? 1 : 0) +
|
|
370
|
+
(features.verification_fail_count > 0 ? 1 : 0) +
|
|
371
|
+
(features.repeated_call_count > 0 ? 1 : 0) +
|
|
372
|
+
(features.repeated_file_check_count > 0 ? 1 : 0);
|
|
373
|
+
const confidence = 0.28 + strongSignals * 0.12 + Math.min(0.24, evidenceCount * 0.03);
|
|
374
|
+
return Math.max(0, Math.min(0.96, Number(confidence.toFixed(2))));
|
|
375
|
+
}
|
|
376
|
+
export function generateFollowupArtifacts(events, input = {}) {
|
|
377
|
+
const mode = input.mode ?? "per_intent";
|
|
378
|
+
const strictness = input.strictness ?? "soft";
|
|
379
|
+
const focus = input.focus ?? "risk";
|
|
380
|
+
const buckets = buildIntentBuckets(events);
|
|
381
|
+
const repeatedCallBaseline = percentile(buckets.map((bucket) => bucket.events.filter((event) => event.kind === "tool_call").length), 0.75);
|
|
382
|
+
const repeatedCheckBaseline = percentile(buckets.map((bucket) => bucket.events.filter((event) => isFileCheckEvent(event)).length), 0.75);
|
|
383
|
+
const repeatedCallThreshold = Math.max(MAX_REPEATED_CALLS_SOFT, Math.round(repeatedCallBaseline));
|
|
384
|
+
const repeatedCheckThreshold = Math.max(MAX_REPEATED_CHECKS_SOFT, Math.round(repeatedCheckBaseline));
|
|
385
|
+
const targetBuckets = mode === "session"
|
|
386
|
+
? [
|
|
387
|
+
{
|
|
388
|
+
intent_id: "session_total",
|
|
389
|
+
intent_title: "Session aggregate",
|
|
390
|
+
start_seq: buckets[0]?.start_seq ?? 0,
|
|
391
|
+
end_seq: buckets[buckets.length - 1]?.end_seq ?? 0,
|
|
392
|
+
events: buckets.flatMap((bucket) => bucket.events),
|
|
393
|
+
},
|
|
394
|
+
]
|
|
395
|
+
: buckets;
|
|
396
|
+
const artifacts = targetBuckets
|
|
397
|
+
.map((bucket) => {
|
|
398
|
+
const { features, template, evidence, thresholdHints } = deriveIntentFeatures(bucket, repeatedCallThreshold, repeatedCheckThreshold);
|
|
399
|
+
const valueClaims = valueClaimsForTemplate(template, features);
|
|
400
|
+
const confidence = computeConfidence(features, evidence.length);
|
|
401
|
+
const insufficient = confidence < MIN_RULE_CONFIDENCE;
|
|
402
|
+
return {
|
|
403
|
+
intent_id: bucket.intent_id,
|
|
404
|
+
intent_title: bucket.intent_title,
|
|
405
|
+
confidence,
|
|
406
|
+
rule_template_id: template,
|
|
407
|
+
features,
|
|
408
|
+
value_claims: valueClaims,
|
|
409
|
+
evidence_refs: evidence,
|
|
410
|
+
rule_spec: buildRuleSpec(template, features, strictness, focus, thresholdHints, evidence),
|
|
411
|
+
skill_draft: buildSkillDraft(features, template, valueClaims, thresholdHints, evidence),
|
|
412
|
+
insufficient_evidence: insufficient,
|
|
413
|
+
};
|
|
414
|
+
})
|
|
415
|
+
.sort((a, b) => b.features.risk_score + b.features.efficiency_waste_score - (a.features.risk_score + a.features.efficiency_waste_score));
|
|
416
|
+
const highRiskIntents = artifacts.filter((artifact) => artifact.features.risk_score >= 60).length;
|
|
417
|
+
const highWasteIntents = artifacts.filter((artifact) => artifact.features.efficiency_waste_score >= 50).length;
|
|
418
|
+
const avgConfidence = artifacts.length > 0
|
|
419
|
+
? Number((artifacts.reduce((sum, artifact) => sum + artifact.confidence, 0) / artifacts.length).toFixed(2))
|
|
420
|
+
: 0;
|
|
421
|
+
return {
|
|
422
|
+
artifacts,
|
|
423
|
+
summary: {
|
|
424
|
+
intent_count: artifacts.length,
|
|
425
|
+
high_risk_intents: highRiskIntents,
|
|
426
|
+
high_efficiency_waste_intents: highWasteIntents,
|
|
427
|
+
avg_confidence: avgConfidence,
|
|
428
|
+
},
|
|
429
|
+
insufficient_evidence: artifacts.length === 0 || artifacts.every((artifact) => artifact.insufficient_evidence),
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
export function deriveIntentTokenBreakdown(events) {
|
|
433
|
+
const buckets = buildIntentBuckets(events);
|
|
434
|
+
const intentBreakdown = [];
|
|
435
|
+
let totalTokens = 0;
|
|
436
|
+
let contextTokens = 0;
|
|
437
|
+
let outputTokens = 0;
|
|
438
|
+
let unknownTokens = 0;
|
|
439
|
+
let cost = 0;
|
|
440
|
+
for (const bucket of buckets) {
|
|
441
|
+
const supporting = [];
|
|
442
|
+
let bucketTotal = 0;
|
|
443
|
+
let bucketContext = 0;
|
|
444
|
+
let bucketOutput = 0;
|
|
445
|
+
let bucketUnknown = 0;
|
|
446
|
+
let bucketCost = 0;
|
|
447
|
+
const tokenEvents = bucket.events.filter((event) => event.kind === "token_usage_checkpoint");
|
|
448
|
+
for (const checkpoint of tokenEvents) {
|
|
449
|
+
const usage = getTokenUsage(checkpoint);
|
|
450
|
+
if (!usage)
|
|
451
|
+
continue;
|
|
452
|
+
const neighbors = bucket.events.filter((event) => event.id !== checkpoint.id &&
|
|
453
|
+
Math.abs(event.seq - checkpoint.seq) <= TOKEN_NEIGHBORHOOD_RADIUS_SEQ);
|
|
454
|
+
const contextNeighbor = neighbors.filter((event) => isContextEvent(event));
|
|
455
|
+
const outputNeighbor = neighbors.filter((event) => isOutputEvent(event));
|
|
456
|
+
let contextPart = 0;
|
|
457
|
+
let outputPart = 0;
|
|
458
|
+
let unknownPart = 0;
|
|
459
|
+
if (contextNeighbor.length === 0 && outputNeighbor.length === 0) {
|
|
460
|
+
unknownPart = usage.total_tokens;
|
|
461
|
+
}
|
|
462
|
+
else if (contextNeighbor.length > 0 && outputNeighbor.length === 0) {
|
|
463
|
+
contextPart = usage.total_tokens;
|
|
464
|
+
}
|
|
465
|
+
else if (outputNeighbor.length > 0 && contextNeighbor.length === 0) {
|
|
466
|
+
outputPart = usage.total_tokens;
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
const totalWeight = contextNeighbor.length + outputNeighbor.length;
|
|
470
|
+
contextPart = Math.round((usage.total_tokens * contextNeighbor.length) / totalWeight);
|
|
471
|
+
outputPart = usage.total_tokens - contextPart;
|
|
472
|
+
}
|
|
473
|
+
supporting.push({
|
|
474
|
+
checkpoint_event_id: checkpoint.id,
|
|
475
|
+
total_tokens: usage.total_tokens,
|
|
476
|
+
context_tokens: contextPart,
|
|
477
|
+
output_tokens: outputPart,
|
|
478
|
+
unknown_tokens: unknownPart,
|
|
479
|
+
neighbor_event_ids: neighbors.slice(0, 8).map((event) => event.id),
|
|
480
|
+
});
|
|
481
|
+
bucketTotal += usage.total_tokens;
|
|
482
|
+
bucketContext += contextPart;
|
|
483
|
+
bucketOutput += outputPart;
|
|
484
|
+
bucketUnknown += unknownPart;
|
|
485
|
+
bucketCost += usage.estimated_cost_usd ?? 0;
|
|
486
|
+
}
|
|
487
|
+
intentBreakdown.push({
|
|
488
|
+
intent_id: bucket.intent_id,
|
|
489
|
+
intent_title: bucket.intent_title,
|
|
490
|
+
total_tokens: bucketTotal,
|
|
491
|
+
context_tokens: bucketContext,
|
|
492
|
+
output_tokens: bucketOutput,
|
|
493
|
+
unknown_tokens: bucketUnknown,
|
|
494
|
+
estimated_cost_usd: bucketCost > 0 ? Number(bucketCost.toFixed(6)) : undefined,
|
|
495
|
+
event_window: {
|
|
496
|
+
start_seq: bucket.start_seq,
|
|
497
|
+
end_seq: bucket.end_seq,
|
|
498
|
+
},
|
|
499
|
+
supporting_events: supporting,
|
|
500
|
+
});
|
|
501
|
+
totalTokens += bucketTotal;
|
|
502
|
+
contextTokens += bucketContext;
|
|
503
|
+
outputTokens += bucketOutput;
|
|
504
|
+
unknownTokens += bucketUnknown;
|
|
505
|
+
cost += bucketCost;
|
|
506
|
+
}
|
|
507
|
+
return {
|
|
508
|
+
intent_breakdown: intentBreakdown.sort((a, b) => b.total_tokens - a.total_tokens),
|
|
509
|
+
totals: {
|
|
510
|
+
total_tokens: totalTokens,
|
|
511
|
+
context_tokens: contextTokens,
|
|
512
|
+
output_tokens: outputTokens,
|
|
513
|
+
unknown_tokens: unknownTokens,
|
|
514
|
+
estimated_cost_usd: cost > 0 ? Number(cost.toFixed(6)) : undefined,
|
|
515
|
+
},
|
|
516
|
+
};
|
|
517
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xiaolei.shawn/mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Local-first MCP server for AI agent session auditing, with embedded dashboard.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -16,19 +16,6 @@
|
|
|
16
16
|
"README.md",
|
|
17
17
|
"LICENSE"
|
|
18
18
|
],
|
|
19
|
-
"scripts": {
|
|
20
|
-
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
21
|
-
"build": "npm run clean && tsc",
|
|
22
|
-
"start": "node dist/index.js start",
|
|
23
|
-
"dev": "tsc && node dist/index.js start",
|
|
24
|
-
"mcp": "node dist/index.js mcp",
|
|
25
|
-
"ingest": "node dist/index.js ingest",
|
|
26
|
-
"ingest:auto": "node dist/index.js ingest --adapter auto",
|
|
27
|
-
"ingest:codex": "node dist/index.js ingest --adapter codex_jsonl",
|
|
28
|
-
"ingest:cursor": "node dist/index.js ingest --adapter cursor_raw",
|
|
29
|
-
"test": "npm run build && node --test dist/__tests__/ingest.test.js",
|
|
30
|
-
"prepublishOnly": "npm run build"
|
|
31
|
-
},
|
|
32
19
|
"dependencies": {
|
|
33
20
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
34
21
|
"zod": "^3.23.0"
|
|
@@ -61,5 +48,17 @@
|
|
|
61
48
|
},
|
|
62
49
|
"engines": {
|
|
63
50
|
"node": ">=18"
|
|
51
|
+
},
|
|
52
|
+
"scripts": {
|
|
53
|
+
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
54
|
+
"build": "npm run clean && tsc",
|
|
55
|
+
"start": "node dist/index.js start",
|
|
56
|
+
"dev": "tsc && node dist/index.js start",
|
|
57
|
+
"mcp": "node dist/index.js mcp",
|
|
58
|
+
"ingest": "node dist/index.js ingest",
|
|
59
|
+
"ingest:auto": "node dist/index.js ingest --adapter auto",
|
|
60
|
+
"ingest:codex": "node dist/index.js ingest --adapter codex_jsonl",
|
|
61
|
+
"ingest:cursor": "node dist/index.js ingest --adapter cursor_raw",
|
|
62
|
+
"test": "npm run build && node --test dist/__tests__/*.test.js"
|
|
64
63
|
}
|
|
65
|
-
}
|
|
64
|
+
}
|