sidekick-shared 0.27.4 → 0.27.5
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.md +13 -0
- package/README.md +57 -11
- package/dist/index.d.ts +3 -0
- package/dist/index.js +13 -11
- package/dist/node.d.ts +3 -0
- package/dist/node.js +4 -2
- package/dist/promptHistory.d.ts +66 -0
- package/dist/promptHistory.js +37 -18
- package/dist/sessionPromptHistory.d.ts +120 -0
- package/dist/sessionPromptHistory.js +278 -0
- package/dist/sessionPromptSignals.d.ts +56 -0
- package/dist/sessionPromptSignals.js +355 -0
- package/package.json +1 -1
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node-only whole-session view of prompt history: every human prompt of a
|
|
3
|
+
* Claude Code or Codex session, grouped by session, for consumers that
|
|
4
|
+
* classify a session as a whole (intent, conflict, issues).
|
|
5
|
+
*
|
|
6
|
+
* Sessions are read from the start with the same scanners as
|
|
7
|
+
* `collectPromptHistory()`, so scope rules, prompt text, first-call model and
|
|
8
|
+
* usage, and ordinals are identical. The replies and interaction signals that
|
|
9
|
+
* the prompt filter leaves out are opt-in through `include`.
|
|
10
|
+
*
|
|
11
|
+
* There is no cursor: each call returns whole sessions. A bound that stops a
|
|
12
|
+
* call lists the sessions it did not read in `unread`; passing them back as
|
|
13
|
+
* `sessionIds` continues. Scope fails closed as in `collectPromptHistory()`,
|
|
14
|
+
* and replies or signals that belong to an out-of-scope prompt are dropped.
|
|
15
|
+
*/
|
|
16
|
+
import { type PromptHistoryEntry, type PromptHistoryExclusion, type PromptHistoryProvider } from './promptHistory';
|
|
17
|
+
import { type PromptHistoryReply, type PromptHistorySignal } from './sessionPromptSignals';
|
|
18
|
+
export interface SessionPromptHistoryInclude {
|
|
19
|
+
/** Interrupts, rejected and failed tool calls, compactions, API errors, rollbacks. */
|
|
20
|
+
signals?: boolean;
|
|
21
|
+
/** The agent's last text for each prompt's turn. */
|
|
22
|
+
replies?: boolean;
|
|
23
|
+
}
|
|
24
|
+
export interface SessionPromptHistoryBounds {
|
|
25
|
+
/** Session files read in one call. Default 1000. */
|
|
26
|
+
maxSessions?: number;
|
|
27
|
+
/** Bytes read from one session; a longer session is returned `truncated`. Default 256 MiB. */
|
|
28
|
+
maxSessionBytes?: number;
|
|
29
|
+
/** Bytes read across all sessions in one call. Default 256 MiB. */
|
|
30
|
+
maxTotalBytes?: number;
|
|
31
|
+
/** Largest single log record that is parsed. Default 16 MiB. */
|
|
32
|
+
maxRecordBytes?: number;
|
|
33
|
+
/** Wall-clock budget for the call. Default 60 000 ms. */
|
|
34
|
+
deadlineMs?: number;
|
|
35
|
+
}
|
|
36
|
+
export interface CollectSessionPromptHistoryOptions {
|
|
37
|
+
/** Absolute workspace roots; realpath-normalized internally. */
|
|
38
|
+
workspacePaths: string[];
|
|
39
|
+
/** Add git worktree siblings of each root. Default true. */
|
|
40
|
+
includeWorktrees?: boolean;
|
|
41
|
+
/** Default both. */
|
|
42
|
+
providers?: PromptHistoryProvider[];
|
|
43
|
+
/** Only these sessions (exact ids). */
|
|
44
|
+
sessionIds?: string[];
|
|
45
|
+
/** Only sessions whose file changed at or after this. Each comes back whole. */
|
|
46
|
+
since?: Date;
|
|
47
|
+
/** Return at most this many sessions, most recently modified first. */
|
|
48
|
+
limit?: number;
|
|
49
|
+
/** Opt-in content beyond the prompts. */
|
|
50
|
+
include?: SessionPromptHistoryInclude;
|
|
51
|
+
bounds?: SessionPromptHistoryBounds;
|
|
52
|
+
signal?: AbortSignal;
|
|
53
|
+
}
|
|
54
|
+
export interface PromptHistorySessionPrompt extends Omit<PromptHistoryEntry, 'provider' | 'sessionId'> {
|
|
55
|
+
/** With `include.replies`: the agent's last text for this prompt's turn. */
|
|
56
|
+
reply?: PromptHistoryReply;
|
|
57
|
+
}
|
|
58
|
+
export interface PromptHistorySession {
|
|
59
|
+
provider: PromptHistoryProvider;
|
|
60
|
+
sessionId: string;
|
|
61
|
+
/** Earliest timestamp in the session log. */
|
|
62
|
+
startedAt: string;
|
|
63
|
+
/** Latest timestamp in the session log. */
|
|
64
|
+
lastActivityAt: string;
|
|
65
|
+
/** Distinct prompt working directories, in order of first use. */
|
|
66
|
+
cwds: string[];
|
|
67
|
+
/** Distinct prompt git branches, in order of first use. */
|
|
68
|
+
gitBranches: string[];
|
|
69
|
+
/** Distinct models that answered a prompt, in order of first use. */
|
|
70
|
+
models: string[];
|
|
71
|
+
/** Every human prompt in scope, by ordinal. */
|
|
72
|
+
prompts: PromptHistorySessionPrompt[];
|
|
73
|
+
/** With `include.signals`: interaction signals in log order. */
|
|
74
|
+
signals?: PromptHistorySignal[];
|
|
75
|
+
/** Read to the end with no dropped prompts and no excluded records. */
|
|
76
|
+
complete: boolean;
|
|
77
|
+
/** `maxSessionBytes` stopped the read; later prompts are missing. */
|
|
78
|
+
truncated: boolean;
|
|
79
|
+
/** Prompts left out because their cwd was out of scope or they had no timestamp. */
|
|
80
|
+
droppedPrompts: number;
|
|
81
|
+
/** Records left out as oversized or malformed (see `exclusions`). */
|
|
82
|
+
excludedRecords: number;
|
|
83
|
+
}
|
|
84
|
+
export interface PromptHistorySessionRef {
|
|
85
|
+
provider: PromptHistoryProvider;
|
|
86
|
+
sessionId: string;
|
|
87
|
+
}
|
|
88
|
+
export type SessionPromptHistoryBound = 'maxSessions' | 'maxSessionBytes' | 'maxTotalBytes' | 'deadline' | 'aborted';
|
|
89
|
+
export interface SessionPromptHistoryStats {
|
|
90
|
+
/** Session files that matched the provider, id, and `since` filters. */
|
|
91
|
+
sessionsMatched: number;
|
|
92
|
+
sessionsScanned: number;
|
|
93
|
+
sessionsReturned: number;
|
|
94
|
+
/** Scanned sessions with no human prompt at all. */
|
|
95
|
+
sessionsWithoutPrompts: number;
|
|
96
|
+
/** Scanned sessions with human prompts but none inside the allowed roots. */
|
|
97
|
+
sessionsOutOfScope: number;
|
|
98
|
+
/** Codex sessions skipped as non-interactive (subagent, fork, unknown source). */
|
|
99
|
+
sessionsNotInteractive: number;
|
|
100
|
+
sessionsTruncated: number;
|
|
101
|
+
recordsOverSizeLimit: number;
|
|
102
|
+
recordsMalformed: number;
|
|
103
|
+
promptsMissingTimestamp: number;
|
|
104
|
+
promptsOutOfScope: number;
|
|
105
|
+
}
|
|
106
|
+
export interface SessionPromptHistoryResult {
|
|
107
|
+
/** Most recent activity first. */
|
|
108
|
+
sessions: PromptHistorySession[];
|
|
109
|
+
/**
|
|
110
|
+
* Sessions this call did not read, because a bound stopped it or the file
|
|
111
|
+
* changed while it was read; pass them as `sessionIds` to continue.
|
|
112
|
+
*/
|
|
113
|
+
unread: PromptHistorySessionRef[];
|
|
114
|
+
boundsHit: SessionPromptHistoryBound[];
|
|
115
|
+
/** Permanent exclusions found in this call (at most 1000; stats count all). */
|
|
116
|
+
exclusions: PromptHistoryExclusion[];
|
|
117
|
+
stats: SessionPromptHistoryStats;
|
|
118
|
+
}
|
|
119
|
+
/** Collect every human prompt of each matching session, grouped by session. */
|
|
120
|
+
export declare function collectSessionPromptHistory(options: CollectSessionPromptHistoryOptions): Promise<SessionPromptHistoryResult>;
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Node-only whole-session view of prompt history: every human prompt of a
|
|
4
|
+
* Claude Code or Codex session, grouped by session, for consumers that
|
|
5
|
+
* classify a session as a whole (intent, conflict, issues).
|
|
6
|
+
*
|
|
7
|
+
* Sessions are read from the start with the same scanners as
|
|
8
|
+
* `collectPromptHistory()`, so scope rules, prompt text, first-call model and
|
|
9
|
+
* usage, and ordinals are identical. The replies and interaction signals that
|
|
10
|
+
* the prompt filter leaves out are opt-in through `include`.
|
|
11
|
+
*
|
|
12
|
+
* There is no cursor: each call returns whole sessions. A bound that stops a
|
|
13
|
+
* call lists the sessions it did not read in `unread`; passing them back as
|
|
14
|
+
* `sessionIds` continues. Scope fails closed as in `collectPromptHistory()`,
|
|
15
|
+
* and replies or signals that belong to an out-of-scope prompt are dropped.
|
|
16
|
+
*/
|
|
17
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
18
|
+
if (k2 === undefined) k2 = k;
|
|
19
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
20
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
21
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
22
|
+
}
|
|
23
|
+
Object.defineProperty(o, k2, desc);
|
|
24
|
+
}) : (function(o, m, k, k2) {
|
|
25
|
+
if (k2 === undefined) k2 = k;
|
|
26
|
+
o[k2] = m[k];
|
|
27
|
+
}));
|
|
28
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
29
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
30
|
+
}) : function(o, v) {
|
|
31
|
+
o["default"] = v;
|
|
32
|
+
});
|
|
33
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
34
|
+
var ownKeys = function(o) {
|
|
35
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
36
|
+
var ar = [];
|
|
37
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
38
|
+
return ar;
|
|
39
|
+
};
|
|
40
|
+
return ownKeys(o);
|
|
41
|
+
};
|
|
42
|
+
return function (mod) {
|
|
43
|
+
if (mod && mod.__esModule) return mod;
|
|
44
|
+
var result = {};
|
|
45
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
46
|
+
__setModuleDefault(result, mod);
|
|
47
|
+
return result;
|
|
48
|
+
};
|
|
49
|
+
})();
|
|
50
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
51
|
+
exports.collectSessionPromptHistory = collectSessionPromptHistory;
|
|
52
|
+
const fs = __importStar(require("fs"));
|
|
53
|
+
const promptHistory_1 = require("./promptHistory");
|
|
54
|
+
const sessionPromptSignals_1 = require("./sessionPromptSignals");
|
|
55
|
+
const DEFAULT_SESSION_BOUNDS = {
|
|
56
|
+
maxSessions: 1000,
|
|
57
|
+
maxSessionBytes: 256 * 1024 * 1024,
|
|
58
|
+
maxTotalBytes: 256 * 1024 * 1024,
|
|
59
|
+
maxRecordBytes: promptHistory_1.DEFAULT_BOUNDS.maxRecordBytes,
|
|
60
|
+
deadlineMs: 60_000,
|
|
61
|
+
};
|
|
62
|
+
const MAX_EXCLUSIONS = 1000;
|
|
63
|
+
/** Collect every human prompt of each matching session, grouped by session. */
|
|
64
|
+
async function collectSessionPromptHistory(options) {
|
|
65
|
+
const bounds = { ...DEFAULT_SESSION_BOUNDS, ...definedBounds(options.bounds) };
|
|
66
|
+
const include = {
|
|
67
|
+
signals: options.include?.signals === true,
|
|
68
|
+
replies: options.include?.replies === true,
|
|
69
|
+
};
|
|
70
|
+
const startedAt = Date.now();
|
|
71
|
+
const sessions = [];
|
|
72
|
+
const unread = [];
|
|
73
|
+
const boundsHit = new Set();
|
|
74
|
+
const exclusions = [];
|
|
75
|
+
const scanStats = (0, promptHistory_1.emptyStats)();
|
|
76
|
+
const stats = {
|
|
77
|
+
sessionsMatched: 0,
|
|
78
|
+
sessionsScanned: 0,
|
|
79
|
+
sessionsReturned: 0,
|
|
80
|
+
sessionsWithoutPrompts: 0,
|
|
81
|
+
sessionsOutOfScope: 0,
|
|
82
|
+
sessionsNotInteractive: 0,
|
|
83
|
+
sessionsTruncated: 0,
|
|
84
|
+
recordsOverSizeLimit: 0,
|
|
85
|
+
recordsMalformed: 0,
|
|
86
|
+
promptsMissingTimestamp: 0,
|
|
87
|
+
promptsOutOfScope: 0,
|
|
88
|
+
};
|
|
89
|
+
const finish = () => {
|
|
90
|
+
stats.sessionsReturned = sessions.length;
|
|
91
|
+
stats.recordsOverSizeLimit = scanStats.recordsOverSizeLimit;
|
|
92
|
+
stats.recordsMalformed = scanStats.recordsMalformed;
|
|
93
|
+
stats.promptsMissingTimestamp = scanStats.promptsMissingTimestamp;
|
|
94
|
+
stats.promptsOutOfScope = scanStats.promptsOutOfScope;
|
|
95
|
+
return {
|
|
96
|
+
sessions: sessions.sort(compareSessions),
|
|
97
|
+
unread,
|
|
98
|
+
boundsHit: [...boundsHit],
|
|
99
|
+
exclusions,
|
|
100
|
+
stats,
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
const realpaths = new Map();
|
|
104
|
+
const { roots, listingRoots } = (0, promptHistory_1.resolveRoots)(options.workspacePaths, options.includeWorktrees !== false, realpaths);
|
|
105
|
+
if (roots.length === 0)
|
|
106
|
+
return finish();
|
|
107
|
+
const selected = await selectCandidates(options, listingRoots);
|
|
108
|
+
stats.sessionsMatched = selected.length;
|
|
109
|
+
const limit = typeof options.limit === 'number' && options.limit > 0 ? Math.floor(options.limit) : Infinity;
|
|
110
|
+
const shouldStop = () => {
|
|
111
|
+
if (options.signal?.aborted)
|
|
112
|
+
return 'aborted';
|
|
113
|
+
if (Date.now() - startedAt >= bounds.deadlineMs)
|
|
114
|
+
return 'deadline';
|
|
115
|
+
return null;
|
|
116
|
+
};
|
|
117
|
+
const refOf = ({ candidate }) => ({
|
|
118
|
+
provider: candidate.provider,
|
|
119
|
+
sessionId: candidate.sessionId,
|
|
120
|
+
});
|
|
121
|
+
let totalBytes = 0;
|
|
122
|
+
for (let index = 0; index < selected.length && sessions.length < limit; index++) {
|
|
123
|
+
const item = selected[index];
|
|
124
|
+
const stopAt = (bound, fromIndex) => {
|
|
125
|
+
boundsHit.add(bound);
|
|
126
|
+
unread.push(...selected.slice(fromIndex).map(refOf));
|
|
127
|
+
};
|
|
128
|
+
const stop = shouldStop();
|
|
129
|
+
if (stop) {
|
|
130
|
+
stopAt(stop, index);
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
if (stats.sessionsScanned >= bounds.maxSessions) {
|
|
134
|
+
stopAt('maxSessions', index);
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
const remainingTotal = bounds.maxTotalBytes - totalBytes;
|
|
138
|
+
if (stats.sessionsScanned > 0 && remainingTotal <= 0) {
|
|
139
|
+
stopAt('maxTotalBytes', index);
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
let excludedRecords = 0;
|
|
143
|
+
const extractor = (0, sessionPromptSignals_1.createSessionRecordExtractor)(item.candidate.provider, include);
|
|
144
|
+
const context = {
|
|
145
|
+
roots,
|
|
146
|
+
// Sessions come back whole: `since` only selects files.
|
|
147
|
+
sinceMs: null,
|
|
148
|
+
stats: scanStats,
|
|
149
|
+
realpaths,
|
|
150
|
+
bounds: {
|
|
151
|
+
...promptHistory_1.DEFAULT_BOUNDS,
|
|
152
|
+
maxRecordBytes: bounds.maxRecordBytes,
|
|
153
|
+
maxEntries: Number.MAX_SAFE_INTEGER,
|
|
154
|
+
},
|
|
155
|
+
shouldStop,
|
|
156
|
+
entriesSoFar: () => 0,
|
|
157
|
+
exclude: (exclusion) => {
|
|
158
|
+
excludedRecords++;
|
|
159
|
+
if (exclusions.length < MAX_EXCLUSIONS)
|
|
160
|
+
exclusions.push(exclusion);
|
|
161
|
+
},
|
|
162
|
+
observe: (record, info) => extractor.observe(record, info),
|
|
163
|
+
};
|
|
164
|
+
const sessionLimited = bounds.maxSessionBytes <= remainingTotal;
|
|
165
|
+
stats.sessionsScanned++;
|
|
166
|
+
let outcome;
|
|
167
|
+
try {
|
|
168
|
+
outcome = await (0, promptHistory_1.scanFile)(item.candidate, item.stat, undefined, {
|
|
169
|
+
bytes: sessionLimited ? bounds.maxSessionBytes : Math.max(0, remainingTotal),
|
|
170
|
+
bound: sessionLimited ? 'maxFileBytes' : 'maxTotalBytes',
|
|
171
|
+
forceProgress: true,
|
|
172
|
+
}, context);
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
// Changed while opening or verifying it: a later call can read it again.
|
|
176
|
+
unread.push(refOf(item));
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
totalBytes += outcome.bytesRead;
|
|
180
|
+
if (outcome.notInteractive) {
|
|
181
|
+
stats.sessionsNotInteractive++;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
const stoppedBy = outcome.stoppedBy;
|
|
185
|
+
if (stoppedBy === 'deadline' || stoppedBy === 'aborted' || stoppedBy === 'maxTotalBytes') {
|
|
186
|
+
// A partial read cannot be resumed without a cursor: read it whole next call.
|
|
187
|
+
stopAt(stoppedBy, index);
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
if (!outcome.session || (outcome.session.backlog && stoppedBy === null)) {
|
|
191
|
+
// Truncated or rewritten while it was being read.
|
|
192
|
+
unread.push(refOf(item));
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (outcome.inScope === 0) {
|
|
196
|
+
if (outcome.outOfScope > 0)
|
|
197
|
+
stats.sessionsOutOfScope++;
|
|
198
|
+
else
|
|
199
|
+
stats.sessionsWithoutPrompts++;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
const truncated = stoppedBy === 'maxFileBytes';
|
|
203
|
+
if (truncated) {
|
|
204
|
+
boundsHit.add('maxSessionBytes');
|
|
205
|
+
stats.sessionsTruncated++;
|
|
206
|
+
}
|
|
207
|
+
sessions.push(buildSession(item.candidate, outcome.entries, outcome.session.lastOrdinal, extractor.result(), include, { truncated, excludedRecords }));
|
|
208
|
+
}
|
|
209
|
+
return finish();
|
|
210
|
+
}
|
|
211
|
+
function definedBounds(bounds) {
|
|
212
|
+
const result = {};
|
|
213
|
+
if (!bounds)
|
|
214
|
+
return result;
|
|
215
|
+
for (const key of Object.keys(DEFAULT_SESSION_BOUNDS)) {
|
|
216
|
+
const value = bounds[key];
|
|
217
|
+
if (typeof value === 'number' && !Number.isNaN(value))
|
|
218
|
+
result[key] = Math.max(0, value);
|
|
219
|
+
}
|
|
220
|
+
return result;
|
|
221
|
+
}
|
|
222
|
+
/** Matching session files, most recently modified first. */
|
|
223
|
+
async function selectCandidates(options, listingRoots) {
|
|
224
|
+
const sinceMs = options.since ? options.since.getTime() : null;
|
|
225
|
+
const providers = options.providers ?? ['claude-code', 'codex'];
|
|
226
|
+
let candidates = await (0, promptHistory_1.listCandidates)(providers, listingRoots, sinceMs);
|
|
227
|
+
if (options.sessionIds) {
|
|
228
|
+
const wanted = new Set(options.sessionIds);
|
|
229
|
+
candidates = candidates.filter((candidate) => wanted.has(candidate.sessionId));
|
|
230
|
+
}
|
|
231
|
+
const selected = [];
|
|
232
|
+
for (const candidate of candidates) {
|
|
233
|
+
try {
|
|
234
|
+
selected.push({ candidate, stat: await fs.promises.stat(candidate.filePath) });
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
// Gone since it was listed.
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return selected.sort((left, right) => right.stat.mtimeMs - left.stat.mtimeMs ||
|
|
241
|
+
(0, promptHistory_1.compareStrings)(left.candidate.key, right.candidate.key));
|
|
242
|
+
}
|
|
243
|
+
function buildSession(candidate, entries, lastOrdinal, extraction, include, read) {
|
|
244
|
+
const ordered = [...entries].sort((left, right) => left.ordinal - right.ordinal);
|
|
245
|
+
const returned = new Set(ordered.map((entry) => entry.ordinal));
|
|
246
|
+
const prompts = ordered.map(({ provider: _provider, sessionId: _sessionId, ...prompt }) => {
|
|
247
|
+
const reply = include.replies ? extraction.replies.get(prompt.ordinal) : undefined;
|
|
248
|
+
return reply ? { ...prompt, reply } : prompt;
|
|
249
|
+
});
|
|
250
|
+
const droppedPrompts = Math.max(0, lastOrdinal + 1 - ordered.length);
|
|
251
|
+
const session = {
|
|
252
|
+
provider: candidate.provider,
|
|
253
|
+
sessionId: candidate.sessionId,
|
|
254
|
+
startedAt: extraction.firstTimestamp ?? ordered[0].timestamp,
|
|
255
|
+
lastActivityAt: extraction.lastTimestamp ?? ordered[ordered.length - 1].timestamp,
|
|
256
|
+
cwds: distinct(ordered.map((entry) => entry.cwd)),
|
|
257
|
+
gitBranches: distinct(ordered.map((entry) => entry.gitBranch)),
|
|
258
|
+
models: distinct(ordered.map((entry) => entry.model)),
|
|
259
|
+
prompts,
|
|
260
|
+
complete: !read.truncated && droppedPrompts === 0 && read.excludedRecords === 0,
|
|
261
|
+
truncated: read.truncated,
|
|
262
|
+
droppedPrompts,
|
|
263
|
+
excludedRecords: read.excludedRecords,
|
|
264
|
+
};
|
|
265
|
+
if (include.signals) {
|
|
266
|
+
// Fail closed: nothing that followed an out-of-scope prompt is returned.
|
|
267
|
+
session.signals = extraction.signals.filter((signal) => signal.afterOrdinal === -1 || returned.has(signal.afterOrdinal));
|
|
268
|
+
}
|
|
269
|
+
return session;
|
|
270
|
+
}
|
|
271
|
+
function distinct(values) {
|
|
272
|
+
return [...new Set(values.filter((value) => typeof value === 'string'))];
|
|
273
|
+
}
|
|
274
|
+
function compareSessions(left, right) {
|
|
275
|
+
return (Date.parse(right.lastActivityAt) - Date.parse(left.lastActivityAt) ||
|
|
276
|
+
(0, promptHistory_1.compareStrings)(left.provider, right.provider) ||
|
|
277
|
+
(0, promptHistory_1.compareStrings)(left.sessionId, right.sessionId));
|
|
278
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the human-prompt filter leaves out of a session, for consumers that
|
|
3
|
+
* classify a whole session: the agent's reply to each prompt and timestamped
|
|
4
|
+
* interaction signals (interrupts, rejected or failed tools, compactions, API
|
|
5
|
+
* errors, rollbacks). Pure: log records in, results out; no file system access.
|
|
6
|
+
*
|
|
7
|
+
* Records reach an extractor after the prompt scanner has seen them, with the
|
|
8
|
+
* ordinal of the latest human prompt, so replies and signals share the prompt
|
|
9
|
+
* ordinals of `collectPromptHistory()`.
|
|
10
|
+
*/
|
|
11
|
+
import type { PromptHistoryProvider } from './promptHistory';
|
|
12
|
+
export type PromptHistorySignalKind = 'interrupt' | 'toolRejected' | 'toolError' | 'compaction' | 'apiError' | 'rollback';
|
|
13
|
+
export interface PromptHistorySignal {
|
|
14
|
+
kind: PromptHistorySignalKind;
|
|
15
|
+
/** ISO timestamp from the log line; never synthesized. */
|
|
16
|
+
timestamp: string;
|
|
17
|
+
/** Ordinal of the human prompt this followed, or -1 before the first prompt. */
|
|
18
|
+
afterOrdinal: number;
|
|
19
|
+
/** The tool involved, for `toolRejected` and `toolError`, when the log names it. */
|
|
20
|
+
tool?: string;
|
|
21
|
+
/**
|
|
22
|
+
* `toolRejected`: the reason the person typed, untruncated. `toolError` and
|
|
23
|
+
* `apiError`: the error text, cut to 1024 characters (`textTruncated`).
|
|
24
|
+
*/
|
|
25
|
+
text?: string;
|
|
26
|
+
textTruncated?: true;
|
|
27
|
+
}
|
|
28
|
+
export interface PromptHistoryReply {
|
|
29
|
+
/** The agent's last text for the prompt's turn, untruncated. */
|
|
30
|
+
text: string;
|
|
31
|
+
/** ISO timestamp of the log line that carried the reply. */
|
|
32
|
+
timestamp: string;
|
|
33
|
+
}
|
|
34
|
+
export interface SessionRecordInclude {
|
|
35
|
+
signals: boolean;
|
|
36
|
+
replies: boolean;
|
|
37
|
+
}
|
|
38
|
+
export interface SessionRecordExtraction {
|
|
39
|
+
/** Earliest timestamp on any record. */
|
|
40
|
+
firstTimestamp?: string;
|
|
41
|
+
/** Latest timestamp on any record. */
|
|
42
|
+
lastTimestamp?: string;
|
|
43
|
+
signals: PromptHistorySignal[];
|
|
44
|
+
/** Keyed by prompt ordinal. */
|
|
45
|
+
replies: Map<number, PromptHistoryReply>;
|
|
46
|
+
}
|
|
47
|
+
export interface SessionRecordExtractor {
|
|
48
|
+
observe(record: Record<string, unknown>, info: {
|
|
49
|
+
ordinal: number;
|
|
50
|
+
lineOffset: number;
|
|
51
|
+
}): void;
|
|
52
|
+
result(): SessionRecordExtraction;
|
|
53
|
+
}
|
|
54
|
+
/** Error text kept on a signal; tool output can be arbitrarily long. */
|
|
55
|
+
export declare const SIGNAL_TEXT_CHARS = 1024;
|
|
56
|
+
export declare function createSessionRecordExtractor(provider: PromptHistoryProvider, include: SessionRecordInclude): SessionRecordExtractor;
|