From c111a25004a13da6b3758380512e06a15e1d8607 Mon Sep 17 00:00:00 2001 From: seal Date: Fri, 3 Jul 2026 12:53:11 -0400 Subject: [PATCH 01/15] =?UTF-8?q?feat(oh-my-pi):=20Cotal=20connector=20for?= =?UTF-8?q?=20oh-my-pi=20=E2=80=94=20headless=20peer=20+=20interactive=20e?= =?UTF-8?q?xtension?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `@cotal-ai/oh-my-pi`, the Cotal connector for [oh-my-pi](https://github.com/can1357/oh-my-pi). It ships two entry points onto one mesh runtime (`MeshAgent` + the shared `cotal_*` tools from `@cotal-ai/connector-core`), mirroring the existing `@cotal-ai/pi` and `@cotal-ai/connector-opencode` adapters so the tool surface can't drift. ### Headless native-embed peer (`runOmpPeer` / `connector`) Embeds a Cotal endpoint inside an oh-my-pi process and answers mesh traffic through the agent's own loop, driven by the shared `InboxTurn` embed loop — `steer()` folds a same-scope message into an in-flight turn. This is the path a Cotal manager uses to spawn and supervise an oh-my-pi worker (`connector.buildLaunch`). ### Interactive session extension (`src/extension.ts`) A `pi --extension` that joins a human- or Compass-launched oh-my-pi session to the mesh — the interactive sibling of the opencode plugin. It holds a `MeshAgent`, registers the `cotal_*` tools via `pi.registerTool`, maps the session event stream to presence, and delivers inbound mesh traffic with `pi.sendMessage(..., { deliverAs })` (waking an idle session, steering a live one, never interrupting a running turn; acks on turn end so a crash redelivers). Identity comes from `COTAL_*` env — no identity, no join, so a plain `omp` never joins as a stray peer. `pnpm build` bundles it to `dist/extension.bundle.js` (esbuild). ### Fork divergences handled here oh-my-pi is a fork of Pi, so this targets `@oh-my-pi/pi-coding-agent`: retries surface as session `auto_retry_*` events (not an `agent_end.willRetry` flag), and imports use the package subpath entrypoints while the published root type barrel is fixed upstream. Spec-impact: none. Refs #181 Co-authored-by: Matt Wilkinson --- examples/04-oh-my-pi/package.json | 20 ++ examples/04-oh-my-pi/src/manager.ts | 28 ++ examples/04-oh-my-pi/tsconfig.json | 8 + extensions/connector-oh-my-pi/README.md | 37 ++ .../interactive-loop.smoke.ts | 328 ++++++++++++++++++ .../oh-my-pi-extension.smoke.ts | 92 +++++ .../connector-oh-my-pi/oh-my-pi-peer.smoke.ts | 282 +++++++++++++++ extensions/connector-oh-my-pi/package.json | 48 +++ .../connector-oh-my-pi/src/connector.ts | 56 +++ .../connector-oh-my-pi/src/extension.ts | 138 ++++++++ extensions/connector-oh-my-pi/src/index.ts | 4 + .../src/interactive-loop.ts | 138 ++++++++ extensions/connector-oh-my-pi/src/loop.ts | 192 ++++++++++ extensions/connector-oh-my-pi/src/main.ts | 6 + extensions/connector-oh-my-pi/src/peer.ts | 96 +++++ extensions/connector-oh-my-pi/tsconfig.json | 8 + package.json | 4 + pnpm-workspace.yaml | 3 + 18 files changed, 1488 insertions(+) create mode 100644 examples/04-oh-my-pi/package.json create mode 100644 examples/04-oh-my-pi/src/manager.ts create mode 100644 examples/04-oh-my-pi/tsconfig.json create mode 100644 extensions/connector-oh-my-pi/README.md create mode 100644 extensions/connector-oh-my-pi/interactive-loop.smoke.ts create mode 100644 extensions/connector-oh-my-pi/oh-my-pi-extension.smoke.ts create mode 100644 extensions/connector-oh-my-pi/oh-my-pi-peer.smoke.ts create mode 100644 extensions/connector-oh-my-pi/package.json create mode 100644 extensions/connector-oh-my-pi/src/connector.ts create mode 100644 extensions/connector-oh-my-pi/src/extension.ts create mode 100644 extensions/connector-oh-my-pi/src/index.ts create mode 100644 extensions/connector-oh-my-pi/src/interactive-loop.ts create mode 100644 extensions/connector-oh-my-pi/src/loop.ts create mode 100644 extensions/connector-oh-my-pi/src/main.ts create mode 100644 extensions/connector-oh-my-pi/src/peer.ts create mode 100644 extensions/connector-oh-my-pi/tsconfig.json diff --git a/examples/04-oh-my-pi/package.json b/examples/04-oh-my-pi/package.json new file mode 100644 index 00000000..3bb61c7e --- /dev/null +++ b/examples/04-oh-my-pi/package.json @@ -0,0 +1,20 @@ +{ + "name": "@cotal-ai/example-04-oh-my-pi", + "version": "0.0.0", + "private": true, + "license": "Apache-2.0", + "type": "module", + "scripts": { + "manager": "tsx src/manager.ts", + "typecheck": "tsc -p tsconfig.json --noEmit", + "build": "tsc -p tsconfig.json" + }, + "dependencies": { + "@cotal-ai/core": "workspace:*", + "@cotal-ai/manager": "workspace:*", + "@cotal-ai/oh-my-pi": "workspace:*" + }, + "devDependencies": { + "tsx": "^4.22.4" + } +} diff --git a/examples/04-oh-my-pi/src/manager.ts b/examples/04-oh-my-pi/src/manager.ts new file mode 100644 index 00000000..7aa724d7 --- /dev/null +++ b/examples/04-oh-my-pi/src/manager.ts @@ -0,0 +1,28 @@ +/** + * Composition root for example 04 (oh-my-pi coding agent). Runs a manager that + * spawns oh-my-pi peers into the space. Each spawn is a real oh-my-pi agent + * session (extensions/connector-oh-my-pi) that embeds a Cotal endpoint and + * answers DMs, anycasts, and @-mentions on channels — waking an idle session + * with prompt() and folding same-scope traffic into a live turn with steer(). + * Importing the connector self-registers it as "oh-my-pi". + */ +import { DEFAULT_SERVER, isReachable } from "@cotal-ai/core"; +import { Manager } from "@cotal-ai/manager"; +import "@cotal-ai/oh-my-pi"; // self-registers "oh-my-pi" + +const space = process.env.COTAL_SPACE?.trim() || "demo"; +const server = process.env.COTAL_SERVERS?.trim() || DEFAULT_SERVER; + +if (!(await isReachable(server))) { + console.error(`Can't reach NATS at ${server}. Run: pnpm cotal up`); + process.exit(1); +} + +const mgr = new Manager({ space, servers: server }); +await mgr.start(); +console.log(`example-04-oh-my-pi manager up in space "${space}" — connector: oh-my-pi`); +console.log(`console: ${mgr.consoleUrl}`); + +process.on("SIGINT", () => void mgr.stop().then(() => process.exit(0))); +process.on("SIGTERM", () => void mgr.stop().then(() => process.exit(0))); +await new Promise(() => {}); diff --git a/examples/04-oh-my-pi/tsconfig.json b/examples/04-oh-my-pi/tsconfig.json new file mode 100644 index 00000000..051d08e2 --- /dev/null +++ b/examples/04-oh-my-pi/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist" + }, + "include": ["src"] +} diff --git a/extensions/connector-oh-my-pi/README.md b/extensions/connector-oh-my-pi/README.md new file mode 100644 index 00000000..267f90e1 --- /dev/null +++ b/extensions/connector-oh-my-pi/README.md @@ -0,0 +1,37 @@ +# @cotal-ai/oh-my-pi + +The Cotal connector for [oh-my-pi](https://github.com/can1357/oh-my-pi). It ships two entry +points onto one mesh runtime (`MeshAgent` + the shared `cotal_*` tools from +[`@cotal-ai/connector-core`](../connector-core)): + +## 1. Headless native-embed peer (`runOmpPeer` / `connector`) + +Embeds a Cotal endpoint inside an oh-my-pi process and answers mesh traffic through the agent's +own loop, driven by the shared `InboxTurn` embed loop — like the [`@cotal-ai/pi`](../pi) adapter, +it drives a *live* turn (`steer()` folds a same-scope message into an in-flight one). This is the +path a Cotal manager uses to spawn and supervise an oh-my-pi worker (`connector.buildLaunch`). + +## 2. Interactive session extension (`src/extension.ts`) + +A `pi --extension` (default export = the extension factory) that joins a **human- or +Compass-launched** oh-my-pi session to the mesh — the interactive sibling of the +[opencode](../connector-opencode) plugin. It holds a `MeshAgent`, registers the `cotal_*` tools +via `pi.registerTool`, maps the session's event stream to presence, and delivers inbound mesh +traffic into the session with `pi.sendMessage(..., { deliverAs })` (waking an idle session, +steering a live one, never interrupting a running turn; acks on turn end so a crash redelivers). +Identity comes from `COTAL_*` env — no identity → inert, so a plain `omp` never joins as a stray +peer. `pnpm build` bundles it to `dist/extension.bundle.js` (esbuild, host `@oh-my-pi/*` +external), the artifact a session loads via `--extension`. + +## Fork divergences handled here + +oh-my-pi is a fork of Pi, so this mirrors [`@cotal-ai/pi`](../pi) but targets +`@oh-my-pi/pi-coding-agent`: retries surface as session `auto_retry_*` events (not an +`agent_end.willRetry` flag), and imports use the package's subpath entrypoints while the published +root type barrel is fixed upstream (see the header comment in `src/peer.ts` / `src/extension.ts`). + +**Tier:** `extensions/`. Peer-depends [`@cotal-ai/core`](../../packages/core); self-registers on +import. + +See [docs/agent-frameworks.md](../../docs/agent-frameworks.md) for the native-embed pattern, +and the [root AGENTS.md](../../AGENTS.md) for the tier rules. diff --git a/extensions/connector-oh-my-pi/interactive-loop.smoke.ts b/extensions/connector-oh-my-pi/interactive-loop.smoke.ts new file mode 100644 index 00000000..985990e3 --- /dev/null +++ b/extensions/connector-oh-my-pi/interactive-loop.smoke.ts @@ -0,0 +1,328 @@ +/** + * Behavioral smoke for the cotal-mesh delivery loop (`runPeerLoop` in loop.ts). Repo style: plain + * assert + console.log, run via `bun loop.smoke.ts`, non-zero exit on failure. No test framework. + * + * Drives the loop with a structural FakeMesh (captures the on(...) handlers so the test can emit + * incoming/mention-wake/wake, array-backed peek/drain inbox, controllable attention/channelMode/ + * pendingWake, recording setStatus/stop) and a fake host recording sendMessage. The 8 documented + * invariants of loop.ts are the spec; each is asserted below. + */ +import { formatInjection, ORIENTATION_BOOTSTRAP, type InboxItem } from "@cotal-ai/connector-core"; +import type { AttentionMode, ChannelMode, PresenceStatus } from "@cotal-ai/core"; +import { runPeerLoop, INCOMING, NUDGE, type PeerMesh, type PeerHost } from "./src/interactive-loop.ts"; + +function assert(cond: unknown, msg: string): asserts cond { + if (!cond) { + console.error(`FAIL: ${msg}`); + process.exit(1); + } +} + +/** Minimal InboxItem builder — required fields per @cotal-ai/connector-core agent.d.ts + * (id, ts, fromId, fromName, kind, mentionsMe, historical, text); channel added for kind:"channel". */ +function item(partial: Partial & Pick): InboxItem { + return { + ts: 0, + fromId: "u1", + fromName: "Alice", + kind: "dm", + mentionsMe: false, + historical: false, + text: `msg-${partial.id}`, + ...partial, + }; +} + +type MeshEvent = "incoming" | "mention-wake" | "wake"; + +/** A structural PeerMesh whose handlers, inbox, and modes the test controls directly. */ +class FakeMesh implements PeerMesh { + connected = true; + attention: AttentionMode = "open"; + inbox: InboxItem[] = []; + private _channelMode: ChannelMode | undefined = undefined; + private _pendingWake = 0; + readonly statusCalls: { status: PresenceStatus; activity?: string }[] = []; + stopCalls = 0; + private readonly handlers: Partial void>> = {}; + + on(event: "incoming" | "mention-wake", handler: (item: InboxItem) => void): void; + on(event: "wake", handler: () => void): void; + on(event: MeshEvent, handler: (item: InboxItem) => void): void { + this.handlers[event] = handler; + } + + /** Fire a captured handler as the mesh would. */ + emit(event: MeshEvent, it?: InboxItem): void { + const h = this.handlers[event]; + assert(h, `loop registered a "${event}" handler`); + h(it as InboxItem); + } + + peekInbox(): InboxItem[] { + return this.inbox; + } + drainInbox(limit?: number): InboxItem[] { + return this.inbox.splice(0, limit ?? this.inbox.length); + } + pendingWake(): number { + return this._pendingWake; + } + setPendingWake(n: number): void { + this._pendingWake = n; + } + setChannelMode(mode: ChannelMode | undefined): void { + this._channelMode = mode; + } + channelMode(_channel?: string): ChannelMode | undefined { + return this._channelMode; + } + async setStatus(status: PresenceStatus, activity?: string): Promise { + this.statusCalls.push({ status, activity }); + } + async stop(): Promise { + this.stopCalls++; + } +} + +interface SentCall { + message: { customType: string; content: string; display: boolean; details: unknown; attribution: "user" | "agent" }; + options: { deliverAs: "steer"; triggerTurn: true }; +} + +/** A fake host that records every sendMessage(message, options). */ +class FakeHost implements PeerHost { + readonly sent: SentCall[] = []; + sendMessage(message: SentCall["message"], options: SentCall["options"]): void { + this.sent.push({ message, options }); + } + get last(): SentCall { + return this.sent[this.sent.length - 1]; + } +} + +/** Assert a sendMessage call carries the fixed steer/turn envelope the loop always uses. */ +function assertEnvelope(call: SentCall, customType: string, ctx: string): void { + assert(call.message.customType === customType, `${ctx}: customType === ${customType}`); + assert(call.message.display === true, `${ctx}: display true`); + assert(call.message.attribution === "user", `${ctx}: attribution "user"`); + assert(JSON.stringify(call.message.details) === "{}", `${ctx}: details {}`); + assert(call.options.deliverAs === "steer", `${ctx}: deliverAs "steer"`); + assert(call.options.triggerTurn === true, `${ctx}: triggerTurn true`); +} + +// ---- 1. Directed drives when idle, with the correct message/options shape ---------------------- +{ + const mesh = new FakeMesh(); + const host = new FakeHost(); + runPeerLoop({ mesh, host }); + + // A DM: directed regardless of attention. + const dm = item({ id: "d1", kind: "dm", text: "hey there" }); + mesh.inbox = [dm]; + mesh.emit("incoming", dm); + assert(host.sent.length === 1, "1) DM drives one sendMessage when idle"); + assertEnvelope(host.last, INCOMING, "1/dm"); + const inj = formatInjection([dm]); + assert(inj && host.last.message.content.includes(inj), "1) content carries the formatted injection"); + + // A channel message with mentionsMe is also directed even when the channel is quiet + attention dnd. + const mesh2 = new FakeMesh(); + const host2 = new FakeHost(); + mesh2.attention = "dnd"; + mesh2.setChannelMode("quiet"); + runPeerLoop({ mesh: mesh2, host: host2 }); + const mention = item({ id: "c1", kind: "channel", channel: "general", mentionsMe: true, text: "@me look" }); + mesh2.inbox = [mention]; + mesh2.emit("incoming", mention); + assert(host2.sent.length === 1, "1) channel @mention drives even in dnd + quiet"); + assertEnvelope(host2.last, INCOMING, "1/mention"); + console.log("1) directed drives when idle OK ✅"); +} + +// ---- 2. Ambient gating by attention + quiet channelMode ---------------------------------------- +{ + // open + non-quiet → drives. + { + const mesh = new FakeMesh(); + const host = new FakeHost(); + mesh.attention = "open"; + mesh.setChannelMode(undefined); // non-quiet channel + runPeerLoop({ mesh, host }); + const it = item({ id: "a1", kind: "channel", channel: "general", mentionsMe: false, text: "ambient chatter" }); + mesh.inbox = [it]; + mesh.emit("incoming", it); + assert(host.sent.length === 1, "2) ambient drives when open + non-quiet"); + } + // dnd → does NOT drive (buffered). + for (const att of ["dnd", "focus"] as AttentionMode[]) { + const mesh = new FakeMesh(); + const host = new FakeHost(); + mesh.attention = att; + mesh.setChannelMode(undefined); // non-quiet channel + runPeerLoop({ mesh, host }); + const it = item({ id: "a2", kind: "channel", channel: "general", mentionsMe: false, text: "ambient" }); + mesh.inbox = [it]; + mesh.emit("incoming", it); + assert(host.sent.length === 0, `2) ambient does NOT drive when attention=${att}`); + } + // open but quiet channel → does NOT drive. + { + const mesh = new FakeMesh(); + const host = new FakeHost(); + mesh.attention = "open"; + mesh.setChannelMode("quiet"); + runPeerLoop({ mesh, host }); + const it = item({ id: "a3", kind: "channel", channel: "quietc", mentionsMe: false, text: "ambient" }); + mesh.inbox = [it]; + mesh.emit("incoming", it); + assert(host.sent.length === 0, "2) ambient does NOT drive on a quiet channel even when open"); + } + console.log("2) ambient gating by attention + quiet channelMode OK ✅"); +} + +// ---- 3. No-interrupt while busy ---------------------------------------------------------------- +{ + const mesh = new FakeMesh(); + const host = new FakeHost(); + const loop = runPeerLoop({ mesh, host }); + loop.onAgentStart(); // turn in progress + const dm = item({ id: "b1", kind: "dm", text: "urgent" }); + mesh.inbox = [dm]; + mesh.emit("incoming", dm); // directed, but busy + assert(host.sent.length === 0, "3) directed item during a turn does NOT interrupt (buffers)"); + const amb = item({ id: "b2", kind: "channel", channel: "general", mentionsMe: false, text: "chatter" }); + mesh.inbox = [dm, amb]; + mesh.emit("incoming", amb); + assert(host.sent.length === 0, "3) ambient item during a turn does NOT interrupt"); + console.log("3) no-interrupt while busy OK ✅"); +} + +// ---- 4. Ack-on-surface by id, incl. eviction/reorder cases ------------------------------------- +{ + // 4a. Partial: front stays matched for a leading prefix, then diverges → only the prefix drains. + { + const mesh = new FakeMesh(); + const host = new FakeHost(); + const loop = runPeerLoop({ mesh, host }); + const a = item({ id: "s1" }), b = item({ id: "s2" }), c = item({ id: "s3" }); + mesh.inbox = [a, b, c]; + mesh.emit("wake"); // surfaces [s1,s2,s3], busy=true + assert(host.sent.length === 1, "4a) wake surfaced the batch"); + // Before turn end, the front's 3rd slot is replaced (c evicted, x arrived at that position). + const x = item({ id: "sX" }); + mesh.inbox = [a, b, x]; + loop.onAgentEnd(); + assert(mesh.inbox.map((i) => i.id).join(",") === "sX", "4a) only leading matched prefix [s1,s2] drained; sX remains"); + } + // 4b. Non-matching front: front[0] no longer matches → NOTHING drained (all redelivered). + { + const mesh = new FakeMesh(); + const host = new FakeHost(); + const loop = runPeerLoop({ mesh, host }); + const a = item({ id: "t1" }), b = item({ id: "t2" }); + mesh.inbox = [a, b]; + mesh.emit("wake"); // surfaces [t1,t2] + // Front-eviction shifts our surfaced prefix out entirely. + mesh.inbox = [b]; + loop.onAgentEnd(); + assert(mesh.inbox.map((i) => i.id).join(",") === "t2", "4b) non-matching front drains nothing; t2 redelivered"); + } + // 4c. Ack only happens on onAgentEnd, not at surface time. + { + const mesh = new FakeMesh(); + const host = new FakeHost(); + const loop = runPeerLoop({ mesh, host }); + const a = item({ id: "u1" }); + mesh.inbox = [a]; + mesh.emit("wake"); + assert(mesh.inbox.length > 0, "4c) surfacing does NOT drain the inbox"); + loop.onAgentEnd(); + assert(mesh.inbox.length === 0, "4c) onAgentEnd drains the still-matching surfaced batch"); + } + console.log("4) ack-on-surface by id (incl. eviction/reorder) OK ✅"); +} + +// ---- 5. Flush next after turn when pendingWake > 0 --------------------------------------------- +{ + const mesh = new FakeMesh(); + const host = new FakeHost(); + const loop = runPeerLoop({ mesh, host }); + loop.onAgentStart(); // busy + // A batch arrives mid-turn; buffered (no send), and the mesh reports it as pending. + const buffered = item({ id: "f1", kind: "dm", text: "arrived during turn" }); + mesh.inbox = [buffered]; + mesh.emit("incoming", buffered); + assert(host.sent.length < 1, "5) buffered during turn — no send yet"); + mesh.setPendingWake(1); + loop.onAgentEnd(); + assert(host.sent.length === 1, "5) onAgentEnd flushes the buffered batch when pendingWake > 0"); + assertEnvelope(host.last, INCOMING, "5/flush"); + + // And when pendingWake === 0, onAgentEnd does NOT drive spuriously. + const mesh2 = new FakeMesh(); + const host2 = new FakeHost(); + const loop2 = runPeerLoop({ mesh: mesh2, host: host2 }); + loop2.onAgentStart(); + mesh2.inbox = [item({ id: "f2", kind: "dm" })]; + mesh2.setPendingWake(0); + loop2.onAgentEnd(); + assert(host2.sent.length === 0, "5) onAgentEnd does not flush when pendingWake === 0"); + console.log("5) flush-next after turn when pendingWake > 0 OK ✅"); +} + +// ---- 6. First turn primed once (orientation bootstrap) ----------------------------------------- +{ + const mesh = new FakeMesh(); + const host = new FakeHost(); + const loop = runPeerLoop({ mesh, host }); + + const first = item({ id: "p1", kind: "dm", text: "first" }); + mesh.inbox = [first]; + mesh.emit("incoming", first); + loop.onAgentEnd(); // ack + no pending → no reflow + assert(host.sent[0].message.content.startsWith(ORIENTATION_BOOTSTRAP), "6) first delivery is prefixed with the orientation bootstrap"); + + const second = item({ id: "p2", kind: "dm", text: "second" }); + mesh.inbox = [second]; + mesh.emit("incoming", second); + assert(!host.sent[1].message.content.startsWith(ORIENTATION_BOOTSTRAP), "6) later deliveries are NOT re-primed"); + console.log("6) first turn primed once OK ✅"); +} + +// ---- 7. mention-wake drives a NUDGE naming the sender + cotal_inbox ------------------------------ +{ + const mesh = new FakeMesh(); + const host = new FakeHost(); + const loop = runPeerLoop({ mesh, host }); + const mw = item({ id: "m1", kind: "channel", channel: "general", fromName: "Bob", mentionsMe: true, text: "@me" }); + mesh.emit("mention-wake", mw); + assert(host.sent.length === 1, "7) mention-wake drives one message when idle"); + assertEnvelope(host.last, NUDGE, "7/nudge"); + const content = host.last.message.content; + assert(content.includes("Bob"), "7) nudge names the sender"); + assert(content.includes("cotal_inbox"), "7) nudge says to use cotal_inbox"); + assert(content.includes("#general"), "7) nudge names the channel"); + + // mention-wake while busy does NOT drive. + const mesh2 = new FakeMesh(); + const host2 = new FakeHost(); + const loop2 = runPeerLoop({ mesh: mesh2, host: host2 }); + loop2.onAgentStart(); + mesh2.emit("mention-wake", mw); + assert(host2.sent.length === 0, "7) mention-wake during a turn does NOT interrupt"); + console.log("7) mention-wake NUDGE OK ✅"); +} + +// ---- 8. shutdown stops the mesh ---------------------------------------------------------------- +{ + const mesh = new FakeMesh(); + const host = new FakeHost(); + const loop = runPeerLoop({ mesh, host }); + await loop.shutdown(); + assert(mesh.stopCalls === 1, "8) shutdown calls mesh.stop() exactly once"); + console.log("8) shutdown stops mesh OK ✅"); +} + +console.log("\nCOTAL-MESH LOOP SMOKE OK ✅"); +process.exit(0); diff --git a/extensions/connector-oh-my-pi/oh-my-pi-extension.smoke.ts b/extensions/connector-oh-my-pi/oh-my-pi-extension.smoke.ts new file mode 100644 index 00000000..95776f95 --- /dev/null +++ b/extensions/connector-oh-my-pi/oh-my-pi-extension.smoke.ts @@ -0,0 +1,92 @@ +/** + * Smoke test for the cotal-mesh OMP extension. Repo style: plain assert + console.log, run via + * `bun cotal-mesh.smoke.ts`, non-zero exit on failure. No test framework. + * + * Drives the extension factory with a FAKE ExtensionAPI (records tool registrations, event + * handlers, and sendMessage calls) so the whole load path is exercised with no NATS connection. + * Asserts: inert without identity; with identity it registers the cotal_* tool surface, subscribes + * to the lifecycle events, and cotal_inbox is read-only. + */ +import cotalMesh from "./src/extension.ts"; +import * as zodV4 from "zod/v4"; + +function assert(cond: unknown, msg: string): asserts cond { + if (!cond) { + console.error(`FAIL: ${msg}`); + process.exit(1); + } +} + +interface RegisteredTool { + name: string; + label: string; + description: string; + parameters: unknown; + approval?: string; + execute: (...a: unknown[]) => Promise<{ content: { type: string; text: string }[]; details: unknown }>; +} + +/** A fake ExtensionAPI that records everything the factory does. */ +function fakePi() { + const tools = new Map(); + const events = new Map unknown>(); + const sent: { message: Record; options: Record }[] = []; + const z = zodV4.z; + const pi = { + zod: zodV4, + logger: console, + registerTool: (t: RegisteredTool) => tools.set(t.name, t), + on: (event: string, handler: (e: unknown) => unknown) => events.set(event, handler), + sendMessage: (message: Record, options: Record) => + sent.push({ message, options }), + registerCommand: () => {}, + setLabel: () => {}, + }; + return { pi, tools, events, sent, z }; +} + +// ---- 1. inert without identity ------------------------------------------------ +delete process.env.COTAL_NAME; +delete process.env.COTAL_LINK; +delete process.env.COTAL_AGENT_FILE; +{ + const { pi, tools, events } = fakePi(); + cotalMesh(pi as never); + assert(tools.size === 0, "no identity → registers no tools"); + assert(events.size === 0, "no identity → subscribes to no events"); + console.log("1) inert without identity OK ✅"); +} + +// ---- 2. with identity: tools + events registered ------------------------------ +process.env.COTAL_NAME = "smoke-peer"; +process.env.COTAL_SERVERS = "nats://127.0.0.1:4222"; // never actually connected in this smoke +{ + const { pi, tools, events } = fakePi(); + cotalMesh(pi as never); + + // The shared cotal_* surface is registered. + for (const name of ["cotal_orientation", "cotal_roster", "cotal_inbox", "cotal_send", "cotal_dm", "cotal_status"]) { + assert(tools.has(name), `registers ${name}`); + } + console.log(` registered ${tools.size} cotal_* tools`); + + // Lifecycle events are subscribed for presence + turn tracking. + for (const ev of ["agent_start", "agent_end", "tool_execution_start", "session_shutdown"]) { + assert(events.has(ev), `subscribes to ${ev}`); + } + console.log("2) identity → tools + events registered OK ✅"); + + // ---- 3. cotal_inbox is read-only (peek) ----------------------------------- + const inbox = tools.get("cotal_inbox")!; + assert(inbox.approval === "read", "cotal_inbox is approval:read"); + // Its schema takes no args (peek is forced), so execute must run without throwing on {}. + console.log("3) cotal_inbox read-only OK ✅"); + + // The factory started a MeshAgent with a background reconnect loop; fire session_shutdown to stop + // it so the smoke process can exit (no live mesh in this test). + const shutdown = events.get("session_shutdown"); + if (shutdown) await shutdown(undefined); +} + +console.log("\nCOTAL-MESH EXTENSION SMOKE OK ✅"); +process.exit(0); diff --git a/extensions/connector-oh-my-pi/oh-my-pi-peer.smoke.ts b/extensions/connector-oh-my-pi/oh-my-pi-peer.smoke.ts new file mode 100644 index 00000000..a021c60c --- /dev/null +++ b/extensions/connector-oh-my-pi/oh-my-pi-peer.smoke.ts @@ -0,0 +1,282 @@ +/** + * Smoke test for the @cotal-ai/oh-my-pi peer loop (no NATS/LLM needed): drives + * `runPeerLoop({ mesh, session })` against a fake MeshAgent and a scripted stub session, and + * asserts the reply-routing / scope-isolation / ack-on-surface invariants the native embed + * relies on — including the oh-my-pi fork divergence (an `agent_end` carries no `willRetry`, + * so it is always the turn's terminal event). + * + * pnpm smoke:oh-my-pi + */ +import { EventEmitter } from "node:events"; +import { runPeerLoop, type PeerMesh, type PeerSession } from "./src/loop.js"; +import type { InboxItem } from "@cotal-ai/connector-core"; +import type { AgentSessionEvent } from "@oh-my-pi/pi-coding-agent/session/agent-session"; + +function assert(cond: boolean, msg: string): void { + if (!cond) throw new Error(`FAIL: ${msg}`); +} + +// --- message factories ------------------------------------------------------------------- +function dm(id: string, fromId: string, text = id): InboxItem { + return { id, ts: 0, fromId, fromName: fromId, kind: "dm", mentionsMe: false, historical: false, text }; +} +function chan( + id: string, + fromId: string, + opts: { mentionsMe?: boolean; channel?: string; text?: string } = {}, +): InboxItem { + const { mentionsMe = true, channel = "general", text = id } = opts; + return { + id, + ts: 0, + fromId, + fromName: fromId, + kind: "channel", + channel, + mentionsMe, + historical: false, + text, + }; +} +const framed = (i: InboxItem): string => `from ${i.fromName} via ${i.kind}: ${i.text}`; + +// --- scripted session-event factories ---------------------------------------------------- +const START: AgentSessionEvent = { type: "agent_start" }; +const toolStart = (toolName: string): AgentSessionEvent => + ({ type: "tool_execution_start", toolCallId: "t", toolName, args: {} }) as AgentSessionEvent; +/** A NORMAL `agent_end` — the oh-my-pi fork has no `willRetry` field, so this is terminal. */ +const end = (reply?: string): AgentSessionEvent => + ({ + type: "agent_end", + messages: reply ? [{ role: "assistant", content: [{ type: "text", text: reply }] }] : [], + }) as AgentSessionEvent; + +// --- fakes ------------------------------------------------------------------------------- +interface Sent { + text: string; + channel?: string; +} +interface DirectMsg { + target: string; + text: string; +} + +/** Mirrors the MeshAgent slice the loop uses: an EventEmitter (`incoming`/`wake`) plus a + * stream-backed inbox (drainInbox acks by front position, ackInbox acks by id, absent id is a + * no-op) and record-only presence/delivery. */ +class FakeMesh extends EventEmitter implements PeerMesh { + readonly id = "me"; + items: InboxItem[] = []; + acked: InboxItem[] = []; + statuses: { status: string; activity?: string }[] = []; + sends: Sent[] = []; + dms: DirectMsg[] = []; + + peekInbox(): InboxItem[] { + return [...this.items]; + } + drainInbox(limit?: number): InboxItem[] { + const n = limit && limit > 0 ? Math.min(limit, this.items.length) : this.items.length; + const taken = this.items.splice(0, n); + this.acked.push(...taken); + return taken; + } + ackInbox(ids: string[]): InboxItem[] { + const wanted = new Set(ids); + const taken: InboxItem[] = []; + this.items = this.items.filter((p) => { + if (!wanted.has(p.id)) return true; + this.acked.push(p); + taken.push(p); + return false; + }); + return taken; + } + async setStatus(status: "idle" | "waiting" | "working", activity?: string): Promise { + this.statuses.push({ status, activity }); + } + async send(text: string, channel?: string): Promise { + this.sends.push({ text, channel }); + return {}; + } + async dm(target: string, text: string): Promise { + this.dms.push({ target, text }); + return {}; + } + + /** Deliver a message: buffer it, then wake the loop on the given mesh event. */ + arrive(item: InboxItem, event: "incoming" | "wake" = "incoming"): void { + this.items.push(item); + this.emit(event); + } +} + +/** A scripted oh-my-pi session: `prompt`/`steer` only RECORD (the test drives the + * `agent_start`/`tool_execution_*`/`agent_end` stream explicitly via {@link emit}), so a turn's + * lifecycle is fully controllable — a live turn can be held open while same-scope peers are + * folded in before it ends. */ +class StubSession implements PeerSession { + prompts: string[] = []; + steers: string[] = []; + aborted = 0; + disposed = 0; + private listeners: ((event: AgentSessionEvent) => void)[] = []; + + subscribe(listener: (event: AgentSessionEvent) => void): () => void { + this.listeners.push(listener); + return () => {}; + } + emit(event: AgentSessionEvent): void { + for (const l of this.listeners) l(event); + } + async prompt(text: string): Promise { + this.prompts.push(text); + return true; + } + async steer(text: string): Promise { + this.steers.push(text); + } + async abort(): Promise { + this.aborted++; + } + async dispose(): Promise { + this.disposed++; + } +} + +const ids = (xs: InboxItem[]): string => xs.map((x) => x.id).join(","); +const last = (xs: T[]): T => xs[xs.length - 1]; + +// 1) reply routing by scope: a DM is answered privately; a channel message on the channel; +// a DM is NEVER broadcast to a channel and vice-versa. +{ + // (a) DM → dm, never send + const mesh = new FakeMesh(); + const session = new StubSession(); + mesh.items = [dm("q1", "alice", "hi")]; + runPeerLoop({ mesh, session }); + assert(session.prompts[0] === framed(dm("q1", "alice", "hi")), "DM origin framed + prompted"); + session.emit(START); + session.emit(end("hello alice")); + assert(mesh.dms.length === 1 && mesh.dms[0].target === "alice", "DM answered privately to sender"); + assert(mesh.dms[0].text === "hello alice", "DM reply is this turn's text"); + assert(mesh.sends.length === 0, "a DM is never broadcast to a channel"); +} +{ + // (b) channel (mentions us) → send on that channel, never dm + const mesh = new FakeMesh(); + const session = new StubSession(); + mesh.items = [chan("c1", "bob", { channel: "eng", text: "hey me" })]; + runPeerLoop({ mesh, session }); + session.emit(START); + session.emit(end("hi eng")); + assert(mesh.sends.length === 1 && mesh.sends[0].channel === "eng", "channel msg answered on its channel"); + assert(mesh.sends[0].text === "hi eng", "channel reply is this turn's text"); + assert(mesh.dms.length === 0, "a channel reply is never sent as a private DM"); +} +console.log("1) reply routing by scope OK ✅"); + +// 2) actionable filter: own echoes (fromId === mesh.id) and ambient channel chatter (not +// mentionsMe) are dropped — acked, never prompted, never answered. +{ + const mesh = new FakeMesh(); + const session = new StubSession(); + mesh.items = [dm("echo", "me"), chan("amb", "bob", { mentionsMe: false })]; + runPeerLoop({ mesh, session }); + assert(session.prompts.length === 0, "no actionable message → nothing prompted"); + assert(ids(mesh.acked) === "echo,amb", "own echo + ambient chatter are ack-dropped"); + assert(mesh.dms.length === 0 && mesh.sends.length === 0, "neither is ever answered"); + assert(last(mesh.statuses).status === "idle", "empty of actionable → idle"); +} +console.log("2) actionable filter OK ✅"); + +// 3) same-scope fold: two same-scope messages arriving during a live turn fold in via steer; +// a different-scope message breaks contiguity and opens its own next turn. +{ + const mesh = new FakeMesh(); + const session = new StubSession(); + mesh.items = [dm("a1", "alice", "q1")]; + runPeerLoop({ mesh, session }); + session.emit(START); // turn is now live (streaming) + mesh.arrive(dm("a2", "alice", "q2")); // same scope → folded via steer + mesh.arrive(dm("b1", "bob", "qb")); // different scope → NOT folded, waits its turn + assert(session.steers.length === 1 && session.steers[0] === framed(dm("a2", "alice", "q2")), + "same-scope peer folded via steer; cross-scope not folded"); + assert(mesh.items.some((x) => x.id === "b1"), "cross-scope message stays on the stream"); + session.emit(end("ans")); // terminal → commit alice run, deliver, pump next scope + assert(ids(mesh.acked) === "a1,a2", "the surfaced same-scope run [a1,a2] was acked on end"); + assert(mesh.dms.length === 1 && mesh.dms[0].target === "alice", "one reply delivered to the shared scope"); + assert(session.prompts[1] === framed(dm("b1", "bob", "qb")), "cross-scope msg opens its own next turn"); +} +console.log("3) same-scope fold OK ✅"); + +// 4) ack-on-surface: a surfaced message is acked ONLY on agent_end; an in-flight turn on +// shutdown abandons (no ack → redelivers). +{ + // (a) acked only on agent_end + const mesh = new FakeMesh(); + const session = new StubSession(); + mesh.items = [dm("s1", "alice")]; + runPeerLoop({ mesh, session }); + assert(mesh.acked.length === 0, "surfaced-but-unacked before the turn completes"); + session.emit(START); + assert(mesh.acked.length === 0, "agent_start does not ack"); + session.emit(end("done")); + assert(ids(mesh.acked) === "s1", "agent_end acks the surfaced run"); +} +{ + // (b) in-flight shutdown abandons — no ack, redelivers + const mesh = new FakeMesh(); + const session = new StubSession(); + mesh.items = [dm("s1", "alice")]; + const loop = runPeerLoop({ mesh, session }); + session.emit(START); // turn in flight + await loop.shutdown(); + assert(mesh.acked.length === 0, "in-flight shutdown acks nothing → redeliver"); + assert(ids(mesh.items) === "s1", "the in-flight message stays on the stream"); + assert(session.aborted === 1, "shutdown aborts the live turn"); + assert(session.disposed === 1, "shutdown disposes the session"); +} +console.log("4) ack-on-surface OK ✅"); + +// 5) presence mapping: agent_start → working(thinking), tool_execution_start → +// working(running ), agent_end → idle (via mesh.setStatus). +{ + const mesh = new FakeMesh(); + const session = new StubSession(); + mesh.items = [dm("p1", "alice")]; + runPeerLoop({ mesh, session }); + session.emit(START); + assert(last(mesh.statuses).status === "working" && last(mesh.statuses).activity === "thinking", + "agent_start → working(thinking)"); + session.emit(toolStart("bash")); + assert(last(mesh.statuses).status === "working" && last(mesh.statuses).activity === "running bash", + "tool_execution_start → working(running )"); + session.emit(end("ok")); + assert(last(mesh.statuses).status === "idle", "agent_end (inbox now empty) → idle"); +} +console.log("5) presence mapping OK ✅"); + +// 6) oh-my-pi fork specific: an `agent_end` has NO `willRetry`; assert a normal agent_end is +// treated as terminal — it commits, delivers, and pumps the next scope's turn. +{ + const mesh = new FakeMesh(); + const session = new StubSession(); + mesh.items = [dm("a1", "alice", "qa"), dm("b1", "bob", "qb")]; // two distinct scopes, FIFO + runPeerLoop({ mesh, session }); + assert(session.prompts[0] === framed(dm("a1", "alice", "qa")), "first turn starts on the front DM"); + session.emit(START); // fold pass: bob is a different scope → not folded + assert(session.steers.length === 0, "cross-scope bob is not folded into alice's turn"); + const terminal = end("ra"); + assert(!("willRetry" in terminal), "the oh-my-pi agent_end carries no willRetry flag"); + session.emit(terminal); // terminal: commit + deliver + pump next + assert(ids(mesh.acked) === "a1", "terminal agent_end committed alice's run"); + assert(mesh.dms.length === 1 && mesh.dms[0].target === "alice", "terminal agent_end delivered alice's reply"); + assert(session.prompts[1] === framed(dm("b1", "bob", "qb")), "terminal agent_end pumped the next scope's turn"); + session.emit(START); + session.emit(end("rb")); + assert(ids(mesh.acked) === "a1,b1" && mesh.dms.length === 2, "the pumped bob turn also commits + delivers"); +} +console.log("6) agent_end terminal without willRetry OK ✅"); + +console.log("OH-MY-PI PEER SMOKE OK ✅"); diff --git a/extensions/connector-oh-my-pi/package.json b/extensions/connector-oh-my-pi/package.json new file mode 100644 index 00000000..4de1992b --- /dev/null +++ b/extensions/connector-oh-my-pi/package.json @@ -0,0 +1,48 @@ +{ + "name": "@cotal-ai/oh-my-pi", + "description": "Cotal connector for oh-my-pi: a headless native-embed peer, plus an interactive session extension that joins a live session to the mesh.", + "version": "0.1.0", + "license": "Apache-2.0", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "omp": { + "extensions": ["./dist/extension.bundle.js"] + }, + "engines": { + "node": ">=22.19.0" + }, + "scripts": { + "typecheck": "tsc -p tsconfig.json --noEmit", + "build": "tsc -p tsconfig.json && pnpm run bundle", + "bundle": "esbuild src/extension.ts --bundle --platform=node --format=esm --target=node20 --outfile=dist/extension.bundle.js --external:@oh-my-pi/*", + "smoke:extension": "tsx oh-my-pi-extension.smoke.ts", + "smoke:interactive-loop": "tsx interactive-loop.smoke.ts", + "prepublishOnly": "pnpm run build" + }, + "dependencies": { + "@oh-my-pi/pi-coding-agent": "^16.2.6", + "@cotal-ai/connector-core": "workspace:*", + "tsx": "^4.22.4" + }, + "peerDependencies": { + "@cotal-ai/core": "workspace:*" + }, + "devDependencies": { + "@cotal-ai/core": "workspace:*", + "esbuild": "^0.28.0", + "zod": "^4.4.3" + }, + "files": [ + "dist" + ], + "publishConfig": { + "access": "public" + } +} diff --git a/extensions/connector-oh-my-pi/src/connector.ts b/extensions/connector-oh-my-pi/src/connector.ts new file mode 100644 index 00000000..54d2b1c0 --- /dev/null +++ b/extensions/connector-oh-my-pi/src/connector.ts @@ -0,0 +1,56 @@ +import { fileURLToPath } from "node:url"; +import { registry, type Connector, type LaunchOpts, type LaunchSpec } from "@cotal-ai/core"; + +/** The peer loop runs via tsx (resolved from this extension's own node_modules, so it works + * regardless of the spawned process's PATH/cwd). `main` is loaded with the same extension as + * this module — `main.ts` when running from source (dev), `main.js` when running from built + * `dist/` — so the entrypoint resolves to a file that actually exists in either mode. */ +const ext = import.meta.url.endsWith(".ts") ? ".ts" : ".js"; +const TSX = fileURLToPath(new URL("../node_modules/.bin/tsx", import.meta.url)); +const MAIN = fileURLToPath(new URL(`./main${ext}`, import.meta.url)); + +/** Provider API keys oh-my-pi resolves from the environment (AuthStorage falls back to env). + * Forwarded when present so a spawned peer has credentials for its model. */ +const PROVIDER_KEYS = [ + "ANTHROPIC_API_KEY", + "OPENAI_API_KEY", + "GEMINI_API_KEY", + "GOOGLE_API_KEY", + "GROQ_API_KEY", + "XAI_API_KEY", + "DEEPSEEK_API_KEY", + "MISTRAL_API_KEY", + "OPENROUTER_API_KEY", +]; + +/** + * The oh-my-pi connector: launches an embedded Cotal peer that runs the oh-my-pi + * coding-agent SDK loop and answers mesh traffic as a lateral peer. Inbound drives the + * loop directly (prompt to wake, steer to interject mid-turn). Forwards the launcher's + * identity + minted creds so the peer authenticates as `id` under auth. Self-registers on + * import; the manager resolves it by agent type "oh-my-pi". + * + * oh-my-pi (https://github.com/can1357/oh-my-pi) is a fork of Pi, so this mirrors the `pi` + * connector but targets `@oh-my-pi/pi-coding-agent`; the divergences the fork introduced + * (e.g. retries surface as session `auto_retry_*` events, not an `agent_end.willRetry` + * flag) are handled in `peer.ts`. + */ +export const ohMyPiConnector: Connector = { + kind: "connector", + name: "oh-my-pi", + buildLaunch(opts: LaunchOpts): LaunchSpec { + const env: Record = { COTAL_SPACE: opts.space, COTAL_NAME: opts.name }; + if (opts.role) env.COTAL_ROLE = opts.role; + if (opts.id) env.COTAL_ID = opts.id; + if (opts.creds) env.COTAL_CREDS = opts.creds; + if (opts.servers) env.COTAL_SERVERS = opts.servers; + if (opts.configPath) env.COTAL_AGENT_FILE = opts.configPath; + for (const key of PROVIDER_KEYS) { + const value = process.env[key]; + if (value) env[key] = value; + } + return { command: TSX, args: [MAIN], env }; + }, +}; + +registry.register(ohMyPiConnector); diff --git a/extensions/connector-oh-my-pi/src/extension.ts b/extensions/connector-oh-my-pi/src/extension.ts new file mode 100644 index 00000000..0c31dc2e --- /dev/null +++ b/extensions/connector-oh-my-pi/src/extension.ts @@ -0,0 +1,138 @@ +/** + * Cotal mesh extension for the oh-my-pi coding agent. + * + * Loaded via `pi --extension` or from `~/.omp/agent/extensions/`, this turns an interactive + * OMP session into a first-class Cotal mesh peer — at parity with the Claude Code (MCP) and + * OpenCode (plugin) connectors, and rendered from the SAME shared source of truth + * (`cotalToolSpecs` in `@cotal-ai/connector-core`), so the cotal_* surface can't drift. + * + * • holds the MeshAgent (NATS endpoint, inbox, presence) for the session's lifetime; + * • registers the cotal_* tools natively via `pi.registerTool` (roster, inbox, send, dm, + * anycast, status, channels, …), rendered from the shared specs; + * • maps the agent's own event stream to presence (working | waiting | idle | offline); + * • DELIVERS inbound mesh traffic into the session via `pi.sendMessage(..., {deliverAs})`: + * an idle session is woken (triggerTurn), a live one is steered — never interrupting a + * running turn, matching the other connectors. It acks on turn completion, so a crash or + * error redelivers. + * + * Identity comes from COTAL_* env (the extension runs in the omp process and inherits it). + * No identity → inert, so a plain `omp` never joins as a stray peer. Set COTAL_NAME (and + * optionally COTAL_LINK / COTAL_AGENT_FILE) before launch to join; `cotal up --open` gives a + * loopback mesh that needs only COTAL_NAME. + */ +// oh-my-pi's published root barrel (`@oh-my-pi/pi-coding-agent`) is currently unconsumable under +// `nodenext` — its dist .d.ts use extensionless relative re-exports and re-export names pi-tui / +// pi-utils don't declare, voiding the whole barrel. Deep-import from the subpath entrypoint, which +// resolves to the specific .d.ts and typechecks clean. Revert to the root import once the upstream +// type-build fix is published (see the header comment in src/peer.ts). +import type { ExtensionAPI, ExtensionContext } from "@oh-my-pi/pi-coding-agent/extensibility/extensions/types"; +import { + configFromEnv, + hasIdentity, + MeshAgent, + cotalToolSpecs, + type CotalToolSpec, + type ToolResult, +} from "@cotal-ai/connector-core"; +import type { PresenceStatus } from "@cotal-ai/core"; +import { runPeerLoop } from "./interactive-loop.js"; + +function log(msg: string): void { + process.stderr.write(`[cotal-mesh] ${msg}\n`); +} + +export default function cotalMesh(pi: ExtensionAPI): void { + // No identity → a plain `omp`, not a launcher-joined session. Stay off the mesh. + if (!hasIdentity()) { + log("no COTAL_NAME / COTAL_LINK / COTAL_AGENT_FILE — staying off the mesh"); + return; + } + + const config = configFromEnv(); + config.connector = "oh-my-pi"; // advertise the host harness on our AgentCard (meta.connector) + const agent = new MeshAgent(config); + agent.start(); // background connect with retry — never blocks startup + + const loop = runPeerLoop({ mesh: agent, host: pi }); + + const safeStatus = async (status: PresenceStatus, activity?: string): Promise => { + try { + if (agent.connected) await agent.setStatus(status, activity); + } catch { + /* presence is best-effort — never throw into the agent loop */ + } + }; + + // ---- session event stream → presence + turn lifecycle ------------------- + pi.on("agent_start", async () => { + loop.onAgentStart(); + await safeStatus("working"); + }); + pi.on("tool_execution_start", async (event) => { + await safeStatus("working", event.toolName); + }); + pi.on("agent_end", async () => { + // agent_end is the turn-end signal (single-session process, notification-only): ack + flush. + await safeStatus("idle"); + loop.onAgentEnd(); + }); + pi.on("session_shutdown", async () => { + await safeStatus("offline"); + await loop.shutdown(); + }); + + // ---- cotal_* tools, rendered from the shared specs ---------------------- + const { z } = pi.zod; + for (const spec of cotalToolSpecs(config, "oh-my-pi")) { + registerSpec(pi, agent, config, spec, z); + } + + log( + `ready — space="${config.space}" name="${config.name}"${config.role ? ` role="${config.role}"` : ""} (${config.servers})`, + ); +} + +/** Render one shared CotalToolSpec onto `pi.registerTool`. `cotal_inbox` is forced read-only + * (peek): this extension delivers + acks each turn, so the agent's inbox tool must never drain, + * or it would race the ack. All others pass their args straight through to the spec's `run`. */ +function registerSpec( + pi: ExtensionAPI, + agent: MeshAgent, + config: ReturnType, + spec: CotalToolSpec, + z: ExtensionAPI["zod"]["z"], +): void { + const toResult = (r: ToolResult) => ({ + content: [{ type: "text" as const, text: r.isError ? `⚠ ${r.text}` : r.text }], + details: {}, + }); + + if (spec.name === "cotal_inbox") { + pi.registerTool({ + name: spec.name, + label: spec.title, + description: + "Show the peer messages currently waiting for you (incl. focus-mode recall). You don't normally need this — the extension delivers peer messages into your turns automatically; use it to re-check what's pending mid-task. Read-only: it never consumes them.", + parameters: z.object({}), + approval: "read", + async execute(_id, _params, _signal, _onUpdate, _ctx: ExtensionContext) { + return toResult(await spec.run(agent, config, { peek: true })); + }, + }); + return; + } + + // The shared spec carries a Zod raw shape from connector-core's own zod copy; rebuild it with the + // host's injected zod (pi.zod) so the schema type matches OMP's tool registry. The cast bridges the + // two structurally-identical zod copies at this single boundary (raw shapes are plain objects). + const parameters = z.object((spec.schema ?? {}) as Parameters[0]); + pi.registerTool({ + name: spec.name, + label: spec.title, + description: spec.description, + parameters, + async execute(_id, params, _signal, _onUpdate, _ctx: ExtensionContext) { + return toResult(await spec.run(agent, config, params ?? {})); + }, + }); +} diff --git a/extensions/connector-oh-my-pi/src/index.ts b/extensions/connector-oh-my-pi/src/index.ts new file mode 100644 index 00000000..febc70ef --- /dev/null +++ b/extensions/connector-oh-my-pi/src/index.ts @@ -0,0 +1,4 @@ +export * from "./connector.js"; +export { runOmpPeer } from "./peer.js"; +// The interactive session extension (default export = the `pi --extension` factory). +export { default as cotalMeshExtension } from "./extension.js"; diff --git a/extensions/connector-oh-my-pi/src/interactive-loop.ts b/extensions/connector-oh-my-pi/src/interactive-loop.ts new file mode 100644 index 00000000..4ce86f69 --- /dev/null +++ b/extensions/connector-oh-my-pi/src/interactive-loop.ts @@ -0,0 +1,138 @@ +/** + * The delivery loop that bridges a Cotal mesh to one oh-my-pi session — extracted from the + * extension factory so it can be driven with fakes (no NATS, no real session) in the smoke. + * + * It owns three things: (1) DELIVERY — inbound mesh traffic is injected into the session, waking + * an idle one and steering a live one, never interrupting a running turn; (2) ACK-ON-SURFACE — the + * batch injected into a turn is acked only when that turn ends, matched by id so a front-eviction + * can't ack the wrong messages; (3) PRESENCE — the session's own lifecycle events map to mesh + * presence. The real `MeshAgent` satisfies {@link PeerMesh} structurally; the real `ExtensionAPI` + * satisfies {@link PeerHost}. + */ +import { formatInjection, fmtFrom, ORIENTATION_BOOTSTRAP, type InboxItem } from "@cotal-ai/connector-core"; +import type { PresenceStatus, AttentionMode, ChannelMode } from "@cotal-ai/core"; + +/** The mesh surface the loop drives. `MeshAgent` satisfies this structurally. */ +export interface PeerMesh { + readonly connected: boolean; + readonly attention: AttentionMode; + channelMode(channel?: string): ChannelMode | undefined; + peekInbox(): InboxItem[]; + drainInbox(limit?: number): InboxItem[]; + pendingWake(): number; + setStatus(status: PresenceStatus, activity?: string): Promise; + stop(): Promise; + on(event: "incoming" | "mention-wake", handler: (item: InboxItem) => void): void; + on(event: "wake", handler: () => void): void; +} + +/** The host-session surface the loop drives. The extension's `ExtensionAPI` satisfies this. */ +export interface PeerHost { + sendMessage( + message: { customType: string; content: string; display: boolean; details: unknown; attribution: "user" | "agent" }, + options: { deliverAs: "steer"; triggerTurn: true }, + ): void; +} + +/** customType tags for injected messages — namespaced like OMP's own `irc:*`. */ +export const INCOMING = "cotal:incoming"; +export const NUDGE = "cotal:nudge"; + +/** A running peer loop. The factory owns the mesh + process lifecycle around it. */ +export interface PeerLoop { + /** Call on `agent_start`: a turn began — hold delivery. */ + onAgentStart(): void; + /** Call on `agent_end`: the turn ended — ack the surfaced batch, then flush the next. */ + onAgentEnd(): void; + /** Presence: this session is offline; stop the mesh. */ + shutdown(): Promise; +} + +/** + * Wire a {@link PeerMesh}'s inbox to a {@link PeerHost} and return the turn-lifecycle hooks the + * factory calls from the session's event stream. + */ +export function runPeerLoop({ mesh, host }: { mesh: PeerMesh; host: PeerHost }): PeerLoop { + // One session, driven straight off the inbox. `busy` gates delivery so a message that arrives + // mid-turn waits for turn end (no-interrupt). `surfaced` holds the ids injected into the current + // turn, acked on completion by id (not count) so a front-eviction can't ack the wrong messages. + let busy = false; + let surfaced: string[] = []; + let primed = false; // orientation bootstrap prepended once, on the first delivered turn + + /** Inject the current inbox batch (peeked, not drained). `override` replaces the body with a bare + * nudge (focus @mention recall) and surfaces nothing to ack. Never drives into a running turn. */ + function drive(override?: string): void { + if (busy) return; + let text: string; + let ids: string[] = []; + if (override) { + text = override; + } else { + const items = mesh.peekInbox(); + if (items.length === 0) return; + ids = items.map((i) => i.id); + const inj = formatInjection(items); + if (!inj) return; + text = inj; + } + if (!primed) { + primed = true; + text = `${ORIENTATION_BOOTSTRAP}\n\n${text}`; + } + busy = true; + surfaced = ids; + // The content participates in LLM context (a CustomMessage); triggerTurn wakes an idle session, + // steer folds into a live one. Attribution "user" — a peer message is external input here. + host.sendMessage( + { customType: override ? NUDGE : INCOMING, content: text, display: true, details: {}, attribution: "user" }, + { deliverAs: "steer", triggerTurn: true }, + ); + } + + /** Ack the surfaced batch — but only the leading run STILL at the front of the inbox, matched by + * id. The mesh evicts from the FRONT at its cap, so a long turn on a chatty channel can shift our + * surfaced prefix out; matching by id (not a raw count) means we never ack the wrong, newer + * messages. Any surfaced survivor that no longer leads is left unacked → redelivered. */ + function ackSurfaced(): void { + if (surfaced.length === 0) return; + const front = mesh.peekInbox(); + let n = 0; + while (n < surfaced.length && n < front.length && front[n].id === surfaced[n]) n++; + if (n > 0) mesh.drainInbox(n); + surfaced = []; + } + + // ---- inbound mesh → delivery -------------------------------------------- + // A directed message (DM / anycast / @mention) drives when idle; ambient channel chatter drives + // only in `open` while idle (dnd/focus hold it for the next turn); a per-channel `quiet` channel + // never ambient-drives. `muted` ambient never reaches here (ack-dropped at ingest). + mesh.on("incoming", (item: InboxItem) => { + if (busy) return; // buffer; onAgentEnd drives at turn end + const directed = item.kind !== "channel" || item.mentionsMe; + const quiet = item.kind === "channel" && mesh.channelMode(item.channel) === "quiet"; + if (directed || (!quiet && mesh.attention === "open")) drive(); + }); + mesh.on("mention-wake", (item: InboxItem) => { + // Focus: the @mention body was acked-and-dropped at ingest — wake a turn to PULL it (recall). + if (!busy) drive(`📨 You were mentioned by ${fmtFrom(item)} on #${item.channel ?? "?"} — read it with cotal_inbox.`); + }); + mesh.on("wake", () => { + if (!busy) drive(); + }); + + return { + onAgentStart(): void { + busy = true; + }, + onAgentEnd(): void { + // turn-end: release the no-interrupt gate, ack the surfaced batch, flush the next. + busy = false; + ackSurfaced(); + if (mesh.pendingWake() > 0) drive(); + }, + async shutdown(): Promise { + await mesh.stop(); + }, + }; +} diff --git a/extensions/connector-oh-my-pi/src/loop.ts b/extensions/connector-oh-my-pi/src/loop.ts new file mode 100644 index 00000000..a9183f75 --- /dev/null +++ b/extensions/connector-oh-my-pi/src/loop.ts @@ -0,0 +1,192 @@ +import { InboxTurn } from "@cotal-ai/connector-core"; +import type { InboxItem, InboxSource } from "@cotal-ai/connector-core"; +// Type-only: the event union is erased at build/runtime, so this module has NO oh-my-pi +// value import and loads under plain node/tsx (the oh-my-pi runtime pulls in `bun`, which is +// unloadable off-Bun). The runtime wiring lives in `peer.ts`; this is the injectable loop. +import type { AgentSessionEvent } from "@oh-my-pi/pi-coding-agent/session/agent-session"; + +export function log(e: unknown): void { + process.stderr.write(`[oh-my-pi-peer] ${e instanceof Error ? e.message : String(e)}\n`); +} + +/** + * The mesh surface {@link runPeerLoop} drives: presence, reply delivery, and a stream-backed + * inbox ({@link InboxSource}). The real `MeshAgent` satisfies this structurally; the smoke + * test passes a fake so the loop can be exercised with no NATS connection. + */ +export interface PeerMesh extends InboxSource { + readonly id: string; + setStatus(status: "idle" | "waiting" | "working", activity?: string): Promise; + send(text: string, channel?: string, mentions?: string[]): Promise; + dm(target: string, text: string): Promise; + on(event: "incoming" | "wake", listener: () => void): unknown; +} + +/** + * The slice of the oh-my-pi `AgentSession` the loop drives. The real session satisfies this + * structurally; the smoke passes a stub whose `prompt`/`steer` record and whose event stream + * is driven with the scripted `agent_start`/`agent_end` events. + */ +export interface PeerSession { + subscribe(listener: (event: AgentSessionEvent) => void): () => void; + prompt(text: string): Promise; + steer(text: string): Promise; + abort(): Promise; + dispose(): Promise; +} + +/** A running peer loop. {@link runOmpPeer} owns the mesh + process lifecycle around it. */ +export interface PeerLoop { + /** Abandon any in-flight turn (no ack → redeliver) and tear the session down. */ + shutdown(): Promise; +} + +/** Actionable = a DM, an anycast to our role, or a channel message that names us — and not + * our own echo. Pure ambient channel chatter is dropped (acked, never answered). */ +function actionable(mesh: Pick, item: InboxItem): boolean { + if (item.fromId === mesh.id) return false; + return item.kind !== "channel" || item.mentionsMe; +} + +/** The audience a reply goes back to. A channel message is answered ON that channel + * (sender-independent — everyone there already saw it); a DM/anycast is answered privately + * to its sender. Two messages with the same scope can share one turn + reply; mixing scopes + * cannot (a DM folded into a channel turn would broadcast private content), so different- + * scope messages get their own scope-isolated turn. */ +function scopeKey(item: InboxItem): string { + return item.kind === "channel" && item.channel ? `channel:${item.channel}` : `dm:${item.fromId}`; +} + +/** Pull this turn's final assistant text from the agent_end payload (not the session-wide + * last message), so a turn that produced no text never re-delivers a previous reply. */ +function turnReplyText(messages: readonly unknown[]): string | undefined { + for (let i = messages.length - 1; i >= 0; i--) { + const m = messages[i]; + if (!m || typeof m !== "object" || !("role" in m) || m.role !== "assistant") continue; + if (!("content" in m) || !Array.isArray(m.content)) continue; + const text = m.content + .map((p) => (p && typeof p === "object" && "type" in p && p.type === "text" && "text" in p ? String(p.text ?? "") : "")) + .join(""); + return text.length ? text : undefined; + } + return undefined; +} + +/** + * Wire an oh-my-pi session's event stream to a {@link PeerMesh}'s inbox and run the + * reply/scope/ack loop. Pure wiring over the two injected surfaces — no NATS, no process + * lifecycle, no session creation — so it can be driven against fakes ({@link PeerMesh} + + * {@link PeerSession}) in the smoke test. {@link runOmpPeer} owns the real instances and the + * mesh/process lifecycle around this. + * + * `prompt()` wakes an idle session on the front message, `steer()` interjects into a live one + * (true mid-turn drive), and presence is read off the session's event stream. The loop owns + * reply routing, so the model never mis-routes. + * + * Delivery is ack-on-surface: the inbox is the single source of truth (no parallel buffer); + * a turn surfaces a front-contiguous run and `commit()`s (drainInbox-acks) it only once the + * turn completes, so a crash/interrupt redelivers. Each turn is owned by one reply scope — + * a mid-turn message is steered in only when it shares that scope; a different-scope message + * stays on the stream and becomes the next turn's origin — so a private DM is never folded + * into a channel broadcast. + * + * oh-my-pi is a fork of Pi, so this mirrors the `@cotal-ai/pi` connector; one fork + * divergence is handled here — oh-my-pi surfaces retries as session-level `auto_retry_*` + * events rather than an `agent_end.willRetry` flag, so an `agent_end` here is always the + * turn's terminal event. + */ +export function runPeerLoop({ mesh, session }: { mesh: PeerMesh; session: PeerSession }): PeerLoop { + const turn = new InboxTurn(mesh); + let streaming = false; // gates steer(): only valid once the agent is actually streaming + + const setStatus = (status: "idle" | "working", activity?: string): void => { + void mesh.setStatus(status, activity).catch(() => {}); + }; + + const framed = (item: InboxItem): string => + `from ${item.fromName} via ${item.kind}: ${item.text}`; + + function deliver(to: InboxItem, text: string): void { + if (to.kind === "channel" && to.channel) void mesh.send(text, to.channel).catch(log); + else void mesh.dm(to.fromId, text).catch(log); + } + + /** Start the next turn on the front actionable message, dropping leading non-actionable + * (own echoes, ambient chatter) first. No-op while a turn is in flight. */ + function pump(): void { + if (turn.inFlight) return; + turn.drop((i) => !actionable(mesh, i)); + const origin = turn.start(); + if (!origin) { + setStatus("idle"); + return; + } + streaming = false; + void session.prompt(framed(origin)).catch(onStartError); // wake into a fresh turn + } + + /** Fold any front-contiguous, same-scope actionable messages into the live turn (mid-turn + * steer). A cross-scope or ambient message breaks contiguity and waits for its own turn. */ + function foldSameScope(): void { + if (!turn.origin || !streaming) return; + for (const item of turn.extend((i, o) => actionable(mesh, i) && scopeKey(i) === scopeKey(o))) { + void session.steer(framed(item)).catch(log); + } + } + + function onStartError(e: unknown): void { + log(e); + if (streaming) return; // already running → agent_end will complete the turn + turn.commit(); // pre-flight failure (e.g. no model/key): drop, no retry-loop + setStatus("idle"); + pump(); + } + + mesh.on("incoming", () => { + if (turn.inFlight) foldSameScope(); + else pump(); + }); + mesh.on("wake", () => { + if (!turn.inFlight) pump(); + }); + + session.subscribe((event: AgentSessionEvent) => { + switch (event.type) { + case "agent_start": + streaming = true; + setStatus("working", "thinking"); + foldSameScope(); // flush same-scope peers that landed before streaming began + break; + case "tool_execution_start": + setStatus("working", `running ${event.toolName}`); + break; + case "tool_execution_end": + setStatus("working", "thinking"); // clear the per-tool activity so it can't read stale + break; + case "agent_end": { + // oh-my-pi has no `agent_end.willRetry`; a retry is its own session `auto_retry_*` + // event and the failed turn still ends here, so `agent_end` is always terminal. + const to = turn.origin; + const reply = turnReplyText(event.messages); + turn.commit(); // ack the surfaced run — clean or failed both consume (no retry-loop) + streaming = false; + if (to && reply) deliver(to, reply); + pump(); // next scope + break; + } + } + }); + + // Drain anything already buffered before the listeners were attached. + pump(); + + return { + async shutdown(): Promise { + if (turn.inFlight) { + turn.abandon(); // leave the in-flight run on the stream → redeliver, no peer dropped + await session.abort(); + } + void session.dispose(); + }, + }; +} diff --git a/extensions/connector-oh-my-pi/src/main.ts b/extensions/connector-oh-my-pi/src/main.ts new file mode 100644 index 00000000..69c98e38 --- /dev/null +++ b/extensions/connector-oh-my-pi/src/main.ts @@ -0,0 +1,6 @@ +import { runOmpPeer } from "./peer.js"; + +runOmpPeer().catch((e) => { + process.stderr.write(`[oh-my-pi-peer] fatal: ${(e as Error).message}\n`); + process.exit(1); +}); diff --git a/extensions/connector-oh-my-pi/src/peer.ts b/extensions/connector-oh-my-pi/src/peer.ts new file mode 100644 index 00000000..0c880742 --- /dev/null +++ b/extensions/connector-oh-my-pi/src/peer.ts @@ -0,0 +1,96 @@ +import { MeshAgent, configFromEnv } from "@cotal-ai/connector-core"; +// oh-my-pi's published root barrel (`@oh-my-pi/pi-coding-agent`) is currently +// unconsumable under `nodenext` — its dist .d.ts use extensionless relative +// re-exports (TS2834) and re-export names pi-tui/pi-utils don't declare, voiding +// the whole barrel. So we deep-import from the subpath entrypoints, which resolve +// to the specific .d.ts and typecheck clean. Revert to the root import once the +// upstream type-build fix (can1357/oh-my-pi) is published. +import { createAgentSession } from "@oh-my-pi/pi-coding-agent/sdk"; +import { SessionManager } from "@oh-my-pi/pi-coding-agent/session/session-manager"; +import { + defineTool, + Type, +} from "@oh-my-pi/pi-coding-agent/extensibility/legacy-pi-coding-agent-shim"; +import type { Static } from "@oh-my-pi/pi-coding-agent/extensibility/typebox"; +import { runPeerLoop } from "./loop.js"; + +/** + * Read-only / awareness tools. Replies are NOT sent by the model — the run loop delivers + * the agent's final text on the right delivery mode (see runOmpPeer), so the model can't + * mis-route or duplicate a reply. These just let it see who is present and report its own + * status. Mirrors the pi / openai-agents / vercel-ai adapters. + */ +function buildTools(mesh: MeshAgent) { + const cotal_roster = defineTool({ + name: "cotal_roster", + label: "Cotal roster", + description: "List the peers currently present on the Cotal mesh.", + parameters: Type.Object({}), + execute: async () => { + const peers = mesh.roster(); + const text = peers.length + ? peers + .map((p) => `${p.card.name}${p.card.role ? `/${p.card.role}` : ""} [${p.status}]`) + .join("\n") + : "roster is empty"; + return { content: [{ type: "text", text }], details: {} }; + }, + }); + + const statusParams = Type.Object({ + status: Type.Union([Type.Literal("idle"), Type.Literal("waiting"), Type.Literal("working")]), + activity: Type.Optional(Type.String()), + }); + const cotal_status = defineTool({ + name: "cotal_status", + label: "Cotal status", + description: "Update this peer's presence status on the mesh.", + parameters: statusParams, + execute: async (_id: string, params: Static) => { + await mesh.setStatus(params.status, params.activity); + return { content: [{ type: "text", text: `status set to ${params.status}` }], details: {} }; + }, + }); + + return [cotal_roster, cotal_status]; +} + +/** + * Embed an oh-my-pi coding-agent session in-process and drive it from mesh traffic. This is + * the native-embed pattern (cf. docs/agent-frameworks.md): MeshAgent owns the NATS + * connection, presence, and a stream-backed inbox; oh-my-pi's loop is driven straight off + * that inbox via {@link runPeerLoop} — `prompt()` wakes an idle session on the front + * message, `steer()` interjects into a live one (true mid-turn drive), and presence is read + * off the session's event stream. The loop owns reply routing, so the model never + * mis-routes. + */ +export async function runOmpPeer(): Promise { + const mesh = new MeshAgent(configFromEnv()); + mesh.start(); + + // oh-my-pi discovers auth + the model registry from the environment when they + // aren't supplied (a spawned peer gets provider keys via the connector's + // buildLaunch env), so we let createAgentSession default them rather than wiring + // them by hand — matches oh-my-pi's own SDK usage. + const { session } = await createAgentSession({ + cwd: process.cwd(), + sessionManager: SessionManager.inMemory(), + customTools: buildTools(mesh), + }); + + const loop = runPeerLoop({ mesh, session }); + + async function shutdown(): Promise { + try { + await loop.shutdown(); + await mesh.stop(); + } finally { + process.exit(0); + } + } + process.on("SIGINT", () => void shutdown()); + process.on("SIGTERM", () => void shutdown()); + + // Keep alive. + await new Promise(() => {}); +} diff --git a/extensions/connector-oh-my-pi/tsconfig.json b/extensions/connector-oh-my-pi/tsconfig.json new file mode 100644 index 00000000..051d08e2 --- /dev/null +++ b/extensions/connector-oh-my-pi/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist" + }, + "include": ["src"] +} diff --git a/package.json b/package.json index 7d81818f..dca2fbd6 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,10 @@ "smoke": "tsx packages/core/smoke.ts", "smoke:auth": "tsx packages/core/smoke-auth.ts", "smoke:orientation": "tsx extensions/connector-core/smoke/orientation.smoke.ts", + "smoke:inbox": "tsx extensions/connector-core/inbox-turn.smoke.ts", + "smoke:oh-my-pi": "tsx extensions/connector-oh-my-pi/oh-my-pi-peer.smoke.ts", + "smoke:oh-my-pi-extension": "tsx extensions/connector-oh-my-pi/oh-my-pi-extension.smoke.ts", + "smoke:oh-my-pi-interactive-loop": "tsx extensions/connector-oh-my-pi/interactive-loop.smoke.ts", "smoke:view": "tsx implementations/cli/smoke/view.smoke.ts", "smoke:members": "tsx packages/core/smoke/members.smoke.ts", "smoke:control-reply-bound": "tsx packages/core/smoke/control-reply-bound.smoke.ts", diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 0774c3ad..160bd541 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -8,3 +8,6 @@ packages: allowBuilds: esbuild: true msgpackr-extract: false + onnxruntime-node: false + protobufjs: false + sharp: false From cbd9301331ab6ad4dbb7bd69eec35cbda935ac6e Mon Sep 17 00:00:00 2001 From: seal Date: Fri, 3 Jul 2026 17:03:57 -0400 Subject: [PATCH 02/15] fix(cli): declare @cotal-ai/delivery so bin/cotal.ts resolves MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bin/cotal.ts imports @cotal-ai/delivery (added with the Plane-3 delivery daemon) but it was never in the root package.json deps, so a clean checkout can't run `cotal` (or `pnpm cotal`) — ERR_MODULE_NOT_FOUND on @cotal-ai/delivery. Co-Authored-By: seal --- package.json | 1 + pnpm-lock.yaml | 3 +++ 2 files changed, 4 insertions(+) diff --git a/package.json b/package.json index dca2fbd6..b83764ac 100644 --- a/package.json +++ b/package.json @@ -108,6 +108,7 @@ "@cotal-ai/connector-hermes": "workspace:*", "@cotal-ai/connector-opencode": "workspace:*", "@cotal-ai/core": "workspace:*", + "@cotal-ai/delivery": "workspace:*", "@cotal-ai/manager": "workspace:*", "@cotal-ai/tmux": "workspace:*" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ee55efea..471be46a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,9 @@ importers: '@cotal-ai/core': specifier: workspace:* version: link:packages/core + '@cotal-ai/delivery': + specifier: workspace:* + version: link:implementations/delivery '@cotal-ai/manager': specifier: workspace:* version: link:implementations/manager From 25bd139fb736a7dcce07d6c26b101534734ff750 Mon Sep 17 00:00:00 2001 From: seal Date: Fri, 3 Jul 2026 19:12:45 -0400 Subject: [PATCH 03/15] fix(oh-my-pi): only join the mesh from an interactive session A task/print/RPC subagent inherits the parent's COTAL_* env, so gating on hasIdentity() alone made every subagent a stray same-named mesh peer (roster pollution + ambiguous DMs, and a subagent could receive traffic meant for the main session). Defer the mesh-join to session_start and start only when ctx.hasUI is true; non-interactive sessions stay off the mesh. Smoke covers both branches. Co-Authored-By: seal --- .../oh-my-pi-extension.smoke.ts | 42 +++++++++++++++++++ .../connector-oh-my-pi/src/extension.ts | 23 +++++++++- 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/extensions/connector-oh-my-pi/oh-my-pi-extension.smoke.ts b/extensions/connector-oh-my-pi/oh-my-pi-extension.smoke.ts index 95776f95..829f9602 100644 --- a/extensions/connector-oh-my-pi/oh-my-pi-extension.smoke.ts +++ b/extensions/connector-oh-my-pi/oh-my-pi-extension.smoke.ts @@ -9,6 +9,7 @@ */ import cotalMesh from "./src/extension.ts"; import * as zodV4 from "zod/v4"; +import { MeshAgent } from "@cotal-ai/connector-core"; function assert(cond: unknown, msg: string): asserts cond { if (!cond) { @@ -88,5 +89,46 @@ process.env.COTAL_SERVERS = "nats://127.0.0.1:4222"; // never actually connected if (shutdown) await shutdown(undefined); } +// ---- 4. session_start gates the mesh-join on ctx.hasUI ------------------------ +// A task/print/RPC subagent inherits the parent's COTAL_* env, so hasIdentity() alone would make +// every subagent a stray same-named peer. The extension defers the mesh-join to session_start and +// only calls agent.start() when ctx.hasUI is true (interactive/top-level). Observe agent.start via a +// spy on MeshAgent.prototype.start — no-op'd so no real NATS reconnect loop spins — then restore it. +// Each branch loads a fresh factory: the `started` guard is per-instance, so one instance can't be +// re-driven. Env from block 2 (COTAL_NAME/COTAL_SERVERS) is still set → identity is present. +{ + const origStart = MeshAgent.prototype.start; + let startCalls = 0; + MeshAgent.prototype.start = function () { + startCalls++; + }; + try { + // (a) non-interactive session (subagent/print/RPC): hasUI:false → stays off the mesh. + { + const { pi, events } = fakePi(); + cotalMesh(pi as never); + const sessionStart = events.get("session_start") as + | ((event: unknown, ctx: { hasUI: boolean }) => unknown) + | undefined; + assert(sessionStart, "identity → subscribes to session_start"); + startCalls = 0; + await sessionStart(undefined, { hasUI: false }); + assert(startCalls === 0, "hasUI:false → agent.start NOT invoked (subagent stays off mesh)"); + } + // (b) interactive top-level session: hasUI:true → joins the mesh. + { + const { pi, events } = fakePi(); + cotalMesh(pi as never); + const sessionStart = events.get("session_start") as (event: unknown, ctx: { hasUI: boolean }) => unknown; + startCalls = 0; + await sessionStart(undefined, { hasUI: true }); + assert(startCalls === 1, "hasUI:true → agent.start invoked (interactive session joins)"); + } + } finally { + MeshAgent.prototype.start = origStart; + } + console.log("4) session_start + hasUI gates mesh-join OK ✅"); +} + console.log("\nCOTAL-MESH EXTENSION SMOKE OK ✅"); process.exit(0); diff --git a/extensions/connector-oh-my-pi/src/extension.ts b/extensions/connector-oh-my-pi/src/extension.ts index 0c31dc2e..1711ea2c 100644 --- a/extensions/connector-oh-my-pi/src/extension.ts +++ b/extensions/connector-oh-my-pi/src/extension.ts @@ -51,7 +51,26 @@ export default function cotalMesh(pi: ExtensionAPI): void { const config = configFromEnv(); config.connector = "oh-my-pi"; // advertise the host harness on our AgentCard (meta.connector) const agent = new MeshAgent(config); - agent.start(); // background connect with retry — never blocks startup + + // Join the mesh only from a real interactive (top-level) session. A `task`/print/RPC subagent + // inherits the parent's COTAL_* env, so hasIdentity() alone would make every subagent a stray + // same-named peer (polluting the roster + making DMs to that name ambiguous, and worse, a + // subagent could receive mesh traffic meant for the main session). `ctx.hasUI` is false in + // print/RPC/subagent mode and true for the interactive session — the available signal that + // distinguishes them (OMP's internal agentKind "main"|"sub" isn't exposed to extensions). + // Deferred to session_start because hasUI is only on the handler ctx, not the factory arg. + // NOTE: a future headless launcher (e.g. Compass spawning a real worker) is also hasUI:false and + // WOULD need to join — revisit with an explicit signal (agentKind/env opt-in) when that lands. + let started = false; + pi.on("session_start", (_event, ctx: ExtensionContext) => { + if (started) return; + started = true; + if (!ctx.hasUI) { + log("non-interactive session (subagent/print/RPC) — staying off the mesh"); + return; + } + agent.start(); // background connect with retry — never blocks + }); const loop = runPeerLoop({ mesh: agent, host: pi }); @@ -88,7 +107,7 @@ export default function cotalMesh(pi: ExtensionAPI): void { } log( - `ready — space="${config.space}" name="${config.name}"${config.role ? ` role="${config.role}"` : ""} (${config.servers})`, + `loaded — space="${config.space}" name="${config.name}"${config.role ? ` role="${config.role}"` : ""} (${config.servers}); joins the mesh on session_start if interactive`, ); } From fe463e2e1915d9bcb1150f14865f7ea1ec536979 Mon Sep 17 00:00:00 2001 From: seal Date: Fri, 3 Jul 2026 20:10:32 -0400 Subject: [PATCH 04/15] fix(connector): stop mesh reconnect churn from flooding + corrupting the host TUI A mid-session mesh drop made MeshAgent log every endpoint reconnect-failure (TIMEOUT) to stderr on a fixed 3s retry, which in the in-process oh-my-pi extension flooded and corrupted the live terminal. Log the drop/recover edges once (suppressing the retry churn), inject a logger so oh-my-pi routes through pi.logger (a file, not the shared terminal), and back the endpoint's reconnect retries off exponentially (3s->30s). Co-Authored-By: seal --- .../smoke/reconnect-log.smoke.ts | 80 +++++++++++++++++ extensions/connector-core/src/agent.ts | 90 +++++++++++++++++-- .../connector-oh-my-pi/src/extension.ts | 16 ++-- package.json | 1 + packages/core/src/endpoint.ts | 14 +-- 5 files changed, 181 insertions(+), 20 deletions(-) create mode 100644 extensions/connector-core/smoke/reconnect-log.smoke.ts diff --git a/extensions/connector-core/smoke/reconnect-log.smoke.ts b/extensions/connector-core/smoke/reconnect-log.smoke.ts new file mode 100644 index 00000000..642dba43 --- /dev/null +++ b/extensions/connector-core/smoke/reconnect-log.smoke.ts @@ -0,0 +1,80 @@ +/** + * Reconnect-logging smoke (no NATS) — proves a mesh drop can't flood the host or corrupt its TUI. + * CotalEndpoint is an EventEmitter, so we drive MeshAgent's endpoint events directly (never + * connecting) and assert the anti-flood + off-terminal contract: + * - a drop logs exactly ONE "connection lost" line; recovery logs exactly ONE "reconnected" line; + * - the repeated endpoint errors during the outage (the TIMEOUT flood) are SUPPRESSED; + * - a live-connection error still surfaces (ACL denial, etc.), and an identical repeat is deduped; + * - an INJECTED logger receives every line and process.stderr is NEVER touched — so the in-process + * OMP extension (which passes pi.logger) can't scribble on the shared terminal. + * Run: pnpm smoke:reconnect-log + */ +import { MeshAgent, type MeshLogLevel } from "../src/agent.js"; +import type { AgentConfig } from "../src/config.js"; + +let failures = 0; +function check(label: string, cond: boolean, extra?: unknown): void { + console.log(`${cond ? "✓" : "✗"} ${label}${cond ? "" : ` — ${JSON.stringify(extra)}`}`); + if (!cond) failures++; +} + +const cfg: AgentConfig = { + space: "smoke", + name: "log-canary", + servers: "nats://127.0.0.1:1", + subscribe: [], + allowSubscribe: [], + allowPublish: [], + kind: "agent", + tls: false, +}; + +const lines: { msg: string; level: MeshLogLevel }[] = []; +const agent = new MeshAgent(cfg, (msg, level) => lines.push({ msg, level })); +const endpointErrors = () => lines.filter((l) => l.msg.includes("endpoint error")); + +// Guard: with a logger injected, NOTHING may reach the shared terminal. +let stderrWrites = 0; +const realWrite = process.stderr.write.bind(process.stderr); +(process.stderr as unknown as { write: (s: string) => boolean }).write = () => { + stderrWrites++; + return true; +}; + +try { + const ep = agent.ep; + + // Initial connect: the observer must NOT announce a "reconnect" (connectLoop logs the first connect). + ep.emit("connection", { connected: true }); + check("initial connect logs no 'reconnected'", lines.filter((l) => l.msg.includes("reconnected")).length === 0, lines); + + // Drop. + ep.emit("connection", { connected: false }); + const lost = lines.filter((l) => l.msg.includes("connection lost")); + check("drop logs exactly one 'connection lost' at warn", lost.length === 1 && lost[0].level === "warn", lost); + + // The flood: repeated endpoint errors while disconnected — the exact spam that broke the TUI. + for (let i = 0; i < 8; i++) ep.emit("error", new Error("TIMEOUT")); + check("outage endpoint errors are suppressed", endpointErrors().length === 0, lines); + + // Recover. + ep.emit("connection", { connected: true }); + const recon = lines.filter((l) => l.msg.includes("reconnected to the mesh")); + check("recovery logs exactly one 'reconnected' at info", recon.length === 1 && recon[0].level === "info", recon); + + // A live-connection error DOES surface (genuine, actionable). + ep.emit("error", new Error("NATS permission denied: cannot publish")); + check("live error surfaces once", endpointErrors().length === 1, lines); + + // An identical consecutive error is deduped (spam guard for a connected-but-flapping error). + ep.emit("error", new Error("NATS permission denied: cannot publish")); + check("identical consecutive live error is deduped", endpointErrors().length === 1, lines); + + // The whole sequence never touched the terminal. + check("no writes to process.stderr (no TUI corruption)", stderrWrites === 0, stderrWrites); +} finally { + (process.stderr as unknown as { write: typeof realWrite }).write = realWrite; +} + +console.log(`\nRECONNECT-LOG SMOKE ${failures === 0 ? "OK ✅" : "FAILED ❌"} (${lines.length} lines)`); +process.exit(failures === 0 ? 0 : 1); diff --git a/extensions/connector-core/src/agent.ts b/extensions/connector-core/src/agent.ts index f6ffc780..e2a1cba9 100644 --- a/extensions/connector-core/src/agent.ts +++ b/extensions/connector-core/src/agent.ts @@ -62,8 +62,31 @@ interface Pending { ack: () => void; } +/** Severity for a connector log line. The default sink maps everything to stderr; an injected + * sink (the in-process oh-my-pi extension passes `pi.logger`) routes by level to a FILE so a + * mesh blip never scribbles on the host TUI's shared terminal. */ +export type MeshLogLevel = "info" | "warn" | "error"; + +/** Where a {@link MeshAgent}'s diagnostics go. Default: one prefixed line per call to stderr — + * correct for the out-of-process connectors (Claude Code MCP, OpenCode, Hermes) that own their + * stderr. An in-process host (oh-my-pi) MUST inject its own file logger, or reconnect churn + * corrupts the rendered screen. */ +export type MeshLogger = (msg: string, level?: MeshLogLevel) => void; + const MAX_INBOX = 200; +/** Backoff ceiling for the initial-connect + self-heal retry loops. Growth from the first + * `retryMs` is exponential up to this, so a mesh that's down at launch (or dropped mid-session) + * is retried politely rather than hammered every 3s. */ +const MAX_RETRY_MS = 30_000; + +/** Default diagnostics sink: one prefixed line per call to stderr. Correct for the out-of-process + * connectors (Claude Code MCP, OpenCode, Hermes) that own their own stderr; the in-process + * oh-my-pi extension injects a file logger instead so mesh churn can't corrupt the TUI. */ +function defaultLogger(msg: string, _level?: MeshLogLevel): void { + process.stderr.write(`[cotal-connector] ${msg}\n`); +} + function sleep(ms: number): Promise { return new Promise((r) => setTimeout(r, ms)); } @@ -110,10 +133,22 @@ export class MeshAgent extends EventEmitter { * published after it ("since you entered focus"). Undefined unless in focus. */ private focusSince?: number; private stopping = false; - - constructor(config: AgentConfig) { + /** Diagnostics sink. Defaults to one prefixed stderr line per call; an in-process host injects + * a file logger so mesh churn never touches the shared terminal. */ + private readonly logger: MeshLogger; + /** Connectivity as last OBSERVED from the endpoint's `connection` event — drives the + * drop/recover edge so a lost mesh logs ONCE, not every retry. Starts true so the first + * `connection:false` before any connect (initial-connect failure) isn't mis-logged as a "drop"; + * the initial-connect banner is {@link connectLoop}'s job. */ + private _observedConnected = true; + /** The last `endpoint error` text logged while connected — suppresses an identical consecutive + * repeat (a connected-but-flapping error) without hiding a genuinely new one. */ + private lastLoggedError?: string; + + constructor(config: AgentConfig, logger?: MeshLogger) { super(); this.config = config; + this.logger = logger ?? defaultLogger; // Seed per-channel attention from the operator's file default (one-way: the runtime never writes // back — the persona file is a shared template). muted/quiet are validated disjoint at file load. for (const c of config.quiet ?? []) this.channelModes.set(c, "quiet"); @@ -142,11 +177,11 @@ export class MeshAgent extends EventEmitter { }, }); this.ep.on("message", (m: CotalMessage, d: Delivery, meta?: MessageMeta) => this.ingest(m, d, meta)); - this.ep.on("error", (e: Error) => this.log(`endpoint error: ${e.message}`)); + this.ep.on("error", (e: Error) => this.onEndpointError(e)); // The endpoint's (re)binds are the single source of truth for connectedness: this fires on // initial start, manual reconnect, AND the background self-heal — so a recovery the endpoint // did on its own can't leave us thinking we're offline (which would skip stop() → leak). - this.ep.on("connection", (e: { connected: boolean }) => { this._connected = e.connected; }); + this.ep.on("connection", (e: { connected: boolean }) => this.onConnectionChange(e.connected)); } get id(): string { @@ -163,22 +198,32 @@ export class MeshAgent extends EventEmitter { this._contextId = clean ? clean : undefined; } - /** Begin connecting (with background retry). Returns immediately. */ + /** Begin connecting (with background retry). Returns immediately. `retryMs` is the FIRST + * backoff; it grows exponentially to {@link MAX_RETRY_MS} so a mesh that's down at launch is + * retried politely, not hammered every 3s. */ start(retryMs = 3000): void { void this.connectLoop(retryMs); } private async connectLoop(retryMs: number): Promise { + let delay = retryMs; while (!this.stopping && !this._connected) { try { await this.ep.start(); // _connected is set by the endpoint's "connection" event (fired inside start()), not here. this.log( `connected to ${this.config.servers} as ${this.who()} in space "${this.config.space}" on #${this.config.subscribe.join(", #")}`, + "info", ); } catch (e) { - this.log(`mesh unreachable (${(e as Error).message}); retrying in ${retryMs}ms`); - await sleep(retryMs); + // Log the FIRST failure of an outage once (at warn), then stay quiet through the retries — + // the drop/recover edge is tracked by _observedConnected so a down mesh never floods. + if (this._observedConnected) { + this._observedConnected = false; + this.log(`mesh unreachable (${(e as Error).message}); retrying in the background`, "warn"); + } + await sleep(delay); + delay = Math.min(delay * 2, MAX_RETRY_MS); } } } @@ -721,7 +766,34 @@ export class MeshAgent extends EventEmitter { } } - private log(msg: string): void { - process.stderr.write(`[cotal-connector] ${msg}\n`); + /** React to an endpoint connectivity change (initial connect, manual reconnect, or background + * self-heal). The drop → recover edges each log ONCE, so a mesh outage can't flood the host: + * the endpoint's `reestablishLoop` emits an `error` per failed retry, all of which + * {@link onEndpointError} then suppresses while we're disconnected. */ + private onConnectionChange(connected: boolean): void { + const was = this._observedConnected; + this._connected = connected; + this._observedConnected = connected; + if (was && !connected) { + this.log("mesh connection lost — retrying in the background", "warn"); + } else if (!was && connected) { + this.lastLoggedError = undefined; // a fresh connection: the next genuine error is worth logging + this.log("reconnected to the mesh", "info"); + } + } + + /** An endpoint `error`. While DISCONNECTED it's reconnect churn (the `reestablishLoop` TIMEOUT + * per retry) — suppressed, since {@link onConnectionChange} already logged the drop once. While + * connected it's a genuine, actionable fault (e.g. an ACL denial); log it, deduping an identical + * consecutive repeat so a flapping error can't spam either. */ + private onEndpointError(e: Error): void { + if (!this._connected) return; + if (e.message === this.lastLoggedError) return; + this.lastLoggedError = e.message; + this.log(`endpoint error: ${e.message}`, "error"); + } + + private log(msg: string, level: MeshLogLevel = "info"): void { + this.logger(msg, level); } } diff --git a/extensions/connector-oh-my-pi/src/extension.ts b/extensions/connector-oh-my-pi/src/extension.ts index 1711ea2c..3154a522 100644 --- a/extensions/connector-oh-my-pi/src/extension.ts +++ b/extensions/connector-oh-my-pi/src/extension.ts @@ -33,15 +33,21 @@ import { cotalToolSpecs, type CotalToolSpec, type ToolResult, + type MeshLogger, } from "@cotal-ai/connector-core"; import type { PresenceStatus } from "@cotal-ai/core"; import { runPeerLoop } from "./interactive-loop.js"; -function log(msg: string): void { - process.stderr.write(`[cotal-mesh] ${msg}\n`); -} - export default function cotalMesh(pi: ExtensionAPI): void { + // Route every connector diagnostic through OMP's FILE logger, never the shared terminal: + // a raw stderr write corrupts the live TUI, and mesh reconnect churn would otherwise flood it. + const log: MeshLogger = (msg, level = "info") => { + const line = `[cotal-mesh] ${msg}`; + if (level === "error") pi.logger.error(line); + else if (level === "warn") pi.logger.warn(line); + else pi.logger.info(line); + }; + // No identity → a plain `omp`, not a launcher-joined session. Stay off the mesh. if (!hasIdentity()) { log("no COTAL_NAME / COTAL_LINK / COTAL_AGENT_FILE — staying off the mesh"); @@ -50,7 +56,7 @@ export default function cotalMesh(pi: ExtensionAPI): void { const config = configFromEnv(); config.connector = "oh-my-pi"; // advertise the host harness on our AgentCard (meta.connector) - const agent = new MeshAgent(config); + const agent = new MeshAgent(config, log); // Join the mesh only from a real interactive (top-level) session. A `task`/print/RPC subagent // inherits the parent's COTAL_* env, so hasIdentity() alone would make every subagent a stray diff --git a/package.json b/package.json index b83764ac..45b3a29e 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "smoke:presence-scrub": "tsx packages/core/smoke/presence-offline-scrub.smoke.ts", "smoke:cross-path-dedup": "tsx extensions/connector-core/smoke/cross-path-dedup.smoke.ts", "smoke:feedback": "tsx extensions/connector-core/smoke/feedback.smoke.ts", + "smoke:reconnect-log": "tsx extensions/connector-core/smoke/reconnect-log.smoke.ts", "smoke:opencode": "tsx extensions/connector-opencode/smoke/turn-wedge.smoke.ts", "smoke:opencode-coop": "tsx extensions/connector-opencode/smoke/cooperative-stop.smoke.ts", "smoke:opencode-transcript": "tsx extensions/connector-opencode/smoke/transcript-mirror.smoke.ts", diff --git a/packages/core/src/endpoint.ts b/packages/core/src/endpoint.ts index 5527c24e..2b55cd06 100644 --- a/packages/core/src/endpoint.ts +++ b/packages/core/src/endpoint.ts @@ -553,23 +553,25 @@ export class CotalEndpoint extends EventEmitter { } } - /** Rebuild with backoff until it sticks or we're stopped. Interruptible: a manual - * {@link reconnect} kicks the backoff so the next attempt runs immediately instead of - * awaiting the full retryMs. One loop at a time ({@link reestablishing}); concurrent - * triggers coalesce via {@link rebuild}. */ + /** Rebuild with exponential backoff until it sticks or we're stopped. The delay grows + * `retryMs · 2^attempt` capped at 30s, so a mesh that stays down is retried politely rather + * than every 3s. Interruptible: a manual {@link reconnect} kicks the backoff so the next + * attempt runs immediately (and resets the growth). One loop at a time ({@link reestablishing}); + * concurrent triggers coalesce via {@link rebuild}. */ private async reestablishLoop(): Promise { if (this.reestablishing) return; this.reestablishing = true; try { - while (!this.stopped) { + for (let attempt = 0; !this.stopped; attempt++) { try { await this.rebuild(); return; // success — re-armed; the supervisor re-triggers on the next terminal close } catch (e) { if (!this.stopped) this.emit("error", e as Error); + const delay = Math.min(30_000, this.retryMs * 2 ** attempt); await new Promise((resolve) => { this.backoffResolve = resolve; - this.backoffTimer = setTimeout(resolve, this.retryMs); + this.backoffTimer = setTimeout(resolve, delay); }); } } From 8175e2f10cb1961be94927b440fd28a4287190c2 Mon Sep 17 00:00:00 2001 From: seal Date: Wed, 8 Jul 2026 15:51:58 -0400 Subject: [PATCH 05/15] fix(connector-core): roll in InboxTurn ack-on-surface so the branch builds standalone loop.ts imports InboxTurn/InboxSource from @cotal-ai/connector-core, but the connector-core half (inbox-turn.ts + the ackInbox source method + the index export) was dropped when this branch was rebased onto upstream/main, leaving a dangling import that fails tsc. Roll those pieces in from the upstream feat/connector-pi work so the branch builds independently of that open PR while staying rebasable on upstream/main. Co-Authored-By: seal --- extensions/connector-core/inbox-turn.smoke.ts | 120 +++++++++++++++++ extensions/connector-core/src/agent.ts | 18 +++ extensions/connector-core/src/inbox-turn.ts | 122 ++++++++++++++++++ extensions/connector-core/src/index.ts | 1 + 4 files changed, 261 insertions(+) create mode 100644 extensions/connector-core/inbox-turn.smoke.ts create mode 100644 extensions/connector-core/src/inbox-turn.ts diff --git a/extensions/connector-core/inbox-turn.smoke.ts b/extensions/connector-core/inbox-turn.smoke.ts new file mode 100644 index 00000000..78bc3510 --- /dev/null +++ b/extensions/connector-core/inbox-turn.smoke.ts @@ -0,0 +1,120 @@ +/** + * Smoke test for the InboxTurn ack-on-surface helper (no NATS/LLM needed): drives it against + * a fake inbox — including the MAX_INBOX front-eviction the real MeshAgent does — and asserts + * the surface/ack invariants the embed adapters rely on. + * + * pnpm smoke:inbox + */ +import { InboxTurn, type InboxSource } from "./src/inbox-turn.js"; +import type { InboxItem } from "./src/agent.js"; + +function item(id: string, fromId: string, kind: InboxItem["kind"] = "dm"): InboxItem { + return { id, ts: 0, fromId, fromName: fromId, kind, mentionsMe: false, text: id }; +} + +/** A fake inbox that mirrors MeshAgent: ingest force-acks + evicts from the front past `cap`, + * drainInbox acks by position, ackInbox acks by id (no-op for an absent id). */ +class FakeInbox implements InboxSource { + items: InboxItem[] = []; + acked: InboxItem[] = []; + constructor(private cap = Infinity) {} + ingest(it: InboxItem): void { + this.items.push(it); + if (this.items.length > this.cap) { + for (const ev of this.items.splice(0, this.items.length - this.cap)) this.acked.push(ev); + } + } + peekInbox(): InboxItem[] { + return [...this.items]; + } + drainInbox(limit?: number): InboxItem[] { + const n = limit && limit > 0 ? Math.min(limit, this.items.length) : this.items.length; + const taken = this.items.splice(0, n); + this.acked.push(...taken); + return taken; + } + ackInbox(ids: string[]): InboxItem[] { + const wanted = new Set(ids); + const taken: InboxItem[] = []; + this.items = this.items.filter((p) => { + if (!wanted.has(p.id)) return true; + this.acked.push(p); + taken.push(p); + return false; + }); + return taken; + } +} + +function assert(cond: boolean, msg: string): void { + if (!cond) throw new Error(`FAIL: ${msg}`); +} + +const ids = (xs: InboxItem[]): string => xs.map((x) => x.id).join(","); +const sameScope = (a: InboxItem, b: InboxItem): boolean => + a.fromId === b.fromId && a.kind === b.kind; + +// 1) drop leading non-actionable, start on the front, commit acks exactly the origin +{ + const fake = new FakeInbox(); + fake.items = [item("echo", "self"), item("b", "alice")]; + const turn = new InboxTurn(fake); + turn.drop((i) => i.fromId === "self"); + assert(ids(fake.acked) === "echo", "drop ack-drops the self echo"); + assert(turn.start()?.id === "b", "start surfaces the front actionable"); + assert(turn.count === 1, "surfaced exactly the origin"); + turn.commit(); + assert(ids(fake.acked) === "echo,b", "commit acks the origin"); + assert(fake.items.length === 0 && !turn.inFlight, "inbox drained, turn idle"); +} + +// 2) extend folds the front-contiguous same-scope run, stops at a different-scope gap +{ + const fake = new FakeInbox(); + fake.items = [item("1", "alice"), item("2", "alice"), item("3", "bob"), item("4", "alice")]; + const turn = new InboxTurn(fake); + assert(turn.start()?.id === "1", "origin = 1"); + assert(ids(turn.extend(sameScope)) === "2", "folds only contiguous same-scope #2, stops at #3"); + assert(turn.count === 2, "surfaced the 2-message run"); + turn.commit(); + assert(ids(fake.acked) === "1,2", "commit acks exactly the surfaced run [1,2]"); + assert(ids(fake.items) === "3,4", "cross-scope #3 and gapped #4 stay on the stream"); +} + +// 3) abandon acks nothing — the surfaced run redelivers +{ + const fake = new FakeInbox(); + fake.items = [item("x", "alice")]; + const turn = new InboxTurn(fake); + turn.start(); + turn.abandon(); + assert(fake.acked.length === 0, "abandon acks nothing"); + assert(ids(fake.items) === "x" && !turn.inFlight, "item stays on the stream; turn idle"); +} + +// 4) 200+ ambient burst mid-turn: the overflow evicts the in-flight prefix from the front; +// ack-by-id no-ops the evicted origin, acks the surviving folded peer, and never touches +// the newer messages that took the prefix's place +{ + const fake = new FakeInbox(200); + fake.ingest(item("origin", "alice")); + const turn = new InboxTurn(fake); + assert(turn.start()?.id === "origin", "origin surfaced"); + fake.ingest(item("peer", "alice")); + assert(ids(turn.extend(sameScope)) === "peer", "folds the same-scope peer"); + for (let i = 0; i < 199; i++) fake.ingest(item(`amb${i}`, "bob", "channel")); // 201 → evict 1 + assert( + fake.acked.some((x) => x.id === "origin") && fake.items.some((x) => x.id === "peer"), + "overflow evicted+acked the origin; the folded peer survived", + ); + const before = fake.acked.length; + turn.commit(); // ackInbox(["origin","peer"]) + assert(fake.acked.length === before + 1, "commit acks only the survivor — evicted origin no-ops"); + assert(!fake.items.some((x) => x.id === "peer"), "the survivor was acked by id"); + assert( + fake.items.length === 199 && fake.items.every((x) => x.id.startsWith("amb")), + "all 199 newer ambient messages left untouched — none mis-acked", + ); +} + +console.log("INBOX-TURN SMOKE OK ✅"); diff --git a/extensions/connector-core/src/agent.ts b/extensions/connector-core/src/agent.ts index e2a1cba9..dc753f46 100644 --- a/extensions/connector-core/src/agent.ts +++ b/extensions/connector-core/src/agent.ts @@ -349,6 +349,24 @@ export class MeshAgent extends EventEmitter { return taken.map((p) => p.item); } + /** Ack + remove the buffered messages with these ids, wherever they sit in the inbox. An id + * that's no longer buffered — already acked, e.g. force-evicted by a MAX_INBOX overflow — is a + * harmless no-op. This lets a consumer ack exactly the messages it surfaced, immune to the + * front-eviction that shifts positions out from under {@link drainInbox}. */ + ackInbox(ids: string[]): InboxItem[] { + if (!ids.length) return []; + const wanted = new Set(ids); + const taken: InboxItem[] = []; + this.inbox = this.inbox.filter((p) => { + if (!wanted.has(p.item.id)) return true; + p.ack(); + this.markHandled(p.item.id); + taken.push(p.item); + return false; + }); + return taken; + } + /** Record an id as surfaced/handled, for {@link ingest}'s commit-aware cross-path dedup. Bounded via * two rotating windows: when the live set fills, it becomes the previous window and a fresh one * starts — so memory stays ~2× the cap while the lookup horizon never shrinks below it. */ diff --git a/extensions/connector-core/src/inbox-turn.ts b/extensions/connector-core/src/inbox-turn.ts new file mode 100644 index 00000000..771da05e --- /dev/null +++ b/extensions/connector-core/src/inbox-turn.ts @@ -0,0 +1,122 @@ +import type { InboxItem } from "./agent.js"; + +/** The slice of {@link MeshAgent} an {@link InboxTurn} drives off of. */ +export interface InboxSource { + /** Buffered messages, oldest first, without acking. */ + peekInbox(): InboxItem[]; + /** Ack + remove the front `limit` messages and return them. */ + drainInbox(limit?: number): InboxItem[]; + /** Ack + remove the messages with these ids (any position); an absent id is a no-op. */ + ackInbox(ids: string[]): InboxItem[]; +} + +/** + * Ack-on-surface delivery off a {@link MeshAgent}'s stream-backed inbox. + * + * The inbox is the single source of truth — there is no parallel buffer to drift out of + * sync. A turn *surfaces* messages by id and acks them (via `ackInbox`) only once the turn + * COMPLETES; a crash or interrupt before {@link commit} leaves them on the stream, so they + * redeliver — nothing is acked merely by being read. Acking by id (rather than by front + * position) keeps it correct even when the `MAX_INBOX` overflow force-evicts the in-flight + * prefix from the front mid-turn: those ids are already gone, so the ack no-ops them and + * never touches the newer messages that took their place. + * + * Fits both shapes the embed adapters use: + * - a one-message serialize loop: `start()` → run → `commit()`; + * - pi's same-scope steer loop: `start()` → `extend(match)`* (fold contiguous peers, e.g. + * same-scope messages, as they stream in) → `commit()` on a clean/failed finish, or + * `abandon()` on interrupt. + * + * `cotal_inbox` (where exposed) must stay on `peekInbox` so a model call can't double-drain + * what the loop already surfaced. + */ +export class InboxTurn { + private surfacedIds: string[] = []; + private _origin?: InboxItem; + + constructor(private readonly source: InboxSource) {} + + /** True while a turn holds surfaced-but-unacked messages. */ + get inFlight(): boolean { + return this.surfacedIds.length > 0; + } + + /** The message that opened the current turn (its reply scope), or undefined when idle. */ + get origin(): InboxItem | undefined { + return this._origin; + } + + /** How many messages this turn has surfaced. */ + get count(): number { + return this.surfacedIds.length; + } + + /** + * Ack-drop the leading messages matching `skip` (own echoes, ambient chatter) so they + * neither block the front nor linger to the inbox cap. Only valid with no turn in flight + * (a between-turns, synchronous front trim — no eviction can interleave). + */ + drop(skip: (item: InboxItem) => boolean): void { + if (this.surfacedIds.length) return; + const pending = this.source.peekInbox(); + let n = 0; + while (n < pending.length && skip(pending[n])) n++; + if (n) this.source.drainInbox(n); + } + + /** + * Open a turn on the front message (its origin) and surface it. Returns the origin, or + * undefined if the inbox is empty. Idempotent while a turn is in flight (returns the + * current origin). Call {@link drop} first so the front is the message you mean to answer. + */ + start(): InboxItem | undefined { + if (this.surfacedIds.length) return this._origin; + const front = this.source.peekInbox()[0]; + if (!front) return undefined; + this._origin = front; + this.surfacedIds = [front.id]; + return front; + } + + /** + * Fold the front-contiguous run of not-yet-surfaced messages that `match(item, origin)` + * into this turn, stopping at the first unsurfaced non-match (so a cross-scope message is + * left to open its own turn, preserving FIFO + scope isolation). Already-surfaced messages + * are skipped, so an overflow that evicts part of the prefix can't desync this. Returns the + * newly surfaced messages for the caller to feed in (e.g. via steer). No-op until + * {@link start}. + */ + extend(match: (item: InboxItem, origin: InboxItem) => boolean): InboxItem[] { + if (!this._origin) return []; + const surfaced = new Set(this.surfacedIds); + const run: InboxItem[] = []; + for (const item of this.source.peekInbox()) { + if (surfaced.has(item.id)) continue; // already surfaced (still buffered) — skip + if (!match(item, this._origin)) break; // first unsurfaced non-match → stop at the gap + run.push(item); + this.surfacedIds.push(item.id); + } + return run; + } + + /** + * Ack the surfaced messages by id — the sole ack site. Call on a terminal status that + * should consume them: a clean finish, or a failed/dropped turn (drop, no retry-loop). Ids + * already evicted by the overflow no-op. Do NOT call on interrupt/crash — use + * {@link abandon} so the run redelivers. + */ + commit(): void { + if (this.surfacedIds.length) this.source.ackInbox(this.surfacedIds); + this.reset(); + } + + /** End the turn without acking — the surfaced run stays on the stream and redelivers. */ + abandon(): void { + this.reset(); + } + + private reset(): void { + this.surfacedIds = []; + this._origin = undefined; + } +} diff --git a/extensions/connector-core/src/index.ts b/extensions/connector-core/src/index.ts index ee15298e..0db4edf5 100644 --- a/extensions/connector-core/src/index.ts +++ b/extensions/connector-core/src/index.ts @@ -1,5 +1,6 @@ export * from "./config.js"; export * from "./agent.js"; +export * from "./inbox-turn.js"; export * from "./runtime.js"; export * from "./launch.js"; export * from "./tool-specs.js"; From 9557f4911ce970e338eebf3c8eebe7b8bbe597a1 Mon Sep 17 00:00:00 2001 From: seal Date: Wed, 8 Jul 2026 16:11:25 -0400 Subject: [PATCH 06/15] feat(oh-my-pi): track pi-coding-agent 16.3.12, migrate tools to zod MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pi-coding-agent 16.3.7 changed the tool-registry typing so the legacy TypeBox `defineTool`/`Type` shim no longer infers params (execute's `params` fell to `unknown`; the result narrowed to `ToolDefinition` and broke `customTools` variance), and `registerTool` began recursing into `Static` on inline zod literals (TS2589 excessively-deep). Move the peer's cotal_roster/cotal_status off the retired shim to zod schemas (the SDK's canonical param format — `Static` infers `z.infer` first), and pin `registerTool>` in the extension so the registry generic no longer deep-infers. Drop the `details: {}` literals that narrowed TDetails. Bump the dep to ^16.3.12 (latest); the whole build + all connector smokes are green. Co-Authored-By: seal --- extensions/connector-oh-my-pi/package.json | 6 ++-- .../connector-oh-my-pi/src/extension.ts | 10 ++++-- extensions/connector-oh-my-pi/src/peer.ts | 35 ++++++++++--------- 3 files changed, 29 insertions(+), 22 deletions(-) diff --git a/extensions/connector-oh-my-pi/package.json b/extensions/connector-oh-my-pi/package.json index 4de1992b..1d8fd64c 100644 --- a/extensions/connector-oh-my-pi/package.json +++ b/extensions/connector-oh-my-pi/package.json @@ -13,7 +13,9 @@ } }, "omp": { - "extensions": ["./dist/extension.bundle.js"] + "extensions": [ + "./dist/extension.bundle.js" + ] }, "engines": { "node": ">=22.19.0" @@ -27,7 +29,7 @@ "prepublishOnly": "pnpm run build" }, "dependencies": { - "@oh-my-pi/pi-coding-agent": "^16.2.6", + "@oh-my-pi/pi-coding-agent": "^16.3.12", "@cotal-ai/connector-core": "workspace:*", "tsx": "^4.22.4" }, diff --git a/extensions/connector-oh-my-pi/src/extension.ts b/extensions/connector-oh-my-pi/src/extension.ts index 3154a522..6d3fce5a 100644 --- a/extensions/connector-oh-my-pi/src/extension.ts +++ b/extensions/connector-oh-my-pi/src/extension.ts @@ -133,12 +133,16 @@ function registerSpec( }); if (spec.name === "cotal_inbox") { - pi.registerTool({ + // Empty params (this tool takes none). The explicit `registerTool<…>` generic below pins + // `TParams` so the tool registry doesn't infer it from the literal and recurse into + // `Static` (TS2589, excessively deep) under pi-coding-agent ≥16.3.7. + const parameters = z.object({}); + pi.registerTool>({ name: spec.name, label: spec.title, description: "Show the peer messages currently waiting for you (incl. focus-mode recall). You don't normally need this — the extension delivers peer messages into your turns automatically; use it to re-check what's pending mid-task. Read-only: it never consumes them.", - parameters: z.object({}), + parameters, approval: "read", async execute(_id, _params, _signal, _onUpdate, _ctx: ExtensionContext) { return toResult(await spec.run(agent, config, { peek: true })); @@ -151,7 +155,7 @@ function registerSpec( // host's injected zod (pi.zod) so the schema type matches OMP's tool registry. The cast bridges the // two structurally-identical zod copies at this single boundary (raw shapes are plain objects). const parameters = z.object((spec.schema ?? {}) as Parameters[0]); - pi.registerTool({ + pi.registerTool>({ name: spec.name, label: spec.title, description: spec.description, diff --git a/extensions/connector-oh-my-pi/src/peer.ts b/extensions/connector-oh-my-pi/src/peer.ts index 0c880742..1c195033 100644 --- a/extensions/connector-oh-my-pi/src/peer.ts +++ b/extensions/connector-oh-my-pi/src/peer.ts @@ -7,11 +7,8 @@ import { MeshAgent, configFromEnv } from "@cotal-ai/connector-core"; // upstream type-build fix (can1357/oh-my-pi) is published. import { createAgentSession } from "@oh-my-pi/pi-coding-agent/sdk"; import { SessionManager } from "@oh-my-pi/pi-coding-agent/session/session-manager"; -import { - defineTool, - Type, -} from "@oh-my-pi/pi-coding-agent/extensibility/legacy-pi-coding-agent-shim"; -import type { Static } from "@oh-my-pi/pi-coding-agent/extensibility/typebox"; +import type { ToolDefinition } from "@oh-my-pi/pi-coding-agent/extensibility/extensions/types"; +import { z } from "zod"; import { runPeerLoop } from "./loop.js"; /** @@ -19,13 +16,17 @@ import { runPeerLoop } from "./loop.js"; * the agent's final text on the right delivery mode (see runOmpPeer), so the model can't * mis-route or duplicate a reply. These just let it see who is present and report its own * status. Mirrors the pi / openai-agents / vercel-ai adapters. + * + * Params are authored in zod (the SDK's canonical schema — `Static` infers `z.infer` first), + * so `execute`'s `params` is typed off the schema and the tool satisfies `ToolDefinition` + * without the retired TypeBox `defineTool` shim. */ -function buildTools(mesh: MeshAgent) { - const cotal_roster = defineTool({ +function buildTools(mesh: MeshAgent): ToolDefinition[] { + const cotal_roster: ToolDefinition>> = { name: "cotal_roster", label: "Cotal roster", description: "List the peers currently present on the Cotal mesh.", - parameters: Type.Object({}), + parameters: z.object({}), execute: async () => { const peers = mesh.roster(); const text = peers.length @@ -33,24 +34,24 @@ function buildTools(mesh: MeshAgent) { .map((p) => `${p.card.name}${p.card.role ? `/${p.card.role}` : ""} [${p.status}]`) .join("\n") : "roster is empty"; - return { content: [{ type: "text", text }], details: {} }; + return { content: [{ type: "text", text }] }; }, - }); + }; - const statusParams = Type.Object({ - status: Type.Union([Type.Literal("idle"), Type.Literal("waiting"), Type.Literal("working")]), - activity: Type.Optional(Type.String()), + const statusParams = z.object({ + status: z.enum(["idle", "waiting", "working"]), + activity: z.string().optional(), }); - const cotal_status = defineTool({ + const cotal_status: ToolDefinition = { name: "cotal_status", label: "Cotal status", description: "Update this peer's presence status on the mesh.", parameters: statusParams, - execute: async (_id: string, params: Static) => { + execute: async (_id, params) => { await mesh.setStatus(params.status, params.activity); - return { content: [{ type: "text", text: `status set to ${params.status}` }], details: {} }; + return { content: [{ type: "text", text: `status set to ${params.status}` }] }; }, - }); + }; return [cotal_roster, cotal_status]; } From 3516909ab56137d5d8eb1c78cb3e7f3e02fbf923 Mon Sep 17 00:00:00 2001 From: seal Date: Wed, 8 Jul 2026 17:54:38 -0400 Subject: [PATCH 07/15] =?UTF-8?q?fix(connector):=20address=20#5=20review?= =?UTF-8?q?=20findings=20=E2=80=94=20delivery,=20shutdown,=20CI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix two P1 `loop.ts` delivery bugs (a declined `prompt()` no longer wedges the peer; a rejected `steer()` un-surfaces its id via new `InboxTurn.unsurface` so it redelivers instead of being falsely acked), make the connect-retry backoff and peer shutdown cancellable/idempotent, `await session.dispose()`, and wire the three hermetic oh-my-pi smokes into `pnpm -r test`. Adds regression tests 7-8 for the P1s. Co-Authored-By: seal --- .../smoke/reconnect-log.smoke.ts | 2 +- extensions/connector-core/src/agent.ts | 21 ++++++-- extensions/connector-core/src/inbox-turn.ts | 11 ++++ .../interactive-loop.smoke.ts | 7 +-- .../oh-my-pi-extension.smoke.ts | 3 ++ .../connector-oh-my-pi/oh-my-pi-peer.smoke.ts | 54 ++++++++++++++++++- extensions/connector-oh-my-pi/package.json | 2 + extensions/connector-oh-my-pi/src/loop.ts | 19 +++++-- extensions/connector-oh-my-pi/src/peer.ts | 3 ++ 9 files changed, 110 insertions(+), 12 deletions(-) diff --git a/extensions/connector-core/smoke/reconnect-log.smoke.ts b/extensions/connector-core/smoke/reconnect-log.smoke.ts index 642dba43..eb0b860f 100644 --- a/extensions/connector-core/smoke/reconnect-log.smoke.ts +++ b/extensions/connector-core/smoke/reconnect-log.smoke.ts @@ -30,7 +30,7 @@ const cfg: AgentConfig = { }; const lines: { msg: string; level: MeshLogLevel }[] = []; -const agent = new MeshAgent(cfg, (msg, level) => lines.push({ msg, level })); +const agent = new MeshAgent(cfg, (msg, level) => lines.push({ msg, level: level ?? "info" })); const endpointErrors = () => lines.filter((l) => l.msg.includes("endpoint error")); // Guard: with a logger injected, NOTHING may reach the shared terminal. diff --git a/extensions/connector-core/src/agent.ts b/extensions/connector-core/src/agent.ts index dc753f46..6470f2ba 100644 --- a/extensions/connector-core/src/agent.ts +++ b/extensions/connector-core/src/agent.ts @@ -83,8 +83,8 @@ const MAX_RETRY_MS = 30_000; /** Default diagnostics sink: one prefixed line per call to stderr. Correct for the out-of-process * connectors (Claude Code MCP, OpenCode, Hermes) that own their own stderr; the in-process * oh-my-pi extension injects a file logger instead so mesh churn can't corrupt the TUI. */ -function defaultLogger(msg: string, _level?: MeshLogLevel): void { - process.stderr.write(`[cotal-connector] ${msg}\n`); +function defaultLogger(msg: string, level: MeshLogLevel = "info"): void { + process.stderr.write(`[cotal-connector:${level}] ${msg}\n`); } function sleep(ms: number): Promise { @@ -133,6 +133,11 @@ export class MeshAgent extends EventEmitter { * published after it ("since you entered focus"). Undefined unless in focus. */ private focusSince?: number; private stopping = false; + /** In-flight connect-retry backoff, so {@link stop} can interrupt it instead of leaking the + * event loop for up to MAX_RETRY_MS when the mesh is unreachable at shutdown. Mirrors the + * endpoint's kickBackoff. */ + private retryTimer?: ReturnType; + private retryResolve?: () => void; /** Diagnostics sink. Defaults to one prefixed stderr line per call; an in-process host injects * a file logger so mesh churn never touches the shared terminal. */ private readonly logger: MeshLogger; @@ -222,7 +227,14 @@ export class MeshAgent extends EventEmitter { this._observedConnected = false; this.log(`mesh unreachable (${(e as Error).message}); retrying in the background`, "warn"); } - await sleep(delay); + // Cancellable backoff: stop() clears the timer + resolves this so shutdown isn't blocked + // for up to MAX_RETRY_MS on an unreachable mesh (the loop then exits on `this.stopping`). + await new Promise((resolve) => { + this.retryResolve = resolve; + this.retryTimer = setTimeout(resolve, delay); + }); + this.retryTimer = undefined; + this.retryResolve = undefined; delay = Math.min(delay * 2, MAX_RETRY_MS); } } @@ -230,6 +242,9 @@ export class MeshAgent extends EventEmitter { async stop(): Promise { this.stopping = true; + // Interrupt any in-flight connect-retry backoff so shutdown doesn't wait out the timer. + clearTimeout(this.retryTimer); + this.retryResolve?.(); // Unconditional: a background self-heal can flip _connected without us, so a `_connected` // guard could skip the stop and leak the live connection/heartbeat/supervisor. ep.stop() is // idempotent (early-returns once stopped), so calling it when already-down is a noop. diff --git a/extensions/connector-core/src/inbox-turn.ts b/extensions/connector-core/src/inbox-turn.ts index 771da05e..f2df0bcf 100644 --- a/extensions/connector-core/src/inbox-turn.ts +++ b/extensions/connector-core/src/inbox-turn.ts @@ -99,6 +99,17 @@ export class InboxTurn { return run; } + /** + * Un-surface a single id that this turn surfaced but could not deliver (e.g. a `steer()` + * the session rejected). It drops off the ack set so {@link commit} won't consume it — the + * message stays on the stream and redelivers on a later turn. A no-op for an id this turn + * never surfaced (or one already evicted by overflow). + */ + unsurface(id: string): void { + const i = this.surfacedIds.indexOf(id); + if (i !== -1) this.surfacedIds.splice(i, 1); + } + /** * Ack the surfaced messages by id — the sole ack site. Call on a terminal status that * should consume them: a clean finish, or a failed/dropped turn (drop, no retry-loop). Ids diff --git a/extensions/connector-oh-my-pi/interactive-loop.smoke.ts b/extensions/connector-oh-my-pi/interactive-loop.smoke.ts index 985990e3..33248812 100644 --- a/extensions/connector-oh-my-pi/interactive-loop.smoke.ts +++ b/extensions/connector-oh-my-pi/interactive-loop.smoke.ts @@ -1,11 +1,12 @@ /** - * Behavioral smoke for the cotal-mesh delivery loop (`runPeerLoop` in loop.ts). Repo style: plain - * assert + console.log, run via `bun loop.smoke.ts`, non-zero exit on failure. No test framework. + * Behavioral smoke for the cotal-mesh delivery loop (`runPeerLoop` in interactive-loop.ts). Repo + * style: plain assert + console.log, run via `bun interactive-loop.smoke.ts`, non-zero exit on + * failure. No test framework. * * Drives the loop with a structural FakeMesh (captures the on(...) handlers so the test can emit * incoming/mention-wake/wake, array-backed peek/drain inbox, controllable attention/channelMode/ * pendingWake, recording setStatus/stop) and a fake host recording sendMessage. The 8 documented - * invariants of loop.ts are the spec; each is asserted below. + * invariants of interactive-loop.ts are the spec; each is asserted below. */ import { formatInjection, ORIENTATION_BOOTSTRAP, type InboxItem } from "@cotal-ai/connector-core"; import type { AttentionMode, ChannelMode, PresenceStatus } from "@cotal-ai/core"; diff --git a/extensions/connector-oh-my-pi/oh-my-pi-extension.smoke.ts b/extensions/connector-oh-my-pi/oh-my-pi-extension.smoke.ts index 829f9602..b9148b22 100644 --- a/extensions/connector-oh-my-pi/oh-my-pi-extension.smoke.ts +++ b/extensions/connector-oh-my-pi/oh-my-pi-extension.smoke.ts @@ -81,6 +81,9 @@ process.env.COTAL_SERVERS = "nats://127.0.0.1:4222"; // never actually connected const inbox = tools.get("cotal_inbox")!; assert(inbox.approval === "read", "cotal_inbox is approval:read"); // Its schema takes no args (peek is forced), so execute must run without throwing on {}. + const inboxResult = await inbox.execute("", {}, undefined, undefined, undefined); + assert(inboxResult.content.length === 1, "cotal_inbox execute returns one content part"); + assert(!inboxResult.content[0].text.startsWith("⚠"), "cotal_inbox execute does not error on empty inbox"); console.log("3) cotal_inbox read-only OK ✅"); // The factory started a MeshAgent with a background reconnect loop; fire session_shutdown to stop diff --git a/extensions/connector-oh-my-pi/oh-my-pi-peer.smoke.ts b/extensions/connector-oh-my-pi/oh-my-pi-peer.smoke.ts index a021c60c..3cbafbe1 100644 --- a/extensions/connector-oh-my-pi/oh-my-pi-peer.smoke.ts +++ b/extensions/connector-oh-my-pi/oh-my-pi-peer.smoke.ts @@ -120,6 +120,12 @@ class StubSession implements PeerSession { steers: string[] = []; aborted = 0; disposed = 0; + /** Opt-in: value `prompt()` resolves to. Default `true` (session accepts the wake). Set + * `false` to simulate a DECLINED wake (no agent_start/agent_end follows). */ + promptResult = true; + /** Opt-in: when `true`, `steer()` records then REJECTS (the fold couldn't reach the model + * turn). Default `false` (records + resolves as before). */ + steerReject = false; private listeners: ((event: AgentSessionEvent) => void)[] = []; subscribe(listener: (event: AgentSessionEvent) => void): () => void { @@ -131,10 +137,14 @@ class StubSession implements PeerSession { } async prompt(text: string): Promise { this.prompts.push(text); - return true; + return this.promptResult; } - async steer(text: string): Promise { + steer(text: string): Promise { this.steers.push(text); + // A non-async return so a rejected steer settles in ONE microtask (an `async` method would + // adopt the thenable and take extra ticks) — the loop's `.catch → unsurface` then runs after + // a single `await Promise.resolve()`, matching how the real session rejects. + return this.steerReject ? Promise.reject(new Error("steer rejected")) : Promise.resolve(); } async abort(): Promise { this.aborted++; @@ -279,4 +289,44 @@ console.log("5) presence mapping OK ✅"); } console.log("6) agent_end terminal without willRetry OK ✅"); +// 7) declined prompt completes the turn (no wedge): prompt() resolving false means the session +// DECLINED the wake — no agent_start/agent_end follows. The turn must still complete (ack the +// origin, go idle) and the NEXT message must pump; pre-fix the origin was never acked and the +// peer wedged with an in-flight-but-never-streaming turn. +{ + const mesh = new FakeMesh(); + const session = new StubSession(); + mesh.items = [dm("d1", "alice", "q")]; + session.promptResult = false; // the session declines the wake + runPeerLoop({ mesh, session }); // pump fires → prompt called → resolves false, no START emitted + await Promise.resolve(); // let the prompt().then handler run on its microtask + assert(session.prompts.length === 1, "the declined origin was prompted exactly once"); + assert(ids(mesh.acked) === "d1", "declined origin committed (acked, drop/no-retry) — not wedged"); + assert(last(mesh.statuses).status === "idle", "the peer went idle after the decline"); + mesh.arrive(dm("d2", "bob", "q2")); // a fresh message must pump — the peer is not wedged + await Promise.resolve(); + assert(session.prompts[1] === framed(dm("d2", "bob", "q2")), "a fresh turn pumped after the decline"); +} +console.log("7) declined prompt completes the turn (no wedge) OK ✅"); + +// 8) rejected steer un-surfaces its message (redelivers, not lost): a folded same-scope message +// is surfaced then steered; if steer() REJECTS the message never reached the model turn, so the +// terminal commit must NOT ack it — it stays on the stream and redelivers. Pre-fix commit acked +// the undelivered fold (acked === "a1,a2") and the message was lost from the stream. +{ + const mesh = new FakeMesh(); + const session = new StubSession(); + mesh.items = [dm("a1", "alice", "q1")]; + session.steerReject = true; // the fold's steer will reject (never reaches the model turn) + runPeerLoop({ mesh, session }); + session.emit(START); // turn live, streaming + mesh.arrive(dm("a2", "alice", "q2")); // same scope → foldSameScope → extend surfaces a2, steer rejects + await Promise.resolve(); // let the steer().catch → turn.unsurface(a2) run + session.emit(end("ans")); // terminal → commit acks the surfaced run + assert(ids(mesh.acked) === "a1", "only the delivered origin a1 acked; the rejected fold a2 is not"); + assert(mesh.items.some((x) => x.id === "a2"), "a2 stays on the stream (redelivers, not lost)"); + assert(session.steers.length === 1, "the fold attempted the steer exactly once"); +} +console.log("8) rejected steer un-surfaces its message (redelivers) OK ✅"); + console.log("OH-MY-PI PEER SMOKE OK ✅"); diff --git a/extensions/connector-oh-my-pi/package.json b/extensions/connector-oh-my-pi/package.json index 1d8fd64c..33686176 100644 --- a/extensions/connector-oh-my-pi/package.json +++ b/extensions/connector-oh-my-pi/package.json @@ -24,8 +24,10 @@ "typecheck": "tsc -p tsconfig.json --noEmit", "build": "tsc -p tsconfig.json && pnpm run bundle", "bundle": "esbuild src/extension.ts --bundle --platform=node --format=esm --target=node20 --outfile=dist/extension.bundle.js --external:@oh-my-pi/*", + "smoke:peer": "tsx oh-my-pi-peer.smoke.ts", "smoke:extension": "tsx oh-my-pi-extension.smoke.ts", "smoke:interactive-loop": "tsx interactive-loop.smoke.ts", + "test": "pnpm run smoke:peer && pnpm run smoke:extension && pnpm run smoke:interactive-loop", "prepublishOnly": "pnpm run build" }, "dependencies": { diff --git a/extensions/connector-oh-my-pi/src/loop.ts b/extensions/connector-oh-my-pi/src/loop.ts index a9183f75..b7610fa0 100644 --- a/extensions/connector-oh-my-pi/src/loop.ts +++ b/extensions/connector-oh-my-pi/src/loop.ts @@ -122,7 +122,15 @@ export function runPeerLoop({ mesh, session }: { mesh: PeerMesh; session: PeerSe return; } streaming = false; - void session.prompt(framed(origin)).catch(onStartError); // wake into a fresh turn + // prompt() resolves false when the session DECLINES the wake (no agent_start/agent_end will + // follow). Treat that like a pre-flight failure: complete the turn so the peer doesn't wedge + // with an in-flight-but-never-streaming origin. A true pre-flight throw routes the same way. + session.prompt(framed(origin)).then( + (accepted) => { + if (!accepted && !streaming) onStartError(new Error("session declined the prompt")); + }, + onStartError, + ); } /** Fold any front-contiguous, same-scope actionable messages into the live turn (mid-turn @@ -130,7 +138,12 @@ export function runPeerLoop({ mesh, session }: { mesh: PeerMesh; session: PeerSe function foldSameScope(): void { if (!turn.origin || !streaming) return; for (const item of turn.extend((i, o) => actionable(mesh, i) && scopeKey(i) === scopeKey(o))) { - void session.steer(framed(item)).catch(log); + // If the steer is rejected the message never reached the model turn — un-surface its id so + // the terminal commit() won't ack it. It stays on the stream and redelivers on a later turn. + void session.steer(framed(item)).catch((e) => { + log(e); + turn.unsurface(item.id); + }); } } @@ -186,7 +199,7 @@ export function runPeerLoop({ mesh, session }: { mesh: PeerMesh; session: PeerSe turn.abandon(); // leave the in-flight run on the stream → redeliver, no peer dropped await session.abort(); } - void session.dispose(); + await session.dispose(); }, }; } diff --git a/extensions/connector-oh-my-pi/src/peer.ts b/extensions/connector-oh-my-pi/src/peer.ts index 1c195033..7df379c3 100644 --- a/extensions/connector-oh-my-pi/src/peer.ts +++ b/extensions/connector-oh-my-pi/src/peer.ts @@ -81,7 +81,10 @@ export async function runOmpPeer(): Promise { const loop = runPeerLoop({ mesh, session }); + let shuttingDown = false; async function shutdown(): Promise { + if (shuttingDown) return; // a second signal during teardown must not re-abort/-dispose/-stop + shuttingDown = true; try { await loop.shutdown(); await mesh.stop(); From 4820cd44159cf467cc84e9e5b2f2cd7b56e5f83b Mon Sep 17 00:00:00 2001 From: seal Date: Wed, 8 Jul 2026 18:40:18 -0400 Subject: [PATCH 08/15] fix(connector): close 3 shutdown/steer races found reviewing the fix The review of the prior commit surfaced real races in it: a folded `steer()` that rejects after `agent_end` acked the turn still lost the message; a `prompt()` settling during shutdown drove a disposed session; and awaiting `dispose()` let its failure skip the caller's `mesh.stop()`. Track pending steers and un-surface any unconfirmed at `agent_end`, guard the prompt callback with a `stopped` flag, and `dispose().catch(log)`. Peer smoke gates the async callbacks on a macrotask drain so the assertions aren't hop-count fragile. Co-Authored-By: seal --- .../connector-oh-my-pi/oh-my-pi-peer.smoke.ts | 15 +++++++-- extensions/connector-oh-my-pi/src/loop.ts | 33 +++++++++++++++---- 2 files changed, 38 insertions(+), 10 deletions(-) diff --git a/extensions/connector-oh-my-pi/oh-my-pi-peer.smoke.ts b/extensions/connector-oh-my-pi/oh-my-pi-peer.smoke.ts index 3cbafbe1..52b73b98 100644 --- a/extensions/connector-oh-my-pi/oh-my-pi-peer.smoke.ts +++ b/extensions/connector-oh-my-pi/oh-my-pi-peer.smoke.ts @@ -16,6 +16,14 @@ function assert(cond: boolean, msg: string): void { if (!cond) throw new Error(`FAIL: ${msg}`); } +/** Drain all pending microtasks (a macrotask tick) so an async prompt/steer callback chain has + * settled before we assert — hop-count-independent, unlike a single `await Promise.resolve()`. */ +const drain = (): Promise => { + const { promise, resolve } = Promise.withResolvers(); + setTimeout(resolve, 0); + return promise; +}; + // --- message factories ------------------------------------------------------------------- function dm(id: string, fromId: string, text = id): InboxItem { return { id, ts: 0, fromId, fromName: fromId, kind: "dm", mentionsMe: false, historical: false, text }; @@ -213,6 +221,7 @@ console.log("2) actionable filter OK ✅"); assert(session.steers.length === 1 && session.steers[0] === framed(dm("a2", "alice", "q2")), "same-scope peer folded via steer; cross-scope not folded"); assert(mesh.items.some((x) => x.id === "b1"), "cross-scope message stays on the stream"); + await Promise.resolve(); // let the folded steer's .then (pendingSteerIds.delete) confirm before commit session.emit(end("ans")); // terminal → commit alice run, deliver, pump next scope assert(ids(mesh.acked) === "a1,a2", "the surfaced same-scope run [a1,a2] was acked on end"); assert(mesh.dms.length === 1 && mesh.dms[0].target === "alice", "one reply delivered to the shared scope"); @@ -299,12 +308,12 @@ console.log("6) agent_end terminal without willRetry OK ✅"); mesh.items = [dm("d1", "alice", "q")]; session.promptResult = false; // the session declines the wake runPeerLoop({ mesh, session }); // pump fires → prompt called → resolves false, no START emitted - await Promise.resolve(); // let the prompt().then handler run on its microtask + await drain(); // let the async prompt().then chain settle (2 hops) before asserting assert(session.prompts.length === 1, "the declined origin was prompted exactly once"); assert(ids(mesh.acked) === "d1", "declined origin committed (acked, drop/no-retry) — not wedged"); assert(last(mesh.statuses).status === "idle", "the peer went idle after the decline"); mesh.arrive(dm("d2", "bob", "q2")); // a fresh message must pump — the peer is not wedged - await Promise.resolve(); + await drain(); assert(session.prompts[1] === framed(dm("d2", "bob", "q2")), "a fresh turn pumped after the decline"); } console.log("7) declined prompt completes the turn (no wedge) OK ✅"); @@ -321,7 +330,7 @@ console.log("7) declined prompt completes the turn (no wedge) OK ✅"); runPeerLoop({ mesh, session }); session.emit(START); // turn live, streaming mesh.arrive(dm("a2", "alice", "q2")); // same scope → foldSameScope → extend surfaces a2, steer rejects - await Promise.resolve(); // let the steer().catch → turn.unsurface(a2) run + await drain(); // let the steer().catch → turn.unsurface(a2) run session.emit(end("ans")); // terminal → commit acks the surfaced run assert(ids(mesh.acked) === "a1", "only the delivered origin a1 acked; the rejected fold a2 is not"); assert(mesh.items.some((x) => x.id === "a2"), "a2 stays on the stream (redelivers, not lost)"); diff --git a/extensions/connector-oh-my-pi/src/loop.ts b/extensions/connector-oh-my-pi/src/loop.ts index b7610fa0..95b34d3d 100644 --- a/extensions/connector-oh-my-pi/src/loop.ts +++ b/extensions/connector-oh-my-pi/src/loop.ts @@ -98,6 +98,13 @@ function turnReplyText(messages: readonly unknown[]): string | undefined { export function runPeerLoop({ mesh, session }: { mesh: PeerMesh; session: PeerSession }): PeerLoop { const turn = new InboxTurn(mesh); let streaming = false; // gates steer(): only valid once the agent is actually streaming + // Set once shutdown() begins so a prompt/steer settling mid-teardown can't drive a disposed + // session (commit/pump/setStatus on a torn-down loop). Guards the async prompt callback below. + let stopped = false; + // Folded ids whose steer() has not yet CONFIRMED delivery. agent_end un-surfaces any still + // pending (or failed) fold before commit, so a message the model never received is left on the + // stream to redeliver rather than falsely acked. + const pendingSteerIds = new Set(); const setStatus = (status: "idle" | "working", activity?: string): void => { void mesh.setStatus(status, activity).catch(() => {}); @@ -138,17 +145,24 @@ export function runPeerLoop({ mesh, session }: { mesh: PeerMesh; session: PeerSe function foldSameScope(): void { if (!turn.origin || !streaming) return; for (const item of turn.extend((i, o) => actionable(mesh, i) && scopeKey(i) === scopeKey(o))) { - // If the steer is rejected the message never reached the model turn — un-surface its id so - // the terminal commit() won't ack it. It stays on the stream and redelivers on a later turn. - void session.steer(framed(item)).catch((e) => { - log(e); - turn.unsurface(item.id); - }); + // extend() surfaces synchronously so a re-entrant fold can't re-pick the item, but the steer + // is async: hold the id as pending until it CONFIRMS. A steer still pending (or rejected) at + // agent_end is un-surfaced there, so a message the model never received redelivers, not acked. + pendingSteerIds.add(item.id); + void session.steer(framed(item)).then( + () => pendingSteerIds.delete(item.id), // delivered → stays surfaced → acked on commit + (e) => { + log(e); + pendingSteerIds.delete(item.id); + turn.unsurface(item.id); // rejected → drop from the ack set → redelivers on a later turn + }, + ); } } function onStartError(e: unknown): void { log(e); + if (stopped) return; // torn down mid-flight → never commit/pump/status a disposed session if (streaming) return; // already running → agent_end will complete the turn turn.commit(); // pre-flight failure (e.g. no model/key): drop, no retry-loop setStatus("idle"); @@ -181,6 +195,10 @@ export function runPeerLoop({ mesh, session }: { mesh: PeerMesh; session: PeerSe // event and the failed turn still ends here, so `agent_end` is always terminal. const to = turn.origin; const reply = turnReplyText(event.messages); + // A fold whose steer hasn't confirmed by turn end (still pending, or rejected) never reached + // the model — un-surface it so commit() can't ack it; it redelivers on a later turn. + for (const id of pendingSteerIds) turn.unsurface(id); + pendingSteerIds.clear(); turn.commit(); // ack the surfaced run — clean or failed both consume (no retry-loop) streaming = false; if (to && reply) deliver(to, reply); @@ -195,11 +213,12 @@ export function runPeerLoop({ mesh, session }: { mesh: PeerMesh; session: PeerSe return { async shutdown(): Promise { + stopped = true; // block any in-flight prompt/steer callback from driving a disposed session if (turn.inFlight) { turn.abandon(); // leave the in-flight run on the stream → redeliver, no peer dropped await session.abort(); } - await session.dispose(); + await session.dispose().catch(log); // await async cleanup, but never let a dispose failure skip the caller's mesh.stop() }, }; } From 3a364780d077785168330391a61efff6017dfd3c Mon Sep 17 00:00:00 2001 From: seal Date: Wed, 8 Jul 2026 18:42:10 -0400 Subject: [PATCH 09/15] fix(connector-core): run the new hermetic smokes in CI `smoke:inbox` and `smoke:reconnect-log` (both added in this PR) ran only by hand. Add a package `test` aggregate so `pnpm -r test` in the unit lane executes them, matching the oh-my-pi connector. Co-Authored-By: seal --- extensions/connector-core/package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/extensions/connector-core/package.json b/extensions/connector-core/package.json index 0bfd1f7a..db0694bb 100644 --- a/extensions/connector-core/package.json +++ b/extensions/connector-core/package.json @@ -20,6 +20,9 @@ "scripts": { "typecheck": "tsc -p tsconfig.json --noEmit", "build": "tsc -p tsconfig.json", + "smoke:inbox": "tsx inbox-turn.smoke.ts", + "smoke:reconnect-log": "tsx smoke/reconnect-log.smoke.ts", + "test": "pnpm run smoke:inbox && pnpm run smoke:reconnect-log", "prepublishOnly": "pnpm run build" }, "dependencies": { From d8cac3800e7134df4c46fd7ba0dcc7de7ad6202a Mon Sep 17 00:00:00 2001 From: seal Date: Wed, 8 Jul 2026 19:06:30 -0400 Subject: [PATCH 10/15] test(connector): red-green the steer-ack + dispose-shutdown races MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fix-review of c09b21e found two races the finding-level tests missed. Add regression tests that fail on c09b21e and pass on the race fix: - test 9: a rejecting same-scope fold with agent_end emitted before the rejection settles must not be acked (c09b21e acks it → message lost; the fix un-surfaces pending steers at agent_end before commit). - test 10: a rejecting session.dispose() must not reject loop.shutdown() (c09b21e's bare await rejects → peer.ts skips mesh.stop → ghost peer; the fix awaits dispose().catch(log)). Both proven red on c09b21e (tests 9 line 368, 10 line 400), green on head. Co-Authored-By: seal --- .../connector-oh-my-pi/oh-my-pi-peer.smoke.ts | 66 ++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/extensions/connector-oh-my-pi/oh-my-pi-peer.smoke.ts b/extensions/connector-oh-my-pi/oh-my-pi-peer.smoke.ts index 52b73b98..34fbba8e 100644 --- a/extensions/connector-oh-my-pi/oh-my-pi-peer.smoke.ts +++ b/extensions/connector-oh-my-pi/oh-my-pi-peer.smoke.ts @@ -134,6 +134,9 @@ class StubSession implements PeerSession { /** Opt-in: when `true`, `steer()` records then REJECTS (the fold couldn't reach the model * turn). Default `false` (records + resolves as before). */ steerReject = false; + /** Opt-in: when `true`, `dispose()` records then REJECTS (an SDK teardown that throws). + * Default `false` (records + resolves as before, so tests 1-9 are unchanged). */ + disposeReject = false; private listeners: ((event: AgentSessionEvent) => void)[] = []; subscribe(listener: (event: AgentSessionEvent) => void): () => void { @@ -157,8 +160,11 @@ class StubSession implements PeerSession { async abort(): Promise { this.aborted++; } - async dispose(): Promise { + dispose(): Promise { this.disposed++; + // A non-async return so a rejected dispose settles in ONE microtask (mirrors `steer`) — the + // loop's `.catch(log)` then swallows it and `shutdown()` still resolves. + return this.disposeReject ? Promise.reject(new Error("dispose rejected")) : Promise.resolve(); } } @@ -338,4 +344,62 @@ console.log("7) declined prompt completes the turn (no wedge) OK ✅"); } console.log("8) rejected steer un-surfaces its message (redelivers) OK ✅"); +// 9) the steer-ack RACE (agent_end commits before the rejection's .catch runs): the stricter +// sibling of test 8. Test 8 drains BEFORE agent_end, so the rejected fold's +// `.catch → unsurface` has already run and commit() sees a2 gone — it passes even on the +// intermediate c09b21e (plain `.catch(→unsurface)` fold + immediate commit()). Here we do NOT +// drain at the critical point: we emit agent_end in the SAME tick the steer rejected, so +// commit() runs while a2's rejection `.catch` is still a queued microtask. Only the final fix +// (7c73207: un-surface every still-pending fold in agent_end, before commit) survives — on +// c09b21e commit() acks a2 before the late `.catch` (which then no-ops on a reset turn) and a2 +// is lost. +{ + const mesh = new FakeMesh(); + const session = new StubSession(); + mesh.items = [dm("a1", "alice", "q1")]; + session.steerReject = true; // the fold's steer will reject (never reaches the model turn) + runPeerLoop({ mesh, session }); + session.emit(START); // turn live, streaming + mesh.arrive(dm("a2", "alice", "q2")); // same scope → foldSameScope surfaces a2, steer() rejects + // RACE WINDOW: do NOT drain. Emit agent_end immediately — commit() runs while a2's rejection + // `.catch` is still queued, so only the final fix's in-agent_end un-surface saves a2. + session.emit(end("ans")); // terminal → commit + await drain(); // now let the steer rejection's `.catch` settle before asserting + assert(ids(mesh.acked) === "a1", + "RACE: only the delivered origin a1 acked; the rejected fold a2 is not (c09b21e acks a1,a2)"); + assert(mesh.items.some((x) => x.id === "a2"), + "RACE: a2 stayed on the stream → redelivers (c09b21e loses it)"); + assert(session.steers.length === 1, "the fold attempted the steer exactly once"); +} +console.log("9) steer-ack race: agent_end before rejection settles does not ack the fold OK ✅"); + +// 10) a rejecting dispose() must NOT reject loop.shutdown(): peer.ts does `await loop.shutdown()` +// THEN `await mesh.stop()`. If shutdown() rejects (an SDK dispose() that throws), mesh.stop() +// is skipped → the peer leaves a ghost presence on the mesh. The final fix +// (`await session.dispose().catch(log)`) swallows the dispose failure so shutdown() resolves; +// on c09b21e (bare `await session.dispose()`) it rejects. +{ + const mesh = new FakeMesh(); + const session = new StubSession(); + mesh.items = [dm("s1", "alice")]; + session.disposeReject = true; // the SDK teardown will throw + const loop = runPeerLoop({ mesh, session }); + // Complete a turn so turn.inFlight is false at shutdown and it goes straight to the dispose() line. + session.emit(START); + await drain(); + session.emit(end("x")); + await drain(); + let resolved = false; + try { + await loop.shutdown(); + resolved = true; + } catch { + resolved = false; + } + assert(resolved === true, + "loop.shutdown() RESOLVED despite the rejecting dispose (c09b21e rejects → mesh.stop skipped)"); + assert(session.disposed === 1, "dispose was still attempted exactly once"); +} +console.log("10) rejecting dispose does not reject shutdown (mesh.stop still runs) OK ✅"); + console.log("OH-MY-PI PEER SMOKE OK ✅"); From 2e42d482dfb86a16c0b48120bb9dae9bff665363 Mon Sep 17 00:00:00 2001 From: seal Date: Wed, 8 Jul 2026 20:32:35 -0400 Subject: [PATCH 11/15] fix(connector): ack folds on steer settle, guard late callbacks + teardown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fix-review of the race fix found three deeper races in agent_end's "un-surface every pending fold before commit". All three are real (the mesh dedups only ACKED ids, so an un-acked redelivery re-surfaces to the model): - Accepted-steer redeliver (greptile): a fold whose steer() RESOLVED but whose .then hadn't flushed when agent_end fired was un-surfaced → not acked → redelivered though the model already received it. Now agent_end defers the commit until each fold's steer settles (Promise.race against a one-macrotask boundary so a never-settling steer can't hang the turn): an accepted fold stays acked, only a rejected or stranded one redelivers. - Cross-turn mutation (cubic P1): the reject callback called turn.unsurface() with no turn identity, so a steer settling after its turn committed could strip an id a LATER turn had re-surfaced. A monotonic generation captured at fold time makes a late settle a no-op on any turn but its own. - Post-shutdown dispatch (cubic P2): pump()/foldSameScope()/the session handler now early-return once stopped, so a mesh event landing mid-teardown can't drive a disposed session. The common one-message turn still commits synchronously (no fold → no defer). Tests (red on the pre-fix 6f82f76, green here): accepted-steer-acked, late-settle-no-ops-later-turn, shutdown-blocks-dispatch, strand-safety-no-hang. Co-Authored-By: seal --- .../connector-oh-my-pi/oh-my-pi-peer.smoke.ts | 134 ++++++++++++++++++ extensions/connector-oh-my-pi/src/loop.ts | 87 ++++++++---- 2 files changed, 196 insertions(+), 25 deletions(-) diff --git a/extensions/connector-oh-my-pi/oh-my-pi-peer.smoke.ts b/extensions/connector-oh-my-pi/oh-my-pi-peer.smoke.ts index 34fbba8e..d0cb5a76 100644 --- a/extensions/connector-oh-my-pi/oh-my-pi-peer.smoke.ts +++ b/extensions/connector-oh-my-pi/oh-my-pi-peer.smoke.ts @@ -137,6 +137,18 @@ class StubSession implements PeerSession { /** Opt-in: when `true`, `dispose()` records then REJECTS (an SDK teardown that throws). * Default `false` (records + resolves as before, so tests 1-9 are unchanged). */ disposeReject = false; + /** Opt-in (test 14): when `true`, `steer()` records then returns a promise that NEVER settles, + * so a fold stays pending forever. Exercises the BOUNDED deferred commit — the terminal commit + * must still fire (the macrotask boundary wins the race). Default `false` (tests 1-13 unchanged). */ + steerHang = false; + /** Opt-in (test 12): when `true`, `steer()` records then returns a PENDING promise whose reject + * fn is pushed onto {@link rejectSteer}, so the test controls exactly WHEN the fold settles. + * Firing the reject after the fold's turn committed exercises the generation guard. Default + * `false` (tests 1-11/13/14 keep the one-hop resolve/reject path). */ + deferSteer = false; + /** Captured reject fns for deferred steers (see {@link deferSteer}): `rejectSteer[i]()` rejects + * the i-th folded steer on demand. Empty unless `deferSteer` is set. */ + rejectSteer: (() => void)[] = []; private listeners: ((event: AgentSessionEvent) => void)[] = []; subscribe(listener: (event: AgentSessionEvent) => void): () => void { @@ -152,6 +164,16 @@ class StubSession implements PeerSession { } steer(text: string): Promise { this.steers.push(text); + // A never-settling steer (test 14): the fold stays pending; the deferred commit must still + // fire off its bounded macrotask boundary, so a hung steer can't wedge the turn. + if (this.steerHang) return new Promise(() => {}); + // A test-controlled deferred steer (test 12): capture the reject so the test can settle the + // fold at a chosen moment (e.g. AFTER its turn committed, to exercise the generation guard). + if (this.deferSteer) { + const { promise, reject } = Promise.withResolvers(); + this.rejectSteer.push(() => reject(new Error("steer rejected (deferred)"))); + return promise; + } // A non-async return so a rejected steer settles in ONE microtask (an `async` method would // adopt the thenable and take extra ticks) — the loop's `.catch → unsurface` then runs after // a single `await Promise.resolve()`, matching how the real session rejects. @@ -402,4 +424,116 @@ console.log("9) steer-ack race: agent_end before rejection settles does not ack } console.log("10) rejecting dispose does not reject shutdown (mesh.stop still runs) OK ✅"); +// 11) an ACCEPTED steer is ACKED, not redelivered (the exact inverse of test 9): a same-scope +// fold whose steer() RESOLVES (the session accepted the message into its turn) must be acked +// even when agent_end fires in the SAME tick — before the accept's `.then(delete)` microtask +// has flushed. The deferred commit awaits the still-pending fold, sees it accepted, and acks +// it. Un-acking an accepted fold would REDELIVER a message the model already got (the mesh +// dedups only ACKED ids, so an un-acked id re-surfaces to the model). This is the greptile +// finding: 6f82f76 un-surfaces every still-pending fold in agent_end unconditionally, so an +// accepted-but-not-yet-flushed fold is dropped from the ack set → double delivery. +{ + const mesh = new FakeMesh(); + const session = new StubSession(); + mesh.items = [dm("a1", "alice", "q1")]; // steer ACCEPTS (default steerReject=false) + runPeerLoop({ mesh, session }); + session.emit(START); // turn live, streaming + mesh.arrive(dm("a2", "alice", "q2")); // same scope → fold; steer() RESOLVES, its .then(delete) queued + // RACE WINDOW: do NOT drain. Emit agent_end while a2's accept `.then` is still a queued microtask, + // so pendingSteers is non-empty → the commit is DEFERRED until the accept settles. + session.emit(end("ans")); + await drain(); // the deferred commit awaits the accept (microtask) then commits within this tick + assert(ids(mesh.acked) === "a1,a2", + "ACCEPTED fold a2 is acked with the origin (6f82f76 acks only a1 → a2 redelivers)"); + assert(!mesh.items.some((x) => x.id === "a2"), + "a2 left the stream (acked, won't redeliver); 6f82f76 leaves it → double delivery"); + assert(session.steers.length === 1, "the fold attempted the steer exactly once"); +} +console.log("11) accepted steer is acked, not redelivered OK ✅"); + +// 12) a late steer settle does NOT mutate a LATER turn (cubic P1, the generation guard): a fold's +// steer that settles AFTER its turn committed must be a no-op — it must never strip an id the +// NEXT turn re-surfaced. Turn 1 folds a2 with a test-controlled (deferred) steer; the fold is +// stranded past the macrotask boundary → un-surfaced (redelivered) and turn 1 commits, bumping +// the generation. a2 redelivers as turn 2's origin. THEN, while turn 2 holds a2 surfaced-but- +// uncommitted, we fire the STALE turn-1 steer reject. On the fixed loop the callback captured +// turn 1's generation and no-ops. On 6f82f76 the reject's `.catch` calls turn.unsurface("a2") +// on the live turn 2 (no generation guard) → strips a2 from turn 2's ack set → a2 is never +// acked and redelivers forever. +{ + const mesh = new FakeMesh(); + const session = new StubSession(); + session.deferSteer = true; // turn 1's fold steer stays pending until we fire rejectSteer[0]() + mesh.items = [dm("a1", "alice", "q1")]; + runPeerLoop({ mesh, session }); + session.emit(START); // turn 1 live + mesh.arrive(dm("a2", "alice", "q2")); // fold a2 under generation g0; steer deferred (unsettled) + session.emit(end("r1")); // pendingSteers non-empty → deferred commit races the macrotask boundary + await drain(); // boundary wins: a2 still pending → un-surfaced (redeliver), turn 1 commits, gen→g1 + assert(ids(mesh.acked) === "a1", "turn 1 acked only its origin a1; the stranded fold a2 redelivers"); + assert(session.prompts.length === 2 && session.prompts[1] === framed(dm("a2", "alice", "q2")), + "a2 redelivered as turn 2's origin"); + session.emit(START); // turn 2 live: a2 surfaced, NOT yet committed + // Fire the STALE turn-1 (g0) steer reject now, WHILE turn 2 holds a2 surfaced. On the fix the + // g0 callback sees generation moved on and no-ops; on 6f82f76 it strips a2 from turn 2. + session.rejectSteer[0](); + await drain(); + session.emit(end("r2")); // turn 2 commits + await drain(); + assert(mesh.acked.filter((x) => x.id === "a2").length === 1, + "a2 acked exactly once under turn 2; the stale g0 reject was a no-op (6f82f76 strips it → 0)"); + assert(!mesh.items.some((x) => x.id === "a2"), + "a2 left the stream after turn 2 (6f82f76 leaves it surfaced-then-stripped → redelivers)"); +} +console.log("12) late steer settle does not mutate a later turn (generation guard) OK ✅"); + +// 13) shutdown blocks further dispatch (cubic P2, the stopped-guards): once shutdown() begins, a +// mesh `incoming`/`wake` event must NOT start a new turn on the disposed session. The initial +// pump surfaces s1 (prompt pending, no START → not streaming); shutdown abandons it and disposes. +// A post-shutdown arrive+wake must not re-pump. 6f82f76's pump() has no stopped guard → the +// late incoming starts a turn and prompts the disposed session. +{ + const mesh = new FakeMesh(); + const session = new StubSession(); + mesh.items = [dm("s1", "alice")]; + const loop = runPeerLoop({ mesh, session }); // pump → surfaces s1, prompt(s1) pending (no START) + await drain(); // let the initial prompt(s1) settle; turn is surfaced-but-not-streaming + await loop.shutdown(); // stopped=true; abandon in-flight s1 (redeliver); dispose + assert(session.disposed === 1, "shutdown disposed the session"); + const promptsBefore = session.prompts.length; // exactly the one pre-shutdown s1 prompt + mesh.arrive(dm("s2", "bob")); // post-shutdown incoming → pump() must early-return (stopped) + mesh.emit("wake"); // post-shutdown wake → pump() must early-return (stopped) + await drain(); + assert(session.prompts.length === promptsBefore, + "no new prompt after shutdown (pump stopped-guard); 6f82f76 re-pumps the disposed session"); +} +console.log("13) shutdown blocks further dispatch (stopped-guard) OK ✅"); + +// 14) strand safety — a never-settling steer does NOT hang the terminal commit (mercator's +// insurance test, a forward guard). The deferred commit races the pending folds against a +// one-macrotask boundary, so even a steer that NEVER settles cannot wedge the turn: the +// boundary wins, the turn commits (origin acked), and the unconfirmed fold is un-surfaced +// (redeliver — the safe direction, since the model may never have received it). This is NOT a +// 6f82f76 differentiator: 6f82f76 commits synchronously so it also would not hang here. Test 14 +// GUARDS the new deferred path — it FAILS if someone later drops the boundary and awaits +// allSettled unbounded (the commit would then never fire and this test would hang, never +// reaching its asserts). See the throwaway boundary-removed probe reported alongside this file. +{ + const mesh = new FakeMesh(); + const session = new StubSession(); + session.steerHang = true; // the fold's steer never resolves or rejects + mesh.items = [dm("a1", "alice", "q1")]; + runPeerLoop({ mesh, session }); + session.emit(START); // turn live, streaming + mesh.arrive(dm("a2", "alice", "q2")); // fold a2; steer hangs → a2 stays pending forever + session.emit(end("ans")); // pendingSteers non-empty → deferred commit races the boundary + await drain(); // ONE macrotask tick — the boundary wins the race, commit fires without the steer + assert(mesh.acked.some((x) => x.id === "a1"), + "the turn COMMITTED off the bounded wait (a1 acked) — a hung steer did not wedge it"); + assert(mesh.items.some((x) => x.id === "a2"), + "the never-confirmed fold a2 was un-surfaced → stays on the stream to redeliver (safe direction)"); + assert(session.steers.length === 1, "the hung fold attempted the steer exactly once"); +} +console.log("14) strand safety: never-settling steer does not hang commit OK ✅"); + console.log("OH-MY-PI PEER SMOKE OK ✅"); diff --git a/extensions/connector-oh-my-pi/src/loop.ts b/extensions/connector-oh-my-pi/src/loop.ts index 95b34d3d..cb7b957f 100644 --- a/extensions/connector-oh-my-pi/src/loop.ts +++ b/extensions/connector-oh-my-pi/src/loop.ts @@ -98,13 +98,21 @@ function turnReplyText(messages: readonly unknown[]): string | undefined { export function runPeerLoop({ mesh, session }: { mesh: PeerMesh; session: PeerSession }): PeerLoop { const turn = new InboxTurn(mesh); let streaming = false; // gates steer(): only valid once the agent is actually streaming - // Set once shutdown() begins so a prompt/steer settling mid-teardown can't drive a disposed - // session (commit/pump/setStatus on a torn-down loop). Guards the async prompt callback below. + // Set once shutdown() begins so nothing dispatched after teardown drives a disposed session + // (commit/pump/setStatus/steer on a torn-down loop). Checked at every entry point that can act + // post-shutdown: pump(), foldSameScope(), the session event handler, the prompt callback + // (onStartError), and the deferred commit. let stopped = false; - // Folded ids whose steer() has not yet CONFIRMED delivery. agent_end un-surfaces any still - // pending (or failed) fold before commit, so a message the model never received is left on the - // stream to redeliver rather than falsely acked. - const pendingSteerIds = new Set(); + // Monotonic turn counter, bumped on each terminal commit. A fold's steer callback captures the + // generation it was issued under, so a late settle (a steer resolving after its turn committed) + // can only mutate ITS OWN turn — never a later turn that re-surfaced a redelivered id (cubic P1). + let generation = 0; + // Folds issued in the current turn whose steer() has not settled: id → the settle-chained promise. + // agent_end awaits these (bounded) so an ACCEPTED fold stays acked and only a rejected/stranded one + // is un-surfaced. steer() resolving means the session accepted the message into its queue (the + // model got it); un-surfacing an accepted fold would redeliver a message already delivered, since + // the mesh only dedups ACKED ids — an un-acked redelivery re-surfaces to the model. + const pendingSteers = new Map>(); const setStatus = (status: "idle" | "working", activity?: string): void => { void mesh.setStatus(status, activity).catch(() => {}); @@ -119,9 +127,9 @@ export function runPeerLoop({ mesh, session }: { mesh: PeerMesh; session: PeerSe } /** Start the next turn on the front actionable message, dropping leading non-actionable - * (own echoes, ambient chatter) first. No-op while a turn is in flight. */ + * (own echoes, ambient chatter) first. No-op while a turn is in flight or after teardown. */ function pump(): void { - if (turn.inFlight) return; + if (stopped || turn.inFlight) return; turn.drop((i) => !actionable(mesh, i)); const origin = turn.start(); if (!origin) { @@ -143,20 +151,25 @@ export function runPeerLoop({ mesh, session }: { mesh: PeerMesh; session: PeerSe /** Fold any front-contiguous, same-scope actionable messages into the live turn (mid-turn * steer). A cross-scope or ambient message breaks contiguity and waits for its own turn. */ function foldSameScope(): void { - if (!turn.origin || !streaming) return; + if (stopped || !turn.origin || !streaming) return; + const gen = generation; // these folds belong to the current turn; a late settle checks this for (const item of turn.extend((i, o) => actionable(mesh, i) && scopeKey(i) === scopeKey(o))) { - // extend() surfaces synchronously so a re-entrant fold can't re-pick the item, but the steer - // is async: hold the id as pending until it CONFIRMS. A steer still pending (or rejected) at - // agent_end is un-surfaced there, so a message the model never received redelivers, not acked. - pendingSteerIds.add(item.id); - void session.steer(framed(item)).then( - () => pendingSteerIds.delete(item.id), // delivered → stays surfaced → acked on commit + // extend() surfaces synchronously so a re-entrant fold can't re-pick the item; the steer is + // async. Track the settle-chained promise so agent_end can await the real accept/reject before + // commit. The generation guard stops a steer that settles after its turn committed from + // mutating a later turn's ack set (its id long since acked or re-surfaced under a new turn). + const settle = session.steer(framed(item)).then( + () => { + if (gen === generation) pendingSteers.delete(item.id); // accepted → stays surfaced → acked + }, (e) => { log(e); - pendingSteerIds.delete(item.id); + if (gen !== generation) return; // turn already committed → never touch a later turn + pendingSteers.delete(item.id); turn.unsurface(item.id); // rejected → drop from the ack set → redelivers on a later turn }, ); + pendingSteers.set(item.id, settle); } } @@ -169,6 +182,31 @@ export function runPeerLoop({ mesh, session }: { mesh: PeerMesh; session: PeerSe pump(); } + /** The sole terminal-commit tail for a streaming turn: un-surface any fold whose steer never + * confirmed (redeliver, not ack), ack the rest, bump generation so a late steer settle can no + * longer mutate this turn, then advance to the next scope. */ + function finishTurn(to: InboxItem | undefined, reply?: string): void { + for (const id of pendingSteers.keys()) turn.unsurface(id); // unconfirmed fold → redeliver, not acked + pendingSteers.clear(); + turn.commit(); // ack the surfaced run (origin + confirmed folds) — clean or failed both consume + generation++; // supersede: a steer settling after this can't touch the next turn's ack set + if (to && reply) deliver(to, reply); + pump(); // next scope + } + + /** agent_end path with folds still unconfirmed: wait — bounded by one macrotask so a steer that + * never settles can't hang the turn — for each fold's steer to settle (its handler in + * {@link foldSameScope} acks an accepted fold by leaving it surfaced, un-surfaces a rejected one), + * then finish. A fold STILL pending after the wait stranded → finishTurn un-surfaces it (redeliver, + * the safe direction). Guarded so a shutdown mid-wait or a superseding turn never commits a + * stale/disposed turn. */ + async function commitAfterSteers(gen: number, to: InboxItem | undefined, reply?: string): Promise { + const boundary = new Promise((resolve) => setTimeout(resolve, 0)); + await Promise.race([Promise.allSettled([...pendingSteers.values()]), boundary]); + if (stopped || gen !== generation) return; // torn down or superseded mid-wait → don't commit + finishTurn(to, reply); + } + mesh.on("incoming", () => { if (turn.inFlight) foldSameScope(); else pump(); @@ -178,6 +216,7 @@ export function runPeerLoop({ mesh, session }: { mesh: PeerMesh; session: PeerSe }); session.subscribe((event: AgentSessionEvent) => { + if (stopped) return; // teardown began → don't drive a disposed session switch (event.type) { case "agent_start": streaming = true; @@ -193,16 +232,14 @@ export function runPeerLoop({ mesh, session }: { mesh: PeerMesh; session: PeerSe case "agent_end": { // oh-my-pi has no `agent_end.willRetry`; a retry is its own session `auto_retry_*` // event and the failed turn still ends here, so `agent_end` is always terminal. + streaming = false; // done streaming — foldSameScope is now a no-op; no new folds this turn const to = turn.origin; const reply = turnReplyText(event.messages); - // A fold whose steer hasn't confirmed by turn end (still pending, or rejected) never reached - // the model — un-surface it so commit() can't ack it; it redelivers on a later turn. - for (const id of pendingSteerIds) turn.unsurface(id); - pendingSteerIds.clear(); - turn.commit(); // ack the surfaced run — clean or failed both consume (no retry-loop) - streaming = false; - if (to && reply) deliver(to, reply); - pump(); // next scope + // No unconfirmed folds → commit synchronously (the common one-message turn, unchanged). With + // folds still settling, defer the commit until they do so an accepted fold is acked and only + // a rejected/stranded one redelivers (bounded, generation-guarded). + if (pendingSteers.size === 0) finishTurn(to, reply); + else void commitAfterSteers(generation, to, reply); break; } } @@ -213,7 +250,7 @@ export function runPeerLoop({ mesh, session }: { mesh: PeerMesh; session: PeerSe return { async shutdown(): Promise { - stopped = true; // block any in-flight prompt/steer callback from driving a disposed session + stopped = true; // block any in-flight prompt/steer/mesh callback from driving a disposed session if (turn.inFlight) { turn.abandon(); // leave the in-flight run on the stream → redeliver, no peer dropped await session.abort(); From 10f94ff188a4add07fc665c6c0f9644fbdabc4e5 Mon Sep 17 00:00:00 2001 From: seal Date: Wed, 8 Jul 2026 21:17:09 -0400 Subject: [PATCH 12/15] fix(connector): swallow abort() failure in shutdown so mesh.stop still runs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sibling of the dispose-teardown fix: shutdown() awaited session.abort() bare, so a rejected abort() rejected shutdown() → skipped dispose() AND the caller's mesh.stop() → ghost peer on the mesh. Handle it like dispose(): await session.abort().catch(log). Regression test (red on the pre-fix commit, green here): a rejecting abort still resolves shutdown() and dispose() still runs (disposed === 1). Co-Authored-By: seal --- .../connector-oh-my-pi/oh-my-pi-peer.smoke.ts | 42 ++++++++++++++++++- extensions/connector-oh-my-pi/src/loop.ts | 2 +- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/extensions/connector-oh-my-pi/oh-my-pi-peer.smoke.ts b/extensions/connector-oh-my-pi/oh-my-pi-peer.smoke.ts index d0cb5a76..a4e6a23d 100644 --- a/extensions/connector-oh-my-pi/oh-my-pi-peer.smoke.ts +++ b/extensions/connector-oh-my-pi/oh-my-pi-peer.smoke.ts @@ -137,6 +137,9 @@ class StubSession implements PeerSession { /** Opt-in: when `true`, `dispose()` records then REJECTS (an SDK teardown that throws). * Default `false` (records + resolves as before, so tests 1-9 are unchanged). */ disposeReject = false; + /** Opt-in: when `true`, `abort()` records then REJECTS (an SDK abort that throws). Default + * `false` (records + resolves as before, so tests 1-14 are unchanged). Mirror of {@link disposeReject}. */ + abortReject = false; /** Opt-in (test 14): when `true`, `steer()` records then returns a promise that NEVER settles, * so a fold stays pending forever. Exercises the BOUNDED deferred commit — the terminal commit * must still fire (the macrotask boundary wins the race). Default `false` (tests 1-13 unchanged). */ @@ -179,8 +182,11 @@ class StubSession implements PeerSession { // a single `await Promise.resolve()`, matching how the real session rejects. return this.steerReject ? Promise.reject(new Error("steer rejected")) : Promise.resolve(); } - async abort(): Promise { + abort(): Promise { this.aborted++; + // A non-async return so a rejected abort settles in ONE microtask (mirrors `dispose`) — the + // loop's `.catch(log)` then swallows it and `shutdown()` still proceeds to dispose()/resolve. + return this.abortReject ? Promise.reject(new Error("abort rejected")) : Promise.resolve(); } dispose(): Promise { this.disposed++; @@ -536,4 +542,38 @@ console.log("13) shutdown blocks further dispatch (stopped-guard) OK ✅"); } console.log("14) strand safety: never-settling steer does not hang commit OK ✅"); +// 15) a rejecting abort() must NOT reject loop.shutdown() (sibling of test 10, one line up): the +// shutdown() sequence is `if (turn.inFlight) { turn.abandon(); await session.abort(); } await +// session.dispose().catch(log)`. peer.ts does `await loop.shutdown()` THEN `await mesh.stop()`. +// If abort() rejects and the loop doesn't swallow it, shutdown() rejects → dispose() is SKIPPED +// AND mesh.stop() never runs → ghost peer on the mesh. The fix (`await session.abort().catch(log)`) +// swallows the abort failure so teardown continues; on d213837 (bare `await session.abort()`) it +// rejects. The abort path only runs when the turn is IN FLIGHT at shutdown, so we emit START (turn +// open + streaming) with NO agent_end. `session.disposed === 1` proves teardown continued past the +// abort rejection (so mesh.stop would run). +{ + const mesh = new FakeMesh(); + const session = new StubSession(); + mesh.items = [dm("s1", "alice")]; + session.abortReject = true; // the SDK abort will throw + const loop = runPeerLoop({ mesh, session }); + // Hold the turn IN FLIGHT: emit START (streaming, turn open) with no agent_end, so shutdown() enters + // the `if (turn.inFlight)` branch and calls abort() (the only path where a rejecting abort matters). + session.emit(START); + await drain(); + let resolved = false; + try { + await loop.shutdown(); + resolved = true; + } catch { + resolved = false; + } + assert(resolved === true, + "loop.shutdown() RESOLVED despite the rejecting abort (d213837 rejects → dispose/mesh.stop skipped)"); + assert(session.aborted === 1, "abort was attempted exactly once (turn was in flight)"); + assert(session.disposed === 1, + "dispose STILL ran after the abort rejection → teardown continued so mesh.stop would run (no ghost peer)"); +} +console.log("15) rejecting abort does not reject shutdown (dispose/mesh.stop still run) OK ✅"); + console.log("OH-MY-PI PEER SMOKE OK ✅"); diff --git a/extensions/connector-oh-my-pi/src/loop.ts b/extensions/connector-oh-my-pi/src/loop.ts index cb7b957f..b374ccdf 100644 --- a/extensions/connector-oh-my-pi/src/loop.ts +++ b/extensions/connector-oh-my-pi/src/loop.ts @@ -253,7 +253,7 @@ export function runPeerLoop({ mesh, session }: { mesh: PeerMesh; session: PeerSe stopped = true; // block any in-flight prompt/steer/mesh callback from driving a disposed session if (turn.inFlight) { turn.abandon(); // leave the in-flight run on the stream → redeliver, no peer dropped - await session.abort(); + await session.abort().catch(log); // a rejected abort must not skip dispose()/the caller's mesh.stop() } await session.dispose().catch(log); // await async cleanup, but never let a dispose failure skip the caller's mesh.stop() }, From 90d27e02beba8dc67fe8f8a6886b48bd8a7d1d8b Mon Sep 17 00:00:00 2001 From: seal Date: Wed, 8 Jul 2026 23:48:33 -0400 Subject: [PATCH 13/15] fix(connector): bound the fold-settle wait by a human-scale timeout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two review findings on the deferred-commit + shutdown hardening: - Slow-accept redeliver: the deferred commit raced pending fold steers against a setTimeout(0) macrotask boundary. A steer accepting after that 0ms tick was treated as undelivered and redelivered though the model already had it. Replace the 0ms boundary with an injectable steerSettleTimeoutMs (default 5s): a healthy steer settles in <=1 microtask so allSettled wins by orders of magnitude and the timeout never fires on the happy path; it only bounds a genuinely stuck steer so the turn can't wedge. Correct by a real margin, not by microtask-vs-macrotask ordering. - Shutdown teardown coupling: abort() and dispose() now sit in independent try/catch blocks, so a failed OR synchronously-thrown abort() still runs dispose() and still resolves shutdown() — peer.ts's mesh.stop() always runs, no ghost peer. (A single wrapping try/catch would let an abort failure skip dispose.) Tests (red on the pre-fix boundary/shutdown, green here): slow-accept steer is acked under the human-scale timeout; a sync-throwing abort still resolves shutdown and runs dispose. Timeout is injectable so tests don't wait the real delay. Co-Authored-By: seal --- .../connector-oh-my-pi/oh-my-pi-peer.smoke.ts | 96 ++++++++++++++++++- extensions/connector-oh-my-pi/src/loop.ts | 54 ++++++++--- 2 files changed, 136 insertions(+), 14 deletions(-) diff --git a/extensions/connector-oh-my-pi/oh-my-pi-peer.smoke.ts b/extensions/connector-oh-my-pi/oh-my-pi-peer.smoke.ts index a4e6a23d..f46ebadb 100644 --- a/extensions/connector-oh-my-pi/oh-my-pi-peer.smoke.ts +++ b/extensions/connector-oh-my-pi/oh-my-pi-peer.smoke.ts @@ -140,6 +140,11 @@ class StubSession implements PeerSession { /** Opt-in: when `true`, `abort()` records then REJECTS (an SDK abort that throws). Default * `false` (records + resolves as before, so tests 1-14 are unchanged). Mirror of {@link disposeReject}. */ abortReject = false; + /** Opt-in (test #6, sync-throw): when `true`, `abort()` increments `aborted` then THROWS + * SYNCHRONOUSLY — the throw happens BEFORE a promise is returned, so a bare `.catch()` on the + * call cannot catch it; only a surrounding try/catch in shutdown() does. Default `false` (tests + * 1-15 unchanged). Distinct from {@link abortReject} (async rejection). */ + abortThrowSync = false; /** Opt-in (test 14): when `true`, `steer()` records then returns a promise that NEVER settles, * so a fold stays pending forever. Exercises the BOUNDED deferred commit — the terminal commit * must still fire (the macrotask boundary wins the race). Default `false` (tests 1-13 unchanged). */ @@ -152,6 +157,15 @@ class StubSession implements PeerSession { /** Captured reject fns for deferred steers (see {@link deferSteer}): `rejectSteer[i]()` rejects * the i-th folded steer on demand. Empty unless `deferSteer` is set. */ rejectSteer: (() => void)[] = []; + /** Opt-in (test #5, slow-accept): when `true`, `steer()` records then returns a promise that + * resolves on `setTimeout(resolve, 5)` — a REAL ~5ms accept (a genuine settle doing work, e.g. a + * future images-carrying steer's normalize/resize), long past the first microtask. 5ms is chosen + * so the race is decided by DELAY MAGNITUDE, not timer-registration order: it LOSES to a 0ms + * boundary (0 < 5 → timeout fires first → fold un-surfaced, finding #5) but WINS against a 50ms + * human-scale boundary (5 < 50 → allSettled resolves first → fold acked). A `setTimeout(0)` accept + * would be non-load-bearing here: registered at fold-time it always beats a later-registered 0ms + * boundary, so it could never go red. Default `false` (tests 1-15 keep the microtask resolve). */ + steerSlowAccept = false; private listeners: ((event: AgentSessionEvent) => void)[] = []; subscribe(listener: (event: AgentSessionEvent) => void): () => void { @@ -177,6 +191,16 @@ class StubSession implements PeerSession { this.rejectSteer.push(() => reject(new Error("steer rejected (deferred)"))); return promise; } + // A real ~5ms accept (test #5, slow-accept): the steer resolves on setTimeout(resolve, 5) — + // genuine settle work, well past the first microtask. It loses to a 0ms boundary (finding #5's + // window) but wins against a 50ms human-scale boundary, so commitAfterSteers awaits and acks it. + // 5ms (a real delay, not setTimeout(0)) makes the race decided by magnitude, not registration + // order — the only shape that actually goes red when the timeout is reverted to 0. + if (this.steerSlowAccept) { + const { promise, resolve } = Promise.withResolvers(); + setTimeout(resolve, 5); + return promise; + } // A non-async return so a rejected steer settles in ONE microtask (an `async` method would // adopt the thenable and take extra ticks) — the loop's `.catch → unsurface` then runs after // a single `await Promise.resolve()`, matching how the real session rejects. @@ -184,6 +208,10 @@ class StubSession implements PeerSession { } abort(): Promise { this.aborted++; + // A SYNCHRONOUS throw (test #6): the throw happens before any promise is returned, so a bare + // `.catch()` on the call never sees it — only shutdown()'s surrounding try/catch does. This is + // the seam a non-conforming adapter could hit; the per-call try/catch must still run dispose(). + if (this.abortThrowSync) throw new Error("abort sync-throw"); // A non-async return so a rejected abort settles in ONE microtask (mirrors `dispose`) — the // loop's `.catch(log)` then swallows it and `shutdown()` still proceeds to dispose()/resolve. return this.abortReject ? Promise.reject(new Error("abort rejected")) : Promise.resolve(); @@ -471,7 +499,7 @@ console.log("11) accepted steer is acked, not redelivered OK ✅"); const session = new StubSession(); session.deferSteer = true; // turn 1's fold steer stays pending until we fire rejectSteer[0]() mesh.items = [dm("a1", "alice", "q1")]; - runPeerLoop({ mesh, session }); + runPeerLoop({ mesh, session, steerSettleTimeoutMs: 0 }); // strand path: timeout must fire within one drain() session.emit(START); // turn 1 live mesh.arrive(dm("a2", "alice", "q2")); // fold a2 under generation g0; steer deferred (unsettled) session.emit(end("r1")); // pendingSteers non-empty → deferred commit races the macrotask boundary @@ -529,7 +557,7 @@ console.log("13) shutdown blocks further dispatch (stopped-guard) OK ✅"); const session = new StubSession(); session.steerHang = true; // the fold's steer never resolves or rejects mesh.items = [dm("a1", "alice", "q1")]; - runPeerLoop({ mesh, session }); + runPeerLoop({ mesh, session, steerSettleTimeoutMs: 0 }); // never-settle path: timeout must fire within one drain() session.emit(START); // turn live, streaming mesh.arrive(dm("a2", "alice", "q2")); // fold a2; steer hangs → a2 stays pending forever session.emit(end("ans")); // pendingSteers non-empty → deferred commit races the boundary @@ -576,4 +604,68 @@ console.log("14) strand safety: never-settling steer does not hang commit OK ✅ } console.log("15) rejecting abort does not reject shutdown (dispose/mesh.stop still run) OK ✅"); +// 16) slow-accept steer is STILL acked under a human-scale timeout (#5-killer, load-bearing): the +// whole point of B-simple. A fold whose steer() accepts after a REAL settle delay (~5ms — genuine +// work, not just one microtask) must still be awaited by the deferred commit and acked, because a +// human-scale steerSettleTimeoutMs is generous enough to wait for it. commitAfterSteers races +// allSettled(pending) against setTimeout(steerSettleTimeoutMs); with a 50ms boundary the 5ms +// accept resolves first, so allSettled wins and a2 is acked. RED at boundary 0 (== the old +// setTimeout(0) window): 0 < 5, so the timeout fires before the accept settles → finishTurn +// un-surfaces a2 → acked==="a1", a2 redelivers (exactly finding #5). The delay is a REAL 5ms (not +// setTimeout(0)) so the race is decided by MAGNITUDE, not timer-registration order — the only +// shape that actually goes red at boundary 0. This test FAILS if someone reverts the timeout to a +// value below the accept delay (e.g. 0) or drops the param. Red-green evidence in the task report. +{ + const mesh = new FakeMesh(); + const session = new StubSession(); + session.steerSlowAccept = true; // the fold's steer resolves after a real ~5ms settle (setTimeout 5) + mesh.items = [dm("a1", "alice", "q1")]; + runPeerLoop({ mesh, session, steerSettleTimeoutMs: 50 }); // human-scale boundary >> the 5ms accept + session.emit(START); // turn live, streaming + mesh.arrive(dm("a2", "alice", "q2")); // same scope → fold; slow-accept steer resolves ~5ms later + session.emit(end("ans")); // pendingSteers non-empty → deferred commit awaits the slow accept + // Wait past the slow accept's ~5ms delay but well UNDER the 50ms timeout, so the commit provably + // comes from allSettled resolving (a2 accepted), NOT from the timeout firing. A real delay (not + // drain()s): two macrotask ticks (~1ms) would assert before the 5ms accept and the deferred commit. + await new Promise((r) => setTimeout(r, 20)); + assert(ids(mesh.acked) === "a1,a2", + "the slow-accepted fold a2 IS acked (allSettled waited for it); at timeout 0 acked==='a1' (#5)"); + assert(!mesh.items.some((x) => x.id === "a2"), + "a2 left the stream (acked, not redelivered); at timeout 0 a2 stays → redelivers"); + assert(session.steers.length === 1, "the slow fold attempted the steer exactly once"); +} +console.log("16) slow-accept steer is still acked under a human-scale timeout (#5) OK ✅"); + +// 17) a synchronously-throwing abort() must STILL resolve shutdown() and run dispose() (#6, the +// ghost-peer seam): a `.catch()` only handles a REJECTED promise; a SYNC throw before the promise +// is even returned escapes it — only a surrounding try/catch in shutdown() catches it. The +// per-call try/catch (abort in one block, dispose in its own) resolves shutdown despite the sync +// throw AND still runs dispose so peer.ts's mesh.stop() runs → no ghost peer. Mirrors test 15 +// (async reject) with a SYNC throw instead. RED on /tmp/loop-bsimple-catchonly.ts (a `.catch`-only +// shutdown): the sync throw escapes `.catch` → shutdown throws → resolved=false, disposed=0. +{ + const mesh = new FakeMesh(); + const session = new StubSession(); + mesh.items = [dm("s1", "alice")]; + session.abortThrowSync = true; // abort() increments then THROWS synchronously (before any promise) + const loop = runPeerLoop({ mesh, session }); + // Hold the turn IN FLIGHT: emit START (streaming, turn open) with no agent_end, so shutdown() enters + // the `if (turn.inFlight)` branch and calls abort() — the only path where a sync-throwing abort matters. + session.emit(START); + await drain(); + let resolved = false; + try { + await loop.shutdown(); + resolved = true; + } catch { + resolved = false; + } + assert(resolved === true, + "loop.shutdown() RESOLVED despite the SYNC throw from abort (.catch-only shutdown → false)"); + assert(session.aborted === 1, "abort was attempted exactly once (turn was in flight)"); + assert(session.disposed === 1, + "dispose STILL ran after the sync throw → per-call try/catch let teardown continue (.catch-only → 0)"); +} +console.log("17) sync-throwing abort still resolves shutdown and runs dispose (#6) OK ✅"); + console.log("OH-MY-PI PEER SMOKE OK ✅"); diff --git a/extensions/connector-oh-my-pi/src/loop.ts b/extensions/connector-oh-my-pi/src/loop.ts index b374ccdf..f315767c 100644 --- a/extensions/connector-oh-my-pi/src/loop.ts +++ b/extensions/connector-oh-my-pi/src/loop.ts @@ -95,7 +95,22 @@ function turnReplyText(messages: readonly unknown[]): string | undefined { * events rather than an `agent_end.willRetry` flag, so an `agent_end` here is always the * turn's terminal event. */ -export function runPeerLoop({ mesh, session }: { mesh: PeerMesh; session: PeerSession }): PeerLoop { +export function runPeerLoop({ + mesh, + session, + // How long a terminal commit waits for a turn's unconfirmed fold steers to settle before giving + // up on them (un-surfacing → redeliver). A healthy steer settles in ≤1 microtask (its promise + // resolves at synchronous enqueue-time — no image work on the connector's string-only steers), so + // allSettled wins this race by orders of magnitude and the timeout never fires on the happy path; + // it only bounds a genuinely-stuck steer so shutdown/commit can't wedge. 5s is generous headroom + // over any realistic settle (even a future images-carrying steer's normalize/resize is ~tens of + // ms) while keeping a stuck-steer commit delay human-tolerable. Injectable so tests don't wait it. + steerSettleTimeoutMs = 5_000, +}: { + mesh: PeerMesh; + session: PeerSession; + steerSettleTimeoutMs?: number; +}): PeerLoop { const turn = new InboxTurn(mesh); let streaming = false; // gates steer(): only valid once the agent is actually streaming // Set once shutdown() begins so nothing dispatched after teardown drives a disposed session @@ -194,15 +209,17 @@ export function runPeerLoop({ mesh, session }: { mesh: PeerMesh; session: PeerSe pump(); // next scope } - /** agent_end path with folds still unconfirmed: wait — bounded by one macrotask so a steer that - * never settles can't hang the turn — for each fold's steer to settle (its handler in + /** agent_end path with folds still unconfirmed: wait — bounded by {@link steerSettleTimeoutMs} so + * a steer that never settles can't hang the turn — for each fold's steer to settle (its handler in * {@link foldSameScope} acks an accepted fold by leaving it surfaced, un-surfaces a rejected one), - * then finish. A fold STILL pending after the wait stranded → finishTurn un-surfaces it (redeliver, - * the safe direction). Guarded so a shutdown mid-wait or a superseding turn never commits a - * stale/disposed turn. */ + * then finish. The timeout is generous (a healthy steer settles in ≤1 microtask, so allSettled + * wins the race with orders of magnitude to spare — even a slow accept lands well within it, so an + * accepted fold is never falsely un-surfaced). A fold STILL pending after the timeout stranded → + * finishTurn un-surfaces it (redeliver, the safe direction). Guarded so a shutdown mid-wait or a + * superseding turn never commits a stale/disposed turn. */ async function commitAfterSteers(gen: number, to: InboxItem | undefined, reply?: string): Promise { - const boundary = new Promise((resolve) => setTimeout(resolve, 0)); - await Promise.race([Promise.allSettled([...pendingSteers.values()]), boundary]); + const timeout = new Promise((resolve) => setTimeout(resolve, steerSettleTimeoutMs)); + await Promise.race([Promise.allSettled([...pendingSteers.values()]), timeout]); if (stopped || gen !== generation) return; // torn down or superseded mid-wait → don't commit finishTurn(to, reply); } @@ -251,11 +268,24 @@ export function runPeerLoop({ mesh, session }: { mesh: PeerMesh; session: PeerSe return { async shutdown(): Promise { stopped = true; // block any in-flight prompt/steer/mesh callback from driving a disposed session - if (turn.inFlight) { - turn.abandon(); // leave the in-flight run on the stream → redeliver, no peer dropped - await session.abort().catch(log); // a rejected abort must not skip dispose()/the caller's mesh.stop() + // abort() and dispose() are INDEPENDENT teardown steps: each must run even if the other fails, + // and neither may propagate out of shutdown() — peer.ts awaits this before mesh.stop(), so a + // throw here would skip mesh cleanup → ghost peer. Per-call try/catch (not one wrapping block: + // that would let an abort failure skip dispose) also covers a SYNCHRONOUS throw that a bare + // .catch() would miss (a non-conforming adapter throwing before it returns its promise). + try { + if (turn.inFlight) { + turn.abandon(); // leave the in-flight run on the stream → redeliver, no peer dropped + await session.abort(); + } + } catch (e) { + log(e); // a failed abort must not skip dispose below + } + try { + await session.dispose(); // await async cleanup before the caller stops the mesh + } catch (e) { + log(e); // a failed dispose must not skip the caller's mesh.stop() } - await session.dispose().catch(log); // await async cleanup, but never let a dispose failure skip the caller's mesh.stop() }, }; } From 682cf5c770220af289a938f1d30781b219b0e57a Mon Sep 17 00:00:00 2001 From: seal Date: Thu, 9 Jul 2026 00:32:32 -0400 Subject: [PATCH 14/15] fix(connector): clear the fold-settle timer when allSettled wins MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The human-scale steerSettleTimeoutMs timer added to bound the deferred commit was never cleared on the common path (allSettled wins before it fires). An uncleared setTimeout stays ref'd on the Node event loop, so every folded turn delayed process/CLI exit by up to the timeout — negligible at the old 0ms boundary, but up to 5s per turn at the new human-scale default. Clear it in a finally so the happy path leaves no live handle; the timeout still bounds a genuinely-stuck steer. Test 18 (red on an uncleared-timer loop, green here): after a fold commits via allSettled, the active Timeout-handle count is unchanged (no leak). Test 16 now exercises the production default (5s) rather than an injected 50ms — safe only because the timer is cleared, and it makes the suite exit promptly instead of hanging on a leaked handle. Co-Authored-By: seal --- .../connector-oh-my-pi/oh-my-pi-peer.smoke.ts | 62 +++++++++++++++---- extensions/connector-oh-my-pi/src/loop.ts | 14 ++++- 2 files changed, 61 insertions(+), 15 deletions(-) diff --git a/extensions/connector-oh-my-pi/oh-my-pi-peer.smoke.ts b/extensions/connector-oh-my-pi/oh-my-pi-peer.smoke.ts index f46ebadb..031ca3db 100644 --- a/extensions/connector-oh-my-pi/oh-my-pi-peer.smoke.ts +++ b/extensions/connector-oh-my-pi/oh-my-pi-peer.smoke.ts @@ -604,27 +604,29 @@ console.log("14) strand safety: never-settling steer does not hang commit OK ✅ } console.log("15) rejecting abort does not reject shutdown (dispose/mesh.stop still run) OK ✅"); -// 16) slow-accept steer is STILL acked under a human-scale timeout (#5-killer, load-bearing): the -// whole point of B-simple. A fold whose steer() accepts after a REAL settle delay (~5ms — genuine -// work, not just one microtask) must still be awaited by the deferred commit and acked, because a -// human-scale steerSettleTimeoutMs is generous enough to wait for it. commitAfterSteers races -// allSettled(pending) against setTimeout(steerSettleTimeoutMs); with a 50ms boundary the 5ms -// accept resolves first, so allSettled wins and a2 is acked. RED at boundary 0 (== the old -// setTimeout(0) window): 0 < 5, so the timeout fires before the accept settles → finishTurn -// un-surfaces a2 → acked==="a1", a2 redelivers (exactly finding #5). The delay is a REAL 5ms (not -// setTimeout(0)) so the race is decided by MAGNITUDE, not timer-registration order — the only -// shape that actually goes red at boundary 0. This test FAILS if someone reverts the timeout to a -// value below the accept delay (e.g. 0) or drops the param. Red-green evidence in the task report. +// 16) slow-accept steer is STILL acked under the PRODUCTION-DEFAULT timeout (#5-killer, load-bearing): +// the whole point of B-simple. A fold whose steer() accepts after a REAL settle delay (~5ms — genuine +// work, not just one microtask) must still be awaited by the deferred commit and acked, because the +// production steerSettleTimeoutMs (5_000ms) is generous enough to wait for it. commitAfterSteers races +// allSettled(pending) against setTimeout(steerSettleTimeoutMs); with the prod-default 5_000ms boundary +// the 5ms accept resolves first (5 << 5_000), so allSettled wins and a2 is acked — the commit provably +// comes from allSettled, not the timeout. RED at boundary 0 (== the old setTimeout(0) window): 0 < 5, so +// the timeout fires before the accept settles → finishTurn un-surfaces a2 → acked==="a1", a2 redelivers +// (exactly finding #5). The delay is a REAL 5ms (not setTimeout(0)) so the race is decided by MAGNITUDE, +// not timer-registration order — the only shape that actually goes red at boundary 0. This test FAILS if +// someone reverts the timeout to a value below the accept delay (e.g. 0). Exercising the real 5_000ms +// default is only safe because #7 now CLEARS the settle timer when allSettled wins — with the leaked +// timer this test would hang the process ~5s at exit (see test 18). Red-green evidence in the task report. { const mesh = new FakeMesh(); const session = new StubSession(); session.steerSlowAccept = true; // the fold's steer resolves after a real ~5ms settle (setTimeout 5) mesh.items = [dm("a1", "alice", "q1")]; - runPeerLoop({ mesh, session, steerSettleTimeoutMs: 50 }); // human-scale boundary >> the 5ms accept + runPeerLoop({ mesh, session }); // PROD DEFAULT boundary 5_000ms >> the 5ms accept (no override) session.emit(START); // turn live, streaming mesh.arrive(dm("a2", "alice", "q2")); // same scope → fold; slow-accept steer resolves ~5ms later session.emit(end("ans")); // pendingSteers non-empty → deferred commit awaits the slow accept - // Wait past the slow accept's ~5ms delay but well UNDER the 50ms timeout, so the commit provably + // Wait past the slow accept's ~5ms delay but well UNDER the 5_000ms timeout, so the commit provably // comes from allSettled resolving (a2 accepted), NOT from the timeout firing. A real delay (not // drain()s): two macrotask ticks (~1ms) would assert before the 5ms accept and the deferred commit. await new Promise((r) => setTimeout(r, 20)); @@ -668,4 +670,38 @@ console.log("16) slow-accept steer is still acked under a human-scale timeout (# } console.log("17) sync-throwing abort still resolves shutdown and runs dispose (#6) OK ✅"); +// 18) the settle timer is CLEARED when allSettled wins — no leaked ref'd Timeout handle (#7, the +// event-loop-leak seam): commitAfterSteers races allSettled(pendingSteers) against +// setTimeout(steerSettleTimeoutMs). On the common path the steer settles fast so allSettled wins, +// but the 5_000ms settle timer is still ARMED — if it isn't cleared it stays ref'd on the Node +// event loop and delays process/CLI exit by up to steerSettleTimeoutMs (5s default) PER folded +// turn. The fix clears it in a `finally` on the allSettled-wins path. We fold a2 with a fast +// (~5ms) accept under the PROD default 5_000ms boundary (a leaked handle here is a 5s Timeout), +// let the deferred commit run, then assert the active 'Timeout' handle count returned to its +// pre-fold baseline — i.e. the settle timer left NO net handle. On /tmp/loop-leak-red.ts (timer +// not cleared) the count is before+1. The fold must STILL commit correctly (acked==='a1,a2'), so +// the test proves the timer is cleared WITHOUT breaking the commit. +{ + const before = process.getActiveResourcesInfo().filter((r) => r === "Timeout").length; + const mesh = new FakeMesh(); + const session = new StubSession(); + session.steerSlowAccept = true; // fold's steer resolves after a real ~5ms settle → allSettled wins + mesh.items = [dm("a1", "alice", "q1")]; + runPeerLoop({ mesh, session }); // PROD DEFAULT 5_000ms — a leaked settle timer would be a 5s handle + session.emit(START); // turn live, streaming + mesh.arrive(dm("a2", "alice", "q2")); // same scope → fold; slow-accept steer resolves ~5ms later + session.emit(end("ans")); // pendingSteers non-empty → commitAfterSteers races allSettled vs setTimeout(5_000) + // Wait past the ~5ms accept so allSettled wins the race, then one setImmediate tick so the deferred + // commit's `finally` (which clears the settle timer) has run before we sample the handle table. + await new Promise((r) => setTimeout(r, 20)); + await new Promise((r) => setImmediate(r)); + const after = process.getActiveResourcesInfo().filter((r) => r === "Timeout").length; + assert(after === before, + `the 5_000ms settle timer was CLEARED after allSettled won — no leaked Timeout handle ` + + `(before=${before}, after=${after}); on the leak-red copy after===before+1 (#7)`); + assert(ids(mesh.acked) === "a1,a2", + "the fold still committed correctly (a2 acked) — the timer is cleared WITHOUT breaking the commit"); +} +console.log("18) settle timer is cleared when allSettled wins (no event-loop leak) (#7) OK ✅"); + console.log("OH-MY-PI PEER SMOKE OK ✅"); diff --git a/extensions/connector-oh-my-pi/src/loop.ts b/extensions/connector-oh-my-pi/src/loop.ts index f315767c..23c4849d 100644 --- a/extensions/connector-oh-my-pi/src/loop.ts +++ b/extensions/connector-oh-my-pi/src/loop.ts @@ -218,8 +218,18 @@ export function runPeerLoop({ * finishTurn un-surfaces it (redeliver, the safe direction). Guarded so a shutdown mid-wait or a * superseding turn never commits a stale/disposed turn. */ async function commitAfterSteers(gen: number, to: InboxItem | undefined, reply?: string): Promise { - const timeout = new Promise((resolve) => setTimeout(resolve, steerSettleTimeoutMs)); - await Promise.race([Promise.allSettled([...pendingSteers.values()]), timeout]); + // Bound the wait on a human-scale timer, but CLEAR it when allSettled wins (the common path): + // an uncleared setTimeout(steerSettleTimeoutMs) stays ref'd on the Node event loop and delays + // process/CLI exit by up to that timeout per folded turn (harmless at 0ms, not at the 5s default). + let timer: ReturnType | undefined; + try { + const timeout = new Promise((resolve) => { + timer = setTimeout(resolve, steerSettleTimeoutMs); + }); + await Promise.race([Promise.allSettled([...pendingSteers.values()]), timeout]); + } finally { + if (timer !== undefined) clearTimeout(timer); + } if (stopped || gen !== generation) return; // torn down or superseded mid-wait → don't commit finishTurn(to, reply); } From c75e85cf54bc67920166b70027471e289ed15931 Mon Sep 17 00:00:00 2001 From: seal Date: Fri, 10 Jul 2026 20:09:01 -0400 Subject: [PATCH 15/15] chore(deps): regenerate lockfile for oh-my-pi connector on current main Rebased the connector stack onto upstream/main; regenerate pnpm-lock.yaml so it reflects @cotal-ai/oh-my-pi and its @oh-my-pi/pi-coding-agent + zod deps against the current dependency graph. Co-Authored-By: seal --- pnpm-lock.yaml | 2415 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 2413 insertions(+), 2 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 471be46a..a682cad8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -129,6 +129,22 @@ importers: specifier: ^2.9.0 version: 2.9.0 + examples/04-oh-my-pi: + dependencies: + '@cotal-ai/core': + specifier: workspace:* + version: link:../../packages/core + '@cotal-ai/manager': + specifier: workspace:* + version: link:../../implementations/manager + '@cotal-ai/oh-my-pi': + specifier: workspace:* + version: link:../../extensions/connector-oh-my-pi + devDependencies: + tsx: + specifier: ^4.22.4 + version: 4.22.4 + extensions/cmux: dependencies: '@cotal-ai/core': @@ -190,6 +206,28 @@ importers: specifier: ^0.28.0 version: 0.28.0 + extensions/connector-oh-my-pi: + dependencies: + '@cotal-ai/connector-core': + specifier: workspace:* + version: link:../connector-core + '@oh-my-pi/pi-coding-agent': + specifier: ^16.3.12 + version: 16.5.2 + tsx: + specifier: ^4.22.4 + version: 4.22.4 + devDependencies: + '@cotal-ai/core': + specifier: workspace:* + version: link:../../packages/core + esbuild: + specifier: ^0.28.0 + version: 0.28.1 + zod: + specifier: ^4.4.3 + version: 4.4.3 + extensions/connector-opencode: dependencies: '@cotal-ai/connector-core': @@ -349,14 +387,45 @@ importers: packages: + '@agentclientprotocol/sdk@1.2.1': + resolution: {integrity: sha512-jwYUdOQR7tc+Zfch53VL4JJyUNK/46q03uUTYb+PjECsmnNl94XFXOfYLJ8RBpMNidXd1rpOAVgb0vqD98xImA==} + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + '@alcalzone/ansi-tokenize@0.2.5': resolution: {integrity: sha512-3NX/MpTdroi0aKz134A6RC2Gb2iXVECN4QaAXnvCIxxIm3C3AVB1mkUe8NaaiyvOpDfsrqWhYtj+Q6a62RrTsw==} engines: {node: '>=18'} + '@ark/schema@0.56.2': + resolution: {integrity: sha512-Qx4D2JFbBWpntiHZaTv7bGG4H/M2rigiknezKg/WVyDSaLdE4YCcWAOoFB7pjjDqHbbV2OqRfntm1nnXvwMexg==} + + '@ark/util@0.56.2': + resolution: {integrity: sha512-9kU2sUE38FZEGG7l3hamYMBieLYEJh2L1mrYD2eXpT+78EnQSV1bhjxJhnxGBMSTbtwpBSDNSK+K60WvaI/DTQ==} + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.8': + resolution: {integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/runtime@7.29.7': resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} engines: {node: '>=6.9.0'} + '@babel/types@7.29.8': + resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} + engines: {node: '>=6.9.0'} + + '@bufbuild/protobuf@2.13.0': + resolution: {integrity: sha512-acq7c49vxfm1ggJ95P70TX7ABDM0vxr1SYD3BB0o0jnBLB4OAqeHyKuN+cD3w80gXEDQ2zxHpR6CUeA+O/aU9g==} + '@changesets/apply-release-plan@7.1.1': resolution: {integrity: sha512-9qPCm/rLx/xoOFXIHGB229+4GOL76S4MC+7tyOuTsR6+1jYlfFDQORdvwR5hDA6y4FL2BPt3qpbcQIS+dW85LA==} @@ -420,6 +489,16 @@ packages: resolution: {integrity: sha512-zccHj2z2oCCO4yrDiRSlFOxWerGqRiysP7a5jPK6uoI9URKAquwY42Dd/iUP8JWHxEzdRe4TlbvZCo8z1/mhrw==} engines: {node: '>= 20.12.0'} + '@colors/colors@1.6.0': + resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} + engines: {node: '>=0.1.90'} + + '@dabh/diagnostics@2.0.8': + resolution: {integrity: sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==} + + '@emnapi/runtime@1.11.3': + resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} + '@eplightning/nats-server-darwin-arm64@2.14.0': resolution: {integrity: sha512-tBCOf4anrlTnbQimh3KSz6C+0IbdJTyfTFLcol2YfGWzfmMgsXqjfrSZvnRnqMK2hWD/TJ6eIhsv3/UlfdTzEg==} engines: {bun: '>=1', deno: '>=2', node: '>=22'} @@ -774,6 +853,169 @@ packages: peerDependencies: hono: ^4 + '@huggingface/jinja@0.5.9': + resolution: {integrity: sha512-uWTG+l3VJRsl7EXxYizuL3P+cCPoc3cRqbWWRcQN0FhejRfbdq0RNhCmbY/YDtnTcz9icdLYuLDjsnz4d8JMuw==} + engines: {node: '>=18'} + + '@huggingface/tokenizers@0.1.3': + resolution: {integrity: sha512-8rF/RRT10u+kn7YuUbUg0OF30K8rjTc78aHpxT+qJ1uWSqxT1MHi8+9ltwYfkFYJzT/oS+qw3JVfHtNMGAdqyA==} + + '@huggingface/transformers@4.2.0': + resolution: {integrity: sha512-8BRCoBMH0XsWaEIamuR0LrJGAfftgHAfb2Vrffy0VKlSAE/MnUJ5/h/zTfEP3fDIft+nk7TqB8xXEyABGitBjQ==} + + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + '@inquirer/external-editor@1.0.3': resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} engines: {node: '>=18'} @@ -783,6 +1025,25 @@ packages: '@types/node': optional: true + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@kurkle/color@0.3.4': + resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==} + '@lydell/node-pty-darwin-arm64@1.2.0-beta.12': resolution: {integrity: sha512-tqaifcY9Cr41SblO1+FLzh8oxxtkNhuW9Dhl22lKme9BreYvKvxEZcdPIXTuqkJc5tagOEC4QHShKmJjLyLXLQ==} cpu: [arm64] @@ -822,6 +1083,9 @@ packages: '@manypkg/get-packages@1.1.3': resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} + '@mixmark-io/domino@2.2.0': + resolution: {integrity: sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==} + '@modelcontextprotocol/sdk@1.29.0': resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==} engines: {node: '>=18'} @@ -832,6 +1096,10 @@ packages: '@cfworker/json-schema': optional: true + '@mozilla/readability@0.6.0': + resolution: {integrity: sha512-juG5VWh4qAivzTAeMzvY9xs9HY5rAcr2E4I7tiSSCokRFi7XIZCAu92ZkSTsIj1OPceCifL3cpfteP3pDT9/QQ==} + engines: {node: '>=14.0.0'} + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4': resolution: {integrity: sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ==} cpu: [arm64] @@ -890,6 +1158,9 @@ packages: resolution: {integrity: sha512-hH7u7ejIBTFEJIZ8rIcMrHJI6wl+HhpO5sVFs1+ppmXa8RuB2+Lh1+UwTzZ5xTNNm1TKcRkYy+2qCV56qp8RxA==} engines: {node: '>= 18.0.0'} + '@nodable/entities@3.0.0': + resolution: {integrity: sha512-8L9xFeTYKhm49xfIypoe2W5wV1m/3Z58kT+7kR9A8OyFxcPduI4VmxaUMQyKYrRjUoLLSXv6EKKID5Tvj9cUVw==} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -902,6 +1173,95 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@oh-my-pi/hashline@16.5.2': + resolution: {integrity: sha512-5dUF7QuERyJoGFgcO8RkFzEtMDpMmvj3OcfQgSGk3GKQscXP2NH3BQreUmCcYsHuvz+hQ30FsEw+cKVKY9homA==} + engines: {bun: '>=1.3.14'} + + '@oh-my-pi/omp-stats@16.5.2': + resolution: {integrity: sha512-olm624iqy0Epgt6AaP85SaKu/aUQNHBLXbOnqh6IXT+WtfMyq0ShUu7J/0HfhEzmFx2qlA4a+Knaf2UZBf2Ewg==} + engines: {bun: '>=1.3.14'} + hasBin: true + + '@oh-my-pi/pi-agent-core@16.5.2': + resolution: {integrity: sha512-5aYKEvhbs/6dAaxvJbjOW6L1dr21TBk6UWTOtg13YudYpPtBbKakfwnMJUMuGsivYNNCipm0Z7uW8OWsg/HTiA==} + engines: {bun: '>=1.3.14'} + + '@oh-my-pi/pi-ai@16.5.2': + resolution: {integrity: sha512-KM3dfTxNaiztckBpMbrN/p651hmIMwbm02wqylUhJmC+XcfcZt0ZjXJ7skgUZ0ljnDCQUhAucTzWhrmMlrfQgA==} + engines: {bun: '>=1.3.14'} + + '@oh-my-pi/pi-catalog@16.5.2': + resolution: {integrity: sha512-adxH47fb4xuoOvPYupTP9nOzR/zSEr0ratNWjDC0yxYUMa3f1MCtKlk5t6a4MF0r5JljgJmUDIeTZ55vxI1QOw==} + engines: {bun: '>=1.3.14'} + + '@oh-my-pi/pi-coding-agent@16.5.2': + resolution: {integrity: sha512-qvMnnZgEyx6xC0dVfYLXuX/afp/wqBzGKu0h3RNvriEBQiobLvA8IxFjyFBokLSq1Sns2bLG0vOhSLmsL1L43Q==} + engines: {bun: '>=1.3.14'} + hasBin: true + + '@oh-my-pi/pi-mnemopi@16.5.2': + resolution: {integrity: sha512-ITrBIahERF/QW/xn7BIwmRRFIGi5ESPHVBNnR2Xws2haUQQcIBLNv9WLhNYZNwzZrDRLsm/OvObIHdv5A2gtyw==} + engines: {bun: '>=1.3.14'} + hasBin: true + peerDependencies: + fastembed: 2.1.0 + onnxruntime-node: 1.21.0 + peerDependenciesMeta: + fastembed: + optional: true + onnxruntime-node: + optional: true + + '@oh-my-pi/pi-natives-darwin-arm64@16.5.2': + resolution: {integrity: sha512-NOMIrjK8NUHhM6+Wyb2t9UqNjhztMG102Dm1xlFBK7044LVYehxWZ1K+HBpluTo7Mmp7OIcVH72/y437TU3DPw==} + engines: {bun: '>=1.3.14'} + cpu: [arm64] + os: [darwin] + + '@oh-my-pi/pi-natives-darwin-x64@16.5.2': + resolution: {integrity: sha512-ZR+8RytmuTcGchFJGmZVrqf3qiXiYKcWI7YYOOpsRu5sOHyhJQt1AQg6QHnGTTUAiMWILOOywQqDfWnS7Jm1bg==} + engines: {bun: '>=1.3.14'} + cpu: [x64] + os: [darwin] + + '@oh-my-pi/pi-natives-linux-arm64@16.5.2': + resolution: {integrity: sha512-e3NmbZwu/SMvVdQV5Gvne8jMhpXE4+izTNQRtrFrNVvWaFf7JvLgOr61UFtu0en/OdS0pt8uj5KfKlwfJRC+Yw==} + engines: {bun: '>=1.3.14'} + cpu: [arm64] + os: [linux] + + '@oh-my-pi/pi-natives-linux-x64@16.5.2': + resolution: {integrity: sha512-o0g7edSynuRGzn6AB8JykQwudX6mApqXl1riqj6hllaFnz2uBsFttHmleiBogY9jWB0AhsaHy5dQ9FWMMHxzSQ==} + engines: {bun: '>=1.3.14'} + cpu: [x64] + os: [linux] + + '@oh-my-pi/pi-natives-win32-x64@16.5.2': + resolution: {integrity: sha512-gnZNte96lmdL0sSRzOaW4k4PEkK4HdirPI85Nwhd0pwPwH2z4YBG1Udz4mUfcRdosGppgUyhbhNOWzwo9QzrIQ==} + engines: {bun: '>=1.3.14'} + cpu: [x64] + os: [win32] + + '@oh-my-pi/pi-natives@16.5.2': + resolution: {integrity: sha512-H+yPlHarVyhmxfVY/2FMIpeP/Dmsmc5TCGLbEjDDoQ1DU97AMxw4rmf/99sNetz/82r0UF0ZbUsP6e+z/UO4cA==} + engines: {bun: '>=1.3.14'} + + '@oh-my-pi/pi-tui@16.5.2': + resolution: {integrity: sha512-U2kqQ4kHHwGvgsaTtO3EnVqMwphb7GEFizQA4vWuMjjt/ckdYEKlU2b/gJI86m99R5tZ3p951rTKmKMWC+RX9Q==} + engines: {bun: '>=1.3.14'} + + '@oh-my-pi/pi-utils@16.5.2': + resolution: {integrity: sha512-Ivwibl2xdec6tQv0Wca+xznf7ZrT5OaqlbW+nZwhPzmO5OTRwz3lceYSqoBdtAjmu3bRqDG1h5vhHr+cSXl4HA==} + engines: {bun: '>=1.3.14'} + + '@oh-my-pi/pi-wire@16.5.2': + resolution: {integrity: sha512-ELdcx4Wow7zTVuj8YHfUdyhWwdalXsPB2Jtnp/h+x270Mwy4B0wUTeUNhRn9SVjPRtFgmDanAhLEjzOoLYu1uw==} + engines: {bun: '>=1.3.14'} + + '@oh-my-pi/snapcompact@16.5.2': + resolution: {integrity: sha512-ev9NHl3+cikPfydI2yMbwEoFdzAQzZ4evMHG0avC9d0NJFNE8ohRadRzceTQBlckBLot1YjM9QUu4QkBJPGrIQ==} + engines: {bun: '>=1.3.14'} + '@opencode-ai/plugin@1.16.2': resolution: {integrity: sha512-FaZhVXrbz93xsdGLCtarRDTeqFt8AkLfh8B34tFBj6G4HXVmKSgBwVXmtELKKC+08xMtawBC9hshiMbXryv6cg==} peerDependencies: @@ -919,9 +1279,168 @@ packages: '@opencode-ai/sdk@1.16.2': resolution: {integrity: sha512-Z/xZ7q79dYeE0afqIk/yFEcRNGEQFcE+H8ssYivUiy+xGZ1mGwT72jpaQZKBwPn3JH4sRCu4KA2lcktBQfcOjg==} + '@opentelemetry/api-logs@0.220.0': + resolution: {integrity: sha512-CmVa4ImJ+ynfrPMNaAXHET6Bhb44SwzmfyVJFq9ni2jgXJR/l7C6gfVFddNmHP+ZOkP9cf4f9DBe68qVLTHc9w==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/api@1.9.1': + resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/context-async-hooks@2.10.0': + resolution: {integrity: sha512-bvyMcgLEkozzSzpEEEo1OMoeQ97bxj6Qs2uN3mPrSdDvObMI1myffD/BPqcLlzZO9//d1SqQA/WPw7Cz2AiqhA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/core@2.10.0': + resolution: {integrity: sha512-/wNZ8twnEQQA4HoHu22+vcsdru6pWPWxW+7w+FlxT6Id7PE/WIbZmVKkte+PF72e0F2dnImFeHD2syyE1Mw6MQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/core@2.9.0': + resolution: {integrity: sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/exporter-trace-otlp-proto@0.220.0': + resolution: {integrity: sha512-voTAD8XgJxlK7zLkXh8EzMB09zrQr3tyY/BsnDTlDiQU/UdK58MZ63A3mUjdEDrxMjCVmBHU3WQJhRmQe+Dvzg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-exporter-base@0.220.0': + resolution: {integrity: sha512-CXYo8UD5Mn9YbgebO2EL4wejtA+gxLmLiu6HCk2KH2BR7XhFN6/6p1UlCb23DYCjeYkndevLHuejCCN1yx4+OQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-transformer@0.220.0': + resolution: {integrity: sha512-lXGrv7KXZ0gNH9SVNUaa6vv6phVYGvJxfXAlMbzbakiXru75f5MZl8Z7oqiMMQD77riVHJCFlQvbZs/VVN2/4A==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/resources@2.10.0': + resolution: {integrity: sha512-q6MMm2zhggzsHVNbabYwut+a6nbuQQe3URUoxaojM/8K1IBfwwPzvxIjNi2/lI1TFe+fMHMW9MWhrtDLEXEnkA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/resources@2.9.0': + resolution: {integrity: sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-logs@0.220.0': + resolution: {integrity: sha512-WywcTkQtv2iNmt+6y5Kcd4rzvx9bLVsBa2Nwcmg01IUaBTkTow3W4d9KE5vNBpEDtb9tp21WcRBY/lANRrApYA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.4.0 <1.10.0' + + '@opentelemetry/sdk-metrics@2.9.0': + resolution: {integrity: sha512-Xx8RGS4H5XEBl01WuCreMIpiah9cCXMbSkeuIePPdD2cUpq/vUzYmj8E/MK1OsbOc93FuAD4jfn2WOacKwLn7Q==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.9.0 <1.10.0' + + '@opentelemetry/sdk-trace-base@2.10.0': + resolution: {integrity: sha512-GuYQQT7QD2EeO8lcZLRQzcbOyhqAzL+6WWTKTU9mSUBYBazkEDl+VrQcXQhbB08OWM9anD1aHleVadzulpOaUQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace-node@2.10.0': + resolution: {integrity: sha512-GZK/G6oZyBLGlH1pUgeDch7D91KoHd2uotUGIkWCPi9GI5T9X0p4L7nNAMDR1BQjkRYoDqo+ddfVx9t5Uhys+Q==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/sdk-trace@2.10.0': + resolution: {integrity: sha512-MfQGq3GRmTh5fM/y+OjaO0vj6+luCB1XO2gfXCalKCfgKw0eHL++sm75DNweC6ohlp+aFvACqeE0fYayqdRaoQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace@2.9.0': + resolution: {integrity: sha512-sGA19HvtrrSKYsseHphluH6j3p6Xa3fqc7c7y8f/7mYWejc1lyDFcpSdD1kYa50HCLUeEo4zA5bW0pniaPszuw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/semantic-conventions@1.43.0': + resolution: {integrity: sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==} + engines: {node: '>=14'} + + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.5': + resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==} + + '@protobufjs/eventemitter@1.1.1': + resolution: {integrity: sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==} + + '@protobufjs/fetch@1.1.1': + resolution: {integrity: sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.2': + resolution: {integrity: sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==} + + '@puppeteer/browsers@3.0.6': + resolution: {integrity: sha512-B/gKoqlFkzhvzsI6jo9K1cZz9o5ypviVv/xu8CwA4grZzyVwN+XfkT+tu8T1zrauuEXv6VhS2oGX+6NL95WcKA==} + engines: {node: '>=22.12.0'} + hasBin: true + peerDependencies: + proxy-agent: '>=8.0.1' + yauzl: ^2.10.0 || ^3.4.0 + peerDependenciesMeta: + proxy-agent: + optional: true + yauzl: + optional: true + + '@puppeteer/browsers@3.1.0': + resolution: {integrity: sha512-RDLpio3fH/qrj5k4DVY6eyiN8tCS0Zovd/6jW//n605oeqkWcUjn+3k+9ZtZBnbwMpsu0F7xDIiKXvVmG5c5Bw==} + engines: {node: '>=22.12.0'} + hasBin: true + peerDependencies: + proxy-agent: '>=8.0.1' + yauzl: ^2.10.0 || ^3.4.0 + peerDependenciesMeta: + proxy-agent: + optional: true + yauzl: + optional: true + + '@sindresorhus/is@4.6.0': + resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} + engines: {node: '>=10'} + + '@so-ric/colorspace@1.1.6': + resolution: {integrity: sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==} + '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@tailwindcss/node@4.3.3': + resolution: {integrity: sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -937,9 +1456,19 @@ packages: '@types/react@19.2.17': resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} + '@types/triple-beam@1.3.5': + resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} + + '@types/turndown@5.0.6': + resolution: {integrity: sha512-ru00MoyeeouE5BX4gRL+6m/BsDfbRayOskWqUvh7CLGW+UXxHQItqALa38kKnOiZPqJrtzJUgAC2+F0rL1S4Pg==} + '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + '@xmldom/xmldom@0.8.13': + resolution: {integrity: sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==} + engines: {node: '>=10.0.0'} + '@xterm/addon-attach@0.11.0': resolution: {integrity: sha512-JboCN0QAY6ZLY/SSB/Zl2cQ5zW1Eh4X3fH7BnuR1NB7xGRhzbqU2Npmpiw/3zFlxDaU88vtKzok44JKi2L2V2Q==} peerDependencies: @@ -958,6 +1487,9 @@ packages: '@xterm/headless@5.5.0': resolution: {integrity: sha512-5xXB7kdQlFBP82ViMJTwwEc3gKCLGKR/eoxQm4zge7GPBl86tCdI0IdPJjoKd8mUSFXz5V7i/25sfsEkP4j46g==} + '@xterm/headless@6.0.0': + resolution: {integrity: sha512-5Yj1QINYCyzrZtf8OFIHi47iQtI+0qYFPHmouEfG8dHNxbZ9Tb9YGSuLcsEwj9Z+OL75GJqPyJbyoFer80a2Hw==} + '@xterm/xterm@5.5.0': resolution: {integrity: sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==} @@ -965,6 +1497,14 @@ packages: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} + adm-zip@0.5.18: + resolution: {integrity: sha512-ufJnssQGbxzLNS1Ho9bCtX4rQKCCvoVuDLHoJyc3F9dOGDB4BkWs2Ci0kv53lqocAEQ/Cbi+I2XCsNYGqVYqng==} + engines: {node: '>=12.0'} + + adm-zip@0.6.0: + resolution: {integrity: sha512-XleryMhbuksdKtofnWZ9Sk+4CUTbms4Mb/EU32SZwToAyZ5RgVos/ki8n+yr0LWHOGKuakbXTuuYNHLQjhddgg==} + engines: {node: '>=14.0'} + ajv-formats@3.0.1: resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} peerDependencies: @@ -996,16 +1536,28 @@ packages: resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} + anynum@1.0.1: + resolution: {integrity: sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==} + argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + arkregex@0.0.8: + resolution: {integrity: sha512-PJcx6G1kQTgLKPUbeYlYecDRaKq15AMSGVajlKFYWlPeJRQL+j3dKE6tyMs40HZ99djS1l9Vhl3ezAHy9JBIqQ==} + + arktype@2.2.3: + resolution: {integrity: sha512-7W+0RLTUNJiBFIIZXwOQxSR8Z273IAd6IvqBeG9+gHnQKFsIx2C0iOtGTmMrPnlX4qLXyc5+ll7A0BIj9WrbTg==} + array-union@2.1.0: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + auto-bind@5.0.1: resolution: {integrity: sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -1014,14 +1566,33 @@ packages: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + baseline-browser-mapping@2.11.12: + resolution: {integrity: sha512-r7WnVImvVCeFpf2DOXfy41aPWzeNg3H/A2X4dKmy1QL0MSyyk/e7z8ihJ3N6Nn2PsdhkVlqnEfnUE4a05P2aTA==} + engines: {node: '>=6.0.0'} + hasBin: true + better-path-resolve@1.0.0: resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} engines: {node: '>=4'} + bluebird@3.4.7: + resolution: {integrity: sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==} + body-parser@2.2.2: resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} engines: {node: '>=18'} + boolbase@2.0.0: + resolution: {integrity: sha512-DkVaaQHymRhpYEYo9x1oo7Q7B0Y6KJUsjm3c9eTyFDby4MHLBTwZ6ZDWBel5zrYxj1WsZgC5oLpiz+93MluXeA==} + engines: {node: '>=20.19.0'} + + boolean@3.2.0: + resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + brace-expansion@5.0.6: resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} engines: {node: 18 || 20 || >=22} @@ -1030,6 +1601,11 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + browserslist@4.28.7: + resolution: {integrity: sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -1042,6 +1618,13 @@ packages: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001807: + resolution: {integrity: sha512-daRXJ9EB/rdRgu7kV+TTl1YUKtlsMWblPl2sLnpg9DZae16QCegol6A1SmCE31Lm9mXC1sRWGt/krouH+/dl7Q==} + chalk@5.6.2: resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} @@ -1049,6 +1632,16 @@ packages: chardet@2.2.0: resolution: {integrity: sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==} + chart.js@4.5.1: + resolution: {integrity: sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==} + engines: {pnpm: '>=8'} + + chromium-bidi@16.0.1: + resolution: {integrity: sha512-J63PGu/9PpeCwLIcKYyzWP6yaVL5pxuBc0shlYCYM8BaAkmlwiQboXO1iNbOgSDbVklEyYFfNEcHD8oOAWacUA==} + engines: {node: '>=20.19.0 <22.0.0 || >=22.12.0'} + peerDependencies: + devtools-protocol: '*' + cli-boxes@3.0.0: resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} engines: {node: '>=10'} @@ -1061,10 +1654,30 @@ packages: resolution: {integrity: sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==} engines: {node: '>=20'} + cliui@9.0.1: + resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==} + engines: {node: '>=20'} + code-excerpt@4.0.0: resolution: {integrity: sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + color-convert@3.1.3: + resolution: {integrity: sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==} + engines: {node: '>=14.6'} + + color-name@2.1.1: + resolution: {integrity: sha512-p2FdgwVx1a9yWBHP2wI0VgShkDpgN4kZISkxdNipGBJWpa5G6b04OINlVWCyJj0JmfvcPrgqt95E9k8yvaOJFg==} + engines: {node: '>=12.20'} + + color-string@2.1.4: + resolution: {integrity: sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==} + engines: {node: '>=18'} + + color@5.0.3: + resolution: {integrity: sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==} + engines: {node: '>=18'} + commander@14.0.3: resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} engines: {node: '>=20'} @@ -1093,6 +1706,9 @@ packages: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + cors@2.8.6: resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} engines: {node: '>= 0.10'} @@ -1101,9 +1717,23 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + css-select@7.0.0: + resolution: {integrity: sha512-snmjEVXy+1LnwXdxhYvTMj1d9tOh4HxkA1YmoayVBeeyR2C14Pum7fcxJIm4SswYspVy866eYNwlH6xC3/VH5g==} + engines: {node: '>=20.19.0'} + + css-what@8.0.0: + resolution: {integrity: sha512-DH0Bqq3DNp5tdOReuNyAA+Ev4Y2GS5FMbZpeTLP6C4CDi0h5nL0BmUPChXw3o/qbHLDWHl49sbNqQVY7bMSDdw==} + engines: {node: '>=20.19.0'} + + cssom@0.5.0: + resolution: {integrity: sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==} + csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + date-fns@4.4.0: + resolution: {integrity: sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -1113,6 +1743,14 @@ packages: supports-color: optional: true + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -1125,10 +1763,59 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + detect-node@2.1.0: + resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} + + devtools-protocol@0.0.1638949: + resolution: {integrity: sha512-mXwg4Fqnv0WR4iuAT/gYUmctNkjILwXFHyZ+m7Ty1dfr0ezZt2U3gnrrJTfRobJTHoXf+IbuFvFITzLrLFjwJA==} + + diff@9.0.0: + resolution: {integrity: sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==} + engines: {node: '>=0.3.1'} + + dingbat-to-unicode@1.0.1: + resolution: {integrity: sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==} + dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + dom-serializer@3.1.1: + resolution: {integrity: sha512-4MEa38/QexBob6gFNwu+EGdWvhJ1OKuNwdYY3Y3NyeWDQfnGeDYQUDfIRzWu5B5gsv03so2Uxd28YC6zrsx3Lw==} + engines: {node: '>=20.19.0'} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domelementtype@3.0.0: + resolution: {integrity: sha512-umCQid3jKbDmVjx8jGaW7uUykm4DEUeyV21hPxNMo2nV955DhUThwqyOIDtreepP31hl84X7G5U9ZfsWvIB3Pg==} + engines: {node: '>=20.19.0'} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domhandler@6.0.1: + resolution: {integrity: sha512-gYzvtM72ZtxQO0T048kd6HWSbbGCNOUwcnfQ01cqIJ4X2IYKFFHZ5mKvrQETcFXxsRObZulDaKmy//R7TPtsBg==} + engines: {node: '>=20.19.0'} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + + domutils@4.0.2: + resolution: {integrity: sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA==} + engines: {node: '>=20.19.0'} + + dot-prop@6.0.1: + resolution: {integrity: sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==} + engines: {node: '>=10'} + + duck@0.1.12: + resolution: {integrity: sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -1139,17 +1826,39 @@ packages: effect@4.0.0-beta.74: resolution: {integrity: sha512-Yx+Kh12U+i2FmjwEfKs+ePFmpMd43RPD1oGqc/VraSS9bYzvF0Ff3PojwEFEVEewp8xc92Uxu28gTspU4qyvHA==} + electron-to-chromium@1.5.402: + resolution: {integrity: sha512-/oOpMaPT6Yg+6/1XQhyIPlzgj7Ye9zf+nNM2Uh6OcE2G2oNptWazFa+qB2Pdqqbsc9KnIDzgAntoYN0dbwOXwA==} + emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + enabled@2.0.0: + resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==} + encodeurl@2.0.0: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} + enhanced-resolve@5.24.5: + resolution: {integrity: sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==} + engines: {node: '>=10.13.0'} + enquirer@2.4.1: resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} engines: {node: '>=8.6'} + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + + entities@8.0.0: + resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==} + engines: {node: '>=20.19.0'} + environment@1.1.0: resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} engines: {node: '>=18'} @@ -1169,6 +1878,9 @@ packages: es-toolkit@1.47.0: resolution: {integrity: sha512-n1GuoD0WEQZMBk5tttoZSqwgyLx01oqa5XsBmCHwPyNe1S9jPBEmtR2pSgp2kJuWE3ciFZ6yRHmY4pM4C3OOkw==} + es6-error@4.1.1: + resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} + esbuild@0.28.0: resolution: {integrity: sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==} engines: {node: '>=18'} @@ -1179,6 +1891,10 @@ packages: engines: {node: '>=18'} hasBin: true + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} @@ -1186,6 +1902,10 @@ packages: resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} engines: {node: '>=8'} + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} @@ -1239,9 +1959,22 @@ packages: fast-wrap-ansi@0.2.2: resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==} + fast-xml-builder@1.3.0: + resolution: {integrity: sha512-F74cZEdCvuw9P41GAC3rod4X04jjWGM1JPEv/GWSqFTWLsdyMSBMBMlm9Hk3GLBgLBbdBNY8yee0pQh2RBVESQ==} + + fast-xml-parser@5.10.1: + resolution: {integrity: sha512-IEMIf7298kXuZSRFoGfMYrl7is8LpavODgbNz1cwIudv7KwVFnuU+UsMporfq6PD6aXSlawZlARiA3UywCTfMw==} + hasBin: true + fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + fecha@4.2.3: + resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} + + file-stream-rotator@0.6.1: + resolution: {integrity: sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ==} + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -1257,6 +1990,12 @@ packages: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} + flatbuffers@25.9.23: + resolution: {integrity: sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==} + + fn.name@1.1.0: + resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} + forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -1281,6 +2020,13 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + generative-bayesian-network@2.1.88: + resolution: {integrity: sha512-kxbW6CCsiEAVdBYPont/6ZVOa47Pyfv5ldYFIvj8wmOx9uQOZ4c8wdR0jEf2pE6DeVuIAfmolm+91bYne6/3uA==} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + get-east-asian-width@1.6.0: resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} engines: {node: '>=18'} @@ -1301,6 +2047,14 @@ packages: resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} engines: {node: 18 || 20 || >=22} + global-agent@3.0.0: + resolution: {integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==} + engines: {node: '>=10.0'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + globby@11.1.0: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} @@ -1312,6 +2066,17 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + guid-typescript@1.0.9: + resolution: {integrity: sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==} + + handlebars@4.7.9: + resolution: {integrity: sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==} + engines: {node: '>=0.4.7'} + hasBin: true + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} @@ -1320,10 +2085,20 @@ packages: resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} engines: {node: '>= 0.4'} + header-generator@2.1.88: + resolution: {integrity: sha512-12GkTL1CDaPTQ6gkd8TPwxNtn7t3wSExnWU9qgWfEDh8IqpD1NAVcvzfHphIzULez8WP2DK9YQsDegajjDdTKQ==} + engines: {node: '>=16.0.0'} + hono@4.12.23: resolution: {integrity: sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA==} engines: {node: '>=16.9.0'} + html-escaper@3.0.3: + resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==} + + htmlparser2@10.1.0: + resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==} + http-errors@2.0.1: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} @@ -1340,6 +2115,9 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + indent-string@5.0.0: resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} engines: {node: '>=12'} @@ -1393,20 +2171,38 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-obj@2.0.0: + resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} + engines: {node: '>=8'} + is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + is-subdir@1.2.0: resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==} engines: {node: '>=4'} + is-unsafe@2.0.0: + resolution: {integrity: sha512-2LdV822R+wmI86unXA93WCFpL6g+av8ynWk0nrHyJqGop5VoocYsSLFgN8jrfalT6iGeLNM4KXuVSsULP53kEA==} + is-windows@1.0.2: resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} engines: {node: '>=0.10.0'} + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + jose@6.2.3: resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} @@ -1424,6 +2220,9 @@ packages: json-schema-typed@8.0.2: resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} @@ -1432,20 +2231,152 @@ packages: jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + jszip@3.10.1: + resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} + kubernetes-types@1.30.0: resolution: {integrity: sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q==} + kuler@2.0.0: + resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} + + lie@3.3.0: + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + linkedom@0.18.13: + resolution: {integrity: sha512-ES/o9qotMpzpN2MHs+Iq/JcVoOj8Fa5wiQYrTdFpvAnwXL0g66XHHUc9WUMk6nAlBtGsFQ24ne+SYnvnaQ2FSw==} + engines: {node: '>=16'} + peerDependencies: + canvas: '>= 2' + peerDependenciesMeta: + canvas: + optional: true + locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} + lodash.isequal@4.5.0: + resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead. + lodash.startcase@4.4.0: resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + logform@2.7.0: + resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==} + engines: {node: '>= 12.0.0'} + + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + + lop@0.4.2: + resolution: {integrity: sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw==} + lru-cache@11.5.1: resolution: {integrity: sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==} engines: {node: 20 || >=22} + lru-cache@11.5.2: + resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} + engines: {node: 20 || >=22} + + lucide-react@1.29.0: + resolution: {integrity: sha512-Xs9QFG5+9sNX04MdKVT4++umA+hJ2qsJVlRlRWHQ7qZobXgMiNHSpZ5eZm8JUoGCdNyoEdXoEwa8HVr0DNjOQg==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + mammoth@1.12.0: + resolution: {integrity: sha512-cwnK1RIcRdDMi2HRx2EXGYlxqIEh0Oo3bLhorgnsVJi2UkbX1+jKxuBNR9PC5+JaX7EkmJxFPmo6mjLpqShI2w==} + engines: {node: '>=12.0.0'} + hasBin: true + + marked@18.0.9: + resolution: {integrity: sha512-/Sa4qiiHZxf0/FQdBBowr9q4r10krCwMvpK48FUBdXdUXScDxiQGR9zCPrFgRVR5LU3iySOiIjy09ZQvADir1w==} + engines: {node: '>= 20'} + hasBin: true + + matcher@3.0.0: + resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==} + engines: {node: '>=10'} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -1482,10 +2413,23 @@ packages: resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} engines: {node: 18 || 20 || >=22} + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + minipass@7.1.3: resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} engines: {node: '>=16 || 14 >=14.17'} + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + + modern-tar@0.7.7: + resolution: {integrity: sha512-t9VmxaqrmANnEOBhpSDI6HD192Ge48k8vmWqQQL7hSFEqHEYwZbbsu49+aKLWZeRvFs3j1pMhXOqqF4kPlvjkQ==} + engines: {node: '>=18.0.0'} + + moment@2.30.1: + resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} + mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -1503,26 +2447,48 @@ packages: multipasta@0.2.7: resolution: {integrity: sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA==} + mupdf@1.28.0: + resolution: {integrity: sha512-ACUnbpECaQ5JLq04pwd89lS+0IGMest5qL5tb08g9TAR7bDtfqflHEkb2Xm3o4rvC/szguLiV+WEbW9kstj8Sg==} + negotiator@1.0.0: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + node-gyp-build-optional-packages@5.2.2: resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==} hasBin: true + node-releases@2.0.53: + resolution: {integrity: sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==} + engines: {node: '>=18'} + normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} + nth-check@3.0.1: + resolution: {integrity: sha512-GX0gsdbGVCgnRgbeGaubfjpBXyYRWOOCVeYh08bSQvDZqxz5ndXs1OTfAt/h36G1xvI94YIspsI0sVFqAV9+RQ==} + engines: {node: '>=20.19.0'} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + object-inspect@1.13.4: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -1530,13 +2496,36 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + one-time@1.0.0: + resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==} + onetime@5.1.2: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} + onnxruntime-common@1.24.0-dev.20251116-b39e144322: + resolution: {integrity: sha512-BOoomdHYmNRL5r4iQ4bMvsl2t0/hzVQ3OM3PHD0gxeXu1PmggqBv3puZicEUVOA3AtHHYmqZtjMj9FOfGrATTw==} + + onnxruntime-common@1.24.3: + resolution: {integrity: sha512-GeuPZO6U/LBJXvwdaqHbuUmoXiEdeCjWi/EG7Y1HNnDwJYuk6WUbNXpF6luSUY8yASul3cmUlLGrCCL1ZgVXqA==} + + onnxruntime-node@1.24.3: + resolution: {integrity: sha512-JH7+czbc8ALA819vlTgcV+Q214/+VjGeBHDjX81+ZCD0PCVCIFGFNtT0V4sXG/1JXypKPgScQcB3ij/hk3YnTg==} + os: [win32, darwin, linux] + + onnxruntime-web@1.26.0-dev.20260416-b7804b056c: + resolution: {integrity: sha512-MD6Ss4GSpQBo6zqoJzyT9LRbKYs7x/JVN23FT24EcEvlqF4VuzPOeH6X38orZPKHQDbprn7K+SBpu0/mj2CQiw==} + + option@0.2.4: + resolution: {integrity: sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==} + outdent@0.5.0: resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} + ow@0.28.2: + resolution: {integrity: sha512-dD4UpyBh/9m4X2NVjA+73/ZPBRF+uF4zIMFvvQsabMiEK8x41L3rQ8EENOi35kyyoaJwNxEeJcP6Fj1H4U409Q==} + engines: {node: '>=12'} + p-filter@2.1.0: resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==} engines: {node: '>=8'} @@ -1560,6 +2549,9 @@ packages: package-manager-detector@0.2.11: resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==} + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -1572,6 +2564,14 @@ packages: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} + path-expression-matcher@1.6.2: + resolution: {integrity: sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ==} + engines: {node: '>=14.0.0'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -1602,15 +2602,29 @@ packages: resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} engines: {node: '>=16.20.0'} + platform@1.3.6: + resolution: {integrity: sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==} + prettier@2.8.8: resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} engines: {node: '>=10.13.0'} hasBin: true + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + protobufjs@7.6.5: + resolution: {integrity: sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==} + engines: {node: '>=12.0.0'} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} + puppeteer-core@25.3.0: + resolution: {integrity: sha512-fm+wpUr2oigH1PXZvwgATrM2tYWHMDG8ASzTEe9uukCye4X5Ldx1K5BTHPFKITrIWvQQAQ256d1NpbEveBcKjA==} + engines: {node: '>=22.12.0'} + pure-rand@8.4.0: resolution: {integrity: sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A==} @@ -1632,6 +2646,17 @@ packages: resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} engines: {node: '>= 0.10'} + react-chartjs-2@5.3.1: + resolution: {integrity: sha512-h5IPXKg9EXpjoBzUfyWJvllMjG2mQ4EiuHQFhms/AjUm0XSZHhyRy2xVmLXHKrtcdrPO4mnGqRtYoD0vp95A0A==} + peerDependencies: + chart.js: ^4.1.1 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + react-dom@19.2.7: + resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==} + peerDependencies: + react: ^19.2.7 + react-reconciler@0.33.0: resolution: {integrity: sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA==} engines: {node: '>=0.10.0'} @@ -1646,6 +2671,13 @@ packages: resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} engines: {node: '>=6'} + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -1662,6 +2694,10 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + roarr@2.15.4: + resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==} + engines: {node: '>=8.0'} + router@2.2.0: resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} engines: {node: '>= 18'} @@ -1669,6 +2705,12 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-stable-stringify@2.5.0: resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} engines: {node: '>=10'} @@ -1679,6 +2721,9 @@ packages: scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + semver-compare@1.0.0: + resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==} + semver@7.8.5: resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} engines: {node: '>=10'} @@ -1688,13 +2733,24 @@ packages: resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} engines: {node: '>= 18'} + serialize-error@7.0.1: + resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==} + engines: {node: '>=10'} + serve-static@2.2.1: resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} engines: {node: '>= 18'} + setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -1703,6 +2759,39 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + sherpa-onnx-darwin-arm64@1.13.4: + resolution: {integrity: sha512-QcYKzyrTzGSx6aKCD6hUODgRS1LetqfG57Z/+i5LCyfMlrgCvDc1lRcl9cdB+TozBsLha9QwLTlI0vmDcf5JKg==} + cpu: [arm64] + os: [darwin] + + sherpa-onnx-darwin-x64@1.13.4: + resolution: {integrity: sha512-6RGeis9K9gV/UQWOgd6Rf3iqXr2/YsBQswxHaCR4hrYkHfEIpHMfFmRWLt6nJJCOWgYW2xFxEd9yzjrafAV/Pw==} + cpu: [x64] + os: [darwin] + + sherpa-onnx-linux-arm64@1.13.4: + resolution: {integrity: sha512-RMjMRqT82BgTXypNNGmLe6ZFYhc3WEvnAGl3DdkK7qB/kuXwkL3iHhV31wAecbnWPsnEpUoD+8cFovWSBzsCuw==} + cpu: [arm64] + os: [linux] + + sherpa-onnx-linux-x64@1.13.4: + resolution: {integrity: sha512-WZh5NCkGPFHHpYSd78iN4OnmxQeSTGyt9uZskH+im/NFHQ7elQ7B0sLzCMeRpvJxiIKvd9C6WxIJ4hYaxClfsQ==} + cpu: [x64] + os: [linux] + + sherpa-onnx-node@1.13.2: + resolution: {integrity: sha512-uIH6SA5Or4pb8HlCYWB3K54XkMtzdef4/tkw1amtIf8GB1tt6hQLpur9p2jSFNfTYRyzZ8XrXofxefXQ0A7EUA==} + + sherpa-onnx-win-ia32@1.13.4: + resolution: {integrity: sha512-/JbPjldrfNv+t+uIS3MlkuhfIf5l3FHUGkRC2oRXgjRqOaVmEyP3vLlQ7dTa4J7raG5oB8c3GoPjuSWSqT9GOQ==} + cpu: [ia32] + os: [win32] + + sherpa-onnx-win-x64@1.13.4: + resolution: {integrity: sha512-R0PWby1VxC14TDZPq7GcfSyXSY6SAFO8Y4JwdCdqouFmeXkZ1L7Is9m98C9KxQ0dN7ZtDzhAmE/43FUs/elXRQ==} + cpu: [x64] + os: [win32] + side-channel-list@1.0.1: resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} engines: {node: '>= 0.4'} @@ -1737,12 +2826,26 @@ packages: resolution: {integrity: sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==} engines: {node: '>=20'} + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + spawndamnit@3.0.1: resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==} sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + sprintf-js@1.1.3: + resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} + + stack-trace@0.0.10: + resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} + stack-utils@2.0.6: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} @@ -1759,6 +2862,12 @@ packages: resolution: {integrity: sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==} engines: {node: '>=20'} + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -1771,10 +2880,20 @@ packages: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} + strnum@2.4.1: + resolution: {integrity: sha512-M9eUSMT2dCB2cTNPG7UYj6KuK7RJR2SN2+yCV/fTW3xzTCS6EaGZ5pSMgDIjB7r8zSfTGk+dvvn9rTjpVS9Mwg==} + tagged-tag@1.0.0: resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} engines: {node: '>=20'} + tailwindcss@4.3.3: + resolution: {integrity: sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==} + + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + term-size@2.2.1: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} @@ -1783,6 +2902,9 @@ packages: resolution: {integrity: sha512-avMLDQpUI9I5XFrklECw1ZEUPJhqzcwSWsyyI8blhRLT+8N1jLJWLWWYQpB2q2xthq8xDvjZPISVh53T/+CLYQ==} engines: {node: '>=18'} + text-hex@1.0.0: + resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -1795,6 +2917,10 @@ packages: resolution: {integrity: sha512-EBJnVBr3dTXdA89WVFoAIPUqkBjxPMwRqsfuo1r240tKFHXv3zgca4+NJib/h6TyvGF7vOawz0jGuryJCdNHrw==} engines: {node: '>=20'} + triple-beam@1.4.1: + resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} + engines: {node: '>= 14.0.0'} + ts-json-schema-generator@2.9.0: resolution: {integrity: sha512-NR5ZE108uiPtBHBJNGnhwoUaUx5vWTDJzDFG9YlRoqxPU76n+5FClRh92dcGgysbe1smRmYalM9Saj97GW1J4Q==} engines: {node: '>=22.0.0'} @@ -1808,9 +2934,20 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + turndown-plugin-gfm@1.0.2: + resolution: {integrity: sha512-vwz9tfvF7XN/jE0dGoBei3FXWuvll78ohzCZQuOb+ZjWrs3a0XhQVomJEb2Qh4VHTPNRO4GPZh0V7VRbiWwkRg==} + + turndown@7.2.4: + resolution: {integrity: sha512-I8yFsfRzmzK0WV1pNNOA4A7y4RDfFxPRxb3t+e3ui14qSGOxGtiSP6GjeX+Y6CHb7HYaFj7ECUD7VE5kQMZWGQ==} + engines: {node: '>=18', npm: '>=9'} + tweetnacl@1.0.3: resolution: {integrity: sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==} + type-fest@0.13.1: + resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==} + engines: {node: '>=10'} + type-fest@5.7.0: resolution: {integrity: sha512-1URUxUqfHFM1c+zfSPsa3gnkO7Aq21qyH75SIduNYz4SzY964rn1X2vCMQaHSHhktiw+0kPa2iyb6PUpXqB6Vg==} engines: {node: '>=20'} @@ -1819,11 +2956,25 @@ packages: resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} engines: {node: '>= 18'} + typed-query-selector@2.12.2: + resolution: {integrity: sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==} + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} hasBin: true + uglify-js@3.19.3: + resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} + engines: {node: '>=0.8.0'} + hasBin: true + + uhyphen@0.2.0: + resolution: {integrity: sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==} + + underscore@1.13.8: + resolution: {integrity: sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==} + undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} @@ -1838,14 +2989,30 @@ packages: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + uuid@14.0.0: resolution: {integrity: sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==} hasBin: true + vali-date@1.0.0: + resolution: {integrity: sha512-sgECfZthyaCKW10N0fm27cg8HYTFK5qMWgypqkXMQ4Wbl/zZKx7xZICgcoxIIE+WFAP/MBL2EFwC/YvLxw3Zeg==} + engines: {node: '>=0.10.0'} + vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + webdriver-bidi-protocol@0.4.2: + resolution: {integrity: sha512-VSV+fzfChirL3e7jay2yUC7B4HQCGtEWEg/MSSQbK+qWbqeGlRLlXTzPpYr3XGUvbpDHumWZBJxgesg4N7dbtA==} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -1855,6 +3022,23 @@ packages: resolution: {integrity: sha512-U89AsyEeAsyoF0zVJBkG9zBgekjgjK7yk9sje3F4IQpXBJ10TF6ByLlIfjMhcmHMJgHZI4KHt4rdNfktzxIAMA==} engines: {node: '>=20'} + winston-daily-rotate-file@5.0.0: + resolution: {integrity: sha512-JDjiXXkM5qvwY06733vf09I2wnMXpZEhxEVOSPenZMii+g7pcDcTBt2MRugnoi8BwVSuCT2jfRXBUy+n1Zz/Yw==} + engines: {node: '>=8'} + peerDependencies: + winston: ^3 + + winston-transport@4.9.0: + resolution: {integrity: sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==} + engines: {node: '>= 12.0.0'} + + winston@3.19.0: + resolution: {integrity: sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==} + engines: {node: '>= 12.0.0'} + + wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + wrap-ansi@9.0.2: resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} engines: {node: '>=18'} @@ -1874,11 +3058,31 @@ packages: utf-8-validate: optional: true + xml-naming@0.3.0: + resolution: {integrity: sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ==} + engines: {node: '>=16.0.0'} + + xmlbuilder@10.1.1: + resolution: {integrity: sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==} + engines: {node: '>=4.0'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + yaml@2.9.0: resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} engines: {node: '>= 14.6'} hasBin: true + yargs-parser@22.0.0: + resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + + yargs@18.1.0: + resolution: {integrity: sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + yoga-layout@3.2.1: resolution: {integrity: sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==} @@ -1887,6 +3091,9 @@ packages: peerDependencies: zod: ^3.25.28 || ^4 + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zod@4.1.8: resolution: {integrity: sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ==} @@ -1895,13 +3102,38 @@ packages: snapshots: + '@agentclientprotocol/sdk@1.2.1(zod@4.4.3)': + dependencies: + zod: 4.4.3 + '@alcalzone/ansi-tokenize@0.2.5': dependencies: ansi-styles: 6.2.3 is-fullwidth-code-point: 5.1.0 + '@ark/schema@0.56.2': + dependencies: + '@ark/util': 0.56.2 + + '@ark/util@0.56.2': {} + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/parser@7.29.8': + dependencies: + '@babel/types': 7.29.8 + '@babel/runtime@7.29.7': {} + '@babel/types@7.29.8': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@bufbuild/protobuf@2.13.0': {} + '@changesets/apply-release-plan@7.1.1': dependencies: '@changesets/config': 3.1.4 @@ -2057,6 +3289,19 @@ snapshots: fast-wrap-ansi: 0.2.2 sisteransi: 1.0.5 + '@colors/colors@1.6.0': {} + + '@dabh/diagnostics@2.0.8': + dependencies: + '@so-ric/colorspace': 1.1.6 + enabled: 2.0.0 + kuler: 2.0.0 + + '@emnapi/runtime@1.11.3': + dependencies: + tslib: 2.8.1 + optional: true + '@eplightning/nats-server-darwin-arm64@2.14.0': optional: true @@ -2235,13 +3480,146 @@ snapshots: dependencies: hono: 4.12.23 - '@inquirer/external-editor@1.0.3(@types/node@22.20.0)': + '@huggingface/jinja@0.5.9': + optional: true + + '@huggingface/tokenizers@0.1.3': + optional: true + + '@huggingface/transformers@4.2.0': + dependencies: + '@huggingface/jinja': 0.5.9 + '@huggingface/tokenizers': 0.1.3 + onnxruntime-node: 1.24.3 + onnxruntime-web: 1.26.0-dev.20260416-b7804b056c + sharp: 0.34.5 + optional: true + + '@img/colour@1.1.0': + optional: true + + '@img/sharp-darwin-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + optional: true + + '@img/sharp-darwin-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true + + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true + + '@img/sharp-linux-ppc64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + optional: true + + '@img/sharp-linux-riscv64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + optional: true + + '@img/sharp-linux-s390x@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + optional: true + + '@img/sharp-linux-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-wasm32@0.34.5': + dependencies: + '@emnapi/runtime': 1.11.3 + optional: true + + '@img/sharp-win32-arm64@0.34.5': + optional: true + + '@img/sharp-win32-ia32@0.34.5': + optional: true + + '@img/sharp-win32-x64@0.34.5': + optional: true + + '@inquirer/external-editor@1.0.3(@types/node@22.20.0)': dependencies: chardet: 2.2.0 iconv-lite: 0.7.2 optionalDependencies: '@types/node': 22.20.0 + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@kurkle/color@0.3.4': {} + '@lydell/node-pty-darwin-arm64@1.2.0-beta.12': optional: true @@ -2285,6 +3663,8 @@ snapshots: globby: 11.1.0 read-yaml-file: 1.1.0 + '@mixmark-io/domino@2.2.0': {} + '@modelcontextprotocol/sdk@1.29.0(zod@4.4.3)': dependencies: '@hono/node-server': 1.19.14(hono@4.12.23) @@ -2307,6 +3687,8 @@ snapshots: transitivePeerDependencies: - supports-color + '@mozilla/readability@0.6.0': {} + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4': optional: true @@ -2359,6 +3741,8 @@ snapshots: '@nats-io/nkeys': 2.0.3 '@nats-io/nuid': 3.0.0 + '@nodable/entities@3.0.0': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -2371,6 +3755,155 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 + '@oh-my-pi/hashline@16.5.2': + dependencies: + diff: 9.0.0 + lru-cache: 11.5.2 + + '@oh-my-pi/omp-stats@16.5.2': + dependencies: + '@oh-my-pi/pi-ai': 16.5.2 + '@oh-my-pi/pi-catalog': 16.5.2 + '@oh-my-pi/pi-utils': 16.5.2 + '@tailwindcss/node': 4.3.3 + chart.js: 4.5.1 + date-fns: 4.4.0 + lucide-react: 1.29.0(react@19.2.7) + react: 19.2.7 + react-chartjs-2: 5.3.1(chart.js@4.5.1)(react@19.2.7) + react-dom: 19.2.7(react@19.2.7) + tailwindcss: 4.3.3 + + '@oh-my-pi/pi-agent-core@16.5.2': + dependencies: + '@oh-my-pi/pi-ai': 16.5.2 + '@oh-my-pi/pi-catalog': 16.5.2 + '@oh-my-pi/pi-natives': 16.5.2 + '@oh-my-pi/pi-utils': 16.5.2 + '@oh-my-pi/pi-wire': 16.5.2 + '@oh-my-pi/snapcompact': 16.5.2 + '@opentelemetry/api': 1.9.1 + + '@oh-my-pi/pi-ai@16.5.2': + dependencies: + '@bufbuild/protobuf': 2.13.0 + '@oh-my-pi/pi-catalog': 16.5.2 + '@oh-my-pi/pi-utils': 16.5.2 + '@oh-my-pi/pi-wire': 16.5.2 + arktype: 2.2.3 + zod: 4.4.3 + + '@oh-my-pi/pi-catalog@16.5.2': + dependencies: + '@bufbuild/protobuf': 2.13.0 + '@oh-my-pi/pi-utils': 16.5.2 + arktype: 2.2.3 + zod: 4.4.3 + + '@oh-my-pi/pi-coding-agent@16.5.2': + dependencies: + '@agentclientprotocol/sdk': 1.2.1(zod@4.4.3) + '@babel/parser': 7.29.8 + '@mozilla/readability': 0.6.0 + '@oh-my-pi/hashline': 16.5.2 + '@oh-my-pi/omp-stats': 16.5.2 + '@oh-my-pi/pi-agent-core': 16.5.2 + '@oh-my-pi/pi-ai': 16.5.2 + '@oh-my-pi/pi-catalog': 16.5.2 + '@oh-my-pi/pi-mnemopi': 16.5.2 + '@oh-my-pi/pi-natives': 16.5.2 + '@oh-my-pi/pi-tui': 16.5.2 + '@oh-my-pi/pi-utils': 16.5.2 + '@oh-my-pi/pi-wire': 16.5.2 + '@oh-my-pi/snapcompact': 16.5.2 + '@opentelemetry/api': 1.9.1 + '@opentelemetry/context-async-hooks': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-trace-otlp-proto': 0.220.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-node': 2.10.0(@opentelemetry/api@1.9.1) + '@puppeteer/browsers': 3.1.0 + '@types/turndown': 5.0.6 + '@xterm/headless': 6.0.0 + arktype: 2.2.3 + chalk: 5.6.2 + diff: 9.0.0 + fast-xml-parser: 5.10.1 + handlebars: 4.7.9 + header-generator: 2.1.88 + linkedom: 0.18.13 + lru-cache: 11.5.2 + mammoth: 1.12.0 + mupdf: 1.28.0 + puppeteer-core: 25.3.0 + turndown: 7.2.4 + turndown-plugin-gfm: 1.0.2 + zod: 4.4.3 + optionalDependencies: + '@huggingface/transformers': 4.2.0 + sherpa-onnx-node: 1.13.2 + transitivePeerDependencies: + - bufferutil + - canvas + - fastembed + - onnxruntime-node + - proxy-agent + - utf-8-validate + - yauzl + + '@oh-my-pi/pi-mnemopi@16.5.2': + dependencies: + '@oh-my-pi/pi-ai': 16.5.2 + '@oh-my-pi/pi-catalog': 16.5.2 + '@oh-my-pi/pi-utils': 16.5.2 + lru-cache: 11.5.2 + + '@oh-my-pi/pi-natives-darwin-arm64@16.5.2': + optional: true + + '@oh-my-pi/pi-natives-darwin-x64@16.5.2': + optional: true + + '@oh-my-pi/pi-natives-linux-arm64@16.5.2': + optional: true + + '@oh-my-pi/pi-natives-linux-x64@16.5.2': + optional: true + + '@oh-my-pi/pi-natives-win32-x64@16.5.2': + optional: true + + '@oh-my-pi/pi-natives@16.5.2': + optionalDependencies: + '@oh-my-pi/pi-natives-darwin-arm64': 16.5.2 + '@oh-my-pi/pi-natives-darwin-x64': 16.5.2 + '@oh-my-pi/pi-natives-linux-arm64': 16.5.2 + '@oh-my-pi/pi-natives-linux-x64': 16.5.2 + '@oh-my-pi/pi-natives-win32-x64': 16.5.2 + + '@oh-my-pi/pi-tui@16.5.2': + dependencies: + '@oh-my-pi/pi-natives': 16.5.2 + '@oh-my-pi/pi-utils': 16.5.2 + lru-cache: 11.5.2 + marked: 18.0.9 + + '@oh-my-pi/pi-utils@16.5.2': + dependencies: + '@oh-my-pi/pi-natives': 16.5.2 + handlebars: 4.7.9 + winston: 3.19.0 + winston-daily-rotate-file: 5.0.0(winston@3.19.0) + + '@oh-my-pi/pi-wire@16.5.2': {} + + '@oh-my-pi/snapcompact@16.5.2': + dependencies: + '@oh-my-pi/pi-ai': 16.5.2 + '@oh-my-pi/pi-natives': 16.5.2 + '@oh-my-pi/pi-utils': 16.5.2 + '@oh-my-pi/pi-wire': 16.5.2 + '@opencode-ai/plugin@1.16.2': dependencies: '@opencode-ai/sdk': 1.16.2 @@ -2381,8 +3914,166 @@ snapshots: dependencies: cross-spawn: 7.0.6 + '@opentelemetry/api-logs@0.220.0': + dependencies: + '@opentelemetry/api': 1.9.1 + + '@opentelemetry/api@1.9.1': {} + + '@opentelemetry/context-async-hooks@2.10.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + + '@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/semantic-conventions': 1.43.0 + + '@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/semantic-conventions': 1.43.0 + + '@opentelemetry/exporter-trace-otlp-proto@0.220.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.220.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.220.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace': 2.9.0(@opentelemetry/api@1.9.1) + + '@opentelemetry/otlp-exporter-base@0.220.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.220.0(@opentelemetry/api@1.9.1) + + '@opentelemetry/otlp-transformer@0.220.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.220.0 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-logs': 0.220.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace': 2.9.0(@opentelemetry/api@1.9.1) + + '@opentelemetry/resources@2.10.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.43.0 + + '@opentelemetry/resources@2.9.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.43.0 + + '@opentelemetry/sdk-logs@0.220.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.220.0 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.43.0 + + '@opentelemetry/sdk-metrics@2.9.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.1) + + '@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.43.0 + + '@opentelemetry/sdk-trace-node@2.10.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/context-async-hooks': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.10.0(@opentelemetry/api@1.9.1) + + '@opentelemetry/sdk-trace@2.10.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.43.0 + + '@opentelemetry/sdk-trace@2.9.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.43.0 + + '@opentelemetry/semantic-conventions@1.43.0': {} + + '@protobufjs/aspromise@1.1.2': + optional: true + + '@protobufjs/base64@1.1.2': + optional: true + + '@protobufjs/codegen@2.0.5': + optional: true + + '@protobufjs/eventemitter@1.1.1': + optional: true + + '@protobufjs/fetch@1.1.1': + dependencies: + '@protobufjs/aspromise': 1.1.2 + optional: true + + '@protobufjs/float@1.0.2': + optional: true + + '@protobufjs/path@1.1.2': + optional: true + + '@protobufjs/pool@1.1.0': + optional: true + + '@protobufjs/utf8@1.1.2': + optional: true + + '@puppeteer/browsers@3.0.6': + dependencies: + modern-tar: 0.7.7 + yargs: 18.1.0 + + '@puppeteer/browsers@3.1.0': + dependencies: + modern-tar: 0.7.7 + yargs: 18.1.0 + + '@sindresorhus/is@4.6.0': {} + + '@so-ric/colorspace@1.1.6': + dependencies: + color: 5.0.3 + text-hex: 1.0.0 + '@standard-schema/spec@1.1.0': {} + '@tailwindcss/node@4.3.3': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.24.5 + jiti: 2.7.0 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.3.3 + '@types/json-schema@7.0.15': {} '@types/node@12.20.55': {} @@ -2399,10 +4090,16 @@ snapshots: dependencies: csstype: 3.2.3 + '@types/triple-beam@1.3.5': {} + + '@types/turndown@5.0.6': {} + '@types/ws@8.18.1': dependencies: '@types/node': 26.0.0 + '@xmldom/xmldom@0.8.13': {} + '@xterm/addon-attach@0.11.0(@xterm/xterm@5.5.0)': dependencies: '@xterm/xterm': 5.5.0 @@ -2417,6 +4114,8 @@ snapshots: '@xterm/headless@5.5.0': {} + '@xterm/headless@6.0.0': {} + '@xterm/xterm@5.5.0': {} accepts@2.0.0: @@ -2424,6 +4123,11 @@ snapshots: mime-types: 3.0.2 negotiator: 1.0.0 + adm-zip@0.5.18: + optional: true + + adm-zip@0.6.0: {} + ajv-formats@3.0.1(ajv@8.20.0): optionalDependencies: ajv: 8.20.0 @@ -2447,22 +4151,42 @@ snapshots: ansi-styles@6.2.3: {} + anynum@1.0.1: {} + argparse@1.0.10: dependencies: sprintf-js: 1.0.3 argparse@2.0.1: {} + arkregex@0.0.8: + dependencies: + '@ark/util': 0.56.2 + + arktype@2.2.3: + dependencies: + '@ark/schema': 0.56.2 + '@ark/util': 0.56.2 + arkregex: 0.0.8 + array-union@2.1.0: {} + async@3.2.6: {} + auto-bind@5.0.1: {} balanced-match@4.0.4: {} + base64-js@1.5.1: {} + + baseline-browser-mapping@2.11.12: {} + better-path-resolve@1.0.0: dependencies: is-windows: 1.0.2 + bluebird@3.4.7: {} + body-parser@2.2.2: dependencies: bytes: 3.1.2 @@ -2477,6 +4201,11 @@ snapshots: transitivePeerDependencies: - supports-color + boolbase@2.0.0: {} + + boolean@3.2.0: + optional: true + brace-expansion@5.0.6: dependencies: balanced-match: 4.0.4 @@ -2485,6 +4214,14 @@ snapshots: dependencies: fill-range: 7.1.1 + browserslist@4.28.7: + dependencies: + baseline-browser-mapping: 2.11.12 + caniuse-lite: 1.0.30001807 + electron-to-chromium: 1.5.402 + node-releases: 2.0.53 + update-browserslist-db: 1.2.3(browserslist@4.28.7) + bytes@3.1.2: {} call-bind-apply-helpers@1.0.2: @@ -2497,10 +4234,24 @@ snapshots: call-bind-apply-helpers: 1.0.2 get-intrinsic: 1.3.0 + callsites@3.1.0: {} + + caniuse-lite@1.0.30001807: {} + chalk@5.6.2: {} chardet@2.2.0: {} + chart.js@4.5.1: + dependencies: + '@kurkle/color': 0.3.4 + + chromium-bidi@16.0.1(devtools-protocol@0.0.1638949): + dependencies: + devtools-protocol: 0.0.1638949 + mitt: 3.0.1 + zod: 3.25.76 + cli-boxes@3.0.0: {} cli-cursor@4.0.0: @@ -2512,10 +4263,31 @@ snapshots: slice-ansi: 8.0.0 string-width: 8.2.1 + cliui@9.0.1: + dependencies: + string-width: 7.2.0 + strip-ansi: 7.2.0 + wrap-ansi: 9.0.2 + code-excerpt@4.0.0: dependencies: convert-to-spaces: 2.0.1 + color-convert@3.1.3: + dependencies: + color-name: 2.1.1 + + color-name@2.1.1: {} + + color-string@2.1.4: + dependencies: + color-name: 2.1.1 + + color@5.0.3: + dependencies: + color-convert: 3.1.3 + color-string: 2.1.4 + commander@14.0.3: {} content-disposition@1.1.0: {} @@ -2530,6 +4302,8 @@ snapshots: cookie@0.7.2: {} + core-util-is@1.0.3: {} + cors@2.8.6: dependencies: object-assign: 4.1.1 @@ -2541,23 +4315,103 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + css-select@7.0.0: + dependencies: + boolbase: 2.0.0 + css-what: 8.0.0 + domhandler: 6.0.1 + domutils: 4.0.2 + nth-check: 3.0.1 + + css-what@8.0.0: {} + + cssom@0.5.0: {} + csstype@3.2.3: {} + date-fns@4.4.0: {} + debug@4.4.3: dependencies: ms: 2.1.3 + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + optional: true + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + optional: true + depd@2.0.0: {} detect-indent@6.1.0: {} - detect-libc@2.1.2: + detect-libc@2.1.2: {} + + detect-node@2.1.0: optional: true + devtools-protocol@0.0.1638949: {} + + diff@9.0.0: {} + + dingbat-to-unicode@1.0.1: {} + dir-glob@3.0.1: dependencies: path-type: 4.0.0 + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + dom-serializer@3.1.1: + dependencies: + domelementtype: 3.0.0 + domhandler: 6.0.1 + entities: 8.0.0 + + domelementtype@2.3.0: {} + + domelementtype@3.0.0: {} + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domhandler@6.0.1: + dependencies: + domelementtype: 3.0.0 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + + domutils@4.0.2: + dependencies: + dom-serializer: 3.1.1 + domelementtype: 3.0.0 + domhandler: 6.0.1 + + dot-prop@6.0.1: + dependencies: + is-obj: 2.0.0 + + duck@0.1.12: + dependencies: + underscore: 1.13.8 + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -2579,15 +4433,30 @@ snapshots: uuid: 14.0.0 yaml: 2.9.0 + electron-to-chromium@1.5.402: {} + emoji-regex@10.6.0: {} + enabled@2.0.0: {} + encodeurl@2.0.0: {} + enhanced-resolve@5.24.5: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + enquirer@2.4.1: dependencies: ansi-colors: 4.1.3 strip-ansi: 6.0.1 + entities@4.5.0: {} + + entities@7.0.1: {} + + entities@8.0.0: {} + environment@1.1.0: {} es-define-property@1.0.1: {} @@ -2600,6 +4469,9 @@ snapshots: es-toolkit@1.47.0: {} + es6-error@4.1.1: + optional: true + esbuild@0.28.0: optionalDependencies: '@esbuild/aix-ppc64': 0.28.0 @@ -2658,10 +4530,15 @@ snapshots: '@esbuild/win32-ia32': 0.28.1 '@esbuild/win32-x64': 0.28.1 + escalade@3.2.0: {} + escape-html@1.0.3: {} escape-string-regexp@2.0.0: {} + escape-string-regexp@4.0.0: + optional: true + esprima@4.0.1: {} etag@1.8.1: {} @@ -2738,10 +4615,30 @@ snapshots: dependencies: fast-string-width: 3.0.2 + fast-xml-builder@1.3.0: + dependencies: + path-expression-matcher: 1.6.2 + xml-naming: 0.3.0 + + fast-xml-parser@5.10.1: + dependencies: + '@nodable/entities': 3.0.0 + fast-xml-builder: 1.3.0 + is-unsafe: 2.0.0 + path-expression-matcher: 1.6.2 + strnum: 2.4.1 + xml-naming: 0.3.0 + fastq@1.20.1: dependencies: reusify: 1.1.0 + fecha@4.2.3: {} + + file-stream-rotator@0.6.1: + dependencies: + moment: 2.30.1 + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -2764,6 +4661,11 @@ snapshots: locate-path: 5.0.0 path-exists: 4.0.0 + flatbuffers@25.9.23: + optional: true + + fn.name@1.1.0: {} + forwarded@0.2.0: {} fresh@2.0.0: {} @@ -2785,6 +4687,13 @@ snapshots: function-bind@1.1.2: {} + generative-bayesian-network@2.1.88: + dependencies: + adm-zip: 0.6.0 + tslib: 2.8.1 + + get-caller-file@2.0.5: {} + get-east-asian-width@1.6.0: {} get-intrinsic@1.3.0: @@ -2815,6 +4724,22 @@ snapshots: minipass: 7.1.3 path-scurry: 2.0.2 + global-agent@3.0.0: + dependencies: + boolean: 3.2.0 + es6-error: 4.1.1 + matcher: 3.0.0 + roarr: 2.15.4 + semver: 7.8.5 + serialize-error: 7.0.1 + optional: true + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + optional: true + globby@11.1.0: dependencies: array-union: 2.1.0 @@ -2828,14 +4753,47 @@ snapshots: graceful-fs@4.2.11: {} + guid-typescript@1.0.9: + optional: true + + handlebars@4.7.9: + dependencies: + minimist: 1.2.8 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.19.3 + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + optional: true + has-symbols@1.1.0: {} hasown@2.0.4: dependencies: function-bind: 1.1.2 + header-generator@2.1.88: + dependencies: + browserslist: 4.28.7 + generative-bayesian-network: 2.1.88 + ow: 0.28.2 + tslib: 2.8.1 + hono@4.12.23: {} + html-escaper@3.0.3: {} + + htmlparser2@10.1.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 7.0.1 + http-errors@2.0.1: dependencies: depd: 2.0.0 @@ -2852,6 +4810,8 @@ snapshots: ignore@5.3.2: {} + immediate@3.0.6: {} + indent-string@5.0.0: {} inherits@2.0.4: {} @@ -2910,16 +4870,26 @@ snapshots: is-number@7.0.0: {} + is-obj@2.0.0: {} + is-promise@4.0.0: {} + is-stream@2.0.1: {} + is-subdir@1.2.0: dependencies: better-path-resolve: 1.0.0 + is-unsafe@2.0.0: {} + is-windows@1.0.2: {} + isarray@1.0.0: {} + isexe@2.0.0: {} + jiti@2.7.0: {} + jose@6.2.3: {} js-yaml@3.14.2: @@ -2935,22 +4905,145 @@ snapshots: json-schema-typed@8.0.2: {} + json-stringify-safe@5.0.1: + optional: true + json5@2.2.3: {} jsonfile@4.0.0: optionalDependencies: graceful-fs: 4.2.11 + jszip@3.10.1: + dependencies: + lie: 3.3.0 + pako: 1.0.11 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + kubernetes-types@1.30.0: {} + kuler@2.0.0: {} + + lie@3.3.0: + dependencies: + immediate: 3.0.6 + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + linkedom@0.18.13: + dependencies: + css-select: 7.0.0 + cssom: 0.5.0 + html-escaper: 3.0.3 + htmlparser2: 10.1.0 + uhyphen: 0.2.0 + locate-path@5.0.0: dependencies: p-locate: 4.1.0 + lodash.isequal@4.5.0: {} + lodash.startcase@4.4.0: {} + logform@2.7.0: + dependencies: + '@colors/colors': 1.6.0 + '@types/triple-beam': 1.3.5 + fecha: 4.2.3 + ms: 2.1.3 + safe-stable-stringify: 2.5.0 + triple-beam: 1.4.1 + + long@5.3.2: + optional: true + + lop@0.4.2: + dependencies: + duck: 0.1.12 + option: 0.2.4 + underscore: 1.13.8 + lru-cache@11.5.1: {} + lru-cache@11.5.2: {} + + lucide-react@1.29.0(react@19.2.7): + dependencies: + react: 19.2.7 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + mammoth@1.12.0: + dependencies: + '@xmldom/xmldom': 0.8.13 + argparse: 1.0.10 + base64-js: 1.5.1 + bluebird: 3.4.7 + dingbat-to-unicode: 1.0.1 + jszip: 3.10.1 + lop: 0.4.2 + path-is-absolute: 1.0.1 + underscore: 1.13.8 + xmlbuilder: 10.1.1 + + marked@18.0.9: {} + + matcher@3.0.0: + dependencies: + escape-string-regexp: 4.0.0 + optional: true + math-intrinsics@1.1.0: {} media-typer@1.1.0: {} @@ -2976,8 +5069,16 @@ snapshots: dependencies: brace-expansion: 5.0.6 + minimist@1.2.8: {} + minipass@7.1.3: {} + mitt@3.0.1: {} + + modern-tar@0.7.7: {} + + moment@2.30.1: {} + mri@1.2.0: {} ms@2.1.3: {} @@ -3000,19 +5101,34 @@ snapshots: multipasta@0.2.7: {} + mupdf@1.28.0: {} + negotiator@1.0.0: {} + neo-async@2.6.2: {} + node-gyp-build-optional-packages@5.2.2: dependencies: detect-libc: 2.1.2 optional: true + node-releases@2.0.53: {} + normalize-path@3.0.0: {} + nth-check@3.0.1: + dependencies: + boolbase: 2.0.0 + object-assign@4.1.1: {} + object-hash@3.0.0: {} + object-inspect@1.13.4: {} + object-keys@1.1.1: + optional: true + on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -3021,12 +5137,49 @@ snapshots: dependencies: wrappy: 1.0.2 + one-time@1.0.0: + dependencies: + fn.name: 1.1.0 + onetime@5.1.2: dependencies: mimic-fn: 2.1.0 + onnxruntime-common@1.24.0-dev.20251116-b39e144322: + optional: true + + onnxruntime-common@1.24.3: + optional: true + + onnxruntime-node@1.24.3: + dependencies: + adm-zip: 0.5.18 + global-agent: 3.0.0 + onnxruntime-common: 1.24.3 + optional: true + + onnxruntime-web@1.26.0-dev.20260416-b7804b056c: + dependencies: + flatbuffers: 25.9.23 + guid-typescript: 1.0.9 + long: 5.3.2 + onnxruntime-common: 1.24.0-dev.20251116-b39e144322 + platform: 1.3.6 + protobufjs: 7.6.5 + optional: true + + option@0.2.4: {} + outdent@0.5.0: {} + ow@0.28.2: + dependencies: + '@sindresorhus/is': 4.6.0 + callsites: 3.1.0 + dot-prop: 6.0.1 + lodash.isequal: 4.5.0 + vali-date: 1.0.0 + p-filter@2.1.0: dependencies: p-map: 2.1.0 @@ -3047,12 +5200,18 @@ snapshots: dependencies: quansync: 0.2.11 + pako@1.0.11: {} + parseurl@1.3.3: {} patch-console@2.0.0: {} path-exists@4.0.0: {} + path-expression-matcher@1.6.2: {} + + path-is-absolute@1.0.1: {} + path-key@3.1.1: {} path-scurry@2.0.2: @@ -3072,13 +5231,47 @@ snapshots: pkce-challenge@5.0.1: {} + platform@1.3.6: + optional: true + prettier@2.8.8: {} + process-nextick-args@2.0.1: {} + + protobufjs@7.6.5: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.5 + '@protobufjs/eventemitter': 1.1.1 + '@protobufjs/fetch': 1.1.1 + '@protobufjs/float': 1.0.2 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.2 + '@types/node': 22.20.0 + long: 5.3.2 + optional: true + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 ipaddr.js: 1.9.1 + puppeteer-core@25.3.0: + dependencies: + '@puppeteer/browsers': 3.0.6 + chromium-bidi: 16.0.1(devtools-protocol@0.0.1638949) + devtools-protocol: 0.0.1638949 + typed-query-selector: 2.12.2 + webdriver-bidi-protocol: 0.4.2 + ws: 8.21.0 + transitivePeerDependencies: + - bufferutil + - proxy-agent + - utf-8-validate + - yauzl + pure-rand@8.4.0: {} qs@6.15.2: @@ -3098,6 +5291,16 @@ snapshots: iconv-lite: 0.7.2 unpipe: 1.0.0 + react-chartjs-2@5.3.1(chart.js@4.5.1)(react@19.2.7): + dependencies: + chart.js: 4.5.1 + react: 19.2.7 + + react-dom@19.2.7(react@19.2.7): + dependencies: + react: 19.2.7 + scheduler: 0.27.0 + react-reconciler@0.33.0(react@19.2.7): dependencies: react: 19.2.7 @@ -3112,6 +5315,22 @@ snapshots: pify: 4.0.1 strip-bom: 3.0.0 + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + require-from-string@2.0.2: {} resolve-from@5.0.0: {} @@ -3123,6 +5342,16 @@ snapshots: reusify@1.1.0: {} + roarr@2.15.4: + dependencies: + boolean: 3.2.0 + detect-node: 2.1.0 + globalthis: 1.0.4 + json-stringify-safe: 5.0.1 + semver-compare: 1.0.0 + sprintf-js: 1.1.3 + optional: true + router@2.2.0: dependencies: debug: 4.4.3 @@ -3137,12 +5366,19 @@ snapshots: dependencies: queue-microtask: 1.2.3 + safe-buffer@5.1.2: {} + + safe-buffer@5.2.1: {} + safe-stable-stringify@2.5.0: {} safer-buffer@2.1.2: {} scheduler@0.27.0: {} + semver-compare@1.0.0: + optional: true + semver@7.8.5: {} send@1.2.1: @@ -3161,6 +5397,11 @@ snapshots: transitivePeerDependencies: - supports-color + serialize-error@7.0.1: + dependencies: + type-fest: 0.13.1 + optional: true + serve-static@2.2.1: dependencies: encodeurl: 2.0.0 @@ -3170,14 +5411,76 @@ snapshots: transitivePeerDependencies: - supports-color + setimmediate@1.0.5: {} + setprototypeof@1.2.0: {} + sharp@0.34.5: + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.8.5 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 + optional: true + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 shebang-regex@3.0.0: {} + sherpa-onnx-darwin-arm64@1.13.4: + optional: true + + sherpa-onnx-darwin-x64@1.13.4: + optional: true + + sherpa-onnx-linux-arm64@1.13.4: + optional: true + + sherpa-onnx-linux-x64@1.13.4: + optional: true + + sherpa-onnx-node@1.13.2: + optionalDependencies: + sherpa-onnx-darwin-arm64: 1.13.4 + sherpa-onnx-darwin-x64: 1.13.4 + sherpa-onnx-linux-arm64: 1.13.4 + sherpa-onnx-linux-x64: 1.13.4 + sherpa-onnx-win-ia32: 1.13.4 + sherpa-onnx-win-x64: 1.13.4 + optional: true + + sherpa-onnx-win-ia32@1.13.4: + optional: true + + sherpa-onnx-win-x64@1.13.4: + optional: true + side-channel-list@1.0.1: dependencies: es-errors: 1.3.0 @@ -3219,6 +5522,10 @@ snapshots: ansi-styles: 6.2.3 is-fullwidth-code-point: 5.1.0 + source-map-js@1.2.1: {} + + source-map@0.6.1: {} + spawndamnit@3.0.1: dependencies: cross-spawn: 7.0.6 @@ -3226,6 +5533,11 @@ snapshots: sprintf-js@1.0.3: {} + sprintf-js@1.1.3: + optional: true + + stack-trace@0.0.10: {} + stack-utils@2.0.6: dependencies: escape-string-regexp: 2.0.0 @@ -3243,6 +5555,14 @@ snapshots: get-east-asian-width: 1.6.0 strip-ansi: 7.2.0 + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -3253,12 +5573,22 @@ snapshots: strip-bom@3.0.0: {} + strnum@2.4.1: + dependencies: + anynum: 1.0.1 + tagged-tag@1.0.0: {} + tailwindcss@4.3.3: {} + + tapable@2.3.3: {} + term-size@2.2.1: {} terminal-size@4.0.1: {} + text-hex@1.0.0: {} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -3267,6 +5597,8 @@ snapshots: toml@4.1.1: {} + triple-beam@1.4.1: {} + ts-json-schema-generator@2.9.0: dependencies: '@types/json-schema': 7.0.15 @@ -3286,8 +5618,17 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + turndown-plugin-gfm@1.0.2: {} + + turndown@7.2.4: + dependencies: + '@mixmark-io/domino': 2.2.0 + tweetnacl@1.0.3: {} + type-fest@0.13.1: + optional: true + type-fest@5.7.0: dependencies: tagged-tag: 1.0.0 @@ -3298,8 +5639,17 @@ snapshots: media-typer: 1.1.0 mime-types: 3.0.2 + typed-query-selector@2.12.2: {} + typescript@5.9.3: {} + uglify-js@3.19.3: + optional: true + + uhyphen@0.2.0: {} + + underscore@1.13.8: {} + undici-types@6.21.0: {} undici-types@8.3.0: {} @@ -3308,10 +5658,22 @@ snapshots: unpipe@1.0.0: {} + update-browserslist-db@1.2.3(browserslist@4.28.7): + dependencies: + browserslist: 4.28.7 + escalade: 3.2.0 + picocolors: 1.1.1 + + util-deprecate@1.0.2: {} + uuid@14.0.0: {} + vali-date@1.0.0: {} + vary@1.1.2: {} + webdriver-bidi-protocol@0.4.2: {} + which@2.0.2: dependencies: isexe: 2.0.0 @@ -3320,6 +5682,36 @@ snapshots: dependencies: string-width: 8.2.1 + winston-daily-rotate-file@5.0.0(winston@3.19.0): + dependencies: + file-stream-rotator: 0.6.1 + object-hash: 3.0.0 + triple-beam: 1.4.1 + winston: 3.19.0 + winston-transport: 4.9.0 + + winston-transport@4.9.0: + dependencies: + logform: 2.7.0 + readable-stream: 3.6.2 + triple-beam: 1.4.1 + + winston@3.19.0: + dependencies: + '@colors/colors': 1.6.0 + '@dabh/diagnostics': 2.0.8 + async: 3.2.6 + is-stream: 2.0.1 + logform: 2.7.0 + one-time: 1.0.0 + readable-stream: 3.6.2 + safe-stable-stringify: 2.5.0 + stack-trace: 0.0.10 + triple-beam: 1.4.1 + winston-transport: 4.9.0 + + wordwrap@1.0.0: {} + wrap-ansi@9.0.2: dependencies: ansi-styles: 6.2.3 @@ -3330,14 +5722,33 @@ snapshots: ws@8.21.0: {} + xml-naming@0.3.0: {} + + xmlbuilder@10.1.1: {} + + y18n@5.0.8: {} + yaml@2.9.0: {} + yargs-parser@22.0.0: {} + + yargs@18.1.0: + dependencies: + cliui: 9.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + string-width: 8.2.1 + y18n: 5.0.8 + yargs-parser: 22.0.0 + yoga-layout@3.2.1: {} zod-to-json-schema@3.25.2(zod@4.4.3): dependencies: zod: 4.4.3 + zod@3.25.76: {} + zod@4.1.8: {} zod@4.4.3: {}