@volter/supercode-teams 0.2.24 → 0.2.25

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/bin/workspace.mjs CHANGED
@@ -865,4 +865,9 @@ Use --help with a command for its required arguments and options.
865
865
  `);
866
866
  }
867
867
  export function errorExit(error) { return error.exitCode ?? ERROR_EXITS[error.code] ?? 1; }
868
- if (import.meta.url === new URL(`file://${process.argv[1]}`).href) workspaceMain().catch(error => { process.stderr.write(`${JSON.stringify({ version: 1, error: { code: error.code ?? 'invalid_request', message: error.message } })}\n`); process.exit(errorExit(error)); });
868
+ if (import.meta.url === new URL(`file://${process.argv[1]}`).href) workspaceMain().catch(error => { process.stderr.write(`${JSON.stringify({ version: 1, error: { code: error.code ?? 'invalid_request', message: error.message } })}\n`); process.exitCode = errorExit(error);
869
+ // Drain rather than process.exit(): on Windows, exiting while a just-used fetch connection is still closing aborts
870
+ // Node inside libuv (src\win\async.c, `!(handle->flags & UV_HANDLE_CLOSING)`) and loses the exit code. Whatever
871
+ // still holds the event loop open gets a moment, then the exit is forced.
872
+ setTimeout(() => process.exit(process.exitCode), 2_000).unref();
873
+ });
@@ -36,6 +36,12 @@ export interface TeamsConnectorOptions {
36
36
  effectExecutor?: (effect: NativeEffect, context: {core: ConnectorCore; signal: AbortSignal}) => Promise<{native_session_id?: string | null; runtime_id?: string | null} | void>;
37
37
  clock?: ConnectorClock;
38
38
  fetch?: typeof globalThis.fetch;
39
+ /** Milliseconds between heartbeats, sent on their own timer independent of the capture pass (default 15000). */
40
+ heartbeatMs?: number;
41
+ /** A native file at least nativeIdleBytes long gets its native bundle only after this long without a write (default 60000). */
42
+ nativeIdleMs?: number;
43
+ /** Size from which a native file's bundle waits for it to be idle (default 8 MiB). */
44
+ nativeIdleBytes?: number;
39
45
  }
40
46
 
41
47
  export interface CaptureOptions {
@@ -1,4 +1,4 @@
1
- import { mkdirSync, readFileSync, writeFileSync, renameSync, existsSync, chmodSync, realpathSync, statSync, openSync, closeSync, fsyncSync, unlinkSync } from 'node:fs';
1
+ import { mkdirSync, readFileSync, writeFileSync, renameSync, existsSync, chmodSync, realpathSync, statSync, openSync, closeSync, fsyncSync, unlinkSync, readSync, readdirSync } from 'node:fs';
2
2
  import { basename, dirname, join } from 'node:path';
3
3
  import { randomBytes, createHash } from 'node:crypto';
4
4
  import { canonicalJson } from '../contracts/index.mjs';
@@ -10,12 +10,15 @@ const SYNC_PROBLEM_LIMIT=20;
10
10
  // core.mjs MAX_PENDING_RUNTIME_OPENS; more reject with SUBSCRIPTION_OPEN_CAPACITY) and every live follow is a follower
11
11
  // the service polls, while a subtree rule can cover hundreds of sessions. Only the most recently active sessions (at
12
12
  // most FOLLOW_LIMIT, half the SDK's limit for headroom, active within FOLLOW_ACTIVE_WINDOW_MS) are followed live; the
13
- // rest are covered by each beat's stamp-based capture. Followers rotate as activity changes.
14
- const FOLLOW_LIMIT=32,FOLLOW_ACTIVE_WINDOW_MS=30*60*1000,FOLLOW_RETRY_MS=60*1000;
13
+ // rest are covered by each beat's stamp-based capture. Followers rotate as activity changes, with hysteresis: a
14
+ // follower is kept while it stays in the window and ranks within FOLLOW_KEEP_RANK, so sessions near the cutoff do not
15
+ // stop and reopen (each reopen a full load and capture) on alternate beats; new follows only fill up to FOLLOW_LIMIT.
16
+ const FOLLOW_LIMIT=32,FOLLOW_KEEP_RANK=40,FOLLOW_ACTIVE_WINDOW_MS=30*60*1000,FOLLOW_RETRY_MS=60*1000;
15
17
  const CAPACITY_CODE='SUBSCRIPTION_OPEN_CAPACITY';
16
18
  const capacityError=error=>error?.code===CAPACITY_CODE||/Too many unresolved subscriptions/.test(String(error?.message??''));
17
19
  // An error of the connection itself (the outbox, authentication, the server), not of one session.
18
20
  const connectionError=error=>Boolean(error?.outbox||error?.status===401||error?.status===403||error?.status>=500);
21
+ const sameError=(a,b)=>a!=null&&b!=null&&a.code===b.code&&a.message===b.message;
19
22
  const errorSummary=(error,at)=>error==null?null:{code:typeof error.code==='string'?error.code:null,message:String(error.message??error).slice(0,500),at};
20
23
  /** The sync status a connector keeps in its state directory (see TeamsConnector#syncStatus); null when it has none. */
21
24
  export function readSyncStatus(stateDir){
@@ -49,6 +52,15 @@ function settledToolTail(previous,messages){
49
52
  return messages.slice(0,count).map(digestOf);
50
53
  }
51
54
  const hash=value=>createHash('sha256').update(value).digest('hex');
55
+ const CHUNK_BYTES=1024*1024;
56
+ // A native file at least NATIVE_IDLE_BYTES long gets its native bundle only once it has gone NATIVE_IDLE_MS
57
+ // without a write. Translating reads the whole file, takes minutes for a 100 MB transcript, and holds the
58
+ // harness server's only request reader while it runs; a live Claude Code transcript is appended to on every
59
+ // message and tool result, so translating it mid-turn is wasted (the file has moved by the time it finishes).
60
+ // A minute is four beats with no write: the turn is over, or waiting on the user or one long tool call.
61
+ // Smaller files translate in well under a second and keep getting a bundle with every capture.
62
+ const NATIVE_IDLE_MS=60000,NATIVE_IDLE_BYTES=8*1024*1024;
63
+ const fileStamp=stat=>`${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeMs}`;
52
64
  const recordedAt=entry=>{if(Number.isSafeInteger(entry.recorded_at))return entry.recorded_at;if(Number.isSafeInteger(entry.timestamp))return entry.timestamp;const value=entry.metadata?.timestamp??entry.value?.metadata?.timestamp??entry.timestamp;if(typeof value!=='string')return null;const parsed=Date.parse(value);return Number.isSafeInteger(parsed)?parsed:null;};
53
65
  const serverOrigin=value=>{const url=new URL(value),loopback=url.hostname==='localhost'||url.hostname==='::1'||url.hostname==='[::1]'||/^127(?:\.\d{1,3}){3}$/.test(url.hostname);if(!['http:','https:'].includes(url.protocol)||url.username||url.password||url.search||url.hash||(url.pathname!==''&&url.pathname!=='/')||(url.protocol==='http:'&&!loopback))throw new TypeError('server must be an HTTPS origin (HTTP is loopback-only)');return url.origin;};
54
66
  function atomic(path, value) {
@@ -67,19 +79,24 @@ function atomic(path, value) {
67
79
  }
68
80
  }
69
81
  export class TeamsConnector {
70
- constructor({server,teamId,machineId,sourceId,credential,stateDir,core,hostKeyId=null,effectExecutor=null,clock={nowMs:()=>Date.now()},fetch:fetchImpl=globalThis.fetch}){Object.assign(this,{server:serverOrigin(server),teamId,machineId,sourceId,credential,stateDir,core,hostKeyId,effectExecutor,clock,fetch:fetchImpl});mkdirSync(stateDir,{recursive:true,mode:0o700});chmodSync(stateDir,0o700);this.statePath=join(stateDir,'connector.json');this.state=existsSync(this.statePath)?JSON.parse(readFileSync(this.statePath,'utf8')):{version:1,sequence:0,origins:{},outbox:[]};this.state.effects??={};if(Array.isArray(this.state.sync_status?.problems))this.state.sync_status.problems=this.state.sync_status.problems.filter(item=>item?.code!==CAPACITY_CODE);this.state.incarnation_id=`inc_${randomBytes(16).toString('hex')}`;this.abort=new AbortController();this.work=Promise.resolve();this.nativeEffects=new NativeEffects(this);this.effects=new EffectConsumer(this,effectExecutor?{prepare:async effect=>({execute:()=>effectExecutor(effect,{core:this.core,signal:this.abort.signal})})}:this.nativeEffects);this.save();}
82
+ constructor({server,teamId,machineId,sourceId,credential,stateDir,core,hostKeyId=null,effectExecutor=null,clock={nowMs:()=>Date.now()},fetch:fetchImpl=globalThis.fetch,heartbeatMs=15000,nativeIdleMs=NATIVE_IDLE_MS,nativeIdleBytes=NATIVE_IDLE_BYTES}){Object.assign(this,{server:serverOrigin(server),teamId,machineId,sourceId,credential,stateDir,core,hostKeyId,effectExecutor,clock,fetch:fetchImpl,heartbeatMs,nativeIdleMs,nativeIdleBytes});mkdirSync(stateDir,{recursive:true,mode:0o700});chmodSync(stateDir,0o700);this.statePath=join(stateDir,'connector.json');this.state=existsSync(this.statePath)?JSON.parse(readFileSync(this.statePath,'utf8')):{version:1,sequence:0,origins:{},outbox:[]};this.state.effects??={};if(Array.isArray(this.state.sync_status?.problems))this.state.sync_status.problems=this.state.sync_status.problems.filter(item=>item?.code!==CAPACITY_CODE);this.state.native_cache??={};this.state.incarnation_id=`inc_${randomBytes(16).toString('hex')}`;this.abort=new AbortController();this.work=Promise.resolve();this.nativeEffects=new NativeEffects(this);this.effects=new EffectConsumer(this,effectExecutor?{prepare:async effect=>({execute:()=>effectExecutor(effect,{core:this.core,signal:this.abort.signal})})}:this.nativeEffects);this.save();}
71
83
  save(){atomic(this.statePath,this.state);}
72
84
  // What `supercode teams status` shows about this connector: its last error, and the sessions it could not capture
73
85
  // (most recent first, bounded; an entry clears when that session next captures). They are kept in connector.json,
74
86
  // so another process reads them while this one runs; the error itself stays in memory as lastError.
75
87
  get lastError(){return this.lastErrorValue;}
76
- set lastError(error){this.lastErrorValue=error;this.syncStatus().last_error=errorSummary(error,this.clock.nowMs());this.saveQuietly();}
88
+ // The same error (code and message) again changes nothing on disk, so it is not saved again: an error that repeats
89
+ // every beat does not rewrite connector.json every beat. Its time stays that of its first occurrence in the run.
90
+ set lastError(error){this.lastErrorValue=error;const status=this.syncStatus(),summary=errorSummary(error,this.clock.nowMs());if(sameError(status.last_error,summary))return;status.last_error=summary;this.saveQuietly();}
77
91
  syncStatus(){const status=this.state.sync_status??={problems:[],last_error:null};status.problems??=[];return status;}
78
92
  saveQuietly(){try{this.save();}catch{}}
79
93
  noteSessionProblem({harness,nativeSessionId,ruleId=null},error){
80
- const status=this.syncStatus(),at=this.clock.nowMs();
81
- status.problems=[{harness,session_id:nativeSessionId,rule_id:ruleId,...errorSummary(error,at)},...status.problems.filter(item=>!(item.harness===harness&&item.session_id===nativeSessionId))].slice(0,SYNC_PROBLEM_LIMIT);
82
- this.lastError=error;
94
+ const status=this.syncStatus(),at=this.clock.nowMs(),summary=errorSummary(error,at);
95
+ // The same session failing the same way on every beat keeps its entry where it is.
96
+ const listed=status.problems.some(item=>item.harness===harness&&item.session_id===nativeSessionId&&item.rule_id===ruleId&&sameError(item,summary));
97
+ if(!listed)status.problems=[{harness,session_id:nativeSessionId,rule_id:ruleId,...summary},...status.problems.filter(item=>!(item.harness===harness&&item.session_id===nativeSessionId))].slice(0,SYNC_PROBLEM_LIMIT);
98
+ this.lastErrorValue=error;const known=sameError(status.last_error,summary);if(!known)status.last_error=summary;
99
+ if(!listed||!known)this.saveQuietly();
83
100
  }
84
101
  clearSessionProblem(harness,nativeSessionId){
85
102
  const status=this.state.sync_status;if(!status?.problems?.some(item=>item.harness===harness&&item.session_id===nativeSessionId))return;
@@ -143,7 +160,7 @@ export class TeamsConnector {
143
160
  this.heartbeatTimer = setInterval(() => {
144
161
  if (this.abort.signal.aborted) return;
145
162
  void heartbeat().catch(error => { this.lastError = error; });
146
- }, 15000);
163
+ }, this.heartbeatMs);
147
164
  return this.configurationValue;
148
165
  }
149
166
  async effectFetch(method, path, {body, contentType='application/json', raw=false, freshKey=false}={}) {
@@ -175,7 +192,7 @@ export class TeamsConnector {
175
192
  // A rule covers its root folder and every folder under it. Core discovery filters that subtree before it
176
193
  // reads any session's tail, so a beat costs one bounded scan per root, not one over every session on the
177
194
  // machine; rules over the same root and harnesses share the scan.
178
- const discoveries=new Map(),followCandidates=[];
195
+ const discoveries=new Map(),followCandidates=[],discovered=new Set();
179
196
  for(const rule of activeRules.values()){
180
197
  const root=roots.find(item=>item.id===rule.selector.root_id);
181
198
  if(!root)continue;
@@ -195,6 +212,7 @@ export class TeamsConnector {
195
212
  for(const descriptor of descriptors){
196
213
  const locator=descriptor.locator,nativeSessionId=locator?.session_id,harness=locator?.harness;
197
214
  if(!locator||!nativeSessionId||!harness)continue;
215
+ discovered.add(`${harness}:${nativeSessionId}`);
198
216
  const options={locator,descriptor,harness,nativeSessionId,ruleId:rule.id,ruleRevision:rule.revision,backfill:rule.backfill,rootPath:workspace,configuration};
199
217
  // A session whose native store has not changed since this rule last captured it is not reloaded.
200
218
  // One session that cannot be captured (a transcript that rewrote its history needs an explicit resync)
@@ -213,6 +231,7 @@ export class TeamsConnector {
213
231
  }
214
232
  }
215
233
  this.nativeEffects.endDiscovery();
234
+ this.pruneNativeCache(discovered);
216
235
  this.rotateFollowers(followCandidates);
217
236
  }
218
237
  // When a session was last active: its descriptor's updated_at_ms, else its native file's modification time.
@@ -222,14 +241,16 @@ export class TeamsConnector {
222
241
  if(path)try{activity=Math.max(activity??0,statSync(path).mtimeMs);}catch{}
223
242
  return activity;
224
243
  }
225
- // Follows the FOLLOW_LIMIT most recently active candidates (active within the window) and stops every other
226
- // follower. A capacity refusal is not a session's failure: the connector backs off opening follows for a while.
244
+ // Follows the most recently active candidates (active within the window): a follower is kept while it ranks
245
+ // within FOLLOW_KEEP_RANK, every other one stops, and the FOLLOW_LIMIT most recent open new follows while fewer
246
+ // than FOLLOW_LIMIT are live. A capacity refusal is not a session's failure: the connector backs off for a while.
227
247
  rotateFollowers(candidates){
228
- const now=this.clock.nowMs(),seen=new Set(),wanted=[];
248
+ const now=this.clock.nowMs(),seen=new Set(),ranked=[];
229
249
  for(const candidate of candidates.filter(item=>item.activity!==null&&now-item.activity<=FOLLOW_ACTIVE_WINDOW_MS).sort((a,b)=>b.activity-a.activity)){
230
250
  if(seen.has(candidate.key)||typeof this.core.session(candidate.options.locator).follow!=='function')continue;
231
- seen.add(candidate.key);wanted.push(candidate);if(wanted.length>=FOLLOW_LIMIT)break;
251
+ seen.add(candidate.key);ranked.push(candidate);if(ranked.length>=FOLLOW_KEEP_RANK)break;
232
252
  }
253
+ const wanted=ranked.slice(0,FOLLOW_LIMIT);
233
254
  for(const key of this.followers.keys())if(!seen.has(key))this.stopFollower(key);
234
255
  for(const [key,until] of this.followRetry)if(until<=now)this.followRetry.delete(key);
235
256
  for(const {key,options} of wanted){
@@ -282,8 +303,77 @@ export class TeamsConnector {
282
303
  if(stamp===null||this.state.outbox.length)return true;
283
304
  const rule=options.configuration?.rules?.find(item=>item.id===options.ruleId&&item.revision===options.ruleRevision&&item.state==='active');if(!rule)return true;
284
305
  const configured=rule.backfill??options.backfill??'all',mode=typeof configured==='string'?configured:configured.mode;
285
- const seen=this.state.origins[`${options.harness}:${options.nativeSessionId}`]?.capture_stamps?.[options.ruleId];
286
- return !(seen&&seen.stamp===stamp&&seen.rule_revision===options.ruleRevision&&seen.backfill_mode===mode);
306
+ const key=`${options.harness}:${options.nativeSessionId}`,origin=this.state.origins[key],seen=origin?.capture_stamps?.[options.ruleId];
307
+ if(!(seen&&seen.stamp===stamp&&seen.rule_revision===options.ruleRevision&&seen.backfill_mode===mode))return true;
308
+ // Published without its native bundle while it was live: captured again once it has been idle long enough.
309
+ if(origin?.native_deferred&&options.locator?.storage?.kind==='file')try{return this.nativeReady(key,statSync(realpathSync(options.locator.storage.path)));}catch{return false;}
310
+ return false;
311
+ }
312
+ // Whether a capture may attach the native bundle of a file in this state: its translation is cached (and did
313
+ // not fail), or it is small, or it has been idle for nativeIdleMs.
314
+ nativeReady(key,stat){
315
+ const cached=this.state.native_cache[key];
316
+ if(cached?.stamp===fileStamp(stat))return !cached.failed;
317
+ return stat.size<this.nativeIdleBytes||this.clock.nowMs()-stat.mtimeMs>=this.nativeIdleMs;
318
+ }
319
+ // Native artifact bytes are spilled to content-addressed files under the state directory, never into
320
+ // connector.json: a 129 MB transcript was ~172 MB of base64 held in the outbox and rewritten on every save().
321
+ spillDir(){return join(this.stateDir,'artifacts');}
322
+ spill(bytes){
323
+ const sha256=hash(bytes),name=`${sha256}.bin`,path=join(this.spillDir(),name);
324
+ if(!existsSync(path)){
325
+ mkdirSync(this.spillDir(),{recursive:true,mode:0o700});
326
+ const temporary=`${path}.${process.pid}.${randomBytes(12).toString('hex')}.tmp`,fd=openSync(temporary,'wx',0o600);
327
+ try{writeFileSync(fd,bytes);fsyncSync(fd);}catch(error){closeSync(fd);try{unlinkSync(temporary);}catch{}throw error;}
328
+ closeSync(fd);renameSync(temporary,path);
329
+ }
330
+ const chunk_sha256=[];for(let at=0;at<bytes.length||chunk_sha256.length===0;at+=CHUNK_BYTES)chunk_sha256.push(hash(bytes.subarray(at,Math.min(bytes.length,at+CHUNK_BYTES))));
331
+ return {spill:name,sha256,byte_length:bytes.length,chunk_sha256};
332
+ }
333
+ openSpill(name){return openSync(join(this.spillDir(),name),'r');}
334
+ // Each chunk of a spilled artifact, read back one at a time; null when the file is gone or no longer matches.
335
+ *spilledChunks(artifact){
336
+ let fd;try{fd=this.openSpill(artifact.spill.file);}catch{yield null;return;}
337
+ try{
338
+ for(let index=0;index<artifact.manifest.chunks;index++){
339
+ const length=Math.max(0,Math.min(CHUNK_BYTES,artifact.manifest.byte_length-index*CHUNK_BYTES)),bytes=Buffer.alloc(length);
340
+ const read=length?readSync(fd,bytes,0,length,index*CHUNK_BYTES):0;
341
+ if(read!==length||hash(bytes)!==artifact.spill.chunk_sha256[index]){yield null;return;}
342
+ yield {upload_id:artifact.manifest.upload_id,index,sha256:artifact.spill.chunk_sha256[index],bytes_base64:bytes.toString('base64')};
343
+ }
344
+ }finally{closeSync(fd);}
345
+ }
346
+ // Spill files no queued capture and no cached translation refers to are removed.
347
+ pruneSpill(){
348
+ const kept=new Set();
349
+ for(const item of this.state.outbox)for(const artifact of item.artifacts??[])if(artifact.spill)kept.add(artifact.spill.file);
350
+ for(const cached of Object.values(this.state.native_cache))for(const file of cached.files??[])kept.add(file.spill);
351
+ let names=[];try{names=readdirSync(this.spillDir());}catch{return;}
352
+ for(const name of names)if(!kept.has(name)&&!name.endsWith('.tmp'))try{unlinkSync(join(this.spillDir(),name));}catch{}
353
+ }
354
+ // A cached translation of a session no active rule discovers any more (deleted, moved, its rule gone) is dropped
355
+ // with its spill files, so a deleted 129 MB transcript's bundle does not stay on disk for good.
356
+ pruneNativeCache(discovered){
357
+ // A beat that discovers nothing while translations are cached (a home on a drive that is briefly offline) says
358
+ // nothing about those sessions, so it keeps them rather than re-translating every large session later.
359
+ if(!discovered.size&&Object.keys(this.state.native_cache).length)return;
360
+ const stale=Object.keys(this.state.native_cache).filter(key=>!discovered.has(key));if(!stale.length)return;
361
+ for(const key of stale)delete this.state.native_cache[key];
362
+ this.save();this.pruneSpill();
363
+ }
364
+ // A translated bundle, spilled, in the form the cache keeps: one entry per file, primary first or not.
365
+ spillTranslated(translated,defaultName){
366
+ const files=translated.files?.length?translated.files:[{path:translated.suggested_filename??defaultName,content:translated.content,role:'primary'}];
367
+ if(files.filter(file=>file.role==='primary').length!==1)throw Object.assign(new Error('native codec bundle must contain exactly one primary file'),{code:'fidelity_unavailable'});
368
+ let total=0;const spilled=[];
369
+ for(const file of files){const bytes=Buffer.from(file.content,'utf8');total+=bytes.length;if(total>256*1024*1024)throw Object.assign(new Error('native artifact bundle exceeds 256 MiB'),{code:'request_too_large'});spilled.push({path:file.path,role:file.role,...this.spill(bytes)});}
370
+ return {fidelity:translated.fidelity,residue:translated.residue??[],files:spilled};
371
+ }
372
+ // The outbox artifacts and native_bundle of one capture from a spilled bundle; every capture gets fresh upload ids.
373
+ bundleArtifacts(bundle,harness){
374
+ const artifacts=[],files=[];
375
+ for(const file of bundle.files){const uploadId=`upload_${randomBytes(16).toString('hex')}`;artifacts.push({manifest:{upload_id:uploadId,relative_name:file.path,sha256:file.sha256,byte_length:file.byte_length,chunks:file.chunk_sha256.length,media_type:'application/octet-stream',role:file.role==='source_recovery'?'sidecar':'native'},spill:{file:file.spill,chunk_sha256:file.chunk_sha256}});files.push({upload_id:uploadId,role:file.role});}
376
+ return {artifacts,nativeBundle:{id:files.find(file=>file.role==='primary').upload_id,harness,fidelity:bundle.fidelity,residue:bundle.residue,files}};
287
377
  }
288
378
  recordStamp(key,ruleId,ruleRevision,backfillMode,stamp){
289
379
  const origin=this.state.origins[key];
@@ -305,7 +395,11 @@ export class TeamsConnector {
305
395
  const loaded=await this.core.session(locator).load(),configuredBackfill=rule.backfill??backfill??'all',backfillPolicy=typeof configuredBackfill==='string'?{mode:configuredBackfill,since:null}:configuredBackfill;
306
396
  const key=`${harness}:${nativeSessionId}`,previous=this.state.origins[key]??null;
307
397
  let messages=(loaded.messages??loaded.turns??[]).map((entry,index)=>{const value=entry.value??Object.fromEntries(Object.entries(entry).filter(([key])=>!['id','native_key','recorded_at','timestamp'].includes(key)));return{native_key:entry.id??entry.native_key??String(index),recorded_at:recordedAt(entry),value};});
308
- if(previous?.last_rule_revision===ruleRevision&&previous?.backfill_mode===backfillPolicy.mode&&previous?.observed_keys?.length===messages.length&&messages.every((message,index)=>message.native_key===previous.observed_keys[index]&&hash(canonicalJson(message.value))===previous.observed_digests?.[index])){this.recordStamp(key,ruleId,ruleRevision,backfillPolicy.mode,stamp);return null;}
398
+ // A session published without its native bundle is captured again, with the same messages, once the bundle can be built.
399
+ const nativeWanted=backfillPolicy.mode==='all'&&typeof this.core.translateSession==='function';
400
+ const retryNative=Boolean(previous?.native_deferred&&nativeWanted&&artifactBefore?.isFile()&&!previous?.baseline&&this.nativeReady(key,artifactBefore));
401
+ const unchanged=previous?.last_rule_revision===ruleRevision&&previous?.backfill_mode===backfillPolicy.mode&&previous?.observed_keys?.length===messages.length&&messages.every((message,index)=>message.native_key===previous.observed_keys[index]&&hash(canonicalJson(message.value))===previous.observed_digests?.[index]);
402
+ if(unchanged&&!retryNative){this.recordStamp(key,ruleId,ruleRevision,backfillPolicy.mode,stamp);return null;}
309
403
  let effectivePrevious=previous;const clearBaseline=backfillPolicy.mode==='all';
310
404
  if(!previous&&backfillPolicy.mode==='none'){const toolTail=trailingToolResults(messages);this.state.origins[key]={generation:1,sequence:0,digest:null,baseline:messages.map(x=>x.native_key),baseline_digests:messages.map(x=>hash(canonicalJson(x.value))),...(toolTail?{baseline_tool_tail:toolTail}:{}),observed_keys:messages.map(x=>x.native_key),observed_digests:messages.map(x=>hash(canonicalJson(x.value))),last_rule_revision:ruleRevision,backfill_mode:backfillPolicy.mode};this.recordStamp(key,ruleId,ruleRevision,backfillPolicy.mode,stamp);return null;}
311
405
  if(!previous&&backfillPolicy.mode==='since'){
@@ -330,7 +424,9 @@ export class TeamsConnector {
330
424
  if(baselineMoved&&backfillPolicy.mode==='since'&&Number.isSafeInteger(backfillPolicy.since)){
331
425
  const excluded=messages.filter(message=>message.recorded_at==null||message.recorded_at<backfillPolicy.since);
332
426
  effectivePrevious={...effectivePrevious,baseline:excluded.map(message=>message.native_key),baseline_digests:excluded.map(message=>hash(canonicalJson(message.value)))};
333
- this.state.origins[key]={...this.state.origins[key],baseline:effectivePrevious.baseline,baseline_digests:effectivePrevious.baseline_digests};
427
+ // A tool tail recorded for the old baseline describes positions that no longer hold; it goes with it.
428
+ delete effectivePrevious.baseline_tool_tail;
429
+ this.state.origins[key]={...this.state.origins[key],baseline:effectivePrevious.baseline,baseline_digests:effectivePrevious.baseline_digests};delete this.state.origins[key].baseline_tool_tail;
334
430
  }else if(baselineMoved)throw Object.assign(new Error(`the history kept back when rule ${ruleId} started was rewritten; run \`supercode teams sync resume ${ruleId} --backfill all\` (or a since window) to republish this session`),{code:'resync_required'});
335
431
  }
336
432
  if(effectivePrevious?.baseline&&!clearBaseline)messages=messages.filter(x=>!effectivePrevious.baseline.includes(x.native_key));
@@ -338,29 +434,55 @@ export class TeamsConnector {
338
434
  const captureId=`cap_${randomBytes(16).toString('hex')}`,generation=effectivePrevious?.generation??1,sequence=(effectivePrevious?.sequence??0)+1;
339
435
  const begin={native_session_id:nativeSessionId,harness,generation,sequence,capture_id:captureId,previous_digest:effectivePrevious?.digest??null,visibility,rule_id:ruleId,rule_revision:ruleRevision};
340
436
  const parts=[];for(let index=0;index<messages.length;index+=500)parts.push({capture_id:captureId,index:parts.length,messages:messages.slice(index,index+500)});
341
- const artifacts=[];
437
+ let artifacts=[];
342
438
  // Native stores normally live in harness homes outside the repository. The locator is
343
439
  // accepted only when it came from this configured core discovery, and raw bytes are safe
344
440
  // to publish only when the complete native history is in the permitted projection.
345
- let nativeBundle=null;
441
+ let nativeBundle=null,nativeDeferred=false;
346
442
  // A session in a shared SQLite store (OpenCode, Goose) has no file of its own to stat; it is
347
443
  // stable when the core reads back the same messages this capture publishes.
348
444
  const sqliteSource=locator?.storage?.kind==='sqlite';
349
- if((artifactSource||sqliteSource)&&backfillPolicy.mode==='all'&&!effectivePrevious?.baseline&&typeof this.core.translateSession==='function'){
350
- if(artifactSource&&!artifactBefore.isFile())throw Object.assign(new Error('native artifact locator is not a regular file'),{code:'invalid_request'});
351
- const translated=(await this.core.translateSession({locator,target_harness:harness})).artifact;
352
- const changed=new Error('native session changed while it was being captured');
353
- if(artifactSource){const after=statSync(artifactSource);if(artifactBefore.dev!==after.dev||artifactBefore.ino!==after.ino||artifactBefore.size!==after.size||artifactBefore.mtimeMs!==after.mtimeMs)throw Object.assign(changed,{code:'resync_required'});}
354
- else{const digest=entries=>hash(canonicalJson((entries.messages??entries.turns??[]).map(entry=>entry.value??entry)));if(digest(await this.core.session(locator).load())!==digest(loaded))throw Object.assign(changed,{code:'resync_required'});}
355
- const translatedFiles=translated.files?.length?translated.files:[{path:translated.suggested_filename??(artifactSource?basename(artifactSource):`${nativeSessionId}.json`),content:translated.content,role:'primary'}];
356
- if(translatedFiles.filter(file=>file.role==='primary').length!==1)throw Object.assign(new Error('native codec bundle must contain exactly one primary file'),{code:'fidelity_unavailable'});
357
- let total=0;const bundleFiles=[];
358
- for(const file of translatedFiles){const bytes=Buffer.from(file.content,'utf8');total+=bytes.length;if(total>256*1024*1024)throw Object.assign(new Error('native artifact bundle exceeds 256 MiB'),{code:'request_too_large'});const uploadId=`upload_${randomBytes(16).toString('hex')}`,chunks=[];for(let at=0,index=0;at<bytes.length||index===0;at+=1024*1024,index++){const chunk=bytes.subarray(at,Math.min(bytes.length,at+1024*1024));chunks.push({upload_id:uploadId,index,sha256:hash(chunk),bytes_base64:chunk.toString('base64')});if(bytes.length===0)break;}artifacts.push({manifest:{upload_id:uploadId,relative_name:file.path,sha256:hash(bytes),byte_length:bytes.length,chunks:chunks.length,media_type:'application/octet-stream',role:file.role==='source_recovery'?'sidecar':'native'},chunks});bundleFiles.push({upload_id:uploadId,role:file.role});}
359
- nativeBundle={id:bundleFiles.find(file=>file.role==='primary').upload_id,harness,fidelity:translated.fidelity,residue:translated.residue??[],files:bundleFiles};
445
+ // Translating reads the whole native file, so its time grows with the file: a 129 MB Claude Code transcript
446
+ // outlasted the client's 30 s default on every beat and never synced. About 2 s per MB, from 30 s to 10 min.
447
+ const translateTimeoutMs=Math.min(600000,Math.max(30000,Math.ceil((artifactBefore?.size??0)/1e6)*2000));
448
+ if(artifactSource&&nativeWanted){
449
+ if(!artifactBefore.isFile())throw Object.assign(new Error('native artifact locator is not a regular file'),{code:'invalid_request'});
450
+ // A file source's bundle is best effort: when it cannot be attached (the file is live, it moved while it was
451
+ // read, the rule's kept-back baseline is still being cleared, or translating it failed) the messages are
452
+ // published without it and nativeDeferred brings the session back once the file is idle. Coverage speaks
453
+ // of the message history, which is whole either way, so it is unchanged; the capture simply names no
454
+ // artifacts, and the server refuses a native export or continue of that revision (fidelity_unavailable).
455
+ const onDisk=fileStamp(artifactBefore);let bundle=null;
456
+ if(!effectivePrevious?.baseline&&this.nativeReady(key,artifactBefore)){
457
+ const cached=this.state.native_cache[key];
458
+ if(cached?.stamp===onDisk&&cached.files.every(file=>existsSync(join(this.spillDir(),file.spill))))bundle=cached;
459
+ else try{
460
+ const translated=(await this.core.translateSession({locator,target_harness:harness},{timeoutMs:translateTimeoutMs})).artifact;
461
+ if(fileStamp(statSync(artifactSource))===onDisk){
462
+ bundle={stamp:onDisk,...this.spillTranslated(translated,basename(artifactSource))};
463
+ // Only a translation that took real time is worth keeping; a small file translates again in no time.
464
+ if(artifactBefore.size>=this.nativeIdleBytes){this.state.native_cache[key]=bundle;this.save();this.pruneSpill();}
465
+ }
466
+ }catch(error){
467
+ if(this.abort.signal.aborted)throw error;
468
+ this.lastError=error;process.stderr.write(`supercode teams: ${harness} session ${nativeSessionId} published without its native bundle: ${error.message}\n`);
469
+ // The same file is not translated again; a write to it clears this.
470
+ this.state.native_cache[key]={stamp:onDisk,failed:true,files:[]};this.save();this.pruneSpill();
471
+ }
472
+ }
473
+ // The messages were loaded after artifactBefore was read, so they match the bundle only if the file is still that file.
474
+ if(bundle&&fileStamp(statSync(artifactSource))!==onDisk)bundle=null;
475
+ if(bundle)({artifacts,nativeBundle}=this.bundleArtifacts(bundle,harness));else nativeDeferred=true;
476
+ // A retry that still has no bundle would republish the same messages for nothing.
477
+ if(unchanged&&!nativeBundle){this.recordStamp(key,ruleId,ruleRevision,backfillPolicy.mode,stamp);return null;}
478
+ }else if(sqliteSource&&nativeWanted&&!effectivePrevious?.baseline){
479
+ const translated=(await this.core.translateSession({locator,target_harness:harness},{timeoutMs:translateTimeoutMs})).artifact;
480
+ const digest=entries=>hash(canonicalJson((entries.messages??entries.turns??[]).map(entry=>entry.value??entry)));if(digest(await this.core.session(locator).load())!==digest(loaded))throw Object.assign(new Error('native session changed while it was being captured'),{code:'resync_required'});
481
+ ({artifacts,nativeBundle}=this.bundleArtifacts(this.spillTranslated(translated,`${nativeSessionId}.json`),harness));
360
482
  }
361
483
  const recordedTimes=messages.map(message=>message.recorded_at).filter(Number.isSafeInteger),commit={capture_id:captureId,parts:parts.length,digest:'',title:descriptor?.title??null,repository:null,reported_user:null,recorded_activity_at:recordedTimes.length?Math.max(...recordedTimes):null,parent_native_id:descriptor?.parent_session_id??null,coverage:effectivePrevious?.baseline&&!clearBaseline?{...coverage,completeness:'partial',reasons:[...new Set([...coverage.reasons,'prefix_not_captured'])]}:coverage,artifacts:artifacts.map(item=>item.manifest),...(nativeBundle?{native_bundle:nativeBundle}:{})};
362
484
  const digestCommit={...commit};delete digestCommit.digest;commit.digest=hash(canonicalJson({begin,parts,commit:digestCommit}));
363
- this.state.outbox.push({begin,parts,commit,artifacts,clearBaseline,backfillMode:backfillPolicy.mode,stamp,observed:{keys:(loaded.messages??loaded.turns??[]).map((entry,index)=>entry.id??entry.native_key??String(index)),digests:(loaded.messages??loaded.turns??[]).map(entry=>hash(canonicalJson(entry.value??Object.fromEntries(Object.entries(entry).filter(([key])=>!['id','native_key','recorded_at','timestamp'].includes(key))))))}});this.save();return this.flushSerial();
485
+ this.state.outbox.push({begin,parts,commit,artifacts,clearBaseline,backfillMode:backfillPolicy.mode,stamp,nativeDeferred,observed:{keys:(loaded.messages??loaded.turns??[]).map((entry,index)=>entry.id??entry.native_key??String(index)),digests:(loaded.messages??loaded.turns??[]).map(entry=>hash(canonicalJson(entry.value??Object.fromEntries(Object.entries(entry).filter(([key])=>!['id','native_key','recorded_at','timestamp'].includes(key))))))}});this.save();return this.flushSerial();
364
486
  }
365
487
  flush() {
366
488
  const work=this.work.then(()=>this.flushSerial());
@@ -369,7 +491,49 @@ export class TeamsConnector {
369
491
  }
370
492
  // Every outbox error is the connection's, not one session's: a capture's own catch passes it on (syncConfiguredSessions).
371
493
  async flushSerial(){try{return await this.flushOutbox();}catch(error){throw Object.assign(error,{outbox:true});}}
372
- async sendCapture(item){const begun=await this.request('POST',`/sources/${this.sourceId}/captures`,item.begin);if(begun.state==='committed')return begun.receipt;for(const part of item.parts)await this.request('PUT',`/sources/${this.sourceId}/captures/${item.begin.capture_id}/parts/${part.index}`,part);for(const artifact of item.artifacts??[])for(const chunk of artifact.chunks)await this.request('PUT',`/sources/${this.sourceId}/uploads/${chunk.upload_id}/chunks/${chunk.index}`,chunk);return this.request('POST',`/sources/${this.sourceId}/captures/${item.begin.capture_id}/commit`,item.commit);}
494
+ // A spill file is hashed once per version on disk (its stat stamp) and only stat'ed after that. It is lost only
495
+ // when it is missing, the wrong size or damaged; any other failure to read it (a Windows antivirus lock) is thrown,
496
+ // which keeps the capture queued with its bundle for the next flush.
497
+ // A read error that never clears (a lock never released, a lasting EACCES) is retried on five flushes, then the
498
+ // file counts as lost: the outbox is flushed in order, so one unreadable file would otherwise stop every session.
499
+ spillLost(artifact){
500
+ const failures=this.spillReadFailures??=new Map();
501
+ try{const lost=this.spillLostOnce(artifact);failures.delete(artifact.spill.file);return lost;}
502
+ catch(error){
503
+ const count=(failures.get(artifact.spill.file)??0)+1;failures.set(artifact.spill.file,count);
504
+ if(count<5)throw error;
505
+ failures.delete(artifact.spill.file);
506
+ process.stderr.write(`supercode teams: a spilled native artifact could not be read on ${count} flushes (${error.message}); treating it as lost\n`);
507
+ return true;
508
+ }
509
+ }
510
+ spillLostOnce(artifact){
511
+ const path=join(this.spillDir(),artifact.spill.file),missing=error=>error?.code==='ENOENT'||error?.code==='ENOTDIR';
512
+ let stat;try{stat=statSync(path);}catch(error){if(missing(error))return true;throw error;}
513
+ if(!stat.isFile()||stat.size!==artifact.manifest.byte_length)return true;
514
+ const stamp=fileStamp(stat),verified=this.verifiedSpills??=new Map();
515
+ if(verified.get(artifact.spill.file)===stamp)return false;
516
+ let fd;try{fd=this.openSpill(artifact.spill.file);}catch(error){if(missing(error))return true;throw error;}
517
+ try{
518
+ for(let index=0;index<artifact.manifest.chunks;index++){
519
+ const length=Math.max(0,Math.min(CHUNK_BYTES,artifact.manifest.byte_length-index*CHUNK_BYTES)),bytes=Buffer.alloc(length);
520
+ if((length?readSync(fd,bytes,0,length,index*CHUNK_BYTES):0)!==length||hash(bytes)!==artifact.spill.chunk_sha256[index])return true;
521
+ }
522
+ }finally{closeSync(fd);}
523
+ verified.set(artifact.spill.file,stamp);return false;
524
+ }
525
+ // A queued capture whose spilled artifact is gone or damaged on disk would never flush: it is sent without
526
+ // its native bundle instead (its digest resealed), and the session is captured again for one.
527
+ dropLostArtifacts(item){
528
+ if(!(item.artifacts??[]).some(artifact=>artifact.spill&&this.spillLost(artifact)))return;
529
+ process.stderr.write(`supercode teams: a queued capture of ${item.begin.harness} session ${item.begin.native_session_id} lost its native artifact on disk; sending it without one\n`);
530
+ item.artifacts=[];item.commit.artifacts=[];delete item.commit.native_bundle;item.nativeDeferred=true;
531
+ const digestCommit={...item.commit};delete digestCommit.digest;item.commit.digest=hash(canonicalJson({begin:item.begin,parts:item.parts,commit:digestCommit}));this.save();
532
+ }
533
+ async sendCapture(item){const begun=await this.request('POST',`/sources/${this.sourceId}/captures`,item.begin);if(begun.state==='committed')return begun.receipt;for(const part of item.parts)await this.request('PUT',`/sources/${this.sourceId}/captures/${item.begin.capture_id}/parts/${part.index}`,part);
534
+ // A capture queued by an older connector holds its chunks inline; a current one reads them from its spill file.
535
+ for(const artifact of item.artifacts??[])for(const chunk of artifact.spill?this.spilledChunks(artifact):artifact.chunks){if(!chunk)throw Object.assign(new Error('a spilled native artifact changed on disk while it was sent'),{code:'resync_required'});await this.request('PUT',`/sources/${this.sourceId}/uploads/${chunk.upload_id}/chunks/${chunk.index}`,chunk);}
536
+ return this.request('POST',`/sources/${this.sourceId}/captures/${item.begin.capture_id}/commit`,item.commit);}
373
537
  // True when the server refused a queued capture because its sync rule was removed, paused or revised since it
374
538
  // was queued: the capture can never be accepted, so it must not hold every later capture behind it. The
375
539
  // refusal is confirmed against a fresh configuration; any other refusal (a revoked credential, a paused
@@ -381,6 +545,6 @@ export class TeamsConnector {
381
545
  if(!Array.isArray(configuration?.rules))return false;
382
546
  return !configuration.rules.some(rule=>rule.id===item.begin.rule_id&&rule.revision===item.begin.rule_revision&&rule.state==='active');
383
547
  }
384
- async flushOutbox(){let latest=null;while(this.state.outbox.length){const item=this.state.outbox[0];let receipt;try{receipt=await this.sendCapture(item);}catch(error){if(!await this.ruleRejected(item,error))throw error;this.lastError=error;process.stderr.write(`supercode teams: dropped a queued capture of ${item.begin.harness} session ${item.begin.native_session_id}; its sync rule changed or was removed: ${error.message}\n`);this.state.outbox.shift();this.save();continue;}const key=`${item.begin.harness}:${item.begin.native_session_id}`,prior=this.state.origins[key]??{},next={...prior,generation:item.begin.generation,sequence:item.begin.sequence,digest:item.commit.digest,observed_keys:item.observed?.keys??prior.observed_keys,observed_digests:item.observed?.digests??prior.observed_digests,last_rule_revision:item.begin.rule_revision,backfill_mode:item.backfillMode};if(item.clearBaseline){delete next.baseline;delete next.baseline_digests;delete next.baseline_tool_tail;}if(item.stamp&&item.begin.rule_id)next.capture_stamps={...prior.capture_stamps,[item.begin.rule_id]:{stamp:item.stamp,rule_revision:item.begin.rule_revision,backfill_mode:item.backfillMode}};this.state.origins[key]=next;this.state.outbox.shift();this.save();latest=receipt;}return latest;}
548
+ async flushOutbox(){let latest=null;while(this.state.outbox.length){const item=this.state.outbox[0];let receipt;this.dropLostArtifacts(item);try{receipt=await this.sendCapture(item);}catch(error){if(!await this.ruleRejected(item,error))throw error;this.lastError=error;process.stderr.write(`supercode teams: dropped a queued capture of ${item.begin.harness} session ${item.begin.native_session_id}; its sync rule changed or was removed: ${error.message}\n`);this.state.outbox.shift();this.save();continue;}const key=`${item.begin.harness}:${item.begin.native_session_id}`,prior=this.state.origins[key]??{},next={...prior,generation:item.begin.generation,sequence:item.begin.sequence,digest:item.commit.digest,observed_keys:item.observed?.keys??prior.observed_keys,observed_digests:item.observed?.digests??prior.observed_digests,last_rule_revision:item.begin.rule_revision,backfill_mode:item.backfillMode};if(item.clearBaseline){delete next.baseline;delete next.baseline_digests;delete next.baseline_tool_tail;}if(item.nativeDeferred)next.native_deferred=true;else delete next.native_deferred;if(item.stamp&&item.begin.rule_id)next.capture_stamps={...prior.capture_stamps,[item.begin.rule_id]:{stamp:item.stamp,rule_revision:item.begin.rule_revision,backfill_mode:item.backfillMode}};this.state.origins[key]=next;this.state.outbox.shift();this.save();if(item.artifacts?.length)this.pruneSpill();latest=receipt;}return latest;}
385
549
  stop(){if(this.timer)clearInterval(this.timer);if(this.heartbeatTimer)clearInterval(this.heartbeatTimer);this.abort.abort();for(const follower of this.followers?.values()??[])follower.controller.abort();return this.nativeEffects.close();}
386
550
  }
@@ -1,5 +1,5 @@
1
1
  import { readFileSync } from 'node:fs';
2
- import { join, resolve } from 'node:path';
2
+ import { join, resolve, sep } from 'node:path';
3
3
 
4
4
  // A continuation materializes a new native session (new ids), so value_lossless is the only
5
5
  // fidelity it delivers.
@@ -19,7 +19,7 @@ export function createNativeContinuationAdapter({ core }) {
19
19
  if (signal?.aborted) throw signal.reason ?? new Error('native continuation was aborted');
20
20
  const files = ticket.artifact.files.map(file => {
21
21
  const path = resolve(directory, file.relative_name);
22
- if (!path.startsWith(`${resolve(directory)}/`)) throw unsupported('native continuation artifact escapes its verified directory');
22
+ if (!path.startsWith(`${resolve(directory)}${sep}`)) throw unsupported('native continuation artifact escapes its verified directory');
23
23
  return { path: file.relative_name, content: readFileSync(path, 'utf8'), role: file.role };
24
24
  });
25
25
  const primary = files.filter(file => file.role === 'primary');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-teams",
3
- "version": "0.2.24",
3
+ "version": "0.2.25",
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",
@@ -61,7 +61,7 @@
61
61
  "@volter/supercode-frontend": "^0.2.2",
62
62
  "@volter/supercode-harness-sdk": "^0.3.43",
63
63
  "@volter/supercode-terminal": "^0.2.30",
64
- "@volter/identity": "^0.4.0",
64
+ "@volter/identity": "^0.4.1",
65
65
  "@volter/teams": "^0.1.8",
66
66
  "@xterm/addon-serialize": "^0.14.0",
67
67
  "@xterm/headless": "^6.0.0",
@@ -46,6 +46,10 @@ class Reader {
46
46
  const signed64 = (value) => BigInt.asIntN(64, value);
47
47
  const zigzag64 = (value) => (value >> 1n) ^ -(value & 1n);
48
48
 
49
+ // As `json: true`: ProtoJSON spells non-finite floats and doubles "NaN", "Infinity" and "-Infinity"
50
+ // (JSON has no such numbers; JSON.stringify would turn them into null).
51
+ const protoJsonDouble = (value) => Number.isFinite(value) ? value : String(value);
52
+
49
53
  function scalar(type, wire, reader) {
50
54
  switch (type) {
51
55
  case 'string': return utf8.decode(reader.take(Number(reader.varint())));
@@ -59,10 +63,10 @@ function scalar(type, wire, reader) {
59
63
  case 'sint64': return zigzag64(reader.varint()).toString();
60
64
  case 'fixed32': return reader.fixed32();
61
65
  case 'sfixed32': return reader.fixed32() | 0;
62
- case 'float': { const at = reader.pos; reader.take(4); return reader.view.getFloat32(at, true); }
66
+ case 'float': { const at = reader.pos; reader.take(4); return protoJsonDouble(reader.view.getFloat32(at, true)); }
63
67
  case 'fixed64': return reader.fixed64().toString();
64
68
  case 'sfixed64': return signed64(reader.fixed64()).toString();
65
- case 'double': { const at = reader.pos; reader.take(8); return reader.view.getFloat64(at, true); }
69
+ case 'double': { const at = reader.pos; reader.take(8); return protoJsonDouble(reader.view.getFloat64(at, true)); }
66
70
  default: throw new TypeError(`unsupported protobuf scalar ${type} (wire ${wire})`);
67
71
  }
68
72
  }