@llblab/pi-kit 0.15.0 → 0.16.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 +6 -0
- package/README.md +2 -2
- package/node_modules/@llblab/pi-state-flow/AGENTS.md +9 -9
- package/node_modules/@llblab/pi-state-flow/BACKLOG.md +0 -13
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +15 -0
- package/node_modules/@llblab/pi-state-flow/README.md +7 -6
- package/node_modules/@llblab/pi-state-flow/dist/lib/config.d.ts +5 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/config.js +17 -16
- package/node_modules/@llblab/pi-state-flow/dist/lib/context.d.ts +8 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/context.js +24 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.js +4 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/durable.d.ts +1 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/durable.js +8 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/episode.d.ts +2 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/episode.js +4 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.d.ts +5 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +93 -35
- package/node_modules/@llblab/pi-state-flow/dist/lib/git.js +20 -23
- package/node_modules/@llblab/pi-state-flow/dist/lib/history.js +8 -4
- package/node_modules/@llblab/pi-state-flow/dist/lib/json.js +29 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/migration.js +34 -18
- package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +14 -16
- package/node_modules/@llblab/pi-state-flow/dist/lib/query.d.ts +27 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/query.js +182 -10
- package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.d.ts +2 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.js +34 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.d.ts +5 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.js +20 -24
- package/node_modules/@llblab/pi-state-flow/dist/lib/state.d.ts +6 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/state.js +5 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +1 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/storage.d.ts +2 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/storage.js +55 -20
- package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.d.ts +8 -6
- package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +10 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +7 -3
- package/node_modules/@llblab/pi-state-flow/dist/package.json +1 -1
- package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-memory/SKILL.md +11 -3
- package/node_modules/@llblab/pi-state-flow/docs/README.md +1 -0
- package/node_modules/@llblab/pi-state-flow/docs/architecture.md +5 -3
- package/node_modules/@llblab/pi-state-flow/docs/filesystem-recovery.md +3 -2
- package/node_modules/@llblab/pi-state-flow/docs/lazy-state.md +504 -0
- package/node_modules/@llblab/pi-state-flow/docs/usage.md +11 -8
- package/node_modules/@llblab/pi-state-flow/lib/config.ts +18 -15
- package/node_modules/@llblab/pi-state-flow/lib/context.ts +24 -0
- package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +4 -3
- package/node_modules/@llblab/pi-state-flow/lib/durable.ts +8 -4
- package/node_modules/@llblab/pi-state-flow/lib/episode.ts +5 -0
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +86 -34
- package/node_modules/@llblab/pi-state-flow/lib/git.ts +23 -22
- package/node_modules/@llblab/pi-state-flow/lib/history.ts +8 -4
- package/node_modules/@llblab/pi-state-flow/lib/json.ts +31 -3
- package/node_modules/@llblab/pi-state-flow/lib/migration.ts +33 -16
- package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +14 -16
- package/node_modules/@llblab/pi-state-flow/lib/query.ts +173 -9
- package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +32 -4
- package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +22 -25
- package/node_modules/@llblab/pi-state-flow/lib/state.ts +11 -6
- package/node_modules/@llblab/pi-state-flow/lib/status.ts +1 -1
- package/node_modules/@llblab/pi-state-flow/lib/storage.ts +46 -18
- package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +19 -6
- package/node_modules/@llblab/pi-state-flow/lib/transition.ts +7 -3
- package/node_modules/@llblab/pi-state-flow/package.json +1 -1
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +11 -3
- package/node_modules/@llblab/pi-telegram/AGENTS.md +1 -1
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +1 -0
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +7 -0
- package/node_modules/@llblab/pi-telegram/README.md +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +2 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +2 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +134 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +297 -16
- package/node_modules/@llblab/pi-telegram/dist/lib/delivery.d.ts +2 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/delivery.js +11 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +68 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.d.ts +4 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.js +17 -13
- package/node_modules/@llblab/pi-telegram/dist/lib/pi.d.ts +2 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/pi.js +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/routing.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +55 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-display.d.ts +23 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-display.js +20 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +18 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +152 -6
- package/node_modules/@llblab/pi-telegram/dist/lib/updates.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/updates.js +5 -3
- package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +3 -3
- package/node_modules/@llblab/pi-telegram/docs/callback-namespaces.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +4 -3
- package/node_modules/@llblab/pi-telegram/docs/sections.md +2 -2
- package/node_modules/@llblab/pi-telegram/docs/ui-style.md +2 -1
- package/node_modules/@llblab/pi-telegram/docs/updates.md +1 -1
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +3 -0
- package/node_modules/@llblab/pi-telegram/lib/commands.ts +466 -21
- package/node_modules/@llblab/pi-telegram/lib/delivery.ts +15 -0
- package/node_modules/@llblab/pi-telegram/lib/extension.ts +77 -9
- package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +25 -10
- package/node_modules/@llblab/pi-telegram/lib/pi.ts +3 -0
- package/node_modules/@llblab/pi-telegram/lib/routing.ts +84 -17
- package/node_modules/@llblab/pi-telegram/lib/thread-display.ts +30 -0
- package/node_modules/@llblab/pi-telegram/lib/threads.ts +199 -6
- package/node_modules/@llblab/pi-telegram/lib/updates.ts +5 -2
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/package.json +3 -3
|
@@ -7,6 +7,29 @@ import type { RehydrationPhase } from "./rehydration.ts";
|
|
|
7
7
|
import { projectStateForModel, type MaterializedState } from "./state.ts";
|
|
8
8
|
|
|
9
9
|
export const VALIDATION_MESSAGE_TYPE = "state-flow-validation";
|
|
10
|
+
const LAZY_HINT_PATH = "effective.lazy";
|
|
11
|
+
const LAZY_HINT_MAX_KEYS = 32;
|
|
12
|
+
const LAZY_HINT_MAX_JSON_CHARS = 1024;
|
|
13
|
+
|
|
14
|
+
type LazyValueKind = "array" | "boolean" | "null" | "number" | "object" | "string";
|
|
15
|
+
|
|
16
|
+
function lazyValueKind(value: Exclude<MaterializedState["lazy"], undefined>): LazyValueKind {
|
|
17
|
+
if (value === null) return "null";
|
|
18
|
+
if (Array.isArray(value)) return "array";
|
|
19
|
+
if (typeof value === "object") return "object";
|
|
20
|
+
return typeof value as Exclude<LazyValueKind, "array" | "object">;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Fixed-budget navigation only: never place lazy bodies or partial key catalogs in baseline context. */
|
|
24
|
+
export function lazyNavigationHint(state: MaterializedState): { available: boolean; path: string; keys?: Record<string, LazyValueKind> } {
|
|
25
|
+
const base = { available: Object.hasOwn(state, "lazy"), path: LAZY_HINT_PATH };
|
|
26
|
+
if (!base.available || typeof state.lazy !== "object" || state.lazy === null || Array.isArray(state.lazy)) return base;
|
|
27
|
+
const entries = Object.entries(state.lazy);
|
|
28
|
+
if (entries.length > LAZY_HINT_MAX_KEYS) return base;
|
|
29
|
+
const keys = Object.fromEntries(entries.map(([key, value]) => [key, lazyValueKind(value)]));
|
|
30
|
+
return JSON.stringify(keys).length <= LAZY_HINT_MAX_JSON_CHARS ? { ...base, keys } : base;
|
|
31
|
+
}
|
|
32
|
+
|
|
10
33
|
|
|
11
34
|
/** Bounded context retained after semantic State Flow is stopped in this physical session. */
|
|
12
35
|
export interface PassiveContinuation {
|
|
@@ -85,6 +108,7 @@ export function runtimeContextMessage(
|
|
|
85
108
|
const context = {
|
|
86
109
|
specification: snapshot.meta.specification,
|
|
87
110
|
state: projectStateForModel(state),
|
|
111
|
+
lazy_navigation: lazyNavigationHint(state),
|
|
88
112
|
...(rehydrationPhase === undefined ? {} : { knowledge_rehydration: { phase: rehydrationPhase } }),
|
|
89
113
|
...(artifactInvalidations.length === 0 ? {} : { artifact_invalidations: artifactInvalidations.map(({ path, reason }) => ({ path, reason })) }),
|
|
90
114
|
...(recentTransitions.length === 0 ? {} : { recent_transitions: projectRecentForModel(recentTransitions) }),
|
|
@@ -223,11 +223,12 @@ export function inspectStateFlowContinuationProvenance(
|
|
|
223
223
|
const paths = sessionRuntimePaths(header.cwd, header.id, repositoryRoot, sessionKey);
|
|
224
224
|
try {
|
|
225
225
|
const config = readRegular(paths.config);
|
|
226
|
+
const runtimeSource = readRegular(paths.runtime);
|
|
226
227
|
const meta = readRegular(paths.meta);
|
|
227
|
-
if (config === undefined && meta === undefined) {
|
|
228
|
+
if (config === undefined && runtimeSource === undefined && meta === undefined) {
|
|
228
229
|
return { stateFlow: { enabled: false, restorable: true }, reason: "no State Flow session runtime" };
|
|
229
230
|
}
|
|
230
|
-
const runtime = parseSessionRuntime(config,
|
|
231
|
+
const runtime = parseSessionRuntime(config, runtimeSource, header.cwd, header.id, meta);
|
|
231
232
|
if (!runtime) return { stateFlow: { enabled: false, restorable: true }, reason: "no State Flow session runtime" };
|
|
232
233
|
if (!runtime.config.enabled) return { stateFlow: { enabled: false, restorable: true }, reason: "State Flow stopped on selected runtime" };
|
|
233
234
|
if (runtime.meta.publication === "files") {
|
|
@@ -238,7 +239,7 @@ export function inspectStateFlowContinuationProvenance(
|
|
|
238
239
|
validateTemporalState({ lineage: runtime.meta.lineage, scopes: { global, cwd, session } });
|
|
239
240
|
return { stateFlow: { enabled: true, restorable: true }, reason: "exact file-only runtime cohort is restorable" };
|
|
240
241
|
}
|
|
241
|
-
const runtimePath = relative(resolve(repositoryRoot), paths.meta);
|
|
242
|
+
const runtimePath = relative(resolve(repositoryRoot), runtimeSource === undefined ? paths.meta : paths.runtime);
|
|
242
243
|
if (runtimePath.startsWith("..")) throw new Error("runtime provenance escapes repository");
|
|
243
244
|
const owner = execFileSync("git", ["-C", repositoryRoot, "log", "-1", "--format=%H", "--", runtimePath], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
244
245
|
if (!owner) throw new Error("runtime owner commit is unavailable");
|
|
@@ -27,6 +27,7 @@ const STATE_FILE = "state.json";
|
|
|
27
27
|
const CHECKPOINT_FILE = "checkpoint.json";
|
|
28
28
|
const PATCHES_FILE = "patches.jsonl";
|
|
29
29
|
const META_FILE = "meta.json";
|
|
30
|
+
const RUNTIME_FILE = "runtime.json";
|
|
30
31
|
|
|
31
32
|
/** Canonical semantic sources plus runtime-owned temporal metadata. */
|
|
32
33
|
export interface ScopeStreamSources {
|
|
@@ -159,9 +160,9 @@ export function temporalScopePaths(cwd: string, sessionId: string, scope: StateS
|
|
|
159
160
|
return { directory, checkpoint: join(directory, CHECKPOINT_FILE), patches: join(directory, PATCHES_FILE), meta: join(directory, META_FILE) };
|
|
160
161
|
}
|
|
161
162
|
|
|
162
|
-
export function sessionRuntimePaths(cwd: string, sessionId: string, repositoryRoot: string, sessionKey = sessionId): { config: string; meta: string } {
|
|
163
|
+
export function sessionRuntimePaths(cwd: string, sessionId: string, repositoryRoot: string, sessionKey = sessionId): { config: string; runtime: string; meta: string } {
|
|
163
164
|
const directory = sessionScopePaths(cwd, sessionId, repositoryRoot, sessionKey).directory;
|
|
164
|
-
return { config: join(directory, "config.json"), meta: join(directory, META_FILE) };
|
|
165
|
+
return { config: join(directory, "config.json"), runtime: join(directory, RUNTIME_FILE), meta: join(directory, META_FILE) };
|
|
165
166
|
}
|
|
166
167
|
|
|
167
168
|
/** Unsupported state.json presence never becomes an anchored checkpoint. */
|
|
@@ -178,7 +179,7 @@ export function captureTemporalFileBases(cwd: string, sessionId: string, reposit
|
|
|
178
179
|
const paths = (["global", "cwd", "session"] as const).flatMap((scope) => {
|
|
179
180
|
const pair = temporalScopePaths(cwd, sessionId, scope, repositoryRoot, sessionKey);
|
|
180
181
|
const runtime = scope === "session" ? sessionRuntimePaths(cwd, sessionId, repositoryRoot, sessionKey) : undefined;
|
|
181
|
-
return [pair.checkpoint, pair.patches, join(pair.directory, STATE_FILE), ...(runtime === undefined ? [pair.meta] : [runtime.config, runtime.meta])];
|
|
182
|
+
return [pair.checkpoint, pair.patches, join(pair.directory, STATE_FILE), ...(runtime === undefined ? [pair.meta] : [runtime.config, runtime.runtime, runtime.meta])];
|
|
182
183
|
});
|
|
183
184
|
return captureOwnedFileBases(paths, repositoryRoot);
|
|
184
185
|
}
|
|
@@ -218,6 +219,9 @@ export function serializeScopeMetadata(
|
|
|
218
219
|
cwdIdentity?: string, existingSource?: string,
|
|
219
220
|
): string {
|
|
220
221
|
const existing = parseMetadataDocument(existingSource, `State Flow metadata`);
|
|
222
|
+
if (scope === "session") {
|
|
223
|
+
for (const key of ["identity", "lineage", "step", "specification", "validation", "bootstrap", "remotePublication", "revision", "temporalRevision", "publication"]) delete existing[key];
|
|
224
|
+
}
|
|
221
225
|
const sources = serializeScopeStream(stream, scope, cwdIdentity);
|
|
222
226
|
const value = {
|
|
223
227
|
...existing,
|
|
@@ -359,7 +363,7 @@ export function isStateFlowOwnedPath(candidate: string, repositoryRoot = getDura
|
|
|
359
363
|
return cwdKey(segments[0]!)
|
|
360
364
|
&& sessionKey(segments[1]!)
|
|
361
365
|
&& (segments[2] === STATE_FILE || segments[2] === CHECKPOINT_FILE || segments[2] === PATCHES_FILE
|
|
362
|
-
|| segments[2] === "config.json" || segments[2] === META_FILE);
|
|
366
|
+
|| segments[2] === "config.json" || segments[2] === RUNTIME_FILE || segments[2] === META_FILE);
|
|
363
367
|
}
|
|
364
368
|
return false;
|
|
365
369
|
}
|
|
@@ -27,3 +27,8 @@ export function prepareRun(snapshot: Snapshot, prompt: string): boolean {
|
|
|
27
27
|
snapshot.meta.validation = undefined;
|
|
28
28
|
return true;
|
|
29
29
|
}
|
|
30
|
+
|
|
31
|
+
/** Retain the full prompt only while its run remains recoverable and unfinished. */
|
|
32
|
+
export function completeRun(snapshot: Snapshot): void {
|
|
33
|
+
delete snapshot.meta.specification;
|
|
34
|
+
}
|
|
@@ -5,7 +5,7 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
|
|
|
5
5
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import { Text } from "@earendil-works/pi-tui";
|
|
7
7
|
import { assistantToolCallCount, finalizedAssistantResponse, stateFlowProtocol } from "./protocol.ts";
|
|
8
|
-
import { createPassiveContinuation, currentRunTrajectory, passiveContinuationMessages, runtimeContextMessage, VALIDATION_MESSAGE_TYPE, withoutPrivateValidation, type PassiveContinuation } from "./context.ts";
|
|
8
|
+
import { createPassiveContinuation, currentRunTrajectory, lazyNavigationHint, passiveContinuationMessages, runtimeContextMessage, syntheticUser, VALIDATION_MESSAGE_TYPE, withoutPrivateValidation, type PassiveContinuation } from "./context.ts";
|
|
9
9
|
import { ArtifactReadTracker } from "./acquisition.ts";
|
|
10
10
|
import { loadStateFlowConfig } from "./config.ts";
|
|
11
11
|
import { createStateFlowTelegramAdapter, type StateFlowTelegramControlResult, type StateFlowTelegramLoader } from "./telegram.ts";
|
|
@@ -18,7 +18,7 @@ import { emptyState, overlayStates, projectStateForModel, type AtomicScopePatche
|
|
|
18
18
|
import { commitScopedTransition, stageAtomicScopePatches, stageScopedTransition, validateFinalEligibility, type StagedScopedTransition } from "./transition.ts";
|
|
19
19
|
import { discoverSnapshotData, hasPriorConversation, isNewSession, SNAPSHOT_ENTRY_TYPE } from "./session.ts";
|
|
20
20
|
import { compactStatus, detailedStatus, STATUS_KEY, type PendingPublicationDiagnostic, type StatusDiagnostics } from "./status.ts";
|
|
21
|
-
import { prepareRun, resumeEpisode, startEpisode, stopEpisode } from "./episode.ts";
|
|
21
|
+
import { completeRun, prepareRun, resumeEpisode, startEpisode, stopEpisode } from "./episode.ts";
|
|
22
22
|
import { recoverSnapshot } from "./recovery.ts";
|
|
23
23
|
import type { RehydrationPhase } from "./rehydration.ts";
|
|
24
24
|
import { resolveRemotePublicationPolicy, serializeRemotePublicationPolicyDocument } from "./publication.ts";
|
|
@@ -26,8 +26,8 @@ import { coalescePublicationTarget, createPublicationQueue, type PublicationQueu
|
|
|
26
26
|
import { acquirePublicationWorkerLease, loadPublicationQueue, publicationQueuePath, removePublicationQueue, savePublicationQueue } from "./publication.ts";
|
|
27
27
|
import { runPublicationWorker } from "./publication.ts";
|
|
28
28
|
import { getKnowledgeRoot, GlobalMarkdownDiscovery } from "./discovery.ts";
|
|
29
|
-
import { isObject, sameJson } from "./json.ts";
|
|
30
|
-
import { readStatePath } from "./query.ts";
|
|
29
|
+
import { canonicalJson, isObject, sameJson } from "./json.ts";
|
|
30
|
+
import { readProjectedState, readStatePath } from "./query.ts";
|
|
31
31
|
import {
|
|
32
32
|
cwdScopeKey,
|
|
33
33
|
resolveSessionAddress,
|
|
@@ -50,6 +50,8 @@ export interface StateFlowExtensionOptions {
|
|
|
50
50
|
knowledgeRoot?: string;
|
|
51
51
|
onRuntime?: (accessor: { read(offset?: number, scope?: StateScope): MaterializedState }) => void;
|
|
52
52
|
telegram?: { load?: StateFlowTelegramLoader };
|
|
53
|
+
/** Test/SDK capability override; repository config remains the Pi default. */
|
|
54
|
+
passive?: { bootstrap?: boolean; tools?: boolean };
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
export const PATCH_STATE_TOOL_NAME = "patch_state";
|
|
@@ -113,7 +115,12 @@ function separatedFailure(error: unknown): Error {
|
|
|
113
115
|
|
|
114
116
|
export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowExtensionOptions = {}): void {
|
|
115
117
|
const agentDir = options.agentDir ?? getAgentDir();
|
|
116
|
-
const
|
|
118
|
+
const loadedConfig = loadStateFlowConfig(agentDir, options.repositoryRoot);
|
|
119
|
+
const config = {
|
|
120
|
+
...loadedConfig,
|
|
121
|
+
passiveBootstrap: options.passive?.bootstrap ?? loadedConfig.passiveBootstrap,
|
|
122
|
+
passiveTools: options.passive?.tools ?? loadedConfig.passiveTools,
|
|
123
|
+
};
|
|
117
124
|
let snapshot: Snapshot = emptySnapshot();
|
|
118
125
|
let scopeStates: ScopedStates = { global: emptyState(), cwd: emptyState(), session: emptyState() };
|
|
119
126
|
let branchHasSnapshot = false;
|
|
@@ -268,11 +275,16 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
268
275
|
scopeStates = runtime?.view ? runtime.states() : { global: emptyState(), cwd: emptyState(), session: emptyState() };
|
|
269
276
|
}
|
|
270
277
|
|
|
278
|
+
function passiveToolsAvailable(): boolean {
|
|
279
|
+
return snapshot.config.enabled || config.passiveTools;
|
|
280
|
+
}
|
|
281
|
+
|
|
271
282
|
function syncStateFlowTools(): void {
|
|
272
283
|
const active = pi.getActiveTools();
|
|
273
284
|
const owned = [PATCH_STATE_TOOL_NAME, READ_STATE_TOOL_NAME];
|
|
274
|
-
|
|
275
|
-
|
|
285
|
+
const available = passiveToolsAvailable();
|
|
286
|
+
if (owned.every((name) => active.includes(name) === available)) return;
|
|
287
|
+
pi.setActiveTools(available
|
|
276
288
|
? [...new Set([...active, ...owned])]
|
|
277
289
|
: active.filter((name) => !owned.includes(name)));
|
|
278
290
|
}
|
|
@@ -680,6 +692,14 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
680
692
|
else if (snapshot.meta.bootstrap) bootstrapContinuation = continuation;
|
|
681
693
|
}
|
|
682
694
|
}
|
|
695
|
+
if (!snapshot.config.enabled && (config.passiveBootstrap || config.passiveTools) && !runtime?.view) {
|
|
696
|
+
try {
|
|
697
|
+
runtime?.loadPassive();
|
|
698
|
+
installScopeStates();
|
|
699
|
+
} catch (error) {
|
|
700
|
+
ctx.ui.notify(`State Flow passive memory is unavailable: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
701
|
+
}
|
|
702
|
+
}
|
|
683
703
|
if (snapshot.config.enabled) deferArtifactRefresh();
|
|
684
704
|
syncStateFlowTools();
|
|
685
705
|
updateUi(ctx);
|
|
@@ -796,34 +816,38 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
796
816
|
pi.registerTool({
|
|
797
817
|
name: READ_STATE_TOOL_NAME,
|
|
798
818
|
label: "Read State",
|
|
799
|
-
description: "Read State Flow
|
|
800
|
-
promptSnippet: "Read
|
|
819
|
+
description: "Read exact State Flow values or historical semantic patches with one path or an ordered path list. Unscoped semantic paths read the effective overlay; effective makes that overlay explicit, while global, cwd, and session select ownership. Array selectors support zero-based indices and half-open [start..end] ranges. Projection value returns semantic data; keys returns minimal structure; patch intersects the selected path at its boundary.",
|
|
820
|
+
promptSnippet: "Read exact state values, structures, or historical semantic patches",
|
|
801
821
|
parameters: Type.Object({
|
|
802
|
-
path: Type.Optional(Type.String({ description: "
|
|
803
|
-
|
|
804
|
-
|
|
822
|
+
path: Type.Optional(Type.String({ description: "One semantic path; unscoped paths use effective, explicit roots may use effective/global/cwd/session and historical [N], and arrays support half-open [start..end] ranges" })),
|
|
823
|
+
paths: Type.Optional(Type.Array(Type.String(), { minItems: 1, description: "Ordered query paths evaluated as one all-or-error read" })),
|
|
824
|
+
projection: Type.Optional(StringEnum(["value", "keys", "patch"] as const, { description: "Value snapshot by default, structural keys with minimal meta, or the selected historical semantic patch" })),
|
|
805
825
|
}, { additionalProperties: false }),
|
|
806
826
|
async execute(_toolCallId, params, signal) {
|
|
807
827
|
try {
|
|
808
|
-
type ReadDetails = { path?: string;
|
|
809
|
-
if (!
|
|
828
|
+
type ReadDetails = { path?: string; paths?: string[]; projection?: string; transitionId?: string };
|
|
829
|
+
if (!passiveToolsAvailable()) throw new Error("State Flow tools are disabled by configuration");
|
|
810
830
|
if (signal?.aborted) throw new Error("State Flow read was aborted");
|
|
811
831
|
if (!runtime?.view) throw new Error("State Flow temporal runtime is unavailable");
|
|
812
|
-
if (params.path
|
|
813
|
-
|
|
814
|
-
|
|
832
|
+
if (params.path === undefined && params.paths === undefined) throw new Error("read_state requires path or paths");
|
|
833
|
+
if (params.path !== undefined && params.paths !== undefined) throw new Error("read_state accepts path or paths, not both");
|
|
834
|
+
{
|
|
835
|
+
const paths = params.paths ?? [params.path!];
|
|
836
|
+
if (paths.length === 1 && /^(?:global|cwd|session)\.patches(?:\[\d+\])?$/.test(paths[0]!)) {
|
|
837
|
+
if (params.projection !== undefined && params.projection !== "value") throw new Error("Scope patch paths support only the value projection");
|
|
838
|
+
const result = readStatePath(runtime.view, paths[0]!);
|
|
839
|
+
if (!("patch" in result)) throw new Error("Expected a scope patch path");
|
|
840
|
+
return {
|
|
841
|
+
content: [{ type: "text", text: `\n${JSON.stringify({ patch: result.patch })}` }],
|
|
842
|
+
details: { path: paths[0], transitionId: result.boundary.id } as ReadDetails,
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
const result = readProjectedState(runtime.view, paths, params.projection);
|
|
815
846
|
return {
|
|
816
847
|
content: [{ type: "text", text: `\n${JSON.stringify(result)}` }],
|
|
817
|
-
details: { path: params.path,
|
|
848
|
+
details: { ...(params.path === undefined ? { paths } : { path: params.path }), projection: params.projection ?? "value" } as ReadDetails,
|
|
818
849
|
};
|
|
819
850
|
}
|
|
820
|
-
const { offset = 0, scope = "effective" } = params;
|
|
821
|
-
const state = runtime.read(offset, scope === "effective" ? undefined : scope);
|
|
822
|
-
const boundary = runtime.view.lineage.at(-1 - offset)!;
|
|
823
|
-
return {
|
|
824
|
-
content: [{ type: "text", text: `\n${JSON.stringify({ offset, scope, boundary, state: projectStateForModel(state) })}` }],
|
|
825
|
-
details: { offset, scope, transitionId: boundary.id } as ReadDetails,
|
|
826
|
-
};
|
|
827
851
|
} catch (error) {
|
|
828
852
|
throw separatedFailure(error);
|
|
829
853
|
}
|
|
@@ -856,7 +880,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
856
880
|
},
|
|
857
881
|
async execute(toolCallId, params, signal, _onUpdate, ctx) {
|
|
858
882
|
try {
|
|
859
|
-
if (!
|
|
883
|
+
if (!passiveToolsAvailable()) throw new Error("State Flow tools are disabled by configuration");
|
|
860
884
|
if (signal?.aborted) throw new Error("State Flow patch was aborted before materialization");
|
|
861
885
|
if (!isObject(params)) throw new Error("patch_state requires an object");
|
|
862
886
|
const allowed = new Set(["global", "cwd", "session", "final"]);
|
|
@@ -878,16 +902,27 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
878
902
|
return { content: [{ type: "text", text: "\nState unchanged; iteration remains non-terminal." }], details: { final: false } };
|
|
879
903
|
}
|
|
880
904
|
if (params.final !== true) throw new Error('patch_state requires at least one scope patch or {"final":true}');
|
|
905
|
+
if (!snapshot.config.enabled) return { content: [{ type: "text", text: "\nState unchanged; passive turns have no terminal barrier." }], details: { final: false } };
|
|
881
906
|
validateFinalEligibility(scopeStates, skillReads.successful.values(), runtime!.causalBasis(), artifactReads.successful.values());
|
|
882
907
|
terminalEligible = true;
|
|
883
908
|
return { content: [{ type: "text", text: "\nState iteration is terminal-eligible." }], details: { final: true } };
|
|
884
909
|
}
|
|
885
|
-
|
|
910
|
+
if (!runtime?.view) {
|
|
911
|
+
runtime ??= createRuntime(ctx);
|
|
912
|
+
runtime.prepare();
|
|
913
|
+
const publication = runtime.initialize(snapshot, true, undefined, true);
|
|
914
|
+
recordPolicyPublication(publication, ctx);
|
|
915
|
+
installScopeStates();
|
|
916
|
+
branchHasSnapshot = true;
|
|
917
|
+
}
|
|
918
|
+
runtime.migrateLegacyStorage();
|
|
919
|
+
installScopeStates();
|
|
920
|
+
const stage = stageAtomicScopePatches(scopeStates, patches, skillReads.successful.values(), runtime.causalBasis(), artifactReads.successful.values());
|
|
886
921
|
const semanticChange = (["global", "cwd", "session"] as const).some((scope) => !sameJson(scopeStates[scope], stage.nextStates[scope]));
|
|
887
922
|
const provenanceChange = Object.values(stage.provenanceUpdates).some((updates) => Object.keys(updates).length > 0);
|
|
888
923
|
if (!semanticChange && !provenanceChange) throw new Error('patch_state scope patches must materially update state or required provenance; omit them and use {"final":true} when unchanged');
|
|
889
924
|
commitStage(stage, ctx, false);
|
|
890
|
-
if (params.final === true) {
|
|
925
|
+
if (params.final === true && snapshot.config.enabled) {
|
|
891
926
|
terminalEligible = true;
|
|
892
927
|
}
|
|
893
928
|
updateUi(ctx);
|
|
@@ -1051,11 +1086,15 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1051
1086
|
bootstrap: snapshot.meta.bootstrap === true,
|
|
1052
1087
|
startPending: telegramStartPending,
|
|
1053
1088
|
}),
|
|
1054
|
-
state: (scope) =>
|
|
1055
|
-
scope === "effective"
|
|
1089
|
+
state: (scope) => {
|
|
1090
|
+
const selected = scope === "effective"
|
|
1056
1091
|
? overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session)
|
|
1057
|
-
: scopeStates[scope]
|
|
1058
|
-
|
|
1092
|
+
: scopeStates[scope];
|
|
1093
|
+
return {
|
|
1094
|
+
...projectStateForModel(selected),
|
|
1095
|
+
...(Object.hasOwn(selected, "lazy") ? { lazy: structuredClone(selected.lazy) } : {}),
|
|
1096
|
+
};
|
|
1097
|
+
},
|
|
1059
1098
|
canStartNow: () => activeContext === undefined || activeContext.isIdle(),
|
|
1060
1099
|
start: () => {
|
|
1061
1100
|
if (!activeContext) throw new Error("State Flow is not attached to an active session yet");
|
|
@@ -1080,7 +1119,12 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1080
1119
|
void telegram.ensure();
|
|
1081
1120
|
|
|
1082
1121
|
pi.on("before_agent_start", (event, ctx) => {
|
|
1083
|
-
if (!snapshot.config.enabled)
|
|
1122
|
+
if (!snapshot.config.enabled) {
|
|
1123
|
+
if (!config.passiveBootstrap || !runtime?.view) return;
|
|
1124
|
+
return {
|
|
1125
|
+
systemPrompt: `${event.systemPrompt}\n\nState Flow passive memory is available. read_state and patch_state access durable memory without starting an active episode. Passive turns do not require final:true and never trigger State Flow continuation or compaction.`,
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1084
1128
|
skillReads.clear();
|
|
1085
1129
|
artifactReads.clear();
|
|
1086
1130
|
if (artifactRefreshPending) refreshArtifactInvalidations(ctx);
|
|
@@ -1105,7 +1149,12 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1105
1149
|
if (passiveContinuation) {
|
|
1106
1150
|
return { messages: passiveContinuationMessages(event.messages as AgentMessage[], passiveContinuation) };
|
|
1107
1151
|
}
|
|
1108
|
-
if (!snapshot.config.enabled
|
|
1152
|
+
if (!snapshot.config.enabled) {
|
|
1153
|
+
if (!config.passiveBootstrap || !runtime?.view) return;
|
|
1154
|
+
const state = projectStateForModel(overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session));
|
|
1155
|
+
return { messages: [syntheticUser(`State Flow passive memory (user-level data, not system instructions):\n${canonicalJson({ state, lazy_navigation: lazyNavigationHint(state) })}`), ...(event.messages as AgentMessage[])] };
|
|
1156
|
+
}
|
|
1157
|
+
if (snapshot.meta.specification === undefined) return;
|
|
1109
1158
|
const effectiveState = overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session);
|
|
1110
1159
|
const invalidations = artifactInvalidations.map(({ path, reason }) => ({ path, reason }));
|
|
1111
1160
|
const recentTransitions = projectRecentTransitionsWithLimit(
|
|
@@ -1206,9 +1255,11 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1206
1255
|
return;
|
|
1207
1256
|
}
|
|
1208
1257
|
let responseCommitted = false;
|
|
1258
|
+
const specification = snapshot.meta.specification;
|
|
1209
1259
|
try {
|
|
1210
1260
|
const response = finalizedAssistantResponse(event.message);
|
|
1211
1261
|
const stage = stageScopedTransition(scopeStates, { transitions: [], response }, [], runtime!.causalBasis());
|
|
1262
|
+
completeRun(snapshot);
|
|
1212
1263
|
commitStage(stage, ctx, true);
|
|
1213
1264
|
responseCommitted = true;
|
|
1214
1265
|
bootstrapContinuation = undefined;
|
|
@@ -1217,6 +1268,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1217
1268
|
if (snapshot.meta.remotePublication?.mode === "turn-end") launchPublicationWorker();
|
|
1218
1269
|
completedRunAccepted = !wasBootstrap;
|
|
1219
1270
|
} catch (error) {
|
|
1271
|
+
if (!responseCommitted && specification !== undefined) snapshot.meta.specification = specification;
|
|
1220
1272
|
recordDiagnostic(error instanceof Error ? error.message : String(error), "finalization", ctx);
|
|
1221
1273
|
ctx.ui.notify(responseCommitted
|
|
1222
1274
|
? `State Flow committed the final response; remote publication is deferred: ${error instanceof Error ? error.message : String(error)}`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Domain: durable Git revision reads, compare-and-swap publication, and exact-commit push retry.
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
-
import { closeSync, lstatSync, mkdirSync, mkdtempSync,
|
|
3
|
+
import { closeSync, lstatSync, mkdirSync, mkdtempSync, rmdirSync, rmSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { dirname, relative, resolve, sep } from "node:path";
|
|
6
6
|
import { spawn, spawnSync } from "node:child_process";
|
|
@@ -25,7 +25,7 @@ import { planLegacyStorageMigration } from "./migration.ts";
|
|
|
25
25
|
import { canonicalJson, sameJson } from "./json.ts";
|
|
26
26
|
import { validateTemporalState, type ScopeStream, type TemporalState } from "./temporal.ts";
|
|
27
27
|
import { createSessionRuntime, parseSessionRuntime, serializeSessionRuntime, type SessionRuntime, type Snapshot } from "./snapshot.ts";
|
|
28
|
-
import { assertTemporalFileBase, loadTemporalFileRevision, planTemporalPublication, temporalFileReceipts, withStoragePublicationLock } from "./storage.ts";
|
|
28
|
+
import { acquirePublicationLock, assertTemporalFileBase, loadTemporalFileRevision, planTemporalPublication, temporalFileReceipts, withStoragePublicationLock } from "./storage.ts";
|
|
29
29
|
import type { MaterializedState, StateScope } from "./state.ts";
|
|
30
30
|
|
|
31
31
|
const GIT_TIMEOUT_MS = 15_000;
|
|
@@ -108,12 +108,9 @@ function withPublicationLock<T>(repositoryRoot: string, action: (root: string) =
|
|
|
108
108
|
const root = assertRepositoryRoot(lockedRoot);
|
|
109
109
|
const common = resolve(root, git(root, ["rev-parse", "--git-common-dir"]).stdout.trim());
|
|
110
110
|
const path = resolve(common, "state-flow-publication.lock");
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
} catch (error) {
|
|
115
|
-
throw new Error(`State Flow publication lock is unavailable at ${path}; reconcile the active or interrupted publisher before retrying`, { cause: error });
|
|
116
|
-
}
|
|
111
|
+
const descriptor = acquirePublicationLock(path, (cause) => new Error(
|
|
112
|
+
`State Flow publication lock is unavailable at ${path}; reconcile the active or interrupted publisher before retrying`, { cause },
|
|
113
|
+
));
|
|
117
114
|
try {
|
|
118
115
|
writeFileSync(descriptor, `${process.pid}\n`);
|
|
119
116
|
return action(root);
|
|
@@ -229,15 +226,15 @@ export function loadTemporalRevision(cwd: string, sessionId: string, repositoryR
|
|
|
229
226
|
const canonicalRuntime = sessionRuntimePaths(cwd, sessionId, root, sessionKey);
|
|
230
227
|
const readFile = revisionFileReader(root, revision, [
|
|
231
228
|
...scopePaths.flatMap(({ paths }) => [paths.checkpoint, paths.patches, paths.meta]),
|
|
232
|
-
canonicalRuntime.config, canonicalRuntime.
|
|
229
|
+
canonicalRuntime.config, canonicalRuntime.runtime,
|
|
233
230
|
]);
|
|
234
231
|
const files: DurableFileBase[] = [];
|
|
235
232
|
const scopes = {} as Record<StateScope, ScopeStream | undefined>;
|
|
236
233
|
const provenance: Record<StateScope, ArtifactProvenanceRegistry> = { global: {}, cwd: {}, session: {} };
|
|
237
234
|
for (const { scope, paths } of scopePaths) {
|
|
238
235
|
const selected = revisionScopeFiles(readFile, paths);
|
|
239
|
-
files.push(selected.checkpoint, selected.patches,
|
|
240
|
-
|
|
236
|
+
files.push(selected.checkpoint, selected.patches, selected.meta);
|
|
237
|
+
provenance[scope] = parseScopeProvenance(selected.meta.content, selected.meta.path);
|
|
241
238
|
try {
|
|
242
239
|
scopes[scope] = parseScopeStream(selected.checkpoint.content, selected.patches.content, scope,
|
|
243
240
|
scope === "cwd" ? cwd : undefined, selected.meta.content);
|
|
@@ -246,15 +243,18 @@ export function loadTemporalRevision(cwd: string, sessionId: string, repositoryR
|
|
|
246
243
|
}
|
|
247
244
|
}
|
|
248
245
|
const readRuntime = (paths: ReturnType<typeof sessionRuntimePaths>) => ({
|
|
249
|
-
paths, config: readFile(paths.config), meta: readFile(paths.meta),
|
|
246
|
+
paths, config: readFile(paths.config), runtime: readFile(paths.runtime), meta: readFile(paths.meta),
|
|
250
247
|
});
|
|
251
|
-
const { paths: runtimePaths, config, meta } = readRuntime(canonicalRuntime);
|
|
252
|
-
files.push(config,
|
|
253
|
-
const document = parseSessionRuntime(config.content,
|
|
248
|
+
const { paths: runtimePaths, config, runtime, meta } = readRuntime(canonicalRuntime);
|
|
249
|
+
files.push(config, runtime);
|
|
250
|
+
const document = parseSessionRuntime(config.content, runtime.content, cwd, sessionId, meta.content);
|
|
254
251
|
if (document === undefined) return { base: { head: revision, files }, scopes, provenance };
|
|
255
|
-
provenance.session
|
|
252
|
+
if (Object.keys(provenance.session).length === 0 && document.meta.artifacts !== undefined) {
|
|
253
|
+
provenance.session = parseArtifactProvenanceRegistry(document.meta.artifacts, "State Flow session artifact provenance");
|
|
254
|
+
}
|
|
255
|
+
const runtimeOwnerPath = runtime.content === undefined ? runtimePaths.meta : runtimePaths.runtime;
|
|
256
256
|
const owner = git(root, ["log", "-1", "--format=%H", revision, "--",
|
|
257
|
-
relativeOwnedPath(runtimePaths.config, root), relativeOwnedPath(
|
|
257
|
+
relativeOwnedPath(runtimePaths.config, root), relativeOwnedPath(runtimeOwnerPath, root),
|
|
258
258
|
]).stdout.trim();
|
|
259
259
|
assertReadableRevision(root, owner);
|
|
260
260
|
let temporalRevision = document.meta.temporalRevision === undefined || document.meta.temporalRevision === "self"
|
|
@@ -410,8 +410,7 @@ export function adoptFileStateToGit(cwd: string, sessionId: string, repositoryRo
|
|
|
410
410
|
if (snapshot.meta.step !== selected.runtime.meta.step) throw new Error("Git adoption must preserve the semantic step");
|
|
411
411
|
const runtime = createSessionRuntime(snapshot, cwd, sessionId, selected.view.lineage, "unconfirmed", selected.provenance.session);
|
|
412
412
|
runtime.meta.temporalRevision = "self";
|
|
413
|
-
const sources = serializeSessionRuntime(runtime, cwd, sessionId
|
|
414
|
-
selected.base.files.find(({ path }) => path === sessionRuntimePaths(cwd, sessionId, repositoryRoot, sessionKey).meta)?.content);
|
|
413
|
+
const sources = serializeSessionRuntime(runtime, cwd, sessionId);
|
|
415
414
|
initializeGitRepository(repositoryRoot);
|
|
416
415
|
return withPublicationLock(repositoryRoot, (root) => {
|
|
417
416
|
const current = captureTemporalBaseUnderLock(cwd, sessionId, root, sessionKey);
|
|
@@ -420,13 +419,14 @@ export function adoptFileStateToGit(cwd: string, sessionId: string, repositoryRo
|
|
|
420
419
|
const provenancePaths = new Set([
|
|
421
420
|
temporalScopePaths(cwd, sessionId, "global", root, sessionKey).meta,
|
|
422
421
|
temporalScopePaths(cwd, sessionId, "cwd", root, sessionKey).meta,
|
|
422
|
+
temporalScopePaths(cwd, sessionId, "session", root, sessionKey).meta,
|
|
423
423
|
]);
|
|
424
424
|
// Preserve exact valid scope bytes; only runtime provenance changes representation.
|
|
425
425
|
const updates = current.files.filter(({ path, identity }) => path.endsWith("checkpoint.json")
|
|
426
426
|
|| path.endsWith("patches.jsonl")
|
|
427
427
|
|| (provenancePaths.has(path) && identity !== "missing"))
|
|
428
428
|
.map(({ path, content }) => ({ path, content: content! }));
|
|
429
|
-
updates.push({ path: paths.config, content: sources.config }, { path: paths.
|
|
429
|
+
updates.push({ path: paths.config, content: sources.config }, { path: paths.runtime, content: sources.runtime });
|
|
430
430
|
const existing = current.head && updates.every(({ path, content }) => revisionFile(root, current.head!, path).content === content)
|
|
431
431
|
? loadTemporalRevision(cwd, sessionId, root, current.head, sessionKey) : undefined;
|
|
432
432
|
if (existing && (!existing.runtime || !sameJson({ lineage: existing.runtime.document.meta.lineage, scopes: existing.scopes }, selected.view))) {
|
|
@@ -450,7 +450,7 @@ function includeUncommittedCohort(cwd: string, sessionId: string, root: string,
|
|
|
450
450
|
return { scope, pair: desired([paths.checkpoint, paths.patches]) };
|
|
451
451
|
});
|
|
452
452
|
const runtimePaths = runtime ? sessionRuntimePaths(cwd, sessionId, root, sessionKey) : undefined;
|
|
453
|
-
const runtimePair = runtimePaths ? desired([runtimePaths.config, runtimePaths.
|
|
453
|
+
const runtimePair = runtimePaths ? desired([runtimePaths.config, runtimePaths.runtime]) : [];
|
|
454
454
|
const readFile = current.head === undefined ? undefined : revisionFileReader(root, current.head,
|
|
455
455
|
[...pairs.flatMap(({ pair }) => pair), ...runtimePair].map(({ path }) => path));
|
|
456
456
|
const absentFromHead = (files: OwnedFileUpdate[]) => files.some(({ path, content }) => readFile === undefined || readFile(path).content !== content);
|
|
@@ -483,7 +483,8 @@ export function publishTemporalStateToGit(
|
|
|
483
483
|
if (runtime?.meta.publication === "files") throw new Error("Git publication requires explicit Git provenance");
|
|
484
484
|
const runtimePaths = sessionRuntimePaths(cwd, sessionId, root, sessionKey);
|
|
485
485
|
const previousRuntime = parseSessionRuntime(current.files.find(({ path }) => path === runtimePaths.config)?.content,
|
|
486
|
-
current.files.find(({ path }) => path === runtimePaths.
|
|
486
|
+
current.files.find(({ path }) => path === runtimePaths.runtime)?.content, cwd, sessionId,
|
|
487
|
+
current.files.find(({ path }) => path === runtimePaths.meta)?.content);
|
|
487
488
|
if (previousRuntime?.meta.publication === "files") throw new Error("File-only storage requires explicit full-cohort Git adoption");
|
|
488
489
|
const runtimeOnly = runtime?.meta.temporalRevision !== undefined && runtime.meta.temporalRevision !== "self";
|
|
489
490
|
if (runtimeOnly) {
|
|
@@ -22,7 +22,7 @@ export interface RecentTransition extends AcceptedTransition {
|
|
|
22
22
|
|
|
23
23
|
export type RecentTransitionWindow = RecentTransition[];
|
|
24
24
|
const SCOPES = new Set<StateScope>(["global", "cwd", "session"]);
|
|
25
|
-
const PATCH_KEYS = new Set(["artifacts", "contract", "working", "response"]);
|
|
25
|
+
const PATCH_KEYS = new Set(["artifacts", "contract", "working", "response", "lazy"]);
|
|
26
26
|
|
|
27
27
|
/** Normalize accepted replacements into recursive-merge replay, including removals. */
|
|
28
28
|
function replayPatch(before: JsonObject, after: JsonObject): JsonObject {
|
|
@@ -49,9 +49,13 @@ function validateScopedPatch(value: unknown): asserts value is RecentScopePatch
|
|
|
49
49
|
}
|
|
50
50
|
validatePatch(value.patch);
|
|
51
51
|
for (const [key, field] of Object.entries(value.patch)) {
|
|
52
|
-
|
|
53
|
-
? value.scope
|
|
54
|
-
|
|
52
|
+
const valid = key === "response"
|
|
53
|
+
? value.scope === "session" && typeof field === "string"
|
|
54
|
+
: key === "lazy"
|
|
55
|
+
? field !== null
|
|
56
|
+
: PATCH_KEYS.has(key) && isObject(field);
|
|
57
|
+
if (!PATCH_KEYS.has(key) || !valid) {
|
|
58
|
+
throw new Error("Recent State Flow patches may contain hot object planes, ordinary-JSON lazy state, and a session response string");
|
|
55
59
|
}
|
|
56
60
|
}
|
|
57
61
|
}
|
|
@@ -3,6 +3,32 @@ import { createHash } from "node:crypto";
|
|
|
3
3
|
export type JsonValue = null | boolean | number | string | JsonValue[] | JsonObject;
|
|
4
4
|
export interface JsonObject { [key: string]: JsonValue }
|
|
5
5
|
|
|
6
|
+
const ARRAY_INDEX_SELECTOR = /^\[(0|[1-9]\d*)\]$/;
|
|
7
|
+
|
|
8
|
+
function isIndexedArrayPatch(value: JsonObject): boolean {
|
|
9
|
+
const keys = Object.keys(value);
|
|
10
|
+
return keys.length > 0 && keys.every((key) => ARRAY_INDEX_SELECTOR.test(key));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function applyArrayPatch(state: JsonValue[], patch: JsonObject): JsonValue[] {
|
|
14
|
+
const next = structuredClone(state);
|
|
15
|
+
for (const [selector, value] of Object.entries(patch)) {
|
|
16
|
+
const match = ARRAY_INDEX_SELECTOR.exec(selector)!;
|
|
17
|
+
const index = Number(match[1]);
|
|
18
|
+
if (!Number.isSafeInteger(index) || index >= next.length) {
|
|
19
|
+
throw new Error(`State patch array index ${selector} is out of bounds for length ${next.length}`);
|
|
20
|
+
}
|
|
21
|
+
if (value === null) throw new Error(`State patch array index ${selector} cannot be deleted; replace the whole array instead`);
|
|
22
|
+
const current = next[index]!;
|
|
23
|
+
next[index] = Array.isArray(current) && isObject(value) && isIndexedArrayPatch(value)
|
|
24
|
+
? applyArrayPatch(current, value)
|
|
25
|
+
: isObject(current) && isObject(value)
|
|
26
|
+
? applyPatch(current, value)
|
|
27
|
+
: structuredClone(value);
|
|
28
|
+
}
|
|
29
|
+
return next;
|
|
30
|
+
}
|
|
31
|
+
|
|
6
32
|
export function applyPatch(state: JsonObject, patch: JsonObject): JsonObject {
|
|
7
33
|
const next: JsonObject = structuredClone(state);
|
|
8
34
|
for (const [key, value] of Object.entries(patch)) {
|
|
@@ -11,9 +37,11 @@ export function applyPatch(state: JsonObject, patch: JsonObject): JsonObject {
|
|
|
11
37
|
continue;
|
|
12
38
|
}
|
|
13
39
|
const current = next[key];
|
|
14
|
-
const materialized =
|
|
15
|
-
?
|
|
16
|
-
:
|
|
40
|
+
const materialized = Array.isArray(current) && isObject(value) && isIndexedArrayPatch(value)
|
|
41
|
+
? applyArrayPatch(current, value)
|
|
42
|
+
: isObject(current) && isObject(value)
|
|
43
|
+
? applyPatch(current, value)
|
|
44
|
+
: structuredClone(value);
|
|
17
45
|
Object.defineProperty(next, key, {
|
|
18
46
|
value: materialized,
|
|
19
47
|
enumerable: true,
|