@volter/supercode-teams 0.3.155 → 0.3.157
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 +14 -0
- package/connector/index.mjs +22 -6
- package/mail/transcript.mjs +42 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -52,6 +52,20 @@ Settled read bodies expire after 30 days, with explicit placeholders and retaine
|
|
|
52
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
|
|
53
53
|
[dated qualification scope](../../docs/evidence/sqlite-mail-store-2026-10-08.md) before inferring platform support.
|
|
54
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
|
+
|
|
61
|
+
The connector's redaction memo keeps serialized redacted JSON, with at most
|
|
62
|
+
200,000 entries and 32 MiB of accounted string payload (two bytes per code unit,
|
|
63
|
+
including keys and digests). An entry over 1 MiB is not retained. Oldest entries
|
|
64
|
+
are evicted to fit; cache misses still go through the redaction worker. These are
|
|
65
|
+
memo bounds, not total connector memory limits. The
|
|
66
|
+
[source investigation](../../docs/evidence/connector-redaction-memo-2026-10-08.md)
|
|
67
|
+
does not attribute the observed process peak or claim a measured runtime improvement.
|
|
68
|
+
|
|
55
69
|
`supercode teams rpc mail.statistics` reads local storage counts and import/retention receipts. It is refused remotely
|
|
56
70
|
and through the app door. Writer groups count stored envelope copies, not call attempts or quotas; database page
|
|
57
71
|
counts exclude physical WAL size. Successful imports record their actual source reads, bytes, batch sizes and time.
|
package/connector/index.mjs
CHANGED
|
@@ -103,14 +103,30 @@ export function foldConnectorState(stateDir){
|
|
|
103
103
|
}
|
|
104
104
|
// What leaves for the server is redacted (@volter/teams/redact), and every digest this connector keeps or sends (what it
|
|
105
105
|
// observed, a baseline, the carried prefix) is of that form: a digest of the local text named a prefix the server never
|
|
106
|
-
// held, every carry was refused, and each session holding a secret went whole on every pass. A message is redacted
|
|
107
|
-
//
|
|
108
|
-
// redacted
|
|
109
|
-
|
|
106
|
+
// held, every carry was refused, and each session holding a secret went whole on every pass. A message is redacted
|
|
107
|
+
// in the connector's redaction worker, with a bounded digest memo: no body for one without a secret (most), serialized
|
|
108
|
+
// redacted JSON for one with. Evicted or oversized values are redacted again. Each loaded message's digest (of the form
|
|
109
|
+
// sent) is taken once and read where needed. The byte budget counts two bytes per retained string code unit, including
|
|
110
|
+
// keys and digests; the entry bound separately limits Map/record overhead. Neither is a total-process heap bound.
|
|
111
|
+
const REDACTED_MEMO=200000,REDACTED_MEMO_BYTES=32*1024*1024,REDACTED_MEMO_ENTRY_BYTES=1024*1024;
|
|
110
112
|
const redactions=new Map();
|
|
113
|
+
let redactionBytes=0;
|
|
111
114
|
const sentDigests=new WeakMap();
|
|
112
115
|
const digestOf=message=>{let digest=sentDigests.get(message);if(digest===undefined){digest=hash(canonicalJson(message.value));sentDigests.set(message,digest);}return digest;};
|
|
113
|
-
|
|
116
|
+
function remember(digest,redacted){
|
|
117
|
+
// Remove the earlier unchanged placeholder even if this redacted body is too large to cache: leaving it would
|
|
118
|
+
// make a later hit reuse the original, unredacted message.
|
|
119
|
+
const previous=redactions.get(digest);
|
|
120
|
+
if(previous){redactionBytes-=previous.bytes;redactions.delete(digest);}
|
|
121
|
+
const kept=redacted?{text:JSON.stringify(redacted.value),digest:redacted.digest}:null;
|
|
122
|
+
const bytes=2*(digest.length+(kept?.digest.length??0)+(kept?.text.length??0));
|
|
123
|
+
if(bytes>REDACTED_MEMO_ENTRY_BYTES)return;
|
|
124
|
+
while(redactions.size>=REDACTED_MEMO||redactionBytes+bytes>REDACTED_MEMO_BYTES){
|
|
125
|
+
const oldest=redactions.keys().next().value;
|
|
126
|
+
redactionBytes-=redactions.get(oldest).bytes;redactions.delete(oldest);
|
|
127
|
+
}
|
|
128
|
+
redactions.set(digest,{kept,bytes});redactionBytes+=bytes;
|
|
129
|
+
}
|
|
114
130
|
// A capture part is one request, and the server refuses a body over 8 MiB (volter-teams request-json.mjs): a part
|
|
115
131
|
// holds at most PART_MESSAGES messages (the contract's bound) and PART_BYTES of them, so a run of large tool results
|
|
116
132
|
// splits across parts instead of failing the whole session. A single message larger than that goes alone.
|
|
@@ -881,7 +897,7 @@ export class TeamsConnector {
|
|
|
881
897
|
const out=new Array(raw.length),missing=[];
|
|
882
898
|
raw.forEach((message,index)=>{
|
|
883
899
|
const text=canonicalJson(message.value),digest=hash(text);
|
|
884
|
-
if(redactions.has(digest)){const kept=redactions.get(digest);out[index]=kept?{...message,value:kept.
|
|
900
|
+
if(redactions.has(digest)){const {kept}=redactions.get(digest);out[index]=kept?{...message,value:JSON.parse(kept.text)}:message;sentDigests.set(out[index],kept?kept.digest:digest);return;}
|
|
885
901
|
if(held?.has(digest)){remember(digest,null);out[index]=message;sentDigests.set(message,digest);return;}
|
|
886
902
|
missing.push([index,digest,text]);
|
|
887
903
|
});
|
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.157",
|
|
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",
|