@yeaft/webchat-agent 0.1.107 → 0.1.114

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.
@@ -17,7 +17,7 @@ import {
17
17
  import {
18
18
  createCrewSession, handleCrewHumanInput, handleCrewControl,
19
19
  addRoleToSession, removeRoleFromSession,
20
- handleListCrewSessions, handleCheckCrewExists, handleDeleteCrewDir, resumeCrewSession, removeFromCrewIndex,
20
+ handleListCrewSessions, handleCheckCrewExists, handleDeleteCrewDir, resumeCrewSession, removeFromCrewIndex, hideCrewSession,
21
21
  handleLoadCrewHistory
22
22
  } from '../crew.js';
23
23
  import { sendToServer, flushMessageBuffer } from './buffer.js';
@@ -240,7 +240,7 @@ export async function handleMessage(msg) {
240
240
  break;
241
241
 
242
242
  case 'delete_crew_session':
243
- await removeFromCrewIndex(msg.sessionId);
243
+ await hideCrewSession(msg.sessionId);
244
244
  (await import('../conversation.js')).sendConversationList();
245
245
  break;
246
246
 
package/conversation.js CHANGED
@@ -85,11 +85,11 @@ export async function sendConversationList() {
85
85
  type: 'crew',
86
86
  });
87
87
  }
88
- // 追加索引中已停止的 crew sessions(不重复)
88
+ // 追加索引中已停止的 crew sessions(不重复,跳过 hidden)
89
89
  try {
90
90
  const index = await loadCrewIndex();
91
91
  for (const entry of index) {
92
- if (!activeCrewIds.has(entry.sessionId)) {
92
+ if (!activeCrewIds.has(entry.sessionId) && !entry.hidden) {
93
93
  list.push({
94
94
  id: entry.sessionId,
95
95
  workDir: entry.projectDir,
@@ -76,6 +76,44 @@ export async function removeFromCrewIndex(sessionId) {
76
76
  // 这些文件在 recreate 时会被复用(合并统计数据 + 恢复消息历史)
77
77
  }
78
78
 
79
+ /**
80
+ * 隐藏 crew session(UI 删除 = 隐藏,不是真删)
81
+ * 在 crew-index 条目上标记 hidden: true + hiddenAt 时间戳,
82
+ * 同时从内存 crewSessions Map 中移除(停止运行态)。
83
+ * 不修改 session.json,不从 index 中移除条目。
84
+ */
85
+ export async function hideCrewSession(sessionId) {
86
+ const { crewSessions } = await import('./session.js');
87
+
88
+ const index = await loadCrewIndex();
89
+ const entry = index.find(e => e.sessionId === sessionId);
90
+ if (entry) {
91
+ entry.hidden = true;
92
+ entry.hiddenAt = Date.now();
93
+ await saveCrewIndex(index);
94
+ console.log(`[Crew] Hidden session ${sessionId} in index`);
95
+ }
96
+ // 从内存中移除(停止运行态,防止 sendConversationList 加入)
97
+ if (crewSessions.has(sessionId)) {
98
+ crewSessions.delete(sessionId);
99
+ console.log(`[Crew] Removed session ${sessionId} from active sessions`);
100
+ }
101
+ }
102
+
103
+ /**
104
+ * 取消隐藏 crew session(用户主动恢复时调用)
105
+ */
106
+ export async function unhideCrewSession(sessionId) {
107
+ const index = await loadCrewIndex();
108
+ const entry = index.find(e => e.sessionId === sessionId);
109
+ if (entry && entry.hidden) {
110
+ delete entry.hidden;
111
+ delete entry.hiddenAt;
112
+ await saveCrewIndex(index);
113
+ console.log(`[Crew] Unhidden session ${sessionId} in index`);
114
+ }
115
+ }
116
+
79
117
  // =====================================================================
80
118
  // Session Metadata (.crew/session.json)
81
119
  // =====================================================================
package/crew/routing.js CHANGED
@@ -27,12 +27,18 @@ export function parseRoutes(text) {
27
27
  const toMatch = block.match(/to:\s*(.+)/i);
28
28
  if (!toMatch) continue;
29
29
 
30
- const summaryMatch = block.match(/summary:\s*([\s\S]+)/i);
30
+ // Clean `to` value: take only the first word (strip parenthetical notes, extra text)
31
+ // e.g. "pm (决策者)" → "pm", "dev-1 // main dev" → "dev-1"
32
+ const toRaw = toMatch[1].trim().toLowerCase();
33
+ const toClean = toRaw.split(/[\s(]/)[0];
34
+
35
+ // ★ summary: match until next known field (task:/taskTitle:) or end of block
36
+ const summaryMatch = block.match(/summary:\s*([\s\S]+?)(?=\n\s*(?:task|taskTitle)\s*:|$)/i);
31
37
  const taskMatch = block.match(/^task:\s*(.+)/im);
32
38
  const taskTitleMatch = block.match(/^taskTitle:\s*(.+)/im);
33
39
 
34
40
  routes.push({
35
- to: toMatch[1].trim().toLowerCase(),
41
+ to: toClean,
36
42
  summary: summaryMatch ? summaryMatch[1].trim() : '',
37
43
  taskId: taskMatch ? taskMatch[1].trim() : null,
38
44
  taskTitle: taskTitleMatch ? taskTitleMatch[1].trim() : null
@@ -42,6 +48,59 @@ export function parseRoutes(text) {
42
48
  return routes;
43
49
  }
44
50
 
51
+ /**
52
+ * Resolve a ROUTE `to` value to an actual role name in the session.
53
+ *
54
+ * Resolution order:
55
+ * 1. Exact match: `to` matches a role name directly (e.g. "dev-1")
56
+ * 2. roleType match: `to` matches a role's roleType (e.g. "developer" → "dev-1")
57
+ * 3. Short prefix match: `to` matches the SHORT_PREFIX of a roleType (e.g. "dev" → "dev-1")
58
+ * 4. Same-group dispatch: if sender is in a multi-instance group (e.g. dev-1),
59
+ * and `to` matches the roleType/prefix of another group (e.g. "reviewer"),
60
+ * route to the instance with matching groupIndex (e.g. rev-1)
61
+ *
62
+ * For multi-instance matches (2/3), prefer the instance with the same groupIndex
63
+ * as the sender. Falls back to the first instance if no groupIndex match.
64
+ *
65
+ * @param {string} to - raw route target from ROUTE block
66
+ * @param {object} session - crew session
67
+ * @param {string} [fromRole] - sending role name (for groupIndex matching)
68
+ * @returns {string|null} resolved role name, or null if unresolvable
69
+ */
70
+ export function resolveRoleName(to, session, fromRole) {
71
+ // 1. Exact match
72
+ if (session.roles.has(to)) return to;
73
+
74
+ // Build candidate list by roleType and short prefix
75
+ const fromRoleConfig = fromRole ? session.roles.get(fromRole) : null;
76
+ const fromGroupIndex = fromRoleConfig?.groupIndex || 0;
77
+
78
+ let candidates = [];
79
+
80
+ for (const [name, config] of session.roles) {
81
+ // 2. roleType match (e.g. "developer" → dev-1, dev-2, dev-3)
82
+ if (config.roleType === to) {
83
+ candidates.push({ name, groupIndex: config.groupIndex || 0 });
84
+ }
85
+ // 3. Short prefix match (e.g. "dev" → developer roleType → dev-1)
86
+ // Match if the role name starts with `to-` (e.g. "dev" matches "dev-1", "dev-2")
87
+ else if (name.startsWith(to + '-') && /^\d+$/.test(name.slice(to.length + 1))) {
88
+ candidates.push({ name, groupIndex: config.groupIndex || 0 });
89
+ }
90
+ }
91
+
92
+ if (candidates.length === 0) return null;
93
+
94
+ // 4. Prefer same groupIndex as sender
95
+ if (fromGroupIndex > 0) {
96
+ const sameGroup = candidates.find(c => c.groupIndex === fromGroupIndex);
97
+ if (sameGroup) return sameGroup.name;
98
+ }
99
+
100
+ // Fall back to first candidate
101
+ return candidates[0].name;
102
+ }
103
+
45
104
  /**
46
105
  * 执行路由
47
106
  */
@@ -68,7 +127,9 @@ export async function executeRoute(session, fromRole, route) {
68
127
  // 更新工作看板:推断状态
69
128
  const { getMessages } = await import('../crew-i18n.js');
70
129
  const m = getMessages(session.language || 'zh-CN');
71
- const toRoleConfig = session.roles.get(to);
130
+ // Use resolveRoleName for kanban status lookup too
131
+ const resolvedKanbanTo = resolveRoleName(to, session, fromRole);
132
+ const toRoleConfig = session.roles.get(resolvedKanbanTo || to);
72
133
  let status = m.kanbanStatusDev;
73
134
  if (toRoleConfig) {
74
135
  switch (toRoleConfig.roleType) {
@@ -111,13 +172,14 @@ export async function executeRoute(session, fromRole, route) {
111
172
  }
112
173
 
113
174
  // 路由到指定角色
114
- if (session.roles.has(to)) {
175
+ const resolvedTo = resolveRoleName(to, session, fromRole);
176
+ if (resolvedTo) {
115
177
  if (session.humanMessageQueue.length > 0) {
116
178
  const { processHumanQueue } = await import('./human-interaction.js');
117
179
  await processHumanQueue(session);
118
180
  } else {
119
181
  const taskPrompt = buildRoutePrompt(fromRole, summary, session);
120
- await dispatchToRole(session, to, taskPrompt, fromRole, taskId, taskTitle);
182
+ await dispatchToRole(session, resolvedTo, taskPrompt, fromRole, taskId, taskTitle);
121
183
  }
122
184
  } else {
123
185
  console.warn(`[Crew] Unknown route target: ${to}`);
package/crew/session.js CHANGED
@@ -8,7 +8,7 @@ import { getMessages } from '../crew-i18n.js';
8
8
  import { initWorktrees } from './worktree.js';
9
9
  import { initSharedDir, writeRoleClaudeMd, updateSharedClaudeMd, backupMemoryContent } from './shared-dir.js';
10
10
  import {
11
- loadCrewIndex, upsertCrewIndex, removeFromCrewIndex,
11
+ loadCrewIndex, upsertCrewIndex, removeFromCrewIndex, unhideCrewSession,
12
12
  loadSessionMeta, saveSessionMeta, loadSessionMessages, getMaxShardIndex
13
13
  } from './persistence.js';
14
14
  import { sendCrewMessage, sendCrewOutput, sendStatusUpdate } from './ui-messages.js';
@@ -121,6 +121,7 @@ async function findExistingSessionByProjectDir(projectDir) {
121
121
  e.projectDir.replace(/\/+$/, '') === normalizedDir
122
122
  && (!agentId || !e.agentId || e.agentId === agentId)
123
123
  && e.status !== 'completed'
124
+ && !e.hidden
124
125
  );
125
126
 
126
127
  if (match) {
@@ -428,6 +429,9 @@ export async function handleDeleteCrewDir(msg) {
428
429
  export async function resumeCrewSession(msg) {
429
430
  const { sessionId, userId, username } = msg;
430
431
 
432
+ // 用户主动恢复 → 清除 hidden 标记(如果有的话)
433
+ await unhideCrewSession(sessionId);
434
+
431
435
  if (crewSessions.has(sessionId)) {
432
436
  const session = crewSessions.get(sessionId);
433
437
  const roles = Array.from(session.roles.values());
@@ -465,17 +469,22 @@ export async function resumeCrewSession(msg) {
465
469
  const index = await loadCrewIndex();
466
470
  const indexEntry = index.find(e => e.sessionId === sessionId);
467
471
  if (!indexEntry) {
468
- sendCrewMessage({ type: 'error', sessionId, message: 'Crew session not found in index' });
472
+ console.warn(`[Crew] resumeCrewSession: session ${sessionId} not found in index`);
473
+ sendCrewMessage({ type: 'crew_session_restore_failed', sessionId, message: 'Crew session not found in index' });
469
474
  return;
470
475
  }
471
476
 
472
477
  const meta = await loadSessionMeta(indexEntry.sharedDir);
473
478
  if (!meta) {
474
- sendCrewMessage({ type: 'error', sessionId, message: 'Crew session metadata not found' });
479
+ console.warn(`[Crew] resumeCrewSession: session.json not found at ${indexEntry.sharedDir}`);
480
+ sendCrewMessage({ type: 'crew_session_restore_failed', sessionId, message: 'Crew session metadata not found' });
475
481
  return;
476
482
  }
477
483
 
478
484
  const roles = meta.roles || [];
485
+ if (roles.length === 0) {
486
+ console.warn(`[Crew] resumeCrewSession: session ${sessionId} has empty roles in session.json`);
487
+ }
479
488
  const decisionMaker = meta.decisionMaker || roles[0]?.name || null;
480
489
  const session = {
481
490
  id: sessionId,
package/crew.js CHANGED
@@ -29,6 +29,7 @@ export {
29
29
  export {
30
30
  loadCrewIndex,
31
31
  removeFromCrewIndex,
32
+ hideCrewSession,
32
33
  handleLoadCrewHistory
33
34
  } from './crew/persistence.js';
34
35
 
package/index.js CHANGED
@@ -78,7 +78,7 @@ const CONFIG = {
78
78
  // 最大上下文 tokens(用于百分比计算的分母)
79
79
  maxContextTokens: parseInt(process.env.MAX_CONTEXT_TOKENS || fileConfig.maxContextTokens, 10) || 128000,
80
80
  // Auto-compact 阈值(tokens):context 超过此值时自动触发 compact
81
- autoCompactThreshold: parseInt(process.env.AUTO_COMPACT_THRESHOLD || fileConfig.autoCompactThreshold, 10) || 100000
81
+ autoCompactThreshold: parseInt(process.env.AUTO_COMPACT_THRESHOLD || fileConfig.autoCompactThreshold, 10) || 110000
82
82
  };
83
83
 
84
84
  // 初始化共享上下文
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.107",
3
+ "version": "0.1.114",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",