@joshuaswarren/openclaw-engram 9.0.46 → 9.0.47

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 CHANGED
@@ -46,7 +46,7 @@ AI agents forget everything between conversations. Engram fixes that.
46
46
  - **Creation-memory ledger** — Engram can now, when `creationMemoryEnabled` is enabled, persist a typed work-product ledger for explicit outputs agents create or update, inspect it with `openclaw engram work-product-status`, and write deterministic entries through `openclaw engram work-product-record`.
47
47
  - **Artifact recovery recall** — Engram can now, when both `creationMemoryEnabled` and `workProductRecallEnabled` are enabled, surface prompt-relevant work-product ledger entries back into recall as a dedicated `Work Products` section and inspect reuse candidates with `openclaw engram work-product-recall-search`.
48
48
  - **Commitment lifecycle foundation** — Engram can now, when `creationMemoryEnabled`, `commitmentLedgerEnabled`, and `commitmentLifecycleEnabled` are enabled, transition existing commitments to `fulfilled` / `cancelled` / `expired`, inspect overdue and stale obligations in `openclaw engram commitment-status`, and run deterministic lifecycle cleanup with `openclaw engram commitment-lifecycle-run`.
49
- - **Resume-bundle format foundation** — Engram can now, when `creationMemoryEnabled` and `resumeBundlesEnabled` are enabled, persist typed crash-recovery resume bundles, inspect them with `openclaw engram resume-bundle-status`, and write deterministic handoff shells through `openclaw engram resume-bundle-record`.
49
+ - **Resume-bundle builder** — Engram can now, when `creationMemoryEnabled` and `resumeBundlesEnabled` are enabled, persist typed crash-recovery resume bundles, inspect them with `openclaw engram resume-bundle-status`, write explicit handoff shells through `openclaw engram resume-bundle-record`, and build bounded resume bundles from transcript recovery, recent objective-state snapshots, work products, and open commitments through `openclaw engram resume-bundle-build`.
50
50
  - **Zero-config start** — Install, add an API key, restart. Engram works out of the box with sensible defaults and progressively unlocks advanced features as you enable them.
51
51
 
52
52
  ## Quick Start
@@ -229,7 +229,7 @@ Key settings:
229
229
  | `commitmentLifecycleEnabled` | `false` | Enable commitment lifecycle transitions, stale tracking, and resolved-entry cleanup for the commitment ledger |
230
230
  | `commitmentStaleDays` | `14` | Days before an open commitment without a due date is considered stale in lifecycle status |
231
231
  | `commitmentLedgerDir` | `{memoryDir}/state/commitment-ledger` | Root directory for typed commitment ledger entries |
232
- | `resumeBundlesEnabled` | `false` | Enable typed resume-bundle storage plus the operator-facing `resume-bundle-status` and `resume-bundle-record` commands |
232
+ | `resumeBundlesEnabled` | `false` | Enable typed resume-bundle storage plus the operator-facing `resume-bundle-status`, `resume-bundle-record`, and `resume-bundle-build` commands |
233
233
  | `resumeBundleDir` | `{memoryDir}/state/resume-bundles` | Root directory for typed resume bundles |
234
234
  | `workProductRecallEnabled` | `false` | Inject prompt-relevant work-product ledger entries into recall and expose `openclaw engram work-product-recall-search` |
235
235
  | `workProductLedgerDir` | `{memoryDir}/state/work-product-ledger` | Root directory for typed work-product ledger entries |
package/dist/index.js CHANGED
@@ -27828,6 +27828,7 @@ async function runCompatChecks(options) {
27828
27828
  // src/resume-bundles.ts
27829
27829
  import path59 from "path";
27830
27830
  import { mkdir as mkdir40, writeFile as writeFile37 } from "fs/promises";
27831
+ var DEFAULT_RESUME_BUNDLE_REF_LIMIT = 5;
27831
27832
  function resolveResumeBundleDir(memoryDir, overrideDir) {
27832
27833
  if (typeof overrideDir === "string" && overrideDir.trim().length > 0) {
27833
27834
  return overrideDir.trim();
@@ -27862,6 +27863,128 @@ function validateResumeBundle(raw) {
27862
27863
  metadata: validateStringRecord(raw.metadata, "metadata")
27863
27864
  };
27864
27865
  }
27866
+ async function readValidatedItems(options) {
27867
+ const files = await listJsonFiles(options.rootDir);
27868
+ const items = [];
27869
+ for (const filePath of files) {
27870
+ try {
27871
+ items.push(options.validate(await readJsonFile(filePath)));
27872
+ } catch {
27873
+ }
27874
+ }
27875
+ return items;
27876
+ }
27877
+ async function readObjectiveStateSnapshotsForSession(options) {
27878
+ const rootDir = resolveObjectiveStateStoreDir(options.memoryDir, options.objectiveStateStoreDir);
27879
+ const items = await readValidatedItems({
27880
+ rootDir: path59.join(rootDir, "snapshots"),
27881
+ validate: validateObjectiveStateSnapshot
27882
+ });
27883
+ return items.filter((item) => item.sessionKey === options.sessionKey).sort((left, right) => right.recordedAt.localeCompare(left.recordedAt)).slice(0, options.maxResults ?? DEFAULT_RESUME_BUNDLE_REF_LIMIT);
27884
+ }
27885
+ async function readWorkProductEntriesForSession(options) {
27886
+ const rootDir = resolveWorkProductLedgerDir(options.memoryDir, options.workProductLedgerDir);
27887
+ const items = await readValidatedItems({
27888
+ rootDir: path59.join(rootDir, "entries"),
27889
+ validate: validateWorkProductLedgerEntry
27890
+ });
27891
+ return items.filter((item) => item.sessionKey === options.sessionKey).sort((left, right) => right.recordedAt.localeCompare(left.recordedAt)).slice(0, options.maxResults ?? DEFAULT_RESUME_BUNDLE_REF_LIMIT);
27892
+ }
27893
+ async function readCommitmentEntriesForSession(options) {
27894
+ const rootDir = resolveCommitmentLedgerDir(options.memoryDir, options.commitmentLedgerDir);
27895
+ const items = await readValidatedItems({
27896
+ rootDir: path59.join(rootDir, "entries"),
27897
+ validate: validateCommitmentLedgerEntry
27898
+ });
27899
+ return items.filter((item) => item.sessionKey === options.sessionKey).filter((item) => options.state ? item.state === options.state : true).sort((left, right) => right.recordedAt.localeCompare(left.recordedAt)).slice(0, options.maxResults ?? DEFAULT_RESUME_BUNDLE_REF_LIMIT);
27900
+ }
27901
+ function buildRecoveryFact(recoverySummary) {
27902
+ return recoverySummary.healthy ? `Transcript recovery healthy with ${recoverySummary.issueCount} issue(s), ${recoverySummary.incompleteTurns} incomplete turn(s), and ${recoverySummary.brokenChains} broken chain(s).` : `Transcript recovery flagged ${recoverySummary.issueCount} issue(s), ${recoverySummary.incompleteTurns} incomplete turn(s), and ${recoverySummary.brokenChains} broken chain(s); checkpoint healthy: ${recoverySummary.checkpointHealthy ? "yes" : "no"}.`;
27903
+ }
27904
+ function buildBundleSummary(options) {
27905
+ const parts = [
27906
+ `${options.openCommitmentCount} open commitment(s)`,
27907
+ `${options.workProductCount} recent work product(s)`,
27908
+ `${options.objectiveSnapshotCount} recent objective-state snapshot(s)`
27909
+ ];
27910
+ if (options.recoveryHealthy === false) {
27911
+ parts.push("transcript recovery issues remain");
27912
+ }
27913
+ return `Resume ${options.sessionKey}: ${parts.join(", ")}.`;
27914
+ }
27915
+ async function buildResumeBundleFromState(options) {
27916
+ const recordedAt = assertIsoRecordedAt(options.recordedAt, "recordedAt");
27917
+ const maxRefsPerStore = Math.max(1, Math.floor(options.maxRefsPerStore ?? DEFAULT_RESUME_BUNDLE_REF_LIMIT));
27918
+ const objectiveSnapshots = options.objectiveStateMemoryEnabled ? await readObjectiveStateSnapshotsForSession({
27919
+ memoryDir: options.memoryDir,
27920
+ objectiveStateStoreDir: options.objectiveStateStoreDir,
27921
+ sessionKey: options.sessionKey,
27922
+ maxResults: maxRefsPerStore
27923
+ }) : [];
27924
+ const workProducts = options.creationMemoryEnabled ? await readWorkProductEntriesForSession({
27925
+ memoryDir: options.memoryDir,
27926
+ workProductLedgerDir: options.workProductLedgerDir,
27927
+ sessionKey: options.sessionKey,
27928
+ maxResults: maxRefsPerStore
27929
+ }) : [];
27930
+ const commitments = options.creationMemoryEnabled && options.commitmentLedgerEnabled ? await readCommitmentEntriesForSession({
27931
+ memoryDir: options.memoryDir,
27932
+ commitmentLedgerDir: options.commitmentLedgerDir,
27933
+ sessionKey: options.sessionKey,
27934
+ maxResults: maxRefsPerStore,
27935
+ state: "open"
27936
+ }) : [];
27937
+ const openCommitments = commitments;
27938
+ const recordedAtMs = Date.parse(recordedAt);
27939
+ const overdueCommitments = openCommitments.filter((entry) => {
27940
+ if (!entry.dueAt) return false;
27941
+ const dueAtMs = Date.parse(entry.dueAt);
27942
+ return Number.isFinite(dueAtMs) && dueAtMs < recordedAtMs;
27943
+ });
27944
+ let recoverySummary;
27945
+ if (options.transcriptEnabled) {
27946
+ const transcript = new TranscriptManager(parseConfig({
27947
+ memoryDir: options.memoryDir,
27948
+ transcriptEnabled: true
27949
+ }));
27950
+ await transcript.initialize();
27951
+ recoverySummary = await transcript.getRecoverySummary(options.sessionKey);
27952
+ }
27953
+ const keyFacts = [
27954
+ recoverySummary ? buildRecoveryFact(recoverySummary) : void 0,
27955
+ workProducts[0]?.summary,
27956
+ objectiveSnapshots[0]?.summary
27957
+ ].filter((value) => typeof value === "string" && value.length > 0);
27958
+ const nextActions = openCommitments.map((entry) => entry.summary).filter((summary, index, values) => values.indexOf(summary) === index).slice(0, maxRefsPerStore);
27959
+ const riskFlags = [
27960
+ recoverySummary && !recoverySummary.healthy ? buildRecoveryFact(recoverySummary) : void 0,
27961
+ ...objectiveSnapshots.filter((snapshot) => snapshot.outcome === "failure" || snapshot.outcome === "partial").map((snapshot) => snapshot.summary),
27962
+ ...overdueCommitments.map((entry) => `Overdue commitment: ${entry.summary}`)
27963
+ ].filter(
27964
+ (value, index, values) => typeof value === "string" && value.length > 0 && values.indexOf(value) === index
27965
+ );
27966
+ return validateResumeBundle({
27967
+ schemaVersion: 1,
27968
+ bundleId: options.bundleId,
27969
+ recordedAt,
27970
+ sessionKey: options.sessionKey,
27971
+ source: options.source ?? "system",
27972
+ scope: options.scope,
27973
+ summary: buildBundleSummary({
27974
+ sessionKey: options.sessionKey,
27975
+ openCommitmentCount: openCommitments.length,
27976
+ workProductCount: workProducts.length,
27977
+ objectiveSnapshotCount: objectiveSnapshots.length,
27978
+ recoveryHealthy: recoverySummary?.healthy
27979
+ }),
27980
+ objectiveStateSnapshotRefs: objectiveSnapshots.map((snapshot) => snapshot.snapshotId),
27981
+ workProductEntryRefs: workProducts.map((entry) => entry.entryId),
27982
+ commitmentEntryRefs: openCommitments.map((entry) => entry.entryId),
27983
+ keyFacts: keyFacts.length > 0 ? keyFacts : void 0,
27984
+ nextActions: nextActions.length > 0 ? nextActions : void 0,
27985
+ riskFlags: riskFlags.length > 0 ? riskFlags : void 0
27986
+ });
27987
+ }
27865
27988
  async function recordResumeBundle(options) {
27866
27989
  const rootDir = resolveResumeBundleDir(options.memoryDir, options.resumeBundleDir);
27867
27990
  const validated = validateResumeBundle(options.bundle);
@@ -28354,6 +28477,31 @@ async function runResumeBundleRecordCliCommand(options) {
28354
28477
  bundle: options.bundle
28355
28478
  });
28356
28479
  }
28480
+ async function runResumeBundleBuildCliCommand(options) {
28481
+ if (!options.creationMemoryEnabled || !options.resumeBundlesEnabled) {
28482
+ return null;
28483
+ }
28484
+ const bundle = await buildResumeBundleFromState({
28485
+ memoryDir: options.memoryDir,
28486
+ sessionKey: options.sessionKey,
28487
+ bundleId: options.bundleId,
28488
+ recordedAt: options.recordedAt,
28489
+ scope: options.scope,
28490
+ transcriptEnabled: options.transcriptEnabled,
28491
+ objectiveStateMemoryEnabled: options.objectiveStateMemoryEnabled,
28492
+ objectiveStateStoreDir: options.objectiveStateStoreDir,
28493
+ creationMemoryEnabled: options.creationMemoryEnabled,
28494
+ workProductLedgerDir: options.workProductLedgerDir,
28495
+ commitmentLedgerEnabled: options.commitmentLedgerEnabled,
28496
+ commitmentLedgerDir: options.commitmentLedgerDir
28497
+ });
28498
+ const filePath = await recordResumeBundle({
28499
+ memoryDir: options.memoryDir,
28500
+ resumeBundleDir: options.resumeBundleDir,
28501
+ bundle
28502
+ });
28503
+ return { bundle, filePath };
28504
+ }
28357
28505
  async function runCommitmentStatusCliCommand(options) {
28358
28506
  return getCommitmentLedgerStatus({
28359
28507
  memoryDir: options.memoryDir,
@@ -29669,6 +29817,31 @@ function registerCli(api, orchestrator) {
29669
29817
  console.log(JSON.stringify({ wrote: filePath !== null, filePath }, null, 2));
29670
29818
  console.log("OK");
29671
29819
  });
29820
+ cmd.command("resume-bundle-build").description("Build and persist a resume bundle from transcript recovery, objective state, work products, and open commitments").requiredOption("--bundle-id <bundleId>", "Resume bundle id").requiredOption("--recorded-at <recordedAt>", "ISO timestamp for the bundle").requiredOption("--session-key <sessionKey>", "Session key that owns the bundle").requiredOption("--scope <scope>", "Primary scope or recovery domain for the bundle").action(async (...args) => {
29821
+ const options = args[0] ?? {};
29822
+ const built = await runResumeBundleBuildCliCommand({
29823
+ memoryDir: orchestrator.config.memoryDir,
29824
+ resumeBundleDir: orchestrator.config.resumeBundleDir,
29825
+ objectiveStateStoreDir: orchestrator.config.objectiveStateStoreDir,
29826
+ workProductLedgerDir: orchestrator.config.workProductLedgerDir,
29827
+ commitmentLedgerDir: orchestrator.config.commitmentLedgerDir,
29828
+ creationMemoryEnabled: orchestrator.config.creationMemoryEnabled,
29829
+ resumeBundlesEnabled: orchestrator.config.resumeBundlesEnabled,
29830
+ transcriptEnabled: orchestrator.config.transcriptEnabled,
29831
+ objectiveStateMemoryEnabled: orchestrator.config.objectiveStateMemoryEnabled,
29832
+ commitmentLedgerEnabled: orchestrator.config.commitmentLedgerEnabled,
29833
+ bundleId: String(options.bundleId ?? ""),
29834
+ recordedAt: String(options.recordedAt ?? ""),
29835
+ sessionKey: String(options.sessionKey ?? ""),
29836
+ scope: String(options.scope ?? "")
29837
+ });
29838
+ console.log(JSON.stringify({
29839
+ wrote: built !== null,
29840
+ filePath: built?.filePath ?? null,
29841
+ bundle: built?.bundle ?? null
29842
+ }, null, 2));
29843
+ console.log("OK");
29844
+ });
29672
29845
  cmd.command("commitment-status").description("Show commitment ledger status, entry counts, and the latest recorded commitment").action(async () => {
29673
29846
  const status = await runCommitmentStatusCliCommand({
29674
29847
  memoryDir: orchestrator.config.memoryDir,