mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-12 18:48:31 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34f7c46087 |
No files matched your search
@@ -182,9 +182,8 @@ With `needs-me.read` supported:
|
||||
about the open rows blocked on it (at most 100; empty when none are recorded).
|
||||
- **A snapshot watermark:** the response's `watermark` is the newest event `seq` read before the snapshot. Follow
|
||||
`GET /api/events` from it with `Last-Event-ID: <watermark>`: every event after the snapshot is replayed, some possibly
|
||||
already reflected in it. A click that records or changes an answer emits `queue.answer_recorded` even while the set
|
||||
is partial. An identical repeated choice, or a later click after a pending ask's answer set is complete, records
|
||||
nothing new and emits none. Events are best effort; re-read the snapshot or row after a stream gap.
|
||||
already reflected in it. Recording one answer of several emits no event yet, so re-read the snapshot (or the row)
|
||||
rather than relying on events alone for partial answers.
|
||||
- **The answers returned to a request:** `GET /api/queue/<qitemId>` adds `answers`, oldest first, each
|
||||
`{ qitemId, by, at, text, episode, recordedIdentity }`. They are found by the answer row's link to the request, not
|
||||
by its text, so don't parse the `In reply to:` line in an answer's body. `text` is the person's answer without that
|
||||
|
||||
@@ -1103,9 +1103,8 @@ 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: 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.
|
||||
// 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.
|
||||
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,7 +105,6 @@ 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 {
|
||||
@@ -822,14 +821,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 = runSyncSite("queue.linked_successors.build", () => db.prepare(
|
||||
const rows = 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 };
|
||||
@@ -2635,13 +2634,8 @@ 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. Named in
|
||||
* the slow-operation log, so a stall during a handoff is attributed to it. */
|
||||
* after commit without being awaited, so a synchronous caller (an answer's close path) can hand a row on. */
|
||||
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(
|
||||
@@ -4569,9 +4563,8 @@ 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
|
||||
? runSyncSiteWhenSlow("queue.decision_revision.replies", () => excludeOp
|
||||
? (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,7 +516,8 @@ 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: the Enter is then refused, nothing is submitted or deleted (#3076). */
|
||||
* 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). */
|
||||
private async quietBeforePaste(target: GuardTarget, read: () => Promise<ComposerInput | null>,
|
||||
unknown: (reading: ComposerInput | null) => DeliveryGuardError): Promise<ComposerInput> {
|
||||
const quiet = async () => {
|
||||
|
||||
@@ -48,8 +48,6 @@ 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;
|
||||
@@ -264,11 +262,6 @@ 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,26 +10,6 @@ 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
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
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,7 +21,6 @@ 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";
|
||||
@@ -291,23 +290,6 @@ 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
|
||||
|
||||
Reference in new issue
Block a user