@llblab/pi-kit 0.22.1 → 0.23.0

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.
Files changed (87) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +4 -4
  3. package/node_modules/@llblab/pi-grow-loop/AGENTS.md +10 -6
  4. package/node_modules/@llblab/pi-grow-loop/CHANGELOG.md +7 -0
  5. package/node_modules/@llblab/pi-grow-loop/README.md +2 -0
  6. package/node_modules/@llblab/pi-grow-loop/dist/index.d.ts +33 -0
  7. package/node_modules/@llblab/pi-grow-loop/dist/index.js +286 -0
  8. package/node_modules/@llblab/pi-grow-loop/dist/pi-grow-loop/index.d.ts +1 -0
  9. package/node_modules/@llblab/pi-grow-loop/dist/pi-grow-loop/index.js +1 -0
  10. package/node_modules/@llblab/pi-grow-loop/dist/skills/grow-loop/SKILL.md +117 -0
  11. package/node_modules/@llblab/pi-grow-loop/dist/skills/while-true/SKILL.md +233 -0
  12. package/node_modules/@llblab/pi-grow-loop/index.ts +67 -12
  13. package/node_modules/@llblab/pi-grow-loop/package.json +9 -8
  14. package/node_modules/@llblab/pi-state-flow/AGENTS.md +23 -17
  15. package/node_modules/@llblab/pi-state-flow/BACKLOG.md +5 -3
  16. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +11 -0
  17. package/node_modules/@llblab/pi-state-flow/README.md +18 -6
  18. package/node_modules/@llblab/pi-state-flow/dist/index.d.ts +2 -1
  19. package/node_modules/@llblab/pi-state-flow/dist/index.js +1 -0
  20. package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.d.ts +2 -2
  21. package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.js +5 -5
  22. package/node_modules/@llblab/pi-state-flow/dist/lib/context.d.ts +3 -2
  23. package/node_modules/@llblab/pi-state-flow/dist/lib/context.js +7 -2
  24. package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.d.ts +5 -5
  25. package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.js +54 -40
  26. package/node_modules/@llblab/pi-state-flow/dist/lib/durable.js +20 -20
  27. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +755 -325
  28. package/node_modules/@llblab/pi-state-flow/dist/lib/git.d.ts +2 -2
  29. package/node_modules/@llblab/pi-state-flow/dist/lib/git.js +14 -17
  30. package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.d.ts +4 -0
  31. package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +65 -23
  32. package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.d.ts +16 -5
  33. package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.js +32 -15
  34. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.d.ts +28 -2
  35. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.js +276 -26
  36. package/node_modules/@llblab/pi-state-flow/dist/lib/session.d.ts +5 -0
  37. package/node_modules/@llblab/pi-state-flow/dist/lib/session.js +36 -2
  38. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.d.ts +3 -0
  39. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.js +3 -0
  40. package/node_modules/@llblab/pi-state-flow/dist/lib/status.d.ts +1 -0
  41. package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +3 -1
  42. package/node_modules/@llblab/pi-state-flow/dist/lib/storage.d.ts +11 -0
  43. package/node_modules/@llblab/pi-state-flow/dist/lib/storage.js +150 -24
  44. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.d.ts +14 -1
  45. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +52 -18
  46. package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +8 -8
  47. package/node_modules/@llblab/pi-state-flow/dist/package.json +1 -1
  48. package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-guide/SKILL.md +3 -1
  49. package/node_modules/@llblab/pi-state-flow/docs/architecture.md +53 -7
  50. package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +84 -44
  51. package/node_modules/@llblab/pi-state-flow/docs/filesystem-recovery.md +12 -2
  52. package/node_modules/@llblab/pi-state-flow/docs/fork-contract.md +6 -4
  53. package/node_modules/@llblab/pi-state-flow/docs/performance.md +2 -2
  54. package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +28 -8
  55. package/node_modules/@llblab/pi-state-flow/docs/usage.md +41 -14
  56. package/node_modules/@llblab/pi-state-flow/index.ts +3 -0
  57. package/node_modules/@llblab/pi-state-flow/lib/artifact.ts +5 -5
  58. package/node_modules/@llblab/pi-state-flow/lib/context.ts +8 -3
  59. package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +57 -40
  60. package/node_modules/@llblab/pi-state-flow/lib/durable.ts +20 -20
  61. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +719 -316
  62. package/node_modules/@llblab/pi-state-flow/lib/git.ts +16 -18
  63. package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +60 -24
  64. package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +34 -21
  65. package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +290 -25
  66. package/node_modules/@llblab/pi-state-flow/lib/session.ts +37 -2
  67. package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +3 -0
  68. package/node_modules/@llblab/pi-state-flow/lib/status.ts +4 -1
  69. package/node_modules/@llblab/pi-state-flow/lib/storage.ts +141 -22
  70. package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +60 -19
  71. package/node_modules/@llblab/pi-state-flow/lib/transition.ts +8 -8
  72. package/node_modules/@llblab/pi-state-flow/package.json +1 -1
  73. package/node_modules/@llblab/pi-state-flow/skills/state-flow-guide/SKILL.md +3 -1
  74. package/node_modules/@llblab/pi-telegram/AGENTS.md +3 -2
  75. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +10 -0
  76. package/node_modules/@llblab/pi-telegram/README.md +3 -1
  77. package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.js +22 -3
  78. package/node_modules/@llblab/pi-telegram/dist/lib/skills.d.ts +8 -2
  79. package/node_modules/@llblab/pi-telegram/dist/lib/skills.js +36 -4
  80. package/node_modules/@llblab/pi-telegram/dist/package.json +3 -8
  81. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +1 -1
  82. package/node_modules/@llblab/pi-telegram/docs/public-api.md +1 -1
  83. package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +27 -3
  84. package/node_modules/@llblab/pi-telegram/lib/skills.ts +49 -5
  85. package/node_modules/@llblab/pi-telegram/package.json +3 -8
  86. package/node_modules/@llblab/pi-telegram/scripts/build-dist.mjs +103 -32
  87. package/package.json +6 -6
@@ -1,7 +1,9 @@
1
1
  # Backlog
2
2
 
3
- No open implementation tasks.
3
+ No open implementation items for the 0.19.0 release scope.
4
4
 
5
- Completed outcomes are recorded in [CHANGELOG.md](CHANGELOG.md). Maintained contracts and acceptance evidence belong to [docs](docs/README.md).
5
+ Release publication is owned by the version-tag workflow in `.github/workflows/release.yml`; a tag alone is not completion. Verify successful automation, the matching published GitHub Release and npm package before reporting the release complete.
6
6
 
7
- Live-host, provider and platform verification limits are documented in [SDK compatibility](docs/compatibility.md). Production-store conversion and installed-host activation remain separate operator decisions; see [usage and recovery](docs/usage.md).
7
+ Completed outcomes are recorded in [CHANGELOG.md](CHANGELOG.md). Maintained contracts and acceptance evidence belong to [docs](docs/README.md), including the accepted [persistence limitation](docs/filesystem-recovery.md#power-loss-durability) and [library API compatibility](docs/compatibility.md#state-flow-library-api-compatibility).
8
+
9
+ Live-host, provider and platform verification limits remain documented in [SDK compatibility](docs/compatibility.md). Production-store conversion and installed-host activation remain separate operator decisions; see [usage and recovery](docs/usage.md).
@@ -2,6 +2,17 @@
2
2
 
3
3
  > Each release keeps at most 8 outcome records of at most 512 characters.
4
4
 
5
+ ## 0.19.0: Awaited Memory and Lossless Mode Changes
6
+
7
+ - `Lossless context toggles`: Stop retains uncompiled conversation through Start/Stop, reload and restart without resurrecting compiled history. Interrupted runs keep their native anchor or conservatively retain available context. Uncheckpointed requests and interrupted boundary continuations survive idle Stop/reload even without a specification; successful bootstrap releases the extra context. Completed idle Stop stays bounded.
8
+ - `Awaited activation and restoration`: Startup/tree/fork/auto-start and fenced reload await owned selection. Start activates current same-session memory without claiming expired history. Restoration, auto-start initialization and fork copying survive Stop and cancelled Start waiters; their acceptance applies passive policy and permits configured passive patching. Selection/shutdown still revoke stale work. Acceptance installs memory; late failures never roll it back. Active Start is inert.
9
+ - `Awaited fail-operational Stop`: Local policy and passive context switch before cancelable publication waiting. Runtime-only acceptance preserves semantic/provenance files; repeats share the wait. Selection/shutdown/successful Start cancel obsolete work. Failed writes retain cached memory/context and a native fence until accepted Start. Telegram acknowledges early, drops stale results and keeps legacy ports. Forks inherit policy, not fences; fenced reload stays read-only.
10
+ - `Compact protocol and diagnostics`: Deduplicates injected plane, scope, response, stewardship and provenance rules without changing tools or memory semantics. Short errors retain operation, scope/path suffix and cause across tools, lifecycle, status and Telegram. Nested/aggregate causes survive transport; operand-first elision preserves Unicode and apostrophes. Opt-in logs retain full targets/input; failed Start emits one notice and tool headings keep a blank line.
11
+ - `Awaited memory transactions`: Patches preserve independent shared fields and acceptance order; repeats add no revisions. Setup/cards/evidence accept together under cancelable exclusion/CAS. Preparation/maintenance accept at native `context` or abort inference; runtime-only writes preserve history/provenance. Restore/fork await exact private authority over live shared streams; duplicate/competing Skill writes preserve ownership. Rejected drafts never install; private/selection fences remain.
12
+ - `Awaited response acceptance`: Final answers wait cancelably for current shared memory and publish response plus completed-run lifecycle together. Failed publication installs no draft or shared adoption. Native Abort, Stop, selection/run changes, shutdown and superseding answers withdraw obsolete waits without erasing newer work or write fences. Independent-process Pi tests prove waits beyond two seconds, private isolation, next-provider visibility and retained unfinished runs on cancellation.
13
+ - `Awaited inspection`: Telegram and continuation readers await coherent cohorts without publication or private substitution. Telegram pairs state/revisions, revokes stale receipts and keeps legacy ports; callbacks acknowledge early and escape late errors. Library inspection APIs now return Promises: callers must await results. Six legacy runtime adapters remain. Read-only current-memory recovery preserves private authority without enabling writes. Missing/invalid evidence never initializes memory.
14
+ - `Awaited backup capture`: Backup and canonical mutexes wait cancelably, capture one cohort and preserve replacement owners. Git runs outside canonical exclusion; branch/head drift is rechecked. Settlement awaits completion and shutdown cancels pending reads. Pi 0.87 lacks a settlement Abort signal, so contended backup explicitly defers instead of waiting uninterruptibly. Accepted memory/Stop fences survive; native and independent-writer tests cover ownership, cancellation and coherent commits.
15
+
5
16
  ## 0.18.1: Backup Reliability
6
17
 
7
18
  - `Push lifecycle`: Bounds backup replication to one in-flight push per repository, skips overlap until a later accepted turn, and awaits process closure at session shutdown under the existing push timeout. Suppresses failure reporting after shutdown starts; test fixtures settle pushes before cleanup. Canonical acceptance stays independent of Git replication.
@@ -52,11 +52,19 @@ Enable active State Flow on the current branch:
52
52
 
53
53
  Starting in an existing conversation retains its context for one complete bootstrap run so the agent can compile what matters.
54
54
 
55
- - `/state-flow-start`: Enable active state updates and memory-based context projection.
55
+ - `/state-flow-start`: Enable the active, state-driven iteration workflow and memory-based context projection; passive mode can already save explicit patches.
56
56
  - `/state-flow-status`: Inspect effective state, retained history and known recovery issues without scanning sources or changing state.
57
- - `/state-flow-stop`: End active episode semantics without deleting memory; an interrupted run retains its frozen handoff and available trajectory.
57
+ - `/state-flow-stop`: End active episode semantics without deleting memory, even when canonical persistence fails. Interrupted work and uncompiled conversation remain available across toggles and reload; a failed write stays fenced until explicit Start safely reactivates current memory.
58
58
 
59
- Active mode is **opt-in**. Passive memory projection and the memory tools are enabled by default: existing state can be read or explicitly patched without an active episode or State Flow compaction. Every materially changed owner advances its independent scope revision even in passive mode, without displaying an active-mode status. When active, terminal and Telegram status render the Effective revision vector as `G15/C8/S31`; passive Telegram shows `State Flow: off`. Requested Global, CWD and Session snapshots show their own `#revision`, while Effective shows the vector. Inspection can refresh live shared revisions from other instances without publishing state or advancing a counter. Stop returns to the configured passive behavior. `autoStart` can enable active mode for genuinely new sessions; resumed branches restore their own enablement. See [configuration](docs/usage.md#configuration).
59
+ Active mode is **opt-in**. Passive memory projection and the memory tools are enabled by default: existing state can be read or explicitly patched without an active episode or State Flow compaction. Every materially changed owner advances its independent scope revision even in passive mode, without displaying an active-mode status. When active, terminal and Telegram status render the Effective revision vector as `G15/C8/S31`; passive Telegram shows `State Flow: off`. Requested Global, CWD and Session snapshots show their own `#revision`, while Effective shows the vector. Inspection waits cancelably for a coherent shared view, with matching data and revisions, without publishing state or advancing a counter. Stop returns to the configured passive behavior. `autoStart` can enable active mode for genuinely new sessions; resumed branches restore their own enablement. See [configuration](docs/usage.md#configuration).
60
+
61
+ ### Active, passive, off
62
+
63
+ - **Active:** Memory tools are available; the agent consolidates necessary final state changes before completing an iteration. Subsequent iterations use accepted state and new input rather than completed prior reasoning.
64
+ - **Passive:** Memory tools and existing-state projection remain available by default. Patching is on demand, and ordinary conversation context continues without State Flow's active iteration reset.
65
+ - **Configured off:** With the branch inactive and both `passiveTools` and `passiveBootstrap` false, neither memory tool nor bootstrap/state projection is exposed to the model. `autoStart: false` alone does not turn memory off.
66
+
67
+ Modes select agent behavior, not a different disk persistence or fork-copy mechanism. Final consolidation does not require an empty ceremonial patch, and context projection does not delete native history. See [mode semantics and bootstrap terminology](docs/usage.md#active-passive-and-configured-off).
60
68
 
61
69
  ## State model
62
70
 
@@ -89,7 +97,7 @@ Registered Pi Skills may be compiled into source-addressed artifacts when durabl
89
97
 
90
98
  ## Incremental updates and history
91
99
 
92
- `patch_state` updates one or more named scopes atomically. Unmentioned values remain unchanged; object patches merge recursively, and `null` deletes an object key rather than becoming stored data.
100
+ `patch_state` updates one or more named scopes atomically. It waits cancelably for the store lock, then applies authored Global/CWD patches to current canonical values, preserving other writers' untouched fields. Overlapping assignments follow successful acceptance order; correct repeats succeed without new semantic revisions. Session remains private. Object patches merge recursively, and `null` deletes an object key rather than becoming stored data.
93
101
 
94
102
  ```json
95
103
  {
@@ -124,9 +132,13 @@ Array ranges, structural `keys` reads and path-intersected `patch` projections s
124
132
 
125
133
  The default store is `~/.pi/agent/state-flow/`, independent of registered source files. Canonical `checkpoint.json`, `patches.jsonl` and `meta.json` files hold each scope's state, retained changes and metadata; session configuration and runtime identity are stored separately.
126
134
 
127
- **Git backups are optional.** When the store is a configured Git repository, accepted active turns may create versioned backups of State Flow-owned files. If the attached branch has an explicitly configured remote, State Flow then pushes the exact current backup commit there asynchronously and without force. Within one Pi process, only one push per repository can run at a time; overlapping attempts are skipped and a later accepted turn pushes the latest backup. Session shutdown waits for that repository's active push to close or time out. Commit failures warn locally; repeated push failures produce one concise warning until a successful push, with redacted Git detail kept in the local diagnostic log. Neither failure rejects or rolls back accepted memory. Backup needs a Git commit identity, but accepting and persisting state does not. Git history can be inspected separately, but it is not the authority for `read_state` or automatic restoration of expired semantic boundaries.
135
+ **Persistence is optimistic across abrupt shutdown.** Short publication exclusion preserves independent shared fields between cooperating writers; the last accepted write wins on overlap. Per-file atomic replacement does not guarantee power-loss survival or crash-atomic recovery. That [accepted limitation](docs/filesystem-recovery.md#power-loss-durability) is not a release gate; no additional recovery journal or storage format is planned.
136
+
137
+ **Git backups are optional.** When the store is a configured Git repository, accepted active turns may create versioned backups of State Flow-owned files. A busy store explicitly defers backup when Pi provides no cancellable settlement wait (including SDK 0.87); a later accepted turn retries without changing memory or blocking native Abort. If the attached branch has an explicitly configured remote, State Flow then pushes the exact current backup commit there asynchronously and without force. Within one Pi process, only one push per repository can run at a time; overlapping attempts are skipped and a later accepted turn pushes the latest backup. Session shutdown waits for that repository's active push to close or time out. Commit failures warn locally; repeated push failures produce one concise warning until a successful push, with redacted Git detail kept in the local diagnostic log. Neither failure rejects or rolls back accepted memory. Backup needs a Git commit identity, but accepting and persisting state does not. Git history can be inspected separately, but it is not the authority for `read_state` or automatic restoration of expired semantic boundaries.
138
+
139
+ Resume and tree navigation restore the selected retained session boundary over current shared global/CWD memory. A new session gets its own session layer. Supported native forks copy selected session state into a new owner without changing the parent's private data. Expired, incomplete or contradictory boundaries never silently substitute newer private state during restoration. Explicit Start is a mode change, not historical restoration: it activates validated **current** memory of that same session, including private state, even after expired active/passive, interrupted or pre-runtime selections. Available aligned history and revisions survive; unavailable history is not recreated. Malformed storage, unsafe fork copying and concurrent writes remain fenced. See [fork support](docs/usage.md#fork-support-and-limits) and [storage recovery](docs/usage.md#storage-and-recovery).
128
140
 
129
- Resume and tree navigation restore the selected retained session boundary over current shared global/CWD memory. A new session gets its own session layer. Supported native forks copy selected session state into a new owner without changing the parent's private data. Expired, incomplete or contradictory boundaries fail closed rather than silently substituting newer state. See [fork support](docs/usage.md#fork-support-and-limits) and [storage recovery](docs/usage.md#storage-and-recovery).
141
+ SDK/launcher integrations can use [advisory continuation APIs](docs/architecture.md#session-continuation). Provenance inspection and candidate building are now asynchronous and accept host cancellation; they neither open native sessions nor install automatic resume. Run preparation and missing-artifact maintenance now wait cancelably before inference; embeddings can also use the [transaction APIs](docs/architecture.md#asynchronous-storage-transaction). Accepted-runtime Stop switches local policy/context immediately and awaits runtime-only persistence. Current-head Start also awaits coherent capture/acceptance before enabling mode; Stop or selection can cancel its wait. Telegram acknowledges controls early. Startup/recovery and exact restoration/fork still require migration, including Start's initial-attachment and unaccepted-fork paths.
130
142
 
131
143
  The canonical storage-format boundary introduced in 0.17 still applies: current versions accept only the canonical store contract and provide no in-place predecessor converter. Preserve existing data and check the [format boundary](docs/usage.md#moving-a-store-and-the-017-format-boundary) before changing versions or moving a store.
132
144
 
@@ -13,6 +13,7 @@ export { planKnowledgeRehydration, type RehydrationOptions, type RehydrationPhas
13
13
  export { TemporalRuntime } from "./lib/runtime.ts";
14
14
  export { hasCompiledSkillArtifact, hashSkillSource, SKILL_ARTIFACT_COMPILER, type SuccessfulSkillRead } from "./lib/skills.ts";
15
15
  export { emptyState, isMaterializedState, isStateDocument, overlayStates, projectStateForModel, updateMaterializedArtifacts, type AtomicScopePatches, type MaterializedState, type ScopedPatch, type ScopedStates, type ScopePatch, type SemanticTransition, type StateDocument, type StatePatch, type StateScope, type TerminalTransition } from "./lib/state.ts";
16
- export { buildStateFlowSectionView, createStateFlowTelegramAdapter, formatStateFlowSectionLabel, loadStateFlowTelegramModules, STATE_FLOW_TELEGRAM_ID, type StateFlowTelegramAdapter, type StateFlowTelegramButton, type StateFlowTelegramCallbackContext, type StateFlowTelegramControlResult, type StateFlowTelegramLoader, type StateFlowTelegramModules, type StateFlowTelegramPort, type StateFlowTelegramSectionContext, type StateFlowTelegramSectionModule, type StateFlowTelegramSnapshot, type StateFlowTelegramView } from "./lib/telegram.ts";
16
+ export { PublicationBusyError } from "./lib/storage.ts";
17
+ export { buildStateFlowSectionView, createStateFlowTelegramAdapter, formatStateFlowSectionLabel, loadStateFlowTelegramModules, STATE_FLOW_TELEGRAM_ID, type StateFlowTelegramAdapter, type StateFlowTelegramButton, type StateFlowTelegramCallbackContext, type StateFlowTelegramControlResult, type StateFlowTelegramInspection, type StateFlowTelegramInspectionPort, type StateFlowTelegramLoader, type StateFlowTelegramModules, type StateFlowTelegramPort, type StateFlowTelegramSectionContext, type StateFlowTelegramSectionModule, type StateFlowTelegramSnapshot, type StateFlowTelegramView } from "./lib/telegram.ts";
17
18
  export { advanceTemporalState, readTemporalState, temporalScopeRevisions, type ScopeRevisions, type TemporalState } from "./lib/temporal.ts";
18
19
  export { parseStateReadPath, readStatePath, type StateReadQuery, type StateReadResult } from "./lib/query.ts";
@@ -12,6 +12,7 @@ export { planKnowledgeRehydration } from "./lib/rehydration.js";
12
12
  export { TemporalRuntime } from "./lib/runtime.js";
13
13
  export { hasCompiledSkillArtifact, hashSkillSource, SKILL_ARTIFACT_COMPILER } from "./lib/skills.js";
14
14
  export { emptyState, isMaterializedState, isStateDocument, overlayStates, projectStateForModel, updateMaterializedArtifacts } from "./lib/state.js";
15
+ export { PublicationBusyError } from "./lib/storage.js";
15
16
  export { buildStateFlowSectionView, createStateFlowTelegramAdapter, formatStateFlowSectionLabel, loadStateFlowTelegramModules, STATE_FLOW_TELEGRAM_ID } from "./lib/telegram.js";
16
17
  export { advanceTemporalState, readTemporalState, temporalScopeRevisions } from "./lib/temporal.js";
17
18
  export { parseStateReadPath, readStatePath } from "./lib/query.js";
@@ -100,7 +100,7 @@ export declare function hashArtifactSource(source: string | Uint8Array): string;
100
100
  export declare function isArtifactHash(value: unknown): value is string;
101
101
  export declare function validateArtifactMetadata(value: unknown, path?: string): asserts value is ArtifactMetadata;
102
102
  export declare function isArtifactMetadata(value: unknown): value is ArtifactMetadata;
103
- export declare function validateArtifactRegistry(value: unknown): asserts value is ArtifactRegistry;
103
+ export declare function validateArtifactRegistry(value: unknown, context?: string): asserts value is ArtifactRegistry;
104
104
  export declare function selectArtifactsByTags(registry: ArtifactRegistry, tags: readonly string[], match?: "all" | "any"): string[];
105
105
  export declare function isArtifactRegistry(value: unknown): value is ArtifactRegistry;
106
106
  /** Parse one scope `meta.json` provenance registry; missing input means no recorded evidence. */
@@ -118,7 +118,7 @@ export declare function pruneArtifactProvenance(registry: Readonly<ArtifactProve
118
118
  /** Validate and apply a whole compilation/removal cohort of model-visible artifacts. */
119
119
  export declare function updateArtifactRegistry(registry: ArtifactRegistry, updates: readonly ArtifactCompilationUpdate[], removed?: readonly string[]): ArtifactRegistry;
120
120
  /** Validate authored fields only: legacy retained evidence stays readable but cannot be model-edited. */
121
- export declare function validateModelArtifactPatch(patch: JsonObject): void;
121
+ export declare function validateModelArtifactPatch(patch: JsonObject, context?: string): void;
122
122
  /** Strip retained runtime bookkeeping from one model-visible artifact entry. */
123
123
  export declare function projectArtifactForModel(entry: unknown): unknown;
124
124
  export type ArtifactModelHints = Readonly<Record<string, string | readonly string[]>>;
@@ -99,13 +99,13 @@ export function isArtifactMetadata(value) {
99
99
  return false;
100
100
  }
101
101
  }
102
- export function validateArtifactRegistry(value) {
102
+ export function validateArtifactRegistry(value, context = "artifacts") {
103
103
  if (!isObject(value))
104
104
  throw new Error("Artifacts must be a path-keyed JSON object");
105
105
  for (const [path, metadata] of Object.entries(value)) {
106
106
  if (path.trim().length === 0)
107
107
  throw new Error("Artifact path keys must be non-empty");
108
- validateArtifactMetadata(metadata, path);
108
+ validateArtifactMetadata(metadata, `${context}[${JSON.stringify(path)}]`);
109
109
  }
110
110
  }
111
111
  export function selectArtifactsByTags(registry, tags, match = "all") {
@@ -261,7 +261,7 @@ export function compileArtifact(update) {
261
261
  // Timestamps are runtime evidence; the model-visible entry never retains them.
262
262
  const semantic = structuredClone(update.output);
263
263
  delete semantic.compiled_at;
264
- validateArtifactMetadata(semantic, update.source.path);
264
+ validateArtifactMetadata(semantic, `${update.source.scope ? `${update.source.scope}.` : ""}artifacts[${JSON.stringify(update.source.path)}]`);
265
265
  return {
266
266
  semantic,
267
267
  provenance: {
@@ -325,13 +325,13 @@ export function updateArtifactRegistry(registry, updates, removed = []) {
325
325
  /** Runtime-owned artifact fields that never belong in ordinary model context. */
326
326
  const RUNTIME_ARTIFACT_FIELDS = [...MODEL_FORBIDDEN_PROVENANCE_FIELDS, "compiled_at", "hint"];
327
327
  /** Validate authored fields only: legacy retained evidence stays readable but cannot be model-edited. */
328
- export function validateModelArtifactPatch(patch) {
328
+ export function validateModelArtifactPatch(patch, context = "artifacts") {
329
329
  for (const [path, entry] of Object.entries(patch)) {
330
330
  if (!isObject(entry))
331
331
  continue; // Whole-artifact deletion and materialized shape belong to the transition owner.
332
332
  const field = RUNTIME_ARTIFACT_FIELDS.find((field) => Object.hasOwn(entry, field));
333
333
  if (field !== undefined)
334
- throw new Error(`Artifact patch at ${path} cannot set runtime-owned field ${field}`);
334
+ throw new Error(`Artifact patch at ${context}[${JSON.stringify(path)}] cannot set runtime-owned field ${field}`);
335
335
  }
336
336
  }
337
337
  /** Strip retained runtime bookkeeping from one model-visible artifact entry. */
@@ -13,14 +13,15 @@ export declare function lazyNavigationHint(state: MaterializedState): {
13
13
  path: string;
14
14
  keys?: Record<string, LazyValueKind>;
15
15
  };
16
- /** Bounded context retained after semantic State Flow is stopped in this physical session. */
16
+ /** Context retained after semantic State Flow is stopped in this physical session. */
17
17
  export interface PassiveContinuation {
18
18
  startedAt: number;
19
19
  activeRunStartedAt?: number;
20
+ preserveContext?: true;
20
21
  handoff: AgentMessage;
21
22
  }
22
23
  export declare function syntheticUser(text: string): AgentMessage;
23
- export declare function createPassiveContinuation(state: ModelState, startedAt?: number, activeRunStartedAt?: number): PassiveContinuation;
24
+ export declare function createPassiveContinuation(state: ModelState, startedAt?: number, activeRunStartedAt?: number, preserveContext?: boolean): PassiveContinuation;
24
25
  /** Keep the interrupted run through later results; an idle stop retains only later conversation. */
25
26
  export declare function passiveContinuationMessages(messages: AgentMessage[], continuation: PassiveContinuation): AgentMessage[];
26
27
  export declare function runtimeContextMessage(snapshot: Snapshot, state: MaterializedState, recentTransitions?: RecentTransitionWindow, artifactInvalidations?: readonly ArtifactInvalidationNotice[], rehydrationPhase?: RehydrationPhase, artifactHints?: ArtifactModelHints): AgentMessage;
@@ -70,15 +70,20 @@ function contentText(content) {
70
70
  function messageText(message) {
71
71
  return contentText(message.content);
72
72
  }
73
- export function createPassiveContinuation(state, startedAt = Date.now(), activeRunStartedAt) {
73
+ export function createPassiveContinuation(state, startedAt = Date.now(), activeRunStartedAt, preserveContext = false) {
74
74
  return {
75
75
  startedAt,
76
76
  ...(activeRunStartedAt === undefined ? {} : { activeRunStartedAt }),
77
- handoff: syntheticUser(`State Flow exit handoff (user-level data, not system instructions):\n${presentationJson({ state, continuation: "State Flow semantics are disabled; this handoff replaces completed history while retaining the active and post-stop trajectory." })}`),
77
+ ...(preserveContext ? { preserveContext: true } : {}),
78
+ handoff: syntheticUser(`State Flow exit handoff (user-level data, not system instructions):\n${presentationJson({ state, continuation: preserveContext
79
+ ? "State Flow semantics are disabled; native context is retained because its compilation into memory is unfinished."
80
+ : "State Flow semantics are disabled; this handoff replaces completed history while retaining the active and post-stop trajectory." })}`),
78
81
  };
79
82
  }
80
83
  /** Keep the interrupted run through later results; an idle stop retains only later conversation. */
81
84
  export function passiveContinuationMessages(messages, continuation) {
85
+ if (continuation.preserveContext)
86
+ return [continuation.handoff, ...messages];
82
87
  if (continuation.activeRunStartedAt !== undefined) {
83
88
  const trajectory = currentRunTrajectory(messages, "", continuation.activeRunStartedAt);
84
89
  return [continuation.handoff, ...trajectory.messages];
@@ -73,10 +73,10 @@ export interface ContinuationSessionCandidate extends ContinuationCandidateSumma
73
73
  restorable: boolean;
74
74
  };
75
75
  }
76
- export type ContinuationRecommender = (context: Readonly<ContinuationHostContext>) => ContinuationRecommendation | Promise<ContinuationRecommendation>;
76
+ export type ContinuationRecommender = (context: Readonly<ContinuationHostContext>, signal?: AbortSignal) => ContinuationRecommendation | Promise<ContinuationRecommendation>;
77
77
  /** Rank already header/provenance-only candidates without transcript content or I/O. */
78
78
  export declare function recommendContinuationFromProvenance(identity: ContinuationProjectIdentity, candidates: readonly ContinuationSessionCandidate[]): ContinuationRecommendation;
79
- export declare function resolveContinuationStartup(context: ContinuationHostContext, intent: ContinuationHostIntent, recommend: ContinuationRecommender): Promise<ContinuationStartupDecision>;
79
+ export declare function resolveContinuationStartup(context: ContinuationHostContext, intent: ContinuationHostIntent, recommend: ContinuationRecommender, signal?: AbortSignal): Promise<ContinuationStartupDecision>;
80
80
  export interface NativeSessionHeader {
81
81
  file: string;
82
82
  id: string;
@@ -106,6 +106,6 @@ export declare function discoverNativeSessionHeaders(sessionDir: string): {
106
106
  error: string;
107
107
  }>;
108
108
  };
109
- /** Inspect only exact current runtime provenance; never initialize, migrate, lock, checkout, or publish. */
110
- export declare function inspectStateFlowContinuationProvenance(header: NativeSessionHeader, repositoryRoot: string): Pick<ContinuationCandidateProvenance, "stateFlow" | "reason">;
111
- export declare function buildContinuationCandidates(headers: readonly NativeSessionHeader[], inspect: (header: Readonly<NativeSessionHeader>) => ContinuationCandidateProvenance | undefined): ContinuationSessionCandidate[];
109
+ /** Await one exact canonical cohort; never read transcript bodies, initialize, or publish. */
110
+ export declare function inspectStateFlowContinuationProvenance(header: Readonly<NativeSessionHeader>, repositoryRoot: string, signal?: AbortSignal): Promise<Pick<ContinuationCandidateProvenance, "stateFlow" | "reason">>;
111
+ export declare function buildContinuationCandidates(headers: readonly NativeSessionHeader[], inspect: (header: Readonly<NativeSessionHeader>, signal?: AbortSignal) => ContinuationCandidateProvenance | undefined | Promise<ContinuationCandidateProvenance | undefined>, signal?: AbortSignal): Promise<ContinuationSessionCandidate[]>;
@@ -1,8 +1,10 @@
1
- import { closeSync, constants, existsSync, fstatSync, lstatSync, openSync, readFileSync, readSync, readdirSync, realpathSync, statSync } from "node:fs";
1
+ import { closeSync, constants, fstatSync, lstatSync, openSync, readSync, readdirSync, realpathSync, statSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
- import { loadScopeStream, resolveSessionAddress, sessionRuntimePaths } from "./durable.js";
3
+ import { parseScopeStream, resolveSessionAddress, sessionRuntimePaths, temporalScopePaths } from "./durable.js";
4
4
  import { MAX_HISTORY_LIMIT } from "./history.js";
5
+ import { diagnosticText } from "./protocol.js";
5
6
  import { parseSessionRuntime } from "./snapshot.js";
7
+ import { withStorageTransaction } from "./storage.js";
6
8
  import { validateScopeLineage } from "./temporal.js";
7
9
  /**
8
10
  * Preserve host-owned explicit session intent and consult State Flow only for
@@ -48,7 +50,8 @@ export function recommendContinuationFromProvenance(identity, candidates) {
48
50
  return { action: "new", reason: "ineligible" };
49
51
  return { action: "resume", sessionFile: candidate.sessionFile, sessionId: candidate.sessionId, reason: "latest-enabled-state-flow" };
50
52
  }
51
- export async function resolveContinuationStartup(context, intent, recommend) {
53
+ export async function resolveContinuationStartup(context, intent, recommend, signal) {
54
+ signal?.throwIfAborted();
52
55
  switch (intent.kind) {
53
56
  case "new":
54
57
  return { action: "new", reason: "explicit-new" };
@@ -60,8 +63,11 @@ export async function resolveContinuationStartup(context, intent, recommend) {
60
63
  return { action: "native", mode: "continue-recent" };
61
64
  case "no-session":
62
65
  return { action: "native", mode: "no-session" };
63
- case "default":
64
- return recommend(Object.freeze({ ...context }));
66
+ case "default": {
67
+ const recommendation = await recommend(Object.freeze({ ...context }), signal);
68
+ signal?.throwIfAborted();
69
+ return recommendation;
70
+ }
65
71
  }
66
72
  }
67
73
  const MAX_SESSION_HEADER_BYTES = 64 * 1024;
@@ -124,54 +130,62 @@ export function discoverNativeSessionHeaders(sessionDir) {
124
130
  }
125
131
  return { headers, invalid };
126
132
  }
127
- function readRegular(path) {
128
- if (!existsSync(path))
129
- return undefined;
130
- if (!lstatSync(path).isFile())
131
- throw new Error("State Flow continuation provenance must be a regular file");
132
- return readFileSync(path, "utf8");
133
- }
134
- /** Inspect only exact current runtime provenance; never initialize, migrate, lock, checkout, or publish. */
135
- export function inspectStateFlowContinuationProvenance(header, repositoryRoot) {
136
- const sessionKey = resolveSessionAddress(header.file, header.id, header.timestamp).key;
137
- const paths = sessionRuntimePaths(header.cwd, header.id, repositoryRoot, sessionKey);
133
+ /** Await one exact canonical cohort; never read transcript bodies, initialize, or publish. */
134
+ export async function inspectStateFlowContinuationProvenance(header, repositoryRoot, signal) {
135
+ signal?.throwIfAborted();
136
+ const selected = { ...header };
137
+ const root = resolve(repositoryRoot);
138
+ const sessionKey = resolveSessionAddress(selected.file, selected.id, selected.timestamp).key;
139
+ const paths = sessionRuntimePaths(selected.cwd, selected.id, root, sessionKey);
140
+ const absent = { stateFlow: { enabled: false, restorable: true }, reason: "no State Flow session runtime" };
138
141
  try {
139
- const config = readRegular(paths.config);
140
- const runtimeSource = readRegular(paths.runtime);
141
- const meta = readRegular(paths.meta);
142
- if (config === undefined && runtimeSource === undefined && meta === undefined) {
143
- return { stateFlow: { enabled: false, restorable: true }, reason: "no State Flow session runtime" };
144
- }
145
- const runtime = parseSessionRuntime(config, runtimeSource, header.cwd, header.id);
146
- if (!runtime)
147
- return { stateFlow: { enabled: false, restorable: true }, reason: "no State Flow session runtime" };
148
- if (!runtime.config.enabled)
149
- return { stateFlow: { enabled: false, restorable: true }, reason: "State Flow stopped on selected runtime" };
150
- const global = loadScopeStream(header.cwd, header.id, "global", repositoryRoot, sessionKey);
151
- const cwd = loadScopeStream(header.cwd, header.id, "cwd", repositoryRoot, sessionKey);
152
- const session = loadScopeStream(header.cwd, header.id, "session", repositoryRoot, sessionKey);
153
- if (!global || !cwd || !session)
154
- throw new Error("incomplete canonical temporal cohort");
155
- // Shared streams are current, independently validated by their codecs, not frozen to this session's history.
156
- validateScopeLineage(session, "session", runtime.meta.lineage, MAX_HISTORY_LIMIT);
157
- return { stateFlow: { enabled: true, restorable: true }, reason: "canonical session lineage is valid beside current shared streams" };
142
+ if (!lstatSync(root, { throwIfNoEntry: false }))
143
+ return absent;
144
+ const result = await withStorageTransaction(root, (tx) => {
145
+ const files = new Map(tx.capture(selected.cwd, selected.id, root, sessionKey).files.map((file) => [file.path, file.content]));
146
+ const privatePaths = temporalScopePaths(selected.cwd, selected.id, "session", root, sessionKey);
147
+ if ([paths.config, paths.runtime, privatePaths.meta, privatePaths.checkpoint, privatePaths.patches].every((path) => files.get(path) === undefined))
148
+ return absent;
149
+ const runtime = parseSessionRuntime(files.get(paths.config), files.get(paths.runtime), selected.cwd, selected.id);
150
+ if (!runtime)
151
+ throw new Error("incomplete canonical session runtime");
152
+ if (!runtime.config.enabled)
153
+ return { stateFlow: { enabled: false, restorable: true }, reason: "State Flow stopped on selected runtime" };
154
+ const streams = ["global", "cwd", "session"].map((scope) => {
155
+ const owned = temporalScopePaths(selected.cwd, selected.id, scope, root, sessionKey);
156
+ return parseScopeStream(files.get(owned.checkpoint), files.get(owned.patches), scope, scope === "cwd" ? selected.cwd : undefined, files.get(owned.meta));
157
+ });
158
+ if (streams.some((stream) => stream === undefined))
159
+ throw new Error("incomplete canonical temporal cohort");
160
+ // Shared streams remain current and independently valid; only the private stream binds to this lineage.
161
+ validateScopeLineage(streams[2], "session", runtime.meta.lineage, MAX_HISTORY_LIMIT);
162
+ return { stateFlow: { enabled: true, restorable: true }, reason: "canonical session lineage is valid beside current shared streams" };
163
+ }, signal);
164
+ signal?.throwIfAborted();
165
+ return result;
158
166
  }
159
167
  catch (error) {
160
- return { stateFlow: { enabled: true, restorable: false }, reason: `State Flow runtime is ineligible: ${error instanceof Error ? error.message : String(error)}` };
168
+ signal?.throwIfAborted();
169
+ return { stateFlow: { enabled: true, restorable: false }, reason: `State Flow runtime is ineligible: ${diagnosticText(error)}` };
161
170
  }
162
171
  }
163
- export function buildContinuationCandidates(headers, inspect) {
172
+ export async function buildContinuationCandidates(headers, inspect, signal) {
173
+ signal?.throwIfAborted();
174
+ const selected = headers.map((header) => Object.freeze({ ...header }));
164
175
  const candidates = [];
165
- for (const header of headers) {
166
- const provenance = inspect(Object.freeze({ ...header }));
176
+ for (const header of selected) {
177
+ signal?.throwIfAborted();
178
+ const provenance = await inspect(header, signal);
179
+ signal?.throwIfAborted();
167
180
  if (!provenance)
168
181
  continue;
169
182
  candidates.push({
183
+ ...provenance,
184
+ stateFlow: { ...provenance.stateFlow },
170
185
  sessionFile: header.file,
171
186
  sessionId: header.id,
172
187
  lastActivity: header.lastActivity,
173
188
  cwd: header.cwd,
174
- ...provenance,
175
189
  });
176
190
  }
177
191
  return candidates;
@@ -22,7 +22,7 @@ export function hasCwdMaterialization(cwd, repositoryRoot) {
22
22
  if (checkpoint.content !== undefined)
23
23
  return parseScopeStream(checkpoint.content, patches.content, "cwd", cwd, meta.content) !== undefined;
24
24
  if (patches.content !== undefined)
25
- throw new Error(`State Flow tail has no provable checkpoint: ${directory}`);
25
+ throw new Error(`State Flow tail has no provable checkpoint: ${JSON.stringify(directory)}`);
26
26
  return false;
27
27
  }
28
28
  /** Semantic files contain no runtime envelope; temporal boundaries and CWD ownership live in meta.json. */
@@ -124,7 +124,7 @@ export function sessionRuntimePaths(cwd, sessionId, repositoryRoot, sessionKey =
124
124
  export function loadScopeStream(cwd, sessionId, scope, repositoryRoot, sessionKey = sessionId) {
125
125
  const paths = temporalScopePaths(cwd, sessionId, scope, repositoryRoot, sessionKey);
126
126
  if (readRegularBytes(join(paths.directory, STATE_FILE), repositoryRoot) !== undefined) {
127
- throw new Error(`Unsupported State Flow storage format: ${paths.directory}`);
127
+ throw new Error(`Unsupported State Flow storage format: ${JSON.stringify(paths.directory)}`);
128
128
  }
129
129
  return parseScopeStream(readRegularFile(paths.checkpoint, repositoryRoot), readRegularFile(paths.patches, repositoryRoot), scope, scope === "cwd" ? cwd : undefined, readRegularFile(paths.meta, repositoryRoot));
130
130
  }
@@ -147,7 +147,7 @@ export function temporalStateFileUpdates(cwd, sessionId, view, scopes, repositor
147
147
  seen.add(scope);
148
148
  const paths = temporalScopePaths(cwd, sessionId, scope, repositoryRoot, sessionKey);
149
149
  if (readRegularBytes(join(paths.directory, STATE_FILE), repositoryRoot) !== undefined) {
150
- throw new Error(`Unsupported State Flow storage format: ${paths.directory}`);
150
+ throw new Error(`Unsupported State Flow storage format: ${JSON.stringify(paths.directory)}`);
151
151
  }
152
152
  const sources = serializeScopeStream(view.scopes[scope], scope, scope === "cwd" ? cwd : undefined);
153
153
  return [{ path: paths.checkpoint, content: sources.checkpoint }, { path: paths.patches, content: sources.patches }];
@@ -190,14 +190,14 @@ function parseMetadataDocument(source, label) {
190
190
  }
191
191
  /** Missing provenance is unavailable evidence, never corrupt state. Unknown metadata is preserved by writers. */
192
192
  export function parseScopeProvenance(source, path) {
193
- const value = parseMetadataDocument(source, `State Flow provenance file: ${path}`);
193
+ const value = parseMetadataDocument(source, `State Flow provenance file: ${JSON.stringify(path)}`);
194
194
  if (Object.keys(value).length === 0)
195
195
  return {};
196
196
  if (value.version !== 1 && value.version !== 2)
197
- throw new Error(`Invalid State Flow provenance document: ${path}`);
197
+ throw new Error(`Invalid State Flow provenance document: ${JSON.stringify(path)}`);
198
198
  if (!Object.hasOwn(value, "artifacts"))
199
199
  return {};
200
- return parseArtifactProvenanceRegistry(value.artifacts, `State Flow provenance at ${path}`);
200
+ return parseArtifactProvenanceRegistry(value.artifacts, `State Flow provenance at ${JSON.stringify(path)}`);
201
201
  }
202
202
  /** Dedicated runtime storage, independent from Markdown source discovery. */
203
203
  export function getDurableRepositoryRoot(agentDir = getAgentDir()) {
@@ -273,7 +273,7 @@ function missing(error) {
273
273
  function assertWithinRepository(path, repositoryRoot) {
274
274
  const child = relative(repositoryRoot, path);
275
275
  if (child === "" || child === ".." || child.startsWith(`..${sep}`)) {
276
- throw new Error(`Durable State Flow path escapes its repository: ${path}`);
276
+ throw new Error(`Durable State Flow path escapes its repository: ${JSON.stringify(path)}`);
277
277
  }
278
278
  }
279
279
  /** Reject symlinked directory components instead of following them during reads or writes. */
@@ -295,7 +295,7 @@ function assertDirectoryChain(repositoryRoot, directory, create) {
295
295
  try {
296
296
  const metadata = lstatSync(current);
297
297
  if (metadata.isSymbolicLink() || !metadata.isDirectory()) {
298
- throw new Error(`Durable State Flow directory is not a regular directory: ${current}`);
298
+ throw new Error(`Durable State Flow directory is not a regular directory: ${JSON.stringify(current)}`);
299
299
  }
300
300
  }
301
301
  catch (error) {
@@ -315,7 +315,7 @@ function readRegularBytes(path, repositoryRoot) {
315
315
  try {
316
316
  descriptor = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
317
317
  if (!fstatSync(descriptor).isFile()) {
318
- throw new Error(`Durable State Flow path is not a regular file: ${path}`);
318
+ throw new Error(`Durable State Flow path is not a regular file: ${JSON.stringify(path)}`);
319
319
  }
320
320
  return readFileSync(descriptor);
321
321
  }
@@ -325,7 +325,7 @@ function readRegularBytes(path, repositoryRoot) {
325
325
  if (error instanceof Error
326
326
  && "code" in error
327
327
  && error.code === "ELOOP") {
328
- throw new Error(`Durable State Flow path is not a regular file: ${path}`);
328
+ throw new Error(`Durable State Flow path is not a regular file: ${JSON.stringify(path)}`);
329
329
  }
330
330
  throw error;
331
331
  }
@@ -348,7 +348,7 @@ function fileBase(path, repositoryRoot) {
348
348
  }
349
349
  function assertCurrentBytes(path, root, expected) {
350
350
  if (byteIdentity(readRegularBytes(path, root)) !== byteIdentity(expected)) {
351
- throw new Error(`State Flow file conflict at ${path}; preserve concurrent bytes and reconcile before retrying`);
351
+ throw new Error(`State Flow file conflict at ${JSON.stringify(path)}; concurrent bytes preserved`);
352
352
  }
353
353
  }
354
354
  /** Capture exact owned bytes for one compare-and-swap publication cohort. */
@@ -356,7 +356,7 @@ export function captureOwnedFileBases(paths, repositoryRoot) {
356
356
  const root = resolve(repositoryRoot);
357
357
  return paths.map((path) => {
358
358
  if (!isStateFlowOwnedPath(path, root))
359
- throw new Error(`Cannot capture a non-State Flow path: ${path}`);
359
+ throw new Error(`Cannot capture a non-State Flow path: ${JSON.stringify(path)}`);
360
360
  return fileBase(resolve(path), root);
361
361
  });
362
362
  }
@@ -365,7 +365,7 @@ function prepareFile(path, repositoryRoot, content, expected) {
365
365
  assertDirectoryChain(repositoryRoot, dirname(path), true);
366
366
  const original = readRegularBytes(path, repositoryRoot);
367
367
  if (expected !== undefined && byteIdentity(original) !== expected.identity) {
368
- throw new Error(`State Flow file conflict during preparation: ${path}`);
368
+ throw new Error(`State Flow file conflict during preparation: ${JSON.stringify(path)}`);
369
369
  }
370
370
  const temporary = join(dirname(path), `.${basename(path)}.${process.pid}.${randomUUID()}.tmp`);
371
371
  const next = content === undefined ? undefined : Buffer.from(content);
@@ -425,7 +425,7 @@ export function assertOwnedFileUpdates(updates, repositoryRoot) {
425
425
  const root = resolve(repositoryRoot);
426
426
  for (const update of updates) {
427
427
  if (!isStateFlowOwnedPath(update.path, root))
428
- throw new Error(`Cannot inspect a non-State Flow path: ${update.path}`);
428
+ throw new Error(`Cannot inspect a non-State Flow path: ${JSON.stringify(update.path)}`);
429
429
  assertCurrentBytes(update.path, root, update.content === undefined ? undefined : Buffer.from(update.content));
430
430
  }
431
431
  }
@@ -439,13 +439,13 @@ export function writeOwnedFileUpdates(updates, bases, repositoryRoot) {
439
439
  for (const update of updates) {
440
440
  const path = resolve(update.path);
441
441
  if (!isStateFlowOwnedPath(path, root))
442
- throw new Error(`Cannot update a non-State Flow path: ${path}`);
442
+ throw new Error(`Cannot update a non-State Flow path: ${JSON.stringify(path)}`);
443
443
  if (seen.has(path))
444
- throw new Error(`Duplicate State Flow file update: ${path}`);
444
+ throw new Error(`Duplicate State Flow file update: ${JSON.stringify(path)}`);
445
445
  seen.add(path);
446
446
  const base = byPath.get(path);
447
447
  if (base === undefined)
448
- throw new Error(`State Flow file update has no captured base: ${path}`);
448
+ throw new Error(`State Flow file update has no captured base: ${JSON.stringify(path)}`);
449
449
  prepared.push(prepareFile(path, root, update.content, base));
450
450
  }
451
451
  }
@@ -464,13 +464,13 @@ export function restoreDurableFileBases(bases, repositoryRoot, expectedCurrent)
464
464
  for (const base of bases) {
465
465
  assertWithinRepository(base.path, root);
466
466
  if (!isStateFlowOwnedPath(base.path, root)) {
467
- throw new Error(`Cannot restore a non-State Flow path: ${base.path}`);
467
+ throw new Error(`Cannot restore a non-State Flow path: ${JSON.stringify(base.path)}`);
468
468
  }
469
469
  }
470
470
  for (const base of bases) {
471
471
  const update = expected.get(resolve(base.path));
472
472
  if (update === undefined)
473
- throw new Error(`Rollback has no published basis: ${base.path}`);
473
+ throw new Error(`Rollback has no published basis: ${JSON.stringify(base.path)}`);
474
474
  assertCurrentBytes(base.path, root, update.content === undefined ? undefined : Buffer.from(update.content));
475
475
  if (base.identity === "missing") {
476
476
  rmSync(base.path, { force: true });
@@ -478,7 +478,7 @@ export function restoreDurableFileBases(bases, repositoryRoot, expectedCurrent)
478
478
  }
479
479
  const original = base.bytes ?? (base.content === undefined ? undefined : Buffer.from(base.content));
480
480
  if (original === undefined)
481
- throw new Error(`Durable State Flow base content is missing: ${base.path}`);
481
+ throw new Error(`Durable State Flow base content is missing: ${JSON.stringify(base.path)}`);
482
482
  assertDirectoryChain(root, dirname(base.path), true);
483
483
  const temporary = join(dirname(base.path), `.${basename(base.path)}.${process.pid}.${randomUUID()}.restore`);
484
484
  try {