@llblab/pi-kit 0.37.0 → 0.38.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 +10 -0
- package/README.md +3 -3
- package/node_modules/@llblab/pi-state-flow/AGENTS.md +7 -3
- package/node_modules/@llblab/pi-state-flow/BACKLOG.md +679 -2
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +17 -0
- package/node_modules/@llblab/pi-state-flow/README.md +30 -14
- package/node_modules/@llblab/pi-state-flow/api/modules.ts +11 -0
- package/node_modules/@llblab/pi-state-flow/dist/api/modules.d.ts +3 -0
- package/node_modules/@llblab/pi-state-flow/dist/api/modules.js +3 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.d.ts +7 -4
- package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.js +77 -36
- package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.d.ts +2 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.js +26 -27
- package/node_modules/@llblab/pi-state-flow/dist/lib/config.d.ts +3 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/config.js +13 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/context.d.ts +27 -21
- package/node_modules/@llblab/pi-state-flow/dist/lib/context.js +144 -46
- package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.js +14 -18
- package/node_modules/@llblab/pi-state-flow/dist/lib/durable.d.ts +6 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/durable.js +60 -50
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +1049 -310
- package/node_modules/@llblab/pi-state-flow/dist/lib/history.d.ts +4 -4
- package/node_modules/@llblab/pi-state-flow/dist/lib/history.js +15 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/json.d.ts +2 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/json.js +2 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/memory.d.ts +3 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/memory.js +7 -7
- package/node_modules/@llblab/pi-state-flow/dist/lib/migration.d.ts +29 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/migration.js +186 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/model-interface.d.ts +17 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/model-interface.js +158 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/module-registry.d.ts +21 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/module-registry.js +108 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/operation.js +3 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/ownership.d.ts +4 -4
- package/node_modules/@llblab/pi-state-flow/dist/lib/ownership.js +5 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/planes.d.ts +29 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/planes.js +84 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.d.ts +6 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +33 -16
- package/node_modules/@llblab/pi-state-flow/dist/lib/query.d.ts +21 -4
- package/node_modules/@llblab/pi-state-flow/dist/lib/query.js +53 -13
- package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.js +5 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/rehydration.d.ts +3 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/rehydration.js +10 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.d.ts +84 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.js +570 -20
- package/node_modules/@llblab/pi-state-flow/dist/lib/selection.d.ts +10 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/selection.js +32 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/semantic-module.d.ts +79 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/semantic-module.js +187 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/session.d.ts +98 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/session.js +398 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/skills.d.ts +7 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/skills.js +55 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.d.ts +67 -14
- package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.js +129 -9
- package/node_modules/@llblab/pi-state-flow/dist/lib/state.d.ts +26 -14
- package/node_modules/@llblab/pi-state-flow/dist/lib/state.js +61 -19
- package/node_modules/@llblab/pi-state-flow/dist/lib/status.d.ts +14 -9
- package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +59 -22
- package/node_modules/@llblab/pi-state-flow/dist/lib/storage.d.ts +111 -10
- package/node_modules/@llblab/pi-state-flow/dist/lib/storage.js +728 -11
- package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.d.ts +10 -7
- package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +50 -14
- package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.d.ts +40 -4
- package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.js +247 -43
- package/node_modules/@llblab/pi-state-flow/dist/lib/transition.d.ts +41 -8
- package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +132 -39
- package/node_modules/@llblab/pi-state-flow/dist/package.json +6 -1
- package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-guide/SKILL.md +28 -16
- package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-memory/SKILL.md +12 -8
- package/node_modules/@llblab/pi-state-flow/docs/README.md +9 -0
- package/node_modules/@llblab/pi-state-flow/docs/architecture.md +76 -43
- package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +9 -1
- package/node_modules/@llblab/pi-state-flow/docs/fork-contract.md +6 -0
- package/node_modules/@llblab/pi-state-flow/docs/lazy-state.md +23 -19
- package/node_modules/@llblab/pi-state-flow/docs/protocol-composition-decision.md +66 -0
- package/node_modules/@llblab/pi-state-flow/docs/public-module-contract.md +73 -0
- package/node_modules/@llblab/pi-state-flow/docs/scope-selection-contract.md +814 -0
- package/node_modules/@llblab/pi-state-flow/docs/semantic-compatibility-decision.md +212 -0
- package/node_modules/@llblab/pi-state-flow/docs/semantic-extensibility-decision.md +75 -0
- package/node_modules/@llblab/pi-state-flow/docs/semantic-handoff-contract.md +174 -0
- package/node_modules/@llblab/pi-state-flow/docs/semantic-memory-evaluation.md +166 -0
- package/node_modules/@llblab/pi-state-flow/docs/semantic-module-contract.md +84 -0
- package/node_modules/@llblab/pi-state-flow/docs/single-scope-interface-contract.md +201 -0
- package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +22 -1
- package/node_modules/@llblab/pi-state-flow/docs/usage.md +32 -18
- package/node_modules/@llblab/pi-state-flow/lib/acquisition.ts +68 -31
- package/node_modules/@llblab/pi-state-flow/lib/artifact.ts +21 -22
- package/node_modules/@llblab/pi-state-flow/lib/config.ts +14 -1
- package/node_modules/@llblab/pi-state-flow/lib/context.ts +142 -46
- package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +14 -17
- package/node_modules/@llblab/pi-state-flow/lib/durable.ts +53 -44
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +897 -297
- package/node_modules/@llblab/pi-state-flow/lib/history.ts +16 -7
- package/node_modules/@llblab/pi-state-flow/lib/json.ts +2 -1
- package/node_modules/@llblab/pi-state-flow/lib/memory.ts +10 -9
- package/node_modules/@llblab/pi-state-flow/lib/migration.ts +189 -0
- package/node_modules/@llblab/pi-state-flow/lib/model-interface.ts +136 -0
- package/node_modules/@llblab/pi-state-flow/lib/module-registry.ts +119 -0
- package/node_modules/@llblab/pi-state-flow/lib/operation.ts +3 -2
- package/node_modules/@llblab/pi-state-flow/lib/ownership.ts +6 -6
- package/node_modules/@llblab/pi-state-flow/lib/planes.ts +98 -0
- package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +37 -16
- package/node_modules/@llblab/pi-state-flow/lib/query.ts +59 -18
- package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +5 -2
- package/node_modules/@llblab/pi-state-flow/lib/rehydration.ts +10 -1
- package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +574 -21
- package/node_modules/@llblab/pi-state-flow/lib/selection.ts +39 -0
- package/node_modules/@llblab/pi-state-flow/lib/semantic-module.ts +239 -0
- package/node_modules/@llblab/pi-state-flow/lib/session.ts +387 -3
- package/node_modules/@llblab/pi-state-flow/lib/skills.ts +45 -4
- package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +156 -16
- package/node_modules/@llblab/pi-state-flow/lib/state.ts +69 -28
- package/node_modules/@llblab/pi-state-flow/lib/status.ts +65 -31
- package/node_modules/@llblab/pi-state-flow/lib/storage.ts +770 -18
- package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +58 -20
- package/node_modules/@llblab/pi-state-flow/lib/temporal.ts +267 -50
- package/node_modules/@llblab/pi-state-flow/lib/transition.ts +184 -52
- package/node_modules/@llblab/pi-state-flow/package.json +6 -1
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-guide/SKILL.md +28 -16
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +12 -8
- package/node_modules/@llblab/pi-telegram/AGENTS.md +1 -1
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +8 -10
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +10 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-buttons.js +20 -18
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.d.ts +6 -6
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.js +23 -65
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound.js +3 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/prompts.d.ts +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/prompts.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/runtime.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/runtime.js +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.d.ts +4 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.js +24 -13
- package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/dist/skills/telegram-bridge/SKILL.md +5 -5
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +5 -5
- package/node_modules/@llblab/pi-telegram/docs/compact-matrix-literal.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/generative-apps.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/outbound.md +8 -8
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +2 -2
- package/node_modules/@llblab/pi-telegram/lib/outbound-buttons.ts +17 -15
- package/node_modules/@llblab/pi-telegram/lib/outbound-markup.ts +28 -68
- package/node_modules/@llblab/pi-telegram/lib/outbound.ts +3 -3
- package/node_modules/@llblab/pi-telegram/lib/prompts.ts +1 -1
- package/node_modules/@llblab/pi-telegram/lib/runtime.ts +2 -1
- package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +25 -12
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/SKILL.md +5 -5
- package/package.json +3 -3
|
@@ -2,11 +2,15 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
3
3
|
import { projectArtifactForModel, type ArtifactInvalidationNotice, type ArtifactModelHints } from "./artifact.ts";
|
|
4
4
|
import type { RecentTransitionWindow } from "./history.ts";
|
|
5
|
-
import { applyPatch, isObject, presentationJson, sameJson, type JsonObject, type JsonValue } from "./json.ts";
|
|
5
|
+
import { applyPatch, isObject, presentationJson, pruneEmptyObjects, sameJson, type JsonObject, type JsonValue } from "./json.ts";
|
|
6
6
|
import type { Snapshot } from "./snapshot.ts";
|
|
7
7
|
import { formatOwnedPath, type OwnedPath } from "./ownership.ts";
|
|
8
|
+
import { projectModelReferences } from "./model-interface.ts";
|
|
9
|
+
import { captureScopeSelection, DEFAULT_SCOPE_SELECTION, type ScopeSelection } from "./selection.ts";
|
|
8
10
|
import type { RehydrationPhase } from "./rehydration.ts";
|
|
9
|
-
import { projectSemanticPatch, projectStateForModel, type AtomicScopePatches, type SemanticState, type StateScope } from "./state.ts";
|
|
11
|
+
import { projectSemanticPatch, projectSemanticState, projectStateForModel, type AtomicScopePatches, type SemanticState, type StateScope } from "./state.ts";
|
|
12
|
+
import { lazyValueKind, projectLazyNavigation, projectPlaneContext } from "./planes.ts";
|
|
13
|
+
import { evaluateModuleContext, type EvaluatedModuleContext, type ModuleFrame, type SemanticModule } from "./semantic-module.ts";
|
|
10
14
|
|
|
11
15
|
/** Refresh only our section; Pi owns system frames, tools and forced-prompt precedence. */
|
|
12
16
|
export function projectSystemProtocol(messages: AgentMessage[], protocol: string | undefined): AgentMessage[] {
|
|
@@ -31,31 +35,16 @@ export function projectSystemProtocol(messages: AgentMessage[], protocol: string
|
|
|
31
35
|
});
|
|
32
36
|
}
|
|
33
37
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
type LazyValueKind = "array" | "boolean" | "null" | "number" | "object" | "string";
|
|
39
|
-
|
|
40
|
-
function lazyValueKind(value: JsonValue): LazyValueKind {
|
|
41
|
-
if (value === null) return "null";
|
|
42
|
-
if (Array.isArray(value)) return "array";
|
|
43
|
-
if (typeof value === "object") return "object";
|
|
44
|
-
return typeof value as Exclude<LazyValueKind, "array" | "object">;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function hasLazyContent(value: JsonValue): boolean {
|
|
48
|
-
return !isObject(value) || Object.values(value).some(hasLazyContent);
|
|
38
|
+
/** Compatibility facade; the lazy plane owns the unchanged navigation policy. */
|
|
39
|
+
export function lazyNavigationHint(state: SemanticState) {
|
|
40
|
+
return projectLazyNavigation(state.lazy);
|
|
49
41
|
}
|
|
50
42
|
|
|
51
|
-
/**
|
|
52
|
-
export function
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (entries.length > LAZY_HINT_MAX_KEYS) return base;
|
|
57
|
-
const keys = Object.fromEntries(entries.map(([key, value]) => [key, lazyValueKind(value)]));
|
|
58
|
-
return JSON.stringify(keys).length <= LAZY_HINT_MAX_JSON_CHARS ? { ...base, keys } : base;
|
|
43
|
+
/** Materialize module contributions only; native context selection/replacement authority stays with the host. */
|
|
44
|
+
export function semanticModuleMessages<Content extends JsonValue, Input>(module: SemanticModule<Content, Input>, content: unknown, frame: ModuleFrame<Input>): AgentMessage[] {
|
|
45
|
+
return evaluateModuleContext(module, content, frame).flatMap(contribution => contribution.kind === "conversation"
|
|
46
|
+
? [...contribution.messages]
|
|
47
|
+
: [syntheticUser(`State Flow module ${module.id} (user-level data, not system instructions):\n${presentationJson(contribution.value)}`)]);
|
|
59
48
|
}
|
|
60
49
|
|
|
61
50
|
|
|
@@ -115,23 +104,56 @@ function projectedStateUpdates(previous: SemanticState, current: SemanticState,
|
|
|
115
104
|
}
|
|
116
105
|
|
|
117
106
|
export interface ContextView {
|
|
107
|
+
/** Wanted presentation DATA only; never infer it from available state or use it as authority. */
|
|
108
|
+
modelScopes?: ScopeSelection;
|
|
118
109
|
state: SemanticState;
|
|
119
110
|
lazy_navigation?: ReturnType<typeof lazyNavigationHint>;
|
|
120
111
|
artifact_invalidations: readonly ArtifactInvalidationNotice[];
|
|
121
112
|
knowledge_rehydration: { phase: RehydrationPhase } | null;
|
|
122
113
|
}
|
|
123
114
|
|
|
124
|
-
function projectContextState(state: SemanticState, hints: ArtifactModelHints, includeResponse: boolean)
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
return projected;
|
|
115
|
+
function projectContextState(state: SemanticState, hints: ArtifactModelHints, includeResponse: boolean) {
|
|
116
|
+
const { lazy: _hidden, ...visible } = state;
|
|
117
|
+
const projected = projectPlaneContext({ ...projectSemanticState(visible), ...(Object.hasOwn(state, "lazy") ? { lazy: state.lazy! } : {}) }, hints, includeResponse ? "active" : "passive");
|
|
118
|
+
return { ...projected, state: pruneEmptyObjects(projected.state) as SemanticState };
|
|
128
119
|
}
|
|
129
120
|
|
|
130
|
-
export function contextView(state: SemanticState, hints: ArtifactModelHints, invalidations: readonly ArtifactInvalidationNotice[], phase?: RehydrationPhase, includeResponse = true): ContextView {
|
|
131
|
-
|
|
121
|
+
export function contextView(state: SemanticState, hints: ArtifactModelHints, invalidations: readonly ArtifactInvalidationNotice[], phase?: RehydrationPhase, includeResponse = true, modelScopes?: ScopeSelection): ContextView {
|
|
122
|
+
const selected = modelScopes === undefined ? undefined : captureScopeSelection(modelScopes);
|
|
123
|
+
return { ...(selected === undefined ? {} : { modelScopes: selected }), ...projectContextState(state, hints, includeResponse),
|
|
132
124
|
artifact_invalidations: structuredClone(invalidations), knowledge_rehydration: phase === undefined ? null : { phase } };
|
|
133
125
|
}
|
|
134
126
|
|
|
127
|
+
function viewScopes(view?: ContextView): ScopeSelection {
|
|
128
|
+
return view && Object.hasOwn(view, "modelScopes") ? captureScopeSelection(view.modelScopes) : DEFAULT_SCOPE_SELECTION;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function modelState(state: SemanticState, scopes: ScopeSelection): SemanticState {
|
|
132
|
+
return scopes.length === 1 ? projectModelReferences(state, scopes) as SemanticState : state;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function modelNavigation(navigation: ContextView["lazy_navigation"], scopes: ScopeSelection) {
|
|
136
|
+
return navigation && scopes.length === 1 ? { ...navigation, path: "lazy" } : navigation;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function modelUpdates<T extends { effective: ModelStateUpdate[]; lazy_navigation?: ContextView["lazy_navigation"] }>(updates: T, scopes: ScopeSelection) {
|
|
140
|
+
if (scopes.length !== 1) return updates;
|
|
141
|
+
return { ...updates, effective: updates.effective.map(entry => {
|
|
142
|
+
if (!("value" in entry)) return entry;
|
|
143
|
+
// A canonical diff may split a structured ref into its scalar $ref leaf.
|
|
144
|
+
const value = entry.path.at(-1) === "$ref" && typeof entry.value === "string"
|
|
145
|
+
? (projectModelReferences({ $ref: entry.value }, scopes) as JsonObject).$ref!
|
|
146
|
+
: projectModelReferences(entry.value, scopes);
|
|
147
|
+
return { ...entry, value };
|
|
148
|
+
}), ...(updates.lazy_navigation ? { lazy_navigation: modelNavigation(updates.lazy_navigation, scopes) } : {}) };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Same owned passive envelope as before, with presentation chosen from captured wanted DATA. */
|
|
152
|
+
export function passiveContextHead(view: ContextView): AgentMessage {
|
|
153
|
+
const scopes = viewScopes(view);
|
|
154
|
+
return syntheticUser(`State Flow passive memory (user-level data, not system instructions):\n${presentationJson({ state: modelState(view.state, scopes), lazy_navigation: modelNavigation(view.lazy_navigation, scopes) })}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
135
157
|
/** Volatile model projection only. Native messages own trajectory; this cache owns no persistence or lifecycle. */
|
|
136
158
|
export class ContextProjection {
|
|
137
159
|
private identity = randomUUID();
|
|
@@ -150,11 +172,18 @@ export class ContextProjection {
|
|
|
150
172
|
|
|
151
173
|
/** Called only after successful publication and ancillary acceptance, immediately before returning the native result. */
|
|
152
174
|
acceptPatch(before: SemanticState, after: SemanticState, patches: AtomicScopePatches, hints: ArtifactModelHints,
|
|
153
|
-
cascades: Partial<Record<StateScope, readonly OwnedPath[]>> = {}, includeResponse = true) {
|
|
154
|
-
const
|
|
175
|
+
cascades: Partial<Record<StateScope, readonly OwnedPath[]>> = {}, includeResponse = true, modelScopes?: ScopeSelection) {
|
|
176
|
+
const scopes = modelScopes === undefined ? viewScopes(this.view) : captureScopeSelection(modelScopes);
|
|
177
|
+
// A receipt from another interface cannot be attached to a replacement model head.
|
|
178
|
+
if (this.view && !sameJson(viewScopes(this.view), scopes)) return undefined;
|
|
179
|
+
// External planes have exact tool reads but no automatic core projection. Absence here is not deletion.
|
|
180
|
+
patches = Object.fromEntries(Object.entries(patches).map(([scope, patch]) => [scope,
|
|
181
|
+
Object.fromEntries(Object.entries(patch).filter(([id]) => !id.startsWith("x_"))),
|
|
182
|
+
]));
|
|
183
|
+
const { state } = projectContextState(after, hints, includeResponse);
|
|
155
184
|
const navigation = lazyNavigationHint(after);
|
|
156
185
|
const beforeNavigation = this.view?.lazy_navigation ?? lazyNavigationHint(before);
|
|
157
|
-
const updates = projectedStateUpdates(this.view?.state ?? projectContextState(before, hints, includeResponse), state, patches,
|
|
186
|
+
const updates = projectedStateUpdates(this.view?.state ?? projectContextState(before, hints, includeResponse).state, state, patches,
|
|
158
187
|
beforeNavigation, navigation);
|
|
159
188
|
// Suppress direct writes only when the accepted effective value matches.
|
|
160
189
|
// Overlap stays conservative except for explicit top-scope replacements:
|
|
@@ -243,13 +272,15 @@ export class ContextProjection {
|
|
|
243
272
|
if (this.view) this.view = { ...this.view, state, lazy_navigation: navigation };
|
|
244
273
|
// Owner paths remain informative even when effective state/navigation is masked
|
|
245
274
|
// or the model could predict every direct write. Never expose target values.
|
|
246
|
-
const cascaded = (
|
|
247
|
-
|
|
275
|
+
const cascaded = scopes.flatMap(scope => (cascades[scope] ?? []).map(path =>
|
|
276
|
+
scopes.length === 1 ? [path.plane, ...path.keys].join(".") : formatOwnedPath(scope, path)));
|
|
248
277
|
return updates.effective.length || updates.lazy_navigation || cascaded.length
|
|
249
|
-
? { projection: this.identity, ...updates, ...(cascaded.length ? { cascaded } : {}) } : undefined;
|
|
278
|
+
? { projection: this.identity, ...modelUpdates(updates, scopes), ...(cascaded.length ? { cascaded } : {}) } : undefined;
|
|
250
279
|
}
|
|
251
280
|
|
|
252
281
|
project(messages: AgentMessage[], current: ContextView, makeHead: () => AgentMessage, initial?: ContextView): AgentMessage[] {
|
|
282
|
+
const scopes = viewScopes(current);
|
|
283
|
+
if (this.view && !sameJson(viewScopes(this.view), scopes)) this.reset();
|
|
253
284
|
const identities = messages.map((message) => JSON.stringify([message.role, message.timestamp,
|
|
254
285
|
"toolCallId" in message ? message.toolCallId : null]));
|
|
255
286
|
// Native compaction/selection normally resets explicitly; a removed/replaced prefix is also a safe cache boundary.
|
|
@@ -263,7 +294,7 @@ export class ContextProjection {
|
|
|
263
294
|
const previous = this.view!;
|
|
264
295
|
const updates = projectedStateUpdates(previous.state, current.state, {}, previous.lazy_navigation, current.lazy_navigation);
|
|
265
296
|
const notice = {
|
|
266
|
-
...(updates.effective.length || updates.lazy_navigation ? { state_updates: { projection: this.identity, ...updates } } : {}),
|
|
297
|
+
...(updates.effective.length || updates.lazy_navigation ? { state_updates: { projection: this.identity, ...modelUpdates(updates, scopes) } } : {}),
|
|
267
298
|
...(!sameJson(previous.artifact_invalidations, current.artifact_invalidations) ? { artifact_invalidations: current.artifact_invalidations } : {}),
|
|
268
299
|
...(!sameJson(previous.knowledge_rehydration, current.knowledge_rehydration) ? { knowledge_rehydration: current.knowledge_rehydration } : {}),
|
|
269
300
|
};
|
|
@@ -308,7 +339,64 @@ function messageText(message: AgentMessage): string {
|
|
|
308
339
|
return contentText((message as { content?: unknown }).content);
|
|
309
340
|
}
|
|
310
341
|
|
|
311
|
-
|
|
342
|
+
/** A unique native user anchor admits completed conversation, never the current run or protocol frames. */
|
|
343
|
+
export function moduleConversationSource(messages: readonly AgentMessage[], anchorTimestamp: number | undefined): { conversation: readonly AgentMessage[]; prompt?: string } {
|
|
344
|
+
if (anchorTimestamp === undefined || !Number.isFinite(anchorTimestamp)) return { conversation: [] };
|
|
345
|
+
const matches = messages.flatMap((message, index) => message.role === "user" && message.timestamp === anchorTimestamp ? [index] : []);
|
|
346
|
+
if (matches.length !== 1) return { conversation: [] };
|
|
347
|
+
const start = matches[0]!;
|
|
348
|
+
return { conversation: messages.slice(0, start).filter(message => ["user", "assistant", "toolResult"].includes(message.role)), prompt: messageText(messages[start]!) };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function assertModuleToolUnits(messages: readonly AgentMessage[]): void {
|
|
352
|
+
const pending = new Map<string, string>(), seen = new Set<string>();
|
|
353
|
+
for (const message of messages) {
|
|
354
|
+
if (message.role === "toolResult") {
|
|
355
|
+
if (!pending.has(message.toolCallId) || pending.get(message.toolCallId) !== message.toolName) throw new Error("Module conversation selection has an orphan tool result");
|
|
356
|
+
pending.delete(message.toolCallId);
|
|
357
|
+
} else {
|
|
358
|
+
if (pending.size) throw new Error("Module conversation selection omits a tool result");
|
|
359
|
+
if (message.role !== "assistant") continue;
|
|
360
|
+
for (const part of message.content) if (part.type === "toolCall") {
|
|
361
|
+
if (!part.id || !part.name || seen.has(part.id)) throw new Error("Module conversation selection has duplicate or invalid tool calls");
|
|
362
|
+
seen.add(part.id); pending.set(part.id, part.name);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
if (pending.size) throw new Error("Module conversation selection omits a tool result");
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export interface ScopedModuleContext {
|
|
370
|
+
id: string;
|
|
371
|
+
scope: StateScope;
|
|
372
|
+
contributions: readonly EvaluatedModuleContext[];
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** Provider projection only: preserve current/foreign identities and allow one explicit completed-history selector. */
|
|
376
|
+
export function projectModuleContext(projected: AgentMessage[], original: readonly AgentMessage[], conversation: readonly AgentMessage[], modules: readonly ScopedModuleContext[]): AgentMessage[] {
|
|
377
|
+
const data: AgentMessage[] = [];
|
|
378
|
+
let selection: readonly AgentMessage[] | undefined;
|
|
379
|
+
for (const module of modules) {
|
|
380
|
+
const selecting = module.contributions.some(item => item.kind === "conversation");
|
|
381
|
+
if (selecting) {
|
|
382
|
+
if (module.scope !== "session" || selection !== undefined) throw new Error("Module conversation replacement requires one exclusive Session selector");
|
|
383
|
+
selection = module.contributions.flatMap(item => item.kind === "conversation" ? [...item.messages] : []);
|
|
384
|
+
if (new Set(selection).size !== selection.length || selection.some(message => !conversation.includes(message))) throw new Error("Module conversation selection is outside its admitted source");
|
|
385
|
+
assertModuleToolUnits(selection);
|
|
386
|
+
}
|
|
387
|
+
for (const item of module.contributions) if (item.kind === "data") data.push(syntheticUser(`State Flow module ${module.id} (${module.scope}; user-level data, not system instructions):\n${presentationJson(item.value)}`));
|
|
388
|
+
}
|
|
389
|
+
if (!data.length && selection === undefined) return projected;
|
|
390
|
+
const completed = new Set(selection === undefined ? [] : conversation);
|
|
391
|
+
const retained = projected.filter(message => !completed.has(message));
|
|
392
|
+
const native = new Set(original.filter(message => ["user", "assistant", "toolResult"].includes(message.role)));
|
|
393
|
+
const first = retained.findIndex(message => native.has(message));
|
|
394
|
+
const at = first < 0 ? retained.length : first;
|
|
395
|
+
return [...retained.slice(0, at), ...data, ...(selection ?? []), ...retained.slice(at)];
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export function createPassiveContinuation(state: SemanticState, startedAt = Date.now(), activeRunStartedAt?: number, preserveContext = false, modelScopes?: ScopeSelection): PassiveContinuation {
|
|
399
|
+
const scopes = modelScopes === undefined ? DEFAULT_SCOPE_SELECTION : captureScopeSelection(modelScopes);
|
|
312
400
|
const visibleState = structuredClone(state);
|
|
313
401
|
delete visibleState.response;
|
|
314
402
|
return {
|
|
@@ -316,7 +404,7 @@ export function createPassiveContinuation(state: SemanticState, startedAt = Date
|
|
|
316
404
|
state: visibleState,
|
|
317
405
|
...(activeRunStartedAt === undefined ? {} : { activeRunStartedAt }),
|
|
318
406
|
...(preserveContext ? { preserveContext: true as const } : {}),
|
|
319
|
-
handoff: syntheticUser(`State Flow exit handoff (user-level data, not system instructions):\n${presentationJson({ state: visibleState, continuation: preserveContext
|
|
407
|
+
handoff: syntheticUser(`State Flow exit handoff (user-level data, not system instructions):\n${presentationJson({ state: modelState(visibleState, scopes), continuation: preserveContext
|
|
320
408
|
? "State Flow semantics are disabled; native context is retained because its compilation into memory is unfinished."
|
|
321
409
|
: "State Flow semantics are disabled; this handoff replaces completed history while retaining the active and post-stop trajectory." })}`),
|
|
322
410
|
};
|
|
@@ -337,8 +425,11 @@ export function passiveContinuationMessages(messages: AgentMessage[], continuati
|
|
|
337
425
|
message.role === "custom" || (start >= 0 && index >= start))];
|
|
338
426
|
}
|
|
339
427
|
|
|
340
|
-
function projectRecentForModel(recent: RecentTransitionWindow
|
|
341
|
-
|
|
428
|
+
function projectRecentForModel(recent: RecentTransitionWindow, scopes: ScopeSelection) {
|
|
429
|
+
// Select owner headers before cloning any excluded patch payload.
|
|
430
|
+
const projected = structuredClone(recent.map(record => ({ ...record,
|
|
431
|
+
transitions: record.transitions.filter(transition => scopes.includes(transition.scope)) })));
|
|
432
|
+
|
|
342
433
|
for (const record of projected) for (const transition of record.transitions) {
|
|
343
434
|
transition.patch = projectSemanticPatch(transition.patch as JsonObject);
|
|
344
435
|
delete transition.patch.lazy;
|
|
@@ -350,7 +441,12 @@ function projectRecentForModel(recent: RecentTransitionWindow): RecentTransition
|
|
|
350
441
|
}
|
|
351
442
|
}
|
|
352
443
|
for (const record of projected) record.transitions = record.transitions.filter(({ patch }) => Object.keys(patch).length > 0);
|
|
353
|
-
|
|
444
|
+
const visible = projected.filter(({ transitions }) => transitions.length > 0);
|
|
445
|
+
if (scopes.length !== 1) return visible;
|
|
446
|
+
return visible.map(record => {
|
|
447
|
+
if (record.transitions.length !== 1) throw new Error("Flat State Flow recent history requires one patch per captured owner");
|
|
448
|
+
return { id: record.id, at: record.at, patch: projectModelReferences(record.transitions[0]!.patch, scopes) };
|
|
449
|
+
});
|
|
354
450
|
}
|
|
355
451
|
|
|
356
452
|
export function runtimeContextMessage(
|
|
@@ -366,11 +462,11 @@ export function runtimeContextMessage(
|
|
|
366
462
|
|
|
367
463
|
/** Render a view already projected by this domain without cloning the full semantic overlay twice. */
|
|
368
464
|
export function runtimeContextHead(snapshot: Snapshot, view: ContextView, recentTransitions: RecentTransitionWindow = []): AgentMessage {
|
|
369
|
-
const recent = projectRecentForModel(recentTransitions);
|
|
465
|
+
const scopes = viewScopes(view), recent = projectRecentForModel(recentTransitions, scopes);
|
|
370
466
|
const context = {
|
|
371
467
|
...(snapshot.meta.specification === undefined ? {} : { specification: snapshot.meta.specification }),
|
|
372
|
-
state: view.state,
|
|
373
|
-
...(view.lazy_navigation === undefined ? {} : { lazy_navigation: view.lazy_navigation }),
|
|
468
|
+
state: modelState(view.state, scopes),
|
|
469
|
+
...(view.lazy_navigation === undefined ? {} : { lazy_navigation: modelNavigation(view.lazy_navigation, scopes) }),
|
|
374
470
|
...(view.knowledge_rehydration === null ? {} : { knowledge_rehydration: view.knowledge_rehydration }),
|
|
375
471
|
...(view.artifact_invalidations.length === 0 ? {} : { artifact_invalidations: view.artifact_invalidations.map(({ path, scope, reason }) => ({ path, ...(scope === undefined ? {} : { scope }), reason })) }),
|
|
376
472
|
...(recent.length === 0 ? {} : { recent_transitions: recent }),
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { closeSync, constants, fstatSync, lstatSync, openSync, readSync, readdirSync, realpathSync, statSync } from "node:fs";
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { captureOwnedFileBases, resolveSessionAddress, sessionRuntimePaths } from "./durable.ts";
|
|
4
4
|
import { MAX_HISTORY_LIMIT } from "./history.ts";
|
|
5
5
|
import { diagnosticText } from "./protocol.ts";
|
|
6
|
-
import {
|
|
6
|
+
import { DEFAULT_SCOPE_SELECTION } from "./selection.ts";
|
|
7
|
+
import { prepareSessionPolicyMigration } from "./snapshot.ts";
|
|
7
8
|
import { withStorageTransaction } from "./storage.ts";
|
|
8
|
-
import { validateScopeLineage } from "./temporal.ts";
|
|
9
9
|
|
|
10
10
|
export type ContinuationTransport = "local" | "sdk" | "telegram" | string;
|
|
11
11
|
|
|
@@ -229,20 +229,17 @@ export async function inspectStateFlowContinuationProvenance(
|
|
|
229
229
|
try {
|
|
230
230
|
if (!lstatSync(root, { throwIfNoEntry: false })) return absent;
|
|
231
231
|
const result = await withStorageTransaction(root, (tx) => {
|
|
232
|
-
const
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
243
|
-
if (streams.some((stream) => stream === undefined)) throw new Error("incomplete canonical temporal cohort");
|
|
244
|
-
// Shared streams remain current and independently valid; only the private stream binds to this lineage.
|
|
245
|
-
validateScopeLineage(streams[2]!, "session", runtime.meta.lineage, MAX_HISTORY_LIMIT);
|
|
232
|
+
const lifecycleFiles = captureOwnedFileBases([paths.config, paths.runtime], root);
|
|
233
|
+
const files = new Map(lifecycleFiles.map(file => [file.path, file.content]));
|
|
234
|
+
const policy = prepareSessionPolicyMigration(files.get(paths.config), files.get(paths.runtime), selected.cwd, selected.id, DEFAULT_SCOPE_SELECTION);
|
|
235
|
+
if (!policy) {
|
|
236
|
+
// Confirm absence/private presence without reading any shared or private semantic body.
|
|
237
|
+
if (tx.hasSelectedCurrentAuthority(selected.cwd, selected.id, DEFAULT_SCOPE_SELECTION, root, sessionKey)) throw new Error("State Flow lifecycle changed during inspection");
|
|
238
|
+
return absent;
|
|
239
|
+
}
|
|
240
|
+
// Reuse current admission; bind the decoded selection to exact lifecycle bytes before any wider interpretation.
|
|
241
|
+
const current = tx.prepareSelectedCurrent(selected.cwd, selected.id, policy.runtime.config.scopes, MAX_HISTORY_LIMIT, root, sessionKey, { files: lifecycleFiles });
|
|
242
|
+
if (current.lifecycle.runtime.config.mode !== "active") return { stateFlow: { enabled: false, restorable: true }, reason: "State Flow is not active on selected runtime" };
|
|
246
243
|
return { stateFlow: { enabled: true, restorable: true }, reason: "canonical session lineage is valid beside current shared streams" };
|
|
247
244
|
}, signal);
|
|
248
245
|
signal?.throwIfAborted();
|
|
@@ -19,8 +19,10 @@ import {
|
|
|
19
19
|
type ArtifactProvenanceRegistry,
|
|
20
20
|
} from "./artifact.ts";
|
|
21
21
|
import { MAX_HISTORY_LIMIT } from "./history.ts";
|
|
22
|
+
import { prepareSerializedScopeMigration } from "./migration.ts";
|
|
22
23
|
import { canonicalJson, isJsonValue, isObject, type JsonObject } from "./json.ts";
|
|
23
24
|
import { validateScopeStream, validateTemporalState, type ScopeStream, type TemporalState } from "./temporal.ts";
|
|
25
|
+
import { assertRetainedModules, validateRetainedModules } from "./semantic-module.ts";
|
|
24
26
|
import { selectSemanticFields, type StateScope } from "./state.ts";
|
|
25
27
|
|
|
26
28
|
const SESSION_KEY_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
|
|
@@ -66,8 +68,8 @@ export function serializeScopeStream(stream: ScopeStream, scope: StateScope, cwd
|
|
|
66
68
|
if (scope === "cwd" && cwdIdentity === undefined) throw new Error("State Flow CWD scope serialization requires its canonical identity");
|
|
67
69
|
if (scope !== "cwd" && cwdIdentity !== undefined) throw new Error("Only State Flow CWD scope serialization accepts a CWD identity");
|
|
68
70
|
return {
|
|
69
|
-
checkpoint: `${canonicalJson(selectSemanticFields(stream.checkpoint.state))}\n`,
|
|
70
|
-
patches: stream.patches.map((record) => `${canonicalJson(selectSemanticFields(record.patch as JsonObject))}\n`).join(""),
|
|
71
|
+
checkpoint: `${canonicalJson(selectSemanticFields(stream.checkpoint.state, stream.modules))}\n`,
|
|
72
|
+
patches: stream.patches.map((record) => `${canonicalJson(selectSemanticFields(record.patch as JsonObject, stream.modules))}\n`).join(""),
|
|
71
73
|
temporal: {
|
|
72
74
|
revision: stream.revision,
|
|
73
75
|
checkpoint: structuredClone(stream.checkpoint.through),
|
|
@@ -80,6 +82,10 @@ export type ScopeStreamPresence =
|
|
|
80
82
|
| { kind: "absent" }
|
|
81
83
|
| { kind: "present"; stream: ScopeStream };
|
|
82
84
|
|
|
85
|
+
function isSupportedScopeMetadataVersion(version: unknown): boolean {
|
|
86
|
+
return version === 1 || version === 2 || version === 3 || version === 4;
|
|
87
|
+
}
|
|
88
|
+
|
|
83
89
|
/** Distinguish a wholly absent semantic cohort from partial or malformed surviving authority. */
|
|
84
90
|
export function classifyScopeStream(
|
|
85
91
|
checkpointSource: string | undefined,
|
|
@@ -93,46 +99,29 @@ export function classifyScopeStream(
|
|
|
93
99
|
if (checkpointSource === undefined || patchesSource === undefined) {
|
|
94
100
|
throw new Error(`State Flow ${scope} scope has an incomplete checkpoint/tail pair`);
|
|
95
101
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
checkpoint
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (metaSource !== undefined) {
|
|
104
|
-
try { meta = JSON.parse(metaSource) as Record<string, unknown>; }
|
|
105
|
-
catch { throw new Error(`State Flow ${scope} metadata contains invalid JSON`); }
|
|
106
|
-
if (!isObject(meta)) throw new Error(`Invalid State Flow ${scope} metadata`);
|
|
107
|
-
}
|
|
108
|
-
const patches: unknown[] = [];
|
|
109
|
-
for (const [index, line] of patchesSource.split(/\r?\n/).entries()) {
|
|
110
|
-
if (line.trim().length === 0) continue;
|
|
111
|
-
try {
|
|
112
|
-
patches.push(JSON.parse(line));
|
|
113
|
-
} catch {
|
|
114
|
-
throw new Error(`State Flow ${scope} tail contains invalid JSON at line ${index + 1}`);
|
|
102
|
+
if (metaSource === undefined) {
|
|
103
|
+
try { JSON.parse(checkpointSource); }
|
|
104
|
+
catch { throw new Error(`State Flow ${scope} checkpoint contains invalid JSON`); }
|
|
105
|
+
for (const [index, line] of patchesSource.split(/\r?\n/).entries()) {
|
|
106
|
+
if (line.trim().length === 0) continue;
|
|
107
|
+
try { JSON.parse(line); }
|
|
108
|
+
catch { throw new Error(`State Flow ${scope} tail contains invalid JSON at line ${index + 1}`); }
|
|
115
109
|
}
|
|
116
|
-
}
|
|
117
|
-
const temporal = meta?.temporal;
|
|
118
|
-
if (!isObject(temporal) || !Object.hasOwn(temporal, "checkpoint") || !Array.isArray(temporal.patches)) {
|
|
119
110
|
throw new Error(`Unsupported State Flow ${scope} storage format`);
|
|
120
111
|
}
|
|
121
|
-
const
|
|
122
|
-
if (
|
|
123
|
-
|
|
124
|
-
|
|
112
|
+
const metadata = parseMetadataDocument(metaSource, `State Flow ${scope} metadata`);
|
|
113
|
+
if (!isSupportedScopeMetadataVersion(metadata.version)) throw new Error(`Unsupported State Flow ${scope} metadata version`);
|
|
114
|
+
// In Unicode mode this range matches lone UTF-16 surrogates, not valid pairs or JSON escapes.
|
|
115
|
+
if ([checkpointSource, patchesSource, metaSource].some((source) => /[\uD800-\uDFFF]/u.test(source))) {
|
|
116
|
+
throw new Error(`State Flow ${scope} source is not lossless UTF-8 text`);
|
|
125
117
|
}
|
|
126
|
-
const
|
|
127
|
-
|
|
118
|
+
const decoded = prepareSerializedScopeMigration({
|
|
119
|
+
checkpoint: Buffer.from(checkpointSource), patches: Buffer.from(patchesSource), meta: Buffer.from(metaSource),
|
|
120
|
+
}, scope, scope === "cwd" ? expectedCwd : undefined);
|
|
128
121
|
const stream = {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
checkpoint: { through: temporal.checkpoint, state: isObject(checkpoint) ? selectSemanticFields(checkpoint) : checkpoint },
|
|
133
|
-
patches: patches.map((patch, index) => ({
|
|
134
|
-
transition: boundaries[index], patch: isObject(patch) ? selectSemanticFields(patch) : patch,
|
|
135
|
-
})),
|
|
122
|
+
...decoded.stream,
|
|
123
|
+
checkpoint: { ...decoded.stream.checkpoint, state: selectSemanticFields(decoded.stream.checkpoint.state, decoded.stream.modules) },
|
|
124
|
+
patches: decoded.stream.patches.map((record) => ({ ...record, patch: selectSemanticFields(record.patch as JsonObject, decoded.stream.modules) })),
|
|
136
125
|
};
|
|
137
126
|
validateScopeStream(stream, scope, MAX_HISTORY_LIMIT);
|
|
138
127
|
return { kind: "present", stream };
|
|
@@ -220,15 +209,17 @@ export function serializeScopeMetadata(
|
|
|
220
209
|
cwdIdentity?: string, existingSource?: string,
|
|
221
210
|
): string {
|
|
222
211
|
const existing = parseMetadataDocument(existingSource, `State Flow metadata`);
|
|
223
|
-
if (
|
|
224
|
-
|
|
212
|
+
if (Object.keys(existing).length > 0 && !isSupportedScopeMetadataVersion(existing.version)) {
|
|
213
|
+
throw new Error("Unsupported State Flow metadata version");
|
|
225
214
|
}
|
|
215
|
+
if (existing.version === 4) assertRetainedModules(existing.modules, stream.modules);
|
|
226
216
|
const sources = serializeScopeStream(stream, scope, cwdIdentity);
|
|
227
217
|
const value = {
|
|
228
218
|
...existing,
|
|
229
|
-
version:
|
|
219
|
+
version: stream.modules === undefined ? 3 : 4,
|
|
220
|
+
...(stream.modules === undefined ? {} : { modules: structuredClone(stream.modules) }),
|
|
230
221
|
...(registry === undefined ? {} : { artifacts: serializeArtifactProvenanceRegistry(registry) }),
|
|
231
|
-
temporal: sources.temporal,
|
|
222
|
+
temporal: { ...(isObject(existing.temporal) ? existing.temporal : {}), ...sources.temporal },
|
|
232
223
|
...(scope === "cwd" ? { owner: { cwd: resolve(cwdIdentity!) } } : {}),
|
|
233
224
|
};
|
|
234
225
|
return `${canonicalJson(value)}\n`;
|
|
@@ -236,7 +227,7 @@ export function serializeScopeMetadata(
|
|
|
236
227
|
|
|
237
228
|
/** Compatibility serializer retained for metadata-only callers. */
|
|
238
229
|
export function serializeScopeProvenance(registry: Readonly<ArtifactProvenanceRegistry>): string {
|
|
239
|
-
return `${canonicalJson({ version:
|
|
230
|
+
return `${canonicalJson({ version: 3, artifacts: serializeArtifactProvenanceRegistry(registry) })}\n`;
|
|
240
231
|
}
|
|
241
232
|
|
|
242
233
|
function parseMetadataDocument(source: string | undefined, label: string): Record<string, unknown> {
|
|
@@ -252,7 +243,8 @@ function parseMetadataDocument(source: string | undefined, label: string): Recor
|
|
|
252
243
|
export function parseScopeProvenance(source: string | undefined, path: string): ArtifactProvenanceRegistry {
|
|
253
244
|
const value = parseMetadataDocument(source, `State Flow provenance file: ${JSON.stringify(path)}`);
|
|
254
245
|
if (Object.keys(value).length === 0) return {};
|
|
255
|
-
if (value.version
|
|
246
|
+
if (!isSupportedScopeMetadataVersion(value.version)) throw new Error(`Invalid State Flow provenance document: ${JSON.stringify(path)}`);
|
|
247
|
+
if (value.version === 4) validateRetainedModules(value.modules);
|
|
256
248
|
if (!Object.hasOwn(value, "artifacts")) return {};
|
|
257
249
|
return parseArtifactProvenanceRegistry(value.artifacts, `State Flow provenance at ${JSON.stringify(path)}`);
|
|
258
250
|
}
|
|
@@ -403,7 +395,8 @@ function readRegularBytes(path: string, repositoryRoot: string): Buffer | undefi
|
|
|
403
395
|
}
|
|
404
396
|
|
|
405
397
|
function readRegularFile(path: string, repositoryRoot: string): string | undefined {
|
|
406
|
-
|
|
398
|
+
const bytes = readRegularBytes(path, repositoryRoot);
|
|
399
|
+
return bytes === undefined ? undefined : new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
407
400
|
}
|
|
408
401
|
|
|
409
402
|
function byteIdentity(bytes: Uint8Array | undefined): DurableFileBase["identity"] {
|
|
@@ -422,6 +415,22 @@ function assertCurrentBytes(path: string, root: string, expected: Uint8Array | u
|
|
|
422
415
|
}
|
|
423
416
|
}
|
|
424
417
|
|
|
418
|
+
export interface OwnedFilePresence { path: string; present: boolean }
|
|
419
|
+
|
|
420
|
+
/** Presence-only private-origin/unsupported-layout evidence; never open or decode semantic bodies. */
|
|
421
|
+
export function captureOwnedFilePresence(paths: readonly string[], repositoryRoot: string): OwnedFilePresence[] {
|
|
422
|
+
const root = resolve(repositoryRoot);
|
|
423
|
+
return paths.map((requested) => {
|
|
424
|
+
const path = resolve(requested);
|
|
425
|
+
const retiredLayout = basename(path) === STATE_FILE && isStateFlowOwnedPath(join(dirname(path), CHECKPOINT_FILE), root);
|
|
426
|
+
if (!isStateFlowOwnedPath(path, root) && !retiredLayout) throw new Error(`Cannot inspect a non-State Flow path: ${JSON.stringify(path)}`);
|
|
427
|
+
if (!assertDirectoryChain(root, dirname(path), false)) return { path, present: false };
|
|
428
|
+
const stat = lstatSync(path, { throwIfNoEntry: false });
|
|
429
|
+
if (stat && (!stat.isFile() || stat.isSymbolicLink())) throw new Error(`Durable State Flow path is not a regular file: ${JSON.stringify(path)}`);
|
|
430
|
+
return { path, present: stat !== undefined };
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
425
434
|
/** Capture exact owned bytes for one compare-and-swap publication cohort. */
|
|
426
435
|
export function captureOwnedFileBases(paths: readonly string[], repositoryRoot: string): DurableFileBase[] {
|
|
427
436
|
const root = resolve(repositoryRoot);
|