@volter/supercode-teams 0.3.85 → 0.3.87

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/machine.mjs CHANGED
@@ -36,6 +36,13 @@ import { RecordHolder, RecordProducer, RECENT_PER_MAILBOX, RECORDS_NOT_GRANTED }
36
36
  import { LiveSessions, ACTING_VARIABLES } from './live-sessions.mjs';
37
37
  import { MailStore } from './mail/store.mjs';
38
38
  import { MailReads, panePrompt } from './mail/reads.mjs';
39
+ import { Agents } from './mail/agents.mjs';
40
+ import { Relays } from './mail/relay.mjs';
41
+ import { MailSend } from './mail/send.mjs';
42
+ import { AgentVerbs } from './mail/verbs.mjs';
43
+ import { deliverToRuntime } from './mail/runtime.mjs';
44
+ import { envelope as envelopeOf, linkOf, parseAddress, senderIdentity } from './mail/compose.mjs';
45
+ import { codexQuestions } from './native-questions.mjs';
39
46
  import { Duplex, PassThrough } from 'node:stream';
40
47
 
41
48
  /** Two ends of a connection made inside this process: what one writes, the other reads. Ending or destroying either
@@ -139,6 +146,22 @@ function trustClaudeFolder(cwd) {
139
146
 
140
147
  /** How long a launch watches its new pane before answering, so a program that ends at once is reported. */
141
148
  const LAUNCH_SETTLE_MS = 500;
149
+ /** How long a send to another machine waits for its mail door's answer (its receiver's door there: a relay's receipt
150
+ * alone may take 90 s). An answer that does not come is unanswered, never refused or failed. */
151
+ const MAIL_SEND_MS = 120_000;
152
+ /**
153
+ * The doors whose work waits on a session, with the bound each answers within (the node door's own is 30 s): a send or
154
+ * a hand-over waits on its receiver's door (a relay's receipt, up to 90 s; another machine's door, MAIL_SEND_MS), a
155
+ * delegation or a declaration that starts a session waits for it to appear (up to a minute). Past its bound a call is
156
+ * answered 504, which its client reads as unanswered: the work may still finish.
157
+ */
158
+ const DOOR_BOUNDS_MS = {
159
+ 'harness.v1.mail.send': 170_000,
160
+ 'harness.v1.mail.message': 170_000,
161
+ 'harness.v1.mail.handover': 140_000,
162
+ 'harness.v1.mail.delegate': 110_000,
163
+ 'harness.v1.agent.declare': 110_000,
164
+ };
142
165
  /** How many typed lines' keys a machine remembers; a server retries a line for minutes, not thousands of lines. */
143
166
  const TURN_KEYS_KEPT = 1000;
144
167
  /** How long a typed line's door gets to confirm it: a loaded machine's reader types it in seconds and answers late. */
@@ -257,7 +280,11 @@ export class TeamsMachine extends MachineCore {
257
280
  async sessionBindings(machineId,incarnationId) {
258
281
  const panes=await this.host.listSessions();
259
282
  const live=panes.filter(pane=>!pane.dead&&pane.rootPid);
260
- const reading=live.length?await this.#serveRequest('harness.v1.sessions.activity_under',{pids:live.map(pane=>pane.rootPid)}):{activities:[]};
283
+ // a harness serve that is not there (between restarts, or still starting) leaves the bindings as last read: a
284
+ // heartbeat never fails, nor marks every session unknown, for want of one reading
285
+ let reading;
286
+ try{reading=live.length?await this.#serveRequest('harness.v1.sessions.activity_under',{pids:live.map(pane=>pane.rootPid)}):{activities:[]};}
287
+ catch{return [...this.#sessionBindings.values()];}
261
288
  const byPid=new Map((reading.activities??[]).map(item=>[item.pid,item.activity]));
262
289
  const current=new Set(panes.filter(pane=>!pane.dead).map(pane=>pane.contextKey));
263
290
  let launchAgents=null;
@@ -593,7 +620,7 @@ export class TeamsMachine extends MachineCore {
593
620
  // ---- this machine's mail (mail/; docs/architecture/overview.md, "The mail operations") ---------------------------
594
621
  #startMail() {
595
622
  const mailRoot = join(supercodeHome(this.env), 'mail');
596
- this.mailStore = new MailStore({ mailRoot, log: (line) => this.emit('records-log', line) }).start();
623
+ this.mailStore = new MailStore({ mailRoot, machine: machineName(this.env), log: (line) => this.emit('records-log', line) }).start();
597
624
  // the session index's rows by address (its descriptors, as the records' index subscription hands them over)
598
625
  this.sessionDescriptors = new Map();
599
626
  const live = this.live;
@@ -609,20 +636,57 @@ export class TeamsMachine extends MachineCore {
609
636
  paneQuestion: async (pane) => { const session = await this.#paneByContext(pane); return session ? panePrompt(await this.host.capture(session.id, { lines: 60 })) : null; },
610
637
  askMachine: (machine, request) => this.#askMailDoor(machine, request),
611
638
  });
639
+ // a send to a session, for every entry (3b-1): its routing on the live map, an agent's plan and its doors
640
+ const machine = machineName(this.env);
641
+ this.agents = new Agents({ mailRoot, machine, supercodeBin: this.supercodeBin, env: this.env,
642
+ discover: (params) => this.#serveRequest('harness.v1.sessions.discover', params, 10_000),
643
+ recordedConfig: (locator) => this.#serveRequest('harness.v1.sessions.recorded_config', { locator }, 15_000) });
644
+ this.relays = new Relays({ mailRoot, machine, supercodeBin: this.supercodeBin, serve: (method, params, timeoutMs) => this.#serveRequest(method, params, timeoutMs),
645
+ runtimeOf: (harness, id) => live.runtimeReceipt(harness, id), runtimeNamed: (receiptId) => live.receiptNamed(receiptId) });
646
+ this.mailSend = new MailSend({ machine, mailRoot, live, store: this.mailStore, agents: this.agents, relays: this.relays,
647
+ // a send waits on its receiver's door there (a relay's receipt takes up to 90 s), never past MAIL_SEND_MS
648
+ askMachine: (to, request) => this.#askMailDoor(to, request, MAIL_SEND_MS),
649
+ deliverRuntime: deliverToRuntime, codexAnswer: (request) => codexQuestions(request) });
650
+ // an agent's verbs (3b-2): its threads, a delegation, its record
651
+ this.agentVerbs = new AgentVerbs({ machine, store: this.mailStore, agents: this.agents, live, supercodeBin: this.supercodeBin, env: this.env,
652
+ paneOf: (session) => this.mail.paneOf(session), mailRoot });
653
+ // the names sessions ran under (`<mail>/names.json`): a name an agent was given reaches the same session after it
654
+ // runs under another, kept from the map's changes (at most once a second)
655
+ let naming = null;
656
+ live.on('changed', () => {
657
+ if (naming) return;
658
+ naming = setTimeout(() => { naming = null; try { this.mailSend.rememberNames(live.sessions()); } catch { /* kept at the next change */ } }, 1000);
659
+ naming.unref?.();
660
+ });
612
661
  }
613
662
 
614
- /** One named read of another machine's mail door, over Teams, given up after ONE_MACHINE_READ_MS with a reason (the
615
- * `show` of a message that machine's record names). */
616
- async #askMailDoor(machine, request) {
617
- const ONE_MACHINE_READ_MS = 10_000;
663
+ /** One send of this machine's own, made in its daemon's process (the connector's machine events): from the sender
664
+ * the request names, which is this machine's own operator address. */
665
+ async sendOwn(request) {
666
+ if (!this.mailSend) throw new Error('this machine\'s daemon has not started its mail yet');
667
+ return this.mailSend.send({ address: request.from, name: request.from_name ?? '' }, request.to, request.body, { id: request.id ?? null, queue: request.queue === true, notice: request.notice === true });
668
+ }
669
+
670
+ /** The name a caller is known by in its envelopes: its session's, else an operator's or a board's `name@machine`. */
671
+ #callerName(address) {
672
+ const row = this.live.sessions().find((session) => session.address === address);
673
+ if (row) return row.name;
674
+ const parsed = parseAddress(address);
675
+ return parsed ? `${parsed.session}@${parsed.machine}` : String(address);
676
+ }
677
+
678
+ /** One request to another machine's mail door, over Teams, given up after `boundMs` with a reason: a named read (the
679
+ * `show` of a message that machine's record names, 10 s) or a send (MAIL_SEND_MS). */
680
+ async #askMailDoor(machine, request, boundMs = 10_000) {
618
681
  const channel = await openMachine({ machine, env: this.env });
619
682
  try {
620
- const stream = await channel.open('mail', { request }, { timeoutMs: ONE_MACHINE_READ_MS });
683
+ const stream = await channel.open('mail', { request }, { timeoutMs: Math.min(boundMs, 15_000) });
684
+ // once the door took the request, an answer that never comes is unanswered: what it did there is unknown
621
685
  const text = await new Promise((resolve, reject) => {
622
686
  let answer = '';
623
- const timer = setTimeout(() => { try { stream.close('timed out'); } catch { /* closed */ } reject(new Error(`${machine} did not answer within ${ONE_MACHINE_READ_MS / 1000} s`)); }, ONE_MACHINE_READ_MS);
687
+ const timer = setTimeout(() => { try { stream.close('timed out'); } catch { /* closed */ } reject(Object.assign(new Error(`${machine} did not answer within ${boundMs / 1000} s`), { unanswered: true })); }, boundMs);
624
688
  stream.on('data', (chunk) => { answer += chunk; });
625
- stream.on('close', (reason) => { clearTimeout(timer); answer.trim() ? resolve(answer) : reject(new Error(String(reason ?? 'its mail door closed without answering'))); });
689
+ stream.on('close', (reason) => { clearTimeout(timer); answer.trim() ? resolve(answer) : reject(Object.assign(new Error(String(reason ?? 'its mail door closed without answering')), { unanswered: true })); });
626
690
  });
627
691
  const answer = JSON.parse(text.trim().split('\n').pop());
628
692
  if (answer?.outcome === 'failed') throw new Error(answer.detail ?? 'its mail door failed');
@@ -873,6 +937,8 @@ export class TeamsMachine extends MachineCore {
873
937
  this.jobs?.stopAll();
874
938
  this.stoppingMailWatch = true;
875
939
  this.mailWatch?.kill('SIGTERM');
940
+ this.stoppingServe = true;
941
+ clearTimeout(this.serveRestart);
876
942
  this.serve?.kill('SIGTERM');
877
943
  this.host.dispose();
878
944
  }
@@ -881,21 +947,69 @@ export class TeamsMachine extends MachineCore {
881
947
  for (const [key, owners] of this.routes) { owners.delete(index); if (owners.size === 0) this.routes.delete(key); }
882
948
  }
883
949
 
950
+ /**
951
+ * The daemon's `harness serve`. Its pipes are a peer that can go away: a write into them after it has ended (EPIPE) is
952
+ * that pipe's error, handled here, never the daemon's end. When it ends, every request waiting on it is answered at
953
+ * once (failed, saying so), every harness stream relayed into it is closed, and it is started again (a growing wait
954
+ * between restarts, from 1 s up to 1 min, reset once one has run a minute); the subscriptions made on it are made
955
+ * again on its `serve-exit`. Only the daemon's own stop ends it for good.
956
+ */
884
957
  #startServe() {
885
958
  const child = spawn(this.supercodeBin, ['harness', 'serve'], { stdio: ['pipe', 'pipe', 'pipe'], env: this.env });
959
+ const started = Date.now();
886
960
  child.stdout.setEncoding('utf8');
887
961
  // A restarted serve starts on a line of its own, never on the tail its predecessor left unfinished.
888
962
  const serveLines = lineSplitter();
963
+ // a serve already replaced never answers into the streams that its successor serves (its late answers would reach a
964
+ // reopened stream that uses the same ids)
889
965
  child.stdout.on('data', (chunk) => {
966
+ if (this.serve !== child) return;
890
967
  for (const line of serveLines(chunk)) if (line) this.#routeServeLine(line);
891
968
  });
892
969
  child.stderr.setEncoding('utf8');
893
970
  child.stderr.on('data', (text) => this.emit('serve-stderr', text));
971
+ let ended = false;
972
+ const end = (why) => {
973
+ if (ended) return;
974
+ ended = true;
975
+ if (this.serve === child) this.serve = null;
976
+ // what waited on it is answered now, never left to its own bound
977
+ for (const [id, pending] of this.internalRequests ?? []) { this.internalRequests.delete(id); pending.reject(new Error(`harness serve ${why}`)); }
978
+ for (const [, entry] of this.connections) entry.harness?.close(`harness serve ${why}`);
979
+ if (this.stoppingServe) return;
980
+ const ranLong = Date.now() - started > 60_000;
981
+ this.serveBackoff = ranLong ? 1000 : Math.min(60_000, Math.max(1000, (this.serveBackoff ?? 500) * 2));
982
+ this.emit('serve-stderr', `harness serve ${why}; started again in ${Math.round(this.serveBackoff / 1000)} s\n`);
983
+ this.serveRestart = setTimeout(() => { if (!this.stoppingServe) this.#startServe(); }, this.serveBackoff);
984
+ this.serveRestart.unref?.();
985
+ };
986
+ // a write into a serve that has gone (EPIPE, ECONNRESET) is its pipe's error: the serve is over, the daemon is not
987
+ child.stdin.on('error', (error) => {
988
+ this.emit('serve-stderr', `harness serve's input: ${error.message}\n`);
989
+ try { child.kill('SIGTERM'); } catch { /* gone */ }
990
+ // one that does not end on SIGTERM within 5 s is killed: a serve that no longer reads is never left behind
991
+ const grace = setTimeout(() => { if (child.exitCode === null && child.signalCode === null) { try { child.kill('SIGKILL'); } catch { /* gone */ } } }, 5000);
992
+ grace.unref?.();
993
+ child.once('exit', () => clearTimeout(grace));
994
+ end(`stopped taking input (${error.code ?? error.message})`);
995
+ });
996
+ child.stdout.on('error', () => {});
997
+ child.stderr.on('error', () => {});
998
+ child.on('error', (error) => end(`could not run (${error.message})`));
894
999
  child.on('exit', (code, signal) => {
895
1000
  this.emit('serve-exit', { code, signal });
896
- for (const [, entry] of this.connections) entry.harness?.close('harness serve exited');
1001
+ end(`exited (${signal ?? `code ${code}`})`);
897
1002
  });
898
1003
  this.serve = child;
1004
+ child.once('spawn', () => { if (this.serve === child) this.emit('serve-started'); });
1005
+ }
1006
+
1007
+ /** One line into the daemon's `harness serve`, when it is there to take it: false when it is not (ended, or between
1008
+ * restarts). A write's own failure is its pipe's error (handled in #startServe), never a throw here. */
1009
+ #toServe(line) {
1010
+ const stdin = this.serve?.stdin;
1011
+ if (!stdin || !stdin.writable || stdin.destroyed) return false;
1012
+ try { stdin.write(line); return true; } catch { return false; }
899
1013
  }
900
1014
 
901
1015
  // Idle notices for every session on this machine (`supercode message watch`):
@@ -921,10 +1035,12 @@ export class TeamsMachine extends MachineCore {
921
1035
  this.internalRequests ??= new Map();
922
1036
  const id = (this.internalNext = (this.internalNext ?? 0) + 1);
923
1037
  return new Promise((resolve, reject) => {
924
- if (!this.serve?.stdin?.writable) return reject(new Error('harness serve is not running'));
925
1038
  this.internalRequests.set(id, { resolve, reject });
1039
+ if (!this.#toServe(`${JSON.stringify({ jsonrpc: '2.0', id: joinId(INTERNAL_CONNECTION, id), method, params })}\n`)) {
1040
+ this.internalRequests.delete(id);
1041
+ return reject(new Error('harness serve is not running'));
1042
+ }
926
1043
  setTimeout(() => { if (this.internalRequests.delete(id)) reject(new Error(`${method} timed out`)); }, timeoutMs).unref();
927
- this.serve.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id: joinId(INTERNAL_CONNECTION, id), method, params })}\n`);
928
1044
  });
929
1045
  }
930
1046
 
@@ -1016,8 +1132,12 @@ export class TeamsMachine extends MachineCore {
1016
1132
  if (!line) continue;
1017
1133
  let message;
1018
1134
  try { message = JSON.parse(line); } catch { continue; }
1019
- if (message.id !== undefined && message.id !== null) message.id = joinId(index, message.id);
1020
- this.serve?.stdin.write(JSON.stringify(message) + '\n');
1135
+ const asked = message.id !== undefined && message.id !== null ? message.id : null;
1136
+ if (asked !== null) message.id = joinId(index, message.id);
1137
+ // a serve that is not there to take it answers the request at once, on this stream only
1138
+ if (!this.#toServe(JSON.stringify(message) + '\n') && asked !== null && !stream.closed) {
1139
+ try { stream.write(`${JSON.stringify({ jsonrpc: '2.0', id: asked, error: { code: -32000, message: 'harness serve is not running; it is being started again' } })}\n`); } catch { /* the stream went */ }
1140
+ }
1021
1141
  }
1022
1142
  });
1023
1143
  stream.on('close', () => { entry.harness = null; });
@@ -1128,16 +1248,12 @@ export class TeamsMachine extends MachineCore {
1128
1248
  }
1129
1249
  }
1130
1250
 
1131
- /** One mail request from this machine: its own mail door, which delivers here or sends on through the one way out to
1132
- * another machine (`ask_machine`, with its guards), never a second exit of its own. */
1251
+ /** One mail request from this machine (its launch notices): its own send, which delivers here or sends on through
1252
+ * the one way out to another machine, in this process. */
1133
1253
  #sendMail(request, target) {
1134
- const child = spawn(this.supercodeBin, ['message', 'remote-deliver'], { stdio: ['pipe', 'pipe', 'pipe'], env: this.env });
1135
- let out = '';
1136
- child.stdout.on('data', (chunk) => { out += chunk; });
1137
- child.stderr.on('data', () => {});
1138
- child.on('error', (error) => this.log.append({ principal: null, cap: 'mail', target, outcome: 'failed', detail: error.message }));
1139
- child.on('close', (code) => this.log.append({ principal: null, cap: 'mail', target, outcome: code === 0 ? 'send' : 'failed', detail: out.trim().slice(0, 200) || undefined }));
1140
- child.stdin.end(JSON.stringify(request));
1254
+ void this.mailSend.send({ address: request.from, name: request.from_name }, request.to, request.body, { queue: request.queue === true, notice: request.notice === true })
1255
+ .then((answer) => this.log.append({ principal: null, cap: 'mail', target, outcome: answer.code === 0 ? 'send' : 'failed', detail: answer.text.slice(0, 200) || undefined }),
1256
+ (error) => this.log.append({ principal: null, cap: 'mail', target, outcome: 'failed', detail: String(error.message).slice(0, 200) }));
1141
1257
  }
1142
1258
 
1143
1259
  // ---- mail ----------------------------------------------------------------
@@ -1174,8 +1290,8 @@ export class TeamsMachine extends MachineCore {
1174
1290
  }
1175
1291
 
1176
1292
  #serveMail(entry, stream, accept, refuse,requestOverride=null) {
1177
- // One message into a session on this machine, delivered by `supercode message
1178
- // remote-deliver` through the same doors a local send uses. The receiver is the
1293
+ // One message into a session on this machine, delivered by this daemon's own send (mail/send.mjs) through the same
1294
+ // doors a local send uses. The receiver is the
1179
1295
  // target: `mail:*` reaches every session here, `mail:<address>` one of them; a
1180
1296
  // receiver named rather than addressed needs `mail:*`.
1181
1297
  const { admitted, principal } = entry;
@@ -1217,7 +1333,7 @@ export class TeamsMachine extends MachineCore {
1217
1333
  try {
1218
1334
  const answer = request.op === 'list'
1219
1335
  ? { code: 0, sessions: await this.mail.localRows() }
1220
- : { code: 0, rows: this.mail.showRows((Array.isArray(request.ids) ? request.ids : []).filter((id) => typeof id === 'string')) };
1336
+ : { code: 0, rows: await this.mail.showRows((Array.isArray(request.ids) ? request.ids : []).filter((id) => typeof id === 'string')) };
1221
1337
  this.log.append({ principal, cap: 'mail', target, outcome: request.op });
1222
1338
  if (!stream.closed) { stream.write(JSON.stringify(answer)); stream.close('answered'); }
1223
1339
  } catch (error) {
@@ -1227,27 +1343,51 @@ export class TeamsMachine extends MachineCore {
1227
1343
  })();
1228
1344
  return;
1229
1345
  }
1230
- const child = spawn(this.supercodeBin, ['message', 'remote-deliver'], { stdio: ['pipe', 'pipe', 'pipe'], env: this.env });
1231
- let out = '';
1232
- let err = '';
1233
- child.stdout.on('data', (chunk) => { out += chunk; });
1234
- child.stderr.on('data', (chunk) => { err += chunk; });
1235
- child.on('error', (error) => { err += error.message; });
1236
- child.on('close', (code) => {
1237
- this.log.append({ principal, cap: 'mail', target, outcome: code === 0 ? request.op : 'failed', detail: err.trim().slice(0, 200) || undefined });
1346
+ // Who sent it is the stream's principal, as Teams authenticated it, and the name Teams has for it (never a name the
1347
+ // caller's app asserts): an envelope made here says so, never the return address the request claims. A local caller
1348
+ // is no link.
1349
+ const link = linkOf(principal, entry.principalName ?? null);
1350
+ void (async () => {
1351
+ let answer;
1352
+ try { answer = request.op === 'file' ? this.#fileHere(request, link) : await this.#sendHere(request, link); }
1353
+ catch (error) { answer = { outcome: 'failed', detail: error.message }; }
1354
+ this.log.append({ principal, cap: 'mail', target, outcome: answer.outcome === 'failed' || (answer.code ?? 0) !== 0 ? 'failed' : request.op, detail: answer.outcome === 'failed' ? String(answer.detail).slice(0, 200) : undefined });
1238
1355
  if (stream.closed) return;
1239
- stream.write(out.trim() || JSON.stringify({ outcome: 'failed', detail: err.trim() || `remote-deliver exited ${code}` }));
1356
+ stream.write(JSON.stringify(answer));
1240
1357
  stream.close('delivered');
1241
- });
1242
- // A list the asker stopped waiting for (its stream closed: it gave up at its bound) ends here too, so its reader does
1243
- // not outlive the ask. A write (a send, a turn, a filing, an inbox read that claims what it shows) is never cancelled
1244
- // half done: it finishes and is logged.
1245
- // Who asked travels with the request, so the receiving session's envelope can say so.
1246
- // Who sent it is the stream's principal, as Teams authenticated it, and the name Teams has for it (never a name the
1247
- // caller's app asserts): remote-deliver takes the sender's identity from these, never from the return address the
1248
- // request claims. A local caller is no link.
1249
- const linked = principal && principal !== 'local';
1250
- child.stdin.end(JSON.stringify({ ...request, via_principal: linked ? principal : null, via_name: linked ? (entry.principalName ?? null) : null }));
1358
+ })();
1359
+ }
1360
+
1361
+ /** A send through the mail door, for a session here (message.rs `remote_deliver` send): one for another machine is
1362
+ * refused when it came through a link (never forwarded on this machine's own credential), and this machine's own
1363
+ * daemon sends on through the one way out. */
1364
+ async #sendHere(request, link) {
1365
+ const from = parseAddress(request.from)?.address;
1366
+ if (!from) return { code: 2, text: `\`${request.from}\` is not a session address; addresses look like sc:<machine>:<harness>:<session-id>`, receipt: null };
1367
+ const text = (key) => (typeof request[key] === 'string' ? request[key] : null);
1368
+ return this.mailSend.send({ address: from, name: text('from_name') ?? '' }, text('to') ?? '', text('body') ?? '', {
1369
+ subject: text('subject'), in_reply_to: text('in_reply_to'), notify_when_idle: request.notify_when_idle === true, queue: request.queue === true,
1370
+ id: text('id'), notice: request.notice === true, thread: text('thread'),
1371
+ }, { link });
1372
+ }
1373
+
1374
+ /** A message another machine's mailbox carried here, filed in a mailbox here (message.rs `remote_deliver` file): the
1375
+ * sender is resolved here from the link it came through, never from the filer's own claim; a wake it carries is
1376
+ * requested once, for a message new here. */
1377
+ #fileHere(request, link) {
1378
+ const to = parseAddress(request.to);
1379
+ const envelope = request.envelope;
1380
+ const valid = envelope && typeof envelope === 'object' && typeof envelope.id === 'string' && /^[A-Za-z0-9_-]+$/.test(envelope.id) && parseAddress(envelope.from)
1381
+ && typeof envelope.from_name === 'string' && typeof envelope.body === 'string' && Number.isSafeInteger(envelope.created_at_ms) && typeof envelope.kind === 'string' && envelope.reply_via && typeof envelope.reply_via.mode === 'string';
1382
+ if (!to || !valid) return { code: 2, text: 'Not filed: the mail door files one envelope for one address here.', receipt: null };
1383
+ if (to.machine !== machineName(this.env)) return { code: 3, text: `${to.address} is not on this machine.`, receipt: null };
1384
+ const { sender_identity: _claimed, ...rest } = envelope;
1385
+ const filed = envelopeOf({ ...rest, sender_identity: senderIdentity(envelope.from, link, machineName(this.env)) });
1386
+ const fresh = this.mailStore.findIn(to.address, filed.id) == null;
1387
+ this.mailStore.deliver(to.address, filed);
1388
+ // carried here by another machine's mailbox: its wake comes with it, once, and this machine's watch delivers it
1389
+ if (fresh && request.wake === true) this.mailStore.requestWake(to.address, filed.id);
1390
+ return { code: 0, text: `filed for ${to.address}`, receipt: null };
1251
1391
  }
1252
1392
 
1253
1393
  /**
@@ -1577,17 +1717,12 @@ export class TeamsMachine extends MachineCore {
1577
1717
  return { observation: `mailbox:${from}`, principal: null, classification: 'unresolved', label: 'unresolved', why: `it claims ${machine ?? 'another machine'}'s address but did not come through that machine's link` };
1578
1718
  }
1579
1719
 
1580
- /** The mailbox's own filing door (`supercode message file`), started and awaited, never waited on synchronously. */
1581
- #runFiling(request) {
1582
- return new Promise((resolve) => {
1583
- let out = '';
1584
- const child = spawn(this.supercodeBin, ['message', 'file'], { stdio: ['pipe', 'pipe', 'ignore'], env: this.env, windowsHide: true });
1585
- const timer = setTimeout(() => { child.kill('SIGKILL'); resolve({ code: 1, text: 'the mailbox\'s filing door did not answer within 30 s' }); }, 30_000);
1586
- child.stdout.setEncoding('utf8').on('data', (chunk) => { out += chunk; });
1587
- child.on('error', (error) => { clearTimeout(timer); resolve({ code: 1, text: error.message }); });
1588
- child.on('close', () => { clearTimeout(timer); try { resolve(JSON.parse(out.trim().split('\n').pop())); } catch { resolve({ code: 1, text: out.trim().slice(-300) || 'the mailbox\'s filing door answered nothing' }); } });
1589
- child.stdin.on('error', () => {});
1590
- child.stdin.end(JSON.stringify(request));
1720
+ /** A filing in this machine's mail (mail/send.mjs `file`), in this process. */
1721
+ async #runFiling(request) {
1722
+ if (!this.mailSend) return { code: 1, text: 'this machine\'s daemon has not started its mail yet' };
1723
+ return this.mailSend.file({ address: request.from, name: request.from_name ?? '' }, request.to, request.body ?? '', {
1724
+ subject: request.subject ?? null, key: request.id ?? null, wake: request.wake !== false,
1725
+ sender_identity: request.sender_identity && typeof request.sender_identity === 'object' ? request.sender_identity : null, typed: request.typed === true,
1591
1726
  });
1592
1727
  }
1593
1728
 
@@ -2352,8 +2487,10 @@ export class TeamsMachine extends MachineCore {
2352
2487
  const reply = (envelope) => { if (stream.closed) return; try { stream.write(JSON.stringify(envelope) + '\n'); } catch { /* peer gone */ } };
2353
2488
  try {
2354
2489
  // Every door answers or fails within a bound; a hung substrate call becomes an error, never silence.
2355
- // The caller may shorten the bound, never lengthen it past the daemon's own 30 s.
2356
- const bound = Math.min(Number(request.params?.timeoutMs) || 30_000, 30_000);
2490
+ // The caller may shorten the bound, never lengthen it past the daemon's own: 30 s, or the longer one a door
2491
+ // that waits on a session states (DOOR_BOUNDS_MS), each above its own inner waits.
2492
+ const own = DOOR_BOUNDS_MS[request.method] ?? 30_000;
2493
+ const bound = Math.min(Number(request.params?.timeoutMs) || own, own);
2357
2494
  const result = await withTimeout(this.#dispatch(entry, request.method, request.params ?? {}), bound, request.method);
2358
2495
  reply({ id: request.id, result });
2359
2496
  this.emit('door', { method: request.method, ms: Date.now() - startedAt, outcome: 'ok' });
@@ -3095,12 +3232,80 @@ export class TeamsMachine extends MachineCore {
3095
3232
  act('title', params.target, 'ok', renamed);
3096
3233
  return { ok: true, target: params.target, title: params.title ?? null, window: renamed };
3097
3234
  }
3235
+ // A send to a session (docs/architecture/overview.md, "The mail operations", 3b-1): `supercode message send` and
3236
+ // `reply`, a session's `send_message` and the native callers' client (D207, pending) on this machine's own socket,
3237
+ // each with its claim; the caller is who the claim resolves to, never who the request says it is.
3238
+ case 'harness.v1.mail.send': {
3239
+ if (entry.principal !== 'local' || entry.appDoor) throw Object.assign(new Error('a send is made here by this machine\'s own processes; another machine\'s goes through its mail door'), { code: 403 });
3240
+ const caller = await this.#callerOf(entry, params);
3241
+ if (typeof caller !== 'string' || !caller) return { code: 2, text: "Can't tell which session is running this command, so replies would have nowhere to go. Nothing was sent. Run it from your agent session's own shell tool.", receipt: null };
3242
+ const text = (key) => (typeof params[key] === 'string' ? params[key] : null);
3243
+ const body = text('body') ?? '';
3244
+ if (!body.trim()) return { code: 1, text: 'Nothing was sent: the message is empty.', receipt: null };
3245
+ const from = { address: caller, name: this.#callerName(caller) };
3246
+ let to = text('to'), options = { subject: text('subject'), in_reply_to: text('in_reply_to'), notify_when_idle: params.notify_when_idle === true, queue: params.queue === true, id: text('id'), message_id: text('message_id') };
3247
+ if (text('reply')) {
3248
+ // a reply: to the message's sender, in its thread, its subject kept (message.rs `Reply`)
3249
+ const found = this.mailStore.findPrefixIn(caller, text('reply'));
3250
+ if (found.length !== 1) return { code: 2, text: `Not sent: ${text('reply')} names ${found.length} messages in your mailbox. Name the one you answer as its envelope shows it.`, receipt: null };
3251
+ const answered = found[0].envelope;
3252
+ if (answered.kind === 'notice') return { code: 2, text: `Not sent: ${text('reply')} is an automated notice; nothing reads a reply to it.`, receipt: null };
3253
+ to = answered.from;
3254
+ options = { ...options, subject: typeof answered.subject === 'string' ? (answered.subject.startsWith('Re: ') ? answered.subject : `Re: ${answered.subject}`) : null, in_reply_to: answered.id, id: null, thread: answered.thread ?? answered.id };
3255
+ }
3256
+ if (!to) return { code: 2, text: 'Not sent: a send names its receiver.', receipt: null };
3257
+ // a bare name no session here answers to may be a session on the team's other machines (their held records)
3258
+ const team = text('reply') ? { to: null } : this.mail.teamAddress(to, (name) => this.mailSend.remembered(name));
3259
+ if (team.error) return { code: 5, text: `Not sent: ${team.error}. Name it as \`name@machine\` or its address.`, receipt: null };
3260
+ const answer = { ...(await this.mailSend.send(from, team.to ?? to, body, options)), ...(team.notes?.length ? { notes: team.notes } : {}) };
3261
+ act('mail.send', to, answer.code === 0 ? 'ok' : 'failed');
3262
+ return answer;
3263
+ }
3264
+ // An agent's verbs (docs/architecture/overview.md, 3b-2): `message thread`, `threads`, `delegate`, and `supercode
3265
+ // agent declare|show`, this machine's own processes only; a delegation and a thread list without --agent are the
3266
+ // caller's, as its claim names it.
3267
+ case 'harness.v1.mail.thread': case 'harness.v1.mail.threads': case 'harness.v1.mail.delegate': case 'harness.v1.agent.declare': case 'harness.v1.agent.show': {
3268
+ if (entry.principal !== 'local' || entry.appDoor) throw Object.assign(new Error('an agent\'s verbs are answered here for this machine\'s own processes'), { code: 403 });
3269
+ const text = (key) => (typeof params[key] === 'string' && params[key] ? params[key] : null);
3270
+ if (method === 'harness.v1.mail.thread') {
3271
+ if (!text('id')) throw Object.assign(new Error('thread names a message id'), { code: 400 });
3272
+ return this.agentVerbs.thread(text('id'));
3273
+ }
3274
+ if (method === 'harness.v1.agent.show') return this.agentVerbs.show(text('name'));
3275
+ if (method === 'harness.v1.agent.declare') {
3276
+ if (!text('name')) throw Object.assign(new Error('declare names its agent'), { code: 400 });
3277
+ const answer = await this.agentVerbs.declare({ name: text('name'), main: text('main'), open: text('open'), input: text('input'), folder: text('folder'), harness: text('harness'),
3278
+ idle_minutes: Number.isInteger(params.idle_minutes) && params.idle_minutes >= 0 ? params.idle_minutes : null, owners_account_manager: params.owners_account_manager === true });
3279
+ act('agent.declare', text('name'), answer.code === 0 ? 'ok' : 'failed');
3280
+ return answer;
3281
+ }
3282
+ let caller = null;
3283
+ try { caller = await this.#callerOf(entry, params); } catch (error) { if (method === 'harness.v1.mail.delegate') throw error; }
3284
+ if (method === 'harness.v1.mail.threads') return this.agentVerbs.threads(text('agent'), typeof caller === 'string' ? caller : null);
3285
+ if (typeof caller !== 'string' || !caller) return { code: 2, text: "Can't tell which session is running this command, so replies would have nowhere to go. Nothing was sent. Run it from your agent session's own shell tool." };
3286
+ if (!text('root')) throw Object.assign(new Error('delegate names its root'), { code: 400 });
3287
+ const answer = await this.agentVerbs.delegate(caller, text('root'), { folder: text('folder'), harness: text('harness') });
3288
+ act('mail.delegate', caller, answer.code === 0 ? 'ok' : 'failed');
3289
+ return answer;
3290
+ }
3291
+ // A filed message handed to its session's door (the native watch's carrier and a relay's `message push`, until
3292
+ // step 4): once, by its id, recorded on its wake.
3293
+ case 'harness.v1.mail.handover': {
3294
+ if (entry.principal !== 'local' || entry.appDoor) throw Object.assign(new Error('mail is handed over here by this machine\'s own processes'), { code: 403 });
3295
+ if (typeof params.address !== 'string' || typeof params.id !== 'string' || !parseAddress(params.address)) throw Object.assign(new Error('a hand-over names an address and a message id'), { code: 400 });
3296
+ return this.mailSend.handOver(params.address, params.id, { markRead: params.mark_read === true, wake: params.wake === true });
3297
+ }
3298
+ // harness serve's `sessions.message` (its mail to a session or an agent, not its `as_user` turn), asked here by
3299
+ // harness serve itself: one implementation of a send, whatever the entry.
3300
+ case 'harness.v1.mail.message': {
3301
+ if (entry.principal !== 'local' || entry.appDoor) throw Object.assign(new Error('sessions.message is answered here for this machine\'s own harness serve'), { code: 403 });
3302
+ return this.mailSend.message(params);
3303
+ }
3098
3304
  case 'harness.v1.mail.file': {
3099
3305
  // One message filed in a receiver's mailbox here (a board owner's notices and mail, D138 step 6): the sender's
3100
- // identity is observed by this daemon, in this process, and handed to the mailbox's filing door with the message,
3101
- // so no filing starts a process of its own to observe it. An observation that cannot be made is said in the
3102
- // record (unresolved, with why), never waited on past its bound. Operators only: the filing door takes the
3103
- // sender a local operator names, as `supercode message file` itself does.
3306
+ // identity is observed by this daemon, in this process, and filed with the message here (mail/send.mjs `file`):
3307
+ // no filing starts a process. An observation that cannot be made is said in the record (unresolved, with why),
3308
+ // never waited on past its bound. Operators only: the filing takes the sender a local operator names.
3104
3309
  if (!operator) throw Object.assign(new Error('operators only'), { code: 403 });
3105
3310
  for (const key of ['from', 'to', 'body']) if (typeof params[key] !== 'string' || !params[key]) throw Object.assign(new Error(`a filing names its ${key}`), { code: 400 });
3106
3311
  const sender_identity = await this.#observeSender(params.from, params.from_name ?? null, typeof params.context === 'string' && params.context ? params.context : null);
@@ -3135,7 +3340,7 @@ export class TeamsMachine extends MachineCore {
3135
3340
  address = found.address;
3136
3341
  }
3137
3342
  const machine = address.split(':')[1];
3138
- if (machine === machineName(this.env)) return { code: 0, address, rows: this.mail.inboxRows(address, last) };
3343
+ if (machine === machineName(this.env)) return { code: 0, address, rows: await this.mail.inboxRows(address, last) };
3139
3344
  try {
3140
3345
  const answer = await this.#askMailDoor(machine, { op: 'inbox', session: address, recent: last });
3141
3346
  return answer?.code === 0 ? { code: 0, address, rows: Array.isArray(answer.rows) ? answer.rows : [] } : { code: 3, text: answer?.text ?? 'its machine refused' };
@@ -3145,7 +3350,7 @@ export class TeamsMachine extends MachineCore {
3145
3350
  // acknowledged once it has printed it (harness.v1.mail.acknowledge)
3146
3351
  const caller = await this.#callerOf(entry, params);
3147
3352
  if (typeof caller !== 'string' || /^sc:[^:]+:board:/.test(caller)) return { code: 2, text: "Can't tell which session is running this command, so replies would have nowhere to go. Nothing was sent. Run it from your agent session's own shell tool." };
3148
- const read = this.mail.inbox(caller, { all: params.all === true, last, pid: Number(params.claim?.pid) || process.pid });
3353
+ const read = await this.mail.inbox(caller, { all: params.all === true, last, pid: Number(params.claim?.pid) || process.pid });
3149
3354
  let token = null;
3150
3355
  if (read.claimed.length) {
3151
3356
  token = randomBytes(12).toString('hex');