@hassangameryt/freeflow 0.7.2 → 0.7.3

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 (79) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.cursor-plugin/plugin.json +1 -1
  5. package/README.md +147 -127
  6. package/capabilities/cognitive-routing/SKILL.md +162 -93
  7. package/gemini-extension.json +1 -1
  8. package/package.json +1 -1
  9. package/pi-extension/dist/cognitive-routing-v2/config.js +35 -19
  10. package/pi-extension/dist/cognitive-routing-v2/projection.js +53 -44
  11. package/pi-extension/dist/cognitive-routing-v2/render.js +31 -10
  12. package/pi-extension/dist/cognitive-routing-v2/runtime.js +701 -105
  13. package/pi-extension/dist/cognitive-routing-v2/schemas.js +90 -24
  14. package/pi-extension/dist/cognitive-routing-v2/state.js +243 -27
  15. package/pi-extension/dist/cognitive-routing-v2/tools.js +19 -50
  16. package/pi-extension/dist/cognitive-routing-v2/types.js +5 -1
  17. package/pi-extension/dist/context-virtualization/runtime.js +29 -7
  18. package/pi-extension/dist/freeflow-context/tool.js +7 -6
  19. package/pi-extension/dist/index.js +36 -15
  20. package/pi-extension/dist/provider-support/astra/adapter.js +110 -0
  21. package/pi-extension/dist/provider-support/astra/history.js +125 -0
  22. package/pi-extension/dist/provider-support/astra/index.js +11 -0
  23. package/pi-extension/dist/provider-support/astra/session-state.js +114 -0
  24. package/pi-extension/dist/provider-support/index.js +4 -0
  25. package/pi-extension/dist/runtime/request-history.js +216 -0
  26. package/pi-extension/dist/runtime/runtime-context.js +29 -10
  27. package/pi-extension/dist/session-sources/events.js +22 -0
  28. package/pi-extension/dist/session-sources/provenance.js +32 -0
  29. package/pi-extension/dist/session-sources/sources.js +51 -7
  30. package/pi-extension/dist/settings/settings-ui.js +247 -47
  31. package/pi-extension/src/cognitive-routing-v2/config.ts +49 -19
  32. package/pi-extension/src/cognitive-routing-v2/projection.ts +53 -43
  33. package/pi-extension/src/cognitive-routing-v2/render.ts +31 -10
  34. package/pi-extension/src/cognitive-routing-v2/runtime.ts +712 -105
  35. package/pi-extension/src/cognitive-routing-v2/schemas.ts +95 -24
  36. package/pi-extension/src/cognitive-routing-v2/state.ts +244 -27
  37. package/pi-extension/src/cognitive-routing-v2/tools.ts +19 -50
  38. package/pi-extension/src/cognitive-routing-v2/types.ts +35 -3
  39. package/pi-extension/src/context-virtualization/runtime.ts +30 -8
  40. package/pi-extension/src/freeflow-context/tool.ts +7 -6
  41. package/pi-extension/src/index.ts +36 -14
  42. package/pi-extension/src/provider-support/astra/adapter.ts +108 -0
  43. package/pi-extension/src/provider-support/astra/history.ts +145 -0
  44. package/pi-extension/src/provider-support/astra/index.ts +12 -0
  45. package/pi-extension/src/provider-support/astra/session-state.ts +113 -0
  46. package/pi-extension/src/provider-support/index.ts +5 -0
  47. package/pi-extension/src/runtime/request-history.ts +243 -0
  48. package/pi-extension/src/runtime/runtime-context.ts +32 -10
  49. package/pi-extension/src/session-sources/events.ts +22 -0
  50. package/pi-extension/src/session-sources/provenance.ts +39 -0
  51. package/pi-extension/src/session-sources/sources.ts +77 -6
  52. package/pi-extension/src/settings/settings-ui.ts +276 -46
  53. package/pi-extension/tests/cache-reuse/history.test.js +117 -0
  54. package/pi-extension/tests/cache-reuse/native.test.js +91 -0
  55. package/pi-extension/tests/cache-reuse/settings.test.js +211 -0
  56. package/pi-extension/tests/cognitive-routing-v2/communication.test.js +94 -1
  57. package/pi-extension/tests/cognitive-routing-v2/delegation.test.js +557 -0
  58. package/pi-extension/tests/cognitive-routing-v2/native.test.js +34 -1
  59. package/pi-extension/tests/cognitive-routing-v2/persistence.test.js +55 -0
  60. package/pi-extension/tests/cognitive-routing-v2/recovery.test.js +800 -2
  61. package/pi-extension/tests/cognitive-routing-v2/render.test.js +15 -1
  62. package/pi-extension/tests/cognitive-routing-v2/runtime.test.js +273 -3
  63. package/pi-extension/tests/cognitive-routing-v2/state.test.js +312 -1
  64. package/pi-extension/tests/context-virtualization/extension.test.js +20 -10
  65. package/pi-extension/tests/context-virtualization/runtime.test.js +17 -3
  66. package/pi-extension/tests/fixtures/routing-native.js +4 -1
  67. package/pi-extension/tests/integration/mode-free-core.test.js +4 -3
  68. package/pi-extension/tests/integration/pi-extension.test.js +508 -29
  69. package/pi-extension/tests/integration/prompt-architecture.test.js +56 -40
  70. package/pi-extension/tests/integration/unaffected-regressions.test.js +3 -3
  71. package/pi-extension/tests/provider-support/astra-boundaries.test.js +75 -0
  72. package/pi-extension/tests/provider-support/astra.test.js +306 -0
  73. package/pi-extension/tests/provider-support/cli.test.js +92 -0
  74. package/pi-extension/tests/provider-support/native.test.js +183 -0
  75. package/plugin.json +1 -1
  76. package/runtime/prompts/cognitive-routing.md +4 -4
  77. package/skills/design-for-depth/SKILL.md +1 -1
  78. package/skills/design-for-depth/references/implementation-decisions.md +3 -3
  79. package/skills/discuss/SKILL.md +2 -2
@@ -83,27 +83,38 @@ test("re-entry recovery is stable and capability-neutral", async () => {
83
83
  assert.match(core, /apply its method and gather or produce evidence/);
84
84
  assert.match(core, /read the complete `full` record/);
85
85
  assert.match(core, /Make material gaps, contradictions, deferrals, and user-owned decisions explicit/);
86
- assert.match(cognitiveRouting, /Before relying on automatic routing, read the complete cognitive-routing skill/);
86
+ assert.match(cognitiveRouting, /Before relying on Automatic routing, read the complete cognitive-routing skill/);
87
87
  assert.match(cognitiveRouting, /This bootstrap read is the only environment call/);
88
88
  assert.match(cognitiveRouting, /If unavailable, stop and report the missing method/);
89
89
  // The approved compact cue keeps bootstrap/control boundaries; the loaded
90
90
  // method owns detailed role and handoff policy. Keep both obligations checked.
91
91
  assert.match(
92
92
  cognitiveRouting,
93
- /Under Manual or inactive routing, stop applying the automatic split and follow ordinary Workflow/,
93
+ /Under Manual control, the held Coordinator, Helper, or Executor follows ordinary unsplit Workflow/,
94
94
  );
95
- assert.match(cognitiveRouting, /Use the latest Runtime State for control, profile, and current responsibility/);
96
- assert.match(cognitiveRouting, /Coordinator and Executor in one agent\/session/);
97
- assert.match(routingSkill, /Automatic delegation, role restrictions, and projection are bypassed/);
98
- assert.match(routingSkill, /Put the actual contract inside `freeflow_delegate/);
99
- assert.match(routingSkill, /submits the actual report and stops ordinary task work/);
95
+ assert.match(cognitiveRouting, /When routing is inactive, stop applying this split/);
96
+ assert.match(
97
+ cognitiveRouting,
98
+ /Use the latest Runtime State for control, profile, delegation mode, and current responsibility/,
99
+ );
100
+ assert.match(cognitiveRouting, /configured profiles in one agent\/session/);
101
+ assert.match(routingSkill, /Automatic role restrictions, delegation, and projection do not apply/);
102
+ assert.match(routingSkill, /Put the actual contract in `freeflow_delegate/);
103
+ assert.match(routingSkill, /A worker return ends its ordinary task work/);
100
104
  assert.match(cognitiveRouting, /project every completed skill and instructional-reference read/);
101
- assert.match(routingSkill, /Attach recovery to the existing responsibility/);
102
105
  assert.match(
103
106
  routingSkill,
104
- /Returned work may use an attached recovery-only phase only if the runtime actually exposes it; otherwise inspect saved communication and stop if insufficient/,
107
+ /For the current returned assessment, Coordinator uses `freeflow_unit\(operation: "recover"\)`/,
105
108
  );
106
- assert.match(routingSkill, /Do not simulate recovery by replacing the assignment or report/);
109
+ assert.match(routingSkill, /freeflow_unit\(operation: "recover"\)/);
110
+ assert.match(
111
+ routingSkill,
112
+ /Preserve the unit, assignment, original report, outcome, revision, selections, and assessment/,
113
+ );
114
+ assert.match(routingSkill, /read only exact admitted task paths/);
115
+ assert.match(routingSkill, /freeflow_return\(operation: "supplement"\)/);
116
+ assert.match(routingSkill, /Deliver the supplement or cancel recovery before `assess`/);
117
+ assert.match(routingSkill, /Do not simulate recovery by replacing an assignment\/report or opening a new unit/);
107
118
  assert.doesNotMatch(
108
119
  conversationHistory,
109
120
  /Current user direction, live source truth, and present runtime state remain authoritative/,
@@ -178,7 +189,7 @@ test("composes the mandatory core fragments, optional capabilities, discovery, a
178
189
  }
179
190
  });
180
191
 
181
- test("subagents keep Freeflow core and base skills without optional capabilities", async () => {
192
+ test("subagents retain reference definitions while optional capabilities stay inactive", async () => {
182
193
  const cwd = await configuredRepo({
183
194
  contextVirtualization: true,
184
195
  conversationHistory: true,
@@ -198,13 +209,13 @@ test("subagents keep Freeflow core and base skills without optional capabilities
198
209
  const before = await handlers.get("before_agent_start")({ systemPrompt: "base prompt" }, ctx);
199
210
  assert.match(before.systemPrompt, /# Freeflow Stable Guidance/);
200
211
  assert.match(before.systemPrompt, /# Freeflow Interaction Contract/);
201
- assert.doesNotMatch(before.systemPrompt, /## Cognitive Routing Cue/);
202
- assert.doesNotMatch(before.systemPrompt, /## Context Virtualization Cue/);
203
- assert.doesNotMatch(before.systemPrompt, /## Conversation History Cue/);
212
+ assert.match(before.systemPrompt, /## Cognitive Routing Cue/);
213
+ assert.match(before.systemPrompt, /## Context Virtualization Cue/);
214
+ assert.match(before.systemPrompt, /## Conversation History Cue/);
204
215
 
205
216
  const resources = await handlers.get("resources_discover")({ cwd }, ctx);
206
217
  assert.ok(resources.skillPaths.some((path) => path.endsWith("/skills/action-selection/SKILL.md")));
207
- assert.ok(!resources.skillPaths.some((path) => path.includes("/capabilities/")));
218
+ assert.equal(resources.skillPaths.filter((path) => path.includes("/capabilities/")).length, 3);
208
219
 
209
220
  const providerContext = await handlers.get("context")({ messages: [] }, ctx);
210
221
  const runtimeState = lastRuntimeState(providerContext.messages);
@@ -212,7 +223,7 @@ test("subagents keep Freeflow core and base skills without optional capabilities
212
223
  assert.match(runtimeState.content, /Context Virtualization: inactive/);
213
224
  assert.match(runtimeState.content, /Conversation History: inactive/);
214
225
  assert.match(runtimeState.content, /Cognitive Routing: inactive/);
215
- assert.ok(!activeToolNames().includes("freeflow_context"));
226
+ assert.ok(activeToolNames().includes("freeflow_context"));
216
227
  } finally {
217
228
  await rm(cwd, { recursive: true, force: true });
218
229
  }
@@ -233,7 +244,7 @@ test("provider context reuses the before-agent surface until the next provider t
233
244
  assert.match(lastRuntimeState(sameTurn.messages).content, /Context Virtualization: active/);
234
245
 
235
246
  const next = await handlers.get("before_agent_start")({ systemPrompt: "base prompt" }, ctx);
236
- assert.doesNotMatch(next.systemPrompt, /## Context Virtualization Cue/);
247
+ assert.equal(next.systemPrompt, before.systemPrompt, "settings preserve the complete reference surface");
237
248
  assert.match(next.systemPrompt, /# Freeflow Interaction Contract/);
238
249
  assert.match(next.systemPrompt, /## Shared Terms/);
239
250
  } finally {
@@ -278,7 +289,7 @@ test("missing optional prompt fragments preserve the mandatory core surface", as
278
289
  }
279
290
  });
280
291
 
281
- test("missing child prompt removes only that capability from every model-facing surface", async () => {
292
+ test("missing child prompt marks that capability unavailable while retaining its definitions", async () => {
282
293
  const root = await mkdtemp(join(tmpdir(), "freeflow-child-prompt-failure-"));
283
294
  const cwd = await configuredRepo({ contextVirtualization: true });
284
295
  try {
@@ -298,13 +309,13 @@ test("missing child prompt removes only that capability from every model-facing
298
309
  assert.doesNotMatch(before.systemPrompt, /## Context Virtualization Cue/);
299
310
  assert.match(before.systemPrompt, /# Freeflow Interaction Contract/);
300
311
  const resources = await handlers.get("resources_discover")({ cwd }, ctx);
301
- assert.ok(!resources.skillPaths.some((path) => path.endsWith("/capabilities/context-virtualization/SKILL.md")));
302
- assert.ok(!activeToolNames().includes("freeflow_context"));
312
+ assert.ok(resources.skillPaths.some((path) => path.endsWith("/capabilities/context-virtualization/SKILL.md")));
313
+ assert.ok(activeToolNames().includes("freeflow_context"));
303
314
 
304
315
  await handlers.get("session_tree")({}, ctx);
305
- assert.ok(!activeToolNames().includes("freeflow_context"));
316
+ assert.ok(activeToolNames().includes("freeflow_context"));
306
317
  await handlers.get("session_compact")({}, ctx);
307
- assert.ok(!activeToolNames().includes("freeflow_context"));
318
+ assert.ok(activeToolNames().includes("freeflow_context"));
308
319
 
309
320
  const providerContext = await handlers.get("context")({ messages: [] }, ctx);
310
321
  assert.match(providerContext.messages.at(-1).content, /Context Virtualization: unavailable/);
@@ -314,9 +325,9 @@ test("missing child prompt removes only that capability from every model-facing
314
325
  assert.doesNotMatch(whitespaceBefore.systemPrompt, /## Context Virtualization Cue/);
315
326
  const whitespaceResources = await handlers.get("resources_discover")({ cwd }, ctx);
316
327
  assert.ok(
317
- !whitespaceResources.skillPaths.some((path) => path.endsWith("/capabilities/context-virtualization/SKILL.md")),
328
+ whitespaceResources.skillPaths.some((path) => path.endsWith("/capabilities/context-virtualization/SKILL.md")),
318
329
  );
319
- assert.ok(!activeToolNames().includes("freeflow_context"));
330
+ assert.ok(activeToolNames().includes("freeflow_context"));
320
331
  const whitespaceContext = await handlers.get("context")({ messages: [] }, ctx);
321
332
  assert.match(whitespaceContext.messages.at(-1).content, /Context Virtualization: unavailable/);
322
333
  } finally {
@@ -325,7 +336,7 @@ test("missing child prompt removes only that capability from every model-facing
325
336
  }
326
337
  });
327
338
 
328
- test("missing mandatory Interaction Contract preserves the host prompt and hides base skills", async () => {
339
+ test("missing mandatory Interaction Contract retains a dormant surface with unavailable state", async () => {
329
340
  const root = await mkdtemp(join(tmpdir(), "freeflow-interaction-contract-failure-"));
330
341
  const cwd = await configuredRepo();
331
342
  try {
@@ -342,19 +353,21 @@ test("missing mandatory Interaction Contract preserves the host prompt and hides
342
353
  const { handlers, activeToolNames } = loadExtension(extension);
343
354
  const ctx = context(cwd);
344
355
  await handlers.get("session_start")({}, ctx);
345
- assert.ok(!activeToolNames().includes("freeflow_context"));
356
+ assert.ok(activeToolNames().includes("freeflow_context"));
346
357
  const before = await handlers.get("before_agent_start")({ systemPrompt: "base prompt" }, ctx);
347
- assert.equal(before.systemPrompt, "base prompt");
358
+ assert.ok(before.systemPrompt.startsWith("base prompt\n\n"));
359
+ assert.match(before.systemPrompt, /guidance is dormant/);
348
360
  const resources = await handlers.get("resources_discover")({ cwd }, ctx);
349
- assert.deepEqual(resources.skillPaths, []);
361
+ assert.equal(resources.skillPaths.length, 27);
350
362
  await handlers.get("session_tree")({}, ctx);
351
- assert.ok(!activeToolNames().includes("freeflow_context"));
363
+ assert.ok(activeToolNames().includes("freeflow_context"));
352
364
 
353
365
  await writeFile(join(root, "runtime", "prompts", "interaction-contract.md"), " \n\t", "utf8");
354
366
  const whitespaceBefore = await handlers.get("before_agent_start")({ systemPrompt: "base prompt" }, ctx);
355
- assert.equal(whitespaceBefore.systemPrompt, "base prompt");
367
+ assert.ok(whitespaceBefore.systemPrompt.startsWith("base prompt\n\n"));
368
+ assert.match(whitespaceBefore.systemPrompt, /guidance is dormant/);
356
369
  const whitespaceResources = await handlers.get("resources_discover")({ cwd }, ctx);
357
- assert.deepEqual(whitespaceResources.skillPaths, []);
370
+ assert.equal(whitespaceResources.skillPaths.length, 27);
358
371
  const whitespaceContext = await handlers.get("context")({ messages: [] }, ctx);
359
372
  assert.match(whitespaceContext.messages.at(-1).content, /Freeflow: unavailable/);
360
373
  } finally {
@@ -363,7 +376,7 @@ test("missing mandatory Interaction Contract preserves the host prompt and hides
363
376
  }
364
377
  });
365
378
 
366
- test("missing mandatory core prompt preserves the host prompt and hides base skills", async () => {
379
+ test("missing mandatory core prompt retains a dormant surface with unavailable state", async () => {
367
380
  const root = await mkdtemp(join(tmpdir(), "freeflow-core-prompt-failure-"));
368
381
  const cwd = await configuredRepo({ contextVirtualization: true });
369
382
  try {
@@ -380,9 +393,10 @@ test("missing mandatory core prompt preserves the host prompt and hides base ski
380
393
  await handlers.get("session_start")({ type: "session_start" }, ctx);
381
394
 
382
395
  const before = await handlers.get("before_agent_start")({ systemPrompt: "base prompt" }, ctx);
383
- assert.equal(before.systemPrompt, "base prompt");
396
+ assert.ok(before.systemPrompt.startsWith("base prompt\n\n"));
397
+ assert.match(before.systemPrompt, /guidance is dormant/);
384
398
  const resources = await handlers.get("resources_discover")({ cwd }, ctx);
385
- assert.deepEqual(resources.skillPaths, []);
399
+ assert.equal(resources.skillPaths.length, 27);
386
400
  const providerContext = await handlers.get("context")({ messages: [] }, ctx);
387
401
  assert.match(providerContext.messages.at(-1).content, /Freeflow: unavailable/);
388
402
  assert.doesNotMatch(
@@ -391,15 +405,16 @@ test("missing mandatory core prompt preserves the host prompt and hides base ski
391
405
  );
392
406
 
393
407
  await handlers.get("session_tree")({}, ctx);
394
- assert.ok(!activeToolNames().includes("freeflow_context"));
408
+ assert.ok(activeToolNames().includes("freeflow_context"));
395
409
  await handlers.get("session_compact")({}, ctx);
396
- assert.ok(!activeToolNames().includes("freeflow_context"));
410
+ assert.ok(activeToolNames().includes("freeflow_context"));
397
411
 
398
412
  await writeFile(join(root, "runtime", "prompts", "core.md"), " \n\t", "utf8");
399
413
  const whitespaceBefore = await handlers.get("before_agent_start")({ systemPrompt: "base prompt" }, ctx);
400
- assert.equal(whitespaceBefore.systemPrompt, "base prompt");
414
+ assert.ok(whitespaceBefore.systemPrompt.startsWith("base prompt\n\n"));
415
+ assert.match(whitespaceBefore.systemPrompt, /guidance is dormant/);
401
416
  const whitespaceResources = await handlers.get("resources_discover")({ cwd }, ctx);
402
- assert.deepEqual(whitespaceResources.skillPaths, []);
417
+ assert.equal(whitespaceResources.skillPaths.length, 27);
403
418
  const whitespaceContext = await handlers.get("context")({ messages: [] }, ctx);
404
419
  assert.match(whitespaceContext.messages.at(-1).content, /Freeflow: unavailable/);
405
420
  } finally {
@@ -414,7 +429,8 @@ test("Runtime State remains present while Freeflow is disabled without optional
414
429
  const { handlers } = loadExtension();
415
430
  const ctx = context(cwd);
416
431
  const before = await handlers.get("before_agent_start")({ systemPrompt: "base prompt" }, ctx);
417
- assert.equal(before.systemPrompt, "base prompt");
432
+ assert.ok(before.systemPrompt.startsWith("base prompt\n\n"));
433
+ assert.match(before.systemPrompt, /guidance is dormant/);
418
434
  assert.equal(before.message, undefined);
419
435
 
420
436
  const providerContext = await handlers.get("context")({ messages: [] }, ctx);
@@ -424,7 +440,7 @@ test("Runtime State remains present while Freeflow is disabled without optional
424
440
  assert.doesNotMatch(runtimeState.content, /Default mode|Active mode|Interaction Contract|Skills/);
425
441
 
426
442
  const resources = await handlers.get("resources_discover")({ cwd }, ctx);
427
- assert.deepEqual(resources.skillPaths, []);
443
+ assert.equal(resources.skillPaths.length, 27);
428
444
  } finally {
429
445
  await rm(cwd, { recursive: true, force: true });
430
446
  }
@@ -290,7 +290,7 @@ test("Cognitive Routing preset cancellation preserves the previous repository va
290
290
  });
291
291
  component.handleInput("\u001b[B");
292
292
  component.handleInput("\r");
293
- component.handleInput("\u001b[B");
293
+ for (let index = 0; index < 4; index += 1) component.handleInput("\u001b[B");
294
294
  component.handleInput("\r");
295
295
  component.handleInput("\u001b");
296
296
  component.handleInput("\u001b");
@@ -339,7 +339,7 @@ test("Cognitive Routing settings refresh after enabling the capability", async (
339
339
  component.handleInput("\r");
340
340
  await component.waitForWrites();
341
341
  component.handleInput("\u001b");
342
- assert.match(component.render(120).join("\n"), /Cognitive Routing\s+enabled \(4\) configured/);
342
+ assert.match(component.render(120).join("\n"), /Cognitive Routing\s+enabled \(6\) configured/);
343
343
  component.handleInput("\u001b");
344
344
  return result;
345
345
  };
@@ -383,7 +383,7 @@ test("Cognitive Routing settings can disable projection without disabling routin
383
383
  });
384
384
  component.handleInput("\u001b[B");
385
385
  component.handleInput("\r");
386
- for (let index = 0; index < 3; index += 1) component.handleInput("\u001b[B");
386
+ for (let index = 0; index < 5; index += 1) component.handleInput("\u001b[B");
387
387
  component.handleInput("\r");
388
388
  component.handleInput("\u001b[B");
389
389
  component.handleInput("\r");
@@ -0,0 +1,75 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { SessionState } from "../../dist/provider-support/astra/session-state.js";
4
+ import { assemble, ENTRY_TYPE } from "../../dist/provider-support/astra/history.js";
5
+
6
+ function fixture() {
7
+ const branch = [];
8
+ const append = (type, fields = {}) => {
9
+ const entry = { id: `entry-${branch.length}`, parentId: branch.at(-1)?.id ?? null, type, ...fields };
10
+ branch.push(entry);
11
+ return entry.id;
12
+ };
13
+ const reader = {
14
+ getBranch: () => branch,
15
+ getLeafId: () => branch.at(-1)?.id ?? null,
16
+ getSessionFile: () => undefined,
17
+ };
18
+ const store = new SessionState({ appendEntry: (customType, data) => append("custom", { customType, data }) }, reader);
19
+ return { branch, append, reader, store };
20
+ }
21
+ const payload = (effort = "high") => ({
22
+ model: "astra",
23
+ instructions: "same",
24
+ tools: [],
25
+ reasoning: { effort },
26
+ input: [{ role: "user", content: "same" }],
27
+ });
28
+
29
+ for (const boundary of ["compaction", "branch_summary"]) {
30
+ test(`${boundary} excludes pre-boundary history even with identical input`, async () => {
31
+ const f = fixture();
32
+ const old = assemble(payload(), "astra", f.store.generation(), f.reader.getLeafId(), []);
33
+ f.store.append(old.record);
34
+ const epoch = f.append(boundary);
35
+ f.append("model_change", { provider: "p", modelId: "luna" });
36
+ f.append("model_change", { provider: "p", modelId: "astra" });
37
+ assert.equal(f.store.generation(), epoch);
38
+ const current = assemble(payload("low"), "astra", epoch, f.reader.getLeafId(), await f.store.records());
39
+ assert.equal(current.baseline, "low");
40
+ assert.equal(current.record.parent, null);
41
+ // A persisted cross-boundary parent must not become valid through normalization.
42
+ f.store.append({ ...current.record, parent: old.record.id, baseline: "high", effective: "high" });
43
+ await assert.rejects(f.store.records(), /Invalid Astra history parent/);
44
+ });
45
+ }
46
+
47
+ test("unknown generation and invalid native ancestry are rejected, not normalized into eligibility", async () => {
48
+ const f = fixture();
49
+ const old = assemble(payload(), "astra", "unrelated-model-entry", null, []);
50
+ f.store.append(old.record);
51
+ await assert.rejects(f.store.records(), /generation unavailable/);
52
+ f.branch[0].parentId = "missing";
53
+ await assert.rejects(f.store.records(), /Invalid Astra session ancestry/);
54
+ });
55
+
56
+ test("changed envelope or earlier projected content cannot replay an old effort update", async () => {
57
+ const high = payload();
58
+ const first = assemble(high, "astra", "root", null, []);
59
+ const low = { ...payload("low"), input: [...high.input, { role: "user", content: "later" }] };
60
+ const second = assemble(low, "astra", "root", null, [first.record]);
61
+ for (const changed of [
62
+ { ...low, instructions: "different" },
63
+ { ...low, input: [{ role: "user", content: "structural omission" }] },
64
+ ]) {
65
+ const result = assemble(changed, "astra", "root", null, [first.record, second.record]);
66
+ assert.equal(result.record.parent, null);
67
+ assert.deepEqual(
68
+ result.payload.input.filter((x) => x.type !== "configuration_update"),
69
+ changed.input,
70
+ );
71
+ }
72
+ const differentModel = assemble(low, "other-model", "root", null, [first.record, second.record]);
73
+ assert.equal(differentModel.record.parent, null);
74
+ assert.equal(differentModel.baseline, "low");
75
+ });
@@ -0,0 +1,306 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { mkdtemp, rm, readFile } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+ import { SessionManager } from "@earendil-works/pi-coding-agent";
7
+ import { AstraAdapter, requestKey } from "../../dist/provider-support/astra/adapter.js";
8
+ import { ENTRY_TYPE, assemble } from "../../dist/provider-support/astra/history.js";
9
+
10
+ const model = {
11
+ id: "gpt-6-astra",
12
+ provider: "openai-codex",
13
+ api: "openai-codex-responses",
14
+ baseUrl: "https://chatgpt.com/backend-api",
15
+ };
16
+ const u = (text) => ({ role: "user", content: text });
17
+ const a = (text) => ({ role: "assistant", content: text });
18
+ const request = (input, effort = "low") => ({
19
+ model: model.id,
20
+ store: false,
21
+ input,
22
+ instructions: "stable",
23
+ reasoning: { effort, summary: "auto" },
24
+ prompt_cache_key: "unchanged-key",
25
+ tools: [],
26
+ });
27
+ function fixture(manager = SessionManager.inMemory("/tmp/astra-fixture")) {
28
+ const status = [];
29
+ const pi = { appendEntry: (type, data) => manager.appendCustomEntry(type, data) };
30
+ const ctx = { model, sessionManager: manager, ui: { setStatus: (...args) => status.push(args) } };
31
+ return { pi, ctx, manager, status, adapter: new AstraAdapter(pi) };
32
+ }
33
+ const updates = (p) =>
34
+ p.input.flatMap((item, index) =>
35
+ item.type === "configuration_update" ? [{ index, effort: item.reasoning.effort }] : [],
36
+ );
37
+
38
+ test("native ancestry records replay Low High Low with original anchors and unchanged selected effort", async () => {
39
+ const f = fixture(),
40
+ h = [u("one")];
41
+ await f.adapter.adapt(request(h), f.ctx);
42
+ h.push(a("one"), u("two"));
43
+ const high = await f.adapter.adapt(request(h, "high"), f.ctx);
44
+ assert.equal(high.reasoning.effort, "low");
45
+ assert.deepEqual(updates(high), [{ index: 2, effort: "high" }]);
46
+ h.push(a("two"), u("three"));
47
+ const low = await new AstraAdapter(f.pi).adapt(request(h), f.ctx);
48
+ assert.deepEqual(low.input.slice(0, high.input.length), high.input);
49
+ assert.deepEqual(updates(low), [
50
+ { index: 2, effort: "high" },
51
+ { index: 5, effort: "low" },
52
+ ]);
53
+ const count = f.manager.getEntries().length;
54
+ assert.deepEqual(await f.adapter.adapt(request(h), f.ctx), low);
55
+ assert.equal(f.manager.getEntries().length, count, "retry writes no duplicate record");
56
+ assert.equal(low.prompt_cache_key, "unchanged-key");
57
+ assert.ok(f.manager.getEntries().every((e) => !JSON.stringify(e.data).includes('"content"')));
58
+ });
59
+
60
+ test("first High is baseline; tool-loop switch occurs after completed results, not before an old user", async () => {
61
+ const f = fixture(),
62
+ h = [u("one")];
63
+ await f.adapter.adapt(request(h, "high"), f.ctx);
64
+ h.push(
65
+ { type: "reasoning", encrypted_content: "opaque-signed-value", summary: [] },
66
+ { type: "function_call", call_id: "call_1", name: "read", arguments: "{}" },
67
+ { type: "function_call_output", call_id: "call_1", output: "body" },
68
+ );
69
+ const p = await f.adapter.adapt(request(h, "low"), f.ctx);
70
+ assert.equal(p.reasoning.effort, "high");
71
+ assert.deepEqual(updates(p), [{ index: 4, effort: "low" }]);
72
+ assert.deepEqual(p.input.slice(0, 4), h);
73
+ });
74
+
75
+ test("changed/omitted/expanded input safely rebases; restored compatible branch recovers its own updates", async () => {
76
+ const f = fixture();
77
+ const h = [u("source")];
78
+ await f.adapter.adapt(request(h), f.ctx);
79
+ h.push(a("one"), u("two"));
80
+ const high = await f.adapter.adapt(request(h, "high"), f.ctx);
81
+ const changed = [u("different")];
82
+ const p = await f.adapter.adapt(request(changed, "high"), f.ctx);
83
+ assert.equal(p.reasoning.effort, "low");
84
+ assert.deepEqual(updates(p), [{ index: 0, effort: "high" }]);
85
+ const restored = await f.adapter.adapt(request([...h, a("two"), u("three")], "high"), f.ctx);
86
+ assert.deepEqual(restored.input.slice(0, high.input.length), high.input);
87
+ });
88
+
89
+ test("same payload effort toggles never manufacture adjacent updates; fallback retains desired effort", async () => {
90
+ const f = fixture(),
91
+ h = [u("one")];
92
+ await f.adapter.adapt(request(h), f.ctx);
93
+ const high = await f.adapter.adapt(request(h, "high"), f.ctx);
94
+ assert.deepEqual(updates(high), [{ index: 1, effort: "high" }]);
95
+ const original = request(h, "low");
96
+ assert.equal(await f.adapter.adapt(original, f.ctx), original);
97
+ assert.equal(original.reasoning.effort, "low");
98
+ assert.deepEqual(updates(original), []);
99
+ });
100
+
101
+ test("unsupported requests and existing native updates pass through without persistence", async () => {
102
+ const f = fixture();
103
+ for (const change of [
104
+ (p) => (p.model = "gpt-5.6-luna"),
105
+ (p) => (p.previous_response_id = "resp_x"),
106
+ (p) => (p.conversation = "c"),
107
+ (p) => (p.context_management = []),
108
+ (p) => (p.truncation = "auto"),
109
+ (p) => (p.reasoning.mode = "pro"),
110
+ (p) => p.input.push({ type: "configuration_update", reasoning: { effort: "high" } }),
111
+ (p) => (p.reasoning.effort = "off"),
112
+ (p) => (p.store = true),
113
+ ]) {
114
+ const p = request([u("one")]);
115
+ change(p);
116
+ assert.equal(await f.adapter.adapt(p, f.ctx), p);
117
+ }
118
+ assert.equal(requestKey(request([]), { ...model, provider: "proxy" }), undefined);
119
+ assert.equal(requestKey(request([]), { ...model, baseUrl: "https://example.com" }), undefined);
120
+ assert.ok(
121
+ requestKey(request([]), {
122
+ ...model,
123
+ provider: "openai",
124
+ api: "openai-responses",
125
+ baseUrl: "https://api.openai.com/v1",
126
+ }),
127
+ );
128
+ assert.equal(f.manager.getEntries().length, 0);
129
+ });
130
+
131
+ test("append failure before or after mutation keeps original request and latches fallback", async () => {
132
+ for (const after of [false, true]) {
133
+ const f = fixture();
134
+ await f.adapter.adapt(request([u("one")]), f.ctx);
135
+ const real = f.pi.appendEntry;
136
+ f.pi.appendEntry = (...args) => {
137
+ if (after) real(...args);
138
+ throw Error("disk fault");
139
+ };
140
+ const original = request([u("one"), a("one"), u("two")], "high");
141
+ assert.equal(await f.adapter.adapt(original, f.ctx), original);
142
+ f.pi.appendEntry = real;
143
+ assert.equal(await f.adapter.adapt(original, f.ctx), original);
144
+ assert.ok(f.status.some((x) => x[1]?.includes("native effort")));
145
+ }
146
+ });
147
+
148
+ test("corrupt effective-effort metadata cannot silently pin the wrong effort", async () => {
149
+ const f = fixture();
150
+ await f.adapter.adapt(request([u("one")]), f.ctx);
151
+ f.manager.getEntries().find((e) => e.customType === ENTRY_TYPE).data.effective = "high";
152
+ const original = request([u("one"), a("one"), u("two")], "high");
153
+ assert.equal(await f.adapter.adapt(original, f.ctx), original);
154
+ });
155
+
156
+ test("selected ancestry, new sessions, model changes and compaction isolate effort histories", async () => {
157
+ const f = fixture();
158
+ f.manager.appendModelChange(model.provider, model.id);
159
+ await f.adapter.adapt(request([u("one")]), f.ctx);
160
+ const baseLeaf = f.manager.getLeafId();
161
+ await f.adapter.adapt(request([u("one"), a("one"), u("two")], "high"), f.ctx);
162
+ f.manager.branch(baseLeaf);
163
+ const branch = await f.adapter.adapt(request([u("one"), a("other"), u("sibling")], "medium"), f.ctx);
164
+ assert.deepEqual(updates(branch), [{ index: 2, effort: "medium" }]);
165
+ f.manager.appendModelChange("openai-codex", "gpt-5.6-luna");
166
+ f.manager.appendModelChange(model.provider, model.id);
167
+ const reset = await f.adapter.adapt(request([u("new")], "high"), f.ctx);
168
+ assert.equal(reset.reasoning.effort, "low");
169
+ assert.deepEqual(updates(reset), [{ index: 0, effort: "high" }]);
170
+ assert.deepEqual(
171
+ reset.input.filter((x) => x.type !== "configuration_update"),
172
+ [u("new")],
173
+ );
174
+ f.adapter.setCompacting(true);
175
+ const summary = request([u("summary")], "low");
176
+ assert.equal(await f.adapter.adapt(summary, f.ctx), summary);
177
+ f.adapter.setCompacting(false);
178
+ const other = fixture();
179
+ assert.equal((await f.adapter.adapt(request([u("new")], "medium"), other.ctx)).reasoning.effort, "medium");
180
+ });
181
+
182
+ test("persisted labeled fork and reload preserve an anchored effort transition", async () => {
183
+ const root = await mkdtemp(join(tmpdir(), "astra-persist-"));
184
+ try {
185
+ const manager = SessionManager.create(root, root),
186
+ f = fixture(manager);
187
+ manager.appendMessage({ role: "user", content: "one", timestamp: 1 });
188
+ await f.adapter.adapt(request([u("one")]), f.ctx);
189
+ manager.appendMessage({
190
+ role: "assistant",
191
+ content: [{ type: "text", text: "one" }],
192
+ api: model.api,
193
+ provider: model.provider,
194
+ model: model.id,
195
+ usage: {
196
+ input: 1,
197
+ output: 1,
198
+ cacheRead: 0,
199
+ cacheWrite: 0,
200
+ totalTokens: 2,
201
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
202
+ },
203
+ stopReason: "stop",
204
+ timestamp: 2,
205
+ });
206
+ manager.appendLabelChange(manager.getLeafId(), "before effort change");
207
+ const h = [u("one"), a("one"), u("two")];
208
+ const high = await f.adapter.adapt(request(h, "high"), f.ctx);
209
+ assert.ok((await readFile(manager.getSessionFile(), "utf8")).includes(ENTRY_TYPE));
210
+ const forkFile = manager.createBranchedSession(manager.getLeafId());
211
+ assert.ok(forkFile, "native fork file created");
212
+ const reloaded = fixture(SessionManager.open(forkFile));
213
+ const p = await reloaded.adapter.adapt(request([...h, a("two"), u("three")], "low"), reloaded.ctx);
214
+ assert.deepEqual(p.input.slice(0, high.input.length), high.input);
215
+ assert.equal(p.reasoning.effort, "low");
216
+ } finally {
217
+ await rm(root, { recursive: true, force: true });
218
+ }
219
+ });
220
+
221
+ test("queued request from a replaced session passes through without a native append", async () => {
222
+ const f = fixture(),
223
+ p = request([u("old")], "high");
224
+ const pending = f.adapter.adapt(p, f.ctx);
225
+ f.adapter.reset();
226
+ assert.equal(await pending, p);
227
+ assert.equal(f.manager.getEntries().length, 0);
228
+ });
229
+
230
+ for (const returnEffort of ["high", "low"]) {
231
+ test(`model round trip preserves original Low anchor through ${returnEffort} return and reload`, async () => {
232
+ const f = fixture();
233
+ f.manager.appendModelChange(model.provider, model.id);
234
+ const h = [u("start")];
235
+ await f.adapter.adapt(request(h, "high"), f.ctx);
236
+ h.push(a("first"), u("low"));
237
+ const low = await f.adapter.adapt(request(h), f.ctx);
238
+ f.manager.appendModelChange(model.provider, "gpt-5.6-luna");
239
+ const luna = request(h);
240
+ luna.model = "gpt-5.6-luna";
241
+ assert.equal(await f.adapter.adapt(luna, { ...f.ctx, model: { ...model, id: luna.model } }), luna);
242
+ f.manager.appendModelChange(model.provider, model.id);
243
+ h.push(a("second"), u("return"));
244
+ const resumed = await new AstraAdapter(f.pi).adapt(request(h, returnEffort), f.ctx);
245
+ assert.equal(resumed.reasoning.effort, "high");
246
+ assert.deepEqual(resumed.input.slice(0, low.input.length), low.input);
247
+ h.push(a("third"), u("low again"));
248
+ const again = await f.adapter.adapt(request(h), f.ctx);
249
+ assert.deepEqual(again.input.slice(0, resumed.input.length), resumed.input);
250
+ assert.equal(updates(again)[0].index, 2);
251
+ });
252
+ }
253
+
254
+ test("legacy model-generation records survive a round trip without rewriting persisted metadata", async () => {
255
+ const root = await mkdtemp(join(tmpdir(), "astra-legacy-"));
256
+ try {
257
+ const f = fixture(SessionManager.create(root, root));
258
+ f.manager.appendMessage({ role: "user", content: "legacy", timestamp: 1 });
259
+ f.manager.appendMessage({
260
+ role: "assistant",
261
+ content: [{ type: "text", text: "legacy" }],
262
+ api: model.api,
263
+ provider: model.provider,
264
+ model: model.id,
265
+ usage: {
266
+ input: 1,
267
+ output: 1,
268
+ cacheRead: 0,
269
+ cacheWrite: 0,
270
+ totalTokens: 2,
271
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
272
+ },
273
+ stopReason: "stop",
274
+ timestamp: 2,
275
+ });
276
+ const oldGeneration = f.manager.appendModelChange(model.provider, model.id);
277
+ const key = requestKey(request([]), model);
278
+ const h = [u("legacy")];
279
+ const first = assemble(request(h, "high"), key, oldGeneration, f.manager.getLeafId(), []);
280
+ f.manager.appendCustomEntry(ENTRY_TYPE, first.record);
281
+ h.push(a("one"), u("two"));
282
+ const second = assemble(request(h), key, oldGeneration, f.manager.getLeafId(), [first.record]);
283
+ f.manager.appendCustomEntry(ENTRY_TYPE, second.record);
284
+ const saved = JSON.stringify(f.manager.getEntries());
285
+ f.manager.appendModelChange(model.provider, "gpt-5.6-luna");
286
+ f.manager.appendModelChange(model.provider, model.id);
287
+ const loaded = fixture(SessionManager.open(f.manager.getSessionFile()));
288
+ const resumed = await loaded.adapter.adapt(request([...h, a("two"), u("three")]), loaded.ctx);
289
+ assert.equal(resumed.reasoning.effort, "high");
290
+ assert.deepEqual(resumed.input.slice(0, second.payload.input.length), second.payload.input);
291
+ assert.equal(JSON.stringify(f.manager.getEntries().slice(0, JSON.parse(saved).length)), saved);
292
+ const again = fixture(SessionManager.open(loaded.manager.getSessionFile()));
293
+ const reloaded = await again.adapter.adapt(request([...h, a("two"), u("three"), a("three"), u("four")]), again.ctx);
294
+ assert.deepEqual(reloaded.input.slice(0, resumed.input.length), resumed.input);
295
+ assert.equal(JSON.stringify(again.manager.getEntries().slice(0, JSON.parse(saved).length)), saved);
296
+ } finally {
297
+ await rm(root, { recursive: true, force: true });
298
+ }
299
+ });
300
+
301
+ test("session reset clears adapter status without changing the next requested effort", async () => {
302
+ const f = fixture();
303
+ await f.adapter.adapt(request([u("one")]), f.ctx);
304
+ f.adapter.reset(f.ctx);
305
+ assert.deepEqual(f.status.at(-1), ["freeflow-astra-effort", undefined]);
306
+ });