@volter/supercode-teams 0.2.92 → 0.2.94

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/teams.mjs CHANGED
@@ -271,7 +271,9 @@ async function main() {
271
271
  try { request = JSON.parse(Buffer.concat(chunks).toString('utf8')); } catch { die('mail reads one JSON request on stdin'); }
272
272
  const channel = await machineChannel();
273
273
  let stream;
274
- try { stream = await channel.open('mail', { request }); } catch (error) { die(error.message, 1); }
274
+ // A typed line waits as long as the server retries it on a loaded machine (2 minutes), and a little longer.
275
+ const timeoutMs = request?.op === 'turn' ? 150_000 : undefined;
276
+ try { stream = await channel.open('mail', { request }, { timeoutMs }); } catch (error) { die(error.message, 1); }
275
277
  let answered = false;
276
278
  stream.on('data', (chunk) => { answered = true; process.stdout.write(`${chunk}\n`); });
277
279
  stream.on('close', (reason) => {
package/bin/workspace.mjs CHANGED
@@ -730,7 +730,7 @@ async function connectorCommand(verb, { flags, rest, output, client, context, te
730
730
  const liveCore = new SupercodeHarnessClient({ command: binary, args: ['harness', 'serve'] });
731
731
  const connector = new TeamsConnector({ server: context.origin, teamId: context.team_id, machineId: saved.machine.id, sourceId: saved.source.id, credential: saved.credential.token, stateDir: state.directory, core, largeCore, slowCore, liveCore, hostKeyId });
732
732
  // A connected machine is a person's: each pane it launches opens a window on its screen (SUPERCODE_POPUPS=0: none).
733
- const machine = new TeamsMachine({ home: daemonHome, supercode: binary, popups: process.env.SUPERCODE_POPUPS !== '0', ...(flags.tmux_socket ? { tmuxSocket: String(flags.tmux_socket) } : {}), link: { server: context.origin, teamId: context.team_id, machineId: saved.machine.id, credential: saved.credential.token, incarnationId: connector.state.incarnation_id, configuration: () => connector.configuration() }, onTurn: (turn) => connector.noteTurn(turn) });
733
+ const machine = new TeamsMachine({ home: daemonHome, supercode: binary, popups: process.env.SUPERCODE_POPUPS !== '0', ...(flags.tmux_socket ? { tmuxSocket: String(flags.tmux_socket) } : {}), link: { server: context.origin, teamId: context.team_id, machineId: saved.machine.id, credential: saved.credential.token, incarnationId: connector.state.incarnation_id, configuration: () => connector.configuration() }, onTurn: (turn) => connector.noteTurn(turn), onTurnWithdrawn: (turn) => connector.withdrawTurn(turn) });
734
734
  machine.on('link', (event) => { if (event.event === 'replaced') process.stderr.write(`supercode teams: ${event.error}. Two connectors were running for this machine; stop one of them (\`supercode teams disconnect\` where it should not run).\n`); });
735
735
  // The connector is the service's daemon and keeps serving; a daemon that took its socket path is the one to stop.
736
736
  machine.on('superseded', (path) => process.stderr.write(`supercode teams: another machine daemon replaced this connector's socket at ${path}; stop it (supercode teams status)\n`));
@@ -475,6 +475,13 @@ export class TeamsConnector {
475
475
  notes[key]=[...(notes[key]??[]).filter(note=>now-note.at<TURN_NOTE_MS),{text:String(text).trim(),at,source}];
476
476
  this.save();
477
477
  }
478
+ /** A noted turn its session refused: nobody typed that line through this door, so nothing is named after it. */
479
+ withdrawTurn({harness,session,text,at}){
480
+ const key=`${harness}:${session}`,notes=this.state.turn_notes?.[key];if(!notes)return;
481
+ const kept=notes.filter(note=>!(note.at===at&&note.text===String(text).trim()));
482
+ if(kept.length===notes.length)return;
483
+ if(kept.length)this.state.turn_notes[key]=kept;else delete this.state.turn_notes[key];this.save();
484
+ }
478
485
  labelTurns(key,entries,messages){
479
486
  const labels={...this.state.turn_labels?.[key]},notes=[...(this.state.turn_notes?.[key]??[])];let found=false;
480
487
  for(const note of [...notes]){
package/machine.mjs CHANGED
@@ -98,6 +98,8 @@ function trustClaudeFolder(cwd) {
98
98
  const LAUNCH_SETTLE_MS = 500;
99
99
  /** How many typed lines' keys a machine remembers; a server retries a line for minutes, not thousands of lines. */
100
100
  const TURN_KEYS_KEPT = 1000;
101
+ /** How long a typed line's door gets to confirm it: a loaded machine's reader types it in seconds and answers late. */
102
+ const TURN_CONFIRM_MS = 60_000;
101
103
 
102
104
  /**
103
105
  * What a pane runs for a launch: the program with its arguments, or, when the caller named a whole command line
@@ -149,11 +151,12 @@ export class TeamsMachine extends MachineCore {
149
151
  * server link; `configuration()` resolves the machine's current configuration from the server
150
152
  * (custodian and machine grants). Without it the daemon serves its local door only.
151
153
  */
152
- constructor({ home = teamsHome(), supercode = 'supercode', tmuxSocket, floor = null, limits = null, env = process.env, link = null, popups = false, onTurn = null } = {}) {
154
+ constructor({ home = teamsHome(), supercode = 'supercode', tmuxSocket, floor = null, limits = null, env = process.env, link = null, popups = false, onTurn = null, onTurnWithdrawn = null } = {}) {
153
155
  super({ home, floor, limits, env, link });
154
156
  this.supercodeBin = supercode;
155
157
  // Told of each turn a Room member typed here (the connector labels the captured message with who typed it).
156
158
  this.onTurn = onTurn;
159
+ this.onTurnWithdrawn = onTurnWithdrawn;
157
160
  this.tmuxSocket = tmuxSocket ?? null;
158
161
  // Each pane launched here opens a terminal window on this machine's screen (popupWindow).
159
162
  this.popups = Boolean(popups);
@@ -501,22 +504,26 @@ export class TeamsMachine extends MachineCore {
501
504
  if (this.#turnKeys.size > TURN_KEYS_KEPT) this.#turnKeys.delete(this.#turnKeys.keys().next().value);
502
505
  }
503
506
  const reply = (value) => { settle?.(value); answer(value); };
504
- const at = Date.now();
507
+ // Who typed it is noted before it is typed: a capture can read the session's new line before the door confirms
508
+ // it (a loaded machine confirmed a frozen-then-resumed line after the line was captured, and it went up unnamed).
509
+ // A refusal withdraws the note; an unconfirmed turn keeps it, as the line may well be in the session.
510
+ const note = { harness, session, text: request.text, at: Date.now(), source: { via: 'room', from: { principalId: principal ?? null, name: entry.name ?? null } } };
511
+ try { this.onTurn?.(note); } catch { /* naming is best-effort; the turn goes either way */ }
505
512
  this.#serveRequest('harness.v1.sessions.message', {
506
513
  locator: { harness, session_id: session, storage: { kind: 'file', path: '' } },
507
514
  text: request.text,
508
515
  from_name: entry.name ?? principal ?? 'a Room member',
509
516
  as_user: true,
510
- }, 15_000).then((result) => {
517
+ }, TURN_CONFIRM_MS).then((result) => {
511
518
  const delivered = result?.message_id && !result.refusal;
512
519
  this.log.append({ principal, cap: 'interact', target, outcome: delivered ? 'turn' : 'failed', detail: result?.refusal?.message?.slice(0, 200) });
513
- if (delivered) try { this.onTurn?.({ harness, session, text: request.text, at, source: { via: 'room', from: { principalId: principal ?? null, name: entry.name ?? null } } }); } catch { /* the turn is delivered either way */ }
520
+ if (!delivered) try { this.onTurnWithdrawn?.(note); } catch { /* nothing to withdraw */ }
514
521
  reply(delivered
515
522
  ? { code: 0, turn_id: result.message_id, door: result.delivery?.how === 'waiting' ? 'waiting' : (result.delivery?.door ?? 'pane') }
516
523
  : { code: 1, text: result?.refusal?.message ?? 'the session did not take the turn' });
517
524
  }, (error) => {
518
- this.log.append({ principal, cap: 'interact', target, outcome: 'failed', detail: error.message.slice(0, 200) });
519
- reply({ code: 1, text: error.message });
525
+ this.log.append({ principal, cap: 'interact', target, outcome: 'unconfirmed', detail: error.message.slice(0, 200) });
526
+ reply({ code: 1, text: `the session did not confirm the line (${error.message}); it may have been typed` });
520
527
  });
521
528
  }
522
529
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-teams",
3
- "version": "0.2.92",
3
+ "version": "0.2.94",
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",