@llblab/pi-kit 0.6.0 → 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.
- package/CHANGELOG.md +4 -0
- package/README.md +4 -4
- package/node_modules/@llblab/pi-actors/AGENTS.md +1 -1
- package/node_modules/@llblab/pi-actors/CHANGELOG.md +6 -0
- package/node_modules/@llblab/pi-actors/README.md +1 -1
- package/node_modules/@llblab/pi-actors/dist/lib/async-runs.d.ts +3 -0
- package/node_modules/@llblab/pi-actors/dist/lib/async-runs.js +14 -1
- package/node_modules/@llblab/pi-actors/dist/lib/command-templates.js +45 -3
- package/node_modules/@llblab/pi-actors/dist/lib/extension-runtime.js +1 -1
- package/node_modules/@llblab/pi-actors/dist/lib/observability.d.ts +16 -3
- package/node_modules/@llblab/pi-actors/dist/lib/observability.js +92 -7
- package/node_modules/@llblab/pi-actors/dist/lib/pi.d.ts +0 -1
- package/node_modules/@llblab/pi-actors/dist/lib/pi.js +15 -24
- package/node_modules/@llblab/pi-actors/dist/lib/run-delivery-lineage.d.ts +17 -0
- package/node_modules/@llblab/pi-actors/dist/lib/run-delivery-lineage.js +44 -0
- package/node_modules/@llblab/pi-actors/dist/lib/run-delivery.d.ts +4 -0
- package/node_modules/@llblab/pi-actors/dist/lib/run-delivery.js +102 -4
- package/node_modules/@llblab/pi-actors/dist/lib/run-ui-runtime.js +58 -39
- package/node_modules/@llblab/pi-actors/dist/lib/runtime.js +14 -6
- package/node_modules/@llblab/pi-actors/dist/skills/actors/SKILL.md +1 -1
- package/node_modules/@llblab/pi-actors/docs/async-runs.md +1 -1
- package/node_modules/@llblab/pi-actors/docs/coordinator-delivery.md +18 -23
- package/node_modules/@llblab/pi-actors/lib/async-runs.ts +18 -1
- package/node_modules/@llblab/pi-actors/lib/command-templates.ts +41 -3
- package/node_modules/@llblab/pi-actors/lib/extension-runtime.ts +1 -1
- package/node_modules/@llblab/pi-actors/lib/observability.ts +119 -5
- package/node_modules/@llblab/pi-actors/lib/pi.ts +15 -28
- package/node_modules/@llblab/pi-actors/lib/run-delivery-lineage.ts +68 -0
- package/node_modules/@llblab/pi-actors/lib/run-delivery.ts +120 -4
- package/node_modules/@llblab/pi-actors/lib/run-ui-runtime.ts +69 -44
- package/node_modules/@llblab/pi-actors/lib/runtime.ts +17 -6
- package/node_modules/@llblab/pi-actors/package.json +1 -1
- package/node_modules/@llblab/pi-actors/skills/actors/SKILL.md +1 -1
- package/node_modules/@llblab/pi-state-flow/AGENTS.md +33 -12
- package/node_modules/@llblab/pi-state-flow/BACKLOG.md +125 -2
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +87 -45
- package/node_modules/@llblab/pi-state-flow/README.md +203 -107
- package/node_modules/@llblab/pi-state-flow/docs/README.md +4 -0
- package/node_modules/@llblab/pi-state-flow/docs/architecture.md +188 -0
- package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +42 -0
- package/node_modules/@llblab/pi-state-flow/index.ts +164 -2
- package/node_modules/@llblab/pi-state-flow/lib/acquisition.ts +138 -0
- package/node_modules/@llblab/pi-state-flow/lib/artifact.ts +273 -0
- package/node_modules/@llblab/pi-state-flow/lib/config.ts +48 -0
- package/node_modules/@llblab/pi-state-flow/lib/context.ts +18 -5
- package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +268 -0
- package/node_modules/@llblab/pi-state-flow/lib/discovery.ts +117 -0
- package/node_modules/@llblab/pi-state-flow/lib/durable.ts +562 -0
- package/node_modules/@llblab/pi-state-flow/lib/episode.ts +24 -12
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +606 -70
- package/node_modules/@llblab/pi-state-flow/lib/git.ts +666 -0
- package/node_modules/@llblab/pi-state-flow/lib/history.ts +95 -0
- package/node_modules/@llblab/pi-state-flow/lib/json.ts +24 -0
- package/node_modules/@llblab/pi-state-flow/lib/maintenance.ts +141 -0
- package/node_modules/@llblab/pi-state-flow/lib/memory.ts +52 -0
- package/node_modules/@llblab/pi-state-flow/lib/migration.ts +88 -0
- package/node_modules/@llblab/pi-state-flow/lib/publication.ts +296 -0
- package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +23 -7
- package/node_modules/@llblab/pi-state-flow/lib/rehydration.ts +79 -0
- package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +264 -0
- package/node_modules/@llblab/pi-state-flow/lib/session.ts +6 -0
- package/node_modules/@llblab/pi-state-flow/lib/skills.ts +99 -7
- package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +305 -48
- package/node_modules/@llblab/pi-state-flow/lib/state.ts +74 -7
- package/node_modules/@llblab/pi-state-flow/lib/status.ts +125 -6
- package/node_modules/@llblab/pi-state-flow/lib/storage.ts +196 -0
- package/node_modules/@llblab/pi-state-flow/lib/temporal.ts +233 -0
- package/node_modules/@llblab/pi-state-flow/lib/terminal.ts +70 -24
- package/node_modules/@llblab/pi-state-flow/lib/transition.ts +254 -29
- package/node_modules/@llblab/pi-state-flow/package.json +8 -2
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +128 -0
- package/node_modules/@llblab/pi-telegram/AGENTS.md +13 -8
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +20 -3
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +13 -0
- package/node_modules/@llblab/pi-telegram/README.md +12 -8
- package/node_modules/@llblab/pi-telegram/docs/README.md +1 -0
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +220 -18
- package/node_modules/@llblab/pi-telegram/docs/generative-apps.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +70 -19
- package/node_modules/@llblab/pi-telegram/docs/outbound.md +6 -6
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +12 -5
- package/node_modules/@llblab/pi-telegram/docs/ui-style.md +3 -1
- package/node_modules/@llblab/pi-telegram/index.ts +4 -1418
- package/node_modules/@llblab/pi-telegram/lib/agent-messages.ts +6 -3
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +28 -1
- package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +600 -135
- package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +962 -55
- package/node_modules/@llblab/pi-telegram/lib/bus.ts +350 -26
- package/node_modules/@llblab/pi-telegram/lib/channel-posts.ts +544 -0
- package/node_modules/@llblab/pi-telegram/lib/commands.ts +234 -11
- package/node_modules/@llblab/pi-telegram/lib/config.ts +177 -24
- package/node_modules/@llblab/pi-telegram/lib/extension.ts +1792 -0
- package/node_modules/@llblab/pi-telegram/lib/generative-apps.ts +20 -2
- package/node_modules/@llblab/pi-telegram/lib/journal.ts +2184 -126
- package/node_modules/@llblab/pi-telegram/lib/locks.ts +38 -1
- package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +152 -13
- package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +51 -10
- package/node_modules/@llblab/pi-telegram/lib/paths.ts +29 -0
- package/node_modules/@llblab/pi-telegram/lib/polling.ts +85 -17
- package/node_modules/@llblab/pi-telegram/lib/prompts.ts +5 -2
- package/node_modules/@llblab/pi-telegram/lib/queue.ts +52 -18
- package/node_modules/@llblab/pi-telegram/lib/replies.ts +2 -2
- package/node_modules/@llblab/pi-telegram/lib/routing.ts +305 -112
- package/node_modules/@llblab/pi-telegram/lib/status.ts +10 -0
- package/node_modules/@llblab/pi-telegram/lib/sync.ts +308 -39
- package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +279 -4
- package/node_modules/@llblab/pi-telegram/lib/thread-cleanup-manager.ts +664 -0
- package/node_modules/@llblab/pi-telegram/lib/thread-display.ts +226 -0
- package/node_modules/@llblab/pi-telegram/lib/thread-naming.ts +118 -0
- package/node_modules/@llblab/pi-telegram/lib/threads.ts +1686 -129
- package/node_modules/@llblab/pi-telegram/lib/updates.ts +1319 -97
- package/node_modules/@llblab/pi-telegram/lib/workspace-admission.ts +1643 -0
- package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +968 -0
- package/node_modules/@llblab/pi-telegram/lib/workspace-slots.ts +84 -0
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/screenshot.png +0 -0
- package/node_modules/@llblab/pi-telegram/scripts/measure-bus.mjs +83 -0
- package/node_modules/@llblab/pi-telegram/scripts/measure-workspace.mjs +101 -0
- package/node_modules/@llblab/{skills → pi-telegram/skills}/show-me/SKILL.md +28 -6
- package/node_modules/@llblab/pi-telegram/skills/show-me/references/telegram-surfaces.md +43 -0
- package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/delivery-and-threads.md +1 -1
- package/node_modules/@llblab/skills/package.json +2 -3
- package/package.json +6 -5
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { containsNull, isJsonValue, isObject, type JsonObject } from "./json.ts";
|
|
3
|
+
|
|
4
|
+
const SHA256_PATTERN = /^sha256:[0-9a-f]{64}$/;
|
|
5
|
+
|
|
6
|
+
/** Current compiler protocol for ordinary source artifacts such as Knowledge Markdown. */
|
|
7
|
+
export const ORDINARY_ARTIFACT_COMPILER = "artifact-v1";
|
|
8
|
+
|
|
9
|
+
/** Source-addressed metadata retained after artifact compilation. */
|
|
10
|
+
export type ArtifactMetadata = JsonObject & {
|
|
11
|
+
description: string;
|
|
12
|
+
hash: string;
|
|
13
|
+
compiler: string;
|
|
14
|
+
compiled_at?: string;
|
|
15
|
+
compilation?: JsonObject;
|
|
16
|
+
kind?: string;
|
|
17
|
+
tags?: string[];
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** Artifact source paths are the canonical registry keys. */
|
|
21
|
+
export type ArtifactRegistry = JsonObject & Record<string, ArtifactMetadata>;
|
|
22
|
+
|
|
23
|
+
/** Source identity is sufficient for freshness decisions; it never contains the source body. */
|
|
24
|
+
export interface ArtifactSourceIdentity {
|
|
25
|
+
path: string;
|
|
26
|
+
hash: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type ArtifactInvalidationReason =
|
|
30
|
+
| "new"
|
|
31
|
+
| "source-changed"
|
|
32
|
+
| "compiler-changed"
|
|
33
|
+
| "invalid-metadata"
|
|
34
|
+
| "explicit-refresh";
|
|
35
|
+
|
|
36
|
+
export type ArtifactFreshness =
|
|
37
|
+
| { kind: "fresh" }
|
|
38
|
+
| { kind: "requires-compilation"; reason: ArtifactInvalidationReason };
|
|
39
|
+
|
|
40
|
+
export interface ArtifactInvalidationRequest extends ArtifactSourceIdentity {
|
|
41
|
+
reason: ArtifactInvalidationReason;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ArtifactInvalidationPlan {
|
|
45
|
+
fresh: ArtifactSourceIdentity[];
|
|
46
|
+
requiresCompilation: ArtifactInvalidationRequest[];
|
|
47
|
+
removed: string[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface ArtifactInvalidationOptions {
|
|
51
|
+
/** Refresh every source, or only paths in the supplied set. */
|
|
52
|
+
explicitRefresh?: boolean | ReadonlySet<string>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Compiler-owned fields before State Flow attaches trusted source freshness metadata. */
|
|
56
|
+
export type ArtifactCompilerOutput = JsonObject & {
|
|
57
|
+
description: string;
|
|
58
|
+
compiled_at?: string;
|
|
59
|
+
compilation?: JsonObject;
|
|
60
|
+
kind?: string;
|
|
61
|
+
tags?: string[];
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export interface ArtifactCompilationUpdate {
|
|
65
|
+
source: ArtifactSourceIdentity;
|
|
66
|
+
compiler: string;
|
|
67
|
+
output: ArtifactCompilerOutput;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function hashArtifactSource(source: string | Uint8Array): string {
|
|
71
|
+
return `sha256:${createHash("sha256").update(source).digest("hex")}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function isArtifactHash(value: unknown): value is string {
|
|
75
|
+
return typeof value === "string" && SHA256_PATTERN.test(value);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function validateArtifactMetadata(value: unknown, path = "<unknown>"): asserts value is ArtifactMetadata {
|
|
79
|
+
if (!isObject(value) || !isJsonValue(value)) {
|
|
80
|
+
throw new Error(`Artifact metadata at ${path} must be finite, acyclic JSON data`);
|
|
81
|
+
}
|
|
82
|
+
if (containsNull(value)) throw new Error(`Artifact metadata at ${path} cannot contain null`);
|
|
83
|
+
if (typeof value.description !== "string" || value.description.trim().length === 0) {
|
|
84
|
+
throw new Error(`Artifact metadata at ${path} must have a non-empty description`);
|
|
85
|
+
}
|
|
86
|
+
if (!isArtifactHash(value.hash)) {
|
|
87
|
+
throw new Error(`Artifact metadata at ${path} must have a sha256:<64 lowercase hex characters> hash`);
|
|
88
|
+
}
|
|
89
|
+
if (typeof value.compiler !== "string" || value.compiler.trim().length === 0) {
|
|
90
|
+
throw new Error(`Artifact metadata at ${path} must have a non-empty compiler revision`);
|
|
91
|
+
}
|
|
92
|
+
if (Object.hasOwn(value, "compiled_at") && typeof value.compiled_at !== "string") {
|
|
93
|
+
throw new Error(`Artifact metadata at ${path} compiled_at must be a string`);
|
|
94
|
+
}
|
|
95
|
+
if (Object.hasOwn(value, "compilation") && !isObject(value.compilation)) {
|
|
96
|
+
throw new Error(`Artifact metadata at ${path} compilation must be an object`);
|
|
97
|
+
}
|
|
98
|
+
if (Object.hasOwn(value, "kind")
|
|
99
|
+
&& (typeof value.kind !== "string" || value.kind.trim().length === 0)) {
|
|
100
|
+
throw new Error(`Artifact metadata at ${path} kind must be a non-empty string`);
|
|
101
|
+
}
|
|
102
|
+
if (Object.hasOwn(value, "tags") && (!Array.isArray(value.tags)
|
|
103
|
+
|| value.tags.some((tag) => typeof tag !== "string" || tag.trim().length === 0 || tag !== tag.trim())
|
|
104
|
+
|| new Set(value.tags).size !== value.tags.length)) {
|
|
105
|
+
throw new Error(`Artifact metadata at ${path} tags must be unique non-empty trimmed strings`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function isArtifactMetadata(value: unknown): value is ArtifactMetadata {
|
|
110
|
+
try {
|
|
111
|
+
validateArtifactMetadata(value);
|
|
112
|
+
return true;
|
|
113
|
+
} catch {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function validateArtifactRegistry(value: unknown): asserts value is ArtifactRegistry {
|
|
119
|
+
if (!isObject(value)) throw new Error("Artifacts must be a path-keyed JSON object");
|
|
120
|
+
for (const [path, metadata] of Object.entries(value)) {
|
|
121
|
+
if (path.trim().length === 0) throw new Error("Artifact path keys must be non-empty");
|
|
122
|
+
validateArtifactMetadata(metadata, path);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function selectArtifactsByTags(
|
|
127
|
+
registry: ArtifactRegistry,
|
|
128
|
+
tags: readonly string[],
|
|
129
|
+
match: "all" | "any" = "all",
|
|
130
|
+
): string[] {
|
|
131
|
+
validateArtifactRegistry(registry);
|
|
132
|
+
if (tags.length === 0 || tags.some((tag) => typeof tag !== "string" || tag.trim().length === 0 || tag !== tag.trim())) {
|
|
133
|
+
throw new Error("Artifact tag query requires one or more non-empty trimmed strings");
|
|
134
|
+
}
|
|
135
|
+
const requested = new Set(tags);
|
|
136
|
+
return Object.entries(registry)
|
|
137
|
+
.filter(([, metadata]) => {
|
|
138
|
+
const available = new Set(metadata.tags ?? []);
|
|
139
|
+
return match === "all"
|
|
140
|
+
? [...requested].every((tag) => available.has(tag))
|
|
141
|
+
: [...requested].some((tag) => available.has(tag));
|
|
142
|
+
})
|
|
143
|
+
.map(([path]) => path)
|
|
144
|
+
.sort();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function isArtifactRegistry(value: unknown): value is ArtifactRegistry {
|
|
148
|
+
try {
|
|
149
|
+
validateArtifactRegistry(value);
|
|
150
|
+
return true;
|
|
151
|
+
} catch {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function validateSourceIdentity(source: ArtifactSourceIdentity): void {
|
|
157
|
+
if (typeof source.path !== "string" || source.path.trim().length === 0) {
|
|
158
|
+
throw new Error("Artifact source path must be non-empty");
|
|
159
|
+
}
|
|
160
|
+
if (!isArtifactHash(source.hash)) {
|
|
161
|
+
throw new Error(`Artifact source at ${source.path} must have a sha256:<64 lowercase hex characters> hash`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function validateCompilerRevision(compiler: string, path?: string): void {
|
|
166
|
+
if (typeof compiler !== "string" || compiler.trim().length === 0) {
|
|
167
|
+
throw new Error(path === undefined
|
|
168
|
+
? "Artifact compiler revision must be non-empty"
|
|
169
|
+
: `Artifact compiler revision at ${path} must be non-empty`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function refreshRequested(path: string, explicitRefresh: boolean | ReadonlySet<string> | undefined): boolean {
|
|
174
|
+
if (explicitRefresh === true) return true;
|
|
175
|
+
if (!explicitRefresh || typeof explicitRefresh !== "object" || typeof explicitRefresh.has !== "function") return false;
|
|
176
|
+
return explicitRefresh.has(path);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Classify freshness from source identity and metadata without acquiring the source body. */
|
|
180
|
+
export function classifyArtifactFreshness(
|
|
181
|
+
source: ArtifactSourceIdentity,
|
|
182
|
+
metadata: unknown,
|
|
183
|
+
compiler: string,
|
|
184
|
+
explicitRefresh = false,
|
|
185
|
+
): ArtifactFreshness {
|
|
186
|
+
validateSourceIdentity(source);
|
|
187
|
+
validateCompilerRevision(compiler);
|
|
188
|
+
if (metadata === undefined) return { kind: "requires-compilation", reason: "new" };
|
|
189
|
+
if (!isArtifactMetadata(metadata)) return { kind: "requires-compilation", reason: "invalid-metadata" };
|
|
190
|
+
if (metadata.hash !== source.hash) return { kind: "requires-compilation", reason: "source-changed" };
|
|
191
|
+
if (metadata.compiler !== compiler) return { kind: "requires-compilation", reason: "compiler-changed" };
|
|
192
|
+
if (explicitRefresh) return { kind: "requires-compilation", reason: "explicit-refresh" };
|
|
193
|
+
return { kind: "fresh" };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Produce a deterministic acquisition plan from path/hash candidates alone. */
|
|
197
|
+
export function planArtifactInvalidation(
|
|
198
|
+
sources: readonly ArtifactSourceIdentity[],
|
|
199
|
+
registry: Readonly<Record<string, unknown>>,
|
|
200
|
+
compiler: string,
|
|
201
|
+
options: ArtifactInvalidationOptions = {},
|
|
202
|
+
): ArtifactInvalidationPlan {
|
|
203
|
+
if (!isObject(registry)) throw new Error("Artifacts must be a path-keyed JSON object");
|
|
204
|
+
validateCompilerRevision(compiler);
|
|
205
|
+
const ordered = [...sources].sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
|
|
206
|
+
const seen = new Set<string>();
|
|
207
|
+
const fresh: ArtifactSourceIdentity[] = [];
|
|
208
|
+
const requiresCompilation: ArtifactInvalidationRequest[] = [];
|
|
209
|
+
for (const source of ordered) {
|
|
210
|
+
if (seen.has(source.path)) throw new Error(`Duplicate artifact source path: ${source.path}`);
|
|
211
|
+
seen.add(source.path);
|
|
212
|
+
const metadata = Object.hasOwn(registry, source.path) ? registry[source.path] : undefined;
|
|
213
|
+
const freshness = classifyArtifactFreshness(
|
|
214
|
+
source,
|
|
215
|
+
metadata,
|
|
216
|
+
compiler,
|
|
217
|
+
refreshRequested(source.path, options.explicitRefresh),
|
|
218
|
+
);
|
|
219
|
+
const identity = { path: source.path, hash: source.hash };
|
|
220
|
+
if (freshness.kind === "fresh") fresh.push(identity);
|
|
221
|
+
else requiresCompilation.push({ ...identity, reason: freshness.reason });
|
|
222
|
+
}
|
|
223
|
+
const removed = Object.keys(registry).filter((path) => !seen.has(path)).sort();
|
|
224
|
+
return { fresh, requiresCompilation, removed };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function compilationMetadata(update: ArtifactCompilationUpdate): ArtifactMetadata {
|
|
228
|
+
validateSourceIdentity(update.source);
|
|
229
|
+
validateCompilerRevision(update.compiler, update.source.path);
|
|
230
|
+
if (!isObject(update.output) || Object.hasOwn(update.output, "hash") || Object.hasOwn(update.output, "compiler")) {
|
|
231
|
+
throw new Error(`Artifact compiler output at ${update.source.path} cannot set runtime-owned hash or compiler fields`);
|
|
232
|
+
}
|
|
233
|
+
const metadata = {
|
|
234
|
+
...structuredClone(update.output),
|
|
235
|
+
hash: update.source.hash,
|
|
236
|
+
compiler: update.compiler,
|
|
237
|
+
};
|
|
238
|
+
validateArtifactMetadata(metadata, update.source.path);
|
|
239
|
+
return metadata;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Validate and apply a whole compilation/removal cohort without mutating the prior registry. */
|
|
243
|
+
export function updateArtifactRegistry(
|
|
244
|
+
registry: ArtifactRegistry,
|
|
245
|
+
updates: readonly ArtifactCompilationUpdate[],
|
|
246
|
+
removed: readonly string[] = [],
|
|
247
|
+
): ArtifactRegistry {
|
|
248
|
+
validateArtifactRegistry(registry);
|
|
249
|
+
const compiled = new Map<string, ArtifactMetadata>();
|
|
250
|
+
for (const update of updates) {
|
|
251
|
+
if (compiled.has(update.source.path)) throw new Error(`Duplicate artifact compilation: ${update.source.path}`);
|
|
252
|
+
compiled.set(update.source.path, compilationMetadata(update));
|
|
253
|
+
}
|
|
254
|
+
const removals = new Set<string>();
|
|
255
|
+
for (const path of removed) {
|
|
256
|
+
if (typeof path !== "string" || path.trim().length === 0) throw new Error("Removed artifact paths must be non-empty");
|
|
257
|
+
if (removals.has(path)) throw new Error(`Duplicate artifact removal: ${path}`);
|
|
258
|
+
if (compiled.has(path)) throw new Error(`Artifact cannot be compiled and removed atomically: ${path}`);
|
|
259
|
+
removals.add(path);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const next = structuredClone(registry);
|
|
263
|
+
for (const path of removals) delete next[path];
|
|
264
|
+
for (const [path, metadata] of compiled) {
|
|
265
|
+
Object.defineProperty(next, path, {
|
|
266
|
+
value: metadata,
|
|
267
|
+
enumerable: true,
|
|
268
|
+
configurable: true,
|
|
269
|
+
writable: true,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
return next;
|
|
273
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Domain: agent-level State Flow configuration, independent of session runtime and semantic state.
|
|
2
|
+
import { lstatSync, readFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { dirname, resolve } from "node:path";
|
|
5
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { getDurableRepositoryRoot } from "./durable.ts";
|
|
7
|
+
import { isObject } from "./json.ts";
|
|
8
|
+
|
|
9
|
+
export interface StateFlowConfig {
|
|
10
|
+
directory: string;
|
|
11
|
+
autoStart: boolean;
|
|
12
|
+
remotePublication?: "off" | "turn-end" | "transition";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Read once at extension load/reload. Missing config uses defaults; invalid config never falls back. */
|
|
16
|
+
export function loadStateFlowConfig(agentDir = getAgentDir()): StateFlowConfig {
|
|
17
|
+
const path = resolve(agentDir, "state-flow.json");
|
|
18
|
+
const defaults: StateFlowConfig = {
|
|
19
|
+
directory: getDurableRepositoryRoot(agentDir),
|
|
20
|
+
autoStart: false,
|
|
21
|
+
};
|
|
22
|
+
let value: unknown;
|
|
23
|
+
try {
|
|
24
|
+
if (!lstatSync(path, { throwIfNoEntry: false })) return defaults;
|
|
25
|
+
value = JSON.parse(readFileSync(path, "utf8"));
|
|
26
|
+
} catch (error) {
|
|
27
|
+
throw new Error(`Cannot read State Flow configuration: ${path}`, { cause: error });
|
|
28
|
+
}
|
|
29
|
+
const allowed = new Set(["directory", "autoStart", "remotePublication"]);
|
|
30
|
+
if (!isObject(value) || Object.keys(value).some((key) => !allowed.has(key))) {
|
|
31
|
+
throw new Error(`State Flow configuration contains unknown settings: ${path}`);
|
|
32
|
+
}
|
|
33
|
+
if (Object.hasOwn(value, "autoStart") && typeof value.autoStart !== "boolean") throw new Error(`State Flow autoStart must be a boolean: ${path}`);
|
|
34
|
+
if (Object.hasOwn(value, "remotePublication") && value.remotePublication !== "off" && value.remotePublication !== "turn-end" && value.remotePublication !== "transition") {
|
|
35
|
+
throw new Error(`State Flow remotePublication must be off, turn-end, or transition: ${path}`);
|
|
36
|
+
}
|
|
37
|
+
if (Object.hasOwn(value, "directory") && (typeof value.directory !== "string" || !value.directory.trim() || value.directory.includes("\0"))) {
|
|
38
|
+
throw new Error(`State Flow directory must be a non-empty path: ${path}`);
|
|
39
|
+
}
|
|
40
|
+
const directory = value.directory as string | undefined;
|
|
41
|
+
if (directory?.startsWith("~") && directory !== "~" && !directory.startsWith("~/")) throw new Error(`State Flow directory supports ~ or ~/ paths, not named-user expansion: ${path}`);
|
|
42
|
+
const expanded = directory === "~" ? homedir() : directory?.startsWith("~/") ? resolve(homedir(), directory.slice(2)) : directory;
|
|
43
|
+
return {
|
|
44
|
+
directory: expanded === undefined ? defaults.directory : resolve(dirname(path), expanded),
|
|
45
|
+
autoStart: value.autoStart === true,
|
|
46
|
+
...(value.remotePublication === undefined ? {} : { remotePublication: value.remotePublication as "off" | "turn-end" | "transition" }),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import type { ArtifactInvalidationRequest } from "./artifact.ts";
|
|
3
|
+
import type { RecentTransitionWindow } from "./history.ts";
|
|
2
4
|
import { canonicalJson } from "./json.ts";
|
|
3
5
|
import type { Snapshot } from "./snapshot.ts";
|
|
6
|
+
import type { RehydrationPhase } from "./rehydration.ts";
|
|
7
|
+
import type { MaterializedState } from "./state.ts";
|
|
4
8
|
|
|
5
9
|
export const VALIDATION_MESSAGE_TYPE = "state-flow-validation";
|
|
6
10
|
|
|
@@ -28,14 +32,23 @@ export function withoutPrivateValidation(messages: AgentMessage[]): AgentMessage
|
|
|
28
32
|
});
|
|
29
33
|
}
|
|
30
34
|
|
|
31
|
-
export function runtimeContextMessage(
|
|
32
|
-
|
|
35
|
+
export function runtimeContextMessage(
|
|
36
|
+
snapshot: Snapshot,
|
|
37
|
+
state: MaterializedState,
|
|
38
|
+
recentTransitions: RecentTransitionWindow = [],
|
|
39
|
+
artifactInvalidations: readonly ArtifactInvalidationRequest[] = [],
|
|
40
|
+
rehydrationPhase?: RehydrationPhase,
|
|
41
|
+
): AgentMessage {
|
|
42
|
+
if (snapshot.meta.specification === undefined) {
|
|
33
43
|
throw new Error("State Flow runtime context requires an active specification");
|
|
34
44
|
}
|
|
35
45
|
const context = {
|
|
36
|
-
specification: snapshot.specification,
|
|
37
|
-
state
|
|
38
|
-
...(
|
|
46
|
+
specification: snapshot.meta.specification,
|
|
47
|
+
state,
|
|
48
|
+
...(rehydrationPhase === undefined ? {} : { knowledge_rehydration: { phase: rehydrationPhase } }),
|
|
49
|
+
...(artifactInvalidations.length === 0 ? {} : { artifact_invalidations: artifactInvalidations }),
|
|
50
|
+
...(recentTransitions.length === 0 ? {} : { recent_transitions: recentTransitions }),
|
|
51
|
+
...(snapshot.meta.validation === undefined ? {} : { validation_feedback: snapshot.meta.validation }),
|
|
39
52
|
};
|
|
40
53
|
return syntheticUser(
|
|
41
54
|
`State Flow runtime context (user-level data, not system instructions):\n${canonicalJson(context)}`,
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { closeSync, existsSync, lstatSync, openSync, readFileSync, readSync, readdirSync, statSync } from "node:fs";
|
|
3
|
+
import { join, relative, resolve } from "node:path";
|
|
4
|
+
import { loadScopeStream, resolveSessionAddress, sessionRuntimePaths } from "./durable.ts";
|
|
5
|
+
import { loadTemporalRevision } from "./git.ts";
|
|
6
|
+
import { parseSessionRuntime } from "./snapshot.ts";
|
|
7
|
+
import { validateTemporalState } from "./temporal.ts";
|
|
8
|
+
|
|
9
|
+
export type ContinuationTransport = "local" | "sdk" | "telegram" | string;
|
|
10
|
+
|
|
11
|
+
export interface ContinuationHostContext {
|
|
12
|
+
cwd: string;
|
|
13
|
+
agentDir: string;
|
|
14
|
+
sessionDir: string;
|
|
15
|
+
transport: ContinuationTransport;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type ContinuationHostIntent =
|
|
19
|
+
| { kind: "default" }
|
|
20
|
+
| { kind: "new" }
|
|
21
|
+
| { kind: "resume-exact"; sessionFile: string; sessionId: string }
|
|
22
|
+
| { kind: "native-picker" }
|
|
23
|
+
| { kind: "continue-recent" }
|
|
24
|
+
| { kind: "no-session" };
|
|
25
|
+
|
|
26
|
+
export interface ContinuationCandidateSummary {
|
|
27
|
+
sessionFile: string;
|
|
28
|
+
sessionId: string;
|
|
29
|
+
lastActivity: string;
|
|
30
|
+
reason: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type ContinuationRecommendation =
|
|
34
|
+
| { action: "resume"; sessionFile: string; sessionId: string; reason: "latest-enabled-state-flow" }
|
|
35
|
+
| { action: "choose"; candidates: ContinuationCandidateSummary[]; reason: "ambiguous" }
|
|
36
|
+
| { action: "new"; reason: "none" | "last-not-state-flow" | "ineligible" };
|
|
37
|
+
|
|
38
|
+
export type ContinuationStartupDecision =
|
|
39
|
+
| ContinuationRecommendation
|
|
40
|
+
| { action: "new"; reason: "explicit-new" }
|
|
41
|
+
| { action: "resume"; sessionFile: string; sessionId: string; reason: "explicit-resume" }
|
|
42
|
+
| { action: "native"; mode: "picker" | "continue-recent" | "no-session" };
|
|
43
|
+
|
|
44
|
+
export interface ContinuationProjectIdentity {
|
|
45
|
+
profile: string;
|
|
46
|
+
cwd: string;
|
|
47
|
+
gitCommonDir?: string;
|
|
48
|
+
worktree?: string;
|
|
49
|
+
branch?: string;
|
|
50
|
+
transport: ContinuationTransport;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ContinuationSessionCandidate extends ContinuationCandidateSummary {
|
|
54
|
+
profile: string;
|
|
55
|
+
cwd: string;
|
|
56
|
+
gitCommonDir?: string;
|
|
57
|
+
worktree?: string;
|
|
58
|
+
branch?: string;
|
|
59
|
+
transport: ContinuationTransport;
|
|
60
|
+
lifecycle: "open" | "closed" | "archived";
|
|
61
|
+
doNotAutoResume?: boolean;
|
|
62
|
+
stateFlow: { enabled: boolean; restorable: boolean };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export type ContinuationRecommender = (
|
|
66
|
+
context: Readonly<ContinuationHostContext>,
|
|
67
|
+
) => ContinuationRecommendation | Promise<ContinuationRecommendation>;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Preserve host-owned explicit session intent and consult State Flow only for
|
|
71
|
+
* an ordinary default launch. The recommender is a read-only advisory port;
|
|
72
|
+
* opening or claiming a native session remains the host's responsibility.
|
|
73
|
+
*/
|
|
74
|
+
function sameOptionalIdentity(current: string | undefined, candidate: string | undefined): boolean {
|
|
75
|
+
return current === undefined ? candidate === undefined : candidate === current;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function structurallyRelevant(identity: ContinuationProjectIdentity, candidate: ContinuationSessionCandidate): boolean {
|
|
79
|
+
return candidate.profile === identity.profile
|
|
80
|
+
&& candidate.cwd === identity.cwd
|
|
81
|
+
&& candidate.transport === identity.transport
|
|
82
|
+
&& candidate.lifecycle === "open"
|
|
83
|
+
&& candidate.doNotAutoResume !== true
|
|
84
|
+
&& sameOptionalIdentity(identity.gitCommonDir, candidate.gitCommonDir)
|
|
85
|
+
&& sameOptionalIdentity(identity.worktree, candidate.worktree)
|
|
86
|
+
&& sameOptionalIdentity(identity.branch, candidate.branch)
|
|
87
|
+
&& Number.isFinite(Date.parse(candidate.lastActivity));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Rank already header/provenance-only candidates without transcript content or I/O. */
|
|
91
|
+
export function recommendContinuationFromProvenance(
|
|
92
|
+
identity: ContinuationProjectIdentity,
|
|
93
|
+
candidates: readonly ContinuationSessionCandidate[],
|
|
94
|
+
): ContinuationRecommendation {
|
|
95
|
+
const relevant = candidates.filter((candidate) => structurallyRelevant(identity, candidate));
|
|
96
|
+
if (relevant.length === 0) return { action: "new", reason: "none" };
|
|
97
|
+
const latestTime = Math.max(...relevant.map(({ lastActivity }) => Date.parse(lastActivity)));
|
|
98
|
+
const latest = relevant.filter(({ lastActivity }) => Date.parse(lastActivity) === latestTime)
|
|
99
|
+
.sort((left, right) => left.sessionFile.localeCompare(right.sessionFile));
|
|
100
|
+
if (latest.length > 1) {
|
|
101
|
+
const resumable = latest.filter(({ stateFlow }) => stateFlow.enabled && stateFlow.restorable);
|
|
102
|
+
if (resumable.length > 1) return { action: "choose", reason: "ambiguous", candidates: resumable.map(({ sessionFile, sessionId, lastActivity }) => ({ sessionFile, sessionId, lastActivity, reason: "equally recent enabled State Flow session" })) };
|
|
103
|
+
if (resumable.length === 1 && latest.every(({ stateFlow }) => stateFlow.enabled && stateFlow.restorable)) {
|
|
104
|
+
const candidate = resumable[0];
|
|
105
|
+
return { action: "resume", sessionFile: candidate.sessionFile, sessionId: candidate.sessionId, reason: "latest-enabled-state-flow" };
|
|
106
|
+
}
|
|
107
|
+
return { action: "new", reason: "ineligible" };
|
|
108
|
+
}
|
|
109
|
+
const candidate = latest[0];
|
|
110
|
+
if (!candidate.stateFlow.enabled) return { action: "new", reason: "last-not-state-flow" };
|
|
111
|
+
if (!candidate.stateFlow.restorable) return { action: "new", reason: "ineligible" };
|
|
112
|
+
return { action: "resume", sessionFile: candidate.sessionFile, sessionId: candidate.sessionId, reason: "latest-enabled-state-flow" };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function resolveContinuationStartup(
|
|
116
|
+
context: ContinuationHostContext,
|
|
117
|
+
intent: ContinuationHostIntent,
|
|
118
|
+
recommend: ContinuationRecommender,
|
|
119
|
+
): Promise<ContinuationStartupDecision> {
|
|
120
|
+
switch (intent.kind) {
|
|
121
|
+
case "new":
|
|
122
|
+
return { action: "new", reason: "explicit-new" };
|
|
123
|
+
case "resume-exact":
|
|
124
|
+
return { action: "resume", sessionFile: intent.sessionFile, sessionId: intent.sessionId, reason: "explicit-resume" };
|
|
125
|
+
case "native-picker":
|
|
126
|
+
return { action: "native", mode: "picker" };
|
|
127
|
+
case "continue-recent":
|
|
128
|
+
return { action: "native", mode: "continue-recent" };
|
|
129
|
+
case "no-session":
|
|
130
|
+
return { action: "native", mode: "no-session" };
|
|
131
|
+
case "default":
|
|
132
|
+
return recommend(Object.freeze({ ...context }));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const MAX_SESSION_HEADER_BYTES = 64 * 1024;
|
|
137
|
+
|
|
138
|
+
export interface NativeSessionHeader {
|
|
139
|
+
file: string;
|
|
140
|
+
id: string;
|
|
141
|
+
cwd: string;
|
|
142
|
+
timestamp: string;
|
|
143
|
+
lastActivity: string;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface ContinuationCandidateProvenance {
|
|
147
|
+
profile: string;
|
|
148
|
+
gitCommonDir?: string;
|
|
149
|
+
worktree?: string;
|
|
150
|
+
branch?: string;
|
|
151
|
+
transport: ContinuationTransport;
|
|
152
|
+
lifecycle: "open" | "closed" | "archived";
|
|
153
|
+
doNotAutoResume?: boolean;
|
|
154
|
+
stateFlow: { enabled: boolean; restorable: boolean };
|
|
155
|
+
reason: string;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function readFirstLine(path: string): string {
|
|
159
|
+
const fd = openSync(path, "r");
|
|
160
|
+
try {
|
|
161
|
+
const bytes: number[] = [];
|
|
162
|
+
const byte = Buffer.allocUnsafe(1);
|
|
163
|
+
while (bytes.length <= MAX_SESSION_HEADER_BYTES) {
|
|
164
|
+
const count = readSync(fd, byte, 0, 1, null);
|
|
165
|
+
if (count === 0 || byte[0] === 0x0a) break;
|
|
166
|
+
bytes.push(byte[0]);
|
|
167
|
+
}
|
|
168
|
+
if (bytes.length > MAX_SESSION_HEADER_BYTES) throw new Error("Native Pi session header exceeds the safe discovery limit");
|
|
169
|
+
return Buffer.from(bytes).toString("utf8").replace(/\r$/, "");
|
|
170
|
+
} finally {
|
|
171
|
+
closeSync(fd);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function readNativeSessionHeader(path: string): NativeSessionHeader {
|
|
176
|
+
const file = resolve(path);
|
|
177
|
+
let value: unknown;
|
|
178
|
+
try {
|
|
179
|
+
value = JSON.parse(readFirstLine(file));
|
|
180
|
+
} catch (error) {
|
|
181
|
+
throw new Error(`Cannot read native Pi session header: ${error instanceof Error ? error.message : String(error)}`);
|
|
182
|
+
}
|
|
183
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("Invalid native Pi session header");
|
|
184
|
+
const header = value as Record<string, unknown>;
|
|
185
|
+
if (header.type !== "session" || typeof header.id !== "string" || header.id.trim().length === 0
|
|
186
|
+
|| typeof header.cwd !== "string" || header.cwd.trim().length === 0
|
|
187
|
+
|| typeof header.timestamp !== "string" || !Number.isFinite(Date.parse(header.timestamp))) {
|
|
188
|
+
throw new Error("Invalid native Pi session header");
|
|
189
|
+
}
|
|
190
|
+
const stats = statSync(file);
|
|
191
|
+
return { file, id: header.id, cwd: resolve(header.cwd), timestamp: header.timestamp, lastActivity: stats.mtime.toISOString() };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function discoverNativeSessionHeaders(sessionDir: string): { headers: NativeSessionHeader[]; invalid: Array<{ file: string; error: string }> } {
|
|
195
|
+
const headers: NativeSessionHeader[] = [];
|
|
196
|
+
const invalid: Array<{ file: string; error: string }> = [];
|
|
197
|
+
for (const entry of readdirSync(sessionDir, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))) {
|
|
198
|
+
if (!entry.isFile() || !entry.name.endsWith(".jsonl")) continue;
|
|
199
|
+
const file = join(sessionDir, entry.name);
|
|
200
|
+
try {
|
|
201
|
+
headers.push(readNativeSessionHeader(file));
|
|
202
|
+
} catch (error) {
|
|
203
|
+
invalid.push({ file: resolve(file), error: error instanceof Error ? error.message : String(error) });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return { headers, invalid };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function readRegular(path: string): string | undefined {
|
|
210
|
+
if (!existsSync(path)) return undefined;
|
|
211
|
+
if (!lstatSync(path).isFile()) throw new Error("State Flow continuation provenance must be a regular file");
|
|
212
|
+
return readFileSync(path, "utf8");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Inspect only exact current runtime provenance; never initialize, migrate, lock, checkout, or publish. */
|
|
216
|
+
export function inspectStateFlowContinuationProvenance(
|
|
217
|
+
header: NativeSessionHeader,
|
|
218
|
+
repositoryRoot: string,
|
|
219
|
+
): Pick<ContinuationCandidateProvenance, "stateFlow" | "reason"> {
|
|
220
|
+
const sessionKey = resolveSessionAddress(header.file, header.id, header.timestamp).key;
|
|
221
|
+
const paths = sessionRuntimePaths(header.cwd, header.id, repositoryRoot, sessionKey);
|
|
222
|
+
try {
|
|
223
|
+
const config = readRegular(paths.config);
|
|
224
|
+
const meta = readRegular(paths.meta);
|
|
225
|
+
if (config === undefined && meta === undefined) {
|
|
226
|
+
return { stateFlow: { enabled: false, restorable: true }, reason: "no State Flow session runtime" };
|
|
227
|
+
}
|
|
228
|
+
const runtime = parseSessionRuntime(config, meta, header.cwd, header.id);
|
|
229
|
+
if (!runtime) return { stateFlow: { enabled: false, restorable: true }, reason: "no State Flow session runtime" };
|
|
230
|
+
if (!runtime.config.enabled) return { stateFlow: { enabled: false, restorable: true }, reason: "State Flow stopped on selected runtime" };
|
|
231
|
+
if (runtime.meta.publication === "files") {
|
|
232
|
+
const global = loadScopeStream(header.cwd, header.id, "global", repositoryRoot, sessionKey);
|
|
233
|
+
const cwd = loadScopeStream(header.cwd, header.id, "cwd", repositoryRoot, sessionKey);
|
|
234
|
+
const session = loadScopeStream(header.cwd, header.id, "session", repositoryRoot, sessionKey);
|
|
235
|
+
if (!global || !cwd || !session) throw new Error("incomplete file-only temporal cohort");
|
|
236
|
+
validateTemporalState({ lineage: runtime.meta.lineage, scopes: { global, cwd, session } });
|
|
237
|
+
return { stateFlow: { enabled: true, restorable: true }, reason: "exact file-only runtime cohort is restorable" };
|
|
238
|
+
}
|
|
239
|
+
const runtimePath = relative(resolve(repositoryRoot), paths.meta);
|
|
240
|
+
if (runtimePath.startsWith("..")) throw new Error("runtime provenance escapes repository");
|
|
241
|
+
const owner = execFileSync("git", ["-C", repositoryRoot, "log", "-1", "--format=%H", "--", runtimePath], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
242
|
+
if (!owner) throw new Error("runtime owner commit is unavailable");
|
|
243
|
+
const restored = loadTemporalRevision(header.cwd, header.id, repositoryRoot, owner, sessionKey);
|
|
244
|
+
if (!restored.runtime || !restored.runtime.document.config.enabled) throw new Error("selected Git runtime is not enabled");
|
|
245
|
+
return { stateFlow: { enabled: true, restorable: true }, reason: `exact Git runtime ${owner.slice(0, 12)} is restorable` };
|
|
246
|
+
} catch (error) {
|
|
247
|
+
return { stateFlow: { enabled: true, restorable: false }, reason: `State Flow runtime is ineligible: ${error instanceof Error ? error.message : String(error)}` };
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function buildContinuationCandidates(
|
|
252
|
+
headers: readonly NativeSessionHeader[],
|
|
253
|
+
inspect: (header: Readonly<NativeSessionHeader>) => ContinuationCandidateProvenance | undefined,
|
|
254
|
+
): ContinuationSessionCandidate[] {
|
|
255
|
+
const candidates: ContinuationSessionCandidate[] = [];
|
|
256
|
+
for (const header of headers) {
|
|
257
|
+
const provenance = inspect(Object.freeze({ ...header }));
|
|
258
|
+
if (!provenance) continue;
|
|
259
|
+
candidates.push({
|
|
260
|
+
sessionFile: header.file,
|
|
261
|
+
sessionId: header.id,
|
|
262
|
+
lastActivity: header.lastActivity,
|
|
263
|
+
cwd: header.cwd,
|
|
264
|
+
...provenance,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
return candidates;
|
|
268
|
+
}
|