@volter/supercode-teams 0.3.129 → 0.3.131

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/bin/fleet.mjs CHANGED
@@ -162,19 +162,50 @@ async function main() {
162
162
  if (typeof flags.for === 'string') setTimeout(() => { fc.detachChannel(); channel.close(); process.exit(0); }, Number(flags.for));
163
163
  return;
164
164
  }
165
- const term = await attachPane(channel, { pane, mode, cols: process.stdout.columns ?? 120, rows: process.stdout.rows ?? 40, acting: await launcher() });
166
- term.on('hello', (frame) => process.stderr.write(`[attached ${pane} ${frame.cols}x${frame.rows}${mode === 'control' ? ' — ctrl-] detaches' : ''}]\n`));
167
- term.on('error-message', (message) => process.stderr.write(`[${message}]\n`));
168
- term.on('data', (text) => process.stdout.write(text));
169
- term.on('close', (reason) => { process.stderr.write(`\n[detached${reason ? `: ${reason}` : ''}]\n`); channel.close(); process.exit(0); });
165
+ // The view outlives a restart of the machine daemon (an upgrade restarts it; the pane itself lives in the pane
166
+ // host and goes on): when the daemon's connection drops, this window attaches again once it answers, for up to
167
+ // REATTACH_MS. The pane ending, a refusal, or ctrl-] ends the view as before.
168
+ const REATTACH_MS = 180_000;
169
+ let term = null;
170
+ let detaching = false;
171
+ const attach = async (into) => {
172
+ const next = await attachPane(into, { pane, mode, cols: process.stdout.columns ?? 120, rows: process.stdout.rows ?? 40, acting: await launcher() });
173
+ next.on('hello', (frame) => process.stderr.write(`[attached ${pane} ${frame.cols}x${frame.rows}${mode === 'control' ? ' — ctrl-] detaches' : ''}]\n`));
174
+ next.on('error-message', (message) => process.stderr.write(`[${message}]\n`));
175
+ next.on('data', (text) => process.stdout.write(text));
176
+ next.on('close', (reason) => {
177
+ into.close();
178
+ if (!detaching && typeof reason === 'string' && reason.startsWith('connection closed')) { void reattach(); return; }
179
+ process.stderr.write(`\n[detached${reason ? `: ${reason}` : ''}]\n`);
180
+ process.exit(0);
181
+ });
182
+ term = next;
183
+ };
184
+ const reattach = async () => {
185
+ process.stderr.write('\n[the machine daemon went away (an upgrade restarts it); attaching again when it is back…]\n');
186
+ const deadline = Date.now() + REATTACH_MS;
187
+ let last = null;
188
+ while (Date.now() < deadline) {
189
+ await new Promise((done) => setTimeout(done, 1_000));
190
+ try { await attach(await channelForOn()); return; } catch (error) {
191
+ last = error;
192
+ // the daemon saying this pane is gone (`terminal refused: no pane …`) will not change by waiting; a daemon not
193
+ // answering yet (ECONNREFUSED, no pipe) will
194
+ if (/terminal refused: .*(no pane|no \w+ on pane)/i.test(error?.message ?? '')) break;
195
+ }
196
+ }
197
+ process.stderr.write(`\n[detached: the machine daemon did not take this pane back${last ? ` (${last.message})` : ''}]\n`);
198
+ process.exit(1);
199
+ };
200
+ await attach(channel);
170
201
  if (mode === 'control' && process.stdin.isTTY) {
171
202
  process.stdin.setRawMode(true);
172
203
  process.stdin.on('data', (chunk) => {
173
- if (chunk[0] === 0x1d) { term.close(); return; } // ctrl-] detaches
174
- term.write(chunk.toString('utf8'));
204
+ if (chunk[0] === 0x1d) { detaching = true; term?.close(); return; } // ctrl-] detaches
205
+ term?.write(chunk.toString('utf8'));
175
206
  });
176
207
  }
177
- process.stdout.on('resize', () => term.resize(process.stdout.columns, process.stdout.rows));
208
+ process.stdout.on('resize', () => term?.resize(process.stdout.columns, process.stdout.rows));
178
209
  return;
179
210
  }
180
211
  default:
package/mail/relay.mjs CHANGED
@@ -373,8 +373,8 @@ export class Relays {
373
373
  */
374
374
  /** Whether `sender`'s relay still holds a send queued (its queue file, written within SEND_TIMEOUT_MS twice over): a
375
375
  * relay runtime outlives a daemon that ended mid-send and may still make it. */
376
- sending(sender) {
377
- try { return Date.now() - statSync(this.spec(sender).queue).mtimeMs < 2 * SEND_TIMEOUT_MS; } catch { return false; }
376
+ sending(sender, senderName) {
377
+ try { return Date.now() - statSync(this.spec(sender, senderName).queue).mtimeMs < 2 * SEND_TIMEOUT_MS; } catch { return false; }
378
378
  }
379
379
 
380
380
  /** Whether message `messageId` from `sender` landed in Claude session `sessionId`'s transcript, as a relay send reads
package/mail/send.mjs CHANGED
@@ -330,13 +330,29 @@ export class MailSend {
330
330
  // at its first tool call finds it, and a daemon that ends before the relay answers (an upgrade's restart, a crash)
331
331
  // leaves it for the carrier, which hands it over again (t_b0f9a2a0: filed read before, such a send was lost). It is
332
332
  // read once the relay has made it; a relay that fails takes back the copy this delivery filed
333
+ // never beside another hand-over of the same message: one running here, one a relay that outlived its daemon may
334
+ // still be making, or one an earlier daemon made that landed (every path: a wake's handOver, an agent's re-plan)
335
+ const key = `${to}\n${handed.id}`;
336
+ const unanswered = (detail) => Object.assign(new Error(detail), { unanswered: true });
337
+ if (this.relaying.has(key)) throw unanswered('its relay send is still running here');
338
+ const earlier = this.store.hasWake(to, handed.id) ? this.store.wakeState(to, handed.id).handover_at_ms : null;
339
+ if (earlier != null) {
340
+ if (this.relays.sending?.(handed.from, handed.from_name)) throw unanswered('its relay may still be making it');
341
+ let landed = false;
342
+ try { landed = await this.relays.landed(door.session.address.split(':').slice(3).join(':'), handed.id, handed.from); } catch { /* unread: hand it over */ }
343
+ if (landed) {
344
+ try { this.store.claimFor(to, handed.id, { pid: process.pid, via: 'door', caller: null, recipient: true, at_ms: Date.now() }); } catch { /* best kept */ }
345
+ const now = this.store.findIn(to, handed.id);
346
+ if (now?.state === 'unread') { try { this.store.markRead(to, now.path); } catch { /* moved meanwhile */ } }
347
+ return 'already';
348
+ }
349
+ }
333
350
  const filedHere = this.store.findIn(to, handed.id) == null;
334
351
  const filed = this.store.deliver(to, handed);
335
352
  if (wake && !this.store.hasWake(to, handed.id)) {
336
353
  // its hand-over begins now: a daemon that restarts reads the receiver's transcript before sending it again
337
354
  try { this.store.requestWake(to, handed.id); this.store.setWakeState(to, handed.id, { attempts: 0, next_at_ms: Date.now() + RELAY_GRACE_MS, handover_at_ms: Date.now() }); } catch { /* the relay's own outcome stands */ }
338
355
  }
339
- const key = `${to}\n${handed.id}`;
340
356
  this.relaying.add(key);
341
357
  let relayed;
342
358
  try { relayed = await this.relays.send(handed.from, handed.from_name, { sessionId: door.session.address.split(':').slice(3).join(':'), name: door.session.name.split('@')[0], socket: door.session.socket }, text, handed.id); }
@@ -783,19 +799,6 @@ export class MailSend {
783
799
  // tried again after its backoff, settled by that hand-over's own outcome
784
800
  const key = `${address}\n${id}`;
785
801
  if (this.handing.has(key) || this.relaying.has(key)) return { outcome: 'later', detail: 'its hand-over is still in progress' };
786
- // an earlier hand-over never settled (the daemon ended mid-way): one its receiver's transcript holds landed, and is
787
- // read; any other is handed over again, never given up (t_b0f9a2a0)
788
- if (state.handover_at_ms != null && door.kind === 'native') {
789
- // a relay that outlived its daemon may still be making that send (its queue still there, fresh): waited for, never
790
- // sent beside it (#1264 review P2a)
791
- if (this.relays.sending?.(sent.from, id)) return { outcome: 'later', detail: 'its relay may still be making it' };
792
- let landed = false;
793
- try { landed = await this.relays.landed(door.session.address.split(':').slice(3).join(':'), id, sent.from); } catch { /* unread: hand it over */ }
794
- if (landed) {
795
- try { this.store.claimFor(address, id, { pid: process.pid, via: 'door', caller: null, recipient: true, at_ms: Date.now() }); this.store.markRead(address, found.path); } catch { /* moved meanwhile */ }
796
- return { outcome: 'filed', detail: 'an earlier hand-over landed' };
797
- }
798
- }
799
802
  this.store.setWakeState(address, id, { ...state, handover_at_ms: Date.now() });
800
803
  this.handing.add(key);
801
804
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-teams",
3
- "version": "0.3.129",
3
+ "version": "0.3.131",
4
4
  "type": "module",
5
5
  "description": "A machine daemon serving this machine's doors, and a local or remote Teams server for shared session discovery, OpenTelemetry ingestion and access to enrolled machines.",
6
6
  "license": "MIT",