@volter/supercode-teams 0.2.79 → 0.2.81

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.
@@ -27,6 +27,9 @@ const sameOwner = owner => {
27
27
  try { process.kill(owner.pid, 0); return true; } catch (error) { return error.code === 'EPERM'; }
28
28
  };
29
29
 
30
+ /** Whether the process holding this state directory's lifecycle lock is still the one that took it. */
31
+ export function lifecycleHeld(directory) { return sameOwner(readOwner(join(directory, 'lifecycle.lock'))); }
32
+
30
33
  export async function activeSocket(path) {
31
34
  return new Promise(resolve => {
32
35
  const socket = createConnection(path);
package/bin/workspace.mjs CHANGED
@@ -17,7 +17,7 @@ import { teamsHome, paths } from '@volter/teams/home';
17
17
  import { loadOrCreateKeyPair, publicKeyId } from '@volter/teams/identity';
18
18
  import { duration, parseArgs, positiveInteger, requireArgs, usage } from './workspace/args.mjs';
19
19
  import { reserveArtifactDirectory, reserveProtected, writeProtected } from './workspace/files.mjs';
20
- import { acquireLifecycle, activeSocket, requestStop } from './workspace/lifecycle.mjs';
20
+ import { acquireLifecycle, activeSocket, lifecycleHeld, requestStop } from './workspace/lifecycle.mjs';
21
21
  import { volterDeviceSignIn } from './workspace/volter-device.mjs';
22
22
  import { loginSummary, signInToTeams } from './workspace/volter-login.mjs';
23
23
  import { readSharedSignIn, signOut } from '@volter/identity';
@@ -653,12 +653,14 @@ async function connectorCommand(verb, { flags, rest, output, client, context, te
653
653
  if (verb === 'status') {
654
654
  let reachable = true; let account = null; try { account = await client.me(); } catch { reachable = false; }
655
655
  const saved = state.read(); const running = saved?.process?.socket ? await activeSocket(saved.process.socket) : false;
656
+ // A connector deep in a large capture can miss the probe's second while its process lives on: it is busy, not stopped.
657
+ const busy = !running && Boolean(saved?.process) && lifecycleHeld(state.directory);
656
658
  // What the connector could not sync, as it last wrote it to its state (it is written while it runs).
657
659
  const sync = saved ? readSyncStatus(state.directory) ?? { not_syncing: [], last_error: null, queued_captures: 0 } : null;
658
- const value = { version: 1, context: scope(context), data: { reachable, account, connector: saved ? { enrolled: true, running, machine: saved.machine, source: saved.source, sync } : { enrolled: false, running: false }, sources: reachable ? (await team.sources.list()).items : [] } };
660
+ const value = { version: 1, context: scope(context), data: { reachable, account, connector: saved ? { enrolled: true, running: running || busy, ...(busy ? { busy: true } : {}), machine: saved.machine, source: saved.source, sync } : { enrolled: false, running: false }, sources: reachable ? (await team.sources.list()).items : [] } };
659
661
  if (flags.json) return output(value);
660
662
  const lines = [`Team ${context.team_name ?? context.team_id} · ${new URL(context.origin).host}${reachable ? '' : ' · unreachable'}`];
661
- lines.push(saved?.machine ? `Connector ${saved.machine.name ?? saved.machine.id} · ${running ? 'running' : 'not running'}${saved.service?.kind ? ` (${saved.service.kind})` : ''}` : 'Connector not connected (supercode teams connect)');
663
+ lines.push(saved?.machine ? `Connector ${saved.machine.name ?? saved.machine.id} · ${running ? 'running' : busy ? 'running (busy: its control socket did not answer within 1 s)' : 'not running'}${saved.service?.kind ? ` (${saved.service.kind})` : ''}` : 'Connector not connected (supercode teams connect)');
662
664
  lines.push(...syncStatusLines(sync));
663
665
  process.stdout.write(`${lines.join('\n')}\n`);
664
666
  return;
@@ -439,7 +439,8 @@ export class TeamsConnector {
439
439
  const artifactBefore=path?statSync(path):null;
440
440
  options={...options,preloaded:{loaded:await (options.live?this.liveCore:this.core).session(locator).load(),artifactBefore}};
441
441
  }
442
- const work=this.work.then(()=>this.captureSerial(options)).then(receipt=>{this.clearSessionProblem(options.harness,options.nativeSessionId);return receipt;});
442
+ // A capture the server refused and the outbox dropped stays that session's problem; any other capture clears it.
443
+ const work=this.work.then(()=>this.captureSerial(options)).then(receipt=>{if(!this.droppedCaptures?.delete(`${options.harness}:${options.nativeSessionId}`))this.clearSessionProblem(options.harness,options.nativeSessionId);return receipt;});
443
444
  this.work=work.catch(()=>{});
444
445
  return work;
445
446
  }
@@ -701,6 +702,6 @@ export class TeamsConnector {
701
702
  if(!Array.isArray(configuration?.rules))return false;
702
703
  return !configuration.rules.some(rule=>rule.id===item.begin.rule_id&&rule.revision===item.begin.rule_revision&&rule.state==='active');
703
704
  }
704
- async flushOutbox(){let latest=null;while(this.state.outbox.length){const item=this.state.outbox[0];let receipt;this.dropLostArtifacts(item);this.refitParts(item);try{receipt=await this.sendCapture(item);}catch(error){if(error.status===409&&error.code==='carry_mismatch'){const key=`${item.begin.harness}:${item.begin.native_session_id}`;this.lastError=error;process.stderr.write(`supercode teams: the server does not hold what a capture of ${item.begin.harness} session ${item.begin.native_session_id} carried; it goes whole next time\n`);this.state.outbox.shift();if(this.state.origins[key])this.state.origins[key]={...this.state.origins[key],observed_keys:[],observed_digests:[],carry_refused:true};this.save();continue;}if(error.status===400&&error.code==='invalid_request'||error.status===413){this.lastError=error;this.noteSessionProblem({harness:item.begin.harness,nativeSessionId:item.begin.native_session_id,ruleId:item.begin.rule_id??null},error);process.stderr.write(`supercode teams: dropped a queued capture of ${item.begin.harness} session ${item.begin.native_session_id} the server refused${error.status===413?' (one message is larger than a request may be)':' as malformed'}: ${error.message}\n`);this.state.outbox.shift();this.save();continue;}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.begin.carry)delete next.carry_refused;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;}
705
+ async flushOutbox(){let latest=null;while(this.state.outbox.length){const item=this.state.outbox[0];let receipt;this.dropLostArtifacts(item);this.refitParts(item);try{receipt=await this.sendCapture(item);}catch(error){if(error.status===409&&error.code==='carry_mismatch'){const key=`${item.begin.harness}:${item.begin.native_session_id}`;this.lastError=error;process.stderr.write(`supercode teams: the server does not hold what a capture of ${item.begin.harness} session ${item.begin.native_session_id} carried; it goes whole next time\n`);this.state.outbox.shift();if(this.state.origins[key])this.state.origins[key]={...this.state.origins[key],observed_keys:[],observed_digests:[],carry_refused:true};this.save();continue;}if(error.status===400&&error.code==='invalid_request'||error.status===413){this.lastError=error;this.noteSessionProblem({harness:item.begin.harness,nativeSessionId:item.begin.native_session_id,ruleId:item.begin.rule_id??null},error);(this.droppedCaptures??=new Set()).add(`${item.begin.harness}:${item.begin.native_session_id}`);process.stderr.write(`supercode teams: dropped a queued capture of ${item.begin.harness} session ${item.begin.native_session_id} the server refused${error.status===413?' (one message is larger than a request may be)':' as malformed'}: ${error.message}\n`);this.state.outbox.shift();this.save();continue;}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.begin.carry)delete next.carry_refused;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;}
705
706
  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();}
706
707
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-teams",
3
- "version": "0.2.79",
3
+ "version": "0.2.81",
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",
package/terminal-look.mjs CHANGED
@@ -45,7 +45,7 @@ function run() {
45
45
  const font = archived('Font');
46
46
  return JSON.stringify({
47
47
  name, colors,
48
- font: font && !font.isNil() ? { family: ObjC.unwrap(font.familyName), size: font.pointSize, lineHeight: $.NSLayoutManager.alloc.init.defaultLineHeightForFont(font), ascender: font.ascender, descender: font.descender } : null,
48
+ font: font && !font.isNil() ? { family: ObjC.unwrap(font.familyName), size: font.pointSize, advance: font.advancementForGlyph(font.glyphWithName('M')).width, ascender: font.ascender, descender: font.descender, leading: font.leading } : null,
49
49
  widthSpacing: value('FontWidthSpacing'), heightSpacing: value('FontHeightSpacing'),
50
50
  cursorType: value('CursorType'), cursorBlink: value('CursorBlink'), optionIsMeta: value('useOptionAsMetaKey'),
51
51
  bell: value('Bell'), visualBell: value('VisualBell'), brightBold: value('UseBrightBold'),
@@ -85,11 +85,26 @@ export function weztermConfig(profile, { fontDirs = existsSync(TERMINAL_FONTS) ?
85
85
  };
86
86
  };
87
87
  const table = (colors) => `{\n${Object.entries(colors).map(([key, value]) => ` ${key} = ${Array.isArray(value) ? `{ ${value.map(lua).join(', ')} }` : lua(value)},`).join('\n')}\n }`;
88
- const families = mac ? [...new Set([profile?.font?.family ?? 'SF Mono Terminal', ...MAC_CASCADE])] : [];
89
- // A row is AppKit's line height for the face (Terminal.app's), where WezTerm's is the face's ascent plus descent.
88
+ // Terminal.app's face drawn by CoreText is heavier than the same face drawn by WezTerm's FreeType: on the same Retina
89
+ // display, SF Mono Terminal 12 put 14432 of ink per cell in Terminal.app, 12043 at Medium and 15864 at DemiBold in
90
+ // WezTerm (Regular: 72%). DemiBold is the nearest; bold text takes the face's Bold in both (94%).
91
+ const primary = `{ family = ${lua(profile?.font?.family ?? 'SF Mono Terminal')}, weight = 'DemiBold' }`;
92
+ const families = mac ? [primary, ...MAC_CASCADE.map(lua)] : [];
93
+ // Terminal.app's cell is whole points: the advance rounded down (7 pt for SF Mono Terminal 12's 7.418) and the line
94
+ // (ascent + descent + leading) rounded up (15 pt for 14.32), measured as 14 x 30 px on a Retina display. WezTerm rounds
95
+ // its cell up to whole pixels before and after these factors, so the width factor is floor/ceil of the advance (14 px
96
+ // at 2x) and the height factor lands on the doubled line at 2x (30 px). Evidence stops at 2x: at 1x the same factors
97
+ // predict 7 x 16 px where Terminal.app draws 7 x 15.
90
98
  const font = profile?.font;
91
- const rowScale = font?.lineHeight && font.ascender - font.descender > 0 ? font.lineHeight / (font.ascender - font.descender) : 1;
92
- const opacity = stored.BackgroundColor?.[3] ?? 1;
99
+ const advance = (font?.advance ?? 0) * (profile?.widthSpacing ?? 1);
100
+ const cellWidth = advance > 0 ? Math.floor(advance) / Math.ceil(advance) : 1;
101
+ const height = font ? font.ascender - font.descender + (font.leading ?? 0) : 0;
102
+ const lineHeight = height > 0 ? (2 * Math.ceil(height) - 0.5) / Math.ceil(2 * height) : 1;
103
+ // Terminal.app's translucent, blurred ground at 0.95 rendered within 2 levels of the opaque colour; WezTerm's at 0.95
104
+ // rendered 8 levels lighter over the same desktop. So a nearly opaque profile is drawn opaque (below 0.9 the profile's
105
+ // opacity is kept: unmeasured).
106
+ const alpha = stored.BackgroundColor?.[3] ?? 1;
107
+ const opacity = alpha >= 0.9 ? 1 : alpha;
93
108
  const cursorShape = ['Block', 'Underline', 'Bar'][profile?.cursorType ?? 0] ?? 'Block';
94
109
  const composed = !profile?.optionIsMeta;
95
110
  const lines = [
@@ -100,14 +115,14 @@ export function weztermConfig(profile, { fontDirs = existsSync(TERMINAL_FONTS) ?
100
115
  "local appearance = wezterm.gui and wezterm.gui.get_appearance() or 'Dark'",
101
116
  "config.colors = appearance:find('Dark') and schemes.dark or schemes.light",
102
117
  `config.font_dirs = { ${fontDirs.map(lua).join(', ')} }`,
103
- ...(families.length ? [`config.font = wezterm.font_with_fallback({ ${families.map(lua).join(', ')} })`] : []),
118
+ ...(families.length ? [`config.font = wezterm.font_with_fallback({ ${families.join(', ')} })`] : []),
104
119
  // CoreText draws glyphs unhinted; FreeType's default hinting snaps their outlines to the pixel grid.
105
120
  "config.freetype_load_flags = 'NO_HINTING'",
106
121
  // Box and block characters are the face's own glyphs, as CoreText draws them, not WezTerm's drawn lines.
107
122
  'config.custom_block_glyphs = false',
108
123
  `config.font_size = ${profile?.font?.size ?? TERMINAL_BASIC.fontSize}`,
109
- `config.cell_width = ${profile?.widthSpacing ?? 1}`,
110
- `config.line_height = ${Number(((profile?.heightSpacing ?? 1) * rowScale).toFixed(4))}`,
124
+ `config.cell_width = ${Number(cellWidth.toFixed(4))}`,
125
+ `config.line_height = ${Number(((profile?.heightSpacing ?? 1) * lineHeight).toFixed(4))}`,
111
126
  `config.default_cursor_style = ${lua(`${profile?.cursorBlink ? 'Blinking' : 'Steady'}${cursorShape}`)}`,
112
127
  `config.send_composed_key_when_left_alt_is_pressed = ${composed}`,
113
128
  `config.send_composed_key_when_right_alt_is_pressed = ${composed}`,
@@ -119,8 +134,8 @@ export function weztermConfig(profile, { fontDirs = existsSync(TERMINAL_FONTS) ?
119
134
  `config.macos_window_background_blur = ${opacity < 1 ? Math.round((profile?.blur ?? 0) * 40) : 0}`,
120
135
  `config.initial_cols = ${profile?.columns ?? 80}`,
121
136
  `config.initial_rows = ${profile?.rows ?? 24}`,
122
- // Terminal.app's margin around the text is not measured; this padding is an estimate.
123
- 'config.window_padding = { left = 4, right = 4, top = 2, bottom = 2 }',
137
+ // Terminal.app's text starts 11 pt from the window's edge (22 px at 2x, one point of it the glyph's own bearing).
138
+ "config.window_padding = { left = '10pt', right = '10pt', top = 2, bottom = 2 }",
124
139
  'config.hide_tab_bar_if_only_one_tab = true',
125
140
  'config.enable_scroll_bar = false',
126
141
  // Terminal.app does not ask before closing a window whose process is tmux (its default "ask before closing" list).