pi-pier 0.3.0 → 0.3.1
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 +12 -11
- package/package.json +2 -2
- package/schemas/role-manifest.schema.json +7 -1
- package/skills/pi-herdr-workflow/SKILL.md +4 -5
- package/src/ansi-text.ts +62 -11
- package/src/ask-dialog.ts +308 -0
- package/src/ask-user.ts +113 -112
- package/src/bootstrap.ts +8 -12
- package/src/compact-coordinator.ts +278 -261
- package/src/compact-economics-core.ts +77 -145
- package/src/config-catalog-core.ts +77 -197
- package/src/config-command.ts +271 -64
- package/src/dashboard-command.ts +38 -19
- package/src/efficiency-config-core.ts +287 -484
- package/src/efficiency-store.ts +16 -29
- package/src/focus-poller.ts +14 -35
- package/src/gc-core.ts +10 -36
- package/src/herdr-client.ts +82 -208
- package/src/history-store.ts +36 -23
- package/src/index-gates.ts +124 -0
- package/src/index-locks.ts +2 -7
- package/src/index-master.ts +24 -18
- package/src/index-notices.ts +28 -15
- package/src/index-pipe.ts +17 -12
- package/src/index-roles.ts +306 -0
- package/src/index-runtime.ts +2 -6
- package/src/index-worker.ts +4 -8
- package/src/index.ts +305 -564
- package/src/jev-client.ts +12 -17
- package/src/jev-core.ts +137 -87
- package/src/ledger.ts +13 -21
- package/src/lock-core.ts +28 -56
- package/src/manifest-compose.ts +35 -20
- package/src/observation-core.ts +52 -100
- package/src/pane-title.ts +19 -38
- package/src/pi-surface.ts +34 -22
- package/src/pier-options.ts +54 -50
- package/src/pipe-channel.ts +27 -59
- package/src/platform-paths.ts +19 -52
- package/src/{core → plugins}/grid-shape.ts +8 -10
- package/src/{core → plugins}/heat-plan.ts +20 -22
- package/src/plugins/observation.ts +475 -0
- package/src/plugins/subagent.ts +728 -0
- package/src/{core → plugins}/terminal.ts +167 -127
- package/src/{core → plugins}/todo.ts +61 -100
- package/src/progress-core.ts +16 -53
- package/src/reconcile-core.ts +24 -33
- package/src/reducer-core.ts +24 -60
- package/src/reducer-invoker.ts +266 -239
- package/src/renderers.ts +76 -87
- package/src/role-loader.ts +33 -28
- package/src/role-manifest.ts +23 -28
- package/src/role-state.ts +119 -0
- package/src/routing-telemetry.ts +136 -0
- package/src/runtime-policy.ts +25 -40
- package/src/session-tail.ts +65 -41
- package/src/settle-wake-core.ts +52 -21
- package/src/slim-frame.ts +25 -122
- package/src/stale-core.ts +17 -24
- package/src/storage-layout.ts +14 -43
- package/src/subagent-core.ts +253 -118
- package/src/subagent-gc.ts +109 -67
- package/src/subagent-poller.ts +367 -24
- package/src/subagent-session.ts +516 -0
- package/src/subagent-spawn.ts +530 -52
- package/src/swallow.ts +7 -17
- package/src/terminal-core.ts +125 -106
- package/src/todo-core.ts +57 -109
- package/src/todo-read-hook.ts +23 -39
- package/src/todo-reminder-core.ts +26 -51
- package/src/todo-window.ts +11 -12
- package/src/todos-service.ts +15 -35
- package/src/tool-error.ts +6 -13
- package/src/tool-gate.ts +38 -21
- package/src/vocab.ts +52 -25
- package/src/ask-multi.ts +0 -260
- package/src/config-guide.ts +0 -361
- package/src/core/observation.ts +0 -555
- package/src/core/subagent.ts +0 -633
- package/src/efficiency-config-io.ts +0 -53
- package/src/git-adapter.ts +0 -113
- package/src/notice-buffer.ts +0 -23
- package/src/subagent-execute.ts +0 -328
- package/src/subagent-git-io.ts +0 -80
- package/src/subagent-launch.ts +0 -114
- package/src/subagent-list-action.ts +0 -69
- package/src/subagent-output-action.ts +0 -104
- package/src/subagent-output-adapter.ts +0 -27
- package/src/subagent-output-core.ts +0 -263
- package/src/subagent-poll-loop.ts +0 -305
- package/src/subagent-port.ts +0 -27
- package/src/subagent-ready.ts +0 -89
- package/src/subagent-registry.ts +0 -70
- package/src/subagent-resolution.ts +0 -41
- package/src/subagent-scope.ts +0 -41
- package/src/subagent-session-io.ts +0 -230
package/src/jev-client.ts
CHANGED
|
@@ -1,23 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Jev decision layer — direct HTTP client (RFC docs/rfc-jev-integration.md §5).
|
|
2
|
+
* Jev decision layer — direct HTTP client (RFC docs/rfc-jev-integration.md §5). Not the official SDK
|
|
3
|
+
* (@typesafe-ai/sdk): its retry machinery has no total-latency budget (timeouts retried by default,
|
|
4
|
+
* Retry-After honored up to 60s), which is incompatible with pier's fail-open budgets.
|
|
3
5
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* retries timeouts by default, Retry-After honored up to 60s — worst case
|
|
7
|
-
* >80s), which is incompatible with pier's fail-open budgets. The call surface
|
|
8
|
-
* here is one endpoint and three question types, and pi-pier ships as an npm
|
|
9
|
-
* package where a zero-dependency transport wins. Payload is pinned to the
|
|
10
|
-
* versioned model id; see the RFC for re-evaluation triggers.
|
|
11
|
-
*
|
|
12
|
-
* Every ask() resolves (never throws): errors, timeouts, 429s and low
|
|
13
|
-
* confidence at the call site all mean " unanswered" -> caller fails open.
|
|
6
|
+
* Every ask() resolves (never throws): errors, timeouts, 429s and low confidence at the call site
|
|
7
|
+
* all mean unanswered, and the caller fails open.
|
|
14
8
|
*/
|
|
15
9
|
import { sha256Hex } from './observation-core.ts';
|
|
16
10
|
import { appendEfficiencyLog, efficiencyLogPath } from './efficiency-store.ts';
|
|
17
11
|
import { parseJevAnswers, type JevAnswer, type JevQuestion, type JevUsage } from './jev-core.ts';
|
|
18
12
|
import type { JevConfig } from './efficiency-config-core.ts';
|
|
19
13
|
|
|
20
|
-
|
|
14
|
+
const JEV_DEFAULT_BASE_URL = 'https://api.typesafe.ai';
|
|
21
15
|
|
|
22
16
|
export interface JevAskMeta {
|
|
23
17
|
/** Stable question-set id for telemetry rows (e.g. 'epr-diagnostic-gate'). */
|
|
@@ -28,9 +22,8 @@ export interface JevAskMeta {
|
|
|
28
22
|
/** Site-specific static fields for telemetry; must never contain request bodies. */
|
|
29
23
|
extra?: Record<string, unknown>;
|
|
30
24
|
/**
|
|
31
|
-
* Post-answer decision fields for telemetry (choices/scores/final verdicts —
|
|
32
|
-
*
|
|
33
|
-
* Called exactly once per ask, success or failure.
|
|
25
|
+
* Post-answer decision fields for telemetry (choices/scores/final verdicts — what threshold tuning
|
|
26
|
+
* needs; still never request bodies). Called exactly once per ask.
|
|
34
27
|
*/
|
|
35
28
|
enrich?: (outcome: { ok: boolean; answers: Record<string, JevAnswer> | null }) => Record<string, unknown>;
|
|
36
29
|
}
|
|
@@ -44,6 +37,8 @@ export interface JevRuntimeDeps {
|
|
|
44
37
|
now?: () => number;
|
|
45
38
|
/** Session root for jev.jsonl; telemetry is skipped when absent. */
|
|
46
39
|
getSessionRoot?: () => string | null;
|
|
40
|
+
/** Session id fallback for telemetry rows; sites that know it pass meta.sessionId instead. */
|
|
41
|
+
getSessionId?: () => string | null | undefined;
|
|
47
42
|
}
|
|
48
43
|
|
|
49
44
|
export interface JevRuntime {
|
|
@@ -70,8 +65,8 @@ export function createJevRuntime(getConfig: () => JevConfig, deps: JevRuntimeDep
|
|
|
70
65
|
const record: Record<string, unknown> = {
|
|
71
66
|
schema: 'pier-efficiency/1',
|
|
72
67
|
mechanism: 'jev',
|
|
68
|
+
sessionId: meta.sessionId ?? deps.getSessionId?.() ?? 'unknown',
|
|
73
69
|
ts: new Date(now()).toISOString(),
|
|
74
|
-
sessionId: meta.sessionId ?? 'unknown',
|
|
75
70
|
questionId: meta.questionId,
|
|
76
71
|
model: outcome.ok ? outcome.model : config.model,
|
|
77
72
|
latencyMs,
|
|
@@ -87,7 +82,7 @@ export function createJevRuntime(getConfig: () => JevConfig, deps: JevRuntimeDep
|
|
|
87
82
|
try {
|
|
88
83
|
await appendEfficiencyLog(efficiencyLogPath(sessionRoot, 'jev'), record);
|
|
89
84
|
} catch {
|
|
90
|
-
|
|
85
|
+
/* Never surfaced to the caller. */
|
|
91
86
|
}
|
|
92
87
|
}
|
|
93
88
|
|
package/src/jev-core.ts
CHANGED
|
@@ -1,23 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Jev decision layer — pure core (RFC docs/rfc-jev-integration.md).
|
|
3
3
|
*
|
|
4
|
-
* Question/answer shapes mirror the closed set of POST /v1/systemone
|
|
5
|
-
*
|
|
6
|
-
* client + jev.jsonl telemetry live in jev-client.ts.
|
|
4
|
+
* Question/answer shapes mirror the closed set of POST /v1/systemone (https://docs.typesafe.ai/api):
|
|
5
|
+
* noul / choice / score. No I/O here; the HTTP client and jev.jsonl telemetry live in jev-client.ts.
|
|
7
6
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* - instructions are English (models page: English is the primary training
|
|
12
|
-
* language; CJK is handled but weaker, so P0-2 runs a stricter gate);
|
|
13
|
-
* - any answer below its confidence gate counts as unanswered -> caller
|
|
14
|
-
* fails open to the existing heuristic.
|
|
7
|
+
* RFC design rules: arithmetic, counting and thresholds stay in code; state carries only what each
|
|
8
|
+
* question needs; instructions are English (CJK is weaker, hence the stricter P0-2 gate); and any
|
|
9
|
+
* answer below its confidence gate counts as unanswered so callers fail open.
|
|
15
10
|
*/
|
|
16
|
-
import { FAILURE_SIGNAL } from './reducer-core.ts';
|
|
11
|
+
import { containsLikelySecret, FAILURE_SIGNAL } from './reducer-core.ts';
|
|
17
12
|
|
|
18
|
-
|
|
19
|
-
// Request/response shapes (closed set)
|
|
20
|
-
// ---------------------------------------------------------------------------
|
|
13
|
+
/* ── Request/response shapes (closed set) ── */
|
|
21
14
|
|
|
22
15
|
export type JevQuestion =
|
|
23
16
|
| { readonly type: 'noul'; readonly instructions: string; readonly criteria?: { readonly true?: string; readonly false?: string } }
|
|
@@ -56,6 +49,18 @@ function unitNumber(v: unknown): number | undefined {
|
|
|
56
49
|
return n !== undefined && n >= 0 && n <= 1 ? n : undefined;
|
|
57
50
|
}
|
|
58
51
|
|
|
52
|
+
type AnswerOf<T extends JevAnswer['type']> = Extract<JevAnswer, { type: T }>;
|
|
53
|
+
|
|
54
|
+
/** A missing or wrongly-shaped answer counts as unanswered, never as a verdict (RFC fail-open). */
|
|
55
|
+
function answerOf<T extends JevAnswer['type']>(
|
|
56
|
+
answers: Record<string, JevAnswer>,
|
|
57
|
+
id: string,
|
|
58
|
+
type: T,
|
|
59
|
+
): AnswerOf<T> | undefined {
|
|
60
|
+
const answer = answers[id];
|
|
61
|
+
return answer?.type === type ? answer as AnswerOf<T> : undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
59
64
|
function parseProbabilities(v: unknown): Record<string, number> | undefined {
|
|
60
65
|
if (!isPlainObject(v)) return undefined;
|
|
61
66
|
const out: Record<string, number> = {};
|
|
@@ -67,11 +72,8 @@ function parseProbabilities(v: unknown): Record<string, number> | undefined {
|
|
|
67
72
|
return out;
|
|
68
73
|
}
|
|
69
74
|
|
|
70
|
-
/**
|
|
71
|
-
*
|
|
72
|
-
* Extra answer ids are ignored; every expected id must be present and
|
|
73
|
-
* well-formed or the whole call counts as unanswered (fail-open).
|
|
74
|
-
*/
|
|
75
|
+
/** Strict validation of one response against the question ids we sent: extra ids are ignored, and
|
|
76
|
+
* every expected id must be present and well-formed or the call counts as unanswered (fail-open). */
|
|
75
77
|
export function parseJevAnswers(raw: unknown, expectedIds: readonly string[]): JevParsed {
|
|
76
78
|
if (!isPlainObject(raw)) return { ok: false, reason: 'response-not-object' };
|
|
77
79
|
const model = typeof raw.model === 'string' && raw.model.trim() ? raw.model : undefined;
|
|
@@ -120,9 +122,7 @@ export function parseJevAnswers(raw: unknown, expectedIds: readonly string[]): J
|
|
|
120
122
|
return { ok: true, answers, model, usage: { inputTokens, outputTokens } };
|
|
121
123
|
}
|
|
122
124
|
|
|
123
|
-
|
|
124
|
-
// P0-1: EPR diagnostic-command gate
|
|
125
|
-
// ---------------------------------------------------------------------------
|
|
125
|
+
/* ── P0-1: EPR diagnostic-command gate ── */
|
|
126
126
|
|
|
127
127
|
export const DIAGNOSTIC_OUTPUT_MIN_NOUL = 0.6;
|
|
128
128
|
|
|
@@ -152,18 +152,16 @@ export interface DiagnosticGateVerdict {
|
|
|
152
152
|
readonly hit: boolean;
|
|
153
153
|
readonly reason: string;
|
|
154
154
|
readonly confidence: number | null;
|
|
155
|
-
/** Selected cmd_kind option (telemetry); null when the answer was malformed. */
|
|
156
155
|
readonly choice: string | null;
|
|
157
|
-
/** diagnostic_output noul value (telemetry); null when the answer was malformed. */
|
|
158
156
|
readonly noul: number | null;
|
|
159
157
|
}
|
|
160
158
|
export function evaluateDiagnosticGate(
|
|
161
159
|
answers: Record<string, JevAnswer>,
|
|
162
160
|
minConfidence: number,
|
|
163
161
|
): DiagnosticGateVerdict {
|
|
164
|
-
const kind = answers
|
|
165
|
-
const output = answers
|
|
166
|
-
if (kind === undefined || output === undefined
|
|
162
|
+
const kind = answerOf(answers, 'cmd_kind', 'choice');
|
|
163
|
+
const output = answerOf(answers, 'diagnostic_output', 'noul');
|
|
164
|
+
if (kind === undefined || output === undefined) {
|
|
167
165
|
return { hit: false, reason: 'missing-answer', confidence: null, choice: null, noul: null };
|
|
168
166
|
}
|
|
169
167
|
if (kind.choice !== 'build_test_run') {
|
|
@@ -178,17 +176,18 @@ export function evaluateDiagnosticGate(
|
|
|
178
176
|
return { hit: true, reason: 'hit', confidence: kind.confidence, choice: kind.choice, noul: output.noul };
|
|
179
177
|
}
|
|
180
178
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
179
|
+
/** Flip semantics: jev judges first, the regex list decides only when jev produced no usable answer
|
|
180
|
+
* (failure, malformed, below gate). A confident rejection outranks a regex match. */
|
|
181
|
+
export function isDiagnosticGateUnanswered(verdict: DiagnosticGateVerdict): boolean {
|
|
182
|
+
return verdict.reason === 'missing-answer' || verdict.reason === 'low-confidence';
|
|
183
|
+
}
|
|
184
184
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
* the initial gate starts at 0.7 and calibrates from jev.jsonl.
|
|
189
|
-
*/
|
|
185
|
+
/* ── P0-2: settlement-notice relevance ranking ── */
|
|
186
|
+
|
|
187
|
+
/** Stricter than the global default: settlement text is mostly Chinese while English is jev's primary
|
|
188
|
+
* training language, so the initial gate starts at 0.7 and calibrates from jev.jsonl. */
|
|
190
189
|
export const NOTICE_RANK_MIN_CONFIDENCE = 0.7;
|
|
191
|
-
|
|
190
|
+
const RANK_SCORE_WEIGHT = 0.6;
|
|
192
191
|
|
|
193
192
|
const RANK_LEVELS = [
|
|
194
193
|
'Routine completion; the master agent can check it later',
|
|
@@ -202,9 +201,7 @@ export function noticeRankRequest(input: {
|
|
|
202
201
|
}): JevRequest {
|
|
203
202
|
const questions: Record<string, JevQuestion> = {};
|
|
204
203
|
for (let i = 0; i < input.notices.length; i++) {
|
|
205
|
-
// Question keys are NOT sent to the model
|
|
206
|
-
// collapse every notice into the same question (observed live: 5x "1.98:0.97").
|
|
207
|
-
// Each instruction must name its settlement by state index (cookbook pattern).
|
|
204
|
+
// Question keys are NOT sent to the model: identical instructions would collapse every notice into one.
|
|
208
205
|
questions[`notice_${i}_rank`] = {
|
|
209
206
|
type: 'score',
|
|
210
207
|
instructions: `How urgently does the master agent need to see settlements[${i}] right now, given in_progress_todos?`,
|
|
@@ -224,23 +221,15 @@ export function noticeRankRequest(input: {
|
|
|
224
221
|
};
|
|
225
222
|
}
|
|
226
223
|
|
|
227
|
-
/**
|
|
228
|
-
*
|
|
229
|
-
|
|
230
|
-
* two runs over the same Chinese batch reordered a failure notice from #2 to
|
|
231
|
-
* #4 — beyond the 3-shown cap. Score noise on CJK must not be able to bury a
|
|
232
|
-
* failure; the invariant lives in code, not in the model.
|
|
233
|
-
*/
|
|
234
|
-
export const NOTICE_FAIL_PIN_THRESHOLD = 0.7;
|
|
224
|
+
/** A settlement whose fail-noul clears this is pinned to the front regardless of relevance score:
|
|
225
|
+
* score noise on CJK reordered the same failure notice beyond the cap of shown notices. */
|
|
226
|
+
const NOTICE_FAIL_PIN_THRESHOLD = 0.7;
|
|
235
227
|
|
|
236
228
|
/**
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
* everything back to arrival). All items gated -> null (nothing was answered).
|
|
242
|
-
* The fail pin itself ignores the score gate: noul answers carry no confidence
|
|
243
|
-
* field, and failure visibility is the safety property.
|
|
229
|
+
* Display order: pinned failures first, then descending composed relevance (stable on ties -> arrival
|
|
230
|
+
* order); the per-item confidence gate sinks an item to the routine bucket instead of voiding the
|
|
231
|
+
* batch, and all items gated -> null. The fail pin ignores the score gate: noul answers carry no
|
|
232
|
+
* confidence field and failure visibility is the safety property.
|
|
244
233
|
*/
|
|
245
234
|
export function composeNoticeRanking(
|
|
246
235
|
noticeCount: number,
|
|
@@ -250,11 +239,9 @@ export function composeNoticeRanking(
|
|
|
250
239
|
const entries: Array<{ pin: number; value: number; index: number }> = [];
|
|
251
240
|
let answered = 0;
|
|
252
241
|
for (let i = 0; i < noticeCount; i++) {
|
|
253
|
-
const rank = answers
|
|
254
|
-
const fail = answers
|
|
255
|
-
if (rank === undefined || fail === undefined
|
|
256
|
-
return null;
|
|
257
|
-
}
|
|
242
|
+
const rank = answerOf(answers, `notice_${i}_rank`, 'score');
|
|
243
|
+
const fail = answerOf(answers, `notice_${i}_fail`, 'noul');
|
|
244
|
+
if (rank === undefined || fail === undefined) return null;
|
|
258
245
|
const gated = rank.confidence < minConfidence;
|
|
259
246
|
if (!gated) answered++;
|
|
260
247
|
const normalizedScore = rank.score / Math.max(1, RANK_LEVELS.length - 1);
|
|
@@ -269,11 +256,9 @@ export function composeNoticeRanking(
|
|
|
269
256
|
return entries.map((entry) => entry.index);
|
|
270
257
|
}
|
|
271
258
|
|
|
272
|
-
|
|
273
|
-
// P0-3: OBS excerpt window candidates (head/tail halves miss the middle)
|
|
274
|
-
// ---------------------------------------------------------------------------
|
|
259
|
+
/* ── P0-3: OBS excerpt window candidates (head/tail halves miss the middle) ── */
|
|
275
260
|
|
|
276
|
-
export type ExcerptWindowId = 'first_signal' | 'densest';
|
|
261
|
+
export type ExcerptWindowId = 'first_signal' | 'densest' | 'mid';
|
|
277
262
|
|
|
278
263
|
export interface ExcerptWindow {
|
|
279
264
|
readonly id: ExcerptWindowId;
|
|
@@ -306,11 +291,9 @@ function buildLineWindow(lines: readonly string[], start: number, budgetBytes: n
|
|
|
306
291
|
return { text: selected.join('\n'), bytes, signalCount };
|
|
307
292
|
}
|
|
308
293
|
|
|
309
|
-
/**
|
|
310
|
-
*
|
|
311
|
-
*
|
|
312
|
-
* of failure-signal lines within one half-budget. Deterministic, no model.
|
|
313
|
-
*/
|
|
294
|
+
/** Code-owned middle-window candidates for a packed observation: failure-signal windows (first hit,
|
|
295
|
+
* densest cluster) when present, plus a plain mid-output window ALWAYS offered, so jev judges every
|
|
296
|
+
* packed output — including logs whose failure lines the English FAILURE_SIGNAL regex cannot see. */
|
|
314
297
|
export function buildExcerptWindows(text: string, halfBudgetBytes: number): ExcerptWindow[] {
|
|
315
298
|
if (halfBudgetBytes <= 0) return [];
|
|
316
299
|
const lines = text.split('\n');
|
|
@@ -318,28 +301,34 @@ export function buildExcerptWindows(text: string, halfBudgetBytes: number): Exce
|
|
|
318
301
|
for (let i = 0; i < lines.length; i++) {
|
|
319
302
|
if (FAILURE_SIGNAL.test(lines[i]!)) signalLines.add(i);
|
|
320
303
|
}
|
|
321
|
-
if (signalLines.size === 0) return [];
|
|
322
304
|
|
|
323
305
|
const out: ExcerptWindow[] = [];
|
|
324
306
|
const seen = new Set<string>();
|
|
325
307
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
seen.
|
|
330
|
-
|
|
331
|
-
|
|
308
|
+
if (signalLines.size > 0) {
|
|
309
|
+
const first = Math.min(...signalLines);
|
|
310
|
+
const firstWindow = buildLineWindow(lines, Math.max(0, first - 2), halfBudgetBytes, signalLines);
|
|
311
|
+
if (firstWindow !== null && !seen.has(firstWindow.text)) {
|
|
312
|
+
seen.add(firstWindow.text);
|
|
313
|
+
out.push({ id: 'first_signal', label: 'first failure-signal region', text: firstWindow.text });
|
|
314
|
+
}
|
|
332
315
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
316
|
+
let best: LineWindow | null = null;
|
|
317
|
+
for (const anchor of signalLines) {
|
|
318
|
+
const window = buildLineWindow(lines, anchor, halfBudgetBytes, signalLines);
|
|
319
|
+
if (window === null) continue;
|
|
320
|
+
if (best === null || window.signalCount > best.signalCount) best = window;
|
|
321
|
+
}
|
|
322
|
+
if (best !== null && !seen.has(best.text)) {
|
|
323
|
+
seen.add(best.text);
|
|
324
|
+
out.push({ id: 'densest', label: 'densest failure-signal region', text: best.text });
|
|
325
|
+
}
|
|
339
326
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
327
|
+
|
|
328
|
+
const midAnchor = Math.min(Math.floor(lines.length / 2), Math.max(0, lines.length - 1));
|
|
329
|
+
const midWindow = buildLineWindow(lines, midAnchor, halfBudgetBytes, signalLines);
|
|
330
|
+
if (midWindow !== null && !seen.has(midWindow.text)) {
|
|
331
|
+
out.push({ id: 'mid', label: 'middle region of the output', text: midWindow.text });
|
|
343
332
|
}
|
|
344
333
|
return out;
|
|
345
334
|
}
|
|
@@ -374,8 +363,69 @@ export function evaluateExcerptPick(
|
|
|
374
363
|
answers: Record<string, JevAnswer>,
|
|
375
364
|
minConfidence: number,
|
|
376
365
|
): ExcerptWindowId | null {
|
|
377
|
-
const picked = answers
|
|
378
|
-
if (picked === undefined
|
|
366
|
+
const picked = answerOf(answers, 'window', 'choice');
|
|
367
|
+
if (picked === undefined) return null;
|
|
379
368
|
if (picked.confidence < minConfidence) return null;
|
|
380
|
-
return picked.choice === 'first_signal' || picked.choice === 'densest'
|
|
369
|
+
return picked.choice === 'first_signal' || picked.choice === 'densest' || picked.choice === 'mid'
|
|
370
|
+
? picked.choice
|
|
371
|
+
: null;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/** Local privacy gate for the excerpt-pick request: its state is sent for EVERY packed output — a path
|
|
375
|
+
* the EPR secret gate never covers — so any credential-shaped text keeps the whole request local. */
|
|
376
|
+
export function excerptAskIsSafe(
|
|
377
|
+
middleWindows: readonly ExcerptWindow[],
|
|
378
|
+
headExcerpt: string,
|
|
379
|
+
tailExcerpt: string,
|
|
380
|
+
): boolean {
|
|
381
|
+
const parts = [headExcerpt, tailExcerpt, ...middleWindows.map((w) => w.text)];
|
|
382
|
+
return !parts.some((part) => containsLikelySecret(part));
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/* ── P0-4: settlement attribution check ─────────────────────────── */
|
|
386
|
+
|
|
387
|
+
/** Noul answers carry no confidence field (unlike choice/score), so this use case gates on the noul
|
|
388
|
+
* probabilities themselves via dedicated constants, like DIAGNOSTIC_OUTPUT_MIN_NOUL. */
|
|
389
|
+
const SETTLE_MATCH_MIN_NOUL = 0.6;
|
|
390
|
+
const SETTLE_FINAL_MIN_NOUL = 0.6;
|
|
391
|
+
|
|
392
|
+
export type SettleNullVerdict = 'silent' | 'attribution-suspect' | 'extraction-failed';
|
|
393
|
+
|
|
394
|
+
export function settleVerdictRequest(input: { description: string; tail: string }): JevRequest {
|
|
395
|
+
return {
|
|
396
|
+
state: { task: input.description, transcriptTail: input.tail },
|
|
397
|
+
questions: {
|
|
398
|
+
tail_matches_task: {
|
|
399
|
+
type: 'noul',
|
|
400
|
+
instructions: 'Is this transcript tail from a session working on the described task? Answer false only when the tail is clearly about a different task or conversation.',
|
|
401
|
+
criteria: {
|
|
402
|
+
true: 'The tail discusses, works on, or reports on the described task',
|
|
403
|
+
false: 'The tail is about an unrelated task or a different conversation',
|
|
404
|
+
},
|
|
405
|
+
},
|
|
406
|
+
tail_has_final_answer: {
|
|
407
|
+
type: 'noul',
|
|
408
|
+
instructions: 'Does the transcript tail end with (or contain) a complete final answer or report for the task, rather than only intermediate tool exchanges?',
|
|
409
|
+
criteria: {
|
|
410
|
+
true: 'A complete final answer/report is present',
|
|
411
|
+
false: 'Only intermediate steps, questions, or tool exchanges — no final answer',
|
|
412
|
+
},
|
|
413
|
+
},
|
|
414
|
+
},
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
/** Compose the null-closing verdict. null = unanswered (jev off / failed / wrong shapes): callers
|
|
418
|
+
* fall back to deterministic signals and the legacy wording, so jev-off stays byte-identical. */
|
|
419
|
+
export function evaluateSettleVerdict(answers: Record<string, JevAnswer>): SettleNullVerdict | null {
|
|
420
|
+
const match = answerOf(answers, 'tail_matches_task', 'noul');
|
|
421
|
+
const fin = answerOf(answers, 'tail_has_final_answer', 'noul');
|
|
422
|
+
if (match === undefined || fin === undefined) return null;
|
|
423
|
+
if (match.noul < SETTLE_MATCH_MIN_NOUL) return 'attribution-suspect';
|
|
424
|
+
if (fin.noul >= SETTLE_FINAL_MIN_NOUL) return 'extraction-failed';
|
|
425
|
+
return 'silent';
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/** Privacy gate shared with EPR: never ship credential-shaped tails off-host. */
|
|
429
|
+
export function settleAskIsSafe(tail: string): boolean {
|
|
430
|
+
return !containsLikelySecret(tail);
|
|
381
431
|
}
|
package/src/ledger.ts
CHANGED
|
@@ -1,24 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Disposal ledger shared by HMR and session shutdown (D80⑤ + D79).
|
|
3
3
|
*
|
|
4
|
-
* HMR partial reload can skip old fiber effect disposers, so process-level resources
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* D79 reuses this mechanism for pi-surface registration cleanup; logical keys need not
|
|
10
|
-
* be file paths. The ledger stays dependency-free and connects through bootstrap/index.
|
|
4
|
+
* HMR partial reload can skip old fiber effect disposers, so process-level resources (intervals, pipe
|
|
5
|
+
* servers, watchers) would leak without compensation. Modules register those resources by module key;
|
|
6
|
+
* reload disposes matching keys, session_shutdown disposes everything. D79 reuses the same mechanism
|
|
7
|
+
* for pi-surface registration cleanup, where keys need not be file paths.
|
|
11
8
|
*/
|
|
12
9
|
|
|
13
10
|
import { fileURLToPath } from 'node:url';
|
|
14
11
|
|
|
15
|
-
|
|
16
|
-
export function normalizeModuleKey(spec: string): string {
|
|
12
|
+
function normalizeModuleKey(spec: string): string {
|
|
17
13
|
let s = String(spec);
|
|
18
14
|
try {
|
|
19
15
|
if (s.startsWith('file://')) s = fileURLToPath(s);
|
|
20
16
|
} catch {
|
|
21
|
-
|
|
17
|
+
// Malformed URL: keep the caller's key so disposal still matches.
|
|
22
18
|
}
|
|
23
19
|
return s.replace(/\\/g, '/');
|
|
24
20
|
}
|
|
@@ -27,7 +23,7 @@ export class DisposeLedger {
|
|
|
27
23
|
/** LIFO order matches Cordis effect disposal semantics. */
|
|
28
24
|
private order: Array<{ key: string; dispose: () => void }> = [];
|
|
29
25
|
|
|
30
|
-
/** Register a resource
|
|
26
|
+
/** Register a resource; the returned cancellation makes self-disposal a no-op. */
|
|
31
27
|
add(spec: string, dispose: () => void): () => void {
|
|
32
28
|
const entry = { key: normalizeModuleKey(spec), dispose };
|
|
33
29
|
this.order.push(entry);
|
|
@@ -37,27 +33,23 @@ export class DisposeLedger {
|
|
|
37
33
|
};
|
|
38
34
|
}
|
|
39
35
|
|
|
40
|
-
/** HMR compensation disposes only matching keys in LIFO order. */
|
|
41
36
|
disposeKey(spec: string | string[]): number {
|
|
42
|
-
const keys =
|
|
37
|
+
const keys = (Array.isArray(spec) ? spec : [spec]).map(normalizeModuleKey);
|
|
43
38
|
let n = 0;
|
|
44
39
|
for (let i = this.order.length - 1; i >= 0; i--) {
|
|
45
|
-
if (keys.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
n++;
|
|
50
|
-
}
|
|
40
|
+
if (!keys.includes(this.order[i]!.key)) continue;
|
|
41
|
+
const { dispose } = this.order.splice(i, 1)[0]!;
|
|
42
|
+
try { dispose(); } catch { /* Compensation must never abort the reload. */ }
|
|
43
|
+
n++;
|
|
51
44
|
}
|
|
52
45
|
return n;
|
|
53
46
|
}
|
|
54
47
|
|
|
55
|
-
/** Dispose every entry in LIFO order during session_shutdown. */
|
|
56
48
|
disposeAll(): number {
|
|
57
49
|
let n = 0;
|
|
58
50
|
while (this.order.length > 0) {
|
|
59
51
|
const { dispose } = this.order.pop()!;
|
|
60
|
-
try { dispose(); } catch { /*
|
|
52
|
+
try { dispose(); } catch { /* Reload/shutdown must survive a broken disposer. */ }
|
|
61
53
|
n++;
|
|
62
54
|
}
|
|
63
55
|
return n;
|