@llblab/pi-kit 0.25.0 → 0.27.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/BACKLOG.md +5 -1
- package/CHANGELOG.md +11 -0
- package/README.md +10 -8
- package/node_modules/@llblab/pi-actors/AGENTS.md +2 -0
- package/node_modules/@llblab/pi-actors/CHANGELOG.md +4 -1
- package/node_modules/@llblab/pi-actors/LICENSE +21 -0
- package/node_modules/@llblab/pi-actors/README.md +1 -1
- package/node_modules/@llblab/pi-actors/docs/coordinator-delivery.md +1 -1
- package/node_modules/@llblab/pi-actors/package.json +4 -3
- package/node_modules/@llblab/pi-claude-usage/AGENTS.md +6 -3
- package/node_modules/@llblab/pi-claude-usage/BACKLOG.md +2 -1
- package/node_modules/@llblab/pi-claude-usage/CHANGELOG.md +8 -0
- package/node_modules/@llblab/pi-claude-usage/README.md +48 -3
- package/node_modules/@llblab/pi-claude-usage/index.ts +8 -1159
- package/node_modules/@llblab/pi-claude-usage/lib/extension.ts +30 -0
- package/node_modules/@llblab/pi-claude-usage/lib/fast.ts +24 -0
- package/node_modules/@llblab/pi-claude-usage/lib/query.ts +146 -0
- package/node_modules/@llblab/pi-claude-usage/lib/status-format.ts +297 -0
- package/node_modules/@llblab/pi-claude-usage/lib/status.ts +366 -0
- package/node_modules/@llblab/pi-claude-usage/lib/telegram.ts +44 -0
- package/node_modules/@llblab/pi-claude-usage/lib/usage-store.ts +221 -0
- package/node_modules/@llblab/pi-claude-usage/lib/usage.ts +128 -0
- package/node_modules/@llblab/pi-claude-usage/package.json +9 -5
- package/node_modules/@llblab/pi-clean-room/AGENTS.md +1 -0
- package/node_modules/@llblab/pi-clean-room/CHANGELOG.md +5 -0
- package/node_modules/@llblab/pi-clean-room/LICENSE +21 -0
- package/node_modules/@llblab/pi-clean-room/README.md +1 -1
- package/node_modules/@llblab/pi-clean-room/package.json +3 -2
- package/node_modules/@llblab/pi-codex-usage/AGENTS.md +9 -6
- package/node_modules/@llblab/pi-codex-usage/BACKLOG.md +2 -1
- package/node_modules/@llblab/pi-codex-usage/CHANGELOG.md +17 -0
- package/node_modules/@llblab/pi-codex-usage/README.md +75 -17
- package/node_modules/@llblab/pi-codex-usage/index.ts +8 -1602
- package/node_modules/@llblab/pi-codex-usage/lib/extension.ts +25 -0
- package/node_modules/@llblab/pi-codex-usage/lib/fast.ts +23 -0
- package/node_modules/@llblab/pi-codex-usage/lib/query.ts +368 -0
- package/node_modules/@llblab/pi-codex-usage/lib/status-format.ts +347 -0
- package/node_modules/@llblab/pi-codex-usage/lib/status.ts +435 -0
- package/node_modules/@llblab/pi-codex-usage/lib/telegram.ts +45 -0
- package/node_modules/@llblab/pi-codex-usage/lib/usage-store.ts +229 -0
- package/node_modules/@llblab/pi-codex-usage/lib/usage.ts +425 -0
- package/node_modules/@llblab/pi-codex-usage/package.json +11 -6
- package/node_modules/@llblab/pi-command-fast/AGENTS.md +7 -0
- package/node_modules/@llblab/pi-command-fast/BACKLOG.md +9 -0
- package/node_modules/@llblab/pi-command-fast/CHANGELOG.md +7 -0
- package/node_modules/@llblab/pi-command-fast/LICENSE +21 -0
- package/node_modules/@llblab/pi-command-fast/README.md +42 -0
- package/node_modules/@llblab/pi-command-fast/dist/command.d.ts +8 -0
- package/node_modules/@llblab/pi-command-fast/dist/command.js +52 -0
- package/node_modules/@llblab/pi-command-fast/dist/index.d.ts +3 -0
- package/node_modules/@llblab/pi-command-fast/dist/index.js +3 -0
- package/node_modules/@llblab/pi-command-fast/dist/models-json.d.ts +10 -0
- package/node_modules/@llblab/pi-command-fast/dist/models-json.js +81 -0
- package/node_modules/@llblab/pi-command-fast/package.json +49 -0
- package/node_modules/@llblab/pi-grow-loop/AGENTS.md +1 -0
- package/node_modules/@llblab/pi-grow-loop/CHANGELOG.md +4 -1
- package/node_modules/@llblab/pi-grow-loop/LICENSE +21 -0
- package/node_modules/@llblab/pi-grow-loop/README.md +1 -1
- package/node_modules/@llblab/pi-grow-loop/package.json +3 -2
- package/node_modules/@llblab/pi-state-flow/AGENTS.md +7 -6
- package/node_modules/@llblab/pi-state-flow/BACKLOG.md +13 -5
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +16 -1
- package/node_modules/@llblab/pi-state-flow/LICENSE +21 -0
- package/node_modules/@llblab/pi-state-flow/README.md +117 -35
- package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.d.ts +24 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.js +80 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.d.ts +18 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.js +41 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +346 -303
- package/node_modules/@llblab/pi-state-flow/dist/lib/git.d.ts +31 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/git.js +74 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/operation.d.ts +37 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/operation.js +59 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/ownership.d.ts +31 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/ownership.js +117 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +7 -7
- package/node_modules/@llblab/pi-state-flow/dist/lib/query.js +1 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/session.d.ts +10 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/session.js +60 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.d.ts +6 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.js +8 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/status.d.ts +2 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +28 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +6 -1
- package/node_modules/@llblab/pi-state-flow/dist/package.json +10 -9
- package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-guide/SKILL.md +14 -6
- package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-memory/SKILL.md +2 -2
- package/node_modules/@llblab/pi-state-flow/docs/README.md +19 -9
- package/node_modules/@llblab/pi-state-flow/docs/agent-contract-relocation.md +4 -4
- package/node_modules/@llblab/pi-state-flow/docs/architecture.md +646 -89
- package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +116 -37
- package/node_modules/@llblab/pi-state-flow/docs/filesystem-recovery.md +118 -21
- package/node_modules/@llblab/pi-state-flow/docs/fork-contract.md +70 -8
- package/node_modules/@llblab/pi-state-flow/docs/lazy-state.md +88 -14
- package/node_modules/@llblab/pi-state-flow/docs/performance.md +83 -66
- package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +391 -62
- package/node_modules/@llblab/pi-state-flow/docs/usage.md +317 -61
- package/node_modules/@llblab/pi-state-flow/lib/acquisition.ts +85 -1
- package/node_modules/@llblab/pi-state-flow/lib/compaction.ts +49 -2
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +342 -301
- package/node_modules/@llblab/pi-state-flow/lib/git.ts +77 -5
- package/node_modules/@llblab/pi-state-flow/lib/operation.ts +75 -0
- package/node_modules/@llblab/pi-state-flow/lib/ownership.ts +120 -0
- package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +7 -8
- package/node_modules/@llblab/pi-state-flow/lib/query.ts +1 -1
- package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +1 -1
- package/node_modules/@llblab/pi-state-flow/lib/session.ts +52 -1
- package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +8 -7
- package/node_modules/@llblab/pi-state-flow/lib/status.ts +29 -0
- package/node_modules/@llblab/pi-state-flow/lib/transition.ts +5 -1
- package/node_modules/@llblab/pi-state-flow/package.json +10 -9
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-guide/SKILL.md +14 -6
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +2 -2
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +1 -0
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +10 -0
- package/node_modules/@llblab/pi-telegram/README.md +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +4 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +16 -12
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.d.ts +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +7 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +12 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +137 -83
- package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +26 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.d.ts +57 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.js +109 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/model.js +2 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/status.d.ts +3 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/status.js +31 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/sync.js +4 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +17 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.d.ts +2 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.js +48 -12
- package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +6 -5
- package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +2 -0
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +2 -2
- package/node_modules/@llblab/pi-telegram/docs/ui-style.md +4 -0
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +17 -10
- package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +8 -2
- package/node_modules/@llblab/pi-telegram/lib/commands.ts +135 -97
- package/node_modules/@llblab/pi-telegram/lib/extension.ts +25 -0
- package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +140 -4
- package/node_modules/@llblab/pi-telegram/lib/model.ts +2 -4
- package/node_modules/@llblab/pi-telegram/lib/status.ts +30 -1
- package/node_modules/@llblab/pi-telegram/lib/sync.ts +4 -4
- package/node_modules/@llblab/pi-telegram/lib/threads.ts +21 -3
- package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +46 -13
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/node_modules/jsonc-parser/CHANGELOG.md +76 -0
- package/node_modules/jsonc-parser/LICENSE.md +21 -0
- package/node_modules/jsonc-parser/README.md +364 -0
- package/node_modules/jsonc-parser/SECURITY.md +41 -0
- package/node_modules/jsonc-parser/lib/esm/impl/edit.js +185 -0
- package/node_modules/jsonc-parser/lib/esm/impl/format.js +261 -0
- package/node_modules/jsonc-parser/lib/esm/impl/parser.js +659 -0
- package/node_modules/jsonc-parser/lib/esm/impl/scanner.js +443 -0
- package/node_modules/jsonc-parser/lib/esm/impl/string-intern.js +29 -0
- package/node_modules/jsonc-parser/lib/esm/main.d.ts +351 -0
- package/node_modules/jsonc-parser/lib/esm/main.js +178 -0
- package/node_modules/jsonc-parser/lib/umd/impl/edit.js +201 -0
- package/node_modules/jsonc-parser/lib/umd/impl/format.js +275 -0
- package/node_modules/jsonc-parser/lib/umd/impl/parser.js +682 -0
- package/node_modules/jsonc-parser/lib/umd/impl/scanner.js +456 -0
- package/node_modules/jsonc-parser/lib/umd/impl/string-intern.js +42 -0
- package/node_modules/jsonc-parser/lib/umd/main.d.ts +351 -0
- package/node_modules/jsonc-parser/lib/umd/main.js +194 -0
- package/node_modules/jsonc-parser/package.json +37 -0
- package/package.json +8 -8
|
@@ -206,14 +206,15 @@ export function backupCurrentStateFlowFiles(repositoryRoot: string, signal?: Abo
|
|
|
206
206
|
}
|
|
207
207
|
|
|
208
208
|
/** Skip overlapping pushes; the next accepted turn can push the latest HEAD. */
|
|
209
|
-
export function startStateFlowBackupPush(repositoryRoot: string, onFailure: (error: unknown) => void, onSuccess?: () => void): boolean {
|
|
209
|
+
export function startStateFlowBackupPush(repositoryRoot: string, onFailure: (error: unknown) => void, onSuccess?: () => void, signal?: AbortSignal): boolean {
|
|
210
210
|
const root = resolve(repositoryRoot);
|
|
211
|
-
if (activePushes.has(root)) return false;
|
|
212
|
-
const push = pushCurrentStateFlowBackup(root).then(
|
|
211
|
+
if (signal?.aborted || activePushes.has(root)) return false;
|
|
212
|
+
const push = pushCurrentStateFlowBackup(root, signal).then(
|
|
213
213
|
(result) => {
|
|
214
|
-
if (result) { try { onSuccess?.(); } catch { /* Reporting cannot change push acceptance. */ } }
|
|
214
|
+
if (result && !signal?.aborted) { try { onSuccess?.(); } catch { /* Reporting cannot change push acceptance. */ } }
|
|
215
215
|
},
|
|
216
216
|
(error) => {
|
|
217
|
+
if (signal?.aborted) return;
|
|
217
218
|
try { onFailure(error); } catch { /* Reporting cannot revive a failed push. */ }
|
|
218
219
|
},
|
|
219
220
|
).finally(() => { activePushes.delete(root); });
|
|
@@ -228,15 +229,17 @@ export async function awaitInFlightBackupPushes(repositoryRoot: string): Promise
|
|
|
228
229
|
}
|
|
229
230
|
|
|
230
231
|
/** Push the current backup commit to its explicitly configured branch remote without blocking settlement. */
|
|
231
|
-
export function pushCurrentStateFlowBackup(repositoryRoot: string): Promise<{ commit: string; remote: string; ref: string } | undefined> {
|
|
232
|
+
export function pushCurrentStateFlowBackup(repositoryRoot: string, signal?: AbortSignal): Promise<{ commit: string; remote: string; ref: string } | undefined> {
|
|
232
233
|
return new Promise((resolvePush, rejectPush) => {
|
|
233
234
|
let root: string;
|
|
234
235
|
let commit: string | undefined;
|
|
235
236
|
let destination: { remote: string; ref: string } | undefined;
|
|
236
237
|
try {
|
|
238
|
+
signal?.throwIfAborted();
|
|
237
239
|
root = assertRepositoryRoot(repositoryRoot);
|
|
238
240
|
commit = currentHead(root);
|
|
239
241
|
destination = configuredPushDestination(root);
|
|
242
|
+
signal?.throwIfAborted();
|
|
240
243
|
} catch (error) {
|
|
241
244
|
rejectPush(new Error(redactGitDiagnostic(error instanceof Error ? error.message : String(error))));
|
|
242
245
|
return;
|
|
@@ -273,6 +276,7 @@ export function pushCurrentStateFlowBackup(repositoryRoot: string): Promise<{ co
|
|
|
273
276
|
if (settled) return;
|
|
274
277
|
settled = true;
|
|
275
278
|
clearTimeout(timeout);
|
|
279
|
+
signal?.removeEventListener("abort", cancelled);
|
|
276
280
|
if (error) rejectPush(error);
|
|
277
281
|
else resolvePush({ commit: commit!, ...destination! });
|
|
278
282
|
}
|
|
@@ -284,5 +288,73 @@ export function pushCurrentStateFlowBackup(repositoryRoot: string): Promise<{ co
|
|
|
284
288
|
child.once("exit", () => { if (failure) child.stderr?.destroy(); });
|
|
285
289
|
child.once("close", (code, endedBy) => finish(failure ?? (code === 0 ? undefined
|
|
286
290
|
: new Error(`Git backup push failed (${endedBy ?? code}): ${redactGitDiagnostic(stderr.trim()) || "no diagnostic output"}`))));
|
|
291
|
+
function cancelled(): void {
|
|
292
|
+
terminate(signal?.reason instanceof Error ? signal.reason : new Error("Git backup push cancelled"));
|
|
293
|
+
}
|
|
294
|
+
signal?.addEventListener("abort", cancelled, { once: true });
|
|
295
|
+
if (signal?.aborted) cancelled();
|
|
287
296
|
});
|
|
288
297
|
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Settled-turn backup obligation owned by one extension instance: an accepted
|
|
301
|
+
* publication makes a backup due, an accepted turn permits it at settlement,
|
|
302
|
+
* and one lifetime cancels owned captures/pushes without touching accepted state.
|
|
303
|
+
*/
|
|
304
|
+
export class SettledTurnBackup {
|
|
305
|
+
#turnAccepted = false;
|
|
306
|
+
#pending = false;
|
|
307
|
+
#lifetime = new AbortController();
|
|
308
|
+
#pushFailureNotified = false;
|
|
309
|
+
readonly #operations = new Set<Promise<unknown>>();
|
|
310
|
+
|
|
311
|
+
/** An accepted canonical publication makes the next settled-turn backup due. */
|
|
312
|
+
markPublished(): void { this.#pending = true; }
|
|
313
|
+
|
|
314
|
+
/** An accepted turn permits the due backup at its settlement. */
|
|
315
|
+
markTurnAccepted(): void { this.#turnAccepted = true; }
|
|
316
|
+
|
|
317
|
+
/** Forget the current turn's permission without dropping a due backup. */
|
|
318
|
+
clearTurn(): void { this.#turnAccepted = false; }
|
|
319
|
+
|
|
320
|
+
/** Consume the accepted-turn permission; true only when a backup is also due. */
|
|
321
|
+
takeSettledTurn(): boolean {
|
|
322
|
+
if (!this.#turnAccepted) return false;
|
|
323
|
+
this.#turnAccepted = false;
|
|
324
|
+
if (!this.#pending) return false;
|
|
325
|
+
this.#pending = false;
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** Lifetime signal for owned captures and pushes. */
|
|
330
|
+
get lifetime(): AbortSignal { return this.#lifetime.signal; }
|
|
331
|
+
|
|
332
|
+
/** Cancel owned work and the due backup, then open a fresh lifetime. */
|
|
333
|
+
cancel(): void {
|
|
334
|
+
this.#lifetime.abort();
|
|
335
|
+
this.#lifetime = new AbortController();
|
|
336
|
+
this.#pending = false;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** Cancel owned work for shutdown without opening a new lifetime. */
|
|
340
|
+
abort(): void { this.#lifetime.abort(); }
|
|
341
|
+
|
|
342
|
+
/** Track one owned capture until it settles. */
|
|
343
|
+
async track<T>(operation: Promise<T>): Promise<T> {
|
|
344
|
+
this.#operations.add(operation);
|
|
345
|
+
try { return await operation; }
|
|
346
|
+
finally { this.#operations.delete(operation); }
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** Owned captures still in flight. */
|
|
350
|
+
get operations(): Promise<unknown>[] { return [...this.#operations]; }
|
|
351
|
+
|
|
352
|
+
/** True once per failure streak; a successful push resets it. */
|
|
353
|
+
claimPushFailureNotice(): boolean {
|
|
354
|
+
if (this.#pushFailureNotified) return false;
|
|
355
|
+
this.#pushFailureNotified = true;
|
|
356
|
+
return true;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
resetPushFailureNotice(): void { this.#pushFailureNotified = false; }
|
|
360
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Domain: one cancellable lifecycle owner slot shared by Start, inactive-mode persistence,
|
|
2
|
+
// branch restoration, inference preparation and response reconciliation.
|
|
3
|
+
|
|
4
|
+
/** Minimal shape of a cancellable owned lifecycle operation. */
|
|
5
|
+
export interface OwnedOperation {
|
|
6
|
+
readonly controller: AbortController;
|
|
7
|
+
operation?: Promise<unknown>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* At most one current owner per lifecycle concern. A newer claim supersedes by
|
|
12
|
+
* explicit cancellation, and identity-guarded release prevents a late
|
|
13
|
+
* completion from clearing newer work.
|
|
14
|
+
*/
|
|
15
|
+
export class OwnedOperationSlot<E extends OwnedOperation> {
|
|
16
|
+
#current: E | undefined;
|
|
17
|
+
readonly #inflight = new Set<Promise<unknown>>();
|
|
18
|
+
|
|
19
|
+
/** The current owner, if any. */
|
|
20
|
+
get current(): E | undefined { return this.#current; }
|
|
21
|
+
|
|
22
|
+
/** True while `entry` still owns the slot. */
|
|
23
|
+
owns(entry: E): boolean { return this.#current === entry; }
|
|
24
|
+
|
|
25
|
+
/** Install a new owner; callers cancel a previous owner explicitly when supersession requires it. */
|
|
26
|
+
claim(entry: E): E {
|
|
27
|
+
this.#current = entry;
|
|
28
|
+
return entry;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Abort and forget the current owner; returns its operation for optional awaiting. */
|
|
32
|
+
cancel(): E["operation"] | undefined {
|
|
33
|
+
const entry = this.#current;
|
|
34
|
+
entry?.controller.abort();
|
|
35
|
+
this.#current = undefined;
|
|
36
|
+
return entry?.operation;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Forget `entry` only if it still owns the slot. */
|
|
40
|
+
release(entry: E): void {
|
|
41
|
+
if (this.#current === entry) this.#current = undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Attach `operation` to `entry` and remember it until it settles, optionally releasing ownership then. */
|
|
45
|
+
track<P extends NonNullable<E["operation"]>>(entry: E, operation: P, releaseOnSettle = true): P {
|
|
46
|
+
entry.operation = operation;
|
|
47
|
+
this.#inflight.add(operation);
|
|
48
|
+
const settle = () => {
|
|
49
|
+
this.#inflight.delete(operation);
|
|
50
|
+
if (releaseOnSettle) this.release(entry);
|
|
51
|
+
};
|
|
52
|
+
void operation.then(settle, settle);
|
|
53
|
+
return operation;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Tracked operations not yet settled, including superseded ones. */
|
|
57
|
+
get inflight(): Promise<unknown>[] { return [...this.#inflight]; }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** A renewable cancellation lifetime: ending it aborts every holder of the current signal. */
|
|
61
|
+
export class RenewableLifetime {
|
|
62
|
+
#controller = new AbortController();
|
|
63
|
+
|
|
64
|
+
/** Signal of the current lifetime. */
|
|
65
|
+
get signal(): AbortSignal { return this.#controller.signal; }
|
|
66
|
+
|
|
67
|
+
/** Abort current holders and open a fresh lifetime. */
|
|
68
|
+
renew(reason?: unknown): void {
|
|
69
|
+
this.#controller.abort(reason);
|
|
70
|
+
this.#controller = new AbortController();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Abort current holders without opening a new lifetime (shutdown). */
|
|
74
|
+
end(reason?: unknown): void { this.#controller.abort(reason); }
|
|
75
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { isObject, type JsonObject, type JsonValue } from "./json.ts";
|
|
2
|
+
import type { SemanticState, StateScope } from "./state.ts";
|
|
3
|
+
|
|
4
|
+
/** Planes whose object keys an intent may own by structured reference. */
|
|
5
|
+
export type OwnedPlane = "working" | "lazy";
|
|
6
|
+
|
|
7
|
+
/** One same-scope object-key target of an intent ownership reference. */
|
|
8
|
+
export interface OwnedPath {
|
|
9
|
+
plane: OwnedPlane;
|
|
10
|
+
keys: string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const OWNED_PLANES = new Set<string>(["working", "lazy"]);
|
|
14
|
+
// Same key grammar as read_state value paths; array selectors never parse.
|
|
15
|
+
const OBJECT_KEY = /^[A-Za-z_$][A-Za-z0-9_$-]*$/;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Parse one structured `$ref` target as an ownership path of `scope`.
|
|
19
|
+
* Cross-scope, unscoped, `effective`, historical, plane-root, array-element
|
|
20
|
+
* and non-`working`/`lazy` targets are not ownership targets.
|
|
21
|
+
*/
|
|
22
|
+
export function parseOwnedPath(scope: StateScope, ref: string): OwnedPath | undefined {
|
|
23
|
+
const parts = ref.split(".");
|
|
24
|
+
if (parts.length < 3 || parts[0] !== scope || !OWNED_PLANES.has(parts[1]!)) return undefined;
|
|
25
|
+
const keys = parts.slice(2);
|
|
26
|
+
if (!keys.every((key) => OBJECT_KEY.test(key))) return undefined;
|
|
27
|
+
return { plane: parts[1] as OwnedPlane, keys };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function pathId(path: OwnedPath): string {
|
|
31
|
+
return JSON.stringify([path.plane, ...path.keys]);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Extract unique ownership targets from structured references anywhere inside one intent value. */
|
|
35
|
+
export function extractOwnedPaths(scope: StateScope, value: JsonValue): OwnedPath[] {
|
|
36
|
+
const found = new Map<string, OwnedPath>();
|
|
37
|
+
const visit = (node: JsonValue): void => {
|
|
38
|
+
if (Array.isArray(node)) {
|
|
39
|
+
for (const item of node) visit(item);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (!isObject(node)) return;
|
|
43
|
+
if (typeof node.$ref === "string") {
|
|
44
|
+
const path = parseOwnedPath(scope, node.$ref);
|
|
45
|
+
if (path) found.set(pathId(path), path);
|
|
46
|
+
}
|
|
47
|
+
for (const key of Object.keys(node)) if (key !== "$ref" || typeof node[key] !== "string") visit(node[key]!);
|
|
48
|
+
};
|
|
49
|
+
visit(value);
|
|
50
|
+
return [...found.values()];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function related(left: OwnedPath, right: OwnedPath): boolean {
|
|
54
|
+
if (left.plane !== right.plane) return false;
|
|
55
|
+
const length = Math.min(left.keys.length, right.keys.length);
|
|
56
|
+
for (let index = 0; index < length; index++) if (left.keys[index] !== right.keys[index]) return false;
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function exists(state: SemanticState, path: OwnedPath): boolean {
|
|
61
|
+
let node: JsonValue | undefined = state[path.plane];
|
|
62
|
+
for (const key of path.keys) {
|
|
63
|
+
if (!isObject(node) || !Object.hasOwn(node, key)) return false;
|
|
64
|
+
node = node[key];
|
|
65
|
+
}
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Compute the same-scope ownership cascade for intent keys removed between
|
|
71
|
+
* `before` and `after` (the state after authored operations). A target is
|
|
72
|
+
* kept while any remaining intent references it, an ancestor or a descendant;
|
|
73
|
+
* missing targets are skipped. The result is minimal (no target below
|
|
74
|
+
* another) and deterministically ordered.
|
|
75
|
+
*/
|
|
76
|
+
export function computeIntentCascade(scope: StateScope, before: SemanticState, after: SemanticState): OwnedPath[] {
|
|
77
|
+
const previous = isObject(before.intents) ? before.intents : {};
|
|
78
|
+
const remaining = isObject(after.intents) ? after.intents : {};
|
|
79
|
+
const deleted = Object.keys(previous).filter((key) => !Object.hasOwn(remaining, key));
|
|
80
|
+
if (deleted.length === 0) return [];
|
|
81
|
+
const candidates = new Map<string, OwnedPath>();
|
|
82
|
+
for (const key of deleted) for (const path of extractOwnedPaths(scope, previous[key]!)) candidates.set(pathId(path), path);
|
|
83
|
+
if (candidates.size === 0) return [];
|
|
84
|
+
const retained = Object.values(remaining).flatMap((value) => extractOwnedPaths(scope, value));
|
|
85
|
+
const owned = [...candidates.values()].filter((path) =>
|
|
86
|
+
!retained.some((other) => related(path, other)) && exists(after, path));
|
|
87
|
+
return owned
|
|
88
|
+
.filter((path) => !owned.some((other) => other.keys.length < path.keys.length && related(path, other)))
|
|
89
|
+
.sort((left, right) => pathId(left).localeCompare(pathId(right)));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Express cascade targets as explicit nested object-key deletions. */
|
|
93
|
+
export function cascadeDeletionPatch(paths: readonly OwnedPath[]): JsonObject {
|
|
94
|
+
const patch: JsonObject = {};
|
|
95
|
+
const define = (target: JsonObject, key: string, value: JsonValue) =>
|
|
96
|
+
Object.defineProperty(target, key, { value, enumerable: true, configurable: true, writable: true });
|
|
97
|
+
for (const path of paths) {
|
|
98
|
+
if (!Object.hasOwn(patch, path.plane)) define(patch, path.plane, {});
|
|
99
|
+
let node = patch[path.plane] as JsonObject;
|
|
100
|
+
for (const key of path.keys.slice(0, -1)) {
|
|
101
|
+
if (!Object.hasOwn(node, key)) define(node, key, {});
|
|
102
|
+
node = node[key] as JsonObject;
|
|
103
|
+
}
|
|
104
|
+
define(node, path.keys.at(-1)!, null);
|
|
105
|
+
}
|
|
106
|
+
return patch;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Render an ownership target as its scoped `read_state` path. */
|
|
110
|
+
export function formatOwnedPath(scope: StateScope, path: OwnedPath): string {
|
|
111
|
+
return [scope, path.plane, ...path.keys].join(".");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Top-level `working`/`lazy` keys that at least one current intent of `scope` owns. */
|
|
115
|
+
export function ownedTopLevelKeys(scope: StateScope, state: SemanticState): Record<OwnedPlane, Set<string>> {
|
|
116
|
+
const owned: Record<OwnedPlane, Set<string>> = { working: new Set(), lazy: new Set() };
|
|
117
|
+
const intents = isObject(state.intents) ? state.intents : {};
|
|
118
|
+
for (const value of Object.values(intents)) for (const path of extractOwnedPaths(scope, value)) owned[path.plane].add(path.keys[0]!);
|
|
119
|
+
return owned;
|
|
120
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
2
|
-
import { isObject } from "./json.ts";
|
|
3
2
|
|
|
4
3
|
export type { StateDocument } from "./state.ts";
|
|
5
4
|
|
|
@@ -89,24 +88,24 @@ export function stateFlowProtocol(bootstrap: boolean): string {
|
|
|
89
88
|
return `State Flow is enabled. It owns durable memory.
|
|
90
89
|
|
|
91
90
|
${bootstrapProtocol}STATE:
|
|
92
|
-
- intents: chosen
|
|
93
|
-
- contract: durable requirements, decisions, rejections, interfaces
|
|
94
|
-
- working: facts, validation, failures, domain state
|
|
91
|
+
- intents: queue of chosen actions; work from them; remove when fulfilled, abandoned, superseded, or impossible.
|
|
92
|
+
- contract: durable requirements, decisions, rejections, interfaces.
|
|
93
|
+
- working: temporary context of intents: facts, validation, failures, domain state.
|
|
95
94
|
- artifacts: source-path routing metadata; descriptions do not imply body acquisition.
|
|
96
|
-
- response: previous answer; runtime stores the exact accepted answer at turn_end (empty="").
|
|
95
|
+
- response: previous answer; runtime stores the exact accepted answer at turn_end (empty="").
|
|
97
96
|
- lazy: retrieve explicitly.
|
|
98
97
|
|
|
99
98
|
SCOPES: Use the narrowest scope: session=branch/run continuation by default; cwd=reusable project truth; global=established cross-project/user/environment knowledge.
|
|
100
99
|
|
|
101
|
-
READ: Use read_state for concrete scope/retained-history gaps. lazy_navigation lists bounded effective lazy keys, not bodies.
|
|
100
|
+
READ: Use read_state for concrete scope/retained-history gaps. lazy_navigation lists bounded effective lazy keys, not bodies. ${TASK_DRIVEN_HISTORY}
|
|
102
101
|
|
|
103
|
-
WRITE: patch_state is the sole model-authored semantic mutation mechanism; all supplied scopes are validated and durably accepted as one atomic transition.
|
|
102
|
+
WRITE: patch_state is the sole model-authored semantic mutation mechanism; all supplied scopes are validated and durably accepted as one atomic transition. Global/CWD use current canonical values after cancelable lock waiting. Correct repeats succeed without new revisions. ${PATCH_RESULT_PROTOCOL}
|
|
104
103
|
|
|
105
104
|
PATCH: Use global/cwd/session object patches for material updates, not acknowledgments. Omit empty scopes. artifacts/contract/working/intents are objects; lazy is ordinary JSON. Omitted fields persist. Never patch runtime config/meta/response. Objects merge recursively; arrays/primitives replace. An object containing only canonical "[N]" keys recursively patches array elements. Indexed deletion is forbidden; nested object null deletes; materialized null is forbidden.
|
|
106
105
|
|
|
107
106
|
MEMORY: Treat every patch as reconciliation rather than append-only notes: merge superseded fragments, remove obsolete progress. Preserve commitments, open questions, consequential results and exact continuation; distinguish requirements, decisions, observations, conclusions and hypotheses. Exclude secrets, raw history, transient progress, speculation and unsupported claims; retain decision-relevant uncertainty. Curate touched state; cleanup and scope reviews require an explicit user request. Proven moves use targeted read_state and one atomic multi-scope patch, then verify both owners. External transfers need verified acceptance before deletion. Never invent memory changes.
|
|
108
107
|
|
|
109
|
-
REFS: State refs use {"$ref":"cwd.lazy.plan"} or \`$cwd.lazy.plan\` in text. Resolve only when needed; infer no authority, hydration, execution, or completion. Hint paths are current reference owners, not relocated targets or proof of staleness. Fix proven stale refs only as needed; never scan refs or history offsets.
|
|
108
|
+
REFS: State refs use {"$ref":"cwd.lazy.plan"} or \`$cwd.lazy.plan\` in text. In an intent, $ref owns a same-scope working/lazy key; deleting the intent deletes keys no other intent refs (supersede in one patch). Text refs only use. Before deleting, save survivors unowned: results to working/contract/broader scope, abandonment reasons to contract. Unowned entries are fine. Resolve only when needed; infer no authority, hydration, execution, or completion. Hint paths are current reference owners, not relocated targets or proof of staleness. Fix proven stale refs only as needed; never scan refs or history offsets.
|
|
110
109
|
|
|
111
110
|
ACQUISITION: Read only for a concrete gap, exact source/edit, invalidation, contradiction/failure, or explicit request; changed source fingerprints require rereading.
|
|
112
111
|
ARTIFACTS: Compile acquired invalidated artifacts at artifacts[exact path] in the reported scope (global/cwd/session), with a description; never relocate or invent global copies. Runtime owns all artifact/Skill provenance.
|
|
@@ -96,7 +96,7 @@ function parseValuePath(path: string): { root: string; selectors: ValueSelector[
|
|
|
96
96
|
rest = rest.slice(index[0].length);
|
|
97
97
|
continue;
|
|
98
98
|
}
|
|
99
|
-
throw new Error("Invalid State Flow read path selector");
|
|
99
|
+
throw new Error("Invalid State Flow read path selector; object keys must match [A-Za-z_$][A-Za-z0-9_$-]* (ASCII), array selectors use [N] or [start..end]");
|
|
100
100
|
}
|
|
101
101
|
return { root, selectors };
|
|
102
102
|
}
|
|
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { lstatSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { classifyScopeStream, hasCwdMaterialization, parseScopeProvenance, parseScopeStream, sessionRuntimePaths, temporalScopePaths, type SessionAddress } from "./durable.ts";
|
|
5
|
-
import {
|
|
5
|
+
import { pruneArtifactProvenance, type ArtifactProvenance, type ArtifactProvenanceRegistry } from "./artifact.ts";
|
|
6
6
|
import { assertTemporalFileBase, captureTemporalFileBase, initializeFileStore, publishTemporalStateToFiles, withStorageTransaction, type StorageTransaction, type TemporalFileBase } from "./storage.ts";
|
|
7
7
|
import { DEFAULT_HISTORY_LIMIT, MAX_HISTORY_LIMIT, type AcceptedTransition, type RecentTransitionWindow } from "./history.ts";
|
|
8
8
|
import { hashJson, sameJson } from "./json.ts";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { parseRetainedPiCheckpoint, type InactiveMode } from "./snapshot.ts";
|
|
1
|
+
import { parseRetainedPiCheckpoint, readCheckpointMode, type InactiveMode, type StateFlowMode } from "./snapshot.ts";
|
|
2
2
|
|
|
3
3
|
export const SNAPSHOT_ENTRY_TYPE = "state-flow-snapshot";
|
|
4
4
|
|
|
@@ -22,6 +22,8 @@ export interface PassiveStopBoundary {
|
|
|
22
22
|
persistenceError?: string;
|
|
23
23
|
/** The inactive mode selected by that failed change; legacy markers omit it. */
|
|
24
24
|
mode?: InactiveMode;
|
|
25
|
+
/** Native-only Off choice; canonical runtime mode is deliberately not updated. */
|
|
26
|
+
memoryDeferred?: true;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
export interface SnapshotDiscovery {
|
|
@@ -44,6 +46,55 @@ export function discoverSnapshotData(branch: readonly BranchEntry[]): SnapshotDi
|
|
|
44
46
|
return { candidates, errors };
|
|
45
47
|
}
|
|
46
48
|
|
|
49
|
+
/** Read native mode policy only; semantic checkpoint validity remains the recovery owner's concern. */
|
|
50
|
+
export function findBranchPolicy(
|
|
51
|
+
branch: readonly BranchEntry[], sessionId: string | undefined, stopEntryType: string | undefined, inactiveMode: InactiveMode,
|
|
52
|
+
): { mode: StateFlowMode; persistenceError?: string } | undefined {
|
|
53
|
+
let stopReset = false;
|
|
54
|
+
for (let index = branch.length - 1; index >= 0; index--) {
|
|
55
|
+
try {
|
|
56
|
+
const entry = branch[index];
|
|
57
|
+
if (entry?.type !== "custom") continue;
|
|
58
|
+
if (entry.customType === SNAPSHOT_ENTRY_TYPE) {
|
|
59
|
+
const mode = readCheckpointMode(entry.data, inactiveMode);
|
|
60
|
+
if (mode !== undefined) return { mode };
|
|
61
|
+
const data = entry.data as { disabled?: unknown; mode?: unknown; enabled?: unknown } | undefined;
|
|
62
|
+
if (data?.disabled === true && !Object.hasOwn(data, "mode") && !Object.hasOwn(data, "enabled")) return { mode: inactiveMode };
|
|
63
|
+
} else if (stopEntryType !== undefined && entry.customType === stopEntryType) {
|
|
64
|
+
const data = entry.data as PassiveStopBoundary & { owner?: unknown; reset?: unknown } | undefined;
|
|
65
|
+
if (!data || stopReset || (sessionId === undefined ? typeof data.owner !== "string" || !data.owner.trim() : data.owner !== sessionId)) continue;
|
|
66
|
+
if (data.reset === true) { stopReset = true; continue; }
|
|
67
|
+
if (Number.isSafeInteger(data.at) && data.at >= 0 && data.memoryDeferred === true && data.mode === "off") {
|
|
68
|
+
return { mode: "off", ...(typeof data.persistenceError === "string" && data.persistenceError.trim() ? { persistenceError: data.persistenceError } : {}) };
|
|
69
|
+
}
|
|
70
|
+
if (Number.isSafeInteger(data.at) && data.at >= 0 && typeof data.persistenceError === "string" && data.persistenceError.trim()) {
|
|
71
|
+
return { mode: data.mode === "off" || data.mode === "passive" ? data.mode : inactiveMode, persistenceError: data.persistenceError };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
// Malformed native policy cannot prevent another explicit retained choice from being read.
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Native policy bookkeeping retains an unacquired fork across extension reloads. */
|
|
82
|
+
export function hasPendingFork(branch: readonly BranchEntry[], sessionId: string, entryType: string): boolean {
|
|
83
|
+
for (let index = branch.length - 1; index >= 0; index--) {
|
|
84
|
+
try {
|
|
85
|
+
const entry = branch[index];
|
|
86
|
+
if (entry?.type !== "custom" || entry.customType !== entryType) continue;
|
|
87
|
+
const data = entry.data as { owner?: unknown; reset?: unknown; forkPending?: unknown } | undefined;
|
|
88
|
+
if (data?.owner !== sessionId) continue;
|
|
89
|
+
if (data.reset === true) return false;
|
|
90
|
+
if (data.forkPending === true) return true;
|
|
91
|
+
} catch {
|
|
92
|
+
// Unrelated native entries grant no fork authority.
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
47
98
|
export function snapshotDataNewestFirst(branch: readonly BranchEntry[]): unknown[] {
|
|
48
99
|
return discoverSnapshotData(branch).candidates;
|
|
49
100
|
}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import { parseArtifactProvenanceRegistry, type ArtifactProvenanceRegistry } from "./artifact.ts";
|
|
3
3
|
import { MAX_HISTORY_LIMIT } from "./history.ts";
|
|
4
|
-
import {
|
|
4
|
+
import { canonicalJson, isJsonValue, isObject, type JsonObject } from "./json.ts";
|
|
5
5
|
import { validateTemporalLineage, type TransitionBoundary } from "./temporal.ts";
|
|
6
|
-
import { isMaterializedState, type MaterializedState } from "./state.ts";
|
|
7
6
|
const MAX_RESTORED_STEP = Number.MAX_SAFE_INTEGER - 1;
|
|
8
7
|
const MAX_LEGACY_VALIDATION_ATTEMPT = 7;
|
|
9
8
|
|
|
@@ -26,10 +25,12 @@ export interface SnapshotConfig {
|
|
|
26
25
|
}
|
|
27
26
|
|
|
28
27
|
/**
|
|
29
|
-
* Read
|
|
30
|
-
*
|
|
28
|
+
* Read only mode policy, without validating or accessing semantic checkpoint metadata.
|
|
29
|
+
* Missing/invalid policy stays undecided; callers must not treat this as restoration proof.
|
|
30
|
+
* Legacy `enabled:false` follows the caller's inactive policy.
|
|
31
31
|
*/
|
|
32
|
-
function
|
|
32
|
+
export function readCheckpointMode(value: unknown, inactiveMode: InactiveMode = "passive"): StateFlowMode | undefined {
|
|
33
|
+
if (!isObject(value)) return undefined;
|
|
33
34
|
if (Object.hasOwn(value, "mode")) return Object.hasOwn(value, "enabled") || !isStateFlowMode(value.mode) ? undefined : value.mode;
|
|
34
35
|
return typeof value.enabled === "boolean" ? value.enabled ? "active" : inactiveMode : undefined;
|
|
35
36
|
}
|
|
@@ -134,7 +135,7 @@ export function parseSessionRuntime(
|
|
|
134
135
|
let runtime: unknown;
|
|
135
136
|
try {
|
|
136
137
|
const settings: unknown = JSON.parse(config);
|
|
137
|
-
const mode = isObject(settings) && Object.keys(settings).length === 1 ?
|
|
138
|
+
const mode = isObject(settings) && Object.keys(settings).length === 1 ? readCheckpointMode(settings, "passive") : undefined;
|
|
138
139
|
if (mode === undefined) throw new Error("Invalid State Flow runtime configuration or counters");
|
|
139
140
|
runtime = { config: { mode }, meta: JSON.parse(runtimeSource) };
|
|
140
141
|
} catch (error) {
|
|
@@ -230,7 +231,7 @@ export function parseRetainedPiCheckpoint(value: unknown, inactiveMode: Inactive
|
|
|
230
231
|
if (keys.length === 1 && value.disabled === true) return { mode: inactiveMode };
|
|
231
232
|
if (keys.length === 1 && (value.mode === "passive" || value.mode === "off")) return { mode: value.mode };
|
|
232
233
|
const allowed = new Set(["boundary", "mode", "enabled", "step", "bootstrap", "specification"]);
|
|
233
|
-
const mode =
|
|
234
|
+
const mode = readCheckpointMode(value, inactiveMode);
|
|
234
235
|
if (keys.some((key) => !allowed.has(key))
|
|
235
236
|
|| typeof value.boundary !== "string" || value.boundary.trim().length === 0
|
|
236
237
|
|| mode === undefined
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ArtifactInvalidationReason } from "./artifact.ts";
|
|
2
2
|
import type { RecentTransitionWindow } from "./history.ts";
|
|
3
|
+
import { ownedTopLevelKeys } from "./ownership.ts";
|
|
3
4
|
import { conciseDiagnostic } from "./protocol.ts";
|
|
4
5
|
import type { Snapshot } from "./snapshot.ts";
|
|
5
6
|
import { overlayStates, projectSemanticState, type SemanticState, type ScopedStates, type StateScope } from "./state.ts";
|
|
@@ -46,6 +47,33 @@ function countArtifacts(states: ScopedStates, scope: StateScope): number {
|
|
|
46
47
|
return Object.keys(states[scope].artifacts).length;
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
const STATUS_PLANES = ["intents", "contract", "working", "artifacts", "response", "lazy"] as const;
|
|
51
|
+
const encoder = new TextEncoder();
|
|
52
|
+
|
|
53
|
+
function isPresentPlane(value: unknown): boolean {
|
|
54
|
+
if (value === undefined || value === "") return false;
|
|
55
|
+
return typeof value !== "object" || value === null || Object.keys(value).length > 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Operator-only footprint: serialized plane sizes and intent-owned share of top-level working/lazy entries. */
|
|
59
|
+
export function scopeMemoryLines(states: ScopedStates): string[] {
|
|
60
|
+
const lines: string[] = [];
|
|
61
|
+
for (const scope of ["global", "cwd", "session"] as const) {
|
|
62
|
+
const state = states[scope] as SemanticState;
|
|
63
|
+
const sizes = STATUS_PLANES.filter((plane) => isPresentPlane(state[plane]))
|
|
64
|
+
.map((plane) => `${plane} ${encoder.encode(JSON.stringify(state[plane])).length} B`);
|
|
65
|
+
if (sizes.length === 0) continue;
|
|
66
|
+
const owned = ownedTopLevelKeys(scope, state);
|
|
67
|
+
const shares = (["working", "lazy"] as const).flatMap((plane) => {
|
|
68
|
+
const value = state[plane];
|
|
69
|
+
const keys = value !== null && typeof value === "object" && !Array.isArray(value) ? Object.keys(value) : [];
|
|
70
|
+
return keys.length === 0 ? [] : [`${plane} ${keys.filter((key) => owned[plane].has(key)).length}/${keys.length}`];
|
|
71
|
+
});
|
|
72
|
+
lines.push(`- ${scope}: ${sizes.join(", ")}${shares.length ? `; intent-owned ${shares.join(", ")}` : ""}`);
|
|
73
|
+
}
|
|
74
|
+
return lines.length ? ["Scope memory:", ...lines] : [];
|
|
75
|
+
}
|
|
76
|
+
|
|
49
77
|
export function detailedStatus(snapshot: Snapshot, diagnostics: StatusDiagnostics): string {
|
|
50
78
|
const available = diagnostics.temporal !== undefined && diagnostics.durableStateError === undefined;
|
|
51
79
|
const materialized = !available ? undefined : diagnostics.effectiveState ?? projectSemanticState(overlayStates(
|
|
@@ -79,6 +107,7 @@ export function detailedStatus(snapshot: Snapshot, diagnostics: StatusDiagnostic
|
|
|
79
107
|
...(diagnostics.publicationError === undefined ? [] : [`Memory writes paused after mode change: ${conciseDiagnostic(diagnostics.publicationError)}`]),
|
|
80
108
|
...(hasArtifacts ? [`Artifacts: global ${artifacts("global")}; CWD ${artifacts("cwd")}; session ${artifacts("session")}; pending invalidations ${invalidated}`] : []),
|
|
81
109
|
...invalidationLines,
|
|
110
|
+
...(available ? scopeMemoryLines(diagnostics.scopeStates) : []),
|
|
82
111
|
...(stateJson === undefined
|
|
83
112
|
? ["Effective memory: unavailable"]
|
|
84
113
|
: ["Effective memory:", "", stateJson]),
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
import type { SuccessfulArtifactRead } from "./acquisition.ts";
|
|
11
11
|
import { createAcceptedTransition, type AcceptedTransition } from "./history.ts";
|
|
12
12
|
import { applyPatch, containsNull, hashJson, isObject, validatePatch, type JsonObject } from "./json.ts";
|
|
13
|
+
import { cascadeDeletionPatch, computeIntentCascade } from "./ownership.ts";
|
|
13
14
|
import { hasCompiledSkillArtifact, SKILL_ARTIFACT_COMPILER, type SuccessfulSkillRead } from "./skills.ts";
|
|
14
15
|
import type { Snapshot } from "./snapshot.ts";
|
|
15
16
|
import { emptyState } from "./state.ts";
|
|
@@ -188,7 +189,10 @@ function stageScopedSemanticTransition(
|
|
|
188
189
|
for (const scope of SCOPES) {
|
|
189
190
|
const authored = patches.get(scope) ?? {};
|
|
190
191
|
const patch = { ...authored, ...(scope === "session" && acceptedResponse !== undefined ? { response: acceptedResponse } : {}) };
|
|
191
|
-
|
|
192
|
+
let materialized = applyPatch({ ...emptyState(), ...currentStates[scope] }, patch) as StateDocument;
|
|
193
|
+
// Authored operations first, then the same-scope intent ownership cascade.
|
|
194
|
+
const cascade = computeIntentCascade(scope, currentStates[scope], materialized);
|
|
195
|
+
if (cascade.length > 0) materialized = applyPatch(materialized, cascadeDeletionPatch(cascade)) as StateDocument;
|
|
192
196
|
compileReadArtifacts(
|
|
193
197
|
materialized,
|
|
194
198
|
{ artifacts: authored.artifacts ?? {} },
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llblab/pi-state-flow",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
|
+
"license": "MIT",
|
|
4
5
|
"private": false,
|
|
5
6
|
"description": "Incremental scoped state/context/memory compiler for Pi, inspired by SKILL.state",
|
|
6
7
|
"keywords": [
|
|
@@ -66,16 +67,16 @@
|
|
|
66
67
|
"node": ">=22.19.0"
|
|
67
68
|
},
|
|
68
69
|
"peerDependencies": {
|
|
69
|
-
"@earendil-works/pi-agent-core": ">=0.
|
|
70
|
-
"@earendil-works/pi-ai": ">=0.
|
|
71
|
-
"@earendil-works/pi-coding-agent": ">=0.
|
|
72
|
-
"@earendil-works/pi-tui": ">=0.
|
|
70
|
+
"@earendil-works/pi-agent-core": ">=1.0.0",
|
|
71
|
+
"@earendil-works/pi-ai": ">=1.0.0",
|
|
72
|
+
"@earendil-works/pi-coding-agent": ">=1.0.0",
|
|
73
|
+
"@earendil-works/pi-tui": ">=1.0.0"
|
|
73
74
|
},
|
|
74
75
|
"devDependencies": {
|
|
75
|
-
"@earendil-works/pi-agent-core": "0.
|
|
76
|
-
"@earendil-works/pi-ai": "0.
|
|
77
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
78
|
-
"@earendil-works/pi-tui": "0.
|
|
76
|
+
"@earendil-works/pi-agent-core": "1.0.0",
|
|
77
|
+
"@earendil-works/pi-ai": "1.0.0",
|
|
78
|
+
"@earendil-works/pi-coding-agent": "1.0.0",
|
|
79
|
+
"@earendil-works/pi-tui": "1.0.0",
|
|
79
80
|
"@types/node": "^26.4.0",
|
|
80
81
|
"typescript": "^7.0.2"
|
|
81
82
|
}
|