dsh-compact-relay 0.5.0 → 0.5.1
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/CHANGELOG.md +294 -0
- package/README.en.md +4 -3
- package/README.md +22 -5
- package/TUTORIAL.md +29 -5
- package/lib/actions/handoff.js +32 -21
- package/lib/actions/pin.js +7 -6
- package/lib/actions/relay-chain.js +17 -0
- package/lib/actions/state.js +127 -16
- package/lib/adapters/http.js +48 -1
- package/lib/client.js +1088 -149
- package/lib/core/clip.js +92 -0
- package/lib/core/errors.js +3 -1
- package/lib/core/native-line.js +27 -0
- package/lib/core/native-pin-risk.js +106 -0
- package/lib/core/pin-anchors.js +3 -1
- package/lib/core/pin-health.js +112 -0
- package/lib/dsh/compaction-log.js +16 -9
- package/lib/dsh/compaction-pair.js +27 -1
- package/lib/dsh/diag-counters.js +4 -1
- package/lib/dsh/diagnostics.js +3 -1
- package/lib/dsh/handoff-archive.js +47 -1
- package/lib/dsh/native-compaction.js +60 -4
- package/lib/dsh/relay-chain.js +238 -0
- package/lib/dsh/relay.js +108 -4
- package/lib/dsh/session-view.js +16 -10
- package/package.json +2 -2
package/lib/actions/pin.js
CHANGED
|
@@ -176,16 +176,17 @@ export function listPins(deps, { agent } = {}) {
|
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
/**
|
|
179
|
-
*
|
|
179
|
+
* 菜单用:有锁定(含全部已失效的快照)就清空,没有就锁最近一轮。
|
|
180
180
|
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
181
|
+
* 0.5.1 批 2:菜单在「全部 pin 已被原生压缩带走」时显示「解除失效锁定(N 条)」,
|
|
182
|
+
* 所以这里必须按**快照**判定:快照非空 → unpinAll,不能因为 live=0 就反过来新锁
|
|
183
|
+
* 一条(按钮文案与动作会完全相反)。`pinLastTurn`/`pinByIndex` 内部仍只带 live
|
|
184
|
+
* 集合进新快照,顺手清掉过期 seq。
|
|
184
185
|
* @param {{engine:object}} deps
|
|
185
186
|
* @param {{agent:object}} input
|
|
186
187
|
*/
|
|
187
188
|
export function togglePin(deps, { agent } = {}) {
|
|
188
189
|
const session = agent?.session;
|
|
189
|
-
if (session === null || session === undefined) throw actionError('pin', '
|
|
190
|
-
return
|
|
190
|
+
if (session === null || session === undefined) throw actionError('pin', '当前没有活动会话');
|
|
191
|
+
return latestPinnedSeqs(sessionLog(session)).length > 0 ? unpinAll(deps, { agent }) : pinLastTurn(deps, { agent });
|
|
191
192
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 完整交接链(只读)动作层 —— HTTP 适配层与 dsh 读取器之间的薄包装。
|
|
3
|
+
*
|
|
4
|
+
* 0.5.1 DCR-05:客户端点「完整交接链」时一次取回整条链;服务端按需只读持久化日志,
|
|
5
|
+
* **不把祖先会话加载进 ctx.sessions**(宿主对未加载会话的 readState 返回 sessionId:null,
|
|
6
|
+
* 逐级 GET 会停在断点)。读取细节见 dsh/relay-chain.js。
|
|
7
|
+
*/
|
|
8
|
+
import { readRelayChain } from '../dsh/relay-chain.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {{ctx:object}} deps
|
|
12
|
+
* @param {{sessionId?:string}} [input]
|
|
13
|
+
* @returns {ReturnType<typeof readRelayChain>}
|
|
14
|
+
*/
|
|
15
|
+
export function readCompactRelayChain(deps, { sessionId } = {}) {
|
|
16
|
+
return readRelayChain(deps?.ctx, { sessionId });
|
|
17
|
+
}
|
package/lib/actions/state.js
CHANGED
|
@@ -6,15 +6,19 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { paletteForBothSchemes } from '../core/colors.js';
|
|
8
8
|
import { adviceFor, ratioOf, thresholdsForWindow } from '../core/policy.js';
|
|
9
|
+
import { nativePinnedRiskOf } from '../core/native-pin-risk.js';
|
|
10
|
+
import { pinHealthOf } from '../core/pin-health.js';
|
|
11
|
+
import { latestPinnedSeqs } from '../core/pins.js';
|
|
9
12
|
import { tokenOf } from '../core/tokens.js';
|
|
10
13
|
import { measurePressure, routeBudgetOf } from '../dsh/pressure.js';
|
|
11
14
|
import { selectCompactableRange } from '../dsh/select-range.js';
|
|
12
|
-
import { compressHint as compressHintOf, countSummaries,
|
|
15
|
+
import { compressHint as compressHintOf, countSummaries, firstCompactableIdx, lastCheckpointSurfaceSeq, liveSessionOf, pickAgent, sessionLog, surfaceSeqs } from '../dsh/session-view.js';
|
|
13
16
|
import { compressHintIsActive } from '../core/compress-hint.js';
|
|
14
17
|
import { readNativeCompaction, resolveNativePolicy } from '../dsh/native-compaction.js';
|
|
15
|
-
import { nativeLineOf } from '../core/native-line.js';
|
|
16
|
-
import { relayChainOf, relayRecordOf } from '../dsh/relay.js';
|
|
17
|
-
import { isSessionArchived } from '../dsh/handoff-archive.js';
|
|
18
|
+
import { nativeLineOf, nativeRetainTokensOf } from '../core/native-line.js';
|
|
19
|
+
import { legacyChainParentIdOf, legacyRelayChainOf, relayChainOf, relayInboundRecordOf, relayOutboundRecordOf, relayRecordOf } from '../dsh/relay.js';
|
|
20
|
+
import { isSessionArchived, isSessionPinned, workspaceRegistryOf } from '../dsh/handoff-archive.js';
|
|
21
|
+
import { sourceTitleOf } from '../dsh/sessions.js';
|
|
18
22
|
|
|
19
23
|
/**
|
|
20
24
|
* 动作层统一解析「这次动作作用在哪个 agent」:只认显式 sessionId,
|
|
@@ -96,10 +100,20 @@ export async function readState(deps, { sessionId, agent } = {}) {
|
|
|
96
100
|
const compactRangeReason = compactRange !== null && !hasLiveAgent ? 'no_agent' : null;
|
|
97
101
|
|
|
98
102
|
// M6:钉住集合(菜单显示「清除钉住(N 条)」,也能在无 agent 时读 live session)。
|
|
99
|
-
|
|
103
|
+
// 0.5.1 批 2(C 方案):pin 快照 vs 当前 surface ——
|
|
104
|
+
// live = 还在 surface 上(环/计数/解锁仍按这些);
|
|
105
|
+
// stale = 快照里有、surface 上没了(原生压缩/其它压缩/历史变化带走)——
|
|
106
|
+
// 单独下发,界面标「已失效」,既不假装还在,也不凭空消失。
|
|
107
|
+
const surface = target === null ? [] : surfaceSeqs(target.session);
|
|
108
|
+
const events = target === null ? [] : sessionLog(target.session);
|
|
109
|
+
const pinHealth =
|
|
110
|
+
target === null || surface.length === 0
|
|
111
|
+
? // 没有 surface(会话刚打开/尚未回放)时不能把全部 pin 判成失效:那是「还不知道」。
|
|
112
|
+
{ live: [], stale: [], cause: null, causes: { native: 0, other: 0, unknown: 0 } }
|
|
113
|
+
: pinHealthOf({ pinnedSeqs: latestPinnedSeqs(events), surfaceSeqs: surface, log: events });
|
|
114
|
+
const pinnedSeqs = pinHealth.live;
|
|
100
115
|
// 0.3.1:锁定消息在 surface 里的相对位置(0–1,方案 A:消息在历史中的位置),
|
|
101
116
|
// 指示灯环上按位置画锁定块;多个锁定按位置从早到晚排序。
|
|
102
|
-
const surface = target === null ? [] : surfaceSeqs(target.session);
|
|
103
117
|
const surfaceIndex = new Map(surface.map((seq, index) => [seq, index]));
|
|
104
118
|
const pinMarks = pinnedSeqs
|
|
105
119
|
.filter((seq) => surfaceIndex.has(seq))
|
|
@@ -145,16 +159,88 @@ export async function readState(deps, { sessionId, agent } = {}) {
|
|
|
145
159
|
}
|
|
146
160
|
}
|
|
147
161
|
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
//
|
|
162
|
+
// 0.5.1 批 2:预判「下一次原生自动压缩会覆盖哪些锁定消息」——事前提示。
|
|
163
|
+
//
|
|
164
|
+
// 宿主信号:原生自动压缩只在 step boundary 触发(pressure ≥ 真实触发线后),
|
|
165
|
+
// 选段 = `selectCompactableRange(session, measurement, retainTokens)`:从第一个非
|
|
166
|
+
// system 节点起,逐字保留尾部 retainTokens,再按工具配对平衡回退。这里用同一份
|
|
167
|
+
// 定价节点(measured.nodes)+ 同一套保留预算复刻该范围,只读、不触发任何压缩。
|
|
168
|
+
//
|
|
169
|
+
// 只在「自动压缩开着 + 已到真实触发线 + 节点定价与 surface 对齐」时预警;
|
|
170
|
+
// 算不出/对不齐就返回 null(宁可不报,也不误报会覆盖)。
|
|
171
|
+
let pinRisk = null;
|
|
172
|
+
if (
|
|
173
|
+
target !== null &&
|
|
174
|
+
pinnedSeqs.length > 0 &&
|
|
175
|
+
nativeCfg.autoEnabled !== false &&
|
|
176
|
+
nativeLine !== null &&
|
|
177
|
+
nativeLine.ok === true &&
|
|
178
|
+
Number.isFinite(measured.tokens) &&
|
|
179
|
+
measured.tokens >= nativeLine.thresholdTokens &&
|
|
180
|
+
nativeBudget !== null &&
|
|
181
|
+
Array.isArray(measured.nodes) &&
|
|
182
|
+
measured.nodes.length === surface.length &&
|
|
183
|
+
measured.nodes.every((node, index) => node?.seq === surface[index])
|
|
184
|
+
) {
|
|
185
|
+
const retainTokens = nativeRetainTokensOf({
|
|
186
|
+
contextWindow: nativeBudget.contextWindow,
|
|
187
|
+
reservedCompletionTokens: nativeBudget.reservedCompletionTokens,
|
|
188
|
+
retainRatio: nativePolicy.retainRatio,
|
|
189
|
+
retainTokens: nativePolicy.retainTokens,
|
|
190
|
+
});
|
|
191
|
+
if (retainTokens !== null) {
|
|
192
|
+
const risk = nativePinnedRiskOf({
|
|
193
|
+
pinnedSeqs,
|
|
194
|
+
nodes: measured.nodes,
|
|
195
|
+
firstIdx: firstCompactableIdx(target.session),
|
|
196
|
+
retainTokens,
|
|
197
|
+
isBalancedBefore: (seq) => {
|
|
198
|
+
try {
|
|
199
|
+
return deps.engine?.toolPairingBalancedBefore?.(target.session, seq) === true;
|
|
200
|
+
} catch {
|
|
201
|
+
return true; // 判定失败时按「可切」处理:只影响预警范围,不会真的压
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
if (risk !== null) pinRisk = { count: risk.count, seqs: risk.seqs };
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
//
|
|
210
|
+
// `relay` 是旧字段:日志里**最近一条**记录(出站/入站都可能)——语义一个字节不改,
|
|
211
|
+
// 下一次交接的链号递增(handoffTitlePlanOf)也依赖它。
|
|
212
|
+
// `chain` 改成「本会话入站记录」(childId===本会话)的只读投影:祖先会话再次交接后,
|
|
213
|
+
// 最近的记录变成出站(childId=下一代),旧实现照它下发 → 客户端「是不是本会话入站」
|
|
214
|
+
// 判定不成立 → 接力条消失(DCR-01)。没有入站记录时(DCR-02)用会话头
|
|
215
|
+
// header.parentSession + 标题兜底合成只读链(排除 subagent 会话与 fork 出来的会话),
|
|
216
|
+
// 让 0.5.0 之前生成、子会话没有记录的老链也能显示接力条。
|
|
151
217
|
const relay = target === null ? null : relayRecordOf(target.session);
|
|
152
|
-
const
|
|
218
|
+
const inbound = target === null ? null : relayInboundRecordOf(target.session);
|
|
219
|
+
const chain =
|
|
220
|
+
target === null
|
|
221
|
+
? null
|
|
222
|
+
: inbound !== null
|
|
223
|
+
? relayChainOf(inbound)
|
|
224
|
+
: // DCR-02:只有会话头看起来像老链时才去读标题(普通会话不付这次查找开销)。
|
|
225
|
+
legacyChainParentIdOf(target.session) === null
|
|
226
|
+
? null
|
|
227
|
+
: legacyRelayChainOf({ session: target.session, title: sourceTitleOf(deps.ctx, target.session) });
|
|
228
|
+
// DCR-01:`next` = 本会话最近一条**出站**记录的只读投影(只加不改)。接力条不看它,
|
|
229
|
+
// 但「父→子入口」与「子会话向父会话补交接单 path」需要出站那一跳的信息。
|
|
230
|
+
const next = target === null ? null : relayChainOf(relayOutboundRecordOf(target.session));
|
|
153
231
|
// 0.5.0:旧会话是否已被自动归档(接力条据此显示「恢复旧会话」)。这是宿主
|
|
154
|
-
// registry 的独立状态,不塞进 chain(chain
|
|
232
|
+
// registry 的独立状态,不塞进 chain(chain 仍然只是链投影)。
|
|
233
|
+
// 0.5.1 DCR-01:用**入站** parentId 查(出站记录里的父会话是本会话自己,会查错)。
|
|
155
234
|
// 拿不到 registry/归档集合 → null(不知道,客户端按「可尝试恢复」处理)。
|
|
156
235
|
const parentArchived =
|
|
157
236
|
chain === null || chain.parentId === null ? null : isSessionArchived(deps.ctx, chain.parentId);
|
|
237
|
+
// 0.5.1 批 3:出站记录的目标会话(下一代)是否已在归档集合里。父会话侧的「去新会话」
|
|
238
|
+
// 用它决定要不要先 unarchive(宿主拒绝打开归档会话;unarchive 幂等)。
|
|
239
|
+
// 和 parentArchived 完全对称:各查各的 id,读不到 → null(不猜 false)。
|
|
240
|
+
const nextArchived = next === null || next.childId === null ? null : isSessionArchived(deps.ctx, next.childId);
|
|
241
|
+
// 0.5.1 DCR-03:宿主有没有「恢复归档会话」的接缝(HTTP recover 动作调的就是它)。
|
|
242
|
+
// 客户端拿不到 ctx.uiWorkspace.unarchiveSession 时据此隐藏「恢复旧会话」死入口。
|
|
243
|
+
const capabilities = { unarchive: typeof workspaceRegistryOf(deps.ctx)?.unarchiveSession === 'function' };
|
|
158
244
|
|
|
159
245
|
return {
|
|
160
246
|
revision: ++stateRevision,
|
|
@@ -166,7 +252,19 @@ export async function readState(deps, { sessionId, agent } = {}) {
|
|
|
166
252
|
summaries,
|
|
167
253
|
advice,
|
|
168
254
|
level: advice === 'none' ? 'idle' : advice,
|
|
169
|
-
|
|
255
|
+
// 0.5.1 批 2:stale = 快照里有、surface 上已没有的 pin(被压缩/历史变化带走)。
|
|
256
|
+
// `count` 仍是 live 集合(老客户端语义不变);`stale` / `atRisk` 只加不改。
|
|
257
|
+
pins: {
|
|
258
|
+
count: pinnedSeqs.length,
|
|
259
|
+
seqs: pinnedSeqs.slice(-20),
|
|
260
|
+
marks: pinMarks,
|
|
261
|
+
stale: {
|
|
262
|
+
count: pinHealth.stale.length,
|
|
263
|
+
seqs: pinHealth.stale.slice(-20),
|
|
264
|
+
...pinHealth.causes,
|
|
265
|
+
},
|
|
266
|
+
atRisk: pinRisk,
|
|
267
|
+
},
|
|
170
268
|
compactRange,
|
|
171
269
|
// 0.3.5:compactRange 非 null 但无活 agent 时为 'no_agent',其它情况 null。
|
|
172
270
|
compactRangeReason,
|
|
@@ -178,12 +276,25 @@ export async function readState(deps, { sessionId, agent } = {}) {
|
|
|
178
276
|
settingsError: deps.settings?.error ?? null,
|
|
179
277
|
// A5:最近一次交接的接力链记录(没有就是 null)。0.5.0 F4:旧字段原样保留。
|
|
180
278
|
relay,
|
|
181
|
-
// 0.5.0 F4
|
|
182
|
-
//
|
|
279
|
+
// 0.5.0 F4 / 0.5.1 DCR-01:本会话**入站**记录的只读链投影
|
|
280
|
+
// {ordinal, baseTitle, parentId, childId, scheme, path?};老链(无记录)可能是
|
|
281
|
+
// header 兜底合成的(多一个 synthesized:true)。没有链信息 → null。
|
|
183
282
|
chain,
|
|
184
|
-
// 0.5.
|
|
185
|
-
//
|
|
283
|
+
// 0.5.1 DCR-01:最近一条**出站**记录的只读投影(本会话交接给下一代那一跳)。
|
|
284
|
+
// 只加不改:老客户端忽略;接力条不用它。没有出站记录 → null。
|
|
285
|
+
next,
|
|
286
|
+
// 0.5.0:链上父会话(入站 parentId)当前是否在 workspace 归档集合里(true/false);
|
|
287
|
+
// 没有父会话或宿主没有可读的归档状态 → null。只加不改。
|
|
186
288
|
parentArchived,
|
|
289
|
+
// 0.5.1 批 3:出站目标(next.childId)是否在同一归档集合里。父会话侧「去新会话」
|
|
290
|
+
// 据此先恢复再切;没有出站记录或读不到归档集合 → null。只加不改。
|
|
291
|
+
nextArchived,
|
|
292
|
+
// 0.5.1 批 2:本会话(agent.session,不是 agent.id)是否在侧栏置顶。
|
|
293
|
+
// 交接前客户端据此弹「归档会取消置顶」确认;读不到置顶集合 → null(不猜)。
|
|
294
|
+
sessionPinned: target === null || target.session === undefined ? null : isSessionPinned(deps.ctx, target.session.id),
|
|
295
|
+
// 0.5.1 DCR-03:宿主能力位(目前只有「能不能恢复归档会话」)。老客户端忽略;
|
|
296
|
+
// 新客户端在缺 `unarchive` 能力时隐藏恢复入口,不把用户带进必然失败的入口。
|
|
297
|
+
capabilities,
|
|
187
298
|
// 0.3.1:模型判断的「适合压缩」断点位置(没有或已失效为 null)。
|
|
188
299
|
compressHint: activeHint,
|
|
189
300
|
// 0.3.1:DSH 原生自动压缩(环上的报警最高阈值 / 设置页开关的数据源)。
|
package/lib/adapters/http.js
CHANGED
|
@@ -9,6 +9,7 @@ import { readDiagnostics } from '../actions/diagnostics.js';
|
|
|
9
9
|
import { handoff, previewHandoff } from '../actions/handoff.js';
|
|
10
10
|
import { listHandoffDocs, readHandoffDoc } from '../actions/handoff-docs.js';
|
|
11
11
|
import { recoverArchivedSession } from '../actions/recover.js';
|
|
12
|
+
import { readCompactRelayChain } from '../actions/relay-chain.js';
|
|
12
13
|
import { togglePin } from '../actions/pin.js';
|
|
13
14
|
import { setNativeAutoCompactionEnabled } from '../actions/native-compaction.js';
|
|
14
15
|
import { readState, resolveTarget } from '../actions/state.js';
|
|
@@ -183,6 +184,38 @@ export function registerHttp(webCtx, deps) {
|
|
|
183
184
|
});
|
|
184
185
|
webCtx.effect(() => stateStop, 'compact-relay: state endpoint');
|
|
185
186
|
|
|
187
|
+
// 0.5.1 DCR-05:完整交接链(一次读完,最多 8 层)。
|
|
188
|
+
//
|
|
189
|
+
// 旧客户端逐级 GET `/api/compact-relay-state?sessionId=父id`,宿主对未加载的祖先返回
|
|
190
|
+
// `sessionId:null` → 整条链只剩 1 层。这里改为服务端只读持久化日志(不加载会话),
|
|
191
|
+
// 把整条链一次返回;旧宿主没有 sessionPersistence 时返回 `ok:false`,客户端回落逐级读。
|
|
192
|
+
const chainStop = webCtx.webServer.register({
|
|
193
|
+
kind: 'exact',
|
|
194
|
+
path: '/api/compact-relay-chain',
|
|
195
|
+
handler: (req, res) => {
|
|
196
|
+
void (async () => {
|
|
197
|
+
try {
|
|
198
|
+
const rejection = hostRejection(webCtx, req);
|
|
199
|
+
if (rejection !== undefined) return denyAuth(res, rejection);
|
|
200
|
+
if (req.method !== 'GET') return send(res, 405, { ok: false, message: '只支持 GET' });
|
|
201
|
+
const url = new URL(req.url ?? '/', 'http://127.0.0.1');
|
|
202
|
+
const sessionId = url.searchParams.get('sessionId') ?? '';
|
|
203
|
+
if (typeof sessionId !== 'string' || sessionId.length === 0) {
|
|
204
|
+
return send(res, 400, { ok: false, message: '缺少 sessionId' });
|
|
205
|
+
}
|
|
206
|
+
send(res, 200, await readCompactRelayChain(deps, { sessionId }));
|
|
207
|
+
} catch (error) {
|
|
208
|
+
try {
|
|
209
|
+
send(res, 500, { ok: false, message: errorText(error) });
|
|
210
|
+
} catch {
|
|
211
|
+
/* 连接可能已断 */
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
})();
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
webCtx.effect(() => chainStop, 'compact-relay: relay chain endpoint');
|
|
218
|
+
|
|
186
219
|
const streamStop = webCtx.webServer.register({
|
|
187
220
|
kind: 'exact',
|
|
188
221
|
path: '/api/compact-relay-stream',
|
|
@@ -383,6 +416,13 @@ export function registerHttp(webCtx, deps) {
|
|
|
383
416
|
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
384
417
|
return send(res, 400, { ok: false, message: '请求体必须是 JSON 对象' });
|
|
385
418
|
}
|
|
419
|
+
// 0.5.1 批 2:交接的归档开关 / 置顶确认只接受布尔值;类型错在动历史前就拒绝。
|
|
420
|
+
if (parsed.archive !== undefined && typeof parsed.archive !== 'boolean') {
|
|
421
|
+
return send(res, 400, { ok: false, message: 'archive 必须是 true/false(false = 只交接、不归档旧会话)' });
|
|
422
|
+
}
|
|
423
|
+
if (parsed.confirmUnpin !== undefined && typeof parsed.confirmUnpin !== 'boolean') {
|
|
424
|
+
return send(res, 400, { ok: false, message: 'confirmUnpin 必须是 true/false(true = 确认归档并取消旧会话置顶)' });
|
|
425
|
+
}
|
|
386
426
|
// 多数动作作用在具体会话上:缺 sessionId 或类型错时绝不能退化成「最后一个 agent」
|
|
387
427
|
// (2026-10-19 debug:旧实现会压错会话)。
|
|
388
428
|
// 例外:
|
|
@@ -462,12 +502,19 @@ export function registerHttp(webCtx, deps) {
|
|
|
462
502
|
}
|
|
463
503
|
if (parsed.action === 'handoff') {
|
|
464
504
|
// 0.4 P2-1:`edits` 只允许「约束」「下一步」两段(多余的段/非法类型由 actions 拒绝)。
|
|
465
|
-
|
|
505
|
+
// 0.5.1 批 2:archive 缺省 true(0.5.0 行为);confirmUnpin 只有用户确认「归档会取消置顶」时才传。
|
|
506
|
+
const outcome = await handoff(deps, {
|
|
507
|
+
agent,
|
|
508
|
+
edits: parsed.edits,
|
|
509
|
+
archive: parsed.archive,
|
|
510
|
+
confirmUnpin: parsed.confirmUnpin,
|
|
511
|
+
});
|
|
466
512
|
return send(res, 200, {
|
|
467
513
|
ok: true,
|
|
468
514
|
message: outcome.message,
|
|
469
515
|
childId: outcome.childId,
|
|
470
516
|
path: outcome.path,
|
|
517
|
+
archive: outcome.archive,
|
|
471
518
|
});
|
|
472
519
|
}
|
|
473
520
|
return send(res, 400, { ok: false, message: `未知动作:${String(parsed.action)}` });
|