acpx 0.17.1 → 0.19.0
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-evUoJINz.js → agent-registry-Ct2yWPW7.js} +18 -18
- package/dist/agent-registry-Ct2yWPW7.js.map +1 -0
- package/dist/agent-registry.d.ts.map +1 -1
- package/dist/agent-registry.js +1 -1
- package/dist/{cli-B2ycKzaO.js → cli-Bmki9cj5.js} +5 -5
- package/dist/{cli-B2ycKzaO.js.map → cli-Bmki9cj5.js.map} +1 -1
- package/dist/cli.js +167 -131
- package/dist/cli.js.map +1 -1
- package/dist/{client-CkbBoSGd.d.ts → client-DzaUKazo.d.ts} +23 -15
- package/dist/client-DzaUKazo.d.ts.map +1 -0
- package/dist/{flows-xGyhmoHO.js → flows--Ewxn_fQ.js} +665 -460
- package/dist/flows--Ewxn_fQ.js.map +1 -0
- package/dist/flows.d.ts +19 -7
- package/dist/flows.d.ts.map +1 -1
- package/dist/flows.js +1 -1
- package/dist/{invocation-options-eZEBh4vd.js → invocation-options-DRzxHZmr.js} +34 -12
- package/dist/invocation-options-DRzxHZmr.js.map +1 -0
- package/dist/{ipc-D2rstexT.js → ipc-fMgj5vrO.js} +818 -302
- package/dist/ipc-fMgj5vrO.js.map +1 -0
- package/dist/{output-D2xL6wdp.js → output-D6kDs8BN.js} +30 -14
- package/dist/output-D6kDs8BN.js.map +1 -0
- package/dist/{queue-owner-runtime-Dgv3e2Tp.js → queue-owner-runtime-CLwjMYNL.js} +1448 -717
- package/dist/queue-owner-runtime-CLwjMYNL.js.map +1 -0
- package/dist/runtime.d.ts +57 -3
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +303 -166
- package/dist/runtime.js.map +1 -1
- package/dist/session-options-Dp6mzNSy.d.ts.map +1 -1
- package/dist/{session-watch-CTe9Bnbc.js → session-watch-BfdIo-n4.js} +5 -5
- package/dist/session-watch-BfdIo-n4.js.map +1 -0
- package/dist/{watch-DxuXEc8n.js → watch-DH-zXQH2.js} +2 -2
- package/dist/{watch-DxuXEc8n.js.map → watch-DH-zXQH2.js.map} +1 -1
- package/package.json +1 -1
- package/skills/acpx/SKILL.md +69 -30
- package/dist/agent-registry-evUoJINz.js.map +0 -1
- package/dist/client-CkbBoSGd.d.ts.map +0 -1
- package/dist/flows-xGyhmoHO.js.map +0 -1
- package/dist/invocation-options-eZEBh4vd.js.map +0 -1
- package/dist/ipc-D2rstexT.js.map +0 -1
- package/dist/output-D2xL6wdp.js.map +0 -1
- package/dist/queue-owner-runtime-Dgv3e2Tp.js.map +0 -1
- package/dist/session-watch-CTe9Bnbc.js.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as AGENT_REGISTRY, r as BUILT_IN_AGENT_PACKAGES, t as AGENT_ARGV_REGISTRY } from "./agent-registry-
|
|
1
|
+
import { M as runTimedExecFile, n as AGENT_REGISTRY, r as BUILT_IN_AGENT_PACKAGES, t as AGENT_ARGV_REGISTRY } from "./agent-registry-Ct2yWPW7.js";
|
|
2
2
|
import { statSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import fs$1 from "node:fs/promises";
|
|
@@ -9,6 +9,7 @@ import { root } from "@openclaw/fs-safe";
|
|
|
9
9
|
import { z } from "zod";
|
|
10
10
|
import { createHash, randomInt, randomUUID } from "node:crypto";
|
|
11
11
|
import { isHardlinkFallbackError } from "@openclaw/fs-safe/durability";
|
|
12
|
+
import { acquireFileLock } from "@openclaw/fs-safe/file-lock";
|
|
12
13
|
import net from "node:net";
|
|
13
14
|
//#region src/errors.ts
|
|
14
15
|
var AcpxOperationalError = class extends Error {
|
|
@@ -216,6 +217,7 @@ const OUTPUT_ERROR_CODES = [
|
|
|
216
217
|
"TIMEOUT",
|
|
217
218
|
"PERMISSION_DENIED",
|
|
218
219
|
"PERMISSION_PROMPT_UNAVAILABLE",
|
|
220
|
+
"EXEC_DISABLED",
|
|
219
221
|
"RUNTIME",
|
|
220
222
|
"USAGE"
|
|
221
223
|
];
|
|
@@ -229,12 +231,12 @@ const SESSION_RECORD_SCHEMA = "acpx.session.v1";
|
|
|
229
231
|
//#endregion
|
|
230
232
|
//#region src/acp/error-shapes.ts
|
|
231
233
|
const RESOURCE_NOT_FOUND_ACP_CODES = /* @__PURE__ */ new Set([-32001, -32002]);
|
|
232
|
-
function asRecord$
|
|
234
|
+
function asRecord$4(value) {
|
|
233
235
|
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
234
236
|
return value;
|
|
235
237
|
}
|
|
236
238
|
function toAcpErrorPayload(value) {
|
|
237
|
-
const record = asRecord$
|
|
239
|
+
const record = asRecord$4(value);
|
|
238
240
|
if (!record) return;
|
|
239
241
|
if (typeof record.code !== "number" || !Number.isFinite(record.code)) return;
|
|
240
242
|
if (typeof record.message !== "string" || record.message.length === 0) return;
|
|
@@ -248,7 +250,7 @@ function extractAcpErrorInternal(value, depth) {
|
|
|
248
250
|
if (depth > 5) return;
|
|
249
251
|
const direct = toAcpErrorPayload(value);
|
|
250
252
|
if (direct) return direct;
|
|
251
|
-
const record = asRecord$
|
|
253
|
+
const record = asRecord$4(value);
|
|
252
254
|
if (!record) return;
|
|
253
255
|
return extractNestedAcpError(record, depth);
|
|
254
256
|
}
|
|
@@ -283,7 +285,7 @@ function hasSessionNotFoundHint(value, depth = 0) {
|
|
|
283
285
|
if (depth > 4) return false;
|
|
284
286
|
if (isSessionNotFoundText(value)) return true;
|
|
285
287
|
if (Array.isArray(value)) return value.some((entry) => hasSessionNotFoundHint(entry, depth + 1));
|
|
286
|
-
const record = asRecord$
|
|
288
|
+
const record = asRecord$4(value);
|
|
287
289
|
if (!record) return false;
|
|
288
290
|
return Object.values(record).some((entry) => hasSessionNotFoundHint(entry, depth + 1));
|
|
289
291
|
}
|
|
@@ -310,6 +312,15 @@ function isProcessAlive(pid) {
|
|
|
310
312
|
return false;
|
|
311
313
|
}
|
|
312
314
|
}
|
|
315
|
+
function isProcessDefinitelyDead(pid) {
|
|
316
|
+
if (!pid || !Number.isSafeInteger(pid) || pid <= 0 || pid === process.pid) return false;
|
|
317
|
+
try {
|
|
318
|
+
process.kill(pid, 0);
|
|
319
|
+
return false;
|
|
320
|
+
} catch (error) {
|
|
321
|
+
return error.code === "ESRCH";
|
|
322
|
+
}
|
|
323
|
+
}
|
|
313
324
|
//#endregion
|
|
314
325
|
//#region \0@oxc-project+runtime@0.149.0/helpers/esm/usingCtx.js
|
|
315
326
|
function _usingCtx() {
|
|
@@ -430,18 +441,18 @@ function defaultSessionEventLog(sessionId) {
|
|
|
430
441
|
}
|
|
431
442
|
//#endregion
|
|
432
443
|
//#region src/acp/jsonrpc.ts
|
|
433
|
-
function asRecord$
|
|
444
|
+
function asRecord$3(value) {
|
|
434
445
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
435
446
|
return value;
|
|
436
447
|
}
|
|
437
448
|
function isAcpMessageObject(value) {
|
|
438
|
-
return asRecord$
|
|
449
|
+
return asRecord$3(value) !== null;
|
|
439
450
|
}
|
|
440
451
|
function hasValidId(value) {
|
|
441
452
|
return value === null || typeof value === "string" || typeof value === "number" && Number.isFinite(value);
|
|
442
453
|
}
|
|
443
454
|
function isErrorObject(value) {
|
|
444
|
-
const record = asRecord$
|
|
455
|
+
const record = asRecord$3(value);
|
|
445
456
|
return !!record && typeof record.code === "number" && Number.isFinite(record.code) && typeof record.message === "string";
|
|
446
457
|
}
|
|
447
458
|
function hasResultOrError(value) {
|
|
@@ -466,7 +477,7 @@ function isJsonRpcResponse(value) {
|
|
|
466
477
|
return hasResultOrError(value);
|
|
467
478
|
}
|
|
468
479
|
function isAcpJsonRpcMessage(value) {
|
|
469
|
-
const record = asRecord$
|
|
480
|
+
const record = asRecord$3(value);
|
|
470
481
|
if (!record || record.jsonrpc !== "2.0") return false;
|
|
471
482
|
return isJsonRpcNotificationRecord(record) || isJsonRpcRequest(record) || isJsonRpcResponse(record);
|
|
472
483
|
}
|
|
@@ -476,14 +487,29 @@ function isJsonRpcNotification(message) {
|
|
|
476
487
|
function isSessionUpdateNotification(message) {
|
|
477
488
|
return isJsonRpcNotification(message) && message.method === "session/update";
|
|
478
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
|
+
}
|
|
479
505
|
function extractSessionUpdateNotification(message) {
|
|
480
506
|
if (!isSessionUpdateNotification(message)) return;
|
|
481
|
-
const params = asRecord$
|
|
507
|
+
const params = asRecord$3(message.params);
|
|
482
508
|
if (!params) return;
|
|
483
509
|
const sessionId = typeof params.sessionId === "string" ? params.sessionId : null;
|
|
484
510
|
if (!sessionId) return;
|
|
485
|
-
const update = asRecord$
|
|
486
|
-
if (!update ||
|
|
511
|
+
const update = asRecord$3(params.update);
|
|
512
|
+
if (!update || !sessionUpdateHasRequiredFields(update)) return;
|
|
487
513
|
return {
|
|
488
514
|
sessionId,
|
|
489
515
|
update
|
|
@@ -491,18 +517,18 @@ function extractSessionUpdateNotification(message) {
|
|
|
491
517
|
}
|
|
492
518
|
function parsePromptStopReason(message) {
|
|
493
519
|
if (!Object.hasOwn(message, "id") || !Object.hasOwn(message, "result")) return;
|
|
494
|
-
const record = asRecord$
|
|
520
|
+
const record = asRecord$3(message.result);
|
|
495
521
|
if (!record) return;
|
|
496
522
|
return typeof record.stopReason === "string" ? record.stopReason : void 0;
|
|
497
523
|
}
|
|
498
524
|
function parsePermissionNotice(message) {
|
|
499
525
|
if (!Object.hasOwn(message, "id") || !Object.hasOwn(message, "result")) return;
|
|
500
|
-
const notice = asRecord$
|
|
526
|
+
const notice = asRecord$3(asRecord$3(asRecord$3(message.result)?._meta)?.acpx)?.permissionNotice;
|
|
501
527
|
return typeof notice === "string" ? notice : void 0;
|
|
502
528
|
}
|
|
503
529
|
function parseJsonRpcErrorMessage(message) {
|
|
504
530
|
if (!Object.hasOwn(message, "error")) return;
|
|
505
|
-
const errorRecord = asRecord$
|
|
531
|
+
const errorRecord = asRecord$3(message.error);
|
|
506
532
|
if (!errorRecord || typeof errorRecord.message !== "string") return;
|
|
507
533
|
return errorRecord.message;
|
|
508
534
|
}
|
|
@@ -1098,7 +1124,7 @@ function resolveAgentArgvForCommand(agentCommand) {
|
|
|
1098
1124
|
}
|
|
1099
1125
|
//#endregion
|
|
1100
1126
|
//#region src/session/persistence/parse.ts
|
|
1101
|
-
function asRecord$
|
|
1127
|
+
function asRecord$2(value) {
|
|
1102
1128
|
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
1103
1129
|
return value;
|
|
1104
1130
|
}
|
|
@@ -1117,12 +1143,12 @@ function parsePersistedAgentArgv(record) {
|
|
|
1117
1143
|
function hasModelConfigOption(options) {
|
|
1118
1144
|
if (!Array.isArray(options)) return false;
|
|
1119
1145
|
return options.some((entry) => {
|
|
1120
|
-
const option = asRecord$
|
|
1146
|
+
const option = asRecord$2(entry);
|
|
1121
1147
|
return option?.category === "model" || option?.id === "model";
|
|
1122
1148
|
});
|
|
1123
1149
|
}
|
|
1124
1150
|
function parseConfigOptions(raw) {
|
|
1125
|
-
if (!Array.isArray(raw) || !raw.every((entry) => asRecord$
|
|
1151
|
+
if (!Array.isArray(raw) || !raw.every((entry) => asRecord$2(entry) !== void 0)) return;
|
|
1126
1152
|
return raw;
|
|
1127
1153
|
}
|
|
1128
1154
|
function parseAvailableCommand(raw) {
|
|
@@ -1130,7 +1156,7 @@ function parseAvailableCommand(raw) {
|
|
|
1130
1156
|
const name = raw.trim();
|
|
1131
1157
|
return name ? { name } : void 0;
|
|
1132
1158
|
}
|
|
1133
|
-
const record = asRecord$
|
|
1159
|
+
const record = asRecord$2(raw);
|
|
1134
1160
|
if (!record) return;
|
|
1135
1161
|
const name = parseNonEmptyString$1(record.name);
|
|
1136
1162
|
if (!name) return;
|
|
@@ -1153,7 +1179,7 @@ function parseNonEmptyString$1(value) {
|
|
|
1153
1179
|
}
|
|
1154
1180
|
function parseTokenUsage(raw) {
|
|
1155
1181
|
if (raw === void 0 || raw === null) return;
|
|
1156
|
-
const record = asRecord$
|
|
1182
|
+
const record = asRecord$2(raw);
|
|
1157
1183
|
if (!record) return null;
|
|
1158
1184
|
const usage = {};
|
|
1159
1185
|
for (const field of [
|
|
@@ -1176,7 +1202,7 @@ function isNonNegativeFiniteNumber(value) {
|
|
|
1176
1202
|
}
|
|
1177
1203
|
function parseUsageCost(raw) {
|
|
1178
1204
|
if (raw === void 0 || raw === null) return;
|
|
1179
|
-
const record = asRecord$
|
|
1205
|
+
const record = asRecord$2(raw);
|
|
1180
1206
|
if (!record) return null;
|
|
1181
1207
|
return parseUsageCostRecord(record);
|
|
1182
1208
|
}
|
|
@@ -1202,7 +1228,7 @@ function parseCostCurrency(value) {
|
|
|
1202
1228
|
}
|
|
1203
1229
|
function parseRequestTokenUsage(raw) {
|
|
1204
1230
|
if (raw === void 0 || raw === null) return;
|
|
1205
|
-
const record = asRecord$
|
|
1231
|
+
const record = asRecord$2(raw);
|
|
1206
1232
|
if (!record) return null;
|
|
1207
1233
|
const usage = {};
|
|
1208
1234
|
for (const [key, value] of Object.entries(record)) {
|
|
@@ -1213,25 +1239,25 @@ function parseRequestTokenUsage(raw) {
|
|
|
1213
1239
|
return usage;
|
|
1214
1240
|
}
|
|
1215
1241
|
function isSessionMessageImage(raw) {
|
|
1216
|
-
const record = asRecord$
|
|
1242
|
+
const record = asRecord$2(raw);
|
|
1217
1243
|
if (!record || typeof record.source !== "string") return false;
|
|
1218
1244
|
if (record.size === void 0 || record.size === null) return true;
|
|
1219
|
-
const size = asRecord$
|
|
1245
|
+
const size = asRecord$2(record.size);
|
|
1220
1246
|
return !!size && isFiniteNumber(size.width) && isFiniteNumber(size.height);
|
|
1221
1247
|
}
|
|
1222
1248
|
function isSessionMessageAudio(raw) {
|
|
1223
|
-
const record = asRecord$
|
|
1249
|
+
const record = asRecord$2(raw);
|
|
1224
1250
|
return !!record && typeof record.source === "string" && typeof record.mime_type === "string";
|
|
1225
1251
|
}
|
|
1226
1252
|
function isFiniteNumber(value) {
|
|
1227
1253
|
return typeof value === "number" && Number.isFinite(value);
|
|
1228
1254
|
}
|
|
1229
1255
|
function isUserContent(raw) {
|
|
1230
|
-
const record = asRecord$
|
|
1256
|
+
const record = asRecord$2(raw);
|
|
1231
1257
|
if (!record) return false;
|
|
1232
1258
|
if (typeof record.Text === "string") return true;
|
|
1233
1259
|
if (record.Mention !== void 0) {
|
|
1234
|
-
const mention = asRecord$
|
|
1260
|
+
const mention = asRecord$2(record.Mention);
|
|
1235
1261
|
return !!mention && typeof mention.uri === "string" && typeof mention.content === "string";
|
|
1236
1262
|
}
|
|
1237
1263
|
if (record.Image !== void 0) return isSessionMessageImage(record.Image);
|
|
@@ -1239,7 +1265,7 @@ function isUserContent(raw) {
|
|
|
1239
1265
|
return false;
|
|
1240
1266
|
}
|
|
1241
1267
|
function isToolUse(raw) {
|
|
1242
|
-
const record = asRecord$
|
|
1268
|
+
const record = asRecord$2(raw);
|
|
1243
1269
|
return !!record && hasStringFields$1(record, [
|
|
1244
1270
|
"id",
|
|
1245
1271
|
"name",
|
|
@@ -1253,18 +1279,18 @@ function isOptionalString(value) {
|
|
|
1253
1279
|
return value === void 0 || value === null || typeof value === "string";
|
|
1254
1280
|
}
|
|
1255
1281
|
function isToolResultContent(raw) {
|
|
1256
|
-
const record = asRecord$
|
|
1282
|
+
const record = asRecord$2(raw);
|
|
1257
1283
|
if (!record) return false;
|
|
1258
1284
|
if (typeof record.Text === "string") return true;
|
|
1259
1285
|
if (record.Image !== void 0) return isSessionMessageImage(record.Image);
|
|
1260
1286
|
return false;
|
|
1261
1287
|
}
|
|
1262
1288
|
function isToolResult(raw) {
|
|
1263
|
-
const record = asRecord$
|
|
1289
|
+
const record = asRecord$2(raw);
|
|
1264
1290
|
return !!record && typeof record.tool_use_id === "string" && typeof record.tool_name === "string" && typeof record.is_error === "boolean" && isToolResultContent(record.content);
|
|
1265
1291
|
}
|
|
1266
1292
|
function isAgentContent(raw) {
|
|
1267
|
-
const record = asRecord$
|
|
1293
|
+
const record = asRecord$2(raw);
|
|
1268
1294
|
if (!record) return false;
|
|
1269
1295
|
if (typeof record.Text === "string") return true;
|
|
1270
1296
|
if (record.Thinking !== void 0) return isThinkingContent(record.Thinking);
|
|
@@ -1273,21 +1299,21 @@ function isAgentContent(raw) {
|
|
|
1273
1299
|
return false;
|
|
1274
1300
|
}
|
|
1275
1301
|
function isThinkingContent(raw) {
|
|
1276
|
-
const thinking = asRecord$
|
|
1302
|
+
const thinking = asRecord$2(raw);
|
|
1277
1303
|
return !!thinking && typeof thinking.text === "string" && isOptionalString(thinking.signature);
|
|
1278
1304
|
}
|
|
1279
1305
|
function isUserMessage(raw) {
|
|
1280
|
-
const record = asRecord$
|
|
1306
|
+
const record = asRecord$2(raw);
|
|
1281
1307
|
if (!record || record.User === void 0) return false;
|
|
1282
|
-
const user = asRecord$
|
|
1308
|
+
const user = asRecord$2(record.User);
|
|
1283
1309
|
return !!user && typeof user.id === "string" && Array.isArray(user.content) && user.content.every((entry) => isUserContent(entry));
|
|
1284
1310
|
}
|
|
1285
1311
|
function isAgentMessage(raw) {
|
|
1286
|
-
const record = asRecord$
|
|
1312
|
+
const record = asRecord$2(raw);
|
|
1287
1313
|
if (!record || record.Agent === void 0) return false;
|
|
1288
|
-
const agent = asRecord$
|
|
1314
|
+
const agent = asRecord$2(record.Agent);
|
|
1289
1315
|
if (!agent || !Array.isArray(agent.content) || !agent.content.every(isAgentContent)) return false;
|
|
1290
|
-
const toolResults = asRecord$
|
|
1316
|
+
const toolResults = asRecord$2(agent.tool_results);
|
|
1291
1317
|
if (!toolResults) return false;
|
|
1292
1318
|
return Object.values(toolResults).every(isToolResult);
|
|
1293
1319
|
}
|
|
@@ -1320,7 +1346,7 @@ function hasValidConversationCore(record) {
|
|
|
1320
1346
|
return Array.isArray(record.messages) && record.messages.every(isConversationMessage) && typeof record.updated_at === "string";
|
|
1321
1347
|
}
|
|
1322
1348
|
function parseAcpxState(raw) {
|
|
1323
|
-
const record = asRecord$
|
|
1349
|
+
const record = asRecord$2(raw);
|
|
1324
1350
|
if (!record) return;
|
|
1325
1351
|
const state = {};
|
|
1326
1352
|
if (record.reset_on_next_ensure === true) state.reset_on_next_ensure = true;
|
|
@@ -1348,13 +1374,13 @@ function assignStringState(state, key, value) {
|
|
|
1348
1374
|
if (typeof value === "string") state[key] = value;
|
|
1349
1375
|
}
|
|
1350
1376
|
function parseStringMap(raw, preserveEmpty = false) {
|
|
1351
|
-
const record = asRecord$
|
|
1377
|
+
const record = asRecord$2(raw);
|
|
1352
1378
|
if (!record) return;
|
|
1353
1379
|
const parsed = Object.fromEntries(Object.entries(record).filter((entry) => typeof entry[1] === "string"));
|
|
1354
1380
|
return Object.keys(parsed).length > 0 || preserveEmpty ? parsed : void 0;
|
|
1355
1381
|
}
|
|
1356
1382
|
function assignParsedSessionOptions(state, raw) {
|
|
1357
|
-
const sessionOptions = asRecord$
|
|
1383
|
+
const sessionOptions = asRecord$2(raw);
|
|
1358
1384
|
if (!sessionOptions) return;
|
|
1359
1385
|
const parsedSessionOptions = {};
|
|
1360
1386
|
if (typeof sessionOptions.model === "string") parsedSessionOptions.model = sessionOptions.model;
|
|
@@ -1370,11 +1396,11 @@ function assignSessionOptionSystemPrompt(options, value) {
|
|
|
1370
1396
|
options.system_prompt = value;
|
|
1371
1397
|
return;
|
|
1372
1398
|
}
|
|
1373
|
-
const appendRecord = asRecord$
|
|
1399
|
+
const appendRecord = asRecord$2(value);
|
|
1374
1400
|
if (appendRecord && typeof appendRecord.append === "string" && appendRecord.append.length > 0) options.system_prompt = { append: appendRecord.append };
|
|
1375
1401
|
}
|
|
1376
1402
|
function parseEventLog(raw, sessionId) {
|
|
1377
|
-
const record = asRecord$
|
|
1403
|
+
const record = asRecord$2(raw);
|
|
1378
1404
|
if (!record || !hasValidEventLogCore(record)) return defaultSessionEventLog(sessionId);
|
|
1379
1405
|
return {
|
|
1380
1406
|
active_path: record.active_path,
|
|
@@ -1393,7 +1419,7 @@ function isPositiveInteger$1(value) {
|
|
|
1393
1419
|
}
|
|
1394
1420
|
function parseImportedFrom(raw) {
|
|
1395
1421
|
if (raw == null) return;
|
|
1396
|
-
const record = asRecord$
|
|
1422
|
+
const record = asRecord$2(raw);
|
|
1397
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;
|
|
1398
1424
|
return {
|
|
1399
1425
|
recordId: record.record_id,
|
|
@@ -1443,7 +1469,7 @@ function normalizeOptionalSignal(value) {
|
|
|
1443
1469
|
return Symbol("invalid");
|
|
1444
1470
|
}
|
|
1445
1471
|
function parseSessionRecord(raw) {
|
|
1446
|
-
const record = asRecord$
|
|
1472
|
+
const record = asRecord$2(raw);
|
|
1447
1473
|
if (!record) return null;
|
|
1448
1474
|
if (record.schema !== "acpx.session.v1") return null;
|
|
1449
1475
|
const optionals = validSessionOptionals({
|
|
@@ -1488,7 +1514,7 @@ function parseSessionRecord(raw) {
|
|
|
1488
1514
|
lastAgentExitAt: optionals.lastAgentExitAt,
|
|
1489
1515
|
lastAgentDisconnectReason: optionals.lastAgentDisconnectReason,
|
|
1490
1516
|
protocolVersion: typeof record.protocol_version === "number" ? record.protocol_version : void 0,
|
|
1491
|
-
agentCapabilities: asRecord$
|
|
1517
|
+
agentCapabilities: asRecord$2(record.agent_capabilities),
|
|
1492
1518
|
title: conversation.title,
|
|
1493
1519
|
messages: conversation.messages,
|
|
1494
1520
|
updated_at: conversation.updated_at,
|
|
@@ -1601,111 +1627,28 @@ function assertPersistedKeyPolicy(value) {
|
|
|
1601
1627
|
throw new Error(`Persisted key policy violation (expected snake_case keys): ${violations.join(", ")}`);
|
|
1602
1628
|
}
|
|
1603
1629
|
//#endregion
|
|
1604
|
-
//#region src/session/persistence/
|
|
1605
|
-
const
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
file: record.file,
|
|
1617
|
-
acpxRecordId: record.acpxRecordId,
|
|
1618
|
-
acpSessionId: record.acpSessionId,
|
|
1619
|
-
agentCommand: record.agentCommand,
|
|
1620
|
-
cwd: record.cwd,
|
|
1621
|
-
name: record.name,
|
|
1622
|
-
closed: record.closed,
|
|
1623
|
-
lastUsedAt: record.lastUsedAt
|
|
1624
|
-
};
|
|
1625
|
-
}
|
|
1626
|
-
function hasRequiredIndexEntryFields(record) {
|
|
1627
|
-
return [
|
|
1628
|
-
"file",
|
|
1629
|
-
"acpxRecordId",
|
|
1630
|
-
"acpSessionId",
|
|
1631
|
-
"agentCommand",
|
|
1632
|
-
"cwd",
|
|
1633
|
-
"lastUsedAt"
|
|
1634
|
-
].every((key) => typeof record[key] === "string") && typeof record.closed === "boolean";
|
|
1635
|
-
}
|
|
1636
|
-
function sessionIndexPath(sessionDir) {
|
|
1637
|
-
return path.join(sessionDir, "index.json");
|
|
1638
|
-
}
|
|
1639
|
-
function toSessionIndexEntry(record, fileName) {
|
|
1640
|
-
return {
|
|
1641
|
-
file: fileName,
|
|
1642
|
-
acpxRecordId: record.acpxRecordId,
|
|
1643
|
-
acpSessionId: record.acpSessionId,
|
|
1644
|
-
agentCommand: record.agentCommand,
|
|
1645
|
-
cwd: record.cwd,
|
|
1646
|
-
name: record.name,
|
|
1647
|
-
closed: record.closed === true,
|
|
1648
|
-
lastUsedAt: record.lastUsedAt
|
|
1649
|
-
};
|
|
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
|
+
}
|
|
1650
1642
|
}
|
|
1651
|
-
async function
|
|
1652
|
-
const filePath = sessionIndexPath(sessionDir);
|
|
1643
|
+
async function readRecord(file) {
|
|
1653
1644
|
try {
|
|
1654
|
-
const payload = await fs$1.readFile(
|
|
1655
|
-
const record =
|
|
1656
|
-
|
|
1657
|
-
const files = record.files.filter((entry) => typeof entry === "string");
|
|
1658
|
-
if (files.length !== record.files.length || !Array.isArray(record.entries)) return;
|
|
1659
|
-
const entries = record.entries.map((entry) => parseIndexEntry(entry)).filter((entry) => Boolean(entry));
|
|
1660
|
-
if (entries.length !== record.entries.length) return;
|
|
1661
|
-
return {
|
|
1662
|
-
schema: SESSION_INDEX_SCHEMA,
|
|
1663
|
-
files,
|
|
1664
|
-
entries
|
|
1665
|
-
};
|
|
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;
|
|
1666
1648
|
} catch {
|
|
1667
1649
|
return;
|
|
1668
1650
|
}
|
|
1669
1651
|
}
|
|
1670
|
-
async function writeSessionIndex(sessionDir, index) {
|
|
1671
|
-
await writePrivateJsonFile(sessionIndexPath(sessionDir), {
|
|
1672
|
-
schema: SESSION_INDEX_SCHEMA,
|
|
1673
|
-
files: index.files.toSorted(),
|
|
1674
|
-
entries: index.entries.toSorted((a, b) => b.lastUsedAt.localeCompare(a.lastUsedAt))
|
|
1675
|
-
});
|
|
1676
|
-
}
|
|
1677
|
-
async function rebuildSessionIndex(sessionDir) {
|
|
1678
|
-
const index = await scanSessionIndex(sessionDir);
|
|
1679
|
-
await writeSessionIndex(sessionDir, index);
|
|
1680
|
-
return index;
|
|
1681
|
-
}
|
|
1682
|
-
async function scanSessionIndex(sessionDir) {
|
|
1683
|
-
const files = await listSessionFiles(sessionDir).catch((error) => {
|
|
1684
|
-
if (error.code === "ENOENT") return [];
|
|
1685
|
-
throw error;
|
|
1686
|
-
});
|
|
1687
|
-
const indexEntries = [];
|
|
1688
|
-
for (const file of files) try {
|
|
1689
|
-
const payload = await fs$1.readFile(path.join(sessionDir, file), "utf8");
|
|
1690
|
-
const parsed = parseSessionRecord(JSON.parse(payload));
|
|
1691
|
-
if (!parsed) continue;
|
|
1692
|
-
indexEntries.push(toSessionIndexEntry(parsed, file));
|
|
1693
|
-
} catch {}
|
|
1694
|
-
return {
|
|
1695
|
-
schema: SESSION_INDEX_SCHEMA,
|
|
1696
|
-
files,
|
|
1697
|
-
entries: indexEntries.toSorted((a, b) => b.lastUsedAt.localeCompare(a.lastUsedAt))
|
|
1698
|
-
};
|
|
1699
|
-
}
|
|
1700
|
-
async function loadOrRebuildSessionIndex(sessionDir) {
|
|
1701
|
-
const files = await listSessionFiles(sessionDir);
|
|
1702
|
-
const existing = await readSessionIndex(sessionDir);
|
|
1703
|
-
if (existing && existing.files.length === files.length && existing.files.every((file, index) => file === files[index])) return existing;
|
|
1704
|
-
return await rebuildSessionIndex(sessionDir);
|
|
1705
|
-
}
|
|
1706
|
-
async function listSessionFiles(sessionDir) {
|
|
1707
|
-
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();
|
|
1708
|
-
}
|
|
1709
1652
|
//#endregion
|
|
1710
1653
|
//#region src/session/persistence/repository.ts
|
|
1711
1654
|
const DEFAULT_HISTORY_LIMIT = 20;
|
|
@@ -1719,62 +1662,56 @@ async function ensureSessionDir() {
|
|
|
1719
1662
|
mode: 448
|
|
1720
1663
|
});
|
|
1721
1664
|
}
|
|
1722
|
-
async function
|
|
1723
|
-
try {
|
|
1724
|
-
const payload = await fs$1.readFile(path.join(sessionBaseDir(), entry.file), "utf8");
|
|
1725
|
-
return parseSessionRecord(JSON.parse(payload)) ?? void 0;
|
|
1726
|
-
} catch {
|
|
1727
|
-
return;
|
|
1728
|
-
}
|
|
1729
|
-
}
|
|
1730
|
-
async function loadSessionIndexEntries(readOnly = false) {
|
|
1665
|
+
async function* sessionRecords(readOnly = false) {
|
|
1731
1666
|
if (!readOnly) await ensureSessionDir();
|
|
1732
|
-
|
|
1733
|
-
return await (readOnly ? scanSessionIndex : loadOrRebuildSessionIndex)(sessionBaseDir());
|
|
1734
|
-
})).entries;
|
|
1667
|
+
yield* scanSessionRecords(sessionBaseDir());
|
|
1735
1668
|
}
|
|
1736
|
-
function
|
|
1737
|
-
if (session.
|
|
1669
|
+
function matchesSession(session, agentCommand, normalizedName, includeClosed = false) {
|
|
1670
|
+
if (session.agentCommand !== agentCommand) return false;
|
|
1738
1671
|
if (!includeClosed && session.closed) return false;
|
|
1739
1672
|
if (normalizedName == null) return session.name == null;
|
|
1740
1673
|
return session.name === normalizedName;
|
|
1741
1674
|
}
|
|
1675
|
+
function isNewer(record, previous) {
|
|
1676
|
+
return !previous || record.lastUsedAt.localeCompare(previous.lastUsedAt) > 0;
|
|
1677
|
+
}
|
|
1742
1678
|
async function writeSessionRecord(record) {
|
|
1743
1679
|
await measurePerf("session.write_record", async () => {
|
|
1744
1680
|
const persisted = serializeSessionRecordForDisk(record);
|
|
1745
1681
|
assertPersistedKeyPolicy(persisted);
|
|
1746
|
-
|
|
1747
|
-
await writePrivateJsonFile(file, persisted);
|
|
1748
|
-
const sessionDir = sessionBaseDir();
|
|
1749
|
-
const index = await loadOrRebuildSessionIndex(sessionDir);
|
|
1750
|
-
const fileName = path.basename(file);
|
|
1751
|
-
const entries = index.entries.filter((entry) => entry.file !== fileName);
|
|
1752
|
-
entries.push(toSessionIndexEntry(record, fileName));
|
|
1753
|
-
await writeSessionIndex(sessionDir, {
|
|
1754
|
-
files: [.../* @__PURE__ */ new Set([...index.files.filter((entry) => entry !== fileName), fileName])],
|
|
1755
|
-
entries
|
|
1756
|
-
});
|
|
1682
|
+
await writePrivateJsonFile(sessionFilePath(record.acpxRecordId), persisted);
|
|
1757
1683
|
});
|
|
1758
1684
|
}
|
|
1759
1685
|
async function resolveSessionRecord(sessionId) {
|
|
1760
1686
|
await ensureSessionDir();
|
|
1761
1687
|
try {
|
|
1762
1688
|
const directRecord = await measurePerf("session.resolve_direct", async () => await readSessionRecord(sessionId));
|
|
1763
|
-
if (directRecord) return directRecord;
|
|
1689
|
+
if (directRecord?.acpxRecordId === sessionId) return directRecord;
|
|
1764
1690
|
} catch {}
|
|
1765
|
-
const
|
|
1766
|
-
const exactEntries = entries.filter((entry) => entry.acpxRecordId === sessionId || entry.acpSessionId === sessionId);
|
|
1767
|
-
const exactRecords = (await Promise.all(exactEntries.map((entry) => loadRecordFromIndexEntry(entry)))).filter((entry) => Boolean(entry));
|
|
1691
|
+
const { exactRecords, suffixRecords } = await findSessionIdMatches(sessionId);
|
|
1768
1692
|
if (exactRecords.length === 1) return exactRecords[0];
|
|
1769
1693
|
if (exactRecords.length > 1) throw new SessionResolutionError(`Multiple sessions match id: ${sessionId}`);
|
|
1770
|
-
const suffixEntries = entries.filter((entry) => entry.acpxRecordId.endsWith(sessionId) || entry.acpSessionId.endsWith(sessionId));
|
|
1771
|
-
const suffixRecords = (await Promise.all(suffixEntries.map((entry) => loadRecordFromIndexEntry(entry)))).filter((entry) => Boolean(entry));
|
|
1772
1694
|
if (suffixRecords.length === 1) return suffixRecords[0];
|
|
1773
1695
|
if (suffixRecords.length > 1) throw new SessionResolutionError(`Session id is ambiguous: ${sessionId}`);
|
|
1774
1696
|
incrementPerfCounter("session.resolve_miss");
|
|
1775
1697
|
throw new SessionNotFoundError(sessionId);
|
|
1776
1698
|
}
|
|
1777
|
-
|
|
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. */
|
|
1778
1715
|
async function readSessionRecord(sessionId) {
|
|
1779
1716
|
let payload;
|
|
1780
1717
|
try {
|
|
@@ -1784,7 +1721,8 @@ async function readSessionRecord(sessionId) {
|
|
|
1784
1721
|
throw error;
|
|
1785
1722
|
}
|
|
1786
1723
|
try {
|
|
1787
|
-
|
|
1724
|
+
const record = parseSessionRecord(JSON.parse(payload));
|
|
1725
|
+
return record?.acpxRecordId === sessionId ? record : void 0;
|
|
1788
1726
|
} catch {
|
|
1789
1727
|
return;
|
|
1790
1728
|
}
|
|
@@ -1824,42 +1762,49 @@ function isoNow() {
|
|
|
1824
1762
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
1825
1763
|
}
|
|
1826
1764
|
async function listSessions() {
|
|
1827
|
-
await
|
|
1828
|
-
const entries = await loadSessionIndexEntries();
|
|
1829
|
-
const records = [];
|
|
1830
|
-
for (const entry of entries) {
|
|
1831
|
-
const parsed = await loadRecordFromIndexEntry(entry);
|
|
1832
|
-
if (parsed) records.push(parsed);
|
|
1833
|
-
}
|
|
1834
|
-
records.sort((a, b) => b.lastUsedAt.localeCompare(a.lastUsedAt));
|
|
1835
|
-
return records;
|
|
1765
|
+
return await collectSessionRecords();
|
|
1836
1766
|
}
|
|
1837
1767
|
async function listSessionsForAgent(agentCommand) {
|
|
1838
|
-
|
|
1839
|
-
|
|
1768
|
+
return await collectSessionRecords(agentCommand);
|
|
1769
|
+
}
|
|
1770
|
+
async function collectSessionRecords(agentCommand) {
|
|
1771
|
+
const records = [];
|
|
1772
|
+
for await (const record of sessionRecords()) if (agentCommand === void 0 || record.agentCommand === agentCommand) records.push(record);
|
|
1773
|
+
return records.toSorted((a, b) => b.lastUsedAt.localeCompare(a.lastUsedAt));
|
|
1840
1774
|
}
|
|
1841
1775
|
async function findSession(options) {
|
|
1842
1776
|
const normalizedCwd = absolutePath(options.cwd);
|
|
1843
1777
|
const normalizedName = normalizeName(options.name);
|
|
1844
|
-
|
|
1845
|
-
if (
|
|
1846
|
-
return
|
|
1778
|
+
let match;
|
|
1779
|
+
for await (const record of sessionRecords(options.readOnly)) if (record.cwd === normalizedCwd && matchesSession(record, options.agentCommand, normalizedName, options.includeClosed) && isNewer(record, match)) match = record;
|
|
1780
|
+
return match;
|
|
1847
1781
|
}
|
|
1848
1782
|
async function findSessionByDirectoryWalk(options) {
|
|
1849
1783
|
const normalizedName = normalizeName(options.name);
|
|
1784
|
+
const directories = walkDirectories(options);
|
|
1785
|
+
let match;
|
|
1786
|
+
let distance = Infinity;
|
|
1787
|
+
for await (const record of sessionRecords()) {
|
|
1788
|
+
const candidateDistance = directories.get(record.cwd);
|
|
1789
|
+
if (candidateDistance !== void 0 && matchesSession(record, options.agentCommand, normalizedName) && (candidateDistance < distance || candidateDistance === distance && isNewer(record, match))) {
|
|
1790
|
+
match = record;
|
|
1791
|
+
distance = candidateDistance;
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
return match;
|
|
1795
|
+
}
|
|
1796
|
+
function walkDirectories(options) {
|
|
1850
1797
|
const normalizedStart = absolutePath(options.cwd);
|
|
1851
1798
|
const normalizedBoundary = absolutePath(options.boundary ?? normalizedStart);
|
|
1852
1799
|
const walkBoundary = isWithinBoundary(normalizedBoundary, normalizedStart) ? normalizedBoundary : normalizedStart;
|
|
1853
|
-
const
|
|
1800
|
+
const directories = /* @__PURE__ */ new Map();
|
|
1801
|
+
const walkRoot = path.parse(normalizedStart).root;
|
|
1854
1802
|
let current = normalizedStart;
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
if (match) return await loadRecordFromIndexEntry(match);
|
|
1859
|
-
const parent = nextWalkParent(current, walkBoundary, walkRoot);
|
|
1860
|
-
if (!parent) return;
|
|
1861
|
-
current = parent;
|
|
1803
|
+
while (current) {
|
|
1804
|
+
directories.set(current, directories.size);
|
|
1805
|
+
current = nextWalkParent(current, walkBoundary, walkRoot);
|
|
1862
1806
|
}
|
|
1807
|
+
return directories;
|
|
1863
1808
|
}
|
|
1864
1809
|
function nextWalkParent(current, walkBoundary, walkRoot) {
|
|
1865
1810
|
if (current === walkBoundary || current === walkRoot) return;
|
|
@@ -1871,11 +1816,11 @@ function closedAtOrLastUsedAt(record) {
|
|
|
1871
1816
|
return record.closedAt ?? record.lastUsedAt;
|
|
1872
1817
|
}
|
|
1873
1818
|
function isSessionStreamFile(fileName, safeId) {
|
|
1874
|
-
|
|
1819
|
+
const segmentPrefix = `${safeId}.stream.`;
|
|
1820
|
+
return fileName === `${safeId}.stream.ndjson` || fileName === `${safeId}.stream.lock` || fileName.startsWith(segmentPrefix) && /^\d+\.ndjson$/.test(fileName.slice(segmentPrefix.length));
|
|
1875
1821
|
}
|
|
1876
1822
|
async function pruneSessions(options = {}) {
|
|
1877
|
-
await
|
|
1878
|
-
const records = await loadPrunableRecords(filterPruneCandidates(await loadSessionIndexEntries(), options.agentCommand), options.before ?? (options.olderThanMs != null ? new Date(Date.now() - options.olderThanMs) : void 0));
|
|
1823
|
+
const records = await loadPrunableRecords(options.before ?? (options.olderThanMs != null ? new Date(Date.now() - options.olderThanMs) : void 0), options.agentCommand);
|
|
1879
1824
|
if (options.dryRun) return {
|
|
1880
1825
|
pruned: records,
|
|
1881
1826
|
bytesFreed: 0,
|
|
@@ -1888,24 +1833,20 @@ async function pruneSessions(options = {}) {
|
|
|
1888
1833
|
dirEntries = await fs$1.readdir(sessionDir);
|
|
1889
1834
|
} catch {}
|
|
1890
1835
|
for (const record of records) bytesFreed += await pruneSessionFiles(record, sessionDir, dirEntries, options.includeHistory === true);
|
|
1891
|
-
await rebuildSessionIndex(sessionDir).catch(() => {});
|
|
1892
1836
|
return {
|
|
1893
1837
|
pruned: records,
|
|
1894
1838
|
bytesFreed,
|
|
1895
1839
|
dryRun: false
|
|
1896
1840
|
};
|
|
1897
1841
|
}
|
|
1898
|
-
function
|
|
1899
|
-
return
|
|
1842
|
+
function isPruneCandidate(record, agentCommand) {
|
|
1843
|
+
return record.closed === true && (!agentCommand || record.agentCommand === agentCommand);
|
|
1900
1844
|
}
|
|
1901
|
-
async function loadPrunableRecords(
|
|
1845
|
+
async function loadPrunableRecords(cutoff, agentCommand) {
|
|
1902
1846
|
const records = [];
|
|
1903
1847
|
const cutoffIso = cutoff?.toISOString();
|
|
1904
|
-
for (const
|
|
1905
|
-
|
|
1906
|
-
if (record && isBeforeCutoff(record, cutoffIso)) records.push(record);
|
|
1907
|
-
}
|
|
1908
|
-
return records;
|
|
1848
|
+
for await (const record of sessionRecords()) if (isPruneCandidate(record, agentCommand) && isBeforeCutoff(record, cutoffIso)) records.push(record);
|
|
1849
|
+
return records.toSorted((a, b) => b.lastUsedAt.localeCompare(a.lastUsedAt));
|
|
1909
1850
|
}
|
|
1910
1851
|
function isBeforeCutoff(record, cutoffIso) {
|
|
1911
1852
|
return !cutoffIso || closedAtOrLastUsedAt(record) < cutoffIso;
|
|
@@ -1963,12 +1904,12 @@ function isAudioBlock(value) {
|
|
|
1963
1904
|
}
|
|
1964
1905
|
function isResourceLinkBlock(value) {
|
|
1965
1906
|
const record = asRecord$1(value);
|
|
1966
|
-
return record?.type === "resource_link" && isNonEmptyString(record.uri) && (record.title
|
|
1907
|
+
return record?.type === "resource_link" && isNonEmptyString(record.uri) && (record.title == null || typeof record.title === "string") && typeof record.name === "string";
|
|
1967
1908
|
}
|
|
1968
1909
|
function isResourcePayload(value) {
|
|
1969
1910
|
const record = asRecord$1(value);
|
|
1970
1911
|
if (!record || !isNonEmptyString(record.uri)) return false;
|
|
1971
|
-
return record.text ===
|
|
1912
|
+
return typeof record.text === "string" || typeof record.blob === "string";
|
|
1972
1913
|
}
|
|
1973
1914
|
function isResourceBlock(value) {
|
|
1974
1915
|
const record = asRecord$1(value);
|
|
@@ -2011,12 +1952,12 @@ function validateAudioContentBlock(record, index) {
|
|
|
2011
1952
|
}
|
|
2012
1953
|
function validateResourceLinkContentBlock(record, index) {
|
|
2013
1954
|
if (!isNonEmptyString(record.uri)) return `prompt[${index}] resource_link block must include a non-empty uri`;
|
|
2014
|
-
if (record.title
|
|
2015
|
-
if (
|
|
1955
|
+
if (record.title != null && typeof record.title !== "string") return `prompt[${index}] resource_link block title must be a string or null when present`;
|
|
1956
|
+
if (typeof record.name !== "string") return `prompt[${index}] resource_link block must include a string name`;
|
|
2016
1957
|
}
|
|
2017
1958
|
function validateResourceContentBlock(record, index) {
|
|
2018
1959
|
if (!asRecord$1(record.resource)) return `prompt[${index}] resource block must include a resource object`;
|
|
2019
|
-
return isResourcePayload(record.resource) ? void 0 : `prompt[${index}] resource block resource must include a non-empty uri and
|
|
1960
|
+
return isResourcePayload(record.resource) ? void 0 : `prompt[${index}] resource block resource must include a non-empty uri and a string text or blob field`;
|
|
2020
1961
|
}
|
|
2021
1962
|
function getContentBlockValidationError(value, index) {
|
|
2022
1963
|
const record = asRecord$1(value);
|
|
@@ -2101,6 +2042,318 @@ function resourceBlockDisplayText(block) {
|
|
|
2101
2042
|
return "text" in block.resource && typeof block.resource.text === "string" ? block.resource.text : block.resource.uri;
|
|
2102
2043
|
}
|
|
2103
2044
|
//#endregion
|
|
2045
|
+
//#region src/process-identity.ts
|
|
2046
|
+
const IDENTITY_QUERY_TIMEOUT_MS$1 = 2e3;
|
|
2047
|
+
const LINUX_PROCESS_QUERY = `
|
|
2048
|
+
const fs = require('node:fs/promises');
|
|
2049
|
+
const [targetPid, observerPid] = process.argv.slice(1);
|
|
2050
|
+
if (process.ppid !== Number(observerPid)) process.exit(1);
|
|
2051
|
+
const timeNamespace = (pid) => fs.readlink('/proc/' + pid + '/ns/time').catch((error) => {
|
|
2052
|
+
if (error.code === 'ENOENT') return 'unsupported';
|
|
2053
|
+
throw error;
|
|
2054
|
+
});
|
|
2055
|
+
Promise.all([
|
|
2056
|
+
fs.readFile('/proc/sys/kernel/random/boot_id', 'utf8'),
|
|
2057
|
+
fs.readlink('/proc/self/ns/pid'),
|
|
2058
|
+
timeNamespace('self'),
|
|
2059
|
+
fs.readlink('/proc/' + observerPid + '/ns/pid'),
|
|
2060
|
+
timeNamespace(observerPid),
|
|
2061
|
+
fs.readFile('/proc/self/stat', 'utf8'),
|
|
2062
|
+
fs.readFile('/proc/' + targetPid + '/stat', 'utf8').catch(() => null),
|
|
2063
|
+
]).then(([bootId, pidNamespace, timeNamespace, observerPidNamespace,
|
|
2064
|
+
observerTimeNamespace, selfStat, targetStat]) => {
|
|
2065
|
+
process.stdout.write(JSON.stringify({bootId: bootId.trim(), pidNamespace,
|
|
2066
|
+
timeNamespace, observerPidNamespace, observerTimeNamespace,
|
|
2067
|
+
helperPid: process.pid, selfStat, targetStat}));
|
|
2068
|
+
}).catch(() => { process.exitCode = 1; });
|
|
2069
|
+
`;
|
|
2070
|
+
const POSIX_MONTHS = [
|
|
2071
|
+
"Jan",
|
|
2072
|
+
"Feb",
|
|
2073
|
+
"Mar",
|
|
2074
|
+
"Apr",
|
|
2075
|
+
"May",
|
|
2076
|
+
"Jun",
|
|
2077
|
+
"Jul",
|
|
2078
|
+
"Aug",
|
|
2079
|
+
"Sep",
|
|
2080
|
+
"Oct",
|
|
2081
|
+
"Nov",
|
|
2082
|
+
"Dec"
|
|
2083
|
+
];
|
|
2084
|
+
let ownIdentity;
|
|
2085
|
+
let ownIdentityPending;
|
|
2086
|
+
function parseProcessBirthIdentity(raw) {
|
|
2087
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return;
|
|
2088
|
+
const identity = raw;
|
|
2089
|
+
if (identity.kind === "linux-proc") return parseLinuxIdentity(identity);
|
|
2090
|
+
if (identity.kind !== "posix-lstart" && identity.kind !== "windows-creation") return;
|
|
2091
|
+
if (!isCanonicalBirth(identity.value, identity.kind)) return;
|
|
2092
|
+
return {
|
|
2093
|
+
kind: identity.kind,
|
|
2094
|
+
value: identity.value
|
|
2095
|
+
};
|
|
2096
|
+
}
|
|
2097
|
+
function isCanonicalBirth(value, kind) {
|
|
2098
|
+
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;
|
|
2099
|
+
const canonical = new Date(value).toISOString();
|
|
2100
|
+
return canonical.slice(0, 19) === value.slice(0, 19) && (kind === "posix-lstart" ? value === canonical && value.endsWith(".000Z") : /\.\d{7}Z$/.test(value));
|
|
2101
|
+
}
|
|
2102
|
+
function parseLinuxScope(raw) {
|
|
2103
|
+
const { bootId, pidNamespace, timeNamespace } = raw;
|
|
2104
|
+
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;
|
|
2105
|
+
return {
|
|
2106
|
+
bootId,
|
|
2107
|
+
pidNamespace,
|
|
2108
|
+
timeNamespace
|
|
2109
|
+
};
|
|
2110
|
+
}
|
|
2111
|
+
function isTimeNamespace(value) {
|
|
2112
|
+
return typeof value === "string" && (value === "unsupported" || /^time:\[[1-9]\d*\]$/.test(value));
|
|
2113
|
+
}
|
|
2114
|
+
function isStartTicks(value) {
|
|
2115
|
+
return typeof value === "string" && /^(?:0|[1-9]\d*)$/.test(value);
|
|
2116
|
+
}
|
|
2117
|
+
function parseLinuxIdentity(raw) {
|
|
2118
|
+
const scope = parseLinuxScope(raw);
|
|
2119
|
+
if (!scope || !isStartTicks(raw.startTicks)) return;
|
|
2120
|
+
return {
|
|
2121
|
+
kind: "linux-proc",
|
|
2122
|
+
...scope,
|
|
2123
|
+
startTicks: raw.startTicks
|
|
2124
|
+
};
|
|
2125
|
+
}
|
|
2126
|
+
function compareLinuxScope(expected, observed) {
|
|
2127
|
+
if (expected.bootId !== observed.bootId) return "different";
|
|
2128
|
+
if (expected.pidNamespace !== observed.pidNamespace || expected.timeNamespace !== observed.timeNamespace) return "unknown";
|
|
2129
|
+
return "matching";
|
|
2130
|
+
}
|
|
2131
|
+
function compareLinuxIdentity(expected, observed) {
|
|
2132
|
+
const scope = compareLinuxScope(expected, observed);
|
|
2133
|
+
return scope === "matching" ? expected.startTicks === observed.startTicks ? "matching" : "different" : scope;
|
|
2134
|
+
}
|
|
2135
|
+
function compareProcessBirthIdentity(expected, observed) {
|
|
2136
|
+
if (!expected || expected.kind !== observed.kind) return "unknown";
|
|
2137
|
+
if (expected.kind === "linux-proc" && observed.kind === "linux-proc") return compareLinuxIdentity(expected, observed);
|
|
2138
|
+
if ("value" in expected && "value" in observed) return expected.value === observed.value ? "matching" : "different";
|
|
2139
|
+
return "unknown";
|
|
2140
|
+
}
|
|
2141
|
+
function parseLinuxStat(raw) {
|
|
2142
|
+
if (typeof raw !== "string") return;
|
|
2143
|
+
const prefix = /^(\d+) \(/.exec(raw);
|
|
2144
|
+
const close = raw.lastIndexOf(")");
|
|
2145
|
+
if (!prefix || close < 0) return;
|
|
2146
|
+
const pid = Number(prefix[1]);
|
|
2147
|
+
const fields = raw.slice(close + 2).trim().split(/\s+/u);
|
|
2148
|
+
const startTicks = fields[19];
|
|
2149
|
+
if (!isPositivePid(pid) || !isStartTicks(startTicks) || !/^[RSDTtKWPI]$/.test(fields[0] ?? "")) return;
|
|
2150
|
+
return {
|
|
2151
|
+
pid,
|
|
2152
|
+
startTicks
|
|
2153
|
+
};
|
|
2154
|
+
}
|
|
2155
|
+
async function readLinuxObservation(pid, timeoutMs) {
|
|
2156
|
+
const output = await runTimedExecFile(process.execPath, [
|
|
2157
|
+
"--input-type=commonjs",
|
|
2158
|
+
"-e",
|
|
2159
|
+
LINUX_PROCESS_QUERY,
|
|
2160
|
+
String(pid),
|
|
2161
|
+
String(process.pid)
|
|
2162
|
+
], {
|
|
2163
|
+
timeoutMs,
|
|
2164
|
+
maxBufferBytes: 8192,
|
|
2165
|
+
env: {
|
|
2166
|
+
...process.env,
|
|
2167
|
+
NODE_OPTIONS: ""
|
|
2168
|
+
}
|
|
2169
|
+
});
|
|
2170
|
+
const raw = JSON.parse(output);
|
|
2171
|
+
const scope = parseLinuxScope(raw);
|
|
2172
|
+
const self = parseLinuxStat(raw.selfStat);
|
|
2173
|
+
if (!scope || !self || self.pid !== raw.helperPid || scope.pidNamespace !== raw.observerPidNamespace || scope.timeNamespace !== raw.observerTimeNamespace) return;
|
|
2174
|
+
const target = parseLinuxStat(raw.targetStat);
|
|
2175
|
+
return {
|
|
2176
|
+
scope,
|
|
2177
|
+
...target?.pid === pid ? { identity: {
|
|
2178
|
+
kind: "linux-proc",
|
|
2179
|
+
...scope,
|
|
2180
|
+
startTicks: target.startTicks
|
|
2181
|
+
} } : {}
|
|
2182
|
+
};
|
|
2183
|
+
}
|
|
2184
|
+
function isPositivePid(pid) {
|
|
2185
|
+
return Number.isSafeInteger(pid) && pid > 0;
|
|
2186
|
+
}
|
|
2187
|
+
function windowsPowerShellPath() {
|
|
2188
|
+
const systemRoot = process.env.SystemRoot;
|
|
2189
|
+
if (!systemRoot || !path.win32.isAbsolute(systemRoot)) throw new Error("Windows system directory is unavailable");
|
|
2190
|
+
return path.win32.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
|
|
2191
|
+
}
|
|
2192
|
+
function windowsProcessQuery(pid) {
|
|
2193
|
+
return [
|
|
2194
|
+
"$ErrorActionPreference = 'Stop'",
|
|
2195
|
+
`Get-CimInstance -ClassName Win32_Process -Property ProcessId,ParentProcessId,CreationDate${pid === void 0 ? "" : ` -Filter 'ProcessId = ${pid}'`} | ForEach-Object {`,
|
|
2196
|
+
"if ($null -ne $_.CreationDate) {",
|
|
2197
|
+
"'{0} {1} 0 S {2}' -f $_.ProcessId,$_.ParentProcessId,$_.CreationDate.ToUniversalTime().ToString('o')",
|
|
2198
|
+
"}",
|
|
2199
|
+
"}"
|
|
2200
|
+
].join("\n");
|
|
2201
|
+
}
|
|
2202
|
+
function posixBirth(value) {
|
|
2203
|
+
const match = /^\w{3}\s+(\w{3})\s+(\d{1,2})\s+(\d{2}):(\d{2}):(\d{2})\s+(\d{4})$/.exec(value);
|
|
2204
|
+
if (!match) return;
|
|
2205
|
+
const month = POSIX_MONTHS.indexOf(match[1]);
|
|
2206
|
+
if (month < 0) return;
|
|
2207
|
+
const valueUtc = `${match[6]}-${String(month + 1).padStart(2, "0")}-${match[2].padStart(2, "0")}T${match[3]}:${match[4]}:${match[5]}.000Z`;
|
|
2208
|
+
return isCanonicalBirth(valueUtc, "posix-lstart") ? valueUtc : void 0;
|
|
2209
|
+
}
|
|
2210
|
+
function parseProcessTable(output) {
|
|
2211
|
+
const table = /* @__PURE__ */ new Map();
|
|
2212
|
+
for (const line of output.split("\n")) {
|
|
2213
|
+
const match = /^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(.+?)\s*$/.exec(line);
|
|
2214
|
+
if (!match || match[4].startsWith("Z")) continue;
|
|
2215
|
+
const pid = Number(match[1]);
|
|
2216
|
+
const birth = processTableBirth(match[5]);
|
|
2217
|
+
if (Number.isSafeInteger(pid) && pid > 0 && birth) table.set(pid, {
|
|
2218
|
+
pid,
|
|
2219
|
+
parentPid: Number(match[2]),
|
|
2220
|
+
groupPid: Number(match[3]),
|
|
2221
|
+
birth
|
|
2222
|
+
});
|
|
2223
|
+
}
|
|
2224
|
+
return table;
|
|
2225
|
+
}
|
|
2226
|
+
function processTableBirth(value) {
|
|
2227
|
+
if (process.platform !== "win32") return posixBirth(value);
|
|
2228
|
+
return isCanonicalBirth(value, "windows-creation") ? value : void 0;
|
|
2229
|
+
}
|
|
2230
|
+
async function readProcessTable(timeoutMs, pid) {
|
|
2231
|
+
if (pid !== void 0 && (!Number.isSafeInteger(pid) || pid <= 0)) throw new Error("Process identity requires a positive PID");
|
|
2232
|
+
if (process.platform === "win32") return parseProcessTable(await runTimedExecFile(windowsPowerShellPath(), [
|
|
2233
|
+
"-NoLogo",
|
|
2234
|
+
"-NoProfile",
|
|
2235
|
+
"-NonInteractive",
|
|
2236
|
+
"-Command",
|
|
2237
|
+
windowsProcessQuery(pid)
|
|
2238
|
+
], {
|
|
2239
|
+
timeoutMs,
|
|
2240
|
+
windowsHide: true
|
|
2241
|
+
}));
|
|
2242
|
+
return parseProcessTable(await runTimedExecFile("ps", [
|
|
2243
|
+
...pid === void 0 ? ["-e"] : ["-p", String(pid)],
|
|
2244
|
+
"-o",
|
|
2245
|
+
"pid=,ppid=,pgid=,stat=,lstart="
|
|
2246
|
+
], {
|
|
2247
|
+
timeoutMs,
|
|
2248
|
+
env: {
|
|
2249
|
+
...process.env,
|
|
2250
|
+
LC_ALL: "C",
|
|
2251
|
+
TZ: "UTC"
|
|
2252
|
+
}
|
|
2253
|
+
}));
|
|
2254
|
+
}
|
|
2255
|
+
async function probeProcessIdentity(pid, timeoutMs = IDENTITY_QUERY_TIMEOUT_MS$1) {
|
|
2256
|
+
if (!isPositivePid(pid)) return { state: "unknown" };
|
|
2257
|
+
if (isProcessDefinitelyDead(pid)) return { state: "dead" };
|
|
2258
|
+
try {
|
|
2259
|
+
const identity = await readNativeIdentity(pid, timeoutMs);
|
|
2260
|
+
if (identity) return {
|
|
2261
|
+
state: "alive",
|
|
2262
|
+
identity
|
|
2263
|
+
};
|
|
2264
|
+
} catch {}
|
|
2265
|
+
return { state: isProcessDefinitelyDead(pid) ? "dead" : "unknown" };
|
|
2266
|
+
}
|
|
2267
|
+
async function readNativeIdentity(pid, timeoutMs) {
|
|
2268
|
+
if (process.platform === "linux") return (await readLinuxObservation(pid, timeoutMs))?.identity;
|
|
2269
|
+
const entry = (await readProcessTable(timeoutMs, pid)).get(pid);
|
|
2270
|
+
return entry ? processBirthIdentity(entry.birth) : void 0;
|
|
2271
|
+
}
|
|
2272
|
+
function processBirthIdentity(value) {
|
|
2273
|
+
return {
|
|
2274
|
+
kind: process.platform === "win32" ? "windows-creation" : "posix-lstart",
|
|
2275
|
+
value
|
|
2276
|
+
};
|
|
2277
|
+
}
|
|
2278
|
+
function nativeIdentityKind() {
|
|
2279
|
+
if (process.platform === "linux") return "linux-proc";
|
|
2280
|
+
return process.platform === "win32" ? "windows-creation" : "posix-lstart";
|
|
2281
|
+
}
|
|
2282
|
+
function comparisonToIncarnation(comparison) {
|
|
2283
|
+
return comparison === "different" ? "gone" : comparison;
|
|
2284
|
+
}
|
|
2285
|
+
async function observeLinuxIncarnation(pid, expected, timeoutMs) {
|
|
2286
|
+
const observed = await readLinuxObservation(pid, timeoutMs).catch(() => void 0);
|
|
2287
|
+
if (!observed) return "unknown";
|
|
2288
|
+
const scope = compareLinuxScope(expected, observed.scope);
|
|
2289
|
+
if (scope !== "matching") return comparisonToIncarnation(scope);
|
|
2290
|
+
if (isProcessDefinitelyDead(pid)) return "gone";
|
|
2291
|
+
return observed.identity ? comparisonToIncarnation(compareProcessBirthIdentity(expected, observed.identity)) : "unknown";
|
|
2292
|
+
}
|
|
2293
|
+
async function observeProcessIncarnation(pid, expected, timeoutMs = IDENTITY_QUERY_TIMEOUT_MS$1) {
|
|
2294
|
+
if (!isPositivePid(pid)) return "unknown";
|
|
2295
|
+
if (expected?.kind === "linux-proc") return process.platform === "linux" ? await observeLinuxIncarnation(pid, expected, timeoutMs) : "unknown";
|
|
2296
|
+
if (expected && expected.kind !== nativeIdentityKind()) return "unknown";
|
|
2297
|
+
return await observeTimestampIncarnation(pid, expected, timeoutMs);
|
|
2298
|
+
}
|
|
2299
|
+
async function observeTimestampIncarnation(pid, expected, timeoutMs) {
|
|
2300
|
+
if (isProcessDefinitelyDead(pid)) return "gone";
|
|
2301
|
+
if (!expected) return "unknown";
|
|
2302
|
+
const observed = await probeProcessIdentity(pid, timeoutMs);
|
|
2303
|
+
if (observed.state === "dead") return "gone";
|
|
2304
|
+
return observed.state === "alive" ? comparisonToIncarnation(compareProcessBirthIdentity(expected, observed.identity)) : "unknown";
|
|
2305
|
+
}
|
|
2306
|
+
async function getOwnProcessIdentity() {
|
|
2307
|
+
if (ownIdentity) return ownIdentity;
|
|
2308
|
+
ownIdentityPending ??= probeProcessIdentity(process.pid).then((probe) => {
|
|
2309
|
+
if (probe.state === "alive") ownIdentity = probe.identity;
|
|
2310
|
+
return ownIdentity;
|
|
2311
|
+
}).finally(() => {
|
|
2312
|
+
ownIdentityPending = void 0;
|
|
2313
|
+
});
|
|
2314
|
+
return await ownIdentityPending;
|
|
2315
|
+
}
|
|
2316
|
+
//#endregion
|
|
2317
|
+
//#region src/session/lock-owner.ts
|
|
2318
|
+
const OBSERVATION_REUSE_MS = 1e3;
|
|
2319
|
+
const IDENTITY_QUERY_TIMEOUT_MS = 1e3;
|
|
2320
|
+
function lockOwnerPid(value) {
|
|
2321
|
+
if (!value || typeof value !== "object" || !("pid" in value)) return;
|
|
2322
|
+
const pid = value.pid;
|
|
2323
|
+
return typeof pid === "number" && Number.isSafeInteger(pid) && pid > 0 ? pid : void 0;
|
|
2324
|
+
}
|
|
2325
|
+
function canReuseObservation(preserved, pid, identity, fresh) {
|
|
2326
|
+
return !fresh && preserved?.pid === pid && compareProcessBirthIdentity(preserved.identity, identity) === "matching" && performance.now() - preserved.checkedAt < OBSERVATION_REUSE_MS;
|
|
2327
|
+
}
|
|
2328
|
+
async function createLockOwner(options = {}) {
|
|
2329
|
+
const { signal, capturedIdentity } = options;
|
|
2330
|
+
signal?.throwIfAborted();
|
|
2331
|
+
const processIdentity = capturedIdentity === void 0 ? await getOwnProcessIdentity() : capturedIdentity.processIdentity;
|
|
2332
|
+
signal?.throwIfAborted();
|
|
2333
|
+
let preserved;
|
|
2334
|
+
return {
|
|
2335
|
+
payload: {
|
|
2336
|
+
pid: process.pid,
|
|
2337
|
+
processIdentity
|
|
2338
|
+
},
|
|
2339
|
+
async hasExited(payload, fresh, signal) {
|
|
2340
|
+
signal?.throwIfAborted();
|
|
2341
|
+
const pid = lockOwnerPid(payload);
|
|
2342
|
+
if (!pid) return false;
|
|
2343
|
+
const identity = parseProcessBirthIdentity(payload.processIdentity);
|
|
2344
|
+
if (identity && canReuseObservation(preserved, pid, identity, fresh)) return false;
|
|
2345
|
+
const incarnation = await observeProcessIncarnation(pid, identity, IDENTITY_QUERY_TIMEOUT_MS);
|
|
2346
|
+
signal?.throwIfAborted();
|
|
2347
|
+
if (identity && incarnation !== "gone") preserved = {
|
|
2348
|
+
pid,
|
|
2349
|
+
identity,
|
|
2350
|
+
checkedAt: performance.now()
|
|
2351
|
+
};
|
|
2352
|
+
return incarnation === "gone";
|
|
2353
|
+
}
|
|
2354
|
+
};
|
|
2355
|
+
}
|
|
2356
|
+
//#endregion
|
|
2104
2357
|
//#region src/session/queue/paths.ts
|
|
2105
2358
|
function shortHash(value, length) {
|
|
2106
2359
|
return createHash("sha256").update(value).digest("hex").slice(0, length);
|
|
@@ -2124,6 +2377,156 @@ function queueSocketPath(sessionId, homeDir = os.homedir()) {
|
|
|
2124
2377
|
return path.join(queueSocketBaseDir(homeDir) ?? "/tmp", `${key}.sock`);
|
|
2125
2378
|
}
|
|
2126
2379
|
//#endregion
|
|
2380
|
+
//#region src/session/queue/lease-mutation.ts
|
|
2381
|
+
const GUARD_WAIT_MS = 2e3;
|
|
2382
|
+
const localMutations = /* @__PURE__ */ new Map();
|
|
2383
|
+
var QueueLeaseGuardSettlementError = class extends QueueConnectionError {
|
|
2384
|
+
constructor(errors) {
|
|
2385
|
+
super("Queue lease guard settlement failed; cleanup remains retryable", {
|
|
2386
|
+
detailCode: "QUEUE_LEASE_GUARD_SETTLEMENT_FAILED",
|
|
2387
|
+
origin: "queue",
|
|
2388
|
+
retryable: false,
|
|
2389
|
+
cause: new AggregateError(errors, "Queue lease mutation and settlement errors")
|
|
2390
|
+
});
|
|
2391
|
+
}
|
|
2392
|
+
};
|
|
2393
|
+
async function canonicalLeasePath(sessionId) {
|
|
2394
|
+
const requested = queueLockFilePath(sessionId);
|
|
2395
|
+
try {
|
|
2396
|
+
return path.join(await fs$1.realpath(path.dirname(requested)), path.basename(requested));
|
|
2397
|
+
} catch (error) {
|
|
2398
|
+
if (error.code === "ENOENT") return path.resolve(requested);
|
|
2399
|
+
throw error;
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
async function acquireGuard(lockPath, owner) {
|
|
2403
|
+
return await acquireFileLock(lockPath, {
|
|
2404
|
+
managerKey: "acpx.queue-lease-mutation",
|
|
2405
|
+
lockPath: `${lockPath}.guard`,
|
|
2406
|
+
staleMs: Infinity,
|
|
2407
|
+
timeoutMs: GUARD_WAIT_MS,
|
|
2408
|
+
retry: {
|
|
2409
|
+
minTimeout: 5,
|
|
2410
|
+
maxTimeout: 15,
|
|
2411
|
+
factor: 1,
|
|
2412
|
+
randomize: false
|
|
2413
|
+
},
|
|
2414
|
+
staleRecovery: "remove-if-unchanged",
|
|
2415
|
+
payload: () => owner.payload,
|
|
2416
|
+
shouldReclaim: ({ payload }) => owner.hasExited(payload),
|
|
2417
|
+
shouldRemoveStaleLock: ({ payload }) => owner.hasExited(payload, true)
|
|
2418
|
+
});
|
|
2419
|
+
}
|
|
2420
|
+
function matchesReservation(value, expected) {
|
|
2421
|
+
if (!value || typeof value !== "object") return false;
|
|
2422
|
+
const record = value;
|
|
2423
|
+
return record.pid === expected.pid && record.sessionId === expected.sessionId && record.ownerGeneration === expected.ownerGeneration;
|
|
2424
|
+
}
|
|
2425
|
+
async function rollbackReservation(lockPath, expected) {
|
|
2426
|
+
try {
|
|
2427
|
+
if (!(await fs$1.lstat(lockPath)).isFile()) return;
|
|
2428
|
+
const raw = await fs$1.readFile(lockPath, "utf8");
|
|
2429
|
+
let value;
|
|
2430
|
+
try {
|
|
2431
|
+
value = JSON.parse(raw);
|
|
2432
|
+
} catch {
|
|
2433
|
+
return;
|
|
2434
|
+
}
|
|
2435
|
+
if (matchesReservation(value, expected)) await fs$1.unlink(lockPath);
|
|
2436
|
+
} catch (error) {
|
|
2437
|
+
if (error.code !== "ENOENT") throw error;
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
async function settleReceipt(lockPath, receipt) {
|
|
2441
|
+
if (receipt.reservation) {
|
|
2442
|
+
if (!await receipt.guard.verifyStillHeld()) {
|
|
2443
|
+
await receipt.guard.release();
|
|
2444
|
+
receipt.guard = await acquireGuard(lockPath, receipt.owner);
|
|
2445
|
+
}
|
|
2446
|
+
await rollbackReservation(lockPath, receipt.reservation);
|
|
2447
|
+
receipt.reservation = void 0;
|
|
2448
|
+
}
|
|
2449
|
+
await receipt.guard.release();
|
|
2450
|
+
}
|
|
2451
|
+
async function settleFailedGuard(lockPath, state) {
|
|
2452
|
+
const failed = state.failed;
|
|
2453
|
+
if (!failed) return;
|
|
2454
|
+
try {
|
|
2455
|
+
await settleReceipt(lockPath, failed);
|
|
2456
|
+
if (state.failed === failed) state.failed = void 0;
|
|
2457
|
+
} catch (error) {
|
|
2458
|
+
throw new QueueLeaseGuardSettlementError([error]);
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
async function locallySerialized(lockPath, run) {
|
|
2462
|
+
let state = localMutations.get(lockPath);
|
|
2463
|
+
if (!state) {
|
|
2464
|
+
state = { tail: Promise.resolve() };
|
|
2465
|
+
localMutations.set(lockPath, state);
|
|
2466
|
+
}
|
|
2467
|
+
const current = state;
|
|
2468
|
+
const operation = current.tail.then(async () => await run(current));
|
|
2469
|
+
const tail = operation.then(() => {}, () => {});
|
|
2470
|
+
current.tail = tail;
|
|
2471
|
+
try {
|
|
2472
|
+
return await operation;
|
|
2473
|
+
} finally {
|
|
2474
|
+
if (current.tail === tail && !current.failed && localMutations.get(lockPath) === current) localMutations.delete(lockPath);
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
async function settlePendingQueueLeaseGuard(sessionId) {
|
|
2478
|
+
if (localMutations.size === 0) return;
|
|
2479
|
+
const lockPath = await canonicalLeasePath(sessionId);
|
|
2480
|
+
if (!localMutations.has(lockPath)) return;
|
|
2481
|
+
await locallySerialized(lockPath, async (state) => await settleFailedGuard(lockPath, state));
|
|
2482
|
+
}
|
|
2483
|
+
async function withQueueLeaseMutation(sessionId, mutate, options = {}) {
|
|
2484
|
+
const { reservation, capturedIdentity } = options;
|
|
2485
|
+
await fs$1.mkdir(path.dirname(queueLockFilePath(sessionId)), {
|
|
2486
|
+
recursive: true,
|
|
2487
|
+
mode: 448
|
|
2488
|
+
});
|
|
2489
|
+
const lockPath = await canonicalLeasePath(sessionId);
|
|
2490
|
+
const expected = reservation ? {
|
|
2491
|
+
pid: reservation.pid,
|
|
2492
|
+
sessionId: reservation.sessionId,
|
|
2493
|
+
ownerGeneration: reservation.ownerGeneration
|
|
2494
|
+
} : void 0;
|
|
2495
|
+
const publishedReservation = () => reservation?.published ? expected : void 0;
|
|
2496
|
+
return await locallySerialized(lockPath, async (state) => {
|
|
2497
|
+
await settleFailedGuard(lockPath, state);
|
|
2498
|
+
const owner = await createLockOwner({ capturedIdentity });
|
|
2499
|
+
const guard = await acquireGuard(lockPath, owner);
|
|
2500
|
+
let outcome;
|
|
2501
|
+
try {
|
|
2502
|
+
outcome = {
|
|
2503
|
+
ok: true,
|
|
2504
|
+
value: await mutate()
|
|
2505
|
+
};
|
|
2506
|
+
} catch (error) {
|
|
2507
|
+
outcome = {
|
|
2508
|
+
ok: false,
|
|
2509
|
+
error
|
|
2510
|
+
};
|
|
2511
|
+
}
|
|
2512
|
+
const published = publishedReservation();
|
|
2513
|
+
const receipt = {
|
|
2514
|
+
guard,
|
|
2515
|
+
owner,
|
|
2516
|
+
reservation: outcome.ok ? void 0 : published
|
|
2517
|
+
};
|
|
2518
|
+
try {
|
|
2519
|
+
await settleReceipt(lockPath, receipt);
|
|
2520
|
+
} catch (error) {
|
|
2521
|
+
if (outcome.ok) receipt.reservation = published;
|
|
2522
|
+
state.failed = receipt;
|
|
2523
|
+
throw new QueueLeaseGuardSettlementError(outcome.ok ? [error] : [outcome.error, error]);
|
|
2524
|
+
}
|
|
2525
|
+
if (!outcome.ok) throw outcome.error;
|
|
2526
|
+
return outcome.value;
|
|
2527
|
+
});
|
|
2528
|
+
}
|
|
2529
|
+
//#endregion
|
|
2127
2530
|
//#region src/session/queue/lease-store.ts
|
|
2128
2531
|
const PROCESS_SIGTERM_GRACE_MS = process.platform === "win32" ? 4e3 : 12e3;
|
|
2129
2532
|
const PROCESS_SIGKILL_GRACE_MS = 1500;
|
|
@@ -2133,6 +2536,7 @@ function parseQueueOwnerRecord(raw) {
|
|
|
2133
2536
|
if (!raw || typeof raw !== "object") return null;
|
|
2134
2537
|
const record = raw;
|
|
2135
2538
|
if (!hasValidQueueOwnerRecordFields(record)) return null;
|
|
2539
|
+
const processIdentity = parseProcessBirthIdentity(record.processIdentity);
|
|
2136
2540
|
return {
|
|
2137
2541
|
pid: record.pid,
|
|
2138
2542
|
sessionId: record.sessionId,
|
|
@@ -2141,12 +2545,19 @@ function parseQueueOwnerRecord(raw) {
|
|
|
2141
2545
|
heartbeatAt: record.heartbeatAt,
|
|
2142
2546
|
ownerGeneration: record.ownerGeneration,
|
|
2143
2547
|
queueDepth: record.queueDepth,
|
|
2144
|
-
...
|
|
2145
|
-
...record
|
|
2548
|
+
...processIdentity ? { processIdentity } : {},
|
|
2549
|
+
...parseQueueOwnerCapabilities(record),
|
|
2146
2550
|
...typeof record.mcpConfigPath === "string" ? { mcpConfigPath: record.mcpConfigPath } : {},
|
|
2147
2551
|
...typeof record.mcpConfigFingerprint === "string" ? { mcpConfigFingerprint: record.mcpConfigFingerprint } : {}
|
|
2148
2552
|
};
|
|
2149
2553
|
}
|
|
2554
|
+
function parseQueueOwnerCapabilities(record) {
|
|
2555
|
+
return {
|
|
2556
|
+
...record.sharedRuntime === true ? { sharedRuntime: true } : {},
|
|
2557
|
+
...record.sessionWatch === true ? { sessionWatch: true } : {},
|
|
2558
|
+
...record.persistsControlState === true ? { persistsControlState: true } : {}
|
|
2559
|
+
};
|
|
2560
|
+
}
|
|
2150
2561
|
function hasValidQueueOwnerRecordFields(record) {
|
|
2151
2562
|
return !Array.isArray(record) && isPositiveInteger(record.pid) && typeof record.sessionId === "string" && typeof record.socketPath === "string" && typeof record.createdAt === "string" && typeof record.heartbeatAt === "string" && isPositiveInteger(record.ownerGeneration) && isNonNegativeInteger(record.queueDepth);
|
|
2152
2563
|
}
|
|
@@ -2199,15 +2610,20 @@ async function removeSocketFile(socketPath) {
|
|
|
2199
2610
|
if (error.code !== "ENOENT") throw error;
|
|
2200
2611
|
}
|
|
2201
2612
|
}
|
|
2202
|
-
async function waitForProcessExit(pid, timeoutMs) {
|
|
2613
|
+
async function waitForProcessExit(pid, timeoutMs, hasExited) {
|
|
2203
2614
|
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
2204
2615
|
while (Date.now() <= deadline) {
|
|
2205
|
-
if (
|
|
2616
|
+
if (hasExited(pid)) return true;
|
|
2206
2617
|
await waitMs(PROCESS_POLL_MS);
|
|
2207
2618
|
}
|
|
2208
|
-
return
|
|
2619
|
+
return hasExited(pid);
|
|
2209
2620
|
}
|
|
2210
|
-
async function cleanupQueueOwnerFiles(sessionId, socketPath, isCurrent) {
|
|
2621
|
+
async function cleanupQueueOwnerFiles(sessionId, socketPath, isCurrent, capturedIdentity) {
|
|
2622
|
+
await withQueueLeaseMutation(sessionId, async () => {
|
|
2623
|
+
await cleanupGuardedQueueOwnerFiles(sessionId, socketPath, isCurrent);
|
|
2624
|
+
}, { capturedIdentity });
|
|
2625
|
+
}
|
|
2626
|
+
async function cleanupGuardedQueueOwnerFiles(sessionId, socketPath, isCurrent) {
|
|
2211
2627
|
if (!await isCurrent()) return;
|
|
2212
2628
|
await removeSocketFile(socketPath).catch(() => {});
|
|
2213
2629
|
if (!await isCurrent()) return;
|
|
@@ -2215,9 +2631,11 @@ async function cleanupQueueOwnerFiles(sessionId, socketPath, isCurrent) {
|
|
|
2215
2631
|
if (error.code !== "ENOENT") throw error;
|
|
2216
2632
|
});
|
|
2217
2633
|
}
|
|
2218
|
-
async function ownsQueueLease(owner
|
|
2219
|
-
|
|
2220
|
-
|
|
2634
|
+
async function ownsQueueLease(owner) {
|
|
2635
|
+
return matchesQueueOwner(await readQueueOwnerRecord(owner.sessionId), owner);
|
|
2636
|
+
}
|
|
2637
|
+
function matchesQueueOwner(current, expected) {
|
|
2638
|
+
return current?.sessionId === expected.sessionId && current.pid === expected.pid && current.ownerGeneration === expected.ownerGeneration;
|
|
2221
2639
|
}
|
|
2222
2640
|
function ownerIsAlive(owner) {
|
|
2223
2641
|
return owner.pid === process.pid || isProcessAlive(owner.pid);
|
|
@@ -2234,41 +2652,55 @@ async function readQueueOwnerRecord(sessionId) {
|
|
|
2234
2652
|
}
|
|
2235
2653
|
async function terminateProcess(pid, beforeSignal) {
|
|
2236
2654
|
if (!isProcessAlive(pid)) return false;
|
|
2237
|
-
|
|
2655
|
+
return await terminateWithDispatch(pid, async (signal) => {
|
|
2238
2656
|
if (beforeSignal && !await beforeSignal(signal)) return false;
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2657
|
+
return dispatchSignal(pid, signal);
|
|
2658
|
+
}, (target) => !isProcessAlive(target));
|
|
2659
|
+
}
|
|
2660
|
+
function dispatchSignal(pid, signal) {
|
|
2661
|
+
try {
|
|
2662
|
+
process.kill(pid, signal);
|
|
2663
|
+
return true;
|
|
2664
|
+
} catch {
|
|
2665
|
+
return false;
|
|
2666
|
+
}
|
|
2667
|
+
}
|
|
2668
|
+
async function dispatchQueueOwnerSignal(pid, signal) {
|
|
2669
|
+
if (process.platform !== "win32") return dispatchSignal(pid, signal);
|
|
2670
|
+
const systemRoot = process.env.SystemRoot;
|
|
2671
|
+
if (!systemRoot || !path.win32.isAbsolute(systemRoot)) throw new Error("Windows queue-owner cleanup requires an absolute SystemRoot directory");
|
|
2672
|
+
const taskkill = path.win32.join(systemRoot, "System32", "taskkill.exe");
|
|
2673
|
+
await runTimedExecFile(taskkill, [
|
|
2674
|
+
"/pid",
|
|
2675
|
+
String(pid),
|
|
2676
|
+
"/T",
|
|
2677
|
+
"/F"
|
|
2678
|
+
], {
|
|
2679
|
+
timeoutMs: PROCESS_SIGTERM_GRACE_MS,
|
|
2680
|
+
windowsHide: true
|
|
2681
|
+
});
|
|
2682
|
+
return true;
|
|
2683
|
+
}
|
|
2684
|
+
async function terminateWithDispatch(pid, dispatch, hasExited) {
|
|
2685
|
+
for (const [signal, graceMs] of [["SIGTERM", PROCESS_SIGTERM_GRACE_MS], ["SIGKILL", PROCESS_SIGKILL_GRACE_MS]]) {
|
|
2686
|
+
if (!await dispatch(signal)) return false;
|
|
2687
|
+
if (await waitForProcessExit(pid, graceMs, hasExited)) return true;
|
|
2245
2688
|
}
|
|
2246
2689
|
return false;
|
|
2247
2690
|
}
|
|
2248
2691
|
async function resolveUsableQueueOwner(sessionId, owner) {
|
|
2249
|
-
|
|
2250
|
-
await terminateQueueOwnerForSession(sessionId, owner, true);
|
|
2251
|
-
const current = await readQueueOwnerRecord(sessionId);
|
|
2252
|
-
return current?.pid === owner.pid && current.ownerGeneration === owner.ownerGeneration && ownerIsAlive(current) && !isQueueOwnerHeartbeatStale(current) ? current : void 0;
|
|
2253
|
-
}
|
|
2254
|
-
async function readQueueOwnerStatus(sessionId) {
|
|
2692
|
+
await settlePendingQueueLeaseGuard(sessionId);
|
|
2255
2693
|
const observed = await readQueueOwnerRecord(sessionId);
|
|
2256
|
-
if (!observed) return;
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
socketPath: owner.socketPath,
|
|
2262
|
-
heartbeatAt: owner.heartbeatAt,
|
|
2263
|
-
ownerGeneration: owner.ownerGeneration,
|
|
2264
|
-
queueDepth: owner.queueDepth,
|
|
2265
|
-
alive: true,
|
|
2266
|
-
stale: isQueueOwnerHeartbeatStale(owner)
|
|
2267
|
-
};
|
|
2694
|
+
if (!matchesQueueOwner(observed, owner)) return;
|
|
2695
|
+
if (ownerIsAlive(observed) && !isQueueOwnerHeartbeatStale(observed)) return observed;
|
|
2696
|
+
await terminateQueueOwnerForSession(sessionId, observed, true);
|
|
2697
|
+
const current = await readQueueOwnerRecord(sessionId);
|
|
2698
|
+
return matchesQueueOwner(current, observed) && ownerIsAlive(current) && !isQueueOwnerHeartbeatStale(current) ? current : void 0;
|
|
2268
2699
|
}
|
|
2269
2700
|
async function tryAcquireQueueOwnerLease(sessionId, mcpConfigOrNowIsoFactory, nowIsoFactory = nowIso) {
|
|
2270
2701
|
const { mcpConfigPath, clock } = resolveLeaseArguments(mcpConfigOrNowIsoFactory, nowIsoFactory);
|
|
2271
2702
|
const mcpConfigMetadata = createMcpConfigMetadata(mcpConfigPath, readMcpConfigFingerprint(mcpConfigOrNowIsoFactory));
|
|
2703
|
+
const processIdentity = await getOwnProcessIdentity();
|
|
2272
2704
|
await ensureQueueDir();
|
|
2273
2705
|
const lockPath = queueLockFilePath(sessionId);
|
|
2274
2706
|
const socketPath = queueSocketPath(sessionId);
|
|
@@ -2281,25 +2713,38 @@ async function tryAcquireQueueOwnerLease(sessionId, mcpConfigOrNowIsoFactory, no
|
|
|
2281
2713
|
socketPath,
|
|
2282
2714
|
createdAt,
|
|
2283
2715
|
ownerGeneration,
|
|
2716
|
+
...processIdentity ? { processIdentity } : {},
|
|
2284
2717
|
...mcpConfigMetadata,
|
|
2285
2718
|
updates: Promise.resolve(),
|
|
2286
2719
|
released: false
|
|
2287
2720
|
};
|
|
2721
|
+
const reservation = {
|
|
2722
|
+
pid: lease.pid,
|
|
2723
|
+
sessionId,
|
|
2724
|
+
ownerGeneration,
|
|
2725
|
+
published: false
|
|
2726
|
+
};
|
|
2288
2727
|
try {
|
|
2289
|
-
await
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2728
|
+
return await withQueueLeaseMutation(sessionId, async () => {
|
|
2729
|
+
await stageQueueOwnerRecord(lease, 0, clock, async (tempPath, payload) => {
|
|
2730
|
+
try {
|
|
2731
|
+
await fs$1.link(tempPath, lockPath);
|
|
2732
|
+
} catch (error) {
|
|
2733
|
+
if (!isHardlinkFallbackError(error)) throw error;
|
|
2734
|
+
await fs$1.writeFile(lockPath, payload, {
|
|
2735
|
+
encoding: "utf8",
|
|
2736
|
+
flag: "wx",
|
|
2737
|
+
mode: 384
|
|
2738
|
+
});
|
|
2739
|
+
}
|
|
2740
|
+
reservation.published = true;
|
|
2741
|
+
});
|
|
2742
|
+
await removeSocketFile(socketPath).catch(() => {});
|
|
2743
|
+
return lease;
|
|
2744
|
+
}, {
|
|
2745
|
+
reservation,
|
|
2746
|
+
capturedIdentity: lease
|
|
2300
2747
|
});
|
|
2301
|
-
await removeSocketFile(socketPath).catch(() => {});
|
|
2302
|
-
return lease;
|
|
2303
2748
|
} catch (error) {
|
|
2304
2749
|
return await handleLeaseCollision(sessionId, error);
|
|
2305
2750
|
}
|
|
@@ -2324,18 +2769,20 @@ async function handleLeaseCollision(sessionId, error) {
|
|
|
2324
2769
|
}
|
|
2325
2770
|
async function cleanupAbandonedReservation(sessionId) {
|
|
2326
2771
|
const lockPath = queueLockFilePath(sessionId);
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2772
|
+
await withQueueLeaseMutation(sessionId, async () => {
|
|
2773
|
+
try {
|
|
2774
|
+
const observed = await fs$1.lstat(lockPath, { bigint: true });
|
|
2775
|
+
if (!observed.isFile() || Date.now() - Number(observed.mtimeMs) <= QUEUE_OWNER_STALE_HEARTBEAT_MS) return;
|
|
2776
|
+
const raw = await fs$1.readFile(lockPath, "utf8");
|
|
2777
|
+
if (await readQueueOwnerRecord(sessionId)) return;
|
|
2778
|
+
await cleanupGuardedQueueOwnerFiles(sessionId, queueSocketPath(sessionId), async () => {
|
|
2779
|
+
const current = await fs$1.lstat(lockPath, { bigint: true });
|
|
2780
|
+
return current.isFile() && current.dev === observed.dev && current.ino === observed.ino && current.mtimeNs === observed.mtimeNs && await fs$1.readFile(lockPath, "utf8") === raw;
|
|
2781
|
+
});
|
|
2782
|
+
} catch (error) {
|
|
2783
|
+
if (error.code !== "ENOENT") throw error;
|
|
2784
|
+
}
|
|
2785
|
+
});
|
|
2339
2786
|
}
|
|
2340
2787
|
function resolveLeaseArguments(mcpConfigOrNowIsoFactory, nowIsoFactory) {
|
|
2341
2788
|
if (typeof mcpConfigOrNowIsoFactory === "string") return {
|
|
@@ -2358,9 +2805,12 @@ function resolveLeaseArguments(mcpConfigOrNowIsoFactory, nowIsoFactory) {
|
|
|
2358
2805
|
function refreshQueueOwnerLease(lease, options, nowIsoFactory = nowIso) {
|
|
2359
2806
|
if (lease.released) return Promise.resolve();
|
|
2360
2807
|
const update = lease.updates.then(async () => {
|
|
2361
|
-
await
|
|
2362
|
-
if (await ownsQueueLease(lease))
|
|
2363
|
-
|
|
2808
|
+
await withQueueLeaseMutation(lease.sessionId, async () => {
|
|
2809
|
+
if (!await ownsQueueLease(lease)) return;
|
|
2810
|
+
await stageQueueOwnerRecord(lease, options.queueDepth, nowIsoFactory, async (tempPath) => {
|
|
2811
|
+
await fs$1.rename(tempPath, lease.lockPath);
|
|
2812
|
+
});
|
|
2813
|
+
}, { capturedIdentity: lease });
|
|
2364
2814
|
});
|
|
2365
2815
|
lease.updates = update.catch(() => {});
|
|
2366
2816
|
return update;
|
|
@@ -2373,9 +2823,11 @@ async function stageQueueOwnerRecord(lease, queueDepth, clock, publish) {
|
|
|
2373
2823
|
createdAt: lease.createdAt,
|
|
2374
2824
|
heartbeatAt: clock(),
|
|
2375
2825
|
ownerGeneration: lease.ownerGeneration,
|
|
2826
|
+
...lease.processIdentity ? { processIdentity: lease.processIdentity } : {},
|
|
2376
2827
|
queueDepth: Math.max(0, Math.round(queueDepth)),
|
|
2377
2828
|
sharedRuntime: true,
|
|
2378
2829
|
sessionWatch: true,
|
|
2830
|
+
persistsControlState: true,
|
|
2379
2831
|
...lease.mcpConfigPath ? { mcpConfigPath: lease.mcpConfigPath } : {},
|
|
2380
2832
|
...lease.mcpConfigFingerprint ? { mcpConfigFingerprint: lease.mcpConfigFingerprint } : {}
|
|
2381
2833
|
}, null, 2);
|
|
@@ -2395,13 +2847,48 @@ async function stageQueueOwnerRecord(lease, queueDepth, clock, publish) {
|
|
|
2395
2847
|
async function releaseQueueOwnerLease(lease) {
|
|
2396
2848
|
lease.released = true;
|
|
2397
2849
|
await lease.updates;
|
|
2398
|
-
await cleanupQueueOwnerFiles(lease.sessionId, lease.socketPath, () => ownsQueueLease(lease));
|
|
2850
|
+
await cleanupQueueOwnerFiles(lease.sessionId, lease.socketPath, () => ownsQueueLease(lease), lease);
|
|
2851
|
+
}
|
|
2852
|
+
function unverifiedQueueOwnerError(owner) {
|
|
2853
|
+
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.`, {
|
|
2854
|
+
detailCode: "QUEUE_OWNER_IDENTITY_UNVERIFIED",
|
|
2855
|
+
origin: "queue",
|
|
2856
|
+
retryable: true
|
|
2857
|
+
});
|
|
2858
|
+
}
|
|
2859
|
+
async function dispatchVerifiedQueueOwnerSignal(owner, signal, requireStale, retirement) {
|
|
2860
|
+
return await withQueueLeaseMutation(owner.sessionId, async () => {
|
|
2861
|
+
const current = await readQueueOwnerRecord(owner.sessionId);
|
|
2862
|
+
retirement.leaseReleased = current?.pid !== owner.pid;
|
|
2863
|
+
if (!matchesQueueOwner(current, owner) || requireStale && signal === "SIGTERM" && !isQueueOwnerHeartbeatStale(current)) return false;
|
|
2864
|
+
const identity = await observeProcessIncarnation(current.pid, current.processIdentity);
|
|
2865
|
+
if (identity !== "matching") {
|
|
2866
|
+
retirement.gone = identity === "gone";
|
|
2867
|
+
retirement.unverified = identity === "unknown";
|
|
2868
|
+
return false;
|
|
2869
|
+
}
|
|
2870
|
+
return await dispatchQueueOwnerSignal(owner.pid, signal);
|
|
2871
|
+
});
|
|
2872
|
+
}
|
|
2873
|
+
async function retireQueueOwner(owner, requireStale) {
|
|
2874
|
+
const deadline = performance.now() + PROCESS_SIGTERM_GRACE_MS + PROCESS_SIGKILL_GRACE_MS;
|
|
2875
|
+
const retirement = {
|
|
2876
|
+
gone: false,
|
|
2877
|
+
unverified: false,
|
|
2878
|
+
leaseReleased: false
|
|
2879
|
+
};
|
|
2880
|
+
await terminateWithDispatch(owner.pid, (signal) => dispatchVerifiedQueueOwnerSignal(owner, signal, requireStale, retirement), isProcessDefinitelyDead);
|
|
2881
|
+
if (retirement.unverified && requireStale) throw unverifiedQueueOwnerError(owner);
|
|
2882
|
+
if (retirement.leaseReleased || retirement.unverified) await waitForProcessExit(owner.pid, Math.max(0, deadline - performance.now()), isProcessDefinitelyDead);
|
|
2883
|
+
const gone = retirement.gone || await observeProcessIncarnation(owner.pid, owner.processIdentity) === "gone";
|
|
2884
|
+
if (retirement.unverified && !gone) throw unverifiedQueueOwnerError(owner);
|
|
2885
|
+
return gone;
|
|
2399
2886
|
}
|
|
2400
2887
|
async function terminateQueueOwnerForSession(sessionId, expectedOwner, requireStale = false) {
|
|
2888
|
+
await settlePendingQueueLeaseGuard(sessionId);
|
|
2401
2889
|
const owner = expectedOwner ?? await readQueueOwnerRecord(sessionId);
|
|
2402
|
-
if (!owner || owner.sessionId !== sessionId) return;
|
|
2403
|
-
if (
|
|
2404
|
-
if (ownerIsAlive(owner)) return;
|
|
2890
|
+
if (!owner || owner.sessionId !== sessionId || owner.pid === process.pid) return;
|
|
2891
|
+
if (!await retireQueueOwner(owner, requireStale)) return;
|
|
2405
2892
|
await cleanupQueueOwnerFiles(sessionId, owner.socketPath, () => ownsQueueLease(owner));
|
|
2406
2893
|
}
|
|
2407
2894
|
async function waitMs(ms) {
|
|
@@ -2456,7 +2943,12 @@ async function connectToQueueOwner(owner, maxAttempts = QUEUE_CONNECT_ATTEMPTS)
|
|
|
2456
2943
|
}
|
|
2457
2944
|
//#endregion
|
|
2458
2945
|
//#region src/session/queue/ipc-health.ts
|
|
2946
|
+
async function isStillCurrent(owner) {
|
|
2947
|
+
const current = await readQueueOwnerRecord(owner.sessionId);
|
|
2948
|
+
return current?.pid === owner.pid && current.ownerGeneration === owner.ownerGeneration;
|
|
2949
|
+
}
|
|
2459
2950
|
async function probeQueueOwnerHealth(sessionId) {
|
|
2951
|
+
await settlePendingQueueLeaseGuard(sessionId);
|
|
2460
2952
|
const ownerRecord = await readQueueOwnerRecord(sessionId);
|
|
2461
2953
|
if (!ownerRecord) return {
|
|
2462
2954
|
sessionId,
|
|
@@ -2465,7 +2957,7 @@ async function probeQueueOwnerHealth(sessionId) {
|
|
|
2465
2957
|
socketReachable: false,
|
|
2466
2958
|
pidAlive: false
|
|
2467
2959
|
};
|
|
2468
|
-
const owner = await
|
|
2960
|
+
const owner = await resolveUsableQueueOwner(sessionId, ownerRecord);
|
|
2469
2961
|
if (!owner) return {
|
|
2470
2962
|
sessionId,
|
|
2471
2963
|
hasLease: false,
|
|
@@ -2473,10 +2965,9 @@ async function probeQueueOwnerHealth(sessionId) {
|
|
|
2473
2965
|
socketReachable: false,
|
|
2474
2966
|
pidAlive: false
|
|
2475
2967
|
};
|
|
2476
|
-
const pidAlive = owner.alive;
|
|
2477
2968
|
let socketReachable = false;
|
|
2478
2969
|
try {
|
|
2479
|
-
const socket = await connectToQueueOwner(
|
|
2970
|
+
const socket = await connectToQueueOwner(owner, 2);
|
|
2480
2971
|
if (socket) {
|
|
2481
2972
|
socketReachable = true;
|
|
2482
2973
|
if (!socket.destroyed) socket.end();
|
|
@@ -2484,12 +2975,19 @@ async function probeQueueOwnerHealth(sessionId) {
|
|
|
2484
2975
|
} catch {
|
|
2485
2976
|
socketReachable = false;
|
|
2486
2977
|
}
|
|
2978
|
+
if (!await isStillCurrent(owner)) return {
|
|
2979
|
+
sessionId,
|
|
2980
|
+
hasLease: false,
|
|
2981
|
+
healthy: false,
|
|
2982
|
+
socketReachable: false,
|
|
2983
|
+
pidAlive: false
|
|
2984
|
+
};
|
|
2487
2985
|
return {
|
|
2488
2986
|
sessionId,
|
|
2489
2987
|
hasLease: true,
|
|
2490
2988
|
healthy: socketReachable,
|
|
2491
2989
|
socketReachable,
|
|
2492
|
-
pidAlive,
|
|
2990
|
+
pidAlive: true,
|
|
2493
2991
|
pid: owner.pid,
|
|
2494
2992
|
socketPath: owner.socketPath,
|
|
2495
2993
|
ownerGeneration: owner.ownerGeneration,
|
|
@@ -3051,6 +3549,12 @@ async function runQueueOwnerRequest(options) {
|
|
|
3051
3549
|
onAbort();
|
|
3052
3550
|
return;
|
|
3053
3551
|
}
|
|
3552
|
+
try {
|
|
3553
|
+
options.assertDispatch?.();
|
|
3554
|
+
} catch (error) {
|
|
3555
|
+
finishReject(error);
|
|
3556
|
+
return;
|
|
3557
|
+
}
|
|
3054
3558
|
try {
|
|
3055
3559
|
socket.write(`${requestLine}\n`);
|
|
3056
3560
|
} catch (error) {
|
|
@@ -3164,10 +3668,11 @@ async function submitToQueueOwner(owner, options) {
|
|
|
3164
3668
|
}
|
|
3165
3669
|
});
|
|
3166
3670
|
}
|
|
3167
|
-
async function submitControlToQueueOwner(owner, request, isExpectedResponse) {
|
|
3671
|
+
async function submitControlToQueueOwner(owner, request, isExpectedResponse, assertDispatch) {
|
|
3168
3672
|
return await runQueueOwnerRequest({
|
|
3169
3673
|
owner,
|
|
3170
3674
|
request,
|
|
3675
|
+
assertDispatch,
|
|
3171
3676
|
onMessage: (message, { state, resolve, reject }) => {
|
|
3172
3677
|
if (message.type === "error") {
|
|
3173
3678
|
reject(queueConnectionErrorFromOwner(message, false));
|
|
@@ -3209,25 +3714,25 @@ async function submitCancelToQueueOwner(owner, targetRequestId) {
|
|
|
3209
3714
|
...targetRequestId !== void 0 ? { targetRequestId } : {}
|
|
3210
3715
|
}, (message) => message.type === "cancel_result"))?.cancelled;
|
|
3211
3716
|
}
|
|
3212
|
-
async function submitSetModeToQueueOwner(owner, modeId, timeoutMs) {
|
|
3717
|
+
async function submitSetModeToQueueOwner(owner, modeId, timeoutMs, assertDispatch) {
|
|
3213
3718
|
return await submitControlToQueueOwner(owner, {
|
|
3214
3719
|
type: "set_mode",
|
|
3215
3720
|
requestId: randomUUID(),
|
|
3216
3721
|
ownerGeneration: owner.ownerGeneration,
|
|
3217
3722
|
modeId,
|
|
3218
3723
|
timeoutMs
|
|
3219
|
-
}, (message) => message.type === "set_mode_result") ? true : void 0;
|
|
3724
|
+
}, (message) => message.type === "set_mode_result", assertDispatch) ? true : void 0;
|
|
3220
3725
|
}
|
|
3221
|
-
async function submitSetModelToQueueOwner(owner, modelId, timeoutMs) {
|
|
3726
|
+
async function submitSetModelToQueueOwner(owner, modelId, timeoutMs, assertDispatch) {
|
|
3222
3727
|
return await submitControlToQueueOwner(owner, {
|
|
3223
3728
|
type: "set_model",
|
|
3224
3729
|
requestId: randomUUID(),
|
|
3225
3730
|
ownerGeneration: owner.ownerGeneration,
|
|
3226
3731
|
modelId,
|
|
3227
3732
|
timeoutMs
|
|
3228
|
-
}, (message) => message.type === "set_model_result");
|
|
3733
|
+
}, (message) => message.type === "set_model_result", assertDispatch);
|
|
3229
3734
|
}
|
|
3230
|
-
async function submitSetConfigOptionToQueueOwner(owner, configId, value, timeoutMs) {
|
|
3735
|
+
async function submitSetConfigOptionToQueueOwner(owner, configId, value, timeoutMs, assertDispatch) {
|
|
3231
3736
|
return (await submitControlToQueueOwner(owner, {
|
|
3232
3737
|
type: "set_config_option",
|
|
3233
3738
|
requestId: randomUUID(),
|
|
@@ -3235,7 +3740,7 @@ async function submitSetConfigOptionToQueueOwner(owner, configId, value, timeout
|
|
|
3235
3740
|
configId,
|
|
3236
3741
|
value,
|
|
3237
3742
|
timeoutMs
|
|
3238
|
-
}, (message) => message.type === "set_config_option_result"))?.response;
|
|
3743
|
+
}, (message) => message.type === "set_config_option_result", assertDispatch))?.response;
|
|
3239
3744
|
}
|
|
3240
3745
|
async function submitCloseSessionToQueueOwner(owner, timeoutMs) {
|
|
3241
3746
|
return (await submitControlToQueueOwner(owner, {
|
|
@@ -3305,7 +3810,10 @@ async function tryCloseSessionOnRunningOwner(options) {
|
|
|
3305
3810
|
verbose: options.verbose,
|
|
3306
3811
|
requestName: "close_session",
|
|
3307
3812
|
logPrefix: "[acpx] requested session/close on active owner pid",
|
|
3308
|
-
submit: (owner) =>
|
|
3813
|
+
submit: (owner) => {
|
|
3814
|
+
options.onOwnerSelected?.(owner);
|
|
3815
|
+
return submitCloseSessionToQueueOwner(owner, options.timeoutMs);
|
|
3816
|
+
}
|
|
3309
3817
|
});
|
|
3310
3818
|
}
|
|
3311
3819
|
async function tryCancelOnRunningOwner(options) {
|
|
@@ -3317,34 +3825,42 @@ async function tryCancelOnRunningOwner(options) {
|
|
|
3317
3825
|
submit: (owner) => submitCancelToQueueOwner(owner, options.targetRequestId)
|
|
3318
3826
|
});
|
|
3319
3827
|
}
|
|
3320
|
-
async function
|
|
3828
|
+
async function withOwnerControlPersistence(owner, operation) {
|
|
3829
|
+
const persistsControlState = owner.persistsControlState === true;
|
|
3830
|
+
const value = await operation;
|
|
3831
|
+
return value === void 0 ? void 0 : {
|
|
3832
|
+
value,
|
|
3833
|
+
persistsControlState
|
|
3834
|
+
};
|
|
3835
|
+
}
|
|
3836
|
+
async function trySetModeOnRunningOwner(sessionId, modeId, timeoutMs, verbose, assertDispatch) {
|
|
3321
3837
|
return await tryControlOnRunningOwner({
|
|
3322
3838
|
sessionId,
|
|
3323
3839
|
verbose,
|
|
3324
3840
|
requestName: "set_mode",
|
|
3325
3841
|
logPrefix: "[acpx] requested session/set_mode on owner pid",
|
|
3326
|
-
submit: (owner) => submitSetModeToQueueOwner(owner, modeId, timeoutMs)
|
|
3842
|
+
submit: (owner) => withOwnerControlPersistence(owner, submitSetModeToQueueOwner(owner, modeId, timeoutMs, assertDispatch))
|
|
3327
3843
|
});
|
|
3328
3844
|
}
|
|
3329
|
-
async function trySetModelOnRunningOwner(sessionId, modelId, timeoutMs, verbose) {
|
|
3845
|
+
async function trySetModelOnRunningOwner(sessionId, modelId, timeoutMs, verbose, assertDispatch) {
|
|
3330
3846
|
return await tryControlOnRunningOwner({
|
|
3331
3847
|
sessionId,
|
|
3332
3848
|
verbose,
|
|
3333
3849
|
requestName: "set_model",
|
|
3334
3850
|
logPrefix: "[acpx] requested a model config update on owner pid",
|
|
3335
|
-
submit: (owner) => submitSetModelToQueueOwner(owner, modelId, timeoutMs)
|
|
3851
|
+
submit: (owner) => withOwnerControlPersistence(owner, submitSetModelToQueueOwner(owner, modelId, timeoutMs, assertDispatch))
|
|
3336
3852
|
});
|
|
3337
3853
|
}
|
|
3338
|
-
async function trySetConfigOptionOnRunningOwner(sessionId, configId, value, timeoutMs, verbose) {
|
|
3854
|
+
async function trySetConfigOptionOnRunningOwner(sessionId, configId, value, timeoutMs, verbose, assertDispatch) {
|
|
3339
3855
|
return await tryControlOnRunningOwner({
|
|
3340
3856
|
sessionId,
|
|
3341
3857
|
verbose,
|
|
3342
3858
|
requestName: "set_config_option",
|
|
3343
3859
|
logPrefix: "[acpx] requested session/set_config_option on owner pid",
|
|
3344
|
-
submit: (owner) => submitSetConfigOptionToQueueOwner(owner, configId, value, timeoutMs)
|
|
3860
|
+
submit: (owner) => withOwnerControlPersistence(owner, submitSetConfigOptionToQueueOwner(owner, configId, value, timeoutMs, assertDispatch))
|
|
3345
3861
|
});
|
|
3346
3862
|
}
|
|
3347
3863
|
//#endregion
|
|
3348
|
-
export {
|
|
3864
|
+
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, readProcessTable as x, formatUnknownErrorMessage as xt, createLockOwner as y, isProcessAlive as yt, resolveSessionRecord as z, GeminiAcpStartupTimeoutError as zt };
|
|
3349
3865
|
|
|
3350
|
-
//# sourceMappingURL=ipc-
|
|
3866
|
+
//# sourceMappingURL=ipc-fMgj5vrO.js.map
|