@jskit-ai/assistant-core 0.1.200 → 0.1.202

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 (164) hide show
  1. package/examples/conversation/README.md +8 -1
  2. package/examples/conversation/package.json +5 -5
  3. package/fixtures/responsive-assistant/App.vue +4 -1
  4. package/package.json +99 -36
  5. package/src/client/conversation/AssistantConversationElement.vue +27 -4
  6. package/src/client/conversation/AssistantConversationStatus.vue +24 -0
  7. package/src/client/conversation/AssistantGoalControl.vue +8 -5
  8. package/src/client/conversation/AssistantTranscript.vue +10 -3
  9. package/src/client/conversation/index.js +2 -0
  10. package/src/client/conversation/messageDelivery.js +115 -32
  11. package/src/client/conversation/retainedConversation.js +32 -0
  12. package/src/client/conversation/useAssistantQuestions.js +76 -0
  13. package/src/client/lib/assistantApi.js +72 -2
  14. package/src/server/conversation/agentRun.js +222 -0
  15. package/src/server/conversation/attachments.js +93 -0
  16. package/src/server/conversation/claudeClient.js +221 -0
  17. package/src/server/conversation/claudeHistory.js +256 -0
  18. package/src/server/conversation/claudeProcess.js +327 -0
  19. package/src/server/conversation/claudeTools.js +42 -0
  20. package/src/server/conversation/claudeTurn.js +1082 -0
  21. package/src/server/conversation/codexConfiguration.js +1183 -0
  22. package/src/server/conversation/codexConversations.js +1082 -0
  23. package/src/server/conversation/codexDetachedTurn.js +332 -0
  24. package/src/server/conversation/codexEvents.js +194 -1
  25. package/src/server/conversation/codexGoalCommands.js +124 -0
  26. package/src/server/conversation/codexHelperExecution.js +306 -0
  27. package/src/server/conversation/codexHelperThreadLedger.js +804 -0
  28. package/src/server/conversation/codexHelperThreadRecovery.js +395 -0
  29. package/src/server/conversation/codexHelperThreads.js +638 -0
  30. package/src/server/conversation/codexHistoryAdapter.js +369 -0
  31. package/src/server/conversation/codexMessageCommands.js +663 -0
  32. package/src/server/conversation/codexMessageDelivery.js +211 -0
  33. package/src/server/conversation/codexNativeHistoryExport.js +95 -0
  34. package/src/server/conversation/codexNotificationObserver.js +482 -0
  35. package/src/server/conversation/codexNotificationQueue.js +103 -0
  36. package/src/server/conversation/codexObservationControl.js +174 -0
  37. package/src/server/conversation/codexOutput.js +931 -0
  38. package/src/server/conversation/codexProcess.js +1559 -0
  39. package/src/server/conversation/codexProcessLeader.js +14 -0
  40. package/src/server/conversation/codexProvider.js +2710 -0
  41. package/src/server/conversation/codexProviderOwner.js +1058 -0
  42. package/src/server/conversation/codexProviderSelection.js +218 -0
  43. package/src/server/conversation/codexReasoning.js +314 -0
  44. package/src/server/conversation/codexRenewalCommands.js +290 -0
  45. package/src/server/conversation/codexResultSettlement.js +409 -0
  46. package/src/server/conversation/codexRunJournal.js +658 -0
  47. package/src/server/conversation/codexRuntimeLifecycle.js +288 -0
  48. package/src/server/conversation/codexSessionCleanup.js +96 -0
  49. package/src/server/conversation/codexSessionReconciliation.js +284 -0
  50. package/src/server/conversation/codexThreadReadiness.js +397 -0
  51. package/src/server/conversation/codexThreadStatus.js +354 -0
  52. package/src/server/conversation/codexTurn.js +701 -255
  53. package/src/server/conversation/codexTurnControl.js +317 -0
  54. package/src/server/conversation/codexTurnRecovery.js +990 -0
  55. package/src/server/conversation/codexTurnState.js +549 -0
  56. package/src/server/conversation/commandHook.js +18 -0
  57. package/src/server/conversation/commandWrapper.js +37 -0
  58. package/src/server/conversation/configuration.js +43 -0
  59. package/src/server/conversation/continuity.js +289 -0
  60. package/src/server/conversation/fileStorage.js +55 -0
  61. package/src/server/conversation/hookBridge.js +19 -0
  62. package/src/server/conversation/index.js +13 -0
  63. package/src/server/conversation/localExecution.js +665 -0
  64. package/src/server/conversation/memoryStorage.js +7 -72
  65. package/src/server/conversation/nativeHistoryExport.js +90 -0
  66. package/src/server/conversation/nativeStorage.js +23 -0
  67. package/src/server/conversation/openCodeClient.js +209 -19
  68. package/src/server/conversation/openCodePlugin.js +84 -0
  69. package/src/server/conversation/openCodeProcess.js +476 -0
  70. package/src/server/conversation/openCodeRuntime.js +1578 -0
  71. package/src/server/conversation/openCodeTools.js +60 -0
  72. package/src/server/conversation/openCodeTurn.js +557 -0
  73. package/src/server/conversation/provider.js +30 -0
  74. package/src/server/conversation/providers/api.js +136 -0
  75. package/src/server/conversation/providers/claude.js +63 -0
  76. package/src/server/conversation/providers/claudeDriver.js +425 -0
  77. package/src/server/conversation/providers/codex.js +248 -0
  78. package/src/server/conversation/providers/codexDriver.js +1078 -0
  79. package/src/server/conversation/providers/opencode.js +49 -0
  80. package/src/server/conversation/providers/opencodeDriver.js +650 -0
  81. package/src/server/conversation/providers/scoped.js +312 -0
  82. package/src/server/conversation/runtime.js +2220 -0
  83. package/src/server/conversation/runtimeStateUpgrade.js +125 -0
  84. package/src/server/conversation/storage.js +184 -0
  85. package/src/server/conversation/streams.js +7 -2
  86. package/src/server/conversation/structuredOutput.js +240 -0
  87. package/src/server/conversation/systemPrompt.js +54 -0
  88. package/src/server/conversation/tools.js +84 -0
  89. package/src/server/conversation/transcript.js +5 -4
  90. package/src/server/hosts/claudeHistory.js +5 -0
  91. package/src/server/hosts/claudeProcess.js +11 -0
  92. package/src/server/hosts/claudeTurn.js +4 -0
  93. package/src/server/hosts/codexConfiguration.js +20 -0
  94. package/src/server/hosts/codexEvents.js +13 -0
  95. package/src/server/hosts/codexProcess.js +12 -0
  96. package/src/server/hosts/codexProvider.js +23 -0
  97. package/src/server/hosts/codexTurn.js +20 -0
  98. package/src/server/hosts/nativeHistory.js +3 -0
  99. package/src/server/hosts/openCodeClient.js +3 -0
  100. package/src/server/hosts/openCodeProcess.js +23 -0
  101. package/src/server/hosts/openCodeTurn.js +7 -0
  102. package/src/server/index.js +2 -0
  103. package/src/server/lib/aiConnectionClient.js +13 -6
  104. package/src/server/lib/assistantCompletion.js +190 -0
  105. package/src/server/lib/assistantToolLoop.js +314 -0
  106. package/src/server/lib/serviceToolCatalog.js +25 -10
  107. package/src/shared/assistantResponseText.js +29 -0
  108. package/src/shared/conversation/attachments.js +19 -0
  109. package/src/shared/conversation/index.js +3 -0
  110. package/src/shared/conversation/pagination.js +78 -0
  111. package/src/shared/conversation/patches.js +150 -0
  112. package/src/shared/conversation/questions.js +48 -0
  113. package/src/shared/conversation/streaming.js +7 -4
  114. package/src/shared/index.js +2 -0
  115. package/src/shared/nativeProviders.js +45 -0
  116. package/src/testing/conversationStorageContract.js +31 -1
  117. package/src/testing/nativeCodex.js +11 -0
  118. package/src/testing/nativeOpenCode.js +3 -0
  119. package/test/aiConnectionClient.test.js +2 -0
  120. package/test/apiConversationDriver.test.js +73 -0
  121. package/test/assistantApiSurfaceHeader.test.js +85 -0
  122. package/test/assistantBoundedToolLoop.test.js +285 -0
  123. package/test/assistantScroll.browser.test.js +16 -0
  124. package/test/claudeClient.test.js +189 -0
  125. package/test/claudeConversationSession.test.js +1173 -0
  126. package/test/claudeInstructionResume.test.js +82 -0
  127. package/test/claudeProcess.test.js +204 -0
  128. package/test/claudeTurn.test.js +253 -0
  129. package/test/codexConversationSession.test.js +2122 -0
  130. package/test/codexGoal.test.js +37 -0
  131. package/test/codexHelperDeletion.test.js +163 -0
  132. package/test/codexHelperThreadLedger.test.js +494 -0
  133. package/test/codexHistoryAdapter.test.js +541 -0
  134. package/test/codexInteractiveArguments.test.js +30 -0
  135. package/test/codexIsolation.test.js +756 -0
  136. package/test/codexModelCatalog.test.js +362 -0
  137. package/test/codexNativeHistoryExport.test.js +81 -0
  138. package/test/codexThreadSelection.test.js +136 -0
  139. package/test/codexTurn.test.js +2158 -0
  140. package/test/conversation.test.js +110 -0
  141. package/test/conversationAgentRun.test.js +326 -0
  142. package/test/conversationApiFacilities.test.js +90 -0
  143. package/test/conversationAttachments.test.js +238 -0
  144. package/test/conversationCommandWrapper.test.js +48 -0
  145. package/test/conversationConfiguration.test.js +174 -0
  146. package/test/conversationNativeStorage.test.js +25 -0
  147. package/test/conversationReplacement.test.js +172 -0
  148. package/test/conversationRuntime.test.js +241 -0
  149. package/test/conversationRuntimeStateUpgrade.test.js +144 -0
  150. package/test/conversationSession.test.js +1080 -0
  151. package/test/conversationStreaming.test.js +68 -0
  152. package/test/conversationSystemPrompt.test.js +84 -0
  153. package/test/conversationTools.test.js +416 -0
  154. package/test/fileConversationStorage.test.js +73 -0
  155. package/test/fixtures/codexHelperThreadLedger.js +39 -0
  156. package/test/localConversationExecution.test.js +90 -0
  157. package/test/messageDelivery.test.js +234 -1
  158. package/test/nativeConversationRetirement.test.js +215 -0
  159. package/test/openCodeClient.test.js +515 -0
  160. package/test/openCodeProcess.test.js +177 -0
  161. package/test/openCodeRuntime.test.js +322 -0
  162. package/test/openCodeTools.test.js +287 -0
  163. package/test/opencodeConversationSession.test.js +789 -0
  164. package/test/serviceToolCatalog.test.js +24 -0
@@ -0,0 +1,804 @@
1
+ import crypto from "node:crypto";
2
+ import { link, mkdir, open, readdir, rename, rm } from "node:fs/promises";
3
+ import path from "node:path";
4
+
5
+ import { tryAcquireExclusiveFileLock } from "@jskit-ai/kernel/server/support";
6
+
7
+ const CODEX_HELPER_THREAD_LEDGER_SCHEMA_VERSION = 1;
8
+ const CODEX_HELPER_THREAD_LEDGER_DIRECTORY = "codex-helper-thread-ownership";
9
+ const CODEX_HELPER_THREAD_LEDGER_MAX_FILE_BYTES = 64 * 1024;
10
+ const CODEX_HELPER_THREAD_LEDGER_MAX_RECORDS = 1024;
11
+ const CODEX_HELPER_THREAD_LEDGER_LOCK_WAIT_MS = 5000;
12
+ const CODEX_HELPER_THREAD_LEDGER_LOCK_POLL_MS = 20;
13
+ const CODEX_HELPER_THREAD_LIFECYCLES = Object.freeze({
14
+ ACTIVE: "active",
15
+ CLEANUP_REQUIRED: "cleanup_required",
16
+ READY: "ready",
17
+ STARTING_TURN: "starting_turn"
18
+ });
19
+ const CODEX_HELPER_THREAD_LIFECYCLE_VALUES = new Set(
20
+ Object.values(CODEX_HELPER_THREAD_LIFECYCLES)
21
+ );
22
+ const RECORD_KEYS = Object.freeze([
23
+ "createdAt",
24
+ "executionProfile",
25
+ "identity",
26
+ "lifecycle",
27
+ "ownershipId",
28
+ "projectContextRoot",
29
+ "projectRuntimeRoot",
30
+ "revision",
31
+ "schemaVersion",
32
+ "sessionId",
33
+ "threadId",
34
+ "turnId",
35
+ "updatedAt",
36
+ "workdir"
37
+ ]);
38
+ const IDENTITY_KEYS = Object.freeze([
39
+ "providerId",
40
+ "providerKeyFingerprint",
41
+ "runtime",
42
+ "server",
43
+ "transportId"
44
+ ]);
45
+ const RUNTIME_IDENTITY_KEYS = Object.freeze([
46
+ "accountIdentitySignature",
47
+ "authStateSignature",
48
+ "endpoint",
49
+ "executionMode",
50
+ "executionContextHash",
51
+ "provider",
52
+ "runtimeDir",
53
+ "runtimesHash",
54
+ "terminalEnvHash",
55
+ "toolHomeSource",
56
+ "transport"
57
+ ]);
58
+ const SERVER_IDENTITY_KEYS = Object.freeze([
59
+ "userAgent"
60
+ ]);
61
+
62
+ // The application retains its profile vocabulary and policy. Each owner retains
63
+ // the original module's independent mutation queue and lock-instance identity.
64
+ function createCodexHelperThreadLedgerOwner({
65
+ executionProfile,
66
+ snapshotExecutionProfile = executionProfile,
67
+ errorPrefix = "codex_helper_"
68
+ } = {}) {
69
+ if (typeof executionProfile !== "function") {
70
+ throw new TypeError("Codex helper ownership requires its execution-profile codec.");
71
+ }
72
+ const codexHelperThreadLedgerMutationQueues = new Map();
73
+ const codexHelperThreadLedgerProcessId = crypto.randomUUID();
74
+
75
+ function normalizeText(value) {
76
+ return String(value ?? "").trim();
77
+ }
78
+
79
+ function isRecord(value) {
80
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
81
+ }
82
+
83
+ function ledgerError(message = "", code = `${errorPrefix}ledger_invalid`) {
84
+ const error = new Error(normalizeText(message) || "Codex helper ownership record is invalid.");
85
+ error.code = code;
86
+ error.retryable = true;
87
+ return error;
88
+ }
89
+
90
+ function assertExactKeys(value, expectedKeys, label) {
91
+ if (!isRecord(value)) {
92
+ throw ledgerError(`${label} must be an object.`);
93
+ }
94
+ const actual = Object.keys(value).sort();
95
+ const expected = [...expectedKeys].sort();
96
+ if (
97
+ actual.length !== expected.length ||
98
+ actual.some((key, index) => key !== expected[index])
99
+ ) {
100
+ throw ledgerError(`${label} has an unsupported shape.`);
101
+ }
102
+ }
103
+
104
+ function requiredText(value, label, maxLength = 4096) {
105
+ const normalized = normalizeText(value);
106
+ if (!normalized) {
107
+ throw ledgerError(`${label} is required.`);
108
+ }
109
+ if (normalized.length > maxLength) {
110
+ throw ledgerError(`${label} exceeds ${maxLength} characters.`);
111
+ }
112
+ return normalized;
113
+ }
114
+
115
+ function requiredSha256Signature(value, label) {
116
+ const signature = requiredText(value, label, 71);
117
+ if (!/^sha256:[a-f0-9]{64}$/u.test(signature)) {
118
+ throw ledgerError(`${label} must be a SHA-256 signature.`);
119
+ }
120
+ return signature;
121
+ }
122
+
123
+ function requiredVersionedStateSignature(value, label) {
124
+ const signature = requiredText(value, label, 32);
125
+ if (!/^v[1-9][0-9]*:[a-f0-9]{24}$/u.test(signature)) {
126
+ throw ledgerError(`${label} must be a versioned state signature.`);
127
+ }
128
+ return signature;
129
+ }
130
+
131
+ function requiredStableHash(value, label) {
132
+ const hash = requiredText(value, label, 12);
133
+ if (!/^[a-f0-9]{12}$/u.test(hash)) {
134
+ throw ledgerError(`${label} must be a stable hash.`);
135
+ }
136
+ return hash;
137
+ }
138
+
139
+ function optionalText(value, label = "value", maxLength = 4096) {
140
+ const normalized = normalizeText(value);
141
+ if (normalized.length > maxLength) {
142
+ throw ledgerError(`${label} exceeds ${maxLength} characters.`);
143
+ }
144
+ return normalized;
145
+ }
146
+
147
+ function absolutePath(value, label) {
148
+ const normalized = requiredText(value, label, 8192);
149
+ if (!path.isAbsolute(normalized) || path.normalize(normalized) !== normalized) {
150
+ throw ledgerError(`${label} must be a normalized absolute path.`);
151
+ }
152
+ return normalized;
153
+ }
154
+
155
+ function optionalAbsolutePath(value, label) {
156
+ const normalized = optionalText(value, label, 8192);
157
+ if (normalized && (!path.isAbsolute(normalized) || path.normalize(normalized) !== normalized)) {
158
+ throw ledgerError(`${label} must be a normalized absolute path when present.`);
159
+ }
160
+ return normalized;
161
+ }
162
+
163
+ function timestamp(value, label) {
164
+ const normalized = requiredText(value, label);
165
+ const parsed = Date.parse(normalized);
166
+ if (!Number.isFinite(parsed) || new Date(parsed).toISOString() !== normalized) {
167
+ throw ledgerError(`${label} must be an ISO timestamp.`);
168
+ }
169
+ return normalized;
170
+ }
171
+
172
+ function positiveInteger(value, label) {
173
+ const normalized = Number(value);
174
+ if (!Number.isSafeInteger(normalized) || normalized < 1) {
175
+ throw ledgerError(`${label} must be a positive integer.`);
176
+ }
177
+ return normalized;
178
+ }
179
+
180
+ function strictIdentity(value) {
181
+ assertExactKeys(value, IDENTITY_KEYS, "identity");
182
+ assertExactKeys(value.runtime, RUNTIME_IDENTITY_KEYS, "identity.runtime");
183
+ assertExactKeys(value.server, SERVER_IDENTITY_KEYS, "identity.server");
184
+ const runtime = value.runtime;
185
+ const providerId = requiredText(value.providerId, "identity.providerId", 64);
186
+ const executionMode = requiredText(
187
+ runtime.executionMode,
188
+ "identity.runtime.executionMode",
189
+ 64
190
+ );
191
+ const provider = requiredText(runtime.provider, "identity.runtime.provider");
192
+ const toolHomeSource = optionalAbsolutePath(
193
+ runtime.toolHomeSource,
194
+ "identity.runtime.toolHomeSource"
195
+ );
196
+ const transportId = requiredText(value.transportId, "identity.transportId", 64);
197
+ const transport = requiredText(runtime.transport, "identity.runtime.transport", 64);
198
+ const endpoint = requiredText(runtime.endpoint, "identity.runtime.endpoint", 8192);
199
+ const endpointPath = endpoint.startsWith("unix://") ? endpoint.slice("unix://".length) : "";
200
+ if (
201
+ providerId !== "codex" ||
202
+ !["helper", "interactive"].includes(executionMode) ||
203
+ provider !== "codex_app_server" ||
204
+ transport !== "unix" ||
205
+ !endpointPath ||
206
+ !path.isAbsolute(endpointPath) ||
207
+ path.normalize(endpointPath) !== endpointPath ||
208
+ transportId !== "codex_app_server"
209
+ ) {
210
+ throw ledgerError(
211
+ "Codex helper ownership identity must use the managed Codex app-server."
212
+ );
213
+ }
214
+ return Object.freeze({
215
+ providerId,
216
+ providerKeyFingerprint: requiredSha256Signature(
217
+ value.providerKeyFingerprint,
218
+ "identity.providerKeyFingerprint"
219
+ ),
220
+ runtime: Object.freeze({
221
+ accountIdentitySignature: requiredSha256Signature(
222
+ runtime.accountIdentitySignature,
223
+ "identity.runtime.accountIdentitySignature"
224
+ ),
225
+ authStateSignature: requiredVersionedStateSignature(
226
+ runtime.authStateSignature,
227
+ "identity.runtime.authStateSignature"
228
+ ),
229
+ endpoint,
230
+ executionMode,
231
+ executionContextHash: requiredStableHash(
232
+ runtime.executionContextHash,
233
+ "identity.runtime.executionContextHash"
234
+ ),
235
+ provider,
236
+ runtimeDir: absolutePath(runtime.runtimeDir, "identity.runtime.runtimeDir"),
237
+ runtimesHash: requiredStableHash(
238
+ runtime.runtimesHash,
239
+ "identity.runtime.runtimesHash"
240
+ ),
241
+ terminalEnvHash: requiredStableHash(
242
+ runtime.terminalEnvHash,
243
+ "identity.runtime.terminalEnvHash"
244
+ ),
245
+ toolHomeSource,
246
+ transport
247
+ }),
248
+ server: Object.freeze({
249
+ userAgent: requiredText(value.server.userAgent, "identity.server.userAgent", 512)
250
+ }),
251
+ transportId
252
+ });
253
+ }
254
+
255
+ function codexHelperThreadRecordId({
256
+ projectRuntimeRoot = "",
257
+ sessionId = "",
258
+ threadId = ""
259
+ } = {}) {
260
+ return crypto.createHash("sha256").update([
261
+ absolutePath(projectRuntimeRoot, "projectRuntimeRoot"),
262
+ requiredText(sessionId, "sessionId", 256),
263
+ requiredText(threadId, "threadId", 256)
264
+ ].join("\0")).digest("hex");
265
+ }
266
+
267
+ function defineCodexHelperThreadRecord(value = {}) {
268
+ assertExactKeys(value, RECORD_KEYS, "Codex helper ownership record");
269
+ if (Number(value.schemaVersion) !== CODEX_HELPER_THREAD_LEDGER_SCHEMA_VERSION) {
270
+ throw ledgerError(
271
+ `Unsupported Codex helper ownership schema: ${String(value.schemaVersion ?? "missing")}.`
272
+ );
273
+ }
274
+ const lifecycle = requiredText(value.lifecycle, "lifecycle");
275
+ if (!CODEX_HELPER_THREAD_LIFECYCLE_VALUES.has(lifecycle)) {
276
+ throw ledgerError(`Unsupported Codex helper ownership lifecycle: ${lifecycle}.`);
277
+ }
278
+ const turnId = optionalText(value.turnId);
279
+ if (lifecycle === CODEX_HELPER_THREAD_LIFECYCLES.ACTIVE && !turnId) {
280
+ throw ledgerError("An active Codex helper ownership record requires turnId.");
281
+ }
282
+ if (lifecycle === CODEX_HELPER_THREAD_LIFECYCLES.READY && turnId) {
283
+ throw ledgerError(`${lifecycle} Codex helper ownership cannot retain turnId.`);
284
+ }
285
+ const createdAt = timestamp(value.createdAt, "createdAt");
286
+ const updatedAt = timestamp(value.updatedAt, "updatedAt");
287
+ if (Date.parse(updatedAt) < Date.parse(createdAt)) {
288
+ throw ledgerError("updatedAt cannot precede createdAt.");
289
+ }
290
+ return Object.freeze({
291
+ createdAt,
292
+ executionProfile: executionProfile(value.executionProfile),
293
+ identity: strictIdentity(value.identity),
294
+ lifecycle,
295
+ ownershipId: requiredText(value.ownershipId, "ownershipId", 128),
296
+ projectContextRoot: absolutePath(value.projectContextRoot, "projectContextRoot"),
297
+ projectRuntimeRoot: absolutePath(value.projectRuntimeRoot, "projectRuntimeRoot"),
298
+ revision: positiveInteger(value.revision, "revision"),
299
+ schemaVersion: CODEX_HELPER_THREAD_LEDGER_SCHEMA_VERSION,
300
+ sessionId: requiredText(value.sessionId, "sessionId", 256),
301
+ threadId: requiredText(value.threadId, "threadId", 256),
302
+ turnId: optionalText(turnId, "turnId", 256),
303
+ updatedAt,
304
+ workdir: absolutePath(value.workdir, "workdir")
305
+ });
306
+ }
307
+
308
+ function codexHelperThreadLedgerRoot(projectRuntimeRoot = "") {
309
+ return path.join(
310
+ absolutePath(projectRuntimeRoot, "projectRuntimeRoot"),
311
+ CODEX_HELPER_THREAD_LEDGER_DIRECTORY
312
+ );
313
+ }
314
+
315
+ function recordsMatch(left = {}, right = {}) {
316
+ return JSON.stringify(left) === JSON.stringify(right);
317
+ }
318
+
319
+ function stableOwnershipMatches(left = {}, right = {}) {
320
+ return left.createdAt === right.createdAt &&
321
+ JSON.stringify(left.executionProfile) === JSON.stringify(right.executionProfile) &&
322
+ JSON.stringify(left.identity) === JSON.stringify(right.identity) &&
323
+ left.ownershipId === right.ownershipId &&
324
+ left.projectContextRoot === right.projectContextRoot &&
325
+ left.projectRuntimeRoot === right.projectRuntimeRoot &&
326
+ left.sessionId === right.sessionId &&
327
+ left.threadId === right.threadId &&
328
+ left.workdir === right.workdir;
329
+ }
330
+
331
+ function delay(milliseconds = 0) {
332
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
333
+ }
334
+
335
+ async function readBoundedFile(
336
+ filePath = "",
337
+ maxBytes = CODEX_HELPER_THREAD_LEDGER_MAX_FILE_BYTES
338
+ ) {
339
+ const handle = await open(filePath, "r");
340
+ try {
341
+ const buffer = Buffer.alloc(maxBytes + 1);
342
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
343
+ if (bytesRead > maxBytes) {
344
+ throw ledgerError(
345
+ `Codex helper ownership state exceeds ${maxBytes} bytes: ${path.basename(filePath)}.`
346
+ );
347
+ }
348
+ return buffer.subarray(0, bytesRead).toString("utf8");
349
+ } finally {
350
+ await handle.close();
351
+ }
352
+ }
353
+
354
+ async function syncDirectory(directory = "") {
355
+ const handle = await open(directory, "r");
356
+ try {
357
+ await handle.sync();
358
+ } finally {
359
+ await handle.close();
360
+ }
361
+ }
362
+
363
+ async function readRecordFile(filePath = "") {
364
+ let value;
365
+ try {
366
+ value = JSON.parse(await readBoundedFile(filePath));
367
+ } catch (error) {
368
+ if (error?.code === "ENOENT") {
369
+ throw error;
370
+ }
371
+ throw ledgerError(
372
+ `Codex helper ownership record cannot be read: ${path.basename(filePath)} (${error.message})`
373
+ );
374
+ }
375
+ const record = defineCodexHelperThreadRecord(value);
376
+ if (path.basename(filePath) !== `${codexHelperThreadRecordId(record)}.json`) {
377
+ throw ledgerError(
378
+ `Codex helper ownership record filename does not match its identity: ${path.basename(filePath)}.`
379
+ );
380
+ }
381
+ return record;
382
+ }
383
+
384
+ async function withMutationQueue(key = "", callback = async () => undefined) {
385
+ const previous = codexHelperThreadLedgerMutationQueues.get(key) || Promise.resolve();
386
+ let release = () => undefined;
387
+ const current = new Promise((resolve) => {
388
+ release = resolve;
389
+ });
390
+ codexHelperThreadLedgerMutationQueues.set(key, current);
391
+ await previous.catch(() => null);
392
+ try {
393
+ return await callback();
394
+ } finally {
395
+ release();
396
+ if (codexHelperThreadLedgerMutationQueues.get(key) === current) {
397
+ codexHelperThreadLedgerMutationQueues.delete(key);
398
+ }
399
+ }
400
+ }
401
+
402
+ function defineLockOwner(value = {}) {
403
+ assertExactKeys(
404
+ value,
405
+ ["createdAt", "pid", "processId", "token"],
406
+ "Codex helper ownership lock"
407
+ );
408
+ return Object.freeze({
409
+ createdAt: timestamp(value.createdAt, "lock.createdAt"),
410
+ pid: positiveInteger(value.pid, "lock.pid"),
411
+ processId: requiredText(value.processId, "lock.processId", 128),
412
+ token: requiredText(value.token, "lock.token", 128)
413
+ });
414
+ }
415
+
416
+ async function readLockOwner(lockPath = "") {
417
+ try {
418
+ return defineLockOwner(JSON.parse(await readBoundedFile(lockPath, 4096)));
419
+ } catch (error) {
420
+ if (error?.code === "ENOENT") {
421
+ throw error;
422
+ }
423
+ throw ledgerError(
424
+ `Codex helper ownership lock cannot be verified: ${path.basename(lockPath)} (${error.message})`
425
+ );
426
+ }
427
+ }
428
+
429
+ function lockOwnerIsAlive(owner = {}) {
430
+ try {
431
+ process.kill(owner.pid, 0);
432
+ return true;
433
+ } catch (error) {
434
+ if (error?.code === "ESRCH") {
435
+ return false;
436
+ }
437
+ if (error?.code === "EPERM") {
438
+ return true;
439
+ }
440
+ throw error;
441
+ }
442
+ }
443
+
444
+ async function acquireHostLock(root = "", recordId = "", {
445
+ observeLock = null
446
+ } = {}) {
447
+ const lockPath = path.join(
448
+ root,
449
+ `.${requiredText(recordId, "recordId", 128)}.host-lock`
450
+ );
451
+ const deadline = Date.now() + CODEX_HELPER_THREAD_LEDGER_LOCK_WAIT_MS;
452
+ while (true) {
453
+ let releaseFileLock = null;
454
+ try {
455
+ releaseFileLock = await tryAcquireExclusiveFileLock(lockPath, { cwd: root });
456
+ if (!releaseFileLock) {
457
+ await observeLock?.({ stage: "host-contended" });
458
+ if (Date.now() >= deadline) {
459
+ throw ledgerError(
460
+ "Codex helper ownership is busy in another process.",
461
+ `${errorPrefix}ledger_busy`
462
+ );
463
+ }
464
+ await delay(CODEX_HELPER_THREAD_LEDGER_LOCK_POLL_MS);
465
+ continue;
466
+ }
467
+ await observeLock?.({ stage: "host-acquired" });
468
+ let released = false;
469
+ return async () => {
470
+ if (released) {
471
+ return;
472
+ }
473
+ released = true;
474
+ try {
475
+ await releaseFileLock();
476
+ } catch (error) {
477
+ throw ledgerError(`Codex helper ownership host lock failed: ${error.message}`);
478
+ }
479
+ };
480
+ } catch (error) {
481
+ throw error?.code?.startsWith?.(errorPrefix)
482
+ ? error
483
+ : ledgerError(`Codex helper ownership host lock failed: ${error.message}`);
484
+ }
485
+ }
486
+ }
487
+
488
+ async function acquireFilesystemLock(root = "", recordId = "", {
489
+ observeLock = null
490
+ } = {}) {
491
+ const locksRoot = path.join(root, ".locks");
492
+ await mkdir(locksRoot, { mode: 0o770, recursive: true });
493
+ const releaseHostLock = await acquireHostLock(root, recordId, { observeLock });
494
+ try {
495
+ const lockPath = path.join(locksRoot, `${requiredText(recordId, "recordId", 128)}.lock`);
496
+ const deadline = Date.now() + CODEX_HELPER_THREAD_LEDGER_LOCK_WAIT_MS;
497
+ const owner = defineLockOwner({
498
+ createdAt: new Date().toISOString(),
499
+ pid: process.pid,
500
+ processId: codexHelperThreadLedgerProcessId,
501
+ token: crypto.randomUUID()
502
+ });
503
+ const candidatePath = `${lockPath}.${owner.token}.candidate`;
504
+ const candidateHandle = await open(candidatePath, "wx", 0o660);
505
+ try {
506
+ await candidateHandle.writeFile(`${JSON.stringify(owner)}\n`, "utf8");
507
+ await candidateHandle.sync();
508
+ } finally {
509
+ await candidateHandle.close();
510
+ }
511
+ await syncDirectory(locksRoot);
512
+ try {
513
+ while (true) {
514
+ try {
515
+ await link(candidatePath, lockPath);
516
+ await syncDirectory(locksRoot);
517
+ break;
518
+ } catch (error) {
519
+ if (error?.code !== "EEXIST") {
520
+ throw error;
521
+ }
522
+ let current;
523
+ try {
524
+ current = await readLockOwner(lockPath);
525
+ } catch (readError) {
526
+ if (readError?.code === "ENOENT") {
527
+ continue;
528
+ }
529
+ throw readError;
530
+ }
531
+ if (!lockOwnerIsAlive(current)) {
532
+ const verified = await readLockOwner(lockPath).catch((readError) => {
533
+ if (readError?.code === "ENOENT") {
534
+ return null;
535
+ }
536
+ throw readError;
537
+ });
538
+ if (verified?.token === current.token) {
539
+ await observeLock?.({ stage: "stale-verified" });
540
+ await rm(lockPath).catch((removeError) => {
541
+ if (removeError?.code !== "ENOENT") {
542
+ throw removeError;
543
+ }
544
+ });
545
+ await syncDirectory(locksRoot);
546
+ }
547
+ continue;
548
+ }
549
+ if (Date.now() >= deadline) {
550
+ throw ledgerError(
551
+ "Codex helper ownership is busy in another process.",
552
+ `${errorPrefix}ledger_busy`
553
+ );
554
+ }
555
+ await delay(CODEX_HELPER_THREAD_LEDGER_LOCK_POLL_MS);
556
+ }
557
+ }
558
+ } finally {
559
+ await rm(candidatePath, { force: true }).catch(() => null);
560
+ await syncDirectory(locksRoot);
561
+ }
562
+ return async () => {
563
+ try {
564
+ const current = await readLockOwner(lockPath);
565
+ if (current.token !== owner.token || current.processId !== owner.processId) {
566
+ throw ledgerError(
567
+ "Codex helper ownership lock changed before it could be released.",
568
+ `${errorPrefix}ledger_conflict`
569
+ );
570
+ }
571
+ await rm(lockPath);
572
+ await syncDirectory(locksRoot);
573
+ } finally {
574
+ await releaseHostLock();
575
+ }
576
+ };
577
+ } catch (error) {
578
+ await releaseHostLock();
579
+ throw error;
580
+ }
581
+ }
582
+
583
+ async function withFilesystemMutationLock(
584
+ root = "",
585
+ callback = async () => undefined,
586
+ options = {}
587
+ ) {
588
+ const key = `${root}\0ledger`;
589
+ return withMutationQueue(key, async () => {
590
+ const release = await acquireFilesystemLock(root, "ledger", options);
591
+ try {
592
+ return await callback();
593
+ } finally {
594
+ await release();
595
+ }
596
+ });
597
+ }
598
+
599
+ function createCodexHelperThreadLedger({
600
+ observeLock = null,
601
+ projectRuntimeRoot = ""
602
+ } = {}) {
603
+ const normalizedProjectRuntimeRoot = absolutePath(projectRuntimeRoot, "projectRuntimeRoot");
604
+ const root = codexHelperThreadLedgerRoot(normalizedProjectRuntimeRoot);
605
+
606
+ async function readAll() {
607
+ let entries;
608
+ try {
609
+ entries = await readdir(root, { withFileTypes: true });
610
+ } catch (error) {
611
+ if (error?.code === "ENOENT") {
612
+ return { failures: [], records: [] };
613
+ }
614
+ throw error;
615
+ }
616
+ const recordEntries = entries.filter(
617
+ (entry) => entry.isFile() && entry.name.endsWith(".json")
618
+ );
619
+ if (recordEntries.length > CODEX_HELPER_THREAD_LEDGER_MAX_RECORDS) {
620
+ return {
621
+ failures: [Object.freeze({
622
+ code: `${errorPrefix}ledger_invalid`,
623
+ error: `Codex helper ownership ledger exceeds ${CODEX_HELPER_THREAD_LEDGER_MAX_RECORDS} records.`,
624
+ fileName: "",
625
+ retryable: true
626
+ })],
627
+ records: []
628
+ };
629
+ }
630
+ const failures = [];
631
+ const records = [];
632
+ for (const entry of recordEntries.sort((left, right) => left.name.localeCompare(right.name))) {
633
+ const filePath = path.join(root, entry.name);
634
+ try {
635
+ records.push(await readRecordFile(filePath));
636
+ } catch (error) {
637
+ failures.push(Object.freeze({
638
+ code: normalizeText(error?.code),
639
+ error: normalizeText(error?.message || error),
640
+ fileName: entry.name,
641
+ retryable: true
642
+ }));
643
+ }
644
+ }
645
+ return { failures, records };
646
+ }
647
+
648
+ async function write(recordValue = {}, { expected = null } = {}) {
649
+ const record = defineCodexHelperThreadRecord(recordValue);
650
+ const expectedRecord = expected ? defineCodexHelperThreadRecord(expected) : null;
651
+ if (record.projectRuntimeRoot !== normalizedProjectRuntimeRoot) {
652
+ throw ledgerError("Codex helper ownership project runtime root does not match its ledger.");
653
+ }
654
+ const recordId = codexHelperThreadRecordId(record);
655
+ if (expectedRecord && codexHelperThreadRecordId(expectedRecord) !== recordId) {
656
+ throw ledgerError(
657
+ "Codex helper ownership identity cannot change during an update.",
658
+ `${errorPrefix}ledger_conflict`
659
+ );
660
+ }
661
+ if (!expectedRecord && record.revision !== 1) {
662
+ throw ledgerError("New Codex helper ownership must start at revision 1.");
663
+ }
664
+ if (expectedRecord && (
665
+ record.revision !== expectedRecord.revision + 1 ||
666
+ Date.parse(record.updatedAt) < Date.parse(expectedRecord.updatedAt) ||
667
+ !stableOwnershipMatches(record, expectedRecord)
668
+ )) {
669
+ throw ledgerError(
670
+ "Codex helper ownership update changed immutable identity, revision, or time.",
671
+ `${errorPrefix}ledger_conflict`
672
+ );
673
+ }
674
+ const filePath = path.join(root, `${recordId}.json`);
675
+ await mkdir(root, { mode: 0o770, recursive: true });
676
+ return withFilesystemMutationLock(root, async () => {
677
+ if (expectedRecord) {
678
+ let current;
679
+ try {
680
+ current = await readRecordFile(filePath);
681
+ } catch (error) {
682
+ if (error?.code === "ENOENT") {
683
+ throw ledgerError(
684
+ "Codex helper ownership changed before it could be updated.",
685
+ `${errorPrefix}ledger_conflict`
686
+ );
687
+ }
688
+ throw error;
689
+ }
690
+ if (!recordsMatch(current, expectedRecord)) {
691
+ throw ledgerError(
692
+ "Codex helper ownership changed before it could be updated.",
693
+ `${errorPrefix}ledger_conflict`
694
+ );
695
+ }
696
+ } else {
697
+ const entries = await readdir(root, { withFileTypes: true });
698
+ const recordCount = entries.filter(
699
+ (entry) => entry.isFile() && entry.name.endsWith(".json")
700
+ ).length;
701
+ if (recordCount >= CODEX_HELPER_THREAD_LEDGER_MAX_RECORDS) {
702
+ throw ledgerError(
703
+ `Codex helper ownership ledger cannot exceed ${CODEX_HELPER_THREAD_LEDGER_MAX_RECORDS} records.`
704
+ );
705
+ }
706
+ try {
707
+ await readBoundedFile(filePath);
708
+ throw ledgerError(
709
+ "Codex helper ownership already exists for this thread.",
710
+ `${errorPrefix}ledger_conflict`
711
+ );
712
+ } catch (error) {
713
+ if (error?.code !== "ENOENT") {
714
+ throw error;
715
+ }
716
+ }
717
+ }
718
+ const serialized = `${JSON.stringify(record, null, 2)}\n`;
719
+ if (Buffer.byteLength(serialized) > CODEX_HELPER_THREAD_LEDGER_MAX_FILE_BYTES) {
720
+ throw ledgerError(
721
+ `Codex helper ownership state exceeds ${CODEX_HELPER_THREAD_LEDGER_MAX_FILE_BYTES} bytes.`
722
+ );
723
+ }
724
+ const temporaryPath = path.join(
725
+ root,
726
+ `.${path.basename(filePath)}.${process.pid}.${crypto.randomUUID()}.tmp`
727
+ );
728
+ let handle = null;
729
+ try {
730
+ handle = await open(temporaryPath, "wx", 0o660);
731
+ await handle.writeFile(serialized, "utf8");
732
+ await handle.sync();
733
+ await handle.close();
734
+ handle = null;
735
+ await rename(temporaryPath, filePath);
736
+ await syncDirectory(root);
737
+ } catch (error) {
738
+ await handle?.close().catch(() => null);
739
+ await rm(temporaryPath, { force: true }).catch(() => null);
740
+ throw error;
741
+ }
742
+ return record;
743
+ }, { observeLock });
744
+ }
745
+
746
+ async function remove(expectedValue = {}) {
747
+ const expected = defineCodexHelperThreadRecord(expectedValue);
748
+ const recordId = codexHelperThreadRecordId(expected);
749
+ const filePath = path.join(root, `${recordId}.json`);
750
+ await mkdir(root, { mode: 0o770, recursive: true });
751
+ return withFilesystemMutationLock(root, async () => {
752
+ let current;
753
+ try {
754
+ current = await readRecordFile(filePath);
755
+ } catch (error) {
756
+ if (error?.code === "ENOENT") {
757
+ throw ledgerError(
758
+ "Codex helper ownership changed before it could be removed.",
759
+ `${errorPrefix}ledger_conflict`
760
+ );
761
+ }
762
+ throw error;
763
+ }
764
+ if (!recordsMatch(current, expected)) {
765
+ throw ledgerError(
766
+ "Codex helper ownership changed before it could be removed.",
767
+ `${errorPrefix}ledger_conflict`
768
+ );
769
+ }
770
+ await rm(filePath);
771
+ await syncDirectory(root);
772
+ return true;
773
+ }, { observeLock });
774
+ }
775
+
776
+ return Object.freeze({
777
+ projectRuntimeRoot: normalizedProjectRuntimeRoot,
778
+ readAll,
779
+ remove,
780
+ root,
781
+ write
782
+ });
783
+ }
784
+
785
+ function matchesExecutionProfile(recorded = {}, expected = {}) {
786
+ return JSON.stringify(recorded) === JSON.stringify(
787
+ snapshotExecutionProfile(expected)
788
+ );
789
+ }
790
+
791
+ return Object.freeze({
792
+ snapshotExecutionProfile,
793
+ matchesExecutionProfile,
794
+ codexHelperThreadRecordId,
795
+ createCodexHelperThreadLedger,
796
+ defineCodexHelperThreadRecord
797
+ });
798
+ }
799
+
800
+ export {
801
+ CODEX_HELPER_THREAD_LEDGER_SCHEMA_VERSION,
802
+ CODEX_HELPER_THREAD_LIFECYCLES,
803
+ createCodexHelperThreadLedgerOwner
804
+ };