@deepseek-insight/dsi 0.6.15 → 0.6.17

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 (86) hide show
  1. package/.agents/skills/dsi-ports/SKILL.md +61 -0
  2. package/.agents/skills/dsi-restart/SKILL.md +85 -0
  3. package/bin/dsi.mjs +216 -51
  4. package/bin/lib/dev-mode.mjs +147 -0
  5. package/bin/lib/plugin-chain.mjs +144 -0
  6. package/build/client/_app/immutable/assets/{0.BNydQv_U.css → 0.KhAZv9mf.css} +1 -1
  7. package/build/client/_app/immutable/assets/2.yPxdQ-n0.css +1 -0
  8. package/build/client/_app/immutable/chunks/BwzVWUYp.js +1 -0
  9. package/build/client/_app/immutable/chunks/{Dz22rCMX.js → CBpybevN.js} +2 -2
  10. package/build/client/_app/immutable/chunks/{Cq1i4fPn.js → CuWK-Is9.js} +1 -1
  11. package/build/client/_app/immutable/chunks/gD0YbXxV.js +1 -0
  12. package/build/client/_app/immutable/entry/{app.BzO5CDQp.js → app.rnAj22Bt.js} +2 -2
  13. package/build/client/_app/immutable/entry/start.BZMX9WpT.js +1 -0
  14. package/build/client/_app/immutable/nodes/{1.DUMUcf6V.js → 1.CEJdtANo.js} +1 -1
  15. package/build/client/_app/immutable/nodes/{2.DibXuWjr.js → 2.D-He2bY-.js} +219 -219
  16. package/build/client/_app/version.json +1 -1
  17. package/build/handler.js +4 -4
  18. package/build/index.js +4 -4
  19. package/build/server/chunks/chunks/{dsh-connection.js-DRtqPR_j.js → dsh-connection.js-DvncrWM4.js} +5 -2
  20. package/build/server/chunks/chunks/{git-probe.js-Daf6r7Z9.js → git-probe.js-S9w9gGXE.js} +2 -2
  21. package/build/server/chunks/chunks/{internal2.js-CFvE_2qT.js → internal2.js-Uh9eeZMM.js} +2 -2
  22. package/build/server/chunks/chunks/restart-chain.js-KZO2ISmM.js +112 -0
  23. package/build/server/chunks/chunks/{state.js-BFJMxIOB.js → state.js-5SpXEMQf.js} +2 -2
  24. package/build/server/chunks/chunks/{watcher.js-CoR53lwR.js → watcher.js-BfZOp5td.js} +2 -2
  25. package/build/server/chunks/entries/endpoints/api/a2a/{_server.ts.js-BKlF4XGH.js → _server.ts.js-D9vpMh8K.js} +3 -3
  26. package/build/server/chunks/entries/endpoints/api/a2a/register/{_server.ts.js-BpEhlidV.js → _server.ts.js-HPJEmfbv.js} +3 -3
  27. package/build/server/chunks/entries/endpoints/api/dsh/directory/{_server.ts.js-Dmh7f-hU.js → _server.ts.js-BaAmRXrU.js} +2 -2
  28. package/build/server/chunks/entries/endpoints/api/dsh/pick-directory/{_server.ts.js-BEci0ZAx.js → _server.ts.js-D-Ftq5ei.js} +2 -2
  29. package/build/server/chunks/entries/endpoints/api/dsh/presets/{_server.ts.js-OXFzXBSp.js → _server.ts.js-C0_j53OW.js} +2 -2
  30. package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/attachment/{_server.ts.js-x44GKy4R.js → _server.ts.js-CxMvUVdS.js} +2 -2
  31. package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/cancel/{_server.ts.js-Da5ZqeJ7.js → _server.ts.js-CfjRtKMp.js} +2 -2
  32. package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/catalog/{_server.ts.js-BACv9hW-.js → _server.ts.js-D7uC5b_j.js} +2 -2
  33. package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/changes/{_server.ts.js-BtiUS3iD.js → _server.ts.js-TeoUfrIl.js} +2 -2
  34. package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/command/{_server.ts.js-BOOyd1LI.js → _server.ts.js-uqUb-EdM.js} +2 -2
  35. package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/events/{_server.ts.js-D0GwQbdF.js → _server.ts.js-C_DF0fAc.js} +2 -2
  36. package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/fork/{_server.ts.js-Bd0JYMlf.js → _server.ts.js-BfkqDQXU.js} +2 -2
  37. package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/goal/{_server.ts.js-DqAV7OOv.js → _server.ts.js-vCfxCACS.js} +2 -2
  38. package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/history/{_server.ts.js-Cz4P-68E.js → _server.ts.js-DJlpW4hl.js} +2 -2
  39. package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/models/{_server.ts.js-C4fXXBIM.js → _server.ts.js-Cqn5ZMGh.js} +2 -2
  40. package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/permission/{_server.ts.js-BiqNZiiN.js → _server.ts.js-ChB_0ZdM.js} +2 -2
  41. package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/prompt/{_server.ts.js-Bdx9V4oY.js → _server.ts.js-BVHypcWF.js} +2 -2
  42. package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/rename/{_server.ts.js-CuBqoV3c.js → _server.ts.js-BwDVfVrR.js} +2 -2
  43. package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/respond/{_server.ts.js-CXPFI_Pn.js → _server.ts.js-DRmd83Ea.js} +2 -2
  44. package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/select-model/{_server.ts.js-CRcaw4Yo.js → _server.ts.js-hc61Nb64.js} +2 -2
  45. package/build/server/chunks/entries/endpoints/api/dsh/sessions/{_server.ts.js-dYrtVHDP.js → _server.ts.js-xkp4IT7y.js} +2 -2
  46. package/build/server/chunks/entries/endpoints/api/dsh/workspace-file/{_server.ts.js-BtlfuMjf.js → _server.ts.js-BvPKBiTg.js} +2 -2
  47. package/build/server/chunks/entries/endpoints/api/dsh/workspace-tree/{_server.ts.js-D12kAP9B.js → _server.ts.js-BSsF01K6.js} +2 -2
  48. package/build/server/chunks/entries/endpoints/api/dsh/workspaces/{_server.ts.js-C9JHp1WU.js → _server.ts.js-DNLxFs1R.js} +2 -2
  49. package/build/server/chunks/entries/endpoints/api/dsh/workspaces/_workspaceId_/delete/{_server.ts.js-bLF4Fbgn.js → _server.ts.js-5jaRPaKn.js} +2 -2
  50. package/build/server/chunks/entries/endpoints/api/dsh/workspaces/_workspaceId_/rename/{_server.ts.js-BSw_zD-0.js → _server.ts.js-A-hvBqyy.js} +2 -2
  51. package/build/server/chunks/entries/endpoints/api/floor/mode/_server.ts.js-CzBvebv2.js +25 -0
  52. package/build/server/chunks/entries/endpoints/api/floor/restart/_server.ts.js-DKsahdJH.js +30 -0
  53. package/build/server/chunks/entries/endpoints/api/plugins/apply/{_server.ts.js-BNEUWbaf.js → _server.ts.js-NZ9IorXC.js} +5 -56
  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/workspace/file-write/{_server.ts.js-CqyKA9g2.js → _server.ts.js-C0x68iMD.js} +3 -3
  56. package/build/server/chunks/entries/endpoints/api/workspace/git-events/{_server.ts.js-yu695YoE.js → _server.ts.js-DzdGKZdV.js} +3 -3
  57. package/build/server/chunks/entries/endpoints/api/workspace/git-file-head/{_server.ts.js-BhDAvM6y.js → _server.ts.js-BC1jb3zy.js} +3 -3
  58. package/build/server/chunks/entries/endpoints/api/workspace/git-map/{_server.ts.js-BRUgPrJ2.js → _server.ts.js-BE9XJyhN.js} +3 -3
  59. package/build/server/chunks/entries/endpoints/api/workspace/git-status/{_server.ts.js-C7VJ2G4_.js → _server.ts.js-CWlGtOu8.js} +3 -3
  60. package/build/server/chunks/entries/endpoints/api/workspace/write-gate/{_server.ts.js-BH5BRA_d.js → _server.ts.js-CgcggNrH.js} +3 -3
  61. package/build/server/chunks/entries/pages/{_error.svelte.js-Cz-l6MbQ.js → _error.svelte.js-5nTeAv_j.js} +3 -3
  62. package/build/server/chunks/entries/pages/{_page.server.ts.js-CityLNBO.js → _page.server.ts.js-CbzURHkB.js} +2 -2
  63. package/build/server/chunks/entries/pages/{_page.svelte.js-J7dfR5ON.js → _page.svelte.js-DhwtRb8h.js} +135 -21
  64. package/build/server/chunks/{handler-Ctb3OEZ3.js → handler-Bx0HQRi_.js} +3 -3
  65. package/build/server/chunks/{index.js-DZ5R_e-K.js → index.js-DlqGbs2e.js} +2 -2
  66. package/build/server/chunks/{manifest.js-D3cyG_zP.js → manifest.js-C5Epl_0n.js} +59 -38
  67. package/build/server/chunks/nodes/{0.js-CedNgbP9.js → 0.js-zSkx80Fq.js} +3 -3
  68. package/build/server/chunks/nodes/1.js-BBFJCoA_.js +9 -0
  69. package/build/server/chunks/nodes/{2.js-B5xHvNNG.js → 2.js-rGsgHmCd.js} +6 -6
  70. package/package.json +1 -1
  71. package/template/.dsi/prompts.sqlite +0 -0
  72. package/template/.dsi/prompts.sqlite-shm +0 -0
  73. package/template/.dsi/prompts.sqlite-wal +0 -0
  74. package/template/.dsi/resources/plugins-reff.md +7 -6
  75. package/.agents/skills/dsi-thin/SKILL.md +0 -140
  76. package/.agents/skills/dsi-thin/references/ADR-0019-snapshot.md +0 -363
  77. package/.agents/skills/dsi-thin/references/authority-adr.md +0 -28
  78. package/.agents/skills/dsi-thin/references/input-contract.md +0 -42
  79. package/.agents/skills/dsi-thin/references/proven-gates.md +0 -61
  80. package/.agents/skills/dsi-thin/references/two-lane-contract.md +0 -53
  81. package/build/client/_app/immutable/assets/2.tyWDc_pW.css +0 -1
  82. package/build/client/_app/immutable/chunks/DlCR4rv7.js +0 -1
  83. package/build/client/_app/immutable/chunks/syBMXJiJ.js +0 -1
  84. package/build/client/_app/immutable/entry/start.DPtsd8of.js +0 -1
  85. package/build/server/chunks/nodes/1.js-BJtXg-yj.js +0 -9
  86. /package/build/client/_app/immutable/nodes/{0.DJ0pyst0.js → 0.Jl8y_yU6.js} +0 -0
@@ -0,0 +1,61 @@
1
+ ---
2
+ name: dsi-ports
3
+ description: Fast feedback-loop port inventory — run /dsi-ports (or ask "what's running / which ports are open / what's eating memory") to list every TCP listener with pid, process, memory usage, and its purpose (DSH host, DSI UI, Playwright, inspector, test floors). Backed by the Port Ledger KB.
4
+ ---
5
+
6
+ # dsi-ports — the Port Ledger, live
7
+
8
+ Companion KB: `dev/kb/open-ports/2026-10-12 - The Port Ledger — Who Opens What, Why, and How Test Floors Stay Isolated.md` — read it for WHY each port exists. This skill is the LOOK-path: one command, one table, no judgment.
9
+
10
+ ## Run
11
+
12
+ Execute this single pipeline (macOS; safe, read-only). AGENT HYGIENE: write
13
+ the block to a temp file (`/tmp/dsi-ports.sh`) and run `sh` on it — inlining
14
+ shell `$-expansions` inside a JS template string breaks the tool call
15
+ (observed 2026-10-12); the failure is the caller's, not the pipeline's.
16
+
17
+ ```sh
18
+ for pid_port in $(lsof -nP -iTCP -sTCP:LISTEN | awk 'NR>1 {match($9, /[^:]*$/); print substr($9, RSTART) "/" $2}' | sort -u); do
19
+ port=${pid_port%%/*}; pid=${pid_port##*/}
20
+ rss=$(ps -o rss= -p "$pid" 2>/dev/null | awk '{printf "%.0f MB", $1/1024}')
21
+ cmd=$(ps -o command= -p "$pid" 2>/dev/null | cut -c1-70)
22
+ echo "$port | $pid | ${rss:-?} | $cmd"
23
+ done | sort -n
24
+ ```
25
+
26
+ ## Format the output
27
+
28
+ Present EXACTLY this table (port ascending), then the purpose line:
29
+
30
+ | Port | PID | Process | Memory (RSS) | Purpose |
31
+ |---|---|---|---|---|
32
+ | 3080 | 1234 | node …/vite.js dev | 412 MB | **DSH host** — browser RPC + token announce |
33
+ | 5175 | 1234 | node …/vite.js dev | 388 MB | **DSI dev UI** — the page the operator uses |
34
+
35
+ Purpose lookup (from the Port Ledger KB — first match wins):
36
+
37
+ | Port | Purpose |
38
+ |---|---|
39
+ | 3080 | DSH host — browser RPC, token announce |
40
+ | 5174 | DSI built UI (production page) |
41
+ | 5175 | DSI dev UI (Vite, HMR) |
42
+ | 5176 | Playwright webServer (vite preview, e2e backend) |
43
+ | 9229 | Node inspector (D7 — one debugger for DSI + embedded host) |
44
+ | 1933 | OpenViking memory server |
45
+ | 34xx | test-floor DSH host (isolated) |
46
+ | 51xx (5180+) | test-floor UI (isolated) |
47
+ | anything else | **unknown — name the binary and ask** (never guess a purpose) |
48
+
49
+ ## Reading the table (tell the operator, one line each)
50
+
51
+ - Same PID on TWO ports (e.g. 3080 + 5175) → the **in-process floor** (one process, D7).
52
+ - 5175 up but 3080 down → UI-only floor; the page will show "Failed to fetch".
53
+ - 3080 up but no 5175/5174 → bare host (tandem's other half missing, or runCli host only).
54
+ - Two PIDs on the same port → TWO listeners — list both rows, flag it.
55
+ - Biggest RSS → name it; vite dev floors routinely sit at 300–500 MB (normal).
56
+
57
+ ## Boundaries
58
+
59
+ - Read-only: `lsof`, `ps`, `sort`. Never kill, never open, never edit — remediation is always a separate, confirmed step.
60
+ - IPv6 vs IPv4 duplicates: same port with different PIDs means TWO listeners — list both rows, flag it.
61
+ - The table is a snapshot; ports move between runs. Re-run, don't cache.
@@ -0,0 +1,85 @@
1
+ ---
2
+ name: dsi-restart
3
+ description: Restart the DSI/DSH floor with a before/after report — /dsi-restart snapshots every listener's port+pid+RSS, restarts (or waits for the operator's restart), re-snapshots, and prints the delta table (which ports restarted, memory freed/claimed). Answers "floor feels heavy, restart and show me what changed".
4
+ ---
5
+
6
+ # dsi-restart — restart with receipts
7
+
8
+ Companion: `dsi-ports` skill (the snapshot pipeline) and the Port Ledger KB (`dev/kb/open-ports/`). This skill adds the DELTA: before, restart, after, memory moved.
9
+
10
+ ## 0. Identify how the floor is running (do not skip)
11
+
12
+ **Facts first (2026-10-12, hard rule):** classify launch shape ONLY from a full
13
+ ancestry walk — `ps -o pid,ppid,lstart,command` repeated up the ppid chain
14
+ until you reach pid 1 or an interactive `zsh`/`login`. Record each hop. An
15
+ immediate-parent glance is not detection: the same zsh can own both a dead
16
+ floor's corpse and the replacement the OPERATOR launched by hand (precedent
17
+ 2026-10-12: the assistant claimed a "detached relaunch" that was actually the
18
+ operator re-running the command in their own terminal). **Never narrate a
19
+ relaunch you did not perform and cannot cite a pid + start time for.**
20
+
21
+ | Launch shape | Detection (full ancestry walk) | Who may restart it |
22
+ |---|---|---|
23
+ | Operator's foreground terminal | the `bin/dsi.mjs` chain's top parent is an interactive `zsh`/`login` (walk ppids; lstart proves which launch is which) | **the operator only** — or the Restart Verb with the operator's go (§2) |
24
+ | Detached chain (plugin-rack / verb bounce) | the chain's top parent is pid 1 (launchd) | the skill, via the Restart Verb or `--sync-kills` + relaunch |
25
+
26
+ If detection is ambiguous → **ask the operator** before killing anything (precedent: pid 37084, 2026-10-12 — the assistant killed the operator's live floor during cleanup).
27
+
28
+ **Exit-143 rule (2026-10-12, corrected):** when the floor dies — Ctrl-C, kill,
29
+ or the Restart Verb hitting a foreground-terminal floor — the terminal prints
30
+ `ELIFECYCLE Command failed with exit code 143` and the floor is **DOWN. It
31
+ does not come back by itself.** Only the detached restart chain (the verb's
32
+ spawned one-shot) or a human re-running the command brings it back. Never
33
+ write or say "the floor came back detached" without a pid + `lstart` you just
34
+ read from `ps` — an unverified rebirth story cost the operator a dead floor
35
+ and a wrong incident report (2026-10-12).
36
+
37
+ ## 1. Snapshot BEFORE
38
+
39
+ Run the dsi-ports pipeline (see dsi-ports skill for the script) and save:
40
+
41
+ ```sh
42
+ sh /tmp/dsi-ports.sh > /tmp/floor-before.txt
43
+ ```
44
+
45
+ ## 2. Restart
46
+
47
+ - **PREFERRED, every shape: the Restart Verb (shipped — ADR "The Restart Verb,
48
+ One Button, One Route, Receipts", 2026-10).** With the operator's go, click
49
+ the power button in the sidebar header (or `POST /api/floor/restart`). It
50
+ spawns the same detached chain plugin-apply uses, so the floor re-serves in
51
+ the same mode (`--sync --dev in-process` fidelity) even when the previous
52
+ floor lived in the operator's terminal. Receipts still apply: BEFORE/AFTER
53
+ snapshots are mandatory.
54
+ - **Fallback, detached/agent-owned floor:** `dsi dsh --sync-kills`, then
55
+ relaunch the exact previous command (copy it from `ps` BEFORE killing).
56
+ - **Fallback, operator foreground floor (operator declines the verb):** print
57
+ the restart command and WAIT for the operator to run it (their Ctrl-C, their
58
+ terminal). Remember: once it dies, it stays dead until someone starts it.
59
+
60
+ ## 3. Snapshot AFTER (wait for both listeners first)
61
+
62
+ Poll until the expected ports answer (host + UI), then:
63
+
64
+ ```sh
65
+ sh /tmp/dsi-ports.sh > /tmp/floor-after.txt
66
+ ```
67
+
68
+ ## 4. The delta report (this is the deliverable)
69
+
70
+ Present exactly this table — join BEFORE and AFTER on port purpose (not pid: pids always change):
71
+
72
+ | Port | Purpose | RSS before | RSS after | Delta | PID before → after |
73
+ |---|---|---|---|---|---|
74
+ | 3080 | DSH host | 3262 MB | 1489 MB | **−1773 MB** | 38237 → 41200 |
75
+
76
+ Then one summary line: total floor RSS before → after, and the verdict:
77
+ - large negative delta + all expected ports up → **restart healthy**
78
+ - a port missing after → **name it loud** (that half of the floor did not come back)
79
+ - RSS after ≥ before → say so honestly; restart did not reclaim (possible leak — offer dsi-rca)
80
+
81
+ ## Boundaries
82
+
83
+ - Never kill a process the operator owns without their confirmed go (§0).
84
+ - Always restore the EXACT previous launch command/flags/env — a restart that changes the floor's shape is a config change, not a restart.
85
+ - The report is the point: a restart without before/after receipts is just downtime.
package/bin/dsi.mjs CHANGED
@@ -28,7 +28,8 @@ 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 } from './lib/plugin-chain.mjs';
31
+ import { dshLauncherCmd, parsePluginArgs, takeoverFloorPorts, hostBounceDecision, floorUiPort, parseDevFloorPids, launchUrl, tokenFromSettings } from './lib/plugin-chain.mjs';
32
+ import { parseDevArg, devModeEnv, modeFromBoolean, resolveBounceMode, extractInspectArgs, parseInspectArgsFromEnv } from './lib/dev-mode.mjs';
32
33
 
33
34
  // Layman-facing help: wrapped near 76 columns, continuation lines indented,
34
35
  // the recommended one-liner first. --zai is optional on purpose — no GLM
@@ -56,7 +57,11 @@ const USAGE = [
56
57
  ' store the dsh launch token as dsh.authToken in',
57
58
  ' ~/.dsi/settings.yaml, then serve',
58
59
  ' --dev serve the Vite dev server instead of the built page (takes',
59
- ' port 5175; needs a full checkout: pnpm install)',
60
+ ' port 5175; needs a full checkout: pnpm install). Optional',
61
+ ' value: --dev in-process boots the DSH host INSIDE the dev',
62
+ ' server process (one process, one Ctrl-C; host still on',
63
+ ' port 3080; token via in-process intercept). Bare --dev',
64
+ ' keeps the two-port pair; --dev pair is the same spelled out.',
60
65
  ' --sync-kills',
61
66
  ' kill orphaned dsh/dsi processes left by an earlier run',
62
67
  ' --ov install the OpenViking memory plugin into the active dsh',
@@ -195,15 +200,16 @@ if (command === 'dsh') {
195
200
  // HMR surface as `pnpm dev`, beside the same synced host.
196
201
  const rest = process.argv.slice(4);
197
202
  const withZai = rest.includes('--zai');
198
- const withDev = rest.includes('--dev');
199
203
  const withOv = rest.includes('--ov');
200
204
  if (withZai) addZaiProvider();
201
205
  if (withOv) runOvPlugin();
202
206
  runSkillsMirror();
203
- runSyncedPair(
204
- rest.filter((arg) => arg !== '--zai' && arg !== '--dev' && arg !== '--ov'),
205
- withDev
206
- );
207
+ // --dev parse (D4): 'in-process' | 'pair' consumed as a value; bare
208
+ // --dev stays 'pair'. dsi-side flags stripped before the host spawn.
209
+ const { mode, rest: hostArgs0 } = parseDevArg(rest);
210
+ // D7: inspector flags ride the vite child's execArgv, never the host.
211
+ const { rest: hostArgs, execArgv: inspectExecArgv } = extractInspectArgs(hostArgs0);
212
+ runSyncedPair(hostArgs, mode, inspectExecArgv);
207
213
  } else if (flag === '--plugin' || flag === '--plugin-restart') {
208
214
  // The Plugin Rack ADR (2026-09-27, D3/D5): the dsh CLI is the writer,
209
215
  // dsi owns the bounce. Install/remove delegates to the launcher rule
@@ -240,17 +246,67 @@ if (command === 'dsh') {
240
246
  // before the marker existed, or from plain `pnpm dev`, carries none.
241
247
  // The vite dev process itself is visible to pgrep (unlike the npx
242
248
  // shims — different argv rewriting), so ask the process table.
243
- const dev = bounceDevMode(parsed.dev === true, isDevFloorAlive);
244
- killOrphans();
245
- // The pkill sweep cannot see every floor shape (RCA 2026-09-27: the
246
- // npx shims — 'npm exec @deepseek-ai/dsh…' and '.bin/dsh web' — are
247
- // invisible to pkill -f on macOS, so the dsh host survived holding
248
- // 3080 and preflightPort exited 64, killing the detached chain
249
- // silently). The bounce chain owns the DSI ports by contract, so
250
- // free them by listener, not by name.
251
- freeFloorPorts([3080, 5174, 5175]);
252
- // Restore the SAME UI mode the floor was serving.
253
- runSyncedPair([], dev);
249
+ // D6 (2026-10-11): the boolean is no longer enough — an in-process
250
+ // floor re-served as 'pair' would double-spawn the host. Read the
251
+ // alive vite's DSI_DEV_MODE via ps -E BEFORE the sweep, resolve the
252
+ // enum, restore exactly what was running.
253
+ const floorEnvDump = readFloorEnv(devFloorPid());
254
+ const mode = resolveBounceMode({
255
+ flagDev: parsed.dev === true,
256
+ devFloorAlive: isDevFloorAlive(),
257
+ floorEnv: floorEnvDump
258
+ });
259
+ // The Faithful Bounce (ADR 2026-10-13, D3): the SAME floor env that
260
+ // records the mode records the inspector flags — re-forward them so
261
+ // the bounce boot comes back debuggable when it left that way.
262
+ const bounceExecArgv = parseInspectArgsFromEnv(floorEnvDump);
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
+ }
254
310
  } else if (flag === '--sync-kills') {
255
311
  killOrphans();
256
312
  process.exit(0);
@@ -367,56 +423,80 @@ function openBrowser(url, delayMs = 2000) {
367
423
  * the Vite dev server (`pnpm dev`'s surface) — as the UI. The pair shares
368
424
  * one lifecycle — a signal or one child's exit tears down the other, so no
369
425
  * orphan survives a Ctrl-C or a crash. */
370
- async function runSyncedPair(extraArgs, dev = false) {
426
+ async function runSyncedPair(extraArgs, dev = 'build', execArgv = []) {
427
+ // dev accepts the D4 mode enum ('build' | 'pair' | 'in-process'); the
428
+ // legacy boolean true is normalized so older call sites keep working.
429
+ const mode = typeof dev === 'boolean' ? modeFromBoolean(dev) : dev;
371
430
  const script = path.join(packageRoot, 'scripts', 'dsh-web-synced.sh');
372
- if (!existsSync(script)) {
431
+ const inProcess = mode === 'in-process';
432
+ if (!inProcess && !existsSync(script)) {
373
433
  console.error(`dsi dsh: no sync script at ${script}`);
374
434
  process.exit(66);
375
435
  }
376
436
  // UI port: the built server takes 5174 — the same port `dsi web` uses,
377
437
  // so plain `dsi dsh --sync` listens on ONE port. --dev's Vite dev server
378
438
  // takes 5175 instead (vite.config.ts pins 5174 strictPort, so pair mode
379
- // passes 5175 explicitly), giving the two-port dev shape.
380
- const uiPort = process.env.PORT ?? (dev ? '5175' : '5174');
439
+ // passes 5175 explicitly), giving the two-port dev shape. in-process is
440
+ // also a vite floor: same 5175.
441
+ const uiPort = process.env.PORT ?? (mode === 'build' ? '5174' : '5175');
381
442
  // The host half takes --port <n> when forwarded (a 3080 held by a
382
443
  // dsh web this CLI does not own — e.g. another live conversation
383
444
  // floor — is exactly the case the preflight must not kill). The
384
445
  // forwarded flag goes to `dsh web` untouched; DSI follows via
385
446
  // DSH_BASE_URL so the page dials the host wherever it landed.
447
+ // IN-PROCESS: no host child will be spawned, so preflighting 3080 is
448
+ // pointless — the plugin boots the host on 3080 AFTER vite is up, and
449
+ // a stale 3080 from a foreign floor is that floor's business (the
450
+ // embedded boot fails loud in the plugin, killing nothing).
386
451
  warnIfFirstRunDownload();
387
- const hostPort = hostPortFromArgs(extraArgs) ?? 3080;
452
+ // IN-PROCESS: the plugin reads its port from DSH_HOST_PORT (default 3080)
453
+ // — the UI half MUST dial the same port, so DSH_HOST_PORT participates in
454
+ // the dshBase derivation exactly like a forwarded --port flag. (Smoke
455
+ // bug 2026-10-11: DSH_HOST_PORT=3477 booted the host there while the
456
+ // page dialed 3080 — the production floor — → "Failed to fetch".)
457
+ const pluginPort = inProcess && process.env.DSH_HOST_PORT ? parseInt(process.env.DSH_HOST_PORT, 10) : undefined;
458
+ const hostPort = hostPortFromArgs(extraArgs) ?? pluginPort ?? 3080;
388
459
  const dshBase = hostPort === 3080 ? undefined : `http://127.0.0.1:${hostPort}`;
389
- // Preflight BOTH halves: a forgotten previous run holds not just the
390
- // host port but often the UI port too — the host would only crash
391
- // later, and the UI server would crash first with EADDRINUSE.
392
- await preflightPort(hostPort, 'dsh host');
393
- await preflightPort(uiPort, 'DSI page');
394
- const host = spawn('/bin/sh', [script, ...extraArgs], {
460
+ if (inProcess) {
461
+ await preflightPort(uiPort, 'DSI page');
462
+ } else {
463
+ await preflightPort(hostPort, 'dsh host');
464
+ await preflightPort(uiPort, 'DSI page');
465
+ }
466
+ // IN-PROCESS: no sync script, no host child — the vite plugin owns the
467
+ // host lifecycle (vite close hook stops it; one Ctrl-C, no orphan).
468
+ const host = inProcess ? null : spawn('/bin/sh', [script, ...extraArgs], {
395
469
  stdio: 'inherit',
396
470
  env: { ...process.env, DSH_WEB_VERSION: dshWebVersion() }
397
471
  });
398
- const ui = dev ? startDsiDevServer(uiPort, dshBase) : startDsiServer(uiPort, dshBase);
472
+ const ui = mode === 'build' ? startDsiServer(uiPort, dshBase) : startDsiDevServer(uiPort, dshBase, mode, execArgv);
399
473
  // The browser follows the UI: the built page (5174) — or, in --dev mode,
400
474
  // the dev server (5175).
401
475
  openBrowser(`http://localhost:${uiPort}/`);
402
476
  for (const signal of ['SIGINT', 'SIGTERM']) {
403
477
  process.on(signal, () => {
404
- host.kill(signal);
478
+ if (host) host.kill(signal); // in-process: no host child — vite close hook stops it
405
479
  ui.kill(signal);
406
480
  });
407
481
  }
408
482
  const teardown = (code, signal) => {
409
- host.kill('SIGTERM');
483
+ if (host) host.kill('SIGTERM');
410
484
  ui.kill('SIGTERM');
411
485
  process.exit(signal ? 1 : (code ?? 1));
412
486
  };
413
- host.on('exit', teardown);
487
+ if (host) host.on('exit', teardown);
414
488
  ui.on('exit', teardown);
415
489
  }
416
490
 
417
491
  /** Kill orphaned dsh/dsi leftovers — a pair whose parent died before it
418
492
  * could tear down its sibling, or a previous run the terminal lost. */
419
493
  function killOrphans() {
494
+ // Test-floor isolation (Port Ledger 34xx/51xx): a scratch floor's bounce
495
+ // must never pkill the production floor's shapes.
496
+ if (process.env.DSI_SKIP_ORPHAN_SWEEP === '1') {
497
+ console.log('dsi dsh: orphan sweep skipped (DSI_SKIP_ORPHAN_SWEEP=1)');
498
+ return;
499
+ }
420
500
  for (const pattern of ORPHAN_PATTERNS) {
421
501
  try {
422
502
  spawnSync('pkill', ['-f', pattern], { stdio: 'pipe' });
@@ -428,31 +508,103 @@ function killOrphans() {
428
508
  console.log('dsi dsh: orphan sweep done (dsh-web-synced.sh, dsh bin, dsi server)');
429
509
  }
430
510
 
431
- /** SIGTERM whatever LISTENS on these ports: the bounce chain's name-blind
432
- * second sweep (RCA 2026-09-27). pkill -f cannot match every floor shape
433
- * (the npx shim processes are invisible to it on macOS), so a surviving
434
- * dsh host held 3080, preflightPort exited 64, and the detached chain died
435
- * silently. Only the bounce uses this; a manual --sync preflight still
436
- * REFUSES a busy port it does not own. */
437
- function freeFloorPorts(ports) {
438
- for (const port of ports) {
511
+ /** The DSI ports the bounce owns. DSI_FLOOR_PORTS overrides for isolated
512
+ * test floors (Port Ledger: 34xx host, 5180+ UI) so a scratch bounce never
513
+ * touches the production ports. */
514
+ function floorPortSet() {
515
+ const raw = process.env.DSI_FLOOR_PORTS;
516
+ if (!raw) return [3080, 5174, 5175];
517
+ const ports = raw.split(',').map((s) => Number(s.trim())).filter((n) => Number.isInteger(n) && n > 0);
518
+ return ports.length ? ports : [3080, 5174, 5175];
519
+ }
520
+
521
+ /** SIGTERM whatever LISTENS on these ports, then WAIT until they actually
522
+ * free (The Port Takeover ADR 2026-10-12, D2/D3/D5): the fire-and-forget
523
+ * kill of RCA 2026-09-27 raced the successor's bind — the dying in-process
524
+ * host still held 3080 two seconds later and the new floor died on
525
+ * EADDRINUSE (incident: tmp/bounce.log 2026-10-12T03:49:06Z). Every kill
526
+ * and every freed port gets a receipt line; a port still held at the
527
+ * budget exits 64 naming the port and the holder. Only the bounce uses
528
+ * this — the click is the consent (ADR D1); a manual --sync preflight
529
+ * still REFUSES a busy port it does not own. */
530
+ async function freeFloorPorts(ports) {
531
+ const listListeners = (port) => {
439
532
  try {
440
- spawnSync('/bin/sh', ['-c', 'lsof -ti tcp:' + port + ' -sTCP:LISTEN | xargs kill 2>/dev/null || true'], { stdio: 'pipe' });
533
+ const probe = spawnSync('/bin/sh', ['-c', 'lsof -ti tcp:' + port + ' -sTCP:LISTEN 2>/dev/null || true'], { stdio: 'pipe', encoding: 'utf8' });
534
+ return (probe.stdout || '').split('\n').map((s) => Number(s.trim())).filter((n) => Number.isInteger(n) && n > 0);
441
535
  } catch {
442
- // no lsof (or no xargs): the pattern sweep above and the 5s
443
- // preflight wait remain the honest fallbacks.
536
+ // no lsof (or no sh): nothing to signal; the wait loop below will
537
+ // report the port honestly if it never frees.
538
+ return [];
444
539
  }
540
+ };
541
+ const result = await takeoverFloorPorts(ports, {
542
+ listListeners,
543
+ terminate: (pid) => {
544
+ try { process.kill(pid, 'SIGTERM'); } catch { /* already gone — the wait decides */ }
545
+ },
546
+ portFree: (port) => portInUse(port).then((busy) => !busy),
547
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
548
+ now: () => Date.now(),
549
+ log: (line) => console.log(line)
550
+ });
551
+ if (!result.ok) {
552
+ console.error('dsi dsh: port ' + result.port + ' is still in use by a process the takeover does not own (pid ' + result.pid + ') — free it yourself, then re-run');
553
+ process.exit(64);
445
554
  }
446
555
  }
447
556
 
448
557
  /** True when a vite dev server of THIS checkout is alive. Only the
449
558
  * checkout's own vite matches — a foreign project's dev server never
450
- * carries the deepseek-insight path segment. pgrep sees this process
451
- * (it does not rewrite argv the way the npm/npx shims do), so the probe
452
- * 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. */
453
563
  function isDevFloorAlive() {
454
- const probe = spawnSync('pgrep', ['-f', 'deepseek-insight/node_modules/vite/bin/vite.js'], { stdio: 'pipe' });
455
- return (probe.status ?? 1) === 0;
564
+ return devFloorPid() !== null;
565
+ }
566
+
567
+ /** The alive dev floor's vite pid (first match), or null. Same match rule
568
+ * as isDevFloorAlive — the pid feeds the D6 mode read (ps -E). */
569
+ function devFloorPid() {
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 });
577
+ if ((probe.status ?? 1) !== 0) return 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
+ }
595
+ }
596
+
597
+ /** The floor's DSI_DEV_MODE via ps -E (macOS: own processes only). Returns
598
+ * '' when the read fails for any reason — resolveBounceMode then falls back
599
+ * to the boolean arms, the pre-D6 behavior, never a wrong restore. */
600
+ function readFloorEnv(pid) {
601
+ if (!pid) return '';
602
+ try {
603
+ const ps = spawnSync('ps', ['-E', '-p', String(pid)], { stdio: 'pipe', timeout: 5000 });
604
+ return ps.status === 0 ? ps.stdout?.toString() || '' : '';
605
+ } catch {
606
+ return '';
607
+ }
456
608
  }
457
609
 
458
610
  /** Add the ZAI provider and default model to ~/.dsh/settings.yaml when
@@ -573,7 +725,7 @@ function runSkillsMirror() {
573
725
  * sources and devDependencies exist — a published tarball (bin + build)
574
726
  * refuses loudly instead of half-booting. The --port CLI flag overrides the
575
727
  * config's server.port while strictPort keeps the fail-loud contract. */
576
- function startDsiDevServer(defaultPort = '5175', dshBase = undefined) {
728
+ function startDsiDevServer(defaultPort = '5175', dshBase = undefined, mode = 'pair', execArgv = []) {
577
729
  const viteBin = path.join(packageRoot, 'node_modules', 'vite', 'bin', 'vite.js');
578
730
  if (!existsSync(viteBin)) {
579
731
  console.error(`dsi dsh --dev: no vite at ${viteBin} — --dev needs a full DSI checkout (pnpm install); use the built server instead`);
@@ -581,7 +733,13 @@ function startDsiDevServer(defaultPort = '5175', dshBase = undefined) {
581
733
  }
582
734
  // --host matches the built server's 127.0.0.1 bind: vite's default
583
735
  // 'localhost' can resolve ::1-only and strand a 127.0.0.1 client.
584
- return spawn(process.execPath, [viteBin, 'dev', '--host', process.env.HOST ?? '127.0.0.1', '--port', process.env.PORT ?? defaultPort], {
736
+ return spawn(process.execPath, [...execArgv, viteBin, 'dev', '--host', process.env.HOST ?? '127.0.0.1', '--port', process.env.PORT ?? defaultPort], {
737
+ // D7: inspector flags lead the child argv — node consumes options before
738
+ // the script path, so `node --inspect vite.js dev` debugs THIS process
739
+ // (vite + the embedded DSH host share one heap). The spawn execArgv
740
+ // OPTION was tried first and empirically never activated the inspector
741
+ // (silent no-op, probed 2026-10-11); leading args are the reliable form.
742
+ // NODE_OPTIONS is banned — it leaks to every child and collides on port.
585
743
  stdio: 'inherit',
586
744
  cwd: packageRoot,
587
745
  env: {
@@ -592,6 +750,13 @@ function startDsiDevServer(defaultPort = '5175', dshBase = undefined) {
592
750
  // this from the API server's env to re-serve --dev instead of the
593
751
  // stale build. Nothing else may set it.
594
752
  DSI_DEV: '1',
753
+ // D4/D6: the mode rides as env, never past the CLI to the host.
754
+ ...devModeEnv(mode),
755
+ // The Faithful Bounce (ADR 2026-10-13, D1): the inspector flags are
756
+ // part of the launch record — stamped so every restorer (the bounce
757
+ // boot via ps -E, the delayed re-serve via inherited env) can put
758
+ // them back exactly as launched.
759
+ ...(execArgv.length ? { DSI_INSPECT_ARGS: execArgv.join(' ') } : {}),
595
760
  ...(dshBase !== undefined ? { DSH_BASE_URL: dshBase } : {})
596
761
  }
597
762
  });