@llblab/pi-kit 0.16.0 → 0.17.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.
Files changed (51) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +1 -1
  3. package/node_modules/@llblab/pi-state-flow/AGENTS.md +4 -4
  4. package/node_modules/@llblab/pi-state-flow/BACKLOG.md +3 -1
  5. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +16 -0
  6. package/node_modules/@llblab/pi-state-flow/README.md +5 -2
  7. package/node_modules/@llblab/pi-state-flow/dist/lib/durable.d.ts +1 -1
  8. package/node_modules/@llblab/pi-state-flow/dist/lib/durable.js +7 -0
  9. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.d.ts +2 -4
  10. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +31 -239
  11. package/node_modules/@llblab/pi-state-flow/dist/lib/history.js +1 -1
  12. package/node_modules/@llblab/pi-state-flow/dist/lib/logging.d.ts +18 -0
  13. package/node_modules/@llblab/pi-state-flow/dist/lib/logging.js +44 -1
  14. package/node_modules/@llblab/pi-state-flow/dist/lib/migration.d.ts +2 -2
  15. package/node_modules/@llblab/pi-state-flow/dist/lib/migration.js +21 -3
  16. package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.d.ts +7 -0
  17. package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +55 -5
  18. package/node_modules/@llblab/pi-state-flow/dist/lib/publication.d.ts +17 -0
  19. package/node_modules/@llblab/pi-state-flow/dist/lib/publication.js +102 -0
  20. package/node_modules/@llblab/pi-state-flow/dist/lib/query.d.ts +16 -0
  21. package/node_modules/@llblab/pi-state-flow/dist/lib/query.js +98 -12
  22. package/node_modules/@llblab/pi-state-flow/dist/lib/session.d.ts +17 -0
  23. package/node_modules/@llblab/pi-state-flow/dist/lib/session.js +38 -0
  24. package/node_modules/@llblab/pi-state-flow/dist/lib/state.d.ts +5 -0
  25. package/node_modules/@llblab/pi-state-flow/dist/lib/state.js +10 -2
  26. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.d.ts +1 -0
  27. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +1 -1
  28. package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +4 -3
  29. package/node_modules/@llblab/pi-state-flow/dist/package.json +1 -1
  30. package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-guide/SKILL.md +79 -0
  31. package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-memory/SKILL.md +23 -129
  32. package/node_modules/@llblab/pi-state-flow/docs/architecture.md +22 -17
  33. package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +1 -1
  34. package/node_modules/@llblab/pi-state-flow/docs/lazy-state.md +15 -6
  35. package/node_modules/@llblab/pi-state-flow/docs/usage.md +2 -2
  36. package/node_modules/@llblab/pi-state-flow/lib/durable.ts +6 -1
  37. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +33 -228
  38. package/node_modules/@llblab/pi-state-flow/lib/history.ts +1 -1
  39. package/node_modules/@llblab/pi-state-flow/lib/logging.ts +53 -1
  40. package/node_modules/@llblab/pi-state-flow/lib/migration.ts +18 -4
  41. package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +51 -5
  42. package/node_modules/@llblab/pi-state-flow/lib/publication.ts +96 -0
  43. package/node_modules/@llblab/pi-state-flow/lib/query.ts +99 -11
  44. package/node_modules/@llblab/pi-state-flow/lib/session.ts +45 -0
  45. package/node_modules/@llblab/pi-state-flow/lib/state.ts +13 -2
  46. package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +2 -1
  47. package/node_modules/@llblab/pi-state-flow/lib/transition.ts +4 -3
  48. package/node_modules/@llblab/pi-state-flow/package.json +1 -1
  49. package/node_modules/@llblab/pi-state-flow/skills/state-flow-guide/SKILL.md +79 -0
  50. package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +23 -129
  51. package/package.json +2 -2
@@ -23,3 +23,21 @@ export declare function projectDiagnosticContent(content: unknown): StateFlowDia
23
23
  /** Diagnostic JSONL lives beneath the active Pi agent directory, never inside the state repository. */
24
24
  export declare function stateFlowLogPath(agentDir: string): string;
25
25
  export declare function appendStateFlowDiagnostic(path: string, record: StateFlowDiagnosticRecord): void;
26
+ export interface DiagnosticExtras {
27
+ content?: unknown;
28
+ input?: unknown;
29
+ tool?: string;
30
+ toolCallId?: string;
31
+ resolutionAttempt?: number;
32
+ terminalEligible?: boolean;
33
+ }
34
+ /** Own diagnostic path safety, projection, persistence, and one-shot failure reporting. */
35
+ export declare class StateFlowDiagnosticWriter {
36
+ private warningReported;
37
+ private readonly enabled;
38
+ private readonly path;
39
+ private readonly repositoryRoot;
40
+ private readonly notify;
41
+ constructor(enabled: boolean, path: string, repositoryRoot: string, notify: (message: string) => void);
42
+ record(sessionId: string, cwd: string, error: string, category: StateFlowDiagnosticCategory, extras?: DiagnosticExtras): void;
43
+ }
@@ -1,6 +1,6 @@
1
1
  // Domain: opt-in diagnostic capture for rejected State Flow resolutions.
2
2
  import { appendFileSync, mkdirSync } from "node:fs";
3
- import { dirname, join } from "node:path";
3
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
4
4
  import { isObject } from "./json.js";
5
5
  /** Preserve exact text blocks and block boundaries; reasoning bodies are never duplicated. */
6
6
  export function projectDiagnosticContent(content) {
@@ -22,3 +22,46 @@ export function appendStateFlowDiagnostic(path, record) {
22
22
  mkdirSync(dirname(path), { recursive: true });
23
23
  appendFileSync(path, `${JSON.stringify(record)}\n`, { encoding: "utf8", mode: 0o600 });
24
24
  }
25
+ /** Own diagnostic path safety, projection, persistence, and one-shot failure reporting. */
26
+ export class StateFlowDiagnosticWriter {
27
+ warningReported = false;
28
+ enabled;
29
+ path;
30
+ repositoryRoot;
31
+ notify;
32
+ constructor(enabled, path, repositoryRoot, notify) {
33
+ this.enabled = enabled;
34
+ this.path = path;
35
+ this.repositoryRoot = repositoryRoot;
36
+ this.notify = notify;
37
+ }
38
+ record(sessionId, cwd, error, category, extras = {}) {
39
+ if (!this.enabled)
40
+ return;
41
+ try {
42
+ const fromRepository = relative(this.repositoryRoot, this.path);
43
+ if (fromRepository === "" || (!isAbsolute(fromRepository) && fromRepository !== ".." && !fromRepository.startsWith(`..${sep}`))) {
44
+ throw new Error("diagnostic path overlaps the State Flow repository");
45
+ }
46
+ appendStateFlowDiagnostic(this.path, {
47
+ at: new Date().toISOString(),
48
+ sessionId,
49
+ cwd: resolve(cwd),
50
+ category,
51
+ error,
52
+ ...(extras.content === undefined ? {} : { content: projectDiagnosticContent(extras.content) }),
53
+ ...(extras.input === undefined ? {} : { input: extras.input }),
54
+ ...(extras.tool === undefined ? {} : { tool: extras.tool }),
55
+ ...(extras.toolCallId === undefined ? {} : { toolCallId: extras.toolCallId }),
56
+ ...(extras.resolutionAttempt === undefined ? {} : { resolutionAttempt: extras.resolutionAttempt }),
57
+ ...(extras.terminalEligible === undefined ? {} : { terminalEligible: extras.terminalEligible }),
58
+ });
59
+ }
60
+ catch (failure) {
61
+ if (this.warningReported)
62
+ return;
63
+ this.warningReported = true;
64
+ this.notify(`State Flow could not write diagnostics: ${failure instanceof Error ? failure.message : String(failure)}`);
65
+ }
66
+ }
67
+ }
@@ -1,5 +1,5 @@
1
1
  import { type DurableFileBase, type OwnedFileUpdate } from "./durable.ts";
2
- import type { StateScope } from "./state.ts";
2
+ import { type StateScope } from "./state.ts";
3
3
  export interface LegacyStorageMigration {
4
4
  bases: DurableFileBase[];
5
5
  updates: OwnedFileUpdate[];
@@ -7,7 +7,7 @@ export interface LegacyStorageMigration {
7
7
  }
8
8
  /** Read-only activation eligibility, before global migration or any repository publication. */
9
9
  export declare function hasCwdMaterialization(cwd: string, repositoryRoot: string): boolean;
10
- /** Detect predecessor snapshots or temporal envelopes without mutating the store. */
10
+ /** Detect predecessor snapshots, temporal envelopes, or pre-intents semantic states without mutating the store. */
11
11
  export declare function hasLegacyStateSources(cwd: string, sessionId: string, repositoryRoot: string, sessionKey?: string): boolean;
12
12
  /** Plan from current scope snapshots only; old explanatory journals are never replay input. */
13
13
  export declare function planLegacyStorageMigration(cwd: string, sessionId: string, repositoryRoot: string, _origin?: string, sessionKey?: string): LegacyStorageMigration;
@@ -3,6 +3,7 @@ import { lstatSync, readFileSync, readdirSync } from "node:fs";
3
3
  import { join, resolve } from "node:path";
4
4
  import { captureOwnedFileBases, cwdScopePaths, parseScopeStream, serializeScopeMetadata, serializeScopeStream, sessionScopeKey, sessionScopePaths, } from "./durable.js";
5
5
  import { parseSessionRuntime, serializeSessionRuntime } from "./snapshot.js";
6
+ import { migratePreIntentState } from "./state.js";
6
7
  /** Discover every owner-proven CWD and session cohort beneath the configured store. */
7
8
  function migrationDirectories(cwd, sessionId, root, sessionKey) {
8
9
  const selectedCwd = cwdScopePaths(cwd, root).directory;
@@ -48,13 +49,20 @@ function migrationDirectories(cwd, sessionId, root, sessionKey) {
48
49
  }
49
50
  const directory = join(cwdDirectory, entry.name);
50
51
  let meta;
52
+ let runtime;
51
53
  try {
52
54
  meta = JSON.parse(readFileSync(join(directory, "meta.json"), "utf8"));
53
55
  }
54
56
  catch {
55
57
  continue;
56
58
  }
57
- const identity = meta && typeof meta === "object" && !Array.isArray(meta) ? meta.identity : undefined;
59
+ try {
60
+ runtime = JSON.parse(readFileSync(join(directory, "runtime.json"), "utf8"));
61
+ }
62
+ catch { /* predecessor sessions keep identity in meta.json */ }
63
+ const identity = meta && typeof meta === "object" && !Array.isArray(meta) && meta.identity !== undefined
64
+ ? meta.identity
65
+ : runtime && typeof runtime === "object" && !Array.isArray(runtime) ? runtime.identity : undefined;
58
66
  if (!identity || typeof identity !== "object" || Array.isArray(identity))
59
67
  continue;
60
68
  const owner = identity;
@@ -81,13 +89,23 @@ export function hasCwdMaterialization(cwd, repositoryRoot) {
81
89
  throw new Error(`State Flow tail has no provable checkpoint: ${directory}`);
82
90
  return false;
83
91
  }
84
- /** Detect predecessor snapshots or temporal envelopes without mutating the store. */
92
+ function checkpointNeedsIntentMigration(source) {
93
+ const checkpoint = JSON.parse(source);
94
+ if (migratePreIntentState(checkpoint) !== undefined)
95
+ return true;
96
+ return checkpoint !== null && typeof checkpoint === "object" && !Array.isArray(checkpoint)
97
+ && migratePreIntentState(checkpoint.state) !== undefined;
98
+ }
99
+ /** Detect predecessor snapshots, temporal envelopes, or pre-intents semantic states without mutating the store. */
85
100
  export function hasLegacyStateSources(cwd, sessionId, repositoryRoot, sessionKey = sessionId) {
86
101
  const root = resolve(repositoryRoot);
87
102
  return migrationDirectories(cwd, sessionId, root, sessionKey).some(({ directory, scope }) => {
88
- if (lstatSync(join(directory, "checkpoint.json"), { throwIfNoEntry: false }) === undefined)
103
+ const checkpointPath = join(directory, "checkpoint.json");
104
+ if (lstatSync(checkpointPath, { throwIfNoEntry: false }) === undefined)
89
105
  return false;
90
106
  try {
107
+ if (checkpointNeedsIntentMigration(readFileSync(checkpointPath, "utf8")))
108
+ return true;
91
109
  const meta = JSON.parse(readFileSync(join(directory, "meta.json"), "utf8"));
92
110
  if (meta === null || typeof meta !== "object" || !("temporal" in meta))
93
111
  return true;
@@ -1,5 +1,12 @@
1
1
  import type { AgentMessage } from "@earendil-works/pi-agent-core";
2
2
  export type { StateDocument } from "./state.ts";
3
+ /** Keep successful patch JSON valid while separating adjacent scopes and memory sections visually. */
4
+ export declare function formatPatchStateArguments(args: unknown): string;
5
+ /** Normalize a bounded compatibility superset without advertising aliases in the model-facing contract. */
6
+ export declare function normalizePatchStateArguments(args: unknown): any;
7
+ /** Keep visible tool output separated from its heading without changing semantics. */
8
+ export declare function separatedOutput(text: string): string;
9
+ export declare function separatedFailure(error: unknown): Error;
3
10
  /** The compact model-facing contract. Semantic writes never travel through terminal prose. */
4
11
  export declare function stateFlowProtocol(bootstrap: boolean): string;
5
12
  export declare function assistantToolCallCount(content: unknown): number;
@@ -1,3 +1,50 @@
1
+ import { isObject } from "./json.js";
2
+ const PATCH_DISPLAY_SECTION_KEYS = new Set(["global", "cwd", "session", "artifacts", "contract", "working", "response", "final"]);
3
+ /** Keep successful patch JSON valid while separating adjacent scopes and memory sections visually. */
4
+ export function formatPatchStateArguments(args) {
5
+ const seenAtIndent = new Set();
6
+ return JSON.stringify(args, null, 2).split("\n").flatMap((line) => {
7
+ const indent = line.length - line.trimStart().length;
8
+ const match = /^(\s+)"([^"]+)":/.exec(line);
9
+ if (match === null || !PATCH_DISPLAY_SECTION_KEYS.has(match[2])) {
10
+ for (const seenIndent of seenAtIndent)
11
+ if (seenIndent > indent)
12
+ seenAtIndent.delete(seenIndent);
13
+ return [line];
14
+ }
15
+ const separator = seenAtIndent.has(indent) ? [""] : [];
16
+ seenAtIndent.add(indent);
17
+ return [...separator, line];
18
+ }).join("\n");
19
+ }
20
+ /** Normalize a bounded compatibility superset without advertising aliases in the model-facing contract. */
21
+ export function normalizePatchStateArguments(args) {
22
+ if (!isObject(args) || !Object.hasOwn(args, "final"))
23
+ return args;
24
+ const value = args.final;
25
+ let final;
26
+ if (typeof value === "boolean")
27
+ final = value;
28
+ else if (value === 1)
29
+ final = true;
30
+ else if (value === 0)
31
+ final = false;
32
+ else if (typeof value === "string" && value.trim().toLowerCase() === "true")
33
+ final = true;
34
+ else if (typeof value === "string" && value.trim().toLowerCase() === "false")
35
+ final = false;
36
+ else
37
+ return args;
38
+ return { ...args, final };
39
+ }
40
+ /** Keep visible tool output separated from its heading without changing semantics. */
41
+ export function separatedOutput(text) {
42
+ return `\n${text.replace(/^\n+/, "")}`;
43
+ }
44
+ export function separatedFailure(error) {
45
+ const message = error instanceof Error ? error.message : String(error);
46
+ return new Error(separatedOutput(message), error instanceof Error ? { cause: error } : undefined);
47
+ }
1
48
  function baselineMemoryProtocol() {
2
49
  return "MEMORY: State Flow owns durable memory while enabled. Put established cross-project/user/environment knowledge in global, reusable project truth in cwd, and branch/run continuation in session. Treat every patch as reconciliation rather than append-only notes: use the narrowest scope; merge superseded fragments; remove obsolete progress. Exclude secrets, raw history, transient progress, speculation, and unsupported claims; retain uncertainty only when decision-relevant.";
3
50
  }
@@ -8,25 +55,28 @@ export function stateFlowProtocol(bootstrap) {
8
55
  : "";
9
56
  return `State Flow is enabled.
10
57
  ${bootstrapProtocol}
11
- STATE: {"artifacts":{},"contract":{},"working":{},"response":"latest complete answer"}
58
+ STATE: {"artifacts":{},"contract":{},"working":{},"intents":{},"response":"latest complete answer"}
12
59
  - artifacts: source-path routing metadata; descriptions do not imply body acquisition.
13
60
  - contract: durable requirements, decisions, rejections, interfaces, compiled knowledge.
14
61
  - working: facts, validation, failures, domain state, unresolved work, continuation.
62
+ - intents: active commitments; remove when fulfilled, abandoned, superseded, or impossible.
15
63
  - response: previous complete answer; runtime-owned.
16
64
 
17
- READ: Use read_state only for a concrete historical/scope gap. lazy_navigation gives the effective lazy root and bounded key kinds, never bodies or a partial catalog. Unscoped paths alias effective; effective/global/cwd/session select overlay or owner. Paths read cached values; arrays support zero-based indices and half-open [start..end]. keys returns minimal structure; patch returns the path-intersected change.
65
+ READ: Use read_state for concrete scope/history gaps. lazy_navigation exposes the effective lazy root's bounded key kinds, not bodies. Unscoped paths alias effective; effective/global/cwd/session select overlay or owner. Arrays support indices and [start..end]; keys gives structure and patch the intersected change.
18
66
 
19
67
  WRITE: patch_state is the sole model-authored semantic mutation mechanism. Supply global/cwd/session patches in any combination; all supplied scopes are validated and durably accepted as one atomic transition. Call it alone in an assistant response, then continue only after its acknowledgement.
20
68
 
21
- FINAL: Every enabled iteration starts terminal-ineligible. A successful patch_state with final:true permits a later turn_end but does not stop reasoning, tools, or later patches. Use {"final":true} if state needs no change. Without eligibility, runtime preserves the terminal answer and starts at most two fallback turns solely for a final:true patch; never restate or replace that answer. Exhaustion closes with the preserved answer/current state. A final-only call creates no semantic transition. Never patch response; runtime records the delivered answer.
69
+ FINAL: Every enabled iteration starts terminal-ineligible. Successful patch_state final:true permits a later turn_end without stopping later work; use {"final":true} when no state change is needed. Otherwise runtime preserves the answer and allows at most two fallback turns only for final:true; never restate it. A final-only call creates no transition. Runtime owns response.
70
+
71
+ INTENTS: Keep chosen actions; detail may stay lazy. State refs use {"$ref":"cwd.lazy.plan"} or \`$cwd.lazy.plan\` in text. Resolve only when needed; infer no authority, hydration, execution, or completion. If that resolution proves a dangling state ref, fix/drop it in owning text; never scan for broken refs.
22
72
 
23
73
  SCOPES: global=cross-project; cwd=project and Skills; session=branch/run. Deleting an override may reveal its parent.
24
74
 
25
75
  ${baselineMemoryProtocol()}
26
76
 
27
- PATCH: Optional global/cwd/session object patches plus optional final:true; require at least one. Omit empty/materially no-op scopes. Semantic fields are object-valued artifacts/contract/working and ordinary-JSON lazy; 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.
77
+ PATCH: Optional global/cwd/session object patches plus optional final:true; require at least one. Omit empty/materially no-op scopes. Semantic fields are object-valued artifacts/contract/working/intents and ordinary-JSON lazy; 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.
28
78
 
29
- HANDOFF: Preserve active commitments, unresolved questions, consequential results, and exact continuation. Distinguish requirements, decisions, observations, conclusions, and hypotheses. Before final handoff curate touched and obviously stale/mis-scoped state. On feature/release/campaign or project/version completion, do one bounded reconciliation: remove obsolete work, retain operative consequences, and use targeted read_state plus destination-verify-source-delete for ownership moves. Never invent memory changes or restyle unrelated state.
79
+ HANDOFF: Preserve active commitments, open questions, consequential results, and exact continuation; distinguish requirements, decisions, observations, conclusions, and hypotheses. Curate touched and obviously stale/mis-scoped state. At feature/release/campaign or project/version completion, reconcile once: remove obsolete work, retain consequences, and use targeted read_state plus destination-verify-source-delete for moves. Never invent memory changes.
30
80
 
31
81
  ACQUISITION: Start materialized. Read only for a compilation gap, exact source/edit, invalidation, contradiction/failure, or explicit request; changed hashes require rereading.
32
82
  ARTIFACTS: For each acquired new/invalidated ordinary artifact, patch global.artifacts[exact path] with a compact non-empty description. Runtime owns provenance.
@@ -67,3 +67,20 @@ export interface PublicationWorkerResult {
67
67
  }
68
68
  /** Execute one immutable target attempt; persistence/CAS remains the caller's responsibility. */
69
69
  export declare function runPublicationWorker(state: PublicationQueueState, push: PublicationPush, current: () => PublicationQueueState, isAncestor: CommitAncestor): Promise<PublicationWorkerResult>;
70
+ export interface PublicationWorkerControllerPorts {
71
+ resolveDestination(): RemotePublicationDestination | undefined;
72
+ isAncestor(ancestor: string, descendant: string): boolean;
73
+ push(destination: RemotePublicationDestination, target: string, signal: AbortSignal): Promise<void>;
74
+ onDiverged(previous: PublicationQueueState, target: string): void;
75
+ }
76
+ /** Own durable queue coalescing, worker leases, retries, generation fencing, and bounded shutdown. */
77
+ export declare class PublicationWorkerController {
78
+ private readonly active;
79
+ private readonly ports;
80
+ private stopped;
81
+ private shutdownPromise;
82
+ constructor(ports: PublicationWorkerControllerPorts);
83
+ enqueue(target: string): void;
84
+ launch(): void;
85
+ shutdown(waitMs: number): Promise<boolean>;
86
+ }
@@ -333,3 +333,105 @@ export async function runPublicationWorker(state, push, current, isAncestor) {
333
333
  };
334
334
  }
335
335
  }
336
+ /** Own durable queue coalescing, worker leases, retries, generation fencing, and bounded shutdown. */
337
+ export class PublicationWorkerController {
338
+ active = new Map();
339
+ ports;
340
+ stopped = false;
341
+ shutdownPromise;
342
+ constructor(ports) {
343
+ this.ports = ports;
344
+ }
345
+ enqueue(target) {
346
+ const destination = this.ports.resolveDestination();
347
+ if (!destination)
348
+ return;
349
+ const path = publicationQueuePath(destination);
350
+ const previous = loadPublicationQueue(path);
351
+ const next = previous
352
+ ? coalescePublicationTarget(previous, destination, target, this.ports.isAncestor, {
353
+ onDivergedLineage: (dropped) => this.ports.onDiverged(dropped, target),
354
+ })
355
+ : createPublicationQueue(destination, target);
356
+ savePublicationQueue(path, next, previous);
357
+ }
358
+ launch() {
359
+ if (this.stopped)
360
+ return;
361
+ let destination;
362
+ try {
363
+ destination = this.ports.resolveDestination();
364
+ }
365
+ catch (error) {
366
+ if (error instanceof Error && /ENOENT/.test(error.message))
367
+ return;
368
+ throw error;
369
+ }
370
+ if (!destination)
371
+ return;
372
+ const path = publicationQueuePath(destination);
373
+ if (this.active.has(path))
374
+ return;
375
+ let queued;
376
+ let lease;
377
+ try {
378
+ queued = loadPublicationQueue(path);
379
+ if (!queued)
380
+ return;
381
+ lease = acquirePublicationWorkerLease(path);
382
+ }
383
+ catch {
384
+ return;
385
+ }
386
+ if (!lease)
387
+ return;
388
+ const controller = new AbortController();
389
+ const done = runPublicationWorker(queued, ({ target }) => this.ports.push(destination, target, controller.signal), () => loadPublicationQueue(path) ?? queued, this.ports.isAncestor).then((result) => {
390
+ if (this.stopped)
391
+ return;
392
+ const current = loadPublicationQueue(path);
393
+ if (!current)
394
+ return;
395
+ if (result.next === undefined) {
396
+ if (current.target === result.attempted.target)
397
+ removePublicationQueue(path, current);
398
+ return;
399
+ }
400
+ if (current.target === result.attempted.target || result.next.target === current.target)
401
+ savePublicationQueue(path, result.next, current);
402
+ }).catch(() => {
403
+ // Queue/CAS truth remains durable; status and a later activation expose retry.
404
+ }).finally(() => {
405
+ this.active.delete(path);
406
+ try {
407
+ lease.release();
408
+ if (!this.stopped && loadPublicationQueue(path)?.status === "pending")
409
+ this.launch();
410
+ }
411
+ catch {
412
+ // Failed lease cleanup or malformed persistence stays inert until retry or repair.
413
+ }
414
+ });
415
+ this.active.set(path, { controller, done });
416
+ }
417
+ shutdown(waitMs) {
418
+ this.stopped = true;
419
+ return this.shutdownPromise ??= (async () => {
420
+ const workers = [...this.active.values()];
421
+ for (const { controller } of workers)
422
+ controller.abort();
423
+ if (workers.length === 0)
424
+ return true;
425
+ let timeout;
426
+ try {
427
+ return await Promise.race([
428
+ Promise.all(workers.map(({ done }) => done)).then(() => true),
429
+ new Promise((resolve) => { timeout = setTimeout(() => resolve(false), waitMs); }),
430
+ ]);
431
+ }
432
+ finally {
433
+ clearTimeout(timeout);
434
+ }
435
+ })();
436
+ }
437
+ }
@@ -37,17 +37,33 @@ type StateReadMeta = {
37
37
  type: "number" | "boolean";
38
38
  };
39
39
  type StateReadKeys = Record<string, string> | [];
40
+ export interface StateReadHint {
41
+ type: "dangling-reference";
42
+ message: string;
43
+ paths: string[];
44
+ }
40
45
  export type ProjectedStateRead = {
41
46
  value: JsonValue | JsonValue[];
47
+ hint?: StateReadHint[];
42
48
  } | {
43
49
  meta: StateReadMeta | StateReadMeta[];
44
50
  keys: StateReadKeys | StateReadKeys[];
45
51
  } | {
46
52
  patch: JsonValue | JsonValue[];
47
53
  };
54
+ export interface StateReferenceSource {
55
+ scope: StateScope;
56
+ path: string;
57
+ form: "structured" | "text";
58
+ }
48
59
  /** Resolve a projection root without repeating the tool name in every path. */
49
60
  export declare function parseStateReadPath(path: string): StateReadQuery;
50
61
  export declare function readStatePath(view: TemporalState, path: string): StateReadResult;
62
+ /** Reactively locate exact durable sources for one failed state-path resolution. */
63
+ export declare function findStateReferenceSources(view: TemporalState, path: string): {
64
+ sources: StateReferenceSource[];
65
+ truncated: boolean;
66
+ };
51
67
  /** Project exact current/historical state paths without exposing temporal metadata. */
52
68
  export declare function readProjectedState(view: TemporalState, paths: readonly string[], projection?: StateReadProjection): ProjectedStateRead;
53
69
  export {};
@@ -1,6 +1,8 @@
1
1
  import { isObject, sameJson } from "./json.js";
2
2
  import { projectStateForModel } from "./state.js";
3
3
  import { readTemporalState } from "./temporal.js";
4
+ const MAX_REFERENCE_SOURCES = 3;
5
+ const MAX_REFERENCE_SCAN_NODES = 10_000;
4
6
  const PATH_PATTERN = /^(effective|global|cwd|session)(?:\[(\d+)\])?(?:\.patches(?:\[(\d+)\])?)?$/;
5
7
  /** Resolve a projection root without repeating the tool name in every path. */
6
8
  export function parseStateReadPath(path) {
@@ -35,7 +37,7 @@ export function readStatePath(view, path) {
35
37
  }
36
38
  function parseValuePath(path) {
37
39
  const explicitRoot = /^(?:effective|global|cwd|session)(?:\[\d+\])?(?=\.|$)/.exec(path)?.[0];
38
- const implicitRoot = /^(?:artifacts|contract|working|response|lazy)(?=\.|\[|$)/.exec(path)?.[0];
40
+ const implicitRoot = /^(?:artifacts|contract|working|intents|response|lazy)(?=\.|\[|$)/.exec(path)?.[0];
39
41
  if (explicitRoot === undefined && implicitRoot === undefined) {
40
42
  throw new Error("State Flow read path requires a semantic path or an effective, global, cwd, or session root");
41
43
  }
@@ -95,6 +97,80 @@ function valueKind(value) {
95
97
  return "object";
96
98
  return typeof value;
97
99
  }
100
+ function referenceCandidates(path) {
101
+ if (/^(?:artifacts|contract|working|intents|response|lazy)(?=\.|\[|$)/.test(path))
102
+ return [path, `effective.${path}`];
103
+ return [path];
104
+ }
105
+ function inlineReferencePattern(path) {
106
+ const escaped = path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
107
+ return new RegExp(`(?:^|[^A-Za-z0-9_$.[\\]:-])\\$${escaped}(?=$|[^A-Za-z0-9_$.[\\]:-])`, "u");
108
+ }
109
+ /** Reactively locate exact durable sources for one failed state-path resolution. */
110
+ export function findStateReferenceSources(view, path) {
111
+ const candidates = referenceCandidates(path);
112
+ const patterns = candidates.map(inlineReferencePattern);
113
+ const sources = [];
114
+ let visited = 0;
115
+ let truncated = false;
116
+ const visit = (value, owner, ownerPath) => {
117
+ if (sources.length >= MAX_REFERENCE_SOURCES || visited >= MAX_REFERENCE_SCAN_NODES) {
118
+ truncated = true;
119
+ return;
120
+ }
121
+ visited += 1;
122
+ if (typeof value === "string") {
123
+ if (patterns.some((pattern) => pattern.test(value)))
124
+ sources.push({ scope: owner, path: ownerPath, form: "text" });
125
+ return;
126
+ }
127
+ if (Array.isArray(value)) {
128
+ for (let index = 0; index < value.length && !truncated; index++)
129
+ visit(value[index], owner, `${ownerPath}[${index}]`);
130
+ return;
131
+ }
132
+ if (!isObject(value))
133
+ return;
134
+ if (typeof value.$ref === "string" && candidates.includes(value.$ref)) {
135
+ sources.push({ scope: owner, path: ownerPath, form: "structured" });
136
+ if (sources.length >= MAX_REFERENCE_SOURCES) {
137
+ truncated = true;
138
+ return;
139
+ }
140
+ }
141
+ for (const key of Object.keys(value).sort()) {
142
+ if (key === "response" || (key === "$ref" && typeof value[key] === "string"))
143
+ continue;
144
+ visit(value[key], owner, ownerPath ? `${ownerPath}.${key}` : `${owner}.${key}`);
145
+ if (truncated)
146
+ return;
147
+ }
148
+ };
149
+ for (const scope of ["global", "cwd", "session"]) {
150
+ const state = readTemporalState(view, 0, scope);
151
+ for (const plane of ["artifacts", "contract", "working", "intents", "lazy"]) {
152
+ const value = state[plane];
153
+ if (value !== undefined)
154
+ visit(value, scope, `${scope}.${plane}`);
155
+ if (truncated)
156
+ break;
157
+ }
158
+ if (truncated)
159
+ break;
160
+ }
161
+ sources.sort((left, right) => (left.form === right.form ? left.path.localeCompare(right.path) : left.form === "structured" ? -1 : 1));
162
+ return { sources, truncated };
163
+ }
164
+ function missingReferenceHint(view, path) {
165
+ const { sources, truncated } = findStateReferenceSources(view, path);
166
+ if (sources.length === 0)
167
+ return undefined;
168
+ return [{
169
+ type: "dangling-reference",
170
+ message: `Reconcile the verified current values that reference this path${truncated ? "; additional sources may exist beyond the bounded scan" : ""}.`,
171
+ paths: sources.map(({ path: sourcePath }) => sourcePath),
172
+ }];
173
+ }
98
174
  function projectValue(value, projection) {
99
175
  if (projection === "value")
100
176
  return { value: structuredClone(value) };
@@ -184,17 +260,27 @@ export function readProjectedState(view, paths, projection = "value") {
184
260
  return { patch: patches.length === 1 ? patches[0] : patches };
185
261
  }
186
262
  const projected = paths.map((path) => {
187
- const { root, selectors } = parseValuePath(path);
188
- const query = parseStateReadPath(root);
189
- if (query.kind !== "state")
190
- throw new Error("Value and keys projections require a state path");
191
- const readsLazy = selectors[0]?.kind === "key" && selectors[0].key === "lazy";
192
- const state = readsLazy
193
- ? readTemporalState(view, query.offset, query.scope)
194
- : projectStateForModel(readTemporalState(view, query.offset, query.scope));
195
- if (readsLazy && !Object.hasOwn(state, "lazy"))
196
- state.lazy = {};
197
- return projectValue(selectValue(state, selectors, path), projection);
263
+ try {
264
+ const { root, selectors } = parseValuePath(path);
265
+ const query = parseStateReadPath(root);
266
+ if (query.kind !== "state")
267
+ throw new Error("Value and keys projections require a state path");
268
+ const readsLazy = selectors[0]?.kind === "key" && selectors[0].key === "lazy";
269
+ const state = readsLazy
270
+ ? readTemporalState(view, query.offset, query.scope)
271
+ : projectStateForModel(readTemporalState(view, query.offset, query.scope));
272
+ if (readsLazy && !Object.hasOwn(state, "lazy"))
273
+ state.lazy = {};
274
+ return projectValue(selectValue(state, selectors, path), projection);
275
+ }
276
+ catch (error) {
277
+ const message = error instanceof Error ? error.message : String(error);
278
+ const missing = /does not exist| is outside /.test(message);
279
+ const hint = missing && projection === "value" && paths.length === 1 ? missingReferenceHint(view, path) : undefined;
280
+ if (hint)
281
+ return { value: null, hint };
282
+ throw new Error(message, error instanceof Error ? { cause: error } : undefined);
283
+ }
198
284
  });
199
285
  if (projected.length === 1)
200
286
  return projected[0];
@@ -7,6 +7,20 @@ interface BranchEntry {
7
7
  role?: unknown;
8
8
  };
9
9
  }
10
+ export interface SessionEntryLookup {
11
+ getLeafEntry(): (BranchEntry & {
12
+ id?: string;
13
+ parentId?: string | null;
14
+ }) | undefined;
15
+ getEntry(id: string): (BranchEntry & {
16
+ id?: string;
17
+ parentId?: string | null;
18
+ }) | undefined;
19
+ }
20
+ export interface PassiveStopBoundary {
21
+ at: number;
22
+ from?: number;
23
+ }
10
24
  export interface SnapshotDiscovery {
11
25
  candidates: unknown[];
12
26
  errors: string[];
@@ -18,4 +32,7 @@ export declare function latestSnapshotData(branch: readonly BranchEntry[]): unkn
18
32
  export declare function hasPriorConversation(branch: readonly BranchEntry[]): boolean;
19
33
  /** Auto-start eligibility is session identity/lifecycle, not the presence of CWD materialization. */
20
34
  export declare function isNewSession(reason: unknown, branch: readonly BranchEntry[]): boolean;
35
+ export declare function findAssistantToolBatch(session: SessionEntryLookup, toolCallId: string): string[] | undefined;
36
+ export declare function findPassiveStopBoundary(branch: readonly BranchEntry[], sessionId: string, entryType: string): PassiveStopBoundary | undefined;
37
+ export declare function retainsPhysicalSessionProjection(reason: unknown): boolean;
21
38
  export {};
@@ -42,3 +42,41 @@ export function isNewSession(reason, branch) {
42
42
  return true;
43
43
  return reason === "startup" && !hasPriorConversation(branch);
44
44
  }
45
+ export function findAssistantToolBatch(session, toolCallId) {
46
+ for (let cursor = session.getLeafEntry(); cursor; cursor = cursor.parentId ? session.getEntry(cursor.parentId) : undefined) {
47
+ if (cursor.type !== "message" || cursor.message?.role !== "assistant" || !Array.isArray(cursor.message.content))
48
+ continue;
49
+ const calls = cursor.message.content.filter((block) => {
50
+ return typeof block === "object" && block !== null
51
+ && block.type === "toolCall"
52
+ && typeof block.id === "string"
53
+ && typeof block.name === "string";
54
+ });
55
+ if (calls.some(({ id }) => id === toolCallId))
56
+ return calls.map(({ name }) => name);
57
+ }
58
+ return undefined;
59
+ }
60
+ export function findPassiveStopBoundary(branch, sessionId, entryType) {
61
+ for (const entry of [...branch].reverse()) {
62
+ try {
63
+ if (entry?.type !== "custom" || entry.customType !== entryType)
64
+ continue;
65
+ const { at, from, reset, owner } = entry.data ?? {};
66
+ if (reset === true && owner === sessionId)
67
+ return undefined;
68
+ if (typeof at === "number" && Number.isSafeInteger(at) && at >= 0)
69
+ return {
70
+ at,
71
+ ...(typeof from === "number" && Number.isSafeInteger(from) && from >= 0 ? { from } : {}),
72
+ };
73
+ }
74
+ catch {
75
+ // A hostile unrelated branch entry cannot manufacture or suppress a valid marker.
76
+ }
77
+ }
78
+ return undefined;
79
+ }
80
+ export function retainsPhysicalSessionProjection(reason) {
81
+ return reason === undefined || reason === "startup" || reason === "reload" || reason === "resume";
82
+ }