@llblab/pi-kit 0.18.2 → 0.19.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 (113) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +3 -1
  3. package/node_modules/@llblab/pi-state-flow/AGENTS.md +35 -37
  4. package/node_modules/@llblab/pi-state-flow/BACKLOG.md +5 -3
  5. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +19 -1
  6. package/node_modules/@llblab/pi-state-flow/README.md +102 -48
  7. package/node_modules/@llblab/pi-state-flow/dist/index.d.ts +6 -9
  8. package/node_modules/@llblab/pi-state-flow/dist/index.js +6 -9
  9. package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.d.ts +2 -2
  10. package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.js +21 -8
  11. package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.d.ts +40 -24
  12. package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.js +93 -63
  13. package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.d.ts +4 -3
  14. package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.js +9 -9
  15. package/node_modules/@llblab/pi-state-flow/dist/lib/config.d.ts +1 -1
  16. package/node_modules/@llblab/pi-state-flow/dist/lib/config.js +6 -5
  17. package/node_modules/@llblab/pi-state-flow/dist/lib/context.d.ts +8 -7
  18. package/node_modules/@llblab/pi-state-flow/dist/lib/context.js +56 -58
  19. package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.js +12 -24
  20. package/node_modules/@llblab/pi-state-flow/dist/lib/durable.d.ts +7 -18
  21. package/node_modules/@llblab/pi-state-flow/dist/lib/durable.js +47 -78
  22. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.d.ts +4 -6
  23. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +241 -437
  24. package/node_modules/@llblab/pi-state-flow/dist/lib/git.d.ts +2 -72
  25. package/node_modules/@llblab/pi-state-flow/dist/lib/git.js +120 -499
  26. package/node_modules/@llblab/pi-state-flow/dist/lib/history.d.ts +2 -1
  27. package/node_modules/@llblab/pi-state-flow/dist/lib/history.js +4 -3
  28. package/node_modules/@llblab/pi-state-flow/dist/lib/json.d.ts +3 -0
  29. package/node_modules/@llblab/pi-state-flow/dist/lib/json.js +43 -22
  30. package/node_modules/@llblab/pi-state-flow/dist/lib/logging.d.ts +1 -5
  31. package/node_modules/@llblab/pi-state-flow/dist/lib/logging.js +0 -2
  32. package/node_modules/@llblab/pi-state-flow/dist/lib/memory.d.ts +1 -14
  33. package/node_modules/@llblab/pi-state-flow/dist/lib/memory.js +5 -37
  34. package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.d.ts +1 -2
  35. package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +13 -32
  36. package/node_modules/@llblab/pi-state-flow/dist/lib/query.d.ts +6 -6
  37. package/node_modules/@llblab/pi-state-flow/dist/lib/query.js +25 -21
  38. package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.d.ts +3 -3
  39. package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.js +15 -12
  40. package/node_modules/@llblab/pi-state-flow/dist/lib/rehydration.d.ts +2 -4
  41. package/node_modules/@llblab/pi-state-flow/dist/lib/rehydration.js +5 -1
  42. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.d.ts +26 -88
  43. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.js +159 -255
  44. package/node_modules/@llblab/pi-state-flow/dist/lib/skills.d.ts +0 -3
  45. package/node_modules/@llblab/pi-state-flow/dist/lib/skills.js +1 -47
  46. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.d.ts +16 -33
  47. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.js +48 -137
  48. package/node_modules/@llblab/pi-state-flow/dist/lib/state.d.ts +16 -11
  49. package/node_modules/@llblab/pi-state-flow/dist/lib/state.js +13 -14
  50. package/node_modules/@llblab/pi-state-flow/dist/lib/status.d.ts +1 -9
  51. package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +15 -43
  52. package/node_modules/@llblab/pi-state-flow/dist/lib/storage.d.ts +5 -9
  53. package/node_modules/@llblab/pi-state-flow/dist/lib/storage.js +13 -45
  54. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +1 -1
  55. package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.d.ts +15 -7
  56. package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.js +104 -24
  57. package/node_modules/@llblab/pi-state-flow/dist/lib/transition.d.ts +0 -2
  58. package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +11 -14
  59. package/node_modules/@llblab/pi-state-flow/dist/package.json +9 -6
  60. package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-guide/SKILL.md +11 -17
  61. package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-memory/SKILL.md +8 -8
  62. package/node_modules/@llblab/pi-state-flow/docs/README.md +2 -2
  63. package/node_modules/@llblab/pi-state-flow/docs/architecture.md +64 -67
  64. package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +24 -6
  65. package/node_modules/@llblab/pi-state-flow/docs/filesystem-recovery.md +12 -15
  66. package/node_modules/@llblab/pi-state-flow/docs/fork-contract.md +22 -27
  67. package/node_modules/@llblab/pi-state-flow/docs/lazy-state.md +25 -41
  68. package/node_modules/@llblab/pi-state-flow/docs/performance.md +38 -421
  69. package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +33 -46
  70. package/node_modules/@llblab/pi-state-flow/docs/usage.md +37 -61
  71. package/node_modules/@llblab/pi-state-flow/index.ts +7 -71
  72. package/node_modules/@llblab/pi-state-flow/lib/acquisition.ts +26 -11
  73. package/node_modules/@llblab/pi-state-flow/lib/artifact.ts +116 -88
  74. package/node_modules/@llblab/pi-state-flow/lib/compaction.ts +12 -10
  75. package/node_modules/@llblab/pi-state-flow/lib/config.ts +6 -6
  76. package/node_modules/@llblab/pi-state-flow/lib/context.ts +52 -59
  77. package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +11 -20
  78. package/node_modules/@llblab/pi-state-flow/lib/durable.ts +44 -86
  79. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +237 -460
  80. package/node_modules/@llblab/pi-state-flow/lib/git.ts +110 -552
  81. package/node_modules/@llblab/pi-state-flow/lib/history.ts +4 -3
  82. package/node_modules/@llblab/pi-state-flow/lib/json.ts +39 -23
  83. package/node_modules/@llblab/pi-state-flow/lib/logging.ts +1 -7
  84. package/node_modules/@llblab/pi-state-flow/lib/memory.ts +5 -44
  85. package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +14 -25
  86. package/node_modules/@llblab/pi-state-flow/lib/query.ts +26 -22
  87. package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +17 -11
  88. package/node_modules/@llblab/pi-state-flow/lib/rehydration.ts +7 -5
  89. package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +154 -254
  90. package/node_modules/@llblab/pi-state-flow/lib/skills.ts +1 -49
  91. package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +58 -142
  92. package/node_modules/@llblab/pi-state-flow/lib/state.ts +27 -19
  93. package/node_modules/@llblab/pi-state-flow/lib/status.ts +16 -54
  94. package/node_modules/@llblab/pi-state-flow/lib/storage.ts +12 -40
  95. package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +1 -1
  96. package/node_modules/@llblab/pi-state-flow/lib/temporal.ts +104 -22
  97. package/node_modules/@llblab/pi-state-flow/lib/transition.ts +16 -26
  98. package/node_modules/@llblab/pi-state-flow/package.json +9 -6
  99. package/node_modules/@llblab/pi-state-flow/skills/state-flow-guide/SKILL.md +11 -17
  100. package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +8 -8
  101. package/package.json +2 -2
  102. package/node_modules/@llblab/pi-state-flow/dist/lib/discovery.d.ts +0 -21
  103. package/node_modules/@llblab/pi-state-flow/dist/lib/discovery.js +0 -125
  104. package/node_modules/@llblab/pi-state-flow/dist/lib/maintenance.d.ts +0 -36
  105. package/node_modules/@llblab/pi-state-flow/dist/lib/maintenance.js +0 -98
  106. package/node_modules/@llblab/pi-state-flow/dist/lib/migration.d.ts +0 -13
  107. package/node_modules/@llblab/pi-state-flow/dist/lib/migration.js +0 -167
  108. package/node_modules/@llblab/pi-state-flow/dist/lib/publication.d.ts +0 -86
  109. package/node_modules/@llblab/pi-state-flow/dist/lib/publication.js +0 -437
  110. package/node_modules/@llblab/pi-state-flow/lib/discovery.ts +0 -133
  111. package/node_modules/@llblab/pi-state-flow/lib/maintenance.ts +0 -147
  112. package/node_modules/@llblab/pi-state-flow/lib/migration.ts +0 -171
  113. package/node_modules/@llblab/pi-state-flow/lib/publication.ts +0 -458
@@ -1,5 +1,5 @@
1
- import { projectRecentTransitionsWithLimit, RECENT_TRANSITION_LIMIT } from "./history.js";
2
- import { inspectMemoryPromotions, retainedMemoryScopes } from "./memory.js";
1
+ import { projectRecentTransitionsWithLimit } from "./history.js";
2
+ import { retainedMemoryScopes } from "./memory.js";
3
3
  import { overlayStates } from "./state.js";
4
4
  export const STATUS_KEY = "state-flow";
5
5
  export function compactStatus(snapshot, colorize) {
@@ -10,68 +10,40 @@ export function compactStatus(snapshot, colorize) {
10
10
  function countArtifacts(states, scope) {
11
11
  return Object.keys(states[scope].artifacts).length;
12
12
  }
13
- function abbreviatedCommit(commit) {
14
- return commit.length > 12 ? commit.slice(0, 12) : commit;
15
- }
16
13
  export function detailedStatus(snapshot, diagnostics) {
17
- const projectedRecent = projectRecentTransitionsWithLimit(RECENT_TRANSITION_LIMIT, diagnostics.recent);
14
+ const projectedRecent = projectRecentTransitionsWithLimit(diagnostics.historyLimit, diagnostics.recent);
18
15
  const available = diagnostics.temporal !== undefined && diagnostics.durableStateError === undefined;
19
16
  const materialized = !available ? undefined : overlayStates(diagnostics.scopeStates.global, diagnostics.scopeStates.cwd, diagnostics.scopeStates.session);
20
17
  const stateJson = materialized === undefined ? undefined : JSON.stringify(materialized, null, 2);
21
- const freshnessError = diagnostics.artifactFreshnessError ?? (available ? undefined : "temporal global artifact registry is unavailable");
22
- const stale = freshnessError === undefined
23
- ? String(diagnostics.staleArtifacts.length)
24
- : "unknown";
25
- const publication = diagnostics.pendingPublication === undefined
26
- ? "idle"
27
- : `pending ${abbreviatedCommit(diagnostics.pendingPublication.commit)} — ${diagnostics.pendingPublication.error}`;
28
- const staleLines = freshnessError !== undefined
29
- ? [`Artifact freshness unavailable: ${freshnessError}`]
30
- : diagnostics.staleArtifacts.length === 0
31
- ? ["Stale artifacts: none"]
32
- : [
33
- "Stale artifacts:",
34
- ...diagnostics.staleArtifacts.map(({ scope, path, reason }) => `- [${scope}] ${path} — ${reason}`),
35
- ];
18
+ const invalidated = available ? String(diagnostics.staleArtifacts.length) : "unavailable";
19
+ const invalidationLines = diagnostics.staleArtifacts.length === 0
20
+ ? ["Pending artifact invalidations: none"]
21
+ : [
22
+ "Pending artifact invalidations:",
23
+ ...diagnostics.staleArtifacts.map(({ scope, path, reason }) => `- [${scope}] ${path} — ${reason}`),
24
+ ];
36
25
  const temporal = available ? diagnostics.temporal : undefined;
37
26
  const temporalLines = temporal === undefined
38
27
  ? [`Temporal materialization unavailable: ${diagnostics.durableStateError ?? "no selected branch runtime"}`,
39
- "Hot history: unavailable; offsets beyond 7 require explicit cold Git inspection",
28
+ `Hot history: unavailable; configured maximum depth ${diagnostics.historyLimit}`,
40
29
  "Retained patch tails: unavailable"]
41
30
  : [`Temporal head: ${JSON.stringify(temporal.head.id)}; branch-local position ${temporal.head.position}`,
42
- `Hot history: offsets 0..${temporal.historyDepth}; maximum depth 7`,
31
+ `Hot history: offsets 0..${temporal.historyDepth}; maximum depth ${diagnostics.historyLimit}`,
43
32
  `Retained patch tails: global ${temporal.tailCounts.global}; CWD ${temporal.tailCounts.cwd}; session ${temporal.tailCounts.session}`];
44
33
  const artifacts = (scope) => available ? countArtifacts(diagnostics.scopeStates, scope) : "unknown";
45
- const promotions = available ? inspectMemoryPromotions(diagnostics.scopeStates.global) : [];
46
- const promotionCounts = Object.fromEntries(["pending", "accepted", "failed", "unknown", "invalid"].map((status) => [status, promotions.filter((entry) => entry.status === status).length]));
47
34
  const memoryScopes = available ? retainedMemoryScopes(diagnostics.scopeStates) : undefined;
48
- const oneLine = (value) => value.replace(/\s+/g, " ").slice(0, 240);
49
- const promotionLines = !available || promotions.length === 0 ? [] : [
50
- "Memory promotions:",
51
- ...promotions.map((entry) => `- ${oneLine(entry.id)} — ${entry.status}; owner ${oneLine(entry.owner ?? "unavailable")}${entry.pointer ? `; pointer ${oneLine(entry.pointer)}` : ""}${entry.revision ? `; revision ${oneLine(entry.revision)}` : ""}${entry.error ? `; error ${oneLine(entry.error)}` : ""}`),
52
- ];
53
35
  return [
54
36
  `State Flow diagnostics — config.enabled=${snapshot.config.enabled}; branch mode=${snapshot.config.enabled ? "active" : "inactive"}`,
55
37
  `Repository: ${diagnostics.repositoryRoot}`,
56
38
  `Scope keys: CWD ${diagnostics.cwdScopeKey}; session ${diagnostics.sessionScopeKey}`,
57
39
  "Session files: config.json owns behavior; runtime.json owns branch recovery; meta.json owns scope provenance",
58
- `Runtime metadata: step #${snapshot.meta.step}; active revision ${snapshot.meta.durableBase ?? "none"}; bootstrap ${snapshot.meta.bootstrap === true}`,
59
- `Remote publication policy: ${snapshot.meta.remotePublication?.mode ?? "legacy-transition"}`,
60
- diagnostics.publicationQueueError !== undefined
61
- ? `Remote queue: unavailable; error ${diagnostics.publicationQueueError}`
62
- : diagnostics.publicationQueue === undefined
63
- ? "Remote queue: idle"
64
- : `Remote queue: ${diagnostics.publicationQueue.status}; target ${abbreviatedCommit(diagnostics.publicationQueue.target)}; confirmed ${diagnostics.publicationQueue.confirmed ? abbreviatedCommit(diagnostics.publicationQueue.confirmed) : "none"}; attempt ${diagnostics.publicationQueue.attempt}${diagnostics.publicationQueue.error ? `; error ${diagnostics.publicationQueue.error}` : ""}`,
40
+ `Runtime metadata: step #${snapshot.meta.step}; bootstrap ${snapshot.meta.bootstrap === true}`,
65
41
  "Memory: owner state-flow; global retention enabled; global fallback active",
66
42
  `Memory-bearing scopes: global ${memoryScopes?.global ?? "unknown"}; CWD ${memoryScopes?.cwd ?? "unknown"}; session ${memoryScopes?.session ?? "unknown"}`,
67
- `Promotion status: pending ${promotionCounts.pending}; accepted ${promotionCounts.accepted}; failed ${promotionCounts.failed}; unknown ${promotionCounts.unknown}; invalid ${promotionCounts.invalid}`,
68
- ...promotionLines,
69
43
  ...temporalLines,
70
- `Artifacts: global ${artifacts("global")}; CWD ${artifacts("cwd")}; session ${artifacts("session")}; stale ${stale}`,
44
+ `Artifacts: global ${artifacts("global")}; CWD ${artifacts("cwd")}; session ${artifacts("session")}; pending invalidations ${invalidated}`,
71
45
  available ? `Recent transitions: global ${diagnostics.recent.filter(({ transitions }) => transitions.some(({ scope }) => scope === "global")).length}; CWD ${diagnostics.recent.filter(({ transitions }) => transitions.some(({ scope }) => scope === "cwd")).length}; session ${diagnostics.recent.filter(({ transitions }) => transitions.some(({ scope }) => scope === "session")).length}; active ${projectedRecent.length}` : "Recent transitions: unavailable",
72
- `Publication policy: ${snapshot.meta.remotePublication?.mode ?? "legacy-unresolved"}`,
73
- `Publication: ${publication}`,
74
- ...staleLines,
46
+ ...invalidationLines,
75
47
  ...(stateJson === undefined
76
48
  ? ["Effective memory: unavailable"]
77
49
  : [`Effective memory (${Buffer.byteLength(stateJson, "utf8")} JSON bytes; global → CWD → session overlay):`, "", stateJson]),
@@ -1,5 +1,5 @@
1
1
  import { type DurableFileBase, type OwnedFileUpdate } from "./durable.ts";
2
- import { type ArtifactProvenanceRegistry } from "./artifact.ts";
2
+ import type { ArtifactProvenanceRegistry } from "./artifact.ts";
3
3
  import { type FileRevision, type SessionRuntime } from "./snapshot.ts";
4
4
  export { isFileRevision, type FileRevision } from "./snapshot.ts";
5
5
  import type { StateScope } from "./state.ts";
@@ -7,17 +7,15 @@ import { type TemporalState } from "./temporal.ts";
7
7
  export interface TemporalFileBase {
8
8
  files: DurableFileBase[];
9
9
  }
10
- /** Probe once at a lifecycle boundary, never on cached state reads. Only spawn ENOENT is absence. */
11
- export declare function detectGitCapability(): "git" | "files";
12
10
  export declare function assertStorageDirectory(path: string): void;
13
11
  /** Explicit creation only; existing bytes and unrelated files are never adopted or rewritten here. */
14
12
  export declare function initializeFileStore(root: string): void;
15
13
  /** Wait only for a cooperating live owner; interrupted or malformed locks remain explicit recovery errors. */
16
14
  export declare function acquirePublicationLock(path: string, unavailable: (cause: unknown) => Error): number;
17
- /** Git writers also acquire this lock before their common-Git-directory lock. */
15
+ /** Canonical writers and bounded backup capture share exclusion; no Git work runs under this lock. */
18
16
  export declare function withStoragePublicationLock<T>(repositoryRoot: string, action: (root: string) => T): T;
19
17
  export declare function assertTemporalFileBase(expected: TemporalFileBase, current: TemporalFileBase): void;
20
- /** One shared publication plan for Git and files; neither backend invents semantic changes. */
18
+ /** Plan exact canonical updates; lifecycle-only writes exclude semantic files and provenance. */
21
19
  export declare function planTemporalPublication(cwd: string, sessionId: string, view: TemporalState, scopes: readonly StateScope[], current: TemporalFileBase, root: string, runtime?: SessionRuntime, runtimeOnly?: boolean, sessionKey?: string, provenance?: Readonly<Record<StateScope, ArtifactProvenanceRegistry>>): {
22
20
  updates: OwnedFileUpdate[];
23
21
  changedScopes: StateScope[];
@@ -38,11 +36,9 @@ export declare function loadTemporalFileRevision(cwd: string, sessionId: string,
38
36
  };
39
37
  revision: `file:${string}`;
40
38
  };
41
- /** Publish a validated full runtime/scoped cohort; no Git commands, success receipts, or pending pushes. */
42
- export declare function publishTemporalStateToFiles(cwd: string, sessionId: string, view: TemporalState, scopes: readonly StateScope[], base: TemporalFileBase, root: string, runtime: SessionRuntime, sessionKey?: string, provenance?: Readonly<Record<StateScope, ArtifactProvenanceRegistry>>): {
39
+ /** Publish a validated canonical cohort; runtimeOnly owns only session config/runtime files. */
40
+ export declare function publishTemporalStateToFiles(cwd: string, sessionId: string, view: TemporalState, scopes: readonly StateScope[], base: TemporalFileBase, root: string, runtime: SessionRuntime, sessionKey?: string, provenance?: Readonly<Record<StateScope, ArtifactProvenanceRegistry>>, runtimeOnly?: boolean): {
43
41
  base: TemporalFileBase;
44
42
  revision: FileRevision;
45
43
  changed: boolean;
46
44
  };
47
- /** In-store format conversion only; no Git history or cross-repository import. */
48
- export declare function migrateLegacyStorageToFiles(cwd: string, sessionId: string, root: string, sessionKey?: string): void;
@@ -1,27 +1,15 @@
1
1
  // Domain: exact file-cohort publication, current-only recovery, and cooperating worktree exclusion.
2
2
  // Excludes: temporal algebra, Pi lifecycle, Git objects/remotes, and backend fallback policy.
3
- import { spawnSync } from "node:child_process";
4
3
  import { createHash } from "node:crypto";
5
4
  import { closeSync, lstatSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync } from "node:fs";
6
5
  import { dirname, relative, resolve } from "node:path";
7
6
  import { assertOwnedFileUpdates, captureTemporalFileBases, parseScopeProvenance, parseScopeStream, restoreDurableFileBases, serializeScopeMetadata, sessionRuntimePaths, temporalScopePaths, temporalStateFileUpdates, writeOwnedFileUpdates, } from "./durable.js";
8
- import { parseArtifactProvenanceRegistry } from "./artifact.js";
7
+ import { MAX_HISTORY_LIMIT } from "./history.js";
9
8
  import { hashJson, sameJson } from "./json.js";
10
9
  import { RevisionUnavailableError, isFileRevision, parseSessionRuntime, serializeSessionRuntime } from "./snapshot.js";
11
- import { planLegacyStorageMigration } from "./migration.js";
12
10
  export { isFileRevision } from "./snapshot.js";
13
11
  import { validateTemporalState } from "./temporal.js";
14
12
  const SCOPES = ["global", "cwd", "session"];
15
- /** Probe once at a lifecycle boundary, never on cached state reads. Only spawn ENOENT is absence. */
16
- export function detectGitCapability() {
17
- const result = spawnSync("git", ["--version"], { encoding: "utf8", timeout: 15_000 });
18
- if (result.error && result.error.code === "ENOENT")
19
- return "files";
20
- if (result.error || result.status !== 0) {
21
- throw new RevisionUnavailableError(`Cannot resolve Git capability: ${result.error?.message ?? result.stderr ?? `exit ${result.status}`}`);
22
- }
23
- return "git";
24
- }
25
13
  export function assertStorageDirectory(path) {
26
14
  const root = resolve(path);
27
15
  const parent = dirname(root);
@@ -76,7 +64,7 @@ export function acquirePublicationLock(path, unavailable) {
76
64
  }
77
65
  }
78
66
  }
79
- /** Git writers also acquire this lock before their common-Git-directory lock. */
67
+ /** Canonical writers and bounded backup capture share exclusion; no Git work runs under this lock. */
80
68
  export function withStoragePublicationLock(repositoryRoot, action) {
81
69
  const root = resolve(repositoryRoot);
82
70
  assertStorageDirectory(root);
@@ -98,17 +86,15 @@ export function assertTemporalFileBase(expected, current) {
98
86
  }))
99
87
  throw new Error("Temporal State Flow base or scope identity changed concurrently");
100
88
  }
101
- /** One shared publication plan for Git and files; neither backend invents semantic changes. */
89
+ /** Plan exact canonical updates; lifecycle-only writes exclude semantic files and provenance. */
102
90
  export function planTemporalPublication(cwd, sessionId, view, scopes, current, root, runtime, runtimeOnly = false, sessionKey = sessionId, provenance) {
103
91
  const candidates = temporalStateFileUpdates(cwd, sessionId, view, scopes, root, sessionKey);
104
92
  const files = new Map(current.files.map((file) => [file.path, file]));
105
- if (runtimeOnly && scopes.length !== 0)
106
- throw new Error("Runtime-only publication cannot write semantic scopes");
93
+ if (runtimeOnly && (scopes.length !== 0 || provenance !== undefined))
94
+ throw new Error("Runtime-only publication cannot write semantic scopes or artifact provenance");
107
95
  const changedScopes = [];
108
96
  for (const scope of SCOPES) {
109
97
  const paths = temporalScopePaths(cwd, sessionId, scope, root, sessionKey);
110
- if (files.get(resolve(paths.directory, "state.json")).identity !== "missing")
111
- throw new Error("Legacy State Flow storage requires explicit migration");
112
98
  const previous = parseScopeStream(files.get(paths.checkpoint).content, files.get(paths.patches).content, scope, scope === "cwd" ? cwd : undefined, files.get(paths.meta).content);
113
99
  if (runtimeOnly || (previous !== undefined && sameJson(previous, view.scopes[scope])))
114
100
  continue;
@@ -132,7 +118,7 @@ export function planTemporalPublication(cwd, sessionId, view, scopes, current, r
132
118
  }
133
119
  }
134
120
  const runtimePaths = sessionRuntimePaths(cwd, sessionId, root, sessionKey);
135
- const previousRuntime = parseSessionRuntime(files.get(runtimePaths.config).content, files.get(runtimePaths.runtime).content, cwd, sessionId, files.get(runtimePaths.meta).content);
121
+ const previousRuntime = parseSessionRuntime(files.get(runtimePaths.config).content, files.get(runtimePaths.runtime).content, cwd, sessionId);
136
122
  if (previousRuntime !== undefined && changedScopes.length > 0 && runtime === undefined)
137
123
  throw new Error("Temporal semantic publication requires its session runtime cohort");
138
124
  const runtimeUpdates = [];
@@ -143,12 +129,6 @@ export function planTemporalPublication(cwd, sessionId, view, scopes, current, r
143
129
  if (files.get(runtimePaths.config).content !== sources.config || files.get(runtimePaths.runtime).content !== sources.runtime) {
144
130
  runtimeUpdates.push({ path: runtimePaths.config, content: sources.config }, { path: runtimePaths.runtime, content: sources.runtime });
145
131
  }
146
- if (files.get(runtimePaths.runtime).identity === "missing" && files.get(runtimePaths.meta).content !== undefined
147
- && previousRuntime !== undefined && !provenanceUpdates.some(({ path }) => path === runtimePaths.meta)) {
148
- const registry = provenance?.session ?? parseArtifactProvenanceRegistry(previousRuntime.meta.artifacts, "State Flow session artifact provenance");
149
- const content = serializeScopeMetadata(registry, view.scopes.session, "session", undefined, files.get(runtimePaths.meta).content);
150
- runtimeUpdates.push({ path: runtimePaths.meta, content });
151
- }
152
132
  }
153
133
  const changedPaths = new Set(changedScopes.flatMap((scope) => {
154
134
  const paths = temporalScopePaths(cwd, sessionId, scope, root, sessionKey);
@@ -177,19 +157,17 @@ function decodeFileCohort(cwd, sessionId, root, base, sessionKey = sessionId) {
177
157
  const scopes = {};
178
158
  for (const scope of SCOPES) {
179
159
  const paths = temporalScopePaths(cwd, sessionId, scope, root, sessionKey);
180
- if (files.get(resolve(paths.directory, "state.json")) !== undefined)
181
- throw new Error("Legacy State Flow storage requires explicit migration");
182
160
  const stream = parseScopeStream(files.get(paths.checkpoint), files.get(paths.patches), scope, scope === "cwd" ? cwd : undefined, files.get(paths.meta));
183
161
  if (!stream)
184
162
  throw new Error("Incomplete file-only temporal scope cohort");
185
163
  scopes[scope] = stream;
186
164
  }
187
165
  const paths = sessionRuntimePaths(cwd, sessionId, root, sessionKey);
188
- const runtime = parseSessionRuntime(files.get(paths.config), files.get(paths.runtime), cwd, sessionId, files.get(paths.meta));
189
- if (!runtime || runtime.meta.publication !== "files")
190
- throw new Error("File-only recovery requires file publication provenance, not a Git self reference");
166
+ const runtime = parseSessionRuntime(files.get(paths.config), files.get(paths.runtime), cwd, sessionId);
167
+ if (!runtime)
168
+ throw new Error("Canonical recovery requires a complete session runtime");
191
169
  const view = { scopes, lineage: runtime.meta.lineage };
192
- validateTemporalState(view);
170
+ validateTemporalState(view, MAX_HISTORY_LIMIT);
193
171
  const provenance = {
194
172
  global: parseScopeProvenance(files.get(temporalScopePaths(cwd, sessionId, "global", root, sessionKey).meta), temporalScopePaths(cwd, sessionId, "global", root, sessionKey).meta),
195
173
  cwd: parseScopeProvenance(files.get(temporalScopePaths(cwd, sessionId, "cwd", root, sessionKey).meta), temporalScopePaths(cwd, sessionId, "cwd", root, sessionKey).meta),
@@ -211,14 +189,12 @@ export function loadTemporalFileRevision(cwd, sessionId, root, revision, session
211
189
  return { base, revision, ...decodeFileCohort(cwd, sessionId, locked, base, sessionKey) };
212
190
  });
213
191
  }
214
- /** Publish a validated full runtime/scoped cohort; no Git commands, success receipts, or pending pushes. */
215
- export function publishTemporalStateToFiles(cwd, sessionId, view, scopes, base, root, runtime, sessionKey = sessionId, provenance) {
192
+ /** Publish a validated canonical cohort; runtimeOnly owns only session config/runtime files. */
193
+ export function publishTemporalStateToFiles(cwd, sessionId, view, scopes, base, root, runtime, sessionKey = sessionId, provenance, runtimeOnly = false) {
216
194
  return withStoragePublicationLock(root, (locked) => {
217
- if (runtime.meta.publication !== "files")
218
- throw new Error("File publication requires explicit file provenance");
219
195
  const current = { files: captureTemporalFileBases(cwd, sessionId, locked, sessionKey) };
220
196
  assertTemporalFileBase(base, current);
221
- const { updates } = planTemporalPublication(cwd, sessionId, view, scopes, current, locked, runtime, false, sessionKey, provenance);
197
+ const { updates } = planTemporalPublication(cwd, sessionId, view, scopes, current, locked, runtime, runtimeOnly, sessionKey, provenance);
222
198
  const next = { files: temporalFileReceipts(current, updates) };
223
199
  decodeFileCohort(cwd, sessionId, locked, next, sessionKey);
224
200
  const revision = fileRevision(next, locked);
@@ -242,11 +218,3 @@ function publishFileUpdates(bases, updates, root) {
242
218
  throw error;
243
219
  }
244
220
  }
245
- /** In-store format conversion only; no Git history or cross-repository import. */
246
- export function migrateLegacyStorageToFiles(cwd, sessionId, root, sessionKey = sessionId) {
247
- withStoragePublicationLock(root, (locked) => {
248
- const plan = planLegacyStorageMigration(cwd, sessionId, locked, undefined, sessionKey);
249
- if (plan.updates.length)
250
- publishFileUpdates(plan.bases, plan.updates, locked);
251
- });
252
- }
@@ -94,7 +94,7 @@ function renderStateFlowTelegramField(value) {
94
94
  return rendered;
95
95
  }
96
96
  export function renderStateFlowRichState(scope, step, state) {
97
- const fields = ["artifacts", "contract", "working", "intents", "response", "lazy"];
97
+ const fields = ["intents", "contract", "working", "artifacts", "response", "lazy"];
98
98
  return {
99
99
  blocks: [
100
100
  {
@@ -25,15 +25,23 @@ export interface TemporalState {
25
25
  scopes: Record<StateScope, ScopeStream>;
26
26
  }
27
27
  /** Replay validation is shared by disk codecs and active-lineage materialization. */
28
- export declare function validateScopeStream(value: unknown, scope: StateScope): asserts value is ScopeStream;
29
- export declare function validateTemporalLineage(value: unknown): asserts value is TransitionBoundary[];
28
+ export declare function validateScopeStream(value: unknown, scope: StateScope, historyLimit?: number): asserts value is ScopeStream;
29
+ export declare function validateTemporalLineage(value: unknown, historyLimit?: number): asserts value is TransitionBoundary[];
30
+ /** Bind one owned stream to its runtime lineage without requiring patches from unrelated scopes. */
31
+ export declare function validateScopeLineage(stream: ScopeStream, scope: StateScope, lineage: readonly TransitionBoundary[], historyLimit?: number): void;
30
32
  /** Validate one revision-selected cohort. Its older ancestry must be bound by the durable loader. */
31
- export declare function validateTemporalState(view: TemporalState): void;
33
+ export declare function validateTemporalState(view: TemporalState, historyLimit?: number): void;
32
34
  /** Adopt revision-proven inherited streams without rewriting their checkpoints or tails. */
33
- export declare function adoptTemporalStreams(scopes: Record<StateScope, ScopeStream>, id: string): TemporalState;
35
+ export declare function adoptTemporalStreams(scopes: Record<StateScope, ScopeStream>, id: string, historyLimit?: number): TemporalState;
34
36
  /** New or migrated state starts at a proven current boundary, with no invented past. */
35
- export declare function createTemporalState(states: ScopedStates, id: string): TemporalState;
37
+ export declare function createTemporalState(states: ScopedStates, id: string, historyLimit?: number): TemporalState;
38
+ /** Fold retained tails to a lower configured limit without inventing history. */
39
+ export declare function constrainTemporalState(view: TemporalState, historyLimit: number): TemporalState;
40
+ /** Select one scope at a proven retained boundary from its owning runtime lineage. */
41
+ export declare function selectScopeStreamAtBoundary(stream: ScopeStream, scope: StateScope, boundary: TransitionBoundary, historyLimit?: number): ScopeStream;
42
+ /** Select one still-retained causal boundary without consulting an external history store. */
43
+ export declare function selectTemporalStateBoundary(view: TemporalState, boundaryId: string, historyLimit?: number): TemporalState;
36
44
  /** Lazy scope/effective read at one shared transition boundary, never by local patch count. */
37
- export declare function readTemporalState(view: TemporalState, offset?: number, scope?: StateScope): MaterializedState;
45
+ export declare function readTemporalState(view: TemporalState, offset?: number, scope?: StateScope, historyLimit?: number): MaterializedState;
38
46
  /** Allocate the identity outside this algebra; only materially effective patches accept it. */
39
- export declare function advanceTemporalState(view: TemporalState, transitions: readonly RecentScopePatch[], id: string): TemporalState;
47
+ export declare function advanceTemporalState(view: TemporalState, transitions: readonly RecentScopePatch[], id: string, historyLimit?: number): TemporalState;
@@ -1,7 +1,12 @@
1
- import { RECENT_TRANSITION_LIMIT, validateRecentTransition } from "./history.js";
1
+ import { DEFAULT_HISTORY_LIMIT, MAX_HISTORY_LIMIT, validateRecentTransition } from "./history.js";
2
2
  import { applyPatch, containsNull, isJsonValue, isObject, sameJson } from "./json.js";
3
3
  import { isMaterializedState, overlayStates } from "./state.js";
4
4
  const SCOPES = ["global", "cwd", "session"];
5
+ function validateHistoryLimit(limit) {
6
+ if (!Number.isSafeInteger(limit) || limit < 0 || limit > MAX_HISTORY_LIMIT) {
7
+ throw new Error(`State Flow history limit must be an integer from 0 to ${MAX_HISTORY_LIMIT}`);
8
+ }
9
+ }
5
10
  function validateBoundary(boundary) {
6
11
  if (!isObject(boundary) || Object.keys(boundary).sort().join(",") !== "id,parent,position"
7
12
  || typeof boundary.id !== "string" || boundary.id.trim().length === 0
@@ -25,7 +30,8 @@ function sameBoundary(left, right) {
25
30
  return left.id === right.id && left.position === right.position && left.parent === right.parent;
26
31
  }
27
32
  /** Replay validation is shared by disk codecs and active-lineage materialization. */
28
- export function validateScopeStream(value, scope) {
33
+ export function validateScopeStream(value, scope, historyLimit = DEFAULT_HISTORY_LIMIT) {
34
+ validateHistoryLimit(historyLimit);
29
35
  if (!SCOPES.includes(scope))
30
36
  throw new Error("Unknown temporal scope");
31
37
  if (!isJsonValue(value) || !isObject(value) || Object.keys(value).sort().join(",") !== "checkpoint,patches"
@@ -36,8 +42,8 @@ export function validateScopeStream(value, scope) {
36
42
  const stream = value;
37
43
  validateBoundary(stream.checkpoint.through);
38
44
  validateState(stream.checkpoint.state);
39
- if (stream.patches.length > RECENT_TRANSITION_LIMIT)
40
- throw new Error("Temporal scope tail exceeds seven patches");
45
+ if (stream.patches.length > historyLimit)
46
+ throw new Error(`Temporal scope tail exceeds configured history limit ${historyLimit}`);
41
47
  let previous = stream.checkpoint.through;
42
48
  let state = stream.checkpoint.state;
43
49
  const identities = new Set([previous.id]);
@@ -61,9 +67,10 @@ export function validateScopeStream(value, scope) {
61
67
  identities.add(previous.id);
62
68
  }
63
69
  }
64
- export function validateTemporalLineage(value) {
65
- if (!isJsonValue(value) || !Array.isArray(value) || value.length === 0 || value.length > RECENT_TRANSITION_LIMIT + 1) {
66
- throw new Error("Temporal lineage must contain between one and eight boundaries");
70
+ export function validateTemporalLineage(value, historyLimit = DEFAULT_HISTORY_LIMIT) {
71
+ validateHistoryLimit(historyLimit);
72
+ if (!isJsonValue(value) || !Array.isArray(value) || value.length === 0 || value.length > historyLimit + 1) {
73
+ throw new Error(`Temporal lineage must contain between one and ${historyLimit + 1} boundaries`);
67
74
  }
68
75
  const seen = new Set();
69
76
  for (let index = 0; index < value.length; index++) {
@@ -78,9 +85,29 @@ export function validateTemporalLineage(value) {
78
85
  }
79
86
  }
80
87
  }
88
+ /** Bind one owned stream to its runtime lineage without requiring patches from unrelated scopes. */
89
+ export function validateScopeLineage(stream, scope, lineage, historyLimit = DEFAULT_HISTORY_LIMIT) {
90
+ validateTemporalLineage(lineage, historyLimit);
91
+ validateScopeStream(stream, scope, historyLimit);
92
+ const oldest = lineage[0];
93
+ const head = lineage.at(-1);
94
+ if (stream.checkpoint.through.position > oldest.position)
95
+ throw new Error("Scope checkpoint is newer than the guaranteed hot boundary");
96
+ const identities = new Map(lineage.map((boundary) => [boundary.id, boundary]));
97
+ for (const boundary of [stream.checkpoint.through, ...stream.patches.map((record) => record.transition)]) {
98
+ if (boundary.position > head.position)
99
+ throw new Error("Temporal scope patch is beyond the active head");
100
+ const identity = identities.get(boundary.id);
101
+ const position = lineage[boundary.position - oldest.position];
102
+ if ((identity && !sameBoundary(identity, boundary)) || (position && !sameBoundary(position, boundary))) {
103
+ throw new Error("Conflicting State Flow temporal lineage");
104
+ }
105
+ }
106
+ }
81
107
  /** Validate one revision-selected cohort. Its older ancestry must be bound by the durable loader. */
82
- export function validateTemporalState(view) {
83
- validateTemporalLineage(view.lineage);
108
+ export function validateTemporalState(view, historyLimit = DEFAULT_HISTORY_LIMIT) {
109
+ validateHistoryLimit(historyLimit);
110
+ validateTemporalLineage(view.lineage, historyLimit);
84
111
  const oldest = view.lineage[0];
85
112
  const identities = new Map();
86
113
  const positions = new Map();
@@ -100,7 +127,7 @@ export function validateTemporalState(view) {
100
127
  const head = view.lineage.at(-1);
101
128
  for (const scope of SCOPES) {
102
129
  const stream = view.scopes[scope];
103
- validateScopeStream(stream, scope);
130
+ validateScopeStream(stream, scope, historyLimit);
104
131
  remember(stream.checkpoint.through);
105
132
  if (stream.checkpoint.through.position > oldest.position) {
106
133
  throw new Error("Scope checkpoint is newer than the guaranteed hot boundary");
@@ -123,22 +150,21 @@ export function validateTemporalState(view) {
123
150
  }
124
151
  }
125
152
  /** Adopt revision-proven inherited streams without rewriting their checkpoints or tails. */
126
- export function adoptTemporalStreams(scopes, id) {
153
+ export function adoptTemporalStreams(scopes, id, historyLimit = DEFAULT_HISTORY_LIMIT) {
127
154
  const boundaries = Object.values(scopes).flatMap((stream) => [stream.checkpoint.through, ...stream.patches.map((record) => record.transition)]);
128
155
  const origin = { id, position: Math.max(...boundaries.map((boundary) => boundary.position)) + 1, parent: null };
129
156
  const view = { lineage: [origin], scopes: structuredClone(scopes) };
130
- validateTemporalState(view);
131
- return view;
157
+ return constrainTemporalState(view, historyLimit);
132
158
  }
133
159
  /** New or migrated state starts at a proven current boundary, with no invented past. */
134
- export function createTemporalState(states, id) {
160
+ export function createTemporalState(states, id, historyLimit = DEFAULT_HISTORY_LIMIT) {
135
161
  const through = { id, position: 0, parent: null };
136
162
  const stream = (scope) => ({
137
163
  checkpoint: { through: structuredClone(through), state: structuredClone(states[scope]) },
138
164
  patches: [],
139
165
  });
140
166
  const view = { lineage: [through], scopes: { global: stream("global"), cwd: stream("cwd"), session: stream("session") } };
141
- validateTemporalState(view);
167
+ validateTemporalState(view, historyLimit);
142
168
  return view;
143
169
  }
144
170
  function scopeAt(stream, boundary) {
@@ -150,14 +176,62 @@ function scopeAt(stream, boundary) {
150
176
  }
151
177
  return state;
152
178
  }
179
+ /** Fold retained tails to a lower configured limit without inventing history. */
180
+ export function constrainTemporalState(view, historyLimit) {
181
+ validateHistoryLimit(historyLimit);
182
+ validateTemporalState(view, MAX_HISTORY_LIMIT);
183
+ const next = structuredClone(view);
184
+ for (const scope of SCOPES) {
185
+ const stream = next.scopes[scope];
186
+ while (stream.patches.length > historyLimit) {
187
+ const folded = stream.patches.shift();
188
+ stream.checkpoint = { through: folded.transition, state: apply(stream.checkpoint.state, folded.patch) };
189
+ }
190
+ }
191
+ next.lineage = next.lineage.slice(-(historyLimit + 1));
192
+ validateTemporalState(next, historyLimit);
193
+ return next;
194
+ }
195
+ /** Select one scope at a proven retained boundary from its owning runtime lineage. */
196
+ export function selectScopeStreamAtBoundary(stream, scope, boundary, historyLimit = DEFAULT_HISTORY_LIMIT) {
197
+ validateHistoryLimit(historyLimit);
198
+ validateBoundary(boundary);
199
+ validateScopeStream(stream, scope, historyLimit);
200
+ if (boundary.position < stream.checkpoint.through.position) {
201
+ throw new Error("Selected State Flow history boundary predates the retained scope checkpoint");
202
+ }
203
+ const selected = structuredClone(stream);
204
+ selected.patches = selected.patches.filter(({ transition }) => transition.position <= boundary.position);
205
+ validateScopeStream(selected, scope, historyLimit);
206
+ return selected;
207
+ }
208
+ /** Select one still-retained causal boundary without consulting an external history store. */
209
+ export function selectTemporalStateBoundary(view, boundaryId, historyLimit = DEFAULT_HISTORY_LIMIT) {
210
+ validateHistoryLimit(historyLimit);
211
+ validateTemporalState(view, historyLimit);
212
+ if (typeof boundaryId !== "string" || boundaryId.trim().length === 0)
213
+ throw new Error("State Flow temporal boundary identity must be non-empty");
214
+ const index = view.lineage.findIndex(({ id }) => id === boundaryId);
215
+ if (index < 0)
216
+ throw new Error("Selected State Flow history boundary is outside the retained temporal window");
217
+ const target = view.lineage[index];
218
+ const selected = structuredClone(view);
219
+ selected.lineage = selected.lineage.slice(0, index + 1);
220
+ for (const scope of SCOPES) {
221
+ selected.scopes[scope].patches = selected.scopes[scope].patches.filter(({ transition }) => transition.position <= target.position);
222
+ }
223
+ validateTemporalState(selected, historyLimit);
224
+ return selected;
225
+ }
153
226
  /** Lazy scope/effective read at one shared transition boundary, never by local patch count. */
154
- export function readTemporalState(view, offset = 0, scope) {
155
- if (!Number.isSafeInteger(offset) || offset < 0 || offset > RECENT_TRANSITION_LIMIT) {
156
- throw new Error("State Flow hot-history offset must be an integer from 0 to 7");
227
+ export function readTemporalState(view, offset = 0, scope, historyLimit = DEFAULT_HISTORY_LIMIT) {
228
+ validateHistoryLimit(historyLimit);
229
+ if (!Number.isSafeInteger(offset) || offset < 0 || offset > historyLimit) {
230
+ throw new Error(`State Flow hot-history offset must be an integer from 0 to ${historyLimit}`);
157
231
  }
158
232
  if (scope !== undefined && !SCOPES.includes(scope))
159
233
  throw new Error("Unknown temporal scope");
160
- validateTemporalState(view);
234
+ validateTemporalState(view, historyLimit);
161
235
  const boundary = view.lineage[view.lineage.length - 1 - offset];
162
236
  if (!boundary)
163
237
  throw new Error("Requested history predates the proven temporal origin");
@@ -166,8 +240,9 @@ export function readTemporalState(view, offset = 0, scope) {
166
240
  return overlayStates(...SCOPES.map((owner) => scopeAt(view.scopes[owner], boundary)));
167
241
  }
168
242
  /** Allocate the identity outside this algebra; only materially effective patches accept it. */
169
- export function advanceTemporalState(view, transitions, id) {
170
- validateTemporalState(view);
243
+ export function advanceTemporalState(view, transitions, id, historyLimit = DEFAULT_HISTORY_LIMIT) {
244
+ validateHistoryLimit(historyLimit);
245
+ validateTemporalState(view, historyLimit);
171
246
  if (transitions.length === 0)
172
247
  return view;
173
248
  validateRecentTransition({ id, at: 0, transitions });
@@ -191,13 +266,18 @@ export function advanceTemporalState(view, transitions, id) {
191
266
  const next = structuredClone(view);
192
267
  for (const { scope, patch } of changes) {
193
268
  const stream = next.scopes[scope];
194
- if (stream.patches.length === RECENT_TRANSITION_LIMIT) {
269
+ if (historyLimit === 0) {
270
+ stream.checkpoint = { through: structuredClone(boundary), state: apply(scopeAt(stream, head), patch) };
271
+ stream.patches = [];
272
+ continue;
273
+ }
274
+ while (stream.patches.length >= historyLimit) {
195
275
  const folded = stream.patches.shift();
196
276
  stream.checkpoint = { through: folded.transition, state: apply(stream.checkpoint.state, folded.patch) };
197
277
  }
198
278
  stream.patches.push({ transition: structuredClone(boundary), patch: structuredClone(patch) });
199
279
  }
200
- next.lineage = [...next.lineage, boundary].slice(-(RECENT_TRANSITION_LIMIT + 1));
201
- validateTemporalState(next);
280
+ next.lineage = [...next.lineage, boundary].slice(-(historyLimit + 1));
281
+ validateTemporalState(next, historyLimit);
202
282
  return next;
203
283
  }
@@ -12,8 +12,6 @@ export interface StagedScopedTransition {
12
12
  causalBasis: string;
13
13
  committed: boolean;
14
14
  }
15
- /** Validate that final eligibility has no pending acquisition/compilation obligation. */
16
- export declare function validateFinalEligibility(currentStates: ScopedStates, successfulSkillReads: Iterable<SuccessfulSkillRead>, causalBasis: string, successfulArtifactReads?: Iterable<SuccessfulArtifactRead>): void;
17
15
  /** Stage one canonical atomic scope cohort without changing the finalized response. */
18
16
  export declare function stageAtomicScopePatches(currentStates: ScopedStates, patches: AtomicScopePatches, successfulSkillReads: Iterable<SuccessfulSkillRead>, causalBasis: string, successfulArtifactReads?: Iterable<SuccessfulArtifactRead>): StagedScopedTransition;
19
17
  export declare function stageScopedTransition(currentStates: ScopedStates, transition: TerminalTransition, successfulSkillReads: Iterable<SuccessfulSkillRead>, causalBasis: string, successfulArtifactReads?: Iterable<SuccessfulArtifactRead>): StagedScopedTransition;
@@ -3,15 +3,15 @@ import { createAcceptedTransition } from "./history.js";
3
3
  import { applyPatch, containsNull, hashJson, isObject, validatePatch } from "./json.js";
4
4
  import { hasCompiledSkillArtifact, SKILL_ARTIFACT_COMPILER } from "./skills.js";
5
5
  const SCOPES = new Set(["global", "cwd", "session"]);
6
- const PATCH_KEYS = new Set(["artifacts", "contract", "working", "intents", "lazy"]);
6
+ const PATCH_KEYS = new Set(["intents", "contract", "working", "artifacts", "lazy"]);
7
7
  function compileReadArtifacts(nextState, patch, successfulArtifactReads, provenance) {
8
8
  for (const read of successfulArtifactReads) {
9
9
  const output = patch.artifacts[read.path];
10
10
  if (!isObject(output)) {
11
- throw new Error(`Every successfully read invalidated artifact must have a global compiler output at artifacts[exact candidate path]; missing: ${read.path}`);
11
+ throw new Error(`Successfully read invalidated artifact requires compiler output at ${read.scope ?? "global"}.artifacts[${JSON.stringify(read.path)}]`);
12
12
  }
13
13
  const compiled = compileArtifact({
14
- source: { path: read.path, hash: read.hash },
14
+ source: { path: read.path, scope: read.scope, hash: read.hash, sourceFingerprint: read.sourceFingerprint },
15
15
  compiler: ORDINARY_ARTIFACT_COMPILER,
16
16
  output: output,
17
17
  });
@@ -77,7 +77,7 @@ function validateScopePatch(scope, patch) {
77
77
  validatePatch(patch);
78
78
  for (const key of Object.keys(patch)) {
79
79
  if (!PATCH_KEYS.has(key)) {
80
- throw new Error(`Scoped State Flow patches cannot modify ${key}; only artifacts, contract, working, intents, and lazy are model-owned`);
80
+ throw new Error(`Unknown State Flow patch key ${JSON.stringify(key)}; expected one of: ${[...PATCH_KEYS].join(", ")}`);
81
81
  }
82
82
  }
83
83
  for (const key of ["artifacts", "contract", "working", "intents"]) {
@@ -85,8 +85,8 @@ function validateScopePatch(scope, patch) {
85
85
  throw new Error(`Scoped State Flow patch field ${key} must be a JSON object`);
86
86
  }
87
87
  }
88
- if (Object.hasOwn(patch, "lazy") && patch.lazy === null) {
89
- throw new Error("Scoped State Flow patch field lazy cannot be null");
88
+ if (Object.hasOwn(patch, "lazy") && !isObject(patch.lazy)) {
89
+ throw new Error("Scoped State Flow patch field lazy must be a JSON object");
90
90
  }
91
91
  if (isObject(patch.artifacts))
92
92
  validateModelArtifactPatch(patch.artifacts);
@@ -98,7 +98,7 @@ function completePatch(patch, response) {
98
98
  working: patch.working ?? {},
99
99
  intents: patch.intents ?? {},
100
100
  response,
101
- ...(Object.hasOwn(patch, "lazy") ? { lazy: structuredClone(patch.lazy) } : {}),
101
+ lazy: structuredClone(patch.lazy ?? {}),
102
102
  };
103
103
  }
104
104
  /** Stage all scope updates against one immutable basis before any state is published. */
@@ -120,7 +120,8 @@ function stageScopedSemanticTransition(currentStates, transition, successfulSkil
120
120
  patches.set(scope, item.patch);
121
121
  }
122
122
  const cwdPatch = patches.get("cwd") ?? {};
123
- const nextStates = structuredClone(currentStates);
123
+ const artifactReads = [...successfulArtifactReads];
124
+ const nextStates = { ...currentStates };
124
125
  const provenanceUpdates = { global: {}, cwd: {}, session: {} };
125
126
  for (const scope of SCOPES) {
126
127
  const authored = patches.get(scope) ?? {};
@@ -128,8 +129,8 @@ function stageScopedSemanticTransition(currentStates, transition, successfulSkil
128
129
  ? acceptedResponse
129
130
  : currentStates[scope].response;
130
131
  const patch = completePatch(authored, response);
131
- const nextState = applyPatch(structuredClone(currentStates[scope]), patch);
132
- compileReadArtifacts(nextState, { artifacts: scope === "global" ? authored.artifacts ?? {} : {} }, scope === "global" ? successfulArtifactReads : [], provenanceUpdates.global);
132
+ const nextState = applyPatch(currentStates[scope], patch);
133
+ compileReadArtifacts(nextState, { artifacts: authored.artifacts ?? {} }, artifactReads.filter((read) => (read.scope ?? "global") === scope), provenanceUpdates[scope]);
133
134
  compileReadSkills(nextState, { artifacts: scope === "cwd" ? cwdPatch.artifacts ?? {} : {} }, scope === "cwd" ? successfulSkillReads : [], provenanceUpdates.cwd);
134
135
  validateMaterializedTransition(nextState);
135
136
  nextStates[scope] = nextState;
@@ -146,10 +147,6 @@ function stageScopedSemanticTransition(currentStates, transition, successfulSkil
146
147
  committed: false,
147
148
  };
148
149
  }
149
- /** Validate that final eligibility has no pending acquisition/compilation obligation. */
150
- export function validateFinalEligibility(currentStates, successfulSkillReads, causalBasis, successfulArtifactReads = []) {
151
- stageScopedSemanticTransition(currentStates, { transitions: [] }, successfulSkillReads, causalBasis, successfulArtifactReads);
152
- }
153
150
  /** Stage one canonical atomic scope cohort without changing the finalized response. */
154
151
  export function stageAtomicScopePatches(currentStates, patches, successfulSkillReads, causalBasis, successfulArtifactReads = []) {
155
152
  if (!isObject(patches))