dsh-ptc-plus 0.4.2 → 0.4.8

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 (89) hide show
  1. package/README.en.md +170 -76
  2. package/README.md +171 -77
  3. package/assets/dsh-ptc-plus-banner-en.webp +0 -0
  4. package/assets/dsh-ptc-plus-banner-zh.webp +0 -0
  5. package/assets/ptc-plus-icon.svg +5 -0
  6. package/client.js +599 -310
  7. package/compiler-core.cjs +162 -160
  8. package/compiler-core.cjs.map +3 -3
  9. package/docs/README.md +4 -1
  10. package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +49 -11
  11. package/docs/adr/0008-define-agent-operating-contract.md +2 -2
  12. package/docs/adr/0010-session-log-derived-recovery-tips.md +7 -0
  13. package/docs/adr/0016-archive-resolved-review-findings.md +3 -3
  14. package/docs/adr/0017-track-the-latest-dsh-public-surface.md +5 -1
  15. package/docs/adr/0019-plugin-settings-and-kill-switch.md +3 -3
  16. package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +3 -1
  17. package/docs/adr/0021-separate-host-bootstrap-from-session-runtime.md +14 -3
  18. package/docs/adr/0023-global-user-bindings.md +1 -1
  19. package/docs/adr/0026-persist-the-delivery-verdict-against-the-verified-candidate.md +23 -7
  20. package/docs/adr/0027-run-the-session-kernel-in-a-killable-helper-process.md +4 -0
  21. package/docs/adr/0028-attach-to-the-host-ptc-execution-seam.md +26 -6
  22. package/docs/adr/0030-serve-both-typert-codec-generations-from-one-codec.md +2 -2
  23. package/docs/adr/0031-close-semantic-obligations-by-owner.md +29 -0
  24. package/docs/adr/README.md +4 -4
  25. package/docs/architecture.md +8 -5
  26. package/docs/binding-review-placement-design.md +1 -1
  27. package/docs/client-ui.md +8 -4
  28. package/docs/durability-design.md +7 -7
  29. package/docs/installation.md +48 -12
  30. package/docs/publishing.md +4 -4
  31. package/docs/release-notes.md +109 -0
  32. package/docs/runtime-reference.md +25 -12
  33. package/docs/semantic-validation.md +7 -3
  34. package/docs/user-bindings.en.md +7 -6
  35. package/docs/user-bindings.md +6 -5
  36. package/docs/value-wire.md +1 -1
  37. package/docs/verification.md +43 -2
  38. package/index.js +39 -34
  39. package/internal/async-local-storage-intrinsics.js +12 -7
  40. package/internal/callable-source-facts.js +37 -1
  41. package/internal/cell-analysis.js +64 -9
  42. package/internal/config-spec.js +1 -1
  43. package/internal/cordis-tools-owner.js +81 -15
  44. package/internal/direct-surface-owner.js +331 -7
  45. package/internal/edit-transport-owner.js +80 -20
  46. package/internal/execution-seam-compat.js +192 -30
  47. package/internal/kernel-worker.js +162 -72
  48. package/internal/message-sources.js +14 -0
  49. package/internal/module-compilation.js +2 -1
  50. package/internal/rejected-cell-editor.js +11 -21
  51. package/internal/repl-memory-projection.js +10 -0
  52. package/internal/repl-scope-normalizer.js +72 -35
  53. package/internal/repl-value-observer.js +141 -45
  54. package/internal/runtime-config.js +62 -2
  55. package/internal/runtime-intrinsics.js +3 -0
  56. package/internal/runtime-messages.js +6 -4
  57. package/internal/session-cell-executor.js +24 -1
  58. package/internal/session-journal-recovery.js +267 -67
  59. package/internal/session-journal.js +466 -40
  60. package/internal/session-log-view.js +5 -4
  61. package/internal/session-runtime.js +45 -21
  62. package/internal/session-state.js +7 -29
  63. package/internal/settings-compat.js +66 -1
  64. package/internal/source-position-map.js +19 -0
  65. package/internal/user-binding-console-worker.js +23 -9
  66. package/internal/user-binding-draft-projection.js +3 -2
  67. package/internal/user-bindings-owner.js +2 -1
  68. package/internal/user-bindings.js +6 -1
  69. package/internal/validated-parse-repair.js +6 -2
  70. package/internal/value-wire.js +95 -30
  71. package/internal/worker-client.js +99 -8
  72. package/internal/worker-cwd-virtualization.js +10 -9
  73. package/internal/worker-output-capture.js +187 -0
  74. package/internal/worker-output-fence.js +26 -0
  75. package/internal/worker-realm-surfaces.js +197 -0
  76. package/internal/worker-repl-realm.js +10 -4
  77. package/package.json +42 -4
  78. package/src/client-activity.js +4 -1
  79. package/src/client-authoring-view.js +121 -62
  80. package/src/client-console.js +4 -2
  81. package/src/client-copy.js +4 -2
  82. package/src/client-feature-gates.js +7 -9
  83. package/src/client-host-compat.js +104 -27
  84. package/src/client-repl-view.js +3 -1
  85. package/src/client-styles.js +12 -1
  86. package/src/client-tool-view.js +3 -1
  87. package/src/client-transport.js +2 -2
  88. package/src/client-workbench.js +10 -7
  89. package/src/client.js +56 -17
@@ -6,7 +6,7 @@ Each top-level `run_code` executes a JavaScript/TypeScript cell with top-level `
6
6
 
7
7
  With the default `bindingUpdates: 'stateful'`, declarations and assignments update one logical identity in the same scope and activation, including local const bindings. An existing closure observes later updates, while a saved function value keeps its own identity. Separate block, function and iteration scopes stay separate. A bare `let x`, `var x` or `const x` preserves an existing value and creates undefined when new. Each declarator publishes its complete candidate only after its initializer and binding pattern succeed. Earlier declarators and ordinary assignment effects remain real if a later operation fails. Unreached lexical declarations do not reserve a name for future cells.
8
8
 
9
- An import alias follows its module until an actual successful assignment or declaration supplies a local value. Re-importing switches that same identity back to the module, so existing new-generation closures observe the change. Named default declarations associate `__default` with the local identity until independently overwritten. Static dependencies link before the body and keep their effects even if a later dependency fails.
9
+ An import alias follows its module until an actual successful assignment or declaration supplies a local value. A static import in a later cell switches that same identity back to the module, so existing new-generation closures observe the change. Within one cell, all static imports install their sources before the body; an ordinary declaration or assignment in the body can override them regardless of where the import appears in the text. Named default declarations associate `__default` with the local identity until independently overwritten. Static dependencies link before the body and keep their effects even if a later dependency fails.
10
10
 
11
11
  `bindingUpdates: 'protected'` retains native local restrictions, rejects cross-cell redeclarations, and protects newly created const/import bindings. Switching policy neither resets the worker nor freezes earlier writable identities. Historical cells replay under their recorded language generation; earlier closures retain that generation's behavior. See [ADR 0025](adr/0025-use-versioned-logical-binding-identities.md) for the ownership and compatibility contract.
12
12
 
@@ -20,9 +20,11 @@ For PTC mode requests using the code-only direct-tool projection, PTC Plus prese
20
20
 
21
21
  `edit_run_code` is a real registered tool call. Session history and later model requests retain its original name and delta arguments. The plugin executes the materialized source as a host-derived `run_code`, returns only the edit status, value, and logs to the model, and keeps the complete source plus journal in private replay metadata. It does not rewrite assistant history or emit edit-specific runtime context.
22
22
 
23
- When a live parse error occurs exactly at the cell EOF, PTC Plus checks the three single-closing-token corrections `}`, `)`, and `]` with the same preparation context. If exactly one correction succeeds, the existing editor can express it from a bounded unique suffix, and the rejected call has a persistent event identity, `PTC-C001` includes a directly callable `edit_run_code({ edits: [...], expected_target_call_seq })` invocation. The guard must match the target captured when the edit call is persisted, so a later editable cell suppresses execution even when the same literal replacement would match it. The plugin does not apply or execute the correction automatically; ambiguous, broader, and unbound repairs keep the ordinary length-adaptive guidance.
23
+ One provider failure has a bounded transport recovery. When the provider reports the exact invalid-tool-JSON `MALFORMED_RESPONSE` as a terminal chunk or stream exception, PTC Plus changes that terminal reason to `tool-calls`; this error cannot terminate the whole turn. DSH already preserves an unparseable argument string and routes it through ordinary validation, tool execution/result recording, and the next model step. A response is trusted for call preservation only when its buffered body consists entirely of tool-call-specific framing plus at most one `usage` event. If such a response contains exactly one internally consistent declared `run_code` or `edit_run_code` block, PTC Plus also tests deletion of one final `}`. It applies that correction only when the resulting value is an object accepted by the live direct-tool schema. The call keeps its index, identity, and name and proceeds through ordinary DSH validation, recording, scheduling, authority, and execution. Without a unique correction, trusted internally consistent complete blocks retain their original identity and argument bytes for the ordinary rejected-tool-result path; PTC Plus re-emits canonical start/delta/end framing so every tolerated provider variant passes DSH's stream invariant. If a tool block is incomplete or inconsistent, none was emitted, multiple usage events appear, or any other event occurs in the buffered body, PTC Plus closes any already visible text or reasoning block from its accumulated content, removes buffered unsafe block material, and emits one deterministic `run_code` call whose argument string cannot parse as a JSON object. DSH rejects it during ordinary schema validation, so no partial model call executes and the model still receives another step. Valid streams and other provider failures retain their original result. This is not a general JSON repair mechanism.
24
24
 
25
- Use `return` for an explicit result and `console` for logs. Native expression completion can also supply a result value:
25
+ When a live parse error occurs exactly at the cell EOF, PTC Plus checks all 12 suffixes of one or two closing tokens drawn from `}`, `)`, and `]` with the same preparation context. If exactly one correction succeeds, the existing editor can express it from a bounded unique suffix, and the rejected call has a persistent event identity, `PTC-C001` includes a directly callable `edit_run_code({ edits: [...], expected_target_call_seq })` invocation. The guard must match the target captured when the edit call is persisted, so a later editable cell suppresses execution even when the same literal replacement would match it. The plugin does not apply or execute the correction automatically; ambiguous, broader, and unbound repairs keep the ordinary length-adaptive guidance.
26
+
27
+ Use `return` for an explicit result and the native Node `console` surface for logs. Its formatting methods, including `dir` options, tables, counters, groups, timers, assertions, and traces, retain Node semantics. `process.stdout/stderr.write`, direct fd 1/2 writes and inherited child stdio are also captured. Console entries retain call order and each descriptor pipe retains byte order; the merged log does not claim a total chronology across stdout, stderr and the private control channel. All forms share the cell's aggregate output budget. Native expression completion can also supply a result value:
26
28
 
27
29
  ```ts
28
30
  const records = [{ id: 1 }, { id: 2 }]
@@ -31,6 +33,8 @@ return records.map(record => record.id)
31
33
 
32
34
  Parse, filter, and aggregate large results before returning them. Aggregate log/return overflow terminates the worker and discards the cell; its bindings are not available for a later reduction. An encoding-budget rejection (`PTC-O001`, `invalid-output`) instead keeps the worker live, so an already initialized binding can be reduced in a later cell. Neither path undoes external effects.
33
35
 
36
+ Each physical cell has acknowledged start and end fences on stdout and stderr. Descriptor bytes arriving inside that window belong to the current physical run. The first output observed outside a round, or structured output whose async-local scheduling cell has already settled, produces an output-attribution diagnostic, closes capture and resets the worker rather than disappearing or being silently attached to another cell. If no cell is active, the Host retains that failure and rejects the next execution once instead of continuing to drain an unbounded background producer. Bare descriptor writes do not carry callback provenance; when they arrive inside a later acknowledged window, only that physical-arrival attribution is knowable.
37
+
34
38
  Prefer executing work directly in the current cell; reserve `code.run` for source already held as data. When nested source must be written inline, escape quotes, backslashes, and newlines exactly — its parse errors point only at the generated text, not at a position in the outer cell.
35
39
 
36
40
  ## Capability Discovery
@@ -59,6 +63,8 @@ The configuration uses `ptc-plus` as the DSH plugin ID and `dsh-ptc-plus` as the
59
63
 
60
64
  `autoDescribeRunCode` defaults to `true` and appears as “Allow run_code execution without a summary.” When enabled, a call that omits the outer `run_code.description` uses derived arguments for local DSH validation, and the fallback summary enters presentation metadata only. When disabled, DSH validates the original arguments. Both states expose byte-identical model requests with the required `description` declaration; original call arguments, existing summaries, cell source, and nested native-tool JSON remain unchanged.
61
65
 
66
+ Missing-description diagnostics append a complete DSH user message to `additionalContexts`, with an identity, text content and a `plugin:ptc-plus` notice source. The diagnostic distinguishes outer transport arguments from nested native-tool arguments and preserves existing additional messages. These notices are ordinary inbox messages, separate from recovery-tip escalation and state snapshots. This prevents new malformed diagnostic records; it does not repair session files already written by older plugin versions.
67
+
62
68
  ```yaml
63
69
  - id: ptc-plus
64
70
  name: dsh-ptc-plus
@@ -71,7 +77,7 @@ The configuration uses `ptc-plus` as the DSH plugin ID and `dsh-ptc-plus` as the
71
77
  durableReplay: true
72
78
  tipsEnabled: true
73
79
  cordisToolsEnabled: false
74
- userBindingsEnabled: false
80
+ userBindingsEnabled: true
75
81
  computeMs: 60000
76
82
  maxWallMs: 600000
77
83
  maxOldGenerationSizeMb: 512
@@ -87,9 +93,11 @@ The configuration uses `ptc-plus` as the DSH plugin ID and `dsh-ptc-plus` as the
87
93
 
88
94
  The five legacy binding and module switches remain migration inputs. Complete enabled/protected choices map to the corresponding new policy; mixed old choices retain their effective controls until the user explicitly selects the new switch. The Host schema preserves an omitted `bindingUpdates` value until migration, and the settings card resolves the same policy. Selecting the switch also clears the compatibility marker.
89
95
 
90
- `enabled: false` is the settings-based kill switch: the Host keeps the settings namespace, client card, and passive cleanup that can withdraw old PTC declarations at the next permitted accepted step. It removes execution hooks, prompt sections, and tool surfaces without reading binding storage. Every setting is applied live when its owner can reconcile it. A submitted cell retains one configuration snapshot through preflight, execution, binding calls, completion validation, and diagnostics; an update accepted while it runs applies to cells submitted afterward. Node fixes a worker's V8 old-generation limit at creation, so changing `maxOldGenerationSizeMb` while a session worker is active is rejected and rolled back rather than reported as applied. Other non-`enabled` updates reconfigure existing owners without replacing session-bound bindings. The settings card renders in whichever settings seat the installed DSH declares — the side-bar **Plugins** page's per-bundle configuration on the current release, Settings → Plugin configuration on the preceding one ([ADR 0019](adr/0019-plugin-settings-and-kill-switch.md)) — shows “已启用” or “已停用”, and disables every control except `enabled` while the plugin is off. If a live enable or reconfiguration cannot install a complete runtime, PTC Plus unwinds or restores every owner and persists the last applied settings; a failed compensating settings write is surfaced as an activation diagnostic. The TypeScript language check is deferred until runtime activation, so a non-TypeScript host can load the plugin in disabled mode but cannot enable it.
96
+ `enabled: false` is the settings-based kill switch: the Host keeps the settings namespace, client card, and passive cleanup that can withdraw old PTC declarations at the next permitted accepted step. It removes execution hooks, prompt sections, and tool surfaces without reading binding storage. Every setting is applied live when its owner can reconcile it. A submitted cell retains one configuration snapshot through preflight, execution, binding calls, completion validation, and diagnostics; an update accepted while it runs applies to cells submitted afterward. Node fixes a worker's V8 old-generation limit at creation, so changing `maxOldGenerationSizeMb` while a session worker is active is rejected and rolled back rather than reported as applied. Other non-`enabled` updates reconfigure existing owners without replacing session-bound bindings. The settings card renders in whichever settings seat the installed DSH declares — the sidebar **Plugins → Installed** page per-bundle configuration (`plugins.row.config`), or the preceding Host's **Settings → Plugin configuration** card (`settings.plugin.item`) ([ADR 0019](adr/0019-plugin-settings-and-kill-switch.md)) — shows “已启用” or “已停用”, and disables every control except `enabled` while the plugin is off. The card does not register in General settings; the sparkle menu opens the same complete form in an application dialog. If a live enable or reconfiguration cannot install a complete runtime, PTC Plus unwinds or restores every owner and persists the last applied settings; a failed compensating settings write is surfaced as an activation diagnostic. The TypeScript language check is deferred until runtime activation, so a non-TypeScript host can load the plugin in disabled mode but cannot enable it.
91
97
 
92
- `cordisToolsEnabled: true` atomically mounts the official `@deepseek-ai/dsh-tool-cordis` plugin and exactly the shipped `cordis-plugin-development` companion Skill in agent scopes that expose the `run_code` and `skill` tools plus the public preset and Skill services. PTC Plus resolves the official `cordis` preset through DSH's public preset service, lets the maintained filesystem provider own its Skill root, and filters that provider at the public registration boundary so sibling Skills are never published to the PTC scope. It does not copy the Skill or switch the agent's preset. The first request waits until the Cordis tool fiber, owner guidance, exact Skill provider, and scoped Skill load are all ready. An agent scope that omits one of those surfaces is left for later: the owner publishes nothing there, reports nothing, and retries when the tool surface changes or the next prompt is assembled. A scope whose exposed preset, Skill/tool service, declared Cordis service, inspect manifest, or activation contradicts the host contract fails the setting change at installation and removes every provisional contribution; reached through the host's own `agent/created` or assemble call it withdraws from that scope with one warning instead, so this plugin's unavailability never fails an agent another plugin created ([ADR 0029](adr/0029-keep-plugin-unavailability-out-of-the-host-agent-lifecycle.md)). Disable and agent/runtime disposal remove both fibers. Native agents inherit neither the resulting `tools.*` members nor the Skill, and the code-only direct-tool projection remains `[run_code, edit_run_code]`. Cordis can evaluate model-written plugins against the live runtime, so enabling it grants shell-equivalent trust.
98
+ `cordisToolsEnabled: true` atomically mounts the official `@deepseek-ai/dsh-tool-cordis` plugin and exactly the shipped `cordis-plugin-development` companion Skill in agent scopes that expose the `run_code` and `skill` tools plus the public preset and Skill services. PTC Plus resolves the official `cordis` preset through DSH's public preset service, lets the maintained filesystem provider read its Skill root as an immutable bundled Host resource, and filters that provider at the public registration boundary so sibling Skills are never published to the PTC scope. It does not copy the Skill or switch the agent's preset. The first request waits until the Cordis tool fiber, owner guidance, exact Skill provider, and scoped Skill load are all ready. An agent scope that omits one of those surfaces is left for later: the owner publishes nothing there, reports nothing, and retries when the tool surface changes or the next prompt is assembled. A scope whose exposed preset, Skill/tool service, declared Cordis service, inspect manifest, or activation contradicts the host contract fails the setting change at installation and removes every provisional contribution; reached through the host's own `agent/created` or assemble call it withdraws from that scope with one warning instead, so this plugin's unavailability never fails an agent another plugin created ([ADR 0029](adr/0029-keep-plugin-unavailability-out-of-the-host-agent-lifecycle.md)). Disable and agent/runtime disposal remove both fibers. Native agents inherit neither the resulting `tools.*` members nor the Skill, and the code-only direct-tool projection remains `[run_code, edit_run_code]`. Cordis can evaluate model-written plugins against the live runtime, so enabling it grants shell-equivalent trust.
99
+
100
+ The Skill root uses the preset's absolute composition path when that generation publishes one. A pathless generation is resolved through the active Host's public `pluginPackages.packageOf` service, which returns the official singular or plural preset package from that installation rather than an ambient package reachable from the plugin profile. An older generation with no such service may resolve only its historical plural package relative to the Host's `ctx.baseUrl`; the fallback is disabled whenever `pluginPackages` exists. The exact `cordis-plugin-development/SKILL.md` must exist before the mount begins. An invalid published path remains authoritative and fails instead of falling through to another generation's package layout.
93
101
 
94
102
  Keep large Cordis host or client source in a top-level binding before calling a Cordis tool. If that call rejects while the worker stays live, an initialized binding can be reused in a short continuation. Decide whether to retry from the operation owner's retry/idempotence contract and available execution facts; a thrown call or a smaller cell does not prove that no effect occurred.
95
103
 
@@ -97,7 +105,7 @@ Cordis Plugins and Runs are process-local. On a resumed agent or after Cordis is
97
105
 
98
106
  ## Global User Bindings
99
107
 
100
- `userBindingsEnabled` defaults to `false`. While disabled, PTC Plus does not read binding storage, add new declarations, inject values, register the management RPC or `/binding` command, or render Global User Binding controls. The next permitted accepted step can withdraw previously delivered declarations. Enabling it loads the single document at `$DSH_HOME/ptc-plus/bindings.json`; writes use a file lock, atomic replacement, owner-only file permissions, and an expected process-local revision. A stale revision or an external file change rejects the mutation and requires reload rather than overwriting newer content. A damaged document produces an explicit catalog error and remains unwritable until the user repairs it and reloads.
108
+ `userBindingsEnabled` defaults to `true`; an explicit `false` is preserved. Omitted settings use the current default, including after an upgrade. Individual entry states and user-controlled draft saving are unchanged. While disabled, PTC Plus does not read binding storage, add new declarations, inject values, register the management RPC or `/binding` command, or render Global User Binding controls. The next permitted accepted step can withdraw previously delivered declarations. Enabling it loads the single document at `$DSH_HOME/ptc-plus/bindings.json`; writes use a file lock, atomic replacement, owner-only file permissions, and an expected process-local revision. A stale revision or an external file change rejects the mutation and requires reload rather than overwriting newer content. A damaged document produces an explicit catalog error and remains unwritable until the user repairs it and reloads.
101
109
 
102
110
  Each entry has a stable `id`, display `name`, `namespace` or `top-level` scope, selected symbols, one-line purpose, enabled state, and TypeScript source. A namespace name must be a JavaScript identifier; a top-level entry uses its exported symbols as call identifiers and keeps `name` for display. Source must provide named value exports; default exports and re-exports are rejected. An omitted symbol list derives every named value export, while an explicit list limits the exposed API; clearing the Settings symbol field requests fresh derivation from the edited source. The parser derives declaration kinds, parameter and return annotations, bounded inferred types, and model-visible declarations from the selected source exports. A `namespace` entry contributes one object binding under its name, while a `top-level` entry contributes each selected export. Reserved REPL names and duplicate active identifiers fail validation. The document accepts at most 64 entries, 65,536 UTF-16 code units per source, 262,144 across all sources, and 16,384 across active declarations. Mutations validate the complete enabled declaration set before atomic replacement; an externally written document that exceeds the limit is reported as damaged storage and contributes no active entries.
103
111
 
@@ -123,18 +131,20 @@ Every completed cell records the exact source-derived active snapshot in private
123
131
 
124
132
  `durableReplay: false` starts new kernels without recorded REPL state while preserving live bindings in the current process. It does not delete session logs.
125
133
 
126
- With `durableReplay: true`, recovery claims only bindings backed by both verified journal ancestry and exact provenance available to the model that produced the current call. DSH's append-only event log proves reconstructability; its ordered model-visible surface proves awareness. Raw events shadowed from that surface and UI-only `dshPtcPlusBindings` metadata do not make a binding model-knowable. A result-only replacement may retain a cell when the assistant call containing its source remains visible. If compaction shadows that call, the live worker and cold recovery contract the affected state unless an explicitly selected bounded structured projection exposes the exact bindings in model context. Such a projection is not injected by default merely to retain hidden state. Natural-language summaries are not parsed as state evidence.
134
+ With `durableReplay: true`, recovery claims only bindings backed by both verified journal ancestry and exact provenance available to the model that produced the current call. DSH's append-only event log proves reconstructability; its ordered model-visible surface proves awareness. Before every subsequent cell in a structured host session, the runtime revalidates each existing live call against that exact surface even when `replaceGeneration` is unchanged. Raw events shadowed from the surface and UI-only `dshPtcPlusBindings` metadata do not make a binding model-knowable. A result-only replacement may retain a cell when the assistant call containing its source remains visible. If compaction shadows that call, the live worker and cold recovery contract the affected state unless an explicitly selected bounded structured projection exposes the exact bindings in model context. Such a projection is not injected by default merely to retain hidden state. Natural-language summaries are not parsed as state evidence.
127
135
 
128
136
  An invalid historical association, prune replacement, or recovery boundary is also rejected as evidence for its bindings; it is not by itself a reason to reject every later valid `run_code`. Recovery takes the intersection of the model-knowable and structurally verified frontiers, then discards cells and bindings that may depend on an unknown boundary. Because the current journal does not contain a complete per-binding dependency graph, the conservative unit is an affected cell and suffix unless stronger owned evidence exists. If no non-empty frontier is provable, the worker starts from an empty REPL and executes the current cell as a new root.
129
137
 
130
138
  The first continued cell reports `PTC-R002` once so the model can redeclare any missing bindings, and its result persists the contraction for later cold starts. Recovery never redispatches, reverses, or certifies effects from discarded history. This availability fallback applies only to historical PTC Plus recovery data; validation, policy, authority, approval, cancellation, and other failures of the current request retain their normal DSH behavior.
131
139
 
132
- PTC dynamic messages carry `source: { kind: 'plugin', plugin: 'ptc-plus', form: 'snapshot' | 'catalog' | 'notice' }`. Current-state snapshots replace only earlier PTC state, including historical PTC aggregate sections; they do not replace the binding API catalog, tasks, Skill instructions, tool results, or other producers. Only a later binding catalog replaces an earlier catalog, and an explicit empty catalog withdraws its API documentation. Host runtime-context clearance does not invalidate the catalog; suppression gates new delivery. Empty snapshots withdraw earlier state claims. Delivery uses the public accepted pre-step decision and honors `includeRuntimeContext`, scoped suppression, and cancellation through an empty assembly witness. Committed messages and the public ordered surface drive deduplication and reconciliation after compaction or resume. Proposals do not count as delivery; raw history does not establish model knowledge.
140
+ PTC dynamic messages carry `source: { kind: 'plugin:ptc-plus', form: 'snapshot' | 'catalog' | 'notice' }`. Current-state snapshots replace only earlier PTC state, including historical PTC aggregate sections; they do not replace the binding API catalog, tasks, Skill instructions, tool results, or other producers. Only a later binding catalog replaces an earlier catalog, and an explicit empty catalog withdraws its API documentation. Host runtime-context clearance does not invalidate the catalog; suppression gates new delivery. Empty snapshots withdraw earlier state claims. Delivery uses the public accepted pre-step decision and honors `includeRuntimeContext`, scoped suppression, and cancellation through an empty assembly witness. Committed messages and the public ordered surface drive deduplication and reconciliation after compaction or resume. Proposals do not count as delivery; raw history does not establish model knowledge.
133
141
 
134
142
  Recovery tips are disabled with `tipsEnabled: false`. When enabled, a bounded independent PTC notice identified by `tools:ptc-plus-tip/<trigger>/<ordinal>` may appear after a repeated binding failure or a diagnostic that identifies an executable, shell, or path problem in the current execution world. The trigger and per-trigger ordinal are reconstructed from committed PTC notices and canonical historical DSH snapshot sections; each identity counts once across both sources, and visible wording does not advance history. A tip is subject to `tipCooldownMessages` and becomes detailed only after `tipEscalationFailures` unresolved matches; it never changes the code-only direct-tool list or schema. `edit_run_code` does not emit a runtime context because its real call and result already carry the relevant fact.
135
143
 
136
144
  Static and dynamic imports retain Node's resolver from the session cwd, falling back to the worker cwd when none was recorded. Relative files, bare packages, conditional exports, URLs and attributes retain their Node meaning. A missing requested export fails at static linking before that target module evaluates. Namespace property writes still follow the namespace object's own contract; updating an alias does not mutate the namespace. A managed namespace's members are readonly: a member write is refused and leaves the member's live value unchanged, while reflection continues to report that live value. `Reflect.set` returns `false`, and `Object.defineProperty` with a different value throws `TypeError`; a direct assignment from a non-strict cell body is silently discarded, exactly as for any other readonly property.
137
145
 
146
+ Current `stateful-v1` and `protected-v1` cells expose one mutable, null-prototype `import.meta` object per cell. Its `url` is the file URL of the synthetic `<session cwd>/repl` parent, `filename` is that synthetic path, and `dirname` is the recorded session cwd; when no cwd was recorded, all three use the worker cwd. Relative `new URL(..., import.meta.url)`, static import, dynamic import and provided `require` therefore start from the same parent. Frozen `legacy-v1` cells retain their historical `import.meta` parse rejection. Cell source otherwise has a deliberate CommonJS-like subset: `require`, `require.resolve` and `require.cache` exist; `require.main` is `undefined`; ambient `__dirname`, `__filename` and `module` do not exist. Use `import.meta.dirname`, `import.meta.filename` and `import.meta.url` for cell path metadata.
147
+
138
148
  Cell export modifiers retain local declarations; remote re-exports retain dependency loading, type-only exports are erased, and default values use the ordinary `__default` identity. Real modules expose their actual export names through a stable PTC-managed namespace. Member reads follow live sources until actual local override and return original function/object values. A namespace retained by an external function continues to provide live reads. Its readonly properties do not make it a native module namespace object. Opaque code independently using native import on the raw compiled URL, or its own native createRequire, can see the transport representation and does not receive PTC's writable-export interface. PTC-provided imports and require, binding modules, workbench commands and `code.run` children share the managed interface with independent state and lifecycle. The complete contract and acceptance conditions belong to [ADR 0025](adr/0025-use-versioned-logical-binding-identities.md#managed-module-interface).
139
149
 
140
150
  User-selected dynamic compilers retain their native syntax goals and results. Their environments remain part of the source contract: direct eval resolves the caller's logical scope, while indirect eval and Function constructors use their owning realm's root. Environment adaptation must preserve those relationships, ordinary shadowed functions and actual effects. A valid call failing because it sees generated storage names is an implementation defect.
@@ -145,13 +155,13 @@ Intrinsic interfaces belong to their source generation. Source selects the ownin
145
155
 
146
156
  Rewrites are recorded as `meta.dshPtcPlusRewrites` on the tool result (parallel to the journal, a closed schema). Settled rewrites do not emit redundant runtime context: failures already carry execution and retry guidance in their result. Only when the rewritten cell has no valid journal does `tools:ptc-plus-rewrite-info` describe unknown completion in the next model request.
147
157
 
148
- `require(...)` is classified exactly like a dynamic import: allowlisted builtins (`node:assert`, `node:buffer`, `node:querystring`, `node:string_decoder`, `node:stream`, `node:util`, `node:url`, `node:zlib`) stay durable, other modules are volatile, `node:worker_threads`/`node:cluster` are rejected before execution (`PTC-C002`), and non-literal arguments are dynamic module resolution.
158
+ `require(...)` is classified exactly like a dynamic import: allowlisted builtins (`node:assert`, `node:buffer`, `node:querystring`, `node:string_decoder`, `node:stream`, `node:util`, `node:url`, `node:zlib`) stay durable, other modules are volatile, `node:worker_threads`/`node:cluster` are rejected during preflight (`PTC-C002`), and non-literal arguments are dynamic module resolution. This rejection occurs before any cell statement executes, so a surrounding `try`/`catch` cannot catch it.
149
159
 
150
160
  ## Diagnostics
151
161
 
152
162
  | Code | Meaning | State effect |
153
163
  | --- | --- | --- |
154
- | `PTC-C001` | The cell cannot be parsed | Not executed; REPL unchanged; a uniquely validated and target-bound single-token EOF closure includes a directly callable declared `edit_run_code` repair; otherwise retry a short corrected cell with `run_code`, use edit for a localized long-cell correction, and resend with `run_code` for broad, ambiguous, or unbound repairs |
164
+ | `PTC-C001` | The cell cannot be parsed | Not executed; REPL unchanged; a uniquely validated and target-bound one- or two-token EOF closure includes a directly callable declared `edit_run_code` repair; otherwise retry a short corrected cell with `run_code`, use edit for a localized long-cell correction, and resend with `run_code` for broad, ambiguous, or unbound repairs |
155
165
  | `PTC-C002` | Preflight rejected a kernel-control import | Not executed; REPL unchanged |
156
166
  | `PTC-N001` | Top-level binding conflict | Not executed; REPL unchanged |
157
167
  | `PTC-O001` | Unsupported or over-budget output | Cell executed; earlier mutations may exist |
@@ -181,7 +191,10 @@ Inside a cell, omitting the JavaScript argument of a native `tools` member is ac
181
191
  - The runtime is identified as `typescript`. Stateful and protected cells erase type-only syntax and lower enums, namespaces, parameter properties, and decorators through maintained TypeScript and decorator transforms. JSX is outside the cell syntax. Historical cells retain their recorded language generation.
182
192
  - `tools.read` is a bounded inspection window in the current DSH tool contract. Whole-file computation in `danger-full-access` should use `node:fs/promises.readFile` or streams and becomes `volatile`.
183
193
  - Relative Node filesystem entry points, including `node:fs/promises` helpers and globbing, resolve from the recorded session cwd. Absolute paths and explicit filesystem options remain unchanged; sessions without a recorded cwd use the worker's native cwd and are classified as volatile when the access is ambient.
194
+ - A recorded `process.cwd` and the `process.exit`/`abort`/`kill`/`chdir` lifecycle guards are immutable, non-configurable realm properties. Lifecycle calls throw `Error` with code `ERR_PTC_PROCESS_CONTROL`; assignment cannot replace these surfaces. Plugin-private `AsyncLocalStorage` receivers have immutable captured `getStore`, `enterWith`, and `run` own methods so user prototype changes cannot alter control state. Separately, during bootstrap only, PTC Plus synchronously intercepts the private Node REPL `AsyncLocalStorage.run` call, protects that one platform receiver, and restores the prototype in `finally` before user source or microtasks can execute.
195
+
196
+ `internal/worker-realm-surfaces.js` is the tracked exception registry and write boundary for worker globals, process properties and native prototypes. Every write is classified as a stable native divergence, temporary compiler name, per-cell program binding, user declaration or restoration. A stable write must cite a registry row with the same classification; it cannot borrow a temporary or language-only row. The cwd-projected `node:fs`, `node:fs/promises`, `node:path`, and `node:child_process` callable family is a separate stable surface from `process.cwd`; its verifier compares dynamically enumerated callable descriptors and retained callable properties with a plain Node worker. Private `AsyncLocalStorage` receiver methods are likewise a separate stable surface from the temporary prototype bootstrap. Stable rows name their semantic owner, native or transport reference and executable verifier; a structured source check rejects supported direct `defineProperty`, assignment and deletion forms that bypass the boundary. Module-language behavior remains owned by the compiler/executor contract even when its `require` installation uses the same boundary.
184
197
  - The durable import allowlist is `node:assert`, `node:buffer`, `node:querystring`, `node:string_decoder`, `node:stream`, `node:url`, `node:util`, and `node:zlib`. Other Node imports remain usable but make the cell volatile.
185
- - Direct `node:worker_threads` and `node:cluster` imports are rejected inside the worker. Calls to `process.exit`, `process.abort`, `process.kill`, and `process.chdir` are rejected through direct `process`, `require`, and dynamic or static `node:process` imports.
198
+ - Direct `node:worker_threads` and `node:cluster` references through static import, dynamic import, or unshadowed `require` are rejected during preflight, before any cell statement executes. Calls to `process.exit`, `process.abort`, `process.kill`, and `process.chdir` are rejected through direct `process`, `require`, and dynamic or static `node:process` imports.
186
199
  - Cold recovery replays journals from the session log; there are no compressed checkpoints or worker-LRU eviction.
187
200
  - DSH services or plugin APIs not exposed as a native tool or owner-provided program binding are not made callable through name-based reflection.
@@ -1,6 +1,10 @@
1
- # 编译语义验证
1
+ # 语义义务与编译验证
2
2
 
3
- `npm run test:semantics` 运行三个互补的确定性测试层。它们也由 `npm run verify` / `npm run check` 的常规测试发现机制执行,不需要网络或模型调用。定向命令用于快速诊断,不能替代完整检查、资源验收和独立审查。
3
+ [`semantic-obligations.json`](../semantic-obligations.json) 是插件自有语义边界的 tracked owner graph,[ADR 0031](adr/0031-close-semantic-obligations-by-owner.md) 定义其闭合范围。`npm run semantic:check` 要求 `internal/`、`src/`、`index.js`、`client.js` 与 journal migration entry 中的每个受管实现文件恰好归属一个义务,并核对 contract、可执行 evidence、依赖图、journal 连续代际以及 language/module 导出代际。报告分别统计 `preserved`、`intentional-difference`、`bounded-external` 与 `unknown`;只有 `unknown: 0` 才表示声明范围内闭合,不表示对全部 Node 或 DSH 行为的等价证明。
4
+
5
+ 每项义务必须记录 owner、实现边界、源 contract、可观察量、入口、生命周期、代际、平台边界、消费者、oracle 与 evidence。新增文件会直接使 source inventory 失败;现有 owner 内新增转换时,维护者还必须判断它是否改变当前义务、依赖或 oracle。能够从中央注册点机械闭合的边界继续使用更强检查,例如 realm mutation 的分类写入入口、journal schema 和 language/module generation set。该图不能替代具体反例、覆盖率或独立 review lane。
6
+
7
+ `npm run test:semantics` 运行三个互补的确定性编译测试层。它们也由 `npm run verify` / `npm run check` 的常规测试发现机制执行,不需要网络或模型调用。定向命令用于快速诊断,不能替代完整检查、资源验收和独立审查。
4
8
 
5
9
  | 层 | Oracle 与执行方式 | 代码 owner |
6
10
  | --- | --- | --- |
@@ -24,6 +28,6 @@ Test262 子集位于 `test/fixtures/test262/`。`manifest.json` 记录上游仓
24
28
 
25
29
  ## 发布验证边界
26
30
 
27
- 完整确定性检查与独立代码审查是提交门槛,不代表发布稳定性证明。发布前仍需补齐 Node 22/24 下连续会话、反复创建与释放 worker、覆盖插桩和资源压力组合的稳定性验证。进程级失败必须保留原始退出码、signal 与 stderr;仅有测试文件级 `test failed` 无法区分运行时缺陷、引擎退出或外部终止。
31
+ 完整确定性检查与有效的独立审查 lane 聚合是提交门槛,不代表发布稳定性证明。发布前仍需补齐 Node 22/24 下连续会话、反复创建与释放 worker、覆盖插桩和资源压力组合的稳定性验证。进程级失败必须保留原始退出码、signal 与 stderr;仅有测试文件级 `test failed` 无法区分运行时缺陷、引擎退出或外部终止。
28
32
 
29
33
  Windows Node 24/26 的 worker teardown native 崩溃已由插件自有 helper 进程边界限制在可回收子进程内(见 [ADR 0027](adr/0027-run-the-session-kernel-in-a-killable-helper-process.md));`npm run check` 现在执行一次确定性运行,不再根据 TAP 文件级 abort 自动重试。发布前仍须在受支持 Node 版本上保留原始退出码、signal 与 stderr,并覆盖连续创建/释放 helper、覆盖插桩和资源压力组合。文件级失败仍按实际 owner 修复,不得用重试掩盖。
@@ -6,10 +6,11 @@ Global bindings save reusable TypeScript helpers for use across sessions. For ex
6
6
 
7
7
  ## Create and use a binding
8
8
 
9
- 1. Open PTC Plus settings (when the sparkle shortcut is already present, choose **PTC Plus settings** from its menu; depending on the DSH interface, you can also find the plugin on the side-bar **Plugins** page and choose **Configure**, or open **Settings → Plugin configuration → PTC Plus**) and enable **Global User Bindings**.
10
- 2. In a PTC session, open the sparkle menu by the composer and choose **Write a new binding**, or enter `/binding new <requirement>`.
11
- 3. Inspect the draft's source, interface, and model prompt in the panel above the composer.
12
- 4. Choose **Save and enable**. Subsequent `run_code` calls load the binding; initialization failures appear in execution results.
9
+ Global bindings and the sparkle shortcut default to on; a new installation needs no setup switch. If an existing configuration explicitly disables global bindings, enable them under **Plugins → Installed → dsh-ptc-plus → Configure**. If only the shortcut is hidden, enable **Show the PTC Plus shortcut**. The feature switch does not change saved entries' enabled states.
10
+
11
+ 1. In a PTC session, open the sparkle menu by the composer and choose **Write a new binding**, or enter `/binding new <requirement>`.
12
+ 2. Inspect the draft's source, interface, and model prompt in the panel above the composer.
13
+ 3. Choose **Save and enable**. Subsequent `run_code` calls load the binding; initialization failures appear in execution results.
13
14
 
14
15
  During authoring, the Agent starts with a small in-memory check of the core behavior, then adds normal, edge, and failure assertions with `node:assert/strict`. Bulk tests report totals and a few representative failures instead of printing every successful case into the context. Tests must not modify external files or services. File and network helpers use in-memory substitutes to verify argument forwarding and error propagation; the answer identifies integration behavior that remains untested. Submission delivers a draft for your review. You decide whether to save and enable it.
15
16
 
@@ -24,9 +25,9 @@ A new draft opens above the composer. Click the title bar or use Enter/Space to
24
25
  - **Discard draft**: abandon this unsaved candidate.
25
26
  - **Close panel**: hide it without saving or discarding.
26
27
 
27
- The sparkle button shows a PTC Plus tooltip on hover or focus that names it as the Global User Binding entry; a pending draft changes the tooltip to the draft state. The button shows a badge for pending drafts. Open its menu by hovering, clicking, or using the keyboard, then select the draft to show it again. Each session retains one current candidate. A confirmed save or discard closes the panel; failures leave the draft available for retry.
28
+ The sparkle button shows a delayed PTC Plus tooltip identifying it as the global bindings and settings entry; a pending draft changes it to the draft state. The tooltip stays hidden while the menu, management dialog, or settings dialog is open. The button shows a badge for pending drafts. Open its menu by hovering, clicking, or using the keyboard, then select the draft to show it again. Each session retains one current candidate. A confirmed save or discard closes the panel; failures leave the draft available for retry.
28
29
 
29
- The sparkle menu is available before the first message and opens once the pointer dwells on the entry; incidental pointer movement across the composer leaves it closed. A hovered menu closes as soon as the pointer leaves it; clicking keeps it open until you click again, click elsewhere, or press Escape. Global bindings show their names, purposes, and enabled states using text and a checkmark. Click an entry to toggle it for all sessions. The catalog refreshes in the background, and the entries stay usable while it does; toggles are disabled only while saving, and reload after a conflict or failure before trying again. **Manage global bindings** opens the full workbench. **Write a new binding** and **Revise a binding** appear when the authoring command is available; the revision step lists existing entries and prefills `/binding edit <id> `. The reusable REPL binding list shows how many later cells reused each binding, with the total in the section title; the count comes from static references in each later cell's compiled source, and a redeclaration counts as neither a reuse nor a reset of the accumulated count.
30
+ The sparkle menu is available before the first message and opens once the pointer dwells on the entry; incidental pointer movement across the composer leaves it closed. A hovered menu closes as soon as the pointer leaves it; clicking keeps it open until you click again, click elsewhere, or press Escape. Global bindings show their names, purposes, and enabled states using status text and color. Click an entry to toggle it for all sessions. The catalog refreshes in the background, and the entries stay usable while it does; writes are serialized, so an entry stays usable during an in-flight write and a duplicate toggle is dropped rather than queued, and you reload after a conflict or failure before trying again. **Manage global bindings** opens the full workbench. **Write a new binding** and **Revise a binding** appear when the authoring command is available; the revision step lists existing entries and prefills `/binding edit <id> `. The reusable REPL binding list shows how many later cells reused each binding, with the total in the section title; the count comes from static references in each later cell's compiled source, and a redeclaration counts as neither a reuse nor a reset of the accumulated count.
30
31
 
31
32
  The original binding request retains its requirement, status, and accepted source in history. Historical inspection does not restore permission to save that draft. When another edit changes the catalog, reload its current state before deciding whether to save.
32
33
 
@@ -6,10 +6,11 @@
6
6
 
7
7
  ## 创建和使用
8
8
 
9
- 1. 打开 PTC Plus 设置(已有星光入口时可直接选择菜单中的 **PTC Plus 设置**;根据 DSH 界面,也可以在侧栏 **Plugins** 页面找到本插件并点 **Configure**,或打开 **设置 → 插件配置 → PTC Plus**),开启“全局用户绑定”。
10
- 2. 在 PTC 会话中打开输入框星光按钮的菜单,选择“编写新绑定”,或输入 `/binding new <需求>`。
11
- 3. 等待 Agent 提交草稿,在输入框上方检查源码、接口和给模型的提示词。
12
- 4. 选择“保存并启用”。后续 `run_code` 会加载该绑定;如果初始化失败,执行结果会说明原因。
9
+ 全局绑定和星光入口默认开启;新安装无需先改设置。若已有配置显式关闭了全局绑定,可从 **插件 → 已安装 → dsh-ptc-plus → 配置** 重新开启;若只隐藏了快捷入口,可打开“显示 PTC Plus 快捷入口”。功能开关不改变已保存条目的启停状态。
10
+
11
+ 1. 在 PTC 会话中打开输入框星光按钮的菜单,选择“编写新绑定”,或输入 `/binding new <需求>`。
12
+ 2. 等待 Agent 提交草稿,在输入框上方检查源码、接口和给模型的提示词。
13
+ 3. 选择“保存并启用”。后续 `run_code` 会加载该绑定;如果初始化失败,执行结果会说明原因。
13
14
 
14
15
  编写过程中,Agent 会先用小型内存样例验证核心行为,再用 `node:assert/strict` 补充正常、边界和失败断言。批量测试只报告总数和少量代表性失败,避免把每个成功样例写入上下文。测试不得修改外部文件或服务;文件和网络 helper 可以用内存替代对象验证参数转发与错误传播,未实际验证的集成部分应在回答中说明。提交草稿只交给你审阅,保存与启用由你决定。
15
16
 
@@ -26,7 +27,7 @@
26
27
 
27
28
  输入框星光按钮显示待处理草稿的角标。悬停、点击或键盘打开菜单后,选择草稿即可重新显示。每个会话保留一份当前候选。保存或丢弃确认成功后面板关闭;失败时保留草稿供重试。
28
29
 
29
- 星光按钮悬浮或聚焦时显示 PTC Plus 提示,说明它是全局用户绑定入口;有待处理草稿时改为提示草稿状态。星光菜单在首条消息发出前即可使用,鼠标停留一下才展开,扫过输入区不会弹出。悬停打开后鼠标移开菜单即自动关闭;点击可让菜单保持常开,再点一次、点击别处或按 Escape 关闭。全局绑定按名称和用途列出,选中标记与文字表示启用状态;点击条目切换启停,配置对所有会话生效。目录在后台刷新,刷新期间沿用上一次的列表并可以继续操作;保存期间暂时禁用切换,发生冲突或失败时可重新加载后再操作。“管理全局绑定”打开完整工作台;编写命令可用时显示“编写新绑定”和“修改绑定”,“修改绑定”第二步从已有条目中选择后预填 `/binding edit <id> `。REPL 可复用绑定列表会显示每项被后续 cell 复用的次数,并在区块标题显示总复用次数;次数按后续 cell 编译后源码中的静态引用统计,重定义或重声明本身不算一次复用,也不会把已累计的次数清零。
30
+ 星光按钮的 PTC Plus 提示延迟显示,说明它是全局绑定与设置入口;有待处理草稿时改为提示草稿状态。菜单、管理窗口或设置窗口打开时隐藏提示,避免遮挡。星光菜单在首条消息发出前即可使用,鼠标停留一下才展开,扫过输入区不会弹出。悬停打开后鼠标移开菜单即自动关闭;点击可让菜单保持常开,再点一次、点击别处或按 Escape 关闭。全局绑定按名称和用途列出,状态文字与颜色样式表示启用状态;点击条目切换启停,配置对所有会话生效。目录在后台刷新,刷新期间沿用上一次的列表并可以继续操作;切换写入逐条串行化,写入进行中条目保持可用,重复请求被直接丢弃;发生冲突或失败时可重新加载后再操作。“管理全局绑定”打开完整工作台;编写命令可用时显示“编写新绑定”和“修改绑定”,“修改绑定”第二步从已有条目中选择后预填 `/binding edit <id> `。REPL 可复用绑定列表会显示每项被后续 cell 复用的次数,并在区块标题显示总复用次数;次数按后续 cell 编译后源码中的静态引用统计,重定义或重声明本身不算一次复用,也不会把已累计的次数清零。
30
31
 
31
32
  历史中的“绑定编写”请求保留需求、状态和接受时的源码。它供查看当时内容,不重新获得保存资格。目录发生并发修改时,重新加载最新状态后再决定是否保存。
32
33
 
@@ -38,7 +38,7 @@ wire 从不使用 `eval` 或 `Function` 解码,模型文本也从不作为可
38
38
 
39
39
  `Atom` 是 JSON primitive,或封闭 tag:`undefined`、special number、BigInt、node reference。object node 保存有序 `[key, Atom]` entries;array node 保存 `length` 与有序 `[index, Atom]` entries,因此 hole 与显式 `undefined` 不同。decoder 拒绝未知/额外字段、重复 key/index、越界或 dangling reference、不可达 node 和非 canonical 编码;`__proto__` 通过 data descriptor hydrate,不触发 setter。
40
40
 
41
- graph 发现顺序与 ECMAScript own-key 顺序共同确定唯一编码。plain JSON tree 保持线性;shared object 只编码一次。结构预算为 `maxValueNodes`、`maxValueEdges`、`maxValueArrayLength` 和 `maxValueBigIntDigits`,总 IPC/journal/render byte ceiling 继续由 `maxOutputBytes` 约束。
41
+ graph 发现顺序与 ECMAScript own-key 顺序共同确定唯一编码。plain JSON tree 保持线性;shared object 只编码一次。结构预算为 `maxValueNodes`、`maxValueEdges`、`maxValueArrayLength` 和 `maxValueBigIntDigits`;`maxValueArrayLength` 同时限制单个数组长度与一份 graph 中全部数组声明的累计槽位,decoder 在分配目标数组前完成整个 wire 的结构、预算、可达性与 canonical 校验。总 IPC/journal/render byte ceiling 继续由 `maxOutputBytes` 约束。
42
42
 
43
43
  ## Completion 与外层投影
44
44
 
@@ -2,7 +2,48 @@
2
2
 
3
3
  `npm run check` runs `npm run verify`, checks that the source tree and review ledger did not change, and records the verified tree for the commit hook. Run `check` alone when the ledger is absent or resolved; running `verify` immediately before it repeats the full suite. While the ledger has unresolved findings, use `npm run verify` to collect verification evidence.
4
4
 
5
- Both commands validate the ledger, compare generated bundles, check syntax, run Client tests, and run all backend tests with coverage. Backend test files run in separate Node processes. The default process count is half the available CPUs, rounded down and bounded between one and four; each process can also own runtime Workers. Tests within each file retain their existing ordering. Coverage still requires 100% lines and functions and at least 95% branches, including compiler source remapping.
5
+ ## Incremental independent review
6
+
7
+ Independent review is partitioned before reviewers start. Create the ignored working plan, replace every placeholder, then record it against the commit that defines the change scope:
8
+
9
+ ```bash
10
+ npm run review:plan:new
11
+ npm run review:plan -- --base <base-commit>
12
+ ```
13
+
14
+ The plan uses `dsh-review-plan/v1`, and the ignored root `REVIEW_PLAN.json` is its only active source. All five standard obligations from the template must remain present. Each standard or change-specific obligation is assigned to a lane or excluded with a concrete reason. A lane declares semantic scope, exact file paths or directory prefixes, upstream lanes, owners, consumers, and counterexamples. `.` selects the complete checkout. Paths only determine content inputs and changed-path coverage; they do not replace the semantic declarations. A lane fingerprint covers that declaration, its assigned obligations, selected raw Git entry identities, persistent invalidation generation, and upstream fingerprints. Dependencies must form a DAG. The plan command refuses missing standard obligations, placeholders, unknown obligations, dependency cycles, a non-ancestor base, and changed paths outside every lane.
15
+
16
+ The plan command prints one `review-lane <id> <head> <fingerprint>` line per lane. Lanes may be reviewed concurrently. Record each clean external report with the identity captured before that review:
17
+
18
+ ```bash
19
+ npm run review:lane -- \
20
+ --lane <id> \
21
+ --evidence <report-path> \
22
+ --expect-head <head> \
23
+ --expect-fingerprint <lane-fingerprint>
24
+ ```
25
+
26
+ The report's last non-empty line must be `VERDICT: NO FINDINGS`. A readable findings or incomplete report retires the named lane and every declared dependent from the recorded plan before checking whether the current candidate drifted; restoring an earlier tree therefore cannot revive evidence that the report rejected. An unreadable or missing report path is a command-input error and does not retire valid evidence. Unrelated lanes remain effective. A clean report retains the captured `HEAD` as provenance; an unrelated concurrent edit or `HEAD` advance does not prevent recording. Direct input, upstream fingerprint, scope, obligation, owner, consumer, counterexample, or dependency changes do. Put findings in `REVIEW_FINDINGS.md`, fix them, and rerun only the incomplete lanes shown by `npm run review:status`.
27
+
28
+ Review-state mutations and authorization decisions use one atomic lock under checkout-local Git metadata. Plan recording, clean or negative lane recording, finalization, clearing, and pre-commit therefore have one observable order. A negative report queued behind an older clean recorder advances the reported lane's persistent invalidation `{epoch, revision}`, including while that lane is temporarily absent from the current plan, before retiring its current dependent closure. Advancement increments the revision normally and rotates to a new random epoch with revision zero at `Number.MAX_SAFE_INTEGER`, so a valid tuple never becomes unretirable. A temporarily absent lane must already have a retained tuple; unknown or mistyped lane IDs are rejected rather than creating state. Every active lane must have an explicit tuple. The tuple participates in the lane fingerprint and propagates through dependency fingerprints, so an older clean report cannot be submitted after re-addition. It survives normal `review:clear` and temporary lane removal; only a review captured after retirement can authorize the repaired fingerprint. Missing, partial, or malformed generation state blocks status and authorization. Explicit clear preserves valid generations but removes damaged generation state; plan recording then creates a new random epoch, so old reports still cannot match. The lock records a PID and unique release token and bounds acquisition, but never reclaims another owner automatically. If a command crashed, confirm that no review-state command is active, obtain the exact checkout-local path with `git rev-parse --git-path review-findings/state.lock`, remove only that directory, and retry. `review:status` remains a diagnostic snapshot and does not grant authorization. Finalization and pre-commit also re-read the active plan, candidate, proof, lane records, and evidence before they persist or return a decision, so edits made outside the review commands fail closed.
29
+
30
+ Do not mutate a recorded boundary implicitly. Every status, clean lane verdict, finalization, and commit decision compares the normalized active file with the recorded plan. Missing, malformed, or semantically changed active content blocks clean authorization; it does not block a findings or incomplete report from retiring the recorded lane closure. Whitespace and object-key formatting alone do not change plan identity. When evidence reveals a missing path or semantic dependency, edit `REVIEW_PLAN.json` and run `npm run review:plan` without another `--base`; the command reuses the recorded base. The new plan hash invalidates its aggregate verdict, but clean lane records survive when their base, complete declaration, selected Git entries, upstream fingerprints, and evidence hashes are unchanged.
31
+
32
+ After `review:status` reports `clean`, run the deterministic gate once and bind the composite verdict to it:
33
+
34
+ ```bash
35
+ npm run check
36
+ npm run review:finalize
37
+ git diff --check HEAD --
38
+ ```
39
+
40
+ The final verdict uses `dsh-review-verdict/v2`. Pre-commit verifies its plan hash, lane fingerprints, evidence hashes, whole source fingerprint, `HEAD`, and prospective index. A later `HEAD` change retires the proof and aggregate verdict. Source or index drift blocks the gate while it differs; restoring the exact finalized candidate preserves those records, while keeping a changed candidate requires another `check` and `review:finalize`. Only lanes whose effective inputs changed need another independent review. `npm run review:clear` abandons the recorded plan, lane records, aggregate verdict, and verification proof while leaving the working `REVIEW_PLAN.json` available for revision; persistent invalidation generations remain so pre-retirement reports cannot revive in a rebuilt plan. Clearing is not a normal retry step. The prior single-report `verdict` command and `dsh-review-verdict/v1` records are not accepted.
41
+
42
+ `npm run verify` and `npm run check` validate the ledger, compare generated bundles, check syntax, run Client tests, and run all backend tests with coverage. Backend test files run in separate Node processes. The default process count is half the available CPUs, rounded down and bounded between one and four; each process can also own runtime Workers. Tests within each file retain their existing ordering. Coverage still requires 100% lines and functions and at least 95% branches, including compiler source remapping. The graded merge additionally asserts that the plugin-source closure it claims — `index.js` and every `internal/*.js` owner — appears in the merged report, so a module no instrumented process loads fails the gate instead of silently leaving it. Generated artifacts stay outside that closure: the root `compiler-*.cjs` bundles and the `internal/*.cjs` shims are remapped onto their original `internal/` sources or verified by the bundle checks, and `scripts/migrate-session-log.mjs` is verified by its own migration suite.
43
+
44
+ Coverage, the exhaustive backend suite, and the review-bound proof belong to the local pre-commit `npm run check`. CI starts from a clean lockfile install and runs `verify:ci` on every supported Node and operating-system entry: frozen-baseline validation, semantic ownership, generated builds, Client tests, and focused runtime contracts covering compiler startup, helper-process lifecycle, plugin takeover, native-call transport, and durable argument diagnostics. Ubuntu Node 24 additionally exercises the packed plugin against both frozen official `latest` and `next` DSH graphs. CI therefore proves portable installation and runtime integration without repeating coverage or the exhaustive semantic suite on hosted runners.
45
+
46
+ `host:baseline:verify`, which is part of deterministic verification, is offline. It requires the installed root DSH `next` cohort to match `package-lock.json` and validates the tracked `compat/latest` declaration and lock without installing it. CI separately runs `test:host-contract`: the root graph exercises `next`, while a temporary consumer created from `compat/latest/package-lock.json` exercises `latest` against the packed plugin. Registry tag lookup and lock regeneration belong only to `host:baseline:update` and the scheduled baseline-refresh workflow. That workflow publishes a changed frozen candidate as a pull request and explicitly dispatches this CI workflow once because its `GITHUB_TOKEN`-authored events cannot start another run; ordinary gates never install a floating channel.
6
47
 
7
48
  Use focused coverage while developing or after a full gate identifies a narrow coverage failure. Select every source whose threshold matters and the smallest test set that can distinguish the correction:
8
49
 
@@ -36,7 +77,7 @@ Coverage runs additionally set `DSH_PTC_TEST_WORKER_COVERAGE=1` and load `script
36
77
 
37
78
  `npm run coverage` and `npm run coverage:focus` print their current stage and total elapsed time. Each invocation collects worker coverage in its own temporary directory under `coverage/`, removed after c8 finishes reporting, so simultaneous runs cannot erase or combine each other's evidence. The default report is printed to the terminal. A separate `coverage/run-*.tap` log retains Node's test diagnostics, including process exit codes and signals that the concise terminal reporter can omit. An explicit Node test reporter option replaces these default reporters. Source changes during verification still invalidate a `check` proof; separate coverage directories do not make concurrent source edits safe.
38
79
 
39
- Windows Node 24 and 26 reproductions and stacks identify a cppgc finalizer (`node::contextify::ContextifyScript`, or `ContextifyContext` when coverage was enabled) running after the environment that owned its private realm was gone, which wrote through a dangling `node::Realm` during isolate teardown. The reproduction ended the whole DSH process when an in-host session worker was terminated; macOS runners also observed whole-file aborts under coverage, but the available evidence does not establish that those aborts came from the same Windows stack or that the same cause applies across platforms. The session kernel now runs in a plugin-owned helper process (`internal/kernel-child.js`); `IsolatedWorker` sends a cooperative shutdown and, if the helper does not leave within its bound, kills only that helper. DSH is no longer the worker-teardown target on any supported host, so a wedged kernel is contained as a helper failure rather than a host-native crash. `.github/workflows/ci.yml` sets `DSH_PTC_TEST_CONCURRENCY: 1` and runs `npm run check` once; the former file-level retry wrapper is gone, and any failing test or file-level abort fails that run. This boundary removes the plugin's in-host kill exposure; it neither repairs the upstream Node path nor proves the macOS abort shared the Windows cause.
80
+ Windows Node 24 and 26 reproductions and stacks identify a cppgc finalizer (`node::contextify::ContextifyScript`, or `ContextifyContext` when coverage was enabled) running after the environment that owned its private realm was gone, which wrote through a dangling `node::Realm` during isolate teardown. The reproduction ended the whole DSH process when an in-host session worker was terminated; macOS runners also observed whole-file aborts under coverage, but the available evidence does not establish that those aborts came from the same Windows stack or that the same cause applies across platforms. The session kernel now runs in a plugin-owned helper process (`internal/kernel-child.js`); `IsolatedWorker` sends a cooperative shutdown and, if the helper does not leave within its bound, kills only that helper. DSH is no longer the worker-teardown target on any supported host, so a wedged kernel is contained as a helper failure rather than a host-native crash. The former file-level retry wrapper is gone, and any failing CI smoke or local exhaustive test fails its run. This boundary removes the plugin's in-host kill exposure; it neither repairs the upstream Node path nor proves the macOS abort shared the Windows cause.
40
81
 
41
82
  Native oracle workers and external Windows command fixtures explicitly disable inherited `NODE_V8_COVERAGE`; they execute no code included by the plugin coverage gate. Subprocesses testing plugin implementations still collect coverage. PowerShell semantic-version observations share one invocation, and Windows registry fixtures read the three effective registry values with one real npm configuration query. Each retains its original inputs and assertions.
42
83
 
package/index.js CHANGED
@@ -14,12 +14,12 @@ import { PTC_DELIVERY_CONTEXT } from './internal/runtime-messages.js'
14
14
  import { createCordisToolsOwner } from './internal/cordis-tools-owner.js'
15
15
  import { createEditTransportOwner, EDIT_RUN_CODE } from './internal/edit-transport-owner.js'
16
16
  import { createRuntimeBridgeOwner, RUN_CODE } from './internal/runtime-bridge-owner.js'
17
- import { resolveConfig } from './internal/runtime-config.js'
17
+ import { resolveConfig, watchVolatileConfig } from './internal/runtime-config.js'
18
18
  import {
19
19
  CONFIG_FIELDS,
20
20
  SETTINGS_NAMESPACE,
21
21
  } from './internal/config-spec.js'
22
- import { installSettingsSectionCompat } from './internal/settings-compat.js'
22
+ import { configFieldSchema, installSettingsSectionCompat } from './internal/settings-compat.js'
23
23
  import { installExecutionSeam } from './internal/execution-seam-compat.js'
24
24
  import { valueLimitsFromConfig } from './internal/value-wire-schema.js'
25
25
  import { createReplMemoryProjection } from './internal/repl-memory-projection.js'
@@ -35,19 +35,13 @@ const INSTALL_CLEANUP = Symbol('ptc-plus install cleanup')
35
35
  /** Plugin name used by loader diagnostics. */
36
36
  export const name = 'ptc-plus'
37
37
 
38
- /** Runtime limits and behavior exposed to Cordis configuration. */
39
- function configSchemaField(field) {
40
- const base = field.type === 'boolean'
41
- ? Schema.boolean().default(field.default)
42
- : field.type === 'enum'
43
- // Keep omission visible until resolveConfig migrates legacy policies.
44
- ? Schema.union(field.options.map(option => Schema.const(option)))
45
- : Schema.number().step(1).min(field.min).max(field.max).default(field.default)
46
- return base.description(field.description)
47
- }
48
-
49
38
  export const Config = Schema.object(Object.fromEntries(
50
- CONFIG_FIELDS.map(field => [field.key, configSchemaField(field)]),
39
+ CONFIG_FIELDS.map(field => [
40
+ field.key,
41
+ // Runtime limits and behavior exposed to Cordis configuration. The shape
42
+ // each field takes is owned by the installed settings generation.
43
+ configFieldSchema({ Schema, field, settingsModule: dshSettings }),
44
+ ]),
51
45
  ))
52
46
 
53
47
  /**
@@ -408,22 +402,27 @@ function installPtCRuntime(ctx, resolvedConfig, seam, toolSchemasForAgent, sessi
408
402
  if (exec.name === EDIT_RUN_CODE) return editTransport.handleResult(exec, result)
409
403
  if (exec.name === RUN_CODE) return runtimeBridge.handleResult(exec, result)
410
404
  }))
411
- disposers.push(ctx.on('agent/disposed', async ({ agent }) => {
412
- directSurface.disposeAgent(agent)
413
- editTransport.disposeAgent(agent)
414
- await Promise.all([
415
- runtimeBridge.disposeAgent(agent),
416
- userBindings.clearAgentPresentation(agent),
417
- ])
418
- }))
419
- disposers.push(ctx.on('session/disposed', async (session) => {
420
- directSurface.disposeSession(session)
421
- editTransport.disposeSession(session)
422
- await Promise.all([
423
- runtimeBridge.disposeSession(session),
424
- userBindings.clearSessionPresentation(session?.id ?? session),
425
- ])
426
- }))
405
+ const disposeLifecycleOwners = async (operations, message) => {
406
+ const results = await Promise.allSettled(operations.map(operation => (
407
+ Promise.resolve().then(operation)
408
+ )))
409
+ const failures = results
410
+ .filter(result => result.status === 'rejected')
411
+ .map(result => result.reason)
412
+ if (failures.length > 0) throw new AggregateError(failures, message)
413
+ }
414
+ disposers.push(ctx.on('agent/disposed', ({ agent }) => disposeLifecycleOwners([
415
+ () => directSurface.disposeAgent(agent),
416
+ () => editTransport.disposeAgent(agent),
417
+ () => runtimeBridge.disposeAgent(agent),
418
+ () => userBindings.clearAgentPresentation(agent),
419
+ ], 'ptc-plus: agent disposal failed')))
420
+ disposers.push(ctx.on('session/disposed', session => disposeLifecycleOwners([
421
+ () => directSurface.disposeSession(session),
422
+ () => editTransport.disposeSession(session),
423
+ () => runtimeBridge.disposeSession(session),
424
+ () => userBindings.clearSessionPresentation(session?.id ?? session),
425
+ ], 'ptc-plus: session disposal failed')))
427
426
  disposers.push(ctx.on('agent-preset/selected', (sessionId) => {
428
427
  directSurface.resetSessionComposition(sessionId)
429
428
  const agent = ctx.agents?.get?.(String(sessionId))
@@ -512,18 +511,22 @@ function installPtCRuntime(ctx, resolvedConfig, seam, toolSchemasForAgent, sessi
512
511
  }
513
512
 
514
513
  /** Register the session-bound REPL runtime. */
515
- export function apply(ctx, config = {}) {
514
+ export const apply = (ctx, config = {}) => {
516
515
  // Configuration is validated before the plugin waits for the host's seam, so
517
516
  // an unusable configuration fails at the load call rather than later.
518
517
  const resolvedConfig = resolveConfig(config)
519
518
  return installExecutionSeam(ctx, {
520
519
  services: inject,
521
- attach: (scope, seam) => applyWithExecutionSeam(scope, resolvedConfig, seam),
520
+ attach: (scope, seam) => applyWithExecutionSeam(scope, resolvedConfig, seam, ctx, config),
521
+ reportFailure: (error, seamName) => ctx.logger?.warn?.(
522
+ `ptc-plus: execution seam ${seamName} failed to attach`,
523
+ error,
524
+ ),
522
525
  })
523
526
  }
524
527
 
525
528
  /** Install the plugin into the scope of the seam the host registered. */
526
- function applyWithExecutionSeam(ctx, resolvedConfig, seam) {
529
+ function applyWithExecutionSeam(ctx, resolvedConfig, seam, volatileHost, volatileConfig) {
527
530
  const toolSchemasForAgent = agent => typeof ctx.tools.schemas === 'function'
528
531
  ? ctx.tools.schemas(agent)
529
532
  : []
@@ -767,7 +770,7 @@ function applyWithExecutionSeam(ctx, resolvedConfig, seam) {
767
770
  ctx.effect(() => ctx.on('agent/disposed', ({ agent }) => messageOwner.disposeAgent(agent)), 'ptc-plus message agent disposal')
768
771
  ctx.effect(() => () => messageOwner.dispose(), 'ptc-plus dynamic message lifecycle')
769
772
 
770
- let configSource = () => resolvedConfig
773
+ let configSource = () => resolveConfig(volatileConfig ?? resolvedConfig)
771
774
  let configurationGeneration = 0
772
775
  let settingsWriter
773
776
  let configurationRollback = false
@@ -826,6 +829,7 @@ function applyWithExecutionSeam(ctx, resolvedConfig, seam) {
826
829
  if (typeof ctx.inject === 'function') {
827
830
  installSettingsSectionCompat({
828
831
  ctx,
832
+ ownerFiber: volatileHost.fiber,
829
833
  settingsModule: dshSettings,
830
834
  namespace: SETTINGS_NAMESPACE,
831
835
  schema: Config,
@@ -841,5 +845,6 @@ function applyWithExecutionSeam(ctx, resolvedConfig, seam) {
841
845
  },
842
846
  })
843
847
  }
848
+ watchVolatileConfig(volatileHost, ctx, volatileConfig, () => reconcile())
844
849
  if (configurationGeneration === 0) return reconcile(settingsWriter === undefined)
845
850
  }
@@ -1,7 +1,7 @@
1
1
  import { AsyncLocalStorage } from 'node:async_hooks'
2
+ import { WORKER_REALM_MUTATION, defineWorkerRealmProperty } from './worker-realm-surfaces.js'
2
3
 
3
4
  const bind = Function.prototype.call.bind(Function.prototype.bind)
4
- const defineProperty = Object.defineProperty
5
5
  const getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor
6
6
  const apply = Reflect.apply
7
7
  const nativeGetStore = AsyncLocalStorage.prototype.getStore
@@ -10,9 +10,12 @@ const nativeRun = AsyncLocalStorage.prototype.run
10
10
  const nativeRunDescriptor = getOwnPropertyDescriptor(AsyncLocalStorage.prototype, 'run')
11
11
 
12
12
  function protect(scope) {
13
- defineProperty(scope, 'getStore', { value: bind(nativeGetStore, scope) })
14
- defineProperty(scope, 'enterWith', { value: bind(nativeEnterWith, scope) })
15
- defineProperty(scope, 'run', { value: bind(nativeRun, scope) })
13
+ defineWorkerRealmProperty(WORKER_REALM_MUTATION.STABLE, 'private AsyncLocalStorage receiver methods',
14
+ scope, 'getStore', { value: bind(nativeGetStore, scope) })
15
+ defineWorkerRealmProperty(WORKER_REALM_MUTATION.STABLE, 'private AsyncLocalStorage receiver methods',
16
+ scope, 'enterWith', { value: bind(nativeEnterWith, scope) })
17
+ defineWorkerRealmProperty(WORKER_REALM_MUTATION.STABLE, 'private AsyncLocalStorage receiver methods',
18
+ scope, 'run', { value: bind(nativeRun, scope) })
16
19
  return scope
17
20
  }
18
21
 
@@ -22,17 +25,19 @@ export function createPrivateAsyncLocalStorage() {
22
25
 
23
26
  export function protectNextAsyncLocalStorageRun(invoke) {
24
27
  let scope
25
- defineProperty(AsyncLocalStorage.prototype, 'run', {
28
+ defineWorkerRealmProperty(WORKER_REALM_MUTATION.TEMPORARY, 'AsyncLocalStorage.prototype.run',
29
+ AsyncLocalStorage.prototype, 'run', {
26
30
  ...nativeRunDescriptor,
27
31
  value: function (...args) {
28
32
  scope ??= protect(this)
29
33
  return apply(nativeRun, this, args)
30
34
  },
31
- })
35
+ })
32
36
  try {
33
37
  invoke()
34
38
  } finally {
35
- defineProperty(AsyncLocalStorage.prototype, 'run', nativeRunDescriptor)
39
+ defineWorkerRealmProperty(WORKER_REALM_MUTATION.RESTORE, 'AsyncLocalStorage.prototype.run',
40
+ AsyncLocalStorage.prototype, 'run', nativeRunDescriptor)
36
41
  }
37
42
  return scope
38
43
  }