@mono-agent/agent-app 0.19.1 → 0.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +307 -32
- package/dist/acp-bridge.d.ts.map +1 -1
- package/dist/acp-bridge.js +113 -54
- package/dist/acp-bridge.js.map +1 -1
- package/dist/acp-session-store.d.ts +12 -0
- package/dist/acp-session-store.d.ts.map +1 -0
- package/dist/acp-session-store.js +74 -0
- package/dist/acp-session-store.js.map +1 -0
- package/dist/agent-artifact-paths.d.ts +8 -0
- package/dist/agent-artifact-paths.d.ts.map +1 -0
- package/dist/agent-artifact-paths.js +11 -0
- package/dist/agent-artifact-paths.js.map +1 -0
- package/dist/agent-root-coordinator.d.ts +101 -0
- package/dist/agent-root-coordinator.d.ts.map +1 -0
- package/dist/agent-root-coordinator.js +470 -0
- package/dist/agent-root-coordinator.js.map +1 -0
- package/dist/app-controller-channels.d.ts +13 -1
- package/dist/app-controller-channels.d.ts.map +1 -1
- package/dist/app-controller-channels.js +152 -20
- package/dist/app-controller-channels.js.map +1 -1
- package/dist/app-controller-continuation.d.ts +2 -0
- package/dist/app-controller-continuation.d.ts.map +1 -1
- package/dist/app-controller-continuation.js.map +1 -1
- package/dist/app-controller-lifecycle.d.ts +5 -0
- package/dist/app-controller-lifecycle.d.ts.map +1 -1
- package/dist/app-controller-lifecycle.js +24 -5
- package/dist/app-controller-lifecycle.js.map +1 -1
- package/dist/app-controller-maintenance.d.ts +4 -0
- package/dist/app-controller-maintenance.d.ts.map +1 -1
- package/dist/app-controller-maintenance.js +34 -2
- package/dist/app-controller-maintenance.js.map +1 -1
- package/dist/app-controller-memory.d.ts +2 -0
- package/dist/app-controller-memory.d.ts.map +1 -1
- package/dist/app-controller-memory.js +6 -3
- package/dist/app-controller-memory.js.map +1 -1
- package/dist/app-controller-process-jobs.d.ts +49 -0
- package/dist/app-controller-process-jobs.d.ts.map +1 -0
- package/dist/app-controller-process-jobs.js +294 -0
- package/dist/app-controller-process-jobs.js.map +1 -0
- package/dist/app-controller-responder.d.ts +17 -0
- package/dist/app-controller-responder.d.ts.map +1 -1
- package/dist/app-controller-responder.js +164 -13
- package/dist/app-controller-responder.js.map +1 -1
- package/dist/app-controller-traceability.d.ts +8 -0
- package/dist/app-controller-traceability.d.ts.map +1 -1
- package/dist/app-controller-traceability.js +22 -1
- package/dist/app-controller-traceability.js.map +1 -1
- package/dist/app-controller-types.d.ts +1 -1
- package/dist/app-controller-types.d.ts.map +1 -1
- package/dist/app-controller-utils.d.ts +8 -0
- package/dist/app-controller-utils.d.ts.map +1 -1
- package/dist/app-controller-utils.js +24 -1
- package/dist/app-controller-utils.js.map +1 -1
- package/dist/app-controller.d.ts +45 -1
- package/dist/app-controller.d.ts.map +1 -1
- package/dist/app-controller.js +135 -39
- package/dist/app-controller.js.map +1 -1
- package/dist/background-log-maintenance.d.ts +85 -2
- package/dist/background-log-maintenance.d.ts.map +1 -1
- package/dist/background-log-maintenance.js +407 -136
- package/dist/background-log-maintenance.js.map +1 -1
- package/dist/background-runtime.d.ts.map +1 -1
- package/dist/background-runtime.js +22 -41
- package/dist/background-runtime.js.map +1 -1
- package/dist/background.d.ts +18 -26
- package/dist/background.d.ts.map +1 -1
- package/dist/background.js +105 -138
- package/dist/background.js.map +1 -1
- package/dist/channel-drivers/cron.d.ts +12 -1
- package/dist/channel-drivers/cron.d.ts.map +1 -1
- package/dist/channel-drivers/cron.js +121 -18
- package/dist/channel-drivers/cron.js.map +1 -1
- package/dist/channel-drivers/native-notify.d.ts +3 -0
- package/dist/channel-drivers/native-notify.d.ts.map +1 -1
- package/dist/channel-drivers/native-notify.js +2 -1
- package/dist/channel-drivers/native-notify.js.map +1 -1
- package/dist/channel-drivers/slack.d.ts.map +1 -1
- package/dist/channel-drivers/slack.js +41 -4
- package/dist/channel-drivers/slack.js.map +1 -1
- package/dist/channel-drivers/telegram.d.ts.map +1 -1
- package/dist/channel-drivers/telegram.js +46 -3
- package/dist/channel-drivers/telegram.js.map +1 -1
- package/dist/channel-drivers/tui.d.ts +13 -2
- package/dist/channel-drivers/tui.d.ts.map +1 -1
- package/dist/channel-drivers/tui.js +112 -3
- package/dist/channel-drivers/tui.js.map +1 -1
- package/dist/channels.d.ts +1 -1
- package/dist/channels.d.ts.map +1 -1
- package/dist/channels.js +4 -2
- package/dist/channels.js.map +1 -1
- package/dist/cli-args.d.ts +15 -1
- package/dist/cli-args.d.ts.map +1 -1
- package/dist/cli-args.js +82 -12
- package/dist/cli-args.js.map +1 -1
- package/dist/cli-background-command.d.ts +9 -2
- package/dist/cli-background-command.d.ts.map +1 -1
- package/dist/cli-background-command.js +179 -66
- package/dist/cli-background-command.js.map +1 -1
- package/dist/cli-commands.d.ts.map +1 -1
- package/dist/cli-commands.js +50 -12
- package/dist/cli-commands.js.map +1 -1
- package/dist/cli-help.d.ts.map +1 -1
- package/dist/cli-help.js +21 -1
- package/dist/cli-help.js.map +1 -1
- package/dist/config-reference.d.ts.map +1 -1
- package/dist/config-reference.js +105 -2
- package/dist/config-reference.js.map +1 -1
- package/dist/configured-agent.d.ts +64 -21
- package/dist/configured-agent.d.ts.map +1 -1
- package/dist/configured-agent.js +840 -156
- package/dist/configured-agent.js.map +1 -1
- package/dist/conversation-state-roots.d.ts +59 -0
- package/dist/conversation-state-roots.d.ts.map +1 -0
- package/dist/conversation-state-roots.js +248 -0
- package/dist/conversation-state-roots.js.map +1 -0
- package/dist/cron-control-store.d.ts +91 -0
- package/dist/cron-control-store.d.ts.map +1 -0
- package/dist/cron-control-store.js +1251 -0
- package/dist/cron-control-store.js.map +1 -0
- package/dist/cron-operator-service.d.ts +54 -0
- package/dist/cron-operator-service.d.ts.map +1 -0
- package/dist/cron-operator-service.js +352 -0
- package/dist/cron-operator-service.js.map +1 -0
- package/dist/doctor-session-history.d.ts +9 -0
- package/dist/doctor-session-history.d.ts.map +1 -0
- package/dist/doctor-session-history.js +313 -0
- package/dist/doctor-session-history.js.map +1 -0
- package/dist/doctor.d.ts +2 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +356 -9
- package/dist/doctor.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/interaction-bridge.d.ts +1 -0
- package/dist/interaction-bridge.d.ts.map +1 -1
- package/dist/interaction-bridge.js +54 -6
- package/dist/interaction-bridge.js.map +1 -1
- package/dist/jobs-command.d.ts +17 -0
- package/dist/jobs-command.d.ts.map +1 -0
- package/dist/jobs-command.js +207 -0
- package/dist/jobs-command.js.map +1 -0
- package/dist/launchd-lifecycle-lock.d.ts +36 -0
- package/dist/launchd-lifecycle-lock.d.ts.map +1 -0
- package/dist/launchd-lifecycle-lock.js +124 -0
- package/dist/launchd-lifecycle-lock.js.map +1 -0
- package/dist/launchd-log-monitor-status.d.ts +7 -0
- package/dist/launchd-log-monitor-status.d.ts.map +1 -0
- package/dist/launchd-log-monitor-status.js +224 -0
- package/dist/launchd-log-monitor-status.js.map +1 -0
- package/dist/launchd-logs.d.ts +9 -1
- package/dist/launchd-logs.d.ts.map +1 -1
- package/dist/launchd-logs.js +69 -16
- package/dist/launchd-logs.js.map +1 -1
- package/dist/launchd-maintenance-command.d.ts +10 -0
- package/dist/launchd-maintenance-command.d.ts.map +1 -0
- package/dist/launchd-maintenance-command.js +2 -0
- package/dist/launchd-maintenance-command.js.map +1 -0
- package/dist/launchd-maintenance-entry.d.ts +30 -0
- package/dist/launchd-maintenance-entry.d.ts.map +1 -0
- package/dist/launchd-maintenance-entry.js +206 -0
- package/dist/launchd-maintenance-entry.js.map +1 -0
- package/dist/launchd-maintenance-gate.d.ts +28 -0
- package/dist/launchd-maintenance-gate.d.ts.map +1 -0
- package/dist/launchd-maintenance-gate.js +80 -0
- package/dist/launchd-maintenance-gate.js.map +1 -0
- package/dist/launchd-private-files.d.ts +3 -0
- package/dist/launchd-private-files.d.ts.map +1 -0
- package/dist/launchd-private-files.js +58 -0
- package/dist/launchd-private-files.js.map +1 -0
- package/dist/launchd.d.ts +56 -2
- package/dist/launchd.d.ts.map +1 -1
- package/dist/launchd.js +210 -5
- package/dist/launchd.js.map +1 -1
- package/dist/local-configuration.d.ts.map +1 -1
- package/dist/local-configuration.js +34 -7
- package/dist/local-configuration.js.map +1 -1
- package/dist/managed-launchd-maintenance-environment.d.ts +3 -0
- package/dist/managed-launchd-maintenance-environment.d.ts.map +1 -0
- package/dist/managed-launchd-maintenance-environment.js +12 -0
- package/dist/managed-launchd-maintenance-environment.js.map +1 -0
- package/dist/managed-runtime-launch-proof.d.ts +11 -0
- package/dist/managed-runtime-launch-proof.d.ts.map +1 -0
- package/dist/managed-runtime-launch-proof.js +59 -0
- package/dist/managed-runtime-launch-proof.js.map +1 -0
- package/dist/managed-runtime-maintenance-entry.d.ts +10 -0
- package/dist/managed-runtime-maintenance-entry.d.ts.map +1 -0
- package/dist/managed-runtime-maintenance-entry.js +244 -0
- package/dist/managed-runtime-maintenance-entry.js.map +1 -0
- package/dist/managed-web-logs.d.ts +29 -11
- package/dist/managed-web-logs.d.ts.map +1 -1
- package/dist/managed-web-logs.js +244 -121
- package/dist/managed-web-logs.js.map +1 -1
- package/dist/managed-web-maintenance-environment.d.ts +6 -0
- package/dist/managed-web-maintenance-environment.d.ts.map +1 -0
- package/dist/managed-web-maintenance-environment.js +19 -0
- package/dist/managed-web-maintenance-environment.js.map +1 -0
- package/dist/mcp-apps.d.ts +37 -0
- package/dist/mcp-apps.d.ts.map +1 -0
- package/dist/mcp-apps.js +1753 -0
- package/dist/mcp-apps.js.map +1 -0
- package/dist/memory-command.d.ts +12 -0
- package/dist/memory-command.d.ts.map +1 -1
- package/dist/memory-command.js +510 -1
- package/dist/memory-command.js.map +1 -1
- package/dist/modules/known-tools.d.ts +1 -1
- package/dist/modules/known-tools.d.ts.map +1 -1
- package/dist/modules/known-tools.js +2 -0
- package/dist/modules/known-tools.js.map +1 -1
- package/dist/posted-reply-history.d.ts.map +1 -1
- package/dist/posted-reply-history.js +12 -0
- package/dist/posted-reply-history.js.map +1 -1
- package/dist/proactive-notify.d.ts +4 -1
- package/dist/proactive-notify.d.ts.map +1 -1
- package/dist/proactive-notify.js +43 -5
- package/dist/proactive-notify.js.map +1 -1
- package/dist/process-jobs-config.d.ts +84 -0
- package/dist/process-jobs-config.d.ts.map +1 -0
- package/dist/process-jobs-config.js +427 -0
- package/dist/process-jobs-config.js.map +1 -0
- package/dist/process-jobs-context.d.ts +24 -0
- package/dist/process-jobs-context.d.ts.map +1 -0
- package/dist/process-jobs-context.js +135 -0
- package/dist/process-jobs-context.js.map +1 -0
- package/dist/process-jobs-protection.d.ts +33 -0
- package/dist/process-jobs-protection.d.ts.map +1 -0
- package/dist/process-jobs-protection.js +74 -0
- package/dist/process-jobs-protection.js.map +1 -0
- package/dist/process-jobs-root-registry.d.ts +115 -0
- package/dist/process-jobs-root-registry.d.ts.map +1 -0
- package/dist/process-jobs-root-registry.js +978 -0
- package/dist/process-jobs-root-registry.js.map +1 -0
- package/dist/process-jobs-runtime.d.ts +38 -0
- package/dist/process-jobs-runtime.d.ts.map +1 -0
- package/dist/process-jobs-runtime.js +178 -0
- package/dist/process-jobs-runtime.js.map +1 -0
- package/dist/process-jobs-service.d.ts +70 -0
- package/dist/process-jobs-service.d.ts.map +1 -0
- package/dist/process-jobs-service.js +1989 -0
- package/dist/process-jobs-service.js.map +1 -0
- package/dist/process-jobs-store.d.ts +151 -0
- package/dist/process-jobs-store.d.ts.map +1 -0
- package/dist/process-jobs-store.js +1290 -0
- package/dist/process-jobs-store.js.map +1 -0
- package/dist/reply-artifacts.d.ts +65 -0
- package/dist/reply-artifacts.d.ts.map +1 -0
- package/dist/reply-artifacts.js +1147 -0
- package/dist/reply-artifacts.js.map +1 -0
- package/dist/reply-part-budget.d.ts +16 -0
- package/dist/reply-part-budget.d.ts.map +1 -0
- package/dist/reply-part-budget.js +57 -0
- package/dist/reply-part-budget.js.map +1 -0
- package/dist/request-model-override.d.ts +19 -0
- package/dist/request-model-override.d.ts.map +1 -1
- package/dist/request-model-override.js +54 -1
- package/dist/request-model-override.js.map +1 -1
- package/dist/request-scoped-mcp.d.ts +31 -0
- package/dist/request-scoped-mcp.d.ts.map +1 -0
- package/dist/request-scoped-mcp.js +197 -0
- package/dist/request-scoped-mcp.js.map +1 -0
- package/dist/run-history.d.ts.map +1 -1
- package/dist/run-history.js +50 -254
- package/dist/run-history.js.map +1 -1
- package/dist/runtime-option-extensions.d.ts +23 -0
- package/dist/runtime-option-extensions.d.ts.map +1 -1
- package/dist/runtime-option-extensions.js +55 -0
- package/dist/runtime-option-extensions.js.map +1 -1
- package/dist/runtime-routes.d.ts +7 -1
- package/dist/runtime-routes.d.ts.map +1 -1
- package/dist/runtime-routes.js +25 -0
- package/dist/runtime-routes.js.map +1 -1
- package/dist/secure-file-replace.d.ts +3 -0
- package/dist/secure-file-replace.d.ts.map +1 -1
- package/dist/secure-file-replace.js +31 -7
- package/dist/secure-file-replace.js.map +1 -1
- package/dist/session-history.d.ts +146 -0
- package/dist/session-history.d.ts.map +1 -0
- package/dist/session-history.js +347 -0
- package/dist/session-history.js.map +1 -0
- package/dist/sessions.d.ts +45 -2
- package/dist/sessions.d.ts.map +1 -1
- package/dist/sessions.js +849 -30
- package/dist/sessions.js.map +1 -1
- package/dist/web-command.d.ts +12 -11
- package/dist/web-command.d.ts.map +1 -1
- package/dist/web-command.js +554 -80
- package/dist/web-command.js.map +1 -1
- package/dist/web-log-maintenance-command.d.ts +7 -0
- package/dist/web-log-maintenance-command.d.ts.map +1 -0
- package/dist/web-log-maintenance-command.js +21 -0
- package/dist/web-log-maintenance-command.js.map +1 -0
- package/dist/web-log-maintenance-status.d.ts +26 -0
- package/dist/web-log-maintenance-status.d.ts.map +1 -0
- package/dist/web-log-maintenance-status.js +171 -0
- package/dist/web-log-maintenance-status.js.map +1 -0
- package/dist/web-log-maintenance.d.ts +32 -0
- package/dist/web-log-maintenance.d.ts.map +1 -0
- package/dist/web-log-maintenance.js +163 -0
- package/dist/web-log-maintenance.js.map +1 -0
- package/dist/web-maintenance-paths.d.ts +12 -0
- package/dist/web-maintenance-paths.d.ts.map +1 -0
- package/dist/web-maintenance-paths.js +24 -0
- package/dist/web-maintenance-paths.js.map +1 -0
- package/dist/wizard/prompts.d.ts.map +1 -1
- package/dist/wizard/prompts.js +1 -0
- package/dist/wizard/prompts.js.map +1 -1
- package/dist/wizard/run.js +1 -1
- package/dist/wizard/run.js.map +1 -1
- package/package.json +15 -15
- package/schema/mono-agent.config.schema.json +106 -1
- package/skills/mono-agent-composer/references/config-blueprint.md +27 -2
- package/skills/mono-agent-composer/references/discovery-questions.md +2 -2
- package/skills/mono-agent-composer/references/feature-coverage.md +6 -4
- package/skills/mono-agent-composer/references/package-map.md +19 -3
- package/skills/mono-agent-composer/references/playbooks.md +3 -3
- package/skills/mono-agent-composer/references/validation.md +4 -2
package/dist/sessions.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { constants as fsConstants } from "node:fs";
|
|
3
|
+
import { lstat, mkdir, open, opendir, readdir, realpath, rename, rm, stat, unlink } from "node:fs/promises";
|
|
4
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
|
+
import { ToolHistoryReader, toolHistoryDiskUsage, TOOL_HISTORY_OWNER_DATABASE, } from "@mono-agent/agent-harness";
|
|
6
|
+
import { assertConversationStatePurgePlanUnchanged, assertConversationStatePurgeRootUnchanged, CLEAR_SESSIONS_CONTROL_DIRECTORY, clearSessionsRegistryRoot as resolveClearSessionsRegistryRoot, conversationStatePurgePlanEntries, resolveAndAttestConversationStatePurgeRoot, resolveConversationStatePurgePlan, resolveConversationStatePurgeRoots, sameFileSystemIdentity, } from "./conversation-state-roots.js";
|
|
7
|
+
import { syncDirectory } from "./continuation-store-fs.js";
|
|
8
|
+
import { assertProcessJobsConfigSnapshotUnchanged, loadProcessJobsSettings, readProcessJobsConfigSnapshot, resolveProcessJobsRegistryWorkspace, } from "./process-jobs-config.js";
|
|
9
|
+
import { assertProcessJobsRegistryDisjointFromPaths, freezeProcessJobsRootRegistry, } from "./process-jobs-root-registry.js";
|
|
4
10
|
/**
|
|
5
11
|
* Remove the durable pi-session store so the next start begins with fresh sessions
|
|
6
12
|
* instead of resuming persisted transcripts. A no-op (`removed: false`) when no
|
|
@@ -12,49 +18,839 @@ import { resolveAppArtifactDir, resolveAppSessionsRoot } from "./app-config.js";
|
|
|
12
18
|
* not writing sessions while they are deleted.
|
|
13
19
|
*/
|
|
14
20
|
export async function purgeSessions(input) {
|
|
15
|
-
|
|
16
|
-
|
|
21
|
+
return await withClearSessionsDestructiveProtection(input.cwd, async (protection) => {
|
|
22
|
+
const current = await resolveCurrentConversationStateRoots(input);
|
|
23
|
+
const root = current.roots.sessions === undefined
|
|
24
|
+
? undefined
|
|
25
|
+
: await resolveAndAttestConversationStatePurgeRoot("Pi provider sessions", current.roots.sessions);
|
|
26
|
+
return await purgeSessionsRoot(root, protection, current);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
async function purgeSessionsRoot(root, protection, current) {
|
|
30
|
+
const roots = root === undefined ? [] : [root];
|
|
31
|
+
await assertStandalonePurgeProtected(protection, current, roots);
|
|
32
|
+
const inspected = await inspectSessionsRoot(root);
|
|
33
|
+
await securelyRemoveStandaloneRoots(protection.registry, roots.filter((candidate) => candidate.target !== undefined), async () => await assertStandalonePurgeProtected(protection, current, roots));
|
|
34
|
+
return inspected;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Remove the configured responder's canonical active-conversation history.
|
|
38
|
+
* This root is separate from both run artifacts and `memory.path`; callers must
|
|
39
|
+
* stop the worker first so no history transaction is active during deletion.
|
|
40
|
+
*/
|
|
41
|
+
export async function purgeConversationHistory(input) {
|
|
42
|
+
return await withClearSessionsDestructiveProtection(input.cwd, async (protection) => {
|
|
43
|
+
const current = await resolveCurrentConversationStateRoots(input);
|
|
44
|
+
const root = await resolveAndAttestConversationStatePurgeRoot("durable session/tool history", current.roots.history);
|
|
45
|
+
return await purgeConversationHistoryRoot(root, protection, current);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
async function purgeConversationHistoryRoot(root, protection, current) {
|
|
49
|
+
await assertStandalonePurgeProtected(protection, current, [root]);
|
|
50
|
+
const inspected = await inspectConversationHistoryRoot(root);
|
|
51
|
+
await securelyRemoveStandaloneRoots(protection.registry, root.target === undefined ? [] : [root], async () => await assertStandalonePurgeProtected(protection, current, [root]));
|
|
52
|
+
return inspected;
|
|
53
|
+
}
|
|
54
|
+
async function inspectConversationHistoryRoot(root) {
|
|
55
|
+
let messageHistory = { files: 0, bytes: 0 };
|
|
56
|
+
if (root.target === undefined) {
|
|
57
|
+
return {
|
|
58
|
+
root: root.path,
|
|
59
|
+
removed: false,
|
|
60
|
+
messageHistory,
|
|
61
|
+
toolHistory: { files: 0, bytes: 0, countsKnown: true, calls: 0, records: 0, tombstones: 0 },
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
messageHistory = await countTopLevelFilesWithSuffix(root.path, ".history.json");
|
|
65
|
+
const sidecarUsage = await toolHistoryDiskUsage(root.path);
|
|
66
|
+
const ownerUsage = await optionalFileBytes(join(root.path, ".locks", TOOL_HISTORY_OWNER_DATABASE));
|
|
67
|
+
let toolCounts;
|
|
68
|
+
let countsKnown = true;
|
|
69
|
+
try {
|
|
70
|
+
const stats = new ToolHistoryReader(root.path).stats();
|
|
71
|
+
toolCounts = stats === undefined ? { calls: 0, records: 0, tombstones: 0 } : stats;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
countsKnown = false;
|
|
75
|
+
}
|
|
76
|
+
const toolHistory = {
|
|
77
|
+
files: sidecarUsage.files + ownerUsage.files,
|
|
78
|
+
bytes: sidecarUsage.bytes + ownerUsage.bytes,
|
|
79
|
+
countsKnown,
|
|
80
|
+
...(toolCounts === undefined ? {} : {
|
|
81
|
+
calls: toolCounts.calls,
|
|
82
|
+
records: toolCounts.records,
|
|
83
|
+
tombstones: toolCounts.tombstones,
|
|
84
|
+
}),
|
|
85
|
+
};
|
|
86
|
+
return { root: root.path, removed: true, messageHistory, toolHistory };
|
|
87
|
+
}
|
|
88
|
+
/** Revoke every durable ACP session id associated with the configured responder. */
|
|
89
|
+
export async function purgeAcpSessionAuthorizations(input) {
|
|
90
|
+
return await withClearSessionsDestructiveProtection(input.cwd, async (protection) => {
|
|
91
|
+
const current = await resolveCurrentConversationStateRoots(input);
|
|
92
|
+
const root = await resolveAndAttestConversationStatePurgeRoot("ACP sessions", current.roots.acpSessions);
|
|
93
|
+
return await purgeAcpSessionAuthorizationsRoot(root, protection, current);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
async function purgeAcpSessionAuthorizationsRoot(root, protection, current) {
|
|
97
|
+
await assertStandalonePurgeProtected(protection, current, [root]);
|
|
98
|
+
const inspected = await inspectAcpSessionAuthorizationsRoot(root);
|
|
99
|
+
await securelyRemoveStandaloneRoots(protection.registry, root.target === undefined ? [] : [root], async () => await assertStandalonePurgeProtected(protection, current, [root]));
|
|
100
|
+
return inspected;
|
|
101
|
+
}
|
|
102
|
+
/** Clear every persisted conversation-continuity store while preserving memory and run artifacts. */
|
|
103
|
+
export async function purgeConversationState(input, options = {}) {
|
|
104
|
+
return await withClearSessionsDestructiveProtection(input.cwd, async (protection) => {
|
|
105
|
+
const snapshot = await readProcessJobsConfigSnapshot(input);
|
|
106
|
+
const frozenInput = { ...input, env: { ...snapshot.env } };
|
|
107
|
+
const workspace = resolveProcessJobsRegistryWorkspace(snapshot, input.cwd);
|
|
108
|
+
const plan = await resolveConversationStatePurgePlan(frozenInput, snapshot);
|
|
109
|
+
// A stale default store remains protected even after processJobs is removed
|
|
110
|
+
// from config. Startup stays dormant; only this destructive path opts in.
|
|
111
|
+
const processJobs = await loadProcessJobsSettings(frozenInput, {
|
|
112
|
+
purgePlan: plan,
|
|
113
|
+
validateDormantStateRoot: true,
|
|
114
|
+
snapshot,
|
|
115
|
+
});
|
|
116
|
+
await assertProcessJobsConfigSnapshotUnchanged(snapshot);
|
|
117
|
+
assertPurgeRootsDisjoint(plan);
|
|
118
|
+
assertRegistryPathDisjoint(clearSessionsRegistryRoot(frozenInput.cwd), plan, processJobs.stateDir);
|
|
119
|
+
await assertRegistryDisjoint(protection.registry, plan, processJobs.stateDir);
|
|
120
|
+
const purgeEntries = conversationStatePurgePlanEntries(plan);
|
|
121
|
+
const attestedProcessRegistry = await protection.processRegistry.reattest(workspace);
|
|
122
|
+
assertProcessJobsRegistryDisjointFromPaths(attestedProcessRegistry, clearSessionsDestructivePaths(protection.registry, purgeEntries));
|
|
123
|
+
await options.hooks?.afterValidation?.(plan);
|
|
124
|
+
// Re-attest every target before counting so a detected swap cannot redirect
|
|
125
|
+
// even read-only traversal, and again after counting before the first rename.
|
|
126
|
+
const preInspectionRegistry = await protection.processRegistry.reattest(workspace);
|
|
127
|
+
assertProcessJobsRegistryDisjointFromPaths(preInspectionRegistry, clearSessionsDestructivePaths(protection.registry, purgeEntries));
|
|
128
|
+
await Promise.all([
|
|
129
|
+
assertProcessJobsConfigSnapshotUnchanged(snapshot),
|
|
130
|
+
assertConversationStatePurgePlanUnchanged(plan),
|
|
131
|
+
]);
|
|
132
|
+
const [sessions, history, acpSessions] = await Promise.all([
|
|
133
|
+
inspectSessionsRoot(plan.sessions),
|
|
134
|
+
inspectConversationHistoryRoot(plan.history),
|
|
135
|
+
inspectAcpSessionAuthorizationsRoot(plan.acpSessions),
|
|
136
|
+
]);
|
|
137
|
+
await Promise.all([
|
|
138
|
+
assertProcessJobsConfigSnapshotUnchanged(snapshot),
|
|
139
|
+
assertConversationStatePurgePlanUnchanged(plan),
|
|
140
|
+
]);
|
|
141
|
+
await securelyRemovePurgeRoots(purgeEntries.filter((root) => root.target !== undefined), protection.registry, options, async () => {
|
|
142
|
+
const currentRegistry = await protection.processRegistry.reattest(workspace);
|
|
143
|
+
assertProcessJobsRegistryDisjointFromPaths(currentRegistry, clearSessionsDestructivePaths(protection.registry, purgeEntries));
|
|
144
|
+
await Promise.all([
|
|
145
|
+
assertProcessJobsConfigSnapshotUnchanged(snapshot),
|
|
146
|
+
assertConversationStatePurgePlanUnchanged(plan),
|
|
147
|
+
]);
|
|
148
|
+
});
|
|
149
|
+
return { sessions, history, acpSessions };
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
async function inspectSessionsRoot(root) {
|
|
153
|
+
if (root === undefined)
|
|
17
154
|
return { removed: false, files: 0 };
|
|
155
|
+
if (root.target === undefined)
|
|
156
|
+
return { root: root.path, removed: false, files: 0 };
|
|
157
|
+
return { root: root.path, removed: true, files: await countSessionFiles(root.path) };
|
|
158
|
+
}
|
|
159
|
+
async function inspectAcpSessionAuthorizationsRoot(root) {
|
|
160
|
+
if (root.target === undefined)
|
|
161
|
+
return { root: root.path, removed: false, files: 0 };
|
|
162
|
+
return { root: root.path, removed: true, files: await countFilesWithSuffix(root.path, ".json") };
|
|
163
|
+
}
|
|
164
|
+
function assertPurgeRootsDisjoint(plan) {
|
|
165
|
+
const roots = conversationStatePurgePlanEntries(plan);
|
|
166
|
+
for (let first = 0; first < roots.length; first += 1) {
|
|
167
|
+
for (let second = first + 1; second < roots.length; second += 1) {
|
|
168
|
+
const left = roots[first];
|
|
169
|
+
const right = roots[second];
|
|
170
|
+
if (!pathsContainEachOther(left.canonicalPath, right.canonicalPath))
|
|
171
|
+
continue;
|
|
172
|
+
throw new Error(`restart --clear-sessions purge roots must be disjoint; ${left.kind} and ${right.kind} overlap. No conversation state was deleted.`);
|
|
173
|
+
}
|
|
18
174
|
}
|
|
19
|
-
|
|
175
|
+
}
|
|
176
|
+
const CLEAR_SESSIONS_MANIFEST_SCHEMA = "mono-agent.clear-sessions-manifest.v1";
|
|
177
|
+
const MAX_CLEAR_SESSIONS_MANIFESTS = 16;
|
|
178
|
+
const MAX_CLEAR_SESSIONS_CONTROL_ENTRIES = 32;
|
|
179
|
+
const MAX_CLEAR_SESSIONS_MANIFEST_BYTES = 8 * 1024;
|
|
180
|
+
const MANIFEST_NAME = /^manifest-([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})\.json$/u;
|
|
181
|
+
const TEMP_MANIFEST_NAME = /^\.manifest-([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})\.tmp$/u;
|
|
182
|
+
const QUARANTINE_NAME = /^([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})\.quarantine$/u;
|
|
183
|
+
/** Stable model-private registry root used by the sandbox recovery guard. */
|
|
184
|
+
export function clearSessionsRegistryRoot(cwd) {
|
|
185
|
+
return resolveClearSessionsRegistryRoot(cwd);
|
|
186
|
+
}
|
|
187
|
+
/** Generic, path-free model boundary: any pending or unsafe recovery state blocks execution. */
|
|
188
|
+
export async function assertClearSessionsRecoveryResolved(cwd) {
|
|
189
|
+
try {
|
|
190
|
+
const path = clearSessionsRegistryRoot(cwd);
|
|
191
|
+
let registry;
|
|
192
|
+
try {
|
|
193
|
+
registry = await attestStableRegistry(cwd);
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
if (isErrno(error, "ENOENT"))
|
|
197
|
+
return;
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
const entries = await boundedDirectoryEntries(registry.path, MAX_CLEAR_SESSIONS_MANIFESTS);
|
|
201
|
+
await assertPrivateDirectoryUnchanged(registry, "clear-sessions registry");
|
|
202
|
+
if (entries.length !== 0)
|
|
203
|
+
throw new Error("pending");
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
throw new Error("Clear-sessions recovery is unresolved; run restart --clear-sessions before model execution.");
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
async function securelyRemoveStandaloneRoots(registry, roots, beforeFirstRename) {
|
|
210
|
+
for (const root of roots) {
|
|
211
|
+
if (pathsContainEachOther(registry.canonicalPath, root.canonicalPath)) {
|
|
212
|
+
throw new Error("Clear-sessions registry must be disjoint from every purge root.");
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
await securelyRemovePurgeRoots(roots, registry, {}, beforeFirstRename);
|
|
216
|
+
}
|
|
217
|
+
async function resolveCurrentConversationStateRoots(input) {
|
|
218
|
+
const snapshot = await readProcessJobsConfigSnapshot(input);
|
|
219
|
+
const frozenInput = { ...input, env: { ...snapshot.env } };
|
|
220
|
+
const workspace = resolveProcessJobsRegistryWorkspace(snapshot, input.cwd);
|
|
221
|
+
const roots = await resolveConversationStatePurgeRoots(frozenInput, snapshot);
|
|
222
|
+
return { snapshot, workspace, roots };
|
|
223
|
+
}
|
|
224
|
+
async function assertStandalonePurgeProtected(protection, current, roots) {
|
|
225
|
+
assertClearSessionsRegistryDisjointFromRoots(protection.registry, roots);
|
|
226
|
+
const processRegistry = await protection.processRegistry.reattest(current.workspace);
|
|
227
|
+
await Promise.all([
|
|
228
|
+
assertProcessJobsConfigSnapshotUnchanged(current.snapshot),
|
|
229
|
+
...roots.map(assertConversationStatePurgeRootUnchanged),
|
|
230
|
+
]);
|
|
231
|
+
assertProcessJobsRegistryDisjointFromPaths(processRegistry, clearSessionsDestructivePaths(protection.registry, roots));
|
|
232
|
+
}
|
|
233
|
+
function assertClearSessionsRegistryDisjointFromRoots(registry, roots) {
|
|
234
|
+
for (const root of roots) {
|
|
235
|
+
if ([registry.path, registry.canonicalPath].some((registryPath) => [root.path, root.canonicalPath].some((rootPath) => pathsContainEachOther(registryPath, rootPath)))) {
|
|
236
|
+
throw new Error("Clear-sessions registry must be disjoint from every purge root.");
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function clearSessionsDestructivePaths(registry, roots) {
|
|
241
|
+
return [
|
|
242
|
+
registry.path,
|
|
243
|
+
registry.canonicalPath,
|
|
244
|
+
...roots.flatMap((root) => [
|
|
245
|
+
root.path,
|
|
246
|
+
root.canonicalPath,
|
|
247
|
+
join(root.parent?.path ?? dirname(root.path), CLEAR_SESSIONS_CONTROL_DIRECTORY),
|
|
248
|
+
join(root.parent?.canonicalPath ?? dirname(root.canonicalPath), CLEAR_SESSIONS_CONTROL_DIRECTORY),
|
|
249
|
+
]),
|
|
250
|
+
];
|
|
251
|
+
}
|
|
252
|
+
async function withClearSessionsDestructiveProtection(cwd, run) {
|
|
253
|
+
const protection = await prepareClearSessionsRecovery(cwd);
|
|
254
|
+
try {
|
|
255
|
+
return await run(protection);
|
|
256
|
+
}
|
|
257
|
+
finally {
|
|
258
|
+
await protection.processRegistry.release().catch(() => undefined);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
async function prepareClearSessionsRecovery(cwd) {
|
|
262
|
+
// Recovery cannot depend on the current config: an invalid replacement must
|
|
263
|
+
// not strand an older durable quarantine. The canonical agent root is a
|
|
264
|
+
// structural workspace that safely validates every registered descendant.
|
|
265
|
+
const structuralWorkspace = await realpath(resolve(cwd));
|
|
266
|
+
const processRegistry = await freezeProcessJobsRootRegistry(cwd, structuralWorkspace);
|
|
20
267
|
try {
|
|
21
|
-
|
|
268
|
+
const registry = await ensureClearSessionsRegistry(cwd);
|
|
269
|
+
const pendingRecoveryPaths = await inspectClearSessionsRecoveryPaths(registry);
|
|
270
|
+
const attestedProcessRegistry = await processRegistry.reattest(structuralWorkspace);
|
|
271
|
+
assertProcessJobsRegistryDisjointFromPaths(attestedProcessRegistry, pendingRecoveryPaths);
|
|
272
|
+
await reconcileClearSessionsRecovery(registry);
|
|
273
|
+
return { registry, processRegistry };
|
|
22
274
|
}
|
|
23
275
|
catch (error) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
276
|
+
await processRegistry.release().catch(() => undefined);
|
|
277
|
+
throw error;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
/** Read every pending destructive/control path without mutating recovery state. */
|
|
281
|
+
async function inspectClearSessionsRecoveryPaths(registry) {
|
|
282
|
+
const entries = await boundedDirectoryEntries(registry.path, MAX_CLEAR_SESSIONS_MANIFESTS);
|
|
283
|
+
const paths = [registry.path, registry.canonicalPath];
|
|
284
|
+
for (const entry of entries) {
|
|
285
|
+
if (TEMP_MANIFEST_NAME.test(entry.name)) {
|
|
286
|
+
if (!entry.isFile() || entry.isSymbolicLink()) {
|
|
287
|
+
throw new Error("Clear-sessions registry contains an unsafe temporary manifest.");
|
|
288
|
+
}
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
if (!entry.isFile() || entry.isSymbolicLink() || !MANIFEST_NAME.test(entry.name)) {
|
|
292
|
+
throw new Error("Clear-sessions registry contains an unsupported entry; recovery remains unresolved.");
|
|
293
|
+
}
|
|
294
|
+
const manifest = (await readManifest(registry, entry.name)).value;
|
|
295
|
+
paths.push(manifest.originalPath, manifest.originalCanonicalPath, manifest.originalParentPath, manifest.originalParentCanonicalPath, manifest.controlPath, manifest.controlCanonicalPath, manifest.quarantinePath, manifest.quarantineCanonicalPath);
|
|
296
|
+
}
|
|
297
|
+
await assertPrivateDirectoryUnchanged(registry, "clear-sessions registry");
|
|
298
|
+
return paths;
|
|
299
|
+
}
|
|
300
|
+
async function securelyRemovePurgeRoots(roots, registry, options, beforeFirstRename) {
|
|
301
|
+
if (roots.length === 0)
|
|
302
|
+
return;
|
|
303
|
+
await Promise.all(roots.map(assertConversationStatePurgeRootUnchanged));
|
|
304
|
+
const quarantined = [];
|
|
305
|
+
for (const [index, root] of roots.entries()) {
|
|
306
|
+
await assertConversationStatePurgeRootUnchanged(root);
|
|
307
|
+
const value = await quarantinePurgeRoot(root, registry, index === 0 ? beforeFirstRename : undefined, options.hooks?.afterManifestPublished);
|
|
308
|
+
quarantined.push(value);
|
|
309
|
+
await options.hooks?.afterRootQuarantined?.(value.path);
|
|
310
|
+
}
|
|
311
|
+
await Promise.all(quarantined.map(assertQuarantinedRootUnchanged));
|
|
312
|
+
for (const value of quarantined) {
|
|
313
|
+
await options.hooks?.beforeQuarantineRemoval?.(value.path);
|
|
314
|
+
await assertQuarantinedRootUnchanged(value);
|
|
315
|
+
// The owner-private control directory is protected from model tools, and
|
|
316
|
+
// same-UID ambient OS processes are outside this deletion boundary.
|
|
317
|
+
await rm(value.path, { recursive: true, force: false });
|
|
318
|
+
await syncAndReattestPrivateDirectory(value.control, "clear-sessions control directory");
|
|
319
|
+
await options.hooks?.afterQuarantineRemoved?.(value.path);
|
|
320
|
+
await removeManifest(value);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
async function quarantinePurgeRoot(root, registry, beforeRename, afterManifestPublished) {
|
|
324
|
+
const target = root.target;
|
|
325
|
+
if (target === undefined)
|
|
326
|
+
throw new Error(`Cannot quarantine missing purge root: ${root.path}`);
|
|
327
|
+
const control = await ensurePrivateDirectory(join(target.parent.path, CLEAR_SESSIONS_CONTROL_DIRECTORY), "clear-sessions control directory");
|
|
328
|
+
if (pathsContainEachOther(control.canonicalPath, root.canonicalPath)) {
|
|
329
|
+
throw new Error("Clear-sessions control directory must be disjoint from its purge root.");
|
|
330
|
+
}
|
|
331
|
+
const id = randomUUID();
|
|
332
|
+
const quarantinePath = join(control.path, `${id}.quarantine`);
|
|
333
|
+
const quarantineCanonicalPath = join(control.canonicalPath, `${id}.quarantine`);
|
|
334
|
+
const manifestPath = join(registry.path, `manifest-${id}.json`);
|
|
335
|
+
const temporaryManifestPath = join(registry.path, `.manifest-${id}.tmp`);
|
|
336
|
+
await Promise.all([
|
|
337
|
+
assertMissing(quarantinePath, "Clear-sessions quarantine destination already exists."),
|
|
338
|
+
assertMissing(manifestPath, "Clear-sessions manifest destination already exists."),
|
|
339
|
+
assertMissing(temporaryManifestPath, "Clear-sessions temporary manifest destination already exists."),
|
|
340
|
+
]);
|
|
341
|
+
const manifest = {
|
|
342
|
+
schema: CLEAR_SESSIONS_MANIFEST_SCHEMA,
|
|
343
|
+
id,
|
|
344
|
+
kind: root.kind,
|
|
345
|
+
originalPath: root.path,
|
|
346
|
+
originalCanonicalPath: root.canonicalPath,
|
|
347
|
+
originalIdentity: wireIdentity(target.identity),
|
|
348
|
+
originalParentPath: target.parent.path,
|
|
349
|
+
originalParentCanonicalPath: target.parent.canonicalPath,
|
|
350
|
+
originalParentIdentity: wireIdentity(target.parent.identity),
|
|
351
|
+
controlPath: control.path,
|
|
352
|
+
controlCanonicalPath: control.canonicalPath,
|
|
353
|
+
controlIdentity: wireIdentity(control.identity),
|
|
354
|
+
quarantinePath,
|
|
355
|
+
quarantineCanonicalPath,
|
|
356
|
+
};
|
|
357
|
+
const manifestIdentity = await writeManifest(registry, temporaryManifestPath, manifestPath, manifest);
|
|
358
|
+
await afterManifestPublished?.(manifestPath);
|
|
359
|
+
await Promise.all([
|
|
360
|
+
assertConversationStatePurgeRootUnchanged(root),
|
|
361
|
+
assertPrivateDirectoryUnchanged(control, "clear-sessions control directory"),
|
|
362
|
+
assertPrivateDirectoryUnchanged(registry, "clear-sessions registry"),
|
|
363
|
+
]);
|
|
364
|
+
await beforeRename?.();
|
|
365
|
+
await rename(root.path, quarantinePath);
|
|
366
|
+
const quarantined = {
|
|
367
|
+
root,
|
|
368
|
+
registry,
|
|
369
|
+
control,
|
|
370
|
+
path: quarantinePath,
|
|
371
|
+
canonicalPath: quarantineCanonicalPath,
|
|
372
|
+
manifestPath,
|
|
373
|
+
manifestIdentity,
|
|
374
|
+
};
|
|
375
|
+
await assertQuarantinedRootUnchanged(quarantined);
|
|
376
|
+
await syncAndReattestParent(root);
|
|
377
|
+
await syncAndReattestPrivateDirectory(control, "clear-sessions control directory");
|
|
378
|
+
return quarantined;
|
|
379
|
+
}
|
|
380
|
+
async function assertQuarantinedRootUnchanged(value) {
|
|
381
|
+
const target = value.root.target;
|
|
382
|
+
if (target === undefined)
|
|
383
|
+
throw new Error(`Missing attestation for quarantined purge root: ${value.root.path}`);
|
|
384
|
+
await Promise.all([
|
|
385
|
+
assertParentUnchanged(value.root),
|
|
386
|
+
assertPrivateDirectoryUnchanged(value.control, "clear-sessions control directory"),
|
|
387
|
+
assertPrivateDirectoryUnchanged(value.registry, "clear-sessions registry"),
|
|
388
|
+
]);
|
|
389
|
+
const details = await lstat(value.path, { bigint: true });
|
|
390
|
+
assertRealDirectory(details, value.path);
|
|
391
|
+
if (!sameFileSystemIdentity(details, target.identity) || await realpath(value.path) !== value.canonicalPath) {
|
|
392
|
+
throw new Error(`restart --clear-sessions quarantined ${value.root.kind} changed; the replacement was left untouched.`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
async function removeManifest(value) {
|
|
396
|
+
await assertPrivateDirectoryUnchanged(value.registry, "clear-sessions registry");
|
|
397
|
+
const details = await lstat(value.manifestPath, { bigint: true });
|
|
398
|
+
assertPrivateFile(details, "clear-sessions manifest");
|
|
399
|
+
if (!sameFileSystemIdentity(details, value.manifestIdentity)) {
|
|
400
|
+
throw new Error("Clear-sessions manifest identity changed; recovery remains unresolved.");
|
|
401
|
+
}
|
|
402
|
+
await unlink(value.manifestPath);
|
|
403
|
+
await syncAndReattestPrivateDirectory(value.registry, "clear-sessions registry");
|
|
404
|
+
}
|
|
405
|
+
async function syncAndReattestParent(root) {
|
|
406
|
+
const parent = root.target?.parent;
|
|
407
|
+
if (parent === undefined)
|
|
408
|
+
throw new Error(`Missing parent attestation for purge root: ${root.path}`);
|
|
409
|
+
await assertParentUnchanged(root);
|
|
410
|
+
await syncDirectory(parent.path);
|
|
411
|
+
await assertParentUnchanged(root);
|
|
412
|
+
}
|
|
413
|
+
async function assertParentUnchanged(root) {
|
|
414
|
+
const parent = root.target?.parent ?? root.parent;
|
|
415
|
+
if (parent === undefined)
|
|
416
|
+
throw new Error(`Missing parent attestation for purge root: ${root.path}`);
|
|
417
|
+
const details = await lstat(parent.path, { bigint: true });
|
|
418
|
+
assertSecureContainingDirectory(details, parent.path);
|
|
419
|
+
if (!sameFileSystemIdentity(details, parent.identity) || await realpath(parent.path) !== parent.canonicalPath) {
|
|
420
|
+
throw new Error(`restart --clear-sessions ${root.kind} parent identity or canonical path changed; no replacement was deleted.`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
async function ensureClearSessionsRegistry(cwd) {
|
|
424
|
+
await ensureClearSessionsRegistryParent(cwd);
|
|
425
|
+
await ensurePrivateDirectory(clearSessionsRegistryRoot(cwd), "clear-sessions registry");
|
|
426
|
+
return await attestStableRegistry(cwd);
|
|
427
|
+
}
|
|
428
|
+
async function ensureClearSessionsRegistryParent(cwd) {
|
|
429
|
+
const agentRoot = resolve(cwd);
|
|
430
|
+
const monoAgentRoot = dirname(clearSessionsRegistryRoot(cwd));
|
|
431
|
+
await ensureDirectoryUnderSecureParent(agentRoot, monoAgentRoot, ".mono-agent state directory", false);
|
|
432
|
+
}
|
|
433
|
+
async function attestStableRegistry(cwd) {
|
|
434
|
+
const agentRoot = await attestDirectory(resolve(cwd), "agent root", false);
|
|
435
|
+
const monoAgentRoot = await attestDirectory(dirname(clearSessionsRegistryRoot(cwd)), ".mono-agent state directory", false);
|
|
436
|
+
const registry = await attestPrivateDirectory(clearSessionsRegistryRoot(cwd), "clear-sessions registry");
|
|
437
|
+
if (dirname(monoAgentRoot.canonicalPath) !== agentRoot.canonicalPath
|
|
438
|
+
|| dirname(registry.canonicalPath) !== monoAgentRoot.canonicalPath) {
|
|
439
|
+
throw new Error("Clear-sessions registry escaped its attested agent root.");
|
|
440
|
+
}
|
|
441
|
+
return registry;
|
|
442
|
+
}
|
|
443
|
+
async function ensureDirectoryUnderSecureParent(parent, path, label, ownerPrivate) {
|
|
444
|
+
const parentBefore = await lstat(parent, { bigint: true });
|
|
445
|
+
assertSecureContainingDirectory(parentBefore, parent);
|
|
446
|
+
try {
|
|
447
|
+
await mkdir(path, { mode: 0o700 });
|
|
448
|
+
}
|
|
449
|
+
catch (error) {
|
|
450
|
+
if (!isErrno(error, "EEXIST"))
|
|
451
|
+
throw error;
|
|
452
|
+
}
|
|
453
|
+
const parentAfter = await lstat(parent, { bigint: true });
|
|
454
|
+
assertSecureContainingDirectory(parentAfter, parent);
|
|
455
|
+
if (!sameFileSystemIdentity(parentBefore, parentAfter))
|
|
456
|
+
throw new Error(`${label} parent changed during creation.`);
|
|
457
|
+
return await attestDirectory(path, label, ownerPrivate);
|
|
458
|
+
}
|
|
459
|
+
async function ensurePrivateDirectory(path, label) {
|
|
460
|
+
return await ensureDirectoryUnderSecureParent(dirname(path), path, label, true);
|
|
461
|
+
}
|
|
462
|
+
async function attestPrivateDirectory(path, label) {
|
|
463
|
+
return await attestDirectory(path, label, true);
|
|
464
|
+
}
|
|
465
|
+
async function attestDirectory(path, label, ownerPrivate) {
|
|
466
|
+
const initial = await lstat(path, { bigint: true });
|
|
467
|
+
assertSecureContainingDirectory(initial, path);
|
|
468
|
+
if (ownerPrivate && (initial.mode & 63n) !== 0n)
|
|
469
|
+
throw new Error(`${label} must be owner-only.`);
|
|
470
|
+
const canonicalPath = await realpath(path);
|
|
471
|
+
const current = await lstat(path, { bigint: true });
|
|
472
|
+
assertSecureContainingDirectory(current, path);
|
|
473
|
+
if (!sameFileSystemIdentity(initial, current))
|
|
474
|
+
throw new Error(`${label} identity changed during attestation.`);
|
|
475
|
+
return { path, canonicalPath, identity: { dev: initial.dev, ino: initial.ino } };
|
|
476
|
+
}
|
|
477
|
+
async function assertPrivateDirectoryUnchanged(value, label) {
|
|
478
|
+
const current = await attestPrivateDirectory(value.path, label);
|
|
479
|
+
if (current.canonicalPath !== value.canonicalPath || !sameFileSystemIdentity(current.identity, value.identity)) {
|
|
480
|
+
throw new Error(`${label} identity or canonical path changed.`);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
async function syncAndReattestPrivateDirectory(value, label) {
|
|
484
|
+
await assertPrivateDirectoryUnchanged(value, label);
|
|
485
|
+
await syncDirectory(value.path);
|
|
486
|
+
await assertPrivateDirectoryUnchanged(value, label);
|
|
487
|
+
}
|
|
488
|
+
async function writeManifest(registry, temporaryPath, path, manifest) {
|
|
489
|
+
const body = `${JSON.stringify(manifest)}\n`;
|
|
490
|
+
if (Buffer.byteLength(body) > MAX_CLEAR_SESSIONS_MANIFEST_BYTES)
|
|
491
|
+
throw new Error("Clear-sessions manifest is too large.");
|
|
492
|
+
await assertPrivateDirectoryUnchanged(registry, "clear-sessions registry");
|
|
493
|
+
const handle = await open(temporaryPath, "wx", 0o600);
|
|
494
|
+
try {
|
|
495
|
+
await handle.writeFile(body, "utf8");
|
|
496
|
+
await handle.sync();
|
|
497
|
+
const details = await handle.stat({ bigint: true });
|
|
498
|
+
assertPrivateFile(details, "clear-sessions manifest");
|
|
499
|
+
const named = await lstat(temporaryPath, { bigint: true });
|
|
500
|
+
assertPrivateFile(named, "clear-sessions manifest");
|
|
501
|
+
if (!sameFileSystemIdentity(details, named))
|
|
502
|
+
throw new Error("Clear-sessions manifest changed during publication.");
|
|
503
|
+
await rename(temporaryPath, path);
|
|
504
|
+
const published = await lstat(path, { bigint: true });
|
|
505
|
+
assertPrivateFile(published, "clear-sessions manifest");
|
|
506
|
+
if (!sameFileSystemIdentity(details, published))
|
|
507
|
+
throw new Error("Clear-sessions manifest changed during publication.");
|
|
508
|
+
await syncAndReattestPrivateDirectory(registry, "clear-sessions registry");
|
|
509
|
+
return { dev: details.dev, ino: details.ino };
|
|
510
|
+
}
|
|
511
|
+
finally {
|
|
512
|
+
await handle.close();
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
async function reconcileClearSessionsRecovery(registry) {
|
|
516
|
+
const entries = await boundedDirectoryEntries(registry.path, MAX_CLEAR_SESSIONS_MANIFESTS);
|
|
517
|
+
const temporaryEntries = entries.filter((entry) => TEMP_MANIFEST_NAME.test(entry.name));
|
|
518
|
+
for (const entry of temporaryEntries) {
|
|
519
|
+
if (!entry.isFile() || entry.isSymbolicLink())
|
|
520
|
+
throw new Error("Clear-sessions registry contains an unsafe temporary manifest.");
|
|
521
|
+
const path = join(registry.path, entry.name);
|
|
522
|
+
const details = await lstat(path, { bigint: true });
|
|
523
|
+
assertPrivateFile(details, "clear-sessions temporary manifest");
|
|
524
|
+
await unlink(path);
|
|
525
|
+
await syncAndReattestPrivateDirectory(registry, "clear-sessions registry");
|
|
526
|
+
}
|
|
527
|
+
const manifestEntries = entries.filter((entry) => !TEMP_MANIFEST_NAME.test(entry.name));
|
|
528
|
+
const loaded = await Promise.all(manifestEntries.map(async (entry) => {
|
|
529
|
+
if (!entry.isFile() || entry.isSymbolicLink() || !MANIFEST_NAME.test(entry.name)) {
|
|
530
|
+
throw new Error("Clear-sessions registry contains an unsupported entry; recovery remains unresolved.");
|
|
531
|
+
}
|
|
532
|
+
return await readManifest(registry, entry.name);
|
|
533
|
+
}));
|
|
534
|
+
const ids = new Set();
|
|
535
|
+
for (const manifest of loaded) {
|
|
536
|
+
if (ids.has(manifest.value.id))
|
|
537
|
+
throw new Error("Clear-sessions registry contains a duplicate manifest.");
|
|
538
|
+
ids.add(manifest.value.id);
|
|
539
|
+
}
|
|
540
|
+
await Promise.all(loaded.map((manifest) => validateRecoveryManifest(manifest.value)));
|
|
541
|
+
await validateRecoveryControls(loaded);
|
|
542
|
+
for (const manifest of loaded)
|
|
543
|
+
await reconcileManifest(registry, manifest);
|
|
544
|
+
const remaining = await boundedDirectoryEntries(registry.path, MAX_CLEAR_SESSIONS_MANIFESTS);
|
|
545
|
+
if (remaining.length !== 0)
|
|
546
|
+
throw new Error("Clear-sessions recovery did not settle every manifest.");
|
|
547
|
+
await syncAndReattestPrivateDirectory(registry, "clear-sessions registry");
|
|
548
|
+
}
|
|
549
|
+
async function validateRecoveryManifest(manifest) {
|
|
550
|
+
await attestManifestControl(manifest);
|
|
551
|
+
const [original, quarantined] = await Promise.all([
|
|
552
|
+
optionalLstat(manifest.originalPath),
|
|
553
|
+
optionalLstat(manifest.quarantinePath),
|
|
554
|
+
]);
|
|
555
|
+
if (original !== undefined && quarantined !== undefined) {
|
|
556
|
+
throw new Error("Clear-sessions recovery found both original and quarantine targets; neither was deleted.");
|
|
557
|
+
}
|
|
558
|
+
if (quarantined !== undefined) {
|
|
559
|
+
assertRealDirectory(quarantined, manifest.quarantinePath);
|
|
560
|
+
if (!sameWireIdentity(quarantined, manifest.originalIdentity)
|
|
561
|
+
|| await realpath(manifest.quarantinePath) !== manifest.quarantineCanonicalPath) {
|
|
562
|
+
throw new Error("Clear-sessions quarantine identity changed; the replacement was left untouched.");
|
|
563
|
+
}
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
if (original !== undefined) {
|
|
567
|
+
assertRealDirectory(original, manifest.originalPath);
|
|
568
|
+
if (!sameWireIdentity(original, manifest.originalIdentity)
|
|
569
|
+
|| await realpath(manifest.originalPath) !== manifest.originalCanonicalPath) {
|
|
570
|
+
throw new Error("Clear-sessions original target changed while recovery was pending; it was left untouched.");
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
async function validateRecoveryControls(manifests) {
|
|
575
|
+
const byControl = new Map();
|
|
576
|
+
for (const manifest of manifests) {
|
|
577
|
+
const values = byControl.get(manifest.value.controlPath) ?? [];
|
|
578
|
+
values.push(manifest);
|
|
579
|
+
byControl.set(manifest.value.controlPath, values);
|
|
580
|
+
}
|
|
581
|
+
for (const [path, values] of byControl) {
|
|
582
|
+
const control = await attestManifestControl(values[0].value);
|
|
583
|
+
const entries = await boundedDirectoryEntries(path, MAX_CLEAR_SESSIONS_CONTROL_ENTRIES);
|
|
584
|
+
const expected = new Set(values.map((value) => `${value.value.id}.quarantine`));
|
|
585
|
+
for (const entry of entries) {
|
|
586
|
+
if (!entry.isDirectory() || entry.isSymbolicLink()
|
|
587
|
+
|| !QUARANTINE_NAME.test(entry.name) || !expected.has(entry.name)) {
|
|
588
|
+
throw new Error("Clear-sessions control directory contains an unattested entry; recovery remains unresolved.");
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
await assertPrivateDirectoryUnchanged(control, "clear-sessions control directory");
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
async function reconcileManifest(registry, loaded) {
|
|
595
|
+
const manifest = loaded.value;
|
|
596
|
+
const control = await attestManifestControl(manifest);
|
|
597
|
+
const original = await optionalLstat(manifest.originalPath);
|
|
598
|
+
const quarantined = await optionalLstat(manifest.quarantinePath);
|
|
599
|
+
if (quarantined !== undefined) {
|
|
600
|
+
if (original !== undefined) {
|
|
601
|
+
throw new Error("Clear-sessions recovery found both original and quarantine targets; neither was deleted.");
|
|
602
|
+
}
|
|
603
|
+
assertRealDirectory(quarantined, manifest.quarantinePath);
|
|
604
|
+
if (!sameWireIdentity(quarantined, manifest.originalIdentity)
|
|
605
|
+
|| await realpath(manifest.quarantinePath) !== manifest.quarantineCanonicalPath) {
|
|
606
|
+
throw new Error("Clear-sessions quarantine identity changed; the replacement was left untouched.");
|
|
607
|
+
}
|
|
608
|
+
await assertPrivateDirectoryUnchanged(control, "clear-sessions control directory");
|
|
609
|
+
await rm(manifest.quarantinePath, { recursive: true, force: false });
|
|
610
|
+
await syncAndReattestPrivateDirectory(control, "clear-sessions control directory");
|
|
611
|
+
}
|
|
612
|
+
else if (original !== undefined) {
|
|
613
|
+
assertRealDirectory(original, manifest.originalPath);
|
|
614
|
+
if (!sameWireIdentity(original, manifest.originalIdentity)
|
|
615
|
+
|| await realpath(manifest.originalPath) !== manifest.originalCanonicalPath) {
|
|
616
|
+
throw new Error("Clear-sessions original target changed while recovery was pending; it was left untouched.");
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
await removeLoadedManifest(registry, loaded);
|
|
620
|
+
}
|
|
621
|
+
async function readManifest(registry, name) {
|
|
622
|
+
const match = MANIFEST_NAME.exec(name);
|
|
623
|
+
if (match === null)
|
|
624
|
+
throw new Error("Clear-sessions manifest name is invalid.");
|
|
625
|
+
const path = join(registry.path, name);
|
|
626
|
+
await assertPrivateDirectoryUnchanged(registry, "clear-sessions registry");
|
|
627
|
+
const handle = await open(path, fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW ?? 0) | (fsConstants.O_NONBLOCK ?? 0));
|
|
628
|
+
try {
|
|
629
|
+
const before = await handle.stat({ bigint: true });
|
|
630
|
+
assertPrivateFile(before, "clear-sessions manifest");
|
|
631
|
+
if (before.size > BigInt(MAX_CLEAR_SESSIONS_MANIFEST_BYTES))
|
|
632
|
+
throw new Error("Clear-sessions manifest is too large.");
|
|
633
|
+
const bytes = await handle.readFile();
|
|
634
|
+
const after = await handle.stat({ bigint: true });
|
|
635
|
+
const named = await lstat(path, { bigint: true });
|
|
636
|
+
assertPrivateFile(after, "clear-sessions manifest");
|
|
637
|
+
assertPrivateFile(named, "clear-sessions manifest");
|
|
638
|
+
if (!sameManifestFile(before, after) || !sameManifestFile(after, named)
|
|
639
|
+
|| bytes.byteLength !== Number(after.size)) {
|
|
640
|
+
throw new Error("Clear-sessions manifest changed while it was read.");
|
|
641
|
+
}
|
|
642
|
+
let parsed;
|
|
643
|
+
try {
|
|
644
|
+
parsed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
645
|
+
}
|
|
646
|
+
catch (error) {
|
|
647
|
+
throw new Error("Clear-sessions manifest is malformed.", { cause: error });
|
|
648
|
+
}
|
|
649
|
+
const value = parseManifest(parsed, match[1]);
|
|
650
|
+
return { value, path, identity: { dev: before.dev, ino: before.ino } };
|
|
651
|
+
}
|
|
652
|
+
finally {
|
|
653
|
+
await handle.close();
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
function parseManifest(value, expectedId) {
|
|
657
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
658
|
+
throw new Error("Clear-sessions manifest is malformed.");
|
|
659
|
+
const record = value;
|
|
660
|
+
const keys = [
|
|
661
|
+
"controlCanonicalPath", "controlIdentity", "controlPath", "id", "kind",
|
|
662
|
+
"originalCanonicalPath", "originalIdentity", "originalParentCanonicalPath",
|
|
663
|
+
"originalParentIdentity", "originalParentPath", "originalPath", "quarantineCanonicalPath",
|
|
664
|
+
"quarantinePath", "schema",
|
|
665
|
+
];
|
|
666
|
+
if (Object.keys(record).sort().join(",") !== keys.sort().join(","))
|
|
667
|
+
throw new Error("Clear-sessions manifest has unknown fields.");
|
|
668
|
+
if (record.schema !== CLEAR_SESSIONS_MANIFEST_SCHEMA || record.id !== expectedId
|
|
669
|
+
|| !isPurgeKind(record.kind))
|
|
670
|
+
throw new Error("Clear-sessions manifest identity is invalid.");
|
|
671
|
+
const manifest = record;
|
|
672
|
+
for (const path of [
|
|
673
|
+
manifest.originalPath,
|
|
674
|
+
manifest.originalCanonicalPath,
|
|
675
|
+
manifest.originalParentPath,
|
|
676
|
+
manifest.originalParentCanonicalPath,
|
|
677
|
+
manifest.controlPath,
|
|
678
|
+
manifest.controlCanonicalPath,
|
|
679
|
+
manifest.quarantinePath,
|
|
680
|
+
manifest.quarantineCanonicalPath,
|
|
681
|
+
]) {
|
|
682
|
+
if (typeof path !== "string" || !isAbsolute(path))
|
|
683
|
+
throw new Error("Clear-sessions manifest path is invalid.");
|
|
684
|
+
}
|
|
685
|
+
for (const identity of [manifest.originalIdentity, manifest.originalParentIdentity, manifest.controlIdentity]) {
|
|
686
|
+
assertWireIdentity(identity);
|
|
687
|
+
}
|
|
688
|
+
if (dirname(manifest.originalPath) !== manifest.originalParentPath
|
|
689
|
+
|| dirname(manifest.originalCanonicalPath) !== manifest.originalParentCanonicalPath
|
|
690
|
+
|| manifest.controlPath !== join(manifest.originalParentPath, CLEAR_SESSIONS_CONTROL_DIRECTORY)
|
|
691
|
+
|| manifest.controlCanonicalPath !== join(manifest.originalParentCanonicalPath, CLEAR_SESSIONS_CONTROL_DIRECTORY)
|
|
692
|
+
|| manifest.quarantinePath !== join(manifest.controlPath, `${expectedId}.quarantine`)
|
|
693
|
+
|| manifest.quarantineCanonicalPath !== join(manifest.controlCanonicalPath, `${expectedId}.quarantine`)) {
|
|
694
|
+
throw new Error("Clear-sessions manifest path relationships are invalid.");
|
|
695
|
+
}
|
|
696
|
+
return manifest;
|
|
697
|
+
}
|
|
698
|
+
async function attestManifestControl(manifest) {
|
|
699
|
+
const control = await attestPrivateDirectory(manifest.controlPath, "clear-sessions control directory");
|
|
700
|
+
if (control.canonicalPath !== manifest.controlCanonicalPath
|
|
701
|
+
|| !sameWireIdentity(control.identity, manifest.controlIdentity)) {
|
|
702
|
+
throw new Error("Clear-sessions control directory identity changed; recovery remains unresolved.");
|
|
703
|
+
}
|
|
704
|
+
const parent = await lstat(manifest.originalParentPath, { bigint: true });
|
|
705
|
+
assertSecureContainingDirectory(parent, manifest.originalParentPath);
|
|
706
|
+
if (!sameWireIdentity(parent, manifest.originalParentIdentity)
|
|
707
|
+
|| await realpath(manifest.originalParentPath) !== manifest.originalParentCanonicalPath) {
|
|
708
|
+
throw new Error("Clear-sessions original parent changed; recovery remains unresolved.");
|
|
709
|
+
}
|
|
710
|
+
return control;
|
|
711
|
+
}
|
|
712
|
+
async function removeLoadedManifest(registry, loaded) {
|
|
713
|
+
await assertPrivateDirectoryUnchanged(registry, "clear-sessions registry");
|
|
714
|
+
const details = await lstat(loaded.path, { bigint: true });
|
|
715
|
+
assertPrivateFile(details, "clear-sessions manifest");
|
|
716
|
+
if (!sameFileSystemIdentity(details, loaded.identity))
|
|
717
|
+
throw new Error("Clear-sessions manifest identity changed.");
|
|
718
|
+
await unlink(loaded.path);
|
|
719
|
+
await syncAndReattestPrivateDirectory(registry, "clear-sessions registry");
|
|
720
|
+
}
|
|
721
|
+
async function assertRegistryDisjoint(registry, plan, processJobsStateDir) {
|
|
722
|
+
for (const root of conversationStatePurgePlanEntries(plan)) {
|
|
723
|
+
if (pathsContainEachOther(registry.canonicalPath, root.canonicalPath)) {
|
|
724
|
+
throw new Error("Clear-sessions registry must be disjoint from every purge root.");
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
const stateDir = await canonicalExistingPrefix(processJobsStateDir);
|
|
728
|
+
if (pathsContainEachOther(registry.path, stateDir)) {
|
|
729
|
+
throw new Error("Clear-sessions registry must be disjoint from process-job durable state.");
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
async function canonicalExistingPrefix(path) {
|
|
733
|
+
let cursor = resolve(path);
|
|
734
|
+
const missing = [];
|
|
735
|
+
while (true) {
|
|
736
|
+
try {
|
|
737
|
+
return resolve(await realpath(cursor), ...missing);
|
|
738
|
+
}
|
|
739
|
+
catch (error) {
|
|
740
|
+
if (!isErrno(error, "ENOENT"))
|
|
741
|
+
throw error;
|
|
742
|
+
}
|
|
743
|
+
const parent = dirname(cursor);
|
|
744
|
+
if (parent === cursor)
|
|
745
|
+
throw new Error("Clear-sessions path has no canonical ancestor.");
|
|
746
|
+
missing.unshift(basename(cursor));
|
|
747
|
+
cursor = parent;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
function assertRegistryPathDisjoint(registryPath, plan, processJobsStateDir) {
|
|
751
|
+
for (const root of conversationStatePurgePlanEntries(plan)) {
|
|
752
|
+
if (pathsContainEachOther(resolve(registryPath), resolve(root.path))) {
|
|
753
|
+
throw new Error("Clear-sessions registry must be disjoint from every purge root.");
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
if (pathsContainEachOther(resolve(registryPath), resolve(processJobsStateDir))) {
|
|
757
|
+
throw new Error("Clear-sessions registry must be disjoint from process-job durable state.");
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
async function boundedDirectoryEntries(path, maximum) {
|
|
761
|
+
const directory = await opendir(path);
|
|
762
|
+
const entries = [];
|
|
763
|
+
try {
|
|
764
|
+
for await (const entry of directory) {
|
|
765
|
+
if (entries.length >= maximum)
|
|
766
|
+
throw new Error("Clear-sessions recovery work exceeds its safety bound.");
|
|
767
|
+
entries.push(entry);
|
|
27
768
|
}
|
|
769
|
+
}
|
|
770
|
+
finally {
|
|
771
|
+
await directory.close().catch(() => undefined);
|
|
772
|
+
}
|
|
773
|
+
return entries;
|
|
774
|
+
}
|
|
775
|
+
async function optionalLstat(path) {
|
|
776
|
+
try {
|
|
777
|
+
return await lstat(path, { bigint: true });
|
|
778
|
+
}
|
|
779
|
+
catch (error) {
|
|
780
|
+
if (isErrno(error, "ENOENT"))
|
|
781
|
+
return undefined;
|
|
28
782
|
throw error;
|
|
29
783
|
}
|
|
30
|
-
await rm(root, { recursive: true, force: true });
|
|
31
|
-
return { root, removed: true, files };
|
|
32
784
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
785
|
+
function assertSecureContainingDirectory(details, path) {
|
|
786
|
+
assertRealDirectory(details, path);
|
|
787
|
+
if (process.getuid !== undefined && details.uid !== BigInt(process.getuid())) {
|
|
788
|
+
throw new Error("Clear-sessions directory is not owned by the current user.");
|
|
789
|
+
}
|
|
790
|
+
if ((details.mode & 18n) !== 0n) {
|
|
791
|
+
throw new Error("Clear-sessions directory must not be group/world writable.");
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
function assertPrivateFile(details, label) {
|
|
795
|
+
if (!details.isFile() || details.isSymbolicLink() || details.nlink !== 1n
|
|
796
|
+
|| (details.mode & 63n) !== 0n
|
|
797
|
+
|| (process.getuid !== undefined && details.uid !== BigInt(process.getuid()))) {
|
|
798
|
+
throw new Error(`${label} must be one owner-private regular file.`);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
function sameManifestFile(left, right) {
|
|
802
|
+
return sameFileSystemIdentity(left, right)
|
|
803
|
+
&& left.size === right.size
|
|
804
|
+
&& left.mtimeNs === right.mtimeNs
|
|
805
|
+
&& left.ctimeNs === right.ctimeNs
|
|
806
|
+
&& left.mode === right.mode
|
|
807
|
+
&& left.uid === right.uid
|
|
808
|
+
&& left.nlink === right.nlink;
|
|
809
|
+
}
|
|
810
|
+
function wireIdentity(value) {
|
|
811
|
+
return { dev: value.dev.toString(), ino: value.ino.toString() };
|
|
812
|
+
}
|
|
813
|
+
function assertWireIdentity(value) {
|
|
814
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
815
|
+
throw new Error("Clear-sessions manifest identity is invalid.");
|
|
816
|
+
const record = value;
|
|
817
|
+
if (Object.keys(record).sort().join(",") !== "dev,ino"
|
|
818
|
+
|| typeof record.dev !== "string" || !/^\d+$/u.test(record.dev)
|
|
819
|
+
|| typeof record.ino !== "string" || !/^\d+$/u.test(record.ino)) {
|
|
820
|
+
throw new Error("Clear-sessions manifest identity is invalid.");
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
function sameWireIdentity(value, expected) {
|
|
824
|
+
return value.dev.toString() === expected.dev && value.ino.toString() === expected.ino;
|
|
825
|
+
}
|
|
826
|
+
function isPurgeKind(value) {
|
|
827
|
+
return value === "Pi provider sessions"
|
|
828
|
+
|| value === "durable session/tool history"
|
|
829
|
+
|| value === "ACP sessions";
|
|
830
|
+
}
|
|
831
|
+
async function assertMissing(path, message) {
|
|
42
832
|
try {
|
|
43
|
-
|
|
833
|
+
await lstat(path);
|
|
44
834
|
}
|
|
45
835
|
catch (error) {
|
|
46
836
|
if (isErrno(error, "ENOENT"))
|
|
47
|
-
return
|
|
837
|
+
return;
|
|
48
838
|
throw error;
|
|
49
839
|
}
|
|
50
|
-
|
|
51
|
-
return { root, removed: true, files };
|
|
840
|
+
throw new Error(message);
|
|
52
841
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
842
|
+
function assertRealDirectory(details, path) {
|
|
843
|
+
if (!details.isDirectory() || details.isSymbolicLink()) {
|
|
844
|
+
throw new Error(`restart --clear-sessions refuses a non-directory or symbolic-link purge target: ${path}`);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
function pathsContainEachOther(first, second) {
|
|
848
|
+
return pathContains(first, second) || pathContains(second, first);
|
|
849
|
+
}
|
|
850
|
+
function pathContains(parent, child) {
|
|
851
|
+
const candidate = relative(parent, child);
|
|
852
|
+
return candidate.length === 0
|
|
853
|
+
|| (candidate !== ".." && !candidate.startsWith(`..${sep}`) && !isAbsolute(candidate));
|
|
58
854
|
}
|
|
59
855
|
/** Recursively count `*.jsonl` session files under a sessions root. */
|
|
60
856
|
async function countSessionFiles(dir) {
|
|
@@ -74,6 +870,29 @@ async function countFilesWithSuffix(dir, suffix) {
|
|
|
74
870
|
}
|
|
75
871
|
return total;
|
|
76
872
|
}
|
|
873
|
+
async function countTopLevelFilesWithSuffix(dir, suffix) {
|
|
874
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
875
|
+
let files = 0;
|
|
876
|
+
let bytes = 0;
|
|
877
|
+
for (const entry of entries) {
|
|
878
|
+
if (!entry.isFile() || entry.isSymbolicLink() || !entry.name.endsWith(suffix))
|
|
879
|
+
continue;
|
|
880
|
+
files += 1;
|
|
881
|
+
bytes += (await stat(join(dir, entry.name))).size;
|
|
882
|
+
}
|
|
883
|
+
return { files, bytes };
|
|
884
|
+
}
|
|
885
|
+
async function optionalFileBytes(path) {
|
|
886
|
+
try {
|
|
887
|
+
const info = await lstat(path);
|
|
888
|
+
return info.isFile() && !info.isSymbolicLink() ? { files: 1, bytes: info.size } : { files: 0, bytes: 0 };
|
|
889
|
+
}
|
|
890
|
+
catch (error) {
|
|
891
|
+
if (isErrno(error, "ENOENT"))
|
|
892
|
+
return { files: 0, bytes: 0 };
|
|
893
|
+
throw error;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
77
896
|
function isErrno(error, code) {
|
|
78
897
|
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
79
898
|
}
|