@covro/covro 0.5.1-dev.2 → 0.5.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 (127) hide show
  1. package/README.md +11 -1
  2. package/README.zh-CN.md +11 -1
  3. package/dist/codex/adapter.js +65 -5
  4. package/dist/codex/adapter.js.map +2 -2
  5. package/dist/codex/collaboration.js +28 -0
  6. package/dist/codex/collaboration.js.map +7 -0
  7. package/dist/codex/guardian-denials.js +67 -0
  8. package/dist/codex/guardian-denials.js.map +7 -0
  9. package/dist/codex/mapping.js +10 -1
  10. package/dist/codex/mapping.js.map +2 -2
  11. package/dist/codex/thread-goals.js +15 -0
  12. package/dist/codex/thread-goals.js.map +7 -0
  13. package/dist/codex/thread-titles.js +135 -60
  14. package/dist/codex/thread-titles.js.map +2 -2
  15. package/dist/server/capacity-retry.js +176 -0
  16. package/dist/server/capacity-retry.js.map +7 -0
  17. package/dist/server/db/operations.js +11 -0
  18. package/dist/server/db/operations.js.map +2 -2
  19. package/dist/server/http.js +51 -0
  20. package/dist/server/http.js.map +2 -2
  21. package/dist/server/project-init.js +28 -0
  22. package/dist/server/project-init.js.map +7 -0
  23. package/dist/server/projection.js +11 -3
  24. package/dist/server/projection.js.map +2 -2
  25. package/dist/server/service.js +315 -8
  26. package/dist/server/service.js.map +2 -2
  27. package/dist/server/thread-recap.js +34 -0
  28. package/dist/server/thread-recap.js.map +7 -0
  29. package/dist/server/workspace-diff.js +149 -0
  30. package/dist/server/workspace-diff.js.map +7 -0
  31. package/dist/shared/guardian-denials.js +1 -0
  32. package/dist/shared/guardian-denials.js.map +7 -0
  33. package/dist/shared/thread-goal.js +1 -0
  34. package/dist/shared/thread-goal.js.map +7 -0
  35. package/dist/shared/turn-errors.js +25 -0
  36. package/dist/shared/turn-errors.js.map +7 -0
  37. package/dist/shared/validation.js +53 -3
  38. package/dist/shared/validation.js.map +2 -2
  39. package/dist/shared/workspace-diff.js +1 -0
  40. package/dist/shared/workspace-diff.js.map +7 -0
  41. package/dist/web/assets/{abnfDiagram-WEYOIA3C-DK1b1m5g.js → abnfDiagram-WEYOIA3C-2_00Zp4O.js} +1 -1
  42. package/dist/web/assets/{arc-0hKPa6cH.js → arc-DAyuVDVU.js} +1 -1
  43. package/dist/web/assets/{architecture-WOLXFQ4H-DUa2ELbW.js → architecture-WOLXFQ4H-CgR1C4gR.js} +1 -1
  44. package/dist/web/assets/{architectureDiagram-ZHVYKT53-DfpAMcHI.js → architectureDiagram-ZHVYKT53-Dkb6AWjz.js} +1 -1
  45. package/dist/web/assets/{blockDiagram-RJAE7QA3-CD8LlPAk.js → blockDiagram-RJAE7QA3-NtOGZdkX.js} +1 -1
  46. package/dist/web/assets/{c4Diagram-XDPPPE54-KVKGUBPP.js → c4Diagram-XDPPPE54-C1dqWKb1.js} +1 -1
  47. package/dist/web/assets/channel-Co9_v2dk.js +1 -0
  48. package/dist/web/assets/{chunk-2E7WHVMD-CPcfaznm.js → chunk-2E7WHVMD-ClzuEiGk.js} +1 -1
  49. package/dist/web/assets/{chunk-4EMNKR2T-Ba4ZkZos.js → chunk-4EMNKR2T-DyTpkWag.js} +1 -1
  50. package/dist/web/assets/{chunk-55XCU76K-COU6aQoV.js → chunk-55XCU76K-DXBA4ib1.js} +1 -1
  51. package/dist/web/assets/{chunk-7S6XSTFO-mof6va94.js → chunk-7S6XSTFO-DA_aq0zc.js} +1 -1
  52. package/dist/web/assets/{chunk-AQDTSKZB-B9S7XD0k.js → chunk-AQDTSKZB-BWTN3SC_.js} +1 -1
  53. package/dist/web/assets/{chunk-DSNLSHJZ-DiTTjo21.js → chunk-DSNLSHJZ-DJnLTeE9.js} +1 -1
  54. package/dist/web/assets/{chunk-HAGFOTED-bBaCv2Gr.js → chunk-HAGFOTED-cDP-g1Eh.js} +1 -1
  55. package/dist/web/assets/{chunk-HNE4Q36P-COubV5SQ.js → chunk-HNE4Q36P-B3TTIYd_.js} +1 -1
  56. package/dist/web/assets/{chunk-NQOKRVIF-D7mCDLmx.js → chunk-NQOKRVIF-D8mmhZHq.js} +1 -1
  57. package/dist/web/assets/{chunk-UHW7PFBP-7E_Y7QMq.js → chunk-UHW7PFBP-DWIIg_jq.js} +1 -1
  58. package/dist/web/assets/{chunk-VQ5W62QI-DIe2sxBO.js → chunk-VQ5W62QI-Bu3R2cpS.js} +1 -1
  59. package/dist/web/assets/{chunk-WQQL4CDK-sGc70NW0.js → chunk-WQQL4CDK-BxsR1mXs.js} +1 -1
  60. package/dist/web/assets/{chunk-XXDRQBXY-CnTJfCp_.js → chunk-XXDRQBXY-Ds0JNdm-.js} +1 -1
  61. package/dist/web/assets/{chunk-YZO67HFG-D1uBxw_D.js → chunk-YZO67HFG-COpqyQaD.js} +1 -1
  62. package/dist/web/assets/{chunk-ZAUX2LTX-quytYPeh.js → chunk-ZAUX2LTX-BUrGjDuS.js} +1 -1
  63. package/dist/web/assets/{chunk-ZCHMQSOR-CebhdtBu.js → chunk-ZCHMQSOR-DYMM9mEK.js} +1 -1
  64. package/dist/web/assets/{classDiagram-v2-4MYWR3UA-_NujQwSa.js → classDiagram-v2-4MYWR3UA-DfJvks0C.js} +1 -1
  65. package/dist/web/assets/{cose-bilkent-JH36ORCC-DltmODTL.js → cose-bilkent-JH36ORCC-CN5mpYIb.js} +1 -1
  66. package/dist/web/assets/{cynefin-EF2NZ3EQ-CyNUtvfu.js → cynefin-EF2NZ3EQ-DmlCHkY-.js} +1 -1
  67. package/dist/web/assets/{cynefinDiagram-WF24UDRP-DH1m77PD.js → cynefinDiagram-WF24UDRP-C0aUblC2.js} +1 -1
  68. package/dist/web/assets/{dagre-NK4GIZVX-DiCwjat3.js → dagre-NK4GIZVX-Ba7RcAgP.js} +1 -1
  69. package/dist/web/assets/{diagram-CBK3MWTC-DnoBkry8.js → diagram-CBK3MWTC-BYQDjB9i.js} +1 -1
  70. package/dist/web/assets/{diagram-I5XIIV2D-CIeRHNwj.js → diagram-I5XIIV2D-EV79hoob.js} +1 -1
  71. package/dist/web/assets/{diagram-ODIL2UCW-BiyJagHP.js → diagram-ODIL2UCW-BvQdwmgA.js} +1 -1
  72. package/dist/web/assets/{diagram-PFBEJUXE-B6xGLENr.js → diagram-PFBEJUXE-hxwI3F00.js} +1 -1
  73. package/dist/web/assets/{diagram-RJA3I4VV-DCbGrXsR.js → diagram-RJA3I4VV-DwaHE2iB.js} +1 -1
  74. package/dist/web/assets/{diagram-TYUNFSNR-BjYXlBMa.js → diagram-TYUNFSNR-BxiOemA0.js} +1 -1
  75. package/dist/web/assets/{dist-C9Qd1YEe.js → dist-nfRX7L9k.js} +1 -1
  76. package/dist/web/assets/{ebnfDiagram-V4P3TTPT-CpRXdAa6.js → ebnfDiagram-V4P3TTPT-CzQv5iDM.js} +1 -1
  77. package/dist/web/assets/{elk-QONTBCTQ-BZa9Vnbc.js → elk-QONTBCTQ-DNR9HWcl.js} +1 -1
  78. package/dist/web/assets/{erDiagram-KYQIGQAA-7AaYG_a9.js → erDiagram-KYQIGQAA-aJlTdb9w.js} +1 -1
  79. package/dist/web/assets/{eventmodeling-K75KTNOO-BzIg1KNf.js → eventmodeling-K75KTNOO-D4J-ujy-.js} +1 -1
  80. package/dist/web/assets/flowDiagram-IUGLLWBU-Bq7dmPlS.js +1 -0
  81. package/dist/web/assets/{ganttDiagram-R7GPNTJY-DeNg-7K2.js → ganttDiagram-R7GPNTJY-DgytSfqO.js} +1 -1
  82. package/dist/web/assets/{gitGraph-VSP46ZUC-BR-A7hdM.js → gitGraph-VSP46ZUC-BtjueFHB.js} +1 -1
  83. package/dist/web/assets/{gitGraphDiagram-PDYL7XHY-LMyLpeqK.js → gitGraphDiagram-PDYL7XHY-WP0Hjdh9.js} +1 -1
  84. package/dist/web/assets/index-BesPYgjq.js +156 -0
  85. package/dist/web/assets/{index-CEM8pkOB.css → index-qYBdGqKM.css} +1 -1
  86. package/dist/web/assets/{info-OHQRW6UA-CRbCGNnm.js → info-OHQRW6UA-iuP0WTfn.js} +1 -1
  87. package/dist/web/assets/{infoDiagram-MVDRKAX5-DTUWae1O.js → infoDiagram-MVDRKAX5-CpAtMyKS.js} +1 -1
  88. package/dist/web/assets/{ishikawaDiagram-HPEHV2GE-CE4r0jTM.js → ishikawaDiagram-HPEHV2GE-JTnItv7N.js} +1 -1
  89. package/dist/web/assets/{journeyDiagram-BOSHW7O7-KcSuVnTV.js → journeyDiagram-BOSHW7O7-B333k1QJ.js} +1 -1
  90. package/dist/web/assets/{kanban-definition-FNSA4HC5-I0QF1Ftr.js → kanban-definition-FNSA4HC5-DpCk5ZH0.js} +1 -1
  91. package/dist/web/assets/{line-DLSy1-se.js → line-CLs-qFxt.js} +1 -1
  92. package/dist/web/assets/{linear-XWNSUdmR.js → linear-E9AWqBNs.js} +1 -1
  93. package/dist/web/assets/{mermaid-parser.core-BcMfaJSH.js → mermaid-parser.core-D1rEzlb-.js} +3 -3
  94. package/dist/web/assets/{mermaid.core-CPl3cZ55.js → mermaid.core-D6TNGGsX.js} +4 -4
  95. package/dist/web/assets/{mindmap-definition-6OY57UG3-BIB4ahHf.js → mindmap-definition-6OY57UG3-DrpuGWuc.js} +1 -1
  96. package/dist/web/assets/{packet-JDAUHWVQ-UZicf9-F.js → packet-JDAUHWVQ-BpBr3aKk.js} +1 -1
  97. package/dist/web/assets/{pegDiagram-ECIUJNBX-ckOqIfTt.js → pegDiagram-ECIUJNBX-BrZX1cdv.js} +1 -1
  98. package/dist/web/assets/{pie-XZMESJXO-DErhKWJX.js → pie-XZMESJXO-CcTpis-K.js} +1 -1
  99. package/dist/web/assets/{pieDiagram-GNMPY74T-CrnS_2qe.js → pieDiagram-GNMPY74T-CR2em875.js} +1 -1
  100. package/dist/web/assets/{quadrantDiagram-D57K4MC3-UK4DsDJz.js → quadrantDiagram-D57K4MC3-D-ET5xbH.js} +1 -1
  101. package/dist/web/assets/{radar-ABXABTNO-BYgJm4I9.js → radar-ABXABTNO-CTFFPCi_.js} +1 -1
  102. package/dist/web/assets/{railroad-I3PHUGI6-2n6yUb7T.js → railroad-I3PHUGI6-S6MHolw_.js} +1 -1
  103. package/dist/web/assets/{railroad-abnf-LNEFI6M7-CGAFqTzs.js → railroad-abnf-LNEFI6M7-NWxYLYMN.js} +1 -1
  104. package/dist/web/assets/{railroad-ebnf-DWYD2UWJ-gJx_FXYW.js → railroad-ebnf-DWYD2UWJ-CRnaPl26.js} +1 -1
  105. package/dist/web/assets/{railroad-peg-JR7BVNR7-Dm0kEaxK.js → railroad-peg-JR7BVNR7-CB85ffhO.js} +1 -1
  106. package/dist/web/assets/{railroadDiagram-Y72NRBL7-CqzySn4U.js → railroadDiagram-Y72NRBL7-Ba2xIF7q.js} +1 -1
  107. package/dist/web/assets/{requirementDiagram-EQYSW5RA-B33Hrc0L.js → requirementDiagram-EQYSW5RA-B7faqgDT.js} +1 -1
  108. package/dist/web/assets/{sankeyDiagram-SY24JHO5-BA9Smc0j.js → sankeyDiagram-SY24JHO5-IjJVI58S.js} +1 -1
  109. package/dist/web/assets/{sequenceDiagram-BLEHNHQU-CUA-rKot.js → sequenceDiagram-BLEHNHQU-D-h5jxwQ.js} +1 -1
  110. package/dist/web/assets/{src-htchAh5W.js → src-hBdSvoFJ.js} +1 -1
  111. package/dist/web/assets/{stateDiagram-v2-A6IESKWZ-kIYMkX3_.js → stateDiagram-v2-A6IESKWZ-rlZaPFqZ.js} +1 -1
  112. package/dist/web/assets/{swimlanes-B3ENRKFQ-C8Lzsqig.js → swimlanes-B3ENRKFQ-CqgKwSbP.js} +1 -1
  113. package/dist/web/assets/swimlanesDiagram-AYQRV4AK-BGvWQ2Hp.js +8 -0
  114. package/dist/web/assets/{timeline-definition-JXYKGLGB-C3uOHb1e.js → timeline-definition-JXYKGLGB-DD76ZtY0.js} +1 -1
  115. package/dist/web/assets/{treeView-D4JQ5SDB-Dq-ug1SE.js → treeView-D4JQ5SDB-CcwWh4Bw.js} +1 -1
  116. package/dist/web/assets/{treemap-SAJKECNS-mX01fnXB.js → treemap-SAJKECNS-8lde6V5O.js} +1 -1
  117. package/dist/web/assets/{usecaseDiagram-WPOLBNNX-DMBep2-D.js → usecaseDiagram-WPOLBNNX-CWowobZv.js} +1 -1
  118. package/dist/web/assets/{vennDiagram-Q3Y3IDCW-CUardqBP.js → vennDiagram-Q3Y3IDCW-U11W8pHe.js} +1 -1
  119. package/dist/web/assets/{wardley-7MLQ67FV-CIOqSz29.js → wardley-7MLQ67FV-Dp2EaBmo.js} +1 -1
  120. package/dist/web/assets/{wardleyDiagram-IVAIDKWA-DtfU2fzi.js → wardleyDiagram-IVAIDKWA-7Qnos8ak.js} +1 -1
  121. package/dist/web/assets/{xychartDiagram-4URNHDU4-Cb3se94q.js → xychartDiagram-4URNHDU4-BwfHNtsG.js} +1 -1
  122. package/dist/web/index.html +2 -2
  123. package/package.json +3 -3
  124. package/dist/web/assets/channel-CpEW0zog.js +0 -1
  125. package/dist/web/assets/flowDiagram-IUGLLWBU-FDG_7_Jf.js +0 -1
  126. package/dist/web/assets/index-D2dQZgkr.js +0 -147
  127. package/dist/web/assets/swimlanesDiagram-AYQRV4AK-CWrPdxiH.js +0 -8
@@ -1,6 +1,12 @@
1
+ import { CapacityRetries } from "./capacity-retry.js";
2
+ import { isModelCapacityError } from "../shared/turn-errors.js";
3
+ import { randomUUID } from "node:crypto";
1
4
  import { attentionKey, pendingAttention } from "../shared/thread-attention.js";
2
5
  import { ShellService } from "./shell.js";
6
+ import { assertProjectInstructionsMissing, PROJECT_INIT_PROMPT } from "./project-init.js";
7
+ import { recapInput } from "./thread-recap.js";
3
8
  import { readGithubRepository } from "./github-repository.js";
9
+ import { readWorkspaceDiff } from "./workspace-diff.js";
4
10
  import { realpathSync } from "node:fs";
5
11
  import { cp } from "node:fs/promises";
6
12
  import { WorkspaceRegistry } from "./workspaces.js";
@@ -13,6 +19,7 @@ import {
13
19
  } from "../shared/elicitation.js";
14
20
  import { toThreadSummary } from "../shared/thread-summary.js";
15
21
  import {
22
+ requestDigest,
16
23
  StoreConflict
17
24
  } from "./db/operations.js";
18
25
  import { EventHub } from "./events.js";
@@ -49,11 +56,16 @@ class GatewayService {
49
56
  this.shell = new ShellService(this.codex);
50
57
  this.events = new EventHub(options.limits?.eventBytes);
51
58
  this.projection = new Projection(options.limits?.itemBytes);
59
+ this.capacityRetries = new CapacityRetries(
60
+ (job) => this.dispatchCapacityRetry(job),
61
+ (id) => this.publishThread(id)
62
+ );
52
63
  this.currentEpoch = this.codex.epoch;
53
64
  this.store.recover();
54
65
  this.removeListener = this.codex.onEvent((event) => this.consume(event));
55
66
  }
56
67
  options;
68
+ capacityRetries;
57
69
  projectClones;
58
70
  shell;
59
71
  events;
@@ -74,6 +86,7 @@ class GatewayService {
74
86
  archiveRevisions = /* @__PURE__ */ new Map();
75
87
  turnBaselines = /* @__PURE__ */ new Map();
76
88
  renameChanges = /* @__PURE__ */ new Set();
89
+ goalWrites = /* @__PURE__ */ new Set();
77
90
  reads = /* @__PURE__ */ new Map();
78
91
  resumes = /* @__PURE__ */ new Map();
79
92
  executionReads = /* @__PURE__ */ new Map();
@@ -86,6 +99,7 @@ class GatewayService {
86
99
  runtimeSettingsUpdates = /* @__PURE__ */ new Map();
87
100
  inFlightOperations = /* @__PURE__ */ new Set();
88
101
  pendingThreadCreations = /* @__PURE__ */ new Map();
102
+ capacityLinks = /* @__PURE__ */ new Map();
89
103
  listProjects() {
90
104
  return this.projects.list();
91
105
  }
@@ -95,6 +109,29 @@ class GatewayService {
95
109
  this.projects.get(projectId);
96
110
  return { url };
97
111
  }
112
+ async getProjectDiff(projectId) {
113
+ const project = this.projects.get(projectId);
114
+ const result = await readWorkspaceDiff(
115
+ project.canonicalRoot,
116
+ this.workspaces.allocations().map((row) => row.canonicalRoot)
117
+ );
118
+ this.projects.get(projectId);
119
+ return result;
120
+ }
121
+ async getThreadDiff(threadId) {
122
+ let link = this.store.getThreadLink(threadId);
123
+ if (!link) {
124
+ this.normalize(await this.codex.readThreadMetadata(threadId));
125
+ link = this.store.getThreadLink(threadId);
126
+ }
127
+ const { cwd } = this.workspaces.assertOwnership(link.projectId, link.cwd, threadId);
128
+ const result = await readWorkspaceDiff(
129
+ cwd,
130
+ this.workspaces.allocations().filter((row) => row.threadId !== threadId).map((row) => row.canonicalRoot)
131
+ );
132
+ this.workspaces.assertOwnership(link.projectId, link.cwd, threadId);
133
+ return result;
134
+ }
98
135
  async getThreadGithubRepository(threadId) {
99
136
  let link = this.store.getThreadLink(threadId);
100
137
  if (!link) {
@@ -132,6 +169,8 @@ class GatewayService {
132
169
  return this.maintenanceReason === "backup" ? new AppError("BACKUP_MAINTENANCE", "\u6B63\u5728\u5907\u4EFD\uFF0C\u8BF7\u5B8C\u6210\u540E\u91CD\u8BD5\u3002", 503) : new AppError("UPDATE_MAINTENANCE", "\u6B63\u5728\u5347\u7EA7\uFF0C\u8BF7\u7B49\u5F85\u670D\u52A1\u91CD\u65B0\u8FDE\u63A5", 503);
133
170
  }
134
171
  prepareUpdate(reason = "update") {
172
+ if (this.capacityRetries.active())
173
+ return { ready: false, reason: "\u7B49\u5F85\u6A21\u578B\u5BB9\u91CF\u91CD\u8BD5\u7ED3\u675F\u6216\u53D6\u6D88" };
135
174
  if (this.projectClones.hasActive) return { ready: false, reason: "\u7B49\u5F85\u9879\u76EE\u514B\u9686\u5B8C\u6210\u6216\u53D6\u6D88" };
136
175
  if (this.shell.active()) return { ready: false, reason: "\u7B49\u5F85 Shell \u547D\u4EE4\u7ED3\u675F" };
137
176
  if (this.inFlightOperations.size || this.resumes.size || this.pendingThreadCreations.size)
@@ -179,7 +218,7 @@ class GatewayService {
179
218
  return link ? link.projectId === projectId : entry.workspaceKey === project.workspaceKey;
180
219
  });
181
220
  const active = this.projection.values().find(
182
- (thread) => thread.projectId === projectId && (isActive(thread.status) || thread.actions.some((action) => ["pending", "responding"].includes(action.status)))
221
+ (thread) => thread.projectId === projectId && (isActive(thread.status) || this.capacityRetries.pending(thread.id) || thread.actions.some((action) => ["pending", "responding"].includes(action.status)))
183
222
  );
184
223
  if (pending || marker || active || this.shell.active(projectId))
185
224
  throw new AppError(
@@ -453,21 +492,34 @@ class GatewayService {
453
492
  updateArchiveState(threadId, archived) {
454
493
  const thread = this.projection.get(threadId);
455
494
  if (!thread || Boolean(thread.archived) === archived) return;
495
+ if (archived) this.capacityRetries.clear(threadId);
456
496
  thread.archived = archived;
457
497
  this.archiveRevisions.set(threadId, (this.archiveRevisions.get(threadId) ?? 0) + 1);
458
498
  if (archived) this.releaseThreadOwnership(threadId);
459
499
  this.projection.touch(threadId);
460
500
  if (!this.archiveChanges.has(threadId)) this.publishThread(threadId);
461
501
  }
502
+ async refreshGoal(thread) {
503
+ if (thread.goal !== void 0 || !this.codex.ready || !this.codex.getGoal) return;
504
+ try {
505
+ const goal = await this.codex.getGoal(thread.id);
506
+ this.workspaces.assertOwnership(thread.projectId, thread.cwd, thread.id);
507
+ if (thread.goal === void 0) thread.goal = goal;
508
+ } catch {
509
+ }
510
+ }
462
511
  async getThreadMetadata(threadId) {
463
512
  const cached = this.projection.get(threadId);
464
513
  if (cached) {
465
514
  this.workspaces.assertOwnership(cached.projectId, cached.cwd, cached.id);
466
515
  if (this.codex.ready) await this.refreshThreadMetadata(threadId);
467
516
  this.workspaces.assertOwnership(cached.projectId, cached.cwd, cached.id);
517
+ await this.refreshGoal(cached);
468
518
  return { ...this.threadView(cached), turns: [], actions: [] };
469
519
  }
470
520
  const metadata = this.threadView(this.normalize(await this.codex.readThreadMetadata(threadId)));
521
+ await this.refreshGoal(metadata);
522
+ metadata.historyVisible = this.historyVisible(metadata);
471
523
  return { ...metadata, turns: [], actions: [] };
472
524
  }
473
525
  async getThread(threadId) {
@@ -476,6 +528,7 @@ class GatewayService {
476
528
  if (cached && this.codex.ready) await this.refreshThreadMetadata(threadId);
477
529
  const thread = this.projection.get(threadId);
478
530
  this.workspaces.assertOwnership(thread.projectId, thread.cwd, thread.id);
531
+ await this.refreshGoal(thread);
479
532
  return this.threadView(thread);
480
533
  }
481
534
  getThreadAttention(threadId) {
@@ -494,6 +547,11 @@ class GatewayService {
494
547
  throw new AppError("HISTORY_UNAVAILABLE", "\u5F53\u524D Codex \u4E0D\u652F\u6301\u5386\u53F2\u5206\u9875", 409);
495
548
  const page = this.normalize(await this.codex.readThreadPage(threadId, cursor));
496
549
  this.workspaces.assertOwnership(page.projectId, page.cwd, page.id);
550
+ const links = this.retryLinks(threadId);
551
+ for (const turn of page.turns) {
552
+ const root = links.get(turn.id);
553
+ if (root) turn.retryOf = root;
554
+ }
497
555
  return page;
498
556
  }
499
557
  async exportThread(threadId) {
@@ -620,6 +678,7 @@ class GatewayService {
620
678
  }
621
679
  async resumeThread(threadId, input) {
622
680
  return this.perform("thread.resume", threadId, input, async () => {
681
+ this.capacityRetries.clear(threadId);
623
682
  const thread = await this.loadThread(threadId);
624
683
  this.ensureReady();
625
684
  this.ensureArchiveAvailable(thread);
@@ -721,40 +780,218 @@ class GatewayService {
721
780
  });
722
781
  }
723
782
  async startTurn(threadId, input) {
783
+ return this.dispatchTurn(threadId, input);
784
+ }
785
+ async dispatchTurn(threadId, input, retry, expectedTurnId) {
724
786
  return this.perform("turn.start", threadId, input, async () => {
787
+ if (!retry && !expectedTurnId && this.capacityRetries.view(threadId)?.status === "waiting")
788
+ this.capacityRetries.clear(threadId);
725
789
  const thread = await this.loadThread(threadId);
726
790
  this.ensureManaged(thread);
727
791
  const ownership = this.workspaces.assertOwnership(thread.projectId, thread.cwd, thread.id);
728
- if (isActive(thread.status))
792
+ if (expectedTurnId) {
793
+ const last = thread.turns.at(-1);
794
+ if (last?.id !== expectedTurnId || last.status !== "failed" || !isModelCapacityError(last.error ?? "", last.errorCode))
795
+ throw new AppError("TURN_CONFLICT", "\u76EE\u6807\u5BB9\u91CF\u5931\u8D25\u5DF2\u6539\u53D8\uFF0C\u8BF7\u5237\u65B0\u540E\u7EE7\u7EED", 409);
796
+ }
797
+ if (isActive(this.projection.get(threadId)?.status ?? thread.status))
729
798
  throw new AppError("THREAD_BUSY", "\u4F1A\u8BDD\u5DF2\u6709\u6D3B\u52A8\u4EFB\u52A1\uFF0C\u8BF7\u8FFD\u52A0\u8981\u6C42\u6216\u4E2D\u65AD", 409, { threadId });
730
799
  this.ensureCompatibilityConfirmed();
800
+ if (input.taskKind === "init") await assertProjectInstructionsMissing(ownership.cwd);
731
801
  this.store.dispatch(input.clientOperationId, {
732
802
  threadId,
733
803
  workspaceKey: ownership.workspaceKey,
734
804
  codexEpoch: this.codex.epoch
735
805
  });
806
+ if (retry && !this.capacityRetries.owns(retry))
807
+ throw new AppError("TURN_CONFLICT", "\u5BB9\u91CF\u91CD\u8BD5\u5DF2\u53D6\u6D88\u6216\u88AB\u66FF\u4EE3", 409);
808
+ const candidate = retry ?? this.capacityRetries.begin(
809
+ threadId,
810
+ input.clientOperationId,
811
+ this.retrySettings(thread.runtimeSettings, input.settings),
812
+ this.codex.epoch
813
+ );
814
+ const retryOf = retry?.rootTurnId ?? (expectedTurnId ? this.retryLinks(threadId).get(expectedTurnId) ?? expectedTurnId : void 0);
736
815
  this.turnBaselines.set(input.clientOperationId, new Set(thread.turns.map((turn2) => turn2.id)));
737
816
  this.publishThread(threadId);
738
817
  const revision = this.projection.revision(threadId);
739
818
  const turn = await this.codex.startTurn(
740
819
  threadId,
741
820
  ownership.cwd,
742
- input.text,
821
+ input.taskKind === "init" ? PROJECT_INIT_PROMPT : input.text,
743
822
  input.settings,
744
823
  input.images
745
824
  );
746
825
  this.store.attachTurn(input.clientOperationId, turn.id);
747
826
  this.turnBaselines.delete(input.clientOperationId);
748
827
  this.projection.turn(threadId, turn, this.projection.revision(threadId) !== revision);
828
+ this.capacityRetries.observe({ type: "turn", threadId, turn }, input.clientOperationId);
829
+ if (retryOf) this.retryLinks(threadId).set(turn.id, retryOf);
830
+ if (candidate.cancelled && retry) await this.interruptCancelledCapacity(candidate, turn.id);
749
831
  const projectedTurn = this.projection.get(threadId)?.turns.find((entry) => entry.id === turn.id);
750
832
  if (projectedTurn && isTerminal(projectedTurn.status))
751
833
  this.store.finishTurn(threadId, turn.id, this.codex.epoch);
752
834
  this.publishThread(threadId);
753
- return { result: { threadId, turnId: turn.id }, turnId: turn.id };
835
+ return {
836
+ result: { threadId, turnId: turn.id, ...retryOf ? { retryOf } : {} },
837
+ turnId: turn.id
838
+ };
839
+ });
840
+ }
841
+ retrySettings(runtime, input = {}) {
842
+ return {
843
+ ...runtime?.model ? { model: runtime.model } : {},
844
+ ...runtime?.reasoningEffort ? { reasoningEffort: runtime.reasoningEffort } : {},
845
+ ...runtime?.serviceTier === "fast" || runtime?.serviceTier === null ? { serviceTier: runtime.serviceTier } : {},
846
+ ...input
847
+ };
848
+ }
849
+ async dispatchCapacityRetry(job) {
850
+ if (!this.capacityRetries.owns(job) || !this.accepting || this.maintenance || !this.codex.ready || this.codex.epoch !== job.epoch)
851
+ throw new AppError("TURN_CONFLICT", "\u5BB9\u91CF\u91CD\u8BD5\u7684\u8FD0\u884C\u73AF\u5883\u5DF2\u6539\u53D8", 409);
852
+ const thread = this.projection.get(job.threadId);
853
+ const last = thread?.turns.at(-1);
854
+ if (!thread || !last || last.id !== job.turnId || last.status !== "failed" || this.store.activeForThread(job.threadId) || !job.previousOperationId || this.store.get(job.previousOperationId)?.state !== "acknowledged" || this.inFlightOperations.has(job.previousOperationId))
855
+ throw new AppError("TURN_CONFLICT", "\u539F\u4EFB\u52A1\u72B6\u6001\u5DF2\u6539\u53D8\uFF0C\u8BF7\u68C0\u67E5\u540E\u7EE7\u7EED", 409);
856
+ await this.dispatchTurn(
857
+ job.threadId,
858
+ {
859
+ clientOperationId: job.operationId,
860
+ text: "",
861
+ settings: job.settings
862
+ },
863
+ job
864
+ );
865
+ }
866
+ async cancelCapacityRetry(threadId, input) {
867
+ return this.perform("turn.retry.cancel", threadId, input, async () => {
868
+ const thread = await this.loadThread(threadId);
869
+ this.ensureManaged(thread);
870
+ const job = this.capacityRetries.current(threadId);
871
+ if (!job?.view || job.rootTurnId !== input.expectedTurnId)
872
+ throw new AppError("TURN_CONFLICT", "\u5BB9\u91CF\u91CD\u8BD5\u5DF2\u7ED3\u675F\u6216\u88AB\u66FF\u4EE3", 409);
873
+ this.store.dispatch(input.clientOperationId);
874
+ this.capacityRetries.cancel(job);
875
+ if (job.attempt > 0 && job.turnId) await this.interruptCancelledCapacity(job, job.turnId);
876
+ return { result: { threadId, cancelled: true } };
877
+ });
878
+ }
879
+ async interruptCancelledCapacity(job, turnId) {
880
+ const turn = this.projection.get(job.threadId)?.turns.find((entry) => entry.id === turnId);
881
+ if (!job.cancelled || job.interruptSent || !turn || !isActive(turn.status) || this.codex.epoch !== job.epoch || !this.codex.ready)
882
+ return;
883
+ job.interruptSent = true;
884
+ await this.codex.interruptTurn(job.threadId, turnId);
885
+ }
886
+ async retryTurn(threadId, input) {
887
+ return this.dispatchTurn(threadId, { ...input, text: "" }, void 0, input.expectedTurnId);
888
+ }
889
+ async getGoal(threadId) {
890
+ const thread = await this.loadThread(threadId);
891
+ this.workspaces.assertOwnership(thread.projectId, thread.cwd, thread.id);
892
+ this.ensureReady();
893
+ if (!this.codex.getGoal) throw new AppError("GOAL_UNAVAILABLE", "\u5F53\u524D Codex \u4E0D\u652F\u6301\u76EE\u6807", 409);
894
+ const goal = await this.codex.getGoal(threadId);
895
+ this.workspaces.assertOwnership(thread.projectId, thread.cwd, thread.id);
896
+ const current = this.projection.get(threadId);
897
+ if (current) {
898
+ current.goal = goal;
899
+ this.projection.touch(threadId);
900
+ this.publishThread(threadId);
901
+ }
902
+ return { goal, version: requestDigest(goal) };
903
+ }
904
+ async mutateGoal(threadId, input) {
905
+ return this.perform("goal.update", threadId, input, async () => {
906
+ if (this.goalWrites.has(threadId))
907
+ throw new AppError("GOAL_BUSY", "\u76EE\u6807\u66F4\u65B0\u6B63\u5728\u8FDB\u884C\uFF0C\u8BF7\u7B49\u5F85\u56DE\u6267", 409);
908
+ this.goalWrites.add(threadId);
909
+ try {
910
+ const thread = await this.loadThread(threadId);
911
+ this.ensureManaged(thread);
912
+ this.workspaces.assertOwnership(thread.projectId, thread.cwd, thread.id);
913
+ const stopping = input.action === "clear" || input.action === "set" && input.status !== "active" && input.status !== void 0 && input.objective === void 0 && input.tokenBudget === void 0;
914
+ if (!stopping && (thread.status === "unknown" || isActive(thread.status) || thread.turns.some((turn) => isActive(turn.status)) || this.store.activeForThread(threadId)))
915
+ throw new AppError("THREAD_BUSY", "\u8BF7\u7B49\u5F85\u5F53\u524D\u4EFB\u52A1\u7ED3\u675F\u5E76\u6838\u5BF9\u72B6\u6001\u540E\u518D\u4FEE\u6539\u6216\u6062\u590D\u76EE\u6807", 409);
916
+ this.ensureCompatibilityConfirmed();
917
+ if (!this.codex.getGoal || !this.codex.mutateGoal)
918
+ throw new AppError("GOAL_UNAVAILABLE", "\u5F53\u524D Codex \u4E0D\u652F\u6301\u76EE\u6807", 409);
919
+ const before = await this.getGoal(threadId);
920
+ if (before.version !== input.expectedVersion)
921
+ throw new AppError("GOAL_CONFLICT", "\u76EE\u6807\u5DF2\u53D1\u751F\u53D8\u5316\uFF0C\u8BF7\u91CD\u65B0\u8BFB\u53D6\u540E\u64CD\u4F5C", 409);
922
+ this.store.dispatch(input.clientOperationId);
923
+ const mutation = input.action === "clear" ? { action: "clear" } : {
924
+ action: "set",
925
+ ...input.objective === void 0 ? {} : { objective: input.objective },
926
+ ...input.status === void 0 ? {} : { status: input.status },
927
+ ...input.tokenBudget === void 0 ? {} : { tokenBudget: input.tokenBudget }
928
+ };
929
+ const previousGoal = this.projection.get(threadId)?.goal;
930
+ const goal = await this.codex.mutateGoal(threadId, mutation);
931
+ const current = this.projection.get(threadId);
932
+ if (current && current.goal === previousGoal) {
933
+ current.goal = goal;
934
+ this.projection.touch(threadId);
935
+ this.publishThread(threadId);
936
+ }
937
+ return { result: { goal } };
938
+ } finally {
939
+ this.goalWrites.delete(threadId);
940
+ }
941
+ });
942
+ }
943
+ async recapThread(threadId, input) {
944
+ return this.perform("thread.recap", threadId, input, async () => {
945
+ const thread = await this.loadThread(threadId);
946
+ this.ensureManaged(thread);
947
+ this.workspaces.assertOwnership(thread.projectId, thread.cwd, thread.id);
948
+ if (thread.status === "unknown" || isActive(thread.status) || thread.turns.some((turn) => isActive(turn.status)))
949
+ throw new AppError("THREAD_BUSY", "\u8BF7\u7B49\u5F85\u4EFB\u52A1\u7ED3\u675F\u5E76\u6838\u5BF9\u72B6\u6001\u540E\u518D\u751F\u6210\u6458\u8981", 409);
950
+ this.ensureCompatibilityConfirmed();
951
+ if (!this.codex.recapThread)
952
+ throw new AppError("METHOD_UNAVAILABLE", "Codex \u4E0D\u652F\u6301\u4F1A\u8BDD\u6458\u8981", 409);
953
+ const history = await (this.codex.readThreadPage?.(threadId) ?? this.codex.readThread(threadId));
954
+ this.workspaces.assertOwnership(thread.projectId, history.cwd, history.id);
955
+ const { prompt, partial } = recapInput(history, input.language);
956
+ if (!history.turns.some(
957
+ (turn) => turn.items.some(
958
+ (item) => (item.type === "userMessage" || item.type === "agentMessage") && item.text?.trim()
959
+ )
960
+ ))
961
+ throw new AppError("EMPTY_THREAD", "\u5F53\u524D\u4F1A\u8BDD\u6CA1\u6709\u53EF\u751F\u6210\u6458\u8981\u7684\u6D88\u606F", 409);
962
+ this.store.dispatch(input.clientOperationId);
963
+ const summary = await this.codex.recapThread(threadId, prompt);
964
+ this.workspaces.assertOwnership(thread.projectId, thread.cwd, thread.id);
965
+ return { result: { summary, partial, readAt: Date.now() } };
966
+ });
967
+ }
968
+ async getGuardianDenials(threadId) {
969
+ const thread = await this.loadThread(threadId);
970
+ this.ensureManaged(thread);
971
+ this.workspaces.assertOwnership(thread.projectId, thread.cwd, thread.id);
972
+ if (!this.codex.listGuardianDenials)
973
+ throw new AppError("METHOD_UNAVAILABLE", "Codex \u4E0D\u652F\u6301\u62D2\u7EDD\u8BB0\u5F55\u8BFB\u53D6", 409);
974
+ return { data: this.codex.listGuardianDenials(threadId) };
975
+ }
976
+ async approveGuardianDenial(threadId, input) {
977
+ return this.perform("guardian.approve", threadId, input, async () => {
978
+ const thread = await this.loadThread(threadId);
979
+ this.ensureManaged(thread);
980
+ this.workspaces.assertOwnership(thread.projectId, thread.cwd, thread.id);
981
+ if (thread.status === "unknown" || isActive(thread.status) || thread.turns.some((turn) => isActive(turn.status)))
982
+ throw new AppError("THREAD_BUSY", "\u8BF7\u7B49\u5F85\u5F53\u524D\u4EFB\u52A1\u7ED3\u675F\u5E76\u786E\u8BA4\u72B6\u6001\u540E\u518D\u6388\u6743\u91CD\u8BD5", 409);
983
+ this.ensureCompatibilityConfirmed();
984
+ if (!this.codex.approveGuardianDenial || !this.codex.listGuardianDenials?.(threadId).some((entry) => entry.id === input.denialId))
985
+ throw new AppError("DENIAL_EXPIRED", "\u62D2\u7EDD\u8BB0\u5F55\u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u67E5\u770B\u5F53\u524D\u4F1A\u8BDD", 409);
986
+ this.store.dispatch(input.clientOperationId);
987
+ await this.codex.approveGuardianDenial(threadId, input.denialId);
988
+ this.publishThread(threadId);
989
+ return { result: { threadId } };
754
990
  });
755
991
  }
756
992
  async compactThread(threadId, input) {
757
993
  return this.perform("thread.compact", threadId, input, async () => {
994
+ this.capacityRetries.clear(threadId);
758
995
  const thread = await this.loadThread(threadId);
759
996
  this.ensureManaged(thread);
760
997
  const ownership = this.workspaces.assertOwnership(thread.projectId, thread.cwd, thread.id);
@@ -779,6 +1016,7 @@ class GatewayService {
779
1016
  }
780
1017
  async startReview(threadId, input) {
781
1018
  return this.perform("review.start", threadId, input, async () => {
1019
+ this.capacityRetries.clear(threadId);
782
1020
  const thread = await this.loadThread(threadId);
783
1021
  this.ensureManaged(thread);
784
1022
  const ownership = this.workspaces.assertOwnership(thread.projectId, thread.cwd, thread.id);
@@ -796,7 +1034,7 @@ class GatewayService {
796
1034
  });
797
1035
  const revision = this.projection.revision(threadId);
798
1036
  this.turnBaselines.set(input.clientOperationId, new Set(thread.turns.map((turn2) => turn2.id)));
799
- const turn = await this.codex.startReview(threadId);
1037
+ const turn = await this.codex.startReview(threadId, input.target);
800
1038
  this.store.attachTurn(input.clientOperationId, turn.id);
801
1039
  this.turnBaselines.delete(input.clientOperationId);
802
1040
  this.projection.turn(threadId, turn, this.projection.revision(threadId) !== revision);
@@ -916,6 +1154,7 @@ class GatewayService {
916
1154
  }
917
1155
  async shutdown() {
918
1156
  this.accepting = false;
1157
+ this.capacityRetries.clearAll();
919
1158
  await this.projectClones.close();
920
1159
  this.store.recover();
921
1160
  this.projection.runtimeReset();
@@ -957,6 +1196,8 @@ class GatewayService {
957
1196
  const action = this.projection.resolveAction(target, "pending");
958
1197
  if (action?.threadId) this.publishThread(action.threadId);
959
1198
  }
1199
+ if (kind === "turn.start")
1200
+ this.capacityRetries.dispatchFailed(target, input.clientOperationId);
960
1201
  if (dispatched && kind === "turn.start") this.publishThread(target);
961
1202
  this.events.publish("operation", operation);
962
1203
  if (uncertain)
@@ -1187,11 +1428,28 @@ class GatewayService {
1187
1428
  }
1188
1429
  historyVisible(thread) {
1189
1430
  return Boolean(
1190
- thread.hasMessages || thread.archived || thread.turns.length || this.store.activeForThread(thread.id) || isActive(thread.status) || thread.actions.some((action) => ["pending", "responding"].includes(action.status))
1431
+ thread.hasMessages || thread.archived || thread.goal || thread.turns.length || this.store.activeForThread(thread.id) || isActive(thread.status) || thread.actions.some((action) => ["pending", "responding"].includes(action.status))
1191
1432
  );
1192
1433
  }
1434
+ retryLinks(threadId) {
1435
+ let links = this.capacityLinks.get(threadId);
1436
+ if (!links) {
1437
+ links = this.store.capacityRetryTurns(threadId);
1438
+ this.capacityLinks.set(threadId, links);
1439
+ }
1440
+ return links;
1441
+ }
1193
1442
  threadView(thread) {
1194
1443
  const result = structuredClone(thread);
1444
+ const links = this.capacityLinks.get(thread.id) ?? (thread.turns.some((turn) => !turn.items.some((item) => item.type === "userMessage")) ? this.retryLinks(thread.id) : void 0);
1445
+ for (const turn of result.turns) {
1446
+ const root = links?.get(turn.id);
1447
+ if (root) turn.retryOf = root;
1448
+ }
1449
+ const retry = this.capacityRetries.view(thread.id);
1450
+ if (retry) result.capacityRetry = structuredClone(retry);
1451
+ else delete result.capacityRetry;
1452
+ if (this.capacityRetries.pending(thread.id)) result.status = "running";
1195
1453
  result.archived = thread.archived ?? false;
1196
1454
  result.historyVisible = this.historyVisible(thread);
1197
1455
  const marker = this.store.activeForThread(thread.id);
@@ -1240,6 +1498,17 @@ class GatewayService {
1240
1498
  }
1241
1499
  }
1242
1500
  consume(event) {
1501
+ if (event.type === "runtime" && (!event.diagnostics.ready || event.diagnostics.codexEpoch !== this.currentEpoch))
1502
+ this.capacityRetries.clearAll();
1503
+ const eventThread = event.type === "action" ? event.action.threadId : "threadId" in event ? event.threadId : void 0;
1504
+ const eventMarker = eventThread ? this.store.activeForThread(eventThread) : void 0;
1505
+ const matchesDispatch = event.type !== "turn" || eventMarker?.turnId === event.turn.id || eventMarker?.turnId === null && this.turnBaselines.get(eventMarker.operationId)?.has(event.turn.id) === false;
1506
+ this.capacityRetries.observe(event, matchesDispatch ? eventMarker?.operationId : void 0);
1507
+ if (event.type === "turn") {
1508
+ const job = this.capacityRetries.current(event.threadId);
1509
+ if (job?.attempt && job.turnId === event.turn.id && job.rootTurnId)
1510
+ this.retryLinks(event.threadId).set(event.turn.id, job.rootTurnId);
1511
+ }
1243
1512
  if (event.type === "runtime") {
1244
1513
  this.compatibilityConfirmation(event.diagnostics);
1245
1514
  if (!event.diagnostics.ready || event.diagnostics.codexEpoch !== this.currentEpoch) {
@@ -1295,6 +1564,12 @@ class GatewayService {
1295
1564
  }
1296
1565
  const thread = this.projection.get(event.threadId);
1297
1566
  if (!thread) return;
1567
+ if (event.type === "goal") {
1568
+ thread.goal = event.goal;
1569
+ this.projection.touch(thread.id);
1570
+ this.publishThread(thread.id);
1571
+ return;
1572
+ }
1298
1573
  const previousAttention = new Set(
1299
1574
  event.type === "item" || event.type === "turn" ? pendingAttention(thread).map(attentionKey) : []
1300
1575
  );
@@ -1304,7 +1579,13 @@ class GatewayService {
1304
1579
  return;
1305
1580
  }
1306
1581
  if (event.type === "turnError" || event.type === "turnProgress") {
1307
- const changed = event.type === "turnError" ? this.projection.turnError(thread.id, event.turnId, event.message, event.willRetry) : this.projection.turnProgress(thread.id, event.turnId);
1582
+ const changed = event.type === "turnError" ? this.projection.turnError(
1583
+ thread.id,
1584
+ event.turnId,
1585
+ event.message,
1586
+ event.willRetry,
1587
+ event
1588
+ ) : this.projection.turnProgress(thread.id, event.turnId);
1308
1589
  if (changed) this.publishThread(thread.id);
1309
1590
  return;
1310
1591
  }
@@ -1324,6 +1605,32 @@ class GatewayService {
1324
1605
  this.projection.touch(thread.id);
1325
1606
  } else if (event.type === "turn") {
1326
1607
  const previous = thread.turns.find((turn) => turn.id === event.turn.id);
1608
+ if (thread.goal && !isTerminal(event.turn.status) && (!previous || !isTerminal(previous.status)) && !this.store.activeForThread(thread.id) && this.store.getThreadLink(thread.id)?.managedEpoch === this.codex.epoch) {
1609
+ try {
1610
+ const ownership = this.workspaces.assertOwnership(
1611
+ thread.projectId,
1612
+ thread.cwd,
1613
+ thread.id
1614
+ );
1615
+ const operationId = randomUUID();
1616
+ this.store.accept(operationId, "goal.continue", thread.id, {
1617
+ turnId: event.turn.id,
1618
+ codexEpoch: this.codex.epoch
1619
+ });
1620
+ this.store.dispatch(operationId, {
1621
+ threadId: thread.id,
1622
+ workspaceKey: ownership.workspaceKey,
1623
+ codexEpoch: this.codex.epoch
1624
+ });
1625
+ this.store.attachTurn(operationId, event.turn.id);
1626
+ this.store.acknowledge(
1627
+ operationId,
1628
+ { threadId: thread.id, turnId: event.turn.id },
1629
+ event.turn.id
1630
+ );
1631
+ } catch {
1632
+ }
1633
+ }
1327
1634
  const newCompletion = !previous || !isTerminal(previous.status);
1328
1635
  if (isTerminal(event.turn.status)) this.turnUsage.save(thread.id, event.turn);
1329
1636
  this.projection.turn(thread.id, event.turn);
@@ -1338,7 +1645,7 @@ class GatewayService {
1338
1645
  if (isTerminal(event.turn.status)) {
1339
1646
  this.store.finishTurn(thread.id, event.turn.id, this.codex.epoch);
1340
1647
  this.projection.expireTurn(thread.id, event.turn.id);
1341
- if (newCompletion) {
1648
+ if (newCompletion && !this.capacityRetries.pending(thread.id)) {
1342
1649
  try {
1343
1650
  this.workspaces.assertOwnership(thread.projectId, thread.cwd, thread.id);
1344
1651
  this.events.publishCompletion({