@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,24 +1,15 @@
1
- import { execFile, spawn } from "node:child_process";
1
+ import { spawn } from "node:child_process";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { closeSync, openSync } from "node:fs";
4
- import { mkdtemp, open, readFile, readdir, rm } from "node:fs/promises";
5
- import { isRecord, normalizePositiveInteger } from "@jskit-ai/kernel/shared/support/normalize";
6
- import { normalizeNullableText } from "./normalize.js";
4
+ import { mkdtemp, open, rm } from "node:fs/promises";
7
5
  import { isAbsolute, join } from "node:path";
8
6
  import { tmpdir } from "node:os";
9
7
  import { setTimeout as delay } from "node:timers/promises";
10
- import { promisify } from "node:util";
11
8
 
12
- const execFileAsync = promisify(execFile);
13
-
14
- // Fixed locale and zone make `lstart` parseable and stable across restarts.
15
- async function runPs(args) {
16
- const { stdout } = await execFileAsync("ps", args, {
17
- env: { ...process.env, LC_ALL: "C", TZ: "UTC" },
18
- maxBuffer: 64 * 1024 * 1024
19
- });
20
- return stdout;
21
- }
9
+ /**
10
+ * The execution host that starts and stops native processes; the local one is the default.
11
+ * @typedef {ReturnType<typeof createLocalConversationExecution>} ConversationExecution
12
+ */
22
13
 
23
14
  /** Owned POSIX process groups for a trusted, single-user application.
24
15
  * Managed hosts replace this facility to enforce resource and identity policy.
@@ -75,7 +66,12 @@ export function createLocalConversationExecution() {
75
66
  return entry.stopping;
76
67
  }
77
68
 
78
- async function start({ command, args = [], cwd, env = process.env, stream = false, limits, logPath } = {}) {
69
+ /**
70
+ * Starts one owned native process in its own process group.
71
+ * @param {{ command: string, args?: string[], cwd: string, env: import("@jskit-ai/kernel/server/types").RuntimeEnv,
72
+ * stream?: boolean, limits?: object, logPath?: string }} request
73
+ */
74
+ async function start({ command, args = [], cwd, env, stream = false, limits, logPath }) {
79
75
  if (closing) throw new Error("The conversation execution host is closed.");
80
76
  if (typeof command !== "string" || !command || !Array.isArray(args) || args.some((arg) => typeof arg !== "string")) {
81
77
  throw new TypeError("An executable and string arguments are required.");
@@ -111,628 +107,89 @@ export function createLocalConversationExecution() {
111
107
  if (executions.has(id)) await stop(id);
112
108
  throw new Error("The conversation execution host closed during startup.");
113
109
  }
114
- return Object.freeze({ id, pid: child.pid, stdin: child.stdin, stdout: child.stdout, exited,
115
- get running() { return child.exitCode === null && child.signalCode === null; } });
116
- }
117
-
118
- return Object.freeze({
119
- start,
120
- async run({ command, args, cwd, baseEnv = process.env, credentialHome = {}, mode,
121
- logPath, timeout = 30_000, limits } = {}) {
122
- if (!["capture", "detached"].includes(mode)) throw new TypeError("Native execution requires capture or detached mode.");
123
- if (!Number.isSafeInteger(timeout) || timeout <= 0) throw new TypeError("Native execution requires a positive timeout.");
124
- const env = { ...baseEnv };
125
- if (credentialHome.home) {
126
- if (!isAbsolute(credentialHome.home)) throw new TypeError("A credential home must be absolute.");
127
- Object.assign(env, { HOME: credentialHome.home,
128
- XDG_CACHE_HOME: join(credentialHome.home, ".cache"),
129
- XDG_CONFIG_HOME: join(credentialHome.home, ".config"),
130
- XDG_DATA_HOME: join(credentialHome.home, ".local", "share") });
131
- if (credentialHome.username) Object.assign(env, { USER: credentialHome.username, LOGNAME: credentialHome.username });
132
- }
133
- const captureRoot = mode === "capture" ? await mkdtemp(join(tmpdir(), "jskit-capture-")) : null;
134
- const outputPath = captureRoot ? join(captureRoot, "output.log") : logPath;
135
- let child;
136
- let timer;
137
- let cleanup = false;
138
- try {
139
- child = await start({ command, args, cwd, env, logPath: outputPath, limits });
140
- if (mode === "detached") {
141
- // A shared account service outlives the application's observer. Its
142
- // durable runtime owner, not this Node event loop, controls shutdown.
143
- executions.get(child.id).child.unref();
144
- return { ok: true, pid: child.pid, execution: { id: child.id } };
145
- }
146
- let timedOut = false;
147
- const result = await Promise.race([child.exited, new Promise((resolve, reject) => {
148
- timer = setTimeout(() => {
149
- timedOut = true;
150
- stop(child.id).then(() => child.exited).then(resolve, reject);
151
- }, timeout);
152
- })]);
153
- clearTimeout(timer);
154
- // The timeout may have already retired this exact execution.
155
- if (executions.has(child.id)) await stop(child.id);
156
- cleanup = true;
157
- const file = await open(outputPath, "r");
158
- let output;
159
- try {
160
- const { size } = await file.stat();
161
- const bytes = Buffer.alloc(Math.min(size, 64 * 1024));
162
- const { bytesRead } = await file.read(bytes, 0, bytes.length, Math.max(0, size - bytes.length));
163
- output = bytes.subarray(0, bytesRead).toString("utf8");
164
- } finally { await file.close(); }
165
- return { ok: !timedOut && result.code === 0, exitCode: result.code, signal: result.signal, timedOut, output };
166
- } finally {
167
- clearTimeout(timer);
168
- if (captureRoot && (!child || cleanup)) await rm(captureRoot, { recursive: true, force: true });
169
- }
170
- },
171
- stop,
172
- async close() {
173
- closing = true;
174
- const results = await Promise.allSettled([...executions.keys()].map(stop));
175
- const errors = results.filter((result) => result.status === "rejected").map((result) => result.reason);
176
- if (errors.length) throw new AggregateError(errors, "Some owned native executions could not be stopped.");
177
- }
178
- });
179
- }
180
-
181
- /** Process-group ownership proof used by execution hosts; callers retain their metadata contract. */
182
- export function createConversationProcessIdentity({
183
- runtimeTokenEnvironmentName = "JSKIT_EXECUTION_RUNTIME_TOKEN",
184
- commandHashEnvironmentName = "JSKIT_EXECUTION_COMMAND_HASH",
185
- label = "Native",
186
- unavailableCode = "native_process_identity_unavailable",
187
- platform = process.platform,
188
- ps = runPs
189
- } = {}) {
190
- const PROCESS_IDENTITY_SETTLE_TIMEOUT_MS = 1000;
191
- // Darwin inspection spawns ps instead of reading /proc, so it polls less often.
192
- const PROCESS_IDENTITY_SETTLE_POLL_MS = platform === "darwin" ? 100 : 25;
193
- const PROCESS_IDENTITY_VERSION = 1;
194
- const PROCESS_IDENTITY_PLATFORMS = Object.freeze({ linux: "linux-proc", darwin: "darwin" });
195
- const hostIdentityPlatform = PROCESS_IDENTITY_PLATFORMS[platform] || "";
196
- const PROCESS_STATE = Object.freeze({ RUNNING: "running", STOPPED: "stopped" });
197
- const PROCESS_IDENTITY_STATUS = Object.freeze({
198
- ABSENT: "absent", AMBIGUOUS: "ambiguous", EXACT: "exact", INVALID: "invalid", MISMATCH: "mismatch"
199
- });
200
-
201
- function processMetadataIsIdentifiable(metadata = {}) {
202
- return Boolean(
203
- Number.isSafeInteger(Number(metadata.pid)) && Number(metadata.pid) > 0 &&
204
- processIdentityIsWellFormed(metadata.processIdentity) &&
205
- [PROCESS_STATE.RUNNING, PROCESS_STATE.STOPPED].includes(metadata.processState)
206
- );
207
- }
208
-
209
- function processIsAlive(pid) {
210
- const normalizedPid = Number(pid);
211
- if (!Number.isSafeInteger(normalizedPid) || normalizedPid <= 0) {
212
- return false;
213
- }
214
- try {
215
- process.kill(normalizedPid, 0);
216
- return true;
217
- } catch {
218
- return false;
219
- }
220
- }
221
-
222
- function processGroupIsAlive(processGroupId) {
223
- const normalizedProcessGroupId = Number(processGroupId);
224
- if (!Number.isSafeInteger(normalizedProcessGroupId) || normalizedProcessGroupId <= 0) {
225
- return false;
226
- }
227
- if (process.platform === "win32") {
228
- return processIsAlive(normalizedProcessGroupId);
229
- }
230
- try {
231
- process.kill(-normalizedProcessGroupId, 0);
232
- return true;
233
- } catch {
234
- return false;
235
- }
110
+ return Object.freeze({
111
+ id,
112
+ pid: child.pid,
113
+ stdin: child.stdin,
114
+ stdout: child.stdout,
115
+ exited,
116
+ get running() { return child.exitCode === null && child.signalCode === null; }
117
+ });
236
118
  }
237
119
 
238
- function signalProcessGroup(processGroupId, signal) {
239
- const normalizedProcessGroupId = Number(processGroupId);
240
- const target = process.platform === "win32"
241
- ? normalizedProcessGroupId
242
- : -normalizedProcessGroupId;
120
+ /**
121
+ * Runs one native command to completion (capture) or leaves it running (detached).
122
+ * @param {{ command: string, args: string[], cwd: string, baseEnv: import("@jskit-ai/kernel/server/types").RuntimeEnv,
123
+ * credentialHome?: { home?: string, username?: string }, mode: "capture" | "detached", logPath?: string, timeout?: number,
124
+ * limits?: object }} request
125
+ */
126
+ async function run({ command, args, cwd, baseEnv, credentialHome = {}, mode, logPath, timeout = 30_000, limits }) {
127
+ if (!["capture", "detached"].includes(mode)) throw new TypeError("Native execution requires capture or detached mode.");
128
+ if (!Number.isSafeInteger(timeout) || timeout <= 0) throw new TypeError("Native execution requires a positive timeout.");
129
+ const env = { ...baseEnv };
130
+ if (credentialHome.home) {
131
+ if (!isAbsolute(credentialHome.home)) throw new TypeError("A credential home must be absolute.");
132
+ Object.assign(env, { HOME: credentialHome.home,
133
+ XDG_CACHE_HOME: join(credentialHome.home, ".cache"),
134
+ XDG_CONFIG_HOME: join(credentialHome.home, ".config"),
135
+ XDG_DATA_HOME: join(credentialHome.home, ".local", "share") });
136
+ if (credentialHome.username) Object.assign(env, { USER: credentialHome.username, LOGNAME: credentialHome.username });
137
+ }
138
+ const captureRoot = mode === "capture" ? await mkdtemp(join(tmpdir(), "jskit-capture-")) : null;
139
+ const outputPath = captureRoot ? join(captureRoot, "output.log") : logPath;
140
+ let child;
141
+ let timer;
142
+ let cleanup = false;
243
143
  try {
244
- process.kill(target, signal);
245
- return true;
246
- } catch (error) {
247
- if (!["ESRCH", "EPERM"].includes(String(error?.code || ""))) {
248
- throw error;
249
- }
250
- return false;
251
- }
252
- }
253
-
254
- function linuxProcessStat(value = "") {
255
- const text = String(value || "");
256
- const commandEnd = text.lastIndexOf(")");
257
- if (commandEnd < 0) {
258
- return null;
259
- }
260
- const fields = text.slice(commandEnd + 1).trim().split(/\s+/u);
261
- const state = String(fields[0] || "");
262
- const parentPid = Number(fields[1]);
263
- const processGroupId = Number(fields[2]);
264
- const startTimeTicks = String(fields[19] || "");
265
- if (
266
- !/^[A-Z]$/u.test(state) ||
267
- !Number.isSafeInteger(parentPid) || parentPid < 0 ||
268
- !Number.isSafeInteger(processGroupId) || processGroupId <= 0 ||
269
- !/^\d+$/u.test(startTimeTicks)
270
- ) {
271
- return null;
272
- }
273
- return {
274
- parentPid,
275
- processGroupId,
276
- startTimeTicks,
277
- state
278
- };
279
- }
280
-
281
- function normalizeProcessIdentity(value = {}) {
282
- const normalized = isRecord(value) ? value : {};
283
- return {
284
- commandHash: normalizeNullableText(normalized.commandHash),
285
- platform: normalizeNullableText(normalized.platform),
286
- runtimeToken: normalizeNullableText(normalized.runtimeToken),
287
- startTimeTicks: normalizeNullableText(normalized.startTimeTicks),
288
- version: Number(normalized.version || 0)
289
- };
290
- }
291
-
292
- function processIdentityIsWellFormed(value = {}) {
293
- const identity = normalizeProcessIdentity(value);
294
- return Boolean(
295
- identity.version === PROCESS_IDENTITY_VERSION &&
296
- Object.values(PROCESS_IDENTITY_PLATFORMS).includes(identity.platform) &&
297
- /^[a-f0-9]{12}$/u.test(identity.commandHash) &&
298
- /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu.test(identity.runtimeToken) &&
299
- /^\d+$/u.test(identity.startTimeTicks)
300
- );
301
- }
302
-
303
- /** The value of `name` in a list of NAME=value environment entries. */
304
- function environmentValue(entries = [], name = "") {
305
- const entry = entries.find((assignment) => assignment.startsWith(`${name}=`));
306
- return entry ? entry.slice(name.length + 1) : "";
307
- }
308
-
309
- async function linuxProcessRecords() {
310
- const records = [];
311
- const entries = await readdir("/proc", {
312
- withFileTypes: true
313
- }).catch(() => []);
314
- for (const entry of entries) {
315
- if (!entry.isDirectory() || !/^\d+$/u.test(entry.name)) {
316
- continue;
317
- }
318
- const pid = Number(entry.name);
319
- const statValue = await readFile(`/proc/${entry.name}/stat`, "utf8").catch(() => "");
320
- const processStat = linuxProcessStat(statValue);
321
- if (!processStat || processStat.state === "Z") {
322
- continue;
323
- }
324
- let environment = null;
325
- let environmentReadable = true;
144
+ child = await start({ command, args, cwd, env, logPath: outputPath, limits });
145
+ if (mode === "detached") {
146
+ // A shared account service outlives the application's observer. Its
147
+ // durable runtime owner, not this Node event loop, controls shutdown.
148
+ executions.get(child.id).child.unref();
149
+ return { ok: true, pid: child.pid, execution: { id: child.id } };
150
+ }
151
+ let timedOut = false;
152
+ const result = await Promise.race([child.exited, new Promise((resolve, reject) => {
153
+ timer = setTimeout(() => {
154
+ timedOut = true;
155
+ stop(child.id).then(() => child.exited).then(resolve, reject);
156
+ }, timeout);
157
+ })]);
158
+ clearTimeout(timer);
159
+ // The timeout may have already retired this exact execution.
160
+ if (executions.has(child.id)) await stop(child.id);
161
+ cleanup = true;
162
+ const file = await open(outputPath, "r");
163
+ let output;
326
164
  try {
327
- environment = await readFile(`/proc/${entry.name}/environ`);
328
- } catch (error) {
329
- if (String(error?.code || "") === "ENOENT") {
330
- continue;
331
- }
332
- environmentReadable = false;
333
- }
334
- const entries = environmentReadable ? environment.toString("utf8").split("\0") : [];
335
- records.push({
336
- ...processStat,
337
- commandHash: environmentValue(entries, commandHashEnvironmentName),
338
- environmentReadable,
339
- pid,
340
- runtimeToken: environmentValue(entries, runtimeTokenEnvironmentName)
341
- });
165
+ const { size } = await file.stat();
166
+ const bytes = Buffer.alloc(Math.min(size, 64 * 1024));
167
+ const { bytesRead } = await file.read(bytes, 0, bytes.length, Math.max(0, size - bytes.length));
168
+ output = bytes.subarray(0, bytesRead).toString("utf8");
169
+ } finally { await file.close(); }
170
+ return { ok: !timedOut && result.code === 0, exitCode: result.code, signal: result.signal, timedOut, output };
171
+ } finally {
172
+ clearTimeout(timer);
173
+ if (captureRoot && (!child || cleanup)) await rm(captureRoot, { recursive: true, force: true });
342
174
  }
343
- return records;
344
175
  }
345
176
 
346
- const DARWIN_MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
347
-
348
- /** `ps -o lstart=` in the C locale and UTC, as whole epoch seconds. */
349
- function darwinStartTime(value = "") {
350
- const match = /^\w{3}\s+(\w{3})\s+(\d{1,2})\s+(\d{2}):(\d{2}):(\d{2})\s+(\d{4})$/u.exec(String(value).trim());
351
- const month = match ? DARWIN_MONTHS.indexOf(match[1]) : -1;
352
- if (month < 0) {
353
- return "";
354
- }
355
- const [, , day, hours, minutes, seconds, year] = match.map(Number);
356
- return String(Date.UTC(year, month, day, hours, minutes, seconds) / 1000);
177
+ async function close() {
178
+ closing = true;
179
+ const results = await Promise.allSettled([...executions.keys()].map(stop));
180
+ const errors = results.filter((result) => result.status === "rejected").map((result) => result.reason);
181
+ if (errors.length) throw new AggregateError(errors, "Some owned native executions could not be stopped.");
357
182
  }
358
183
 
359
- function darwinCommandsByPid(output = "") {
360
- const commands = new Map();
361
- for (const line of String(output).split("\n")) {
362
- const match = /^\s*(\d+) (.*)$/u.exec(line);
363
- if (match) {
364
- commands.set(Number(match[1]), match[2]);
365
- }
366
- }
367
- return commands;
368
- }
369
-
370
- /** The `ps -E` text after the argument list, as its assignments, or null when no environment is shown. */
371
- function darwinEnvironment(command, commandWithEnvironment) {
372
- if (command === undefined || !commandWithEnvironment?.startsWith(command)) {
373
- return null;
374
- }
375
- const assignments = commandWithEnvironment.slice(command.length).split(/\s+/u).filter((entry) => /^[^=\s]+=/u.test(entry));
376
- return assignments.length ? assignments : null;
377
- }
378
-
379
- /** The darwin counterpart of the /proc scan. Only the current user's environments are readable;
380
- * a listing without a separate environment section stays unreadable so inspection degrades to ambiguous. */
381
- async function darwinProcessRecords() {
382
- const currentUid = process.getuid();
383
- const ownProcesses = ["-x", "-U", String(currentUid), "-ww", "-o", "pid=,command="];
384
- const [table, commandOutput, environmentOutput] = await Promise.all([
385
- ps(["-ax", "-o", "pid=,pgid=,uid=,stat=,lstart="]),
386
- ps(ownProcesses),
387
- ps(["-E", ...ownProcesses])
388
- ]);
389
- const commands = darwinCommandsByPid(commandOutput);
390
- const commandsWithEnvironment = darwinCommandsByPid(environmentOutput);
391
- const records = [];
392
- for (const line of String(table).split("\n")) {
393
- const match = /^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(\w{3}\s+\w{3}\s+\d{1,2}\s+[\d:]{8}\s+\d{4})\s*$/u.exec(line);
394
- if (!match || match[4].startsWith("Z")) {
395
- continue;
396
- }
397
- const pid = Number(match[1]);
398
- const environment = Number(match[3]) === currentUid
399
- ? darwinEnvironment(commands.get(pid), commandsWithEnvironment.get(pid))
400
- : null;
401
- records.push({
402
- commandHash: environmentValue(environment || [], commandHashEnvironmentName),
403
- environmentReadable: Boolean(environment),
404
- pid,
405
- processGroupId: Number(match[2]),
406
- runtimeToken: environmentValue(environment || [], runtimeTokenEnvironmentName),
407
- startTimeTicks: darwinStartTime(match[5])
408
- });
409
- }
410
- return records;
411
- }
412
-
413
- function normalizeProcessIdentityInspection(value = {}) {
414
- const status = Object.values(PROCESS_IDENTITY_STATUS).includes(value?.status)
415
- ? value.status
416
- : PROCESS_IDENTITY_STATUS.AMBIGUOUS;
417
- return {
418
- processGroupIds: [...new Set((Array.isArray(value?.processGroupIds)
419
- ? value.processGroupIds
420
- : [])
421
- .map(Number)
422
- .filter((processGroupId) => Number.isSafeInteger(processGroupId) && processGroupId > 0))],
423
- status
424
- };
425
- }
426
-
427
- async function inspectHostProcessIdentity(metadata = {}) {
428
- if (!processMetadataIsIdentifiable(metadata)) {
429
- return {
430
- processGroupIds: [],
431
- status: PROCESS_IDENTITY_STATUS.INVALID
432
- };
433
- }
434
- if (metadata.processState === PROCESS_STATE.STOPPED) {
435
- return {
436
- processGroupIds: [],
437
- status: PROCESS_IDENTITY_STATUS.ABSENT
438
- };
439
- }
440
- const identity = metadata.processIdentity;
441
- const records = identity.platform !== hostIdentityPlatform
442
- ? null
443
- : platform === "darwin"
444
- ? await darwinProcessRecords().catch(() => null)
445
- : await linuxProcessRecords();
446
- if (!records) {
447
- return {
448
- processGroupIds: [],
449
- status: PROCESS_IDENTITY_STATUS.AMBIGUOUS
450
- };
451
- }
452
- const rootProcessGroupId = Number(metadata.pid);
453
- const rootGroupMembers = records.filter(({ processGroupId }) => processGroupId === rootProcessGroupId);
454
- const exactRecords = [];
455
- const rootUnreadable = rootGroupMembers.some(({ environmentReadable }) => !environmentReadable);
456
- for (const record of records) {
457
- if (!record.environmentReadable) {
458
- continue;
459
- }
460
- if (record.runtimeToken === identity.runtimeToken && record.commandHash === identity.commandHash) {
461
- exactRecords.push(record);
462
- }
463
- }
464
- const exactLeader = exactRecords.find(({ pid }) => pid === rootProcessGroupId);
465
- if (exactLeader && exactLeader.startTimeTicks !== identity.startTimeTicks) {
466
- return {
467
- processGroupIds: [],
468
- status: PROCESS_IDENTITY_STATUS.AMBIGUOUS
469
- };
470
- }
471
- if (exactRecords.length === 0) {
472
- if (rootUnreadable) {
473
- return {
474
- processGroupIds: [],
475
- status: PROCESS_IDENTITY_STATUS.AMBIGUOUS
476
- };
477
- }
478
- return {
479
- processGroupIds: [],
480
- status: rootGroupMembers.length > 0 || processGroupIsAlive(rootProcessGroupId)
481
- ? PROCESS_IDENTITY_STATUS.MISMATCH
482
- : PROCESS_IDENTITY_STATUS.ABSENT
483
- };
484
- }
485
- const processGroupIds = [...new Set(exactRecords.map(({ processGroupId }) => processGroupId))];
486
- for (const processGroupId of processGroupIds) {
487
- const groupMembers = records.filter((record) => record.processGroupId === processGroupId);
488
- if (groupMembers.some((record) => (
489
- !record.environmentReadable || !exactRecords.some(({ pid }) => pid === record.pid)
490
- ))) {
491
- return {
492
- processGroupIds: [],
493
- status: PROCESS_IDENTITY_STATUS.AMBIGUOUS
494
- };
495
- }
496
- }
497
- if (
498
- rootGroupMembers.some((record) => !exactRecords.some(({ pid }) => pid === record.pid))
499
- ) {
500
- return {
501
- processGroupIds: [],
502
- status: PROCESS_IDENTITY_STATUS.AMBIGUOUS
503
- };
504
- }
505
- return {
506
- processGroupIds,
507
- status: PROCESS_IDENTITY_STATUS.EXACT
508
- };
509
- }
510
-
511
- async function inspectProcessIdentity(metadata = {}, options = {}) {
512
- const inspector = typeof options.processIdentityInspector === "function"
513
- ? options.processIdentityInspector
514
- : inspectHostProcessIdentity;
515
- return normalizeProcessIdentityInspection(await inspector(metadata));
516
- }
517
-
518
- async function waitForProcessIdentityExit(metadata = {}, options = {}, timeoutMs = 0) {
519
- const deadline = Date.now() + timeoutMs;
520
- let inspection = await inspectProcessIdentity(metadata, options);
521
- while (
522
- Date.now() < deadline &&
523
- inspection.status === PROCESS_IDENTITY_STATUS.EXACT
524
- ) {
525
- await delay(100);
526
- inspection = await inspectProcessIdentity(metadata, options);
527
- }
528
- return inspection;
529
- }
184
+ return Object.freeze({
530
185
 
531
- async function waitForProcessIdentityToSettle(metadata = {}, options = {}) {
532
- const timeoutMs = normalizePositiveInteger(
533
- options.processIdentitySettleTimeoutMs,
534
- { fallback: PROCESS_IDENTITY_SETTLE_TIMEOUT_MS }
535
- );
536
- const pollMs = normalizePositiveInteger(
537
- options.processIdentitySettlePollMs,
538
- { fallback: PROCESS_IDENTITY_SETTLE_POLL_MS }
539
- );
540
- const deadline = Date.now() + timeoutMs;
541
- let inspection = await inspectProcessIdentity(metadata, options);
542
- while (
543
- inspection.status === PROCESS_IDENTITY_STATUS.AMBIGUOUS &&
544
- Date.now() < deadline
545
- ) {
546
- await delay(Math.min(pollMs, Math.max(1, deadline - Date.now())));
547
- inspection = await inspectProcessIdentity(metadata, options);
548
- }
549
- return inspection;
550
- }
186
+ start,
551
187
 
552
- async function signalVerifiedProcessGroups(metadata = {}, signal = "SIGTERM", options = {}) {
553
- const signalProcessGroupImpl = typeof options.signalProcessGroup === "function"
554
- ? options.signalProcessGroup
555
- : signalProcessGroup;
556
- const initial = await waitForProcessIdentityToSettle(metadata, options);
557
- if (initial.status !== PROCESS_IDENTITY_STATUS.EXACT) {
558
- return {
559
- inspection: initial,
560
- signaledProcessGroups: []
561
- };
562
- }
563
- const rootProcessGroupId = Number(metadata.pid);
564
- const orderedProcessGroupIds = [
565
- ...initial.processGroupIds.filter((processGroupId) => processGroupId !== rootProcessGroupId),
566
- ...initial.processGroupIds.filter((processGroupId) => processGroupId === rootProcessGroupId)
567
- ];
568
- const signaledProcessGroups = [];
569
- for (const processGroupId of orderedProcessGroupIds) {
570
- const current = await waitForProcessIdentityToSettle(metadata, options);
571
- if (
572
- current.status === PROCESS_IDENTITY_STATUS.ABSENT ||
573
- current.status === PROCESS_IDENTITY_STATUS.MISMATCH
574
- ) {
575
- return {
576
- inspection: current,
577
- signaledProcessGroups
578
- };
579
- }
580
- if (current.status !== PROCESS_IDENTITY_STATUS.EXACT) {
581
- return {
582
- inspection: current,
583
- signaledProcessGroups
584
- };
585
- }
586
- if (!current.processGroupIds.includes(processGroupId)) {
587
- continue;
588
- }
589
- signalProcessGroupImpl(processGroupId, signal);
590
- signaledProcessGroups.push(processGroupId);
591
- }
592
- return {
593
- inspection: await waitForProcessIdentityToSettle(metadata, options),
594
- signaledProcessGroups
595
- };
596
- }
188
+ run,
597
189
 
598
- async function stopProcessGroup(metadata = {}, options = {}) {
599
- if (!processMetadataIsIdentifiable(metadata)) {
600
- return {
601
- identityStatus: PROCESS_IDENTITY_STATUS.INVALID,
602
- processExitVerified: false,
603
- stopped: false
604
- };
605
- }
606
- if (
607
- metadata.processState === PROCESS_STATE.STOPPED &&
608
- metadata.processExitVerifiedAt
609
- ) {
610
- return {
611
- alreadyStopped: true,
612
- identityStatus: PROCESS_IDENTITY_STATUS.ABSENT,
613
- processExitVerified: true,
614
- stopped: false
615
- };
616
- }
617
- const before = await waitForProcessIdentityToSettle(metadata, options);
618
- if (
619
- before.status === PROCESS_IDENTITY_STATUS.ABSENT ||
620
- before.status === PROCESS_IDENTITY_STATUS.MISMATCH
621
- ) {
622
- return {
623
- identityStatus: before.status,
624
- processExitVerified: true,
625
- stopped: false
626
- };
627
- }
628
- if (before.status !== PROCESS_IDENTITY_STATUS.EXACT) {
629
- return {
630
- identityStatus: before.status,
631
- processExitVerified: false,
632
- stopped: false
633
- };
634
- }
635
- const term = await signalVerifiedProcessGroups(metadata, "SIGTERM", options);
636
- if (term.inspection.status === PROCESS_IDENTITY_STATUS.AMBIGUOUS) {
637
- return {
638
- identityStatus: term.inspection.status,
639
- processExitVerified: false,
640
- signaledProcessGroups: term.signaledProcessGroups,
641
- stopped: false
642
- };
643
- }
644
- let after = await waitForProcessIdentityExit(
645
- metadata,
646
- options,
647
- normalizePositiveInteger(options.termTimeoutMs, { fallback: 3000 })
648
- );
649
- let signaledProcessGroups = [...term.signaledProcessGroups];
650
- if (after.status === PROCESS_IDENTITY_STATUS.EXACT) {
651
- const kill = await signalVerifiedProcessGroups(metadata, "SIGKILL", options);
652
- signaledProcessGroups = [...new Set([
653
- ...signaledProcessGroups,
654
- ...kill.signaledProcessGroups
655
- ])];
656
- if (kill.inspection.status === PROCESS_IDENTITY_STATUS.AMBIGUOUS) {
657
- return {
658
- identityStatus: kill.inspection.status,
659
- processExitVerified: false,
660
- signaledProcessGroups,
661
- stopped: false
662
- };
663
- }
664
- after = await waitForProcessIdentityExit(
665
- metadata,
666
- options,
667
- normalizePositiveInteger(options.killTimeoutMs, { fallback: 1000 })
668
- );
669
- }
670
- const processExitVerified = [
671
- PROCESS_IDENTITY_STATUS.ABSENT,
672
- PROCESS_IDENTITY_STATUS.MISMATCH
673
- ].includes(after.status);
674
- return {
675
- descendantProcessGroups: signaledProcessGroups.filter((processGroupId) => (
676
- processGroupId !== Number(metadata.pid)
677
- )),
678
- identityStatus: after.status,
679
- pid: Number(metadata.pid),
680
- processExitVerified,
681
- signaledProcessGroups,
682
- stopped: processExitVerified && signaledProcessGroups.length > 0
683
- };
684
- }
685
-
686
- async function captureProcessStart(pid) {
687
- if (platform !== "darwin") {
688
- return linuxProcessStat(await readFile(`/proc/${pid}/stat`, "utf8").catch(() => ""));
689
- }
690
- const output = String(await ps(["-o", "pgid=,lstart=", "-p", String(pid)]).catch(() => ""));
691
- const match = /^\s*(\d+)\s+(.+?)\s*$/u.exec(output);
692
- return match ? { processGroupId: Number(match[1]), startTimeTicks: darwinStartTime(match[2]) } : null;
693
- }
190
+ stop,
694
191
 
695
- async function captureProcessIdentity({
696
- commandHash = "",
697
- pid = null,
698
- reportedIdentity = null,
699
- runtimeToken = ""
700
- } = {}) {
701
- const expectedCommandHash = normalizeNullableText(commandHash);
702
- const expectedRuntimeToken = normalizeNullableText(runtimeToken);
703
- const reported = normalizeProcessIdentity(reportedIdentity);
704
- if (
705
- processIdentityIsWellFormed(reported) &&
706
- reported.commandHash === expectedCommandHash &&
707
- reported.runtimeToken === expectedRuntimeToken
708
- ) {
709
- return reported;
710
- }
711
- const normalizedPid = Number(pid);
712
- const processStat = hostIdentityPlatform && Number.isSafeInteger(normalizedPid) && normalizedPid > 0
713
- ? await captureProcessStart(normalizedPid)
714
- : null;
715
- if (!processStat?.startTimeTicks || processStat.processGroupId !== normalizedPid) {
716
- const error = new Error(`${label} process identity could not be captured.`);
717
- error.code = unavailableCode;
718
- throw error;
719
- }
720
- return {
721
- commandHash: expectedCommandHash,
722
- platform: hostIdentityPlatform,
723
- runtimeToken: expectedRuntimeToken,
724
- startTimeTicks: processStat.startTimeTicks,
725
- version: PROCESS_IDENTITY_VERSION
726
- };
727
- }
192
+ close
728
193
 
729
- return Object.freeze({
730
- normalize: normalizeProcessIdentity,
731
- isValid: processIdentityIsWellFormed,
732
- normalizeInspection: normalizeProcessIdentityInspection,
733
- capture: captureProcessIdentity,
734
- inspect: inspectProcessIdentity,
735
- stop: stopProcessGroup,
736
- isProcessAlive: processIsAlive
737
194
  });
738
195
  }