@llblab/pi-kit 0.14.1 → 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 +11 -0
- package/README.md +3 -3
- package/node_modules/@llblab/pi-codex-usage/CHANGELOG.md +5 -0
- package/node_modules/@llblab/pi-codex-usage/README.md +6 -0
- package/node_modules/@llblab/pi-codex-usage/index.ts +81 -7
- package/node_modules/@llblab/pi-codex-usage/package.json +1 -1
- 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 +12 -0
- package/node_modules/@llblab/pi-telegram/README.md +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/activity.d.ts +2 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/activity.js +12 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +3 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +7 -2
- 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/queue.d.ts +2 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/queue.js +5 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/replies.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/replies.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 +59 -7
- 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/activity.ts +14 -0
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +10 -2
- 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/queue.ts +7 -1
- package/node_modules/@llblab/pi-telegram/lib/replies.ts +2 -0
- package/node_modules/@llblab/pi-telegram/lib/routing.ts +90 -19
- 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 +4 -4
|
@@ -10,6 +10,7 @@ const STATE_FILE = "state.json";
|
|
|
10
10
|
const CHECKPOINT_FILE = "checkpoint.json";
|
|
11
11
|
const PATCHES_FILE = "patches.jsonl";
|
|
12
12
|
const META_FILE = "meta.json";
|
|
13
|
+
const RUNTIME_FILE = "runtime.json";
|
|
13
14
|
/** Semantic files contain no runtime envelope; temporal boundaries and CWD ownership live in meta.json. */
|
|
14
15
|
export function serializeScopeStream(stream, scope, cwdIdentity) {
|
|
15
16
|
validateScopeStream(stream, scope);
|
|
@@ -113,7 +114,7 @@ export function temporalScopePaths(cwd, sessionId, scope, repositoryRoot, sessio
|
|
|
113
114
|
}
|
|
114
115
|
export function sessionRuntimePaths(cwd, sessionId, repositoryRoot, sessionKey = sessionId) {
|
|
115
116
|
const directory = sessionScopePaths(cwd, sessionId, repositoryRoot, sessionKey).directory;
|
|
116
|
-
return { config: join(directory, "config.json"), meta: join(directory, META_FILE) };
|
|
117
|
+
return { config: join(directory, "config.json"), runtime: join(directory, RUNTIME_FILE), meta: join(directory, META_FILE) };
|
|
117
118
|
}
|
|
118
119
|
/** Unsupported state.json presence never becomes an anchored checkpoint. */
|
|
119
120
|
export function loadScopeStream(cwd, sessionId, scope, repositoryRoot, sessionKey = sessionId) {
|
|
@@ -128,7 +129,7 @@ export function captureTemporalFileBases(cwd, sessionId, repositoryRoot, session
|
|
|
128
129
|
const paths = ["global", "cwd", "session"].flatMap((scope) => {
|
|
129
130
|
const pair = temporalScopePaths(cwd, sessionId, scope, repositoryRoot, sessionKey);
|
|
130
131
|
const runtime = scope === "session" ? sessionRuntimePaths(cwd, sessionId, repositoryRoot, sessionKey) : undefined;
|
|
131
|
-
return [pair.checkpoint, pair.patches, join(pair.directory, STATE_FILE), ...(runtime === undefined ? [pair.meta] : [runtime.config, runtime.meta])];
|
|
132
|
+
return [pair.checkpoint, pair.patches, join(pair.directory, STATE_FILE), ...(runtime === undefined ? [pair.meta] : [runtime.config, runtime.runtime, runtime.meta])];
|
|
132
133
|
});
|
|
133
134
|
return captureOwnedFileBases(paths, repositoryRoot);
|
|
134
135
|
}
|
|
@@ -151,6 +152,10 @@ export function temporalStateFileUpdates(cwd, sessionId, view, scopes, repositor
|
|
|
151
152
|
/** Merge authoritative owned leaves while preserving forward-compatible metadata siblings. */
|
|
152
153
|
export function serializeScopeMetadata(registry, stream, scope, cwdIdentity, existingSource) {
|
|
153
154
|
const existing = parseMetadataDocument(existingSource, `State Flow metadata`);
|
|
155
|
+
if (scope === "session") {
|
|
156
|
+
for (const key of ["identity", "lineage", "step", "specification", "validation", "bootstrap", "remotePublication", "revision", "temporalRevision", "publication"])
|
|
157
|
+
delete existing[key];
|
|
158
|
+
}
|
|
154
159
|
const sources = serializeScopeStream(stream, scope, cwdIdentity);
|
|
155
160
|
const value = {
|
|
156
161
|
...existing,
|
|
@@ -276,7 +281,7 @@ export function isStateFlowOwnedPath(candidate, repositoryRoot = getDurableRepos
|
|
|
276
281
|
return cwdKey(segments[0])
|
|
277
282
|
&& sessionKey(segments[1])
|
|
278
283
|
&& (segments[2] === STATE_FILE || segments[2] === CHECKPOINT_FILE || segments[2] === PATCHES_FILE
|
|
279
|
-
|| segments[2] === "config.json" || segments[2] === META_FILE);
|
|
284
|
+
|| segments[2] === "config.json" || segments[2] === RUNTIME_FILE || segments[2] === META_FILE);
|
|
280
285
|
}
|
|
281
286
|
return false;
|
|
282
287
|
}
|
|
@@ -6,3 +6,5 @@ export declare function resumeEpisode(snapshot: Snapshot, bootstrap: boolean): S
|
|
|
6
6
|
export declare function stopEpisode(snapshot: Snapshot): Snapshot;
|
|
7
7
|
/** Apply one user-run boundary while preserving checkpoint-owned runtime state. */
|
|
8
8
|
export declare function prepareRun(snapshot: Snapshot, prompt: string): boolean;
|
|
9
|
+
/** Retain the full prompt only while its run remains recoverable and unfinished. */
|
|
10
|
+
export declare function completeRun(snapshot: Snapshot): void;
|
|
@@ -25,3 +25,7 @@ export function prepareRun(snapshot, prompt) {
|
|
|
25
25
|
snapshot.meta.validation = undefined;
|
|
26
26
|
return true;
|
|
27
27
|
}
|
|
28
|
+
/** Retain the full prompt only while its run remains recoverable and unfinished. */
|
|
29
|
+
export function completeRun(snapshot) {
|
|
30
|
+
delete snapshot.meta.specification;
|
|
31
|
+
}
|
|
@@ -11,6 +11,11 @@ export interface StateFlowExtensionOptions {
|
|
|
11
11
|
telegram?: {
|
|
12
12
|
load?: StateFlowTelegramLoader;
|
|
13
13
|
};
|
|
14
|
+
/** Test/SDK capability override; repository config remains the Pi default. */
|
|
15
|
+
passive?: {
|
|
16
|
+
bootstrap?: boolean;
|
|
17
|
+
tools?: boolean;
|
|
18
|
+
};
|
|
14
19
|
}
|
|
15
20
|
export declare const PATCH_STATE_TOOL_NAME = "patch_state";
|
|
16
21
|
export declare const READ_STATE_TOOL_NAME = "read_state";
|
|
@@ -3,7 +3,7 @@ import { StringEnum, Type } from "@earendil-works/pi-ai";
|
|
|
3
3
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { Text } from "@earendil-works/pi-tui";
|
|
5
5
|
import { assistantToolCallCount, finalizedAssistantResponse, stateFlowProtocol } from "./protocol.js";
|
|
6
|
-
import { createPassiveContinuation, currentRunTrajectory, passiveContinuationMessages, runtimeContextMessage, VALIDATION_MESSAGE_TYPE, withoutPrivateValidation } from "./context.js";
|
|
6
|
+
import { createPassiveContinuation, currentRunTrajectory, lazyNavigationHint, passiveContinuationMessages, runtimeContextMessage, syntheticUser, VALIDATION_MESSAGE_TYPE, withoutPrivateValidation } from "./context.js";
|
|
7
7
|
import { ArtifactReadTracker } from "./acquisition.js";
|
|
8
8
|
import { loadStateFlowConfig } from "./config.js";
|
|
9
9
|
import { createStateFlowTelegramAdapter } from "./telegram.js";
|
|
@@ -16,15 +16,15 @@ import { emptyState, overlayStates, projectStateForModel } from "./state.js";
|
|
|
16
16
|
import { commitScopedTransition, stageAtomicScopePatches, stageScopedTransition, validateFinalEligibility } from "./transition.js";
|
|
17
17
|
import { discoverSnapshotData, hasPriorConversation, isNewSession, SNAPSHOT_ENTRY_TYPE } from "./session.js";
|
|
18
18
|
import { compactStatus, detailedStatus, STATUS_KEY } from "./status.js";
|
|
19
|
-
import { prepareRun, resumeEpisode, startEpisode, stopEpisode } from "./episode.js";
|
|
19
|
+
import { completeRun, prepareRun, resumeEpisode, startEpisode, stopEpisode } from "./episode.js";
|
|
20
20
|
import { recoverSnapshot } from "./recovery.js";
|
|
21
21
|
import { resolveRemotePublicationPolicy, serializeRemotePublicationPolicyDocument } from "./publication.js";
|
|
22
22
|
import { coalescePublicationTarget, createPublicationQueue } from "./publication.js";
|
|
23
23
|
import { acquirePublicationWorkerLease, loadPublicationQueue, publicationQueuePath, removePublicationQueue, savePublicationQueue } from "./publication.js";
|
|
24
24
|
import { runPublicationWorker } from "./publication.js";
|
|
25
25
|
import { getKnowledgeRoot, GlobalMarkdownDiscovery } from "./discovery.js";
|
|
26
|
-
import { isObject, sameJson } from "./json.js";
|
|
27
|
-
import { readStatePath } from "./query.js";
|
|
26
|
+
import { canonicalJson, isObject, sameJson } from "./json.js";
|
|
27
|
+
import { readProjectedState, readStatePath } from "./query.js";
|
|
28
28
|
import { cwdScopeKey, resolveSessionAddress, sessionScopeKey, } from "./durable.js";
|
|
29
29
|
import { projectRecentTransitionsWithLimit, RECENT_TRANSITION_LIMIT } from "./history.js";
|
|
30
30
|
import { appendStateFlowDiagnostic, projectDiagnosticContent, stateFlowLogPath } from "./logging.js";
|
|
@@ -95,7 +95,12 @@ function separatedFailure(error) {
|
|
|
95
95
|
}
|
|
96
96
|
export default function stateFlowExtension(pi, options = {}) {
|
|
97
97
|
const agentDir = options.agentDir ?? getAgentDir();
|
|
98
|
-
const
|
|
98
|
+
const loadedConfig = loadStateFlowConfig(agentDir, options.repositoryRoot);
|
|
99
|
+
const config = {
|
|
100
|
+
...loadedConfig,
|
|
101
|
+
passiveBootstrap: options.passive?.bootstrap ?? loadedConfig.passiveBootstrap,
|
|
102
|
+
passiveTools: options.passive?.tools ?? loadedConfig.passiveTools,
|
|
103
|
+
};
|
|
99
104
|
let snapshot = emptySnapshot();
|
|
100
105
|
let scopeStates = { global: emptyState(), cwd: emptyState(), session: emptyState() };
|
|
101
106
|
let branchHasSnapshot = false;
|
|
@@ -234,12 +239,16 @@ export default function stateFlowExtension(pi, options = {}) {
|
|
|
234
239
|
function installScopeStates() {
|
|
235
240
|
scopeStates = runtime?.view ? runtime.states() : { global: emptyState(), cwd: emptyState(), session: emptyState() };
|
|
236
241
|
}
|
|
242
|
+
function passiveToolsAvailable() {
|
|
243
|
+
return snapshot.config.enabled || config.passiveTools;
|
|
244
|
+
}
|
|
237
245
|
function syncStateFlowTools() {
|
|
238
246
|
const active = pi.getActiveTools();
|
|
239
247
|
const owned = [PATCH_STATE_TOOL_NAME, READ_STATE_TOOL_NAME];
|
|
240
|
-
|
|
248
|
+
const available = passiveToolsAvailable();
|
|
249
|
+
if (owned.every((name) => active.includes(name) === available))
|
|
241
250
|
return;
|
|
242
|
-
pi.setActiveTools(
|
|
251
|
+
pi.setActiveTools(available
|
|
243
252
|
? [...new Set([...active, ...owned])]
|
|
244
253
|
: active.filter((name) => !owned.includes(name)));
|
|
245
254
|
}
|
|
@@ -655,6 +664,15 @@ export default function stateFlowExtension(pi, options = {}) {
|
|
|
655
664
|
bootstrapContinuation = continuation;
|
|
656
665
|
}
|
|
657
666
|
}
|
|
667
|
+
if (!snapshot.config.enabled && (config.passiveBootstrap || config.passiveTools) && !runtime?.view) {
|
|
668
|
+
try {
|
|
669
|
+
runtime?.loadPassive();
|
|
670
|
+
installScopeStates();
|
|
671
|
+
}
|
|
672
|
+
catch (error) {
|
|
673
|
+
ctx.ui.notify(`State Flow passive memory is unavailable: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
674
|
+
}
|
|
675
|
+
}
|
|
658
676
|
if (snapshot.config.enabled)
|
|
659
677
|
deferArtifactRefresh();
|
|
660
678
|
syncStateFlowTools();
|
|
@@ -762,37 +780,44 @@ export default function stateFlowExtension(pi, options = {}) {
|
|
|
762
780
|
pi.registerTool({
|
|
763
781
|
name: READ_STATE_TOOL_NAME,
|
|
764
782
|
label: "Read State",
|
|
765
|
-
description: "Read State Flow
|
|
766
|
-
promptSnippet: "Read
|
|
783
|
+
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.",
|
|
784
|
+
promptSnippet: "Read exact state values, structures, or historical semantic patches",
|
|
767
785
|
parameters: Type.Object({
|
|
768
|
-
path: Type.Optional(Type.String({ description: "
|
|
769
|
-
|
|
770
|
-
|
|
786
|
+
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" })),
|
|
787
|
+
paths: Type.Optional(Type.Array(Type.String(), { minItems: 1, description: "Ordered query paths evaluated as one all-or-error read" })),
|
|
788
|
+
projection: Type.Optional(StringEnum(["value", "keys", "patch"], { description: "Value snapshot by default, structural keys with minimal meta, or the selected historical semantic patch" })),
|
|
771
789
|
}, { additionalProperties: false }),
|
|
772
790
|
async execute(_toolCallId, params, signal) {
|
|
773
791
|
try {
|
|
774
|
-
if (!
|
|
775
|
-
throw new Error("State Flow
|
|
792
|
+
if (!passiveToolsAvailable())
|
|
793
|
+
throw new Error("State Flow tools are disabled by configuration");
|
|
776
794
|
if (signal?.aborted)
|
|
777
795
|
throw new Error("State Flow read was aborted");
|
|
778
796
|
if (!runtime?.view)
|
|
779
797
|
throw new Error("State Flow temporal runtime is unavailable");
|
|
780
|
-
if (params.path
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
798
|
+
if (params.path === undefined && params.paths === undefined)
|
|
799
|
+
throw new Error("read_state requires path or paths");
|
|
800
|
+
if (params.path !== undefined && params.paths !== undefined)
|
|
801
|
+
throw new Error("read_state accepts path or paths, not both");
|
|
802
|
+
{
|
|
803
|
+
const paths = params.paths ?? [params.path];
|
|
804
|
+
if (paths.length === 1 && /^(?:global|cwd|session)\.patches(?:\[\d+\])?$/.test(paths[0])) {
|
|
805
|
+
if (params.projection !== undefined && params.projection !== "value")
|
|
806
|
+
throw new Error("Scope patch paths support only the value projection");
|
|
807
|
+
const result = readStatePath(runtime.view, paths[0]);
|
|
808
|
+
if (!("patch" in result))
|
|
809
|
+
throw new Error("Expected a scope patch path");
|
|
810
|
+
return {
|
|
811
|
+
content: [{ type: "text", text: `\n${JSON.stringify({ patch: result.patch })}` }],
|
|
812
|
+
details: { path: paths[0], transitionId: result.boundary.id },
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
const result = readProjectedState(runtime.view, paths, params.projection);
|
|
784
816
|
return {
|
|
785
817
|
content: [{ type: "text", text: `\n${JSON.stringify(result)}` }],
|
|
786
|
-
details: { path: params.path,
|
|
818
|
+
details: { ...(params.path === undefined ? { paths } : { path: params.path }), projection: params.projection ?? "value" },
|
|
787
819
|
};
|
|
788
820
|
}
|
|
789
|
-
const { offset = 0, scope = "effective" } = params;
|
|
790
|
-
const state = runtime.read(offset, scope === "effective" ? undefined : scope);
|
|
791
|
-
const boundary = runtime.view.lineage.at(-1 - offset);
|
|
792
|
-
return {
|
|
793
|
-
content: [{ type: "text", text: `\n${JSON.stringify({ offset, scope, boundary, state: projectStateForModel(state) })}` }],
|
|
794
|
-
details: { offset, scope, transitionId: boundary.id },
|
|
795
|
-
};
|
|
796
821
|
}
|
|
797
822
|
catch (error) {
|
|
798
823
|
throw separatedFailure(error);
|
|
@@ -827,8 +852,8 @@ export default function stateFlowExtension(pi, options = {}) {
|
|
|
827
852
|
},
|
|
828
853
|
async execute(toolCallId, params, signal, _onUpdate, ctx) {
|
|
829
854
|
try {
|
|
830
|
-
if (!
|
|
831
|
-
throw new Error("State Flow
|
|
855
|
+
if (!passiveToolsAvailable())
|
|
856
|
+
throw new Error("State Flow tools are disabled by configuration");
|
|
832
857
|
if (signal?.aborted)
|
|
833
858
|
throw new Error("State Flow patch was aborted before materialization");
|
|
834
859
|
if (!isObject(params))
|
|
@@ -858,17 +883,29 @@ export default function stateFlowExtension(pi, options = {}) {
|
|
|
858
883
|
}
|
|
859
884
|
if (params.final !== true)
|
|
860
885
|
throw new Error('patch_state requires at least one scope patch or {"final":true}');
|
|
886
|
+
if (!snapshot.config.enabled)
|
|
887
|
+
return { content: [{ type: "text", text: "\nState unchanged; passive turns have no terminal barrier." }], details: { final: false } };
|
|
861
888
|
validateFinalEligibility(scopeStates, skillReads.successful.values(), runtime.causalBasis(), artifactReads.successful.values());
|
|
862
889
|
terminalEligible = true;
|
|
863
890
|
return { content: [{ type: "text", text: "\nState iteration is terminal-eligible." }], details: { final: true } };
|
|
864
891
|
}
|
|
892
|
+
if (!runtime?.view) {
|
|
893
|
+
runtime ??= createRuntime(ctx);
|
|
894
|
+
runtime.prepare();
|
|
895
|
+
const publication = runtime.initialize(snapshot, true, undefined, true);
|
|
896
|
+
recordPolicyPublication(publication, ctx);
|
|
897
|
+
installScopeStates();
|
|
898
|
+
branchHasSnapshot = true;
|
|
899
|
+
}
|
|
900
|
+
runtime.migrateLegacyStorage();
|
|
901
|
+
installScopeStates();
|
|
865
902
|
const stage = stageAtomicScopePatches(scopeStates, patches, skillReads.successful.values(), runtime.causalBasis(), artifactReads.successful.values());
|
|
866
903
|
const semanticChange = ["global", "cwd", "session"].some((scope) => !sameJson(scopeStates[scope], stage.nextStates[scope]));
|
|
867
904
|
const provenanceChange = Object.values(stage.provenanceUpdates).some((updates) => Object.keys(updates).length > 0);
|
|
868
905
|
if (!semanticChange && !provenanceChange)
|
|
869
906
|
throw new Error('patch_state scope patches must materially update state or required provenance; omit them and use {"final":true} when unchanged');
|
|
870
907
|
commitStage(stage, ctx, false);
|
|
871
|
-
if (params.final === true) {
|
|
908
|
+
if (params.final === true && snapshot.config.enabled) {
|
|
872
909
|
terminalEligible = true;
|
|
873
910
|
}
|
|
874
911
|
updateUi(ctx);
|
|
@@ -1028,9 +1065,15 @@ export default function stateFlowExtension(pi, options = {}) {
|
|
|
1028
1065
|
bootstrap: snapshot.meta.bootstrap === true,
|
|
1029
1066
|
startPending: telegramStartPending,
|
|
1030
1067
|
}),
|
|
1031
|
-
state: (scope) =>
|
|
1032
|
-
|
|
1033
|
-
|
|
1068
|
+
state: (scope) => {
|
|
1069
|
+
const selected = scope === "effective"
|
|
1070
|
+
? overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session)
|
|
1071
|
+
: scopeStates[scope];
|
|
1072
|
+
return {
|
|
1073
|
+
...projectStateForModel(selected),
|
|
1074
|
+
...(Object.hasOwn(selected, "lazy") ? { lazy: structuredClone(selected.lazy) } : {}),
|
|
1075
|
+
};
|
|
1076
|
+
},
|
|
1034
1077
|
canStartNow: () => activeContext === undefined || activeContext.isIdle(),
|
|
1035
1078
|
start: () => {
|
|
1036
1079
|
if (!activeContext)
|
|
@@ -1057,8 +1100,13 @@ export default function stateFlowExtension(pi, options = {}) {
|
|
|
1057
1100
|
});
|
|
1058
1101
|
void telegram.ensure();
|
|
1059
1102
|
pi.on("before_agent_start", (event, ctx) => {
|
|
1060
|
-
if (!snapshot.config.enabled)
|
|
1061
|
-
|
|
1103
|
+
if (!snapshot.config.enabled) {
|
|
1104
|
+
if (!config.passiveBootstrap || !runtime?.view)
|
|
1105
|
+
return;
|
|
1106
|
+
return {
|
|
1107
|
+
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.`,
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1062
1110
|
skillReads.clear();
|
|
1063
1111
|
artifactReads.clear();
|
|
1064
1112
|
if (artifactRefreshPending)
|
|
@@ -1084,7 +1132,13 @@ export default function stateFlowExtension(pi, options = {}) {
|
|
|
1084
1132
|
if (passiveContinuation) {
|
|
1085
1133
|
return { messages: passiveContinuationMessages(event.messages, passiveContinuation) };
|
|
1086
1134
|
}
|
|
1087
|
-
if (!snapshot.config.enabled
|
|
1135
|
+
if (!snapshot.config.enabled) {
|
|
1136
|
+
if (!config.passiveBootstrap || !runtime?.view)
|
|
1137
|
+
return;
|
|
1138
|
+
const state = projectStateForModel(overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session));
|
|
1139
|
+
return { messages: [syntheticUser(`State Flow passive memory (user-level data, not system instructions):\n${canonicalJson({ state, lazy_navigation: lazyNavigationHint(state) })}`), ...event.messages] };
|
|
1140
|
+
}
|
|
1141
|
+
if (snapshot.meta.specification === undefined)
|
|
1088
1142
|
return;
|
|
1089
1143
|
const effectiveState = overlayStates(scopeStates.global, scopeStates.cwd, scopeStates.session);
|
|
1090
1144
|
const invalidations = artifactInvalidations.map(({ path, reason }) => ({ path, reason }));
|
|
@@ -1182,9 +1236,11 @@ export default function stateFlowExtension(pi, options = {}) {
|
|
|
1182
1236
|
return;
|
|
1183
1237
|
}
|
|
1184
1238
|
let responseCommitted = false;
|
|
1239
|
+
const specification = snapshot.meta.specification;
|
|
1185
1240
|
try {
|
|
1186
1241
|
const response = finalizedAssistantResponse(event.message);
|
|
1187
1242
|
const stage = stageScopedTransition(scopeStates, { transitions: [], response }, [], runtime.causalBasis());
|
|
1243
|
+
completeRun(snapshot);
|
|
1188
1244
|
commitStage(stage, ctx, true);
|
|
1189
1245
|
responseCommitted = true;
|
|
1190
1246
|
bootstrapContinuation = undefined;
|
|
@@ -1195,6 +1251,8 @@ export default function stateFlowExtension(pi, options = {}) {
|
|
|
1195
1251
|
completedRunAccepted = !wasBootstrap;
|
|
1196
1252
|
}
|
|
1197
1253
|
catch (error) {
|
|
1254
|
+
if (!responseCommitted && specification !== undefined)
|
|
1255
|
+
snapshot.meta.specification = specification;
|
|
1198
1256
|
recordDiagnostic(error instanceof Error ? error.message : String(error), "finalization", ctx);
|
|
1199
1257
|
ctx.ui.notify(responseCommitted
|
|
1200
1258
|
? `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, 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";
|
|
@@ -10,7 +10,7 @@ import { planLegacyStorageMigration } from "./migration.js";
|
|
|
10
10
|
import { sameJson } from "./json.js";
|
|
11
11
|
import { validateTemporalState } from "./temporal.js";
|
|
12
12
|
import { createSessionRuntime, parseSessionRuntime, serializeSessionRuntime } from "./snapshot.js";
|
|
13
|
-
import { assertTemporalFileBase, loadTemporalFileRevision, planTemporalPublication, temporalFileReceipts, withStoragePublicationLock } from "./storage.js";
|
|
13
|
+
import { acquirePublicationLock, assertTemporalFileBase, loadTemporalFileRevision, planTemporalPublication, temporalFileReceipts, withStoragePublicationLock } from "./storage.js";
|
|
14
14
|
const GIT_TIMEOUT_MS = 15_000;
|
|
15
15
|
const STATE_FLOW_COMMIT_TRAILER = "State-Flow-Durable: v1";
|
|
16
16
|
function git(repositoryRoot, args, options = {}) {
|
|
@@ -72,13 +72,7 @@ function withPublicationLock(repositoryRoot, action) {
|
|
|
72
72
|
const root = assertRepositoryRoot(lockedRoot);
|
|
73
73
|
const common = resolve(root, git(root, ["rev-parse", "--git-common-dir"]).stdout.trim());
|
|
74
74
|
const path = resolve(common, "state-flow-publication.lock");
|
|
75
|
-
|
|
76
|
-
try {
|
|
77
|
-
descriptor = openSync(path, "wx", 0o600);
|
|
78
|
-
}
|
|
79
|
-
catch (error) {
|
|
80
|
-
throw new Error(`State Flow publication lock is unavailable at ${path}; reconcile the active or interrupted publisher before retrying`, { cause: error });
|
|
81
|
-
}
|
|
75
|
+
const descriptor = acquirePublicationLock(path, (cause) => new Error(`State Flow publication lock is unavailable at ${path}; reconcile the active or interrupted publisher before retrying`, { cause }));
|
|
82
76
|
try {
|
|
83
77
|
writeFileSync(descriptor, `${process.pid}\n`);
|
|
84
78
|
return action(root);
|
|
@@ -180,16 +174,15 @@ export function loadTemporalRevision(cwd, sessionId, repositoryRoot, revision, s
|
|
|
180
174
|
const canonicalRuntime = sessionRuntimePaths(cwd, sessionId, root, sessionKey);
|
|
181
175
|
const readFile = revisionFileReader(root, revision, [
|
|
182
176
|
...scopePaths.flatMap(({ paths }) => [paths.checkpoint, paths.patches, paths.meta]),
|
|
183
|
-
canonicalRuntime.config, canonicalRuntime.
|
|
177
|
+
canonicalRuntime.config, canonicalRuntime.runtime,
|
|
184
178
|
]);
|
|
185
179
|
const files = [];
|
|
186
180
|
const scopes = {};
|
|
187
181
|
const provenance = { global: {}, cwd: {}, session: {} };
|
|
188
182
|
for (const { scope, paths } of scopePaths) {
|
|
189
183
|
const selected = revisionScopeFiles(readFile, paths);
|
|
190
|
-
files.push(selected.checkpoint, selected.patches,
|
|
191
|
-
|
|
192
|
-
provenance[scope] = parseScopeProvenance(selected.meta.content, selected.meta.path);
|
|
184
|
+
files.push(selected.checkpoint, selected.patches, selected.meta);
|
|
185
|
+
provenance[scope] = parseScopeProvenance(selected.meta.content, selected.meta.path);
|
|
193
186
|
try {
|
|
194
187
|
scopes[scope] = parseScopeStream(selected.checkpoint.content, selected.patches.content, scope, scope === "cwd" ? cwd : undefined, selected.meta.content);
|
|
195
188
|
}
|
|
@@ -198,16 +191,19 @@ export function loadTemporalRevision(cwd, sessionId, repositoryRoot, revision, s
|
|
|
198
191
|
}
|
|
199
192
|
}
|
|
200
193
|
const readRuntime = (paths) => ({
|
|
201
|
-
paths, config: readFile(paths.config), meta: readFile(paths.meta),
|
|
194
|
+
paths, config: readFile(paths.config), runtime: readFile(paths.runtime), meta: readFile(paths.meta),
|
|
202
195
|
});
|
|
203
|
-
const { paths: runtimePaths, config, meta } = readRuntime(canonicalRuntime);
|
|
204
|
-
files.push(config,
|
|
205
|
-
const document = parseSessionRuntime(config.content,
|
|
196
|
+
const { paths: runtimePaths, config, runtime, meta } = readRuntime(canonicalRuntime);
|
|
197
|
+
files.push(config, runtime);
|
|
198
|
+
const document = parseSessionRuntime(config.content, runtime.content, cwd, sessionId, meta.content);
|
|
206
199
|
if (document === undefined)
|
|
207
200
|
return { base: { head: revision, files }, scopes, provenance };
|
|
208
|
-
provenance.session
|
|
201
|
+
if (Object.keys(provenance.session).length === 0 && document.meta.artifacts !== undefined) {
|
|
202
|
+
provenance.session = parseArtifactProvenanceRegistry(document.meta.artifacts, "State Flow session artifact provenance");
|
|
203
|
+
}
|
|
204
|
+
const runtimeOwnerPath = runtime.content === undefined ? runtimePaths.meta : runtimePaths.runtime;
|
|
209
205
|
const owner = git(root, ["log", "-1", "--format=%H", revision, "--",
|
|
210
|
-
relativeOwnedPath(runtimePaths.config, root), relativeOwnedPath(
|
|
206
|
+
relativeOwnedPath(runtimePaths.config, root), relativeOwnedPath(runtimeOwnerPath, root),
|
|
211
207
|
]).stdout.trim();
|
|
212
208
|
assertReadableRevision(root, owner);
|
|
213
209
|
let temporalRevision = document.meta.temporalRevision === undefined || document.meta.temporalRevision === "self"
|
|
@@ -342,7 +338,7 @@ export function adoptFileStateToGit(cwd, sessionId, repositoryRoot, revision, sn
|
|
|
342
338
|
throw new Error("Git adoption must preserve the semantic step");
|
|
343
339
|
const runtime = createSessionRuntime(snapshot, cwd, sessionId, selected.view.lineage, "unconfirmed", selected.provenance.session);
|
|
344
340
|
runtime.meta.temporalRevision = "self";
|
|
345
|
-
const sources = serializeSessionRuntime(runtime, cwd, sessionId
|
|
341
|
+
const sources = serializeSessionRuntime(runtime, cwd, sessionId);
|
|
346
342
|
initializeGitRepository(repositoryRoot);
|
|
347
343
|
return withPublicationLock(repositoryRoot, (root) => {
|
|
348
344
|
const current = captureTemporalBaseUnderLock(cwd, sessionId, root, sessionKey);
|
|
@@ -351,13 +347,14 @@ export function adoptFileStateToGit(cwd, sessionId, repositoryRoot, revision, sn
|
|
|
351
347
|
const provenancePaths = new Set([
|
|
352
348
|
temporalScopePaths(cwd, sessionId, "global", root, sessionKey).meta,
|
|
353
349
|
temporalScopePaths(cwd, sessionId, "cwd", root, sessionKey).meta,
|
|
350
|
+
temporalScopePaths(cwd, sessionId, "session", root, sessionKey).meta,
|
|
354
351
|
]);
|
|
355
352
|
// Preserve exact valid scope bytes; only runtime provenance changes representation.
|
|
356
353
|
const updates = current.files.filter(({ path, identity }) => path.endsWith("checkpoint.json")
|
|
357
354
|
|| path.endsWith("patches.jsonl")
|
|
358
355
|
|| (provenancePaths.has(path) && identity !== "missing"))
|
|
359
356
|
.map(({ path, content }) => ({ path, content: content }));
|
|
360
|
-
updates.push({ path: paths.config, content: sources.config }, { path: paths.
|
|
357
|
+
updates.push({ path: paths.config, content: sources.config }, { path: paths.runtime, content: sources.runtime });
|
|
361
358
|
const existing = current.head && updates.every(({ path, content }) => revisionFile(root, current.head, path).content === content)
|
|
362
359
|
? loadTemporalRevision(cwd, sessionId, root, current.head, sessionKey) : undefined;
|
|
363
360
|
if (existing && (!existing.runtime || !sameJson({ lineage: existing.runtime.document.meta.lineage, scopes: existing.scopes }, selected.view))) {
|
|
@@ -379,7 +376,7 @@ function includeUncommittedCohort(cwd, sessionId, root, current, updates, change
|
|
|
379
376
|
return { scope, pair: desired([paths.checkpoint, paths.patches]) };
|
|
380
377
|
});
|
|
381
378
|
const runtimePaths = runtime ? sessionRuntimePaths(cwd, sessionId, root, sessionKey) : undefined;
|
|
382
|
-
const runtimePair = runtimePaths ? desired([runtimePaths.config, runtimePaths.
|
|
379
|
+
const runtimePair = runtimePaths ? desired([runtimePaths.config, runtimePaths.runtime]) : [];
|
|
383
380
|
const readFile = current.head === undefined ? undefined : revisionFileReader(root, current.head, [...pairs.flatMap(({ pair }) => pair), ...runtimePair].map(({ path }) => path));
|
|
384
381
|
const absentFromHead = (files) => files.some(({ path, content }) => readFile === undefined || readFile(path).content !== content);
|
|
385
382
|
for (const { scope, pair } of pairs) {
|
|
@@ -405,7 +402,7 @@ export function publishTemporalStateToGit(cwd, sessionId, view, scopes, base, re
|
|
|
405
402
|
if (runtime?.meta.publication === "files")
|
|
406
403
|
throw new Error("Git publication requires explicit Git provenance");
|
|
407
404
|
const runtimePaths = sessionRuntimePaths(cwd, sessionId, root, sessionKey);
|
|
408
|
-
const previousRuntime = parseSessionRuntime(current.files.find(({ path }) => path === runtimePaths.config)?.content, current.files.find(({ path }) => path === runtimePaths.
|
|
405
|
+
const previousRuntime = parseSessionRuntime(current.files.find(({ path }) => path === runtimePaths.config)?.content, current.files.find(({ path }) => path === runtimePaths.runtime)?.content, cwd, sessionId, current.files.find(({ path }) => path === runtimePaths.meta)?.content);
|
|
409
406
|
if (previousRuntime?.meta.publication === "files")
|
|
410
407
|
throw new Error("File-only storage requires explicit full-cohort Git adoption");
|
|
411
408
|
const runtimeOnly = runtime?.meta.temporalRevision !== undefined && runtime.meta.temporalRevision !== "self";
|
|
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { isJsonValue, isObject, sameJson, validatePatch } from "./json.js";
|
|
3
3
|
export const RECENT_TRANSITION_LIMIT = 7;
|
|
4
4
|
const SCOPES = new Set(["global", "cwd", "session"]);
|
|
5
|
-
const PATCH_KEYS = new Set(["artifacts", "contract", "working", "response"]);
|
|
5
|
+
const PATCH_KEYS = new Set(["artifacts", "contract", "working", "response", "lazy"]);
|
|
6
6
|
/** Normalize accepted replacements into recursive-merge replay, including removals. */
|
|
7
7
|
function replayPatch(before, after) {
|
|
8
8
|
const entries = [];
|
|
@@ -28,9 +28,13 @@ function validateScopedPatch(value) {
|
|
|
28
28
|
}
|
|
29
29
|
validatePatch(value.patch);
|
|
30
30
|
for (const [key, field] of Object.entries(value.patch)) {
|
|
31
|
-
|
|
32
|
-
? value.scope
|
|
33
|
-
|
|
31
|
+
const valid = key === "response"
|
|
32
|
+
? value.scope === "session" && typeof field === "string"
|
|
33
|
+
: key === "lazy"
|
|
34
|
+
? field !== null
|
|
35
|
+
: PATCH_KEYS.has(key) && isObject(field);
|
|
36
|
+
if (!PATCH_KEYS.has(key) || !valid) {
|
|
37
|
+
throw new Error("Recent State Flow patches may contain hot object planes, ordinary-JSON lazy state, and a session response string");
|
|
34
38
|
}
|
|
35
39
|
}
|
|
36
40
|
}
|
|
@@ -1,4 +1,28 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
const ARRAY_INDEX_SELECTOR = /^\[(0|[1-9]\d*)\]$/;
|
|
3
|
+
function isIndexedArrayPatch(value) {
|
|
4
|
+
const keys = Object.keys(value);
|
|
5
|
+
return keys.length > 0 && keys.every((key) => ARRAY_INDEX_SELECTOR.test(key));
|
|
6
|
+
}
|
|
7
|
+
function applyArrayPatch(state, patch) {
|
|
8
|
+
const next = structuredClone(state);
|
|
9
|
+
for (const [selector, value] of Object.entries(patch)) {
|
|
10
|
+
const match = ARRAY_INDEX_SELECTOR.exec(selector);
|
|
11
|
+
const index = Number(match[1]);
|
|
12
|
+
if (!Number.isSafeInteger(index) || index >= next.length) {
|
|
13
|
+
throw new Error(`State patch array index ${selector} is out of bounds for length ${next.length}`);
|
|
14
|
+
}
|
|
15
|
+
if (value === null)
|
|
16
|
+
throw new Error(`State patch array index ${selector} cannot be deleted; replace the whole array instead`);
|
|
17
|
+
const current = next[index];
|
|
18
|
+
next[index] = Array.isArray(current) && isObject(value) && isIndexedArrayPatch(value)
|
|
19
|
+
? applyArrayPatch(current, value)
|
|
20
|
+
: isObject(current) && isObject(value)
|
|
21
|
+
? applyPatch(current, value)
|
|
22
|
+
: structuredClone(value);
|
|
23
|
+
}
|
|
24
|
+
return next;
|
|
25
|
+
}
|
|
2
26
|
export function applyPatch(state, patch) {
|
|
3
27
|
const next = structuredClone(state);
|
|
4
28
|
for (const [key, value] of Object.entries(patch)) {
|
|
@@ -7,9 +31,11 @@ export function applyPatch(state, patch) {
|
|
|
7
31
|
continue;
|
|
8
32
|
}
|
|
9
33
|
const current = next[key];
|
|
10
|
-
const materialized =
|
|
11
|
-
?
|
|
12
|
-
:
|
|
34
|
+
const materialized = Array.isArray(current) && isObject(value) && isIndexedArrayPatch(value)
|
|
35
|
+
? applyArrayPatch(current, value)
|
|
36
|
+
: isObject(current) && isObject(value)
|
|
37
|
+
? applyPatch(current, value)
|
|
38
|
+
: structuredClone(value);
|
|
13
39
|
Object.defineProperty(next, key, {
|
|
14
40
|
value: materialized,
|
|
15
41
|
enumerable: true,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { lstatSync, readFileSync, readdirSync } from "node:fs";
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
4
|
import { captureOwnedFileBases, cwdScopePaths, parseScopeStream, serializeScopeMetadata, serializeScopeStream, sessionScopeKey, sessionScopePaths, } from "./durable.js";
|
|
5
|
-
import {
|
|
5
|
+
import { parseSessionRuntime, serializeSessionRuntime } from "./snapshot.js";
|
|
6
6
|
/** Discover every owner-proven CWD and session cohort beneath the configured store. */
|
|
7
7
|
function migrationDirectories(cwd, sessionId, root, sessionKey) {
|
|
8
8
|
const selectedCwd = cwdScopePaths(cwd, root).directory;
|
|
@@ -58,12 +58,14 @@ function migrationDirectories(cwd, sessionId, root, sessionKey) {
|
|
|
58
58
|
if (!identity || typeof identity !== "object" || Array.isArray(identity))
|
|
59
59
|
continue;
|
|
60
60
|
const owner = identity;
|
|
61
|
-
if (owner.cwd === cwdOwner && typeof owner.sessionId === "string" && owner.sessionId.length > 0)
|
|
62
|
-
directories.push({ directory, scope: "session" });
|
|
61
|
+
if (owner.cwd === cwdOwner && typeof owner.sessionId === "string" && owner.sessionId.length > 0) {
|
|
62
|
+
directories.push({ directory, scope: "session", cwdIdentity: cwdOwner, sessionId: owner.sessionId });
|
|
63
|
+
}
|
|
63
64
|
}
|
|
64
65
|
}
|
|
65
|
-
if (!directories.some(({ directory }) => directory === selectedSession))
|
|
66
|
-
directories.push({ directory: selectedSession, scope: "session" });
|
|
66
|
+
if (!directories.some(({ directory }) => directory === selectedSession)) {
|
|
67
|
+
directories.push({ directory: selectedSession, scope: "session", cwdIdentity: resolve(cwd), sessionId });
|
|
68
|
+
}
|
|
67
69
|
return directories;
|
|
68
70
|
}
|
|
69
71
|
/** Read-only activation eligibility, before global migration or any repository publication. */
|
|
@@ -82,12 +84,16 @@ export function hasCwdMaterialization(cwd, repositoryRoot) {
|
|
|
82
84
|
/** Detect predecessor snapshots or temporal envelopes without mutating the store. */
|
|
83
85
|
export function hasLegacyStateSources(cwd, sessionId, repositoryRoot, sessionKey = sessionId) {
|
|
84
86
|
const root = resolve(repositoryRoot);
|
|
85
|
-
return migrationDirectories(cwd, sessionId, root, sessionKey).some(({ directory }) => {
|
|
87
|
+
return migrationDirectories(cwd, sessionId, root, sessionKey).some(({ directory, scope }) => {
|
|
86
88
|
if (lstatSync(join(directory, "checkpoint.json"), { throwIfNoEntry: false }) === undefined)
|
|
87
89
|
return false;
|
|
88
90
|
try {
|
|
89
91
|
const meta = JSON.parse(readFileSync(join(directory, "meta.json"), "utf8"));
|
|
90
|
-
|
|
92
|
+
if (meta === null || typeof meta !== "object" || !("temporal" in meta))
|
|
93
|
+
return true;
|
|
94
|
+
return scope === "session"
|
|
95
|
+
&& lstatSync(join(directory, "runtime.json"), { throwIfNoEntry: false }) === undefined
|
|
96
|
+
&& ("identity" in meta || "lineage" in meta);
|
|
91
97
|
}
|
|
92
98
|
catch {
|
|
93
99
|
return true;
|
|
@@ -98,31 +104,41 @@ export function hasLegacyStateSources(cwd, sessionId, repositoryRoot, sessionKey
|
|
|
98
104
|
export function planLegacyStorageMigration(cwd, sessionId, repositoryRoot, _origin, sessionKey = sessionId) {
|
|
99
105
|
const root = resolve(repositoryRoot);
|
|
100
106
|
const directories = migrationDirectories(cwd, sessionId, root, sessionKey);
|
|
101
|
-
const paths = directories.flatMap(({ directory }) => [
|
|
107
|
+
const paths = directories.flatMap(({ directory, scope }) => [
|
|
102
108
|
join(directory, "checkpoint.json"), join(directory, "patches.jsonl"), join(directory, "meta.json"),
|
|
109
|
+
...(scope === "session" ? [join(directory, "config.json"), join(directory, "runtime.json")] : []),
|
|
103
110
|
]);
|
|
104
111
|
const bases = captureOwnedFileBases(paths, root);
|
|
105
112
|
const byPath = new Map(bases.map((base) => [base.path, base]));
|
|
106
113
|
const updates = [];
|
|
107
114
|
const scopes = [];
|
|
108
|
-
for (const { scope, directory, cwdIdentity } of directories) {
|
|
115
|
+
for (const { scope, directory, cwdIdentity, sessionId: ownedSessionId } of directories) {
|
|
109
116
|
const checkpoint = byPath.get(join(directory, "checkpoint.json"));
|
|
110
117
|
const patches = byPath.get(join(directory, "patches.jsonl"));
|
|
111
118
|
const meta = byPath.get(join(directory, "meta.json"));
|
|
112
119
|
if (checkpoint.content !== undefined) {
|
|
113
|
-
const stream = parseScopeStream(checkpoint.content, patches.content, scope, cwdIdentity, meta.content);
|
|
114
|
-
const source = serializeScopeStream(stream, scope, cwdIdentity);
|
|
115
|
-
|
|
120
|
+
const stream = parseScopeStream(checkpoint.content, patches.content, scope, scope === "cwd" ? cwdIdentity : undefined, meta.content);
|
|
121
|
+
const source = serializeScopeStream(stream, scope, scope === "cwd" ? cwdIdentity : undefined);
|
|
122
|
+
const metadata = serializeScopeMetadata(undefined, stream, scope, scope === "cwd" ? cwdIdentity : undefined, meta.content);
|
|
123
|
+
const cohortUpdates = [
|
|
124
|
+
...(checkpoint.content === source.checkpoint ? [] : [{ path: checkpoint.path, content: source.checkpoint }]),
|
|
125
|
+
...(patches.content === source.patches ? [] : [{ path: patches.path, content: source.patches }]),
|
|
126
|
+
...(meta.content === metadata ? [] : [{ path: meta.path, content: metadata }]),
|
|
127
|
+
];
|
|
116
128
|
if (scope === "session") {
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
runtime
|
|
120
|
-
|
|
129
|
+
const config = byPath.get(join(directory, "config.json"));
|
|
130
|
+
const runtimeFile = byPath.get(join(directory, "runtime.json"));
|
|
131
|
+
const runtime = parseSessionRuntime(config.content, runtimeFile.content, cwdIdentity, ownedSessionId, meta.content);
|
|
132
|
+
if (runtime !== undefined) {
|
|
133
|
+
const serialized = serializeSessionRuntime(runtime, cwdIdentity, ownedSessionId);
|
|
134
|
+
if (runtimeFile.content !== serialized.runtime)
|
|
135
|
+
cohortUpdates.push({ path: runtimeFile.path, content: serialized.runtime });
|
|
136
|
+
}
|
|
121
137
|
}
|
|
122
|
-
if (
|
|
138
|
+
if (cohortUpdates.length > 0) {
|
|
123
139
|
if (!scopes.includes(scope))
|
|
124
140
|
scopes.push(scope);
|
|
125
|
-
updates.push(
|
|
141
|
+
updates.push(...cohortUpdates);
|
|
126
142
|
}
|
|
127
143
|
continue;
|
|
128
144
|
}
|