@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,111 @@
|
|
|
1
|
+
export type ContinuationTransport = "local" | "sdk" | "telegram" | string;
|
|
2
|
+
export interface ContinuationHostContext {
|
|
3
|
+
cwd: string;
|
|
4
|
+
agentDir: string;
|
|
5
|
+
sessionDir: string;
|
|
6
|
+
transport: ContinuationTransport;
|
|
7
|
+
}
|
|
8
|
+
export type ContinuationHostIntent = {
|
|
9
|
+
kind: "default";
|
|
10
|
+
} | {
|
|
11
|
+
kind: "new";
|
|
12
|
+
} | {
|
|
13
|
+
kind: "resume-exact";
|
|
14
|
+
sessionFile: string;
|
|
15
|
+
sessionId: string;
|
|
16
|
+
} | {
|
|
17
|
+
kind: "native-picker";
|
|
18
|
+
} | {
|
|
19
|
+
kind: "continue-recent";
|
|
20
|
+
} | {
|
|
21
|
+
kind: "no-session";
|
|
22
|
+
};
|
|
23
|
+
export interface ContinuationCandidateSummary {
|
|
24
|
+
sessionFile: string;
|
|
25
|
+
sessionId: string;
|
|
26
|
+
lastActivity: string;
|
|
27
|
+
reason: string;
|
|
28
|
+
}
|
|
29
|
+
export type ContinuationRecommendation = {
|
|
30
|
+
action: "resume";
|
|
31
|
+
sessionFile: string;
|
|
32
|
+
sessionId: string;
|
|
33
|
+
reason: "latest-enabled-state-flow";
|
|
34
|
+
} | {
|
|
35
|
+
action: "choose";
|
|
36
|
+
candidates: ContinuationCandidateSummary[];
|
|
37
|
+
reason: "ambiguous";
|
|
38
|
+
} | {
|
|
39
|
+
action: "new";
|
|
40
|
+
reason: "none" | "last-not-state-flow" | "ineligible";
|
|
41
|
+
};
|
|
42
|
+
export type ContinuationStartupDecision = ContinuationRecommendation | {
|
|
43
|
+
action: "new";
|
|
44
|
+
reason: "explicit-new";
|
|
45
|
+
} | {
|
|
46
|
+
action: "resume";
|
|
47
|
+
sessionFile: string;
|
|
48
|
+
sessionId: string;
|
|
49
|
+
reason: "explicit-resume";
|
|
50
|
+
} | {
|
|
51
|
+
action: "native";
|
|
52
|
+
mode: "picker" | "continue-recent" | "no-session";
|
|
53
|
+
};
|
|
54
|
+
export interface ContinuationProjectIdentity {
|
|
55
|
+
profile: string;
|
|
56
|
+
cwd: string;
|
|
57
|
+
gitCommonDir?: string;
|
|
58
|
+
worktree?: string;
|
|
59
|
+
branch?: string;
|
|
60
|
+
transport: ContinuationTransport;
|
|
61
|
+
}
|
|
62
|
+
export interface ContinuationSessionCandidate extends ContinuationCandidateSummary {
|
|
63
|
+
profile: string;
|
|
64
|
+
cwd: string;
|
|
65
|
+
gitCommonDir?: string;
|
|
66
|
+
worktree?: string;
|
|
67
|
+
branch?: string;
|
|
68
|
+
transport: ContinuationTransport;
|
|
69
|
+
lifecycle: "open" | "closed" | "archived";
|
|
70
|
+
doNotAutoResume?: boolean;
|
|
71
|
+
stateFlow: {
|
|
72
|
+
enabled: boolean;
|
|
73
|
+
restorable: boolean;
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
export type ContinuationRecommender = (context: Readonly<ContinuationHostContext>) => ContinuationRecommendation | Promise<ContinuationRecommendation>;
|
|
77
|
+
/** Rank already header/provenance-only candidates without transcript content or I/O. */
|
|
78
|
+
export declare function recommendContinuationFromProvenance(identity: ContinuationProjectIdentity, candidates: readonly ContinuationSessionCandidate[]): ContinuationRecommendation;
|
|
79
|
+
export declare function resolveContinuationStartup(context: ContinuationHostContext, intent: ContinuationHostIntent, recommend: ContinuationRecommender): Promise<ContinuationStartupDecision>;
|
|
80
|
+
export interface NativeSessionHeader {
|
|
81
|
+
file: string;
|
|
82
|
+
id: string;
|
|
83
|
+
cwd: string;
|
|
84
|
+
timestamp: string;
|
|
85
|
+
lastActivity: string;
|
|
86
|
+
}
|
|
87
|
+
export interface ContinuationCandidateProvenance {
|
|
88
|
+
profile: string;
|
|
89
|
+
gitCommonDir?: string;
|
|
90
|
+
worktree?: string;
|
|
91
|
+
branch?: string;
|
|
92
|
+
transport: ContinuationTransport;
|
|
93
|
+
lifecycle: "open" | "closed" | "archived";
|
|
94
|
+
doNotAutoResume?: boolean;
|
|
95
|
+
stateFlow: {
|
|
96
|
+
enabled: boolean;
|
|
97
|
+
restorable: boolean;
|
|
98
|
+
};
|
|
99
|
+
reason: string;
|
|
100
|
+
}
|
|
101
|
+
export declare function readNativeSessionHeader(path: string): NativeSessionHeader;
|
|
102
|
+
export declare function discoverNativeSessionHeaders(sessionDir: string): {
|
|
103
|
+
headers: NativeSessionHeader[];
|
|
104
|
+
invalid: Array<{
|
|
105
|
+
file: string;
|
|
106
|
+
error: string;
|
|
107
|
+
}>;
|
|
108
|
+
};
|
|
109
|
+
/** Inspect only exact current runtime provenance; never initialize, migrate, lock, checkout, or publish. */
|
|
110
|
+
export declare function inspectStateFlowContinuationProvenance(header: NativeSessionHeader, repositoryRoot: string): Pick<ContinuationCandidateProvenance, "stateFlow" | "reason">;
|
|
111
|
+
export declare function buildContinuationCandidates(headers: readonly NativeSessionHeader[], inspect: (header: Readonly<NativeSessionHeader>) => ContinuationCandidateProvenance | undefined): ContinuationSessionCandidate[];
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { closeSync, constants, existsSync, fstatSync, lstatSync, openSync, readFileSync, readSync, readdirSync, realpathSync, statSync } from "node:fs";
|
|
3
|
+
import { join, relative, resolve } from "node:path";
|
|
4
|
+
import { loadScopeStream, resolveSessionAddress, sessionRuntimePaths } from "./durable.js";
|
|
5
|
+
import { loadTemporalRevision } from "./git.js";
|
|
6
|
+
import { parseSessionRuntime } from "./snapshot.js";
|
|
7
|
+
import { validateTemporalState } from "./temporal.js";
|
|
8
|
+
/**
|
|
9
|
+
* Preserve host-owned explicit session intent and consult State Flow only for
|
|
10
|
+
* an ordinary default launch. The recommender is a read-only advisory port;
|
|
11
|
+
* opening or claiming a native session remains the host's responsibility.
|
|
12
|
+
*/
|
|
13
|
+
function sameOptionalIdentity(current, candidate) {
|
|
14
|
+
return current === undefined ? candidate === undefined : candidate === current;
|
|
15
|
+
}
|
|
16
|
+
function structurallyRelevant(identity, candidate) {
|
|
17
|
+
return candidate.profile === identity.profile
|
|
18
|
+
&& candidate.cwd === identity.cwd
|
|
19
|
+
&& candidate.transport === identity.transport
|
|
20
|
+
&& candidate.lifecycle === "open"
|
|
21
|
+
&& candidate.doNotAutoResume !== true
|
|
22
|
+
&& sameOptionalIdentity(identity.gitCommonDir, candidate.gitCommonDir)
|
|
23
|
+
&& sameOptionalIdentity(identity.worktree, candidate.worktree)
|
|
24
|
+
&& sameOptionalIdentity(identity.branch, candidate.branch)
|
|
25
|
+
&& Number.isFinite(Date.parse(candidate.lastActivity));
|
|
26
|
+
}
|
|
27
|
+
/** Rank already header/provenance-only candidates without transcript content or I/O. */
|
|
28
|
+
export function recommendContinuationFromProvenance(identity, candidates) {
|
|
29
|
+
const relevant = candidates.filter((candidate) => structurallyRelevant(identity, candidate));
|
|
30
|
+
if (relevant.length === 0)
|
|
31
|
+
return { action: "new", reason: "none" };
|
|
32
|
+
const latestTime = Math.max(...relevant.map(({ lastActivity }) => Date.parse(lastActivity)));
|
|
33
|
+
const latest = relevant.filter(({ lastActivity }) => Date.parse(lastActivity) === latestTime)
|
|
34
|
+
.sort((left, right) => left.sessionFile.localeCompare(right.sessionFile));
|
|
35
|
+
if (latest.length > 1) {
|
|
36
|
+
const resumable = latest.filter(({ stateFlow }) => stateFlow.enabled && stateFlow.restorable);
|
|
37
|
+
if (resumable.length > 1)
|
|
38
|
+
return { action: "choose", reason: "ambiguous", candidates: resumable.map(({ sessionFile, sessionId, lastActivity }) => ({ sessionFile, sessionId, lastActivity, reason: "equally recent enabled State Flow session" })) };
|
|
39
|
+
if (resumable.length === 1 && latest.every(({ stateFlow }) => stateFlow.enabled && stateFlow.restorable)) {
|
|
40
|
+
const candidate = resumable[0];
|
|
41
|
+
return { action: "resume", sessionFile: candidate.sessionFile, sessionId: candidate.sessionId, reason: "latest-enabled-state-flow" };
|
|
42
|
+
}
|
|
43
|
+
return { action: "new", reason: "ineligible" };
|
|
44
|
+
}
|
|
45
|
+
const candidate = latest[0];
|
|
46
|
+
if (!candidate.stateFlow.enabled)
|
|
47
|
+
return { action: "new", reason: "last-not-state-flow" };
|
|
48
|
+
if (!candidate.stateFlow.restorable)
|
|
49
|
+
return { action: "new", reason: "ineligible" };
|
|
50
|
+
return { action: "resume", sessionFile: candidate.sessionFile, sessionId: candidate.sessionId, reason: "latest-enabled-state-flow" };
|
|
51
|
+
}
|
|
52
|
+
export async function resolveContinuationStartup(context, intent, recommend) {
|
|
53
|
+
switch (intent.kind) {
|
|
54
|
+
case "new":
|
|
55
|
+
return { action: "new", reason: "explicit-new" };
|
|
56
|
+
case "resume-exact":
|
|
57
|
+
return { action: "resume", sessionFile: intent.sessionFile, sessionId: intent.sessionId, reason: "explicit-resume" };
|
|
58
|
+
case "native-picker":
|
|
59
|
+
return { action: "native", mode: "picker" };
|
|
60
|
+
case "continue-recent":
|
|
61
|
+
return { action: "native", mode: "continue-recent" };
|
|
62
|
+
case "no-session":
|
|
63
|
+
return { action: "native", mode: "no-session" };
|
|
64
|
+
case "default":
|
|
65
|
+
return recommend(Object.freeze({ ...context }));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const MAX_SESSION_HEADER_BYTES = 64 * 1024;
|
|
69
|
+
function readFirstLine(path) {
|
|
70
|
+
if (realpathSync(path) !== path || !lstatSync(path).isFile())
|
|
71
|
+
throw new Error("Native Pi session header requires a regular canonical file");
|
|
72
|
+
const fd = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
|
|
73
|
+
try {
|
|
74
|
+
if (!fstatSync(fd).isFile())
|
|
75
|
+
throw new Error("Native Pi session header requires a regular file");
|
|
76
|
+
const bytes = [];
|
|
77
|
+
const byte = Buffer.allocUnsafe(1);
|
|
78
|
+
while (bytes.length <= MAX_SESSION_HEADER_BYTES) {
|
|
79
|
+
const count = readSync(fd, byte, 0, 1, null);
|
|
80
|
+
if (count === 0 || byte[0] === 0x0a)
|
|
81
|
+
break;
|
|
82
|
+
bytes.push(byte[0]);
|
|
83
|
+
}
|
|
84
|
+
if (bytes.length > MAX_SESSION_HEADER_BYTES)
|
|
85
|
+
throw new Error("Native Pi session header exceeds the safe discovery limit");
|
|
86
|
+
return Buffer.from(bytes).toString("utf8").replace(/\r$/, "");
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
closeSync(fd);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
export function readNativeSessionHeader(path) {
|
|
93
|
+
const file = resolve(path);
|
|
94
|
+
let value;
|
|
95
|
+
try {
|
|
96
|
+
value = JSON.parse(readFirstLine(file));
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
throw new Error(`Cannot read native Pi session header: ${error instanceof Error ? error.message : String(error)}`);
|
|
100
|
+
}
|
|
101
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
102
|
+
throw new Error("Invalid native Pi session header");
|
|
103
|
+
const header = value;
|
|
104
|
+
if (header.type !== "session" || typeof header.id !== "string" || header.id.trim().length === 0
|
|
105
|
+
|| typeof header.cwd !== "string" || header.cwd.trim().length === 0
|
|
106
|
+
|| typeof header.timestamp !== "string" || !Number.isFinite(Date.parse(header.timestamp))) {
|
|
107
|
+
throw new Error("Invalid native Pi session header");
|
|
108
|
+
}
|
|
109
|
+
const stats = statSync(file);
|
|
110
|
+
return { file, id: header.id, cwd: resolve(header.cwd), timestamp: header.timestamp, lastActivity: stats.mtime.toISOString() };
|
|
111
|
+
}
|
|
112
|
+
export function discoverNativeSessionHeaders(sessionDir) {
|
|
113
|
+
const headers = [];
|
|
114
|
+
const invalid = [];
|
|
115
|
+
for (const entry of readdirSync(sessionDir, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))) {
|
|
116
|
+
if (!entry.isFile() || !entry.name.endsWith(".jsonl"))
|
|
117
|
+
continue;
|
|
118
|
+
const file = join(sessionDir, entry.name);
|
|
119
|
+
try {
|
|
120
|
+
headers.push(readNativeSessionHeader(file));
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
invalid.push({ file: resolve(file), error: error instanceof Error ? error.message : String(error) });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return { headers, invalid };
|
|
127
|
+
}
|
|
128
|
+
function readRegular(path) {
|
|
129
|
+
if (!existsSync(path))
|
|
130
|
+
return undefined;
|
|
131
|
+
if (!lstatSync(path).isFile())
|
|
132
|
+
throw new Error("State Flow continuation provenance must be a regular file");
|
|
133
|
+
return readFileSync(path, "utf8");
|
|
134
|
+
}
|
|
135
|
+
/** Inspect only exact current runtime provenance; never initialize, migrate, lock, checkout, or publish. */
|
|
136
|
+
export function inspectStateFlowContinuationProvenance(header, repositoryRoot) {
|
|
137
|
+
const sessionKey = resolveSessionAddress(header.file, header.id, header.timestamp).key;
|
|
138
|
+
const paths = sessionRuntimePaths(header.cwd, header.id, repositoryRoot, sessionKey);
|
|
139
|
+
try {
|
|
140
|
+
const config = readRegular(paths.config);
|
|
141
|
+
const meta = readRegular(paths.meta);
|
|
142
|
+
if (config === undefined && meta === undefined) {
|
|
143
|
+
return { stateFlow: { enabled: false, restorable: true }, reason: "no State Flow session runtime" };
|
|
144
|
+
}
|
|
145
|
+
const runtime = parseSessionRuntime(config, meta, header.cwd, header.id);
|
|
146
|
+
if (!runtime)
|
|
147
|
+
return { stateFlow: { enabled: false, restorable: true }, reason: "no State Flow session runtime" };
|
|
148
|
+
if (!runtime.config.enabled)
|
|
149
|
+
return { stateFlow: { enabled: false, restorable: true }, reason: "State Flow stopped on selected runtime" };
|
|
150
|
+
if (runtime.meta.publication === "files") {
|
|
151
|
+
const global = loadScopeStream(header.cwd, header.id, "global", repositoryRoot, sessionKey);
|
|
152
|
+
const cwd = loadScopeStream(header.cwd, header.id, "cwd", repositoryRoot, sessionKey);
|
|
153
|
+
const session = loadScopeStream(header.cwd, header.id, "session", repositoryRoot, sessionKey);
|
|
154
|
+
if (!global || !cwd || !session)
|
|
155
|
+
throw new Error("incomplete file-only temporal cohort");
|
|
156
|
+
validateTemporalState({ lineage: runtime.meta.lineage, scopes: { global, cwd, session } });
|
|
157
|
+
return { stateFlow: { enabled: true, restorable: true }, reason: "exact file-only runtime cohort is restorable" };
|
|
158
|
+
}
|
|
159
|
+
const runtimePath = relative(resolve(repositoryRoot), paths.meta);
|
|
160
|
+
if (runtimePath.startsWith(".."))
|
|
161
|
+
throw new Error("runtime provenance escapes repository");
|
|
162
|
+
const owner = execFileSync("git", ["-C", repositoryRoot, "log", "-1", "--format=%H", "--", runtimePath], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
163
|
+
if (!owner)
|
|
164
|
+
throw new Error("runtime owner commit is unavailable");
|
|
165
|
+
const restored = loadTemporalRevision(header.cwd, header.id, repositoryRoot, owner, sessionKey);
|
|
166
|
+
if (!restored.runtime || !restored.runtime.document.config.enabled)
|
|
167
|
+
throw new Error("selected Git runtime is not enabled");
|
|
168
|
+
return { stateFlow: { enabled: true, restorable: true }, reason: `exact Git runtime ${owner.slice(0, 12)} is restorable` };
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
return { stateFlow: { enabled: true, restorable: false }, reason: `State Flow runtime is ineligible: ${error instanceof Error ? error.message : String(error)}` };
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
export function buildContinuationCandidates(headers, inspect) {
|
|
175
|
+
const candidates = [];
|
|
176
|
+
for (const header of headers) {
|
|
177
|
+
const provenance = inspect(Object.freeze({ ...header }));
|
|
178
|
+
if (!provenance)
|
|
179
|
+
continue;
|
|
180
|
+
candidates.push({
|
|
181
|
+
sessionFile: header.file,
|
|
182
|
+
sessionId: header.id,
|
|
183
|
+
lastActivity: header.lastActivity,
|
|
184
|
+
cwd: header.cwd,
|
|
185
|
+
...provenance,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
return candidates;
|
|
189
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type ArtifactSourceIdentity } from "./artifact.ts";
|
|
2
|
+
/** Opaque source metadata discovered without decoding or retaining Markdown bodies. */
|
|
3
|
+
export interface ArtifactSourceCandidate extends ArtifactSourceIdentity {
|
|
4
|
+
/** Raw byte count used as a conservative maintenance token-budget ceiling. */
|
|
5
|
+
bytes: number;
|
|
6
|
+
}
|
|
7
|
+
export interface GlobalMarkdownDiscoveryResult {
|
|
8
|
+
sources: ArtifactSourceCandidate[];
|
|
9
|
+
removed: string[];
|
|
10
|
+
unavailable?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function getKnowledgeRoot(agentDir?: string): string;
|
|
13
|
+
/** Discover regular `*.md` files beneath a Knowledge root without following directory or file symlinks. */
|
|
14
|
+
export declare function discoverGlobalMarkdownSources(knowledgeRoot?: string): ArtifactSourceCandidate[];
|
|
15
|
+
/** Source-owner discovery; retained paths let restart/status re-observe removals until publication. */
|
|
16
|
+
export declare class GlobalMarkdownDiscovery {
|
|
17
|
+
#private;
|
|
18
|
+
readonly knowledgeRoot: string;
|
|
19
|
+
constructor(knowledgeRoot?: string);
|
|
20
|
+
refresh(retainedPaths?: Iterable<string>): GlobalMarkdownDiscoveryResult;
|
|
21
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { closeSync, constants, fstatSync, lstatSync, openSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
|
|
2
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { hashArtifactSource } from "./artifact.js";
|
|
5
|
+
export function getKnowledgeRoot(agentDir = getAgentDir()) {
|
|
6
|
+
return resolve(agentDir, "knowledge");
|
|
7
|
+
}
|
|
8
|
+
function errorCode(error) {
|
|
9
|
+
return error instanceof Error && "code" in error
|
|
10
|
+
? error.code
|
|
11
|
+
: undefined;
|
|
12
|
+
}
|
|
13
|
+
function isMissing(error) {
|
|
14
|
+
return errorCode(error) === "ENOENT";
|
|
15
|
+
}
|
|
16
|
+
function isInside(root, path) {
|
|
17
|
+
const child = relative(root, path);
|
|
18
|
+
return child === "" || (!isAbsolute(child) && child !== ".." && !child.startsWith(`..${sep}`));
|
|
19
|
+
}
|
|
20
|
+
function canonicalExistingPath(path) {
|
|
21
|
+
try {
|
|
22
|
+
return realpathSync.native(path);
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
if (isMissing(error))
|
|
26
|
+
return undefined;
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function readRegularFile(path) {
|
|
31
|
+
let descriptor;
|
|
32
|
+
try {
|
|
33
|
+
descriptor = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
34
|
+
if (!fstatSync(descriptor).isFile())
|
|
35
|
+
return undefined;
|
|
36
|
+
return readFileSync(descriptor);
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
if (isMissing(error) || errorCode(error) === "ELOOP")
|
|
40
|
+
return undefined;
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
if (descriptor !== undefined)
|
|
45
|
+
closeSync(descriptor);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** Discover regular `*.md` files beneath a Knowledge root without following directory or file symlinks. */
|
|
49
|
+
export function discoverGlobalMarkdownSources(knowledgeRoot = getKnowledgeRoot()) {
|
|
50
|
+
const configuredRoot = resolve(knowledgeRoot);
|
|
51
|
+
const root = canonicalExistingPath(configuredRoot);
|
|
52
|
+
if (root === undefined)
|
|
53
|
+
return [];
|
|
54
|
+
if (!statSync(root).isDirectory())
|
|
55
|
+
throw new Error(`Knowledge root is not a directory: ${configuredRoot}`);
|
|
56
|
+
const sources = [];
|
|
57
|
+
const visit = (directory) => {
|
|
58
|
+
const entries = readdirSync(directory, { withFileTypes: true })
|
|
59
|
+
.sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
|
|
60
|
+
for (const entry of entries) {
|
|
61
|
+
const candidate = join(directory, entry.name);
|
|
62
|
+
let metadata;
|
|
63
|
+
try {
|
|
64
|
+
metadata = lstatSync(candidate);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
if (isMissing(error))
|
|
68
|
+
continue;
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
if (metadata.isSymbolicLink())
|
|
72
|
+
continue;
|
|
73
|
+
const canonical = canonicalExistingPath(candidate);
|
|
74
|
+
if (canonical === undefined || !isInside(root, canonical))
|
|
75
|
+
continue;
|
|
76
|
+
if (metadata.isDirectory()) {
|
|
77
|
+
visit(canonical);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (!metadata.isFile() || !entry.name.endsWith(".md"))
|
|
81
|
+
continue;
|
|
82
|
+
const body = readRegularFile(canonical);
|
|
83
|
+
if (body === undefined)
|
|
84
|
+
continue;
|
|
85
|
+
sources.push({ path: canonical, hash: hashArtifactSource(body), bytes: body.byteLength });
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
visit(root);
|
|
89
|
+
return sources.sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
|
|
90
|
+
}
|
|
91
|
+
/** Prove absence inside the current source root without following excluded symlink components. */
|
|
92
|
+
function missingOwnedMarkdown(root, path) {
|
|
93
|
+
if (!isAbsolute(path) || path !== resolve(path) || path === root || !isInside(root, path) || !path.endsWith(".md"))
|
|
94
|
+
return false;
|
|
95
|
+
let candidate = root;
|
|
96
|
+
for (const segment of relative(root, path).split(sep)) {
|
|
97
|
+
candidate = join(candidate, segment);
|
|
98
|
+
const metadata = lstatSync(candidate, { throwIfNoEntry: false });
|
|
99
|
+
if (metadata === undefined)
|
|
100
|
+
return true;
|
|
101
|
+
if (metadata.isSymbolicLink() || !metadata.isDirectory())
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
/** Source-owner discovery; retained paths let restart/status re-observe removals until publication. */
|
|
107
|
+
export class GlobalMarkdownDiscovery {
|
|
108
|
+
knowledgeRoot;
|
|
109
|
+
#knownPaths = new Set();
|
|
110
|
+
constructor(knowledgeRoot = getKnowledgeRoot()) {
|
|
111
|
+
this.knowledgeRoot = resolve(knowledgeRoot);
|
|
112
|
+
}
|
|
113
|
+
refresh(retainedPaths = this.#knownPaths) {
|
|
114
|
+
const root = canonicalExistingPath(this.knowledgeRoot);
|
|
115
|
+
if (root === undefined)
|
|
116
|
+
return { sources: [], removed: [], unavailable: `Knowledge root is unavailable: ${this.knowledgeRoot}` };
|
|
117
|
+
const sources = discoverGlobalMarkdownSources(root);
|
|
118
|
+
const currentPaths = new Set(sources.map((source) => source.path));
|
|
119
|
+
const removed = [...new Set(retainedPaths)]
|
|
120
|
+
.filter((path) => !currentPaths.has(path) && missingOwnedMarkdown(root, path))
|
|
121
|
+
.sort();
|
|
122
|
+
this.#knownPaths = currentPaths;
|
|
123
|
+
return { sources, removed };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { type ArtifactProvenanceRegistry } from "./artifact.ts";
|
|
2
|
+
import { type ScopeStream, type TemporalState } from "./temporal.ts";
|
|
3
|
+
import type { StateScope } from "./state.ts";
|
|
4
|
+
/** Canonical semantic sources plus runtime-owned temporal metadata. */
|
|
5
|
+
export interface ScopeStreamSources {
|
|
6
|
+
checkpoint: string;
|
|
7
|
+
patches: string;
|
|
8
|
+
temporal: ScopeTemporalMetadata;
|
|
9
|
+
}
|
|
10
|
+
export interface ScopeTemporalMetadata {
|
|
11
|
+
checkpoint: ScopeStream["checkpoint"]["through"];
|
|
12
|
+
patches: ScopeStream["patches"][number]["transition"][];
|
|
13
|
+
}
|
|
14
|
+
export interface SessionAddress {
|
|
15
|
+
readonly id: string;
|
|
16
|
+
readonly key: string;
|
|
17
|
+
}
|
|
18
|
+
/** Semantic files contain no runtime envelope; temporal boundaries and CWD ownership live in meta.json. */
|
|
19
|
+
export declare function serializeScopeStream(stream: ScopeStream, scope: StateScope, cwdIdentity?: string): ScopeStreamSources;
|
|
20
|
+
export type ScopeStreamPresence = {
|
|
21
|
+
kind: "absent";
|
|
22
|
+
} | {
|
|
23
|
+
kind: "present";
|
|
24
|
+
stream: ScopeStream;
|
|
25
|
+
};
|
|
26
|
+
/** Distinguish a wholly absent semantic cohort from partial or malformed surviving authority. */
|
|
27
|
+
export declare function classifyScopeStream(checkpointSource: string | undefined, patchesSource: string | undefined, scope: StateScope, expectedCwd?: string, metaSource?: string): ScopeStreamPresence;
|
|
28
|
+
/** Decode the entire bounded replay input before accepting any materialized state. */
|
|
29
|
+
export declare function parseScopeStream(checkpointSource: string | undefined, patchesSource: string | undefined, scope: StateScope, expectedCwd?: string, metaSource?: string): ScopeStream | undefined;
|
|
30
|
+
export interface TemporalScopePaths {
|
|
31
|
+
directory: string;
|
|
32
|
+
checkpoint: string;
|
|
33
|
+
patches: string;
|
|
34
|
+
/** Runtime-owned artifact provenance for this scope; the session file also owns runtime lineage. */
|
|
35
|
+
meta: string;
|
|
36
|
+
}
|
|
37
|
+
export declare function temporalScopePaths(cwd: string, sessionId: string, scope: StateScope, repositoryRoot: string, sessionKey?: string): TemporalScopePaths;
|
|
38
|
+
export declare function sessionRuntimePaths(cwd: string, sessionId: string, repositoryRoot: string, sessionKey?: string): {
|
|
39
|
+
config: string;
|
|
40
|
+
meta: string;
|
|
41
|
+
};
|
|
42
|
+
/** Unsupported state.json presence never becomes an anchored checkpoint. */
|
|
43
|
+
export declare function loadScopeStream(cwd: string, sessionId: string, scope: StateScope, repositoryRoot: string, sessionKey?: string): ScopeStream | undefined;
|
|
44
|
+
/** Include legacy names in the CAS basis solely to prevent format races during cutover. */
|
|
45
|
+
export declare function captureTemporalFileBases(cwd: string, sessionId: string, repositoryRoot: string, sessionKey?: string): DurableFileBase[];
|
|
46
|
+
/** Select exact serialized scope updates from one validated active temporal cohort. */
|
|
47
|
+
export declare function temporalStateFileUpdates(cwd: string, sessionId: string, view: TemporalState, scopes: readonly StateScope[], repositoryRoot: string, sessionKey?: string): OwnedFileUpdate[];
|
|
48
|
+
export interface DurablePaths {
|
|
49
|
+
repositoryRoot: string;
|
|
50
|
+
globalState: string;
|
|
51
|
+
globalPatches: string;
|
|
52
|
+
}
|
|
53
|
+
/** Merge authoritative owned leaves while preserving forward-compatible metadata siblings. */
|
|
54
|
+
export declare function serializeScopeMetadata(registry: Readonly<ArtifactProvenanceRegistry> | undefined, stream: ScopeStream, scope: StateScope, cwdIdentity?: string, existingSource?: string): string;
|
|
55
|
+
/** Compatibility serializer retained for metadata-only callers. */
|
|
56
|
+
export declare function serializeScopeProvenance(registry: Readonly<ArtifactProvenanceRegistry>): string;
|
|
57
|
+
/** Missing provenance is unavailable evidence, never corrupt state. Unknown metadata is preserved by writers. */
|
|
58
|
+
export declare function parseScopeProvenance(source: string | undefined, path: string): ArtifactProvenanceRegistry;
|
|
59
|
+
export interface ScopePaths {
|
|
60
|
+
directory: string;
|
|
61
|
+
state: string;
|
|
62
|
+
patches: string;
|
|
63
|
+
}
|
|
64
|
+
export interface DurableFileBase {
|
|
65
|
+
path: string;
|
|
66
|
+
identity: "missing" | `sha256:${string}`;
|
|
67
|
+
content?: string;
|
|
68
|
+
/** Opaque originals for byte-exact rollback, including non-UTF-8 legacy journals. */
|
|
69
|
+
bytes?: Uint8Array;
|
|
70
|
+
}
|
|
71
|
+
/** Dedicated runtime storage, independent from Markdown source discovery. */
|
|
72
|
+
export declare function getDurableRepositoryRoot(agentDir?: string): string;
|
|
73
|
+
/** Legacy snapshot paths retained only for one-way migration and exact ownership checks. */
|
|
74
|
+
export declare function durablePaths(repositoryRoot?: string): DurablePaths;
|
|
75
|
+
/** Match Pi's native project-session directory convention exactly. */
|
|
76
|
+
export declare function cwdScopeKey(cwd: string): string;
|
|
77
|
+
/** One safe directory segment, normally the native Pi session filename stem. */
|
|
78
|
+
export declare function sessionScopeKey(key: string): string;
|
|
79
|
+
/** Prefer the actual native file stem; reproduce it from the immutable header when in-memory. */
|
|
80
|
+
export declare function sessionStorageKey(sessionFile: string | undefined, sessionId: string, timestamp?: string): string;
|
|
81
|
+
export declare function resolveSessionAddress(sessionFile: string | undefined, sessionId: string, timestamp?: string): SessionAddress;
|
|
82
|
+
export declare function cwdScopePaths(cwd: string, repositoryRoot?: string): ScopePaths;
|
|
83
|
+
export declare function sessionScopePaths(cwd: string, sessionId: string, repositoryRoot?: string, sessionKey?: string): ScopePaths;
|
|
84
|
+
export declare function cwdStatePath(cwd: string, repositoryRoot?: string): string;
|
|
85
|
+
export declare function cwdPatchesPath(cwd: string, repositoryRoot?: string): string;
|
|
86
|
+
export declare function sessionStatePath(cwd: string, sessionId: string, repositoryRoot?: string, sessionKey?: string): string;
|
|
87
|
+
export declare function sessionPatchesPath(cwd: string, sessionId: string, repositoryRoot?: string, sessionKey?: string): string;
|
|
88
|
+
/** Exact semantic file shapes, including legacy snapshots only during the storage cutover. */
|
|
89
|
+
export declare function isStateFlowOwnedPath(candidate: string, repositoryRoot?: string): boolean;
|
|
90
|
+
/** Capture exact owned bytes for one compare-and-swap publication or migration cohort. */
|
|
91
|
+
export declare function captureOwnedFileBases(paths: readonly string[], repositoryRoot: string): DurableFileBase[];
|
|
92
|
+
export interface OwnedFileUpdate {
|
|
93
|
+
path: string;
|
|
94
|
+
/** Omission means removal of this exact owned file, not a semantic null patch. */
|
|
95
|
+
content?: string;
|
|
96
|
+
}
|
|
97
|
+
/** Verify the publisher's exact output before commit or rollback, without trusting changed worktree bytes. */
|
|
98
|
+
export declare function assertOwnedFileUpdates(updates: readonly OwnedFileUpdate[], repositoryRoot: string): void;
|
|
99
|
+
/** Publish a prevalidated file cohort, preserving original bytes for failed preparation/publication. */
|
|
100
|
+
export declare function writeOwnedFileUpdates(updates: readonly OwnedFileUpdate[], bases: readonly DurableFileBase[], repositoryRoot: string): string[];
|
|
101
|
+
/** Restore exact pre-transition bytes only while files still match this publisher's output. */
|
|
102
|
+
export declare function restoreDurableFileBases(bases: readonly DurableFileBase[], repositoryRoot: string, expectedCurrent: readonly OwnedFileUpdate[]): void;
|