dsh-agent-tasks 0.1.0 → 0.1.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.
Files changed (3) hide show
  1. package/README.md +15 -1
  2. package/index.js +117 -19
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -34,6 +34,20 @@ AI-Agent-Hub 的**任务线 + agent 通信**工具集,共 **12 个 MCP 工具*
34
34
  (共 12 处),管理者协议 `managerProtocol:232`。本包为占位,下一步搬入;
35
35
  搬入后**工具只能注册一次**,其余包通过 `ctx.get('agentTasks')` 调用。
36
36
 
37
+ ## 已知坑(读消息 / 派单模板)
38
+
39
+ - **`taskline_messages` 恒返「没有消息」的根因(2026-11-10 修)**:两个叠加——①旧实现只读
40
+ 活会话的 `session.events`;②持久日志把回复存成 `assistant/chunk` 的 block-end,正文**不在**
41
+ `data.message.content`。修法:走 `dsh-hub-core` 的 `readSessionEvents`(活 + 冷都能读)+
42
+ `compactSessionEvents` 重建正文。判据:`/sync/messages` 能读到而 `taskline_messages` 读不到 =
43
+ 读法问题,不是会话空。
44
+ - **`compactSessionEvents` 返回 `{ events, lastSeq, streaming }`,不是数组**(`Array.isArray()` 判空会漏)。
45
+ - **它会把插件注入的 `user/message`(`source.kind !== 'user'`)过滤掉**,所以「原始 64 条 → 可展示 11 条」
46
+ 是正常的,不是丢消息。
47
+ - **eventId 恒为 `seq`**(真实事件没有 `id`/`key`),压缩后仍保留 `seq`,路由键不会漂移。
48
+ - **§20 用户可见验收**:`taskline_create` / `taskline_prompt` 的 description 里已写死模板
49
+ (可打开产物 / 可复现命令输出 / 重启后一步核验),**禁自证式验收**。
50
+
37
51
  ## License
38
52
 
39
- MIT
53
+ MIT
package/index.js CHANGED
@@ -61,10 +61,17 @@ import {
61
61
  routingPath,
62
62
  handoffsDir,
63
63
  triageDecisionOf,
64
+ readSessionEvents,
65
+ compactSessionEvents,
64
66
  } from 'dsh-hub-core';
65
67
 
66
68
  export function registerAgentTools({ hub, register, deps = {} }) {
67
- const { callerTitleOf, isRunningNow, pendingDispatchesOf, recordDispatch, resolveSession, snapshotTargetRunning, spawnQueueTriage, titleOfSession, waitForAgentIdle, wakeAgentMessageTarget, liveRunning, queueTriageEnabled } = deps;
69
+ // defaultPreset(2026-11-10 修):12 个工具迁出 hub-desktop-ui 时漏注入了这个。
70
+ // 因为 taskline_create 里写的是 `args.preset || defaultPreset`(短路求值),
71
+ // 只要调用方传了 preset 就永远不会求值 → 一直没暴露;
72
+ // 一旦不传 preset 就直接 ReferenceError: defaultPreset is not defined。
73
+ // 兜底 'cordis' 与 hub-desktop-ui 那边 `config.defaultPreset || 'cordis'` 保持一致。
74
+ const { callerTitleOf, isRunningNow, pendingDispatchesOf, recordDispatch, resolveSession, snapshotTargetRunning, spawnQueueTriage, titleOfSession, waitForAgentIdle, wakeAgentMessageTarget, liveRunning, queueTriageEnabled, defaultPreset = 'cordis' } = deps;
68
75
  const disposers = [];
69
76
 
70
77
  // —— agent_message(2026-10-23 用户令;原 hub-desktop-ui L6191-6337)——
@@ -554,6 +561,10 @@ export function registerAgentTools({ hub, register, deps = {} }) {
554
561
  properties: {
555
562
  ok: { type: 'boolean' },
556
563
  error: { type: 'string' },
564
+ // t38-A:三态外显(ok=有消息 / empty=会话真空 / unreadable=读不到)
565
+ state: { type: 'string' },
566
+ title: { type: 'string' },
567
+ diagnosis: { type: 'string' },
557
568
  items: {
558
569
  type: 'array',
559
570
  items: {
@@ -572,7 +583,17 @@ export function registerAgentTools({ hub, register, deps = {} }) {
572
583
  },
573
584
  render: (_args, value) => {
574
585
  if (!value.ok) return [{ type: 'text', text: `读取失败:${value.error}` }];
575
- if (!value.items || value.items.length === 0) return [{ type: 'text', text: '(这个会话没有消息)' }];
586
+ if (!value.items || value.items.length === 0) {
587
+ // 三态可区分(t38-A,2026-11-10):空会话 ≠ 读不到。以前两者都显示
588
+ //「这个会话没有消息」,导致把「读不到」误当「没消息」,归线随之断链。
589
+ if (value.state === 'empty') {
590
+ return [{ type: 'text', text: `(会话存在但确实还没有消息:${value.diagnosis || value.title || ''})` }];
591
+ }
592
+ if (value.state === 'unreadable') {
593
+ return [{ type: 'text', text: `读取失败(读不到,不是空会话):${value.diagnosis || value.error || '未知原因'}` }];
594
+ }
595
+ return [{ type: 'text', text: '(这个会话没有消息)' }];
596
+ }
576
597
  return [{
577
598
  type: 'text',
578
599
  text: value.items
@@ -582,8 +603,34 @@ export function registerAgentTools({ hub, register, deps = {} }) {
582
603
  },
583
604
  },
584
605
  async execute(args) {
585
- const session = deps.resolveSession(args.session);
586
- if (!session) return { ok: false, error: '找不到这个会话' };
606
+ let session = deps.resolveSession(args.session);
607
+ if (!session) {
608
+ // t38-A(2026-11-10):resolveSession 只查内存 sessions.list(),冷/僵尸会话会被判「找不到」。
609
+ // 若传的是完整 sessionId,就先当 id 直读持久日志(与 /sync/messages 同口径),
610
+ // 读得到就正常返回;读不到再明确回「读不到」而不是含糊的「找不到」。
611
+ const raw = String(args.session || '').trim();
612
+ if (/^session-[0-9a-f-]{16,}$/i.test(raw)) {
613
+ let probe = [];
614
+ try {
615
+ const read = await readSessionEvents(hub, raw);
616
+ probe = Array.isArray(read && read.events) ? read.events : [];
617
+ } catch { /* 下面按读不到处理 */ }
618
+ if (probe.length) {
619
+ session = { id: raw, title: raw, coldFallback: true, events: probe };
620
+ } else {
621
+ return {
622
+ ok: true,
623
+ items: [],
624
+ state: 'unreadable',
625
+ title: raw,
626
+ diagnosis: '该 sessionId 既不在内存会话列表,持久日志也读不到(壳可能已被换掉或日志已删);'
627
+ + '请用 taskline_list 取当前 sessionId。',
628
+ };
629
+ }
630
+ } else {
631
+ return { ok: false, error: '找不到这个会话' };
632
+ }
633
+ }
587
634
  const limit = Number.isFinite(args.limit) && args.limit > 0 ? Math.floor(args.limit) : 20;
588
635
  // P6(2026-10-27 夜场):默认截断 140→400 字;full=true 不截断(上限 4000 防爆)。
589
636
  // 长回报的标准做法仍是「写临时文件 + 消息给路径」,full 只用于核对关键句。
@@ -591,21 +638,33 @@ export function registerAgentTools({ hub, register, deps = {} }) {
591
638
  const clip = (t) => String(t)
592
639
  .replace(/\s+/g, ' ')
593
640
  .slice(0, args.full === true ? fullCap : 400);
594
- const controller = hub.get('sessionController');
595
- const pools = [];
596
- if (session && Array.isArray(session.events)) pools.push(session.events);
597
- if (session && session.events && Array.isArray(session.events.entries)) pools.push(session.events.entries);
598
- if (pools.length === 0 && controller && typeof controller.inspect === 'function') {
641
+ // t38-A(2026-11-10 修「对部分会话恒返空/读不到」):
642
+ // 旧实现只看 session.events(活会话)→ controller.inspect 的 data.message(活格式)。
643
+ // 冷会话(如日常·Yat2 停着的线、PrimCell·Yat2)恰恰是:①内存里没有 events;
644
+ // ②持久日志把回复存成 assistant/chunk 的 block-end/text-chunks,正文不在 data.message。
645
+ // 于是两条都落空 → 恒返「没有消息」。现改为走 hub-core 的 readSessionEvents(活+冷都读)
646
+ // + compactSessionEvents(把 chunk 形态重建成 assistant/message 正文),
647
+ // 并对「会话存在但真空」与「读不到」分别给出可判定状态。
648
+ const sid = session.id || (session && session.sessionId) || '';
649
+ let events = [];
650
+ let readError = '';
651
+ try {
652
+ const read = await readSessionEvents(hub, sid);
653
+ events = Array.isArray(read && read.events) ? read.events : [];
654
+ } catch (error) {
655
+ readError = String((error && error.message) || error);
656
+ }
657
+ if (events.length) {
599
658
  try {
600
- const info = await controller.inspect(session.id);
601
- for (const candidate of [info.entries, info.events, info.prefix, info.record && info.record.events]) {
602
- if (Array.isArray(candidate)) { pools.push(candidate); break; }
603
- }
659
+ // compactSessionEvents 返回 { events, lastSeq, streaming }(不是数组,2026-11-10 实测)
660
+ const compacted = compactSessionEvents(events);
661
+ const rebuilt = Array.isArray(compacted) ? compacted
662
+ : (compacted && Array.isArray(compacted.events) ? compacted.events : []);
663
+ if (rebuilt.length) events = rebuilt;
604
664
  } catch (error) {
605
- console.warn(`[hub-desktop-ui] inspect failed: ${String(error)}`);
665
+ console.warn(`[hub-desktop-ui] compactSessionEvents failed: ${String(error)}`);
606
666
  }
607
667
  }
608
- const events = pools[0] || [];
609
668
  const items = [];
610
669
  for (const event of events) {
611
670
  if (!event || (event.type !== 'user/message' && event.type !== 'assistant/message')) continue;
@@ -629,7 +688,29 @@ export function registerAgentTools({ hub, register, deps = {} }) {
629
688
  text,
630
689
  });
631
690
  }
632
- return { ok: true, items: items.slice(-limit) };
691
+ // 三态判定(t38-A):有消息 / 会话确实空 / 读不到。
692
+ if (items.length === 0) {
693
+ // 「读不到」判据:连一个事件都没拿到(不是拿到事件但都无正文)。
694
+ if (!events.length) {
695
+ return {
696
+ ok: true,
697
+ items: [],
698
+ state: 'unreadable',
699
+ title: (session && (session.title || session.name)) || '',
700
+ diagnosis: `会话在注册表可见,但事件池为空${readError ? `(读取异常:${readError})` : ''}`
701
+ + ';可能是壳已换(旧 sessionId 残留)或持久日志未落盘。'
702
+ + '请用 taskline_list 取当前 sessionId 再试。',
703
+ };
704
+ }
705
+ return {
706
+ ok: true,
707
+ items: [],
708
+ state: 'empty',
709
+ title: (session && (session.title || session.name)) || '',
710
+ diagnosis: `读到 ${events.length} 个事件但无可展示正文`,
711
+ };
712
+ }
713
+ return { ok: true, items: items.slice(-limit), state: 'ok' };
633
714
  },
634
715
  }));
635
716
 
@@ -1185,14 +1266,25 @@ export function registerAgentTools({ hub, register, deps = {} }) {
1185
1266
  // —— taskline_create ——
1186
1267
  disposers.push(register({
1187
1268
  name: 'taskline_create',
1188
- description: '新建一条任务线:创建一个新会话并命名(可选立刻给它派第一条任务)。',
1269
+ description: '新建一条任务线:创建一个新会话并命名(可选立刻给它派第一条任务)。\n'
1270
+ + '**任务书里必须带「用户可见验收」段**(§20,2026-11-10 用户令):'
1271
+ + '写清用户能**直接看/点/复制**的验收物——截图、可打开的文件或 URL、一段可粘贴的输出——'
1272
+ + '而不是「测试通过 / 已自测」这类自证。模板:\n'
1273
+ + '【验收(用户可见)】①<可打开的产物路径或 URL>;②<用户一条命令能复现的输出>;'
1274
+ + '③<若涉及重启,写清重启后一步核验的方法>。\n'
1275
+ + '**不写自证式验收("单测通过""逻辑正确")**:验收物必须由用户侧可见,审核者据此判定完成。',
1189
1276
  parameters: {
1190
1277
  type: 'object',
1191
1278
  additionalProperties: false,
1192
1279
  required: ['title'],
1193
1280
  properties: {
1194
1281
  title: { type: 'string', description: '新任务线标题,建议「<项目> · <成员>」' },
1195
- text: { type: 'string', description: '可选的初始任务内容' },
1282
+ text: {
1283
+ type: 'string',
1284
+ description: '可选的初始任务内容。**必须含「【验收(用户可见)】」段**:'
1285
+ + '列出用户可直接看/点/复制的验收物(产物路径或 URL、可复现命令与输出、重启后一步核验法),'
1286
+ + '禁止只写「测试通过」「已自测」等自证。',
1287
+ },
1196
1288
  preset: { type: 'string', description: '可选的 agent preset,默认 cordis(Creator mode)' },
1197
1289
  project: { type: 'string', description: '所属项目名,默认从 title 的「 · 」前解析' },
1198
1290
  avatar: { type: 'string', description: '头像 emoji,可不填(自动从候选池分配一个未被占用的)' },
@@ -1351,7 +1443,13 @@ export function registerAgentTools({ hub, register, deps = {} }) {
1351
1443
  required: ['session', 'text'],
1352
1444
  properties: {
1353
1445
  session: { type: 'string', description: '目标会话的 sessionId 或标题(可部分匹配)' },
1354
- text: { type: 'string', description: '要发给它的话,写清任务与验收标准' },
1446
+ text: {
1447
+ type: 'string',
1448
+ description: '要发给它的话,写清任务与验收标准。'
1449
+ + '**验收必须是「用户可见验收」**(§20):给出用户能直接看/点/复制的验收物'
1450
+ + '(产物路径或 URL、可复现命令及输出、重启后一步核验法),'
1451
+ + '不要写「测试通过」「已自测」这类自证;模板见 taskline_create。',
1452
+ },
1355
1453
  goal: {
1356
1454
  type: 'string',
1357
1455
  description: '本线目标(一句话、交付物导向)。传了会写进 tasks.json 的 goal 并把目标化指令插到 prompt 最前面;不传保持原行为',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-agent-tasks",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "type": "module",
5
5
  "description": "Task-line and agent-messaging tools for AI-Agent-Hub DSH projects: taskline_create/prompt/list/messages/archive/rename/waits, agent_message, routing_put, resume_session, plus the project-manager protocol injection.",
6
6
  "main": "index.js",