@deepseek-insight/dsi 0.6.16 → 0.6.18

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 (82) hide show
  1. package/README.md +2 -2
  2. package/bin/dsi.mjs +77 -21
  3. package/bin/lib/dev-mode.mjs +18 -1
  4. package/bin/lib/plugin-chain.mjs +97 -0
  5. package/build/client/_app/immutable/assets/2.C7lNCQCV.css +1 -0
  6. package/build/client/_app/immutable/chunks/{DOkUpqkd.js → Bqotzgnw.js} +1 -1
  7. package/build/client/_app/immutable/chunks/{BXGojodM.js → CtUqZStl.js} +2 -2
  8. package/build/client/_app/immutable/chunks/D-YP16Kb.js +1 -0
  9. package/build/client/_app/immutable/chunks/D21lpWN6.js +1 -0
  10. package/build/client/_app/immutable/entry/{app.CfbFUHxw.js → app.CGvLdbvX.js} +2 -2
  11. package/build/client/_app/immutable/entry/start.-NibTCMf.js +1 -0
  12. package/build/client/_app/immutable/nodes/{1.C2BfYC2f.js → 1.CiNEHoEh.js} +1 -1
  13. package/build/client/_app/immutable/nodes/{2.UdTbIvwR.js → 2.5NpWuIn7.js} +219 -219
  14. package/build/client/_app/version.json +1 -1
  15. package/build/handler.js +4 -4
  16. package/build/index.js +4 -4
  17. package/build/server/chunks/chunks/{dsh-connection.js-DvncrWM4.js → dsh-connection.js-DL5qjlq2.js} +211 -6
  18. package/build/server/chunks/chunks/{dsh-events.js-B_E1BU9F.js → dsh-events.js-35seDLwL.js} +3 -1
  19. package/build/server/chunks/chunks/{engine.js-1gYqtdBP.js → engine.js-EfjwaLzA.js} +2 -2
  20. package/build/server/chunks/chunks/{git-probe.js-S9w9gGXE.js → git-probe.js-D4DjBXSY.js} +3 -3
  21. package/build/server/chunks/chunks/{internal2.js-DMu68nYR.js → internal2.js-C4ukUWRE.js} +2 -2
  22. package/build/server/chunks/chunks/{restart-chain.js-D-VwGWE2.js → restart-chain.js-KZO2ISmM.js} +2 -2
  23. package/build/server/chunks/chunks/{state.js-CBD6RE0B.js → state.js-OSxIx7MQ.js} +2 -2
  24. package/build/server/chunks/chunks/{watcher.js-BfZOp5td.js → watcher.js-CACduBB9.js} +3 -3
  25. package/build/server/chunks/entries/endpoints/api/a2a/{_server.ts.js-D9vpMh8K.js → _server.ts.js-DHFHuGSm.js} +4 -4
  26. package/build/server/chunks/entries/endpoints/api/a2a/register/{_server.ts.js-HPJEmfbv.js → _server.ts.js-CUUfDBcc.js} +4 -4
  27. package/build/server/chunks/entries/endpoints/api/dsh/directory/{_server.ts.js-BaAmRXrU.js → _server.ts.js-Cae4x1c7.js} +2 -2
  28. package/build/server/chunks/entries/endpoints/api/dsh/jobs/_server.ts.js-hGmoSsDv.js +93 -0
  29. package/build/server/chunks/entries/endpoints/api/dsh/pick-directory/{_server.ts.js-D-Ftq5ei.js → _server.ts.js-BLb8o1cY.js} +2 -2
  30. package/build/server/chunks/entries/endpoints/api/dsh/presets/{_server.ts.js-C0_j53OW.js → _server.ts.js-BFeDKrPz.js} +2 -2
  31. package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/attachment/{_server.ts.js-CxMvUVdS.js → _server.ts.js-DmSTTfI5.js} +2 -2
  32. package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/cancel/{_server.ts.js-CfjRtKMp.js → _server.ts.js-Cus9XrC9.js} +2 -2
  33. package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/catalog/{_server.ts.js-D7uC5b_j.js → _server.ts.js-DcrNU0_y.js} +2 -2
  34. package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/changes/{_server.ts.js-TeoUfrIl.js → _server.ts.js-CpuEcp6p.js} +2 -2
  35. package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/command/{_server.ts.js-uqUb-EdM.js → _server.ts.js-CU9o8Tho.js} +2 -2
  36. package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/events/{_server.ts.js-C_DF0fAc.js → _server.ts.js-BiAJcPpb.js} +3 -3
  37. package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/fork/{_server.ts.js-BfkqDQXU.js → _server.ts.js-BMMZlvr7.js} +2 -2
  38. package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/goal/{_server.ts.js-vCfxCACS.js → _server.ts.js-DZ6qFrfm.js} +2 -2
  39. package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/history/{_server.ts.js-DJlpW4hl.js → _server.ts.js-ecbtat_C.js} +3 -3
  40. package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/models/{_server.ts.js-Cqn5ZMGh.js → _server.ts.js-CPZuvN-_.js} +2 -2
  41. package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/permission/{_server.ts.js-ChB_0ZdM.js → _server.ts.js-Dcfp7gSm.js} +2 -2
  42. package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/prompt/{_server.ts.js-BVHypcWF.js → _server.ts.js-CmxqUwLT.js} +2 -2
  43. package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/rename/{_server.ts.js-BwDVfVrR.js → _server.ts.js-DCNKXRwy.js} +2 -2
  44. package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/respond/{_server.ts.js-DRmd83Ea.js → _server.ts.js-AURyR2ll.js} +2 -2
  45. package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/select-model/{_server.ts.js-hc61Nb64.js → _server.ts.js-BVF36vTO.js} +2 -2
  46. package/build/server/chunks/entries/endpoints/api/dsh/sessions/{_server.ts.js-xkp4IT7y.js → _server.ts.js-LX3JZbRr.js} +2 -2
  47. package/build/server/chunks/entries/endpoints/api/dsh/workspace-file/{_server.ts.js-BvPKBiTg.js → _server.ts.js-BJ0d199o.js} +2 -2
  48. package/build/server/chunks/entries/endpoints/api/dsh/workspace-tree/{_server.ts.js-BSsF01K6.js → _server.ts.js-BDy9jQXv.js} +2 -2
  49. package/build/server/chunks/entries/endpoints/api/dsh/workspaces/{_server.ts.js-DNLxFs1R.js → _server.ts.js-DoTpSXIc.js} +2 -2
  50. package/build/server/chunks/entries/endpoints/api/dsh/workspaces/_workspaceId_/delete/{_server.ts.js-5jaRPaKn.js → _server.ts.js-BnfIobo7.js} +2 -2
  51. package/build/server/chunks/entries/endpoints/api/dsh/workspaces/_workspaceId_/rename/{_server.ts.js-A-hvBqyy.js → _server.ts.js-KRyjIODB.js} +2 -2
  52. package/build/server/chunks/entries/endpoints/api/floor/restart/{_server.ts.js-D5wnUph-.js → _server.ts.js-DKsahdJH.js} +2 -2
  53. package/build/server/chunks/entries/endpoints/api/plugins/apply/{_server.ts.js-CCs56XtH.js → _server.ts.js-D-m-ci1C.js} +3 -3
  54. package/build/server/chunks/entries/endpoints/api/plugins/host-bounce/_server.ts.js-ELTA8Cv_.js +49 -0
  55. package/build/server/chunks/entries/endpoints/api/plugins/reload/{_server.ts.js-Ct28d7_t.js → _server.ts.js-BwingoJE.js} +2 -2
  56. package/build/server/chunks/entries/endpoints/api/plugins/snapshot/{_server.ts.js-DswGIO4S.js → _server.ts.js-50wKeeCi.js} +2 -2
  57. package/build/server/chunks/entries/endpoints/api/workspace/file-write/{_server.ts.js-C0x68iMD.js → _server.ts.js-Cb7X2G82.js} +4 -4
  58. package/build/server/chunks/entries/endpoints/api/workspace/git-events/{_server.ts.js-DzdGKZdV.js → _server.ts.js-DZ67v729.js} +4 -4
  59. package/build/server/chunks/entries/endpoints/api/workspace/git-file-head/{_server.ts.js-BC1jb3zy.js → _server.ts.js-BuO5Stmm.js} +4 -4
  60. package/build/server/chunks/entries/endpoints/api/workspace/git-map/{_server.ts.js-BE9XJyhN.js → _server.ts.js-Bb_I5Ze7.js} +4 -4
  61. package/build/server/chunks/entries/endpoints/api/workspace/git-status/{_server.ts.js-CWlGtOu8.js → _server.ts.js-cHWxh5VB.js} +4 -4
  62. package/build/server/chunks/entries/endpoints/api/workspace/write-gate/{_server.ts.js-CgcggNrH.js → _server.ts.js-Blcfn0zm.js} +4 -4
  63. package/build/server/chunks/entries/pages/{_error.svelte.js-CKrYUsNh.js → _error.svelte.js-2wYgv54Z.js} +3 -3
  64. package/build/server/chunks/entries/pages/{_page.server.ts.js-CbzURHkB.js → _page.server.ts.js-C_ydvzuE.js} +3 -3
  65. package/build/server/chunks/entries/pages/{_page.svelte.js-Bub68nnw.js → _page.svelte.js-BvcQOPvs.js} +472 -7
  66. package/build/server/chunks/{handler-29vKRsIm.js → handler-D0h_ogA5.js} +3 -3
  67. package/build/server/chunks/{index.js-BpxR9RqE.js → index.js-C43GYBhl.js} +2 -2
  68. package/build/server/chunks/{manifest.js-BUA2pFJz.js → manifest.js-DN0QODc_.js} +54 -40
  69. package/build/server/chunks/nodes/1.js-Da_l90xT.js +9 -0
  70. package/build/server/chunks/nodes/{2.js-B3UD9xCC.js → 2.js-_OZDbZ-E.js} +7 -7
  71. package/package.json +4 -4
  72. package/skills/dsi-add-plugin/references/allowbuilds-template.md +1 -1
  73. package/skills/dsi-sync/SKILL.md +8 -8
  74. package/template/.dsi/prompts.sqlite-shm +0 -0
  75. package/template/.dsi/prompts.sqlite-wal +0 -0
  76. package/template/.dsi/resources/plugins-reff.md +7 -6
  77. package/template/.dsi/resources/skills-reff.md +1 -1
  78. package/build/client/_app/immutable/assets/2.yPxdQ-n0.css +0 -1
  79. package/build/client/_app/immutable/chunks/C0ZiNuOj.js +0 -1
  80. package/build/client/_app/immutable/chunks/b58grRSF.js +0 -1
  81. package/build/client/_app/immutable/entry/start.4M41VX7w.js +0 -1
  82. package/build/server/chunks/nodes/1.js-BFDsDe_G.js +0 -9
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # DeepSeek Insight
2
2
 
3
- > [DeepSeek Insight](https://github.com/mitmplay/deepseek-insight) (**DSI**) **See your multiple agents run on the floor (agent, sub-agent, etc)**, each **conversation on the floor are updated instantly**.
3
+ > [DeepSeek Insight](https://github.com/mitmplay/dsh-insight) (**DSI**) **See your multiple agents run on the floor (agent, sub-agent, etc)**, each **conversation on the floor are updated instantly**.
4
4
 
5
5
  > A `Control room` built on [Deepseek Harness's](https://github.com/deepseek-ai/deepseek-harness) (**DSH**) **records**: any (running) session, show side by side on the floor; **Lineage tree** of **parent to sub-agents**; Clean conversation with **tool-call(s) chips** with status **+** auto-expand **thingking chips**.
6
6
 
@@ -43,7 +43,7 @@ The workspace explorer is DSI's window into the folder your agents are working i
43
43
 
44
44
  <details>
45
45
  <summary>Deepseek-insight-panels</summary>
46
- <img src="https://raw.githubusercontent.com/mitmplay/deepseek-insight/refs/heads/main/docs/images/dsi-panels.png"/>
46
+ <img src="https://raw.githubusercontent.com/mitmplay/dsh-insight/refs/heads/main/docs/images/dsi-panels.png"/>
47
47
  </details>
48
48
 
49
49
  ---
package/bin/dsi.mjs CHANGED
@@ -28,7 +28,7 @@ import { fileURLToPath } from 'node:url';
28
28
  import { parse, stringify } from 'yaml';
29
29
 
30
30
  import { mirrorSkills } from './lib/mirror-skills.mjs';
31
- import { dshLauncherCmd, parsePluginArgs, takeoverFloorPorts } from './lib/plugin-chain.mjs';
31
+ import { dshLauncherCmd, parsePluginArgs, takeoverFloorPorts, hostBounceDecision, floorUiPort, parseDevFloorPids, launchUrl, tokenFromSettings } from './lib/plugin-chain.mjs';
32
32
  import { parseDevArg, devModeEnv, modeFromBoolean, resolveBounceMode, extractInspectArgs, parseInspectArgsFromEnv } from './lib/dev-mode.mjs';
33
33
 
34
34
  // Layman-facing help: wrapped near 76 columns, continuation lines indented,
@@ -260,18 +260,53 @@ if (command === 'dsh') {
260
260
  // records the mode records the inspector flags — re-forward them so
261
261
  // the bounce boot comes back debuggable when it left that way.
262
262
  const bounceExecArgv = parseInspectArgsFromEnv(floorEnvDump);
263
- killOrphans();
264
- // The pkill sweep cannot see every floor shape (RCA 2026-09-27: the
265
- // npx shims — 'npm exec @deepseek-ai/dsh…' and '.bin/dsh web' — are
266
- // invisible to pkill -f on macOS, so the dsh host survived holding
267
- // 3080 and preflightPort exited 64, killing the detached chain
268
- // silently). The bounce chain owns the DSI ports by contract, so
269
- // free them by listener, not by name.
270
- await freeFloorPorts(floorPortSet());
271
- // Restore the SAME UI mode the floor was serving. The bounce's
272
- // boolean signal normalizes to 'pair'/'build' inside runSyncedPair;
273
- // 3.5 teaches the bounce the full enum via DSI_DEV_MODE.
274
- runSyncedPair([], mode, bounceExecArgv);
263
+ // ADR-0025 D1/D3 — the bounce fork: an in-process floor with a live
264
+ // bounce verb bounces AT THE HOST via the running floor's route; the
265
+ // vite process never dies and the sweep is SKIPPED. Pair/build floors,
266
+ // a missing verb, or an unreachable route take the sweep below.
267
+ const sweepAndReserve = async () => {
268
+ killOrphans();
269
+ // The pkill sweep cannot see every floor shape (RCA 2026-09-27: the
270
+ // npx shims — 'npm exec @deepseek-ai/dsh…' and '.bin/dsh web' — are
271
+ // invisible to pkill -f on macOS, so the dsh host survived holding
272
+ // 3080 and preflightPort exited 64, killing the detached chain
273
+ // silently). The bounce chain owns the DSI ports by contract, so
274
+ // free them by listener, not by name.
275
+ await freeFloorPorts(floorPortSet());
276
+ // Restore the SAME UI mode the floor was serving. The bounce's
277
+ // boolean signal normalizes to 'pair'/'build' inside runSyncedPair;
278
+ // 3.5 teaches the bounce the full enum via DSI_DEV_MODE.
279
+ runSyncedPair([], mode, bounceExecArgv);
280
+ };
281
+ const bounceVerdict = await hostBounceDecision({
282
+ mode,
283
+ post: async () => {
284
+ // ADR-0025 Wave 3 gate finding (2026-10-14): the route is a DSI
285
+ // (vite) route - POST the vite UI port, not the DSH host port.
286
+ const uiPort = floorUiPort({
287
+ floorEnv: floorEnvDump,
288
+ command: floorCommand(devFloorPid())
289
+ });
290
+ const res = await fetch('http://127.0.0.1:' + uiPort + '/api/plugins/host-bounce', { method: 'POST' });
291
+ return res.ok;
292
+ },
293
+ sweep: sweepAndReserve,
294
+ });
295
+ if (bounceVerdict === 'host') {
296
+ console.log('dsi dsh: host-level bounce done via the running floor — vite never died, sweep skipped (ADR-0025)');
297
+ // D4 amended (2026-10-14): the DSH launch token is minted inside the
298
+ // host process (dsh-client-connection processLaunchToken, a WeakMap on
299
+ // the cordis root — no preset seam), so the bounce CANNOT re-boot with
300
+ // the old token. The bounce's announce intercept lands the NEW token in
301
+ // settings.yaml; hand the operator a URL that works on first click.
302
+ await new Promise((r) => setTimeout(r, 1500));
303
+ const fresh = tokenFromSettings(readFileSync(dsiConfigPath(), 'utf-8'));
304
+ const url = launchUrl({ port: process.env.DSH_HOST_PORT ?? 3080, token: fresh });
305
+ if (url) console.log('dsi dsh: fresh launch URL after the bounce (loaded pages keep working — durable cookie):\n\t ' + url);
306
+ } else {
307
+ console.log('dsi dsh: host bounce unavailable — whole-floor sweep fallback (ADR-0025 D3)');
308
+ await sweepAndReserve();
309
+ }
275
310
  } else if (flag === '--sync-kills') {
276
311
  killOrphans();
277
312
  process.exit(0);
@@ -521,21 +556,42 @@ async function freeFloorPorts(ports) {
521
556
 
522
557
  /** True when a vite dev server of THIS checkout is alive. Only the
523
558
  * checkout's own vite matches — a foreign project's dev server never
524
- * carries the deepseek-insight path segment. pgrep sees this process
525
- * (it does not rewrite argv the way the npm/npx shims do), so the probe
526
- * is reliable where the orphan sweep's name matching is not. */
559
+ * carries the deepseek-insight path segment. ADR-0025 Wave 3 (2026-10-14):
560
+ * the probe rides devFloorPid's ps -eo discovery — pgrep -f went blind to
561
+ * the vite process on the live floor, so this boolean read false-negative
562
+ * and the bounce chain swept floors it could have host-bounced. */
527
563
  function isDevFloorAlive() {
528
- const probe = spawnSync('pgrep', ['-f', 'deepseek-insight/node_modules/vite/bin/vite.js'], { stdio: 'pipe' });
529
- return (probe.status ?? 1) === 0;
564
+ return devFloorPid() !== null;
530
565
  }
531
566
 
532
567
  /** The alive dev floor's vite pid (first match), or null. Same match rule
533
568
  * as isDevFloorAlive — the pid feeds the D6 mode read (ps -E). */
534
569
  function devFloorPid() {
535
- const probe = spawnSync('pgrep', ['-f', 'deepseek-insight/node_modules/vite/bin/vite.js'], { stdio: 'pipe' });
570
+
571
+ // ADR-0025 Wave 3 gate finding (2026-10-14): pgrep -f went BLIND to the
572
+ // vite process on the live floor (exit 1 while ps listed it), so the
573
+ // chain misread the floor as absent and swept a bounceable in-process
574
+ // floor. ps -eo pid,command sees what pgrep does not; the parse is the
575
+ // pure parseDevFloorPids twin in plugin-chain.mjs.
576
+ const probe = spawnSync('ps', ['-eo', 'pid=,command='], { stdio: 'pipe', timeout: 5000 });
536
577
  if ((probe.status ?? 1) !== 0) return null;
537
- const first = (probe.stdout?.toString() || '').split('\n').find(Boolean);
538
- return first ? first.trim() : null;
578
+ const first = parseDevFloorPids(probe.stdout?.toString() || '')[0];
579
+ return first ?? null;
580
+ }
581
+
582
+ /** The floor process's command line (ps -o command), or '' on any failure -
583
+ * feeds floorUiPort's --port arm (the launcher passes the vite port as an
584
+ * argument, not as env). */
585
+ function floorCommand(pid) {
586
+ if (!pid) return '';
587
+ try {
588
+ const ps = spawnSync('ps', ['-o', 'command', '-p', String(pid)], { stdio: 'pipe', timeout: 5000 });
589
+ if (ps.status !== 0) return '';
590
+ const lines = (ps.stdout?.toString() || '').split('\n').filter(Boolean);
591
+ return lines.length > 1 ? lines.slice(1).join('\n') : '';
592
+ } catch {
593
+ return '';
594
+ }
539
595
  }
540
596
 
541
597
  /** The floor's DSI_DEV_MODE via ps -E (macOS: own processes only). Returns
@@ -24,6 +24,7 @@ const DEV_VALUES = new Set(['in-process', 'pair']);
24
24
  */
25
25
  export function parseDevArg(rest) {
26
26
  const out = [];
27
+ /** @type {'build' | 'pair' | 'in-process'} */
27
28
  let mode = 'build';
28
29
  for (let i = 0; i < rest.length; i++) {
29
30
  const arg = rest[i];
@@ -48,10 +49,23 @@ export function parseDevArg(rest) {
48
49
  * DSI_DEV=1 is the pre-existing dev-floor marker (RCA 2026-09-27) — kept for
49
50
  * the bounce chain's boolean probe; DSI_DEV_MODE is the D6 mode enum.
50
51
  */
52
+ /** @param {string} mode
53
+ * @returns {Record<string, string>}
54
+ */
51
55
  export function devModeEnv(mode) {
52
56
  if (mode === 'build') return {};
57
+ /** @type {Record<string, string>} */
53
58
  const env = { DSI_DEV: '1', DSI_DEV_MODE: mode };
54
- if (mode === 'in-process') env.DSI_DEV_IN_PROCESS = '1';
59
+ if (mode === 'in-process') {
60
+ env.DSI_DEV_IN_PROCESS = '1';
61
+ // ADR-0025 Wave 3 (real-floor gate finding, 2026-10-14): the bounce
62
+ // verb only exists when the embed OWNS the host - a runCli boot has
63
+ // no stop handle and the host-bounce route answers 409 no-handle,
64
+ // sending every in-process bounce down the whole-floor sweep. The
65
+ // introspection boot (DSH_HOST_INTROSPECT=1, ADR-0024) is the boot
66
+ // that carries the handle, so in-process floors default to it.
67
+ env.DSH_HOST_INTROSPECT = '1';
68
+ }
55
69
  return env;
56
70
  }
57
71
 
@@ -60,6 +74,9 @@ export function devModeEnv(mode) {
60
74
  * to a mode. Wave 3.5 will teach the bounce the enum; until then the bounce
61
75
  * can only ever restore 'pair' or 'build'.
62
76
  */
77
+ /** @param {boolean} dev
78
+ * @returns {'pair' | 'build'}
79
+ */
63
80
  export function modeFromBoolean(dev) {
64
81
  return dev ? 'pair' : 'build';
65
82
  }
@@ -77,6 +77,103 @@ export function bounceDevMode(flagDev, devFloorAlive) {
77
77
  export const TAKEOVER_BUDGET_MS = 10_000;
78
78
  export const TAKEOVER_POLL_MS = 250;
79
79
 
80
+ /** The host-level bounce's retry window (ADR-0025 D3): the chain POSTs the
81
+ * running floor's /api/plugins/host-bounce at most this many times before
82
+ * giving up and taking the whole-floor sweep fallback. */
83
+ export const HOST_BOUNCE_TRIES = 2;
84
+ export const HOST_BOUNCE_RETRY_MS = 1_000;
85
+
86
+ /**
87
+ * The bounce fork (ADR-0025 D1/D3): an in-process floor with a live bounce
88
+ * verb bounces AT THE HOST — the chain POSTs the route and, on ok, sweeps
89
+ * NOTHING (vite never dies). Any other mode, refusal, or unreachable route
90
+ * falls back to today's whole-floor sweep. Pure orchestration: the POST and
91
+ * the sweep are injected, so unit tests pin BOTH branches.
92
+ *
93
+ * @param {{ mode: string, post: () => Promise<boolean>, sweep: () => void | Promise<void>,
94
+ * sleep?: (ms: number) => Promise<void>, tries?: number }} params
95
+ * @returns {Promise<'host' | 'sweep'>} which body ran
96
+ */
97
+ export async function hostBounceDecision({ mode, post, sweep, sleep = (ms) => new Promise((r) => setTimeout(r, ms)), tries = HOST_BOUNCE_TRIES }) {
98
+ if (mode !== 'in-process') {
99
+ await sweep();
100
+ return 'sweep';
101
+ }
102
+ for (let attempt = 1; attempt <= tries; attempt++) {
103
+ try {
104
+ if ((await post()) === true) return 'host';
105
+ } catch {
106
+ // unreachable or refused — retry, then sweep
107
+ }
108
+ if (attempt < tries) await sleep(HOST_BOUNCE_RETRY_MS);
109
+ }
110
+ await sweep();
111
+ return 'sweep';
112
+ }
113
+
114
+ /** The browser launch URL after a bounce (ADR-0025 D4, amended
115
+ * 2026-10-14): the DSH launch token is minted INSIDE the host process
116
+ * (WeakMap keyed on the cordis root, dsh-client-connection processLaunchToken)
117
+ * and has no preset seam, so an in-place bounce cannot re-boot with the OLD
118
+ * token. The equivalent guarantee: after the bounce the chain reads the NEW
119
+ * token from the sink and hands the operator a URL that works on first
120
+ * click. Pure: port and token are injected.
121
+ *
122
+ * @param {{ port?: number|string, token?: string, host?: string }} p
123
+ * @returns {string} the launch URL ('' when the token is unknown)
124
+ */
125
+ export function launchUrl({ port = 3080, token = '', host = '127.0.0.1' } = {}) {
126
+ if (!token) return '';
127
+ return 'http://' + host + ':' + port + '/?token=' + encodeURIComponent(token);
128
+ }
129
+
130
+ /** Reads dsh.authToken out of the DSI settings.yaml text. Pure: the file
131
+ * text is injected; dsi.mjs reads the file.
132
+ *
133
+ * @param {string} yamlText - full settings.yaml text
134
+ * @returns {string} the token ('' when absent or not parseable)
135
+ */
136
+ export function tokenFromSettings(yamlText) {
137
+ const m = /authToken:\s*['"]?([^"'/\s]+)/.exec(String(yamlText || ''));
138
+ return m ? m[1] : '';
139
+ }
140
+
141
+ /** The dev floor's vite pid(s) parsed from ONE ps -eo pid,command dump
142
+ * (ADR-0025 Wave 3 gate finding, 2026-10-14): pgrep -f proved blind to the
143
+ * vite process on the live floor (exit 1 while ps listed it), which made
144
+ * the bounce chain misread the mode and sweep a bounceable floor. ps -eo
145
+ * sees what pgrep does not, and parsing is pure + unit-pinned here; the
146
+ * ps invocation stays in dsi.mjs.
147
+ *
148
+ * @param {string} psOutput - full `ps -eo pid=,command=` dump
149
+ * @returns {string[]} matching pids (as strings), oldest-first order kept
150
+ */
151
+ export function parseDevFloorPids(psOutput) {
152
+ return String(psOutput || '').split('\n').flatMap((line) => {
153
+ const m = /^\s*(\d+)\s+(.*)$/.exec(line);
154
+ return m && m[2].includes('node_modules/vite/bin/vite.js') ? [m[1]] : [];
155
+ });
156
+ }
157
+
158
+ /** The port the floor's DSI UI answers on (ADR-0025 Wave 3 gate finding,
159
+ * 2026-10-14): the host-bounce route is a DSI (vite) route, so the bounce
160
+ * chain must POST the VITE port - the DSH host port only knows the fence
161
+ * (401) or 404. Resolution order: PORT= in the floor env dump, then
162
+ * --port N on the vite command line, then the 5175 default the launcher
163
+ * pins. Pure: both inputs are strings read by dsi.mjs (ps -E and ps -o
164
+ * command), so unit tests pin all three arms.
165
+ *
166
+ * @param {{ floorEnv?: string, command?: string }} params
167
+ * @returns {number} the UI port
168
+ */
169
+ export function floorUiPort({ floorEnv = '', command = '' } = {}) {
170
+ const envPort = /(?:^|\s)PORT=(\d+)/.exec(floorEnv || '');
171
+ if (envPort) return Number(envPort[1]);
172
+ const argPort = /--port\s+(\d+)/.exec(command || '');
173
+ if (argPort) return Number(argPort[1]);
174
+ return 5175;
175
+ }
176
+
80
177
  /**
81
178
  * The takeover sweep (The Port Takeover ADR 2026-10-12, D2/D3/D5): SIGTERM
82
179
  * whatever LISTENS on the given ports, then WAIT until each port frees,