@llblab/pi-kit 0.6.0 → 0.7.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 +4 -4
- package/node_modules/@llblab/pi-actors/AGENTS.md +1 -1
- package/node_modules/@llblab/pi-actors/CHANGELOG.md +6 -0
- package/node_modules/@llblab/pi-actors/README.md +1 -1
- package/node_modules/@llblab/pi-actors/dist/lib/async-runs.d.ts +3 -0
- package/node_modules/@llblab/pi-actors/dist/lib/async-runs.js +14 -1
- package/node_modules/@llblab/pi-actors/dist/lib/command-templates.js +45 -3
- package/node_modules/@llblab/pi-actors/dist/lib/extension-runtime.js +1 -1
- package/node_modules/@llblab/pi-actors/dist/lib/observability.d.ts +16 -3
- package/node_modules/@llblab/pi-actors/dist/lib/observability.js +92 -7
- package/node_modules/@llblab/pi-actors/dist/lib/pi.d.ts +0 -1
- package/node_modules/@llblab/pi-actors/dist/lib/pi.js +15 -24
- package/node_modules/@llblab/pi-actors/dist/lib/run-delivery-lineage.d.ts +17 -0
- package/node_modules/@llblab/pi-actors/dist/lib/run-delivery-lineage.js +44 -0
- package/node_modules/@llblab/pi-actors/dist/lib/run-delivery.d.ts +4 -0
- package/node_modules/@llblab/pi-actors/dist/lib/run-delivery.js +102 -4
- package/node_modules/@llblab/pi-actors/dist/lib/run-ui-runtime.js +58 -39
- package/node_modules/@llblab/pi-actors/dist/lib/runtime.js +14 -6
- package/node_modules/@llblab/pi-actors/dist/skills/actors/SKILL.md +1 -1
- package/node_modules/@llblab/pi-actors/docs/async-runs.md +1 -1
- package/node_modules/@llblab/pi-actors/docs/coordinator-delivery.md +18 -23
- package/node_modules/@llblab/pi-actors/lib/async-runs.ts +18 -1
- package/node_modules/@llblab/pi-actors/lib/command-templates.ts +41 -3
- package/node_modules/@llblab/pi-actors/lib/extension-runtime.ts +1 -1
- package/node_modules/@llblab/pi-actors/lib/observability.ts +119 -5
- package/node_modules/@llblab/pi-actors/lib/pi.ts +15 -28
- package/node_modules/@llblab/pi-actors/lib/run-delivery-lineage.ts +68 -0
- package/node_modules/@llblab/pi-actors/lib/run-delivery.ts +120 -4
- package/node_modules/@llblab/pi-actors/lib/run-ui-runtime.ts +69 -44
- package/node_modules/@llblab/pi-actors/lib/runtime.ts +17 -6
- package/node_modules/@llblab/pi-actors/package.json +1 -1
- package/node_modules/@llblab/pi-actors/skills/actors/SKILL.md +1 -1
- package/node_modules/@llblab/pi-state-flow/AGENTS.md +33 -12
- package/node_modules/@llblab/pi-state-flow/BACKLOG.md +125 -2
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +87 -45
- package/node_modules/@llblab/pi-state-flow/README.md +203 -107
- package/node_modules/@llblab/pi-state-flow/docs/README.md +4 -0
- package/node_modules/@llblab/pi-state-flow/docs/architecture.md +188 -0
- package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +42 -0
- package/node_modules/@llblab/pi-state-flow/index.ts +164 -2
- package/node_modules/@llblab/pi-state-flow/lib/acquisition.ts +138 -0
- package/node_modules/@llblab/pi-state-flow/lib/artifact.ts +273 -0
- package/node_modules/@llblab/pi-state-flow/lib/config.ts +48 -0
- package/node_modules/@llblab/pi-state-flow/lib/context.ts +18 -5
- package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +268 -0
- package/node_modules/@llblab/pi-state-flow/lib/discovery.ts +117 -0
- package/node_modules/@llblab/pi-state-flow/lib/durable.ts +562 -0
- package/node_modules/@llblab/pi-state-flow/lib/episode.ts +24 -12
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +606 -70
- package/node_modules/@llblab/pi-state-flow/lib/git.ts +666 -0
- package/node_modules/@llblab/pi-state-flow/lib/history.ts +95 -0
- package/node_modules/@llblab/pi-state-flow/lib/json.ts +24 -0
- package/node_modules/@llblab/pi-state-flow/lib/maintenance.ts +141 -0
- package/node_modules/@llblab/pi-state-flow/lib/memory.ts +52 -0
- package/node_modules/@llblab/pi-state-flow/lib/migration.ts +88 -0
- package/node_modules/@llblab/pi-state-flow/lib/publication.ts +296 -0
- package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +23 -7
- package/node_modules/@llblab/pi-state-flow/lib/rehydration.ts +79 -0
- package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +264 -0
- package/node_modules/@llblab/pi-state-flow/lib/session.ts +6 -0
- package/node_modules/@llblab/pi-state-flow/lib/skills.ts +99 -7
- package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +305 -48
- package/node_modules/@llblab/pi-state-flow/lib/state.ts +74 -7
- package/node_modules/@llblab/pi-state-flow/lib/status.ts +125 -6
- package/node_modules/@llblab/pi-state-flow/lib/storage.ts +196 -0
- package/node_modules/@llblab/pi-state-flow/lib/temporal.ts +233 -0
- package/node_modules/@llblab/pi-state-flow/lib/terminal.ts +70 -24
- package/node_modules/@llblab/pi-state-flow/lib/transition.ts +254 -29
- package/node_modules/@llblab/pi-state-flow/package.json +8 -2
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +128 -0
- package/node_modules/@llblab/pi-telegram/AGENTS.md +13 -8
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +20 -3
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +13 -0
- package/node_modules/@llblab/pi-telegram/README.md +12 -8
- package/node_modules/@llblab/pi-telegram/docs/README.md +1 -0
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +220 -18
- package/node_modules/@llblab/pi-telegram/docs/generative-apps.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +70 -19
- package/node_modules/@llblab/pi-telegram/docs/outbound.md +6 -6
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +12 -5
- package/node_modules/@llblab/pi-telegram/docs/ui-style.md +3 -1
- package/node_modules/@llblab/pi-telegram/index.ts +4 -1418
- package/node_modules/@llblab/pi-telegram/lib/agent-messages.ts +6 -3
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +28 -1
- package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +600 -135
- package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +962 -55
- package/node_modules/@llblab/pi-telegram/lib/bus.ts +350 -26
- package/node_modules/@llblab/pi-telegram/lib/channel-posts.ts +544 -0
- package/node_modules/@llblab/pi-telegram/lib/commands.ts +234 -11
- package/node_modules/@llblab/pi-telegram/lib/config.ts +177 -24
- package/node_modules/@llblab/pi-telegram/lib/extension.ts +1792 -0
- package/node_modules/@llblab/pi-telegram/lib/generative-apps.ts +20 -2
- package/node_modules/@llblab/pi-telegram/lib/journal.ts +2184 -126
- package/node_modules/@llblab/pi-telegram/lib/locks.ts +38 -1
- package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +152 -13
- package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +51 -10
- package/node_modules/@llblab/pi-telegram/lib/paths.ts +29 -0
- package/node_modules/@llblab/pi-telegram/lib/polling.ts +85 -17
- package/node_modules/@llblab/pi-telegram/lib/prompts.ts +5 -2
- package/node_modules/@llblab/pi-telegram/lib/queue.ts +52 -18
- package/node_modules/@llblab/pi-telegram/lib/replies.ts +2 -2
- package/node_modules/@llblab/pi-telegram/lib/routing.ts +305 -112
- package/node_modules/@llblab/pi-telegram/lib/status.ts +10 -0
- package/node_modules/@llblab/pi-telegram/lib/sync.ts +308 -39
- package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +279 -4
- package/node_modules/@llblab/pi-telegram/lib/thread-cleanup-manager.ts +664 -0
- package/node_modules/@llblab/pi-telegram/lib/thread-display.ts +226 -0
- package/node_modules/@llblab/pi-telegram/lib/thread-naming.ts +118 -0
- package/node_modules/@llblab/pi-telegram/lib/threads.ts +1686 -129
- package/node_modules/@llblab/pi-telegram/lib/updates.ts +1319 -97
- package/node_modules/@llblab/pi-telegram/lib/workspace-admission.ts +1643 -0
- package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +968 -0
- package/node_modules/@llblab/pi-telegram/lib/workspace-slots.ts +84 -0
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/screenshot.png +0 -0
- package/node_modules/@llblab/pi-telegram/scripts/measure-bus.mjs +83 -0
- package/node_modules/@llblab/pi-telegram/scripts/measure-workspace.mjs +101 -0
- package/node_modules/@llblab/{skills → pi-telegram/skills}/show-me/SKILL.md +28 -6
- package/node_modules/@llblab/pi-telegram/skills/show-me/references/telegram-surfaces.md +43 -0
- package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/delivery-and-threads.md +1 -1
- package/node_modules/@llblab/skills/package.json +2 -3
- package/package.json +6 -5
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { lstatSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { parseScopeStream, sessionRuntimePaths, temporalScopePaths, type SessionAddress } from "./durable.ts";
|
|
5
|
+
import { adoptFileStateToGit, initializeGitRepository, isLocalGitRepository, captureTemporalGitBase, loadLegacyStatesAtRevision, loadTemporalRevision, migrateHashedCwdAtHead, migrateHashedLayoutAtHead, migrateLegacyStorageToGit, publishTemporalStateToGit, type TemporalGitBase } from "./git.ts";
|
|
6
|
+
import { captureTemporalFileBase, detectGitCapability, initializeFileStore, loadTemporalFileRevision, migrateLegacyStorageToFiles, publishTemporalStateToFiles } from "./storage.ts";
|
|
7
|
+
import { type AcceptedTransition, type RecentTransitionWindow } from "./history.ts";
|
|
8
|
+
import { hashJson, sameJson } from "./json.ts";
|
|
9
|
+
import { hasCwdMaterialization } from "./migration.ts";
|
|
10
|
+
import { RevisionUnavailableError, createSessionRuntime, isFileRevision, parseSessionRuntime, resolveFileSessionRuntime, resolveSessionRuntime, type Snapshot } from "./snapshot.ts";
|
|
11
|
+
import { emptyState, type MaterializedState, type ScopedStates, type StateScope } from "./state.ts";
|
|
12
|
+
import { adoptTemporalStreams, advanceTemporalState, createTemporalState, readTemporalState, validateTemporalState, type TemporalState } from "./temporal.ts";
|
|
13
|
+
|
|
14
|
+
const SCOPES = ["global", "cwd", "session"] as const;
|
|
15
|
+
export type RuntimePublication = ReturnType<typeof publishTemporalStateToGit> & { revision?: string };
|
|
16
|
+
|
|
17
|
+
/** Immutable target validation is independent of acquiring the live publication basis. */
|
|
18
|
+
export function inspectRuntimeRevision(cwd: string, sessionId: string, root: string, revision: string, sessionKey = sessionId) {
|
|
19
|
+
const loaded = loadTemporalRevision(cwd, sessionId, root, revision, sessionKey);
|
|
20
|
+
if (!loaded.runtime) throw new Error("Linked State Flow revision has no session runtime");
|
|
21
|
+
const resolved = resolveSessionRuntime(loaded.runtime.document, loaded.runtime.revision);
|
|
22
|
+
if (!loaded.scopes.global || !loaded.scopes.cwd || !loaded.scopes.session) throw new Error("Incomplete temporal scope cohort");
|
|
23
|
+
const view = { lineage: resolved.lineage, scopes: { global: loaded.scopes.global, cwd: loaded.scopes.cwd, session: loaded.scopes.session } };
|
|
24
|
+
validateTemporalState(view);
|
|
25
|
+
return { runtime: loaded.runtime, resolved, view, ...(loaded.legacyLayout ? { legacyLayout: true as const } : {}) };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Both checkpoint generations validate their immutable target before any live migration/publication. */
|
|
29
|
+
export function inspectSnapshotRevision(cwd: string, sessionId: string, root: string, revision: string, legacySnapshot?: Snapshot, sessionKey = sessionId) {
|
|
30
|
+
if (isFileRevision(revision)) {
|
|
31
|
+
const file = loadTemporalFileRevision(cwd, sessionId, root, revision, sessionKey);
|
|
32
|
+
return { snapshot: resolveFileSessionRuntime(file.runtime, revision), file };
|
|
33
|
+
}
|
|
34
|
+
if (detectGitCapability() === "files") throw new RevisionUnavailableError("Git is unavailable; cannot restore a Git-linked revision");
|
|
35
|
+
try {
|
|
36
|
+
const temporal = inspectRuntimeRevision(cwd, sessionId, root, revision, sessionKey);
|
|
37
|
+
return { snapshot: temporal.resolved.snapshot, temporal };
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if (!legacySnapshot || !(error instanceof Error) || !error.message.includes("Historical legacy storage requires explicit migration")) throw error;
|
|
40
|
+
const historical = loadLegacyStatesAtRevision(cwd, sessionId, root, revision, sessionKey);
|
|
41
|
+
const snapshot = structuredClone(legacySnapshot);
|
|
42
|
+
if (historical.session !== undefined) snapshot.legacySession = { state: historical.session };
|
|
43
|
+
return { snapshot, shared: { global: historical.global ?? emptyState(), cwd: historical.cwd ?? emptyState() } };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Cached branch-selected temporal state and publication basis; excludes Pi event policy. */
|
|
48
|
+
export class TemporalRuntime {
|
|
49
|
+
view: TemporalState | undefined;
|
|
50
|
+
private base: TemporalGitBase | undefined;
|
|
51
|
+
private semanticRevision: string | undefined;
|
|
52
|
+
private savedRuntime: string | undefined;
|
|
53
|
+
private backend: "git" | "files" | undefined;
|
|
54
|
+
readonly cwd: string;
|
|
55
|
+
private readonly session: SessionAddress;
|
|
56
|
+
readonly root: string;
|
|
57
|
+
constructor(cwd: string, session: string | SessionAddress, root: string, sessionKey?: string) {
|
|
58
|
+
this.cwd = cwd;
|
|
59
|
+
this.session = Object.freeze(typeof session === "string" ? { id: session, key: sessionKey ?? session } : { ...session });
|
|
60
|
+
this.root = root;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
get sessionId(): string { return this.session.id; }
|
|
64
|
+
get sessionKey(): string { return this.session.key; }
|
|
65
|
+
|
|
66
|
+
/** Explicit start owns directory/repository creation; reads never call this. */
|
|
67
|
+
prepare(): void {
|
|
68
|
+
const backend = detectGitCapability();
|
|
69
|
+
if (backend === "git") initializeGitRepository(this.root);
|
|
70
|
+
else initializeFileStore(this.root);
|
|
71
|
+
this.backend = backend;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Explicit start can upgrade a proven file cohort; failed adoption leaves the cache in file mode. */
|
|
75
|
+
promote(snapshot: Snapshot): RuntimePublication | undefined {
|
|
76
|
+
if (this.backend !== "files" || !this.view || detectGitCapability() === "files") return undefined;
|
|
77
|
+
if (!snapshot.meta.durableBase || snapshot.meta.durableBase !== this.semanticRevision) throw new Error("Git adoption requires the selected file revision");
|
|
78
|
+
const result = adoptFileStateToGit(this.cwd, this.sessionId, this.root, snapshot.meta.durableBase, snapshot, this.sessionKey);
|
|
79
|
+
const savedRuntime = hashJson(createSessionRuntime(snapshot, this.cwd, this.sessionId, result.view.lineage));
|
|
80
|
+
this.view = result.view;
|
|
81
|
+
this.base = result.base;
|
|
82
|
+
this.backend = "git";
|
|
83
|
+
this.semanticRevision = result.revision;
|
|
84
|
+
this.savedRuntime = savedRuntime;
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
read(offset = 0, scope?: StateScope): MaterializedState {
|
|
89
|
+
if (!this.view) throw new Error("State Flow temporal runtime is unavailable");
|
|
90
|
+
return readTemporalState(this.view, offset, scope);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
states(): ScopedStates {
|
|
94
|
+
return { global: this.read(0, "global"), cwd: this.read(0, "cwd"), session: this.read(0, "session") };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
causalBasis(): string {
|
|
98
|
+
if (!this.view) throw new Error("State Flow temporal runtime is unavailable");
|
|
99
|
+
return this.view.lineage.at(-1)!.id;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
recent(): RecentTransitionWindow {
|
|
103
|
+
const result: RecentTransitionWindow = [];
|
|
104
|
+
for (const boundary of this.view?.lineage.slice(1) ?? []) {
|
|
105
|
+
const transitions = SCOPES.flatMap((scope) => {
|
|
106
|
+
const record = this.view!.scopes[scope].patches.find(({ transition }) => transition.id === boundary.id);
|
|
107
|
+
return record ? [{ scope, patch: structuredClone(record.patch) }] : [];
|
|
108
|
+
});
|
|
109
|
+
if (transitions.length) result.push({ id: boundary.id, at: boundary.position, transitions });
|
|
110
|
+
}
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
restore(revision: string, legacySnapshot?: Snapshot): Snapshot {
|
|
115
|
+
const inspected = inspectSnapshotRevision(this.cwd, this.sessionId, this.root, revision, legacySnapshot, this.sessionKey);
|
|
116
|
+
if (inspected.file) {
|
|
117
|
+
const savedRuntime = hashJson(createSessionRuntime(inspected.snapshot, this.cwd, this.sessionId, inspected.file.view.lineage, "files"));
|
|
118
|
+
this.view = inspected.file.view;
|
|
119
|
+
this.base = inspected.file.base;
|
|
120
|
+
this.backend = "files";
|
|
121
|
+
this.semanticRevision = revision;
|
|
122
|
+
this.savedRuntime = savedRuntime;
|
|
123
|
+
return inspected.snapshot;
|
|
124
|
+
}
|
|
125
|
+
if (!inspected.temporal) {
|
|
126
|
+
const migrated = inspected.snapshot;
|
|
127
|
+
const publication = this.initialize(migrated, true, inspected.shared);
|
|
128
|
+
if (publication?.commit) migrated.meta.durableBase = publication.commit;
|
|
129
|
+
if (publication?.push?.status === "pending") migrated.meta.pendingPublication = { commit: publication.push.commit, error: publication.push.error ?? "Unconfirmed publication" };
|
|
130
|
+
else delete migrated.meta.pendingPublication;
|
|
131
|
+
delete migrated.legacySession;
|
|
132
|
+
return migrated;
|
|
133
|
+
}
|
|
134
|
+
const loaded = inspected.temporal;
|
|
135
|
+
const { resolved, view } = loaded;
|
|
136
|
+
let base = captureTemporalGitBase(this.cwd, this.sessionId, this.root, this.sessionKey);
|
|
137
|
+
const reference = loaded.runtime.document.meta.temporalRevision;
|
|
138
|
+
let semanticRevision = reference === undefined || reference === "self" ? loaded.runtime.revision : reference;
|
|
139
|
+
let publicationTarget = resolved.publicationTarget;
|
|
140
|
+
let migrationPush;
|
|
141
|
+
if (loaded.legacyLayout && base.head) {
|
|
142
|
+
const liveRevision = base.head;
|
|
143
|
+
const migration = migrateHashedLayoutAtHead(this.cwd, this.sessionId, this.root, liveRevision, this.sessionKey);
|
|
144
|
+
if (migration?.commit) {
|
|
145
|
+
base = migration.base;
|
|
146
|
+
if (liveRevision === revision) {
|
|
147
|
+
semanticRevision = migration.commit;
|
|
148
|
+
publicationTarget = migration.commit;
|
|
149
|
+
migrationPush = migration.push;
|
|
150
|
+
}
|
|
151
|
+
} else {
|
|
152
|
+
migrateHashedCwdAtHead(this.cwd, this.root);
|
|
153
|
+
base = captureTemporalGitBase(this.cwd, this.sessionId, this.root, this.sessionKey);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
resolved.snapshot.meta.durableBase = publicationTarget;
|
|
157
|
+
const savedRuntime = hashJson(createSessionRuntime(resolved.snapshot, this.cwd, this.sessionId, view.lineage));
|
|
158
|
+
resolved.snapshot.meta.pendingPublication = { commit: publicationTarget, error: "Durable publication intent is unconfirmed" };
|
|
159
|
+
if (migrationPush?.status === "pending") {
|
|
160
|
+
resolved.snapshot.meta.pendingPublication = { commit: migrationPush.commit, error: migrationPush.error ?? "Unconfirmed publication" };
|
|
161
|
+
} else if (migrationPush !== undefined) {
|
|
162
|
+
delete resolved.snapshot.meta.pendingPublication;
|
|
163
|
+
} else {
|
|
164
|
+
try {
|
|
165
|
+
if (isLocalGitRepository(this.root)) delete resolved.snapshot.meta.pendingPublication;
|
|
166
|
+
} catch {
|
|
167
|
+
// Invalid remote/branch configuration retains unconfirmed publication for explicit retry.
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
this.view = view;
|
|
171
|
+
this.base = base;
|
|
172
|
+
this.backend = "git";
|
|
173
|
+
this.semanticRevision = semanticRevision;
|
|
174
|
+
this.savedRuntime = savedRuntime;
|
|
175
|
+
return resolved.snapshot;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
initialize(snapshot: Snapshot, allowCreateCwd: boolean, expectedShared?: Pick<ScopedStates, "global" | "cwd">, newSessionOrigin = false): RuntimePublication | undefined {
|
|
179
|
+
const hasCwd = hasCwdMaterialization(this.cwd, this.root);
|
|
180
|
+
if (!allowCreateCwd && !hasCwd) return undefined;
|
|
181
|
+
const backend = this.backend ?? (detectGitCapability() === "git" && lstatSync(join(this.root, ".git"), { throwIfNoEntry: false }) ? "git" : "files");
|
|
182
|
+
if (backend === "git") {
|
|
183
|
+
if (!hasCwd) migrateHashedCwdAtHead(this.cwd, this.root);
|
|
184
|
+
migrateLegacyStorageToGit(this.cwd, this.sessionId, this.root, this.sessionKey);
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
if (allowCreateCwd) initializeFileStore(this.root);
|
|
188
|
+
migrateLegacyStorageToFiles(this.cwd, this.sessionId, this.root, this.sessionKey);
|
|
189
|
+
}
|
|
190
|
+
const base: TemporalGitBase = backend === "git" ? captureTemporalGitBase(this.cwd, this.sessionId, this.root, this.sessionKey) : captureTemporalFileBase(this.cwd, this.sessionId, this.root, this.sessionKey);
|
|
191
|
+
const files = new Map(base.files.map((file) => [file.path, file.content]));
|
|
192
|
+
const streams = Object.fromEntries(SCOPES.map((scope) => {
|
|
193
|
+
const paths = temporalScopePaths(this.cwd, this.sessionId, scope, this.root, this.sessionKey);
|
|
194
|
+
return [scope, parseScopeStream(files.get(paths.checkpoint), files.get(paths.patches), scope, scope === "cwd" ? this.cwd : undefined)];
|
|
195
|
+
})) as Record<StateScope, TemporalState["scopes"][StateScope] | undefined>;
|
|
196
|
+
if (!streams.cwd && !allowCreateCwd) return undefined;
|
|
197
|
+
const paths = sessionRuntimePaths(this.cwd, this.sessionId, this.root, this.sessionKey);
|
|
198
|
+
const existingRuntime = parseSessionRuntime(files.get(paths.config), files.get(paths.meta), this.cwd, this.sessionId);
|
|
199
|
+
if (existingRuntime && !snapshot.legacySession && !newSessionOrigin) throw new Error("Existing session runtime requires a branch revision pointer");
|
|
200
|
+
// Explicit start before any branch runtime is a new origin, never inheritance of a later session layer.
|
|
201
|
+
if (snapshot.legacySession || newSessionOrigin) streams.session = undefined;
|
|
202
|
+
const fresh = createTemporalState({ global: emptyState(), cwd: emptyState(), session: snapshot.legacySession?.state ?? emptyState() }, randomUUID());
|
|
203
|
+
const candidate = new TemporalRuntime(this.cwd, this.session, this.root);
|
|
204
|
+
candidate.backend = backend;
|
|
205
|
+
candidate.base = base;
|
|
206
|
+
candidate.view = adoptTemporalStreams({ global: streams.global ?? fresh.scopes.global, cwd: streams.cwd ?? fresh.scopes.cwd, session: streams.session ?? fresh.scopes.session }, `${base.head ?? "unborn"}:${randomUUID()}`);
|
|
207
|
+
if (expectedShared && (["global", "cwd"] as const).some((scope) => !sameJson(candidate.read(0, scope), expectedShared[scope]))) {
|
|
208
|
+
throw new Error("Legacy branch shared scopes diverged from the selected revision; migration cannot overwrite them");
|
|
209
|
+
}
|
|
210
|
+
const publication = candidate.publish(snapshot, true);
|
|
211
|
+
this.view = candidate.view;
|
|
212
|
+
this.base = candidate.base;
|
|
213
|
+
this.backend = backend;
|
|
214
|
+
this.semanticRevision = candidate.semanticRevision;
|
|
215
|
+
this.savedRuntime = candidate.savedRuntime;
|
|
216
|
+
return publication;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
publish(snapshot: Snapshot, semantic = false, accepted?: AcceptedTransition, options: { pushRemote?: boolean } = {}): RuntimePublication | undefined {
|
|
220
|
+
if (!this.view || !this.base) throw new Error("State Flow temporal publication is unavailable; restore or initialize before accepting a transition");
|
|
221
|
+
const next = accepted ? advanceTemporalState(this.view, accepted.transitions, accepted.id) : this.view;
|
|
222
|
+
const runtime = createSessionRuntime(snapshot, this.cwd, this.sessionId, next.lineage, this.backend === "files" ? "files" : "unconfirmed");
|
|
223
|
+
const fingerprint = hashJson(runtime);
|
|
224
|
+
if (!semantic && fingerprint === this.savedRuntime) return undefined;
|
|
225
|
+
if (semantic && this.semanticRevision) {
|
|
226
|
+
// Session files may branch; shared scopes may not be silently merged or rewound.
|
|
227
|
+
const live = new Map(this.base.files.map((file) => [file.path, file.content]));
|
|
228
|
+
for (const scope of ["global", "cwd"] as const) {
|
|
229
|
+
const paths = temporalScopePaths(this.cwd, this.sessionId, scope, this.root, this.sessionKey);
|
|
230
|
+
const stream = parseScopeStream(live.get(paths.checkpoint), live.get(paths.patches), scope, scope === "cwd" ? this.cwd : undefined);
|
|
231
|
+
if (!sameJson(stream, this.view.scopes[scope])) throw new Error("State Flow cannot publish this restored branch because its global or CWD scope changed concurrently after the linked revision");
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (this.backend === "files") {
|
|
235
|
+
runtime.meta.temporalRevision = "self";
|
|
236
|
+
const result = publishTemporalStateToFiles(this.cwd, this.sessionId, next, semantic ? SCOPES : [], this.base, this.root, runtime, this.sessionKey);
|
|
237
|
+
this.base = result.base;
|
|
238
|
+
this.view = next;
|
|
239
|
+
this.savedRuntime = fingerprint;
|
|
240
|
+
this.semanticRevision = result.revision;
|
|
241
|
+
return { base: result.base, revision: result.revision };
|
|
242
|
+
}
|
|
243
|
+
if (!semantic) {
|
|
244
|
+
const current = captureTemporalGitBase(this.cwd, this.sessionId, this.root, this.sessionKey);
|
|
245
|
+
const paths = sessionRuntimePaths(this.cwd, this.sessionId, this.root, this.sessionKey);
|
|
246
|
+
for (const path of [paths.config, paths.meta]) {
|
|
247
|
+
if (current.files.find((file) => file.path === path)?.identity !== this.base.files.find((file) => file.path === path)?.identity) {
|
|
248
|
+
throw new Error("Temporal State Flow runtime changed concurrently");
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
this.base = current;
|
|
252
|
+
}
|
|
253
|
+
runtime.meta.temporalRevision = semantic ? "self" : this.semanticRevision!;
|
|
254
|
+
const result = publishTemporalStateToGit(
|
|
255
|
+
this.cwd, this.sessionId, next, semantic ? SCOPES : [], this.base, this.root, runtime, this.sessionKey,
|
|
256
|
+
options.pushRemote !== false,
|
|
257
|
+
);
|
|
258
|
+
this.base = result.base;
|
|
259
|
+
this.view = next;
|
|
260
|
+
this.savedRuntime = fingerprint;
|
|
261
|
+
if (semantic && result.commit) this.semanticRevision = result.commit;
|
|
262
|
+
return result;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
@@ -47,3 +47,9 @@ export function hasPriorConversation(branch: readonly BranchEntry[]): boolean {
|
|
|
47
47
|
}
|
|
48
48
|
return false;
|
|
49
49
|
}
|
|
50
|
+
|
|
51
|
+
/** Auto-start eligibility is session identity/lifecycle, not the presence of CWD materialization. */
|
|
52
|
+
export function isNewSession(reason: unknown, branch: readonly BranchEntry[]): boolean {
|
|
53
|
+
if (reason === "new") return true;
|
|
54
|
+
return reason === "startup" && !hasPriorConversation(branch);
|
|
55
|
+
}
|
|
@@ -1,4 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import {
|
|
3
|
+
hashArtifactSource,
|
|
4
|
+
isArtifactHash,
|
|
5
|
+
type ArtifactMetadata,
|
|
6
|
+
type ArtifactRegistry,
|
|
7
|
+
} from "./artifact.ts";
|
|
8
|
+
import { canonicalJson, isObject, type JsonObject, type JsonValue } from "./json.ts";
|
|
9
|
+
import type { MaterializedState } from "./state.ts";
|
|
10
|
+
|
|
11
|
+
export const SKILL_ARTIFACT_COMPILER = "skill-artifact-v1";
|
|
2
12
|
|
|
3
13
|
function hasContent(value: unknown): boolean {
|
|
4
14
|
if (typeof value === "string") return value.trim().length > 0;
|
|
@@ -7,9 +17,70 @@ function hasContent(value: unknown): boolean {
|
|
|
7
17
|
return value !== undefined && value !== null;
|
|
8
18
|
}
|
|
9
19
|
|
|
10
|
-
export function
|
|
11
|
-
|
|
12
|
-
|
|
20
|
+
export function hasCompiledSkillArtifact(
|
|
21
|
+
artifacts: ArtifactRegistry,
|
|
22
|
+
source: string,
|
|
23
|
+
expectedHash?: string,
|
|
24
|
+
): boolean {
|
|
25
|
+
const metadata = artifacts[source];
|
|
26
|
+
return isObject(metadata)
|
|
27
|
+
&& metadata.kind === "skill"
|
|
28
|
+
&& metadata.compiler === SKILL_ARTIFACT_COMPILER
|
|
29
|
+
&& (expectedHash === undefined || metadata.hash === expectedHash)
|
|
30
|
+
&& isObject(metadata.compilation)
|
|
31
|
+
&& hasContent(metadata.compilation);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type SkillSourceHasher = (source: string) => string;
|
|
35
|
+
|
|
36
|
+
export function hashSkillSource(source: string): string {
|
|
37
|
+
return hashArtifactSource(readFileSync(source));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function legacyCompilation(value: JsonValue): JsonObject | undefined {
|
|
41
|
+
if (!hasContent(value)) return undefined;
|
|
42
|
+
return isObject(value) ? structuredClone(value) : { value: structuredClone(value) };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Move the retired contract store into source-addressed Skill artifacts. */
|
|
46
|
+
export function migrateLegacySkillCompilations(
|
|
47
|
+
state: MaterializedState,
|
|
48
|
+
hasher: SkillSourceHasher = hashSkillSource,
|
|
49
|
+
): MaterializedState {
|
|
50
|
+
if (!isObject(state.contract.compiled_skills)) return structuredClone(state);
|
|
51
|
+
const legacy = state.contract.compiled_skills;
|
|
52
|
+
const next = structuredClone(state);
|
|
53
|
+
delete next.contract.compiled_skills;
|
|
54
|
+
for (const [source, value] of Object.entries(legacy)) {
|
|
55
|
+
const compilation = legacyCompilation(value);
|
|
56
|
+
if (!compilation) continue;
|
|
57
|
+
let hash: string;
|
|
58
|
+
let verified = true;
|
|
59
|
+
try {
|
|
60
|
+
hash = hasher(source);
|
|
61
|
+
if (!isArtifactHash(hash)) throw new Error("invalid source hash");
|
|
62
|
+
} catch {
|
|
63
|
+
// Preserve useful legacy behavior while ensuring a later observable source
|
|
64
|
+
// identity normally invalidates this explicitly unverified fallback.
|
|
65
|
+
hash = hashArtifactSource(canonicalJson({ source, compilation }));
|
|
66
|
+
verified = false;
|
|
67
|
+
}
|
|
68
|
+
const metadata: ArtifactMetadata = {
|
|
69
|
+
description: `Compiled operational guidance for the Skill at ${source}`,
|
|
70
|
+
hash,
|
|
71
|
+
compiler: SKILL_ARTIFACT_COMPILER,
|
|
72
|
+
kind: "skill",
|
|
73
|
+
compilation,
|
|
74
|
+
...(verified ? {} : { source_hash_verified: false }),
|
|
75
|
+
};
|
|
76
|
+
Object.defineProperty(next.artifacts, source, {
|
|
77
|
+
value: metadata,
|
|
78
|
+
enumerable: true,
|
|
79
|
+
configurable: true,
|
|
80
|
+
writable: true,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return next;
|
|
13
84
|
}
|
|
14
85
|
|
|
15
86
|
export function skillPathFromRead(toolName: unknown, args: unknown): string | undefined {
|
|
@@ -17,15 +88,26 @@ export function skillPathFromRead(toolName: unknown, args: unknown): string | un
|
|
|
17
88
|
return /(^|[\\/])SKILL\.md$/.test(args.path) ? args.path : undefined;
|
|
18
89
|
}
|
|
19
90
|
|
|
91
|
+
export interface SuccessfulSkillRead {
|
|
92
|
+
path: string;
|
|
93
|
+
hash?: string;
|
|
94
|
+
error?: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
20
97
|
interface PendingRead {
|
|
21
98
|
toolName: string;
|
|
22
99
|
args: unknown;
|
|
23
100
|
}
|
|
24
101
|
|
|
25
|
-
/** Correlates Pi's mutable tool lifecycle
|
|
102
|
+
/** Correlates Pi's mutable tool lifecycle and captures trusted source identity. */
|
|
26
103
|
export class SkillReadTracker {
|
|
27
|
-
readonly successful = new
|
|
104
|
+
readonly successful = new Map<string, SuccessfulSkillRead>();
|
|
28
105
|
readonly #pending = new Map<string, PendingRead>();
|
|
106
|
+
readonly hashSource: SkillSourceHasher;
|
|
107
|
+
|
|
108
|
+
constructor(hashSource: SkillSourceHasher = hashSkillSource) {
|
|
109
|
+
this.hashSource = hashSource;
|
|
110
|
+
}
|
|
29
111
|
|
|
30
112
|
clear(): void {
|
|
31
113
|
this.successful.clear();
|
|
@@ -45,7 +127,17 @@ export class SkillReadTracker {
|
|
|
45
127
|
this.#pending.delete(toolCallId);
|
|
46
128
|
if (isError || !pending || toolName !== pending.toolName) return;
|
|
47
129
|
const source = skillPathFromRead(pending.toolName, pending.args);
|
|
48
|
-
if (source)
|
|
130
|
+
if (!source) return;
|
|
131
|
+
try {
|
|
132
|
+
const hash = this.hashSource(source);
|
|
133
|
+
if (!isArtifactHash(hash)) throw new Error("hasher returned a non-canonical SHA-256 identity");
|
|
134
|
+
this.successful.set(source, { path: source, hash });
|
|
135
|
+
} catch (error) {
|
|
136
|
+
this.successful.set(source, {
|
|
137
|
+
path: source,
|
|
138
|
+
error: error instanceof Error ? error.message : String(error),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
49
141
|
}
|
|
50
142
|
|
|
51
143
|
#record(toolCallId: string, toolName: string, args: unknown): void {
|