@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
@@ -2,7 +2,8 @@ import { resolve } from "node:path";
2
2
  import { parseArtifactProvenanceRegistry, type ArtifactProvenanceRegistry } from "./artifact.ts";
3
3
  import { MAX_HISTORY_LIMIT } from "./history.ts";
4
4
  import { canonicalJson, isJsonValue, isObject, type JsonObject } from "./json.ts";
5
- import { validateTemporalLineage, type TransitionBoundary } from "./temporal.ts";
5
+ import { captureTemporalWitnesses, validateTemporalLineage, type ExcludedTransitionWitness, type TransitionBoundary } from "./temporal.ts";
6
+ import { captureScopeSelection, type ScopeSelection } from "./selection.ts";
6
7
  const MAX_RESTORED_STEP = Number.MAX_SAFE_INTEGER - 1;
7
8
  const MAX_LEGACY_VALIDATION_ATTEMPT = 7;
8
9
 
@@ -55,37 +56,92 @@ export interface StateFlowSnapshot {
55
56
 
56
57
  export type Snapshot = StateFlowSnapshot;
57
58
 
59
+ /** Capture closed caller-owned run fields, omitting only undefined optional in-memory fields. */
60
+ export function captureLifecycleSnapshot(value: Snapshot): Snapshot {
61
+ if (!isObject(value) || Object.keys(value).sort().join(",") !== "config,meta" || !isObject(value.config)
62
+ || Object.keys(value.config).join(",") !== "mode" || !isObject(value.meta)
63
+ || Object.keys(value.meta).some((key) => !["step", "specification", "bootstrap", "validation"].includes(key))) {
64
+ throw new Error("Selected State Flow lifecycle update contains non-owned fields");
65
+ }
66
+ const meta = Object.fromEntries(Object.entries(value.meta).filter(([, field]) => field !== undefined));
67
+ const supplied = { config: value.config, meta };
68
+ if (!isJsonValue(supplied)) throw new Error("Invalid State Flow lifecycle snapshot");
69
+ const normalized: Snapshot = { config: { mode: isStateFlowMode(value.config.mode) ? value.config.mode : "off" }, meta: restoredMeta(meta) };
70
+ if (meta.bootstrap === false) normalized.meta.bootstrap = false;
71
+ if (meta.step === Number.MAX_SAFE_INTEGER) normalized.meta.step = Number.MAX_SAFE_INTEGER;
72
+ if (canonicalJson(normalized) !== canonicalJson(supplied)) throw new Error("Invalid State Flow lifecycle snapshot");
73
+ return structuredClone(normalized);
74
+ }
75
+
58
76
  /** Failed Active selection retains its policy and fences inference until explicit recovery or an inactive choice. */
59
77
  export function isActiveRestorationBlocked(snapshot: Snapshot): boolean {
60
78
  return snapshot.config.mode === "active" && snapshot.meta.validation?.attempt === 0;
61
79
  }
62
80
 
81
+ interface LifecycleMetadata extends SnapshotMeta {
82
+ identity: { cwd: string; sessionId: string };
83
+ lineage: TransitionBoundary[];
84
+ [key: string]: unknown;
85
+ }
86
+
87
+ interface SessionRuntimeMetadata extends LifecycleMetadata {
88
+ /** Runtime-owned artifact compilation evidence; never projected as semantic state. */
89
+ artifacts?: ArtifactProvenanceRegistry;
90
+ temporal?: { checkpoint: TransitionBoundary; patches: TransitionBoundary[] };
91
+ }
92
+
63
93
  export interface SessionRuntime {
64
94
  config: SnapshotConfig;
65
- meta: SnapshotMeta & {
66
- version: 1;
67
- identity: { cwd: string; sessionId: string };
68
- lineage: TransitionBoundary[];
69
- /** Runtime-owned artifact compilation evidence; never projected as semantic state. */
70
- artifacts?: ArtifactProvenanceRegistry;
71
- temporal?: { checkpoint: TransitionBoundary; patches: TransitionBoundary[] };
72
- [key: string]: unknown;
73
- };
95
+ meta: SessionRuntimeMetadata & { version: 1 };
96
+ }
97
+
98
+ export type PrivateAcquisition = "unacquired" | "deferred" | "acquired";
99
+ export interface PrivateCausalBasis {
100
+ lineage: TransitionBoundary[];
101
+ causalWitnesses: ExcludedTransitionWitness[];
102
+ }
103
+
104
+ /** Retained private identity proof, never private values or acquisition permission. */
105
+ export function capturePrivateCausalBasis(value: unknown): PrivateCausalBasis {
106
+ if (!isJsonValue(value) || !isObject(value) || Object.keys(value).sort().join(",") !== "causalWitnesses,lineage") throw new Error("Invalid State Flow private causal basis");
107
+ const lineage: unknown = value.lineage;
108
+ validateTemporalLineage(lineage, MAX_HISTORY_LIMIT);
109
+ const causalWitnesses = captureTemporalWitnesses(lineage, value.causalWitnesses, MAX_HISTORY_LIMIT);
110
+ const witnessed = new Set(causalWitnesses.map(witness => witness.transition.id));
111
+ if (lineage.slice(1).some(boundary => !witnessed.has(boundary.id))) throw new Error("Insufficient State Flow private causal basis witnesses");
112
+ return structuredClone({ lineage, causalWitnesses });
113
+ }
114
+
115
+ export interface SelectedSessionRuntime {
116
+ config: SnapshotConfig & { scopes: ScopeSelection };
117
+ meta: LifecycleMetadata & { version: 2; privateAcquisition: PrivateAcquisition; privateCausalBasis?: PrivateCausalBasis };
74
118
  }
75
119
 
76
120
  export function validateSessionRuntime(value: unknown, cwd: string, sessionId: string): asserts value is SessionRuntime {
121
+ validateRuntimeVersion(value, cwd, sessionId, 1);
122
+ }
123
+
124
+ /** Shared syntax checks do not widen native admission or prove private-stream acquisition. */
125
+ function validateRuntimeVersion(value: unknown, cwd: string, sessionId: string, expectedVersion: 1 | 2): void {
77
126
  if (!isJsonValue(value) || !isObject(value) || Object.keys(value).sort().join(",") !== "config,meta"
78
127
  || !isObject(value.config) || !isObject(value.meta)) throw new Error("Invalid State Flow session runtime envelope");
79
128
  const { version, identity, lineage, artifacts, temporal: _temporalScope, ...fields } = value.meta;
129
+ if (!Object.hasOwn(value.meta, "version") || version !== expectedVersion) throw new Error("Unsupported State Flow runtime provenance format");
80
130
  if (artifacts !== undefined) parseArtifactProvenanceRegistry(artifacts, "State Flow session artifact provenance");
81
- if (version !== 1) throw new Error("Unsupported State Flow runtime provenance format");
131
+ if (expectedVersion === 2 && (!Object.hasOwn(value.meta, "privateAcquisition") || !["unacquired", "deferred", "acquired"].includes(value.meta.privateAcquisition as string))) {
132
+ throw new Error("Invalid State Flow private acquisition provenance");
133
+ }
134
+ if (Object.hasOwn(value.meta, "privateCausalBasis")) {
135
+ if (expectedVersion !== 2 || value.meta.privateAcquisition !== "acquired") throw new Error("Invalid State Flow private causal basis provenance");
136
+ capturePrivateCausalBasis(value.meta.privateCausalBasis);
137
+ }
82
138
  if (!isObject(identity) || Object.keys(identity).sort().join(",") !== "cwd,sessionId"
83
139
  || identity.cwd !== resolve(cwd) || identity.sessionId !== sessionId || sessionId.trim().length === 0 || sessionId !== sessionId.trim()) {
84
140
  throw new Error("State Flow runtime scope identity mismatch");
85
141
  }
86
142
  validateTemporalLineage(lineage, MAX_HISTORY_LIMIT);
87
143
  const known = new Set(["step", "specification", "validation", "bootstrap"]);
88
- if (Object.keys(fields).some((key) => ["state", "contract", "working", "response"].includes(key))) {
144
+ if (Object.keys(fields).some((key) => ["state", "strategy", "tactics", "contract", "working", "response"].includes(key))) {
89
145
  throw new Error("Semantic state does not belong in State Flow runtime metadata");
90
146
  }
91
147
  const runtimeFields = Object.fromEntries(Object.entries(fields).filter(([key]) => known.has(key)));
@@ -95,7 +151,11 @@ export function validateSessionRuntime(value: unknown, cwd: string, sessionId: s
95
151
  };
96
152
  if (runtimeFields.bootstrap === false) normalized.meta.bootstrap = false;
97
153
  if (runtimeFields.step === Number.MAX_SAFE_INTEGER) normalized.meta.step = Number.MAX_SAFE_INTEGER;
98
- if (canonicalJson({ config: normalized.config, meta: normalized.meta }) !== canonicalJson({ config: value.config, meta: runtimeFields })) {
154
+ if (expectedVersion === 2 && !Object.hasOwn(value.config, "scopes")) throw new Error("State Flow lifecycle v2 requires explicit scopes");
155
+ const scopes = expectedVersion === 2 ? captureScopeSelection(value.config.scopes) : undefined;
156
+ const normalizedConfig = scopes === undefined ? normalized.config : { ...normalized.config, scopes };
157
+ const suppliedConfig = scopes === undefined ? value.config : { ...value.config, scopes };
158
+ if (canonicalJson({ config: normalizedConfig, meta: normalized.meta }) !== canonicalJson({ config: suppliedConfig, meta: runtimeFields })) {
99
159
  throw new Error("Invalid State Flow runtime configuration or counters");
100
160
  }
101
161
  }
@@ -138,9 +198,7 @@ export function parseSessionRuntime(
138
198
  let runtime: unknown;
139
199
  try {
140
200
  const settings: unknown = JSON.parse(config);
141
- const mode = isObject(settings) && Object.keys(settings).length === 1 ? readCheckpointMode(settings, "passive") : undefined;
142
- if (mode === undefined) throw new Error("Invalid State Flow runtime configuration or counters");
143
- runtime = { config: { mode }, meta: JSON.parse(runtimeSource) };
201
+ runtime = { config: legacySessionConfig(settings), meta: JSON.parse(runtimeSource) };
144
202
  } catch (error) {
145
203
  if (error instanceof SyntaxError) throw new Error("State Flow session runtime contains invalid JSON");
146
204
  throw error;
@@ -150,6 +208,55 @@ export function parseSessionRuntime(
150
208
  return { config: { mode: runtime.config.mode }, meta: runtimeMeta };
151
209
  }
152
210
 
211
+ function legacySessionConfig(settings: unknown): SnapshotConfig {
212
+ const mode = isObject(settings) && Object.keys(settings).length === 1 ? readCheckpointMode(settings, "passive") : undefined;
213
+ if (mode === undefined) throw new Error("Invalid State Flow runtime configuration or counters");
214
+ return { mode };
215
+ }
216
+
217
+ export interface SessionPolicyMigrationCandidate {
218
+ sourceVersion: 1 | 2;
219
+ targetVersion: 2;
220
+ requiresConversion: boolean;
221
+ /** Original strings, not file observations or publication authority. */
222
+ source: { config: string; runtime: string };
223
+ runtime: SelectedSessionRuntime;
224
+ target: { config: string; runtime: string };
225
+ }
226
+
227
+ /** Detached lifecycle preflight only; current native readers/writers still admit version 1 exclusively. */
228
+ export function prepareSessionPolicyMigration(
229
+ config: string | undefined, runtimeSource: string | undefined, cwd: string, sessionId: string, legacySelection: ScopeSelection,
230
+ ): SessionPolicyMigrationCandidate | undefined {
231
+ if (config === undefined && runtimeSource === undefined) return undefined;
232
+ if (config === undefined || runtimeSource === undefined) throw new Error("Incomplete State Flow config/runtime pair");
233
+ const metadata = parsePolicyDocument(runtimeSource);
234
+ if (!isObject(metadata) || !Object.hasOwn(metadata, "version") || (metadata.version !== 1 && metadata.version !== 2)) throw new Error("Unsupported State Flow runtime provenance format");
235
+ const sourceVersion = metadata.version;
236
+ const settings = parsePolicyDocument(config);
237
+ if (sourceVersion === 1 && (Object.hasOwn(metadata, "privateAcquisition") || Object.hasOwn(metadata, "causalWitnesses") || Object.hasOwn(metadata, "privateCausalBasis") || (isObject(settings) && Object.hasOwn(settings, "scopes")))) {
238
+ throw new Error("Target State Flow policy fields in legacy lifecycle format");
239
+ }
240
+ const sourceValue = { config: sourceVersion === 1 ? legacySessionConfig(settings) : settings, meta: metadata };
241
+ validateRuntimeVersion(sourceValue, cwd, sessionId, sourceVersion);
242
+ const scopes = captureScopeSelection(sourceVersion === 1 ? legacySelection : (settings as JsonObject).scopes);
243
+ const runtime = {
244
+ config: { ...(sourceValue.config as SnapshotConfig), scopes },
245
+ meta: { ...metadata, version: 2, ...(sourceVersion === 1 ? { privateAcquisition: "acquired" } : {}) },
246
+ } as SelectedSessionRuntime;
247
+ validateRuntimeVersion(runtime, cwd, sessionId, 2);
248
+ return {
249
+ sourceVersion, targetVersion: 2, requiresConversion: sourceVersion === 1,
250
+ source: { config, runtime: runtimeSource }, runtime,
251
+ target: { config: `${canonicalJson(runtime.config)}\n`, runtime: `${canonicalJson(runtime.meta)}\n` },
252
+ };
253
+ }
254
+
255
+ function parsePolicyDocument(source: string): unknown {
256
+ try { return JSON.parse(source); }
257
+ catch { throw new Error("State Flow session policy contains invalid JSON"); }
258
+ }
259
+
153
260
  function restoredStep(value: unknown): number {
154
261
  return typeof value === "number"
155
262
  && Number.isSafeInteger(value)
@@ -252,6 +359,39 @@ export function parseRetainedPiCheckpoint(value: unknown, inactiveMode: Inactive
252
359
  };
253
360
  }
254
361
 
362
+ export type SelectedRetainedPiCheckpoint = RetainedPiCheckpoint & { version: 2; scopes: ScopeSelection };
363
+ export interface RetainedPolicyMigrationCandidate {
364
+ sourceVersion: "legacy" | 2;
365
+ targetVersion: 2;
366
+ requiresConversion: boolean;
367
+ checkpoint: SelectedRetainedPiCheckpoint;
368
+ }
369
+
370
+ /** Native policy value preflight; no retained boundary, fork or acquisition authority is created. */
371
+ export function prepareRetainedPolicyMigration(
372
+ value: unknown, legacySelection: ScopeSelection, inactiveMode: InactiveMode = "passive",
373
+ ): RetainedPolicyMigrationCandidate {
374
+ if (!isObject(value)) throw new Error("Invalid State Flow policy checkpoint");
375
+ const legacy = !Object.hasOwn(value, "version");
376
+ let declaredVersion: unknown;
377
+ try { if (!legacy) declaredVersion = value.version; }
378
+ catch { throw new Error("Invalid State Flow policy checkpoint"); }
379
+ if (!legacy && declaredVersion !== 2) throw new Error("Unsupported State Flow native policy version");
380
+ const sourceVersion = legacy ? "legacy" : 2;
381
+ if (sourceVersion === "legacy" && Object.hasOwn(value, "scopes")) throw new Error("Target State Flow policy fields in legacy native checkpoint");
382
+ if (!isJsonValue(value)) throw new Error("Invalid State Flow policy checkpoint");
383
+ const { version: _version, scopes, ...lifecycle } = value;
384
+ if (sourceVersion === 2 && (!Object.hasOwn(value, "scopes") || !isStateFlowMode(lifecycle.mode)
385
+ || Object.hasOwn(lifecycle, "enabled") || Object.hasOwn(lifecycle, "disabled"))) {
386
+ throw new Error("Invalid State Flow lifecycle v2 policy");
387
+ }
388
+ const checkpoint = parseRetainedPiCheckpoint(lifecycle, inactiveMode);
389
+ return {
390
+ sourceVersion, targetVersion: 2, requiresConversion: sourceVersion === "legacy",
391
+ checkpoint: { ...checkpoint, version: 2, scopes: captureScopeSelection(sourceVersion === "legacy" ? legacySelection : scopes) },
392
+ };
393
+ }
394
+
255
395
  export function migrationFailure(data: JsonObject, error: string, mode: StateFlowMode = "passive"): Snapshot {
256
396
  const meta = restoredMeta(data.meta, data);
257
397
  meta.validation = {
@@ -1,18 +1,19 @@
1
1
  import {
2
2
  isArtifactRegistry,
3
- projectArtifactsForModel,
4
3
  updateArtifactRegistry,
5
4
  type ArtifactCompilationUpdate,
6
5
  type ArtifactModelHints,
7
6
  type ArtifactRegistry,
8
7
  } from "./artifact.ts";
9
- import { applyPatch, isObject, pruneEmptyObjects, type JsonObject } from "./json.ts";
8
+ import { applyPatch, isJsonValue, isObject, pruneEmptyObjects, type JsonObject } from "./json.ts";
9
+ import { projectPlaneContext, SEMANTIC_PLANES } from "./planes.ts";
10
+ import { validateModuleObject, validateRetainedModules, type RetainedModules } from "./semantic-module.ts";
10
11
 
11
12
  /** Runtime defaults for documented semantic planes; stored objects may omit them or retain other fields. */
12
13
  export type MaterializedState = JsonObject & {
13
14
  intents: JsonObject;
14
- contract: JsonObject;
15
- working: JsonObject;
15
+ strategy: JsonObject;
16
+ tactics: JsonObject;
16
17
  artifacts: ArtifactRegistry;
17
18
  response: string;
18
19
  lazy: JsonObject;
@@ -21,13 +22,23 @@ export type MaterializedState = JsonObject & {
21
22
  /** Sparse semantic state: documented planes may be absent. Disk codecs select only known fields. */
22
23
  export type SemanticState = JsonObject & Partial<{
23
24
  intents: JsonObject;
24
- contract: JsonObject;
25
- working: JsonObject;
25
+ strategy: JsonObject;
26
+ tactics: JsonObject;
26
27
  artifacts: ArtifactRegistry;
27
28
  response: string;
28
29
  lazy: JsonObject;
29
30
  }>;
30
31
  export type ScopedSemanticStates = Record<StateScope, SemanticState>;
32
+ /** Present owners only; a selection-aware caller must validate the exact acquired key set. */
33
+ export type ScopeStateValues = Partial<Record<StateScope, SemanticState>>;
34
+
35
+ /** Value admission only: owners must already be a captured policy, never inferred from missing data. */
36
+ export function assertScopeStateValues(states: ScopeStateValues, owners: readonly StateScope[]): void {
37
+ if (!isObject(states) || Object.keys(states).length !== owners.length
38
+ || Object.keys(states).some((scope) => !owners.includes(scope as StateScope))
39
+ || owners.some((scope) => !Object.hasOwn(states, scope))) throw new Error("Selected State Flow values must match their owners exactly");
40
+ for (const scope of owners) if (!isJsonValue(states[scope]) || !isSemanticState(states[scope])) throw new Error(`Invalid selected State Flow ${scope} value`);
41
+ }
31
42
 
32
43
  /** Compatibility name for callers that still treat materialized state as a document. */
33
44
  export type StateDocument = MaterializedState;
@@ -35,8 +46,8 @@ export type StateDocument = MaterializedState;
35
46
  /** Model patch shape; artifact entries may be compiler outputs before trusted metadata is attached. */
36
47
  export interface StatePatch extends JsonObject {
37
48
  artifacts: JsonObject;
38
- contract: JsonObject;
39
- working: JsonObject;
49
+ strategy: JsonObject;
50
+ tactics: JsonObject;
40
51
  intents: JsonObject;
41
52
  response: string;
42
53
  lazy: JsonObject;
@@ -46,9 +57,10 @@ export type StateScope = "global" | "cwd" | "session";
46
57
 
47
58
  /** A model-authored patch for one scope. Response is captured by the runtime in session state. */
48
59
  export interface ScopePatch {
60
+ [module: `x_${string}`]: JsonObject | undefined;
49
61
  artifacts?: JsonObject;
50
- contract?: JsonObject;
51
- working?: JsonObject;
62
+ strategy?: JsonObject;
63
+ tactics?: JsonObject;
52
64
  intents?: JsonObject;
53
65
  lazy?: JsonObject;
54
66
  }
@@ -79,18 +91,22 @@ export interface ScopedStates {
79
91
  session: MaterializedState;
80
92
  }
81
93
 
94
+ /** Temporary root-plane reservation; nested domain data is not an alternate semantic namespace. */
95
+ export function assertNoRetiredSemanticPlanes(value: JsonObject): void {
96
+ for (const plane of ["contract", "working"]) {
97
+ if (Object.hasOwn(value, plane)) throw new Error(`Retired State Flow semantic plane: ${plane}`);
98
+ }
99
+ }
100
+
82
101
  export function emptyState(): MaterializedState {
83
- return { intents: {}, contract: {}, working: {}, artifacts: {}, response: "", lazy: {} };
102
+ return { intents: {}, strategy: {}, tactics: {}, artifacts: {}, response: "", lazy: {} };
84
103
  }
85
104
 
86
105
  export function isMaterializedState(value: unknown): value is MaterializedState {
87
106
  return isObject(value)
107
+ && !Object.hasOwn(value, "contract") && !Object.hasOwn(value, "working")
88
108
  && isArtifactRegistry(value.artifacts)
89
- && isObject(value.contract)
90
- && isObject(value.working)
91
- && isObject(value.intents)
92
- && typeof value.response === "string"
93
- && isObject(value.lazy);
109
+ && SEMANTIC_PLANES.every(({ module }) => module.validate(value[module.id]));
94
110
  }
95
111
 
96
112
  /** Missing planes are valid storage, not missing authority. Present values retain their type checks. */
@@ -114,6 +130,7 @@ export function updateMaterializedArtifacts(
114
130
  /** Overlay lower-to-higher scopes without mutating any scope document. */
115
131
  export function overlayStates(...scopes: readonly JsonObject[]): MaterializedState {
116
132
  return scopes.reduce<MaterializedState>((effective, scope) => {
133
+ assertNoRetiredSemanticPlanes(scope);
117
134
  return applyPatch(effective, scope) as MaterializedState;
118
135
  }, emptyState());
119
136
  }
@@ -121,39 +138,63 @@ export function overlayStates(...scopes: readonly JsonObject[]): MaterializedSta
121
138
  /** Default-bearing SDK compatibility view; model transport uses sparse SemanticState instead. */
122
139
  export type ModelState = JsonObject & {
123
140
  intents: JsonObject;
124
- contract: JsonObject;
125
- working: JsonObject;
141
+ strategy: JsonObject;
142
+ tactics: JsonObject;
126
143
  artifacts: ArtifactRegistry;
127
144
  response: string;
128
145
  };
129
146
 
130
147
  /** Select only owned top-level fields, preserving nested data and replay deletion markers. */
131
- export function selectSemanticFields(value: JsonObject): JsonObject {
132
- return structuredClone(Object.fromEntries(Object.keys(emptyState())
148
+ export function selectSemanticFields(value: JsonObject, modules?: RetainedModules): JsonObject {
149
+ assertNoRetiredSemanticPlanes(value);
150
+ if (modules !== undefined) validateExternalPlanes(value, modules, true);
151
+ return structuredClone(Object.fromEntries([...Object.keys(emptyState()), ...Object.keys(modules ?? {})]
133
152
  .filter((key) => Object.hasOwn(value, key))
134
153
  .map((key) => [key, value[key]])));
135
154
  }
136
155
 
156
+ /** v4 admits only declared object planes; null is a replay deletion, never stored content. */
157
+ export function validateExternalPlanes(value: JsonObject, modules: RetainedModules, patch = false): void {
158
+ validateRetainedModules(modules);
159
+ const core = emptyState();
160
+ for (const [key, field] of Object.entries(value)) {
161
+ if (Object.hasOwn(core, key)) continue;
162
+ if (!Object.hasOwn(modules, key) || !(patch && field === null || isObject(field))) {
163
+ throw new Error(`Undeclared or invalid State Flow external plane: ${key}`);
164
+ }
165
+ if (field !== null) validateModuleObject(field);
166
+ }
167
+ }
168
+
169
+ /** Normalize only core planes: opaque external bodies are not empty-branch policy subjects. */
170
+ export function pruneSemanticState(value: JsonObject): JsonObject {
171
+ return Object.fromEntries(Object.entries(value).flatMap(([key, field]) => {
172
+ if (key.startsWith("x_")) return [[key, field]];
173
+ const pruned = pruneEmptyObjects({ [key]: field });
174
+ return Object.entries(pruned);
175
+ }));
176
+ }
177
+
137
178
  /** Read only documented, present planes; empty responses carry no semantic value. */
138
- export function projectSemanticState(state: JsonObject): SemanticState {
139
- const projected = selectSemanticFields(state);
179
+ export function projectSemanticState(state: JsonObject, modules?: RetainedModules): SemanticState {
180
+ const projected = selectSemanticFields(state, modules);
140
181
  if (projected.response === "") delete projected.response;
141
182
  return projected;
142
183
  }
143
184
 
144
185
  /** Preserve deletion meaning in visible history without exposing ignored fields or empty responses. */
145
- export function projectSemanticPatch(patch: JsonObject): JsonObject {
146
- const projected = selectSemanticFields(patch);
186
+ export function projectSemanticPatch(patch: JsonObject, modules?: RetainedModules): JsonObject {
187
+ const projected = selectSemanticFields(patch, modules);
147
188
  if (projected.response === "") projected.response = null;
148
189
  return projected;
149
190
  }
150
191
 
151
192
  /** Explicit hot reads filter hidden bodies/bookkeeping without normalizing legacy semantic data. */
152
- export function projectStateForRead(state: SemanticState, artifactHints: ArtifactModelHints = {}): SemanticState {
193
+ export function projectStateForRead(state: SemanticState, artifactHints: ArtifactModelHints = {}, modules?: RetainedModules): SemanticState {
153
194
  const { lazy: _hidden, ...visible } = state;
154
- const projected = projectSemanticState(visible);
155
- if (projected.artifacts) projected.artifacts = projectArtifactsForModel(projected.artifacts, artifactHints);
156
- return projected;
195
+ const selected = projectSemanticState(visible, modules);
196
+ const core = projectPlaneContext(selected, artifactHints).state;
197
+ return { ...core, ...Object.fromEntries(Object.keys(modules ?? {}).filter(id => Object.hasOwn(selected, id)).map(id => [id, structuredClone(selected[id])])) } as SemanticState;
157
198
  }
158
199
 
159
200
  /** Ordinary model context omits empty object fields; exact reads and replay remain observational. */
@@ -3,7 +3,9 @@ import type { RecentTransitionWindow } from "./history.ts";
3
3
  import { ownedTopLevelKeys } from "./ownership.ts";
4
4
  import { conciseDiagnostic } from "./protocol.ts";
5
5
  import { isActiveRestorationBlocked, type Snapshot } from "./snapshot.ts";
6
- import { overlayStates, projectSemanticState, type SemanticState, type ScopedStates, type StateScope } from "./state.ts";
6
+ import { assertScopeStateValues, overlayStates, projectSemanticState, type SemanticState, type ScopeStateValues, type StateScope } from "./state.ts";
7
+ import { applyPatch, isObject } from "./json.ts";
8
+ import { captureScopeSelection, DEFAULT_SCOPE_SELECTION, type ScopeSelection } from "./selection.ts";
7
9
  import type { ScopeRevisions, TransitionBoundary } from "./temporal.ts";
8
10
 
9
11
  export const STATUS_KEY = "state-flow";
@@ -22,32 +24,46 @@ export interface StatusDiagnostics {
22
24
  repositoryRoot: string;
23
25
  cwdScopeKey: string;
24
26
  sessionScopeKey: string;
25
- scopeStates: ScopedStates;
26
- /** Overlay raw scopes before defaults so absent scalar planes cannot mask lower scopes. */
27
+ /** Omission retains all-three compatibility; a subset must be explicit, never inferred from missing data. */
28
+ selection?: ScopeSelection;
29
+ /** Captured policy may request Session while only shared owners are observable. Never a materialization grant. */
30
+ requestedSelection?: ScopeSelection;
31
+ scopeStates: ScopeStateValues;
32
+ /** Legacy overlay override. Explicit selected observations derive only from scopeStates. */
27
33
  effectiveState?: SemanticState;
28
34
  recent: RecentTransitionWindow;
29
35
  historyLimit: number;
30
- temporal?: { head: TransitionBoundary; historyDepth: number; tailCounts: Record<StateScope, number>; revisions: ScopeRevisions };
36
+ temporal?: { head: TransitionBoundary; historyDepth: number; tailCounts: Partial<Record<StateScope, number>>; revisions: Partial<ScopeRevisions> };
31
37
  staleArtifacts: readonly StaleArtifactDiagnostic[];
32
38
  durableStateError?: string;
33
39
  publicationError?: string;
34
40
  }
35
41
 
36
- export function formatScopeRevisionVector(revisions: ScopeRevisions): string {
37
- return `g${revisions.global}c${revisions.cwd}s${revisions.session}`;
42
+ function assertScopeNumbers(values: Partial<ScopeRevisions>, selection: ScopeSelection, label: string): void {
43
+ if (!isObject(values) || Object.keys(values).length !== selection.length
44
+ || Object.keys(values).some(scope => !selection.includes(scope as StateScope))
45
+ || selection.some(scope => !Object.hasOwn(values, scope))) throw new Error(`Invalid State Flow selected ${label}`);
46
+ for (const scope of selection) if (!Number.isSafeInteger(values[scope]) || values[scope]! < 0) throw new Error(`Invalid State Flow selected ${label}`);
38
47
  }
39
48
 
40
- export function compactStatus(snapshot: Snapshot, _revisions: ScopeRevisions, colorize: Colorize): string | undefined {
49
+ export function formatScopeRevisionVector(revisions: Partial<ScopeRevisions>, selection: ScopeSelection = DEFAULT_SCOPE_SELECTION): string {
50
+ const selected = captureScopeSelection(selection);
51
+ assertScopeNumbers(revisions, selected, "revision vector");
52
+ const prefix = { global: "g", cwd: "c", session: "s" };
53
+ return selected.map(scope => `${prefix[scope]}${revisions[scope]}`).join("");
54
+ }
55
+
56
+ export function compactStatus(snapshot: Snapshot, _revisions: Partial<ScopeRevisions>, colorize: Colorize): string | undefined {
41
57
  const { mode } = snapshot.config;
42
58
  if (mode === "off") return undefined;
43
59
  return `${colorize("accent", "state-flow")} ${colorize("dim", isActiveRestorationBlocked(snapshot) ? "active (blocked)" : mode)}`;
44
60
  }
45
61
 
46
- function countArtifacts(states: ScopedStates, scope: StateScope): number {
47
- return Object.keys(states[scope].artifacts).length;
62
+ function countArtifacts(states: ScopeStateValues, scope: StateScope): number {
63
+ return Object.keys(states[scope]!.artifacts ?? {}).length;
48
64
  }
49
65
 
50
- const STATUS_PLANES = ["intents", "contract", "working", "artifacts", "response", "lazy"] as const;
66
+ const STATUS_PLANES = ["intents", "strategy", "tactics", "artifacts", "response", "lazy"] as const;
51
67
  const encoder = new TextEncoder();
52
68
 
53
69
  function isPresentPlane(value: unknown): boolean {
@@ -55,16 +71,18 @@ function isPresentPlane(value: unknown): boolean {
55
71
  return typeof value !== "object" || value === null || Object.keys(value).length > 0;
56
72
  }
57
73
 
58
- /** Operator-only footprint: serialized plane sizes and intent-owned share of top-level working/lazy entries. */
59
- export function scopeMemoryLines(states: ScopedStates): string[] {
74
+ /** Operator-only footprint: serialized plane sizes and intent-owned share of top-level tactics/lazy entries. */
75
+ export function scopeMemoryLines(states: ScopeStateValues, selection: ScopeSelection = DEFAULT_SCOPE_SELECTION): string[] {
76
+ const selected = captureScopeSelection(selection);
77
+ assertScopeStateValues(states, selected);
60
78
  const lines: string[] = [];
61
- for (const scope of ["global", "cwd", "session"] as const) {
79
+ for (const scope of selected) {
62
80
  const state = states[scope] as SemanticState;
63
81
  const sizes = STATUS_PLANES.filter((plane) => isPresentPlane(state[plane]))
64
82
  .map((plane) => `${plane} ${encoder.encode(JSON.stringify(state[plane])).length} B`);
65
83
  if (sizes.length === 0) continue;
66
84
  const owned = ownedTopLevelKeys(scope, state);
67
- const shares = (["working", "lazy"] as const).flatMap((plane) => {
85
+ const shares = (["tactics", "lazy"] as const).flatMap((plane) => {
68
86
  const value = state[plane];
69
87
  const keys = value !== null && typeof value === "object" && !Array.isArray(value) ? Object.keys(value) : [];
70
88
  return keys.length === 0 ? [] : [`${plane} ${keys.filter((key) => owned[plane].has(key)).length}/${keys.length}`];
@@ -75,41 +93,57 @@ export function scopeMemoryLines(states: ScopedStates): string[] {
75
93
  }
76
94
 
77
95
  export function detailedStatus(snapshot: Snapshot, diagnostics: StatusDiagnostics): string {
96
+ const selection = diagnostics.selection;
97
+ const explicit = selection !== undefined;
98
+ const selected = captureScopeSelection(explicit ? selection : DEFAULT_SCOPE_SELECTION);
99
+ const requested = diagnostics.requestedSelection === undefined ? selected : captureScopeSelection(diagnostics.requestedSelection);
78
100
  const available = diagnostics.temporal !== undefined && diagnostics.durableStateError === undefined;
79
- const materialized = !available ? undefined : diagnostics.effectiveState ?? projectSemanticState(overlayStates(
80
- diagnostics.scopeStates.global,
81
- diagnostics.scopeStates.cwd,
82
- diagnostics.scopeStates.session,
83
- ));
101
+ if (available) {
102
+ if (selected.some(scope => !requested.includes(scope))) throw new Error("State Flow materialization exceeds requested scopes");
103
+ assertScopeStateValues(diagnostics.scopeStates, selected);
104
+ assertScopeNumbers(diagnostics.temporal!.revisions, selected, "revision vector");
105
+ assertScopeNumbers(diagnostics.temporal!.tailCounts, selected, "tail counts");
106
+ }
107
+ // A selected observation derives its overlay from exact owners, never a stale all-owner override.
108
+ const materialized = !available ? undefined : explicit
109
+ ? selected.reduce<SemanticState>((state, scope) => applyPatch(state, projectSemanticState(diagnostics.scopeStates[scope]!)), {})
110
+ : diagnostics.effectiveState ?? projectSemanticState(overlayStates(...selected.map(scope => diagnostics.scopeStates[scope]!)));
84
111
  // Only top-level plane boundaries gain whitespace; nested user JSON stays unchanged.
85
112
  const stateJson = materialized === undefined ? undefined : JSON.stringify(materialized, null, 2).replace(/,\n(?= ")/g, ",\n\n");
86
- const invalidated = available ? String(diagnostics.staleArtifacts.length) : "unavailable";
87
- const invalidationLines = diagnostics.staleArtifacts.length === 0
88
- ? []
89
- : [
90
- "Pending artifact invalidations:",
91
- ...diagnostics.staleArtifacts.map(({ scope, path, reason }) => `- [${scope}] ${path} — ${reason}`),
92
- ];
113
+ const stale = explicit ? !available ? [] : diagnostics.staleArtifacts.flatMap(notice => {
114
+ const scope = notice.scope;
115
+ return selected.includes(scope) ? [{ scope, path: notice.path, reason: notice.reason }] : [];
116
+ }) : diagnostics.staleArtifacts;
117
+ const invalidated = available ? String(stale.length) : "unavailable";
118
+ const invalidationLines = stale.length === 0 ? []
119
+ : ["Pending artifact invalidations:", ...stale.map(({ scope, path, reason }) => `- [${scope}] ${path} — ${reason}`)];
93
120
  const temporal = available ? diagnostics.temporal : undefined;
94
121
  const temporalLines = temporal === undefined
95
122
  ? [`Temporal materialization unavailable: ${conciseDiagnostic(diagnostics.durableStateError ?? "no selected branch runtime")}`,
96
123
  `Hot history: unavailable; configured maximum depth ${diagnostics.historyLimit}`]
97
- : [`Scope revisions: ${formatScopeRevisionVector(temporal.revisions)}`,
124
+ : [`Scope revisions: ${formatScopeRevisionVector(temporal.revisions, selected)}`,
98
125
  `Runtime metadata: step #${snapshot.meta.step}${snapshot.meta.bootstrap ? "; bootstrap" : ""}`,
99
126
  `Hot history: offsets 0..${temporal.historyDepth}; maximum depth ${diagnostics.historyLimit}`];
100
127
  const artifacts = (scope: StateScope) => countArtifacts(diagnostics.scopeStates, scope);
101
- const hasArtifacts = available && (["global", "cwd", "session"] as const).some((scope) => artifacts(scope) > 0);
128
+ const hasArtifacts = available && selected.some((scope) => artifacts(scope) > 0);
129
+ const scopeKeys = [
130
+ ...(selected.includes("cwd") ? [`CWD ${diagnostics.cwdScopeKey}`] : []),
131
+ ...(selected.includes("session") ? [`session ${diagnostics.sessionScopeKey}`] : []),
132
+ ];
133
+ const artifactLabels = { global: "global", cwd: "CWD", session: "session" };
102
134
 
103
135
  return [
104
136
  `Mode: ${snapshot.config.mode}`,
137
+ ...(explicit ? [`Selected scopes: ${requested.join(", ")}`] : []),
138
+ ...(available && selected.length !== requested.length ? [`Materialized scopes: ${selected.join(", ")}`] : []),
105
139
  ...(isActiveRestorationBlocked(snapshot) ? ["Inference blocked: Active memory restoration failed. Use /state-flow-active for current memory or select Passive/Off for native context."] : []),
106
140
  `Repository: ${diagnostics.repositoryRoot}`,
107
- `Scope keys: CWD ${diagnostics.cwdScopeKey}; session ${diagnostics.sessionScopeKey}`,
141
+ ...(scopeKeys.length ? [`Scope keys: ${scopeKeys.join("; ")}`] : []),
108
142
  ...temporalLines,
109
143
  ...(diagnostics.publicationError === undefined ? [] : [`Memory writes paused after mode change: ${conciseDiagnostic(diagnostics.publicationError)}`]),
110
- ...(hasArtifacts ? [`Artifacts: global ${artifacts("global")}; CWD ${artifacts("cwd")}; session ${artifacts("session")}; pending invalidations ${invalidated}`] : []),
144
+ ...(hasArtifacts ? [`Artifacts: ${selected.map(scope => `${artifactLabels[scope]} ${artifacts(scope)}`).join("; ")}; pending invalidations ${invalidated}`] : []),
111
145
  ...invalidationLines,
112
- ...(available ? scopeMemoryLines(diagnostics.scopeStates) : []),
146
+ ...(available ? scopeMemoryLines(diagnostics.scopeStates, selected) : []),
113
147
  ...(stateJson === undefined
114
148
  ? ["Effective memory: unavailable"]
115
149
  : ["Effective memory:", "", stateJson]),