@volter/supercode-orchestrator 0.5.181 → 0.5.182

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.
Files changed (2) hide show
  1. package/board/doors.mjs +42 -3
  2. package/package.json +1 -1
package/board/doors.mjs CHANGED
@@ -26,8 +26,11 @@ async function opened(machine) {
26
26
  }
27
27
  const opening = (async () => {
28
28
  const channel = await openMachine({ machine: machine || null });
29
- const door = { channel, node: null, harness: null };
29
+ const door = { channel, node: null, harness: null, heardAt: performance.now() };
30
+ // when the channel last carried anything (an answer, a stream's data, the keep-alive's pong): see retireSilent
31
+ channel.socket?.on?.('data', () => { door.heardAt = performance.now(); });
30
32
  channel.on('close', () => { if (held.get(key) === opening) held.delete(key); });
33
+ door.opening = opening;
31
34
  return door;
32
35
  })();
33
36
  held.set(key, opening);
@@ -43,13 +46,14 @@ async function opened(machine) {
43
46
  export async function machineCall(machine, method, params = {}, { timeoutMs = 60_000 } = {}) {
44
47
  if (!method.startsWith('harness.v1.')) throw new Error('a machine door is a harness.v1 method');
45
48
  const started = performance.now();
49
+ let door = null;
46
50
  try {
47
- const door = await opened(machine);
51
+ door = await opened(machine);
48
52
  if (method.startsWith('harness.v1.sessions.')) {
49
53
  // The harness client starts its transport again itself once it ends (a daemon restart there); one that never
50
54
  // started is let go, so the next call starts another.
51
55
  if (!door.harness) {
52
- const starting = (async () => { const client = new SupercodeHarnessClient({ transport: harnessTransport(door.channel) }); await client.start?.(); return client; })();
56
+ const starting = (async () => { const client = new SupercodeHarnessClient({ transport: channelHarness(door.channel, machine) }); await client.start?.(); return client; })();
53
57
  starting.catch(() => { if (door.harness === starting) door.harness = null; });
54
58
  door.harness = starting;
55
59
  }
@@ -66,11 +70,46 @@ export async function machineCall(machine, method, params = {}, { timeoutMs = 60
66
70
  door.node = opening;
67
71
  }
68
72
  return await (await door.node).call(method, params, { timeoutMs });
73
+ } catch (error) {
74
+ if (machine && door && (error?.code === 'REQUEST_TIMEOUT' || error?.code === 504)) retireSilent(machine, door, started);
75
+ throw error;
69
76
  } finally {
70
77
  noteStep(`door ${method}${machine ? ' (another machine)' : ''}`, performance.now() - started);
71
78
  }
72
79
  }
73
80
 
81
+ /**
82
+ * Another machine's channel that carried nothing at all for the whole of a request's bound is closed, so the next call
83
+ * opens a fresh one. Such a channel's socket to the Teams server can stay open after the server has lost the far side:
84
+ * every request on it waits out its bound, and nothing ends the channel until the server closes the socket. On
85
+ * 2026-10-14 the board owner's discoveries on other machines timed out five times running (00:05:16 to 00:06:31), the
86
+ * server closed their channel at 00:06:37 (two calls failed "connection closed"), and the next channel answered in
87
+ * 1.4 s. The keep-alive's ping is answered by the far machine's own channel, so a channel that carried anything
88
+ * meanwhile (another answer, a pong) reaches that machine and is kept: the request was slow there.
89
+ */
90
+ function retireSilent(machine, door, started) {
91
+ if (door.heardAt >= started || door.channel.closed) return;
92
+ if (held.get(machine) === door.opening) held.delete(machine);
93
+ door.channel.close();
94
+ }
95
+
96
+ /**
97
+ * The harness transport over a machine channel, saying what ended when it ends: the channel's stream is no harness
98
+ * process, and its close (`connection closed`) read as "Volter Harness service exited" sent F81 looking for a child
99
+ * that never exited.
100
+ */
101
+ function channelHarness(channel, machine) {
102
+ const inner = harnessTransport(channel);
103
+ const where = machine ? `the Teams server channel to ${machine}` : "the socket to this machine's daemon";
104
+ return {
105
+ get running() { return inner.running; },
106
+ start: (handlers) => inner.start({ ...handlers,
107
+ onExit: (code, reason) => handlers.onExit(code, `${reason ?? 'closed'}: ${where} ended, no harness process exited`) }),
108
+ write: (line) => inner.write(line),
109
+ close: () => inner.close(),
110
+ };
111
+ }
112
+
74
113
  export async function machineRecordStream(params = {}) {
75
114
  const door = await opened(null);
76
115
  return door.channel.open('machine-record-changes', params);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-orchestrator",
3
- "version": "0.5.181",
3
+ "version": "0.5.182",
4
4
  "type": "module",
5
5
  "description": "The orchestrator runtime over the Volter Harness ontology: one typed operational model whose folder is its serialization, read and written through the harness orchestration doors (docs/ORCHESTRATOR-IR.md)",
6
6
  "exports": {