@llblab/pi-kit 0.13.0 → 0.14.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 (95) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +1 -1
  3. package/node_modules/@llblab/pi-state-flow/AGENTS.md +7 -7
  4. package/node_modules/@llblab/pi-state-flow/BACKLOG.md +8 -9
  5. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +26 -0
  6. package/node_modules/@llblab/pi-state-flow/README.md +1 -3
  7. package/node_modules/@llblab/pi-state-flow/dist/index.d.ts +21 -0
  8. package/node_modules/@llblab/pi-state-flow/dist/index.js +20 -0
  9. package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.d.ts +39 -0
  10. package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.js +78 -0
  11. package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.d.ts +110 -0
  12. package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.js +334 -0
  13. package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.d.ts +49 -0
  14. package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.js +67 -0
  15. package/node_modules/@llblab/pi-state-flow/dist/lib/config.d.ts +11 -0
  16. package/node_modules/@llblab/pi-state-flow/dist/lib/config.js +53 -0
  17. package/node_modules/@llblab/pi-state-flow/dist/lib/context.d.ts +23 -0
  18. package/node_modules/@llblab/pi-state-flow/dist/lib/context.js +109 -0
  19. package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.d.ts +111 -0
  20. package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.js +189 -0
  21. package/node_modules/@llblab/pi-state-flow/dist/lib/discovery.d.ts +21 -0
  22. package/node_modules/@llblab/pi-state-flow/dist/lib/discovery.js +125 -0
  23. package/node_modules/@llblab/pi-state-flow/dist/lib/durable.d.ts +102 -0
  24. package/node_modules/@llblab/pi-state-flow/dist/lib/durable.js +507 -0
  25. package/node_modules/@llblab/pi-state-flow/dist/lib/episode.d.ts +8 -0
  26. package/node_modules/@llblab/pi-state-flow/dist/lib/episode.js +27 -0
  27. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.d.ts +22 -0
  28. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +1263 -0
  29. package/node_modules/@llblab/pi-state-flow/dist/lib/git.d.ts +72 -0
  30. package/node_modules/@llblab/pi-state-flow/dist/lib/git.js +565 -0
  31. package/node_modules/@llblab/pi-state-flow/dist/lib/history.d.ts +22 -0
  32. package/node_modules/@llblab/pi-state-flow/dist/lib/history.js +79 -0
  33. package/node_modules/@llblab/pi-state-flow/dist/lib/json.d.ts +12 -0
  34. package/node_modules/@llblab/pi-state-flow/dist/lib/json.js +109 -0
  35. package/node_modules/@llblab/pi-state-flow/dist/lib/logging.d.ts +25 -0
  36. package/node_modules/@llblab/pi-state-flow/dist/lib/logging.js +24 -0
  37. package/node_modules/@llblab/pi-state-flow/dist/lib/maintenance.d.ts +36 -0
  38. package/node_modules/@llblab/pi-state-flow/dist/lib/maintenance.js +98 -0
  39. package/node_modules/@llblab/pi-state-flow/dist/lib/memory.d.ts +15 -0
  40. package/node_modules/@llblab/pi-state-flow/dist/lib/memory.js +42 -0
  41. package/node_modules/@llblab/pi-state-flow/dist/lib/migration.d.ts +13 -0
  42. package/node_modules/@llblab/pi-state-flow/dist/lib/migration.js +133 -0
  43. package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.d.ts +7 -0
  44. package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +59 -0
  45. package/node_modules/@llblab/pi-state-flow/dist/lib/publication.d.ts +69 -0
  46. package/node_modules/@llblab/pi-state-flow/dist/lib/publication.js +335 -0
  47. package/node_modules/@llblab/pi-state-flow/dist/lib/query.d.ts +27 -0
  48. package/node_modules/@llblab/pi-state-flow/dist/lib/query.js +35 -0
  49. package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.d.ts +8 -0
  50. package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.js +27 -0
  51. package/node_modules/@llblab/pi-state-flow/dist/lib/rehydration.d.ts +36 -0
  52. package/node_modules/@llblab/pi-state-flow/dist/lib/rehydration.js +38 -0
  53. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.d.ts +142 -0
  54. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.js +529 -0
  55. package/node_modules/@llblab/pi-state-flow/dist/lib/session.d.ts +21 -0
  56. package/node_modules/@llblab/pi-state-flow/dist/lib/session.js +44 -0
  57. package/node_modules/@llblab/pi-state-flow/dist/lib/skills.d.ts +25 -0
  58. package/node_modules/@llblab/pi-state-flow/dist/lib/skills.js +131 -0
  59. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.d.ts +88 -0
  60. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.js +255 -0
  61. package/node_modules/@llblab/pi-state-flow/dist/lib/state.d.ts +55 -0
  62. package/node_modules/@llblab/pi-state-flow/dist/lib/state.js +31 -0
  63. package/node_modules/@llblab/pi-state-flow/dist/lib/status.d.ts +38 -0
  64. package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +79 -0
  65. package/node_modules/@llblab/pi-state-flow/dist/lib/storage.d.ts +46 -0
  66. package/node_modules/@llblab/pi-state-flow/dist/lib/storage.js +217 -0
  67. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.d.ts +98 -0
  68. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +231 -0
  69. package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.d.ts +39 -0
  70. package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.js +203 -0
  71. package/node_modules/@llblab/pi-state-flow/dist/lib/transition.d.ts +25 -0
  72. package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +204 -0
  73. package/node_modules/@llblab/pi-state-flow/dist/package.json +79 -0
  74. package/node_modules/@llblab/pi-state-flow/dist/pi-state-flow/index.js +1 -0
  75. package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-memory/SKILL.md +138 -0
  76. package/node_modules/@llblab/pi-state-flow/docs/architecture.md +11 -5
  77. package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +5 -1
  78. package/node_modules/@llblab/pi-state-flow/docs/filesystem-recovery.md +3 -3
  79. package/node_modules/@llblab/pi-state-flow/docs/usage.md +6 -4
  80. package/node_modules/@llblab/pi-state-flow/index.ts +1 -0
  81. package/node_modules/@llblab/pi-state-flow/lib/compaction.ts +17 -1
  82. package/node_modules/@llblab/pi-state-flow/lib/config.ts +6 -1
  83. package/node_modules/@llblab/pi-state-flow/lib/durable.ts +88 -96
  84. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +64 -12
  85. package/node_modules/@llblab/pi-state-flow/lib/git.ts +32 -188
  86. package/node_modules/@llblab/pi-state-flow/lib/migration.ts +84 -48
  87. package/node_modules/@llblab/pi-state-flow/lib/query.ts +40 -0
  88. package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +7 -30
  89. package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +48 -50
  90. package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +41 -97
  91. package/node_modules/@llblab/pi-state-flow/lib/storage.ts +17 -12
  92. package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +4 -4
  93. package/node_modules/@llblab/pi-state-flow/package.json +23 -6
  94. package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +3 -1
  95. package/package.json +4 -4
@@ -0,0 +1,79 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { isJsonValue, isObject, sameJson, validatePatch } from "./json.js";
3
+ export const RECENT_TRANSITION_LIMIT = 7;
4
+ const SCOPES = new Set(["global", "cwd", "session"]);
5
+ const PATCH_KEYS = new Set(["artifacts", "contract", "working", "response"]);
6
+ /** Normalize accepted replacements into recursive-merge replay, including removals. */
7
+ function replayPatch(before, after) {
8
+ const entries = [];
9
+ for (const key of new Set([...Object.keys(before), ...Object.keys(after)])) {
10
+ if (!Object.hasOwn(after, key)) {
11
+ entries.push([key, null]);
12
+ continue;
13
+ }
14
+ if (Object.hasOwn(before, key) && sameJson(before[key], after[key]))
15
+ continue;
16
+ const previous = before[key];
17
+ const next = after[key];
18
+ entries.push([key, isObject(previous) && isObject(next) ? replayPatch(previous, next) : structuredClone(next)]);
19
+ }
20
+ return Object.fromEntries(entries);
21
+ }
22
+ function validateScopedPatch(value) {
23
+ if (!isObject(value) || Object.keys(value).sort().join(",") !== "patch,scope") {
24
+ throw new Error('Recent State Flow transition entries must contain exactly "scope" and "patch"');
25
+ }
26
+ if (typeof value.scope !== "string" || !SCOPES.has(value.scope)) {
27
+ throw new Error(`Recent State Flow transition has an unknown scope: ${String(value.scope)}`);
28
+ }
29
+ validatePatch(value.patch);
30
+ for (const [key, field] of Object.entries(value.patch)) {
31
+ if (!PATCH_KEYS.has(key) || (key === "response"
32
+ ? value.scope !== "session" || typeof field !== "string" : !isObject(field))) {
33
+ throw new Error("Recent State Flow patches may contain object-valued artifacts, contract, and working plus a session response string");
34
+ }
35
+ }
36
+ }
37
+ export function validateRecentTransition(value) {
38
+ if (!isObject(value) || Object.keys(value).sort().join(",") !== "at,id,transitions")
39
+ throw new Error("Recent State Flow transition has an invalid envelope");
40
+ if (typeof value.id !== "string" || value.id.length === 0)
41
+ throw new Error("Recent State Flow transition must have a non-empty id");
42
+ if (!Number.isSafeInteger(value.at) || value.at < 0)
43
+ throw new Error("Recent State Flow transition must have a safe non-negative position");
44
+ if (!Array.isArray(value.transitions) || value.transitions.length === 0 || !isJsonValue(value.transitions))
45
+ throw new Error("Recent State Flow transition must contain semantic patches");
46
+ const seen = new Set();
47
+ for (const transition of value.transitions) {
48
+ validateScopedPatch(transition);
49
+ if (seen.has(transition.scope))
50
+ throw new Error(`Duplicate recent State Flow transition scope: ${transition.scope}`);
51
+ seen.add(transition.scope);
52
+ }
53
+ }
54
+ export function createAcceptedTransition(currentStates, nextStates, id) {
55
+ const transitions = [];
56
+ for (const scope of SCOPES) {
57
+ const patch = replayPatch(currentStates[scope], nextStates[scope]);
58
+ if (Object.keys(patch).length > 0)
59
+ transitions.push({ scope, patch });
60
+ }
61
+ if (transitions.length === 0)
62
+ return undefined;
63
+ return { id: id ?? randomUUID(), transitions };
64
+ }
65
+ /** Preserve the configured per-scope budget, filtering in selected-lineage order. */
66
+ export function projectRecentTransitionsWithLimit(limit, lineage) {
67
+ if (!Number.isSafeInteger(limit) || limit < 0 || limit > RECENT_TRANSITION_LIMIT) {
68
+ throw new Error(`Recent State Flow transition limit must be an integer from 0 to ${RECENT_TRANSITION_LIMIT}`);
69
+ }
70
+ const remaining = { global: limit, cwd: limit, session: limit };
71
+ const result = [];
72
+ for (let index = lineage.length - 1; index >= 0; index--) {
73
+ const record = lineage[index];
74
+ const transitions = record.transitions.filter(({ scope }) => remaining[scope]-- > 0);
75
+ if (transitions.length)
76
+ result.push({ ...record, transitions });
77
+ }
78
+ return structuredClone(result.reverse());
79
+ }
@@ -0,0 +1,12 @@
1
+ export type JsonValue = null | boolean | number | string | JsonValue[] | JsonObject;
2
+ export interface JsonObject {
3
+ [key: string]: JsonValue;
4
+ }
5
+ export declare function applyPatch(state: JsonObject, patch: JsonObject): JsonObject;
6
+ export declare function isObject(value: JsonValue | unknown): value is JsonObject;
7
+ export declare function validatePatch(value: unknown): asserts value is JsonObject;
8
+ export declare function canonicalJson(value: JsonValue | unknown): string;
9
+ export declare function sameJson(left: JsonValue | unknown, right: JsonValue | unknown): boolean;
10
+ export declare function hashJson(value: JsonValue | unknown): string;
11
+ export declare function containsNull(value: unknown): boolean;
12
+ export declare function isJsonValue(value: unknown, ancestors?: WeakSet<object>): value is JsonValue;
@@ -0,0 +1,109 @@
1
+ import { createHash } from "node:crypto";
2
+ export function applyPatch(state, patch) {
3
+ const next = structuredClone(state);
4
+ for (const [key, value] of Object.entries(patch)) {
5
+ if (value === null) {
6
+ delete next[key];
7
+ continue;
8
+ }
9
+ const current = next[key];
10
+ const materialized = isObject(current) && isObject(value)
11
+ ? applyPatch(current, value)
12
+ : structuredClone(value);
13
+ Object.defineProperty(next, key, {
14
+ value: materialized,
15
+ enumerable: true,
16
+ configurable: true,
17
+ writable: true,
18
+ });
19
+ }
20
+ return next;
21
+ }
22
+ export function isObject(value) {
23
+ return typeof value === "object" && value !== null && !Array.isArray(value);
24
+ }
25
+ export function validatePatch(value) {
26
+ if (!isObject(value))
27
+ throw new Error("State patch must be a JSON object");
28
+ if (!isJsonValue(value))
29
+ throw new Error("State patch must contain finite, acyclic JSON data");
30
+ }
31
+ export function canonicalJson(value) {
32
+ if (!isJsonValue(value))
33
+ throw new Error("Value must be finite, acyclic JSON data");
34
+ return JSON.stringify(orderValue(value));
35
+ }
36
+ export function sameJson(left, right) {
37
+ if (left === right) {
38
+ if (!isJsonValue(left))
39
+ throw new Error("Values must be finite, acyclic JSON data");
40
+ return true;
41
+ }
42
+ if (!isJsonValue(left) || !isJsonValue(right))
43
+ throw new Error("Values must be finite, acyclic JSON data");
44
+ return equalJsonValues(left, right);
45
+ }
46
+ function equalJsonValues(left, right) {
47
+ if (left === right)
48
+ return true;
49
+ if (Array.isArray(left) || Array.isArray(right)) {
50
+ return Array.isArray(left) && Array.isArray(right) && left.length === right.length
51
+ && left.every((value, index) => equalJsonValues(value, right[index]));
52
+ }
53
+ if (isObject(left) || isObject(right)) {
54
+ if (!isObject(left) || !isObject(right))
55
+ return false;
56
+ const keys = Object.keys(left);
57
+ return keys.length === Object.keys(right).length
58
+ && keys.every((key) => Object.hasOwn(right, key) && equalJsonValues(left[key], right[key]));
59
+ }
60
+ return false;
61
+ }
62
+ export function hashJson(value) {
63
+ return createHash("sha256").update(canonicalJson(value)).digest("hex");
64
+ }
65
+ function orderValue(value) {
66
+ if (Array.isArray(value))
67
+ return value.map((item) => orderValue(item));
68
+ if (!isObject(value))
69
+ return value;
70
+ return Object.fromEntries(Object.keys(value).sort().map((key) => [key, orderValue(value[key])]));
71
+ }
72
+ export function containsNull(value) {
73
+ if (value === null)
74
+ return true;
75
+ if (Array.isArray(value))
76
+ return value.some((item) => containsNull(item));
77
+ if (!isObject(value))
78
+ return false;
79
+ return Object.values(value).some((item) => containsNull(item));
80
+ }
81
+ export function isJsonValue(value, ancestors = new WeakSet()) {
82
+ if (value === null || typeof value === "string" || typeof value === "boolean")
83
+ return true;
84
+ if (typeof value === "number")
85
+ return Number.isFinite(value);
86
+ if (typeof value !== "object")
87
+ return false;
88
+ if (ancestors.has(value))
89
+ return false;
90
+ ancestors.add(value);
91
+ try {
92
+ if (Array.isArray(value))
93
+ return value.every((item) => isJsonValue(item, ancestors));
94
+ if (!isObject(value))
95
+ return false;
96
+ const prototype = Object.getPrototypeOf(value);
97
+ if (prototype !== Object.prototype && prototype !== null)
98
+ return false;
99
+ if (Reflect.ownKeys(value).some((key) => typeof key === "symbol"))
100
+ return false;
101
+ return Object.values(value).every((item) => isJsonValue(item, ancestors));
102
+ }
103
+ catch {
104
+ return false;
105
+ }
106
+ finally {
107
+ ancestors.delete(value);
108
+ }
109
+ }
@@ -0,0 +1,25 @@
1
+ export type StateFlowDiagnosticCategory = "invalid-patch" | "publication-conflict" | "terminal-pending" | "finalization";
2
+ /** Minimal structural block; only ordinary text keeps its exact content. */
3
+ export interface StateFlowDiagnosticBlock {
4
+ type: string;
5
+ text?: string;
6
+ }
7
+ export interface StateFlowDiagnosticRecord {
8
+ at: string;
9
+ sessionId: string;
10
+ cwd: string;
11
+ category: StateFlowDiagnosticCategory;
12
+ error: string;
13
+ content?: StateFlowDiagnosticBlock[];
14
+ /** Rejected tool arguments, captured for reproducible diagnosis. Never reasoning bodies. */
15
+ input?: unknown;
16
+ tool?: string;
17
+ toolCallId?: string;
18
+ resolutionAttempt?: number;
19
+ terminalEligible?: boolean;
20
+ }
21
+ /** Preserve exact text blocks and block boundaries; reasoning bodies are never duplicated. */
22
+ export declare function projectDiagnosticContent(content: unknown): StateFlowDiagnosticBlock[];
23
+ /** Diagnostic JSONL lives beneath the active Pi agent directory, never inside the state repository. */
24
+ export declare function stateFlowLogPath(agentDir: string): string;
25
+ export declare function appendStateFlowDiagnostic(path: string, record: StateFlowDiagnosticRecord): void;
@@ -0,0 +1,24 @@
1
+ // Domain: opt-in diagnostic capture for rejected State Flow resolutions.
2
+ import { appendFileSync, mkdirSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { isObject } from "./json.js";
5
+ /** Preserve exact text blocks and block boundaries; reasoning bodies are never duplicated. */
6
+ export function projectDiagnosticContent(content) {
7
+ if (!Array.isArray(content))
8
+ return [];
9
+ return content.map((block) => {
10
+ if (!isObject(block) || typeof block.type !== "string")
11
+ return { type: "unknown" };
12
+ if (block.type === "text" && typeof block.text === "string")
13
+ return { type: "text", text: block.text };
14
+ return { type: block.type };
15
+ });
16
+ }
17
+ /** Diagnostic JSONL lives beneath the active Pi agent directory, never inside the state repository. */
18
+ export function stateFlowLogPath(agentDir) {
19
+ return join(agentDir, "tmp", "state-flow", "logs.jsonl");
20
+ }
21
+ export function appendStateFlowDiagnostic(path, record) {
22
+ mkdirSync(dirname(path), { recursive: true });
23
+ appendFileSync(path, `${JSON.stringify(record)}\n`, { encoding: "utf8", mode: 0o600 });
24
+ }
@@ -0,0 +1,36 @@
1
+ import { type ArtifactProvenance, type ArtifactSourceIdentity } from "./artifact.ts";
2
+ import type { ArtifactSourceCandidate } from "./discovery.ts";
3
+ export declare const DEFAULT_ARTIFACT_MAINTENANCE_MAX_READS = 1;
4
+ export declare const DEFAULT_ARTIFACT_MAINTENANCE_MAX_SOURCE_BYTES: number;
5
+ export declare const DEFAULT_ARTIFACT_MAINTENANCE_MINIMUM_AGE_MS: number;
6
+ export interface ArtifactMaintenanceOptions {
7
+ /** Stable cycle time supplied by the caller. */
8
+ now?: Date | number | string;
9
+ /** A source must be at least this old. Missing/unparseable timestamps rank as oldest. */
10
+ minimumAgeMs?: number;
11
+ /** Strict source-read count ceiling for this cycle. */
12
+ maxReads?: number;
13
+ /** Strict source-byte ceiling; bytes conservatively upper-bound source tokenizer input. */
14
+ maxSourceBytes?: number;
15
+ }
16
+ export interface ArtifactMaintenanceRequest extends ArtifactSourceIdentity {
17
+ reason: "maintenance";
18
+ sourceBytes: number;
19
+ }
20
+ export interface ArtifactMaintenancePlan {
21
+ requiresCompilation: ArtifactMaintenanceRequest[];
22
+ deferred: ArtifactMaintenanceRequest[];
23
+ budget: {
24
+ maxReads: number;
25
+ maxSourceBytes: number;
26
+ usedReads: number;
27
+ usedSourceBytes: number;
28
+ };
29
+ }
30
+ /**
31
+ * Select a bounded, oldest-first maintenance cohort from otherwise fresh artifacts.
32
+ *
33
+ * This planner is opt-in and side-effect free. Correctness invalidations remain the
34
+ * responsibility of planArtifactInvalidation; they are never displaced by maintenance.
35
+ */
36
+ export declare function planArtifactMaintenance(sources: readonly ArtifactSourceCandidate[], registry: Readonly<Record<string, unknown>>, compiler: string, options?: ArtifactMaintenanceOptions, provenance?: Readonly<Record<string, ArtifactProvenance>>): ArtifactMaintenancePlan;
@@ -0,0 +1,98 @@
1
+ import { classifyArtifactFreshness, } from "./artifact.js";
2
+ import { isObject } from "./json.js";
3
+ export const DEFAULT_ARTIFACT_MAINTENANCE_MAX_READS = 1;
4
+ export const DEFAULT_ARTIFACT_MAINTENANCE_MAX_SOURCE_BYTES = 16 * 1024;
5
+ export const DEFAULT_ARTIFACT_MAINTENANCE_MINIMUM_AGE_MS = 30 * 24 * 60 * 60 * 1_000;
6
+ function nonNegativeSafeInteger(value, name) {
7
+ if (!Number.isSafeInteger(value) || value < 0)
8
+ throw new Error(`${name} must be a non-negative safe integer`);
9
+ return value;
10
+ }
11
+ function cycleTime(value) {
12
+ const timestamp = value === undefined
13
+ ? Date.now()
14
+ : value instanceof Date
15
+ ? value.getTime()
16
+ : typeof value === "number"
17
+ ? value
18
+ : Date.parse(value);
19
+ if (!Number.isFinite(timestamp))
20
+ throw new Error("Artifact maintenance cycle time must be valid");
21
+ return timestamp;
22
+ }
23
+ function compiledTime(entry, legacy) {
24
+ const timestamp = entry?.malformed === true ? undefined
25
+ : entry !== undefined && Object.hasOwn(entry, "compiledAt") ? entry.compiledAt
26
+ : legacy;
27
+ if (typeof timestamp !== "string")
28
+ return Number.NEGATIVE_INFINITY;
29
+ const parsed = Date.parse(timestamp);
30
+ return Number.isFinite(parsed) ? parsed : Number.NEGATIVE_INFINITY;
31
+ }
32
+ function request(source) {
33
+ return {
34
+ path: source.path,
35
+ hash: source.hash,
36
+ reason: "maintenance",
37
+ sourceBytes: source.bytes,
38
+ };
39
+ }
40
+ /**
41
+ * Select a bounded, oldest-first maintenance cohort from otherwise fresh artifacts.
42
+ *
43
+ * This planner is opt-in and side-effect free. Correctness invalidations remain the
44
+ * responsibility of planArtifactInvalidation; they are never displaced by maintenance.
45
+ */
46
+ export function planArtifactMaintenance(sources, registry, compiler, options = {}, provenance = {}) {
47
+ if (!isObject(registry))
48
+ throw new Error("Artifacts must be a path-keyed JSON object");
49
+ const now = cycleTime(options.now);
50
+ const minimumAgeMs = nonNegativeSafeInteger(options.minimumAgeMs ?? DEFAULT_ARTIFACT_MAINTENANCE_MINIMUM_AGE_MS, "Artifact maintenance minimum age");
51
+ const maxReads = nonNegativeSafeInteger(options.maxReads ?? DEFAULT_ARTIFACT_MAINTENANCE_MAX_READS, "Artifact maintenance read budget");
52
+ const maxSourceBytes = nonNegativeSafeInteger(options.maxSourceBytes ?? DEFAULT_ARTIFACT_MAINTENANCE_MAX_SOURCE_BYTES, "Artifact maintenance source-byte budget");
53
+ const seen = new Set();
54
+ const eligible = [];
55
+ for (const source of sources) {
56
+ if (seen.has(source.path))
57
+ throw new Error(`Duplicate artifact source path: ${source.path}`);
58
+ seen.add(source.path);
59
+ nonNegativeSafeInteger(source.bytes, `Artifact source bytes at ${source.path}`);
60
+ const metadata = Object.hasOwn(registry, source.path) ? registry[source.path] : undefined;
61
+ const entry = Object.hasOwn(provenance, source.path) ? provenance[source.path] : undefined;
62
+ const freshness = classifyArtifactFreshness(source, metadata, compiler, false, entry);
63
+ if (freshness.kind !== "fresh")
64
+ continue;
65
+ const compiledAt = compiledTime(entry, isObject(metadata) ? metadata.compiled_at : undefined);
66
+ if (compiledAt !== Number.NEGATIVE_INFINITY && now - compiledAt < minimumAgeMs)
67
+ continue;
68
+ eligible.push({ source, compiledAt });
69
+ }
70
+ eligible.sort((left, right) => {
71
+ if (left.compiledAt !== right.compiledAt)
72
+ return left.compiledAt - right.compiledAt;
73
+ return left.source.path < right.source.path ? -1 : left.source.path > right.source.path ? 1 : 0;
74
+ });
75
+ const requiresCompilation = [];
76
+ const deferred = [];
77
+ let usedSourceBytes = 0;
78
+ for (const candidate of eligible) {
79
+ const next = request(candidate.source);
80
+ if (requiresCompilation.length >= maxReads
81
+ || candidate.source.bytes > maxSourceBytes - usedSourceBytes) {
82
+ deferred.push(next);
83
+ continue;
84
+ }
85
+ requiresCompilation.push(next);
86
+ usedSourceBytes += candidate.source.bytes;
87
+ }
88
+ return {
89
+ requiresCompilation,
90
+ deferred,
91
+ budget: {
92
+ maxReads,
93
+ maxSourceBytes,
94
+ usedReads: requiresCompilation.length,
95
+ usedSourceBytes,
96
+ },
97
+ };
98
+ }
@@ -0,0 +1,15 @@
1
+ import type { MaterializedState, ScopedStates, StateScope } from "./state.ts";
2
+ export declare const MEMORY_PROMOTIONS_KEY = "memory_promotions";
3
+ export declare const MEMORY_PROMOTION_STATUSES: readonly ["pending", "accepted", "failed", "unknown"];
4
+ export type MemoryPromotionStatus = typeof MEMORY_PROMOTION_STATUSES[number];
5
+ export interface MemoryPromotionDiagnostic {
6
+ id: string;
7
+ status: MemoryPromotionStatus | "invalid";
8
+ owner?: string;
9
+ pointer?: string;
10
+ revision?: string;
11
+ error?: string;
12
+ }
13
+ /** Inspect the generic State Flow promotion convention without importing an external owner's schema. */
14
+ export declare function inspectMemoryPromotions(globalState: MaterializedState): MemoryPromotionDiagnostic[];
15
+ export declare function retainedMemoryScopes(states: ScopedStates): Record<StateScope, boolean>;
@@ -0,0 +1,42 @@
1
+ import { isObject } from "./json.js";
2
+ export const MEMORY_PROMOTIONS_KEY = "memory_promotions";
3
+ export const MEMORY_PROMOTION_STATUSES = ["pending", "accepted", "failed", "unknown"];
4
+ function nonEmpty(value) {
5
+ return typeof value === "string" && value.trim().length > 0 ? value : undefined;
6
+ }
7
+ /** Inspect the generic State Flow promotion convention without importing an external owner's schema. */
8
+ export function inspectMemoryPromotions(globalState) {
9
+ const records = globalState.working[MEMORY_PROMOTIONS_KEY];
10
+ if (records === undefined)
11
+ return [];
12
+ if (!isObject(records))
13
+ return [{ id: MEMORY_PROMOTIONS_KEY, status: "invalid", error: "promotion registry is not an object" }];
14
+ return Object.entries(records).sort(([left], [right]) => left.localeCompare(right)).map(([id, value]) => {
15
+ if (!isObject(value))
16
+ return { id, status: "invalid", error: "promotion record is not an object" };
17
+ const owner = nonEmpty(value.owner);
18
+ const pointer = nonEmpty(value.pointer);
19
+ const revision = nonEmpty(value.revision);
20
+ const error = nonEmpty(value.error);
21
+ const status = typeof value.status === "string" && MEMORY_PROMOTION_STATUSES.includes(value.status)
22
+ ? value.status
23
+ : undefined;
24
+ if (!status || !owner)
25
+ return { id, status: "invalid", ...(owner ? { owner } : {}), ...(pointer ? { pointer } : {}), ...(revision ? { revision } : {}), error: error ?? "promotion status/owner is invalid" };
26
+ if (status === "accepted" && (!pointer || !revision))
27
+ return { id, status: "invalid", owner, ...(pointer ? { pointer } : {}), ...(revision ? { revision } : {}), error: "accepted promotion requires pointer and revision" };
28
+ return { id, status, owner, ...(pointer ? { pointer } : {}), ...(revision ? { revision } : {}), ...(error ? { error } : {}) };
29
+ });
30
+ }
31
+ function hasSemanticMemory(state, ignorePromotions) {
32
+ if (Object.keys(state.contract).length > 0)
33
+ return true;
34
+ return Object.keys(state.working).some((key) => !ignorePromotions || key !== MEMORY_PROMOTIONS_KEY);
35
+ }
36
+ export function retainedMemoryScopes(states) {
37
+ return {
38
+ global: hasSemanticMemory(states.global, true),
39
+ cwd: hasSemanticMemory(states.cwd, false),
40
+ session: hasSemanticMemory(states.session, false),
41
+ };
42
+ }
@@ -0,0 +1,13 @@
1
+ import { type DurableFileBase, type OwnedFileUpdate } from "./durable.ts";
2
+ import type { StateScope } from "./state.ts";
3
+ export interface LegacyStorageMigration {
4
+ bases: DurableFileBase[];
5
+ updates: OwnedFileUpdate[];
6
+ scopes: StateScope[];
7
+ }
8
+ /** Read-only activation eligibility, before global migration or any repository publication. */
9
+ export declare function hasCwdMaterialization(cwd: string, repositoryRoot: string): boolean;
10
+ /** Detect predecessor snapshots or temporal envelopes without mutating the store. */
11
+ export declare function hasLegacyStateSources(cwd: string, sessionId: string, repositoryRoot: string, sessionKey?: string): boolean;
12
+ /** Plan from current scope snapshots only; old explanatory journals are never replay input. */
13
+ export declare function planLegacyStorageMigration(cwd: string, sessionId: string, repositoryRoot: string, _origin?: string, sessionKey?: string): LegacyStorageMigration;
@@ -0,0 +1,133 @@
1
+ // Domain: predecessor temporal-envelope migration planning; Git/files backends own publication and rollback.
2
+ import { lstatSync, readFileSync, readdirSync } from "node:fs";
3
+ import { join, resolve } from "node:path";
4
+ import { captureOwnedFileBases, cwdScopePaths, parseScopeStream, serializeScopeMetadata, serializeScopeStream, sessionScopeKey, sessionScopePaths, } from "./durable.js";
5
+ import { canonicalJson } from "./json.js";
6
+ /** Discover every owner-proven CWD and session cohort beneath the configured store. */
7
+ function migrationDirectories(cwd, sessionId, root, sessionKey) {
8
+ const selectedCwd = cwdScopePaths(cwd, root).directory;
9
+ const selectedSession = sessionScopePaths(cwd, sessionId, root, sessionKey).directory;
10
+ const directories = [{ directory: root, scope: "global" }];
11
+ const cwdOwners = new Map([[selectedCwd, resolve(cwd)]]);
12
+ if (lstatSync(root, { throwIfNoEntry: false }) !== undefined) {
13
+ for (const entry of readdirSync(root, { withFileTypes: true })) {
14
+ if (!entry.isDirectory())
15
+ continue;
16
+ const directory = join(root, entry.name);
17
+ let checkpoint;
18
+ let meta;
19
+ try {
20
+ checkpoint = JSON.parse(readFileSync(join(directory, "checkpoint.json"), "utf8"));
21
+ }
22
+ catch {
23
+ continue;
24
+ }
25
+ try {
26
+ meta = JSON.parse(readFileSync(join(directory, "meta.json"), "utf8"));
27
+ }
28
+ catch { /* predecessor metadata may be absent */ }
29
+ const owner = checkpoint && typeof checkpoint === "object" && !Array.isArray(checkpoint) && checkpoint.owner
30
+ ? checkpoint.owner
31
+ : meta && typeof meta === "object" && !Array.isArray(meta) ? meta.owner : undefined;
32
+ if (owner && typeof owner.cwd === "string" && resolve(owner.cwd) === owner.cwd)
33
+ cwdOwners.set(directory, owner.cwd);
34
+ }
35
+ }
36
+ for (const [cwdDirectory, cwdOwner] of cwdOwners) {
37
+ directories.push({ directory: cwdDirectory, scope: "cwd", cwdIdentity: cwdOwner });
38
+ if (lstatSync(cwdDirectory, { throwIfNoEntry: false }) === undefined)
39
+ continue;
40
+ for (const entry of readdirSync(cwdDirectory, { withFileTypes: true })) {
41
+ if (!entry.isDirectory())
42
+ continue;
43
+ try {
44
+ sessionScopeKey(entry.name);
45
+ }
46
+ catch {
47
+ continue;
48
+ }
49
+ const directory = join(cwdDirectory, entry.name);
50
+ let meta;
51
+ try {
52
+ meta = JSON.parse(readFileSync(join(directory, "meta.json"), "utf8"));
53
+ }
54
+ catch {
55
+ continue;
56
+ }
57
+ const identity = meta && typeof meta === "object" && !Array.isArray(meta) ? meta.identity : undefined;
58
+ if (!identity || typeof identity !== "object" || Array.isArray(identity))
59
+ continue;
60
+ const owner = identity;
61
+ if (owner.cwd === cwdOwner && typeof owner.sessionId === "string" && owner.sessionId.length > 0)
62
+ directories.push({ directory, scope: "session" });
63
+ }
64
+ }
65
+ if (!directories.some(({ directory }) => directory === selectedSession))
66
+ directories.push({ directory: selectedSession, scope: "session" });
67
+ return directories;
68
+ }
69
+ /** Read-only activation eligibility, before global migration or any repository publication. */
70
+ export function hasCwdMaterialization(cwd, repositoryRoot) {
71
+ const root = resolve(repositoryRoot);
72
+ const directory = cwdScopePaths(cwd, root).directory;
73
+ const [checkpoint, patches, meta] = captureOwnedFileBases([
74
+ join(directory, "checkpoint.json"), join(directory, "patches.jsonl"), join(directory, "meta.json"),
75
+ ], root);
76
+ if (checkpoint.content !== undefined)
77
+ return parseScopeStream(checkpoint.content, patches.content, "cwd", cwd, meta.content) !== undefined;
78
+ if (patches.content !== undefined)
79
+ throw new Error(`State Flow tail has no provable checkpoint: ${directory}`);
80
+ return false;
81
+ }
82
+ /** Detect predecessor snapshots or temporal envelopes without mutating the store. */
83
+ export function hasLegacyStateSources(cwd, sessionId, repositoryRoot, sessionKey = sessionId) {
84
+ const root = resolve(repositoryRoot);
85
+ return migrationDirectories(cwd, sessionId, root, sessionKey).some(({ directory }) => {
86
+ if (lstatSync(join(directory, "checkpoint.json"), { throwIfNoEntry: false }) === undefined)
87
+ return false;
88
+ try {
89
+ const meta = JSON.parse(readFileSync(join(directory, "meta.json"), "utf8"));
90
+ return meta === null || typeof meta !== "object" || !("temporal" in meta);
91
+ }
92
+ catch {
93
+ return true;
94
+ }
95
+ });
96
+ }
97
+ /** Plan from current scope snapshots only; old explanatory journals are never replay input. */
98
+ export function planLegacyStorageMigration(cwd, sessionId, repositoryRoot, _origin, sessionKey = sessionId) {
99
+ const root = resolve(repositoryRoot);
100
+ const directories = migrationDirectories(cwd, sessionId, root, sessionKey);
101
+ const paths = directories.flatMap(({ directory }) => [
102
+ join(directory, "checkpoint.json"), join(directory, "patches.jsonl"), join(directory, "meta.json"),
103
+ ]);
104
+ const bases = captureOwnedFileBases(paths, root);
105
+ const byPath = new Map(bases.map((base) => [base.path, base]));
106
+ const updates = [];
107
+ const scopes = [];
108
+ for (const { scope, directory, cwdIdentity } of directories) {
109
+ const checkpoint = byPath.get(join(directory, "checkpoint.json"));
110
+ const patches = byPath.get(join(directory, "patches.jsonl"));
111
+ const meta = byPath.get(join(directory, "meta.json"));
112
+ if (checkpoint.content !== undefined) {
113
+ const stream = parseScopeStream(checkpoint.content, patches.content, scope, cwdIdentity, meta.content);
114
+ const source = serializeScopeStream(stream, scope, cwdIdentity);
115
+ let metadata = serializeScopeMetadata(undefined, stream, scope, cwdIdentity, meta.content);
116
+ if (scope === "session") {
117
+ const runtime = JSON.parse(metadata);
118
+ runtime.revision = "self";
119
+ runtime.temporalRevision = "self";
120
+ metadata = `${canonicalJson(runtime)}\n`;
121
+ }
122
+ if (checkpoint.content !== source.checkpoint || patches.content !== source.patches || meta.content !== metadata) {
123
+ if (!scopes.includes(scope))
124
+ scopes.push(scope);
125
+ updates.push({ path: checkpoint.path, content: source.checkpoint }, { path: patches.path, content: source.patches }, { path: meta.path, content: metadata });
126
+ }
127
+ continue;
128
+ }
129
+ if (patches.content !== undefined)
130
+ throw new Error(`State Flow tail has no provable checkpoint: ${directory}`);
131
+ }
132
+ return { bases, updates, scopes };
133
+ }
@@ -0,0 +1,7 @@
1
+ import type { AgentMessage } from "@earendil-works/pi-agent-core";
2
+ export type { StateDocument } from "./state.ts";
3
+ /** The compact model-facing contract. Semantic writes never travel through terminal prose. */
4
+ export declare function stateFlowProtocol(bootstrap: boolean): string;
5
+ export declare function assistantToolCallCount(content: unknown): number;
6
+ /** The post-handler assistant message is authoritative; State Flow does not parse service comments. */
7
+ export declare function finalizedAssistantResponse(message: AgentMessage): string;