@sellable/install 0.1.791 → 0.1.793
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/container/Dockerfile +7 -7
- package/container/README.md +1 -1
- package/lib/sellable-agent/external-runtime-builder.mjs +1 -1
- package/lib/sellable-agent/fly-admin-image/admin-runtime.mjs +12 -2
- package/lib/sellable-agent/fly-customer-image/Dockerfile +3 -3
- package/lib/sellable-agent/fly-customer-image/customer-runtime.mjs +16 -2
- package/lib/sellable-agent/fly-customer-image/hermes-runs-client.mjs +169 -2
- package/lib/sellable-agent/hermes-memory-snapshot.mjs +64 -13
- package/lib/sellable-agent/host-bootstrap.mjs +2 -2
- package/lib/sellable-agent/host-worker.mjs +2 -2
- package/lib/sellable-agent/profile-materializer.mjs +2 -2
- package/lib/sellable-agent/provisioning-adapter.mjs +2 -2
- package/package.json +1 -1
package/container/Dockerfile
CHANGED
|
@@ -3,13 +3,13 @@ FROM ${HERMES_IMAGE}
|
|
|
3
3
|
|
|
4
4
|
USER root
|
|
5
5
|
|
|
6
|
-
ARG INSTALLER_PACKAGE=@sellable/install@0.1.
|
|
7
|
-
ARG MCP_PACKAGE=@sellable/mcp@0.1.
|
|
6
|
+
ARG INSTALLER_PACKAGE=@sellable/install@0.1.793
|
|
7
|
+
ARG MCP_PACKAGE=@sellable/mcp@0.1.957
|
|
8
8
|
ARG INSTALLER_INTEGRITY
|
|
9
|
-
ARG MCP_INTEGRITY=sha512-
|
|
9
|
+
ARG MCP_INTEGRITY=sha512-tIZuO5IEanT/afamDIWdorUNMdqZA6xnjL9jGAmGqYIlIqkXdHB9slWiwfLfmM44iEp7OT98IBHJlvbpMThk6A==
|
|
10
10
|
|
|
11
|
-
RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.
|
|
12
|
-
&& test "${MCP_PACKAGE}" = "@sellable/mcp@0.1.
|
|
11
|
+
RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.793" \
|
|
12
|
+
&& test "${MCP_PACKAGE}" = "@sellable/mcp@0.1.957" \
|
|
13
13
|
&& test -n "${INSTALLER_INTEGRITY}" \
|
|
14
14
|
&& test "$(npm view "${INSTALLER_PACKAGE}" dist.integrity)" = "${INSTALLER_INTEGRITY}" \
|
|
15
15
|
&& test "$(npm view "${MCP_PACKAGE}" dist.integrity)" = "${MCP_INTEGRITY}" \
|
|
@@ -23,9 +23,9 @@ RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.791" \
|
|
|
23
23
|
&& mkdir -p /opt/sellable-agent/install-root /opt/sellable-agent/mcp-root \
|
|
24
24
|
&& npm install --prefix /opt/sellable-agent/install-root --omit=dev --ignore-scripts --no-audit --no-fund "${INSTALLER_PACKAGE}" \
|
|
25
25
|
&& npm install --prefix /opt/sellable-agent/mcp-root --omit=dev --ignore-scripts --no-audit --no-fund "${MCP_PACKAGE}" \
|
|
26
|
-
&& node --input-type=module -e "import { readFileSync } from 'node:fs'; const install = JSON.parse(readFileSync('/opt/sellable-agent/install-root/node_modules/@sellable/install/package.json')); const mcp = JSON.parse(readFileSync('/opt/sellable-agent/mcp-root/node_modules/@sellable/mcp/package.json')); if (install.name !== '@sellable/install' || install.version !== '0.1.
|
|
26
|
+
&& node --input-type=module -e "import { readFileSync } from 'node:fs'; const install = JSON.parse(readFileSync('/opt/sellable-agent/install-root/node_modules/@sellable/install/package.json')); const mcp = JSON.parse(readFileSync('/opt/sellable-agent/mcp-root/node_modules/@sellable/mcp/package.json')); if (install.name !== '@sellable/install' || install.version !== '0.1.793' || mcp.name !== '@sellable/mcp' || mcp.version !== '0.1.957') throw new Error('package identity rejected');" \
|
|
27
27
|
&& node --input-type=module -e "import { buildExternalRuntimeClosure } from '/opt/sellable-agent/install-root/node_modules/@sellable/install/lib/sellable-agent/external-runtime-builder.mjs'; const built = buildExternalRuntimeClosure({ mcpPackageRoot: '/opt/sellable-agent/mcp-root/node_modules/@sellable/mcp', mcpNodeModulesRoot: '/opt/sellable-agent/mcp-root/node_modules', hermesSourceRoot: '/opt/hermes', ownerUid: 0, ownerGid: 0 }); if (!built.closureDigest) throw new Error('runtime closure rejected');" \
|
|
28
|
-
&& node --input-type=module -e "import { chmodSync, writeFileSync } from 'node:fs'; const release = { installerPackage: '@sellable/install@0.1.
|
|
28
|
+
&& node --input-type=module -e "import { chmodSync, writeFileSync } from 'node:fs'; const release = { installerPackage: '@sellable/install@0.1.793', installerIntegrity: process.argv[1], mcpPackage: '@sellable/mcp@0.1.957', mcpIntegrity: process.argv[2] }; writeFileSync('/opt/sellable-agent/release.json', JSON.stringify(release) + '\\n', { mode: 0o444 }); chmodSync('/opt/sellable-agent/release.json', 0o444);" "${INSTALLER_INTEGRITY}" "${MCP_INTEGRITY}" \
|
|
29
29
|
&& npm cache clean --force
|
|
30
30
|
|
|
31
31
|
COPY entrypoint.sh /usr/local/bin/sellable-agent-container-entrypoint
|
package/container/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
This image is a dedicated Agent worker/runtime boundary beside an existing Hermes dashboard. It never replaces or reconfigures the dashboard container.
|
|
4
4
|
|
|
5
5
|
Build from this directory after
|
|
6
|
-
`@sellable/install@0.1.
|
|
6
|
+
`@sellable/install@0.1.793` is
|
|
7
7
|
published:
|
|
8
8
|
|
|
9
9
|
```sh
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
|
|
29
29
|
const MANIFEST_VERSION = "sellable-agent-external-runtime-closure/v1";
|
|
30
30
|
const MCP_PACKAGE = "@sellable/mcp";
|
|
31
|
-
const MCP_VERSION = "0.1.
|
|
31
|
+
const MCP_VERSION = "0.1.957";
|
|
32
32
|
const SAFE_COMPONENT = /^[A-Za-z0-9@._+-]+$/;
|
|
33
33
|
const HERMES_EXCLUDES = new Set([".git", ".playwright", "node_modules"]);
|
|
34
34
|
|
|
@@ -62,6 +62,7 @@ import {
|
|
|
62
62
|
sellableSlackToolsetsForMaterialization,
|
|
63
63
|
} from "../fly-capabilities-bridge.mjs";
|
|
64
64
|
import { createCustomerEndpointRelay } from "../fly-customer-image/customer-runtime.mjs";
|
|
65
|
+
import { createHermesSessionRetention } from "../fly-customer-image/hermes-runs-client.mjs";
|
|
65
66
|
import {
|
|
66
67
|
claimWithSoulObservation,
|
|
67
68
|
createFlyCustomerControlClient,
|
|
@@ -88,12 +89,12 @@ import {
|
|
|
88
89
|
HERMES_SNAPSHOT_HOOK_ALLOWLIST_BYTES,
|
|
89
90
|
applyHermesSellableHostedPolicy,
|
|
90
91
|
ensureHermesClientRelationshipsCrons,
|
|
91
|
-
ensureHermesRefillSendsCron,
|
|
92
92
|
ensureHermesCustomerProgramCrons,
|
|
93
93
|
ensureHermesOpportunitySequencesCron,
|
|
94
|
+
ensureHermesRefillSendsCron,
|
|
94
95
|
ensureHermesSnapshotCron,
|
|
95
|
-
markHermesSnapshotDirty,
|
|
96
96
|
linkHermesState,
|
|
97
|
+
markHermesSnapshotDirty,
|
|
97
98
|
migrateLegacyHermesUserState,
|
|
98
99
|
quarantineLegacyLightfieldVenv,
|
|
99
100
|
reconcileHermesSnapshot,
|
|
@@ -142,6 +143,7 @@ const PROFILES_ROOT = join(DATA_ROOT, "profiles");
|
|
|
142
143
|
const RUNTIME_ROOT = join(DATA_ROOT, ".sellable-agent", "admin-runtime");
|
|
143
144
|
const DELIVERY_ROOT = join(RUNTIME_ROOT, "delivery");
|
|
144
145
|
const SECRET_ROOT = "/run/sellable-agent/admin-runtime";
|
|
146
|
+
const sessionRetention = createHermesSessionRetention();
|
|
145
147
|
// Turn descriptor + consumed roots for the Product MCP context proxy. They are
|
|
146
148
|
// normally empty and receive one signed descriptor only while the fenced
|
|
147
149
|
// endpoint relay owns a turn; the proxy fails closed if either root is absent.
|
|
@@ -3972,6 +3974,14 @@ async function runAdminProduction() {
|
|
|
3972
3974
|
bytes
|
|
3973
3975
|
),
|
|
3974
3976
|
});
|
|
3977
|
+
void sessionRetention.tick({
|
|
3978
|
+
apiKeyFile: join(SECRET_ROOT, "api-server-key"),
|
|
3979
|
+
statePath: join(
|
|
3980
|
+
activeLaunch.profile.profileHome,
|
|
3981
|
+
".sellable-agent",
|
|
3982
|
+
"session-retention.json"
|
|
3983
|
+
),
|
|
3984
|
+
});
|
|
3975
3985
|
}
|
|
3976
3986
|
await sleep(1_000);
|
|
3977
3987
|
continue;
|
|
@@ -6,9 +6,9 @@ USER root
|
|
|
6
6
|
|
|
7
7
|
ENV PLAYWRIGHT_BROWSERS_PATH=/opt/hermes/.playwright
|
|
8
8
|
|
|
9
|
-
ARG MCP_PACKAGE=@sellable/mcp@0.1.
|
|
10
|
-
ARG MCP_INTEGRITY=sha512-
|
|
11
|
-
ARG INSTALLER_PACKAGE=@sellable/install@0.1.
|
|
9
|
+
ARG MCP_PACKAGE=@sellable/mcp@0.1.957
|
|
10
|
+
ARG MCP_INTEGRITY=sha512-tIZuO5IEanT/afamDIWdorUNMdqZA6xnjL9jGAmGqYIlIqkXdHB9slWiwfLfmM44iEp7OT98IBHJlvbpMThk6A==
|
|
11
|
+
ARG INSTALLER_PACKAGE=@sellable/install@0.1.793
|
|
12
12
|
ARG ADMIN_MCP_PACKAGE=@sellable/admin-mcp@0.1.202
|
|
13
13
|
ARG ADMIN_MCP_INTEGRITY=sha512-ym3aye/0vQ8tfULJlKz9JFOxvIQsl1G+n3np+TTgK/tecwNblBo/j3AkTnRqH9iomKzPbZzP+n4tEQsvsU8ByA==
|
|
14
14
|
ARG TUS_JS_CLIENT_VERSION=4.3.1
|
|
@@ -94,6 +94,7 @@ import {
|
|
|
94
94
|
} from "./endpoint-uat-completion-gate.mjs";
|
|
95
95
|
import {
|
|
96
96
|
createHermesRunsClient,
|
|
97
|
+
createHermesSessionRetention,
|
|
97
98
|
HERMES_RUNS_FIXTURE_IDENTITY,
|
|
98
99
|
validateEndpointModelSelection,
|
|
99
100
|
validateOpenerOutputContract,
|
|
@@ -106,6 +107,7 @@ import {
|
|
|
106
107
|
const DATA_ROOT = "/opt/data";
|
|
107
108
|
const PROFILES_ROOT = join(DATA_ROOT, "profiles");
|
|
108
109
|
const SECRET_ROOT = "/run/sellable-agent";
|
|
110
|
+
const sessionRetention = createHermesSessionRetention();
|
|
109
111
|
const TRUST_ROOT = "/usr/local/share/sellable-agent/mcp-turn-trust-root.pem";
|
|
110
112
|
const HERMES = "/opt/hermes/.venv/bin/hermes";
|
|
111
113
|
const HERMES_PYTHON = "/opt/hermes/.venv/bin/python3";
|
|
@@ -124,9 +126,9 @@ const RUNTIME_GENERATION = Number(
|
|
|
124
126
|
const BOOT_SESSION_ID = randomUUID();
|
|
125
127
|
const HERMES_VERSION = process.env.SELLABLE_AGENT_HERMES_VERSION ?? "0.21.5";
|
|
126
128
|
const INSTALLER_PACKAGE =
|
|
127
|
-
process.env.SELLABLE_AGENT_INSTALLER_PACKAGE ?? "@sellable/install@0.1.
|
|
129
|
+
process.env.SELLABLE_AGENT_INSTALLER_PACKAGE ?? "@sellable/install@0.1.793";
|
|
128
130
|
const MCP_PACKAGE =
|
|
129
|
-
process.env.SELLABLE_AGENT_MCP_PACKAGE ?? "@sellable/mcp@0.1.
|
|
131
|
+
process.env.SELLABLE_AGENT_MCP_PACKAGE ?? "@sellable/mcp@0.1.957";
|
|
130
132
|
export const ENDPOINT_MAX_CONCURRENT_RUNS = 10;
|
|
131
133
|
const DEFAULT_PROFILE_BUNDLE_ROOT =
|
|
132
134
|
"/usr/local/share/sellable-agent/default-profile-bundle";
|
|
@@ -4236,6 +4238,18 @@ async function main() {
|
|
|
4236
4238
|
bytes
|
|
4237
4239
|
),
|
|
4238
4240
|
});
|
|
4241
|
+
void sessionRetention.tick({
|
|
4242
|
+
apiKeyFile: join(
|
|
4243
|
+
SECRET_ROOT,
|
|
4244
|
+
activeRuntime.desired.profileId,
|
|
4245
|
+
"api-server-key"
|
|
4246
|
+
),
|
|
4247
|
+
statePath: join(
|
|
4248
|
+
activeRuntime.profileRoot,
|
|
4249
|
+
".sellable-agent",
|
|
4250
|
+
"session-retention.json"
|
|
4251
|
+
),
|
|
4252
|
+
});
|
|
4239
4253
|
}
|
|
4240
4254
|
if (
|
|
4241
4255
|
activeRuntime?.action !== "INSTANTIATE" &&
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import {
|
|
3
|
+
lstatSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
renameSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
} from "node:fs";
|
|
9
|
+
import { dirname, isAbsolute } from "node:path";
|
|
4
10
|
|
|
5
11
|
const LOOPBACK_ORIGIN = "http://127.0.0.1:8642";
|
|
6
12
|
const RUN_ID = /^run_[a-f0-9]{32}$/;
|
|
@@ -887,3 +893,164 @@ export function createHermesRunsClient(options = {}) {
|
|
|
887
893
|
},
|
|
888
894
|
});
|
|
889
895
|
}
|
|
896
|
+
|
|
897
|
+
// Endpoint runs (`api_server`) and cron jobs each leave a Hermes session that
|
|
898
|
+
// nothing reads again: an endpoint attempt gets a fresh session and its result
|
|
899
|
+
// lives in Sellable. Hermes only closes these after 90 idle days and deletes
|
|
900
|
+
// them after another 90, so state.db grows with every run, and so do the
|
|
901
|
+
// backup and the volume. Retention deletes them after a week. It goes through
|
|
902
|
+
// the running gateway's session API because Hermes refuses bulk deletes from a
|
|
903
|
+
// second writer, and it never touches conversations (Slack, Desktop, TUI).
|
|
904
|
+
export const HERMES_AUTOMATION_SESSION_SOURCES = Object.freeze([
|
|
905
|
+
"api_server",
|
|
906
|
+
"cron",
|
|
907
|
+
]);
|
|
908
|
+
export const HERMES_AUTOMATION_SESSION_RETENTION_DAYS = 7;
|
|
909
|
+
const SESSION_PAGE_SIZE = 200;
|
|
910
|
+
const SESSION_RETENTION_MAX_DELETES = 1_000;
|
|
911
|
+
const SESSION_RETENTION_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
912
|
+
const SESSION_RETENTION_RETRY_MS = 60 * 60 * 1000;
|
|
913
|
+
|
|
914
|
+
function sessionLastActiveSeconds(session) {
|
|
915
|
+
return Math.max(
|
|
916
|
+
...[session.last_active, session.ended_at, session.started_at].filter(
|
|
917
|
+
(value) => typeof value === "number" && Number.isFinite(value)
|
|
918
|
+
),
|
|
919
|
+
0
|
|
920
|
+
);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/** @param {any} options */
|
|
924
|
+
export async function pruneHermesAutomationSessions({
|
|
925
|
+
apiKeyFile,
|
|
926
|
+
fetchImpl = fetch,
|
|
927
|
+
now = Date.now(),
|
|
928
|
+
retentionDays = HERMES_AUTOMATION_SESSION_RETENTION_DAYS,
|
|
929
|
+
maxDeletes = SESSION_RETENTION_MAX_DELETES,
|
|
930
|
+
}) {
|
|
931
|
+
const apiKey = readApiKey(apiKeyFile);
|
|
932
|
+
const call = (method, path) =>
|
|
933
|
+
fetchImpl(`${LOOPBACK_ORIGIN}${path}`, {
|
|
934
|
+
method,
|
|
935
|
+
headers: { authorization: `Bearer ${apiKey}` },
|
|
936
|
+
signal: AbortSignal.timeout(20_000),
|
|
937
|
+
});
|
|
938
|
+
const cutoff = now / 1000 - retentionDays * 86_400;
|
|
939
|
+
const stale = new Set();
|
|
940
|
+
for (const source of HERMES_AUTOMATION_SESSION_SOURCES) {
|
|
941
|
+
// Newest activity first, so every page is read; listing stops once one
|
|
942
|
+
// pass worth of stale sessions is known.
|
|
943
|
+
for (
|
|
944
|
+
let offset = 0;
|
|
945
|
+
stale.size <= maxDeletes;
|
|
946
|
+
offset += SESSION_PAGE_SIZE
|
|
947
|
+
) {
|
|
948
|
+
const response = await call(
|
|
949
|
+
"GET",
|
|
950
|
+
`/api/sessions?source=${source}&limit=${SESSION_PAGE_SIZE}&offset=${offset}`
|
|
951
|
+
);
|
|
952
|
+
if (!response.ok) throw new Error("hermes_session_retention_list_failed");
|
|
953
|
+
const page = await response.json();
|
|
954
|
+
const rows = Array.isArray(page?.data) ? page.data : [];
|
|
955
|
+
for (const session of rows) {
|
|
956
|
+
if (
|
|
957
|
+
session?.source === source &&
|
|
958
|
+
typeof session.id === "string" &&
|
|
959
|
+
session.pinned !== true &&
|
|
960
|
+
sessionLastActiveSeconds(session) < cutoff
|
|
961
|
+
) {
|
|
962
|
+
stale.add(session.id);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
if (page?.has_more !== true || rows.length === 0) break;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
let deleted = 0;
|
|
969
|
+
for (const id of [...stale].slice(0, maxDeletes)) {
|
|
970
|
+
const response = await call(
|
|
971
|
+
"DELETE",
|
|
972
|
+
`/api/sessions/${encodeURIComponent(id)}`
|
|
973
|
+
);
|
|
974
|
+
if (response.ok) deleted += 1;
|
|
975
|
+
else if (response.status !== 404) {
|
|
976
|
+
throw new Error("hermes_session_retention_delete_failed");
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
return {
|
|
980
|
+
ok: true,
|
|
981
|
+
deleted,
|
|
982
|
+
remaining: Math.max(0, stale.size - maxDeletes),
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
function readRetentionNextRunAt(statePath) {
|
|
987
|
+
try {
|
|
988
|
+
const value = JSON.parse(readFileSync(statePath, "utf8"))?.nextRunAt;
|
|
989
|
+
return Number.isFinite(value) ? value : 0;
|
|
990
|
+
} catch {
|
|
991
|
+
return 0;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
function writeRetentionNextRunAt(statePath, nextRunAt) {
|
|
996
|
+
mkdirSync(dirname(statePath), { recursive: true, mode: 0o700 });
|
|
997
|
+
const temporary = `${statePath}.${process.pid}.tmp`;
|
|
998
|
+
writeFileSync(temporary, `${JSON.stringify({ nextRunAt })}\n`, {
|
|
999
|
+
mode: 0o600,
|
|
1000
|
+
});
|
|
1001
|
+
renameSync(temporary, statePath);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
/**
|
|
1005
|
+
* Daily, single-flight session retention for a runtime's idle loop. `tick`
|
|
1006
|
+
* returns the running pass (or null when not due) and never rejects, so the
|
|
1007
|
+
* loop can fire it without waiting.
|
|
1008
|
+
* @param {any} options
|
|
1009
|
+
*/
|
|
1010
|
+
export function createHermesSessionRetention({
|
|
1011
|
+
fetchImpl = fetch,
|
|
1012
|
+
now = () => Date.now(),
|
|
1013
|
+
maxDeletes = SESSION_RETENTION_MAX_DELETES,
|
|
1014
|
+
log = (entry) => process.stderr.write(`${JSON.stringify(entry)}\n`),
|
|
1015
|
+
} = {}) {
|
|
1016
|
+
let running = null;
|
|
1017
|
+
return {
|
|
1018
|
+
tick({ apiKeyFile, statePath }) {
|
|
1019
|
+
if (running) return running;
|
|
1020
|
+
if (now() < readRetentionNextRunAt(statePath)) return null;
|
|
1021
|
+
running = (async () => {
|
|
1022
|
+
let nextRunAt;
|
|
1023
|
+
try {
|
|
1024
|
+
const result = await pruneHermesAutomationSessions({
|
|
1025
|
+
apiKeyFile,
|
|
1026
|
+
fetchImpl,
|
|
1027
|
+
now: now(),
|
|
1028
|
+
maxDeletes,
|
|
1029
|
+
});
|
|
1030
|
+
log({
|
|
1031
|
+
ok: true,
|
|
1032
|
+
code: "hermes_session_retention_pruned",
|
|
1033
|
+
deleted: result.deleted,
|
|
1034
|
+
remaining: result.remaining,
|
|
1035
|
+
});
|
|
1036
|
+
nextRunAt =
|
|
1037
|
+
now() +
|
|
1038
|
+
(result.remaining > 0
|
|
1039
|
+
? SESSION_RETENTION_RETRY_MS
|
|
1040
|
+
: SESSION_RETENTION_INTERVAL_MS);
|
|
1041
|
+
} catch {
|
|
1042
|
+
log({ ok: false, code: "hermes_session_retention_failed" });
|
|
1043
|
+
nextRunAt = now() + SESSION_RETENTION_RETRY_MS;
|
|
1044
|
+
}
|
|
1045
|
+
try {
|
|
1046
|
+
writeRetentionNextRunAt(statePath, nextRunAt);
|
|
1047
|
+
} catch {
|
|
1048
|
+
// The next tick simply runs again.
|
|
1049
|
+
}
|
|
1050
|
+
})().finally(() => {
|
|
1051
|
+
running = null;
|
|
1052
|
+
});
|
|
1053
|
+
return running;
|
|
1054
|
+
},
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
readdirSync,
|
|
17
17
|
renameSync,
|
|
18
18
|
rmSync,
|
|
19
|
+
statfsSync,
|
|
19
20
|
writeFileSync,
|
|
20
21
|
} from "node:fs";
|
|
21
22
|
import { tmpdir } from "node:os";
|
|
@@ -1006,12 +1007,14 @@ import time
|
|
|
1006
1007
|
|
|
1007
1008
|
source_path, target_path = sys.argv[1], sys.argv[2]
|
|
1008
1009
|
retention_hours = int(sys.argv[3])
|
|
1010
|
+
# VACUUM INTO writes one consistent, compact copy of the live pages only, so
|
|
1011
|
+
# space the live database has freed never travels with the backup.
|
|
1009
1012
|
source = sqlite3.connect("file:" + source_path + "?mode=ro", uri=True)
|
|
1010
|
-
destination = sqlite3.connect(target_path)
|
|
1011
1013
|
try:
|
|
1012
|
-
source.
|
|
1014
|
+
source.execute("VACUUM INTO ?", (target_path,))
|
|
1013
1015
|
finally:
|
|
1014
1016
|
source.close()
|
|
1017
|
+
destination = sqlite3.connect(target_path)
|
|
1015
1018
|
|
|
1016
1019
|
try:
|
|
1017
1020
|
if destination.execute("PRAGMA quick_check").fetchone()[0] != "ok":
|
|
@@ -1038,8 +1041,11 @@ try:
|
|
|
1038
1041
|
)
|
|
1039
1042
|
destination.execute(
|
|
1040
1043
|
"WITH RECURSIVE doomed(id) AS ("
|
|
1044
|
+
# Hermes never ends an endpoint run's (api_server) session; past
|
|
1045
|
+
# the retention it is as finished as an ended cron session.
|
|
1041
1046
|
" SELECT id FROM sessions"
|
|
1042
|
-
" WHERE source = 'cron' AND ended_at IS NOT NULL"
|
|
1047
|
+
" WHERE ((source = 'cron' AND ended_at IS NOT NULL)"
|
|
1048
|
+
" OR source = 'api_server')"
|
|
1043
1049
|
" AND COALESCE(last_activity_at, ended_at, started_at) < ? "
|
|
1044
1050
|
+ pinned_clause
|
|
1045
1051
|
+ " UNION"
|
|
@@ -1077,13 +1083,14 @@ try:
|
|
|
1077
1083
|
")"
|
|
1078
1084
|
)
|
|
1079
1085
|
destination.commit()
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1086
|
+
if pruned_sessions:
|
|
1087
|
+
for table in ("messages_fts", "messages_fts_trigram", "messages_fts_cjk"):
|
|
1088
|
+
if table in table_names:
|
|
1089
|
+
destination.execute(
|
|
1090
|
+
'INSERT INTO "' + table + '" ("' + table + '") VALUES (\'optimize\')'
|
|
1091
|
+
)
|
|
1092
|
+
destination.commit()
|
|
1093
|
+
destination.execute("VACUUM")
|
|
1087
1094
|
if destination.execute("PRAGMA quick_check").fetchone()[0] != "ok":
|
|
1088
1095
|
raise RuntimeError("compacted backup failed quick_check")
|
|
1089
1096
|
if destination.execute("PRAGMA foreign_key_check").fetchone() is not None:
|
|
@@ -1101,12 +1108,38 @@ print(json.dumps({
|
|
|
1101
1108
|
}, separators=(",", ":")))
|
|
1102
1109
|
`;
|
|
1103
1110
|
|
|
1111
|
+
// Room the volume keeps after the compact copy lands, so a backup never
|
|
1112
|
+
// leaves the live gateway without space to write.
|
|
1113
|
+
const STATE_DB_VOLUME_HEADROOM_BYTES = 64 * 1024 * 1024;
|
|
1114
|
+
|
|
1115
|
+
function freeBytesAt(directory) {
|
|
1116
|
+
const stats = statfsSync(directory);
|
|
1117
|
+
return stats.bavail * stats.bsize;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
const compactionFailure = (reason) =>
|
|
1121
|
+
new Error("hermes_snapshot_state_db_compaction_failed", { cause: reason });
|
|
1122
|
+
|
|
1123
|
+
// A bounded reason for the operator log; never the child's raw output.
|
|
1124
|
+
function compactionChildFailureReason(result) {
|
|
1125
|
+
if (result?.timedOut) return "timeout";
|
|
1126
|
+
if (result?.outputLimitExceeded) return "output_limit";
|
|
1127
|
+
const stderr = String(result?.stderr ?? "");
|
|
1128
|
+
if (/database or disk is full|No space left on device/i.test(stderr)) {
|
|
1129
|
+
return "disk_full";
|
|
1130
|
+
}
|
|
1131
|
+
if (/quick_check|foreign_key_check/.test(stderr)) return "integrity";
|
|
1132
|
+
if (/sqlite3\.\w*Error/.test(stderr)) return "sqlite_error";
|
|
1133
|
+
return "child_failed";
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1104
1136
|
export async function compactHermesSnapshotStateDb({
|
|
1105
1137
|
sourcePath,
|
|
1106
1138
|
targetPath,
|
|
1107
1139
|
retentionHours = HERMES_SNAPSHOT_CRON_SESSION_RETENTION_HOURS,
|
|
1108
1140
|
hermesPython = HERMES_PYTHON,
|
|
1109
1141
|
scratchDirectory = tmpdir(),
|
|
1142
|
+
volumeFreeBytes = freeBytesAt,
|
|
1110
1143
|
runChild = /** @type {typeof runBoundedHermesChild | undefined} */ (
|
|
1111
1144
|
undefined
|
|
1112
1145
|
),
|
|
@@ -1161,7 +1194,7 @@ export async function compactHermesSnapshotStateDb({
|
|
|
1161
1194
|
maxOutputBytes: 64 * 1024,
|
|
1162
1195
|
});
|
|
1163
1196
|
if (childFailed(result) || !existsSync(scratchTargetPath)) {
|
|
1164
|
-
throw
|
|
1197
|
+
throw compactionFailure(compactionChildFailureReason(result));
|
|
1165
1198
|
}
|
|
1166
1199
|
const scratchTarget = regularFile(scratchTargetPath);
|
|
1167
1200
|
const summary = JSON.parse(String(result.stdout ?? ""));
|
|
@@ -1174,7 +1207,16 @@ export async function compactHermesSnapshotStateDb({
|
|
|
1174
1207
|
summary.prunedMessages < 0 ||
|
|
1175
1208
|
summary.archiveStateBytes !== scratchTarget.size
|
|
1176
1209
|
) {
|
|
1177
|
-
throw
|
|
1210
|
+
throw compactionFailure("invalid_result");
|
|
1211
|
+
}
|
|
1212
|
+
// Only this compact copy lands on the persistent volume (the rest of the
|
|
1213
|
+
// staged profile is hard links). Refuse up front when it cannot fit,
|
|
1214
|
+
// rather than half-writing it and failing as a generic error.
|
|
1215
|
+
if (
|
|
1216
|
+
volumeFreeBytes(dirname(targetPath)) <
|
|
1217
|
+
scratchTarget.size + STATE_DB_VOLUME_HEADROOM_BYTES
|
|
1218
|
+
) {
|
|
1219
|
+
throw compactionFailure("disk_full");
|
|
1178
1220
|
}
|
|
1179
1221
|
finalTemporaryPath = join(
|
|
1180
1222
|
dirname(targetPath),
|
|
@@ -1204,8 +1246,17 @@ export async function compactHermesSnapshotStateDb({
|
|
|
1204
1246
|
throw new Error("hermes_snapshot_state_db_compaction_failed");
|
|
1205
1247
|
}
|
|
1206
1248
|
return summary;
|
|
1207
|
-
} catch {
|
|
1249
|
+
} catch (error) {
|
|
1208
1250
|
if (targetCreated) rmSync(targetPath, { force: true });
|
|
1251
|
+
const reason =
|
|
1252
|
+
error?.cause ?? (error?.code === "ENOSPC" ? "disk_full" : "copy_failed");
|
|
1253
|
+
process.stderr.write(
|
|
1254
|
+
`${JSON.stringify({
|
|
1255
|
+
ok: false,
|
|
1256
|
+
code: "hermes_snapshot_state_db_compaction_failed",
|
|
1257
|
+
reason,
|
|
1258
|
+
})}\n`
|
|
1259
|
+
);
|
|
1209
1260
|
throw new Error("hermes_snapshot_state_db_compaction_failed");
|
|
1210
1261
|
} finally {
|
|
1211
1262
|
if (finalTemporaryPath) rmSync(finalTemporaryPath, { force: true });
|
|
@@ -38,8 +38,8 @@ import { installAgentHostServices } from "./service-installer.mjs";
|
|
|
38
38
|
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
|
|
39
39
|
const VERSION = "sellable-agent-host-bootstrap/v1";
|
|
40
40
|
const RECEIPT_VERSION = "sellable-agent-host-registration/v1";
|
|
41
|
-
const INSTALLER_PACKAGE = "@sellable/install@0.1.
|
|
42
|
-
const MCP_PACKAGE = "@sellable/mcp@0.1.
|
|
41
|
+
const INSTALLER_PACKAGE = "@sellable/install@0.1.793";
|
|
42
|
+
const MCP_PACKAGE = "@sellable/mcp@0.1.957";
|
|
43
43
|
const HERMES_VERSION = "0.18.0";
|
|
44
44
|
const SHA256 = /^[a-f0-9]{64}$/;
|
|
45
45
|
const SHA256_16 = /^[a-f0-9]{16}$/;
|
|
@@ -1546,8 +1546,8 @@ export function compileClaimToProfileDesired(activeClaim, config) {
|
|
|
1546
1546
|
]) ||
|
|
1547
1547
|
pinned.hermesCli !== "hermes" ||
|
|
1548
1548
|
pinned.hermesVersion !== "0.18.0" ||
|
|
1549
|
-
pinned.installerPackage !== "@sellable/install@0.1.
|
|
1550
|
-
pinned.mcpPackage !== "@sellable/mcp@0.1.
|
|
1549
|
+
pinned.installerPackage !== "@sellable/install@0.1.793" ||
|
|
1550
|
+
pinned.mcpPackage !== "@sellable/mcp@0.1.957" ||
|
|
1551
1551
|
!Array.isArray(policy.toolInclude) ||
|
|
1552
1552
|
policy.toolInclude.length === 0 ||
|
|
1553
1553
|
!Number.isSafeInteger(slack.generation) ||
|
|
@@ -312,8 +312,8 @@ function validateDesired(desired) {
|
|
|
312
312
|
desired.serviceCredentialGeneration < 1 ||
|
|
313
313
|
desired.hermesCli !== "hermes" ||
|
|
314
314
|
desired.hermesVersion !== "0.18.0" ||
|
|
315
|
-
desired.installerPackage !== "@sellable/install@0.1.
|
|
316
|
-
desired.mcpPackage !== "@sellable/mcp@0.1.
|
|
315
|
+
desired.installerPackage !== "@sellable/install@0.1.793" ||
|
|
316
|
+
desired.mcpPackage !== "@sellable/mcp@0.1.957" ||
|
|
317
317
|
!Array.isArray(desired.toolInclude) ||
|
|
318
318
|
desired.toolInclude.length === 0 ||
|
|
319
319
|
desired.toolInclude.some((tool) => !/^[a-z][a-z0-9_]{0,127}$/.test(tool)) ||
|
|
@@ -25,8 +25,8 @@ import { fileURLToPath } from "node:url";
|
|
|
25
25
|
import { deriveContainedProfileId } from "./profile-materializer.mjs";
|
|
26
26
|
|
|
27
27
|
export const PROVISIONING_ACTION = "PROVISION_HERMES_PROFILE";
|
|
28
|
-
export const PINNED_INSTALL_PACKAGE = "@sellable/install@0.1.
|
|
29
|
-
export const PINNED_MCP_PACKAGE = "@sellable/mcp@0.1.
|
|
28
|
+
export const PINNED_INSTALL_PACKAGE = "@sellable/install@0.1.793";
|
|
29
|
+
export const PINNED_MCP_PACKAGE = "@sellable/mcp@0.1.957";
|
|
30
30
|
const PINNED_INSTALL_VERSION = PINNED_INSTALL_PACKAGE.slice(
|
|
31
31
|
PINNED_INSTALL_PACKAGE.lastIndexOf("@") + 1
|
|
32
32
|
);
|