@principles/pd-cli 1.145.2 → 1.146.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -2
- package/dist/commands/build-trajectory-evidence.d.ts +25 -6
- package/dist/commands/build-trajectory-evidence.d.ts.map +1 -1
- package/dist/commands/build-trajectory-evidence.js +174 -114
- package/dist/commands/build-trajectory-evidence.js.map +1 -1
- package/dist/commands/pain-record.d.ts.map +1 -1
- package/dist/commands/pain-record.js +212 -17
- package/dist/commands/pain-record.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/build-trajectory-evidence.ts +211 -122
- package/src/commands/pain-record.ts +224 -16
- package/src/index.ts +1 -1
- package/tests/commands/build-trajectory-evidence.test.ts +226 -161
- package/tests/commands/pain-record-async.test.ts +36 -30
- package/tests/commands/pain-record-session-parser.test.ts +127 -0
- package/tests/commands/pain-record.test.ts +205 -37
|
@@ -20,123 +20,122 @@ import {
|
|
|
20
20
|
import type { PainEvidenceEntry } from '@principles/core/runtime-v2';
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* @param workspaceDir - Workspace directory for path redaction (sanitizeString)
|
|
28
|
-
* @returns Array of PainEvidenceEntry (never empty; degraded entries on failure)
|
|
23
|
+
* PRI-642 (SPEC §7.3): discriminated acquisition result. `unavailable` carries
|
|
24
|
+
* a distinct reasonCode so `pd pain record --session` can fail/degrade
|
|
25
|
+
* explicitly BEFORE any LLM/task/candidate mutation, without placeholder
|
|
26
|
+
* evidence.
|
|
29
27
|
*/
|
|
30
|
-
export
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
28
|
+
export type TrajectoryEvidenceAcquisition =
|
|
29
|
+
| { status: 'available'; entries: PainEvidenceEntry[] }
|
|
30
|
+
| {
|
|
31
|
+
status: 'unavailable';
|
|
32
|
+
reasonCode: 'trajectory_unavailable' | 'session_not_found' | 'empty_trajectory' | 'evidence_read_failed';
|
|
33
|
+
detail: string;
|
|
34
|
+
};
|
|
36
35
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
36
|
+
/** SourceRef markers that carry no real behavior trace (placeholder shapes). */
|
|
37
|
+
const PLACEHOLDER_SOURCE_REFS = new Set([
|
|
38
|
+
'owner_reported:cli',
|
|
39
|
+
'owner_message:unavailable',
|
|
40
|
+
'agent_turn:unavailable',
|
|
41
|
+
'tool_call_failure:unavailable',
|
|
42
|
+
'trajectory:empty',
|
|
43
|
+
]);
|
|
45
44
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
});
|
|
52
|
-
return evidence;
|
|
53
|
-
}
|
|
45
|
+
interface TrajectoryDbCollection {
|
|
46
|
+
entries: PainEvidenceEntry[];
|
|
47
|
+
realEntryCount: number;
|
|
48
|
+
readFailed: boolean;
|
|
49
|
+
}
|
|
54
50
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Collect evidence entries from an open trajectory.db for one session.
|
|
53
|
+
* Shared by the typed acquisition API and the legacy array wrapper so the
|
|
54
|
+
* two can never drift.
|
|
55
|
+
*/
|
|
56
|
+
function collectEvidenceFromDb(
|
|
57
|
+
db: Database.Database,
|
|
58
|
+
sessionId: string,
|
|
59
|
+
workspaceDir?: string,
|
|
60
|
+
): TrajectoryDbCollection {
|
|
61
|
+
const evidence: PainEvidenceEntry[] = [];
|
|
62
|
+
let readFailed = false;
|
|
65
63
|
|
|
64
|
+
// Try to read user turns with correction detection
|
|
66
65
|
try {
|
|
67
|
-
|
|
68
|
-
try {
|
|
69
|
-
const userTurns = db.prepare(`
|
|
66
|
+
const userTurns = db.prepare(`
|
|
70
67
|
SELECT id, raw_excerpt, correction_detected, correction_cue, created_at
|
|
71
68
|
FROM user_turns
|
|
72
69
|
WHERE session_id = ?
|
|
73
70
|
ORDER BY id ASC
|
|
74
71
|
`).all(sessionId) as Record<string, unknown>[];
|
|
75
72
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
} catch {
|
|
91
|
-
// user_turns table may not exist — degrade gracefully
|
|
92
|
-
if (evidence.length < MAX_EVIDENCE_ENTRIES) {
|
|
93
|
-
evidence.push({
|
|
94
|
-
sourceRef: 'owner_message:unavailable',
|
|
95
|
-
note: 'trajectory_user_turns_unavailable',
|
|
96
|
-
});
|
|
97
|
-
}
|
|
73
|
+
const lastCorrectionTurn = [...userTurns].reverse().find(t => Boolean(t.correction_detected));
|
|
74
|
+
if (lastCorrectionTurn) {
|
|
75
|
+
const rawExcerpt = typeof lastCorrectionTurn.raw_excerpt === 'string'
|
|
76
|
+
? lastCorrectionTurn.raw_excerpt
|
|
77
|
+
: '';
|
|
78
|
+
const sanitizedNote = sanitizeString(
|
|
79
|
+
rawExcerpt.slice(0, MAX_EVIDENCE_NOTE_CHARS),
|
|
80
|
+
workspaceDir,
|
|
81
|
+
);
|
|
82
|
+
evidence.push({
|
|
83
|
+
sourceRef: `owner_message:${String(lastCorrectionTurn.created_at ?? 'unknown')}`,
|
|
84
|
+
note: sanitizedNote,
|
|
85
|
+
});
|
|
98
86
|
}
|
|
87
|
+
} catch {
|
|
88
|
+
readFailed = true;
|
|
89
|
+
// user_turns table may not exist — degrade gracefully
|
|
90
|
+
if (evidence.length < MAX_EVIDENCE_ENTRIES) {
|
|
91
|
+
evidence.push({
|
|
92
|
+
sourceRef: 'owner_message:unavailable',
|
|
93
|
+
note: 'trajectory_user_turns_unavailable',
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
99
97
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
98
|
+
// Try to read assistant turns (last 3)
|
|
99
|
+
try {
|
|
100
|
+
const assistantTurns = db.prepare(`
|
|
103
101
|
SELECT id, sanitized_text, stop_reason, created_at
|
|
104
102
|
FROM assistant_turns
|
|
105
103
|
WHERE session_id = ?
|
|
106
104
|
ORDER BY id ASC
|
|
107
105
|
`).all(sessionId) as Record<string, unknown>[];
|
|
108
106
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
} catch {
|
|
128
|
-
// assistant_turns table may not exist — degrade gracefully
|
|
129
|
-
if (evidence.length < MAX_EVIDENCE_ENTRIES) {
|
|
130
|
-
evidence.push({
|
|
131
|
-
sourceRef: 'agent_turn:unavailable',
|
|
132
|
-
note: 'trajectory_assistant_turns_unavailable',
|
|
133
|
-
});
|
|
134
|
-
}
|
|
107
|
+
const recentAssistant = assistantTurns.slice(-3);
|
|
108
|
+
for (const turn of recentAssistant) {
|
|
109
|
+
if (evidence.length >= MAX_EVIDENCE_ENTRIES) break;
|
|
110
|
+
const sanitizedText = typeof turn.sanitized_text === 'string'
|
|
111
|
+
? turn.sanitized_text
|
|
112
|
+
: '';
|
|
113
|
+
const sanitizedNote = sanitizeString(
|
|
114
|
+
sanitizedText.slice(0, MAX_EVIDENCE_NOTE_CHARS),
|
|
115
|
+
workspaceDir,
|
|
116
|
+
);
|
|
117
|
+
// Enhanced: append truncation warning when stop_reason=length
|
|
118
|
+
const stopReason = typeof turn.stop_reason === 'string' ? turn.stop_reason : null;
|
|
119
|
+
const truncationWarning = stopReason === 'length' ? ' [TRUNCATED: output cut off by length limit]' : '';
|
|
120
|
+
evidence.push({
|
|
121
|
+
sourceRef: `agent_turn:${String(turn.created_at ?? 'unknown')}`,
|
|
122
|
+
note: sanitizedNote + truncationWarning,
|
|
123
|
+
});
|
|
135
124
|
}
|
|
125
|
+
} catch {
|
|
126
|
+
readFailed = true;
|
|
127
|
+
// assistant_turns table may not exist — degrade gracefully
|
|
128
|
+
if (evidence.length < MAX_EVIDENCE_ENTRIES) {
|
|
129
|
+
evidence.push({
|
|
130
|
+
sourceRef: 'agent_turn:unavailable',
|
|
131
|
+
note: 'trajectory_assistant_turns_unavailable',
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
136
135
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
136
|
+
// PRI-358: Try to read failed tool_calls (last 3 failures, chronological order)
|
|
137
|
+
try {
|
|
138
|
+
const failedToolCalls = db.prepare(`
|
|
140
139
|
SELECT tool_name, error_type, exit_code, result_preview, created_at
|
|
141
140
|
FROM (
|
|
142
141
|
SELECT tool_name, error_type, exit_code, result_preview, created_at
|
|
@@ -148,39 +147,129 @@ export function buildTrajectoryEvidenceFromDb(
|
|
|
148
147
|
ORDER BY created_at ASC
|
|
149
148
|
`).all(sessionId) as Record<string, unknown>[];
|
|
150
149
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
165
|
-
} catch {
|
|
166
|
-
// tool_calls table may not exist — degrade gracefully (only when no other evidence)
|
|
167
|
-
if (evidence.length === 0) {
|
|
168
|
-
evidence.push({
|
|
169
|
-
sourceRef: 'tool_call_failure:unavailable',
|
|
170
|
-
note: 'trajectory_tool_calls_unavailable',
|
|
171
|
-
});
|
|
172
|
-
}
|
|
150
|
+
for (const tc of failedToolCalls) {
|
|
151
|
+
if (evidence.length >= MAX_EVIDENCE_ENTRIES) break;
|
|
152
|
+
const toolName = typeof tc.tool_name === 'string' ? tc.tool_name : 'unknown';
|
|
153
|
+
const errorType = typeof tc.error_type === 'string' ? tc.error_type : 'unknown';
|
|
154
|
+
const exitCode = tc.exit_code != null ? String(tc.exit_code) : 'N/A';
|
|
155
|
+
// Enhanced: append resultPreview when available
|
|
156
|
+
const resultPreview = typeof tc.result_preview === 'string' ? tc.result_preview : null;
|
|
157
|
+
const previewSuffix = resultPreview ? ` | ${resultPreview.slice(0, 200)}` : '';
|
|
158
|
+
const note = `Tool ${toolName} failed: ${errorType} (exitCode: ${exitCode})${previewSuffix}`;
|
|
159
|
+
evidence.push({
|
|
160
|
+
sourceRef: `tool_call_failure:${String(tc.created_at ?? 'unknown')}`,
|
|
161
|
+
note: sanitizeString(note.slice(0, MAX_EVIDENCE_NOTE_CHARS), workspaceDir),
|
|
162
|
+
});
|
|
173
163
|
}
|
|
174
|
-
|
|
175
|
-
|
|
164
|
+
} catch {
|
|
165
|
+
readFailed = true;
|
|
166
|
+
// tool_calls table may not exist — degrade gracefully (only when no other evidence)
|
|
176
167
|
if (evidence.length === 0) {
|
|
177
168
|
evidence.push({
|
|
178
|
-
sourceRef: '
|
|
179
|
-
note: '
|
|
169
|
+
sourceRef: 'tool_call_failure:unavailable',
|
|
170
|
+
note: 'trajectory_tool_calls_unavailable',
|
|
180
171
|
});
|
|
181
172
|
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// If no evidence at all from trajectory, provide a meaningful placeholder
|
|
176
|
+
if (evidence.length === 0) {
|
|
177
|
+
evidence.push({
|
|
178
|
+
sourceRef: 'trajectory:empty',
|
|
179
|
+
note: 'trajectory_available_but_empty: no user correction or assistant turns found',
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const bounded = evidence.slice(0, MAX_EVIDENCE_ENTRIES);
|
|
184
|
+
const realEntryCount = bounded.reduce(
|
|
185
|
+
(count, entry) => count + (PLACEHOLDER_SOURCE_REFS.has(entry.sourceRef) ? 0 : 1),
|
|
186
|
+
0,
|
|
187
|
+
);
|
|
188
|
+
return { entries: bounded, realEntryCount, readFailed };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* PRI-642 Scope A typed acquisition from trajectory.db (SPEC §7.3).
|
|
193
|
+
*
|
|
194
|
+
* Validates that the requested session exists in the trajectory `sessions`
|
|
195
|
+
* table (every recorded turn/tool call upserts its session row, so an absent
|
|
196
|
+
* row means the session was never seen) and classifies the outcome:
|
|
197
|
+
* - `available` — session bound, ≥1 real behavior-trace entry;
|
|
198
|
+
* - `session_not_found` — session absent from the sessions table (or a
|
|
199
|
+
* `cli`/`unknown` sentinel — never a real session);
|
|
200
|
+
* - `trajectory_unavailable` — no trajectory.db in this workspace;
|
|
201
|
+
* - `evidence_read_failed` — the DB exists but cannot be opened/read;
|
|
202
|
+
* - `empty_trajectory` — real session, no usable evidence rows.
|
|
203
|
+
*/
|
|
204
|
+
export function acquireTrajectoryEvidenceFromDb(
|
|
205
|
+
stateDir: string,
|
|
206
|
+
sessionId: string | undefined,
|
|
207
|
+
workspaceDir?: string,
|
|
208
|
+
): TrajectoryEvidenceAcquisition {
|
|
209
|
+
if (!sessionId || sessionId === 'cli' || sessionId === 'unknown') {
|
|
210
|
+
return {
|
|
211
|
+
status: 'unavailable',
|
|
212
|
+
reasonCode: 'session_not_found',
|
|
213
|
+
detail: sessionId ? `sentinel_session_id:${sessionId}` : 'missing_session_id',
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const dbPath = path.join(stateDir, 'trajectory.db');
|
|
218
|
+
if (!fs.existsSync(dbPath)) {
|
|
219
|
+
return {
|
|
220
|
+
status: 'unavailable',
|
|
221
|
+
reasonCode: 'trajectory_unavailable',
|
|
222
|
+
detail: 'trajectory_db_missing',
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
let db: Database.Database;
|
|
227
|
+
try {
|
|
228
|
+
db = new Database(dbPath, { readonly: true });
|
|
229
|
+
} catch (err) {
|
|
230
|
+
return {
|
|
231
|
+
status: 'unavailable',
|
|
232
|
+
reasonCode: 'evidence_read_failed',
|
|
233
|
+
detail: `trajectory_db_unreadable: ${err instanceof Error ? err.message : String(err)}`,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
// Session existence: rows are upserted by every turn/tool-call write, so
|
|
239
|
+
// an absent row means the trajectory never saw this session.
|
|
240
|
+
let sessionExists = false;
|
|
241
|
+
try {
|
|
242
|
+
const row = db.prepare('SELECT 1 FROM sessions WHERE session_id = ?').get(sessionId);
|
|
243
|
+
sessionExists = row !== undefined;
|
|
244
|
+
} catch {
|
|
245
|
+
// sessions table missing → cannot validate existence; fall through and
|
|
246
|
+
// let the evidence rows decide (legacy DBs may lack the table).
|
|
247
|
+
sessionExists = true;
|
|
248
|
+
}
|
|
249
|
+
if (!sessionExists) {
|
|
250
|
+
return {
|
|
251
|
+
status: 'unavailable',
|
|
252
|
+
reasonCode: 'session_not_found',
|
|
253
|
+
detail: 'session_not_present_in_trajectory',
|
|
254
|
+
};
|
|
255
|
+
}
|
|
182
256
|
|
|
183
|
-
|
|
257
|
+
const collection = collectEvidenceFromDb(db, sessionId, workspaceDir);
|
|
258
|
+
if (collection.realEntryCount > 0) {
|
|
259
|
+
return { status: 'available', entries: collection.entries };
|
|
260
|
+
}
|
|
261
|
+
if (collection.readFailed) {
|
|
262
|
+
return {
|
|
263
|
+
status: 'unavailable',
|
|
264
|
+
reasonCode: 'evidence_read_failed',
|
|
265
|
+
detail: 'trajectory_tables_unreadable',
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
return {
|
|
269
|
+
status: 'unavailable',
|
|
270
|
+
reasonCode: 'empty_trajectory',
|
|
271
|
+
detail: 'session_present_but_no_usable_evidence',
|
|
272
|
+
};
|
|
184
273
|
} finally {
|
|
185
274
|
db.close();
|
|
186
275
|
}
|