@saadnvd1/agent-os 0.3.466 → 0.3.467

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 CHANGED
@@ -908,10 +908,13 @@ it may do on its own, and the lines that always come back to you as asks.
908
908
  `BLOCKED:` or waiting, the diff stays in scope (no secrets, not only
909
909
  lockfiles, nothing outside the repo, within the card) and its stack parent
910
910
  has merged, and the PR body's Code review section names that commit. `land` judges each item again at its own head right before
911
- merging it. The second failure of a gate or a repo with no CI goes to you
912
- as an ask, and the orchestrator stops merging that task. Failures count per
913
- task and gate: a review that blocks counts when its verdict arrives, once
914
- per commit, whether or not `sign_off` ever reads it.
911
+ merging it. A gate failing again on a different commit, or a repo with no
912
+ CI, goes to you as an ask, and the orchestrator stops merging that task.
913
+ A failure counts once per task, gate and head commit, however many times
914
+ and from wherever that commit is judged (the armed watcher, CI finishing,
915
+ `sign_off`, `done`, `land`); a pass in between doesn't reset it. A review
916
+ that blocks counts when its verdict arrives, whether or not `sign_off`
917
+ ever reads it.
915
918
  - **Arm to merge:** `arm` (or **Arm merge** on a task's row) makes AgentOS
916
919
  merge the task's PR itself the moment every `sign_off` gate passes at its
917
920
  head, starting the review once CI is green, so nothing polls `sign_off`.
@@ -1327,6 +1327,37 @@ const migrations: Migration[] = [
1327
1327
  installChangeTriggers(db, TABLES_WATCHED_58);
1328
1328
  },
1329
1329
  },
1330
+ {
1331
+ id: 59,
1332
+ name: "gate_failure_commits",
1333
+ up: (db) => {
1334
+ // Each head a gate failed on, once: a gate's failure counts once per
1335
+ // commit however many times that commit is judged (the armed watcher,
1336
+ // CI finishing, sign_off, done, land), and the second commit it fails
1337
+ // on goes to Saad. The key is what makes it once, across restarts and
1338
+ // concurrent evaluations.
1339
+ db.exec(`
1340
+ CREATE TABLE IF NOT EXISTS orchestrator_gate_failure_commits (
1341
+ session_id TEXT NOT NULL,
1342
+ gate TEXT NOT NULL,
1343
+ sha TEXT NOT NULL,
1344
+ workspace_id TEXT NOT NULL,
1345
+ reason TEXT,
1346
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1347
+ PRIMARY KEY (session_id, gate, sha)
1348
+ );
1349
+ INSERT OR IGNORE INTO orchestrator_gate_failure_commits (session_id, gate, sha, workspace_id, reason)
1350
+ SELECT session_id, gate, last_sha, workspace_id, last_reason
1351
+ FROM orchestrator_gate_failures WHERE last_sha IS NOT NULL;
1352
+ CREATE TRIGGER IF NOT EXISTS gate_failures_clear_commits
1353
+ AFTER DELETE ON orchestrator_gate_failures
1354
+ BEGIN
1355
+ DELETE FROM orchestrator_gate_failure_commits
1356
+ WHERE session_id = OLD.session_id AND gate = OLD.gate;
1357
+ END;
1358
+ `);
1359
+ },
1360
+ },
1330
1361
  ];
1331
1362
 
1332
1363
  // `upTo`: stop after this id (tests that start from an older database).
@@ -229,8 +229,8 @@ async function mergeOne(
229
229
  }
230
230
  const seen = `${pr.head}:${failures(verdict.text)}`;
231
231
  if (task.merge_armed_seen === seen) return;
232
- // Counted once, as sign_off would count it: a second failure of a gate
233
- // goes to Saad.
232
+ // Counted as sign_off would count it: once per head (the database keys
233
+ // it), and a gate failing on a second commit goes to Saad.
234
234
  if (!stillArmed(workspaceId, task.id)) return;
235
235
  const counted = await deps.judge(workspaceId, task);
236
236
  if (counted.ok) return merge(workspaceId, task, counted, deps);
@@ -54,7 +54,7 @@ When the base moves and a task's PR conflicts with it, AgentOS rebases it onto t
54
54
 
55
55
  Tasks are woken by AgentOS itself when a check fails on their PR's head (with the failing step and the log around the error) and when a collaborator reviews or comments on their PR, and you get one "task X woken: ..." line for each. Don't message a task about the same failure or review; act only if it doesn't fix it.
56
56
 
57
- "Not yet" (CI running or settling, no review of this commit yet) is not a failure: wait for the event and try again. A failure counts against the task: a review that blocks counts when its verdict arrives, once per commit, and the second failure of the same gate goes to Saad, with a note in your chat, and from then on only he merges or drops that task. Don't retry it; say so in your chat and move on. ${mergeApprovalsOn() ? APPROVALS_ON : APPROVALS_OFF} \`land\` merges a whole stack only if every open item passes the same gates, and judges each one again at its own head right before merging it.
57
+ "Not yet" (CI running or settling, no review of this commit yet) is not a failure: wait for the event and try again. A failure counts against the task once per commit, whichever path judges that commit and however often (a review that blocks counts when its verdict arrives); the same gate failing again on a different commit goes to Saad, with a note in your chat, and from then on only he merges or drops that task. Don't retry it; say so in your chat and move on. ${mergeApprovalsOn() ? APPROVALS_ON : APPROVALS_OFF} \`land\` merges a whole stack only if every open item passes the same gates, and judges each one again at its own head right before merging it.
58
58
 
59
59
  ## Brakes
60
60
 
@@ -6,7 +6,6 @@ import { AskRefused, raiseAsk, workSubject } from "./asks";
6
6
  import {
7
7
  failureOf,
8
8
  markEscalated,
9
- PER_COMMIT_GATES,
10
9
  recordFailure,
11
10
  type GateOutcome,
12
11
  } from "./gates";
@@ -20,7 +19,8 @@ export function escalate(
20
19
  url: string,
21
20
  sha: string | null = null
22
21
  ): string {
23
- if (!failureOf(task.id, gate)) recordFailure(workspaceId, task.id, gate, why);
22
+ if (!failureOf(task.id, gate))
23
+ recordFailure(workspaceId, task.id, gate, why, sha);
24
24
  markEscalated(task.id, gate);
25
25
  let created = false;
26
26
  try {
@@ -50,8 +50,9 @@ export function escalate(
50
50
  return `Escalated to Saad: ${why}. It's on his asks list; his answer reaches you as an event. Don't retry; say so in your chat.`;
51
51
  }
52
52
 
53
- // Counts a gate's failure against the task; the second failure of the same
54
- // gate goes to Saad. A review or scope verdict counts once per commit.
53
+ // Counts a gate's failure on this head against the task; the same gate
54
+ // failing on a second, different head goes to Saad. The same head judged
55
+ // again, from any path, isn't another failure.
55
56
  export function countFailure(
56
57
  workspaceId: string,
57
58
  task: { id: string; name: string },
@@ -59,12 +60,11 @@ export function countFailure(
59
60
  url: string,
60
61
  sha: string
61
62
  ): { line: string; escalated: boolean } {
62
- const key = PER_COMMIT_GATES.includes(o.gate) ? sha : null;
63
- const n = recordFailure(workspaceId, task.id, o.gate, o.reason ?? "", key);
63
+ const n = recordFailure(workspaceId, task.id, o.gate, o.reason ?? "", sha);
64
64
  const line = `- ${o.gate} failed (${n === 1 ? "first" : "again"}): ${o.reason}`;
65
65
  if (n < 2 || failureOf(task.id, o.gate)?.escalated_at)
66
66
  return { line, escalated: false };
67
- const why = `the ${o.gate} gate failed twice: ${o.reason}`;
67
+ const why = `the ${o.gate} gate failed on two different commits: ${o.reason}`;
68
68
  return {
69
69
  line: `${line}\n ${escalate(workspaceId, task, o.gate, why, url, sha)}`,
70
70
  escalated: true,
@@ -2,10 +2,12 @@
2
2
  * The merge gates, ported from dispatch orchestrate. Each one passes, waits
3
3
  * (not ready yet: CI running or settling, no review of this commit yet),
4
4
  * fails, or goes straight to Saad (no CI at all). Only a failure counts
5
- * against the task, and the second failure of the same gate goes to Saad
6
- * instead of being retried. The count is per task and gate: a review that
7
- * blocks counts when its verdict arrives, whether or not sign_off ever
8
- * reads it, and reading the same commit's verdict again isn't another.
5
+ * against the task, and a gate failing again on a different commit goes to
6
+ * Saad instead of being retried. A failure counts once per task, gate and
7
+ * head commit, whichever path judged it (the armed watcher, CI finishing,
8
+ * sign_off, done, land, a review's verdict arriving): judging the same head
9
+ * again is never another failure. A pass doesn't reset the count, so a gate
10
+ * that fails on A, passes on B and fails on C has failed twice.
9
11
  */
10
12
 
11
13
  import { db } from "../db";
@@ -170,12 +172,10 @@ export interface FailureRow {
170
172
  last_sha: string | null;
171
173
  }
172
174
 
173
- // The gates whose failure is a verdict stored against one commit: the same
174
- // verdict read again is the same failure.
175
- export const PER_COMMIT_GATES: readonly string[] = ["review", "scope"];
176
-
177
- // Counts one failure; returns the count now. With `sha`, a failure of the
178
- // commit last counted for this gate isn't counted again.
175
+ // Counts one failure; returns the count now. With `sha`, a failure is
176
+ // counted once per commit: the (task, gate, sha) key in
177
+ // orchestrator_gate_failure_commits decides, so neither a restart nor two
178
+ // evaluations at once count the same head twice.
179
179
  export function recordFailure(
180
180
  workspaceId: string,
181
181
  sessionId: string,
@@ -183,14 +183,34 @@ export function recordFailure(
183
183
  reason: string,
184
184
  sha: string | null = null
185
185
  ): number {
186
+ return db
187
+ .transaction(countOnce)
188
+ .immediate(workspaceId, sessionId, gate, reason, sha);
189
+ }
190
+
191
+ function countOnce(
192
+ workspaceId: string,
193
+ sessionId: string,
194
+ gate: string,
195
+ reason: string,
196
+ sha: string | null
197
+ ): number {
198
+ const fresh =
199
+ sha === null ||
200
+ db
201
+ .prepare(
202
+ `INSERT OR IGNORE INTO orchestrator_gate_failure_commits (session_id, gate, sha, workspace_id, reason)
203
+ VALUES (?, ?, ?, ?, ?)`
204
+ )
205
+ .run(sessionId, gate, sha, workspaceId, reason).changes > 0;
186
206
  db.prepare(
187
207
  `INSERT INTO orchestrator_gate_failures (session_id, gate, workspace_id, count, last_reason, last_sha)
188
- VALUES (?, ?, ?, 1, ?, ?)
189
- ON CONFLICT(session_id, gate) DO UPDATE SET
190
- count = count + CASE WHEN excluded.last_sha IS NOT NULL AND last_sha IS excluded.last_sha THEN 0 ELSE 1 END,
191
- last_reason = excluded.last_reason,
192
- last_sha = excluded.last_sha`
193
- ).run(sessionId, gate, workspaceId, reason, sha);
208
+ VALUES (?, ?, ?, 1, ?, ?)
209
+ ON CONFLICT(session_id, gate) DO UPDATE SET
210
+ count = count + ?,
211
+ last_reason = excluded.last_reason,
212
+ last_sha = excluded.last_sha`
213
+ ).run(sessionId, gate, workspaceId, reason, sha, fresh ? 1 : 0);
194
214
  return failureOf(sessionId, gate)!.count;
195
215
  }
196
216
 
@@ -3,7 +3,7 @@
3
3
  * commit that was judged. With merge approvals on (merge-approvals.ts),
4
4
  * anything touching CI config, build and hook scripts, agent config, deploys
5
5
  * or secrets handling goes to Saad whatever the gates say. A repository with
6
- * no CI and a gate failing twice on a task go to him either way. Once a task
6
+ * no CI and a gate failing on two different commits go to him either way. Once a task
7
7
  * is with Saad the orchestrator doesn't merge it; he signs it off or drops it.
8
8
  */
9
9
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saadnvd1/agent-os",
3
- "version": "0.3.466",
3
+ "version": "0.3.467",
4
4
  "description": "Self-hosted web UI for managing Claude Code sessions",
5
5
  "bin": {
6
6
  "agent-os": "scripts/agent-os"