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,36 +1,43 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- Aa
4
- } from "./chunk-QWPKAUSV.js";
3
+ E$$
4
+ } from "./chunk-C4L34VJK.js";
5
5
  import {
6
6
  buildDeliveryPlan,
7
7
  cleanupOldMessages,
8
8
  cleanupSessionPanel,
9
9
  deliver,
10
+ dispatchEvent,
10
11
  flushDigest,
11
- gateCoordinator,
12
12
  generatePerformanceReport,
13
13
  getPendingAnswers,
14
14
  getQuestionCount,
15
+ getSessionContext,
15
16
  getSessionProjection,
17
+ getSessionView,
18
+ getSubagents,
16
19
  handleAwaitingHuman,
17
20
  handleResultEvent,
18
21
  initializeSessionPanel,
19
- normalizeCodexEvent,
22
+ makeModeButtons,
20
23
  notifyUnmanagedCodexHint,
21
24
  queueDigest,
22
25
  registerExistingStatusCard,
23
26
  relocateSessionPanelToBottom,
27
+ resolveEffectiveClaudePermissionMode as resolveEffectiveClaudePermissionMode2,
28
+ runSubagentWatchdog,
24
29
  sendAckReaction,
30
+ sendSystemNotice,
25
31
  sendTyping,
26
32
  setPendingAnswer,
33
+ spawnSubagent,
27
34
  startPerformanceMonitoring,
28
35
  stopPerformanceMonitoring,
29
36
  updateSessionState
30
- } from "./chunk-ZAQV2RZS.js";
37
+ } from "./chunk-NNTMVOTM.js";
31
38
  import {
32
39
  registerMessageAttachments
33
- } from "./chunk-L2ZJXV6H.js";
40
+ } from "./chunk-66B64WL3.js";
34
41
  import {
35
42
  addMcpServer,
36
43
  addSkill,
@@ -51,14 +58,14 @@ import {
51
58
  setControlChannelId,
52
59
  setHistoryChannelId,
53
60
  setPersonality
54
- } from "./chunk-GMYN4SYT.js";
61
+ } from "./chunk-SV7EHL3B.js";
55
62
  import {
56
63
  getAllRegisteredProjects
57
- } from "./chunk-54DP53ZK.js";
64
+ } from "./chunk-5EMN2IL5.js";
58
65
  import {
59
66
  buildClaudeSubagentProviderSessionId,
60
67
  registerLocalSession
61
- } from "./chunk-COXPTYH5.js";
68
+ } from "./chunk-PWMEOBXG.js";
62
69
  import {
63
70
  abortSession,
64
71
  abortSessionWithReason,
@@ -66,10 +73,13 @@ import {
66
73
  consumeAbortReason,
67
74
  createSession,
68
75
  debouncedSaveSession,
76
+ drainBatchApprovals,
69
77
  endSession,
78
+ enqueueBatchApproval,
79
+ gateService,
70
80
  getAllSessions,
81
+ getBatchApprovalCount,
71
82
  getOutputPort,
72
- getSession,
73
83
  getSessionByChannel,
74
84
  getSessionByProviderSession,
75
85
  getSessionController,
@@ -78,7 +88,9 @@ import {
78
88
  getSessionsByCategory,
79
89
  loadSessions,
80
90
  markSessionGenerating,
91
+ normalizeCodexEvent,
81
92
  registerOutputPort,
93
+ removeBatchApproval,
82
94
  resolveCodexSessionFromMonitor,
83
95
  resolveEffectiveClaudePermissionMode,
84
96
  resolveEffectiveCodexOptions,
@@ -91,21 +103,26 @@ import {
91
103
  setSessionController,
92
104
  setStatusCardBinding,
93
105
  setVerbose,
106
+ stateMachine,
94
107
  updateSession,
95
108
  updateSessionPermissions,
96
109
  updateWorkflowState
97
- } from "./chunk-RK6EIZOL.js";
110
+ } from "./chunk-JR4B4L7I.js";
98
111
  import {
112
+ JsonFileRepository,
113
+ MonitorRunEnded,
114
+ MonitorRunStarted,
99
115
  ServiceBus,
100
116
  config,
101
117
  formatDuration,
102
118
  formatRelative,
103
119
  formatUptime,
120
+ getDomainBus,
104
121
  intervalService,
105
122
  isAbortError,
106
123
  isUserAllowed,
107
124
  truncate
108
- } from "./chunk-UEX7U2KW.js";
125
+ } from "./chunk-MJ5JKFGS.js";
109
126
  import {
110
127
  __commonJS,
111
128
  __require,
@@ -620,15 +637,43 @@ import cac from "cac";
620
637
  import {
621
638
  Client as Client2,
622
639
  GatewayIntentBits,
623
- ChannelType as ChannelType8
640
+ ChannelType as ChannelType7
624
641
  } from "discord.js";
625
642
 
626
643
  // ../bot/src/bot-services-orchestrator.ts
627
- import { ChannelType as ChannelType4 } from "discord.js";
644
+ import { ChannelType as ChannelType3 } from "discord.js";
628
645
  import { homedir as homedir3 } from "os";
629
646
  import { join as join4 } from "path";
630
647
 
631
648
  // ../providers/src/claude-provider.ts
649
+ function mapClaudeTerminalReason(raw) {
650
+ if (!raw) return void 0;
651
+ switch (raw) {
652
+ case "completed":
653
+ return "completed";
654
+ case "max_turns":
655
+ return "max_turns";
656
+ case "blocking_limit":
657
+ case "rapid_refill_breaker":
658
+ return "rate_limited";
659
+ case "prompt_too_long":
660
+ return "context_too_long";
661
+ case "image_error":
662
+ return "image_error";
663
+ case "model_error":
664
+ return "model_error";
665
+ case "aborted_streaming":
666
+ case "aborted_tools":
667
+ return "aborted";
668
+ case "stop_hook_prevented":
669
+ case "hook_stopped":
670
+ return "hook_stopped";
671
+ case "tool_deferred":
672
+ return "tool_deferred";
673
+ default:
674
+ return "error";
675
+ }
676
+ }
632
677
  var TASK_TOOLS = /* @__PURE__ */ new Set(["TaskCreate", "TaskUpdate", "TaskList", "TaskGet"]);
633
678
  var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".bmp"]);
634
679
  function extractImagePath(toolName, toolInput) {
@@ -681,7 +726,7 @@ var ClaudeProvider = class {
681
726
  let resumeId = options.providerSessionId;
682
727
  while (true) {
683
728
  let failed = false;
684
- const stream = Aa({
729
+ const stream = E$$({
685
730
  prompt: buildQueryPrompt(),
686
731
  options: {
687
732
  cwd: options.directory,
@@ -716,7 +761,7 @@ var ClaudeProvider = class {
716
761
  let resumeId = options.providerSessionId;
717
762
  while (true) {
718
763
  let failed = false;
719
- const stream = Aa({
764
+ const stream = E$$({
720
765
  prompt: "",
721
766
  options: {
722
767
  cwd: options.directory,
@@ -791,13 +836,14 @@ var ClaudeProvider = class {
791
836
  let resultText = "";
792
837
  const toolName = "";
793
838
  if (Array.isArray(content)) {
794
- for (const block of content) {
839
+ for (const rawBlock of content) {
840
+ const block = rawBlock;
795
841
  if (block.type === "tool_result" && block.content) {
796
842
  if (typeof block.content === "string") {
797
843
  resultText += block.content;
798
844
  } else if (Array.isArray(block.content)) {
799
845
  for (const sub of block.content) {
800
- if (sub.type === "text") resultText += sub.text;
846
+ if (sub.type === "text" && sub.text) resultText += sub.text;
801
847
  }
802
848
  }
803
849
  }
@@ -845,7 +891,8 @@ var ClaudeProvider = class {
845
891
  costUsd: r.total_cost_usd ?? 0,
846
892
  durationMs: r.duration_ms ?? 0,
847
893
  numTurns: r.num_turns ?? 0,
848
- errors: r.errors ?? []
894
+ errors: r.errors ?? [],
895
+ terminalReason: mapClaudeTerminalReason(r.terminal_reason) ?? (r.subtype === "success" ? "completed" : "error")
849
896
  };
850
897
  }
851
898
  }
@@ -858,7 +905,7 @@ providers.set("claude", new ClaudeProvider());
858
905
  var codexLoadAttempted = false;
859
906
  async function loadCodexProvider() {
860
907
  try {
861
- const { CodexProvider } = await import("./codex-provider-NYI7KBGO.js");
908
+ const { CodexProvider } = await import("./codex-provider-VLOS5QB6.js");
862
909
  providers.set("codex", new CodexProvider());
863
910
  codexLoadAttempted = true;
864
911
  } catch (err) {
@@ -1017,15 +1064,18 @@ function buildMonitorSystemPromptParts(session) {
1017
1064
  // ../engine/src/session/provider-runtime.ts
1018
1065
  function buildProviderOptions(session, controller, isMonitor = false, runtimeOverrides = {}) {
1019
1066
  const effectiveCodex = resolveEffectiveCodexOptions(session);
1067
+ const monitorEffort = isMonitor ? config.monitorReasoningEffort || void 0 : void 0;
1068
+ const useMonitorJudgeModel = isMonitor && session.provider === "claude" && (monitorEffort === "high" || monitorEffort === "xhigh");
1069
+ const model = useMonitorJudgeModel ? config.monitorClaudeModel || session.model : session.model;
1020
1070
  return {
1021
1071
  directory: session.directory,
1022
1072
  providerSessionId: isMonitor ? session.monitorProviderSessionId : session.providerSessionId,
1023
- model: session.model,
1073
+ model,
1024
1074
  sandboxMode: effectiveCodex.sandboxMode,
1025
1075
  approvalPolicy: effectiveCodex.approvalPolicy,
1026
1076
  networkAccessEnabled: effectiveCodex.networkAccessEnabled,
1027
1077
  webSearchMode: effectiveCodex.webSearchMode,
1028
- modelReasoningEffort: config.codexReasoningEffort || void 0,
1078
+ modelReasoningEffort: monitorEffort ?? (config.codexReasoningEffort || void 0),
1029
1079
  claudePermissionMode: resolveEffectiveClaudePermissionMode(session),
1030
1080
  systemPromptParts: isMonitor ? buildMonitorSystemPromptParts(session) : buildSystemPromptParts(session),
1031
1081
  abortController: controller,
@@ -1033,34 +1083,29 @@ function buildProviderOptions(session, controller, isMonitor = false, runtimeOve
1033
1083
  };
1034
1084
  }
1035
1085
  async function* sendPrompt(sessionId, prompt, runtimeOverrides = {}) {
1036
- const session = getSession(sessionId);
1037
- if (!session) throw new Error(`Session "${sessionId}" not found`);
1038
- if (session.isGenerating) throw new Error("Session is already generating");
1086
+ const ctx = getSessionContext(sessionId);
1087
+ if (!ctx) throw new Error(`Session "${sessionId}" not found`);
1088
+ if (ctx.session.isGenerating) throw new Error("Session is already generating");
1039
1089
  const controller = new AbortController();
1040
- setSessionController(session.id, controller);
1041
- markSessionGenerating(session.id, true);
1042
- const provider = await ensureProvider(session.provider);
1090
+ setSessionController(ctx.sessionId, controller);
1091
+ markSessionGenerating(ctx.sessionId, true);
1092
+ const provider = await ensureProvider(ctx.session.provider);
1043
1093
  try {
1044
1094
  const stream = provider.sendPrompt(
1045
1095
  prompt,
1046
- buildProviderOptions(session, controller, false, runtimeOverrides)
1096
+ buildProviderOptions(ctx.session, controller, false, runtimeOverrides)
1047
1097
  );
1048
1098
  for await (const event of stream) {
1049
1099
  if (event.type === "session_init") {
1050
- const current2 = getSession(sessionId);
1051
- if (current2) {
1052
- current2.providerSessionId = event.providerSessionId || void 0;
1053
- debouncedSaveSession();
1054
- }
1100
+ ctx.session.providerSessionId = event.providerSessionId || void 0;
1101
+ ctx.save();
1055
1102
  }
1056
1103
  if (event.type === "result") {
1057
- const current2 = getSession(sessionId);
1058
- if (current2) current2.totalCost += event.costUsd;
1104
+ ctx.session.totalCost += event.costUsd;
1059
1105
  }
1060
1106
  yield event;
1061
1107
  }
1062
- const current = getSession(sessionId);
1063
- if (current) current.messageCount++;
1108
+ ctx.session.messageCount++;
1064
1109
  } catch (err) {
1065
1110
  if (!isAbortError(err)) {
1066
1111
  throw err;
@@ -1072,39 +1117,34 @@ async function* sendPrompt(sessionId, prompt, runtimeOverrides = {}) {
1072
1117
  await saveSessionImmediate();
1073
1118
  } catch (cleanupErr) {
1074
1119
  console.error(`[ProviderRuntime] cleanup error for session ${sessionId}:`, cleanupErr);
1075
- const s = getSession(sessionId);
1076
- if (s) s.isGenerating = false;
1120
+ const fallback = getSessionContext(sessionId);
1121
+ if (fallback) fallback.session.isGenerating = false;
1077
1122
  }
1078
1123
  }
1079
1124
  }
1080
1125
  async function* continueSessionWithOverrides(sessionId, runtimeOverrides = {}) {
1081
- const session = getSession(sessionId);
1082
- if (!session) throw new Error(`Session "${sessionId}" not found`);
1083
- if (session.isGenerating) throw new Error("Session is already generating");
1126
+ const ctx = getSessionContext(sessionId);
1127
+ if (!ctx) throw new Error(`Session "${sessionId}" not found`);
1128
+ if (ctx.session.isGenerating) throw new Error("Session is already generating");
1084
1129
  const controller = new AbortController();
1085
- setSessionController(session.id, controller);
1086
- markSessionGenerating(session.id, true);
1087
- const provider = await ensureProvider(session.provider);
1130
+ setSessionController(ctx.sessionId, controller);
1131
+ markSessionGenerating(ctx.sessionId, true);
1132
+ const provider = await ensureProvider(ctx.session.provider);
1088
1133
  try {
1089
1134
  const stream = provider.continueSession(
1090
- buildProviderOptions(session, controller, false, runtimeOverrides)
1135
+ buildProviderOptions(ctx.session, controller, false, runtimeOverrides)
1091
1136
  );
1092
1137
  for await (const event of stream) {
1093
1138
  if (event.type === "session_init") {
1094
- const current2 = getSession(sessionId);
1095
- if (current2) {
1096
- current2.providerSessionId = event.providerSessionId || void 0;
1097
- debouncedSaveSession();
1098
- }
1139
+ ctx.session.providerSessionId = event.providerSessionId || void 0;
1140
+ ctx.save();
1099
1141
  }
1100
1142
  if (event.type === "result") {
1101
- const current2 = getSession(sessionId);
1102
- if (current2) current2.totalCost += event.costUsd;
1143
+ ctx.session.totalCost += event.costUsd;
1103
1144
  }
1104
1145
  yield event;
1105
1146
  }
1106
- const current = getSession(sessionId);
1107
- if (current) current.messageCount++;
1147
+ ctx.session.messageCount++;
1108
1148
  } catch (err) {
1109
1149
  if (!isAbortError(err)) {
1110
1150
  throw err;
@@ -1116,39 +1156,33 @@ async function* continueSessionWithOverrides(sessionId, runtimeOverrides = {}) {
1116
1156
  await saveSessionImmediate();
1117
1157
  } catch (cleanupErr) {
1118
1158
  console.error(`[ProviderRuntime] cleanup error for session ${sessionId}:`, cleanupErr);
1119
- const s = getSession(sessionId);
1120
- if (s) s.isGenerating = false;
1159
+ const fallback = getSessionContext(sessionId);
1160
+ if (fallback) fallback.session.isGenerating = false;
1121
1161
  }
1122
1162
  }
1123
1163
  }
1124
1164
  async function* sendMonitorPrompt(sessionId, prompt) {
1125
- const session = getSession(sessionId);
1126
- if (!session) throw new Error(`Session "${sessionId}" not found`);
1127
- const provider = await ensureProvider(session.provider);
1128
- const current = getSession(sessionId);
1129
- if (current) current.lastActivity = Date.now();
1165
+ const ctx = getSessionContext(sessionId);
1166
+ if (!ctx) throw new Error(`Session "${sessionId}" not found`);
1167
+ const provider = await ensureProvider(ctx.session.provider);
1168
+ ctx.session.lastActivity = Date.now();
1130
1169
  const mainController = getSessionController(sessionId);
1131
1170
  const controller = new AbortController();
1132
1171
  const onMainAbort = () => controller.abort();
1133
1172
  mainController?.signal.addEventListener("abort", onMainAbort, { once: true });
1134
1173
  try {
1135
- const stream = provider.sendPrompt(prompt, buildProviderOptions(session, controller, true));
1174
+ const stream = provider.sendPrompt(prompt, buildProviderOptions(ctx.session, controller, true));
1136
1175
  for await (const event of stream) {
1137
1176
  if (event.type === "session_init") {
1138
- const cur2 = getSession(sessionId);
1139
- if (cur2) {
1140
- cur2.monitorProviderSessionId = event.providerSessionId || void 0;
1141
- debouncedSaveSession();
1142
- }
1177
+ ctx.session.monitorProviderSessionId = event.providerSessionId || void 0;
1178
+ ctx.save();
1143
1179
  }
1144
1180
  if (event.type === "result") {
1145
- const cur2 = getSession(sessionId);
1146
- if (cur2) cur2.totalCost += event.costUsd;
1181
+ ctx.session.totalCost += event.costUsd;
1147
1182
  }
1148
1183
  yield event;
1149
1184
  }
1150
- const cur = getSession(sessionId);
1151
- if (cur) cur.lastActivity = Date.now();
1185
+ ctx.session.lastActivity = Date.now();
1152
1186
  debouncedSaveSession();
1153
1187
  } catch (err) {
1154
1188
  if (!isAbortError(err)) {
@@ -1161,7 +1195,21 @@ async function* sendMonitorPrompt(sessionId, prompt) {
1161
1195
 
1162
1196
  // ../engine/src/executor/permission-gate.ts
1163
1197
  function refreshSession(session) {
1164
- return getSession(session.id) ?? session;
1198
+ return getSessionView(session.id) ?? session;
1199
+ }
1200
+ async function recordPermissionDenial(session, toolName, reason, source) {
1201
+ await getOutputPort().updateState(session.id, {
1202
+ type: "permission_denied",
1203
+ sessionId: session.id,
1204
+ source: session.provider === "codex" ? "codex" : "claude",
1205
+ confidence: "high",
1206
+ timestamp: Date.now(),
1207
+ metadata: { toolName, reason, source }
1208
+ });
1209
+ getOutputPort().queueDigest(session.id, {
1210
+ kind: "denied",
1211
+ text: `\u26D4 \u6743\u9650\u62D2\u7EDD\uFF1A${truncate(toolName, 40)} \u2014 ${truncate(reason, 80)}`
1212
+ });
1165
1213
  }
1166
1214
  function waitForGateResolution(session, gateId) {
1167
1215
  console.log(`[SessionExecutor] gate:waiting sessionId=${session.id} gateId=${gateId}`);
@@ -1175,7 +1223,7 @@ function waitForGateResolution(session, gateId) {
1175
1223
  );
1176
1224
  resolve2(result);
1177
1225
  };
1178
- gateCoordinator.registerReceiptHandle(gateId, {
1226
+ gateService.registerReceiptHandle(gateId, {
1179
1227
  type: session.provider === "codex" ? "codex" : "claude",
1180
1228
  sessionId: session.id,
1181
1229
  resolve: (action, source) => settle({ action, source }),
@@ -1202,6 +1250,80 @@ function createClaudePermissionHandler(session, _channel) {
1202
1250
  console.log(
1203
1251
  `[SessionExecutor] permission:request sessionId=${liveSession.id} tool=${toolName} action=${context.displayName || toolName}`
1204
1252
  );
1253
+ const projection = stateMachine.getSnapshot(liveSession.id);
1254
+ if (projection.batchApprovalMode) {
1255
+ const gateId2 = `batch-${liveSession.id}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
1256
+ const timestamp = Date.now();
1257
+ const displayName = context.displayName || toolName;
1258
+ const batchAction = await new Promise((resolve2) => {
1259
+ let settled = false;
1260
+ const settle = (action) => {
1261
+ if (settled) return;
1262
+ settled = true;
1263
+ resolve2(action);
1264
+ };
1265
+ const enqueueResult = enqueueBatchApproval(liveSession.id, {
1266
+ gateId: gateId2,
1267
+ toolUseID: context.toolUseID,
1268
+ toolName: displayName,
1269
+ detail,
1270
+ timestamp,
1271
+ resolve: settle
1272
+ });
1273
+ if (enqueueResult === "overflow") {
1274
+ console.warn(
1275
+ `[SessionExecutor] permission:batch-overflow sessionId=${liveSession.id} tool=${toolName} reason="queue at capacity"`
1276
+ );
1277
+ settle("reject");
1278
+ return;
1279
+ }
1280
+ stateMachine.enqueuePendingApproval(liveSession.id, {
1281
+ gateId: gateId2,
1282
+ toolName: displayName,
1283
+ detail,
1284
+ timestamp
1285
+ });
1286
+ void getOutputPort().updateState(liveSession.id, {
1287
+ type: "batch_approval_changed",
1288
+ sessionId: liveSession.id,
1289
+ source: liveSession.provider === "codex" ? "codex" : "claude",
1290
+ confidence: "high",
1291
+ timestamp,
1292
+ metadata: {
1293
+ enabled: true,
1294
+ pendingApprovals: stateMachine.getSnapshot(liveSession.id).pendingApprovals
1295
+ }
1296
+ }).catch(() => {
1297
+ });
1298
+ getOutputPort().queueDigest(liveSession.id, {
1299
+ kind: "batch",
1300
+ text: `\u5DF2\u5165\u6279\u91CF\u5BA1\u6279\u961F\u5217\uFF1A${truncate(toolName, 40)}`
1301
+ });
1302
+ const onAbort = () => {
1303
+ settle("reject");
1304
+ removeBatchApproval(liveSession.id, gateId2);
1305
+ stateMachine.removePendingApproval(liveSession.id, gateId2);
1306
+ };
1307
+ if (context.signal.aborted) onAbort();
1308
+ else context.signal.addEventListener("abort", onAbort, { once: true });
1309
+ });
1310
+ if (batchAction === "approve") {
1311
+ console.log(
1312
+ `[SessionExecutor] permission:batch-approved sessionId=${liveSession.id} tool=${toolName}`
1313
+ );
1314
+ return { behavior: "allow", toolUseID: context.toolUseID };
1315
+ }
1316
+ console.log(
1317
+ `[SessionExecutor] permission:batch-rejected sessionId=${liveSession.id} tool=${toolName}`
1318
+ );
1319
+ await recordPermissionDenial(liveSession, toolName, "\u6279\u91CF\u62D2\u7EDD", "user");
1320
+ return {
1321
+ behavior: "deny",
1322
+ message: "\u6279\u91CF\u62D2\u7EDD",
1323
+ interrupt: true,
1324
+ toolUseID: context.toolUseID
1325
+ };
1326
+ }
1205
1327
  await getOutputPort().handleAwaitingHuman(liveSession.id, detail, { source: "claude" });
1206
1328
  const currentSession = refreshSession(liveSession);
1207
1329
  const gateId = currentSession.activeHumanGateId;
@@ -1242,9 +1364,11 @@ function createClaudePermissionHandler(session, _channel) {
1242
1364
  if (resolved.action === "approve") {
1243
1365
  return { behavior: "allow", toolUseID: context.toolUseID };
1244
1366
  }
1367
+ const denyReason = resolved.source === "timeout" ? "\u5BA1\u6279\u8D85\u65F6\uFF085 \u5206\u949F\uFF09" : resolved.source === "terminal" ? "\u5DF2\u5728\u7EC8\u7AEF\u62D2\u7EDD" : "\u5DF2\u5728 Discord \u62D2\u7EDD";
1368
+ await recordPermissionDenial(currentSession, toolName, denyReason, resolved.source);
1245
1369
  return {
1246
1370
  behavior: "deny",
1247
- message: resolved.source === "timeout" ? "\u5BA1\u6279\u8D85\u65F6\uFF085 \u5206\u949F\uFF09" : resolved.source === "terminal" ? "\u5DF2\u5728\u7EC8\u7AEF\u62D2\u7EDD" : "\u5DF2\u5728 Discord \u62D2\u7EDD",
1371
+ message: denyReason,
1248
1372
  interrupt: true,
1249
1373
  toolUseID: context.toolUseID
1250
1374
  };
@@ -1259,7 +1383,7 @@ function shouldUseClaudePermissionHandler(session) {
1259
1383
 
1260
1384
  // ../engine/src/executor/session-hooks.ts
1261
1385
  function refreshSession2(session) {
1262
- return getSession(session.id) ?? session;
1386
+ return getSessionView(session.id) ?? session;
1263
1387
  }
1264
1388
  function applyWorkflowHook(session, hook, patch = {}) {
1265
1389
  return updateWorkflowState(session.id, (current) => ({
@@ -1569,6 +1693,100 @@ function parseAskUserDecision(text) {
1569
1693
  }
1570
1694
  }
1571
1695
 
1696
+ // ../engine/src/executor/monitor-run-store.ts
1697
+ var monitorRepo = new JsonFileRepository({
1698
+ filename: "monitor-runs.json",
1699
+ idField: "id",
1700
+ debounceMs: 0
1701
+ });
1702
+ var initialized = false;
1703
+ async function ensureInit() {
1704
+ if (initialized) return;
1705
+ await monitorRepo.init();
1706
+ initialized = true;
1707
+ }
1708
+ function makeId(sessionId, startedAt) {
1709
+ const suffix = Math.random().toString(36).slice(2, 8);
1710
+ return `${sessionId}:${startedAt}:${suffix}`;
1711
+ }
1712
+ async function beginMonitorRun(params) {
1713
+ await ensureInit();
1714
+ const now = Date.now();
1715
+ const stale = monitorRepo.find({
1716
+ where: { sessionId: params.sessionId, status: "running" }
1717
+ });
1718
+ for (const old of stale) {
1719
+ await monitorRepo.update(old.id, { status: "abandoned", lastCheckpointAt: now });
1720
+ }
1721
+ const run = {
1722
+ id: makeId(params.sessionId, now),
1723
+ sessionId: params.sessionId,
1724
+ goal: params.goal,
1725
+ iteration: 0,
1726
+ maxIterations: params.maxIterations,
1727
+ status: "running",
1728
+ startedAt: now,
1729
+ lastCheckpointAt: now
1730
+ };
1731
+ await monitorRepo.save(run);
1732
+ getDomainBus().emit(
1733
+ MonitorRunStarted,
1734
+ {
1735
+ sessionId: run.sessionId,
1736
+ runId: run.id,
1737
+ goal: run.goal,
1738
+ maxIterations: run.maxIterations
1739
+ },
1740
+ "monitor-run-store"
1741
+ );
1742
+ return run;
1743
+ }
1744
+ async function checkpointMonitorRun(runId, patch) {
1745
+ await ensureInit();
1746
+ return monitorRepo.update(runId, { ...patch, lastCheckpointAt: Date.now() });
1747
+ }
1748
+ async function finishMonitorRun(runId, status, finalPatch = {}) {
1749
+ await ensureInit();
1750
+ const updated = await monitorRepo.update(runId, {
1751
+ ...finalPatch,
1752
+ status,
1753
+ lastCheckpointAt: Date.now()
1754
+ });
1755
+ if (updated) {
1756
+ getDomainBus().emit(
1757
+ MonitorRunEnded,
1758
+ {
1759
+ sessionId: updated.sessionId,
1760
+ runId: updated.id,
1761
+ status,
1762
+ iteration: updated.iteration,
1763
+ rationale: updated.lastRationale
1764
+ },
1765
+ "monitor-run-store"
1766
+ );
1767
+ }
1768
+ }
1769
+ async function reconcileMonitorRunsOnStartup() {
1770
+ await ensureInit();
1771
+ const running = monitorRepo.find({ where: { status: "running" } });
1772
+ const now = Date.now();
1773
+ for (const run of running) {
1774
+ await monitorRepo.update(run.id, { status: "abandoned", lastCheckpointAt: now });
1775
+ getDomainBus().emit(
1776
+ MonitorRunEnded,
1777
+ {
1778
+ sessionId: run.sessionId,
1779
+ runId: run.id,
1780
+ status: "abandoned",
1781
+ iteration: run.iteration,
1782
+ rationale: run.lastRationale
1783
+ },
1784
+ "monitor-run-store"
1785
+ );
1786
+ }
1787
+ return running;
1788
+ }
1789
+
1572
1790
  // ../engine/src/executor/monitor-loop.ts
1573
1791
  var MAX_MONITOR_ITERATIONS = 6;
1574
1792
  async function runMonitorDecision(session, goal, workerResult, iteration) {
@@ -1786,6 +2004,16 @@ async function runMonitorLoop(session, channel, goal, initialResult, runWorkerPa
1786
2004
  console.log(
1787
2005
  `[SessionExecutor] monitor:loop-start sessionId=${session.id} goal=${truncate(goal, 80)}`
1788
2006
  );
2007
+ const monitorRun = await beginMonitorRun({
2008
+ sessionId: session.id,
2009
+ goal,
2010
+ maxIterations: MAX_MONITOR_ITERATIONS
2011
+ }).catch((err) => {
2012
+ console.warn(
2013
+ `[SessionExecutor] failed to persist MonitorRun for session ${session.id}: ${err.message}`
2014
+ );
2015
+ return null;
2016
+ });
1789
2017
  let workerResult = initialResult;
1790
2018
  let currentSession = refreshSession2(session);
1791
2019
  for (let iteration = 1; iteration <= MAX_MONITOR_ITERATIONS; iteration++) {
@@ -1855,6 +2083,13 @@ async function runMonitorLoop(session, channel, goal, initialResult, runWorkerPa
1855
2083
  });
1856
2084
  const summary = decision.completionSummary || decision.rationale || "The monitor judged the request complete.";
1857
2085
  await getOutputPort().handleResult(currentSession.id, createSyntheticResult(true, summary), summary);
2086
+ if (monitorRun) {
2087
+ await finishMonitorRun(monitorRun.id, "completed", {
2088
+ iteration,
2089
+ lastRationale: decision.rationale
2090
+ }).catch(() => {
2091
+ });
2092
+ }
1858
2093
  console.log(
1859
2094
  `[SessionExecutor] monitor:complete sessionId=${currentSession.id} iteration=${iteration} rationale=${truncate(decision.rationale, 80)}`
1860
2095
  );
@@ -1875,11 +2110,26 @@ async function runMonitorLoop(session, channel, goal, initialResult, runWorkerPa
1875
2110
  await getOutputPort().handleAwaitingHuman(currentSession.id, blocker, {
1876
2111
  source: currentSession.provider === "codex" ? "codex" : "claude"
1877
2112
  });
2113
+ if (monitorRun) {
2114
+ await finishMonitorRun(monitorRun.id, "blocked", {
2115
+ iteration,
2116
+ lastRationale: decision.rationale
2117
+ }).catch(() => {
2118
+ });
2119
+ }
1878
2120
  console.warn(
1879
2121
  `[SessionExecutor] monitor:blocked sessionId=${currentSession.id} iteration=${iteration} reason=${truncate(decision.rationale, 80)}`
1880
2122
  );
1881
2123
  return;
1882
2124
  }
2125
+ if (monitorRun) {
2126
+ await checkpointMonitorRun(monitorRun.id, {
2127
+ iteration,
2128
+ lastRationale: decision.rationale,
2129
+ lastWorkerSummary: summarizeWorkerPass(buildWorkerProgressReport(goal, workerResult))
2130
+ }).catch(() => {
2131
+ });
2132
+ }
1883
2133
  await updatePanelState(currentSession, "work_started", channel);
1884
2134
  getOutputPort().queueDigest(currentSession.id, {
1885
2135
  kind: "monitor",
@@ -1926,6 +2176,13 @@ async function runMonitorLoop(session, channel, goal, initialResult, runWorkerPa
1926
2176
  await getOutputPort().handleAwaitingHuman(currentSession.id, limitSummary, {
1927
2177
  source: currentSession.provider === "codex" ? "codex" : "claude"
1928
2178
  });
2179
+ if (monitorRun) {
2180
+ await finishMonitorRun(monitorRun.id, "failed", {
2181
+ iteration: MAX_MONITOR_ITERATIONS,
2182
+ lastRationale: "Reached the continuation safety limit."
2183
+ }).catch(() => {
2184
+ });
2185
+ }
1929
2186
  console.warn(
1930
2187
  `[SessionExecutor] monitor:limit-reached sessionId=${currentSession.id} iterations=${MAX_MONITOR_ITERATIONS}`
1931
2188
  );
@@ -2031,7 +2288,7 @@ async function executeSessionPrompt(session, channel, prompt, options = {}) {
2031
2288
  }
2032
2289
  if ((options.updateMonitorGoal ?? true) && goalText && !session.monitorGoal) {
2033
2290
  setMonitorGoal(session.id, goalText);
2034
- session = getSession(session.id) ?? session;
2291
+ session = getSessionView(session.id) ?? session;
2035
2292
  }
2036
2293
  const goal = session.monitorGoal || goalText;
2037
2294
  if (!goal) {
@@ -2141,166 +2398,31 @@ function getExpandableContent(id) {
2141
2398
  return expandableStore.get(id)?.content;
2142
2399
  }
2143
2400
 
2144
- // ../bot/src/output-handler.ts
2145
- import { existsSync } from "fs";
2146
-
2147
- // ../bot/src/codex-renderer.ts
2148
- import { EmbedBuilder } from "discord.js";
2149
-
2150
- // ../bot/src/subagent-manager.ts
2151
- import {
2152
- ChannelType,
2153
- ThreadAutoArchiveDuration
2154
- } from "discord.js";
2155
-
2156
- // ../bot/src/discord/delivery-notices.ts
2157
- function getDeliveryPolicy() {
2158
- return {
2159
- textChunkLimit: config.textChunkLimit ?? 2e3,
2160
- chunkMode: config.chunkMode ?? "length",
2161
- replyToMode: config.replyToMode ?? "first",
2162
- ackReaction: config.ackReaction ?? "\u{1F440}"
2163
- };
2164
- }
2165
- async function sendSystemNotice(channel, sessionId, text, replyToMessageId) {
2166
- if (!text.trim()) return;
2167
- const plan = buildDeliveryPlan({
2168
- sessionId,
2169
- chatId: channel.id,
2170
- text,
2171
- files: [],
2172
- mode: "system_notice",
2173
- replyToMessageId,
2174
- policy: getDeliveryPolicy()
2175
- });
2176
- try {
2177
- await deliver(channel, plan);
2178
- } catch {
2179
- }
2180
- }
2181
-
2182
- // ../bot/src/subagent-manager.ts
2183
- var SUBAGENT_IDLE_TIMEOUT_MS = 60 * 60 * 1e3;
2184
- function canSpawnSubagent(parentSession) {
2185
- return parentSession.subagentDepth < config.maxSubagentDepth;
2186
- }
2187
- async function spawnSubagent(parentSession, label, provider, sessionChannel) {
2188
- if (!canSpawnSubagent(parentSession)) {
2189
- console.warn(`[SubagentManager] Depth limit hit for session ${parentSession.id} (max depth ${config.maxSubagentDepth})`);
2190
- throw new Error(
2191
- `Max subagent depth (${config.maxSubagentDepth}) reached. Cannot spawn further subagents.`
2192
- );
2401
+ // ../engine/src/executor/monitor-autoresume.ts
2402
+ async function reconcileAndCollectAutoResumeCandidates(policy) {
2403
+ const abandoned = await reconcileMonitorRunsOnStartup();
2404
+ if (policy === "abandon-only" || abandoned.length === 0) {
2405
+ return { abandoned, candidates: [] };
2193
2406
  }
2194
- const threadName = `[sub:${provider}] ${label}`.slice(0, 100);
2195
- const thread = await sessionChannel.threads.create({
2196
- name: threadName,
2197
- type: ChannelType.PublicThread,
2198
- autoArchiveDuration: ThreadAutoArchiveDuration.OneHour,
2199
- reason: `Subagent spawned by session ${parentSession.id}`
2200
- });
2201
- const session = await createSession({
2202
- channelId: thread.id,
2203
- // Subagent's primary ID is the Thread
2204
- categoryId: parentSession.categoryId,
2205
- projectName: parentSession.projectName,
2206
- agentLabel: label,
2207
- provider,
2208
- directory: parentSession.directory,
2209
- type: "subagent",
2210
- parentChannelId: parentSession.channelId,
2211
- // Parent session's TextChannel
2212
- subagentDepth: parentSession.subagentDepth + 1,
2213
- mode: parentSession.mode,
2214
- claudePermissionMode: provider === "claude" ? parentSession.claudePermissionMode : void 0
2215
- });
2216
- console.log(`[SubagentManager] Spawned subagent "${label}" session ${session.id} thread ${thread.id} (depth ${session.subagentDepth}, parent ${parentSession.id})`);
2217
- return session;
2218
- }
2219
- async function archiveSubagent(session, thread, summary) {
2220
- if (summary) {
2221
- await sendSystemNotice(thread, session.id, `*\u5B50\u4EFB\u52A1\u5B8C\u6210\uFF1A${summary}*`);
2222
- }
2223
- try {
2224
- await thread.setArchived(true, "Subagent task completed");
2225
- } catch (error) {
2226
- console.warn(`[SubagentManager] Failed to archive thread ${thread.id} for subagent ${session.id}: ${error instanceof Error ? error.message : String(error)}`);
2227
- }
2228
- try {
2229
- await endSession(session.id);
2230
- console.log(`[SubagentManager] Archived subagent ${session.id} thread ${thread.id}${summary ? ` \u2014 ${summary}` : ""}`);
2231
- } catch (error) {
2232
- console.warn(`[SubagentManager] Failed to end session for subagent ${session.id}: ${error instanceof Error ? error.message : String(error)}`);
2233
- }
2234
- }
2235
- function getSubagents(parentSession) {
2236
- return getAllSessions().filter(
2237
- (s) => s.type === "subagent" && s.parentChannelId === parentSession.channelId
2238
- );
2239
- }
2240
- async function runSubagentWatchdog(getThread) {
2241
- const now = Date.now();
2242
- const checked = /* @__PURE__ */ new Set();
2243
- let archived = 0;
2244
- let errors = 0;
2245
- for (const session of getSubagentSessions()) {
2246
- if (checked.has(session.id)) continue;
2247
- checked.add(session.id);
2248
- const idle = now - session.lastActivity;
2249
- if (idle < SUBAGENT_IDLE_TIMEOUT_MS) continue;
2407
+ const candidates = [];
2408
+ for (const run of abandoned) {
2409
+ const session = getSessionView(run.sessionId);
2410
+ if (!session) continue;
2411
+ if (session.mode !== "monitor") continue;
2250
2412
  if (session.isGenerating) continue;
2251
- const thread = getThread(session.channelId);
2252
- if (!thread) {
2253
- await endSession(session.id).catch((e) => console.warn(`[SubagentWatchdog] Failed to end orphaned subagent ${session.id}: ${e.message}`));
2254
- console.log(`[SubagentWatchdog] Ended orphaned subagent ${session.id} \u2014 thread ${session.channelId} not found`);
2255
- continue;
2256
- }
2257
- try {
2258
- await archiveSubagent(session, thread, "Idle timeout reached.");
2259
- archived += 1;
2260
- } catch (error) {
2261
- console.error(`[SubagentWatchdog] Failed to archive idle subagent ${session.id}: ${error instanceof Error ? error.message : String(error)}`);
2262
- errors += 1;
2263
- }
2264
- }
2265
- console.log(`[SubagentWatchdog] Watchdog run: checked ${checked.size} subagents, archived ${archived}, errors ${errors}`);
2266
- }
2267
- async function autoSpawnSubagentThread(parentSession, taskId, description, sessionChannel) {
2268
- if (parentSession.type === "subagent") return null;
2269
- if (!canSpawnSubagent(parentSession)) return null;
2270
- const threadName = `[task] ${description}`.slice(0, 100);
2271
- const thread = await sessionChannel.threads.create({
2272
- name: threadName,
2273
- type: ChannelType.PublicThread,
2274
- autoArchiveDuration: ThreadAutoArchiveDuration.OneHour,
2275
- reason: `Auto-created for task ${taskId} in session ${parentSession.id}`
2276
- });
2277
- let session;
2278
- try {
2279
- session = await createSession({
2280
- channelId: thread.id,
2281
- categoryId: parentSession.categoryId,
2282
- projectName: parentSession.projectName,
2283
- agentLabel: description,
2284
- provider: parentSession.provider,
2285
- directory: parentSession.directory,
2286
- type: "subagent",
2287
- parentChannelId: parentSession.channelId,
2288
- subagentDepth: (parentSession.subagentDepth || 0) + 1,
2289
- mode: parentSession.mode,
2290
- claudePermissionMode: parentSession.provider === "claude" ? parentSession.claudePermissionMode : void 0
2413
+ const goal = session.monitorGoal || run.goal;
2414
+ if (!goal) continue;
2415
+ candidates.push({
2416
+ sessionId: session.id,
2417
+ channelId: session.channelId,
2418
+ runId: run.id,
2419
+ goal,
2420
+ lastIteration: run.iteration,
2421
+ lastRationale: run.lastRationale,
2422
+ abandonedRun: run
2291
2423
  });
2292
- } catch (err) {
2293
- console.warn(`[SubagentManager] createSession failed for auto-spawned task ${taskId}, deleting orphan thread ${thread.id}: ${err instanceof Error ? err.message : String(err)}`);
2294
- await thread.delete("Session creation failed").catch(
2295
- (deleteErr) => console.warn(`[SubagentManager] Failed to delete orphan thread ${thread.id}: ${deleteErr instanceof Error ? deleteErr.message : String(deleteErr)}`)
2296
- );
2297
- throw err;
2298
2424
  }
2299
- console.log(`[SubagentManager] Auto-spawned thread for task ${taskId} session ${session.id} thread ${thread.id}`);
2300
- return { threadId: thread.id, session };
2301
- }
2302
- function getSubagentSessions() {
2303
- return getAllSessions().filter((s) => s.type === "subagent");
2425
+ return { abandoned, candidates };
2304
2426
  }
2305
2427
 
2306
2428
  // ../bot/src/output/message-streamer.ts
@@ -2431,67 +2553,30 @@ var MessageStreamer = class {
2431
2553
  }
2432
2554
  };
2433
2555
 
2434
- // ../bot/src/output/interaction-controls.ts
2435
- import {
2436
- ActionRowBuilder,
2437
- ButtonBuilder,
2438
- ButtonStyle,
2439
- StringSelectMenuBuilder,
2440
- EmbedBuilder as EmbedBuilder2
2441
- } from "discord.js";
2442
- function resolveEffectiveClaudePermissionMode2(currentMode, claudePermissionMode) {
2443
- if (!claudePermissionMode) return void 0;
2444
- return currentMode === "auto" ? "bypass" : claudePermissionMode;
2445
- }
2446
- function makeModeButtons(sessionId, currentMode, claudePermissionMode) {
2447
- const modes = [
2448
- { id: "auto", label: "\u26A1 \u81EA\u52A8\u6A21\u5F0F" },
2449
- { id: "plan", label: "\u{1F4CB} \u8BA1\u5212\u6A21\u5F0F" },
2450
- { id: "normal", label: "\u{1F6E1}\uFE0F \u666E\u901A\u6A21\u5F0F" },
2451
- { id: "monitor", label: "\u{1F9E0} \u76D1\u63A7\u6A21\u5F0F" }
2452
- ];
2453
- const row = new ActionRowBuilder();
2454
- for (const m of modes) {
2455
- row.addComponents(
2456
- 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)
2457
- );
2458
- }
2459
- const effectiveClaudePermissionMode = resolveEffectiveClaudePermissionMode2(
2460
- currentMode,
2461
- claudePermissionMode
2462
- );
2463
- if (effectiveClaudePermissionMode) {
2464
- const permLabel = effectiveClaudePermissionMode === "bypass" ? "\u26A1 \u7ED5\u8FC7\u6743\u9650" : "\u{1F6E1}\uFE0F \u9700\u8981\u786E\u8BA4";
2465
- row.addComponents(
2466
- new ButtonBuilder().setCustomId(`perm-info:${sessionId}`).setLabel(permLabel).setStyle(
2467
- effectiveClaudePermissionMode === "bypass" ? ButtonStyle.Danger : ButtonStyle.Success
2468
- ).setDisabled(true)
2469
- );
2470
- }
2471
- return row;
2472
- }
2473
- function shouldSuppressCommandExecution(command) {
2474
- return command.toLowerCase().includes("total-recall");
2475
- }
2476
-
2477
2556
  // ../bot/src/output-handler.ts
2557
+ var DIGEST_FLUSH_INTERVAL_MS = 8e3;
2558
+ function createInitialState() {
2559
+ return {
2560
+ askedUser: false,
2561
+ hadError: false,
2562
+ success: null,
2563
+ commandCount: 0,
2564
+ fileChangeCount: 0,
2565
+ recentCommands: [],
2566
+ changedFiles: [],
2567
+ pendingAttachments: [],
2568
+ lastToolName: null,
2569
+ taskThreadMap: /* @__PURE__ */ new Map()
2570
+ };
2571
+ }
2478
2572
  async function handleOutputStream(stream, channel, sessionId, verbose = false, mode = "auto", _provider = "claude", options = {}) {
2479
2573
  const streamer = new MessageStreamer(channel, sessionId);
2480
- console.log(`[OutputHandler] Stream started for session ${sessionId} (provider: ${_provider}, mode: ${mode}, verbose: ${verbose})`);
2481
- let lastToolName = null;
2482
- let askedUser = false;
2483
- let askUserQuestionsJson;
2484
- let hadError = false;
2485
- let success = null;
2486
- let commandCount = 0;
2487
- let fileChangeCount = 0;
2488
- const recentCommands = [];
2489
- const changedFiles = [];
2490
- const pendingAttachments = [];
2491
- const taskThreadMap = /* @__PURE__ */ new Map();
2492
- let deferredResult;
2574
+ console.log(
2575
+ `[OutputHandler] Stream started for session ${sessionId} (provider: ${_provider}, mode: ${mode}, verbose: ${verbose})`
2576
+ );
2577
+ const state = createInitialState();
2493
2578
  let lastDigestFlushAt = Date.now();
2494
- const session = getSession(sessionId);
2579
+ const session = getSessionView(sessionId);
2495
2580
  if (session) {
2496
2581
  await initializeSessionPanel(sessionId, channel, {
2497
2582
  statusCardMessageId: session.statusCardMessageId,
@@ -2506,238 +2591,34 @@ async function handleOutputStream(stream, channel, sessionId, verbose = false, m
2506
2591
  timestamp: Date.now()
2507
2592
  });
2508
2593
  }
2594
+ const ctx = { sessionId, channel, streamer, verbose, mode, state };
2509
2595
  try {
2510
2596
  for await (const event of stream) {
2511
2597
  options.onEvent?.(event);
2512
- switch (event.type) {
2513
- case "text_delta": {
2514
- streamer.append(event.text);
2515
- break;
2516
- }
2517
- case "ask_user": {
2518
- console.log(`[OutputHandler] Session ${sessionId}: ask_user event (human input requested)`);
2519
- askedUser = true;
2520
- askUserQuestionsJson = event.questionsJson;
2521
- await streamer.discard();
2522
- if (session) {
2523
- await updateSessionState(sessionId, {
2524
- type: "awaiting_human",
2525
- sessionId,
2526
- source: session.provider === "claude" ? "claude" : "codex",
2527
- confidence: "high",
2528
- timestamp: Date.now(),
2529
- metadata: { detail: event.questionsJson }
2530
- });
2531
- await flushDigest(sessionId);
2532
- await handleAwaitingHuman(sessionId, event.questionsJson, {
2533
- source: session.provider === "claude" ? "claude" : "codex"
2534
- });
2535
- }
2536
- break;
2537
- }
2538
- case "task": {
2539
- await streamer.finalize();
2540
- queueDigest(sessionId, { kind: "tool", text: `\u4EFB\u52A1\u5DE5\u5177\uFF1A${event.action}` });
2541
- lastToolName = event.action;
2542
- break;
2543
- }
2544
- case "task_started": {
2545
- await streamer.finalize();
2546
- queueDigest(sessionId, {
2547
- kind: "subagent",
2548
- text: `\u5B50\u4EE3\u7406\u542F\u52A8\uFF1A${truncate(event.description, 80)}`
2549
- });
2550
- if (session && channel.type !== void 0) {
2551
- autoSpawnSubagentThread(session, event.taskId, event.description, channel).then((result) => {
2552
- if (result) taskThreadMap.set(event.taskId, result.threadId);
2553
- }).catch((err) => console.warn(`[OutputHandler] Failed to auto-spawn thread for task ${event.taskId}: ${err.message}`));
2554
- }
2555
- break;
2556
- }
2557
- case "task_progress": {
2558
- if (event.summary) {
2559
- queueDigest(sessionId, {
2560
- kind: "subagent",
2561
- text: `\u5B50\u4EE3\u7406\u8FDB\u5C55\uFF1A${truncate(event.summary, 100)}`
2562
- });
2563
- }
2564
- const progressThreadId = taskThreadMap.get(event.taskId);
2565
- if (progressThreadId && event.summary) {
2566
- const thread = channel.threads?.cache.get(progressThreadId);
2567
- if (thread) {
2568
- thread.send(`\u{1F4DD} ${truncate(event.summary, 1900)}`).catch((e) => console.warn(`[OutputHandler] Failed to send progress to thread: ${e.message}`));
2569
- }
2570
- }
2571
- break;
2572
- }
2573
- case "task_done": {
2574
- await streamer.finalize();
2575
- queueDigest(sessionId, {
2576
- kind: "subagent",
2577
- text: `\u5B50\u4EE3\u7406${event.status === "completed" ? "\u5B8C\u6210" : "\u7ED3\u675F"}\uFF1A${truncate(event.summary || "No summary.", 100)}`
2578
- });
2579
- const doneThreadId = taskThreadMap.get(event.taskId);
2580
- if (doneThreadId) {
2581
- const thread = channel.threads?.cache.get(doneThreadId);
2582
- if (thread) {
2583
- const statusEmoji = event.status === "completed" ? "\u2705" : "\u274C";
2584
- thread.send(`${statusEmoji} \u5B50\u4EFB\u52A1${event.status === "completed" ? "\u5B8C\u6210" : "\u7ED3\u675F"}\uFF1A${truncate(event.summary || "", 1900)}`).catch((e) => console.warn(`[OutputHandler] Failed to send task done to thread: ${e.message}`));
2585
- }
2586
- taskThreadMap.delete(event.taskId);
2587
- }
2588
- break;
2589
- }
2590
- case "web_search": {
2591
- if (verbose) {
2592
- queueDigest(sessionId, { kind: "search", text: `\u68C0\u7D22\uFF1A${truncate(event.query, 80)}` });
2593
- }
2594
- break;
2595
- }
2596
- case "tool_start": {
2597
- await streamer.finalize();
2598
- queueDigest(sessionId, { kind: "tool", text: `\u5DE5\u5177\uFF1A${event.toolName}` });
2599
- lastToolName = event.toolName;
2600
- break;
2601
- }
2602
- case "tool_result": {
2603
- await streamer.finalize();
2604
- if (verbose && event.result) {
2605
- queueDigest(sessionId, {
2606
- kind: "tool",
2607
- text: `\u5DE5\u5177\u7ED3\u679C\uFF1A${truncate(lastToolName || event.toolName || "tool", 60)}`
2608
- });
2609
- }
2610
- break;
2611
- }
2612
- case "image_file": {
2613
- if (existsSync(event.filePath)) {
2614
- pendingAttachments.push(event.filePath);
2615
- }
2616
- break;
2617
- }
2618
- case "command_execution": {
2619
- commandCount++;
2620
- if (recentCommands.length < 8) recentCommands.push(event.command);
2621
- if (!shouldSuppressCommandExecution(event.command)) {
2622
- queueDigest(sessionId, {
2623
- kind: "command",
2624
- text: `\u547D\u4EE4\uFF1A${truncate(event.command, 80)}${event.exitCode !== null ? `\uFF08\u9000\u51FA\u7801 ${event.exitCode}\uFF09` : ""}`
2625
- });
2626
- }
2627
- break;
2628
- }
2629
- case "file_change": {
2630
- fileChangeCount += event.changes.length;
2631
- for (const change of event.changes) {
2632
- if (!change.filePath) continue;
2633
- if (changedFiles.includes(change.filePath)) continue;
2634
- if (changedFiles.length >= 12) break;
2635
- changedFiles.push(change.filePath);
2636
- }
2637
- queueDigest(sessionId, {
2638
- kind: "file",
2639
- text: `\u6587\u4EF6\u53D8\u66F4\uFF1A${event.changes.length} \u4E2A\uFF08\u6700\u8FD1\uFF1A${truncate(changedFiles.slice(-3).join(", "), 120)}\uFF09`
2640
- });
2641
- break;
2642
- }
2643
- case "reasoning": {
2644
- if (verbose) {
2645
- queueDigest(sessionId, { kind: "reasoning", text: `\u63A8\u7406\uFF1A${truncate(event.text, 100)}` });
2646
- }
2647
- break;
2648
- }
2649
- case "todo_list": {
2650
- queueDigest(sessionId, {
2651
- kind: "todo",
2652
- text: `\u5F85\u529E\u66F4\u65B0\uFF1A${event.items.filter((item) => item.completed).length}/${event.items.length} \u5DF2\u5B8C\u6210`
2653
- });
2654
- break;
2655
- }
2656
- case "result": {
2657
- success = event.success;
2658
- const lastText = streamer.getText();
2659
- const cost = event.costUsd.toFixed(4);
2660
- const duration = event.durationMs ? `${(event.durationMs / 1e3).toFixed(1)}s` : "unknown";
2661
- const turns = event.numTurns || 0;
2662
- const modeLabel = { auto: "\u81EA\u52A8", plan: "\u8BA1\u5212", normal: "\u666E\u901A", monitor: "\u76D1\u63A7" }[mode] || "\u81EA\u52A8";
2663
- const statusLine = event.success ? `-# $${cost} | ${duration} | ${turns} turns | ${modeLabel}` : `-# Error | $${cost} | ${duration} | ${turns} turns`;
2664
- streamer.append(`
2665
- ${statusLine}`, { persist: false });
2666
- if (!event.success && event.errors.length) {
2667
- streamer.append(`
2668
- \`\`\`
2669
- ${event.errors.join("\n")}
2670
- \`\`\``, { persist: false });
2671
- }
2672
- await streamer.finalize();
2673
- if (session) {
2674
- if (mode === "monitor") {
2675
- await flushDigest(sessionId);
2676
- await updateSessionState(sessionId, {
2677
- type: "work_started",
2678
- sessionId,
2679
- source: session.provider === "claude" ? "claude" : "codex",
2680
- confidence: "high",
2681
- timestamp: Date.now(),
2682
- metadata: {
2683
- phase: "\u7B49\u5F85\u76D1\u7763\u5224\u65AD",
2684
- 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"
2685
- }
2686
- });
2687
- } else {
2688
- deferredResult = {
2689
- event,
2690
- text: lastText,
2691
- attachments: [...pendingAttachments]
2692
- };
2693
- }
2694
- }
2695
- pendingAttachments.length = 0;
2696
- break;
2697
- }
2698
- case "error": {
2699
- console.warn(`[OutputHandler] Session ${sessionId}: error event \u2014 ${event.message}`);
2700
- hadError = true;
2701
- await streamer.finalize();
2702
- queueDigest(sessionId, { kind: "error", text: `\u9519\u8BEF\uFF1A${truncate(event.message, 120)}` });
2703
- if (session && mode !== "monitor") {
2704
- await flushDigest(sessionId);
2705
- await updateSessionState(sessionId, {
2706
- type: "errored",
2707
- sessionId,
2708
- source: session.provider === "claude" ? "claude" : "codex",
2709
- confidence: "high",
2710
- timestamp: Date.now(),
2711
- metadata: { errorMessage: event.message }
2712
- });
2713
- }
2714
- break;
2715
- }
2716
- case "session_init": {
2717
- break;
2718
- }
2719
- }
2720
- if (session && Date.now() - lastDigestFlushAt >= 8e3) {
2598
+ await dispatchEvent(event, ctx);
2599
+ if (session && Date.now() - lastDigestFlushAt >= DIGEST_FLUSH_INTERVAL_MS) {
2721
2600
  await flushDigest(sessionId);
2722
2601
  lastDigestFlushAt = Date.now();
2723
2602
  }
2724
2603
  }
2725
- if (session && mode !== "monitor" && deferredResult) {
2726
- console.log(`[OutputHandler] Session ${sessionId}: delivering deferred result (success: ${deferredResult.event.success})`);
2604
+ if (session && mode !== "monitor" && state.deferredResult) {
2605
+ console.log(
2606
+ `[OutputHandler] Session ${sessionId}: delivering deferred result (success: ${state.deferredResult.event.success})`
2607
+ );
2727
2608
  await flushDigest(sessionId);
2728
2609
  await handleResultEvent(
2729
2610
  sessionId,
2730
- deferredResult.event,
2731
- deferredResult.text,
2732
- deferredResult.attachments
2611
+ state.deferredResult.event,
2612
+ state.deferredResult.text,
2613
+ state.deferredResult.attachments
2733
2614
  );
2734
2615
  }
2735
2616
  } catch (err) {
2736
- hadError = true;
2617
+ state.hadError = true;
2737
2618
  await streamer.finalize();
2738
2619
  if (!isAbortError(err)) {
2739
- console.error(`[OutputHandler] Session ${sessionId}: unhandled stream error \u2014 ${err.message || ""}`);
2740
2620
  const errMsg = err.message || "";
2621
+ console.error(`[OutputHandler] Session ${sessionId}: unhandled stream error \u2014 ${errMsg}`);
2741
2622
  queueDigest(sessionId, { kind: "error", text: `\u5F02\u5E38\uFF1A${truncate(errMsg, 120)}` });
2742
2623
  if (session) {
2743
2624
  await flushDigest(sessionId);
@@ -2755,37 +2636,56 @@ ${event.errors.join("\n")}
2755
2636
  streamer.destroy();
2756
2637
  }
2757
2638
  const finalText = streamer.getText();
2758
- console.log(`[OutputHandler] Stream ended for session ${sessionId} (text: ${finalText.length} chars, commands: ${commandCount}, files: ${fileChangeCount}, success: ${success}, hadError: ${hadError})`);
2639
+ console.log(
2640
+ `[OutputHandler] Stream ended for session ${sessionId} (text: ${finalText.length} chars, commands: ${state.commandCount}, files: ${state.fileChangeCount}, success: ${state.success}, hadError: ${state.hadError})`
2641
+ );
2759
2642
  return {
2760
2643
  text: finalText,
2761
- askedUser,
2762
- askUserQuestionsJson,
2763
- hadError,
2764
- success,
2765
- commandCount,
2766
- fileChangeCount,
2767
- recentCommands,
2768
- changedFiles
2644
+ askedUser: state.askedUser,
2645
+ askUserQuestionsJson: state.askUserQuestionsJson,
2646
+ hadError: state.hadError,
2647
+ success: state.success,
2648
+ commandCount: state.commandCount,
2649
+ fileChangeCount: state.fileChangeCount,
2650
+ recentCommands: state.recentCommands,
2651
+ changedFiles: state.changedFiles
2769
2652
  };
2770
2653
  }
2771
2654
 
2772
2655
  // ../bot/src/discord-output-port.ts
2656
+ function asSessionChannel(ref) {
2657
+ return ref;
2658
+ }
2659
+ function toResultEvent(data) {
2660
+ return {
2661
+ type: "result",
2662
+ success: data.success,
2663
+ costUsd: data.costUsd,
2664
+ durationMs: data.durationMs,
2665
+ numTurns: data.numTurns,
2666
+ errors: data.errors,
2667
+ metadata: data.metadata
2668
+ };
2669
+ }
2773
2670
  var DiscordOutputPort = class {
2774
2671
  async initializePanel(session, channel) {
2775
- await initializeSessionPanel(session.id, channel);
2672
+ await initializeSessionPanel(session.id, asSessionChannel(channel));
2776
2673
  }
2777
2674
  async updateState(sessionId, event, options) {
2778
- await updateSessionState(sessionId, event, options?.channel ? { channel: options.channel } : void 0);
2675
+ await updateSessionState(
2676
+ sessionId,
2677
+ event,
2678
+ options?.channel ? { channel: asSessionChannel(options.channel) } : void 0
2679
+ );
2779
2680
  }
2780
2681
  async handleResult(sessionId, resultEvent, summary) {
2781
- const event = resultEvent;
2782
- await handleResultEvent(sessionId, event, summary ?? "");
2682
+ await handleResultEvent(sessionId, toResultEvent(resultEvent), summary ?? "");
2783
2683
  }
2784
2684
  async handleAwaitingHuman(sessionId, reason, options) {
2785
2685
  await handleAwaitingHuman(sessionId, reason, options);
2786
2686
  }
2787
2687
  async relocatePanel(sessionId, channel) {
2788
- await relocateSessionPanelToBottom(sessionId, channel);
2688
+ await relocateSessionPanelToBottom(sessionId, asSessionChannel(channel));
2789
2689
  }
2790
2690
  cleanupPanel(sessionId) {
2791
2691
  cleanupSessionPanel(sessionId);
@@ -2796,7 +2696,7 @@ var DiscordOutputPort = class {
2796
2696
  async handleOutputStream(stream, channel, sessionId, verbose = false, mode = "auto", provider = "claude", options = {}) {
2797
2697
  return handleOutputStream(
2798
2698
  stream,
2799
- channel,
2699
+ asSessionChannel(channel),
2800
2700
  sessionId,
2801
2701
  verbose,
2802
2702
  mode,
@@ -2934,7 +2834,7 @@ async function archiveSessionsById(guild, sessionIds, summary = "Bulk cleanup fr
2934
2834
  failed: []
2935
2835
  };
2936
2836
  for (const sessionId of new Set(Array.from(sessionIds).filter(Boolean))) {
2937
- const session = getSession(sessionId);
2837
+ const session = getSessionView(sessionId);
2938
2838
  if (!session || session.type !== "persistent") {
2939
2839
  result.missingSessions += 1;
2940
2840
  continue;
@@ -2976,10 +2876,10 @@ async function reconcileSessionRecordsWithGuild(guild) {
2976
2876
  }
2977
2877
 
2978
2878
  // ../bot/src/session-sync.ts
2979
- import { ChannelType as ChannelType2 } from "discord.js";
2879
+ import { ChannelType } from "discord.js";
2980
2880
 
2981
2881
  // ../bot/src/codex-session-discovery.ts
2982
- import { existsSync as existsSync2 } from "fs";
2882
+ import { existsSync } from "fs";
2983
2883
  import { readFile, open } from "fs/promises";
2984
2884
  import { glob } from "fs/promises";
2985
2885
  import { join, resolve, sep } from "path";
@@ -3008,7 +2908,7 @@ function parseUpdatedAt(value) {
3008
2908
  }
3009
2909
  async function readSessionIndex(codexHome = join(homedir(), ".codex")) {
3010
2910
  const indexPath = join(codexHome, "session_index.jsonl");
3011
- if (!existsSync2(indexPath)) return [];
2911
+ if (!existsSync(indexPath)) return [];
3012
2912
  let content;
3013
2913
  try {
3014
2914
  content = await readFile(indexPath, "utf-8");
@@ -3078,7 +2978,7 @@ async function fileMatchesSessionId(file, id) {
3078
2978
  }
3079
2979
  async function findSessionFileById(id, codexHome = join(homedir(), ".codex")) {
3080
2980
  const sessionsDir = join(codexHome, "sessions");
3081
- if (!existsSync2(sessionsDir)) return null;
2981
+ if (!existsSync(sessionsDir)) return null;
3082
2982
  if (await isRipgrepAvailable()) {
3083
2983
  try {
3084
2984
  const { stdout } = await execFileAsync(
@@ -3252,12 +3152,12 @@ function stopSync() {
3252
3152
  }
3253
3153
  async function findOrCreateSessionChannel(guild, category, provider, providerSessionId, fallbackName) {
3254
3154
  const existing = category.children.cache.find(
3255
- (channel) => channel.type === ChannelType2.GuildText && typeof channel.topic === "string" && channel.topic.includes(`Provider Session: ${providerSessionId}`)
3155
+ (channel) => channel.type === ChannelType.GuildText && typeof channel.topic === "string" && channel.topic.includes(`Provider Session: ${providerSessionId}`)
3256
3156
  );
3257
3157
  if (existing) return existing;
3258
3158
  return guild.channels.create({
3259
3159
  name: fallbackName,
3260
- type: ChannelType2.GuildText,
3160
+ type: ChannelType.GuildText,
3261
3161
  parent: category.id,
3262
3162
  topic: `${provider} session (synced) | Provider Session: ${providerSessionId}`
3263
3163
  }).then((channel) => {
@@ -3308,11 +3208,11 @@ async function runSync(client2) {
3308
3208
  );
3309
3209
  let createdThisRun = 0;
3310
3210
  try {
3311
- const claudeSdk = await import("./sdk-V7A7IF7F.js");
3211
+ const claudeSdk = await import("./sdk-C3D6X4EB.js");
3312
3212
  for (const project of projects) {
3313
3213
  if (createdThisRun >= MAX_SESSIONS_PER_SYNC) break;
3314
3214
  const category = guild.channels.cache.get(project.discordCategoryId);
3315
- if (!category || category.type !== ChannelType2.GuildCategory) {
3215
+ if (!category || category.type !== ChannelType.GuildCategory) {
3316
3216
  recordSkip("categoryMissing");
3317
3217
  continue;
3318
3218
  }
@@ -3403,7 +3303,7 @@ async function runSync(client2) {
3403
3303
  continue;
3404
3304
  }
3405
3305
  const category = guild.channels.cache.get(project.discordCategoryId);
3406
- if (!category || category.type !== ChannelType2.GuildCategory) {
3306
+ if (!category || category.type !== ChannelType.GuildCategory) {
3407
3307
  recordSkip("categoryMissing");
3408
3308
  continue;
3409
3309
  }
@@ -3446,6 +3346,7 @@ var POLL_INTERVAL_IDLE_MS = 5e3;
3446
3346
  var STALE_CLEANUP_MS = 12e4;
3447
3347
  var MAX_LINES_PER_POLL = 100;
3448
3348
  var MAX_BUFFER_SIZE = 10 * 1024 * 1024;
3349
+ var MAX_FILE_SIZE_BYTES = 500 * 1024 * 1024;
3449
3350
  var CodexLogMonitor = class {
3450
3351
  tracked = /* @__PURE__ */ new Map();
3451
3352
  interval = null;
@@ -3539,6 +3440,14 @@ var CodexLogMonitor = class {
3539
3440
  } catch {
3540
3441
  return;
3541
3442
  }
3443
+ if (stat.size > MAX_FILE_SIZE_BYTES) {
3444
+ if (!this.tracked.has(filePath)) {
3445
+ console.warn(
3446
+ `[CodexLogMonitor] Skipping oversized rollout file (${Math.round(stat.size / 1024 / 1024)}MB): ${filePath}`
3447
+ );
3448
+ }
3449
+ return;
3450
+ }
3542
3451
  let tracked = this.tracked.get(filePath);
3543
3452
  if (!tracked) {
3544
3453
  const sessionId = this.extractSessionId(fileName);
@@ -3785,7 +3694,7 @@ async function handleCodexMonitorStateChange(resolveChannel2, monitorSessionId,
3785
3694
 
3786
3695
  // ../bot/src/ipc-server.ts
3787
3696
  import { createServer } from "net";
3788
- import { unlinkSync, existsSync as existsSync3 } from "fs";
3697
+ import { unlinkSync, existsSync as existsSync2, chmodSync } from "fs";
3789
3698
 
3790
3699
  // ../bot/src/session-discovery.ts
3791
3700
  async function discoverAndRegisterSession(client2, params) {
@@ -3832,7 +3741,7 @@ function startIpcServer(client2) {
3832
3741
  discordClient = client2;
3833
3742
  const socketPath = config.socketPath;
3834
3743
  activeSocketPath = socketPath;
3835
- if (existsSync3(socketPath)) {
3744
+ if (existsSync2(socketPath)) {
3836
3745
  try {
3837
3746
  unlinkSync(socketPath);
3838
3747
  } catch {
@@ -3871,6 +3780,15 @@ function startIpcServer(client2) {
3871
3780
  });
3872
3781
  });
3873
3782
  server.listen(socketPath, () => {
3783
+ if (process.platform !== "win32") {
3784
+ try {
3785
+ chmodSync(socketPath, 384);
3786
+ } catch (err) {
3787
+ console.warn(
3788
+ `[IpcServer] Failed to set 0600 on socket ${socketPath}: ${err.message}`
3789
+ );
3790
+ }
3791
+ }
3874
3792
  console.log(`[IpcServer] Listening on ${socketPath}`);
3875
3793
  });
3876
3794
  server.on("error", (err) => {
@@ -3886,7 +3804,7 @@ function stopIpcServer() {
3886
3804
  server.close();
3887
3805
  server = null;
3888
3806
  }
3889
- if (activeSocketPath && existsSync3(activeSocketPath)) {
3807
+ if (activeSocketPath && existsSync2(activeSocketPath)) {
3890
3808
  try {
3891
3809
  unlinkSync(activeSocketPath);
3892
3810
  } catch {
@@ -3941,7 +3859,7 @@ async function handleHookEvent(payload) {
3941
3859
  } : void 0
3942
3860
  });
3943
3861
  if (registered) {
3944
- session = getSession(registered.sessionId);
3862
+ session = getSessionView(registered.sessionId);
3945
3863
  console.log(`[IpcServer] Auto-registered session: ${registered.sessionId}`);
3946
3864
  }
3947
3865
  }
@@ -3961,14 +3879,14 @@ async function handleHookEvent(payload) {
3961
3879
  async function handleGateResolved(payload) {
3962
3880
  const data = payload;
3963
3881
  console.log(`[IpcServer] Terminal resolved gate ${data.gateId}: ${data.action}`);
3964
- const result = gateCoordinator.notifyTerminalResolved(data.gateId, data.action);
3882
+ const result = gateService.notifyTerminalResolved(data.gateId, data.action);
3965
3883
  if (!result.success) {
3966
3884
  console.warn(`[IpcServer] Gate resolution failed: ${result.message}`);
3967
3885
  return;
3968
3886
  }
3969
- const gate = gateCoordinator.getGate(data.gateId);
3887
+ const gate = gateService.getGate(data.gateId);
3970
3888
  if (gate?.discordMessageId) {
3971
- const session = getSession(data.sessionId);
3889
+ const session = getSessionView(data.sessionId);
3972
3890
  if (session) {
3973
3891
  const channel = discordClient?.channels.cache.get(session.channelId);
3974
3892
  if (channel) {
@@ -4265,7 +4183,7 @@ function stopHealthMonitor() {
4265
4183
 
4266
4184
  // ../bot/src/message-handler.ts
4267
4185
  import {
4268
- ChannelType as ChannelType3
4186
+ ChannelType as ChannelType2
4269
4187
  } from "discord.js";
4270
4188
 
4271
4189
  // ../bot/src/discord/inbound-envelope.ts
@@ -4332,7 +4250,7 @@ function stopMessageHandler() {
4332
4250
  async function handleMessage(message) {
4333
4251
  if (message.author.bot) return;
4334
4252
  const channel = message.channel;
4335
- const isSessionChannel3 = channel.type === ChannelType3.GuildText;
4253
+ const isSessionChannel3 = channel.type === ChannelType2.GuildText;
4336
4254
  const isSubagentThread = channel.isThread();
4337
4255
  if (!isSessionChannel3 && !isSubagentThread) return;
4338
4256
  const session = getSessionByChannel(channel.id);
@@ -4421,7 +4339,7 @@ async function handleMessage(message) {
4421
4339
  // ../bot/src/commands.ts
4422
4340
  import { REST, Routes } from "discord.js";
4423
4341
  import { createHash } from "crypto";
4424
- import { existsSync as existsSync4, readFileSync as readFileSync2, writeFileSync } from "fs";
4342
+ import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync } from "fs";
4425
4343
  import { join as join3 } from "path";
4426
4344
 
4427
4345
  // ../bot/src/command-definitions/project-command.ts
@@ -4548,7 +4466,16 @@ function buildAgentCommand() {
4548
4466
  { name: "\u5F00\u542F", value: "on" }
4549
4467
  )
4550
4468
  )
4551
- ).addSubcommand((sub) => sub.setName("continue").setDescription("\u7EE7\u7EED\u5F53\u524D\u4F1A\u8BDD\u7684\u751F\u6210"));
4469
+ ).addSubcommand((sub) => sub.setName("continue").setDescription("\u7EE7\u7EED\u5F53\u524D\u4F1A\u8BDD\u7684\u751F\u6210")).addSubcommand(
4470
+ (sub) => sub.setName("batch").setDescription("\u6279\u91CF\u5BA1\u6279\u6A21\u5F0F\uFF08\u5EF6\u540E\u6240\u6709\u5DE5\u5177\u5BA1\u6279\u81F3\u4E00\u6B21\u6027\u5904\u7406\uFF09").addStringOption(
4471
+ (opt) => opt.setName("action").setDescription("\u5F00\u5173 / \u4E00\u952E\u6279\u51C6 / \u4E00\u952E\u62D2\u7EDD").setRequired(true).addChoices(
4472
+ { name: "\u5F00\u542F\u6279\u91CF\u5BA1\u6279\u6A21\u5F0F", value: "on" },
4473
+ { name: "\u5173\u95ED\u6279\u91CF\u5BA1\u6279\u6A21\u5F0F", value: "off" },
4474
+ { name: "\u6279\u51C6\u961F\u5217\u4E2D\u7684\u5168\u90E8\u5DE5\u5177\u8C03\u7528", value: "approve-all" },
4475
+ { name: "\u62D2\u7EDD\u961F\u5217\u4E2D\u7684\u5168\u90E8\u5DE5\u5177\u8C03\u7528", value: "reject-all" }
4476
+ )
4477
+ )
4478
+ );
4552
4479
  }
4553
4480
 
4554
4481
  // ../bot/src/command-definitions/subagent-command.ts
@@ -4590,7 +4517,7 @@ function computeCommandsHash(body) {
4590
4517
  }
4591
4518
  function readStoredHash() {
4592
4519
  try {
4593
- if (existsSync4(HASH_FILE)) {
4520
+ if (existsSync3(HASH_FILE)) {
4594
4521
  return readFileSync2(HASH_FILE, "utf-8").trim();
4595
4522
  }
4596
4523
  } catch {
@@ -4625,13 +4552,13 @@ async function registerCommands() {
4625
4552
  // ../bot/src/bot-services-orchestrator.ts
4626
4553
  async function findOrCreateLogChannel(guild) {
4627
4554
  const existing = guild.channels.cache.find(
4628
- (ch) => ch.name === "bot-logs" && ch.type === ChannelType4.GuildText && !ch.parentId
4555
+ (ch) => ch.name === "bot-logs" && ch.type === ChannelType3.GuildText && !ch.parentId
4629
4556
  );
4630
4557
  if (existing) return existing;
4631
4558
  try {
4632
4559
  return await guild.channels.create({
4633
4560
  name: "bot-logs",
4634
- type: ChannelType4.GuildText,
4561
+ type: ChannelType3.GuildText,
4635
4562
  reason: "Auto-created by workspacecord for bot logs"
4636
4563
  });
4637
4564
  } catch {
@@ -4656,7 +4583,9 @@ var BotServicesOrchestrator = class {
4656
4583
  logBuffer2.log(`Reconciled ${reconciled.endedMissingSessions} stale session record(s) on startup.`);
4657
4584
  }
4658
4585
  setBotStartTime(Date.now());
4659
- await this.#invalidatePendingGates(client2);
4586
+ await gateService.init();
4587
+ await this.#reconcilePendingGates(client2, logBuffer2);
4588
+ await this.#resumeAbandonedMonitorRuns(client2, logBuffer2);
4660
4589
  if (config.messageRetentionDays) {
4661
4590
  await cleanupOldMessages(client2);
4662
4591
  }
@@ -4667,15 +4596,55 @@ var BotServicesOrchestrator = class {
4667
4596
  logBuffer2.log("Codex log monitor started");
4668
4597
  return { serviceBus: this.#serviceBus, logBuffer: logBuffer2, presenceManager, logChannel, codexMonitor: null };
4669
4598
  }
4670
- async #invalidatePendingGates(client2) {
4671
- const invalidated = gateCoordinator.invalidateAllOnRestart();
4672
- if (!invalidated.length) return;
4673
- console.log(`[GateInvalidation] Invalidating ${invalidated.length} pending gates on restart`);
4674
- for (const { gateId, discordMessageId } of invalidated) {
4599
+ /**
4600
+ * 重启后对悬挂的 Monitor run 执行 reconcile,并按 config.monitorAutoResumePolicy 续跑符合条件的 session。
4601
+ * 续跑采用 fire-and-forget:每个 session 独立异步运行,不阻塞启动。
4602
+ */
4603
+ async #resumeAbandonedMonitorRuns(client2, logBuffer2) {
4604
+ let result;
4605
+ try {
4606
+ result = await reconcileAndCollectAutoResumeCandidates(config.monitorAutoResumePolicy);
4607
+ } catch (err) {
4608
+ console.error("[MonitorAutoResume] reconcile failed:", err);
4609
+ return;
4610
+ }
4611
+ if (result.abandoned.length > 0) {
4612
+ logBuffer2.log(
4613
+ `Marked ${result.abandoned.length} orphan monitor run(s) as abandoned on startup.`
4614
+ );
4615
+ }
4616
+ if (result.candidates.length === 0) return;
4617
+ logBuffer2.log(
4618
+ `Auto-resuming ${result.candidates.length} monitor session(s) (policy=${config.monitorAutoResumePolicy}).`
4619
+ );
4620
+ for (const candidate of result.candidates) {
4621
+ const session = getSessionView(candidate.sessionId);
4622
+ if (!session) continue;
4623
+ const channel = client2.channels.cache.get(candidate.channelId);
4624
+ if (!channel) {
4625
+ console.warn(
4626
+ `[MonitorAutoResume] sessionId=${candidate.sessionId} channel ${candidate.channelId} not accessible, skipping`
4627
+ );
4628
+ continue;
4629
+ }
4630
+ console.log(
4631
+ `[MonitorAutoResume] resuming sessionId=${candidate.sessionId} runId=${candidate.runId} lastIteration=${candidate.lastIteration}`
4632
+ );
4633
+ void executeSessionContinue(session, channel).catch((err) => {
4634
+ console.error(
4635
+ `[MonitorAutoResume] sessionId=${candidate.sessionId} resume failed:`,
4636
+ err
4637
+ );
4638
+ });
4639
+ }
4640
+ }
4641
+ async #reconcilePendingGates(client2, logBuffer2) {
4642
+ const result = gateService.reconcileOnStartup(config.gateRestartPolicy);
4643
+ for (const { gateId, discordMessageId } of result.invalidated) {
4675
4644
  if (!discordMessageId) continue;
4676
- const gate = gateCoordinator.getGate(gateId);
4645
+ const gate = gateService.getGate(gateId);
4677
4646
  if (!gate?.sessionId) continue;
4678
- const session = getSession(gate.sessionId);
4647
+ const session = getSessionView(gate.sessionId);
4679
4648
  if (!session) continue;
4680
4649
  const channel = client2.channels.cache.get(session.channelId);
4681
4650
  if (!channel?.messages) continue;
@@ -4692,10 +4661,17 @@ var BotServicesOrchestrator = class {
4692
4661
  }))
4693
4662
  });
4694
4663
  } catch (err) {
4695
- console.error(`[GateInvalidation] Failed to update message for gate ${gateId}:`, err);
4664
+ console.error(`[GateReconcile] Failed to update message for gate ${gateId}:`, err);
4696
4665
  }
4697
4666
  }
4698
- console.log(`[GateInvalidation] ${invalidated.length} gates invalidated`);
4667
+ if (result.resumed.length > 0) {
4668
+ logBuffer2.log(
4669
+ `Resumed ${result.resumed.length} pending gate(s) across restart (policy=resume-pending).`
4670
+ );
4671
+ }
4672
+ if (result.invalidated.length > 0) {
4673
+ logBuffer2.log(`Closed ${result.invalidated.length} orphan/overdue gate(s) on startup.`);
4674
+ }
4699
4675
  }
4700
4676
  #registerServices(client2, logBuffer2, presence) {
4701
4677
  this.#serviceBus.register({ name: "message-handler", start() {
@@ -4720,7 +4696,7 @@ var BotServicesOrchestrator = class {
4720
4696
  );
4721
4697
  },
4722
4698
  async (providerSessionId, cwd, remoteHumanControl, subagent) => {
4723
- const { registerLocalSession: registerLocalSession2 } = await import("./session-local-registration-RIO5EPZ5.js");
4699
+ const { registerLocalSession: registerLocalSession2 } = await import("./session-local-registration-RL2A3QZB.js");
4724
4700
  const g = client2.guilds.cache.first();
4725
4701
  if (!g) return false;
4726
4702
  const result = await registerLocalSession2(
@@ -4768,8 +4744,8 @@ var BotServicesOrchestrator = class {
4768
4744
  stopPerformanceMonitoring();
4769
4745
  } });
4770
4746
  this.#serviceBus.register(intervalService("gate-housekeeping", () => {
4771
- const expired = gateCoordinator.cleanupExpired();
4772
- const archived = gateCoordinator.archiveResolved(100);
4747
+ const expired = gateService.cleanupExpired();
4748
+ const archived = gateService.archiveResolved(100);
4773
4749
  if (expired || archived) console.log(`[GateHousekeeping] expired=${expired}, archived=${archived}`);
4774
4750
  }, 6e4));
4775
4751
  this.#serviceBus.register(intervalService("presence", () => presence.updatePresence(), 3e4));
@@ -4796,7 +4772,7 @@ var BotServicesOrchestrator = class {
4796
4772
  };
4797
4773
 
4798
4774
  // ../engine/src/bot-locks.ts
4799
- import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync2, unlinkSync as unlinkSync2 } from "fs";
4775
+ import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2, unlinkSync as unlinkSync2 } from "fs";
4800
4776
  import { join as join5 } from "path";
4801
4777
  var LOCK_FILE = join5(config.dataDir, "bot.lock");
4802
4778
  var STALE_THRESHOLD_MS = 5 * 60 * 1e3;
@@ -4809,7 +4785,7 @@ function isProcessRunning(pid) {
4809
4785
  }
4810
4786
  }
4811
4787
  function acquireLock() {
4812
- if (!existsSync5(LOCK_FILE)) {
4788
+ if (!existsSync4(LOCK_FILE)) {
4813
4789
  const data2 = { pid: process.pid, startedAt: Date.now() };
4814
4790
  writeFileSync2(LOCK_FILE, JSON.stringify(data2), "utf-8");
4815
4791
  return true;
@@ -4839,7 +4815,7 @@ function acquireLock() {
4839
4815
  }
4840
4816
  function releaseLock() {
4841
4817
  try {
4842
- if (!existsSync5(LOCK_FILE)) return;
4818
+ if (!existsSync4(LOCK_FILE)) return;
4843
4819
  const raw = readFileSync3(LOCK_FILE, "utf-8").trim();
4844
4820
  const data = JSON.parse(raw);
4845
4821
  if (data.pid === process.pid) {
@@ -4851,13 +4827,13 @@ function releaseLock() {
4851
4827
 
4852
4828
  // ../bot/src/command-handlers-modules/project-handlers.ts
4853
4829
  import {
4854
- ChannelType as ChannelType6,
4855
- EmbedBuilder as EmbedBuilder3
4830
+ ChannelType as ChannelType5,
4831
+ EmbedBuilder
4856
4832
  } from "discord.js";
4857
4833
 
4858
4834
  // ../bot/src/command-handlers-shared.ts
4859
4835
  import {
4860
- ChannelType as ChannelType5
4836
+ ChannelType as ChannelType4
4861
4837
  } from "discord.js";
4862
4838
  var logFn = console.log;
4863
4839
  function setLogger(fn) {
@@ -4884,7 +4860,7 @@ var CONTROL_CHANNEL_NAME = "control";
4884
4860
  var CLEANUP_PREVIEW_LIST_LIMIT = 10;
4885
4861
  var CLEANUP_PREVIEW_LABEL_LIMIT = 120;
4886
4862
  async function registerStatusCardWithPanelAdapter(sessionId, channel, statusCardMessageId) {
4887
- const { registerExistingStatusCard: registerExistingStatusCard2 } = await import("./panel-adapter-QTDL3S6O.js");
4863
+ const { registerExistingStatusCard: registerExistingStatusCard2 } = await import("./panel-adapter-CLI4WDII.js");
4888
4864
  if (typeof registerExistingStatusCard2 !== "function") {
4889
4865
  log(`[panel-adapter] registerExistingStatusCard \u672A\u66B4\u9732\uFF0Csession=${sessionId}`);
4890
4866
  return false;
@@ -5004,7 +4980,7 @@ function resolveCleanupCurrentChannelId(interaction) {
5004
4980
  }
5005
4981
  function findControlChannel(guild, categoryId) {
5006
4982
  const existing = guild.channels.cache.find(
5007
- (channel) => channel.type === ChannelType5.GuildText && channel.parentId === categoryId && channel.name === CONTROL_CHANNEL_NAME
4983
+ (channel) => channel.type === ChannelType4.GuildText && channel.parentId === categoryId && channel.name === CONTROL_CHANNEL_NAME
5008
4984
  );
5009
4985
  return existing ?? null;
5010
4986
  }
@@ -5012,7 +4988,7 @@ async function resolveOrCreateControlChannel(interaction, guild, categoryId, sto
5012
4988
  const currentChannel = interaction.channel;
5013
4989
  if (storedControlChannelId) {
5014
4990
  const existing = guild.channels.cache.get(storedControlChannelId);
5015
- if (existing?.type === ChannelType5.GuildText) {
4991
+ if (existing?.type === ChannelType4.GuildText) {
5016
4992
  return existing;
5017
4993
  }
5018
4994
  }
@@ -5028,7 +5004,7 @@ async function resolveOrCreateControlChannel(interaction, guild, categoryId, sto
5028
5004
  }
5029
5005
  const created = await guild.channels.create({
5030
5006
  name: CONTROL_CHANNEL_NAME,
5031
- type: ChannelType5.GuildText,
5007
+ type: ChannelType4.GuildText,
5032
5008
  parent: categoryId,
5033
5009
  topic: "Use /agent spawn here to create new agent sessions",
5034
5010
  reason: `Project control channel created for ${interaction.user.tag}`
@@ -5101,7 +5077,7 @@ async function handleProjectSetup(interaction) {
5101
5077
  try {
5102
5078
  const historyForum = await guild.channels.create({
5103
5079
  name: "history",
5104
- type: ChannelType6.GuildForum,
5080
+ type: ChannelType5.GuildForum,
5105
5081
  parent: categoryId,
5106
5082
  topic: "Archived agent sessions for this project",
5107
5083
  reason: "Created by workspacecord for session archiving"
@@ -5119,7 +5095,7 @@ async function handleProjectSetup(interaction) {
5119
5095
  setControlChannelId(categoryId, interaction.channelId);
5120
5096
  const controlInfo = `
5121
5097
  \u2022 \u63A7\u5236\u9891\u9053\uFF1A<#${interaction.channelId}>`;
5122
- const embed = new EmbedBuilder3().setColor(3066993).setTitle(`\u2705 \u9879\u76EE\u5C31\u7EEA\uFF1A${project.name}`).addFields(
5098
+ const embed = new EmbedBuilder().setColor(3066993).setTitle(`\u2705 \u9879\u76EE\u5C31\u7EEA\uFF1A${project.name}`).addFields(
5123
5099
  { name: "\u5206\u7C7B", value: `**${categoryName}**`, inline: true },
5124
5100
  { name: "\u76EE\u5F55", value: `\`${project.directory}\``, inline: true }
5125
5101
  ).setDescription(
@@ -5147,7 +5123,7 @@ async function handleProjectInfo(interaction) {
5147
5123
  }
5148
5124
  const sessions = getSessionsByCategory(categoryId);
5149
5125
  const activeSessions = sessions.filter((s) => s.type === "persistent");
5150
- const embed = new EmbedBuilder3().setColor(3447003).setTitle(`\u{1F4C1} \u9879\u76EE\uFF1A${project.name}`).addFields(
5126
+ const embed = new EmbedBuilder().setColor(3447003).setTitle(`\u{1F4C1} \u9879\u76EE\uFF1A${project.name}`).addFields(
5151
5127
  { name: "\u76EE\u5F55", value: `\`${project.directory}\``, inline: false },
5152
5128
  { name: "\u6D3B\u8DC3\u4F1A\u8BDD", value: `${activeSessions.length}`, inline: true },
5153
5129
  { name: "\u6280\u80FD", value: `${project.skills.length}`, inline: true },
@@ -5324,11 +5300,11 @@ ${lines.join("\n")}`, ephemeral: true });
5324
5300
 
5325
5301
  // ../bot/src/command-handlers-modules/agent-handlers.ts
5326
5302
  import {
5327
- ActionRowBuilder as ActionRowBuilder2,
5328
- ButtonBuilder as ButtonBuilder2,
5329
- ButtonStyle as ButtonStyle2,
5330
- ChannelType as ChannelType7,
5331
- EmbedBuilder as EmbedBuilder4
5303
+ ActionRowBuilder,
5304
+ ButtonBuilder,
5305
+ ButtonStyle,
5306
+ ChannelType as ChannelType6,
5307
+ EmbedBuilder as EmbedBuilder2
5332
5308
  } from "discord.js";
5333
5309
 
5334
5310
  // ../engine/src/agent-cleanup-request-store.ts
@@ -5406,6 +5382,8 @@ async function handleAgent(interaction) {
5406
5382
  return handleAgentPermissions(interaction);
5407
5383
  case "continue":
5408
5384
  return handleAgentContinue(interaction);
5385
+ case "batch":
5386
+ return handleAgentBatch(interaction);
5409
5387
  default:
5410
5388
  await interaction.reply({ content: `\u672A\u77E5\u5B50\u547D\u4EE4\uFF1A${sub}`, ephemeral: true });
5411
5389
  }
@@ -5458,7 +5436,7 @@ async function handleAgentSpawn(interaction) {
5458
5436
  try {
5459
5437
  sessionChannel = await guild.channels.create({
5460
5438
  name: channelName,
5461
- type: ChannelType7.GuildText,
5439
+ type: ChannelType6.GuildText,
5462
5440
  parent: categoryId,
5463
5441
  topic: `[${PROVIDER_LABELS[provider]}] ${label}`,
5464
5442
  reason: `Agent session spawned by ${interaction.user.tag}`
@@ -5489,7 +5467,7 @@ async function handleAgentSpawn(interaction) {
5489
5467
  if (mode !== "auto") {
5490
5468
  setMode(session.id, mode);
5491
5469
  }
5492
- const statusEmbed = new EmbedBuilder4().setColor(PROVIDER_COLORS[provider]).setTitle("\u{1F4A4} \u5F85\u547D").setDescription("\u7B49\u5F85\u9996\u6761\u6D88\u606F").addFields({
5470
+ const statusEmbed = new EmbedBuilder2().setColor(PROVIDER_COLORS[provider]).setTitle("\u{1F4A4} \u5F85\u547D").setDescription("\u7B49\u5F85\u9996\u6761\u6D88\u606F").addFields({
5493
5471
  name: "\u6743\u9650",
5494
5472
  value: getSessionPermissionSummary(session),
5495
5473
  inline: false
@@ -5511,7 +5489,7 @@ async function handleAgentSpawn(interaction) {
5511
5489
  }
5512
5490
  setStatusCardBinding(session.id, { messageId: statusMessage.id });
5513
5491
  log(`[panel-adapter] \u72B6\u6001\u5361\u5DF2\u6CE8\u518C\uFF0Csession=${session.id}`);
5514
- const embed = new EmbedBuilder4().setColor(3066993).setTitle(`\u2705 Agent \u5DF2\u521B\u5EFA\uFF1A${label}`).addFields(
5492
+ const embed = new EmbedBuilder2().setColor(3066993).setTitle(`\u2705 Agent \u5DF2\u521B\u5EFA\uFF1A${label}`).addFields(
5515
5493
  { name: "\u9891\u9053", value: `<#${sessionChannel.id}>`, inline: true },
5516
5494
  { name: "\u63D0\u4F9B\u5546", value: PROVIDER_LABELS[provider], inline: true },
5517
5495
  { name: "\u6A21\u5F0F", value: MODE_LABELS?.[mode] ?? mode, inline: false },
@@ -5552,7 +5530,7 @@ async function handleAgentList(interaction) {
5552
5530
  const status = s.isGenerating ? "\u{1F504} \u6267\u884C\u4E2D" : "\u{1F4A4} \u7A7A\u95F2";
5553
5531
  return `${status} | \`${s.agentLabel}\` | ${s.provider} | <#${s.channelId}> | ${formatRelative(s.lastActivity)}`;
5554
5532
  });
5555
- const embed = new EmbedBuilder4().setColor(3447003).setTitle(`Agent \u4F1A\u8BDD\uFF08${sessions.length}\uFF09`).setDescription(lines.join("\n"));
5533
+ const embed = new EmbedBuilder2().setColor(3447003).setTitle(`Agent \u4F1A\u8BDD\uFF08${sessions.length}\uFF09`).setDescription(lines.join("\n"));
5556
5534
  await interaction.reply({ embeds: [embed], ephemeral: true });
5557
5535
  }
5558
5536
  async function handleAgentCleanup(interaction) {
@@ -5597,9 +5575,9 @@ async function handleAgentCleanup(interaction) {
5597
5575
  candidateSessionIds: preview.archiveCandidates.map((session) => session.id)
5598
5576
  });
5599
5577
  const components = [
5600
- new ActionRowBuilder2().addComponents(
5601
- new ButtonBuilder2().setCustomId(`cleanup:confirm:${request.id}`).setLabel("\u786E\u8BA4\u5F52\u6863").setStyle(ButtonStyle2.Danger),
5602
- new ButtonBuilder2().setCustomId(`cleanup:cancel:${request.id}`).setLabel("\u53D6\u6D88").setStyle(ButtonStyle2.Secondary)
5578
+ new ActionRowBuilder().addComponents(
5579
+ new ButtonBuilder().setCustomId(`cleanup:confirm:${request.id}`).setLabel("\u786E\u8BA4\u5F52\u6863").setStyle(ButtonStyle.Danger),
5580
+ new ButtonBuilder().setCustomId(`cleanup:cancel:${request.id}`).setLabel("\u53D6\u6D88").setStyle(ButtonStyle.Secondary)
5603
5581
  )
5604
5582
  ];
5605
5583
  await interaction.reply({
@@ -5736,7 +5714,7 @@ async function handleAgentPermissions(interaction) {
5736
5714
  return;
5737
5715
  }
5738
5716
  await updateSessionPermissions(session.id, patch);
5739
- const refreshed = getSession(session.id) ?? { ...session, ...patch };
5717
+ const refreshed = getSessionView(session.id) ?? { ...session, ...patch };
5740
5718
  const timing = session.isGenerating ? "\u5DF2\u4FDD\u5B58\uFF0C\u5C06\u5728\u4E0B\u4E00\u8F6E\u751F\u6548\u3002" : "\u5DF2\u66F4\u65B0\u5E76\u7ACB\u5373\u751F\u6548\u3002";
5741
5719
  await interaction.reply({
5742
5720
  content: `${timing}
@@ -5763,9 +5741,93 @@ async function handleAgentContinue(interaction) {
5763
5741
  await interaction.editReply("\u7EE7\u7EED\u4E2D...");
5764
5742
  await executeSessionContinue(session, channel);
5765
5743
  }
5744
+ async function handleAgentBatch(interaction) {
5745
+ const channel = interaction.channel;
5746
+ if (!channel) {
5747
+ await interaction.reply({ content: "\u65E0\u9891\u9053\u4E0A\u4E0B\u6587\u3002", ephemeral: true });
5748
+ return;
5749
+ }
5750
+ const session = getSessionByChannel(channel.id);
5751
+ if (!session) {
5752
+ await interaction.reply({ content: "\u6B64\u9891\u9053\u6CA1\u6709\u6D3B\u8DC3\u7684\u4F1A\u8BDD\u3002", ephemeral: true });
5753
+ return;
5754
+ }
5755
+ if (session.provider !== "claude") {
5756
+ await interaction.reply({
5757
+ content: "\u6279\u91CF\u5BA1\u6279\u6A21\u5F0F\u76EE\u524D\u4EC5\u652F\u6301 Claude \u4F1A\u8BDD\u3002Codex \u8BF7\u4F7F\u7528 `/agent permissions codex-approval` \u914D\u7F6E\u3002",
5758
+ ephemeral: true
5759
+ });
5760
+ return;
5761
+ }
5762
+ const action = interaction.options.getString("action", true);
5763
+ const source = "claude";
5764
+ if (action === "on") {
5765
+ if (!shouldUseClaudePermissionHandler(session)) {
5766
+ const hint = session.mode === "auto" ? "\u5F53\u524D\u4F1A\u8BDD\u662F `auto` \u6A21\u5F0F\uFF0C\u5DE5\u5177\u4E0D\u8D70\u5BA1\u6279 hook\u3002\u8BF7\u5148 `/agent mode mode:normal` \u518D\u5F00\u542F\u6279\u91CF\u5BA1\u6279\u3002" : "\u5F53\u524D\u4F1A\u8BDD\u7684 Claude \u6743\u9650\u6A21\u5F0F\u4E3A `bypass`\uFF0C\u5DE5\u5177\u4E0D\u8D70\u5BA1\u6279 hook\u3002\u8BF7\u5148 `/agent permissions claude-permissions:normal`\u3002";
5767
+ await interaction.reply({ content: `\u26A0\uFE0F \u65E0\u6CD5\u5F00\u542F\u6279\u91CF\u5BA1\u6279\uFF1A${hint}`, ephemeral: true });
5768
+ return;
5769
+ }
5770
+ await updateSessionState(session.id, {
5771
+ type: "batch_approval_changed",
5772
+ sessionId: session.id,
5773
+ source,
5774
+ confidence: "high",
5775
+ timestamp: Date.now(),
5776
+ metadata: { enabled: true }
5777
+ });
5778
+ await interaction.reply({
5779
+ content: "\u2705 \u5DF2\u5F00\u542F\u6279\u91CF\u5BA1\u6279\u6A21\u5F0F\u3002\u540E\u7EED\u6743\u9650\u8BF7\u6C42\u5C06\u6392\u961F\uFF0C\u4F7F\u7528 `/agent batch action:approve-all` \u6216 `reject-all` \u7EDF\u4E00\u5904\u7406\u3002",
5780
+ ephemeral: true
5781
+ });
5782
+ return;
5783
+ }
5784
+ if (action === "off") {
5785
+ const pending = getBatchApprovalCount(session.id);
5786
+ drainBatchApprovals(session.id, "reject");
5787
+ await updateSessionState(session.id, {
5788
+ type: "batch_approval_changed",
5789
+ sessionId: session.id,
5790
+ source,
5791
+ confidence: "high",
5792
+ timestamp: Date.now(),
5793
+ metadata: { enabled: false }
5794
+ });
5795
+ await interaction.reply({
5796
+ content: `\u2705 \u5DF2\u5173\u95ED\u6279\u91CF\u5BA1\u6279\u6A21\u5F0F${pending > 0 ? `\uFF0C\u961F\u5217\u4E2D ${pending} \u6761\u5F85\u6279\u8BF7\u6C42\u5DF2\u6309\u62D2\u7EDD\u5904\u7406\u3002` : "\u3002"}`,
5797
+ ephemeral: true
5798
+ });
5799
+ return;
5800
+ }
5801
+ if (action === "approve-all" || action === "reject-all") {
5802
+ const projection = stateMachine.getSnapshot(session.id);
5803
+ if (!projection.batchApprovalMode) {
5804
+ await interaction.reply({
5805
+ content: "\u6279\u91CF\u5BA1\u6279\u6A21\u5F0F\u672A\u542F\u7528\u3002\u5148\u7528 `/agent batch action:on` \u5F00\u542F\u3002",
5806
+ ephemeral: true
5807
+ });
5808
+ return;
5809
+ }
5810
+ const batchAction = action === "approve-all" ? "approve" : "reject";
5811
+ const count2 = drainBatchApprovals(session.id, batchAction);
5812
+ await updateSessionState(session.id, {
5813
+ type: "batch_approval_changed",
5814
+ sessionId: session.id,
5815
+ source,
5816
+ confidence: "high",
5817
+ timestamp: Date.now(),
5818
+ metadata: { enabled: true, pendingApprovals: [] }
5819
+ });
5820
+ await interaction.reply({
5821
+ content: count2 === 0 ? "\u961F\u5217\u4E2D\u6CA1\u6709\u5F85\u6279\u51C6\u7684\u8BF7\u6C42\u3002" : `\u2705 \u5DF2${batchAction === "approve" ? "\u6279\u51C6" : "\u62D2\u7EDD"} ${count2} \u6761\u5F85\u6279\u8BF7\u6C42\u3002`,
5822
+ ephemeral: true
5823
+ });
5824
+ return;
5825
+ }
5826
+ await interaction.reply({ content: `\u672A\u77E5\u52A8\u4F5C\uFF1A${action}`, ephemeral: true });
5827
+ }
5766
5828
 
5767
5829
  // ../bot/src/command-handlers-modules/subagent-handlers.ts
5768
- import { EmbedBuilder as EmbedBuilder5 } from "discord.js";
5830
+ import { EmbedBuilder as EmbedBuilder3 } from "discord.js";
5769
5831
  async function handleSubagent(interaction) {
5770
5832
  if (!assertUserAllowed(interaction)) return;
5771
5833
  const sub = interaction.options.getSubcommand();
@@ -5805,7 +5867,7 @@ async function handleSubagentRun(interaction) {
5805
5867
  }
5806
5868
  try {
5807
5869
  const subSession = await spawnSubagent(session, label, provider, sessionChannel);
5808
- const embed = new EmbedBuilder5().setColor(15965202).setTitle(`\u{1F916} \u5B50\u4EFB\u52A1\u5DF2\u521B\u5EFA\uFF1A${label}`).addFields(
5870
+ const embed = new EmbedBuilder3().setColor(15965202).setTitle(`\u{1F916} \u5B50\u4EFB\u52A1\u5DF2\u521B\u5EFA\uFF1A${label}`).addFields(
5809
5871
  { name: "\u5B50\u533A", value: `<#${subSession.channelId}>`, inline: true },
5810
5872
  { name: "\u63D0\u4F9B\u5546", value: PROVIDER_LABELS[provider], inline: true },
5811
5873
  { name: "\u5D4C\u5957\u6DF1\u5EA6", value: `${subSession.subagentDepth}`, inline: true }
@@ -12774,24 +12836,20 @@ async function handleShellKill(interaction) {
12774
12836
  });
12775
12837
  }
12776
12838
 
12777
- // ../bot/src/button-handler.ts
12778
- import "discord.js";
12779
-
12780
12839
  // ../bot/src/button-handler-actions.ts
12781
12840
  import {
12782
- ActionRowBuilder as ActionRowBuilder3,
12783
- StringSelectMenuBuilder as StringSelectMenuBuilder2
12841
+ ActionRowBuilder as ActionRowBuilder2,
12842
+ StringSelectMenuBuilder
12784
12843
  } from "discord.js";
12785
12844
  function asComponentLike(component) {
12786
12845
  return component || {};
12787
12846
  }
12788
12847
  async function resolveAwaitingHumanIfNeeded(sessionId) {
12789
- const session = getSession(sessionId);
12848
+ const session = getSessionView(sessionId);
12790
12849
  if (!session?.currentInteractionMessageId) {
12791
12850
  return;
12792
12851
  }
12793
12852
  updateSession(sessionId, {
12794
- humanResolved: true,
12795
12853
  currentInteractionMessageId: void 0
12796
12854
  });
12797
12855
  await updateSessionState(sessionId, {
@@ -12838,7 +12896,7 @@ async function handleAwaitingHumanButton(interaction) {
12838
12896
  const sessionId = parts[1];
12839
12897
  const turn = parseInt(parts[2], 10);
12840
12898
  const action = parts[3];
12841
- const session = getSession(sessionId);
12899
+ const session = getSessionView(sessionId);
12842
12900
  if (!session) {
12843
12901
  await interaction.reply({ content: "\u4F1A\u8BDD\u4E0D\u5B58\u5728", ephemeral: true });
12844
12902
  return true;
@@ -12856,12 +12914,12 @@ async function handleAwaitingHumanButton(interaction) {
12856
12914
  await interaction.reply({ content: "\u5DF2\u88AB\u5176\u4ED6\u4EBA\u5904\u7406", ephemeral: true });
12857
12915
  return true;
12858
12916
  }
12859
- const activeGate = session.activeHumanGateId ? gateCoordinator.getGate(session.activeHumanGateId) : gateCoordinator.getActiveGateForSession(sessionId);
12917
+ const activeGate = session.activeHumanGateId ? gateService.getGate(session.activeHumanGateId) : gateService.getActiveGateForSession(sessionId);
12860
12918
  if (!activeGate) {
12861
12919
  await interaction.reply({ content: "\u672A\u627E\u5230\u6D3B\u8DC3\u7684\u95E8\u63A7\u8BB0\u5F55", ephemeral: true });
12862
12920
  return true;
12863
12921
  }
12864
- const result = await gateCoordinator.resolveFromDiscord(
12922
+ const result = await gateService.resolveFromDiscord(
12865
12923
  activeGate.id,
12866
12924
  action === "approve" ? "approve" : "reject"
12867
12925
  );
@@ -12872,8 +12930,8 @@ async function handleAwaitingHumanButton(interaction) {
12872
12930
  });
12873
12931
  return true;
12874
12932
  }
12933
+ stateMachine.setHumanResolved(sessionId, true);
12875
12934
  updateSession(sessionId, {
12876
- humanResolved: true,
12877
12935
  currentInteractionMessageId: void 0,
12878
12936
  activeHumanGateId: void 0
12879
12937
  });
@@ -12929,7 +12987,7 @@ async function handleContinueButton(interaction) {
12929
12987
  const customId = interaction.customId;
12930
12988
  if (!customId.startsWith("continue:")) return false;
12931
12989
  const sessionId = customId.slice(9);
12932
- const session = getSession(sessionId);
12990
+ const session = getSessionView(sessionId);
12933
12991
  if (!session) {
12934
12992
  await interaction.reply({ content: "\u4F1A\u8BDD\u4E0D\u5B58\u5728\u3002", ephemeral: true });
12935
12993
  return true;
@@ -13036,7 +13094,7 @@ async function handleModeButton(interaction) {
13036
13094
  const parts = customId.split(":");
13037
13095
  const sessionId = parts[1];
13038
13096
  const newMode = parts[2];
13039
- const session = getSession(sessionId);
13097
+ const session = getSessionView(sessionId);
13040
13098
  if (!session) {
13041
13099
  await interaction.reply({ content: "\u4F1A\u8BDD\u4E0D\u5B58\u5728\u3002", ephemeral: true });
13042
13100
  return true;
@@ -13053,7 +13111,7 @@ async function handleModeButton(interaction) {
13053
13111
  await interaction.reply({ content: `\u5DF2\u5207\u6362\u81F3 **${labels[newMode]}**`, ephemeral: true });
13054
13112
  try {
13055
13113
  const original = interaction.message;
13056
- const liveSession = getSession(sessionId);
13114
+ const liveSession = getSessionView(sessionId);
13057
13115
  const updatedComponents = original.components.map((row) => {
13058
13116
  if (!("components" in row)) return row;
13059
13117
  const first = asComponentLike(row.components?.[0]);
@@ -13074,7 +13132,7 @@ async function handleSelectMenuAction(interaction) {
13074
13132
  const sessionId = parts[1];
13075
13133
  const questionIndex = parseInt(parts[2], 10);
13076
13134
  const selected = interaction.values[0];
13077
- const session = getSession(sessionId);
13135
+ const session = getSessionView(sessionId);
13078
13136
  if (!session) {
13079
13137
  await interaction.reply({ content: "\u4F1A\u8BDD\u4E0D\u5B58\u5728\u3002", ephemeral: true });
13080
13138
  return true;
@@ -13089,7 +13147,7 @@ async function handleSelectMenuAction(interaction) {
13089
13147
  if (!("components" in row)) return row;
13090
13148
  const comp = asComponentLike(row.components?.[0]);
13091
13149
  if (comp?.customId !== customId) return row;
13092
- const menu = new StringSelectMenuBuilder2().setCustomId(customId).setPlaceholder(`\u5DF2\u9009\u62E9\uFF1A${selected.slice(0, 80)}`);
13150
+ const menu = new StringSelectMenuBuilder().setCustomId(customId).setPlaceholder(`\u5DF2\u9009\u62E9\uFF1A${selected.slice(0, 80)}`);
13093
13151
  for (const opt of comp.options || []) {
13094
13152
  menu.addOptions({
13095
13153
  label: opt.label,
@@ -13098,7 +13156,7 @@ async function handleSelectMenuAction(interaction) {
13098
13156
  default: opt.value === selected
13099
13157
  });
13100
13158
  }
13101
- return new ActionRowBuilder3().addComponents(menu);
13159
+ return new ActionRowBuilder2().addComponents(menu);
13102
13160
  });
13103
13161
  await original.edit({ components: updatedComponents });
13104
13162
  } catch {
@@ -13113,7 +13171,7 @@ async function handleSelectMenuAction(interaction) {
13113
13171
  const afterPrefix = customId.slice(14);
13114
13172
  const sessionId = afterPrefix.includes(":") ? afterPrefix.split(":")[0] : afterPrefix;
13115
13173
  const selected = interaction.values[0];
13116
- const session = getSession(sessionId);
13174
+ const session = getSessionView(sessionId);
13117
13175
  if (!session) {
13118
13176
  await interaction.reply({ content: "\u4F1A\u8BDD\u4E0D\u5B58\u5728\u3002", ephemeral: true });
13119
13177
  return true;
@@ -13132,7 +13190,7 @@ async function handleSelectMenuAction(interaction) {
13132
13190
  if (customId.startsWith("select:")) {
13133
13191
  const sessionId = customId.slice(7);
13134
13192
  const selected = interaction.values[0];
13135
- const session = getSession(sessionId);
13193
+ const session = getSessionView(sessionId);
13136
13194
  if (!session) {
13137
13195
  await interaction.reply({ content: "\u4F1A\u8BDD\u4E0D\u5B58\u5728\u3002", ephemeral: true });
13138
13196
  return true;
@@ -13151,31 +13209,58 @@ async function handleSelectMenuAction(interaction) {
13151
13209
  return false;
13152
13210
  }
13153
13211
 
13154
- // ../bot/src/button-handler.ts
13155
- async function handleButton(interaction) {
13156
- if (!isUserAllowed(interaction.user.id, config.allowedUsers, config.allowAllUsers)) {
13157
- console.warn(`[ButtonHandler] Unauthorized button press by user ${interaction.user.id}: ${interaction.customId}`);
13158
- await interaction.reply({ content: "\u672A\u6388\u6743\u3002", ephemeral: true });
13212
+ // ../bot/src/button-router.ts
13213
+ var BUTTON_ROUTES = [
13214
+ handleStopButton,
13215
+ handleAwaitingHumanButton,
13216
+ handleContinueButton,
13217
+ handleExpandButton,
13218
+ handleDeprecatedInteractionButton,
13219
+ handleCleanupButtons,
13220
+ handleModeButton
13221
+ ];
13222
+ var SELECT_ROUTES = [handleSelectMenuAction];
13223
+ async function checkAuthorization(userId, reply) {
13224
+ if (isUserAllowed(userId, config.allowedUsers, config.allowAllUsers)) return true;
13225
+ await reply("\u672A\u6388\u6743\u3002");
13226
+ return false;
13227
+ }
13228
+ async function routeButton(interaction) {
13229
+ const ok = await checkAuthorization(
13230
+ interaction.user.id,
13231
+ (content) => interaction.reply({ content, ephemeral: true })
13232
+ );
13233
+ if (!ok) {
13234
+ console.warn(
13235
+ `[ButtonHandler] Unauthorized button press by user ${interaction.user.id}: ${interaction.customId}`
13236
+ );
13159
13237
  return;
13160
13238
  }
13161
- if (await handleStopButton(interaction)) return;
13162
- if (await handleAwaitingHumanButton(interaction)) return;
13163
- if (await handleContinueButton(interaction)) return;
13164
- if (await handleExpandButton(interaction)) return;
13165
- if (await handleDeprecatedInteractionButton(interaction)) return;
13166
- if (await handleCleanupButtons(interaction)) return;
13167
- if (await handleModeButton(interaction)) return;
13239
+ for (const route of BUTTON_ROUTES) {
13240
+ if (await route(interaction)) return;
13241
+ }
13168
13242
  await interaction.reply({ content: "\u672A\u77E5\u6309\u94AE\u3002", ephemeral: true });
13169
13243
  }
13170
- async function handleSelectMenu(interaction) {
13171
- if (!isUserAllowed(interaction.user.id, config.allowedUsers, config.allowAllUsers)) {
13172
- await interaction.reply({ content: "\u672A\u6388\u6743\u3002", ephemeral: true });
13173
- return;
13244
+ async function routeSelectMenu(interaction) {
13245
+ const ok = await checkAuthorization(
13246
+ interaction.user.id,
13247
+ (content) => interaction.reply({ content, ephemeral: true })
13248
+ );
13249
+ if (!ok) return;
13250
+ for (const route of SELECT_ROUTES) {
13251
+ if (await route(interaction)) return;
13174
13252
  }
13175
- if (await handleSelectMenuAction(interaction)) return;
13176
13253
  await interaction.reply({ content: "\u672A\u77E5\u9009\u62E9\u3002", ephemeral: true });
13177
13254
  }
13178
13255
 
13256
+ // ../bot/src/button-handler.ts
13257
+ async function handleButton(interaction) {
13258
+ await routeButton(interaction);
13259
+ }
13260
+ async function handleSelectMenu(interaction) {
13261
+ await routeSelectMenu(interaction);
13262
+ }
13263
+
13179
13264
  // ../bot/src/bot-event-router.ts
13180
13265
  import {
13181
13266
  InteractionType
@@ -13290,7 +13375,7 @@ async function startBot() {
13290
13375
  const guild = client.guilds.cache.first();
13291
13376
  if (guild) {
13292
13377
  serviceContainer.logChannel = guild.channels.cache.find(
13293
- (ch) => ch.name === "bot-logs" && ch.type === ChannelType8.GuildText && !ch.parentId
13378
+ (ch) => ch.name === "bot-logs" && ch.type === ChannelType7.GuildText && !ch.parentId
13294
13379
  ) ?? serviceContainer.logChannel;
13295
13380
  logBuffer.setChannel(serviceContainer.logChannel);
13296
13381
  }
@@ -13356,7 +13441,7 @@ async function runCli() {
13356
13441
  await startBot();
13357
13442
  });
13358
13443
  cli.command("config [action] [key] [value]", "Manage configuration").action(async (action, key, value) => {
13359
- const { handleConfig } = await import("./config-cli-RQR2ZRQ5.js");
13444
+ const { handleConfig } = await import("./config-cli-7G5YWKJU.js");
13360
13445
  const args = [action || "list"].filter(Boolean);
13361
13446
  if (key) args.push(key);
13362
13447
  if (value) args.push(value);
@@ -13364,12 +13449,12 @@ async function runCli() {
13364
13449
  await handleConfig(args);
13365
13450
  });
13366
13451
  cli.command("project [action] [...args]", "Manage mounted projects").action(async (action, args) => {
13367
- const { handleProject: handleProject2 } = await import("./project-cli-6P6ZWDR6.js");
13452
+ const { handleProject: handleProject2 } = await import("./project-cli-ALKDLRMZ.js");
13368
13453
  const cmdArgs = [action || "help"].filter(Boolean).concat(args || []);
13369
13454
  await handleProject2(cmdArgs);
13370
13455
  });
13371
13456
  cli.command("attachment [action]", "Manage attachments").action(async (action) => {
13372
- const { handleAttachment } = await import("./attachment-cli-IGQBB6YJ.js");
13457
+ const { handleAttachment } = await import("./attachment-cli-AT4HXAGU.js");
13373
13458
  await handleAttachment([action || "fetch"]);
13374
13459
  });
13375
13460
  cli.command("daemon <action>", "Manage background service").action(async (action) => {
@@ -13377,7 +13462,7 @@ async function runCli() {
13377
13462
  await handleDaemon(action);
13378
13463
  });
13379
13464
  cli.command("codex [...options]", "Launch managed Codex session with remote approval").allowUnknownOptions().action(async function(options) {
13380
- const { handleCodexCommand } = await import("./codex-launcher-VDQ5VZPT.js");
13465
+ const { handleCodexCommand } = await import("./codex-launcher-CLGG4CVY.js");
13381
13466
  const parsed = this.options;
13382
13467
  const rawArgs = [];
13383
13468
  for (const [key, value] of Object.entries(parsed)) {