@hassangameryt/freeflow 0.7.1 → 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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/README.md +153 -128
- package/capabilities/cognitive-routing/SKILL.md +126 -274
- package/command-surface.json +7 -1
- package/gemini-extension.json +1 -1
- package/package.json +4 -4
- package/pi-extension/dist/cognitive-routing-v2/budget.js +42 -0
- package/pi-extension/dist/cognitive-routing-v2/config.js +136 -0
- package/pi-extension/dist/cognitive-routing-v2/projection.js +330 -0
- package/pi-extension/dist/cognitive-routing-v2/render.js +252 -0
- package/pi-extension/dist/cognitive-routing-v2/runtime.js +2181 -0
- package/pi-extension/dist/cognitive-routing-v2/schemas.js +173 -0
- package/pi-extension/dist/cognitive-routing-v2/state.js +828 -0
- package/pi-extension/dist/cognitive-routing-v2/tools.js +60 -0
- package/pi-extension/dist/cognitive-routing-v2/types.js +48 -0
- package/pi-extension/dist/context-virtualization/runtime.js +29 -7
- package/pi-extension/dist/freeflow-context/tool.js +12 -6
- package/pi-extension/dist/index.js +302 -626
- package/pi-extension/dist/provider-support/astra/adapter.js +110 -0
- package/pi-extension/dist/provider-support/astra/history.js +125 -0
- package/pi-extension/dist/provider-support/astra/index.js +11 -0
- package/pi-extension/dist/provider-support/astra/session-state.js +114 -0
- package/pi-extension/dist/provider-support/index.js +4 -0
- package/pi-extension/dist/runtime/request-history.js +216 -0
- package/pi-extension/dist/runtime/runtime-context.js +85 -16
- package/pi-extension/dist/session-sources/events.js +217 -0
- package/pi-extension/dist/session-sources/provenance.js +32 -0
- package/pi-extension/dist/session-sources/read-only-session.js +148 -0
- package/pi-extension/dist/session-sources/sources.js +277 -0
- package/pi-extension/dist/settings/settings-ui.js +274 -147
- package/pi-extension/src/cognitive-routing-v2/budget.ts +42 -0
- package/pi-extension/src/cognitive-routing-v2/config.ts +173 -0
- package/pi-extension/src/cognitive-routing-v2/projection.ts +375 -0
- package/pi-extension/src/cognitive-routing-v2/render.ts +277 -0
- package/pi-extension/src/cognitive-routing-v2/runtime.ts +2241 -0
- package/pi-extension/src/cognitive-routing-v2/schemas.ts +180 -0
- package/pi-extension/src/cognitive-routing-v2/state.ts +839 -0
- package/pi-extension/src/cognitive-routing-v2/tools.ts +62 -0
- package/pi-extension/src/cognitive-routing-v2/types.ts +254 -0
- package/pi-extension/src/context-virtualization/runtime.ts +30 -8
- package/pi-extension/src/freeflow-context/tool.ts +12 -6
- package/pi-extension/src/index.ts +308 -675
- package/pi-extension/src/provider-support/astra/adapter.ts +108 -0
- package/pi-extension/src/provider-support/astra/history.ts +145 -0
- package/pi-extension/src/provider-support/astra/index.ts +12 -0
- package/pi-extension/src/provider-support/astra/session-state.ts +113 -0
- package/pi-extension/src/provider-support/index.ts +5 -0
- package/pi-extension/src/runtime/request-history.ts +243 -0
- package/pi-extension/src/runtime/runtime-context.ts +100 -17
- package/pi-extension/src/session-sources/events.ts +230 -0
- package/pi-extension/src/session-sources/provenance.ts +39 -0
- package/pi-extension/src/session-sources/read-only-session.ts +170 -0
- package/pi-extension/src/session-sources/sources.ts +334 -0
- package/pi-extension/src/settings/settings-ui.ts +311 -158
- package/pi-extension/tests/cache-reuse/history.test.js +117 -0
- package/pi-extension/tests/cache-reuse/native.test.js +91 -0
- package/pi-extension/tests/cache-reuse/settings.test.js +211 -0
- package/pi-extension/tests/cognitive-routing-v2/communication.test.js +577 -0
- package/pi-extension/tests/cognitive-routing-v2/compaction-input.test.js +183 -0
- package/pi-extension/tests/cognitive-routing-v2/delegation.test.js +557 -0
- package/pi-extension/tests/cognitive-routing-v2/native.test.js +474 -0
- package/pi-extension/tests/cognitive-routing-v2/performance.test.js +87 -0
- package/pi-extension/tests/cognitive-routing-v2/persistence.test.js +300 -0
- package/pi-extension/tests/cognitive-routing-v2/projection.test.js +241 -0
- package/pi-extension/tests/cognitive-routing-v2/recovery.test.js +1034 -0
- package/pi-extension/tests/cognitive-routing-v2/render.test.js +244 -0
- package/pi-extension/tests/cognitive-routing-v2/runtime.test.js +423 -0
- package/pi-extension/tests/cognitive-routing-v2/stabilization.test.js +521 -0
- package/pi-extension/tests/cognitive-routing-v2/state.test.js +586 -0
- package/pi-extension/tests/context-virtualization/extension.test.js +33 -7
- package/pi-extension/tests/context-virtualization/runtime.test.js +17 -3
- package/pi-extension/tests/fixtures/routing-native.js +191 -0
- package/pi-extension/tests/integration/mode-free-core.test.js +6 -4
- package/pi-extension/tests/integration/pi-extension.test.js +655 -59
- package/pi-extension/tests/integration/prompt-architecture.test.js +127 -30
- package/pi-extension/tests/integration/unaffected-regressions.test.js +78 -11
- package/pi-extension/tests/provider-support/astra-boundaries.test.js +75 -0
- package/pi-extension/tests/provider-support/astra.test.js +306 -0
- package/pi-extension/tests/provider-support/cli.test.js +92 -0
- package/pi-extension/tests/provider-support/native.test.js +183 -0
- package/plugin.json +1 -1
- package/runtime/prompts/cognitive-routing.md +4 -5
- package/runtime/prompts/core.md +51 -45
- package/runtime/prompts/interaction-contract.md +5 -5
- package/skills/action-selection/SKILL.md +57 -97
- package/skills/design-for-depth/SKILL.md +58 -193
- package/skills/design-for-depth/references/implementation-decisions.md +76 -0
- package/skills/diagnose-failure/SKILL.md +64 -196
- package/skills/discuss/SKILL.md +64 -123
- package/skills/discuss/references/discussion-continuity.md +7 -3
- package/skills/execute-work/SKILL.md +69 -88
- package/skills/execute-work/references/execute-work-edges.md +9 -3
- package/skills/handoff/SKILL.md +44 -54
- package/skills/handoff/references/handoff-templates.md +15 -6
- package/skills/review-artifact/SKILL.md +80 -127
- package/skills/review-artifact/references/independent-artifact-reviewer-contract.md +1 -1
- package/skills/review-work/SKILL.md +65 -115
- package/skills/review-work/references/independent-work-reviewer-contract.md +1 -1
- package/skills/simplify-code/SKILL.md +47 -100
- package/skills/track-work/SKILL.md +128 -119
- package/skills/track-work/references/working-record-format.md +14 -2
- package/skills/verify-work/SKILL.md +58 -62
- package/skills/verify-work/references/integration-evidence.md +4 -2
- package/skills/verify-work/references/test-design.md +89 -0
- package/skills/workflow/SKILL.md +73 -119
- package/skills/write-plan/SKILL.md +46 -77
- package/skills/write-plan/references/plan-shapes.md +5 -5
- package/skills/write-spec/SKILL.md +51 -63
- package/skills/write-spec/references/artifact-standards.md +2 -2
- package/skills/write-spec/references/decision-records.md +3 -2
- package/skills/write-spec/references/spec-shapes.md +15 -0
- package/pi-extension/dist/cognitive-routing/commands.js +0 -96
- package/pi-extension/dist/cognitive-routing/config.js +0 -232
- package/pi-extension/dist/cognitive-routing/controller.js +0 -1292
- package/pi-extension/dist/cognitive-routing/history.js +0 -451
- package/pi-extension/dist/cognitive-routing/host.js +0 -188
- package/pi-extension/dist/cognitive-routing/pi-routing-session.js +0 -414
- package/pi-extension/dist/cognitive-routing/pi-session-control.js +0 -320
- package/pi-extension/dist/cognitive-routing/runtime.js +0 -43
- package/pi-extension/dist/cognitive-routing/tool.js +0 -212
- package/pi-extension/dist/cognitive-routing/types.js +0 -6
- package/pi-extension/src/cognitive-routing/commands.ts +0 -129
- package/pi-extension/src/cognitive-routing/config.ts +0 -303
- package/pi-extension/src/cognitive-routing/controller.ts +0 -1628
- package/pi-extension/src/cognitive-routing/history.ts +0 -650
- package/pi-extension/src/cognitive-routing/host.ts +0 -266
- package/pi-extension/src/cognitive-routing/pi-routing-session.ts +0 -538
- package/pi-extension/src/cognitive-routing/pi-session-control.ts +0 -444
- package/pi-extension/src/cognitive-routing/runtime.ts +0 -57
- package/pi-extension/src/cognitive-routing/tool.ts +0 -273
- package/pi-extension/src/cognitive-routing/types.ts +0 -89
- package/pi-extension/tests/cognitive-routing/commands.test.js +0 -134
- package/pi-extension/tests/cognitive-routing/controller.test.js +0 -1128
- package/pi-extension/tests/cognitive-routing/extension-lifecycle.test.js +0 -1142
- package/pi-extension/tests/cognitive-routing/history.test.js +0 -274
- package/pi-extension/tests/cognitive-routing/host-identity.test.js +0 -63
- package/pi-extension/tests/cognitive-routing/pi-normal-lifecycle.test.js +0 -434
- package/pi-extension/tests/cognitive-routing/pi-session-control.test.js +0 -174
- package/pi-extension/tests/cognitive-routing/pi-session-history.test.js +0 -91
- package/pi-extension/tests/cognitive-routing/profile-resolution.test.js +0 -279
- package/pi-extension/tests/cognitive-routing/tool.test.js +0 -230
- package/skills/tdd/SKILL.md +0 -143
- package/skills/tdd/references/test-design.md +0 -96
- /package/pi-extension/tests/{cognitive-routing/host-fixture.js → fixtures/pi-host.js} +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { resolveCognitiveRoutingState } from "../cognitive-routing/
|
|
5
|
-
import { supportsCognitiveRoutingModelRegistry } from "../cognitive-routing/
|
|
4
|
+
import { resolveCognitiveRoutingState } from "../cognitive-routing-v2/config.js";
|
|
5
|
+
import { supportsCognitiveRoutingModelRegistry } from "../cognitive-routing-v2/config.js";
|
|
6
6
|
export const WORKFLOW_COMMANDS = [
|
|
7
7
|
{ command: "discuss", skill: "discuss" },
|
|
8
8
|
{ command: "action-selection", skill: "action-selection" },
|
|
@@ -43,7 +43,6 @@ export const FREEFLOW_MODEL_SKILL_NAMES = [
|
|
|
43
43
|
"setup-freeflow",
|
|
44
44
|
"launch-work",
|
|
45
45
|
"simplify-code",
|
|
46
|
-
"tdd",
|
|
47
46
|
"track-work",
|
|
48
47
|
"verify-work",
|
|
49
48
|
"workflow",
|
|
@@ -51,6 +50,12 @@ export const FREEFLOW_MODEL_SKILL_NAMES = [
|
|
|
51
50
|
"write-skill",
|
|
52
51
|
"write-spec",
|
|
53
52
|
];
|
|
53
|
+
export const STABLE_FREEFLOW_SURFACE = Object.freeze({
|
|
54
|
+
enabled: true,
|
|
55
|
+
cognitiveRouting: { effective: true },
|
|
56
|
+
contextVirtualization: { effective: true },
|
|
57
|
+
conversationHistory: { effective: true },
|
|
58
|
+
});
|
|
54
59
|
export const FREEFLOW_CAPABILITY_SKILL_NAMES = ["cognitive-routing", "context-virtualization", "conversation-history"];
|
|
55
60
|
export function freeflowSkillPath(skillName) {
|
|
56
61
|
return fileURLToPath(new URL(`../../../skills/${skillName}/SKILL.md`, import.meta.url));
|
|
@@ -74,7 +79,6 @@ export function freeflowModelSkillPaths(capabilityState = undefined) {
|
|
|
74
79
|
}
|
|
75
80
|
const SESSION_OVERRIDES_ENTRY = "freeflow-session-overrides";
|
|
76
81
|
const SESSION_CORE_KEYS = new Set(["enabled", "contextVirtualization", "conversationHistory"]);
|
|
77
|
-
export const COGNITIVE_ROUTING_SWITCH_TOOL_NAME = "freeflow_switch_profile";
|
|
78
82
|
export const FREEFLOW_RUNTIME_STATE_MESSAGE_TYPE = "freeflow-runtime-state";
|
|
79
83
|
export const COGNITIVE_ROUTING_RUNTIME_STATE_MESSAGE_TYPE = "freeflow-cognitive-routing-runtime-state";
|
|
80
84
|
export const WORKFLOW_BOOTSTRAP_MESSAGE_TYPE = "freeflow-workflow-bootstrap";
|
|
@@ -85,6 +89,30 @@ let currentSessionOverrides = {};
|
|
|
85
89
|
export function isPromptAvailable(value) {
|
|
86
90
|
return typeof value === "string" && value.trim().length > 0;
|
|
87
91
|
}
|
|
92
|
+
// pi-subagents stamps child system prompts with this tag before binding extensions.
|
|
93
|
+
// Keeping detection at the prompt boundary avoids a process-global child flag.
|
|
94
|
+
export const SUBAGENT_AGENT_TAG = /<active_agent\s+name="[^"]+"\s*\/>/;
|
|
95
|
+
export const FREEFLOW_SUBAGENT_CAPABILITIES_DISABLED_MARKER = "<!-- freeflow-subagent-capabilities: disabled -->";
|
|
96
|
+
const SUBAGENT_OPTIONAL_CAPABILITIES_MESSAGE = "Optional Freeflow capabilities are disabled for subagents.";
|
|
97
|
+
export function isSubagentContext(context) {
|
|
98
|
+
try {
|
|
99
|
+
const systemPrompt = context?.getSystemPrompt?.();
|
|
100
|
+
return (
|
|
101
|
+
typeof systemPrompt === "string" &&
|
|
102
|
+
(SUBAGENT_AGENT_TAG.test(systemPrompt) || systemPrompt.includes(FREEFLOW_SUBAGENT_CAPABILITIES_DISABLED_MARKER))
|
|
103
|
+
);
|
|
104
|
+
} catch {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function disableSubagentCapability(capability) {
|
|
109
|
+
return {
|
|
110
|
+
...capability,
|
|
111
|
+
enabled: false,
|
|
112
|
+
effective: false,
|
|
113
|
+
blockingReason: { code: "disabled", message: SUBAGENT_OPTIONAL_CAPABILITIES_MESSAGE },
|
|
114
|
+
};
|
|
115
|
+
}
|
|
88
116
|
export function hasUsableMandatoryPrompts(freeflowContext) {
|
|
89
117
|
return (
|
|
90
118
|
isPromptAvailable(freeflowContext?.corePrompt) && isPromptAvailable(freeflowContext?.interactionContractPrompt)
|
|
@@ -380,7 +408,8 @@ export async function readCapabilityState(cwd, host = undefined, extensionHost =
|
|
|
380
408
|
const enabled = layers.configured && effectiveCore.config.enabled;
|
|
381
409
|
const contextVirtualizationConfigEnabled = effectiveCore.config.contextVirtualization;
|
|
382
410
|
const conversationHistoryConfigEnabled = effectiveCore.config.conversationHistory;
|
|
383
|
-
const
|
|
411
|
+
const subagentContext = isSubagentContext(host);
|
|
412
|
+
const hostSupportsCognitiveRouting = !subagentContext && supportsCognitiveRoutingModelRegistry(host);
|
|
384
413
|
const configuredCognitiveRouting = await resolveCognitiveRoutingState(
|
|
385
414
|
layers.repository.parsed,
|
|
386
415
|
layers.local.parsed,
|
|
@@ -400,7 +429,7 @@ export async function readCapabilityState(cwd, host = undefined, extensionHost =
|
|
|
400
429
|
effective: false,
|
|
401
430
|
blockingReason: disabledReason,
|
|
402
431
|
};
|
|
403
|
-
|
|
432
|
+
const capabilityState = {
|
|
404
433
|
configured: layers.configured,
|
|
405
434
|
repositoryConfigured: layers.repositoryConfigured,
|
|
406
435
|
configExists: layers.repository.exists,
|
|
@@ -421,6 +450,13 @@ export async function readCapabilityState(cwd, host = undefined, extensionHost =
|
|
|
421
450
|
hostSupportsCognitiveRouting,
|
|
422
451
|
cognitiveRouting,
|
|
423
452
|
};
|
|
453
|
+
if (!subagentContext) return capabilityState;
|
|
454
|
+
return {
|
|
455
|
+
...capabilityState,
|
|
456
|
+
contextVirtualization: disableSubagentCapability(capabilityState.contextVirtualization),
|
|
457
|
+
conversationHistory: disableSubagentCapability(capabilityState.conversationHistory),
|
|
458
|
+
cognitiveRouting: disableSubagentCapability(capabilityState.cognitiveRouting),
|
|
459
|
+
};
|
|
424
460
|
}
|
|
425
461
|
export const readRuntimeState = readCapabilityState;
|
|
426
462
|
export function restoreSessionOverrides(ctx) {
|
|
@@ -468,11 +504,7 @@ export function setFreeflowStatus(
|
|
|
468
504
|
active.push(`cognitive blocked · ${cognitiveRoutingRuntime?.runtimeReason ?? "runtime_blocked"}`);
|
|
469
505
|
} else if (cognitiveRoutingActive) {
|
|
470
506
|
const profile = cognitiveRoutingRuntime.activeProfile;
|
|
471
|
-
const control =
|
|
472
|
-
cognitiveRoutingRuntime.controlMode === "manual-standard" ||
|
|
473
|
-
cognitiveRoutingRuntime.controlMode === "manual-reasoning"
|
|
474
|
-
? "manual hold"
|
|
475
|
-
: "automatic";
|
|
507
|
+
const control = String(cognitiveRoutingRuntime.controlMode).startsWith("manual-") ? "manual hold" : "automatic";
|
|
476
508
|
active.push(`${profile} · ${control}`);
|
|
477
509
|
} else if (cognitiveRouting?.enabled === true) {
|
|
478
510
|
if (cognitiveRouting.blockingReason?.code === "runtime_disabled") {
|
|
@@ -485,8 +517,8 @@ export function setFreeflowStatus(
|
|
|
485
517
|
) {
|
|
486
518
|
const startupProfile =
|
|
487
519
|
cognitiveRouting.sessionStart?.control === "manual"
|
|
488
|
-
? (cognitiveRouting.sessionStart.profile ?? "
|
|
489
|
-
: "
|
|
520
|
+
? (cognitiveRouting.sessionStart.profile ?? "coordinator")
|
|
521
|
+
: "coordinator";
|
|
490
522
|
active.push(`${startupProfile} · pending`);
|
|
491
523
|
} else {
|
|
492
524
|
const reason =
|
|
@@ -507,12 +539,12 @@ export function skillPrompt(skill, args) {
|
|
|
507
539
|
}
|
|
508
540
|
function publicCognitiveRoutingControl(controlMode) {
|
|
509
541
|
if (controlMode === "automatic") return "automatic";
|
|
510
|
-
if (
|
|
542
|
+
if (["manual-helper", "manual-executor", "manual-coordinator"].includes(controlMode)) return "manual";
|
|
511
543
|
return "unavailable";
|
|
512
544
|
}
|
|
513
545
|
function publicCognitiveRoutingProfile(activeProfile, effective) {
|
|
514
546
|
if (effective !== true) return "unavailable";
|
|
515
|
-
return
|
|
547
|
+
return ["helper", "executor", "coordinator"].includes(activeProfile) ? activeProfile : "unavailable";
|
|
516
548
|
}
|
|
517
549
|
function publicCapabilityStatus(capability) {
|
|
518
550
|
if (capability?.effective === true) return "active";
|
|
@@ -525,10 +557,20 @@ function publicCognitiveRoutingStatus(capability, runtime) {
|
|
|
525
557
|
if (runtime?.runtimeStatus === "inactive") return "inactive";
|
|
526
558
|
return publicCapabilityStatus(capability);
|
|
527
559
|
}
|
|
560
|
+
function publicCognitiveRoutingProjectionMode(capability, runtime, projectionFailure) {
|
|
561
|
+
if (capability?.projection !== true) return "disabled";
|
|
562
|
+
if (capability?.effective !== true) return "unavailable";
|
|
563
|
+
if (String(runtime?.controlMode).startsWith("manual-")) return "manual-bypass";
|
|
564
|
+
if (projectionFailure) return "blocked";
|
|
565
|
+
if (runtime?.runtimeStatus === "inactive" || runtime?.runtimeStatus === "blocked") return "unavailable";
|
|
566
|
+
if (runtime?.effective === true && runtime.controlMode === "automatic") return "enabled";
|
|
567
|
+
return "pending";
|
|
568
|
+
}
|
|
528
569
|
export function freeflowRuntimeStateMessage(
|
|
529
570
|
capabilityState,
|
|
530
571
|
cognitiveRoutingRuntime = undefined,
|
|
531
572
|
freeflowContext = undefined,
|
|
573
|
+
options = {},
|
|
532
574
|
) {
|
|
533
575
|
const cognitiveRoutingEffective = capabilityState?.cognitiveRouting?.effective === true;
|
|
534
576
|
const profile = publicCognitiveRoutingProfile(
|
|
@@ -537,6 +579,11 @@ export function freeflowRuntimeStateMessage(
|
|
|
537
579
|
);
|
|
538
580
|
const control =
|
|
539
581
|
profile === "unavailable" ? "unavailable" : publicCognitiveRoutingControl(cognitiveRoutingRuntime?.controlMode);
|
|
582
|
+
const projectionMode = publicCognitiveRoutingProjectionMode(
|
|
583
|
+
capabilityState?.cognitiveRouting,
|
|
584
|
+
cognitiveRoutingRuntime,
|
|
585
|
+
options.projectionFailure,
|
|
586
|
+
);
|
|
540
587
|
const mandatoryPromptAvailable = capabilityState?.enabled !== true || hasUsableMandatoryPrompts(freeflowContext);
|
|
541
588
|
const freeflowStatus = capabilityState?.configured
|
|
542
589
|
? capabilityState.enabled
|
|
@@ -565,6 +612,8 @@ export function freeflowRuntimeStateMessage(
|
|
|
565
612
|
"Cognitive Routing:",
|
|
566
613
|
`- Control: \`${control}\``,
|
|
567
614
|
`- Profile: \`${profile}\``,
|
|
615
|
+
`- Delegation: \`${capabilityState?.cognitiveRouting?.delegation ?? "executor"}\``,
|
|
616
|
+
`- Projection: \`${projectionMode}\``,
|
|
568
617
|
].join("\n"),
|
|
569
618
|
display: false,
|
|
570
619
|
details: { source: "provider-request-runtime-state" },
|
|
@@ -596,7 +645,9 @@ export function withFreeflowRuntimeState(
|
|
|
596
645
|
options = {},
|
|
597
646
|
) {
|
|
598
647
|
const source = Array.isArray(messages) ? messages : [];
|
|
599
|
-
const runtimeState = freeflowRuntimeStateMessage(capabilityState, cognitiveRoutingRuntime, freeflowContext
|
|
648
|
+
const runtimeState = freeflowRuntimeStateMessage(capabilityState, cognitiveRoutingRuntime, freeflowContext, {
|
|
649
|
+
projectionFailure: options.projectionFailure,
|
|
650
|
+
});
|
|
600
651
|
const runtimeStateMessages = source.filter(
|
|
601
652
|
(message) =>
|
|
602
653
|
message?.customType === FREEFLOW_RUNTIME_STATE_MESSAGE_TYPE ||
|
|
@@ -614,6 +665,24 @@ export function withFreeflowRuntimeState(
|
|
|
614
665
|
if (unchanged) return source;
|
|
615
666
|
return insertRuntimeStateBeforeLatestUser(withoutRuntimeState, runtimeState);
|
|
616
667
|
}
|
|
668
|
+
export function stableRuntimeContext(context) {
|
|
669
|
+
const sections = [
|
|
670
|
+
["Freeflow core", context?.corePrompt],
|
|
671
|
+
["Freeflow core", context?.interactionContractPrompt],
|
|
672
|
+
["Cognitive Routing", context?.cognitiveRoutingPrompt],
|
|
673
|
+
["Context Virtualization", context?.contextVirtualizationPrompt],
|
|
674
|
+
["Conversation History", context?.conversationHistoryPrompt],
|
|
675
|
+
];
|
|
676
|
+
return [
|
|
677
|
+
"# Freeflow availability contract",
|
|
678
|
+
"Freeflow keeps a fixed reference catalog of its instructions, skills, and tools to preserve prompt caching. Presence in this catalog does not mean a feature is active or an operation is permitted.",
|
|
679
|
+
"Apply the following guidance and Freeflow skills only when the latest extension-generated Freeflow Runtime State marks the corresponding feature active. When Freeflow is inactive, unavailable, or not configured, its core and capability guidance is dormant; do not bootstrap or follow it merely because it is listed. Explicit user instructions retain their normal authority.",
|
|
680
|
+
"For Cognitive Routing, follow the latest control/profile/responsibility snapshots on this branch. Earlier snapshots and notices are historical; they grant no current permission. Tool permissions are checked by the runtime, even though all definitions remain visible. Never call a disabled operation.",
|
|
681
|
+
...sections
|
|
682
|
+
.filter(([, text]) => isPromptAvailable(text))
|
|
683
|
+
.map(([feature, text]) => `## Reference guidance: ${feature}\n\n${text.trim()}`),
|
|
684
|
+
].join("\n\n");
|
|
685
|
+
}
|
|
617
686
|
export function runtimeContext(freeflowContext, capabilityState) {
|
|
618
687
|
if (
|
|
619
688
|
capabilityState?.configured !== true ||
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { activeReadOnlySessionBranch, readOnlySessionSnapshot, ReadOnlySessionError } from "./read-only-session.js";
|
|
3
|
+
import {
|
|
4
|
+
ROUTING_ENTRY,
|
|
5
|
+
RoutingError,
|
|
6
|
+
canonical,
|
|
7
|
+
eventKey,
|
|
8
|
+
eventValue,
|
|
9
|
+
requireCondition as check,
|
|
10
|
+
} from "../cognitive-routing-v2/types.js";
|
|
11
|
+
import { parseRoutingEvent, reduce, replay } from "../cognitive-routing-v2/state.js";
|
|
12
|
+
export class EventStore {
|
|
13
|
+
pi;
|
|
14
|
+
reader;
|
|
15
|
+
observed = new Map();
|
|
16
|
+
attempted = new Map();
|
|
17
|
+
fault;
|
|
18
|
+
ready = false;
|
|
19
|
+
cachedEntries = [];
|
|
20
|
+
cachedState = replay([]);
|
|
21
|
+
constructor(pi, reader) {
|
|
22
|
+
this.pi = pi;
|
|
23
|
+
this.reader = reader;
|
|
24
|
+
}
|
|
25
|
+
get blocked() {
|
|
26
|
+
return this.fault ?? (!this.ready ? "acknowledgment_unclassified" : undefined);
|
|
27
|
+
}
|
|
28
|
+
state() {
|
|
29
|
+
const entries = this.reader.getBranch();
|
|
30
|
+
const prefix =
|
|
31
|
+
this.cachedEntries.length <= entries.length && this.cachedEntries.every((entry, i) => entry === entries[i]);
|
|
32
|
+
if (!prefix) {
|
|
33
|
+
this.cachedState = replay(entries);
|
|
34
|
+
} else {
|
|
35
|
+
let state = this.cachedState;
|
|
36
|
+
for (const entry of entries.slice(this.cachedEntries.length))
|
|
37
|
+
if (entry.type === "custom" && entry.customType === ROUTING_ENTRY)
|
|
38
|
+
state = reduce(state, parseRoutingEvent(entry.data));
|
|
39
|
+
this.cachedState = state;
|
|
40
|
+
}
|
|
41
|
+
this.cachedEntries = [...entries];
|
|
42
|
+
return this.cachedState;
|
|
43
|
+
}
|
|
44
|
+
block(reason) {
|
|
45
|
+
this.fault = reason;
|
|
46
|
+
this.ready = false;
|
|
47
|
+
}
|
|
48
|
+
make(data, operationId = randomUUID(), stepId = data.type) {
|
|
49
|
+
return parseRoutingEvent({
|
|
50
|
+
version: 2,
|
|
51
|
+
eventId: randomUUID(),
|
|
52
|
+
operationId,
|
|
53
|
+
stepId,
|
|
54
|
+
recordedSessionId: this.reader.getSessionId(),
|
|
55
|
+
data,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
async reconcile() {
|
|
59
|
+
const acknowledged = this.ready && !this.fault;
|
|
60
|
+
this.ready = false;
|
|
61
|
+
const branch = this.reader.getBranch();
|
|
62
|
+
const live = branch.filter((e) => e.type === "custom" && e.customType === ROUTING_ENTRY);
|
|
63
|
+
try {
|
|
64
|
+
const ids = new Set();
|
|
65
|
+
let parent = null;
|
|
66
|
+
for (const entry of branch) {
|
|
67
|
+
check(
|
|
68
|
+
typeof entry.id === "string" && !ids.has(entry.id) && entry.parentId === parent,
|
|
69
|
+
"invalid_native_ancestry",
|
|
70
|
+
);
|
|
71
|
+
ids.add(entry.id);
|
|
72
|
+
parent = entry.id;
|
|
73
|
+
}
|
|
74
|
+
check(parent === this.reader.getLeafId(), "invalid_native_ancestry");
|
|
75
|
+
if (
|
|
76
|
+
acknowledged &&
|
|
77
|
+
live.every((entry) => {
|
|
78
|
+
const event = parseRoutingEvent(entry.data);
|
|
79
|
+
const receipt = this.observed.get(eventKey(event));
|
|
80
|
+
return (
|
|
81
|
+
receipt?.entryId === entry.id && receipt.eventId === event.eventId && receipt.value === eventValue(event)
|
|
82
|
+
);
|
|
83
|
+
})
|
|
84
|
+
) {
|
|
85
|
+
this.ready = true;
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (!live.length && !this.attempted.size) {
|
|
89
|
+
this.observed.clear();
|
|
90
|
+
this.fault = undefined;
|
|
91
|
+
this.ready = true;
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
// Pi defers creating a new session file until its first assistant message.
|
|
95
|
+
// Before that flush boundary, the complete in-memory native session is the
|
|
96
|
+
// only available source; uncertain appends still require persisted readback.
|
|
97
|
+
const preFlush =
|
|
98
|
+
!this.attempted.size &&
|
|
99
|
+
!this.reader.getEntries().some((entry) => entry.type === "message" && entry.message?.role === "assistant");
|
|
100
|
+
if (preFlush) {
|
|
101
|
+
replay(branch);
|
|
102
|
+
const known = new Map();
|
|
103
|
+
for (const entry of branch) {
|
|
104
|
+
if (entry.type === "custom" && entry.customType === ROUTING_ENTRY) {
|
|
105
|
+
const event = parseRoutingEvent(entry.data);
|
|
106
|
+
known.set(eventKey(event), { value: eventValue(event), eventId: event.eventId, entryId: entry.id });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
this.observed = known;
|
|
110
|
+
this.fault = undefined;
|
|
111
|
+
this.ready = true;
|
|
112
|
+
this.cachedEntries = [];
|
|
113
|
+
this.cachedState = replay([]);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const path = this.reader.getSessionFile();
|
|
117
|
+
check(path, "persisted_snapshot_unavailable", "Existing or uncertain routing state needs a persisted snapshot.");
|
|
118
|
+
const snapshot = await readOnlySessionSnapshot(path);
|
|
119
|
+
check(snapshot.sessionId === this.reader.getSessionId(), "session_identity_changed");
|
|
120
|
+
const persistedBranch = activeReadOnlySessionBranch(snapshot, this.reader.getLeafId());
|
|
121
|
+
check(
|
|
122
|
+
persistedBranch.length === branch.length && persistedBranch.every((e, i) => e.id === branch[i].id),
|
|
123
|
+
"snapshot_ancestry_mismatch",
|
|
124
|
+
"Live branch is not the complete persisted ancestry for its claimed leaf.",
|
|
125
|
+
);
|
|
126
|
+
replay(persistedBranch);
|
|
127
|
+
const disk = new Map(persistedBranch.map((e) => [e.id, e]));
|
|
128
|
+
const known = new Map();
|
|
129
|
+
// Compare the live selected ancestry, not whichever branch was last written on disk.
|
|
130
|
+
for (const entry of branch) {
|
|
131
|
+
const captured = disk.get(entry.id);
|
|
132
|
+
check(
|
|
133
|
+
captured && canonical(captured) === canonical(entry),
|
|
134
|
+
"snapshot_divergence",
|
|
135
|
+
"Live ancestry is not fully established by the fresh persisted snapshot; reconcile the session before retrying.",
|
|
136
|
+
);
|
|
137
|
+
if (entry.type === "custom" && entry.customType === ROUTING_ENTRY) {
|
|
138
|
+
const event = parseRoutingEvent(entry.data);
|
|
139
|
+
known.set(eventKey(event), { value: eventValue(event), eventId: event.eventId, entryId: entry.id });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
for (const [key, event] of this.attempted)
|
|
143
|
+
check(
|
|
144
|
+
known.get(key)?.value === eventValue(event) && known.get(key)?.eventId === event.eventId,
|
|
145
|
+
"append_acknowledgment_uncertain",
|
|
146
|
+
"The attempted routing event is not established by persisted readback.",
|
|
147
|
+
);
|
|
148
|
+
this.observed = known;
|
|
149
|
+
this.attempted.clear();
|
|
150
|
+
this.fault = undefined;
|
|
151
|
+
this.ready = true;
|
|
152
|
+
this.cachedEntries = [];
|
|
153
|
+
this.cachedState = replay([]);
|
|
154
|
+
} catch (error) {
|
|
155
|
+
this.block(
|
|
156
|
+
error instanceof RoutingError
|
|
157
|
+
? error.code
|
|
158
|
+
: error instanceof ReadOnlySessionError
|
|
159
|
+
? `${error.code}: ${error.message}`
|
|
160
|
+
: "snapshot_unavailable",
|
|
161
|
+
);
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
append(event) {
|
|
166
|
+
const value = parseRoutingEvent(event),
|
|
167
|
+
key = eventKey(value),
|
|
168
|
+
body = eventValue(value);
|
|
169
|
+
if (this.blocked) throw new RoutingError("acknowledgment_uncertain", this.blocked);
|
|
170
|
+
const prior = this.state().events.get(key);
|
|
171
|
+
if (prior) {
|
|
172
|
+
check(eventValue(prior) === body, "operation_conflict");
|
|
173
|
+
const occurrence = this.reader
|
|
174
|
+
.getBranch()
|
|
175
|
+
.find(
|
|
176
|
+
(entry) =>
|
|
177
|
+
entry.type === "custom" && entry.customType === ROUTING_ENTRY && entry.data?.eventId === prior.eventId,
|
|
178
|
+
);
|
|
179
|
+
if (
|
|
180
|
+
this.observed.get(key)?.value !== body ||
|
|
181
|
+
this.observed.get(key)?.eventId !== prior.eventId ||
|
|
182
|
+
this.observed.get(key)?.entryId !== occurrence?.id
|
|
183
|
+
) {
|
|
184
|
+
this.block("prior_acknowledgment_unclassified");
|
|
185
|
+
throw new RoutingError("acknowledgment_uncertain", this.blocked);
|
|
186
|
+
}
|
|
187
|
+
return prior;
|
|
188
|
+
}
|
|
189
|
+
// Validate the whole state transition before mutating Pi's session memory.
|
|
190
|
+
const candidate = reduce(this.state(), value);
|
|
191
|
+
const baseline = this.cachedEntries;
|
|
192
|
+
this.attempted.set(key, value);
|
|
193
|
+
try {
|
|
194
|
+
this.pi.appendEntry(ROUTING_ENTRY, value);
|
|
195
|
+
const found = this.reader
|
|
196
|
+
.getBranch()
|
|
197
|
+
.find((e) => e.type === "custom" && e.customType === ROUTING_ENTRY && e.data?.eventId === value.eventId);
|
|
198
|
+
check(found && eventValue(parseRoutingEvent(found.data)) === body, "append_not_observed");
|
|
199
|
+
this.observed.set(key, { value: body, eventId: value.eventId, entryId: found.id });
|
|
200
|
+
this.attempted.delete(key);
|
|
201
|
+
const branch = this.reader.getBranch();
|
|
202
|
+
const delta = branch.slice(baseline.length).filter((e) => e.type === "custom" && e.customType === ROUTING_ENTRY);
|
|
203
|
+
if (baseline.every((e, i) => branch[i] === e) && delta.length === 1 && delta[0].id === found.id) {
|
|
204
|
+
// Publish the already validated candidate only after acknowledged native append.
|
|
205
|
+
this.cachedState = candidate;
|
|
206
|
+
this.cachedEntries = [...branch];
|
|
207
|
+
}
|
|
208
|
+
return value;
|
|
209
|
+
} catch (error) {
|
|
210
|
+
this.block("append_acknowledgment_uncertain");
|
|
211
|
+
throw new RoutingError(
|
|
212
|
+
"acknowledgment_uncertain",
|
|
213
|
+
error instanceof Error ? error.message : "Pi append/readback did not acknowledge the event.",
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { isWorkerProfile } from "../cognitive-routing-v2/types.js";
|
|
2
|
+
import { isTaskEvidence, textRef } from "./sources.js";
|
|
3
|
+
export function annotateSources(messages, renderedSources, full, sources, instance) {
|
|
4
|
+
const annotated = [];
|
|
5
|
+
for (let i = 0; i < messages.length;) {
|
|
6
|
+
const group = [messages[i++]];
|
|
7
|
+
while (i < messages.length && messages[i].role === "toolResult") group.push(messages[i++]);
|
|
8
|
+
const rows = group.flatMap((message) => {
|
|
9
|
+
const s = renderedSources.get(message);
|
|
10
|
+
if (!s) return [];
|
|
11
|
+
const representation = full.has(s.ref) ? "full" : "structural only; omitted result bodies are not evidence";
|
|
12
|
+
const selection =
|
|
13
|
+
isWorkerProfile(s.producer) && isTaskEvidence(s) && full.has(s.ref)
|
|
14
|
+
? "task evidence; selection checks apply"
|
|
15
|
+
: "not offered for new evidence selection";
|
|
16
|
+
return [
|
|
17
|
+
`${s.ref} | producer: ${s.producer === "common" ? "unknown/common (no observed routing profile)" : s.producer} | ${s.original ? "assistant-text" : s.message.role}${s.message.toolName ? ` | ${s.message.toolName}` : ""}${s.assignmentId ? ` | assignment: ${s.assignmentId}` : ""} | ${representation} | ${selection}${!s.original && full.has(s.ref) && sources.byRef.has(textRef(s.ref)) ? ` | visible text: ${textRef(s.ref)}` : ""}`,
|
|
18
|
+
];
|
|
19
|
+
});
|
|
20
|
+
annotated.push(...group);
|
|
21
|
+
if (rows.length)
|
|
22
|
+
annotated.push({
|
|
23
|
+
role: "custom",
|
|
24
|
+
customType: "freeflow-routing-v2-refs",
|
|
25
|
+
display: false,
|
|
26
|
+
content: `Source provenance for the preceding message/exchange:\n${rows.join("\n")}`,
|
|
27
|
+
details: { routingInstance: instance },
|
|
28
|
+
timestamp: 0,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
return annotated;
|
|
32
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { open, stat } from "node:fs/promises";
|
|
3
|
+
import { TextDecoder } from "node:util";
|
|
4
|
+
export class ReadOnlySessionError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
line;
|
|
7
|
+
constructor(code, message, line) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.line = line;
|
|
11
|
+
this.name = "ReadOnlySessionError";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export const SESSION_READ_LIMITS = {
|
|
15
|
+
maxBytes: 512 * 1024 * 1024,
|
|
16
|
+
maxEntryBytes: 128 * 1024 * 1024,
|
|
17
|
+
maxEntries: 250000,
|
|
18
|
+
};
|
|
19
|
+
function decodeEntry(text, line, seen) {
|
|
20
|
+
let value;
|
|
21
|
+
try {
|
|
22
|
+
value = JSON.parse(text);
|
|
23
|
+
} catch {
|
|
24
|
+
throw new ReadOnlySessionError("invalid_json", "Incomplete or invalid session JSON.", line);
|
|
25
|
+
}
|
|
26
|
+
if (!value || Array.isArray(value) || typeof value !== "object" || typeof value.type !== "string")
|
|
27
|
+
throw new ReadOnlySessionError("invalid_entry", "Invalid session entry.", line);
|
|
28
|
+
if (line === 1) {
|
|
29
|
+
if (
|
|
30
|
+
value.type !== "session" ||
|
|
31
|
+
typeof value.id !== "string" ||
|
|
32
|
+
!value.id ||
|
|
33
|
+
![2, 3].includes(value.version) ||
|
|
34
|
+
typeof value.cwd !== "string"
|
|
35
|
+
)
|
|
36
|
+
throw new ReadOnlySessionError("invalid_header", "A supported native session header is required.", line);
|
|
37
|
+
} else {
|
|
38
|
+
if (value.type === "session" || typeof value.id !== "string" || !value.id || seen.has(value.id))
|
|
39
|
+
throw new ReadOnlySessionError("invalid_id", "Missing, duplicate, or misplaced session identity.", line);
|
|
40
|
+
if (value.parentId !== null && (typeof value.parentId !== "string" || !seen.has(value.parentId)))
|
|
41
|
+
throw new ReadOnlySessionError("invalid_parent", "Parent is absent or occurs after its child.", line);
|
|
42
|
+
seen.add(value.id);
|
|
43
|
+
}
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
export function parseReadOnlySessionText(text) {
|
|
47
|
+
const lines = text.split("\n");
|
|
48
|
+
if (lines.at(-1) === "") lines.pop();
|
|
49
|
+
if (!lines.length || !lines[0]) throw new ReadOnlySessionError("empty", "Session snapshot is empty.");
|
|
50
|
+
const seen = new Set();
|
|
51
|
+
return lines.map((line, i) => decodeEntry(line, i + 1, seen));
|
|
52
|
+
}
|
|
53
|
+
export async function readOnlySessionSnapshot(path, options = {}) {
|
|
54
|
+
const limits = { ...SESSION_READ_LIMITS, ...options };
|
|
55
|
+
if (Object.values(limits).some((n) => !Number.isSafeInteger(n) || n <= 0))
|
|
56
|
+
throw new ReadOnlySessionError("invalid_limit", "Invalid snapshot limit.");
|
|
57
|
+
let file;
|
|
58
|
+
try {
|
|
59
|
+
file = await open(path, "r");
|
|
60
|
+
const before = await file.stat({ bigint: true });
|
|
61
|
+
if (!before.isFile()) throw new ReadOnlySessionError("read_limit", "Session is not a regular file.");
|
|
62
|
+
if (before.size > BigInt(limits.maxBytes))
|
|
63
|
+
throw new ReadOnlySessionError(
|
|
64
|
+
"read_limit",
|
|
65
|
+
`Session exceeds the supported ${limits.maxBytes} byte snapshot limit.`,
|
|
66
|
+
);
|
|
67
|
+
const hash = createHash("sha256"),
|
|
68
|
+
entries = [],
|
|
69
|
+
seen = new Set();
|
|
70
|
+
let total = 0,
|
|
71
|
+
lineBytes = 0,
|
|
72
|
+
parts = [];
|
|
73
|
+
const append = (part) => {
|
|
74
|
+
lineBytes += part.length;
|
|
75
|
+
if (lineBytes > limits.maxEntryBytes)
|
|
76
|
+
throw new ReadOnlySessionError(
|
|
77
|
+
"entry_limit",
|
|
78
|
+
`Session entry exceeds ${limits.maxEntryBytes} bytes.`,
|
|
79
|
+
entries.length + 1,
|
|
80
|
+
);
|
|
81
|
+
if (part.length) parts.push(part);
|
|
82
|
+
};
|
|
83
|
+
const decode = () => {
|
|
84
|
+
if (entries.length >= limits.maxEntries)
|
|
85
|
+
throw new ReadOnlySessionError("entry_count_limit", `Session exceeds ${limits.maxEntries} entries.`);
|
|
86
|
+
let text;
|
|
87
|
+
try {
|
|
88
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(parts, lineBytes));
|
|
89
|
+
} catch {
|
|
90
|
+
throw new ReadOnlySessionError("invalid_encoding", "Session is not complete UTF-8.", entries.length + 1);
|
|
91
|
+
}
|
|
92
|
+
entries.push(decodeEntry(text, entries.length + 1, seen));
|
|
93
|
+
parts = [];
|
|
94
|
+
lineBytes = 0;
|
|
95
|
+
};
|
|
96
|
+
for (;;) {
|
|
97
|
+
const buffer = Buffer.alloc(64 * 1024),
|
|
98
|
+
{ bytesRead } = await file.read(buffer, 0, buffer.length, null);
|
|
99
|
+
if (!bytesRead) break;
|
|
100
|
+
total += bytesRead;
|
|
101
|
+
if (total > limits.maxBytes)
|
|
102
|
+
throw new ReadOnlySessionError("read_limit", "Session grew beyond the supported snapshot limit.");
|
|
103
|
+
const bytes = buffer.subarray(0, bytesRead);
|
|
104
|
+
hash.update(bytes);
|
|
105
|
+
let start = 0,
|
|
106
|
+
end;
|
|
107
|
+
while ((end = bytes.indexOf(10, start)) !== -1) {
|
|
108
|
+
append(bytes.subarray(start, end));
|
|
109
|
+
decode();
|
|
110
|
+
start = end + 1;
|
|
111
|
+
}
|
|
112
|
+
append(bytes.subarray(start));
|
|
113
|
+
}
|
|
114
|
+
if (lineBytes) decode();
|
|
115
|
+
if (!entries.length) throw new ReadOnlySessionError("empty", "Session snapshot is empty.");
|
|
116
|
+
const after = await file.stat({ bigint: true }),
|
|
117
|
+
current = await stat(path, { bigint: true });
|
|
118
|
+
const signature = (v) => [v.dev, v.ino, v.size, v.mtimeNs, v.ctimeNs].join(":");
|
|
119
|
+
if (
|
|
120
|
+
signature(before) !== signature(after) ||
|
|
121
|
+
signature(after) !== signature(current) ||
|
|
122
|
+
after.size !== BigInt(total)
|
|
123
|
+
)
|
|
124
|
+
throw new ReadOnlySessionError("source_changed", "Session changed during snapshot capture.");
|
|
125
|
+
return { path, sessionId: entries[0].id, version: entries[0].version, hash: hash.digest("hex"), entries };
|
|
126
|
+
} catch (error) {
|
|
127
|
+
if (error instanceof ReadOnlySessionError) throw error;
|
|
128
|
+
throw new ReadOnlySessionError("read_failed", error instanceof Error ? error.message : String(error));
|
|
129
|
+
} finally {
|
|
130
|
+
await file?.close();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
export function activeReadOnlySessionBranch(snapshot, leafId) {
|
|
134
|
+
if (leafId === null) return [];
|
|
135
|
+
const byId = new Map(snapshot.entries.slice(1).map((entry) => [entry.id, entry]));
|
|
136
|
+
const path = [];
|
|
137
|
+
const seen = new Set();
|
|
138
|
+
let cursor = leafId;
|
|
139
|
+
while (cursor !== null) {
|
|
140
|
+
const entry = byId.get(cursor);
|
|
141
|
+
if (!entry || seen.has(cursor))
|
|
142
|
+
throw new ReadOnlySessionError("ancestry_unavailable", "Requested native ancestry is unavailable.");
|
|
143
|
+
seen.add(cursor);
|
|
144
|
+
path.push(entry);
|
|
145
|
+
cursor = entry.parentId;
|
|
146
|
+
}
|
|
147
|
+
return path.reverse();
|
|
148
|
+
}
|