@volter/supercode-teams 0.3.154 → 0.3.156

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/README.md CHANGED
@@ -27,7 +27,10 @@ verified caller at its local origin. `harness.v1.sessions.close_remote` accepts
27
27
  that forwarded caller at the target's Teams door only, under pane control. It
28
28
  retains native live-work checks, then calls the existing pane-close door to
29
29
  recheck its grant, recorded starter and host entry. It adds no caller identity
30
- or origin-attestation protocol. A nonstarter needs explicit
30
+ or origin-attestation protocol. Native activity coverage on this path currently
31
+ includes Claude Code and Codex. Other recognized harness panes refuse without
32
+ `foreign: true` when their activity cannot be read; plain command panes are
33
+ handled separately. A nonstarter needs explicit
31
34
  `foreign: true` for that one pane. These are node-door methods, not additions to
32
35
  the native harness service. [Scope and qualification](../../docs/evidence/remote-pane-close-2026-10-08.md).
33
36
 
@@ -49,6 +52,12 @@ Settled read bodies expire after 30 days, with explicit placeholders and retaine
49
52
  its originals. Expired read headers are capped at 1,000,000 and carrier/terminal event log tables at 10,000 rows each. Read the
50
53
  [dated qualification scope](../../docs/evidence/sqlite-mail-store-2026-10-08.md) before inferring platform support.
51
54
 
55
+ Native mail-history reads process the opened file's initial extent in 64 KiB blocks,
56
+ holding one unfinished record instead of the whole transcript and line array. Returned
57
+ mail history and individual records can still be large. The
58
+ [allocation evidence](../../docs/evidence/mail-transcript-stream-2026-10-08.md) names
59
+ the source boundary and the remaining runtime qualification.
60
+
52
61
  `supercode teams rpc mail.statistics` reads local storage counts and import/retention receipts. It is refused remotely
53
62
  and through the app door. Writer groups count stored envelope copies, not call attempts or quotas; database page
54
63
  counts exclude physical WAL size. Successful imports record their actual source reads, bytes, batch sizes and time.
package/bin/workspace.mjs CHANGED
@@ -1160,6 +1160,11 @@ async function connectorCommand(verb, { flags, rest, output, client, context, te
1160
1160
  // on the machine credential and the daemon keeps the agent's reader context for it; a pane session's agent is the
1161
1161
  // one its launch recorded.
1162
1162
  connector.recordAgentSession=binding=>machine.bindAgentSession(binding,{delegator:context.machine_only?null:contextName});
1163
+ machine.agentDelegator=context.machine_only?null:contextName;
1164
+ machine.agentSessionIntent=session=>{
1165
+ const item=connector.state.agent_sessions?.[session];
1166
+ return item&&['pending','bound'].includes(item.state)?{agent:item.agent,org:item.org,context:item.context,assignments:item.assignments}:null;
1167
+ };
1163
1168
  machine.onLaunchAgent=(session,environment)=>connector.queueAgentSession(session,environment);
1164
1169
  connector.flushAgentIdentity=()=>machine.flushAgentIdentity({delegator:context.machine_only?null:contextName});
1165
1170
  connector.agentClosureReceipt=async id=>{
@@ -1411,6 +1416,7 @@ function humanOutput(value) {
1411
1416
  const data = value?.data ?? value;
1412
1417
  if (data === null) return `${context}Done.\n`;
1413
1418
  if (typeof data === 'string') return `${context}${data}\n`;
1419
+ if(typeof data?.session==='string'&&data.agent)return `${context}Session: ${data.session}\nAgent: ${data.agent.path??data.agent.name??data.agent.id} (${data.agent.id})\n${data.main?'Main session':'Concurrent session'}${data.source?`\nSource: ${data.source}`:''}\n`;
1414
1420
  const row = humanRow(data);
1415
1421
  return `${context}${Object.entries(row).map(([key, item]) => `${key}: ${item}`).join('\n')}\n`;
1416
1422
  }
@@ -1423,7 +1429,7 @@ function humanTranscript(descriptor, messages) {
1423
1429
  const lines = messages.map(message => { const value = message.value ?? message; const body = [text(value.content ?? value.text).trim(), calls(value)].filter(Boolean).join('\n'); return `\n${value.role ?? 'message'}:\n${body || '(no text)'}\n`; });
1424
1430
  return head + coverage + (lines.length ? lines.join('') : '\n(no messages in this window)\n');
1425
1431
  }
1426
- function humanRow(value) { if (!value || typeof value !== 'object') return { value }; const descriptor = value.session ?? value; return Object.fromEntries(Object.entries(descriptor).filter(([, item]) => item === null || ['string', 'number', 'boolean'].includes(typeof item)).slice(0, 8)); }
1432
+ function humanRow(value) { if (!value || typeof value !== 'object') return { value }; const descriptor = value.session&&typeof value.session==='object'?value.session:value; return Object.fromEntries(Object.entries(descriptor).filter(([, item]) => item === null || ['string', 'number', 'boolean'].includes(typeof item)).slice(0, 8)); }
1427
1433
  async function stream(iterator, json) { for await (const event of iterator) process.stdout.write(json ? `${JSON.stringify(event)}\n` : humanOutput(event)); }
1428
1434
  function unsupported(message) { throw Object.assign(new Error(message), { code: 'unsupported', exitCode: 5 }); }
1429
1435
  function notFound(message) { throw Object.assign(new Error(message), { code: 'not_found', exitCode: 4 }); }
@@ -1256,12 +1256,15 @@ export class TeamsConnector {
1256
1256
  for(const [session,item] of Object.entries(sessions)){
1257
1257
  if(item.state!=='pending'||item.next_at>now)continue;
1258
1258
  try{
1259
+ // Reading as the launch's agent needs its delegation, not a transcript already captured in Teams. Keep the
1260
+ // reader first, so a slow initial capture cannot strand the session's whoami/sessions commands.
1261
+ const binding=await this.recordAgentSession?.({session,agent:item.agent,org:item.org,context:item.context,assignments:item.assignments});
1262
+ if(binding)item.context=binding.context;
1259
1263
  if(!item.declared){
1260
1264
  await this.request('POST',`/machines/${this.machineId}/agent-sessions`,{session,agent:item.agent,main:item.main});item.declared=true;
1261
1265
  // its lineage exists now: a session held for want of it is sent again in this pass
1262
1266
  if(this.state.profile_holds?.[session])delete this.state.profile_holds[session];
1263
1267
  }
1264
- await this.recordAgentSession?.({session,agent:item.agent,org:item.org,context:item.context,assignments:item.assignments});
1265
1268
  item.state='bound';item.bound_at=now;delete item.error;
1266
1269
  }catch(error){
1267
1270
  item.attempts+=1;item.error=errorSummary(error,now);
package/machine.mjs CHANGED
@@ -365,6 +365,7 @@ export class TeamsMachine extends MachineCore {
365
365
  /** Typed lines by key → the answer their first attempt gives (see #serveTurn). */
366
366
  #turnKeys = new Map();
367
367
  #sessionBindings = new Map();
368
+ #agentReaders = new Map();
368
369
 
369
370
  /** Native process evidence, published by the connector; a missing transport never reports an end. */
370
371
  async sessionBindings(machineId,incarnationId) {
@@ -402,10 +403,10 @@ export class TeamsMachine extends MachineCore {
402
403
 
403
404
  /** An agent's delegated reader credential installed as a context of this machine's supercode home: what its
404
405
  * sessions' Teams verbs run under. */
405
- async #installAgentContext({origin,team,agent,credential}){
406
+ async #installAgentContext({origin,team,agent,credential},signal=AbortSignal.timeout(15000)){
406
407
  if(typeof origin!=='string'||typeof team!=='string'||typeof agent!=='string'||typeof credential?.token!=='string')throw Object.assign(new Error('agent context requires server, Org, identity and delegated credential'),{code:400});
407
408
  const client=new TeamsClient({server:origin,credential:credential.token});
408
- const me=await client.me({signal:AbortSignal.timeout(15000)});
409
+ const me=await client.me({signal});
409
410
  if(me.principal.id!==agent||me.principal.kind!=='agent'||!me.teams.some(item=>item.team.id===team))throw Object.assign(new Error('delegated credential does not belong to this agent and Org'),{code:403});
410
411
  const key=createHash('sha256').update(`${client.server}/${team}/${agent}`).digest('hex');
411
412
  const name=`agent-${key.slice(0,32)}`,ref=`cred_${key}`,contexts=new WorkspaceContexts(supercodeHome(this.env));
@@ -414,23 +415,49 @@ export class TeamsMachine extends MachineCore {
414
415
  return {context:name,agent,org:team,expires_at:credential.expires_at};
415
416
  }
416
417
 
418
+ /** A reader is reused only while it belongs to this agent and Org and has not expired. Concurrent sessions of
419
+ * one agent share a renewal. The connected person's authority is used only to delegate; no command runs as them. */
420
+ async #agentReader({agent,org,context},delegator){
421
+ const contexts=new WorkspaceContexts(supercodeHome(this.env)),document=contexts.read();
422
+ let selected=context?document.contexts[context]:null;
423
+ if(selected&&(selected.principal_id!==agent||(org&&selected.team_id!==org)))
424
+ throw Object.assign(new Error('agent reader context belongs to another agent or Org'),{code:403});
425
+ if(!selected){
426
+ const existing=Object.entries(document.contexts).find(([,item])=>item.principal_id===agent&&item.team_id===org);
427
+ if(existing)[context,selected]=existing;
428
+ }
429
+ org??=selected?.team_id;
430
+ const credential=selected?contexts.credential(selected.credential_ref):null;
431
+ if(credential?.token&&credential.server_id===selected.server_id&&credential.origin===selected.origin
432
+ &&Number.isSafeInteger(credential.expires_at)&&credential.expires_at>Date.now())return {context,org};
433
+ if(!delegator)return {context:null,org};
434
+ const key=`${org??''}/${agent}`;
435
+ if(this.#agentReaders.has(key))return this.#agentReaders.get(key);
436
+ const renewal=(async()=>{
437
+ const source=fromContext({supercodeHome:supercodeHome(this.env),name:delegator}),signal=AbortSignal.timeout(20000);
438
+ if(org&&source.context.team_id!==org)throw Object.assign(new Error('agent reader renewal belongs to another Org'),{code:403});
439
+ const team=source.client.team(source.context.team_id),identity=(await team.agents.get(agent,{signal})).data;
440
+ if(identity.state!=='active')throw Object.assign(new Error('only an active agent receives a reader context'),{code:403});
441
+ const reader=await team.agents.delegateReader(identity.id,{expires_in_ms:86400000},{signal,ifMatch:identity.revision});
442
+ return this.#installAgentContext({origin:source.context.origin,team:source.context.team_id,agent:identity.id,credential:reader.data},signal);
443
+ })();
444
+ this.#agentReaders.set(key,renewal);
445
+ try{return await renewal;}finally{this.#agentReaders.delete(key);}
446
+ }
447
+
417
448
  /**
418
449
  * A session this machine's connector declared its agent's (D138 step 5): the agent's reader context, as its launch
419
450
  * named it, is kept for the session (agent-session-contexts.json, read by `harness.v1.agents.session`), so its Teams
420
- * verbs run as that agent. A launch that named none gets one delegated here through `delegator`, the context of the
421
- * person this machine is connected as, who manages the agent; a machine connected on its machine credential alone
422
- * keeps the session without one.
451
+ * verbs run as that agent. Missing or expired readers are delegated through the connected person's context, which
452
+ * manages the agent. A machine connected on its machine credential alone keeps the session without one.
423
453
  */
424
454
  async bindAgentSession({session,agent,org,context=null,assignments=[]},{delegator=null}={}){
425
- if(!context&&delegator){
426
- const selected=fromContext({supercodeHome:supercodeHome(this.env),name:delegator}),signal=AbortSignal.timeout(20000);
427
- const team=selected.client.team(selected.context.team_id),identity=(await team.agents.get(agent,{signal})).data;
428
- if(identity.state!=='ended'){
429
- const reader=await team.agents.delegateReader(identity.id,{expires_in_ms:86400000},{signal,ifMatch:identity.revision});
430
- context=(await this.#installAgentContext({origin:selected.context.origin,team:selected.context.team_id,agent:identity.id,credential:reader.data})).context;
431
- }
432
- org??=selected.context.team_id;
433
- }
455
+ const previous=readJson(join(this.home,'agent-session-contexts.json'),{sessions:{}}).sessions[session];
456
+ if(previous&&(previous.agent!==agent||(org&&previous.org!==org)))
457
+ throw Object.assign(new Error('a recorded session cannot change agent or Org'),{code:403});
458
+ context??=previous?.context;
459
+ org??=previous?.org;
460
+ ({context,org}=await this.#agentReader({agent,org,context},delegator));
434
461
  const file=join(this.home,'agent-session-contexts.json'),record=readJson(file,{version:1,sessions:{}});
435
462
  record.sessions[session]={context,agent,org,assignments};
436
463
  const temporary=`${file}.${randomBytes(12).toString('hex')}.tmp`;
@@ -3390,7 +3417,10 @@ export class TeamsMachine extends MachineCore {
3390
3417
  case 'harness.v1.agents.session': {
3391
3418
  if(!operator)throw Object.assign(new Error('local agent session lookup requires operator authority'),{code:403});
3392
3419
  if(typeof params.session!=='string')throw Object.assign(new Error('name a native session'),{code:400});
3393
- return readJson(join(this.home,'agent-session-contexts.json'),{sessions:{}}).sessions[params.session]??null;
3420
+ const binding=readJson(join(this.home,'agent-session-contexts.json'),{sessions:{}}).sessions[params.session]
3421
+ ??this.agentSessionIntent?.(params.session);
3422
+ if(!binding)return null;
3423
+ return this.bindAgentSession({session:params.session,...binding},{delegator:this.agentDelegator??null});
3394
3424
  }
3395
3425
  case 'harness.v1.agents.closureReceipt': {
3396
3426
  if(!operator)throw Object.assign(new Error('native closure receipts require operator authority'),{code:403});
@@ -1,17 +1,48 @@
1
1
  // What a session's transcript says about its mail (crates/harness/src/mail_transcript.rs, ported line for line): the
2
2
  // lines its own user typed, the lines it took from its harness's own channels, its answers, and when each filed message
3
- // first reached the model. The mailbox records a message as sent; only the transcript records it delivered. A transcript
4
- // is read only when a call names the message or the session it is about (one named read, never a sweep).
3
+ // first reached the model. The mailbox records a message as sent; only the transcript records it delivered. Named mail
4
+ // reads use this reader; agent upkeep also reads changed Claude transcripts to copy the person's typed lines.
5
5
  import { readdirSync, statSync } from 'node:fs';
6
- import { readFile } from 'node:fs/promises';
6
+ import { open } from 'node:fs/promises';
7
7
  import { join } from 'node:path';
8
8
  import { blake3Hex } from './envelope.mjs';
9
9
 
10
- // A transcript is read off the daemon's event loop (its file read awaited) and parsed in turns of YIELD_LINES lines: one
11
- // large session's history never holds every other door while it is read.
10
+ // Read one native record at a time. Reading the whole file and splitting it before the first yield retained every
11
+ // tool result while parsing unrelated mail. The read buffer is bounded; one record and the returned mail remain.
12
+ const READ_BYTES = 64 * 1024;
12
13
  const YIELD_LINES = 2000;
13
14
  const yieldTurn = () => new Promise((resolve) => setImmediate(resolve));
14
15
 
16
+ async function* transcriptLines(path) {
17
+ const file = await open(path, 'r');
18
+ try {
19
+ // This pass reads the opened file's initial extent; an appended turn belongs to the next pass.
20
+ const { size } = await file.stat();
21
+ let position = 0, fragments = [];
22
+ while (position < size) {
23
+ const buffer = Buffer.allocUnsafe(Math.min(READ_BYTES, size - position));
24
+ const { bytesRead } = await file.read(buffer, 0, buffer.length, position);
25
+ if (!bytesRead) break;
26
+ position += bytesRead;
27
+ const chunk = buffer.subarray(0, bytesRead);
28
+ let start = 0;
29
+ for (let end = chunk.indexOf(10); end !== -1; end = chunk.indexOf(10, start)) {
30
+ const line = fragments.length
31
+ ? Buffer.concat([...fragments, chunk.subarray(start, end)]).toString('utf8')
32
+ : chunk.toString('utf8', start, end);
33
+ fragments = [];
34
+ start = end + 1;
35
+ yield line;
36
+ }
37
+ if (start < bytesRead) fragments.push(chunk.subarray(start));
38
+ // Yield even across a long record or many ignored lines, before another read/parse block.
39
+ await yieldTurn();
40
+ }
41
+ // Preserve the former split's final non-newline record, including its JSON-parse refusal if incomplete.
42
+ if (fragments.length) yield Buffer.concat(fragments).toString('utf8');
43
+ } finally { await file.close(); }
44
+ }
45
+
15
46
  export const TYPED_ID_PREFIX = 'u-';
16
47
  const CHANNEL_ID_PREFIX = 'c-';
17
48
  const ANSWER_ID_PREFIX = 'a-';
@@ -88,10 +119,9 @@ export async function readClaude(path) {
88
119
  const takenIn = new Map(); // words → [times]
89
120
  const early = []; // [words, ids, index, at]
90
121
  const deliver = (id, at) => { if (!mail.delivered.has(id)) mail.delivered.set(id, at); };
91
- const lines = (await readFile(path, 'utf8')).split('\n');
92
- for (let index = 0; index < lines.length; index++) {
93
- if (index && index % YIELD_LINES === 0) await yieldTurn();
94
- const line = lines[index];
122
+ let lineIndex = 0;
123
+ for await (const line of transcriptLines(path)) {
124
+ if (++lineIndex % YIELD_LINES === 0) await yieldTurn();
95
125
  const prompt = line.includes('"promptSource"') || line.includes('"type":"user"');
96
126
  const queue = line.includes('"queue-operation"');
97
127
  const attachment = line.includes('"queued_command"');
@@ -214,10 +244,9 @@ function codexRenderedMailIds(line) {
214
244
  /** Final answers and delivered mail from a Codex session's own transcript (`read_codex_answers`). */
215
245
  export async function readCodex(path) {
216
246
  const mail = { typed: [], answers: [], channel: [], delivered: new Map() };
217
- const lines = (await readFile(path, 'utf8')).split('\n');
218
- for (let index = 0; index < lines.length; index++) {
219
- if (index && index % YIELD_LINES === 0) await yieldTurn();
220
- const line = lines[index];
247
+ let lineIndex = 0;
248
+ for await (const line of transcriptLines(path)) {
249
+ if (++lineIndex % YIELD_LINES === 0) await yieldTurn();
221
250
  const rendered = line.includes('cross-session-message id=');
222
251
  if (!rendered && !line.includes('"final_answer"')) continue;
223
252
  let record; try { record = JSON.parse(line); } catch { continue; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-teams",
3
- "version": "0.3.154",
3
+ "version": "0.3.156",
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",