@volter/supercode-teams 0.3.124 → 0.3.125

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
@@ -31,6 +31,7 @@
31
31
  // CLI in ./workspace.mjs.
32
32
  import { appendFileSync, createWriteStream, readFileSync, writeFileSync } from 'node:fs';
33
33
  import { spawnSync } from 'node:child_process';
34
+ import { resolve } from 'node:path';
34
35
  import { fileURLToPath, pathToFileURL } from 'node:url';
35
36
  import { SupercodeHarnessClient } from '@volter/supercode-harness-sdk';
36
37
  import { noteStep, rotatedLogs, slowLogPath, timeCommand } from '@volter/supercode-harness-sdk/slow-log';
@@ -195,7 +196,15 @@ async function main() {
195
196
  }
196
197
  // `machine restart [--machine M] [--reason text]`: the daemon (this one, or M's through the server) restarts under its service.
197
198
  if (sub === 'restart') return withNode(async (rpc) => out(await rpc.call('machine.restart', { reason: flags.reason ? String(flags.reason) : null })));
198
- die('machine start|status|restart');
199
+ // `machine boards [list]` and `machine boards unregister <root>`: the board homes this machine's connector follows
200
+ // and publishes, and one no longer followed (a proof's scratch home: t_f7b219b8)
201
+ if (sub === 'boards') {
202
+ const [action = 'list', root] = rest;
203
+ if (action === 'list') return withNode(async (rpc) => out(await rpc.call('harness.v1.boards.list', {})));
204
+ if (action === 'unregister' && root) return withNode(async (rpc) => out(await rpc.call('harness.v1.boards.unregister', { root: resolve(root) })));
205
+ die('machine boards [list] | machine boards unregister <root>');
206
+ }
207
+ die('machine start|status|restart|boards');
199
208
  }
200
209
  case 'slow': {
201
210
  // `slow [--since 1h|30m|<iso>]`: the slow log on this machine, grouped by what was slow (kind and name), the
package/bin/workspace.mjs CHANGED
@@ -1477,7 +1477,9 @@ function stampStderr() {
1477
1477
  const decoder = new StringDecoder('utf8');
1478
1478
  let atStart = true;
1479
1479
  process.stderr.write = (chunk, ...rest) => {
1480
- const text = typeof chunk === 'string' ? chunk : decoder.write(Buffer.from(chunk));
1480
+ // a string written while a Buffer's split character is pending ends that character first (as U+FFFD, the bytes
1481
+ // it had), so the bytes are never held back to land after the string
1482
+ const text = typeof chunk === 'string' ? decoder.end() + chunk : decoder.write(Buffer.from(chunk));
1481
1483
  // an empty write (or a Buffer holding only part of a character) changes no line
1482
1484
  if (!text) return write(text, ...rest);
1483
1485
  const stamp = new Date().toISOString();
@@ -1497,7 +1499,10 @@ async function releaseSkills() {
1497
1499
  const release = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..', '..');
1498
1500
  try {
1499
1501
  if (JSON.parse(readFileSync(join(release, 'package.json'), 'utf8')).name !== '@volter/supercode') return [];
1500
- const { installSkills } = await import(pathToFileURL(join(release, 'lib', 'skills.mjs')).href);
1502
+ const { installSkills, notThisHomesOwner } = await import(pathToFileURL(join(release, 'lib', 'skills.mjs')).href);
1503
+ // only as the home's owner (a `sudo -E` connect would leave root-owned skill folders in the user's harness homes)
1504
+ const refused = notThisHomesOwner?.();
1505
+ if (refused) return [`skills not installed (${refused}); run \`supercode skills install\` as the home's owner`];
1501
1506
  return installSkills();
1502
1507
  } catch { return []; }
1503
1508
  }
package/board-events.mjs CHANGED
@@ -5,8 +5,8 @@
5
5
  // Each board home's cursor is kept beside the connector, so a restart resumes where it left off; a published event's id
6
6
  // is derived from its server, team, source and publication, so a follower that starts over republishes the same ids.
7
7
  import { spawn } from 'node:child_process';
8
- import { mkdirSync, readFileSync, realpathSync, renameSync, writeFileSync } from 'node:fs';
9
- import { join } from 'node:path';
8
+ import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, writeFileSync } from 'node:fs';
9
+ import { join, resolve } from 'node:path';
10
10
  import { createHash } from 'node:crypto';
11
11
  import { createInterface } from 'node:readline';
12
12
  import { TeamsClient, retryAfterMs, spentClause } from '@volter/teams/http-client';
@@ -91,6 +91,23 @@ export class BoardEvents {
91
91
  }
92
92
  return { root, registered: true, refused: this.refused };
93
93
  }
94
+ /**
95
+ * A home no longer followed: its follower ended for good and its record dropped, so no restart brings it back
96
+ * (t_f7b219b8: 16 scratch proof homes on aarons-mac-mini, each with its own follower, after every daemon start). By an
97
+ * operator (`supercode teams machine boards unregister <root>`), or here for a home whose folder is gone. Its cards
98
+ * already published stay on the team.
99
+ */
100
+ unregister(path, why = 'unregistered') {
101
+ let root = resolve(String(path));
102
+ try { root = realpathSync(root); } catch { /* gone: named as it was registered */ }
103
+ if (!this.state.boards[root]) return { root, unregistered: false };
104
+ delete this.state.boards[root]; delete this.state.sources[root]; this.save();
105
+ this.waits.delete(root);
106
+ const child = this.followers.get(root);
107
+ if (child) { child.gone = true; end(child); }
108
+ this.log(`board events of ${root}: no longer followed (${why})`);
109
+ return { root, unregistered: true };
110
+ }
94
111
  start() {
95
112
  this.started = true;
96
113
  for (const root of Object.keys(this.state.boards)) this.follow(root);
@@ -101,6 +118,9 @@ export class BoardEvents {
101
118
  follow(root) {
102
119
  if (this.followers.has(root) || this.stopped || this.refused) return;
103
120
  const board = this.state.boards[root];
121
+ if (!board) return; // unregistered while it waited
122
+ // a home whose folder is gone (a proof's scratch home, removed) has no board left to follow: dropped, not retried
123
+ if (!existsSync(root)) { this.unregister(root, 'its folder is gone'); return; }
104
124
  // In a process group of its own (end): the follower is the native binary, the orchestrator it runs and the one that
105
125
  // hands the line to, and ending the first alone left the others running, holding its output open.
106
126
  const child = spawn(this.binary, ['workflow', 'events', '--root', root, ...(board.cursor ? ['--after', board.cursor] : [])], { env: this.env, stdio: ['ignore', 'pipe', 'pipe'], detached: true });
@@ -148,7 +168,7 @@ export class BoardEvents {
148
168
  child.on('close', async (code) => {
149
169
  await frames;
150
170
  this.followers.delete(root);
151
- if (this.stopped || this.refused) return;
171
+ if (this.stopped || this.refused || child.gone) return;
152
172
  if (child.again) { this.follow(root); return; } // registered again: on the orchestrator the home names now
153
173
  // its last lines, a stack among them when the store's own error ended it (orchestrator.mjs)
154
174
  if (code && !board.error) this.log(`board events of ${root}: workflow events ended (${code}): ${stderr.trim().split('\n').slice(-12).join('\n ')}`);
package/machine.mjs CHANGED
@@ -3990,11 +3990,15 @@ export class TeamsMachine extends MachineCore {
3990
3990
  if (entry.principal !== 'local' || entry.appDoor) throw Object.assign(new Error('a caller is resolved only for a process on this machine'), { code: 403 });
3991
3991
  return { caller: await this.#callerOf(entry, params) };
3992
3992
  }
3993
- case 'harness.v1.boards.register': case 'harness.v1.boards.list': {
3993
+ case 'harness.v1.boards.register': case 'harness.v1.boards.list': case 'harness.v1.boards.unregister': {
3994
3994
  // A board home this machine keeps, published to its team by its connector (board-events.mjs).
3995
3995
  if (!operator) throw Object.assign(new Error('operators only'), { code: 403 });
3996
3996
  if (!this.boardEvents) throw Object.assign(new Error('this daemon serves no team: a board is published by a connected machine (`supercode teams connect`)'), { code: 409 });
3997
3997
  if (method === 'harness.v1.boards.list') return this.boardEvents.list();
3998
+ if (method === 'harness.v1.boards.unregister') {
3999
+ if (typeof params.root !== 'string' || !params.root) throw Object.assign(new Error('name the board home (root)'), { code: 400 });
4000
+ return this.boardEvents.unregister(params.root);
4001
+ }
3998
4002
  if (typeof params.root !== 'string' || !params.root) throw Object.assign(new Error('name the board home (root)'), { code: 400 });
3999
4003
  const registered = this.boardEvents.register(params.root);
4000
4004
  // The home's board owner acts as its boards: the caller it names for a board of that home is taken from it
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-teams",
3
- "version": "0.3.124",
3
+ "version": "0.3.125",
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",
@@ -70,7 +70,7 @@
70
70
  "@volter/supercode-health": "^0.1.0",
71
71
  "@volter/supercode-terminal": "^0.2.54",
72
72
  "@volter/supercode-trace": "^0.1.0",
73
- "@volter/teams": "0.2.34",
73
+ "@volter/teams": "0.2.35",
74
74
  "@xterm/addon-serialize": "^0.14.0",
75
75
  "@xterm/headless": "^6.0.0",
76
76
  "protobufjs": "8.8.0",
package/worker/index.mjs CHANGED
@@ -145,7 +145,7 @@ export class TeamsServer extends DurableObject {
145
145
 
146
146
  /** What the Durable Object's runtime said of a failed call (its error's own flags), for the log and the answer. */
147
147
  const objectFailure = (error) => ({
148
- name: String(error?.name ?? 'Error'),
148
+ name: String(error?.name ?? 'Error').slice(0, 100),
149
149
  message: String(error?.message ?? error).slice(0, 200),
150
150
  retryable: error?.retryable === true,
151
151
  overloaded: error?.overloaded === true,
@@ -173,12 +173,16 @@ export default {
173
173
  },
174
174
  };
175
175
 
176
+ /** The error names an unauthenticated caller is told; any other (a library's or the runtime's own) is said as
177
+ * `Error`, and is in the log line only (#1246 review). */
178
+ const PUBLIC_NAMES = new Set(['Error', 'TypeError', 'RangeError', 'ReferenceError', 'SyntaxError', 'AbortError', 'TimeoutError', 'DOMException']);
179
+
176
180
  /** Failures before the HTTP handler (object initialization, migrations, storage, a reset) as our API contract, not a
177
181
  * platform HTML page. The answer goes to callers not yet authenticated, so it names only the error's kind and the
178
182
  * runtime's flags; its message (storage or SQL text, internal paths) is in the log line its request id leads to. */
179
183
  function unavailable(failure, retried) {
180
184
  const requestId = crypto.randomUUID();
181
185
  console.error(JSON.stringify({ event: 'teams_unavailable', request_id: requestId, failure, ...(retried ? { first: retried } : {}) }));
182
- const cause = `${failure.name}${failure.reset ? ', object reset' : ''}${failure.overloaded ? ', overloaded' : ''}${retried ? ', after one retry' : ''}`;
183
- return Response.json({ version: 1, error: { code: 'unavailable', message: `Teams server is temporarily unavailable (${cause})`, request_id: requestId, retryable: true, cause: { name: failure.name, retryable: failure.retryable, overloaded: failure.overloaded, remote: failure.remote, reset: failure.reset } } }, { status: 503, headers: { 'Retry-After': failure.overloaded ? '15' : '5', 'X-Request-ID': requestId } });
186
+ const cause = `${PUBLIC_NAMES.has(failure.name) ? failure.name : 'Error'}${failure.reset ? ', object reset' : ''}${failure.overloaded ? ', overloaded' : ''}${retried ? ', after one retry' : ''}`;
187
+ return Response.json({ version: 1, error: { code: 'unavailable', message: `Teams server is temporarily unavailable (${cause})`, request_id: requestId, retryable: true, cause: { name: PUBLIC_NAMES.has(failure.name) ? failure.name : 'Error', retryable: failure.retryable, overloaded: failure.overloaded, remote: failure.remote, reset: failure.reset } } }, { status: 503, headers: { 'Retry-After': failure.overloaded ? '15' : '5', 'X-Request-ID': requestId } });
184
188
  }