workspacecord 1.1.3 → 1.1.4

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 (25) hide show
  1. package/dist/{archive-manager-RW36JGUV.js → archive-manager-FJU7YEEH.js} +4 -4
  2. package/dist/{attachment-cli-MF7XZ4WT.js → attachment-cli-AT4HXAGU.js} +2 -2
  3. package/dist/{chunk-ZO62NAYX.js → chunk-5EMN2IL5.js} +1 -1
  4. package/dist/{chunk-BFINJJYL.js → chunk-66B64WL3.js} +1 -1
  5. package/dist/{chunk-QWPKAUSV.js → chunk-C4L34VJK.js} +8287 -7421
  6. package/dist/{chunk-WON3DPE4.js → chunk-JR4B4L7I.js} +213 -4
  7. package/dist/{chunk-IVXCJA5I.js → chunk-MJ5JKFGS.js} +9 -0
  8. package/dist/{chunk-7GTUWAQR.js → chunk-NNTMVOTM.js} +876 -315
  9. package/dist/{chunk-WP6YJVAE.js → chunk-PWMEOBXG.js} +4 -4
  10. package/dist/{chunk-AGB4GP4G.js → chunk-SNPFYUQ3.js} +441 -654
  11. package/dist/{chunk-4KQ7OSK7.js → chunk-SV7EHL3B.js} +3 -3
  12. package/dist/cli-framework-YF3LPLMT.js +18 -0
  13. package/dist/cli.js +10 -10
  14. package/dist/{codex-launcher-ZBQ5VL6L.js → codex-launcher-CLGG4CVY.js} +1 -1
  15. package/dist/{codex-provider-Q4Z6UKO6.js → codex-provider-VLOS5QB6.js} +18 -4
  16. package/dist/{config-cli-7JEV3WYY.js → config-cli-7G5YWKJU.js} +2 -2
  17. package/dist/{panel-adapter-U75WXDLB.js → panel-adapter-CLI4WDII.js} +4 -4
  18. package/dist/{project-cli-ZXMHOFUJ.js → project-cli-ALKDLRMZ.js} +2 -2
  19. package/dist/{project-registry-ED6P5ZTM.js → project-registry-ZO3KSS25.js} +2 -2
  20. package/dist/sdk-C3D6X4EB.js +55 -0
  21. package/dist/{session-local-registration-MISPPGXF.js → session-local-registration-RL2A3QZB.js} +3 -3
  22. package/dist/{setup-ZFVMMNT2.js → setup-GP3MML2R.js} +1 -1
  23. package/package.json +3 -3
  24. package/dist/cli-framework-7E5MKPMM.js +0 -18
  25. package/dist/sdk-V7A7IF7F.js +0 -43
@@ -1,11 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  cleanupSessionAttachments
4
- } from "./chunk-BFINJJYL.js";
4
+ } from "./chunk-66B64WL3.js";
5
5
  import {
6
6
  STATE_COLORS,
7
7
  STATE_LABELS,
8
+ createSession,
8
9
  debouncedSaveSession,
10
+ endSession,
9
11
  gateService,
10
12
  getAllSessions,
11
13
  getSession,
@@ -17,17 +19,22 @@ import {
17
19
  stateMachine,
18
20
  toPlatformEvent,
19
21
  updateSession
20
- } from "./chunk-WON3DPE4.js";
22
+ } from "./chunk-JR4B4L7I.js";
21
23
  import {
22
24
  config,
23
25
  truncate
24
- } from "./chunk-IVXCJA5I.js";
26
+ } from "./chunk-MJ5JKFGS.js";
25
27
 
26
28
  // ../bot/src/discord/status-card-projection-renderer.ts
27
29
  var StatusCardProjectionRenderer = class {
28
30
  #pending = /* @__PURE__ */ new Map();
29
31
  async renderNow(sessionId, projection, context) {
30
32
  if (!context) return;
33
+ if (process.env.E2E_DEBUG_RENDER === "1") {
34
+ console.log(
35
+ `[renderer] ${sessionId} todoList=${projection.todoList?.length ?? 0} denials=${projection.recentPermissionDenials?.length ?? 0} batch=${projection.batchApprovalMode}`
36
+ );
37
+ }
31
38
  try {
32
39
  await context.statusCard.update(projection.state, {
33
40
  turn: projection.turn,
@@ -37,7 +44,11 @@ var StatusCardProjectionRenderer = class {
37
44
  provider: context.provider,
38
45
  permissionsSummary: context.permissionsSummary,
39
46
  verbose: context.verbose,
40
- monitorGoal: context.monitorGoal
47
+ monitorGoal: context.monitorGoal,
48
+ todoList: projection.todoList,
49
+ recentPermissionDenials: projection.recentPermissionDenials,
50
+ batchApprovalMode: projection.batchApprovalMode,
51
+ pendingApprovals: projection.pendingApprovals
41
52
  });
42
53
  } catch (error) {
43
54
  console.error(`\u72B6\u6001\u5361\u66F4\u65B0\u5931\u8D25 (${sessionId}):`, error);
@@ -73,156 +84,105 @@ var StatusCardProjectionRenderer = class {
73
84
  }
74
85
  };
75
86
 
76
- // ../bot/src/discord/status-card.ts
77
- import {
78
- EmbedBuilder
79
- } from "discord.js";
80
- var StatusCard = class {
81
- messageId = null;
82
- channel;
83
- lastState = "idle";
84
- lastData = null;
85
- constructor(channel) {
86
- this.channel = channel;
87
- }
88
- adopt(messageId) {
89
- this.messageId = messageId;
87
+ // ../bot/src/output/event-handlers.ts
88
+ import { existsSync as existsSync2 } from "fs";
89
+
90
+ // ../engine/src/session-context.ts
91
+ var EMPTY_PROJECTION = Object.freeze({
92
+ turn: 0,
93
+ humanResolved: false,
94
+ updatedAt: 0
95
+ });
96
+ function safeGetSessionController(sessionId) {
97
+ try {
98
+ const fn = getSessionController;
99
+ return typeof fn === "function" ? fn(sessionId) : void 0;
100
+ } catch {
101
+ return void 0;
90
102
  }
91
- getMessageId() {
92
- return this.messageId;
103
+ }
104
+ function safeDebouncedSaveSession() {
105
+ try {
106
+ const fn = debouncedSaveSession;
107
+ if (typeof fn === "function") fn();
108
+ } catch {
93
109
  }
94
- async initialize(data = {}) {
95
- const payload = {
96
- turn: data.turn ?? 1,
97
- updatedAt: data.updatedAt ?? Date.now(),
98
- phase: data.phase,
99
- remoteHumanControl: data.remoteHumanControl,
100
- provider: data.provider,
101
- permissionsSummary: data.permissionsSummary
102
- };
103
- this.lastState = "idle";
104
- this.lastData = payload;
105
- if (this.messageId) {
106
- await this.update("idle", payload);
107
- return;
110
+ }
111
+ function safeGetProjection(sessionId) {
112
+ try {
113
+ const sm = stateMachine;
114
+ if (sm && typeof sm.getSnapshot === "function") {
115
+ return sm.getSnapshot(sessionId);
108
116
  }
109
- const embed = this.buildEmbed("idle", payload);
110
- await this.sendNewMessage(embed);
117
+ } catch {
111
118
  }
112
- async update(state, data) {
113
- this.lastState = state;
114
- this.lastData = { ...data };
115
- const embed = this.buildEmbed(state, data);
116
- if (!this.messageId) {
117
- await this.sendNewMessage(embed);
118
- return;
119
+ return EMPTY_PROJECTION;
120
+ }
121
+ var SessionSupervisor = class {
122
+ contexts = /* @__PURE__ */ new Map();
123
+ get(sessionId) {
124
+ const live = getSession(sessionId);
125
+ const cached = this.contexts.get(sessionId);
126
+ if (!live) {
127
+ if (cached) this.contexts.delete(sessionId);
128
+ return void 0;
119
129
  }
120
- await this.editExistingMessage(embed);
121
- }
122
- async recreateAtBottom() {
123
- if (!this.messageId || !this.lastData) return null;
124
- const oldMessageId = this.messageId;
125
- const embed = this.buildEmbed(this.lastState, this.lastData);
126
- const msg = await this.channel.send({ embeds: [embed] });
127
- this.messageId = msg.id;
128
- return { oldMessageId, newMessageId: msg.id };
129
- }
130
- async sendNewMessage(embed) {
131
- try {
132
- const msg = await this.channel.send({ embeds: [embed] });
133
- this.messageId = msg.id;
134
- } catch (error) {
135
- console.error("\u72B6\u6001\u5361\u521B\u5EFA\u5931\u8D25:", error);
136
- throw error;
130
+ if (cached) {
131
+ this.syncContext(cached);
132
+ return cached;
137
133
  }
134
+ const ctx = this.build(live);
135
+ this.contexts.set(sessionId, ctx);
136
+ return ctx;
138
137
  }
139
- async editExistingMessage(embed) {
140
- if (!this.messageId) {
141
- await this.sendNewMessage(embed);
142
- return;
143
- }
144
- try {
145
- const msg = await this.channel.messages.edit(this.messageId, {
146
- embeds: [embed],
147
- components: []
148
- });
149
- this.messageId = msg.id;
150
- } catch (error) {
151
- console.warn(`\u72B6\u6001\u5361\u7F16\u8F91\u5931\u8D25 (${this.messageId}), \u521B\u5EFA\u65B0\u6D88\u606F:`, error);
152
- await this.sendNewMessage(embed);
153
- }
138
+ /** 枚举现有 context。用于 supervisor 层面的扫描(健康检查、空闲回收) */
139
+ all() {
140
+ return Array.from(this.contexts.values());
154
141
  }
155
- buildEmbed(state, data) {
156
- const embed = new EmbedBuilder().setColor(STATE_COLORS[state]).setTitle(`\u{1F916} ${STATE_LABELS[state]}`).addFields(
157
- { name: "\u8F6E\u6B21", value: `#${data.turn}`, inline: true },
158
- { name: "\u66F4\u65B0", value: `<t:${Math.floor(data.updatedAt / 1e3)}:R>`, inline: true }
159
- ).setTimestamp();
160
- if (data.provider === "codex") {
161
- const managedLabel = data.remoteHumanControl ? "\u2713 \u53D7\u7BA1\u4F1A\u8BDD" : "\u25CB \u975E\u53D7\u7BA1\u4F1A\u8BDD\uFF08\u4EC5\u72B6\u6001\u76D1\u63A7\uFF09";
162
- embed.addFields({ name: "\u4F1A\u8BDD\u7C7B\u578B", value: managedLabel, inline: true });
163
- }
164
- if (data.phase) {
165
- const sanitizedPhase = this.sanitizePhase(data.phase);
166
- if (sanitizedPhase) {
167
- embed.addFields({ name: "\u9636\u6BB5", value: sanitizedPhase, inline: true });
168
- }
169
- }
170
- if (data.verbose !== void 0) {
171
- embed.addFields({ name: "\u8F93\u51FA", value: data.verbose ? "\u{1F50A} \u8BE6\u7EC6" : "\u{1F507} \u7CBE\u7B80", inline: true });
172
- }
173
- if (data.monitorGoal) {
174
- embed.addFields({ name: "\u76D1\u63A7\u76EE\u6807", value: truncate(data.monitorGoal, 150) });
175
- }
176
- if (data.monitorIteration !== void 0 && data.maxMonitorIterations !== void 0) {
177
- embed.addFields({ name: "\u8FED\u4EE3", value: `${data.monitorIteration}/${data.maxMonitorIterations}`, inline: true });
178
- }
179
- if (data.permissionsSummary) {
180
- embed.addFields({ name: "\u6743\u9650", value: data.permissionsSummary, inline: false });
181
- }
182
- return embed;
142
+ /** 释放某个 session 的 context(endSession 调用时触发) */
143
+ release(sessionId) {
144
+ this.contexts.delete(sessionId);
183
145
  }
184
- /**
185
- * Sanitize phase text for status card display.
186
- * Returns cleaned text or empty string if unsuitable.
187
- */
188
- sanitizePhase(description) {
189
- let normalized = description.trim();
190
- if (!normalized) return "";
191
- if (normalized.includes("```") || /diff --git/.test(normalized) || this.isLikelyFileList(normalized)) {
192
- console.warn(`[StatusCard] Phase contains unsuitable content, stripping: ${normalized.slice(0, 60)}...`);
193
- return "";
194
- }
195
- if (normalized.length > 200) {
196
- normalized = normalized.slice(0, 197) + "...";
197
- }
198
- return normalized;
146
+ releaseAll() {
147
+ this.contexts.clear();
199
148
  }
200
- validate(description) {
201
- if (!description) return;
202
- const normalized = description.trim();
203
- if (!normalized) return;
204
- if (normalized.length > 200) {
205
- throw new Error("\u72B6\u6001\u5361\u63CF\u8FF0\u8FC7\u957F\uFF0C\u5E94\u79FB\u81F3\u6458\u8981\u5361\u6216\u7ED3\u679C\u6D88\u606F");
206
- }
207
- if (normalized.includes("```")) {
208
- throw new Error("\u72B6\u6001\u5361\u4E0D\u5E94\u5305\u542B\u4EE3\u7801\u5757");
209
- }
210
- if (/diff --git/.test(normalized)) {
211
- throw new Error("\u72B6\u6001\u5361\u4E0D\u5E94\u5305\u542B diff");
212
- }
213
- if (this.isLikelyFileList(normalized)) {
214
- throw new Error("\u72B6\u6001\u5361\u4E0D\u5E94\u5305\u542B\u6587\u4EF6\u5217\u8868");
215
- }
149
+ build(session) {
150
+ const ctx = {
151
+ sessionId: session.id,
152
+ session,
153
+ controller: safeGetSessionController(session.id),
154
+ projection: safeGetProjection(session.id),
155
+ save: () => safeDebouncedSaveSession(),
156
+ refresh: () => this.syncContext(ctx)
157
+ };
158
+ return ctx;
216
159
  }
217
- isLikelyFileList(text) {
218
- const lines = text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
219
- if (lines.length < 2) return false;
220
- return lines.every((line) => /^[-+*]\s+[\w./\\-]+$/.test(line));
160
+ syncContext(ctx) {
161
+ const live = getSession(ctx.sessionId);
162
+ if (live) {
163
+ ctx.session = live;
164
+ }
165
+ ctx.controller = safeGetSessionController(
166
+ ctx.sessionId
167
+ );
168
+ ctx.projection = safeGetProjection(
169
+ ctx.sessionId
170
+ );
221
171
  }
222
172
  };
173
+ var sessionSupervisor = new SessionSupervisor();
174
+ function getSessionContext(sessionId) {
175
+ return sessionSupervisor.get(sessionId);
176
+ }
177
+ function getSessionView(sessionId) {
178
+ return sessionSupervisor.get(sessionId)?.session;
179
+ }
223
180
 
224
- // ../bot/src/discord/summary-handler.ts
225
- import "discord.js";
181
+ // ../bot/src/subagent-manager.ts
182
+ import {
183
+ ChannelType,
184
+ ThreadAutoArchiveDuration
185
+ } from "discord.js";
226
186
 
227
187
  // ../bot/src/discord/delivery-policy.ts
228
188
  import { existsSync, statSync } from "fs";
@@ -338,71 +298,772 @@ async function sendWithBackoff(channel, payload) {
338
298
  const message = await channel.send(payload);
339
299
  return message.id;
340
300
  } catch (error) {
341
- if (!isDiscordRateLimitError(error) || attempt >= SEND_MAX_RETRIES) {
342
- throw error;
343
- }
344
- const delayMs = 250 * 2 ** attempt;
345
- console.warn(
346
- `[Delivery] Discord rate-limited (attempt ${attempt + 1}/${SEND_MAX_RETRIES}), retrying in ${delayMs}ms`
347
- );
348
- await sleep(delayMs);
349
- attempt++;
301
+ if (!isDiscordRateLimitError(error) || attempt >= SEND_MAX_RETRIES) {
302
+ throw error;
303
+ }
304
+ const delayMs = 250 * 2 ** attempt;
305
+ console.warn(
306
+ `[Delivery] Discord rate-limited (attempt ${attempt + 1}/${SEND_MAX_RETRIES}), retrying in ${delayMs}ms`
307
+ );
308
+ await sleep(delayMs);
309
+ attempt++;
310
+ }
311
+ }
312
+ }
313
+ async function sendChunk(channel, chunk, options = {}) {
314
+ const payload = { content: chunk };
315
+ if (options.files?.length) payload.files = options.files;
316
+ if (options.replyToMessageId) {
317
+ payload.reply = { messageReference: options.replyToMessageId };
318
+ }
319
+ try {
320
+ return await sendWithBackoff(channel, payload);
321
+ } catch (error) {
322
+ if (!options.replyToMessageId) {
323
+ throw error;
324
+ }
325
+ const fallbackPayload = { content: chunk };
326
+ if (options.files?.length) fallbackPayload.files = options.files;
327
+ return sendWithBackoff(channel, fallbackPayload);
328
+ }
329
+ }
330
+ async function deliver(channel, plan) {
331
+ pruneStaleSessionStates();
332
+ const state = getSessionState(plan.sessionId);
333
+ if (plan.mode === "progress_update") {
334
+ const targetMessageId = plan.editTargetMessageId ?? state.recentProgressMessageId;
335
+ if (targetMessageId && plan.chunks.length === 1 && plan.filesOnFirstChunk.length === 0) {
336
+ try {
337
+ await channel.messages?.edit(targetMessageId, { content: plan.chunks[0] });
338
+ state.recentProgressMessageId = targetMessageId;
339
+ return [targetMessageId];
340
+ } catch {
341
+ }
342
+ }
343
+ }
344
+ const ids = [];
345
+ const replyToMode = plan.replyToMode ?? "first";
346
+ for (let index = 0; index < plan.chunks.length; index++) {
347
+ const shouldReply = !!plan.replyToMessageId && replyToMode !== "off" && (replyToMode === "all" || index === 0);
348
+ const id = await sendChunk(channel, plan.chunks[index], {
349
+ replyToMessageId: shouldReply ? plan.replyToMessageId : void 0,
350
+ files: index === 0 ? plan.filesOnFirstChunk : void 0
351
+ });
352
+ ids.push(id);
353
+ }
354
+ if (ids.length > 0) {
355
+ if (plan.mode === "progress_update") {
356
+ state.recentProgressMessageId = ids[ids.length - 1];
357
+ } else {
358
+ state.recentFinalMessageId = ids[ids.length - 1];
359
+ }
360
+ }
361
+ return ids;
362
+ }
363
+
364
+ // ../bot/src/discord/delivery-notices.ts
365
+ function getDeliveryPolicy() {
366
+ return {
367
+ textChunkLimit: config.textChunkLimit ?? 2e3,
368
+ chunkMode: config.chunkMode ?? "length",
369
+ replyToMode: config.replyToMode ?? "first",
370
+ ackReaction: config.ackReaction ?? "\u{1F440}"
371
+ };
372
+ }
373
+ async function sendSystemNotice(channel, sessionId, text, replyToMessageId) {
374
+ if (!text.trim()) return;
375
+ const plan = buildDeliveryPlan({
376
+ sessionId,
377
+ chatId: channel.id,
378
+ text,
379
+ files: [],
380
+ mode: "system_notice",
381
+ replyToMessageId,
382
+ policy: getDeliveryPolicy()
383
+ });
384
+ try {
385
+ await deliver(channel, plan);
386
+ } catch {
387
+ }
388
+ }
389
+
390
+ // ../bot/src/subagent-manager.ts
391
+ var SUBAGENT_IDLE_TIMEOUT_MS = 60 * 60 * 1e3;
392
+ function canSpawnSubagent(parentSession) {
393
+ return parentSession.subagentDepth < config.maxSubagentDepth;
394
+ }
395
+ async function spawnSubagent(parentSession, label, provider, sessionChannel) {
396
+ if (!canSpawnSubagent(parentSession)) {
397
+ console.warn(`[SubagentManager] Depth limit hit for session ${parentSession.id} (max depth ${config.maxSubagentDepth})`);
398
+ throw new Error(
399
+ `Max subagent depth (${config.maxSubagentDepth}) reached. Cannot spawn further subagents.`
400
+ );
401
+ }
402
+ const threadName = `[sub:${provider}] ${label}`.slice(0, 100);
403
+ const thread = await sessionChannel.threads.create({
404
+ name: threadName,
405
+ type: ChannelType.PublicThread,
406
+ autoArchiveDuration: ThreadAutoArchiveDuration.OneHour,
407
+ reason: `Subagent spawned by session ${parentSession.id}`
408
+ });
409
+ const session = await createSession({
410
+ channelId: thread.id,
411
+ // Subagent's primary ID is the Thread
412
+ categoryId: parentSession.categoryId,
413
+ projectName: parentSession.projectName,
414
+ agentLabel: label,
415
+ provider,
416
+ directory: parentSession.directory,
417
+ type: "subagent",
418
+ parentChannelId: parentSession.channelId,
419
+ // Parent session's TextChannel
420
+ subagentDepth: parentSession.subagentDepth + 1,
421
+ mode: parentSession.mode,
422
+ claudePermissionMode: provider === "claude" ? parentSession.claudePermissionMode : void 0
423
+ });
424
+ console.log(`[SubagentManager] Spawned subagent "${label}" session ${session.id} thread ${thread.id} (depth ${session.subagentDepth}, parent ${parentSession.id})`);
425
+ return session;
426
+ }
427
+ async function archiveSubagent(session, thread, summary) {
428
+ if (summary) {
429
+ await sendSystemNotice(thread, session.id, `*\u5B50\u4EFB\u52A1\u5B8C\u6210\uFF1A${summary}*`);
430
+ }
431
+ try {
432
+ await thread.setArchived(true, "Subagent task completed");
433
+ } catch (error) {
434
+ console.warn(`[SubagentManager] Failed to archive thread ${thread.id} for subagent ${session.id}: ${error instanceof Error ? error.message : String(error)}`);
435
+ }
436
+ try {
437
+ await endSession(session.id);
438
+ console.log(`[SubagentManager] Archived subagent ${session.id} thread ${thread.id}${summary ? ` \u2014 ${summary}` : ""}`);
439
+ } catch (error) {
440
+ console.warn(`[SubagentManager] Failed to end session for subagent ${session.id}: ${error instanceof Error ? error.message : String(error)}`);
441
+ }
442
+ }
443
+ function getSubagents(parentSession) {
444
+ return getAllSessions().filter(
445
+ (s) => s.type === "subagent" && s.parentChannelId === parentSession.channelId
446
+ );
447
+ }
448
+ async function runSubagentWatchdog(getThread) {
449
+ const now = Date.now();
450
+ const checked = /* @__PURE__ */ new Set();
451
+ let archived = 0;
452
+ let errors = 0;
453
+ for (const session of getSubagentSessions()) {
454
+ if (checked.has(session.id)) continue;
455
+ checked.add(session.id);
456
+ const idle = now - session.lastActivity;
457
+ if (idle < SUBAGENT_IDLE_TIMEOUT_MS) continue;
458
+ if (session.isGenerating) continue;
459
+ const thread = getThread(session.channelId);
460
+ if (!thread) {
461
+ await endSession(session.id).catch((e) => console.warn(`[SubagentWatchdog] Failed to end orphaned subagent ${session.id}: ${e.message}`));
462
+ console.log(`[SubagentWatchdog] Ended orphaned subagent ${session.id} \u2014 thread ${session.channelId} not found`);
463
+ continue;
464
+ }
465
+ try {
466
+ await archiveSubagent(session, thread, "Idle timeout reached.");
467
+ archived += 1;
468
+ } catch (error) {
469
+ console.error(`[SubagentWatchdog] Failed to archive idle subagent ${session.id}: ${error instanceof Error ? error.message : String(error)}`);
470
+ errors += 1;
471
+ }
472
+ }
473
+ console.log(`[SubagentWatchdog] Watchdog run: checked ${checked.size} subagents, archived ${archived}, errors ${errors}`);
474
+ }
475
+ async function autoSpawnSubagentThread(parentSession, taskId, description, sessionChannel) {
476
+ if (parentSession.type === "subagent") return null;
477
+ if (!canSpawnSubagent(parentSession)) return null;
478
+ const threadName = `[task] ${description}`.slice(0, 100);
479
+ const thread = await sessionChannel.threads.create({
480
+ name: threadName,
481
+ type: ChannelType.PublicThread,
482
+ autoArchiveDuration: ThreadAutoArchiveDuration.OneHour,
483
+ reason: `Auto-created for task ${taskId} in session ${parentSession.id}`
484
+ });
485
+ let session;
486
+ try {
487
+ session = await createSession({
488
+ channelId: thread.id,
489
+ categoryId: parentSession.categoryId,
490
+ projectName: parentSession.projectName,
491
+ agentLabel: description,
492
+ provider: parentSession.provider,
493
+ directory: parentSession.directory,
494
+ type: "subagent",
495
+ parentChannelId: parentSession.channelId,
496
+ subagentDepth: (parentSession.subagentDepth || 0) + 1,
497
+ mode: parentSession.mode,
498
+ claudePermissionMode: parentSession.provider === "claude" ? parentSession.claudePermissionMode : void 0
499
+ });
500
+ } catch (err) {
501
+ console.warn(`[SubagentManager] createSession failed for auto-spawned task ${taskId}, deleting orphan thread ${thread.id}: ${err instanceof Error ? err.message : String(err)}`);
502
+ await thread.delete("Session creation failed").catch(
503
+ (deleteErr) => console.warn(`[SubagentManager] Failed to delete orphan thread ${thread.id}: ${deleteErr instanceof Error ? deleteErr.message : String(deleteErr)}`)
504
+ );
505
+ throw err;
506
+ }
507
+ console.log(`[SubagentManager] Auto-spawned thread for task ${taskId} session ${session.id} thread ${thread.id}`);
508
+ return { threadId: thread.id, session };
509
+ }
510
+ function getSubagentSessions() {
511
+ return getAllSessions().filter((s) => s.type === "subagent");
512
+ }
513
+
514
+ // ../bot/src/codex-renderer.ts
515
+ import { EmbedBuilder } from "discord.js";
516
+
517
+ // ../bot/src/output/interaction-controls.ts
518
+ import {
519
+ ActionRowBuilder,
520
+ ButtonBuilder,
521
+ ButtonStyle,
522
+ StringSelectMenuBuilder,
523
+ EmbedBuilder as EmbedBuilder2
524
+ } from "discord.js";
525
+
526
+ // ../engine/src/output/answer-store.ts
527
+ var pendingAnswersStore = /* @__PURE__ */ new Map();
528
+ var questionCountStore = /* @__PURE__ */ new Map();
529
+ function setPendingAnswer(sessionId, questionIndex, answer) {
530
+ if (!pendingAnswersStore.has(sessionId)) {
531
+ pendingAnswersStore.set(sessionId, /* @__PURE__ */ new Map());
532
+ }
533
+ pendingAnswersStore.get(sessionId).set(questionIndex, answer);
534
+ }
535
+ function getPendingAnswers(sessionId) {
536
+ return pendingAnswersStore.get(sessionId);
537
+ }
538
+ function clearPendingAnswers(sessionId) {
539
+ pendingAnswersStore.delete(sessionId);
540
+ questionCountStore.delete(sessionId);
541
+ }
542
+ function getQuestionCount(sessionId) {
543
+ return questionCountStore.get(sessionId) || 0;
544
+ }
545
+
546
+ // ../bot/src/output/interaction-controls.ts
547
+ function resolveEffectiveClaudePermissionMode(currentMode, claudePermissionMode) {
548
+ if (!claudePermissionMode) return void 0;
549
+ return currentMode === "auto" ? "bypass" : claudePermissionMode;
550
+ }
551
+ function makeModeButtons(sessionId, currentMode, claudePermissionMode) {
552
+ const modes = [
553
+ { id: "auto", label: "\u26A1 \u81EA\u52A8\u6A21\u5F0F" },
554
+ { id: "plan", label: "\u{1F4CB} \u8BA1\u5212\u6A21\u5F0F" },
555
+ { id: "normal", label: "\u{1F6E1}\uFE0F \u666E\u901A\u6A21\u5F0F" },
556
+ { id: "monitor", label: "\u{1F9E0} \u76D1\u63A7\u6A21\u5F0F" }
557
+ ];
558
+ const row = new ActionRowBuilder();
559
+ for (const m of modes) {
560
+ row.addComponents(
561
+ new ButtonBuilder().setCustomId(`mode:${sessionId}:${m.id}`).setLabel(m.id === currentMode ? `\u2713 ${m.label}` : m.label).setStyle(m.id === currentMode ? ButtonStyle.Success : ButtonStyle.Secondary).setDisabled(false)
562
+ );
563
+ }
564
+ const effectiveClaudePermissionMode = resolveEffectiveClaudePermissionMode(
565
+ currentMode,
566
+ claudePermissionMode
567
+ );
568
+ if (effectiveClaudePermissionMode) {
569
+ const permLabel = effectiveClaudePermissionMode === "bypass" ? "\u26A1 \u7ED5\u8FC7\u6743\u9650" : "\u{1F6E1}\uFE0F \u9700\u8981\u786E\u8BA4";
570
+ row.addComponents(
571
+ new ButtonBuilder().setCustomId(`perm-info:${sessionId}`).setLabel(permLabel).setStyle(
572
+ effectiveClaudePermissionMode === "bypass" ? ButtonStyle.Danger : ButtonStyle.Success
573
+ ).setDisabled(true)
574
+ );
575
+ }
576
+ return row;
577
+ }
578
+ function shouldSuppressCommandExecution(command) {
579
+ return command.toLowerCase().includes("total-recall");
580
+ }
581
+
582
+ // ../bot/src/output/event-handlers.ts
583
+ function providerSource(sessionId) {
584
+ const session = getSessionView(sessionId);
585
+ return session?.provider === "codex" ? "codex" : "claude";
586
+ }
587
+ var textDelta = (event, ctx) => {
588
+ ctx.streamer.append(event.text);
589
+ };
590
+ var askUser = async (event, ctx) => {
591
+ ctx.state.askedUser = true;
592
+ ctx.state.askUserQuestionsJson = event.questionsJson;
593
+ await ctx.streamer.discard();
594
+ const session = getSessionView(ctx.sessionId);
595
+ if (!session) return;
596
+ const source = providerSource(ctx.sessionId);
597
+ await updateSessionState(ctx.sessionId, {
598
+ type: "awaiting_human",
599
+ sessionId: ctx.sessionId,
600
+ source,
601
+ confidence: "high",
602
+ timestamp: Date.now(),
603
+ metadata: { detail: event.questionsJson }
604
+ });
605
+ await flushDigest(ctx.sessionId);
606
+ await handleAwaitingHuman(ctx.sessionId, event.questionsJson, { source });
607
+ };
608
+ var task = async (event, ctx) => {
609
+ await ctx.streamer.finalize();
610
+ queueDigest(ctx.sessionId, { kind: "tool", text: `\u4EFB\u52A1\u5DE5\u5177\uFF1A${event.action}` });
611
+ ctx.state.lastToolName = event.action;
612
+ };
613
+ var taskStarted = async (event, ctx) => {
614
+ await ctx.streamer.finalize();
615
+ queueDigest(ctx.sessionId, {
616
+ kind: "subagent",
617
+ text: `\u5B50\u4EE3\u7406\u542F\u52A8\uFF1A${truncate(event.description, 80)}`
618
+ });
619
+ const session = getSessionView(ctx.sessionId);
620
+ if (!session || ctx.channel.type === void 0) return;
621
+ autoSpawnSubagentThread(session, event.taskId, event.description, ctx.channel).then((result2) => {
622
+ if (result2) ctx.state.taskThreadMap.set(event.taskId, result2.threadId);
623
+ }).catch(
624
+ (err) => console.warn(
625
+ `[OutputHandler] Failed to auto-spawn thread for task ${event.taskId}: ${err.message}`
626
+ )
627
+ );
628
+ };
629
+ var taskProgress = (event, ctx) => {
630
+ if (event.summary) {
631
+ queueDigest(ctx.sessionId, {
632
+ kind: "subagent",
633
+ text: `\u5B50\u4EE3\u7406\u8FDB\u5C55\uFF1A${truncate(event.summary, 100)}`
634
+ });
635
+ }
636
+ const threadId = ctx.state.taskThreadMap.get(event.taskId);
637
+ if (!threadId || !event.summary) return;
638
+ const thread = ctx.channel.threads?.cache.get(threadId);
639
+ if (!thread) return;
640
+ thread.send(`\u{1F4DD} ${truncate(event.summary, 1900)}`).catch(
641
+ (e) => console.warn(
642
+ `[OutputHandler] Failed to send progress to thread: ${e.message}`
643
+ )
644
+ );
645
+ };
646
+ var taskDone = async (event, ctx) => {
647
+ await ctx.streamer.finalize();
648
+ queueDigest(ctx.sessionId, {
649
+ kind: "subagent",
650
+ text: `\u5B50\u4EE3\u7406${event.status === "completed" ? "\u5B8C\u6210" : "\u7ED3\u675F"}\uFF1A${truncate(event.summary || "No summary.", 100)}`
651
+ });
652
+ const threadId = ctx.state.taskThreadMap.get(event.taskId);
653
+ if (!threadId) return;
654
+ const thread = ctx.channel.threads?.cache.get(threadId);
655
+ if (thread) {
656
+ const emoji = event.status === "completed" ? "\u2705" : "\u274C";
657
+ thread.send(`${emoji} \u5B50\u4EFB\u52A1${event.status === "completed" ? "\u5B8C\u6210" : "\u7ED3\u675F"}\uFF1A${truncate(event.summary || "", 1900)}`).catch(
658
+ (e) => console.warn(
659
+ `[OutputHandler] Failed to send task done to thread: ${e.message}`
660
+ )
661
+ );
662
+ }
663
+ ctx.state.taskThreadMap.delete(event.taskId);
664
+ };
665
+ var webSearch = (event, ctx) => {
666
+ if (ctx.verbose) {
667
+ queueDigest(ctx.sessionId, { kind: "search", text: `\u68C0\u7D22\uFF1A${truncate(event.query, 80)}` });
668
+ }
669
+ };
670
+ var toolStart = async (event, ctx) => {
671
+ await ctx.streamer.finalize();
672
+ queueDigest(ctx.sessionId, { kind: "tool", text: `\u5DE5\u5177\uFF1A${event.toolName}` });
673
+ ctx.state.lastToolName = event.toolName;
674
+ };
675
+ var toolResult = async (event, ctx) => {
676
+ await ctx.streamer.finalize();
677
+ if (ctx.verbose && event.result) {
678
+ queueDigest(ctx.sessionId, {
679
+ kind: "tool",
680
+ text: `\u5DE5\u5177\u7ED3\u679C\uFF1A${truncate(ctx.state.lastToolName || event.toolName || "tool", 60)}`
681
+ });
682
+ }
683
+ };
684
+ var imageFile = (event, ctx) => {
685
+ if (existsSync2(event.filePath)) {
686
+ ctx.state.pendingAttachments.push(event.filePath);
687
+ }
688
+ };
689
+ var commandExecution = (event, ctx) => {
690
+ ctx.state.commandCount++;
691
+ if (ctx.state.recentCommands.length < 8) ctx.state.recentCommands.push(event.command);
692
+ if (!shouldSuppressCommandExecution(event.command)) {
693
+ queueDigest(ctx.sessionId, {
694
+ kind: "command",
695
+ text: `\u547D\u4EE4\uFF1A${truncate(event.command, 80)}${event.exitCode !== null ? `\uFF08\u9000\u51FA\u7801 ${event.exitCode}\uFF09` : ""}`
696
+ });
697
+ }
698
+ };
699
+ var fileChange = (event, ctx) => {
700
+ ctx.state.fileChangeCount += event.changes.length;
701
+ for (const change of event.changes) {
702
+ if (!change.filePath) continue;
703
+ if (ctx.state.changedFiles.includes(change.filePath)) continue;
704
+ if (ctx.state.changedFiles.length >= 12) break;
705
+ ctx.state.changedFiles.push(change.filePath);
706
+ }
707
+ queueDigest(ctx.sessionId, {
708
+ kind: "file",
709
+ text: `\u6587\u4EF6\u53D8\u66F4\uFF1A${event.changes.length} \u4E2A\uFF08\u6700\u8FD1\uFF1A${truncate(ctx.state.changedFiles.slice(-3).join(", "), 120)}\uFF09`
710
+ });
711
+ };
712
+ var reasoning = (event, ctx) => {
713
+ if (ctx.verbose) {
714
+ queueDigest(ctx.sessionId, { kind: "reasoning", text: `\u63A8\u7406\uFF1A${truncate(event.text, 100)}` });
715
+ }
716
+ };
717
+ var todoList = async (event, ctx) => {
718
+ const completed = event.items.filter((item) => item.completed).length;
719
+ queueDigest(ctx.sessionId, {
720
+ kind: "todo",
721
+ text: `\u5F85\u529E\u66F4\u65B0\uFF1A${completed}/${event.items.length} \u5DF2\u5B8C\u6210`
722
+ });
723
+ await updateSessionState(ctx.sessionId, {
724
+ type: "todo_updated",
725
+ sessionId: ctx.sessionId,
726
+ source: providerSource(ctx.sessionId),
727
+ confidence: "high",
728
+ timestamp: Date.now(),
729
+ metadata: { items: event.items }
730
+ });
731
+ };
732
+ var MODE_LABELS = {
733
+ auto: "\u81EA\u52A8",
734
+ plan: "\u8BA1\u5212",
735
+ normal: "\u666E\u901A",
736
+ monitor: "\u76D1\u63A7"
737
+ };
738
+ var TERMINAL_REASON_LABELS = {
739
+ completed: "\u5DF2\u5B8C\u6210",
740
+ max_turns: "\u5DF2\u8FBE\u6700\u5927\u8F6E\u6B21",
741
+ aborted: "\u5DF2\u4E2D\u6B62",
742
+ rate_limited: "\u89E6\u53D1\u901F\u7387\u9650\u5236",
743
+ context_too_long: "\u4E0A\u4E0B\u6587\u8D85\u957F",
744
+ model_error: "\u6A21\u578B\u9519\u8BEF",
745
+ tool_deferred: "\u5DE5\u5177\u5BA1\u6279\u5EF6\u540E",
746
+ hook_stopped: "\u88AB\u94A9\u5B50\u963B\u65AD",
747
+ image_error: "\u56FE\u50CF\u9519\u8BEF",
748
+ error: "\u5F02\u5E38"
749
+ };
750
+ function formatTerminalReason(reason, fallbackSuccess) {
751
+ if (!reason) return fallbackSuccess ? "\u5DF2\u5B8C\u6210" : "\u5F02\u5E38";
752
+ return TERMINAL_REASON_LABELS[reason] ?? reason;
753
+ }
754
+ var result = async (event, ctx) => {
755
+ ctx.state.success = event.success;
756
+ const lastText = ctx.streamer.getText();
757
+ const cost = event.costUsd.toFixed(4);
758
+ const duration = event.durationMs ? `${(event.durationMs / 1e3).toFixed(1)}s` : "unknown";
759
+ const turns = event.numTurns || 0;
760
+ const modeLabel = MODE_LABELS[ctx.mode] || "\u81EA\u52A8";
761
+ const reasonLabel = formatTerminalReason(event.terminalReason, event.success);
762
+ const showReason = event.terminalReason && event.terminalReason !== "completed";
763
+ const reasonSuffix = showReason ? ` | ${reasonLabel}` : "";
764
+ const statusLine = event.success ? `-# $${cost} | ${duration} | ${turns} turns | ${modeLabel}${reasonSuffix}` : `-# ${reasonLabel} | $${cost} | ${duration} | ${turns} turns`;
765
+ ctx.streamer.append(`
766
+ ${statusLine}`, { persist: false });
767
+ if (!event.success && event.errors.length) {
768
+ ctx.streamer.append(`
769
+ \`\`\`
770
+ ${event.errors.join("\n")}
771
+ \`\`\``, { persist: false });
772
+ }
773
+ await ctx.streamer.finalize();
774
+ const session = getSessionView(ctx.sessionId);
775
+ if (!session) {
776
+ ctx.state.pendingAttachments = [];
777
+ return;
778
+ }
779
+ if (ctx.mode === "monitor") {
780
+ await flushDigest(ctx.sessionId);
781
+ await updateSessionState(ctx.sessionId, {
782
+ type: "work_started",
783
+ sessionId: ctx.sessionId,
784
+ source: providerSource(ctx.sessionId),
785
+ confidence: "high",
786
+ timestamp: Date.now(),
787
+ metadata: {
788
+ phase: "\u7B49\u5F85\u76D1\u7763\u5224\u65AD",
789
+ summary: event.success ? "\u672C\u8F6E\u6267\u884C\u7ED3\u675F\uFF0C\u7B49\u5F85\u76D1\u7763\u5224\u65AD" : "\u672C\u8F6E\u6267\u884C\u5931\u8D25\uFF0C\u7B49\u5F85\u76D1\u7763\u5224\u65AD"
790
+ }
791
+ });
792
+ } else {
793
+ ctx.state.deferredResult = {
794
+ event,
795
+ text: lastText,
796
+ attachments: [...ctx.state.pendingAttachments]
797
+ };
798
+ }
799
+ ctx.state.pendingAttachments = [];
800
+ };
801
+ var permissionDenied = async (event, ctx) => {
802
+ queueDigest(ctx.sessionId, {
803
+ kind: "denied",
804
+ text: `\u26D4 \u6743\u9650\u62D2\u7EDD\uFF1A${truncate(event.toolName, 40)} \u2014 ${truncate(event.reason, 80)}`
805
+ });
806
+ await updateSessionState(ctx.sessionId, {
807
+ type: "permission_denied",
808
+ sessionId: ctx.sessionId,
809
+ source: providerSource(ctx.sessionId),
810
+ confidence: "high",
811
+ timestamp: Date.now(),
812
+ metadata: {
813
+ toolName: event.toolName,
814
+ reason: event.reason,
815
+ source: event.source
816
+ }
817
+ });
818
+ };
819
+ var errorEvent = async (event, ctx) => {
820
+ console.warn(`[OutputHandler] Session ${ctx.sessionId}: error event \u2014 ${event.message}`);
821
+ ctx.state.hadError = true;
822
+ await ctx.streamer.finalize();
823
+ queueDigest(ctx.sessionId, { kind: "error", text: `\u9519\u8BEF\uFF1A${truncate(event.message, 120)}` });
824
+ if (ctx.mode === "monitor") return;
825
+ const session = getSessionView(ctx.sessionId);
826
+ if (!session) return;
827
+ await flushDigest(ctx.sessionId);
828
+ await updateSessionState(ctx.sessionId, {
829
+ type: "errored",
830
+ sessionId: ctx.sessionId,
831
+ source: providerSource(ctx.sessionId),
832
+ confidence: "high",
833
+ timestamp: Date.now(),
834
+ metadata: { errorMessage: event.message }
835
+ });
836
+ };
837
+ var sessionInit = () => {
838
+ };
839
+ var EVENT_HANDLERS = {
840
+ text_delta: textDelta,
841
+ ask_user: askUser,
842
+ task,
843
+ task_started: taskStarted,
844
+ task_progress: taskProgress,
845
+ task_done: taskDone,
846
+ web_search: webSearch,
847
+ tool_start: toolStart,
848
+ tool_result: toolResult,
849
+ image_file: imageFile,
850
+ command_execution: commandExecution,
851
+ file_change: fileChange,
852
+ reasoning,
853
+ todo_list: todoList,
854
+ permission_denied: permissionDenied,
855
+ result,
856
+ error: errorEvent,
857
+ session_init: sessionInit
858
+ };
859
+ async function dispatchEvent(event, ctx) {
860
+ const handler = EVENT_HANDLERS[event.type];
861
+ if (!handler) return;
862
+ await handler(event, ctx);
863
+ }
864
+
865
+ // ../bot/src/discord/status-card.ts
866
+ import {
867
+ EmbedBuilder as EmbedBuilder3
868
+ } from "discord.js";
869
+ var StatusCard = class {
870
+ messageId = null;
871
+ channel;
872
+ lastState = "idle";
873
+ lastData = null;
874
+ constructor(channel) {
875
+ this.channel = channel;
876
+ }
877
+ adopt(messageId) {
878
+ this.messageId = messageId;
879
+ }
880
+ getMessageId() {
881
+ return this.messageId;
882
+ }
883
+ async initialize(data = {}) {
884
+ const payload = {
885
+ turn: data.turn ?? 1,
886
+ updatedAt: data.updatedAt ?? Date.now(),
887
+ phase: data.phase,
888
+ remoteHumanControl: data.remoteHumanControl,
889
+ provider: data.provider,
890
+ permissionsSummary: data.permissionsSummary
891
+ };
892
+ this.lastState = "idle";
893
+ this.lastData = payload;
894
+ if (this.messageId) {
895
+ await this.update("idle", payload);
896
+ return;
897
+ }
898
+ const embed = this.buildEmbed("idle", payload);
899
+ await this.sendNewMessage(embed);
900
+ }
901
+ async update(state, data) {
902
+ this.lastState = state;
903
+ this.lastData = { ...data };
904
+ const embed = this.buildEmbed(state, data);
905
+ if (!this.messageId) {
906
+ await this.sendNewMessage(embed);
907
+ return;
908
+ }
909
+ await this.editExistingMessage(embed);
910
+ }
911
+ async recreateAtBottom() {
912
+ if (!this.messageId || !this.lastData) return null;
913
+ const oldMessageId = this.messageId;
914
+ const embed = this.buildEmbed(this.lastState, this.lastData);
915
+ const msg = await this.channel.send({ embeds: [embed] });
916
+ this.messageId = msg.id;
917
+ return { oldMessageId, newMessageId: msg.id };
918
+ }
919
+ async sendNewMessage(embed) {
920
+ try {
921
+ const msg = await this.channel.send({ embeds: [embed] });
922
+ this.messageId = msg.id;
923
+ } catch (error) {
924
+ console.error("\u72B6\u6001\u5361\u521B\u5EFA\u5931\u8D25:", error);
925
+ throw error;
350
926
  }
351
927
  }
352
- }
353
- async function sendChunk(channel, chunk, options = {}) {
354
- const payload = { content: chunk };
355
- if (options.files?.length) payload.files = options.files;
356
- if (options.replyToMessageId) {
357
- payload.reply = { messageReference: options.replyToMessageId };
358
- }
359
- try {
360
- return await sendWithBackoff(channel, payload);
361
- } catch (error) {
362
- if (!options.replyToMessageId) {
363
- throw error;
928
+ async editExistingMessage(embed) {
929
+ if (!this.messageId) {
930
+ await this.sendNewMessage(embed);
931
+ return;
932
+ }
933
+ try {
934
+ const msg = await this.channel.messages.edit(this.messageId, {
935
+ embeds: [embed],
936
+ components: []
937
+ });
938
+ this.messageId = msg.id;
939
+ } catch (error) {
940
+ console.warn(`\u72B6\u6001\u5361\u7F16\u8F91\u5931\u8D25 (${this.messageId}), \u521B\u5EFA\u65B0\u6D88\u606F:`, error);
941
+ await this.sendNewMessage(embed);
364
942
  }
365
- const fallbackPayload = { content: chunk };
366
- if (options.files?.length) fallbackPayload.files = options.files;
367
- return sendWithBackoff(channel, fallbackPayload);
368
943
  }
369
- }
370
- async function deliver(channel, plan) {
371
- pruneStaleSessionStates();
372
- const state = getSessionState(plan.sessionId);
373
- if (plan.mode === "progress_update") {
374
- const targetMessageId = plan.editTargetMessageId ?? state.recentProgressMessageId;
375
- if (targetMessageId && plan.chunks.length === 1 && plan.filesOnFirstChunk.length === 0) {
376
- try {
377
- await channel.messages?.edit(targetMessageId, { content: plan.chunks[0] });
378
- state.recentProgressMessageId = targetMessageId;
379
- return [targetMessageId];
380
- } catch {
944
+ buildEmbed(state, data) {
945
+ const embed = new EmbedBuilder3().setColor(STATE_COLORS[state]).setTitle(`\u{1F916} ${STATE_LABELS[state]}`).addFields(
946
+ { name: "\u8F6E\u6B21", value: `#${data.turn}`, inline: true },
947
+ { name: "\u66F4\u65B0", value: `<t:${Math.floor(data.updatedAt / 1e3)}:R>`, inline: true }
948
+ ).setTimestamp();
949
+ if (data.provider === "codex") {
950
+ const managedLabel = data.remoteHumanControl ? "\u2713 \u53D7\u7BA1\u4F1A\u8BDD" : "\u25CB \u975E\u53D7\u7BA1\u4F1A\u8BDD\uFF08\u4EC5\u72B6\u6001\u76D1\u63A7\uFF09";
951
+ embed.addFields({ name: "\u4F1A\u8BDD\u7C7B\u578B", value: managedLabel, inline: true });
952
+ }
953
+ if (data.phase) {
954
+ const sanitizedPhase = this.sanitizePhase(data.phase);
955
+ if (sanitizedPhase) {
956
+ embed.addFields({ name: "\u9636\u6BB5", value: sanitizedPhase, inline: true });
957
+ }
958
+ }
959
+ if (data.verbose !== void 0) {
960
+ embed.addFields({ name: "\u8F93\u51FA", value: data.verbose ? "\u{1F50A} \u8BE6\u7EC6" : "\u{1F507} \u7CBE\u7B80", inline: true });
961
+ }
962
+ if (data.monitorGoal) {
963
+ embed.addFields({ name: "\u76D1\u63A7\u76EE\u6807", value: truncate(data.monitorGoal, 150) });
964
+ }
965
+ if (data.monitorIteration !== void 0 && data.maxMonitorIterations !== void 0) {
966
+ embed.addFields({ name: "\u8FED\u4EE3", value: `${data.monitorIteration}/${data.maxMonitorIterations}`, inline: true });
967
+ }
968
+ if (data.permissionsSummary) {
969
+ embed.addFields({ name: "\u6743\u9650", value: data.permissionsSummary, inline: false });
970
+ }
971
+ if (data.todoList && data.todoList.length > 0) {
972
+ const rendered = this.renderTodoList(data.todoList);
973
+ if (rendered) {
974
+ const completed = data.todoList.filter((t) => t.completed).length;
975
+ embed.addFields({
976
+ name: `\u5F85\u529E\uFF08${completed}/${data.todoList.length}\uFF09`,
977
+ value: rendered,
978
+ inline: false
979
+ });
381
980
  }
382
981
  }
982
+ if (data.batchApprovalMode) {
983
+ const pending = data.pendingApprovals ?? [];
984
+ const queueLine = pending.length === 0 ? "\u961F\u5217\u4E3A\u7A7A" : pending.slice(0, 5).map((a) => `\u2022 ${truncate(a.toolName, 32)} \u2014 ${truncate(a.detail, 80)}`).join("\n");
985
+ embed.addFields({
986
+ name: `\u6279\u91CF\u5BA1\u6279\uFF08${pending.length} \u5F85\u6279\uFF09`,
987
+ value: queueLine + (pending.length > 5 ? `
988
+ \u2026\u8FD8\u6709 ${pending.length - 5} \u6761` : ""),
989
+ inline: false
990
+ });
991
+ }
992
+ if (data.recentPermissionDenials && data.recentPermissionDenials.length > 0) {
993
+ const lines = data.recentPermissionDenials.slice(0, 3).map((d) => `\u26D4 ${truncate(d.toolName, 28)} \u2014 ${truncate(d.reason, 80)}`).join("\n");
994
+ embed.addFields({ name: "\u6700\u8FD1\u62D2\u7EDD", value: lines, inline: false });
995
+ }
996
+ return embed;
383
997
  }
384
- const ids = [];
385
- const replyToMode = plan.replyToMode ?? "first";
386
- for (let index = 0; index < plan.chunks.length; index++) {
387
- const shouldReply = !!plan.replyToMessageId && replyToMode !== "off" && (replyToMode === "all" || index === 0);
388
- const id = await sendChunk(channel, plan.chunks[index], {
389
- replyToMessageId: shouldReply ? plan.replyToMessageId : void 0,
390
- files: index === 0 ? plan.filesOnFirstChunk : void 0
391
- });
392
- ids.push(id);
998
+ renderTodoList(items) {
999
+ const TOTAL_BUDGET = 950;
1000
+ const MAX_LINES = 8;
1001
+ const MAX_CHAR_PER_LINE = 180;
1002
+ const lines = [];
1003
+ let used = 0;
1004
+ let renderedCount = 0;
1005
+ for (const item of items.slice(0, MAX_LINES)) {
1006
+ const mark = item.completed ? "\u2611" : "\u2610";
1007
+ const line = `${mark} ${truncate(item.text, MAX_CHAR_PER_LINE)}`;
1008
+ const cost = (lines.length > 0 ? 1 : 0) + line.length;
1009
+ if (used + cost > TOTAL_BUDGET) break;
1010
+ lines.push(line);
1011
+ used += cost;
1012
+ renderedCount++;
1013
+ }
1014
+ const hidden = items.length - renderedCount;
1015
+ if (hidden > 0) {
1016
+ const more = `\u2026 \u8FD8\u6709 ${hidden} \u9879`;
1017
+ const cost = (lines.length > 0 ? 1 : 0) + more.length;
1018
+ if (used + cost <= TOTAL_BUDGET) lines.push(more);
1019
+ }
1020
+ return lines.join("\n");
393
1021
  }
394
- if (ids.length > 0) {
395
- if (plan.mode === "progress_update") {
396
- state.recentProgressMessageId = ids[ids.length - 1];
397
- } else {
398
- state.recentFinalMessageId = ids[ids.length - 1];
1022
+ /**
1023
+ * Sanitize phase text for status card display.
1024
+ * Returns cleaned text or empty string if unsuitable.
1025
+ */
1026
+ sanitizePhase(description) {
1027
+ let normalized = description.trim();
1028
+ if (!normalized) return "";
1029
+ if (normalized.includes("```") || /diff --git/.test(normalized) || this.isLikelyFileList(normalized)) {
1030
+ console.warn(`[StatusCard] Phase contains unsuitable content, stripping: ${normalized.slice(0, 60)}...`);
1031
+ return "";
1032
+ }
1033
+ if (normalized.length > 200) {
1034
+ normalized = normalized.slice(0, 197) + "...";
399
1035
  }
1036
+ return normalized;
400
1037
  }
401
- return ids;
402
- }
1038
+ validate(description) {
1039
+ if (!description) return;
1040
+ const normalized = description.trim();
1041
+ if (!normalized) return;
1042
+ if (normalized.length > 200) {
1043
+ throw new Error("\u72B6\u6001\u5361\u63CF\u8FF0\u8FC7\u957F\uFF0C\u5E94\u79FB\u81F3\u6458\u8981\u5361\u6216\u7ED3\u679C\u6D88\u606F");
1044
+ }
1045
+ if (normalized.includes("```")) {
1046
+ throw new Error("\u72B6\u6001\u5361\u4E0D\u5E94\u5305\u542B\u4EE3\u7801\u5757");
1047
+ }
1048
+ if (/diff --git/.test(normalized)) {
1049
+ throw new Error("\u72B6\u6001\u5361\u4E0D\u5E94\u5305\u542B diff");
1050
+ }
1051
+ if (this.isLikelyFileList(normalized)) {
1052
+ throw new Error("\u72B6\u6001\u5361\u4E0D\u5E94\u5305\u542B\u6587\u4EF6\u5217\u8868");
1053
+ }
1054
+ }
1055
+ isLikelyFileList(text) {
1056
+ const lines = text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
1057
+ if (lines.length < 2) return false;
1058
+ return lines.every((line) => /^[-+*]\s+[\w./\\-]+$/.test(line));
1059
+ }
1060
+ };
1061
+
1062
+ // ../bot/src/discord/summary-handler.ts
1063
+ import "discord.js";
403
1064
 
404
1065
  // ../bot/src/discord/digest-delivery.ts
405
- import { EmbedBuilder as EmbedBuilder2 } from "discord.js";
1066
+ import { EmbedBuilder as EmbedBuilder4 } from "discord.js";
406
1067
  var DigestDelivery = class {
407
1068
  channel;
408
1069
  messageIds = [];
@@ -461,7 +1122,7 @@ var DigestDelivery = class {
461
1122
  return { oldMessageIds, newMessageIds };
462
1123
  }
463
1124
  buildDigestEmbed(content, index, total) {
464
- const embed = new EmbedBuilder2().setColor(3447003).setDescription(content).setTimestamp();
1125
+ const embed = new EmbedBuilder4().setColor(3447003).setDescription(content).setTimestamp();
465
1126
  if (index === 0) embed.setTitle("\u{1F4CC} \u6700\u8FD1\u6458\u8981");
466
1127
  if (total > 1) {
467
1128
  embed.setFooter({ text: `\u7B2C ${index + 1}/${total} \u90E8\u5206` });
@@ -543,10 +1204,10 @@ var SummaryHandler = class {
543
1204
 
544
1205
  // ../bot/src/discord/interaction-card.ts
545
1206
  import {
546
- EmbedBuilder as EmbedBuilder3,
547
- ActionRowBuilder,
548
- ButtonBuilder,
549
- ButtonStyle
1207
+ EmbedBuilder as EmbedBuilder5,
1208
+ ActionRowBuilder as ActionRowBuilder2,
1209
+ ButtonBuilder as ButtonBuilder2,
1210
+ ButtonStyle as ButtonStyle2
550
1211
  } from "discord.js";
551
1212
  var InteractionCard = class {
552
1213
  channel;
@@ -556,16 +1217,16 @@ var InteractionCard = class {
556
1217
  }
557
1218
  async show(sessionId, turn, detail, options = {}) {
558
1219
  const customIdBase = `awaiting_human:${sessionId}:${turn}`;
559
- const embed = new EmbedBuilder3().setTitle("\u23F8\uFE0F \u7B49\u5F85\u4EBA\u5DE5\u5904\u7406").setDescription(detail).setColor(16755200).setTimestamp();
1220
+ const embed = new EmbedBuilder5().setTitle("\u23F8\uFE0F \u7B49\u5F85\u4EBA\u5DE5\u5904\u7406").setDescription(detail).setColor(16755200).setTimestamp();
560
1221
  embed.addFields(
561
1222
  { name: "\u8F6E\u6B21", value: `#${turn}`, inline: true },
562
1223
  { name: "\u7B49\u5F85\u81EA", value: `<t:${Math.floor(Date.now() / 1e3)}:R>`, inline: true }
563
1224
  );
564
1225
  let components = [];
565
1226
  if (options.remoteHumanControl !== false) {
566
- const row = new ActionRowBuilder().addComponents(
567
- new ButtonBuilder().setCustomId(`${customIdBase}:approve`).setLabel("\u5141\u8BB8\u7EE7\u7EED").setStyle(ButtonStyle.Success),
568
- new ButtonBuilder().setCustomId(`${customIdBase}:deny`).setLabel("\u62D2\u7EDD").setStyle(ButtonStyle.Danger)
1227
+ const row = new ActionRowBuilder2().addComponents(
1228
+ new ButtonBuilder2().setCustomId(`${customIdBase}:approve`).setLabel("\u5141\u8BB8\u7EE7\u7EED").setStyle(ButtonStyle2.Success),
1229
+ new ButtonBuilder2().setCustomId(`${customIdBase}:deny`).setLabel("\u62D2\u7EDD").setStyle(ButtonStyle2.Danger)
569
1230
  );
570
1231
  components = [row];
571
1232
  } else {
@@ -689,97 +1350,6 @@ var SessionPanelComponent = class {
689
1350
  }
690
1351
  };
691
1352
 
692
- // ../engine/src/session-context.ts
693
- var EMPTY_PROJECTION = Object.freeze({
694
- turn: 0,
695
- humanResolved: false,
696
- updatedAt: 0
697
- });
698
- function safeGetSessionController(sessionId) {
699
- try {
700
- const fn = getSessionController;
701
- return typeof fn === "function" ? fn(sessionId) : void 0;
702
- } catch {
703
- return void 0;
704
- }
705
- }
706
- function safeDebouncedSaveSession() {
707
- try {
708
- const fn = debouncedSaveSession;
709
- if (typeof fn === "function") fn();
710
- } catch {
711
- }
712
- }
713
- function safeGetProjection(sessionId) {
714
- try {
715
- const sm = stateMachine;
716
- if (sm && typeof sm.getSnapshot === "function") {
717
- return sm.getSnapshot(sessionId);
718
- }
719
- } catch {
720
- }
721
- return EMPTY_PROJECTION;
722
- }
723
- var SessionSupervisor = class {
724
- contexts = /* @__PURE__ */ new Map();
725
- get(sessionId) {
726
- const live = getSession(sessionId);
727
- const cached = this.contexts.get(sessionId);
728
- if (!live) {
729
- if (cached) this.contexts.delete(sessionId);
730
- return void 0;
731
- }
732
- if (cached) {
733
- this.syncContext(cached);
734
- return cached;
735
- }
736
- const ctx = this.build(live);
737
- this.contexts.set(sessionId, ctx);
738
- return ctx;
739
- }
740
- /** 枚举现有 context。用于 supervisor 层面的扫描(健康检查、空闲回收)。 */
741
- all() {
742
- return Array.from(this.contexts.values());
743
- }
744
- /** 释放某个 session 的 context(endSession 调用时触发)。 */
745
- release(sessionId) {
746
- this.contexts.delete(sessionId);
747
- }
748
- releaseAll() {
749
- this.contexts.clear();
750
- }
751
- build(session) {
752
- const ctx = {
753
- sessionId: session.id,
754
- session,
755
- controller: safeGetSessionController(session.id),
756
- projection: safeGetProjection(session.id),
757
- save: () => safeDebouncedSaveSession(),
758
- refresh: () => this.syncContext(ctx)
759
- };
760
- return ctx;
761
- }
762
- syncContext(ctx) {
763
- const live = getSession(ctx.sessionId);
764
- if (live) {
765
- ctx.session = live;
766
- }
767
- ctx.controller = safeGetSessionController(
768
- ctx.sessionId
769
- );
770
- ctx.projection = safeGetProjection(
771
- ctx.sessionId
772
- );
773
- }
774
- };
775
- var sessionSupervisor = new SessionSupervisor();
776
- function getSessionContext(sessionId) {
777
- return sessionSupervisor.get(sessionId);
778
- }
779
- function getSessionView(sessionId) {
780
- return sessionSupervisor.get(sessionId)?.session;
781
- }
782
-
783
1353
  // ../bot/src/monitoring/performance-tracker.ts
784
1354
  import { performance } from "perf_hooks";
785
1355
  import { memoryUsage, cpuUsage } from "process";
@@ -1018,26 +1588,6 @@ async function relocateSessionPanelToBottom(sessionId, channel, initialize) {
1018
1588
  }
1019
1589
  }
1020
1590
 
1021
- // ../engine/src/output/answer-store.ts
1022
- var pendingAnswersStore = /* @__PURE__ */ new Map();
1023
- var questionCountStore = /* @__PURE__ */ new Map();
1024
- function setPendingAnswer(sessionId, questionIndex, answer) {
1025
- if (!pendingAnswersStore.has(sessionId)) {
1026
- pendingAnswersStore.set(sessionId, /* @__PURE__ */ new Map());
1027
- }
1028
- pendingAnswersStore.get(sessionId).set(questionIndex, answer);
1029
- }
1030
- function getPendingAnswers(sessionId) {
1031
- return pendingAnswersStore.get(sessionId);
1032
- }
1033
- function clearPendingAnswers(sessionId) {
1034
- pendingAnswersStore.delete(sessionId);
1035
- questionCountStore.delete(sessionId);
1036
- }
1037
- function getQuestionCount(sessionId) {
1038
- return questionCountStore.get(sessionId) || 0;
1039
- }
1040
-
1041
1591
  // ../bot/src/bot-services-helpers.ts
1042
1592
  var unmanagedCodexHintedSessions = /* @__PURE__ */ new Set();
1043
1593
  function clearCodexHint(sessionId) {
@@ -1293,7 +1843,8 @@ async function updateSessionState(sessionId, event, options = {}) {
1293
1843
  const projection = stateMachine.applyPlatformEvent(platformEvent);
1294
1844
  cacheProjection(sessionId, projection);
1295
1845
  const stateChanged = projection.state !== previousProjection.state || projection.turn !== previousProjection.turn || projection.phase !== previousProjection.phase;
1296
- if (stateChanged) {
1846
+ const contextChanged = projection.todoList !== previousProjection.todoList || projection.recentPermissionDenials !== previousProjection.recentPermissionDenials || projection.batchApprovalMode !== previousProjection.batchApprovalMode || projection.pendingApprovals !== previousProjection.pendingApprovals;
1847
+ if (stateChanged || contextChanged) {
1297
1848
  await scheduleProjectionRender(sessionId, projection, updateKey);
1298
1849
  } else {
1299
1850
  performanceTracker.endStateUpdate(updateKey, { skipped: true });
@@ -1321,8 +1872,11 @@ async function handleResultEvent(sessionId, event, textContent, attachments = []
1321
1872
  } else if (!event.success) {
1322
1873
  const session = getSessionView(sessionId);
1323
1874
  const failureText = textContent.trim() || event.errors.join("\n").trim() || "\u4EFB\u52A1\u5931\u8D25";
1875
+ const reasonLabel = event.terminalReason && event.terminalReason !== "completed" ? formatTerminalReason(event.terminalReason, false) : null;
1876
+ const withReason = reasonLabel ? `\uFF08${reasonLabel}\uFF09
1877
+ ${failureText}` : failureText;
1324
1878
  await panel.summaryHandler.sendTurnFailure(
1325
- failureText,
1879
+ withReason,
1326
1880
  projection.turn,
1327
1881
  session?.lastInboundMessageId,
1328
1882
  attachments
@@ -1443,6 +1997,13 @@ export {
1443
1997
  sendTyping,
1444
1998
  sendAckReaction,
1445
1999
  deliver,
2000
+ sendSystemNotice,
2001
+ spawnSubagent,
2002
+ getSubagents,
2003
+ runSubagentWatchdog,
2004
+ resolveEffectiveClaudePermissionMode,
2005
+ makeModeButtons,
2006
+ dispatchEvent,
1446
2007
  cleanupOldMessages,
1447
2008
  notifyUnmanagedCodexHint,
1448
2009
  stopPerformanceMonitoring,