@covro/covro 0.2.0-beta.1 → 0.2.1-beta.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 (144) hide show
  1. package/README.md +30 -11
  2. package/README.zh-CN.md +30 -11
  3. package/dist/cli/background.js +103 -0
  4. package/dist/cli/background.js.map +7 -0
  5. package/dist/cli/daemon.js +6 -1
  6. package/dist/cli/daemon.js.map +2 -2
  7. package/dist/cli/doctor.js +92 -0
  8. package/dist/cli/doctor.js.map +7 -0
  9. package/dist/cli/launch-environment.js +85 -0
  10. package/dist/cli/launch-environment.js.map +7 -0
  11. package/dist/cli/launchd.js +172 -0
  12. package/dist/cli/launchd.js.map +7 -0
  13. package/dist/cli/main.js +7 -35
  14. package/dist/cli/main.js.map +2 -2
  15. package/dist/cli/service.js +23 -49
  16. package/dist/cli/service.js.map +2 -2
  17. package/dist/codex/adapter.js +76 -2
  18. package/dist/codex/adapter.js.map +2 -2
  19. package/dist/drizzle/0005_session_workspaces.sql +24 -0
  20. package/dist/drizzle/meta/0005_snapshot.json +480 -0
  21. package/dist/drizzle/meta/_journal.json +7 -0
  22. package/dist/server/auth.js +1 -1
  23. package/dist/server/auth.js.map +2 -2
  24. package/dist/server/db/operations.js +14 -1
  25. package/dist/server/db/operations.js.map +2 -2
  26. package/dist/server/db/schema.js +9 -1
  27. package/dist/server/db/schema.js.map +2 -2
  28. package/dist/server/http.js +46 -0
  29. package/dist/server/http.js.map +2 -2
  30. package/dist/server/local-files.js +2 -2
  31. package/dist/server/local-files.js.map +2 -2
  32. package/dist/server/main.js +1 -0
  33. package/dist/server/main.js.map +2 -2
  34. package/dist/server/service.js +111 -51
  35. package/dist/server/service.js.map +2 -2
  36. package/dist/server/workspaces.js +138 -0
  37. package/dist/server/workspaces.js.map +7 -0
  38. package/dist/shared/workspace-scope.js +9 -0
  39. package/dist/shared/workspace-scope.js.map +7 -0
  40. package/dist/update-worker.cjs +419 -168
  41. package/dist/updates/worker.js +7 -61
  42. package/dist/updates/worker.js.map +2 -2
  43. package/dist/web/assets/abnfDiagram-WEYOIA3C-B3om3w2y.js +1 -0
  44. package/dist/web/assets/arc-hdxlL3Om.js +1 -0
  45. package/dist/web/assets/architecture-WOLXFQ4H-FZOiLQD-.js +1 -0
  46. package/dist/web/assets/architectureDiagram-ZHVYKT53-DjatMTKS.js +36 -0
  47. package/dist/web/assets/array-BifhSqXX.js +1 -0
  48. package/dist/web/assets/blockDiagram-RJAE7QA3-Bu6LTv0p.js +140 -0
  49. package/dist/web/assets/c4Diagram-XDPPPE54-BGFHYP5H.js +38 -0
  50. package/dist/web/assets/channel-DmN_FTbE.js +1 -0
  51. package/dist/web/assets/chunk-2E7WHVMD-k8528aQZ.js +88 -0
  52. package/dist/web/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
  53. package/dist/web/assets/chunk-4EMNKR2T-CCkntZuJ.js +1 -0
  54. package/dist/web/assets/chunk-55XCU76K-Br-Kelsg.js +1 -0
  55. package/dist/web/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
  56. package/dist/web/assets/chunk-7S6XSTFO-C3W_8ji8.js +2 -0
  57. package/dist/web/assets/chunk-AQDTSKZB-DZQV76Iu.js +72 -0
  58. package/dist/web/assets/chunk-DSNLSHJZ-BRfUxqaq.js +129 -0
  59. package/dist/web/assets/chunk-DUW6YSOI-Dgx8z5s3.js +1 -0
  60. package/dist/web/assets/chunk-HAGFOTED-BD5jLT7f.js +62 -0
  61. package/dist/web/assets/chunk-HNE4Q36P-iJGu5Gi6.js +1 -0
  62. package/dist/web/assets/chunk-J5ZVWO5B-oDCd-gWO.js +1 -0
  63. package/dist/web/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
  64. package/dist/web/assets/chunk-NGNAAXSQ-C2Wy9Xmz.js +136 -0
  65. package/dist/web/assets/chunk-NQOKRVIF-D-yS8zxU.js +213 -0
  66. package/dist/web/assets/chunk-UHW7PFBP-Dnu0oPmA.js +1 -0
  67. package/dist/web/assets/chunk-VQ5W62QI-CzZ8zHjp.js +1 -0
  68. package/dist/web/assets/chunk-WQQL4CDK-Cu4nebs6.js +10 -0
  69. package/dist/web/assets/chunk-XXDRQBXY-DgtjWyl3.js +1 -0
  70. package/dist/web/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
  71. package/dist/web/assets/chunk-YZO67HFG-CGApWL5X.js +1 -0
  72. package/dist/web/assets/chunk-ZAUX2LTX-D5DhZJoE.js +1 -0
  73. package/dist/web/assets/chunk-ZCHMQSOR-BC0ITpUM.js +2 -0
  74. package/dist/web/assets/classDiagram-v2-4MYWR3UA-Budj7a3U.js +217 -0
  75. package/dist/web/assets/cose-bilkent-JH36ORCC-DIHNUNHx.js +1 -0
  76. package/dist/web/assets/cynefin-EF2NZ3EQ-BRluapvc.js +1 -0
  77. package/dist/web/assets/cynefinDiagram-WF24UDRP-CATBOOnO.js +62 -0
  78. package/dist/web/assets/cytoscape.esm-BegBNzOV.js +321 -0
  79. package/dist/web/assets/dagre-NK4GIZVX-B8DaH0ao.js +4 -0
  80. package/dist/web/assets/defaultLocale-BFoDCU3G.js +1 -0
  81. package/dist/web/assets/diagram-CBK3MWTC-C6HHNKIg.js +24 -0
  82. package/dist/web/assets/diagram-I5XIIV2D-BftOP4e2.js +24 -0
  83. package/dist/web/assets/diagram-ODIL2UCW-CCO6wwvD.js +30 -0
  84. package/dist/web/assets/diagram-PFBEJUXE-y6w_y2UD.js +3 -0
  85. package/dist/web/assets/diagram-RJA3I4VV-gsgGHcdI.js +41 -0
  86. package/dist/web/assets/diagram-TYUNFSNR-BFuCHkk0.js +200 -0
  87. package/dist/web/assets/dist-DLTSHXmk.js +1 -0
  88. package/dist/web/assets/ebnfDiagram-V4P3TTPT-B6_sRdDs.js +1 -0
  89. package/dist/web/assets/elk-QONTBCTQ-BtVt8qRm.js +27 -0
  90. package/dist/web/assets/erDiagram-KYQIGQAA-rgfheTvQ.js +99 -0
  91. package/dist/web/assets/eventmodeling-K75KTNOO-r9vnCD2d.js +1 -0
  92. package/dist/web/assets/flowDiagram-IUGLLWBU-CiGVEt1a.js +1 -0
  93. package/dist/web/assets/ganttDiagram-R7GPNTJY-CJFYABWz.js +292 -0
  94. package/dist/web/assets/gitGraph-VSP46ZUC-ccOuonLa.js +1 -0
  95. package/dist/web/assets/gitGraphDiagram-PDYL7XHY-e5kzwjY2.js +106 -0
  96. package/dist/web/assets/graphlib-DS17s2tU.js +1 -0
  97. package/dist/web/assets/index-B4RJQRiD.js +142 -0
  98. package/dist/web/assets/index-DaMLk_sl.css +1 -0
  99. package/dist/web/assets/info-OHQRW6UA-C8aOXyva.js +1 -0
  100. package/dist/web/assets/infoDiagram-MVDRKAX5-ztgQX_ha.js +2 -0
  101. package/dist/web/assets/init-C-OQMol4.js +1 -0
  102. package/dist/web/assets/ishikawaDiagram-HPEHV2GE-BPCGAT_m.js +70 -0
  103. package/dist/web/assets/journeyDiagram-BOSHW7O7-BoiZL6al.js +139 -0
  104. package/dist/web/assets/kanban-definition-FNSA4HC5-BDcWGn8E.js +89 -0
  105. package/dist/web/assets/katex-B_JcIwqj.js +257 -0
  106. package/dist/web/assets/line-nGnhrXrD.js +1 -0
  107. package/dist/web/assets/linear-DiAwgAUj.js +1 -0
  108. package/dist/web/assets/mermaid-parser.core-DAzSIS8w.js +7 -0
  109. package/dist/web/assets/mermaid.core-CL_7HT-R.js +44 -0
  110. package/dist/web/assets/mindmap-definition-6OY57UG3-Bp7IBNG5.js +98 -0
  111. package/dist/web/assets/ordinal-BDEzSJ7C.js +1 -0
  112. package/dist/web/assets/packet-JDAUHWVQ-C-1SMzOZ.js +1 -0
  113. package/dist/web/assets/path-fybaL0A-.js +1 -0
  114. package/dist/web/assets/pegDiagram-ECIUJNBX-Be1uqoNt.js +1 -0
  115. package/dist/web/assets/pie-XZMESJXO-Dq5h06i3.js +1 -0
  116. package/dist/web/assets/pieDiagram-GNMPY74T-CN5ZGBXZ.js +39 -0
  117. package/dist/web/assets/quadrantDiagram-D57K4MC3-C7sLKW4U.js +7 -0
  118. package/dist/web/assets/radar-ABXABTNO-DVVIp0T9.js +1 -0
  119. package/dist/web/assets/railroad-I3PHUGI6-CxxBEQri.js +1 -0
  120. package/dist/web/assets/railroad-abnf-LNEFI6M7-CM7hMwVK.js +1 -0
  121. package/dist/web/assets/railroad-ebnf-DWYD2UWJ-KUafccMV.js +1 -0
  122. package/dist/web/assets/railroad-peg-JR7BVNR7-5BmypBHR.js +1 -0
  123. package/dist/web/assets/railroadDiagram-Y72NRBL7-DHAJk4_o.js +1 -0
  124. package/dist/web/assets/requirementDiagram-EQYSW5RA-y9zEf_QB.js +84 -0
  125. package/dist/web/assets/rough.esm-Dy-Kn_BL.js +1 -0
  126. package/dist/web/assets/sankeyDiagram-SY24JHO5-C6pWLy-I.js +40 -0
  127. package/dist/web/assets/sequenceDiagram-BLEHNHQU-Cj-Vitmg.js +169 -0
  128. package/dist/web/assets/sizeCapture-INFHLROL-3uHXrFqA.js +1 -0
  129. package/dist/web/assets/src-DbSGPN_8.js +1 -0
  130. package/dist/web/assets/stateDiagram-v2-A6IESKWZ-RbKYMXiF.js +291 -0
  131. package/dist/web/assets/swimlanes-B3ENRKFQ-7bBxQYlO.js +1 -0
  132. package/dist/web/assets/swimlanesDiagram-AYQRV4AK-CfAECQ56.js +8 -0
  133. package/dist/web/assets/timeline-definition-JXYKGLGB-5mrf5Irf.js +120 -0
  134. package/dist/web/assets/treeView-D4JQ5SDB-DP63r0Rt.js +1 -0
  135. package/dist/web/assets/treemap-SAJKECNS-ChWSB650.js +1 -0
  136. package/dist/web/assets/usecaseDiagram-WPOLBNNX-_OObMcZu.js +328 -0
  137. package/dist/web/assets/vennDiagram-Q3Y3IDCW-DO1Tmzxv.js +34 -0
  138. package/dist/web/assets/wardley-7MLQ67FV-DcDqvVeS.js +1 -0
  139. package/dist/web/assets/wardleyDiagram-IVAIDKWA-i-x65JPs.js +78 -0
  140. package/dist/web/assets/xychartDiagram-4URNHDU4-Dlk1JIjO.js +7 -0
  141. package/dist/web/index.html +2 -2
  142. package/package.json +6 -3
  143. package/dist/web/assets/index-Dr0Qkixk.css +0 -1
  144. package/dist/web/assets/index-z5RGzL8u.js +0 -138
@@ -1,5 +1,8 @@
1
1
  import { ShellService } from "./shell.js";
2
2
  import { realpathSync } from "node:fs";
3
+ import { cp } from "node:fs/promises";
4
+ import { WorkspaceRegistry } from "./workspaces.js";
5
+ import { UNASSIGNED_SCOPE } from "../shared/workspace-scope.js";
3
6
  import { AppError } from "../shared/errors.js";
4
7
  import { threadNameError } from "../shared/thread-name.js";
5
8
  import {
@@ -24,6 +27,11 @@ class GatewayService {
24
27
  this.store = options.store;
25
28
  this.turnUsage = new TurnUsageStore(this.store.database);
26
29
  this.projects = options.projects;
30
+ this.workspaces = new WorkspaceRegistry(
31
+ this.store.database,
32
+ this.projects,
33
+ options.workspacesRoot
34
+ );
27
35
  this.codex = options.codex;
28
36
  this.shell = new ShellService(this.codex);
29
37
  this.events = new EventHub(options.limits?.eventBytes);
@@ -39,6 +47,7 @@ class GatewayService {
39
47
  store;
40
48
  turnUsage;
41
49
  projects;
50
+ workspaces;
42
51
  codex;
43
52
  removeListener;
44
53
  fileTemporaryRoots = temporaryFileRoots();
@@ -47,6 +56,7 @@ class GatewayService {
47
56
  currentEpoch;
48
57
  confirmedCompatibility;
49
58
  archiveChanges = /* @__PURE__ */ new Set();
59
+ archiveRevisions = /* @__PURE__ */ new Map();
50
60
  turnBaselines = /* @__PURE__ */ new Map();
51
61
  renameChanges = /* @__PURE__ */ new Set();
52
62
  reads = /* @__PURE__ */ new Map();
@@ -99,6 +109,7 @@ class GatewayService {
99
109
  return { ready: true };
100
110
  }
101
111
  addProject(input) {
112
+ this.workspaces.assertProjectDirectory(input.path);
102
113
  const project = this.projects.add(input);
103
114
  this.events.publishProjects(this.projects.list());
104
115
  return project;
@@ -177,12 +188,12 @@ class GatewayService {
177
188
  });
178
189
  }
179
190
  async readRuntimeSettings(projectId) {
180
- const project = this.projects.get(projectId);
181
- return this.codex.readRuntimeSettings(project.canonicalRoot);
191
+ const cwd = projectId === null ? this.workspaces.defaultsDirectory() : this.projects.get(projectId).canonicalRoot;
192
+ return this.codex.readRuntimeSettings(cwd);
182
193
  }
183
194
  async saveModelDefaults(projectId, input) {
184
- return this.perform("config.modelDefaults", projectId, input, async () => {
185
- this.projects.get(projectId);
195
+ return this.perform("config.modelDefaults", projectId ?? UNASSIGNED_SCOPE, input, async () => {
196
+ if (projectId !== null) this.projects.get(projectId);
186
197
  this.ensureReady();
187
198
  this.ensureCompatibilityConfirmed();
188
199
  this.store.associateProject(input.clientOperationId, projectId);
@@ -191,28 +202,43 @@ class GatewayService {
191
202
  });
192
203
  }
193
204
  async listThreads(projectId, cursor, archived = false) {
194
- const project = this.projects.get(projectId);
205
+ const project = projectId === null ? void 0 : this.projects.get(projectId);
206
+ const directories = project ? project.canonicalRoot : this.workspaces.allocations().map((row) => row.canonicalRoot);
195
207
  const titleReads = new Map(
196
208
  this.projection.values().filter((thread) => thread.projectId === projectId).map((thread) => [thread.id, this.beginTitleRead(thread.id)])
197
209
  );
198
- const page = await this.codex.listThreads(project.canonicalRoot, cursor, archived);
199
- this.projects.get(projectId);
210
+ const archiveReads = new Map(
211
+ this.projection.values().filter((thread) => thread.projectId === projectId).map((thread) => [thread.id, this.archiveRevisions.get(thread.id) ?? 0])
212
+ );
213
+ const page = Array.isArray(directories) && !directories.length ? { data: [], nextCursor: null } : await this.codex.listThreads(directories, cursor, archived);
214
+ if (projectId !== null) this.projects.get(projectId);
200
215
  const data = [];
201
216
  for (const thread of page.data) {
202
217
  try {
218
+ if (projectId === null && thread.source === "appServer" && page.data.filter((entry) => entry.cwd === thread.cwd).length === 1) {
219
+ const operationId = this.workspaces.recoverCreation(thread.id, thread.cwd);
220
+ if (operationId && !this.inFlightOperations.has(operationId)) {
221
+ const recovered = this.store.confirmWorkspaceCreation(operationId, thread.id);
222
+ this.events.publish("operation", this.operation(recovered));
223
+ }
224
+ }
203
225
  const normalized = this.normalize(thread, projectId);
204
226
  normalized.hasMessages ??= true;
205
227
  const cached = this.projection.get(thread.id);
206
228
  if (cached && normalized.hasMessages) cached.hasMessages = true;
207
229
  const titleRead = titleReads.get(thread.id);
208
- if (cached && titleRead !== void 0) {
209
- if (this.titleReads.get(thread.id) === titleRead)
210
- this.updateArchiveState(thread.id, Boolean(normalized.archived));
230
+ if (cached && archiveReads.get(thread.id) === (this.archiveRevisions.get(thread.id) ?? 0))
231
+ this.updateArchiveState(thread.id, Boolean(normalized.archived));
232
+ if (cached && titleRead !== void 0)
211
233
  this.updateThreadTitle(thread.id, normalized.title, titleRead);
212
- }
213
234
  data.push(this.threadView(cached ?? normalized));
214
235
  } catch (error) {
215
- if (!(error instanceof AppError) || !["PROJECT_OWNERSHIP", "PROJECT_UNAVAILABLE", "PROJECT_NOT_FOUND"].includes(error.code))
236
+ if (!(error instanceof AppError) || ![
237
+ "PROJECT_OWNERSHIP",
238
+ "PROJECT_UNAVAILABLE",
239
+ "PROJECT_NOT_FOUND",
240
+ "WORKSPACE_UNAVAILABLE"
241
+ ].includes(error.code))
216
242
  throw error;
217
243
  }
218
244
  }
@@ -239,7 +265,7 @@ class GatewayService {
239
265
  (thread) => (thread.managed || this.store.activeForThread(thread.id)) && thread.projectId === projectId && this.historyVisible(thread)
240
266
  ).sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0)).slice(0, 30);
241
267
  for (const thread of managed) {
242
- this.projects.assertOwnership(projectId, thread.cwd);
268
+ this.workspaces.assertOwnership(projectId, thread.cwd, thread.id);
243
269
  if (!data.some((entry) => entry.id === thread.id)) data.unshift(this.threadView(thread));
244
270
  }
245
271
  }
@@ -264,7 +290,7 @@ class GatewayService {
264
290
  this.archiveChanges.add(threadId);
265
291
  try {
266
292
  const actual = await this.codex.readThreadMetadata(threadId);
267
- this.projects.assertOwnership(thread.projectId, actual.cwd);
293
+ this.workspaces.assertOwnership(thread.projectId, actual.cwd, thread.id);
268
294
  const current = this.projection.get(threadId);
269
295
  if (input.archived && (this.resumes.has(threadId) || this.store.activeForThread(threadId) || current.managed && current.status === "unknown" || [actual, current].some(
270
296
  (entry) => isActive(entry.status) || entry.turns.some((turn) => isActive(turn.status) || turn.status === "unknown") || entry.actions.some((action) => ["pending", "responding"].includes(action.status))
@@ -328,7 +354,7 @@ class GatewayService {
328
354
  actual = await this.codex.readThreadMetadata(threadId);
329
355
  if (actual.id !== threadId || this.codex.epoch !== epoch || !this.codex.ready)
330
356
  throw new Error("\u4F1A\u8BDD\u6216 Codex \u5B9E\u4F8B\u5DF2\u53D8\u5316");
331
- this.projects.assertOwnership(thread.projectId, actual.cwd);
357
+ this.workspaces.assertOwnership(thread.projectId, actual.cwd, thread.id);
332
358
  } catch (cause) {
333
359
  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 });
334
360
  }
@@ -376,6 +402,7 @@ class GatewayService {
376
402
  const thread = this.projection.get(threadId);
377
403
  if (!thread || Boolean(thread.archived) === archived) return;
378
404
  thread.archived = archived;
405
+ this.archiveRevisions.set(threadId, (this.archiveRevisions.get(threadId) ?? 0) + 1);
379
406
  if (archived) this.releaseThreadOwnership(threadId);
380
407
  this.projection.touch(threadId);
381
408
  if (!this.archiveChanges.has(threadId)) this.publishThread(threadId);
@@ -383,9 +410,9 @@ class GatewayService {
383
410
  async getThreadMetadata(threadId) {
384
411
  const cached = this.projection.get(threadId);
385
412
  if (cached) {
386
- this.projects.assertOwnership(cached.projectId, cached.cwd);
413
+ this.workspaces.assertOwnership(cached.projectId, cached.cwd, cached.id);
387
414
  if (this.codex.ready) await this.refreshThreadMetadata(threadId);
388
- this.projects.assertOwnership(cached.projectId, cached.cwd);
415
+ this.workspaces.assertOwnership(cached.projectId, cached.cwd, cached.id);
389
416
  return { ...this.threadView(cached), turns: [], actions: [] };
390
417
  }
391
418
  const metadata = this.threadView(this.normalize(await this.codex.readThreadMetadata(threadId)));
@@ -396,9 +423,21 @@ class GatewayService {
396
423
  await this.loadThread(threadId);
397
424
  if (cached && this.codex.ready) await this.refreshThreadMetadata(threadId);
398
425
  const thread = this.projection.get(threadId);
399
- this.projects.assertOwnership(thread.projectId, thread.cwd);
426
+ this.workspaces.assertOwnership(thread.projectId, thread.cwd, thread.id);
400
427
  return this.threadView(thread);
401
428
  }
429
+ async getThreadHistory(threadId, cursor) {
430
+ if (!cursor || cursor.length > 8192)
431
+ throw new AppError("VALIDATION_FAILED", "Missing or invalid history cursor", 400);
432
+ this.ensureReady();
433
+ const metadata = this.normalize(await this.codex.readThreadMetadata(threadId));
434
+ this.workspaces.assertOwnership(metadata.projectId, metadata.cwd, metadata.id);
435
+ if (!this.codex.readThreadPage)
436
+ throw new AppError("HISTORY_UNAVAILABLE", "\u5F53\u524D Codex \u4E0D\u652F\u6301\u5386\u53F2\u5206\u9875", 409);
437
+ const page = this.normalize(await this.codex.readThreadPage(threadId, cursor));
438
+ this.workspaces.assertOwnership(page.projectId, page.cwd, page.id);
439
+ return page;
440
+ }
402
441
  async exportThread(threadId) {
403
442
  this.ensureReady();
404
443
  const epoch = this.codex.epoch;
@@ -415,8 +454,8 @@ class GatewayService {
415
454
  409
416
455
  );
417
456
  const link = this.store.getThreadLink(threadId);
418
- if (link) this.projects.assertOwnership(link.projectId, thread.cwd);
419
- else this.projects.findOwner(thread.cwd);
457
+ if (link) this.workspaces.assertOwnership(link.projectId, thread.cwd, thread.id);
458
+ else this.workspaces.findOwner(thread.cwd, thread.id);
420
459
  return renderThreadExport(thread, {
421
460
  executionUncertain: Boolean(this.store.activeForThread(threadId)),
422
461
  historyChanged: revision !== this.projection.revision(threadId) || thread.updatedAt !== latest.updatedAt || thread.status !== latest.status
@@ -437,33 +476,38 @@ class GatewayService {
437
476
  this.normalize(await this.codex.readThreadMetadata(threadId));
438
477
  link = this.store.getThreadLink(threadId);
439
478
  }
440
- const { project, cwd } = this.projects.assertOwnership(link.projectId, link.cwd);
479
+ const ownership = this.workspaces.assertOwnership(link.projectId, link.cwd, threadId);
480
+ const { cwd } = ownership;
441
481
  const file = await read(path, {
442
482
  cwd,
483
+ excludedRoots: this.workspaces.allocations().filter((row) => row.threadId !== threadId).map((row) => row.canonicalRoot),
443
484
  roots: [
444
- { canonicalRoot: project.canonicalRoot, aliases: [project.rootPath] },
445
- ...this.fileTemporaryRoots
485
+ { canonicalRoot: ownership.canonicalRoot, aliases: [ownership.rootPath] },
486
+ ...link.projectId === null ? [] : this.fileTemporaryRoots
446
487
  ]
447
488
  });
448
- this.projects.assertOwnership(link.projectId, link.cwd);
489
+ this.workspaces.assertOwnership(link.projectId, link.cwd, threadId);
449
490
  return file;
450
491
  }
451
492
  async loadThread(threadId) {
452
493
  const cached = this.projection.get(threadId);
453
494
  if (cached) {
454
- this.projects.assertOwnership(cached.projectId, cached.cwd);
495
+ this.workspaces.assertOwnership(cached.projectId, cached.cwd, cached.id);
455
496
  const marker = this.store.activeForThread(threadId);
456
497
  if (marker?.turnId && this.codex.ready) {
457
498
  await this.refreshExecution(marker);
458
499
  return this.threadView(cached);
459
500
  }
460
- if (cached.status !== "unknown" || !this.codex.ready) return this.threadView(cached);
501
+ if ((cached.managed || isActive(cached.status)) && cached.status !== "unknown" || !this.codex.ready)
502
+ return this.threadView(cached);
461
503
  }
462
504
  const existing = this.reads.get(threadId);
463
505
  if (existing) return this.threadView(await existing);
464
506
  const revision = this.projection.revision(threadId);
465
507
  const loading = (async () => {
466
- const thread = this.normalize(await this.codex.readThread(threadId));
508
+ const thread = this.normalize(
509
+ await (this.codex.readThreadPage?.(threadId) ?? this.codex.readThread(threadId))
510
+ );
467
511
  const current = this.projection.get(threadId);
468
512
  const preserveCurrent = current && (cached === void 0 || this.projection.revision(threadId) !== revision);
469
513
  const result = preserveCurrent ? current : this.projection.thread(thread);
@@ -483,18 +527,26 @@ class GatewayService {
483
527
  }
484
528
  }
485
529
  async createThread(projectId, input) {
486
- return this.perform("thread.create", projectId, input, async () => {
487
- const project = this.projects.get(projectId);
530
+ return this.perform("thread.create", projectId ?? UNASSIGNED_SCOPE, input, async () => {
488
531
  this.ensureReady();
489
532
  this.ensureCompatibilityConfirmed();
533
+ const cwd = projectId === null ? this.workspaces.allocate(input.clientOperationId).cwd : this.projects.get(projectId).canonicalRoot;
534
+ if (projectId !== null) this.workspaces.assertProjectDirectory(cwd);
490
535
  this.store.associateProject(input.clientOperationId, projectId);
491
536
  this.store.dispatch(input.clientOperationId);
492
- this.pendingThreadCreations.set(input.clientOperationId, project.canonicalRoot);
537
+ this.pendingThreadCreations.set(input.clientOperationId, cwd);
493
538
  const epoch = this.codex.epoch;
494
539
  const settingsSequence = this.runtimeSettingsSequence;
495
- const thread = await this.codex.createThread(project.canonicalRoot, input.settings);
540
+ const thread = await this.codex.createThread(cwd, input.settings);
496
541
  if (this.codex.epoch !== epoch || !this.codex.ready)
497
542
  throw new Error("\u521B\u5EFA\u8BF7\u6C42\u5DF2\u53D1\u9001\uFF0C\u4F46 Codex \u5B9E\u4F8B\u5DF2\u53D8\u5316\uFF0C\u8BF7\u6838\u5BF9\u5386\u53F2");
543
+ if (projectId === null) {
544
+ try {
545
+ this.workspaces.bind(input.clientOperationId, thread.id, thread.cwd);
546
+ } catch (cause) {
547
+ throw new Error("\u521B\u5EFA\u8BF7\u6C42\u5DF2\u53D1\u9001\uFF0C\u4F46\u8FD4\u56DE\u7684\u5DE5\u4F5C\u76EE\u5F55\u65E0\u6CD5\u786E\u8BA4\uFF0C\u8BF7\u6838\u5BF9\u5386\u53F2", { cause });
548
+ }
549
+ }
498
550
  const normalized = this.normalize(thread, projectId, true);
499
551
  this.projection.thread(
500
552
  normalized,
@@ -511,7 +563,7 @@ class GatewayService {
511
563
  this.ensureReady();
512
564
  this.ensureArchiveAvailable(thread);
513
565
  if (thread.archived) throw new AppError("THREAD_ARCHIVED", "\u8BF7\u5148\u53D6\u6D88\u5F52\u6863\u540E\u518D\u6062\u590D\u4F1A\u8BDD", 409);
514
- this.projects.assertOwnership(thread.projectId, thread.cwd);
566
+ this.workspaces.assertOwnership(thread.projectId, thread.cwd, thread.id);
515
567
  this.store.associateProject(input.clientOperationId, thread.projectId);
516
568
  const link = this.store.getThreadLink(threadId);
517
569
  if (link?.managedEpoch === this.codex.epoch) {
@@ -525,7 +577,7 @@ class GatewayService {
525
577
  operationId: marker.operationId,
526
578
  threadId
527
579
  });
528
- const ownership = this.projects.assertOwnership(thread.projectId, thread.cwd);
580
+ const ownership = this.workspaces.assertOwnership(thread.projectId, thread.cwd, thread.id);
529
581
  this.ensureCompatibilityConfirmed();
530
582
  this.store.dispatch(input.clientOperationId);
531
583
  let pending = this.resumes.get(threadId);
@@ -570,21 +622,29 @@ class GatewayService {
570
622
  this.ensureReady();
571
623
  const source = this.projection.get(threadId) ?? await this.codex.readThread(threadId);
572
624
  const link = this.store.getThreadLink(threadId);
573
- const ownership = link ? this.projects.assertOwnership(link.projectId, source.cwd) : this.projects.findOwner(source.cwd);
625
+ const ownership = link ? this.workspaces.assertOwnership(link.projectId, source.cwd, threadId) : this.workspaces.findOwner(source.cwd, source.id);
574
626
  this.ensureCompatibilityConfirmed();
575
627
  const epoch = this.codex.epoch;
576
- this.store.associateProject(input.clientOperationId, ownership.project.id);
628
+ let forkCwd = ownership.cwd;
629
+ if (ownership.projectId === null) {
630
+ forkCwd = this.workspaces.allocate(input.clientOperationId).cwd;
631
+ await cp(ownership.cwd, forkCwd, { recursive: true, verbatimSymlinks: true });
632
+ this.workspaces.assertOwnership(null, source.cwd, source.id);
633
+ }
634
+ this.store.associateProject(input.clientOperationId, ownership.projectId);
577
635
  this.store.dispatch(input.clientOperationId);
578
- this.pendingThreadCreations.set(input.clientOperationId, ownership.cwd);
636
+ this.pendingThreadCreations.set(input.clientOperationId, forkCwd);
579
637
  const settingsSequence = this.runtimeSettingsSequence;
580
- const forked = await this.codex.forkThread(threadId, ownership.cwd, input.settings);
638
+ const forked = await this.codex.forkThread(threadId, forkCwd, input.settings);
581
639
  if (forked.id === threadId || this.codex.epoch !== epoch || !this.codex.ready)
582
640
  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");
583
641
  let normalized;
584
642
  try {
643
+ if (ownership.projectId === null)
644
+ this.workspaces.bind(input.clientOperationId, forked.id, forked.cwd);
585
645
  normalized = this.normalize(
586
646
  { ...forked, forkedFromId: threadId },
587
- ownership.project.id,
647
+ ownership.projectId,
588
648
  true
589
649
  );
590
650
  } catch (error) {
@@ -603,7 +663,7 @@ class GatewayService {
603
663
  return this.perform("turn.start", threadId, input, async () => {
604
664
  const thread = await this.loadThread(threadId);
605
665
  this.ensureManaged(thread);
606
- const ownership = this.projects.assertOwnership(thread.projectId, thread.cwd);
666
+ const ownership = this.workspaces.assertOwnership(thread.projectId, thread.cwd, thread.id);
607
667
  if (isActive(thread.status))
608
668
  throw new AppError("THREAD_BUSY", "\u4F1A\u8BDD\u5DF2\u6709\u6D3B\u52A8\u4EFB\u52A1\uFF0C\u8BF7\u8FFD\u52A0\u8981\u6C42\u6216\u4E2D\u65AD", 409, { threadId });
609
669
  this.ensureCompatibilityConfirmed();
@@ -636,7 +696,7 @@ class GatewayService {
636
696
  return this.perform("thread.compact", threadId, input, async () => {
637
697
  const thread = await this.loadThread(threadId);
638
698
  this.ensureManaged(thread);
639
- const ownership = this.projects.assertOwnership(thread.projectId, thread.cwd);
699
+ const ownership = this.workspaces.assertOwnership(thread.projectId, thread.cwd, thread.id);
640
700
  if (isActive(thread.status) || thread.turns.some((turn) => isActive(turn.status)))
641
701
  throw new AppError("THREAD_BUSY", "\u8BF7\u7B49\u5F85\u5F53\u524D\u4EFB\u52A1\u7ED3\u675F\u540E\u518D\u538B\u7F29\u4E0A\u4E0B\u6587", 409);
642
702
  if (!thread.turns.length)
@@ -660,7 +720,7 @@ class GatewayService {
660
720
  return this.perform("review.start", threadId, input, async () => {
661
721
  const thread = await this.loadThread(threadId);
662
722
  this.ensureManaged(thread);
663
- const ownership = this.projects.assertOwnership(thread.projectId, thread.cwd);
723
+ const ownership = this.workspaces.assertOwnership(thread.projectId, thread.cwd, thread.id);
664
724
  if (isActive(thread.status) || thread.turns.some((turn2) => isActive(turn2.status)))
665
725
  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, {
666
726
  threadId
@@ -759,8 +819,8 @@ class GatewayService {
759
819
  try {
760
820
  const actual = await this.codex.readThread(marker.threadId);
761
821
  const link = this.store.getThreadLink(marker.threadId);
762
- if (link) this.projects.assertOwnership(link.projectId, actual.cwd);
763
- else this.projects.findOwner(actual.cwd);
822
+ if (link) this.workspaces.assertOwnership(link.projectId, actual.cwd, actual.id);
823
+ else this.workspaces.findOwner(actual.cwd, actual.id);
764
824
  if (this.markerIsActive(actual, marker.turnId))
765
825
  throw new AppError("THREAD_BUSY", "\u4E0A\u6E38\u4ECD\u62A5\u544A\u8BE5\u4EFB\u52A1\u6D3B\u52A8\uFF0C\u4E0D\u80FD\u89E3\u9664\u5360\u7528", 409, {
766
826
  threadId: marker.threadId
@@ -855,14 +915,14 @@ class GatewayService {
855
915
  }
856
916
  normalize(thread, projectId, managed = false) {
857
917
  const link = this.store.getThreadLink(thread.id);
858
- const ownership = link ? this.projects.assertOwnership(link.projectId, thread.cwd) : managed && projectId ? this.projects.assertOwnership(projectId, thread.cwd) : this.projects.findOwner(thread.cwd);
859
- if (projectId && ownership.project.id !== projectId)
918
+ const ownership = link ? this.workspaces.assertOwnership(link.projectId, thread.cwd, thread.id) : managed && projectId !== void 0 ? this.workspaces.assertOwnership(projectId, thread.cwd, thread.id) : this.workspaces.findOwner(thread.cwd, thread.id);
919
+ if (projectId !== void 0 && ownership.projectId !== projectId)
860
920
  throw new AppError("PROJECT_OWNERSHIP", "\u4F1A\u8BDD\u5C5E\u4E8E\u5176\u4ED6\u9879\u76EE\uFF0C\u4E0D\u80FD\u6539\u53D8\u9879\u76EE\u5F52\u5C5E", 403);
861
921
  const managedEpoch = thread.archived ? null : managed ? this.codex.epoch : link?.managedEpoch ?? null;
862
922
  const result = {
863
923
  ...thread,
864
924
  turns: this.turnUsage.restore(thread.id, thread.turns),
865
- projectId: ownership.project.id,
925
+ projectId: ownership.projectId,
866
926
  cwd: ownership.cwd,
867
927
  managed: managedEpoch === this.codex.epoch,
868
928
  resumeError: managed ? void 0 : this.projection.get(thread.id)?.resumeError,
@@ -872,7 +932,7 @@ class GatewayService {
872
932
  };
873
933
  this.store.linkThread({
874
934
  threadId: thread.id,
875
- projectId: ownership.project.id,
935
+ projectId: ownership.projectId,
876
936
  cwd: ownership.cwd,
877
937
  source: thread.source,
878
938
  managedEpoch
@@ -907,7 +967,7 @@ class GatewayService {
907
967
  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)
908
968
  return;
909
969
  const link = this.store.getThreadLink(marker.threadId);
910
- const ownership = link ? this.projects.assertOwnership(link.projectId, actual.cwd) : this.projects.findOwner(actual.cwd);
970
+ const ownership = link ? this.workspaces.assertOwnership(link.projectId, actual.cwd, actual.id) : this.workspaces.findOwner(actual.cwd, actual.id);
911
971
  if (ownership.workspaceKey !== marker.workspaceKey) return;
912
972
  const turn = actual.turns.find((entry) => entry.id === marker.turnId);
913
973
  if (turn && isActive(turn.status) && latest.state === "active" && this.projection.get(marker.threadId)?.status === "unknown") {
@@ -1057,7 +1117,7 @@ class GatewayService {
1057
1117
  const encoded = JSON.stringify(summary);
1058
1118
  if (this.summaries.get(threadId) === encoded) return;
1059
1119
  try {
1060
- this.projects.assertOwnership(thread.projectId, thread.cwd);
1120
+ this.workspaces.assertOwnership(thread.projectId, thread.cwd, thread.id);
1061
1121
  } catch {
1062
1122
  return;
1063
1123
  }
@@ -1105,7 +1165,7 @@ class GatewayService {
1105
1165
  const thread = await this.codex.readThreadMetadata(threadId);
1106
1166
  const current = this.projection.get(threadId);
1107
1167
  if (!current || thread.id !== threadId) return;
1108
- this.projects.assertOwnership(current.projectId, thread.cwd);
1168
+ this.workspaces.assertOwnership(current.projectId, thread.cwd, thread.id);
1109
1169
  const settings = thread.runtimeSettings ? { ...current.runtimeSettings, ...thread.runtimeSettings } : void 0;
1110
1170
  if (this.titleReads.get(threadId) === sequence && this.projection.revision(threadId) === revision && settings && JSON.stringify(current.runtimeSettings) !== JSON.stringify(settings)) {
1111
1171
  current.runtimeSettings = settings;
@@ -1210,7 +1270,7 @@ class GatewayService {
1210
1270
  this.projection.expireTurn(thread.id, event.turn.id);
1211
1271
  if (newCompletion) {
1212
1272
  try {
1213
- this.projects.assertOwnership(thread.projectId, thread.cwd);
1273
+ this.workspaces.assertOwnership(thread.projectId, thread.cwd, thread.id);
1214
1274
  this.events.publishCompletion({
1215
1275
  threadId: thread.id,
1216
1276
  turnId: event.turn.id,