From 865f63c6e3caa62030b9f1b0ababe740d8d0a4b1 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Wed, 14 Oct 2026 08:23:18 -0700 Subject: [PATCH 1/3] chore(vendor): sync vendored interchange trees to 0.4.0 (5453d0b0) Re-sync per the docs/VENDORING.md procedure; adjudication lands with the patch commit. --- vendor/intx-agent/src/agent.test.ts | 10 +- .../intx-agent/src/compactor-wiring.test.ts | 15 ++- vendor/intx-agent/src/testing/index.ts | 1 + .../src/testing/reactor-waiters.test.ts | 30 ++++++ .../intx-agent/src/testing/reactor-waiters.ts | 25 +++++ .../src/tool-namespace-invariants.test.ts | 96 +++++++++++++++++++ vendor/intx-harness/src/harness.test.ts | 74 ++++++++------ vendor/intx-harness/src/reply-drain.test.ts | 26 +++-- .../src/default-director.test.ts | 5 +- vendor/intx-log/src/default-sink-probe.ts | 18 ++++ vendor/intx-log/src/default-sink.test.ts | 52 ++++++---- vendor/intx-types/src/base64.ts | 25 ++++- vendor/intx-types/src/grants.ts | 22 +++++ vendor/intx-types/src/sidecar.test.ts | 64 +++++++++++++ vendor/intx-types/src/sidecar.ts | 12 ++- vendor/intx-types/src/testing/index.ts | 7 ++ .../intx-types/src/testing/wait-until.test.ts | 44 +++++++++ vendor/intx-types/src/testing/wait-until.ts | 30 ++++++ 18 files changed, 480 insertions(+), 76 deletions(-) create mode 100644 vendor/intx-agent/src/testing/reactor-waiters.test.ts create mode 100644 vendor/intx-agent/src/testing/reactor-waiters.ts create mode 100644 vendor/intx-authz/src/tool-namespace-invariants.test.ts create mode 100644 vendor/intx-log/src/default-sink-probe.ts create mode 100644 vendor/intx-types/src/testing/index.ts create mode 100644 vendor/intx-types/src/testing/wait-until.test.ts create mode 100644 vendor/intx-types/src/testing/wait-until.ts diff --git a/vendor/intx-agent/src/agent.test.ts b/vendor/intx-agent/src/agent.test.ts index c65f4bc15..f9f79774e 100644 --- a/vendor/intx-agent/src/agent.test.ts +++ b/vendor/intx-agent/src/agent.test.ts @@ -289,9 +289,13 @@ describe("createAgent tool-rollback dispose handling", () => { if (!(caught instanceof Error)) throw new Error("unreachable"); expect(caught.name).toBe("DuplicateToolError"); - // Let any in-flight microtasks settle so an escaped rejection - // would land. - await new Promise((resolve) => setTimeout(resolve, 25)); + // One macrotask boundary is enough: the runtime reports an + // unhandled rejection when the microtask queue drains, so a + // rejection escaping the rollback above would already have + // reached the handler by the time this resolves. Verified against + // this runtime with a `void`-discarded rejection of the same + // shape as the disposer's. + await new Promise((resolve) => setTimeout(resolve, 0)); expect(observedRejection).toBeNull(); } finally { process.off("unhandledRejection", onUnhandled); diff --git a/vendor/intx-agent/src/compactor-wiring.test.ts b/vendor/intx-agent/src/compactor-wiring.test.ts index 9c9fa7db3..5de450a78 100644 --- a/vendor/intx-agent/src/compactor-wiring.test.ts +++ b/vendor/intx-agent/src/compactor-wiring.test.ts @@ -43,6 +43,7 @@ import { createDirectorRegistry } from "./director-registry"; import type { BaseEnv } from "./env"; import { noopAuditStore } from "./testing/audit-noop"; import { permissiveAuthorize } from "./testing/authorize-allow"; +import { waitForReactorDone } from "./testing"; // The compactor tests never call infer(); the URL is unreachable so any // accidental infer() would fail fast rather than hang. @@ -187,14 +188,6 @@ function inboundConversation(content: string) { }); } -async function waitForReactorDone( - stream: AsyncIterable, -): Promise { - for await (const event of stream) { - if (event.type === "reactor.done") return; - } -} - async function collectUntilDone( stream: AsyncIterable, ): Promise { @@ -203,7 +196,11 @@ async function collectUntilDone( events.push(event); if (event.type === "reactor.done") return events; } - return events; + // A fatal reactor.error still shuts the reactor down, so reactor.done + // arrives on that path too and the early return above is what fires. + // Reaching here means the stream closed without it, and returning the + // partial events would let the caller assert against a truncated run. + throw new Error("reactor event stream ended before reactor.done"); } describe("createAgent compactor wiring", () => { diff --git a/vendor/intx-agent/src/testing/index.ts b/vendor/intx-agent/src/testing/index.ts index 1819854b4..36faffa27 100644 --- a/vendor/intx-agent/src/testing/index.ts +++ b/vendor/intx-agent/src/testing/index.ts @@ -16,3 +16,4 @@ export { noopAuditStore } from "./audit-noop"; export { permissiveAuthorize } from "./authorize-allow"; +export { waitForReactorDone } from "./reactor-waiters"; diff --git a/vendor/intx-agent/src/testing/reactor-waiters.test.ts b/vendor/intx-agent/src/testing/reactor-waiters.test.ts new file mode 100644 index 000000000..8756dd697 --- /dev/null +++ b/vendor/intx-agent/src/testing/reactor-waiters.test.ts @@ -0,0 +1,30 @@ +import { describe, test, expect } from "bun:test"; + +import { waitForReactorDone } from "./reactor-waiters"; + +async function* stream(...events: { type: string }[]) { + for (const event of events) yield event; +} + +describe("waitForReactorDone", () => { + test("resolves on the terminal event", async () => { + await waitForReactorDone( + stream({ type: "reactor.start" }, { type: "reactor.done" }), + ); + }); + + test("throws when the stream ends without the terminal event", async () => { + // Returning instead would resolve as though the run had completed, and + // the caller would then fail on a later assertion rather than on the + // real cause. All four copies of this helper returned. + await expect( + waitForReactorDone(stream({ type: "reactor.start" })), + ).rejects.toThrow(/ended before reactor.done/); + }); + + test("ignores events after the terminal one", async () => { + await waitForReactorDone( + stream({ type: "reactor.done" }, { type: "reactor.stray" }), + ); + }); +}); diff --git a/vendor/intx-agent/src/testing/reactor-waiters.ts b/vendor/intx-agent/src/testing/reactor-waiters.ts new file mode 100644 index 000000000..8cb69a560 --- /dev/null +++ b/vendor/intx-agent/src/testing/reactor-waiters.ts @@ -0,0 +1,25 @@ +// Waiter over a reactor's emitted-event stream. +// +// A reactor run ends with a `reactor.done` event, including on the fatal +// error path, so a test that needs the run finished waits for that event +// rather than for an interval. CONVENTIONS.md names this shape as the one to +// copy; it lives here so it is imported rather than copied again. + +/** + * Resolve once the stream emits `reactor.done`. + * + * Throws if the stream ends without it: returning would resolve as though the + * run had completed, and the caller would then fail on a later assertion + * instead of on the real cause. + * + * Typed structurally so a caller need not import the reactor's event union to + * await its terminal event. + */ +export async function waitForReactorDone( + stream: AsyncIterable<{ type: string }>, +): Promise { + for await (const event of stream) { + if (event.type === "reactor.done") return; + } + throw new Error("reactor event stream ended before reactor.done"); +} diff --git a/vendor/intx-authz/src/tool-namespace-invariants.test.ts b/vendor/intx-authz/src/tool-namespace-invariants.test.ts new file mode 100644 index 000000000..717345f48 --- /dev/null +++ b/vendor/intx-authz/src/tool-namespace-invariants.test.ts @@ -0,0 +1,96 @@ +// How a wildcard grant row and a bare `tool:` row each fare against a +// PINNED tool's runtime gate. +// +// A pinned tool package is loaded through the namespacing loader, which +// rewrites every definition name to `:`. The workflow child +// gates each call on `tool:`, so the query it issues for a pinned +// tool is `tool::`. The deploy-time capability walk, by +// contrast, reads inline `agent.toolFactories`, which carry no bundle context, +// so the rows it contributes are bare `tool:`. +// +// That asymmetry is what makes the namespaced form a step-scoping mechanism: +// a bare row cannot address a pinned tool's gate, and the authority a pinned +// tool actually needs is supplied per step by the sidecar's tool-mark floor, +// derived from the factories that step loaded. These cases pin what a wildcard +// row does to that scoping. + +import { describe, test, expect } from "bun:test"; + +import { evaluateGrants } from "./evaluate"; +import type { GrantRule } from "./types"; + +const BUNDLE_ID = "tp_pinned_bundle"; +const TOOL_NAME = "search"; +const NAMESPACED_GATE = `tool:${BUNDLE_ID}:${TOOL_NAME}`; +const BARE_GATE = `tool:${TOOL_NAME}`; +const INVOKE = "invoke"; + +function grant( + overrides: Partial & + Pick, +): GrantRule { + return { + id: `grt_${Math.random().toString(36).slice(2, 10)}`, + origin: "creator", + conditions: null, + expiresAt: null, + roleId: null, + principalId: null, + ...overrides, + }; +} + +describe("a wildcard row reaches a pinned tool's namespaced gate", () => { + test("*/* resolves to allow for tool::/invoke", async () => { + const result = await evaluateGrants( + [grant({ resource: "*", action: "*", effect: "allow" })], + NAMESPACED_GATE, + INVOKE, + ); + expect(result.effect).toBe("allow"); + }); + + test("the walk's bare tool: row does NOT reach that gate", async () => { + const result = await evaluateGrants( + [grant({ resource: BARE_GATE, action: INVOKE, effect: "allow" })], + NAMESPACED_GATE, + INVOKE, + ); + expect(result.effect).toBe(null); + }); + + test("the bare row is operative against a bare, unpinned gate", async () => { + const result = await evaluateGrants( + [grant({ resource: BARE_GATE, action: INVOKE, effect: "allow" })], + BARE_GATE, + INVOKE, + ); + expect(result.effect).toBe("allow"); + }); + + test("a wildcard row reaches a second bundle's tool the step never loaded", async () => { + const result = await evaluateGrants( + [grant({ resource: "*", action: "*", effect: "allow" })], + "tool:tp_other_bundle:transfer", + INVOKE, + ); + expect(result.effect).toBe("allow"); + }); +}); + +describe("the tool-mark floor still outranks a wildcard row", () => { + test("an ask floor beats */* allow at higher specificity", async () => { + const floor = grant({ + resource: NAMESPACED_GATE, + action: INVOKE, + effect: "ask", + }); + const wildcard = grant({ resource: "*", action: "*", effect: "allow" }); + const result = await evaluateGrants( + [wildcard, floor], + NAMESPACED_GATE, + INVOKE, + ); + expect(result.effect).toBe("ask"); + }); +}); diff --git a/vendor/intx-harness/src/harness.test.ts b/vendor/intx-harness/src/harness.test.ts index 5a97d6478..b40916447 100644 --- a/vendor/intx-harness/src/harness.test.ts +++ b/vendor/intx-harness/src/harness.test.ts @@ -47,6 +47,8 @@ import { defineMailTools, type MailEnv, } from "./harness"; +import { waitForReactorDone } from "@intx/agent/testing"; +import { waitUntil } from "@intx/types/testing"; const SOURCE: InferenceSource = { id: "anthropic:claude-3-5-sonnet", @@ -59,6 +61,7 @@ const SOURCE: InferenceSource = { const AGENT_ADDRESS = "agent@test.local"; interface MockTransportShape { + fire(event: { type: string; uid: number }): void; fireExists(uid: number): void; enqueue(uid: number, message: InboundMessage): void; watchCount(): number; @@ -142,11 +145,14 @@ function makeMockTransport(): { return { transport, control: { - fireExists(uid: number) { + fire(event: { type: string; uid: number }) { for (const cb of callbacks) { - cb({ type: "exists", uid }); + cb(event); } }, + fireExists(uid: number) { + this.fire({ type: "exists", uid }); + }, enqueue(uid: number, message: InboundMessage) { messages.set(uid, message); }, @@ -278,8 +284,9 @@ describe("createHarness", () => { control.enqueue(42, message); control.fireExists(42); - // Yield so the async watch callback resolves its fetch. - await new Promise((resolve) => setTimeout(resolve, 50)); + // The watch callback's fetch runs on its own async body, so wait + // for the recorded fetch rather than for an interval. + await waitUntil(() => control.getFetchedUids().includes(42)); // The harness must have fetched the message via the transport. // Whether it then consumes from INBOX (start/continue routing) @@ -379,14 +386,13 @@ describe("createHarness outbound pipeline", () => { control.enqueue(101, stored); control.fireExists(101); - // Poll for the outbound send rather than relying on fixed-time - // sleeps; the reply drain runs on microtasks, so the assertion - // meets within a few iterations. - const deadline = Date.now() + 2000; - while (Date.now() < deadline) { + // The stubbed inference advances on the harness clock, and the + // reply drain that follows it runs on the real event loop, so + // the send needs both pumped. The loop carries no deadline: a + // send that never lands is the lane timeout's to report. + while (control.getSent().length === 0) { await inference.run(); - if (control.getSent().length > 0) break; - await new Promise((resolve) => setTimeout(resolve, 25)); + await new Promise((resolve) => setTimeout(resolve, 0)); } expect(control.getSent().length).toBeGreaterThanOrEqual(1); @@ -599,14 +605,6 @@ function recordingDef() { }); } -async function waitForReactorDone( - stream: AsyncIterable<{ type: string }>, -): Promise { - for await (const event of stream) { - if (event.type === "reactor.done") return; - } -} - describe("createHarness message delivery", () => { let workDir: string; @@ -661,16 +659,34 @@ describe("createHarness message delivery", () => { const harness = await createHarness(recordingDef(), env); try { - // The mock's `fireExists` is the only event shape that - // should reach a `fetchFull`. The harness's watch callback - // checks `event.type === "exists"` and short-circuits - // otherwise -- so a callback yield with no fireExists must - // produce no fetches and no reactor deliveries. Holding - // off briefly gives any erroneous async fetch a chance to - // land before we assert. - await new Promise((resolve) => setTimeout(resolve, 50)); - expect(control.getFetchedUids().length).toBe(0); - expect(received.count).toBe(0); + const message = createInboundMessage({ + from: "alice@example.com", + to: AGENT_ADDRESS, + content: "Hello", + interchangeType: "conversation.message", + }); + const stored: InboundMessage = { + ...message, + ref: { uid: 9, mailbox: "INBOX" }, + }; + control.enqueue(9, stored); + + // The harness's watch callback checks `event.type === "exists"` + // and returns synchronously for anything else, so a non-exists + // event queues no work at all. Fire one, then fire a real + // `exists` for a different uid and wait for the fetch that one + // does produce. The mock invokes its callbacks in order, so that + // fetch is ordered after the non-exists callback returned: had + // the guard let the expunged event through, uid 8 would already + // be in the fetch record by then. + control.fire({ type: "expunged", uid: 8 }); + control.fire({ type: "exists", uid: 9 }); + + await waitUntil(() => control.getFetchedUids().length > 0); + expect(control.getFetchedUids()).toEqual([9]); + + await waitUntil(() => received.count >= 1); + expect(received.count).toBe(1); } finally { await harness.close(); } diff --git a/vendor/intx-harness/src/reply-drain.test.ts b/vendor/intx-harness/src/reply-drain.test.ts index d09bc8218..64311c5a1 100644 --- a/vendor/intx-harness/src/reply-drain.test.ts +++ b/vendor/intx-harness/src/reply-drain.test.ts @@ -12,6 +12,8 @@ import type { SendReceipt, } from "@intx/types/runtime"; +import { waitUntil } from "@intx/types/testing"; + import type { ConnectorReplyParts } from "./connector-router"; import { driveConnectorReplies } from "./reply-drain"; @@ -41,15 +43,22 @@ async function* streamOf( * A push-based event stream: a test feeds events with `push` and ends the * stream with `end`, so the barrier's capture-then-await ordering can be * exercised against a stream that stays open between events. + * + * `pulled()` reports how many events the consumer has taken off the queue. + * The drain consumes serially, so `pulled()` reaching n+1 is a signal that + * the drain finished its handling of event n -- which is what a test asserting + * "event n changed nothing" needs, since there is no event of its own to await. */ function pushStream(): { stream: AsyncGenerator; push: (event: InferenceEvent) => void; end: () => void; + pulled: () => number; } { const queue: InferenceEvent[] = []; let notify: (() => void) | null = null; let ended = false; + let pulledCount = 0; const wake = (): void => { const resume = notify; notify = null; @@ -59,6 +68,7 @@ function pushStream(): { for (;;) { const next = queue.shift(); if (next !== undefined) { + pulledCount += 1; yield next; continue; } @@ -78,15 +88,10 @@ function pushStream(): { ended = true; wake(); }, + pulled: () => pulledCount, }; } -/** Resolve after enough ticks for the drain's chain to settle a pushed reply. */ -async function settleTicks(): Promise { - for (let i = 0; i < 5; i += 1) await Promise.resolve(); - await new Promise((resolve) => setTimeout(resolve, 10)); -} - describe("driveConnectorReplies", () => { test("sends exactly one threaded reply per connector.reply", async () => { const sent: OutboundMessage[] = []; @@ -401,7 +406,7 @@ describe("driveConnectorReplies", () => { // -- which is why the warm step must NOT await the barrier for such a turn. // When the drain ends before the reply arrives, the barrier resolves as a // failure rather than hanging forever. - const { stream, push, end } = pushStream(); + const { stream, push, end, pulled } = pushStream(); const drain = driveConnectorReplies({ stream, @@ -424,8 +429,13 @@ describe("driveConnectorReplies", () => { }); // A non-reply event flows past without advancing the reply sequence. + // The drain consumes the stream serially, so it takes the second noise + // event only after it has finished handling the first: waiting for that + // second pull puts the assertions strictly after any settlement the first + // event could have caused, with no interval to guess at. push(noiseEvent(1)); - await settleTicks(); + push(noiseEvent(2)); + await waitUntil(() => pulled() >= 2); expect(settledEarly).toBe(false); expect(drain.replySeq()).toBe(0); diff --git a/vendor/intx-inference/src/default-director.test.ts b/vendor/intx-inference/src/default-director.test.ts index e41ac2e10..7d01a84a3 100644 --- a/vendor/intx-inference/src/default-director.test.ts +++ b/vendor/intx-inference/src/default-director.test.ts @@ -197,7 +197,10 @@ describe("DefaultDirector — afterInferenceDone hook", () => { test("hook returning a Promise is awaited", async () => { const hook: AfterInferenceHook = async () => { - await new Promise((resolve) => setTimeout(resolve, 5)); + // Yield a full event-loop turn so the hook's promise cannot be settled + // when decide() receives it. The director awaits the hook, so the + // assertions below do not depend on how long this takes. + await new Promise((resolve) => setTimeout(resolve, 0)); const decision: AfterInferenceDecision = { type: "abort", reason: "async abort", diff --git a/vendor/intx-log/src/default-sink-probe.ts b/vendor/intx-log/src/default-sink-probe.ts new file mode 100644 index 000000000..ce9bba4d1 --- /dev/null +++ b/vendor/intx-log/src/default-sink-probe.ts @@ -0,0 +1,18 @@ +// Probe for the module-load contract: importing this package installs a +// default console sink. It runs as a child process because that contract +// cannot be observed in-process -- `installDefaultConsoleSink` returns early +// when a config already exists, and under a shared module registry some +// earlier file has almost always triggered the install already, so an +// in-process check reads back whatever that file left behind. +// +// Reports to stdout rather than through a logger, so the report cannot be +// routed by the very configuration under test. +import { getConfig } from "./index"; + +const config = getConfig(); +process.stdout.write( + JSON.stringify({ + installed: config !== null, + sinks: config === null ? [] : Object.keys(config.sinks).sort(), + }), +); diff --git a/vendor/intx-log/src/default-sink.test.ts b/vendor/intx-log/src/default-sink.test.ts index 5be0dc080..9a7f41851 100644 --- a/vendor/intx-log/src/default-sink.test.ts +++ b/vendor/intx-log/src/default-sink.test.ts @@ -1,18 +1,10 @@ import { describe, test, expect, beforeEach, afterEach } from "bun:test"; +import path from "node:path"; import { getConfig, getLogger, resetSync, setup } from "./index"; import { installDefaultConsoleSink } from "./default-sink"; -// Snapshot taken at test-file load, before any beforeEach hooks run. This is -// the only point at which we can observe the side effect at the bottom of -// `default-sink.ts` directly; the hooks below reset and reinstall manually -// for the per-test scenarios. -// -// Order-sensitive: any future test file in this directory that calls -// `resetSync()` at top level *before* this file imports will null this -// snapshot. The "importing the module installs a config" test below would -// then throw with a misleading error pointing at this file rather than the -// real cause. -const moduleLoadConfig = getConfig(); +const REPO_ROOT = path.resolve(import.meta.dir, "../../.."); +const PROBE = path.join(import.meta.dir, "default-sink-probe.ts"); describe("default console sink", () => { const warnCaptured: string[] = []; @@ -35,14 +27,36 @@ describe("default console sink", () => { }); /* eslint-enable no-console */ - test("importing the module installs a config with the default sink", () => { - if (!moduleLoadConfig) { - throw new Error( - "default-sink module-load side effect did not populate getConfig()", - ); - } - expect(Object.keys(moduleLoadConfig.sinks)).toEqual(["default"]); - }); + test("importing the package installs a config with the default sink", async () => { + // Asserted in a child process. In-process this contract is unobservable: + // the installer returns early when a config exists, and every package + // imports this one, so by the time any test here runs the install has + // already happened somewhere else and the reading is of that file's + // leftovers. This assertion passed before the probe existed even though + // it observed nothing -- the config it found happened to name its sink + // "default" too. + const proc = Bun.spawn(["bun", "run", "--conditions=intx-src", PROBE], { + cwd: REPO_ROOT, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr] = await Promise.all([ + new Response(proc.stdout).text(), + new Response(proc.stderr).text(), + ]); + const exitCode = await proc.exited; + + // Report both streams: the default sink writes through console, so a + // failure to install can surface on either. + expect(exitCode, `stdout: ${stdout}\nstderr: ${stderr}`).toBe(0); + // Guards against a child that dies before reporting, which would + // otherwise leave the assertions below trivially satisfied. + expect(stdout).toContain("installed"); + expect(JSON.parse(stdout)).toEqual({ + installed: true, + sinks: ["default"], + }); + }, 30_000); test("warn lands on console.warn through the default sink", () => { getLogger(["test-default-sink-probe"]).warn`probe`; diff --git a/vendor/intx-types/src/base64.ts b/vendor/intx-types/src/base64.ts index 5158372ce..7e7221f30 100644 --- a/vendor/intx-types/src/base64.ts +++ b/vendor/intx-types/src/base64.ts @@ -4,12 +4,29 @@ // Centralizing here keeps the encoding stable across the sidecar/hub // boundary and any other consumer that needs the same wire shape. +// Bytes per encoding chunk. Must be a multiple of 3 so each chunk except the +// last encodes to whole base64 groups with no padding, letting the per-chunk +// outputs be joined without re-encoding. The bound keeps intermediates small +// even for a body at the 44 MB `mail.outbound` cap, which the unchunked +// per-byte string build inflated to multi-GB RSS. +const BASE64_CHUNK_BYTES = 24576; + +function encodeChunk(bytes: Uint8Array, start: number, end: number): string { + const chars = new Array(end - start); + let i = 0; + for (const byte of bytes.subarray(start, end)) { + chars[i++] = String.fromCharCode(byte); + } + return btoa(chars.join("")); +} + export function base64Encode(bytes: Uint8Array): string { - let binary = ""; - for (const byte of bytes) { - binary += String.fromCharCode(byte); + const parts: string[] = []; + for (let start = 0; start < bytes.length; start += BASE64_CHUNK_BYTES) { + const end = Math.min(start + BASE64_CHUNK_BYTES, bytes.length); + parts.push(encodeChunk(bytes, start, end)); } - return btoa(binary); + return parts.join(""); } export function base64Decode(base64: string): Uint8Array { diff --git a/vendor/intx-types/src/grants.ts b/vendor/intx-types/src/grants.ts index 99f64a684..723ec5789 100644 --- a/vendor/intx-types/src/grants.ts +++ b/vendor/intx-types/src/grants.ts @@ -112,3 +112,25 @@ export const GrantRequirement = type({ "conditions?": "Record | null", }); export type GrantRequirement = typeof GrantRequirement.infer; + +export const ApprovalItem = type("string").or(GrantRequirement); + +/** + * One thing an operator approved for a workflow deployment. The approved + * surface has two kinds of entry and they live on different axes, so the + * vocabulary carries both rather than flattening one into the other: + * + * - A grant-shape string (`tool:`, `effect:`, + * `inference.source::`, ...) -- what the deploy-time + * capability walk surfaces per step. + * - A `GrantRequirement` -- what the definition declares, and what the run + * path materializes into real grant rows on the run principal. It is not + * expressible as a grant string: `source` names WHOSE authority is + * delegated, which is not a resource/action fact, and `conditions` is an + * open record. + * + * Keeping both in one vocabulary keeps "what the operator approved" a single + * record with a single gate, and lets the gate compare a declared requirement + * to an approved one structurally rather than through a lossy string form. + */ +export type ApprovalItem = typeof ApprovalItem.infer; diff --git a/vendor/intx-types/src/sidecar.test.ts b/vendor/intx-types/src/sidecar.test.ts index 6964ac33d..5b99553cd 100644 --- a/vendor/intx-types/src/sidecar.test.ts +++ b/vendor/intx-types/src/sidecar.test.ts @@ -5,6 +5,7 @@ import { AgentDeployFrame, CredentialsUpdateFrame, DeployApplyErrorCategory, + FrozenApprovalBundle, HubFrame, MAX_AGENT_ADDRESSES_FRAME, MAX_CACHED_SENDER_ADDRESSES_FRAME, @@ -751,3 +752,66 @@ describe("SenderKeyEvictFrame", () => { expect(out instanceof type.errors).toBe(true); }); }); + +// `FrozenApprovalBundle` is asserted against a persisted jsonb column +// (`parseWorkflowRunLaunchSpecRow`, packages/db/src/parse-row.ts), so a row +// written by an older build must keep parsing. `approvedGrants` widened from +// `string[]` to `ApprovalItem[]`; nothing else in the suite holds a row from +// before that widening, so this block is the only place the compatibility +// claim is checked. +describe("FrozenApprovalBundle approvedGrants", () => { + const bundle = { + source: { + kind: "registry", + registry: "npm", + package: { packageName: "p", version: "1.0.0" }, + }, + entry: "./src/workflow.ts", + projection: { + id: "w", + stepOrder: ["a"], + steps: { a: { kind: "step", id: "a" } }, + triggers: [], + }, + closure: { schemaVersion: "1", topLevel: [], entries: [] }, + approvedWireHash: "deadbeef", + }; + + test("a row written before the requirement kind existed still parses", () => { + const parsed = FrozenApprovalBundle({ + ...bundle, + approvedGrants: ["tool:x", "inference.source:anthropic:m"], + }); + if (parsed instanceof type.errors) { + throw new Error(`legacy row rejected: ${parsed.summary}`); + } + expect(parsed.approvedGrants).toEqual([ + "tool:x", + "inference.source:anthropic:m", + ]); + }); + + test("a row mixing grant strings and requirement records parses", () => { + const requirement = { + resource: "tool:*", + action: "invoke", + source: "creator", + } as const; + const parsed = FrozenApprovalBundle({ + ...bundle, + approvedGrants: ["tool:x", requirement], + }); + if (parsed instanceof type.errors) { + throw new Error(`mixed row rejected: ${parsed.summary}`); + } + expect(parsed.approvedGrants).toEqual(["tool:x", requirement]); + }); + + test("an entry that is neither a grant string nor a requirement is refused", () => { + const parsed = FrozenApprovalBundle({ + ...bundle, + approvedGrants: [{ resource: "tool:*" }], + }); + expect(parsed instanceof type.errors).toBe(true); + }); +}); diff --git a/vendor/intx-types/src/sidecar.ts b/vendor/intx-types/src/sidecar.ts index ae0180a60..65ace287d 100644 --- a/vendor/intx-types/src/sidecar.ts +++ b/vendor/intx-types/src/sidecar.ts @@ -9,6 +9,7 @@ import { type } from "arktype"; import { GrantWalkSnapshot } from "./grant-snapshot"; +import { ApprovalItem } from "./grants"; import { WireGrantRule } from "./grant-wire"; import { BoundedApprovalSnapshot, @@ -589,8 +590,13 @@ export type SourceRefPin = typeof SourceRefPin.infer; * definition's bytes come from and the entry module the probe evaluated; * `projection` is the inert wire projection the freeze hashed; `closure` is the * frozen dependency closure the pin resolved to; `approvedWireHash` is the freeze - * anchor; `approvedGrants` is the approved grant set (rehydrated to a `Set` on - * the deploy hand-off). Per-step inference sources are deliberately NOT frozen + * anchor; `approvedGrants` is the approved surface -- the walk's grant-shape + * strings plus the definition's declared grant requirements, each an + * `ApprovalItem` (partitioned by kind into an `ApprovalSet` on the deploy + * hand-off, via `approvalSetFromItems`). The persisted form stays this flat + * list: rows written before the requirement kind existed hold plain strings, + * and a string is an `ApprovalItem`, so they still parse. Per-step + * inference sources are deliberately NOT frozen * here -- they carry credential secrets and are re-resolved from the launch * spec's offering ids at deploy time. */ @@ -600,7 +606,7 @@ export const FrozenApprovalBundle = type({ projection: WorkflowProjectionDefinition, closure: ToolPackageManifest, approvedWireHash: "string > 0", - approvedGrants: "string[]", + approvedGrants: ApprovalItem.array(), }); export type FrozenApprovalBundle = typeof FrozenApprovalBundle.infer; diff --git a/vendor/intx-types/src/testing/index.ts b/vendor/intx-types/src/testing/index.ts new file mode 100644 index 000000000..d2508f547 --- /dev/null +++ b/vendor/intx-types/src/testing/index.ts @@ -0,0 +1,7 @@ +// @intx/types/testing -- waiting helpers for tests. +// +// These exist so a test can wait for a condition without choosing an interval +// to wait for it in. Nothing here belongs in production: a production caller +// that needs to know when something happened should be given the event, not a +// loop that re-reads until it sees it. +export { waitUntil } from "./wait-until"; diff --git a/vendor/intx-types/src/testing/wait-until.test.ts b/vendor/intx-types/src/testing/wait-until.test.ts new file mode 100644 index 000000000..955a4ca0c --- /dev/null +++ b/vendor/intx-types/src/testing/wait-until.test.ts @@ -0,0 +1,44 @@ +import { describe, test, expect } from "bun:test"; + +import { waitUntil } from "./wait-until"; + +describe("waitUntil", () => { + test("returns without a timer turn when the condition already holds", async () => { + let armedTimerRan = false; + const armed = setTimeout(() => { + armedTimerRan = true; + }, 0); + await waitUntil(() => true); + clearTimeout(armed); + // The only yield in `waitUntil` is its loop body's `setTimeout`, which a + // predicate holding on entry never reaches -- so the returned promise + // settles on the microtask queue, and microtasks drain before any timer + // callback runs. Had it yielded, its own `setTimeout` would sit behind the + // one armed above and this flag would be set by the time the await + // resumed. The `0` is an ordering probe, not a duration: no delay makes a + // timer callback overtake the microtask queue. + expect(armedTimerRan).toBe(false); + }); + + test("returns once the condition becomes true", async () => { + let ready = false; + setTimeout(() => { + ready = true; + }, 5); + await waitUntil(() => ready); + expect(ready).toBe(true); + }); + + test("outlasts a condition that takes many turns", async () => { + // No deadline, so the only thing a slow machine changes is how many + // turns this takes. A fixed pause in its place is what decides a run: + // too short on a loaded worker and the caller reads a state that has + // not arrived. + let turns = 0; + await waitUntil(() => { + turns += 1; + return turns > 50; + }); + expect(turns).toBeGreaterThan(50); + }); +}); diff --git a/vendor/intx-types/src/testing/wait-until.ts b/vendor/intx-types/src/testing/wait-until.ts new file mode 100644 index 000000000..514fc7480 --- /dev/null +++ b/vendor/intx-types/src/testing/wait-until.ts @@ -0,0 +1,30 @@ +// Waiting for a condition without betting on how long it takes. +// +// The shape this replaces is `await new Promise((r) => setTimeout(r, 50))` +// followed by an assertion: a duration picked to be longer than the work, +// which on a loaded machine is not, so the assertion reads a state that had +// not arrived yet. The interval decides whether the run passes. +// +// `waitUntil` removes the decision. It re-checks after yielding the event +// loop and carries no deadline, so a slow machine makes it take longer and +// never makes it fail; a condition that never holds is caught by the lane +// timeout, which is where CONVENTIONS.md puts the failsafe for a hang. +// +// It is second-best on purpose. CONVENTIONS.md asks for the SIGNAL -- the +// emitted event, the reported mutation -- and where a double can report what +// it did, that is better than this: it wakes exactly once, on the thing the +// test is actually waiting for, instead of spinning. Prefer a report. Reach +// for this where the state under test has no reporter and giving it one would +// mean reshaping production code for a test's convenience. + +/** + * Resolve once `predicate` returns true, re-checking after each event-loop + * turn. No deadline: see the module comment. + */ +export async function waitUntil(predicate: () => boolean): Promise { + while (!predicate()) { + await new Promise((resolve) => { + setTimeout(resolve, 0); + }); + } +} From 5b9aafa5860b04c7058bba9cf1725edeaf54c143 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Wed, 14 Oct 2026 08:23:28 -0700 Subject: [PATCH 2/3] chore(vendor): re-carry local patches on interchange 0.4.0 Adjudicated the local patch ledger against upstream 0.4.0 (5453d0b0): all 38 ledger entries verified KEEP; the four carried-forward DROP notes are contradicted by evidence at this pin and are kept, with reasons recorded in docs/VENDORING.md; three unclear labels adjudicated no-match with the closest candidates noted; one previously never-ledgered tools-posix divergence (paged read_file description + registry-contract test) recovered and ledgered now. PATCHES.md ledgers, docs/VENDORING.md, and the vendored package.json src-mode export convention arrive here, not in the pristine commit. --- docs/VENDORING.md | 152 ++++++++--- vendor/intx-agent/PATCHES.md | 11 + .../intx-agent/src/audit-integration.test.ts | 9 +- vendor/intx-agent/src/flush-errors.test.ts | 16 +- vendor/intx-inference/PATCHES.md | 48 ++++ vendor/intx-inference/src/errors.test.ts | 7 + vendor/intx-inference/src/reactor.test.ts | 239 +++++++++++++----- vendor/intx-storage-isogit/PATCHES.md | 6 + vendor/intx-tools-posix/PATCHES.md | 34 +++ vendor/intx-tools-posix/src/registry.ts | 1 + .../intx-tools-posix/src/tools-posix.test.ts | 1 + vendor/intx-types/PATCHES.md | 13 + vendor/intx-types/package.json | 4 + 13 files changed, 427 insertions(+), 114 deletions(-) create mode 100644 vendor/intx-tools-posix/PATCHES.md diff --git a/docs/VENDORING.md b/docs/VENDORING.md index 6c93d8a27..8be9e580b 100644 --- a/docs/VENDORING.md +++ b/docs/VENDORING.md @@ -24,17 +24,17 @@ points straight at `./src/*.ts` files rather than a `dist/` build. | Package | Vendor path | License | Synced from upstream commit | Retrieved | Local patches | | ------------------------------------------------------------------------------- | --------------------------------------------------------------- | ------------- | ------------------------------------------ | ---------- | ------------------------------------------------- | -| `@intx/inference` | `vendor/intx-inference/` | LGPL-2.1-only | `1ad010463a6bce6034cded3e078b14db482882a8` | 2026-09-14 | Yes — see `vendor/intx-inference/PATCHES.md` | -| `@intx/types` | `vendor/intx-types/` | LGPL-2.1-only | `1ad010463a6bce6034cded3e078b14db482882a8` | 2026-09-14 | Yes — see `vendor/intx-types/PATCHES.md` | -| `@intx/storage-isogit` | `vendor/intx-storage-isogit/` | LGPL-2.1-only | `1ad010463a6bce6034cded3e078b14db482882a8` | 2026-09-14 | Yes — see `vendor/intx-storage-isogit/PATCHES.md` | -| `@intx/agent` | `vendor/intx-agent/` | LGPL-2.1-only | `1ad010463a6bce6034cded3e078b14db482882a8` | 2026-09-14 | Yes — see `vendor/intx-agent/PATCHES.md` | -| `@intx/authz` | `vendor/intx-authz/` | LGPL-2.1-only | `1ad010463a6bce6034cded3e078b14db482882a8` | 2026-09-14 | None — verbatim | -| `@intx/log` | `vendor/intx-log/` | LGPL-2.1-only | `1ad010463a6bce6034cded3e078b14db482882a8` | 2026-09-14 | None — verbatim | -| `@intx/tools-posix` | `vendor/intx-tools-posix/` | LGPL-2.1-only | `1ad010463a6bce6034cded3e078b14db482882a8` | 2026-09-14 | None — verbatim | -| `@intx/mailbox` | `vendor/intx-mailbox/` | LGPL-2.1-only | `1ad010463a6bce6034cded3e078b14db482882a8` | 2026-09-14 | None — verbatim | -| `@intx/harness` | `vendor/intx-harness/` | LGPL-2.1-only | `1ad010463a6bce6034cded3e078b14db482882a8` | 2026-09-14 | None — verbatim | -| `@intx/mime` | `vendor/intx-mime/` | LGPL-2.1-only | `1ad010463a6bce6034cded3e078b14db482882a8` | 2026-09-14 | None — verbatim | -| `@intx/workflow-host` (partial — `adapters/` + `workflow-definition-loader.ts`) | `vendor/intx-workflow-host/` (two paths, not the whole package) | LGPL-2.1-only | `1ad010463a6bce6034cded3e078b14db482882a8` | 2026-09-14 | None — verbatim | +| `@intx/inference` | `vendor/intx-inference/` | LGPL-2.1-only | `5453d0b01da9d749aa446c4ba34fcf2aeb4549c0` | 2026-10-14 | Yes — see `vendor/intx-inference/PATCHES.md` | +| `@intx/types` | `vendor/intx-types/` | LGPL-2.1-only | `5453d0b01da9d749aa446c4ba34fcf2aeb4549c0` | 2026-10-14 | Yes — see `vendor/intx-types/PATCHES.md` | +| `@intx/storage-isogit` | `vendor/intx-storage-isogit/` | LGPL-2.1-only | `5453d0b01da9d749aa446c4ba34fcf2aeb4549c0` | 2026-10-14 | Yes — see `vendor/intx-storage-isogit/PATCHES.md` | +| `@intx/agent` | `vendor/intx-agent/` | LGPL-2.1-only | `5453d0b01da9d749aa446c4ba34fcf2aeb4549c0` | 2026-10-14 | Yes — see `vendor/intx-agent/PATCHES.md` | +| `@intx/authz` | `vendor/intx-authz/` | LGPL-2.1-only | `5453d0b01da9d749aa446c4ba34fcf2aeb4549c0` | 2026-10-14 | None — verbatim | +| `@intx/log` | `vendor/intx-log/` | LGPL-2.1-only | `5453d0b01da9d749aa446c4ba34fcf2aeb4549c0` | 2026-10-14 | None — verbatim | +| `@intx/tools-posix` | `vendor/intx-tools-posix/` | LGPL-2.1-only | `5453d0b01da9d749aa446c4ba34fcf2aeb4549c0` | 2026-10-14 | Yes — see `vendor/intx-tools-posix/PATCHES.md` | +| `@intx/mailbox` | `vendor/intx-mailbox/` | LGPL-2.1-only | `5453d0b01da9d749aa446c4ba34fcf2aeb4549c0` | 2026-10-14 | None — verbatim | +| `@intx/harness` | `vendor/intx-harness/` | LGPL-2.1-only | `5453d0b01da9d749aa446c4ba34fcf2aeb4549c0` | 2026-10-14 | None — verbatim | +| `@intx/mime` | `vendor/intx-mime/` | LGPL-2.1-only | `5453d0b01da9d749aa446c4ba34fcf2aeb4549c0` | 2026-10-14 | None — verbatim | +| `@intx/workflow-host` (partial — `adapters/` + `workflow-definition-loader.ts`) | `vendor/intx-workflow-host/` (two paths, not the whole package) | LGPL-2.1-only | `5453d0b01da9d749aa446c4ba34fcf2aeb4549c0` | 2026-10-14 | None — verbatim | ## Provenance, ownership, and kill dates @@ -47,18 +47,18 @@ package; the date is the deadline even if it is not. | Vendor path | Upstream repo | Upstream commit | Patched | Why not the published package | Owner | Proposed kill date | | --------------------------------------------------------- | ----------------------- | ------------------------------------------ | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------ | -| `vendor/intx-inference/` | `faremeter/interchange` | `1ad010463a6bce6034cded3e078b14db482882a8` | Yes — `PATCHES.md` | Local fixes not yet upstream | runtime | 2027-03-14 or when patches land upstream and publish | -| `vendor/intx-types/` | `faremeter/interchange` | `1ad010463a6bce6034cded3e078b14db482882a8` | Yes — `PATCHES.md` | Cross-package coupling with `@intx/inference` | runtime | 2027-03-14 or when the coupled trio publishes past `0.3.0` | -| `vendor/intx-storage-isogit/` | `faremeter/interchange` | `1ad010463a6bce6034cded3e078b14db482882a8` | Yes — `PATCHES.md` | Cross-package coupling with `@intx/inference` | runtime | 2027-03-14 or when the coupled trio publishes past `0.3.0` | -| `vendor/intx-agent/` | `faremeter/interchange` | `1ad010463a6bce6034cded3e078b14db482882a8` | Yes — `PATCHES.md` | Vendored at Interchange head ahead of npm | runtime | 2027-03-14 or when `@intx/agent@>=0.4.0` publishes | -| `vendor/intx-authz/` | `faremeter/interchange` | `1ad010463a6bce6034cded3e078b14db482882a8` | No | Vendored at Interchange head ahead of npm | runtime | 2027-03-14 or when `@intx/authz@>=0.4.0` publishes | -| `vendor/intx-log/` | `faremeter/interchange` | `1ad010463a6bce6034cded3e078b14db482882a8` | No | Vendored at Interchange head ahead of npm | runtime | 2027-03-14 or when `@intx/log@>=0.4.0` publishes | -| `vendor/intx-tools-posix/` | `faremeter/interchange` | `1ad010463a6bce6034cded3e078b14db482882a8` | No | Vendored at Interchange head ahead of npm | runtime | 2027-03-14 or when `@intx/tools-posix@>=0.4.0` publishes | -| `vendor/intx-mailbox/` | `faremeter/interchange` | `1ad010463a6bce6034cded3e078b14db482882a8` | No | Never published to npm (verified 2026-09-07: registry 404 for all versions) | step-1 | 2027-03-14 or when any `@intx/mailbox` version publishes to npm | -| `vendor/intx-harness/` | `faremeter/interchange` | `1ad010463a6bce6034cded3e078b14db482882a8` | No | `driveConnectorReplies`/`AgentEventStream` (`src/reply-drain.ts`) is past npm `0.3.0` (verified 2026-09-07: absent from the published tarball) | step-1 | 2027-03-14 or when a published `@intx/harness` exports `driveConnectorReplies` | -| `vendor/intx-mime/` | `faremeter/interchange` | `1ad010463a6bce6034cded3e078b14db482882a8` | No | `buildMessageHeaders` is past npm `0.3.0` (verified 2026-09-07: absent from the published tarball); `@intx/mailbox` re-exports it | step-1 | 2027-03-14 or when a published `@intx/mime` exports `buildMessageHeaders` | -| `vendor/intx-workflow-host/adapters/` | `faremeter/interchange` | `1ad010463a6bce6034cded3e078b14db482882a8` | No | App-internal: `substrate-mailbox-store.ts` has never been published in any `@intx/workflow-host` release (verified 2026-09-07: absent from the `0.3.0` tarball) | step-1 | 2027-03-14 or when a published `@intx/workflow-host` exports `createSubstrateMailboxStore` | -| `vendor/intx-workflow-host/workflow-definition-loader.ts` | `faremeter/interchange` | `1ad010463a6bce6034cded3e078b14db482882a8` | No | Director loading from a workflow closure (`loadWorkflowDirectorRegistryFromClosure`); never published in any `@intx/workflow-host` release (verified 2026-09-14: absent from the `0.3.0` tarball) | step-1 | 2027-03-14 or when a published `@intx/workflow-host` exports `loadWorkflowDirectorRegistryFromClosure` | +| `vendor/intx-inference/` | `faremeter/interchange` | `5453d0b01da9d749aa446c4ba34fcf2aeb4549c0` | Yes — `PATCHES.md` | Local fixes not yet upstream | runtime | 2027-03-14 or when patches land upstream and publish | +| `vendor/intx-types/` | `faremeter/interchange` | `5453d0b01da9d749aa446c4ba34fcf2aeb4549c0` | Yes — `PATCHES.md` | Cross-package coupling with `@intx/inference` | runtime | 2027-03-14 or when the coupled trio publishes past `0.3.0` | +| `vendor/intx-storage-isogit/` | `faremeter/interchange` | `5453d0b01da9d749aa446c4ba34fcf2aeb4549c0` | Yes — `PATCHES.md` | Cross-package coupling with `@intx/inference` | runtime | 2027-03-14 or when the coupled trio publishes past `0.3.0` | +| `vendor/intx-agent/` | `faremeter/interchange` | `5453d0b01da9d749aa446c4ba34fcf2aeb4549c0` | Yes — `PATCHES.md` | Vendored at Interchange head ahead of npm | runtime | 2027-03-14 or when `@intx/agent@>=0.4.0` publishes | +| `vendor/intx-authz/` | `faremeter/interchange` | `5453d0b01da9d749aa446c4ba34fcf2aeb4549c0` | No | Vendored at Interchange head ahead of npm | runtime | 2027-03-14 or when `@intx/authz@>=0.4.0` publishes | +| `vendor/intx-log/` | `faremeter/interchange` | `5453d0b01da9d749aa446c4ba34fcf2aeb4549c0` | No | Vendored at Interchange head ahead of npm | runtime | 2027-03-14 or when `@intx/log@>=0.4.0` publishes | +| `vendor/intx-tools-posix/` | `faremeter/interchange` | `5453d0b01da9d749aa446c4ba34fcf2aeb4549c0` | Yes — `PATCHES.md` | Vendored at Interchange head ahead of npm; the read_file paging description patch rides the coupled size-cap patch (see `vendor/intx-tools-posix/PATCHES.md`) | runtime | 2027-03-14 or when `@intx/tools-posix@>=0.4.0` publishes | +| `vendor/intx-mailbox/` | `faremeter/interchange` | `5453d0b01da9d749aa446c4ba34fcf2aeb4549c0` | No | Never published to npm (verified 2026-09-07: registry 404 for all versions) | step-1 | 2027-03-14 or when any `@intx/mailbox` version publishes to npm | +| `vendor/intx-harness/` | `faremeter/interchange` | `5453d0b01da9d749aa446c4ba34fcf2aeb4549c0` | No | `driveConnectorReplies`/`AgentEventStream` (`src/reply-drain.ts`) is past npm `0.3.0` (verified 2026-09-07: absent from the published tarball) | step-1 | 2027-03-14 or when a published `@intx/harness` exports `driveConnectorReplies` | +| `vendor/intx-mime/` | `faremeter/interchange` | `5453d0b01da9d749aa446c4ba34fcf2aeb4549c0` | No | `buildMessageHeaders` is past npm `0.3.0` (verified 2026-09-07: absent from the published tarball); `@intx/mailbox` re-exports it | step-1 | 2027-03-14 or when a published `@intx/mime` exports `buildMessageHeaders` | +| `vendor/intx-workflow-host/adapters/` | `faremeter/interchange` | `5453d0b01da9d749aa446c4ba34fcf2aeb4549c0` | No | App-internal: `substrate-mailbox-store.ts` has never been published in any `@intx/workflow-host` release (verified 2026-09-07: absent from the `0.3.0` tarball) | step-1 | 2027-03-14 or when a published `@intx/workflow-host` exports `createSubstrateMailboxStore` | +| `vendor/intx-workflow-host/workflow-definition-loader.ts` | `faremeter/interchange` | `5453d0b01da9d749aa446c4ba34fcf2aeb4549c0` | No | Director loading from a workflow closure (`loadWorkflowDirectorRegistryFromClosure`); never published in any `@intx/workflow-host` release (verified 2026-09-14: absent from the `0.3.0` tarball) | step-1 | 2027-03-14 or when a published `@intx/workflow-host` exports `loadWorkflowDirectorRegistryFromClosure` | ### The 2026-09-07 step-1 vendor pass @@ -148,6 +148,89 @@ No first-party consumer imports tool-packaging; stays on transitive published 0.3.0. Revisit if a future ticket establishes tool packages from closures at runtime. +### The 2026-10-14 re-sync to `0.4.0` (`5453d0b0`, CL-10651) + +All eleven paths moved `1ad0104` → `5453d0b0` (npm `0.4.0`, retrieved +2026-10-14). Upstream churn in the vendored trees for this range is +small: package version bumps across the board, plus +`packages/inference` test-infra reworks (`reactor.test.ts` adopting +`@intx/types/testing`'s new `waitUntil`; `default-director.test.ts`), +three source files in `@intx/types` (`base64.ts` chunked encoding, +`grants.ts` gaining `ApprovalItem`, `sidecar.ts` widening +`approvedGrants` to `ApprovalItem[]`), a new `@intx/agent` testing helper +(`testing/reactor-waiters.ts`, `waitForReactorDone` moved out of three +agent test files), and a new `@intx/log` probe (`default-sink-probe.ts`). +No vendored production source besides those three `@intx/types` files +changed upstream in this range, so the four patched packages re-carried +every ledger entry against effectively unchanged upstream shapes. + +Procedure: `bin/vendor-patch-diff` re-derived the local divergence +against the recorded SHA before anything was overwritten; verbatim trees +were overwritten from the new pin; the two agent test files and the +inference `reactor.test.ts` that had both local patches and upstream +changes were three-way merged (`git merge-file`), and every other +patched file kept its prior state because upstream did not touch it. + +Ledger adjudication (ticket CL-10651, verified against upstream at this +pin — all line-grep evidence below is against pristine `5453d0b0`): + +- **All ledger entries KEEP.** None is subsumed upstream at `0.4.0`: + `isStreamTerminal`, `ephemeralTurns`, `isPollOnlyPendingBatch`, + `loadErrors`, `turnsRevision`/`deepFreeze`, `isCommitting`, + `MAX_LINE_LENGTH`, and `ApprovalItem`-independent `stopReason` are all + absent from the `0.4.0` tree. +- The four carried-forward DROP notes are contradicted by evidence at + this pin and stay: the **context-transforms pair** survives because + `@intx/agent` `0.4.0` still exposes no `contextTransforms` field on + its assembly env (upstream `@intx/inference` `assembly.ts` accepts it + directly; the published `agent` package does not forward it, so the + `deps` channel remains the only path); the **agent flush chain** + survives because `loadErrors` is absent upstream; the + **commitment-boundary** redesign survives because `isCommitting` is + absent upstream; the **frozen-turns pair** survives because + `turnsRevision`/`deepFreeze` are absent upstream. +- Three ticket labels adjudicated **no-match**: no vendored tree or + upstream `0.4.0` code corresponds to `harness-ts-normok-model-compat`; + the closest model-compatibility candidate is + `providers-ts-anthropic-adaptive-fable-5-1` (which stays). "Mailbox + signed outbound" corresponds to upstream's own signing composition + (`fcdbc8a5`, "Compose signed outbound mail in one place") carried + natively by the verbatim `@intx/mime`/`@intx/mailbox` trees — no local + patch exists to adjudicate. "Adapter manifest" corresponds to upstream + `4cc6972` ("Refuse sidecar reuse across an adapter manifest change"), + past this pin and touching no vendored path. +- `expectedFrom` reconciliation: upstream still carries the field at + `0.4.0` (reactor marker spread; `runtime.ts`/`store.ts` validators). + Its removal lands after `0.4.0` (`0f72f95`, "Stop treating a + correlation id as an authenticated claim"; `1ff8805`, "Stop inventing + an identity for a message that names none"). Re-syncing past that + removal drops the field with zero first-party fallout (`grep` of + first-party `src/` is clean today) because this repo never consumed + `expectedFrom`; the meaningful change is the correlator-as-capability + model, which CL-10678 handles app-side. + +New 0.4.0 APIs adopted where apt; adoption is deliberately modest: + +- `@intx/types/testing` (`waitUntil`) — the subpath export is wired into + the vendored `@intx/types` tree verbatim; nothing first-party imports + it yet, and vendored upstream tests consume it directly. +- `ApprovalItem` — arrives verbatim inside `@intx/types`; no first-party + adoption in this PR (the approval-correlation work CL-10678 is the + natural adopter). +- `@intx/agent` testing helpers (`waitForReactorDone`) — arrive + verbatim in the vendored tree; first-party e2e keeps its own helpers. +- `PerCallInferenceOptions`/`mergeInferenceOptions` (`d1835be`) is + **post-0.4.0** (only on upstream `main`, not `5453d0b0`), so it is + explicitly out of scope here (CL-10656 owns the HEAD drift). For the + nudge use-case, `ephemeralTurns` stays: the nudge director needs + prompt-only turns the per-call-options path does not model. + +`vendor/intx-tools-posix/` moves from verbatim to patched in this sync: +the pre-existing (never-ledgered) read_file paging-description +divergence was found by re-running `bin/vendor-patch-diff` during this +sync and is now ledgered in `vendor/intx-tools-posix/PATCHES.md` with +markers, rather than silently dropped or reverted. + The license column records what each package declares in its own `package.json`; the corresponding `LICENSE` file travels with every vendored tree and is never edited during a sync. Corbits Code is distributed under @@ -173,8 +256,14 @@ repo imports now resolves to vendored source, except `@intx/crypto` and `@intx/tools-lsp`, which remain direct published-npm imports at `0.3.0`. None of the four carried local patches at vendoring time. `@intx/agent` later gained the error-seq -patches ledgered in `vendor/intx-agent/PATCHES.md`; the other three remain -verbatim upstream copies. `@intx/tools-lsp` remains on published npm +patches ledgered in `vendor/intx-agent/PATCHES.md`; the other three were +verbatim upstream copies until the 2026-10-14 re-sync, whose +`bin/vendor-patch-diff` re-derivation surfaced a never-ledgered +`registry.ts` paging-description divergence in `@intx/tools-posix` +(companion to the coupled +`size-cap-ts-paged-read-file` patch) — now +ledgered in `vendor/intx-tools-posix/PATCHES.md` instead of silently +dropped. `@intx/tools-lsp` remains on published npm (`0.3.0`) — it is a thin adapter whose transitive `@intx/*` dependencies resolve to the vendored workspaces via root `overrides`, so it tracks the vendored set @@ -244,14 +333,15 @@ those packages now resolves to the single root instance. As of this sync, ## Patched vs. verbatim -Of the twelve vendored paths, four carry local patches: -`@intx/inference`, `@intx/types`, `@intx/storage-isogit`, and -`@intx/agent` — every divergence is a real fix not yet upstream, listed -one-per-patch in that package's `PATCHES.md`. The other eight paths -(six whole packages plus the two partial `vendor/intx-workflow-host/` +Of the twelve vendored paths, five carry local patches: +`@intx/inference`, `@intx/types`, `@intx/storage-isogit`, `@intx/agent`, +and (since the 2026-10-14 re-sync) `@intx/tools-posix` — every +divergence is a real fix not yet upstream, listed +one-per-patch in that package's `PATCHES.md`. The other seven paths +(five whole packages plus the two partial `vendor/intx-workflow-host/` paths) are verbatim copies of upstream — no modifications. A diff against a pristine upstream checkout at the same commit and paths will show 100% -upstream-authored lines for those eight. +upstream-authored lines for those seven. ## Notable upstream shape changes carried by the 2026-08-22 sync diff --git a/vendor/intx-agent/PATCHES.md b/vendor/intx-agent/PATCHES.md index 378195731..2b786fcf4 100644 --- a/vendor/intx-agent/PATCHES.md +++ b/vendor/intx-agent/PATCHES.md @@ -15,6 +15,17 @@ All three entries re-carried as-is; upstream has no `loadErrors` No adaptation needed — upstream did not touch these regions. Upstream's `credentialId` auth-model rewrite does not intersect these patches. +### 2026-10-14 re-sync (upstream `5453d0b0` — npm `0.4.0`, CL-10651) + +All three entries re-carried unchanged: upstream did not touch +`agent.ts` or `testing/audit-noop.ts` in this range, and `loadErrors` is +still absent upstream. Upstream moved `waitForReactorDone` out of three +test files into the new `testing/reactor-waiters.ts` (exported from +`testing/index.ts`); `flush-errors.test.ts` and +`audit-integration.test.ts` were three-way merged so the local +`loadErrors` hooks and the FORBIDDEN_DEPS credential-failure companion +suite re-carried onto the new shapes. + ## agent-ts-resume-error-seq `agent.ts` — `createAgent` resumes `errorSeq` from `auditStore.loadErrors` diff --git a/vendor/intx-agent/src/audit-integration.test.ts b/vendor/intx-agent/src/audit-integration.test.ts index a3a910ba8..e9a26516b 100644 --- a/vendor/intx-agent/src/audit-integration.test.ts +++ b/vendor/intx-agent/src/audit-integration.test.ts @@ -40,6 +40,7 @@ import { defineDirector } from "./director"; import { createDirectorRegistry } from "./director-registry"; import type { BaseEnv } from "./env"; import { defineTool } from "./tool"; +import { waitForReactorDone } from "./testing"; // The audit pipeline needs an inference source to construct the // reactor, but the test director never calls infer(); the URL is @@ -206,14 +207,6 @@ function inboundConversation(): ReturnType { }); } -async function waitForReactorDone( - stream: AsyncIterable<{ type: string }>, -): Promise { - for await (const event of stream) { - if (event.type === "reactor.done") return; - } -} - describe("agent audit integration", () => { let workDir: string; diff --git a/vendor/intx-agent/src/flush-errors.test.ts b/vendor/intx-agent/src/flush-errors.test.ts index 187cd5cb5..46dc12e00 100644 --- a/vendor/intx-agent/src/flush-errors.test.ts +++ b/vendor/intx-agent/src/flush-errors.test.ts @@ -33,6 +33,7 @@ import { defineDirector } from "./director"; import { createDirectorRegistry } from "./director-registry"; import type { BaseEnv } from "./env"; import { permissiveAuthorize } from "./testing/authorize-allow"; +import { waitForReactorDone } from "./testing"; // An unreachable URL causes the inference call to fail with a network // error, which the reactor surfaces as an `inference.error` event. @@ -212,18 +213,9 @@ function inboundConversation(): ReturnType { }); } -// Drain agent.stream() until a `reactor.done` event is observed, then -// resolve. The agent's terminal event signals the reactor has settled -// and any pending audit-flush has had a chance to run via the -// assembly's onShutdown hook. -async function waitForReactorDone( - stream: AsyncIterable<{ type: string }>, -): Promise { - for await (const event of stream) { - if (event.type === "reactor.done") return; - } -} - +// Locally patched — see vendor/intx-agent/PATCHES.md#agent-ts-duplicate-error-flush +// (the FORBIDDEN_DEPS credential-failure suite below is the companion test +// block re-carried with the auth-recovery and duplicate-flush entries). const FORBIDDEN_DEPS = { ...createDefaultDependencies(), fetch: async () => diff --git a/vendor/intx-inference/PATCHES.md b/vendor/intx-inference/PATCHES.md index c4fba90b6..6cb73fa35 100644 --- a/vendor/intx-inference/PATCHES.md +++ b/vendor/intx-inference/PATCHES.md @@ -139,6 +139,52 @@ new upstream moved under the patch: `src/config/source-credentials.ts`, handed to the vendored trees as their resolver). +### 2026-10-14 re-sync (upstream `5453d0b0` — npm `0.4.0`, CL-10651) + +Every entry below re-verified against the pristine `5453d0b0` tree; none +was dropped as upstream-absorbed. The ticket's carried-forward DROP +suspicions are contradicted by direct greps of the `0.4.0` tree — +`isStreamTerminal`, `isPollOnlyPendingBatch`, `ephemeralTurns`, +`loadErrors`, `turnsRevision`/`deepFreeze`, `isCommitting`, and +`ClassifiedAbortRaw`-grade `classifyAbortError(reason)` plumbing are all +absent upstream at this pin — so every entry KEEPs and no disposition +label changes. Upstream churn in this package for the range is limited +to `reactor.test.ts` (test-infra: new `@intx/types/testing` `waitUntil` +adoption and race-window rewrites) and `default-director.test.ts`; **no +upstream-producing source file under a ledger entry changed**, so the +src/ entries re-carried byte-identically with zero adaptation. Marker +spot-checks at the recorded positions are unchanged for the same reason +(`providers/openai.ts:574`, `harness.ts:644/1528/1606/1635/1875`, +`errors.ts:54`, `providers/anthropic.ts:825/832/1084`, +`providers/google-genai-files.ts:172-174`). + +- `reactor.test.ts` was the only file with both local content and + upstream changes; it was three-way merged (ours/base/theirs via + `git merge-file`). `reactor-test-frozen-turns-mutation` re-applied + onto upstream's rewritten probe; the local doom-loop trivial-shell + suite (`reactor-ts-doom-loop-trivial-shell-churn` companion tests) and + the credential-failure terminal tests + (`harness-ts-auth-recovery` companions) carried through intact. +- `providers/anthropic.test.ts` (CL-7783/adaptive suites) was untouched + in this range and keeps its prior state. +- New upstream test dependencies on `@intx/types/testing` (`waitUntil`) + resolve through the vendored `@intx/types` tree, which now carries the + `./testing` subpath export. +- CL-7783's `stopReason` chain verified end-to-end at this pin: + `types/runtime.ts:1449/1715`, `providers/anthropic.ts:825/832/1084`, + and the harness sites `harness.ts:380/1128-1136/1200` all present. +- Carried-patch coverage at this pin: `ephemeralTurns` and the + context-transforms `deps` channel are pinned by first-party + `e2e/vendored-carry.test.ts` (both integration tests pass) plus + `src/provider/anthropic-cache-breakpoint.test.ts` and + `src/subagent/nudge-director.test.ts`; the poll-exemption predicate by + `src/subagent/poll-exempt.test.ts` (wired verbatim through + `src/provider/inference-dependencies.ts`); the stream-terminal + consumer by `src/provider/grok-responses.test.ts` and + `src/provider/codex-responses-sse.test.ts`; `classifyAbortError`'s + reason now gains an in-tree companion test (added this sync) and is + exercised on the abort paths of `e2e/reactor-approval-acceptance.test.ts`. + ## adapter-ts-stream-terminal-detector `adapter.ts` — Adds `StreamTerminalDetector`/`ProviderAdapter.isStreamTerminal`. @@ -225,6 +271,8 @@ four abort-check sites in `harness.ts`. optional `reason` param to `classifyAbortError` and enriching `raw`. **Removal path:** Upstream PR adding the optional `reason` parameter and `ClassifiedAbortRaw` type. No kill date until upstream ships it. +Companion test: the "carries a caller-supplied reason as raw.origin" case in +`errors.test.ts` (added at the CL-10651 sync). **Re-carry:** clean three-way at `0205b07b`, zero conflicts. Low risk — `errors.ts` sees little upstream churn. diff --git a/vendor/intx-inference/src/errors.test.ts b/vendor/intx-inference/src/errors.test.ts index 7323a3f51..fdd05226a 100644 --- a/vendor/intx-inference/src/errors.test.ts +++ b/vendor/intx-inference/src/errors.test.ts @@ -87,6 +87,13 @@ describe("classifyAbortError", () => { const err = classifyAbortError(); expect(err.category).toBe("aborted"); }); + + // Locally patched — see vendor/intx-inference/PATCHES.md#errors-ts-classify-abort-reason + test("carries a caller-supplied reason as raw.origin", () => { + const err = classifyAbortError("user-stop"); + expect(err.category).toBe("aborted"); + expect(err.raw).toEqual({ origin: "user-stop" }); + }); }); describe("classifyStreamError", () => { diff --git a/vendor/intx-inference/src/reactor.test.ts b/vendor/intx-inference/src/reactor.test.ts index d7d86203f..296568a55 100644 --- a/vendor/intx-inference/src/reactor.test.ts +++ b/vendor/intx-inference/src/reactor.test.ts @@ -9,6 +9,7 @@ import { createDefaultDependencies } from "./providers"; import { createDefaultDirector } from "./default-director"; import { assertWellFormedToolSequence } from "./turns"; import { createInboundMessage } from "@intx/mime"; +import { waitUntil } from "@intx/types/testing"; import type { ReactorDirector, @@ -146,28 +147,23 @@ function collectEvents(): { }; } -function waitForEvent( +// Resolve with the first collected event matching `predicate`, re-checking +// after each event-loop turn. No deadline: a wall-clock number here is a race +// the test loses under load, and a never-arriving event is the lane timeout's +// job to report (CONVENTIONS.md, "Synchronizing on State, Not Time"). +async function waitForEvent( events: ReactorEmittedEvent[], predicate: (e: ReactorEmittedEvent) => boolean, - timeoutMs = 2000, ): Promise { - return new Promise((resolve, reject) => { - const deadline = setTimeout( - () => reject(new Error("Timed out waiting for event")), - timeoutMs, - ); - - function check() { - const found = events.find(predicate); - if (found !== undefined) { - clearTimeout(deadline); - resolve(found); - return; - } - setTimeout(check, 10); - } - check(); + let found: ReactorEmittedEvent | undefined; + await waitUntil(() => { + found = events.find(predicate); + return found !== undefined; }); + if (found === undefined) { + throw new Error("unreachable: waitUntil resolved without a match"); + } + return found; } // Simple inbound message factory. Delegates to the mail-builder so the @@ -329,9 +325,8 @@ function createTestReactor( function waitFor( type: ReactorEmittedEvent["type"], - timeoutMs = 2000, ): Promise { - return waitForEvent(events, (e) => e.type === type, timeoutMs); + return waitForEvent(events, (e) => e.type === type); } return { reactor, events, waitFor }; @@ -1079,11 +1074,35 @@ describe("createReactor — gate lifecycle", () => { const commitGate = new Promise((resolve) => { releaseCommit = resolve; }); + // Resolves when the reactor reaches the held commit, so the test observes + // the window rather than guessing when it opens. + let signalCommitEntered: (() => void) | undefined; + const commitEntered = new Promise((resolve) => { + signalCommitEntered = resolve; + }); + // Whether `blocked` had been emitted at the moment the commit was + // released -- read inside the store, which is the last point still inside + // the window. A reactor that emitted before or concurrently with the + // commit shows up here as `true`. + let blockedDuringCommit: boolean | undefined; + let signalCommitObserved: (() => void) | undefined; + const commitObserved = new Promise((resolve) => { + signalCommitObserved = resolve; + }); const base = makeContextStore(); const contextStore: ContextStore = { ...base, async writeMetadata(metadata, signal) { + signalCommitEntered?.(); await commitGate; + // Record the suspend's own commit only. A later cycle's commit runs + // after `blocked` has legitimately fired and must not overwrite it. + if (blockedDuringCommit === undefined) { + blockedDuringCommit = events.some( + (e) => e.type === "reactor.gate.blocked", + ); + signalCommitObserved?.(); + } return base.writeMetadata(metadata, signal); }, }; @@ -1103,13 +1122,19 @@ describe("createReactor — gate lifecycle", () => { reactor.start(); reactor.deliver(makeInboundMessage()); - // The commit is held, so a correctly-ordered reactor has not emitted - // `blocked` yet; a reactor that emits before committing would have. - await new Promise((r) => setTimeout(r, 50)); + // The reactor is parked inside the held commit, so a correctly-ordered + // reactor has not emitted `blocked` yet; a reactor that emits before + // committing would have emitted it before reaching this point. + await commitEntered; expect(events.some((e) => e.type === "reactor.gate.blocked")).toBe(false); - // Release the durable commit; only now may `blocked` be emitted. + // Release the durable commit; only now may `blocked` be emitted. The + // in-store check runs before the release returns control to the reactor, + // so it closes the window the check above leaves open: an emit that races + // the commit instead of preceding it. releaseCommit?.(); + await commitObserved; + expect(blockedDuringCommit).toBe(false); await waitFor("reactor.gate.blocked"); reactor.abort("admin_kill"); @@ -1128,10 +1153,18 @@ describe("createReactor — gate lifecycle", () => { const commitGate = new Promise((resolve) => { releaseCommit = resolve; }); + // Resolves when the reactor reaches the held commit. suspendOnGate arms + // the gate's timeout timer before it commits, so once this resolves the + // 30ms timer below is already scheduled. + let signalCommitEntered: (() => void) | undefined; + const commitEntered = new Promise((resolve) => { + signalCommitEntered = resolve; + }); const base = makeContextStore(); const contextStore: ContextStore = { ...base, async writeMetadata(metadata, signal) { + signalCommitEntered?.(); await commitGate; return base.writeMetadata(metadata, signal); }, @@ -1152,8 +1185,15 @@ describe("createReactor — gate lifecycle", () => { reactor.start(); reactor.deliver(makeInboundMessage()); - // Let the gate's timeout elapse while the commit is still held. Neither the - // block nor the clear may surface until the commit is released. + // The gate timer firing inside the commit window is the race under test, + // and nothing observable reports it -- a deferred clear is by definition + // not emitted. So the wait below is ordered against the timer rather than + // sized to outlast it: the gate's 30ms deadline was set before + // `commitEntered` resolved, and this timer's 80ms deadline is set after, + // so the gate's deadline is the earlier of the two and the event loop + // fires it first no matter how loaded the machine is. Neither the block + // nor the clear may surface until the commit is released. + await commitEntered; await new Promise((r) => setTimeout(r, 80)); expect(events.some((e) => e.type === "reactor.gate.blocked")).toBe(false); expect(events.some((e) => e.type === "reactor.gate.cleared")).toBe(false); @@ -1191,7 +1231,7 @@ describe("createReactor — gate lifecycle", () => { reactor.start(); reactor.deliver(makeInboundMessage()); - await waitFor("reactor.done", 3000); + await waitFor("reactor.done"); const cleared = getEvent(events, "reactor.gate.cleared"); expect(cleared.data.reason).toBe("timeout"); @@ -2082,7 +2122,7 @@ describe("createReactor — correlation", () => { // Deliver the correlated response. reactor.deliver(makeInboundMessage(CORR_ID)); - await waitFor("reactor.done", 3000); + await waitFor("reactor.done"); const correlated = getEvent(events, "message.correlated"); expect(correlated.data.correlationId).toBe(CORR_ID); @@ -2532,9 +2572,11 @@ describe("createReactor — director misbehavior", () => { reactor.start(); reactor.deliver(makeInboundMessage()); - // After wait, deliver a second message which triggers done. - // Small delay to let the reactor process the first message. - setTimeout(() => reactor.deliver(makeInboundMessage()), 20); + // After wait, deliver a second message which triggers done. The + // director's own counter reports that the first message was processed, so + // the second delivery is ordered against that rather than a delay. + await waitUntil(() => messageCount >= 1); + reactor.deliver(makeInboundMessage()); await waitFor("reactor.done"); expect(messageCount).toBe(2); @@ -2563,8 +2605,10 @@ describe("createReactor — reply action", () => { reactor.start(); reactor.deliver(makeInboundMessage()); - // After reply, reactor waits for next event. Deliver another message. - setTimeout(() => reactor.deliver(makeInboundMessage()), 20); + // After reply, reactor waits for next event. Deliver another message + // once the director reports it handled the first one. + await waitUntil(() => messageCount >= 1); + reactor.deliver(makeInboundMessage()); await waitFor("reactor.done"); const replyEvent = getEvent(events, "connector.reply"); @@ -2646,8 +2690,11 @@ describe("createReactor — checkpoint failure", () => { reactor.start(); reactor.deliver(makeInboundMessage()); - // After checkpoint failure + wait, deliver another message. - setTimeout(() => reactor.deliver(makeInboundMessage()), 50); + // After checkpoint failure + wait, deliver another message. The + // non-fatal reactor.error the failed checkpoint emits is the signal that + // the first cycle got that far. + await waitFor("reactor.error"); + reactor.deliver(makeInboundMessage()); await waitFor("reactor.done"); expect(checkpointCalled).toBe(true); @@ -2898,7 +2945,7 @@ describe("createReactor — dequeue priority", () => { // director shuts down on it, and tool.done is never processed. const order: string[] = []; - const { reactor, waitFor } = createTestReactor({ + const { reactor, events, waitFor } = createTestReactor({ contextStore: makeContextStore([assistantToolCallTurn(["tc-pending"])]), director: { async decide(event, _state, caps) { @@ -2920,12 +2967,22 @@ describe("createReactor — dequeue priority", () => { }, }, toolRunner: makeToolRunner(async (call) => { - // Deliver a second message while the tool runs, then yield to a real - // timer so deliver()'s async enqueue lands before executeTools + // Deliver a second message while the tool runs and wait for its + // enqueue, so deliver()'s async enqueue lands before executeTools // enqueues tool.done. This pins the queue order to - // [message.received, tool.done] — the interleave that triggers the bug. - reactor.deliver(makeInboundMessage()); - await new Promise((resolve) => setTimeout(resolve, 10)); + // [message.received, tool.done] — the interleave that triggers the + // bug. processDelivery emits `message.received` in the same + // synchronous step as the enqueue, so the emitted event carrying this + // message's id is the enqueue's own signal. + const mail = makeInboundMessage(); + reactor.deliver(mail); + await waitUntil(() => + events.some( + (e) => + e.type === "message.received" && + e.data.message.headers.messageId === mail.headers.messageId, + ), + ); return { callId: call.id, content: "ok" }; }), }); @@ -2949,7 +3006,7 @@ describe("createReactor — dequeue priority", () => { const order: string[] = []; let toolDoneCount = 0; - const { reactor, waitFor } = createTestReactor({ + const { reactor, events, waitFor } = createTestReactor({ contextStore: makeContextStore([assistantToolCallTurn(["tc-a", "tc-b"])]), director: { async decide(event, _state, caps) { @@ -2975,8 +3032,19 @@ describe("createReactor — dequeue priority", () => { }, }, toolRunner: makeToolRunner(async (call) => { - reactor.deliver(makeInboundMessage()); - await new Promise((resolve) => setTimeout(resolve, 10)); + // Mail delivered mid-batch must be enqueued before this call's + // tool.done. The emitted `message.received` carrying this message's + // id is published in the same synchronous step as the enqueue, so it + // is the signal that the delivery landed. + const mail = makeInboundMessage(); + reactor.deliver(mail); + await waitUntil(() => + events.some( + (e) => + e.type === "message.received" && + e.data.message.headers.messageId === mail.headers.messageId, + ), + ); return { callId: call.id, content: "ok" }; }), }); @@ -3022,8 +3090,10 @@ describe("createReactor — dequeue priority", () => { }, }, toolRunner: makeToolRunner(async (call) => { + // abort() enqueues the abort event synchronously, so it is already + // queued ahead of this call's tool.done when abort() returns; no wait + // is needed to order the two. reactor.abort("admin_kill"); - await new Promise((resolve) => setTimeout(resolve, 10)); return { callId: call.id, content: "ok" }; }), }); @@ -3044,7 +3114,7 @@ describe("createReactor — dequeue priority", () => { const order: string[] = []; let inferenceCount = 0; - const { reactor, waitFor } = createTestReactor({ + const { reactor, events, waitFor } = createTestReactor({ director: { async decide(event, _state, caps) { order.push(event.type); @@ -3062,10 +3132,19 @@ describe("createReactor — dequeue priority", () => { }, inferenceRunner: async function* (opts) { inferenceCount += 1; - // Deliver mail mid-inference, then yield to a real timer so the - // message is enqueued before inference.done. - reactor.deliver(makeInboundMessage()); - await new Promise((resolve) => setTimeout(resolve, 10)); + // Deliver mail mid-inference and wait for its enqueue, so the message + // is queued before inference.done. The emitted `message.received` + // carrying this message's id is published in the same synchronous + // step as the enqueue. + const mail = makeInboundMessage(); + reactor.deliver(mail); + await waitUntil(() => + events.some( + (e) => + e.type === "message.received" && + e.data.message.headers.messageId === mail.headers.messageId, + ), + ); yield { type: "inference.done", seq: opts.nextSeq(), @@ -3488,7 +3567,8 @@ describe("createReactor — state snapshot inspection", () => { reactor.deliver(makeInboundMessage()); // Wait for the first message to be processed, then deliver a second. - setTimeout(() => reactor.deliver(makeInboundMessage()), 30); + await waitUntil(() => messageCount >= 1); + reactor.deliver(makeInboundMessage()); await waitFor("reactor.done"); if (secondSnapshot === undefined) throw new Error("unreachable"); @@ -3524,8 +3604,13 @@ describe("createReactor — deliver after done", () => { reactor.deliver(makeInboundMessage()); reactor.deliver(makeInboundMessage()); - // Give the event loop a chance to process any spurious events. - await new Promise((resolve) => setTimeout(resolve, 50)); + // A macrotask boundary drains the microtask queue, which is the whole + // window in which a spurious event could appear: the only path from + // deliver() to an emitted event is processDelivery's async body, and for + // an uncorrelated message (these carry no correlationId) tryCorrelate + // returns on its first statement, so the emit would land in a microtask. + // If the done guard had regressed, the event would be here by now. + await new Promise((resolve) => setTimeout(resolve, 0)); // No new events should have been emitted after reactor.done. expect(events.length).toBe(eventsBeforeDeliver); @@ -5725,9 +5810,8 @@ function createDirectReactor(opts: { function waitForType( type: ReactorEmittedEvent["type"], - timeoutMs = 2000, ): Promise { - return waitForEvent(events, (e) => e.type === type, timeoutMs); + return waitForEvent(events, (e) => e.type === type); } return { reactor, events, waitFor: waitForType }; } @@ -5843,7 +5927,12 @@ describe("createReactor — transform chain ordering and compact action", () => reactor.start(); reactor.deliver(makeInboundMessage()); - setTimeout(() => reactor.deliver(makeInboundMessage()), 30); + // The second message drives the terminal done; deliver it once the + // compact cycle it must follow has committed. + await waitUntil(() => + recording.commits.some((c) => c.message.startsWith("Cycle: compaction")), + ); + reactor.deliver(makeInboundMessage()); await waitFor("reactor.done"); expect(messages).toBeGreaterThanOrEqual(1); @@ -5966,7 +6055,14 @@ describe("createReactor — transform chain ordering and compact action", () => // The director did not chain compact + infer; it only emitted compact. // Drive the next infer by delivering another message that re-runs the // event loop. The director's message.received → infer rule will fire. - setTimeout(() => reactor.deliver(makeInboundMessage()), 30); + // The compactor's manifest record is written by the compact cycle, so it + // reports that the cycle this delivery must follow has happened. + await waitUntil(() => + recording.manifests + .flat() + .some((r) => r.strategy === "overflow-compactor"), + ); + reactor.deliver(makeInboundMessage()); await waitFor("reactor.done"); // The reactor ran inference twice: the first attempt failed with @@ -6053,7 +6149,10 @@ describe("createReactor — transform chain ordering and compact action", () => reactor.start(); reactor.deliver(makeInboundMessage()); - setTimeout(() => reactor.deliver(makeInboundMessage()), 80); + // The second message must land after the first cycle commits, since the + // assertions below key off commit order. + await waitUntil(() => recording.commits.length >= 1); + reactor.deliver(makeInboundMessage()); await waitFor("reactor.done"); expect(recording.commits.length).toBeGreaterThanOrEqual(1); @@ -6117,7 +6216,10 @@ describe("createReactor — message.run bracket emission", () => { reactor.start(); reactor.deliver(makeInboundMessage()); - setTimeout(() => reactor.deliver(makeInboundMessage()), 20); + // One bracket per message run, so the second message is delivered after + // the director reports the first one handled. + await waitUntil(() => count >= 1); + reactor.deliver(makeInboundMessage()); await waitFor("reactor.done"); const endedEvents = events.filter((e) => e.type === "message.run.ended"); @@ -6143,7 +6245,10 @@ describe("createReactor — message.run bracket emission", () => { reactor.start(); reactor.deliver(makeInboundMessage()); - setTimeout(() => reactor.deliver(makeInboundMessage()), 20); + // One bracket per message run, so the second message is delivered after + // the director reports the first one handled. + await waitUntil(() => count >= 1); + reactor.deliver(makeInboundMessage()); await waitFor("reactor.done"); const replyIdx = events.findIndex((e) => e.type === "connector.reply"); @@ -6217,7 +6322,10 @@ describe("createReactor — message.run bracket emission", () => { reactor.start(); reactor.deliver(makeInboundMessage()); - setTimeout(() => reactor.deliver(makeInboundMessage()), 30); + // The second message drives the terminal done; deliver it once the + // director reports the first message handled. + await waitUntil(() => count >= 1); + reactor.deliver(makeInboundMessage()); await waitFor("reactor.done"); const types = events.map((e) => e.type); @@ -6245,9 +6353,14 @@ describe("createReactor — message.run bracket emission", () => { }); reactor.start(); + // Three message runs, one per delivery. The director's counter reports + // each message handled, so each delivery follows the previous run rather + // than a delay chosen to separate them. + reactor.deliver(makeInboundMessage()); + await waitUntil(() => count >= 1); + reactor.deliver(makeInboundMessage()); + await waitUntil(() => count >= 2); reactor.deliver(makeInboundMessage()); - setTimeout(() => reactor.deliver(makeInboundMessage()), 20); - setTimeout(() => reactor.deliver(makeInboundMessage()), 40); await waitFor("reactor.done"); const started = events.filter((e) => e.type === "message.run.started"); diff --git a/vendor/intx-storage-isogit/PATCHES.md b/vendor/intx-storage-isogit/PATCHES.md index 8a4320bce..a81a0bf89 100644 --- a/vendor/intx-storage-isogit/PATCHES.md +++ b/vendor/intx-storage-isogit/PATCHES.md @@ -13,6 +13,12 @@ signposts that point into that diff; they do not define its extent. Re-carried as-is with its three tests; upstream `store.ts` has no `loadErrors`. No adaptation needed. +### 2026-10-14 re-sync (upstream `5453d0b0` — npm `0.4.0`, CL-10651) + +Re-carried as-is: upstream did not touch `src/store.ts` or +`src/store.test.ts` in this range (`loadErrors` still absent upstream), +so both files and the entry are byte-identical to the prior pin. + ## store-ts-load-errors `store.ts` — Implements `AuditStore.loadErrors` by reading diff --git a/vendor/intx-tools-posix/PATCHES.md b/vendor/intx-tools-posix/PATCHES.md new file mode 100644 index 000000000..1414a3399 --- /dev/null +++ b/vendor/intx-tools-posix/PATCHES.md @@ -0,0 +1,34 @@ +# Patch ledger — vendor/intx-tools-posix + +**The SHA-diff is authoritative; markers are navigation.** + +Recorded upstream commit lives in `docs/VENDORING.md`. A pristine checkout +at that SHA, diffed against `vendor/intx-tools-posix/src`, is the only +proof of which lines are ours — run `bin/vendor-patch-diff` to produce it. + +This tree was recorded as verbatim before the 2026-10-14 re-sync, but the +`bin/vendor-patch-diff` re-derivation for that sync found one divergence +that pre-dates it (present against the prior pin `1ad0104` too) and had +never been ledgered. It is ledgered here now rather than silently +dropped, and the `docs/VENDORING.md` table records the tree as patched +from this sync onward. + +## registry-ts-read-file-paging-description + +`registry.ts` — The `read_file` tool description documents the +offset/limit page-and-continue contract ("Large files are returned one +page at a time … follow the `Use offset=N to continue.` footer"), matching +the behavior the vendored `@intx/inference` `size-cap-ts-paged-read-file` +patch guarantees for footer-bearing pages. Upstream's description is +silent on paging because upstream has no paged-read-file guarantee to +describe. Companion to `size-cap-ts-paged-read-file` in +`vendor/intx-inference/PATCHES.md` — ships out or dies with it. + +`tools-posix.test.ts` — carries a small registry-contract test asserting +the description names `Use offset=` and paging, so the wording cannot +drift from the behavior it documents. + +**Disposition:** Companion to `size-cap-ts-paged-read-file` (vendor/ +intx-inference). **Removal path:** the same upstream PR that adds the +paged-read-file guarantee to `@intx/inference` would also own the +description wording; drop this entry with it. diff --git a/vendor/intx-tools-posix/src/registry.ts b/vendor/intx-tools-posix/src/registry.ts index d57aa8f82..f0190a967 100644 --- a/vendor/intx-tools-posix/src/registry.ts +++ b/vendor/intx-tools-posix/src/registry.ts @@ -42,6 +42,7 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [ { name: TOOL_NAMES.READ_FILE, description: + // Locally patched — see vendor/intx-tools-posix/PATCHES.md#registry-ts-read-file-paging-description "Read a file and return its content with line numbers. The path argument accepts either a filesystem path or a tool-output URI of the form tool-output:///{callId} that references a prior tool result. Large files are returned one page at a time: pass offset and limit to page through the file, then follow the `Use offset=N to continue.` footer to read the next page through to the end.", inputSchema: { type: "object", diff --git a/vendor/intx-tools-posix/src/tools-posix.test.ts b/vendor/intx-tools-posix/src/tools-posix.test.ts index f384ab535..78c7694a3 100644 --- a/vendor/intx-tools-posix/src/tools-posix.test.ts +++ b/vendor/intx-tools-posix/src/tools-posix.test.ts @@ -13,6 +13,7 @@ import { realpathSync } from "node:fs"; import { createBlobReader, type BlobReader } from "@intx/types/runtime"; import { createPosixTools, composeMiddleware } from "./index"; import type { PosixTools, ToolHandler, ToolPlugin } from "./index"; +// Locally patched — see vendor/intx-tools-posix/PATCHES.md#registry-ts-read-file-paging-description import { TOOL_DEFINITIONS } from "./registry"; import { matchGlob, shouldSkip } from "./glob-match"; diff --git a/vendor/intx-types/PATCHES.md b/vendor/intx-types/PATCHES.md index e0d50a8d9..16bb1a18b 100644 --- a/vendor/intx-types/PATCHES.md +++ b/vendor/intx-types/PATCHES.md @@ -16,6 +16,19 @@ usage emission. Upstream deleted `packages/types/src/sidecar-placement.ts`; no entry lived there. Unaffected by upstream's `credentialId` auth-model rewrite. +### 2026-10-14 re-sync (upstream `5453d0b0` — npm `0.4.0`, CL-10651) + +All three entries re-carried; upstream did not touch `runtime.ts` or +`mediated-credential.ts` in this range, so both entries re-applied +byte-identically with zero adaptation (`stopReason` chain verified at +`runtime.ts:1449/1715`, auth-recovery context at +`mediated-credential.ts:27`). Upstream did change `base64.ts` +(chunked encoding), `grants.ts` (new `ApprovalItem` type), +`sidecar.ts`/`sidecar.test.ts` (`approvedGrants` widened to +`ApprovalItem[]`) — all carried verbatim — and added the new +`src/testing/` subpackage (`waitUntil`), which the vendored +`package.json` now exposes via the upstream `./testing` subpath export. + ## runtime-ts-auth-recovery-context `mediated-credential.ts` and `runtime.ts` — credential material can atomically diff --git a/vendor/intx-types/package.json b/vendor/intx-types/package.json index b3b8779a9..bdfa481af 100644 --- a/vendor/intx-types/package.json +++ b/vendor/intx-types/package.json @@ -51,6 +51,10 @@ "./workflow-sources": { "types": "./src/workflow-sources.ts", "default": "./src/workflow-sources.ts" + }, + "./testing": { + "types": "./src/testing/index.ts", + "default": "./src/testing/index.ts" } }, "dependencies": { From 78141445dd98ec97f5332323071412b5fc7c27da Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Thu, 15 Oct 2026 12:43:21 -0700 Subject: [PATCH 3/3] docs(vendor): correct the 0.4.0 sync audit record The first sync commit still contains local lines, so it is not an unmodified upstream tree, and the second commit is not a patch-only replay onto a later pin. The auditable tree is the branch tip. @intx/tools-posix stays in the patched set, and packages published at npm 0.4.0 are no longer described as unpublished or ahead of npm. --- docs/VENDORING.md | 54 ++++++++++++++++++++++++++++++----------------- 1 file changed, 35 insertions(+), 19 deletions(-) diff --git a/docs/VENDORING.md b/docs/VENDORING.md index 8be9e580b..5407c4c90 100644 --- a/docs/VENDORING.md +++ b/docs/VENDORING.md @@ -50,12 +50,12 @@ package; the date is the deadline even if it is not. | `vendor/intx-inference/` | `faremeter/interchange` | `5453d0b01da9d749aa446c4ba34fcf2aeb4549c0` | Yes — `PATCHES.md` | Local fixes not yet upstream | runtime | 2027-03-14 or when patches land upstream and publish | | `vendor/intx-types/` | `faremeter/interchange` | `5453d0b01da9d749aa446c4ba34fcf2aeb4549c0` | Yes — `PATCHES.md` | Cross-package coupling with `@intx/inference` | runtime | 2027-03-14 or when the coupled trio publishes past `0.3.0` | | `vendor/intx-storage-isogit/` | `faremeter/interchange` | `5453d0b01da9d749aa446c4ba34fcf2aeb4549c0` | Yes — `PATCHES.md` | Cross-package coupling with `@intx/inference` | runtime | 2027-03-14 or when the coupled trio publishes past `0.3.0` | -| `vendor/intx-agent/` | `faremeter/interchange` | `5453d0b01da9d749aa446c4ba34fcf2aeb4549c0` | Yes — `PATCHES.md` | Vendored at Interchange head ahead of npm | runtime | 2027-03-14 or when `@intx/agent@>=0.4.0` publishes | -| `vendor/intx-authz/` | `faremeter/interchange` | `5453d0b01da9d749aa446c4ba34fcf2aeb4549c0` | No | Vendored at Interchange head ahead of npm | runtime | 2027-03-14 or when `@intx/authz@>=0.4.0` publishes | -| `vendor/intx-log/` | `faremeter/interchange` | `5453d0b01da9d749aa446c4ba34fcf2aeb4549c0` | No | Vendored at Interchange head ahead of npm | runtime | 2027-03-14 or when `@intx/log@>=0.4.0` publishes | -| `vendor/intx-tools-posix/` | `faremeter/interchange` | `5453d0b01da9d749aa446c4ba34fcf2aeb4549c0` | Yes — `PATCHES.md` | Vendored at Interchange head ahead of npm; the read_file paging description patch rides the coupled size-cap patch (see `vendor/intx-tools-posix/PATCHES.md`) | runtime | 2027-03-14 or when `@intx/tools-posix@>=0.4.0` publishes | -| `vendor/intx-mailbox/` | `faremeter/interchange` | `5453d0b01da9d749aa446c4ba34fcf2aeb4549c0` | No | Never published to npm (verified 2026-09-07: registry 404 for all versions) | step-1 | 2027-03-14 or when any `@intx/mailbox` version publishes to npm | -| `vendor/intx-harness/` | `faremeter/interchange` | `5453d0b01da9d749aa446c4ba34fcf2aeb4549c0` | No | `driveConnectorReplies`/`AgentEventStream` (`src/reply-drain.ts`) is past npm `0.3.0` (verified 2026-09-07: absent from the published tarball) | step-1 | 2027-03-14 or when a published `@intx/harness` exports `driveConnectorReplies` | +| `vendor/intx-agent/` | `faremeter/interchange` | `5453d0b01da9d749aa446c4ba34fcf2aeb4549c0` | Yes — `PATCHES.md` | Local patches (`vendor/intx-agent/PATCHES.md`); published `@intx/agent@0.4.0` does not include them | runtime | 2027-03-14 or when those patches land upstream and publish | +| `vendor/intx-authz/` | `faremeter/interchange` | `5453d0b01da9d749aa446c4ba34fcf2aeb4549c0` | No | Published at npm `0.4.0` (verbatim). Kept so the vendored set stays one workspace pin with the patched packages | runtime | 2027-03-14 or when patched siblings no longer need the shared vendored pin | +| `vendor/intx-log/` | `faremeter/interchange` | `5453d0b01da9d749aa446c4ba34fcf2aeb4549c0` | No | Published at npm `0.4.0` (verbatim). Kept so the vendored set stays one workspace pin with the patched packages | runtime | 2027-03-14 or when patched siblings no longer need the shared vendored pin | +| `vendor/intx-tools-posix/` | `faremeter/interchange` | `5453d0b01da9d749aa446c4ba34fcf2aeb4549c0` | Yes — `PATCHES.md` | Local `registry.ts` paging patch (`vendor/intx-tools-posix/PATCHES.md`); `@intx/tools-posix@0.4.0` does not include it | runtime | 2027-03-14 or when that patch lands upstream and publishes | +| `vendor/intx-mailbox/` | `faremeter/interchange` | `5453d0b01da9d749aa446c4ba34fcf2aeb4549c0` | No | Published at npm `0.4.0` (verbatim). Kept so the vendored set stays one workspace pin with the patched packages | step-1 | 2027-03-14 or when patched siblings no longer need the shared vendored pin | +| `vendor/intx-harness/` | `faremeter/interchange` | `5453d0b01da9d749aa446c4ba34fcf2aeb4549c0` | No | Published `@intx/harness@0.4.0` exports `driveConnectorReplies` (verbatim). Kept on the shared vendored pin | step-1 | 2027-03-14 or when patched siblings no longer need the shared vendored pin | | `vendor/intx-mime/` | `faremeter/interchange` | `5453d0b01da9d749aa446c4ba34fcf2aeb4549c0` | No | `buildMessageHeaders` is past npm `0.3.0` (verified 2026-09-07: absent from the published tarball); `@intx/mailbox` re-exports it | step-1 | 2027-03-14 or when a published `@intx/mime` exports `buildMessageHeaders` | | `vendor/intx-workflow-host/adapters/` | `faremeter/interchange` | `5453d0b01da9d749aa446c4ba34fcf2aeb4549c0` | No | App-internal: `substrate-mailbox-store.ts` has never been published in any `@intx/workflow-host` release (verified 2026-09-07: absent from the `0.3.0` tarball) | step-1 | 2027-03-14 or when a published `@intx/workflow-host` exports `createSubstrateMailboxStore` | | `vendor/intx-workflow-host/workflow-definition-loader.ts` | `faremeter/interchange` | `5453d0b01da9d749aa446c4ba34fcf2aeb4549c0` | No | Director loading from a workflow closure (`loadWorkflowDirectorRegistryFromClosure`); never published in any `@intx/workflow-host` release (verified 2026-09-14: absent from the `0.3.0` tarball) | step-1 | 2027-03-14 or when a published `@intx/workflow-host` exports `loadWorkflowDirectorRegistryFromClosure` | @@ -158,7 +158,7 @@ small: package version bumps across the board, plus three source files in `@intx/types` (`base64.ts` chunked encoding, `grants.ts` gaining `ApprovalItem`, `sidecar.ts` widening `approvedGrants` to `ApprovalItem[]`), a new `@intx/agent` testing helper -(`testing/reactor-waiters.ts`, `waitForReactorDone` moved out of three +(`testing/reactor-waiters.ts`, `waitForReactorDone` moved out of four agent test files), and a new `@intx/log` probe (`default-sink-probe.ts`). No vendored production source besides those three `@intx/types` files changed upstream in this range, so the four patched packages re-carried @@ -170,6 +170,13 @@ were overwritten from the new pin; the two agent test files and the inference `reactor.test.ts` that had both local patches and upstream changes were three-way merged (`git merge-file`), and every other patched file kept its prior state because upstream did not touch it. +Commit `865f63c6` is not a pristine byte-identical snapshot of +`5453d0b0`: local lines remain in that commit, including +`ephemeralTurns` and `Locally patched` in +`vendor/intx-inference/src/reactor.ts`. No exhaustive `cmp` showed +otherwise. The auditable artifact is the branch tip — upstream +`5453d0b0` plus the carried local patches. Replaying the following +commit onto a later pristine pin is not a patch-only replay. Ledger adjudication (ticket CL-10651, verified against upstream at this pin — all line-grep evidence below is against pristine `5453d0b0`): @@ -402,13 +409,17 @@ does not resolve to a ledger heading, or if a ledger heading has no marker. `adapters/substrate-mailbox-store.test.ts`). Run `bun install`, `bun run typecheck`, `bun run build`, `bun run test`. 3. For a **patched** package (`@intx/inference`, `@intx/types`, - `@intx/storage-isogit`, `@intx/agent`): before overwriting - anything, run `bin/vendor-patch-diff` (optionally + `@intx/storage-isogit`, `@intx/agent`, and `@intx/tools-posix`): + before overwriting anything, run `bin/vendor-patch-diff` (optionally `--upstream /path/to/interchange`) to re-derive the exact local divergences against the recorded SHA — do not trust `PATCHES.md`'s - prose alone. Then overwrite `src/` with the new upstream commit's - source, and re-apply each patch from the ledger by hand against the - new file shapes. For each patch, confirm from the new upstream source + prose alone. `@intx/tools-posix` is patched + (`vendor/intx-tools-posix/PATCHES.md`); `registry.ts` carries + `registry-ts-read-file-paging-description`. Copying that package as + verbatim drops the paging description on the next resync. Then + overwrite `src/` with the new upstream commit's source, and re-apply + each patch from the ledger by hand against the new file shapes. For + each patch, confirm from the new upstream source whether it: (a) still applies as-is, (b) needs adapting to a changed surrounding shape, or (c) has been subsumed by an equivalent upstream fix and can be dropped — verify (c) by reading the new upstream code, @@ -424,13 +435,18 @@ does not resolve to a ledger heading, or if a ledger heading has no marker. vendoring — otherwise the trio drifts out of the single-commit coherence this document assumes. 5. Update this document's table with the new commit hash and retrieval date. -6. Land the sync as **two commits, in this order**: first the pristine - upstream copy with no local changes, then the re-applied patches. The - point is that the unmodified upstream state becomes a checkout rather - than a reconstruction — an auditor diffs one commit against the upstream - clone and is done, instead of subtracting a prose ledger from a merged - tree. It also makes the next upgrade cheaper, because the patch commit is - exactly the thing to replay. The 2026-08-08 sync landed as two +6. Land the sync as **two commits, in this order**, only when the first + commit really is unmodified upstream: a pristine copy with no local + lines, then the re-applied patches. An auditor can then diff that + first commit against the upstream clone. The 2026-10-14 re-sync did + not meet that bar. Commit `865f63c6` is not a pristine byte-identical + snapshot of `5453d0b0` (local lines remain, including `ephemeralTurns` + and `Locally patched` in `vendor/intx-inference/src/reactor.ts`), and + there was no exhaustive `cmp` that proved otherwise. The auditable + artifact is the branch tip: upstream `5453d0b0` plus the carried + local patches. Replaying the second commit onto a later pristine pin + is not patch-only — that commit is not a clean patch set against an + unmodified `5453d0b0` tree. The 2026-08-08 sync landed as two commits split by package rather than by pristine-then-patched, so neither isolates an unmodified upstream tree; `PATCHES.md` is what makes that state reconstructible, which is why that ledger is load-bearing rather than