dsh-compact-relay 0.5.0 → 0.5.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.
@@ -6,9 +6,11 @@
6
6
  import { betterCompact, cancelBetterCompact, previewBetterCompact } from '../actions/better-compact.js';
7
7
  import { readCompactDiffs } from '../actions/compact-diff.js';
8
8
  import { readDiagnostics } from '../actions/diagnostics.js';
9
+ import { forceArchiveSession } from '../actions/force-archive.js';
9
10
  import { handoff, previewHandoff } from '../actions/handoff.js';
10
11
  import { listHandoffDocs, readHandoffDoc } from '../actions/handoff-docs.js';
11
12
  import { recoverArchivedSession } from '../actions/recover.js';
13
+ import { readCompactRelayChain } from '../actions/relay-chain.js';
12
14
  import { togglePin } from '../actions/pin.js';
13
15
  import { setNativeAutoCompactionEnabled } from '../actions/native-compaction.js';
14
16
  import { readState, resolveTarget } from '../actions/state.js';
@@ -183,6 +185,38 @@ export function registerHttp(webCtx, deps) {
183
185
  });
184
186
  webCtx.effect(() => stateStop, 'compact-relay: state endpoint');
185
187
 
188
+ // 0.5.1 DCR-05:完整交接链(一次读完,最多 8 层)。
189
+ //
190
+ // 旧客户端逐级 GET `/api/compact-relay-state?sessionId=父id`,宿主对未加载的祖先返回
191
+ // `sessionId:null` → 整条链只剩 1 层。这里改为服务端只读持久化日志(不加载会话),
192
+ // 把整条链一次返回;旧宿主没有 sessionPersistence 时返回 `ok:false`,客户端回落逐级读。
193
+ const chainStop = webCtx.webServer.register({
194
+ kind: 'exact',
195
+ path: '/api/compact-relay-chain',
196
+ handler: (req, res) => {
197
+ void (async () => {
198
+ try {
199
+ const rejection = hostRejection(webCtx, req);
200
+ if (rejection !== undefined) return denyAuth(res, rejection);
201
+ if (req.method !== 'GET') return send(res, 405, { ok: false, message: '只支持 GET' });
202
+ const url = new URL(req.url ?? '/', 'http://127.0.0.1');
203
+ const sessionId = url.searchParams.get('sessionId') ?? '';
204
+ if (typeof sessionId !== 'string' || sessionId.length === 0) {
205
+ return send(res, 400, { ok: false, message: '缺少 sessionId' });
206
+ }
207
+ send(res, 200, await readCompactRelayChain(deps, { sessionId }));
208
+ } catch (error) {
209
+ try {
210
+ send(res, 500, { ok: false, message: errorText(error) });
211
+ } catch {
212
+ /* 连接可能已断 */
213
+ }
214
+ }
215
+ })();
216
+ },
217
+ });
218
+ webCtx.effect(() => chainStop, 'compact-relay: relay chain endpoint');
219
+
186
220
  const streamStop = webCtx.webServer.register({
187
221
  kind: 'exact',
188
222
  path: '/api/compact-relay-stream',
@@ -383,6 +417,21 @@ export function registerHttp(webCtx, deps) {
383
417
  if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
384
418
  return send(res, 400, { ok: false, message: '请求体必须是 JSON 对象' });
385
419
  }
420
+ // 0.5.1 批 2:交接的归档开关 / 置顶确认只接受布尔值;类型错在动历史前就拒绝。
421
+ if (parsed.archive !== undefined && typeof parsed.archive !== 'boolean') {
422
+ return send(res, 400, { ok: false, message: 'archive 必须是 true/false(false = 只交接、不归档旧会话)' });
423
+ }
424
+ if (parsed.confirmUnpin !== undefined && typeof parsed.confirmUnpin !== 'boolean') {
425
+ return send(res, 400, { ok: false, message: 'confirmUnpin 必须是 true/false(true = 确认归档并取消旧会话置顶)' });
426
+ }
427
+ // 0.5.2 L 项:强制归档的破坏性动作只有用户点过二次确认才允许执行
428
+ // (confirmStop:true = 会停止旧会话上正在跑的 turn/subagent/job/schedule)。
429
+ if (parsed.confirmStop !== undefined && typeof parsed.confirmStop !== 'boolean') {
430
+ return send(res, 400, {
431
+ ok: false,
432
+ message: 'confirmStop 必须是 true/false(true = 确认停止旧会话上的任务后归档)',
433
+ });
434
+ }
386
435
  // 多数动作作用在具体会话上:缺 sessionId 或类型错时绝不能退化成「最后一个 agent」
387
436
  // (2026-10-19 debug:旧实现会压错会话)。
388
437
  // 例外:
@@ -391,8 +440,10 @@ export function registerHttp(webCtx, deps) {
391
440
  // 恒 400「缺少 sessionId」,于是「关闭自动压缩」永远写不进去、也看不到反馈。
392
441
  // - recover 作用在**已归档**的旧会话上,它通常没有活 agent;不能走 resolveTarget
393
442
  // (否则恒 409「没有活动会话」)。sessionId 由 recover 分支自己校验。
443
+ // - force-archive 同样作用在**旧会话**(交接前的那个)上,通常没有活 agent 可解;
444
+ // 它自己的动作层会校验 sessionId 与 confirmStop,不走 resolveTarget。
394
445
  let agent = null;
395
- if (parsed.action !== 'native-auto' && parsed.action !== 'recover') {
446
+ if (parsed.action !== 'native-auto' && parsed.action !== 'recover' && parsed.action !== 'force-archive') {
396
447
  if (typeof parsed.sessionId !== 'string' || parsed.sessionId.length === 0) {
397
448
  return send(res, 400, { ok: false, message: '缺少 sessionId' });
398
449
  }
@@ -409,6 +460,26 @@ export function registerHttp(webCtx, deps) {
409
460
  return send(res, 200, outcome);
410
461
  }
411
462
 
463
+ if (parsed.action === 'force-archive') {
464
+ // 0.5.2 L 项:用户确认「会停止旧会话上正在跑的任务」后强制归档(唯一 stopActivity 调用点)。
465
+ if (typeof parsed.sessionId !== 'string' || parsed.sessionId.length === 0) {
466
+ return send(res, 400, { ok: false, message: '缺少要强制归档的旧会话 sessionId' });
467
+ }
468
+ if (parsed.confirmStop !== true) {
469
+ return send(res, 400, {
470
+ ok: false,
471
+ code: 'force-archive',
472
+ message: '强制归档会停止旧会话上正在跑的任务,需要显式确认(confirmStop:true)',
473
+ });
474
+ }
475
+ const outcome = await forceArchiveSession(deps, {
476
+ sessionId: parsed.sessionId,
477
+ confirmStop: true,
478
+ confirmUnpin: parsed.confirmUnpin,
479
+ });
480
+ return send(res, 200, outcome);
481
+ }
482
+
412
483
  if (parsed.action === 'preview') {
413
484
  // M6:只算不写——先告诉用户「要压哪段、多少 token」,再让他确认。
414
485
  // A3:返回 planId,confirm 会带着它回来校验「同一段范围」。
@@ -462,12 +533,19 @@ export function registerHttp(webCtx, deps) {
462
533
  }
463
534
  if (parsed.action === 'handoff') {
464
535
  // 0.4 P2-1:`edits` 只允许「约束」「下一步」两段(多余的段/非法类型由 actions 拒绝)。
465
- const outcome = await handoff(deps, { agent, edits: parsed.edits });
536
+ // 0.5.1 批 2:archive 缺省 true(0.5.0 行为);confirmUnpin 只有用户确认「归档会取消置顶」时才传。
537
+ const outcome = await handoff(deps, {
538
+ agent,
539
+ edits: parsed.edits,
540
+ archive: parsed.archive,
541
+ confirmUnpin: parsed.confirmUnpin,
542
+ });
466
543
  return send(res, 200, {
467
544
  ok: true,
468
545
  message: outcome.message,
469
546
  childId: outcome.childId,
470
547
  path: outcome.path,
548
+ archive: outcome.archive,
471
549
  });
472
550
  }
473
551
  return send(res, 400, { ok: false, message: `未知动作:${String(parsed.action)}` });
@@ -494,6 +572,14 @@ export function registerHttp(webCtx, deps) {
494
572
  if (code === 'busy') {
495
573
  return send(res, 409, { ok: false, code, message: errorText(error) });
496
574
  }
575
+ // 0.5.2 L 项:强制归档失败要把**具体原因**带回界面(不静默、不吞成通用文案)。
576
+ if (code === 'force-archive') {
577
+ return send(res, 400, {
578
+ ok: false,
579
+ code,
580
+ message: error instanceof Error ? error.message : String(error),
581
+ });
582
+ }
497
583
  if (error?.statusCode === 413) {
498
584
  return send(res, 413, { ok: false, message: '请求体太大' });
499
585
  }