tickmarkr 1.97.0 → 2.1.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/README.md +19 -1
- package/dist/brand.d.ts +28 -0
- package/dist/brand.js +41 -0
- package/dist/cli/commands/compile.js +32 -1
- package/dist/cli/commands/doctor.d.ts +19 -0
- package/dist/cli/commands/doctor.js +59 -0
- package/dist/cli/commands/init.js +5 -2
- package/dist/cli/commands/resume.js +25 -8
- package/dist/cli/commands/run.d.ts +61 -1
- package/dist/cli/commands/run.js +372 -18
- package/dist/cli/commands/status.js +145 -28
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/compile/collateral.d.ts +25 -0
- package/dist/compile/collateral.js +46 -11
- package/dist/compile/native.js +10 -0
- package/dist/config/config.d.ts +1 -0
- package/dist/config/config.js +2 -2
- package/dist/drivers/herdr.d.ts +19 -13
- package/dist/drivers/herdr.js +90 -26
- package/dist/drivers/index.d.ts +5 -1
- package/dist/drivers/index.js +16 -1
- package/dist/drivers/orca.d.ts +189 -0
- package/dist/drivers/orca.js +879 -0
- package/dist/drivers/types.d.ts +2 -0
- package/dist/gates/acceptance.js +17 -7
- package/dist/gates/llm.d.ts +19 -0
- package/dist/gates/llm.js +104 -6
- package/dist/gates/run-gates.d.ts +18 -0
- package/dist/gates/run-gates.js +195 -29
- package/dist/gates/scope.d.ts +9 -1
- package/dist/gates/scope.js +22 -2
- package/dist/graph/graph.d.ts +1 -0
- package/dist/graph/graph.js +19 -2
- package/dist/report/compare.js +17 -2
- package/dist/run/daemon.d.ts +1 -8
- package/dist/run/daemon.js +231 -246
- package/dist/run/environment.d.ts +18 -1
- package/dist/run/environment.js +19 -2
- package/dist/run/journal.d.ts +50 -3
- package/dist/run/journal.js +181 -5
- package/dist/run/protocol.d.ts +4 -4
- package/dist/run/stall.d.ts +30 -0
- package/dist/run/stall.js +173 -0
- package/dist/tui/ink/init-app.js +14 -4
- package/package.json +1 -1
- package/skills/tickmarkr-overseer/SKILL.md +27 -15
|
@@ -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
|
|
29
|
+
export declare function runEnvironment(cfg: TickmarkrConfig, channels: BillingChannel[], health: Record<string, AuthHealth>, capacity?: CapacityProviders): StampedRunEnvironment;
|
package/dist/run/environment.js
CHANGED
|
@@ -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 {
|
|
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
|
}
|
package/dist/run/journal.d.ts
CHANGED
|
@@ -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>;
|
package/dist/run/journal.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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);
|
package/dist/run/protocol.d.ts
CHANGED
|
@@ -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<{
|
package/dist/run/stall.d.ts
CHANGED
|
@@ -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;
|