workspacecord 1.1.2 → 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 (26) hide show
  1. package/dist/{archive-manager-XM3UPOY2.js → archive-manager-FJU7YEEH.js} +4 -4
  2. package/dist/{attachment-cli-IGQBB6YJ.js → attachment-cli-AT4HXAGU.js} +2 -2
  3. package/dist/{chunk-54DP53ZK.js → chunk-5EMN2IL5.js} +29 -3
  4. package/dist/{chunk-L2ZJXV6H.js → chunk-66B64WL3.js} +1 -1
  5. package/dist/{chunk-QWPKAUSV.js → chunk-C4L34VJK.js} +8287 -7421
  6. package/dist/chunk-JR4B4L7I.js +5301 -0
  7. package/dist/{chunk-UEX7U2KW.js → chunk-MJ5JKFGS.js} +911 -232
  8. package/dist/{chunk-ZAQV2RZS.js → chunk-NNTMVOTM.js} +1054 -1310
  9. package/dist/{chunk-COXPTYH5.js → chunk-PWMEOBXG.js} +4 -4
  10. package/dist/{chunk-I6EOCCQV.js → chunk-SNPFYUQ3.js} +713 -628
  11. package/dist/{chunk-GMYN4SYT.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-VDQ5VZPT.js → codex-launcher-CLGG4CVY.js} +1 -1
  15. package/dist/{codex-provider-NYI7KBGO.js → codex-provider-VLOS5QB6.js} +18 -4
  16. package/dist/{config-cli-RQR2ZRQ5.js → config-cli-7G5YWKJU.js} +2 -2
  17. package/dist/{panel-adapter-QTDL3S6O.js → panel-adapter-CLI4WDII.js} +4 -4
  18. package/dist/{project-cli-6P6ZWDR6.js → project-cli-ALKDLRMZ.js} +2 -2
  19. package/dist/{project-registry-OEVPECMS.js → project-registry-ZO3KSS25.js} +2 -2
  20. package/dist/sdk-C3D6X4EB.js +55 -0
  21. package/dist/{session-local-registration-RIO5EPZ5.js → session-local-registration-RL2A3QZB.js} +3 -3
  22. package/dist/{setup-KOS7SRSL.js → setup-GP3MML2R.js} +1 -1
  23. package/package.json +3 -3
  24. package/dist/chunk-RK6EIZOL.js +0 -589
  25. package/dist/cli-framework-SQM2465A.js +0 -18
  26. package/dist/sdk-V7A7IF7F.js +0 -43
@@ -1,26 +1,40 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  cleanupSessionAttachments
4
- } from "./chunk-L2ZJXV6H.js";
4
+ } from "./chunk-66B64WL3.js";
5
5
  import {
6
+ STATE_COLORS,
7
+ STATE_LABELS,
8
+ createSession,
9
+ debouncedSaveSession,
10
+ endSession,
11
+ gateService,
6
12
  getAllSessions,
7
13
  getSession,
14
+ getSessionController,
8
15
  getSessionPermissionSummary,
16
+ mapPlatformEventToState,
9
17
  setCurrentInteractionMessage,
10
18
  setStatusCardBinding,
19
+ stateMachine,
20
+ toPlatformEvent,
11
21
  updateSession
12
- } from "./chunk-RK6EIZOL.js";
22
+ } from "./chunk-JR4B4L7I.js";
13
23
  import {
14
- Store,
15
24
  config,
16
25
  truncate
17
- } from "./chunk-UEX7U2KW.js";
26
+ } from "./chunk-MJ5JKFGS.js";
18
27
 
19
28
  // ../bot/src/discord/status-card-projection-renderer.ts
20
29
  var StatusCardProjectionRenderer = class {
21
30
  #pending = /* @__PURE__ */ new Map();
22
31
  async renderNow(sessionId, projection, context) {
23
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
+ }
24
38
  try {
25
39
  await context.statusCard.update(projection.state, {
26
40
  turn: projection.turn,
@@ -30,7 +44,11 @@ var StatusCardProjectionRenderer = class {
30
44
  provider: context.provider,
31
45
  permissionsSummary: context.permissionsSummary,
32
46
  verbose: context.verbose,
33
- monitorGoal: context.monitorGoal
47
+ monitorGoal: context.monitorGoal,
48
+ todoList: projection.todoList,
49
+ recentPermissionDenials: projection.recentPermissionDenials,
50
+ batchApprovalMode: projection.batchApprovalMode,
51
+ pendingApprovals: projection.pendingApprovals
34
52
  });
35
53
  } catch (error) {
36
54
  console.error(`\u72B6\u6001\u5361\u66F4\u65B0\u5931\u8D25 (${sessionId}):`, error);
@@ -66,1005 +84,788 @@ var StatusCardProjectionRenderer = class {
66
84
  }
67
85
  };
68
86
 
69
- // ../bot/src/discord/status-card.ts
70
- import {
71
- EmbedBuilder
72
- } from "discord.js";
73
-
74
- // ../state/src/types.ts
75
- var STATE_PRIORITY = {
76
- error: 9,
77
- awaiting_human: 8,
78
- stalled: 7,
79
- summarizing: 6,
80
- working: 5,
81
- thinking: 4,
82
- completed: 3,
83
- idle: 2,
84
- offline: 1
85
- };
86
- var STATE_LABELS = {
87
- idle: "\u5F85\u547D",
88
- thinking: "\u6B63\u5728\u601D\u8003",
89
- working: "\u6B63\u5728\u6267\u884C",
90
- awaiting_human: "\u7B49\u5F85\u4EBA\u5DE5\u5904\u7406",
91
- summarizing: "\u6B63\u5728\u6574\u7406\u4E0A\u4E0B\u6587",
92
- completed: "\u672C\u8F6E\u5DF2\u5B8C\u6210",
93
- error: "\u51FA\u73B0\u5F02\u5E38",
94
- stalled: "\u7591\u4F3C\u5361\u4F4F",
95
- offline: "\u5DF2\u79BB\u7EBF"
96
- };
97
- var STATE_COLORS = {
98
- idle: 8421504,
99
- // 灰色
100
- thinking: 3447003,
101
- // 蓝色
102
- working: 3066993,
103
- // 绿色
104
- awaiting_human: 15965202,
105
- // 橙色
106
- summarizing: 10181046,
107
- // 紫色
108
- completed: 2600544,
109
- // 深绿
110
- error: 15158332,
111
- // 红色
112
- stalled: 15105570,
113
- // 深橙
114
- offline: 9807270
115
- // 浅灰
116
- };
117
- var PLATFORM_EVENT_TO_STATE = {
118
- session_started: "idle",
119
- session_idle: "idle",
120
- thinking_started: "thinking",
121
- work_started: "working",
122
- awaiting_human: "awaiting_human",
123
- human_resolved: "working",
124
- compaction_started: "summarizing",
125
- completed: "completed",
126
- errored: "error",
127
- stalled: "stalled",
128
- session_ended: "offline"
129
- };
130
-
131
- // ../state/src/state-projections.ts
132
- function toProjection(state) {
133
- return {
134
- state: state.displayState,
135
- stateSource: state.stateSource,
136
- confidence: state.confidence,
137
- updatedAt: state.updatedAt,
138
- turn: state.turn,
139
- phase: state.phase,
140
- humanResolved: state.humanResolved
141
- };
142
- }
143
- function toPanelProjection(state) {
144
- return {
145
- ...toProjection(state),
146
- isWaitingHuman: state.lifecycle === "waiting_human" || state.gate === "pending" || state.displayState === "awaiting_human",
147
- isCompleted: state.displayState === "completed" || state.lifecycle === "completed",
148
- isError: state.displayState === "error" || state.lifecycle === "error",
149
- isStalled: state.displayState === "stalled"
150
- };
151
- }
152
- function resolveDisplayState(sessions) {
153
- let best = "idle";
154
- let bestPri = 0;
155
- for (const state of sessions) {
156
- const pri = STATE_PRIORITY[state.displayState] || 0;
157
- if (pri > bestPri) {
158
- best = state.displayState;
159
- bestPri = pri;
160
- }
161
- }
162
- return best;
163
- }
164
- function getStateLabel(state) {
165
- return STATE_LABELS[state] || state;
166
- }
167
- function getStateColor(state) {
168
- return STATE_COLORS[state] || 8421504;
169
- }
87
+ // ../bot/src/output/event-handlers.ts
88
+ import { existsSync as existsSync2 } from "fs";
170
89
 
171
- // ../state/src/state-event-mapper.ts
172
- function mapEventToTransition(event, currentState, context) {
173
- const mappedState = PLATFORM_EVENT_TO_STATE[event.type];
174
- if (!mappedState) return null;
175
- if (event.type === "session_idle" && !context.isSessionIdleTransitionAllowed(event, currentState)) {
176
- return null;
177
- }
178
- const allowTransition = event.type === "human_resolved" || event.type === "completed" || event.type === "session_ended" || context.shouldTransition(
179
- currentState.displayState,
180
- mappedState,
181
- currentState.stateSource,
182
- event.stateSource ?? "formal"
183
- );
184
- if (!allowTransition) {
185
- return null;
186
- }
187
- const lifecycle = mapLifecycle(event, currentState, mappedState);
188
- const execution = mapExecution(event, currentState, mappedState, lifecycle);
189
- const gate = mapGate(event, currentState);
190
- const turn = resolveTurn(event, currentState);
191
- const humanResolved = resolveHumanResolved(event, currentState);
192
- const phaseLabel = event.metadata?.phase ?? getStateLabel(mappedState);
193
- return {
194
- updates: {
195
- lifecycle,
196
- execution,
197
- gate
198
- },
199
- metadata: {
200
- displayState: mappedState,
201
- stateSource: event.stateSource ?? "formal",
202
- confidence: event.confidence,
203
- updatedAt: event.timestamp,
204
- turn,
205
- phase: phaseLabel,
206
- humanResolved
207
- }
208
- };
209
- }
210
- function mapLifecycle(event, current, mappedState) {
211
- switch (event.type) {
212
- case "session_started":
213
- case "session_idle":
214
- case "thinking_started":
215
- case "work_started":
216
- case "human_resolved":
217
- case "compaction_started":
218
- return "active";
219
- case "awaiting_human":
220
- return "waiting_human";
221
- case "completed":
222
- return "completed";
223
- case "errored":
224
- return "error";
225
- case "stalled":
226
- return current.lifecycle === "active" ? "paused" : current.lifecycle;
227
- case "session_ended":
228
- return current.lifecycle === "initializing" ? "initializing" : "paused";
229
- default:
230
- return mapLifecycleFromState(mappedState, current.lifecycle);
231
- }
232
- }
233
- function mapLifecycleFromState(mappedState, fallback) {
234
- switch (mappedState) {
235
- case "idle":
236
- case "thinking":
237
- case "working":
238
- case "summarizing":
239
- return "active";
240
- case "awaiting_human":
241
- return "waiting_human";
242
- case "completed":
243
- return "completed";
244
- case "error":
245
- return "error";
246
- case "stalled":
247
- return "paused";
248
- case "offline":
249
- return fallback === "initializing" ? "initializing" : "paused";
250
- default:
251
- return fallback;
252
- }
253
- }
254
- function mapExecution(event, current, mappedState, lifecycle) {
255
- if (lifecycle !== "active") return null;
256
- switch (event.type) {
257
- case "session_started":
258
- case "session_idle":
259
- return "idle";
260
- case "thinking_started":
261
- return "thinking";
262
- case "work_started":
263
- return "tool_executing";
264
- case "human_resolved":
265
- return "tool_executing";
266
- case "compaction_started":
267
- return "thinking";
268
- default:
269
- return mapExecutionFromState(mappedState, current.execution);
270
- }
271
- }
272
- function mapExecutionFromState(mappedState, fallback) {
273
- switch (mappedState) {
274
- case "idle":
275
- return "idle";
276
- case "thinking":
277
- case "summarizing":
278
- return "thinking";
279
- case "working":
280
- return "tool_executing";
281
- default:
282
- return fallback;
283
- }
284
- }
285
- function mapGate(event, current) {
286
- switch (event.type) {
287
- case "awaiting_human":
288
- return "pending";
289
- case "human_resolved":
290
- return event.metadata?.action === "reject" ? "rejected" : "approved";
291
- case "session_idle":
292
- if (event.metadata?.action === "reject" && current.gate === "pending") {
293
- return "rejected";
294
- }
295
- return current.gate;
296
- case "session_ended":
297
- return current.gate === "pending" ? "invalidated" : current.gate;
298
- default:
299
- return current.gate;
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;
300
102
  }
301
103
  }
302
- function resolveTurn(event, current) {
303
- if (event.type === "session_started" && current.turn <= 0) {
304
- return 1;
104
+ function safeDebouncedSaveSession() {
105
+ try {
106
+ const fn = debouncedSaveSession;
107
+ if (typeof fn === "function") fn();
108
+ } catch {
305
109
  }
306
- return current.turn;
307
110
  }
308
- function resolveHumanResolved(event, current) {
309
- if (event.type === "awaiting_human") return false;
310
- if (event.type === "human_resolved") return true;
311
- if (event.type === "session_idle" && event.metadata?.action === "reject") return true;
312
- return current.humanResolved;
313
- }
314
-
315
- // ../state/src/state-machine.ts
316
- var LIFECYCLE_TRANSITIONS = {
317
- initializing: ["active", "waiting_human", "paused", "completed", "error"],
318
- active: ["waiting_human", "paused", "completed", "error"],
319
- waiting_human: ["active", "paused", "error"],
320
- paused: ["active", "completed", "error"],
321
- completed: ["active"],
322
- error: ["active", "completed"]
323
- };
324
- var EXECUTION_TRANSITIONS = {
325
- idle: ["thinking", "tool_executing"],
326
- thinking: ["tool_executing", "streaming_output", "idle"],
327
- tool_executing: ["thinking", "streaming_output", "idle"],
328
- streaming_output: ["idle", "thinking"]
329
- };
330
- var StateMachine = class {
331
- sessions = /* @__PURE__ */ new Map();
332
- transitionHistory = /* @__PURE__ */ new Map();
333
- completedTimers = /* @__PURE__ */ new Map();
334
- completedTimerTokens = /* @__PURE__ */ new Map();
335
- completedTimerSequence = 0;
336
- getState(sessionId) {
337
- const existing = this.sessions.get(sessionId);
338
- if (existing) return existing;
339
- const defaultState = this.createDefaultState();
340
- this.sessions.set(sessionId, defaultState);
341
- return defaultState;
342
- }
343
- getSnapshot(sessionId) {
344
- return toProjection(this.getState(sessionId));
345
- }
346
- getPanelProjection(sessionId) {
347
- return toPanelProjection(this.getState(sessionId));
348
- }
349
- transition(sessionId, event, updates, metadata = {}) {
350
- const current = this.getState(sessionId);
351
- const timestamp = metadata.updatedAt ?? Date.now();
352
- const target = {
353
- ...current,
354
- lifecycle: updates.lifecycle ?? current.lifecycle,
355
- execution: updates.execution !== void 0 ? updates.execution : current.execution,
356
- gate: updates.gate !== void 0 ? updates.gate : current.gate,
357
- displayState: metadata.displayState ?? current.displayState,
358
- stateSource: metadata.stateSource ?? current.stateSource,
359
- confidence: metadata.confidence ?? current.confidence,
360
- updatedAt: timestamp,
361
- turn: metadata.turn ?? current.turn,
362
- phase: metadata.phase ?? current.phase,
363
- humanResolved: metadata.humanResolved ?? current.humanResolved
364
- };
365
- if (updates.lifecycle && updates.lifecycle !== current.lifecycle) {
366
- const allowed = LIFECYCLE_TRANSITIONS[current.lifecycle];
367
- if (!allowed.includes(updates.lifecycle)) {
368
- return {
369
- success: false,
370
- state: current,
371
- error: `\u975E\u6CD5\u751F\u547D\u5468\u671F\u8F6C\u6362: ${current.lifecycle} -> ${updates.lifecycle}`
372
- };
373
- }
374
- }
375
- if (updates.execution !== void 0 && updates.execution !== current.execution) {
376
- if (target.lifecycle !== "active" && updates.execution !== null) {
377
- return {
378
- success: false,
379
- state: current,
380
- error: `\u6267\u884C\u72B6\u6001\u4EC5\u5728 lifecycle=active \u65F6\u6709\u6548\uFF0C\u5F53\u524D lifecycle=${target.lifecycle}`
381
- };
382
- }
383
- if (current.execution && updates.execution) {
384
- const allowed = EXECUTION_TRANSITIONS[current.execution];
385
- if (!allowed.includes(updates.execution)) {
386
- return {
387
- success: false,
388
- state: current,
389
- error: `\u975E\u6CD5\u6267\u884C\u72B6\u6001\u8F6C\u6362: ${current.execution} -> ${updates.execution}`
390
- };
391
- }
392
- }
393
- }
394
- if (target.lifecycle !== "active" && target.execution !== null) {
395
- target.execution = null;
396
- }
397
- if (this.isSameState(current, target)) {
398
- return { success: true, state: current };
111
+ function safeGetProjection(sessionId) {
112
+ try {
113
+ const sm = stateMachine;
114
+ if (sm && typeof sm.getSnapshot === "function") {
115
+ return sm.getSnapshot(sessionId);
399
116
  }
400
- this.sessions.set(sessionId, target);
401
- const transition = {
402
- from: { ...current },
403
- to: { ...target },
404
- event,
405
- timestamp,
406
- sessionId
407
- };
408
- const history = this.transitionHistory.get(sessionId) || [];
409
- history.push(transition);
410
- if (history.length > 100) {
411
- history.shift();
412
- }
413
- this.transitionHistory.set(sessionId, history);
414
- console.log(
415
- `[state-machine] ${sessionId} | ${event} | lifecycle: ${current.lifecycle} -> ${target.lifecycle} | execution: ${current.execution} -> ${target.execution} | gate: ${current.gate} -> ${target.gate} | display: ${current.displayState} -> ${target.displayState}`
416
- );
417
- return { success: true, state: target };
418
- }
419
- getTransitionHistory(sessionId) {
420
- return this.transitionHistory.get(sessionId) || [];
117
+ } catch {
421
118
  }
422
- resolveDisplayState() {
423
- return resolveDisplayState(this.sessions.values());
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;
129
+ }
130
+ if (cached) {
131
+ this.syncContext(cached);
132
+ return cached;
133
+ }
134
+ const ctx = this.build(live);
135
+ this.contexts.set(sessionId, ctx);
136
+ return ctx;
137
+ }
138
+ /** 枚举现有 context。用于 supervisor 层面的扫描(健康检查、空闲回收)。 */
139
+ all() {
140
+ return Array.from(this.contexts.values());
141
+ }
142
+ /** 释放某个 session 的 context(endSession 调用时触发)。 */
143
+ release(sessionId) {
144
+ this.contexts.delete(sessionId);
145
+ }
146
+ releaseAll() {
147
+ this.contexts.clear();
148
+ }
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;
424
159
  }
425
- shouldTransition(from, to, fromSource = "formal", toSource = "formal") {
426
- const fromPri = STATE_PRIORITY[from] || 0;
427
- const toPri = STATE_PRIORITY[to] || 0;
428
- if (fromSource === "formal" && toSource === "inferred") {
429
- return toPri > fromPri;
160
+ syncContext(ctx) {
161
+ const live = getSession(ctx.sessionId);
162
+ if (live) {
163
+ ctx.session = live;
430
164
  }
431
- return toPri >= fromPri;
432
- }
433
- getStateLabel(state) {
434
- return getStateLabel(state);
435
- }
436
- getStateColor(state) {
437
- return getStateColor(state);
438
- }
439
- setTurn(sessionId, turn, event = "turn_set") {
440
- const result = this.transition(sessionId, event, {}, { turn, humanResolved: false });
441
- return toProjection(result.state);
442
- }
443
- incrementTurn(sessionId) {
444
- const current = this.getState(sessionId);
445
- const result = this.transition(sessionId, "turn_incremented", {}, {
446
- turn: current.turn + 1,
447
- humanResolved: false
448
- });
449
- return toProjection(result.state);
450
- }
451
- advanceTurnToIdle(sessionId) {
452
- const current = this.getState(sessionId);
453
- const incremented = this.transition(sessionId, "turn_incremented", {}, {
454
- turn: current.turn + 1,
455
- humanResolved: false
456
- });
457
- const baseState = incremented.success ? incremented.state : current;
458
- const settled = this.transition(
459
- sessionId,
460
- "turn_completed",
461
- { lifecycle: "active", execution: "idle", gate: null },
462
- {
463
- displayState: "idle",
464
- stateSource: "formal",
465
- confidence: "high",
466
- phase: getStateLabel("idle"),
467
- humanResolved: false,
468
- turn: baseState.turn
469
- }
165
+ ctx.controller = safeGetSessionController(
166
+ ctx.sessionId
470
167
  );
471
- return toProjection(settled.success ? settled.state : baseState);
472
- }
473
- applyPlatformEvent(event) {
474
- const current = this.getState(event.sessionId);
475
- const mapped = mapEventToTransition(event, current, {
476
- shouldTransition: this.shouldTransition.bind(this),
477
- isSessionIdleTransitionAllowed: this.isSessionIdleTransitionAllowed.bind(this)
478
- });
479
- if (!mapped) return toProjection(current);
480
- const mappedState = PLATFORM_EVENT_TO_STATE[event.type];
481
- const shouldResetCompletedTimer = event.type === "session_started" || event.type === "session_ended" || event.type !== "completed" && mappedState !== "completed" && current.displayState === "completed";
482
- if (shouldResetCompletedTimer) {
483
- this.clearCompletedTimer(event.sessionId);
484
- }
485
- const result = this.transition(
486
- event.sessionId,
487
- event.type,
488
- mapped.updates,
489
- mapped.metadata
168
+ ctx.projection = safeGetProjection(
169
+ ctx.sessionId
490
170
  );
491
- if (!result.success) {
492
- return toProjection(current);
493
- }
494
- if (mappedState === "completed") {
495
- this.clearCompletedTimer(event.sessionId);
496
- const timerToken = ++this.completedTimerSequence;
497
- const completedTurn = result.state.turn;
498
- const timer = setTimeout(() => {
499
- this.applyPlatformEvent({
500
- type: "session_idle",
501
- sessionId: event.sessionId,
502
- source: event.source,
503
- stateSource: "formal",
504
- confidence: "high",
505
- timestamp: Date.now(),
506
- metadata: {
507
- phase: "\u5F85\u547D",
508
- idleTimerToken: timerToken,
509
- turn: completedTurn
510
- }
511
- });
512
- this.clearCompletedTimer(event.sessionId);
513
- }, 3e3);
514
- this.completedTimers.set(event.sessionId, timer);
515
- this.completedTimerTokens.set(event.sessionId, timerToken);
516
- }
517
- return toProjection(result.state);
518
- }
519
- clearSession(sessionId) {
520
- this.sessions.delete(sessionId);
521
- this.transitionHistory.delete(sessionId);
522
- this.clearCompletedTimer(sessionId);
523
- }
524
- getSessionCount() {
525
- return this.sessions.size;
526
- }
527
- createDefaultState() {
528
- return {
529
- lifecycle: "initializing",
530
- execution: null,
531
- gate: null,
532
- displayState: "idle",
533
- stateSource: "formal",
534
- confidence: "high",
535
- updatedAt: Date.now(),
536
- turn: 0,
537
- phase: STATE_LABELS.idle,
538
- humanResolved: false
539
- };
540
171
  }
541
- isSameState(a, b) {
542
- return a.lifecycle === b.lifecycle && a.execution === b.execution && a.gate === b.gate && a.displayState === b.displayState && a.stateSource === b.stateSource && a.confidence === b.confidence && a.updatedAt === b.updatedAt && a.turn === b.turn && a.phase === b.phase && a.humanResolved === b.humanResolved;
543
- }
544
- clearCompletedTimer(sessionId) {
545
- const completedTimer = this.completedTimers.get(sessionId);
546
- if (completedTimer) {
547
- clearTimeout(completedTimer);
548
- this.completedTimers.delete(sessionId);
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
+ }
180
+
181
+ // ../bot/src/subagent-manager.ts
182
+ import {
183
+ ChannelType,
184
+ ThreadAutoArchiveDuration
185
+ } from "discord.js";
186
+
187
+ // ../bot/src/discord/delivery-policy.ts
188
+ import { existsSync, statSync } from "fs";
189
+ var MAX_CHUNK_LIMIT = 2e3;
190
+ var MAX_ATTACHMENTS = 10;
191
+ var MAX_ATTACHMENT_BYTES = 25 * 1024 * 1024;
192
+ function clampChunkLimit(limit) {
193
+ if (!Number.isFinite(limit)) return MAX_CHUNK_LIMIT;
194
+ return Math.max(1, Math.min(Math.trunc(limit), MAX_CHUNK_LIMIT));
195
+ }
196
+ function chunkText(text, limit, mode) {
197
+ const safeLimit = clampChunkLimit(limit);
198
+ if (text.length <= safeLimit) return [text];
199
+ const out = [];
200
+ let rest = text;
201
+ while (rest.length > safeLimit) {
202
+ let cut = safeLimit;
203
+ if (mode === "newline") {
204
+ const para = rest.lastIndexOf("\n\n", safeLimit);
205
+ const line = rest.lastIndexOf("\n", safeLimit);
206
+ const space = rest.lastIndexOf(" ", safeLimit);
207
+ cut = para > safeLimit / 2 ? para : line > safeLimit / 2 ? line : space > 0 ? space : safeLimit;
549
208
  }
550
- this.completedTimerTokens.delete(sessionId);
209
+ out.push(rest.slice(0, cut));
210
+ rest = rest.slice(cut).replace(/^\n+/, "");
551
211
  }
552
- isSessionIdleTransitionAllowed(event, current) {
553
- if (current.displayState === "completed") {
554
- return true;
555
- }
556
- const idleTimerToken = this.readNumericMetadata(event.metadata, "idleTimerToken");
557
- if (idleTimerToken === void 0) {
558
- return false;
212
+ if (rest) out.push(rest);
213
+ return out;
214
+ }
215
+ function validateOutboundAttachments(files) {
216
+ if (files.length > MAX_ATTACHMENTS) {
217
+ throw new Error(`\u6700\u591A\u53EA\u80FD\u53D1\u9001 ${MAX_ATTACHMENTS} \u4E2A\u9644\u4EF6`);
218
+ }
219
+ for (const filePath of files) {
220
+ if (!filePath.trim()) {
221
+ throw new Error("\u9644\u4EF6\u8DEF\u5F84\u65E0\u6548");
559
222
  }
560
- const activeTimerToken = this.completedTimerTokens.get(event.sessionId);
561
- if (activeTimerToken !== idleTimerToken) {
562
- return false;
223
+ if (!existsSync(filePath)) continue;
224
+ const stats = statSync(filePath);
225
+ if (stats.isDirectory()) {
226
+ throw new Error(`\u9644\u4EF6\u8DEF\u5F84\u4E0D\u80FD\u662F\u76EE\u5F55: ${filePath}`);
563
227
  }
564
- const timerTurn = this.readNumericMetadata(event.metadata, "turn");
565
- if (timerTurn !== void 0 && timerTurn !== current.turn) {
566
- return false;
228
+ if (stats.size > MAX_ATTACHMENT_BYTES) {
229
+ throw new Error(`\u9644\u4EF6 ${filePath} \u5927\u5C0F\u8D85\u8FC7 ${MAX_ATTACHMENT_BYTES} bytes`);
567
230
  }
568
- return true;
569
231
  }
570
- readNumericMetadata(metadata, field) {
571
- const value = metadata?.[field];
572
- return typeof value === "number" ? value : void 0;
573
- }
574
- };
575
- var stateMachine = new StateMachine();
576
-
577
- // ../state/src/event-normalizer.ts
578
- var CLAUDE_EVENT_MAP = {
579
- session_init: "idle",
580
- text_delta: "thinking",
581
- tool_start: "working",
582
- tool_result: "working",
583
- ask_user: "awaiting_human",
584
- result: "completed",
585
- error: "error"
586
- };
587
- function normalizeClaudeEvent(event, sessionId) {
588
- const state = CLAUDE_EVENT_MAP[event.type];
589
- if (!state) return null;
590
- const platformType = mapToPlatformType(event.type);
591
- if (!platformType) return null;
592
- return {
593
- type: platformType,
594
- sessionId,
595
- source: "claude",
596
- stateSource: "formal",
597
- confidence: "high",
598
- metadata: event,
599
- timestamp: Date.now()
600
- };
601
232
  }
602
- function mapToPlatformType(eventType) {
603
- const mapping = {
604
- session_init: "session_started",
605
- text_delta: "thinking_started",
606
- tool_start: "work_started",
607
- ask_user: "awaiting_human",
608
- result: "completed",
609
- error: "errored"
610
- };
611
- return mapping[eventType] || null;
612
- }
613
- function normalizeCodexEvent(eventKey, sessionId, extra) {
614
- const byEvent = mapCodexToPlatformType(eventKey);
615
- const byState = mapCodexStateToPlatformType(extra.observedState);
616
- const preferStateOverride = extra.observedState === "codex-permission";
617
- const platformType = preferStateOverride ? byState ?? byEvent : byEvent ?? byState;
618
- if (!platformType) return null;
619
- const isPermissionEvent = eventKey === "codex-permission" || extra.observedState === "codex-permission";
620
- const inferredFromState = Boolean(byState && !byEvent);
621
- const stateSource = isPermissionEvent || inferredFromState ? "inferred" : "formal";
233
+ function buildDeliveryPlan(input) {
234
+ validateOutboundAttachments(input.files);
235
+ const chunks = chunkText(input.text, input.policy.textChunkLimit, input.policy.chunkMode);
236
+ const filesOnFirstChunk = input.files.slice(0, MAX_ATTACHMENTS);
237
+ const replyToMode = input.policy.replyToMode;
238
+ const replyToMessageId = input.mode === "system_notice" || input.mode === "summary" ? void 0 : replyToMode === "off" ? void 0 : input.replyToMessageId;
622
239
  return {
623
- type: platformType,
624
- sessionId,
625
- source: "codex",
626
- stateSource,
627
- confidence: isPermissionEvent ? "medium" : "high",
628
- metadata: { eventKey, ...extra },
629
- timestamp: Date.now()
240
+ sessionId: input.sessionId,
241
+ chatId: input.chatId,
242
+ replyToMessageId,
243
+ replyToMode,
244
+ editTargetMessageId: input.editTargetMessageId,
245
+ chunks,
246
+ filesOnFirstChunk,
247
+ mode: input.mode
630
248
  };
631
249
  }
632
- function mapCodexToPlatformType(eventKey) {
633
- const mapping = {
634
- "session_meta": "session_started",
635
- "event_msg:task_started": "thinking_started",
636
- "event_msg:user_message": "thinking_started",
637
- "response_item:function_call": "work_started",
638
- "response_item:custom_tool_call": "work_started",
639
- "response_item:web_search_call": "work_started",
640
- "event_msg:exec_command_start": "work_started",
641
- "codex-permission": "awaiting_human",
642
- "event_msg:task_complete": "completed",
643
- "event_msg:context_compacted": "compaction_started",
644
- "event_msg:turn_aborted": "session_idle",
645
- "codex-turn-end": "completed",
646
- "event_msg:error": "errored",
647
- "stale-cleanup": "session_ended"
648
- };
649
- return mapping[eventKey] || null;
650
- }
651
- function mapCodexStateToPlatformType(state) {
652
- if (!state) return null;
653
- const mapping = {
654
- thinking: "thinking_started",
655
- working: "work_started",
656
- sweeping: "compaction_started",
657
- "codex-permission": "awaiting_human",
658
- attention: "completed",
659
- idle: "session_idle",
660
- error: "errored",
661
- sleeping: "session_ended"
662
- };
663
- return mapping[state] ?? null;
250
+
251
+ // ../bot/src/discord/delivery.ts
252
+ var sessionState = /* @__PURE__ */ new Map();
253
+ function getSessionState(sessionId) {
254
+ if (!sessionState.has(sessionId)) {
255
+ sessionState.set(sessionId, {});
256
+ }
257
+ const state = sessionState.get(sessionId);
258
+ state.lastUsed = Date.now();
259
+ return state;
664
260
  }
665
- function isPlatformEvent(input) {
666
- if (!input || typeof input !== "object") return false;
667
- const obj = input;
668
- if (typeof obj.type !== "string") return false;
669
- if (typeof obj.sessionId !== "string") return false;
670
- if (obj.source !== "claude" && obj.source !== "codex") return false;
671
- if (obj.confidence !== "high" && obj.confidence !== "medium" && obj.confidence !== "low") {
672
- return false;
261
+ function pruneStaleSessionStates(maxAgeMs = 5 * 60 * 1e3) {
262
+ const cutoff = Date.now() - maxAgeMs;
263
+ for (const [sessionId, state] of sessionState) {
264
+ if (!state.lastUsed || state.lastUsed < cutoff) {
265
+ sessionState.delete(sessionId);
266
+ }
673
267
  }
674
- return typeof obj.timestamp === "number";
675
268
  }
676
- function toPlatformEvent(event, sessionId, source = "claude") {
677
- if (isPlatformEvent(event)) {
678
- return {
679
- ...event,
680
- sessionId: event.sessionId || sessionId,
681
- source: event.source || source,
682
- stateSource: event.stateSource ?? "formal",
683
- timestamp: event.timestamp || Date.now()
684
- };
269
+ function cleanupSessionDeliveryState(sessionId) {
270
+ sessionState.delete(sessionId);
271
+ }
272
+ async function sendTyping(channel) {
273
+ try {
274
+ await channel.sendTyping?.();
275
+ } catch {
685
276
  }
686
- if (source === "codex") {
687
- return null;
277
+ }
278
+ async function sendAckReaction(message, reaction) {
279
+ if (!reaction) return;
280
+ try {
281
+ await message.react(reaction);
282
+ } catch {
688
283
  }
689
- return normalizeClaudeEvent(event, sessionId);
690
284
  }
691
- function mapPlatformEventToState(type) {
692
- return PLATFORM_EVENT_TO_STATE[type] ?? null;
285
+ var SEND_MAX_RETRIES = 3;
286
+ function isDiscordRateLimitError(err) {
287
+ if (!err || typeof err !== "object") return false;
288
+ const e = err;
289
+ return e.code === 429 || e.status === 429 || e.httpStatus === 429;
693
290
  }
694
-
695
- // ../state/src/human-gate.ts
696
- var HumanGateRegistry = class {
697
- gates = /* @__PURE__ */ new Map();
698
- store;
699
- constructor() {
700
- this.store = new Store("gates.json");
701
- }
702
- /** Load persisted gates from disk. Call during startup before using the registry. */
703
- async init() {
291
+ function sleep(ms) {
292
+ return new Promise((resolve) => setTimeout(resolve, ms));
293
+ }
294
+ async function sendWithBackoff(channel, payload) {
295
+ let attempt = 0;
296
+ while (true) {
704
297
  try {
705
- const data = await this.store.read();
706
- if (data && Array.isArray(data)) {
707
- for (const record of data) {
708
- this.gates.set(record.id, record);
709
- }
298
+ const message = await channel.send(payload);
299
+ return message.id;
300
+ } catch (error) {
301
+ if (!isDiscordRateLimitError(error) || attempt >= SEND_MAX_RETRIES) {
302
+ throw error;
710
303
  }
711
- } catch (err) {
712
- console.error("[HumanGateRegistry] Failed to load persisted gates:", err);
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++;
713
310
  }
714
311
  }
715
- async _save() {
716
- return this.store.write(Array.from(this.gates.values())).catch((err) => {
717
- console.error("[HumanGateRegistry] Failed to persist gates:", err);
718
- });
719
- }
720
- // 创建新门控
721
- create(params) {
722
- const id = this.generateId();
723
- const record = {
724
- ...params,
725
- id,
726
- version: 1,
727
- createdAt: Date.now(),
728
- status: "pending"
729
- };
730
- this.gates.set(id, record);
731
- this._save();
732
- return record;
733
- }
734
- // 获取门控记录
735
- get(id) {
736
- return this.gates.get(id);
737
- }
738
- // 获取会话的所有门控
739
- getBySession(sessionId) {
740
- return Array.from(this.gates.values()).filter((g) => g.sessionId === sessionId);
741
- }
742
- // 获取会话的活跃门控(pending 状态)
743
- getActiveBySession(sessionId) {
744
- return this.getBySession(sessionId).filter((g) => g.status === "pending");
745
- }
746
- // CAS 更新:使用乐观锁保证原子性
747
- update(id, expectedVersion, updates) {
748
- const current = this.gates.get(id);
749
- if (!current) {
750
- return {
751
- success: false,
752
- error: "not_found",
753
- message: `Gate ${id} not found`
754
- };
755
- }
756
- if (current.version !== expectedVersion) {
757
- return {
758
- success: false,
759
- error: "version_conflict",
760
- message: `Version conflict: expected ${expectedVersion}, got ${current.version}`
761
- };
762
- }
763
- if (updates.status && !this.isValidTransition(current.status, updates.status)) {
764
- return {
765
- success: false,
766
- error: "invalid_transition",
767
- message: `Invalid transition: ${current.status} -> ${updates.status}`
768
- };
769
- }
770
- const updated = {
771
- ...current,
772
- ...updates,
773
- version: current.version + 1
774
- };
775
- this.gates.set(id, updated);
776
- this._save();
777
- return {
778
- success: true,
779
- record: updated
780
- };
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 };
781
318
  }
782
- // 批量失效门控(用于重启后清理)
783
- invalidateAll(reason = "restart") {
784
- let count = 0;
785
- for (const [id, gate] of this.gates.entries()) {
786
- if (gate.status === "pending") {
787
- const updated = {
788
- ...gate,
789
- status: "invalidated",
790
- resolvedAt: Date.now(),
791
- resolvedBy: reason,
792
- version: gate.version + 1
793
- };
794
- this.gates.set(id, updated);
795
- count++;
796
- }
319
+ try {
320
+ return await sendWithBackoff(channel, payload);
321
+ } catch (error) {
322
+ if (!options.replyToMessageId) {
323
+ throw error;
797
324
  }
798
- this._save();
799
- return count;
800
- }
801
- // 删除门控记录
802
- delete(id) {
803
- const result = this.gates.delete(id);
804
- if (result) this._save();
805
- return result;
325
+ const fallbackPayload = { content: chunk };
326
+ if (options.files?.length) fallbackPayload.files = options.files;
327
+ return sendWithBackoff(channel, fallbackPayload);
806
328
  }
807
- // 清理过期门控(超过指定时间未解决)
808
- cleanupExpired(maxAgeMs = 5 * 60 * 1e3) {
809
- const now = Date.now();
810
- let count = 0;
811
- for (const [id, gate] of this.gates.entries()) {
812
- if (gate.status === "pending" && now - gate.createdAt >= maxAgeMs) {
813
- const updated = {
814
- ...gate,
815
- status: "expired",
816
- resolvedAt: now,
817
- resolvedBy: "timeout",
818
- version: gate.version + 1
819
- };
820
- this.gates.set(id, updated);
821
- count++;
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 {
822
341
  }
823
342
  }
824
- this._save();
825
- return count;
826
343
  }
827
- // 归档已解决的门控(保留最近 N 条)
828
- archiveResolved(keepCount = 100) {
829
- const resolved = Array.from(this.gates.values()).filter((g) => g.status !== "pending").sort((a, b) => (b.resolvedAt || 0) - (a.resolvedAt || 0));
830
- if (resolved.length <= keepCount) {
831
- return 0;
832
- }
833
- const toArchive = resolved.slice(keepCount);
834
- for (const gate of toArchive) {
835
- this.gates.delete(gate.id);
836
- }
837
- this._save();
838
- return toArchive.length;
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);
839
353
  }
840
- // 获取所有门控(用于调试)
841
- getAll() {
842
- return Array.from(this.gates.values());
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
+ }
843
360
  }
844
- // 获取统计信息
845
- getStats() {
846
- const all = this.getAll();
847
- return {
848
- total: all.length,
849
- pending: all.filter((g) => g.status === "pending").length,
850
- approved: all.filter((g) => g.status === "approved").length,
851
- rejected: all.filter((g) => g.status === "rejected").length,
852
- expired: all.filter((g) => g.status === "expired").length,
853
- invalidated: all.filter((g) => g.status === "invalidated").length
854
- };
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 {
855
387
  }
856
- // 验证状态转换合法性
857
- isValidTransition(from, to) {
858
- const validTransitions = {
859
- pending: ["approved", "rejected", "expired", "invalidated"],
860
- approved: [],
861
- // 终态
862
- rejected: [],
863
- // 终态
864
- expired: [],
865
- // 终态
866
- invalidated: []
867
- // 终态
868
- };
869
- return validTransitions[from]?.includes(to) ?? false;
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
+ );
870
401
  }
871
- // 生成唯一 ID
872
- generateId() {
873
- return `gate_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
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}*`);
874
430
  }
875
- };
876
- var humanGateRegistry = new HumanGateRegistry();
877
-
878
- // ../state/src/gate-coordinator.ts
879
- var GateCoordinator = class {
880
- registry;
881
- receiptHandles = /* @__PURE__ */ new Map();
882
- timeoutTimers = /* @__PURE__ */ new Map();
883
- constructor(registry) {
884
- this.registry = registry;
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)}`);
885
435
  }
886
- /**
887
- * 创建新的人工门控
888
- */
889
- createGate(params) {
890
- const record = this.registry.create(params);
891
- if (record.supportsRemoteDecision && record.isBlocking) {
892
- this.setupTimeout(record.id);
893
- }
894
- return record;
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)}`);
895
441
  }
896
- /**
897
- * 注册回执句柄(仅内存,不持久化)
898
- */
899
- registerReceiptHandle(gateId, handle) {
900
- const record = this.registry.get(gateId);
901
- if (!record || record.status !== "pending") {
902
- handle.reject("\u95E8\u63A7\u4E0D\u5B58\u5728\u6216\u5DF2\u5904\u7406");
903
- return;
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;
904
464
  }
905
- this.receiptHandles.set(gateId, { ...handle, gateId });
906
- }
907
- /**
908
- * 绑定 Discord 交互卡消息 ID
909
- */
910
- bindDiscordMessage(gateId, discordMessageId) {
911
- const record = this.registry.get(gateId);
912
- if (!record) {
913
- return false;
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;
914
471
  }
915
- const result = this.registry.update(gateId, record.version, { discordMessageId });
916
- return result.success;
917
472
  }
918
- /**
919
- * 尝试通过 Discord 解决门控(CAS 更新)
920
- */
921
- async resolveFromDiscord(gateId, action) {
922
- const record = this.registry.get(gateId);
923
- if (!record) {
924
- return { success: false, message: "\u95E8\u63A7\u4E0D\u5B58\u5728", handledByReceipt: false };
925
- }
926
- if (record.status !== "pending") {
927
- return { success: false, message: "\u95E8\u63A7\u5DF2\u88AB\u5904\u7406", handledByReceipt: false };
928
- }
929
- const result = this.registry.update(record.id, record.version, {
930
- status: action === "approve" ? "approved" : "rejected",
931
- resolvedAt: Date.now(),
932
- resolvedBy: "discord",
933
- resolvedAction: action
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
934
499
  });
935
- if (!result.success) {
936
- return { success: false, message: result.message, handledByReceipt: false };
937
- }
938
- this.clearTimeout(gateId);
939
- const handle = this.receiptHandles.get(gateId);
940
- let handledByReceipt = false;
941
- if (handle) {
942
- handle.resolve(action, "discord");
943
- this.receiptHandles.delete(gateId);
944
- handledByReceipt = true;
945
- }
946
- return { success: true, handledByReceipt };
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;
947
506
  }
948
- /**
949
- * 通知终端已处理(由钩子调用)
950
- */
951
- notifyTerminalResolved(gateId, action) {
952
- const record = this.registry.get(gateId);
953
- if (!record) {
954
- return { success: false, message: "\u95E8\u63A7\u4E0D\u5B58\u5728", handledByReceipt: false };
955
- }
956
- if (record.status !== "pending") {
957
- return { success: false, message: "\u95E8\u63A7\u5DF2\u88AB\u5904\u7406", handledByReceipt: false };
958
- }
959
- const result = this.registry.update(record.id, record.version, {
960
- status: action === "approve" ? "approved" : "rejected",
961
- resolvedAt: Date.now(),
962
- resolvedBy: "terminal",
963
- resolvedAction: action
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)}`
964
634
  });
965
- if (!result.success) {
966
- return { success: false, message: result.message, handledByReceipt: false };
967
- }
968
- this.clearTimeout(gateId);
969
- const handle = this.receiptHandles.get(gateId);
970
- let handledByReceipt = false;
971
- if (handle) {
972
- handle.resolve(action, "terminal");
973
- this.receiptHandles.delete(gateId);
974
- handledByReceipt = true;
975
- }
976
- return { success: true, handledByReceipt };
977
635
  }
978
- /**
979
- * 设置超时定时器(5 分钟)
980
- */
981
- setupTimeout(gateId) {
982
- const timer = setTimeout(() => {
983
- this.handleTimeout(gateId);
984
- }, 5 * 60 * 1e3);
985
- this.timeoutTimers.set(gateId, timer);
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
+ );
986
662
  }
987
- /**
988
- * 清理超时定时器
989
- */
990
- clearTimeout(gateId) {
991
- const timer = this.timeoutTimers.get(gateId);
992
- if (timer) {
993
- clearTimeout(timer);
994
- this.timeoutTimers.delete(gateId);
995
- }
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)}` });
996
668
  }
997
- /**
998
- * 处理超时
999
- */
1000
- handleTimeout(gateId) {
1001
- const record = this.registry.get(gateId);
1002
- if (!record || record.status !== "pending") {
1003
- return;
1004
- }
1005
- this.registry.update(record.id, record.version, {
1006
- status: "expired",
1007
- resolvedAt: Date.now(),
1008
- resolvedBy: "timeout"
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)}`
1009
681
  });
1010
- const handle = this.receiptHandles.get(gateId);
1011
- if (handle) {
1012
- handle.reject("\u5BA1\u6279\u8D85\u65F6\uFF085 \u5206\u949F\uFF09");
1013
- this.receiptHandles.delete(gateId);
1014
- }
1015
- this.timeoutTimers.delete(gateId);
1016
682
  }
1017
- /**
1018
- * 重启时失效所有待处理门控
1019
- */
1020
- invalidateAllOnRestart() {
1021
- const count = this.registry.invalidateAll("restart");
1022
- console.log(`[GateCoordinator] Invalidated ${count} pending gates on restart`);
1023
- const toUpdate = [];
1024
- for (const gate of this.registry.getAll()) {
1025
- if (gate.status === "invalidated" && gate.discordMessageId) {
1026
- toUpdate.push({
1027
- gateId: gate.id,
1028
- discordMessageId: gate.discordMessageId
1029
- });
1030
- }
1031
- }
1032
- this.receiptHandles.clear();
1033
- for (const timer of this.timeoutTimers.values()) {
1034
- clearTimeout(timer);
1035
- }
1036
- this.timeoutTimers.clear();
1037
- return toUpdate;
683
+ };
684
+ var imageFile = (event, ctx) => {
685
+ if (existsSync2(event.filePath)) {
686
+ ctx.state.pendingAttachments.push(event.filePath);
1038
687
  }
1039
- /**
1040
- * 获取门控记录
1041
- */
1042
- getGate(gateId) {
1043
- return this.registry.get(gateId);
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
+ });
1044
697
  }
1045
- /**
1046
- * 获取会话的活跃门控
1047
- */
1048
- getActiveGateForSession(sessionId) {
1049
- const active = this.registry.getActiveBySession(sessionId);
1050
- return active[0];
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)}` });
1051
715
  }
1052
- /**
1053
- * 清理过期门控
1054
- */
1055
- cleanupExpired() {
1056
- return this.registry.cleanupExpired();
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;
1057
778
  }
1058
- /**
1059
- * 归档已解决门控,保留最近 N 条
1060
- */
1061
- archiveResolved(keepCount = 100) {
1062
- return this.registry.archiveResolved(keepCount);
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
+ };
1063
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
1064
858
  };
1065
- var gateCoordinator = new GateCoordinator(humanGateRegistry);
859
+ async function dispatchEvent(event, ctx) {
860
+ const handler = EVENT_HANDLERS[event.type];
861
+ if (!handler) return;
862
+ await handler(event, ctx);
863
+ }
1066
864
 
1067
865
  // ../bot/src/discord/status-card.ts
866
+ import {
867
+ EmbedBuilder as EmbedBuilder3
868
+ } from "discord.js";
1068
869
  var StatusCard = class {
1069
870
  messageId = null;
1070
871
  channel;
@@ -1141,7 +942,7 @@ var StatusCard = class {
1141
942
  }
1142
943
  }
1143
944
  buildEmbed(state, data) {
1144
- const embed = new EmbedBuilder().setColor(STATE_COLORS[state]).setTitle(`\u{1F916} ${STATE_LABELS[state]}`).addFields(
945
+ const embed = new EmbedBuilder3().setColor(STATE_COLORS[state]).setTitle(`\u{1F916} ${STATE_LABELS[state]}`).addFields(
1145
946
  { name: "\u8F6E\u6B21", value: `#${data.turn}`, inline: true },
1146
947
  { name: "\u66F4\u65B0", value: `<t:${Math.floor(data.updatedAt / 1e3)}:R>`, inline: true }
1147
948
  ).setTimestamp();
@@ -1167,204 +968,102 @@ var StatusCard = class {
1167
968
  if (data.permissionsSummary) {
1168
969
  embed.addFields({ name: "\u6743\u9650", value: data.permissionsSummary, inline: false });
1169
970
  }
1170
- return embed;
1171
- }
1172
- /**
1173
- * Sanitize phase text for status card display.
1174
- * Returns cleaned text or empty string if unsuitable.
1175
- */
1176
- sanitizePhase(description) {
1177
- let normalized = description.trim();
1178
- if (!normalized) return "";
1179
- if (normalized.includes("```") || /diff --git/.test(normalized) || this.isLikelyFileList(normalized)) {
1180
- console.warn(`[StatusCard] Phase contains unsuitable content, stripping: ${normalized.slice(0, 60)}...`);
1181
- return "";
1182
- }
1183
- if (normalized.length > 200) {
1184
- normalized = normalized.slice(0, 197) + "...";
1185
- }
1186
- return normalized;
1187
- }
1188
- validate(description) {
1189
- if (!description) return;
1190
- const normalized = description.trim();
1191
- if (!normalized) return;
1192
- if (normalized.length > 200) {
1193
- throw new Error("\u72B6\u6001\u5361\u63CF\u8FF0\u8FC7\u957F\uFF0C\u5E94\u79FB\u81F3\u6458\u8981\u5361\u6216\u7ED3\u679C\u6D88\u606F");
1194
- }
1195
- if (normalized.includes("```")) {
1196
- throw new Error("\u72B6\u6001\u5361\u4E0D\u5E94\u5305\u542B\u4EE3\u7801\u5757");
1197
- }
1198
- if (/diff --git/.test(normalized)) {
1199
- throw new Error("\u72B6\u6001\u5361\u4E0D\u5E94\u5305\u542B diff");
1200
- }
1201
- if (this.isLikelyFileList(normalized)) {
1202
- throw new Error("\u72B6\u6001\u5361\u4E0D\u5E94\u5305\u542B\u6587\u4EF6\u5217\u8868");
1203
- }
1204
- }
1205
- isLikelyFileList(text) {
1206
- const lines = text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
1207
- if (lines.length < 2) return false;
1208
- return lines.every((line) => /^[-+*]\s+[\w./\\-]+$/.test(line));
1209
- }
1210
- };
1211
-
1212
- // ../bot/src/discord/summary-handler.ts
1213
- import "discord.js";
1214
-
1215
- // ../bot/src/discord/delivery-policy.ts
1216
- import { existsSync, statSync } from "fs";
1217
- var MAX_CHUNK_LIMIT = 2e3;
1218
- var MAX_ATTACHMENTS = 10;
1219
- var MAX_ATTACHMENT_BYTES = 25 * 1024 * 1024;
1220
- function clampChunkLimit(limit) {
1221
- if (!Number.isFinite(limit)) return MAX_CHUNK_LIMIT;
1222
- return Math.max(1, Math.min(Math.trunc(limit), MAX_CHUNK_LIMIT));
1223
- }
1224
- function chunkText(text, limit, mode) {
1225
- const safeLimit = clampChunkLimit(limit);
1226
- if (text.length <= safeLimit) return [text];
1227
- const out = [];
1228
- let rest = text;
1229
- while (rest.length > safeLimit) {
1230
- let cut = safeLimit;
1231
- if (mode === "newline") {
1232
- const para = rest.lastIndexOf("\n\n", safeLimit);
1233
- const line = rest.lastIndexOf("\n", safeLimit);
1234
- const space = rest.lastIndexOf(" ", safeLimit);
1235
- cut = para > safeLimit / 2 ? para : line > safeLimit / 2 ? line : space > 0 ? space : safeLimit;
1236
- }
1237
- out.push(rest.slice(0, cut));
1238
- rest = rest.slice(cut).replace(/^\n+/, "");
1239
- }
1240
- if (rest) out.push(rest);
1241
- return out;
1242
- }
1243
- function validateOutboundAttachments(files) {
1244
- if (files.length > MAX_ATTACHMENTS) {
1245
- throw new Error(`\u6700\u591A\u53EA\u80FD\u53D1\u9001 ${MAX_ATTACHMENTS} \u4E2A\u9644\u4EF6`);
1246
- }
1247
- for (const filePath of files) {
1248
- if (!filePath.trim()) {
1249
- throw new Error("\u9644\u4EF6\u8DEF\u5F84\u65E0\u6548");
1250
- }
1251
- if (!existsSync(filePath)) continue;
1252
- const stats = statSync(filePath);
1253
- if (stats.isDirectory()) {
1254
- throw new Error(`\u9644\u4EF6\u8DEF\u5F84\u4E0D\u80FD\u662F\u76EE\u5F55: ${filePath}`);
1255
- }
1256
- if (stats.size > MAX_ATTACHMENT_BYTES) {
1257
- throw new Error(`\u9644\u4EF6 ${filePath} \u5927\u5C0F\u8D85\u8FC7 ${MAX_ATTACHMENT_BYTES} bytes`);
1258
- }
1259
- }
1260
- }
1261
- function buildDeliveryPlan(input) {
1262
- validateOutboundAttachments(input.files);
1263
- const chunks = chunkText(input.text, input.policy.textChunkLimit, input.policy.chunkMode);
1264
- const filesOnFirstChunk = input.files.slice(0, MAX_ATTACHMENTS);
1265
- const replyToMode = input.policy.replyToMode;
1266
- const replyToMessageId = input.mode === "system_notice" || input.mode === "summary" ? void 0 : replyToMode === "off" ? void 0 : input.replyToMessageId;
1267
- return {
1268
- sessionId: input.sessionId,
1269
- chatId: input.chatId,
1270
- replyToMessageId,
1271
- replyToMode,
1272
- editTargetMessageId: input.editTargetMessageId,
1273
- chunks,
1274
- filesOnFirstChunk,
1275
- mode: input.mode
1276
- };
1277
- }
1278
-
1279
- // ../bot/src/discord/delivery.ts
1280
- var sessionState = /* @__PURE__ */ new Map();
1281
- function getSessionState(sessionId) {
1282
- if (!sessionState.has(sessionId)) {
1283
- sessionState.set(sessionId, {});
1284
- }
1285
- const state = sessionState.get(sessionId);
1286
- state.lastUsed = Date.now();
1287
- return state;
1288
- }
1289
- function pruneStaleSessionStates(maxAgeMs = 5 * 60 * 1e3) {
1290
- const cutoff = Date.now() - maxAgeMs;
1291
- for (const [sessionId, state] of sessionState) {
1292
- if (!state.lastUsed || state.lastUsed < cutoff) {
1293
- sessionState.delete(sessionId);
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
+ });
980
+ }
1294
981
  }
1295
- }
1296
- }
1297
- function cleanupSessionDeliveryState(sessionId) {
1298
- sessionState.delete(sessionId);
1299
- }
1300
- async function sendTyping(channel) {
1301
- try {
1302
- await channel.sendTyping?.();
1303
- } catch {
1304
- }
1305
- }
1306
- async function sendAckReaction(message, reaction) {
1307
- if (!reaction) return;
1308
- try {
1309
- await message.react(reaction);
1310
- } catch {
1311
- }
1312
- }
1313
- async function sendChunk(channel, chunk, options = {}) {
1314
- const payload = { content: chunk };
1315
- if (options.files?.length) payload.files = options.files;
1316
- if (options.replyToMessageId) {
1317
- payload.reply = { messageReference: options.replyToMessageId };
1318
- }
1319
- try {
1320
- const message = await channel.send(payload);
1321
- return message.id;
1322
- } catch (error) {
1323
- if (!options.replyToMessageId) {
1324
- throw error;
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
+ });
1325
991
  }
1326
- const fallbackPayload = { content: chunk };
1327
- if (options.files?.length) fallbackPayload.files = options.files;
1328
- const fallbackMessage = await channel.send(fallbackPayload);
1329
- return fallbackMessage.id;
1330
- }
1331
- }
1332
- async function deliver(channel, plan) {
1333
- pruneStaleSessionStates();
1334
- const state = getSessionState(plan.sessionId);
1335
- if (plan.mode === "progress_update") {
1336
- const targetMessageId = plan.editTargetMessageId ?? state.recentProgressMessageId;
1337
- if (targetMessageId && plan.chunks.length === 1 && plan.filesOnFirstChunk.length === 0) {
1338
- try {
1339
- await channel.messages?.edit(targetMessageId, { content: plan.chunks[0] });
1340
- state.recentProgressMessageId = targetMessageId;
1341
- return [targetMessageId];
1342
- } catch {
1343
- }
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 });
1344
995
  }
996
+ return embed;
1345
997
  }
1346
- const ids = [];
1347
- const replyToMode = plan.replyToMode ?? "first";
1348
- for (let index = 0; index < plan.chunks.length; index++) {
1349
- const shouldReply = !!plan.replyToMessageId && replyToMode !== "off" && (replyToMode === "all" || index === 0);
1350
- const id = await sendChunk(channel, plan.chunks[index], {
1351
- replyToMessageId: shouldReply ? plan.replyToMessageId : void 0,
1352
- files: index === 0 ? plan.filesOnFirstChunk : void 0
1353
- });
1354
- 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");
1355
1021
  }
1356
- if (ids.length > 0) {
1357
- if (plan.mode === "progress_update") {
1358
- state.recentProgressMessageId = ids[ids.length - 1];
1359
- } else {
1360
- 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) + "...";
1361
1035
  }
1036
+ return normalized;
1362
1037
  }
1363
- return ids;
1364
- }
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";
1365
1064
 
1366
1065
  // ../bot/src/discord/digest-delivery.ts
1367
- import { EmbedBuilder as EmbedBuilder2 } from "discord.js";
1066
+ import { EmbedBuilder as EmbedBuilder4 } from "discord.js";
1368
1067
  var DigestDelivery = class {
1369
1068
  channel;
1370
1069
  messageIds = [];
@@ -1423,7 +1122,7 @@ var DigestDelivery = class {
1423
1122
  return { oldMessageIds, newMessageIds };
1424
1123
  }
1425
1124
  buildDigestEmbed(content, index, total) {
1426
- const embed = new EmbedBuilder2().setColor(3447003).setDescription(content).setTimestamp();
1125
+ const embed = new EmbedBuilder4().setColor(3447003).setDescription(content).setTimestamp();
1427
1126
  if (index === 0) embed.setTitle("\u{1F4CC} \u6700\u8FD1\u6458\u8981");
1428
1127
  if (total > 1) {
1429
1128
  embed.setFooter({ text: `\u7B2C ${index + 1}/${total} \u90E8\u5206` });
@@ -1505,10 +1204,10 @@ var SummaryHandler = class {
1505
1204
 
1506
1205
  // ../bot/src/discord/interaction-card.ts
1507
1206
  import {
1508
- EmbedBuilder as EmbedBuilder3,
1509
- ActionRowBuilder,
1510
- ButtonBuilder,
1511
- ButtonStyle
1207
+ EmbedBuilder as EmbedBuilder5,
1208
+ ActionRowBuilder as ActionRowBuilder2,
1209
+ ButtonBuilder as ButtonBuilder2,
1210
+ ButtonStyle as ButtonStyle2
1512
1211
  } from "discord.js";
1513
1212
  var InteractionCard = class {
1514
1213
  channel;
@@ -1518,16 +1217,16 @@ var InteractionCard = class {
1518
1217
  }
1519
1218
  async show(sessionId, turn, detail, options = {}) {
1520
1219
  const customIdBase = `awaiting_human:${sessionId}:${turn}`;
1521
- 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();
1522
1221
  embed.addFields(
1523
1222
  { name: "\u8F6E\u6B21", value: `#${turn}`, inline: true },
1524
1223
  { name: "\u7B49\u5F85\u81EA", value: `<t:${Math.floor(Date.now() / 1e3)}:R>`, inline: true }
1525
1224
  );
1526
1225
  let components = [];
1527
1226
  if (options.remoteHumanControl !== false) {
1528
- const row = new ActionRowBuilder().addComponents(
1529
- new ButtonBuilder().setCustomId(`${customIdBase}:approve`).setLabel("\u5141\u8BB8\u7EE7\u7EED").setStyle(ButtonStyle.Success),
1530
- 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)
1531
1230
  );
1532
1231
  components = [row];
1533
1232
  } else {
@@ -1795,24 +1494,98 @@ var PerformanceTracker = class {
1795
1494
  };
1796
1495
  var performanceTracker = new PerformanceTracker();
1797
1496
 
1798
- // ../engine/src/output/answer-store.ts
1799
- var pendingAnswersStore = /* @__PURE__ */ new Map();
1800
- var questionCountStore = /* @__PURE__ */ new Map();
1801
- function setPendingAnswer(sessionId, questionIndex, answer) {
1802
- if (!pendingAnswersStore.has(sessionId)) {
1803
- pendingAnswersStore.set(sessionId, /* @__PURE__ */ new Map());
1804
- }
1805
- pendingAnswersStore.get(sessionId).set(questionIndex, answer);
1497
+ // ../bot/src/panel/panel-state.ts
1498
+ var sessionPanels = /* @__PURE__ */ new Map();
1499
+ var sessionInitializationPromises = /* @__PURE__ */ new Map();
1500
+ function getPanel(sessionId) {
1501
+ return sessionPanels.get(sessionId);
1806
1502
  }
1807
- function getPendingAnswers(sessionId) {
1808
- return pendingAnswersStore.get(sessionId);
1503
+ function setPanel(sessionId, panel) {
1504
+ sessionPanels.set(sessionId, panel);
1809
1505
  }
1810
- function clearPendingAnswers(sessionId) {
1811
- pendingAnswersStore.delete(sessionId);
1812
- questionCountStore.delete(sessionId);
1506
+ function deletePanel(sessionId) {
1507
+ sessionPanels.delete(sessionId);
1813
1508
  }
1814
- function getQuestionCount(sessionId) {
1815
- return questionCountStore.get(sessionId) || 0;
1509
+ function getAllPanels() {
1510
+ return sessionPanels.entries();
1511
+ }
1512
+ function getPanelCount() {
1513
+ return sessionPanels.size;
1514
+ }
1515
+ function getInitializationPromise(sessionId) {
1516
+ return sessionInitializationPromises.get(sessionId);
1517
+ }
1518
+ function setInitializationPromise(sessionId, promise) {
1519
+ sessionInitializationPromises.set(sessionId, promise);
1520
+ }
1521
+ function deleteInitializationPromise(sessionId) {
1522
+ sessionInitializationPromises.delete(sessionId);
1523
+ }
1524
+
1525
+ // ../bot/src/panel/panel-relocation.ts
1526
+ var RELOCATION_TIMEOUT_MS = 15e3;
1527
+ async function relocateSessionPanelToBottom(sessionId, channel, initialize) {
1528
+ let panel = getPanel(sessionId);
1529
+ if (!panel && channel) {
1530
+ const session = getSessionView(sessionId);
1531
+ const initPromise = initialize(sessionId, channel, {
1532
+ statusCardMessageId: session?.statusCardMessageId,
1533
+ initialTurn: session?.currentTurn || 1
1534
+ });
1535
+ let timeoutHandle;
1536
+ const timeout = new Promise((_, reject) => {
1537
+ timeoutHandle = setTimeout(
1538
+ () => reject(new Error("Panel initialization timeout")),
1539
+ RELOCATION_TIMEOUT_MS
1540
+ );
1541
+ });
1542
+ try {
1543
+ await Promise.race([initPromise, timeout]);
1544
+ } finally {
1545
+ if (timeoutHandle) clearTimeout(timeoutHandle);
1546
+ }
1547
+ panel = getPanel(sessionId);
1548
+ }
1549
+ if (!panel) return;
1550
+ let statusRelocation = null;
1551
+ try {
1552
+ statusRelocation = await panel.statusCard.recreateAtBottom();
1553
+ } catch (error) {
1554
+ console.warn(`\u72B6\u6001\u6D88\u606F\u8FC1\u79FB\u5931\u8D25 (${sessionId})\uFF1A`, error);
1555
+ return;
1556
+ }
1557
+ let digestRelocation = { oldMessageIds: [], newMessageIds: [] };
1558
+ try {
1559
+ digestRelocation = await panel.summaryHandler.relocateDigestToBottom();
1560
+ } catch (error) {
1561
+ console.warn(`\u6458\u8981\u6D88\u606F\u8FC1\u79FB\u5931\u8D25 (${sessionId})\uFF1A`, error);
1562
+ if (statusRelocation?.oldMessageId && statusRelocation.newMessageId) {
1563
+ panel.statusCard.adopt(statusRelocation.oldMessageId);
1564
+ await panel.channel.messages.delete(statusRelocation.newMessageId).catch(
1565
+ (e) => console.warn(
1566
+ `[PanelAdapter] Failed to cleanup new status card (${sessionId}): ${e.message}`
1567
+ )
1568
+ );
1569
+ }
1570
+ return;
1571
+ }
1572
+ if (statusRelocation?.newMessageId) {
1573
+ setStatusCardBinding(sessionId, { messageId: statusRelocation.newMessageId });
1574
+ }
1575
+ if (statusRelocation?.oldMessageId) {
1576
+ await panel.channel.messages.delete(statusRelocation.oldMessageId).catch(
1577
+ (e) => console.warn(
1578
+ `[PanelAdapter] Failed to delete old status card (${sessionId}): ${e.message}`
1579
+ )
1580
+ );
1581
+ }
1582
+ for (const messageId of digestRelocation.oldMessageIds) {
1583
+ await panel.channel.messages.delete(messageId).catch(
1584
+ (e) => console.warn(
1585
+ `[PanelAdapter] Failed to delete old digest (${sessionId}): ${e.message}`
1586
+ )
1587
+ );
1588
+ }
1816
1589
  }
1817
1590
 
1818
1591
  // ../bot/src/bot-services-helpers.ts
@@ -1868,15 +1641,82 @@ async function notifyUnmanagedCodexHint(client, sessionId, channelId) {
1868
1641
  }
1869
1642
  }
1870
1643
 
1644
+ // ../bot/src/panel/panel-performance.ts
1645
+ var SESSION_INACTIVE_TIMEOUT_MS = 36e5;
1646
+ function createCleanupSessionPanel(renderer) {
1647
+ return function cleanupSessionPanel2(sessionId) {
1648
+ const panel = getPanel(sessionId);
1649
+ if (panel) panel.cleanup();
1650
+ deletePanel(sessionId);
1651
+ renderer.clear(sessionId);
1652
+ stateMachine.clearSession(sessionId);
1653
+ clearPendingAnswers(sessionId);
1654
+ cleanupSessionDeliveryState(sessionId);
1655
+ clearCodexHint(sessionId);
1656
+ cleanupSessionAttachments(sessionId).catch(
1657
+ (e) => console.warn(
1658
+ `[PanelAdapter] Failed to cleanup attachments (${sessionId}): ${e.message}`
1659
+ )
1660
+ );
1661
+ const activeGate = gateService.getActiveGateForSession(sessionId);
1662
+ if (activeGate) {
1663
+ gateService.resolveFromDiscord(activeGate.id, "reject").catch(
1664
+ (e) => console.warn(
1665
+ `[PanelAdapter] Failed to invalidate gate on cleanup (${sessionId}): ${e.message}`
1666
+ )
1667
+ );
1668
+ }
1669
+ };
1670
+ }
1671
+ function createCleanupInactiveSessions(renderer) {
1672
+ return function cleanupInactiveSessions2() {
1673
+ const now = Date.now();
1674
+ for (const [sessionId, panel] of getAllPanels()) {
1675
+ if (now - panel.getLastActivity() > SESSION_INACTIVE_TIMEOUT_MS) {
1676
+ panel.cleanup();
1677
+ deletePanel(sessionId);
1678
+ renderer.clear(sessionId);
1679
+ console.log(`\u6E05\u7406\u5931\u6D3B\u4F1A\u8BDD\u72B6\u6001\u6295\u5F71: ${sessionId}`);
1680
+ }
1681
+ }
1682
+ };
1683
+ }
1684
+ function createGetPerformanceStats() {
1685
+ return function getPerformanceStats2() {
1686
+ let projectionCount = 0;
1687
+ for (const [, panel] of getAllPanels()) {
1688
+ if (panel.getCachedProjection() !== null) projectionCount++;
1689
+ }
1690
+ return {
1691
+ discoveryLatency: performanceTracker.getMetricStats("session_discovery_latency"),
1692
+ updateLatency: performanceTracker.getMetricStats("state_update_latency"),
1693
+ activeSessions: getPanelCount(),
1694
+ projectionCount
1695
+ };
1696
+ };
1697
+ }
1698
+ var cleanupInterval = null;
1699
+ function startPerformanceMonitoring(onTick) {
1700
+ if (cleanupInterval) return;
1701
+ cleanupInterval = setInterval(() => {
1702
+ onTick();
1703
+ performanceTracker.takeSnapshot();
1704
+ performanceTracker.cleanup();
1705
+ }, 6e4);
1706
+ }
1707
+ function stopPerformanceMonitoring() {
1708
+ if (cleanupInterval) {
1709
+ clearInterval(cleanupInterval);
1710
+ cleanupInterval = null;
1711
+ }
1712
+ }
1713
+ function generatePerformanceReport() {
1714
+ return performanceTracker.generateReport();
1715
+ }
1716
+
1871
1717
  // ../bot/src/panel-adapter.ts
1872
- var sessionPanels = /* @__PURE__ */ new Map();
1873
- var sessionInitializationPromises = /* @__PURE__ */ new Map();
1874
1718
  var BATCH_UPDATE_DELAY_MS = 500;
1875
1719
  var statusCardProjectionRenderer = new StatusCardProjectionRenderer();
1876
- var SESSION_INACTIVE_TIMEOUT_MS = 36e5;
1877
- function getPanel(sessionId) {
1878
- return sessionPanels.get(sessionId);
1879
- }
1880
1720
  function getSessionProjection(sessionId) {
1881
1721
  return stateMachine.getSnapshot(sessionId);
1882
1722
  }
@@ -1889,20 +1729,18 @@ function ensureProjectionTurn(sessionId, turn = 1, event = "turn_bootstrap") {
1889
1729
  }
1890
1730
  function cacheProjection(sessionId, projection) {
1891
1731
  const panel = getPanel(sessionId);
1892
- if (panel) {
1893
- panel.updateProjection(projection);
1894
- }
1732
+ if (panel) panel.updateProjection(projection);
1895
1733
  }
1896
- function persistTurnState(sessionId, projection) {
1734
+ stateMachine.registerTurnStatePersister((sessionId, projection) => {
1897
1735
  updateSession(sessionId, {
1898
1736
  currentTurn: projection.turn,
1899
1737
  humanResolved: projection.humanResolved
1900
1738
  });
1901
- }
1739
+ });
1902
1740
  function createStatusCardProjectionContext(sessionId) {
1903
1741
  const panel = getPanel(sessionId);
1904
1742
  if (!panel) return void 0;
1905
- const session = getSession(sessionId);
1743
+ const session = getSessionView(sessionId);
1906
1744
  return {
1907
1745
  statusCard: panel.statusCard,
1908
1746
  remoteHumanControl: session?.remoteHumanControl,
@@ -1929,17 +1767,16 @@ async function scheduleProjectionRender(sessionId, projection, updateKey) {
1929
1767
  );
1930
1768
  }
1931
1769
  function resolveProviderSource(sessionId, fallback = "claude") {
1932
- const session = getSession(sessionId);
1770
+ const session = getSessionView(sessionId);
1933
1771
  return session?.provider === "codex" ? "codex" : fallback;
1934
1772
  }
1935
1773
  async function initializeSessionPanel(sessionId, channel, options = {}) {
1936
1774
  performanceTracker.startSessionDiscovery(sessionId);
1937
- const existing = getPanel(sessionId);
1938
- if (existing) {
1775
+ if (getPanel(sessionId)) {
1939
1776
  performanceTracker.endSessionDiscovery(sessionId, { cached: true });
1940
1777
  return;
1941
1778
  }
1942
- const pendingInitialization = sessionInitializationPromises.get(sessionId);
1779
+ const pendingInitialization = getInitializationPromise(sessionId);
1943
1780
  if (pendingInitialization) {
1944
1781
  await pendingInitialization;
1945
1782
  performanceTracker.endSessionDiscovery(sessionId, { cached: true });
@@ -1947,7 +1784,7 @@ async function initializeSessionPanel(sessionId, channel, options = {}) {
1947
1784
  }
1948
1785
  const initialization = (async () => {
1949
1786
  const panel = new SessionPanelComponent(sessionId, channel);
1950
- const session = getSession(sessionId);
1787
+ const session = getSessionView(sessionId);
1951
1788
  await panel.initialize({
1952
1789
  statusCardMessageId: options.statusCardMessageId,
1953
1790
  initialTurn: options.initialTurn,
@@ -1956,7 +1793,7 @@ async function initializeSessionPanel(sessionId, channel, options = {}) {
1956
1793
  provider: session?.provider,
1957
1794
  permissionsSummary: session ? getSessionPermissionSummary(session) : void 0
1958
1795
  });
1959
- sessionPanels.set(sessionId, panel);
1796
+ setPanel(sessionId, panel);
1960
1797
  setStatusCardBinding(sessionId, {
1961
1798
  messageId: panel.getMessageId() ?? options.statusCardMessageId
1962
1799
  });
@@ -1967,24 +1804,22 @@ async function initializeSessionPanel(sessionId, channel, options = {}) {
1967
1804
  );
1968
1805
  cacheProjection(sessionId, projection);
1969
1806
  })();
1970
- sessionInitializationPromises.set(sessionId, initialization);
1807
+ setInitializationPromise(sessionId, initialization);
1971
1808
  try {
1972
1809
  await initialization;
1973
1810
  performanceTracker.endSessionDiscovery(sessionId, { cached: false });
1974
1811
  } finally {
1975
- sessionInitializationPromises.delete(sessionId);
1812
+ deleteInitializationPromise(sessionId);
1976
1813
  }
1977
1814
  }
1978
1815
  async function registerExistingStatusCard(sessionId, channel, statusCardMessageId) {
1979
- await initializeSessionPanel(sessionId, channel, {
1980
- statusCardMessageId
1981
- });
1816
+ await initializeSessionPanel(sessionId, channel, { statusCardMessageId });
1982
1817
  }
1983
1818
  async function updateSessionState(sessionId, event, options = {}) {
1984
1819
  const updateKey = `${sessionId}:state`;
1985
1820
  performanceTracker.startStateUpdate(updateKey);
1986
1821
  if (!getPanel(sessionId) && options.channel) {
1987
- const session2 = getSession(sessionId);
1822
+ const session2 = getSessionView(sessionId);
1988
1823
  await initializeSessionPanel(sessionId, options.channel, {
1989
1824
  statusCardMessageId: session2?.statusCardMessageId,
1990
1825
  initialTurn: session2?.currentTurn || 1
@@ -1999,7 +1834,7 @@ async function updateSessionState(sessionId, event, options = {}) {
1999
1834
  performanceTracker.endStateUpdate(updateKey, { skipped: true });
2000
1835
  return null;
2001
1836
  }
2002
- const session = getSession(sessionId);
1837
+ const session = getSessionView(sessionId);
2003
1838
  if (platformEvent.source === "codex" && platformEvent.type === "completed" && session?.isGenerating) {
2004
1839
  performanceTracker.endStateUpdate(updateKey, { skipped: true });
2005
1840
  return getSessionProjection(sessionId);
@@ -2008,12 +1843,12 @@ async function updateSessionState(sessionId, event, options = {}) {
2008
1843
  const projection = stateMachine.applyPlatformEvent(platformEvent);
2009
1844
  cacheProjection(sessionId, projection);
2010
1845
  const stateChanged = projection.state !== previousProjection.state || projection.turn !== previousProjection.turn || projection.phase !== previousProjection.phase;
2011
- 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) {
2012
1848
  await scheduleProjectionRender(sessionId, projection, updateKey);
2013
1849
  } else {
2014
1850
  performanceTracker.endStateUpdate(updateKey, { skipped: true });
2015
1851
  }
2016
- persistTurnState(sessionId, projection);
2017
1852
  return projection;
2018
1853
  }
2019
1854
  async function handleResultEvent(sessionId, event, textContent, attachments = []) {
@@ -2035,10 +1870,13 @@ async function handleResultEvent(sessionId, event, textContent, attachments = []
2035
1870
  metadata: { from: "result" }
2036
1871
  });
2037
1872
  } else if (!event.success) {
2038
- const session = getSession(sessionId);
1873
+ const session = getSessionView(sessionId);
2039
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;
2040
1878
  await panel.summaryHandler.sendTurnFailure(
2041
- failureText,
1879
+ withReason,
2042
1880
  projection.turn,
2043
1881
  session?.lastInboundMessageId,
2044
1882
  attachments
@@ -2053,7 +1891,7 @@ async function handleResultEvent(sessionId, event, textContent, attachments = []
2053
1891
  });
2054
1892
  } else {
2055
1893
  const beforeProjection = getSessionProjection(sessionId);
2056
- const session = getSession(sessionId);
1894
+ const session = getSessionView(sessionId);
2057
1895
  await panel.summaryHandler.sendTurnSummary(
2058
1896
  textContent,
2059
1897
  beforeProjection.turn,
@@ -2061,7 +1899,6 @@ async function handleResultEvent(sessionId, event, textContent, attachments = []
2061
1899
  attachments
2062
1900
  );
2063
1901
  const projectionAfterTurn = stateMachine.advanceTurnToIdle(sessionId);
2064
- persistTurnState(sessionId, projectionAfterTurn);
2065
1902
  await renderProjectionToStatusCard(sessionId, projectionAfterTurn);
2066
1903
  cacheProjection(sessionId, projectionAfterTurn);
2067
1904
  }
@@ -2069,15 +1906,17 @@ async function handleResultEvent(sessionId, event, textContent, attachments = []
2069
1906
  async function handleAwaitingHuman(sessionId, detail, options = {}) {
2070
1907
  const panel = getPanel(sessionId);
2071
1908
  if (!panel) return null;
2072
- const session = getSession(sessionId);
1909
+ const session = getSessionView(sessionId);
2073
1910
  if (!panel.checkInteractionCooldown()) {
2074
- console.warn(`\u4EA4\u4E92\u5361\u521B\u5EFA\u9650\u6D41 (${sessionId}): \u8DDD\u4E0A\u6B21\u521B\u5EFA\u4EC5 ${panel.getTimeSinceLastInteraction()}ms`);
1911
+ console.warn(
1912
+ `\u4EA4\u4E92\u5361\u521B\u5EFA\u9650\u6D41 (${sessionId}): \u8DDD\u4E0A\u6B21\u521B\u5EFA\u4EC5 ${panel.getTimeSinceLastInteraction()}ms`
1913
+ );
2075
1914
  return null;
2076
1915
  }
2077
1916
  const projection = ensureProjectionTurn(sessionId, 1, "turn_bootstrap");
2078
1917
  const provider = session?.provider ?? resolveProviderSource(sessionId);
2079
1918
  const remoteHumanControl = session?.remoteHumanControl !== false;
2080
- const gate = gateCoordinator.createGate({
1919
+ const gate = gateService.createGate({
2081
1920
  sessionId,
2082
1921
  provider,
2083
1922
  type: "binary_approval",
@@ -2099,7 +1938,7 @@ async function handleAwaitingHuman(sessionId, detail, options = {}) {
2099
1938
  remoteHumanControl,
2100
1939
  provider
2101
1940
  });
2102
- gateCoordinator.bindDiscordMessage(gate.id, messageId);
1941
+ gateService.bindDiscordMessage(gate.id, messageId);
2103
1942
  panel.recordInteractionCardTime();
2104
1943
  cacheProjection(sessionId, getSessionProjection(sessionId));
2105
1944
  updateSession(sessionId, {
@@ -2111,73 +1950,21 @@ async function handleAwaitingHuman(sessionId, detail, options = {}) {
2111
1950
  setCurrentInteractionMessage(sessionId, messageId);
2112
1951
  return messageId;
2113
1952
  }
2114
- var RELOCATION_TIMEOUT_MS = 15e3;
2115
- async function relocateSessionPanelToBottom(sessionId, channel) {
2116
- let panel = getPanel(sessionId);
2117
- if (!panel && channel) {
2118
- const session = getSession(sessionId);
2119
- const initPromise = initializeSessionPanel(sessionId, channel, {
2120
- statusCardMessageId: session?.statusCardMessageId,
2121
- initialTurn: session?.currentTurn || 1
2122
- });
2123
- let timeoutHandle;
2124
- const timeout = new Promise((_, reject) => {
2125
- timeoutHandle = setTimeout(
2126
- () => reject(new Error("Panel initialization timeout")),
2127
- RELOCATION_TIMEOUT_MS
2128
- );
2129
- });
2130
- try {
2131
- await Promise.race([initPromise, timeout]);
2132
- } finally {
2133
- if (timeoutHandle) clearTimeout(timeoutHandle);
2134
- }
2135
- panel = getPanel(sessionId);
2136
- }
2137
- if (!panel) return;
2138
- let statusRelocation = null;
2139
- try {
2140
- statusRelocation = await panel.statusCard.recreateAtBottom();
2141
- } catch (error) {
2142
- console.warn(`\u72B6\u6001\u6D88\u606F\u8FC1\u79FB\u5931\u8D25 (${sessionId})\uFF1A`, error);
2143
- return;
2144
- }
2145
- let digestRelocation = { oldMessageIds: [], newMessageIds: [] };
2146
- try {
2147
- digestRelocation = await panel.summaryHandler.relocateDigestToBottom();
2148
- } catch (error) {
2149
- console.warn(`\u6458\u8981\u6D88\u606F\u8FC1\u79FB\u5931\u8D25 (${sessionId})\uFF1A`, error);
2150
- if (statusRelocation?.oldMessageId && statusRelocation.newMessageId) {
2151
- panel.statusCard.adopt(statusRelocation.oldMessageId);
2152
- await panel.channel.messages.delete(statusRelocation.newMessageId).catch((e) => console.warn(`[PanelAdapter] Failed to cleanup new status card (${sessionId}): ${e.message}`));
2153
- }
2154
- return;
2155
- }
2156
- if (statusRelocation?.newMessageId) {
2157
- setStatusCardBinding(sessionId, {
2158
- messageId: statusRelocation.newMessageId
2159
- });
2160
- }
2161
- if (statusRelocation?.oldMessageId) {
2162
- await panel.channel.messages.delete(statusRelocation.oldMessageId).catch((e) => console.warn(`[PanelAdapter] Failed to delete old status card (${sessionId}): ${e.message}`));
2163
- }
2164
- for (const messageId of digestRelocation.oldMessageIds) {
2165
- await panel.channel.messages.delete(messageId).catch((e) => console.warn(`[PanelAdapter] Failed to delete old digest (${sessionId}): ${e.message}`));
2166
- }
1953
+ async function relocateSessionPanelToBottom2(sessionId, channel) {
1954
+ await relocateSessionPanelToBottom(
1955
+ sessionId,
1956
+ channel,
1957
+ (sid, ch, options) => initializeSessionPanel(sid, ch, options)
1958
+ );
2167
1959
  }
2168
1960
  function queueDigest(sessionId, item) {
2169
- const panel = getPanel(sessionId);
2170
- if (!panel) return;
2171
- panel.queueDigest(item);
1961
+ getPanel(sessionId)?.queueDigest(item);
2172
1962
  }
2173
1963
  function getDigestQueue(sessionId) {
2174
- const panel = getPanel(sessionId);
2175
- if (!panel) return [];
2176
- return panel.getDigestQueue();
1964
+ return getPanel(sessionId)?.getDigestQueue() ?? [];
2177
1965
  }
2178
1966
  function clearDigestQueue(sessionId) {
2179
- const panel = getPanel(sessionId);
2180
- if (panel) panel.clearDigestQueue();
1967
+ getPanel(sessionId)?.clearDigestQueue();
2181
1968
  }
2182
1969
  async function flushDigest(sessionId) {
2183
1970
  const panel = getPanel(sessionId);
@@ -2193,66 +1980,16 @@ function mapPlatformEventTypeToUnifiedState(type) {
2193
1980
  function getStateMachine() {
2194
1981
  return stateMachine;
2195
1982
  }
2196
- function cleanupSessionPanel(sessionId) {
2197
- const panel = getPanel(sessionId);
2198
- if (panel) panel.cleanup();
2199
- sessionPanels.delete(sessionId);
2200
- statusCardProjectionRenderer.clear(sessionId);
2201
- stateMachine.clearSession(sessionId);
2202
- clearPendingAnswers(sessionId);
2203
- cleanupSessionDeliveryState(sessionId);
2204
- clearCodexHint(sessionId);
2205
- cleanupSessionAttachments(sessionId).catch((e) => console.warn(`[PanelAdapter] Failed to cleanup attachments (${sessionId}): ${e.message}`));
2206
- const activeGate = gateCoordinator.getActiveGateForSession(sessionId);
2207
- if (activeGate) {
2208
- gateCoordinator.resolveFromDiscord(activeGate.id, "reject").catch((e) => console.warn(`[PanelAdapter] Failed to invalidate gate on cleanup (${sessionId}): ${e.message}`));
2209
- }
2210
- }
2211
- function cleanupInactiveSessions() {
2212
- const now = Date.now();
2213
- for (const [sessionId, panel] of sessionPanels) {
2214
- if (now - panel.getLastActivity() > SESSION_INACTIVE_TIMEOUT_MS) {
2215
- panel.cleanup();
2216
- sessionPanels.delete(sessionId);
2217
- statusCardProjectionRenderer.clear(sessionId);
2218
- console.log(`\u6E05\u7406\u5931\u6D3B\u4F1A\u8BDD\u72B6\u6001\u6295\u5F71: ${sessionId}`);
2219
- }
2220
- }
2221
- }
2222
- function getPerformanceStats() {
2223
- let projectionCount = 0;
2224
- for (const panel of sessionPanels.values()) {
2225
- if (panel.getCachedProjection() !== null) projectionCount++;
2226
- }
2227
- return {
2228
- discoveryLatency: performanceTracker.getMetricStats("session_discovery_latency"),
2229
- updateLatency: performanceTracker.getMetricStats("state_update_latency"),
2230
- activeSessions: sessionPanels.size,
2231
- projectionCount
2232
- };
2233
- }
2234
- var cleanupInterval = null;
2235
- function startPerformanceMonitoring() {
2236
- if (cleanupInterval) return;
2237
- cleanupInterval = setInterval(() => {
2238
- cleanupInactiveSessions();
2239
- performanceTracker.takeSnapshot();
2240
- performanceTracker.cleanup();
2241
- }, 6e4);
2242
- }
2243
- function stopPerformanceMonitoring() {
2244
- if (cleanupInterval) {
2245
- clearInterval(cleanupInterval);
2246
- cleanupInterval = null;
2247
- }
2248
- }
2249
- function generatePerformanceReport() {
2250
- return performanceTracker.generateReport();
1983
+ var cleanupSessionPanel = createCleanupSessionPanel(statusCardProjectionRenderer);
1984
+ var cleanupInactiveSessions = createCleanupInactiveSessions(statusCardProjectionRenderer);
1985
+ var getPerformanceStats = createGetPerformanceStats();
1986
+ function startPerformanceMonitoring2() {
1987
+ startPerformanceMonitoring(() => cleanupInactiveSessions());
2251
1988
  }
2252
1989
 
2253
1990
  export {
2254
- normalizeCodexEvent,
2255
- gateCoordinator,
1991
+ getSessionContext,
1992
+ getSessionView,
2256
1993
  setPendingAnswer,
2257
1994
  getPendingAnswers,
2258
1995
  getQuestionCount,
@@ -2260,15 +1997,24 @@ export {
2260
1997
  sendTyping,
2261
1998
  sendAckReaction,
2262
1999
  deliver,
2000
+ sendSystemNotice,
2001
+ spawnSubagent,
2002
+ getSubagents,
2003
+ runSubagentWatchdog,
2004
+ resolveEffectiveClaudePermissionMode,
2005
+ makeModeButtons,
2006
+ dispatchEvent,
2263
2007
  cleanupOldMessages,
2264
2008
  notifyUnmanagedCodexHint,
2009
+ stopPerformanceMonitoring,
2010
+ generatePerformanceReport,
2265
2011
  getSessionProjection,
2266
2012
  initializeSessionPanel,
2267
2013
  registerExistingStatusCard,
2268
2014
  updateSessionState,
2269
2015
  handleResultEvent,
2270
2016
  handleAwaitingHuman,
2271
- relocateSessionPanelToBottom,
2017
+ relocateSessionPanelToBottom2 as relocateSessionPanelToBottom,
2272
2018
  queueDigest,
2273
2019
  getDigestQueue,
2274
2020
  clearDigestQueue,
@@ -2278,7 +2024,5 @@ export {
2278
2024
  cleanupSessionPanel,
2279
2025
  cleanupInactiveSessions,
2280
2026
  getPerformanceStats,
2281
- startPerformanceMonitoring,
2282
- stopPerformanceMonitoring,
2283
- generatePerformanceReport
2027
+ startPerformanceMonitoring2 as startPerformanceMonitoring
2284
2028
  };