better-dsh 0.2.2-a → 0.2.2-c

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/docs/50_test-reports/2026-09-06-write/345/267/245/345/205/267sandbox/345/215/207/347/272/247/351/200/217/344/274/240bug/345/244/215/345/217/221/345/217/212/346/214/202/350/265/267-/344/272/213/344/273/266/346/212/245/345/221/212.md +43 -0
  2. package/docs/50_test-reports/v0.2.4-ios-focus-zoom-suppression/345/256/236/346/265/213/346/212/245/345/221/212.md +158 -0
  3. package/docs/60_exploration-and-research/01-cordis-runtime/bun-compile-cordis-runtime-bootstrap-research.md +536 -0
  4. package/docs/60_exploration-and-research/01-cordis-runtime/cordis-customization-and-override-mechanics.md +418 -0
  5. package/docs/60_exploration-and-research/01-cordis-runtime/cordis-research.md +350 -0
  6. package/docs/60_exploration-and-research/01-cordis-runtime/dsh-cordis-hotplug-mcp-patch-research.md +265 -0
  7. package/docs/60_exploration-and-research/02-dsh-webui/dsh-web-profile-package-map.md +186 -0
  8. package/docs/60_exploration-and-research/02-dsh-webui/dsh-web-ui-slot-system-research.md +310 -0
  9. package/docs/60_exploration-and-research/02-dsh-webui/dsh-webui-backend-data-inventory.md +633 -0
  10. package/docs/60_exploration-and-research/02-dsh-webui/dsh-webui-strip-boundary-research.md +300 -0
  11. package/docs/60_exploration-and-research/02-dsh-webui/dsh-webui-wire-appendix.md +3729 -0
  12. package/docs/60_exploration-and-research/02-dsh-webui/web-frontend-composability-research.md +191 -0
  13. package/docs/60_exploration-and-research/02-dsh-webui/webui-wire-data/capture-live-turn.json +1 -0
  14. package/docs/60_exploration-and-research/02-dsh-webui/webui-wire-data/observed-endpoints.json +224 -0
  15. package/docs/60_exploration-and-research/02-dsh-webui/webui-wire-data/remote-inventory.json +110954 -0
  16. package/docs/60_exploration-and-research/02-dsh-webui/webui-wire-data/served-index-sample.html +47 -0
  17. package/docs/60_exploration-and-research/02-dsh-webui/webui-wire-data/session-events.json +9411 -0
  18. package/docs/60_exploration-and-research/02-dsh-webui/webui-wire-data/ws-frame-examples.json +20 -0
  19. package/docs/60_exploration-and-research/03-mobile-ios/dsh-mobile-spa-ios-input-experience-research.md +160 -0
  20. package/docs/60_exploration-and-research/03-mobile-ios/ios-chat-app-bridge-research.md +324 -0
  21. package/docs/60_exploration-and-research/04-session-storage/alpha5-compaction-jsonl-mapping.md +1532 -0
  22. package/docs/60_exploration-and-research/04-session-storage/alpha5-compaction-sample/episode-failed.json +46 -0
  23. package/docs/60_exploration-and-research/04-session-storage/alpha5-compaction-sample/episode1.json +1124 -0
  24. package/docs/60_exploration-and-research/04-session-storage/alpha5-compaction-sample/episode2.json +1240 -0
  25. package/docs/60_exploration-and-research/05-dashr-dev/plugin-development.md +148 -0
  26. package/docs/60_exploration-and-research/05-dashr-dev/upstream-alignment.md +102 -0
  27. package/docs/60_exploration-and-research/README.md +87 -0
  28. package/docs/60_exploration-and-research/bun-compile-cordis-runtime-bootstrap-research.md +348 -0
  29. package/docs/60_exploration-and-research/dsh-mobile-spa-ios-input-experience-research.md +160 -0
  30. package/lib/index.d.ts +9 -1
  31. package/lib/index.js +258 -6
  32. package/package.json +1 -1
  33. package/docs/REPL-/345/267/245/345/205/267/350/260/203/347/224/250-/346/210/252/346/226/255/350/257/212/346/226/255.md +0 -110
  34. package/docs/adr/0001-bridge-tool-layer-not-service-layer.md +0 -14
  35. package/docs/adr/0002-masking-is-presentation-only.md +0 -15
  36. package/docs/plans/A2A-messaging-channel-test-archive.md +0 -256
  37. package/docs/plans/code-mode-vs-rlm-ipython-comparison.md +0 -137
  38. package/docs/plans/dashr-blueprint-review.md +0 -201
  39. package/docs/plans/dashr-blueprint.md +0 -561
  40. package/docs/plans/dashr-compaction-window-and-archive.md +0 -307
  41. package/docs/plans/dashr-profile-layer-feasibility.md +0 -367
  42. package/docs/plans/dashr-sandbox-escalation-semantics-gap.md +0 -171
  43. package/docs/plans/dashr-security-sandbox-analysis.md +0 -187
  44. package/docs/plans/dashr-surface-invariant-and-omp-imports.md +0 -97
  45. package/docs/plans/ipython-kernel-interactive-interface-test-report.md +0 -152
  46. package/docs/plans/kernel-refactoring/Dash-IPython-Control-Prompt-draft.md +0 -146
  47. package/docs/plans/kernel-refactoring/Dash-IPython-Control-Prompt-draft_v3.md +0 -50
  48. package/docs/plans/kernel-refactoring/Dash-IPython-Control-Prompt-draft_v4.md +0 -79
  49. package/docs/plans/kernel-refactoring/Dash-vs-PrimeAgent-systemprompt-toolcatalog-comparison.md +0 -138
  50. package/docs/plans/kernel-refactoring/RLM-system-prompt-injection-gap-report.md +0 -161
  51. package/docs/plans/kernel-refactoring/V0.1.5-development-plan.md +0 -109
  52. package/docs/plans/kernel-refactoring/actinoable-surface-to-llm-in-agent-runtime_dsh.md +0 -50
  53. package/docs/plans/kernel-refactoring/actinoable-surface-to-llm-in-agent-runtime_prime.md +0 -113
  54. package/docs/plans/recallable-compaction.md +0 -147
  55. package/docs/plans/spike-tag-repro.mjs +0 -102
  56. package/docs/plans/upstream-analysis.md +0 -128
  57. package/docs/v0.1.8-improved-/345/256/236/346/265/213/346/212/245/345/221/212.md +0 -142
  58. package/docs/v0.1.8-/345/256/236/346/265/213/346/212/245/345/221/212.md +0 -193
  59. package/docs/v0.1.8b-/345/256/236/346/265/213/346/212/245/345/221/212.md +0 -96
  60. package/docs/v0.1.8c-/345/256/236/346/265/213/346/212/245/345/221/212.md +0 -127
  61. package/docs/v0.1.8d-/345/256/236/346/265/213/346/212/245/345/221/212.md +0 -150
  62. package/docs/v0.1.8d_artifacts/README.md +0 -138
  63. package/docs/v0.1.8d_artifacts/code-mode-repl-only.observation.md +0 -74
  64. package/docs/v0.1.8d_artifacts/dsh-session-session-4a293388-9ae1-474b-87a0-9e17bb556d94.jsonl +0 -3890
  65. package/docs/v0.1.8d_artifacts/dsh-session-session-4a293388-9ae1-474b-87a0-9e17bb556d94.w-sample-0435.jsonl +0 -544
  66. package/docs/v0.1.8d_artifacts/functions.json +0 -592
  67. package/docs/v0.1.8d_artifacts/skills-catalog.snapshot.md +0 -30
  68. package/docs/v0.1.8d_artifacts/tools-sdk.output-schemas.json +0 -1236
  69. package/docs/v0.1.8d_artifacts/tools-sdk.python.txt +0 -592
  70. package/docs/v0.1.8d_artifacts/tools-sdk.typescript.txt +0 -516
  71. package/docs/v0.1.8d_artifacts/wire-vs-transcription.diff.md +0 -54
  72. package/docs/v0.1.8e-/345/256/236/346/265/213/346/212/245/345/221/212.md +0 -224
  73. package/docs/v0.1.9a-/345/256/236/346/265/213/346/212/245/345/221/212.md +0 -168
  74. package/docs/v0.2.0b-/345/256/236/346/265/213/346/212/245/345/221/212.md +0 -123
  75. package/docs/v0.2.0b_artifacts/f2probe/Cargo.lock +0 -7
  76. package/docs/v0.2.0b_artifacts/f2probe/Cargo.toml +0 -6
  77. package/docs/v0.2.0b_artifacts/f2probe/src/bin/messy.rs +0 -8
  78. package/docs/v0.2.0b_artifacts/f2probe/src/main.rs +0 -4
  79. package/docs/v0.2.0b_artifacts/hashline-probe.md +0 -5
  80. package/docs/v0.2.0b_artifacts/slowprobe/Cargo.lock +0 -7
  81. package/docs/v0.2.0b_artifacts/slowprobe/Cargo.toml +0 -7
  82. package/docs/v0.2.0b_artifacts/slowprobe/build.rs +0 -4
  83. package/docs/v0.2.0b_artifacts/slowprobe/src/main.rs +0 -13
  84. package/docs/v0.2.1-/345/256/236/346/265/213/346/212/245/345/221/212.md +0 -110
  85. package/docs/v0.2.1b-/345/256/236/346/265/213/346/212/245/345/221/212.md +0 -86
  86. package/docs/v0.2.1c-/345/256/236/346/265/213/346/212/245/345/221/212.md +0 -66
package/lib/index.js CHANGED
@@ -8319,7 +8319,7 @@ async function defaultSchemeWrite(scheme, path$1, content) {
8319
8319
  * write definition with args and exec passed through untouched.
8320
8320
  */
8321
8321
  function createWriteTool(deps) {
8322
- const { nativeWrite, postWrite, preWriteFormat } = deps;
8322
+ const { nativeWrite, postWrite, preWriteFormat, sandbox } = deps;
8323
8323
  const writeScheme = deps.writeScheme ?? defaultSchemeWrite;
8324
8324
  return defineTool({
8325
8325
  name: "write",
@@ -8334,7 +8334,8 @@ function createWriteTool(deps) {
8334
8334
  type: "string",
8335
8335
  required: true,
8336
8336
  description: "Full new content (an empty string writes an empty file)."
8337
- }
8337
+ },
8338
+ ...sandbox !== void 0 && sandbox.escalationModes.length > 0 ? sandbox.schemaFields() : {}
8338
8339
  },
8339
8340
  output: {
8340
8341
  schema: {
@@ -9832,6 +9833,7 @@ function installAgentTools(rootCtx, agent, resolver) {
9832
9833
  agent.ctx.effect(async () => {
9833
9834
  const native = captureNativeTools(rootCtx, agent);
9834
9835
  const disposers = [];
9836
+ const hashlineSandbox = new FsSandboxController(rootCtx);
9835
9837
  disposers.push(agent.ctx.tools.register(createReadTool({
9836
9838
  resolver,
9837
9839
  fs: rootCtx.fs,
@@ -9839,10 +9841,10 @@ function installAgentTools(rootCtx, agent, resolver) {
9839
9841
  })));
9840
9842
  disposers.push(agent.ctx.tools.register(createWriteTool({
9841
9843
  nativeWrite: native.write,
9844
+ sandbox: hashlineSandbox,
9842
9845
  ...buildLspWriteFeedback()
9843
9846
  })));
9844
9847
  const hashlineIo = ctxFsIO(rootCtx.fs, rootCtx);
9845
- const hashlineSandbox = new FsSandboxController(rootCtx);
9846
9848
  disposers.push(registerEditTool(rootCtx, agent.ctx, hashlineIo, hashlineSandbox));
9847
9849
  disposers.push(registerUndoTool(rootCtx, agent.ctx, hashlineIo, hashlineSandbox));
9848
9850
  disposers.push(registerWriteHook(rootCtx, agent.ctx, hashlineIo));
@@ -10707,6 +10709,251 @@ function installFailover(ctx, config = defaultFailoverConfig) {
10707
10709
  });
10708
10710
  }
10709
10711
 
10712
+ //#endregion
10713
+ //#region src/mobile/zoom-guard.ts
10714
+ /**
10715
+ * Whether a UA string + touch-capability pair is an iOS-class browser
10716
+ * (design D2, mirroring Discourse `capabilities.isIOS`): iPhone/iPod/iPad,
10717
+ * plus iPadOS 13+ desktop-Safari masquerade (`Macintosh` UA with a
10718
+ * multi-touch digitizer). Deliberately NOT `pointer:coarse` — Android Chrome
10719
+ * never focus-zooms and `maximum-scale` has historical side effects there,
10720
+ * so the gate is tightened to the iOS family. UA-shape drift degrades
10721
+ * benignly: a novel UA simply reads as non-iOS and no rewriting happens.
10722
+ *
10723
+ * @param ua - `navigator.userAgent`.
10724
+ * @param maxTouchPoints - `navigator.maxTouchPoints` (0 when absent).
10725
+ * @returns `true` for iOS-class browsers.
10726
+ */
10727
+ function isIOSClassUA(ua, maxTouchPoints) {
10728
+ return /iPhone|iPod|iPad/.test(ua) || /Macintosh/.test(ua) && maxTouchPoints > 1;
10729
+ }
10730
+ /**
10731
+ * Token-level merge of viewport directives into a viewport meta `content`
10732
+ * string (design D4): existing tokens keep their positions, a token whose key
10733
+ * (compared case-insensitively) is in `tokens` is REPLACED by the configured
10734
+ * key=value, and configured keys not present are APPENDED in insertion order.
10735
+ * Idempotent by construction — merging always re-derives from the input, so
10736
+ * repeated evaluation cannot accumulate duplicates.
10737
+ *
10738
+ * mergeViewportTokens('width=device-width, initial-scale=1', T)
10739
+ * → 'width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no'
10740
+ * mergeViewportTokens('width=device-width, maximum-scale=5', T)
10741
+ * → 'width=device-width, maximum-scale=1, user-scalable=no' // same-key override, position kept
10742
+ *
10743
+ * @param content - the stock (or stock-equivalent) content string.
10744
+ * @param tokens - the directives to merge, keyed lowercase.
10745
+ * @returns the merged content string.
10746
+ */
10747
+ function mergeViewportTokens(content, tokens) {
10748
+ var out = [];
10749
+ var seen = {};
10750
+ var parts = content.split(",");
10751
+ for (var i = 0; i < parts.length; i++) {
10752
+ var part = parts[i];
10753
+ var raw = (part === void 0 ? "" : part).replace(/^\s+|\s+$/g, "");
10754
+ if (raw === "") continue;
10755
+ var eq = raw.indexOf("=");
10756
+ var key = eq < 0 ? raw : raw.slice(0, eq).replace(/^\s+|\s+$/g, "");
10757
+ var hit = null;
10758
+ for (var k in tokens) if (tokens.hasOwnProperty(k) && k.toLowerCase() === key.toLowerCase()) {
10759
+ hit = k;
10760
+ break;
10761
+ }
10762
+ if (hit === null) {
10763
+ out[out.length] = raw;
10764
+ continue;
10765
+ }
10766
+ out[out.length] = hit + "=" + tokens[hit];
10767
+ seen[hit] = true;
10768
+ }
10769
+ for (var j in tokens) if (tokens.hasOwnProperty(j) && !seen[j]) out[out.length] = j + "=" + tokens[j];
10770
+ return out.join(", ");
10771
+ }
10772
+ /**
10773
+ * The double gate (design D2/D3 + the `zoomGuard` config): rewrite applies
10774
+ * only on an iOS-class browser AND a narrow viewport AND a non-`off`
10775
+ * `zoomGuard`. An absent payload (or absent key) means the `'meta'` default —
10776
+ * the same default-on posture as the mobile leg itself — so only an explicit
10777
+ * `'off'` disarms the guard. Unknown non-`off` values read as `'meta'`
10778
+ * (schema validation makes them unreachable in production; benign here).
10779
+ *
10780
+ * @param config - the `__DASHR_MOBILE__` payload (only `zoomGuard` is read).
10781
+ * @param isIOS - {@link isIOSClassUA} verdict for this page load.
10782
+ * @param isNarrow - `matchMedia('(max-width: breakpoint-0.02px)')` verdict.
10783
+ * @returns `true` when the viewport rewrite should be applied.
10784
+ */
10785
+ function shouldApplyZoomGuard(config, isIOS, isNarrow) {
10786
+ if (!isIOS || !isNarrow) return false;
10787
+ if (!config) return true;
10788
+ return config.zoomGuard !== "off";
10789
+ }
10790
+ /**
10791
+ * Standalone display-mode verdict (design D4 of change
10792
+ * `2026-09-03-zoomguard-standalone-font-floor`): standalone PWAs HONOR
10793
+ * `user-scalable=no` (the real-device pinch regression that motivated the
10794
+ * display-mode split), so they must never enter the meta-rewrite path. Two
10795
+ * sources, strict-`true` OR: the `(display-mode: standalone)` media query
10796
+ * (primary) and legacy `navigator.standalone` (older iOS Safari exposes only
10797
+ * the latter). Anything else — `undefined` (engine without the source),
10798
+ * `false`, or truthy junk — reads as NOT standalone.
10799
+ *
10800
+ * @param mqMatchesStandalone - `matchMedia('(display-mode: standalone)').matches`.
10801
+ * @param navStandalone - `navigator.standalone === true`.
10802
+ * @returns `true` in standalone display mode (either source).
10803
+ */
10804
+ function isStandaloneDisplay(mqMatchesStandalone, navStandalone) {
10805
+ return mqMatchesStandalone === true || navStandalone === true;
10806
+ }
10807
+ /**
10808
+ * The 16px font floor stylesheet (design D3): iOS auto-zooms on focus into
10809
+ * ANY control whose computed font-size is below 16px, so in standalone mode
10810
+ * — where the meta rewrite is off the table (pinch must stay
10811
+ * engine-available) — the floor IS the suppression. Scoped by the same
10812
+ * breakpoint band the meta leg uses and derived the same way
10813
+ * (`breakpoint - 0.02`: 768 → 767.98, 900 → 899.98); `!important` beats the
10814
+ * 13/14px CSS module rules and the body inline var chain;
10815
+ * `[contenteditable="true"]` matches only the editable state (React
10816
+ * serializes `contentEditable={bool}` to the literal attribute value
10817
+ * "true"/"false").
10818
+ *
10819
+ * @param breakpointPx - the configured `mobile.breakpoint` (raw — this
10820
+ * function derives the `-0.02` band itself, do NOT pre-subtract).
10821
+ * @returns the complete stylesheet text.
10822
+ */
10823
+ function buildFontFloorCss(breakpointPx) {
10824
+ return "@media (max-width:" + (breakpointPx - .02) + "px){ input,textarea,select,[contenteditable=\"true\"]{ font-size:16px !important } }";
10825
+ }
10826
+ /**
10827
+ * Build the boot script's zoomGuard section: the five decision predicates
10828
+ * embedded via `toString()` plus the DOM state machine that applies them.
10829
+ * The machine implements design D3/D4 with one discovered refinement —
10830
+ *
10831
+ * **The standalone fork (2026-09-03, change
10832
+ * `2026-09-03-zoomguard-standalone-font-floor`).** Real-device datum:
10833
+ * standalone (Add to Home Screen) iOS HONORS `user-scalable=no`, so the
10834
+ * meta rewrite that is benign in-browser (pinch stays engine-controlled)
10835
+ * kills pinch in the PWA — the user's primary surface. The section
10836
+ * therefore forks right after the iOS gate, BEFORE any meta machinery: a
10837
+ * one-shot startup verdict (display-mode never flips mid-session) via the
10838
+ * `(display-mode: standalone)` MQ or legacy `navigator.standalone` (D4).
10839
+ * Standalone → inject the `ios-zoom-font-floor` style element into the
10840
+ * head (16px floor, media-query-scoped to the same breakpoint band, D3)
10841
+ * and RETURN — no provisional meta, no reconcile observer, no ladder
10842
+ * timer, no listeners, byte-identical stock meta (D1). The width dimension
10843
+ * lives inside the stylesheet's media query, so the injection itself is
10844
+ * NOT width-gated: a wide standalone load still gets the element; the rule
10845
+ * simply does not match above the breakpoint. Browser mode runs the v0.2.4
10846
+ * machinery unchanged.
10847
+ * **Why reconciliation exists.** The webserver splices head injections
10848
+ * immediately after the opening `<head>` tag, i.e. BEFORE the stock
10849
+ * `<meta name="viewport">` in upstream's `index.html`. At boot-script time
10850
+ * the stock meta is therefore NOT yet parsed, so the "meta missing"
10851
+ * branch is the NORMAL path on the served shell, not an edge case. A
10852
+ * provisional meta is created right away (the timing requirement: the
10853
+ * suppressive tokens are live before any application bundle can focus an
10854
+ * input), and a MutationObserver watches for the parser to insert the stock
10855
+ * meta; the moment it appears, the guard records the true stock content,
10856
+ * rewrites the STOCK meta (token merge, D4's actual semantics — restore
10857
+ * later writes these exact bytes back), and removes the provisional one —
10858
+ * leaving a single-meta document instead of relying on engines'
10859
+ * unspecified multi-meta merge behavior. Engines without MutationObserver
10860
+ * keep the provisional meta for the page's lifetime (degraded but still
10861
+ * suppressive); the observer is never started once the stock meta was
10862
+ * found synchronously.
10863
+ *
10864
+ * **Why the re-evaluation ladder exists (2026-09-03 rework, lead CDP
10865
+ * finding on the 4999 instance).** The boot script runs during head parsing,
10866
+ * BEFORE the engine applies the stock viewport meta — the layout viewport at
10867
+ * first evaluation is the pre-meta engine default (980px observed), so
10868
+ * `mq.matches` reads false on an initial narrow load, and the narrowing that
10869
+ * follows meta application does NOT reliably dispatch `resize` to the page
10870
+ * during load (headless Chrome: never). A ~10ms poll ladder therefore
10871
+ * re-evaluates until the guard IS applied, `document.readyState` reaches
10872
+ * `'complete'`, or ~2s elapse — the first layout always precedes deferred
10873
+ * scripts, so the ladder lands the guard before any application bundle can
10874
+ * focus an input. Steady-state ticks are idempotent no-ops; a wide load
10875
+ * idles to the readyState stop and leaves no timer behind (a pending tick
10876
+ * is also cancelled the moment the guard lands). MediaQuery `change` events
10877
+ * (with the legacy `addListener` fallback) form a second re-evaluation
10878
+ * channel alongside window `resize`.
10879
+ *
10880
+ * State machine (ES5, zero dependencies, synchronous first evaluation):
10881
+ * `meta` is the element under management (provisional or reconciled stock),
10882
+ * `stock` its recorded stock content, `mine` whether we created it,
10883
+ * `applied` the last applied gate verdict. `apply(on)` is idempotent per
10884
+ * state (no-op when the verdict is unchanged); merges always re-derive
10885
+ * from the recorded stock, never from the live content, so resize storms
10886
+ * cannot accumulate tokens. Non-iOS pages never wire any listener.
10887
+ *
10888
+ * Emitted only when the mobile payload exists and `zoomGuard !== 'off'`.
10889
+ *
10890
+ * @returns the inline section text (truthy `var` declarations + IIFE).
10891
+ */
10892
+ function buildZoomGuardSection() {
10893
+ return [
10894
+ `var ZI=${isIOSClassUA.toString()};`,
10895
+ `var ZM=${mergeViewportTokens.toString()};`,
10896
+ `var ZS=${shouldApplyZoomGuard.toString()};`,
10897
+ `var ZD=${isStandaloneDisplay.toString()};`,
10898
+ `var ZF=${buildFontFloorCss.toString()};`,
10899
+ "(function(){",
10900
+ "var m=window.__DASHR_MOBILE__;",
10901
+ "var ios=ZI(navigator.userAgent,(navigator.maxTouchPoints||0));",
10902
+ "if(!ios)return;",
10903
+ "var bpx=(m&&typeof m.breakpoint==='number'&&m.breakpoint>0)?m.breakpoint:768;",
10904
+ "var bp=bpx-0.02;",
10905
+ "var sa=ZD((window.matchMedia&&window.matchMedia('(display-mode: standalone)').matches),(navigator.standalone===true));",
10906
+ "if(sa){var f=document.createElement('style');f.setAttribute('id','ios-zoom-font-floor');f.setAttribute('data-plugin','better-dsh');f.setAttribute('data-plugin-css','better-dsh/zoom-font-floor');f.textContent=ZF(bpx);headEl().appendChild(f);return}",
10907
+ "var mq=(typeof window.matchMedia==='function')?window.matchMedia('(max-width:'+bp+'px)'):null;",
10908
+ "var T={'maximum-scale':'1','user-scalable':'no'};",
10909
+ "var meta=null,stock=null,mine=false,applied=false,obs=null,lad=null,lN=0;",
10910
+ "function findStock(){var s=document.getElementsByTagName('meta');for(var i=0;i<s.length;i++){var e=s[i];if(e!==meta&&(e.getAttribute('name')||'').toLowerCase()==='viewport')return e}return null}",
10911
+ "function headEl(){return document.head||document.getElementsByTagName('head')[0]||document.documentElement}",
10912
+ "function stopWatch(){if(obs){obs.disconnect();obs=null}}",
10913
+ "function stopLadder(){if(lad){clearTimeout(lad);lad=null}}",
10914
+ "function dropMine(){if(meta&&mine){if(meta.parentNode)meta.parentNode.removeChild(meta);meta=null;mine=false}}",
10915
+ "function apply(on){",
10916
+ "if(on===applied)return;",
10917
+ "applied=on;",
10918
+ "if(on)stopLadder();",
10919
+ "if(on){",
10920
+ "var el=findStock();",
10921
+ "if(el){meta=el;mine=false;stock=el.getAttribute('content')||''}",
10922
+ "else if(!meta){meta=document.createElement('meta');meta.setAttribute('name','viewport');mine=true;stock='';headEl().appendChild(meta)}",
10923
+ "meta.setAttribute('content',ZM(stock,T));",
10924
+ "if(mine&&typeof MutationObserver==='function'&&!obs){obs=new MutationObserver(check);obs.observe(document.documentElement,{childList:true,subtree:true})}",
10925
+ "}else{",
10926
+ "stopWatch();",
10927
+ "if(!meta)return;",
10928
+ "if(mine)dropMine();",
10929
+ "else meta.setAttribute('content',stock);",
10930
+ "}",
10931
+ "}",
10932
+ "function check(){",
10933
+ "var el=findStock();",
10934
+ "if(!el)return;",
10935
+ "stopWatch();",
10936
+ "stock=el.getAttribute('content')||'';",
10937
+ "if(applied){dropMine();meta=el;mine=false;meta.setAttribute('content',ZM(stock,T))}",
10938
+ "else{dropMine();meta=null;stock=null}",
10939
+ "}",
10940
+ "function re(){apply(ZS(m,true,mq?mq.matches:false))}",
10941
+ "function tick(){",
10942
+ "lad=null;",
10943
+ "re();",
10944
+ "if(applied)return;",
10945
+ "lN++;",
10946
+ "if(document.readyState==='complete'||lN>=200)return;",
10947
+ "lad=setTimeout(tick,10);",
10948
+ "}",
10949
+ "re();",
10950
+ "window.addEventListener('resize',re);",
10951
+ "if(mq&&mq.addEventListener)mq.addEventListener('change',re);else if(mq&&mq.addListener)mq.addListener(re);",
10952
+ "if(!applied&&typeof setTimeout==='function')lad=setTimeout(tick,10);",
10953
+ "})()"
10954
+ ].join("");
10955
+ }
10956
+
10710
10957
  //#endregion
10711
10958
  //#region src/web-trust.ts
10712
10959
  /**
@@ -10752,12 +10999,16 @@ function buildBootScript(config) {
10752
10999
  ...mobile?.dominanceRatio !== void 0 ? { dominanceRatio: mobile.dominanceRatio } : {},
10753
11000
  ...mobile?.leftEdgeBandPx !== void 0 ? { leftEdgeBandPx: mobile.leftEdgeBandPx } : {},
10754
11001
  ...mobile?.rightZoneRatio !== void 0 ? { rightZoneRatio: mobile.rightZoneRatio } : {},
10755
- ...mobile?.swipeVelocityPxPerMs !== void 0 ? { swipeVelocityPxPerMs: mobile.swipeVelocityPxPerMs } : {}
11002
+ ...mobile?.swipeVelocityPxPerMs !== void 0 ? { swipeVelocityPxPerMs: mobile.swipeVelocityPxPerMs } : {},
11003
+ ...mobile?.zoomGuard !== void 0 ? { zoomGuard: mobile.zoomGuard } : {}
10756
11004
  } : void 0;
10757
11005
  if (authorities.length === 0 && mobilePayload === void 0) return void 0;
10758
11006
  const parts = ["(function(){try{"];
10759
11007
  if (authorities.length > 0) parts.push("if(!window.__DSH_TRANSPORT__){", `var h=location.hostname,a=${JSON.stringify(authorities)};`, "for(var i=0;i<a.length;i++)if(a[i]===h){window.__DSH_TRANSPORT__={ownsHost:true};break}", "}");
10760
- if (mobilePayload !== void 0) parts.push(`window.__DASHR_MOBILE__=${JSON.stringify(mobilePayload)};`);
11008
+ if (mobilePayload !== void 0) {
11009
+ parts.push(`window.__DASHR_MOBILE__=${JSON.stringify(mobilePayload)};`);
11010
+ if (mobile?.zoomGuard !== "off") parts.push(buildZoomGuardSection());
11011
+ }
10761
11012
  parts.push("}catch(e){}})();");
10762
11013
  return parts.join("");
10763
11014
  }
@@ -10802,7 +11053,8 @@ const MOBILE_CONFIG = z.object({
10802
11053
  dominanceRatio: z.number().min(1).default(1.3),
10803
11054
  leftEdgeBandPx: z.natural().min(8).default(120),
10804
11055
  rightZoneRatio: z.number().min(.05).max(.9).default(.25),
10805
- swipeVelocityPxPerMs: z.number().min(0).default(.15)
11056
+ swipeVelocityPxPerMs: z.number().min(0).default(.15),
11057
+ zoomGuard: z.union(["meta", "off"]).default("meta")
10806
11058
  });
10807
11059
  /** Runtime schema. */
10808
11060
  const Config = z.intersect([DashrRuntime.Config, z.object({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "better-dsh",
3
- "version": "0.2.2-a",
3
+ "version": "0.2.2-c",
4
4
  "type": "module",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -1,110 +0,0 @@
1
- # REPL 工具调用截断诊断(v0.2.1b 实测中发现,2026-09-01)
2
-
3
- ## 结论先行
4
-
5
- **任何从 REPL(`eval` cell)内部发起的工具调用(`tool.<name>(...)`)都会让 dsh web daemon 进程崩溃并自动重启;会话日志在 `tool/code-dispatch-start` 处撕裂,恢复时 repair 机制合成「interrupted-tool-result」,模型看到的即「工具调用被截断、结果未知」。** 4 次探针 4 次复现,与 journald 守护进程崩溃记录逐条对应。
6
-
7
- 这不是 REPL 同步/异步的问题,也不是 subagent 特有问题——是 dashr 子分派桥接层的结构性缺陷 + 部署运行态注册表视图缺 symbol 的直接原因叠加。
8
-
9
- ## 一、复现矩阵(第一人称)
10
-
11
- | # | cell 内容 | 结果 |
12
- |---|---|---|
13
- | 1 | `await tool.subagent({… run_in_background: false })` | 截断(daemon 崩溃 17:39:17) |
14
- | 2 | `await tool.subagent({… run_in_background: true })` | 截断(daemon 崩溃 17:41:15) |
15
- | 3 | `await tool.bash({… run_in_background: true })` | 截断(daemon 崩溃 17:57:05) |
16
- | 4 | `await tool.read({path: "dashr/package.json"})` | 截断(daemon 崩溃 18:02:27) |
17
-
18
- 对照:纯 Python cell(`dir(tool)`、顶层 `return` 判错、`await asyncio.sleep(0)`)全部正常。**凡含 `tool.*` RPC 的 cell 必崩,与工具、与 background 与否无关。**
19
-
20
- ## 二、截断面定位(三层证据)
21
-
22
- ### 1. 会话日志(`~/.dsh/sessions/…/session.jsonl.zstd`)
23
- 4 次截断的共同指纹:`tool/call (eval)` → `tool/code-dispatch-start`(**已持久化**)→ 日志撕裂(无 `tool/code-dispatch`、无 `step/end`/`turn/end`)→ 重载时 `packages/core/session/src/repair.ts` 的 `interruptedTurnClosers` 合成 `interrupted-tool-result-<callId>-<seq>`(`TOOL_OUTCOME_UNKNOWN`)。会话内 **0 条 error 事件**——错误根本没走到会话层。
24
-
25
- ### 2. 守护进程日志(`journalctl --user -u dsh.service`)——铁证
26
- ```
27
- Sep 01 17:39:17 pnpm[414449]: dsh: fatal load failure: TypeError: Cannot read properties of undefined (reading 'prepare')
28
- at Object.start (…/better-dsh/lib/index.js:11281:41)
29
- at <anonymous> (…/lib/index.js:11191:21)
30
- at drive (…/lib/index.js:11207:7)
31
- at outcome (…/lib/index.js:11310:6)
32
- at new Promise (<anonymous>)
33
- at toolFunctions.<computed> (…/lib/index.js:11320:72)
34
- at Proxy.dispatchHostRequest (…/lib/index.js:2180:21)
35
- Sep 01 17:39:19 systemd: dsh.service: Main process exited, code=exited, status=1/FAILURE
36
- Sep 01 17:39:23 systemd: dsh.service: Scheduled restart job, restart counter is at 1.
37
- ```
38
- 17:39:17 / 17:41:15 / 17:57:05 / 18:02:27 四次崩溃,栈完全一致。`scheduler.prepare` 中 `scheduler` 为 `undefined`——即 `registry[TOOL_RUNTIME_SCHEDULER]` 在部署运行态取不到值。
39
-
40
- ### 3. 源码链(`dashr/src/index.ts`)
41
- - kernel 侧:`tool.<name>(...)` → `binding.call` RPC(`src/bootstrap.ts` `_dashr_callable`)。
42
- - host 侧:`dispatchHostRequest`(`src/runtime.ts:545`)→ `await fn(args)`(`src/index.ts` `binding()`)。
43
- - `binding()` 内部(`src/index.ts:666`):`const scheduler = registry[TOOL_RUNTIME_SCHEDULER]` → 单驱动道 `drive()`(`src/index.ts:591-621`)→ `start()` 内 `await scheduler.prepare(input)`(`src/index.ts:712`)。
44
- - **结构性缺陷**:`drive()` 的 async IIFE 是 `try { … } finally { … }`,**没有 catch**;调用处是 `void drive()`。任何 `start()`/`commit()` 抛错(如 `scheduler.prepare` TypeError)都会变成 **unhandled rejection**——binding promise 既不 resolve 也不 reject,cell 永久挂起,错误外泄给进程。
45
- **完整链条**:cell 工具调用 → dispatch 启动(`code-dispatch-start` 已入日志)→ `registry[TOOL_RUNTIME_SCHEDULER]` 为 undefined → `scheduler.prepare` TypeError → driver 无 catch → unhandled rejection → `installFailLoud` 判定 fatal → daemon exit(1) → systemd 重启 → 会话重载 repair 合成「interrupted-tool-result」→ 模型收到截断消息。**截断面:binding 桥 → registry symbol 查询 → driver 的未处理拒绝。**
46
-
47
- ## 三、命名澄清:REPL dispatch log 与 code-dispatch 是两个不同层(重要)
48
-
49
- 容易混淆,先分清:
50
-
51
- | 名字 | 层级 | 是谁 | 本次 change 是否改名 |
52
- |---|---|---|---|
53
- | `tool/code-dispatch-start` / `tool/code-dispatch` | **会话持久日志事件**(durable log,`agent.session.append`) | dashr `src/index.ts:689,712`;harness `known-event-types.ts:63-64` 注册 | **否**——v0.1.x 就有,历史遗留名 |
54
- | `dashr/repl-dispatch-log` | **waterfall 扩展点**(内容整形监听事件) | dashr 自注册(`src/index.ts:458`) | **是**——v0.2.1 从上游 `tools/code-dispatch-log`/`tools/ptc-dispatch-log` 改名为「自己的 REPL dispatch log」 |
55
- | `tools/ptc-dispatch-log` | waterfall(上游 PTC 用) | harness `src/ptc.ts:282` + spill policy 监听(`spill-policy/src/index.ts:217`) | dashr 已不再 emit(仅注释提及) |
56
-
57
- **结论**:
58
- - 「自己的 REPL dispatch log」`dashr/repl-dispatch-log` **在代码里存在且自注册**——目标形态已就位。它之所以「没被触发」,是因为崩溃发生在它之前的 `scheduler.prepare`(它在 settle 之后才跑)——**是崩溃的结果,不是原因**。
59
- - 会话日志里我用作定位证据的 `tool/code-dispatch-start` 是**持久日志事件名**,与 waterfall 改名无关,只是「撕裂点之前最后一条已写入事件」的定位标记。
60
- - 改名引入了一个**真实但非崩溃的副作用**:harness 的 spill policy 仍监听上游 `tools/ptc-dispatch-log`,dashr 不再 emit → 溢出限幅(oversized 结果的 preview+locator 替换)对 eval 子调用日志**静默失效**。**已补(2026-09-01 晚,见六)**:dashr `shapeDispatchLog` 跑完 `dashr/repl-dispatch-log` 后,再把同一 dispatch(载荷结构与上游 `PtcDispatchLog` 完全一致)喂进 `tools/ptc-dispatch-log` 载体,spill 臂恢复生效。改动在 `dashr/src/index.ts`,lib 已重建(`f0febba7…`),**部署位同步 + daemon 重启待用户执行**。
61
-
62
- ## 四、为什么 `registry[TOOL_RUNTIME_SCHEDULER]` 是 undefined(直接原因)
63
-
64
-
65
- - 已排除:dsh-tools 双副本 symbol 不一致(实测插件与 daemon 均解析到同一 realpath `/home/u1/workspaces/dsh-alpha/packages/core/tools`,symbol 严格相等);dashr 分派代码改动(266d232/06e516a/HEAD 逐字节一致);dsh-tools alpha.1 vs alpha.3 差异(仅 import 整理/ToolCallId brand 等外观差异)。
66
- - 关键观察:`classify()` 调用的 `registry.executionMode(input)` **正常**(`code-dispatch-start` 已写入日志,证明 `start()` 之前的分派逻辑跑通),而 `registry[TOOL_RUNTIME_SCHEDULER]` **取不到**。`executionMode` 是 ToolRuntime 类方法、`TOOL_RUNTIME_SCHEDULER` 是类实例 symbol 字段——即部署运行态 `runtimeCtx.tools` 返回的**不是原生 ToolRuntime 实例**,而是一个有方法、缺实例 symbol 字段的服务视图(cordis 作用域服务解析 / plugin tree 挂载路径差异)。
67
- - 测试为什么全绿:dashr 测试组合(`test/helpers.ts`)把 `ctx.tools` 解析到**根作用域的原生 ToolRuntime 实例**,symbol 在位;生产组合经 harness plugin tree / scope 层解析,拿到的是视图。
68
- - **何时坏的(重要修正)**:v0.1.8 实测报告(2026-08-24)实证 DASHR 自己的 `eval` 桥当时**正常工作**——同 cell 并发 `tool.read` + `tool.bash` 完成。2026-08-28 会话用的是 harness 自带 PTC `run_cell`(`tools.bash`,复数命名空间 = PTC SDK 形态;该 PTC 核心经 `@deepseek-ai/dsh-code-runtime-python` 执行 **Python**,非 TS),不能代表 dashr 桥状态。**崩溃由 2026-09-01 的 harness alpha(0.1.2-alpha.1)升级引入**——升级后 plugin tree 里 `runtimeCtx.tools` 变成缺实例 symbol 字段的服务视图;dashr v0.2.1 的 dispatch-log 改名不是崩溃原因。
69
-
70
- ## 五、REPL 同步/异步问题的回答(顺带闭环)
71
-
72
- - 模型层:`eval` 是同步工具调用,模型等 cell 结果——这是设计使然,与缺陷无关。
73
- - cell 内:`tool.<name>(...)` 是 kernel→host 的 `binding.call` RPC,被 cell `await`;host 经与原生调用同一 registry 分派器执行。`run_in_background: true` 的工具实现**快速返回 id**(subagent `{kind:'continuable', subagentId}`、bash 返回 job id),background 语义本应保留、子任务与 cell 结束无关。
74
- - 但**当前运行时该路径整体坏掉**:任何子分派在 `scheduler.prepare` 即 TypeError,与 background 无关——所以「在 REPL 里 background 是否异步」目前是伪问题,路径不通;修好后 background 语义会按上述设计成立。
75
-
76
- ## 六、修复方向(分层)
77
-
78
- 1. **dashr 立即修(阻断 daemon 崩溃)**:`drive()` 加 catch / `start()`、`commit()` 失败必须 settle binding promise 为错误结果——cell 内工具调用失败应变成可见的 `ToolCallError`,**绝不允许以 unhandled rejection 外泄杀 daemon**。这是把「任何分派错误」放大成「整个运行时崩溃」的放大器,无论直接原因是什么都必须先修。
79
- 2. **dashr 防御**:`registry[TOOL_RUNTIME_SCHEDULER]` 为 undefined 时抛带上下文的 loud 错误(cell 可见),而不是裸 `undefined.prepare` TypeError。
80
- 3. **根因定位(harness 接线)**:在活体 daemon 上探针确认 plugin tree 中 `runtimeCtx.tools` 的实际对象(cordis 作用域服务视图 / 第二个 tools 实例),修正挂载使插件拿到原生 ToolRuntime(symbol 在位)。这一步需要 daemon 侧可写环境(本会话 bwrap 沙箱只读宿主,无法直接探查活体)。
81
- 4. **测试补强(部分完成)**:已加「`tools/ptc-dispatch-log` 载体回喂」回归用例(`test/bridge.spec.ts`,全量 396 绿)。仍缺「生产形态」挂载测试——按 harness 的 plugin-tree/scope 方式挂载插件,断言 `ctx.tools[TOOL_RUNTIME_SCHEDULER]` 已定义 + 一个真实工具 RPC 从 cell 完成;防止此环境差异回归。
82
- 5. **回归核验基线**:修复后重跑本文档第一节的 4 探针矩阵(4/4 应全部返回结果而非截断),并观察 `journalctl --user -u dsh.service` 无新崩溃。
83
-
84
- ## 附:证据文件
85
- - 会话日志:`~/.dsh/sessions/--home-u1-workspaces-dashr--/session-ad9ac311-3400-4304-b501-e7303da850df/session.jsonl.zstd`
86
- - 守护进程日志:`journalctl --user -u dsh.service`(17:39:17 / 17:41:15 / 17:57:05 / 18:02:27 四条 fatal load failure)
87
- - 对照工作会话:`06ac8b2c-fb2e-458f-80d6-a0d28e50f193`(2026-08-28,9 对完整 code-dispatch,PTC run_cell 路径)
88
-
89
- ---
90
-
91
- ## 七、v0.2.1c 修复执行记录(2026-09-01,change v0-2-1c-repl-tool-crash-fix)
92
-
93
- ### 六.1 + 六.2 已实现(dashr 侧)
94
-
95
- - `drive()` async IIFE 补 `catch`:lane 级 backstop——任何逃逸 start()/commit() 自身包裹的错误(或 classify() 抛错)settle 所有 queued-but-unsettled 分派为可见错误、清空队列、复位 exclusive、记 warn,**绝不外泄 unhandled rejection**。
96
- - `PendingDispatch` 新增 `fail(error)`:把 binding promise settle 为 `{ isError: true, message }`(经 `settleError` → `settle`,kernel 转 `ToolCallError`)。
97
- - `start()` / `commit()` 各自 try/catch:prepare 失败、dispatch body rejection、finalize/finish 失败都 settle 自己的 binding,不再向 lane 抛。
98
- - `binding()` 内 `registry[TOOL_RUNTIME_SCHEDULER]` 判空:undefined 时抛带上下文的 loud 错误(点名 symbol、指向 harness 挂载接线),替代裸 `undefined.prepare` TypeError。
99
- - 回归测试 3 个(`test/bridge.spec.ts`,全量 399 绿含 3 新):view 缺 symbol → cell 收到含 `TOOL_RUNTIME_SCHEDULER` 的 loud 错误;`scheduler.prepare` 抛错 → binding settle 为可见错误;classify 抛错 → lane backstop settle 且进程存活。
100
-
101
- ### 六.3 根因探针(本轮结论,待活体 daemon 复核)
102
-
103
- - **根因已锁定并更正(2026-09-02 活体探针 + 受控实验;推翻本节 2026-09-01 的 shadow 分支推断)**:真实根因是**部署拓扑 dual-copy**——崩溃期(2026-09-01 17:39–18:02)部署位插件嵌套 `node_modules/@deepseek-ai/*` 的 17 条 symlink(已退役 dev 接线)指向 dsh-alpha 源码树,插件 import 的 `TOOL_RUNTIME_SCHEDULER` 来自 alpha 副本的 dsh-tools 模块实例;daemon 的 `ToolRuntime` 实例由 host vendored 副本构建,其 symbol 字段以 host 副本 symbol 为键。两 symbol 为不同 `Symbol()` 实例:跨副本读实例字段 → `undefined`(`executionMode` 等字符串键方法不受影响、照常工作——症状逐字吻合)。
104
- - 证据链(2026-09-02):① 活体 4 探针(`tool.bash` fg/bg、`tool.read`、`tool.subagent` bg:false/bg:true)经 `eval` cell 路径 4/4 真实结果——现行生产形态 symbol 在位;② 部署位 `require.resolve('@deepseek-ai/dsh-tools')` = `/home/u1/.local/lib/node_modules/@deepseek-ai/dsh/node_modules/@deepseek-ai/dsh-tools/lib/index.js`(host vendored,③→④ 层);③ 受控实验:host 与 z_dsh-alpha 两副本 `TOOL_RUNTIME_SCHEDULER` 严格不等,host 键实例经 alpha symbol 读 → `undefined`、经 host symbol 读 → object;④ 时间线闭环:改名 `dsh-alpha → z_dsh-alpha`(2026-09-01 22:29)→ symlink 悬空 → 2026-09-02 清理删除 → daemon 重启 → 自愈。
105
- - shadow 分支理论为何被否:它与症状吻合但从未被活体证实,且无法解释「拓扑清理 + 重启后自愈」;dual-copy 机制同时解释故障与恢复。v0.2.1c 报告引「symbol 严格相等」的排除测量与崩溃期拓扑矛盾(疑测同一副本两次或取于改名后),以直接证据为准。
106
- - cordis traceable proxy 本身无嫌疑(symbol 转发分支在位;现行活体 plain read 即返回带 symbol 原生实例)。
107
- - dashr 侧交付边界:六.1+六.2(不崩溃 + loud 错误)继续有效;dual-copy 已由部署拓扑收敛消除(部署纪律:插件 `node_modules/@deepseek-ai/` 只留 `schemastery`+`cosmokit`);v0.2.1d 给 guard 错误附 dsh-tools 解析路径实现事件自诊断。
108
- ### 六.4 生产形态挂载测试(说明)
109
-
110
- - 三个回归用例以「缺 symbol 视图」「prepare 抛错」「classify 抛错」三种形态驱动 `createRunCellTool`,覆盖六.1+六.2 全部防御面;测试组合(根作用域原生实例)本就 symbol 在位,断言 `runtimeCtx.tools[TOOL_RUNTIME_SCHEDULER]` 定义的用例属于测试组合自身形态(根挂载原生实例),无法复现生产 realm 差异——该差异须在活体 daemon 上复核(六.5)。
@@ -1,14 +0,0 @@
1
- # Bridge the tool layer, not the service layer
2
-
3
- DASHR's `rlm()` family dispatches upstream delegation tools through the tool registry (nested sub-dispatch), not `ctx.subagents` service methods. The tool layer carries the deployment's enforcement surface — approval pipeline, sandbox policy, per-instance config (maxDepth, backgroundMode, persona) — that a direct service call would silently bypass. The cost: per-call model selection is impossible, because the tool schema exposes no `model` parameter; `rlm(model=...)` from 0.1.4 is dropped, and `subagentModel` degrades to a static `agentOptions.model` in the preset patch.
4
-
5
- ## Considered Options
6
-
7
- - **Service layer direct** (0.1.4's approach): `ctx.subagents.start()` with a hand-built request. Full control over request fields (including `agentOptions.model` and `maxDepth`), but every policy the tool instance would have applied must be re-implemented or lost.
8
- - **Tool layer nested dispatch** (chosen): `rlm("spawn")` executes the registry's `subagent` tool with a parent token. Upstream policy is inherited wholesale; the schema boundary is the tool's own contract.
9
-
10
- ## Consequences
11
-
12
- - `rlm(mode, prompt, *, label, run_in_background)` — no `model` kwarg. Parent-model inheritance is the default; a different child model requires a preset patch, not a call argument.
13
- - Depth enforcement comes from the tool instance's `maxDepth` config, patched to 10 in the preset (see `dev/kernel-refactoring/V0.1.5-development-plan.md` Q22/Q23).
14
- - The upstream delegation tools (`subagent`, `subagent_fork`, `interrupt_agent`) must stay registered and executable even though the model never sees their names — which is why masking is presentation-only (ADR-0002).
@@ -1,15 +0,0 @@
1
- # Masking is presentation-only
2
-
3
- Hiding exactly two upstream A2A tool names (`send_message` — the parent→child downlink — and `report` — the child→parent uplink) from the model is done by excluding them from the generated Tool Catalog text and from the kernel binding names — nothing else. The two tools stay registered, executable, and dispatchable; the single `send_message` bridge dispatches them internally. Every OTHER upstream delegation tool (`subagent`, `subagent_fork`, `list_agents`, `interrupt_agent`, `workflow`, `ralph`) is exposed directly as a native `tool.*` member — no re-wrapping (v0.1.9).
4
-
5
- ## Considered Options
6
-
7
- - **`restrict()` at runtime**: hides names in the registry's model-facing view, but validates against the live view at call time — ordering hazards against late tool registration can fail the whole preset mount.
8
- - **`disabled: true` include patches**: physically unregisters the tool, which also removes the bridge's dispatch target. Masking must not break the bridge.
9
- - **Presentation-only exclusion** (chosen): the registry is never touched. The model's surface (wire schema collapse to `eval`, Tool Catalog text, kernel bindings) is entirely DASHR-generated, so exclusion happens at the two points DASHR owns.
10
-
11
- ## Consequences
12
-
13
- - The masked tools remain in the registry and are reachable via nested sub-dispatch with a parent token, which passes the model-direct guard.
14
- - **Field-verified (v0.1.8d, `both` presentation mode)**: reachable by MODEL-DIRECT native call too — no parent token needed. A probe calling the masked `skill({"name":…})` through the API function-call surface executed in full. Cause: the mask registers no visibility filter, so the name stays in `view(scope).visible`, and `resolveExecution` collapses model-direct calls only under `code` mode. The mask is an ADVERTISING cut, not an enforcement point; the hard gate is the REPL binding allowlist (`unknown binding`). If a deployment ever needs true model-direct rejection, the mechanisms are a visibility-layer `restrict()` (rejected here for ordering hazards) or a `code`-mode collapse — both are deployment-level decisions, not mask-level ones.
15
- - Zero upstream mutation means zero interference with host-plane modules that enumerate or interact with the delegation tools.