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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/README.md +8 -1
  2. package/README.zh-CN.md +8 -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 +41 -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/project-clones.js +296 -0
  19. package/dist/server/project-clones.js.map +7 -0
  20. package/dist/server/service.js +116 -51
  21. package/dist/server/service.js.map +2 -2
  22. package/dist/server/workspaces.js +142 -0
  23. package/dist/server/workspaces.js.map +7 -0
  24. package/dist/shared/validation.js +5 -0
  25. package/dist/shared/validation.js.map +2 -2
  26. package/dist/shared/workspace-scope.js +9 -0
  27. package/dist/shared/workspace-scope.js.map +7 -0
  28. package/dist/web/assets/{abnfDiagram-WEYOIA3C-6n1mVJ0V.js → abnfDiagram-WEYOIA3C-CwLX554E.js} +1 -1
  29. package/dist/web/assets/{arc-DFIzIYgQ.js → arc-DAaDyyqc.js} +1 -1
  30. package/dist/web/assets/{architecture-WOLXFQ4H-CoZpoP0F.js → architecture-WOLXFQ4H-y6MnEqN0.js} +1 -1
  31. package/dist/web/assets/{architectureDiagram-ZHVYKT53-LcqVhgRh.js → architectureDiagram-ZHVYKT53-_B_4k-Pp.js} +1 -1
  32. package/dist/web/assets/{blockDiagram-RJAE7QA3-CnVQsmM8.js → blockDiagram-RJAE7QA3-C9jZ7FHy.js} +1 -1
  33. package/dist/web/assets/{c4Diagram-XDPPPE54-C38UO-rV.js → c4Diagram-XDPPPE54-DQAtMvqN.js} +1 -1
  34. package/dist/web/assets/channel-ByzMCsOB.js +1 -0
  35. package/dist/web/assets/{chunk-2E7WHVMD-SlpXCVVC.js → chunk-2E7WHVMD-Bu427yab.js} +1 -1
  36. package/dist/web/assets/{chunk-4EMNKR2T-BPJa0lfJ.js → chunk-4EMNKR2T-BrGrBG7B.js} +1 -1
  37. package/dist/web/assets/{chunk-55XCU76K-CkP_cE5N.js → chunk-55XCU76K-l4f0OZWA.js} +1 -1
  38. package/dist/web/assets/{chunk-7S6XSTFO-BnrWU32l.js → chunk-7S6XSTFO-D7ixF313.js} +1 -1
  39. package/dist/web/assets/{chunk-AQDTSKZB-J2M9tBY-.js → chunk-AQDTSKZB-Cesd-BF2.js} +1 -1
  40. package/dist/web/assets/{chunk-DSNLSHJZ-ClfNpgVO.js → chunk-DSNLSHJZ-B7dim5uT.js} +1 -1
  41. package/dist/web/assets/{chunk-HAGFOTED-k7XcZqWB.js → chunk-HAGFOTED-B-f2I7TA.js} +1 -1
  42. package/dist/web/assets/{chunk-HNE4Q36P-DHHenyG4.js → chunk-HNE4Q36P-CoIvaeX6.js} +1 -1
  43. package/dist/web/assets/{chunk-NQOKRVIF-CkA3TLYc.js → chunk-NQOKRVIF-BybRxR7n.js} +1 -1
  44. package/dist/web/assets/{chunk-UHW7PFBP-DSLKZJoJ.js → chunk-UHW7PFBP-CV5WG4tq.js} +1 -1
  45. package/dist/web/assets/{chunk-VQ5W62QI-BBkSWILl.js → chunk-VQ5W62QI-BO4AjXi1.js} +1 -1
  46. package/dist/web/assets/{chunk-WQQL4CDK-BeKgda0U.js → chunk-WQQL4CDK-CADNDGXO.js} +1 -1
  47. package/dist/web/assets/{chunk-XXDRQBXY-BC-ozzjn.js → chunk-XXDRQBXY-C6QZT7KW.js} +1 -1
  48. package/dist/web/assets/{chunk-YZO67HFG-C19e3AP5.js → chunk-YZO67HFG-Cm6eGReX.js} +1 -1
  49. package/dist/web/assets/{chunk-ZAUX2LTX-C03NsQfr.js → chunk-ZAUX2LTX-CUBoGbKg.js} +1 -1
  50. package/dist/web/assets/{chunk-ZCHMQSOR-CrXzPEWh.js → chunk-ZCHMQSOR-B0ImOOtq.js} +1 -1
  51. package/dist/web/assets/{classDiagram-v2-4MYWR3UA-DCbxTUm6.js → classDiagram-v2-4MYWR3UA-D5NWZ9vm.js} +1 -1
  52. package/dist/web/assets/{cose-bilkent-JH36ORCC-CQZjK1BR.js → cose-bilkent-JH36ORCC-BzckEyjA.js} +1 -1
  53. package/dist/web/assets/{cynefin-EF2NZ3EQ-i1jWxeqI.js → cynefin-EF2NZ3EQ-Da_J6-Jb.js} +1 -1
  54. package/dist/web/assets/{cynefinDiagram-WF24UDRP-DfVYiPE9.js → cynefinDiagram-WF24UDRP-b4g_B1A-.js} +1 -1
  55. package/dist/web/assets/{dagre-NK4GIZVX-OhkyZBvK.js → dagre-NK4GIZVX-89H2mfs9.js} +1 -1
  56. package/dist/web/assets/{diagram-CBK3MWTC-CXdVEIne.js → diagram-CBK3MWTC-CW4f_P0l.js} +1 -1
  57. package/dist/web/assets/{diagram-I5XIIV2D-BjrxVPW0.js → diagram-I5XIIV2D-Cv5Qndgc.js} +1 -1
  58. package/dist/web/assets/{diagram-ODIL2UCW-C5BpAqPG.js → diagram-ODIL2UCW-LrXxeoCt.js} +1 -1
  59. package/dist/web/assets/{diagram-PFBEJUXE-3yWOE5ZP.js → diagram-PFBEJUXE-uJV2PooB.js} +1 -1
  60. package/dist/web/assets/{diagram-RJA3I4VV-DdRZojn9.js → diagram-RJA3I4VV-DlY0PGRs.js} +1 -1
  61. package/dist/web/assets/{diagram-TYUNFSNR-m_gnI0-3.js → diagram-TYUNFSNR-B1KoXBEE.js} +1 -1
  62. package/dist/web/assets/{dist-DMGebNoK.js → dist-B4eX4BCP.js} +1 -1
  63. package/dist/web/assets/{ebnfDiagram-V4P3TTPT-CVwc9K-O.js → ebnfDiagram-V4P3TTPT-xCvQVbdj.js} +1 -1
  64. package/dist/web/assets/{elk-QONTBCTQ-BDC-J8E3.js → elk-QONTBCTQ-Cl8Hu-Uy.js} +1 -1
  65. package/dist/web/assets/{erDiagram-KYQIGQAA-D-dqUb0a.js → erDiagram-KYQIGQAA-Xp-AvSwE.js} +1 -1
  66. package/dist/web/assets/{eventmodeling-K75KTNOO-ByZ_MfSZ.js → eventmodeling-K75KTNOO-C0truZGU.js} +1 -1
  67. package/dist/web/assets/flowDiagram-IUGLLWBU-T67jQ-T_.js +1 -0
  68. package/dist/web/assets/{ganttDiagram-R7GPNTJY-BHzyPnqV.js → ganttDiagram-R7GPNTJY-rPaYp-D7.js} +1 -1
  69. package/dist/web/assets/{gitGraph-VSP46ZUC-DQhxrj3o.js → gitGraph-VSP46ZUC-Bgfpho41.js} +1 -1
  70. package/dist/web/assets/{gitGraphDiagram-PDYL7XHY-8M2oNXTx.js → gitGraphDiagram-PDYL7XHY-kDcmuv6k.js} +1 -1
  71. package/dist/web/assets/index-3DUP8YCe.js +142 -0
  72. package/dist/web/assets/index-B7tnf28H.css +1 -0
  73. package/dist/web/assets/{info-OHQRW6UA-717A6Kfa.js → info-OHQRW6UA-CAWg5k9R.js} +1 -1
  74. package/dist/web/assets/{infoDiagram-MVDRKAX5-D-fxOvE-.js → infoDiagram-MVDRKAX5-Cd08ebhn.js} +1 -1
  75. package/dist/web/assets/{ishikawaDiagram-HPEHV2GE-CKrXOGrR.js → ishikawaDiagram-HPEHV2GE-tAXBLt9k.js} +1 -1
  76. package/dist/web/assets/{journeyDiagram-BOSHW7O7-BLD4lmI6.js → journeyDiagram-BOSHW7O7-ZgnWqXkg.js} +1 -1
  77. package/dist/web/assets/{kanban-definition-FNSA4HC5-Mj1GI5Fb.js → kanban-definition-FNSA4HC5-BUjtjd-h.js} +1 -1
  78. package/dist/web/assets/{line-DotNyRbu.js → line-DG48HRVK.js} +1 -1
  79. package/dist/web/assets/{linear-DQ0Uvp2K.js → linear-CqnXVCVw.js} +1 -1
  80. package/dist/web/assets/{mermaid-parser.core-BtFC_UVn.js → mermaid-parser.core-gIvgB9WK.js} +3 -3
  81. package/dist/web/assets/{mermaid.core-DHL14mKq.js → mermaid.core-C_R7Y1XB.js} +4 -4
  82. package/dist/web/assets/{mindmap-definition-6OY57UG3-CMCguBhk.js → mindmap-definition-6OY57UG3-CwUEEl_y.js} +1 -1
  83. package/dist/web/assets/{packet-JDAUHWVQ-CGyEqUmo.js → packet-JDAUHWVQ-BrXmpRQ3.js} +1 -1
  84. package/dist/web/assets/{pegDiagram-ECIUJNBX-DPgLxTHJ.js → pegDiagram-ECIUJNBX-lLApO85Q.js} +1 -1
  85. package/dist/web/assets/{pie-XZMESJXO-Ddsl6geS.js → pie-XZMESJXO-C_8kR5WX.js} +1 -1
  86. package/dist/web/assets/{pieDiagram-GNMPY74T-BJdkqrU_.js → pieDiagram-GNMPY74T-CawGmJZn.js} +1 -1
  87. package/dist/web/assets/{quadrantDiagram-D57K4MC3-47l23gov.js → quadrantDiagram-D57K4MC3-Dg6Yq3s_.js} +1 -1
  88. package/dist/web/assets/{radar-ABXABTNO-DAyB1li0.js → radar-ABXABTNO-Bs1xY09D.js} +1 -1
  89. package/dist/web/assets/{railroad-I3PHUGI6-BZYuBfPR.js → railroad-I3PHUGI6-Bw_qS6e3.js} +1 -1
  90. package/dist/web/assets/{railroad-abnf-LNEFI6M7-PBkoxIWP.js → railroad-abnf-LNEFI6M7-CQr33CuI.js} +1 -1
  91. package/dist/web/assets/{railroad-ebnf-DWYD2UWJ-Bl_rTgpR.js → railroad-ebnf-DWYD2UWJ-BY9nk5pg.js} +1 -1
  92. package/dist/web/assets/{railroad-peg-JR7BVNR7-U9kj7aP3.js → railroad-peg-JR7BVNR7-CcFsAEI4.js} +1 -1
  93. package/dist/web/assets/{railroadDiagram-Y72NRBL7-Dpq4us5_.js → railroadDiagram-Y72NRBL7-rkZbchmc.js} +1 -1
  94. package/dist/web/assets/{requirementDiagram-EQYSW5RA-CCqQbcTz.js → requirementDiagram-EQYSW5RA-CSOcijj5.js} +1 -1
  95. package/dist/web/assets/{sankeyDiagram-SY24JHO5-DDwytZM3.js → sankeyDiagram-SY24JHO5-E0uo0rRp.js} +1 -1
  96. package/dist/web/assets/{sequenceDiagram-BLEHNHQU-C-z57Y06.js → sequenceDiagram-BLEHNHQU-BKWAAcRu.js} +1 -1
  97. package/dist/web/assets/{src-CK51kAyQ.js → src-F6L8HcEC.js} +1 -1
  98. package/dist/web/assets/{stateDiagram-v2-A6IESKWZ-DgTp0O6g.js → stateDiagram-v2-A6IESKWZ-BBk2tpPh.js} +1 -1
  99. package/dist/web/assets/{swimlanes-B3ENRKFQ-BiyS7wH3.js → swimlanes-B3ENRKFQ-CAUrhGwF.js} +1 -1
  100. package/dist/web/assets/swimlanesDiagram-AYQRV4AK-BCnJymLL.js +8 -0
  101. package/dist/web/assets/{timeline-definition-JXYKGLGB-D_PAPA8Z.js → timeline-definition-JXYKGLGB-D0FWj9fa.js} +1 -1
  102. package/dist/web/assets/{treeView-D4JQ5SDB-m1fbOydD.js → treeView-D4JQ5SDB-CkJOmf93.js} +1 -1
  103. package/dist/web/assets/{treemap-SAJKECNS-Bxt7Udu9.js → treemap-SAJKECNS-BSFsardB.js} +1 -1
  104. package/dist/web/assets/{usecaseDiagram-WPOLBNNX-BTrMcoPL.js → usecaseDiagram-WPOLBNNX-B2U-2o4g.js} +1 -1
  105. package/dist/web/assets/{vennDiagram-Q3Y3IDCW-C_kOOxPJ.js → vennDiagram-Q3Y3IDCW-BijTH0fT.js} +1 -1
  106. package/dist/web/assets/{wardley-7MLQ67FV-C33rdYbB.js → wardley-7MLQ67FV-DDOa5RzL.js} +1 -1
  107. package/dist/web/assets/{wardleyDiagram-IVAIDKWA-DpG2rl09.js → wardleyDiagram-IVAIDKWA-mbb5GvW5.js} +1 -1
  108. package/dist/web/assets/{xychartDiagram-4URNHDU4-5a7LnsSZ.js → xychartDiagram-4URNHDU4-DHd60D_Y.js} +1 -1
  109. package/dist/web/index.html +2 -2
  110. package/package.json +3 -3
  111. package/dist/web/assets/channel-CV6Myu73.js +0 -1
  112. package/dist/web/assets/flowDiagram-IUGLLWBU-CiUTloqN.js +0 -1
  113. package/dist/web/assets/index-DYwdlB-_.js +0 -142
  114. package/dist/web/assets/index-TP24ydrn.css +0 -1
  115. 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 {
@@ -13,6 +16,7 @@ import {
13
16
  import { EventHub } from "./events.js";
14
17
  import { TurnUsageStore } from "./db/turn-usage.js";
15
18
  import { Projection, isActive, isTerminal } from "./projection.js";
19
+ import { ProjectClones } from "./project-clones.js";
16
20
  import { renderThreadExport } from "./thread-export.js";
17
21
  import { readLocalImage } from "./images.js";
18
22
  import { temporaryFileRoots } from "./local-files.js";
@@ -24,6 +28,20 @@ class GatewayService {
24
28
  this.store = options.store;
25
29
  this.turnUsage = new TurnUsageStore(this.store.database);
26
30
  this.projects = options.projects;
31
+ this.workspaces = new WorkspaceRegistry(
32
+ this.store.database,
33
+ this.projects,
34
+ options.workspacesRoot
35
+ );
36
+ this.projectClones = new ProjectClones(
37
+ (input) => {
38
+ this.workspaces.assertProjectDirectory(input.path);
39
+ const project = this.projects.add(input);
40
+ this.events.publishProjects(this.projects.list());
41
+ return project;
42
+ },
43
+ (path) => this.projects.list().some((project) => project.canonicalRoot === path)
44
+ );
27
45
  this.codex = options.codex;
28
46
  this.shell = new ShellService(this.codex);
29
47
  this.events = new EventHub(options.limits?.eventBytes);
@@ -33,12 +51,14 @@ class GatewayService {
33
51
  this.removeListener = this.codex.onEvent((event) => this.consume(event));
34
52
  }
35
53
  options;
54
+ projectClones;
36
55
  shell;
37
56
  events;
38
57
  projection;
39
58
  store;
40
59
  turnUsage;
41
60
  projects;
61
+ workspaces;
42
62
  codex;
43
63
  removeListener;
44
64
  fileTemporaryRoots = temporaryFileRoots();
@@ -47,6 +67,7 @@ class GatewayService {
47
67
  currentEpoch;
48
68
  confirmedCompatibility;
49
69
  archiveChanges = /* @__PURE__ */ new Set();
70
+ archiveRevisions = /* @__PURE__ */ new Map();
50
71
  turnBaselines = /* @__PURE__ */ new Map();
51
72
  renameChanges = /* @__PURE__ */ new Set();
52
73
  reads = /* @__PURE__ */ new Map();
@@ -86,6 +107,7 @@ class GatewayService {
86
107
  this.maintenance = value;
87
108
  }
88
109
  prepareUpdate() {
110
+ if (this.projectClones.hasActive) return { ready: false, reason: "\u7B49\u5F85\u9879\u76EE\u514B\u9686\u5B8C\u6210\u6216\u53D6\u6D88" };
89
111
  if (this.shell.active()) return { ready: false, reason: "\u7B49\u5F85 Shell \u547D\u4EE4\u7ED3\u675F" };
90
112
  if (this.inFlightOperations.size || this.resumes.size || this.pendingThreadCreations.size)
91
113
  return { ready: false, reason: "\u7B49\u5F85\u6B63\u5728\u63D0\u4EA4\u6216\u6062\u590D\u7684\u64CD\u4F5C\u5B8C\u6210" };
@@ -99,10 +121,18 @@ class GatewayService {
99
121
  return { ready: true };
100
122
  }
101
123
  addProject(input) {
124
+ this.workspaces.assertProjectDirectory(input.path);
125
+ this.projectClones.assertIdlePath(input.path);
102
126
  const project = this.projects.add(input);
103
127
  this.events.publishProjects(this.projects.list());
104
128
  return project;
105
129
  }
130
+ cloneProject(input) {
131
+ if (this.maintenance || !this.accepting)
132
+ throw new AppError("GATEWAY_STOPPING", "\u670D\u52A1\u6B63\u5728\u5347\u7EA7\u6216\u505C\u6B62\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5", 503);
133
+ this.workspaces.assertProjectDirectory(input.path);
134
+ return this.projectClones.start(input);
135
+ }
106
136
  renameProject(projectId, input) {
107
137
  const project = this.projects.rename(projectId, input);
108
138
  this.events.publishProjects(this.projects.list());
@@ -177,12 +207,12 @@ class GatewayService {
177
207
  });
178
208
  }
179
209
  async readRuntimeSettings(projectId) {
180
- const project = this.projects.get(projectId);
181
- return this.codex.readRuntimeSettings(project.canonicalRoot);
210
+ const cwd = projectId === null ? this.workspaces.defaultsDirectory() : this.projects.get(projectId).canonicalRoot;
211
+ return this.codex.readRuntimeSettings(cwd);
182
212
  }
183
213
  async saveModelDefaults(projectId, input) {
184
- return this.perform("config.modelDefaults", projectId, input, async () => {
185
- this.projects.get(projectId);
214
+ return this.perform("config.modelDefaults", projectId ?? UNASSIGNED_SCOPE, input, async () => {
215
+ if (projectId !== null) this.projects.get(projectId);
186
216
  this.ensureReady();
187
217
  this.ensureCompatibilityConfirmed();
188
218
  this.store.associateProject(input.clientOperationId, projectId);
@@ -191,28 +221,43 @@ class GatewayService {
191
221
  });
192
222
  }
193
223
  async listThreads(projectId, cursor, archived = false) {
194
- const project = this.projects.get(projectId);
224
+ const project = projectId === null ? void 0 : this.projects.get(projectId);
225
+ const directories = project ? project.canonicalRoot : this.workspaces.allocations().map((row) => row.canonicalRoot);
195
226
  const titleReads = new Map(
196
227
  this.projection.values().filter((thread) => thread.projectId === projectId).map((thread) => [thread.id, this.beginTitleRead(thread.id)])
197
228
  );
198
- const page = await this.codex.listThreads(project.canonicalRoot, cursor, archived);
199
- this.projects.get(projectId);
229
+ const archiveReads = new Map(
230
+ this.projection.values().filter((thread) => thread.projectId === projectId).map((thread) => [thread.id, this.archiveRevisions.get(thread.id) ?? 0])
231
+ );
232
+ const page = Array.isArray(directories) && !directories.length ? { data: [], nextCursor: null } : await this.codex.listThreads(directories, cursor, archived);
233
+ if (projectId !== null) this.projects.get(projectId);
200
234
  const data = [];
201
235
  for (const thread of page.data) {
202
236
  try {
237
+ if (projectId === null && thread.source === "appServer" && page.data.filter((entry) => entry.cwd === thread.cwd).length === 1) {
238
+ const operationId = this.workspaces.recoverCreation(thread.id, thread.cwd);
239
+ if (operationId && !this.inFlightOperations.has(operationId)) {
240
+ const recovered = this.store.confirmWorkspaceCreation(operationId, thread.id);
241
+ this.events.publish("operation", this.operation(recovered));
242
+ }
243
+ }
203
244
  const normalized = this.normalize(thread, projectId);
204
245
  normalized.hasMessages ??= true;
205
246
  const cached = this.projection.get(thread.id);
206
247
  if (cached && normalized.hasMessages) cached.hasMessages = true;
207
248
  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));
249
+ if (cached && archiveReads.get(thread.id) === (this.archiveRevisions.get(thread.id) ?? 0))
250
+ this.updateArchiveState(thread.id, Boolean(normalized.archived));
251
+ if (cached && titleRead !== void 0)
211
252
  this.updateThreadTitle(thread.id, normalized.title, titleRead);
212
- }
213
253
  data.push(this.threadView(cached ?? normalized));
214
254
  } catch (error) {
215
- if (!(error instanceof AppError) || !["PROJECT_OWNERSHIP", "PROJECT_UNAVAILABLE", "PROJECT_NOT_FOUND"].includes(error.code))
255
+ if (!(error instanceof AppError) || ![
256
+ "PROJECT_OWNERSHIP",
257
+ "PROJECT_UNAVAILABLE",
258
+ "PROJECT_NOT_FOUND",
259
+ "WORKSPACE_UNAVAILABLE"
260
+ ].includes(error.code))
216
261
  throw error;
217
262
  }
218
263
  }
@@ -239,7 +284,7 @@ class GatewayService {
239
284
  (thread) => (thread.managed || this.store.activeForThread(thread.id)) && thread.projectId === projectId && this.historyVisible(thread)
240
285
  ).sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0)).slice(0, 30);
241
286
  for (const thread of managed) {
242
- this.projects.assertOwnership(projectId, thread.cwd);
287
+ this.workspaces.assertOwnership(projectId, thread.cwd, thread.id);
243
288
  if (!data.some((entry) => entry.id === thread.id)) data.unshift(this.threadView(thread));
244
289
  }
245
290
  }
@@ -264,7 +309,7 @@ class GatewayService {
264
309
  this.archiveChanges.add(threadId);
265
310
  try {
266
311
  const actual = await this.codex.readThreadMetadata(threadId);
267
- this.projects.assertOwnership(thread.projectId, actual.cwd);
312
+ this.workspaces.assertOwnership(thread.projectId, actual.cwd, thread.id);
268
313
  const current = this.projection.get(threadId);
269
314
  if (input.archived && (this.resumes.has(threadId) || this.store.activeForThread(threadId) || current.managed && current.status === "unknown" || [actual, current].some(
270
315
  (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 +373,7 @@ class GatewayService {
328
373
  actual = await this.codex.readThreadMetadata(threadId);
329
374
  if (actual.id !== threadId || this.codex.epoch !== epoch || !this.codex.ready)
330
375
  throw new Error("\u4F1A\u8BDD\u6216 Codex \u5B9E\u4F8B\u5DF2\u53D8\u5316");
331
- this.projects.assertOwnership(thread.projectId, actual.cwd);
376
+ this.workspaces.assertOwnership(thread.projectId, actual.cwd, thread.id);
332
377
  } catch (cause) {
333
378
  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
379
  }
@@ -376,6 +421,7 @@ class GatewayService {
376
421
  const thread = this.projection.get(threadId);
377
422
  if (!thread || Boolean(thread.archived) === archived) return;
378
423
  thread.archived = archived;
424
+ this.archiveRevisions.set(threadId, (this.archiveRevisions.get(threadId) ?? 0) + 1);
379
425
  if (archived) this.releaseThreadOwnership(threadId);
380
426
  this.projection.touch(threadId);
381
427
  if (!this.archiveChanges.has(threadId)) this.publishThread(threadId);
@@ -383,9 +429,9 @@ class GatewayService {
383
429
  async getThreadMetadata(threadId) {
384
430
  const cached = this.projection.get(threadId);
385
431
  if (cached) {
386
- this.projects.assertOwnership(cached.projectId, cached.cwd);
432
+ this.workspaces.assertOwnership(cached.projectId, cached.cwd, cached.id);
387
433
  if (this.codex.ready) await this.refreshThreadMetadata(threadId);
388
- this.projects.assertOwnership(cached.projectId, cached.cwd);
434
+ this.workspaces.assertOwnership(cached.projectId, cached.cwd, cached.id);
389
435
  return { ...this.threadView(cached), turns: [], actions: [] };
390
436
  }
391
437
  const metadata = this.threadView(this.normalize(await this.codex.readThreadMetadata(threadId)));
@@ -396,7 +442,7 @@ class GatewayService {
396
442
  await this.loadThread(threadId);
397
443
  if (cached && this.codex.ready) await this.refreshThreadMetadata(threadId);
398
444
  const thread = this.projection.get(threadId);
399
- this.projects.assertOwnership(thread.projectId, thread.cwd);
445
+ this.workspaces.assertOwnership(thread.projectId, thread.cwd, thread.id);
400
446
  return this.threadView(thread);
401
447
  }
402
448
  async getThreadHistory(threadId, cursor) {
@@ -404,11 +450,11 @@ class GatewayService {
404
450
  throw new AppError("VALIDATION_FAILED", "Missing or invalid history cursor", 400);
405
451
  this.ensureReady();
406
452
  const metadata = this.normalize(await this.codex.readThreadMetadata(threadId));
407
- this.projects.assertOwnership(metadata.projectId, metadata.cwd);
453
+ this.workspaces.assertOwnership(metadata.projectId, metadata.cwd, metadata.id);
408
454
  if (!this.codex.readThreadPage)
409
455
  throw new AppError("HISTORY_UNAVAILABLE", "\u5F53\u524D Codex \u4E0D\u652F\u6301\u5386\u53F2\u5206\u9875", 409);
410
456
  const page = this.normalize(await this.codex.readThreadPage(threadId, cursor));
411
- this.projects.assertOwnership(page.projectId, page.cwd);
457
+ this.workspaces.assertOwnership(page.projectId, page.cwd, page.id);
412
458
  return page;
413
459
  }
414
460
  async exportThread(threadId) {
@@ -427,8 +473,8 @@ class GatewayService {
427
473
  409
428
474
  );
429
475
  const link = this.store.getThreadLink(threadId);
430
- if (link) this.projects.assertOwnership(link.projectId, thread.cwd);
431
- else this.projects.findOwner(thread.cwd);
476
+ if (link) this.workspaces.assertOwnership(link.projectId, thread.cwd, thread.id);
477
+ else this.workspaces.findOwner(thread.cwd, thread.id);
432
478
  return renderThreadExport(thread, {
433
479
  executionUncertain: Boolean(this.store.activeForThread(threadId)),
434
480
  historyChanged: revision !== this.projection.revision(threadId) || thread.updatedAt !== latest.updatedAt || thread.status !== latest.status
@@ -449,21 +495,23 @@ class GatewayService {
449
495
  this.normalize(await this.codex.readThreadMetadata(threadId));
450
496
  link = this.store.getThreadLink(threadId);
451
497
  }
452
- const { project, cwd } = this.projects.assertOwnership(link.projectId, link.cwd);
498
+ const ownership = this.workspaces.assertOwnership(link.projectId, link.cwd, threadId);
499
+ const { cwd } = ownership;
453
500
  const file = await read(path, {
454
501
  cwd,
502
+ excludedRoots: this.workspaces.allocations().filter((row) => row.threadId !== threadId).map((row) => row.canonicalRoot),
455
503
  roots: [
456
- { canonicalRoot: project.canonicalRoot, aliases: [project.rootPath] },
457
- ...this.fileTemporaryRoots
504
+ { canonicalRoot: ownership.canonicalRoot, aliases: [ownership.rootPath] },
505
+ ...link.projectId === null ? [] : this.fileTemporaryRoots
458
506
  ]
459
507
  });
460
- this.projects.assertOwnership(link.projectId, link.cwd);
508
+ this.workspaces.assertOwnership(link.projectId, link.cwd, threadId);
461
509
  return file;
462
510
  }
463
511
  async loadThread(threadId) {
464
512
  const cached = this.projection.get(threadId);
465
513
  if (cached) {
466
- this.projects.assertOwnership(cached.projectId, cached.cwd);
514
+ this.workspaces.assertOwnership(cached.projectId, cached.cwd, cached.id);
467
515
  const marker = this.store.activeForThread(threadId);
468
516
  if (marker?.turnId && this.codex.ready) {
469
517
  await this.refreshExecution(marker);
@@ -498,18 +546,26 @@ class GatewayService {
498
546
  }
499
547
  }
500
548
  async createThread(projectId, input) {
501
- return this.perform("thread.create", projectId, input, async () => {
502
- const project = this.projects.get(projectId);
549
+ return this.perform("thread.create", projectId ?? UNASSIGNED_SCOPE, input, async () => {
503
550
  this.ensureReady();
504
551
  this.ensureCompatibilityConfirmed();
552
+ const cwd = projectId === null ? this.workspaces.allocate(input.clientOperationId).cwd : this.projects.get(projectId).canonicalRoot;
553
+ if (projectId !== null) this.workspaces.assertProjectDirectory(cwd);
505
554
  this.store.associateProject(input.clientOperationId, projectId);
506
555
  this.store.dispatch(input.clientOperationId);
507
- this.pendingThreadCreations.set(input.clientOperationId, project.canonicalRoot);
556
+ this.pendingThreadCreations.set(input.clientOperationId, cwd);
508
557
  const epoch = this.codex.epoch;
509
558
  const settingsSequence = this.runtimeSettingsSequence;
510
- const thread = await this.codex.createThread(project.canonicalRoot, input.settings);
559
+ const thread = await this.codex.createThread(cwd, input.settings);
511
560
  if (this.codex.epoch !== epoch || !this.codex.ready)
512
561
  throw new Error("\u521B\u5EFA\u8BF7\u6C42\u5DF2\u53D1\u9001\uFF0C\u4F46 Codex \u5B9E\u4F8B\u5DF2\u53D8\u5316\uFF0C\u8BF7\u6838\u5BF9\u5386\u53F2");
562
+ if (projectId === null) {
563
+ try {
564
+ this.workspaces.bind(input.clientOperationId, thread.id, thread.cwd);
565
+ } catch (cause) {
566
+ 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 });
567
+ }
568
+ }
513
569
  const normalized = this.normalize(thread, projectId, true);
514
570
  this.projection.thread(
515
571
  normalized,
@@ -526,7 +582,7 @@ class GatewayService {
526
582
  this.ensureReady();
527
583
  this.ensureArchiveAvailable(thread);
528
584
  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);
585
+ this.workspaces.assertOwnership(thread.projectId, thread.cwd, thread.id);
530
586
  this.store.associateProject(input.clientOperationId, thread.projectId);
531
587
  const link = this.store.getThreadLink(threadId);
532
588
  if (link?.managedEpoch === this.codex.epoch) {
@@ -540,7 +596,7 @@ class GatewayService {
540
596
  operationId: marker.operationId,
541
597
  threadId
542
598
  });
543
- const ownership = this.projects.assertOwnership(thread.projectId, thread.cwd);
599
+ const ownership = this.workspaces.assertOwnership(thread.projectId, thread.cwd, thread.id);
544
600
  this.ensureCompatibilityConfirmed();
545
601
  this.store.dispatch(input.clientOperationId);
546
602
  let pending = this.resumes.get(threadId);
@@ -585,21 +641,29 @@ class GatewayService {
585
641
  this.ensureReady();
586
642
  const source = this.projection.get(threadId) ?? await this.codex.readThread(threadId);
587
643
  const link = this.store.getThreadLink(threadId);
588
- const ownership = link ? this.projects.assertOwnership(link.projectId, source.cwd) : this.projects.findOwner(source.cwd);
644
+ const ownership = link ? this.workspaces.assertOwnership(link.projectId, source.cwd, threadId) : this.workspaces.findOwner(source.cwd, source.id);
589
645
  this.ensureCompatibilityConfirmed();
590
646
  const epoch = this.codex.epoch;
591
- this.store.associateProject(input.clientOperationId, ownership.project.id);
647
+ let forkCwd = ownership.cwd;
648
+ if (ownership.projectId === null) {
649
+ forkCwd = this.workspaces.allocate(input.clientOperationId).cwd;
650
+ await cp(ownership.cwd, forkCwd, { recursive: true, verbatimSymlinks: true });
651
+ this.workspaces.assertOwnership(null, source.cwd, source.id);
652
+ }
653
+ this.store.associateProject(input.clientOperationId, ownership.projectId);
592
654
  this.store.dispatch(input.clientOperationId);
593
- this.pendingThreadCreations.set(input.clientOperationId, ownership.cwd);
655
+ this.pendingThreadCreations.set(input.clientOperationId, forkCwd);
594
656
  const settingsSequence = this.runtimeSettingsSequence;
595
- const forked = await this.codex.forkThread(threadId, ownership.cwd, input.settings);
657
+ const forked = await this.codex.forkThread(threadId, forkCwd, input.settings);
596
658
  if (forked.id === threadId || this.codex.epoch !== epoch || !this.codex.ready)
597
659
  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
660
  let normalized;
599
661
  try {
662
+ if (ownership.projectId === null)
663
+ this.workspaces.bind(input.clientOperationId, forked.id, forked.cwd);
600
664
  normalized = this.normalize(
601
665
  { ...forked, forkedFromId: threadId },
602
- ownership.project.id,
666
+ ownership.projectId,
603
667
  true
604
668
  );
605
669
  } catch (error) {
@@ -618,7 +682,7 @@ class GatewayService {
618
682
  return this.perform("turn.start", threadId, input, async () => {
619
683
  const thread = await this.loadThread(threadId);
620
684
  this.ensureManaged(thread);
621
- const ownership = this.projects.assertOwnership(thread.projectId, thread.cwd);
685
+ const ownership = this.workspaces.assertOwnership(thread.projectId, thread.cwd, thread.id);
622
686
  if (isActive(thread.status))
623
687
  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
688
  this.ensureCompatibilityConfirmed();
@@ -651,7 +715,7 @@ class GatewayService {
651
715
  return this.perform("thread.compact", threadId, input, async () => {
652
716
  const thread = await this.loadThread(threadId);
653
717
  this.ensureManaged(thread);
654
- const ownership = this.projects.assertOwnership(thread.projectId, thread.cwd);
718
+ const ownership = this.workspaces.assertOwnership(thread.projectId, thread.cwd, thread.id);
655
719
  if (isActive(thread.status) || thread.turns.some((turn) => isActive(turn.status)))
656
720
  throw new AppError("THREAD_BUSY", "\u8BF7\u7B49\u5F85\u5F53\u524D\u4EFB\u52A1\u7ED3\u675F\u540E\u518D\u538B\u7F29\u4E0A\u4E0B\u6587", 409);
657
721
  if (!thread.turns.length)
@@ -675,7 +739,7 @@ class GatewayService {
675
739
  return this.perform("review.start", threadId, input, async () => {
676
740
  const thread = await this.loadThread(threadId);
677
741
  this.ensureManaged(thread);
678
- const ownership = this.projects.assertOwnership(thread.projectId, thread.cwd);
742
+ const ownership = this.workspaces.assertOwnership(thread.projectId, thread.cwd, thread.id);
679
743
  if (isActive(thread.status) || thread.turns.some((turn2) => isActive(turn2.status)))
680
744
  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
745
  threadId
@@ -774,8 +838,8 @@ class GatewayService {
774
838
  try {
775
839
  const actual = await this.codex.readThread(marker.threadId);
776
840
  const link = this.store.getThreadLink(marker.threadId);
777
- if (link) this.projects.assertOwnership(link.projectId, actual.cwd);
778
- else this.projects.findOwner(actual.cwd);
841
+ if (link) this.workspaces.assertOwnership(link.projectId, actual.cwd, actual.id);
842
+ else this.workspaces.findOwner(actual.cwd, actual.id);
779
843
  if (this.markerIsActive(actual, marker.turnId))
780
844
  throw new AppError("THREAD_BUSY", "\u4E0A\u6E38\u4ECD\u62A5\u544A\u8BE5\u4EFB\u52A1\u6D3B\u52A8\uFF0C\u4E0D\u80FD\u89E3\u9664\u5360\u7528", 409, {
781
845
  threadId: marker.threadId
@@ -810,6 +874,7 @@ class GatewayService {
810
874
  }
811
875
  async shutdown() {
812
876
  this.accepting = false;
877
+ await this.projectClones.close();
813
878
  this.store.recover();
814
879
  this.projection.runtimeReset();
815
880
  this.removeListener();
@@ -870,14 +935,14 @@ class GatewayService {
870
935
  }
871
936
  normalize(thread, projectId, managed = false) {
872
937
  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)
938
+ 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);
939
+ if (projectId !== void 0 && ownership.projectId !== projectId)
875
940
  throw new AppError("PROJECT_OWNERSHIP", "\u4F1A\u8BDD\u5C5E\u4E8E\u5176\u4ED6\u9879\u76EE\uFF0C\u4E0D\u80FD\u6539\u53D8\u9879\u76EE\u5F52\u5C5E", 403);
876
941
  const managedEpoch = thread.archived ? null : managed ? this.codex.epoch : link?.managedEpoch ?? null;
877
942
  const result = {
878
943
  ...thread,
879
944
  turns: this.turnUsage.restore(thread.id, thread.turns),
880
- projectId: ownership.project.id,
945
+ projectId: ownership.projectId,
881
946
  cwd: ownership.cwd,
882
947
  managed: managedEpoch === this.codex.epoch,
883
948
  resumeError: managed ? void 0 : this.projection.get(thread.id)?.resumeError,
@@ -887,7 +952,7 @@ class GatewayService {
887
952
  };
888
953
  this.store.linkThread({
889
954
  threadId: thread.id,
890
- projectId: ownership.project.id,
955
+ projectId: ownership.projectId,
891
956
  cwd: ownership.cwd,
892
957
  source: thread.source,
893
958
  managedEpoch
@@ -922,7 +987,7 @@ class GatewayService {
922
987
  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
988
  return;
924
989
  const link = this.store.getThreadLink(marker.threadId);
925
- const ownership = link ? this.projects.assertOwnership(link.projectId, actual.cwd) : this.projects.findOwner(actual.cwd);
990
+ const ownership = link ? this.workspaces.assertOwnership(link.projectId, actual.cwd, actual.id) : this.workspaces.findOwner(actual.cwd, actual.id);
926
991
  if (ownership.workspaceKey !== marker.workspaceKey) return;
927
992
  const turn = actual.turns.find((entry) => entry.id === marker.turnId);
928
993
  if (turn && isActive(turn.status) && latest.state === "active" && this.projection.get(marker.threadId)?.status === "unknown") {
@@ -1072,7 +1137,7 @@ class GatewayService {
1072
1137
  const encoded = JSON.stringify(summary);
1073
1138
  if (this.summaries.get(threadId) === encoded) return;
1074
1139
  try {
1075
- this.projects.assertOwnership(thread.projectId, thread.cwd);
1140
+ this.workspaces.assertOwnership(thread.projectId, thread.cwd, thread.id);
1076
1141
  } catch {
1077
1142
  return;
1078
1143
  }
@@ -1120,7 +1185,7 @@ class GatewayService {
1120
1185
  const thread = await this.codex.readThreadMetadata(threadId);
1121
1186
  const current = this.projection.get(threadId);
1122
1187
  if (!current || thread.id !== threadId) return;
1123
- this.projects.assertOwnership(current.projectId, thread.cwd);
1188
+ this.workspaces.assertOwnership(current.projectId, thread.cwd, thread.id);
1124
1189
  const settings = thread.runtimeSettings ? { ...current.runtimeSettings, ...thread.runtimeSettings } : void 0;
1125
1190
  if (this.titleReads.get(threadId) === sequence && this.projection.revision(threadId) === revision && settings && JSON.stringify(current.runtimeSettings) !== JSON.stringify(settings)) {
1126
1191
  current.runtimeSettings = settings;
@@ -1225,7 +1290,7 @@ class GatewayService {
1225
1290
  this.projection.expireTurn(thread.id, event.turn.id);
1226
1291
  if (newCompletion) {
1227
1292
  try {
1228
- this.projects.assertOwnership(thread.projectId, thread.cwd);
1293
+ this.workspaces.assertOwnership(thread.projectId, thread.cwd, thread.id);
1229
1294
  this.events.publishCompletion({
1230
1295
  threadId: thread.id,
1231
1296
  turnId: event.turn.id,