mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-12 18:48:31 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f44641637 | ||
|
|
b1460a11d9 | ||
|
|
d4855cbbec | ||
|
|
b5df49407c |
No files matched your search
@@ -423,7 +423,7 @@ Each startup file and action specifies when it applies:
|
||||
- a launch replaces it, as launches always did, and names it (`guidance_block_refreshed_unrecorded`);
|
||||
- an exact resume or a handover leaves it, and names it as possibly stale (`guidance_block_may_be_stale`).
|
||||
|
||||
**A file keeps one copy of each distinct block text, and nothing you wrote is removed.**
|
||||
**A file keeps one copy of each distinct block text, and tidying the copies never removes a text you wrote.**
|
||||
- **Touched markers:** a marker an editor or formatter rewrapped, with spaces or line breaks inside the comment, still reads as its block, and the next write puts it back in OpenRig's form.
|
||||
- **Collected copies:** a copy that repeats an earlier one exactly is removed. Beside a copy you edited, OpenRig's own unedited copies are removed too, and every different edited copy stays (`guidance_block_copies_removed`).
|
||||
- **A marker that lost its pair:** OpenRig can't tell where that block starts or ends, so it adds no copy beside it and names it (`guidance_block_markers_damaged`). Put back the missing marker or remove the partial block, and OpenRig's next write treats that block like any other. `rig package install` leaves such a file as it is, names it in its result (`notes`), and installs the rest. A marker without its pair beside a complete block is named too (`guidance_block_marker_unpaired`), and the block is written.
|
||||
|
||||
@@ -143,7 +143,8 @@ export function downCommand(depsOverride?: DownDeps): Command {
|
||||
const cmd = new Command("down").description("Tear down a rig")
|
||||
.addHelpText("after", `
|
||||
rig down stops every agent OpenRig started in the rig: their sessions end, and any work in progress stops
|
||||
with them. A session OpenRig didn't start (adopted, bound or discovered) is left running, and rig down names it.
|
||||
with them. For a community harness only its pane stops: its own processes may keep running, and rig down
|
||||
names it. A session OpenRig didn't start (adopted, bound or discovered) is left running, and rig down names it.
|
||||
Before running it, check who is working (rig ps --nodes --rig <rig>) and that the person asked for it;
|
||||
don't stop a team you didn't start unless asked. OpenRig snapshots the rig first (best-effort), and
|
||||
rig up <rig> --existing brings it back, resuming each seat's conversation where its harness allows. With
|
||||
|
||||
@@ -14,6 +14,12 @@ import { runFrontDoor, resolveTuiPath, probeFrontDoor, type FrontDoorIo } from "
|
||||
import { diagnoseRuntimePosture, observeClaudePermission } from "../../daemon/src/domain/permission-drift.js";
|
||||
import { DaemonClient, DaemonConnectionError, DaemonResponseError, DaemonTimeoutError } from "../src/client.js";
|
||||
|
||||
// The compiled front door's spawns guard against a hang (the piped TUI never exiting). A hang never ends, so a budget well
|
||||
// past a slow start still catches one: loaded CI runners took just over 5 s to start the CLI, and 30 s leaves room.
|
||||
const HANG_LIMIT_MS = 30_000;
|
||||
/** Those tests outlive their spawn's budget, so the spawn's own result is what reports a hang. */
|
||||
const HANG_TEST_TIMEOUT_MS = HANG_LIMIT_MS + 10_000;
|
||||
|
||||
// A GUARANTEED-refused local port (bind an ephemeral port, capture it, close → connecting refuses).
|
||||
// A REAL socket refusal, not a hand-built error — buys the actual Node/undici rejection shape.
|
||||
async function refusedPort(): Promise<number> {
|
||||
@@ -487,7 +493,7 @@ describe("PUBLIC bin ownership (guard finding 1 — the wrapper is the real fron
|
||||
const started = Date.now();
|
||||
const result = spawnSync(process.execPath, [binWrapper], {
|
||||
input: "x\n",
|
||||
timeout: 5000,
|
||||
timeout: HANG_LIMIT_MS,
|
||||
encoding: "utf-8",
|
||||
});
|
||||
expect(result.status, JSON.stringify({
|
||||
@@ -496,13 +502,13 @@ describe("PUBLIC bin ownership (guard finding 1 — the wrapper is the real fron
|
||||
stdout: result.stdout?.slice(-4096), stderr: result.stderr?.slice(-4096),
|
||||
})).toBe(0);
|
||||
expect(`${result.stderr}${result.stdout}`).toMatch(/Usage: rig/);
|
||||
});
|
||||
}, HANG_TEST_TIMEOUT_MS);
|
||||
|
||||
it.skipIf(!existsSync(binWrapper))("PUBLIC bin subcommands are untouched (rig --version via wrapper)", () => {
|
||||
const result = spawnSync(process.execPath, [binWrapper, "--version"], { timeout: 5000, encoding: "utf-8" });
|
||||
const result = spawnSync(process.execPath, [binWrapper, "--version"], { timeout: HANG_LIMIT_MS, encoding: "utf-8" });
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.stdout.trim()).toMatch(/^\d+\.\d+\.\d+/);
|
||||
});
|
||||
}, HANG_TEST_TIMEOUT_MS);
|
||||
});
|
||||
|
||||
describe("script-safety integration (the compiled front door)", () => {
|
||||
@@ -511,22 +517,22 @@ describe("script-safety integration (the compiled front door)", () => {
|
||||
const started = Date.now();
|
||||
const result = spawnSync(process.execPath, [cliEntry], {
|
||||
input: "x\n",
|
||||
timeout: 5000,
|
||||
timeout: HANG_LIMIT_MS,
|
||||
encoding: "utf-8",
|
||||
env: { ...process.env, OPENRIG_URL: "", OPENRIG_PORT: "" },
|
||||
});
|
||||
expect(Date.now() - started).toBeLessThan(5000);
|
||||
expect(Date.now() - started).toBeLessThan(HANG_LIMIT_MS);
|
||||
// pre-slice-17 baseline: commander's clean-help path prints usage and
|
||||
// exits 0 — the front door PRESERVES the piped path byte-for-byte
|
||||
expect(result.status).toBe(0);
|
||||
expect(`${result.stderr}${result.stdout}`).toMatch(/Usage: rig/);
|
||||
expect(`${result.stderr}${result.stdout}`).not.toMatch(/\n\s+at /);
|
||||
});
|
||||
}, HANG_TEST_TIMEOUT_MS);
|
||||
|
||||
it.skipIf(!existsSync(cliEntry))("`rig --help` still exits 0 with the full usage (front-door regression)", () => {
|
||||
const result = spawnSync(process.execPath, [cliEntry, "--help"], { timeout: 5000, encoding: "utf-8" });
|
||||
const result = spawnSync(process.execPath, [cliEntry, "--help"], { timeout: HANG_LIMIT_MS, encoding: "utf-8" });
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.stdout).toMatch(/Usage: rig/);
|
||||
expect(result.stdout).toMatch(/daemon/);
|
||||
});
|
||||
}, HANG_TEST_TIMEOUT_MS);
|
||||
});
|
||||
@@ -14,6 +14,13 @@ interface Result {
|
||||
stderr: string;
|
||||
}
|
||||
|
||||
// A create waits on nothing (each claim is one exclusive file create), so one still running after a minute is hung; a
|
||||
// slow start on a loaded CI runner took over 15 s. A batch's creates run together, so the batch tests allow each batch the
|
||||
// full limit plus one for setup and checks, and a hung create there is reported by its own limit. A single-create test
|
||||
// keeps the suite's default timeout, which reports a hang first.
|
||||
const CREATE_LIMIT_MS = 60_000;
|
||||
const batchesTimeout = (batches: number) => (batches + 1) * CREATE_LIMIT_MS;
|
||||
|
||||
describe("public slice creation ordinal claims", () => {
|
||||
let root: string;
|
||||
let workspace: string;
|
||||
@@ -49,7 +56,7 @@ describe("public slice creation ordinal claims", () => {
|
||||
];
|
||||
try {
|
||||
const result = await promisify(execFile)(process.execPath, args, {
|
||||
env: ownedEnv, encoding: "utf8", timeout: 15000,
|
||||
env: ownedEnv, encoding: "utf8", timeout: CREATE_LIMIT_MS,
|
||||
});
|
||||
return { exitCode: 0, ...result };
|
||||
} catch (error) {
|
||||
@@ -95,7 +102,7 @@ describe("public slice creation ordinal claims", () => {
|
||||
expect(fs.readdirSync(path.join(mission, "slices"), { withFileTypes: true }).filter((entry) => entry.isDirectory())).toHaveLength(33);
|
||||
expect(fs.readFileSync(path.join(mission, "README.md"), "utf8")).toBe(original);
|
||||
expect(fs.existsSync(path.join(mission, ".openrig-slice-create-lock"))).toBe(false);
|
||||
}, 30000);
|
||||
}, batchesTimeout(4));
|
||||
|
||||
it("twenty eight-process manifest-backed batches preserve the admitted concurrency contract", async () => {
|
||||
const ref = "slices/01-existing/slice.yaml";
|
||||
@@ -144,7 +151,7 @@ describe("public slice creation ordinal claims", () => {
|
||||
expect(new Set(successful(results).map((slice) => slice.id)).size).toBe(160);
|
||||
expect(fs.readFileSync(path.join(unrelated, "PROOF.md"), "utf8")).toBe("# unrelated authored proof\n");
|
||||
expect(fs.existsSync(path.join(mission, ".openrig-slice-create-lock"))).toBe(false);
|
||||
}, 180000);
|
||||
}, batchesTimeout(20));
|
||||
|
||||
it("a later create repairs only pending complete registrations and preserves authored membership", async () => {
|
||||
const ref = "slices/01-existing/slice.yaml";
|
||||
|
||||
+4
-4
@@ -163,8 +163,8 @@ is not a restore request.
|
||||
1. **Check for a hold first.** Read the restore request, the per-seat instruction file
|
||||
(`<OPENRIG_HOME>/compaction/post-compact-extra/<session>.md`, named after your full session, such as
|
||||
`dev-impl@my-rig` with `.md` added) when it exists, the newest row or message from whoever routes your work, and any hold
|
||||
from the authority above them. A hold, a release order or an operator's own restore map overrides the default
|
||||
order below. Before any write, also run `rig whoami --json` and `rig queue whoami`.
|
||||
from the authority above them. A hold you verified as live, a release order or an operator's own restore map overrides
|
||||
the default order below for the work it covers; do the unaffected work. Before any write, also run `rig whoami --json` and `rig queue whoami`.
|
||||
2. **Name your class and state its read budget** before reading (see "Two restore classes"), with a
|
||||
checkpoint at each step below. The budget exists so that the restore leaves room for the work it was
|
||||
restored to do. Step 5 takes a high-context seat to its Tier 2 line; the extra sources under "Two restore classes" apply only
|
||||
@@ -218,7 +218,7 @@ The audit message asks for a read-depth table and tells you not to conserve toke
|
||||
- where you are in time: what earlier windows established, what the last window did, what is true now;
|
||||
- the connections you have rebuilt, in a few lines;
|
||||
- the connections you could not rebuild, and where you would look;
|
||||
- the next authorized step and who authorizes it. If a hold stands, the next step is waiting.
|
||||
- the next authorized step and who authorizes it. If a hold you verified as live covers it, wait on that; do the unaffected work.
|
||||
|
||||
The next restore map links this note, which keeps the chain unbroken.
|
||||
|
||||
@@ -238,7 +238,7 @@ unaffected work.
|
||||
- Do not launch a fresh session in place of restoring.
|
||||
- An honest `PARTIAL` with its reason is a correct outcome. Claiming coverage you did not reach is the
|
||||
failure.
|
||||
- Do not resume task work until the read-depth table and the reconnect note exist.
|
||||
- Write the read-depth table and the reconnect note as you resume; don't hold unaffected work for them.
|
||||
|
||||
## Failure modes
|
||||
|
||||
|
||||
@@ -1103,8 +1103,9 @@ export class TmuxAdapter {
|
||||
// A paste the guard proves it left unsubmitted (#2429) is submitted first, under this lease, once this send's own
|
||||
// checks pass; they then aren't run a second time.
|
||||
beforePaste: async () => {
|
||||
// This send's own checks can wait (a conversation lookup, an input capture), so they run first, and the
|
||||
// guard's checks run last (#3076). A check that refuses leaves a leftover's record for the next delivery.
|
||||
// This send's own checks can wait (a conversation lookup, an input capture), so they run first: the guard's
|
||||
// read of the input is then the last wait before the paste (#3076). A check that refuses leaves a leftover's
|
||||
// record for the next delivery.
|
||||
await options?.beforePaste?.();
|
||||
const checks = async () => { beforeWrite(); beforeInput?.(); };
|
||||
// The synchronous checks run again just before the key: the guard's re-read awaits after `checks`.
|
||||
|
||||
@@ -105,6 +105,7 @@ import { armQueueWait, backOffQueueWait, refreshQueueWaits, evaluateQueueWait, r
|
||||
import { parseHumanQuestions, unansweredQuestions, describeTypedReplyPlacement, formatHumanAnswers, humanAnswerText, type HumanQuestion, type HumanAnswers, type TypedHumanReply, type RecordHumanAnswerResult } from "./human-questions.js";
|
||||
import { slackMicros } from "./gateway/slack/state-store.js";
|
||||
import { askBackRoute, RUNNER_CLOSE_WAITS, terminalAnswerMeaning } from "./receiver-context.js";
|
||||
import { runSyncSite, runSyncSiteWhenSlow } from "./sync-site-wrap.js";
|
||||
|
||||
/** A direct ask's answer row ID: from the ask, or from the ask and the park episode its answer belongs to. */
|
||||
function humanAnswerId(askId: string, episodeKey?: string): string {
|
||||
@@ -821,14 +822,14 @@ export function createLinkedSuccessorLookup(db: Database.Database, sourceQitemId
|
||||
const startedAt = performance.now();
|
||||
const map = new Map<string, Array<{ qitemId: string; destination: string | null }>>();
|
||||
const ids = JSON.stringify([...sources]);
|
||||
const rows = db.prepare(
|
||||
const rows = runSyncSite("queue.linked_successors.build", () => db.prepare(
|
||||
`SELECT s.qitem_id, s.destination_session, s.handed_off_from AS source FROM queue_items s
|
||||
WHERE s.handed_off_from IN (SELECT value FROM json_each(?))
|
||||
UNION
|
||||
SELECT s.qitem_id, s.destination_session, j.value AS source FROM queue_items s, json_each(s.chain_of_record) j
|
||||
WHERE s.chain_of_record IS NOT NULL AND json_valid(s.chain_of_record)
|
||||
AND j.value IN (SELECT value FROM json_each(?))`,
|
||||
).all(ids, ids) as Array<{ qitem_id: string; destination_session: string | null; source: string }>;
|
||||
).all(ids, ids)) as Array<{ qitem_id: string; destination_session: string | null; source: string }>;
|
||||
for (const r of rows) {
|
||||
const list = map.get(r.source);
|
||||
const entry = { qitemId: r.qitem_id, destination: r.destination_session };
|
||||
@@ -2634,8 +2635,13 @@ export class QueueRepository {
|
||||
}
|
||||
|
||||
/** {@link handoff}'s whole act, synchronously: its transaction commits before this returns, and the wake is delivered
|
||||
* after commit without being awaited, so a synchronous caller (an answer's close path) can hand a row on. */
|
||||
* after commit without being awaited, so a synchronous caller (an answer's close path) can hand a row on. Named in
|
||||
* the slow-operation log, so a stall during a handoff is attributed to it. */
|
||||
private handoffNow(input: QueueHandoffInput): { closed: QueueItem; created: QueueItem } {
|
||||
return runSyncSite("queue.handoff", () => this.handoffWithin(input));
|
||||
}
|
||||
|
||||
private handoffWithin(input: QueueHandoffInput): { closed: QueueItem; created: QueueItem } {
|
||||
const source = this.getById(input.qitemId);
|
||||
if (!source) {
|
||||
throw new QueueRepositoryError(
|
||||
@@ -4563,8 +4569,9 @@ export class QueueRepository {
|
||||
typeof opts?.excludeOperationReply === "string" && opts.excludeOperationReply.length > 0
|
||||
? mcOperationTag(opts.excludeOperationReply)
|
||||
: null;
|
||||
// Named only when slow: it runs on every row read and on each row of the attention read.
|
||||
const replies = this.hasReplyToColumn
|
||||
? (excludeOp
|
||||
? runSyncSiteWhenSlow("queue.decision_revision.replies", () => excludeOp
|
||||
? (this.db
|
||||
.prepare(
|
||||
"SELECT qitem_id FROM queue_items WHERE reply_to = ? AND NOT (tags IS NOT NULL AND json_valid(tags) AND EXISTS (SELECT 1 FROM json_each(tags) WHERE value = ?)) ORDER BY ts_created, rowid",
|
||||
|
||||
@@ -516,8 +516,7 @@ export class SeatDeliveryGuard {
|
||||
/** No recent writable-client activity before a short wait, the input still empty when read again after it, and still
|
||||
* no recent activity after that read. A time tmux can't report holds the paste, as an unreadable input does. The
|
||||
* read it returns is the one the paste goes on. Typing after this final check and before the paste can still leave
|
||||
* the notice beside the person's draft. In the tested append-mode schedule the Enter is then refused and nothing is
|
||||
* submitted or deleted; that isn't a guarantee for every interleaving (#3076). */
|
||||
* the notice beside the person's draft: the Enter is then refused, nothing is submitted or deleted (#3076). */
|
||||
private async quietBeforePaste(target: GuardTarget, read: () => Promise<ComposerInput | null>,
|
||||
unknown: (reading: ComposerInput | null) => DeliveryGuardError): Promise<ComposerInput> {
|
||||
const quiet = async () => {
|
||||
|
||||
@@ -48,6 +48,8 @@ export interface SlowOperationInstrumentation {
|
||||
classify?: (value: T) => "ok" | "failed",
|
||||
): Promise<T>;
|
||||
recordRequest?(site: string, durationMs: number): void;
|
||||
/** A synchronous site's duration, recorded only at or above the slow threshold (runSyncSiteWhenSlow). */
|
||||
recordSlowSync?(site: string, durationMs: number, outcome: "ok" | "failed"): void;
|
||||
/** Bounded nonblocking diagnostic append; false means observation was dropped. */
|
||||
recordDiagnostic?(record: Record<string, unknown>): boolean;
|
||||
snapshot?(): SlowOperationSnapshot;
|
||||
@@ -262,6 +264,11 @@ export class SlowOpRecorder implements SlowOperationInstrumentation {
|
||||
this.recordMeasurement(`request:${site}`, durationMs);
|
||||
}
|
||||
|
||||
recordSlowSync(site: string, durationMs: number, outcome: "ok" | "failed" = "ok"): void {
|
||||
if (!Number.isFinite(durationMs) || durationMs < this.slowThresholdMs) return;
|
||||
this.recordMeasurement(site, durationMs, outcome);
|
||||
}
|
||||
|
||||
recordDiagnostic(record: Record<string, unknown>): boolean {
|
||||
this.diagnostic.offered++;
|
||||
const coverage = { ...this.diagnostic, pending: this.diagnosticIds.size,
|
||||
|
||||
@@ -10,6 +10,26 @@ export function runSyncSite<T>(site: string, fn: () => T): T {
|
||||
return recorder?.runSync ? recorder.runSync(site, fn) : fn();
|
||||
}
|
||||
|
||||
/**
|
||||
* runSyncSite for a site on a hot path (per request, per row), where a begin and an end record per call would crowd
|
||||
* the bounded log: it records only a call that took at least the recorder's slow threshold, and writes no begin
|
||||
* record, so a call that wedges the loop is named once it ends rather than while it runs.
|
||||
*/
|
||||
export function runSyncSiteWhenSlow<T>(site: string, fn: () => T): T {
|
||||
const record = recorder?.recordSlowSync?.bind(recorder);
|
||||
if (!record) return fn();
|
||||
const startedAt = performance.now();
|
||||
let outcome: "ok" | "failed" = "failed";
|
||||
try {
|
||||
const value = fn();
|
||||
outcome = "ok";
|
||||
return value;
|
||||
} finally {
|
||||
// Measurement only: a failing record never replaces fn's own result or error.
|
||||
try { record(site, performance.now() - startedAt, outcome); } catch { /* the recorder reports its own health */ }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Async twin of runSyncSite: instruments a NON-blocking site through the
|
||||
* recorder's async `runStage` seam (falls back to a bare call when no recorder
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import type Database from "better-sqlite3";
|
||||
import { createDb } from "../src/db/connection.js";
|
||||
import { migrate } from "../src/db/migrate.js";
|
||||
import { ALL_MIGRATIONS } from "../src/db/all-migrations.js";
|
||||
import { EventBus } from "../src/domain/event-bus.js";
|
||||
import { QueueRepository, createLinkedSuccessorLookup } from "../src/domain/queue-repository.js";
|
||||
import { configureSyncSiteRecorder, runSyncSiteWhenSlow } from "../src/domain/sync-site-wrap.js";
|
||||
|
||||
// The queue's synchronous reads named in the slow-operation log, so an event-loop stall during one is attributed to it.
|
||||
// A fake recorder takes every call; the real recorder's slow threshold is pinned in slow-op-recorder.test.ts.
|
||||
describe("queue reads named in the slow-operation log", () => {
|
||||
let db: Database.Database;
|
||||
let repo: QueueRepository;
|
||||
const sync: string[] = [];
|
||||
const slow: Array<{ site: string; outcome: string }> = [];
|
||||
beforeEach(() => {
|
||||
db = createDb();
|
||||
migrate(db, ALL_MIGRATIONS);
|
||||
repo = new QueueRepository(db, new EventBus(db));
|
||||
configureSyncSiteRecorder({
|
||||
runSync<T>(site: string, fn: () => T): T { sync.push(site); return fn(); },
|
||||
recordSlowSync(site: string, _durationMs: number, outcome: "ok" | "failed"): void { slow.push({ site, outcome }); },
|
||||
});
|
||||
});
|
||||
afterEach(() => { configureSyncSiteRecorder(undefined); sync.length = 0; slow.length = 0; db.close(); });
|
||||
|
||||
it("names a handoff", async () => {
|
||||
const source = await repo.create({ sourceSession: "planner@r", destinationSession: "relay@r", body: "work", nudge: false });
|
||||
sync.length = 0;
|
||||
const { created } = await repo.handoff({ qitemId: source.qitemId, fromSession: "relay@r", toSession: "worker@r", nudge: false });
|
||||
expect(created.destinationSession).toBe("worker@r");
|
||||
expect(sync).toContain("queue.handoff");
|
||||
});
|
||||
|
||||
it("names decisionRevision's reply read through the slow-only site, once per revision", async () => {
|
||||
const ask = await repo.create({ sourceSession: "planner@r", destinationSession: "relay@r", body: "work", nudge: false });
|
||||
slow.length = 0;
|
||||
const revision = repo.decisionRevision(repo.getById(ask.qitemId)!);
|
||||
expect(typeof revision).toBe("string");
|
||||
expect(slow).toEqual([{ site: "queue.decision_revision.replies", outcome: "ok" }]);
|
||||
});
|
||||
|
||||
it("names the stuck sweep's linked-successor build when it is built", () => {
|
||||
const lookup = createLinkedSuccessorLookup(db, ["qitem-source"]);
|
||||
expect(sync).toEqual([]);
|
||||
expect(lookup.has("qitem-source", "worker@r")).toBe(false);
|
||||
expect(lookup.has("qitem-source", "other@r")).toBe(false);
|
||||
expect(sync).toEqual(["queue.linked_successors.build"]);
|
||||
});
|
||||
|
||||
it("keeps the wrapped call's result and error, and records each outcome", () => {
|
||||
expect(runSyncSiteWhenSlow("test.site", () => 7)).toBe(7);
|
||||
expect(() => runSyncSiteWhenSlow("test.site", () => { throw new Error("read failed"); })).toThrow("read failed");
|
||||
expect(slow).toEqual([{ site: "test.site", outcome: "ok" }, { site: "test.site", outcome: "failed" }]);
|
||||
});
|
||||
|
||||
it("never lets a failing record replace the wrapped call's result", () => {
|
||||
configureSyncSiteRecorder({ recordSlowSync(): void { throw new Error("recorder gone"); } });
|
||||
expect(runSyncSiteWhenSlow("test.site", () => "value")).toBe("value");
|
||||
expect(() => runSyncSiteWhenSlow("test.site", () => { throw new Error("read failed"); })).toThrow("read failed");
|
||||
});
|
||||
|
||||
it("runs the call bare when no recorder is configured", () => {
|
||||
configureSyncSiteRecorder(undefined);
|
||||
expect(runSyncSiteWhenSlow("test.site", () => "bare")).toBe("bare");
|
||||
expect(slow).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -21,6 +21,7 @@ const expectedSites = new Map<string, string[]>([
|
||||
["domain/codex-thread-id.ts", ["codex_thread_id.resolve_home"]],
|
||||
["domain/review/gather.ts", ["review.gather.git"]],
|
||||
["domain/tmux-option-defaults.ts", ["tmux_options.command_v"]],
|
||||
["domain/queue-repository.ts", ["queue.decision_revision.replies", "queue.linked_successors.build", "queue.handoff"]],
|
||||
]);
|
||||
|
||||
const pluginVendorUrl = "https://github.com/mvschwarz/openrig-plugins/releases/latest/download/openrig-core.tar.gz";
|
||||
@@ -290,6 +291,23 @@ describe("SlowOpRecorder locked instrumentation contract", () => {
|
||||
await recorder.close();
|
||||
});
|
||||
|
||||
it("records a slow synchronous site only at or above the slow threshold, with its outcome", async () => {
|
||||
const mod = await loadRecorderModule();
|
||||
expect(mod, "slow-op-recorder production module is missing").not.toBeNull();
|
||||
if (!mod) return;
|
||||
|
||||
const dir = tempDir();
|
||||
const logPath = path.join(dir, "slow-operations.jsonl");
|
||||
const recorder = new mod.SlowOpRecorder({ logPath, slowThresholdMs: 250 });
|
||||
recorder.recordSlowSync("queue.decision_revision.replies", 249, "ok");
|
||||
recorder.recordSlowSync("queue.decision_revision.replies", 250, "ok");
|
||||
recorder.recordSlowSync("queue.decision_revision.replies", 400, "failed");
|
||||
await recorder.flush();
|
||||
const records = (await readRecords(logPath)).filter((record) => record.site === "queue.decision_revision.replies");
|
||||
expect(records.map((record) => [record.phase, record.durationMs, record.outcome])).toEqual([["end", 250, "ok"], ["end", 400, "failed"]]);
|
||||
await recorder.close();
|
||||
});
|
||||
|
||||
// The request-timing MIDDLEWARE CONTRACT, hermetic + deterministic. The prior "real server" form
|
||||
// ran the full daemon (event-loop monitor + a globalThis.fetch monkey-patch), which is structurally
|
||||
// ambient-fragile under the concurrent-workspace gate: it intermittently returned 200 for an unmatched
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
"agent-operated-software/SKILL.md": "ec910bf5f631d7f20872786a565a0437a446fa2d5fd8b52b685979978243a6a8",
|
||||
"agent-operated-workflows/SKILL.md": "24e54cbfa979999f523516112670618b2926ae5d42c7aa0846c94db214857ea5",
|
||||
"applying-a-permission-policy/SKILL.md": "f7bba8d50b10fc8715f40a57bf82677ca9388c6718c8a162833d9c6daf2afdca",
|
||||
"claude-compaction-restore/SKILL.md": "79a707974824e91d5888c7bc10b152989ff891491f1adecf3e93991e2af55f81",
|
||||
"claude-compaction-restore/SKILL.md": "cd8369a62647ad389a9b0cc91c6f784a0d93e2d2b874bf9570d47c2134647d1c",
|
||||
"claude-compaction-restore/reference/SKILL-v1.md": "6af2a889c9c38227fb186897c6e29185f81b215822070b469a6ceeb47734e133",
|
||||
"claude-compaction-restore/scripts/precompact-hook.mjs": "712e97c8beb02eb6c2815884a960778a225d8f6238dfe04a9b0547c34eaa131d",
|
||||
"claude-compaction-restore/scripts/restore-from-jsonl.mjs": "5584cff4ab36cd843e14c81bd201e4a2f8e498e995abacc4f81ec6c1fc556b10",
|
||||
|
||||
Reference in new issue
Block a user