@jskit-ai/assistant-core 0.2.7 → 0.2.8

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 (214) hide show
  1. package/examples/conversation/package.json +5 -5
  2. package/package.json +18 -7
  3. package/src/client/browser.js +21 -0
  4. package/src/client/conversation/AssistantAttachmentQueue.vue +25 -108
  5. package/src/client/conversation/AssistantComposerActions.vue +1 -1
  6. package/src/client/conversation/AssistantConversationElement.vue +14 -52
  7. package/src/client/conversation/AssistantGoalControl.vue +13 -10
  8. package/src/client/conversation/AssistantPromptInput.vue +6 -91
  9. package/src/client/conversation/AssistantTranscript.css +505 -0
  10. package/src/client/conversation/AssistantTranscript.vue +24 -1197
  11. package/src/client/conversation/attachmentFiles.js +158 -0
  12. package/src/client/conversation/attachmentQueueItems.js +121 -0
  13. package/src/client/conversation/attachmentUploadRows.js +265 -0
  14. package/src/client/conversation/conversationDisplay.js +54 -0
  15. package/src/client/conversation/messageDelivery.js +13 -9
  16. package/src/client/conversation/retainedConversation.js +14 -3
  17. package/src/client/conversation/submitText.js +13 -2
  18. package/src/client/conversation/transcriptEntries.js +225 -0
  19. package/src/client/conversation/useAssistantAttachments.js +86 -388
  20. package/src/client/conversation/useAssistantQuestions.js +16 -3
  21. package/src/client/conversation/useAssistantSuggestions.js +16 -2
  22. package/src/client/conversation/useScrollToBottom.js +17 -19
  23. package/src/client/conversation/useTextareaAutoGrow.js +115 -0
  24. package/src/client/conversation/useTranscriptScrollFollow.js +525 -0
  25. package/src/client/lib/assistantApi.js +192 -161
  26. package/src/client/types.js +110 -0
  27. package/src/server/conversation/attachments.js +21 -6
  28. package/src/server/conversation/boundedToolLoop.js +1 -0
  29. package/src/server/conversation/claudeClient.js +71 -20
  30. package/src/server/conversation/claudeCommandRun.js +191 -0
  31. package/src/server/conversation/claudeHistory.js +7 -0
  32. package/src/server/conversation/claudeNativeTurn.js +708 -0
  33. package/src/server/conversation/claudeProcess.js +186 -112
  34. package/src/server/conversation/claudeProcessStart.js +214 -0
  35. package/src/server/conversation/claudePromptDelivery.js +183 -0
  36. package/src/server/conversation/claudeSessionCleanup.js +248 -0
  37. package/src/server/conversation/claudeTools.js +15 -3
  38. package/src/server/conversation/claudeTurn.js +212 -1388
  39. package/src/server/conversation/codexAccountReader.js +168 -0
  40. package/src/server/conversation/codexAuthPreflight.js +5 -4
  41. package/src/server/conversation/codexClient.js +18 -6
  42. package/src/server/conversation/codexConfiguration.js +8 -844
  43. package/src/server/conversation/codexConversationStops.js +377 -0
  44. package/src/server/conversation/codexConversationStore.js +119 -85
  45. package/src/server/conversation/codexConversationTurns.js +378 -0
  46. package/src/server/conversation/codexConversations.js +134 -866
  47. package/src/server/conversation/codexDetachedConversation.js +203 -0
  48. package/src/server/conversation/codexDetachedTurn.js +145 -104
  49. package/src/server/conversation/codexEvents.js +5 -200
  50. package/src/server/conversation/codexGoalCommands.js +14 -4
  51. package/src/server/conversation/codexHelperExecution.js +57 -57
  52. package/src/server/conversation/codexHelperThreadLedger.js +30 -288
  53. package/src/server/conversation/codexHelperThreadRecord.js +304 -0
  54. package/src/server/conversation/codexHelperThreadRecovery.js +54 -76
  55. package/src/server/conversation/codexHelperThreads.js +94 -93
  56. package/src/server/conversation/codexHistoryAdapter.js +13 -8
  57. package/src/server/conversation/codexIsolation.js +672 -0
  58. package/src/server/conversation/codexLiveItems.js +379 -0
  59. package/src/server/conversation/codexMessageCommands.js +68 -286
  60. package/src/server/conversation/codexMessageDelivery.js +46 -37
  61. package/src/server/conversation/codexModelCatalog.js +86 -7
  62. package/src/server/conversation/codexNotificationObserver.js +81 -88
  63. package/src/server/conversation/codexNotificationQueue.js +21 -10
  64. package/src/server/conversation/codexObservationControl.js +105 -89
  65. package/src/server/conversation/codexOutput.js +84 -419
  66. package/src/server/conversation/codexProcess.js +29 -1053
  67. package/src/server/conversation/codexProcessLeader.js +1 -0
  68. package/src/server/conversation/codexPromptDispatch.js +241 -0
  69. package/src/server/conversation/codexProvider.js +147 -1807
  70. package/src/server/conversation/codexProviderAuth.js +348 -0
  71. package/src/server/conversation/codexProviderInvalidation.js +371 -0
  72. package/src/server/conversation/codexProviderInventories.js +437 -0
  73. package/src/server/conversation/codexProviderOwner.js +149 -957
  74. package/src/server/conversation/codexProviderRelease.js +465 -0
  75. package/src/server/conversation/codexProviderSelection.js +26 -15
  76. package/src/server/conversation/codexProviderThreads.js +698 -0
  77. package/src/server/conversation/codexProviderTurns.js +453 -0
  78. package/src/server/conversation/codexReasoning.js +32 -23
  79. package/src/server/conversation/codexRenewalCommands.js +18 -6
  80. package/src/server/conversation/codexRenewalThreads.js +7 -22
  81. package/src/server/conversation/codexResultSettlement.js +70 -88
  82. package/src/server/conversation/codexRunJournal.js +91 -97
  83. package/src/server/conversation/codexRuntime.js +426 -0
  84. package/src/server/conversation/codexRuntimeInventory.js +113 -0
  85. package/src/server/conversation/codexRuntimeLifecycle.js +39 -13
  86. package/src/server/conversation/codexRuntimeStart.js +431 -0
  87. package/src/server/conversation/codexRuntimeStop.js +324 -0
  88. package/src/server/conversation/codexSessionCleanup.js +25 -10
  89. package/src/server/conversation/codexSessionReconciliation.js +21 -7
  90. package/src/server/conversation/codexThreadReadiness.js +49 -52
  91. package/src/server/conversation/codexThreadSnapshot.js +210 -0
  92. package/src/server/conversation/codexThreadStatus.js +48 -62
  93. package/src/server/conversation/codexTurn.js +171 -55
  94. package/src/server/conversation/codexTurnActivity.js +210 -0
  95. package/src/server/conversation/codexTurnControl.js +44 -48
  96. package/src/server/conversation/codexTurnEndings.js +610 -0
  97. package/src/server/conversation/codexTurnRecovery.js +97 -848
  98. package/src/server/conversation/codexTurnState.js +138 -111
  99. package/src/server/conversation/completedResponses.js +227 -0
  100. package/src/server/conversation/configuration.js +2 -2
  101. package/src/server/conversation/continuity.js +43 -6
  102. package/src/server/conversation/conversationDelivery.js +394 -0
  103. package/src/server/conversation/conversationEntries.js +365 -0
  104. package/src/server/conversation/conversationEvents.js +192 -0
  105. package/src/server/conversation/conversationGoals.js +187 -0
  106. package/src/server/conversation/conversationHandles.js +237 -0
  107. package/src/server/conversation/conversationReads.js +206 -0
  108. package/src/server/conversation/conversationRefusals.js +58 -0
  109. package/src/server/conversation/conversationReplacement.js +254 -0
  110. package/src/server/conversation/conversationSending.js +273 -0
  111. package/src/server/conversation/conversationStopping.js +121 -0
  112. package/src/server/conversation/conversationTurnDelivery.js +413 -0
  113. package/src/server/conversation/conversationTurns.js +511 -0
  114. package/src/server/conversation/deliveryTiming.js +25 -3
  115. package/src/server/conversation/fileStorage.js +11 -6
  116. package/src/server/conversation/hookBridge.js +14 -3
  117. package/src/server/conversation/index.js +1 -1
  118. package/src/server/conversation/localExecution.js +82 -625
  119. package/src/server/conversation/nativeCommandCallbacks.js +10 -0
  120. package/src/server/conversation/nativeConversationLifecycle.js +379 -0
  121. package/src/server/conversation/nativeHistoryExport.js +27 -18
  122. package/src/server/conversation/normalize.js +9 -0
  123. package/src/server/conversation/openCodeClient.js +295 -366
  124. package/src/server/conversation/openCodeNativeHistory.js +141 -0
  125. package/src/server/conversation/openCodeNativeSession.js +79 -0
  126. package/src/server/conversation/openCodeProcess.js +109 -53
  127. package/src/server/conversation/openCodeProcessOwner.js +565 -0
  128. package/src/server/conversation/openCodeRuntime.js +111 -1814
  129. package/src/server/conversation/openCodeSessionTurns.js +547 -0
  130. package/src/server/conversation/openCodeTemporaryConversations.js +394 -0
  131. package/src/server/conversation/openCodeTools.js +28 -14
  132. package/src/server/conversation/openCodeTurn.js +80 -74
  133. package/src/server/conversation/openCodeTurnMonitor.js +388 -0
  134. package/src/server/conversation/processIdentity.js +582 -0
  135. package/src/server/conversation/provider.js +21 -6
  136. package/src/server/conversation/providers/api.js +268 -107
  137. package/src/server/conversation/providers/claude.js +29 -6
  138. package/src/server/conversation/providers/claudeDriver.js +325 -250
  139. package/src/server/conversation/providers/codex.js +69 -33
  140. package/src/server/conversation/providers/codexDriver.js +503 -1081
  141. package/src/server/conversation/providers/codexDriverConnection.js +331 -0
  142. package/src/server/conversation/providers/codexDriverTurns.js +464 -0
  143. package/src/server/conversation/providers/opencode.js +30 -4
  144. package/src/server/conversation/providers/opencodeDriver.js +321 -659
  145. package/src/server/conversation/providers/opencodeStandalone.js +475 -0
  146. package/src/server/conversation/providers/scoped.js +64 -19
  147. package/src/server/conversation/runtime.js +171 -2863
  148. package/src/server/conversation/storage.js +91 -50
  149. package/src/server/conversation/streams.js +15 -2
  150. package/src/server/conversation/structuredOutput.js +7 -11
  151. package/src/server/conversation/systemPrompt.js +13 -1
  152. package/src/server/conversation/tools.js +47 -15
  153. package/src/server/conversation/transcript.js +49 -20
  154. package/src/server/hosts/claudeTurn.js +2 -4
  155. package/src/server/hosts/codexConfiguration.js +6 -8
  156. package/src/server/hosts/codexEvents.js +4 -2
  157. package/src/server/hosts/codexProcess.js +1 -1
  158. package/src/server/hosts/codexProvider.js +11 -9
  159. package/src/server/hosts/codexTurn.js +3 -3
  160. package/src/server/hosts/openCodeClient.js +4 -2
  161. package/src/server/index.js +0 -2
  162. package/src/server/lib/aiConnectionClient.js +35 -25
  163. package/src/server/lib/ndjson.js +1 -0
  164. package/src/server/lib/providers/anthropicClient.js +15 -6
  165. package/src/server/lib/providers/common.js +10 -6
  166. package/src/server/lib/providers/openAiCompatibleClient.js +40 -23
  167. package/src/server/lib/serviceToolCatalog.js +29 -283
  168. package/src/server/lib/serviceToolEntries.js +270 -0
  169. package/src/server/types.js +78 -0
  170. package/src/shared/assistantResource.js +0 -23
  171. package/src/shared/conversation/answerChoiceSugar.js +2 -14
  172. package/src/shared/conversation/messageInputSugar.js +18 -0
  173. package/src/shared/conversation/numberedQuestionSugar.js +6 -16
  174. package/src/shared/conversation/pagination.js +1 -1
  175. package/src/shared/conversation/questions.js +1 -0
  176. package/src/shared/conversation/streaming.js +1 -0
  177. package/src/shared/index.js +0 -1
  178. package/src/testing/conversationStorageContract.js +20 -0
  179. package/test/aiConnectionClient.test.js +42 -1
  180. package/test/apiConversationDriver.test.js +217 -38
  181. package/test/assistantBoundedToolLoop.test.js +1 -1
  182. package/test/claudeConversationSession.test.js +15 -11
  183. package/test/claudeInstructionResume.test.js +1 -1
  184. package/test/claudeProcess.test.js +9 -9
  185. package/test/claudeTurn.test.js +21 -8
  186. package/test/codexAccountChange.test.js +5 -3
  187. package/test/codexConversationSession.test.js +22 -21
  188. package/test/codexEvents.test.js +3 -1
  189. package/test/codexHelperDeletion.test.js +3 -7
  190. package/test/codexIsolation.test.js +6 -6
  191. package/test/codexModelCatalog.test.js +6 -7
  192. package/test/codexProcess.test.js +3 -3
  193. package/test/codexThreadSelection.test.js +4 -1
  194. package/test/codexTurn.test.js +29 -20
  195. package/test/conversationApiFacilities.test.js +11 -9
  196. package/test/conversationAttachments.test.js +3 -3
  197. package/test/conversationConfiguration.test.js +4 -4
  198. package/test/conversationNativeStorage.test.js +11 -10
  199. package/test/conversationReplacement.test.js +13 -13
  200. package/test/conversationRuntime.test.js +9 -5
  201. package/test/conversationSession.test.js +78 -22
  202. package/test/conversationTools.test.js +47 -26
  203. package/test/fileConversationStorage.test.js +10 -3
  204. package/test/fixtures/codexHelperThreadLedger.js +3 -3
  205. package/test/frameworkActions.test.js +33 -18
  206. package/test/localConversationExecution.test.js +4 -4
  207. package/test/localExecution.test.js +1 -1
  208. package/test/openCodeConnectionChange.test.js +3 -2
  209. package/test/openCodeRuntime.test.js +8 -7
  210. package/test/openCodeTools.test.js +1 -1
  211. package/test/opencodeConversationSession.test.js +4 -3
  212. package/test/serviceToolCatalog.test.js +3 -8
  213. package/src/server/lib/assistantCompletion.js +0 -190
  214. package/src/server/lib/assistantToolLoop.js +0 -259
@@ -1,304 +1,48 @@
1
1
  import crypto from "node:crypto";
2
2
  import { link, mkdir, open, readdir, rename, rm } from "node:fs/promises";
3
3
  import path from "node:path";
4
- import { isRecord } from "@jskit-ai/kernel/shared/support/normalize";
5
4
  import { normalizeNullableText } from "./normalize.js";
6
5
 
7
6
  import { tryAcquireExclusiveFileLock } from "@jskit-ai/kernel/server/support";
7
+ import { createCodexHelperThreadRecordSchema } from "./codexHelperThreadRecord.js";
8
8
 
9
- const CODEX_HELPER_THREAD_LEDGER_SCHEMA_VERSION = 1;
10
9
  const CODEX_HELPER_THREAD_LEDGER_DIRECTORY = "codex-helper-thread-ownership";
11
10
  const CODEX_HELPER_THREAD_LEDGER_MAX_FILE_BYTES = 64 * 1024;
12
11
  const CODEX_HELPER_THREAD_LEDGER_MAX_RECORDS = 1024;
13
12
  const CODEX_HELPER_THREAD_LEDGER_LOCK_WAIT_MS = 5000;
14
13
  const CODEX_HELPER_THREAD_LEDGER_LOCK_POLL_MS = 20;
15
- const CODEX_HELPER_THREAD_LIFECYCLES = Object.freeze({
16
- ACTIVE: "active",
17
- CLEANUP_REQUIRED: "cleanup_required",
18
- READY: "ready",
19
- STARTING_TURN: "starting_turn"
20
- });
21
- const CODEX_HELPER_THREAD_LIFECYCLE_VALUES = new Set(
22
- Object.values(CODEX_HELPER_THREAD_LIFECYCLES)
23
- );
24
- const RECORD_KEYS = Object.freeze([
25
- "createdAt",
26
- "executionProfile",
27
- "identity",
28
- "lifecycle",
29
- "ownershipId",
30
- "projectContextRoot",
31
- "projectRuntimeRoot",
32
- "revision",
33
- "schemaVersion",
34
- "sessionId",
35
- "threadId",
36
- "turnId",
37
- "updatedAt",
38
- "workdir"
39
- ]);
40
- const IDENTITY_KEYS = Object.freeze([
41
- "providerId",
42
- "providerKeyFingerprint",
43
- "runtime",
44
- "server",
45
- "transportId"
46
- ]);
47
- const RUNTIME_IDENTITY_KEYS = Object.freeze([
48
- "accountIdentitySignature",
49
- "authStateSignature",
50
- "endpoint",
51
- "executionMode",
52
- "executionContextHash",
53
- "provider",
54
- "runtimeDir",
55
- "runtimesHash",
56
- "terminalEnvHash",
57
- "toolHomeSource",
58
- "transport"
59
- ]);
60
- const SERVER_IDENTITY_KEYS = Object.freeze([
61
- "userAgent"
62
- ]);
63
-
64
- // The application retains its profile vocabulary and policy. Each owner retains
65
- // the original module's independent mutation queue and lock-instance identity.
14
+ function observeNoLock() {}
15
+
16
+ /**
17
+ * The application retains its profile vocabulary and policy. Each owner retains its own mutation queue and
18
+ * lock-instance identity.
19
+ * @param {object} options
20
+ * @param {(profile: unknown) => object} options.executionProfile The application's execution-profile codec.
21
+ * @param {(profile: unknown) => object} [options.snapshotExecutionProfile]
22
+ * @param {string} [options.errorPrefix]
23
+ */
66
24
  function createCodexHelperThreadLedgerOwner({
67
25
  executionProfile,
68
26
  snapshotExecutionProfile = executionProfile,
69
27
  errorPrefix = "codex_helper_"
70
- } = {}) {
28
+ }) {
29
+ // readable-check: allow method-probe - input validation: the application must supply its execution-profile codec
71
30
  if (typeof executionProfile !== "function") {
72
31
  throw new TypeError("Codex helper ownership requires its execution-profile codec.");
73
32
  }
33
+ const {
34
+ ledgerError,
35
+ assertExactKeys,
36
+ requiredText,
37
+ absolutePath,
38
+ timestamp,
39
+ positiveInteger,
40
+ codexHelperThreadRecordId,
41
+ defineCodexHelperThreadRecord
42
+ } = createCodexHelperThreadRecordSchema({ executionProfile, errorPrefix });
74
43
  const codexHelperThreadLedgerMutationQueues = new Map();
75
44
  const codexHelperThreadLedgerProcessId = crypto.randomUUID();
76
45
 
77
- function ledgerError(message = "", code = `${errorPrefix}ledger_invalid`) {
78
- const error = new Error(normalizeNullableText(message) || "Codex helper ownership record is invalid.");
79
- error.code = code;
80
- error.retryable = true;
81
- return error;
82
- }
83
-
84
- function assertExactKeys(value, expectedKeys, label) {
85
- if (!isRecord(value)) {
86
- throw ledgerError(`${label} must be an object.`);
87
- }
88
- const actual = Object.keys(value).sort();
89
- const expected = [...expectedKeys].sort();
90
- if (
91
- actual.length !== expected.length ||
92
- actual.some((key, index) => key !== expected[index])
93
- ) {
94
- throw ledgerError(`${label} has an unsupported shape.`);
95
- }
96
- }
97
-
98
- function requiredText(value, label, maxLength = 4096) {
99
- const normalized = normalizeNullableText(value);
100
- if (!normalized) {
101
- throw ledgerError(`${label} is required.`);
102
- }
103
- if (normalized.length > maxLength) {
104
- throw ledgerError(`${label} exceeds ${maxLength} characters.`);
105
- }
106
- return normalized;
107
- }
108
-
109
- function requiredSha256Signature(value, label) {
110
- const signature = requiredText(value, label, 71);
111
- if (!/^sha256:[a-f0-9]{64}$/u.test(signature)) {
112
- throw ledgerError(`${label} must be a SHA-256 signature.`);
113
- }
114
- return signature;
115
- }
116
-
117
- function requiredVersionedStateSignature(value, label) {
118
- const signature = requiredText(value, label, 32);
119
- if (!/^v[1-9][0-9]*:[a-f0-9]{24}$/u.test(signature)) {
120
- throw ledgerError(`${label} must be a versioned state signature.`);
121
- }
122
- return signature;
123
- }
124
-
125
- function requiredStableHash(value, label) {
126
- const hash = requiredText(value, label, 12);
127
- if (!/^[a-f0-9]{12}$/u.test(hash)) {
128
- throw ledgerError(`${label} must be a stable hash.`);
129
- }
130
- return hash;
131
- }
132
-
133
- function optionalText(value, label = "value", maxLength = 4096) {
134
- const normalized = normalizeNullableText(value);
135
- if (normalized.length > maxLength) {
136
- throw ledgerError(`${label} exceeds ${maxLength} characters.`);
137
- }
138
- return normalized;
139
- }
140
-
141
- function absolutePath(value, label) {
142
- const normalized = requiredText(value, label, 8192);
143
- if (!path.isAbsolute(normalized) || path.normalize(normalized) !== normalized) {
144
- throw ledgerError(`${label} must be a normalized absolute path.`);
145
- }
146
- return normalized;
147
- }
148
-
149
- function optionalAbsolutePath(value, label) {
150
- const normalized = optionalText(value, label, 8192);
151
- if (normalized && (!path.isAbsolute(normalized) || path.normalize(normalized) !== normalized)) {
152
- throw ledgerError(`${label} must be a normalized absolute path when present.`);
153
- }
154
- return normalized;
155
- }
156
-
157
- function timestamp(value, label) {
158
- const normalized = requiredText(value, label);
159
- const parsed = Date.parse(normalized);
160
- if (!Number.isFinite(parsed) || new Date(parsed).toISOString() !== normalized) {
161
- throw ledgerError(`${label} must be an ISO timestamp.`);
162
- }
163
- return normalized;
164
- }
165
-
166
- function positiveInteger(value, label) {
167
- const normalized = Number(value);
168
- if (!Number.isSafeInteger(normalized) || normalized < 1) {
169
- throw ledgerError(`${label} must be a positive integer.`);
170
- }
171
- return normalized;
172
- }
173
-
174
- function strictIdentity(value) {
175
- assertExactKeys(value, IDENTITY_KEYS, "identity");
176
- assertExactKeys(value.runtime, RUNTIME_IDENTITY_KEYS, "identity.runtime");
177
- assertExactKeys(value.server, SERVER_IDENTITY_KEYS, "identity.server");
178
- const runtime = value.runtime;
179
- const providerId = requiredText(value.providerId, "identity.providerId", 64);
180
- const executionMode = requiredText(
181
- runtime.executionMode,
182
- "identity.runtime.executionMode",
183
- 64
184
- );
185
- const provider = requiredText(runtime.provider, "identity.runtime.provider");
186
- const toolHomeSource = optionalAbsolutePath(
187
- runtime.toolHomeSource,
188
- "identity.runtime.toolHomeSource"
189
- );
190
- const transportId = requiredText(value.transportId, "identity.transportId", 64);
191
- const transport = requiredText(runtime.transport, "identity.runtime.transport", 64);
192
- const endpoint = requiredText(runtime.endpoint, "identity.runtime.endpoint", 8192);
193
- const endpointPath = endpoint.startsWith("unix://") ? endpoint.slice("unix://".length) : "";
194
- if (
195
- providerId !== "codex" ||
196
- !["helper", "interactive"].includes(executionMode) ||
197
- provider !== "codex_app_server" ||
198
- transport !== "unix" ||
199
- !endpointPath ||
200
- !path.isAbsolute(endpointPath) ||
201
- path.normalize(endpointPath) !== endpointPath ||
202
- transportId !== "codex_app_server"
203
- ) {
204
- throw ledgerError(
205
- "Codex helper ownership identity must use the managed Codex app-server."
206
- );
207
- }
208
- return Object.freeze({
209
- providerId,
210
- providerKeyFingerprint: requiredSha256Signature(
211
- value.providerKeyFingerprint,
212
- "identity.providerKeyFingerprint"
213
- ),
214
- runtime: Object.freeze({
215
- accountIdentitySignature: requiredSha256Signature(
216
- runtime.accountIdentitySignature,
217
- "identity.runtime.accountIdentitySignature"
218
- ),
219
- authStateSignature: requiredVersionedStateSignature(
220
- runtime.authStateSignature,
221
- "identity.runtime.authStateSignature"
222
- ),
223
- endpoint,
224
- executionMode,
225
- executionContextHash: requiredStableHash(
226
- runtime.executionContextHash,
227
- "identity.runtime.executionContextHash"
228
- ),
229
- provider,
230
- runtimeDir: absolutePath(runtime.runtimeDir, "identity.runtime.runtimeDir"),
231
- runtimesHash: requiredStableHash(
232
- runtime.runtimesHash,
233
- "identity.runtime.runtimesHash"
234
- ),
235
- terminalEnvHash: requiredStableHash(
236
- runtime.terminalEnvHash,
237
- "identity.runtime.terminalEnvHash"
238
- ),
239
- toolHomeSource,
240
- transport
241
- }),
242
- server: Object.freeze({
243
- userAgent: requiredText(value.server.userAgent, "identity.server.userAgent", 512)
244
- }),
245
- transportId
246
- });
247
- }
248
-
249
- function codexHelperThreadRecordId({
250
- projectRuntimeRoot = "",
251
- sessionId = "",
252
- threadId = ""
253
- } = {}) {
254
- return crypto.createHash("sha256").update([
255
- absolutePath(projectRuntimeRoot, "projectRuntimeRoot"),
256
- requiredText(sessionId, "sessionId", 256),
257
- requiredText(threadId, "threadId", 256)
258
- ].join("\0")).digest("hex");
259
- }
260
-
261
- function defineCodexHelperThreadRecord(value = {}) {
262
- assertExactKeys(value, RECORD_KEYS, "Codex helper ownership record");
263
- if (Number(value.schemaVersion) !== CODEX_HELPER_THREAD_LEDGER_SCHEMA_VERSION) {
264
- throw ledgerError(
265
- `Unsupported Codex helper ownership schema: ${String(value.schemaVersion ?? "missing")}.`
266
- );
267
- }
268
- const lifecycle = requiredText(value.lifecycle, "lifecycle");
269
- if (!CODEX_HELPER_THREAD_LIFECYCLE_VALUES.has(lifecycle)) {
270
- throw ledgerError(`Unsupported Codex helper ownership lifecycle: ${lifecycle}.`);
271
- }
272
- const turnId = optionalText(value.turnId);
273
- if (lifecycle === CODEX_HELPER_THREAD_LIFECYCLES.ACTIVE && !turnId) {
274
- throw ledgerError("An active Codex helper ownership record requires turnId.");
275
- }
276
- if (lifecycle === CODEX_HELPER_THREAD_LIFECYCLES.READY && turnId) {
277
- throw ledgerError(`${lifecycle} Codex helper ownership cannot retain turnId.`);
278
- }
279
- const createdAt = timestamp(value.createdAt, "createdAt");
280
- const updatedAt = timestamp(value.updatedAt, "updatedAt");
281
- if (Date.parse(updatedAt) < Date.parse(createdAt)) {
282
- throw ledgerError("updatedAt cannot precede createdAt.");
283
- }
284
- return Object.freeze({
285
- createdAt,
286
- executionProfile: executionProfile(value.executionProfile),
287
- identity: strictIdentity(value.identity),
288
- lifecycle,
289
- ownershipId: requiredText(value.ownershipId, "ownershipId", 128),
290
- projectContextRoot: absolutePath(value.projectContextRoot, "projectContextRoot"),
291
- projectRuntimeRoot: absolutePath(value.projectRuntimeRoot, "projectRuntimeRoot"),
292
- revision: positiveInteger(value.revision, "revision"),
293
- schemaVersion: CODEX_HELPER_THREAD_LEDGER_SCHEMA_VERSION,
294
- sessionId: requiredText(value.sessionId, "sessionId", 256),
295
- threadId: requiredText(value.threadId, "threadId", 256),
296
- turnId: optionalText(turnId, "turnId", 256),
297
- updatedAt,
298
- workdir: absolutePath(value.workdir, "workdir")
299
- });
300
- }
301
-
302
46
  function codexHelperThreadLedgerRoot(projectRuntimeRoot = "") {
303
47
  return path.join(
304
48
  absolutePath(projectRuntimeRoot, "projectRuntimeRoot"),
@@ -436,7 +180,7 @@ function createCodexHelperThreadLedgerOwner({
436
180
  }
437
181
 
438
182
  async function acquireHostLock(root = "", recordId = "", {
439
- observeLock = null
183
+ observeLock = observeNoLock
440
184
  } = {}) {
441
185
  const lockPath = path.join(
442
186
  root,
@@ -448,7 +192,7 @@ function createCodexHelperThreadLedgerOwner({
448
192
  try {
449
193
  releaseFileLock = await tryAcquireExclusiveFileLock(lockPath, { cwd: root });
450
194
  if (!releaseFileLock) {
451
- await observeLock?.({ stage: "host-contended" });
195
+ await observeLock({ stage: "host-contended" });
452
196
  if (Date.now() >= deadline) {
453
197
  throw ledgerError(
454
198
  "Codex helper ownership is busy in another process.",
@@ -458,7 +202,7 @@ function createCodexHelperThreadLedgerOwner({
458
202
  await delay(CODEX_HELPER_THREAD_LEDGER_LOCK_POLL_MS);
459
203
  continue;
460
204
  }
461
- await observeLock?.({ stage: "host-acquired" });
205
+ await observeLock({ stage: "host-acquired" });
462
206
  let released = false;
463
207
  return async () => {
464
208
  if (released) {
@@ -472,7 +216,7 @@ function createCodexHelperThreadLedgerOwner({
472
216
  }
473
217
  };
474
218
  } catch (error) {
475
- throw error?.code?.startsWith?.(errorPrefix)
219
+ throw typeof error?.code === "string" && error.code.startsWith(errorPrefix)
476
220
  ? error
477
221
  : ledgerError(`Codex helper ownership host lock failed: ${error.message}`);
478
222
  }
@@ -480,7 +224,7 @@ function createCodexHelperThreadLedgerOwner({
480
224
  }
481
225
 
482
226
  async function acquireFilesystemLock(root = "", recordId = "", {
483
- observeLock = null
227
+ observeLock = observeNoLock
484
228
  } = {}) {
485
229
  const locksRoot = path.join(root, ".locks");
486
230
  await mkdir(locksRoot, { mode: 0o770, recursive: true });
@@ -530,7 +274,7 @@ function createCodexHelperThreadLedgerOwner({
530
274
  throw readError;
531
275
  });
532
276
  if (verified?.token === current.token) {
533
- await observeLock?.({ stage: "stale-verified" });
277
+ await observeLock({ stage: "stale-verified" });
534
278
  await rm(lockPath).catch((removeError) => {
535
279
  if (removeError?.code !== "ENOENT") {
536
280
  throw removeError;
@@ -591,7 +335,7 @@ function createCodexHelperThreadLedgerOwner({
591
335
  }
592
336
 
593
337
  function createCodexHelperThreadLedger({
594
- observeLock = null,
338
+ observeLock = observeNoLock,
595
339
  projectRuntimeRoot = ""
596
340
  } = {}) {
597
341
  const normalizedProjectRuntimeRoot = absolutePath(projectRuntimeRoot, "projectRuntimeRoot");
@@ -792,7 +536,5 @@ function createCodexHelperThreadLedgerOwner({
792
536
  }
793
537
 
794
538
  export {
795
- CODEX_HELPER_THREAD_LEDGER_SCHEMA_VERSION,
796
- CODEX_HELPER_THREAD_LIFECYCLES,
797
539
  createCodexHelperThreadLedgerOwner
798
540
  };
@@ -0,0 +1,304 @@
1
+ import crypto from "node:crypto";
2
+ import path from "node:path";
3
+ import { isRecord } from "@jskit-ai/kernel/shared/support/normalize";
4
+ import { normalizeNullableText } from "./normalize.js";
5
+
6
+ const CODEX_HELPER_THREAD_LEDGER_SCHEMA_VERSION = 1;
7
+ const CODEX_HELPER_THREAD_LIFECYCLES = Object.freeze({
8
+ ACTIVE: "active",
9
+ CLEANUP_REQUIRED: "cleanup_required",
10
+ READY: "ready",
11
+ STARTING_TURN: "starting_turn"
12
+ });
13
+ const CODEX_HELPER_THREAD_LIFECYCLE_VALUES = new Set(
14
+ Object.values(CODEX_HELPER_THREAD_LIFECYCLES)
15
+ );
16
+ const RECORD_KEYS = Object.freeze([
17
+ "createdAt",
18
+ "executionProfile",
19
+ "identity",
20
+ "lifecycle",
21
+ "ownershipId",
22
+ "projectContextRoot",
23
+ "projectRuntimeRoot",
24
+ "revision",
25
+ "schemaVersion",
26
+ "sessionId",
27
+ "threadId",
28
+ "turnId",
29
+ "updatedAt",
30
+ "workdir"
31
+ ]);
32
+ const IDENTITY_KEYS = Object.freeze([
33
+ "providerId",
34
+ "providerKeyFingerprint",
35
+ "runtime",
36
+ "server",
37
+ "transportId"
38
+ ]);
39
+ const RUNTIME_IDENTITY_KEYS = Object.freeze([
40
+ "accountIdentitySignature",
41
+ "authStateSignature",
42
+ "endpoint",
43
+ "executionMode",
44
+ "executionContextHash",
45
+ "provider",
46
+ "runtimeDir",
47
+ "runtimesHash",
48
+ "terminalEnvHash",
49
+ "toolHomeSource",
50
+ "transport"
51
+ ]);
52
+ const SERVER_IDENTITY_KEYS = Object.freeze([
53
+ "userAgent"
54
+ ]);
55
+
56
+ /**
57
+ * The helper thread ownership record: its exact shape, its identity and the validation every stored record passes.
58
+ * @param {object} options
59
+ * @param {(profile: unknown) => object} options.executionProfile The application's execution-profile codec.
60
+ * @param {string} options.errorPrefix
61
+ */
62
+ function createCodexHelperThreadRecordSchema({ executionProfile, errorPrefix }) {
63
+ function ledgerError(message = "", code = `${errorPrefix}ledger_invalid`) {
64
+ const error = new Error(normalizeNullableText(message) || "Codex helper ownership record is invalid.");
65
+ error.code = code;
66
+ error.retryable = true;
67
+ return error;
68
+ }
69
+
70
+ function assertExactKeys(value, expectedKeys, label) {
71
+ if (!isRecord(value)) {
72
+ throw ledgerError(`${label} must be an object.`);
73
+ }
74
+ const actual = Object.keys(value).sort();
75
+ const expected = [...expectedKeys].sort();
76
+ if (
77
+ actual.length !== expected.length ||
78
+ actual.some((key, index) => key !== expected[index])
79
+ ) {
80
+ throw ledgerError(`${label} has an unsupported shape.`);
81
+ }
82
+ }
83
+
84
+ function requiredText(value, label, maxLength = 4096) {
85
+ const normalized = normalizeNullableText(value);
86
+ if (!normalized) {
87
+ throw ledgerError(`${label} is required.`);
88
+ }
89
+ if (normalized.length > maxLength) {
90
+ throw ledgerError(`${label} exceeds ${maxLength} characters.`);
91
+ }
92
+ return normalized;
93
+ }
94
+
95
+ function requiredSha256Signature(value, label) {
96
+ const signature = requiredText(value, label, 71);
97
+ if (!/^sha256:[a-f0-9]{64}$/u.test(signature)) {
98
+ throw ledgerError(`${label} must be a SHA-256 signature.`);
99
+ }
100
+ return signature;
101
+ }
102
+
103
+ function requiredVersionedStateSignature(value, label) {
104
+ const signature = requiredText(value, label, 32);
105
+ if (!/^v[1-9][0-9]*:[a-f0-9]{24}$/u.test(signature)) {
106
+ throw ledgerError(`${label} must be a versioned state signature.`);
107
+ }
108
+ return signature;
109
+ }
110
+
111
+ function requiredStableHash(value, label) {
112
+ const hash = requiredText(value, label, 12);
113
+ if (!/^[a-f0-9]{12}$/u.test(hash)) {
114
+ throw ledgerError(`${label} must be a stable hash.`);
115
+ }
116
+ return hash;
117
+ }
118
+
119
+ function optionalText(value, label = "value", maxLength = 4096) {
120
+ const normalized = normalizeNullableText(value);
121
+ if (normalized.length > maxLength) {
122
+ throw ledgerError(`${label} exceeds ${maxLength} characters.`);
123
+ }
124
+ return normalized;
125
+ }
126
+
127
+ function absolutePath(value, label) {
128
+ const normalized = requiredText(value, label, 8192);
129
+ if (!path.isAbsolute(normalized) || path.normalize(normalized) !== normalized) {
130
+ throw ledgerError(`${label} must be a normalized absolute path.`);
131
+ }
132
+ return normalized;
133
+ }
134
+
135
+ function optionalAbsolutePath(value, label) {
136
+ const normalized = optionalText(value, label, 8192);
137
+ if (normalized && (!path.isAbsolute(normalized) || path.normalize(normalized) !== normalized)) {
138
+ throw ledgerError(`${label} must be a normalized absolute path when present.`);
139
+ }
140
+ return normalized;
141
+ }
142
+
143
+ function timestamp(value, label) {
144
+ const normalized = requiredText(value, label);
145
+ const parsed = Date.parse(normalized);
146
+ if (!Number.isFinite(parsed) || new Date(parsed).toISOString() !== normalized) {
147
+ throw ledgerError(`${label} must be an ISO timestamp.`);
148
+ }
149
+ return normalized;
150
+ }
151
+
152
+ function positiveInteger(value, label) {
153
+ const normalized = Number(value);
154
+ if (!Number.isSafeInteger(normalized) || normalized < 1) {
155
+ throw ledgerError(`${label} must be a positive integer.`);
156
+ }
157
+ return normalized;
158
+ }
159
+
160
+ function strictIdentity(value) {
161
+ assertExactKeys(value, IDENTITY_KEYS, "identity");
162
+ assertExactKeys(value.runtime, RUNTIME_IDENTITY_KEYS, "identity.runtime");
163
+ assertExactKeys(value.server, SERVER_IDENTITY_KEYS, "identity.server");
164
+ const runtime = value.runtime;
165
+ const providerId = requiredText(value.providerId, "identity.providerId", 64);
166
+ const executionMode = requiredText(
167
+ runtime.executionMode,
168
+ "identity.runtime.executionMode",
169
+ 64
170
+ );
171
+ const provider = requiredText(runtime.provider, "identity.runtime.provider");
172
+ const toolHomeSource = optionalAbsolutePath(
173
+ runtime.toolHomeSource,
174
+ "identity.runtime.toolHomeSource"
175
+ );
176
+ const transportId = requiredText(value.transportId, "identity.transportId", 64);
177
+ const transport = requiredText(runtime.transport, "identity.runtime.transport", 64);
178
+ const endpoint = requiredText(runtime.endpoint, "identity.runtime.endpoint", 8192);
179
+ const endpointPath = endpoint.startsWith("unix://") ? endpoint.slice("unix://".length) : "";
180
+ if (
181
+ providerId !== "codex" ||
182
+ !["helper", "interactive"].includes(executionMode) ||
183
+ provider !== "codex_app_server" ||
184
+ transport !== "unix" ||
185
+ !endpointPath ||
186
+ !path.isAbsolute(endpointPath) ||
187
+ path.normalize(endpointPath) !== endpointPath ||
188
+ transportId !== "codex_app_server"
189
+ ) {
190
+ throw ledgerError(
191
+ "Codex helper ownership identity must use the managed Codex app-server."
192
+ );
193
+ }
194
+ return Object.freeze({
195
+ providerId,
196
+ providerKeyFingerprint: requiredSha256Signature(
197
+ value.providerKeyFingerprint,
198
+ "identity.providerKeyFingerprint"
199
+ ),
200
+ runtime: Object.freeze({
201
+ accountIdentitySignature: requiredSha256Signature(
202
+ runtime.accountIdentitySignature,
203
+ "identity.runtime.accountIdentitySignature"
204
+ ),
205
+ authStateSignature: requiredVersionedStateSignature(
206
+ runtime.authStateSignature,
207
+ "identity.runtime.authStateSignature"
208
+ ),
209
+ endpoint,
210
+ executionMode,
211
+ executionContextHash: requiredStableHash(
212
+ runtime.executionContextHash,
213
+ "identity.runtime.executionContextHash"
214
+ ),
215
+ provider,
216
+ runtimeDir: absolutePath(runtime.runtimeDir, "identity.runtime.runtimeDir"),
217
+ runtimesHash: requiredStableHash(
218
+ runtime.runtimesHash,
219
+ "identity.runtime.runtimesHash"
220
+ ),
221
+ terminalEnvHash: requiredStableHash(
222
+ runtime.terminalEnvHash,
223
+ "identity.runtime.terminalEnvHash"
224
+ ),
225
+ toolHomeSource,
226
+ transport
227
+ }),
228
+ server: Object.freeze({
229
+ userAgent: requiredText(value.server.userAgent, "identity.server.userAgent", 512)
230
+ }),
231
+ transportId
232
+ });
233
+ }
234
+
235
+ function codexHelperThreadRecordId({
236
+ projectRuntimeRoot = "",
237
+ sessionId = "",
238
+ threadId = ""
239
+ } = {}) {
240
+ return crypto.createHash("sha256").update([
241
+ absolutePath(projectRuntimeRoot, "projectRuntimeRoot"),
242
+ requiredText(sessionId, "sessionId", 256),
243
+ requiredText(threadId, "threadId", 256)
244
+ ].join("\0")).digest("hex");
245
+ }
246
+
247
+ function defineCodexHelperThreadRecord(value = {}) {
248
+ assertExactKeys(value, RECORD_KEYS, "Codex helper ownership record");
249
+ if (Number(value.schemaVersion) !== CODEX_HELPER_THREAD_LEDGER_SCHEMA_VERSION) {
250
+ throw ledgerError(
251
+ `Unsupported Codex helper ownership schema: ${String(value.schemaVersion ?? "missing")}.`
252
+ );
253
+ }
254
+ const lifecycle = requiredText(value.lifecycle, "lifecycle");
255
+ if (!CODEX_HELPER_THREAD_LIFECYCLE_VALUES.has(lifecycle)) {
256
+ throw ledgerError(`Unsupported Codex helper ownership lifecycle: ${lifecycle}.`);
257
+ }
258
+ const turnId = optionalText(value.turnId);
259
+ if (lifecycle === CODEX_HELPER_THREAD_LIFECYCLES.ACTIVE && !turnId) {
260
+ throw ledgerError("An active Codex helper ownership record requires turnId.");
261
+ }
262
+ if (lifecycle === CODEX_HELPER_THREAD_LIFECYCLES.READY && turnId) {
263
+ throw ledgerError(`${lifecycle} Codex helper ownership cannot retain turnId.`);
264
+ }
265
+ const createdAt = timestamp(value.createdAt, "createdAt");
266
+ const updatedAt = timestamp(value.updatedAt, "updatedAt");
267
+ if (Date.parse(updatedAt) < Date.parse(createdAt)) {
268
+ throw ledgerError("updatedAt cannot precede createdAt.");
269
+ }
270
+ return Object.freeze({
271
+ createdAt,
272
+ executionProfile: executionProfile(value.executionProfile),
273
+ identity: strictIdentity(value.identity),
274
+ lifecycle,
275
+ ownershipId: requiredText(value.ownershipId, "ownershipId", 128),
276
+ projectContextRoot: absolutePath(value.projectContextRoot, "projectContextRoot"),
277
+ projectRuntimeRoot: absolutePath(value.projectRuntimeRoot, "projectRuntimeRoot"),
278
+ revision: positiveInteger(value.revision, "revision"),
279
+ schemaVersion: CODEX_HELPER_THREAD_LEDGER_SCHEMA_VERSION,
280
+ sessionId: requiredText(value.sessionId, "sessionId", 256),
281
+ threadId: requiredText(value.threadId, "threadId", 256),
282
+ turnId: optionalText(turnId, "turnId", 256),
283
+ updatedAt,
284
+ workdir: absolutePath(value.workdir, "workdir")
285
+ });
286
+ }
287
+
288
+ return Object.freeze({
289
+ ledgerError,
290
+ assertExactKeys,
291
+ requiredText,
292
+ absolutePath,
293
+ timestamp,
294
+ positiveInteger,
295
+ codexHelperThreadRecordId,
296
+ defineCodexHelperThreadRecord
297
+ });
298
+ }
299
+
300
+ export {
301
+ CODEX_HELPER_THREAD_LEDGER_SCHEMA_VERSION,
302
+ CODEX_HELPER_THREAD_LIFECYCLES,
303
+ createCodexHelperThreadRecordSchema
304
+ };