@llblab/pi-kit 0.5.2 → 0.7.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 (141) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +5 -5
  3. package/node_modules/@llblab/pi-actors/AGENTS.md +1 -1
  4. package/node_modules/@llblab/pi-actors/CHANGELOG.md +6 -0
  5. package/node_modules/@llblab/pi-actors/README.md +1 -1
  6. package/node_modules/@llblab/pi-actors/dist/lib/async-runs.d.ts +3 -0
  7. package/node_modules/@llblab/pi-actors/dist/lib/async-runs.js +14 -1
  8. package/node_modules/@llblab/pi-actors/dist/lib/command-templates.js +45 -3
  9. package/node_modules/@llblab/pi-actors/dist/lib/extension-runtime.js +1 -1
  10. package/node_modules/@llblab/pi-actors/dist/lib/observability.d.ts +16 -3
  11. package/node_modules/@llblab/pi-actors/dist/lib/observability.js +92 -7
  12. package/node_modules/@llblab/pi-actors/dist/lib/pi.d.ts +0 -1
  13. package/node_modules/@llblab/pi-actors/dist/lib/pi.js +15 -24
  14. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery-lineage.d.ts +17 -0
  15. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery-lineage.js +44 -0
  16. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery.d.ts +4 -0
  17. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery.js +102 -4
  18. package/node_modules/@llblab/pi-actors/dist/lib/run-ui-runtime.js +58 -39
  19. package/node_modules/@llblab/pi-actors/dist/lib/runtime.js +14 -6
  20. package/node_modules/@llblab/pi-actors/dist/skills/actors/SKILL.md +1 -1
  21. package/node_modules/@llblab/pi-actors/docs/async-runs.md +1 -1
  22. package/node_modules/@llblab/pi-actors/docs/coordinator-delivery.md +18 -23
  23. package/node_modules/@llblab/pi-actors/lib/async-runs.ts +18 -1
  24. package/node_modules/@llblab/pi-actors/lib/command-templates.ts +41 -3
  25. package/node_modules/@llblab/pi-actors/lib/extension-runtime.ts +1 -1
  26. package/node_modules/@llblab/pi-actors/lib/observability.ts +119 -5
  27. package/node_modules/@llblab/pi-actors/lib/pi.ts +15 -28
  28. package/node_modules/@llblab/pi-actors/lib/run-delivery-lineage.ts +68 -0
  29. package/node_modules/@llblab/pi-actors/lib/run-delivery.ts +120 -4
  30. package/node_modules/@llblab/pi-actors/lib/run-ui-runtime.ts +69 -44
  31. package/node_modules/@llblab/pi-actors/lib/runtime.ts +17 -6
  32. package/node_modules/@llblab/pi-actors/package.json +1 -1
  33. package/node_modules/@llblab/pi-actors/skills/actors/SKILL.md +1 -1
  34. package/node_modules/@llblab/pi-grow-loop/AGENTS.md +2 -2
  35. package/node_modules/@llblab/pi-grow-loop/CHANGELOG.md +4 -0
  36. package/node_modules/@llblab/pi-grow-loop/README.md +6 -6
  37. package/node_modules/@llblab/pi-grow-loop/index.ts +6 -3
  38. package/node_modules/@llblab/pi-grow-loop/package.json +1 -1
  39. package/node_modules/@llblab/pi-state-flow/AGENTS.md +33 -12
  40. package/node_modules/@llblab/pi-state-flow/BACKLOG.md +125 -2
  41. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +87 -45
  42. package/node_modules/@llblab/pi-state-flow/README.md +203 -107
  43. package/node_modules/@llblab/pi-state-flow/docs/README.md +4 -0
  44. package/node_modules/@llblab/pi-state-flow/docs/architecture.md +188 -0
  45. package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +42 -0
  46. package/node_modules/@llblab/pi-state-flow/index.ts +164 -2
  47. package/node_modules/@llblab/pi-state-flow/lib/acquisition.ts +138 -0
  48. package/node_modules/@llblab/pi-state-flow/lib/artifact.ts +273 -0
  49. package/node_modules/@llblab/pi-state-flow/lib/config.ts +48 -0
  50. package/node_modules/@llblab/pi-state-flow/lib/context.ts +18 -5
  51. package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +268 -0
  52. package/node_modules/@llblab/pi-state-flow/lib/discovery.ts +117 -0
  53. package/node_modules/@llblab/pi-state-flow/lib/durable.ts +562 -0
  54. package/node_modules/@llblab/pi-state-flow/lib/episode.ts +24 -12
  55. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +606 -70
  56. package/node_modules/@llblab/pi-state-flow/lib/git.ts +666 -0
  57. package/node_modules/@llblab/pi-state-flow/lib/history.ts +95 -0
  58. package/node_modules/@llblab/pi-state-flow/lib/json.ts +24 -0
  59. package/node_modules/@llblab/pi-state-flow/lib/maintenance.ts +141 -0
  60. package/node_modules/@llblab/pi-state-flow/lib/memory.ts +52 -0
  61. package/node_modules/@llblab/pi-state-flow/lib/migration.ts +88 -0
  62. package/node_modules/@llblab/pi-state-flow/lib/publication.ts +296 -0
  63. package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +23 -7
  64. package/node_modules/@llblab/pi-state-flow/lib/rehydration.ts +79 -0
  65. package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +264 -0
  66. package/node_modules/@llblab/pi-state-flow/lib/session.ts +6 -0
  67. package/node_modules/@llblab/pi-state-flow/lib/skills.ts +99 -7
  68. package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +305 -48
  69. package/node_modules/@llblab/pi-state-flow/lib/state.ts +74 -7
  70. package/node_modules/@llblab/pi-state-flow/lib/status.ts +125 -6
  71. package/node_modules/@llblab/pi-state-flow/lib/storage.ts +196 -0
  72. package/node_modules/@llblab/pi-state-flow/lib/temporal.ts +233 -0
  73. package/node_modules/@llblab/pi-state-flow/lib/terminal.ts +70 -24
  74. package/node_modules/@llblab/pi-state-flow/lib/transition.ts +254 -29
  75. package/node_modules/@llblab/pi-state-flow/package.json +8 -2
  76. package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +128 -0
  77. package/node_modules/@llblab/pi-telegram/AGENTS.md +14 -9
  78. package/node_modules/@llblab/pi-telegram/BACKLOG.md +20 -4
  79. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +22 -5
  80. package/node_modules/@llblab/pi-telegram/README.md +13 -9
  81. package/node_modules/@llblab/pi-telegram/docs/README.md +1 -0
  82. package/node_modules/@llblab/pi-telegram/docs/architecture.md +220 -18
  83. package/node_modules/@llblab/pi-telegram/docs/generative-apps.md +1 -1
  84. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +70 -19
  85. package/node_modules/@llblab/pi-telegram/docs/outbound.md +13 -7
  86. package/node_modules/@llblab/pi-telegram/docs/public-api.md +13 -5
  87. package/node_modules/@llblab/pi-telegram/docs/ui-style.md +3 -1
  88. package/node_modules/@llblab/pi-telegram/index.ts +4 -1415
  89. package/node_modules/@llblab/pi-telegram/lib/activity.ts +19 -5
  90. package/node_modules/@llblab/pi-telegram/lib/agent-messages.ts +6 -3
  91. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +37 -2
  92. package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +600 -135
  93. package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +962 -55
  94. package/node_modules/@llblab/pi-telegram/lib/bus.ts +350 -26
  95. package/node_modules/@llblab/pi-telegram/lib/channel-posts.ts +544 -0
  96. package/node_modules/@llblab/pi-telegram/lib/commands.ts +234 -11
  97. package/node_modules/@llblab/pi-telegram/lib/config.ts +178 -25
  98. package/node_modules/@llblab/pi-telegram/lib/delivery.ts +18 -18
  99. package/node_modules/@llblab/pi-telegram/lib/extension.ts +1792 -0
  100. package/node_modules/@llblab/pi-telegram/lib/generative-apps.ts +20 -2
  101. package/node_modules/@llblab/pi-telegram/lib/journal.ts +2184 -126
  102. package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +7 -1
  103. package/node_modules/@llblab/pi-telegram/lib/locks.ts +38 -1
  104. package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +154 -15
  105. package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +74 -40
  106. package/node_modules/@llblab/pi-telegram/lib/outbound-voice.ts +28 -42
  107. package/node_modules/@llblab/pi-telegram/lib/outbound.ts +18 -14
  108. package/node_modules/@llblab/pi-telegram/lib/paths.ts +29 -0
  109. package/node_modules/@llblab/pi-telegram/lib/polling.ts +85 -17
  110. package/node_modules/@llblab/pi-telegram/lib/preview.ts +115 -70
  111. package/node_modules/@llblab/pi-telegram/lib/prompts.ts +5 -2
  112. package/node_modules/@llblab/pi-telegram/lib/queue.ts +66 -22
  113. package/node_modules/@llblab/pi-telegram/lib/replies.ts +47 -39
  114. package/node_modules/@llblab/pi-telegram/lib/routing.ts +305 -112
  115. package/node_modules/@llblab/pi-telegram/lib/status.ts +10 -0
  116. package/node_modules/@llblab/pi-telegram/lib/sync.ts +308 -39
  117. package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +315 -7
  118. package/node_modules/@llblab/pi-telegram/lib/thread-cleanup-manager.ts +664 -0
  119. package/node_modules/@llblab/pi-telegram/lib/thread-display.ts +226 -0
  120. package/node_modules/@llblab/pi-telegram/lib/thread-naming.ts +118 -0
  121. package/node_modules/@llblab/pi-telegram/lib/threads.ts +1686 -129
  122. package/node_modules/@llblab/pi-telegram/lib/updates.ts +1319 -97
  123. package/node_modules/@llblab/pi-telegram/lib/workspace-admission.ts +1643 -0
  124. package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +968 -0
  125. package/node_modules/@llblab/pi-telegram/lib/workspace-slots.ts +84 -0
  126. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  127. package/node_modules/@llblab/pi-telegram/screenshot.png +0 -0
  128. package/node_modules/@llblab/pi-telegram/scripts/measure-bus.mjs +83 -0
  129. package/node_modules/@llblab/pi-telegram/scripts/measure-workspace.mjs +101 -0
  130. package/node_modules/@llblab/{skills → pi-telegram/skills}/show-me/SKILL.md +28 -6
  131. package/node_modules/@llblab/pi-telegram/skills/show-me/references/telegram-surfaces.md +43 -0
  132. package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/delivery-and-threads.md +1 -1
  133. package/node_modules/@llblab/skills/abcd-context/AGENTS.md +1 -0
  134. package/node_modules/@llblab/skills/abcd-context/CHANGELOG.md +6 -2
  135. package/node_modules/@llblab/skills/abcd-context/SKILL.md +1 -1
  136. package/node_modules/@llblab/skills/abcd-context/docs/validation-design.md +11 -5
  137. package/node_modules/@llblab/skills/abcd-context/scripts/_self-test.mjs +61 -0
  138. package/node_modules/@llblab/skills/abcd-context/scripts/validate-context.mjs +67 -0
  139. package/node_modules/@llblab/skills/package.json +2 -3
  140. package/node_modules/@llblab/skills/release-flow/SKILL.md +2 -4
  141. package/package.json +7 -6
@@ -0,0 +1,95 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { isJsonValue, isObject, sameJson, validatePatch, type JsonObject } from "./json.ts";
3
+ import type { ScopePatch, ScopedStates, StateScope } from "./state.ts";
4
+
5
+ export const RECENT_TRANSITION_LIMIT = 7;
6
+
7
+ export interface RecentScopePatch {
8
+ scope: StateScope;
9
+ patch: ScopePatch & { response?: string };
10
+ }
11
+
12
+ /** Exact accepted replay cohort; temporal runtime owns its causal boundary. */
13
+ export interface AcceptedTransition {
14
+ id: string;
15
+ transitions: RecentScopePatch[];
16
+ }
17
+
18
+ /** Compact lineage projection; at is a branch-local position, not a clock. */
19
+ export interface RecentTransition extends AcceptedTransition {
20
+ at: number;
21
+ }
22
+
23
+ export type RecentTransitionWindow = RecentTransition[];
24
+ const SCOPES = new Set<StateScope>(["global", "cwd", "session"]);
25
+ const PATCH_KEYS = new Set(["artifacts", "contract", "working", "response"]);
26
+
27
+ /** Normalize accepted replacements into recursive-merge replay, including removals. */
28
+ function replayPatch(before: JsonObject, after: JsonObject): JsonObject {
29
+ const entries: Array<[string, JsonObject[string]]> = [];
30
+ for (const key of new Set([...Object.keys(before), ...Object.keys(after)])) {
31
+ if (!Object.hasOwn(after, key)) {
32
+ entries.push([key, null]);
33
+ continue;
34
+ }
35
+ if (Object.hasOwn(before, key) && sameJson(before[key], after[key])) continue;
36
+ const previous = before[key];
37
+ const next = after[key]!;
38
+ entries.push([key, isObject(previous) && isObject(next) ? replayPatch(previous, next) : structuredClone(next)]);
39
+ }
40
+ return Object.fromEntries(entries);
41
+ }
42
+
43
+ function validateScopedPatch(value: unknown): asserts value is RecentScopePatch {
44
+ if (!isObject(value) || Object.keys(value).sort().join(",") !== "patch,scope") {
45
+ throw new Error('Recent State Flow transition entries must contain exactly "scope" and "patch"');
46
+ }
47
+ if (typeof value.scope !== "string" || !SCOPES.has(value.scope as StateScope)) {
48
+ throw new Error(`Recent State Flow transition has an unknown scope: ${String(value.scope)}`);
49
+ }
50
+ validatePatch(value.patch);
51
+ for (const [key, field] of Object.entries(value.patch)) {
52
+ if (!PATCH_KEYS.has(key) || (key === "response"
53
+ ? value.scope !== "session" || typeof field !== "string" : !isObject(field))) {
54
+ throw new Error("Recent State Flow patches may contain object-valued artifacts, contract, and working plus a session response string");
55
+ }
56
+ }
57
+ }
58
+
59
+ export function validateRecentTransition(value: unknown): asserts value is RecentTransition {
60
+ if (!isObject(value) || Object.keys(value).sort().join(",") !== "at,id,transitions") throw new Error("Recent State Flow transition has an invalid envelope");
61
+ if (typeof value.id !== "string" || value.id.length === 0) throw new Error("Recent State Flow transition must have a non-empty id");
62
+ if (!Number.isSafeInteger(value.at) || (value.at as number) < 0) throw new Error("Recent State Flow transition must have a safe non-negative position");
63
+ if (!Array.isArray(value.transitions) || value.transitions.length === 0 || !isJsonValue(value.transitions)) throw new Error("Recent State Flow transition must contain semantic patches");
64
+ const seen = new Set<StateScope>();
65
+ for (const transition of value.transitions) {
66
+ validateScopedPatch(transition);
67
+ if (seen.has(transition.scope)) throw new Error(`Duplicate recent State Flow transition scope: ${transition.scope}`);
68
+ seen.add(transition.scope);
69
+ }
70
+ }
71
+
72
+ export function createAcceptedTransition(currentStates: ScopedStates, nextStates: ScopedStates, id?: string): AcceptedTransition | undefined {
73
+ const transitions: RecentScopePatch[] = [];
74
+ for (const scope of SCOPES) {
75
+ const patch = replayPatch(currentStates[scope], nextStates[scope]);
76
+ if (Object.keys(patch).length > 0) transitions.push({ scope, patch });
77
+ }
78
+ if (transitions.length === 0) return undefined;
79
+ return { id: id ?? randomUUID(), transitions };
80
+ }
81
+
82
+ /** Preserve the configured per-scope budget, filtering in selected-lineage order. */
83
+ export function projectRecentTransitionsWithLimit(limit: number, lineage: readonly RecentTransition[]): RecentTransitionWindow {
84
+ if (!Number.isSafeInteger(limit) || limit < 0 || limit > RECENT_TRANSITION_LIMIT) {
85
+ throw new Error(`Recent State Flow transition limit must be an integer from 0 to ${RECENT_TRANSITION_LIMIT}`);
86
+ }
87
+ const remaining = { global: limit, cwd: limit, session: limit };
88
+ const result: RecentTransitionWindow = [];
89
+ for (let index = lineage.length - 1; index >= 0; index--) {
90
+ const record = lineage[index]!;
91
+ const transitions = record.transitions.filter(({ scope }) => remaining[scope]-- > 0);
92
+ if (transitions.length) result.push({ ...record, transitions });
93
+ }
94
+ return structuredClone(result.reverse());
95
+ }
@@ -38,6 +38,30 @@ export function canonicalJson(value: JsonValue | unknown): string {
38
38
  return JSON.stringify(orderValue(value));
39
39
  }
40
40
 
41
+ export function sameJson(left: JsonValue | unknown, right: JsonValue | unknown): boolean {
42
+ if (left === right) {
43
+ if (!isJsonValue(left)) throw new Error("Values must be finite, acyclic JSON data");
44
+ return true;
45
+ }
46
+ if (!isJsonValue(left) || !isJsonValue(right)) throw new Error("Values must be finite, acyclic JSON data");
47
+ return equalJsonValues(left, right);
48
+ }
49
+
50
+ function equalJsonValues(left: JsonValue, right: JsonValue): boolean {
51
+ if (left === right) return true;
52
+ if (Array.isArray(left) || Array.isArray(right)) {
53
+ return Array.isArray(left) && Array.isArray(right) && left.length === right.length
54
+ && left.every((value, index) => equalJsonValues(value, right[index]!));
55
+ }
56
+ if (isObject(left) || isObject(right)) {
57
+ if (!isObject(left) || !isObject(right)) return false;
58
+ const keys = Object.keys(left);
59
+ return keys.length === Object.keys(right).length
60
+ && keys.every((key) => Object.hasOwn(right, key) && equalJsonValues(left[key]!, right[key]!));
61
+ }
62
+ return false;
63
+ }
64
+
41
65
  export function hashJson(value: JsonValue | unknown): string {
42
66
  return createHash("sha256").update(canonicalJson(value)).digest("hex");
43
67
  }
@@ -0,0 +1,141 @@
1
+ import {
2
+ classifyArtifactFreshness,
3
+ type ArtifactMetadata,
4
+ type ArtifactRegistry,
5
+ type ArtifactSourceIdentity,
6
+ } from "./artifact.ts";
7
+ import type { ArtifactSourceCandidate } from "./discovery.ts";
8
+ import { isObject } from "./json.ts";
9
+
10
+ export const DEFAULT_ARTIFACT_MAINTENANCE_MAX_READS = 1;
11
+ export const DEFAULT_ARTIFACT_MAINTENANCE_MAX_SOURCE_BYTES = 16 * 1024;
12
+ export const DEFAULT_ARTIFACT_MAINTENANCE_MINIMUM_AGE_MS = 30 * 24 * 60 * 60 * 1_000;
13
+
14
+ export interface ArtifactMaintenanceOptions {
15
+ /** Stable cycle time supplied by the caller. */
16
+ now?: Date | number | string;
17
+ /** A source must be at least this old. Missing/unparseable timestamps rank as oldest. */
18
+ minimumAgeMs?: number;
19
+ /** Strict source-read count ceiling for this cycle. */
20
+ maxReads?: number;
21
+ /** Strict source-byte ceiling; bytes conservatively upper-bound source tokenizer input. */
22
+ maxSourceBytes?: number;
23
+ }
24
+
25
+ export interface ArtifactMaintenanceRequest extends ArtifactSourceIdentity {
26
+ reason: "maintenance";
27
+ sourceBytes: number;
28
+ }
29
+
30
+ export interface ArtifactMaintenancePlan {
31
+ requiresCompilation: ArtifactMaintenanceRequest[];
32
+ deferred: ArtifactMaintenanceRequest[];
33
+ budget: {
34
+ maxReads: number;
35
+ maxSourceBytes: number;
36
+ usedReads: number;
37
+ usedSourceBytes: number;
38
+ };
39
+ }
40
+
41
+ function nonNegativeSafeInteger(value: number, name: string): number {
42
+ if (!Number.isSafeInteger(value) || value < 0) throw new Error(`${name} must be a non-negative safe integer`);
43
+ return value;
44
+ }
45
+
46
+ function cycleTime(value: Date | number | string | undefined): number {
47
+ const timestamp = value === undefined
48
+ ? Date.now()
49
+ : value instanceof Date
50
+ ? value.getTime()
51
+ : typeof value === "number"
52
+ ? value
53
+ : Date.parse(value);
54
+ if (!Number.isFinite(timestamp)) throw new Error("Artifact maintenance cycle time must be valid");
55
+ return timestamp;
56
+ }
57
+
58
+ function compiledTime(metadata: ArtifactMetadata): number {
59
+ if (typeof metadata.compiled_at !== "string") return Number.NEGATIVE_INFINITY;
60
+ const timestamp = Date.parse(metadata.compiled_at);
61
+ return Number.isFinite(timestamp) ? timestamp : Number.NEGATIVE_INFINITY;
62
+ }
63
+
64
+ function request(source: ArtifactSourceCandidate): ArtifactMaintenanceRequest {
65
+ return {
66
+ path: source.path,
67
+ hash: source.hash,
68
+ reason: "maintenance",
69
+ sourceBytes: source.bytes,
70
+ };
71
+ }
72
+
73
+ /**
74
+ * Select a bounded, oldest-first maintenance cohort from otherwise fresh artifacts.
75
+ *
76
+ * This planner is opt-in and side-effect free. Correctness invalidations remain the
77
+ * responsibility of planArtifactInvalidation; they are never displaced by maintenance.
78
+ */
79
+ export function planArtifactMaintenance(
80
+ sources: readonly ArtifactSourceCandidate[],
81
+ registry: Readonly<Record<string, unknown>>,
82
+ compiler: string,
83
+ options: ArtifactMaintenanceOptions = {},
84
+ ): ArtifactMaintenancePlan {
85
+ if (!isObject(registry)) throw new Error("Artifacts must be a path-keyed JSON object");
86
+ const now = cycleTime(options.now);
87
+ const minimumAgeMs = nonNegativeSafeInteger(
88
+ options.minimumAgeMs ?? DEFAULT_ARTIFACT_MAINTENANCE_MINIMUM_AGE_MS,
89
+ "Artifact maintenance minimum age",
90
+ );
91
+ const maxReads = nonNegativeSafeInteger(
92
+ options.maxReads ?? DEFAULT_ARTIFACT_MAINTENANCE_MAX_READS,
93
+ "Artifact maintenance read budget",
94
+ );
95
+ const maxSourceBytes = nonNegativeSafeInteger(
96
+ options.maxSourceBytes ?? DEFAULT_ARTIFACT_MAINTENANCE_MAX_SOURCE_BYTES,
97
+ "Artifact maintenance source-byte budget",
98
+ );
99
+
100
+ const seen = new Set<string>();
101
+ const eligible: { source: ArtifactSourceCandidate; compiledAt: number }[] = [];
102
+ for (const source of sources) {
103
+ if (seen.has(source.path)) throw new Error(`Duplicate artifact source path: ${source.path}`);
104
+ seen.add(source.path);
105
+ nonNegativeSafeInteger(source.bytes, `Artifact source bytes at ${source.path}`);
106
+ const metadata = Object.hasOwn(registry, source.path) ? registry[source.path] : undefined;
107
+ const freshness = classifyArtifactFreshness(source, metadata, compiler);
108
+ if (freshness.kind !== "fresh") continue;
109
+ const compiledAt = compiledTime(metadata as ArtifactMetadata);
110
+ if (compiledAt !== Number.NEGATIVE_INFINITY && now - compiledAt < minimumAgeMs) continue;
111
+ eligible.push({ source, compiledAt });
112
+ }
113
+ eligible.sort((left, right) => {
114
+ if (left.compiledAt !== right.compiledAt) return left.compiledAt - right.compiledAt;
115
+ return left.source.path < right.source.path ? -1 : left.source.path > right.source.path ? 1 : 0;
116
+ });
117
+
118
+ const requiresCompilation: ArtifactMaintenanceRequest[] = [];
119
+ const deferred: ArtifactMaintenanceRequest[] = [];
120
+ let usedSourceBytes = 0;
121
+ for (const candidate of eligible) {
122
+ const next = request(candidate.source);
123
+ if (requiresCompilation.length >= maxReads
124
+ || candidate.source.bytes > maxSourceBytes - usedSourceBytes) {
125
+ deferred.push(next);
126
+ continue;
127
+ }
128
+ requiresCompilation.push(next);
129
+ usedSourceBytes += candidate.source.bytes;
130
+ }
131
+ return {
132
+ requiresCompilation,
133
+ deferred,
134
+ budget: {
135
+ maxReads,
136
+ maxSourceBytes,
137
+ usedReads: requiresCompilation.length,
138
+ usedSourceBytes,
139
+ },
140
+ };
141
+ }
@@ -0,0 +1,52 @@
1
+ import { isObject } from "./json.ts";
2
+ import type { MaterializedState, ScopedStates, StateScope } from "./state.ts";
3
+
4
+ export const MEMORY_PROMOTIONS_KEY = "memory_promotions";
5
+ export const MEMORY_PROMOTION_STATUSES = ["pending", "accepted", "failed", "unknown"] as const;
6
+ export type MemoryPromotionStatus = typeof MEMORY_PROMOTION_STATUSES[number];
7
+
8
+ export interface MemoryPromotionDiagnostic {
9
+ id: string;
10
+ status: MemoryPromotionStatus | "invalid";
11
+ owner?: string;
12
+ pointer?: string;
13
+ revision?: string;
14
+ error?: string;
15
+ }
16
+
17
+ function nonEmpty(value: unknown): string | undefined {
18
+ return typeof value === "string" && value.trim().length > 0 ? value : undefined;
19
+ }
20
+
21
+ /** Inspect the generic State Flow promotion convention without importing an external owner's schema. */
22
+ export function inspectMemoryPromotions(globalState: MaterializedState): MemoryPromotionDiagnostic[] {
23
+ const records = globalState.working[MEMORY_PROMOTIONS_KEY];
24
+ if (records === undefined) return [];
25
+ if (!isObject(records)) return [{ id: MEMORY_PROMOTIONS_KEY, status: "invalid", error: "promotion registry is not an object" }];
26
+ return Object.entries(records).sort(([left], [right]) => left.localeCompare(right)).map(([id, value]) => {
27
+ if (!isObject(value)) return { id, status: "invalid", error: "promotion record is not an object" };
28
+ const owner = nonEmpty(value.owner);
29
+ const pointer = nonEmpty(value.pointer);
30
+ const revision = nonEmpty(value.revision);
31
+ const error = nonEmpty(value.error);
32
+ const status = typeof value.status === "string" && (MEMORY_PROMOTION_STATUSES as readonly string[]).includes(value.status)
33
+ ? value.status as MemoryPromotionStatus
34
+ : undefined;
35
+ if (!status || !owner) return { id, status: "invalid", ...(owner ? { owner } : {}), ...(pointer ? { pointer } : {}), ...(revision ? { revision } : {}), error: error ?? "promotion status/owner is invalid" };
36
+ if (status === "accepted" && (!pointer || !revision)) return { id, status: "invalid", owner, ...(pointer ? { pointer } : {}), ...(revision ? { revision } : {}), error: "accepted promotion requires pointer and revision" };
37
+ return { id, status, owner, ...(pointer ? { pointer } : {}), ...(revision ? { revision } : {}), ...(error ? { error } : {}) };
38
+ });
39
+ }
40
+
41
+ function hasSemanticMemory(state: MaterializedState, ignorePromotions: boolean): boolean {
42
+ if (Object.keys(state.contract).length > 0) return true;
43
+ return Object.keys(state.working).some((key) => !ignorePromotions || key !== MEMORY_PROMOTIONS_KEY);
44
+ }
45
+
46
+ export function retainedMemoryScopes(states: ScopedStates): Record<StateScope, boolean> {
47
+ return {
48
+ global: hasSemanticMemory(states.global, true),
49
+ cwd: hasSemanticMemory(states.cwd, false),
50
+ session: hasSemanticMemory(states.session, false),
51
+ };
52
+ }
@@ -0,0 +1,88 @@
1
+ // Domain: conservative current-snapshot migration planning; Git owns publication and rollback.
2
+ import { randomUUID } from "node:crypto";
3
+ import { join, resolve } from "node:path";
4
+ import {
5
+ captureOwnedFileBases,
6
+ cwdScopePaths,
7
+ parseScopeStream,
8
+ parseStateSource,
9
+ serializeScopeStream,
10
+ sessionScopePaths,
11
+ type DurableFileBase,
12
+ type OwnedFileUpdate,
13
+ } from "./durable.ts";
14
+ import type { StateScope } from "./state.ts";
15
+ import type { ScopeStream } from "./temporal.ts";
16
+
17
+ export interface LegacyStorageMigration {
18
+ bases: DurableFileBase[];
19
+ updates: OwnedFileUpdate[];
20
+ scopes: StateScope[];
21
+ }
22
+
23
+ /** Read-only activation eligibility, before global migration or any repository publication. */
24
+ export function hasCwdMaterialization(cwd: string, repositoryRoot: string): boolean {
25
+ const root = resolve(repositoryRoot);
26
+ const directory = cwdScopePaths(cwd, root).directory;
27
+ const [legacy, checkpoint, patches] = captureOwnedFileBases([
28
+ join(directory, "state.json"), join(directory, "checkpoint.json"), join(directory, "patches.jsonl"),
29
+ ], root);
30
+ if (checkpoint!.content !== undefined) {
31
+ if (legacy!.content !== undefined) throw new Error(`Ambiguous State Flow storage has both current and checkpoint snapshots: ${directory}`);
32
+ return parseScopeStream(checkpoint!.content, patches!.content, "cwd", cwd) !== undefined;
33
+ }
34
+ if (legacy!.content !== undefined) return parseStateSource(legacy!.content, legacy!.path) !== undefined;
35
+ if (patches!.content !== undefined) throw new Error(`State Flow tail has no provable snapshot: ${directory}`);
36
+ return false;
37
+ }
38
+
39
+ /** Plan from current scope snapshots only; old explanatory journals are never replay input. */
40
+ export function planLegacyStorageMigration(
41
+ cwd: string,
42
+ sessionId: string,
43
+ repositoryRoot: string,
44
+ origin: string = randomUUID(),
45
+ sessionKey = sessionId,
46
+ ): LegacyStorageMigration {
47
+ const root = resolve(repositoryRoot);
48
+ const directories: Record<StateScope, string> = {
49
+ global: root,
50
+ cwd: cwdScopePaths(cwd, root).directory,
51
+ session: sessionScopePaths(cwd, sessionId, root, sessionKey).directory,
52
+ };
53
+ const paths = Object.values(directories).flatMap((directory) => [
54
+ join(directory, "state.json"), join(directory, "checkpoint.json"), join(directory, "patches.jsonl"),
55
+ ]);
56
+ const bases = captureOwnedFileBases(paths, root);
57
+ const byPath = new Map(bases.map((base) => [base.path, base]));
58
+ const updates: OwnedFileUpdate[] = [];
59
+ const scopes: StateScope[] = [];
60
+ for (const scope of ["global", "cwd", "session"] as const) {
61
+ const directory = directories[scope];
62
+ const legacy = byPath.get(join(directory, "state.json"))!;
63
+ const checkpoint = byPath.get(join(directory, "checkpoint.json"))!;
64
+ const patches = byPath.get(join(directory, "patches.jsonl"))!;
65
+ if (checkpoint.content !== undefined) {
66
+ if (legacy.content !== undefined) throw new Error(`Ambiguous State Flow storage has both current and checkpoint snapshots: ${directory}`);
67
+ parseScopeStream(checkpoint.content, patches.content, scope, scope === "cwd" ? cwd : undefined);
68
+ continue;
69
+ }
70
+ if (legacy.content === undefined) {
71
+ if (patches.content !== undefined) throw new Error(`State Flow tail has no provable snapshot: ${directory}`);
72
+ continue;
73
+ }
74
+ const state = parseStateSource(legacy.content, legacy.path)!;
75
+ const stream: ScopeStream = {
76
+ checkpoint: { through: { id: origin, position: 0, parent: null }, state },
77
+ patches: [],
78
+ };
79
+ const source = serializeScopeStream(stream, scope, scope === "cwd" ? cwd : undefined);
80
+ scopes.push(scope);
81
+ updates.push(
82
+ { path: checkpoint.path, content: source.checkpoint },
83
+ { path: patches.path, content: source.patches },
84
+ { path: legacy.path },
85
+ );
86
+ }
87
+ return { bases, updates, scopes };
88
+ }