@covro/covro 0.2.1-dev.3 → 0.2.1-dev.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/README.md +6 -1
  2. package/README.zh-CN.md +6 -1
  3. package/dist/codex/adapter.js +8 -1
  4. package/dist/codex/adapter.js.map +2 -2
  5. package/dist/drizzle/0005_session_workspaces.sql +24 -0
  6. package/dist/drizzle/meta/0005_snapshot.json +480 -0
  7. package/dist/drizzle/meta/_journal.json +7 -0
  8. package/dist/server/db/operations.js +14 -1
  9. package/dist/server/db/operations.js.map +2 -2
  10. package/dist/server/db/schema.js +9 -1
  11. package/dist/server/db/schema.js.map +2 -2
  12. package/dist/server/http.js +25 -0
  13. package/dist/server/http.js.map +2 -2
  14. package/dist/server/local-files.js +2 -2
  15. package/dist/server/local-files.js.map +2 -2
  16. package/dist/server/main.js +1 -0
  17. package/dist/server/main.js.map +2 -2
  18. package/dist/server/service.js +96 -51
  19. package/dist/server/service.js.map +2 -2
  20. package/dist/server/workspaces.js +138 -0
  21. package/dist/server/workspaces.js.map +7 -0
  22. package/dist/shared/workspace-scope.js +9 -0
  23. package/dist/shared/workspace-scope.js.map +7 -0
  24. package/dist/web/assets/{abnfDiagram-WEYOIA3C-6n1mVJ0V.js → abnfDiagram-WEYOIA3C-DTTcFQGL.js} +1 -1
  25. package/dist/web/assets/{arc-DFIzIYgQ.js → arc-K2pKIwfu.js} +1 -1
  26. package/dist/web/assets/{architecture-WOLXFQ4H-CoZpoP0F.js → architecture-WOLXFQ4H-DrxN_1CG.js} +1 -1
  27. package/dist/web/assets/{architectureDiagram-ZHVYKT53-LcqVhgRh.js → architectureDiagram-ZHVYKT53-CyyCrVAb.js} +1 -1
  28. package/dist/web/assets/{blockDiagram-RJAE7QA3-CnVQsmM8.js → blockDiagram-RJAE7QA3-C8vl8QXS.js} +1 -1
  29. package/dist/web/assets/{c4Diagram-XDPPPE54-C38UO-rV.js → c4Diagram-XDPPPE54-BtnIITFN.js} +1 -1
  30. package/dist/web/assets/channel-CDExoPVe.js +1 -0
  31. package/dist/web/assets/{chunk-2E7WHVMD-SlpXCVVC.js → chunk-2E7WHVMD-BssauSch.js} +1 -1
  32. package/dist/web/assets/{chunk-4EMNKR2T-BPJa0lfJ.js → chunk-4EMNKR2T-BZOo1RWg.js} +1 -1
  33. package/dist/web/assets/{chunk-55XCU76K-CkP_cE5N.js → chunk-55XCU76K-D8ZWazMQ.js} +1 -1
  34. package/dist/web/assets/{chunk-7S6XSTFO-BnrWU32l.js → chunk-7S6XSTFO-C8WqPzUl.js} +1 -1
  35. package/dist/web/assets/{chunk-AQDTSKZB-J2M9tBY-.js → chunk-AQDTSKZB-Coo6Axpg.js} +1 -1
  36. package/dist/web/assets/{chunk-DSNLSHJZ-ClfNpgVO.js → chunk-DSNLSHJZ-5TwmEPWR.js} +1 -1
  37. package/dist/web/assets/{chunk-HAGFOTED-k7XcZqWB.js → chunk-HAGFOTED-Dgtj-Fc_.js} +1 -1
  38. package/dist/web/assets/{chunk-HNE4Q36P-DHHenyG4.js → chunk-HNE4Q36P-COoS4UPL.js} +1 -1
  39. package/dist/web/assets/{chunk-NQOKRVIF-CkA3TLYc.js → chunk-NQOKRVIF-tPhNkBBj.js} +1 -1
  40. package/dist/web/assets/{chunk-UHW7PFBP-DSLKZJoJ.js → chunk-UHW7PFBP-BLk_BkCl.js} +1 -1
  41. package/dist/web/assets/{chunk-VQ5W62QI-BBkSWILl.js → chunk-VQ5W62QI-HBeSVTU0.js} +1 -1
  42. package/dist/web/assets/{chunk-WQQL4CDK-BeKgda0U.js → chunk-WQQL4CDK-LmZUm8If.js} +1 -1
  43. package/dist/web/assets/{chunk-XXDRQBXY-BC-ozzjn.js → chunk-XXDRQBXY-49rKU8Nk.js} +1 -1
  44. package/dist/web/assets/{chunk-YZO67HFG-C19e3AP5.js → chunk-YZO67HFG-B_TgpaEG.js} +1 -1
  45. package/dist/web/assets/{chunk-ZAUX2LTX-C03NsQfr.js → chunk-ZAUX2LTX-f7-DSOrY.js} +1 -1
  46. package/dist/web/assets/{chunk-ZCHMQSOR-CrXzPEWh.js → chunk-ZCHMQSOR-QhJOh7x0.js} +1 -1
  47. package/dist/web/assets/{classDiagram-v2-4MYWR3UA-DCbxTUm6.js → classDiagram-v2-4MYWR3UA-CwF2HuoZ.js} +1 -1
  48. package/dist/web/assets/{cose-bilkent-JH36ORCC-CQZjK1BR.js → cose-bilkent-JH36ORCC-CLvKOlx5.js} +1 -1
  49. package/dist/web/assets/{cynefin-EF2NZ3EQ-i1jWxeqI.js → cynefin-EF2NZ3EQ-BNRVavZN.js} +1 -1
  50. package/dist/web/assets/{cynefinDiagram-WF24UDRP-DfVYiPE9.js → cynefinDiagram-WF24UDRP-DEd83kgI.js} +1 -1
  51. package/dist/web/assets/{dagre-NK4GIZVX-OhkyZBvK.js → dagre-NK4GIZVX-C340UEI3.js} +1 -1
  52. package/dist/web/assets/{diagram-CBK3MWTC-CXdVEIne.js → diagram-CBK3MWTC-CuN7ncUm.js} +1 -1
  53. package/dist/web/assets/{diagram-I5XIIV2D-BjrxVPW0.js → diagram-I5XIIV2D-D9mGAfA7.js} +1 -1
  54. package/dist/web/assets/{diagram-ODIL2UCW-C5BpAqPG.js → diagram-ODIL2UCW-CXtBv4bI.js} +1 -1
  55. package/dist/web/assets/{diagram-PFBEJUXE-3yWOE5ZP.js → diagram-PFBEJUXE-B8qP5nGT.js} +1 -1
  56. package/dist/web/assets/{diagram-RJA3I4VV-DdRZojn9.js → diagram-RJA3I4VV-BVrzsw2b.js} +1 -1
  57. package/dist/web/assets/{diagram-TYUNFSNR-m_gnI0-3.js → diagram-TYUNFSNR-Djfine3D.js} +1 -1
  58. package/dist/web/assets/{dist-DMGebNoK.js → dist-D8kDnYqD.js} +1 -1
  59. package/dist/web/assets/{ebnfDiagram-V4P3TTPT-CVwc9K-O.js → ebnfDiagram-V4P3TTPT-C69Oiekp.js} +1 -1
  60. package/dist/web/assets/{elk-QONTBCTQ-BDC-J8E3.js → elk-QONTBCTQ-Bd3BP1h1.js} +1 -1
  61. package/dist/web/assets/{erDiagram-KYQIGQAA-D-dqUb0a.js → erDiagram-KYQIGQAA-Dxcl5jVl.js} +1 -1
  62. package/dist/web/assets/{eventmodeling-K75KTNOO-ByZ_MfSZ.js → eventmodeling-K75KTNOO-Do8p535i.js} +1 -1
  63. package/dist/web/assets/flowDiagram-IUGLLWBU-X85uzgrG.js +1 -0
  64. package/dist/web/assets/{ganttDiagram-R7GPNTJY-BHzyPnqV.js → ganttDiagram-R7GPNTJY-MBGUZbVy.js} +1 -1
  65. package/dist/web/assets/{gitGraph-VSP46ZUC-DQhxrj3o.js → gitGraph-VSP46ZUC-CHxRIbkg.js} +1 -1
  66. package/dist/web/assets/{gitGraphDiagram-PDYL7XHY-8M2oNXTx.js → gitGraphDiagram-PDYL7XHY-DbW0GeW4.js} +1 -1
  67. package/dist/web/assets/index-Cz3AOJOw.js +142 -0
  68. package/dist/web/assets/index-DaMLk_sl.css +1 -0
  69. package/dist/web/assets/{info-OHQRW6UA-717A6Kfa.js → info-OHQRW6UA-K8BF1xZb.js} +1 -1
  70. package/dist/web/assets/{infoDiagram-MVDRKAX5-D-fxOvE-.js → infoDiagram-MVDRKAX5-QaWV5-SY.js} +1 -1
  71. package/dist/web/assets/{ishikawaDiagram-HPEHV2GE-CKrXOGrR.js → ishikawaDiagram-HPEHV2GE-DpVz_sC0.js} +1 -1
  72. package/dist/web/assets/{journeyDiagram-BOSHW7O7-BLD4lmI6.js → journeyDiagram-BOSHW7O7-S9MzozV7.js} +1 -1
  73. package/dist/web/assets/{kanban-definition-FNSA4HC5-Mj1GI5Fb.js → kanban-definition-FNSA4HC5-BiU8Ibw-.js} +1 -1
  74. package/dist/web/assets/{line-DotNyRbu.js → line-B1KaX5ms.js} +1 -1
  75. package/dist/web/assets/{linear-DQ0Uvp2K.js → linear-Cg1dwAtU.js} +1 -1
  76. package/dist/web/assets/{mermaid-parser.core-BtFC_UVn.js → mermaid-parser.core-hzYHYxfI.js} +3 -3
  77. package/dist/web/assets/{mermaid.core-DHL14mKq.js → mermaid.core-N8xYYvXy.js} +4 -4
  78. package/dist/web/assets/{mindmap-definition-6OY57UG3-CMCguBhk.js → mindmap-definition-6OY57UG3-BmIQCrEd.js} +1 -1
  79. package/dist/web/assets/{packet-JDAUHWVQ-CGyEqUmo.js → packet-JDAUHWVQ-DhOTX9De.js} +1 -1
  80. package/dist/web/assets/{pegDiagram-ECIUJNBX-DPgLxTHJ.js → pegDiagram-ECIUJNBX-CVfE6yUu.js} +1 -1
  81. package/dist/web/assets/{pie-XZMESJXO-Ddsl6geS.js → pie-XZMESJXO-BayfKVL8.js} +1 -1
  82. package/dist/web/assets/{pieDiagram-GNMPY74T-BJdkqrU_.js → pieDiagram-GNMPY74T-ClbMRve3.js} +1 -1
  83. package/dist/web/assets/{quadrantDiagram-D57K4MC3-47l23gov.js → quadrantDiagram-D57K4MC3-DFp-P_EZ.js} +1 -1
  84. package/dist/web/assets/{radar-ABXABTNO-DAyB1li0.js → radar-ABXABTNO-Bt1EAsxC.js} +1 -1
  85. package/dist/web/assets/{railroad-I3PHUGI6-BZYuBfPR.js → railroad-I3PHUGI6-CSos2Rm9.js} +1 -1
  86. package/dist/web/assets/{railroad-abnf-LNEFI6M7-PBkoxIWP.js → railroad-abnf-LNEFI6M7-CebY-3Kh.js} +1 -1
  87. package/dist/web/assets/{railroad-ebnf-DWYD2UWJ-Bl_rTgpR.js → railroad-ebnf-DWYD2UWJ-DpA6-TBs.js} +1 -1
  88. package/dist/web/assets/{railroad-peg-JR7BVNR7-U9kj7aP3.js → railroad-peg-JR7BVNR7-Bg3QPvi5.js} +1 -1
  89. package/dist/web/assets/{railroadDiagram-Y72NRBL7-Dpq4us5_.js → railroadDiagram-Y72NRBL7-BM4RAgqS.js} +1 -1
  90. package/dist/web/assets/{requirementDiagram-EQYSW5RA-CCqQbcTz.js → requirementDiagram-EQYSW5RA-CqP9bCS_.js} +1 -1
  91. package/dist/web/assets/{sankeyDiagram-SY24JHO5-DDwytZM3.js → sankeyDiagram-SY24JHO5-BhS8f6-k.js} +1 -1
  92. package/dist/web/assets/{sequenceDiagram-BLEHNHQU-C-z57Y06.js → sequenceDiagram-BLEHNHQU-p3C1DgeC.js} +1 -1
  93. package/dist/web/assets/{src-CK51kAyQ.js → src-9XJAFibc.js} +1 -1
  94. package/dist/web/assets/{stateDiagram-v2-A6IESKWZ-DgTp0O6g.js → stateDiagram-v2-A6IESKWZ-DVVy8chY.js} +1 -1
  95. package/dist/web/assets/{swimlanes-B3ENRKFQ-BiyS7wH3.js → swimlanes-B3ENRKFQ-CtMPO6Gw.js} +1 -1
  96. package/dist/web/assets/swimlanesDiagram-AYQRV4AK-BRXJWgnO.js +8 -0
  97. package/dist/web/assets/{timeline-definition-JXYKGLGB-D_PAPA8Z.js → timeline-definition-JXYKGLGB-DuTqpjLQ.js} +1 -1
  98. package/dist/web/assets/{treeView-D4JQ5SDB-m1fbOydD.js → treeView-D4JQ5SDB-qpOZEphu.js} +1 -1
  99. package/dist/web/assets/{treemap-SAJKECNS-Bxt7Udu9.js → treemap-SAJKECNS-Dm6e3TpG.js} +1 -1
  100. package/dist/web/assets/{usecaseDiagram-WPOLBNNX-BTrMcoPL.js → usecaseDiagram-WPOLBNNX-DoCSdm5y.js} +1 -1
  101. package/dist/web/assets/{vennDiagram-Q3Y3IDCW-C_kOOxPJ.js → vennDiagram-Q3Y3IDCW-qzd4yvh5.js} +1 -1
  102. package/dist/web/assets/{wardley-7MLQ67FV-C33rdYbB.js → wardley-7MLQ67FV-CkcgIQgQ.js} +1 -1
  103. package/dist/web/assets/{wardleyDiagram-IVAIDKWA-DpG2rl09.js → wardleyDiagram-IVAIDKWA-CE6OR9hA.js} +1 -1
  104. package/dist/web/assets/{xychartDiagram-4URNHDU4-5a7LnsSZ.js → xychartDiagram-4URNHDU4-OrFXHVc6.js} +1 -1
  105. package/dist/web/index.html +2 -2
  106. package/package.json +3 -3
  107. package/dist/web/assets/channel-CV6Myu73.js +0 -1
  108. package/dist/web/assets/flowDiagram-IUGLLWBU-CiUTloqN.js +0 -1
  109. package/dist/web/assets/index-DYwdlB-_.js +0 -142
  110. package/dist/web/assets/index-TP24ydrn.css +0 -1
  111. package/dist/web/assets/swimlanesDiagram-AYQRV4AK-DJ90Jhqq.js +0 -8
@@ -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,7 +423,7 @@ 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
  }
402
429
  async getThreadHistory(threadId, cursor) {
@@ -404,11 +431,11 @@ class GatewayService {
404
431
  throw new AppError("VALIDATION_FAILED", "Missing or invalid history cursor", 400);
405
432
  this.ensureReady();
406
433
  const metadata = this.normalize(await this.codex.readThreadMetadata(threadId));
407
- this.projects.assertOwnership(metadata.projectId, metadata.cwd);
434
+ this.workspaces.assertOwnership(metadata.projectId, metadata.cwd, metadata.id);
408
435
  if (!this.codex.readThreadPage)
409
436
  throw new AppError("HISTORY_UNAVAILABLE", "\u5F53\u524D Codex \u4E0D\u652F\u6301\u5386\u53F2\u5206\u9875", 409);
410
437
  const page = this.normalize(await this.codex.readThreadPage(threadId, cursor));
411
- this.projects.assertOwnership(page.projectId, page.cwd);
438
+ this.workspaces.assertOwnership(page.projectId, page.cwd, page.id);
412
439
  return page;
413
440
  }
414
441
  async exportThread(threadId) {
@@ -427,8 +454,8 @@ class GatewayService {
427
454
  409
428
455
  );
429
456
  const link = this.store.getThreadLink(threadId);
430
- if (link) this.projects.assertOwnership(link.projectId, thread.cwd);
431
- 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);
432
459
  return renderThreadExport(thread, {
433
460
  executionUncertain: Boolean(this.store.activeForThread(threadId)),
434
461
  historyChanged: revision !== this.projection.revision(threadId) || thread.updatedAt !== latest.updatedAt || thread.status !== latest.status
@@ -449,21 +476,23 @@ class GatewayService {
449
476
  this.normalize(await this.codex.readThreadMetadata(threadId));
450
477
  link = this.store.getThreadLink(threadId);
451
478
  }
452
- 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;
453
481
  const file = await read(path, {
454
482
  cwd,
483
+ excludedRoots: this.workspaces.allocations().filter((row) => row.threadId !== threadId).map((row) => row.canonicalRoot),
455
484
  roots: [
456
- { canonicalRoot: project.canonicalRoot, aliases: [project.rootPath] },
457
- ...this.fileTemporaryRoots
485
+ { canonicalRoot: ownership.canonicalRoot, aliases: [ownership.rootPath] },
486
+ ...link.projectId === null ? [] : this.fileTemporaryRoots
458
487
  ]
459
488
  });
460
- this.projects.assertOwnership(link.projectId, link.cwd);
489
+ this.workspaces.assertOwnership(link.projectId, link.cwd, threadId);
461
490
  return file;
462
491
  }
463
492
  async loadThread(threadId) {
464
493
  const cached = this.projection.get(threadId);
465
494
  if (cached) {
466
- this.projects.assertOwnership(cached.projectId, cached.cwd);
495
+ this.workspaces.assertOwnership(cached.projectId, cached.cwd, cached.id);
467
496
  const marker = this.store.activeForThread(threadId);
468
497
  if (marker?.turnId && this.codex.ready) {
469
498
  await this.refreshExecution(marker);
@@ -498,18 +527,26 @@ class GatewayService {
498
527
  }
499
528
  }
500
529
  async createThread(projectId, input) {
501
- return this.perform("thread.create", projectId, input, async () => {
502
- const project = this.projects.get(projectId);
530
+ return this.perform("thread.create", projectId ?? UNASSIGNED_SCOPE, input, async () => {
503
531
  this.ensureReady();
504
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);
505
535
  this.store.associateProject(input.clientOperationId, projectId);
506
536
  this.store.dispatch(input.clientOperationId);
507
- this.pendingThreadCreations.set(input.clientOperationId, project.canonicalRoot);
537
+ this.pendingThreadCreations.set(input.clientOperationId, cwd);
508
538
  const epoch = this.codex.epoch;
509
539
  const settingsSequence = this.runtimeSettingsSequence;
510
- const thread = await this.codex.createThread(project.canonicalRoot, input.settings);
540
+ const thread = await this.codex.createThread(cwd, input.settings);
511
541
  if (this.codex.epoch !== epoch || !this.codex.ready)
512
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
+ }
513
550
  const normalized = this.normalize(thread, projectId, true);
514
551
  this.projection.thread(
515
552
  normalized,
@@ -526,7 +563,7 @@ class GatewayService {
526
563
  this.ensureReady();
527
564
  this.ensureArchiveAvailable(thread);
528
565
  if (thread.archived) throw new AppError("THREAD_ARCHIVED", "\u8BF7\u5148\u53D6\u6D88\u5F52\u6863\u540E\u518D\u6062\u590D\u4F1A\u8BDD", 409);
529
- this.projects.assertOwnership(thread.projectId, thread.cwd);
566
+ this.workspaces.assertOwnership(thread.projectId, thread.cwd, thread.id);
530
567
  this.store.associateProject(input.clientOperationId, thread.projectId);
531
568
  const link = this.store.getThreadLink(threadId);
532
569
  if (link?.managedEpoch === this.codex.epoch) {
@@ -540,7 +577,7 @@ class GatewayService {
540
577
  operationId: marker.operationId,
541
578
  threadId
542
579
  });
543
- const ownership = this.projects.assertOwnership(thread.projectId, thread.cwd);
580
+ const ownership = this.workspaces.assertOwnership(thread.projectId, thread.cwd, thread.id);
544
581
  this.ensureCompatibilityConfirmed();
545
582
  this.store.dispatch(input.clientOperationId);
546
583
  let pending = this.resumes.get(threadId);
@@ -585,21 +622,29 @@ class GatewayService {
585
622
  this.ensureReady();
586
623
  const source = this.projection.get(threadId) ?? await this.codex.readThread(threadId);
587
624
  const link = this.store.getThreadLink(threadId);
588
- 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);
589
626
  this.ensureCompatibilityConfirmed();
590
627
  const epoch = this.codex.epoch;
591
- 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);
592
635
  this.store.dispatch(input.clientOperationId);
593
- this.pendingThreadCreations.set(input.clientOperationId, ownership.cwd);
636
+ this.pendingThreadCreations.set(input.clientOperationId, forkCwd);
594
637
  const settingsSequence = this.runtimeSettingsSequence;
595
- const forked = await this.codex.forkThread(threadId, ownership.cwd, input.settings);
638
+ const forked = await this.codex.forkThread(threadId, forkCwd, input.settings);
596
639
  if (forked.id === threadId || this.codex.epoch !== epoch || !this.codex.ready)
597
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");
598
641
  let normalized;
599
642
  try {
643
+ if (ownership.projectId === null)
644
+ this.workspaces.bind(input.clientOperationId, forked.id, forked.cwd);
600
645
  normalized = this.normalize(
601
646
  { ...forked, forkedFromId: threadId },
602
- ownership.project.id,
647
+ ownership.projectId,
603
648
  true
604
649
  );
605
650
  } catch (error) {
@@ -618,7 +663,7 @@ class GatewayService {
618
663
  return this.perform("turn.start", threadId, input, async () => {
619
664
  const thread = await this.loadThread(threadId);
620
665
  this.ensureManaged(thread);
621
- const ownership = this.projects.assertOwnership(thread.projectId, thread.cwd);
666
+ const ownership = this.workspaces.assertOwnership(thread.projectId, thread.cwd, thread.id);
622
667
  if (isActive(thread.status))
623
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 });
624
669
  this.ensureCompatibilityConfirmed();
@@ -651,7 +696,7 @@ class GatewayService {
651
696
  return this.perform("thread.compact", threadId, input, async () => {
652
697
  const thread = await this.loadThread(threadId);
653
698
  this.ensureManaged(thread);
654
- const ownership = this.projects.assertOwnership(thread.projectId, thread.cwd);
699
+ const ownership = this.workspaces.assertOwnership(thread.projectId, thread.cwd, thread.id);
655
700
  if (isActive(thread.status) || thread.turns.some((turn) => isActive(turn.status)))
656
701
  throw new AppError("THREAD_BUSY", "\u8BF7\u7B49\u5F85\u5F53\u524D\u4EFB\u52A1\u7ED3\u675F\u540E\u518D\u538B\u7F29\u4E0A\u4E0B\u6587", 409);
657
702
  if (!thread.turns.length)
@@ -675,7 +720,7 @@ class GatewayService {
675
720
  return this.perform("review.start", threadId, input, async () => {
676
721
  const thread = await this.loadThread(threadId);
677
722
  this.ensureManaged(thread);
678
- const ownership = this.projects.assertOwnership(thread.projectId, thread.cwd);
723
+ const ownership = this.workspaces.assertOwnership(thread.projectId, thread.cwd, thread.id);
679
724
  if (isActive(thread.status) || thread.turns.some((turn2) => isActive(turn2.status)))
680
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, {
681
726
  threadId
@@ -774,8 +819,8 @@ class GatewayService {
774
819
  try {
775
820
  const actual = await this.codex.readThread(marker.threadId);
776
821
  const link = this.store.getThreadLink(marker.threadId);
777
- if (link) this.projects.assertOwnership(link.projectId, actual.cwd);
778
- 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);
779
824
  if (this.markerIsActive(actual, marker.turnId))
780
825
  throw new AppError("THREAD_BUSY", "\u4E0A\u6E38\u4ECD\u62A5\u544A\u8BE5\u4EFB\u52A1\u6D3B\u52A8\uFF0C\u4E0D\u80FD\u89E3\u9664\u5360\u7528", 409, {
781
826
  threadId: marker.threadId
@@ -870,14 +915,14 @@ class GatewayService {
870
915
  }
871
916
  normalize(thread, projectId, managed = false) {
872
917
  const link = this.store.getThreadLink(thread.id);
873
- const ownership = link ? this.projects.assertOwnership(link.projectId, thread.cwd) : managed && projectId ? this.projects.assertOwnership(projectId, thread.cwd) : this.projects.findOwner(thread.cwd);
874
- 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)
875
920
  throw new AppError("PROJECT_OWNERSHIP", "\u4F1A\u8BDD\u5C5E\u4E8E\u5176\u4ED6\u9879\u76EE\uFF0C\u4E0D\u80FD\u6539\u53D8\u9879\u76EE\u5F52\u5C5E", 403);
876
921
  const managedEpoch = thread.archived ? null : managed ? this.codex.epoch : link?.managedEpoch ?? null;
877
922
  const result = {
878
923
  ...thread,
879
924
  turns: this.turnUsage.restore(thread.id, thread.turns),
880
- projectId: ownership.project.id,
925
+ projectId: ownership.projectId,
881
926
  cwd: ownership.cwd,
882
927
  managed: managedEpoch === this.codex.epoch,
883
928
  resumeError: managed ? void 0 : this.projection.get(thread.id)?.resumeError,
@@ -887,7 +932,7 @@ class GatewayService {
887
932
  };
888
933
  this.store.linkThread({
889
934
  threadId: thread.id,
890
- projectId: ownership.project.id,
935
+ projectId: ownership.projectId,
891
936
  cwd: ownership.cwd,
892
937
  source: thread.source,
893
938
  managedEpoch
@@ -922,7 +967,7 @@ class GatewayService {
922
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)
923
968
  return;
924
969
  const link = this.store.getThreadLink(marker.threadId);
925
- 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);
926
971
  if (ownership.workspaceKey !== marker.workspaceKey) return;
927
972
  const turn = actual.turns.find((entry) => entry.id === marker.turnId);
928
973
  if (turn && isActive(turn.status) && latest.state === "active" && this.projection.get(marker.threadId)?.status === "unknown") {
@@ -1072,7 +1117,7 @@ class GatewayService {
1072
1117
  const encoded = JSON.stringify(summary);
1073
1118
  if (this.summaries.get(threadId) === encoded) return;
1074
1119
  try {
1075
- this.projects.assertOwnership(thread.projectId, thread.cwd);
1120
+ this.workspaces.assertOwnership(thread.projectId, thread.cwd, thread.id);
1076
1121
  } catch {
1077
1122
  return;
1078
1123
  }
@@ -1120,7 +1165,7 @@ class GatewayService {
1120
1165
  const thread = await this.codex.readThreadMetadata(threadId);
1121
1166
  const current = this.projection.get(threadId);
1122
1167
  if (!current || thread.id !== threadId) return;
1123
- this.projects.assertOwnership(current.projectId, thread.cwd);
1168
+ this.workspaces.assertOwnership(current.projectId, thread.cwd, thread.id);
1124
1169
  const settings = thread.runtimeSettings ? { ...current.runtimeSettings, ...thread.runtimeSettings } : void 0;
1125
1170
  if (this.titleReads.get(threadId) === sequence && this.projection.revision(threadId) === revision && settings && JSON.stringify(current.runtimeSettings) !== JSON.stringify(settings)) {
1126
1171
  current.runtimeSettings = settings;
@@ -1225,7 +1270,7 @@ class GatewayService {
1225
1270
  this.projection.expireTurn(thread.id, event.turn.id);
1226
1271
  if (newCompletion) {
1227
1272
  try {
1228
- this.projects.assertOwnership(thread.projectId, thread.cwd);
1273
+ this.workspaces.assertOwnership(thread.projectId, thread.cwd, thread.id);
1229
1274
  this.events.publishCompletion({
1230
1275
  threadId: thread.id,
1231
1276
  turnId: event.turn.id,