@sellable/install 0.1.792 → 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.
@@ -3,12 +3,12 @@ FROM ${HERMES_IMAGE}
3
3
 
4
4
  USER root
5
5
 
6
- ARG INSTALLER_PACKAGE=@sellable/install@0.1.792
6
+ ARG INSTALLER_PACKAGE=@sellable/install@0.1.793
7
7
  ARG MCP_PACKAGE=@sellable/mcp@0.1.957
8
8
  ARG INSTALLER_INTEGRITY
9
9
  ARG MCP_INTEGRITY=sha512-tIZuO5IEanT/afamDIWdorUNMdqZA6xnjL9jGAmGqYIlIqkXdHB9slWiwfLfmM44iEp7OT98IBHJlvbpMThk6A==
10
10
 
11
- RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.792" \
11
+ RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.793" \
12
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}" \
@@ -23,9 +23,9 @@ RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.792" \
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.792' || mcp.name !== '@sellable/mcp' || mcp.version !== '0.1.957') throw new Error('package identity rejected');" \
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.792', 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}" \
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
@@ -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.792` is
6
+ `@sellable/install@0.1.793` is
7
7
  published:
8
8
 
9
9
  ```sh
@@ -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;
@@ -8,7 +8,7 @@ ENV PLAYWRIGHT_BROWSERS_PATH=/opt/hermes/.playwright
8
8
 
9
9
  ARG MCP_PACKAGE=@sellable/mcp@0.1.957
10
10
  ARG MCP_INTEGRITY=sha512-tIZuO5IEanT/afamDIWdorUNMdqZA6xnjL9jGAmGqYIlIqkXdHB9slWiwfLfmM44iEp7OT98IBHJlvbpMThk6A==
11
- ARG INSTALLER_PACKAGE=@sellable/install@0.1.792
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,7 +126,7 @@ 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.792";
129
+ process.env.SELLABLE_AGENT_INSTALLER_PACKAGE ?? "@sellable/install@0.1.793";
128
130
  const MCP_PACKAGE =
129
131
  process.env.SELLABLE_AGENT_MCP_PACKAGE ?? "@sellable/mcp@0.1.957";
130
132
  export const ENDPOINT_MAX_CONCURRENT_RUNS = 10;
@@ -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 { lstatSync, readFileSync } from "node:fs";
3
- import { isAbsolute } from "node:path";
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.backup(destination)
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
- for table in ("messages_fts", "messages_fts_trigram", "messages_fts_cjk"):
1081
- if table in table_names:
1082
- destination.execute(
1083
- 'INSERT INTO "' + table + '" ("' + table + '") VALUES (\'optimize\')'
1084
- )
1085
- destination.commit()
1086
- destination.execute("VACUUM")
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 new Error("hermes_snapshot_state_db_compaction_failed");
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 new Error("hermes_snapshot_state_db_compaction_failed");
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,7 +38,7 @@ 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.792";
41
+ const INSTALLER_PACKAGE = "@sellable/install@0.1.793";
42
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}$/;
@@ -1546,7 +1546,7 @@ 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.792" ||
1549
+ pinned.installerPackage !== "@sellable/install@0.1.793" ||
1550
1550
  pinned.mcpPackage !== "@sellable/mcp@0.1.957" ||
1551
1551
  !Array.isArray(policy.toolInclude) ||
1552
1552
  policy.toolInclude.length === 0 ||
@@ -312,7 +312,7 @@ 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.792" ||
315
+ desired.installerPackage !== "@sellable/install@0.1.793" ||
316
316
  desired.mcpPackage !== "@sellable/mcp@0.1.957" ||
317
317
  !Array.isArray(desired.toolInclude) ||
318
318
  desired.toolInclude.length === 0 ||
@@ -25,7 +25,7 @@ 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.792";
28
+ export const PINNED_INSTALL_PACKAGE = "@sellable/install@0.1.793";
29
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/install",
3
- "version": "0.1.792",
3
+ "version": "0.1.793",
4
4
  "type": "module",
5
5
  "description": "One-command installer for Sellable MCP in Claude Code, Codex, and Hermes",
6
6
  "bin": {