agl 1.0.17 → 21.0.2

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 (277) hide show
  1. package/.smartconfig.json +55 -0
  2. package/changelog.md +1026 -0
  3. package/cli.js +14 -0
  4. package/dist_serve/bundle.js +44778 -0
  5. package/dist_serve/index.html +13 -0
  6. package/dist_ts/00_commitinfo_data.d.ts +8 -0
  7. package/dist_ts/00_commitinfo_data.js +9 -0
  8. package/dist_ts/classes.authmodels.d.ts +126 -0
  9. package/dist_ts/classes.authmodels.js +1422 -0
  10. package/dist_ts/classes.authstore.d.ts +160 -0
  11. package/dist_ts/classes.authstore.js +2483 -0
  12. package/dist_ts/classes.browserresourcehost.d.ts +85 -0
  13. package/dist_ts/classes.browserresourcehost.js +912 -0
  14. package/dist_ts/classes.cli.d.ts +21 -0
  15. package/dist_ts/classes.cli.js +1082 -0
  16. package/dist_ts/classes.config.d.ts +23 -0
  17. package/dist_ts/classes.config.js +267 -0
  18. package/dist_ts/classes.controller.d.ts +523 -0
  19. package/dist_ts/classes.controller.js +10611 -0
  20. package/dist_ts/classes.controllermanagement.d.ts +32 -0
  21. package/dist_ts/classes.controllermanagement.js +222 -0
  22. package/dist_ts/classes.draftmanager.d.ts +30 -0
  23. package/dist_ts/classes.draftmanager.js +179 -0
  24. package/dist_ts/classes.embeddeddb.d.ts +30 -0
  25. package/dist_ts/classes.embeddeddb.js +159 -0
  26. package/dist_ts/classes.flexframedtransport.d.ts +56 -0
  27. package/dist_ts/classes.flexframedtransport.js +275 -0
  28. package/dist_ts/classes.flexmodels.private.d.ts +231 -0
  29. package/dist_ts/classes.flexmodels.private.js +985 -0
  30. package/dist_ts/classes.flexmodels.public.d.ts +207 -0
  31. package/dist_ts/classes.flexmodels.public.js +885 -0
  32. package/dist_ts/classes.flexprojectionreader.d.ts +89 -0
  33. package/dist_ts/classes.flexprojectionreader.js +580 -0
  34. package/dist_ts/classes.flexservice.d.ts +196 -0
  35. package/dist_ts/classes.flexservice.js +3704 -0
  36. package/dist_ts/classes.flexstore.d.ts +174 -0
  37. package/dist_ts/classes.flexstore.js +3400 -0
  38. package/dist_ts/classes.flexsupervisor.d.ts +105 -0
  39. package/dist_ts/classes.flexsupervisor.js +1191 -0
  40. package/dist_ts/classes.gitreversion.d.ts +431 -0
  41. package/dist_ts/classes.gitreversion.js +10592 -0
  42. package/dist_ts/classes.harnessoperationmanager.d.ts +113 -0
  43. package/dist_ts/classes.harnessoperationmanager.js +602 -0
  44. package/dist_ts/classes.opencodeclient.d.ts +146 -0
  45. package/dist_ts/classes.opencodeclient.js +3003 -0
  46. package/dist_ts/classes.opencodesupervisor.d.ts +60 -0
  47. package/dist_ts/classes.opencodesupervisor.js +1003 -0
  48. package/dist_ts/classes.passkeymanager.d.ts +34 -0
  49. package/dist_ts/classes.passkeymanager.js +456 -0
  50. package/dist_ts/classes.processinspection.d.ts +50 -0
  51. package/dist_ts/classes.processinspection.js +484 -0
  52. package/dist_ts/classes.resourcecoordinator.d.ts +64 -0
  53. package/dist_ts/classes.resourcecoordinator.js +294 -0
  54. package/dist_ts/classes.sessionintelligencereader.d.ts +13 -0
  55. package/dist_ts/classes.sessionintelligencereader.js +40 -0
  56. package/dist_ts/classes.systemmetricssampler.d.ts +53 -0
  57. package/dist_ts/classes.systemmetricssampler.js +168 -0
  58. package/dist_ts/classes.terminalmanager.d.ts +95 -0
  59. package/dist_ts/classes.terminalmanager.js +981 -0
  60. package/dist_ts/classes.terminalresourcehost.d.ts +20 -0
  61. package/dist_ts/classes.terminalresourcehost.js +84 -0
  62. package/dist_ts/classes.upgradecoordinator.d.ts +244 -0
  63. package/dist_ts/classes.upgradecoordinator.js +2155 -0
  64. package/dist_ts/classes.upgradetransaction.d.ts +92 -0
  65. package/dist_ts/classes.upgradetransaction.js +2125 -0
  66. package/dist_ts/classes.uploadmanager.d.ts +24 -0
  67. package/dist_ts/classes.uploadmanager.js +210 -0
  68. package/dist_ts/classes.validation.d.ts +91 -0
  69. package/dist_ts/classes.validation.js +1087 -0
  70. package/dist_ts/flexharness.child.d.ts +1 -0
  71. package/dist_ts/flexharness.child.js +28 -0
  72. package/dist_ts/functions.canonicaldirectory.d.ts +21 -0
  73. package/dist_ts/functions.canonicaldirectory.js +144 -0
  74. package/dist_ts/functions.controllerdataroot.d.ts +15 -0
  75. package/dist_ts/functions.controllerdataroot.js +76 -0
  76. package/dist_ts/functions.embeddeddb.d.ts +2 -0
  77. package/dist_ts/functions.embeddeddb.js +21 -0
  78. package/dist_ts/functions.flexcredentialstore.d.ts +16 -0
  79. package/dist_ts/functions.flexcredentialstore.js +93 -0
  80. package/dist_ts/functions.opencodeauth.d.ts +3 -0
  81. package/dist_ts/functions.opencodeauth.js +31 -0
  82. package/dist_ts/functions.runtimeenvironment.d.ts +2 -0
  83. package/dist_ts/functions.runtimeenvironment.js +79 -0
  84. package/dist_ts/functions.upgradefollower.d.ts +3 -0
  85. package/dist_ts/functions.upgradefollower.js +95 -0
  86. package/dist_ts/index.d.ts +24 -0
  87. package/dist_ts/index.js +25 -0
  88. package/dist_ts/interfaces.auth.d.ts +181 -0
  89. package/dist_ts/interfaces.auth.js +82 -0
  90. package/dist_ts/interfaces.config.d.ts +34 -0
  91. package/dist_ts/interfaces.config.js +2 -0
  92. package/dist_ts/interfaces.flexipc.d.ts +939 -0
  93. package/dist_ts/interfaces.flexipc.js +1492 -0
  94. package/dist_ts/interfaces.gitreversion.d.ts +246 -0
  95. package/dist_ts/interfaces.gitreversion.js +2 -0
  96. package/dist_ts/interfaces.lifecycle.d.ts +37 -0
  97. package/dist_ts/interfaces.lifecycle.js +2 -0
  98. package/dist_ts/interfaces.opencode.d.ts +101 -0
  99. package/dist_ts/interfaces.opencode.js +3 -0
  100. package/dist_ts/interfaces.projects.d.ts +156 -0
  101. package/dist_ts/interfaces.projects.js +5 -0
  102. package/dist_ts/plugins.d.ts +36 -0
  103. package/dist_ts/plugins.js +40 -0
  104. package/dist_ts_interfaces/index.d.ts +2 -0
  105. package/dist_ts_interfaces/index.js +3 -0
  106. package/dist_ts_interfaces/interfaces.d.ts +747 -0
  107. package/dist_ts_interfaces/interfaces.js +47 -0
  108. package/dist_ts_interfaces/plugins.d.ts +4 -0
  109. package/dist_ts_interfaces/plugins.js +3 -0
  110. package/dist_ts_interfaces/requests.d.ts +806 -0
  111. package/dist_ts_interfaces/requests.js +2 -0
  112. package/dist_ts_migration/classes.documentmigrationrunner.d.ts +8 -0
  113. package/dist_ts_migration/classes.documentmigrationrunner.js +24 -0
  114. package/dist_ts_migration/classes.flexmigrationrunner.d.ts +11 -0
  115. package/dist_ts_migration/classes.flexmigrationrunner.js +25 -0
  116. package/dist_ts_migration/classes.flexpostharnessmigrationrunner.d.ts +10 -0
  117. package/dist_ts_migration/classes.flexpostharnessmigrationrunner.js +15 -0
  118. package/dist_ts_migration/classes.migrationrunner.d.ts +8 -0
  119. package/dist_ts_migration/classes.migrationrunner.js +34 -0
  120. package/dist_ts_migration/index.d.ts +12 -0
  121. package/dist_ts_migration/index.js +13 -0
  122. package/dist_ts_migration/v10_flexharnessv3migration.d.ts +11 -0
  123. package/dist_ts_migration/v10_flexharnessv3migration.js +23 -0
  124. package/dist_ts_migration/v13_flexprojectionorder.d.ts +11 -0
  125. package/dist_ts_migration/v13_flexprojectionorder.js +23 -0
  126. package/dist_ts_migration/v16_flexprovidercredentials.d.ts +22 -0
  127. package/dist_ts_migration/v16_flexprovidercredentials.js +149 -0
  128. package/dist_ts_migration/v18_resourcelayout.d.ts +22 -0
  129. package/dist_ts_migration/v18_resourcelayout.js +23 -0
  130. package/dist_ts_migration/v18_resourcelayoutmigration.d.ts +6 -0
  131. package/dist_ts_migration/v18_resourcelayoutmigration.js +61 -0
  132. package/dist_ts_migration/v19_flexprojectmanagement.d.ts +11 -0
  133. package/dist_ts_migration/v19_flexprojectmanagement.js +21 -0
  134. package/dist_ts_migration/v1_embeddeddatabaselocation.d.ts +28 -0
  135. package/dist_ts_migration/v1_embeddeddatabaselocation.js +415 -0
  136. package/dist_ts_migration/v22_projectfilesystemidentitymigration.d.ts +7 -0
  137. package/dist_ts_migration/v22_projectfilesystemidentitymigration.js +105 -0
  138. package/dist_ts_migration/v2_controllerdataroot.d.ts +130 -0
  139. package/dist_ts_migration/v2_controllerdataroot.js +1887 -0
  140. package/dist_ts_migration/v6_protocoldocumentmigration.d.ts +7 -0
  141. package/dist_ts_migration/v6_protocoldocumentmigration.js +100 -0
  142. package/dist_ts_migration/v6_protocoldocuments.d.ts +7 -0
  143. package/dist_ts_migration/v6_protocoldocuments.js +138 -0
  144. package/html/index.html +13 -0
  145. package/{LICENSE → license.md} +2 -2
  146. package/package.json +86 -48
  147. package/readme.md +480 -0
  148. package/readme.plan.md +188 -0
  149. package/ts/00_commitinfo_data.ts +8 -0
  150. package/ts/classes.authmodels.ts +1245 -0
  151. package/ts/classes.authstore.ts +3274 -0
  152. package/ts/classes.browserresourcehost.ts +1205 -0
  153. package/ts/classes.cli.ts +1254 -0
  154. package/ts/classes.config.ts +351 -0
  155. package/ts/classes.controller.ts +14426 -0
  156. package/ts/classes.controllermanagement.ts +330 -0
  157. package/ts/classes.draftmanager.ts +232 -0
  158. package/ts/classes.embeddeddb.ts +166 -0
  159. package/ts/classes.flexframedtransport.ts +283 -0
  160. package/ts/classes.flexmodels.private.ts +823 -0
  161. package/ts/classes.flexmodels.public.ts +732 -0
  162. package/ts/classes.flexprojectionreader.ts +816 -0
  163. package/ts/classes.flexservice.ts +4302 -0
  164. package/ts/classes.flexstore.ts +4565 -0
  165. package/ts/classes.flexsupervisor.ts +1453 -0
  166. package/ts/classes.gitreversion.ts +13353 -0
  167. package/ts/classes.harnessoperationmanager.ts +870 -0
  168. package/ts/classes.opencodeclient.ts +4110 -0
  169. package/ts/classes.opencodesupervisor.ts +1280 -0
  170. package/ts/classes.passkeymanager.ts +570 -0
  171. package/ts/classes.processinspection.ts +623 -0
  172. package/ts/classes.resourcecoordinator.ts +436 -0
  173. package/ts/classes.sessionintelligencereader.ts +52 -0
  174. package/ts/classes.systemmetricssampler.ts +215 -0
  175. package/ts/classes.terminalmanager.ts +1207 -0
  176. package/ts/classes.terminalresourcehost.ts +122 -0
  177. package/ts/classes.upgradecoordinator.ts +2675 -0
  178. package/ts/classes.upgradetransaction.ts +2900 -0
  179. package/ts/classes.uploadmanager.ts +240 -0
  180. package/ts/classes.validation.ts +1409 -0
  181. package/ts/flexharness.child.ts +35 -0
  182. package/ts/functions.canonicaldirectory.ts +213 -0
  183. package/ts/functions.controllerdataroot.ts +107 -0
  184. package/ts/functions.embeddeddb.ts +22 -0
  185. package/ts/functions.flexcredentialstore.ts +128 -0
  186. package/ts/functions.opencodeauth.ts +34 -0
  187. package/ts/functions.runtimeenvironment.ts +90 -0
  188. package/ts/functions.upgradefollower.ts +115 -0
  189. package/ts/index.ts +36 -6
  190. package/ts/interfaces.auth.ts +316 -0
  191. package/ts/interfaces.config.ts +37 -0
  192. package/ts/interfaces.flexipc.ts +2586 -0
  193. package/ts/interfaces.gitreversion.ts +336 -0
  194. package/ts/interfaces.lifecycle.ts +46 -0
  195. package/ts/interfaces.opencode.ts +153 -0
  196. package/ts/interfaces.projects.ts +182 -0
  197. package/ts/plugins.ts +64 -0
  198. package/ts_interfaces/index.ts +2 -0
  199. package/ts_interfaces/interfaces.ts +827 -0
  200. package/ts_interfaces/plugins.ts +15 -0
  201. package/ts_interfaces/requests.ts +1186 -0
  202. package/ts_migration/classes.documentmigrationrunner.ts +32 -0
  203. package/ts_migration/classes.flexmigrationrunner.ts +33 -0
  204. package/ts_migration/classes.flexpostharnessmigrationrunner.ts +22 -0
  205. package/ts_migration/classes.migrationrunner.ts +40 -0
  206. package/ts_migration/index.ts +12 -0
  207. package/ts_migration/v10_flexharnessv3migration.ts +35 -0
  208. package/ts_migration/v13_flexprojectionorder.ts +33 -0
  209. package/ts_migration/v16_flexprovidercredentials.ts +196 -0
  210. package/ts_migration/v18_resourcelayout.ts +55 -0
  211. package/ts_migration/v18_resourcelayoutmigration.ts +68 -0
  212. package/ts_migration/v19_flexprojectmanagement.ts +28 -0
  213. package/ts_migration/v1_embeddeddatabaselocation.ts +560 -0
  214. package/ts_migration/v22_projectfilesystemidentitymigration.ts +121 -0
  215. package/ts_migration/v2_controllerdataroot.ts +2349 -0
  216. package/ts_migration/v6_protocoldocumentmigration.ts +144 -0
  217. package/ts_migration/v6_protocoldocuments.ts +198 -0
  218. package/ts_web/00_commitinfo_data.ts +8 -0
  219. package/ts_web/classes.controllersocketclient.ts +517 -0
  220. package/ts_web/classes.sessiondraftsync.ts +390 -0
  221. package/ts_web/elements.harnesscontrollerapp.ts +11494 -0
  222. package/ts_web/index.ts +2 -0
  223. package/ts_web/plugins.ts +31 -0
  224. package/.gitlab-ci.yml +0 -71
  225. package/.npmignore +0 -5
  226. package/README.md +0 -40
  227. package/dist/cli.js +0 -2
  228. package/dist/index.d.ts +0 -0
  229. package/dist/index.js +0 -9
  230. package/dist/mod_bundle/index.d.ts +0 -1
  231. package/dist/mod_bundle/index.js +0 -18
  232. package/dist/mod_bundle/mod.plugins.d.ts +0 -4
  233. package/dist/mod_bundle/mod.plugins.js +0 -11
  234. package/dist/mod_install/index.d.ts +0 -4
  235. package/dist/mod_install/index.js +0 -21
  236. package/dist/mod_install/mod.plugins.d.ts +0 -1
  237. package/dist/mod_install/mod.plugins.js +0 -7
  238. package/dist/mod_standard/index.d.ts +0 -4
  239. package/dist/mod_standard/index.js +0 -21
  240. package/dist/mod_standard/mod.browser.d.ts +0 -6
  241. package/dist/mod_standard/mod.browser.js +0 -14
  242. package/dist/mod_standard/mod.clean.d.ts +0 -4
  243. package/dist/mod_standard/mod.clean.js +0 -12
  244. package/dist/mod_standard/mod.compile.d.ts +0 -4
  245. package/dist/mod_standard/mod.compile.js +0 -21
  246. package/dist/mod_standard/mod.plugins.d.ts +0 -8
  247. package/dist/mod_standard/mod.plugins.js +0 -19
  248. package/dist/mod_standard/mod.test.d.ts +0 -4
  249. package/dist/mod_standard/mod.test.js +0 -12
  250. package/dist/plr.cli.d.ts +0 -3
  251. package/dist/plr.cli.js +0 -57
  252. package/dist/plr.config.d.ts +0 -11
  253. package/dist/plr.config.js +0 -26
  254. package/dist/plr.mods.d.ts +0 -17
  255. package/dist/plr.mods.js +0 -17
  256. package/dist/plr.paths.d.ts +0 -19
  257. package/dist/plr.paths.js +0 -26
  258. package/dist/plr.plugins.d.ts +0 -8
  259. package/dist/plr.plugins.js +0 -16
  260. package/npmextra.json +0 -10
  261. package/ts/mod_bundle/index.ts +0 -20
  262. package/ts/mod_bundle/mod.plugins.ts +0 -9
  263. package/ts/mod_install/index.ts +0 -20
  264. package/ts/mod_install/mod.plugins.ts +0 -1
  265. package/ts/mod_standard/index.ts +0 -23
  266. package/ts/mod_standard/mod.browser.ts +0 -17
  267. package/ts/mod_standard/mod.clean.ts +0 -13
  268. package/ts/mod_standard/mod.compile.ts +0 -23
  269. package/ts/mod_standard/mod.plugins.ts +0 -17
  270. package/ts/mod_standard/mod.test.ts +0 -13
  271. package/ts/plr.cli.ts +0 -60
  272. package/ts/plr.config.ts +0 -36
  273. package/ts/plr.mods.ts +0 -25
  274. package/ts/plr.paths.ts +0 -29
  275. package/ts/plr.plugins.ts +0 -17
  276. package/tslint.json +0 -3
  277. package/yarn.lock +0 -3464
@@ -0,0 +1,870 @@
1
+ import * as plugins from './plugins.js';
2
+ import {
3
+ controllerRuntimeIdKey,
4
+ type IControllerDraftAttachment,
5
+ type IControllerRuntimeId,
6
+ type IControllerSessionDraft,
7
+ type IControllerSessionDraftUpdate,
8
+ type TControllerSessionHarnessId,
9
+ } from '../ts_interfaces/interfaces.js';
10
+ import { ControllerDraftManager } from './classes.draftmanager.js';
11
+ import {
12
+ ControllerUploadManager,
13
+ type IControllerUploadHandle,
14
+ } from './classes.uploadmanager.js';
15
+
16
+ const maximumActiveHarnessOperations = 64;
17
+ const maximumHarnessOperationsPerSession = 16;
18
+ const maximumPendingUploadCleanups = 64;
19
+ const harnessOperationCleanupTimeoutMs = 10_000;
20
+
21
+ export class ControllerHarnessOperationConflictError extends Error {
22
+ constructor(messageArg: string) {
23
+ super(messageArg);
24
+ this.name = 'ControllerHarnessOperationConflictError';
25
+ }
26
+ }
27
+
28
+ export class ControllerHarnessOperationLimitError extends Error {
29
+ constructor(messageArg: string) {
30
+ super(messageArg);
31
+ this.name = 'ControllerHarnessOperationLimitError';
32
+ }
33
+ }
34
+
35
+ export type TControllerHarnessOperationKind = 'send' | 'slash';
36
+ type TControllerHarnessOperationState = 'submitting' | 'accepted' | 'outcomeUnknown';
37
+
38
+ export interface IControllerHarnessOperationReservation {
39
+ operationId: string;
40
+ createdAt: number;
41
+ kind: TControllerHarnessOperationKind;
42
+ projectId: string;
43
+ sessionId: IControllerRuntimeId;
44
+ draft: IControllerSessionDraft;
45
+ promptSuffix: string;
46
+ /** Native user-message identity when the harness accepts a caller-selected ID. */
47
+ messageId?: IControllerRuntimeId;
48
+ uploadDirectory?: string;
49
+ }
50
+
51
+ export interface IControllerHarnessOperationSnapshot {
52
+ reservation: IControllerHarnessOperationReservation;
53
+ state: TControllerHarnessOperationState;
54
+ flexQueueId?: string;
55
+ flexRunId?: string;
56
+ }
57
+
58
+ export interface IControllerHarnessOperationBarrier {
59
+ token: string;
60
+ harnessId?: 'opencode' | 'flex';
61
+ operationIds: string[];
62
+ sessions: Array<{
63
+ projectId: string;
64
+ sessionId: IControllerRuntimeId;
65
+ }>;
66
+ }
67
+
68
+ export interface IControllerHarnessOperationManagerOptions {
69
+ onDraftChanged: (
70
+ projectIdArg: string,
71
+ sessionIdArg: IControllerRuntimeId,
72
+ updateArg: IControllerSessionDraftUpdate,
73
+ ) => void;
74
+ onOperationReleased?: (reservationArg: IControllerHarnessOperationReservation) => void;
75
+ cleanupTimeoutMs?: number;
76
+ }
77
+
78
+ interface IActiveHarnessOperation {
79
+ reservation: IControllerHarnessOperationReservation;
80
+ harnessAdmissionGeneration: number;
81
+ state: TControllerHarnessOperationState;
82
+ observedTerminal: boolean;
83
+ upload?: IControllerUploadHandle;
84
+ flexQueueKey?: string;
85
+ flexQueueId?: string;
86
+ flexRunId?: string;
87
+ releasePromise?: Promise<void>;
88
+ }
89
+
90
+ interface IPendingUploadCleanup {
91
+ active: IActiveHarnessOperation;
92
+ upload: IControllerUploadHandle;
93
+ cleanupPromise?: Promise<void>;
94
+ }
95
+
96
+ const cloneDraft = (draftArg: IControllerSessionDraft): IControllerSessionDraft => ({
97
+ text: draftArg.text,
98
+ attachments: draftArg.attachments.map((attachment) => ({ ...attachment })),
99
+ revision: draftArg.revision,
100
+ });
101
+
102
+ const cloneReservation = (
103
+ reservationArg: IControllerHarnessOperationReservation,
104
+ ): IControllerHarnessOperationReservation => ({
105
+ ...reservationArg,
106
+ sessionId: { ...reservationArg.sessionId },
107
+ draft: cloneDraft(reservationArg.draft),
108
+ });
109
+
110
+ export class ControllerHarnessOperationManager {
111
+ private readonly drafts = new ControllerDraftManager();
112
+ private readonly uploads = new ControllerUploadManager();
113
+ private readonly operationsBySession = new Map<string, IActiveHarnessOperation[]>();
114
+ private readonly operationsByOpenCodeMessage = new Map<string, IActiveHarnessOperation>();
115
+ private readonly operationsByFlexQueue = new Map<string, IActiveHarnessOperation>();
116
+ private readonly pendingUploadCleanups = new Map<string, IPendingUploadCleanup>();
117
+ private readonly harnessAdmissionGenerations = new Map<TControllerSessionHarnessId, number>();
118
+ private readonly sealedHarnessAdmissions = new Map<TControllerSessionHarnessId, number>();
119
+ private activeOperationCount = 0;
120
+ private operationBarrier?: Pick<IControllerHarnessOperationBarrier, 'token' | 'harnessId'>;
121
+ private closed = false;
122
+ private closePromise?: Promise<void>;
123
+ private readonly cleanupTimeoutMs: number;
124
+
125
+ constructor(private readonly options: IControllerHarnessOperationManagerOptions) {
126
+ const cleanupTimeoutMs = options.cleanupTimeoutMs ?? harnessOperationCleanupTimeoutMs;
127
+ if (!Number.isSafeInteger(cleanupTimeoutMs) || cleanupTimeoutMs < 1) {
128
+ throw new Error('cleanupTimeoutMs must be a positive safe integer.');
129
+ }
130
+ this.cleanupTimeoutMs = cleanupTimeoutMs;
131
+ }
132
+
133
+ public getDraft(projectIdArg: string, sessionIdArg: IControllerRuntimeId): IControllerSessionDraft {
134
+ return this.drafts.get(this.sessionKey(projectIdArg, sessionIdArg));
135
+ }
136
+
137
+ public updateDraft(
138
+ projectIdArg: string,
139
+ sessionIdArg: IControllerRuntimeId,
140
+ expectedRevisionArg: number,
141
+ patchArg: { text?: string; attachments?: readonly IControllerDraftAttachment[] },
142
+ ): IControllerSessionDraft {
143
+ if (this.closed) throw new Error('The harness operation manager is closed.');
144
+ const result = this.drafts.update(
145
+ this.sessionKey(projectIdArg, sessionIdArg),
146
+ expectedRevisionArg,
147
+ patchArg,
148
+ );
149
+ if (result.update) this.options.onDraftChanged(projectIdArg, sessionIdArg, result.update);
150
+ return result.draft;
151
+ }
152
+
153
+ public async reserve(
154
+ kindArg: TControllerHarnessOperationKind,
155
+ projectIdArg: string,
156
+ sessionIdArg: IControllerRuntimeId,
157
+ draftRevisionArg: number,
158
+ optionsArg: { allowAttachments: boolean },
159
+ ): Promise<IControllerHarnessOperationReservation> {
160
+ if (this.closed) throw new Error('The harness operation manager is closed.');
161
+ if (sessionIdArg.harnessId !== 'opencode' && sessionIdArg.harnessId !== 'flex') {
162
+ throw new Error('Only session harnesses may reserve prompt operations.');
163
+ }
164
+ const harnessId = sessionIdArg.harnessId;
165
+ const harnessAdmissionGeneration = this.currentHarnessAdmissionGeneration(harnessId);
166
+ this.assertHarnessAdmission(harnessId, harnessAdmissionGeneration);
167
+ const key = this.sessionKey(projectIdArg, sessionIdArg);
168
+ let operations = this.operationsBySession.get(key) ?? [];
169
+ if (operations.some((operation) => operation.reservation.draft.revision === draftRevisionArg)) {
170
+ throw new ControllerHarnessOperationConflictError(
171
+ 'The composer draft revision is already being submitted.',
172
+ );
173
+ }
174
+ if (this.pendingUploadCleanups.size > 0) {
175
+ await this.retryPendingUploadCleanups().catch(() => undefined);
176
+ }
177
+ if (this.closed) throw new Error('The harness operation manager is closed.');
178
+ this.assertHarnessAdmission(harnessId, harnessAdmissionGeneration);
179
+ operations = this.operationsBySession.get(key) ?? [];
180
+ if (operations.some((operation) => operation.reservation.draft.revision === draftRevisionArg)) {
181
+ throw new ControllerHarnessOperationConflictError(
182
+ 'The composer draft revision is already being submitted.',
183
+ );
184
+ }
185
+ if (
186
+ this.activeOperationCount >= maximumActiveHarnessOperations
187
+ || operations.length >= maximumHarnessOperationsPerSession
188
+ || this.pendingUploadCleanups.size >= maximumPendingUploadCleanups
189
+ ) {
190
+ throw new ControllerHarnessOperationLimitError(
191
+ 'The active harness operation limit is exhausted.',
192
+ );
193
+ }
194
+ const draft = this.drafts.snapshot(key, draftRevisionArg);
195
+ if (!optionsArg.allowAttachments && draft.attachments.length > 0) {
196
+ throw new Error('Attachments are not supported by this composer command.');
197
+ }
198
+ const reservation: IControllerHarnessOperationReservation = {
199
+ operationId: plugins.crypto.randomBytes(16).toString('base64url'),
200
+ createdAt: Date.now(),
201
+ kind: kindArg,
202
+ projectId: projectIdArg,
203
+ sessionId: { ...sessionIdArg },
204
+ draft,
205
+ promptSuffix: '',
206
+ };
207
+ const active: IActiveHarnessOperation = {
208
+ reservation,
209
+ harnessAdmissionGeneration,
210
+ state: 'submitting',
211
+ observedTerminal: false,
212
+ };
213
+ operations.push(active);
214
+ this.operationsBySession.set(key, operations);
215
+ this.activeOperationCount += 1;
216
+ try {
217
+ const upload = await this.uploads.create(draft.attachments);
218
+ if (upload) active.upload = upload;
219
+ if (this.closed || !this.operationsBySession.get(key)?.includes(active)) {
220
+ throw new Error('The harness operation reservation closed during upload materialization.');
221
+ }
222
+ this.assertHarnessAdmission(harnessId, harnessAdmissionGeneration);
223
+ if (upload) {
224
+ reservation.promptSuffix = upload.promptSuffix;
225
+ reservation.uploadDirectory = upload.directory;
226
+ }
227
+ return cloneReservation(reservation);
228
+ } catch (errorArg) {
229
+ await this.releaseActive(active);
230
+ throw errorArg;
231
+ }
232
+ }
233
+
234
+ public accept(reservationArg: IControllerHarnessOperationReservation): IControllerSessionDraft {
235
+ const active = this.requireActive(reservationArg);
236
+ if (active.state !== 'submitting') {
237
+ throw new Error('The harness operation is not awaiting submission acceptance.');
238
+ }
239
+ active.state = 'accepted';
240
+ const draft = this.clearReservationDraft(reservationArg);
241
+ if (active.observedTerminal) void this.releaseActive(active).catch(() => undefined);
242
+ return draft;
243
+ }
244
+
245
+ public bindOpenCodeMessage(
246
+ reservationArg: IControllerHarnessOperationReservation,
247
+ messageIdArg: IControllerRuntimeId,
248
+ ): void {
249
+ const active = this.requireActive(reservationArg);
250
+ if (reservationArg.sessionId.harnessId !== 'opencode' || messageIdArg.harnessId !== 'opencode') {
251
+ throw new Error('Only OpenCode operations may bind an OpenCode message ID.');
252
+ }
253
+ if (active.reservation.messageId) {
254
+ throw new Error('The harness operation already has a native message ID.');
255
+ }
256
+ const key = this.openCodeMessageKey(
257
+ reservationArg.projectId,
258
+ reservationArg.sessionId,
259
+ messageIdArg,
260
+ );
261
+ if (this.operationsByOpenCodeMessage.has(key)) {
262
+ throw new Error('The OpenCode message ID is already bound to another operation.');
263
+ }
264
+ active.reservation.messageId = { ...messageIdArg };
265
+ reservationArg.messageId = { ...messageIdArg };
266
+ this.operationsByOpenCodeMessage.set(key, active);
267
+ }
268
+
269
+ private clearReservationDraft(
270
+ reservationArg: IControllerHarnessOperationReservation,
271
+ ): IControllerSessionDraft {
272
+ const result = this.drafts.clearExact(
273
+ this.sessionKey(reservationArg.projectId, reservationArg.sessionId),
274
+ reservationArg.draft.revision,
275
+ );
276
+ if (result.update) {
277
+ this.options.onDraftChanged(
278
+ reservationArg.projectId,
279
+ reservationArg.sessionId,
280
+ result.update,
281
+ );
282
+ }
283
+ return result.draft;
284
+ }
285
+
286
+ public markOutcomeUnknown(reservationArg: IControllerHarnessOperationReservation): void {
287
+ const active = this.findActive(reservationArg);
288
+ if (!active) return;
289
+ active.state = 'outcomeUnknown';
290
+ if (active.observedTerminal) void this.releaseActive(active).catch(() => undefined);
291
+ }
292
+
293
+ public async fail(reservationArg: IControllerHarnessOperationReservation): Promise<void> {
294
+ const active = this.findActive(reservationArg);
295
+ if (active) await this.releaseActive(active);
296
+ else await this.retryPendingUploadCleanup(reservationArg.operationId);
297
+ }
298
+
299
+ public async complete(reservationArg: IControllerHarnessOperationReservation): Promise<void> {
300
+ const active = this.findActive(reservationArg);
301
+ if (active) await this.releaseActive(active);
302
+ else await this.retryPendingUploadCleanup(reservationArg.operationId);
303
+ }
304
+
305
+ public bindFlex(
306
+ reservationArg: IControllerHarnessOperationReservation,
307
+ queueIdArg: string,
308
+ runIdArg?: string,
309
+ ): void {
310
+ const active = this.requireActive(reservationArg);
311
+ const queueKey = this.flexQueueKey(
312
+ reservationArg.projectId,
313
+ reservationArg.sessionId,
314
+ queueIdArg,
315
+ );
316
+ if (this.operationsByFlexQueue.has(queueKey)) {
317
+ throw new Error('The Flex queue entry is already bound to a harness operation.');
318
+ }
319
+ active.flexQueueKey = queueKey;
320
+ active.flexQueueId = queueIdArg;
321
+ active.flexRunId = runIdArg;
322
+ this.operationsByFlexQueue.set(queueKey, active);
323
+ }
324
+
325
+ public bindFlexRun(
326
+ projectIdArg: string,
327
+ sessionIdArg: IControllerRuntimeId,
328
+ queueIdArg: string,
329
+ runIdArg: string,
330
+ ): boolean {
331
+ const active = this.operationsByFlexQueue.get(
332
+ this.flexQueueKey(projectIdArg, sessionIdArg, queueIdArg),
333
+ );
334
+ if (!active || active.flexQueueId !== queueIdArg) return false;
335
+ if (active.flexRunId && active.flexRunId !== runIdArg) {
336
+ throw new Error('The Flex queue entry was bound to conflicting run IDs.');
337
+ }
338
+ active.flexRunId = runIdArg;
339
+ return true;
340
+ }
341
+
342
+ public async completeFlex(
343
+ projectIdArg: string,
344
+ sessionIdArg: IControllerRuntimeId,
345
+ queueIdArg: string,
346
+ runIdArg?: string,
347
+ ): Promise<boolean> {
348
+ const queueKey = this.flexQueueKey(projectIdArg, sessionIdArg, queueIdArg);
349
+ const active = this.operationsByFlexQueue.get(queueKey);
350
+ if (
351
+ !active
352
+ || active.flexQueueId !== queueIdArg
353
+ || (active.flexRunId !== undefined && active.flexRunId !== runIdArg)
354
+ || (active.flexRunId === undefined && runIdArg !== undefined)
355
+ ) return false;
356
+ if (active.state === 'submitting') {
357
+ active.observedTerminal = true;
358
+ return true;
359
+ }
360
+ await this.releaseActive(active);
361
+ return true;
362
+ }
363
+
364
+ public ownsFlexRun(
365
+ projectIdArg: string,
366
+ sessionIdArg: IControllerRuntimeId,
367
+ queueIdArg: string,
368
+ runIdArg: string,
369
+ ): boolean {
370
+ if (sessionIdArg.harnessId !== 'flex') return false;
371
+ return (this.operationsBySession.get(this.sessionKey(projectIdArg, sessionIdArg)) ?? [])
372
+ .some((active) => active.flexQueueId === queueIdArg && active.flexRunId === runIdArg);
373
+ }
374
+
375
+ public async completeOpenCodeMessage(
376
+ projectIdArg: string,
377
+ sessionIdArg: IControllerRuntimeId,
378
+ messageIdArg: IControllerRuntimeId,
379
+ ): Promise<boolean> {
380
+ if (sessionIdArg.harnessId !== 'opencode' || messageIdArg.harnessId !== 'opencode') return false;
381
+ const active = this.operationsByOpenCodeMessage.get(
382
+ this.openCodeMessageKey(projectIdArg, sessionIdArg, messageIdArg),
383
+ );
384
+ if (!active) return false;
385
+ active.observedTerminal = true;
386
+ if (active.state !== 'submitting') {
387
+ await this.releaseActive(active);
388
+ }
389
+ return true;
390
+ }
391
+
392
+ public getActive(
393
+ projectIdArg: string,
394
+ sessionIdArg: IControllerRuntimeId,
395
+ ): IControllerHarnessOperationSnapshot | undefined {
396
+ const active = this.operationsBySession.get(
397
+ this.sessionKey(projectIdArg, sessionIdArg),
398
+ )?.[0];
399
+ return active ? {
400
+ reservation: cloneReservation(active.reservation),
401
+ state: active.state,
402
+ ...(active.flexQueueId ? { flexQueueId: active.flexQueueId } : {}),
403
+ ...(active.flexRunId ? { flexRunId: active.flexRunId } : {}),
404
+ } : undefined;
405
+ }
406
+
407
+ public getOperation(
408
+ reservationArg: IControllerHarnessOperationReservation,
409
+ ): IControllerHarnessOperationSnapshot | undefined {
410
+ const active = this.findActive(reservationArg);
411
+ return active ? {
412
+ reservation: cloneReservation(active.reservation),
413
+ state: active.state,
414
+ ...(active.flexQueueId ? { flexQueueId: active.flexQueueId } : {}),
415
+ ...(active.flexRunId ? { flexRunId: active.flexRunId } : {}),
416
+ } : undefined;
417
+ }
418
+
419
+ public listSessionActive(
420
+ projectIdArg: string,
421
+ sessionIdArg: IControllerRuntimeId,
422
+ ): IControllerHarnessOperationSnapshot[] {
423
+ return (this.operationsBySession.get(this.sessionKey(projectIdArg, sessionIdArg)) ?? [])
424
+ .map((active) => ({
425
+ reservation: cloneReservation(active.reservation),
426
+ state: active.state,
427
+ ...(active.flexQueueId ? { flexQueueId: active.flexQueueId } : {}),
428
+ ...(active.flexRunId ? { flexRunId: active.flexRunId } : {}),
429
+ }));
430
+ }
431
+
432
+ public bindFlexMessage(
433
+ projectIdArg: string,
434
+ sessionIdArg: IControllerRuntimeId,
435
+ queueIdArg: string,
436
+ runIdArg: string,
437
+ messageIdArg: IControllerRuntimeId,
438
+ ): boolean {
439
+ const active = this.operationsByFlexQueue.get(
440
+ this.flexQueueKey(projectIdArg, sessionIdArg, queueIdArg),
441
+ );
442
+ if (
443
+ !active
444
+ || active.flexQueueId !== queueIdArg
445
+ || active.flexRunId !== runIdArg
446
+ || messageIdArg.harnessId !== 'flex'
447
+ ) {
448
+ return false;
449
+ }
450
+ if (active.reservation.messageId) {
451
+ if (controllerRuntimeIdKey(active.reservation.messageId) === controllerRuntimeIdKey(messageIdArg)) {
452
+ return true;
453
+ }
454
+ throw new Error('The Flex queue entry was bound to conflicting message IDs.');
455
+ }
456
+ active.reservation.messageId = { ...messageIdArg };
457
+ return true;
458
+ }
459
+
460
+ public listProjectActive(projectIdArg: string): IControllerHarnessOperationSnapshot[] {
461
+ const prefix = `${projectIdArg}\0`;
462
+ return [...this.operationsBySession.entries()]
463
+ .filter(([key]) => key.startsWith(prefix))
464
+ .flatMap(([, operations]) => operations.map((active) => ({
465
+ reservation: cloneReservation(active.reservation),
466
+ state: active.state,
467
+ ...(active.flexQueueId ? { flexQueueId: active.flexQueueId } : {}),
468
+ ...(active.flexRunId ? { flexRunId: active.flexRunId } : {}),
469
+ })));
470
+ }
471
+
472
+ public listHarnessActive(
473
+ harnessIdArg: 'opencode' | 'flex',
474
+ ): IControllerHarnessOperationSnapshot[] {
475
+ return [...this.operationsBySession.values()].flat()
476
+ .filter((active) => active.reservation.sessionId.harnessId === harnessIdArg)
477
+ .map((active) => ({
478
+ reservation: cloneReservation(active.reservation),
479
+ state: active.state,
480
+ ...(active.flexQueueId ? { flexQueueId: active.flexQueueId } : {}),
481
+ ...(active.flexRunId ? { flexRunId: active.flexRunId } : {}),
482
+ }));
483
+ }
484
+
485
+ public assertExclusive(reservationArg: IControllerHarnessOperationReservation): void {
486
+ const active = this.requireActive(reservationArg);
487
+ const operations = this.operationsBySession.get(
488
+ this.sessionKey(reservationArg.projectId, reservationArg.sessionId),
489
+ ) ?? [];
490
+ if (operations.some((operation) => operation !== active)) {
491
+ throw new ControllerHarnessOperationConflictError(
492
+ 'Another harness operation is still active for this conversation.',
493
+ );
494
+ }
495
+ }
496
+
497
+ public sealHarnessAdmission(harnessIdArg: TControllerSessionHarnessId): number {
498
+ if (this.closed) throw new Error('The harness operation manager is closed.');
499
+ const generation = this.currentHarnessAdmissionGeneration(harnessIdArg) + 1;
500
+ this.harnessAdmissionGenerations.set(harnessIdArg, generation);
501
+ this.sealedHarnessAdmissions.set(harnessIdArg, generation);
502
+ return generation;
503
+ }
504
+
505
+ public reopenHarnessAdmission(
506
+ harnessIdArg: TControllerSessionHarnessId,
507
+ generationArg: number,
508
+ ): boolean {
509
+ if (
510
+ this.closed
511
+ || this.currentHarnessAdmissionGeneration(harnessIdArg) !== generationArg
512
+ || this.sealedHarnessAdmissions.get(harnessIdArg) !== generationArg
513
+ ) return false;
514
+ this.sealedHarnessAdmissions.delete(harnessIdArg);
515
+ return true;
516
+ }
517
+
518
+ public assertCurrentHarnessAdmission(
519
+ reservationArg: IControllerHarnessOperationReservation,
520
+ ): void {
521
+ const active = this.requireActive(reservationArg);
522
+ const harnessId = reservationArg.sessionId.harnessId;
523
+ if (harnessId !== 'opencode' && harnessId !== 'flex') {
524
+ throw new Error('Only session harnesses may own prompt operations.');
525
+ }
526
+ this.assertHarnessAdmission(harnessId, active.harnessAdmissionGeneration);
527
+ }
528
+
529
+ public beginOperationBarrier(
530
+ harnessIdArg?: 'opencode' | 'flex',
531
+ maximumSessionsArg = 64,
532
+ ): IControllerHarnessOperationBarrier {
533
+ if (this.closed) throw new Error('The harness operation manager is closed.');
534
+ if (this.operationBarrier) {
535
+ throw new ControllerHarnessOperationConflictError('Harness prompt admission is already sealed.');
536
+ }
537
+ const token = plugins.crypto.randomBytes(16).toString('base64url');
538
+ this.operationBarrier = { token, ...(harnessIdArg ? { harnessId: harnessIdArg } : {}) };
539
+ const operations = [...this.operationsBySession.values()].flat().filter((operation) => (
540
+ harnessIdArg === undefined || operation.reservation.sessionId.harnessId === harnessIdArg
541
+ ));
542
+ const sessionsByKey = new Map<string, {
543
+ projectId: string;
544
+ sessionId: IControllerRuntimeId;
545
+ }>();
546
+ for (const operation of operations) {
547
+ const { projectId, sessionId } = operation.reservation;
548
+ sessionsByKey.set(this.sessionKey(projectId, sessionId), {
549
+ projectId,
550
+ sessionId: { ...sessionId },
551
+ });
552
+ }
553
+ if (sessionsByKey.size > maximumSessionsArg) {
554
+ this.operationBarrier = undefined;
555
+ throw new ControllerHarnessOperationLimitError(
556
+ 'Too many active sessions are present for a bounded harness handoff.',
557
+ );
558
+ }
559
+ return {
560
+ token,
561
+ ...(harnessIdArg ? { harnessId: harnessIdArg } : {}),
562
+ operationIds: operations.map((operation) => operation.reservation.operationId),
563
+ sessions: [...sessionsByKey.values()],
564
+ };
565
+ }
566
+
567
+ public beginUpgradeBarrier(maximumSessionsArg = 64): IControllerHarnessOperationBarrier {
568
+ return this.beginOperationBarrier(undefined, maximumSessionsArg);
569
+ }
570
+
571
+ public async waitForOperationBarrier(
572
+ barrierArg: IControllerHarnessOperationBarrier,
573
+ timeoutMsArg: number,
574
+ signalArg?: AbortSignal,
575
+ ): Promise<boolean> {
576
+ this.assertOperationBarrier(barrierArg);
577
+ const pendingIds = new Set(barrierArg.operationIds);
578
+ const deadline = Date.now() + timeoutMsArg;
579
+ while (pendingIds.size > 0 && Date.now() < deadline) {
580
+ signalArg?.throwIfAborted();
581
+ const activeIds = new Set(
582
+ [...this.operationsBySession.values()].flat()
583
+ .map((operation) => operation.reservation.operationId),
584
+ );
585
+ for (const operationId of pendingIds) {
586
+ if (!activeIds.has(operationId)) pendingIds.delete(operationId);
587
+ }
588
+ if (pendingIds.size > 0) {
589
+ await new Promise((resolve) => setTimeout(resolve, 50));
590
+ }
591
+ }
592
+ signalArg?.throwIfAborted();
593
+ return pendingIds.size === 0;
594
+ }
595
+
596
+ public waitForUpgradeBarrier(
597
+ barrierArg: IControllerHarnessOperationBarrier,
598
+ timeoutMsArg: number,
599
+ signalArg?: AbortSignal,
600
+ ): Promise<boolean> {
601
+ return this.waitForOperationBarrier(barrierArg, timeoutMsArg, signalArg);
602
+ }
603
+
604
+ public endOperationBarrier(barrierArg: IControllerHarnessOperationBarrier): void {
605
+ this.assertOperationBarrier(barrierArg);
606
+ this.operationBarrier = undefined;
607
+ }
608
+
609
+ public endUpgradeBarrier(barrierArg: IControllerHarnessOperationBarrier): void {
610
+ this.endOperationBarrier(barrierArg);
611
+ }
612
+
613
+ public async purgeSession(
614
+ projectIdArg: string,
615
+ sessionIdArg: IControllerRuntimeId,
616
+ ): Promise<void> {
617
+ const key = this.sessionKey(projectIdArg, sessionIdArg);
618
+ const operations = [...(this.operationsBySession.get(key) ?? [])];
619
+ await Promise.all(operations.map((active) => this.releaseActive(active)));
620
+ this.drafts.purge(key);
621
+ }
622
+
623
+ public async releaseSession(
624
+ projectIdArg: string,
625
+ sessionIdArg: IControllerRuntimeId,
626
+ ): Promise<void> {
627
+ const operations = [
628
+ ...(this.operationsBySession.get(this.sessionKey(projectIdArg, sessionIdArg)) ?? []),
629
+ ];
630
+ await Promise.all(operations.map((active) => this.releaseActive(active)));
631
+ }
632
+
633
+ public async purgeProject(projectIdArg: string): Promise<void> {
634
+ const prefix = `${projectIdArg}\0`;
635
+ const operations = [...this.operationsBySession.entries()]
636
+ .filter(([key]) => key.startsWith(prefix))
637
+ .flatMap(([, active]) => active);
638
+ await Promise.all(operations.map((active) => this.releaseActive(active)));
639
+ this.drafts.purgePrefix(prefix);
640
+ }
641
+
642
+ public async releaseHarness(harnessIdArg: 'opencode' | 'flex'): Promise<void> {
643
+ const operations = [...this.operationsBySession.values()].flat().filter(
644
+ (active) => active.reservation.sessionId.harnessId === harnessIdArg,
645
+ );
646
+ await Promise.all(operations.map((active) => this.releaseActive(active)));
647
+ }
648
+
649
+ public async close(): Promise<void> {
650
+ if (this.closePromise) return this.closePromise;
651
+ this.closed = true;
652
+ this.operationBarrier = undefined;
653
+ const closePromise = (async () => {
654
+ await Promise.allSettled([...this.operationsBySession.values()].map(
655
+ (operations) => Promise.all(operations.map((active) => this.releaseActive(active))),
656
+ ));
657
+ this.drafts.clear();
658
+ const errors: unknown[] = [];
659
+ try {
660
+ await this.waitForCleanup(
661
+ this.uploads.close(),
662
+ 'Harness upload-manager cleanup exceeded its deadline.',
663
+ );
664
+ } catch (errorArg) {
665
+ errors.push(errorArg);
666
+ }
667
+ try {
668
+ await this.retryPendingUploadCleanups();
669
+ } catch (errorArg) {
670
+ errors.push(errorArg);
671
+ }
672
+ if (errors.length > 0) {
673
+ throw new AggregateError(errors, 'Harness operation resource cleanup failed.');
674
+ }
675
+ })();
676
+ this.closePromise = closePromise;
677
+ try {
678
+ await closePromise;
679
+ } catch (errorArg) {
680
+ if (this.closePromise === closePromise) this.closePromise = undefined;
681
+ throw errorArg;
682
+ }
683
+ }
684
+
685
+ private sessionKey(projectIdArg: string, sessionIdArg: IControllerRuntimeId): string {
686
+ return `${projectIdArg}\0${controllerRuntimeIdKey(sessionIdArg)}`;
687
+ }
688
+
689
+ private assertOperationBarrier(barrierArg: IControllerHarnessOperationBarrier): void {
690
+ if (
691
+ !this.operationBarrier
692
+ || barrierArg.token !== this.operationBarrier.token
693
+ || barrierArg.harnessId !== this.operationBarrier.harnessId
694
+ ) {
695
+ throw new Error('The harness operation admission barrier is no longer authoritative.');
696
+ }
697
+ }
698
+
699
+ private currentHarnessAdmissionGeneration(harnessIdArg: TControllerSessionHarnessId): number {
700
+ return this.harnessAdmissionGenerations.get(harnessIdArg) ?? 0;
701
+ }
702
+
703
+ private assertHarnessAdmission(
704
+ harnessIdArg: TControllerSessionHarnessId,
705
+ expectedGenerationArg: number,
706
+ ): void {
707
+ if (this.currentHarnessAdmissionGeneration(harnessIdArg) !== expectedGenerationArg) {
708
+ throw new ControllerHarnessOperationConflictError(
709
+ 'The harness prompt belongs to a stale runtime generation.',
710
+ );
711
+ }
712
+ if (
713
+ this.sealedHarnessAdmissions.has(harnessIdArg)
714
+ || (
715
+ this.operationBarrier
716
+ && (
717
+ this.operationBarrier.harnessId === undefined
718
+ || this.operationBarrier.harnessId === harnessIdArg
719
+ )
720
+ )
721
+ ) {
722
+ throw new ControllerHarnessOperationConflictError(
723
+ 'Harness prompt admission is temporarily sealed.',
724
+ );
725
+ }
726
+ }
727
+
728
+ private flexQueueKey(
729
+ projectIdArg: string,
730
+ sessionIdArg: IControllerRuntimeId,
731
+ queueIdArg: string,
732
+ ): string {
733
+ return `${this.sessionKey(projectIdArg, sessionIdArg)}\0${queueIdArg}`;
734
+ }
735
+
736
+ private openCodeMessageKey(
737
+ projectIdArg: string,
738
+ sessionIdArg: IControllerRuntimeId,
739
+ messageIdArg: IControllerRuntimeId,
740
+ ): string {
741
+ return `${this.sessionKey(projectIdArg, sessionIdArg)}\0${controllerRuntimeIdKey(messageIdArg)}`;
742
+ }
743
+
744
+ private findActive(
745
+ reservationArg: IControllerHarnessOperationReservation,
746
+ ): IActiveHarnessOperation | undefined {
747
+ const operations = this.operationsBySession.get(
748
+ this.sessionKey(reservationArg.projectId, reservationArg.sessionId),
749
+ );
750
+ return operations?.find(
751
+ (active) => active.reservation.operationId === reservationArg.operationId,
752
+ );
753
+ }
754
+
755
+ private requireActive(
756
+ reservationArg: IControllerHarnessOperationReservation,
757
+ ): IActiveHarnessOperation {
758
+ const active = this.findActive(reservationArg);
759
+ if (!active) throw new Error('The harness operation reservation is no longer active.');
760
+ return active;
761
+ }
762
+
763
+ private async releaseActive(activeArg: IActiveHarnessOperation): Promise<void> {
764
+ if (activeArg.releasePromise) return activeArg.releasePromise;
765
+ const releasePromise = (async () => {
766
+ if (activeArg.upload) await this.cleanupUpload(activeArg, activeArg.upload);
767
+ this.finalizeRelease(activeArg);
768
+ })();
769
+ activeArg.releasePromise = releasePromise;
770
+ try {
771
+ await releasePromise;
772
+ } finally {
773
+ if (activeArg.releasePromise === releasePromise) activeArg.releasePromise = undefined;
774
+ }
775
+ }
776
+
777
+ private async waitForCleanup(cleanupArg: Promise<void>, messageArg: string): Promise<void> {
778
+ let timeout: NodeJS.Timeout | undefined;
779
+ try {
780
+ await Promise.race([
781
+ cleanupArg,
782
+ new Promise<never>((_resolve, reject) => {
783
+ timeout = setTimeout(() => reject(new Error(messageArg)), this.cleanupTimeoutMs);
784
+ }),
785
+ ]);
786
+ } finally {
787
+ if (timeout) clearTimeout(timeout);
788
+ }
789
+ }
790
+
791
+ private finalizeRelease(activeArg: IActiveHarnessOperation): void {
792
+ const key = this.sessionKey(activeArg.reservation.projectId, activeArg.reservation.sessionId);
793
+ const operations = this.operationsBySession.get(key);
794
+ const operationIndex = operations?.indexOf(activeArg) ?? -1;
795
+ const wasActive = operations !== undefined && operationIndex >= 0;
796
+ if (operations && operationIndex >= 0) {
797
+ operations.splice(operationIndex, 1);
798
+ this.activeOperationCount -= 1;
799
+ if (operations.length === 0) this.operationsBySession.delete(key);
800
+ }
801
+ if (
802
+ activeArg.flexQueueKey
803
+ && this.operationsByFlexQueue.get(activeArg.flexQueueKey) === activeArg
804
+ ) this.operationsByFlexQueue.delete(activeArg.flexQueueKey);
805
+ const messageId = activeArg.reservation.messageId;
806
+ if (messageId?.harnessId === 'opencode') {
807
+ const messageKey = this.openCodeMessageKey(
808
+ activeArg.reservation.projectId,
809
+ activeArg.reservation.sessionId,
810
+ messageId,
811
+ );
812
+ if (this.operationsByOpenCodeMessage.get(messageKey) === activeArg) {
813
+ this.operationsByOpenCodeMessage.delete(messageKey);
814
+ }
815
+ }
816
+ if (!wasActive) return;
817
+ queueMicrotask(() => {
818
+ try {
819
+ this.options.onOperationReleased?.(cloneReservation(activeArg.reservation));
820
+ } catch {
821
+ // Release ownership must not depend on observer behavior.
822
+ }
823
+ });
824
+ }
825
+
826
+ private cleanupUpload(
827
+ activeArg: IActiveHarnessOperation,
828
+ uploadArg: IControllerUploadHandle,
829
+ ): Promise<void> {
830
+ const operationId = activeArg.reservation.operationId;
831
+ let pending = this.pendingUploadCleanups.get(operationId);
832
+ if (!pending) {
833
+ pending = { active: activeArg, upload: uploadArg };
834
+ this.pendingUploadCleanups.set(operationId, pending);
835
+ }
836
+ if (!pending.cleanupPromise) {
837
+ const cleanupPromise = pending.upload.cleanup().then(() => {
838
+ if (pending!.active.upload === pending!.upload) pending!.active.upload = undefined;
839
+ if (this.pendingUploadCleanups.get(operationId) === pending) {
840
+ this.pendingUploadCleanups.delete(operationId);
841
+ }
842
+ this.finalizeRelease(pending!.active);
843
+ }).finally(() => {
844
+ if (pending!.cleanupPromise === cleanupPromise) pending!.cleanupPromise = undefined;
845
+ });
846
+ pending.cleanupPromise = cleanupPromise;
847
+ }
848
+ return this.waitForCleanup(
849
+ pending.cleanupPromise,
850
+ 'Harness operation upload cleanup exceeded its deadline.',
851
+ );
852
+ }
853
+
854
+ private async retryPendingUploadCleanup(operationIdArg: string): Promise<void> {
855
+ const pending = this.pendingUploadCleanups.get(operationIdArg);
856
+ if (pending) await this.releaseActive(pending.active);
857
+ }
858
+
859
+ private async retryPendingUploadCleanups(): Promise<void> {
860
+ const results = await Promise.allSettled(
861
+ [...this.pendingUploadCleanups.values()].map((pending) => this.releaseActive(pending.active)),
862
+ );
863
+ const errors = results
864
+ .filter((result): result is PromiseRejectedResult => result.status === 'rejected')
865
+ .map((result) => result.reason);
866
+ if (errors.length > 0) {
867
+ throw new AggregateError(errors, 'Harness operation upload cleanup retry failed.');
868
+ }
869
+ }
870
+ }