@llblab/pi-kit 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/README.md +1 -1
- package/node_modules/@llblab/pi-state-flow/AGENTS.md +7 -7
- package/node_modules/@llblab/pi-state-flow/BACKLOG.md +8 -9
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +26 -0
- package/node_modules/@llblab/pi-state-flow/README.md +1 -3
- package/node_modules/@llblab/pi-state-flow/dist/index.d.ts +21 -0
- package/node_modules/@llblab/pi-state-flow/dist/index.js +20 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.d.ts +39 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.js +78 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.d.ts +110 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.js +334 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.d.ts +49 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.js +67 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/config.d.ts +11 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/config.js +53 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/context.d.ts +23 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/context.js +109 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.d.ts +111 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.js +189 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/discovery.d.ts +21 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/discovery.js +125 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/durable.d.ts +102 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/durable.js +507 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/episode.d.ts +8 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/episode.js +27 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.d.ts +22 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +1263 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/git.d.ts +72 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/git.js +565 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/history.d.ts +22 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/history.js +79 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/json.d.ts +12 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/json.js +109 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/logging.d.ts +25 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/logging.js +24 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/maintenance.d.ts +36 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/maintenance.js +98 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/memory.d.ts +15 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/memory.js +42 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/migration.d.ts +13 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/migration.js +133 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.d.ts +7 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +59 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/publication.d.ts +69 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/publication.js +335 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/query.d.ts +27 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/query.js +35 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.d.ts +8 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.js +27 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/rehydration.d.ts +36 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/rehydration.js +38 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.d.ts +142 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.js +529 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/session.d.ts +21 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/session.js +44 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/skills.d.ts +25 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/skills.js +131 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.d.ts +88 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.js +255 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/state.d.ts +55 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/state.js +31 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/status.d.ts +38 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +79 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/storage.d.ts +46 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/storage.js +217 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.d.ts +98 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +231 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.d.ts +39 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.js +203 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/transition.d.ts +25 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +204 -0
- package/node_modules/@llblab/pi-state-flow/dist/package.json +79 -0
- package/node_modules/@llblab/pi-state-flow/dist/pi-state-flow/index.js +1 -0
- package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-memory/SKILL.md +138 -0
- package/node_modules/@llblab/pi-state-flow/docs/architecture.md +11 -5
- package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +5 -1
- package/node_modules/@llblab/pi-state-flow/docs/filesystem-recovery.md +3 -3
- package/node_modules/@llblab/pi-state-flow/docs/usage.md +6 -4
- package/node_modules/@llblab/pi-state-flow/index.ts +1 -0
- package/node_modules/@llblab/pi-state-flow/lib/compaction.ts +17 -1
- package/node_modules/@llblab/pi-state-flow/lib/config.ts +6 -1
- package/node_modules/@llblab/pi-state-flow/lib/durable.ts +88 -96
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +64 -12
- package/node_modules/@llblab/pi-state-flow/lib/git.ts +32 -188
- package/node_modules/@llblab/pi-state-flow/lib/migration.ts +84 -48
- package/node_modules/@llblab/pi-state-flow/lib/query.ts +40 -0
- package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +7 -30
- package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +48 -50
- package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +41 -97
- package/node_modules/@llblab/pi-state-flow/lib/storage.ts +17 -12
- package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +4 -4
- package/node_modules/@llblab/pi-state-flow/package.json +23 -6
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +3 -1
- package/package.json +4 -4
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ArtifactInvalidationReason } from "./artifact.ts";
|
|
2
|
+
import { type RecentTransitionWindow } from "./history.ts";
|
|
3
|
+
import type { PublicationQueueState } from "./publication.ts";
|
|
4
|
+
import type { Snapshot } from "./snapshot.ts";
|
|
5
|
+
import { type ScopedStates, type StateScope } from "./state.ts";
|
|
6
|
+
import type { TransitionBoundary } from "./temporal.ts";
|
|
7
|
+
export declare const STATUS_KEY = "state-flow";
|
|
8
|
+
export type Colorize = (color: "accent" | "dim", text: string) => string;
|
|
9
|
+
export type StaleArtifactReason = ArtifactInvalidationReason | "source-removed";
|
|
10
|
+
export interface StaleArtifactDiagnostic {
|
|
11
|
+
scope: StateScope;
|
|
12
|
+
path: string;
|
|
13
|
+
reason: StaleArtifactReason;
|
|
14
|
+
}
|
|
15
|
+
export interface PendingPublicationDiagnostic {
|
|
16
|
+
commit: string;
|
|
17
|
+
error: string;
|
|
18
|
+
}
|
|
19
|
+
export interface StatusDiagnostics {
|
|
20
|
+
repositoryRoot: string;
|
|
21
|
+
cwdScopeKey: string;
|
|
22
|
+
sessionScopeKey: string;
|
|
23
|
+
scopeStates: ScopedStates;
|
|
24
|
+
recent: RecentTransitionWindow;
|
|
25
|
+
temporal?: {
|
|
26
|
+
head: TransitionBoundary;
|
|
27
|
+
historyDepth: number;
|
|
28
|
+
tailCounts: Record<StateScope, number>;
|
|
29
|
+
};
|
|
30
|
+
staleArtifacts: readonly StaleArtifactDiagnostic[];
|
|
31
|
+
artifactFreshnessError?: string;
|
|
32
|
+
durableStateError?: string;
|
|
33
|
+
pendingPublication?: PendingPublicationDiagnostic;
|
|
34
|
+
publicationQueue?: PublicationQueueState;
|
|
35
|
+
publicationQueueError?: string;
|
|
36
|
+
}
|
|
37
|
+
export declare function compactStatus(snapshot: Snapshot, colorize: Colorize): string | undefined;
|
|
38
|
+
export declare function detailedStatus(snapshot: Snapshot, diagnostics: StatusDiagnostics): string;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { projectRecentTransitionsWithLimit, RECENT_TRANSITION_LIMIT } from "./history.js";
|
|
2
|
+
import { inspectMemoryPromotions, retainedMemoryScopes } from "./memory.js";
|
|
3
|
+
import { overlayStates } from "./state.js";
|
|
4
|
+
export const STATUS_KEY = "state-flow";
|
|
5
|
+
export function compactStatus(snapshot, colorize) {
|
|
6
|
+
if (!snapshot.config.enabled)
|
|
7
|
+
return undefined;
|
|
8
|
+
return `${colorize("accent", "state-flow")} ${colorize("dim", `#${snapshot.meta.step}`)}`;
|
|
9
|
+
}
|
|
10
|
+
function countArtifacts(states, scope) {
|
|
11
|
+
return Object.keys(states[scope].artifacts).length;
|
|
12
|
+
}
|
|
13
|
+
function abbreviatedCommit(commit) {
|
|
14
|
+
return commit.length > 12 ? commit.slice(0, 12) : commit;
|
|
15
|
+
}
|
|
16
|
+
export function detailedStatus(snapshot, diagnostics) {
|
|
17
|
+
const projectedRecent = projectRecentTransitionsWithLimit(RECENT_TRANSITION_LIMIT, diagnostics.recent);
|
|
18
|
+
const available = diagnostics.temporal !== undefined && diagnostics.durableStateError === undefined;
|
|
19
|
+
const materialized = !available ? undefined : overlayStates(diagnostics.scopeStates.global, diagnostics.scopeStates.cwd, diagnostics.scopeStates.session);
|
|
20
|
+
const stateJson = materialized === undefined ? undefined : JSON.stringify(materialized, null, 2);
|
|
21
|
+
const freshnessError = diagnostics.artifactFreshnessError ?? (available ? undefined : "temporal global artifact registry is unavailable");
|
|
22
|
+
const stale = freshnessError === undefined
|
|
23
|
+
? String(diagnostics.staleArtifacts.length)
|
|
24
|
+
: "unknown";
|
|
25
|
+
const publication = diagnostics.pendingPublication === undefined
|
|
26
|
+
? "idle"
|
|
27
|
+
: `pending ${abbreviatedCommit(diagnostics.pendingPublication.commit)} — ${diagnostics.pendingPublication.error}`;
|
|
28
|
+
const staleLines = freshnessError !== undefined
|
|
29
|
+
? [`Artifact freshness unavailable: ${freshnessError}`]
|
|
30
|
+
: diagnostics.staleArtifacts.length === 0
|
|
31
|
+
? ["Stale artifacts: none"]
|
|
32
|
+
: [
|
|
33
|
+
"Stale artifacts:",
|
|
34
|
+
...diagnostics.staleArtifacts.map(({ scope, path, reason }) => `- [${scope}] ${path} — ${reason}`),
|
|
35
|
+
];
|
|
36
|
+
const temporal = available ? diagnostics.temporal : undefined;
|
|
37
|
+
const temporalLines = temporal === undefined
|
|
38
|
+
? [`Temporal materialization unavailable: ${diagnostics.durableStateError ?? "no selected branch runtime"}`,
|
|
39
|
+
"Hot history: unavailable; offsets beyond 7 require explicit cold Git inspection",
|
|
40
|
+
"Retained patch tails: unavailable"]
|
|
41
|
+
: [`Temporal head: ${JSON.stringify(temporal.head.id)}; branch-local position ${temporal.head.position}`,
|
|
42
|
+
`Hot history: offsets 0..${temporal.historyDepth}; maximum depth 7`,
|
|
43
|
+
`Retained patch tails: global ${temporal.tailCounts.global}; CWD ${temporal.tailCounts.cwd}; session ${temporal.tailCounts.session}`];
|
|
44
|
+
const artifacts = (scope) => available ? countArtifacts(diagnostics.scopeStates, scope) : "unknown";
|
|
45
|
+
const promotions = available ? inspectMemoryPromotions(diagnostics.scopeStates.global) : [];
|
|
46
|
+
const promotionCounts = Object.fromEntries(["pending", "accepted", "failed", "unknown", "invalid"].map((status) => [status, promotions.filter((entry) => entry.status === status).length]));
|
|
47
|
+
const memoryScopes = available ? retainedMemoryScopes(diagnostics.scopeStates) : undefined;
|
|
48
|
+
const oneLine = (value) => value.replace(/\s+/g, " ").slice(0, 240);
|
|
49
|
+
const promotionLines = !available || promotions.length === 0 ? [] : [
|
|
50
|
+
"Memory promotions:",
|
|
51
|
+
...promotions.map((entry) => `- ${oneLine(entry.id)} — ${entry.status}; owner ${oneLine(entry.owner ?? "unavailable")}${entry.pointer ? `; pointer ${oneLine(entry.pointer)}` : ""}${entry.revision ? `; revision ${oneLine(entry.revision)}` : ""}${entry.error ? `; error ${oneLine(entry.error)}` : ""}`),
|
|
52
|
+
];
|
|
53
|
+
return [
|
|
54
|
+
`State Flow diagnostics — config.enabled=${snapshot.config.enabled}; branch mode=${snapshot.config.enabled ? "active" : "inactive"}`,
|
|
55
|
+
`Repository: ${diagnostics.repositoryRoot}`,
|
|
56
|
+
`Scope keys: CWD ${diagnostics.cwdScopeKey}; session ${diagnostics.sessionScopeKey}`,
|
|
57
|
+
"Session files: config.json owns behavior; meta.json owns lineage and provenance",
|
|
58
|
+
`Runtime metadata: step #${snapshot.meta.step}; active revision ${snapshot.meta.durableBase ?? "none"}; bootstrap ${snapshot.meta.bootstrap === true}`,
|
|
59
|
+
`Remote publication policy: ${snapshot.meta.remotePublication?.mode ?? "legacy-transition"}`,
|
|
60
|
+
diagnostics.publicationQueueError !== undefined
|
|
61
|
+
? `Remote queue: unavailable; error ${diagnostics.publicationQueueError}`
|
|
62
|
+
: diagnostics.publicationQueue === undefined
|
|
63
|
+
? "Remote queue: idle"
|
|
64
|
+
: `Remote queue: ${diagnostics.publicationQueue.status}; target ${abbreviatedCommit(diagnostics.publicationQueue.target)}; confirmed ${diagnostics.publicationQueue.confirmed ? abbreviatedCommit(diagnostics.publicationQueue.confirmed) : "none"}; attempt ${diagnostics.publicationQueue.attempt}${diagnostics.publicationQueue.error ? `; error ${diagnostics.publicationQueue.error}` : ""}`,
|
|
65
|
+
"Memory: owner state-flow; global retention enabled; global fallback active",
|
|
66
|
+
`Memory-bearing scopes: global ${memoryScopes?.global ?? "unknown"}; CWD ${memoryScopes?.cwd ?? "unknown"}; session ${memoryScopes?.session ?? "unknown"}`,
|
|
67
|
+
`Promotion status: pending ${promotionCounts.pending}; accepted ${promotionCounts.accepted}; failed ${promotionCounts.failed}; unknown ${promotionCounts.unknown}; invalid ${promotionCounts.invalid}`,
|
|
68
|
+
...promotionLines,
|
|
69
|
+
...temporalLines,
|
|
70
|
+
`Artifacts: global ${artifacts("global")}; CWD ${artifacts("cwd")}; session ${artifacts("session")}; stale ${stale}`,
|
|
71
|
+
available ? `Recent transitions: global ${diagnostics.recent.filter(({ transitions }) => transitions.some(({ scope }) => scope === "global")).length}; CWD ${diagnostics.recent.filter(({ transitions }) => transitions.some(({ scope }) => scope === "cwd")).length}; session ${diagnostics.recent.filter(({ transitions }) => transitions.some(({ scope }) => scope === "session")).length}; active ${projectedRecent.length}` : "Recent transitions: unavailable",
|
|
72
|
+
`Publication policy: ${snapshot.meta.remotePublication?.mode ?? "legacy-unresolved"}`,
|
|
73
|
+
`Publication: ${publication}`,
|
|
74
|
+
...staleLines,
|
|
75
|
+
...(stateJson === undefined
|
|
76
|
+
? ["Effective memory: unavailable"]
|
|
77
|
+
: [`Effective memory (${Buffer.byteLength(stateJson, "utf8")} JSON bytes; global → CWD → session overlay):`, "", stateJson]),
|
|
78
|
+
].join("\n");
|
|
79
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type DurableFileBase, type OwnedFileUpdate } from "./durable.ts";
|
|
2
|
+
import { type ArtifactProvenanceRegistry } from "./artifact.ts";
|
|
3
|
+
import { type FileRevision, type SessionRuntime } from "./snapshot.ts";
|
|
4
|
+
export { isFileRevision, type FileRevision } from "./snapshot.ts";
|
|
5
|
+
import type { StateScope } from "./state.ts";
|
|
6
|
+
import { type TemporalState } from "./temporal.ts";
|
|
7
|
+
export interface TemporalFileBase {
|
|
8
|
+
files: DurableFileBase[];
|
|
9
|
+
}
|
|
10
|
+
/** Probe once at a lifecycle boundary, never on cached state reads. Only spawn ENOENT is absence. */
|
|
11
|
+
export declare function detectGitCapability(): "git" | "files";
|
|
12
|
+
export declare function assertStorageDirectory(path: string): void;
|
|
13
|
+
/** Explicit creation only; existing bytes and unrelated files are never adopted or rewritten here. */
|
|
14
|
+
export declare function initializeFileStore(root: string): void;
|
|
15
|
+
/** Git writers also acquire this lock before their common-Git-directory lock. */
|
|
16
|
+
export declare function withStoragePublicationLock<T>(repositoryRoot: string, action: (root: string) => T): T;
|
|
17
|
+
export declare function assertTemporalFileBase(expected: TemporalFileBase, current: TemporalFileBase): void;
|
|
18
|
+
/** One shared publication plan for Git and files; neither backend invents semantic changes. */
|
|
19
|
+
export declare function planTemporalPublication(cwd: string, sessionId: string, view: TemporalState, scopes: readonly StateScope[], current: TemporalFileBase, root: string, runtime?: SessionRuntime, runtimeOnly?: boolean, sessionKey?: string, provenance?: Readonly<Record<StateScope, ArtifactProvenanceRegistry>>): {
|
|
20
|
+
updates: OwnedFileUpdate[];
|
|
21
|
+
changedScopes: StateScope[];
|
|
22
|
+
};
|
|
23
|
+
/** The accepted basis comes from prepared outputs, never a post-publication worktree reread. */
|
|
24
|
+
export declare function temporalFileReceipts(current: TemporalFileBase, updates: readonly OwnedFileUpdate[]): DurableFileBase[];
|
|
25
|
+
export declare function captureTemporalFileBase(cwd: string, sessionId: string, root: string, sessionKey?: string): TemporalFileBase;
|
|
26
|
+
/** Current-only reference: exact bytes, complete identities and lineage, no aliases or history store. */
|
|
27
|
+
export declare function loadTemporalFileRevision(cwd: string, sessionId: string, root: string, revision: string, sessionKey?: string): {
|
|
28
|
+
runtime: SessionRuntime;
|
|
29
|
+
view: {
|
|
30
|
+
scopes: Record<StateScope, import("./temporal.ts").ScopeStream>;
|
|
31
|
+
lineage: import("./temporal.ts").TransitionBoundary[];
|
|
32
|
+
};
|
|
33
|
+
provenance: Record<StateScope, ArtifactProvenanceRegistry>;
|
|
34
|
+
base: {
|
|
35
|
+
files: DurableFileBase[];
|
|
36
|
+
};
|
|
37
|
+
revision: `file:${string}`;
|
|
38
|
+
};
|
|
39
|
+
/** Publish a validated full runtime/scoped cohort; no Git commands, success receipts, or pending pushes. */
|
|
40
|
+
export declare function publishTemporalStateToFiles(cwd: string, sessionId: string, view: TemporalState, scopes: readonly StateScope[], base: TemporalFileBase, root: string, runtime: SessionRuntime, sessionKey?: string, provenance?: Readonly<Record<StateScope, ArtifactProvenanceRegistry>>): {
|
|
41
|
+
base: TemporalFileBase;
|
|
42
|
+
revision: FileRevision;
|
|
43
|
+
changed: boolean;
|
|
44
|
+
};
|
|
45
|
+
/** In-store format conversion only; no Git history or cross-repository import. */
|
|
46
|
+
export declare function migrateLegacyStorageToFiles(cwd: string, sessionId: string, root: string, sessionKey?: string): void;
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// Domain: exact file-cohort publication, current-only recovery, and cooperating worktree exclusion.
|
|
2
|
+
// Excludes: temporal algebra, Pi lifecycle, Git objects/remotes, and backend fallback policy.
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { closeSync, lstatSync, mkdirSync, openSync, rmSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { dirname, relative, resolve } from "node:path";
|
|
7
|
+
import { assertOwnedFileUpdates, captureTemporalFileBases, parseScopeProvenance, parseScopeStream, restoreDurableFileBases, serializeScopeMetadata, sessionRuntimePaths, temporalScopePaths, temporalStateFileUpdates, writeOwnedFileUpdates, } from "./durable.js";
|
|
8
|
+
import { parseArtifactProvenanceRegistry } from "./artifact.js";
|
|
9
|
+
import { hashJson, sameJson } from "./json.js";
|
|
10
|
+
import { RevisionUnavailableError, isFileRevision, parseSessionRuntime, serializeSessionRuntime } from "./snapshot.js";
|
|
11
|
+
import { planLegacyStorageMigration } from "./migration.js";
|
|
12
|
+
export { isFileRevision } from "./snapshot.js";
|
|
13
|
+
import { validateTemporalState } from "./temporal.js";
|
|
14
|
+
const SCOPES = ["global", "cwd", "session"];
|
|
15
|
+
/** Probe once at a lifecycle boundary, never on cached state reads. Only spawn ENOENT is absence. */
|
|
16
|
+
export function detectGitCapability() {
|
|
17
|
+
const result = spawnSync("git", ["--version"], { encoding: "utf8", timeout: 15_000 });
|
|
18
|
+
if (result.error && result.error.code === "ENOENT")
|
|
19
|
+
return "files";
|
|
20
|
+
if (result.error || result.status !== 0) {
|
|
21
|
+
throw new RevisionUnavailableError(`Cannot resolve Git capability: ${result.error?.message ?? result.stderr ?? `exit ${result.status}`}`);
|
|
22
|
+
}
|
|
23
|
+
return "git";
|
|
24
|
+
}
|
|
25
|
+
export function assertStorageDirectory(path) {
|
|
26
|
+
const root = resolve(path);
|
|
27
|
+
const parent = dirname(root);
|
|
28
|
+
if (parent !== root)
|
|
29
|
+
assertStorageDirectory(parent);
|
|
30
|
+
const stat = lstatSync(root, { throwIfNoEntry: false });
|
|
31
|
+
if (stat && (!stat.isDirectory() || stat.isSymbolicLink()))
|
|
32
|
+
throw new Error(`State Flow repository path is not a regular directory: ${root}`);
|
|
33
|
+
}
|
|
34
|
+
/** Explicit creation only; existing bytes and unrelated files are never adopted or rewritten here. */
|
|
35
|
+
export function initializeFileStore(root) {
|
|
36
|
+
assertStorageDirectory(root);
|
|
37
|
+
mkdirSync(resolve(root), { recursive: true });
|
|
38
|
+
}
|
|
39
|
+
/** Git writers also acquire this lock before their common-Git-directory lock. */
|
|
40
|
+
export function withStoragePublicationLock(repositoryRoot, action) {
|
|
41
|
+
const root = resolve(repositoryRoot);
|
|
42
|
+
assertStorageDirectory(root);
|
|
43
|
+
const path = resolve(root, ".state-flow-publication.lock");
|
|
44
|
+
let descriptor;
|
|
45
|
+
try {
|
|
46
|
+
descriptor = openSync(path, "wx", 0o600);
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
throw new RevisionUnavailableError(`State Flow publication lock is unavailable at ${path}; reconcile the active or interrupted publisher before retrying`, { cause: error });
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
writeFileSync(descriptor, `${process.pid}\n`);
|
|
53
|
+
return action(root);
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
closeSync(descriptor);
|
|
57
|
+
rmSync(path);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export function assertTemporalFileBase(expected, current) {
|
|
61
|
+
if (expected.files.length !== current.files.length || current.files.some((file, index) => {
|
|
62
|
+
const previous = expected.files[index];
|
|
63
|
+
return file.path !== previous.path || file.identity !== previous.identity;
|
|
64
|
+
}))
|
|
65
|
+
throw new Error("Temporal State Flow base or scope identity changed concurrently");
|
|
66
|
+
}
|
|
67
|
+
/** One shared publication plan for Git and files; neither backend invents semantic changes. */
|
|
68
|
+
export function planTemporalPublication(cwd, sessionId, view, scopes, current, root, runtime, runtimeOnly = false, sessionKey = sessionId, provenance) {
|
|
69
|
+
const candidates = temporalStateFileUpdates(cwd, sessionId, view, scopes, root, sessionKey);
|
|
70
|
+
const files = new Map(current.files.map((file) => [file.path, file]));
|
|
71
|
+
if (runtimeOnly && scopes.length !== 0)
|
|
72
|
+
throw new Error("Runtime-only publication cannot write semantic scopes");
|
|
73
|
+
const changedScopes = [];
|
|
74
|
+
for (const scope of SCOPES) {
|
|
75
|
+
const paths = temporalScopePaths(cwd, sessionId, scope, root, sessionKey);
|
|
76
|
+
if (files.get(resolve(paths.directory, "state.json")).identity !== "missing")
|
|
77
|
+
throw new Error("Legacy State Flow storage requires explicit migration");
|
|
78
|
+
const previous = parseScopeStream(files.get(paths.checkpoint).content, files.get(paths.patches).content, scope, scope === "cwd" ? cwd : undefined, files.get(paths.meta).content);
|
|
79
|
+
if (runtimeOnly || (previous !== undefined && sameJson(previous, view.scopes[scope])))
|
|
80
|
+
continue;
|
|
81
|
+
if (!scopes.includes(scope))
|
|
82
|
+
throw new Error(`Temporal scope update omitted a changed stream: ${scope}`);
|
|
83
|
+
changedScopes.push(scope);
|
|
84
|
+
}
|
|
85
|
+
const provenanceUpdates = [];
|
|
86
|
+
// Shared metadata owns provenance, temporal boundaries, and CWD identity beside semantic files.
|
|
87
|
+
if (!runtimeOnly) {
|
|
88
|
+
for (const scope of ["global", "cwd"]) {
|
|
89
|
+
const paths = temporalScopePaths(cwd, sessionId, scope, root, sessionKey);
|
|
90
|
+
const registry = provenance?.[scope] ?? parseScopeProvenance(files.get(paths.meta).content, paths.meta);
|
|
91
|
+
const currentFile = files.get(paths.meta);
|
|
92
|
+
if (!changedScopes.includes(scope)
|
|
93
|
+
&& (provenance === undefined || sameJson(parseScopeProvenance(currentFile.content, paths.meta), registry)))
|
|
94
|
+
continue;
|
|
95
|
+
const content = serializeScopeMetadata(registry, view.scopes[scope], scope, scope === "cwd" ? cwd : undefined, currentFile.content);
|
|
96
|
+
if (currentFile.content !== content)
|
|
97
|
+
provenanceUpdates.push({ path: paths.meta, content });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const runtimePaths = sessionRuntimePaths(cwd, sessionId, root, sessionKey);
|
|
101
|
+
const previousRuntime = parseSessionRuntime(files.get(runtimePaths.config).content, files.get(runtimePaths.meta).content, cwd, sessionId);
|
|
102
|
+
if (previousRuntime !== undefined && changedScopes.length > 0 && runtime === undefined)
|
|
103
|
+
throw new Error("Temporal semantic publication requires its session runtime cohort");
|
|
104
|
+
const runtimeUpdates = [];
|
|
105
|
+
if (runtime !== undefined) {
|
|
106
|
+
const sources = serializeSessionRuntime(runtime, cwd, sessionId, runtimeOnly ? undefined : view.scopes.session, files.get(runtimePaths.meta).content);
|
|
107
|
+
if (!sameJson(runtime.meta.lineage, view.lineage))
|
|
108
|
+
throw new Error("Runtime lineage does not match the temporal cohort");
|
|
109
|
+
if (files.get(runtimePaths.config).content !== sources.config || files.get(runtimePaths.meta).content !== sources.meta) {
|
|
110
|
+
runtimeUpdates.push({ path: runtimePaths.config, content: sources.config }, { path: runtimePaths.meta, content: sources.meta });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
else if (!runtimeOnly && changedScopes.includes("session")) {
|
|
114
|
+
const content = serializeScopeMetadata(undefined, view.scopes.session, "session", undefined, files.get(runtimePaths.meta).content);
|
|
115
|
+
if (files.get(runtimePaths.meta).content !== content)
|
|
116
|
+
runtimeUpdates.push({ path: runtimePaths.meta, content });
|
|
117
|
+
}
|
|
118
|
+
const changedPaths = new Set(changedScopes.flatMap((scope) => {
|
|
119
|
+
const paths = temporalScopePaths(cwd, sessionId, scope, root, sessionKey);
|
|
120
|
+
return [paths.checkpoint, paths.patches];
|
|
121
|
+
}));
|
|
122
|
+
return { updates: [...candidates.filter(({ path }) => changedPaths.has(path)), ...provenanceUpdates, ...runtimeUpdates], changedScopes };
|
|
123
|
+
}
|
|
124
|
+
/** The accepted basis comes from prepared outputs, never a post-publication worktree reread. */
|
|
125
|
+
export function temporalFileReceipts(current, updates) {
|
|
126
|
+
const receipts = new Map(updates.map((update) => [update.path, update.content]));
|
|
127
|
+
return current.files.map((file) => {
|
|
128
|
+
if (!receipts.has(file.path))
|
|
129
|
+
return file;
|
|
130
|
+
const content = receipts.get(file.path);
|
|
131
|
+
return content === undefined ? { path: file.path, identity: "missing" } : {
|
|
132
|
+
path: file.path, content, bytes: Buffer.from(content),
|
|
133
|
+
identity: `sha256:${createHash("sha256").update(content).digest("hex")}`,
|
|
134
|
+
};
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
function fileRevision(base, root) {
|
|
138
|
+
return `file:${hashJson({ root: resolve(root), files: base.files.map(({ path, identity }) => [relative(root, path), identity]) })}`;
|
|
139
|
+
}
|
|
140
|
+
function decodeFileCohort(cwd, sessionId, root, base, sessionKey = sessionId) {
|
|
141
|
+
const files = new Map(base.files.map((file) => [file.path, file.content]));
|
|
142
|
+
const scopes = {};
|
|
143
|
+
for (const scope of SCOPES) {
|
|
144
|
+
const paths = temporalScopePaths(cwd, sessionId, scope, root, sessionKey);
|
|
145
|
+
if (files.get(resolve(paths.directory, "state.json")) !== undefined)
|
|
146
|
+
throw new Error("Legacy State Flow storage requires explicit migration");
|
|
147
|
+
const stream = parseScopeStream(files.get(paths.checkpoint), files.get(paths.patches), scope, scope === "cwd" ? cwd : undefined, files.get(paths.meta));
|
|
148
|
+
if (!stream)
|
|
149
|
+
throw new Error("Incomplete file-only temporal scope cohort");
|
|
150
|
+
scopes[scope] = stream;
|
|
151
|
+
}
|
|
152
|
+
const paths = sessionRuntimePaths(cwd, sessionId, root, sessionKey);
|
|
153
|
+
const runtime = parseSessionRuntime(files.get(paths.config), files.get(paths.meta), cwd, sessionId);
|
|
154
|
+
if (!runtime || runtime.meta.publication !== "files")
|
|
155
|
+
throw new Error("File-only recovery requires file publication provenance, not a Git self reference");
|
|
156
|
+
const view = { scopes, lineage: runtime.meta.lineage };
|
|
157
|
+
validateTemporalState(view);
|
|
158
|
+
const provenance = {
|
|
159
|
+
global: parseScopeProvenance(files.get(temporalScopePaths(cwd, sessionId, "global", root, sessionKey).meta), temporalScopePaths(cwd, sessionId, "global", root, sessionKey).meta),
|
|
160
|
+
cwd: parseScopeProvenance(files.get(temporalScopePaths(cwd, sessionId, "cwd", root, sessionKey).meta), temporalScopePaths(cwd, sessionId, "cwd", root, sessionKey).meta),
|
|
161
|
+
session: parseArtifactProvenanceRegistry(runtime.meta.artifacts, "State Flow session artifact provenance"),
|
|
162
|
+
};
|
|
163
|
+
return { runtime, view, provenance };
|
|
164
|
+
}
|
|
165
|
+
export function captureTemporalFileBase(cwd, sessionId, root, sessionKey = sessionId) {
|
|
166
|
+
return withStoragePublicationLock(root, (locked) => ({ files: captureTemporalFileBases(cwd, sessionId, locked, sessionKey) }));
|
|
167
|
+
}
|
|
168
|
+
/** Current-only reference: exact bytes, complete identities and lineage, no aliases or history store. */
|
|
169
|
+
export function loadTemporalFileRevision(cwd, sessionId, root, revision, sessionKey = sessionId) {
|
|
170
|
+
if (!isFileRevision(revision))
|
|
171
|
+
throw new Error("File recovery requires an exact file revision");
|
|
172
|
+
return withStoragePublicationLock(root, (locked) => {
|
|
173
|
+
const base = { files: captureTemporalFileBases(cwd, sessionId, locked, sessionKey) };
|
|
174
|
+
if (fileRevision(base, locked) !== revision)
|
|
175
|
+
throw new RevisionUnavailableError(`State Flow file revision is unavailable: ${revision}`);
|
|
176
|
+
return { base, revision, ...decodeFileCohort(cwd, sessionId, locked, base, sessionKey) };
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
/** Publish a validated full runtime/scoped cohort; no Git commands, success receipts, or pending pushes. */
|
|
180
|
+
export function publishTemporalStateToFiles(cwd, sessionId, view, scopes, base, root, runtime, sessionKey = sessionId, provenance) {
|
|
181
|
+
return withStoragePublicationLock(root, (locked) => {
|
|
182
|
+
if (runtime.meta.publication !== "files")
|
|
183
|
+
throw new Error("File publication requires explicit file provenance");
|
|
184
|
+
const current = { files: captureTemporalFileBases(cwd, sessionId, locked, sessionKey) };
|
|
185
|
+
assertTemporalFileBase(base, current);
|
|
186
|
+
const { updates } = planTemporalPublication(cwd, sessionId, view, scopes, current, locked, runtime, false, sessionKey, provenance);
|
|
187
|
+
const next = { files: temporalFileReceipts(current, updates) };
|
|
188
|
+
decodeFileCohort(cwd, sessionId, locked, next, sessionKey);
|
|
189
|
+
const revision = fileRevision(next, locked);
|
|
190
|
+
publishFileUpdates(current.files, updates, locked);
|
|
191
|
+
return { base: next, revision, changed: updates.length > 0 };
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
function publishFileUpdates(bases, updates, root) {
|
|
195
|
+
writeOwnedFileUpdates(updates, bases, root);
|
|
196
|
+
try {
|
|
197
|
+
assertOwnedFileUpdates(updates, root);
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
try {
|
|
201
|
+
const touched = new Set(updates.map(({ path }) => path));
|
|
202
|
+
restoreDurableFileBases(bases.filter(({ path }) => touched.has(path)), root, updates);
|
|
203
|
+
}
|
|
204
|
+
catch (rollbackError) {
|
|
205
|
+
throw new AggregateError([error, rollbackError], "State Flow file publication and rollback failed");
|
|
206
|
+
}
|
|
207
|
+
throw error;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/** In-store format conversion only; no Git history or cross-repository import. */
|
|
211
|
+
export function migrateLegacyStorageToFiles(cwd, sessionId, root, sessionKey = sessionId) {
|
|
212
|
+
withStoragePublicationLock(root, (locked) => {
|
|
213
|
+
const plan = planLegacyStorageMigration(cwd, sessionId, locked, undefined, sessionKey);
|
|
214
|
+
if (plan.updates.length)
|
|
215
|
+
publishFileUpdates(plan.bases, plan.updates, locked);
|
|
216
|
+
});
|
|
217
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
export declare const STATE_FLOW_TELEGRAM_ID = "@llblab/pi-state-flow";
|
|
2
|
+
export interface StateFlowTelegramSnapshot {
|
|
3
|
+
enabled: boolean;
|
|
4
|
+
step: number;
|
|
5
|
+
bootstrap: boolean;
|
|
6
|
+
startPending: boolean;
|
|
7
|
+
}
|
|
8
|
+
export type StateFlowTelegramScope = "global" | "cwd" | "session" | "effective";
|
|
9
|
+
export interface StateFlowTelegramState {
|
|
10
|
+
artifacts: Record<string, unknown>;
|
|
11
|
+
contract: Record<string, unknown>;
|
|
12
|
+
working: Record<string, unknown>;
|
|
13
|
+
response: string;
|
|
14
|
+
}
|
|
15
|
+
export type StateFlowTelegramRichBlock = {
|
|
16
|
+
type: "heading";
|
|
17
|
+
text: string;
|
|
18
|
+
size: 3;
|
|
19
|
+
} | {
|
|
20
|
+
type: "pre";
|
|
21
|
+
text: string;
|
|
22
|
+
language?: string;
|
|
23
|
+
} | {
|
|
24
|
+
type: "details";
|
|
25
|
+
summary: string | {
|
|
26
|
+
type: "bold" | "code";
|
|
27
|
+
text: string;
|
|
28
|
+
};
|
|
29
|
+
blocks: StateFlowTelegramRichBlock[];
|
|
30
|
+
is_open?: true;
|
|
31
|
+
};
|
|
32
|
+
export interface StateFlowTelegramRichMessage {
|
|
33
|
+
blocks: StateFlowTelegramRichBlock[];
|
|
34
|
+
skip_entity_detection?: boolean;
|
|
35
|
+
}
|
|
36
|
+
export interface StateFlowTelegramButton {
|
|
37
|
+
text: string;
|
|
38
|
+
callback_data: string;
|
|
39
|
+
}
|
|
40
|
+
export interface StateFlowTelegramView {
|
|
41
|
+
text: string;
|
|
42
|
+
parseMode?: "markdown" | "html" | "plain";
|
|
43
|
+
replyMarkup?: {
|
|
44
|
+
inline_keyboard: StateFlowTelegramButton[][];
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export interface StateFlowTelegramSectionContext {
|
|
48
|
+
callbackData(action: string, payload?: string): string;
|
|
49
|
+
edit(view: StateFlowTelegramView): Promise<void>;
|
|
50
|
+
openRich(message: StateFlowTelegramRichMessage): Promise<void>;
|
|
51
|
+
answerCallback(text?: string): Promise<void>;
|
|
52
|
+
}
|
|
53
|
+
export interface StateFlowTelegramCallbackContext extends StateFlowTelegramSectionContext {
|
|
54
|
+
action: string;
|
|
55
|
+
payload: string;
|
|
56
|
+
}
|
|
57
|
+
export interface StateFlowTelegramSectionModule {
|
|
58
|
+
registerTelegramSection(section: {
|
|
59
|
+
id: string;
|
|
60
|
+
label: string;
|
|
61
|
+
getLabel?: () => string;
|
|
62
|
+
render: (ctx: StateFlowTelegramSectionContext) => StateFlowTelegramView | Promise<StateFlowTelegramView>;
|
|
63
|
+
handleCallback?: (ctx: StateFlowTelegramCallbackContext) => "handled" | "pass" | Promise<"handled" | "pass">;
|
|
64
|
+
}): () => void;
|
|
65
|
+
}
|
|
66
|
+
export interface StateFlowTelegramModules {
|
|
67
|
+
sections?: StateFlowTelegramSectionModule;
|
|
68
|
+
}
|
|
69
|
+
export type StateFlowTelegramLoader = () => Promise<StateFlowTelegramModules>;
|
|
70
|
+
export interface StateFlowTelegramControlResult {
|
|
71
|
+
ok: boolean;
|
|
72
|
+
message: string;
|
|
73
|
+
}
|
|
74
|
+
export interface StateFlowTelegramPort {
|
|
75
|
+
snapshot(): StateFlowTelegramSnapshot;
|
|
76
|
+
state(scope: StateFlowTelegramScope): StateFlowTelegramState;
|
|
77
|
+
canStartNow(): boolean;
|
|
78
|
+
start(): StateFlowTelegramControlResult;
|
|
79
|
+
stop(): StateFlowTelegramControlResult;
|
|
80
|
+
deferStart(): void;
|
|
81
|
+
cancelStart(): void;
|
|
82
|
+
}
|
|
83
|
+
export interface StateFlowTelegramAdapter {
|
|
84
|
+
ensure(): Promise<boolean>;
|
|
85
|
+
dispose(): void;
|
|
86
|
+
}
|
|
87
|
+
/** Main-menu section label doubles as the live status value: the spiral identity is constant, the value is not. */
|
|
88
|
+
export declare function formatStateFlowSectionLabel(snapshot: StateFlowTelegramSnapshot): string;
|
|
89
|
+
/** The submenu header repeats the button's state line; the single action matches the current state. */
|
|
90
|
+
export declare function buildStateFlowSectionView(snapshot: StateFlowTelegramSnapshot, callbackData: (action: string) => string): StateFlowTelegramView;
|
|
91
|
+
export declare function buildStateFlowScopeChooser(callbackData: (action: string, payload?: string) => string): StateFlowTelegramView;
|
|
92
|
+
export declare function renderStateFlowRichState(scope: StateFlowTelegramScope, step: number, state: StateFlowTelegramState): StateFlowTelegramRichMessage;
|
|
93
|
+
/** Default loader; injectable so tests and embedded hosts can control transport presence. */
|
|
94
|
+
export declare function loadStateFlowTelegramModules(): Promise<StateFlowTelegramModules>;
|
|
95
|
+
export declare function createStateFlowTelegramAdapter(options: {
|
|
96
|
+
port: StateFlowTelegramPort;
|
|
97
|
+
load?: StateFlowTelegramLoader;
|
|
98
|
+
}): StateFlowTelegramAdapter;
|