@llblab/pi-kit 0.26.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 +2 -2
- package/CHANGELOG.md +5 -0
- package/README.md +4 -4
- package/node_modules/@llblab/pi-state-flow/AGENTS.md +1 -1
- package/node_modules/@llblab/pi-state-flow/BACKLOG.md +12 -5
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +9 -0
- package/node_modules/@llblab/pi-state-flow/README.md +116 -34
- 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 +17 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.js +40 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +149 -298
- package/node_modules/@llblab/pi-state-flow/dist/lib/git.d.ts +29 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/git.js +58 -0
- 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/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 +1 -1
- 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 +644 -91
- 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 +68 -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 -62
- package/node_modules/@llblab/pi-state-flow/lib/acquisition.ts +85 -1
- package/node_modules/@llblab/pi-state-flow/lib/compaction.ts +47 -0
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +161 -295
- package/node_modules/@llblab/pi-state-flow/lib/git.ts +63 -0
- 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/snapshot.ts +1 -2
- 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 +1 -1
- 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/package.json +3 -3
|
@@ -295,3 +295,66 @@ export function pushCurrentStateFlowBackup(repositoryRoot: string, signal?: Abor
|
|
|
295
295
|
if (signal?.aborted) cancelled();
|
|
296
296
|
});
|
|
297
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,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
|
|
|
@@ -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 ?? {} },
|
|
@@ -21,9 +21,9 @@ Operator commands: `/state-flow-status` inspects; `/state-flow-active` selects s
|
|
|
21
21
|
|
|
22
22
|
| Field | Purpose |
|
|
23
23
|
| --- | --- |
|
|
24
|
-
| `intents` |
|
|
25
|
-
| `contract` | Requirements, decisions, constraints, interfaces |
|
|
26
|
-
| `working` |
|
|
24
|
+
| `intents` | Queue of chosen actions, not possibilities; may own `working`/`lazy` keys |
|
|
25
|
+
| `contract` | Requirements, decisions, rejections, constraints, interfaces |
|
|
26
|
+
| `working` | Temporary context of intents: observations, results, open questions |
|
|
27
27
|
| `artifacts` | Exact source paths, descriptions, compilations |
|
|
28
28
|
| `response` | Previous completed answer; runtime-owned |
|
|
29
29
|
| `lazy` | Durable detail omitted from ordinary context |
|
|
@@ -46,7 +46,7 @@ Example arguments:
|
|
|
46
46
|
{"paths":["cwd.working","session.working"]}
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
Unscoped paths use effective state. `cwd[1].working` reads the preceding causal boundary. Materialized-history and scope patch-history paths such as `cwd.patches[1]` share the configured `historyLimit` bound (default 7) and require actually retained history. Lowering the limit folds excess tails without erasing current state; increasing it does not reconstruct discarded history. Array ranges such as `cwd.lazy.checks[0..3]` exclude the endpoint and require existing elements. Missing paths are unavailable; inspect parent keys only when needed for the task. Missing history is not empty history. Read `lazy` explicitly. Treat structured `$ref` values and `$`-prefixed `read_state` paths inside ordinary strings, such as `$effective.lazy.memory[7]`, as semantic-state references. Other resources retain their native locators. Resolve any reference through the appropriate read/tool only when needed. Neither form proves authority or existence, hydrates, or executes anything. Never scan or resolve references merely to test them. A missing single value path with exact durable sources returns `{value:null, hint:[{type:"dangling-reference", message, paths}]}`. Treat `hint` as top-level diagnostic metadata, never as the requested state: its message is descriptive and conditional; its paths are runtime-verified current reference owners, not verified new locations of the target. The hint proves provenance rather than staleness and is absent when no current durable source matches; keys, patch, and batch reads keep all-or-error behavior. Only then inspect ownership as needed and patch a proven stale owning value while preserving its surrounding meaning. Effective absence, inaccessible external resources, and transient failures are not proof.
|
|
49
|
+
Unscoped paths use effective state. `cwd[1].working` reads the preceding causal boundary. Materialized-history and scope patch-history paths such as `cwd.patches[1]` share the configured `historyLimit` bound (default 7) and require actually retained history. Lowering the limit folds excess tails without erasing current state; increasing it does not reconstruct discarded history. Array ranges such as `cwd.lazy.checks[0..3]` exclude the endpoint and require existing elements. Missing paths are unavailable; inspect parent keys only when needed for the task. Missing history is not empty history. Read `lazy` explicitly. Treat structured `$ref` values and `$`-prefixed `read_state` paths inside ordinary strings, such as `$effective.lazy.memory[7]`, as semantic-state references. Other resources retain their native locators. Resolve any reference through the appropriate read/tool only when needed. Neither form proves authority or existence, hydrates, or executes anything; only intent ownership (see Write) has a deletion consequence. Never scan or resolve references merely to test them. A missing single value path with exact durable sources returns `{value:null, hint:[{type:"dangling-reference", message, paths}]}`. Treat `hint` as top-level diagnostic metadata, never as the requested state: its message is descriptive and conditional; its paths are runtime-verified current reference owners, not verified new locations of the target. The hint proves provenance rather than staleness and is absent when no current durable source matches; keys, patch, and batch reads keep all-or-error behavior. Only then inspect ownership as needed and patch a proven stale owning value while preserving its surrounding meaning. Effective absence, inaccessible external resources, and transient failures are not proof.
|
|
50
50
|
|
|
51
51
|
Missing paths or runtime hints alone do not require historical search. The agent may choose a targeted historical read when a previous value is useful to the current task, without separate user permission. Otherwise continue without searching. Use found values as historical evidence, not automatically as current state; never automatically restore deleted memory. Do not scan all offsets, hydrate automatically or request repair inference. A hint does not prove prior existence, retained history or relocation. A proven stale reference may be repaired within touched work without resurrecting its target. Automatic state and recent-transition projections omit lazy bodies; bounded `lazy_navigation` preserves structure, and explicit current/historical reads still return requested lazy values or patches.
|
|
52
52
|
|
|
@@ -58,12 +58,20 @@ The runtime waits cancelably for publication ownership, then applies authored Gl
|
|
|
58
58
|
|
|
59
59
|
When present, semantic planes `intents`, `contract`, `working`, `artifacts`, and `lazy` are objects; nested lazy values may contain ordinary JSON without stored nulls. Stored checkpoints and patches may omit any documented plane. Current and historical views assemble only known fields present in the selected scopes. Absent fields and empty responses are omitted from views. Checkpoint/tail readers ignore unknown top-level fields, and writers emit only known fields. Nested data within known planes remains intact. Explicit value reads of an absent documented top-level field return `null`. Authored `patch_state` keeps its documented field grammar. Objects merge, arrays/scalars replace, omitted fields persist. Nested `null` removes an owned object key; inherited content may reappear. Canonical `"[N]"` keys patch array elements; indexed deletion is forbidden.
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
Work from intents. A structured `{"$ref"}` anywhere inside an intent owns ("delete with me") an existing object key under `working` or `lazy` in the same scope; a textual `$path` mention only uses it. Opening a task:
|
|
62
62
|
|
|
63
63
|
```json
|
|
64
|
-
{"session":{"intents":{"check_api":
|
|
64
|
+
{"session":{"intents":{"check_api":{"action":"Verify the API","notes":{"$ref":"session.working.api"},"plan":{"$ref":"session.lazy.api_plan"}}},"working":{"api":"draft findings"},"lazy":{"api_plan":["probe","compare"]}}}
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
+
Deleting the intent deletes the owned keys after the authored operations, in the same atomic patch, unless another remaining same-scope intent references the target, an ancestor or a descendant. Before closing, move what must survive to an unowned path: results to `working`, `contract` or a broader scope; reasons for abandoned work to `contract` as rejected approaches. Supersede in one patch by deleting the old intent and referencing the same targets from its replacement. Writing to an owned target in the same patch that deletes its intent does not save it: the write is deleted too. Only keys matching `[A-Za-z_$][A-Za-z0-9_$-]*` can be owned. Cross-scope, plane-root, array-element and non-`working`/`lazy` targets are never deleted, nothing is rejected or warned about, and unowned entries remain legal. Illustrative closing, only for an actually completed intent and after satisfying pending acquisitions:
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
{"session":{"intents":{"check_api":null},"contract":{"api":"verified: v2 only"}}}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Name object keys in ASCII matching `[A-Za-z_$][A-Za-z0-9_$-]*` (for example `api_plan`, not a Cyrillic or spaced key): other keys cannot be addressed by `read_state` paths or `$` references, and cannot be owned by intents. Values may use any language.
|
|
74
|
+
|
|
67
75
|
Never edit backing files, `response`, configuration, provenance, or runtime metadata. Verify changed owner paths when needed; check effective state after override deletion.
|
|
68
76
|
|
|
69
77
|
## Acquire and finish
|
|
@@ -20,9 +20,9 @@ Follow the installed runtime contract. This registered Skill follows its Pi sour
|
|
|
20
20
|
## Reconcile one bounded set
|
|
21
21
|
|
|
22
22
|
1. **Limit the review.** Address the requested scope. A completed phase may motivate recommending cleanup, not starting it without a request. For a whole-state cleanup, inspect global, CWD, and session ownership explicitly; for a narrower request, inspect only affected owners. Use targeted reads for gaps, contradictions, ownership, or verification; do not rerun the project.
|
|
23
|
-
2. **Classify.** Put user requirements and binding confirmed decisions in `contract`, observations, assistant conclusions,
|
|
23
|
+
2. **Classify.** Put user requirements and binding confirmed decisions in `contract`, the queue of chosen actions in `intents`, their temporary context (observations, assistant conclusions, results in progress) in `working`, and inactive reusable detail in `lazy`. Never give an assistant conclusion user authority. Possibilities are not commitments. Name keys in ASCII (`[A-Za-z_$][A-Za-z0-9_$-]*`) so paths, references and ownership resolve; values may use any language. Work from intents: a structured `{"$ref"}` inside an intent owns ("delete with me") a same-scope `working`/`lazy` key; a textual `$path` mention only uses it. Deleting a fulfilled, abandoned, superseded, or impossible intent deletes owned keys no remaining same-scope intent references, so first move what must survive to an unowned path (a write to an owned key in the deleting patch is deleted too): results to `working`, `contract`, or a broader scope; reasons for abandoned work to `contract` as rejected approaches. Supersede in one patch by referencing the same targets from the replacement. Unowned entries remain legal.
|
|
24
24
|
3. **Keep evidence boundaries.** Preserve corrections, prerequisites, bounded negative results, and useful uncertainty. Separate requirements, decisions, observations, conclusions, and hypotheses. Silence is not acceptance; repetition is not verification. One implementation's failure does not reject an approach. Neither freeze provisional methods nor reopen confirmed decisions without grounds.
|
|
25
|
-
4. **Compact for continuation.** Remove duplicates, obsolete progress, unsupported claims, and secrets. Keep sufficient results, real retrieval pointers, pending interaction, and known next checks. Observations are not live external facts. Keep `lazy` shallow and priority-ordered. Recognize optional structured `$ref` values and `$`-prefixed `read_state` paths inside ordinary strings as semantic-state references; other resources retain native locators. No reference form proves authority or existence, authorizes execution, or implies completion. Never scan or resolve references merely to find broken ones. When the bounded review independently needs a reference, a missing single value path with exact durable sources returns `{value:null, hint:[{type:"dangling-reference", message, paths}]}`. Treat the top-level hint as conditional navigation and provenance, never as requested state or proof of staleness; its paths are runtime-verified current reference owners, not verified new locations of the target, while no hint does not prove invention. Inspect ownership only as needed, then patch a proven stale owning value while preserving surrounding meaning. Effective absence or external inaccessibility is insufficient.
|
|
25
|
+
4. **Compact for continuation.** Remove duplicates, obsolete progress, unsupported claims, and secrets. Keep sufficient results, real retrieval pointers, pending interaction, and known next checks. Observations are not live external facts. Keep `lazy` shallow and priority-ordered. Recognize optional structured `$ref` values and `$`-prefixed `read_state` paths inside ordinary strings as semantic-state references; other resources retain native locators. No reference form proves authority or existence, authorizes execution, or implies completion; only intent ownership has a deletion consequence. Never scan or resolve references merely to find broken ones. When the bounded review independently needs a reference, a missing single value path with exact durable sources returns `{value:null, hint:[{type:"dangling-reference", message, paths}]}`. Treat the top-level hint as conditional navigation and provenance, never as requested state or proof of staleness; its paths are runtime-verified current reference owners, not verified new locations of the target, while no hint does not prove invention. Inspect ownership only as needed, then patch a proven stale owning value while preserving surrounding meaning. Effective absence or external inaccessibility is insufficient.
|
|
26
26
|
5. **Check ownership.** Prefer `session` for branch/run continuation, `cwd` for project knowledge, and `global` for established cross-project knowledge. Effective values do not prove ownership; inspect owners before moves. Broader applicability requires evidence.
|
|
27
27
|
|
|
28
28
|
Missing paths or runtime hints alone do not require historical search. The agent may choose a targeted historical read when a previous value is useful to the current task, without separate user permission. Otherwise continue without searching. Use found values as historical evidence, not automatically as current state; never automatically restore deleted memory. Do not scan all offsets, hydrate automatically or request repair inference. A hint does not prove prior existence, retained history or relocation. A proven stale reference may be repaired within touched work without resurrecting its target. Lazy bodies require explicit reads; automatic state/history projections retain navigation without hydrating those bodies.
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
_This file owns unresolved project work only. Completed behavior belongs in `CHANGELOG.md`; durable contracts belong in `AGENTS.md` and `/docs`._
|
|
4
4
|
|
|
5
|
+
- [ ] `Connect intent across resume` (`post-release acceptance`): The operator approved 0.51.6 release on the successful smoke and green Linux/macOS/Windows CI; this expanded live matrix is not a release blocker. Run live acceptance after rebuilding/reloading: start bare Pi, invoke `/telegram-connect` (default and named profile), immediately `/resume` a session with/without a binding. Check both leader and follower; confirm one connected destination, its correct retained/new slot, no source-target transplant, stale-context error or duplicate notice. Repeat a switch during startup and confirm disconnect cancels it. Native composition, destination-binding, cancellation, and late-completion regressions pass locally. The operator observed one immediate-resume connection complete after a short wait without error; the full profile/role/binding and cancellation matrix remains unverified live.
|
|
5
6
|
- [ ] [`Workspace Thread recovery`](./docs/multi-instance-bus.md) (`environment-gated`): Complete native-Windows coverage for follower restore, inaccessible callbacks, stale targets, and Singleton↔Threaded transitions while preserving accepted work and current ownership.
|
|
6
7
|
- [ ] Select an approved exact-absence observation before implementing ambiguous `deletion-issued` recovery. Never replay an unknown deletion or infer absence from cache, heartbeat silence, empty `editForumTopic`, or `sendChatAction` success.
|
|
7
8
|
- [ ] [`Automatic pairing and follower input custody`](./docs/architecture.md#automatic-pairing-confirmation-design) (`activation-gated`): Keep production custody disabled until historical sources and physical references are reconciled, all legacy writers/consumers are retired or excluded under operator authority, migration is complete, and every participating peer supports the final protocol. Preserve outcome-unknown running work, exact receipt/group authority, authenticated handoff, and rollback/downgrade safety. Then compose the prepared v3 lifecycle/bus ports, add bounded operator disposition for retained legacy retry state, and run real-process plus native-Windows acceptance.
|
|
@@ -4,6 +4,16 @@
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.51.6: Connection resume and Workspace recovery hotfix
|
|
8
|
+
|
|
9
|
+
- `Workspace slot recovery`: Confirmed pressure-retirement deletion invalidates the exact stale active-target record before binding removal. Same-process and successor retries finish a retained `commit-ready` fence without repeating Telegram deletion, preventing exhausted A–Z slots from deadlocking on `protection-changed`. Includes [#305](https://github.com/llblab/pi-telegram/pull/305).
|
|
10
|
+
- `Connect lifecycle`: `/telegram-connect` checks plain session generation before reading Pi context getters after awaited work. Replaced commands cannot publish stale connection notices or retry startup against the old session; config, startup, recovery and takeover confirmation discard obsolete results.
|
|
11
|
+
- `Resume connection`: In-flight `/telegram-connect` or an already connected bridge carries bounded same-process intent into the resumed session, retaining the selected profile but not the source Thread. Startup restores the destination's exact binding or provisions its own slot; cancellation, expiry and late completion cannot revive obsolete work.
|
|
12
|
+
- `Thread binding isolation`: Session-aware startup no longer reuses another session's active target or legacy instance binding. Pending Thread creation records its Workspace binding key; unproven or foreign-session recovery is blocked without consuming evidence or repeating creation.
|
|
13
|
+
- `Connection feedback`: Manual/resumed connection failures use compact cause-and-action notices rather than raw exception guidance. Detailed errors remain in redacted diagnostics; recovery success produces one short notice, and failed disconnect keeps Pi open without a duplicate exception banner. Delayed disconnect completion cannot touch a replaced Pi context.
|
|
14
|
+
- `Model continuation`: In-flight model switching injects one compact line with the resume instruction, selected model and optional thinking level, preserving the control lane and exact reply target.
|
|
15
|
+
- `Lifecycle ownership`: Connect intent and resume scheduling belong to the existing session lifecycle domain; compact failure formatting belongs to status. Transport authority, durable bindings and queue custody keep their existing owners, without introducing a separate connection domain.
|
|
16
|
+
|
|
7
17
|
## 0.51.5: Follower Thread new-session hotfix
|
|
8
18
|
|
|
9
19
|
- `Follower Thread /new`: Telegram `/new` now starts a new session in a follower's Pi process and preserves its Thread binding. The leader publishes and later claims the durable replacement intent through capability-gated, generation-fenced bus requests, validating its own live registration and Workspace binding before accepting the follower's request. Incompatible or stale leaders fail closed rather than silently switching sessions.
|
|
@@ -57,7 +57,7 @@ Paste the bot token. If `~/.pi/agent/telegram.json` already contains a saved tok
|
|
|
57
57
|
/telegram-connect
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
-
The connected Pi instance owns Telegram polling. Use `/telegram-connect <profile>` to activate a named profile. Each profile is a parallel bot runtime with isolated polling, diagnostics, Threaded Mode state, and local bus transport; the `default` profile keeps unsuffixed runtime paths. In classic mode each profile uses a singleton lock. When Telegram private-chat Threaded Mode is available, one live instance becomes the profile's leader and later visible Pi instances register as followers. Reopening or resuming the same Pi session restores its remembered Thread at session startup
|
|
60
|
+
The connected Pi instance owns Telegram polling. Use `/telegram-connect <profile>` to activate a named profile. Each profile is a parallel bot runtime with isolated polling, diagnostics, Threaded Mode state, and local bus transport; the `default` profile keeps unsuffixed runtime paths. In classic mode each profile uses a singleton lock. When Telegram private-chat Threaded Mode is available, one live instance becomes the profile's leader and later visible Pi instances register as followers. Reopening or resuming the same Pi session restores its remembered Thread at session startup. If `/resume` immediately follows `/telegram-connect`, or the bridge is already connected, connection intent follows the switch: the destination restores its own Thread/slot or receives a new binding, never the source session's Thread. Otherwise, a distinct unbound session still requires explicit `/telegram-connect`.
|
|
61
61
|
|
|
62
62
|
After an unclean computer shutdown, `/telegram-connect` detects truncated or structurally invalid temporary ownership/routing files, quarantines only the damaged files under `tmp/telegram/recovery/`, and retries once. A journal snapshot removed by older broad temp cleanup is rebuilt when its complete segment history proves an empty result, while a revisionless snapshot is repaired from the first surviving segment's exact predecessor when the reconstructed tail validates. Otherwise the snapshot and segments are quarantined as recovery evidence, a fresh journal is published, and startup continues with an informational diagnostic instead of requiring manual JSON repair. Unsupported journal versions block recovery without rewriting or quarantining the retained files; use a compatible runtime rather than deleting journals. Saved `telegram.json` configuration and runtime diagnostics remain intact. Recovery never replaces a verifiable live owner; if safe automatic recovery cannot complete, the command gives one explicit Pi-restart instruction instead of requiring deletion of the whole `tmp/` directory.
|
|
63
63
|
|
|
@@ -194,9 +194,12 @@ interface TelegramCommandsAndToolsBindingDeps {
|
|
|
194
194
|
canSendDirect: () => boolean;
|
|
195
195
|
setGenerativeAppLiveSurfaceRuntime?: (runtime: GenerativeApps.GenerativeAppLiveSurfaceRuntime<GenerativeApps.TelegramBindLiveHandle> | undefined) => void;
|
|
196
196
|
updateStatus: TelegramBridgeStatusUpdater;
|
|
197
|
+
isContextCurrent: (ctx: Pi.ExtensionContext) => boolean;
|
|
198
|
+
getSessionGeneration: () => number;
|
|
199
|
+
connectionIntent: NonNullable<Commands.TelegramBridgeCommandRegistrationDeps["connectionIntent"]>;
|
|
197
200
|
recordRuntimeEvent: TelegramRuntimeEventRecorder;
|
|
198
201
|
}
|
|
199
|
-
export declare function registerTelegramCommandsAndTools({ pi, agentDir, configStore, persistConfig, setup, activeTurnRuntime, lockedPollingRuntime, stopPolling, recoverPollingStart, getDisconnectThreadName, onTransportChanged, getStatusLines, buttonActionStore, sendMarkdownReply, sendChannelMarkdownMessage, sendChannelMediaMessage, listChannelPosts, mutateChannelPost, callMultipart, getDefaultChatId, getDefaultTarget, resolveAgentTarget, routeAgentMessage, canSendDirect, setGenerativeAppLiveSurfaceRuntime, recordRuntimeEvent, updateStatus, }: TelegramCommandsAndToolsBindingDeps): void;
|
|
202
|
+
export declare function registerTelegramCommandsAndTools({ pi, agentDir, configStore, persistConfig, setup, activeTurnRuntime, lockedPollingRuntime, stopPolling, recoverPollingStart, getDisconnectThreadName, onTransportChanged, getStatusLines, buttonActionStore, sendMarkdownReply, sendChannelMarkdownMessage, sendChannelMediaMessage, listChannelPosts, mutateChannelPost, callMultipart, getDefaultChatId, getDefaultTarget, resolveAgentTarget, routeAgentMessage, canSendDirect, setGenerativeAppLiveSurfaceRuntime, recordRuntimeEvent, updateStatus, isContextCurrent, getSessionGeneration, connectionIntent, }: TelegramCommandsAndToolsBindingDeps): void;
|
|
200
203
|
interface TelegramLifecycleBindingDeps {
|
|
201
204
|
pi: Pi.ExtensionAPI;
|
|
202
205
|
publicationRuntime: TelegramBridgePublicationRuntime;
|
|
@@ -323,7 +323,7 @@ export function createTelegramActivityBindingRuntime(deps) {
|
|
|
323
323
|
},
|
|
324
324
|
};
|
|
325
325
|
}
|
|
326
|
-
export function registerTelegramCommandsAndTools({ pi, agentDir, configStore, persistConfig, setup, activeTurnRuntime, lockedPollingRuntime, stopPolling, recoverPollingStart, getDisconnectThreadName, onTransportChanged, getStatusLines, buttonActionStore, sendMarkdownReply, sendChannelMarkdownMessage, sendChannelMediaMessage, listChannelPosts, mutateChannelPost, callMultipart, getDefaultChatId, getDefaultTarget, resolveAgentTarget, routeAgentMessage, canSendDirect, setGenerativeAppLiveSurfaceRuntime, recordRuntimeEvent, updateStatus, }) {
|
|
326
|
+
export function registerTelegramCommandsAndTools({ pi, agentDir, configStore, persistConfig, setup, activeTurnRuntime, lockedPollingRuntime, stopPolling, recoverPollingStart, getDisconnectThreadName, onTransportChanged, getStatusLines, buttonActionStore, sendMarkdownReply, sendChannelMarkdownMessage, sendChannelMediaMessage, listChannelPosts, mutateChannelPost, callMultipart, getDefaultChatId, getDefaultTarget, resolveAgentTarget, routeAgentMessage, canSendDirect, setGenerativeAppLiveSurfaceRuntime, recordRuntimeEvent, updateStatus, isContextCurrent, getSessionGeneration, connectionIntent, }) {
|
|
327
327
|
GenerativeApps.registerTelegramBindTool(pi, {
|
|
328
328
|
agentDir,
|
|
329
329
|
getActiveProfileName: configStore.getActiveProfileName,
|
|
@@ -452,33 +452,35 @@ export function registerTelegramCommandsAndTools({ pi, agentDir, configStore, pe
|
|
|
452
452
|
reloadConfig: configStore.load,
|
|
453
453
|
hasBotToken: configStore.hasBotToken,
|
|
454
454
|
getBotTokenDiagnostic: configStore.getBotTokenDiagnostic,
|
|
455
|
-
startPolling:
|
|
456
|
-
|
|
457
|
-
return await lockedPollingRuntime.start(ctx, options);
|
|
458
|
-
}
|
|
459
|
-
catch (error) {
|
|
460
|
-
recordRuntimeEvent("recovery", error, { phase: "polling-start" });
|
|
461
|
-
throw error;
|
|
462
|
-
}
|
|
463
|
-
},
|
|
455
|
+
startPolling: lockedPollingRuntime.start,
|
|
456
|
+
recordConnectionEvent: (error, phase) => recordRuntimeEvent("connection", error, { phase }),
|
|
464
457
|
stopPolling: stopPolling ?? lockedPollingRuntime.stop,
|
|
465
458
|
recoverPollingStart,
|
|
466
459
|
getDisconnectThreadName,
|
|
467
460
|
queueAgentConnectionContext,
|
|
468
461
|
updateStatus,
|
|
462
|
+
isContextCurrent,
|
|
463
|
+
getSessionGeneration,
|
|
464
|
+
connectionIntent,
|
|
469
465
|
getProfileNames: () => Config.getTelegramProfileNames(configStore.getStoredConfig()),
|
|
470
|
-
activateDefaultProfileConfig: async () => {
|
|
466
|
+
activateDefaultProfileConfig: async (_ctx, isCurrent) => {
|
|
471
467
|
const previousProfileName = configStore.getActiveProfileName();
|
|
472
468
|
await configStore.load();
|
|
469
|
+
if (!isCurrent())
|
|
470
|
+
return;
|
|
473
471
|
if (previousProfileName) {
|
|
474
472
|
await (stopPolling ?? lockedPollingRuntime.stop)();
|
|
473
|
+
if (!isCurrent())
|
|
474
|
+
return;
|
|
475
475
|
}
|
|
476
476
|
configStore.activateProfile(undefined);
|
|
477
477
|
await onTransportChanged?.();
|
|
478
478
|
},
|
|
479
|
-
activateProfileConfig: async (_ctx, profileName) => {
|
|
479
|
+
activateProfileConfig: async (_ctx, profileName, isCurrent) => {
|
|
480
480
|
const previousProfileName = configStore.getActiveProfileName();
|
|
481
481
|
await configStore.load();
|
|
482
|
+
if (!isCurrent())
|
|
483
|
+
return false;
|
|
482
484
|
if (!Config.isValidTelegramProfileName(profileName))
|
|
483
485
|
return false;
|
|
484
486
|
const storedConfig = configStore.getStoredConfig();
|
|
@@ -486,6 +488,8 @@ export function registerTelegramCommandsAndTools({ pi, agentDir, configStore, pe
|
|
|
486
488
|
return false;
|
|
487
489
|
if (previousProfileName !== profileName) {
|
|
488
490
|
await (stopPolling ?? lockedPollingRuntime.stop)();
|
|
491
|
+
if (!isCurrent())
|
|
492
|
+
return false;
|
|
489
493
|
}
|
|
490
494
|
if (!configStore.activateProfile(profileName))
|
|
491
495
|
return false;
|