@covro/covro 0.0.0-stage → 0.1.0-dev.1

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 (133) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +173 -2
  3. package/README.zh-CN.md +174 -0
  4. package/bin/covro.mjs +8 -0
  5. package/dist/cli/control.js +66 -0
  6. package/dist/cli/control.js.map +7 -0
  7. package/dist/cli/daemon.js +118 -0
  8. package/dist/cli/daemon.js.map +7 -0
  9. package/dist/cli/main.js +152 -0
  10. package/dist/cli/main.js.map +7 -0
  11. package/dist/cli/paths.js +60 -0
  12. package/dist/cli/paths.js.map +7 -0
  13. package/dist/cli/service.js +87 -0
  14. package/dist/cli/service.js.map +7 -0
  15. package/dist/codex/adapter.js +1195 -0
  16. package/dist/codex/adapter.js.map +7 -0
  17. package/dist/codex/auto-review.js +102 -0
  18. package/dist/codex/auto-review.js.map +7 -0
  19. package/dist/codex/compatibility.js +7 -0
  20. package/dist/codex/compatibility.js.map +7 -0
  21. package/dist/codex/mapping.js +210 -0
  22. package/dist/codex/mapping.js.map +7 -0
  23. package/dist/codex/permissions.js +51 -0
  24. package/dist/codex/permissions.js.map +7 -0
  25. package/dist/codex/thread-title-policy.js +52 -0
  26. package/dist/codex/thread-title-policy.js.map +7 -0
  27. package/dist/codex/thread-titles.js +259 -0
  28. package/dist/codex/thread-titles.js.map +7 -0
  29. package/dist/codex/transport.js +236 -0
  30. package/dist/codex/transport.js.map +7 -0
  31. package/dist/codex/turn-token-usage.js +46 -0
  32. package/dist/codex/turn-token-usage.js.map +7 -0
  33. package/dist/codex/types.js +1 -0
  34. package/dist/codex/types.js.map +7 -0
  35. package/dist/drizzle/0000_past_wildside.sql +45 -0
  36. package/dist/drizzle/0001_shocking_ender_wiggin.sql +2 -0
  37. package/dist/drizzle/0002_allow_parallel_threads.sql +1 -0
  38. package/dist/drizzle/0003_project_registrations.sql +11 -0
  39. package/dist/drizzle/0004_turn_usage.sql +6 -0
  40. package/dist/drizzle/meta/0000_snapshot.json +309 -0
  41. package/dist/drizzle/meta/0001_snapshot.json +323 -0
  42. package/dist/drizzle/meta/0002_snapshot.json +315 -0
  43. package/dist/drizzle/meta/0003_snapshot.json +381 -0
  44. package/dist/drizzle/meta/0004_snapshot.json +420 -0
  45. package/dist/drizzle/meta/_journal.json +41 -0
  46. package/dist/server/auth.js +47 -0
  47. package/dist/server/auth.js.map +7 -0
  48. package/dist/server/compaction.js +40 -0
  49. package/dist/server/compaction.js.map +7 -0
  50. package/dist/server/config-store.js +147 -0
  51. package/dist/server/config-store.js.map +7 -0
  52. package/dist/server/config.js +124 -0
  53. package/dist/server/config.js.map +7 -0
  54. package/dist/server/db/index.js +21 -0
  55. package/dist/server/db/index.js.map +7 -0
  56. package/dist/server/db/migrate.js +26 -0
  57. package/dist/server/db/migrate.js.map +7 -0
  58. package/dist/server/db/operations.js +201 -0
  59. package/dist/server/db/operations.js.map +7 -0
  60. package/dist/server/db/schema.js +99 -0
  61. package/dist/server/db/schema.js.map +7 -0
  62. package/dist/server/db/turn-usage.js +31 -0
  63. package/dist/server/db/turn-usage.js.map +7 -0
  64. package/dist/server/events.js +123 -0
  65. package/dist/server/events.js.map +7 -0
  66. package/dist/server/http.js +305 -0
  67. package/dist/server/http.js.map +7 -0
  68. package/dist/server/images.js +49 -0
  69. package/dist/server/images.js.map +7 -0
  70. package/dist/server/instance-lock.js +25 -0
  71. package/dist/server/instance-lock.js.map +7 -0
  72. package/dist/server/local-files.js +100 -0
  73. package/dist/server/local-files.js.map +7 -0
  74. package/dist/server/main.js +125 -0
  75. package/dist/server/main.js.map +7 -0
  76. package/dist/server/markdown.js +28 -0
  77. package/dist/server/markdown.js.map +7 -0
  78. package/dist/server/projection.js +348 -0
  79. package/dist/server/projection.js.map +7 -0
  80. package/dist/server/projects.js +230 -0
  81. package/dist/server/projects.js.map +7 -0
  82. package/dist/server/service.js +1221 -0
  83. package/dist/server/service.js.map +7 -0
  84. package/dist/server/snapshot.js +98 -0
  85. package/dist/server/snapshot.js.map +7 -0
  86. package/dist/server/thread-export.js +97 -0
  87. package/dist/server/thread-export.js.map +7 -0
  88. package/dist/server/websocket.js +126 -0
  89. package/dist/server/websocket.js.map +7 -0
  90. package/dist/shared/async-questions.js +39 -0
  91. package/dist/shared/async-questions.js.map +7 -0
  92. package/dist/shared/codex-compatibility.js +56 -0
  93. package/dist/shared/codex-compatibility.js.map +7 -0
  94. package/dist/shared/elicitation.js +314 -0
  95. package/dist/shared/elicitation.js.map +7 -0
  96. package/dist/shared/errors.js +24 -0
  97. package/dist/shared/errors.js.map +7 -0
  98. package/dist/shared/gateway-config.js +1 -0
  99. package/dist/shared/gateway-config.js.map +7 -0
  100. package/dist/shared/images.js +21 -0
  101. package/dist/shared/images.js.map +7 -0
  102. package/dist/shared/package.js +5 -0
  103. package/dist/shared/package.js.map +7 -0
  104. package/dist/shared/permissions.js +63 -0
  105. package/dist/shared/permissions.js.map +7 -0
  106. package/dist/shared/thread-name.js +14 -0
  107. package/dist/shared/thread-name.js.map +7 -0
  108. package/dist/shared/thread-summary.js +24 -0
  109. package/dist/shared/thread-summary.js.map +7 -0
  110. package/dist/shared/types.js +1 -0
  111. package/dist/shared/types.js.map +7 -0
  112. package/dist/shared/updates.js +7 -0
  113. package/dist/shared/updates.js.map +7 -0
  114. package/dist/shared/validation.js +116 -0
  115. package/dist/shared/validation.js.map +7 -0
  116. package/dist/shared/versions.js +36 -0
  117. package/dist/shared/versions.js.map +7 -0
  118. package/dist/update-worker.cjs +21968 -0
  119. package/dist/updates/files.js +54 -0
  120. package/dist/updates/files.js.map +7 -0
  121. package/dist/updates/manager.js +315 -0
  122. package/dist/updates/manager.js.map +7 -0
  123. package/dist/updates/registry.js +71 -0
  124. package/dist/updates/registry.js.map +7 -0
  125. package/dist/updates/transaction.js +96 -0
  126. package/dist/updates/transaction.js.map +7 -0
  127. package/dist/updates/worker.js +345 -0
  128. package/dist/updates/worker.js.map +7 -0
  129. package/dist/web/assets/index-BW9afH_Y.js +83 -0
  130. package/dist/web/assets/index-L3S1ZiO0.css +1 -0
  131. package/dist/web/covro.svg +6 -0
  132. package/dist/web/index.html +15 -0
  133. package/package.json +88 -4
@@ -0,0 +1,1221 @@
1
+ import { realpathSync } from "node:fs";
2
+ import { AppError } from "../shared/errors.js";
3
+ import { threadNameError } from "../shared/thread-name.js";
4
+ import {
5
+ validateElicitationResolution,
6
+ validatePermissionResolution
7
+ } from "../shared/elicitation.js";
8
+ import { toThreadSummary } from "../shared/thread-summary.js";
9
+ import {
10
+ StoreConflict
11
+ } from "./db/operations.js";
12
+ import { EventHub } from "./events.js";
13
+ import { TurnUsageStore } from "./db/turn-usage.js";
14
+ import { Projection, isActive, isTerminal } from "./projection.js";
15
+ import { renderThreadExport } from "./thread-export.js";
16
+ import { readLocalImage } from "./images.js";
17
+ import { temporaryFileRoots } from "./local-files.js";
18
+ import { readLocalMarkdown } from "./markdown.js";
19
+ class GatewayService {
20
+ constructor(options) {
21
+ this.options = options;
22
+ this.store = options.store;
23
+ this.turnUsage = new TurnUsageStore(this.store.database);
24
+ this.projects = options.projects;
25
+ this.codex = options.codex;
26
+ this.events = new EventHub(options.limits?.eventBytes);
27
+ this.projection = new Projection(options.limits?.itemBytes);
28
+ this.currentEpoch = this.codex.epoch;
29
+ this.store.recover();
30
+ this.removeListener = this.codex.onEvent((event) => this.consume(event));
31
+ }
32
+ options;
33
+ events;
34
+ projection;
35
+ store;
36
+ turnUsage;
37
+ projects;
38
+ codex;
39
+ removeListener;
40
+ fileTemporaryRoots = temporaryFileRoots();
41
+ accepting = true;
42
+ maintenance = false;
43
+ currentEpoch;
44
+ confirmedCompatibility;
45
+ archiveChanges = /* @__PURE__ */ new Set();
46
+ turnBaselines = /* @__PURE__ */ new Map();
47
+ renameChanges = /* @__PURE__ */ new Set();
48
+ reads = /* @__PURE__ */ new Map();
49
+ resumes = /* @__PURE__ */ new Map();
50
+ executionReads = /* @__PURE__ */ new Map();
51
+ summaries = /* @__PURE__ */ new Map();
52
+ // Request ordering only: title values always come from app-server.
53
+ titleReadSequence = 0;
54
+ titleReads = /* @__PURE__ */ new Map();
55
+ // A status/title/history update must not discard a lifecycle response's full configuration.
56
+ runtimeSettingsSequence = 0;
57
+ runtimeSettingsUpdates = /* @__PURE__ */ new Map();
58
+ inFlightOperations = /* @__PURE__ */ new Set();
59
+ pendingThreadCreations = /* @__PURE__ */ new Map();
60
+ listProjects() {
61
+ return this.projects.list();
62
+ }
63
+ isInMaintenance() {
64
+ return this.maintenance;
65
+ }
66
+ setMaintenance(value) {
67
+ this.maintenance = value;
68
+ }
69
+ prepareUpdate() {
70
+ if (this.inFlightOperations.size || this.resumes.size || this.pendingThreadCreations.size)
71
+ return { ready: false, reason: "\u7B49\u5F85\u6B63\u5728\u63D0\u4EA4\u6216\u6062\u590D\u7684\u64CD\u4F5C\u5B8C\u6210" };
72
+ if (this.store.activeMarkers().length || this.store.unresolvedOperations().length)
73
+ return { ready: false, reason: "\u7B49\u5F85\u4EFB\u52A1\u7ED3\u675F\uFF0C\u5E76\u6838\u5BF9\u7ED3\u679C\u5C1A\u672A\u786E\u8BA4\u7684\u64CD\u4F5C" };
74
+ if (this.projection.values().some((thread) => thread.managed && isActive(thread.status)) || [...this.projection.actions.values()].some(
75
+ (action) => ["pending", "responding"].includes(action.status)
76
+ ))
77
+ return { ready: false, reason: "\u7B49\u5F85\u6267\u884C\u4E2D\u7684\u4EFB\u52A1\u3001\u5BA1\u6279\u6216\u8F93\u5165\u7ED3\u675F" };
78
+ this.maintenance = true;
79
+ return { ready: true };
80
+ }
81
+ addProject(input) {
82
+ const project = this.projects.add(input);
83
+ this.events.publishProjects(this.projects.list());
84
+ return project;
85
+ }
86
+ renameProject(projectId, input) {
87
+ const project = this.projects.rename(projectId, input);
88
+ this.events.publishProjects(this.projects.list());
89
+ return project;
90
+ }
91
+ removeProject(projectId) {
92
+ const project = this.projects.describe(projectId);
93
+ const pending = this.store.pendingProjectOperations(projectId)[0];
94
+ const marker = this.store.activeMarkers().find((entry) => {
95
+ const link = this.store.getThreadLink(entry.threadId);
96
+ return link ? link.projectId === projectId : entry.workspaceKey === project.workspaceKey;
97
+ });
98
+ const active = this.projection.values().find(
99
+ (thread) => thread.projectId === projectId && (isActive(thread.status) || thread.actions.some((action) => ["pending", "responding"].includes(action.status)))
100
+ );
101
+ if (pending || marker || active)
102
+ throw new AppError(
103
+ "PROJECT_BUSY",
104
+ "\u9879\u76EE\u4ECD\u6709\u6D3B\u52A8\u4EFB\u52A1\u6216\u7ED3\u679C\u5F85\u6838\u5BF9\u7684\u64CD\u4F5C\uFF1B\u8BF7\u5148\u7ED3\u675F\u4EFB\u52A1\u5E76\u6838\u5BF9\u65E7\u64CD\u4F5C",
105
+ 409,
106
+ {
107
+ projectId,
108
+ ...pending || marker ? { operationId: pending?.clientOperationId ?? marker?.operationId } : {},
109
+ ...marker || active ? { threadId: marker?.threadId ?? active?.id } : {}
110
+ }
111
+ );
112
+ this.projects.remove(projectId);
113
+ this.events.publishProjects(this.projects.list());
114
+ return { removed: true };
115
+ }
116
+ getSystem() {
117
+ const diagnostics = this.codex.diagnostics();
118
+ return {
119
+ ...diagnostics,
120
+ compatibilityConfirmation: this.compatibilityConfirmation(diagnostics),
121
+ authMode: this.options.authMode ?? "local",
122
+ demo: this.options.demo ?? false
123
+ };
124
+ }
125
+ subscribeRuntime(listener) {
126
+ return this.events.subscribeRuntime(listener);
127
+ }
128
+ subscribeOverview(listener) {
129
+ return this.events.subscribeOverview(listener);
130
+ }
131
+ async confirmCompatibility(input) {
132
+ return this.perform("system.compatibility.confirm", "system", input, async () => {
133
+ const diagnostics = this.codex.diagnostics();
134
+ this.ensureCompatibilityAvailable(diagnostics);
135
+ if (input.confirm !== true || input.codexEpoch !== diagnostics.codexEpoch || input.codexVersion !== diagnostics.codexVersion || input.verifiedVersion !== diagnostics.verifiedVersion)
136
+ throw new AppError(
137
+ "COMPATIBILITY_CHANGED",
138
+ "Codex \u8FD0\u884C\u7248\u672C\u6216\u9A8C\u8BC1\u57FA\u7EBF\u5DF2\u53D8\u5316\uFF0C\u8BF7\u5237\u65B0\u540E\u91CD\u65B0\u786E\u8BA4",
139
+ 409
140
+ );
141
+ this.store.dispatch(input.clientOperationId);
142
+ const fingerprint = this.compatibilityFingerprint(diagnostics);
143
+ return {
144
+ result: {
145
+ codexEpoch: diagnostics.codexEpoch,
146
+ codexVersion: diagnostics.codexVersion,
147
+ verifiedVersion: diagnostics.verifiedVersion,
148
+ compatibilityConfirmation: diagnostics.compatibility === "unverified" ? "confirmed" : "not-required"
149
+ },
150
+ onAcknowledged: () => {
151
+ const current = this.codex.diagnostics();
152
+ if (this.codex.ready && current.ready && current.compatibility === "unverified" && this.compatibilityFingerprint(current) === fingerprint)
153
+ this.confirmedCompatibility = fingerprint;
154
+ this.events.publish("runtime", this.getSystem());
155
+ }
156
+ };
157
+ });
158
+ }
159
+ async readRuntimeSettings(projectId) {
160
+ const project = this.projects.get(projectId);
161
+ return this.codex.readRuntimeSettings(project.canonicalRoot);
162
+ }
163
+ async saveModelDefaults(projectId, input) {
164
+ return this.perform("config.modelDefaults", projectId, input, async () => {
165
+ this.projects.get(projectId);
166
+ this.ensureReady();
167
+ this.ensureCompatibilityConfirmed();
168
+ this.store.associateProject(input.clientOperationId, projectId);
169
+ this.store.dispatch(input.clientOperationId);
170
+ return { result: await this.codex.saveModelDefaults(input.settings) };
171
+ });
172
+ }
173
+ async listThreads(projectId, cursor, archived = false) {
174
+ const project = this.projects.get(projectId);
175
+ const titleReads = new Map(
176
+ this.projection.values().filter((thread) => thread.projectId === projectId).map((thread) => [thread.id, this.beginTitleRead(thread.id)])
177
+ );
178
+ const page = await this.codex.listThreads(project.canonicalRoot, cursor, archived);
179
+ this.projects.get(projectId);
180
+ const data = [];
181
+ for (const thread of page.data) {
182
+ try {
183
+ const normalized = this.normalize(thread, projectId);
184
+ normalized.hasMessages ??= true;
185
+ const cached = this.projection.get(thread.id);
186
+ if (cached && normalized.hasMessages) cached.hasMessages = true;
187
+ const titleRead = titleReads.get(thread.id);
188
+ if (cached && titleRead !== void 0) {
189
+ if (this.titleReads.get(thread.id) === titleRead)
190
+ this.updateArchiveState(thread.id, Boolean(normalized.archived));
191
+ this.updateThreadTitle(thread.id, normalized.title, titleRead);
192
+ }
193
+ data.push(this.threadView(cached ?? normalized));
194
+ } catch (error) {
195
+ if (!(error instanceof AppError) || !["PROJECT_OWNERSHIP", "PROJECT_UNAVAILABLE", "PROJECT_NOT_FOUND"].includes(error.code))
196
+ throw error;
197
+ }
198
+ }
199
+ if (!archived && cursor === void 0) {
200
+ const pending = this.store.activeMarkers().flatMap((marker) => {
201
+ const link = this.store.getThreadLink(marker.threadId);
202
+ if (link?.projectId !== projectId || this.projection.get(marker.threadId)) return [];
203
+ return [
204
+ {
205
+ id: marker.threadId,
206
+ projectId,
207
+ cwd: link.cwd,
208
+ source: link.source,
209
+ title: "\u63D0\u4EA4\u7ED3\u679C\u5F85\u6838\u5BF9",
210
+ status: "unknown",
211
+ turns: [],
212
+ actions: [],
213
+ managed: false,
214
+ updatedAt: marker.createdAt
215
+ }
216
+ ];
217
+ });
218
+ const managed = [...this.projection.values(), ...pending].filter(
219
+ (thread) => (thread.managed || this.store.activeForThread(thread.id)) && thread.projectId === projectId && this.historyVisible(thread)
220
+ ).sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0)).slice(0, 30);
221
+ for (const thread of managed) {
222
+ this.projects.assertOwnership(projectId, thread.cwd);
223
+ if (!data.some((entry) => entry.id === thread.id)) data.unshift(this.threadView(thread));
224
+ }
225
+ }
226
+ return {
227
+ data: data.filter((thread) => thread.historyVisible && Boolean(thread.archived) === archived),
228
+ nextCursor: page.nextCursor
229
+ };
230
+ }
231
+ async listThreadSummaries(projectId, cursor, archived = false) {
232
+ const boundary = this.events.cursor();
233
+ const page = await this.listThreads(projectId, cursor, archived);
234
+ return { data: page.data.map(toThreadSummary), nextCursor: page.nextCursor, cursor: boundary };
235
+ }
236
+ async setThreadArchived(threadId, input) {
237
+ return this.perform("thread.archive", threadId, input, async () => {
238
+ const thread = await this.loadThread(threadId);
239
+ this.ensureReady();
240
+ this.ensureCompatibilityConfirmed();
241
+ this.ensureArchiveAvailable(thread, input.clientOperationId);
242
+ this.ensureRenameAvailable(thread);
243
+ this.store.associateProject(input.clientOperationId, thread.projectId);
244
+ this.archiveChanges.add(threadId);
245
+ try {
246
+ const actual = await this.codex.readThreadMetadata(threadId);
247
+ this.projects.assertOwnership(thread.projectId, actual.cwd);
248
+ const current = this.projection.get(threadId);
249
+ if (input.archived && (this.resumes.has(threadId) || this.store.activeForThread(threadId) || current.managed && current.status === "unknown" || [actual, current].some(
250
+ (entry) => isActive(entry.status) || entry.turns.some((turn) => isActive(turn.status) || turn.status === "unknown") || entry.actions.some((action) => ["pending", "responding"].includes(action.status))
251
+ )))
252
+ throw new AppError("THREAD_BUSY", "\u4EFB\u52A1\u7ED3\u675F\u5E76\u5904\u7406\u5B8C\u5F85\u529E\u540E\u624D\u80FD\u5F52\u6863\u4F1A\u8BDD", 409);
253
+ this.ensureReady();
254
+ this.ensureCompatibilityConfirmed();
255
+ this.store.dispatch(input.clientOperationId);
256
+ if (Boolean(actual.archived) !== input.archived) {
257
+ try {
258
+ if (input.archived) await this.codex.archiveThread(threadId);
259
+ else await this.codex.unarchiveThread(threadId);
260
+ } catch (error) {
261
+ try {
262
+ const refreshed = await this.codex.readThreadMetadata(threadId);
263
+ this.updateArchiveState(threadId, Boolean(refreshed.archived));
264
+ } catch {
265
+ } finally {
266
+ this.releaseThreadOwnership(threadId);
267
+ this.publishThread(threadId);
268
+ }
269
+ throw error;
270
+ }
271
+ }
272
+ this.beginTitleRead(threadId);
273
+ this.updateArchiveState(threadId, input.archived);
274
+ this.publishThread(threadId);
275
+ return {
276
+ result: {
277
+ threadId,
278
+ archived: input.archived,
279
+ summary: toThreadSummary(this.threadView(this.projection.get(threadId))),
280
+ cursor: this.events.cursor()
281
+ }
282
+ };
283
+ } finally {
284
+ this.archiveChanges.delete(threadId);
285
+ }
286
+ });
287
+ }
288
+ async renameThread(threadId, input) {
289
+ return this.perform("thread.rename", threadId, input, async () => {
290
+ const problem = threadNameError(input.name);
291
+ if (problem) throw new AppError("VALIDATION_ERROR", problem);
292
+ const name = input.name.trim();
293
+ const thread = await this.loadThread(threadId);
294
+ this.ensureManaged(thread);
295
+ this.ensureCompatibilityConfirmed();
296
+ this.ensureRenameAvailable(thread, input.clientOperationId);
297
+ this.store.associateProject(input.clientOperationId, thread.projectId);
298
+ this.renameChanges.add(threadId);
299
+ try {
300
+ const epoch = this.codex.epoch;
301
+ this.store.dispatch(input.clientOperationId);
302
+ await this.codex.renameThread(threadId, name);
303
+ if (this.codex.epoch !== epoch || !this.codex.ready)
304
+ throw new Error("\u91CD\u547D\u540D\u5DF2\u53D1\u9001\uFF0C\u4F46 Codex \u5B9E\u4F8B\u5DF2\u53D8\u5316\uFF0C\u8BF7\u6838\u5BF9\u4F1A\u8BDD\u540D\u79F0");
305
+ const sequence = this.beginTitleRead(threadId);
306
+ let actual;
307
+ try {
308
+ actual = await this.codex.readThreadMetadata(threadId);
309
+ if (actual.id !== threadId || this.codex.epoch !== epoch || !this.codex.ready)
310
+ throw new Error("\u4F1A\u8BDD\u6216 Codex \u5B9E\u4F8B\u5DF2\u53D8\u5316");
311
+ this.projects.assertOwnership(thread.projectId, actual.cwd);
312
+ } catch (cause) {
313
+ throw new Error("\u91CD\u547D\u540D\u5DF2\u53D1\u9001\uFF0C\u4F46\u65E0\u6CD5\u786E\u8BA4\u4F1A\u8BDD\u540D\u79F0\uFF0C\u8BF7\u6838\u5BF9\u63D0\u4EA4", { cause });
314
+ }
315
+ this.updateThreadTitle(threadId, actual.title, sequence);
316
+ if (actual.title !== name || this.projection.get(threadId)?.title !== name)
317
+ throw new Error("\u91CD\u547D\u540D\u5DF2\u53D1\u9001\uFF0C\u4F46\u5F53\u524D\u540D\u79F0\u4E0E\u8BF7\u6C42\u4E0D\u4E00\u81F4\uFF0C\u8BF7\u6838\u5BF9\u63D0\u4EA4");
318
+ return {
319
+ result: {
320
+ threadId,
321
+ name,
322
+ summary: toThreadSummary(this.threadView(this.projection.get(threadId))),
323
+ cursor: this.events.cursor()
324
+ }
325
+ };
326
+ } finally {
327
+ this.renameChanges.delete(threadId);
328
+ }
329
+ });
330
+ }
331
+ ensureRenameAvailable(thread, operationId) {
332
+ const pending = this.store.pendingProjectOperations(thread.projectId).some(
333
+ (entry) => entry.kind === "thread.rename" && entry.target === thread.id && entry.clientOperationId !== operationId && entry.state !== "accepted"
334
+ );
335
+ if (this.renameChanges.has(thread.id) || pending)
336
+ throw new AppError("THREAD_BUSY", "\u4F1A\u8BDD\u91CD\u547D\u540D\u6B63\u5728\u5904\u7406\u6216\u7ED3\u679C\u5F85\u6838\u5BF9\uFF0C\u8BF7\u5148\u6838\u5BF9\u63D0\u4EA4", 409);
337
+ }
338
+ ensureArchiveAvailable(thread, operationId) {
339
+ const pending = this.store.pendingProjectOperations(thread.projectId).find(
340
+ (entry) => entry.kind === "thread.archive" && entry.target === thread.id && entry.clientOperationId !== operationId
341
+ );
342
+ if (this.archiveChanges.has(thread.id) || pending)
343
+ throw new AppError("THREAD_BUSY", "\u4F1A\u8BDD\u5F52\u6863\u64CD\u4F5C\u6B63\u5728\u5904\u7406\u6216\u7ED3\u679C\u5F85\u6838\u5BF9\uFF0C\u8BF7\u5237\u65B0\u72B6\u6001", 409);
344
+ }
345
+ releaseThreadOwnership(threadId) {
346
+ const link = this.store.getThreadLink(threadId);
347
+ if (link) this.store.linkThread({ ...link, managedEpoch: null });
348
+ const thread = this.projection.get(threadId);
349
+ if (thread) {
350
+ thread.managed = false;
351
+ delete thread.resumeError;
352
+ this.projection.touch(threadId);
353
+ }
354
+ }
355
+ updateArchiveState(threadId, archived) {
356
+ const thread = this.projection.get(threadId);
357
+ if (!thread || Boolean(thread.archived) === archived) return;
358
+ thread.archived = archived;
359
+ if (archived) this.releaseThreadOwnership(threadId);
360
+ this.projection.touch(threadId);
361
+ if (!this.archiveChanges.has(threadId)) this.publishThread(threadId);
362
+ }
363
+ async getThreadMetadata(threadId) {
364
+ const cached = this.projection.get(threadId);
365
+ if (cached) {
366
+ this.projects.assertOwnership(cached.projectId, cached.cwd);
367
+ if (this.codex.ready) await this.refreshThreadMetadata(threadId);
368
+ this.projects.assertOwnership(cached.projectId, cached.cwd);
369
+ return { ...this.threadView(cached), turns: [], actions: [] };
370
+ }
371
+ const metadata = this.threadView(this.normalize(await this.codex.readThreadMetadata(threadId)));
372
+ return { ...metadata, turns: [], actions: [] };
373
+ }
374
+ async getThread(threadId) {
375
+ const cached = this.projection.get(threadId);
376
+ await this.loadThread(threadId);
377
+ if (cached && this.codex.ready) await this.refreshThreadMetadata(threadId);
378
+ const thread = this.projection.get(threadId);
379
+ this.projects.assertOwnership(thread.projectId, thread.cwd);
380
+ return this.threadView(thread);
381
+ }
382
+ async exportThread(threadId) {
383
+ this.ensureReady();
384
+ const epoch = this.codex.epoch;
385
+ const revision = this.projection.revision(threadId);
386
+ const thread = await this.codex.readThread(threadId);
387
+ const latest = await this.codex.readThreadMetadata(threadId);
388
+ this.ensureReady();
389
+ if (this.codex.epoch !== epoch)
390
+ throw new AppError("EXPORT_RUNTIME_CHANGED", "\u8BFB\u53D6\u671F\u95F4 Codex \u5DF2\u91CD\u542F\uFF0C\u8BF7\u91CD\u65B0\u5BFC\u51FA\u3002", 409);
391
+ if (thread.id !== threadId || latest.id !== threadId || thread.historyTruncated)
392
+ throw new AppError(
393
+ "EXPORT_HISTORY_INCOMPLETE",
394
+ "\u672A\u80FD\u53D6\u5F97\u5B8C\u6574\u4F1A\u8BDD\u5386\u53F2\uFF0C\u672A\u751F\u6210\u5BFC\u51FA\u6587\u4EF6\u3002",
395
+ 409
396
+ );
397
+ const link = this.store.getThreadLink(threadId);
398
+ if (link) this.projects.assertOwnership(link.projectId, thread.cwd);
399
+ else this.projects.findOwner(thread.cwd);
400
+ return renderThreadExport(thread, {
401
+ executionUncertain: Boolean(this.store.activeForThread(threadId)),
402
+ historyChanged: revision !== this.projection.revision(threadId) || thread.updatedAt !== latest.updatedAt || thread.status !== latest.status
403
+ });
404
+ }
405
+ getThreadImage(threadId, path) {
406
+ return this.readThreadFile(threadId, path, readLocalImage);
407
+ }
408
+ getThreadMarkdown(threadId, path) {
409
+ return this.readThreadFile(threadId, path, readLocalMarkdown);
410
+ }
411
+ async readThreadFile(threadId, path, read) {
412
+ let link = this.store.getThreadLink(threadId);
413
+ if (!link) {
414
+ this.normalize(await this.codex.readThreadMetadata(threadId));
415
+ link = this.store.getThreadLink(threadId);
416
+ }
417
+ const { project, cwd } = this.projects.assertOwnership(link.projectId, link.cwd);
418
+ const file = await read(path, {
419
+ cwd,
420
+ roots: [
421
+ { canonicalRoot: project.canonicalRoot, aliases: [project.rootPath] },
422
+ ...this.fileTemporaryRoots
423
+ ]
424
+ });
425
+ this.projects.assertOwnership(link.projectId, link.cwd);
426
+ return file;
427
+ }
428
+ async loadThread(threadId) {
429
+ const cached = this.projection.get(threadId);
430
+ if (cached) {
431
+ this.projects.assertOwnership(cached.projectId, cached.cwd);
432
+ const marker = this.store.activeForThread(threadId);
433
+ if (marker?.turnId && this.codex.ready) {
434
+ await this.refreshExecution(marker);
435
+ return this.threadView(cached);
436
+ }
437
+ if (cached.status !== "unknown" || !this.codex.ready) return this.threadView(cached);
438
+ }
439
+ const existing = this.reads.get(threadId);
440
+ if (existing) return this.threadView(await existing);
441
+ const revision = this.projection.revision(threadId);
442
+ const loading = (async () => {
443
+ const thread = this.normalize(await this.codex.readThread(threadId));
444
+ const current = this.projection.get(threadId);
445
+ const preserveCurrent = current && (cached === void 0 || this.projection.revision(threadId) !== revision);
446
+ const result = preserveCurrent ? current : this.projection.thread(thread);
447
+ this.applyRecoveredMarker(result);
448
+ if (!preserveCurrent) {
449
+ for (const turn of result.turns)
450
+ if (isTerminal(turn.status)) this.projection.expireTurn(threadId, turn.id);
451
+ }
452
+ if (cached !== void 0 && !preserveCurrent) this.publishThread(threadId);
453
+ return result;
454
+ })();
455
+ this.reads.set(threadId, loading);
456
+ try {
457
+ return this.threadView(await loading);
458
+ } finally {
459
+ this.reads.delete(threadId);
460
+ }
461
+ }
462
+ async createThread(projectId, input) {
463
+ return this.perform("thread.create", projectId, input, async () => {
464
+ const project = this.projects.get(projectId);
465
+ this.ensureReady();
466
+ this.ensureCompatibilityConfirmed();
467
+ this.store.associateProject(input.clientOperationId, projectId);
468
+ this.store.dispatch(input.clientOperationId);
469
+ this.pendingThreadCreations.set(input.clientOperationId, project.canonicalRoot);
470
+ const epoch = this.codex.epoch;
471
+ const settingsSequence = this.runtimeSettingsSequence;
472
+ const thread = await this.codex.createThread(project.canonicalRoot, input.settings);
473
+ if (this.codex.epoch !== epoch || !this.codex.ready)
474
+ throw new Error("\u521B\u5EFA\u8BF7\u6C42\u5DF2\u53D1\u9001\uFF0C\u4F46 Codex \u5B9E\u4F8B\u5DF2\u53D8\u5316\uFF0C\u8BF7\u6838\u5BF9\u5386\u53F2");
475
+ const normalized = this.normalize(thread, projectId, true);
476
+ this.projection.thread(
477
+ normalized,
478
+ this.projection.get(thread.id) !== void 0,
479
+ (this.runtimeSettingsUpdates.get(thread.id) ?? 0) > settingsSequence
480
+ );
481
+ this.publishThread(thread.id);
482
+ return { result: { threadId: thread.id } };
483
+ });
484
+ }
485
+ async resumeThread(threadId, input) {
486
+ return this.perform("thread.resume", threadId, input, async () => {
487
+ const thread = await this.loadThread(threadId);
488
+ this.ensureReady();
489
+ this.ensureArchiveAvailable(thread);
490
+ if (thread.archived) throw new AppError("THREAD_ARCHIVED", "\u8BF7\u5148\u53D6\u6D88\u5F52\u6863\u540E\u518D\u6062\u590D\u4F1A\u8BDD", 409);
491
+ this.projects.assertOwnership(thread.projectId, thread.cwd);
492
+ this.store.associateProject(input.clientOperationId, thread.projectId);
493
+ const link = this.store.getThreadLink(threadId);
494
+ if (link?.managedEpoch === this.codex.epoch) {
495
+ this.ensureCompatibilityConfirmed();
496
+ this.store.dispatch(input.clientOperationId);
497
+ return { result: { threadId } };
498
+ }
499
+ const marker = this.store.activeForThread(threadId);
500
+ if (marker)
501
+ throw new AppError("RESULT_UNKNOWN", "\u65E7\u6267\u884C\u4ECD\u5F85\u6838\u5BF9\uFF0C\u8BF7\u5148\u68C0\u67E5\u5386\u53F2\u5E76\u6838\u5BF9\u65E7\u64CD\u4F5C", 409, {
502
+ operationId: marker.operationId,
503
+ threadId
504
+ });
505
+ const ownership = this.projects.assertOwnership(thread.projectId, thread.cwd);
506
+ this.ensureCompatibilityConfirmed();
507
+ this.store.dispatch(input.clientOperationId);
508
+ let pending = this.resumes.get(threadId);
509
+ if (!pending) {
510
+ const epoch = this.codex.epoch;
511
+ const revision = this.projection.revision(threadId);
512
+ const settingsSequence = this.runtimeSettingsSequence;
513
+ const current = this.projection.get(threadId);
514
+ delete current.resumeError;
515
+ pending = (async () => {
516
+ try {
517
+ const resumed = await this.codex.resumeThread(threadId, ownership.cwd, input.settings);
518
+ if (this.codex.epoch !== epoch || !this.codex.ready)
519
+ throw new Error("\u6062\u590D\u8BF7\u6C42\u5DF2\u53D1\u9001\uFF0C\u4F46 Codex \u5B9E\u4F8B\u5DF2\u53D8\u5316\uFF0C\u8BF7\u6838\u5BF9\u63D0\u4EA4");
520
+ this.projection.thread(
521
+ this.normalize(resumed, thread.projectId, true),
522
+ this.projection.revision(threadId) !== revision,
523
+ (this.runtimeSettingsUpdates.get(threadId) ?? 0) > settingsSequence
524
+ );
525
+ this.publishThread(threadId);
526
+ } catch (error) {
527
+ if (this.codex.epoch === epoch && this.codex.ready && error instanceof AppError && !("uncertain" in error && error.uncertain)) {
528
+ current.resumeError = error.toJSON();
529
+ this.projection.touch(threadId);
530
+ this.publishThread(threadId);
531
+ }
532
+ throw error;
533
+ }
534
+ })();
535
+ this.resumes.set(threadId, pending);
536
+ }
537
+ try {
538
+ await pending;
539
+ } finally {
540
+ if (this.resumes.get(threadId) === pending) this.resumes.delete(threadId);
541
+ }
542
+ return { result: { threadId } };
543
+ });
544
+ }
545
+ async forkThread(threadId, input) {
546
+ return this.perform("thread.fork", threadId, input, async () => {
547
+ this.ensureReady();
548
+ const source = this.projection.get(threadId) ?? await this.codex.readThread(threadId);
549
+ const link = this.store.getThreadLink(threadId);
550
+ const ownership = link ? this.projects.assertOwnership(link.projectId, source.cwd) : this.projects.findOwner(source.cwd);
551
+ this.ensureCompatibilityConfirmed();
552
+ const epoch = this.codex.epoch;
553
+ this.store.associateProject(input.clientOperationId, ownership.project.id);
554
+ this.store.dispatch(input.clientOperationId);
555
+ this.pendingThreadCreations.set(input.clientOperationId, ownership.cwd);
556
+ const settingsSequence = this.runtimeSettingsSequence;
557
+ const forked = await this.codex.forkThread(threadId, ownership.cwd, input.settings);
558
+ if (forked.id === threadId || this.codex.epoch !== epoch || !this.codex.ready)
559
+ throw new Error("\u5206\u53C9\u8BF7\u6C42\u5DF2\u53D1\u9001\uFF0C\u4F46\u8FD4\u56DE\u7684\u65B0\u4F1A\u8BDD\u65E0\u6CD5\u786E\u8BA4\uFF0C\u8BF7\u6838\u5BF9\u5386\u53F2");
560
+ let normalized;
561
+ try {
562
+ normalized = this.normalize(
563
+ { ...forked, forkedFromId: threadId },
564
+ ownership.project.id,
565
+ true
566
+ );
567
+ } catch (error) {
568
+ throw new Error("\u5206\u53C9\u8BF7\u6C42\u5DF2\u53D1\u9001\uFF0C\u4F46\u8FD4\u56DE\u7684\u4F1A\u8BDD\u76EE\u5F55\u65E0\u6CD5\u786E\u8BA4\uFF0C\u8BF7\u6838\u5BF9\u5386\u53F2", { cause: error });
569
+ }
570
+ this.projection.thread(
571
+ normalized,
572
+ this.projection.get(forked.id) !== void 0,
573
+ (this.runtimeSettingsUpdates.get(forked.id) ?? 0) > settingsSequence
574
+ );
575
+ this.publishThread(forked.id);
576
+ return { result: { threadId: forked.id, forkedFromId: threadId } };
577
+ });
578
+ }
579
+ async startTurn(threadId, input) {
580
+ return this.perform("turn.start", threadId, input, async () => {
581
+ const thread = await this.loadThread(threadId);
582
+ this.ensureManaged(thread);
583
+ const ownership = this.projects.assertOwnership(thread.projectId, thread.cwd);
584
+ if (isActive(thread.status))
585
+ throw new AppError("THREAD_BUSY", "\u4F1A\u8BDD\u5DF2\u6709\u6D3B\u52A8\u4EFB\u52A1\uFF0C\u8BF7\u8FFD\u52A0\u8981\u6C42\u6216\u4E2D\u65AD", 409, { threadId });
586
+ this.ensureCompatibilityConfirmed();
587
+ this.store.dispatch(input.clientOperationId, {
588
+ threadId,
589
+ workspaceKey: ownership.workspaceKey,
590
+ codexEpoch: this.codex.epoch
591
+ });
592
+ this.turnBaselines.set(input.clientOperationId, new Set(thread.turns.map((turn2) => turn2.id)));
593
+ this.publishThread(threadId);
594
+ const revision = this.projection.revision(threadId);
595
+ const turn = await this.codex.startTurn(
596
+ threadId,
597
+ ownership.cwd,
598
+ input.text,
599
+ input.settings,
600
+ input.images
601
+ );
602
+ this.store.attachTurn(input.clientOperationId, turn.id);
603
+ this.turnBaselines.delete(input.clientOperationId);
604
+ this.projection.turn(threadId, turn, this.projection.revision(threadId) !== revision);
605
+ const projectedTurn = this.projection.get(threadId)?.turns.find((entry) => entry.id === turn.id);
606
+ if (projectedTurn && isTerminal(projectedTurn.status))
607
+ this.store.finishTurn(threadId, turn.id, this.codex.epoch);
608
+ this.publishThread(threadId);
609
+ return { result: { threadId, turnId: turn.id }, turnId: turn.id };
610
+ });
611
+ }
612
+ async compactThread(threadId, input) {
613
+ return this.perform("thread.compact", threadId, input, async () => {
614
+ const thread = await this.loadThread(threadId);
615
+ this.ensureManaged(thread);
616
+ const ownership = this.projects.assertOwnership(thread.projectId, thread.cwd);
617
+ if (isActive(thread.status) || thread.turns.some((turn) => isActive(turn.status)))
618
+ throw new AppError("THREAD_BUSY", "\u8BF7\u7B49\u5F85\u5F53\u524D\u4EFB\u52A1\u7ED3\u675F\u540E\u518D\u538B\u7F29\u4E0A\u4E0B\u6587", 409);
619
+ if (!thread.turns.length)
620
+ throw new AppError("EMPTY_THREAD", "\u5F53\u524D\u4F1A\u8BDD\u4E3A\u7A7A\uFF0C\u65E0\u9700\u538B\u7F29\u4E0A\u4E0B\u6587", 409);
621
+ if (thread.status === "unknown")
622
+ throw new AppError("RESULT_UNKNOWN", "\u8BF7\u5148\u6838\u5BF9\u5F53\u524D\u4F1A\u8BDD\u7684\u6267\u884C\u72B6\u6001", 409);
623
+ this.ensureCompatibilityConfirmed();
624
+ this.store.dispatch(input.clientOperationId, {
625
+ threadId,
626
+ workspaceKey: ownership.workspaceKey,
627
+ codexEpoch: this.codex.epoch
628
+ });
629
+ this.turnBaselines.set(input.clientOperationId, new Set(thread.turns.map((turn) => turn.id)));
630
+ this.publishThread(threadId);
631
+ await this.codex.compactThread(threadId);
632
+ this.publishThread(threadId);
633
+ return { result: { threadId } };
634
+ });
635
+ }
636
+ async startReview(threadId, input) {
637
+ return this.perform("review.start", threadId, input, async () => {
638
+ const thread = await this.loadThread(threadId);
639
+ this.ensureManaged(thread);
640
+ const ownership = this.projects.assertOwnership(thread.projectId, thread.cwd);
641
+ if (isActive(thread.status) || thread.turns.some((turn2) => isActive(turn2.status)))
642
+ throw new AppError("THREAD_BUSY", "\u4F1A\u8BDD\u5DF2\u6709\u6D3B\u52A8\u4EFB\u52A1\uFF0C\u7ED3\u675F\u540E\u624D\u80FD\u5F00\u59CB\u4EE3\u7801\u5BA1\u67E5", 409, {
643
+ threadId
644
+ });
645
+ if (thread.status === "unknown")
646
+ throw new AppError("RESULT_UNKNOWN", "\u4F1A\u8BDD\u72B6\u6001\u5C1A\u672A\u786E\u8BA4\uFF0C\u8BF7\u5148\u6838\u5BF9\u63D0\u4EA4", 409, { threadId });
647
+ this.ensureCompatibilityConfirmed();
648
+ this.store.dispatch(input.clientOperationId, {
649
+ threadId,
650
+ workspaceKey: ownership.workspaceKey,
651
+ codexEpoch: this.codex.epoch
652
+ });
653
+ const revision = this.projection.revision(threadId);
654
+ this.turnBaselines.set(input.clientOperationId, new Set(thread.turns.map((turn2) => turn2.id)));
655
+ const turn = await this.codex.startReview(threadId);
656
+ this.store.attachTurn(input.clientOperationId, turn.id);
657
+ this.turnBaselines.delete(input.clientOperationId);
658
+ this.projection.turn(threadId, turn, this.projection.revision(threadId) !== revision);
659
+ const projectedTurn = this.projection.get(threadId)?.turns.find((entry) => entry.id === turn.id);
660
+ if (projectedTurn && isTerminal(projectedTurn.status))
661
+ this.store.finishTurn(threadId, turn.id, this.codex.epoch);
662
+ this.publishThread(threadId);
663
+ return { result: { threadId, turnId: turn.id }, turnId: turn.id };
664
+ });
665
+ }
666
+ async steer(threadId, input) {
667
+ return this.perform("turn.steer", threadId, input, async () => {
668
+ await this.requireActiveTurn(threadId, input.expectedTurnId);
669
+ this.ensureCompatibilityConfirmed();
670
+ this.store.dispatch(input.clientOperationId);
671
+ const result = await this.codex.steerTurn(
672
+ threadId,
673
+ input.expectedTurnId,
674
+ input.text,
675
+ input.images
676
+ );
677
+ return { result: { threadId, turnId: result.turnId } };
678
+ });
679
+ }
680
+ async interrupt(threadId, input) {
681
+ return this.perform("turn.interrupt", threadId, input, async () => {
682
+ await this.requireActiveTurn(threadId, input.expectedTurnId);
683
+ this.store.dispatch(input.clientOperationId);
684
+ await this.codex.interruptTurn(threadId, input.expectedTurnId);
685
+ return { result: { threadId, turnId: input.expectedTurnId } };
686
+ });
687
+ }
688
+ async resolveAction(actionId, input) {
689
+ return this.perform("action.resolve", actionId, input, async () => {
690
+ this.ensureReady();
691
+ let action = this.projection.actions.get(actionId);
692
+ if (!action || action.codexEpoch !== this.codex.epoch || action.status === "expired")
693
+ throw new AppError("ACTION_EXPIRED", "\u8BE5\u4EBA\u5DE5\u8BF7\u6C42\u5DF2\u5931\u6548", 409);
694
+ if (action.status !== "pending")
695
+ throw new AppError("ACTION_CONFLICT", "\u8BE5\u4EBA\u5DE5\u8BF7\u6C42\u5DF2\u7531\u53E6\u4E00\u6B21\u63D0\u4EA4\u5904\u7406", 409);
696
+ if (action.threadId) await this.loadThread(action.threadId);
697
+ action = this.projection.actions.get(actionId);
698
+ if (!action || action.status === "expired" || action.codexEpoch !== this.codex.epoch)
699
+ throw new AppError("ACTION_EXPIRED", "\u8BE5\u4EBA\u5DE5\u8BF7\u6C42\u5DF2\u5931\u6548", 409);
700
+ if (action.status !== "pending")
701
+ throw new AppError("ACTION_CONFLICT", "\u8BE5\u4EBA\u5DE5\u8BF7\u6C42\u5DF2\u88AB\u5904\u7406", 409);
702
+ this.validateResolution(action, input);
703
+ this.ensureCompatibilityConfirmed();
704
+ this.store.dispatch(input.clientOperationId);
705
+ this.projection.resolveAction(actionId, "responding");
706
+ if (action.threadId) this.publishThread(action.threadId);
707
+ await this.codex.respond(actionId, input.resolution);
708
+ return { result: { actionId } };
709
+ });
710
+ }
711
+ getOperation(id) {
712
+ const row = this.store.get(id);
713
+ if (!row) throw new AppError("OPERATION_NOT_FOUND", "\u672A\u627E\u5230\u8BE5\u64CD\u4F5C", 404);
714
+ return this.operation(row);
715
+ }
716
+ async reconcileOperation(targetOperationId, input) {
717
+ return this.perform("operation.reconcile", targetOperationId, input, async () => {
718
+ if (!input.confirmEnded)
719
+ throw new AppError("CONFIRMATION_REQUIRED", "\u5FC5\u987B\u786E\u8BA4\u5BF9\u5E94\u65E7\u8FD0\u884C\u5DF2\u7ECF\u7ED3\u675F", 400);
720
+ const target = this.store.get(targetOperationId);
721
+ if (!target) throw new AppError("OPERATION_NOT_FOUND", "\u5F85\u6838\u5BF9\u64CD\u4F5C\u4E0D\u5B58\u5728", 404);
722
+ if (this.inFlightOperations.has(targetOperationId))
723
+ throw new AppError("OPERATION_BUSY", "\u64CD\u4F5C\u4ECD\u5728\u5904\u7406\u4E2D\uFF0C\u8BF7\u7B49\u5F85\u8FD4\u56DE\u540E\u518D\u6838\u5BF9", 409);
724
+ const marker = this.store.marker(targetOperationId);
725
+ if (!marker || marker.endedAt !== null) {
726
+ this.store.dispatch(input.clientOperationId);
727
+ this.store.reconcile(targetOperationId, input.clientOperationId);
728
+ return { result: this.store.get(input.clientOperationId)?.result };
729
+ }
730
+ const cached = this.projection.get(marker.threadId);
731
+ if (marker.codexEpoch === this.codex.epoch && cached && this.markerIsActive(cached, marker.turnId))
732
+ throw new AppError("THREAD_BUSY", "\u53D7\u7BA1\u4EFB\u52A1\u4ECD\u5728\u6267\u884C\uFF0C\u8BF7\u5148\u6B63\u5E38\u4E2D\u65AD", 409, {
733
+ threadId: marker.threadId
734
+ });
735
+ if (this.codex.ready) {
736
+ try {
737
+ const actual = await this.codex.readThread(marker.threadId);
738
+ const link = this.store.getThreadLink(marker.threadId);
739
+ if (link) this.projects.assertOwnership(link.projectId, actual.cwd);
740
+ else this.projects.findOwner(actual.cwd);
741
+ if (this.markerIsActive(actual, marker.turnId))
742
+ throw new AppError("THREAD_BUSY", "\u4E0A\u6E38\u4ECD\u62A5\u544A\u8BE5\u4EFB\u52A1\u6D3B\u52A8\uFF0C\u4E0D\u80FD\u89E3\u9664\u5360\u7528", 409, {
743
+ threadId: marker.threadId
744
+ });
745
+ } catch (error) {
746
+ if (error instanceof AppError && ["THREAD_BUSY", "PROJECT_OWNERSHIP"].includes(error.code))
747
+ throw error;
748
+ }
749
+ }
750
+ const latest = this.projection.get(marker.threadId);
751
+ if (marker.codexEpoch === this.codex.epoch && latest && this.markerIsActive(latest, marker.turnId))
752
+ throw new AppError("THREAD_BUSY", "\u53D7\u7BA1\u4EFB\u52A1\u4ECD\u5728\u6267\u884C\uFF0C\u8BF7\u5148\u6B63\u5E38\u4E2D\u65AD", 409);
753
+ this.store.dispatch(input.clientOperationId);
754
+ this.store.reconcile(targetOperationId, input.clientOperationId);
755
+ const thread = this.projection.get(marker.threadId);
756
+ if (thread?.status === "unknown") {
757
+ thread.status = "idle";
758
+ this.publishThread(thread.id);
759
+ }
760
+ this.events.publish("operation", this.getOperation(targetOperationId), marker.threadId);
761
+ return { result: this.store.get(input.clientOperationId)?.result };
762
+ });
763
+ }
764
+ async subscribe(threadId, after, listener) {
765
+ await this.getThread(threadId);
766
+ return this.events.subscribe(
767
+ threadId,
768
+ () => this.threadView(this.projection.get(threadId)),
769
+ after,
770
+ listener
771
+ );
772
+ }
773
+ async shutdown() {
774
+ this.accepting = false;
775
+ this.store.recover();
776
+ this.projection.runtimeReset();
777
+ this.removeListener();
778
+ await this.codex.stop();
779
+ }
780
+ async perform(kind, target, input, work) {
781
+ if (!this.accepting) throw new AppError("CODEX_UNAVAILABLE", "\u670D\u52A1\u6B63\u5728\u5173\u95ED", 503);
782
+ if (this.maintenance)
783
+ throw new AppError("UPDATE_MAINTENANCE", "\u6B63\u5728\u5347\u7EA7\uFF0C\u8BF7\u7B49\u5F85\u670D\u52A1\u91CD\u65B0\u8FDE\u63A5", 503);
784
+ let accepted;
785
+ try {
786
+ accepted = this.store.accept(input.clientOperationId, kind, target, input);
787
+ } catch (error) {
788
+ throw this.asAppError(error);
789
+ }
790
+ if (!accepted.inserted) return this.operation(accepted.operation);
791
+ this.inFlightOperations.add(input.clientOperationId);
792
+ try {
793
+ const { result, turnId, onAcknowledged } = await work();
794
+ const operation = this.operation(
795
+ this.store.acknowledge(input.clientOperationId, result, turnId)
796
+ );
797
+ if (operation.status === "acknowledged") onAcknowledged?.();
798
+ this.events.publish("operation", operation);
799
+ return operation;
800
+ } catch (error) {
801
+ const appError = this.asAppError(error);
802
+ const state = this.store.get(input.clientOperationId)?.state;
803
+ const dispatched = state === "dispatching";
804
+ const uncertain = state === "unknown" || dispatched && (typeof error === "object" && error !== null && "uncertain" in error ? Boolean(error.uncertain) : !(error instanceof AppError));
805
+ const operation = this.operation(
806
+ this.store.fail(
807
+ input.clientOperationId,
808
+ { ...appError.toJSON(), code: uncertain ? "RESULT_UNKNOWN" : appError.code },
809
+ uncertain
810
+ )
811
+ );
812
+ if (dispatched && !uncertain && kind === "action.resolve" && this.projection.actions.get(target)?.status === "responding") {
813
+ const action = this.projection.resolveAction(target, "pending");
814
+ if (action?.threadId) this.publishThread(action.threadId);
815
+ }
816
+ if (dispatched && kind === "turn.start") this.publishThread(target);
817
+ this.events.publish("operation", operation);
818
+ if (uncertain)
819
+ throw new AppError(
820
+ "RESULT_UNKNOWN",
821
+ "\u8BF7\u6C42\u53EF\u80FD\u5DF2\u7ECF\u6267\u884C\uFF0C\u7ED3\u679C\u5F85\u6838\u5BF9\uFF1B\u8BF7\u52FF\u81EA\u52A8\u91CD\u65B0\u63D0\u4EA4",
822
+ 409,
823
+ { clientOperationId: input.clientOperationId }
824
+ );
825
+ throw appError;
826
+ } finally {
827
+ this.inFlightOperations.delete(input.clientOperationId);
828
+ this.pendingThreadCreations.delete(input.clientOperationId);
829
+ if (!this.store.activeForThread(target)) this.turnBaselines.delete(input.clientOperationId);
830
+ if (kind === "thread.compact") this.publishThread(target);
831
+ }
832
+ }
833
+ normalize(thread, projectId, managed = false) {
834
+ const link = this.store.getThreadLink(thread.id);
835
+ const ownership = link ? this.projects.assertOwnership(link.projectId, thread.cwd) : managed && projectId ? this.projects.assertOwnership(projectId, thread.cwd) : this.projects.findOwner(thread.cwd);
836
+ if (projectId && ownership.project.id !== projectId)
837
+ throw new AppError("PROJECT_OWNERSHIP", "\u4F1A\u8BDD\u5C5E\u4E8E\u5176\u4ED6\u9879\u76EE\uFF0C\u4E0D\u80FD\u6539\u53D8\u9879\u76EE\u5F52\u5C5E", 403);
838
+ const managedEpoch = thread.archived ? null : managed ? this.codex.epoch : link?.managedEpoch ?? null;
839
+ const result = {
840
+ ...thread,
841
+ turns: this.turnUsage.restore(thread.id, thread.turns),
842
+ projectId: ownership.project.id,
843
+ cwd: ownership.cwd,
844
+ managed: managedEpoch === this.codex.epoch,
845
+ resumeError: managed ? void 0 : this.projection.get(thread.id)?.resumeError,
846
+ actions: [...this.projection.actions.values()].filter(
847
+ (action) => action.threadId === thread.id
848
+ )
849
+ };
850
+ this.store.linkThread({
851
+ threadId: thread.id,
852
+ projectId: ownership.project.id,
853
+ cwd: ownership.cwd,
854
+ source: thread.source,
855
+ managedEpoch
856
+ });
857
+ this.applyRecoveredMarker(result);
858
+ return result;
859
+ }
860
+ applyRecoveredMarker(thread) {
861
+ const marker = this.store.activeForThread(thread.id);
862
+ if (marker?.state === "unknown") {
863
+ const confirmed = thread.turns.find((entry) => entry.id === marker.turnId);
864
+ if (confirmed && isTerminal(confirmed.status) && !isActive(thread.status) && !thread.turns.some((entry) => isActive(entry.status))) {
865
+ this.store.finishTurn(thread.id, confirmed.id, marker.codexEpoch);
866
+ return;
867
+ }
868
+ thread.status = "unknown";
869
+ const turn = thread.turns.find((entry) => entry.id === marker.turnId);
870
+ if (turn && !isTerminal(turn.status)) turn.status = "unknown";
871
+ }
872
+ }
873
+ /** Refresh only this thread; missing completion events must not block its next turn. */
874
+ async refreshExecution(marker) {
875
+ if (!marker.turnId || !this.codex.ready) return;
876
+ const existing = this.executionReads.get(marker.operationId);
877
+ if (existing) return existing;
878
+ const epoch = this.codex.epoch;
879
+ const revision = this.projection.revision(marker.threadId);
880
+ const reading = (async () => {
881
+ try {
882
+ const actual = await this.codex.readThread(marker.threadId);
883
+ const latest = this.store.activeForThread(marker.threadId);
884
+ if (!this.codex.ready || this.codex.epoch !== epoch || actual.id !== marker.threadId || latest?.operationId !== marker.operationId || latest.turnId !== marker.turnId || latest.codexEpoch !== marker.codexEpoch || this.projection.revision(marker.threadId) !== revision)
885
+ return;
886
+ const link = this.store.getThreadLink(marker.threadId);
887
+ const ownership = link ? this.projects.assertOwnership(link.projectId, actual.cwd) : this.projects.findOwner(actual.cwd);
888
+ if (ownership.workspaceKey !== marker.workspaceKey) return;
889
+ const turn = actual.turns.find((entry) => entry.id === marker.turnId);
890
+ if (turn && isActive(turn.status) && latest.state === "active" && this.projection.get(marker.threadId)?.status === "unknown") {
891
+ this.projection.thread(this.normalize(actual));
892
+ this.publishThread(marker.threadId);
893
+ return;
894
+ }
895
+ if (!turn || !isTerminal(turn.status) || isActive(actual.status) || actual.turns.some((entry) => isActive(entry.status)))
896
+ return;
897
+ this.store.finishTurn(marker.threadId, turn.id, marker.codexEpoch);
898
+ this.projection.thread(this.normalize(actual));
899
+ this.projection.expireTurn(marker.threadId, turn.id);
900
+ this.publishThread(marker.threadId);
901
+ this.events.publish("operation", this.getOperation(marker.operationId), marker.threadId);
902
+ } catch {
903
+ }
904
+ })();
905
+ this.executionReads.set(marker.operationId, reading);
906
+ try {
907
+ return await reading;
908
+ } finally {
909
+ this.executionReads.delete(marker.operationId);
910
+ }
911
+ }
912
+ ensureReady() {
913
+ if (!this.codex.ready)
914
+ throw new AppError("CODEX_UNAVAILABLE", "Codex \u5C1A\u672A\u5C31\u7EEA\uFF0C\u8BF7\u67E5\u770B\u8FD0\u884C\u72B6\u6001", 503);
915
+ }
916
+ compatibilityFingerprint(diagnostics) {
917
+ return JSON.stringify([
918
+ diagnostics.codexEpoch,
919
+ diagnostics.codexVersion,
920
+ diagnostics.verifiedVersion
921
+ ]);
922
+ }
923
+ compatibilityConfirmation(diagnostics) {
924
+ const fingerprint = this.compatibilityFingerprint(diagnostics);
925
+ if (!this.codex.ready || !diagnostics.ready || diagnostics.compatibility !== "unverified" || !diagnostics.codexVersion || this.confirmedCompatibility !== fingerprint)
926
+ this.confirmedCompatibility = void 0;
927
+ if (diagnostics.compatibility !== "unverified") return "not-required";
928
+ return this.confirmedCompatibility === fingerprint ? "confirmed" : "required";
929
+ }
930
+ ensureCompatibilityAvailable(diagnostics) {
931
+ this.compatibilityConfirmation(diagnostics);
932
+ this.ensureReady();
933
+ if (!diagnostics.ready || diagnostics.compatibility === "unavailable" || !diagnostics.codexVersion)
934
+ throw new AppError("CODEX_UNAVAILABLE", "Codex \u4E0D\u53EF\u7528\u6216\u65E0\u6CD5\u8BC6\u522B\u8FD0\u884C\u7248\u672C\uFF0C\u8BF7\u5148\u68C0\u67E5\u8BCA\u65AD", 503);
935
+ }
936
+ ensureCompatibilityConfirmed() {
937
+ const diagnostics = this.codex.diagnostics();
938
+ this.ensureCompatibilityAvailable(diagnostics);
939
+ if (this.compatibilityConfirmation(diagnostics) === "required")
940
+ throw new AppError(
941
+ "COMPATIBILITY_CONFIRMATION_REQUIRED",
942
+ "\u5F53\u524D Codex \u7248\u672C\u5C1A\u672A\u9A8C\u8BC1\uFF0C\u8BF7\u5148\u786E\u8BA4\u7EE7\u7EED\u4F7F\u7528\u6B64\u7248\u672C",
943
+ 409
944
+ );
945
+ }
946
+ ensureManaged(thread) {
947
+ this.ensureReady();
948
+ this.ensureArchiveAvailable(thread);
949
+ if (thread.archived) throw new AppError("THREAD_ARCHIVED", "\u8BF7\u5148\u53D6\u6D88\u5F52\u6863\u540E\u518D\u7EE7\u7EED\u5BF9\u8BDD", 409);
950
+ if (!thread.managed || this.store.getThreadLink(thread.id)?.managedEpoch !== this.codex.epoch)
951
+ throw new AppError("THREAD_NOT_RESUMED", "\u4F1A\u8BDD\u5C1A\u672A\u6062\u590D\uFF0C\u8BF7\u91CD\u8BD5\u6062\u590D\u540E\u7EE7\u7EED", 409);
952
+ }
953
+ async requireActiveTurn(threadId, expectedTurnId) {
954
+ const thread = await this.loadThread(threadId);
955
+ this.ensureManaged(thread);
956
+ const turn = thread.turns.find((entry) => entry.id === expectedTurnId);
957
+ const marker = this.store.activeForThread(threadId);
958
+ if (!turn || !isActive(turn.status) || !marker || marker.turnId !== expectedTurnId || marker.codexEpoch !== this.codex.epoch)
959
+ throw new AppError("TURN_CONFLICT", "\u76EE\u6807\u4EFB\u52A1\u5DF2\u6539\u53D8\u6216\u4E0D\u518D\u6D3B\u52A8\uFF0C\u8BF7\u5237\u65B0\u72B6\u6001", 409);
960
+ }
961
+ markerIsActive(thread, turnId) {
962
+ if (turnId) return thread.turns.some((turn) => turn.id === turnId && isActive(turn.status));
963
+ return isActive(thread.status) || thread.turns.some((turn) => isActive(turn.status));
964
+ }
965
+ validateResolution(action, input) {
966
+ const resolution = input.resolution;
967
+ if (action.kind === "permissions") {
968
+ validatePermissionResolution(resolution);
969
+ } else if (action.kind === "elicitation") {
970
+ validateElicitationResolution(action.elicitation, resolution);
971
+ } else if (action.kind === "questions") {
972
+ if (!("answers" in resolution)) throw new AppError("VALIDATION_ERROR", "\u8BF7\u6309\u95EE\u9898\u63D0\u4EA4\u7B54\u6848");
973
+ const questions = action.questions ?? [];
974
+ if (Object.keys(resolution.answers).some(
975
+ (id) => !questions.some((question) => question.id === id)
976
+ ))
977
+ throw new AppError("VALIDATION_ERROR", "\u56DE\u7B54\u5305\u542B\u672A\u77E5\u95EE\u9898");
978
+ for (const question of questions) {
979
+ const answers = resolution.answers[question.id];
980
+ if (!answers?.length || answers.some(
981
+ (answer) => !answer.trim() || !question.isOther && question.options.length > 0 && !question.options.some((option) => option.label === answer)
982
+ ))
983
+ throw new AppError("VALIDATION_ERROR", `\u95EE\u9898 ${question.header} \u7684\u56DE\u7B54\u4E0D\u7B26\u5408\u9009\u9879\u8981\u6C42`);
984
+ }
985
+ } else if (!("decision" in resolution) || "content" in resolution || "persist" in resolution || !action.options.some((option) => option.id === resolution.decision))
986
+ throw new AppError("VALIDATION_ERROR", "\u8BF7\u9009\u62E9\u4E0A\u6E38\u63D0\u4F9B\u7684\u5408\u6CD5\u9009\u9879");
987
+ }
988
+ operation(row) {
989
+ const marker = this.store.marker(row.clientOperationId);
990
+ return {
991
+ clientOperationId: row.clientOperationId,
992
+ kind: row.kind,
993
+ target: row.target,
994
+ status: row.state,
995
+ createdAt: row.createdAt,
996
+ updatedAt: row.updatedAt,
997
+ ...row.result === null ? {} : { result: row.result },
998
+ ...row.error === null ? {} : { error: { ...row.error, clientOperationId: row.clientOperationId } },
999
+ ...marker ? {
1000
+ execution: {
1001
+ operationId: marker.operationId,
1002
+ threadId: marker.threadId,
1003
+ ...marker.turnId === null ? {} : { turnId: marker.turnId },
1004
+ workspaceKey: marker.workspaceKey,
1005
+ codexEpoch: marker.codexEpoch,
1006
+ ended: marker.endedAt !== null
1007
+ }
1008
+ } : {},
1009
+ ...row.reconciledAt ? {
1010
+ reconciliation: {
1011
+ at: row.reconciledAt,
1012
+ decision: row.reconciliationDecision ?? "confirmed-ended"
1013
+ }
1014
+ } : {}
1015
+ };
1016
+ }
1017
+ asAppError(error) {
1018
+ if (error instanceof AppError) return error;
1019
+ if (error instanceof StoreConflict)
1020
+ return new AppError(
1021
+ error.code,
1022
+ error.message,
1023
+ 409,
1024
+ error.threadId ? { threadId: error.threadId } : void 0
1025
+ );
1026
+ return new AppError("INTERNAL_ERROR", error instanceof Error ? error.message : "\u672A\u77E5\u9519\u8BEF", 500);
1027
+ }
1028
+ publishThread(threadId) {
1029
+ const thread = this.projection.get(threadId);
1030
+ if (!thread) return;
1031
+ const view = this.threadView(thread);
1032
+ this.events.publish("thread", view, threadId);
1033
+ const summary = toThreadSummary(view);
1034
+ const encoded = JSON.stringify(summary);
1035
+ if (this.summaries.get(threadId) === encoded) return;
1036
+ try {
1037
+ this.projects.assertOwnership(thread.projectId, thread.cwd);
1038
+ } catch {
1039
+ return;
1040
+ }
1041
+ this.summaries.set(threadId, encoded);
1042
+ this.events.publishSummary(summary);
1043
+ }
1044
+ historyVisible(thread) {
1045
+ return Boolean(
1046
+ thread.hasMessages || thread.archived || thread.turns.length || this.store.activeForThread(thread.id) || isActive(thread.status) || thread.actions.some((action) => ["pending", "responding"].includes(action.status))
1047
+ );
1048
+ }
1049
+ threadView(thread) {
1050
+ const result = structuredClone(thread);
1051
+ result.archived = thread.archived ?? false;
1052
+ result.historyVisible = this.historyVisible(thread);
1053
+ const marker = this.store.activeForThread(thread.id);
1054
+ delete result.execution;
1055
+ if (marker)
1056
+ result.execution = {
1057
+ operationId: marker.operationId,
1058
+ threadId: marker.threadId,
1059
+ ...marker.turnId === null ? {} : { turnId: marker.turnId },
1060
+ workspaceKey: marker.workspaceKey,
1061
+ codexEpoch: marker.codexEpoch,
1062
+ ended: false
1063
+ };
1064
+ return result;
1065
+ }
1066
+ beginTitleRead(threadId) {
1067
+ const sequence = ++this.titleReadSequence;
1068
+ this.titleReads.set(threadId, sequence);
1069
+ return sequence;
1070
+ }
1071
+ updateThreadTitle(threadId, title, sequence) {
1072
+ const thread = this.projection.get(threadId);
1073
+ if (!thread || this.titleReads.get(threadId) !== sequence || thread.title === title) return;
1074
+ thread.title = title;
1075
+ this.projection.touch(threadId);
1076
+ this.publishThread(threadId);
1077
+ }
1078
+ async refreshThreadMetadata(threadId) {
1079
+ const sequence = this.beginTitleRead(threadId);
1080
+ const revision = this.projection.revision(threadId);
1081
+ try {
1082
+ const thread = await this.codex.readThreadMetadata(threadId);
1083
+ const current = this.projection.get(threadId);
1084
+ if (!current || thread.id !== threadId) return;
1085
+ this.projects.assertOwnership(current.projectId, thread.cwd);
1086
+ const settings = thread.runtimeSettings ? { ...current.runtimeSettings, ...thread.runtimeSettings } : void 0;
1087
+ if (this.titleReads.get(threadId) === sequence && this.projection.revision(threadId) === revision && settings && JSON.stringify(current.runtimeSettings) !== JSON.stringify(settings)) {
1088
+ current.runtimeSettings = settings;
1089
+ this.projection.touch(threadId);
1090
+ this.publishThread(threadId);
1091
+ }
1092
+ if (this.titleReads.get(threadId) === sequence)
1093
+ this.updateArchiveState(threadId, Boolean(thread.archived));
1094
+ this.updateThreadTitle(threadId, thread.title, sequence);
1095
+ } catch {
1096
+ }
1097
+ }
1098
+ consume(event) {
1099
+ if (event.type === "runtime") {
1100
+ this.compatibilityConfirmation(event.diagnostics);
1101
+ if (!event.diagnostics.ready || event.diagnostics.codexEpoch !== this.currentEpoch) {
1102
+ this.currentEpoch = event.diagnostics.codexEpoch;
1103
+ this.titleReads.clear();
1104
+ this.runtimeSettingsUpdates.clear();
1105
+ this.turnBaselines.clear();
1106
+ this.resumes.clear();
1107
+ this.store.recover();
1108
+ this.projection.runtimeReset();
1109
+ for (const thread2 of this.projection.values()) this.publishThread(thread2.id);
1110
+ }
1111
+ this.events.publish("runtime", this.getSystem());
1112
+ return;
1113
+ }
1114
+ if (event.type === "thread") {
1115
+ try {
1116
+ if (!this.store.getThreadLink(event.thread.id) && [...this.pendingThreadCreations.values()].includes(realpathSync(event.thread.cwd)))
1117
+ return;
1118
+ this.projection.thread(this.normalize(event.thread), true);
1119
+ this.publishThread(event.thread.id);
1120
+ } catch {
1121
+ }
1122
+ return;
1123
+ }
1124
+ if (event.type === "threadArchive") {
1125
+ this.beginTitleRead(event.threadId);
1126
+ this.updateArchiveState(event.threadId, event.archived);
1127
+ return;
1128
+ }
1129
+ if (event.type === "action") {
1130
+ const action = event.action;
1131
+ if (action.codexEpoch !== this.codex.epoch) return;
1132
+ this.projection.action(action);
1133
+ if (action.threadId) this.publishThread(action.threadId);
1134
+ return;
1135
+ }
1136
+ if (event.type === "actionResolved") {
1137
+ const action = this.projection.resolveAction(event.actionId, "responded");
1138
+ if (action?.threadId) this.publishThread(action.threadId);
1139
+ return;
1140
+ }
1141
+ if (event.type === "autoReview") {
1142
+ if (this.projection.autoReview(event.threadId, event.turnId, event.review))
1143
+ this.publishThread(event.threadId);
1144
+ return;
1145
+ }
1146
+ const thread = this.projection.get(event.threadId);
1147
+ if (!thread) return;
1148
+ if (event.type === "compactionCompleted") {
1149
+ if (this.projection.compactionCompleted(thread.id, event.turnId))
1150
+ this.publishThread(thread.id);
1151
+ return;
1152
+ }
1153
+ if (event.type === "turnError" || event.type === "turnProgress") {
1154
+ const changed = event.type === "turnError" ? this.projection.turnError(thread.id, event.turnId, event.message, event.willRetry) : this.projection.turnProgress(thread.id, event.turnId);
1155
+ if (changed) this.publishThread(thread.id);
1156
+ return;
1157
+ }
1158
+ if (event.type === "threadName") {
1159
+ this.projection.touch(thread.id);
1160
+ if (event.name) this.updateThreadTitle(thread.id, event.name, this.beginTitleRead(thread.id));
1161
+ else void this.refreshThreadMetadata(thread.id);
1162
+ return;
1163
+ }
1164
+ if (event.type === "threadSettings") {
1165
+ this.runtimeSettingsUpdates.set(thread.id, ++this.runtimeSettingsSequence);
1166
+ thread.runtimeSettings = event.settings;
1167
+ this.projection.touch(thread.id);
1168
+ } else if (event.type === "threadStatus") {
1169
+ if (!isActive(event.status)) this.projection.endThreadCompactions(thread.id);
1170
+ if (this.store.activeForThread(thread.id)?.state !== "unknown") thread.status = event.status;
1171
+ this.projection.touch(thread.id);
1172
+ } else if (event.type === "turn") {
1173
+ const previous = thread.turns.find((turn) => turn.id === event.turn.id);
1174
+ const newCompletion = !previous || !isTerminal(previous.status);
1175
+ if (isTerminal(event.turn.status)) this.turnUsage.save(thread.id, event.turn);
1176
+ this.projection.turn(thread.id, event.turn);
1177
+ void this.refreshThreadMetadata(thread.id);
1178
+ const marker = this.store.activeForThread(thread.id);
1179
+ if (marker?.codexEpoch === this.codex.epoch && marker.turnId === null && // A cancellation event from an existing turn cannot identify newly
1180
+ // dispatched work, even when its first notification/receipt is delayed.
1181
+ this.turnBaselines.get(marker.operationId)?.has(event.turn.id) === false) {
1182
+ this.store.attachTurn(marker.operationId, event.turn.id);
1183
+ this.turnBaselines.delete(marker.operationId);
1184
+ }
1185
+ if (isTerminal(event.turn.status)) {
1186
+ this.store.finishTurn(thread.id, event.turn.id, this.codex.epoch);
1187
+ this.projection.expireTurn(thread.id, event.turn.id);
1188
+ if (newCompletion) {
1189
+ try {
1190
+ this.projects.assertOwnership(thread.projectId, thread.cwd);
1191
+ this.events.publishCompletion({
1192
+ threadId: thread.id,
1193
+ turnId: event.turn.id,
1194
+ projectId: thread.projectId,
1195
+ title: thread.title,
1196
+ status: event.turn.status
1197
+ });
1198
+ } catch {
1199
+ }
1200
+ }
1201
+ }
1202
+ } else if (event.type === "item") this.projection.item(thread.id, event.turnId, event.item);
1203
+ else if (event.type === "delta")
1204
+ this.projection.delta(
1205
+ thread.id,
1206
+ event.turnId,
1207
+ event.itemId,
1208
+ event.field,
1209
+ event.delta,
1210
+ event.itemType
1211
+ );
1212
+ else if (event.type === "diff") {
1213
+ this.projection.diff(thread.id, event.turnId, event.diff);
1214
+ }
1215
+ this.publishThread(thread.id);
1216
+ }
1217
+ }
1218
+ export {
1219
+ GatewayService
1220
+ };
1221
+ //# sourceMappingURL=service.js.map