@jingyi0605/codingns 0.3.5 → 0.3.6

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 (140) hide show
  1. package/README.md +13 -0
  2. package/bin/codingns.mjs +880 -9
  3. package/dist/public/assets/{TerminalPage-CgrfstRm.js → TerminalPage-D00S4KM6.js} +16 -16
  4. package/dist/public/assets/index-BlOinYqR.js +122 -0
  5. package/dist/public/assets/index-Dg_7g6lA.css +1 -0
  6. package/dist/public/index.html +2 -2
  7. package/dist/server/config/opencode-base-url-resolver.d.ts +7 -0
  8. package/dist/server/config/opencode-base-url-resolver.js +48 -11
  9. package/dist/server/config/opencode-base-url-resolver.js.map +1 -1
  10. package/dist/server/config/opencode-system-probe-helper-client.d.ts +4 -0
  11. package/dist/server/config/opencode-system-probe-helper-client.js +29 -0
  12. package/dist/server/config/opencode-system-probe-helper-client.js.map +1 -1
  13. package/dist/server/config/opencode-system-probe-helper-process.d.ts +12 -0
  14. package/dist/server/config/opencode-system-probe-helper-process.js +34 -7
  15. package/dist/server/config/opencode-system-probe-helper-process.js.map +1 -1
  16. package/dist/server/modules/assistant-capability/assistant-capability-controller.d.ts +144 -0
  17. package/dist/server/modules/assistant-capability/assistant-capability-controller.js +242 -1
  18. package/dist/server/modules/assistant-capability/assistant-capability-controller.js.map +1 -1
  19. package/dist/server/modules/assistant-capability/assistant-capability-service.d.ts +133 -2
  20. package/dist/server/modules/assistant-capability/assistant-capability-service.js +395 -2
  21. package/dist/server/modules/assistant-capability/assistant-capability-service.js.map +1 -1
  22. package/dist/server/modules/butler/butler-inbox-instruction-adapter.js +7 -6
  23. package/dist/server/modules/butler/butler-inbox-instruction-adapter.js.map +1 -1
  24. package/dist/server/modules/butler/butler-workspace-context.js +24 -13
  25. package/dist/server/modules/butler/butler-workspace-context.js.map +1 -1
  26. package/dist/server/modules/debug-target/debug-target-controller.d.ts +13 -0
  27. package/dist/server/modules/debug-target/debug-target-controller.js +77 -2
  28. package/dist/server/modules/debug-target/debug-target-controller.js.map +1 -1
  29. package/dist/server/modules/debug-target/debug-target-service.d.ts +11 -2
  30. package/dist/server/modules/debug-target/debug-target-service.js +138 -3
  31. package/dist/server/modules/debug-target/debug-target-service.js.map +1 -1
  32. package/dist/server/modules/git/git-command-helper-client.d.ts +2 -0
  33. package/dist/server/modules/git/git-command-helper-client.js +52 -3
  34. package/dist/server/modules/git/git-command-helper-client.js.map +1 -1
  35. package/dist/server/modules/git/git-command-helper-process.js +62 -9
  36. package/dist/server/modules/git/git-command-helper-process.js.map +1 -1
  37. package/dist/server/modules/git/git-command-runner.d.ts +1 -0
  38. package/dist/server/modules/git/git-command-runner.js +25 -0
  39. package/dist/server/modules/git/git-command-runner.js.map +1 -1
  40. package/dist/server/modules/git/git-controller.js +8 -7
  41. package/dist/server/modules/git/git-controller.js.map +1 -1
  42. package/dist/server/modules/git/git-read-service.d.ts +7 -7
  43. package/dist/server/modules/git/git-read-service.js +41 -24
  44. package/dist/server/modules/git/git-read-service.js.map +1 -1
  45. package/dist/server/modules/model-switch/cc-switch-adapter.js +6 -2
  46. package/dist/server/modules/model-switch/cc-switch-adapter.js.map +1 -1
  47. package/dist/server/modules/provider/codex-model-options.js +2 -3
  48. package/dist/server/modules/provider/codex-model-options.js.map +1 -1
  49. package/dist/server/modules/provider/opencode-model-options.js +2 -3
  50. package/dist/server/modules/provider/opencode-model-options.js.map +1 -1
  51. package/dist/server/modules/provider/provider-discovery-helper-client.d.ts +9 -4
  52. package/dist/server/modules/provider/provider-discovery-helper-client.js +87 -11
  53. package/dist/server/modules/provider/provider-discovery-helper-client.js.map +1 -1
  54. package/dist/server/modules/provider/provider-discovery-helper-process.js +52 -47
  55. package/dist/server/modules/provider/provider-discovery-helper-process.js.map +1 -1
  56. package/dist/server/modules/provider/provider-discovery-runtime.d.ts +4 -0
  57. package/dist/server/modules/provider/provider-discovery-runtime.js +65 -0
  58. package/dist/server/modules/provider/provider-discovery-runtime.js.map +1 -0
  59. package/dist/server/modules/sessions/session-history-service.d.ts +5 -2
  60. package/dist/server/modules/sessions/session-history-service.js +214 -26
  61. package/dist/server/modules/sessions/session-history-service.js.map +1 -1
  62. package/dist/server/modules/sessions/session-live-runtime-service.d.ts +2 -0
  63. package/dist/server/modules/sessions/session-live-runtime-service.js +67 -23
  64. package/dist/server/modules/sessions/session-live-runtime-service.js.map +1 -1
  65. package/dist/server/modules/skills/builtin-skill-service.d.ts +12 -0
  66. package/dist/server/modules/skills/builtin-skill-service.js +49 -0
  67. package/dist/server/modules/skills/builtin-skill-service.js.map +1 -0
  68. package/dist/server/modules/skills/builtin-skills/codingns-assistant/SKILL.md +75 -0
  69. package/dist/server/modules/skills/builtin-skills/codingns-assistant/agents/openai.yaml +4 -0
  70. package/dist/server/modules/skills/builtin-skills/codingns-assistant/references/cli-workflow.md +130 -0
  71. package/dist/server/modules/skills/skill-manager-service.d.ts +7 -0
  72. package/dist/server/modules/skills/skill-manager-service.js +98 -0
  73. package/dist/server/modules/skills/skill-manager-service.js.map +1 -1
  74. package/dist/server/modules/tailscale/tailscale-helper-client.d.ts +1 -0
  75. package/dist/server/modules/tailscale/tailscale-helper-client.js +12 -0
  76. package/dist/server/modules/tailscale/tailscale-helper-client.js.map +1 -1
  77. package/dist/server/modules/tasks/task-helper-client.d.ts +5 -0
  78. package/dist/server/modules/tasks/task-helper-client.js +45 -0
  79. package/dist/server/modules/tasks/task-helper-client.js.map +1 -1
  80. package/dist/server/modules/tasks/task-helper-process-handlers.d.ts +10 -3
  81. package/dist/server/modules/tasks/task-helper-process-handlers.js +7 -5
  82. package/dist/server/modules/tasks/task-helper-process-handlers.js.map +1 -1
  83. package/dist/server/modules/tasks/task-helper-process.js +11 -1
  84. package/dist/server/modules/tasks/task-helper-process.js.map +1 -1
  85. package/dist/server/modules/tasks/task-lane-executors.js +2 -2
  86. package/dist/server/modules/tasks/task-lane-executors.js.map +1 -1
  87. package/dist/server/modules/tasks/task-types.d.ts +1 -0
  88. package/dist/server/modules/tasks/task-types.js +1 -0
  89. package/dist/server/modules/tasks/task-types.js.map +1 -1
  90. package/dist/server/modules/terminal/command-template-service.d.ts +2 -2
  91. package/dist/server/modules/terminal/command-template-service.js +4 -4
  92. package/dist/server/modules/terminal/command-template-service.js.map +1 -1
  93. package/dist/server/modules/terminal/runtime/terminal-log-writer-client.js +1 -1
  94. package/dist/server/modules/terminal/runtime/terminal-log-writer-client.js.map +1 -1
  95. package/dist/server/modules/terminal/runtime/terminal-log-writer-process.js +160 -11
  96. package/dist/server/modules/terminal/runtime/terminal-log-writer-process.js.map +1 -1
  97. package/dist/server/modules/terminal/template-port-runtime.d.ts +1 -1
  98. package/dist/server/modules/terminal/template-port-runtime.js +87 -37
  99. package/dist/server/modules/terminal/template-port-runtime.js.map +1 -1
  100. package/dist/server/modules/terminal/terminal-service.d.ts +4 -0
  101. package/dist/server/modules/terminal/terminal-service.js +35 -1
  102. package/dist/server/modules/terminal/terminal-service.js.map +1 -1
  103. package/dist/server/modules/workbench/workbench-service.js +3 -3
  104. package/dist/server/modules/workbench/workbench-service.js.map +1 -1
  105. package/dist/server/modules/workbench/workspace-panel-snapshot-service.d.ts +1 -0
  106. package/dist/server/modules/workbench/workspace-panel-snapshot-service.js +118 -39
  107. package/dist/server/modules/workbench/workspace-panel-snapshot-service.js.map +1 -1
  108. package/dist/server/modules/workspace/workspace-code-composition.d.ts +1 -0
  109. package/dist/server/modules/workspace/workspace-code-composition.js +183 -1
  110. package/dist/server/modules/workspace/workspace-code-composition.js.map +1 -1
  111. package/dist/server/modules/workspace/workspace-service.js +54 -17
  112. package/dist/server/modules/workspace/workspace-service.js.map +1 -1
  113. package/dist/server/modules/worktree/worktree-cleanup-service.d.ts +1 -1
  114. package/dist/server/modules/worktree/worktree-cleanup-service.js +22 -17
  115. package/dist/server/modules/worktree/worktree-cleanup-service.js.map +1 -1
  116. package/dist/server/modules/worktree/worktree-controller.js +6 -5
  117. package/dist/server/modules/worktree/worktree-controller.js.map +1 -1
  118. package/dist/server/modules/worktree/worktree-manager.d.ts +1 -1
  119. package/dist/server/modules/worktree/worktree-manager.js +26 -19
  120. package/dist/server/modules/worktree/worktree-manager.js.map +1 -1
  121. package/dist/server/modules/worktree/worktree-merge-service.d.ts +2 -2
  122. package/dist/server/modules/worktree/worktree-merge-service.js +34 -27
  123. package/dist/server/modules/worktree/worktree-merge-service.js.map +1 -1
  124. package/dist/server/modules/worktree/worktree-sync-service.d.ts +1 -1
  125. package/dist/server/modules/worktree/worktree-sync-service.js +5 -3
  126. package/dist/server/modules/worktree/worktree-sync-service.js.map +1 -1
  127. package/dist/server/routes/assistant.js +24 -0
  128. package/dist/server/routes/assistant.js.map +1 -1
  129. package/dist/server/server/create-server.js +16 -1
  130. package/dist/server/server/create-server.js.map +1 -1
  131. package/dist/server/shared/http/request-abort.d.ts +2 -0
  132. package/dist/server/shared/http/request-abort.js +38 -0
  133. package/dist/server/shared/http/request-abort.js.map +1 -0
  134. package/dist/server/ws/workbench-ws-hub.d.ts +1 -0
  135. package/dist/server/ws/workbench-ws-hub.js +25 -3
  136. package/dist/server/ws/workbench-ws-hub.js.map +1 -1
  137. package/package.json +1 -1
  138. package/scripts/postinstall.mjs +33 -0
  139. package/dist/public/assets/index-Cek6u0b9.css +0 -1
  140. package/dist/public/assets/index-THHY79si.js +0 -122
@@ -14,13 +14,15 @@ import { SessionForkRepository } from "../../storage/repositories/session-fork-r
14
14
  import { enrichClaudeCapabilities } from "../provider/claude-model-options.js";
15
15
  import { CodexModelOptionsService, createFallbackCodexModelOptions, enrichCodexCapabilities } from "../provider/codex-model-options.js";
16
16
  import { OpenCodeModelOptionsService, createFallbackOpenCodeModelOptions, enrichOpenCodeCapabilities } from "../provider/opencode-model-options.js";
17
- import { ProviderDiscoveryHelperClient } from "../provider/provider-discovery-helper-client.js";
17
+ import { getSharedProviderDiscoveryHelperClient } from "../provider/provider-discovery-helper-client.js";
18
+ import { discoverWorkspaceSessionsInRuntime } from "../provider/provider-discovery-runtime.js";
18
19
  import { createTaskManager } from "../tasks/task-manager.js";
19
20
  import { HOST_TASK_TYPES } from "../tasks/task-types.js";
20
21
  import { CodexAppServerHelperClient } from "./codex-app-server-helper-client.js";
21
22
  const RECONSTRUCTED_FORK_TARGET_PROVIDERS = new Set(["codex", "claude-code", "opencode"]);
22
23
  const FORK_RECONSTRUCTION_PAGE_SIZE = 200;
23
24
  const MAX_FORK_DEPTH = 4;
25
+ const SYNTHETIC_CODEX_SESSION_CLEANUP_GRACE_MS = 120_000;
24
26
  const SESSION_START_DEFERRED_PROVIDERS = new Set([
25
27
  "codex",
26
28
  "claude-code",
@@ -32,6 +34,7 @@ const MUTABLE_HISTORY_TAIL_REFRESH_INTERVAL_MS = 1_200;
32
34
  const WORKSPACE_DISCOVERY_BACKGROUND_MAX_AGE_MS = 15_000;
33
35
  const PROVIDER_CAPABILITY_CACHE_MAX_AGE_MS = 5_000;
34
36
  const WORKSPACE_DISCOVERY_PERSIST_BATCH_SIZE = 25;
37
+ const SESSION_TRANSACTION_HOTSPOT_THRESHOLD_MS = 150;
35
38
  export class SessionHistoryService {
36
39
  db;
37
40
  workspaceRepository;
@@ -52,7 +55,7 @@ export class SessionHistoryService {
52
55
  openCodeModelOptionsService;
53
56
  providerCliCommandPaths;
54
57
  providerCliAvailability;
55
- providerDiscoveryHelperClient = new ProviderDiscoveryHelperClient();
58
+ providerDiscoveryHelperClient = getSharedProviderDiscoveryHelperClient();
56
59
  providerSessionDiscoveryConfig;
57
60
  taskManager;
58
61
  workspaceDiscoveryStatuses = new Map();
@@ -148,8 +151,16 @@ export class SessionHistoryService {
148
151
  if (!this.taskManager.has(HOST_TASK_TYPES.workspaceDiscovery)) {
149
152
  this.taskManager.register({
150
153
  taskType: HOST_TASK_TYPES.workspaceDiscovery,
154
+ executionLane: "host_background",
155
+ run: async ({ workspaceId, userId, refreshStateMode }, context) => this.runDiscoverWorkspaceSessions(workspaceId, userId, refreshStateMode, context.signal)
156
+ });
157
+ }
158
+ if (!this.taskManager.has(HOST_TASK_TYPES.workspaceDiscoveryScan)) {
159
+ this.taskManager.register({
160
+ taskType: HOST_TASK_TYPES.workspaceDiscoveryScan,
151
161
  executionLane: "helper_process",
152
- run: async ({ workspaceId, userId, refreshStateMode }) => this.runDiscoverWorkspaceSessions(workspaceId, userId, refreshStateMode)
162
+ helperProcessHandler: "session.workspace_discovery",
163
+ run: async ({ config, workspacePath, knownSessions }, context) => await discoverWorkspaceSessionsInRuntime(config, workspacePath, knownSessions, context.signal)
153
164
  });
154
165
  }
155
166
  if (!this.taskManager.has(HOST_TASK_TYPES.providerCapabilityRefresh)) {
@@ -233,7 +244,10 @@ export class SessionHistoryService {
233
244
  async readSessionHistory(sessionId, cursor, limit, direction = "forward", userId) {
234
245
  const startedAt = Date.now();
235
246
  const resolvedSessionId = this.resolveCanonicalSessionId(sessionId, userId);
236
- const binding = this.getBindingOrThrow(resolvedSessionId);
247
+ let binding = this.getBindingOrThrow(resolvedSessionId);
248
+ if (userId) {
249
+ binding = await this.repairCodexDirtyBindingBeforeHistoryRead(resolvedSessionId, userId, binding);
250
+ }
237
251
  const current = this.sessionStatusSnapshotRepository.findBySessionId(resolvedSessionId);
238
252
  const safeLimit = clampLimit(limit);
239
253
  const knownTotalMessageCount = direction === "backward" && cursor === null
@@ -335,17 +349,17 @@ export class SessionHistoryService {
335
349
  await this.refreshSessionState(sessionId, userId);
336
350
  return this.enrichSessionItem(this.getSessionListItemOrThrow(sessionId, userId));
337
351
  }
338
- async syncSessionTitle(sessionId) {
352
+ async syncSessionTitle(sessionId, signal) {
339
353
  const binding = this.getBindingOrThrow(sessionId);
340
- await this.syncSessionTitleFromProvider(sessionId, binding);
354
+ await this.syncSessionTitleFromProvider(sessionId, binding, signal);
341
355
  }
342
- async syncWorkspaceSessionTitles(workspaceId, userId, concurrency = 1) {
356
+ async syncWorkspaceSessionTitles(workspaceId, userId, concurrency = 1, signal) {
343
357
  const sessions = this.sessionIndexRepository.listByWorkspace(workspaceId, userId);
344
358
  await runWithConcurrency(sessions, concurrency, async (session) => {
345
- await this.syncSessionTitle(session.sessionId).catch(() => {
359
+ await this.syncSessionTitle(session.sessionId, signal).catch(() => {
346
360
  return;
347
361
  });
348
- });
362
+ }, signal);
349
363
  }
350
364
  async listSessionChangedFiles(sessionId, userId) {
351
365
  this.getSession(sessionId, userId);
@@ -1137,7 +1151,7 @@ export class SessionHistoryService {
1137
1151
  }
1138
1152
  }
1139
1153
  }
1140
- async runDiscoverWorkspaceSessions(workspaceId, userId, refreshStateMode = "inline") {
1154
+ async runDiscoverWorkspaceSessions(workspaceId, userId, refreshStateMode = "inline", signal) {
1141
1155
  const startedAt = Date.now();
1142
1156
  const debugStartedAtMs = terminalDebugNowMs();
1143
1157
  const workspace = this.getWorkspaceOrThrow(workspaceId);
@@ -1158,13 +1172,16 @@ export class SessionHistoryService {
1158
1172
  const discoverStartedAt = Date.now();
1159
1173
  const existingWorkspaceSessions = this.sessionIndexRepository.listByWorkspace(workspaceId, userId);
1160
1174
  const knownSessions = this.buildKnownSessionSummaries(existingWorkspaceSessions, workspace.path);
1161
- const discovery = await this.providerDiscoveryHelperClient
1162
- .discoverWorkspaceSessions({
1163
- config: this.providerSessionDiscoveryConfig,
1164
- workspacePath: workspace.path,
1165
- knownSessions
1166
- })
1167
- .catch((error) => {
1175
+ const discoveryHandle = this.taskManager.enqueue(HOST_TASK_TYPES.workspaceDiscoveryScan, {
1176
+ key: workspaceId,
1177
+ source: "session_history.workspace_discovery.scan",
1178
+ input: {
1179
+ config: this.providerSessionDiscoveryConfig,
1180
+ workspacePath: workspace.path,
1181
+ knownSessions
1182
+ }
1183
+ });
1184
+ const discovery = await awaitTaskHandleWithSignal(discoveryHandle, signal).catch((error) => {
1168
1185
  throw mapSessionProviderError(error);
1169
1186
  });
1170
1187
  const sessions = discovery.sessions;
@@ -1251,7 +1268,15 @@ export class SessionHistoryService {
1251
1268
  }
1252
1269
  });
1253
1270
  const persistPass1StartedAt = Date.now();
1254
- const persistPass1Stats = await runBatchedTransactions(sessions, WORKSPACE_DISCOVERY_PERSIST_BATCH_SIZE, persistPass1Transaction);
1271
+ const persistPass1Stats = await runBatchedTransactions(sessions, WORKSPACE_DISCOVERY_PERSIST_BATCH_SIZE, persistPass1Transaction, {
1272
+ scope: "workspace.discover_sessions.persist_pass1.batch",
1273
+ thresholdMs: SESSION_TRANSACTION_HOTSPOT_THRESHOLD_MS,
1274
+ detail: {
1275
+ workspaceId,
1276
+ workspacePath: workspace.path,
1277
+ phase: "pass1"
1278
+ }
1279
+ });
1255
1280
  persistPass1DurationMs = Date.now() - persistPass1StartedAt;
1256
1281
  persistPass1BatchCount = persistPass1Stats.batchCount;
1257
1282
  persistPass1MaxBatchMs = persistPass1Stats.maxBatchMs;
@@ -1298,7 +1323,15 @@ export class SessionHistoryService {
1298
1323
  }
1299
1324
  });
1300
1325
  const persistPass2StartedAt = Date.now();
1301
- const persistPass2Stats = await runBatchedTransactions(persistedSessions, WORKSPACE_DISCOVERY_PERSIST_BATCH_SIZE, persistPass2Transaction);
1326
+ const persistPass2Stats = await runBatchedTransactions(persistedSessions, WORKSPACE_DISCOVERY_PERSIST_BATCH_SIZE, persistPass2Transaction, {
1327
+ scope: "workspace.discover_sessions.persist_pass2.batch",
1328
+ thresholdMs: SESSION_TRANSACTION_HOTSPOT_THRESHOLD_MS,
1329
+ detail: {
1330
+ workspaceId,
1331
+ workspacePath: workspace.path,
1332
+ phase: "pass2"
1333
+ }
1334
+ });
1302
1335
  persistPass2DurationMs = Date.now() - persistPass2StartedAt;
1303
1336
  persistPass2BatchCount = persistPass2Stats.batchCount;
1304
1337
  persistPass2MaxBatchMs = persistPass2Stats.maxBatchMs;
@@ -1677,7 +1710,7 @@ export class SessionHistoryService {
1677
1710
  messages
1678
1711
  });
1679
1712
  }
1680
- async syncSessionTitleFromProvider(sessionId, binding) {
1713
+ async syncSessionTitleFromProvider(sessionId, binding, signal) {
1681
1714
  const currentIndex = this.sessionIndexRepository.findIndexRecordBySessionId(sessionId);
1682
1715
  if (!currentIndex) {
1683
1716
  return;
@@ -1690,7 +1723,7 @@ export class SessionHistoryService {
1690
1723
  provider: binding.provider,
1691
1724
  providerSessionId: binding.providerSessionId,
1692
1725
  rawStoreRef: binding.rawStoreRef
1693
- })).trim();
1726
+ }, signal)).trim();
1694
1727
  const resolvedTitle = resolvePersistedSessionTitle(binding.provider, nextTitle, currentIndex.title);
1695
1728
  if (resolvedTitle.length === 0 || resolvedTitle === currentIndex.title) {
1696
1729
  return;
@@ -1925,6 +1958,7 @@ export class SessionHistoryService {
1925
1958
  async cleanupStaleHiddenSessions(workspaceId, userId, sessions) {
1926
1959
  const discoveredProviderSessionIds = new Set(sessions.map((session) => buildProviderSessionKey(session.provider, session.providerSessionId)));
1927
1960
  const discoveredRawStoreRefs = new Set(sessions.map((session) => session.rawStoreRef));
1961
+ const nowMs = Date.now();
1928
1962
  const staleHiddenSessions = this.sessionIndexRepository
1929
1963
  .listByWorkspace(workspaceId, userId)
1930
1964
  .filter((session) => {
@@ -1936,10 +1970,13 @@ export class SessionHistoryService {
1936
1970
  }
1937
1971
  return ((session.provider === "codex" &&
1938
1972
  (isLegacyCodingNsRolloutSession(session.providerSessionId, session.rawStoreRef) ||
1939
- shouldRemoveMissingSyntheticCodexSession(session.rawStoreRef))) ||
1973
+ (shouldRemoveMissingSyntheticCodexSession(session.rawStoreRef) &&
1974
+ !this.shouldPreserveSyntheticCodexSession(session, nowMs)))) ||
1940
1975
  (session.provider === "claude-code" && shouldRemoveHiddenClaudeDebugSession(session)));
1941
1976
  });
1942
- if (staleHiddenSessions.length === 0) {
1977
+ const managedButlerSessionIds = this.listManagedButlerSessionIds(staleHiddenSessions.map((session) => session.sessionId));
1978
+ const deletableSessions = staleHiddenSessions.filter((session) => !managedButlerSessionIds.has(session.sessionId));
1979
+ if (deletableSessions.length === 0) {
1943
1980
  return;
1944
1981
  }
1945
1982
  const deleteTransaction = this.db.transaction((ids) => {
@@ -1947,7 +1984,53 @@ export class SessionHistoryService {
1947
1984
  this.deleteSessionById(sessionId);
1948
1985
  }
1949
1986
  });
1950
- await runBatchedTransactions(staleHiddenSessions.map((session) => session.sessionId), WORKSPACE_DISCOVERY_PERSIST_BATCH_SIZE, deleteTransaction);
1987
+ await runBatchedTransactions(deletableSessions.map((session) => session.sessionId), WORKSPACE_DISCOVERY_PERSIST_BATCH_SIZE, deleteTransaction, {
1988
+ scope: "workspace.discover_sessions.cleanup_hidden.batch",
1989
+ thresholdMs: SESSION_TRANSACTION_HOTSPOT_THRESHOLD_MS,
1990
+ detail: {
1991
+ workspaceId,
1992
+ userId,
1993
+ phase: "cleanup_hidden"
1994
+ }
1995
+ });
1996
+ }
1997
+ listManagedButlerSessionIds(sessionIds) {
1998
+ if (sessionIds.length === 0) {
1999
+ return new Set();
2000
+ }
2001
+ const managedSessionIds = new Set();
2002
+ for (let index = 0; index < sessionIds.length; index += WORKSPACE_DISCOVERY_PERSIST_BATCH_SIZE) {
2003
+ const batch = sessionIds.slice(index, index + WORKSPACE_DISCOVERY_PERSIST_BATCH_SIZE);
2004
+ const placeholders = batch.map(() => "?").join(", ");
2005
+ const rows = this.db
2006
+ .prepare(`SELECT session_id
2007
+ FROM butler_sessions
2008
+ WHERE session_id IN (${placeholders})`)
2009
+ .all(...batch);
2010
+ for (const row of rows) {
2011
+ managedSessionIds.add(row.session_id);
2012
+ }
2013
+ }
2014
+ return managedSessionIds;
2015
+ }
2016
+ shouldPreserveSyntheticCodexSession(session, nowMs) {
2017
+ if (session.activitySource === "runtime"
2018
+ || session.runningState === "starting"
2019
+ || session.runningState === "running") {
2020
+ return true;
2021
+ }
2022
+ const hasActiveRuntimeState = this.listSessionStatesBySessionId(session.sessionId).some((state) => state.activitySource === "runtime"
2023
+ || state.runningState === "starting"
2024
+ || state.runningState === "running");
2025
+ if (hasActiveRuntimeState) {
2026
+ return true;
2027
+ }
2028
+ const latestTouchedAt = pickLaterIso(session.updatedAt, session.createdAt) ?? session.updatedAt;
2029
+ const latestTouchedAtMs = Date.parse(latestTouchedAt);
2030
+ if (!Number.isFinite(latestTouchedAtMs)) {
2031
+ return false;
2032
+ }
2033
+ return nowMs - latestTouchedAtMs <= SYNTHETIC_CODEX_SESSION_CLEANUP_GRACE_MS;
1951
2034
  }
1952
2035
  findSameWorkspaceBindingDuplicate(sessionId, workspaceId, snapshot) {
1953
2036
  if (isPendingBindingValue(snapshot.providerSessionId)) {
@@ -2282,6 +2365,18 @@ export class SessionHistoryService {
2282
2365
  });
2283
2366
  return nextRecord;
2284
2367
  }
2368
+ async repairCodexDirtyBindingBeforeHistoryRead(sessionId, userId, binding) {
2369
+ if (!shouldRepairCodexDirtyBinding(binding)) {
2370
+ return binding;
2371
+ }
2372
+ await this.discoverWorkspaceSessions(binding.workspaceId, userId, {
2373
+ force: true,
2374
+ refreshStateMode: "deferred"
2375
+ }).catch(() => {
2376
+ return [];
2377
+ });
2378
+ return this.getBindingOrThrow(sessionId);
2379
+ }
2285
2380
  resolveLiveActivityObservation(sessionId) {
2286
2381
  for (const resolver of this.liveActivityObservationResolvers) {
2287
2382
  const observation = resolver(sessionId);
@@ -2945,6 +3040,48 @@ function isLegacyCodingNsRolloutSession(providerSessionId, rawStoreRef) {
2945
3040
  function shouldRemoveMissingSyntheticCodexSession(rawStoreRef) {
2946
3041
  return isSyntheticCodexRawStoreRef(rawStoreRef) && !existsSync(rawStoreRef);
2947
3042
  }
3043
+ function shouldRepairCodexDirtyBinding(binding) {
3044
+ if (binding.provider !== "codex") {
3045
+ return false;
3046
+ }
3047
+ if (isSyntheticCodexRawStoreRef(binding.rawStoreRef)) {
3048
+ return false;
3049
+ }
3050
+ const expectedThreadId = binding.providerSessionId.trim();
3051
+ if (!expectedThreadId) {
3052
+ return false;
3053
+ }
3054
+ const boundThreadId = readCodexThreadIdFromRawStore(binding.rawStoreRef);
3055
+ if (boundThreadId) {
3056
+ return boundThreadId !== expectedThreadId;
3057
+ }
3058
+ return !existsSync(binding.rawStoreRef);
3059
+ }
3060
+ function readCodexThreadIdFromRawStore(filePath) {
3061
+ if (!existsSync(filePath)) {
3062
+ return null;
3063
+ }
3064
+ try {
3065
+ const firstLine = readFileSync(filePath, "utf8")
3066
+ .split(/\r?\n/, 1)
3067
+ .at(0)
3068
+ ?.trim();
3069
+ if (!firstLine) {
3070
+ return null;
3071
+ }
3072
+ const record = JSON.parse(firstLine);
3073
+ if (record.type !== "session_meta") {
3074
+ return null;
3075
+ }
3076
+ const threadId = typeof record.payload?.id === "string"
3077
+ ? record.payload.id.trim()
3078
+ : "";
3079
+ return threadId.length > 0 ? threadId : null;
3080
+ }
3081
+ catch {
3082
+ return null;
3083
+ }
3084
+ }
2948
3085
  function shouldMatchSessionBindingByRawStoreRef(provider) {
2949
3086
  return provider !== "codex";
2950
3087
  }
@@ -3134,13 +3271,14 @@ function buildReconstructedForkPrompt(input) {
3134
3271
  function buildProviderCapabilityCacheKey(provider, workspacePath) {
3135
3272
  return `${provider}::${workspacePath ?? ""}`;
3136
3273
  }
3137
- async function runWithConcurrency(items, concurrency, worker) {
3274
+ async function runWithConcurrency(items, concurrency, worker, signal) {
3138
3275
  const normalizedConcurrency = Math.max(1, Math.floor(concurrency) || 1);
3139
3276
  const queue = [...items];
3140
3277
  const runners = Array.from({
3141
3278
  length: Math.min(normalizedConcurrency, queue.length || 1)
3142
3279
  }, async () => {
3143
3280
  while (queue.length > 0) {
3281
+ throwIfAborted(signal);
3144
3282
  const current = queue.shift();
3145
3283
  if (current === undefined) {
3146
3284
  return;
@@ -3150,7 +3288,44 @@ async function runWithConcurrency(items, concurrency, worker) {
3150
3288
  });
3151
3289
  await Promise.all(runners);
3152
3290
  }
3153
- async function runBatchedTransactions(items, batchSize, transaction) {
3291
+ function throwIfAborted(signal) {
3292
+ if (signal?.aborted) {
3293
+ throw signal.reason ?? new Error("任务已取消");
3294
+ }
3295
+ }
3296
+ async function awaitTaskHandleWithSignal(handle, signal) {
3297
+ if (!signal) {
3298
+ return await handle.promise;
3299
+ }
3300
+ if (signal.aborted) {
3301
+ handle.cancel(getAbortMessage(signal.reason));
3302
+ throw signal.reason ?? new Error("任务已取消");
3303
+ }
3304
+ return await new Promise((resolve, reject) => {
3305
+ const onAbort = () => {
3306
+ handle.cancel(getAbortMessage(signal.reason));
3307
+ reject(signal.reason ?? new Error("任务已取消"));
3308
+ };
3309
+ signal.addEventListener("abort", onAbort, { once: true });
3310
+ handle.promise.then((value) => {
3311
+ signal.removeEventListener("abort", onAbort);
3312
+ resolve(value);
3313
+ }, (error) => {
3314
+ signal.removeEventListener("abort", onAbort);
3315
+ reject(error);
3316
+ });
3317
+ });
3318
+ }
3319
+ function getAbortMessage(reason) {
3320
+ if (reason instanceof Error && reason.message.trim().length > 0) {
3321
+ return reason.message;
3322
+ }
3323
+ if (typeof reason === "string" && reason.trim().length > 0) {
3324
+ return reason;
3325
+ }
3326
+ return "任务已取消";
3327
+ }
3328
+ async function runBatchedTransactions(items, batchSize, transaction, logOptions) {
3154
3329
  const normalizedBatchSize = Math.max(1, Math.floor(batchSize) || 1);
3155
3330
  let batchCount = 0;
3156
3331
  let maxBatchMs = 0;
@@ -3159,6 +3334,19 @@ async function runBatchedTransactions(items, batchSize, transaction) {
3159
3334
  const batchStartedAt = Date.now();
3160
3335
  transaction(batch);
3161
3336
  const batchDurationMs = Date.now() - batchStartedAt;
3337
+ const nextBatchIndex = batchCount + 1;
3338
+ if (logOptions) {
3339
+ logPerformance(logOptions.scope, batchDurationMs, {
3340
+ ...logOptions.detail,
3341
+ batchIndex: nextBatchIndex,
3342
+ batchSize: batch.length,
3343
+ batchStartIndex: index,
3344
+ totalItems: items.length,
3345
+ configuredBatchSize: normalizedBatchSize
3346
+ }, {
3347
+ thresholdMs: logOptions.thresholdMs ?? SESSION_TRANSACTION_HOTSPOT_THRESHOLD_MS
3348
+ });
3349
+ }
3162
3350
  batchCount += 1;
3163
3351
  maxBatchMs = Math.max(maxBatchMs, batchDurationMs);
3164
3352
  if (index + normalizedBatchSize < items.length) {