@volter/supercode-teams 0.2.51 → 0.2.53
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 +4 -2
- package/connector/index.mjs +13 -5
- package/package.json +1 -1
- package/server/event-routes.mjs +17 -1
- package/server/harness-routes.mjs +2 -2
package/bin/workspace.mjs
CHANGED
|
@@ -724,11 +724,13 @@ async function connectorCommand(verb, { flags, rest, output, client, context, te
|
|
|
724
724
|
// answers one request at a time, and a long load would hold discovery and every other capture behind it.
|
|
725
725
|
const largeCore = new SupercodeHarnessClient({ command: binary, args: ['harness', 'serve'] });
|
|
726
726
|
const slowCore = new SupercodeHarnessClient({ command: binary, args: ['harness', 'serve'] });
|
|
727
|
-
|
|
727
|
+
// Followed (live) sessions follow and load through one of their own, so a watched session never waits behind a pass.
|
|
728
|
+
const liveCore = new SupercodeHarnessClient({ command: binary, args: ['harness', 'serve'] });
|
|
729
|
+
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 });
|
|
728
730
|
// A connected machine is a person's: each pane it launches opens a window on its screen (SUPERCODE_POPUPS=0: none).
|
|
729
731
|
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) });
|
|
730
732
|
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`); });
|
|
731
|
-
const stopAll = async () => { await machine.stop().catch(() => {}); await connector.stop(); await core.close(); await largeCore.close(); await slowCore.close(); await lifecycle.close(); state.update(current => current ? ({ ...current, process: undefined }) : null); };
|
|
733
|
+
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); };
|
|
732
734
|
try { await connector.start({ roots: () => saved.roots }); await machine.start(); } catch (error) { await stopAll(); throw error; }
|
|
733
735
|
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 } });
|
|
734
736
|
await new Promise(resolveStop => { let stopped = false; stopConnector = async () => { if (stopped) return; stopped = true; await stopAll(); resolveStop(); }; for (const signal of ['SIGINT', 'SIGTERM', ...(process.platform === 'win32' ? ['SIGHUP', 'SIGBREAK'] : [])]) process.once(signal, stopConnector); });
|
package/connector/index.mjs
CHANGED
|
@@ -88,7 +88,7 @@ function atomic(path, value) {
|
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
export class TeamsConnector {
|
|
91
|
-
constructor({server,teamId,machineId,sourceId,credential,stateDir,core,largeCore=core,slowCore=largeCore,hostKeyId=null,effectExecutor=null,clock={nowMs:()=>Date.now()},fetch:fetchImpl=globalThis.fetch,heartbeatMs=15000,nativeIdleMs=NATIVE_IDLE_MS,nativeIdleBytes=NATIVE_IDLE_BYTES}){Object.assign(this,{server:serverOrigin(server),teamId,machineId,sourceId,credential,stateDir,core,largeCore,slowCore,hostKeyId,effectExecutor,clock,fetch:fetchImpl,heartbeatMs,nativeIdleMs,nativeIdleBytes});mkdirSync(stateDir,{recursive:true,mode:0o700});chmodSync(stateDir,0o700);this.statePath=join(stateDir,'connector.json');this.state=existsSync(this.statePath)?JSON.parse(readFileSync(this.statePath,'utf8')):{version:1,sequence:0,origins:{},outbox:[]};this.state.effects??={};if(Array.isArray(this.state.sync_status?.problems))this.state.sync_status.problems=this.state.sync_status.problems.filter(item=>item?.code!==CAPACITY_CODE);this.state.native_cache??={};this.state.incarnation_id=`inc_${randomBytes(16).toString('hex')}`;this.abort=new AbortController();this.work=Promise.resolve();this.nativeEffects=new NativeEffects(this);this.effects=new EffectConsumer(this,effectExecutor?{prepare:async effect=>({execute:()=>effectExecutor(effect,{core:this.core,signal:this.abort.signal})})}:this.nativeEffects);this.save();}
|
|
91
|
+
constructor({server,teamId,machineId,sourceId,credential,stateDir,core,largeCore=core,slowCore=largeCore,liveCore=core,hostKeyId=null,effectExecutor=null,clock={nowMs:()=>Date.now()},fetch:fetchImpl=globalThis.fetch,heartbeatMs=15000,nativeIdleMs=NATIVE_IDLE_MS,nativeIdleBytes=NATIVE_IDLE_BYTES}){Object.assign(this,{server:serverOrigin(server),teamId,machineId,sourceId,credential,stateDir,core,largeCore,slowCore,liveCore,hostKeyId,effectExecutor,clock,fetch:fetchImpl,heartbeatMs,nativeIdleMs,nativeIdleBytes});mkdirSync(stateDir,{recursive:true,mode:0o700});chmodSync(stateDir,0o700);this.statePath=join(stateDir,'connector.json');this.state=existsSync(this.statePath)?JSON.parse(readFileSync(this.statePath,'utf8')):{version:1,sequence:0,origins:{},outbox:[]};this.state.effects??={};if(Array.isArray(this.state.sync_status?.problems))this.state.sync_status.problems=this.state.sync_status.problems.filter(item=>item?.code!==CAPACITY_CODE);this.state.native_cache??={};this.state.incarnation_id=`inc_${randomBytes(16).toString('hex')}`;this.abort=new AbortController();this.work=Promise.resolve();this.nativeEffects=new NativeEffects(this);this.effects=new EffectConsumer(this,effectExecutor?{prepare:async effect=>({execute:()=>effectExecutor(effect,{core:this.core,signal:this.abort.signal})})}:this.nativeEffects);this.save();}
|
|
92
92
|
save(){atomic(this.statePath,this.state);}
|
|
93
93
|
// What `supercode teams status` shows about this connector: its last error, and the sessions it could not capture
|
|
94
94
|
// (most recent first, bounded; an entry clears when that session next captures). They are kept in connector.json,
|
|
@@ -291,9 +291,9 @@ export class TeamsConnector {
|
|
|
291
291
|
follow(key,options){
|
|
292
292
|
const {locator}=options,controller=new AbortController(),stop=()=>controller.abort(this.abort.signal.reason);this.abort.signal.addEventListener('abort',stop,{once:true});
|
|
293
293
|
// A follow event may land between beats, after the rule changed: it reads the configuration afresh.
|
|
294
|
-
const followed={...options,configuration:null,stamp:undefined};
|
|
294
|
+
const followed={...options,configuration:null,stamp:undefined,live:true};
|
|
295
295
|
const task=(async()=>{
|
|
296
|
-
for await(const _event of this.
|
|
296
|
+
for await(const _event of this.liveCore.session(locator).follow({signal:controller.signal})){
|
|
297
297
|
if(controller.signal.aborted)break;
|
|
298
298
|
// A capture that fails is that session's problem, as on a beat; the follow goes on.
|
|
299
299
|
try{await this.capture(followed);}catch(error){if(controller.signal.aborted)break;if(connectionError(error))this.lastError=error;else this.noteSessionProblem(followed,error);}
|
|
@@ -403,8 +403,16 @@ export class TeamsConnector {
|
|
|
403
403
|
if(origin&&ruleId&&stamp)origin.capture_stamps={...origin.capture_stamps,[ruleId]:{stamp,rule_revision:ruleRevision,backfill_mode:backfillMode}};
|
|
404
404
|
this.save();
|
|
405
405
|
}
|
|
406
|
-
capture
|
|
407
|
-
|
|
406
|
+
// Every load happens before the capture joins the serial queue, which then holds only the quick compare and
|
|
407
|
+
// upload; a followed (live) session loads through a harness process of its own, so a watched session's new turn
|
|
408
|
+
// never waits behind a pass's loads.
|
|
409
|
+
async capture(options) {
|
|
410
|
+
if(!options.preloaded&&this.largeSession(options))return this.captureLarge(options);
|
|
411
|
+
if(!options.preloaded){
|
|
412
|
+
const {locator}=options,path=locator?.storage?.kind==='file'&&existsSync(locator.storage.path)?realpathSync(locator.storage.path):null;
|
|
413
|
+
const artifactBefore=path?statSync(path):null;
|
|
414
|
+
options={...options,preloaded:{loaded:await (options.live?this.liveCore:this.core).session(locator).load(),artifactBefore}};
|
|
415
|
+
}
|
|
408
416
|
const work=this.work.then(()=>this.captureSerial(options)).then(receipt=>{this.clearSessionProblem(options.harness,options.nativeSessionId);return receipt;});
|
|
409
417
|
this.work=work.catch(()=>{});
|
|
410
418
|
return work;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-teams",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.53",
|
|
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",
|
package/server/event-routes.mjs
CHANGED
|
@@ -66,6 +66,21 @@ const sessionArgs = (api, auth, ref) => ({
|
|
|
66
66
|
credentialId: auth.credential_id, ref, now: api.clock.nowMs(), bound: auth.bound,
|
|
67
67
|
});
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* The people and machines a session's reader sees named: its owner's display name and the names of the machines
|
|
71
|
+
* it ran on. A Room's session token reads no member or machine list, so its view would show raw ids otherwise.
|
|
72
|
+
*/
|
|
73
|
+
export async function sessionNames(api, teamId, descriptor) {
|
|
74
|
+
const owner = descriptor?.user?.principal_id;
|
|
75
|
+
const ids = [...new Set(descriptor?.machine_ids ?? [])].slice(0, 20);
|
|
76
|
+
const person = owner ? await api.store.one('SELECT id, kind, display_name FROM principals WHERE id=?', [owner]) : null;
|
|
77
|
+
const machines = ids.length ? await api.store.query(`SELECT id, name, custodian_id, last_seen_at FROM machines WHERE team_id=? AND id IN (${ids.map(() => '?').join(',')})`, [teamId, ...ids]) : [];
|
|
78
|
+
return {
|
|
79
|
+
members: person ? [{ principal: { id: person.id, kind: person.kind, display_name: person.display_name }, role: 'member' }] : [],
|
|
80
|
+
machines: machines.map((row) => ({ id: row.id, name: row.name, custodian_id: row.custodian_id, connection: 'offline', last_seen_at: row.last_seen_at })),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
69
84
|
async function readForEvent(api, auth, ref, tail) {
|
|
70
85
|
const nextCursorToken = cursorToken();
|
|
71
86
|
const snapshot = await api.store.readSessionSnapshot({
|
|
@@ -76,10 +91,11 @@ async function readForEvent(api, auth, ref, tail) {
|
|
|
76
91
|
id: item.id, source_id: item.source_id, native_key: item.native_key,
|
|
77
92
|
recorded_at: item.recorded_at, value: JSON.parse(item.value_json),
|
|
78
93
|
}));
|
|
94
|
+
const descriptor = JSON.parse(snapshot.descriptor_json);
|
|
79
95
|
return {
|
|
80
96
|
version: 1,
|
|
81
97
|
context: { server_id: api.serverId, team_id: auth.team_id, principal_id: auth.principal_id },
|
|
82
|
-
descriptor:
|
|
98
|
+
descriptor, messages, names: await sessionNames(api, auth.team_id, descriptor),
|
|
83
99
|
before_cursor: snapshot.beforeCursor, event_cursor: snapshot.eventCursor,
|
|
84
100
|
revision: snapshot.revision,
|
|
85
101
|
display: { returned: messages.length, total: snapshot.total, truncated_fields: 0 },
|
|
@@ -10,7 +10,7 @@ import { bearer, hashToken, canRead, canManage, isAdmin } from '@volter/teams/se
|
|
|
10
10
|
import { validateListQuery, validateSearchQuery, validateReadQuery, validateCaptureBegin, validateCapturePart, canonicalJson } from '../contracts/index.mjs';
|
|
11
11
|
import { normalizeSearch, textFromContent, cursorHash, cursorToken } from './catalog.mjs';
|
|
12
12
|
import { captureDigest, captureMessages, handleOtlpRoute, sha256, validateArtifactName } from './ingest.mjs';
|
|
13
|
-
import { handleCatalogEvents, handleSessionEvents } from './event-routes.mjs';
|
|
13
|
+
import { handleCatalogEvents, handleSessionEvents, sessionNames } from './event-routes.mjs';
|
|
14
14
|
import { exact } from '@volter/teams/server/request-json';
|
|
15
15
|
import { delegatedSessionOf, looksDelegated } from '@volter/teams/server/delegated';
|
|
16
16
|
import { renderSourceCollectorConfig } from './otlp/collector-config.mjs';
|
|
@@ -208,7 +208,7 @@ async function readSession(api,res,auth,id,url,requestId){
|
|
|
208
208
|
const {rows,beforeCursor,revision}=snapshot;
|
|
209
209
|
const messages=rows.map(row=>({id:row.id,source_id:row.source_id,native_key:row.native_key,recorded_at:row.recorded_at,value:JSON.parse(row.value_json)}));
|
|
210
210
|
const descriptor=JSON.parse(snapshot.descriptor_json);
|
|
211
|
-
send(res,200,{version:1,context:{server_id:api.serverId,team_id:auth.team_id,principal_id:auth.principal_id},descriptor,working:await workingOf(api,auth.team_id,descriptor.id),messages,before_cursor:beforeCursor,event_cursor:snapshot.eventCursor,revision,display:{returned:messages.length,total:snapshot.total,truncated_fields:0}},requestId);
|
|
211
|
+
send(res,200,{version:1,context:{server_id:api.serverId,team_id:auth.team_id,principal_id:auth.principal_id},descriptor,working:await workingOf(api,auth.team_id,descriptor.id),names:await sessionNames(api,auth.team_id,descriptor),messages,before_cursor:beforeCursor,event_cursor:snapshot.eventCursor,revision,display:{returned:messages.length,total:snapshot.total,truncated_fields:0}},requestId);
|
|
212
212
|
}
|
|
213
213
|
async function readArtifact(api,req,res,auth,id,uploadId,requestId){
|
|
214
214
|
const artifact=await api.store.readArtifact({teamId:auth.team_id,principalId:auth.principal_id,credentialId:auth.credential_id,ref:id,uploadId,now:api.clock.nowMs()}),bytes=Buffer.from(artifact.bytes),range=req.headers.range;
|