agent-yes 1.194.0 → 1.196.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lab/ui/sw.js CHANGED
@@ -21,7 +21,7 @@ async function pickClient() {
21
21
  return all.find((c) => !new URL(c.url).pathname.startsWith(BASE + "p/")) || all[0] || null;
22
22
  }
23
23
 
24
- async function proxyPreview(request, src, port, rest) {
24
+ async function proxyPreview(request, src, port, rest, allowInject) {
25
25
  const client = await pickClient();
26
26
  if (!client) return new Response("open the agent-yes console to preview", { status: 502 });
27
27
  const headers = {};
@@ -35,23 +35,40 @@ async function proxyPreview(request, src, port, rest) {
35
35
  let resolved = false;
36
36
  mc.port1.onmessage = (ev) => {
37
37
  const msg = ev.data;
38
- if (msg.type === "head") {
39
- const stream = new ReadableStream({
40
- start(controller) {
41
- mc.port1.onmessage = (e2) => {
42
- const m2 = e2.data;
43
- if (m2.type === "body") controller.enqueue(new Uint8Array(m2.chunk));
44
- else if (m2.type === "end") controller.close();
45
- else if (m2.type === "error") controller.error(new Error(m2.message));
46
- };
47
- },
48
- });
49
- resolved = true;
50
- resolve(new Response(stream, { status: msg.status, statusText: msg.statusText, headers: msg.headers }));
51
- } else if (msg.type === "error" && !resolved) {
52
- resolved = true;
53
- resolve(new Response("preview tunnel error: " + msg.message, { status: 502 }));
38
+ if (msg.type !== "head") {
39
+ if (msg.type === "error" && !resolved) {
40
+ resolved = true;
41
+ resolve(new Response("preview tunnel error: " + msg.message, { status: 502 }));
42
+ }
43
+ return;
54
44
  }
45
+ resolved = true;
46
+ const headers = new Headers(msg.headers);
47
+ const ct = headers.get("content-type") || "";
48
+ // HTML document navigations (the /p/ path itself, not app subresources):
49
+ // buffer, strip CSP, and inject a window.WebSocket shim so the app's own
50
+ // sockets (Vite HMR) also ride the P2P tunnel. Everything else streams.
51
+ if (allowInject && ct.includes("text/html")) {
52
+ const chunks = [];
53
+ mc.port1.onmessage = (e2) => {
54
+ const m2 = e2.data;
55
+ if (m2.type === "body") chunks.push(new Uint8Array(m2.chunk));
56
+ else if (m2.type === "end") resolve(injectBootstrap(chunks, msg, headers, src, Number(port)));
57
+ else if (m2.type === "error") resolve(new Response("preview error: " + m2.message, { status: 502 }));
58
+ };
59
+ return;
60
+ }
61
+ const stream = new ReadableStream({
62
+ start(controller) {
63
+ mc.port1.onmessage = (e2) => {
64
+ const m2 = e2.data;
65
+ if (m2.type === "body") controller.enqueue(new Uint8Array(m2.chunk));
66
+ else if (m2.type === "end") controller.close();
67
+ else if (m2.type === "error") controller.error(new Error(m2.message));
68
+ };
69
+ },
70
+ });
71
+ resolve(new Response(stream, { status: msg.status, statusText: msg.statusText, headers }));
55
72
  };
56
73
  client.postMessage(
57
74
  { type: "ay-preview-fetch", src, port: Number(port), method: request.method, path: rest, headers, body },
@@ -60,6 +77,37 @@ async function proxyPreview(request, src, port, rest) {
60
77
  });
61
78
  }
62
79
 
80
+ // Concatenate the buffered HTML chunks, strip CSP, and inject a bootstrap
81
+ // <script> that replaces window.WebSocket with the parent console's tunneled
82
+ // shim — so a dev server's own sockets (Vite HMR) ride the P2P tunnel too. Runs
83
+ // first (right after <head>) so the override is in place before app scripts.
84
+ function injectBootstrap(chunks, msg, headers, src, port) {
85
+ let total = 0;
86
+ for (const c of chunks) total += c.byteLength;
87
+ const all = new Uint8Array(total);
88
+ let off = 0;
89
+ for (const c of chunks) {
90
+ all.set(c, off);
91
+ off += c.byteLength;
92
+ }
93
+ const raw = new TextDecoder().decode(all);
94
+ const boot =
95
+ "<script>(function(){try{var mk=window.parent&&window.parent.__ayMakeWS;" +
96
+ "if(mk)window.WebSocket=mk(" +
97
+ JSON.stringify(src) +
98
+ "," +
99
+ JSON.stringify(port) +
100
+ ");}catch(e){}})();</" +
101
+ "script>";
102
+ const html = /<head[^>]*>/i.test(raw)
103
+ ? raw.replace(/<head[^>]*>/i, (m) => m + boot)
104
+ : boot + raw;
105
+ headers.delete("content-security-policy");
106
+ headers.delete("content-security-policy-report-only");
107
+ headers.delete("content-length");
108
+ return new Response(html, { status: msg.status, statusText: msg.statusText, headers });
109
+ }
110
+
63
111
  const SHELL = [
64
112
  "./",
65
113
  "./index.html",
@@ -98,7 +146,9 @@ self.addEventListener("fetch", (e) => {
98
146
  const own = PREVIEW.exec(url.pathname);
99
147
  if (own) {
100
148
  const rest = (own[3] || "/") + url.search;
101
- e.respondWith(proxyPreview(req, decodeURIComponent(own[1]), own[2], rest));
149
+ // Navigations to the /p/ root/route may be an HTML doc → allow WS-shim
150
+ // injection; app subresources (below) never inject.
151
+ e.respondWith(proxyPreview(req, decodeURIComponent(own[1]), own[2], rest, true));
102
152
  return;
103
153
  }
104
154
  e.respondWith(maybePreviewFromClient(e, req, url));
@@ -112,7 +162,9 @@ async function maybePreviewFromClient(e, req, url) {
112
162
  const client = await self.clients.get(clientId).catch(() => null);
113
163
  const cm = client && PREVIEW.exec(new URL(client.url).pathname);
114
164
  if (cm) {
115
- return proxyPreview(req, decodeURIComponent(cm[1]), cm[2], url.pathname + url.search);
165
+ // Subresources (Vite's /@vite/client, module chunks, etc.) never get the
166
+ // WS shim injected — only the top-level /p/ navigation does.
167
+ return proxyPreview(req, decodeURIComponent(cm[1]), cm[2], url.pathname + url.search, false);
116
168
  }
117
169
  }
118
170
  return shellFetch(req, url);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-yes",
3
- "version": "1.194.0",
3
+ "version": "1.196.0",
4
4
  "description": "A wrapper tool that automates interactions with various AI CLI tools by automatically handling common prompts and responses.",
5
5
  "keywords": [
6
6
  "ai",
@@ -0,0 +1,145 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
2
+ import { mkdtemp, readFile, rm } from "fs/promises";
3
+ import { tmpdir } from "os";
4
+ import path from "path";
5
+ import {
6
+ mailboxPath,
7
+ partyMatches,
8
+ readMailbox,
9
+ recordInbox,
10
+ recordMessage,
11
+ recordOutbox,
12
+ type MessageRecord,
13
+ } from "./messageLog.ts";
14
+
15
+ function makeRecord(over: Partial<MessageRecord> = {}): MessageRecord {
16
+ return {
17
+ at: 1_000,
18
+ nonce: "abcd",
19
+ from: { pid: 11, cli: "claude", cwd: "/from", agent_id: "agent-A" },
20
+ to: { pid: 22, cli: "codex", cwd: "/to", agent_id: "agent-B" },
21
+ body: "hello",
22
+ confirmed: true,
23
+ wrapped: true,
24
+ ...over,
25
+ };
26
+ }
27
+
28
+ describe("messageLog", () => {
29
+ let dir: string;
30
+ let prevCwd: string;
31
+
32
+ beforeEach(async () => {
33
+ dir = await mkdtemp(path.join(tmpdir(), "msglog-"));
34
+ prevCwd = process.cwd();
35
+ });
36
+
37
+ afterEach(async () => {
38
+ process.chdir(prevCwd);
39
+ await rm(dir, { recursive: true, force: true });
40
+ });
41
+
42
+ it("mailboxPath colocates under <cwd>/.agent-yes", () => {
43
+ expect(mailboxPath("/x", "inbox")).toBe(path.join("/x", ".agent-yes", "inbox.jsonl"));
44
+ expect(mailboxPath("/x", "outbox")).toBe(path.join("/x", ".agent-yes", "outbox.jsonl"));
45
+ });
46
+
47
+ it("records to sender outbox and recipient inbox", async () => {
48
+ const from = path.join(dir, "sender");
49
+ const to = path.join(dir, "recipient");
50
+ const rec = makeRecord({
51
+ from: { pid: 11, cli: "claude", cwd: from, agent_id: "A" },
52
+ to: { pid: 22, cli: "codex", cwd: to, agent_id: "B" },
53
+ });
54
+ await recordMessage(rec);
55
+
56
+ const outbox = await readMailbox(from, "outbox");
57
+ const inbox = await readMailbox(to, "inbox");
58
+ expect(outbox).toHaveLength(1);
59
+ expect(inbox).toHaveLength(1);
60
+ expect(outbox[0]!.body).toBe("hello");
61
+ expect(inbox[0]!.to.agent_id).toBe("B");
62
+ // The sender's inbox and recipient's outbox stay empty.
63
+ expect(await readMailbox(from, "inbox")).toHaveLength(0);
64
+ expect(await readMailbox(to, "outbox")).toHaveLength(0);
65
+ });
66
+
67
+ it("writes a human sender's outbox under process.cwd()", async () => {
68
+ process.chdir(dir);
69
+ const to = path.join(dir, "recipient");
70
+ await recordMessage(makeRecord({ from: null, to: { pid: 22, cli: "codex", cwd: to } }));
71
+ const outbox = await readMailbox(dir, "outbox");
72
+ expect(outbox).toHaveLength(1);
73
+ expect(outbox[0]!.from).toBeNull();
74
+ });
75
+
76
+ it("readMailbox skips corrupt lines and returns empty for a missing file", async () => {
77
+ expect(await readMailbox(dir, "inbox")).toEqual([]);
78
+ const from = path.join(dir, "s");
79
+ await recordMessage(makeRecord({ from: { pid: 1, cli: "c", cwd: from } }));
80
+ // Corrupt the file with a partial line; the good record still parses.
81
+ const p = mailboxPath(from, "outbox");
82
+ const raw = await readFile(p, "utf-8");
83
+ const { appendFile } = await import("fs/promises");
84
+ await appendFile(p, "{ not json\n");
85
+ expect(raw.trim().split("\n")).toHaveLength(1);
86
+ expect(await readMailbox(from, "outbox")).toHaveLength(1);
87
+ });
88
+
89
+ it("recordOutbox writes only the sender's outbox (remote peer's cwd untouched)", async () => {
90
+ const from = path.join(dir, "local");
91
+ const to = path.join(dir, "remote");
92
+ await recordOutbox(
93
+ makeRecord({
94
+ from: { pid: 1, cli: "claude", cwd: from, agent_id: "A" },
95
+ to: { pid: 2, cli: "codex", cwd: to, agent_id: "B" },
96
+ remote: "http://host:8080",
97
+ wrapped: false,
98
+ }),
99
+ );
100
+ expect(await readMailbox(from, "outbox")).toHaveLength(1);
101
+ // The remote peer's cwd is on another host — nothing is written there.
102
+ expect(await readMailbox(to, "inbox")).toHaveLength(0);
103
+ expect((await readMailbox(from, "outbox"))[0]!.remote).toBe("http://host:8080");
104
+ });
105
+
106
+ it("recordInbox writes only the recipient's inbox (remote sender's cwd untouched)", async () => {
107
+ const from = path.join(dir, "remote-sender");
108
+ const to = path.join(dir, "local-recipient");
109
+ await recordInbox(
110
+ makeRecord({
111
+ from: { pid: 1, cli: "claude", cwd: from, agent_id: "A" },
112
+ to: { pid: 2, cli: "codex", cwd: to, agent_id: "B" },
113
+ remote: "wire",
114
+ wrapped: false,
115
+ }),
116
+ );
117
+ expect(await readMailbox(to, "inbox")).toHaveLength(1);
118
+ expect(await readMailbox(from, "outbox")).toHaveLength(0);
119
+ });
120
+
121
+ it("preserves the kind tag for key/select events", async () => {
122
+ const from = path.join(dir, "kfrom");
123
+ const to = path.join(dir, "kto");
124
+ await recordMessage(
125
+ makeRecord({
126
+ from: { pid: 1, cli: "bash", cwd: from, agent_id: "A" },
127
+ to: { pid: 2, cli: "bash", cwd: to, agent_id: "B" },
128
+ kind: "key",
129
+ body: "down down enter",
130
+ wrapped: false,
131
+ }),
132
+ );
133
+ const rec = (await readMailbox(to, "inbox"))[0]!;
134
+ expect(rec.kind).toBe("key");
135
+ expect(rec.body).toBe("down down enter");
136
+ });
137
+
138
+ it("partyMatches prefers agent_id, falls back to pid", () => {
139
+ const party = { pid: 5, cli: "c", cwd: "/x", agent_id: "stable" };
140
+ expect(partyMatches(party, "stable", 999)).toBe(true); // agent_id wins across pid churn
141
+ expect(partyMatches(party, "other", 5)).toBe(true); // pid fallback
142
+ expect(partyMatches(party, "other", 6)).toBe(false);
143
+ expect(partyMatches(null, "stable", 5)).toBe(false);
144
+ });
145
+ });
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Durable inter-agent message log.
3
+ *
4
+ * Every `ay send` that carries a real body is recorded twice — from the two
5
+ * ends' points of view — as append-only JSONL colocated with each agent's
6
+ * project dir (the same `<cwd>/.agent-yes/` convention the session logs use):
7
+ *
8
+ * - the SENDER's `<from.cwd>/.agent-yes/outbox.jsonl`
9
+ * - the RECIPIENT's `<to.cwd>/.agent-yes/inbox.jsonl`
10
+ *
11
+ * A single cwd may host several agents, so records carry the stable `agent_id`
12
+ * (falling back to `pid`) of each end; `ay msgs` filters a mailbox down to one
13
+ * agent by that key. Reading needs no lock (last line wins isn't relevant — a
14
+ * message log keeps every entry); writing is best-effort and never blocks or
15
+ * fails a send.
16
+ */
17
+
18
+ import { appendFile, mkdir, readFile, writeFile } from "fs/promises";
19
+ import path from "path";
20
+ import { logger } from "./logger.ts";
21
+
22
+ /** One end of a message: enough to attribute and to route a reply. */
23
+ export interface MailParty {
24
+ pid: number;
25
+ cli: string;
26
+ cwd: string;
27
+ agent_id?: string | null;
28
+ }
29
+
30
+ /** A single delivered inter-agent message, stored verbatim in both mailboxes. */
31
+ export interface MessageRecord {
32
+ /** Epoch ms the send was recorded. */
33
+ at: number;
34
+ /** The per-send nonce from the `[ay-msg …]` wrapper, when the body was wrapped. */
35
+ nonce?: string;
36
+ /** Sender; `null` when an interactive human shell sent it (no agent context). */
37
+ from: MailParty | null;
38
+ /** Recipient agent. */
39
+ to: MailParty;
40
+ /** What kind of stdin write this was. Omitted for a normal `ay send` text
41
+ * message; "key" for raw keystrokes (`ay key`), "select" for a menu pick
42
+ * (`ay select`). For key/select the `body` holds the key names / choice. */
43
+ kind?: "key" | "select";
44
+ /** The message body (without the `[ay-msg …]` wrapper), or — for a key/select
45
+ * record — the keystroke names / chosen option. */
46
+ body: string;
47
+ /** Trailing control code name (e.g. "enter", "ctrl-c") when not a plain submit. */
48
+ code?: string;
49
+ /** Whether `ay send` confirmed the CLI acted on it. */
50
+ confirmed?: boolean;
51
+ /** Whether the body was wrapped in an `[ay-msg …]` attribution block. */
52
+ wrapped: boolean;
53
+ /** The remote url/alias when this message crossed the wire (`ay send <remote>:<kw>`);
54
+ * absent for a same-host send. The two ends record their own mailbox on their
55
+ * own machine, so this marks that the peer's cwd is on another host. */
56
+ remote?: string;
57
+ }
58
+
59
+ /** Keep at most this many lines per mailbox; older entries are compacted away. */
60
+ const MAILBOX_MAX_LINES = 2000;
61
+
62
+ export type Mailbox = "inbox" | "outbox";
63
+
64
+ /** Path to a cwd's mailbox file (`<cwd>/.agent-yes/{inbox,outbox}.jsonl`). */
65
+ export function mailboxPath(cwd: string, box: Mailbox): string {
66
+ return path.join(cwd, ".agent-yes", `${box}.jsonl`);
67
+ }
68
+
69
+ /** Whether a mail party is the agent identified by (agentId, pid). Prefers the
70
+ * stable agent_id (survives restart); falls back to pid for legacy records. */
71
+ export function partyMatches(
72
+ party: MailParty | null,
73
+ agentId: string | null | undefined,
74
+ pid: number | null | undefined,
75
+ ): boolean {
76
+ if (!party) return false;
77
+ if (agentId && party.agent_id && party.agent_id === agentId) return true;
78
+ if (typeof pid === "number" && party.pid === pid) return true;
79
+ return false;
80
+ }
81
+
82
+ async function appendCapped(filePath: string, record: MessageRecord): Promise<void> {
83
+ await mkdir(path.dirname(filePath), { recursive: true });
84
+ await appendFile(filePath, JSON.stringify(record) + "\n");
85
+ // Opportunistic compaction: keep the file bounded despite append-only writes.
86
+ const raw = await readFile(filePath, "utf-8").catch(() => "");
87
+ const lines = raw.split("\n").filter((l) => l.trim());
88
+ if (lines.length > MAILBOX_MAX_LINES) {
89
+ const kept = lines.slice(lines.length - MAILBOX_MAX_LINES).join("\n");
90
+ await writeFile(filePath, kept + "\n");
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Record the SENDER's view in its outbox. Best-effort — a filesystem error is
96
+ * logged and swallowed so persistence never breaks a send. The outbox lives
97
+ * under `from.cwd`; a human sender (from === null) writes under `process.cwd()`.
98
+ */
99
+ export async function recordOutbox(record: MessageRecord): Promise<void> {
100
+ const outCwd = record.from?.cwd ?? process.cwd();
101
+ try {
102
+ await appendCapped(mailboxPath(outCwd, "outbox"), record);
103
+ } catch (err) {
104
+ logger.debug(`[messageLog] outbox append failed: ${err}`);
105
+ }
106
+ }
107
+
108
+ /** Record the RECIPIENT's view in its inbox (under `to.cwd`). Best-effort. */
109
+ export async function recordInbox(record: MessageRecord): Promise<void> {
110
+ try {
111
+ await appendCapped(mailboxPath(record.to.cwd, "inbox"), record);
112
+ } catch (err) {
113
+ logger.debug(`[messageLog] inbox append failed: ${err}`);
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Record a same-host message in both mailboxes — the sender's outbox and the
119
+ * recipient's inbox both live on this machine. For a message that crossed the
120
+ * wire, each end calls `recordOutbox`/`recordInbox` on its own host instead.
121
+ */
122
+ export async function recordMessage(record: MessageRecord): Promise<void> {
123
+ await recordOutbox(record);
124
+ await recordInbox(record);
125
+ }
126
+
127
+ /** Read and parse a cwd's mailbox, oldest first. Missing/corrupt lines skipped. */
128
+ export async function readMailbox(cwd: string, box: Mailbox): Promise<MessageRecord[]> {
129
+ let raw: string;
130
+ try {
131
+ raw = await readFile(mailboxPath(cwd, box), "utf-8");
132
+ } catch {
133
+ return [];
134
+ }
135
+ const out: MessageRecord[] = [];
136
+ for (const line of raw.split("\n")) {
137
+ const t = line.trim();
138
+ if (!t) continue;
139
+ try {
140
+ const rec = JSON.parse(t) as MessageRecord;
141
+ if (rec && typeof rec.at === "number" && rec.to) out.push(rec);
142
+ } catch {
143
+ /* skip corrupt/partial line */
144
+ }
145
+ }
146
+ return out;
147
+ }
package/ts/serve.ts CHANGED
@@ -16,6 +16,7 @@ import {
16
16
  listRecords,
17
17
  readNotes,
18
18
  readPtysize,
19
+ recentMessageEdges,
19
20
  recentReadEdges,
20
21
  renderLogTailLines,
21
22
  renderRawLog,
@@ -26,6 +27,7 @@ import {
26
27
  } from "./subcommands.ts";
27
28
  import { TYPING_BADGE } from "./badges.ts";
28
29
  import { ensureNodeRuntime, liveEnv } from "./nodeRuntime.ts";
30
+ import { type MailParty, recordInbox } from "./messageLog.ts";
29
31
  import { updateGlobalPidStatus } from "./globalPidIndex.ts";
30
32
  import { spawnRejectionReason } from "./spawnGate.ts";
31
33
  import { findSpawnHiddenLauncher } from "./rustBinary.ts";
@@ -1723,12 +1725,16 @@ export async function cmdServe(rest: string[]): Promise<number> {
1723
1725
  });
1724
1726
  }
1725
1727
 
1726
- // GET /api/edges — recent inter-agent relationship edges for the /rgui wire
1727
- // view. Currently the read/tail edges (agent `by` read agent `target` within
1728
- // the last minute, from ~/.agent-yes/reads.jsonl). Directional, ephemeral.
1728
+ // GET /api/edges — recent inter-agent relationship edges for the /rgui + /w
1729
+ // wire view. Two directional, ephemeral edge sets:
1730
+ // reads agent `by` read/tailed agent `target` (from ~/.agent-yes/reads.jsonl)
1731
+ // sends — agent `by` sent `target` a message/key/select (from the per-cwd
1732
+ // outbox logs); carries the send `kind` so the UI can style it.
1733
+ // Both are last-minute windows the client fades out by `at`.
1729
1734
  if (req.method === "GET" && p === "/api/edges") {
1730
1735
  try {
1731
- return Response.json({ reads: await recentReadEdges() });
1736
+ const [reads, sends] = await Promise.all([recentReadEdges(), recentMessageEdges()]);
1737
+ return Response.json({ reads, sends });
1732
1738
  } catch (e) {
1733
1739
  return new Response((e as Error).message, { status: 500 });
1734
1740
  }
@@ -2199,15 +2205,20 @@ export async function cmdServe(rest: string[]): Promise<number> {
2199
2205
  }
2200
2206
  }
2201
2207
 
2202
- // POST /api/send body: {keyword, msg, code?}
2208
+ // POST /api/send body: {keyword, msg, code?, from?}
2203
2209
  if (req.method === "POST" && p === "/api/send") {
2204
- let body: { keyword: string; msg: string; code?: string };
2210
+ let body: {
2211
+ keyword: string;
2212
+ msg: string;
2213
+ code?: string;
2214
+ from?: MailParty | null;
2215
+ };
2205
2216
  try {
2206
2217
  body = (await req.json()) as typeof body;
2207
2218
  } catch {
2208
2219
  return new Response("invalid JSON body", { status: 400 });
2209
2220
  }
2210
- const { keyword, msg = "", code = "enter" } = body;
2221
+ const { keyword, msg = "", code = "enter", from = null } = body;
2211
2222
  if (!keyword || typeof keyword !== "string") {
2212
2223
  return new Response("missing keyword", { status: 400 });
2213
2224
  }
@@ -2228,7 +2239,43 @@ export async function cmdServe(rest: string[]): Promise<number> {
2228
2239
  // over this same wire) is protocol noise, not input — stamp anyDaemonWriteAt
2229
2240
  // but not the "meaningful" time, so a resize/redraw can't trip the flash.
2230
2241
  await noteStdinWrite(record.pid, record.fifo_file, !isTerminalReply(msg));
2231
- return Response.json({ ok: true, pid: record.pid });
2242
+ // Record the recipient's inbox on THIS host (the sender records its own
2243
+ // outbox on its host). The sender crossed the wire, so `from.cwd` names a
2244
+ // path on another machine — mark it remote so the peer's cwd isn't misread
2245
+ // as local. Only real message bodies are logged. Best-effort.
2246
+ if (msg) {
2247
+ const senderParty: MailParty | null =
2248
+ from && typeof from === "object" && typeof from.pid === "number"
2249
+ ? {
2250
+ pid: from.pid,
2251
+ cli: String(from.cli ?? "?"),
2252
+ cwd: String(from.cwd ?? ""),
2253
+ agent_id: from.agent_id ?? undefined,
2254
+ }
2255
+ : null;
2256
+ await recordInbox({
2257
+ at: Date.now(),
2258
+ from: senderParty,
2259
+ to: {
2260
+ pid: record.pid,
2261
+ cli: record.cli,
2262
+ cwd: record.cwd,
2263
+ agent_id: record.agent_id,
2264
+ },
2265
+ body: msg,
2266
+ code: code.toLowerCase() === "enter" ? undefined : code.toLowerCase(),
2267
+ confirmed: true,
2268
+ wrapped: false,
2269
+ remote: senderParty ? "wire" : undefined,
2270
+ });
2271
+ }
2272
+ return Response.json({
2273
+ ok: true,
2274
+ pid: record.pid,
2275
+ cli: record.cli,
2276
+ cwd: record.cwd,
2277
+ agentId: record.agent_id ?? undefined,
2278
+ });
2232
2279
  } catch (e) {
2233
2280
  return new Response((e as Error).message, { status: 404 });
2234
2281
  }