@volter/supercode-teams 0.2.129 → 0.2.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/workspace.mjs +17 -4
- package/connector/index.mjs +4 -3
- package/machine-events.mjs +79 -0
- package/machine.mjs +11 -1
- package/package.json +2 -2
- package/worker/index.mjs +9 -2
package/bin/workspace.mjs
CHANGED
|
@@ -13,6 +13,8 @@ import { moveWorkspace } from '../workspace-move.mjs';
|
|
|
13
13
|
import { TeamsConnector, readSyncStatus } from '../connector/index.mjs';
|
|
14
14
|
import { syncStatusLines } from './workspace/sync-status.mjs';
|
|
15
15
|
import { CONTINUATION_FIDELITY, CONTINUATION_HARNESSES } from '../connector/native-continuation.mjs';
|
|
16
|
+
import { MachineEvents } from '../machine-events.mjs';
|
|
17
|
+
import { openLocalMachine, nodeRpc } from '../client.mjs';
|
|
16
18
|
import { TeamsMachine } from '../machine.mjs';
|
|
17
19
|
import { teamsHome, paths } from '@volter/teams/home';
|
|
18
20
|
import { loadOrCreateKeyPair, publicKeyId } from '@volter/teams/identity';
|
|
@@ -564,6 +566,12 @@ async function sourcesCommand(sub, command) {
|
|
|
564
566
|
}
|
|
565
567
|
|
|
566
568
|
async function machinesCommand(sub, { flags, rest, output, client, context, team }) {
|
|
569
|
+
if (['subscribe','unsubscribe','events'].includes(sub)) {
|
|
570
|
+
requireArgs(rest,sub==='events'?0:1,sub==='events'?0:1,`teams machines ${sub} ${sub==='events'?'':'ADDRESS'}`);
|
|
571
|
+
const channel=await openLocalMachine(paths(teamsHome()).socket);
|
|
572
|
+
try { const rpc=await nodeRpc(channel); return output(await rpc.request(`machine.events.${sub}`,{team_id:context.team_id,address:rest[0]})); }
|
|
573
|
+
finally { channel.close(); }
|
|
574
|
+
}
|
|
567
575
|
if (sub === 'list') return output(await team.machines.list());
|
|
568
576
|
if (sub === 'roots') { requireArgs(rest, 1, 1, 'teams machines roots MACHINE'); return output(await client.request('GET', teamPath(context, `/machines/${rest[0]}/roots`))); }
|
|
569
577
|
if (sub === 'authorize-continuation') {
|
|
@@ -617,7 +625,7 @@ async function machinesCommand(sub, { flags, rest, output, client, context, team
|
|
|
617
625
|
return output(await client.request('POST', machineBase, { body: { subject_id: subject, caps, targets, expires_at: Date.now() + ttl } }));
|
|
618
626
|
}
|
|
619
627
|
}
|
|
620
|
-
usage('teams machines list|roots|access|authorize-continuation');
|
|
628
|
+
usage('teams machines list|roots|access|authorize-continuation|subscribe|unsubscribe|events');
|
|
621
629
|
}
|
|
622
630
|
|
|
623
631
|
async function syncCommand(sub, { flags, rest, output, client, context, team, home }) {
|
|
@@ -745,7 +753,9 @@ async function connectorCommand(verb, { flags, rest, output, client, context, te
|
|
|
745
753
|
const liveCore = new SupercodeHarnessClient({ command: binary, args: ['harness', 'serve'] });
|
|
746
754
|
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 });
|
|
747
755
|
// A connected machine is a person's: each pane it launches opens a window on its screen (SUPERCODE_POPUPS=0: none).
|
|
756
|
+
const machineEvents = new MachineEvents({home:daemonHome,server:context.origin,teamId:context.team_id,credential:saved.credential.token,machineName:saved.machine.name,binary});
|
|
748
757
|
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) });
|
|
758
|
+
machine.machineEvents = machineEvents;
|
|
749
759
|
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`); });
|
|
750
760
|
// The link's state for the watch after start: down until it is up, down again (since when) when it drops. A
|
|
751
761
|
// `replaced` link is left alone: another connector for this machine owns it, by design.
|
|
@@ -758,12 +768,12 @@ async function connectorCommand(verb, { flags, rest, output, client, context, te
|
|
|
758
768
|
});
|
|
759
769
|
// The connector is the service's daemon and keeps serving; a daemon that took its socket path is the one to stop.
|
|
760
770
|
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`));
|
|
761
|
-
const stopAll = async () => { await machine.stop().catch(() => {}); await connector.stop(); await core.close(); await largeCore.close(); await slowCore.close(); await liveCore.close(); await lifecycle.close(); state.update(current => current ? ({ ...current, process: undefined }) : null); };
|
|
771
|
+
const stopAll = async () => { machineEvents.stop(); await machine.stop().catch(() => {}); await connector.stop(); await core.close(); await largeCore.close(); await slowCore.close(); await liveCore.close(); await lifecycle.close(); state.update(current => current ? ({ ...current, process: undefined }) : null); };
|
|
762
772
|
// A daemon with no server link holding the socket is a session's auto-start while this service was between
|
|
763
773
|
// restarts (from a supercode old enough not to wait for the service): it yields to the service's connector, which
|
|
764
774
|
// otherwise failed on EADDRINUSE at every restart and left this machine unlinked.
|
|
765
775
|
if (saved.service) await retireLinklessDaemon(paths(daemonHome).machine);
|
|
766
|
-
try { await connector.start({ roots: () => saved.roots });
|
|
776
|
+
try { await machine.start(); await connector.start({ roots: () => saved.roots }); machineEvents.start(); } catch (error) { await stopAll(); throw error; }
|
|
767
777
|
output({ version: 1, context: scope(context), data: { machine: saved.machine, source: saved.source, roots: saved.roots.map(({ path: _path, ...item }) => item), socket: machine.socketPath, foreground: true } });
|
|
768
778
|
// Run by a service, the connector moves to an upgrade on its own: its readers would otherwise answer from the code
|
|
769
779
|
// they loaded until something restarted it (after 0.5.51 this Mac's went on reading a Codex turn waiting on an
|
|
@@ -792,6 +802,9 @@ async function connectorCommand(verb, { flags, rest, output, client, context, te
|
|
|
792
802
|
if (saved.service) {
|
|
793
803
|
const linkWatch = setInterval(() => {
|
|
794
804
|
if (linkReplaced || linkDownSince === null || Date.now() - linkDownSince < LINK_LOST_MS) return;
|
|
805
|
+
// A server outage is retried in place. Restart only a broken link while
|
|
806
|
+
// HTTP heartbeats still prove that server reachable, not every two minutes of an outage.
|
|
807
|
+
if (!connector.lastHeartbeatAt || Date.now() - connector.lastHeartbeatAt > 60_000) return;
|
|
795
808
|
clearInterval(linkWatch);
|
|
796
809
|
process.stderr.write(`supercode teams: this machine has had no link to its Teams server for ${Math.round((Date.now() - linkDownSince) / 1000)} s; restarting the connector\n`);
|
|
797
810
|
void (async () => { await stopConnector?.(); process.exit(75); })();
|
|
@@ -993,7 +1006,7 @@ function printHelp() {
|
|
|
993
1006
|
members invite|list|remove|role
|
|
994
1007
|
groups list|create|remove|add-member|remove-member
|
|
995
1008
|
sources create|list|status|pause|revoke|credentials|config
|
|
996
|
-
machines list|roots|access|authorize-continuation
|
|
1009
|
+
machines list|roots|access|authorize-continuation|subscribe|unsubscribe|events
|
|
997
1010
|
sync add|list|show|pause|resume|remove
|
|
998
1011
|
sessions list|search|show|follow|access|rename|delete|url|attach|export|continue|bind|unbind
|
|
999
1012
|
Use --help with a command for its required arguments and options.
|
package/connector/index.mjs
CHANGED
|
@@ -141,7 +141,7 @@ export class TeamsConnector {
|
|
|
141
141
|
clearSessionProblem(harness,nativeSessionId){
|
|
142
142
|
const status=this.state.sync_status;if(!status?.problems?.some(item=>item.harness===harness&&item.session_id===nativeSessionId))return;
|
|
143
143
|
status.problems=status.problems.filter(item=>!(item.harness===harness&&item.session_id===nativeSessionId));this.saveQuietly();
|
|
144
|
-
} async request(method,path,body){const json=body===undefined?undefined:JSON.stringify(body),mutation=['POST','PATCH','DELETE'].includes(method);let response;for(let attempt=0;;attempt++){response=await this.fetch(this.server+`/api/v1/teams/${this.teamId}`+path,{method,redirect:'manual',headers:{Authorization:`Bearer ${this.credential}`,'Content-Type':'application/json',...(mutation?{'Idempotency-Key':hash(`${method}\n${path}\n${json??''}`)}:{})},body:json,signal:this.abort.signal});
|
|
144
|
+
} async request(method,path,body){const json=body===undefined?undefined:JSON.stringify(body),mutation=['POST','PATCH','DELETE'].includes(method);let response;for(let attempt=0;;attempt++){response=await this.fetch(this.server+`/api/v1/teams/${this.teamId}`+path,{method,redirect:'manual',headers:{Authorization:`Bearer ${this.credential}`,'Content-Type':'application/json',...(mutation?{'Idempotency-Key':hash(`${method}\n${path}\n${json??''}`)}:{})},body:json,signal:AbortSignal.any([this.abort.signal, AbortSignal.timeout(30_000)])});
|
|
145
145
|
// A rate limit is waited out (the server says how long; the same idempotency key makes the retry safe), so a
|
|
146
146
|
// large first sync slows down instead of failing its pass and restarting the connector.
|
|
147
147
|
if(response.status!==429||attempt>=5)break;// The server names the wait in error.details (wire.mjs validationFailure); a Retry-After header is the fallback.
|
|
@@ -159,7 +159,8 @@ export class TeamsConnector {
|
|
|
159
159
|
if(pruned)this.save();
|
|
160
160
|
}
|
|
161
161
|
async start({bindings=()=>[],roots=()=>[]}={}) {
|
|
162
|
-
|
|
162
|
+
// Remote outages must not prevent the local machine doors from opening.
|
|
163
|
+
// Configuration and heartbeat failures are retried by their existing timers.
|
|
163
164
|
// Heartbeats are serialized (the server takes each sequence number once) and run on their own
|
|
164
165
|
// timer: a long sync pass (a large first capture) never makes a live machine read as offline.
|
|
165
166
|
let beating = Promise.resolve();
|
|
@@ -174,6 +175,7 @@ export class TeamsConnector {
|
|
|
174
175
|
// What each synced session's harness is doing (its turn: working, idle, …), from the last discovery.
|
|
175
176
|
...(this.reportedActivity ? { activity: this.reportedActivity } : {}),
|
|
176
177
|
});
|
|
178
|
+
this.lastHeartbeatAt = this.clock.nowMs();
|
|
177
179
|
// The sessions someone is following now (a viewer or channel open on them) skip the capture floors.
|
|
178
180
|
if (Array.isArray(reply?.watched)) this.watched = new Set(reply.watched.map(item => `${item.harness}:${item.native_id}`));
|
|
179
181
|
this.save();
|
|
@@ -188,7 +190,6 @@ export class TeamsConnector {
|
|
|
188
190
|
await this.pollEffects();
|
|
189
191
|
};
|
|
190
192
|
this.localRoots = await roots();
|
|
191
|
-
await heartbeat();
|
|
192
193
|
// The first sync pass runs behind `start`, so the machine's link opens without waiting for it.
|
|
193
194
|
let running = false;
|
|
194
195
|
const syncPass = () => {
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// One retained server stream per connector. Subscriptions and pending deliveries
|
|
2
|
+
// live beside the connector, so neither a restart nor a lost reply duplicates mail.
|
|
3
|
+
import { mkdirSync, readFileSync, writeFileSync, renameSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
7
|
+
|
|
8
|
+
export class MachineEvents {
|
|
9
|
+
constructor({home,server,teamId,credential,machineName,binary,env=process.env}) {
|
|
10
|
+
Object.assign(this,{server,teamId,credential,binary,env});
|
|
11
|
+
this.machine=machineName.split('.')[0].toLowerCase().replace(/[^a-z0-9_-]/g,'-');
|
|
12
|
+
const key=createHash('sha256').update(`${server}\0${teamId}`).digest('hex').slice(0,20);
|
|
13
|
+
const dir=join(home,'machine-events');mkdirSync(dir,{recursive:true,mode:0o700});this.file=join(dir,`${key}.json`);
|
|
14
|
+
try{this.state=JSON.parse(readFileSync(this.file,'utf8'));}catch(error){if(error.code!=='ENOENT')throw error;this.state={cursor:null,items:{},subscriptions:[],pending:[]};}
|
|
15
|
+
this.abort=new AbortController();this.connected=false;
|
|
16
|
+
}
|
|
17
|
+
save(){const temp=`${this.file}.${process.pid}.tmp`;writeFileSync(temp,JSON.stringify(this.state)+'\n',{mode:0o600});renameSync(temp,this.file);}
|
|
18
|
+
snapshot(){return {team_id:this.teamId,connected:this.connected,cursor:this.state.cursor,items:Object.values(this.state.items),subscriptions:this.state.subscriptions,pending:this.state.pending.length,error:this.error??null};}
|
|
19
|
+
subscribe(address,remove=false){
|
|
20
|
+
if(!/^sc:[^:\s]+:[^:\s]+:[^\s]+$/.test(address??'')||address.split(':')[1]!==this.machine)throw new Error('subscribe a full session or board address on this connector machine');
|
|
21
|
+
this.state.subscriptions=this.state.subscriptions.filter(value=>value!==address);
|
|
22
|
+
if(!remove)this.state.subscriptions.push(address);
|
|
23
|
+
else this.state.pending=this.state.pending.filter(item=>item.to!==address);
|
|
24
|
+
this.save();return this.snapshot();
|
|
25
|
+
}
|
|
26
|
+
start(){void this.follow();this.deliveryTimer=setInterval(()=>void this.flush(),5000);this.deliveryTimer.unref?.();}
|
|
27
|
+
stop(){this.abort.abort();clearInterval(this.deliveryTimer);}
|
|
28
|
+
async follow(){
|
|
29
|
+
while(!this.abort.signal.aborted){
|
|
30
|
+
const streamAbort=new AbortController();
|
|
31
|
+
let deadline=setTimeout(()=>streamAbort.abort(new Error('machine event stream timed out')),45000);
|
|
32
|
+
try{
|
|
33
|
+
const suffix=this.state.cursor==null?'':`?after=${this.state.cursor}`;
|
|
34
|
+
const response=await fetch(`${this.server}/api/v1/teams/${this.teamId}/machines/events${suffix}`,{headers:{Authorization:`Bearer ${this.credential}`,Accept:'text/event-stream'},redirect:'error',signal:AbortSignal.any([this.abort.signal,streamAbort.signal])});
|
|
35
|
+
if(!response.ok||!response.headers.get('content-type')?.includes('text/event-stream'))throw new Error(`machine events returned HTTP ${response.status}, expected event stream`);
|
|
36
|
+
const reader=response.body.getReader();const decoder=new TextDecoder();let buffer='';
|
|
37
|
+
try{for(;;){const {value,done}=await reader.read();if(done)break;clearTimeout(deadline);deadline=setTimeout(()=>streamAbort.abort(new Error('machine event stream timed out')),45000);buffer+=decoder.decode(value,{stream:true});if(buffer.length>2*1024*1024)throw new Error('machine event exceeded stream bound');let boundary;while((boundary=buffer.indexOf('\n\n'))>=0){const frame=buffer.slice(0,boundary);buffer=buffer.slice(boundary+2);const data=frame.split('\n').filter(line=>line.startsWith('data:')).map(line=>line.slice(5).trimStart()).join('\n');if(data)await this.receive(JSON.parse(data));}}}finally{await reader.cancel().catch(()=>{});}
|
|
38
|
+
}catch(error){if(!this.abort.signal.aborted){if(this.error!==error.message)process.stderr.write(`supercode teams machine events: ${error.message}\n`);this.error=error.message;}}
|
|
39
|
+
clearTimeout(deadline);this.connected=false;
|
|
40
|
+
if(!this.abort.signal.aborted)await new Promise(resolve=>{const done=()=>{clearTimeout(timer);this.abort.signal.removeEventListener('abort',done);resolve();};const timer=setTimeout(done,5000);this.abort.signal.addEventListener('abort',done,{once:true});});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async receive(event){
|
|
44
|
+
if(event.type==='machine_ready'){this.connected=true;this.error=null;return;}
|
|
45
|
+
if(event.type==='access_revoked')throw new Error('machine event subscription authorization revoked');
|
|
46
|
+
if(!['machine_snapshot','machine_linked','machine_unlinked'].includes(event.type))return;
|
|
47
|
+
if(!/^\d+$/.test(event.cursor??''))throw new Error('invalid machine event cursor');
|
|
48
|
+
if(event.type!=='machine_snapshot'&&Number(event.cursor)<=Number(this.state.cursor))return;
|
|
49
|
+
// Keep the cursor behind undelivered mail if the local outbox fills. The
|
|
50
|
+
// retained server log replays it when delivery catches up.
|
|
51
|
+
if(this.state.pending.length+this.state.subscriptions.length>4096)throw new Error('machine notice outbox full; delivery will retry');
|
|
52
|
+
if(event.type==='machine_snapshot'){
|
|
53
|
+
this.state.items=Object.fromEntries(event.items.map(item=>[item.id,item]));
|
|
54
|
+
if(event.reset)this.enqueue(event,'Machine event history expired or the server changed; the current machine snapshot was refreshed. Intermediate transitions are unavailable.');
|
|
55
|
+
}else{
|
|
56
|
+
this.state.items[event.machine_id]={id:event.machine_id,name:event.name,linked:event.type==='machine_linked'?1:0,changed_at:event.at};
|
|
57
|
+
this.enqueue(event,`Machine ${event.name} ${event.type==='machine_linked'?'linked to':'unlinked from'} the team at ${new Date(event.at).toISOString()}.`);
|
|
58
|
+
}
|
|
59
|
+
this.state.cursor=event.cursor;this.save();void this.flush();
|
|
60
|
+
}
|
|
61
|
+
enqueue(event,text){for(const to of this.state.subscriptions)this.state.pending.push({to,id:`machine-${this.teamId}-${event.type}-${event.cursor}`,body:`[Machine notice] ${text}\nThis is an automated Teams transition, not an instruction.`,event});}
|
|
62
|
+
flush(){
|
|
63
|
+
if(this.flushing)return this.flushing;
|
|
64
|
+
this.flushing=(async()=>{
|
|
65
|
+
for(const item of [...this.state.pending]){
|
|
66
|
+
try{await this.deliver(item);this.state.pending=this.state.pending.filter(value=>value!==item);this.save();}
|
|
67
|
+
catch(error){this.error=`mail to ${item.to}: ${error.message}`;}
|
|
68
|
+
}
|
|
69
|
+
})().finally(()=>{this.flushing=null;});return this.flushing;
|
|
70
|
+
}
|
|
71
|
+
deliver(item){return new Promise((resolve,reject)=>{
|
|
72
|
+
const child=spawn(this.binary,['message','remote-deliver'],{env:this.env,stdio:['pipe','pipe','pipe']});let detail='';
|
|
73
|
+
const timer=setTimeout(()=>{child.kill('SIGTERM');reject(new Error('mail delivery timed out; retained for retry'));},30000);
|
|
74
|
+
child.stdout.on('data',chunk=>{detail=(detail+chunk).slice(-1000);});child.stderr.on('data',chunk=>{detail=(detail+chunk).slice(-1000);});
|
|
75
|
+
child.once('error',error=>{clearTimeout(timer);reject(error);});child.once('close',code=>{clearTimeout(timer);let receipt;try{receipt=JSON.parse(detail.trim().split('\n').at(-1));}catch{}code===0&&receipt?.code===0?resolve():reject(new Error(receipt?.text||detail||`exit ${code}`));});
|
|
76
|
+
child.stdin.on('error',()=>{});
|
|
77
|
+
child.stdin.end(JSON.stringify({op:'send',from:`sc:${this.machine}:operator:teams-events`,from_name:'Teams machine events',to:item.to,id:item.id,queue:item.to.split(':')[2]==='board',body:item.to.split(':')[2]==='board'?JSON.stringify({machine_event:item.event,text:item.body}):item.body}));
|
|
78
|
+
});}
|
|
79
|
+
}
|
package/machine.mjs
CHANGED
|
@@ -207,12 +207,15 @@ export class TeamsMachine extends MachineCore {
|
|
|
207
207
|
async beforeListen() {
|
|
208
208
|
mkdirSync(this.portsDir, { recursive: true }); // the port table's announce directory exists before any pane launches
|
|
209
209
|
this.#startServe();
|
|
210
|
-
this.#startMailWatch();
|
|
211
210
|
if (this.#pendingNotices().length) this.#watchEnds();
|
|
212
211
|
this.jobs = new JobOwner({ home: this.home, env: this.env, log: (row) => this.log.append({ principal: null, cap: 'exec', target: `job:${row.job}`, outcome: row.outcome, detail: row.reason }) });
|
|
213
212
|
this.jobs.reapOrphans();
|
|
214
213
|
}
|
|
215
214
|
|
|
215
|
+
async afterListen() {
|
|
216
|
+
this.#startMailWatch();
|
|
217
|
+
}
|
|
218
|
+
|
|
216
219
|
async beforeClose() {
|
|
217
220
|
clearInterval(this.endWatch); this.endWatch = null;
|
|
218
221
|
// A launch still waiting for its pane to be ready is abandoned, not hurried:
|
|
@@ -1291,6 +1294,13 @@ export class TeamsMachine extends MachineCore {
|
|
|
1291
1294
|
if (!operator) throw Object.assign(new Error('listing machine voice packs requires operator authority'), { code: 403 });
|
|
1292
1295
|
return [...this.voiceProcesses].map(([session, child]) => ({ session, pid: child.pid }));
|
|
1293
1296
|
}
|
|
1297
|
+
case 'machine.events.events':
|
|
1298
|
+
case 'machine.events.subscribe':
|
|
1299
|
+
case 'machine.events.unsubscribe': {
|
|
1300
|
+
if (!operator) throw Object.assign(new Error('machine event subscriptions require operator authority'), {code:403});
|
|
1301
|
+
if (!this.machineEvents || params.team_id!==this.machineEvents.teamId) throw new Error('this connector is not serving that team');
|
|
1302
|
+
return method==='machine.events.events'?this.machineEvents.snapshot():this.machineEvents.subscribe(params.address,method.endsWith('.unsubscribe'));
|
|
1303
|
+
}
|
|
1294
1304
|
case 'describe':
|
|
1295
1305
|
return this.describe();
|
|
1296
1306
|
case 'panes.present': {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-teams",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.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",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"@volter/supercode-harness-sdk": "^0.3.43",
|
|
63
63
|
"@volter/supercode-terminal": "^0.2.30",
|
|
64
64
|
"@volter/identity": "^0.4.1",
|
|
65
|
-
"@volter/teams": "^0.1.
|
|
65
|
+
"@volter/teams": "^0.1.54",
|
|
66
66
|
"@xterm/addon-serialize": "^0.14.0",
|
|
67
67
|
"@xterm/headless": "^6.0.0",
|
|
68
68
|
"protobufjs": "8.8.0",
|
package/worker/index.mjs
CHANGED
|
@@ -134,7 +134,14 @@ export class TeamsServer extends DurableObject {
|
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
export default {
|
|
137
|
-
fetch(request, env) {
|
|
138
|
-
return env.TEAMS_SERVER.get(env.TEAMS_SERVER.idFromName('server')).fetch(request);
|
|
137
|
+
async fetch(request, env) {
|
|
138
|
+
try { return await env.TEAMS_SERVER.get(env.TEAMS_SERVER.idFromName('server')).fetch(request); }
|
|
139
|
+
catch (error) {
|
|
140
|
+
// Failures before the HTTP handler (object initialization, migrations or
|
|
141
|
+
// storage) otherwise become a platform HTML page, not our API contract.
|
|
142
|
+
const requestId = crypto.randomUUID();
|
|
143
|
+
console.error(`Teams request ${requestId} failed before response: ${error.message}`);
|
|
144
|
+
return Response.json({ version: 1, error: { code: 'unavailable', message: 'Teams server is temporarily unavailable', request_id: requestId, retryable: true } }, { status: 503, headers: { 'Retry-After': '5', 'X-Request-ID': requestId } });
|
|
145
|
+
}
|
|
139
146
|
},
|
|
140
147
|
};
|