@jingyi0605/codingns 0.1.3 → 0.1.5

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 (224) hide show
  1. package/bin/codingns.mjs +47 -8
  2. package/dist/public/assets/{TerminalPage-Nq5sPc5c.js → TerminalPage-4p6EBqrR.js} +19 -19
  3. package/dist/public/assets/gemini-D4G1NbrE.png +0 -0
  4. package/dist/public/assets/index-CxeghocY.css +1 -0
  5. package/dist/public/assets/index-DXusStl0.js +108 -0
  6. package/dist/public/assets/kimi-BWNNSh7e.png +0 -0
  7. package/dist/public/index.html +2 -2
  8. package/dist/server/config/env.d.ts +7 -0
  9. package/dist/server/config/env.js +147 -1
  10. package/dist/server/config/env.js.map +1 -1
  11. package/dist/server/config/opencode-system-probe-helper-process.d.ts +24 -0
  12. package/dist/server/config/opencode-system-probe-helper-process.js +70 -5
  13. package/dist/server/config/opencode-system-probe-helper-process.js.map +1 -1
  14. package/dist/server/modules/auth/auth-service.d.ts +7 -1
  15. package/dist/server/modules/auth/auth-service.js +23 -3
  16. package/dist/server/modules/auth/auth-service.js.map +1 -1
  17. package/dist/server/modules/bootstrap/bootstrap-service.d.ts +3 -1
  18. package/dist/server/modules/bootstrap/bootstrap-service.js +7 -2
  19. package/dist/server/modules/bootstrap/bootstrap-service.js.map +1 -1
  20. package/dist/server/modules/butler/butler-action-context-service.d.ts +30 -0
  21. package/dist/server/modules/butler/butler-action-context-service.js +108 -0
  22. package/dist/server/modules/butler/butler-action-context-service.js.map +1 -0
  23. package/dist/server/modules/butler/butler-auth-service.d.ts +17 -0
  24. package/dist/server/modules/butler/butler-auth-service.js +91 -0
  25. package/dist/server/modules/butler/butler-auth-service.js.map +1 -0
  26. package/dist/server/modules/butler/butler-control-action-service.d.ts +65 -0
  27. package/dist/server/modules/butler/butler-control-action-service.js +296 -0
  28. package/dist/server/modules/butler/butler-control-action-service.js.map +1 -0
  29. package/dist/server/modules/butler/butler-control-session-service.d.ts +55 -0
  30. package/dist/server/modules/butler/butler-control-session-service.js +367 -0
  31. package/dist/server/modules/butler/butler-control-session-service.js.map +1 -0
  32. package/dist/server/modules/butler/butler-controller.d.ts +367 -0
  33. package/dist/server/modules/butler/butler-controller.js +475 -0
  34. package/dist/server/modules/butler/butler-controller.js.map +1 -0
  35. package/dist/server/modules/butler/butler-follow-up-evaluation-instruction-adapter.d.ts +34 -0
  36. package/dist/server/modules/butler/butler-follow-up-evaluation-instruction-adapter.js +77 -0
  37. package/dist/server/modules/butler/butler-follow-up-evaluation-instruction-adapter.js.map +1 -0
  38. package/dist/server/modules/butler/butler-follow-up-scheduler.d.ts +23 -0
  39. package/dist/server/modules/butler/butler-follow-up-scheduler.js +57 -0
  40. package/dist/server/modules/butler/butler-follow-up-scheduler.js.map +1 -0
  41. package/dist/server/modules/butler/butler-follow-up-service.d.ts +86 -0
  42. package/dist/server/modules/butler/butler-follow-up-service.js +948 -0
  43. package/dist/server/modules/butler/butler-follow-up-service.js.map +1 -0
  44. package/dist/server/modules/butler/butler-inbox-service.d.ts +35 -0
  45. package/dist/server/modules/butler/butler-inbox-service.js +136 -0
  46. package/dist/server/modules/butler/butler-inbox-service.js.map +1 -0
  47. package/dist/server/modules/butler/butler-notification-service.d.ts +12 -0
  48. package/dist/server/modules/butler/butler-notification-service.js +45 -0
  49. package/dist/server/modules/butler/butler-notification-service.js.map +1 -0
  50. package/dist/server/modules/butler/butler-profile-service.d.ts +26 -0
  51. package/dist/server/modules/butler/butler-profile-service.js +529 -0
  52. package/dist/server/modules/butler/butler-profile-service.js.map +1 -0
  53. package/dist/server/modules/butler/butler-project-service.d.ts +48 -0
  54. package/dist/server/modules/butler/butler-project-service.js +253 -0
  55. package/dist/server/modules/butler/butler-project-service.js.map +1 -0
  56. package/dist/server/modules/butler/butler-session-service.d.ts +79 -0
  57. package/dist/server/modules/butler/butler-session-service.js +503 -0
  58. package/dist/server/modules/butler/butler-session-service.js.map +1 -0
  59. package/dist/server/modules/butler/butler-session-summary-service.d.ts +55 -0
  60. package/dist/server/modules/butler/butler-session-summary-service.js +382 -0
  61. package/dist/server/modules/butler/butler-session-summary-service.js.map +1 -0
  62. package/dist/server/modules/butler/context-aggregator.d.ts +187 -0
  63. package/dist/server/modules/butler/context-aggregator.js +807 -0
  64. package/dist/server/modules/butler/context-aggregator.js.map +1 -0
  65. package/dist/server/modules/butler/instruction-adapter.d.ts +28 -0
  66. package/dist/server/modules/butler/instruction-adapter.js +101 -0
  67. package/dist/server/modules/butler/instruction-adapter.js.map +1 -0
  68. package/dist/server/modules/butler/patrol-execution-service.d.ts +47 -0
  69. package/dist/server/modules/butler/patrol-execution-service.js +347 -0
  70. package/dist/server/modules/butler/patrol-execution-service.js.map +1 -0
  71. package/dist/server/modules/butler/patrol-plan-service.d.ts +54 -0
  72. package/dist/server/modules/butler/patrol-plan-service.js +272 -0
  73. package/dist/server/modules/butler/patrol-plan-service.js.map +1 -0
  74. package/dist/server/modules/butler/patrol-run-service.d.ts +60 -0
  75. package/dist/server/modules/butler/patrol-run-service.js +185 -0
  76. package/dist/server/modules/butler/patrol-run-service.js.map +1 -0
  77. package/dist/server/modules/butler/patrol-scheduler.d.ts +36 -0
  78. package/dist/server/modules/butler/patrol-scheduler.js +99 -0
  79. package/dist/server/modules/butler/patrol-scheduler.js.map +1 -0
  80. package/dist/server/modules/butler/project-memory-service.d.ts +30 -0
  81. package/dist/server/modules/butler/project-memory-service.js +103 -0
  82. package/dist/server/modules/butler/project-memory-service.js.map +1 -0
  83. package/dist/server/modules/butler/provider-adapter-registry.d.ts +61 -0
  84. package/dist/server/modules/butler/provider-adapter-registry.js +430 -0
  85. package/dist/server/modules/butler/provider-adapter-registry.js.map +1 -0
  86. package/dist/server/modules/butler/session-summary-instruction-adapter.d.ts +28 -0
  87. package/dist/server/modules/butler/session-summary-instruction-adapter.js +79 -0
  88. package/dist/server/modules/butler/session-summary-instruction-adapter.js.map +1 -0
  89. package/dist/server/modules/butler/session-summary-scheduler.d.ts +23 -0
  90. package/dist/server/modules/butler/session-summary-scheduler.js +57 -0
  91. package/dist/server/modules/butler/session-summary-scheduler.js.map +1 -0
  92. package/dist/server/modules/butler/verification-run-service.d.ts +73 -0
  93. package/dist/server/modules/butler/verification-run-service.js +633 -0
  94. package/dist/server/modules/butler/verification-run-service.js.map +1 -0
  95. package/dist/server/modules/demo/demo-cleanup-service.d.ts +41 -0
  96. package/dist/server/modules/demo/demo-cleanup-service.js +111 -0
  97. package/dist/server/modules/demo/demo-cleanup-service.js.map +1 -0
  98. package/dist/server/modules/preferences/profile-service.js +8 -2
  99. package/dist/server/modules/preferences/profile-service.js.map +1 -1
  100. package/dist/server/modules/sessions/claude-runtime-helper-process.js +1 -1
  101. package/dist/server/modules/sessions/claude-runtime-helper-process.js.map +1 -1
  102. package/dist/server/modules/sessions/codex-app-server-helper-client.d.ts +5 -1
  103. package/dist/server/modules/sessions/codex-app-server-helper-client.js +10 -2
  104. package/dist/server/modules/sessions/codex-app-server-helper-client.js.map +1 -1
  105. package/dist/server/modules/sessions/session-controller.d.ts +3 -1
  106. package/dist/server/modules/sessions/session-controller.js +11 -2
  107. package/dist/server/modules/sessions/session-controller.js.map +1 -1
  108. package/dist/server/modules/sessions/session-history-service.d.ts +14 -1
  109. package/dist/server/modules/sessions/session-history-service.js +291 -30
  110. package/dist/server/modules/sessions/session-history-service.js.map +1 -1
  111. package/dist/server/modules/sessions/session-live-runtime-service.d.ts +25 -2
  112. package/dist/server/modules/sessions/session-live-runtime-service.js +526 -158
  113. package/dist/server/modules/sessions/session-live-runtime-service.js.map +1 -1
  114. package/dist/server/modules/sessions/session-provider-error-mapper.js +28 -0
  115. package/dist/server/modules/sessions/session-provider-error-mapper.js.map +1 -1
  116. package/dist/server/modules/terminal/terminal-service.js +2 -2
  117. package/dist/server/modules/terminal/terminal-service.js.map +1 -1
  118. package/dist/server/modules/workbench/workbench-service.d.ts +7 -1
  119. package/dist/server/modules/workbench/workbench-service.js +31 -7
  120. package/dist/server/modules/workbench/workbench-service.js.map +1 -1
  121. package/dist/server/routes/butler.d.ts +3 -0
  122. package/dist/server/routes/butler.js +54 -0
  123. package/dist/server/routes/butler.js.map +1 -0
  124. package/dist/server/routes/sessions.d.ts +1 -0
  125. package/dist/server/routes/sessions.js +12 -3
  126. package/dist/server/routes/sessions.js.map +1 -1
  127. package/dist/server/server/create-server.d.ts +61 -0
  128. package/dist/server/server/create-server.js +180 -10
  129. package/dist/server/server/create-server.js.map +1 -1
  130. package/dist/server/storage/repositories/butler-control-event-repository.d.ts +8 -0
  131. package/dist/server/storage/repositories/butler-control-event-repository.js +78 -0
  132. package/dist/server/storage/repositories/butler-control-event-repository.js.map +1 -0
  133. package/dist/server/storage/repositories/butler-control-session-repository.d.ts +11 -0
  134. package/dist/server/storage/repositories/butler-control-session-repository.js +86 -0
  135. package/dist/server/storage/repositories/butler-control-session-repository.js.map +1 -0
  136. package/dist/server/storage/repositories/butler-follow-up-task-repository.d.ts +16 -0
  137. package/dist/server/storage/repositories/butler-follow-up-task-repository.js +252 -0
  138. package/dist/server/storage/repositories/butler-follow-up-task-repository.js.map +1 -0
  139. package/dist/server/storage/repositories/butler-inbox-item-repository.d.ts +15 -0
  140. package/dist/server/storage/repositories/butler-inbox-item-repository.js +111 -0
  141. package/dist/server/storage/repositories/butler-inbox-item-repository.js.map +1 -0
  142. package/dist/server/storage/repositories/butler-notification-archive-repository.d.ts +9 -0
  143. package/dist/server/storage/repositories/butler-notification-archive-repository.js +48 -0
  144. package/dist/server/storage/repositories/butler-notification-archive-repository.js.map +1 -0
  145. package/dist/server/storage/repositories/butler-profile-repository.d.ts +9 -0
  146. package/dist/server/storage/repositories/butler-profile-repository.js +86 -0
  147. package/dist/server/storage/repositories/butler-profile-repository.js.map +1 -0
  148. package/dist/server/storage/repositories/butler-project-repository.d.ts +14 -0
  149. package/dist/server/storage/repositories/butler-project-repository.js +140 -0
  150. package/dist/server/storage/repositories/butler-project-repository.js.map +1 -0
  151. package/dist/server/storage/repositories/butler-session-repository.d.ts +11 -0
  152. package/dist/server/storage/repositories/butler-session-repository.js +106 -0
  153. package/dist/server/storage/repositories/butler-session-repository.js.map +1 -0
  154. package/dist/server/storage/repositories/butler-session-summary-state-repository.d.ts +8 -0
  155. package/dist/server/storage/repositories/butler-session-summary-state-repository.js +62 -0
  156. package/dist/server/storage/repositories/butler-session-summary-state-repository.js.map +1 -0
  157. package/dist/server/storage/repositories/patrol-plan-repository.d.ts +27 -0
  158. package/dist/server/storage/repositories/patrol-plan-repository.js +119 -0
  159. package/dist/server/storage/repositories/patrol-plan-repository.js.map +1 -0
  160. package/dist/server/storage/repositories/patrol-run-repository.d.ts +28 -0
  161. package/dist/server/storage/repositories/patrol-run-repository.js +121 -0
  162. package/dist/server/storage/repositories/patrol-run-repository.js.map +1 -0
  163. package/dist/server/storage/repositories/project-memory-repository.d.ts +15 -0
  164. package/dist/server/storage/repositories/project-memory-repository.js +150 -0
  165. package/dist/server/storage/repositories/project-memory-repository.js.map +1 -0
  166. package/dist/server/storage/repositories/session-checkpoint-repository.d.ts +9 -0
  167. package/dist/server/storage/repositories/session-checkpoint-repository.js +72 -0
  168. package/dist/server/storage/repositories/session-checkpoint-repository.js.map +1 -0
  169. package/dist/server/storage/repositories/session-message-origin-repository.d.ts +10 -0
  170. package/dist/server/storage/repositories/session-message-origin-repository.js +93 -0
  171. package/dist/server/storage/repositories/session-message-origin-repository.js.map +1 -0
  172. package/dist/server/storage/repositories/verification-run-repository.d.ts +29 -0
  173. package/dist/server/storage/repositories/verification-run-repository.js +125 -0
  174. package/dist/server/storage/repositories/verification-run-repository.js.map +1 -0
  175. package/dist/server/storage/sqlite/client.js +39 -0
  176. package/dist/server/storage/sqlite/client.js.map +1 -1
  177. package/dist/server/storage/sqlite/schema.sql +324 -0
  178. package/dist/server/types/domain.d.ts +261 -1
  179. package/dist/server/ws/ws-server.d.ts +2 -1
  180. package/dist/server/ws/ws-server.js +2 -1
  181. package/dist/server/ws/ws-server.js.map +1 -1
  182. package/node_modules/@codingns/session-sync-core/dist/index.d.ts +4 -0
  183. package/node_modules/@codingns/session-sync-core/dist/index.js +4 -0
  184. package/node_modules/@codingns/session-sync-core/dist/index.js.map +1 -1
  185. package/node_modules/@codingns/session-sync-core/dist/kimi-message-normalizer.d.ts +18 -0
  186. package/node_modules/@codingns/session-sync-core/dist/kimi-message-normalizer.js +659 -0
  187. package/node_modules/@codingns/session-sync-core/dist/kimi-message-normalizer.js.map +1 -0
  188. package/node_modules/@codingns/session-sync-core/dist/kimi-shared.d.ts +11 -0
  189. package/node_modules/@codingns/session-sync-core/dist/kimi-shared.js +72 -0
  190. package/node_modules/@codingns/session-sync-core/dist/kimi-shared.js.map +1 -0
  191. package/node_modules/@codingns/session-sync-core/dist/patch-builder.d.ts +8 -0
  192. package/node_modules/@codingns/session-sync-core/dist/patch-builder.js +89 -0
  193. package/node_modules/@codingns/session-sync-core/dist/patch-builder.js.map +1 -1
  194. package/node_modules/@codingns/session-sync-core/dist/providers/codex.js +4 -1
  195. package/node_modules/@codingns/session-sync-core/dist/providers/codex.js.map +1 -1
  196. package/node_modules/@codingns/session-sync-core/dist/providers/gemini.d.ts +41 -0
  197. package/node_modules/@codingns/session-sync-core/dist/providers/gemini.js +1086 -0
  198. package/node_modules/@codingns/session-sync-core/dist/providers/gemini.js.map +1 -0
  199. package/node_modules/@codingns/session-sync-core/dist/providers/kimi.d.ts +29 -0
  200. package/node_modules/@codingns/session-sync-core/dist/providers/kimi.js +578 -0
  201. package/node_modules/@codingns/session-sync-core/dist/providers/kimi.js.map +1 -0
  202. package/node_modules/@codingns/session-sync-core/dist/providers/opencode.js +2 -1
  203. package/node_modules/@codingns/session-sync-core/dist/providers/opencode.js.map +1 -1
  204. package/node_modules/@codingns/session-sync-core/dist/providers/utils.js +30 -2
  205. package/node_modules/@codingns/session-sync-core/dist/providers/utils.js.map +1 -1
  206. package/node_modules/@codingns/session-sync-core/dist/runtime/active-run-registry.d.ts +2 -0
  207. package/node_modules/@codingns/session-sync-core/dist/runtime/active-run-registry.js +43 -5
  208. package/node_modules/@codingns/session-sync-core/dist/runtime/active-run-registry.js.map +1 -1
  209. package/node_modules/@codingns/session-sync-core/dist/runtime/codex-runtime.d.ts +2 -0
  210. package/node_modules/@codingns/session-sync-core/dist/runtime/codex-runtime.js +320 -69
  211. package/node_modules/@codingns/session-sync-core/dist/runtime/codex-runtime.js.map +1 -1
  212. package/node_modules/@codingns/session-sync-core/dist/runtime/gemini-runtime.d.ts +21 -0
  213. package/node_modules/@codingns/session-sync-core/dist/runtime/gemini-runtime.js +537 -0
  214. package/node_modules/@codingns/session-sync-core/dist/runtime/gemini-runtime.js.map +1 -0
  215. package/node_modules/@codingns/session-sync-core/dist/runtime/kimi-runtime.d.ts +38 -0
  216. package/node_modules/@codingns/session-sync-core/dist/runtime/kimi-runtime.js +911 -0
  217. package/node_modules/@codingns/session-sync-core/dist/runtime/kimi-runtime.js.map +1 -0
  218. package/node_modules/@codingns/session-sync-core/dist/sqlite/node-sqlite.d.ts +6 -0
  219. package/node_modules/@codingns/session-sync-core/dist/sqlite/node-sqlite.js +9 -0
  220. package/node_modules/@codingns/session-sync-core/dist/sqlite/node-sqlite.js.map +1 -0
  221. package/node_modules/@codingns/session-sync-core/package.json +8 -0
  222. package/package.json +1 -1
  223. package/dist/public/assets/index-9hnprhO7.css +0 -1
  224. package/dist/public/assets/index-BTpmuKhG.js +0 -108
@@ -1,8 +1,10 @@
1
1
  import { existsSync, readdirSync } from "node:fs";
2
2
  import path from "node:path";
3
- import { ClaudeRuntimeAdapter, CodexRuntimeAdapter, OpenCodeRuntimeAdapter, ProviderRuntimeService } from "@codingns/session-sync-core";
3
+ import { performance } from "node:perf_hooks";
4
+ import { ClaudeRuntimeAdapter, CodexRuntimeAdapter, GeminiRuntimeAdapter, KimiRuntimeAdapter, OpenCodeRuntimeAdapter, ProviderRuntimeService } from "@codingns/session-sync-core";
4
5
  import { AppError } from "../../shared/errors/app-error.js";
5
6
  import { createId } from "../../shared/utils/id.js";
7
+ import { isPerfDebugEnabled, logPerformance } from "../../shared/utils/perf-log.js";
6
8
  import { logPermissionDebug } from "../../shared/utils/permission-debug-log.js";
7
9
  import { nowIso } from "../../shared/utils/time.js";
8
10
  import { SessionActivityAuthorityService } from "./session-activity-authority-service.js";
@@ -10,6 +12,8 @@ import { SessionPermissionRequestService } from "./session-permission-request-se
10
12
  import { mapSessionProviderError } from "./session-provider-error-mapper.js";
11
13
  import { ClaudeRuntimeHelperAdapter } from "./claude-runtime-helper-client.js";
12
14
  import { CodexAppServerHelperClient } from "./codex-app-server-helper-client.js";
15
+ const RUNTIME_START_BINDING_WAIT_TIMEOUT_MS = 10_000;
16
+ const START_BINDING_POLL_INTERVAL_MS = 50;
13
17
  export class SessionLiveRuntimeService {
14
18
  sessionHistoryService;
15
19
  sessionMessageAttachmentService;
@@ -28,10 +32,12 @@ export class SessionLiveRuntimeService {
28
32
  runtimeAdapterDisposables;
29
33
  externalRuntimeSnapshots = new Map();
30
34
  runtimeListeners = new Map();
35
+ terminalStateListeners = new Set();
31
36
  runtimeMessageSeenSessions = new Set();
32
37
  runtimeHistoryFallbackSentSessions = new Set();
33
38
  queueDispatchSessions = new Set();
34
39
  queueRetryTimers = new Map();
40
+ pendingSendDebugTracesBySessionId = new Map();
35
41
  constructor(sessionHistoryService, sessionMessageAttachmentService, workspaceService, sessionChangedFileService, sessionBindingRepository, authUserRepository, sessionSendQueueRepository, sessionIndexRepository, sessionStateRepository, sessionStatusSnapshotRepository, config, sessionActivityAuthorityService = new SessionActivityAuthorityService()) {
36
42
  this.sessionHistoryService = sessionHistoryService;
37
43
  this.sessionMessageAttachmentService = sessionMessageAttachmentService;
@@ -58,63 +64,109 @@ export class SessionLiveRuntimeService {
58
64
  }
59
65
  async startLiveSession(input) {
60
66
  const requestStartedAt = nowIso();
61
- const capabilities = this.sessionHistoryService.getProviderCapabilitiesSnapshot(input.provider);
62
- const workspace = this.workspaceService.getWorkspaceOrThrow(input.workspaceId);
63
67
  const sessionId = createId();
64
- this.ensurePendingSessionBinding(sessionId, workspace.id, input.provider);
65
- const persistedAttachments = this.persistMessageAttachments(sessionId, input.clientRequestId, input.runtimeOptions?.attachments ?? []);
66
- const providerPrompt = this.sessionMessageAttachmentService.buildProviderPrompt(input.provider, input.content, persistedAttachments.runtimeAttachments);
67
- this.ensureCapability(capabilities.canStartSession, "provider", "provider 不支持 start-live");
68
- this.ensureCapability(capabilities.canSendMessage, "provider", "provider 不支持实时对话");
69
- const handle = await this.launchRuntimeRun({
70
- sessionId,
71
- workspaceId: workspace.id,
72
- workspacePath: workspace.path,
73
- provider: input.provider,
74
- providerSessionId: null,
75
- rawStoreRef: null,
76
- sequenceBase: 1,
77
- options: {
78
- content: input.content,
79
- clientRequestId: input.clientRequestId,
80
- model: input.runtimeOptions?.model ?? null,
81
- reasoningLevel: input.runtimeOptions?.reasoningLevel ?? null,
82
- permissionMode: input.runtimeOptions?.permissionMode ?? null,
83
- providerPrompt,
84
- attachments: persistedAttachments.runtimeAttachments
85
- }
86
- }, "start");
87
- const snapshot = handle.getSnapshot();
88
- this.attachRuntimePersistence(handle, sessionId, workspace.id, input.userId);
89
- this.createRuntimeBackedSession({
68
+ const workspace = this.workspaceService.getWorkspaceOrThrow(input.workspaceId);
69
+ const debugTrace = this.beginPendingSendDebugTrace({
70
+ mode: "start_live",
90
71
  sessionId,
91
72
  workspaceId: workspace.id,
92
- userId: input.userId,
93
73
  provider: input.provider,
94
- initialContent: input.content,
95
- snapshot
74
+ clientRequestId: input.clientRequestId
96
75
  });
97
- const binding = this.sessionHistoryService.getBindingOrThrow(sessionId);
98
- const acceptedMessage = await this.findAcceptedUserMessage(sessionId, this.sessionMessageAttachmentService.buildAcceptedContentCandidates(input.content, providerPrompt), requestStartedAt);
99
- const acceptedAt = acceptedMessage?.timestamp ?? nowIso();
100
- const boundAttachments = this.sessionMessageAttachmentService.bindClientRequestToMessage(sessionId, input.clientRequestId, acceptedMessage?.messageId ?? null);
101
- return {
102
- sessionId,
103
- provider: input.provider,
104
- providerSessionId: binding.providerSessionId,
105
- acceptedAt,
106
- clientRequestId: input.clientRequestId,
107
- message: (acceptedMessage
108
- ? {
109
- ...acceptedMessage,
110
- attachments: boundAttachments
76
+ try {
77
+ const capabilities = this.sessionHistoryService.getProviderCapabilitiesSnapshot(input.provider);
78
+ this.ensurePendingSessionBinding(sessionId, workspace.id, input.provider);
79
+ const persistedAttachments = this.persistMessageAttachments(sessionId, input.clientRequestId, input.runtimeOptions?.attachments ?? []);
80
+ const providerPrompt = this.sessionMessageAttachmentService.buildProviderPrompt(input.provider, input.content, persistedAttachments.runtimeAttachments);
81
+ this.ensureCapability(capabilities.canStartSession, "provider", "provider 不支持 start-live");
82
+ this.ensureCapability(capabilities.canSendMessage, "provider", "provider 不支持实时对话");
83
+ const launchRuntimeStartedAtMs = performance.now();
84
+ const handle = await this.launchRuntimeRun({
85
+ sessionId,
86
+ workspaceId: workspace.id,
87
+ workspacePath: workspace.path,
88
+ provider: input.provider,
89
+ providerSessionId: null,
90
+ rawStoreRef: null,
91
+ sequenceBase: 1,
92
+ options: {
93
+ content: input.content,
94
+ clientRequestId: input.clientRequestId,
95
+ model: input.runtimeOptions?.model ?? null,
96
+ reasoningLevel: input.runtimeOptions?.reasoningLevel ?? null,
97
+ permissionMode: input.runtimeOptions?.permissionMode ?? null,
98
+ providerPrompt,
99
+ attachments: persistedAttachments.runtimeAttachments
111
100
  }
112
- : null) ??
113
- createSyntheticUserMessage(input.provider, binding.providerSessionId, input.content, acceptedAt, 1, boundAttachments.length > 0
114
- ? boundAttachments
115
- : persistedAttachments.messageAttachments),
116
- session: this.sessionHistoryService.getSession(sessionId, input.userId)
117
- };
101
+ }, "start");
102
+ this.logSendDebugStep(debugTrace, "launch_runtime", launchRuntimeStartedAtMs, {
103
+ userId: input.userId
104
+ });
105
+ const snapshot = handle.getSnapshot();
106
+ this.attachRuntimePersistence(handle, sessionId, workspace.id, input.userId);
107
+ this.createRuntimeBackedSession({
108
+ sessionId,
109
+ workspaceId: workspace.id,
110
+ userId: input.userId,
111
+ provider: input.provider,
112
+ initialContent: input.content,
113
+ snapshot
114
+ });
115
+ const startBindingTask = this.waitForResolvedStartBinding(sessionId, workspace.id, input.provider, handle).catch(() => {
116
+ return;
117
+ });
118
+ if (shouldAwaitStartBindingBeforeAcceptedUserLookup(input.provider)) {
119
+ const bindingWaitStartedAtMs = performance.now();
120
+ await Promise.race([
121
+ startBindingTask,
122
+ waitForAcceptedUserLookupWindow()
123
+ ]);
124
+ this.logSendDebugStep(debugTrace, "binding_wait", bindingWaitStartedAtMs, {
125
+ provider: input.provider
126
+ });
127
+ }
128
+ const binding = this.sessionHistoryService.getBindingOrThrow(sessionId);
129
+ const acceptedLookupStartedAtMs = performance.now();
130
+ const acceptedMessage = shouldAwaitAcceptedUserMessage(input.provider)
131
+ ? await this.findAcceptedUserMessage(sessionId, this.sessionMessageAttachmentService.buildAcceptedContentCandidates(input.content, providerPrompt), requestStartedAt)
132
+ : null;
133
+ this.logSendDebugStep(debugTrace, "accepted_user_lookup", acceptedLookupStartedAtMs, {
134
+ awaited: shouldAwaitAcceptedUserMessage(input.provider),
135
+ matched: Boolean(acceptedMessage)
136
+ });
137
+ if (!shouldAwaitStartBindingBeforeAcceptedUserLookup(input.provider)) {
138
+ void startBindingTask;
139
+ }
140
+ const acceptedAt = acceptedMessage?.timestamp ?? nowIso();
141
+ const boundAttachments = this.sessionMessageAttachmentService.bindClientRequestToMessage(sessionId, input.clientRequestId, acceptedMessage?.messageId ?? null);
142
+ const session = this.sessionHistoryService.getSession(sessionId, input.userId);
143
+ this.markSendDebugResponseReady(debugTrace, {
144
+ returnedAcceptedMessage: Boolean(acceptedMessage),
145
+ returnedSyntheticUser: !acceptedMessage,
146
+ providerSessionId: binding.providerSessionId
147
+ });
148
+ return {
149
+ sessionId: session.sessionId,
150
+ provider: input.provider,
151
+ providerSessionId: binding.providerSessionId,
152
+ acceptedAt,
153
+ clientRequestId: input.clientRequestId,
154
+ message: (acceptedMessage
155
+ ? {
156
+ ...acceptedMessage,
157
+ attachments: boundAttachments
158
+ }
159
+ : null) ??
160
+ createSyntheticUserMessage(input.provider, binding.providerSessionId, input.content, acceptedAt, 1, boundAttachments.length > 0
161
+ ? boundAttachments
162
+ : persistedAttachments.messageAttachments),
163
+ session
164
+ };
165
+ }
166
+ catch (error) {
167
+ this.failPendingSendDebugTrace(debugTrace, error);
168
+ throw error;
169
+ }
118
170
  }
119
171
  async sendLiveMessage(input) {
120
172
  return this.sendLiveMessageDirect(input);
@@ -169,7 +221,8 @@ export class SessionLiveRuntimeService {
169
221
  field: "queueItemId"
170
222
  });
171
223
  }
172
- const runtimeSnapshot = this.providerRuntimeService.getSnapshot(sessionId);
224
+ const runtimeSessionId = this.resolveRuntimeSessionId(sessionId);
225
+ const runtimeSnapshot = this.providerRuntimeService.getSnapshot(runtimeSessionId);
173
226
  if (!runtimeSnapshot || !isActiveRuntimeState(runtimeSnapshot.runningState)) {
174
227
  throw new AppError({
175
228
  statusCode: 409,
@@ -225,7 +278,7 @@ export class SessionLiveRuntimeService {
225
278
  };
226
279
  }
227
280
  catch (error) {
228
- if (isQueueDispatchDeferredError(error)) {
281
+ if (isQueueDispatchRetryableError(error)) {
229
282
  this.sessionSendQueueRepository.markQueued(queueItem.id, nowIso());
230
283
  this.scheduleQueueRetry(sessionId);
231
284
  }
@@ -368,8 +421,9 @@ export class SessionLiveRuntimeService {
368
421
  };
369
422
  }
370
423
  async getSessionRuntime(sessionId, userId) {
371
- const runtimeSnapshot = this.providerRuntimeService.getSnapshot(sessionId);
372
- const externalRuntimeSnapshot = this.externalRuntimeSnapshots.get(sessionId) ?? null;
424
+ const runtimeSessionId = this.resolveRuntimeSessionId(sessionId);
425
+ const runtimeSnapshot = this.providerRuntimeService.getSnapshot(runtimeSessionId);
426
+ const externalRuntimeSnapshot = this.externalRuntimeSnapshots.get(runtimeSessionId) ?? null;
373
427
  const session = runtimeSnapshot || externalRuntimeSnapshot
374
428
  ? this.sessionHistoryService.getSession(sessionId, userId)
375
429
  : await this.sessionHistoryService.refreshRuntimeFallbackSession(sessionId, userId);
@@ -377,9 +431,9 @@ export class SessionLiveRuntimeService {
377
431
  const capabilities = await this.sessionHistoryService.getSessionCapabilities(sessionId);
378
432
  const contextUsage = await this.sessionHistoryService.getSessionContextUsage(sessionId).catch(() => null);
379
433
  const resolution = runtimeSnapshot
380
- ? this.sessionActivityAuthorityService.observe(createRuntimeActivityObservation(sessionId, runtimeSnapshot))
434
+ ? this.sessionActivityAuthorityService.observe(createRuntimeActivityObservation(runtimeSessionId, runtimeSnapshot))
381
435
  : externalRuntimeSnapshot
382
- ? this.sessionActivityAuthorityService.observe(createExternalRuntimeActivityObservation(sessionId, externalRuntimeSnapshot))
436
+ ? this.sessionActivityAuthorityService.observe(createExternalRuntimeActivityObservation(runtimeSessionId, externalRuntimeSnapshot))
383
437
  : this.sessionActivityAuthorityService.resolvePersistedSession(session);
384
438
  if (runtimeSnapshot) {
385
439
  return {
@@ -451,7 +505,8 @@ export class SessionLiveRuntimeService {
451
505
  }
452
506
  async interruptSession(sessionId, userId) {
453
507
  this.sessionHistoryService.getSession(sessionId, userId);
454
- const runtime = this.providerRuntimeService.getSnapshot(sessionId);
508
+ const runtimeSessionId = this.resolveRuntimeSessionId(sessionId);
509
+ const runtime = this.providerRuntimeService.getSnapshot(runtimeSessionId);
455
510
  if (!runtime || (runtime.runningState !== "running" && runtime.runningState !== "starting")) {
456
511
  throw new AppError({
457
512
  statusCode: 409,
@@ -460,7 +515,7 @@ export class SessionLiveRuntimeService {
460
515
  field: "sessionId"
461
516
  });
462
517
  }
463
- const interrupted = await this.providerRuntimeService.interrupt(sessionId).catch((error) => {
518
+ const interrupted = await this.providerRuntimeService.interrupt(runtimeSessionId).catch((error) => {
464
519
  if (error instanceof Error && error.message === "INTERRUPT_NOT_SUPPORTED") {
465
520
  throw new AppError({
466
521
  statusCode: 400,
@@ -477,6 +532,14 @@ export class SessionLiveRuntimeService {
477
532
  detail: interrupted.detail ?? "interrupt requested"
478
533
  };
479
534
  }
535
+ registerTerminalStateListener(listener) {
536
+ this.terminalStateListeners.add(listener);
537
+ return {
538
+ close: () => {
539
+ this.terminalStateListeners.delete(listener);
540
+ }
541
+ };
542
+ }
480
543
  async listPermissionRequests(sessionId, userId) {
481
544
  return this.sessionPermissionRequestService.listSessionPermissionRequests(sessionId, userId);
482
545
  }
@@ -484,9 +547,10 @@ export class SessionLiveRuntimeService {
484
547
  return this.sessionPermissionRequestService.replyToSessionPermissionRequest(sessionId, userId, requestId, input);
485
548
  }
486
549
  subscribeRuntime(sessionId, onEnvelope) {
487
- const runtimeSnapshot = this.providerRuntimeService.getSnapshot(sessionId);
488
- const externalRuntimeSnapshot = this.externalRuntimeSnapshots.get(sessionId) ?? null;
489
- const initialActivityEnvelope = this.buildSessionActivityEnvelope(sessionId);
550
+ const runtimeSessionId = this.resolveRuntimeSessionId(sessionId);
551
+ const runtimeSnapshot = this.providerRuntimeService.getSnapshot(runtimeSessionId);
552
+ const externalRuntimeSnapshot = this.externalRuntimeSnapshots.get(runtimeSessionId) ?? null;
553
+ const initialActivityEnvelope = this.buildSessionActivityEnvelope(sessionId, runtimeSessionId);
490
554
  if (runtimeSnapshot) {
491
555
  void onEnvelope({
492
556
  type: "session.runtime_status",
@@ -508,16 +572,21 @@ export class SessionLiveRuntimeService {
508
572
  if (initialActivityEnvelope) {
509
573
  void onEnvelope(initialActivityEnvelope);
510
574
  }
511
- const runtimeSubscription = this.providerRuntimeService.subscribe(sessionId, async (event) => {
512
- const envelope = this.mapRuntimeEventToEnvelope(sessionId, event);
575
+ const runtimeSubscription = this.providerRuntimeService.subscribe(runtimeSessionId, async (event) => {
576
+ const envelope = this.mapRuntimeEventToEnvelope(sessionId, event, runtimeSessionId);
513
577
  if (!envelope) {
514
578
  return;
515
579
  }
516
580
  await onEnvelope(envelope);
517
581
  });
518
- const externalSubscription = this.subscribeExternalRuntime(sessionId, onEnvelope);
519
- const activitySubscription = this.sessionActivityAuthorityService.subscribe(sessionId, async () => {
520
- const envelope = this.buildSessionActivityEnvelope(sessionId);
582
+ const externalSubscription = this.subscribeExternalRuntime(runtimeSessionId, async (envelope) => {
583
+ await onEnvelope({
584
+ ...envelope,
585
+ sessionId
586
+ });
587
+ });
588
+ const activitySubscription = this.sessionActivityAuthorityService.subscribe(runtimeSessionId, async () => {
589
+ const envelope = this.buildSessionActivityEnvelope(sessionId, runtimeSessionId);
521
590
  if (!envelope) {
522
591
  return;
523
592
  }
@@ -578,31 +647,67 @@ export class SessionLiveRuntimeService {
578
647
  await listener(envelope);
579
648
  }));
580
649
  }
581
- buildSessionActivityEnvelope(sessionId) {
582
- const runtimeSnapshot = this.providerRuntimeService.getSnapshot(sessionId);
650
+ buildSessionActivityEnvelope(sessionId, runtimeSessionId = sessionId) {
651
+ const runtimeSnapshot = this.providerRuntimeService.getSnapshot(runtimeSessionId);
583
652
  if (runtimeSnapshot) {
584
- const resolution = this.sessionActivityAuthorityService.observe(createRuntimeActivityObservation(sessionId, runtimeSnapshot));
585
- return this.mapResolutionToActivityEnvelope(resolution, {
586
- hasActiveRun: true,
587
- canInterrupt: runtimeSnapshot.supportsInterrupt
588
- });
653
+ const resolution = this.sessionActivityAuthorityService.observe(createRuntimeActivityObservation(runtimeSessionId, runtimeSnapshot));
654
+ return {
655
+ ...this.mapResolutionToActivityEnvelope(resolution, {
656
+ hasActiveRun: true,
657
+ canInterrupt: runtimeSnapshot.supportsInterrupt
658
+ }),
659
+ sessionId
660
+ };
589
661
  }
590
- const externalRuntimeSnapshot = this.externalRuntimeSnapshots.get(sessionId) ?? null;
662
+ const externalRuntimeSnapshot = this.externalRuntimeSnapshots.get(runtimeSessionId) ?? null;
591
663
  if (externalRuntimeSnapshot) {
592
- const resolution = this.sessionActivityAuthorityService.observe(createExternalRuntimeActivityObservation(sessionId, externalRuntimeSnapshot));
593
- return this.mapResolutionToActivityEnvelope(resolution, {
594
- hasActiveRun: true,
595
- canInterrupt: false
596
- });
664
+ const resolution = this.sessionActivityAuthorityService.observe(createExternalRuntimeActivityObservation(runtimeSessionId, externalRuntimeSnapshot));
665
+ return {
666
+ ...this.mapResolutionToActivityEnvelope(resolution, {
667
+ hasActiveRun: true,
668
+ canInterrupt: false
669
+ }),
670
+ sessionId
671
+ };
597
672
  }
598
- const resolution = this.sessionActivityAuthorityService.getResolution(sessionId);
673
+ const resolution = this.sessionActivityAuthorityService.getResolution(runtimeSessionId);
599
674
  if (!resolution) {
600
675
  return null;
601
676
  }
602
- return this.mapResolutionToActivityEnvelope(resolution, {
603
- hasActiveRun: resolution.runningState === "stale" || resolution.runningState === "unknown",
604
- canInterrupt: false
605
- });
677
+ return {
678
+ ...this.mapResolutionToActivityEnvelope(resolution, {
679
+ hasActiveRun: resolution.runningState === "stale" || resolution.runningState === "unknown",
680
+ canInterrupt: false
681
+ }),
682
+ sessionId
683
+ };
684
+ }
685
+ resolveRuntimeSessionId(sessionId) {
686
+ if (this.providerRuntimeService.getSnapshot(sessionId)
687
+ || this.externalRuntimeSnapshots.has(sessionId)) {
688
+ return sessionId;
689
+ }
690
+ const listSnapshots = "listSnapshots" in this.providerRuntimeService
691
+ && typeof this.providerRuntimeService.listSnapshots === "function"
692
+ ? this.providerRuntimeService.listSnapshots.bind(this.providerRuntimeService)
693
+ : null;
694
+ if (!listSnapshots) {
695
+ return sessionId;
696
+ }
697
+ const linkedSnapshot = listSnapshots()
698
+ .find((snapshot) => this.isLinkedGeminiRuntimeSession(snapshot.sessionId, sessionId));
699
+ return linkedSnapshot?.sessionId ?? sessionId;
700
+ }
701
+ isLinkedGeminiRuntimeSession(candidateSessionId, targetSessionId) {
702
+ if (candidateSessionId === targetSessionId) {
703
+ return true;
704
+ }
705
+ const binding = this.sessionBindingRepository.findBySessionId(candidateSessionId);
706
+ if (!binding || binding.provider !== "gemini") {
707
+ return false;
708
+ }
709
+ return isGeminiPendingRuntimeAliasBinding(binding.providerSessionId, targetSessionId)
710
+ || isGeminiPendingRuntimeAliasBinding(binding.rawStoreRef, targetSessionId);
606
711
  }
607
712
  mapResolutionToActivityEnvelope(resolution, options) {
608
713
  return {
@@ -764,6 +869,13 @@ export class SessionLiveRuntimeService {
764
869
  };
765
870
  await this.emitExternalRuntimeEnvelope(envelope);
766
871
  if (isTerminalSessionRunningState(input.runningState)) {
872
+ await this.emitTerminalStateEvent({
873
+ sessionId: input.sessionId,
874
+ status: input.runningState,
875
+ timestamp: input.timestamp,
876
+ detail: input.detail,
877
+ source: "external_runtime"
878
+ });
767
879
  void this.dispatchNextQueuedMessage(input.sessionId);
768
880
  }
769
881
  }
@@ -794,80 +906,114 @@ export class SessionLiveRuntimeService {
794
906
  async sendLiveMessageDirect(input, persistedAttachments) {
795
907
  const requestStartedAt = nowIso();
796
908
  const session = this.sessionHistoryService.getSession(input.sessionId, input.userId);
797
- const capabilities = await this.sessionHistoryService.getSessionCapabilities(input.sessionId);
798
- const workspace = this.workspaceService.getWorkspaceOrThrow(session.workspaceId);
799
- const runtimeMode = shouldStartNativeSessionOnFirstMessage(session);
800
- const resolvedAttachments = persistedAttachments
801
- ?? this.persistMessageAttachments(input.sessionId, input.clientRequestId, input.runtimeOptions?.attachments ?? []);
802
- const providerPrompt = this.sessionMessageAttachmentService.buildProviderPrompt(session.provider, input.content, resolvedAttachments.runtimeAttachments);
803
- this.ensureCapability(capabilities.canSendMessage, "sessionId", "provider 不支持实时对话");
804
- const runtimeRequest = {
909
+ const debugTrace = this.beginPendingSendDebugTrace({
910
+ mode: "send_live",
805
911
  sessionId: input.sessionId,
806
912
  workspaceId: session.workspaceId,
807
- workspacePath: workspace.path,
808
913
  provider: session.provider,
809
- providerSessionId: runtimeMode === "start" ? null : session.providerSessionId,
810
- rawStoreRef: runtimeMode === "start" ? null : session.rawStoreRef,
811
- sequenceBase: runtimeMode === "start"
914
+ clientRequestId: input.clientRequestId
915
+ });
916
+ try {
917
+ const capabilities = await this.sessionHistoryService.getSessionCapabilities(input.sessionId);
918
+ const workspace = this.workspaceService.getWorkspaceOrThrow(session.workspaceId);
919
+ const runtimeMode = shouldStartNativeSessionOnFirstMessage(session);
920
+ const nextUserSequence = runtimeMode === "start"
812
921
  ? 1
813
- : Math.max(session.messageCount + 1, 1),
814
- options: {
815
- content: input.content,
816
- clientRequestId: input.clientRequestId,
817
- model: input.runtimeOptions?.model ?? null,
818
- reasoningLevel: input.runtimeOptions?.reasoningLevel ?? null,
819
- permissionMode: input.runtimeOptions?.permissionMode ?? null,
820
- providerPrompt,
821
- attachments: resolvedAttachments.runtimeAttachments
922
+ : await this.resolveNextUserSequence(input.sessionId, session.messageCount);
923
+ const resolvedAttachments = persistedAttachments
924
+ ?? this.persistMessageAttachments(input.sessionId, input.clientRequestId, input.runtimeOptions?.attachments ?? []);
925
+ const providerPrompt = this.sessionMessageAttachmentService.buildProviderPrompt(session.provider, input.content, resolvedAttachments.runtimeAttachments);
926
+ this.ensureCapability(capabilities.canSendMessage, "sessionId", "provider 不支持实时对话");
927
+ const runtimeRequest = {
928
+ sessionId: input.sessionId,
929
+ workspaceId: session.workspaceId,
930
+ workspacePath: workspace.path,
931
+ provider: session.provider,
932
+ providerSessionId: runtimeMode === "start" ? null : session.providerSessionId,
933
+ rawStoreRef: runtimeMode === "start" ? null : session.rawStoreRef,
934
+ sequenceBase: nextUserSequence,
935
+ options: {
936
+ content: input.content,
937
+ clientRequestId: input.clientRequestId,
938
+ model: input.runtimeOptions?.model ?? null,
939
+ reasoningLevel: input.runtimeOptions?.reasoningLevel ?? null,
940
+ permissionMode: input.runtimeOptions?.permissionMode ?? null,
941
+ providerPrompt,
942
+ attachments: resolvedAttachments.runtimeAttachments
943
+ }
944
+ };
945
+ const runtimeSessionId = this.resolveRuntimeSessionId(input.sessionId);
946
+ const activeRun = this.providerRuntimeService.getSnapshot(runtimeSessionId);
947
+ const externalRuntimeSnapshot = this.externalRuntimeSnapshots.get(runtimeSessionId);
948
+ if (activeRun &&
949
+ activeRun.provider === "claude-code" &&
950
+ isActiveRuntimeState(activeRun.runningState)) {
951
+ this.clearExternalRuntimeSnapshot(runtimeSessionId);
822
952
  }
823
- };
824
- const activeRun = this.providerRuntimeService.getSnapshot(input.sessionId);
825
- const externalRuntimeSnapshot = this.externalRuntimeSnapshots.get(input.sessionId);
826
- if (activeRun &&
827
- activeRun.provider === "claude-code" &&
828
- isActiveRuntimeState(activeRun.runningState)) {
829
- this.clearExternalRuntimeSnapshot(input.sessionId);
830
- }
831
- if (!activeRun &&
832
- session.provider === "claude-code" &&
833
- externalRuntimeSnapshot &&
834
- isActiveRuntimeState(externalRuntimeSnapshot.runningState)) {
835
- throw new AppError({
836
- statusCode: 409,
837
- errorCode: "SESSION_EXTERNAL_RUN_ACTIVE",
838
- detail: "当前 Claude 外部会话仍在运行,不能直接追加;请加入队列或等待当前轮结束",
839
- field: "sessionId"
953
+ if (!activeRun &&
954
+ session.provider === "claude-code" &&
955
+ externalRuntimeSnapshot &&
956
+ isActiveRuntimeState(externalRuntimeSnapshot.runningState)) {
957
+ throw new AppError({
958
+ statusCode: 409,
959
+ errorCode: "SESSION_EXTERNAL_RUN_ACTIVE",
960
+ detail: "当前 Claude 外部会话仍在运行,不能直接追加;请加入队列或等待当前轮结束",
961
+ field: "sessionId"
962
+ });
963
+ }
964
+ if (activeRun && isActiveRuntimeState(activeRun.runningState)) {
965
+ const submitStartedAtMs = performance.now();
966
+ await this.providerRuntimeService.submitToActiveRun(runtimeSessionId, runtimeRequest.options)
967
+ .catch((error) => {
968
+ throw mapSessionProviderError(error);
969
+ });
970
+ this.logSendDebugStep(debugTrace, "submit_to_active_run", submitStartedAtMs, {
971
+ runtimeMode,
972
+ activeRunState: activeRun.runningState
973
+ });
974
+ }
975
+ else {
976
+ const startRuntimeStartedAtMs = performance.now();
977
+ await this.startRuntimeRun(runtimeRequest, input.userId, runtimeMode);
978
+ this.logSendDebugStep(debugTrace, "start_runtime_run", startRuntimeStartedAtMs, {
979
+ runtimeMode
980
+ });
981
+ }
982
+ const binding = this.sessionHistoryService.getBindingOrThrow(input.sessionId);
983
+ const acceptedLookupStartedAtMs = performance.now();
984
+ const acceptedMessage = await this.findAcceptedUserMessage(input.sessionId, this.sessionMessageAttachmentService.buildAcceptedContentCandidates(input.content, providerPrompt), requestStartedAt);
985
+ this.logSendDebugStep(debugTrace, "accepted_user_lookup", acceptedLookupStartedAtMs, {
986
+ matched: Boolean(acceptedMessage)
840
987
  });
841
- }
842
- if (activeRun && isActiveRuntimeState(activeRun.runningState)) {
843
- await this.providerRuntimeService.submitToActiveRun(input.sessionId, runtimeRequest.options)
844
- .catch((error) => {
845
- throw mapSessionProviderError(error);
988
+ const acceptedAt = acceptedMessage?.timestamp ?? nowIso();
989
+ const boundAttachments = this.sessionMessageAttachmentService.bindClientRequestToMessage(input.sessionId, input.clientRequestId, acceptedMessage?.messageId ?? null);
990
+ this.markSendDebugResponseReady(debugTrace, {
991
+ runtimeMode,
992
+ returnedAcceptedMessage: Boolean(acceptedMessage),
993
+ returnedSyntheticUser: !acceptedMessage,
994
+ providerSessionId: binding.providerSessionId
846
995
  });
996
+ return {
997
+ sessionId: input.sessionId,
998
+ provider: session.provider,
999
+ providerSessionId: binding.providerSessionId,
1000
+ acceptedAt,
1001
+ clientRequestId: input.clientRequestId,
1002
+ message: (acceptedMessage
1003
+ ? {
1004
+ ...acceptedMessage,
1005
+ attachments: boundAttachments
1006
+ }
1007
+ : null) ??
1008
+ createSyntheticUserMessage(session.provider, binding.providerSessionId, input.content, acceptedAt, nextUserSequence, boundAttachments.length > 0
1009
+ ? boundAttachments
1010
+ : resolvedAttachments.messageAttachments)
1011
+ };
847
1012
  }
848
- else {
849
- await this.startRuntimeRun(runtimeRequest, input.userId, runtimeMode);
1013
+ catch (error) {
1014
+ this.failPendingSendDebugTrace(debugTrace, error);
1015
+ throw error;
850
1016
  }
851
- const binding = this.sessionHistoryService.getBindingOrThrow(input.sessionId);
852
- const acceptedMessage = await this.findAcceptedUserMessage(input.sessionId, this.sessionMessageAttachmentService.buildAcceptedContentCandidates(input.content, providerPrompt), requestStartedAt);
853
- const acceptedAt = acceptedMessage?.timestamp ?? nowIso();
854
- const boundAttachments = this.sessionMessageAttachmentService.bindClientRequestToMessage(input.sessionId, input.clientRequestId, acceptedMessage?.messageId ?? null);
855
- return {
856
- sessionId: input.sessionId,
857
- provider: session.provider,
858
- providerSessionId: binding.providerSessionId,
859
- acceptedAt,
860
- clientRequestId: input.clientRequestId,
861
- message: (acceptedMessage
862
- ? {
863
- ...acceptedMessage,
864
- attachments: boundAttachments
865
- }
866
- : null) ??
867
- createSyntheticUserMessage(session.provider, binding.providerSessionId, input.content, acceptedAt, Math.max(session.messageCount + 1, 1), boundAttachments.length > 0
868
- ? boundAttachments
869
- : resolvedAttachments.messageAttachments)
870
- };
871
1017
  }
872
1018
  async dispatchNextQueuedMessage(sessionId) {
873
1019
  if (this.queueDispatchSessions.has(sessionId)) {
@@ -919,7 +1065,7 @@ export class SessionLiveRuntimeService {
919
1065
  this.sessionSendQueueRepository.delete(nextQueueItem.id);
920
1066
  }
921
1067
  catch (error) {
922
- if (isQueueDispatchDeferredError(error)) {
1068
+ if (isQueueDispatchRetryableError(error)) {
923
1069
  this.sessionSendQueueRepository.markQueued(nextQueueItem.id, nowIso());
924
1070
  this.scheduleQueueRetry(sessionId);
925
1071
  return;
@@ -1053,6 +1199,7 @@ export class SessionLiveRuntimeService {
1053
1199
  };
1054
1200
  }
1055
1201
  async persistRuntimeEvent(sessionId, workspaceId, userId, event) {
1202
+ this.observePendingSendDebugTraceEvent(sessionId, event);
1056
1203
  this.sessionHistoryService.persistSessionBinding(sessionId, workspaceId, {
1057
1204
  provider: event.provider,
1058
1205
  providerSessionId: event.providerSessionId,
@@ -1152,9 +1299,151 @@ export class SessionLiveRuntimeService {
1152
1299
  });
1153
1300
  await this.maybeEmitRuntimeHistoryFallback(sessionId, event);
1154
1301
  if (isTerminalRuntimeEventStatus(event.status)) {
1302
+ if (!isTerminalSessionRunningState(currentRunningState)) {
1303
+ await this.emitTerminalStateEvent({
1304
+ sessionId,
1305
+ status: event.status,
1306
+ timestamp: event.timestamp,
1307
+ detail: event.detail ?? null,
1308
+ source: "runtime"
1309
+ });
1310
+ }
1155
1311
  void this.dispatchNextQueuedMessage(sessionId);
1156
1312
  }
1157
1313
  }
1314
+ async emitTerminalStateEvent(event) {
1315
+ for (const listener of this.terminalStateListeners) {
1316
+ await listener(event);
1317
+ }
1318
+ }
1319
+ beginPendingSendDebugTrace(input) {
1320
+ if (!isPerfDebugEnabled()) {
1321
+ return null;
1322
+ }
1323
+ const trace = {
1324
+ ...input,
1325
+ startedAtMs: performance.now(),
1326
+ responseReadyAtMs: null,
1327
+ firstRuntimeEventAtMs: null
1328
+ };
1329
+ const queue = this.pendingSendDebugTracesBySessionId.get(input.sessionId) ?? [];
1330
+ queue.push(trace);
1331
+ this.pendingSendDebugTracesBySessionId.set(input.sessionId, queue);
1332
+ logPerformance(`session_send.${trace.mode}.begin`, 0, this.buildSendDebugDetail(trace), {
1333
+ force: true,
1334
+ thresholdMs: 0
1335
+ });
1336
+ return trace;
1337
+ }
1338
+ logSendDebugStep(trace, step, startedAtMs, detail = {}) {
1339
+ if (!trace) {
1340
+ return;
1341
+ }
1342
+ logPerformance(`session_send.${trace.mode}.${step}`, performance.now() - startedAtMs, {
1343
+ ...this.buildSendDebugDetail(trace),
1344
+ ...detail
1345
+ }, {
1346
+ force: true,
1347
+ thresholdMs: 0
1348
+ });
1349
+ }
1350
+ markSendDebugResponseReady(trace, detail = {}) {
1351
+ if (!trace || trace.responseReadyAtMs !== null) {
1352
+ return;
1353
+ }
1354
+ trace.responseReadyAtMs = performance.now();
1355
+ logPerformance(`session_send.${trace.mode}.response_ready`, trace.responseReadyAtMs - trace.startedAtMs, {
1356
+ ...this.buildSendDebugDetail(trace),
1357
+ ...detail
1358
+ }, {
1359
+ force: true,
1360
+ thresholdMs: 0
1361
+ });
1362
+ }
1363
+ failPendingSendDebugTrace(trace, error) {
1364
+ if (!trace) {
1365
+ return;
1366
+ }
1367
+ logPerformance(`session_send.${trace.mode}.error`, performance.now() - trace.startedAtMs, {
1368
+ ...this.buildSendDebugDetail(trace),
1369
+ error: error instanceof Error ? error.message : String(error)
1370
+ }, {
1371
+ force: true,
1372
+ thresholdMs: 0
1373
+ });
1374
+ this.removePendingSendDebugTrace(trace);
1375
+ }
1376
+ observePendingSendDebugTraceEvent(sessionId, event) {
1377
+ const trace = this.peekPendingSendDebugTrace(sessionId);
1378
+ if (!trace) {
1379
+ return;
1380
+ }
1381
+ const nowMs = performance.now();
1382
+ if (trace.firstRuntimeEventAtMs === null) {
1383
+ trace.firstRuntimeEventAtMs = nowMs;
1384
+ logPerformance(`session_send.${trace.mode}.first_runtime_event`, trace.firstRuntimeEventAtMs - trace.startedAtMs, {
1385
+ ...this.buildSendDebugDetail(trace),
1386
+ eventType: event.type,
1387
+ status: event.status,
1388
+ role: event.type === "message" ? event.message.role : null,
1389
+ kind: event.type === "message" ? event.message.kind : null,
1390
+ responseReady: trace.responseReadyAtMs !== null
1391
+ }, {
1392
+ force: true,
1393
+ thresholdMs: 0
1394
+ });
1395
+ }
1396
+ if (event.type === "message" && event.message.role === "assistant") {
1397
+ logPerformance(`session_send.${trace.mode}.first_assistant_message`, nowMs - trace.startedAtMs, {
1398
+ ...this.buildSendDebugDetail(trace),
1399
+ kind: event.message.kind,
1400
+ contentLength: event.message.content.length,
1401
+ responseToAssistantMs: trace.responseReadyAtMs === null ? null : nowMs - trace.responseReadyAtMs
1402
+ }, {
1403
+ force: true,
1404
+ thresholdMs: 0
1405
+ });
1406
+ this.removePendingSendDebugTrace(trace);
1407
+ return;
1408
+ }
1409
+ if (event.type === "error" ||
1410
+ (event.type !== "message" && isTerminalRuntimeEventStatus(event.status))) {
1411
+ logPerformance(`session_send.${trace.mode}.completed_without_assistant`, nowMs - trace.startedAtMs, {
1412
+ ...this.buildSendDebugDetail(trace),
1413
+ eventType: event.type,
1414
+ status: event.status,
1415
+ detail: event.detail
1416
+ }, {
1417
+ force: true,
1418
+ thresholdMs: 0
1419
+ });
1420
+ this.removePendingSendDebugTrace(trace);
1421
+ }
1422
+ }
1423
+ peekPendingSendDebugTrace(sessionId) {
1424
+ const queue = this.pendingSendDebugTracesBySessionId.get(sessionId);
1425
+ return queue && queue.length > 0 ? queue[0] : null;
1426
+ }
1427
+ removePendingSendDebugTrace(trace) {
1428
+ const queue = this.pendingSendDebugTracesBySessionId.get(trace.sessionId);
1429
+ if (!queue || queue.length === 0) {
1430
+ return;
1431
+ }
1432
+ const nextQueue = queue.filter((item) => item !== trace);
1433
+ if (nextQueue.length === 0) {
1434
+ this.pendingSendDebugTracesBySessionId.delete(trace.sessionId);
1435
+ return;
1436
+ }
1437
+ this.pendingSendDebugTracesBySessionId.set(trace.sessionId, nextQueue);
1438
+ }
1439
+ buildSendDebugDetail(trace) {
1440
+ return {
1441
+ sessionId: trace.sessionId,
1442
+ workspaceId: trace.workspaceId,
1443
+ provider: trace.provider,
1444
+ clientRequestId: trace.clientRequestId
1445
+ };
1446
+ }
1158
1447
  async findAcceptedUserMessage(sessionId, content, minTimestamp) {
1159
1448
  try {
1160
1449
  return await withTimeout(this.sessionHistoryService.findLatestUserMessage(sessionId, content, 12, minTimestamp), 1200);
@@ -1163,6 +1452,36 @@ export class SessionLiveRuntimeService {
1163
1452
  return null;
1164
1453
  }
1165
1454
  }
1455
+ async resolveNextUserSequence(sessionId, messageCount) {
1456
+ let maxSequence = Math.max(messageCount, 0);
1457
+ const envelope = await Promise.resolve(this.sessionHistoryService.readRecentHistoryEnvelope(sessionId, 10)).catch(() => {
1458
+ return null;
1459
+ });
1460
+ for (const message of envelope?.messages ?? []) {
1461
+ if (Number.isFinite(message.sequence) && message.sequence > maxSequence) {
1462
+ maxSequence = message.sequence;
1463
+ }
1464
+ }
1465
+ return Math.max(maxSequence + 1, 1);
1466
+ }
1467
+ async waitForResolvedStartBinding(sessionId, workspaceId, provider, handle) {
1468
+ if (provider !== "gemini" && provider !== "kimi") {
1469
+ return;
1470
+ }
1471
+ const startedAt = Date.now();
1472
+ while (Date.now() - startedAt < RUNTIME_START_BINDING_WAIT_TIMEOUT_MS) {
1473
+ const snapshot = handle.getSnapshot();
1474
+ if (hasResolvedRuntimeBinding(snapshot.providerSessionId, snapshot.rawStoreRef)) {
1475
+ this.sessionHistoryService.persistSessionBinding(sessionId, workspaceId, {
1476
+ provider: snapshot.provider,
1477
+ providerSessionId: snapshot.providerSessionId,
1478
+ rawStoreRef: snapshot.rawStoreRef
1479
+ });
1480
+ return;
1481
+ }
1482
+ await waitForRuntimeBindingPoll();
1483
+ }
1484
+ }
1166
1485
  persistMessageAttachments(sessionId, clientRequestId, attachments) {
1167
1486
  if (!clientRequestId || attachments.length === 0) {
1168
1487
  return {
@@ -1176,12 +1495,12 @@ export class SessionLiveRuntimeService {
1176
1495
  attachments
1177
1496
  });
1178
1497
  }
1179
- mapRuntimeEventToEnvelope(sessionId, event) {
1498
+ mapRuntimeEventToEnvelope(sessionId, event, originSessionId = sessionId) {
1180
1499
  if (event.type === "message") {
1181
1500
  return {
1182
1501
  type: "session.runtime_message",
1183
1502
  sessionId,
1184
- message: event.message,
1503
+ message: this.sessionHistoryService.resolveMessageOrigin(originSessionId, event.message),
1185
1504
  source: "runtime"
1186
1505
  };
1187
1506
  }
@@ -1465,12 +1784,25 @@ function isTerminalRuntimeEventStatus(status) {
1465
1784
  function isPendingSessionRunningState(state) {
1466
1785
  return state === "starting" || state === "running";
1467
1786
  }
1468
- function isQueueDispatchDeferredError(error) {
1787
+ function isQueueDispatchRetryableError(error) {
1469
1788
  if (error instanceof AppError) {
1470
- return error.errorCode === "ACTIVE_RUN_EXISTS" || error.errorCode === "SESSION_NOT_RUNNING";
1789
+ if (error.errorCode === "ACTIVE_RUN_EXISTS"
1790
+ || error.errorCode === "SESSION_NOT_RUNNING"
1791
+ || error.errorCode === "IN_RUN_INPUT_NOT_SUPPORTED"
1792
+ || error.errorCode === "SESSION_EXTERNAL_RUN_ACTIVE"
1793
+ || error.errorCode === "PROVIDER_RUNTIME_UNAVAILABLE"
1794
+ || error.errorCode === "PROVIDER_RUNTIME_TIMEOUT") {
1795
+ return true;
1796
+ }
1797
+ return error.statusCode >= 500;
1471
1798
  }
1472
1799
  if (error instanceof Error) {
1473
- return error.message === "ACTIVE_RUN_EXISTS";
1800
+ return (error.message === "ACTIVE_RUN_EXISTS"
1801
+ || error.message === "SESSION_NOT_RUNNING"
1802
+ || error.message === "IN_RUN_INPUT_NOT_SUPPORTED"
1803
+ || error.message === "SESSION_EXTERNAL_RUN_ACTIVE"
1804
+ || error.message === "SERVER_UNAVAILABLE"
1805
+ || error.message === "SERVER_TIMEOUT");
1474
1806
  }
1475
1807
  return false;
1476
1808
  }
@@ -1493,6 +1825,32 @@ function mapQueueItemRecordToView(record) {
1493
1825
  function isTerminalSessionRunningState(state) {
1494
1826
  return state === "completed" || state === "interrupted" || state === "failed";
1495
1827
  }
1828
+ function hasResolvedRuntimeBinding(providerSessionId, rawStoreRef) {
1829
+ if (!providerSessionId?.trim() || !rawStoreRef?.trim()) {
1830
+ return false;
1831
+ }
1832
+ return !providerSessionId.trim().toLowerCase().startsWith("pending://")
1833
+ && !rawStoreRef.trim().toLowerCase().startsWith("pending://");
1834
+ }
1835
+ function waitForRuntimeBindingPoll() {
1836
+ return new Promise((resolve) => {
1837
+ setTimeout(resolve, START_BINDING_POLL_INTERVAL_MS);
1838
+ });
1839
+ }
1840
+ function isGeminiPendingRuntimeAliasBinding(value, targetSessionId) {
1841
+ return value.trim().toLowerCase() === `pending://gemini/${targetSessionId.trim().toLowerCase()}`;
1842
+ }
1843
+ function shouldAwaitAcceptedUserMessage(provider) {
1844
+ return provider !== "gemini";
1845
+ }
1846
+ function shouldAwaitStartBindingBeforeAcceptedUserLookup(provider) {
1847
+ return provider === "kimi";
1848
+ }
1849
+ function waitForAcceptedUserLookupWindow() {
1850
+ return new Promise((resolve) => {
1851
+ setTimeout(resolve, 1200);
1852
+ });
1853
+ }
1496
1854
  function createProviderRuntimeAdapters(config, options = {}) {
1497
1855
  const claudeHookBridgeConfig = buildClaudeHookBridgeConfig(config);
1498
1856
  const claudeAdapter = process.env.VITEST
@@ -1518,7 +1876,9 @@ function createProviderRuntimeAdapters(config, options = {}) {
1518
1876
  }
1519
1877
  const codexTransportHelper = process.env.VITEST
1520
1878
  ? null
1521
- : new CodexAppServerHelperClient(config.codexCliPath);
1879
+ : new CodexAppServerHelperClient(config.codexCliPath, {
1880
+ homeDir: config.codexHomeDir
1881
+ });
1522
1882
  if (codexTransportHelper) {
1523
1883
  disposables.push(codexTransportHelper);
1524
1884
  }
@@ -1531,6 +1891,14 @@ function createProviderRuntimeAdapters(config, options = {}) {
1531
1891
  transportFactory: codexTransportHelper?.createTransport.bind(codexTransportHelper),
1532
1892
  handleServerRequest: options.handleCodexServerRequest
1533
1893
  }),
1894
+ new GeminiRuntimeAdapter({
1895
+ homeDir: config.geminiHomeDir,
1896
+ commandPath: config.geminiCliPath
1897
+ }),
1898
+ new KimiRuntimeAdapter({
1899
+ homeDir: config.kimiHomeDir,
1900
+ commandPath: config.kimiCliPath
1901
+ }),
1534
1902
  new OpenCodeRuntimeAdapter({
1535
1903
  baseUrl: config.opencodeBaseUrl,
1536
1904
  baseUrlResolver: config.opencodeBaseUrlResolver?.resolve.bind(config.opencodeBaseUrlResolver)