@llblab/pi-kit 0.37.1 → 0.38.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 (124) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/README.md +2 -2
  3. package/node_modules/@llblab/pi-state-flow/AGENTS.md +7 -3
  4. package/node_modules/@llblab/pi-state-flow/BACKLOG.md +679 -2
  5. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +17 -0
  6. package/node_modules/@llblab/pi-state-flow/README.md +30 -14
  7. package/node_modules/@llblab/pi-state-flow/api/modules.ts +11 -0
  8. package/node_modules/@llblab/pi-state-flow/dist/api/modules.d.ts +3 -0
  9. package/node_modules/@llblab/pi-state-flow/dist/api/modules.js +3 -0
  10. package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.d.ts +7 -4
  11. package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.js +77 -36
  12. package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.d.ts +2 -0
  13. package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.js +26 -27
  14. package/node_modules/@llblab/pi-state-flow/dist/lib/config.d.ts +3 -0
  15. package/node_modules/@llblab/pi-state-flow/dist/lib/config.js +13 -1
  16. package/node_modules/@llblab/pi-state-flow/dist/lib/context.d.ts +27 -21
  17. package/node_modules/@llblab/pi-state-flow/dist/lib/context.js +144 -46
  18. package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.js +14 -18
  19. package/node_modules/@llblab/pi-state-flow/dist/lib/durable.d.ts +6 -0
  20. package/node_modules/@llblab/pi-state-flow/dist/lib/durable.js +60 -50
  21. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +1049 -310
  22. package/node_modules/@llblab/pi-state-flow/dist/lib/history.d.ts +4 -4
  23. package/node_modules/@llblab/pi-state-flow/dist/lib/history.js +15 -3
  24. package/node_modules/@llblab/pi-state-flow/dist/lib/json.d.ts +2 -0
  25. package/node_modules/@llblab/pi-state-flow/dist/lib/json.js +2 -1
  26. package/node_modules/@llblab/pi-state-flow/dist/lib/memory.d.ts +3 -1
  27. package/node_modules/@llblab/pi-state-flow/dist/lib/memory.js +7 -7
  28. package/node_modules/@llblab/pi-state-flow/dist/lib/migration.d.ts +29 -0
  29. package/node_modules/@llblab/pi-state-flow/dist/lib/migration.js +186 -0
  30. package/node_modules/@llblab/pi-state-flow/dist/lib/model-interface.d.ts +17 -0
  31. package/node_modules/@llblab/pi-state-flow/dist/lib/model-interface.js +158 -0
  32. package/node_modules/@llblab/pi-state-flow/dist/lib/module-registry.d.ts +21 -0
  33. package/node_modules/@llblab/pi-state-flow/dist/lib/module-registry.js +108 -0
  34. package/node_modules/@llblab/pi-state-flow/dist/lib/operation.js +3 -2
  35. package/node_modules/@llblab/pi-state-flow/dist/lib/ownership.d.ts +4 -4
  36. package/node_modules/@llblab/pi-state-flow/dist/lib/ownership.js +5 -5
  37. package/node_modules/@llblab/pi-state-flow/dist/lib/planes.d.ts +29 -0
  38. package/node_modules/@llblab/pi-state-flow/dist/lib/planes.js +84 -0
  39. package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.d.ts +6 -3
  40. package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +33 -16
  41. package/node_modules/@llblab/pi-state-flow/dist/lib/query.d.ts +21 -4
  42. package/node_modules/@llblab/pi-state-flow/dist/lib/query.js +53 -13
  43. package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.js +5 -2
  44. package/node_modules/@llblab/pi-state-flow/dist/lib/rehydration.d.ts +3 -0
  45. package/node_modules/@llblab/pi-state-flow/dist/lib/rehydration.js +10 -1
  46. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.d.ts +84 -5
  47. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.js +570 -20
  48. package/node_modules/@llblab/pi-state-flow/dist/lib/selection.d.ts +10 -0
  49. package/node_modules/@llblab/pi-state-flow/dist/lib/selection.js +32 -0
  50. package/node_modules/@llblab/pi-state-flow/dist/lib/semantic-module.d.ts +79 -0
  51. package/node_modules/@llblab/pi-state-flow/dist/lib/semantic-module.js +187 -0
  52. package/node_modules/@llblab/pi-state-flow/dist/lib/session.d.ts +98 -1
  53. package/node_modules/@llblab/pi-state-flow/dist/lib/session.js +398 -3
  54. package/node_modules/@llblab/pi-state-flow/dist/lib/skills.d.ts +7 -2
  55. package/node_modules/@llblab/pi-state-flow/dist/lib/skills.js +55 -3
  56. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.d.ts +67 -14
  57. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.js +129 -9
  58. package/node_modules/@llblab/pi-state-flow/dist/lib/state.d.ts +26 -14
  59. package/node_modules/@llblab/pi-state-flow/dist/lib/state.js +61 -19
  60. package/node_modules/@llblab/pi-state-flow/dist/lib/status.d.ts +14 -9
  61. package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +59 -22
  62. package/node_modules/@llblab/pi-state-flow/dist/lib/storage.d.ts +111 -10
  63. package/node_modules/@llblab/pi-state-flow/dist/lib/storage.js +728 -11
  64. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.d.ts +10 -7
  65. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +50 -14
  66. package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.d.ts +40 -4
  67. package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.js +247 -43
  68. package/node_modules/@llblab/pi-state-flow/dist/lib/transition.d.ts +41 -8
  69. package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +132 -39
  70. package/node_modules/@llblab/pi-state-flow/dist/package.json +6 -1
  71. package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-guide/SKILL.md +28 -16
  72. package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-memory/SKILL.md +12 -8
  73. package/node_modules/@llblab/pi-state-flow/docs/README.md +9 -0
  74. package/node_modules/@llblab/pi-state-flow/docs/architecture.md +76 -43
  75. package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +9 -1
  76. package/node_modules/@llblab/pi-state-flow/docs/fork-contract.md +6 -0
  77. package/node_modules/@llblab/pi-state-flow/docs/lazy-state.md +23 -19
  78. package/node_modules/@llblab/pi-state-flow/docs/protocol-composition-decision.md +66 -0
  79. package/node_modules/@llblab/pi-state-flow/docs/public-module-contract.md +73 -0
  80. package/node_modules/@llblab/pi-state-flow/docs/scope-selection-contract.md +814 -0
  81. package/node_modules/@llblab/pi-state-flow/docs/semantic-compatibility-decision.md +212 -0
  82. package/node_modules/@llblab/pi-state-flow/docs/semantic-extensibility-decision.md +75 -0
  83. package/node_modules/@llblab/pi-state-flow/docs/semantic-handoff-contract.md +174 -0
  84. package/node_modules/@llblab/pi-state-flow/docs/semantic-memory-evaluation.md +166 -0
  85. package/node_modules/@llblab/pi-state-flow/docs/semantic-module-contract.md +84 -0
  86. package/node_modules/@llblab/pi-state-flow/docs/single-scope-interface-contract.md +201 -0
  87. package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +22 -1
  88. package/node_modules/@llblab/pi-state-flow/docs/usage.md +32 -18
  89. package/node_modules/@llblab/pi-state-flow/lib/acquisition.ts +68 -31
  90. package/node_modules/@llblab/pi-state-flow/lib/artifact.ts +21 -22
  91. package/node_modules/@llblab/pi-state-flow/lib/config.ts +14 -1
  92. package/node_modules/@llblab/pi-state-flow/lib/context.ts +142 -46
  93. package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +14 -17
  94. package/node_modules/@llblab/pi-state-flow/lib/durable.ts +53 -44
  95. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +897 -297
  96. package/node_modules/@llblab/pi-state-flow/lib/history.ts +16 -7
  97. package/node_modules/@llblab/pi-state-flow/lib/json.ts +2 -1
  98. package/node_modules/@llblab/pi-state-flow/lib/memory.ts +10 -9
  99. package/node_modules/@llblab/pi-state-flow/lib/migration.ts +189 -0
  100. package/node_modules/@llblab/pi-state-flow/lib/model-interface.ts +136 -0
  101. package/node_modules/@llblab/pi-state-flow/lib/module-registry.ts +119 -0
  102. package/node_modules/@llblab/pi-state-flow/lib/operation.ts +3 -2
  103. package/node_modules/@llblab/pi-state-flow/lib/ownership.ts +6 -6
  104. package/node_modules/@llblab/pi-state-flow/lib/planes.ts +98 -0
  105. package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +37 -16
  106. package/node_modules/@llblab/pi-state-flow/lib/query.ts +59 -18
  107. package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +5 -2
  108. package/node_modules/@llblab/pi-state-flow/lib/rehydration.ts +10 -1
  109. package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +574 -21
  110. package/node_modules/@llblab/pi-state-flow/lib/selection.ts +39 -0
  111. package/node_modules/@llblab/pi-state-flow/lib/semantic-module.ts +239 -0
  112. package/node_modules/@llblab/pi-state-flow/lib/session.ts +387 -3
  113. package/node_modules/@llblab/pi-state-flow/lib/skills.ts +45 -4
  114. package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +156 -16
  115. package/node_modules/@llblab/pi-state-flow/lib/state.ts +69 -28
  116. package/node_modules/@llblab/pi-state-flow/lib/status.ts +65 -31
  117. package/node_modules/@llblab/pi-state-flow/lib/storage.ts +770 -18
  118. package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +58 -20
  119. package/node_modules/@llblab/pi-state-flow/lib/temporal.ts +267 -50
  120. package/node_modules/@llblab/pi-state-flow/lib/transition.ts +184 -52
  121. package/node_modules/@llblab/pi-state-flow/package.json +6 -1
  122. package/node_modules/@llblab/pi-state-flow/skills/state-flow-guide/SKILL.md +28 -16
  123. package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +12 -8
  124. package/package.json +2 -2
@@ -1,7 +1,187 @@
1
- import { parseRetainedPiCheckpoint, readCheckpointMode, type InactiveMode, type StateFlowMode } from "./snapshot.ts";
1
+ import { parseRetainedPiCheckpoint, prepareRetainedPolicyMigration, readCheckpointMode, isStateFlowMode, type InactiveMode, type StateFlowMode, type RetainedPolicyMigrationCandidate } from "./snapshot.ts";
2
+ import { captureScopeSelection, DEFAULT_SCOPE_SELECTION, type ScopeSelection } from "./selection.ts";
3
+ import { isJsonValue, isObject } from "./json.ts";
4
+ import { isAbsolute, resolve } from "node:path";
5
+ import { RenewableLifetime } from "./operation.ts";
2
6
 
3
7
  export const SNAPSHOT_ENTRY_TYPE = "state-flow-snapshot";
4
8
 
9
+ /** Requested native policy, not proof of accepted memory or acquisition permission. */
10
+ export interface SelectedNativeRequestPolicy {
11
+ readonly cwd: string;
12
+ readonly owner: string;
13
+ readonly mode: StateFlowMode;
14
+ readonly scopes: ScopeSelection;
15
+ }
16
+
17
+ /** Cancelable pre-provider turn, including Off bookkeeping; not memory/acquisition authority. */
18
+ export type NativeContextRequest = Pick<SelectedPolicyRequest, "signal" | "revalidate">;
19
+
20
+ export interface SelectedPolicyRequest {
21
+ readonly policy: SelectedNativeRequestPolicy;
22
+ readonly signal: AbortSignal;
23
+ /** Synchronous in-memory recheck, suitable for the selected runtime's native-policy port. */
24
+ revalidate(): void;
25
+ }
26
+
27
+ function captureRequestPolicy(value: SelectedNativeRequestPolicy): SelectedNativeRequestPolicy {
28
+ try {
29
+ if (!isObject(value) || Reflect.ownKeys(value).length !== 4
30
+ || !["cwd", "owner", "mode", "scopes"].every(key => Object.hasOwn(value, key))) throw new Error();
31
+ const { cwd, owner, mode, scopes } = value;
32
+ if (typeof cwd !== "string" || !isAbsolute(cwd) || typeof owner !== "string" || !owner.trim() || !isStateFlowMode(mode)) throw new Error();
33
+ return Object.freeze({ cwd: resolve(cwd), owner, mode, scopes: captureScopeSelection(scopes) });
34
+ } catch {
35
+ throw new Error("Invalid State Flow selected request policy");
36
+ }
37
+ }
38
+
39
+ /** Owns policy-bound cancellation only; no storage, attachment, source acquisition or slot registry. */
40
+ export class SelectedPolicyLifetime {
41
+ readonly #lifetime = new RenewableLifetime();
42
+ #policy: SelectedNativeRequestPolicy | undefined;
43
+ #ended = false;
44
+
45
+ get policy(): SelectedNativeRequestPolicy | undefined { return this.#policy; }
46
+
47
+ /** Cancellation only, including Off; unlike capture(), this grants no enabled memory receipt. */
48
+ get signal(): AbortSignal { return this.#lifetime.signal; }
49
+
50
+ /** Same captured policy preserves independent owners; a real choice revokes all old requests. */
51
+ select(value: SelectedNativeRequestPolicy): void {
52
+ if (this.#ended) throw new Error("State Flow selected policy lifetime has ended");
53
+ const next = captureRequestPolicy(value), previous = this.#policy;
54
+ if (previous && previous.cwd === next.cwd && previous.owner === next.owner && previous.mode === next.mode
55
+ && previous.scopes.length === next.scopes.length && previous.scopes.every((scope, index) => scope === next.scopes[index])) return;
56
+ this.#policy = next;
57
+ this.#lifetime.renew(new Error("State Flow selected native policy changed"));
58
+ }
59
+
60
+ /** Capture at admission, before any source acquisition, asynchronous wait or queued work. */
61
+ capture(): SelectedPolicyRequest {
62
+ const policy = this.#policy, signal = this.#lifetime.signal;
63
+ if (this.#ended || !policy || signal.aborted) throw new Error("State Flow selected policy lifetime is unavailable");
64
+ if (policy.mode === "off") throw new Error("Off cannot capture State Flow memory requests");
65
+ const revalidate = () => {
66
+ if (this.#ended || this.#policy !== policy || this.#lifetime.signal !== signal || signal.aborted)
67
+ throw new Error("State Flow selected policy request is no longer current");
68
+ };
69
+ return Object.freeze({ policy, signal, revalidate });
70
+ }
71
+
72
+ /** Same-policy branch movement/Abort may revoke requests without inventing a different selection. */
73
+ invalidate(): void {
74
+ if (!this.#ended) this.#lifetime.renew(new Error("State Flow selected native request lifetime changed"));
75
+ }
76
+
77
+ /** Shutdown cannot be reopened by a late selection or abort listener. */
78
+ end(): void {
79
+ this.#ended = true;
80
+ this.#policy = undefined;
81
+ this.#lifetime.end(new Error("State Flow selected policy lifetime has ended"));
82
+ }
83
+ }
84
+
85
+ /** One SDK turn and its finalized assistant batch; IDs are evidence only within this owned turn. */
86
+ export class NativeToolRequestCorrelation {
87
+ #turn: { request?: SelectedPolicyRequest; signal?: AbortSignal; currentIdentity: () => boolean; finalized: boolean; controller: AbortController; prepared?: () => void } | undefined;
88
+ #ambiguous = false;
89
+ #context: NativeContextRequest | undefined;
90
+ #operation: AbortSignal | undefined;
91
+ readonly #calls = new Map<string, { name: string; consumed: boolean; parent?: string }>();
92
+
93
+ begin(request: SelectedPolicyRequest | undefined, signal: AbortSignal | undefined, currentIdentity: () => boolean, controlSignal?: AbortSignal): void {
94
+ this.#operation = signal;
95
+ // An overlapping turn has no public request ID to disambiguate its later messages.
96
+ if (this.#turn || this.#ambiguous) { this.end(); this.#ambiguous = true; return; }
97
+ this.#turn = { request, signal: controlSignal ? AbortSignal.any([controlSignal, ...(signal ? [signal] : [])]) : signal, currentIdentity, finalized: false, controller: new AbortController() };
98
+ }
99
+
100
+ /** Stable lease from turn_start through provider preparation, before assistant finalization. */
101
+ preparing(): NativeContextRequest {
102
+ const turn = this.#turn;
103
+ const revalidate = () => {
104
+ if (this.#ambiguous || !turn || this.#turn !== turn || turn.finalized || turn.signal?.aborted || !turn.currentIdentity())
105
+ throw new Error("State Flow context has no current native request");
106
+ turn.request?.revalidate();
107
+ turn.prepared?.();
108
+ };
109
+ revalidate();
110
+ return this.#context ??= Object.freeze({
111
+ signal: AbortSignal.any([turn!.controller.signal, ...(turn!.signal ? [turn!.signal] : []), ...(turn!.request ? [turn!.request.signal] : [])]),
112
+ revalidate,
113
+ });
114
+ }
115
+
116
+ /** Provider-only revalidation of captured context data; callbacks must neither acquire nor recurse into this request. */
117
+ bindPreparedContext(check: () => void): void {
118
+ this.preparing().revalidate();
119
+ const turn = this.#turn!;
120
+ check();
121
+ if (this.#turn !== turn) throw new Error("State Flow prepared context owner changed");
122
+ turn.prepared = check;
123
+ }
124
+
125
+ finalize(calls: readonly { id: string; name: string }[]): void {
126
+ if (!this.#turn || this.#ambiguous) return;
127
+ if (this.#turn.finalized) { this.end(); this.#ambiguous = true; return; }
128
+ this.#turn.finalized = true;
129
+ for (const { id, name } of calls) {
130
+ if (!id || !name || this.#calls.has(id)) { this.end(); this.#ambiguous = true; return; }
131
+ this.#calls.set(id, { name, consumed: false });
132
+ }
133
+ }
134
+
135
+ get active(): boolean { return this.#turn !== undefined || this.#ambiguous; }
136
+
137
+ /** Identify the ambient model owner; deciding which Abort cause to forward belongs to the native port. */
138
+ ownsOperation(signal: AbortSignal | undefined): boolean { return signal !== undefined && signal === this.#operation; }
139
+
140
+ has(id: string): boolean { return this.#calls.has(id); }
141
+
142
+ current(): SelectedPolicyRequest {
143
+ const turn = this.#turn;
144
+ if (this.#ambiguous || !turn?.request || !turn.finalized || turn.signal?.aborted || !turn.currentIdentity())
145
+ throw new Error("State Flow tool batch has no current native request");
146
+ turn.request.revalidate();
147
+ return turn.request;
148
+ }
149
+
150
+ check(id: string, name: string): SelectedPolicyRequest {
151
+ const request = this.current(), call = this.#calls.get(id);
152
+ if (!call || call.name !== name) throw new Error("State Flow tool batch has no current native request");
153
+ return request;
154
+ }
155
+
156
+ /** Only explicit SDK parent evidence, never an ID prefix or branch-history guess. */
157
+ nested(id: string, name: string, parent: string): void {
158
+ const call = this.#calls.get(parent), previous = this.#calls.get(id);
159
+ if (!id || !call || previous && (previous.name !== name || previous.parent !== parent)) throw new Error("State Flow nested tool has no current native parent");
160
+ this.check(parent, call.name);
161
+ if (!previous) this.#calls.set(id, { name, consumed: false, parent });
162
+ }
163
+
164
+ take(id: string, name: string): SelectedPolicyRequest {
165
+ const request = this.check(id, name), call = this.#calls.get(id)!;
166
+ if (call.consumed) throw new Error("State Flow native tool admission was already consumed");
167
+ call.consumed = true;
168
+ const turn = this.#turn!;
169
+ return Object.freeze({ policy: request.policy,
170
+ signal: AbortSignal.any([request.signal, turn.controller.signal, ...(turn.signal ? [turn.signal] : [])]),
171
+ revalidate: () => { if (this.#turn !== turn) throw new Error("State Flow native request turn has ended"); this.check(id, name); },
172
+ });
173
+ }
174
+
175
+ end(): void {
176
+ const previous = this.#turn;
177
+ this.#turn = undefined;
178
+ this.#ambiguous = false;
179
+ this.#context = undefined;
180
+ this.#calls.clear();
181
+ previous?.controller.abort();
182
+ }
183
+ }
184
+
5
185
  interface BranchEntry {
6
186
  type?: unknown;
7
187
  customType?: unknown;
@@ -78,6 +258,205 @@ export function findBranchPolicy(
78
258
  return undefined;
79
259
  }
80
260
 
261
+ /** Checkpoint-only preflight: no Stop/fork precedence, installation or authority. Invalid owned data cannot fall back. */
262
+ export function prepareBranchCheckpointPolicy(
263
+ branch: readonly BranchEntry[], legacySelection: ScopeSelection, inactiveMode: InactiveMode = "passive",
264
+ ): RetainedPolicyMigrationCandidate | undefined {
265
+ for (let index = branch.length - 1; index >= 0; index--) {
266
+ let entry: BranchEntry;
267
+ try {
268
+ entry = branch[index];
269
+ if (entry?.type !== "custom" || entry.customType !== SNAPSHOT_ENTRY_TYPE) continue;
270
+ } catch {
271
+ // Unclassifiable foreign entries grant no policy authority.
272
+ continue;
273
+ }
274
+ return prepareRetainedPolicyMigration(entry.data, legacySelection, inactiveMode);
275
+ }
276
+ return undefined;
277
+ }
278
+
279
+ /** Native-only policy/bookkeeping. This marker never grants canonical memory or parent-copy authority. */
280
+ export interface SelectedNativePolicyMarker {
281
+ version: 2;
282
+ owner: string;
283
+ mode: StateFlowMode;
284
+ scopes: ScopeSelection;
285
+ reset?: true;
286
+ forkPending?: true;
287
+ at?: number;
288
+ from?: number;
289
+ preserveContext?: true;
290
+ persistenceError?: string;
291
+ memoryDeferred?: true;
292
+ }
293
+
294
+ type ParsedNativeMarker = Omit<SelectedNativePolicyMarker, "version" | "owner" | "mode" | "scopes"> & {
295
+ version: "legacy" | 2; owner?: string; mode?: StateFlowMode; scopes?: ScopeSelection;
296
+ };
297
+
298
+ function nativeMarkerVersion(value: Record<string, unknown>): "legacy" | 2 {
299
+ let version: unknown;
300
+ try { if (!Object.hasOwn(value, "version")) return "legacy"; version = value.version; }
301
+ catch { throw new Error("Unreadable State Flow native policy marker version"); }
302
+ if (version !== 2) throw new Error("Unsupported State Flow native policy marker version");
303
+ return 2;
304
+ }
305
+
306
+ function parseNativeMarker(value: unknown): ParsedNativeMarker {
307
+ if (!isObject(value)) throw new Error("Invalid State Flow native policy marker");
308
+ const version = nativeMarkerVersion(value);
309
+ if (version === "legacy" && Object.hasOwn(value, "scopes")) throw new Error("Target policy fields in legacy State Flow native marker");
310
+ const keys = ["version", "owner", "mode", "scopes", "reset", "forkPending", "at", "from", "preserveContext", "persistenceError", "memoryDeferred"];
311
+ if (Object.keys(value).some(key => !keys.includes(key)) || !isJsonValue(value)) throw new Error("Invalid State Flow native policy marker");
312
+ const { owner, mode, reset, forkPending, at, from: rawFrom, preserveContext, persistenceError, memoryDeferred } = value;
313
+ // Legacy Stop treated an invalid optional anchor as absent, retaining the valid cutoff.
314
+ const from = version === "legacy" && (!Number.isSafeInteger(rawFrom) || (rawFrom as number) < 0) ? undefined : rawFrom;
315
+ if ((owner !== undefined && (typeof owner !== "string" || !owner.trim()))
316
+ || (mode !== undefined && !isStateFlowMode(mode))
317
+ || (version === 2 && (typeof owner !== "string" || !isStateFlowMode(mode) || !Object.hasOwn(value, "scopes")))
318
+ || [reset, forkPending, preserveContext, memoryDeferred].some(flag => flag !== undefined && flag !== true)
319
+ || [at, from].some(time => time !== undefined && (!Number.isSafeInteger(time) || (time as number) < 0))
320
+ || (persistenceError !== undefined && (typeof persistenceError !== "string" || !persistenceError.trim()))
321
+ || ((from !== undefined || preserveContext !== undefined || persistenceError !== undefined || memoryDeferred !== undefined) && at === undefined)
322
+ || (at !== undefined && mode === "active")
323
+ || (memoryDeferred === true && (mode !== "off" || owner === undefined))
324
+ || ((reset === true || forkPending === true) && owner === undefined)
325
+ || (reset === true && (forkPending !== undefined || at !== undefined || from !== undefined || preserveContext !== undefined || persistenceError !== undefined || memoryDeferred !== undefined))
326
+ || (version === "legacy" && at === undefined && reset !== true && forkPending !== true)
327
+ || (version === "legacy" && reset === true && mode !== undefined)) throw new Error("Invalid State Flow native policy marker");
328
+ return {
329
+ version, ...(owner === undefined ? {} : { owner: owner as string }), ...(mode === undefined ? {} : { mode: mode as StateFlowMode }),
330
+ ...(version === 2 ? { scopes: captureScopeSelection(value.scopes) } : {}),
331
+ ...(reset === true ? { reset: true } : {}), ...(forkPending === true ? { forkPending: true } : {}),
332
+ ...(at === undefined ? {} : { at: at as number }), ...(from === undefined ? {} : { from: from as number }),
333
+ ...(preserveContext === true ? { preserveContext: true } : {}), ...(memoryDeferred === true ? { memoryDeferred: true } : {}),
334
+ ...(persistenceError === undefined ? {} : { persistenceError: persistenceError as string }),
335
+ };
336
+ }
337
+
338
+ /** Closed v2 writer value. Legacy records are translated only in branch context, never given a fabricated owner. */
339
+ export function captureSelectedNativeMarker(value: unknown): SelectedNativePolicyMarker {
340
+ const marker = parseNativeMarker(value);
341
+ if (marker.version !== 2) throw new Error("Selected State Flow native marker requires version 2");
342
+ return marker as SelectedNativePolicyMarker;
343
+ }
344
+
345
+ export interface SelectedBranchPolicyOptions {
346
+ owner: string;
347
+ markerEntryType: string;
348
+ /** The caller chooses new-session versus inactive fallback mode; absence itself grants no acquisition. */
349
+ defaults: { mode: StateFlowMode; scopes: ScopeSelection };
350
+ inactiveMode?: InactiveMode;
351
+ /** Native fresh-fork fact only; no parent header or store lookup belongs in this resolver. */
352
+ fork?: boolean;
353
+ }
354
+
355
+ export interface SelectedBranchPolicyCandidate {
356
+ mode: StateFlowMode;
357
+ scopes: ScopeSelection;
358
+ source: "default" | "checkpoint" | "marker";
359
+ forkPending: boolean;
360
+ stop?: PassiveStopBoundary;
361
+ persistenceError?: string;
362
+ requiresCapture: boolean;
363
+ /** Policy-only capture preserves fences; only a separately owned acceptance may append reset/checkpoint. */
364
+ capture: SelectedNativePolicyMarker;
365
+ }
366
+
367
+ /** Complete native branch admission only; never read canonical/parent files, attach policy or confer memory authority. */
368
+ export function prepareSelectedBranchPolicy(branch: readonly BranchEntry[], options: SelectedBranchPolicyOptions): SelectedBranchPolicyCandidate {
369
+ const { owner, markerEntryType } = options;
370
+ const inactiveMode = options.inactiveMode ?? "passive";
371
+ const fallbackMode = options.defaults?.mode;
372
+ if (typeof owner !== "string" || !owner.trim() || typeof markerEntryType !== "string" || !markerEntryType.trim() || markerEntryType === SNAPSHOT_ENTRY_TYPE
373
+ || !isStateFlowMode(fallbackMode) || (inactiveMode !== "passive" && inactiveMode !== "off")
374
+ || (options.fork !== undefined && typeof options.fork !== "boolean")) throw new Error("Invalid State Flow selected branch policy context");
375
+ let mode = fallbackMode, scopes = captureScopeSelection(options.defaults.scopes);
376
+ const classified: Array<{ type: "checkpoint" | "marker"; entry: BranchEntry; index: number }> = [];
377
+ for (let index = 0; index < branch.length; index++) {
378
+ try {
379
+ const entry = branch[index];
380
+ if (entry?.type !== "custom") continue;
381
+ if (entry.customType === SNAPSHOT_ENTRY_TYPE) classified.push({ type: "checkpoint", entry, index });
382
+ else if (entry.customType === markerEntryType) classified.push({ type: "marker", entry, index });
383
+ } catch { /* Unclassifiable foreign entries supply no policy. */ }
384
+ }
385
+ const data = new Map<number, unknown>(), decoded = new Map<number, ParsedNativeMarker>();
386
+ const read = (record: typeof classified[number]): unknown => {
387
+ if (!data.has(record.index)) {
388
+ try { data.set(record.index, record.entry.data); }
389
+ catch { throw new Error("Unreadable State Flow native policy data"); }
390
+ }
391
+ return data.get(record.index);
392
+ };
393
+ const markerOwner = (value: unknown): unknown => {
394
+ try { return isObject(value) ? value.owner : undefined; }
395
+ catch { throw new Error("Unreadable State Flow native policy owner"); }
396
+ };
397
+ const marker = (record: typeof classified[number]): ParsedNativeMarker => {
398
+ if (!decoded.has(record.index)) decoded.set(record.index, parseNativeMarker(read(record)));
399
+ return decoded.get(record.index)!;
400
+ };
401
+ let forkPending = options.fork === true;
402
+ // A later owned reset/pending record supersedes older fork evidence, not a checkpoint or mode-only choice.
403
+ for (const record of [...classified].reverse()) {
404
+ if (record.type !== "marker") continue;
405
+ const value = read(record);
406
+ if (markerOwner(value) !== owner || !isObject(value)) continue;
407
+ nativeMarkerVersion(value);
408
+ if (!Object.hasOwn(value, "reset") && !Object.hasOwn(value, "forkPending")) continue;
409
+ const selected = marker(record);
410
+ if (selected.reset) { forkPending = false; break; }
411
+ if (selected.forkPending) { forkPending = true; break; }
412
+ }
413
+ const inherit = options.fork === true || forkPending;
414
+ let localSeen = false, policyError = false, requiresCapture = true;
415
+ let source: SelectedBranchPolicyCandidate["source"] = "default";
416
+ let checkpointMode: StateFlowMode | undefined;
417
+ let stop: PassiveStopBoundary | undefined, persistenceError: string | undefined;
418
+ for (const record of classified) {
419
+ if (record.type === "checkpoint") {
420
+ try {
421
+ const selected = prepareRetainedPolicyMigration(read(record), scopes, inactiveMode);
422
+ mode = selected.checkpoint.mode; scopes = selected.checkpoint.scopes; checkpointMode = mode;
423
+ if (selected.sourceVersion === 2) policyError = false;
424
+ requiresCapture = selected.requiresConversion || inherit;
425
+ source = "checkpoint";
426
+ if (Object.hasOwn(selected.checkpoint, "boundary")) persistenceError = undefined;
427
+ } catch { policyError = true; }
428
+ continue;
429
+ }
430
+ const raw = read(record), physicalOwner = markerOwner(raw);
431
+ const local = physicalOwner === owner;
432
+ const inherited = typeof physicalOwner === "string" && physicalOwner.trim() && inherit && !localSeen;
433
+ if (physicalOwner !== undefined && !local && !inherited) continue;
434
+ try {
435
+ const selected = marker(record);
436
+ const policyOwner = local || inherited;
437
+ if (policyOwner) {
438
+ if (selected.version === 2) { scopes = selected.scopes!; mode = selected.mode!; policyError = false; source = "marker"; requiresCapture = !local; }
439
+ else if (selected.reset) { mode = checkpointMode ?? fallbackMode; source = "marker"; requiresCapture = true; }
440
+ else if (selected.memoryDeferred || selected.persistenceError) { mode = selected.memoryDeferred ? "off" : selected.mode ?? inactiveMode; source = "marker"; requiresCapture = true; }
441
+ if (local) localSeen = true;
442
+ }
443
+ if (local && selected.reset) { stop = undefined; persistenceError = undefined; }
444
+ if ((local || physicalOwner === undefined) && selected.at !== undefined) {
445
+ stop = { at: selected.at, ...(selected.from === undefined ? {} : { from: selected.from }),
446
+ ...(selected.preserveContext ? { preserveContext: true } : {}), ...(selected.mode === "passive" || selected.mode === "off" ? { mode: selected.mode } : {}),
447
+ ...(selected.memoryDeferred ? { memoryDeferred: true } : {}) };
448
+ if (local && selected.persistenceError) persistenceError = selected.persistenceError;
449
+ }
450
+ } catch { if (local || inherited || isObject(raw) && Object.hasOwn(raw, "version")) policyError = true; }
451
+ }
452
+ if (policyError) throw new Error("State Flow selected branch policy is invalid or unsupported");
453
+ scopes = captureScopeSelection(scopes);
454
+ const capture = captureSelectedNativeMarker({ version: 2, owner, mode, scopes, ...(forkPending ? { forkPending: true } : {}) });
455
+ return { mode, scopes, source, forkPending, requiresCapture, capture,
456
+ ...(stop === undefined ? {} : { stop: { ...stop, ...(persistenceError === undefined ? {} : { persistenceError }) } }),
457
+ ...(persistenceError === undefined ? {} : { persistenceError }) };
458
+ }
459
+
81
460
  /** Native policy bookkeeping retains an unacquired fork across extension reloads. */
82
461
  export function hasPendingFork(branch: readonly BranchEntry[], sessionId: string, entryType: string): boolean {
83
462
  for (let index = branch.length - 1; index >= 0; index--) {
@@ -126,8 +505,13 @@ export function hasUncheckpointedConversation(branch: readonly BranchEntry[]): b
126
505
  if (role === "user" || role === "assistant" || role === "toolResult") return true;
127
506
  }
128
507
  if (entry?.type === "custom" && entry.customType === SNAPSHOT_ENTRY_TYPE) {
129
- parseRetainedPiCheckpoint(entry.data);
130
- return false;
508
+ if (isObject(entry.data) && Object.hasOwn(entry.data, "version")) {
509
+ const retained = prepareRetainedPolicyMigration(entry.data, DEFAULT_SCOPE_SELECTION).checkpoint;
510
+ if (Object.hasOwn(retained, "boundary")) return false;
511
+ } else {
512
+ parseRetainedPiCheckpoint(entry.data);
513
+ return false;
514
+ }
131
515
  }
132
516
  } catch {
133
517
  // Malformed entries cannot prove that pending input was checkpointed.
@@ -1,4 +1,4 @@
1
- import { readFileSync } from "node:fs";
1
+ import { closeSync, constants, fstatSync, lstatSync, openSync, readFileSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
3
  import {
4
4
  hashArtifactSource,
@@ -7,7 +7,8 @@ import {
7
7
  type ArtifactRegistry,
8
8
  } from "./artifact.ts";
9
9
  import { isObject } from "./json.ts";
10
- import type { StateScope } from "./state.ts";
10
+ import { captureScopeSelection, DEFAULT_SCOPE_SELECTION, type ScopeSelection } from "./selection.ts";
11
+ import type { AtomicScopePatches, StateScope } from "./state.ts";
11
12
 
12
13
  export const SKILL_ARTIFACT_COMPILER = "skill-artifact-v1";
13
14
 
@@ -43,7 +44,20 @@ export function hasCompiledSkillArtifact(
43
44
  export type SkillSourceHasher = (source: string) => string;
44
45
 
45
46
  export function hashSkillSource(source: string): string {
46
- return hashArtifactSource(readFileSync(source));
47
+ const expected = lstatSync(source, { bigint: true });
48
+ if (!expected.isFile() || expected.isSymbolicLink()) throw new Error("Skill source must be a regular non-symlink file");
49
+ const descriptor = openSync(source, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
50
+ try {
51
+ const before = fstatSync(descriptor, { bigint: true });
52
+ if (!before.isFile() || before.dev !== expected.dev || before.ino !== expected.ino) throw new Error("Skill source changed before identity capture");
53
+ const bytes = readFileSync(descriptor);
54
+ const after = fstatSync(descriptor, { bigint: true });
55
+ const current = lstatSync(source, { bigint: true });
56
+ if (current.isSymbolicLink() || !current.isFile() || current.dev !== before.dev || current.ino !== before.ino
57
+ || after.size !== before.size || after.mtimeNs !== before.mtimeNs || after.ctimeNs !== before.ctimeNs
58
+ || current.size !== after.size || current.mtimeNs !== after.mtimeNs || current.ctimeNs !== after.ctimeNs) throw new Error("Skill source changed during identity capture");
59
+ return hashArtifactSource(bytes);
60
+ } finally { closeSync(descriptor); }
47
61
  }
48
62
 
49
63
  function readPath(toolName: unknown, args: unknown): string | undefined {
@@ -103,10 +117,32 @@ export class SkillReadTracker {
103
117
  readonly #pending = new Map<string, PendingRead>();
104
118
  readonly hashSource: SkillSourceHasher;
105
119
  readonly resolveRegistered: RegisteredSkillResolver;
120
+ #selection: ScopeSelection;
106
121
 
107
- constructor(hashSource: SkillSourceHasher = hashSkillSource, resolveRegistered: RegisteredSkillResolver = () => undefined) {
122
+ constructor(hashSource: SkillSourceHasher = hashSkillSource, resolveRegistered: RegisteredSkillResolver = () => undefined, selection: ScopeSelection = DEFAULT_SCOPE_SELECTION) {
108
123
  this.hashSource = hashSource;
109
124
  this.resolveRegistered = resolveRegistered;
125
+ this.#selection = captureScopeSelection(selection);
126
+ }
127
+
128
+ /** Selection changes revoke correlated reads; a disabled Skill still retains ownership-only volatile evidence. */
129
+ select(selection: ScopeSelection): void {
130
+ const wanted = captureScopeSelection(selection);
131
+ if (wanted.join(",") !== this.#selection.join(",")) this.clear();
132
+ this.#selection = wanted;
133
+ }
134
+
135
+ /** Rehash only attempted selected compilations; this body acquisition must run outside canonical exclusion. */
136
+ validateCompilations(patches: AtomicScopePatches): void {
137
+ for (const scope of Object.keys(patches)) if (!this.#selection.includes(scope as StateScope)) throw new Error(`State Flow ${scope} Skill compilation owner is excluded`);
138
+ for (const read of this.successful.values()) {
139
+ if (!this.#selection.includes(read.scope) || !Object.hasOwn(patches, read.scope)) continue;
140
+ const artifacts = patches[read.scope]?.artifacts;
141
+ if (!isObject(artifacts) || !Object.hasOwn(artifacts, read.path) || !isObject(artifacts[read.path])) continue;
142
+ const registered = this.resolveRegistered(read.path);
143
+ if (!registered || registered.path !== read.path || registered.scope !== read.scope) throw new Error(`Registered Skill ownership changed after acquisition: ${read.path}`);
144
+ if (!isArtifactHash(read.hash) || read.error !== undefined || this.hashSource(read.path) !== read.hash) throw new Error(`Skill source changed after acquisition: ${read.path}`);
145
+ }
110
146
  }
111
147
 
112
148
  clear(): void {
@@ -143,6 +179,11 @@ export class SkillReadTracker {
143
179
  if (!source) return undefined;
144
180
  const registered = this.resolveRegistered(source);
145
181
  if (!registered) return undefined;
182
+ if (!this.#selection.includes(registered.scope)) {
183
+ const read = { path: registered.path, scope: registered.scope };
184
+ this.successful.set(registered.path, read);
185
+ return read;
186
+ }
146
187
  let read: SuccessfulSkillRead;
147
188
  try {
148
189
  const hash = this.hashSource(registered.path);