acpx 0.18.0 → 0.19.1
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.
- package/dist/{agent-registry-DdKB-zia.js → agent-registry-DWlx5BQ4.js} +5 -3
- package/dist/agent-registry-DWlx5BQ4.js.map +1 -0
- package/dist/agent-registry.js +1 -1
- package/dist/{cli-8mVHeUXh.js → cli-BerngYEz.js} +5 -5
- package/dist/{cli-8mVHeUXh.js.map → cli-BerngYEz.js.map} +1 -1
- package/dist/cli.js +77 -53
- package/dist/cli.js.map +1 -1
- package/dist/{client-DeNKZZcu.d.ts → client-BchvQRpz.d.ts} +17 -4
- package/dist/client-BchvQRpz.d.ts.map +1 -0
- package/dist/{flows-WOQNzCMC.js → flows-DimX0it5.js} +693 -475
- package/dist/flows-DimX0it5.js.map +1 -0
- package/dist/flows.d.ts +20 -7
- package/dist/flows.d.ts.map +1 -1
- package/dist/flows.js +1 -1
- package/dist/{invocation-options-B6d7TI-y.js → invocation-options-vobVApfO.js} +32 -11
- package/dist/invocation-options-vobVApfO.js.map +1 -0
- package/dist/{ipc-BJrvfNXr.js → ipc-DAApJ5mi.js} +1041 -263
- package/dist/ipc-DAApJ5mi.js.map +1 -0
- package/dist/{output-CL18YmsG.js → output-Cnuji8Bo.js} +41 -20
- package/dist/output-Cnuji8Bo.js.map +1 -0
- package/dist/{queue-owner-runtime-Cr0dWJuL.js → queue-owner-runtime-C4OOosbo.js} +732 -507
- package/dist/queue-owner-runtime-C4OOosbo.js.map +1 -0
- package/dist/runtime.d.ts +19 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +48 -29
- package/dist/runtime.js.map +1 -1
- package/dist/session-options-Dp6mzNSy.d.ts.map +1 -1
- package/dist/{session-watch-B8Q_TsGf.js → session-watch-Bj9LoYUq.js} +5 -5
- package/dist/session-watch-Bj9LoYUq.js.map +1 -0
- package/dist/{watch-Dp73ph8h.js → watch-Cz6jbrRs.js} +2 -2
- package/dist/{watch-Dp73ph8h.js.map → watch-Cz6jbrRs.js.map} +1 -1
- package/package.json +1 -1
- package/skills/acpx/SKILL.md +45 -23
- package/dist/agent-registry-DdKB-zia.js.map +0 -1
- package/dist/client-DeNKZZcu.d.ts.map +0 -1
- package/dist/flows-WOQNzCMC.js.map +0 -1
- package/dist/invocation-options-B6d7TI-y.js.map +0 -1
- package/dist/ipc-BJrvfNXr.js.map +0 -1
- package/dist/output-CL18YmsG.js.map +0 -1
- package/dist/queue-owner-runtime-Cr0dWJuL.js.map +0 -1
- package/dist/session-watch-B8Q_TsGf.js.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { n as AGENT_REGISTRY, r as BUILT_IN_AGENT_PACKAGES, t as AGENT_ARGV_REGISTRY } from "./agent-registry-
|
|
2
|
-
import { statSync } from "node:fs";
|
|
1
|
+
import { C as PROCESS_HELPER_TIMEOUT_MS, E as isChildProcessRunning, N as runTimedExecFile, S as PROCESS_HELPER_MAX_BUFFER_BYTES, n as AGENT_REGISTRY, r as BUILT_IN_AGENT_PACKAGES, t as AGENT_ARGV_REGISTRY } from "./agent-registry-DWlx5BQ4.js";
|
|
2
|
+
import fs, { statSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import fs$1 from "node:fs/promises";
|
|
5
5
|
import os from "node:os";
|
|
@@ -217,6 +217,7 @@ const OUTPUT_ERROR_CODES = [
|
|
|
217
217
|
"TIMEOUT",
|
|
218
218
|
"PERMISSION_DENIED",
|
|
219
219
|
"PERMISSION_PROMPT_UNAVAILABLE",
|
|
220
|
+
"EXEC_DISABLED",
|
|
220
221
|
"RUNTIME",
|
|
221
222
|
"USAGE"
|
|
222
223
|
];
|
|
@@ -230,12 +231,12 @@ const SESSION_RECORD_SCHEMA = "acpx.session.v1";
|
|
|
230
231
|
//#endregion
|
|
231
232
|
//#region src/acp/error-shapes.ts
|
|
232
233
|
const RESOURCE_NOT_FOUND_ACP_CODES = /* @__PURE__ */ new Set([-32001, -32002]);
|
|
233
|
-
function asRecord$
|
|
234
|
+
function asRecord$4(value) {
|
|
234
235
|
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
235
236
|
return value;
|
|
236
237
|
}
|
|
237
238
|
function toAcpErrorPayload(value) {
|
|
238
|
-
const record = asRecord$
|
|
239
|
+
const record = asRecord$4(value);
|
|
239
240
|
if (!record) return;
|
|
240
241
|
if (typeof record.code !== "number" || !Number.isFinite(record.code)) return;
|
|
241
242
|
if (typeof record.message !== "string" || record.message.length === 0) return;
|
|
@@ -249,7 +250,7 @@ function extractAcpErrorInternal(value, depth) {
|
|
|
249
250
|
if (depth > 5) return;
|
|
250
251
|
const direct = toAcpErrorPayload(value);
|
|
251
252
|
if (direct) return direct;
|
|
252
|
-
const record = asRecord$
|
|
253
|
+
const record = asRecord$4(value);
|
|
253
254
|
if (!record) return;
|
|
254
255
|
return extractNestedAcpError(record, depth);
|
|
255
256
|
}
|
|
@@ -284,7 +285,7 @@ function hasSessionNotFoundHint(value, depth = 0) {
|
|
|
284
285
|
if (depth > 4) return false;
|
|
285
286
|
if (isSessionNotFoundText(value)) return true;
|
|
286
287
|
if (Array.isArray(value)) return value.some((entry) => hasSessionNotFoundHint(entry, depth + 1));
|
|
287
|
-
const record = asRecord$
|
|
288
|
+
const record = asRecord$4(value);
|
|
288
289
|
if (!record) return false;
|
|
289
290
|
return Object.values(record).some((entry) => hasSessionNotFoundHint(entry, depth + 1));
|
|
290
291
|
}
|
|
@@ -440,18 +441,18 @@ function defaultSessionEventLog(sessionId) {
|
|
|
440
441
|
}
|
|
441
442
|
//#endregion
|
|
442
443
|
//#region src/acp/jsonrpc.ts
|
|
443
|
-
function asRecord$
|
|
444
|
+
function asRecord$3(value) {
|
|
444
445
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
445
446
|
return value;
|
|
446
447
|
}
|
|
447
448
|
function isAcpMessageObject(value) {
|
|
448
|
-
return asRecord$
|
|
449
|
+
return asRecord$3(value) !== null;
|
|
449
450
|
}
|
|
450
451
|
function hasValidId(value) {
|
|
451
452
|
return value === null || typeof value === "string" || typeof value === "number" && Number.isFinite(value);
|
|
452
453
|
}
|
|
453
454
|
function isErrorObject(value) {
|
|
454
|
-
const record = asRecord$
|
|
455
|
+
const record = asRecord$3(value);
|
|
455
456
|
return !!record && typeof record.code === "number" && Number.isFinite(record.code) && typeof record.message === "string";
|
|
456
457
|
}
|
|
457
458
|
function hasResultOrError(value) {
|
|
@@ -476,7 +477,7 @@ function isJsonRpcResponse(value) {
|
|
|
476
477
|
return hasResultOrError(value);
|
|
477
478
|
}
|
|
478
479
|
function isAcpJsonRpcMessage(value) {
|
|
479
|
-
const record = asRecord$
|
|
480
|
+
const record = asRecord$3(value);
|
|
480
481
|
if (!record || record.jsonrpc !== "2.0") return false;
|
|
481
482
|
return isJsonRpcNotificationRecord(record) || isJsonRpcRequest(record) || isJsonRpcResponse(record);
|
|
482
483
|
}
|
|
@@ -486,14 +487,29 @@ function isJsonRpcNotification(message) {
|
|
|
486
487
|
function isSessionUpdateNotification(message) {
|
|
487
488
|
return isJsonRpcNotification(message) && message.method === "session/update";
|
|
488
489
|
}
|
|
490
|
+
function sessionUpdateHasRequiredFields(update) {
|
|
491
|
+
const kind = update.sessionUpdate;
|
|
492
|
+
if (typeof kind !== "string") return false;
|
|
493
|
+
if (kind === "agent_message_chunk" || kind === "agent_thought_chunk" || kind === "user_message_chunk") return isChunkContent(update.content);
|
|
494
|
+
if (kind === "plan") return Array.isArray(update.entries) && update.entries.every(isPlanEntry);
|
|
495
|
+
return true;
|
|
496
|
+
}
|
|
497
|
+
function isChunkContent(value) {
|
|
498
|
+
const content = asRecord$3(value);
|
|
499
|
+
return content !== null && typeof content.type === "string" && (content.type !== "text" || typeof content.text === "string");
|
|
500
|
+
}
|
|
501
|
+
function isPlanEntry(value) {
|
|
502
|
+
const entry = asRecord$3(value);
|
|
503
|
+
return entry !== null && typeof entry.status === "string" && typeof entry.content === "string";
|
|
504
|
+
}
|
|
489
505
|
function extractSessionUpdateNotification(message) {
|
|
490
506
|
if (!isSessionUpdateNotification(message)) return;
|
|
491
|
-
const params = asRecord$
|
|
507
|
+
const params = asRecord$3(message.params);
|
|
492
508
|
if (!params) return;
|
|
493
509
|
const sessionId = typeof params.sessionId === "string" ? params.sessionId : null;
|
|
494
510
|
if (!sessionId) return;
|
|
495
|
-
const update = asRecord$
|
|
496
|
-
if (!update ||
|
|
511
|
+
const update = asRecord$3(params.update);
|
|
512
|
+
if (!update || !sessionUpdateHasRequiredFields(update)) return;
|
|
497
513
|
return {
|
|
498
514
|
sessionId,
|
|
499
515
|
update
|
|
@@ -501,18 +517,18 @@ function extractSessionUpdateNotification(message) {
|
|
|
501
517
|
}
|
|
502
518
|
function parsePromptStopReason(message) {
|
|
503
519
|
if (!Object.hasOwn(message, "id") || !Object.hasOwn(message, "result")) return;
|
|
504
|
-
const record = asRecord$
|
|
520
|
+
const record = asRecord$3(message.result);
|
|
505
521
|
if (!record) return;
|
|
506
522
|
return typeof record.stopReason === "string" ? record.stopReason : void 0;
|
|
507
523
|
}
|
|
508
524
|
function parsePermissionNotice(message) {
|
|
509
525
|
if (!Object.hasOwn(message, "id") || !Object.hasOwn(message, "result")) return;
|
|
510
|
-
const notice = asRecord$
|
|
526
|
+
const notice = asRecord$3(asRecord$3(asRecord$3(message.result)?._meta)?.acpx)?.permissionNotice;
|
|
511
527
|
return typeof notice === "string" ? notice : void 0;
|
|
512
528
|
}
|
|
513
529
|
function parseJsonRpcErrorMessage(message) {
|
|
514
530
|
if (!Object.hasOwn(message, "error")) return;
|
|
515
|
-
const errorRecord = asRecord$
|
|
531
|
+
const errorRecord = asRecord$3(message.error);
|
|
516
532
|
if (!errorRecord || typeof errorRecord.message !== "string") return;
|
|
517
533
|
return errorRecord.message;
|
|
518
534
|
}
|
|
@@ -1108,7 +1124,7 @@ function resolveAgentArgvForCommand(agentCommand) {
|
|
|
1108
1124
|
}
|
|
1109
1125
|
//#endregion
|
|
1110
1126
|
//#region src/session/persistence/parse.ts
|
|
1111
|
-
function asRecord$
|
|
1127
|
+
function asRecord$2(value) {
|
|
1112
1128
|
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
1113
1129
|
return value;
|
|
1114
1130
|
}
|
|
@@ -1127,12 +1143,12 @@ function parsePersistedAgentArgv(record) {
|
|
|
1127
1143
|
function hasModelConfigOption(options) {
|
|
1128
1144
|
if (!Array.isArray(options)) return false;
|
|
1129
1145
|
return options.some((entry) => {
|
|
1130
|
-
const option = asRecord$
|
|
1146
|
+
const option = asRecord$2(entry);
|
|
1131
1147
|
return option?.category === "model" || option?.id === "model";
|
|
1132
1148
|
});
|
|
1133
1149
|
}
|
|
1134
1150
|
function parseConfigOptions(raw) {
|
|
1135
|
-
if (!Array.isArray(raw) || !raw.every((entry) => asRecord$
|
|
1151
|
+
if (!Array.isArray(raw) || !raw.every((entry) => asRecord$2(entry) !== void 0)) return;
|
|
1136
1152
|
return raw;
|
|
1137
1153
|
}
|
|
1138
1154
|
function parseAvailableCommand(raw) {
|
|
@@ -1140,7 +1156,7 @@ function parseAvailableCommand(raw) {
|
|
|
1140
1156
|
const name = raw.trim();
|
|
1141
1157
|
return name ? { name } : void 0;
|
|
1142
1158
|
}
|
|
1143
|
-
const record = asRecord$
|
|
1159
|
+
const record = asRecord$2(raw);
|
|
1144
1160
|
if (!record) return;
|
|
1145
1161
|
const name = parseNonEmptyString$1(record.name);
|
|
1146
1162
|
if (!name) return;
|
|
@@ -1163,7 +1179,7 @@ function parseNonEmptyString$1(value) {
|
|
|
1163
1179
|
}
|
|
1164
1180
|
function parseTokenUsage(raw) {
|
|
1165
1181
|
if (raw === void 0 || raw === null) return;
|
|
1166
|
-
const record = asRecord$
|
|
1182
|
+
const record = asRecord$2(raw);
|
|
1167
1183
|
if (!record) return null;
|
|
1168
1184
|
const usage = {};
|
|
1169
1185
|
for (const field of [
|
|
@@ -1186,7 +1202,7 @@ function isNonNegativeFiniteNumber(value) {
|
|
|
1186
1202
|
}
|
|
1187
1203
|
function parseUsageCost(raw) {
|
|
1188
1204
|
if (raw === void 0 || raw === null) return;
|
|
1189
|
-
const record = asRecord$
|
|
1205
|
+
const record = asRecord$2(raw);
|
|
1190
1206
|
if (!record) return null;
|
|
1191
1207
|
return parseUsageCostRecord(record);
|
|
1192
1208
|
}
|
|
@@ -1212,7 +1228,7 @@ function parseCostCurrency(value) {
|
|
|
1212
1228
|
}
|
|
1213
1229
|
function parseRequestTokenUsage(raw) {
|
|
1214
1230
|
if (raw === void 0 || raw === null) return;
|
|
1215
|
-
const record = asRecord$
|
|
1231
|
+
const record = asRecord$2(raw);
|
|
1216
1232
|
if (!record) return null;
|
|
1217
1233
|
const usage = {};
|
|
1218
1234
|
for (const [key, value] of Object.entries(record)) {
|
|
@@ -1223,25 +1239,25 @@ function parseRequestTokenUsage(raw) {
|
|
|
1223
1239
|
return usage;
|
|
1224
1240
|
}
|
|
1225
1241
|
function isSessionMessageImage(raw) {
|
|
1226
|
-
const record = asRecord$
|
|
1242
|
+
const record = asRecord$2(raw);
|
|
1227
1243
|
if (!record || typeof record.source !== "string") return false;
|
|
1228
1244
|
if (record.size === void 0 || record.size === null) return true;
|
|
1229
|
-
const size = asRecord$
|
|
1245
|
+
const size = asRecord$2(record.size);
|
|
1230
1246
|
return !!size && isFiniteNumber(size.width) && isFiniteNumber(size.height);
|
|
1231
1247
|
}
|
|
1232
1248
|
function isSessionMessageAudio(raw) {
|
|
1233
|
-
const record = asRecord$
|
|
1249
|
+
const record = asRecord$2(raw);
|
|
1234
1250
|
return !!record && typeof record.source === "string" && typeof record.mime_type === "string";
|
|
1235
1251
|
}
|
|
1236
1252
|
function isFiniteNumber(value) {
|
|
1237
1253
|
return typeof value === "number" && Number.isFinite(value);
|
|
1238
1254
|
}
|
|
1239
1255
|
function isUserContent(raw) {
|
|
1240
|
-
const record = asRecord$
|
|
1256
|
+
const record = asRecord$2(raw);
|
|
1241
1257
|
if (!record) return false;
|
|
1242
1258
|
if (typeof record.Text === "string") return true;
|
|
1243
1259
|
if (record.Mention !== void 0) {
|
|
1244
|
-
const mention = asRecord$
|
|
1260
|
+
const mention = asRecord$2(record.Mention);
|
|
1245
1261
|
return !!mention && typeof mention.uri === "string" && typeof mention.content === "string";
|
|
1246
1262
|
}
|
|
1247
1263
|
if (record.Image !== void 0) return isSessionMessageImage(record.Image);
|
|
@@ -1249,7 +1265,7 @@ function isUserContent(raw) {
|
|
|
1249
1265
|
return false;
|
|
1250
1266
|
}
|
|
1251
1267
|
function isToolUse(raw) {
|
|
1252
|
-
const record = asRecord$
|
|
1268
|
+
const record = asRecord$2(raw);
|
|
1253
1269
|
return !!record && hasStringFields$1(record, [
|
|
1254
1270
|
"id",
|
|
1255
1271
|
"name",
|
|
@@ -1263,18 +1279,18 @@ function isOptionalString(value) {
|
|
|
1263
1279
|
return value === void 0 || value === null || typeof value === "string";
|
|
1264
1280
|
}
|
|
1265
1281
|
function isToolResultContent(raw) {
|
|
1266
|
-
const record = asRecord$
|
|
1282
|
+
const record = asRecord$2(raw);
|
|
1267
1283
|
if (!record) return false;
|
|
1268
1284
|
if (typeof record.Text === "string") return true;
|
|
1269
1285
|
if (record.Image !== void 0) return isSessionMessageImage(record.Image);
|
|
1270
1286
|
return false;
|
|
1271
1287
|
}
|
|
1272
1288
|
function isToolResult(raw) {
|
|
1273
|
-
const record = asRecord$
|
|
1289
|
+
const record = asRecord$2(raw);
|
|
1274
1290
|
return !!record && typeof record.tool_use_id === "string" && typeof record.tool_name === "string" && typeof record.is_error === "boolean" && isToolResultContent(record.content);
|
|
1275
1291
|
}
|
|
1276
1292
|
function isAgentContent(raw) {
|
|
1277
|
-
const record = asRecord$
|
|
1293
|
+
const record = asRecord$2(raw);
|
|
1278
1294
|
if (!record) return false;
|
|
1279
1295
|
if (typeof record.Text === "string") return true;
|
|
1280
1296
|
if (record.Thinking !== void 0) return isThinkingContent(record.Thinking);
|
|
@@ -1283,21 +1299,21 @@ function isAgentContent(raw) {
|
|
|
1283
1299
|
return false;
|
|
1284
1300
|
}
|
|
1285
1301
|
function isThinkingContent(raw) {
|
|
1286
|
-
const thinking = asRecord$
|
|
1302
|
+
const thinking = asRecord$2(raw);
|
|
1287
1303
|
return !!thinking && typeof thinking.text === "string" && isOptionalString(thinking.signature);
|
|
1288
1304
|
}
|
|
1289
1305
|
function isUserMessage(raw) {
|
|
1290
|
-
const record = asRecord$
|
|
1306
|
+
const record = asRecord$2(raw);
|
|
1291
1307
|
if (!record || record.User === void 0) return false;
|
|
1292
|
-
const user = asRecord$
|
|
1308
|
+
const user = asRecord$2(record.User);
|
|
1293
1309
|
return !!user && typeof user.id === "string" && Array.isArray(user.content) && user.content.every((entry) => isUserContent(entry));
|
|
1294
1310
|
}
|
|
1295
1311
|
function isAgentMessage(raw) {
|
|
1296
|
-
const record = asRecord$
|
|
1312
|
+
const record = asRecord$2(raw);
|
|
1297
1313
|
if (!record || record.Agent === void 0) return false;
|
|
1298
|
-
const agent = asRecord$
|
|
1314
|
+
const agent = asRecord$2(record.Agent);
|
|
1299
1315
|
if (!agent || !Array.isArray(agent.content) || !agent.content.every(isAgentContent)) return false;
|
|
1300
|
-
const toolResults = asRecord$
|
|
1316
|
+
const toolResults = asRecord$2(agent.tool_results);
|
|
1301
1317
|
if (!toolResults) return false;
|
|
1302
1318
|
return Object.values(toolResults).every(isToolResult);
|
|
1303
1319
|
}
|
|
@@ -1330,7 +1346,7 @@ function hasValidConversationCore(record) {
|
|
|
1330
1346
|
return Array.isArray(record.messages) && record.messages.every(isConversationMessage) && typeof record.updated_at === "string";
|
|
1331
1347
|
}
|
|
1332
1348
|
function parseAcpxState(raw) {
|
|
1333
|
-
const record = asRecord$
|
|
1349
|
+
const record = asRecord$2(raw);
|
|
1334
1350
|
if (!record) return;
|
|
1335
1351
|
const state = {};
|
|
1336
1352
|
if (record.reset_on_next_ensure === true) state.reset_on_next_ensure = true;
|
|
@@ -1358,13 +1374,13 @@ function assignStringState(state, key, value) {
|
|
|
1358
1374
|
if (typeof value === "string") state[key] = value;
|
|
1359
1375
|
}
|
|
1360
1376
|
function parseStringMap(raw, preserveEmpty = false) {
|
|
1361
|
-
const record = asRecord$
|
|
1377
|
+
const record = asRecord$2(raw);
|
|
1362
1378
|
if (!record) return;
|
|
1363
1379
|
const parsed = Object.fromEntries(Object.entries(record).filter((entry) => typeof entry[1] === "string"));
|
|
1364
1380
|
return Object.keys(parsed).length > 0 || preserveEmpty ? parsed : void 0;
|
|
1365
1381
|
}
|
|
1366
1382
|
function assignParsedSessionOptions(state, raw) {
|
|
1367
|
-
const sessionOptions = asRecord$
|
|
1383
|
+
const sessionOptions = asRecord$2(raw);
|
|
1368
1384
|
if (!sessionOptions) return;
|
|
1369
1385
|
const parsedSessionOptions = {};
|
|
1370
1386
|
if (typeof sessionOptions.model === "string") parsedSessionOptions.model = sessionOptions.model;
|
|
@@ -1380,11 +1396,11 @@ function assignSessionOptionSystemPrompt(options, value) {
|
|
|
1380
1396
|
options.system_prompt = value;
|
|
1381
1397
|
return;
|
|
1382
1398
|
}
|
|
1383
|
-
const appendRecord = asRecord$
|
|
1399
|
+
const appendRecord = asRecord$2(value);
|
|
1384
1400
|
if (appendRecord && typeof appendRecord.append === "string" && appendRecord.append.length > 0) options.system_prompt = { append: appendRecord.append };
|
|
1385
1401
|
}
|
|
1386
1402
|
function parseEventLog(raw, sessionId) {
|
|
1387
|
-
const record = asRecord$
|
|
1403
|
+
const record = asRecord$2(raw);
|
|
1388
1404
|
if (!record || !hasValidEventLogCore(record)) return defaultSessionEventLog(sessionId);
|
|
1389
1405
|
return {
|
|
1390
1406
|
active_path: record.active_path,
|
|
@@ -1403,7 +1419,7 @@ function isPositiveInteger$1(value) {
|
|
|
1403
1419
|
}
|
|
1404
1420
|
function parseImportedFrom(raw) {
|
|
1405
1421
|
if (raw == null) return;
|
|
1406
|
-
const record = asRecord$
|
|
1422
|
+
const record = asRecord$2(raw);
|
|
1407
1423
|
if (!record || typeof record.record_id !== "string" || typeof record.cwd_original !== "string" || typeof record.exported_by !== "string" || typeof record.exported_at !== "string") return null;
|
|
1408
1424
|
return {
|
|
1409
1425
|
recordId: record.record_id,
|
|
@@ -1453,7 +1469,7 @@ function normalizeOptionalSignal(value) {
|
|
|
1453
1469
|
return Symbol("invalid");
|
|
1454
1470
|
}
|
|
1455
1471
|
function parseSessionRecord(raw) {
|
|
1456
|
-
const record = asRecord$
|
|
1472
|
+
const record = asRecord$2(raw);
|
|
1457
1473
|
if (!record) return null;
|
|
1458
1474
|
if (record.schema !== "acpx.session.v1") return null;
|
|
1459
1475
|
const optionals = validSessionOptionals({
|
|
@@ -1498,7 +1514,7 @@ function parseSessionRecord(raw) {
|
|
|
1498
1514
|
lastAgentExitAt: optionals.lastAgentExitAt,
|
|
1499
1515
|
lastAgentDisconnectReason: optionals.lastAgentDisconnectReason,
|
|
1500
1516
|
protocolVersion: typeof record.protocol_version === "number" ? record.protocol_version : void 0,
|
|
1501
|
-
agentCapabilities: asRecord$
|
|
1517
|
+
agentCapabilities: asRecord$2(record.agent_capabilities),
|
|
1502
1518
|
title: conversation.title,
|
|
1503
1519
|
messages: conversation.messages,
|
|
1504
1520
|
updated_at: conversation.updated_at,
|
|
@@ -1611,111 +1627,28 @@ function assertPersistedKeyPolicy(value) {
|
|
|
1611
1627
|
throw new Error(`Persisted key policy violation (expected snake_case keys): ${violations.join(", ")}`);
|
|
1612
1628
|
}
|
|
1613
1629
|
//#endregion
|
|
1614
|
-
//#region src/session/persistence/
|
|
1615
|
-
const
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
file: record.file,
|
|
1627
|
-
acpxRecordId: record.acpxRecordId,
|
|
1628
|
-
acpSessionId: record.acpSessionId,
|
|
1629
|
-
agentCommand: record.agentCommand,
|
|
1630
|
-
cwd: record.cwd,
|
|
1631
|
-
name: record.name,
|
|
1632
|
-
closed: record.closed,
|
|
1633
|
-
lastUsedAt: record.lastUsedAt
|
|
1634
|
-
};
|
|
1635
|
-
}
|
|
1636
|
-
function hasRequiredIndexEntryFields(record) {
|
|
1637
|
-
return [
|
|
1638
|
-
"file",
|
|
1639
|
-
"acpxRecordId",
|
|
1640
|
-
"acpSessionId",
|
|
1641
|
-
"agentCommand",
|
|
1642
|
-
"cwd",
|
|
1643
|
-
"lastUsedAt"
|
|
1644
|
-
].every((key) => typeof record[key] === "string") && typeof record.closed === "boolean";
|
|
1645
|
-
}
|
|
1646
|
-
function sessionIndexPath(sessionDir) {
|
|
1647
|
-
return path.join(sessionDir, "index.json");
|
|
1648
|
-
}
|
|
1649
|
-
function toSessionIndexEntry(record, fileName) {
|
|
1650
|
-
return {
|
|
1651
|
-
file: fileName,
|
|
1652
|
-
acpxRecordId: record.acpxRecordId,
|
|
1653
|
-
acpSessionId: record.acpSessionId,
|
|
1654
|
-
agentCommand: record.agentCommand,
|
|
1655
|
-
cwd: record.cwd,
|
|
1656
|
-
name: record.name,
|
|
1657
|
-
closed: record.closed === true,
|
|
1658
|
-
lastUsedAt: record.lastUsedAt
|
|
1659
|
-
};
|
|
1630
|
+
//#region src/session/persistence/discovery.ts
|
|
1631
|
+
const DISCOVERY_BATCH_SIZE = 4;
|
|
1632
|
+
/** Select and return the same canonical snapshot; cached metadata cannot exclude records. */
|
|
1633
|
+
async function* scanSessionRecords(sessionDir) {
|
|
1634
|
+
const files = (await fs$1.readdir(sessionDir, { withFileTypes: true }).catch((error) => {
|
|
1635
|
+
if (error.code === "ENOENT") return [];
|
|
1636
|
+
throw error;
|
|
1637
|
+
})).filter((entry) => entry.isFile() && entry.name.endsWith(".json") && entry.name !== "index.json").map((entry) => entry.name).toSorted();
|
|
1638
|
+
for (let offset = 0; offset < files.length; offset += DISCOVERY_BATCH_SIZE) {
|
|
1639
|
+
const records = await Promise.all(files.slice(offset, offset + DISCOVERY_BATCH_SIZE).map((file) => readRecord(path.join(sessionDir, file))));
|
|
1640
|
+
for (const record of records) if (record) yield record;
|
|
1641
|
+
}
|
|
1660
1642
|
}
|
|
1661
|
-
async function
|
|
1662
|
-
const filePath = sessionIndexPath(sessionDir);
|
|
1643
|
+
async function readRecord(file) {
|
|
1663
1644
|
try {
|
|
1664
|
-
const payload = await fs$1.readFile(
|
|
1665
|
-
const record =
|
|
1666
|
-
|
|
1667
|
-
const files = record.files.filter((entry) => typeof entry === "string");
|
|
1668
|
-
if (files.length !== record.files.length || !Array.isArray(record.entries)) return;
|
|
1669
|
-
const entries = record.entries.map((entry) => parseIndexEntry(entry)).filter((entry) => Boolean(entry));
|
|
1670
|
-
if (entries.length !== record.entries.length) return;
|
|
1671
|
-
return {
|
|
1672
|
-
schema: SESSION_INDEX_SCHEMA,
|
|
1673
|
-
files,
|
|
1674
|
-
entries
|
|
1675
|
-
};
|
|
1645
|
+
const payload = await fs$1.readFile(file, "utf8");
|
|
1646
|
+
const record = parseSessionRecord(JSON.parse(payload));
|
|
1647
|
+
return record && path.basename(file) === `${safeSessionId(record.acpxRecordId)}.json` ? record : void 0;
|
|
1676
1648
|
} catch {
|
|
1677
1649
|
return;
|
|
1678
1650
|
}
|
|
1679
1651
|
}
|
|
1680
|
-
async function writeSessionIndex(sessionDir, index) {
|
|
1681
|
-
await writePrivateJsonFile(sessionIndexPath(sessionDir), {
|
|
1682
|
-
schema: SESSION_INDEX_SCHEMA,
|
|
1683
|
-
files: index.files.toSorted(),
|
|
1684
|
-
entries: index.entries.toSorted((a, b) => b.lastUsedAt.localeCompare(a.lastUsedAt))
|
|
1685
|
-
});
|
|
1686
|
-
}
|
|
1687
|
-
async function rebuildSessionIndex(sessionDir) {
|
|
1688
|
-
const index = await scanSessionIndex(sessionDir);
|
|
1689
|
-
await writeSessionIndex(sessionDir, index);
|
|
1690
|
-
return index;
|
|
1691
|
-
}
|
|
1692
|
-
async function scanSessionIndex(sessionDir) {
|
|
1693
|
-
const files = await listSessionFiles(sessionDir).catch((error) => {
|
|
1694
|
-
if (error.code === "ENOENT") return [];
|
|
1695
|
-
throw error;
|
|
1696
|
-
});
|
|
1697
|
-
const indexEntries = [];
|
|
1698
|
-
for (const file of files) try {
|
|
1699
|
-
const payload = await fs$1.readFile(path.join(sessionDir, file), "utf8");
|
|
1700
|
-
const parsed = parseSessionRecord(JSON.parse(payload));
|
|
1701
|
-
if (!parsed) continue;
|
|
1702
|
-
indexEntries.push(toSessionIndexEntry(parsed, file));
|
|
1703
|
-
} catch {}
|
|
1704
|
-
return {
|
|
1705
|
-
schema: SESSION_INDEX_SCHEMA,
|
|
1706
|
-
files,
|
|
1707
|
-
entries: indexEntries.toSorted((a, b) => b.lastUsedAt.localeCompare(a.lastUsedAt))
|
|
1708
|
-
};
|
|
1709
|
-
}
|
|
1710
|
-
async function loadOrRebuildSessionIndex(sessionDir) {
|
|
1711
|
-
const files = await listSessionFiles(sessionDir);
|
|
1712
|
-
const existing = await readSessionIndex(sessionDir);
|
|
1713
|
-
if (existing && existing.files.length === files.length && existing.files.every((file, index) => file === files[index])) return existing;
|
|
1714
|
-
return await rebuildSessionIndex(sessionDir);
|
|
1715
|
-
}
|
|
1716
|
-
async function listSessionFiles(sessionDir) {
|
|
1717
|
-
return (await fs$1.readdir(sessionDir, { withFileTypes: true })).filter((entry) => entry.isFile() && entry.name.endsWith(".json") && entry.name !== "index.json").map((entry) => entry.name).toSorted();
|
|
1718
|
-
}
|
|
1719
1652
|
//#endregion
|
|
1720
1653
|
//#region src/session/persistence/repository.ts
|
|
1721
1654
|
const DEFAULT_HISTORY_LIMIT = 20;
|
|
@@ -1729,62 +1662,56 @@ async function ensureSessionDir() {
|
|
|
1729
1662
|
mode: 448
|
|
1730
1663
|
});
|
|
1731
1664
|
}
|
|
1732
|
-
async function
|
|
1733
|
-
try {
|
|
1734
|
-
const payload = await fs$1.readFile(path.join(sessionBaseDir(), entry.file), "utf8");
|
|
1735
|
-
return parseSessionRecord(JSON.parse(payload)) ?? void 0;
|
|
1736
|
-
} catch {
|
|
1737
|
-
return;
|
|
1738
|
-
}
|
|
1739
|
-
}
|
|
1740
|
-
async function loadSessionIndexEntries(readOnly = false) {
|
|
1665
|
+
async function* sessionRecords(readOnly = false) {
|
|
1741
1666
|
if (!readOnly) await ensureSessionDir();
|
|
1742
|
-
|
|
1743
|
-
return await (readOnly ? scanSessionIndex : loadOrRebuildSessionIndex)(sessionBaseDir());
|
|
1744
|
-
})).entries;
|
|
1667
|
+
yield* scanSessionRecords(sessionBaseDir());
|
|
1745
1668
|
}
|
|
1746
|
-
function
|
|
1747
|
-
if (session.
|
|
1669
|
+
function matchesSession(session, agentCommand, normalizedName, includeClosed = false) {
|
|
1670
|
+
if (session.agentCommand !== agentCommand) return false;
|
|
1748
1671
|
if (!includeClosed && session.closed) return false;
|
|
1749
1672
|
if (normalizedName == null) return session.name == null;
|
|
1750
1673
|
return session.name === normalizedName;
|
|
1751
1674
|
}
|
|
1675
|
+
function isNewer(record, previous) {
|
|
1676
|
+
return !previous || record.lastUsedAt.localeCompare(previous.lastUsedAt) > 0;
|
|
1677
|
+
}
|
|
1752
1678
|
async function writeSessionRecord(record) {
|
|
1753
1679
|
await measurePerf("session.write_record", async () => {
|
|
1754
1680
|
const persisted = serializeSessionRecordForDisk(record);
|
|
1755
1681
|
assertPersistedKeyPolicy(persisted);
|
|
1756
|
-
|
|
1757
|
-
await writePrivateJsonFile(file, persisted);
|
|
1758
|
-
const sessionDir = sessionBaseDir();
|
|
1759
|
-
const index = await loadOrRebuildSessionIndex(sessionDir);
|
|
1760
|
-
const fileName = path.basename(file);
|
|
1761
|
-
const entries = index.entries.filter((entry) => entry.file !== fileName);
|
|
1762
|
-
entries.push(toSessionIndexEntry(record, fileName));
|
|
1763
|
-
await writeSessionIndex(sessionDir, {
|
|
1764
|
-
files: [.../* @__PURE__ */ new Set([...index.files.filter((entry) => entry !== fileName), fileName])],
|
|
1765
|
-
entries
|
|
1766
|
-
});
|
|
1682
|
+
await writePrivateJsonFile(sessionFilePath(record.acpxRecordId), persisted);
|
|
1767
1683
|
});
|
|
1768
1684
|
}
|
|
1769
1685
|
async function resolveSessionRecord(sessionId) {
|
|
1770
1686
|
await ensureSessionDir();
|
|
1771
1687
|
try {
|
|
1772
1688
|
const directRecord = await measurePerf("session.resolve_direct", async () => await readSessionRecord(sessionId));
|
|
1773
|
-
if (directRecord) return directRecord;
|
|
1689
|
+
if (directRecord?.acpxRecordId === sessionId) return directRecord;
|
|
1774
1690
|
} catch {}
|
|
1775
|
-
const
|
|
1776
|
-
const exactEntries = entries.filter((entry) => entry.acpxRecordId === sessionId || entry.acpSessionId === sessionId);
|
|
1777
|
-
const exactRecords = (await Promise.all(exactEntries.map((entry) => loadRecordFromIndexEntry(entry)))).filter((entry) => Boolean(entry));
|
|
1691
|
+
const { exactRecords, suffixRecords } = await findSessionIdMatches(sessionId);
|
|
1778
1692
|
if (exactRecords.length === 1) return exactRecords[0];
|
|
1779
1693
|
if (exactRecords.length > 1) throw new SessionResolutionError(`Multiple sessions match id: ${sessionId}`);
|
|
1780
|
-
const suffixEntries = entries.filter((entry) => entry.acpxRecordId.endsWith(sessionId) || entry.acpSessionId.endsWith(sessionId));
|
|
1781
|
-
const suffixRecords = (await Promise.all(suffixEntries.map((entry) => loadRecordFromIndexEntry(entry)))).filter((entry) => Boolean(entry));
|
|
1782
1694
|
if (suffixRecords.length === 1) return suffixRecords[0];
|
|
1783
1695
|
if (suffixRecords.length > 1) throw new SessionResolutionError(`Session id is ambiguous: ${sessionId}`);
|
|
1784
1696
|
incrementPerfCounter("session.resolve_miss");
|
|
1785
1697
|
throw new SessionNotFoundError(sessionId);
|
|
1786
1698
|
}
|
|
1787
|
-
|
|
1699
|
+
async function findSessionIdMatches(sessionId) {
|
|
1700
|
+
const exactRecords = [];
|
|
1701
|
+
const suffixRecords = [];
|
|
1702
|
+
for await (const record of sessionRecords()) {
|
|
1703
|
+
if (record.acpxRecordId === sessionId || record.acpSessionId === sessionId) retainMatch(exactRecords, record);
|
|
1704
|
+
if (record.acpxRecordId.endsWith(sessionId) || record.acpSessionId.endsWith(sessionId)) retainMatch(suffixRecords, record);
|
|
1705
|
+
}
|
|
1706
|
+
return {
|
|
1707
|
+
exactRecords,
|
|
1708
|
+
suffixRecords
|
|
1709
|
+
};
|
|
1710
|
+
}
|
|
1711
|
+
function retainMatch(matches, record) {
|
|
1712
|
+
if (matches.length < 2) matches.push(record);
|
|
1713
|
+
}
|
|
1714
|
+
/** Reads one canonical record without creating directories or scanning other sessions. */
|
|
1788
1715
|
async function readSessionRecord(sessionId) {
|
|
1789
1716
|
let payload;
|
|
1790
1717
|
try {
|
|
@@ -1794,22 +1721,24 @@ async function readSessionRecord(sessionId) {
|
|
|
1794
1721
|
throw error;
|
|
1795
1722
|
}
|
|
1796
1723
|
try {
|
|
1797
|
-
|
|
1724
|
+
const record = parseSessionRecord(JSON.parse(payload));
|
|
1725
|
+
return record?.acpxRecordId === sessionId ? record : void 0;
|
|
1798
1726
|
} catch {
|
|
1799
1727
|
return;
|
|
1800
1728
|
}
|
|
1801
1729
|
}
|
|
1802
|
-
function
|
|
1730
|
+
function hasGitMarker(dir) {
|
|
1803
1731
|
const gitPath = path.join(dir, ".git");
|
|
1804
1732
|
try {
|
|
1805
|
-
|
|
1733
|
+
const marker = statSync(gitPath);
|
|
1734
|
+
return marker.isDirectory() || marker.isFile();
|
|
1806
1735
|
} catch {
|
|
1807
1736
|
return false;
|
|
1808
1737
|
}
|
|
1809
1738
|
}
|
|
1810
1739
|
function isWithinBoundary(boundary, target) {
|
|
1811
1740
|
const relative = path.relative(boundary, target);
|
|
1812
|
-
return relative
|
|
1741
|
+
return relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
1813
1742
|
}
|
|
1814
1743
|
function absolutePath(value) {
|
|
1815
1744
|
return path.resolve(value);
|
|
@@ -1818,7 +1747,7 @@ function findGitRepositoryRoot(startDir) {
|
|
|
1818
1747
|
let current = absolutePath(startDir);
|
|
1819
1748
|
const root = path.parse(current).root;
|
|
1820
1749
|
for (;;) {
|
|
1821
|
-
if (
|
|
1750
|
+
if (hasGitMarker(current)) return current;
|
|
1822
1751
|
if (current === root) return;
|
|
1823
1752
|
const parent = path.dirname(current);
|
|
1824
1753
|
if (parent === current) return;
|
|
@@ -1834,42 +1763,49 @@ function isoNow() {
|
|
|
1834
1763
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
1835
1764
|
}
|
|
1836
1765
|
async function listSessions() {
|
|
1837
|
-
await
|
|
1838
|
-
const entries = await loadSessionIndexEntries();
|
|
1839
|
-
const records = [];
|
|
1840
|
-
for (const entry of entries) {
|
|
1841
|
-
const parsed = await loadRecordFromIndexEntry(entry);
|
|
1842
|
-
if (parsed) records.push(parsed);
|
|
1843
|
-
}
|
|
1844
|
-
records.sort((a, b) => b.lastUsedAt.localeCompare(a.lastUsedAt));
|
|
1845
|
-
return records;
|
|
1766
|
+
return await collectSessionRecords();
|
|
1846
1767
|
}
|
|
1847
1768
|
async function listSessionsForAgent(agentCommand) {
|
|
1848
|
-
|
|
1849
|
-
|
|
1769
|
+
return await collectSessionRecords(agentCommand);
|
|
1770
|
+
}
|
|
1771
|
+
async function collectSessionRecords(agentCommand) {
|
|
1772
|
+
const records = [];
|
|
1773
|
+
for await (const record of sessionRecords()) if (agentCommand === void 0 || record.agentCommand === agentCommand) records.push(record);
|
|
1774
|
+
return records.toSorted((a, b) => b.lastUsedAt.localeCompare(a.lastUsedAt));
|
|
1850
1775
|
}
|
|
1851
1776
|
async function findSession(options) {
|
|
1852
1777
|
const normalizedCwd = absolutePath(options.cwd);
|
|
1853
1778
|
const normalizedName = normalizeName(options.name);
|
|
1854
|
-
|
|
1855
|
-
if (
|
|
1856
|
-
return
|
|
1779
|
+
let match;
|
|
1780
|
+
for await (const record of sessionRecords(options.readOnly)) if (record.cwd === normalizedCwd && matchesSession(record, options.agentCommand, normalizedName, options.includeClosed) && isNewer(record, match)) match = record;
|
|
1781
|
+
return match;
|
|
1857
1782
|
}
|
|
1858
1783
|
async function findSessionByDirectoryWalk(options) {
|
|
1859
1784
|
const normalizedName = normalizeName(options.name);
|
|
1785
|
+
const directories = walkDirectories(options);
|
|
1786
|
+
let match;
|
|
1787
|
+
let distance = Infinity;
|
|
1788
|
+
for await (const record of sessionRecords()) {
|
|
1789
|
+
const candidateDistance = directories.get(record.cwd);
|
|
1790
|
+
if (candidateDistance !== void 0 && matchesSession(record, options.agentCommand, normalizedName) && (candidateDistance < distance || candidateDistance === distance && isNewer(record, match))) {
|
|
1791
|
+
match = record;
|
|
1792
|
+
distance = candidateDistance;
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
return match;
|
|
1796
|
+
}
|
|
1797
|
+
function walkDirectories(options) {
|
|
1860
1798
|
const normalizedStart = absolutePath(options.cwd);
|
|
1861
1799
|
const normalizedBoundary = absolutePath(options.boundary ?? normalizedStart);
|
|
1862
1800
|
const walkBoundary = isWithinBoundary(normalizedBoundary, normalizedStart) ? normalizedBoundary : normalizedStart;
|
|
1863
|
-
const
|
|
1801
|
+
const directories = /* @__PURE__ */ new Map();
|
|
1802
|
+
const walkRoot = path.parse(normalizedStart).root;
|
|
1864
1803
|
let current = normalizedStart;
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
if (match) return await loadRecordFromIndexEntry(match);
|
|
1869
|
-
const parent = nextWalkParent(current, walkBoundary, walkRoot);
|
|
1870
|
-
if (!parent) return;
|
|
1871
|
-
current = parent;
|
|
1804
|
+
while (current) {
|
|
1805
|
+
directories.set(current, directories.size);
|
|
1806
|
+
current = nextWalkParent(current, walkBoundary, walkRoot);
|
|
1872
1807
|
}
|
|
1808
|
+
return directories;
|
|
1873
1809
|
}
|
|
1874
1810
|
function nextWalkParent(current, walkBoundary, walkRoot) {
|
|
1875
1811
|
if (current === walkBoundary || current === walkRoot) return;
|
|
@@ -1885,8 +1821,7 @@ function isSessionStreamFile(fileName, safeId) {
|
|
|
1885
1821
|
return fileName === `${safeId}.stream.ndjson` || fileName === `${safeId}.stream.lock` || fileName.startsWith(segmentPrefix) && /^\d+\.ndjson$/.test(fileName.slice(segmentPrefix.length));
|
|
1886
1822
|
}
|
|
1887
1823
|
async function pruneSessions(options = {}) {
|
|
1888
|
-
await
|
|
1889
|
-
const records = await loadPrunableRecords((await loadSessionIndexEntries()).filter((entry) => isPruneCandidate(entry, options.agentCommand)), options.before ?? (options.olderThanMs != null ? new Date(Date.now() - options.olderThanMs) : void 0), options.agentCommand);
|
|
1824
|
+
const records = await loadPrunableRecords(options.before ?? (options.olderThanMs != null ? new Date(Date.now() - options.olderThanMs) : void 0), options.agentCommand);
|
|
1890
1825
|
if (options.dryRun) return {
|
|
1891
1826
|
pruned: records,
|
|
1892
1827
|
bytesFreed: 0,
|
|
@@ -1899,7 +1834,6 @@ async function pruneSessions(options = {}) {
|
|
|
1899
1834
|
dirEntries = await fs$1.readdir(sessionDir);
|
|
1900
1835
|
} catch {}
|
|
1901
1836
|
for (const record of records) bytesFreed += await pruneSessionFiles(record, sessionDir, dirEntries, options.includeHistory === true);
|
|
1902
|
-
await rebuildSessionIndex(sessionDir).catch(() => {});
|
|
1903
1837
|
return {
|
|
1904
1838
|
pruned: records,
|
|
1905
1839
|
bytesFreed,
|
|
@@ -1909,14 +1843,11 @@ async function pruneSessions(options = {}) {
|
|
|
1909
1843
|
function isPruneCandidate(record, agentCommand) {
|
|
1910
1844
|
return record.closed === true && (!agentCommand || record.agentCommand === agentCommand);
|
|
1911
1845
|
}
|
|
1912
|
-
async function loadPrunableRecords(
|
|
1846
|
+
async function loadPrunableRecords(cutoff, agentCommand) {
|
|
1913
1847
|
const records = [];
|
|
1914
1848
|
const cutoffIso = cutoff?.toISOString();
|
|
1915
|
-
for (const
|
|
1916
|
-
|
|
1917
|
-
if (record && isPruneCandidate(record, agentCommand) && isBeforeCutoff(record, cutoffIso)) records.push(record);
|
|
1918
|
-
}
|
|
1919
|
-
return records;
|
|
1849
|
+
for await (const record of sessionRecords()) if (isPruneCandidate(record, agentCommand) && isBeforeCutoff(record, cutoffIso)) records.push(record);
|
|
1850
|
+
return records.toSorted((a, b) => b.lastUsedAt.localeCompare(a.lastUsedAt));
|
|
1920
1851
|
}
|
|
1921
1852
|
function isBeforeCutoff(record, cutoffIso) {
|
|
1922
1853
|
return !cutoffIso || closedAtOrLastUsedAt(record) < cutoffIso;
|
|
@@ -1974,12 +1905,12 @@ function isAudioBlock(value) {
|
|
|
1974
1905
|
}
|
|
1975
1906
|
function isResourceLinkBlock(value) {
|
|
1976
1907
|
const record = asRecord$1(value);
|
|
1977
|
-
return record?.type === "resource_link" && isNonEmptyString(record.uri) && (record.title
|
|
1908
|
+
return record?.type === "resource_link" && isNonEmptyString(record.uri) && (record.title == null || typeof record.title === "string") && typeof record.name === "string";
|
|
1978
1909
|
}
|
|
1979
1910
|
function isResourcePayload(value) {
|
|
1980
1911
|
const record = asRecord$1(value);
|
|
1981
1912
|
if (!record || !isNonEmptyString(record.uri)) return false;
|
|
1982
|
-
return record.text ===
|
|
1913
|
+
return typeof record.text === "string" || typeof record.blob === "string";
|
|
1983
1914
|
}
|
|
1984
1915
|
function isResourceBlock(value) {
|
|
1985
1916
|
const record = asRecord$1(value);
|
|
@@ -2022,12 +1953,12 @@ function validateAudioContentBlock(record, index) {
|
|
|
2022
1953
|
}
|
|
2023
1954
|
function validateResourceLinkContentBlock(record, index) {
|
|
2024
1955
|
if (!isNonEmptyString(record.uri)) return `prompt[${index}] resource_link block must include a non-empty uri`;
|
|
2025
|
-
if (record.title
|
|
2026
|
-
if (
|
|
1956
|
+
if (record.title != null && typeof record.title !== "string") return `prompt[${index}] resource_link block title must be a string or null when present`;
|
|
1957
|
+
if (typeof record.name !== "string") return `prompt[${index}] resource_link block must include a string name`;
|
|
2027
1958
|
}
|
|
2028
1959
|
function validateResourceContentBlock(record, index) {
|
|
2029
1960
|
if (!asRecord$1(record.resource)) return `prompt[${index}] resource block must include a resource object`;
|
|
2030
|
-
return isResourcePayload(record.resource) ? void 0 : `prompt[${index}] resource block resource must include a non-empty uri and
|
|
1961
|
+
return isResourcePayload(record.resource) ? void 0 : `prompt[${index}] resource block resource must include a non-empty uri and a string text or blob field`;
|
|
2031
1962
|
}
|
|
2032
1963
|
function getContentBlockValidationError(value, index) {
|
|
2033
1964
|
const record = asRecord$1(value);
|
|
@@ -2112,6 +2043,574 @@ function resourceBlockDisplayText(block) {
|
|
|
2112
2043
|
return "text" in block.resource && typeof block.resource.text === "string" ? block.resource.text : block.resource.uri;
|
|
2113
2044
|
}
|
|
2114
2045
|
//#endregion
|
|
2046
|
+
//#region src/process-identity.ts
|
|
2047
|
+
const IDENTITY_QUERY_TIMEOUT_MS$1 = 2e3;
|
|
2048
|
+
const LINUX_PROCESS_QUERY = `
|
|
2049
|
+
const fs = require('node:fs/promises');
|
|
2050
|
+
const [targetPid, observerPid] = process.argv.slice(1);
|
|
2051
|
+
if (process.ppid !== Number(observerPid)) process.exit(1);
|
|
2052
|
+
const timeNamespace = (pid) => fs.readlink('/proc/' + pid + '/ns/time').catch((error) => {
|
|
2053
|
+
if (error.code === 'ENOENT') return 'unsupported';
|
|
2054
|
+
throw error;
|
|
2055
|
+
});
|
|
2056
|
+
const prefix = async (name, size) => {
|
|
2057
|
+
const file = await fs.open(name, 'r');
|
|
2058
|
+
try {
|
|
2059
|
+
const buffer = Buffer.alloc(size);
|
|
2060
|
+
const { bytesRead } = await file.read(buffer, 0, size, 0);
|
|
2061
|
+
return buffer.subarray(0, bytesRead).toString('base64');
|
|
2062
|
+
} finally { await file.close(); }
|
|
2063
|
+
};
|
|
2064
|
+
const targets = async () => {
|
|
2065
|
+
if (targetPid !== 'all') {
|
|
2066
|
+
return { targetStat: await fs.readFile('/proc/' + targetPid + '/stat', 'utf8').catch(() => null) };
|
|
2067
|
+
}
|
|
2068
|
+
const required = new Set(JSON.parse(process.argv[3] || '[]'));
|
|
2069
|
+
const pids = new Set((await fs.readdir('/proc')).filter(pid => /^[1-9][0-9]*$/.test(pid)));
|
|
2070
|
+
for (const pid of required) pids.add(String(pid));
|
|
2071
|
+
const processStats = [];
|
|
2072
|
+
let bytes = 0;
|
|
2073
|
+
// Sequential reads keep proc I/O and retained memory bounded within this
|
|
2074
|
+
// single killable helper; never launch one helper or pending read per PID.
|
|
2075
|
+
for (const pid of [...pids].sort((a, b) => Number(a) - Number(b))) {
|
|
2076
|
+
let stat;
|
|
2077
|
+
try { stat = await fs.readFile('/proc/' + pid + '/stat', 'utf8'); }
|
|
2078
|
+
catch (error) {
|
|
2079
|
+
if (error.code === 'ENOENT' || error.code === 'ESRCH') continue;
|
|
2080
|
+
if (!required.has(Number(pid)) && (error.code === 'EACCES' || error.code === 'EPERM')) continue;
|
|
2081
|
+
throw error;
|
|
2082
|
+
}
|
|
2083
|
+
bytes += Buffer.byteLength(JSON.stringify(stat)) + 1;
|
|
2084
|
+
if (bytes > ${PROCESS_HELPER_MAX_BUFFER_BYTES} - 8192) throw new Error('Process table exceeds its byte limit');
|
|
2085
|
+
processStats.push(stat);
|
|
2086
|
+
}
|
|
2087
|
+
return { processStats, auxv: await prefix('/proc/self/auxv', 4096), elf: await prefix('/proc/self/exe', 6) };
|
|
2088
|
+
};
|
|
2089
|
+
Promise.all([
|
|
2090
|
+
fs.readFile('/proc/sys/kernel/random/boot_id', 'utf8'),
|
|
2091
|
+
fs.readlink('/proc/self/ns/pid'),
|
|
2092
|
+
timeNamespace('self'),
|
|
2093
|
+
fs.readlink('/proc/' + observerPid + '/ns/pid'),
|
|
2094
|
+
timeNamespace(observerPid),
|
|
2095
|
+
fs.readFile('/proc/self/stat', 'utf8'),
|
|
2096
|
+
targets(),
|
|
2097
|
+
]).then(([bootId, pidNamespace, timeNamespace, observerPidNamespace,
|
|
2098
|
+
observerTimeNamespace, selfStat, target]) => {
|
|
2099
|
+
process.stdout.write(JSON.stringify({bootId: bootId.trim(), pidNamespace,
|
|
2100
|
+
timeNamespace, observerPidNamespace, observerTimeNamespace,
|
|
2101
|
+
helperPid: process.pid, selfStat, ...target}));
|
|
2102
|
+
}).catch(() => { process.exitCode = 1; });
|
|
2103
|
+
`;
|
|
2104
|
+
const POSIX_MONTHS = [
|
|
2105
|
+
"Jan",
|
|
2106
|
+
"Feb",
|
|
2107
|
+
"Mar",
|
|
2108
|
+
"Apr",
|
|
2109
|
+
"May",
|
|
2110
|
+
"Jun",
|
|
2111
|
+
"Jul",
|
|
2112
|
+
"Aug",
|
|
2113
|
+
"Sep",
|
|
2114
|
+
"Oct",
|
|
2115
|
+
"Nov",
|
|
2116
|
+
"Dec"
|
|
2117
|
+
];
|
|
2118
|
+
let ownIdentity;
|
|
2119
|
+
let ownIdentityPending;
|
|
2120
|
+
function parseProcessBirthIdentity(raw) {
|
|
2121
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return;
|
|
2122
|
+
const identity = raw;
|
|
2123
|
+
if (identity.kind === "linux-proc") return parseLinuxIdentity(identity);
|
|
2124
|
+
if (identity.kind !== "posix-lstart" && identity.kind !== "windows-creation") return;
|
|
2125
|
+
if (!isCanonicalBirth(identity.value, identity.kind)) return;
|
|
2126
|
+
return {
|
|
2127
|
+
kind: identity.kind,
|
|
2128
|
+
value: identity.value
|
|
2129
|
+
};
|
|
2130
|
+
}
|
|
2131
|
+
function isCanonicalBirth(value, kind) {
|
|
2132
|
+
if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3,7}Z$/.test(value) || !Number.isFinite(Date.parse(value))) return false;
|
|
2133
|
+
const canonical = new Date(value).toISOString();
|
|
2134
|
+
return canonical.slice(0, 19) === value.slice(0, 19) && (kind === "posix-lstart" ? value === canonical && value.endsWith(".000Z") : /\.\d{7}Z$/.test(value));
|
|
2135
|
+
}
|
|
2136
|
+
function parseLinuxScope(raw) {
|
|
2137
|
+
const { bootId, pidNamespace, timeNamespace } = raw;
|
|
2138
|
+
if (typeof bootId !== "string" || !/^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/.test(bootId) || typeof pidNamespace !== "string" || !/^pid:\[[1-9]\d*\]$/.test(pidNamespace) || !isTimeNamespace(timeNamespace)) return;
|
|
2139
|
+
return {
|
|
2140
|
+
bootId,
|
|
2141
|
+
pidNamespace,
|
|
2142
|
+
timeNamespace
|
|
2143
|
+
};
|
|
2144
|
+
}
|
|
2145
|
+
function isTimeNamespace(value) {
|
|
2146
|
+
return typeof value === "string" && (value === "unsupported" || /^time:\[[1-9]\d*\]$/.test(value));
|
|
2147
|
+
}
|
|
2148
|
+
function isStartTicks(value) {
|
|
2149
|
+
return typeof value === "string" && /^(?:0|[1-9]\d*)$/.test(value);
|
|
2150
|
+
}
|
|
2151
|
+
function parseLinuxIdentity(raw) {
|
|
2152
|
+
const scope = parseLinuxScope(raw);
|
|
2153
|
+
if (!scope || !isStartTicks(raw.startTicks)) return;
|
|
2154
|
+
return {
|
|
2155
|
+
kind: "linux-proc",
|
|
2156
|
+
...scope,
|
|
2157
|
+
startTicks: raw.startTicks
|
|
2158
|
+
};
|
|
2159
|
+
}
|
|
2160
|
+
function compareLinuxScope(expected, observed) {
|
|
2161
|
+
if (expected.bootId !== observed.bootId) return "different";
|
|
2162
|
+
if (expected.pidNamespace !== observed.pidNamespace || expected.timeNamespace !== observed.timeNamespace) return "unknown";
|
|
2163
|
+
return "matching";
|
|
2164
|
+
}
|
|
2165
|
+
function compareLinuxIdentity(expected, observed) {
|
|
2166
|
+
const scope = compareLinuxScope(expected, observed);
|
|
2167
|
+
return scope === "matching" ? expected.startTicks === observed.startTicks ? "matching" : "different" : scope;
|
|
2168
|
+
}
|
|
2169
|
+
function compareProcessBirthIdentity(expected, observed) {
|
|
2170
|
+
if (!expected || expected.kind !== observed.kind) return "unknown";
|
|
2171
|
+
if (expected.kind === "linux-proc" && observed.kind === "linux-proc") return compareLinuxIdentity(expected, observed);
|
|
2172
|
+
if ("value" in expected && "value" in observed) return expected.value === observed.value ? "matching" : "different";
|
|
2173
|
+
return "unknown";
|
|
2174
|
+
}
|
|
2175
|
+
function parseLinuxStat(raw) {
|
|
2176
|
+
if (typeof raw !== "string") return;
|
|
2177
|
+
const prefix = /^(\d+) \(/.exec(raw);
|
|
2178
|
+
const close = raw.lastIndexOf(")");
|
|
2179
|
+
if (!prefix || close < 0) return;
|
|
2180
|
+
const pid = Number(prefix[1]);
|
|
2181
|
+
const fields = raw.slice(close + 2).trim().split(/\s+/u);
|
|
2182
|
+
const startTicks = fields[19];
|
|
2183
|
+
if (!isPositivePid(pid) || !isStartTicks(startTicks) || !/^[RSDTtKWPI]$/.test(fields[0])) return;
|
|
2184
|
+
const parentPid = Number(fields[1]);
|
|
2185
|
+
const groupPid = Number(fields[2]);
|
|
2186
|
+
return validProcessLinks(parentPid, groupPid) ? {
|
|
2187
|
+
pid,
|
|
2188
|
+
parentPid,
|
|
2189
|
+
groupPid,
|
|
2190
|
+
startTicks
|
|
2191
|
+
} : void 0;
|
|
2192
|
+
}
|
|
2193
|
+
function validProcessLinks(parentPid, groupPid) {
|
|
2194
|
+
return Number.isSafeInteger(parentPid) && parentPid >= 0 && Number.isSafeInteger(groupPid) && groupPid >= 0;
|
|
2195
|
+
}
|
|
2196
|
+
async function readLinuxQuery(pid, timeoutMs, requiredPids = []) {
|
|
2197
|
+
const args = [
|
|
2198
|
+
"--input-type=commonjs",
|
|
2199
|
+
"-e",
|
|
2200
|
+
LINUX_PROCESS_QUERY,
|
|
2201
|
+
String(pid),
|
|
2202
|
+
String(process.pid)
|
|
2203
|
+
];
|
|
2204
|
+
if (pid === "all") args.push(JSON.stringify(requiredPids));
|
|
2205
|
+
const output = await runTimedExecFile(process.execPath, args, {
|
|
2206
|
+
timeoutMs,
|
|
2207
|
+
maxBufferBytes: pid === "all" ? PROCESS_HELPER_MAX_BUFFER_BYTES : 8192,
|
|
2208
|
+
env: {
|
|
2209
|
+
...process.env,
|
|
2210
|
+
NODE_OPTIONS: ""
|
|
2211
|
+
}
|
|
2212
|
+
});
|
|
2213
|
+
return JSON.parse(output);
|
|
2214
|
+
}
|
|
2215
|
+
function verifiedLinuxScope(raw) {
|
|
2216
|
+
const scope = parseLinuxScope(raw);
|
|
2217
|
+
const self = parseLinuxStat(raw.selfStat);
|
|
2218
|
+
if (!scope || !self || self.pid !== raw.helperPid || scope.pidNamespace !== raw.observerPidNamespace || scope.timeNamespace !== raw.observerTimeNamespace) return;
|
|
2219
|
+
return scope;
|
|
2220
|
+
}
|
|
2221
|
+
async function readLinuxObservation(pid, timeoutMs) {
|
|
2222
|
+
const raw = await readLinuxQuery(pid, timeoutMs);
|
|
2223
|
+
const scope = verifiedLinuxScope(raw);
|
|
2224
|
+
if (!scope) return;
|
|
2225
|
+
const target = parseLinuxStat(raw.targetStat);
|
|
2226
|
+
return {
|
|
2227
|
+
scope,
|
|
2228
|
+
...target?.pid === pid ? { identity: {
|
|
2229
|
+
kind: "linux-proc",
|
|
2230
|
+
...scope,
|
|
2231
|
+
startTicks: target.startTicks
|
|
2232
|
+
} } : {}
|
|
2233
|
+
};
|
|
2234
|
+
}
|
|
2235
|
+
function linuxAuxv(raw) {
|
|
2236
|
+
if (typeof raw.elf !== "string" || typeof raw.auxv !== "string") throw new Error("Linux process clock is unavailable");
|
|
2237
|
+
const elf = Buffer.from(raw.elf, "base64");
|
|
2238
|
+
const auxv = Buffer.from(raw.auxv, "base64");
|
|
2239
|
+
if (elf.subarray(0, 4).toString("hex") !== "7f454c46" || ![1, 2].includes(elf[4]) || ![1, 2].includes(elf[5])) throw new Error("Linux process clock has an invalid ELF format");
|
|
2240
|
+
return {
|
|
2241
|
+
auxv,
|
|
2242
|
+
size: elf[4] === 1 ? 4 : 8,
|
|
2243
|
+
littleEndian: elf[5] === 1
|
|
2244
|
+
};
|
|
2245
|
+
}
|
|
2246
|
+
function linuxClockTicks(raw) {
|
|
2247
|
+
const { auxv, size, littleEndian } = linuxAuxv(raw);
|
|
2248
|
+
const word = (offset) => readAuxvWord(auxv, offset, size, littleEndian);
|
|
2249
|
+
for (let offset = 0; offset + size * 2 <= auxv.length; offset += size * 2) {
|
|
2250
|
+
const key = word(offset);
|
|
2251
|
+
if (key === 0n) break;
|
|
2252
|
+
if (key === 17n && word(offset + size) > 0n) return word(offset + size);
|
|
2253
|
+
}
|
|
2254
|
+
throw new Error("Linux process clock has no AT_CLKTCK");
|
|
2255
|
+
}
|
|
2256
|
+
function readAuxvWord(buffer, offset, size, littleEndian) {
|
|
2257
|
+
if (size === 8) return littleEndian ? buffer.readBigUInt64LE(offset) : buffer.readBigUInt64BE(offset);
|
|
2258
|
+
return BigInt(littleEndian ? buffer.readUInt32LE(offset) : buffer.readUInt32BE(offset));
|
|
2259
|
+
}
|
|
2260
|
+
async function readLinuxProcessTable(timeoutMs, requiredPids, pid) {
|
|
2261
|
+
const raw = await readLinuxQuery("all", timeoutMs, requiredPids);
|
|
2262
|
+
const scope = verifiedLinuxScope(raw);
|
|
2263
|
+
if (!scope || !Array.isArray(raw.processStats)) throw new Error("Linux process table scope is unverified");
|
|
2264
|
+
const clockTicksPerSecond = linuxClockTicks(raw);
|
|
2265
|
+
const table = /* @__PURE__ */ new Map();
|
|
2266
|
+
for (const value of raw.processStats) {
|
|
2267
|
+
const stat = parseLinuxStat(value);
|
|
2268
|
+
if (stat && (pid === void 0 || stat.pid === pid)) table.set(stat.pid, {
|
|
2269
|
+
pid: stat.pid,
|
|
2270
|
+
parentPid: stat.parentPid,
|
|
2271
|
+
groupPid: stat.groupPid,
|
|
2272
|
+
birth: {
|
|
2273
|
+
kind: "linux-proc",
|
|
2274
|
+
...scope,
|
|
2275
|
+
startTicks: stat.startTicks
|
|
2276
|
+
},
|
|
2277
|
+
clockTicksPerSecond
|
|
2278
|
+
});
|
|
2279
|
+
}
|
|
2280
|
+
return table;
|
|
2281
|
+
}
|
|
2282
|
+
function readLinuxExitClock() {
|
|
2283
|
+
let descriptor;
|
|
2284
|
+
try {
|
|
2285
|
+
descriptor = fs.openSync("/proc/uptime", "r");
|
|
2286
|
+
const buffer = Buffer.alloc(64);
|
|
2287
|
+
const bytes = fs.readSync(descriptor, buffer, 0, buffer.length, 0);
|
|
2288
|
+
const match = /^(\d+)\.(\d{2})\s/u.exec(buffer.toString("ascii", 0, bytes));
|
|
2289
|
+
return match ? {
|
|
2290
|
+
centiseconds: BigInt(match[1]) * 100n + BigInt(match[2]),
|
|
2291
|
+
timeNamespace: readOwnTimeNamespace()
|
|
2292
|
+
} : void 0;
|
|
2293
|
+
} catch {
|
|
2294
|
+
return;
|
|
2295
|
+
} finally {
|
|
2296
|
+
if (descriptor !== void 0) try {
|
|
2297
|
+
fs.closeSync(descriptor);
|
|
2298
|
+
} catch {}
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
function readOwnTimeNamespace() {
|
|
2302
|
+
try {
|
|
2303
|
+
return fs.readlinkSync("/proc/self/ns/time");
|
|
2304
|
+
} catch (error) {
|
|
2305
|
+
if (error.code === "ENOENT") return "unsupported";
|
|
2306
|
+
throw error;
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
function isPositivePid(pid) {
|
|
2310
|
+
return Number.isSafeInteger(pid) && pid > 0;
|
|
2311
|
+
}
|
|
2312
|
+
function windowsPowerShellPath() {
|
|
2313
|
+
const systemRoot = process.env.SystemRoot;
|
|
2314
|
+
if (!systemRoot || !path.win32.isAbsolute(systemRoot)) throw new Error("Windows system directory is unavailable");
|
|
2315
|
+
return path.win32.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
|
|
2316
|
+
}
|
|
2317
|
+
function windowsProcessQuery(pid) {
|
|
2318
|
+
return [
|
|
2319
|
+
"$ErrorActionPreference = 'Stop'",
|
|
2320
|
+
`Get-CimInstance -ClassName Win32_Process -Property ProcessId,ParentProcessId,CreationDate${pid === void 0 ? "" : ` -Filter 'ProcessId = ${pid}'`} | ForEach-Object {`,
|
|
2321
|
+
"if ($null -ne $_.CreationDate) {",
|
|
2322
|
+
"'{0} {1} 0 S {2}' -f $_.ProcessId,$_.ParentProcessId,$_.CreationDate.ToUniversalTime().ToString('o')",
|
|
2323
|
+
"}",
|
|
2324
|
+
"}"
|
|
2325
|
+
].join("\n");
|
|
2326
|
+
}
|
|
2327
|
+
function posixBirth(value) {
|
|
2328
|
+
const match = /^\w{3}\s+(\w{3})\s+(\d{1,2})\s+(\d{2}):(\d{2}):(\d{2})\s+(\d{4})$/.exec(value);
|
|
2329
|
+
if (!match) return;
|
|
2330
|
+
const month = POSIX_MONTHS.indexOf(match[1]);
|
|
2331
|
+
if (month < 0) return;
|
|
2332
|
+
const valueUtc = `${match[6]}-${String(month + 1).padStart(2, "0")}-${match[2].padStart(2, "0")}T${match[3]}:${match[4]}:${match[5]}.000Z`;
|
|
2333
|
+
return isCanonicalBirth(valueUtc, "posix-lstart") ? valueUtc : void 0;
|
|
2334
|
+
}
|
|
2335
|
+
function parseProcessTable(output) {
|
|
2336
|
+
const table = /* @__PURE__ */ new Map();
|
|
2337
|
+
for (const line of output.split("\n")) {
|
|
2338
|
+
const match = /^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(.+?)\s*$/.exec(line);
|
|
2339
|
+
if (!match || match[4].startsWith("Z")) continue;
|
|
2340
|
+
const pid = Number(match[1]);
|
|
2341
|
+
const birth = processTableBirth(match[5]);
|
|
2342
|
+
if (Number.isSafeInteger(pid) && pid > 0 && birth) table.set(pid, {
|
|
2343
|
+
pid,
|
|
2344
|
+
parentPid: Number(match[2]),
|
|
2345
|
+
groupPid: Number(match[3]),
|
|
2346
|
+
birth: processBirthIdentity(birth)
|
|
2347
|
+
});
|
|
2348
|
+
}
|
|
2349
|
+
return table;
|
|
2350
|
+
}
|
|
2351
|
+
function processTableBirth(value) {
|
|
2352
|
+
if (process.platform !== "win32") return posixBirth(value);
|
|
2353
|
+
return isCanonicalBirth(value, "windows-creation") ? value : void 0;
|
|
2354
|
+
}
|
|
2355
|
+
async function readProcessTable(timeoutMs, pid, requiredPids = []) {
|
|
2356
|
+
if (pid !== void 0 && (!Number.isSafeInteger(pid) || pid <= 0)) throw new Error("Process identity requires a positive PID");
|
|
2357
|
+
if (process.platform === "win32") return parseProcessTable(await runTimedExecFile(windowsPowerShellPath(), [
|
|
2358
|
+
"-NoLogo",
|
|
2359
|
+
"-NoProfile",
|
|
2360
|
+
"-NonInteractive",
|
|
2361
|
+
"-Command",
|
|
2362
|
+
windowsProcessQuery(pid)
|
|
2363
|
+
], {
|
|
2364
|
+
timeoutMs,
|
|
2365
|
+
windowsHide: true
|
|
2366
|
+
}));
|
|
2367
|
+
if (process.platform === "linux") return await readLinuxProcessTable(timeoutMs, requiredPids, pid);
|
|
2368
|
+
return parseProcessTable(await runTimedExecFile("ps", [
|
|
2369
|
+
...pid === void 0 ? ["-e"] : ["-p", String(pid)],
|
|
2370
|
+
"-o",
|
|
2371
|
+
"pid=,ppid=,pgid=,stat=,lstart="
|
|
2372
|
+
], {
|
|
2373
|
+
timeoutMs,
|
|
2374
|
+
env: {
|
|
2375
|
+
...process.env,
|
|
2376
|
+
LC_ALL: "C",
|
|
2377
|
+
TZ: "UTC"
|
|
2378
|
+
}
|
|
2379
|
+
}));
|
|
2380
|
+
}
|
|
2381
|
+
async function probeProcessIdentity(pid, timeoutMs = IDENTITY_QUERY_TIMEOUT_MS$1) {
|
|
2382
|
+
if (!isPositivePid(pid)) return { state: "unknown" };
|
|
2383
|
+
if (isProcessDefinitelyDead(pid)) return { state: "dead" };
|
|
2384
|
+
try {
|
|
2385
|
+
const identity = await readNativeIdentity(pid, timeoutMs);
|
|
2386
|
+
if (identity) return {
|
|
2387
|
+
state: "alive",
|
|
2388
|
+
identity
|
|
2389
|
+
};
|
|
2390
|
+
} catch {}
|
|
2391
|
+
return { state: isProcessDefinitelyDead(pid) ? "dead" : "unknown" };
|
|
2392
|
+
}
|
|
2393
|
+
async function readNativeIdentity(pid, timeoutMs) {
|
|
2394
|
+
if (process.platform === "linux") return (await readLinuxObservation(pid, timeoutMs))?.identity;
|
|
2395
|
+
return (await readProcessTable(timeoutMs, pid)).get(pid)?.birth;
|
|
2396
|
+
}
|
|
2397
|
+
function processBirthIdentity(value) {
|
|
2398
|
+
return {
|
|
2399
|
+
kind: process.platform === "win32" ? "windows-creation" : "posix-lstart",
|
|
2400
|
+
value
|
|
2401
|
+
};
|
|
2402
|
+
}
|
|
2403
|
+
function nativeIdentityKind() {
|
|
2404
|
+
if (process.platform === "linux") return "linux-proc";
|
|
2405
|
+
return process.platform === "win32" ? "windows-creation" : "posix-lstart";
|
|
2406
|
+
}
|
|
2407
|
+
function comparisonToIncarnation(comparison) {
|
|
2408
|
+
return comparison === "different" ? "gone" : comparison;
|
|
2409
|
+
}
|
|
2410
|
+
async function observeLinuxIncarnation(pid, expected, timeoutMs) {
|
|
2411
|
+
const observed = await readLinuxObservation(pid, timeoutMs).catch(() => void 0);
|
|
2412
|
+
if (!observed) return "unknown";
|
|
2413
|
+
const scope = compareLinuxScope(expected, observed.scope);
|
|
2414
|
+
if (scope !== "matching") return comparisonToIncarnation(scope);
|
|
2415
|
+
if (isProcessDefinitelyDead(pid)) return "gone";
|
|
2416
|
+
return observed.identity ? comparisonToIncarnation(compareProcessBirthIdentity(expected, observed.identity)) : "unknown";
|
|
2417
|
+
}
|
|
2418
|
+
async function observeProcessIncarnation(pid, expected, timeoutMs = IDENTITY_QUERY_TIMEOUT_MS$1) {
|
|
2419
|
+
if (!isPositivePid(pid)) return "unknown";
|
|
2420
|
+
if (expected?.kind === "linux-proc") return process.platform === "linux" ? await observeLinuxIncarnation(pid, expected, timeoutMs) : "unknown";
|
|
2421
|
+
if (expected && expected.kind !== nativeIdentityKind()) return "unknown";
|
|
2422
|
+
return await observeTimestampIncarnation(pid, expected, timeoutMs);
|
|
2423
|
+
}
|
|
2424
|
+
async function observeTimestampIncarnation(pid, expected, timeoutMs) {
|
|
2425
|
+
if (isProcessDefinitelyDead(pid)) return "gone";
|
|
2426
|
+
if (!expected) return "unknown";
|
|
2427
|
+
const observed = await probeProcessIdentity(pid, timeoutMs);
|
|
2428
|
+
if (observed.state === "dead") return "gone";
|
|
2429
|
+
return observed.state === "alive" ? comparisonToIncarnation(compareProcessBirthIdentity(expected, observed.identity)) : "unknown";
|
|
2430
|
+
}
|
|
2431
|
+
async function getOwnProcessIdentity() {
|
|
2432
|
+
if (ownIdentity) return ownIdentity;
|
|
2433
|
+
ownIdentityPending ??= probeProcessIdentity(process.pid).then((probe) => {
|
|
2434
|
+
if (probe.state === "alive") ownIdentity = probe.identity;
|
|
2435
|
+
return ownIdentity;
|
|
2436
|
+
}).finally(() => {
|
|
2437
|
+
ownIdentityPending = void 0;
|
|
2438
|
+
});
|
|
2439
|
+
return await ownIdentityPending;
|
|
2440
|
+
}
|
|
2441
|
+
//#endregion
|
|
2442
|
+
//#region src/acp/process-descendants.ts
|
|
2443
|
+
function hasStaleWindowsParent(identity, parent) {
|
|
2444
|
+
return identity.birth.kind === "windows-creation" && (parent?.birth.kind !== "windows-creation" || parent.birth.value > identity.birth.value);
|
|
2445
|
+
}
|
|
2446
|
+
function includeProcessDescendants(table, owned) {
|
|
2447
|
+
let expanded;
|
|
2448
|
+
do {
|
|
2449
|
+
expanded = false;
|
|
2450
|
+
for (const identity of table.values()) {
|
|
2451
|
+
if (hasStaleWindowsParent(identity, table.get(identity.parentPid))) continue;
|
|
2452
|
+
if (owned.has(identity.parentPid) && !owned.has(identity.pid)) {
|
|
2453
|
+
owned.add(identity.pid);
|
|
2454
|
+
expanded = true;
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
} while (expanded);
|
|
2458
|
+
}
|
|
2459
|
+
/** Best-effort cleanup for descendants witnessed during this child launch. */
|
|
2460
|
+
var ProcessDescendants = class {
|
|
2461
|
+
child;
|
|
2462
|
+
identities = /* @__PURE__ */ new Map();
|
|
2463
|
+
rootBirth;
|
|
2464
|
+
pending;
|
|
2465
|
+
retired = false;
|
|
2466
|
+
ownProcessGroup;
|
|
2467
|
+
captureGroupAfterExit;
|
|
2468
|
+
groupExitedAt;
|
|
2469
|
+
onRootExit = () => {
|
|
2470
|
+
this.groupExitedAt = process.platform === "linux" ? readLinuxExitClock() ?? null : Date.now();
|
|
2471
|
+
};
|
|
2472
|
+
constructor(child, options = {}) {
|
|
2473
|
+
this.child = child;
|
|
2474
|
+
this.ownProcessGroup = process.platform !== "win32" && Boolean(options.ownProcessGroup);
|
|
2475
|
+
this.captureGroupAfterExit = this.ownProcessGroup && isChildProcessRunning(child);
|
|
2476
|
+
if (this.ownProcessGroup) child.once("exit", this.onRootExit);
|
|
2477
|
+
}
|
|
2478
|
+
capture(timeoutMs = PROCESS_HELPER_TIMEOUT_MS) {
|
|
2479
|
+
if (this.retired) return Promise.resolve(true);
|
|
2480
|
+
this.pending ??= this.readSnapshot(timeoutMs).finally(() => {
|
|
2481
|
+
this.pending = void 0;
|
|
2482
|
+
});
|
|
2483
|
+
return this.pending;
|
|
2484
|
+
}
|
|
2485
|
+
async readSnapshot(timeoutMs) {
|
|
2486
|
+
const deadline = performance.now() + timeoutMs;
|
|
2487
|
+
const rootWasRunning = isChildProcessRunning(this.child);
|
|
2488
|
+
const captureRootGroup = rootWasRunning || this.captureGroupAfterExit;
|
|
2489
|
+
if (!rootWasRunning) this.captureGroupAfterExit = false;
|
|
2490
|
+
try {
|
|
2491
|
+
const table = await readProcessTable(timeoutMs, void 0, this.requiredSnapshotPids(rootWasRunning));
|
|
2492
|
+
if (!this.retired) {
|
|
2493
|
+
table.delete(1);
|
|
2494
|
+
table.delete(process.pid);
|
|
2495
|
+
this.refresh(table, captureRootGroup);
|
|
2496
|
+
if (this.captureGroupAfterExit && !isChildProcessRunning(this.child)) return await this.readSnapshot(Math.max(1, deadline - performance.now()));
|
|
2497
|
+
}
|
|
2498
|
+
return true;
|
|
2499
|
+
} catch {
|
|
2500
|
+
if (!isChildProcessRunning(this.child)) this.captureGroupAfterExit = false;
|
|
2501
|
+
return false;
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
requiredSnapshotPids(rootWasRunning) {
|
|
2505
|
+
return [...this.identities.keys(), ...rootWasRunning && this.child.pid ? [this.child.pid] : []];
|
|
2506
|
+
}
|
|
2507
|
+
refresh(table, rootWasRunning) {
|
|
2508
|
+
this.verifyScope(table);
|
|
2509
|
+
const owned = /* @__PURE__ */ new Set();
|
|
2510
|
+
for (const [pid, identity] of this.identities) {
|
|
2511
|
+
const observed = table.get(pid);
|
|
2512
|
+
if (observed && compareProcessBirthIdentity(identity.birth, observed.birth) === "matching") owned.add(pid);
|
|
2513
|
+
}
|
|
2514
|
+
this.includeRoot(table, owned);
|
|
2515
|
+
if (this.ownProcessGroup) this.includeProcessGroup(table, owned, rootWasRunning);
|
|
2516
|
+
includeProcessDescendants(table, owned);
|
|
2517
|
+
owned.delete(this.child.pid ?? 0);
|
|
2518
|
+
this.identities = new Map([...table].filter(([pid]) => owned.has(pid)));
|
|
2519
|
+
}
|
|
2520
|
+
verifyScope(table) {
|
|
2521
|
+
const expected = this.rootBirth ?? this.identities.values().next().value?.birth;
|
|
2522
|
+
const observation = table.values().next().value?.birth;
|
|
2523
|
+
if (expected && observation && compareProcessBirthIdentity(expected, observation) === "unknown") throw new Error("Descendant process scope changed");
|
|
2524
|
+
}
|
|
2525
|
+
includeRoot(table, owned) {
|
|
2526
|
+
const root = this.child.pid;
|
|
2527
|
+
const rootIdentity = root && table.get(root);
|
|
2528
|
+
if (rootIdentity && isChildProcessRunning(this.child)) {
|
|
2529
|
+
this.rootBirth ??= rootIdentity.birth;
|
|
2530
|
+
if (compareProcessBirthIdentity(this.rootBirth, rootIdentity.birth) === "matching") owned.add(rootIdentity.pid);
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
includeProcessGroup(table, owned, rootWasRunning) {
|
|
2534
|
+
const root = this.child.pid;
|
|
2535
|
+
if (root && (rootWasRunning || [...owned].some((pid) => table.get(pid)?.groupPid === root))) {
|
|
2536
|
+
for (const identity of table.values()) if (identity.groupPid === root && this.startedBeforeRootExit(identity)) owned.add(identity.pid);
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
startedBeforeRootExit(identity) {
|
|
2540
|
+
const cutoff = this.groupExitedAt;
|
|
2541
|
+
if (cutoff === void 0) return true;
|
|
2542
|
+
if (cutoff === null) return false;
|
|
2543
|
+
if (identity.birth.kind !== "linux-proc") return typeof cutoff === "number" && Date.parse(identity.birth.value) <= cutoff;
|
|
2544
|
+
if (typeof cutoff === "number" || identity.birth.timeNamespace !== cutoff.timeNamespace || !identity.clockTicksPerSecond) return false;
|
|
2545
|
+
return BigInt(identity.birth.startTicks) * 100n < (cutoff.centiseconds + 1n) * identity.clockTicksPerSecond;
|
|
2546
|
+
}
|
|
2547
|
+
async signal(signal, timeoutMs) {
|
|
2548
|
+
if (!await this.capture(timeoutMs) || this.retired) return;
|
|
2549
|
+
for (const pid of this.identities.keys()) try {
|
|
2550
|
+
process.kill(pid, signal);
|
|
2551
|
+
} catch {
|
|
2552
|
+
this.identities.delete(pid);
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
hasTrackedProcesses() {
|
|
2556
|
+
return this.identities.size > 0;
|
|
2557
|
+
}
|
|
2558
|
+
async waitForExit(timeoutMs) {
|
|
2559
|
+
const deadline = performance.now() + timeoutMs;
|
|
2560
|
+
do {
|
|
2561
|
+
if (!await this.capture(Math.max(1, deadline - performance.now()))) return false;
|
|
2562
|
+
if (this.identities.size === 0) return true;
|
|
2563
|
+
await setTimeout$1(Math.min(100, Math.max(0, deadline - performance.now())));
|
|
2564
|
+
} while (performance.now() < deadline);
|
|
2565
|
+
return false;
|
|
2566
|
+
}
|
|
2567
|
+
retire() {
|
|
2568
|
+
this.retired = true;
|
|
2569
|
+
this.child.off("exit", this.onRootExit);
|
|
2570
|
+
this.identities.clear();
|
|
2571
|
+
}
|
|
2572
|
+
};
|
|
2573
|
+
//#endregion
|
|
2574
|
+
//#region src/session/lock-owner.ts
|
|
2575
|
+
const OBSERVATION_REUSE_MS = 1e3;
|
|
2576
|
+
const IDENTITY_QUERY_TIMEOUT_MS = 1e3;
|
|
2577
|
+
function lockOwnerPid(value) {
|
|
2578
|
+
if (!value || typeof value !== "object" || !("pid" in value)) return;
|
|
2579
|
+
const pid = value.pid;
|
|
2580
|
+
return typeof pid === "number" && Number.isSafeInteger(pid) && pid > 0 ? pid : void 0;
|
|
2581
|
+
}
|
|
2582
|
+
function canReuseObservation(preserved, pid, identity, fresh) {
|
|
2583
|
+
return !fresh && preserved?.pid === pid && compareProcessBirthIdentity(preserved.identity, identity) === "matching" && performance.now() - preserved.checkedAt < OBSERVATION_REUSE_MS;
|
|
2584
|
+
}
|
|
2585
|
+
async function createLockOwner(options = {}) {
|
|
2586
|
+
const { signal, capturedIdentity } = options;
|
|
2587
|
+
signal?.throwIfAborted();
|
|
2588
|
+
const processIdentity = capturedIdentity === void 0 ? await getOwnProcessIdentity() : capturedIdentity.processIdentity;
|
|
2589
|
+
signal?.throwIfAborted();
|
|
2590
|
+
let preserved;
|
|
2591
|
+
return {
|
|
2592
|
+
payload: {
|
|
2593
|
+
pid: process.pid,
|
|
2594
|
+
processIdentity
|
|
2595
|
+
},
|
|
2596
|
+
async hasExited(payload, fresh, signal) {
|
|
2597
|
+
signal?.throwIfAborted();
|
|
2598
|
+
const pid = lockOwnerPid(payload);
|
|
2599
|
+
if (!pid) return false;
|
|
2600
|
+
const identity = parseProcessBirthIdentity(payload.processIdentity);
|
|
2601
|
+
if (identity && canReuseObservation(preserved, pid, identity, fresh)) return false;
|
|
2602
|
+
const incarnation = await observeProcessIncarnation(pid, identity, IDENTITY_QUERY_TIMEOUT_MS);
|
|
2603
|
+
signal?.throwIfAborted();
|
|
2604
|
+
if (identity && incarnation !== "gone") preserved = {
|
|
2605
|
+
pid,
|
|
2606
|
+
identity,
|
|
2607
|
+
checkedAt: performance.now()
|
|
2608
|
+
};
|
|
2609
|
+
return incarnation === "gone";
|
|
2610
|
+
}
|
|
2611
|
+
};
|
|
2612
|
+
}
|
|
2613
|
+
//#endregion
|
|
2115
2614
|
//#region src/session/queue/paths.ts
|
|
2116
2615
|
function shortHash(value, length) {
|
|
2117
2616
|
return createHash("sha256").update(value).digest("hex").slice(0, length);
|
|
@@ -2135,6 +2634,127 @@ function queueSocketPath(sessionId, homeDir = os.homedir()) {
|
|
|
2135
2634
|
return path.join(queueSocketBaseDir(homeDir) ?? "/tmp", `${key}.sock`);
|
|
2136
2635
|
}
|
|
2137
2636
|
//#endregion
|
|
2637
|
+
//#region src/session/queue/retirement-receipt.ts
|
|
2638
|
+
const MAX_RETIREMENT_MEMBERS = 4096;
|
|
2639
|
+
const MAX_RETIREMENT_BYTES = 1048576;
|
|
2640
|
+
function hasQueueRetirementReceipt(value) {
|
|
2641
|
+
return value !== null && typeof value === "object" && Object.hasOwn(value, "retirement");
|
|
2642
|
+
}
|
|
2643
|
+
function parseWitness(raw) {
|
|
2644
|
+
if (!raw || typeof raw !== "object") return;
|
|
2645
|
+
const value = raw;
|
|
2646
|
+
const processIdentity = parseProcessBirthIdentity(value.processIdentity);
|
|
2647
|
+
if (typeof value.pid !== "number" || !Number.isSafeInteger(value.pid) || value.pid <= 1 || processIdentity?.kind !== "windows-creation") return;
|
|
2648
|
+
return {
|
|
2649
|
+
pid: value.pid,
|
|
2650
|
+
processIdentity
|
|
2651
|
+
};
|
|
2652
|
+
}
|
|
2653
|
+
function parseQueueRetirementReceipt(raw, owner) {
|
|
2654
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
2655
|
+
const value = raw;
|
|
2656
|
+
const root = parseReceiptRoot(value, owner);
|
|
2657
|
+
if (!root) return null;
|
|
2658
|
+
const descendants = parseDescendants(value.descendants, root.pid);
|
|
2659
|
+
if (!descendants) return null;
|
|
2660
|
+
try {
|
|
2661
|
+
if (Buffer.byteLength(JSON.stringify(raw)) > MAX_RETIREMENT_BYTES) return null;
|
|
2662
|
+
} catch {
|
|
2663
|
+
return null;
|
|
2664
|
+
}
|
|
2665
|
+
return {
|
|
2666
|
+
ownerGeneration: owner.ownerGeneration,
|
|
2667
|
+
root,
|
|
2668
|
+
descendants
|
|
2669
|
+
};
|
|
2670
|
+
}
|
|
2671
|
+
function parseReceiptRoot(value, owner) {
|
|
2672
|
+
const root = parseWitness(value.root);
|
|
2673
|
+
if (!root || root.pid !== owner.pid || value.ownerGeneration !== owner.ownerGeneration || compareProcessBirthIdentity(owner.processIdentity, root.processIdentity) !== "matching") return;
|
|
2674
|
+
return root;
|
|
2675
|
+
}
|
|
2676
|
+
function parseDescendants(raw, rootPid) {
|
|
2677
|
+
if (!Array.isArray(raw) || raw.length >= MAX_RETIREMENT_MEMBERS) return;
|
|
2678
|
+
const pids = /* @__PURE__ */ new Set([rootPid]);
|
|
2679
|
+
const witnesses = [];
|
|
2680
|
+
for (const value of raw) {
|
|
2681
|
+
const witness = parseWitness(value);
|
|
2682
|
+
if (!witness || pids.has(witness.pid)) return;
|
|
2683
|
+
pids.add(witness.pid);
|
|
2684
|
+
witnesses.push(witness);
|
|
2685
|
+
}
|
|
2686
|
+
return witnesses;
|
|
2687
|
+
}
|
|
2688
|
+
function queueRetirementIncomplete() {
|
|
2689
|
+
return new QueueConnectionError("Queue-owner descendant cleanup is incomplete or unverified. Its lease was retained. Retry cleanup with updated clients and process-query access; do not remove the lease while recorded processes may remain.", {
|
|
2690
|
+
detailCode: "QUEUE_OWNER_RETIREMENT_INCOMPLETE",
|
|
2691
|
+
origin: "queue",
|
|
2692
|
+
retryable: true
|
|
2693
|
+
});
|
|
2694
|
+
}
|
|
2695
|
+
function retirementTimeRemaining(deadline, maximumMs = 2e3) {
|
|
2696
|
+
const remaining = deadline - performance.now();
|
|
2697
|
+
if (remaining <= 0) throw queueRetirementIncomplete();
|
|
2698
|
+
return Math.min(maximumMs, remaining);
|
|
2699
|
+
}
|
|
2700
|
+
async function captureQueueRetirementReceipt(owner, previous, rootRunning, deadline) {
|
|
2701
|
+
const root = parseWitness({
|
|
2702
|
+
pid: owner.pid,
|
|
2703
|
+
processIdentity: owner.processIdentity
|
|
2704
|
+
});
|
|
2705
|
+
if (!root || process.platform !== "win32") throw queueRetirementIncomplete();
|
|
2706
|
+
const table = await readProcessTable(retirementTimeRemaining(deadline)).catch(() => {
|
|
2707
|
+
throw queueRetirementIncomplete();
|
|
2708
|
+
});
|
|
2709
|
+
const saved = unsettledSnapshotWitnesses(table, previous ? previous.descendants : []);
|
|
2710
|
+
const owned = matchingSnapshotPids(table, rootRunning ? [root, ...saved] : saved);
|
|
2711
|
+
if (rootRunning && !owned.has(root.pid)) throw queueRetirementIncomplete();
|
|
2712
|
+
table.delete(1);
|
|
2713
|
+
table.delete(process.pid);
|
|
2714
|
+
includeProcessDescendants(table, owned);
|
|
2715
|
+
owned.delete(root.pid);
|
|
2716
|
+
const receipt = {
|
|
2717
|
+
ownerGeneration: owner.ownerGeneration,
|
|
2718
|
+
root,
|
|
2719
|
+
descendants: mergeSnapshotDescendants(table, owned, saved)
|
|
2720
|
+
};
|
|
2721
|
+
if (!parseQueueRetirementReceipt(receipt, owner)) throw queueRetirementIncomplete();
|
|
2722
|
+
return receipt;
|
|
2723
|
+
}
|
|
2724
|
+
function unsettledSnapshotWitnesses(table, witnesses) {
|
|
2725
|
+
return witnesses.filter((witness) => {
|
|
2726
|
+
const observed = table.get(witness.pid);
|
|
2727
|
+
return !observed || compareProcessBirthIdentity(witness.processIdentity, observed.birth) !== "different";
|
|
2728
|
+
});
|
|
2729
|
+
}
|
|
2730
|
+
function matchingSnapshotPids(table, witnesses) {
|
|
2731
|
+
const owned = /* @__PURE__ */ new Set();
|
|
2732
|
+
for (const witness of witnesses) {
|
|
2733
|
+
const observed = table.get(witness.pid);
|
|
2734
|
+
if (observed && compareProcessBirthIdentity(witness.processIdentity, observed.birth) === "matching") owned.add(witness.pid);
|
|
2735
|
+
}
|
|
2736
|
+
return owned;
|
|
2737
|
+
}
|
|
2738
|
+
function mergeSnapshotDescendants(table, owned, saved) {
|
|
2739
|
+
const descendants = new Map(saved.map((witness) => [witness.pid, witness]));
|
|
2740
|
+
for (const pid of owned) {
|
|
2741
|
+
const observed = table.get(pid);
|
|
2742
|
+
if (observed) descendants.set(pid, {
|
|
2743
|
+
pid,
|
|
2744
|
+
processIdentity: observed.birth
|
|
2745
|
+
});
|
|
2746
|
+
}
|
|
2747
|
+
return [...descendants.values()];
|
|
2748
|
+
}
|
|
2749
|
+
async function observeRetirementWitness(witness, deadline) {
|
|
2750
|
+
if (witness.pid === process.pid) throw queueRetirementIncomplete();
|
|
2751
|
+
return await observeProcessIncarnation(witness.pid, witness.processIdentity, retirementTimeRemaining(deadline));
|
|
2752
|
+
}
|
|
2753
|
+
async function assertQueueRetirementComplete(receipt, deadline) {
|
|
2754
|
+
if (receipt === null) throw queueRetirementIncomplete();
|
|
2755
|
+
for (const witness of receipt?.descendants ?? []) if (await observeRetirementWitness(witness, deadline) !== "gone") throw queueRetirementIncomplete();
|
|
2756
|
+
}
|
|
2757
|
+
//#endregion
|
|
2138
2758
|
//#region src/session/queue/lease-mutation.ts
|
|
2139
2759
|
const GUARD_WAIT_MS = 2e3;
|
|
2140
2760
|
const localMutations = /* @__PURE__ */ new Map();
|
|
@@ -2157,10 +2777,7 @@ async function canonicalLeasePath(sessionId) {
|
|
|
2157
2777
|
throw error;
|
|
2158
2778
|
}
|
|
2159
2779
|
}
|
|
2160
|
-
function
|
|
2161
|
-
return payload !== null && typeof payload === "object" && "pid" in payload && typeof payload.pid === "number" && isProcessDefinitelyDead(payload.pid);
|
|
2162
|
-
}
|
|
2163
|
-
async function acquireGuard(lockPath) {
|
|
2780
|
+
async function acquireGuard(lockPath, owner) {
|
|
2164
2781
|
return await acquireFileLock(lockPath, {
|
|
2165
2782
|
managerKey: "acpx.queue-lease-mutation",
|
|
2166
2783
|
lockPath: `${lockPath}.guard`,
|
|
@@ -2173,9 +2790,9 @@ async function acquireGuard(lockPath) {
|
|
|
2173
2790
|
randomize: false
|
|
2174
2791
|
},
|
|
2175
2792
|
staleRecovery: "remove-if-unchanged",
|
|
2176
|
-
payload: () =>
|
|
2177
|
-
shouldReclaim:
|
|
2178
|
-
shouldRemoveStaleLock:
|
|
2793
|
+
payload: () => owner.payload,
|
|
2794
|
+
shouldReclaim: ({ payload }) => owner.hasExited(payload),
|
|
2795
|
+
shouldRemoveStaleLock: ({ payload }) => owner.hasExited(payload, true)
|
|
2179
2796
|
});
|
|
2180
2797
|
}
|
|
2181
2798
|
function matchesReservation(value, expected) {
|
|
@@ -2193,7 +2810,7 @@ async function rollbackReservation(lockPath, expected) {
|
|
|
2193
2810
|
} catch {
|
|
2194
2811
|
return;
|
|
2195
2812
|
}
|
|
2196
|
-
if (matchesReservation(value, expected)) await fs$1.unlink(lockPath);
|
|
2813
|
+
if (matchesReservation(value, expected) && !hasQueueRetirementReceipt(value)) await fs$1.unlink(lockPath);
|
|
2197
2814
|
} catch (error) {
|
|
2198
2815
|
if (error.code !== "ENOENT") throw error;
|
|
2199
2816
|
}
|
|
@@ -2202,7 +2819,7 @@ async function settleReceipt(lockPath, receipt) {
|
|
|
2202
2819
|
if (receipt.reservation) {
|
|
2203
2820
|
if (!await receipt.guard.verifyStillHeld()) {
|
|
2204
2821
|
await receipt.guard.release();
|
|
2205
|
-
receipt.guard = await acquireGuard(lockPath);
|
|
2822
|
+
receipt.guard = await acquireGuard(lockPath, receipt.owner);
|
|
2206
2823
|
}
|
|
2207
2824
|
await rollbackReservation(lockPath, receipt.reservation);
|
|
2208
2825
|
receipt.reservation = void 0;
|
|
@@ -2241,7 +2858,8 @@ async function settlePendingQueueLeaseGuard(sessionId) {
|
|
|
2241
2858
|
if (!localMutations.has(lockPath)) return;
|
|
2242
2859
|
await locallySerialized(lockPath, async (state) => await settleFailedGuard(lockPath, state));
|
|
2243
2860
|
}
|
|
2244
|
-
async function withQueueLeaseMutation(sessionId, mutate,
|
|
2861
|
+
async function withQueueLeaseMutation(sessionId, mutate, options = {}) {
|
|
2862
|
+
const { reservation, capturedIdentity } = options;
|
|
2245
2863
|
await fs$1.mkdir(path.dirname(queueLockFilePath(sessionId)), {
|
|
2246
2864
|
recursive: true,
|
|
2247
2865
|
mode: 448
|
|
@@ -2255,7 +2873,8 @@ async function withQueueLeaseMutation(sessionId, mutate, reservation) {
|
|
|
2255
2873
|
const publishedReservation = () => reservation?.published ? expected : void 0;
|
|
2256
2874
|
return await locallySerialized(lockPath, async (state) => {
|
|
2257
2875
|
await settleFailedGuard(lockPath, state);
|
|
2258
|
-
const
|
|
2876
|
+
const owner = await createLockOwner({ capturedIdentity });
|
|
2877
|
+
const guard = await acquireGuard(lockPath, owner);
|
|
2259
2878
|
let outcome;
|
|
2260
2879
|
try {
|
|
2261
2880
|
outcome = {
|
|
@@ -2271,6 +2890,7 @@ async function withQueueLeaseMutation(sessionId, mutate, reservation) {
|
|
|
2271
2890
|
const published = publishedReservation();
|
|
2272
2891
|
const receipt = {
|
|
2273
2892
|
guard,
|
|
2893
|
+
owner,
|
|
2274
2894
|
reservation: outcome.ok ? void 0 : published
|
|
2275
2895
|
};
|
|
2276
2896
|
try {
|
|
@@ -2294,7 +2914,8 @@ function parseQueueOwnerRecord(raw) {
|
|
|
2294
2914
|
if (!raw || typeof raw !== "object") return null;
|
|
2295
2915
|
const record = raw;
|
|
2296
2916
|
if (!hasValidQueueOwnerRecordFields(record)) return null;
|
|
2297
|
-
|
|
2917
|
+
const processIdentity = parseProcessBirthIdentity(record.processIdentity);
|
|
2918
|
+
const owner = {
|
|
2298
2919
|
pid: record.pid,
|
|
2299
2920
|
sessionId: record.sessionId,
|
|
2300
2921
|
socketPath: record.socketPath,
|
|
@@ -2302,10 +2923,13 @@ function parseQueueOwnerRecord(raw) {
|
|
|
2302
2923
|
heartbeatAt: record.heartbeatAt,
|
|
2303
2924
|
ownerGeneration: record.ownerGeneration,
|
|
2304
2925
|
queueDepth: record.queueDepth,
|
|
2926
|
+
...processIdentity ? { processIdentity } : {},
|
|
2305
2927
|
...parseQueueOwnerCapabilities(record),
|
|
2306
2928
|
...typeof record.mcpConfigPath === "string" ? { mcpConfigPath: record.mcpConfigPath } : {},
|
|
2307
2929
|
...typeof record.mcpConfigFingerprint === "string" ? { mcpConfigFingerprint: record.mcpConfigFingerprint } : {}
|
|
2308
2930
|
};
|
|
2931
|
+
if (hasQueueRetirementReceipt(record)) owner.retirement = parseQueueRetirementReceipt(record.retirement, owner);
|
|
2932
|
+
return owner;
|
|
2309
2933
|
}
|
|
2310
2934
|
function parseQueueOwnerCapabilities(record) {
|
|
2311
2935
|
return {
|
|
@@ -2367,29 +2991,29 @@ async function removeSocketFile(socketPath) {
|
|
|
2367
2991
|
}
|
|
2368
2992
|
}
|
|
2369
2993
|
async function waitForProcessExit(pid, timeoutMs, hasExited) {
|
|
2370
|
-
const deadline =
|
|
2371
|
-
while (
|
|
2994
|
+
const deadline = performance.now() + Math.max(0, timeoutMs);
|
|
2995
|
+
while (performance.now() <= deadline) {
|
|
2372
2996
|
if (hasExited(pid)) return true;
|
|
2373
2997
|
await waitMs(PROCESS_POLL_MS);
|
|
2374
2998
|
}
|
|
2375
2999
|
return hasExited(pid);
|
|
2376
3000
|
}
|
|
2377
|
-
async function cleanupQueueOwnerFiles(sessionId, socketPath, isCurrent) {
|
|
3001
|
+
async function cleanupQueueOwnerFiles(sessionId, socketPath, isCurrent, capturedIdentity, deadline = retirementDeadline()) {
|
|
2378
3002
|
await withQueueLeaseMutation(sessionId, async () => {
|
|
2379
|
-
await cleanupGuardedQueueOwnerFiles(sessionId, socketPath, isCurrent);
|
|
2380
|
-
});
|
|
3003
|
+
await cleanupGuardedQueueOwnerFiles(sessionId, socketPath, isCurrent, deadline);
|
|
3004
|
+
}, { capturedIdentity });
|
|
2381
3005
|
}
|
|
2382
|
-
async function cleanupGuardedQueueOwnerFiles(sessionId, socketPath, isCurrent) {
|
|
3006
|
+
async function cleanupGuardedQueueOwnerFiles(sessionId, socketPath, isCurrent, deadline = retirementDeadline()) {
|
|
2383
3007
|
if (!await isCurrent()) return;
|
|
3008
|
+
await assertQueueRetirementComplete((await readQueueOwnerRecord(sessionId))?.retirement, deadline);
|
|
2384
3009
|
await removeSocketFile(socketPath).catch(() => {});
|
|
2385
3010
|
if (!await isCurrent()) return;
|
|
2386
3011
|
await fs$1.unlink(queueLockFilePath(sessionId)).catch((error) => {
|
|
2387
3012
|
if (error.code !== "ENOENT") throw error;
|
|
2388
3013
|
});
|
|
2389
3014
|
}
|
|
2390
|
-
async function ownsQueueLease(owner
|
|
2391
|
-
|
|
2392
|
-
return matchesQueueOwner(current, owner) && (!requireStale || isQueueOwnerHeartbeatStale(current));
|
|
3015
|
+
async function ownsQueueLease(owner) {
|
|
3016
|
+
return matchesQueueOwner(await readQueueOwnerRecord(owner.sessionId), owner);
|
|
2393
3017
|
}
|
|
2394
3018
|
function matchesQueueOwner(current, expected) {
|
|
2395
3019
|
return current?.sessionId === expected.sessionId && current.pid === expected.pid && current.ownerGeneration === expected.ownerGeneration;
|
|
@@ -2397,6 +3021,9 @@ function matchesQueueOwner(current, expected) {
|
|
|
2397
3021
|
function ownerIsAlive(owner) {
|
|
2398
3022
|
return owner.pid === process.pid || isProcessAlive(owner.pid);
|
|
2399
3023
|
}
|
|
3024
|
+
function ownerAcceptsWork(owner) {
|
|
3025
|
+
return owner.retirement === void 0 && ownerIsAlive(owner) && !isQueueOwnerHeartbeatStale(owner);
|
|
3026
|
+
}
|
|
2400
3027
|
async function readQueueOwnerRecord(sessionId) {
|
|
2401
3028
|
const lockPath = queueLockFilePath(sessionId);
|
|
2402
3029
|
try {
|
|
@@ -2422,10 +3049,26 @@ function dispatchSignal(pid, signal) {
|
|
|
2422
3049
|
return false;
|
|
2423
3050
|
}
|
|
2424
3051
|
}
|
|
2425
|
-
async function
|
|
3052
|
+
async function dispatchQueueOwnerSignal(pid, signal, deadline) {
|
|
3053
|
+
if (process.platform !== "win32") return dispatchSignal(pid, signal);
|
|
3054
|
+
const systemRoot = process.env.SystemRoot;
|
|
3055
|
+
if (!systemRoot || !path.win32.isAbsolute(systemRoot)) throw new Error("Windows queue-owner cleanup requires an absolute SystemRoot directory");
|
|
3056
|
+
const taskkill = path.win32.join(systemRoot, "System32", "taskkill.exe");
|
|
3057
|
+
await runTimedExecFile(taskkill, [
|
|
3058
|
+
"/pid",
|
|
3059
|
+
String(pid),
|
|
3060
|
+
"/T",
|
|
3061
|
+
"/F"
|
|
3062
|
+
], {
|
|
3063
|
+
timeoutMs: retirementTimeRemaining(deadline, PROCESS_SIGTERM_GRACE_MS),
|
|
3064
|
+
windowsHide: true
|
|
3065
|
+
});
|
|
3066
|
+
return true;
|
|
3067
|
+
}
|
|
3068
|
+
async function terminateWithDispatch(pid, dispatch, hasExited, deadline = Infinity) {
|
|
2426
3069
|
for (const [signal, graceMs] of [["SIGTERM", PROCESS_SIGTERM_GRACE_MS], ["SIGKILL", PROCESS_SIGKILL_GRACE_MS]]) {
|
|
2427
3070
|
if (!await dispatch(signal)) return false;
|
|
2428
|
-
if (await waitForProcessExit(pid, graceMs, hasExited)) return true;
|
|
3071
|
+
if (await waitForProcessExit(pid, Math.min(graceMs, deadline - performance.now()), hasExited)) return true;
|
|
2429
3072
|
}
|
|
2430
3073
|
return false;
|
|
2431
3074
|
}
|
|
@@ -2433,14 +3076,15 @@ async function resolveUsableQueueOwner(sessionId, owner) {
|
|
|
2433
3076
|
await settlePendingQueueLeaseGuard(sessionId);
|
|
2434
3077
|
const observed = await readQueueOwnerRecord(sessionId);
|
|
2435
3078
|
if (!matchesQueueOwner(observed, owner)) return;
|
|
2436
|
-
if (
|
|
3079
|
+
if (ownerAcceptsWork(observed)) return observed;
|
|
2437
3080
|
await terminateQueueOwnerForSession(sessionId, observed, true);
|
|
2438
3081
|
const current = await readQueueOwnerRecord(sessionId);
|
|
2439
|
-
return matchesQueueOwner(current, observed) &&
|
|
3082
|
+
return matchesQueueOwner(current, observed) && ownerAcceptsWork(current) ? current : void 0;
|
|
2440
3083
|
}
|
|
2441
3084
|
async function tryAcquireQueueOwnerLease(sessionId, mcpConfigOrNowIsoFactory, nowIsoFactory = nowIso) {
|
|
2442
3085
|
const { mcpConfigPath, clock } = resolveLeaseArguments(mcpConfigOrNowIsoFactory, nowIsoFactory);
|
|
2443
3086
|
const mcpConfigMetadata = createMcpConfigMetadata(mcpConfigPath, readMcpConfigFingerprint(mcpConfigOrNowIsoFactory));
|
|
3087
|
+
const processIdentity = await getOwnProcessIdentity();
|
|
2444
3088
|
await ensureQueueDir();
|
|
2445
3089
|
const lockPath = queueLockFilePath(sessionId);
|
|
2446
3090
|
const socketPath = queueSocketPath(sessionId);
|
|
@@ -2453,6 +3097,7 @@ async function tryAcquireQueueOwnerLease(sessionId, mcpConfigOrNowIsoFactory, no
|
|
|
2453
3097
|
socketPath,
|
|
2454
3098
|
createdAt,
|
|
2455
3099
|
ownerGeneration,
|
|
3100
|
+
...processIdentity ? { processIdentity } : {},
|
|
2456
3101
|
...mcpConfigMetadata,
|
|
2457
3102
|
updates: Promise.resolve(),
|
|
2458
3103
|
released: false
|
|
@@ -2480,7 +3125,10 @@ async function tryAcquireQueueOwnerLease(sessionId, mcpConfigOrNowIsoFactory, no
|
|
|
2480
3125
|
});
|
|
2481
3126
|
await removeSocketFile(socketPath).catch(() => {});
|
|
2482
3127
|
return lease;
|
|
2483
|
-
},
|
|
3128
|
+
}, {
|
|
3129
|
+
reservation,
|
|
3130
|
+
capturedIdentity: lease
|
|
3131
|
+
});
|
|
2484
3132
|
} catch (error) {
|
|
2485
3133
|
return await handleLeaseCollision(sessionId, error);
|
|
2486
3134
|
}
|
|
@@ -2542,32 +3190,45 @@ function refreshQueueOwnerLease(lease, options, nowIsoFactory = nowIso) {
|
|
|
2542
3190
|
if (lease.released) return Promise.resolve();
|
|
2543
3191
|
const update = lease.updates.then(async () => {
|
|
2544
3192
|
await withQueueLeaseMutation(lease.sessionId, async () => {
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
3193
|
+
const current = await readQueueOwnerRecord(lease.sessionId);
|
|
3194
|
+
if (!matchesQueueOwner(current, lease)) return;
|
|
3195
|
+
if (current.retirement === null) throw queueRetirementIncomplete();
|
|
3196
|
+
await replaceQueueOwnerRecord({
|
|
3197
|
+
...current,
|
|
3198
|
+
heartbeatAt: nowIsoFactory(),
|
|
3199
|
+
queueDepth: Math.max(0, Math.round(options.queueDepth))
|
|
2548
3200
|
});
|
|
2549
|
-
});
|
|
3201
|
+
}, { capturedIdentity: lease });
|
|
2550
3202
|
});
|
|
2551
3203
|
lease.updates = update.catch(() => {});
|
|
2552
3204
|
return update;
|
|
2553
3205
|
}
|
|
2554
3206
|
async function stageQueueOwnerRecord(lease, queueDepth, clock, publish) {
|
|
2555
|
-
|
|
2556
|
-
pid:
|
|
3207
|
+
await stagePreparedQueueOwnerRecord({
|
|
3208
|
+
pid: lease.pid,
|
|
2557
3209
|
sessionId: lease.sessionId,
|
|
2558
3210
|
socketPath: lease.socketPath,
|
|
2559
3211
|
createdAt: lease.createdAt,
|
|
2560
3212
|
heartbeatAt: clock(),
|
|
2561
3213
|
ownerGeneration: lease.ownerGeneration,
|
|
3214
|
+
...lease.processIdentity ? { processIdentity: lease.processIdentity } : {},
|
|
2562
3215
|
queueDepth: Math.max(0, Math.round(queueDepth)),
|
|
2563
3216
|
sharedRuntime: true,
|
|
2564
3217
|
sessionWatch: true,
|
|
2565
3218
|
persistsControlState: true,
|
|
2566
3219
|
...lease.mcpConfigPath ? { mcpConfigPath: lease.mcpConfigPath } : {},
|
|
2567
3220
|
...lease.mcpConfigFingerprint ? { mcpConfigFingerprint: lease.mcpConfigFingerprint } : {}
|
|
2568
|
-
},
|
|
3221
|
+
}, publish);
|
|
3222
|
+
}
|
|
3223
|
+
async function replaceQueueOwnerRecord(owner) {
|
|
3224
|
+
await stagePreparedQueueOwnerRecord(owner, async (tempPath) => {
|
|
3225
|
+
await fs$1.rename(tempPath, queueLockFilePath(owner.sessionId));
|
|
3226
|
+
});
|
|
3227
|
+
}
|
|
3228
|
+
async function stagePreparedQueueOwnerRecord(owner, publish) {
|
|
3229
|
+
const payload = JSON.stringify(owner, null, 2);
|
|
2569
3230
|
await withTempFile({
|
|
2570
|
-
rootDir: path.dirname(
|
|
3231
|
+
rootDir: path.dirname(queueLockFilePath(owner.sessionId)),
|
|
2571
3232
|
prefix: "owner",
|
|
2572
3233
|
fileName: "lease"
|
|
2573
3234
|
}, async (tempPath) => {
|
|
@@ -2582,18 +3243,129 @@ async function stageQueueOwnerRecord(lease, queueDepth, clock, publish) {
|
|
|
2582
3243
|
async function releaseQueueOwnerLease(lease) {
|
|
2583
3244
|
lease.released = true;
|
|
2584
3245
|
await lease.updates;
|
|
2585
|
-
await cleanupQueueOwnerFiles(lease.sessionId, lease.socketPath, () => ownsQueueLease(lease));
|
|
3246
|
+
await cleanupQueueOwnerFiles(lease.sessionId, lease.socketPath, () => ownsQueueLease(lease), lease);
|
|
3247
|
+
}
|
|
3248
|
+
function unverifiedQueueOwnerError(owner) {
|
|
3249
|
+
return new QueueConnectionError(`Cannot safely retire queue owner pid ${owner.pid}: its process birth identity is unverified. Its lease was retained. Retry from the same local process namespace with process-query access, or let the owner finish normal shutdown or idle expiry.`, {
|
|
3250
|
+
detailCode: "QUEUE_OWNER_IDENTITY_UNVERIFIED",
|
|
3251
|
+
origin: "queue",
|
|
3252
|
+
retryable: true
|
|
3253
|
+
});
|
|
3254
|
+
}
|
|
3255
|
+
function retirementDeadline() {
|
|
3256
|
+
return performance.now() + PROCESS_SIGTERM_GRACE_MS + PROCESS_SIGKILL_GRACE_MS;
|
|
3257
|
+
}
|
|
3258
|
+
async function observeRetiringOwner(owner, deadline) {
|
|
3259
|
+
return await observeProcessIncarnation(owner.pid, owner.processIdentity, process.platform === "win32" ? retirementTimeRemaining(deadline) : void 0);
|
|
3260
|
+
}
|
|
3261
|
+
function retirementBlockedByHeartbeat(owner, signal, requireStale) {
|
|
3262
|
+
return requireStale && owner.retirement === void 0 && signal === "SIGTERM" && !isQueueOwnerHeartbeatStale(owner);
|
|
3263
|
+
}
|
|
3264
|
+
async function prepareWindowsRetirement(owner, deadline) {
|
|
3265
|
+
if (owner.retirement === null) throw queueRetirementIncomplete();
|
|
3266
|
+
owner.retirement = await captureQueueRetirementReceipt(owner, owner.retirement, true, deadline);
|
|
3267
|
+
await replaceQueueOwnerRecord(owner);
|
|
3268
|
+
return await observeRetirementWitness(owner.retirement.root, deadline) === "matching";
|
|
3269
|
+
}
|
|
3270
|
+
async function dispatchVerifiedQueueOwnerSignal(owner, signal, requireStale, retirement) {
|
|
3271
|
+
return await withQueueLeaseMutation(owner.sessionId, async () => {
|
|
3272
|
+
const current = await readQueueOwnerRecord(owner.sessionId);
|
|
3273
|
+
retirement.leaseReleased = current?.pid !== owner.pid;
|
|
3274
|
+
if (!matchesQueueOwner(current, owner) || retirementBlockedByHeartbeat(current, signal, requireStale)) return false;
|
|
3275
|
+
if (current.retirement === null) throw queueRetirementIncomplete();
|
|
3276
|
+
const identity = await observeRetiringOwner(current, retirement.deadline);
|
|
3277
|
+
if (identity !== "matching") {
|
|
3278
|
+
retirement.gone = identity === "gone";
|
|
3279
|
+
retirement.unverified = identity === "unknown";
|
|
3280
|
+
return false;
|
|
3281
|
+
}
|
|
3282
|
+
if (process.platform === "win32" && !await prepareWindowsRetirement(current, retirement.deadline)) return false;
|
|
3283
|
+
return await dispatchQueueOwnerSignal(owner.pid, signal, retirement.deadline);
|
|
3284
|
+
});
|
|
3285
|
+
}
|
|
3286
|
+
async function retireQueueOwner(owner, requireStale, deadline) {
|
|
3287
|
+
const retirement = {
|
|
3288
|
+
gone: false,
|
|
3289
|
+
unverified: false,
|
|
3290
|
+
leaseReleased: false,
|
|
3291
|
+
deadline
|
|
3292
|
+
};
|
|
3293
|
+
await terminateWithDispatch(owner.pid, (signal) => dispatchVerifiedQueueOwnerSignal(owner, signal, requireStale, retirement), isProcessDefinitelyDead, process.platform === "win32" ? deadline : Infinity);
|
|
3294
|
+
if (await waitForReleasedOwner(owner, requireStale, retirement)) return false;
|
|
3295
|
+
const gone = retirement.gone || await observeRetiringOwner(owner, deadline) === "gone";
|
|
3296
|
+
if (retirement.unverified && !gone) throw unverifiedQueueOwnerError(owner);
|
|
3297
|
+
return gone;
|
|
3298
|
+
}
|
|
3299
|
+
async function waitForReleasedOwner(owner, requireStale, retirement) {
|
|
3300
|
+
if (retirement.unverified && requireStale) throw unverifiedQueueOwnerError(owner);
|
|
3301
|
+
if (retirement.leaseReleased || retirement.unverified) {
|
|
3302
|
+
await waitForProcessExit(owner.pid, Math.max(0, retirement.deadline - performance.now()), isProcessDefinitelyDead);
|
|
3303
|
+
if (performance.now() >= retirement.deadline) {
|
|
3304
|
+
if (retirement.unverified) throw unverifiedQueueOwnerError(owner);
|
|
3305
|
+
return true;
|
|
3306
|
+
}
|
|
3307
|
+
}
|
|
3308
|
+
return false;
|
|
3309
|
+
}
|
|
3310
|
+
async function retireRecordedDescendants(owner, deadline) {
|
|
3311
|
+
await withQueueLeaseMutation(owner.sessionId, async () => {
|
|
3312
|
+
const current = await readQueueOwnerRecord(owner.sessionId);
|
|
3313
|
+
if (!matchesQueueOwner(current, owner) || current.retirement === void 0) return;
|
|
3314
|
+
if (current.retirement === null || process.platform !== "win32") throw queueRetirementIncomplete();
|
|
3315
|
+
if (current.retirement.descendants.length === 0) return;
|
|
3316
|
+
current.retirement = await captureQueueRetirementReceipt(current, current.retirement, false, deadline);
|
|
3317
|
+
await replaceQueueOwnerRecord(current);
|
|
3318
|
+
await signalRecordedDescendants(current.retirement, deadline);
|
|
3319
|
+
await waitForRecordedDescendants(current.retirement, deadline);
|
|
3320
|
+
});
|
|
3321
|
+
}
|
|
3322
|
+
async function hasSurvivingRetirementDescendant(receipt, deadline) {
|
|
3323
|
+
for (const witness of receipt.descendants) {
|
|
3324
|
+
const state = await observeRetirementWitness(witness, deadline);
|
|
3325
|
+
if (state === "unknown") throw queueRetirementIncomplete();
|
|
3326
|
+
if (state === "matching") return true;
|
|
3327
|
+
}
|
|
3328
|
+
return false;
|
|
3329
|
+
}
|
|
3330
|
+
async function signalRecordedDescendants(receipt, deadline) {
|
|
3331
|
+
for (const witness of receipt.descendants) {
|
|
3332
|
+
const state = await observeRetirementWitness(witness, deadline);
|
|
3333
|
+
if (state === "unknown") throw queueRetirementIncomplete();
|
|
3334
|
+
if (state === "matching") {
|
|
3335
|
+
retirementTimeRemaining(deadline);
|
|
3336
|
+
try {
|
|
3337
|
+
process.kill(witness.pid, "SIGKILL");
|
|
3338
|
+
} catch (error) {
|
|
3339
|
+
if (await observeRetirementWitness(witness, deadline) !== "gone") throw error;
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3342
|
+
}
|
|
3343
|
+
}
|
|
3344
|
+
async function waitForRecordedDescendants(receipt, deadline) {
|
|
3345
|
+
while (true) {
|
|
3346
|
+
if (!await hasSurvivingRetirementDescendant(receipt, deadline)) return;
|
|
3347
|
+
await waitMs(Math.min(1e3, retirementTimeRemaining(deadline)));
|
|
3348
|
+
}
|
|
3349
|
+
}
|
|
3350
|
+
async function assertNoPendingRetirement(owner) {
|
|
3351
|
+
const current = await readQueueOwnerRecord(owner.sessionId);
|
|
3352
|
+
if (matchesQueueOwner(current, owner) && current.retirement !== void 0) throw queueRetirementIncomplete();
|
|
2586
3353
|
}
|
|
2587
3354
|
async function terminateQueueOwnerForSession(sessionId, expectedOwner, requireStale = false) {
|
|
2588
3355
|
await settlePendingQueueLeaseGuard(sessionId);
|
|
2589
3356
|
const owner = expectedOwner ?? await readQueueOwnerRecord(sessionId);
|
|
2590
3357
|
if (!owner || owner.sessionId !== sessionId) return;
|
|
2591
|
-
if (owner.pid
|
|
2592
|
-
if (
|
|
2593
|
-
return
|
|
2594
|
-
}
|
|
2595
|
-
|
|
2596
|
-
await
|
|
3358
|
+
if (owner.pid === process.pid) {
|
|
3359
|
+
if (owner.retirement !== void 0) throw queueRetirementIncomplete();
|
|
3360
|
+
return;
|
|
3361
|
+
}
|
|
3362
|
+
const deadline = retirementDeadline();
|
|
3363
|
+
if (!await retireQueueOwner(owner, requireStale, deadline)) {
|
|
3364
|
+
await assertNoPendingRetirement(owner);
|
|
3365
|
+
return;
|
|
3366
|
+
}
|
|
3367
|
+
await retireRecordedDescendants(owner, deadline);
|
|
3368
|
+
await cleanupQueueOwnerFiles(sessionId, owner.socketPath, () => ownsQueueLease(owner), void 0, deadline);
|
|
2597
3369
|
}
|
|
2598
3370
|
async function waitMs(ms) {
|
|
2599
3371
|
await new Promise((resolve) => {
|
|
@@ -2998,9 +3770,12 @@ function queueOwnerMessageParser(type) {
|
|
|
2998
3770
|
}
|
|
2999
3771
|
function parseEventOwnerMessage(message, context) {
|
|
3000
3772
|
if (!isAcpJsonRpcMessage(message.message)) return null;
|
|
3773
|
+
const direction = message.direction;
|
|
3774
|
+
if (direction !== void 0 && direction !== "inbound" && direction !== "outbound") return null;
|
|
3001
3775
|
return {
|
|
3002
3776
|
type: "event",
|
|
3003
3777
|
...context,
|
|
3778
|
+
...direction === void 0 ? {} : { direction },
|
|
3004
3779
|
message: message.message
|
|
3005
3780
|
};
|
|
3006
3781
|
}
|
|
@@ -3282,7 +4057,7 @@ function unexpectedQueueResponseError() {
|
|
|
3282
4057
|
}
|
|
3283
4058
|
function handleAcknowledgedSubmitMessage(message, controls, formatter) {
|
|
3284
4059
|
if (message.type === "event") {
|
|
3285
|
-
notifyObserver(() => formatter.onAcpMessage(message.message));
|
|
4060
|
+
notifyObserver(() => formatter.onAcpMessage(message.message, message.direction));
|
|
3286
4061
|
return;
|
|
3287
4062
|
}
|
|
3288
4063
|
if (message.type === "permission_escalation") {
|
|
@@ -3514,7 +4289,10 @@ async function tryCloseSessionOnRunningOwner(options) {
|
|
|
3514
4289
|
verbose: options.verbose,
|
|
3515
4290
|
requestName: "close_session",
|
|
3516
4291
|
logPrefix: "[acpx] requested session/close on active owner pid",
|
|
3517
|
-
submit: (owner) =>
|
|
4292
|
+
submit: (owner) => {
|
|
4293
|
+
options.onOwnerSelected?.(owner);
|
|
4294
|
+
return submitCloseSessionToQueueOwner(owner, options.timeoutMs);
|
|
4295
|
+
}
|
|
3518
4296
|
});
|
|
3519
4297
|
}
|
|
3520
4298
|
async function tryCancelOnRunningOwner(options) {
|
|
@@ -3562,6 +4340,6 @@ async function trySetConfigOptionOnRunningOwner(sessionId, configId, value, time
|
|
|
3562
4340
|
});
|
|
3563
4341
|
}
|
|
3564
4342
|
//#endregion
|
|
3565
|
-
export {
|
|
4343
|
+
export { measurePerf as $, findGitRepositoryRoot as A, PERMISSION_POLICY_ACTIONS as At, writeSessionRecord as B, PermissionDeniedError as Bt, getUnsupportedPromptContentMessage as C, AUTH_POLICIES as Ct, textPrompt as D, OUTPUT_ERROR_ORIGINS as Dt, promptToDisplayText as E, OUTPUT_ERROR_CODES as Et, listSessionsForAgent as F, AgentStartupError as Ft, normalizeAgentSessionId as G, SessionModelReplayError as Gt, parseSessionRecord as H, QueueConnectionError as Ht, normalizeName as I, AuthPolicyError as It, SessionWatchError as J, UnsupportedPromptContentError as Jt, SESSION_JOURNAL_SCHEMA as K, SessionNotFoundError as Kt, pruneSessions as L, ClaudeAcpSessionCreateTimeoutError as Lt, findSessionByDirectoryWalk as M, AcpxOperationalError as Mt, isoNow as N, AgentDisconnectedError as Nt, DEFAULT_HISTORY_LIMIT as O, OUTPUT_FORMATS as Ot, listSessions as P, AgentSpawnError as Pt, incrementPerfCounter as Q, readSessionRecord as R, CopilotAcpUnsupportedError as Rt, PromptInputValidationError as S, isAcpResourceNotFoundError as St, parsePromptSource as T, NON_INTERACTIVE_PERMISSION_POLICIES as Tt, serializeSessionRecordForDisk as U, SessionConfigOptionReplayError as Ut, assertPersistedKeyPolicy as V, PermissionPromptUnavailableError as Vt, extractAgentSessionId as W, SessionModeReplayError as Wt, formatPerfMetric as X, watchSession as Y, getPerfMetricsSnapshot as Z, waitMs as _, writePrivateFile as _t, trySetModelOnRunningOwner as a, isAcpJsonRpcMessage as at, lockOwnerPid as b, extractAcpError as bt, queueRequestExceedsLimit as c, parseJsonRpcErrorMessage as ct, readQueueOwnerRecord as d, DEFAULT_EVENT_SEGMENT_MAX_BYTES as dt, recordPerfDuration as et, refreshQueueOwnerLease as f, defaultSessionEventLog as ft, tryAcquireQueueOwnerLease as g, sessionEventSegmentPath as gt, terminateQueueOwnerForSession as h, sessionEventLockPath as ht, trySetModeOnRunningOwner as i, extractSessionUpdateNotification as it, findSession as j, SESSION_RECORD_SCHEMA as jt, absolutePath as k, PERMISSION_MODES as kt, parseQueueRequest as l, parsePermissionNotice as lt, terminateProcess as m, sessionEventActivePath as mt, tryCloseSessionOnRunningOwner as n, setPerfGauge as nt, trySubmitToRunningOwner as o, isAcpMessageObject as ot, releaseQueueOwnerLease as p, sessionBaseDir as pt, SessionJournalReader as q, SessionResumeRequiredError as qt, trySetConfigOptionOnRunningOwner as r, startPerfTimer as rt, queueRequestByteLimit as s, isSessionUpdateNotification as st, tryCancelOnRunningOwner as t, resetPerfMetrics as tt, probeQueueOwnerHealth as u, parsePromptStopReason as ut, QueueLeaseGuardSettlementError as v, writePrivateJsonFile as vt, mergePromptSourceWithText as w, EXIT_CODES as wt, ProcessDescendants as x, formatUnknownErrorMessage as xt, createLockOwner as y, isProcessAlive as yt, resolveSessionRecord as z, GeminiAcpStartupTimeoutError as zt };
|
|
3566
4344
|
|
|
3567
|
-
//# sourceMappingURL=ipc-
|
|
4345
|
+
//# sourceMappingURL=ipc-DAApJ5mi.js.map
|