instar 1.3.1300 → 1.3.1302

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.
Files changed (79) hide show
  1. package/dist/commands/server.js +9 -14
  2. package/dist/commands/server.js.map +1 -1
  3. package/dist/config/ConfigDefaults.d.ts.map +1 -1
  4. package/dist/config/ConfigDefaults.js +6 -0
  5. package/dist/config/ConfigDefaults.js.map +1 -1
  6. package/dist/core/AutonomousSessions.d.ts +11 -0
  7. package/dist/core/AutonomousSessions.d.ts.map +1 -1
  8. package/dist/core/AutonomousSessions.js +29 -0
  9. package/dist/core/AutonomousSessions.js.map +1 -1
  10. package/dist/core/GateSignalDetectors.d.ts +9 -0
  11. package/dist/core/GateSignalDetectors.d.ts.map +1 -1
  12. package/dist/core/GateSignalDetectors.js.map +1 -1
  13. package/dist/core/JevCascade.d.ts +9 -0
  14. package/dist/core/JevCascade.d.ts.map +1 -1
  15. package/dist/core/JevCascade.js +12 -3
  16. package/dist/core/JevCascade.js.map +1 -1
  17. package/dist/core/JevSignalShadow.d.ts +128 -1
  18. package/dist/core/JevSignalShadow.d.ts.map +1 -1
  19. package/dist/core/JevSignalShadow.js +277 -24
  20. package/dist/core/JevSignalShadow.js.map +1 -1
  21. package/dist/core/MessagingToneGate.d.ts +34 -3
  22. package/dist/core/MessagingToneGate.d.ts.map +1 -1
  23. package/dist/core/MessagingToneGate.js +81 -13
  24. package/dist/core/MessagingToneGate.js.map +1 -1
  25. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  26. package/dist/core/PostUpdateMigrator.js +36 -2
  27. package/dist/core/PostUpdateMigrator.js.map +1 -1
  28. package/dist/core/SessionManager.d.ts +9 -0
  29. package/dist/core/SessionManager.d.ts.map +1 -1
  30. package/dist/core/SessionManager.js +46 -0
  31. package/dist/core/SessionManager.js.map +1 -1
  32. package/dist/core/devGatedFeatures.d.ts.map +1 -1
  33. package/dist/core/devGatedFeatures.js +6 -0
  34. package/dist/core/devGatedFeatures.js.map +1 -1
  35. package/dist/core/types.d.ts +28 -2
  36. package/dist/core/types.d.ts.map +1 -1
  37. package/dist/core/types.js.map +1 -1
  38. package/dist/data/standards-guard-index.json +1 -1
  39. package/dist/data/standards-guard-index.meta.json +2 -2
  40. package/dist/data/standards-registry.meta.json +1 -1
  41. package/dist/feedback-factory/inbox/inboxAwareness.d.ts +12 -0
  42. package/dist/feedback-factory/inbox/inboxAwareness.d.ts.map +1 -0
  43. package/dist/feedback-factory/inbox/inboxAwareness.js +18 -0
  44. package/dist/feedback-factory/inbox/inboxAwareness.js.map +1 -0
  45. package/dist/feedback-factory/inbox/resolveInboxBlobToken.d.ts +19 -0
  46. package/dist/feedback-factory/inbox/resolveInboxBlobToken.d.ts.map +1 -0
  47. package/dist/feedback-factory/inbox/resolveInboxBlobToken.js +40 -0
  48. package/dist/feedback-factory/inbox/resolveInboxBlobToken.js.map +1 -0
  49. package/dist/scaffold/templates.d.ts +4 -0
  50. package/dist/scaffold/templates.d.ts.map +1 -1
  51. package/dist/scaffold/templates.js +14 -2
  52. package/dist/scaffold/templates.js.map +1 -1
  53. package/dist/scheduler/JobScheduler.d.ts.map +1 -1
  54. package/dist/scheduler/JobScheduler.js +12 -3
  55. package/dist/scheduler/JobScheduler.js.map +1 -1
  56. package/dist/scheduler/jobDeclaredFailure.d.ts +13 -0
  57. package/dist/scheduler/jobDeclaredFailure.d.ts.map +1 -0
  58. package/dist/scheduler/jobDeclaredFailure.js +67 -0
  59. package/dist/scheduler/jobDeclaredFailure.js.map +1 -0
  60. package/dist/server/AgentServer.d.ts.map +1 -1
  61. package/dist/server/AgentServer.js +48 -6
  62. package/dist/server/AgentServer.js.map +1 -1
  63. package/dist/server/CapabilityIndex.js +1 -1
  64. package/dist/server/CapabilityIndex.js.map +1 -1
  65. package/dist/server/routes.d.ts.map +1 -1
  66. package/dist/server/routes.js +12 -1
  67. package/dist/server/routes.js.map +1 -1
  68. package/package.json +1 -1
  69. package/src/data/builtin-manifest.json +66 -66
  70. package/src/data/standards-guard-index.json +1 -1
  71. package/src/data/standards-guard-index.meta.json +2 -2
  72. package/src/data/standards-registry.meta.json +1 -1
  73. package/src/scaffold/templates/jobs/instar/feedback-factory-process.md +13 -5
  74. package/src/scaffold/templates.ts +15 -2
  75. package/upgrades/1.3.1301.md +69 -0
  76. package/upgrades/1.3.1302.md +121 -0
  77. package/upgrades/side-effects/age-limit-coordinator-r2.md +296 -0
  78. package/upgrades/side-effects/feedback-inbox-vault-token.md +230 -0
  79. package/upgrades/side-effects/jev-signal-live.md +173 -0
@@ -1,3 +1,4 @@
1
+ import { type GateSignal } from './GateSignalDetectors.js';
1
2
  import { type CascadeBand, type EscalationReason } from './JevCascade.js';
2
3
  import type { IntelligenceProvider } from './types.js';
3
4
  export declare const JEV_SHADOW_FEATURE = "jev-signal-shadow";
@@ -34,8 +35,57 @@ export interface JevSignalShadowConfig {
34
35
  maxPerDay?: number;
35
36
  };
36
37
  }
38
+ /**
39
+ * The live advisory switch (docs/specs/jev-signal-live.md). When on, the tone
40
+ * gate awaits Jev (bounded) and uses its CONFIDENT answers for the B1–B7
41
+ * artefact signals; unsure or missing answers fall back to the detectors.
42
+ * `enabled` is resolved through the development-agent gate before it reaches
43
+ * the shadow (live on a dev agent, dark on the fleet unless flipped).
44
+ */
45
+ export interface JevSignalLiveConfig {
46
+ enabled?: boolean;
47
+ /** Fetch abort for the live call (default LIVE_DEFAULT_TIMEOUT_MS, clamped to LIVE_TIMEOUT_MIN_MS–LIVE_TIMEOUT_MAX_MS). */
48
+ timeoutMs?: number;
49
+ /** Confident-vs-unsure band (default: the cascade's 0.30–0.70). */
50
+ band?: CascadeBand;
51
+ bands?: Record<string, CascadeBand>;
52
+ }
53
+ /** Covers the measured p99 (834 ms) of Jev calls on real candidates. */
54
+ export declare const LIVE_DEFAULT_TIMEOUT_MS = 1000;
55
+ /** Operator `timeoutMs` is clamped to this range, so no config value can make the gate wait long. */
56
+ export declare const LIVE_TIMEOUT_MIN_MS = 100;
57
+ export declare const LIVE_TIMEOUT_MAX_MS = 3000;
58
+ /** Extra wait past the fetch abort before the caller stops waiting regardless. */
59
+ export declare const LIVE_RACE_SLACK_MS = 250;
60
+ /** An in-flight slot older than this is treated as abandoned and reclaimed. */
61
+ export declare const STALE_SLOT_MS = 30000;
62
+ /** Consecutive live failures (missed deadline, timeout, HTTP error, model mismatch) that open the breaker. */
63
+ export declare const LIVE_BREAKER_FAILURES = 3;
64
+ /** How long an open breaker keeps live mode on the detector path. */
65
+ export declare const LIVE_BREAKER_OPEN_MS: number;
66
+ /**
67
+ * What a rule's line on a live candidate rests on: `jev` = a confident Jev
68
+ * answer decided it; `disputed` = the detector matched and Jev confidently
69
+ * disagrees — the detection stands, annotated with Jev's view; `detector-fallback`
70
+ * = Jev was unsure or had no answer, the detector decided.
71
+ */
72
+ export type LiveSource = 'jev' | 'disputed' | 'detector-fallback';
73
+ /** What the tone gate receives from a live call. Never rejects. */
74
+ export interface LiveSignalsResult {
75
+ signals: GateSignal[];
76
+ /** True when at least one signal line came from a confident Jev answer —
77
+ * i.e. the list the judge sees differs from the detector list. An agreed
78
+ * "nothing here" does not count, so the prompt stays today's. */
79
+ jevUsed: boolean;
80
+ }
37
81
  /** The closed set of reasons a candidate was not compared. */
38
- export type NotComparedReason = 'timeout' | 'http-error' | 'oversize' | 'skipped-concurrent' | 'skipped-sample' | 'model-mismatch' | 'disabled-no-key' | 'soak-expired';
82
+ export type NotComparedReason = 'timeout' | 'http-error' | 'oversize' | 'skipped-concurrent' | 'skipped-sample' | 'model-mismatch' | 'disabled-no-key' | 'soak-expired'
83
+ /** Live only: the text scrub failed, so nothing was sent. */
84
+ | 'scrub-error'
85
+ /** Live only: the vendor answered but gave no usable probability at all. */
86
+ | 'no-answers'
87
+ /** Live only: the vendor breaker opened (one row per opening). */
88
+ | 'breaker-open';
39
89
  export type ShadowRow = {
40
90
  kind: 'compared';
41
91
  ts: string;
@@ -46,6 +96,13 @@ export type ShadowRow = {
46
96
  ms: number;
47
97
  modelServed: string;
48
98
  disagree: string[];
99
+ /** Present on live candidates (the answers fed the tone gate). */
100
+ live?: true;
101
+ /** Live only: which source decided each rule's signal. */
102
+ liveSources?: Record<string, LiveSource>;
103
+ /** Live only: the answer arrived after the caller stopped waiting, so
104
+ * every rule for this message used the detector. */
105
+ liveLate?: true;
49
106
  /** Scrubbed, span-anchored excerpt — present ONLY on disagreeing rows,
50
107
  * and only when retention is enabled. */
51
108
  excerpt?: string;
@@ -59,6 +116,8 @@ export type ShadowRow = {
59
116
  sha256: string;
60
117
  bytes: number;
61
118
  reason: NotComparedReason;
119
+ /** Live candidate: every rule for this message fell back to the detector. */
120
+ live?: true;
62
121
  } | {
63
122
  kind: 'referee';
64
123
  ts: string;
@@ -98,6 +157,8 @@ export declare const KEY_REREAD_MS: number;
98
157
  export interface JevSignalShadowDeps {
99
158
  /** Live config read — called per candidate so the kill-switch needs no restart. */
100
159
  getConfig: () => JevSignalShadowConfig | undefined;
160
+ /** Live advisory config, `enabled` already dev-gate-resolved. Absent ⇒ never live. */
161
+ getLiveConfig?: () => JevSignalLiveConfig | undefined;
101
162
  /**
102
163
  * Resolves the vault key. May block (a keychain lookup), so it is called once
103
164
  * at construction — boot, never the message path — and afterwards at most once
@@ -120,6 +181,13 @@ export interface JevSignalShadowDeps {
120
181
  }
121
182
  export declare class JevSignalShadow {
122
183
  private inFlight;
184
+ /** When the current in-flight call started (stale-slot reclaim). */
185
+ private inFlightSince;
186
+ /** Slot generation: only the call that holds the CURRENT slot may release it. */
187
+ private slotGen;
188
+ /** Vendor-down breaker for live mode: consecutive failures, and when it closes again. */
189
+ private liveFailures;
190
+ private liveBreakerUntil;
123
191
  private cachedKey;
124
192
  /** When the key was last read; re-reads are rate-limited from here. */
125
193
  private keyReadAt;
@@ -127,6 +195,34 @@ export declare class JevSignalShadow {
127
195
  private readonly deps;
128
196
  constructor(deps: JevSignalShadowDeps);
129
197
  private refreshKey;
198
+ /**
199
+ * The single-flight slot. A slot held past STALE_SLOT_MS belongs to a call
200
+ * that never settled (a fetch that ignored its abort); it is reclaimed so one
201
+ * stuck request cannot turn every later candidate into a fallback forever.
202
+ * Outstanding requests stay bounded: at most one per STALE_SLOT_MS.
203
+ */
204
+ private slotBusy;
205
+ /**
206
+ * Take the slot and return its release. The release is generation-checked: a
207
+ * reclaimed (stale) call that settles later cannot free the slot a newer call
208
+ * now holds, so single-flight survives a reclaim.
209
+ */
210
+ private takeSlot;
211
+ /**
212
+ * Live entry point (docs/specs/jev-signal-live.md). Returns null — without
213
+ * doing anything — when live mode is off, so the caller falls back to
214
+ * `observe()` and today's detector-only path. Otherwise returns a promise that
215
+ * never rejects and settles within timeoutMs + LIVE_RACE_SLACK_MS with the
216
+ * merged signals; any failure resolves to the plain detector signals.
217
+ * The live call IS the shadow's measurement call: it writes the same rows
218
+ * (tagged `live: true`) and feeds the same referee cascade.
219
+ */
220
+ liveSignals(text: string): Promise<LiveSignalsResult> | null;
221
+ private liveInner;
222
+ /** Breaker bookkeeping per live candidate, as the caller saw it: an answer in
223
+ * time resets the count; a missed deadline, HTTP error, timeout or model
224
+ * mismatch counts as one failure. */
225
+ private noteLiveOutcome;
130
226
  /**
131
227
  * Fire-and-forget entry point. Synchronous, never throws, never awaited by
132
228
  * the caller. The only synchronous work is a config read, a hash, and (when
@@ -166,6 +262,27 @@ export declare class JevSignalShadow {
166
262
  private maybeRefer;
167
263
  private write;
168
264
  }
265
+ /**
266
+ * Merge Jev's answers into the detector signals (docs/specs/jev-signal-live.md).
267
+ * Per kind:
268
+ * - Jev confident YES (p > band.hi): detected. The detector's signal is kept
269
+ * (its spans + sample still anchor the judge); if the detector missed it, a
270
+ * sample-less model signal is added.
271
+ * - Jev confident NO (p < band.lo) against a detector match: the detection
272
+ * STANDS (detected:true, so every B1–B7 rule can still fire on it) and is
273
+ * annotated with Jev's disagreement for the judge to weigh. Jev never hides
274
+ * or overrides an observation; it can only add to or comment on the list.
275
+ * - Unsure or no answer: the detector's output, unchanged.
276
+ * Pure. Output order follows GATE_SIGNAL_KINDS.
277
+ */
278
+ export declare function mergeLiveSignals(detected: GateSignal[], jev: Record<string, number>, opts?: {
279
+ band?: CascadeBand;
280
+ bands?: Record<string, CascadeBand>;
281
+ }): {
282
+ signals: GateSignal[];
283
+ sources: Record<string, LiveSource>;
284
+ jevUsed: boolean;
285
+ };
169
286
  /**
170
287
  * The production wiring, shared by server.ts and the E2E test so the test
171
288
  * exercises the same config resolution, key lookup and log location the server
@@ -179,5 +296,15 @@ export declare function buildJevSignalShadow(opts: {
179
296
  metrics?: JevSignalShadowDeps['metrics'];
180
297
  fetchImpl?: typeof fetch;
181
298
  referee?: IntelligenceProvider | null;
299
+ /** Boot-time `intelligence.jevSignalLive`, the fallback when the live read has none. */
300
+ bootLiveBlock?: JevSignalLiveConfig;
301
+ /** The agent's `developmentAgent` flag — decides live mode when `enabled` is omitted. */
302
+ developmentAgent?: boolean;
303
+ /**
304
+ * Is decision-quality recording live? The outbound route hardens a migration
305
+ * advisory into a non-overridable hold when it is not, so live mode is off
306
+ * then. Default: the real recorder's answer.
307
+ */
308
+ recordingLive?: () => boolean;
182
309
  }): JevSignalShadow;
183
310
  //# sourceMappingURL=JevSignalShadow.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"JevSignalShadow.d.ts","sourceRoot":"","sources":["../../src/core/JevSignalShadow.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"JevSignalShadow.d.ts","sourceRoot":"","sources":["../../src/core/JevSignalShadow.ts"],"names":[],"mappings":"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"}
@@ -27,15 +27,30 @@
27
27
  import fs from 'node:fs';
28
28
  import path from 'node:path';
29
29
  import crypto from 'node:crypto';
30
- import { detectGateSignals } from './GateSignalDetectors.js';
30
+ import { detectGateSignals, GATE_SIGNAL_KINDS } from './GateSignalDetectors.js';
31
31
  import { scrubForStore } from './durableSecretScrub.js';
32
- import { selectEscalations, askReferee } from './JevCascade.js';
32
+ import { selectEscalations, askReferee, bandFor } from './JevCascade.js';
33
+ import { resolveDevAgentGate } from './devAgentGate.js';
34
+ import { decisionQualityRecordingLive } from './DecisionQualityRecorderImpl.js';
33
35
  export const JEV_SHADOW_FEATURE = 'jev-signal-shadow';
34
36
  /** Attribution label for the referee's LLM calls (feature metrics + routing). */
35
37
  export const JEV_REFEREE_COMPONENT = 'JevLunaReferee';
36
38
  /** Default volume bound on referee calls per UTC day. */
37
39
  export const REFEREE_DAILY_CAP = 300;
38
40
  const ENDPOINT = 'https://api.typesafe.ai/v1/systemone';
41
+ /** Covers the measured p99 (834 ms) of Jev calls on real candidates. */
42
+ export const LIVE_DEFAULT_TIMEOUT_MS = 1000;
43
+ /** Operator `timeoutMs` is clamped to this range, so no config value can make the gate wait long. */
44
+ export const LIVE_TIMEOUT_MIN_MS = 100;
45
+ export const LIVE_TIMEOUT_MAX_MS = 3000;
46
+ /** Extra wait past the fetch abort before the caller stops waiting regardless. */
47
+ export const LIVE_RACE_SLACK_MS = 250;
48
+ /** An in-flight slot older than this is treated as abandoned and reclaimed. */
49
+ export const STALE_SLOT_MS = 30_000;
50
+ /** Consecutive live failures (missed deadline, timeout, HTTP error, model mismatch) that open the breaker. */
51
+ export const LIVE_BREAKER_FAILURES = 3;
52
+ /** How long an open breaker keeps live mode on the detector path. */
53
+ export const LIVE_BREAKER_OPEN_MS = 5 * 60_000;
39
54
  /**
40
55
  * The frozen comparison contract: one Noul question per rule, the detector
41
56
  * signal kind it is compared against, and the positive threshold. Changing a
@@ -62,6 +77,13 @@ export const EXCERPT_DAILY_CAP = 50;
62
77
  export const KEY_REREAD_MS = 10 * 60_000;
63
78
  export class JevSignalShadow {
64
79
  inFlight = false;
80
+ /** When the current in-flight call started (stale-slot reclaim). */
81
+ inFlightSince = 0;
82
+ /** Slot generation: only the call that holds the CURRENT slot may release it. */
83
+ slotGen = 0;
84
+ /** Vendor-down breaker for live mode: consecutive failures, and when it closes again. */
85
+ liveFailures = 0;
86
+ liveBreakerUntil = 0;
65
87
  cachedKey = null;
66
88
  /** When the key was last read; re-reads are rate-limited from here. */
67
89
  keyReadAt = 0;
@@ -81,6 +103,133 @@ export class JevSignalShadow {
81
103
  this.cachedKey = null;
82
104
  }
83
105
  }
106
+ /**
107
+ * The single-flight slot. A slot held past STALE_SLOT_MS belongs to a call
108
+ * that never settled (a fetch that ignored its abort); it is reclaimed so one
109
+ * stuck request cannot turn every later candidate into a fallback forever.
110
+ * Outstanding requests stay bounded: at most one per STALE_SLOT_MS.
111
+ */
112
+ slotBusy(nowMs) {
113
+ return this.inFlight && nowMs - this.inFlightSince < STALE_SLOT_MS;
114
+ }
115
+ /**
116
+ * Take the slot and return its release. The release is generation-checked: a
117
+ * reclaimed (stale) call that settles later cannot free the slot a newer call
118
+ * now holds, so single-flight survives a reclaim.
119
+ */
120
+ takeSlot(nowMs) {
121
+ const gen = ++this.slotGen;
122
+ this.inFlight = true;
123
+ this.inFlightSince = nowMs;
124
+ return () => { if (this.slotGen === gen)
125
+ this.inFlight = false; };
126
+ }
127
+ /**
128
+ * Live entry point (docs/specs/jev-signal-live.md). Returns null — without
129
+ * doing anything — when live mode is off, so the caller falls back to
130
+ * `observe()` and today's detector-only path. Otherwise returns a promise that
131
+ * never rejects and settles within timeoutMs + LIVE_RACE_SLACK_MS with the
132
+ * merged signals; any failure resolves to the plain detector signals.
133
+ * The live call IS the shadow's measurement call: it writes the same rows
134
+ * (tagged `live: true`) and feeds the same referee cascade.
135
+ */
136
+ liveSignals(text) {
137
+ let live;
138
+ try {
139
+ live = this.deps.getLiveConfig?.();
140
+ }
141
+ catch {
142
+ // @silent-fallback-ok — an unreadable live config means live is off; the gate keeps today's detector-only path.
143
+ return null;
144
+ }
145
+ if (!live || live.enabled !== true)
146
+ return null;
147
+ try {
148
+ return this.liveInner(text, live);
149
+ }
150
+ catch {
151
+ // @silent-fallback-ok — any live-path fault resolves to the detector signals (today's behaviour).
152
+ return Promise.resolve(detectorOnly(text));
153
+ }
154
+ }
155
+ liveInner(text, live) {
156
+ const cfg = this.deps.getConfig() ?? {};
157
+ const nowMs = (this.deps.now ?? Date.now)();
158
+ const ts = new Date(nowMs).toISOString();
159
+ const bytes = Buffer.byteLength(text, 'utf8');
160
+ const maxBytes = this.deps.maxScanBytes ?? 1_000_000;
161
+ const sha256 = crypto.createHash('sha256').update(bytes > maxBytes ? text.slice(0, maxBytes) : text, 'utf8').digest('hex');
162
+ const fallback = (reason) => {
163
+ this.write({ kind: 'not-compared', ts, sha256, bytes, reason, live: true });
164
+ return Promise.resolve(detectorOnly(text));
165
+ };
166
+ if (!this.cachedKey && nowMs - this.keyReadAt >= KEY_REREAD_MS)
167
+ this.refreshKey();
168
+ if (!this.cachedKey) {
169
+ // A standing condition: one row per process, not one per message.
170
+ if (this.bootNoted.has('disabled-no-key'))
171
+ return Promise.resolve(detectorOnly(text));
172
+ this.bootNoted.add('disabled-no-key');
173
+ return fallback('disabled-no-key');
174
+ }
175
+ // During a vendor outage every message would otherwise wait the full
176
+ // timeout; an open breaker keeps them on the detector path (one row when it
177
+ // opens, not one per message).
178
+ if (nowMs < this.liveBreakerUntil)
179
+ return Promise.resolve(detectorOnly(text));
180
+ if (bytes > maxBytes)
181
+ return fallback('oversize');
182
+ if (this.slotBusy(nowMs))
183
+ return fallback('skipped-concurrent');
184
+ // Live mode is not soak-bounded, so the text is secret-scrubbed before it
185
+ // leaves the machine (the detectors and the row hash still see the original).
186
+ const scrubbed = scrubForStore(text);
187
+ if (scrubbed.error || scrubbed.truncated)
188
+ return fallback('scrub-error');
189
+ const signals = detectGateSignals(text);
190
+ const timeoutMs = typeof live.timeoutMs === 'number' && Number.isFinite(live.timeoutMs)
191
+ ? Math.max(LIVE_TIMEOUT_MIN_MS, Math.min(LIVE_TIMEOUT_MAX_MS, live.timeoutMs))
192
+ : LIVE_DEFAULT_TIMEOUT_MS;
193
+ const race = { late: false };
194
+ const release = this.takeSlot(nowMs);
195
+ const call = this.dispatch(text, cfg, { ts, sha256, bytes, detectorSignals: signals.map((x) => x.kind), signals }, { live, timeoutMs, race, sendText: scrubbed.text });
196
+ this.lastDispatch = call.then(() => undefined, () => undefined).finally(release);
197
+ let timer;
198
+ const deadline = new Promise((resolve) => {
199
+ timer = setTimeout(() => {
200
+ race.late = true;
201
+ // The caller stops waiting now: record that outcome now, so a call that
202
+ // never settles still leaves a row (a late answer adds a liveLate row).
203
+ this.write({ kind: 'not-compared', ts, sha256, bytes, reason: 'timeout', live: true });
204
+ resolve(null);
205
+ }, timeoutMs + LIVE_RACE_SLACK_MS);
206
+ timer.unref?.();
207
+ });
208
+ // The breaker counts what the CALLER saw, once: a missed deadline is a
209
+ // failure even if the answer lands later, and a late success resets nothing.
210
+ return Promise.race([call.catch(() => null), deadline])
211
+ .then((merged) => {
212
+ this.noteLiveOutcome(merged !== null, ts, sha256, bytes);
213
+ return merged ?? { signals, jevUsed: false };
214
+ })
215
+ .finally(() => { if (timer)
216
+ clearTimeout(timer); });
217
+ }
218
+ /** Breaker bookkeeping per live candidate, as the caller saw it: an answer in
219
+ * time resets the count; a missed deadline, HTTP error, timeout or model
220
+ * mismatch counts as one failure. */
221
+ noteLiveOutcome(ok, ts, sha256, bytes) {
222
+ if (ok) {
223
+ this.liveFailures = 0;
224
+ return;
225
+ }
226
+ this.liveFailures++;
227
+ if (this.liveFailures < LIVE_BREAKER_FAILURES)
228
+ return;
229
+ this.liveFailures = 0;
230
+ this.liveBreakerUntil = (this.deps.now ?? Date.now)() + LIVE_BREAKER_OPEN_MS;
231
+ this.write({ kind: 'not-compared', ts, sha256, bytes, reason: 'breaker-open', live: true });
232
+ }
84
233
  /**
85
234
  * Fire-and-forget entry point. Synchronous, never throws, never awaited by
86
235
  * the caller. The only synchronous work is a config read, a hash, and (when
@@ -134,7 +283,7 @@ export class JevSignalShadow {
134
283
  notCompared('oversize');
135
284
  return;
136
285
  }
137
- if (this.inFlight) {
286
+ if (this.slotBusy(nowMs)) {
138
287
  notCompared('skipped-concurrent');
139
288
  return;
140
289
  }
@@ -143,19 +292,31 @@ export class JevSignalShadow {
143
292
  // beyond what the detector actually pointed at.
144
293
  const signals = detectGateSignals(text);
145
294
  const detectorSignals = signals.map((s) => s.kind);
146
- this.inFlight = true;
295
+ const release = this.takeSlot(nowMs);
147
296
  this.lastDispatch = this.dispatch(text, cfg, { ts, sha256, bytes, detectorSignals, signals })
297
+ .then(() => undefined)
148
298
  .catch(() => { })
149
- .finally(() => { this.inFlight = false; });
299
+ .finally(release);
150
300
  }
151
- async dispatch(text, cfg, base) {
301
+ async dispatch(text, cfg, base,
302
+ /** Live mode: `sendText` is the scrubbed text that leaves the machine; `text`
303
+ * stays the original for excerpt anchoring (spans index the original). */
304
+ live) {
152
305
  const model = cfg.model || 'jev-1.13.0';
153
- const timeoutMs = typeof cfg.timeoutMs === 'number' && cfg.timeoutMs > 0 ? cfg.timeoutMs : 1500;
306
+ const timeoutMs = live ? live.timeoutMs : typeof cfg.timeoutMs === 'number' && cfg.timeoutMs > 0 ? cfg.timeoutMs : 1500;
307
+ const liveTag = live ? { live: true } : {};
154
308
  const questions = Object.fromEntries(SHADOW_QUESTIONS.map((q) => [q.rule, { type: 'noul', instructions: q.instructions }]));
155
309
  const ctrl = new AbortController();
156
310
  const timer = setTimeout(() => ctrl.abort(), timeoutMs);
157
311
  const t0 = (this.deps.now ?? Date.now)();
158
- const nc = (reason) => this.write({ kind: 'not-compared', ts: base.ts, sha256: base.sha256, bytes: base.bytes, reason });
312
+ const nc = (reason) => {
313
+ // A live candidate whose caller deadline already passed has its timeout
314
+ // row; a failure landing after that is the same candidate, not a new row.
315
+ if (live?.race.late)
316
+ return null;
317
+ this.write({ kind: 'not-compared', ts: base.ts, sha256: base.sha256, bytes: base.bytes, reason, ...liveTag });
318
+ return null;
319
+ };
159
320
  let tokensIn;
160
321
  let outcome = 'error';
161
322
  let modelServed;
@@ -163,7 +324,7 @@ export class JevSignalShadow {
163
324
  const res = await (this.deps.fetchImpl ?? fetch)(ENDPOINT, {
164
325
  method: 'POST',
165
326
  headers: { Authorization: `Bearer ${this.cachedKey}`, 'Content-Type': 'application/json' },
166
- body: JSON.stringify({ state: text, model, questions }),
327
+ body: JSON.stringify({ state: live ? live.sendText : text, model, questions }),
167
328
  signal: ctrl.signal,
168
329
  });
169
330
  if (!res.ok) {
@@ -171,21 +332,18 @@ export class JevSignalShadow {
171
332
  this.cachedKey = null;
172
333
  this.keyReadAt = (this.deps.now ?? Date.now)();
173
334
  } // re-read after KEY_REREAD_MS, never per message
174
- nc('http-error');
175
- return;
335
+ return nc('http-error');
176
336
  }
177
337
  const json = (await res.json());
178
338
  modelServed = typeof json.model === 'string' ? json.model : undefined;
179
339
  tokensIn = json.usage?.input_tokens;
180
- if (modelServed !== model) {
181
- nc('model-mismatch');
182
- return;
183
- }
340
+ if (modelServed !== model)
341
+ return nc('model-mismatch');
184
342
  const jev = {};
185
343
  const disagree = [];
186
344
  for (const q of SHADOW_QUESTIONS) {
187
345
  const p = json.answers?.[q.rule]?.noul;
188
- if (typeof p !== 'number')
346
+ if (typeof p !== 'number' || !Number.isFinite(p))
189
347
  continue;
190
348
  jev[q.rule] = p;
191
349
  const jevSays = p > POSITIVE_THRESHOLD;
@@ -193,16 +351,36 @@ export class JevSignalShadow {
193
351
  if (jevSays !== detectorSays)
194
352
  disagree.push(q.rule);
195
353
  }
354
+ // A response with no usable probability is a failure, not an answer: it
355
+ // must count toward the breaker instead of resetting it.
356
+ if (live && Object.keys(jev).length === 0)
357
+ return nc('no-answers');
196
358
  outcome = disagree.length ? 'fired' : 'noop';
197
359
  const { signals, ...rowBase } = base;
198
- const retained = disagree.length > 0 && cfg.retainDisagreementExcerpts === true
360
+ // Measurement EXTRAS (retained excerpts, the referee) belong to the shadow:
361
+ // on a live call they run only while the shadow itself is measuring (on,
362
+ // inside its soak), so live mode — which is not soak-bound — never carries
363
+ // them past the window. One check, so every extra is bound the same way.
364
+ const measuring = !live || shadowMeasuring(cfg, (this.deps.now ?? Date.now)());
365
+ const retained = measuring && disagree.length > 0 && cfg.retainDisagreementExcerpts === true
199
366
  ? this.buildExcerpt(text, signals, disagree, cfg, base.ts)
200
367
  : {};
201
- this.write({ kind: 'compared', ...rowBase, jev, ms: (this.deps.now ?? Date.now)() - t0, modelServed, disagree, ...retained });
202
- this.maybeRefer(text, cfg, base.ts, base.sha256, jev, base.detectorSignals);
368
+ // Live: merge, and record which source decided each rule. A late answer
369
+ // (the caller already stopped waiting) is recorded as such, so a row never
370
+ // claims Jev supplied a signal the gate did not actually use.
371
+ const merged = live ? mergeLiveSignals(signals, jev, live.live) : null;
372
+ const liveFields = !live ? {}
373
+ : live.race.late ? { live: true, liveLate: true }
374
+ : { live: true, liveSources: merged.sources };
375
+ this.write({ kind: 'compared', ...rowBase, jev, ms: (this.deps.now ?? Date.now)() - t0, modelServed, disagree, ...liveFields, ...retained });
376
+ if (measuring)
377
+ this.maybeRefer(text, cfg, base.ts, base.sha256, jev, base.detectorSignals);
378
+ return merged ? { signals: merged.signals, jevUsed: merged.jevUsed } : null;
203
379
  }
204
380
  catch (err) {
205
- nc(err?.name === 'AbortError' ? 'timeout' : 'http-error');
381
+ // Everything after the fetch is guarded (merge is pure, excerpt and referee
382
+ // hand-off catch their own errors), so a throw here is the transport.
383
+ return nc(err?.name === 'AbortError' ? 'timeout' : 'http-error');
206
384
  }
207
385
  finally {
208
386
  clearTimeout(timer);
@@ -365,17 +543,92 @@ export class JevSignalShadow {
365
543
  }
366
544
  }
367
545
  }
546
+ /** True while the shadow itself is measuring: enabled and inside its soak window. */
547
+ function shadowMeasuring(cfg, nowMs) {
548
+ const endsAt = cfg.soakEndsAt ? Date.parse(cfg.soakEndsAt) : NaN;
549
+ return cfg.enabled === true && Number.isFinite(endsAt) && nowMs < endsAt;
550
+ }
551
+ /** The plain detector path, for every live fallback. Never throws. */
552
+ function detectorOnly(text) {
553
+ try {
554
+ return { signals: detectGateSignals(text), jevUsed: false };
555
+ }
556
+ catch {
557
+ // @silent-fallback-ok — detectGateSignals already contains its own throws; this only guards the impossible.
558
+ return { signals: [], jevUsed: false };
559
+ }
560
+ }
561
+ /**
562
+ * Merge Jev's answers into the detector signals (docs/specs/jev-signal-live.md).
563
+ * Per kind:
564
+ * - Jev confident YES (p > band.hi): detected. The detector's signal is kept
565
+ * (its spans + sample still anchor the judge); if the detector missed it, a
566
+ * sample-less model signal is added.
567
+ * - Jev confident NO (p < band.lo) against a detector match: the detection
568
+ * STANDS (detected:true, so every B1–B7 rule can still fire on it) and is
569
+ * annotated with Jev's disagreement for the judge to weigh. Jev never hides
570
+ * or overrides an observation; it can only add to or comment on the list.
571
+ * - Unsure or no answer: the detector's output, unchanged.
572
+ * Pure. Output order follows GATE_SIGNAL_KINDS.
573
+ */
574
+ export function mergeLiveSignals(detected, jev, opts = {}) {
575
+ const byKind = new Map();
576
+ for (const sig of detected)
577
+ if (sig.detected)
578
+ byKind.set(sig.kind, sig);
579
+ const signals = [];
580
+ const sources = {};
581
+ for (const kind of GATE_SIGNAL_KINDS) {
582
+ const rule = SHADOW_QUESTIONS.find((q) => q.signalKind === kind)?.rule;
583
+ const det = byKind.get(kind);
584
+ const p = rule ? jev[rule] : undefined;
585
+ const band = rule ? bandFor(rule, opts) : undefined;
586
+ const confident = typeof p === 'number' && Number.isFinite(p) && !!band && (p < band.lo || p > band.hi);
587
+ if (!rule || !confident) {
588
+ if (rule)
589
+ sources[rule] = 'detector-fallback';
590
+ if (det)
591
+ signals.push(det.source ? det : { ...det, source: 'detector' });
592
+ continue;
593
+ }
594
+ const prob = Math.max(0, Math.min(1, p));
595
+ if (prob > band.hi) {
596
+ sources[rule] = 'jev';
597
+ signals.push(det ? { ...det, source: 'jev', modelProbability: prob } : { kind, detected: true, source: 'jev', modelProbability: prob });
598
+ }
599
+ else if (det) {
600
+ sources[rule] = 'disputed';
601
+ signals.push({ ...det, source: 'detector', modelProbability: prob });
602
+ }
603
+ else {
604
+ sources[rule] = 'jev';
605
+ }
606
+ }
607
+ // Nothing Jev-sourced reached the list (all unsure, or only agreed "nothing
608
+ // here") ⇒ hand back the detector list untouched, so the prompt and its
609
+ // promptId stay exactly today's.
610
+ const jevUsed = signals.some((sig) => sig.modelProbability !== undefined);
611
+ return jevUsed ? { signals, sources, jevUsed } : { signals: detected, sources, jevUsed };
612
+ }
368
613
  /**
369
614
  * The production wiring, shared by server.ts and the E2E test so the test
370
615
  * exercises the same config resolution, key lookup and log location the server
371
616
  * uses. The live `intelligence` block wins; the boot config is the fallback.
372
617
  */
373
618
  export function buildJevSignalShadow(opts) {
619
+ const liveBlock = (key) => {
620
+ const intel = opts.readLiveIntelligence();
621
+ const block = intel && typeof intel === 'object' ? intel[key] : undefined;
622
+ return block && typeof block === 'object' ? block : undefined;
623
+ };
374
624
  return new JevSignalShadow({
375
- getConfig: () => {
376
- const intel = opts.readLiveIntelligence();
377
- const block = intel && typeof intel === 'object' ? intel.jevSignalShadow : undefined;
378
- return (block && typeof block === 'object' ? block : opts.bootBlock);
625
+ getConfig: () => (liveBlock('jevSignalShadow') ?? opts.bootBlock),
626
+ getLiveConfig: () => {
627
+ const block = (liveBlock('jevSignalLive') ?? opts.bootLiveBlock ?? {});
628
+ // Dev-gated: an omitted `enabled` is live on a development agent, dark on the fleet.
629
+ // And never while decision-quality recording is off (the route would harden the advisory).
630
+ const gated = resolveDevAgentGate(typeof block.enabled === 'boolean' ? block.enabled : undefined, { developmentAgent: opts.developmentAgent });
631
+ return { ...block, enabled: gated && (opts.recordingLive ?? decisionQualityRecordingLive)() };
379
632
  },
380
633
  readKey: () => {
381
634
  const v = opts.readSecret('typesafe_api_key');