@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 +10 -1
- package/bin/workspace.mjs +7 -1
- package/connector/index.mjs +4 -1
- package/machine.mjs +45 -15
- package/mail/transcript.mjs +42 -13
- package/package.json +1 -1
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.
|
|
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
|
|
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 }); }
|
package/connector/index.mjs
CHANGED
|
@@ -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
|
|
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.
|
|
421
|
-
*
|
|
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
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
-
|
|
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});
|
package/mail/transcript.mjs
CHANGED
|
@@ -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.
|
|
4
|
-
//
|
|
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 {
|
|
6
|
+
import { open } from 'node:fs/promises';
|
|
7
7
|
import { join } from 'node:path';
|
|
8
8
|
import { blake3Hex } from './envelope.mjs';
|
|
9
9
|
|
|
10
|
-
//
|
|
11
|
-
//
|
|
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
|
-
|
|
92
|
-
for (
|
|
93
|
-
if (
|
|
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
|
-
|
|
218
|
-
for (
|
|
219
|
-
if (
|
|
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.
|
|
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",
|