dsh-mutipleagent 0.0.0-stage → 0.2.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 (138) hide show
  1. package/CHANGELOG.md +149 -0
  2. package/CONTRIBUTING.md +49 -0
  3. package/LICENSE +21 -0
  4. package/README.en.md +95 -0
  5. package/README.md +273 -2
  6. package/SECURITY.md +27 -0
  7. package/THIRD_PARTY_NOTICES.md +29 -0
  8. package/assets/agents/MAGPIE-LICENSE +21 -0
  9. package/assets/agents/README.md +47 -0
  10. package/assets/agents/antigravity.png +0 -0
  11. package/assets/agents/claude.png +0 -0
  12. package/assets/agents/codex.png +0 -0
  13. package/assets/agents/copilot.svg +1 -0
  14. package/assets/agents/dsh.svg +1 -0
  15. package/assets/agents/gemini.png +0 -0
  16. package/assets/agents/grok.svg +1 -0
  17. package/assets/agents/hermes.svg +1 -0
  18. package/assets/agents/kimi.svg +1 -0
  19. package/assets/agents/omp.svg +7 -0
  20. package/assets/agents/opencode.svg +1 -0
  21. package/assets/agents/pi.svg +1 -0
  22. package/assets/kenney/LICENSE-furniture-kit.txt +23 -0
  23. package/assets/kenney/LICENSE-mini-characters.txt +28 -0
  24. package/assets/kenney/characters/Textures/colormap.png +0 -0
  25. package/assets/kenney/characters/character-female-a.glb +0 -0
  26. package/assets/kenney/characters/character-female-b.glb +0 -0
  27. package/assets/kenney/characters/character-female-c.glb +0 -0
  28. package/assets/kenney/characters/character-female-d.glb +0 -0
  29. package/assets/kenney/characters/character-female-e.glb +0 -0
  30. package/assets/kenney/characters/character-female-f.glb +0 -0
  31. package/assets/kenney/characters/character-male-a.glb +0 -0
  32. package/assets/kenney/characters/character-male-b.glb +0 -0
  33. package/assets/kenney/characters/character-male-c.glb +0 -0
  34. package/assets/kenney/characters/character-male-d.glb +0 -0
  35. package/assets/kenney/characters/character-male-e.glb +0 -0
  36. package/assets/kenney/characters/character-male-f.glb +0 -0
  37. package/assets/kenney/furniture/benchCushion.glb +0 -0
  38. package/assets/kenney/furniture/bookcaseOpen.glb +0 -0
  39. package/assets/kenney/furniture/books.glb +0 -0
  40. package/assets/kenney/furniture/cabinetTelevision.glb +0 -0
  41. package/assets/kenney/furniture/cardboardBoxClosed.glb +0 -0
  42. package/assets/kenney/furniture/chairCushion.glb +0 -0
  43. package/assets/kenney/furniture/chairDesk.glb +0 -0
  44. package/assets/kenney/furniture/chairModernCushion.glb +0 -0
  45. package/assets/kenney/furniture/coatRackStanding.glb +0 -0
  46. package/assets/kenney/furniture/computerKeyboard.glb +0 -0
  47. package/assets/kenney/furniture/computerMouse.glb +0 -0
  48. package/assets/kenney/furniture/computerScreen.glb +0 -0
  49. package/assets/kenney/furniture/desk.glb +0 -0
  50. package/assets/kenney/furniture/doorway.glb +0 -0
  51. package/assets/kenney/furniture/kitchenBar.glb +0 -0
  52. package/assets/kenney/furniture/kitchenCabinet.glb +0 -0
  53. package/assets/kenney/furniture/kitchenCoffeeMachine.glb +0 -0
  54. package/assets/kenney/furniture/kitchenFridgeLarge.glb +0 -0
  55. package/assets/kenney/furniture/kitchenMicrowave.glb +0 -0
  56. package/assets/kenney/furniture/kitchenSink.glb +0 -0
  57. package/assets/kenney/furniture/lampRoundFloor.glb +0 -0
  58. package/assets/kenney/furniture/lampSquareTable.glb +0 -0
  59. package/assets/kenney/furniture/laptop.glb +0 -0
  60. package/assets/kenney/furniture/loungeChairRelax.glb +0 -0
  61. package/assets/kenney/furniture/loungeSofa.glb +0 -0
  62. package/assets/kenney/furniture/plantSmall1.glb +0 -0
  63. package/assets/kenney/furniture/plantSmall2.glb +0 -0
  64. package/assets/kenney/furniture/plantSmall3.glb +0 -0
  65. package/assets/kenney/furniture/pottedPlant.glb +0 -0
  66. package/assets/kenney/furniture/radio.glb +0 -0
  67. package/assets/kenney/furniture/rugRectangle.glb +0 -0
  68. package/assets/kenney/furniture/rugRound.glb +0 -0
  69. package/assets/kenney/furniture/sideTable.glb +0 -0
  70. package/assets/kenney/furniture/speaker.glb +0 -0
  71. package/assets/kenney/furniture/stoolBar.glb +0 -0
  72. package/assets/kenney/furniture/table.glb +0 -0
  73. package/assets/kenney/furniture/tableCoffee.glb +0 -0
  74. package/assets/kenney/furniture/tableRound.glb +0 -0
  75. package/assets/kenney/furniture/televisionModern.glb +0 -0
  76. package/assets/kenney/furniture/toaster.glb +0 -0
  77. package/assets/kenney/furniture/trashcan.glb +0 -0
  78. package/assets/kenney/furniture/wallWindow.glb +0 -0
  79. package/assets/logo.svg +12 -0
  80. package/assets/scene-icons/arcade.png +0 -0
  81. package/assets/scene-icons/cafe.png +0 -0
  82. package/assets/scene-icons/castle.png +0 -0
  83. package/assets/scene-icons/christmas.png +0 -0
  84. package/assets/scene-icons/graveyard.png +0 -0
  85. package/assets/scene-icons/map.png +0 -0
  86. package/assets/scene-icons/office.png +0 -0
  87. package/assets/scene-icons/pirate.png +0 -0
  88. package/assets/scene-icons/station.png +0 -0
  89. package/assets/scenes/arcade.jpg +0 -0
  90. package/assets/scenes/cafe.jpg +0 -0
  91. package/assets/scenes/castle.jpg +0 -0
  92. package/assets/scenes/christmas.jpg +0 -0
  93. package/assets/scenes/graveyard.jpg +0 -0
  94. package/assets/scenes/office.jpg +0 -0
  95. package/assets/scenes/pirate.jpg +0 -0
  96. package/assets/scenes/station.jpg +0 -0
  97. package/client.js +90 -0
  98. package/cordis.patch.yml +3 -0
  99. package/harness.cjs +49 -0
  100. package/package.json +87 -4
  101. package/public/app.js +1 -0
  102. package/public/chunks/chunk-RQUUGNU7.js +4305 -0
  103. package/public/demo-icons/arrow-counterclockwise.svg +4 -0
  104. package/public/demo-icons/arrow-right.svg +3 -0
  105. package/public/demo-icons/bell.svg +3 -0
  106. package/public/demo-icons/box-seam-fill.svg +3 -0
  107. package/public/demo-icons/chat-left-text-fill.svg +3 -0
  108. package/public/demo-icons/check-lg.svg +3 -0
  109. package/public/demo-icons/chevron-down.svg +3 -0
  110. package/public/demo-icons/cpu-fill.svg +4 -0
  111. package/public/demo-icons/crosshair.svg +3 -0
  112. package/public/demo-icons/diagram-3-fill.svg +3 -0
  113. package/public/demo-icons/file-earmark-code-fill.svg +3 -0
  114. package/public/demo-icons/fullscreen.svg +3 -0
  115. package/public/demo-icons/pause-fill.svg +3 -0
  116. package/public/demo-icons/people-fill.svg +3 -0
  117. package/public/demo-icons/pie-chart-fill.svg +3 -0
  118. package/public/demo-icons/play-fill.svg +3 -0
  119. package/public/demo-icons/search.svg +3 -0
  120. package/public/demo-icons/shield-check.svg +4 -0
  121. package/public/demo-icons/stack.svg +4 -0
  122. package/public/demo-icons/x-lg.svg +3 -0
  123. package/public/demo.css +173 -0
  124. package/public/demo.html +64 -0
  125. package/public/demo.js +7 -0
  126. package/public/index.html +59 -0
  127. package/public/style.css +15 -0
  128. package/public/vendor/office.js +1631 -0
  129. package/src/codex.js +674 -0
  130. package/src/demo.js +103 -0
  131. package/src/harness-watcher.js +584 -0
  132. package/src/server.js +239 -0
  133. package/vendor/licenses/BOOTSTRAP-ICONS-LICENSE.txt +21 -0
  134. package/vendor/licenses/ESBUILD-LICENSE.txt +21 -0
  135. package/vendor/licenses/MESHOPTIMIZER-LICENSE.txt +22 -0
  136. package/vendor/licenses/PLAYWRIGHT-LICENSE.txt +202 -0
  137. package/vendor/licenses/PLAYWRIGHT-NOTICE.txt +5 -0
  138. package/vendor/licenses/THREE-LICENSE.txt +21 -0
package/src/codex.js ADDED
@@ -0,0 +1,674 @@
1
+ 'use strict';
2
+
3
+ // 基于上游 watcher 的增量读取方式,仅保留 Codex 数据源。
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+ const os = require('node:os');
7
+ const crypto = require('node:crypto');
8
+ const { EventEmitter } = require('node:events');
9
+
10
+ const unreadableTask = '任务正文未记录为可见明文';
11
+ const isOpaque = text => typeof text === 'string' && /^gAAAAA[A-Za-z0-9_-]{90,}={0,2}$/.test(text.trim());
12
+ const visibleText = value => (typeof value === 'string' && !isOpaque(value) ? value : '');
13
+ const short = (v, n = 180) =>
14
+ visibleText(String(v || ''))
15
+ .replace(/\s+/g, ' ')
16
+ .trim()
17
+ .slice(0, n);
18
+ const textOf = content =>
19
+ typeof content === 'string'
20
+ ? visibleText(content)
21
+ : Array.isArray(content)
22
+ ? content
23
+ .map(x => visibleText(x.text))
24
+ .filter(Boolean)
25
+ .join('\n')
26
+ : '';
27
+ const contextOnly = text => {
28
+ let rest = text.trim();
29
+ // 只排除完整的上下文包装;普通请求中的示例、代码块仍属于用户任务。
30
+ const block =
31
+ /^(?:# AGENTS\.md instructions[ \t]*\r?\n\s*<INSTRUCTIONS>[\s\S]*?<\/INSTRUCTIONS>|<(environment_context|external_codex_apps_[\w.-]+)>[\s\S]*?<\/\1>)\s*/;
32
+ while (rest) {
33
+ const match = rest.match(block);
34
+ if (!match) return false;
35
+ rest = rest.slice(match[0].length);
36
+ }
37
+ return true;
38
+ };
39
+ const digest = v => crypto.createHash('sha256').update(v).digest('hex').slice(0, 20);
40
+ const parseArgs = v => {
41
+ if (v && typeof v === 'object') return v;
42
+ try {
43
+ const parsed = JSON.parse(v || '{}');
44
+ return parsed && typeof parsed === 'object' ? parsed : {};
45
+ } catch {
46
+ return {};
47
+ }
48
+ };
49
+ const terminal = new Set(['done', 'error', 'interrupted', 'closed']);
50
+ const toolName = v =>
51
+ String(v || '')
52
+ .split('.')
53
+ .pop();
54
+
55
+ function newAgent(id) {
56
+ return {
57
+ id,
58
+ key: id,
59
+ agent: 'codex',
60
+ parentId: null,
61
+ rootId: id,
62
+ name: null,
63
+ role: null,
64
+ cwd: '',
65
+ model: '',
66
+ status: 'unknown',
67
+ activity: '',
68
+ task: '',
69
+ taskEventId: null,
70
+ taskSessionId: null,
71
+ reportEventId: null,
72
+ startedAt: null,
73
+ turnStartedAt: null,
74
+ lastEventAt: null,
75
+ endedAt: null,
76
+ relationship: 'unconfirmed',
77
+ reports: [],
78
+ };
79
+ }
80
+
81
+ class CodexWatcher extends EventEmitter {
82
+ constructor({
83
+ codexHome = process.env.CODEX_HOME || path.join(os.homedir(), '.codex'),
84
+ interval = 1000,
85
+ scanInterval = 5000,
86
+ hours = 24,
87
+ rootsLimit = 60,
88
+ } = {}) {
89
+ super();
90
+ this.home = codexHome;
91
+ this.dir = path.join(codexHome, 'sessions');
92
+ this.interval = interval;
93
+ this.scanInterval = scanInterval;
94
+ this.window = hours * 3600000;
95
+ this.rootsLimit = rootsLimit;
96
+ this.files = new Map();
97
+ this.agents = new Map();
98
+ this.tracks = new Map();
99
+ this.events = new Map();
100
+ this.eventKeys = new Map();
101
+ this.pins = new Set();
102
+ this.pendingCalls = new Map();
103
+ this.pathAssignments = new Map();
104
+ this.pendingTargets = new Map();
105
+ this.lastScan = 0;
106
+ this.revision = 0;
107
+ this.warnings = new Map();
108
+ }
109
+
110
+ agent(id) {
111
+ if (!this.agents.has(id)) this.agents.set(id, newAgent(id));
112
+ return this.agents.get(id);
113
+ }
114
+
115
+ start() {
116
+ this.tick();
117
+ this.timer = setInterval(() => this.tick(), this.interval);
118
+ return this;
119
+ }
120
+
121
+ stop() {
122
+ clearInterval(this.timer);
123
+ }
124
+
125
+ readHeader(file) {
126
+ const fd = fs.openSync(file, 'r');
127
+ try {
128
+ const blocks = [];
129
+ let at = 0;
130
+ for (;;) {
131
+ const buf = Buffer.alloc(65536);
132
+ const n = fs.readSync(fd, buf, 0, buf.length, at);
133
+ if (!n) return null;
134
+ at += n;
135
+ const end = buf.subarray(0, n).indexOf(10);
136
+ blocks.push(buf.subarray(0, end < 0 ? n : end));
137
+ if (end >= 0) return JSON.parse(Buffer.concat(blocks).toString('utf8'));
138
+ }
139
+ } finally {
140
+ fs.closeSync(fd);
141
+ }
142
+ }
143
+
144
+ scan(now) {
145
+ const found = new Map();
146
+ const walk = dir => {
147
+ let entries;
148
+ try {
149
+ entries = fs.readdirSync(dir, { withFileTypes: true });
150
+ } catch (e) {
151
+ if (e.code !== 'ENOENT') this.warnings.set('scan', '部分会话目录暂时无法读取');
152
+ return;
153
+ }
154
+ for (const entry of entries) {
155
+ const file = path.join(dir, entry.name);
156
+ if (entry.isDirectory()) walk(file);
157
+ else if (entry.isFile() && entry.name.startsWith('rollout-') && entry.name.endsWith('.jsonl')) {
158
+ const match = entry.name.match(/([0-9a-f]{8}-[0-9a-f-]{27,})\.jsonl$/i);
159
+ if (!match) continue;
160
+ try {
161
+ const st = fs.statSync(file);
162
+ found.set(match[1], { file, size: st.size, mtime: st.mtimeMs });
163
+ } catch {
164
+ this.warnings.set('scan', '部分会话日志暂时无法读取');
165
+ }
166
+ }
167
+ }
168
+ };
169
+ walk(this.dir);
170
+ this.files = found;
171
+ const recent = [...found]
172
+ .filter(([, f]) => f.mtime >= now - this.window)
173
+ .sort((a, b) => b[1].mtime - a[1].mtime)
174
+ .slice(0, this.rootsLimit * 4);
175
+ for (const [id, f] of recent) this.ensureMeta(id, f);
176
+ // 沿已知父 ID 补读元数据,不依赖父会话最近是否有写入。
177
+ for (const s of [...this.agents.values()]) {
178
+ if (s.parentId && found.has(s.parentId)) this.ensureMeta(s.parentId, found.get(s.parentId));
179
+ }
180
+ this.lastScan = now;
181
+ }
182
+
183
+ ensureMeta(id, f = this.files.get(id)) {
184
+ if (!f) return;
185
+ const s = this.agent(id);
186
+ if (s._metaFile === f.file && s._hasMeta) return;
187
+ try {
188
+ const d = this.readHeader(f.file);
189
+ if (d?.type === 'session_meta') {
190
+ this.metadata(s, d.payload || {});
191
+ s._metaFile = f.file;
192
+ s._hasMeta = true;
193
+ }
194
+ } catch {
195
+ this.warnings.set('header:' + id, '有会话元数据暂时不可用,将继续重试');
196
+ }
197
+ }
198
+
199
+ metadata(s, p) {
200
+ if (p.id && p.id !== s.id) return;
201
+ s.cwd = p.cwd || s.cwd;
202
+ s.startedAt ||= Date.parse(p.timestamp) || null;
203
+ s.name = p.agent_nickname || s.name;
204
+ s.role = p.agent_role || p.agent_type || s.role;
205
+ const spawn = p.source?.subagent?.thread_spawn;
206
+ s._historyStart = Number.isInteger(p.subagent_history_start_ordinal) ? p.subagent_history_start_ordinal : s._historyStart || 0;
207
+ s._internal =
208
+ !!p.source?.internal ||
209
+ p.thread_source === 'guardian_review' ||
210
+ ['guardian', 'review', 'compact', 'memory_consolidation'].includes(p.source?.subagent?.other) ||
211
+ ['review', 'compact', 'memory_consolidation'].includes(p.source?.subagent);
212
+ s.agentPath = p.agent_path || spawn?.agent_path || s.agentPath || null;
213
+ s.name ||= s.agentPath?.split('/').filter(Boolean).pop() || null;
214
+ const parent = p.parent_thread_id || spawn?.parent_thread_id;
215
+ if (parent) this.link(s.id, parent, 'metadata');
216
+ else if (p.thread_source === 'user' || typeof p.source === 'string') {
217
+ if (!s.parentId) s.relationship = 'root';
218
+ }
219
+ if (spawn) {
220
+ s.name = spawn.agent_nickname || s.name;
221
+ s.role = spawn.agent_role || spawn.agent_type || s.role;
222
+ }
223
+ s._isSubagent = !!parent || !!p.source?.subagent || p.thread_source === 'subagent';
224
+ if (s.agentPath && s.parentId) {
225
+ const assignment = this.pathAssignments.get(s.parentId + ':' + s.agentPath);
226
+ if (assignment) this.assign(this.agent(s.parentId), s.id, assignment.prompt, assignment.ref, assignment.callId);
227
+ }
228
+ this.resolvePendingTargets();
229
+ }
230
+
231
+ resolveTarget(sender, target) {
232
+ if (typeof target !== 'string' || !target) return null;
233
+ if (this.agents.has(target)) return target;
234
+ const base = sender.agentPath || (!sender.parentId ? '/root' : null);
235
+ const agentPath = target.startsWith('/') ? target : base && base + '/' + target;
236
+ if (!agentPath) return null;
237
+ return [...this.agents.values()].find(s => !s._internal && s.agentPath === agentPath && this.rootOf(s.id) === this.rootOf(sender.id))?.id || null;
238
+ }
239
+
240
+ resolvePendingTargets() {
241
+ for (const [id, pending] of this.pendingTargets) {
242
+ const event = this.events.get(id);
243
+ const sender = this.agents.get(pending.sender);
244
+ if (!event || !sender) {
245
+ this.pendingTargets.delete(id);
246
+ continue;
247
+ }
248
+ const targetId = this.resolveTarget(sender, pending.target);
249
+ if (targetId) {
250
+ event.targetId = targetId;
251
+ this.pendingTargets.delete(id);
252
+ this.revision++;
253
+ }
254
+ }
255
+ }
256
+
257
+ link(childId, parentId, evidence) {
258
+ if (!childId || !parentId || childId === parentId) return;
259
+ const child = this.agent(childId);
260
+ if (child.parentId && child.parentId !== parentId) {
261
+ child.relationship = 'conflict';
262
+ return;
263
+ }
264
+ // 防止异常元数据形成环,进而污染整棵团队树。
265
+ let cursor = parentId;
266
+ const seen = new Set([childId]);
267
+ while (cursor) {
268
+ if (seen.has(cursor)) {
269
+ child.relationship = 'conflict';
270
+ return;
271
+ }
272
+ seen.add(cursor);
273
+ cursor = this.agents.get(cursor)?.parentId;
274
+ }
275
+ this.agent(parentId);
276
+ child.parentId = parentId;
277
+ child.relationship = evidence;
278
+ child._isSubagent = true;
279
+ }
280
+
281
+ rootOf(id) {
282
+ let s = this.agents.get(id);
283
+ const seen = new Set();
284
+ while (s?.parentId && !seen.has(s.id)) {
285
+ seen.add(s.id);
286
+ s = this.agents.get(s.parentId);
287
+ }
288
+ return s?.id || id;
289
+ }
290
+
291
+ add(s, type, text, ref, { targetId = null, key = null } = {}) {
292
+ const content = visibleText(text);
293
+ const dedupe = key ? s.id + ':' + type + ':' + key : null;
294
+ if (dedupe && this.eventKeys.has(dedupe)) return this.events.get(this.eventKeys.get(dedupe));
295
+ const id = digest(s.id + ':' + ref.offset + ':' + (ref.hash || '') + ':' + type + ':' + (targetId || ''));
296
+ const e = { id, sessionId: s.id, targetId, type, at: ref.at, preview: short(content), hasText: !!content, _ref: { ...ref } };
297
+ this.events.set(id, e);
298
+ if (dedupe) this.eventKeys.set(dedupe, id);
299
+ s.lastEventAt = Math.max(s.lastEventAt || 0, ref.at || 0);
300
+ this.revision++;
301
+ return e;
302
+ }
303
+
304
+ status(s, state, at) {
305
+ s.status = state;
306
+ s.lastEventAt = Math.max(s.lastEventAt || 0, at || 0);
307
+ s.endedAt = terminal.has(state) ? at : null;
308
+ if (state !== 'tool' && state !== 'waiting') s.activity = '';
309
+ }
310
+
311
+ report(s, text, ref) {
312
+ text = visibleText(text);
313
+ if (!text) return null;
314
+ const hash = digest(text);
315
+ const previous = [...this.events.values()]
316
+ .reverse()
317
+ .find(e => e.type === 'report' && e.sessionId === s.id && e._textHash === hash && (e._ref.ownerSessionId || ref.ownerSessionId));
318
+ if (previous) {
319
+ if (!ref.ownerSessionId) previous._ref = { ...ref };
320
+ s.reportEventId = previous.id;
321
+ if (!s.reports.includes(previous.id)) s.reports.push(previous.id);
322
+ this.eventKeys.set(s.id + ':report:' + String(s._turn || '') + ':' + hash, previous.id);
323
+ return previous;
324
+ }
325
+ const e = this.add(s, 'report', text, ref, { key: String(s._turn || '') + ':' + digest(text) });
326
+ s.reportEventId = e.id;
327
+ e._textHash = hash;
328
+ if (!s.reports.includes(e.id)) s.reports.push(e.id);
329
+ return e;
330
+ }
331
+
332
+ process(s, d, ref) {
333
+ if (!d || typeof d !== 'object' || !d.payload || typeof d.payload !== 'object') return;
334
+ const p = d.payload || {};
335
+ if (d.type === 'session_meta') {
336
+ this.metadata(s, p);
337
+ return;
338
+ }
339
+ if (d.type === 'turn_context') {
340
+ s.model = p.model || s.model;
341
+ s.cwd = p.cwd || s.cwd;
342
+ return;
343
+ }
344
+ if (d.type === 'event_msg') {
345
+ const t = p.type;
346
+ if (typeof t !== 'string') {
347
+ this.warnings.set('structure:' + s.id, '日志中存在缺少类型的事件,已跳过该条记录');
348
+ return;
349
+ }
350
+ if (t === 'task_started' || t === 'turn_started') {
351
+ s._turn = p.turn_id || ref.offset;
352
+ s.turnStartedAt = ref.at;
353
+ s.reportEventId = null;
354
+ this.status(s, 'thinking', ref.at);
355
+ } else if (t === 'user_message') {
356
+ if (!visibleText(p.message) || contextOnly(p.message)) return;
357
+ const e = this.add(s, 'task', p.message, ref);
358
+ s.task = short(p.message);
359
+ s.taskEventId = e.id;
360
+ s.taskSessionId = s.id;
361
+ this.status(s, 'thinking', ref.at);
362
+ } else if (t === 'agent_message') {
363
+ if (p.phase === 'final_answer' || p.channel === 'final') this.report(s, p.message, ref);
364
+ else this.add(s, 'progress', p.message, ref, { key: String(s._turn || '') + ':' + digest(p.message || '') });
365
+ this.status(s, 'typing', ref.at);
366
+ } else if (t === 'task_complete' || t === 'turn_completed') {
367
+ this.report(s, p.last_agent_message, ref);
368
+ this.status(s, 'done', ref.at);
369
+ this.add(s, 'complete', '', ref, { key: String(s._turn || ref.offset) });
370
+ } else if (t === 'turn_aborted') {
371
+ this.status(s, 'interrupted', ref.at);
372
+ this.add(s, 'interrupted', p.reason || '任务已中断', ref);
373
+ } else if (t === 'error') {
374
+ this.status(s, 'error', ref.at);
375
+ this.add(s, 'error', p.message || 'Codex 记录了错误', ref);
376
+ } else if (['exec_approval_request', 'apply_patch_approval_request', 'request_user_input'].includes(t)) {
377
+ this.status(s, 'waiting', ref.at);
378
+ s.activity = '等待用户处理';
379
+ this.add(s, 'approval', p.message || p.reason || '等待用户处理,具体内容请查看 Codex', ref);
380
+ } else if (t.startsWith('collab_')) this.collaboration(s, p, ref);
381
+ else if (t === 'item_started' || t === 'item_completed') {
382
+ const item = p.item || {};
383
+ if (item.type === 'AgentMessage') {
384
+ const text = textOf(item.content);
385
+ if (item.phase === 'final_answer' || item.channel === 'final') this.report(s, text, ref);
386
+ else this.add(s, 'progress', text, ref, { key: String(s._turn || '') + ':' + digest(text) });
387
+ } else if (item.type === 'CommandExecution' || item.type === 'McpToolCall' || item.type === 'FileChange') {
388
+ const activity =
389
+ item.type === 'CommandExecution'
390
+ ? short(Array.isArray(item.command) ? item.command.join(' ') : item.command)
391
+ : item.type === 'FileChange'
392
+ ? '修改文件'
393
+ : String(item.tool || 'MCP 工具');
394
+ this.status(s, t === 'item_started' ? 'tool' : 'thinking', ref.at);
395
+ s.activity = activity;
396
+ if (t === 'item_started') this.add(s, 'tool', activity, ref, { key: item.id || null });
397
+ }
398
+ }
399
+ return;
400
+ }
401
+ if (d.type !== 'response_item') return;
402
+ if (p.type === 'message' && p.role === 'user') {
403
+ const text = textOf(p.content);
404
+ if (!visibleText(text) || contextOnly(text)) return;
405
+ const e = this.add(s, 'task', text, ref, { key: String(s._turn || '') + ':' + digest(text) });
406
+ s.task = short(text);
407
+ s.taskEventId = e.id;
408
+ s.taskSessionId = s.id;
409
+ } else if (p.type === 'message' && p.role === 'assistant') {
410
+ const text = textOf(p.content);
411
+ if (!text) return;
412
+ if (p.phase === 'final_answer' || p.channel === 'final') this.report(s, text, ref);
413
+ else this.add(s, 'progress', text, ref, { key: String(s._turn || '') + ':' + digest(text) });
414
+ if (!terminal.has(s.status)) this.status(s, 'typing', ref.at);
415
+ } else if (['function_call', 'custom_tool_call', 'local_shell_call', 'web_search_call'].includes(p.type)) {
416
+ const name = toolName(p.name || p.type);
417
+ const args = parseArgs(p.arguments || p.input || p.action);
418
+ const callId = p.call_id || p.id;
419
+ if (callId) this.pendingCalls.set(callId, { name, args, sender: s.id, ref });
420
+ const waiting = name.startsWith('request_user_input');
421
+ const waitingAgents = name === 'wait_agent';
422
+ this.status(s, waitingAgents ? 'waiting_agents' : waiting ? 'waiting' : 'tool', ref.at);
423
+ s.activity = name === 'interrupt_agent' ? short('请求中断 ' + (args.target || 'Agent'), 240) : short(args.cmd || args.command || p.name || p.type, 240);
424
+ this.add(s, waitingAgents ? 'wait' : waiting ? 'approval' : 'tool', waitingAgents ? '' : s.activity, ref, { key: callId });
425
+ if (['send_input', 'send_message', 'followup_task'].includes(name)) {
426
+ const target = args.id || args.target;
427
+ // 先记真实正文,再补关联;ID 不依赖子日志先后到达的顺序。
428
+ const event = this.add(s, 'message', args.message || '', ref, { key: callId });
429
+ event.targetId ||= this.resolveTarget(s, target);
430
+ if (!event.targetId && typeof target === 'string') this.pendingTargets.set(event.id, { sender: s.id, target });
431
+ }
432
+ } else if (['function_call_output', 'custom_tool_call_output'].includes(p.type)) {
433
+ const call = this.pendingCalls.get(p.call_id);
434
+ if (call?.name === 'spawn_agent') {
435
+ const out = parseArgs(p.output);
436
+ const childId = out.agent_id || out.thread_id;
437
+ if (childId) this.assign(s, childId, call.args.message || call.args.prompt || '', { ...call.ref, at: ref.at }, p.call_id, out);
438
+ else if (out.task_name) {
439
+ const assignment = { prompt: call.args.message || call.args.prompt || '', ref: { ...call.ref, at: ref.at }, callId: p.call_id };
440
+ this.pathAssignments.set(s.id + ':' + out.task_name, assignment);
441
+ const child = [...this.agents.values()].find(v => v.parentId === s.id && v.agentPath === out.task_name);
442
+ if (child) this.assign(s, child.id, assignment.prompt, assignment.ref, assignment.callId);
443
+ }
444
+ }
445
+ if (call) this.pendingCalls.delete(p.call_id);
446
+ if (!terminal.has(s.status)) this.status(s, 'thinking', ref.at);
447
+ }
448
+ }
449
+
450
+ assign(s, childId, prompt, ref, callId, p = {}) {
451
+ this.link(childId, s.id, 'spawn');
452
+ const child = this.agent(childId);
453
+ child.name = p.new_agent_nickname || p.agent_nickname || child.name;
454
+ child.role = p.new_agent_role || p.agent_role || child.role;
455
+ const e = this.add(s, 'assign', prompt, ref, { targetId: childId, key: callId || childId });
456
+ if (!child.taskEventId) {
457
+ child.task = isOpaque(prompt) ? unreadableTask : short(prompt);
458
+ child.taskEventId = e.id;
459
+ child.taskSessionId = s.id;
460
+ }
461
+ if (p.model) child.model = p.model;
462
+ }
463
+
464
+ collaboration(s, p, ref) {
465
+ const sender = this.agent(p.sender_thread_id || s.id);
466
+ if (p.type === 'collab_agent_spawn_begin') {
467
+ this.pendingCalls.set(p.call_id, { prompt: p.prompt });
468
+ } else if (p.type === 'collab_agent_spawn_end') {
469
+ if (p.new_thread_id) this.assign(sender, p.new_thread_id, p.prompt || this.pendingCalls.get(p.call_id)?.prompt || '', ref, p.call_id, p);
470
+ else this.add(sender, 'error', '子 Agent 创建失败', ref, { key: p.call_id });
471
+ this.pendingCalls.delete(p.call_id);
472
+ } else if (p.type === 'collab_agent_interaction_end') {
473
+ this.add(sender, 'message', p.prompt || '', ref, { targetId: p.receiver_thread_id, key: p.call_id });
474
+ } else if (p.type === 'collab_waiting_begin') {
475
+ this.status(sender, 'waiting_agents', ref.at);
476
+ this.add(sender, 'wait', '', ref, { key: p.call_id });
477
+ } else if (p.type === 'collab_waiting_end') {
478
+ this.status(sender, 'thinking', ref.at);
479
+ const statuses = p.statuses || Object.fromEntries((p.agent_statuses || []).map(v => [v.thread_id, v.status]));
480
+ for (const [id, state] of Object.entries(statuses)) {
481
+ const child = this.agent(id);
482
+ if (typeof state === 'object' && state?.completed != null) {
483
+ this.report(child, state.completed, { ...ref, ownerSessionId: s.id });
484
+ this.status(child, 'done', ref.at);
485
+ } else if (typeof state === 'object' && state?.errored != null) this.status(child, 'error', ref.at);
486
+ }
487
+ } else if (p.type === 'collab_close_end' && p.receiver_thread_id) {
488
+ const child = this.agent(p.receiver_thread_id);
489
+ if (!terminal.has(child.status)) this.status(child, 'closed', ref.at);
490
+ this.add(sender, 'close', '', ref, { targetId: child.id, key: p.call_id });
491
+ }
492
+ }
493
+
494
+ load(id) {
495
+ const f = this.files.get(id);
496
+ if (!f) return;
497
+ const st = fs.statSync(f.file);
498
+ let track = this.tracks.get(id);
499
+ if (track && (st.size < track.offset || track.file !== f.file || (st.size === track.offset && st.mtimeMs !== track.mtime))) {
500
+ for (const [key, e] of this.events) if (e.sessionId === id || e._ref.ownerSessionId === id) this.events.delete(key);
501
+ for (const [key, value] of this.eventKeys) if (!this.events.has(value)) this.eventKeys.delete(key);
502
+ this.agents.set(id, newAgent(id));
503
+ track = null;
504
+ this.revision++;
505
+ }
506
+ if (!track) {
507
+ track = { file: f.file, offset: 0, ordinal: 0, pending: Buffer.alloc(0), mtime: st.mtimeMs };
508
+ this.tracks.set(id, track);
509
+ }
510
+ const s = this.agent(id);
511
+ const fd = fs.openSync(f.file, 'r');
512
+ try {
513
+ // 按字节定位记录,UTF-8 半字和半行会留到下次写入后再解析。
514
+ let budget = 16 * 1024 * 1024;
515
+ while (track.offset < st.size && budget > 0) {
516
+ const buf = Buffer.alloc(Math.min(1024 * 1024, st.size - track.offset));
517
+ const n = fs.readSync(fd, buf, 0, buf.length, track.offset);
518
+ if (!n) break;
519
+ const start = track.offset - track.pending.length;
520
+ const joined = Buffer.concat([track.pending, buf.subarray(0, n)]);
521
+ track.offset += n;
522
+ budget -= n;
523
+ let lineStart = 0;
524
+ for (let i = 0; i < joined.length; i++)
525
+ if (joined[i] === 10) {
526
+ const line = joined.subarray(lineStart, i).toString('utf8').trim();
527
+ if (line) {
528
+ try {
529
+ const d = JSON.parse(line);
530
+ if (track.ordinal === 0 || track.ordinal >= (s._historyStart || 0)) {
531
+ this.process(s, d, {
532
+ file: f.file,
533
+ offset: start + lineStart,
534
+ length: i - lineStart,
535
+ hash: digest(line),
536
+ at: Date.parse(d.timestamp) || st.mtimeMs,
537
+ });
538
+ }
539
+ } catch (e) {
540
+ if (e instanceof SyntaxError) this.warnings.set('parse:' + id, '日志中存在无法解析的记录,其他活动继续同步');
541
+ else throw e;
542
+ }
543
+ }
544
+ track.ordinal++;
545
+ lineStart = i + 1;
546
+ }
547
+ track.pending = Buffer.from(joined.subarray(lineStart));
548
+ }
549
+ track.mtime = st.mtimeMs;
550
+ s.loading = track.offset < st.size;
551
+ s.lastEventAt ||= st.mtimeMs;
552
+ } finally {
553
+ fs.closeSync(fd);
554
+ }
555
+ }
556
+
557
+ roots() {
558
+ return [...this.agents.values()]
559
+ .filter(s => !s.parentId && !s._isSubagent && !s._internal && this.files.has(s.id))
560
+ .sort((a, b) => (this.files.get(b.id)?.mtime || 0) - (this.files.get(a.id)?.mtime || 0))
561
+ .slice(0, this.rootsLimit)
562
+ .map(s => ({
563
+ id: s.id,
564
+ title: s.task || path.basename(s.cwd) || 'Codex 任务',
565
+ cwd: s.cwd,
566
+ status: s.status,
567
+ at: this.files.get(s.id)?.mtime || s.lastEventAt,
568
+ }));
569
+ }
570
+
571
+ select(id) {
572
+ if (!this.agents.has(id)) return false;
573
+ this.pins.add(id);
574
+ this.tick();
575
+ return true;
576
+ }
577
+
578
+ tick() {
579
+ const before = this.revision;
580
+ try {
581
+ const now = Date.now();
582
+ if (!this.lastScan || now - this.lastScan >= this.scanInterval) this.scan(now);
583
+ const roots = this.pins.size
584
+ ? this.pins
585
+ : new Set(
586
+ this.roots()
587
+ .slice(0, 1)
588
+ .map(s => s.id),
589
+ );
590
+ // 创建事件可能先于子日志落盘;每一轮都重新扩展团队成员。
591
+ for (let round = 0; round < 12; round++) {
592
+ let added = false;
593
+ for (const s of [...this.agents.values()])
594
+ if (roots.has(this.rootOf(s.id))) {
595
+ if (this.files.has(s.id) && !s._internal) {
596
+ try {
597
+ this.ensureMeta(s.id);
598
+ const had = this.tracks.has(s.id);
599
+ this.load(s.id);
600
+ if (!had) added = true;
601
+ this.warnings.delete('read:' + s.id);
602
+ } catch (e) {
603
+ this.warnings.set('read:' + s.id, '有成员日志读取受阻(' + (e.code || e.name) + '),其他成员继续同步');
604
+ }
605
+ }
606
+ }
607
+ if (!added) break;
608
+ }
609
+ this.warnings.delete('runtime');
610
+ } catch (e) {
611
+ this.warnings.set('runtime', '会话读取暂时受阻(' + (e.code || e.name) + '),将自动重试');
612
+ }
613
+ if (before !== this.revision) this.emit('change');
614
+ }
615
+
616
+ snapshot(rootId) {
617
+ const roots = this.roots();
618
+ const selected = rootId && this.agents.has(rootId) ? this.rootOf(rootId) : roots[0]?.id || null;
619
+ const members = [...this.agents.values()].filter(s => this.rootOf(s.id) === selected && !s._internal);
620
+ const ids = new Set(members.map(s => s.id));
621
+ const allEvents = [...this.events.values()].filter(e => ids.has(e.sessionId)).sort((a, b) => a.at - b.at || a._ref.offset - b._ref.offset);
622
+ const publicEvent = ({ _ref, _textHash, ...e }) => e;
623
+ return {
624
+ mode: 'live',
625
+ revision: this.revision,
626
+ rootId: selected,
627
+ roots,
628
+ agents: members.map(s => {
629
+ const agent = Object.fromEntries(Object.entries(s).filter(([k]) => !k.startsWith('_')));
630
+ agent.rootId = selected;
631
+ agent.name ||= s.id === selected ? '主 Agent' : '子 Agent ' + s.id.slice(0, 6);
632
+ agent.subagent = !!s.parentId;
633
+ agent.events = allEvents
634
+ .filter(e => e.sessionId === s.id || e.targetId === s.id)
635
+ .slice(-80)
636
+ .map(publicEvent);
637
+ return agent;
638
+ }),
639
+ events: allEvents.slice(-160).map(publicEvent),
640
+ totalEvents: allEvents.length,
641
+ warnings: [...new Set(this.warnings.values())],
642
+ updatedAt: Date.now(),
643
+ };
644
+ }
645
+
646
+ fullText(sessionId, eventId) {
647
+ const e = this.events.get(eventId);
648
+ if (!e || e.sessionId !== sessionId) return null;
649
+ const ref = e._ref;
650
+ const fd = fs.openSync(ref.file, 'r');
651
+ let d;
652
+ try {
653
+ const buf = Buffer.alloc(ref.length);
654
+ const n = fs.readSync(fd, buf, 0, buf.length, ref.offset);
655
+ if (n !== ref.length) throw new Error('原始记录已经改变');
656
+ const raw = buf.toString('utf8').trim();
657
+ if (ref.hash && digest(raw) !== ref.hash) throw new Error('原始记录已经改变');
658
+ d = JSON.parse(raw);
659
+ } finally {
660
+ fs.closeSync(fd);
661
+ }
662
+ const p = d.payload || {};
663
+ let text = p.message || p.last_agent_message || p.prompt || textOf(p.content) || textOf(p.item?.content);
664
+ if (p.type === 'collab_waiting_end') text = p.statuses?.[sessionId]?.completed || p.agent_statuses?.find(x => x.thread_id === sessionId)?.status?.completed;
665
+ if (p.type === 'function_call' || p.type === 'custom_tool_call') {
666
+ const a = parseArgs(p.arguments || p.input);
667
+ text = a.message || a.prompt || a.cmd || a.command || p.name;
668
+ }
669
+ const unavailable = isOpaque(text) && ['assign', 'task'].includes(e.type) ? unreadableTask : '';
670
+ return { id: e.id, sessionId, type: e.type, text: visibleText(String(text || '')) || unavailable || e.preview || '' };
671
+ }
672
+ }
673
+
674
+ module.exports = { CodexWatcher, newAgent, short, textOf };