@volter/supercode-teams 0.3.124 → 0.3.126
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 +18 -3
- package/bin/workspace.mjs +7 -2
- package/board-events.mjs +23 -3
- package/codex-startup.mjs +10 -1
- package/machine.mjs +33 -9
- 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';
|
|
@@ -54,7 +55,7 @@ import { APP_DOOR_PATH, appDoorFile, appDoorLink, normalOrigin, readAppDoor, rea
|
|
|
54
55
|
|
|
55
56
|
const MACHINE_VERBS = new Set(['machine', 'slow', 'describe', 'records', 'panes', 'input', 'suspend', 'resume', 'files', 'ports', 'port', 'browser', 'stream', 'jobs', 'workspace', 'title', 'attention', 'watch', 'rpc', 'launches', 'log', 'mail', 'board', 'health', 'app-door']);
|
|
56
57
|
// Every flag a machine verb reads; any other is refused rather than silently ignored.
|
|
57
|
-
const KNOWN_FLAGS = new Set(['home', 'since', 'machine', 'context', 'floor', 'supercode', 'tmux-socket', 'verbose', 'lines', 'input', 'name', 'for', 'urgency', 'reason', 'interrupt', 'query', 'out', 'in', 'interval', 'no-enter', 'when-composer-empty', 'key', 'listen', 'list', 'from', 'to', 'path', 'to-path', 'repo', 'branch', 'solo', 'card', 'create', 'presence-only', 'processes', 'panes', 'clear-draft', 'message-ids', 'harness', 'observe', 'attach-context', 'json', 'all', 'origin', 'port', 'remove-origin', 'open']);
|
|
58
|
+
const KNOWN_FLAGS = new Set(['home', 'since', 'machine', 'context', 'floor', 'supercode', 'tmux-socket', 'verbose', 'lines', 'input', 'name', 'for', 'urgency', 'reason', 'interrupt', 'query', 'out', 'in', 'interval', 'no-enter', 'when-composer-empty', 'key', 'listen', 'list', 'from', 'to', 'path', 'to-path', 'repo', 'branch', 'solo', 'card', 'create', 'presence-only', 'processes', 'panes', 'clear-draft', 'message-ids', 'harness', 'observe', 'attach-context', 'json', 'all', 'origin', 'port', 'remove-origin', 'open', 'foreign']);
|
|
58
59
|
|
|
59
60
|
const argv = process.argv.slice(2);
|
|
60
61
|
const flags = {};
|
|
@@ -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
|
|
@@ -238,7 +247,13 @@ async function main() {
|
|
|
238
247
|
flags.processes ? 'panes.processes' : flags['presence-only'] ? 'panes.present' : 'panes.list',
|
|
239
248
|
flags['presence-only'] && typeof flags.panes === 'string' ? { panes: flags.panes.split(',').filter(Boolean) } : {})));
|
|
240
249
|
if (sub === 'capture') return withNode(async (rpc) => out((await rpc.call('panes.capture', { pane: rest[0], lines: Number(flags.lines ?? 200) })).text));
|
|
241
|
-
|
|
250
|
+
// One pane, named: `--foreign` closes a pane another session started, and is said for that pane alone.
|
|
251
|
+
if (sub === 'close') {
|
|
252
|
+
// the flag parser takes `--foreign p_…` as the flag's value: that pane is the one named
|
|
253
|
+
const named = typeof flags.foreign === 'string' ? [flags.foreign, ...rest] : rest;
|
|
254
|
+
if (named.length !== 1) die('panes close <pane> [--foreign]: one pane per close');
|
|
255
|
+
return withNode(async (rpc) => out(await rpc.call('panes.close', { pane: named[0], foreign: flags.foreign !== undefined, ...(await acting()) })));
|
|
256
|
+
}
|
|
242
257
|
// `window <pane> [machine]`: a terminal window on this machine's screen showing the pane, this machine's or, named,
|
|
243
258
|
// another machine's (attached through Teams, in that machine's Teams context `--attach-context`); `--observe` shows it
|
|
244
259
|
// read-only.
|
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/codex-startup.mjs
CHANGED
|
@@ -42,7 +42,16 @@ export function codexStartupArguments(args = []) {
|
|
|
42
42
|
}
|
|
43
43
|
normalized.push(argument);
|
|
44
44
|
}
|
|
45
|
-
|
|
45
|
+
// Codex 0.160's `resume` does not take this override from before the subcommand: a resumed TUI with it there still
|
|
46
|
+
// stopped on "Update available" once another Codex had cached a newer version, and `codex resume <id> -c …` did not
|
|
47
|
+
// (yuerans, 2026-10-06). It goes after the subcommand and its session id (readers of the process take the word after
|
|
48
|
+
// `resume` as the id: codex_peer.rs `resumed_session_in`), in front when there is no subcommand. `fork` is placed the
|
|
49
|
+
// same way by extension; only `resume` was measured.
|
|
50
|
+
const subcommand = normalized.findIndex((argument, index) => ['resume', 'fork'].includes(argument)
|
|
51
|
+
&& !['-c', '--config'].includes(normalized[index - 1]) && !normalized.slice(0, index).includes('--'));
|
|
52
|
+
const id = subcommand >= 0 && normalized[subcommand + 1] !== undefined && normalized[subcommand + 1] !== '--' && !normalized[subcommand + 1].startsWith('-');
|
|
53
|
+
const at = subcommand < 0 ? 0 : subcommand + (id ? 2 : 1);
|
|
54
|
+
return [...normalized.slice(0, at), '-c', 'check_for_update_on_startup=false', ...normalized.slice(at)];
|
|
46
55
|
}
|
|
47
56
|
|
|
48
57
|
export function codexUpdatePrompt(screen) {
|
package/machine.mjs
CHANGED
|
@@ -2079,16 +2079,22 @@ export class TeamsMachine extends MachineCore {
|
|
|
2079
2079
|
}
|
|
2080
2080
|
|
|
2081
2081
|
#refuseForeign(pane, caller) {
|
|
2082
|
-
|
|
2082
|
+
const starter = this.#foreignStarter(pane, caller);
|
|
2083
|
+
if (starter) throw Object.assign(new Error(`pane ${pane} ${starter}, not by ${caller}: a session acts only on the panes it started. Nothing was done; reach that session through its own doors (supercode message send)`), { code: 403 });
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
/** Who started `pane`, said, when `caller` is a session that did not start it; null when the caller may act on it (a
|
|
2087
|
+
* person, a board, its starter). */
|
|
2088
|
+
#foreignStarter(pane, caller) {
|
|
2089
|
+
if (typeof caller !== 'string' || !caller || /^sc:[^:]+:board:/.test(caller)) return null;
|
|
2083
2090
|
const launch = this.launches.all().find((entry) => entry.pane === pane);
|
|
2084
2091
|
const by = launch?.by ?? null;
|
|
2085
|
-
if (by === caller) return;
|
|
2092
|
+
if (by === caller) return null;
|
|
2086
2093
|
// A pane that resumes a session is that session's as well: its first starter acts on it as before the resume
|
|
2087
2094
|
// (t_09a795fd item 18). The starter is the oldest harness launch whose own transcript confirmed it started that
|
|
2088
2095
|
// session, never a launch row's claim (any caller can launch a program with --session-id S).
|
|
2089
|
-
if (launch?.resumes && this.#sessionOrigin(launch.resumes)?.by === caller) return;
|
|
2090
|
-
|
|
2091
|
-
throw Object.assign(new Error(`pane ${pane} ${starter}, not by ${caller}: a session acts only on the panes it started. Nothing was done; reach that session through its own doors (supercode message send)`), { code: 403 });
|
|
2096
|
+
if (launch?.resumes && this.#sessionOrigin(launch.resumes)?.by === caller) return null;
|
|
2097
|
+
return by ? `was started by ${by}` : 'was not started by a session (a person or an earlier daemon started it)';
|
|
2092
2098
|
}
|
|
2093
2099
|
|
|
2094
2100
|
/** The launch that started native Claude session `session`: the oldest journal row of a Claude launch whose own
|
|
@@ -3307,7 +3313,7 @@ export class TeamsMachine extends MachineCore {
|
|
|
3307
3313
|
}
|
|
3308
3314
|
const contextKey = params.pane ?? `p_${randomBytes(9).toString('base64url')}`;
|
|
3309
3315
|
// Who started the pane: the launching session's (or board's) address, null for a person. A session's later
|
|
3310
|
-
// input on the pane is refused unless it is this one (#refuseForeign); close
|
|
3316
|
+
// input on the pane is refused unless it is this one (#refuseForeign); its close needs `foreign` (panes.close).
|
|
3311
3317
|
// Who to tell when the pane ends (`open --new … --notify-when-ended`): a session's mail address, and this
|
|
3312
3318
|
// machine's name as the address a notice from its pane comes from.
|
|
3313
3319
|
if (params.notify !== undefined && !(params.notify && typeof params.notify.to === 'string' && /^sc:[^:\s]+:[^:\s]+:\S+$/.test(params.notify.to) && typeof params.notify.machine === 'string' && /^[a-z0-9][a-z0-9-]*$/.test(params.notify.machine))) throw Object.assign(new Error('launch notify is { to: <sc: address>, machine: <this machine\'s mail name> }'), { code: 400 });
|
|
@@ -3459,14 +3465,28 @@ export class TeamsMachine extends MachineCore {
|
|
|
3459
3465
|
case 'harness.v1.panes.close':
|
|
3460
3466
|
case 'panes.close': {
|
|
3461
3467
|
need('control', `pane:${params.pane}`);
|
|
3462
|
-
|
|
3468
|
+
// A session closes the panes it started and the one it runs in. Another's pane is closed only when the caller
|
|
3469
|
+
// says so (`foreign`), one pane at a time, by name: a listing piped into close is refused pane by pane rather
|
|
3470
|
+
// than ending every session on the machine (docs/adr/0004-pane-close-authority.md).
|
|
3471
|
+
const starting = [...this.pendingStartups.values()].filter((pending) => pending.pane === params.pane);
|
|
3472
|
+
// only a pane here (open, or still starting) is anyone's: one this machine lacks is answered as absent, before
|
|
3473
|
+
// the caller is asked for
|
|
3474
|
+
const here = starting.length > 0 || Boolean(await this.#paneByContext(params.pane));
|
|
3475
|
+
const closeBy = here ? await this.#callerOf(entry, params) : null;
|
|
3476
|
+
const starter = params.foreign === true || !here ? null : this.#foreignStarter(params.pane, closeBy);
|
|
3477
|
+
// the pane the caller runs in: a Claude row names it as its tmux locator (`p_…:@window.%pane`), the others as the pane
|
|
3478
|
+
if (starter && !this.live.sessions().some((live) => live.address === closeBy && String(live.pane ?? '').split(':')[0] === params.pane)) {
|
|
3479
|
+
act('close', `pane:${params.pane}`, 'refused', `foreign: ${starter}`);
|
|
3480
|
+
throw Object.assign(new Error(`pane ${params.pane} ${starter}, not by ${closeBy}. Nothing was closed. A session closes the panes it started; closing another's is deliberate: read what the pane holds, settle it with whoever started it, then close that one pane with --foreign`), { code: 403 });
|
|
3481
|
+
}
|
|
3482
|
+
for (const pending of starting) pending.abort.abort();
|
|
3463
3483
|
const session = await this.#paneByContext(params.pane);
|
|
3464
3484
|
if (!session) {
|
|
3465
3485
|
act('close', `pane:${params.pane}`, 'ok', 'already absent');
|
|
3466
3486
|
return { closed: params.pane, alreadyAbsent: true };
|
|
3467
3487
|
}
|
|
3468
3488
|
await this.host.closeSession(session.id);
|
|
3469
|
-
act('close', `pane:${params.pane}`);
|
|
3489
|
+
act('close', `pane:${params.pane}`, 'ok', params.foreign === true ? `foreign, by ${closeBy ?? 'a person'}` : null);
|
|
3470
3490
|
this.#panesChanged();
|
|
3471
3491
|
return { closed: params.pane };
|
|
3472
3492
|
}
|
|
@@ -3990,11 +4010,15 @@ export class TeamsMachine extends MachineCore {
|
|
|
3990
4010
|
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
4011
|
return { caller: await this.#callerOf(entry, params) };
|
|
3992
4012
|
}
|
|
3993
|
-
case 'harness.v1.boards.register': case 'harness.v1.boards.list': {
|
|
4013
|
+
case 'harness.v1.boards.register': case 'harness.v1.boards.list': case 'harness.v1.boards.unregister': {
|
|
3994
4014
|
// A board home this machine keeps, published to its team by its connector (board-events.mjs).
|
|
3995
4015
|
if (!operator) throw Object.assign(new Error('operators only'), { code: 403 });
|
|
3996
4016
|
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
4017
|
if (method === 'harness.v1.boards.list') return this.boardEvents.list();
|
|
4018
|
+
if (method === 'harness.v1.boards.unregister') {
|
|
4019
|
+
if (typeof params.root !== 'string' || !params.root) throw Object.assign(new Error('name the board home (root)'), { code: 400 });
|
|
4020
|
+
return this.boardEvents.unregister(params.root);
|
|
4021
|
+
}
|
|
3998
4022
|
if (typeof params.root !== 'string' || !params.root) throw Object.assign(new Error('name the board home (root)'), { code: 400 });
|
|
3999
4023
|
const registered = this.boardEvents.register(params.root);
|
|
4000
4024
|
// 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.126",
|
|
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
|
}
|