@volter/supercode-teams 0.3.3 → 0.3.4

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.
@@ -5,6 +5,8 @@ const BOOLEANS = new Set([
5
5
  ]);
6
6
 
7
7
  const VALUES = new Set([
8
+ // calendar (bin/workspace/calendar.mjs)
9
+ 'kind', 'title', 'at', 'card', 'reason', 'number', 'start', 'end', 'holder', 'requester', 'thread', 'guarantee', 'from', 'draws', 'notify', 'threshold',
8
10
  'context', 'server', 'team', 'credential-ref', 'data-dir', 'listen',
9
11
  'public-origin', 'init-team', 'as', 'user', 'repo', 'harness', 'machine',
10
12
  'source', 'cursor', 'limit', 'tail', 'before', 'after', 'revision', 'role',
@@ -0,0 +1,126 @@
1
+ // `supercode calendar` (company RFC 0023 decisions 12-19): the calendar's verbs over the team's calendar door and the
2
+ // calendar pack. People and agents record what only they know (a holder's window, a fixed date, the manager's call);
3
+ // the projection, the risks and the calibration are computed from the board's own record, never stated by an agent.
4
+ //
5
+ // calendar list fixed events, windows, calls; the IR's recurring jobs read live
6
+ // calendar event add --kind deadline|delivery --title T --at ISO [--card ID] [--reason R]
7
+ // calendar window add --number N --start ISO --end ISO --holder H --requester R --thread ID --guarantee TEXT
8
+ // calendar call --card ID --from ISO --to ISO --reason R the manager's call beside the base rate
9
+ // calendar projection --root HOME [--draws N] each open card's forecast; a changed one is kept as a revision
10
+ // calendar risk --root HOME --notify MANAGER mails the manager once per crossing of a date's threshold
11
+ // calendar calibration --root HOME the base rate and the manager's calls graded against outcomes
12
+ import { execFileSync } from 'node:child_process';
13
+ import { randomUUID } from 'node:crypto';
14
+ import { usage, requireArgs } from './args.mjs';
15
+
16
+ async function pack() {
17
+ try { return await import('@volter/supercode-calendar'); } catch { /* not installed */ }
18
+ try { return await import(new URL('../../../calendar/index.mjs', import.meta.url).href); } catch { /* not beside it */ }
19
+ usage('calendar needs the calendar pack (@volter/supercode-calendar)');
20
+ }
21
+
22
+ const supercode = () => process.env.SUPERCODE_BIN ?? 'supercode';
23
+ const at = (value, name) => { const t = Date.parse(String(value ?? '')); if (!Number.isFinite(t)) usage(`--${name} is an ISO date and time`); return t; };
24
+
25
+ function readBoard(root) {
26
+ if (!root) usage('--root names the board\'s home');
27
+ const text = execFileSync(supercode(), ['workflow', 'load', '--from', 'hermes', '--json', String(root)], { encoding: 'utf8', maxBuffer: 512 * 1024 * 1024, stdio: ['ignore', 'pipe', 'pipe'] });
28
+ const board = Object.values(JSON.parse(text).workflow.boards)[0];
29
+ return { tasks: Object.entries(board.tasks).map(([id, task]) => ({ id, ...task })), dependencies: board.dependencies ?? [] };
30
+ }
31
+
32
+ async function objects(client, path, kind, revisions = false) {
33
+ const query = new URLSearchParams({ ...(kind ? { kind } : {}), ...(revisions ? { revisions: 'true' } : {}) });
34
+ return (await client.request('GET', path(`/calendar/objects?${query}`))).data.items;
35
+ }
36
+ // A write is keyed by the object's uid, so a rate-limited one is retried after the wait the server names: a board with
37
+ // many open cards writes as many forecasts on its first projection.
38
+ async function put(client, path, kind, object) {
39
+ for (let attempt = 0; ; attempt++) {
40
+ try { return await client.request('PUT', path(`/calendar/objects/${encodeURIComponent(object.uid)}`), { body: { kind, object } }); } catch (error) {
41
+ if (error.code !== 'rate_limited' || attempt >= 30) throw error;
42
+ const seconds = Number(error.details?.retry_after_seconds ?? error.retry_after_seconds ?? 1);
43
+ await new Promise((resolve) => setTimeout(resolve, Math.max(1, seconds) * 1000));
44
+ }
45
+ }
46
+ }
47
+
48
+ /** Fixed events linked to cards, as the projection reads them. */
49
+ const datedCards = (items) => items.filter((item) => item.object['volter:card'] && (item.kind === 'deadline' || item.kind === 'delivery')).map((item) => ({ card: item.object['volter:card'], at: Date.parse(item.object.start), kind: item.kind }));
50
+
51
+ export async function calendarCommand(sub, { flags, rest, output, client, context, teamPath }) {
52
+ const path = (suffix) => teamPath(context, suffix);
53
+ const calendar = await pack();
54
+ if (sub === 'list') {
55
+ requireArgs(rest, 0, 0, 'calendar list');
56
+ const items = await objects(client, path);
57
+ let jobs = [];
58
+ // Recurring jobs are the IR's, read where they live and never copied (decision 7).
59
+ try { jobs = JSON.parse(execFileSync(supercode(), ['jobs', 'list', '--json', ...(flags.harness ? ['--harness', String(flags.harness)] : [])], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] })); } catch { /* no jobs here */ }
60
+ return output({ version: 1, data: { objects: items, recurring: (Array.isArray(jobs) ? jobs : jobs.jobs ?? []).map((job) => ({ id: job.id ?? job.name, schedule: job.schedule ?? job.schedule_display, next: job.next_run_at ?? job.nextRunAt ?? null })) } });
61
+ }
62
+ if (sub === 'event') {
63
+ if (rest[0] !== 'add') usage('calendar event add --kind deadline|delivery --title T --at ISO [--card ID]');
64
+ if (!['deadline', 'delivery'].includes(flags.kind)) usage('--kind is deadline (never moves) or delivery (moves only on the owner\'s word)');
65
+ if (!flags.title) usage('--title is required');
66
+ const object = calendar.fixedEvent({ uid: `event-${randomUUID()}`, kind: flags.kind, title: String(flags.title), at: at(flags.at, 'at'), card: flags.card ?? null, reason: flags.reason ?? null });
67
+ return output(await put(client, path, flags.kind, object));
68
+ }
69
+ if (sub === 'window') {
70
+ if (rest[0] !== 'add') usage('calendar window add --number N --start ISO --end ISO --holder H --requester R --thread ID --guarantee TEXT');
71
+ for (const key of ['number', 'holder', 'requester', 'thread', 'guarantee']) if (!flags[key]) usage(`--${key} is required`);
72
+ const object = calendar.windowEvent({ uid: `window-${String(flags.holder)}-${Number(flags.number)}`, number: Number(flags.number), start: at(flags.start, 'start'), end: at(flags.end, 'end'), holder: String(flags.holder), requester: String(flags.requester), thread: String(flags.thread), guarantee: String(flags.guarantee) });
73
+ return output(await put(client, path, 'window', object));
74
+ }
75
+ if (sub === 'call') {
76
+ requireArgs(rest, 0, 0, 'calendar call --card ID --from ISO --to ISO --reason R');
77
+ if (!flags.card || !flags.reason) usage('--card and --reason are required');
78
+ const forecast = (await objects(client, path, 'forecast')).find((item) => item.object['volter:card'] === flags.card);
79
+ const object = calendar.callTask({ uid: `call-${flags.card}`, card: String(flags.card), from: at(flags.from, 'from'), to: at(flags.to, 'to'), reason: String(flags.reason), baseRate: forecast ? { p50: forecast.object['volter:p50'], p85: forecast.object['volter:p85'] } : null });
80
+ return output(await put(client, path, 'call', object));
81
+ }
82
+ if (sub === 'projection' || sub === 'risk') {
83
+ const board = readBoard(flags.root);
84
+ const priors = calendar.boardPriors(board.tasks);
85
+ const events = datedCards(await objects(client, path));
86
+ const projection = calendar.project({ ...board, priors, events, draws: flags.draws ? Number(flags.draws) : 2000 });
87
+ if (sub === 'projection') {
88
+ // A card's forecast is kept as a revision when it moved (decision 16: no change, no entry).
89
+ const kept = new Map((await objects(client, path, 'forecast')).map((item) => [item.object['volter:card'], item.object]));
90
+ let revised = 0;
91
+ for (const card of projection.cards) {
92
+ const before = kept.get(card.id);
93
+ const hours = (iso) => Math.round(Date.parse(iso) / 3_600_000);
94
+ if (before && hours(before['volter:p85']) === Math.round(card.p85 / 3_600_000) && hours(before['volter:p50']) === Math.round(card.p50 / 3_600_000)) continue;
95
+ // Why it moved, from the card's own record: a review asking for changes, a move of lane, else the base rate.
96
+ const cause = !before ? 'first forecast'
97
+ : card.changes > (before['volter:changes'] ?? 0) ? `a review asked for changes (round ${card.changes})`
98
+ : before['volter:lane'] && before['volter:lane'] !== card.lane ? `moved to ${card.lane}`
99
+ : 'the base rate moved';
100
+ await put(client, path, 'forecast', calendar.forecastTask({ uid: `forecast-${card.id}`, card: card.id, p50: card.p50, p85: card.p85, due: card.due ?? null, chanceByDue: card.chanceByDue ?? null, cause, lane: card.lane, changes: card.changes }));
101
+ revised += 1;
102
+ }
103
+ return output({ version: 1, data: { revised, priors: { coding: { median: priors.coding.median, p85: priors.coding.p85, n: priors.coding.n }, rework: { median: priors.rework.median, p85: priors.rework.p85, n: priors.rework.n }, reviewWait: { median: priors.reviewWait.median, n: priors.reviewWait.n }, reviewRounds: priors.reviewRounds.counts },
104
+ cards: projection.cards.map((card) => ({ id: card.id, title: card.title, lane: card.lane, forecast: calendar.sentence(card.chanceByDue ?? 0.85, card.p50, card.due ?? card.p85), p50: new Date(card.p50).toISOString(), p85: new Date(card.p85).toISOString(), ...(card.due ? { due: new Date(card.due).toISOString(), chanceByDue: card.chanceByDue } : {}) })) } });
105
+ }
106
+ if (!flags.notify) usage('--notify names the manager the calendar mails');
107
+ const found = calendar.risks(projection, { threshold: flags.threshold ? Number(flags.threshold) : 0.85 });
108
+ const notices = new Set((await objects(client, path, 'notice')).map((item) => item.object.uid));
109
+ const sent = [];
110
+ for (const risk of found) {
111
+ const uid = `notice-${risk.id}-${risk.due}`;
112
+ if (notices.has(uid)) continue;
113
+ const body = `${risk.title} (${risk.id}) is at risk for ${new Date(risk.due).toISOString().slice(0, 16)}Z: ${calendar.sentence(risk.chance, risk.p85, risk.due)} (${risk.reason}). The calendar's base rate; your call is yours.`;
114
+ execFileSync(supercode(), ['message', 'send', String(flags.notify), body, '--subject', `At risk: ${risk.id}`], { stdio: ['ignore', 'pipe', 'pipe'] });
115
+ await put(client, path, 'notice', { '@type': 'Event', uid, title: `Mailed ${flags.notify} about ${risk.id}`, start: new Date().toISOString(), duration: 'PT0S', 'volter:kind': 'notice', 'volter:card': risk.id });
116
+ sent.push(risk.id);
117
+ }
118
+ return output({ version: 1, data: { risks: found.map((risk) => ({ ...risk, due: new Date(risk.due).toISOString(), p85: new Date(risk.p85).toISOString() })), mailed: sent } });
119
+ }
120
+ if (sub === 'calibration') {
121
+ const board = readBoard(flags.root);
122
+ const revisions = (await objects(client, path, undefined, true)).filter((item) => item.kind === 'forecast' || item.kind === 'call').map((item) => ({ card: item.object['volter:card'], ...(item.kind === 'forecast' ? { p85: Date.parse(item.object['volter:p85']) } : { callTo: Date.parse(item.object.due) }) }));
123
+ return output({ version: 1, data: calendar.calibration(revisions, board.tasks) });
124
+ }
125
+ usage('calendar list | event add | window add | call | projection | risk | calibration');
126
+ }
package/bin/workspace.mjs CHANGED
@@ -14,6 +14,7 @@ import { makeToken } from '@volter/teams/server/auth';
14
14
  import { moveWorkspace } from '../workspace-move.mjs';
15
15
  import { TeamsConnector, readSyncStatus } from '../connector/index.mjs';
16
16
  import { syncStatusLines } from './workspace/sync-status.mjs';
17
+ import { calendarCommand } from './workspace/calendar.mjs';
17
18
  import { CONTINUATION_FIDELITY, CONTINUATION_HARNESSES } from '../connector/native-continuation.mjs';
18
19
  import { MachineEvents } from '../machine-events.mjs';
19
20
  import { openLocalMachine, nodeRpc } from '../client.mjs';
@@ -70,7 +71,7 @@ export async function workspaceMain(argv = process.argv.slice(2), env = process.
70
71
  if(identity&&!flags.context&&!env.SUPERCODE_AGENT_CONTEXT)env={...env,SUPERCODE_AGENT_CONTEXT:identity.context,SUPERCODE_AGENT_ID:identity.agent,SUPERCODE_AGENT_ASSIGNMENTS:JSON.stringify(identity.assignments)};
71
72
  }catch(error){
72
73
  // Only absence of an older/offline daemon permits the ordinary plain-session context.
73
- if(!['ENOENT','ECONNREFUSED',-32601].includes(error.code))throw error;
74
+ if(!['ENOENT','ECONNREFUSED','machine_offline',-32601].includes(error.code))throw error;
74
75
  }finally{channel?.close();}
75
76
  }
76
77
  }
@@ -105,6 +106,7 @@ export async function workspaceMain(argv = process.argv.slice(2), env = process.
105
106
  if (verb === 'identities') return identityRecordCommand(sub,command);
106
107
  if (verb === 'apps' || verb === 'app-installations') return appsCommand(verb,sub,{...command,output:value=>output(flags.json?value:redactAppSecrets(value))});
107
108
  if (verb === 'sessions') return sessionsCommand(sub, command);
109
+ if (verb === 'calendar') return calendarCommand(sub, { ...command, teamPath });
108
110
  if (verb === 'members') return membersCommand(sub, command);
109
111
  if (verb === 'groups') return groupsCommand(sub, command);
110
112
  if (verb === 'sources') return sourcesCommand(sub, command);
@@ -481,6 +483,7 @@ async function sessionsCommand(sub, command) {
481
483
  if (sub === 'follow') { only(flags, ['json', 'help', 'context', 'tail', 'after']); requireArgs(rest, 1, 1, 'sessions follow REF'); return stream(session.follow({ tail: positiveInteger(flags.tail, 100, 'tail'), ...optional(flags, ['after']) }), flags.json); }
482
484
  if (sub === 'attach') return attachSession(session, command);
483
485
  if (sub === 'export') return exportSession(session, command);
486
+ if (sub === 'trace') return traceSession(session, command);
484
487
  if (sub === 'continue') return continueSession(session, command);
485
488
  if (sub === 'url') { only(flags, ['json', 'help', 'context']); requireArgs(rest, 1, 1, 'sessions url REF'); const descriptor = await session.descriptor(); return output({ version: 1, context: scope(context), data: { url: `${context.origin}/teams/${context.team_id}/sessions/${descriptor.data.id}` } }); }
486
489
  // A session view (RH2 ADR 0008): its owner lets a workplace Room's Conversation read this session with the tokens that
@@ -544,6 +547,47 @@ async function exportSession(session, { flags, rest, output, client, context })
544
547
  } catch (error) { destination.abort(); throw error; }
545
548
  }
546
549
 
550
+ /** The trace pack (company RFC 0023 decision 17), outside core: installed, or beside this package in the repository. */
551
+ async function tracePack() {
552
+ try { return await import('@volter/supercode-trace'); } catch { /* not installed */ }
553
+ try { return await import(new URL('../../trace/trace.mjs', import.meta.url).href); } catch { /* not beside it */ }
554
+ unsupported('sessions trace needs the trace pack (@volter/supercode-trace)');
555
+ }
556
+
557
+ /** `sessions trace REF`: the session's captured record, exported byte-lossless through the export door, read as its trace. */
558
+ async function traceSession(session, { flags, rest, output, client, context }) {
559
+ only(flags, ['json', 'help', 'context', 'since', 'until', 'timeout']);
560
+ requireArgs(rest, 1, 1, 'sessions trace REF [--since ISO] [--until ISO]');
561
+ const pack = await tracePack();
562
+ const descriptor = (await session.descriptor()).data;
563
+ const format = descriptor.harness;
564
+ if (format !== 'claude-code' && format !== 'codex') unsupported(`trace reads Claude Code and Codex sessions; this one is ${format}`);
565
+ const window = { ...(flags.since ? { since: String(flags.since) } : {}), ...(flags.until ? { until: String(flags.until) } : {}) };
566
+ const print = (trace) => flags.json ? output({ version: 1, context: scope(context), data: { session: descriptor.id, revision: descriptor.revision, ...trace } }) : process.stdout.write(pack.formatTrace(trace));
567
+ // A session with no exportable record (known only from telemetry) is read from its stored evidence.
568
+ const fromEvidence = async () => {
569
+ const items = []; let after = 0;
570
+ // Read until the door says it is done: a page may hold none of this session's records and still not be the last.
571
+ for (;;) { const page = (await client.request('GET', `${session.path}/evidence?after=${after}&limit=5000`)).data; items.push(...page.items); if (page.done || page.next_after === after) break; after = page.next_after; }
572
+ if (!items.length) return null;
573
+ return pack.traceTelemetry(items, format, window);
574
+ };
575
+ let operation = null;
576
+ try {
577
+ const started = await session.export({ format, fidelity: 'byte_lossless', revision: descriptor.revision });
578
+ operation = await waitForOperation(client, context, started.data, flags.timeout ? duration(flags.timeout) : undefined);
579
+ } catch (error) { operation = { state: 'failed', error }; }
580
+ const result = operation.result;
581
+ if (operation.state !== 'succeeded' || result?.kind !== 'export' || result.source_revision !== descriptor.revision) {
582
+ const trace = await fromEvidence();
583
+ if (trace) return print(trace);
584
+ throw new Error(`the session's record could not be exported losslessly (${operation.error?.message ?? operation.state}) and it has no stored telemetry; read its native file on its own machine with supercode-trace FILE`);
585
+ }
586
+ const primary = result.files.find(file => file.role === 'primary');
587
+ const text = new TextDecoder().decode(await downloadArtifact(client, context, result.source_session_id, primary));
588
+ return print(pack.traceText(text, { harness: format, ...window }));
589
+ }
590
+
547
591
  async function continueSession(session, { flags, rest, output, client, context, team, home, env, dependencies }) {
548
592
  only(flags, ['json', 'help', 'context', 'plan', 'on', 'harness', 'cwd', 'root', 'fidelity', 'dry_run', 'timeout', 'workspace_from']);
549
593
  requireArgs(rest, 1, 1, 'sessions continue REF (--plan ID | --on local|MACHINE --harness ID --cwd PATH [--workspace-from MACHINE:PATH])');
@@ -1268,7 +1312,8 @@ function printHelp() {
1268
1312
  sources create|list|status|pause|revoke|credentials|config
1269
1313
  machines list|roots|access|authorize-continuation|subscribe|unsubscribe|events
1270
1314
  sync add|list|show|pause|resume|remove
1271
- sessions list|search|show|follow|access|rename|delete|url|attach|export|continue|bind|unbind
1315
+ sessions list|search|show|follow|access|rename|delete|url|attach|export|trace|continue|bind|unbind
1316
+ calendar list|event add|window add|call|projection|risk|calibration
1272
1317
  Use --help with a command for its required arguments and options.
1273
1318
  `);
1274
1319
  }
package/machine.mjs CHANGED
@@ -24,6 +24,7 @@
24
24
  // grants before acting. There is no inbound port and no address book.
25
25
  //
26
26
  // It decides nothing on anyone's behalf. Every act is logged.
27
+ import { ProcessTree } from './process-tree.mjs';
27
28
  import { spawn, spawnSync } from 'node:child_process';
28
29
  import { EventEmitter } from 'node:events';
29
30
  import { appendFileSync, readFileSync, writeFileSync, writeSync, statSync, readdirSync, mkdirSync, renameSync, rmSync, existsSync, openSync, readSync, closeSync, accessSync, constants } from 'node:fs';
@@ -231,6 +232,13 @@ export class TeamsMachine extends MachineCore {
231
232
  this.terminalMailWork = this.#drainTerminalMail().catch(error => this.emit('mail-watch-stderr', `terminal mailbox: ${error.message}`)).finally(() => { this.terminalMailWork = null; });
232
233
  }, 1000);
233
234
  this.terminalMailTimer.unref?.();
235
+ // What actually runs under each hosted pane, for the trace pack (process-tree.mjs): one read every five seconds.
236
+ this.processTree = new ProcessTree({ root: join(this.home, 'processes'), panes: async () => (await this.attentionEvidence()).map(({ session }) => ({ pane: session.contextKey, rootPid: session.rootPid })) });
237
+ this.processTimer = setInterval(() => {
238
+ if (this.processWork) return;
239
+ this.processWork = this.processTree.sample().catch(error => this.emit('mail-watch-stderr', `process tree: ${error.message}`)).finally(() => { this.processWork = null; });
240
+ }, 5000);
241
+ this.processTimer.unref?.();
234
242
  this.inputInFlight = new Set(); // one input transaction per pane
235
243
  this.inputRepairsPath = join(this.home, 'input-repairs.json');
236
244
  this.inputRepairs = readJson(this.inputRepairsPath, {});
@@ -299,6 +307,7 @@ export class TeamsMachine extends MachineCore {
299
307
  async beforeClose() {
300
308
  this.#stopHealth();
301
309
  clearInterval(this.terminalMailTimer); await this.terminalMailWork;
310
+ clearInterval(this.processTimer); await this.processWork;
302
311
  clearInterval(this.inputRepairTimer);this.inputRepairTimer=null;
303
312
  await this.inputRepairWork;
304
313
  clearInterval(this.endWatch); this.endWatch = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-teams",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
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",
@@ -59,12 +59,14 @@
59
59
  "scripts": {},
60
60
  "dependencies": {
61
61
  "@homebridge/node-pty-prebuilt-multiarch": "^0.14.1",
62
+ "@volter/identity": "^0.4.1",
63
+ "@volter/supercode-calendar": "^0.1.0",
62
64
  "@volter/supercode-frontend": "^0.2.2",
63
65
  "@volter/supercode-harness-sdk": "^0.3.43",
64
66
  "@volter/supercode-health": "^0.1.0",
65
67
  "@volter/supercode-terminal": "^0.2.54",
66
- "@volter/identity": "^0.4.1",
67
- "@volter/teams": "0.2.1",
68
+ "@volter/supercode-trace": "^0.1.0",
69
+ "@volter/teams": "0.2.2",
68
70
  "@xterm/addon-serialize": "^0.14.0",
69
71
  "@xterm/headless": "^6.0.0",
70
72
  "protobufjs": "8.8.0",
@@ -0,0 +1,71 @@
1
+ // A HOSTED SESSION'S PROCESS TREE (company RFC 0023 decision 17, Experiment 5). A transcript keeps only the text of the
2
+ // commands an agent ran; what actually ran is the processes under its pane. This daemon hosts the pane, so it records
3
+ // them: every few seconds one `ps` read, and for each pane each descendant of its harness process that started or ended
4
+ // since the last read, with its program identity (the binary, or for an interpreter the package owning its script:
5
+ // `node:wrangler`, `bun:@volter/world-runtime`) and its subcommand by position. The trace pack names shell time by what
6
+ // ran from this record. Exit codes are not visible to a sampler (a process's end is the read it was last seen in), and
7
+ // processes shorter than one interval may be missed; both are said here, not guessed at.
8
+ import { execFile } from 'node:child_process';
9
+ import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
10
+ import { basename, dirname, join } from 'node:path';
11
+
12
+ const INTERPRETERS = new Set(['node', 'bun', 'deno', 'python', 'python3', 'ruby', 'tsx']);
13
+ const SHELLS = new Set(['sh', 'bash', 'zsh', 'fish', 'login']);
14
+
15
+ function ps() {
16
+ return new Promise((resolve) => execFile('ps', ['-axo', 'pid=,ppid=,lstart=,command='], { maxBuffer: 16 * 1024 * 1024, timeout: 10_000 }, (error, stdout) => resolve(error ? '' : stdout)));
17
+ }
18
+
19
+ /** The package that owns a script, by the nearest package.json above it; cached by folder. */
20
+ const owners = new Map();
21
+ function packageOf(path) {
22
+ let folder = dirname(path);
23
+ for (let i = 0; i < 12 && folder !== dirname(folder); i++, folder = dirname(folder)) {
24
+ if (owners.has(folder)) return owners.get(folder);
25
+ const manifest = join(folder, 'package.json');
26
+ if (existsSync(manifest)) { let name = null; try { name = JSON.parse(readFileSync(manifest, 'utf8')).name ?? null; } catch { /* unreadable */ } owners.set(folder, name); return name; }
27
+ }
28
+ return null;
29
+ }
30
+
31
+ /** A process's program identity and its subcommand, from its argv. */
32
+ export function identityOf(command) {
33
+ const argv = command.trim().split(/\s+/);
34
+ const binary = basename(argv[0] ?? '');
35
+ if (INTERPRETERS.has(binary)) {
36
+ const script = argv.slice(1).find((arg) => !arg.startsWith('-'));
37
+ const owner = script && script.includes('/') ? packageOf(script) : null;
38
+ const name = owner ?? (script ? basename(script).replace(/\.(m?[jt]s|py)$/, '') : binary);
39
+ const rest = argv.slice(argv.indexOf(script ?? '') + 1).filter((arg) => !arg.startsWith('-'));
40
+ return { program: `${binary}:${name}`, sub: rest[0] ?? null };
41
+ }
42
+ return { program: binary, sub: argv.slice(1).find((arg) => !arg.startsWith('-')) ?? null };
43
+ }
44
+
45
+ export class ProcessTree {
46
+ /** `panes()` answers the hosted panes as `{ pane, rootPid }`; records go under `root`. */
47
+ constructor({ root, panes }) { this.root = root; this.panes = panes; this.seen = new Map(); }
48
+
49
+ async sample(now = Date.now()) {
50
+ const panes = (await this.panes()).filter((row) => Number.isInteger(row.rootPid));
51
+ if (!panes.length) return;
52
+ const rows = (await ps()).split('\n').map((line) => /^\s*(\d+)\s+(\d+)\s+(\w{3} \w{3}\s+\d+ [\d:]+ \d{4})\s+(.*)$/.exec(line)).filter(Boolean)
53
+ .map((m) => ({ pid: Number(m[1]), ppid: Number(m[2]), started: Date.parse(m[3]), command: m[4] }));
54
+ const children = new Map();
55
+ for (const row of rows) children.set(row.ppid, [...(children.get(row.ppid) ?? []), row]);
56
+ mkdirSync(this.root, { recursive: true });
57
+ for (const { pane, rootPid } of panes) {
58
+ const live = new Map();
59
+ const walk = (pid) => { for (const child of children.get(pid) ?? []) { live.set(child.pid, child); walk(child.pid); } };
60
+ walk(rootPid);
61
+ // A shell is the command's carrier, not what ran: walked through, never recorded.
62
+ const recorded = new Map([...live].filter(([, row]) => !SHELLS.has(basename(row.command.split(/\s+/)[0] ?? '').replace(/^-/, ''))));
63
+ const before = this.seen.get(pane) ?? new Map();
64
+ const file = join(this.root, `${String(pane).replace(/[^\w.-]/g, '_')}.jsonl`);
65
+ const write = (record) => appendFileSync(file, `${JSON.stringify(record)}\n`);
66
+ for (const [pid, row] of recorded) if (!before.has(pid)) write({ event: 'start', pane, rootPid, pid, ppid: row.ppid, at: Number.isFinite(row.started) ? row.started : now, ...identityOf(row.command) });
67
+ for (const pid of before.keys()) if (!recorded.has(pid)) write({ event: 'end', pane, pid, at: now });
68
+ this.seen.set(pane, recorded);
69
+ }
70
+ }
71
+ }
@@ -0,0 +1,39 @@
1
+ // THE CALENDAR'S DOOR (company RFC 0023 decisions 18-19). A team member reads the calendar's objects and writes one: a
2
+ // fixed event, a resource window its holder grants, the manager's call on a card, a forecast revision. Objects are
3
+ // JSCalendar (an `Event` or a `Task`, with Volter's own fields under `volter:`); each write is a new revision of its
4
+ // `uid`, and a read returns each object's latest revision unless `revisions=true`. Nothing here decides anything: the
5
+ // calendar pack computes, people and agents act.
6
+ const KINDS = new Set(['deadline', 'delivery', 'window', 'call', 'forecast', 'notice']);
7
+
8
+ export async function calendarRoute({ api, req, res, auth, url, uid, requestId, parseJson, fail, send, result }) {
9
+ if (!['user', 'service'].includes(auth.kind)) fail(403, 'forbidden', 'credential cannot use the calendar');
10
+ if (req.method === 'GET' && uid === undefined) {
11
+ const kind = url.searchParams.get('kind'), all = url.searchParams.get('revisions') === 'true', since = Number(url.searchParams.get('since') ?? 0);
12
+ if (kind !== null && !KINDS.has(kind)) fail(400, 'invalid_request', 'kind is deadline, delivery, window, call, forecast or notice');
13
+ const rows = all
14
+ ? await api.store.query('SELECT uid,revision,kind,object_json,written_by,written_at,deleted FROM calendar_objects WHERE team_id=? AND (? IS NULL OR kind=?) AND written_at>=? ORDER BY written_at, uid, revision', [auth.team_id, kind, kind, since])
15
+ : await api.store.query('SELECT c.uid,c.revision,c.kind,c.object_json,c.written_by,c.written_at,c.deleted FROM calendar_objects c WHERE c.team_id=? AND (? IS NULL OR c.kind=?) AND c.revision=(SELECT MAX(revision) FROM calendar_objects l WHERE l.team_id=c.team_id AND l.uid=c.uid) AND c.deleted=0 ORDER BY c.written_at', [auth.team_id, kind, kind]);
16
+ return send(res, 200, result(api, auth, { items: rows.map(row => ({ uid: row.uid, revision: row.revision, kind: row.kind, written_by: row.written_by, written_at: row.written_at, deleted: row.deleted === 1, object: JSON.parse(row.object_json) })) }), requestId);
17
+ }
18
+ if (uid === undefined || !/^[\w.:-]{1,200}$/.test(uid)) fail(404, 'not_found', 'not found');
19
+ const latest = await api.store.one('SELECT MAX(revision) revision FROM calendar_objects WHERE team_id=? AND uid=?', [auth.team_id, uid]);
20
+ const now = api.clock.nowMs();
21
+ // The next revision is taken in the write itself, so two writers never share one.
22
+ const write = async (kind, text, deleted) => {
23
+ await api.store.exec('INSERT INTO calendar_objects(team_id,uid,revision,kind,object_json,written_by,written_at,deleted) SELECT ?,?,COALESCE(MAX(revision),0)+1,?,?,?,?,? FROM calendar_objects WHERE team_id=? AND uid=?', [auth.team_id, uid, kind, text, auth.principal_id, now, deleted, auth.team_id, uid]);
24
+ return Number((await api.store.one('SELECT MAX(revision) revision FROM calendar_objects WHERE team_id=? AND uid=?', [auth.team_id, uid])).revision);
25
+ };
26
+ if (req.method === 'DELETE') {
27
+ if (!latest?.revision) fail(404, 'not_found', 'not found');
28
+ const prior = await api.store.one('SELECT kind,object_json FROM calendar_objects WHERE team_id=? AND uid=? AND revision=?', [auth.team_id, uid, latest.revision]);
29
+ return send(res, 200, result(api, auth, { uid, revision: await write(prior.kind, prior.object_json, 1), deleted: true }), requestId);
30
+ }
31
+ if (req.method !== 'PUT') fail(405, 'method_not_allowed', 'method not allowed');
32
+ const body = await parseJson(req);
33
+ const object = body?.object, kind = body?.kind;
34
+ if (!KINDS.has(kind)) fail(400, 'invalid_request', 'kind is deadline, delivery, window, call, forecast or notice');
35
+ if (!object || typeof object !== 'object' || !['Event', 'Task'].includes(object['@type']) || object.uid !== uid) fail(400, 'invalid_request', 'object is a JSCalendar Event or Task whose uid is the path');
36
+ const text = JSON.stringify(object);
37
+ if (text.length > 64_000) fail(413, 'too_large', 'a calendar object is at most 64 KB');
38
+ send(res, 200, result(api, auth, { uid, revision: await write(kind, text, 0), kind, written_at: now }), requestId);
39
+ }
@@ -0,0 +1,42 @@
1
+ // A SESSION'S TELEMETRY, READ BACK (company RFC 0023 decision 17). Teams stores every OTLP record a source sends in
2
+ // `evidence` and projects only prompts and responses into messages; the rest (each API request's duration, first-token
3
+ // time and tokens, each tool result's name and duration) is what a telemetry-only session's trace is read from. This
4
+ // door returns the session's log evidence, decoded per harness by the same decoder ingestion used, oldest first. Read
5
+ // access is the session's own: whoever may read its descriptor reads its evidence.
6
+ import { decodeLogEvidence } from './otlp/decoders/index.mjs';
7
+
8
+ const attributes = (value) => value?.attributes ?? [];
9
+ const value = (any) => any == null ? null : any.stringValue ?? any.intValue ?? any.doubleValue ?? any.boolValue ?? null;
10
+
11
+ export async function sessionEvidenceRoute({ api, res, auth, sessionId, url, requestId, send, result }) {
12
+ const session = await api.store.authorizedSession({ teamId: auth.team_id, principalId: auth.principal_id, credentialId: auth.credential_id, ref: sessionId, now: api.clock.nowMs(), bound: auth.bound });
13
+ const after = Number(url.searchParams.get('after') ?? 0), limit = Math.min(Math.max(Number(url.searchParams.get('limit') ?? 2000), 1), 5000);
14
+ const origins = await api.store.query('SELECT source_id,harness,native_id FROM origins WHERE team_id=? AND session_id=? AND deleted_at IS NULL', [auth.team_id, session.id]);
15
+ const items = [];
16
+ let cursor = after, done = origins.length === 0;
17
+ // Every origin's rows in one pass, in the order they arrived: a source (one machine's OTLP stream) carries many
18
+ // sessions, so its rows are narrowed to this session's own id in the query and checked again after decoding. The scan
19
+ // goes on until a page holds `limit` of this session's records or nothing is left (`done`), never stopping at a page
20
+ // of other sessions' records.
21
+ const where = origins.map(() => "(source_id=? AND raw_json LIKE ? ESCAPE '\\')").join(' OR ');
22
+ const bound = origins.flatMap((origin) => [origin.source_id, `%${String(origin.native_id).replace(/[%_\\]/g, (c) => `\\${c}`)}%`]);
23
+ const byKey = new Map(origins.map((origin) => [origin.source_id, origin]));
24
+ while (!done && items.length < limit) {
25
+ const rows = await api.store.query(`SELECT rowid AS position,id,source_id,raw_json,received_at FROM evidence WHERE team_id=? AND signal='logs' AND rowid>? AND (${where}) ORDER BY rowid LIMIT ?`, [auth.team_id, cursor, ...bound, limit]);
26
+ if (rows.length < limit) done = true;
27
+ for (const row of rows) {
28
+ cursor = Math.max(cursor, Number(row.position));
29
+ const origin = byKey.get(row.source_id);
30
+ const raw = JSON.parse(row.raw_json);
31
+ if (!origin || raw.evidenceKind !== 'log_record') continue;
32
+ const decoded = decodeLogEvidence({ record: raw.record, resourceAttributes: attributes(raw.resource), scopeAttributes: attributes(raw.scope), receivedAt: raw.receivedAt });
33
+ // The decoder names a harness only on the events it projects (prompts, responses); the session's id is on each.
34
+ if (decoded?.native_session_id !== origin.native_id || (decoded.harness && decoded.harness !== origin.harness)) continue;
35
+ const fields = Object.fromEntries(attributes(raw.record).map((entry) => [entry.key, value(entry.value)]));
36
+ const nanos = raw.record?.timeUnixNano ?? raw.record?.observedTimeUnixNano;
37
+ items.push({ id: row.id, at: nanos ? Math.floor(Number(BigInt(nanos) / 1_000_000n)) : decoded.recorded_at ?? row.received_at, harness: origin.harness, event: fields['event.name'] ?? value(raw.record?.body) ?? null, fields });
38
+ }
39
+ }
40
+ items.sort((a, b) => a.at - b.at);
41
+ send(res, 200, result(api, auth, { session_id: session.id, items, next_after: cursor, done }), requestId);
42
+ }
@@ -21,6 +21,8 @@ import { deliverSessionRoomNotice, sessionDeliveryResolution } from '@volter/tea
21
21
  import { renderSourceCollectorConfig } from './otlp/collector-config.mjs';
22
22
  import { attachmentActionRoute, createAttachment, liveGrantRoute, machineEffectRoute } from './effects.mjs';
23
23
  import { exportSessionRoute, operationRoute } from './exports.mjs';
24
+ import { sessionEvidenceRoute } from './evidence-route.mjs';
25
+ import { calendarRoute } from './calendar-route.mjs';
24
26
  import { attachmentStreamRoute, attachmentFramesRoute, closeMachineAttachmentRoute, detachAttachmentRoute } from './attachment-relay.mjs';
25
27
  import { continuationRoute, remoteContinuationArtifactRoute, remoteContinuationRoute } from './continuations.mjs';
26
28
  import { fail, parseJson, result, send, guardedMutation, requireRevision, checkRate, grants, syncRuleJson, authGuards } from '@volter/teams/server/http';
@@ -94,6 +96,8 @@ async function team({ api, req, res, auth, url, path, sub, requestId }) {
94
96
  if(req.method==='GET'&&(m=/^\/sessions\/([^/]+)$/.exec(sub))){await sessionDescriptor(api,res,auth,sessionRef(m[1]),requestId);return true;}
95
97
  if((req.method==='PATCH'||req.method==='DELETE')&&(m=/^\/sessions\/([^/]+)$/.exec(sub))){await mutateSession(api,req,res,auth,sessionRef(m[1]),requestId);return true;}
96
98
  if(req.method==='GET'&&(m=/^\/sessions\/([^/]+)\/messages$/.exec(sub))){await readSession(api,res,auth,sessionRef(m[1]),url,requestId);return true;}
99
+ if((m=/^\/calendar\/objects(?:\/([^/]+))?$/.exec(sub))){await calendarRoute({api,req,res,auth,url,uid:m[1]===undefined?undefined:decodeURIComponent(m[1]),requestId,parseJson,fail,send,result});return true;}
100
+ if(req.method==='GET'&&(m=/^\/sessions\/([^/]+)\/evidence$/.exec(sub))){await sessionEvidenceRoute({api,res,auth,sessionId:sessionRef(m[1]),url,requestId,send,result});return true;}
97
101
  if(req.method==='GET'&&(m=/^\/sessions\/([^/]+)\/artifacts\/([^/]+)$/.exec(sub))){await readArtifact(api,req,res,auth,sessionRef(m[1]),m[2],requestId);return true;}
98
102
  if(req.method==='POST'&&(m=/^\/sessions\/([^/]+)\/exports$/.exec(sub))){await exportSessionRoute({api,req,res,auth,sessionId:sessionRef(m[1]),requestId,parseJson,exact,fail,send,result,guardedMutation});return true;}
99
103
  if(req.method==='POST'&&(m=/^\/sessions\/([^/]+)\/continuations$/.exec(sub))){await continuationRoute({api,req,res,auth,sessionId:sessionRef(m[1]),requestId,parseJson,exact,fail,send,result,guardedMutation});return true;}