@ouro.bot/cli 0.1.0-alpha.504 → 0.1.0-alpha.506
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/changelog.json
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
|
|
3
3
|
"versions": [
|
|
4
|
+
{
|
|
5
|
+
"version": "0.1.0-alpha.506",
|
|
6
|
+
"changes": [
|
|
7
|
+
"Detect duplicate tool_call_id across assistant messages in `validateSessionMessages`. MiniMax-M2.7 emits canonical tool_call ids of the form `call_function_<hash>_<n>` and reuses the same id across turns when the same function gets called — which causes provider rejections on replay because tool_call_id is supposed to be unique per request. The session sanitize pass already had position-aware orphan detection (#613) and inline-reasoning strip (#612); this adds the third member of the family — collision detection.",
|
|
8
|
+
"New exported `detectDuplicateToolCallIds(messages)` returns `{ id, indices }[]` for each tool_call_id that appears in multiple assistant messages. Same-message duplicates (one assistant calling the same id twice) are not flagged — those are a legitimate parallel-call shape. `validateSessionMessages` now folds collisions into its violations list with a message that calls out MiniMax specifically so operators reading nerves know what they're looking at.",
|
|
9
|
+
"Detection only — no rewriting yet, since rewriting tool_call_ids and the matching tool_results requires careful pairing logic that risks regression. The collision is visible to operators via the `mind.session_invariant_violation` nerves event the sanitize pass already emits when violations are present, and the existing `nerves-review` CLI from #622 makes it filterable. 3 new tests cover collision detection, single-message parallel-call shape (no false positive), and the all-distinct happy path."
|
|
10
|
+
]
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"version": "0.1.0-alpha.505",
|
|
14
|
+
"changes": [
|
|
15
|
+
"New `ouro session-stats <session.json>` CLI for at-a-glance metrics on a saved session: total events, breakdown by role (system/user/assistant/tool), tool-call totals + top 5 by frequency, attachment count, time range with duration, projection breakdown (in/out, input tokens, max tokens, trimmed), and last usage. Read-only.",
|
|
16
|
+
"Pure `computeSessionStats(envelope, path)` core in `src/heart/session-stats.ts` — testable with synthesized envelopes, embeddable in future doctor checks. `runSessionStats(path)` adds the file-load layer; `formatStatsReport(report)` renders human-readable text; `--json` mode for jq piping. Composes with #619 (session-playback) and #622 (nerves-review): three pure-analyzer-plus-thin-CLI tools that together make a stuck session immediately diagnosable end-to-end.",
|
|
17
|
+
"8 tests cover role counts, tool-call name aggregation with frequency-sorted top-5, time range with and without authoredAt timestamps, attachment counting, projection-omission detection, the unrecognized-envelope stub, CLI no-args help, and CLI --json output. Wired as `npm run session:stats -- <path>` and `dist/heart/session-stats-cli-main.js`."
|
|
18
|
+
]
|
|
19
|
+
},
|
|
4
20
|
{
|
|
5
21
|
"version": "0.1.0-alpha.504",
|
|
6
22
|
"changes": [
|
|
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.normalizeContinuityState = normalizeContinuityState;
|
|
37
37
|
exports.validateSessionMessages = validateSessionMessages;
|
|
38
|
+
exports.detectDuplicateToolCallIds = detectDuplicateToolCallIds;
|
|
38
39
|
exports.repairSessionMessages = repairSessionMessages;
|
|
39
40
|
exports.migrateToolNames = migrateToolNames;
|
|
40
41
|
exports.sanitizeProviderMessages = sanitizeProviderMessages;
|
|
@@ -305,8 +306,49 @@ function validateSessionMessages(messages) {
|
|
|
305
306
|
sawToolResultSincePrevAssistant = false;
|
|
306
307
|
prevNonToolRole = msg.role;
|
|
307
308
|
}
|
|
309
|
+
for (const collision of detectDuplicateToolCallIds(messages)) {
|
|
310
|
+
violations.push(`duplicate tool_call_id '${collision.id}' across assistant messages at indices ${collision.indices.join(", ")} — provider may reject (MiniMax canonicalizes call_function_<hash>_<n> across turns)`);
|
|
311
|
+
}
|
|
308
312
|
return violations;
|
|
309
313
|
}
|
|
314
|
+
/**
|
|
315
|
+
* Detect tool_call_ids that appear in more than one assistant message
|
|
316
|
+
* within the conversation. MiniMax-M2.7 in particular emits canonical
|
|
317
|
+
* ids of the form `call_function_<hash>_<n>` and reuses the same id
|
|
318
|
+
* across turns when the same function is called — which causes provider
|
|
319
|
+
* rejections on replay because tool_call_id is supposed to be unique
|
|
320
|
+
* per request. We don't (yet) rewrite these here; this function exists
|
|
321
|
+
* so the sanitize pipeline can surface the collision through nerves
|
|
322
|
+
* (`mind.session_invariant_violation`) and operators can decide.
|
|
323
|
+
*
|
|
324
|
+
* Same-message duplicates (one assistant calling the same id twice)
|
|
325
|
+
* are not collisions — they're a legitimate parallel call shape and
|
|
326
|
+
* would be handled by the assistant's own emit logic. We only flag
|
|
327
|
+
* cross-message reuse.
|
|
328
|
+
*/
|
|
329
|
+
function detectDuplicateToolCallIds(messages) {
|
|
330
|
+
const idsByFirstIndex = new Map();
|
|
331
|
+
for (let i = 0; i < messages.length; i++) {
|
|
332
|
+
const msg = normalizeMessage(messages[i]);
|
|
333
|
+
if (msg.role !== "assistant")
|
|
334
|
+
continue;
|
|
335
|
+
const seenInThisMessage = new Set();
|
|
336
|
+
for (const call of msg.toolCalls) {
|
|
337
|
+
if (!call.id || seenInThisMessage.has(call.id))
|
|
338
|
+
continue;
|
|
339
|
+
seenInThisMessage.add(call.id);
|
|
340
|
+
const indices = idsByFirstIndex.get(call.id) ?? [];
|
|
341
|
+
indices.push(i);
|
|
342
|
+
idsByFirstIndex.set(call.id, indices);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
const collisions = [];
|
|
346
|
+
for (const [id, indices] of idsByFirstIndex) {
|
|
347
|
+
if (indices.length > 1)
|
|
348
|
+
collisions.push({ id, indices });
|
|
349
|
+
}
|
|
350
|
+
return collisions;
|
|
351
|
+
}
|
|
310
352
|
function repairSessionMessages(messages) {
|
|
311
353
|
const normalized = messages.map(normalizeMessage);
|
|
312
354
|
const violations = validateSessionMessages(messages);
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.computeSessionStats = computeSessionStats;
|
|
37
|
+
exports.runSessionStats = runSessionStats;
|
|
38
|
+
exports.formatStatsReport = formatStatsReport;
|
|
39
|
+
exports.runSessionStatsCli = runSessionStatsCli;
|
|
40
|
+
const fs = __importStar(require("node:fs"));
|
|
41
|
+
const session_events_1 = require("./session-events");
|
|
42
|
+
const ROLES = ["system", "user", "assistant", "tool"];
|
|
43
|
+
function emptyByRole() {
|
|
44
|
+
const counts = {};
|
|
45
|
+
for (const role of ROLES)
|
|
46
|
+
counts[role] = 0;
|
|
47
|
+
return counts;
|
|
48
|
+
}
|
|
49
|
+
function eventTimeMs(event) {
|
|
50
|
+
const value = event.time.authoredAt ?? event.time.observedAt ?? null;
|
|
51
|
+
if (!value)
|
|
52
|
+
return null;
|
|
53
|
+
const ms = Date.parse(value);
|
|
54
|
+
return Number.isFinite(ms) ? ms : null;
|
|
55
|
+
}
|
|
56
|
+
function computeSessionStats(envelope, sessionPath) {
|
|
57
|
+
const byRole = emptyByRole();
|
|
58
|
+
let toolCallTotal = 0;
|
|
59
|
+
let attachments = 0;
|
|
60
|
+
const toolNameCounts = new Map();
|
|
61
|
+
let earliestMs = null;
|
|
62
|
+
let latestMs = null;
|
|
63
|
+
for (const event of envelope.events) {
|
|
64
|
+
byRole[event.role] = (byRole[event.role] ?? 0) + 1;
|
|
65
|
+
attachments += event.attachments.length;
|
|
66
|
+
for (const call of event.toolCalls) {
|
|
67
|
+
toolCallTotal += 1;
|
|
68
|
+
const name = call.function.name;
|
|
69
|
+
toolNameCounts.set(name, (toolNameCounts.get(name) ?? 0) + 1);
|
|
70
|
+
}
|
|
71
|
+
const ms = eventTimeMs(event);
|
|
72
|
+
if (ms !== null) {
|
|
73
|
+
if (earliestMs === null || ms < earliestMs)
|
|
74
|
+
earliestMs = ms;
|
|
75
|
+
if (latestMs === null || ms > latestMs)
|
|
76
|
+
latestMs = ms;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const top = [...toolNameCounts.entries()]
|
|
80
|
+
.sort((left, right) => right[1] - left[1])
|
|
81
|
+
.slice(0, 5)
|
|
82
|
+
.map(([name, count]) => ({ name, count }));
|
|
83
|
+
return {
|
|
84
|
+
sessionPath,
|
|
85
|
+
envelopeVersion: envelope.version,
|
|
86
|
+
totalEvents: envelope.events.length,
|
|
87
|
+
byRole,
|
|
88
|
+
toolCalls: {
|
|
89
|
+
total: toolCallTotal,
|
|
90
|
+
distinctNames: toolNameCounts.size,
|
|
91
|
+
topByFrequency: top,
|
|
92
|
+
},
|
|
93
|
+
attachments,
|
|
94
|
+
timeRange: {
|
|
95
|
+
earliest: earliestMs !== null ? new Date(earliestMs).toISOString() : null,
|
|
96
|
+
latest: latestMs !== null ? new Date(latestMs).toISOString() : null,
|
|
97
|
+
durationMs: earliestMs !== null && latestMs !== null ? latestMs - earliestMs : null,
|
|
98
|
+
},
|
|
99
|
+
projection: {
|
|
100
|
+
eventCount: envelope.projection.eventIds.length,
|
|
101
|
+
omittedFromProjection: Math.max(0, envelope.events.length - envelope.projection.eventIds.length),
|
|
102
|
+
inputTokens: envelope.projection.inputTokens,
|
|
103
|
+
maxTokens: envelope.projection.maxTokens,
|
|
104
|
+
trimmed: envelope.projection.trimmed,
|
|
105
|
+
},
|
|
106
|
+
lastUsage: envelope.lastUsage,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function runSessionStats(sessionPath) {
|
|
110
|
+
const text = fs.readFileSync(sessionPath, "utf-8");
|
|
111
|
+
const raw = JSON.parse(text);
|
|
112
|
+
const envelope = (0, session_events_1.parseSessionEnvelope)(raw);
|
|
113
|
+
if (!envelope) {
|
|
114
|
+
return {
|
|
115
|
+
sessionPath,
|
|
116
|
+
envelopeVersion: null,
|
|
117
|
+
totalEvents: 0,
|
|
118
|
+
byRole: emptyByRole(),
|
|
119
|
+
toolCalls: { total: 0, distinctNames: 0, topByFrequency: [] },
|
|
120
|
+
attachments: 0,
|
|
121
|
+
timeRange: { earliest: null, latest: null, durationMs: null },
|
|
122
|
+
projection: { eventCount: 0, omittedFromProjection: 0, inputTokens: null, maxTokens: null, trimmed: false },
|
|
123
|
+
lastUsage: null,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
return computeSessionStats(envelope, sessionPath);
|
|
127
|
+
}
|
|
128
|
+
function formatStatsReport(report) {
|
|
129
|
+
const lines = [];
|
|
130
|
+
lines.push(`Session stats: ${report.sessionPath}`);
|
|
131
|
+
if (report.envelopeVersion === null) {
|
|
132
|
+
lines.push(" envelope: unrecognized (could not parse)");
|
|
133
|
+
return lines.join("\n");
|
|
134
|
+
}
|
|
135
|
+
lines.push(` envelope version: ${report.envelopeVersion}`);
|
|
136
|
+
lines.push(` total events: ${report.totalEvents}`);
|
|
137
|
+
lines.push(` by role: system=${report.byRole.system} user=${report.byRole.user} assistant=${report.byRole.assistant} tool=${report.byRole.tool}`);
|
|
138
|
+
lines.push(` tool calls: ${report.toolCalls.total} (${report.toolCalls.distinctNames} distinct names)`);
|
|
139
|
+
if (report.toolCalls.topByFrequency.length > 0) {
|
|
140
|
+
lines.push(" top tools:");
|
|
141
|
+
for (const { name, count } of report.toolCalls.topByFrequency) {
|
|
142
|
+
lines.push(` ${name}: ${count}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
lines.push(` attachments: ${report.attachments}`);
|
|
146
|
+
if (report.timeRange.earliest && report.timeRange.latest) {
|
|
147
|
+
const durationSec = report.timeRange.durationMs !== null ? Math.round(report.timeRange.durationMs / 1000) : null;
|
|
148
|
+
lines.push(` time range: ${report.timeRange.earliest} → ${report.timeRange.latest}${durationSec !== null ? ` (${durationSec}s)` : ""}`);
|
|
149
|
+
}
|
|
150
|
+
lines.push(" projection:");
|
|
151
|
+
lines.push(` in projection: ${report.projection.eventCount}`);
|
|
152
|
+
lines.push(` omitted: ${report.projection.omittedFromProjection}`);
|
|
153
|
+
if (report.projection.inputTokens !== null)
|
|
154
|
+
lines.push(` input tokens: ${report.projection.inputTokens}`);
|
|
155
|
+
if (report.projection.maxTokens !== null)
|
|
156
|
+
lines.push(` max tokens: ${report.projection.maxTokens}`);
|
|
157
|
+
if (report.projection.trimmed)
|
|
158
|
+
lines.push(" trimmed: true");
|
|
159
|
+
if (report.lastUsage) {
|
|
160
|
+
lines.push(` last usage: ${JSON.stringify(report.lastUsage)}`);
|
|
161
|
+
}
|
|
162
|
+
return lines.join("\n");
|
|
163
|
+
}
|
|
164
|
+
function runSessionStatsCli(argv) {
|
|
165
|
+
const positional = argv.filter((token) => !token.startsWith("--"));
|
|
166
|
+
const flags = new Set(argv.filter((token) => token.startsWith("--")));
|
|
167
|
+
if (flags.has("--help") || flags.has("-h") || positional.length === 0) {
|
|
168
|
+
// eslint-disable-next-line no-console -- meta-tooling
|
|
169
|
+
console.log("usage: ouro session-stats <session.json> [--json]");
|
|
170
|
+
return positional.length === 0 ? 2 : 0;
|
|
171
|
+
}
|
|
172
|
+
const report = runSessionStats(positional[0]);
|
|
173
|
+
if (flags.has("--json")) {
|
|
174
|
+
// eslint-disable-next-line no-console -- meta-tooling
|
|
175
|
+
console.log(JSON.stringify(report, null, 2));
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
// eslint-disable-next-line no-console -- meta-tooling
|
|
179
|
+
console.log(formatStatsReport(report));
|
|
180
|
+
}
|
|
181
|
+
return 0;
|
|
182
|
+
}
|
package/dist/mind/context.js
CHANGED
|
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.validateSessionMessages = exports.repairSessionMessages = exports.migrateToolNames = void 0;
|
|
36
|
+
exports.validateSessionMessages = exports.repairSessionMessages = exports.migrateToolNames = exports.detectDuplicateToolCallIds = void 0;
|
|
37
37
|
exports.trimMessages = trimMessages;
|
|
38
38
|
exports.saveSession = saveSession;
|
|
39
39
|
exports.appendSyntheticAssistantMessage = appendSyntheticAssistantMessage;
|
|
@@ -50,6 +50,7 @@ const fs = __importStar(require("fs"));
|
|
|
50
50
|
const path = __importStar(require("path"));
|
|
51
51
|
const token_estimate_1 = require("./token-estimate");
|
|
52
52
|
var session_events_2 = require("../heart/session-events");
|
|
53
|
+
Object.defineProperty(exports, "detectDuplicateToolCallIds", { enumerable: true, get: function () { return session_events_2.detectDuplicateToolCallIds; } });
|
|
53
54
|
Object.defineProperty(exports, "migrateToolNames", { enumerable: true, get: function () { return session_events_2.migrateToolNames; } });
|
|
54
55
|
Object.defineProperty(exports, "repairSessionMessages", { enumerable: true, get: function () { return session_events_2.repairSessionMessages; } });
|
|
55
56
|
Object.defineProperty(exports, "validateSessionMessages", { enumerable: true, get: function () { return session_events_2.validateSessionMessages; } });
|
|
@@ -129,6 +129,10 @@ const DISPATCH_EXEMPT_PATTERNS = [
|
|
|
129
129
|
// lives at the caller (tools-mail.ts mail_body handler) which fires
|
|
130
130
|
// repertoire.mail_body_cache_hit on cache reuse.
|
|
131
131
|
"mailroom/body-cache",
|
|
132
|
+
// Session stats: read-only session.json analyzer CLI for debugging.
|
|
133
|
+
// Diagnostics-only utility; output is human-readable summary.
|
|
134
|
+
"heart/session-stats-cli-main",
|
|
135
|
+
"heart/session-stats",
|
|
132
136
|
];
|
|
133
137
|
function isDispatchExempt(filePath) {
|
|
134
138
|
return DISPATCH_EXEMPT_PATTERNS.some((pattern) => filePath.includes(pattern));
|