create-principles-disciple 1.147.11 → 1.147.12
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/_release/product-identity.json +2 -2
- package/core/dist/runtime-v2/adapter/output-schema-registry.d.ts +3 -1
- package/core/dist/runtime-v2/adapter/output-schema-registry.d.ts.map +1 -1
- package/core/dist/runtime-v2/adapter/output-schema-registry.js +46 -2
- package/core/dist/runtime-v2/adapter/output-schema-registry.js.map +1 -1
- package/core/dist/runtime-v2/pain-signal-observability.d.ts.map +1 -1
- package/core/dist/runtime-v2/pain-signal-observability.js +5 -3
- package/core/dist/runtime-v2/pain-signal-observability.js.map +1 -1
- package/core/dist/runtime-v2/trajectory-schema.d.ts +34 -4
- package/core/dist/runtime-v2/trajectory-schema.d.ts.map +1 -1
- package/core/dist/runtime-v2/trajectory-schema.js +30 -2
- package/core/dist/runtime-v2/trajectory-schema.js.map +1 -1
- package/core/package.json +1 -1
- package/host-runtime/dist/adaptive-behavior-linkage.d.ts +223 -0
- package/host-runtime/dist/adaptive-behavior-linkage.js +1014 -0
- package/host-runtime/dist/adaptive-journal-reconciliation.d.ts +10 -0
- package/host-runtime/dist/adaptive-journal-reconciliation.js +16 -0
- package/host-runtime/dist/index.d.ts +1 -0
- package/host-runtime/dist/index.js +2 -0
- package/host-runtime/dist/production-pain-evidence.js +6 -6
- package/host-runtime/dist/prompt-injection-budget-observation.js +31 -2
- package/host-runtime/package.json +1 -1
- package/package.json +3 -3
- package/pd-cli/dist/commands/runtime-activation.d.ts +19 -0
- package/pd-cli/dist/commands/runtime-activation.d.ts.map +1 -1
- package/pd-cli/dist/commands/runtime-activation.js +75 -2
- package/pd-cli/dist/commands/runtime-activation.js.map +1 -1
- package/pd-cli/dist/index.js +3 -1
- package/pd-cli/dist/index.js.map +1 -1
- package/pd-cli/package.json +1 -1
- package/plugin/dist/bundle.js +399 -399
- package/plugin/dist/openclaw.plugin.json +1 -1
- package/plugin/dist/rulehost-evidence.js +13 -13
- package/plugin/openclaw.plugin.json +1 -1
- package/plugin/package.json +1 -1
- package/release-manager/package.json +1 -1
|
@@ -0,0 +1,1014 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PRI-1114 — read-only behavior evidence linkage.
|
|
3
|
+
*
|
|
4
|
+
* Design: docs/specs/PD-AA-PRI-1114-BEHAVIOR-EVIDENCE-LINK-DESIGN-v0.2.md
|
|
5
|
+
*
|
|
6
|
+
* This module answers a deliberately narrow question: which recorded actions,
|
|
7
|
+
* corrections and task outcomes belong to the same *run* as a recorded
|
|
8
|
+
* principle intervention. It never claims a request-level link, never claims
|
|
9
|
+
* the intervention was delivered before an action, and never upgrades a
|
|
10
|
+
* model-completion record into task success.
|
|
11
|
+
*
|
|
12
|
+
* No writes, no model calls, no migrations. Every read is bounded.
|
|
13
|
+
*/
|
|
14
|
+
import * as crypto from 'node:crypto';
|
|
15
|
+
import * as fs from 'node:fs';
|
|
16
|
+
import * as path from 'node:path';
|
|
17
|
+
import { openTrajectoryDbReadonly, } from '@principles/core/trajectory-store';
|
|
18
|
+
import { ADAPTIVE_JOURNAL_CANDIDATE_DIRS, ADAPTIVE_JOURNAL_MAX_DATE_FILES_PER_DIR, ADAPTIVE_JOURNAL_MAX_FILE_BYTES, ADAPTIVE_JOURNAL_MAX_INVOCATIONS, ADAPTIVE_JOURNAL_MAX_TERMINAL_VARIANTS, reconcileAdaptiveJournal, } from './adaptive-journal-reconciliation.js';
|
|
19
|
+
export const ADAPTIVE_BEHAVIOR_LINKAGE_SCHEMA_VERSION = 'pd-aa-behavior-linkage/v1';
|
|
20
|
+
export const BEHAVIOR_LINKAGE_MAX_EVENT_LOG_LINES = 20_000;
|
|
21
|
+
export const BEHAVIOR_LINKAGE_MAX_TOOL_CALLS = 5_000;
|
|
22
|
+
export const BEHAVIOR_LINKAGE_MAX_TRAJECTORY_ROWS = 5_000;
|
|
23
|
+
export const BEHAVIOR_LINKAGE_MAX_ACTIONS_PER_INVOCATION = 200;
|
|
24
|
+
export const BEHAVIOR_LINKAGE_MAX_CORRECTIONS_PER_INVOCATION = 50;
|
|
25
|
+
export const BEHAVIOR_LINKAGE_MAX_OUTCOMES_PER_INVOCATION = 20;
|
|
26
|
+
export const BEHAVIOR_LINKAGE_MAX_WEAK_CANDIDATES_PER_INVOCATION = 20;
|
|
27
|
+
export const BEHAVIOR_LINKAGE_MAX_SESSION_QUERIES = 100;
|
|
28
|
+
export const BEHAVIOR_LINKAGE_ROWS_PER_SESSION = 200;
|
|
29
|
+
const DAY_MS = 86_400_000;
|
|
30
|
+
function isRecord(value) {
|
|
31
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
32
|
+
}
|
|
33
|
+
function optionalString(value) {
|
|
34
|
+
return typeof value === 'string' && value.length > 0 ? value : null;
|
|
35
|
+
}
|
|
36
|
+
function optionalNumber(value) {
|
|
37
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
38
|
+
}
|
|
39
|
+
function positiveInt(value, fallback) {
|
|
40
|
+
if (value === undefined)
|
|
41
|
+
return fallback;
|
|
42
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value <= 0)
|
|
43
|
+
return null;
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
function resolveLimits(input) {
|
|
47
|
+
const raw = input ?? {};
|
|
48
|
+
const values = {
|
|
49
|
+
maxDateFilesPerDir: positiveInt(raw.maxDateFilesPerDir, ADAPTIVE_JOURNAL_MAX_DATE_FILES_PER_DIR) ?? -1,
|
|
50
|
+
maxFileBytes: positiveInt(raw.maxFileBytes, ADAPTIVE_JOURNAL_MAX_FILE_BYTES) ?? -1,
|
|
51
|
+
maxInvocations: positiveInt(raw.maxInvocations, ADAPTIVE_JOURNAL_MAX_INVOCATIONS) ?? -1,
|
|
52
|
+
maxTerminalVariantsPerInvocation: positiveInt(raw.maxTerminalVariantsPerInvocation, ADAPTIVE_JOURNAL_MAX_TERMINAL_VARIANTS) ?? -1,
|
|
53
|
+
maxEventLogLines: positiveInt(raw.maxEventLogLines, BEHAVIOR_LINKAGE_MAX_EVENT_LOG_LINES) ?? -1,
|
|
54
|
+
maxToolCalls: positiveInt(raw.maxToolCalls, BEHAVIOR_LINKAGE_MAX_TOOL_CALLS) ?? -1,
|
|
55
|
+
maxTrajectoryRows: positiveInt(raw.maxTrajectoryRows, BEHAVIOR_LINKAGE_MAX_TRAJECTORY_ROWS) ?? -1,
|
|
56
|
+
maxActionsPerInvocation: positiveInt(raw.maxActionsPerInvocation, BEHAVIOR_LINKAGE_MAX_ACTIONS_PER_INVOCATION) ?? -1,
|
|
57
|
+
maxCorrectionsPerInvocation: positiveInt(raw.maxCorrectionsPerInvocation, BEHAVIOR_LINKAGE_MAX_CORRECTIONS_PER_INVOCATION) ?? -1,
|
|
58
|
+
maxOutcomesPerInvocation: positiveInt(raw.maxOutcomesPerInvocation, BEHAVIOR_LINKAGE_MAX_OUTCOMES_PER_INVOCATION) ?? -1,
|
|
59
|
+
maxWeakCandidatesPerInvocation: positiveInt(raw.maxWeakCandidatesPerInvocation, BEHAVIOR_LINKAGE_MAX_WEAK_CANDIDATES_PER_INVOCATION) ?? -1,
|
|
60
|
+
maxSessionQueries: positiveInt(raw.maxSessionQueries, BEHAVIOR_LINKAGE_MAX_SESSION_QUERIES) ?? -1,
|
|
61
|
+
};
|
|
62
|
+
return Object.values(values).some((value) => value < 0) ? null : values;
|
|
63
|
+
}
|
|
64
|
+
function utcDateKey(ms) {
|
|
65
|
+
return new Date(ms).toISOString().slice(0, 10);
|
|
66
|
+
}
|
|
67
|
+
function windowDateKeys(startIso, endIso) {
|
|
68
|
+
const startMs = Date.parse(startIso);
|
|
69
|
+
const endMs = Date.parse(endIso);
|
|
70
|
+
const firstDayMs = Date.UTC(new Date(startMs).getUTCFullYear(), new Date(startMs).getUTCMonth(), new Date(startMs).getUTCDate());
|
|
71
|
+
const dates = [];
|
|
72
|
+
for (let cursor = firstDayMs; cursor <= endMs; cursor += DAY_MS) {
|
|
73
|
+
dates.push(utcDateKey(cursor));
|
|
74
|
+
}
|
|
75
|
+
// EventLog names files by flush time, so a record created just before
|
|
76
|
+
// midnight can live in the next UTC day's file. Envelope ts still filters.
|
|
77
|
+
dates.push(utcDateKey(firstDayMs + dates.length * DAY_MS));
|
|
78
|
+
return dates;
|
|
79
|
+
}
|
|
80
|
+
function normalizePathForComparison(value) {
|
|
81
|
+
return value.replace(/\\/g, '/').replace(/\/+$/, '').toLowerCase();
|
|
82
|
+
}
|
|
83
|
+
function realpathOrNull(value) {
|
|
84
|
+
try {
|
|
85
|
+
return fs.realpathSync(value);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function isInside(root, candidate) {
|
|
92
|
+
const normalizedRoot = normalizePathForComparison(root);
|
|
93
|
+
const normalizedCandidate = normalizePathForComparison(candidate);
|
|
94
|
+
return normalizedCandidate === normalizedRoot
|
|
95
|
+
|| normalizedCandidate.startsWith(`${normalizedRoot}/`);
|
|
96
|
+
}
|
|
97
|
+
function selectEventLogFiles(input) {
|
|
98
|
+
const { workspaceDir, dates, maxDateFilesPerDir, coverage } = input;
|
|
99
|
+
const dateSet = new Set(dates);
|
|
100
|
+
const selected = [];
|
|
101
|
+
const canonicalWorkspace = realpathOrNull(workspaceDir);
|
|
102
|
+
if (canonicalWorkspace === null)
|
|
103
|
+
return selected;
|
|
104
|
+
ADAPTIVE_JOURNAL_CANDIDATE_DIRS.forEach((logDir, dirPriority) => {
|
|
105
|
+
const absoluteDir = path.resolve(workspaceDir, logDir);
|
|
106
|
+
const canonicalDir = realpathOrNull(absoluteDir);
|
|
107
|
+
if (canonicalDir === null)
|
|
108
|
+
return;
|
|
109
|
+
if (!isInside(canonicalWorkspace, canonicalDir)) {
|
|
110
|
+
coverage.errors += 1;
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
coverage.dirsFound += 1;
|
|
114
|
+
let names;
|
|
115
|
+
try {
|
|
116
|
+
names = fs.readdirSync(canonicalDir);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
coverage.errors += 1;
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const dateFiles = names
|
|
123
|
+
.map((name) => {
|
|
124
|
+
const match = /^events_(\d{4}-\d{2}-\d{2})\.jsonl$/.exec(name);
|
|
125
|
+
return match?.[1] ? { name, dateKey: match[1] } : null;
|
|
126
|
+
})
|
|
127
|
+
.filter((entry) => entry !== null && dateSet.has(entry.dateKey))
|
|
128
|
+
.sort((left, right) => right.dateKey.localeCompare(left.dateKey));
|
|
129
|
+
const omitted = Math.max(0, dateFiles.length - maxDateFilesPerDir);
|
|
130
|
+
if (omitted > 0) {
|
|
131
|
+
coverage.omittedFiles += omitted;
|
|
132
|
+
// A whole date file was not read: this is bounded coverage, never "absent".
|
|
133
|
+
coverage.truncated = true;
|
|
134
|
+
}
|
|
135
|
+
for (const entry of dateFiles.slice(0, maxDateFilesPerDir)) {
|
|
136
|
+
selected.push({
|
|
137
|
+
absolutePath: path.join(canonicalDir, entry.name),
|
|
138
|
+
dateKey: entry.dateKey,
|
|
139
|
+
logDir,
|
|
140
|
+
dirPriority,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
return selected.sort((left, right) => {
|
|
145
|
+
const byDate = right.dateKey.localeCompare(left.dateKey);
|
|
146
|
+
return byDate !== 0 ? byDate : left.dirPriority - right.dirPriority;
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
function readTailLines(absolutePath, maxBytes) {
|
|
150
|
+
const fd = fs.openSync(absolutePath, 'r');
|
|
151
|
+
try {
|
|
152
|
+
const stat = fs.fstatSync(fd);
|
|
153
|
+
const start = Math.max(0, stat.size - maxBytes);
|
|
154
|
+
const length = stat.size - start;
|
|
155
|
+
if (length === 0)
|
|
156
|
+
return { lines: [], bytesRead: 0, truncated: false };
|
|
157
|
+
const buffer = Buffer.alloc(length);
|
|
158
|
+
fs.readSync(fd, buffer, 0, length, start);
|
|
159
|
+
let text = buffer.toString('utf8');
|
|
160
|
+
if (start > 0) {
|
|
161
|
+
const firstNewline = text.indexOf('\n');
|
|
162
|
+
text = firstNewline >= 0 ? text.slice(firstNewline + 1) : '';
|
|
163
|
+
}
|
|
164
|
+
const rawLines = text.split(/\r?\n/).filter((line) => line.trim().length > 0);
|
|
165
|
+
const lines = rawLines.map((line, index) => ({
|
|
166
|
+
text: line,
|
|
167
|
+
ordinal: start > 0 ? -1 : index + 1,
|
|
168
|
+
}));
|
|
169
|
+
return { lines, bytesRead: length, truncated: start > 0 };
|
|
170
|
+
}
|
|
171
|
+
finally {
|
|
172
|
+
fs.closeSync(fd);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function hashRef(value) {
|
|
176
|
+
return crypto.createHash('sha256').update(value).digest('hex').slice(0, 12);
|
|
177
|
+
}
|
|
178
|
+
function readToolCallRows(input) {
|
|
179
|
+
const { workspaceDir, window, limits, coverage } = input;
|
|
180
|
+
const startMs = Date.parse(window.start);
|
|
181
|
+
const endMs = Date.parse(window.end);
|
|
182
|
+
const files = selectEventLogFiles({
|
|
183
|
+
workspaceDir,
|
|
184
|
+
dates: windowDateKeys(window.start, window.end),
|
|
185
|
+
maxDateFilesPerDir: limits.maxDateFilesPerDir,
|
|
186
|
+
coverage,
|
|
187
|
+
});
|
|
188
|
+
const rows = [];
|
|
189
|
+
let lineBudget = limits.maxEventLogLines;
|
|
190
|
+
let stopped = false;
|
|
191
|
+
for (const file of files) {
|
|
192
|
+
if (stopped)
|
|
193
|
+
break;
|
|
194
|
+
let bounded;
|
|
195
|
+
try {
|
|
196
|
+
bounded = readTailLines(file.absolutePath, limits.maxFileBytes);
|
|
197
|
+
coverage.filesRead += 1;
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
coverage.errors += 1;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (bounded.truncated)
|
|
204
|
+
coverage.truncated = true;
|
|
205
|
+
for (const line of bounded.lines) {
|
|
206
|
+
if (lineBudget <= 0) {
|
|
207
|
+
coverage.truncated = true;
|
|
208
|
+
stopped = true;
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
lineBudget -= 1;
|
|
212
|
+
coverage.linesRead += 1;
|
|
213
|
+
let parsed;
|
|
214
|
+
try {
|
|
215
|
+
parsed = JSON.parse(line.text);
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
coverage.parseErrors += 1;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (!isRecord(parsed) || parsed.type !== 'tool_call')
|
|
222
|
+
continue;
|
|
223
|
+
const ts = optionalString(parsed.ts);
|
|
224
|
+
if (ts === null)
|
|
225
|
+
continue;
|
|
226
|
+
const tsMs = Date.parse(ts);
|
|
227
|
+
if (!Number.isFinite(tsMs) || tsMs < startMs || tsMs > endMs)
|
|
228
|
+
continue;
|
|
229
|
+
if (rows.length >= limits.maxToolCalls) {
|
|
230
|
+
coverage.truncated = true;
|
|
231
|
+
stopped = true;
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
const data = isRecord(parsed.data) ? parsed.data : {};
|
|
235
|
+
const sessionId = optionalString(parsed.sessionId) ?? optionalString(data.sessionId);
|
|
236
|
+
const runId = optionalString(data.runId);
|
|
237
|
+
const toolName = optionalString(data.toolName) ?? 'unknown';
|
|
238
|
+
const outcome = parsed.category === 'success' || parsed.category === 'failure'
|
|
239
|
+
? parsed.category
|
|
240
|
+
: (optionalString(data.error) !== null || (optionalNumber(data.exitCode) ?? 0) !== 0 ? 'failure' : 'unknown');
|
|
241
|
+
const toolCallId = optionalString(data.toolCallId);
|
|
242
|
+
const ordinal = line.ordinal >= 0 ? `${line.ordinal}` : `tail:${rows.length + 1}`;
|
|
243
|
+
rows.push({
|
|
244
|
+
ts,
|
|
245
|
+
tsMs,
|
|
246
|
+
sessionId,
|
|
247
|
+
runId,
|
|
248
|
+
toolName,
|
|
249
|
+
toolCallIdRef: toolCallId === null ? null : hashRef(toolCallId),
|
|
250
|
+
outcome,
|
|
251
|
+
exitCode: optionalNumber(data.exitCode),
|
|
252
|
+
errorType: optionalString(data.errorType),
|
|
253
|
+
errorPresent: optionalString(data.error) !== null,
|
|
254
|
+
declaresFilePath: optionalString(data.filePath) !== null,
|
|
255
|
+
logRef: `${path.basename(file.absolutePath)}:${ordinal}`,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
coverage.toolCalls = rows.length;
|
|
260
|
+
coverage.unattributedToolCalls = rows.filter((row) => row.sessionId === null || row.runId === null).length;
|
|
261
|
+
return rows;
|
|
262
|
+
}
|
|
263
|
+
class SqlRowBudget {
|
|
264
|
+
remaining;
|
|
265
|
+
constructor(max) {
|
|
266
|
+
this.remaining = max;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
function isRecordRow(value) {
|
|
270
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
271
|
+
}
|
|
272
|
+
function requireRecordRows(value) {
|
|
273
|
+
const rows = [];
|
|
274
|
+
for (const row of value) {
|
|
275
|
+
if (!isRecordRow(row))
|
|
276
|
+
throw new Error('trajectory_row_malformed');
|
|
277
|
+
rows.push(row);
|
|
278
|
+
}
|
|
279
|
+
return rows;
|
|
280
|
+
}
|
|
281
|
+
function boundedQuery(input) {
|
|
282
|
+
const { db, sql, params, budget, requested } = input;
|
|
283
|
+
if (budget.remaining <= 0)
|
|
284
|
+
return { rows: [], truncated: true };
|
|
285
|
+
const limit = Math.min(requested, budget.remaining);
|
|
286
|
+
const rows = requireRecordRows(db.prepare(sql).all(...params, limit + 1));
|
|
287
|
+
const kept = rows.slice(0, limit);
|
|
288
|
+
budget.remaining = Math.max(0, budget.remaining - kept.length);
|
|
289
|
+
return { rows: kept, truncated: rows.length > limit };
|
|
290
|
+
}
|
|
291
|
+
function tableColumns(db, table) {
|
|
292
|
+
const columns = new Set();
|
|
293
|
+
try {
|
|
294
|
+
const rows = db.prepare(`PRAGMA table_info(${table})`).all();
|
|
295
|
+
for (const row of rows) {
|
|
296
|
+
if (isRecordRow(row) && typeof row.name === 'string')
|
|
297
|
+
columns.add(row.name);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
return new Set();
|
|
302
|
+
}
|
|
303
|
+
return columns;
|
|
304
|
+
}
|
|
305
|
+
function hasRequiredSchema(db) {
|
|
306
|
+
const required = {
|
|
307
|
+
assistant_turns: ['id', 'session_id', 'run_id', 'created_at'],
|
|
308
|
+
user_turns: ['id', 'session_id', 'turn_index', 'correction_detected', 'references_assistant_turn_id', 'created_at'],
|
|
309
|
+
tool_calls: ['id', 'session_id', 'tool_name', 'outcome', 'created_at'],
|
|
310
|
+
task_outcomes: ['id', 'session_id', 'task_id', 'outcome', 'principle_ids_json', 'created_at'],
|
|
311
|
+
};
|
|
312
|
+
for (const [table, columns] of Object.entries(required)) {
|
|
313
|
+
const actual = tableColumns(db, table);
|
|
314
|
+
if (actual.size === 0)
|
|
315
|
+
return false;
|
|
316
|
+
for (const column of columns) {
|
|
317
|
+
if (!actual.has(column))
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
function readTrajectory(input) {
|
|
324
|
+
const { workspaceDir, invocations, window, limits } = input;
|
|
325
|
+
const sessions = new Map();
|
|
326
|
+
const distinctSessions = [...new Set(invocations.flatMap((invocation) => (invocation.identity.sessionId === null ? [] : [invocation.identity.sessionId])))].sort();
|
|
327
|
+
const result = {
|
|
328
|
+
status: 'unavailable',
|
|
329
|
+
rowsRead: 0,
|
|
330
|
+
truncated: false,
|
|
331
|
+
reasons: [],
|
|
332
|
+
sessions,
|
|
333
|
+
};
|
|
334
|
+
let db;
|
|
335
|
+
try {
|
|
336
|
+
db = openTrajectoryDbReadonly(workspaceDir);
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
result.reasons.push('trajectory_db_unavailable');
|
|
340
|
+
return result;
|
|
341
|
+
}
|
|
342
|
+
try {
|
|
343
|
+
try {
|
|
344
|
+
db.pragma('query_only = ON');
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
result.reasons.push('query_only_unavailable');
|
|
348
|
+
}
|
|
349
|
+
if (!hasRequiredSchema(db)) {
|
|
350
|
+
result.reasons.push('trajectory_schema_incomplete');
|
|
351
|
+
return result;
|
|
352
|
+
}
|
|
353
|
+
result.status = 'read';
|
|
354
|
+
const budget = new SqlRowBudget(limits.maxTrajectoryRows);
|
|
355
|
+
const sessionsToQuery = distinctSessions.slice(0, limits.maxSessionQueries);
|
|
356
|
+
if (sessionsToQuery.length < distinctSessions.length) {
|
|
357
|
+
result.truncated = true;
|
|
358
|
+
result.reasons.push('session_query_limit_reached');
|
|
359
|
+
}
|
|
360
|
+
for (const sessionId of sessionsToQuery) {
|
|
361
|
+
const row = {
|
|
362
|
+
corrections: [],
|
|
363
|
+
outcomes: [],
|
|
364
|
+
weakToolCalls: [],
|
|
365
|
+
correctionsTruncated: false,
|
|
366
|
+
outcomesTruncated: false,
|
|
367
|
+
weakTruncated: false,
|
|
368
|
+
queryFailed: false,
|
|
369
|
+
};
|
|
370
|
+
try {
|
|
371
|
+
const corrections = boundedQuery({
|
|
372
|
+
db,
|
|
373
|
+
sql: `SELECT u.id AS user_turn_id, u.turn_index, u.correction_detected,
|
|
374
|
+
u.references_assistant_turn_id, u.created_at, a.run_id AS referenced_run_id
|
|
375
|
+
FROM user_turns u
|
|
376
|
+
LEFT JOIN assistant_turns a ON a.id = u.references_assistant_turn_id
|
|
377
|
+
WHERE u.session_id = ? AND u.correction_detected = 1
|
|
378
|
+
AND u.created_at >= ? AND u.created_at <= ?
|
|
379
|
+
ORDER BY u.created_at ASC, u.id ASC
|
|
380
|
+
LIMIT ?`,
|
|
381
|
+
params: [sessionId, window.start, window.end],
|
|
382
|
+
budget,
|
|
383
|
+
requested: BEHAVIOR_LINKAGE_ROWS_PER_SESSION,
|
|
384
|
+
});
|
|
385
|
+
row.corrections = corrections.rows;
|
|
386
|
+
row.correctionsTruncated = corrections.truncated;
|
|
387
|
+
const outcomes = boundedQuery({
|
|
388
|
+
db,
|
|
389
|
+
sql: `SELECT id, task_id, outcome, principle_ids_json, created_at
|
|
390
|
+
FROM task_outcomes
|
|
391
|
+
WHERE session_id = ? AND created_at >= ? AND created_at <= ?
|
|
392
|
+
ORDER BY created_at ASC, id ASC
|
|
393
|
+
LIMIT ?`,
|
|
394
|
+
params: [sessionId, window.start, window.end],
|
|
395
|
+
budget,
|
|
396
|
+
requested: BEHAVIOR_LINKAGE_ROWS_PER_SESSION,
|
|
397
|
+
});
|
|
398
|
+
row.outcomes = outcomes.rows;
|
|
399
|
+
row.outcomesTruncated = outcomes.truncated;
|
|
400
|
+
const weakToolCalls = boundedQuery({
|
|
401
|
+
db,
|
|
402
|
+
sql: `SELECT id, tool_name, outcome, created_at
|
|
403
|
+
FROM tool_calls
|
|
404
|
+
WHERE session_id = ? AND created_at >= ? AND created_at <= ?
|
|
405
|
+
ORDER BY created_at ASC, id ASC
|
|
406
|
+
LIMIT ?`,
|
|
407
|
+
params: [sessionId, window.start, window.end],
|
|
408
|
+
budget,
|
|
409
|
+
requested: BEHAVIOR_LINKAGE_ROWS_PER_SESSION,
|
|
410
|
+
});
|
|
411
|
+
row.weakToolCalls = weakToolCalls.rows;
|
|
412
|
+
row.weakTruncated = weakToolCalls.truncated;
|
|
413
|
+
// A truncated per-session read means rows may exist past the cap; the
|
|
414
|
+
// targeted run's row can be among them, so the whole read is incomplete.
|
|
415
|
+
if (corrections.truncated || outcomes.truncated || weakToolCalls.truncated) {
|
|
416
|
+
result.truncated = true;
|
|
417
|
+
if (!result.reasons.includes('session_rows_truncated')) {
|
|
418
|
+
result.reasons.push('session_rows_truncated');
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
catch {
|
|
423
|
+
row.queryFailed = true;
|
|
424
|
+
result.truncated = true;
|
|
425
|
+
result.reasons.push('trajectory_query_failed');
|
|
426
|
+
}
|
|
427
|
+
sessions.set(sessionId, row);
|
|
428
|
+
}
|
|
429
|
+
result.rowsRead = limits.maxTrajectoryRows - budget.remaining;
|
|
430
|
+
if (budget.remaining <= 0) {
|
|
431
|
+
result.truncated = true;
|
|
432
|
+
result.reasons.push('trajectory_row_limit_reached');
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
finally {
|
|
436
|
+
db.close();
|
|
437
|
+
}
|
|
438
|
+
return result;
|
|
439
|
+
}
|
|
440
|
+
function configChanged(invocation) {
|
|
441
|
+
const open = invocation.open?.configIdentity ?? null;
|
|
442
|
+
const terminal = invocation.terminal?.configIdentity ?? null;
|
|
443
|
+
if (open === null || terminal === null)
|
|
444
|
+
return false;
|
|
445
|
+
return open.effectiveHookSetFingerprint !== terminal.effectiveHookSetFingerprint
|
|
446
|
+
|| open.hostVersion !== terminal.hostVersion
|
|
447
|
+
|| open.pluginVersion !== terminal.pluginVersion;
|
|
448
|
+
}
|
|
449
|
+
function journalRangeComplete(report) {
|
|
450
|
+
return report.coverage.status === 'complete'
|
|
451
|
+
&& (report.truncation?.omitted ?? 0) === 0
|
|
452
|
+
&& !report.outputTruncated
|
|
453
|
+
// Design §4.2: dropped parse/envelope/identity failures make uniqueness unproven.
|
|
454
|
+
&& (report.totals.malformedJsonLines ?? 0) === 0
|
|
455
|
+
&& (report.totals.invalidEnvelopeRecords ?? 0) === 0
|
|
456
|
+
&& (report.totals.invalidRecords ?? 0) === 0;
|
|
457
|
+
}
|
|
458
|
+
function classifyAssociation(invocation, sameRunCount, rangeComplete) {
|
|
459
|
+
const reasons = [];
|
|
460
|
+
if (invocation.classification === 'conflicted') {
|
|
461
|
+
reasons.push('terminal_conflicted');
|
|
462
|
+
return { association: 'conflict', reasons };
|
|
463
|
+
}
|
|
464
|
+
const identityValid = invocation.identity.runId !== null
|
|
465
|
+
&& invocation.identity.sessionId !== null
|
|
466
|
+
&& invocation.identity.workspaceVerified;
|
|
467
|
+
if (invocation.open === null) {
|
|
468
|
+
reasons.push('open_not_visible_in_range');
|
|
469
|
+
return { association: identityValid ? 'weak' : 'unknown', reasons };
|
|
470
|
+
}
|
|
471
|
+
if (invocation.terminal === null) {
|
|
472
|
+
reasons.push('terminal_not_visible_in_range');
|
|
473
|
+
return { association: identityValid ? 'weak' : 'unknown', reasons };
|
|
474
|
+
}
|
|
475
|
+
if (!rangeComplete) {
|
|
476
|
+
reasons.push('journal_range_incomplete');
|
|
477
|
+
return { association: 'unknown', reasons };
|
|
478
|
+
}
|
|
479
|
+
if (!identityValid) {
|
|
480
|
+
reasons.push(invocation.identity.runId === null ? 'run_identity_missing' : 'workspace_identity_unknown');
|
|
481
|
+
return { association: 'unknown', reasons };
|
|
482
|
+
}
|
|
483
|
+
if (invocation.duplicates.open > 0 || invocation.duplicates.terminal > 0) {
|
|
484
|
+
reasons.push('duplicate_records');
|
|
485
|
+
return { association: 'unknown', reasons };
|
|
486
|
+
}
|
|
487
|
+
if (invocation.targetConsistent !== true) {
|
|
488
|
+
reasons.push('target_identity_unproven');
|
|
489
|
+
return { association: 'unknown', reasons };
|
|
490
|
+
}
|
|
491
|
+
if (configChanged(invocation)) {
|
|
492
|
+
reasons.push('config_changed');
|
|
493
|
+
return { association: 'unknown', reasons };
|
|
494
|
+
}
|
|
495
|
+
if (sameRunCount > 1) {
|
|
496
|
+
reasons.push('run_has_multiple_invocations');
|
|
497
|
+
return { association: 'ambiguous', reasons };
|
|
498
|
+
}
|
|
499
|
+
reasons.push('run_identity_proven');
|
|
500
|
+
return { association: 'strong_run', reasons };
|
|
501
|
+
}
|
|
502
|
+
function parsePrincipleIds(value) {
|
|
503
|
+
if (typeof value !== 'string')
|
|
504
|
+
return null;
|
|
505
|
+
try {
|
|
506
|
+
const parsed = JSON.parse(value);
|
|
507
|
+
if (!Array.isArray(parsed))
|
|
508
|
+
return null;
|
|
509
|
+
const ids = [];
|
|
510
|
+
for (const entry of parsed) {
|
|
511
|
+
// rc-4/rc-9: a partially invalid array is incomparable, not "absent".
|
|
512
|
+
if (typeof entry !== 'string' || entry.length === 0)
|
|
513
|
+
return null;
|
|
514
|
+
ids.push(entry);
|
|
515
|
+
}
|
|
516
|
+
return ids;
|
|
517
|
+
}
|
|
518
|
+
catch {
|
|
519
|
+
return null;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
function stringField(row, key) {
|
|
523
|
+
return optionalString(row[key]);
|
|
524
|
+
}
|
|
525
|
+
function numericField(row, key) {
|
|
526
|
+
return optionalNumber(row[key]);
|
|
527
|
+
}
|
|
528
|
+
function buildInvocation(input) {
|
|
529
|
+
const { invocation, sameRunCount, rangeComplete, actionsByRun, trajectory, limits } = input;
|
|
530
|
+
const { association, reasons } = classifyAssociation(invocation, sameRunCount, rangeComplete);
|
|
531
|
+
const openTs = invocation.open?.ts ?? null;
|
|
532
|
+
const openMs = openTs === null ? null : Date.parse(openTs);
|
|
533
|
+
const { runId, sessionId } = invocation.identity;
|
|
534
|
+
const key = runId === null || sessionId === null ? null : `${sessionId}\u0000${runId}`;
|
|
535
|
+
const runActions = key === null ? [] : (actionsByRun.get(key) ?? []);
|
|
536
|
+
const actions = [];
|
|
537
|
+
let actionsTruncated = false;
|
|
538
|
+
for (const row of runActions) {
|
|
539
|
+
if (actions.length >= limits.maxActionsPerInvocation) {
|
|
540
|
+
actionsTruncated = true;
|
|
541
|
+
break;
|
|
542
|
+
}
|
|
543
|
+
const relative = openMs === null
|
|
544
|
+
? 'no_open_timestamp'
|
|
545
|
+
: row.tsMs < openMs ? 'before_open' : 'after_open';
|
|
546
|
+
actions.push({
|
|
547
|
+
source: 'event_log.tool_call',
|
|
548
|
+
toolName: row.toolName,
|
|
549
|
+
toolCallIdRef: row.toolCallIdRef,
|
|
550
|
+
outcome: row.outcome,
|
|
551
|
+
exitCode: row.exitCode,
|
|
552
|
+
errorType: row.errorType,
|
|
553
|
+
errorPresent: row.errorPresent,
|
|
554
|
+
ts: row.ts,
|
|
555
|
+
ordering: 'unknown',
|
|
556
|
+
relativeToRecordedOpen: relative,
|
|
557
|
+
strength: association === 'strong_run' ? 'run_strong' : 'run_ambiguous',
|
|
558
|
+
verified: association === 'strong_run',
|
|
559
|
+
logRef: row.logRef,
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
if (actions.some((action) => action.relativeToRecordedOpen === 'before_open')) {
|
|
563
|
+
reasons.push('action_before_open');
|
|
564
|
+
}
|
|
565
|
+
actions.sort((left, right) => Date.parse(left.ts) - Date.parse(right.ts));
|
|
566
|
+
const actionsByTool = new Set();
|
|
567
|
+
let declaredTargetCount = 0;
|
|
568
|
+
for (const row of runActions) {
|
|
569
|
+
if (!row.declaresFilePath)
|
|
570
|
+
continue;
|
|
571
|
+
declaredTargetCount += 1;
|
|
572
|
+
actionsByTool.add(row.toolName);
|
|
573
|
+
}
|
|
574
|
+
const sessionRows = sessionId === null ? null : (trajectory.sessions.get(sessionId) ?? null);
|
|
575
|
+
const corrections = [];
|
|
576
|
+
let correctionsTruncated = false;
|
|
577
|
+
for (const row of sessionRows?.corrections ?? []) {
|
|
578
|
+
const userId = numericField(row, 'user_turn_id');
|
|
579
|
+
const referencedAssistantTurnId = numericField(row, 'references_assistant_turn_id');
|
|
580
|
+
const referencedRunId = stringField(row, 'referenced_run_id');
|
|
581
|
+
const runBound = runId !== null && referencedRunId === runId;
|
|
582
|
+
if (!runBound && referencedAssistantTurnId !== null && referencedRunId !== null)
|
|
583
|
+
continue;
|
|
584
|
+
if (corrections.length >= limits.maxCorrectionsPerInvocation) {
|
|
585
|
+
correctionsTruncated = true;
|
|
586
|
+
break;
|
|
587
|
+
}
|
|
588
|
+
corrections.push({
|
|
589
|
+
source: 'trajectory.user_turns',
|
|
590
|
+
userTurnId: userId ?? -1,
|
|
591
|
+
turnIndex: numericField(row, 'turn_index') ?? -1,
|
|
592
|
+
referencedAssistantTurnId,
|
|
593
|
+
referencedRunId,
|
|
594
|
+
createdAt: stringField(row, 'created_at') ?? '',
|
|
595
|
+
ordering: 'unknown',
|
|
596
|
+
strength: runBound ? (association === 'strong_run' ? 'run_strong' : 'run_ambiguous') : 'weak',
|
|
597
|
+
verified: runBound && association === 'strong_run',
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
const outcomes = [];
|
|
601
|
+
let outcomesTruncated = false;
|
|
602
|
+
for (const row of sessionRows?.outcomes ?? []) {
|
|
603
|
+
if (runId === null || stringField(row, 'task_id') !== runId)
|
|
604
|
+
continue;
|
|
605
|
+
if (outcomes.length >= limits.maxOutcomesPerInvocation) {
|
|
606
|
+
outcomesTruncated = true;
|
|
607
|
+
break;
|
|
608
|
+
}
|
|
609
|
+
const recordedOutcome = stringField(row, 'outcome');
|
|
610
|
+
const principleIds = parsePrincipleIds(row.principle_ids_json);
|
|
611
|
+
outcomes.push({
|
|
612
|
+
source: 'trajectory.task_outcomes',
|
|
613
|
+
outcomeId: numericField(row, 'id') ?? -1,
|
|
614
|
+
recordedOutcome,
|
|
615
|
+
modelCompletionRecorded: recordedOutcome === 'completed',
|
|
616
|
+
semantics: 'model_completion_only',
|
|
617
|
+
taskSuccess: 'unknown',
|
|
618
|
+
principleBinding: principleIds !== null && principleIds.length === 0 ? 'absent' : 'session_level_unknown',
|
|
619
|
+
principleIdCount: principleIds?.length ?? 0,
|
|
620
|
+
createdAt: stringField(row, 'created_at') ?? '',
|
|
621
|
+
strength: association === 'strong_run' ? 'run_strong' : 'run_ambiguous',
|
|
622
|
+
verified: association === 'strong_run',
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
const weakCandidates = [];
|
|
626
|
+
let weakTruncated = false;
|
|
627
|
+
if (sessionRows !== null) {
|
|
628
|
+
for (const row of sessionRows.weakToolCalls) {
|
|
629
|
+
if (weakCandidates.length >= limits.maxWeakCandidatesPerInvocation) {
|
|
630
|
+
weakTruncated = true;
|
|
631
|
+
break;
|
|
632
|
+
}
|
|
633
|
+
weakCandidates.push({
|
|
634
|
+
source: 'trajectory.tool_calls',
|
|
635
|
+
rowId: numericField(row, 'id') ?? -1,
|
|
636
|
+
toolName: stringField(row, 'tool_name') ?? 'unknown',
|
|
637
|
+
outcome: stringField(row, 'outcome'),
|
|
638
|
+
createdAt: stringField(row, 'created_at') ?? '',
|
|
639
|
+
reason: 'session_tool_time_only',
|
|
640
|
+
verified: false,
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
const unknowns = [];
|
|
645
|
+
const addUnknown = (fact, reasonCode) => {
|
|
646
|
+
unknowns.push({ fact, reasonCode });
|
|
647
|
+
};
|
|
648
|
+
for (const reason of reasons) {
|
|
649
|
+
if (reason === 'journal_range_incomplete')
|
|
650
|
+
addUnknown('run_uniqueness', 'journal_range_incomplete');
|
|
651
|
+
if (reason === 'run_has_multiple_invocations')
|
|
652
|
+
addUnknown('run_uniqueness', 'run_has_multiple_invocations');
|
|
653
|
+
if (reason === 'duplicate_records')
|
|
654
|
+
addUnknown('terminal_identity', 'duplicate_records');
|
|
655
|
+
if (reason === 'config_changed')
|
|
656
|
+
addUnknown('config_identity', 'config_changed');
|
|
657
|
+
if (reason === 'terminal_not_visible_in_range')
|
|
658
|
+
addUnknown('terminal', 'terminal_not_visible_in_range');
|
|
659
|
+
if (reason === 'open_not_visible_in_range')
|
|
660
|
+
addUnknown('open', 'open_not_visible_in_range');
|
|
661
|
+
}
|
|
662
|
+
if (actions.length > 0)
|
|
663
|
+
addUnknown('ordering', 'delivery_time_unavailable');
|
|
664
|
+
if (actionsTruncated)
|
|
665
|
+
addUnknown('actions', 'action_limit_reached');
|
|
666
|
+
if (correctionsTruncated)
|
|
667
|
+
addUnknown('corrections', 'correction_limit_reached');
|
|
668
|
+
if (outcomesTruncated)
|
|
669
|
+
addUnknown('outcomes', 'outcome_limit_reached');
|
|
670
|
+
if (weakTruncated)
|
|
671
|
+
addUnknown('weak_candidates', 'weak_candidate_limit_reached');
|
|
672
|
+
// Session-level truncation is not the per-opportunity cap: filtering by run
|
|
673
|
+
// can hide the missing rows, so it must degrade coverage on its own.
|
|
674
|
+
if (sessionRows?.correctionsTruncated)
|
|
675
|
+
addUnknown('corrections', 'session_rows_truncated');
|
|
676
|
+
if (sessionRows?.outcomesTruncated)
|
|
677
|
+
addUnknown('outcomes', 'session_rows_truncated');
|
|
678
|
+
if (sessionRows?.weakTruncated)
|
|
679
|
+
addUnknown('weak_candidates', 'session_rows_truncated');
|
|
680
|
+
if (sessionRows?.queryFailed)
|
|
681
|
+
addUnknown('trajectory', 'trajectory_row_limit_reached');
|
|
682
|
+
if (trajectory.status === 'unavailable') {
|
|
683
|
+
addUnknown('trajectory', trajectory.reasons.includes('trajectory_schema_incomplete')
|
|
684
|
+
? 'trajectory_schema_incomplete'
|
|
685
|
+
: 'trajectory_db_unavailable');
|
|
686
|
+
}
|
|
687
|
+
else if (trajectory.truncated) {
|
|
688
|
+
for (const reason of trajectory.reasons) {
|
|
689
|
+
if (reason === 'session_query_limit_reached')
|
|
690
|
+
addUnknown('trajectory', 'session_query_limit_reached');
|
|
691
|
+
if (reason === 'session_rows_truncated')
|
|
692
|
+
addUnknown('trajectory', 'session_rows_truncated');
|
|
693
|
+
if (reason === 'trajectory_row_limit_reached')
|
|
694
|
+
addUnknown('trajectory', 'trajectory_row_limit_reached');
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
if (outcomes.length > 0) {
|
|
698
|
+
addUnknown('task_result', 'model_completion_only');
|
|
699
|
+
addUnknown('task_result', 'task_success_not_observable');
|
|
700
|
+
addUnknown('principle_binding', 'principle_ids_not_comparable');
|
|
701
|
+
}
|
|
702
|
+
if (declaredTargetCount > 0)
|
|
703
|
+
addUnknown('artifact', 'artifact_effect_not_observable');
|
|
704
|
+
const conflicts = [];
|
|
705
|
+
if (invocation.classification === 'conflicted') {
|
|
706
|
+
conflicts.push({
|
|
707
|
+
kind: 'journal',
|
|
708
|
+
reasonCodes: invocation.reasonCodes.filter((code) => code !== 'output_truncated').sort(),
|
|
709
|
+
duplicateCount: invocation.duplicates.open + invocation.duplicates.terminal,
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
if (reasons.includes('config_changed')) {
|
|
713
|
+
conflicts.push({
|
|
714
|
+
kind: 'config_identity',
|
|
715
|
+
reasonCodes: ['effectiveHookSetFingerprint_mismatch'],
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
return {
|
|
719
|
+
intervention: {
|
|
720
|
+
hookInvocationId: invocation.invocationId,
|
|
721
|
+
runId,
|
|
722
|
+
sessionId,
|
|
723
|
+
host: invocation.identity.host,
|
|
724
|
+
workspaceVerified: invocation.identity.workspaceVerified,
|
|
725
|
+
artifactId: invocation.terminal?.artifactId ?? invocation.open?.artifactId ?? null,
|
|
726
|
+
activationId: invocation.terminal?.activationId ?? invocation.open?.activationId ?? null,
|
|
727
|
+
targetBodySha256: invocation.terminal?.targetBodySha256 ?? null,
|
|
728
|
+
configIdentity: invocation.terminal?.configIdentity ?? invocation.open?.configIdentity ?? null,
|
|
729
|
+
terminal: {
|
|
730
|
+
receiptState: invocation.terminal?.receiptState ?? null,
|
|
731
|
+
variants: invocation.terminalVariants.length,
|
|
732
|
+
classification: invocation.classification,
|
|
733
|
+
},
|
|
734
|
+
openTs,
|
|
735
|
+
terminalTs: invocation.terminal?.ts ?? null,
|
|
736
|
+
},
|
|
737
|
+
association,
|
|
738
|
+
associationReasons: reasons,
|
|
739
|
+
evidenceLayers: {
|
|
740
|
+
runAssociation: association,
|
|
741
|
+
requestAssociation: 'unknown',
|
|
742
|
+
principleAdherence: 'unknown',
|
|
743
|
+
behaviorImprovement: 'unknown',
|
|
744
|
+
},
|
|
745
|
+
facts: {
|
|
746
|
+
actions,
|
|
747
|
+
corrections,
|
|
748
|
+
outcomes,
|
|
749
|
+
artifacts: {
|
|
750
|
+
declaredTargetCount,
|
|
751
|
+
declaredByToolNames: [...actionsByTool].sort(),
|
|
752
|
+
confirmedCount: 0,
|
|
753
|
+
confirmation: 'unknown',
|
|
754
|
+
},
|
|
755
|
+
},
|
|
756
|
+
weakCandidates,
|
|
757
|
+
unknowns,
|
|
758
|
+
conflicts,
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
export function linkAdaptiveBehaviorEvidence(input) {
|
|
762
|
+
const now = input.now ?? new Date();
|
|
763
|
+
const generatedAt = now.toISOString();
|
|
764
|
+
const limits = resolveLimits(input.limits);
|
|
765
|
+
if (limits === null) {
|
|
766
|
+
return {
|
|
767
|
+
ok: false,
|
|
768
|
+
schemaVersion: ADAPTIVE_BEHAVIOR_LINKAGE_SCHEMA_VERSION,
|
|
769
|
+
generatedAt,
|
|
770
|
+
readOnly: true,
|
|
771
|
+
error: {
|
|
772
|
+
code: 'invalid_limit',
|
|
773
|
+
message: 'One or more read limits is not a positive integer.',
|
|
774
|
+
nextAction: '给出大于 0 的整数上限,或省略该上限使用默认值。',
|
|
775
|
+
},
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
if (!fs.existsSync(input.workspaceDir)) {
|
|
779
|
+
return {
|
|
780
|
+
ok: false,
|
|
781
|
+
schemaVersion: ADAPTIVE_BEHAVIOR_LINKAGE_SCHEMA_VERSION,
|
|
782
|
+
generatedAt,
|
|
783
|
+
readOnly: true,
|
|
784
|
+
error: {
|
|
785
|
+
code: 'workspace_unavailable',
|
|
786
|
+
message: `Workspace does not exist: ${input.workspaceDir}`,
|
|
787
|
+
nextAction: '使用 -w/--workspace 指向一个存在的 PD 工作区。',
|
|
788
|
+
},
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
const journal = reconcileAdaptiveJournal({
|
|
792
|
+
workspaceDir: input.workspaceDir,
|
|
793
|
+
since: input.since,
|
|
794
|
+
until: input.until,
|
|
795
|
+
host: input.host,
|
|
796
|
+
now,
|
|
797
|
+
limits: {
|
|
798
|
+
maxDateFilesPerDir: limits.maxDateFilesPerDir,
|
|
799
|
+
maxFileBytes: limits.maxFileBytes,
|
|
800
|
+
maxInvocations: limits.maxInvocations,
|
|
801
|
+
maxTerminalVariantsPerInvocation: limits.maxTerminalVariantsPerInvocation,
|
|
802
|
+
},
|
|
803
|
+
});
|
|
804
|
+
if (!journal.ok) {
|
|
805
|
+
return {
|
|
806
|
+
ok: false,
|
|
807
|
+
schemaVersion: ADAPTIVE_BEHAVIOR_LINKAGE_SCHEMA_VERSION,
|
|
808
|
+
generatedAt,
|
|
809
|
+
readOnly: true,
|
|
810
|
+
error: journal.error,
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
const { report } = journal;
|
|
814
|
+
const { window } = report.request;
|
|
815
|
+
const rangeComplete = journalRangeComplete(report);
|
|
816
|
+
const eventLog = {
|
|
817
|
+
dirsFound: 0,
|
|
818
|
+
filesRead: 0,
|
|
819
|
+
linesRead: 0,
|
|
820
|
+
toolCalls: 0,
|
|
821
|
+
truncated: false,
|
|
822
|
+
omittedFiles: 0,
|
|
823
|
+
parseErrors: 0,
|
|
824
|
+
errors: 0,
|
|
825
|
+
unattributedToolCalls: 0,
|
|
826
|
+
};
|
|
827
|
+
const toolCalls = readToolCallRows({
|
|
828
|
+
workspaceDir: input.workspaceDir,
|
|
829
|
+
window,
|
|
830
|
+
limits,
|
|
831
|
+
coverage: eventLog,
|
|
832
|
+
});
|
|
833
|
+
const actionsByRun = new Map();
|
|
834
|
+
for (const row of toolCalls) {
|
|
835
|
+
if (row.runId === null || row.sessionId === null)
|
|
836
|
+
continue;
|
|
837
|
+
const key = `${row.sessionId}\u0000${row.runId}`;
|
|
838
|
+
const bucket = actionsByRun.get(key);
|
|
839
|
+
if (bucket === undefined)
|
|
840
|
+
actionsByRun.set(key, [row]);
|
|
841
|
+
else
|
|
842
|
+
bucket.push(row);
|
|
843
|
+
}
|
|
844
|
+
const trajectory = readTrajectory({
|
|
845
|
+
workspaceDir: input.workspaceDir,
|
|
846
|
+
invocations: report.invocations,
|
|
847
|
+
window,
|
|
848
|
+
limits,
|
|
849
|
+
});
|
|
850
|
+
const runCounts = new Map();
|
|
851
|
+
for (const invocation of report.invocations) {
|
|
852
|
+
const { runId, sessionId } = invocation.identity;
|
|
853
|
+
if (runId === null || sessionId === null)
|
|
854
|
+
continue;
|
|
855
|
+
const key = `${sessionId}\u0000${runId}`;
|
|
856
|
+
runCounts.set(key, (runCounts.get(key) ?? 0) + 1);
|
|
857
|
+
}
|
|
858
|
+
const invocations = report.invocations.map((invocation) => {
|
|
859
|
+
const { runId, sessionId } = invocation.identity;
|
|
860
|
+
const key = runId === null || sessionId === null ? null : `${sessionId}\u0000${runId}`;
|
|
861
|
+
const sameRunCount = key === null ? 0 : (runCounts.get(key) ?? 0);
|
|
862
|
+
return buildInvocation({
|
|
863
|
+
invocation,
|
|
864
|
+
sameRunCount,
|
|
865
|
+
rangeComplete,
|
|
866
|
+
actionsByRun,
|
|
867
|
+
trajectory,
|
|
868
|
+
limits,
|
|
869
|
+
});
|
|
870
|
+
});
|
|
871
|
+
const incompleteReasons = [];
|
|
872
|
+
if (!rangeComplete)
|
|
873
|
+
incompleteReasons.push('journal_range_incomplete');
|
|
874
|
+
if (eventLog.dirsFound === 0)
|
|
875
|
+
incompleteReasons.push('event_log_unavailable');
|
|
876
|
+
if (eventLog.omittedFiles > 0)
|
|
877
|
+
incompleteReasons.push('event_log_files_omitted');
|
|
878
|
+
else if (eventLog.truncated)
|
|
879
|
+
incompleteReasons.push('event_log_parse_limit_reached');
|
|
880
|
+
if (eventLog.parseErrors > 0)
|
|
881
|
+
incompleteReasons.push('event_log_parse_errors');
|
|
882
|
+
if (eventLog.errors > 0)
|
|
883
|
+
incompleteReasons.push('event_log_read_error');
|
|
884
|
+
if (trajectory.status === 'unavailable') {
|
|
885
|
+
incompleteReasons.push(trajectory.reasons.includes('trajectory_schema_incomplete')
|
|
886
|
+
? 'trajectory_schema_incomplete'
|
|
887
|
+
: 'trajectory_db_unavailable');
|
|
888
|
+
}
|
|
889
|
+
else {
|
|
890
|
+
if (trajectory.reasons.includes('trajectory_row_limit_reached'))
|
|
891
|
+
incompleteReasons.push('trajectory_row_limit_reached');
|
|
892
|
+
if (trajectory.reasons.includes('session_query_limit_reached'))
|
|
893
|
+
incompleteReasons.push('session_query_limit_reached');
|
|
894
|
+
if (trajectory.reasons.includes('session_rows_truncated'))
|
|
895
|
+
incompleteReasons.push('session_rows_truncated');
|
|
896
|
+
if (trajectory.reasons.includes('trajectory_query_failed'))
|
|
897
|
+
incompleteReasons.push('trajectory_query_failed');
|
|
898
|
+
}
|
|
899
|
+
// Per-opportunity caps are budgets too: a lower-bound list must not read as complete.
|
|
900
|
+
const perInvocationCaps = [
|
|
901
|
+
'action_limit_reached',
|
|
902
|
+
'correction_limit_reached',
|
|
903
|
+
'outcome_limit_reached',
|
|
904
|
+
'weak_candidate_limit_reached',
|
|
905
|
+
'session_rows_truncated',
|
|
906
|
+
];
|
|
907
|
+
for (const code of perInvocationCaps) {
|
|
908
|
+
if (invocations.some((item) => item.unknowns.some((entry) => entry.reasonCode === code))) {
|
|
909
|
+
incompleteReasons.push(code);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
const byAssociation = {
|
|
913
|
+
strong_run: 0,
|
|
914
|
+
weak: 0,
|
|
915
|
+
ambiguous: 0,
|
|
916
|
+
unknown: 0,
|
|
917
|
+
conflict: 0,
|
|
918
|
+
};
|
|
919
|
+
for (const invocation of invocations)
|
|
920
|
+
byAssociation[invocation.association] += 1;
|
|
921
|
+
const nextAction = [];
|
|
922
|
+
if (incompleteReasons.includes('journal_range_incomplete'))
|
|
923
|
+
nextAction.push('缩小 --since 窗口或提高 --max-date-files 后重试;日志中被丢弃的解析/身份失败不会被当作“不存在”。');
|
|
924
|
+
if (incompleteReasons.includes('event_log_unavailable'))
|
|
925
|
+
nextAction.push('未找到 EventLog 目录:本次没有可关联的工具事件,不代表没有发生行动。');
|
|
926
|
+
if (incompleteReasons.includes('event_log_files_omitted'))
|
|
927
|
+
nextAction.push('提高 --max-date-files,或缩小 --since 窗口以读取全部工具事件文件。');
|
|
928
|
+
if (incompleteReasons.includes('event_log_parse_limit_reached'))
|
|
929
|
+
nextAction.push('提高 --max-event-lines,或缩小时间窗口以读取完整的工具事件。');
|
|
930
|
+
if (incompleteReasons.includes('event_log_parse_errors'))
|
|
931
|
+
nextAction.push('部分 EventLog 行无法解析;这些行未被当作“没有行动”。');
|
|
932
|
+
if (incompleteReasons.includes('event_log_read_error'))
|
|
933
|
+
nextAction.push('部分日志目录/文件不可读;检查权限与符号链接后重试。');
|
|
934
|
+
if (incompleteReasons.includes('trajectory_db_unavailable'))
|
|
935
|
+
nextAction.push('轨迹库尚不存在;先让插件写入轨迹,或仅使用运行级关联。');
|
|
936
|
+
if (incompleteReasons.includes('trajectory_schema_incomplete'))
|
|
937
|
+
nextAction.push('轨迹库缺少本读取器所需列;本命令不迁移,请先修复写侧 schema。');
|
|
938
|
+
if (incompleteReasons.includes('trajectory_query_failed'))
|
|
939
|
+
nextAction.push('轨迹查询失败;修复数据库可读性后重试。');
|
|
940
|
+
if (incompleteReasons.includes('action_limit_reached'))
|
|
941
|
+
nextAction.push('提高 --max-actions 以读取该机会的全部行动。');
|
|
942
|
+
if (incompleteReasons.includes('correction_limit_reached'))
|
|
943
|
+
nextAction.push('提高 --max-corrections 以读取该机会的全部纠正记录。');
|
|
944
|
+
if (incompleteReasons.includes('outcome_limit_reached'))
|
|
945
|
+
nextAction.push('提高 --max-outcomes 以读取该机会的全部任务结果。');
|
|
946
|
+
if (incompleteReasons.includes('weak_candidate_limit_reached'))
|
|
947
|
+
nextAction.push('提高 --max-weak-candidates 以读取全部弱候选。');
|
|
948
|
+
if (incompleteReasons.includes('trajectory_row_limit_reached') || incompleteReasons.includes('session_query_limit_reached')) {
|
|
949
|
+
nextAction.push('提高 --max-trajectory-rows/--max-session-queries,或缩小时间窗口。');
|
|
950
|
+
}
|
|
951
|
+
if (incompleteReasons.includes('session_rows_truncated')) {
|
|
952
|
+
nextAction.push(`该会话的分类查询已达到固定的单会话 ${BEHAVIOR_LINKAGE_ROWS_PER_SESSION} 行上限;请缩小 --since/--until 时间窗口后重试(--max-trajectory-rows 不控制该上限)。空结果不代表“不存在”。`);
|
|
953
|
+
}
|
|
954
|
+
if (eventLog.unattributedToolCalls > 0)
|
|
955
|
+
nextAction.push('部分工具事件缺少 run/session 身份,已排除在关联之外;不要把它们当作未发生。');
|
|
956
|
+
if (invocations.some((item) => item.association === 'ambiguous'))
|
|
957
|
+
nextAction.push('同一 run 存在多个干预机会;按 hookInvocationId 单独复核。');
|
|
958
|
+
if (nextAction.length === 0)
|
|
959
|
+
nextAction.push('无需操作。');
|
|
960
|
+
const reportOut = {
|
|
961
|
+
schemaVersion: ADAPTIVE_BEHAVIOR_LINKAGE_SCHEMA_VERSION,
|
|
962
|
+
generatedAt,
|
|
963
|
+
readOnly: true,
|
|
964
|
+
request: {
|
|
965
|
+
workspaceDir: input.workspaceDir,
|
|
966
|
+
host: report.request.host,
|
|
967
|
+
window,
|
|
968
|
+
},
|
|
969
|
+
coverage: {
|
|
970
|
+
complete: incompleteReasons.length === 0,
|
|
971
|
+
window,
|
|
972
|
+
journal: {
|
|
973
|
+
status: report.coverage.status,
|
|
974
|
+
complete: rangeComplete,
|
|
975
|
+
filesRead: report.coverage.filesRead,
|
|
976
|
+
filesSkipped: report.coverage.filesSkipped,
|
|
977
|
+
limitCode: report.coverage.limitCode,
|
|
978
|
+
truncatedFiles: report.coverage.truncatedFiles.length,
|
|
979
|
+
errors: report.coverage.errors.length,
|
|
980
|
+
malformedLines: report.totals.malformedJsonLines ?? 0,
|
|
981
|
+
invalidRecords: report.totals.invalidRecords ?? 0,
|
|
982
|
+
},
|
|
983
|
+
eventLog: {
|
|
984
|
+
dirsFound: eventLog.dirsFound,
|
|
985
|
+
filesRead: eventLog.filesRead,
|
|
986
|
+
linesRead: eventLog.linesRead,
|
|
987
|
+
toolCalls: eventLog.toolCalls,
|
|
988
|
+
truncated: eventLog.truncated,
|
|
989
|
+
omittedFiles: eventLog.omittedFiles,
|
|
990
|
+
parseErrors: eventLog.parseErrors,
|
|
991
|
+
errors: eventLog.errors,
|
|
992
|
+
unattributedToolCalls: eventLog.unattributedToolCalls,
|
|
993
|
+
},
|
|
994
|
+
trajectory: {
|
|
995
|
+
status: trajectory.status,
|
|
996
|
+
rowsRead: trajectory.rowsRead,
|
|
997
|
+
truncated: trajectory.truncated,
|
|
998
|
+
reasons: [...new Set(trajectory.reasons)].sort(),
|
|
999
|
+
},
|
|
1000
|
+
incompleteReasons,
|
|
1001
|
+
},
|
|
1002
|
+
totals: {
|
|
1003
|
+
invocations: invocations.length,
|
|
1004
|
+
byAssociation,
|
|
1005
|
+
actions: invocations.reduce((sum, item) => sum + item.facts.actions.length, 0),
|
|
1006
|
+
corrections: invocations.reduce((sum, item) => sum + item.facts.corrections.length, 0),
|
|
1007
|
+
outcomes: invocations.reduce((sum, item) => sum + item.facts.outcomes.length, 0),
|
|
1008
|
+
weakCandidates: invocations.reduce((sum, item) => sum + item.weakCandidates.length, 0),
|
|
1009
|
+
},
|
|
1010
|
+
invocations,
|
|
1011
|
+
nextAction,
|
|
1012
|
+
};
|
|
1013
|
+
return { ok: true, report: reportOut };
|
|
1014
|
+
}
|