@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 +10 -1
- package/bin/workspace.mjs +7 -2
- package/board-events.mjs +23 -3
- package/machine.mjs +5 -1
- package/package.json +2 -2
- package/worker/index.mjs +7 -3
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
}
|