tickmarkr 1.97.0 → 2.0.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.
@@ -4,9 +4,26 @@ export interface RunEnvironment {
4
4
  tickmarkrVersion: string;
5
5
  configHash: string;
6
6
  adapterVersions: Record<string, string>;
7
+ cores?: number;
8
+ forkCap?: number;
9
+ }
10
+ /** A record this run wrote: capacity is present by construction, so "absent" cannot compile. */
11
+ export type StampedRunEnvironment = RunEnvironment & {
12
+ cores: number;
13
+ forkCap: number;
14
+ };
15
+ /**
16
+ * Both capacity readings come through providers so a test can state the host it is describing.
17
+ * The defaults are the same two surfaces production already uses — `availableParallelism` (the one
18
+ * `deriveForkCap` divides) and `resolvedForkCap`, the run-scoped cap every gate shell inherits —
19
+ * so nothing here is a second mechanism, and neither number is hardcoded.
20
+ */
21
+ export interface CapacityProviders {
22
+ cores?: () => number;
23
+ forkCap?: () => number;
7
24
  }
8
25
  export declare const UNKNOWN_ADAPTER_VERSION = "unknown";
9
26
  export declare function tickmarkrVersion(): string;
10
27
  export declare function configHash(cfg: TickmarkrConfig): string;
11
28
  export declare function adapterVersions(channels: BillingChannel[], health: Record<string, AuthHealth>): Record<string, string>;
12
- export declare function runEnvironment(cfg: TickmarkrConfig, channels: BillingChannel[], health: Record<string, AuthHealth>): RunEnvironment;
29
+ export declare function runEnvironment(cfg: TickmarkrConfig, channels: BillingChannel[], health: Record<string, AuthHealth>, capacity?: CapacityProviders): StampedRunEnvironment;
@@ -1,7 +1,18 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { readFileSync } from "node:fs";
3
+ import { availableParallelism } from "node:os";
3
4
  import { dirname, join } from "node:path";
4
5
  import { fileURLToPath } from "node:url";
6
+ import { FORK_CAP_ENV, resolvedForkCap } from "./git.js";
7
+ /**
8
+ * The cap a gate suite ACTUALLY runs under — the same precedence `sh` applies when it builds a child's
9
+ * environment (src/run/git.ts): an operator export of VITEST_MAX_FORKS wins, and only when there is
10
+ * none does the run's own derived cap apply. Recording the derived number while children run at the
11
+ * operator's would describe a host this run never used.
12
+ * resolvedForkCap is an AsyncLocalStorage read: it must run INSIDE the run's fork budget (daemon.ts
13
+ * enters it around the whole run body) or it reports the standalone default instead of this run's cap.
14
+ */
15
+ const defaultForkCap = () => Number(FORK_CAP_ENV in process.env ? process.env[FORK_CAP_ENV] : resolvedForkCap());
5
16
  // An adapter whose version probe failed is recorded, not dropped — "unknown", never a fabricated string.
6
17
  export const UNKNOWN_ADAPTER_VERSION = "unknown";
7
18
  // Same package.json read as src/cli/commands/version.ts (one resolution pattern, two consumers).
@@ -36,6 +47,12 @@ export function adapterVersions(channels, health) {
36
47
  }
37
48
  return out;
38
49
  }
39
- export function runEnvironment(cfg, channels, health) {
40
- return { tickmarkrVersion: tickmarkrVersion(), configHash: configHash(cfg), adapterVersions: adapterVersions(channels, health) };
50
+ export function runEnvironment(cfg, channels, health, capacity = {}) {
51
+ return {
52
+ tickmarkrVersion: tickmarkrVersion(),
53
+ configHash: configHash(cfg),
54
+ adapterVersions: adapterVersions(channels, health),
55
+ cores: (capacity.cores ?? availableParallelism)(),
56
+ forkCap: (capacity.forkCap ?? defaultForkCap)(),
57
+ };
41
58
  }
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { type Assignment } from "../adapters/types.js";
3
3
  import type { TickmarkrConfig } from "../config/config.js";
4
- import { type GateName, type TaskStatus } from "../graph/schema.js";
4
+ import { type GateName, type Task, type TaskStatus } from "../graph/schema.js";
5
5
  import { type ProfileDiscount, type RoutingProfile } from "../route/profile.js";
6
6
  import { type DecisionEventWrite, type TrackedJournalRow } from "./protocol.js";
7
7
  export interface JournalEvent {
@@ -52,6 +52,36 @@ export declare const UNIDENTIFIED = "<unidentified>";
52
52
  * normalized words (see toFinding).
53
53
  */
54
54
  export declare function structuredFindings(gate: string, details: string, _scopeFiles?: string[]): StructuredFinding[];
55
+ export interface PriorRunJournal {
56
+ runId: string;
57
+ events: JournalEvent[];
58
+ }
59
+ export interface PriorFindingEvidence {
60
+ runId: string;
61
+ taskId: string;
62
+ gate: "acceptance" | "review";
63
+ taskContentDigest: string;
64
+ finding: StructuredFinding;
65
+ }
66
+ export interface PriorMergeEvidence {
67
+ runId: string;
68
+ taskId: string;
69
+ commit: string;
70
+ taskContentDigest: string;
71
+ }
72
+ export interface PriorRunEvidence {
73
+ findings: PriorFindingEvidence[];
74
+ merges: PriorMergeEvidence[];
75
+ }
76
+ /**
77
+ * Pure chronological fold over already-bounded journals. Its retirement set is deliberately closed:
78
+ * task completion or any ordinary approval retires prior findings; review-upheld retains them. A
79
+ * dispatch failure, a later run-start, a pass, or a merge is inert. Completion is digest-scoped, so
80
+ * proof for one version of a task cannot clear evidence for another version that reused the id.
81
+ */
82
+ export declare function foldPriorEvidence(runs: readonly PriorRunJournal[]): PriorRunEvidence;
83
+ /** One rendering shared verbatim by compile diagnostics and the fresh worker's feedback seam. */
84
+ export declare function formatPriorFindingEvidence(evidence: PriorFindingEvidence): string;
55
85
  /** Normalized identity of a gate failure: the same defect, seen twice, normalizes to the same bytes. */
56
86
  export declare function normalizeGateFailure(details: string): string;
57
87
  export declare const GATE_FINGERPRINT_CAP = 2;
@@ -82,11 +112,11 @@ export declare function pendingRepairFindings(events: JournalEvent[], taskId: st
82
112
  * a channel it is no longer using, and a later unrelated failure is not parked under a stale reason.
83
113
  */
84
114
  export declare function activeRetryBan(events: JournalEvent[], taskId: string, channel: string): string | undefined;
85
- export declare const PARK_KINDS: readonly ["human-gate", "ladder-exhausted", "attempt-cap", "gate-fail", "quota", "reroute-exhausted", "setup", "stall", "merge-conflict", "tip-moved", "infra", "dispatch"];
115
+ export declare const PARK_KINDS: readonly ["human-gate", "ladder-exhausted", "attempt-cap", "gate-fail", "quota", "reroute-exhausted", "setup", "stall", "merge-conflict", "tip-moved", "infra", "dispatch", "authoring"];
86
116
  export type ParkKind = (typeof PARK_KINDS)[number];
87
117
  export declare const RETRY_MODES: readonly ["resume", "fresh", "repair"];
88
118
  export type RetryMode = (typeof RETRY_MODES)[number];
89
- export declare const WORKER_RESULT_CAUSES: readonly ["provider-death", "stall-timeout", "malformed-trailer", "clean-exit-no-trailer"];
119
+ export declare const WORKER_RESULT_CAUSES: readonly ["provider-death", "dead-channel", "stall-timeout", "malformed-trailer", "clean-exit-no-trailer"];
90
120
  export type WorkerResultCause = (typeof WORKER_RESULT_CAUSES)[number];
91
121
  export declare function isQualityFailureParkKind(kind: ParkKind): boolean;
92
122
  export declare function classifyTaskFailure(taskEvents: JournalEvent[]): ParkKind;
@@ -100,6 +130,8 @@ export declare function classifyWorkerResultCause(opts: {
100
130
  exitCode: number | null;
101
131
  summary: string;
102
132
  timedOut: boolean;
133
+ /** OBS-548: the daemon's dead-channel fast-kill ended this attempt, not the rolling stall window. */
134
+ deadChannel?: boolean;
103
135
  }): WorkerResultCause | undefined;
104
136
  export declare const TelemetryRowSchema: z.ZodObject<{
105
137
  taskId: z.ZodString;
@@ -130,6 +162,7 @@ export declare const TelemetryRowSchema: z.ZodObject<{
130
162
  stall: "stall";
131
163
  "merge-conflict": "merge-conflict";
132
164
  "tip-moved": "tip-moved";
165
+ authoring: "authoring";
133
166
  }>>;
134
167
  tokens: z.ZodCatch<z.ZodOptional<z.ZodObject<{
135
168
  input: z.ZodNumber;
@@ -204,6 +237,10 @@ export declare function readAllTelemetry(repoRoot: string, lastK: number, opts?:
204
237
  runId: string;
205
238
  })[];
206
239
  export declare const RUNS_WINDOW = 50;
240
+ export declare const PRIOR_JOURNAL_RUN_WINDOW = 50;
241
+ export declare function readPriorRunEvidence(repoRoot: string, tasks: readonly Pick<Task, "id" | "goal" | "files" | "acceptance">[], opts?: {
242
+ suppressRunId?: string;
243
+ }): PriorRunEvidence;
207
244
  export declare function readProfileCursor(repoRoot: string): string | undefined;
208
245
  export declare function profileDiscountsPath(repoRoot: string): string;
209
246
  export declare function readProfileDiscounts(repoRoot: string): ProfileDiscount[];
@@ -228,6 +265,16 @@ export declare class Journal {
228
265
  read(): JournalEvent[];
229
266
  readTracked(): TrackedJournalRow[];
230
267
  replayStatuses(): Map<string, TaskStatus>;
268
+ /**
269
+ * OBS-547: the task's latest dispatch IFF a `scope-authoring` event already closed it — the state a
270
+ * crash between that classification and the park that follows it leaves behind. replayResumeState()
271
+ * has already rewound that dispatch, so a resumed gate replay must neither take it back a second time
272
+ * (which erases an EARLIER chargeable attempt) nor attribute the park to the assignment the rewind
273
+ * restored. null ⇒ the latest dispatch is unclassified: today's accounting, unchanged.
274
+ */
275
+ classifiedDispatch(taskId: string): {
276
+ assignment?: Assignment;
277
+ } | null;
231
278
  replayResumeState(): Map<string, ResumeState>;
232
279
  replaySatisfiedGates(): Map<string, GateName>;
233
280
  replayCurrentAttemptGateResults(): Map<string, CurrentAttemptGateReplay>;
@@ -3,7 +3,7 @@ import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync } from
3
3
  import { join } from "node:path";
4
4
  import { z } from "zod";
5
5
  import { channelKey, TokenUsageSchema } from "../adapters/types.js";
6
- import { stateDirName, tickmarkrDir } from "../graph/graph.js";
6
+ import { stateDirName, taskContentDigest, tickmarkrDir } from "../graph/graph.js";
7
7
  import { GATE_NAMES, TIERS } from "../graph/schema.js";
8
8
  import { buildProfile, classify } from "../route/profile.js";
9
9
  import { DecisionEventSchema, trackJournalRows, } from "./protocol.js";
@@ -190,6 +190,91 @@ export function structuredFindings(gate, details, _scopeFiles = []) {
190
190
  }
191
191
  return rows;
192
192
  }
193
+ const findingRows = (event, gate) => {
194
+ if (Array.isArray(event.data.findings)) {
195
+ const rows = event.data.findings.filter((finding) => {
196
+ if (finding === null || typeof finding !== "object")
197
+ return false;
198
+ const row = finding;
199
+ return typeof row.class === "string" && typeof row.path === "string"
200
+ && typeof row.symbol === "string" && typeof row.note === "string"
201
+ && typeof row.fingerprint === "string";
202
+ });
203
+ if (rows.length > 0)
204
+ return rows;
205
+ }
206
+ // Compatibility for hand-written/additive journals: the daemon always emits `findings`, but a
207
+ // digest-bound blocking row still has truthful evidence in details and must not vanish because its
208
+ // optional structured projection was lost. Missing DIGEST remains fail-closed below.
209
+ return typeof event.data.details === "string" ? structuredFindings(gate, event.data.details) : [];
210
+ };
211
+ /**
212
+ * Pure chronological fold over already-bounded journals. Its retirement set is deliberately closed:
213
+ * task completion or any ordinary approval retires prior findings; review-upheld retains them. A
214
+ * dispatch failure, a later run-start, a pass, or a merge is inert. Completion is digest-scoped, so
215
+ * proof for one version of a task cannot clear evidence for another version that reused the id.
216
+ */
217
+ export function foldPriorEvidence(runs) {
218
+ const unresolved = new Map();
219
+ const merges = [];
220
+ const clearTask = (taskId, digest) => {
221
+ for (const [key, evidence] of unresolved) {
222
+ if (evidence.taskId === taskId && (digest === undefined || evidence.taskContentDigest === digest)) {
223
+ unresolved.delete(key);
224
+ }
225
+ }
226
+ };
227
+ for (const run of runs) {
228
+ // task-done immediately precedes merge in daemon journals. This per-run association lets the
229
+ // existing merge row stay byte-compatible while the stamped completion supplies its task identity.
230
+ const completedDigest = new Map();
231
+ for (const event of run.events) {
232
+ const taskId = event.taskId;
233
+ if (!taskId)
234
+ continue;
235
+ if (event.event === "task-approved") {
236
+ if (event.data.release !== REVIEW_UPHELD_RELEASE)
237
+ clearTask(taskId);
238
+ continue;
239
+ }
240
+ if (event.event === "task-done") {
241
+ const digest = event.data.taskContentDigest;
242
+ if (typeof digest === "string") {
243
+ clearTask(taskId, digest);
244
+ completedDigest.set(taskId, digest);
245
+ }
246
+ else {
247
+ completedDigest.delete(taskId);
248
+ }
249
+ continue;
250
+ }
251
+ if (event.event === "merge") {
252
+ const digest = completedDigest.get(taskId);
253
+ const commit = event.data.commit;
254
+ if (digest && typeof commit === "string" && /^[0-9a-f]{40}$/i.test(commit)) {
255
+ merges.push({ runId: run.runId, taskId, commit, taskContentDigest: digest });
256
+ }
257
+ continue;
258
+ }
259
+ const gate = event.data.gate;
260
+ const digest = event.data.taskContentDigest;
261
+ if (event.event !== "gate-result" || event.data.pass !== false || event.data.skipped === true
262
+ || (gate !== "acceptance" && gate !== "review") || typeof digest !== "string")
263
+ continue;
264
+ for (const finding of findingRows(event, gate)) {
265
+ const evidence = {
266
+ runId: run.runId, taskId, gate, taskContentDigest: digest, finding,
267
+ };
268
+ unresolved.set(`${taskId}\0${digest}\0${finding.fingerprint}`, evidence);
269
+ }
270
+ }
271
+ }
272
+ return { findings: [...unresolved.values()], merges };
273
+ }
274
+ /** One rendering shared verbatim by compile diagnostics and the fresh worker's feedback seam. */
275
+ export function formatPriorFindingEvidence(evidence) {
276
+ return `Prior-run EVIDENCE (not a verdict) from ${evidence.runId} ${evidence.gate}: ${evidence.finding.note}`;
277
+ }
193
278
  // v1.85 T3: volatile tokens carry no information about WHY a gate failed — ~663m across 5 runs went to
194
279
  // re-dispatching against failures that differed only in these. Every rule below erases a token PROVEN
195
280
  // to be a diagnostic location or a clock reading; nothing erases a value the failure asserts ABOUT.
@@ -418,13 +503,21 @@ const DispatchAssignmentSchema = z.object({
418
503
  channel: z.enum(["sub", "api"]),
419
504
  tier: z.enum(TIERS),
420
505
  });
506
+ // OBS-547: "authoring" is a scope red every one of whose offenders the collateral map had already
507
+ // named — a missing files[] line, not a worker quality failure. It is deliberately absent from the
508
+ // routing profile's QUALITY_FAIL_PARKS (route/profile.ts): the channel did nothing wrong.
421
509
  export const PARK_KINDS = ["human-gate", "ladder-exhausted", "attempt-cap", "gate-fail", "quota",
422
- "reroute-exhausted", "setup", "stall", "merge-conflict", "tip-moved", "infra", "dispatch"];
510
+ "reroute-exhausted", "setup", "stall", "merge-conflict", "tip-moved", "infra", "dispatch",
511
+ "authoring"];
423
512
  // v1.85 T3: "repair" is a third dispatch mode beside the v1.29 session pair — a fix-only attempt that
424
513
  // carries the failing findings and the diff CONTENT of the work already landed, instead of re-buying
425
514
  // ~20m of onboarding to rediscover them (62 of 68 measured re-dispatches were fresh).
426
515
  export const RETRY_MODES = ["resume", "fresh", "repair"];
427
- export const WORKER_RESULT_CAUSES = ["provider-death", "stall-timeout", "malformed-trailer", "clean-exit-no-trailer"];
516
+ // OBS-548: "dead-channel" is the fourth conflation OBS-53 opened, named. The dead-channel fast-kill
517
+ // concludes a worker while the rolling stall window still has most of its time left, so labelling it
518
+ // stall-timeout points every downstream reader — the repair brief, the consult — at a mechanism that
519
+ // cannot have fired.
520
+ export const WORKER_RESULT_CAUSES = ["provider-death", "dead-channel", "stall-timeout", "malformed-trailer", "clean-exit-no-trailer"];
428
521
  // Status consumes the routing profile's existing quality split directly: verified park kinds classify
429
522
  // to 0, while availability/recovery noise classifies to null. Keep the synthetic row here at the
430
523
  // run↔route seam so presentation code never grows a second list of "bad" park kinds.
@@ -493,6 +586,10 @@ export function classifyWorkerResultCause(opts) {
493
586
  // v1.89 T7: a provider-outage signature is independent of trailer state and has its own remedy.
494
587
  if (PROVIDER_OUTAGE_RE.test(opts.output))
495
588
  return "provider-death";
589
+ // OBS-548: the fast-kill is its own mechanism and outranks every trailer-derived field below —
590
+ // those all bottom out in stall-timeout, which is the one thing this death is NOT.
591
+ if (opts.deadChannel)
592
+ return "dead-channel";
496
593
  // A stall kill is distinguished by its timeout signal, before any trailer-derived result fields.
497
594
  if (opts.timedOut)
498
595
  return "stall-timeout";
@@ -721,6 +818,30 @@ export function readAllTelemetry(repoRoot, lastK, opts = {}) {
721
818
  }
722
819
  // ponytail: fixed 50-run window; promote to a routing.learned.* config knob only if operators need to tune it.
723
820
  export const RUNS_WINDOW = 50;
821
+ // OBS-543/OBS-549 use the same documented recency budget the routing profile already trusts. There is
822
+ // one directory enumeration and one journal read per selected run; both compile history and fresh-run
823
+ // feedback consume this function's folded result, so neither grows an unbounded or second reader.
824
+ export const PRIOR_JOURNAL_RUN_WINDOW = RUNS_WINDOW;
825
+ export function readPriorRunEvidence(repoRoot, tasks, opts = {}) {
826
+ const dir = runsDir(repoRoot);
827
+ if (!existsSync(dir))
828
+ return { findings: [], merges: [] };
829
+ const runIds = readdirSync(dir)
830
+ .filter((runId) => runId.startsWith("run-") && existsSync(join(dir, runId, "journal.jsonl")))
831
+ .sort()
832
+ .slice(-PRIOR_JOURNAL_RUN_WINDOW);
833
+ const folded = foldPriorEvidence(runIds.map((runId) => ({
834
+ runId,
835
+ events: readJsonl(join(dir, runId, "journal.jsonl")),
836
+ })));
837
+ const current = new Map(tasks.map((task) => [task.id, taskContentDigest(task)]));
838
+ return {
839
+ findings: folded.findings.filter((evidence) => evidence.runId !== opts.suppressRunId
840
+ && current.get(evidence.taskId) === evidence.taskContentDigest),
841
+ merges: folded.merges.filter((evidence) => evidence.runId !== opts.suppressRunId
842
+ && current.get(evidence.taskId) === evidence.taskContentDigest),
843
+ };
844
+ }
724
845
  // VIS-03 reset cursor — one trimmed runId line at .tickmarkr/profile-since; absent/empty ⇒ undefined.
725
846
  // Opaque: used ONLY in the runId > comparison above, never a shell or path join beyond .tickmarkr/.
726
847
  export function readProfileCursor(repoRoot) {
@@ -907,9 +1028,37 @@ export class Journal {
907
1028
  // logged attempt 0 two ms after run-resume). Count === max+1 on clean journals and is truthful on
908
1029
  // corrupted ones. tried is the ordered dedup of channelKey(assignment) across dispatches (≡ the
909
1030
  // pre-kill tried list). lastAssignment is the last well-formed dispatched assignment.
1031
+ /**
1032
+ * OBS-547: the task's latest dispatch IFF a `scope-authoring` event already closed it — the state a
1033
+ * crash between that classification and the park that follows it leaves behind. replayResumeState()
1034
+ * has already rewound that dispatch, so a resumed gate replay must neither take it back a second time
1035
+ * (which erases an EARLIER chargeable attempt) nor attribute the park to the assignment the rewind
1036
+ * restored. null ⇒ the latest dispatch is unclassified: today's accounting, unchanged.
1037
+ */
1038
+ classifiedDispatch(taskId) {
1039
+ let outstanding = null;
1040
+ let classified = null;
1041
+ for (const e of this.read()) {
1042
+ if (e.taskId !== taskId)
1043
+ continue;
1044
+ if (e.event === "task-dispatch") {
1045
+ const parsed = DispatchAssignmentSchema.safeParse(e.data.assignment);
1046
+ outstanding = parsed.success ? { assignment: parsed.data } : {}; // malformed: closable, unattributable
1047
+ classified = null;
1048
+ }
1049
+ else if (e.event === "scope-authoring" && outstanding) {
1050
+ classified = outstanding; // a duplicate classification closes nothing more (same rule as the replay)
1051
+ outstanding = null;
1052
+ }
1053
+ }
1054
+ return classified;
1055
+ }
910
1056
  replayResumeState() {
911
1057
  const m = new Map();
912
1058
  const pendingReroute = new Set(); // reroute verdicts not yet cleared by a later dispatch
1059
+ // OBS-547: what the last dispatch ADDED, so a scope-authoring event can take it back. An
1060
+ // unchargeable dispatch must replay as if it never happened — no attempt counted, no channel burned.
1061
+ const lastDispatch = new Map();
913
1062
  const lastReviewFail = new Map(); // OBS-189: newest failed review details per task
914
1063
  for (const e of this.read()) {
915
1064
  if (!e.taskId)
@@ -920,7 +1069,7 @@ export class Journal {
920
1069
  }
921
1070
  if (e.event === "task-dispatch") {
922
1071
  // A subsequent dispatch clears the pending reroute — the reroute was acted on pre-kill.
923
- pendingReroute.delete(e.taskId);
1072
+ const consumedReroute = pendingReroute.delete(e.taskId);
924
1073
  let st = m.get(e.taskId);
925
1074
  if (!st) {
926
1075
  st = { attempts: 0, tried: [] };
@@ -930,16 +1079,43 @@ export class Journal {
930
1079
  const parsed = DispatchAssignmentSchema.safeParse(e.data.assignment);
931
1080
  if (parsed.success) {
932
1081
  const key = channelKey(parsed.data);
933
- if (!st.tried.includes(key))
1082
+ const added = !st.tried.includes(key);
1083
+ if (added)
934
1084
  st.tried.push(key);
1085
+ lastDispatch.set(e.taskId, {
1086
+ ...(added ? { addedKey: key } : {}),
1087
+ prevAssignment: st.lastAssignment,
1088
+ consumedReroute,
1089
+ });
935
1090
  st.lastAssignment = parsed.data;
936
1091
  }
937
1092
  else {
1093
+ lastDispatch.set(e.taskId, { prevAssignment: st.lastAssignment, consumedReroute });
938
1094
  // fail closed: malformed assignment still COUNTS (above) but adds nothing to tried and
939
1095
  // poisons only lastAssignment (a malformed LAST dispatch must not be restored).
940
1096
  st.lastAssignment = undefined;
941
1097
  }
942
1098
  }
1099
+ else if (e.event === "scope-authoring") {
1100
+ // OBS-547: the dispatch this event closes was an authoring defect — the spec is missing a
1101
+ // files[] line and the worker was never at fault. Rewind its accounting so a resume neither
1102
+ // charges the attempt nor treats its channel as burned.
1103
+ // Idempotent: undo the OUTSTANDING dispatch or nothing. A crash between this append and the
1104
+ // park that follows it makes a resume classify again and append a duplicate — the duplicate has
1105
+ // no dispatch left to take back, and an unconditional decrement would erase an EARLIER
1106
+ // chargeable attempt instead (the clamp at zero only hides that when there is no earlier one).
1107
+ const st = m.get(e.taskId);
1108
+ const undo = lastDispatch.get(e.taskId);
1109
+ if (st && undo) {
1110
+ st.attempts = Math.max(0, st.attempts - 1);
1111
+ if (undo.addedKey)
1112
+ st.tried = st.tried.filter((k) => k !== undo.addedKey);
1113
+ st.lastAssignment = undo.prevAssignment;
1114
+ if (undo.consumedReroute)
1115
+ pendingReroute.add(e.taskId);
1116
+ lastDispatch.delete(e.taskId);
1117
+ }
1118
+ }
943
1119
  else if (e.event === "consult-verdict" && e.data.action === "reroute") {
944
1120
  // A reroute bans the in-force channel; retry/decompose/human verdicts ban nothing (D-03).
945
1121
  pendingReroute.add(e.taskId);
@@ -267,6 +267,7 @@ export declare const DecisionEventSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
267
267
  }, z.core.$strict>], "event">;
268
268
  export type DecisionEvent = z.infer<typeof DecisionEventSchema>;
269
269
  export declare const DecisionEventWriteSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
270
+ taskId: z.ZodString;
270
271
  data: z.ZodObject<{
271
272
  attempt: z.ZodNumber;
272
273
  gate: z.ZodEnum<{
@@ -280,9 +281,9 @@ export declare const DecisionEventWriteSchema: z.ZodDiscriminatedUnion<[z.ZodObj
280
281
  }>;
281
282
  evidence: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
282
283
  }, z.core.$strict>;
283
- taskId: z.ZodString;
284
284
  event: z.ZodLiteral<"gate-phase-start">;
285
285
  }, z.core.$strict>, z.ZodObject<{
286
+ taskId: z.ZodString;
286
287
  data: z.ZodObject<{
287
288
  attempt: z.ZodNumber;
288
289
  gate: z.ZodEnum<{
@@ -305,9 +306,9 @@ export declare const DecisionEventWriteSchema: z.ZodDiscriminatedUnion<[z.ZodObj
305
306
  }, z.core.$strict>]>>;
306
307
  evidence: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
307
308
  }, z.core.$strict>;
308
- taskId: z.ZodString;
309
309
  event: z.ZodLiteral<"gate-result">;
310
310
  }, z.core.$strict>, z.ZodObject<{
311
+ taskId: z.ZodString;
311
312
  data: z.ZodObject<{
312
313
  attempt: z.ZodNumber;
313
314
  role: z.ZodEnum<{
@@ -321,9 +322,9 @@ export declare const DecisionEventWriteSchema: z.ZodDiscriminatedUnion<[z.ZodObj
321
322
  vendor: z.ZodOptional<z.ZodString>;
322
323
  evidence: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
323
324
  }, z.core.$strict>;
324
- taskId: z.ZodString;
325
325
  event: z.ZodLiteral<"role-invocation-start">;
326
326
  }, z.core.$strict>, z.ZodObject<{
327
+ taskId: z.ZodString;
327
328
  data: z.ZodObject<{
328
329
  attempt: z.ZodNumber;
329
330
  role: z.ZodEnum<{
@@ -351,7 +352,6 @@ export declare const DecisionEventWriteSchema: z.ZodDiscriminatedUnion<[z.ZodObj
351
352
  }, z.core.$strict>>;
352
353
  unmeteredReason: z.ZodOptional<z.ZodString>;
353
354
  }, z.core.$strict>;
354
- taskId: z.ZodString;
355
355
  event: z.ZodLiteral<"role-invocation-terminal">;
356
356
  }, z.core.$strict>, z.ZodObject<{
357
357
  data: z.ZodObject<{
@@ -6,6 +6,36 @@ export interface StallProgressSample {
6
6
  seedSubmitted?: boolean;
7
7
  contextTokens?: number;
8
8
  }
9
+ export declare function harvestCpuFlatWindowMs(resolutionMs: number): number;
10
+ /** Test seam — pin the quantization-aware flat window without changing production policy. */
11
+ export declare function setHarvestCpuFlatMsForTests(ms: number): void;
12
+ export declare function resetHarvestCpuFlatMsForTests(): void;
13
+ export declare function workerTreeCpuMs(marker: string, cwd: string): Promise<{
14
+ ms: number;
15
+ resolutionMs: number;
16
+ } | undefined>;
17
+ export declare class WorkerTreeCpuAccountant {
18
+ private marker;
19
+ private cwd;
20
+ private active;
21
+ private loop;
22
+ private live;
23
+ private totalMs;
24
+ private gaps;
25
+ private consecutiveGaps;
26
+ private latest;
27
+ constructor(marker: string, cwd: string);
28
+ private sample;
29
+ start(): Promise<void>;
30
+ read(): {
31
+ cpu: {
32
+ ms: number;
33
+ resolutionMs: number;
34
+ } | undefined;
35
+ gaps: number;
36
+ };
37
+ stop(): Promise<void>;
38
+ }
9
39
  export declare const NUDGEABLE_ADAPTERS: Set<string>;
10
40
  export declare const QUOTA_BANNER_TAIL_ROWS = 12;
11
41
  export declare function stallSnapshotTail(text: string, rows?: number): string;