dsh-side-branch 0.1.1 → 0.2.2
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.
- package/AGENTS.md +103 -13
- package/README.md +92 -55
- package/docs/AGENTS.en.md +106 -14
- package/docs/CHANGELOG.md +82 -0
- package/docs/README.zh-CN.md +90 -53
- package/docs/TECHNICAL.md +105 -10
- package/docs/TECHNICAL.zh-CN.md +105 -10
- package/lib/client.js +618 -64
- package/lib/index.js +1041 -90
- package/package.json +10 -4
package/docs/AGENTS.en.md
CHANGED
|
@@ -42,6 +42,7 @@ Failure criteria and what they mean:
|
|
|
42
42
|
| `cordis.patch.yml` | Bundle patch, inserts a single line, `name` must be the real package name |
|
|
43
43
|
| `package.json` | `dsh.bundle.patch` declares the patch; `dsh.client` declares the browser half |
|
|
44
44
|
| `scripts/smoke.mjs` | Contract self-check: loads the host half, verifies the read-only allow-list and the shape of the guard, confirms the dependency packages exist. **Only exists in the source repository** and is not published with the npm package (what it asserts is source shape, so it cannot run outside the source tree) |
|
|
45
|
+
| `scripts/client-check.mjs` | **Runtime** self-check for the client half: inside Node it loads the **real bundle** with a minimal React + minimal DOM, renders the **real component**, feeds it **SSE frames of the real shape**, and then asserts whether the answer is present in the rendered tree. **Likewise only exists in the source repository** and is not published with the npm package (what it asserts is real runtime behaviour, so it cannot run outside the source tree) |
|
|
45
46
|
|
|
46
47
|
## Hard constraints before changing anything
|
|
47
48
|
|
|
@@ -59,12 +60,35 @@ Failure criteria and what they mean:
|
|
|
59
60
|
|
|
60
61
|
**Do not remove the "un-archive before a turn, re-archive after it" cycle, and do not turn `unarchiveSession` into a hard call.** From DSH `0.1.7-rc.1`, `dsh-api-session-controller`'s `ArchivedSessionGate` rejects any model step proposed for an **archived session** (`agent/pre-step` ⇒ reject ⇒ `dsh-agent-loop` ends the turn as `turn/end { reason: 'blocked' }`, **without a model request ever being sent**). This plugin hides side sessions precisely by archiving them ⇒ drop the `releaseArchiveGate()` call before `followup` in `handleStart`, or the `rehideAfterTurn()` call in `finishJob`, and the branch is **dead on every single turn** on 0.1.7 (the panel shows `T.blocked`; that "content blocked by security policy" wording has nothing to do with any content policy — do not be misled by it). And `unarchiveSession` only exists from **`0.1.6-alpha.2`**: it **must be feature-detected with `typeof`**, because a hard call throws on `0.1.5-rc.2` and destroys backward compatibility outright. Section ⑥ of `scripts/smoke.mjs` guards this shape.
|
|
61
62
|
|
|
63
|
+
**Do not remove Re-Fork, and do not "cut corners" on it.** Every follow-up must first consult `shouldReFork`: if **the main session has settled a new turn** or **the model / reasoning effort changed**, a new segment is created (seed = the main session's latest settled prefix) and the previous segment's own side-branch conversation is replayed into it. Four things are not negotiable:
|
|
64
|
+
|
|
65
|
+
1. **`shouldReFork` must test both conditions.** Testing only "the main session grew" means that when the user **changes the model while the main session has not moved**, that turn keeps using the old model — silently ignoring the user's choice.
|
|
66
|
+
2. **Order: replay successfully first, release the old session second.** Reversed there is no way back (the old session is gone and the new one has no history). If the replay fails, **discard the segment just created**, leave the old session exactly as it was, and refuse the turn with `side-branch/refork-failed`. ⛔ Never silently degrade to "a new session with no history".
|
|
67
|
+
3. **The replay passes only `event.data`, verbatim, and replays only the 5 surface event types.** Hand-building an `assistant/message` is always rejected by `dsh-session` (it requires a real `source.provider/model` and a `stream` array); `turn/start`/`tool/call` and the like are pure log events and `append`ing them throws. ⛔ **And never "helpfully" fill in `event.sourceEventSeqs`** — a `tool/result` carries `[the old session's callSeq]` at the event level, and copying it over trips `sourceEventSeqs must reference earlier events`.
|
|
68
|
+
4. **Do not truncate the side branch's history.** At the context limit the turn is **refused** (`estimateRequestTokens` + `side-branch/context-full`). "A refusal is an explicit failure; truncation is silent distortion."
|
|
69
|
+
|
|
70
|
+
**Do not bring back "changed model ⇒ carry the history into a new segment"** (`seedSource`, the `sameAgentOptions` handoff, the manual `skipPrependNotice` handover). Re-fork absorbs that whole path: changing the model is just one more reason to rebuild. `createConversation` can only be seeded from the **parent session**.
|
|
71
|
+
|
|
72
|
+
**Do not drop `parentCut` from the segment-creation path.** `createConversation` must record `parentCut` when a segment is created (= the length of the parent session's settled prefix at that moment), and `sleepConversation`/`resumeConversation` must carry it along. Without it, `completedTurnPrefixLength(...) > undefined` is always true ⇒ **every turn re-forks for nothing**.
|
|
73
|
+
|
|
74
|
+
**Do not turn multi-segment answers (`job.segments`) back into "two string slots".** **Every step** of a turn emits its own `start` frame: only a changed `turn`/`step` means "a new segment", and only a changed `attemptId` within the same step means a **retry** (that, and only that, drops that step's segments). Treating every `start` as a retry is exactly the "the middle reasoning and the middle answer disappeared" bug. Also, `replace` rewrites **only the current segment** (replacing the whole answer would overwrite the earlier segments), and `snapshot`/terminal events must carry `segments` (otherwise a refresh loses every segment again).
|
|
75
|
+
|
|
76
|
+
**Do not stuff the constants and pure functions that "both sides share" in `lib/client.js` into any function.** `clampString` / `SEGMENT_KINDS` / `MAX_SEGMENTS` / `MAX_SEGMENT_CHARS` / `MAX_SEGMENTS_TOTAL_CHARS` / `normalizeSegment` / `normalizeSegments` / `trimSegments` **must stay at module scope** — `createPanelStore()` and `SideBranchBody` **both use them** (the store for persisted-shape validation, the component for live streaming writes). Put them inside one of the two and the other gets a `ReferenceError`, and that error **only ever shows up in the browser console, swallowed by event dispatch** (the symptom: the panel sits still and the answer never appears). `scripts/client-check.mjs` exists precisely to catch this class of error.
|
|
77
|
+
|
|
78
|
+
**Do not remove the instance lease (`side-branch-instances/`), and do not sweep orphans around it.** The sweep's criterion is "a `side-*` directory on disk that is not in this process's ledger", and that ledger **is necessarily empty at startup** ⇒ when **two instances share one `DSH_HOME` and run at the same time**, the one that starts later deletes the segments the earlier one **is using** as orphans. So before sweeping, an instance must post its own lease and then check whether any other **live** lease is present — if one is, **skip the entire sweep**.
|
|
79
|
+
|
|
80
|
+
**The payload of `GET /side-branch/inherited` must never enter persisted panel state.** A main-session prefix can be hundreds of kilobytes, and panel state goes through `sessionStorage` (a 5–10 MB quota) ⇒ putting it into `rounds`/`normalizeRound` blows the quota instantly and breaks "survives a refresh". It lives only in component memory and is fetched on demand.
|
|
81
|
+
|
|
62
82
|
**The SSE response is hand-written from end to end** (`Connection: close`). Replacing it with the framework's built-in way of writing it will break it; the reason is in the `handleStream` comment in `lib/index.js`.
|
|
63
83
|
|
|
64
84
|
**All client input is untrusted.** Question length, model identifier and `conversationId` ownership must all be validated host-side.
|
|
65
85
|
|
|
66
86
|
**Do not touch any of the four read-only guardrails**: the `ALLOWED_TOOLS` constant, the `tools.guard` in `setup(agentCtx)` (`throw` if it cannot be obtained — fail-loud), `ROUTE_METHODS` (the 405 + `Allow` method contract), and the host-side validation of `conversationId` ownership.
|
|
67
87
|
|
|
88
|
+
**Do not give a turn a wall-clock timeout.** Apart from the tool allow-list, a branch should not behave differently from the main session: the main session does not cap how long a turn runs (long thinking, long tools, several steps), so neither may a branch. The only backstop is the `STALL_TIMEOUT_MS` **stall** watchdog — its timer is re-armed by **every** event (job creation, stream frames, session events), so running long never trips it; only "no event at all for this long" does. Up to 0.2.1 this was a 90-second **wall-clock** cap, which cut long turns off and showed "Request timed out (90s)". Section 10 of `scripts/smoke.mjs` guards this.
|
|
89
|
+
|
|
90
|
+
**The quote entry may only appear inside the main session's body.** Its listener sits on `document` (so selecting anywhere in a conversation works), which means scope has to come from the DOM: `insideMainConversation()` requires an ancestor with `[data-conversation-content]` (falling back to `[data-conversation-scroll]` for older hosts) and excludes the right sidebar's `[data-sidebar-right-panel]` / `[data-sidebar-right-session]` / `[data-sidebar-chat]` and the `[data-conversation-region="composer"]` seat. **An embedded sub-agent conversation carries the same surface marker** (the right-sidebar chat), so testing the marker alone would treat a child session as the main one. When no surface marker is present the entry does **not** appear (fail-closed). Getting this wrong is not cosmetic: the user ends up quoting text from a file preview, the settings dialog or another plugin's panel into the branch's prompt. `scripts/client-check.mjs` drives the **real `selectionchange` path** against a fake DOM and asserts this for seven locations.
|
|
91
|
+
|
|
68
92
|
**The `@deepseek-ai/cordis` in `peerDependencies` is declarative**: the plugin code **does not import it** (the host half obtains all of its runtime services softly via `ctx`, and the client half only `require`s platform built-in modules). It is declared by ecosystem convention, to state which contract this plugin hangs off — **do not delete it as a dependency someone forgot to remove**, and do not add an import just "to use it".
|
|
69
93
|
|
|
70
94
|
## Why it is done this way (read before changing anything; do not revert to intuition)
|
|
@@ -75,7 +99,9 @@ The following are approaches that "look like less work but break this plugin's p
|
|
|
75
99
|
|
|
76
100
|
**Why not use the official continuable child session.** `ctx.subagents.startContinuable` looks exactly right on the surface (officially provided, a child session you can keep talking to), but on settlement it posts the child session's final answer back into the parent session as a **parent-session user message** and wakes the parent model for a full turn (`watchSettlement` / `notifySettlement` in `dsh-subagent`; the official README states this is designed behaviour with no switch). That is reasonable for most scenarios, but fatal for this plugin: it directly violates the "the answer does not enter the main session" premise, and costs an extra model turn on top.
|
|
77
101
|
|
|
78
|
-
**Why we build the side session ourselves, and why the seed is the "completed-turn prefix".** The current approach is `ctx.agents.create()` to create a **plain side session** (`meta.parentSession` is used only as lineage, `origin:'subagent'` is **not** set — we do not need a subagent lifecycle, and we do not want a subagent card appearing in the main session), then at birth we use the public `session.snapshotEvents()` to read out the parent session's events and cut to the last `turn/end` ourselves as the seed, and in `setup` we use `agentPresets.composeFrom(agentCtx, parent.ctx)` to inherit the parent session's preset/tools/persona. This makes the prompt prefix word-for-word identical to the parent session's, so it **keeps hitting the existing prefix cache
|
|
102
|
+
**Why we build the side session ourselves, and why the seed is the "completed-turn prefix".** The current approach is `ctx.agents.create()` to create a **plain side session** (`meta.parentSession` is used only as lineage, `origin:'subagent'` is **not** set — we do not need a subagent lifecycle, and we do not want a subagent card appearing in the main session), then at birth we use the public `session.snapshotEvents()` to read out the parent session's events and cut to the last `turn/end` ourselves as the seed, and in `setup` we use `agentPresets.composeFrom(agentCtx, parent.ctx)` to inherit the parent session's preset/tools/persona. This makes the prompt prefix word-for-word identical to the parent session's, so it **keeps hitting the existing prefix cache**. That seed is re-taken from the main session on every follow-up — see the next paragraph.
|
|
103
|
+
|
|
104
|
+
**Why every turn "rebuilds" (re-fork) instead of letting one session live on.** While a session lives, its seed is frozen at the moment the segment was created — whatever the main session says afterwards is **never** visible to that branch, and the only way to see it is "Clear" and start a new segment (that is what 0.1.x did). Conversely, creating a new segment on every follow-up, re-taking the main session's **latest** settled prefix as the seed, and then replaying the branch's own questions and answers into it gets you two things at once: **the main session's updates** and **the branch's own continuity**. It is no worse for the cache either: the main session's newest content **was just requested by the main session itself**, so it is probably still in the provider's hot cache, and the re-fork request prefix lines up with it exactly (a cache read); under "frozen prefix + append", the same content is sent by the branch in that shape for the **first** time (a cache write). **Rebuild only when something actually changed** (the main session has not grown and the model is unchanged ⇒ keep the current segment): the seed would be byte-identical, and rebuilding would only add one more session record the platform cannot delete plus one deep copy. This is also why the model-facing text now says the inherited history is rebuilt from the main session every turn rather than describing it as a snapshot taken when the branch was opened.
|
|
79
105
|
|
|
80
106
|
**Why the side session is archived, and why it is un-archived and re-archived around every turn.** A side session is a **real session**, and left alone it would add an extra row to the left sidebar, mixed in with the user's own conversations. The only official way to hide a session from the grouping surfaces is to **archive** it (the official README itself describes the archive set as "sessions hidden from every grouping surface"), so the plugin archives a side session the moment it is created. The problem is that `0.1.7-rc.1` gave "archived" a second meaning: **archived ⇒ must not run** (`ArchivedSessionGate` rejects it at `agent/pre-step`). So "archive at birth" **locks the plugin out of itself** on the new version (every turn is stopped before `followup`, no model request is sent). And it **cannot** simply be changed to "do not archive at all" — that would put side sessions back into the session list, which is product behaviour, not an implementation detail. The current trade-off is three steps: **archive while idle (clean list) → un-archive before each turn is delivered (not gated) → archive again once that turn settles (clean list again)**. The costs are recorded honestly: the row may briefly appear in the sidebar while an answer is running, and each turn costs two or three extra durable writes. The other route is the official `origin: 'subagent'` (the sidebar natively hides rows whose summary has that origin), but it drags in subagent lifecycle semantics and conflicts with the "do not set `origin:'subagent'`" premise, so it was not taken.
|
|
81
107
|
|
|
@@ -83,7 +109,18 @@ The following are approaches that "look like less work but break this plugin's p
|
|
|
83
109
|
|
|
84
110
|
**Why settings do not go through the official settings service.** `settings.register(ns, schema)` wants **schemastery's `z<T>` itself** (`resolve()` in `dsh-settings` calls it directly, so the schema must be callable), and `@deepseek-ai/schemastery` is a platform built-in package that **cannot be resolved from the plugin workspace by static import, dynamic import or `createRequire`** (all three give `ERR_MODULE_NOT_FOUND`); hand-building a "compatible schema" does not work either (node objects work through properties and are not callable). ⇒ Settings are instead persisted client-side in `localStorage`, with the value sent to the host on every request, and the host only validates and clamps. The cost is that this namespace is not visible in the official settings page, and the plugin carries its own gear panel.
|
|
85
111
|
|
|
86
|
-
**Why reaching the context limit is a "rejection" rather than "drop the earliest turns".** The latter is simpler to implement and does not interrupt the user, but it makes the model **silently amnesiac**: it does not remember the dropped content yet still behaves as if it does, which the user can hardly notice. So instead
|
|
112
|
+
**Why reaching the context limit is a "rejection" rather than "drop the earliest turns".** The latter is simpler to implement and does not interrupt the user, but it makes the model **silently amnesiac**: it does not remember the dropped content yet still behaves as if it does, which the user can hardly notice. So instead the turn is **refused** at the limit, with `used` / `limit` / `window` handed back to the panel (`contextFull`), and the history is **never truncated**.
|
|
113
|
+
|
|
114
|
+
⚠️ **In 0.2.0 the criterion changed from "read the usage" to "estimate".** After a re-fork the new session **has not run a single turn yet** ⇒ the old criterion, `conv.lastContextTokens` (the side session's `assistant/message` `input + cacheRead`), is necessarily `undefined` and cannot be used. The criterion now (`estimateRequestTokens`) is:
|
|
115
|
+
|
|
116
|
+
```text
|
|
117
|
+
used ≈ prompt tokens of the parent prefix (measured: the `input + cacheRead + cacheWrite` of the last `assistant/message` in the parent's log)
|
|
118
|
+
+ (characters of the branch history + characters of this turn's question) × CONTEXT_ESTIMATE_SAFETY
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The turn is refused once that exceeds `contextWindow × CONTEXT_LIMIT_RATIO` (0.8). When `contextWindow` cannot be read the limit falls back to `CONTEXT_LIMIT_FALLBACK_TOKENS`, and when the parent's usage cannot be read the parent-prefix half falls back to `CONTEXT_ESTIMATE_PARENT_FALLBACK`. ⚠️ **Do not read `lastContextTokens` off the parent Agent instead**: the parent Agent **has no such field** (it is maintained by this plugin from session events).
|
|
122
|
+
|
|
123
|
+
⚠️ **Recorded honestly: `used` is an estimate.** The parent-prefix half is measured; the branch-history-and-question half is "character count × safety factor" ⇒ the number shown in the panel is **not** a measured usage, and it is normal for it to disagree with the real number in the usage row once that turn settles. Better to refuse early than to send something too large.
|
|
87
124
|
|
|
88
125
|
**Why the allow-list is not configurable.** The fewer pivot points a read-only promise has, the better. If the allowed surface were the intersection of "host constants × the set of names in settings × the subset submitted by the client", all three would have to stay consistent at all times, and a mistake in any one of them could widen the allowed surface. Now there is only one place: the two same-source constants `ALLOWED_TOOLS` and `TOOL_CHOICES` — the branch preamble the model sees is also taken from the same constant, so "what the model is told it may use" and "what the execution layer actually allows" cannot diverge. The cost is that users cannot narrow the permissions themselves; narrowing them means changing the code.
|
|
89
126
|
|
|
@@ -95,28 +132,41 @@ All routes first pass through the trust fence of `ctx.connection.requestRejectio
|
|
|
95
132
|
|
|
96
133
|
| Method and path | Request body | Response |
|
|
97
134
|
| --- | --- | --- |
|
|
98
|
-
| `POST /side-branch/start` | `{ sessionId, question, selection?, conversationId?, locale?, settings? }` | `{ jobId, conversationId, handoff? }`; on failure `{ error, code }`, possibly with `conversationGone: true` or `contextFull: { used, limit, window }` |
|
|
99
|
-
| `GET /side-branch/stream?job=<jobId>` | — | SSE: `snapshot` / `delta` / `reasoning` / `reset` / `replace` / `tool` / `done` / `error` / `stopped` |
|
|
135
|
+
| `POST /side-branch/start` | `{ sessionId, question, selection?, conversationId?, locale?, settings? }` | `{ jobId, conversationId, handoff?, synced?, notice? }`; on failure `{ error, code }`, possibly with `conversationGone: true` or `contextFull: { used, limit, window }` |
|
|
136
|
+
| `GET /side-branch/stream?job=<jobId>` | — | SSE: `snapshot` / **`segment`** / `delta` / `reasoning` / `reset` / `replace` / `tool` / `done` / `error` / `stopped` |
|
|
100
137
|
| `POST /side-branch/stop` | `{ job }` | `{ status }`, stops only this turn, the side session is kept |
|
|
101
138
|
| `POST /side-branch/close` | `{ conversationId }` | `{ status }`, releases this segment's side session |
|
|
139
|
+
| `GET /side-branch/inherited?conversationId=&locale=` | — | **read-only**: the main-session turns this segment **actually inherited** (`{ turns, chars, truncated, parentHasNewer, synced }`; the last turn in full, earlier turns as one-line summaries) |
|
|
102
140
|
| `GET /side-branch/settings` | — | `{ settings: { quickEntry } }` |
|
|
103
141
|
| `POST /side-branch/settings` | `{ settings: { quickEntry } }` | `{ settings: { quickEntry } }` |
|
|
104
142
|
|
|
143
|
+
The meaning of the three optional fields in the `/start` response (the client uses them to decide what to do):
|
|
144
|
+
|
|
145
|
+
- `handoff: true` = this call **rebuilt** the segment (re-fork). The history **does** connect ⇒ the client must **not** draw a "a new segment starts here" divider.
|
|
146
|
+
- `synced: { turns, chars }` = how much main-session content this turn inherited (the panel's `Inherited N main-session turns · ~X chars` row).
|
|
147
|
+
- `notice` = **the branch preamble verbatim, as injected into the model** (returned only on a turn where it was actually prepended, i.e. the first turn of a segment). This is the **only** text this plugin injects; the system prompt and the tool declarations are assembled by DSH, do not belong to this plugin, and are not handed out here.
|
|
148
|
+
|
|
149
|
+
SSE payload changes (0.2.0): `segment` is new (entering the next step ⇒ a new segment); `snapshot`/`done`/`stopped`/`error` all carry `segments` (the ordered segments `{kind, text, turn, step}`); `reset`/`delta`/`reasoning`/`replace`/`tool` all carry `turn`/`step` (the client uses them to scope a `reset` precisely to **the same step**).
|
|
150
|
+
|
|
105
151
|
There is only one setting: `quickEntry` (the switch for the main session's quick entry button). Tool lists submitted by the client are **not accepted** — the allow-list is a host-side constant, and the plugin does not offer a "choose your own available tools" feature. If a door is ever to be opened for that feature, read the "read-only must be guaranteed at the execution layer" section at the head of `lib/index.js` first.
|
|
106
152
|
|
|
107
153
|
Failure responses carry a machine-readable `code`, always with a `side-branch/` prefix in kebab-case, for example `side-branch/question-too-long`, `side-branch/context-full`. The client branches on `code`, not on the message text. When the path exists but the method is wrong, the reply is **405 + `Allow`** (the table of supported methods is `ROUTE_METHODS` in `lib/index.js`), not 404.
|
|
108
154
|
|
|
109
155
|
## Verifying changes
|
|
110
156
|
|
|
111
|
-
> ⚠️
|
|
112
|
-
>
|
|
113
|
-
>
|
|
157
|
+
> ⚠️ Both commands below **are only valid in the source repository**: neither `scripts/smoke.mjs` nor
|
|
158
|
+
> `scripts/client-check.mjs` is published with the npm package (the first reads `lib/` and `package.json`
|
|
159
|
+
> and makes source-level assertions, the second really runs the client bundle; neither is meaningful outside
|
|
160
|
+
> the source tree).
|
|
161
|
+
> If you are double-checking an npm-installed plugin, run them in the repository.
|
|
114
162
|
|
|
115
163
|
```powershell
|
|
116
|
-
node scripts/smoke.mjs
|
|
164
|
+
node scripts/smoke.mjs # ① source-level contract self-check
|
|
165
|
+
node scripts/client-check.mjs # ② client runtime self-check
|
|
166
|
+
# both at once: npm run check | only the client one: npm run client-check
|
|
117
167
|
```
|
|
118
168
|
|
|
119
|
-
|
|
169
|
+
**① `scripts/smoke.mjs` (source-level)** needs no DSH runtime; it loads the host half directly and calls `apply()` with a fake ctx. There are currently **153 assertions**, in ten sections:
|
|
120
170
|
|
|
121
171
|
1. the module loads, the export name matches `package.json`, the route prefix is `/side-branch`, and the unload disposer is callable;
|
|
122
172
|
2. **the read-only promise**: `ALLOWED_TOOLS` and `TOOL_CHOICES` are still those six names and consistent with each other, and none of the nine dangerous tool names is among them; the client has no leftover tool switches and the host does not accept a tool list submitted by the client; the branch preamble's list comes directly from the constants; the guard's criterion is still to allow by list, its registration point is still inside `setup(agentCtx)`, and failing to obtain the guard is still fail-loud.
|
|
@@ -124,21 +174,63 @@ It needs no DSH runtime; it loads the host half directly and calls `apply()` wit
|
|
|
124
174
|
3. the client bundle exists, the inject is still those five service names, and there are no debug exports;
|
|
125
175
|
4. the package names in `package.json` and `cordis.patch.yml` match;
|
|
126
176
|
5. every package in `dsh.client.inject` really exists in the local DSH installation directory (give the path with `DSH_INSTALL_DIR`; if it cannot be found, skip this item);
|
|
127
|
-
6. **the archive-gate countermeasure**: the unlock must come before `followup`, must be feature-detected with `typeof`, and must not throw; the re-hide must hang off `finishJob`, use a bounded backoff, and equally must not throw; creation must still archive first. This section guards the "one published version serves both 0.1.5 and 0.1.7" line
|
|
177
|
+
6. **the archive-gate countermeasure**: the unlock must come before `followup`, must be feature-detected with `typeof`, and must not throw; the re-hide must hang off `finishJob`, use a bounded backoff, and equally must not throw; creation must still archive first. This section guards the "one published version serves both 0.1.5 and 0.1.7" line;
|
|
178
|
+
7. **the Re-Fork architecture (0.2.0)**: `shouldReFork` must test **both** "the main session settled a new turn" and "the model/reasoning effort changed"; `parentCut` must be recorded **when the segment is created** (and carried through sleep/wake, or every turn re-forks for nothing); the replay allow-list is **exactly** the 5 surface event types (`system/message`, `developer/message`, `user/message`, `assistant/message`, `tool/result`) and contains no pure log events; the replay **never copies `sourceEventSeqs`**; the replay **never truncates**; **replay succeeds first, then the old session is released** (on failure the just-created segment is discarded); every re-fork answers `handoff: true`; the old `seedSource` handover path is gone for good. This section also guards the three spots of the estimate switch: `estimateRequestTokens` / `lastPromptTokensOf` exist, and the old `conv.lastContextTokens` criterion (necessarily `undefined` after a re-fork) is **no longer** used;
|
|
179
|
+
8. **multi-segment answers (0.2.0)**: the ordered `job.segments`; the `start` frame being split by `turn`/`step` (a changed `attemptId` within the same step means a retry ⇒ only that step is dropped; a changed step ⇒ `segment` opens a new one); `replace` rewriting **only** the current segment; `snapshot` and the terminal events carrying `segments`; `syncSegmentShortcuts` making `text`/`reasoning` a **pure function** of the segment array; the old `attempts` field split into `steps`/`retries`; the client's `normalizeSegments` staying backward-compatible with **old data** (which only had `answer`/`reasoning`); They also guard "the quote is recorded with the round": sending stores the clipped reference on that round (`reference`), renders it as a collapsible quote block above the question, and **clears the composer's quote slot**; `normalizeRound` keeps it, so the block survives a refresh.
|
|
180
|
+
9. **startup sweep and the read-only route (0.2.0)**: `/inherited` registered in `ROUTE_METHODS` (without it a wrong method falls into 404 and breaks the 405 + `Allow` contract); `handleInherited` truncating to "the last turn in full + one line per earlier turn"; the inherited content **not** entering `normalizeRound` (component memory only); the orphan criterion requiring the directory name to be **exactly** `side-<uuid>`; the **instance lease** (`side-branch-instances/*.json`, heartbeat + PID liveness); a live foreign instance making it **skip the entire sweep**; deletions leaving a **manifest log**; the sweep strictly confined under the `sessions` root and recognising only the `side-` prefix (never the `session-*` main sessions).
|
|
181
|
+
10. **parity with the main session (0.2.2)**: `ANSWER_TIMEOUT_MS` (the 90-second wall-clock cap) is **gone**, leaving only the `STALL_TIMEOUT_MS` stall watchdog (>= 10 minutes, re-armed by **every** event: job creation, stream frames, session events); `turn/end`'s `error` reason now carries the concrete `message` to the panel (no more "ended abnormally: error"); the selection entry's scope predicate `insideMainConversation` exists and is called by `selectionText`, testing `[data-conversation-content]` / `[data-conversation-scroll]` and excluding `[data-sidebar-right-panel]` / `[data-sidebar-right-session]` / `[data-sidebar-chat]` and the composer seat.
|
|
182
|
+
|
|
183
|
+
**① does not verify**: answer quality, real guard interception (those items in section 2 are **source-level regex assertions**, not a driven interception), SSE behaviour or the interface — the client half is covered by ② below, up to the rendered tree; real DOM layout, CSS, real scroll positions, the real network and a real model are equally out of its reach, and those spots depend on the one-off headless end-to-end harness **outside the repository** (see below) plus manual checking.
|
|
184
|
+
|
|
185
|
+
### ② Client runtime self-check (`scripts/client-check.mjs`)
|
|
186
|
+
|
|
187
|
+
```powershell
|
|
188
|
+
node scripts/client-check.mjs # run just this one
|
|
189
|
+
node scripts/client-check.mjs <another client.js> # reverse-verify the script itself (see below)
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
**Why it exists.** Looking at the panel by hand during development turned up several bugs in a row, and **all three existing automations missed them**: `node --check` only looks at syntax; `scripts/smoke.mjs` is **source-level regex assertions**; the end-to-end harness hits the **HTTP routes and never loads the client at all**. The worst one: constants such as `const MAX_SEGMENTS = 200` were declared **inside `createPanelStore()`** yet used in the **`SideBranchBody` component** ⇒ the first SSE `delta` frame threw `ReferenceError: MAX_SEGMENTS is not defined`, which the browser's event dispatch **swallowed** (it only shows up in the console) ⇒ every increment was lost and **the answer never appeared**, so the user saw "the panel looks stuck after asking". This script was written to close that hole, and it was **verified in reverse**: run against a deliberately broken copy it does FAIL and prints its own diagnostic `FAIL SSE 帧 snapshot 的处理函数抛错:MAX_SEGMENTS is not defined`.
|
|
193
|
+
|
|
194
|
+
**How it does it.** No browser is installed; instead it **really runs** `lib/client.js` inside Node: the bundle's shape is `window.__ModuleLoader__.load({ factory })`, and it takes only three things from `require` — `react`, `react-dom` and `@deepseek-ai/dsh-client-ui-primitives` (the last two are **deliberately not provided**, and the bundle carries its own degraded branches, which conveniently exercises those branches too); only **8** React APIs are used (`createElement` / `useState` / `useEffect` / `useLayoutEffect` / `useReducer` / `useRef` / `Fragment` / `Component`), so a minimal React + minimal DOM is enough. The panel's capabilities **all come from the slot's `inject` face** (`inject: () => ({ startSideBranch, … })`), which the script spreads into props exactly as the official host does — which also verifies that the keys the face should have are there. Then real-shaped `fetch` / `EventSource` stubs drive it: type into the input through the real `onChange`, press send through the real `onClick`, and feed in SSE frames of the real shape.
|
|
128
195
|
|
|
129
|
-
|
|
196
|
+
**What it verifies.** The bundle loads, exports `apply()`, and the body component is registered and renders successfully; the client registers its dictionaries with the locale service (so the assertions can watch **real copy** rather than the weak "returns the key" kind); pressing send really issues **one** `POST /side-branch/start` carrying the question verbatim and opens an SSE stream for that job; every frame fed in has a listener; **all multi-segment answer text is rendered, in the right order, with the tool line between the two answer segments** (the regression criterion for the "only the last segment is left" bug); the injected branch preamble is visible in full; and **auto-scroll lands on the layer that actually scrolls** (`.dsh-side-branch-scroll`), while the non-scrolling rounds list has no ref attached. It also covers **the quote entry's scope**: a fake DOM builds the ancestor chains (main-session body, right-sidebar file preview, settings overlay, an embedded sub-agent conversation in the right sidebar, the composer seat, this plugin's own panel) and drives the real `selectionchange` → 120 ms debounce → `update()` path, asserting that the button appears exactly where it should and nowhere else.
|
|
197
|
+
|
|
198
|
+
There are currently **42 items**; PASS/FAIL is read straight from stdout (when everything passes the last line is the script's own `全部通过(42 项)`, and any failure exits with a non-zero code).
|
|
199
|
+
|
|
200
|
+
⚠️ **What it does not cover (recorded honestly)**: real DOM layout, CSS, real scroll positions, real browser events, the real network and a real model. It is a test of the **"data → rendered tree"** stretch, **not a browser test** — those spots still have to be eyeballed in a browser.
|
|
201
|
+
|
|
202
|
+
**The optional argument reverse-verifies the script itself**: `node scripts/client-check.mjs <another client.js>` makes it run a different bundle (for instance a deliberately broken copy), and that one **must FAIL**. **A test that cannot fail is no test at all.**
|
|
203
|
+
|
|
204
|
+
**Manual** end-to-end verification needs:
|
|
130
205
|
|
|
131
206
|
1. a profile with this plugin installed;
|
|
132
207
|
2. a main session with at least one completed turn;
|
|
133
208
|
3. select text on the page, open the panel, ask a question;
|
|
134
209
|
4. check the startup log for lines with the `[side-branch]` prefix.
|
|
135
210
|
|
|
136
|
-
|
|
211
|
+
The repository has **two self-checks (source-level + client runtime), but no end-to-end suite**. When changing the guard, the prompt prefix, the model-facing text (`PROMPT_TEXT`), the archive gate, Re-Fork, multi-segment answers or the SSE spots, steps 2 to 4 must be run manually; the same applies when changing the **client half** (`lib/client.js`) — `scripts/client-check.mjs` only automates the "data → rendered tree" stretch, and the interface itself still has to be looked at.
|
|
212
|
+
|
|
213
|
+
### The one-off headless end-to-end harness
|
|
214
|
+
|
|
215
|
+
The repository has **no** end-to-end suite (it is a test asset: not published with the npm package, and it does not belong in the working tree). When a change lands where the self-checks cannot reach — the guard's real denials, the prompt prefix, the archive gate, Re-Fork, multi-segment answers, the SSE spots — build a **one-off** harness. Its approach: inside **one DSH process** it plays two roles at once —
|
|
216
|
+
|
|
217
|
+
- **the client**: it first passes the official trust fence to obtain a browser session cookie, then hits the plugin's **real** `POST /side-branch/start` and reads the SSE stream from `GET /side-branch/stream`;
|
|
218
|
+
- **the main session**: it uses `ctx.agents.create` to build a **real** parent session (`agentPresets.resolve` + `mount` with the `standard` preset, so it **has tools**) and runs real turns.
|
|
219
|
+
|
|
220
|
+
It covers exactly what the unit check (`scripts/smoke.mjs`) cannot reach: the branch **really used a tool** on that turn (`tool/call` + `tool/result` both present); a follow-up after the main session advanced ⇒ re-fork (`conversationId` changed, `handoff: true`, the inherited turn count rising); after the re-fork the model could answer with **three different origins** at once (a token that exists only in the branch history / one that exists only in the latest main-session prefix / one that exists only in the **replayed tool result**); the main session unchanged ⇒ **no** rebuild; only the reasoning effort changed ⇒ a rebuild **too**; `GET /side-branch/inherited` returning 200 with the last turn in full and a wrong method returning **405 + `Allow: GET`**; the branch's own token **not** flowing back into the main session; the finished branch **back in the archive set**; a forged `conversationId` answering `conversation-gone`; and "the replayed `tool/result` matching the `toolCallId` of the `tool/call` in the new segment's log" (orphan tool results are the nastiest trap on this path).
|
|
221
|
+
|
|
222
|
+
⚠️ Use it only for a **pre-release** pass: do not commit the harness, and do not write its logs or verdicts into `docs/`.
|
|
137
223
|
|
|
138
224
|
## Committing and pushing
|
|
139
225
|
|
|
140
226
|
- **Commit messages are always in English.** This is a public repository and its history is read in English: use `type: imperative subject` (e.g. `fix: un-archive the side session before each turn`) and explain "why" in the body. Do not mix Chinese into it.
|
|
141
227
|
- Prefer two commits: one for the `lib/` + `scripts/` behaviour change, one for documentation / version bumps.
|
|
142
|
-
- **Before pushing**, run `node scripts/smoke.mjs` (previous section) and run the affected path manually in a real host.
|
|
143
|
-
- Keep the version in `package.json` and `docs/CHANGELOG.md` in sync; tag releases as `v<version>` (`v0.1.0`
|
|
228
|
+
- **Before pushing**, run `node scripts/smoke.mjs` and `node scripts/client-check.mjs` (previous section; or `npm run check` to run both at once) and run the affected path manually in a real host.
|
|
229
|
+
- Keep the version in `package.json` and `docs/CHANGELOG.md` in sync; tag releases as `v<version>` (`v0.1.0`, `v0.1.1` and `v0.2.0` exist; this release tags `v0.2.2`).
|
|
230
|
+
- **Releases go through GitHub Actions (npm Trusted Publishing / OIDC) with staged publishing.** Pushing a `v<version>` tag triggers `.github/workflows/release.yml`, which checks the tag against `package.json`, runs both self-checks, and then `npm stage publish`es the tarball into the stage queue. A maintainer approves it under **Staged versions** on npmjs.com (security key) before the version goes live and `latest` moves.
|
|
231
|
+
- The trusted publisher is **stage-only** (only `npm stage publish` is allowed, `npm publish` is not): npm rejects a direct publish from that workflow, so even a malicious tag push can only leave a stage entry that still needs a human approval.
|
|
232
|
+
- One-time setup: npmjs.com → the package → **Settings → Trusted Publishing** → add GitHub Actions with Repository = `wucl12/dsh-side-branch`, Workflow filename = `release.yml`, and only `npm stage publish` under Allowed actions.
|
|
233
|
+
- Approval **has to happen on the website**: the local 2FA method is a security key, while `npm stage approve` on the CLI wants a TOTP code.
|
|
234
|
+
- `npm stage` needs npm >= 11.15.0, so the workflow installs `npm@^11.15.0` explicitly instead of trusting whatever the runner ships.
|
|
235
|
+
- A local `npm publish` is an emergency path only: npm now requires 2FA or a granular token with bypass, and the latter is being retired.
|
|
144
236
|
- Remote: `origin` = `github.com/wucl12/dsh-side-branch`, main branch `main`.
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,46 @@
|
|
|
2
2
|
|
|
3
3
|
[中文](#中文版本记录) · English · [README](../README.md)
|
|
4
4
|
|
|
5
|
+
## 0.2.2
|
|
6
|
+
|
|
7
|
+
Two bugs fixed. Both were cases of a branch behaving differently from the main session for no good reason.
|
|
8
|
+
|
|
9
|
+
- **A turn no longer has a wall-clock timeout.** Every turn used to be killed 90 seconds after it started ("Request timed out (90s)"), even while the model was still streaming, still thinking, or inside a long tool run — while the same question in the main session simply kept waiting. The hard cap is gone. What remains is a **stall watchdog**: it only fires after 30 minutes with **no event at all**, and every stream frame and every session event (tool calls, usage, messages) resets it. So a long turn is never cut off, while a genuinely wedged job cannot stay "running" forever (which would leave that branch permanently busy and leak a concurrency slot). A dead model stream is still caught earlier by the platform's own `streamIdleTimeoutMs` (5 minutes per request, reset on every chunk).
|
|
10
|
+
- **The "quote and ask" entry now appears only in the main session's body.** The entry listens on `document` so it works anywhere in a conversation, and it used to be filtered by a blacklist of inputs — which meant that selecting text in the right-hand sidebar (an open markdown file, the settings dialog, another plugin's panel) also raised it. Clicking it then quoted **text that is not part of the main session** into the branch's prompt. The entry now requires the selection to sit inside the conversation surface (`[data-conversation-content]`, falling back to `[data-conversation-scroll]`) and excludes the right sidebar — including the embedded sub-agent conversation it can host, which carries the same surface marker — and the composer seat. No conversation surface on screen means no entry (fail-closed) rather than a fallback to the old guesswork.
|
|
11
|
+
- **A failed turn now says why.** `turn/end` with `reason: { kind: 'error', error }` used to be flattened into "The ask ended abnormally: error", dropping the real cause (provider error, platform stream timeout, model offline). The message/code is now shown to the user.
|
|
12
|
+
- **0.2.1's documentation work ships here.** 0.2.1 was never published on its own; the README rewrite, the metadata changes and the `engines.dsh` declaration are all in this release.
|
|
13
|
+
- **Verified on DSH `0.2.1-rc.3`** in an isolated playground instance: the plugin loads, the `/side-branch` routes register and answer (200, and 405 + `Allow` for a wrong method), and the client bundle the browser receives carries the scoped entry. The long-turn path was not re-run against a real model in this round — it is covered by the new source-level assertions instead.
|
|
14
|
+
|
|
15
|
+
## 0.2.1
|
|
16
|
+
|
|
17
|
+
Documentation and metadata only — no behaviour change, no code change.
|
|
18
|
+
|
|
19
|
+
- **README rebuilt around what a first-time reader needs.** The first screen leads with the three situations the plugin is for, the install command and the restart warning follow immediately, and the guarantees are stated as outcomes rather than as mechanisms.
|
|
20
|
+
- **The comparison table moved to the end**, and one row is left as a deliberate loss: `dsh-sidebar-qa` supports nested follow-ups, this plugin does not.
|
|
21
|
+
- **Dropped an unsupported claim.** The README and `TECHNICAL.md` used to say that comparable plugins usually implement read-only through the prompt layer (`toolFilter` / `tools.restrict`). That was never verified, and it is not true of the closest alternative — the text now explains the design decision without asserting what anyone else does.
|
|
22
|
+
- **Documented how the plugin was built.** The codebase was written by DSH itself; the human work was direction, plan review and hands-on verification. See *How this was built* in the README.
|
|
23
|
+
- **Bilingual `package.json` description, English first, plus an `engines.dsh: >=0.1.5-rc.2` declaration.** dsh-market reads the latter for host-aware discovery and evaluates it with `includePrerelease`, so it is a declaration and not a gate.
|
|
24
|
+
- **Verified on DSH `0.2.0-rc.2`** — the plugin loads, the `/side-branch` routes register, and the panel was exercised by hand. The `0.2.0` boot-time peer gate only inspects `@deepseek-ai/dsh*` peer ranges, which this plugin declares none of.
|
|
25
|
+
|
|
26
|
+
## 0.2.0
|
|
27
|
+
|
|
28
|
+
Behaviour change: the branch now **follows the main session** instead of being frozen at the moment it was opened.
|
|
29
|
+
|
|
30
|
+
- **Re-fork.** Every follow-up whose main session has settled a new turn — or whose model/reasoning effort changed — rebuilds the branch: a fresh side session is seeded from the main session's **latest settled prefix**, the branch's own questions and answers are replayed into it, and only then is the new question delivered. The branch therefore always sees the main session as it is now, while keeping everything it was asked before.
|
|
31
|
+
- **Only when something changed.** If the main session has not advanced and the model is unchanged, the existing segment simply continues — the seed would be byte-identical, so rebuilding would only add a session record (which the platform cannot delete) and a deep copy for no gain.
|
|
32
|
+
- `conversationId` changes **whenever** a re-fork happens, and `/start` answers `handoff: true` so the panel does not draw a "new segment" divider: the history really is continuous.
|
|
33
|
+
- **Failure is explicit.** If rebuilding fails, the previous segment is left untouched and the turn is refused with `side-branch/refork-failed`. The plugin never silently falls back to "a new session with no history" — the model would think the branch had never been asked anything while the panel still shows the old answers.
|
|
34
|
+
- **Fixed: a turn with several steps only showed the last step.** A turn can contain several steps, and every step opens a new streaming attempt and emits its own `start` frame; the plugin treated *every* `start` as a retry and cleared the accumulated text, so step 2 wiped step 1, and with only one `text` slot and one `reasoning` slot the intermediate output could not be kept at all. A job now keeps an **ordered `segments` list** (`{kind: 'reasoning'|'text'|'tool', text, turn, step}`), `start` frames are split by `turn`/`step` (same step with a new `attemptId` = retry, so only that step's segments are dropped), `chunk`/`tool/call` append to the current segment, and the terminal `replace` rewrites **only** the current segment. New SSE event `segment`; `snapshot`, `done`, `stopped` and `error` now carry `segments` so a refresh keeps every segment.
|
|
35
|
+
- This also fixes a related wobble: when the last step had no text (a pure tool finish), the old "replace the whole answer with the final text" branch never fired and the panel kept *some* intermediate segment as the final answer.
|
|
36
|
+
- **Context limit is now estimated instead of read.** After a re-fork the new session has no usage yet, so the old `lastContextTokens` check cannot work. The host estimates: the parent prefix's prompt tokens (measured — the `input + cacheRead + cacheWrite` of the parent's last `assistant/message`) plus the branch history and question in characters. At the limit the turn is **refused** with the actual numbers (`contextFull: { used, limit, window }`); history is never truncated.
|
|
37
|
+
- **New: the panel shows what was inherited and what was injected.** A `▸ Inherited N main-session turns · ~X chars` row per segment, expandable into a lazy read-only `GET /side-branch/inherited` view (the last inherited turn in full, earlier turns as one-line summaries — truncated server-side, no model call, never persisted into panel state because a main-session prefix can be hundreds of kilobytes and panel state lives in `sessionStorage`). The branch notice that is injected into the prompt is also shown once, verbatim. System prompts and tool declarations are assembled by DSH, not by this plugin, and are deliberately not shown.
|
|
38
|
+
- **The inherited reasoning is shown too, and counted.** The main session's thinking is part of the seed and *is* sent to the model — `dsh-llm-deepseek` serializes an assistant history `reasoning` block as `{ type: 'thinking' }` (only user and tool-result content drops reasoning). Each inherited turn therefore has its own collapsible `Thinking · N chars`, and `~X chars` counts question + reasoning + answer. Counting only the visible text under-reported what the model actually receives.
|
|
39
|
+
- **The per-turn question is the first `user/message` of that turn only.** DSH appends its own runtime snapshot (file policy, approval policy, skills, time context — over a thousand characters) as a *second* `user/message` after every user turn, and concatenating them displayed that boilerplate as "what the user asked" and inflated the character count several-fold.
|
|
40
|
+
- **The quoted text is now part of the record.** Selecting text in the main session and clicking "Quote and ask" used to put the passage only into the prompt envelope: the panel showed just the question, so a round never recorded what had been quoted, and the one-line quote chip stayed in the composer until it was cleared by hand. A round now keeps the clipped reference (the exact text the model received) and renders it as a collapsible quote block above the question, and sending clears the composer's quote slot automatically so the next question starts clean. The quote survives a refresh with the rest of the panel state.
|
|
41
|
+
- **New: orphan side-session records are swept at startup.** Re-fork creates one session record per rebuild and the platform has **no** API to delete a session, so records accumulate. At `apply()` the plugin deletes `side-<uuid>` records that no instance can still be using. The sweep is guarded by an **instance lease** (`<DSH_HOME>/side-branch-instances/<pid>-<uuid>.json` with a heartbeat): if any other live instance shares the same `DSH_HOME`, the sweep is skipped entirely, so a second DSH never has its live branches deleted. Everything deleted is logged as a manifest.
|
|
42
|
+
- **Removed:** the "switch model ⇒ carry history into a new segment" path (`seedSource`, the `sameAgentOptions` handoff, `skipPrependNotice` handover). Re-fork subsumes it: changing the model is just another reason to rebuild. A `reasoningEffort` change counts as a model change too, so the chosen effort is never silently ignored.
|
|
43
|
+
- The prompt text now tells the model that its inherited history is rebuilt from the main session on every turn, instead of describing it as a snapshot taken when the branch was opened.
|
|
44
|
+
|
|
5
45
|
## 0.1.1
|
|
6
46
|
|
|
7
47
|
Fixes the branch being blocked on DSH `0.1.7-rc.1` and later.
|
|
@@ -30,6 +70,48 @@ First public release.
|
|
|
30
70
|
|
|
31
71
|
## 中文版本记录
|
|
32
72
|
|
|
73
|
+
## 0.2.2
|
|
74
|
+
|
|
75
|
+
修两个 BUG。两个都是"侧枝在没有理由的地方表现得和主会话不一样"。
|
|
76
|
+
|
|
77
|
+
- **一轮不再有总时长超时。** 以前每一轮从建 job 起算 90 秒就被无条件掐断(面板显示「请求超时(90 秒)」),哪怕模型还在流式输出、还在思考、或正卡在一次长工具里;而同样的提问在主会话里只是继续等。硬上限已删除,只留一个**停滞看门狗**:**整整 30 分钟一个事件都没有**才触发,而任何一个流式帧、任何一个会话事件(工具调用、用量、消息)都会重新计时。所以长回合永远不会被它掐断,而真卡死的 job 也不会永驻(永驻会让这一段会话永久 busy,还占着并发名额)。模型流真断掉的情况仍然由平台自己的 `streamIdleTimeoutMs`(每请求 5 分钟、收到 chunk 就重置)先收口。
|
|
78
|
+
- **「引用并提问」只在主会话正文区出现。** 入口的监听挂在 `document` 上(这样在会话里划字都能用),过滤原本只有一层输入框黑名单——于是右栏(打开的 md、设置弹层、其它插件的面板)里划字也会弹,点下去就把**不属于主会话的原文**引进了侧枝的 prompt。现在要求选区落在会话正文里(`[data-conversation-content]`,旧版退回 `[data-conversation-scroll]`),并排除右栏(含右栏里的子代理嵌入会话——它自己也带同样的正文标记)与 composer 座位。屏幕上没有会话正文时**不弹**(fail-closed),不再退回旧的黑名单猜测。
|
|
79
|
+
- **失败的一轮现在会说原因。** `turn/end` 的 `reason: { kind: 'error', error }` 以前被压成「提问意外终止:error」,真正的原因(供应商报错、平台流空闲超时、模型下线)全丢了;现在把 message/code 显示给用户。
|
|
80
|
+
- **0.2.1 的文档改动随这一版发布。** 0.2.1 没有单独发布;README 重写、元数据改动与 `engines.dsh` 声明都在这一版里。
|
|
81
|
+
- **已在 DSH `0.2.1-rc.3` 上验证**(隔离试验场):插件加载成功、`/side-branch` 路由注册并能应答(200;方法不对回 405 + `Allow`),浏览器收到的 client bundle 带上了限定作用域的引用入口。这一轮**没有**再用真实模型跑长回合,那部分由新增的源码级断言守着。
|
|
82
|
+
|
|
83
|
+
## 0.2.1
|
|
84
|
+
|
|
85
|
+
仅文档与元数据,无行为改动、无代码改动。
|
|
86
|
+
|
|
87
|
+
- **README 按「第一次来的人需要什么」重写。** 首屏先讲这个插件适用于哪三种情况,紧接着就是安装命令与重启提示;各项保证改成讲结果,而不是讲机制。
|
|
88
|
+
- **对比表挪到文末**,并保留一行主动认输:`dsh-sidebar-qa` 支持嵌套追问,本插件不支持。
|
|
89
|
+
- **删掉一条没有依据的说法。** README 与 `TECHNICAL.md` 原先写「同类插件普遍在提示词层实现只读(`toolFilter` / `tools.restrict`)」。这从未核实过,而且对最接近的替代品并不成立——现在只讲我们自己的设计取舍,不再断言别人怎么做。
|
|
90
|
+
- **补上「这个插件是怎么写出来的」**:整个代码库由 DSH 自己完成,人负责的是定方向、审计划、亲手验证。见 README 的《它是怎么被写出来的》。
|
|
91
|
+
- **`package.json` 的 description 改为中英双语、英文在前**,并声明 `engines.dsh: >=0.1.5-rc.2`。dsh-market 会读取后者做宿主兼容性发现,并用 `includePrerelease` 求值,所以这是声明而不是闸门。
|
|
92
|
+
- **已在 DSH `0.2.0-rc.2` 上验证**——插件加载、`/side-branch` 路由注册、面板人工实跑通过。`0.2.0` 启动期的 peer 闸门只检查 `@deepseek-ai/dsh*` 的 peer 区间,本插件一个都没声明。
|
|
93
|
+
|
|
94
|
+
## 0.2.0
|
|
95
|
+
|
|
96
|
+
行为变更:侧枝不再冻结在开段那一刻,而是**跟着主会话走**。
|
|
97
|
+
|
|
98
|
+
- **Re-Fork(每轮重建)。** 只要主会话**结算了新的回合**,或者**模型/推理等级变了**,这一次追问就会重建侧枝:新建一条侧会话、种子取主会话**最新的已结算前缀**、把侧枝自己历来的问答**重放**进去,然后才投递新问题。于是侧枝看到的永远是"主会话此刻的样子",同时一条都不丢自己之前问过什么。
|
|
99
|
+
- **只在"真的有变化"时重建。** 主会话没长、模型也没换时,就沿用当前这一段 —— 种子会逐字节相同,重建只会白白多一条**平台删不掉**的会话记录和一次深拷贝。
|
|
100
|
+
- `conversationId` **在重建时**会变,而 `/start` 一律回 `handoff: true`,所以面板**不会**画"以下是新的一段"分隔线 —— 历史确实是连续的。
|
|
101
|
+
- **失败是显式的。** 重建失败时**上一段原样保留**,这一轮以 `side-branch/refork-failed` 拒绝。插件绝不静默降级成"没有历史的新会话" —— 那会让模型以为侧枝没聊过,而面板上还显示着旧答案。
|
|
102
|
+
- **修复:一个回合里多段回答只剩最后一段。** 一个 turn 可以含多个 step,每个 step 都会开一次新的流式 attempt、各发一个 `start` 帧;插件把**每一个** `start` 都当成重试并清空已累积正文,于是 step 2 一来 step 1 就没了,而 job 只有 `text`/`reasoning` 两个槽位、根本装不下多段。现在 job 保有**有序的 `segments`**(`{kind: 'reasoning'|'text'|'tool', text, turn, step}`),`start` 帧按 `turn`/`step` 分流(同 step 换 `attemptId` 才算重试 ⇒ 只丢那一个 step 的段落),`chunk`/`tool/call` 追加到当前段,终局的 `replace` **只**重写当前段。新增 SSE 事件 `segment`;`snapshot`/`done`/`stopped`/`error` 都带 `segments`,刷新后每一段都在。
|
|
103
|
+
- 顺带修掉一处"显示哪一段是飘忽的":最后一步**没有正文**(纯工具收尾)时,老代码"用终局文本整段替换"那条分支根本不触发,面板会留着**某一段中间输出**当最终答案。
|
|
104
|
+
- **上下文上限改成估算。** re-fork 之后新会话还没有 usage,老判据 `lastContextTokens` 用不了。现在按"父前缀的提示词 tokens(**实测**:父会话最后一条 `assistant/message` 的 `input + cacheRead + cacheWrite`)+ 侧枝历史与问题的字符数"估算。到上限就**拒绝**并把实际数字给面板(`contextFull: { used, limit, window }`),**绝不截断**历史。
|
|
105
|
+
- **新增:面板能看见"继承了什么、注入了什么"。** 每段一行 `▸ 已继承主会话 N 轮 · 约 X 字`,点开按需拉一次只读的 `GET /side-branch/inherited`(**最后一轮全文 + 更早每轮一行摘要**,服务端截断、不调模型;**绝不进面板持久化状态** —— 一份主会话前缀可能几十万字符,而面板状态走 `sessionStorage`)。注入给模型的分支引导词也**逐字**展示一次。系统提示词与工具声明是 DSH 自己组装的,不属于本插件,**刻意不显示**。
|
|
106
|
+
- **继承来的"思考"也展示、也计数。** 主会话的推理**在种子里**,而且**确实会发给模型** —— `dsh-llm-deepseek` 把 assistant 历史里的 `reasoning` 块序列化成 `{ type: 'thinking' }`(只有 user / tool-result 内容才丢掉推理)。所以每轮里多一层可折叠的 `思考 · N 字`,而 `约 X 字` = **问 + 思考 + 答**。只算正文是**低报**:模型实际收到的比面板显示的多得多。
|
|
107
|
+
- **每轮的「问」只取该轮第一条 `user/message`。** DSH 会在每个用户回合之后**追加一条自己那上千字的运行期快照**(文件策略 / 审批策略 / 技能清单 / 时间上下文)作为**第二条** `user/message`;拼起来看就会把那一大坨平台样板文字当成"用户问的话",字数也被撑大好几倍。
|
|
108
|
+
- **面板现在真的会自动滚到底了。** 原先"自动滚底"的 ref 挂在**不滚动**的轮次列表(`.dsh-side-branch-rounds`,`display:flex`)上,`scrollTop = scrollHeight` 是空操作 —— 只是以前内容短、看不出来。在轮次上方加了可展开的「继承 / 注入」区之后,新答案会被顶到可视区之外,现象就是"问了之后回答不出现"(其实回答已经在 DOM 里)。现在 ref 指向真正可滚动的 `.dsh-side-branch-scroll`,并在段落数变化与继承区展开/收起时也重新滚底。
|
|
109
|
+
- **划选入口不再在侧枝自己的面板里弹出。** 那个按钮的语义是"把**主会话**原文引用进来";在面板里点它会把这**一段自己**的内容引用回**同一段**(`quoteBus` 按最后聚焦的面板投递 ⇒ 确实会投给自己),形成自指循环。黑名单里加上了 `.dsh-side-branch-panel` 与模型选择悬浮窗。
|
|
110
|
+
- **引用原文现在会进入这一轮的记录。** 在主会话里划选文字、点「引用并提问」,以前这段话**只进 prompt 信封**:面板上只显示问题原文,所以一轮记录里查不到"我引用了什么",而输入框上方那行引用条会一直挂着,得手动点 × 或「清空」。现在这一轮会保留**收窄后的引用原文**(与模型收到的**逐字一致**),渲染成问题上方的可折叠引用块;发送后引用槽**自动清空**,下一轮从干净的输入框开始。引用块随面板状态一起落盘 ⇒ 刷新后还在。
|
|
111
|
+
- **新增:启动时清理孤儿的侧枝会话记录。** re-fork 每次重建都会新增一条会话记录,而平台**没有**删除会话的 API ⇒ 记录会堆积。`apply()` 时删掉 `side-<uuid>` 形状、不可能还有实例在用的记录。清理由**实例租约**(`<DSH_HOME>/side-branch-instances/<pid>-<uuid>.json` + 心跳)把守:只要还有**别的活着的实例**共用同一个 `DSH_HOME`,就**整个跳过**,所以同时跑第二个 DSH 时绝不会删掉它正在用的侧枝。删掉哪些会**逐条记进日志**。
|
|
112
|
+
- **删除:** "换模型 ⇒ 带历史换段"那条路(`seedSource`、`sameAgentOptions` 交接、`skipPrependNotice` 手动传递)。re-fork 把它整个吸收了:换模型只是"该重建"的又一个理由。`reasoningEffort` 的变化同样算换模型,所以用户选的推理等级不会再被静默忽略。
|
|
113
|
+
- 推给模型的文本现在告诉它"继承的历史**每轮都会用主会话当时的最新内容重建**",不再说"开段那一刻的快照"。
|
|
114
|
+
|
|
33
115
|
## 0.1.1
|
|
34
116
|
|
|
35
117
|
修复在 DSH `0.1.7-rc.1` 及之后版本上侧枝被拦的问题。
|
package/docs/README.zh-CN.md
CHANGED
|
@@ -1,88 +1,125 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 临时会话 Side Ask · DSH 侧枝分支
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/dsh-side-branch)
|
|
4
|
+
[](https://www.npmjs.com/package/dsh-side-branch)
|
|
5
|
+
[](../LICENSE)
|
|
6
|
+
[](https://github.com/deepseek-ai/deepseek-harness)
|
|
2
7
|
|
|
3
8
|
[English](../README.md) | 简体中文
|
|
4
9
|
|
|
5
|
-
|
|
10
|
+
**在主会话里划选一段文字,在右栏开一条只读分支追问。答案流式显示在侧栏,不进入主会话的模型上下文。**
|
|
6
11
|
|
|
7
|
-
|
|
12
|
+
有些问题你只想顺口一问:agent 正在重构,你想知道「这个函数别处是怎么用的」,但不想让主会话记住这次提问;主会话上下文已经很长,你不想为一句追问让它再膨胀一轮;或者主会话跑的是贵模型,你想换个便宜模型在边上试错。
|
|
8
13
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
* **已验证 DSH 版本:** `0.1.5-rc.2` 与 `0.1.7-rc.2`
|
|
14
|
+
- 基于当前对话追问,主会话上下文一个 token 都不增加。
|
|
15
|
+
- 分支可以单独指定模型和推理等级,不用跟随主会话。
|
|
16
|
+
- 长任务在主线跑着,你在侧栏问别的,互不打断。
|
|
13
17
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
18
|
+
```bash
|
|
19
|
+
dsh plugin --profile web add dsh-side-branch
|
|
20
|
+
# 装完必须重启 DSH——客户端插件只在启动时装载,刷新浏览器没用
|
|
21
|
+
```
|
|
17
22
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
23
|
+
`--profile` 用你实际在跑的那个(`web`、`desktop`……)。需要 Node.js ≥ 20,且 `PATH` 上有 `pnpm`。
|
|
24
|
+
|
|
25
|
+
> **这个插件的代码是 agent 自己写的。** 本仓库的每一行代码都由 DSH 在真实会话里完成;作者负责的是工作流:定方向、审计划、亲手验证行为。详见 [它是怎么被写出来的](#它是怎么被写出来的)。
|
|
21
26
|
|
|
22
27
|
<p align="center">
|
|
23
|
-
<img src="../assets/panel-answering.png" alt="
|
|
28
|
+
<img src="../assets/panel-answering.png" alt="分支在右侧栏流式作答" width="700">
|
|
24
29
|
</p>
|
|
25
30
|
|
|
26
|
-
##
|
|
31
|
+
## 它有什么不一样
|
|
27
32
|
|
|
28
|
-
|
|
33
|
+
**答案不回流。** 宿主端从不把分支的输出写进父会话。你在分支里问过什么,就留在分支里——主会话的 agent 不会莫名其妙地「知道」你在旁边聊了什么。
|
|
29
34
|
|
|
30
|
-
|
|
31
|
-
2. **支持同一段内多轮追问**(不是一次性气泡)。
|
|
32
|
-
3. **逐词流式输出**(包含推理过程)。
|
|
33
|
-
4. **轻量稳定**(只使用官方公开 API,无臃肿依赖)。
|
|
34
|
-
5. **执行层只读,且绝不破坏前缀缓存**。
|
|
35
|
+
**它碰不到你的文件。** 只读是在**调用工具的那一刻**拦下的,不是靠把工具从提示词里藏起来:`ctx.tools.guard()` 只放行六个无副作用的工具(`read`、`glob`、`grep`、`web_search`、`web_fetch`、`lsp`),其余一律拒绝。因为工具仍留在提示词里,分支的提示词前缀与主会话逐字一致,能继续命中父会话攒下的前缀缓存——实测 97%–98%,这也是「顺口问一句几乎不额外花钱」的原因。为什么这么做、代价是什么,见 [`TECHNICAL.zh-CN.md`](./TECHNICAL.zh-CN.md)。
|
|
35
36
|
|
|
36
|
-
|
|
37
|
+
**它跟着主会话走。** 每次追问都用主会话最新落定的内容重建分支,再把这条分支自己之前的问答放回去:主会话刚聊完的那一轮它已经看得到,你三轮前问过什么它也还记得。
|
|
37
38
|
|
|
38
|
-
|
|
39
|
-
本项目坚决选择将“只读”拦截放在**执行层**,分支的提示词前缀与主会话**逐字一致**,从而能够继续享受极高的前缀缓存命中率(实测可达 97%–98%)。
|
|
39
|
+
**注入内容可以逐字核对。** 面板里展开「已继承主会话」,就能逐轮看到分支实际收到了什么,连注入给模型的引导词全文都在。
|
|
40
40
|
|
|
41
|
-
##
|
|
41
|
+
## 安装
|
|
42
42
|
|
|
43
|
-
* **真分支,零污染:** 宿主端绝不向父会话写入任何内容。临时会话的问答不会出现在主会话的模型上下文中。
|
|
44
|
-
* **前缀缓存极速命中:** 自动读取主会话已完成的回合前缀作为种子。沿用主会话模型时,继续命中原有前缀缓存,无需重算。
|
|
45
|
-
* **执行层只读隔离:** 安全策略不依赖提示词。由底层 `ctx.tools.guard` 强制接管,仅白名单放行六个无副作用的查询类工具(`read`, `glob`, `grep`, `web_search`, `web_fetch`, `lsp`),其余指令(如写文件、执行脚本)在执行层被直接拦截。
|
|
46
|
-
* **极致的流式体验:** 正文与推理过程(可折叠的“思考”行)逐词流式输出。即使关闭浏览器标签,后台回答也不会中断,重新打开即可续接。
|
|
47
|
-
* **会话状态持久化:** 面板状态按「会话 + 面板格」本地持久化。闲置自动休眠,下次提问静默唤醒。
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
## 📦 安装与启用
|
|
51
|
-
|
|
52
|
-
安装后必须**重启 DSH** 以使客户端插件加载生效。
|
|
53
|
-
|
|
54
|
-
**通过 npm 安装 (推荐):**
|
|
55
43
|
```bash
|
|
44
|
+
# npm
|
|
56
45
|
dsh plugin --profile web add dsh-side-branch
|
|
57
|
-
```
|
|
58
46
|
|
|
59
|
-
|
|
60
|
-
```bash
|
|
47
|
+
# 或直接从 GitHub 装
|
|
61
48
|
dsh plugin --profile web add github:wucl12/dsh-side-branch
|
|
62
49
|
```
|
|
63
50
|
|
|
64
|
-
|
|
51
|
+
`--profile` 用你实际在跑的那个(`web`、`desktop`……)。装完**必须重启 DSH**——客户端插件只在启动时装载,刷新浏览器不够。
|
|
52
|
+
|
|
53
|
+
前置:Node.js ≥ 20,且 `PATH` 上有 `pnpm`(`dsh plugin` 会转发给 pnpm)。
|
|
54
|
+
|
|
55
|
+
也可以在市场里装——[dsh-market](https://github.com/dsh-market/dsh-market) 与 [awesome-dsh-plugin 目录](https://awesome-dsh-plugin.com) 都已收录。
|
|
56
|
+
|
|
57
|
+
> 🤖 **给 AI Agent 的提示:** 如果你是负责安装此插件或阅读源码的 Agent,请先读本仓库的 [`AGENTS.md`](../AGENTS.md)——硬性约束、架构细节与安装指引都在那里。
|
|
58
|
+
|
|
59
|
+
## 如何使用
|
|
60
|
+
|
|
61
|
+
1. **划选提问:** 在主会话里选中文字,点击弹出的「引用并提问」,分支就在右侧面板打开,原文作为引用一并发送;这段引用会**随该轮入档**(问题上方一个可折叠的引用块),输入框上方的引用槽**自动清空**,下一轮从干净的输入框开始。
|
|
62
|
+
2. **快速入口:** 主会话输入框右侧有一个 `↗` 按钮,点击直接呼出面板(可在设置中关闭)。
|
|
63
|
+
3. **独立设置:** 面板里可以给当前分支单独指定模型与推理等级,不用跟随主会话。
|
|
64
|
+
|
|
65
|
+
<p align="center">
|
|
66
|
+
<img src="../assets/selection-entry.png" alt="在主会话里划选文本,打开分支" width="700">
|
|
67
|
+
</p>
|
|
68
|
+
|
|
69
|
+
<p align="center">
|
|
70
|
+
<img src="../assets/sidebar-guide.png" alt="右侧栏引导页里的「临时会话」入口" width="700">
|
|
71
|
+
</p>
|
|
72
|
+
|
|
73
|
+
## 兼容性
|
|
74
|
+
|
|
75
|
+
| DSH | 状态 |
|
|
76
|
+
| --- | --- |
|
|
77
|
+
| `0.1.5-rc.2` | ✅ 已验证 |
|
|
78
|
+
| `0.1.7-rc.2` | ✅ 已验证(归档闸) |
|
|
79
|
+
| `0.2.0-rc.2` | ✅ 已验证——加载、`/side-branch` 路由注册、面板人工实跑 |
|
|
80
|
+
| `0.2.1-rc.3` | ✅ 已验证——加载、`/side-branch` 路由注册且能应答(200 / 405 + `Allow`),浏览器收到的 client bundle 带上了限定作用域的引用入口 |
|
|
81
|
+
|
|
82
|
+
版本相关的接缝(归档闸、`unarchiveSession`)走的是运行期特性探测而非版本号比较,所以一个已发布版本同时服务这四条线。细节见 [`TECHNICAL.zh-CN.md`](./TECHNICAL.zh-CN.md)。
|
|
83
|
+
|
|
84
|
+
`0.2.2` 的引用入口依赖 DSH 给会话正文加的那组标记(`[data-conversation-content]`,旧宿主退回 `[data-conversation-scroll]`)。宿主若两者都没有渲染,入口就不出现(而不是退回猜测);插件其余部分不受影响。
|
|
85
|
+
|
|
86
|
+
## 安全与限制说明
|
|
87
|
+
|
|
88
|
+
* **关于「只读」边界:** 这里的只读是指**不产生系统副作用**(不写文件、不执行命令)。但 `web_fetch` / `web_search` 仍有网络访问,`read` 可以读取进程权限内的任何文件。放行名单硬编码在代码里,不能通过设置放宽。
|
|
89
|
+
* **分支能看到什么:** 它用主会话**最新的已结算回合**重建,所以主会话**答完结算**的新回合会自动跟上;主会话**还没答完**的那一轮看不到(切点是最后一个 `turn/end`)——但**手动划选引用不受影响**:受限的是「自动看到」,不是「能不能引用」。
|
|
90
|
+
* **重建会留下会话记录:** 每次重建都新增一条侧会话记录,而平台**没有删除会话的 API**,记录会堆积。它们由插件启动时的孤儿清理回收,删了什么会逐条记日志。同一时刻有两个共用同一个 `DSH_HOME` 的实例在跑时,清理会整个跳过——第二个 DSH 绝不会删掉它正在用的分支。
|
|
91
|
+
* **到上下文上限是「拒绝」:** 到达上限时插件拒绝发送并提示清空,绝不静默丢掉最早的轮次。它报出的 `used` 是**估算值**,与该轮结束后的真实用量对不上是正常的。
|
|
92
|
+
* **缓存命中率是实测值,不是设计保证:** 97%–98% 是量出来的,不是承诺——平台可能在分支第一步**原地替换 system 节点**,那样从第 0 个 token 起全废。并且命中缓存 ≠ 不占上下文窗口,命中的 token 一样占窗口。
|
|
93
|
+
* **PTC 模式:** 主会话若用 `ptc` preset(纯代码执行工具面),分支会退化为纯文本问答,因为 `run_code` 永远被拒。
|
|
94
|
+
|
|
95
|
+
## 选型对比
|
|
96
|
+
|
|
97
|
+
这个生态里的侧边问答插件,差别主要在**答案最后落在哪**,以及**侧会话被允许做什么**。
|
|
98
|
+
|
|
99
|
+
| | **临时会话**(`dsh-side-branch`) | [`dsh-sidebar-qa`](https://github.com/ChenRuoT/dsh-sidebar-qa) | [`dsh-selection-toolbar`](https://github.com/suiyideali/dsh-selection-toolbar) |
|
|
100
|
+
| --- | --- | --- | --- |
|
|
101
|
+
| 答案最后落在哪 | 独立的一条分支;宿主端不回写 | 一条独立 DSH 会话 | **当前会话**——划选文本作为用户消息发进去 |
|
|
102
|
+
| 侧会话的工具权限 | 六个无副作用工具;调用写操作时被拒 | 其 README 未说明 | 同一条会话,因此是完整工具集 |
|
|
103
|
+
| 是否跟到主会话最新已结算回合 | 每轮追问都强制重建 | 可选——全量继承 / 摘要压缩 / 机械裁切 | 是——本来就是同一条会话 |
|
|
104
|
+
| 嵌套追问 / 追问记录树 | 无 | 有 | 无 |
|
|
105
|
+
| 运行时依赖 | 无 | 无(1.0.0 起) | — |
|
|
65
106
|
|
|
66
|
-
|
|
107
|
+
> 另外两列取自它们当时公开的 README,之后可能已变化。表里有一行是我们主动认输的:`dsh-sidebar-qa` 支持嵌套追问,本插件不支持。这张表是帮你选型,不是给人排名。
|
|
67
108
|
|
|
68
|
-
|
|
69
|
-
2. **快速入口:** 主会话底部输入框右侧有一个 `↗` 快速入口按钮,点击可直接呼出临时会话面板(可在设置中关闭)。
|
|
70
|
-
3. **独立设置:** 在临时会话面板中,支持为当前分支单独指定模型与推理等级,无需跟随主会话。
|
|
109
|
+
## 它是怎么被写出来的
|
|
71
110
|
|
|
72
|
-
|
|
111
|
+
这个插件是一次「吃自己的狗粮」的结果:整个代码库——宿主半、客户端半、`scripts/` 下的两套自检——都由 DSH 自己写,照着 [`AGENTS.md`](../AGENTS.md) 里的契约干活。没有一行代码是手写的。
|
|
73
112
|
|
|
74
|
-
|
|
75
|
-
* **上下文继承的快照机制:** 临时分支继承的是**开段那一刻**的历史快照。开启后,主会话产生的新内容不会自动同步到当前分支。若需包含新内容,请点击面板左下角的「清空」另起一段。
|
|
76
|
-
* **PTC 模式的特殊处理:** 若主会话使用 `ptc` preset(纯代码执行工具面),由于 `run_code` 永远被本插件拒绝,此分支将变为**纯文本问答模式**(无法调用任何工具)。
|
|
113
|
+
人的部分在流程上:决定做什么、定方向、在每个方案执行前审一遍、然后在真实会话里亲手验证结果。验证发现的问题会作为新需求回给它——归档闸的修法、多段回答只显示最后一段的 bug、以及整条 re-fork 设计,都是这样找出来的。
|
|
77
114
|
|
|
78
|
-
##
|
|
115
|
+
## 更多文档
|
|
79
116
|
|
|
80
117
|
* [English](../README.md):English documentation (the same content)。
|
|
81
118
|
* [`TECHNICAL.zh-CN.md`](./TECHNICAL.zh-CN.md):完整技术文档——前缀缓存机制、只读拦截链路、PTC 行为与全部已知限制。
|
|
82
|
-
* [`AGENTS.md`](../AGENTS.md)
|
|
119
|
+
* [`AGENTS.md`](../AGENTS.md):agent 干活时依据的架构契约与硬约束。
|
|
83
120
|
* [`SECURITY.zh-CN.md`](./SECURITY.zh-CN.md):安全承诺范围与漏洞报告渠道。
|
|
84
121
|
* [`CHANGELOG.md`](./CHANGELOG.md):版本更新历史。
|
|
85
122
|
|
|
86
|
-
##
|
|
123
|
+
## 许可证
|
|
87
124
|
|
|
88
|
-
[MIT](../LICENSE)
|
|
125
|
+
[MIT](../LICENSE)
|