patchwork-os 1.2.0-beta.3.canary.929 → 1.2.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.
@@ -0,0 +1,93 @@
1
+ /**
2
+ * The third `sweep` gate: a scheduled recipe that used to work and now fails.
3
+ *
4
+ * On 2026-10-14 the hourly privacy heartbeat on the reference install began
5
+ * failing every run (the local model it named had been removed) and nothing
6
+ * surfaced it for 30 hours. `halts` could see it, but `halts` answers a
7
+ * question somebody has to ask; `sweep` is the verb that runs unasked. Every
8
+ * one of the 24 runs on the following day was `error`.
9
+ *
10
+ * ## The rule, decided and deliberately narrow
11
+ *
12
+ * A cron-triggered recipe whose runs were `done` for at least seven
13
+ * consecutive days and whose most recent six or more runs are all `error`
14
+ * is FAILING. The gate is healthy when no recipe is in that state. A recipe
15
+ * with fewer than seven days of history is not eligible — it has no "used to
16
+ * work" to flip from, and a baseline is not a flip, the same distinction the
17
+ * sweep itself draws on its first run.
18
+ *
19
+ * Why both halves. Six errors alone would fire on a brand-new recipe that
20
+ * never worked; that is a defect, but it is not THIS defect, and it is already
21
+ * visible on the run page the moment anyone installs it. Seven clean days
22
+ * alone would fire on one bad hour. Together they describe the failure that
23
+ * went unnoticed: something that had earned the right to be ignored, and then
24
+ * quietly stopped.
25
+ *
26
+ * ## Counts in the snapshot, names in the inbox
27
+ *
28
+ * The snapshot row is counts only — how many recipes are failing, how many
29
+ * were eligible — because `sweep_snapshots.jsonl` must never accumulate
30
+ * operator data. The recipe name and the first failing run time go to ONE
31
+ * inbox note, written when the gate flips and not again while it stays
32
+ * failing, because a note per sweep is a note nobody reads.
33
+ */
34
+ import { type RecipeRun } from "../runLog.js";
35
+ import type { SweepDelta } from "./sweep.js";
36
+ /** Days of consecutive `done` runs a recipe needs before it can flip. */
37
+ export declare const CRON_HALT_MIN_CLEAN_DAYS = 7;
38
+ /** Consecutive `error` runs, most recent first, that constitute the flip. */
39
+ export declare const CRON_HALT_MIN_ERROR_STREAK = 6;
40
+ /** Gate key in the sweep reading. */
41
+ export declare const CRON_HALT_GATE = "cronHalts";
42
+ /** The slice of a run row this gate reads. */
43
+ export type CronRunRow = Pick<RecipeRun, "taskId" | "recipeName" | "trigger" | "status" | "createdAt" | "doneAt">;
44
+ export interface FailingRecipe {
45
+ recipeName: string;
46
+ /** `createdAt` of the first run in the trailing error streak. */
47
+ firstFailureAt: number;
48
+ /** Length of the trailing error streak. */
49
+ errorStreak: number;
50
+ /** Span of the `done` runs immediately before the streak, in ms. */
51
+ cleanSpanMs: number;
52
+ }
53
+ export interface CronHaltAssessment {
54
+ /** Cron recipes with at least `CRON_HALT_MIN_CLEAN_DAYS` of clean history. */
55
+ eligible: number;
56
+ failing: FailingRecipe[];
57
+ }
58
+ /**
59
+ * Pure: decide which cron recipes are in the failing state. Rows may arrive in
60
+ * any order and may include non-cron or non-terminal rows; both are ignored.
61
+ */
62
+ export declare function assessCronHalts(rows: CronRunRow[], _opts: {
63
+ now: number;
64
+ }): CronHaltAssessment;
65
+ /**
66
+ * Terminal cron runs from the run log and its rotation archive, through the
67
+ * same reader `workers shadow` uses: `RecipeRunLog` dedups by `taskId`
68
+ * (never `seq`, #1324) and the archive is merged the same way, so a run that
69
+ * rotated out is still history and a run written twice is still one run.
70
+ * Replay rows are not runs the scheduler started.
71
+ *
72
+ * Returns an empty list when there is no log to read; the caller treats that
73
+ * as a reading it could not take, not as a healthy gate.
74
+ */
75
+ export declare function readCronRuns(dir: string): CronRunRow[];
76
+ /** The log exists (live or archive), so a reading can be taken. */
77
+ export declare function cronRunLogPresent(dir: string): boolean;
78
+ /**
79
+ * Write the inbox note for a flip of the cron-halt gate, and only for a flip:
80
+ * the previous snapshot carried the gate as healthy and this reading does not.
81
+ * A first sweep, a gate the previous snapshot never had, and a gate that is
82
+ * still failing all write nothing. Returns the path written, or undefined.
83
+ *
84
+ * The note carries the same run header a recipe's `file.write` to the inbox
85
+ * gets (`recipe` / `trigger` / `deliveredAt`), so the inbox lists it like any
86
+ * other delivery. There is no `runSeq`: a sweep is not a run.
87
+ */
88
+ export declare function noteCronHaltFlip(opts: {
89
+ delta: SweepDelta;
90
+ assessment: CronHaltAssessment;
91
+ inbox: string;
92
+ now: number;
93
+ }): string | undefined;
@@ -0,0 +1,185 @@
1
+ /**
2
+ * The third `sweep` gate: a scheduled recipe that used to work and now fails.
3
+ *
4
+ * On 2026-10-14 the hourly privacy heartbeat on the reference install began
5
+ * failing every run (the local model it named had been removed) and nothing
6
+ * surfaced it for 30 hours. `halts` could see it, but `halts` answers a
7
+ * question somebody has to ask; `sweep` is the verb that runs unasked. Every
8
+ * one of the 24 runs on the following day was `error`.
9
+ *
10
+ * ## The rule, decided and deliberately narrow
11
+ *
12
+ * A cron-triggered recipe whose runs were `done` for at least seven
13
+ * consecutive days and whose most recent six or more runs are all `error`
14
+ * is FAILING. The gate is healthy when no recipe is in that state. A recipe
15
+ * with fewer than seven days of history is not eligible — it has no "used to
16
+ * work" to flip from, and a baseline is not a flip, the same distinction the
17
+ * sweep itself draws on its first run.
18
+ *
19
+ * Why both halves. Six errors alone would fire on a brand-new recipe that
20
+ * never worked; that is a defect, but it is not THIS defect, and it is already
21
+ * visible on the run page the moment anyone installs it. Seven clean days
22
+ * alone would fire on one bad hour. Together they describe the failure that
23
+ * went unnoticed: something that had earned the right to be ignored, and then
24
+ * quietly stopped.
25
+ *
26
+ * ## Counts in the snapshot, names in the inbox
27
+ *
28
+ * The snapshot row is counts only — how many recipes are failing, how many
29
+ * were eligible — because `sweep_snapshots.jsonl` must never accumulate
30
+ * operator data. The recipe name and the first failing run time go to ONE
31
+ * inbox note, written when the gate flips and not again while it stays
32
+ * failing, because a note per sweep is a note nobody reads.
33
+ */
34
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
35
+ import path from "node:path";
36
+ import { isReplayRun, RecipeRunLog } from "../runLog.js";
37
+ /** Days of consecutive `done` runs a recipe needs before it can flip. */
38
+ export const CRON_HALT_MIN_CLEAN_DAYS = 7;
39
+ /** Consecutive `error` runs, most recent first, that constitute the flip. */
40
+ export const CRON_HALT_MIN_ERROR_STREAK = 6;
41
+ /** Gate key in the sweep reading. */
42
+ export const CRON_HALT_GATE = "cronHalts";
43
+ const DAY_MS = 24 * 60 * 60 * 1000;
44
+ /**
45
+ * Pure: decide which cron recipes are in the failing state. Rows may arrive in
46
+ * any order and may include non-cron or non-terminal rows; both are ignored.
47
+ */
48
+ export function assessCronHalts(rows, _opts) {
49
+ const byRecipe = new Map();
50
+ for (const r of rows) {
51
+ if (r.trigger !== "cron")
52
+ continue;
53
+ if (r.status !== "done" && r.status !== "error")
54
+ continue;
55
+ const list = byRecipe.get(r.recipeName) ?? [];
56
+ list.push(r);
57
+ byRecipe.set(r.recipeName, list);
58
+ }
59
+ let eligible = 0;
60
+ const failing = [];
61
+ for (const [recipeName, list] of byRecipe) {
62
+ list.sort((a, b) => a.createdAt - b.createdAt);
63
+ // Trailing error streak, newest backwards.
64
+ let i = list.length - 1;
65
+ let errorStreak = 0;
66
+ while (i >= 0 && list[i]?.status === "error") {
67
+ errorStreak++;
68
+ i--;
69
+ }
70
+ // The `done` run immediately before the streak, walking back while done.
71
+ const lastClean = i;
72
+ while (i >= 0 && list[i]?.status === "done")
73
+ i--;
74
+ const firstClean = i + 1;
75
+ const cleanSpanMs = lastClean >= firstClean
76
+ ? list[lastClean].createdAt -
77
+ list[firstClean].createdAt
78
+ : 0;
79
+ if (cleanSpanMs < CRON_HALT_MIN_CLEAN_DAYS * DAY_MS)
80
+ continue;
81
+ eligible++;
82
+ if (errorStreak < CRON_HALT_MIN_ERROR_STREAK)
83
+ continue;
84
+ failing.push({
85
+ recipeName,
86
+ firstFailureAt: list[lastClean + 1].createdAt,
87
+ errorStreak,
88
+ cleanSpanMs,
89
+ });
90
+ }
91
+ failing.sort((a, b) => a.recipeName.localeCompare(b.recipeName));
92
+ return { eligible, failing };
93
+ }
94
+ /**
95
+ * Terminal cron runs from the run log and its rotation archive, through the
96
+ * same reader `workers shadow` uses: `RecipeRunLog` dedups by `taskId`
97
+ * (never `seq`, #1324) and the archive is merged the same way, so a run that
98
+ * rotated out is still history and a run written twice is still one run.
99
+ * Replay rows are not runs the scheduler started.
100
+ *
101
+ * Returns an empty list when there is no log to read; the caller treats that
102
+ * as a reading it could not take, not as a healthy gate.
103
+ */
104
+ export function readCronRuns(dir) {
105
+ if (!existsSync(path.join(dir, "runs.jsonl")) &&
106
+ !existsSync(path.join(dir, "runs.jsonl.1"))) {
107
+ return [];
108
+ }
109
+ // A cap large enough that one busy recipe cannot evict another's history
110
+ // before it is read; the default (500) is sized for a dashboard ring.
111
+ const log = new RecipeRunLog({ dir, memoryCap: 50_000 });
112
+ const merged = new Map();
113
+ for (const r of [...log.readArchive(), ...log.query({ limit: 500 })]) {
114
+ merged.set(r.taskId ? `task:${r.taskId}` : `seq:${r.seq}`, r);
115
+ }
116
+ // `query` clamps at 500 rows overall; re-query per cron recipe so a busy
117
+ // install does not truncate the recipe this gate is watching.
118
+ const names = new Set();
119
+ for (const r of merged.values())
120
+ if (r.trigger === "cron")
121
+ names.add(r.recipeName);
122
+ for (const recipe of names) {
123
+ for (const r of log.query({ recipe, trigger: "cron", limit: 500 })) {
124
+ merged.set(r.taskId ? `task:${r.taskId}` : `seq:${r.seq}`, r);
125
+ }
126
+ }
127
+ const out = [];
128
+ for (const r of merged.values()) {
129
+ if (r.trigger !== "cron" || isReplayRun(r))
130
+ continue;
131
+ if (r.status !== "done" && r.status !== "error")
132
+ continue;
133
+ out.push({
134
+ taskId: r.taskId,
135
+ recipeName: r.recipeName,
136
+ trigger: r.trigger,
137
+ status: r.status,
138
+ createdAt: r.createdAt,
139
+ doneAt: r.doneAt,
140
+ });
141
+ }
142
+ return out;
143
+ }
144
+ /** The log exists (live or archive), so a reading can be taken. */
145
+ export function cronRunLogPresent(dir) {
146
+ return (existsSync(path.join(dir, "runs.jsonl")) ||
147
+ existsSync(path.join(dir, "runs.jsonl.1")));
148
+ }
149
+ /**
150
+ * Write the inbox note for a flip of the cron-halt gate, and only for a flip:
151
+ * the previous snapshot carried the gate as healthy and this reading does not.
152
+ * A first sweep, a gate the previous snapshot never had, and a gate that is
153
+ * still failing all write nothing. Returns the path written, or undefined.
154
+ *
155
+ * The note carries the same run header a recipe's `file.write` to the inbox
156
+ * gets (`recipe` / `trigger` / `deliveredAt`), so the inbox lists it like any
157
+ * other delivery. There is no `runSeq`: a sweep is not a run.
158
+ */
159
+ export function noteCronHaltFlip(opts) {
160
+ const flipped = opts.delta.gates.some((g) => g.key === CRON_HALT_GATE && g.regression);
161
+ if (!flipped || opts.assessment.failing.length === 0)
162
+ return undefined;
163
+ const stamp = new Date(opts.now).toISOString();
164
+ const lines = [
165
+ "---",
166
+ "recipe: sweep",
167
+ "trigger: manual",
168
+ `deliveredAt: ${stamp}`,
169
+ "---",
170
+ "",
171
+ "# A scheduled recipe used to work and is now failing",
172
+ "",
173
+ `\`patchwork sweep\` found ${opts.assessment.failing.length} cron recipe(s) that ran \`done\` for at least ${CRON_HALT_MIN_CLEAN_DAYS} days and whose latest ${CRON_HALT_MIN_ERROR_STREAK}+ runs are all \`error\`.`,
174
+ "",
175
+ ];
176
+ for (const f of opts.assessment.failing) {
177
+ lines.push(`- **${f.recipeName}** — first failing run ${new Date(f.firstFailureAt).toISOString()}, ${f.errorStreak} consecutive errors after ${Math.floor(f.cleanSpanMs / DAY_MS)} clean days.`);
178
+ }
179
+ lines.push("", "Run `patchwork halts --window 7d --recipe <name>` for the error messages, then `patchwork recipe doctor`.", "");
180
+ mkdirSync(opts.inbox, { recursive: true, mode: 0o700 });
181
+ const file = path.join(opts.inbox, `sweep-cron-halt-${stamp.replace(/[:.]/g, "-")}.md`);
182
+ writeFileSync(file, lines.join("\n"), { mode: 0o600 });
183
+ return file;
184
+ }
185
+ //# sourceMappingURL=cronHaltGate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cronHaltGate.js","sourceRoot":"","sources":["../../src/maintenance/cronHaltGate.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Collect one `SweepReading` from the five read-only verbs.
2
+ * Collect one `SweepReading` from the five read-only verbs plus the run log.
3
3
  *
4
4
  * Kept apart from `sweep.ts` so the diff stays a pure function a test can drive.
5
5
  * This half touches the filesystem and every subsystem; that half decides what a
@@ -13,6 +13,7 @@
13
13
  * is no version of the snapshot that ever held them. A privacy rule enforced by
14
14
  * a downstream formatter is one refactor away from being lost.
15
15
  */
16
+ import { type CronHaltAssessment } from "./cronHaltGate.js";
16
17
  import { type SweepReading } from "./sweep.js";
17
18
  export interface CollectOptions {
18
19
  /** Ledger directory. Defaults to `$PATCHWORK_HOME`. */
@@ -21,7 +22,18 @@ export interface CollectOptions {
21
22
  expectRunning?: number;
22
23
  /** ms epoch for the reading. Injected so a test is not at the mercy of a clock. */
23
24
  now?: number;
25
+ /**
26
+ * A cron-halt assessment already taken (the dispatcher needs the recipe
27
+ * names for the inbox note, which the reading must not carry). Absent ⇒
28
+ * taken here.
29
+ */
30
+ cronHalts?: CronHaltAssessment;
24
31
  }
32
+ /**
33
+ * Take the cron-halt reading, or undefined when there is no run log to read —
34
+ * an absent log is a reading that could not be taken, not a healthy gate.
35
+ */
36
+ export declare function collectCronHalts(dir: string, now: number): CronHaltAssessment | undefined;
25
37
  /**
26
38
  * Readings that could not be taken are OMITTED, never recorded as zero.
27
39
  *
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Collect one `SweepReading` from the five read-only verbs.
2
+ * Collect one `SweepReading` from the five read-only verbs plus the run log.
3
3
  *
4
4
  * Kept apart from `sweep.ts` so the diff stays a pure function a test can drive.
5
5
  * This half touches the filesystem and every subsystem; that half decides what a
@@ -19,8 +19,23 @@ import { evidenceCoverage } from "../evidenceCoverage.js";
19
19
  import { patchworkHome } from "../patchworkHome.js";
20
20
  import { scanRecipeDir } from "../privacy/undeclaredSteps.js";
21
21
  import { defaultRecipesDir, defaultWorkersDir, validateWorkers, } from "../workers/workersCli.js";
22
+ import { assessCronHalts, CRON_HALT_GATE, cronRunLogPresent, readCronRuns, } from "./cronHaltGate.js";
22
23
  import { readObservations, summarise } from "./prOutcomeLedger.js";
23
24
  import { SWEEP_RV } from "./sweep.js";
25
+ /**
26
+ * Take the cron-halt reading, or undefined when there is no run log to read —
27
+ * an absent log is a reading that could not be taken, not a healthy gate.
28
+ */
29
+ export function collectCronHalts(dir, now) {
30
+ if (!cronRunLogPresent(dir))
31
+ return undefined;
32
+ try {
33
+ return assessCronHalts(readCronRuns(dir), { now });
34
+ }
35
+ catch {
36
+ return undefined;
37
+ }
38
+ }
24
39
  /**
25
40
  * Readings that could not be taken are OMITTED, never recorded as zero.
26
41
  *
@@ -70,6 +85,15 @@ export function collectSweep(opts = {}) {
70
85
  catch {
71
86
  // No workers directory is a legitimate install. Omit rather than zero.
72
87
  }
88
+ // --- gate 3: did a scheduled recipe that used to work stop working? ---
89
+ // Counts only. The names go to an inbox note written by the dispatcher on
90
+ // the FLIP, never into this row (see cronHaltGate.ts).
91
+ const halts = opts.cronHalts ?? collectCronHalts(dir, opts.now ?? Date.now());
92
+ if (halts !== undefined) {
93
+ gates[CRON_HALT_GATE] = halts.failing.length === 0;
94
+ counts["cronHalts.eligibleRecipes"] = halts.eligible;
95
+ counts["cronHalts.failing"] = halts.failing.length;
96
+ }
73
97
  // --- drift: evidence spine ---
74
98
  const ev = evidenceCoverage(dir);
75
99
  for (const l of ev.ledgers) {
@@ -1 +1 @@
1
- {"version":3,"file":"sweepCollect.js","sourceRoot":"","sources":["../../src/maintenance/sweepCollect.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"sweepCollect.js","sourceRoot":"","sources":["../../src/maintenance/sweepCollect.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "patchwork-os",
3
- "version": "1.2.0-beta.3.canary.929",
4
- "description": "Your personal AI runtime, local-first. Patchwork OS gives any AI model a consistent set of tools, YAML recipes, a delegation policy with approval queue, and a durable trace memory — all on your machine, all under your policy.",
3
+ "version": "1.2.0",
4
+ "description": "Your personal AI runtime, local-first. Patchwork OS gives any AI model a consistent set of tools, YAML recipes, a delegation policy with approval queue, and a durable trace memory \u2014 all on your machine, all under your policy.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",