synabun 2.0.0-beta → 2.0.0

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.
Files changed (70) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/THIRD-PARTY-LICENSES.md +5 -0
  3. package/launcher.mjs +40 -0
  4. package/lib/claude-hooks.js +61 -8
  5. package/lib/client-config-repair.js +4 -3
  6. package/lib/data-home-migration.js +4 -0
  7. package/lib/desktop-pwa.js +206 -0
  8. package/lib/external-tools.js +221 -0
  9. package/lib/first-launch-protection.js +5 -0
  10. package/lib/packaged-runtime.js +110 -0
  11. package/lib/start-launcher.js +1309 -0
  12. package/neural-interface/i18n/en.json +41 -2
  13. package/neural-interface/i18n/pt-BR.json +41 -2
  14. package/neural-interface/lib/assistant-envelope.js +9 -6
  15. package/neural-interface/lib/augmented-path.js +23 -3
  16. package/neural-interface/lib/browser-language.js +83 -0
  17. package/neural-interface/lib/claude-agent-bridge.js +54 -147
  18. package/neural-interface/lib/claude-executable.js +241 -0
  19. package/neural-interface/lib/claude-model-catalog.js +52 -91
  20. package/neural-interface/lib/native-binary-runtime.js +69 -337
  21. package/neural-interface/lib/native-loop-providers.js +30 -44
  22. package/neural-interface/lib/offline-page.js +54 -0
  23. package/neural-interface/lib/session-title-generator.js +5 -0
  24. package/neural-interface/lib/synabun-update-plan.js +19 -0
  25. package/neural-interface/package-lock.json +0 -251
  26. package/neural-interface/public/install-app.html +31 -0
  27. package/neural-interface/public/manifest.json +2 -0
  28. package/neural-interface/public/offline.html +401 -66
  29. package/neural-interface/public/onboarding.html +252 -146
  30. package/neural-interface/public/shared/assistant/asst-mascot.js +12 -10
  31. package/neural-interface/public/shared/assistant/asst-rack-styles.js +0 -3
  32. package/neural-interface/public/shared/assistant/asst-render.js +58 -49
  33. package/neural-interface/public/shared/assistant/asst-styles.js +4 -2
  34. package/neural-interface/public/shared/command-runner-model.js +30 -0
  35. package/neural-interface/public/shared/html-shell.js +9 -5
  36. package/neural-interface/public/shared/pwa-install-page.js +34 -0
  37. package/neural-interface/public/shared/pwa-install.css +12 -0
  38. package/neural-interface/public/shared/pwa-install.js +28 -0
  39. package/neural-interface/public/shared/settings/settings-rules.js +160 -0
  40. package/neural-interface/public/shared/start-bridge.js +583 -0
  41. package/neural-interface/public/shared/styleguide/sg-colors.js +5 -2
  42. package/neural-interface/public/shared/styleguide/sg-import-export.js +4 -3
  43. package/neural-interface/public/shared/styleguide/sg-kit.js +4 -1
  44. package/neural-interface/public/shared/styleguide/sg-panel.js +9 -1
  45. package/neural-interface/public/shared/styleguide/sg-typography.js +1 -0
  46. package/neural-interface/public/shared/styles.css +801 -392
  47. package/neural-interface/public/shared/term-ready.js +307 -0
  48. package/neural-interface/public/shared/ui-claude-panel.js +3 -3
  49. package/neural-interface/public/shared/ui-command-runner.js +251 -468
  50. package/neural-interface/public/shared/ui-judgments.js +1 -1
  51. package/neural-interface/public/shared/ui-loading.js +221 -10
  52. package/neural-interface/public/shared/ui-menubar.js +11 -27
  53. package/neural-interface/public/shared/ui-settings.js +36 -59
  54. package/neural-interface/public/shared/ui-terminal.js +130 -144
  55. package/neural-interface/public/sw.js +3 -1
  56. package/neural-interface/public/variant/2d/main.js +2 -1
  57. package/neural-interface/public/variant/3d/main.js +2 -1
  58. package/neural-interface/scripts/rebuild-pty.js +15 -12
  59. package/neural-interface/server.js +220 -185
  60. package/package.json +13 -1
  61. package/postinstall.js +11 -81
  62. package/setup.js +72 -4
  63. package/docs/browser-v2.md +0 -109
  64. package/docs/hook-integrity.md +0 -89
  65. package/docs/judgments.md +0 -182
  66. package/docs/style-guide.md +0 -246
  67. package/scripts/audit-claude-hook-usage.mjs +0 -12
  68. package/scripts/audit-memory-duplicates.mjs +0 -44
  69. package/scripts/measure-claude-hook-overhead.mjs +0 -77
  70. package/scripts/recover-user-config.mjs +0 -254
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synabun",
3
- "version": "2.0.0-beta",
3
+ "version": "2.0.0",
4
4
  "description": "Local-first persistent vector memory, MCP tooling, and multi-agent workflows for AI assistants",
5
5
  "license": "Apache-2.0",
6
6
  "author": "The SynaBun Authors",
@@ -19,6 +19,7 @@
19
19
  "postinstall.js",
20
20
  "preuninstall.js",
21
21
  "updater.mjs",
22
+ "launcher.mjs",
22
23
  "updater.cmd",
23
24
  "updater.sh",
24
25
  "NOTICE",
@@ -56,6 +57,10 @@
56
57
  "hooks/claude-code/task-gate.mjs",
57
58
  "skills/",
58
59
  "lib/paths.js",
60
+ "lib/start-launcher.js",
61
+ "lib/packaged-runtime.js",
62
+ "lib/desktop-pwa.js",
63
+ "lib/external-tools.js",
59
64
  "lib/paths.d.ts",
60
65
  "lib/data-home-migration.js",
61
66
  "lib/first-launch-protection.js",
@@ -87,6 +92,13 @@
87
92
  "test:mcp": "npm --prefix mcp-server test",
88
93
  "test:neural": "npm --prefix neural-interface test",
89
94
  "pack:check": "npm pack --dry-run --json --ignore-scripts",
95
+ "build:targets": "node packaging/build.mjs targets",
96
+ "build:mac:arm": "node packaging/build.mjs build --target macos-arm64",
97
+ "build:mac:intel": "node packaging/build.mjs build --target macos-x64",
98
+ "build:linux:x64": "node packaging/build.mjs build --target linux-x64",
99
+ "build:windows:x64": "node packaging/build.mjs build --target windows-x64",
100
+ "build:windows:x86": "node packaging/build.mjs build --target windows-x86",
101
+ "test:packaging": "node --test packaging/tests/*.test.mjs",
90
102
  "postinstall": "node postinstall.js",
91
103
  "preuninstall": "node preuninstall.js",
92
104
  "prepublishOnly": "npm --prefix mcp-server ci --ignore-scripts && npm --prefix neural-interface ci --ignore-scripts && npm run mcp:build && npm test && node -e \"const fs = require('node:fs'); for (const path of ['mcp-server/dist/index.js', 'NOTICE', 'THIRD-PARTY-LICENSES.md']) fs.accessSync(path)\" && npm run pack:check"
package/postinstall.js CHANGED
@@ -9,10 +9,11 @@
9
9
  */
10
10
 
11
11
  import { execSync } from 'node:child_process';
12
- import { existsSync, mkdirSync, writeFileSync, chmodSync } from 'node:fs';
12
+ import { existsSync, chmodSync } from 'node:fs';
13
13
  import { resolve, dirname } from 'node:path';
14
14
  import { fileURLToPath } from 'node:url';
15
- import { homedir, platform } from 'node:os';
15
+ import { platform } from 'node:os';
16
+ import { registerStartLauncher } from './lib/start-launcher.js';
16
17
 
17
18
  const __dirname = dirname(fileURLToPath(import.meta.url));
18
19
 
@@ -114,87 +115,16 @@ function buildMcpServer() {
114
115
  }
115
116
 
116
117
  // ── Protocol handler registration ──
118
+ //
119
+ // The synabun:// handler behind the offline page's Start Server button. The
120
+ // same registration runs on every start (setup.js), so this only makes the
121
+ // button work before the first one.
117
122
 
118
123
  function registerProtocolHandler() {
119
- const nodeBin = process.execPath;
120
- const setupJs = resolve(__dirname, 'setup.js');
121
- const plat = platform();
122
-
123
- if (plat === 'win32') {
124
- // Windows — HKCU registry (no admin required)
125
- try {
126
- const cmd = `"${nodeBin}" "${setupJs}" "%1"`;
127
- execSync(`reg add "HKCU\\Software\\Classes\\synabun" /ve /d "URL:SynaBun Protocol" /f`, { stdio: 'pipe' });
128
- execSync(`reg add "HKCU\\Software\\Classes\\synabun" /v "URL Protocol" /d "" /f`, { stdio: 'pipe' });
129
- execSync(`reg add "HKCU\\Software\\Classes\\synabun\\shell\\open\\command" /ve /d "${cmd}" /f`, { stdio: 'pipe' });
130
- ok('Registered synabun:// protocol handler');
131
- } catch {
132
- warn('Could not register synabun:// protocol handler');
133
- }
134
- } else if (plat === 'darwin') {
135
- // macOS — minimal .app bundle in ~/.synabun/
136
- try {
137
- const appDir = resolve(homedir(), '.synabun', 'SynaBun.app', 'Contents');
138
- const macosDir = resolve(appDir, 'MacOS');
139
- mkdirSync(macosDir, { recursive: true });
140
-
141
- writeFileSync(resolve(appDir, 'Info.plist'), [
142
- '<?xml version="1.0" encoding="UTF-8"?>',
143
- '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
144
- '<plist version="1.0">',
145
- '<dict>',
146
- ' <key>CFBundleIdentifier</key><string>ai.synabun.launcher</string>',
147
- ' <key>CFBundleName</key><string>SynaBun</string>',
148
- ' <key>CFBundleExecutable</key><string>synabun-launcher</string>',
149
- ' <key>CFBundleVersion</key><string>1.0</string>',
150
- ' <key>CFBundleURLTypes</key>',
151
- ' <array><dict>',
152
- ' <key>CFBundleURLName</key><string>SynaBun Protocol</string>',
153
- ' <key>CFBundleURLSchemes</key><array><string>synabun</string></array>',
154
- ' </dict></array>',
155
- '</dict>',
156
- '</plist>',
157
- ].join('\n'));
158
-
159
- const launcherPath = resolve(macosDir, 'synabun-launcher');
160
- writeFileSync(launcherPath, `#!/bin/sh\nexec "${nodeBin}" "${setupJs}" "$@"\n`);
161
- chmodSync(launcherPath, 0o755);
162
-
163
- // Register with LaunchServices
164
- try {
165
- execSync(`/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister -R "${resolve(homedir(), '.synabun', 'SynaBun.app')}"`, { stdio: 'pipe' });
166
- } catch {
167
- try { execSync(`open "${resolve(homedir(), '.synabun', 'SynaBun.app')}"`, { stdio: 'pipe' }); } catch {}
168
- }
169
-
170
- ok('Registered synabun:// protocol handler');
171
- } catch {
172
- warn('Could not register synabun:// protocol handler');
173
- }
174
- } else {
175
- // Linux — .desktop file
176
- try {
177
- const appsDir = resolve(homedir(), '.local', 'share', 'applications');
178
- mkdirSync(appsDir, { recursive: true });
179
-
180
- writeFileSync(resolve(appsDir, 'synabun.desktop'), [
181
- '[Desktop Entry]',
182
- 'Type=Application',
183
- 'Name=SynaBun',
184
- `Exec="${nodeBin}" "${setupJs}" %u`,
185
- 'MimeType=x-scheme-handler/synabun;',
186
- 'NoDisplay=true',
187
- 'Terminal=true',
188
- '',
189
- ].join('\n'));
190
-
191
- try { execSync('xdg-mime default synabun.desktop x-scheme-handler/synabun', { stdio: 'pipe' }); } catch {}
192
-
193
- ok('Registered synabun:// protocol handler');
194
- } catch {
195
- warn('Could not register synabun:// protocol handler');
196
- }
197
- }
124
+ const result = registerStartLauncher({ packageRoot: __dirname });
125
+ if (result.state === 'skipped') return;
126
+ if (result.ok) ok('Registered synabun:// protocol handler');
127
+ else warn(`Could not register synabun:// protocol handler (${result.errors[0]})`);
198
128
  }
199
129
 
200
130
  // ── Updater shim executable bit ──
package/setup.js CHANGED
@@ -44,6 +44,8 @@ import {
44
44
  protectFirstLaunchAfterUpdate,
45
45
  recordLauncherFailure,
46
46
  } from './lib/first-launch-protection.js';
47
+ import { noteSupervisorFailure, openSupervisorRecord, registerStartLauncher } from './lib/start-launcher.js';
48
+ import { packagedRuntime } from './lib/packaged-runtime.js';
47
49
 
48
50
  const __dirname = dirname(fileURLToPath(import.meta.url));
49
51
 
@@ -76,6 +78,14 @@ const DATA_HOME = REQUESTED_DATA_HOME_DIAGNOSTICS.dataHomeInsidePackage
76
78
  process.env.SYNABUN_DATA_HOME = DATA_HOME;
77
79
  process.env.MEMORY_DATA_DIR = resolve(DATA_HOME, 'mcp-data');
78
80
 
81
+ // What this start is doing, written where the Start Server launcher reads it
82
+ // (DATA_HOME/data/supervisor.json): the offline page that asked for the start
83
+ // has no server to ask, and shows these phases instead of a guess. Each phase
84
+ // is noted when it begins. Opened in main(), once the data home is settled.
85
+ let supervisor = null;
86
+ const startPhase = (name, detail) => { try { supervisor?.phase(name, detail); } catch {} };
87
+ const startFailed = (code, message, extra = {}) => { try { supervisor?.fail({ code, message, ...extra }); } catch {} };
88
+
79
89
  // ── Migration: move legacy in-repository state to the platform data home ──
80
90
 
81
91
  function looksLikeSynabunCheckout(root) {
@@ -235,6 +245,16 @@ function handleDoctorCommand() {
235
245
  console.log('');
236
246
  }
237
247
 
248
+ // The offline page's Start Server button hands the OS a synabun:// link. Keep
249
+ // the handler pointing at this install: a GitHub checkout never ran the npm
250
+ // postinstall, and node or the package may have moved since it did.
251
+ function ensureStartLauncher() {
252
+ const result = registerStartLauncher({ packageRoot: PACKAGE_ROOT });
253
+ if (result.state === 'skipped') return;
254
+ if (!result.ok) warn(`Could not register the Start Server launcher: ${result.errors[0]}`);
255
+ else if (result.changed) ok('Start Server launcher registered (synabun://)');
256
+ }
257
+
238
258
  function repairClientConfigs() {
239
259
  try {
240
260
  const result = auditAndRepairClientConfigs({ dataHome: DATA_HOME, packageRoot: PACKAGE_ROOT, apply: true });
@@ -279,6 +299,7 @@ function installDeps(name, dir, { includeDev = false } = {}) {
279
299
  }
280
300
 
281
301
  info(`Installing ${name} dependencies...`);
302
+ startPhase('dependencies', { name });
282
303
  try {
283
304
  const omitFlag = includeDev ? '' : ' --omit=dev';
284
305
  execSync(`npm install${omitFlag} --ignore-scripts`, {
@@ -290,6 +311,7 @@ function installDeps(name, dir, { includeDev = false } = {}) {
290
311
  } catch (err) {
291
312
  fail(`Failed to install ${name} dependencies`);
292
313
  console.error(err.stderr?.toString() || err.message);
314
+ startFailed('DEPENDENCIES_FAILED', `npm could not install the ${name} dependencies`);
293
315
  process.exit(1);
294
316
  }
295
317
  }
@@ -309,6 +331,7 @@ function installPlaywrightChromium() {
309
331
  } catch { /* not installed */ }
310
332
 
311
333
  info('Installing Playwright Chromium (for browser automation)...');
334
+ startPhase('browser');
312
335
  try {
313
336
  execSync('npx playwright install chromium', {
314
337
  cwd: niDir,
@@ -335,10 +358,12 @@ function buildMcpServer() {
335
358
  }
336
359
 
337
360
  info('Building MCP server from source...');
361
+ startPhase('build');
338
362
  try {
339
363
  execSync('npx tsc', {
340
364
  cwd: resolve(PACKAGE_ROOT, 'mcp-server'),
341
365
  stdio: ['ignore', 'pipe', 'pipe'],
366
+ windowsHide: !!packagedRuntime(),
342
367
  timeout: 120_000,
343
368
  });
344
369
  ok('MCP server built');
@@ -450,9 +475,10 @@ function startServer() {
450
475
  const CRASH_LIMIT = 5;
451
476
 
452
477
  function spawnOnce() {
453
- const child = spawn('node', ['--disable-warning=ExperimentalWarning', '--max-old-space-size=6144', serverPath], {
478
+ const child = spawn(process.execPath, ['--disable-warning=ExperimentalWarning', '--max-old-space-size=6144', serverPath], {
454
479
  cwd: niDir,
455
480
  stdio: ['ignore', 'pipe', 'pipe'],
481
+ windowsHide: !!packagedRuntime(),
456
482
  env: {
457
483
  ...process.env,
458
484
  SYNABUN_DATA_HOME: DATA_HOME,
@@ -461,17 +487,30 @@ function startServer() {
461
487
  },
462
488
  });
463
489
  currentChild = child;
490
+ startPhase('server', { pid: child.pid });
491
+ child.on('error', (error) => {
492
+ fail(`Could not start the server: ${error.message}`);
493
+ startFailed('SERVER_SPAWN_FAILED', `the server could not be started: ${error.message}`);
494
+ process.exit(1);
495
+ });
464
496
 
465
497
  let opened = !firstLaunch;
498
+ let announced = false;
466
499
  child.stdout.on('data', (data) => {
467
500
  const s = data.toString();
468
501
  process.stdout.write(s);
469
502
  try { outLog?.write(s); } catch {}
503
+ // The server prints its address from its listen callback: it is up.
504
+ if (!announced && s.includes('Server:')) {
505
+ announced = true;
506
+ startPhase('listening', { port: Number(s.match(/Server:\s+http:\/\/localhost:(\d+)/)?.[1]) || undefined });
507
+ }
470
508
  if (!opened && s.includes('Server:')) {
471
509
  opened = true;
472
510
  const port = s.match(/Server:\s+http:\/\/localhost:(\d+)/)?.[1] || '3344';
473
511
  const url = `http://localhost:${port}${setupComplete ? '/' : '/onboarding.html'}`;
474
- setTimeout(() => openBrowser(url), 1500);
512
+ // The Start Server button already has a tab waiting for the server.
513
+ if (process.env.SYNABUN_OPEN_BROWSER !== '0') setTimeout(() => openBrowser(url), 1500);
475
514
  }
476
515
  });
477
516
  child.stderr.on('data', (data) => {
@@ -501,6 +540,7 @@ function startServer() {
501
540
  while (restartLog.length && now - restartLog[0] > CRASH_WINDOW_MS) restartLog.shift();
502
541
  if (restartLog.length > CRASH_LIMIT) {
503
542
  fail(`Server restarted ${restartLog.length} times in ${Math.round(CRASH_WINDOW_MS / 1000)}s — aborting supervisor.`);
543
+ startFailed('RESTART_LOOP', `the server restarted ${restartLog.length} times in ${Math.round(CRASH_WINDOW_MS / 1000)} s`);
504
544
  process.exit(1);
505
545
  }
506
546
  firstLaunch = false;
@@ -510,6 +550,7 @@ function startServer() {
510
550
 
511
551
  if (code !== 0 && code !== null) {
512
552
  fail(`Server exited with code ${code}`);
553
+ startFailed('SERVER_EXITED', `the server exited with code ${code}`, { exitCode: code });
513
554
  process.exit(code);
514
555
  }
515
556
  process.exit(0);
@@ -740,32 +781,55 @@ async function main() {
740
781
  // Inspect and migrate before creating empty destination directories. A
741
782
  // divergent pair stops normal launch without overwriting either root.
742
783
  const preflight = preflightDataHome();
743
- if (preflight.status === 'conflict') process.exit(2);
784
+ if (preflight.status === 'conflict') {
785
+ noteSupervisorFailure(DATA_HOME, {
786
+ code: 'DATA_HOME_CONFLICT',
787
+ message: 'SynaBun found different user state in two locations and stopped before changing either. Run "synabun doctor".',
788
+ exitCode: 2,
789
+ });
790
+ process.exit(2);
791
+ }
744
792
 
745
793
  // Ensure data directories
746
794
  ensureDataDirs(DATA_HOME);
747
795
 
796
+ // From here until this process exits a start is under way: the Start Server
797
+ // launcher reads this record so a second click never starts a second one,
798
+ // and passes what it says on to the page that is waiting.
799
+ supervisor = openSupervisorRecord(DATA_HOME);
800
+ process.on('exit', supervisor.release);
801
+
748
802
  // Prerequisites
749
803
  checkNodeVersion();
750
804
  console.log('');
751
805
 
752
806
  // Dependencies (installed in global package location)
807
+ // A slow step names itself when it begins (startPhase inside each); between
808
+ // them the record goes back to 'checking', so it never names a finished one.
753
809
  installDeps('Neural Interface', resolve(PACKAGE_ROOT, 'neural-interface'));
754
810
  installDeps('MCP Server', resolve(PACKAGE_ROOT, 'mcp-server'), { includeDev: needsBuild() });
811
+ startPhase('checking');
755
812
  console.log('');
756
813
 
757
814
  // Playwright browser
758
815
  installPlaywrightChromium();
816
+ startPhase('checking');
759
817
  console.log('');
760
818
 
761
819
  // Build
762
820
  buildMcpServer();
821
+ startPhase('checking');
763
822
  console.log('');
764
823
 
765
824
  // A new version never runs on user state that has no verified snapshot. When
766
825
  // the snapshot cannot be created the launch stops here, before the server
767
826
  // (and any data migration) starts. There is no flag to launch anyway.
768
- const protection = await protectFirstLaunchAfterUpdate({ dataHome: DATA_HOME, version, log: { info, ok, warn } });
827
+ const protection = await protectFirstLaunchAfterUpdate({
828
+ dataHome: DATA_HOME,
829
+ version,
830
+ log: { info, ok, warn },
831
+ onProgress: (detail) => startPhase('snapshot', { version, ...detail }),
832
+ });
769
833
  if (protection.status === 'failed') {
770
834
  const failure = describeSnapshotFailure({ ...protection, dataHome: DATA_HOME, version });
771
835
  // Server logging has not begun, and a detached launch (the updater's
@@ -776,13 +840,16 @@ async function main() {
776
840
  for (const line of failure.lines) info(line);
777
841
  if (logPath) info(`This message is also in: ${logPath}`);
778
842
  console.log('');
843
+ startFailed('SNAPSHOT_FAILED', failure.headline, { exitCode: SNAPSHOT_FAILED_EXIT_CODE });
779
844
  process.exit(SNAPSHOT_FAILED_EXIT_CODE);
780
845
  }
846
+ startPhase('checking');
781
847
  console.log('');
782
848
 
783
849
  // Existing registrations can carry checkout-local DOTENV_PATH and data-home
784
850
  // overrides. Rewrite only their SynaBun entry after the new MCP build exists.
785
851
  repairClientConfigs();
852
+ ensureStartLauncher();
786
853
  console.log('');
787
854
 
788
855
  // State
@@ -800,5 +867,6 @@ async function main() {
800
867
  main().catch(error => {
801
868
  fail(error.message);
802
869
  recordLauncherFailure({ dataHome: DATA_HOME, exitCode: 1, headline: `SynaBun did not start: ${error.message}` });
870
+ startFailed('SETUP_FAILED', error.message);
803
871
  process.exit(1);
804
872
  });
@@ -1,109 +0,0 @@
1
- # Browser execution and efficiency
2
-
3
- SynaBun still uses its configured browser and the existing MCP tools. This change fixes action correctness and enables the faster browser path by default in version **2.0.0**, for general browsing and social automations.
4
-
5
- ## Rollout
6
-
7
- Correctness fixes apply after the updated Neural Interface and MCP processes restart: compatible delivered snapshot baselines, cancellation/deadline handling, ref-aware publishing guards, caller/tab ownership checks, and protection against selectors changing targets during pacing.
8
-
9
- Performance changes are **on by default**; no opt-in variable is needed. After the updated Neural Interface and MCP processes restart, this enables bounded automatic snapshot capture, cached session routing, server-side paged extraction, omission of non-writing think pauses, and registration of `browser_batch`. Leave existing `SYNABUN_BROWSER_FAST`, `SYNABUN_BROWSER_COMPACT` and `SYNABUN_BROWSER_AUTOSNAPSHOT` settings as they are.
10
-
11
- For rollback, set `SYNABUN_BROWSER_V2=0` in the environment used by **both Neural Interface and its MCP processes**, then restart them normally. Remove that override to return to the new default. Correctness fixes remain enabled. No database migration or saved automation rewrite is required. This implementation does not change the running service's environment or restart active automations.
12
-
13
- Social engagement writes retain the existing randomized 30–90-second spacing, serialized within each browser session. It applies to the resolved target whether the caller uses refs or selectors. A blocked preflight does not consume a write slot; an attempted write with an uncertain outcome does. Typing cadence is preserved.
14
-
15
- ## Interfaces and workflow
16
-
17
- - `browser_snapshot` accepts optional `baselineId` and returns a compact snapshot ID. A diff requires a compatible delivered observation from the same caller, page generation, scope, depth settings and budget. Removals are explicit. `force:true` requests a full view. Aborted requests never establish a baseline.
18
- - Navigation, click and scroll observations use the same budget at capture and formatting. Click defaults to 6,000 characters; navigation and scroll use 12,000; explicit snapshots retain 30,000. Truncation ends at complete lines. V2 automatic captures start at depth 12 and make at most one shallower retry; callers can override depth or scope to inspect deeper controls.
19
- - Playwright refs identify elements in its latest captured scope. A scoped snapshot can remove previously observed refs from that map. Re-snapshot when a ref is stale; do not assume every previously returned ref remains usable after another capture.
20
- - Paged `browser_extract_*` tools accept optional `fields` and `maxChars`. Identity deduplication occurs before projection; repeated identities update their fields. Results report stopping reasons and partial failures. Known loading states must finish before newly discovered items are accepted. FB group classification occurs before field projection.
21
- - `browser_content` accepts `selector` for one element's subtree with plain text or Markdown. Existing offsets and budgets remain supported. Missing/ambiguous scopes return an error. Relative source links are made absolute, and extraction does not modify the live DOM.
22
- - `browser_batch` accepts 1–10 `{tool,args,observe?}` steps, one `sessionId`/`tabId`, and final `snapshot:full|diff|none` (default `full`). It invokes the existing guarded tool handlers sequentially, validates every step before acting, and returns completed outcomes if a later step fails. Steps cannot change routing or use arbitrary evaluation.
23
-
24
- ### MoreLogin as the default browser
25
-
26
- While Browser Settings name MoreLogin as the default (`connectMode: "morelogin"`), every loop, schedule, dispatched run, recovery and interactive acquisition drives the default MoreLogin profile (`moreloginEnvId`). Reuse and a caller-requested session count only when the session is MoreLogin on that exact env. If MoreLogin cannot be reached after the auto-launch wait, the run fails and its log, `lastRunResult` and the `schedule:failed` toast carry the reason (`Could not start the shared browser session: MoreLogin connect failed: …`). Nothing falls back to Chrome.
27
-
28
- The server decides which session may carry pages. `GET /api/browser/sessions` reports `servesDefault` per session and `defaultBrowser` for the list: with MoreLogin as the default only a MoreLogin session on the default env serves it; in managed and real-Chrome setups every session does, so nothing changes there. The MCP requires `servesDefault` for every page tool (navigations, localhost included, platform tools, screenshots, snapshots, content, console, clicks, typing, batches) and a run's pinned browser. A pinned session that does not serve the default fails the call with the reason. Navigations and platform tools drop an explicit id that does not serve it, then resolve the default browser; any other page tool refuses it (`Browser session <id> is not the default browser (…), so SynaBun's browser tools do not act on it`), since the page it names is in the other browser. The server holds the same line at its REST boundary: an agent request (one that carries `X-Synabun-Terminal`, as every MCP call does) on a session that does not serve the default is refused with 409 `NOT_DEFAULT_BROWSER` before anything runs, for any page action, a new tab or CDP; closing the session or a tab and listing its tabs stay allowed, and the Neural Interface's own UI (no header) keeps working with any session. Every `/api/browser` response names the default browser in `X-Synabun-Browser-Default` (`morelogin:<env>`, `other:<mode>`, or `unreadable`), and the MCP keys its route caches on it, so a change of connect mode or MoreLogin env drops every cached route; the in-process MCP is told the moment the settings are saved, and a `NOT_DEFAULT_BROWSER` refusal also forgets the caller's cached route and tab. Affinity, auto-selection and adoption skip it. When Browser V2 recovery of a cached route fails, the action returns the recovery's own reason (for example `MoreLogin connect failed: …`) with `recoveryFailed: true`, not a bare "Session not found".
29
-
30
- `bluesky_*` tools renew an expired access token themselves. They wait for bsky.app to refresh it (a freshly initialized tab is still booting), then reload the assigned bsky.app tab at most once. "Not logged in" is reported only when the page holds no session. A token the server still rejects gets one renewal and one re-run, and only when nothing was written yet. The whole call has one 48 s deadline, under the roughly 60 s tool limit of OpenCode and Codex. Finding and opening the tab, the token checks, the reload and image uploads must end 12 s before it, so the request itself always has time. A step that runs out of time returns a clear error. A request that doesn't answer in time is reported as possibly finished (check before repeating) and is never retried.
31
-
32
- #### Localhost in MoreLogin
33
-
34
- Localhost pages open in the MoreLogin profile like any other page; nothing reroutes them to another browser. MoreLogin's per-profile **Port scan protection** decides which local ports load: a port listed under Edit profile → Advanced settings → Port scan protection (allowed ports, read when the profile starts) loads normally, and any other local port is redirected to `getip.morelogin.com/black_whiteList_stop_page_v2.html?blockedUrl=<url>`. Keep protection on and list the dev-server ports (P-1 allows 3344, 3100, 3210, 3917, 9003 and 9017), then restart the profile. A blocked navigation, back, forward or click fails with `moreLoginBlocked: true` and the stop page's URL, and, for a local port, the error is `MoreLogin blocked localhost:<port>. Most likely the profile's Port scan protection: add <port> to its Advanced settings → Port scan protection allowed ports in MoreLogin. If a URL Access Restriction in MoreLogin's Security Center blocks localhost, remove localhost from that block policy. Then restart the profile.` (other hosts keep the website-whitelist message). A session created at a blocked URL is still created and says so (`moreLoginBlocked`, `notice`). `/api/morelogin/status` counts observed blocks (`blockedNavigations`, `lastBlockedUrl`). The earlier loopback sidecar (system Google Chrome on the leftover `userDataDir`), its page-level handoff and `LOOPBACK_ONLY_SESSION` are gone.
35
-
36
- #### Screenshots for visual QA
37
-
38
- `browser_screenshot` (`GET /api/browser/sessions/:id/screenshot-base64`, `lib/browser-screenshot.js`) returns the tab as an image, localhost included. Without options it is the viewport as JPEG (quality 60), downscaled to `maxWidth` (default 1024 px of real pixels; `0` = native resolution). Options:
39
-
40
- - `width` / `height` (100–4096 CSS px): the viewport for this capture only, as a CDP `Emulation.setDeviceMetricsOverride` (`deviceScaleFactor: 1`, `mobile` below 768 px) sent from a CDP session opened for the capture. Chromium keeps one override per page and a detach alone leaves it in place, so it is always cleared with `Emulation.clearDeviceMetricsOverride`, on errors and timeouts too, and then confirmed: the tab's `innerWidth` and `devicePixelRatio` must equal what they were before the capture. A clear that fails, hangs (2 s) or does not take is sent once more from a fresh CDP session, which first sets an empty override (0 width, height and scale), because Chromium ignores a clear from a session that never set one. A tab still not confirmed is closed (its session's other tabs stay; the only tab's page is closed and the next call opens a fresh one) and the result says so (`tabClosed: true` and a `notice`, or the notice appended to the error), so a MoreLogin tab never keeps a window size, DPR or mobile flag its profile does not have.
41
- - `fullPage`: the whole document (`captureBeyondViewport`, up to 16384 px; longer pages report `truncated`).
42
- - `format` (`png` | `jpeg`): the returned image is a JPEG unless PNG is asked for; a saved file is a PNG unless JPEG is asked for (saved JPEG quality 90).
43
- - `save` / `path`: also write the full-resolution image and return `savedPath`. The default file is `~/.synabun/data/media/screenshots/<yyyy-mm-dd>/<host>-<width>x<height>-<hhmmss>.png` (never an existing file). An explicit `path` must be absolute, end in .png, .jpg or .jpeg, and lie inside a registered project (`claude-code-projects.json`, never one at or above the home folder) or the screenshots folder, resolved through symlinks; parent folders are created, and only an existing PNG or JPEG is ever replaced. The file is checked again as it is written (a capture can take up to a minute): the parent's real path must still lie inside those folders, a symlink or a non-image at the name is refused, the bytes go to a temp file created `O_CREAT | O_EXCL | O_NOFOLLOW` in that real folder, the folder's real path is read again, and a rename puts it in place (a rename replaces a symlink, it never follows one). `savedPath` is the real path written.
44
-
45
- The returned image never exceeds 7900 px on a side. Its size is measured from a small capture of the same CDP session, not taken from the page's `devicePixelRatio` (a fingerprint value in MoreLogin). The MCP client waits the long timeout (30 s) for it. A saving screenshot is not a read-only call for plan mode and remote read-only sessions (`lib/assistant-plan-permissions.js`).
46
-
47
- #### Console and page errors
48
-
49
- `browser_console` (`GET /api/browser/sessions/:id/console?tabId=&level=&since=&limit=&clear=`, `lib/browser-console.js`) reads a per-page ring buffer of the last 200 console messages (`{ kind: "console", level, type, text, location, at }`, network errors such as a 404 script included) and uncaught page errors (`{ kind: "pageerror", level: "error", message, stack, at }`). The buffer is attached before the first navigation of every tab SynaBun registers (a new session, MoreLogin attach included, a new tab, a recovered tab) and follows their popups; attaching twice is a no-op. A popup is not a tab of its session, so its entries come back with the tab that opened it, each with `popup: <the popup's URL at the time>` and in the order they happened (the last 5 popups of a tab, popups of popups too; `popups` counts them, and `clear` empties them with the tab). The MCP shows them as `[popup <url>]`. `level` (`all` | `error` | `warning` | `info` | `log`) keeps that level and the more severe ones; `since` (ISO or epoch ms, e.g. the `latest` of the previous read) keeps newer entries; `limit` (1–200, default 50) returns the newest matches; `clear` empties the buffer after reading. The console arguments' JS handles are released as each message arrives, so a chatty page pins nothing. A tab registered before the buffer existed starts recording on its first read (`attached: false`).
50
-
51
- #### Browser settings file
52
-
53
- `browser-config.json` is written atomically (a temp file renamed over it). `loadBrowserConfig()` keeps the text of the last good read or write: when the file is torn, unparsable or not a JSON object it returns that config (logging a warning once per failure streak, then at most once a minute) instead of `{}`, which used to switch MoreLogin mode off for whatever read it at that moment. A missing file is `{}` (the defaults). A file that exists but has never been read well (no earlier good config) is not treated as unconfigured: `loadBrowserConfig()` returns `{}` marked unreadable, `createBrowserSession` refuses with `browser-config.json is unreadable; fix it in Settings → Browser (<reason>)` instead of taking the Chrome path, `GET /api/browser/config` adds `configError`, and saving the settings in Settings → Browser replaces the file.
54
-
55
- ### Jev assistance (advisory)
56
-
57
- Three TypeSafe surfaces (`browser-page-state`, `browser-target`, `browser-social`; full contract in `docs/judgments.md`) add a typed reading of the page to the tools above. It is text only, bounded, and never part of the control flow: with TypeSafe off, a surface off, a timeout or a rate limit, every tool answers exactly as it did before.
58
-
59
- - `browser_navigate` accepts optional `intent`. One line may follow the snapshot: `Jev assessment: authentication_required (94%) — pause for human sign-in`. A blocker (`authentication_required`, `verification_required`, `consent_blocker`) means stop and hand over to the human; `loading` means wait and inspect again; a consent dialog is never dismissed automatically. With an `intent`, the line also says whether the page already satisfies it.
60
- - `browser_snapshot` accepts optional `intent` in AI mode and may append `Jev target: ref e12 "Account settings" (choice 91%, match 96%); alternatives: e15, e31`, or `no candidate qualified`. The refs come from the same capture that was delivered, so the recommendation is a fresh ref. It never executes.
61
- - A failed or ambiguous `browser_click`, `browser_fill`, `browser_type`, `browser_hover`, `browser_select` or `browser_upload` (the last two now accept `textHint`) returns its usual hints, reordered and annotated with each candidate's risk class when Jev found a confident match. Shadow mode sends no second request: the caller decides and acts with a new call.
62
- - An empty social extractor keeps its message verbatim as the first line and appends a diagnosis: genuinely empty, still loading, wrong surface, sign-in, verification, consent, or a rendered error. Populated output is byte-identical.
63
- - `browser_fb_composer_state` and `browser_x_compose_state` add a `judgment` key only when the probe itself is ambiguous. `recommendedAction`, `boostRisk` and the locale lists stay authoritative; a suggested control is a suggestion, and Boost / Promote controls are removed before Jev sees the options.
64
- - Inside `browser_batch` there is no assistance at all. A batch that navigated gets one `assessment` of its final page after it ends; steps are never added, replaced or replayed.
65
-
66
- **The one-retry invariant.** Safe auto-heal is off and locked until the browser benchmark passes for the exact running configuration (Settings → AI decisions → Browser assistance). When armed it permits exactly one retry, only for `browser_click`, only when the selector matched nothing or failed to parse, and only onto a plain same-origin navigation link that the server re-verifies on the element it is about to click. It never follows `actionStarted:true`, an uncertain outcome, a transport error, a cancellation or a guard; it never types, submits, publishes, likes, follows, pays, uploads or changes a toggle; a refused, failed or uncertain heal is final. A healed click says so in its result, `(auto-healed via Jev to "Pricing": choice 93%, match 97%; selector … matched nothing)`, and the route's body carries `autoHealed:true`.
67
-
68
- Page text is adversarial data. It reaches Jev only as `state`, never inside an instruction, and a control whose label addresses the agent disqualifies the whole page from healing.
69
-
70
- Use extraction for data and scoped snapshots for interaction. Choose fields needed by the task instead of returning avatars/media URLs by default. Use a batch only when the selectors and sequence are already known; otherwise inspect between actions. A local form example, after observing its selectors:
71
-
72
- ```json
73
- {
74
- "steps": [
75
- {"tool":"browser_fill","args":{"selector":"#email","value":"fixture@example.invalid"}},
76
- {"tool":"browser_click","args":{"selector":"#save"}}
77
- ]
78
- }
79
- ```
80
-
81
- Tool names inside batches are the unprefixed names shown above. Call the batch itself using the exact host-provided MCP name. Host cancellation stops pending HTTP calls and prevents subsequent batch steps. Never replay a whole batch or an uncertain click; inspect the current state first.
82
-
83
- ## Metrics and measurements
84
-
85
- Set `SYNABUN_BROWSER_METRICS=1` on the same processes for diagnostic JSON on stderr. Client and server correlate with `X-Synabun-Request-Id` and share an absolute `X-Synabun-Deadline`. Records contain timings, operation names, response sizes and estimated tokens, without URLs, page text, selectors, scripts, form values or credentials. Intentional pacing is reported separately from processing. Metrics are disabled by default.
86
-
87
- The local benchmark behind these numbers uses isolated Chromium fixtures, no user profiles or external requests, alternates variant order, and runs at least 20 samples per variant for articles, search, dynamic forms, feeds, composers and virtualized extraction. Its report holds cold startup and per-scenario median/p95 processing time, required-data assertions, captures, response bytes and text-token estimates: [browser-v2-results.json](../benchmarks/browser-v2-results.json).
88
-
89
- The final measured run reduced the sum of scenario median processing times by about **78%**, and estimated output text tokens by **23%**. The largest change was virtualized extraction (about 1.8 seconds to 0.2 seconds for three items). Dense snapshots went from four captures to two. All required fixture data was retained. Optional field projection accounts for the measured output-token saving; it is not an automatic saving on every call.
90
-
91
- Deferred full-profile server instructions use concise capability summaries instead of repeating the entire tool catalog: **5,289 → 3,219 characters**, approximately **39% less instruction text** (~1,323 → ~805 estimated tokens). Legacy profiled-host instructions remain identical. This separate instruction measurement is not included in the 23% response-token result, and does not measure provider billing or how frequently a particular host injects instructions.
92
-
93
- These are component measurements, not full MCP/provider automation measurements. HTTP call counts in this fixture report are derived from the implementation, not network instrumentation. Deliberate social write delays and model reasoning time are excluded. Estimates are not provider-billed tokens. The provisional 30% output-token target remains an optimization goal rather than a release blocker. The measured processing and instruction reductions, full automated suites, and read-only real-page checks support the default rollout with an explicit rollback switch. Live checks exercise the updated algorithms through an adapter to the configured SynaBun browser; changed route wiring and mutating actions are verified separately using local Express/Chromium fixtures. They do not establish real-site publishing success, and no live posting is used for validation.
94
-
95
- ## Validation
96
-
97
- The figures below are from the validation run of this change in September 2026. The suites that ship with 2.0.0 are larger.
98
-
99
- - Complete Node suite: **520 passed, 0 failed, 1 Windows-only skip**; includes four live Claude bridge scenarios.
100
- - Complete MCP suite: **130 passed, 0 failed**, including compiled Codex discovery of `browser_batch` with no opt-in.
101
- - All five local browser/UI suites: **19 passed**, including memory maintenance and sidepanel windows.
102
- - The MCP test command invokes Vitest through Node, so a launcher file that is not executable does not break it.
103
- - Real-page checks use a new test tab for each site, scoped snapshots/content and projected extraction without posting. Results and their transport limitations are recorded in [browser-live-results.json](../benchmarks/browser-live-results.json).
104
-
105
-
106
- - Node tests exercise cancellation, actual write spacing, selector ambiguity/identity, snapshot delivery and diffs, extraction completeness, browser recovery and policy contracts.
107
- - MCP Vitest tests exercise routing isolation, one safe pre-execution recovery, no ambiguous replay, cancellation, batch validation/partial outcomes, extraction projection and snapshot formatting.
108
- - Browser-driven suites, which are not part of this package, exercise the installed Playwright/Chromium against local fixtures and real Express route handlers. They cover live refs, deletions, reloads, truncation, background-frame deadlines, foreign-tab rejection, guarded publishing, cancelled clicks, replaced targets and hydration before extraction. macOS sandboxed runners may need permission to launch Chromium and bind loopback.
109
- - `npm run build` in `mcp-server` compiles the implementation and its generated runtime files.
@@ -1,89 +0,0 @@
1
- # Hook identity, state, and measurement
2
-
3
- SynaBun has 10 executable hook scripts with 11 registrations covering nine Claude Code events. The canonical
4
- definitions live in `lib/claude-hooks.js`. Every settings scope gets the same
5
- handler command, matcher, and timeout. Claude Code can therefore deduplicate
6
- the handler for each event even when both project and global hooks are installed.
7
-
8
- The command resolves a SynaBun checkout from the current project (including
9
- subdirectories), then falls back to the installer-managed `SYNABUN_HOOK_ROOT`.
10
- The tracked template contains no machine-specific root. Server startup repairs
11
- recognized old commands in global settings and registered projects' settings
12
- and local settings. Other handlers, including plugins, retain their own behavior.
13
- Repairing one settings file without the other can temporarily retain duplicate
14
- execution; no event is suppressed by a guessed identity or a marker file.
15
-
16
- ## State integrity
17
-
18
- - `post-plan` and MCP `remember` use the same transactional writer. A plan's
19
- retry key includes its project, canonical source path, and content hash.
20
- Concurrent retries return the same UUID. An edited plan gets a new entry.
21
- - A verified old plan receipt can acquire a retry key without copying its row.
22
- Stop records a retry count, never a successful receipt before storage succeeds.
23
- - Short JSON read–modify–write operations are serialized with SQLite's writer
24
- lock in `data/hook-state-lock.sqlite`. JSON replacement is atomic. The
25
- coordination database contains no memories; process death releases its lock.
26
- - Compaction flags carry a session ID and generation. The matching conversation
27
- remember must carry `source_ref: synabun-compaction:<session>:<generation>`.
28
- Hook instructions supply this value. Legacy generationless flags can only be
29
- cleared by their own session. An old completion cannot clear a new generation.
30
-
31
- Runtime state resides under `SYNABUN_DATA_HOME` (normally `~/.synabun`). Existing
32
- memory rows are not deleted by this repair.
33
-
34
- ## Recall admission
35
-
36
- Hybrid `score` remains a ranking value, preserving compatibility. Retrieval also
37
- exposes `semantic_score`, `passage_score`, `keyword_coverage`, and `fusion_score`.
38
- An absent channel value means that channel was not measured or did not qualify;
39
- it is not a zero or a probability.
40
-
41
- Automatic injection requires document or passage similarity of at least 0.4.
42
- Explicit recall can still retrieve keyword-only evidence such as error codes.
43
- Short referential questions such as “is there a better setting for that?” do not
44
- start another automatic search. Repeatedly supplied evidence produces no new
45
- context. MiniLM and its existing vectors are retained.
46
-
47
- ## Reproducible accounting
48
-
49
- From the repository root:
50
-
51
- ```sh
52
- node scripts/audit-claude-hook-usage.mjs --since 2026-09-14T06:27:59.243Z --until 2026-09-15T06:27:59.243Z --output benchmarks/hook-repair-usage-24h.json
53
- node scripts/audit-memory-duplicates.mjs --output benchmarks/hook-repair-duplicate-candidates.json
54
- ```
55
-
56
- Usage records are deduplicated by API message ID. Uncached input, cache writes,
57
- cache reads, and output remain separate. Payloads retain request provenance,
58
- Unicode character counts, UTF-8 byte counts, image counts, and hashes. The join
59
- to the next recorded request does not prove subsequent retention or billing.
60
- Unknown component token counts are `null`, never character-based estimates.
61
-
62
- The duplicate audit is read-only. Exact hashes and near-duplicate candidates
63
- include UUIDs and metadata for review; they are not deletion instructions.
64
-
65
- For an isolated provider measurement, supply a transcript containing an exact
66
- duplicate SynaBun SessionStart block:
67
-
68
- ```sh
69
- node scripts/measure-claude-hook-overhead.mjs --run --transcript /path/to/session.jsonl --output benchmarks/hook-repair-provider-measurement.json
70
- ```
71
-
72
- This runs four short Claude requests: two reversed-order pairs with identical
73
- system/user text and the recorded context. Only temporary fixture hooks run;
74
- tools and MCP servers are disabled. It also checks native handler multiplicity.
75
-
76
- The September 15, 2026 probe used `claude-sonnet-5` and a 525-character recorded
77
- startup block. Legacy registrations ran SessionStart, UserPromptSubmit, and Stop
78
- twice each; canonical registrations ran them once each. Both pairs measured
79
- **940 versus 748 input tokens: 192 tokens removed per startup fixture**. This is
80
- not an attribution of historical account-wide usage or every hook's token cost.
81
-
82
- ## Watchdogs and rollout
83
-
84
- Both sidepanel engines use `high: 120s`, `xhigh: 300s`, `max: 300s`. Lower efforts
85
- retain the engine's existing default. Legacy retries retain their budget across
86
- respawns.
87
-
88
- After changing MCP code, build with `npm run mcp:build` and restart the Neural
89
- Interface server. A build alone leaves the running server's ESM imports cached.