@volter/supercode-teams 0.2.38 → 0.2.40

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.
@@ -137,6 +137,8 @@ export class TeamsConnector {
137
137
  bindings: [...this.nativeEffects.published(), ...await bindings()],
138
138
  roots: this.localRoots.map(({id,machine_id,name,harnesses})=>({id,machine_id,name,harnesses})),
139
139
  ...(this.hostKeyId ? { host_key_id: this.hostKeyId } : {}),
140
+ // What each synced session's harness is doing (its turn: working, idle, …), from the last discovery.
141
+ ...(this.reportedActivity ? { activity: this.reportedActivity } : {}),
140
142
  });
141
143
  this.save();
142
144
  });
@@ -204,7 +206,8 @@ export class TeamsConnector {
204
206
  // A rule covers its root folder and every folder under it. Core discovery filters that subtree before it
205
207
  // reads any session's tail, so a beat costs one bounded scan per root, not one over every session on the
206
208
  // machine; rules over the same root and harnesses share the scan.
207
- const discoveries=new Map(),followCandidates=[],discovered=new Set();
209
+ const discoveries=new Map(),followCandidates=[],discovered=new Set(),activity=[];
210
+ this.turnStates??=new Map();
208
211
  for(const rule of activeRules.values()){
209
212
  const root=roots.find(item=>item.id===rule.selector.root_id);
210
213
  if(!root)continue;
@@ -225,6 +228,9 @@ export class TeamsConnector {
225
228
  const locator=descriptor.locator,nativeSessionId=locator?.session_id,harness=locator?.harness;
226
229
  if(!locator||!nativeSessionId||!harness)continue;
227
230
  discovered.add(`${harness}:${nativeSessionId}`);
231
+ // A session's turn state as its harness reports it, and since when this connector has seen it so.
232
+ const turn=descriptor.activity?.turn;
233
+ if(typeof turn==='string'&&turn&&activity.length<4096){const key=`${harness}:${nativeSessionId}`,held=this.turnStates.get(key),since=held?.turn===turn?held.since:this.clock.nowMs();this.turnStates.set(key,{turn,since});activity.push({harness,native_id:nativeSessionId,turn,since});}
228
234
  const options={locator,descriptor,harness,nativeSessionId,ruleId:rule.id,ruleRevision:rule.revision,backfill:rule.backfill,rootPath:workspace,configuration};
229
235
  // A session whose native store has not changed since this rule last captured it is not reloaded.
230
236
  // One session that cannot be captured (a transcript that rewrote its history needs an explicit resync)
@@ -243,6 +249,8 @@ export class TeamsConnector {
243
249
  }
244
250
  }
245
251
  this.nativeEffects.endDiscovery();
252
+ for(const key of this.turnStates.keys())if(!discovered.has(key))this.turnStates.delete(key);
253
+ this.reportedActivity=activity;
246
254
  this.pruneNativeCache(discovered);
247
255
  this.rotateFollowers(followCandidates);
248
256
  }
package/machine.mjs CHANGED
@@ -419,19 +419,24 @@ export class TeamsMachine extends MachineCore {
419
419
  return refuse(`no interact grant for ${target}`);
420
420
  }
421
421
  accept();
422
- const child = spawn(this.supercodeBin, ['message', 'remote-deliver'], { stdio: ['pipe', 'pipe', 'pipe'], env: this.env });
423
- let out = '';
424
- let err = '';
425
- child.stdout.on('data', (chunk) => { out += chunk; });
426
- child.stderr.on('data', (chunk) => { err += chunk; });
427
- child.on('error', (error) => { err += error.message; });
428
- child.on('close', (code) => {
429
- this.log.append({ principal, cap: 'interact', target, outcome: code === 0 ? 'turn' : 'failed', detail: err.trim().slice(0, 200) || undefined });
430
- if (stream.closed) return;
431
- stream.write(out.trim() || JSON.stringify({ code: 1, text: err.trim() || `remote-deliver exited ${code}` }));
432
- stream.close('delivered');
422
+ // The user-turn door every supercode has (harness.v1 sessions.message, as_user): the session's runtime input,
423
+ // its pane's empty composer, or its mailbox until the composer is free. The sender is named after the member.
424
+ const answer = (value) => { if (!stream.closed) { stream.write(JSON.stringify(value)); stream.close('delivered'); } };
425
+ this.#serveRequest('harness.v1.sessions.message', {
426
+ locator: { harness, session_id: session, storage: { kind: 'file', path: '' } },
427
+ text: request.text,
428
+ from_name: entry.name ?? principal ?? 'a Room member',
429
+ as_user: true,
430
+ }, 15_000).then((result) => {
431
+ const delivered = result?.message_id && !result.refusal;
432
+ this.log.append({ principal, cap: 'interact', target, outcome: delivered ? 'turn' : 'failed', detail: result?.refusal?.message?.slice(0, 200) });
433
+ answer(delivered
434
+ ? { code: 0, turn_id: result.message_id, door: result.delivery?.how === 'waiting' ? 'waiting' : (result.delivery?.door ?? 'pane') }
435
+ : { code: 1, text: result?.refusal?.message ?? 'the session did not take the turn' });
436
+ }, (error) => {
437
+ this.log.append({ principal, cap: 'interact', target, outcome: 'failed', detail: error.message.slice(0, 200) });
438
+ answer({ code: 1, text: error.message });
433
439
  });
434
- child.stdin.end(JSON.stringify({ op: 'turn', harness, session, text: request.text, from_name: entry.name ?? principal ?? 'a Room member', via: 'room' }));
435
440
  }
436
441
 
437
442
  // ---- terminal ------------------------------------------------------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-teams",
3
- "version": "0.2.38",
3
+ "version": "0.2.40",
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",
@@ -172,9 +172,16 @@ async function listSessions(api,res,auth,url,requestId,search){
172
172
  if(search)body.coverage={scope:'retained_text',indexed_at:boundaryNow,indexed_revision:team.journal_revision};
173
173
  send(res,200,body,requestId);
174
174
  }
175
+ /** A report older than this (four missed heartbeats) says nothing about what the session is doing now. */
176
+ const ACTIVITY_FRESH_MS=60000;
177
+ /** `{since}` while the session's harness is working on a turn, by its connector's latest fresh report; else null. */
178
+ async function workingOf(api,teamId,sessionId){
179
+ const row=await api.store.one('SELECT a.turn,a.since,a.observed_at FROM session_activity a JOIN origins o ON o.team_id=a.team_id AND o.source_id=a.source_id AND o.harness=a.harness AND o.native_id=a.native_id WHERE a.team_id=? AND o.deleted_at IS NULL AND o.session_id IN (SELECT id FROM sessions WHERE team_id=? AND (id=? OR canonical_id=?)) ORDER BY a.observed_at DESC LIMIT 1',[teamId,teamId,sessionId,sessionId]);
180
+ return row&&row.turn==='working'&&api.clock.nowMs()-row.observed_at<ACTIVITY_FRESH_MS?{since:row.since??row.observed_at}:null;
181
+ }
175
182
  async function sessionDescriptor(api,res,auth,id,requestId){
176
183
  const row=await api.store.authorizedSession({teamId:auth.team_id,principalId:auth.principal_id,credentialId:auth.credential_id,ref:id,now:api.clock.nowMs(),bound:auth.bound});
177
- send(res,200,result(api,auth,JSON.parse(row.descriptor_json)),requestId,{ETag:`"${row.revision}"`});
184
+ send(res,200,result(api,auth,{...JSON.parse(row.descriptor_json),working:await workingOf(api,auth.team_id,row.id)}),requestId,{ETag:`"${row.revision}"`});
178
185
  }
179
186
  async function correlateRoute(api,req,res,auth,path,requestId){
180
187
  if(!['user','service'].includes(auth.kind)||auth.role==='viewer')fail(403,'forbidden','credential cannot correlate sessions');
@@ -198,7 +205,8 @@ async function readSession(api,res,auth,id,url,requestId){
198
205
  const snapshot=await api.store.readSessionSnapshot({teamId:auth.team_id,principalId:auth.principal_id,credentialId:auth.credential_id,ref:id,now:api.clock.nowMs(),bound:auth.bound,beforeHash:query.before?cursorHash(query.before):null,requestedRevision:query.revision,tail,nextCursorHash:cursorHash(nextCursor),nextCursorToken:nextCursor});
199
206
  const {rows,beforeCursor,revision}=snapshot;
200
207
  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)}));
201
- send(res,200,{version:1,context:{server_id:api.serverId,team_id:auth.team_id,principal_id:auth.principal_id},descriptor:JSON.parse(snapshot.descriptor_json),messages,before_cursor:beforeCursor,event_cursor:snapshot.eventCursor,revision,display:{returned:messages.length,total:snapshot.total,truncated_fields:0}},requestId);
208
+ const descriptor=JSON.parse(snapshot.descriptor_json);
209
+ 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);
202
210
  }
203
211
  async function readArtifact(api,req,res,auth,id,uploadId,requestId){
204
212
  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;