Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
c111a25
feat(oh-my-pi): Cotal connector for oh-my-pi — headless peer + intera…
rigel-mintaka Jul 3, 2026
cbd9301
fix(cli): declare @cotal-ai/delivery so bin/cotal.ts resolves
rigel-mintaka Jul 3, 2026
25bd139
fix(oh-my-pi): only join the mesh from an interactive session
rigel-mintaka Jul 3, 2026
fe463e2
fix(connector): stop mesh reconnect churn from flooding + corrupting …
rigel-mintaka Jul 4, 2026
8175e2f
fix(connector-core): roll in InboxTurn ack-on-surface so the branch b…
rigel-mintaka Jul 8, 2026
9557f49
feat(oh-my-pi): track pi-coding-agent 16.3.12, migrate tools to zod
rigel-mintaka Jul 8, 2026
3516909
fix(connector): address #5 review findings — delivery, shutdown, CI
rigel-mintaka Jul 8, 2026
4820cd4
fix(connector): close 3 shutdown/steer races found reviewing the fix
rigel-mintaka Jul 8, 2026
3a36478
fix(connector-core): run the new hermetic smokes in CI
rigel-mintaka Jul 8, 2026
d8cac38
test(connector): red-green the steer-ack + dispose-shutdown races
rigel-mintaka Jul 8, 2026
2e42d48
fix(connector): ack folds on steer settle, guard late callbacks + tea…
rigel-mintaka Jul 9, 2026
10f94ff
fix(connector): swallow abort() failure in shutdown so mesh.stop stil…
rigel-mintaka Jul 9, 2026
90d27e0
fix(connector): bound the fold-settle wait by a human-scale timeout
rigel-mintaka Jul 9, 2026
682cf5c
fix(connector): clear the fold-settle timer when allSettled wins
rigel-mintaka Jul 9, 2026
c75e85c
chore(deps): regenerate lockfile for oh-my-pi connector on current main
rigel-mintaka Jul 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions examples/04-oh-my-pi/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
28 changes: 28 additions & 0 deletions examples/04-oh-my-pi/src/manager.ts
Original file line number Diff line number Diff line change
@@ -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<void>(() => {});
8 changes: 8 additions & 0 deletions examples/04-oh-my-pi/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist"
},
"include": ["src"]
}
120 changes: 120 additions & 0 deletions extensions/connector-core/inbox-turn.smoke.ts
Original file line number Diff line number Diff line change
@@ -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 ✅");
3 changes: 3 additions & 0 deletions extensions/connector-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
80 changes: 80 additions & 0 deletions extensions/connector-core/smoke/reconnect-log.smoke.ts
Original file line number Diff line number Diff line change
@@ -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: level ?? "info" }));
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);
Loading