@ngockhoale/ukit 3.3.2 → 3.4.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/CHANGELOG.md +44 -0
- package/manifests/engineConformance.yaml +17 -1
- package/manifests/hostCapabilities.yaml +68 -1
- package/manifests/platform.full.yaml +138 -0
- package/manifests/platform.user.yaml +255 -3
- package/package.json +1 -1
- package/scripts/bench/subagent-orchestrator-corpus.mjs +275 -0
- package/scripts/bench/subagent-orchestrator-eval.mjs +565 -0
- package/scripts/probe/codex-capability-probe.mjs +169 -0
- package/src/cli/commands/doctor.js +168 -0
- package/src/cli/commands/indexTools.js +7 -0
- package/src/cli/commands/metrics.js +66 -2
- package/src/cli/commands/playbook.js +4 -4
- package/src/cli/commands/vm.js +49 -8
- package/src/core/agentRuntime/adapters.js +328 -27
- package/src/core/agentRuntime/artifacts.js +89 -0
- package/src/core/agentRuntime/context.js +345 -1
- package/src/core/agentRuntime/contract.js +296 -0
- package/src/core/agentRuntime/eventStore.js +176 -0
- package/src/core/agentRuntime/shadowRun.js +481 -5
- package/src/core/agentRuntime/telemetry.js +121 -0
- package/src/core/observability/emit/lifecycle.js +68 -1
- package/src/core/observability/emit/sessionBoot.js +393 -0
- package/src/core/observability/privacy/allowlist.js +10 -1
- package/src/core/observability/schema/registry.js +10 -0
- package/src/core/runtimeConfig.js +133 -0
- package/src/core/userPlaybooks.js +18 -3
- package/src/decision/registry.js +19 -0
- package/src/diagnostics/feedbackEvents.js +7 -4
- package/src/diagnostics/routeOutcomes.js +51 -6
- package/src/diagnostics/skillAccuracy.js +43 -3
- package/src/index/crossCheckMatrix.js +412 -0
- package/src/index/fixLoopEscalation.js +453 -0
- package/src/index/playbookRegistry.js +691 -0
- package/src/index/reviewPolicy.js +368 -0
- package/src/index/routeResolver.js +915 -0
- package/src/index/sessionHistoryExtractor.js +359 -0
- package/src/index/taskRouting.js +764 -581
- package/src/index/tierSelection.js +308 -0
- package/src/index/verificationMap.js +404 -0
- package/template_project/.claude/hooks/observability-emit.mjs +14 -0
- package/template_project/.claude/hooks/record-execution.mjs +19 -1
- package/template_project/.claude/hooks/skill-router.sh +691 -25
- package/template_project/.claude/hooks/verification-guard.sh +230 -1
- package/template_project/.claude/settings.json +2 -2
- package/template_project/.claude/ukit/index/cross-check-matrix.mjs +415 -0
- package/template_project/.claude/ukit/index/fix-loop-escalation.mjs +456 -0
- package/template_project/.claude/ukit/index/playbook-registry.mjs +690 -0
- package/template_project/.claude/ukit/index/review-panel-aggregate.mjs +20 -2
- package/template_project/.claude/ukit/index/review-policy.mjs +376 -0
- package/template_project/.claude/ukit/index/route-resolver.mjs +1059 -0
- package/template_project/.claude/ukit/index/route-task.mjs +1253 -846
- package/template_project/.claude/ukit/index/session-history-extractor.mjs +362 -0
- package/template_project/.claude/ukit/index/tier-selection.mjs +309 -0
- package/template_project/.claude/ukit/index/verification-map.mjs +403 -0
- package/template_project/.claude/ukit/index/worktree-sweep.mjs +195 -0
- package/template_project/.claude/ukit/runtime/execution-ledger.mjs +789 -11
- package/template_project/.claude/ukit/runtime/observability-emit.mjs +1102 -0
- package/template_project/.claude/ukit/runtime/reinject-context.mjs +9 -1
- package/template_project/.claude/ukit/runtime/resumable-run.mjs +149 -5
- package/template_project/.claude/ukit/runtime/stop-coordinator.mjs +323 -6
- package/template_project/.codex/README.md +8 -0
- package/template_project/.omp/hooks/pre/ukit-bridge.js +8 -1
- package/template_project/ukit/README.md +1 -1
- package/template_project/ukit/storage/config.json +20 -0
- package/template_user/playbooks/architecture-decision.md +28 -0
- package/template_user/playbooks/autonomous-run.md +43 -0
- package/template_user/playbooks/autopilot-full.md +59 -0
- package/template_user/playbooks/autopilot-stack.md +54 -0
- package/template_user/playbooks/babysit.md +39 -0
- package/template_user/playbooks/bug-fix.md +3 -1
- package/template_user/playbooks/{issue-implementation.md → feature-implementation.md} +4 -2
- package/template_user/playbooks/hillclimb.md +44 -0
- package/template_user/playbooks/investigation.md +21 -0
- package/template_user/playbooks/migration.md +21 -0
- package/template_user/playbooks/open-pr.md +48 -0
- package/template_user/playbooks/orchestrate.md +45 -0
- package/template_user/playbooks/performance.md +33 -0
- package/template_user/playbooks/prototype.md +28 -0
- package/template_user/playbooks/refactor.md +19 -0
- package/template_user/playbooks/release.md +28 -0
- package/template_user/playbooks/runtime-forensics.md +23 -0
- package/template_user/playbooks/session-pickup.md +31 -0
- package/template_user/playbooks/shipping.md +53 -0
- package/template_user/playbooks/skill-evaluation.md +48 -0
- package/template_user/playbooks/small-feature.md +20 -0
- package/template_user/playbooks/verification-map.json +153 -0
- package/template_user/playbooks/verification.md +22 -0
- package/template_user/playbooks/worktree-cleanup.md +37 -0
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
// sessionHistoryExtractor.js — bounded session-history signal extractor (BL-013).
|
|
2
|
+
//
|
|
3
|
+
// Reads the transcript tail (<=16KiB by default) carried on hook/bridge payloads
|
|
4
|
+
// (`transcriptPath`, ukit-bridge.js:217,228) and emits bounded counters/enums:
|
|
5
|
+
//
|
|
6
|
+
// { priorAttemptCount, sameSymptomReask, correctionEvents, fixLoopCount,
|
|
7
|
+
// degraded, reason? }
|
|
8
|
+
//
|
|
9
|
+
// Detection is deliberately heuristic + bounded (SPEC §14):
|
|
10
|
+
// * Route markers — any tail entry carrying `requestKey`/`routeFingerprint`
|
|
11
|
+
// (top-level or nested under route/ukit/metadata/message) is a prior route
|
|
12
|
+
// record; markers matching the current requestKey OR routeFingerprint count
|
|
13
|
+
// as prior attempts, and a failure-valued outcome counts a fix-loop round.
|
|
14
|
+
// * Re-ask — a user entry after the last matched marker (or >=2 matched
|
|
15
|
+
// markers) means the same route was asked again.
|
|
16
|
+
// * Corrections — explicit correction markers (`ukit-correction`/`correction`
|
|
17
|
+
// kinds) plus bounded correction phrasing in user text.
|
|
18
|
+
//
|
|
19
|
+
// Contracts (SPEC FR-001 + §10):
|
|
20
|
+
// * NEVER THROWS. Missing/unreadable transcript → zeroed struct with
|
|
21
|
+
// degraded:true + reason; malformed lines are skipped, never fatal.
|
|
22
|
+
// * BOUNDED: tail-only read (<=maxBytes, hard ceiling 4MiB like
|
|
23
|
+
// transcript-tail.mjs), counters clamp at 99, ≤50ms wall budget.
|
|
24
|
+
// * PRIVACY: output is counters/enums only — no transcript text may ever
|
|
25
|
+
// reach route state, decisions.tsv, or any receipt through this module.
|
|
26
|
+
//
|
|
27
|
+
// Two entry points share one classifier: extractHistorySignals (sync) for
|
|
28
|
+
// callers outside deadline-guarded hook blocks, extractHistorySignalsAsync for
|
|
29
|
+
// the hook path where every fs call must be async (BUG-C21-04 posture — a
|
|
30
|
+
// stalled mount must not park the event loop under an armed deadline).
|
|
31
|
+
|
|
32
|
+
import fs from 'node:fs';
|
|
33
|
+
import fsp from 'node:fs/promises';
|
|
34
|
+
|
|
35
|
+
// SPEC §14: the history extraction bound is a 16KiB tail. The ceiling mirrors
|
|
36
|
+
// transcript-tail.mjs's hard cap so one number describes a UKit tail scan.
|
|
37
|
+
export const DEFAULT_HISTORY_MAX_BYTES = 16 * 1024;
|
|
38
|
+
const HISTORY_MAX_BYTES_CEILING = 4 * 1024 * 1024;
|
|
39
|
+
|
|
40
|
+
const COUNTER_MAX = 99;
|
|
41
|
+
|
|
42
|
+
// Outcome fields probed on markers, in priority order. A marker without an
|
|
43
|
+
// outcome field counts as an attempt but never as a failed round.
|
|
44
|
+
const OUTCOME_FIELDS = ['outcome', 'verdict', 'status', 'result'];
|
|
45
|
+
const FAILURE_PATTERN = /fail|error|timeout|abort|crash|block/i;
|
|
46
|
+
|
|
47
|
+
// Object containers probed for marker keys — top-level plus the bounded nests
|
|
48
|
+
// writers may use. `message` is included so stamped user-prompt entries
|
|
49
|
+
// ({type:'user', message:{content, ukit:{requestKey}}}) still match.
|
|
50
|
+
const MARKER_PROBE_KEYS = ['ukit', 'route', 'routeRecord', 'routeMeta', 'metadata', 'meta', 'message'];
|
|
51
|
+
|
|
52
|
+
const USER_ENTRY_TYPES = new Set(['user', 'human']);
|
|
53
|
+
const CORRECTION_TYPES = new Set(['ukit-correction', 'correction', 'user-correction']);
|
|
54
|
+
|
|
55
|
+
// Bounded correction phrasing — explicit pushback patterns only; frustration
|
|
56
|
+
// noise stays out of the counter by design (GAP M12 failure-mode note).
|
|
57
|
+
const CORRECTION_PATTERN = /\b(?:no[,.!]?\s+that'?s|that'?s not|not what i|wrong|you (?:said|did|were|meant)|still (?:fail|failing|broken|wrong|crash|not working)|didn'?t work|incorrect|try again)\b/i;
|
|
58
|
+
|
|
59
|
+
// User message text is scanned only this far — long pasted diffs/logs in a
|
|
60
|
+
// user entry never extend the classification surface.
|
|
61
|
+
const USER_TEXT_SCAN_CHARS = 4096;
|
|
62
|
+
|
|
63
|
+
function isPlainObject(value) {
|
|
64
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function nonEmptyString(value) {
|
|
68
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function boundedCap(maxBytes) {
|
|
72
|
+
const requested = Number.isFinite(maxBytes) && maxBytes > 0
|
|
73
|
+
? Math.floor(maxBytes)
|
|
74
|
+
: DEFAULT_HISTORY_MAX_BYTES;
|
|
75
|
+
return Math.min(requested, HISTORY_MAX_BYTES_CEILING);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function degradedResult(reason) {
|
|
79
|
+
return {
|
|
80
|
+
priorAttemptCount: 0,
|
|
81
|
+
sameSymptomReask: false,
|
|
82
|
+
correctionEvents: 0,
|
|
83
|
+
fixLoopCount: 0,
|
|
84
|
+
degraded: true,
|
|
85
|
+
reason,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function zeroedResult() {
|
|
90
|
+
return {
|
|
91
|
+
priorAttemptCount: 0,
|
|
92
|
+
sameSymptomReask: false,
|
|
93
|
+
correctionEvents: 0,
|
|
94
|
+
fixLoopCount: 0,
|
|
95
|
+
degraded: false,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function clampCounter(value) {
|
|
100
|
+
const count = Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
|
|
101
|
+
return Math.min(count, COUNTER_MAX);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// --- bounded tail read -------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
// A tail read almost certainly starts mid-record: drop everything before the
|
|
107
|
+
// first complete line. Malformed or non-object lines are skipped — consumers
|
|
108
|
+
// count only well-formed bounded events.
|
|
109
|
+
function tailTextToEntries(text, truncated) {
|
|
110
|
+
let payload = text;
|
|
111
|
+
if (truncated) {
|
|
112
|
+
const firstBreak = payload.indexOf('\n');
|
|
113
|
+
payload = firstBreak >= 0 ? payload.slice(firstBreak + 1) : '';
|
|
114
|
+
}
|
|
115
|
+
const entries = [];
|
|
116
|
+
for (const line of payload.split('\n')) {
|
|
117
|
+
const trimmed = line.trim();
|
|
118
|
+
if (!trimmed) continue;
|
|
119
|
+
try {
|
|
120
|
+
const parsed = JSON.parse(trimmed);
|
|
121
|
+
if (isPlainObject(parsed)) entries.push(parsed);
|
|
122
|
+
} catch {
|
|
123
|
+
// partial/corrupt line — skipped, never fatal
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return entries;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function readTailEntriesAsync(filePath, cap) {
|
|
130
|
+
const stat = await fsp.stat(filePath);
|
|
131
|
+
if (!stat.isFile()) {
|
|
132
|
+
throw new Error('not-a-file');
|
|
133
|
+
}
|
|
134
|
+
if (stat.size <= 0) {
|
|
135
|
+
return { entries: [], bytesRead: 0 };
|
|
136
|
+
}
|
|
137
|
+
const start = Math.max(0, stat.size - cap);
|
|
138
|
+
const length = Math.min(cap, stat.size);
|
|
139
|
+
const buffer = Buffer.allocUnsafe(length);
|
|
140
|
+
const handle = await fsp.open(filePath, 'r');
|
|
141
|
+
let bytesRead = 0;
|
|
142
|
+
try {
|
|
143
|
+
const result = await handle.read(buffer, 0, length, start);
|
|
144
|
+
bytesRead = result.bytesRead;
|
|
145
|
+
} finally {
|
|
146
|
+
try { await handle.close(); } catch {}
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
entries: tailTextToEntries(buffer.toString('utf8', 0, bytesRead), start > 0),
|
|
150
|
+
bytesRead,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function readTailEntriesSync(filePath, cap) {
|
|
155
|
+
const stat = fs.statSync(filePath);
|
|
156
|
+
if (!stat.isFile()) {
|
|
157
|
+
throw new Error('not-a-file');
|
|
158
|
+
}
|
|
159
|
+
if (stat.size <= 0) {
|
|
160
|
+
return { entries: [], bytesRead: 0 };
|
|
161
|
+
}
|
|
162
|
+
const start = Math.max(0, stat.size - cap);
|
|
163
|
+
const length = Math.min(cap, stat.size);
|
|
164
|
+
const buffer = Buffer.allocUnsafe(length);
|
|
165
|
+
const fd = fs.openSync(filePath, 'r');
|
|
166
|
+
let bytesRead = 0;
|
|
167
|
+
try {
|
|
168
|
+
bytesRead = fs.readSync(fd, buffer, 0, length, start);
|
|
169
|
+
} finally {
|
|
170
|
+
try { fs.closeSync(fd); } catch {}
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
entries: tailTextToEntries(buffer.toString('utf8', 0, bytesRead), start > 0),
|
|
174
|
+
bytesRead,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// --- bounded classification --------------------------------------------------
|
|
179
|
+
|
|
180
|
+
// Probe order for marker keys + outcomes: the entry itself first, then the
|
|
181
|
+
// bounded nests. Returns the first non-empty string found.
|
|
182
|
+
function probeField(entry, names) {
|
|
183
|
+
if (!isPlainObject(entry)) return null;
|
|
184
|
+
for (const name of names) {
|
|
185
|
+
const direct = nonEmptyString(entry[name]);
|
|
186
|
+
if (direct) return direct;
|
|
187
|
+
}
|
|
188
|
+
for (const nestKey of MARKER_PROBE_KEYS) {
|
|
189
|
+
const nested = entry[nestKey];
|
|
190
|
+
if (!isPlainObject(nested)) continue;
|
|
191
|
+
for (const name of names) {
|
|
192
|
+
const value = nonEmptyString(nested[name]);
|
|
193
|
+
if (value) return value;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function markerKeysOf(entry) {
|
|
200
|
+
const requestKey = probeField(entry, ['requestKey']);
|
|
201
|
+
const routeFingerprint = probeField(entry, ['routeFingerprint', 'fingerprint']);
|
|
202
|
+
return requestKey || routeFingerprint ? { requestKey, routeFingerprint } : null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function markerOutcome(entry) {
|
|
206
|
+
return probeField(entry, OUTCOME_FIELDS);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function isFailedOutcome(outcome) {
|
|
210
|
+
return typeof outcome === 'string' && FAILURE_PATTERN.test(outcome);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function isCorrectionMarker(entry) {
|
|
214
|
+
if (!isPlainObject(entry)) return false;
|
|
215
|
+
if (CORRECTION_TYPES.has(entry.type)) return true;
|
|
216
|
+
if (entry.event === 'correction' || entry.kind === 'correction') return true;
|
|
217
|
+
if (isPlainObject(entry.event) && entry.event.kind === 'correction') return true;
|
|
218
|
+
if (isPlainObject(entry.ukit) && entry.ukit.kind === 'correction') return true;
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function isUserEntry(entry) {
|
|
223
|
+
if (!isPlainObject(entry)) return false;
|
|
224
|
+
if (USER_ENTRY_TYPES.has(entry.type)) return true;
|
|
225
|
+
if (isPlainObject(entry.message) && entry.message.role === 'user') return true;
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Bounded user text: string content, or the text blocks of a content array.
|
|
230
|
+
// Tool-result/thinking blocks are ignored — only user-authored text feeds the
|
|
231
|
+
// correction heuristic.
|
|
232
|
+
function userTextOf(entry) {
|
|
233
|
+
if (!isUserEntry(entry)) return null;
|
|
234
|
+
const content = entry.message?.content ?? entry.text ?? entry.content;
|
|
235
|
+
let text = '';
|
|
236
|
+
if (typeof content === 'string') {
|
|
237
|
+
text = content;
|
|
238
|
+
} else if (Array.isArray(content)) {
|
|
239
|
+
text = content
|
|
240
|
+
.map((block) => (isPlainObject(block) && block.type === 'text' ? String(block.text ?? '') : ''))
|
|
241
|
+
.filter(Boolean)
|
|
242
|
+
.join('\n');
|
|
243
|
+
}
|
|
244
|
+
return text ? text.slice(0, USER_TEXT_SCAN_CHARS) : null;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function computeHistorySignals(entries, { routeFingerprint = null, requestKey = null } = {}) {
|
|
248
|
+
const result = zeroedResult();
|
|
249
|
+
let attempts = 0;
|
|
250
|
+
let failedAttempts = 0;
|
|
251
|
+
let corrections = 0;
|
|
252
|
+
let lastMatchedIndex = -1;
|
|
253
|
+
let userAfterLastMatch = false;
|
|
254
|
+
|
|
255
|
+
entries.forEach((entry, index) => {
|
|
256
|
+
const marker = markerKeysOf(entry);
|
|
257
|
+
if (marker) {
|
|
258
|
+
const matches = Boolean(
|
|
259
|
+
(marker.requestKey && requestKey && marker.requestKey === requestKey)
|
|
260
|
+
|| (marker.routeFingerprint && routeFingerprint && marker.routeFingerprint === routeFingerprint),
|
|
261
|
+
);
|
|
262
|
+
if (matches) {
|
|
263
|
+
attempts += 1;
|
|
264
|
+
lastMatchedIndex = index;
|
|
265
|
+
if (isFailedOutcome(markerOutcome(entry))) {
|
|
266
|
+
failedAttempts += 1;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const userText = userTextOf(entry);
|
|
272
|
+
if (isCorrectionMarker(entry) || (userText !== null && CORRECTION_PATTERN.test(userText))) {
|
|
273
|
+
corrections += 1;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (lastMatchedIndex >= 0 && index > lastMatchedIndex && userText !== null) {
|
|
277
|
+
userAfterLastMatch = true;
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
result.priorAttemptCount = clampCounter(attempts);
|
|
282
|
+
result.fixLoopCount = clampCounter(failedAttempts);
|
|
283
|
+
result.correctionEvents = clampCounter(corrections);
|
|
284
|
+
// A re-ask on the same route shape: either the route resolved more than once
|
|
285
|
+
// in the tail, or a user prompt followed the last matched attempt (the
|
|
286
|
+
// current prompt is appended before the hook runs).
|
|
287
|
+
result.sameSymptomReask = attempts > 0 && (attempts >= 2 || userAfterLastMatch);
|
|
288
|
+
return result;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// --- public contract ---------------------------------------------------------
|
|
292
|
+
|
|
293
|
+
// Coerce arbitrary input into the historySignals route-field shape. Non-object
|
|
294
|
+
// input yields null so callers can emit the additive nullable field honestly.
|
|
295
|
+
export function normalizeHistorySignals(value) {
|
|
296
|
+
if (!isPlainObject(value)) return null;
|
|
297
|
+
const normalized = {
|
|
298
|
+
priorAttemptCount: clampCounter(value.priorAttemptCount),
|
|
299
|
+
sameSymptomReask: Boolean(value.sameSymptomReask),
|
|
300
|
+
correctionEvents: clampCounter(value.correctionEvents),
|
|
301
|
+
fixLoopCount: clampCounter(value.fixLoopCount),
|
|
302
|
+
degraded: Boolean(value.degraded),
|
|
303
|
+
};
|
|
304
|
+
const reason = nonEmptyString(value.reason);
|
|
305
|
+
if (reason) normalized.reason = reason.slice(0, 120);
|
|
306
|
+
return normalized;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// SPEC §8: extractHistorySignals({transcriptPath, routeFingerprint, requestKey,
|
|
310
|
+
// maxBytes?}) → bounded counters, never throws. Sync reader — the ≤50ms wall
|
|
311
|
+
// budget assumes a healthy filesystem; deadline-guarded hook code must use the
|
|
312
|
+
// async twin so a stalled mount cannot park the event loop.
|
|
313
|
+
export function extractHistorySignals({
|
|
314
|
+
transcriptPath,
|
|
315
|
+
routeFingerprint = null,
|
|
316
|
+
requestKey = null,
|
|
317
|
+
maxBytes,
|
|
318
|
+
} = {}) {
|
|
319
|
+
try {
|
|
320
|
+
const normalizedPath = nonEmptyString(transcriptPath);
|
|
321
|
+
if (!normalizedPath) {
|
|
322
|
+
return degradedResult('missing-transcript-path');
|
|
323
|
+
}
|
|
324
|
+
let tail;
|
|
325
|
+
try {
|
|
326
|
+
tail = readTailEntriesSync(normalizedPath, boundedCap(maxBytes));
|
|
327
|
+
} catch {
|
|
328
|
+
return degradedResult('transcript-unreadable');
|
|
329
|
+
}
|
|
330
|
+
return computeHistorySignals(tail.entries, { routeFingerprint, requestKey });
|
|
331
|
+
} catch {
|
|
332
|
+
return degradedResult('extract-failed');
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Async twin of extractHistorySignals — identical classification, fsp-based
|
|
337
|
+
// bounded read for deadline-guarded hook code (skill-router.sh).
|
|
338
|
+
export async function extractHistorySignalsAsync({
|
|
339
|
+
transcriptPath,
|
|
340
|
+
routeFingerprint = null,
|
|
341
|
+
requestKey = null,
|
|
342
|
+
maxBytes,
|
|
343
|
+
} = {}) {
|
|
344
|
+
try {
|
|
345
|
+
const normalizedPath = nonEmptyString(transcriptPath);
|
|
346
|
+
if (!normalizedPath) {
|
|
347
|
+
return degradedResult('missing-transcript-path');
|
|
348
|
+
}
|
|
349
|
+
let tail;
|
|
350
|
+
try {
|
|
351
|
+
tail = await readTailEntriesAsync(normalizedPath, boundedCap(maxBytes));
|
|
352
|
+
} catch {
|
|
353
|
+
return degradedResult('transcript-unreadable');
|
|
354
|
+
}
|
|
355
|
+
return computeHistorySignals(tail.entries, { routeFingerprint, requestKey });
|
|
356
|
+
} catch {
|
|
357
|
+
return degradedResult('extract-failed');
|
|
358
|
+
}
|
|
359
|
+
}
|