@sellable/install 0.1.682 → 0.1.683

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,13 +3,13 @@ FROM ${HERMES_IMAGE}
3
3
 
4
4
  USER root
5
5
 
6
- ARG INSTALLER_PACKAGE=@sellable/install@0.1.682
7
- ARG MCP_PACKAGE=@sellable/mcp@0.1.899
6
+ ARG INSTALLER_PACKAGE=@sellable/install@0.1.683
7
+ ARG MCP_PACKAGE=@sellable/mcp@0.1.900
8
8
  ARG INSTALLER_INTEGRITY
9
- ARG MCP_INTEGRITY=sha512-hnwdSFBun/DESrkNTbM+sRLMVjkRF1iMFRHw8zurlse4r5XxBu1VIo03mo9TVR5mEUVrJmpNxsU4QweODpmHMg==
9
+ ARG MCP_INTEGRITY=sha512-xLAA8pzfKujvUYT+eg8FPTGgA2CIcgsPG3bM1NjMPLOSib7l32H4ca9SkbeBV7jOPCxcc3+XZBKEIhmtJufiEg==
10
10
 
11
- RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.682" \
12
- && test "${MCP_PACKAGE}" = "@sellable/mcp@0.1.899" \
11
+ RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.683" \
12
+ && test "${MCP_PACKAGE}" = "@sellable/mcp@0.1.900" \
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.682" \
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.682' || mcp.name !== '@sellable/mcp' || mcp.version !== '0.1.899') 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.683' || mcp.name !== '@sellable/mcp' || mcp.version !== '0.1.900') 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.682', installerIntegrity: process.argv[1], mcpPackage: '@sellable/mcp@0.1.899', 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.683', installerIntegrity: process.argv[1], mcpPackage: '@sellable/mcp@0.1.900', 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.682` is
6
+ `@sellable/install@0.1.683` 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.899";
31
+ const MCP_VERSION = "0.1.900";
32
32
  const SAFE_COMPONENT = /^[A-Za-z0-9@._+-]+$/;
33
33
  const HERMES_EXCLUDES = new Set([".git", ".playwright", "node_modules"]);
34
34
 
@@ -831,8 +831,10 @@ const ADMIN_LIVE_SKILLS_CONFIG_KEYS = Object.freeze([
831
831
  "platform_disabled",
832
832
  ]);
833
833
  const ADMIN_LIVE_PLUGIN_CONFIG_KEYS = Object.freeze(["enabled", "disabled"]);
834
- const ADMIN_SEALED_SKILL_DIRECTORIES = new Set([
835
- "sellable",
834
+ const ADMIN_SEALED_SKILL_PATHS = new Set([
835
+ // The package owns only the root routing skill. Customer/operator skills
836
+ // live below this namespace and must survive profile materialization.
837
+ "sellable/SKILL.md",
836
838
  // The Admin profile factory owns this whole namespace. Preserving an old
837
839
  // package version as mutable operator state makes every package upgrade
838
840
  // restage forever when any Admin skill changes.
@@ -865,7 +867,9 @@ function adminPreservedSkillsIdentity(profileRoot) {
865
867
  const nextRelative = relativePath ? `${relativePath}/${name}` : name;
866
868
  if (
867
869
  rootName === "skills" &&
868
- ADMIN_SEALED_SKILL_DIRECTORIES.has(nextRelative)
870
+ [...ADMIN_SEALED_SKILL_PATHS].some(
871
+ (path) => nextRelative === path || nextRelative.startsWith(`${path}/`)
872
+ )
869
873
  )
870
874
  continue;
871
875
  const path = join(directory, name);
@@ -1188,9 +1192,7 @@ async function materializeAdminProfile({
1188
1192
  join(profileHome, "skills"),
1189
1193
  join(stageRoot, "skills"),
1190
1194
  {
1191
- preserveTargetTopLevelNames: [
1192
- ...ADMIN_SEALED_SKILL_DIRECTORIES,
1193
- ],
1195
+ preserveTargetPaths: [...ADMIN_SEALED_SKILL_PATHS],
1194
1196
  }
1195
1197
  );
1196
1198
  }
@@ -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.899
10
- ARG MCP_INTEGRITY=sha512-hnwdSFBun/DESrkNTbM+sRLMVjkRF1iMFRHw8zurlse4r5XxBu1VIo03mo9TVR5mEUVrJmpNxsU4QweODpmHMg==
11
- ARG INSTALLER_PACKAGE=@sellable/install@0.1.682
9
+ ARG MCP_PACKAGE=@sellable/mcp@0.1.900
10
+ ARG MCP_INTEGRITY=sha512-xLAA8pzfKujvUYT+eg8FPTGgA2CIcgsPG3bM1NjMPLOSib7l32H4ca9SkbeBV7jOPCxcc3+XZBKEIhmtJufiEg==
11
+ ARG INSTALLER_PACKAGE=@sellable/install@0.1.683
12
12
  ARG ADMIN_MCP_PACKAGE=@sellable/admin-mcp@0.1.186-wip.phase158.20260802034727
13
13
  ARG ADMIN_MCP_INTEGRITY=sha512-qYNew5wd2IfgVcp1UYOG9QvpF/8Mx19YJ5HQtMKzvSfUf3evePjtaKsoXLf2C/GUJ5QlLlHJo84VWOI3V01+9A==
14
14
  ARG TUS_JS_CLIENT_VERSION=4.3.1
@@ -106,9 +106,9 @@ const BOOT_SESSION_ID = randomUUID();
106
106
  const HERMES_VERSION = process.env.SELLABLE_AGENT_HERMES_VERSION ?? "0.20.4";
107
107
  const INSTALLER_PACKAGE =
108
108
  process.env.SELLABLE_AGENT_INSTALLER_PACKAGE ??
109
- "@sellable/install@0.1.682";
109
+ "@sellable/install@0.1.683";
110
110
  const MCP_PACKAGE =
111
- process.env.SELLABLE_AGENT_MCP_PACKAGE ?? "@sellable/mcp@0.1.899";
111
+ process.env.SELLABLE_AGENT_MCP_PACKAGE ?? "@sellable/mcp@0.1.900";
112
112
  const DEFAULT_PROFILE_BUNDLE_ROOT =
113
113
  "/usr/local/share/sellable-agent/default-profile-bundle";
114
114
  const DEFAULT_PROFILE_CRON_PYTHON = [
@@ -1026,7 +1026,7 @@ export function createFlyCustomerControlClient({
1026
1026
  /^[a-f0-9]{64}$/.test(value?.archiveDigest ?? "") &&
1027
1027
  Number.isSafeInteger(value?.archiveBytes) &&
1028
1028
  value.archiveBytes > 0 &&
1029
- value.archiveBytes <= 128 * 1024 * 1024 &&
1029
+ value.archiveBytes <= 512 * 1024 * 1024 &&
1030
1030
  value.hermesVersion === "0.20.4" &&
1031
1031
  /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(value?.sourceProfileId ?? "");
1032
1032
  const boundedSnapshotIdentity = (value) =>
@@ -1159,7 +1159,7 @@ export function createFlyCustomerControlClient({
1159
1159
  /^[a-f0-9]{64}$/.test(result.archiveDigest ?? "") &&
1160
1160
  Number.isSafeInteger(result.archiveBytes) &&
1161
1161
  result.archiveBytes > 0 &&
1162
- result.archiveBytes <= 128 * 1024 * 1024);
1162
+ result.archiveBytes <= 512 * 1024 * 1024);
1163
1163
  if (
1164
1164
  !(isV2 ? identityReadbackAccepted : metadataAccepted(result)) ||
1165
1165
  !signedUrlAccepted(result.signedUrl)
@@ -64,13 +64,7 @@ export const HERMES_OPPORTUNITY_SEQUENCES_CRON = Object.freeze({
64
64
  schedule: "every 30m",
65
65
  deliver: "slack",
66
66
  skills: Object.freeze([HERMES_OPPORTUNITY_SEQUENCES_SKILL]),
67
- enabledToolsets: Object.freeze([
68
- "file",
69
- "terminal",
70
- "safe",
71
- "sellable",
72
- "mcp",
73
- ]),
67
+ enabledToolsets: Object.freeze(["file", "terminal", "safe", "sellable", "mcp"]),
74
68
  noAgent: false,
75
69
  });
76
70
  export const HERMES_OPPORTUNITY_SEQUENCES_JOURNAL_ROOT =
@@ -88,7 +82,13 @@ export const HERMES_CLIENT_RELATIONSHIPS_CRONS = Object.freeze({
88
82
  chaseSchedule: "30 13 * * 1-5",
89
83
  deliver: "slack",
90
84
  skills: Object.freeze([HERMES_CLIENT_RELATIONSHIPS_SKILL]),
91
- enabledToolsets: Object.freeze(["file", "terminal", "safe", "sellable", "mcp"]),
85
+ enabledToolsets: Object.freeze([
86
+ "file",
87
+ "terminal",
88
+ "safe",
89
+ "sellable",
90
+ "mcp",
91
+ ]),
92
92
  noAgent: false,
93
93
  });
94
94
  export const CLIENT_RELATIONSHIPS_ASSETS = Object.freeze([
@@ -104,7 +104,12 @@ const VERSION = "0.20.4";
104
104
  const ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
105
105
  const BOUNDED_ID = /^[A-Za-z0-9][A-Za-z0-9_./:-]{0,255}$/;
106
106
  const SHA256 = /^[a-f0-9]{64}$/;
107
- const MAX_BYTES = 128 * 1024 * 1024;
107
+ // Native state.db is durable conversation/thread state and grows over time.
108
+ // The previous 128 MiB ceiling left the Admin recovery ZIP only ~1.5 MiB of
109
+ // headroom in production and eventually made every protected Backup/Refresh
110
+ // fail after Hermes had already created the archive. Keep this bounded for
111
+ // memory/storage safety, but large enough for an established profile.
112
+ export const HERMES_SNAPSHOT_MAX_ARCHIVE_BYTES = 512 * 1024 * 1024;
108
113
  const MAX_PLUGIN_DATA_BYTES = 10 * 1024 * 1024;
109
114
  const MAX_PORTABLE_EXTERNAL_BYTES = 32 * 1024 * 1024;
110
115
  const MAX_PORTABLE_CONFIG_BYTES = 64 * 1024;
@@ -115,6 +120,8 @@ const MAX_PORTABLE_CONFIG_BYTES = 64 * 1024;
115
120
  // persistent retry scheduler back off instead of contending with the live
116
121
  // Slack gateway for a full ten minutes.
117
122
  const NATIVE_COMMAND_TIMEOUT_MS = 120_000;
123
+ const STATE_DB_COMPACTION_TIMEOUT_MS = 240_000;
124
+ export const HERMES_SNAPSHOT_CRON_SESSION_RETENTION_HOURS = 48;
118
125
  const TRANSIENT_PROFILE_ROOTS = new Set([
119
126
  ".artifact-venv",
120
127
  ".cache",
@@ -415,27 +422,55 @@ export function mergeHermesState(
415
422
  export function replaceHermesState(
416
423
  sourceRoot,
417
424
  targetRoot,
418
- /** @type {{preserveTargetTopLevelNames?:string[]}} */
419
- { preserveTargetTopLevelNames = [] } = {}
425
+ /** @type {{preserveTargetTopLevelNames?:string[],preserveTargetPaths?:string[]}} */
426
+ { preserveTargetTopLevelNames = [], preserveTargetPaths = [] } = {}
420
427
  ) {
421
428
  assertHermesMergeRoot(targetRoot);
422
- const preserved = hermesTopLevelNameSet(
429
+ const preservedTopLevel = hermesTopLevelNameSet(
423
430
  preserveTargetTopLevelNames,
424
431
  "hermes_snapshot_replace_policy_rejected"
425
432
  );
426
- for (const name of readdirSync(targetRoot)) {
427
- if (preserved.has(name)) continue;
428
- const targetPath = join(targetRoot, name);
429
- const stat = lstatSync(targetPath);
430
- if (stat.isSymbolicLink() || (!stat.isDirectory() && !stat.isFile())) {
431
- throw new Error("hermes_snapshot_merge_target_rejected");
433
+ const preservedPaths = new Set(
434
+ preserveTargetPaths.map((path) => {
435
+ if (
436
+ typeof path !== "string" ||
437
+ !/^[A-Za-z0-9][A-Za-z0-9._-]*(?:\/[A-Za-z0-9][A-Za-z0-9._-]*)*$/.test(
438
+ path
439
+ )
440
+ ) {
441
+ throw new Error("hermes_snapshot_replace_policy_rejected");
442
+ }
443
+ return path;
444
+ })
445
+ );
446
+ for (const name of preservedTopLevel) preservedPaths.add(name);
447
+ const prune = (target, relativeRoot = "") => {
448
+ for (const name of readdirSync(target)) {
449
+ const relativePath = relativeRoot ? `${relativeRoot}/${name}` : name;
450
+ const targetPath = join(target, name);
451
+ const stat = lstatSync(targetPath);
452
+ if (stat.isSymbolicLink() || (!stat.isDirectory() && !stat.isFile())) {
453
+ throw new Error("hermes_snapshot_merge_target_rejected");
454
+ }
455
+ if (preservedPaths.has(relativePath)) continue;
456
+ if (
457
+ [...preservedPaths].some((path) => path.startsWith(`${relativePath}/`))
458
+ ) {
459
+ if (!stat.isDirectory()) {
460
+ throw new Error("hermes_snapshot_merge_target_rejected");
461
+ }
462
+ prune(targetPath, relativePath);
463
+ continue;
464
+ }
465
+ rmSync(targetPath, { recursive: true, force: false });
432
466
  }
433
- rmSync(targetPath, { recursive: true, force: false });
434
- }
467
+ };
468
+ prune(targetRoot);
435
469
  if (!assertHermesMergeRoot(sourceRoot, { allowMissing: true })) return;
436
- mergeHermesStateEntries(sourceRoot, targetRoot, {
437
- skippedTopLevelNames: preserved,
438
- });
470
+ // The merge is deliberately no-clobber. Exact sealed target paths survive,
471
+ // while mutable source siblings (for example sellable/<custom-skill>) are
472
+ // carried into the staged profile.
473
+ mergeHermesStateEntries(sourceRoot, targetRoot);
439
474
  }
440
475
 
441
476
  const LEGACY_LIGHTFIELD_VENV = ".venv-amplify-audit";
@@ -557,6 +592,77 @@ function cronJobs(profileRoot) {
557
592
  return jobs;
558
593
  }
559
594
 
595
+ function assertStagedCronDependencies(profileRoot) {
596
+ const skillNames = new Set();
597
+ const skillsRoot = join(profileRoot, "skills");
598
+ const walkSkills = (directory) => {
599
+ for (const name of readdirSync(directory)) {
600
+ const path = join(directory, name);
601
+ const stat = lstatSync(path);
602
+ if (stat.isSymbolicLink()) {
603
+ throw new Error("hermes_snapshot_dependency_rejected");
604
+ }
605
+ if (stat.isDirectory()) walkSkills(path);
606
+ else if (stat.isFile() && name === "SKILL.md") {
607
+ skillNames.add(basename(directory));
608
+ }
609
+ }
610
+ };
611
+ if (existsSync(skillsRoot)) {
612
+ const stat = lstatSync(skillsRoot);
613
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
614
+ throw new Error("hermes_snapshot_dependency_rejected");
615
+ }
616
+ walkSkills(skillsRoot);
617
+ }
618
+ const scriptsRoot = join(profileRoot, "scripts");
619
+ for (const job of cronJobs(profileRoot)) {
620
+ if (!job || typeof job !== "object" || Array.isArray(job)) {
621
+ throw new Error("hermes_snapshot_dependency_rejected");
622
+ }
623
+ const declaredSkills = [];
624
+ if (Object.hasOwn(job, "skill") && job.skill !== null) {
625
+ if (typeof job.skill !== "string" || !ID.test(job.skill)) {
626
+ throw new Error("hermes_snapshot_dependency_rejected");
627
+ }
628
+ declaredSkills.push(job.skill);
629
+ }
630
+ if (Object.hasOwn(job, "skills") && job.skills !== null) {
631
+ if (
632
+ !Array.isArray(job.skills) ||
633
+ job.skills.some((skill) => typeof skill !== "string" || !ID.test(skill))
634
+ ) {
635
+ throw new Error("hermes_snapshot_dependency_rejected");
636
+ }
637
+ declaredSkills.push(...job.skills);
638
+ }
639
+ if (declaredSkills.some((skill) => !skillNames.has(skill))) {
640
+ throw new Error("hermes_snapshot_dependency_rejected");
641
+ }
642
+ if (Object.hasOwn(job, "script") && job.script !== null) {
643
+ if (
644
+ typeof job.script !== "string" ||
645
+ !/^[A-Za-z0-9][A-Za-z0-9._-]*(?:\/[A-Za-z0-9][A-Za-z0-9._-]*)*$/.test(
646
+ job.script
647
+ )
648
+ ) {
649
+ throw new Error("hermes_snapshot_dependency_rejected");
650
+ }
651
+ const script = job.script.startsWith("scripts/")
652
+ ? job.script.slice("scripts/".length)
653
+ : job.script;
654
+ const scriptPath = join(scriptsRoot, script);
655
+ if (!existsSync(scriptPath)) {
656
+ throw new Error("hermes_snapshot_dependency_rejected");
657
+ }
658
+ const stat = lstatSync(scriptPath);
659
+ if (stat.isSymbolicLink() || !stat.isFile()) {
660
+ throw new Error("hermes_snapshot_dependency_rejected");
661
+ }
662
+ }
663
+ }
664
+ }
665
+
560
666
  const sameJson = (left, right) =>
561
667
  JSON.stringify(left) === JSON.stringify(right);
562
668
 
@@ -695,6 +801,169 @@ const childRunner = (runChild) => runChild ?? runBoundedHermesChild;
695
801
  const childFailed = (result) =>
696
802
  result?.exitCode !== 0 || result?.timedOut || result?.outputLimitExceeded;
697
803
 
804
+ const STATE_DB_COMPACTION_SCRIPT = String.raw`
805
+ import json
806
+ import os
807
+ import sqlite3
808
+ import sys
809
+ import time
810
+
811
+ source_path, target_path = sys.argv[1], sys.argv[2]
812
+ retention_hours = int(sys.argv[3])
813
+ source = sqlite3.connect("file:" + source_path + "?mode=ro", uri=True)
814
+ destination = sqlite3.connect(target_path)
815
+ try:
816
+ source.backup(destination)
817
+ finally:
818
+ source.close()
819
+
820
+ try:
821
+ if destination.execute("PRAGMA quick_check").fetchone()[0] != "ok":
822
+ raise RuntimeError("source backup failed quick_check")
823
+ destination.execute("PRAGMA foreign_keys=ON")
824
+ table_names = {
825
+ row[0]
826
+ for row in destination.execute(
827
+ "SELECT name FROM sqlite_master WHERE type='table'"
828
+ )
829
+ }
830
+ pruned_sessions = 0
831
+ pruned_messages = 0
832
+ if {"sessions", "messages"}.issubset(table_names):
833
+ cutoff = time.time() - retention_hours * 60 * 60
834
+ session_columns = {
835
+ row[1] for row in destination.execute("PRAGMA table_info(sessions)")
836
+ }
837
+ pinned_clause = (
838
+ "AND COALESCE(pinned, 0) = 0" if "pinned" in session_columns else ""
839
+ )
840
+ destination.execute(
841
+ "CREATE TEMP TABLE snapshot_pruned_sessions(id TEXT PRIMARY KEY)"
842
+ )
843
+ destination.execute(
844
+ "WITH RECURSIVE doomed(id) AS ("
845
+ " SELECT id FROM sessions"
846
+ " WHERE source = 'cron' AND ended_at IS NOT NULL"
847
+ " AND COALESCE(last_activity_at, ended_at, started_at) < ? "
848
+ + pinned_clause
849
+ + " UNION"
850
+ " SELECT child.id FROM sessions child"
851
+ " JOIN doomed parent ON child.parent_session_id = parent.id"
852
+ ") INSERT OR IGNORE INTO snapshot_pruned_sessions(id)"
853
+ " SELECT id FROM doomed",
854
+ (cutoff,),
855
+ )
856
+ pruned_sessions = destination.execute(
857
+ "SELECT COUNT(*) FROM snapshot_pruned_sessions"
858
+ ).fetchone()[0]
859
+ pruned_messages = destination.execute(
860
+ "SELECT COUNT(*) FROM messages WHERE session_id IN "
861
+ "(SELECT id FROM snapshot_pruned_sessions)"
862
+ ).fetchone()[0]
863
+ destination.execute(
864
+ "DELETE FROM messages WHERE session_id IN "
865
+ "(SELECT id FROM snapshot_pruned_sessions)"
866
+ )
867
+ if "session_model_usage" in table_names:
868
+ destination.execute(
869
+ "DELETE FROM session_model_usage WHERE session_id IN "
870
+ "(SELECT id FROM snapshot_pruned_sessions)"
871
+ )
872
+ destination.execute(
873
+ "DELETE FROM sessions WHERE id IN "
874
+ "(SELECT id FROM snapshot_pruned_sessions)"
875
+ )
876
+ if "system_prompts" in table_names:
877
+ destination.execute(
878
+ "DELETE FROM system_prompts WHERE NOT EXISTS ("
879
+ " SELECT 1 FROM sessions"
880
+ " WHERE sessions.system_prompt_hash = system_prompts.hash"
881
+ ")"
882
+ )
883
+ destination.commit()
884
+ for table in ("messages_fts", "messages_fts_trigram", "messages_fts_cjk"):
885
+ if table in table_names:
886
+ destination.execute(
887
+ 'INSERT INTO "' + table + '" ("' + table + '") VALUES (\'optimize\')'
888
+ )
889
+ destination.commit()
890
+ destination.execute("VACUUM")
891
+ if destination.execute("PRAGMA quick_check").fetchone()[0] != "ok":
892
+ raise RuntimeError("compacted backup failed quick_check")
893
+ if destination.execute("PRAGMA foreign_key_check").fetchone() is not None:
894
+ raise RuntimeError("compacted backup failed foreign_key_check")
895
+ finally:
896
+ destination.close()
897
+
898
+ os.chmod(target_path, 0o600)
899
+ print(json.dumps({
900
+ "ok": True,
901
+ "prunedSessions": pruned_sessions,
902
+ "prunedMessages": pruned_messages,
903
+ "retentionHours": retention_hours,
904
+ "archiveStateBytes": os.path.getsize(target_path),
905
+ }, separators=(",", ":")))
906
+ `;
907
+
908
+ export async function compactHermesSnapshotStateDb({
909
+ sourcePath,
910
+ targetPath,
911
+ retentionHours = HERMES_SNAPSHOT_CRON_SESSION_RETENTION_HOURS,
912
+ hermesPython = HERMES_PYTHON,
913
+ runChild = undefined,
914
+ }) {
915
+ if (
916
+ !isAbsolute(sourcePath) ||
917
+ !isAbsolute(targetPath) ||
918
+ resolve(sourcePath) === resolve(targetPath) ||
919
+ !existsSync(sourcePath) ||
920
+ existsSync(targetPath) ||
921
+ !Number.isSafeInteger(retentionHours) ||
922
+ retentionHours < 1 ||
923
+ retentionHours > 30 * 24
924
+ ) {
925
+ throw new Error("hermes_snapshot_state_db_compaction_rejected");
926
+ }
927
+ regularFile(sourcePath);
928
+ mkdirSync(dirname(targetPath), { recursive: true, mode: 0o700 });
929
+ const result = await childRunner(runChild)({
930
+ command: hermesPython,
931
+ args: [
932
+ "-c",
933
+ STATE_DB_COMPACTION_SCRIPT,
934
+ sourcePath,
935
+ targetPath,
936
+ String(retentionHours),
937
+ ],
938
+ env: { ...process.env, NO_COLOR: "1" },
939
+ shell: false,
940
+ timeoutMs: STATE_DB_COMPACTION_TIMEOUT_MS,
941
+ maxOutputBytes: 64 * 1024,
942
+ });
943
+ if (childFailed(result) || !existsSync(targetPath)) {
944
+ throw new Error("hermes_snapshot_state_db_compaction_failed");
945
+ }
946
+ const target = regularFile(targetPath);
947
+ let summary;
948
+ try {
949
+ summary = JSON.parse(String(result.stdout ?? ""));
950
+ } catch {
951
+ throw new Error("hermes_snapshot_state_db_compaction_failed");
952
+ }
953
+ if (
954
+ summary?.ok !== true ||
955
+ summary.retentionHours !== retentionHours ||
956
+ !Number.isSafeInteger(summary.prunedSessions) ||
957
+ summary.prunedSessions < 0 ||
958
+ !Number.isSafeInteger(summary.prunedMessages) ||
959
+ summary.prunedMessages < 0 ||
960
+ summary.archiveStateBytes !== target.size
961
+ ) {
962
+ throw new Error("hermes_snapshot_state_db_compaction_failed");
963
+ }
964
+ return summary;
965
+ }
966
+
698
967
  function nativeSummaryAccepted(result, complete, incomplete) {
699
968
  const output = `${result?.stdout ?? ""}\n${result?.stderr ?? ""}`;
700
969
  return (
@@ -807,6 +1076,10 @@ function stageHermesUserState({ profileId, profileRoot, hermesRoot }) {
807
1076
  for (const name of HERMES_SNAPSHOT_USER_STATE_ENTRIES) {
808
1077
  const source = join(profileRoot, name);
809
1078
  if (existsSync(source)) {
1079
+ // state.db is copied online and compacted after the rest of the staged
1080
+ // tree is assembled. Never hard-link this one file: pruning the staged
1081
+ // recovery copy must be physically incapable of touching the live DB.
1082
+ if (name === "state.db") continue;
810
1083
  stageUserStateEntry(
811
1084
  source,
812
1085
  join(stagedProfileRoot, name),
@@ -1426,7 +1699,9 @@ export async function ensureHermesClientRelationshipsCrons({
1426
1699
  return failed("hermes_client_relationships_cron_prune_failed");
1427
1700
  }
1428
1701
  if (cronJobs(profileRoot).some((after) => after?.id === job.id)) {
1429
- return failed("hermes_client_relationships_cron_prune_readback_rejected");
1702
+ return failed(
1703
+ "hermes_client_relationships_cron_prune_readback_rejected"
1704
+ );
1430
1705
  }
1431
1706
  removed.push({ name: job.name, jobId: job.id, status: "REMOVED" });
1432
1707
  }
@@ -1516,7 +1791,10 @@ export async function captureHermesBackup({
1516
1791
  hermesVersion,
1517
1792
  archiveDirectory = "/tmp",
1518
1793
  hermesCli = HERMES,
1519
- maxArchiveBytes = MAX_BYTES,
1794
+ hermesPython = HERMES_PYTHON,
1795
+ maxArchiveBytes = HERMES_SNAPSHOT_MAX_ARCHIVE_BYTES,
1796
+ cronSessionRetentionHours = HERMES_SNAPSHOT_CRON_SESSION_RETENTION_HOURS,
1797
+ stateDbCompactor = compactHermesSnapshotStateDb,
1520
1798
  runChild,
1521
1799
  operationIdentity = /** @type {any} */ (undefined),
1522
1800
  }) {
@@ -1554,6 +1832,18 @@ export async function captureHermesBackup({
1554
1832
  let stagingRoot;
1555
1833
  try {
1556
1834
  stagingRoot = stageHermesUserState({ profileId, profileRoot, hermesRoot });
1835
+ const stagedProfileRoot = join(stagingRoot, "profiles", profileId);
1836
+ const sourceStateDb = join(profileRoot, "state.db");
1837
+ if (existsSync(sourceStateDb)) {
1838
+ await stateDbCompactor({
1839
+ sourcePath: sourceStateDb,
1840
+ targetPath: join(stagedProfileRoot, "state.db"),
1841
+ retentionHours: cronSessionRetentionHours,
1842
+ hermesPython,
1843
+ runChild,
1844
+ });
1845
+ }
1846
+ assertStagedCronDependencies(stagedProfileRoot);
1557
1847
  const result = await childRunner(runChild)(
1558
1848
  invocation(["backup", "-o", archivePath], stagingRoot, hermesCli)
1559
1849
  );
@@ -1588,7 +1878,7 @@ export async function captureHermesBackup({
1588
1878
  } catch (error) {
1589
1879
  const code = error instanceof Error ? error.message : "";
1590
1880
  return failed(
1591
- /^hermes_snapshot_(?:external_state|plugin_data|portable_config)/.test(
1881
+ /^hermes_snapshot_(?:external_state|plugin_data|portable_config|dependency|state_db)/.test(
1592
1882
  code
1593
1883
  )
1594
1884
  ? code
@@ -1783,7 +2073,7 @@ function validLatest(latest, hermesVersion) {
1783
2073
  return (
1784
2074
  Buffer.isBuffer(latest?.bytes) &&
1785
2075
  latest.bytes.byteLength > 0 &&
1786
- latest.bytes.byteLength <= MAX_BYTES &&
2076
+ latest.bytes.byteLength <= HERMES_SNAPSHOT_MAX_ARCHIVE_BYTES &&
1787
2077
  latest.archiveBytes === latest.bytes.byteLength &&
1788
2078
  SHA256.test(latest.archiveDigest ?? "") &&
1789
2079
  sha256(latest.bytes) === latest.archiveDigest &&
@@ -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.682";
42
- const MCP_PACKAGE = "@sellable/mcp@0.1.899";
41
+ const INSTALLER_PACKAGE = "@sellable/install@0.1.683";
42
+ const MCP_PACKAGE = "@sellable/mcp@0.1.900";
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}$/;
@@ -1547,8 +1547,8 @@ export function compileClaimToProfileDesired(activeClaim, config) {
1547
1547
  pinned.hermesCli !== "hermes" ||
1548
1548
  pinned.hermesVersion !== "0.18.0" ||
1549
1549
  pinned.installerPackage !==
1550
- "@sellable/install@0.1.682" ||
1551
- pinned.mcpPackage !== "@sellable/mcp@0.1.899" ||
1550
+ "@sellable/install@0.1.683" ||
1551
+ pinned.mcpPackage !== "@sellable/mcp@0.1.900" ||
1552
1552
  !Array.isArray(policy.toolInclude) ||
1553
1553
  policy.toolInclude.length === 0 ||
1554
1554
  !Number.isSafeInteger(slack.generation) ||
@@ -283,8 +283,8 @@ function validateDesired(desired) {
283
283
  desired.hermesCli !== "hermes" ||
284
284
  desired.hermesVersion !== "0.18.0" ||
285
285
  desired.installerPackage !==
286
- "@sellable/install@0.1.682" ||
287
- desired.mcpPackage !== "@sellable/mcp@0.1.899" ||
286
+ "@sellable/install@0.1.683" ||
287
+ desired.mcpPackage !== "@sellable/mcp@0.1.900" ||
288
288
  !Array.isArray(desired.toolInclude) ||
289
289
  desired.toolInclude.length === 0 ||
290
290
  desired.toolInclude.some((tool) => !/^[a-z][a-z0-9_]{0,127}$/.test(tool)) ||
@@ -26,8 +26,8 @@ import { deriveContainedProfileId } from "./profile-materializer.mjs";
26
26
 
27
27
  export const PROVISIONING_ACTION = "PROVISION_HERMES_PROFILE";
28
28
  export const PINNED_INSTALL_PACKAGE =
29
- "@sellable/install@0.1.682";
30
- export const PINNED_MCP_PACKAGE = "@sellable/mcp@0.1.899";
29
+ "@sellable/install@0.1.683";
30
+ export const PINNED_MCP_PACKAGE = "@sellable/mcp@0.1.900";
31
31
  const PINNED_INSTALL_VERSION = PINNED_INSTALL_PACKAGE.slice(
32
32
  PINNED_INSTALL_PACKAGE.lastIndexOf("@") + 1
33
33
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/install",
3
- "version": "0.1.682",
3
+ "version": "0.1.683",
4
4
  "type": "module",
5
5
  "description": "One-command installer for Sellable MCP in Claude Code, Codex, and Hermes",
6
6
  "bin": {