@hassangameryt/freeflow 0.8.0 → 0.9.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.
- package/.agents/plugins/marketplace.json +2 -2
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +2 -2
- package/.codex-plugin/plugin.json +3 -3
- package/.cursor-plugin/plugin.json +1 -1
- package/README.md +96 -149
- package/capabilities/cognitive-routing/SKILL.md +21 -129
- package/capabilities/cognitive-routing/references/coordinator.md +163 -0
- package/capabilities/cognitive-routing/references/worker.md +38 -0
- package/capabilities/compaction/SKILL.md +3 -3
- package/capabilities/compaction/references/summary-format.md +26 -15
- package/capabilities/messaging/SKILL.md +49 -0
- package/capabilities/task-memory/SKILL.md +87 -0
- package/command-surface.json +6 -54
- package/gemini-extension.json +1 -1
- package/hooks/claude/hooks.json +13 -3
- package/hooks/codex/hooks.json +2 -1
- package/hooks/shared/runtime-context.mjs +69 -77
- package/package.json +25 -8
- package/pi-extension/dist/ask-user/ask.js +108 -0
- package/pi-extension/dist/ask-user/form.js +250 -0
- package/pi-extension/dist/ask-user/questions.js +66 -0
- package/pi-extension/dist/ask-user/tool.js +124 -0
- package/pi-extension/dist/cognitive-routing/assembler.js +35 -11
- package/pi-extension/dist/cognitive-routing/bootstrap.js +90 -0
- package/pi-extension/dist/cognitive-routing/config.js +3 -4
- package/pi-extension/dist/cognitive-routing/event-store.js +4 -1
- package/pi-extension/dist/cognitive-routing/gate.js +20 -5
- package/pi-extension/dist/cognitive-routing/handlers.js +15 -13
- package/pi-extension/dist/cognitive-routing/model-control.js +17 -8
- package/pi-extension/dist/cognitive-routing/projection.js +23 -6
- package/pi-extension/dist/cognitive-routing/provenance.js +3 -1
- package/pi-extension/dist/cognitive-routing/render.js +15 -15
- package/pi-extension/dist/cognitive-routing/runtime.js +4 -2
- package/pi-extension/dist/cognitive-routing/schemas.js +4 -4
- package/pi-extension/dist/cognitive-routing/session.js +3 -1
- package/pi-extension/dist/cognitive-routing/sources.js +29 -4
- package/pi-extension/dist/cognitive-routing/state.js +18 -8
- package/pi-extension/dist/cognitive-routing/status.js +4 -3
- package/pi-extension/dist/cognitive-routing/tools.js +29 -15
- package/pi-extension/dist/cognitive-routing/types.js +10 -1
- package/pi-extension/dist/compaction/carry.js +36 -22
- package/pi-extension/dist/compaction/config.js +4 -2
- package/pi-extension/dist/compaction/controller.js +52 -29
- package/pi-extension/dist/compaction/harness.js +19 -3
- package/pi-extension/dist/compaction/results.js +14 -13
- package/pi-extension/dist/compaction/tool.js +6 -6
- package/pi-extension/dist/host/catalog.js +20 -35
- package/pi-extension/dist/host/config-validation.js +233 -0
- package/pi-extension/dist/host/config.js +78 -91
- package/pi-extension/dist/host/inbound.js +142 -0
- package/pi-extension/dist/host/projection-tags.js +1 -1
- package/pi-extension/dist/host/prompts.js +24 -13
- package/pi-extension/dist/host/read-only-session.js +6 -1
- package/pi-extension/dist/host/request-history.js +7 -3
- package/pi-extension/dist/host/rows.js +25 -0
- package/pi-extension/dist/host/runtime-state.js +17 -2
- package/pi-extension/dist/host/section-keeper.js +85 -0
- package/pi-extension/dist/host/settings/freeflow-command.js +846 -93
- package/pi-extension/dist/host/settings/settings-view.js +27 -8
- package/pi-extension/dist/host/status.js +19 -5
- package/pi-extension/dist/index.js +638 -72
- package/pi-extension/dist/messaging/config.js +27 -0
- package/pi-extension/dist/messaging/messaging.js +398 -0
- package/pi-extension/dist/messaging/registry.js +57 -0
- package/pi-extension/dist/messaging/render.js +90 -0
- package/pi-extension/dist/messaging/tools.js +109 -0
- package/pi-extension/dist/messaging/transport.js +101 -0
- package/pi-extension/dist/provider-support/cache/keepalive.js +16 -4
- package/pi-extension/dist/task-memory/config.js +49 -0
- package/pi-extension/dist/task-memory/registry.js +119 -0
- package/pi-extension/dist/task-memory/render.js +107 -0
- package/pi-extension/dist/task-memory/runtime.js +994 -0
- package/pi-extension/dist/task-memory/tool.js +233 -0
- package/pi-extension/dist/tool-execution/apply-patch/tool.js +1 -1
- package/pi-extension/dist/tool-execution/background.js +220 -28
- package/pi-extension/dist/tool-execution/config.js +14 -3
- package/pi-extension/dist/tool-execution/file-tracking.js +24 -4
- package/pi-extension/dist/tool-execution/guidance-notices.js +124 -0
- package/pi-extension/dist/tool-execution/skill-reads.js +143 -0
- package/pi-extension/dist/tool-execution/tools.js +2 -2
- package/runtime/prompts/cognitive-routing.md +4 -4
- package/runtime/prompts/core.md +24 -14
- package/runtime/prompts/interaction-contract.md +7 -4
- package/runtime/prompts/tool-execution.md +3 -16
- package/runtime/prompts/working-method.md +16 -99
- package/skills/deliver-work/SKILL.md +76 -0
- package/skills/deliver-work/references/branch-closeout.md +134 -0
- package/skills/deliver-work/references/commit.md +83 -0
- package/skills/deliver-work/references/launch.md +94 -0
- package/skills/deliver-work/references/release.md +85 -0
- package/skills/deliver-work/scripts/commit-state.mjs +139 -0
- package/skills/deliver-work/scripts/delivery-state.mjs +107 -0
- package/skills/deliver-work/scripts/lib/git-state.mjs +178 -0
- package/skills/deliver-work/scripts/push-route.mjs +122 -0
- package/skills/deliver-work/scripts/release-preflight.mjs +177 -0
- package/skills/discuss-work/SKILL.md +148 -0
- package/skills/{discuss → discuss-work}/references/discussion-continuity.md +10 -27
- package/skills/discuss-work/references/learning-action.md +36 -0
- package/skills/discuss-work/scripts/choices.mjs +101 -0
- package/skills/discuss-work/scripts/lib/choices.mjs +159 -0
- package/skills/evaluate-behavior/SKILL.md +107 -0
- package/skills/evaluate-behavior/references/authoring.md +381 -0
- package/skills/evaluate-behavior/references/coding-tasks.md +145 -0
- package/skills/evaluate-behavior/references/conversations-pipelines-judges.md +177 -0
- package/skills/evaluate-behavior/references/design-and-measurement.md +273 -0
- package/skills/evaluate-behavior/references/running-and-evidence.md +245 -0
- package/skills/evaluate-behavior/scripts/claude-guard.mjs +76 -0
- package/skills/evaluate-behavior/scripts/guard-rules.mjs +309 -0
- package/skills/evaluate-behavior/scripts/lib/agreement.mjs +90 -0
- package/skills/evaluate-behavior/scripts/lib/arms.mjs +55 -0
- package/skills/evaluate-behavior/scripts/lib/budget.mjs +87 -0
- package/skills/evaluate-behavior/scripts/lib/build-source.mjs +287 -0
- package/skills/evaluate-behavior/scripts/lib/clear.mjs +74 -0
- package/skills/evaluate-behavior/scripts/lib/container.mjs +395 -0
- package/skills/evaluate-behavior/scripts/lib/decision.mjs +418 -0
- package/skills/evaluate-behavior/scripts/lib/definitions.mjs +1072 -0
- package/skills/evaluate-behavior/scripts/lib/freeflow.mjs +103 -0
- package/skills/evaluate-behavior/scripts/lib/grade-command.mjs +591 -0
- package/skills/evaluate-behavior/scripts/lib/grade-comparison.mjs +90 -0
- package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/grade-interaction.mjs +19 -18
- package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/grade-support.mjs +32 -5
- package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/grade-workspace.mjs +28 -12
- package/skills/evaluate-behavior/scripts/lib/grade.mjs +101 -0
- package/skills/evaluate-behavior/scripts/lib/hooks.mjs +98 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/confinement.mjs +196 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/constraints.mjs +111 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/index.mjs +48 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/isolation.mjs +74 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/launch.mjs +123 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/model-call.mjs +73 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/recorder.mjs +178 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/session.mjs +494 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/skills.mjs +81 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/index.mjs +31 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/names.mjs +3 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/pi/confinement.mjs +173 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/pi/host.mjs +148 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/pi/index.mjs +70 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/pi/isolation.mjs +81 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/pi/model-call.mjs +79 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/pi/process.mjs +241 -0
- package/skills/{evaluate-skill/scripts/lib → evaluate-behavior/scripts/lib/hosts/pi}/rpc.mjs +144 -26
- package/skills/evaluate-behavior/scripts/lib/identity.mjs +82 -0
- package/skills/evaluate-behavior/scripts/lib/judge.mjs +688 -0
- package/skills/evaluate-behavior/scripts/lib/metrics.mjs +332 -0
- package/skills/evaluate-behavior/scripts/lib/model-call.mjs +17 -0
- package/skills/evaluate-behavior/scripts/lib/participants.mjs +616 -0
- package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/process.mjs +22 -0
- package/skills/evaluate-behavior/scripts/lib/queue.mjs +54 -0
- package/skills/evaluate-behavior/scripts/lib/reference.mjs +343 -0
- package/skills/evaluate-behavior/scripts/lib/reuse.mjs +277 -0
- package/skills/evaluate-behavior/scripts/lib/review.mjs +933 -0
- package/skills/evaluate-behavior/scripts/lib/runner.mjs +732 -0
- package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/sandbox.mjs +119 -14
- package/skills/evaluate-behavior/scripts/lib/scan-shell.mjs +457 -0
- package/skills/evaluate-behavior/scripts/lib/scan.mjs +479 -0
- package/skills/evaluate-behavior/scripts/lib/seal.mjs +91 -0
- package/skills/evaluate-behavior/scripts/lib/shell.mjs +560 -0
- package/skills/evaluate-behavior/scripts/lib/source-override.mjs +64 -0
- package/skills/evaluate-behavior/scripts/lib/subject-file.mjs +88 -0
- package/skills/{evaluate-skill/scripts/lib/pi.mjs → evaluate-behavior/scripts/lib/subject.mjs} +235 -265
- package/skills/evaluate-behavior/scripts/lib/task-copy.mjs +124 -0
- package/skills/evaluate-behavior/scripts/lib/trials.mjs +524 -0
- package/skills/evaluate-behavior/scripts/lib/validity.mjs +68 -0
- package/skills/evaluate-behavior/scripts/lib/view-html.mjs +454 -0
- package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/view.mjs +159 -18
- package/skills/{evaluate-skill → evaluate-behavior}/scripts/pi-command-tool.mjs +21 -6
- package/skills/evaluate-behavior/scripts/pi-container-route.mjs +23 -0
- package/skills/evaluate-behavior/scripts/pi-guard.mjs +77 -0
- package/skills/evaluate-behavior/scripts/pi-login-refresh.mjs +31 -0
- package/skills/evaluate-behavior/scripts/skill-eval.mjs +586 -0
- package/skills/execute-work/SKILL.md +96 -104
- package/skills/execute-work/references/code-practices.md +27 -51
- package/skills/execute-work/references/design.md +271 -0
- package/skills/execute-work/references/diagnose.md +117 -0
- package/skills/execute-work/references/migration.md +108 -0
- package/skills/execute-work/references/simplify.md +65 -0
- package/skills/execute-work/scripts/self-review.mjs +422 -0
- package/skills/execute-work/scripts/touch-map.mjs +290 -0
- package/skills/review-work/SKILL.md +89 -65
- package/skills/review-work/references/adjudication.md +94 -0
- package/skills/review-work/references/artifact.md +198 -0
- package/skills/review-work/references/evals-and-judges.md +66 -0
- package/skills/review-work/references/work.md +177 -0
- package/skills/review-work/scripts/check-brief.mjs +128 -0
- package/skills/review-work/scripts/review-packet.mjs +233 -0
- package/skills/setup-freeflow/SKILL.md +68 -17
- package/skills/track-work/SKILL.md +52 -20
- package/skills/track-work/references/handoff.md +190 -0
- package/skills/track-work/references/projects.md +51 -0
- package/skills/track-work/references/working-record-format.md +36 -18
- package/skills/track-work/scripts/lib/document.mjs +17 -13
- package/skills/track-work/scripts/lib/format.mjs +2 -0
- package/skills/track-work/scripts/lib/recover.mjs +254 -0
- package/skills/track-work/scripts/lib/registry.mjs +656 -0
- package/skills/track-work/scripts/lib/search.mjs +40 -0
- package/skills/track-work/scripts/lib/store.mjs +46 -15
- package/skills/track-work/scripts/lib/views.mjs +50 -1
- package/skills/track-work/scripts/working-record.mjs +232 -13
- package/skills/verify-work/SKILL.md +45 -37
- package/skills/verify-work/references/feature-map.md +120 -0
- package/skills/verify-work/references/runtime-evidence.md +203 -0
- package/skills/verify-work/references/test-design.md +25 -6
- package/skills/verify-work/scripts/test-changes.mjs +246 -0
- package/skills/write-docs/SKILL.md +113 -25
- package/skills/write-docs/references/documentation-shapes.md +68 -3
- package/skills/write-docs/references/spec-and-plan-shapes.md +308 -0
- package/skills/write-docs/scripts/artifact-check.mjs +148 -0
- package/skills/write-docs/scripts/docs-check.mjs +96 -6
- package/skills/write-docs/scripts/plan-lint.mjs +123 -0
- package/skills/write-skill/SKILL.md +92 -52
- package/skills/write-skill/references/activation-and-wording.md +143 -0
- package/skills/write-skill/references/placement-and-dependencies.md +154 -0
- package/skills/write-skill/scripts/lib/compare.mjs +701 -0
- package/skills/write-skill/scripts/lib/skill-author.mjs +197 -3
- package/skills/write-skill/scripts/skill-author.mjs +51 -8
- package/capabilities/cognitive-routing/references/both-mode.md +0 -36
- package/capabilities/cognitive-routing/references/executor-mode.md +0 -48
- package/capabilities/cognitive-routing/references/helper-mode.md +0 -47
- package/plugin.json +0 -33
- package/skills/action-selection/SKILL.md +0 -92
- package/skills/action-selection/references/trajectory-stalls.md +0 -73
- package/skills/bypass/SKILL.md +0 -153
- package/skills/commit-work/SKILL.md +0 -93
- package/skills/commit-work/references/git-state-edges.md +0 -94
- package/skills/commit-work/references/simple-push.md +0 -94
- package/skills/commit-work/references/staging-decisions.md +0 -106
- package/skills/decision-gate/SKILL.md +0 -87
- package/skills/design-for-depth/SKILL.md +0 -108
- package/skills/design-for-depth/references/design-pressure-signals.md +0 -84
- package/skills/design-for-depth/references/implementation-decisions.md +0 -76
- package/skills/design-for-depth/references/interface-design-loop.md +0 -105
- package/skills/design-for-depth/references/module-and-dependency-design.md +0 -117
- package/skills/design-for-depth/references/state-and-failure-boundaries.md +0 -125
- package/skills/diagnose-failure/SKILL.md +0 -140
- package/skills/diagnose-failure/references/diagnostic-loop-catalog.md +0 -50
- package/skills/diagnose-failure/references/flaky-and-performance-diagnosis.md +0 -51
- package/skills/discuss/SKILL.md +0 -140
- package/skills/evaluate-skill/SKILL.md +0 -188
- package/skills/evaluate-skill/references/evaluation-definition-schema.md +0 -415
- package/skills/evaluate-skill/references/evaluation-design.md +0 -112
- package/skills/evaluate-skill/references/execution-and-evidence.md +0 -111
- package/skills/evaluate-skill/references/review-and-revision.md +0 -94
- package/skills/evaluate-skill/scripts/lib/definitions.mjs +0 -448
- package/skills/evaluate-skill/scripts/lib/grade-comparison.mjs +0 -60
- package/skills/evaluate-skill/scripts/lib/grade.mjs +0 -64
- package/skills/evaluate-skill/scripts/lib/review.mjs +0 -196
- package/skills/evaluate-skill/scripts/lib/runner.mjs +0 -405
- package/skills/evaluate-skill/scripts/lib/trials.mjs +0 -103
- package/skills/evaluate-skill/scripts/pi-guard.mjs +0 -181
- package/skills/evaluate-skill/scripts/skill-eval.mjs +0 -182
- package/skills/execute-work/references/execute-work-edges.md +0 -78
- package/skills/finish-branch/SKILL.md +0 -117
- package/skills/finish-branch/references/integration-options.md +0 -80
- package/skills/handoff/SKILL.md +0 -82
- package/skills/handoff/references/handoff-templates.md +0 -150
- package/skills/launch-work/SKILL.md +0 -116
- package/skills/launch-work/references/launch-readiness.md +0 -85
- package/skills/migration-work/SKILL.md +0 -115
- package/skills/migration-work/references/migration-lifecycle.md +0 -82
- package/skills/release-work/SKILL.md +0 -118
- package/skills/release-work/references/release-evidence.md +0 -78
- package/skills/review-artifact/SKILL.md +0 -143
- package/skills/review-artifact/references/adjudicate-artifact-review.md +0 -101
- package/skills/review-artifact/references/independent-artifact-reviewer-contract.md +0 -159
- package/skills/review-work/references/adjudicate-work-review.md +0 -97
- package/skills/review-work/references/independent-work-reviewer-contract.md +0 -139
- package/skills/review-work/references/security-risk-lens.md +0 -67
- package/skills/setup-freeflow/references/activation-contract.md +0 -103
- package/skills/setup-freeflow/references/host-setup.md +0 -67
- package/skills/simplify-code/SKILL.md +0 -85
- package/skills/simplify-code/references/simplification-patterns.md +0 -70
- package/skills/verify-work/references/browser-runtime-evidence.md +0 -51
- package/skills/verify-work/references/integration-evidence.md +0 -49
- package/skills/verify-work/references/performance-evidence.md +0 -59
- package/skills/workflow/SKILL.md +0 -171
- package/skills/workflow/references/domain-skill-composition.md +0 -45
- package/skills/write-plan/SKILL.md +0 -92
- package/skills/write-plan/references/plan-shapes.md +0 -156
- package/skills/write-skill/references/activation-boundaries.md +0 -106
- package/skills/write-skill/references/agent-first-instructions.md +0 -133
- package/skills/write-skill/references/progressive-disclosure.md +0 -129
- package/skills/write-skill/references/skill-dependency-graphs.md +0 -192
- package/skills/write-spec/SKILL.md +0 -85
- package/skills/write-spec/references/artifact-standards.md +0 -86
- package/skills/write-spec/references/decision-records.md +0 -80
- package/skills/write-spec/references/spec-shapes.md +0 -143
- /package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/evidence.mjs +0 -0
- /package/skills/{evaluate-skill → evaluate-behavior}/scripts/pi-observer.mjs +0 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The inbound channel: content that reaches the model from outside the turn (a background command's exit, and later
|
|
3
|
+
* monitor events and messages from other sessions). One delivery rule for all of them:
|
|
4
|
+
* - while the agent works, a notice is steered in after the current tool batch;
|
|
5
|
+
* - while it is idle, a notice that should wake it starts a turn as Freeflow's own message (host/section-keeper.ts
|
|
6
|
+
* keeps the last prompt's system sections on such a run);
|
|
7
|
+
* - a notice that should not wake the agent waits for the next turn, and so does every notice to a session that has
|
|
8
|
+
* never had a prompt: Pi before 1.2.0 builds the system prompt only for prompts, so a turn there would have none;
|
|
9
|
+
* Notices from one source within a batching window join one notice, and a source that sends faster than its limit is
|
|
10
|
+
* stopped with one notice saying so. Pi sends every custom message to the model in the user role, so a notice marks
|
|
11
|
+
* its source itself: a `<freeflow-notice>` tag for Freeflow's own (freeflowNotice()), a `<message>` tag for a message
|
|
12
|
+
* from another session. Notices are appended to the conversation; nothing earlier changes.
|
|
13
|
+
*/
|
|
14
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
15
|
+
/** The marker Freeflow's notices carried before the tag; saved sessions still contain it, so rows still read it. */
|
|
16
|
+
export const LEGACY_NOTICE_PREFIX = "[Freeflow notice, not from the user]";
|
|
17
|
+
/**
|
|
18
|
+
* Text that came from outside Freeflow (a command's output, another session's message) with every `<freeflow-notice`
|
|
19
|
+
* tag in it broken, so it can neither close a notice nor open one that looks like Freeflow's.
|
|
20
|
+
*/
|
|
21
|
+
export function withoutNoticeTags(text) {
|
|
22
|
+
return text.replace(/<(\/?)(freeflow-notice)/gi, "<\\$1$2");
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Freeflow's own notice as the model reads it. Pi gives the model every custom message in the user role; the tag, which
|
|
26
|
+
* Freeflow's guidance explains, says the text is Freeflow's and not the user's. The text cannot open or close the tag.
|
|
27
|
+
*/
|
|
28
|
+
export function freeflowNotice(type, text) {
|
|
29
|
+
return `<freeflow-notice type="${type}">\n${withoutNoticeTags(text)}\n</freeflow-notice>`;
|
|
30
|
+
}
|
|
31
|
+
/** A notice's text without its tag, or without the old prefix. */
|
|
32
|
+
export function noticeBody(content) {
|
|
33
|
+
return content
|
|
34
|
+
.replace(/^\s*<freeflow-notice[^>]*>\n?/, "")
|
|
35
|
+
.replace(/\n?<\/freeflow-notice>\s*$/, "")
|
|
36
|
+
.split(LEGACY_NOTICE_PREFIX)
|
|
37
|
+
.join("")
|
|
38
|
+
.trim();
|
|
39
|
+
}
|
|
40
|
+
export class Inbound {
|
|
41
|
+
host;
|
|
42
|
+
pending = new Map();
|
|
43
|
+
sent = new Map();
|
|
44
|
+
limited = new Set();
|
|
45
|
+
constructor(host) {
|
|
46
|
+
this.host = host;
|
|
47
|
+
}
|
|
48
|
+
/** Deliver one notice now, or, with a batch key, after its window together with the others for that key. */
|
|
49
|
+
deliver(notice, batch) {
|
|
50
|
+
if (!batch) return void this.send(notice);
|
|
51
|
+
if (this.limited.has(batch.key)) return;
|
|
52
|
+
const pending = this.pending.get(batch.key);
|
|
53
|
+
if (pending) {
|
|
54
|
+
pending.notices.push(notice);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const timer = setTimeout(() => this.flush(batch.key), batch.windowMs ?? 200);
|
|
58
|
+
this.pending.set(batch.key, { notices: [notice], timer, batch });
|
|
59
|
+
}
|
|
60
|
+
/** Deliver what a key has pending now, before its window ends: its source finished and has more to say after it. */
|
|
61
|
+
flush(key) {
|
|
62
|
+
const pending = this.pending.get(key);
|
|
63
|
+
if (!pending) return;
|
|
64
|
+
clearTimeout(pending.timer);
|
|
65
|
+
this.pending.delete(key);
|
|
66
|
+
this.sendBatch(pending);
|
|
67
|
+
}
|
|
68
|
+
/** Forget a source: drop what it has pending and its limit state. */
|
|
69
|
+
close(key) {
|
|
70
|
+
const pending = this.pending.get(key);
|
|
71
|
+
if (pending) clearTimeout(pending.timer);
|
|
72
|
+
this.pending.delete(key);
|
|
73
|
+
this.sent.delete(key);
|
|
74
|
+
this.limited.delete(key);
|
|
75
|
+
}
|
|
76
|
+
/** Drop everything pending, at session end. */
|
|
77
|
+
reset() {
|
|
78
|
+
for (const { timer } of this.pending.values()) clearTimeout(timer);
|
|
79
|
+
this.pending.clear();
|
|
80
|
+
this.sent.clear();
|
|
81
|
+
this.limited.clear();
|
|
82
|
+
}
|
|
83
|
+
sendBatch({ notices, batch }) {
|
|
84
|
+
const now = Date.now();
|
|
85
|
+
const recent = (this.sent.get(batch.key) ?? []).filter((at) => now - at < 60_000);
|
|
86
|
+
if (batch.perMinute !== undefined && recent.length >= batch.perMinute) {
|
|
87
|
+
this.limited.add(batch.key);
|
|
88
|
+
batch.onLimit?.();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
this.sent.set(batch.key, [...recent, now]);
|
|
92
|
+
const [first] = notices;
|
|
93
|
+
// send() never rejects: a failed delivery is dropped there.
|
|
94
|
+
void this.send({
|
|
95
|
+
customType: first.customType,
|
|
96
|
+
content: batch.join ? batch.join(notices) : notices.map((notice) => notice.content).join("\n"),
|
|
97
|
+
details: { ...first.details, batched: notices.length },
|
|
98
|
+
wake: notices.some((notice) => notice.wake),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
send(notice) {
|
|
102
|
+
const message = { customType: notice.customType, content: notice.content, display: true, details: notice.details };
|
|
103
|
+
const wake = notice.wake && this.host.canWake?.() !== false;
|
|
104
|
+
// A send that throws at once is caught like one that rejects later: a notice never breaks its source.
|
|
105
|
+
const asMessage = () =>
|
|
106
|
+
new Promise((resolve) =>
|
|
107
|
+
resolve(
|
|
108
|
+
this.host.sendMessage(message, wake ? { triggerTurn: true, deliverAs: "steer" } : { triggerTurn: false }),
|
|
109
|
+
),
|
|
110
|
+
).then(
|
|
111
|
+
() => {},
|
|
112
|
+
() => {},
|
|
113
|
+
);
|
|
114
|
+
// Always Freeflow's own message, never the user's: the run it starts keeps Freeflow's system sections
|
|
115
|
+
// (host/section-keeper.ts); Pi #10267 once made idle notices go out as user messages.
|
|
116
|
+
return asMessage();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* A notice in the transcript: one line naming its source and what happened, expanding to the whole notice. The model
|
|
121
|
+
* receives the same text either way.
|
|
122
|
+
*/
|
|
123
|
+
export function renderNotice(label) {
|
|
124
|
+
return (message, options, theme) => {
|
|
125
|
+
const content = typeof message?.content === "string" ? message.content : "";
|
|
126
|
+
const body = noticeBody(content);
|
|
127
|
+
const paint = (color, text) => (theme?.fg ? theme.fg(color, text) : text);
|
|
128
|
+
// Blank lines separate a notice's parts; the row skips them.
|
|
129
|
+
const lines = body.split("\n").filter((line) => line.trim() !== "");
|
|
130
|
+
// A heading that ends in ":" introduces lines (a monitor's events): the row shows the first of them too.
|
|
131
|
+
const introduces = (lines[0] ?? "").endsWith(":") && lines.length > 1;
|
|
132
|
+
const more = introduces && lines.length > 2 ? ` (+${lines.length - 2} more)` : "";
|
|
133
|
+
// A long first line (a compaction notice is one paragraph) shows its first sentence; the rest is on expand.
|
|
134
|
+
const opening = lines[0] ?? "";
|
|
135
|
+
const sentence = opening.length > 140 ? opening.slice(0, opening.search(/\.(\s|$)/) + 1) || opening : opening;
|
|
136
|
+
const first = introduces ? `${lines[0]} ${lines[1]}${more}` : sentence;
|
|
137
|
+
// "Background command bg1 completed" reads as label plus the rest; anything else gets the label in front.
|
|
138
|
+
const rest = first.toLowerCase().startsWith(label.toLowerCase()) ? first.slice(label.length).trimStart() : first;
|
|
139
|
+
const head = `${paint("customMessageLabel", label)} ${paint("muted", rest)}`;
|
|
140
|
+
return new Text(options?.expanded && (body.includes("\n") || first !== body) ? `${head}\n\n${body}` : head, 0, 0);
|
|
141
|
+
};
|
|
142
|
+
}
|
|
@@ -17,7 +17,7 @@ export function tagProjectedMessages(messages, projection, branch) {
|
|
|
17
17
|
for (const projected of entry.messages) if (projected?.role !== messages[index++]?.role) return;
|
|
18
18
|
// Host edits change the visible body, so their targets keep exact content association.
|
|
19
19
|
const edited = new Set();
|
|
20
|
-
for (const entry of branch) if (entry.type === "context_edit") edited.add(entry.targetId);
|
|
20
|
+
for (const entry of branch) if (entry.type === "context_edit" && entry.targetId) edited.add(entry.targetId);
|
|
21
21
|
index = 0;
|
|
22
22
|
for (const entry of projection.entries) {
|
|
23
23
|
const source = entry.sourceEntry;
|
|
@@ -50,33 +50,44 @@ function runtimeContextCacheSatisfies(capabilityState) {
|
|
|
50
50
|
interactionContractPrompt: capabilityState?.enabled === true,
|
|
51
51
|
cognitiveRoutingPrompt: capabilityState?.cognitiveRouting?.effective === true,
|
|
52
52
|
};
|
|
53
|
-
return Object.entries(expected).every(
|
|
53
|
+
return Object.entries(expected).every(
|
|
54
|
+
([key, required]) => !required || isPromptAvailable(runtimeContextCache?.[key]),
|
|
55
|
+
);
|
|
54
56
|
}
|
|
55
57
|
export async function refreshRuntimeContext(capabilityState = undefined) {
|
|
56
58
|
runtimeContextCache = await loadRuntimeContext(capabilityState);
|
|
57
59
|
return runtimeContextCache;
|
|
58
60
|
}
|
|
59
61
|
export async function getRuntimeContext(capabilityState = undefined) {
|
|
60
|
-
if (runtimeContextCacheSatisfies(capabilityState)) {
|
|
62
|
+
if (runtimeContextCache && runtimeContextCacheSatisfies(capabilityState)) {
|
|
61
63
|
return runtimeContextCache;
|
|
62
64
|
}
|
|
63
65
|
return refreshRuntimeContext(capabilityState);
|
|
64
66
|
}
|
|
65
|
-
|
|
67
|
+
/**
|
|
68
|
+
* Freeflow's system section for the current configuration: sent only while Freeflow is enabled, with the routing cue
|
|
69
|
+
* only while Cognitive Routing is effective (Task 003 D-002). It changes only when a setting changes, which costs one
|
|
70
|
+
* prompt-cache write (D-001); between such changes it is identical from request to request.
|
|
71
|
+
*/
|
|
72
|
+
export function stableRuntimeContext(context, options = {}) {
|
|
73
|
+
// A prompt the user turned off (`prompts` in the config) is left out; a missing file is a different matter, which
|
|
74
|
+
// hasUsableMandatoryPrompts reports.
|
|
75
|
+
const on = (prompt) => options.prompts?.[prompt] !== false;
|
|
66
76
|
const sections = [
|
|
67
|
-
["Freeflow core", context?.corePrompt],
|
|
68
|
-
["Freeflow core", context?.interactionContractPrompt],
|
|
69
|
-
["Working Method", context?.workingMethodPrompt],
|
|
70
|
-
["Cognitive Routing", context?.cognitiveRoutingPrompt],
|
|
77
|
+
...(on("core") ? [["Freeflow core", context?.corePrompt]] : []),
|
|
78
|
+
...(on("interactionContract") ? [["Freeflow core", context?.interactionContractPrompt]] : []),
|
|
79
|
+
...(on("workingMethod") ? [["Working Method", context?.workingMethodPrompt]] : []),
|
|
80
|
+
...(options.routing ? [["Cognitive Routing", context?.cognitiveRoutingPrompt]] : []),
|
|
71
81
|
];
|
|
72
82
|
return [
|
|
73
|
-
"
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
83
|
+
"Text inside `<freeflow-notice>` tags comes from Freeflow, not the user: it reports facts and harness steps and is not the user's instruction, except the user's own earlier messages it quotes in `<user-message>` tags.",
|
|
84
|
+
...(options.routing
|
|
85
|
+
? [
|
|
86
|
+
"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.",
|
|
87
|
+
]
|
|
88
|
+
: []),
|
|
78
89
|
...sections
|
|
79
90
|
.filter(([, text]) => isPromptAvailable(text))
|
|
80
|
-
.map(([feature, text]) => `##
|
|
91
|
+
.map(([feature, text]) => `## ${feature}\n\n${text.trim()}`),
|
|
81
92
|
].join("\n\n");
|
|
82
93
|
}
|
|
@@ -253,7 +253,12 @@ export async function persistedBranchMatches(reader, leaf, branch) {
|
|
|
253
253
|
const matches =
|
|
254
254
|
snapshot.sessionId === reader.getSessionId() &&
|
|
255
255
|
JSON.stringify(activeReadOnlySessionBranch(snapshot, leaf)) === JSON.stringify(branch);
|
|
256
|
-
|
|
256
|
+
// The trust point covers every entry Pi holds, so it is set only when the file holds them all: a match on this
|
|
257
|
+
// branch says nothing of an entry on another branch that was never written.
|
|
258
|
+
const onDisk = new Set(snapshot.entries.slice(1).map((entry) => entry.id));
|
|
259
|
+
const inMemory = reader.getEntries();
|
|
260
|
+
const complete = inMemory.length === onDisk.size && inMemory.every((entry) => onDisk.has(entry.id));
|
|
261
|
+
if (matches && complete) trustLoadedSession(reader);
|
|
257
262
|
else verified.delete(path);
|
|
258
263
|
return matches;
|
|
259
264
|
}
|
|
@@ -62,7 +62,7 @@ export class RequestHistory {
|
|
|
62
62
|
branchCache;
|
|
63
63
|
frameHash = (entry) => {
|
|
64
64
|
const cached = this.validatedFrames.get(entry.id);
|
|
65
|
-
return cached
|
|
65
|
+
return cached !== undefined && cached.entry === entry ? cached.hash : hash(entry);
|
|
66
66
|
};
|
|
67
67
|
// Unedited native entries and Freeflow's renderings of them never change under their identity, so their
|
|
68
68
|
// fingerprints are computed once per session.
|
|
@@ -178,11 +178,15 @@ export class RequestHistory {
|
|
|
178
178
|
const byId = extendsCache ? (newEntries.length ? new Map(cachedBranch.byId) : cachedBranch.byId) : new Map();
|
|
179
179
|
for (const entry of newEntries) {
|
|
180
180
|
const cached = this.validatedFrames.get(entry.id);
|
|
181
|
-
const same = cached
|
|
181
|
+
const same = cached !== undefined && cached.entry === entry;
|
|
182
182
|
const frame = same ? cached.frame : parse(entry.data);
|
|
183
183
|
if (
|
|
184
184
|
(frame.basis !== entry.parentId &&
|
|
185
|
-
|
|
185
|
+
// A null basis is in neither set, so it counts as outside the parent session's branch.
|
|
186
|
+
!(
|
|
187
|
+
reader.getHeader?.()?.parentSession &&
|
|
188
|
+
(frame.basis === null || (!ids.has(frame.basis) && !newIds.has(frame.basis)))
|
|
189
|
+
)) ||
|
|
186
190
|
byId.has(frame.id)
|
|
187
191
|
)
|
|
188
192
|
throw new Error("Context frame anchor differs");
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
2
|
+
/**
|
|
3
|
+
* A transcript row the way Pi draws its own: a one-line head, then the body. A short body (a few lines) shows in both
|
|
4
|
+
* views; a long one shows only when the row is expanded (ctrl+o), with a count of what is hidden.
|
|
5
|
+
*/
|
|
6
|
+
export const SHORT_LINES = 4;
|
|
7
|
+
const SHORT_CHARS = 400;
|
|
8
|
+
export function paint(theme, color, text) {
|
|
9
|
+
return theme?.fg ? theme.fg(color, text) : text;
|
|
10
|
+
}
|
|
11
|
+
export function firstLine(text, max = 100) {
|
|
12
|
+
const line =
|
|
13
|
+
String(text ?? "")
|
|
14
|
+
.split("\n")
|
|
15
|
+
.find((each) => each.trim()) ?? "";
|
|
16
|
+
const clean = line.trim();
|
|
17
|
+
return clean.length > max ? `${clean.slice(0, max - 1)}…` : clean;
|
|
18
|
+
}
|
|
19
|
+
export function foldRow(head, body, expanded, theme) {
|
|
20
|
+
const lines = body.flatMap((each) => String(each).split("\n"));
|
|
21
|
+
const short = lines.length <= SHORT_LINES && lines.join("\n").length <= SHORT_CHARS;
|
|
22
|
+
if (!lines.length) return new Text(head, 0, 0);
|
|
23
|
+
if (expanded || short) return new Text(`${head}\n${lines.map((line) => ` ${line}`).join("\n")}`, 0, 0);
|
|
24
|
+
return new Text(`${head} ${paint(theme, "dim", `(${lines.length} lines, ctrl+o to expand)`)}`, 0, 0);
|
|
25
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { reviewStateLine, skillsStateLine } from "./config-validation.js";
|
|
1
2
|
import { hasUsableMandatoryPrompts } from "./prompts.js";
|
|
2
3
|
/**
|
|
3
4
|
* The Freeflow Runtime State message: what is active this request, placed so unchanged state keeps its cached
|
|
@@ -38,12 +39,14 @@ export function filterBootstrapMessage(message) {
|
|
|
38
39
|
}
|
|
39
40
|
function publicCognitiveRoutingControl(controlMode) {
|
|
40
41
|
if (controlMode === "automatic") return "automatic";
|
|
41
|
-
if (["manual-helper", "manual-executor", "manual-coordinator"].includes(controlMode)) return "manual";
|
|
42
|
+
if (["manual-helper", "manual-executor", "manual-coordinator"].includes(String(controlMode))) return "manual";
|
|
42
43
|
return "unavailable";
|
|
43
44
|
}
|
|
44
45
|
function publicCognitiveRoutingProfile(activeProfile, effective) {
|
|
45
46
|
if (effective !== true) return "unavailable";
|
|
46
|
-
return ["helper", "executor", "coordinator"].includes(activeProfile)
|
|
47
|
+
return typeof activeProfile === "string" && ["helper", "executor", "coordinator"].includes(activeProfile)
|
|
48
|
+
? activeProfile
|
|
49
|
+
: "unavailable";
|
|
47
50
|
}
|
|
48
51
|
function publicCapabilityStatus(capability) {
|
|
49
52
|
if (capability?.effective === true) return "active";
|
|
@@ -93,6 +96,9 @@ export function freeflowRuntimeStateMessage(
|
|
|
93
96
|
: capabilityState?.configExists
|
|
94
97
|
? "config error"
|
|
95
98
|
: "setup needed";
|
|
99
|
+
// Only a choice the user made: the default adds nothing, so most users' state stays as it was.
|
|
100
|
+
const review = freeflowStatus === "active" ? reviewStateLine(capabilityState?.review) : null;
|
|
101
|
+
const skillsOff = freeflowStatus === "active" ? skillsStateLine(capabilityState?.skills?.off) : null;
|
|
96
102
|
return {
|
|
97
103
|
role: "custom",
|
|
98
104
|
customType: FREEFLOW_RUNTIME_STATE_MESSAGE_TYPE,
|
|
@@ -102,10 +108,15 @@ export function freeflowRuntimeStateMessage(
|
|
|
102
108
|
"This is extension-generated runtime state. Use it to interpret the stable Freeflow guidance.",
|
|
103
109
|
"",
|
|
104
110
|
`Freeflow: ${freeflowStatus}`,
|
|
111
|
+
...(review ? [review] : []),
|
|
112
|
+
...(skillsOff ? [skillsOff] : []),
|
|
105
113
|
"",
|
|
106
114
|
"Capabilities:",
|
|
107
115
|
`- Cognitive Routing: ${publicCognitiveRoutingStatus(capabilityState?.cognitiveRouting, cognitiveRoutingRuntime)}`,
|
|
108
116
|
`- Tool Execution: ${publicCapabilityStatus(capabilityState?.toolExecution)}`,
|
|
117
|
+
...(capabilityState?.messaging?.enabled
|
|
118
|
+
? [`- Messaging: ${publicCapabilityStatus(capabilityState.messaging)}`]
|
|
119
|
+
: []),
|
|
109
120
|
"",
|
|
110
121
|
"Cognitive Routing:",
|
|
111
122
|
`- Control: \`${control}\``,
|
|
@@ -113,6 +124,8 @@ export function freeflowRuntimeStateMessage(
|
|
|
113
124
|
...(control === "automatic" ? [] : [`- Profile: \`${profile}\``]),
|
|
114
125
|
`- Delegation: \`${cognitiveRoutingRuntime?.delegation ?? capabilityState?.cognitiveRouting?.delegation ?? "executor"}\``,
|
|
115
126
|
`- Projection: \`${projectionMode}\``,
|
|
127
|
+
...(options.skills?.length ? ["", `Freeflow skills read in this context: ${options.skills.join(", ")}`] : []),
|
|
128
|
+
...(options.taskMemory?.length ? ["", ...options.taskMemory] : []),
|
|
116
129
|
].join("\n"),
|
|
117
130
|
display: false,
|
|
118
131
|
details: { source: "provider-request-runtime-state" },
|
|
@@ -146,6 +159,8 @@ export function withFreeflowRuntimeState(
|
|
|
146
159
|
const source = Array.isArray(messages) ? messages : [];
|
|
147
160
|
const runtimeState = freeflowRuntimeStateMessage(capabilityState, cognitiveRoutingRuntime, freeflowContext, {
|
|
148
161
|
projectionFailure: options.projectionFailure,
|
|
162
|
+
taskMemory: options.taskMemory,
|
|
163
|
+
skills: options.skills,
|
|
149
164
|
});
|
|
150
165
|
const runtimeStateMessages = source.filter(
|
|
151
166
|
(message) =>
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/** The value of each section after replaying the transcript's system messages. */
|
|
2
|
+
export function currentSections(messages) {
|
|
3
|
+
const current = {};
|
|
4
|
+
for (const message of messages)
|
|
5
|
+
if (message?.role === "system" && message.sections && typeof message.sections === "object")
|
|
6
|
+
for (const [name, value] of Object.entries(message.sections)) current[name] = value;
|
|
7
|
+
return current;
|
|
8
|
+
}
|
|
9
|
+
// One filtered copy per patch and section set, so a request reuses the object the previous request carried.
|
|
10
|
+
const copies = new WeakMap();
|
|
11
|
+
/** The sections of the first system message, the head of the prompt. */
|
|
12
|
+
export function headSections(messages) {
|
|
13
|
+
const head = messages.find((message) => message?.role === "system" && message.sections);
|
|
14
|
+
return head ? { ...head.sections } : undefined;
|
|
15
|
+
}
|
|
16
|
+
function sameSections(a, b) {
|
|
17
|
+
const names = Object.keys(a);
|
|
18
|
+
return names.length === Object.keys(b).length && names.every((name) => a[name] === b[name]);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* `version` changes whenever `own` does; it keys the reused copies without serialising the sections per request.
|
|
22
|
+
* `head` is the head's sections when `own` was recorded: a head still equal to it is kept as it is, since later
|
|
23
|
+
* patches carry any change; one that differs was rebuilt, and gets the recorded values.
|
|
24
|
+
*/
|
|
25
|
+
export function keepSections(messages, own, version = 0, head) {
|
|
26
|
+
if (!own || !Object.keys(own).length) return [...messages];
|
|
27
|
+
const key = String(version);
|
|
28
|
+
const current = {};
|
|
29
|
+
const out = [];
|
|
30
|
+
let first = true;
|
|
31
|
+
for (const message of messages) {
|
|
32
|
+
const sections = message?.role === "system" ? message.sections : undefined;
|
|
33
|
+
if (!sections || typeof sections !== "object") {
|
|
34
|
+
out.push(message);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
// A head Pi rebuilt from its base options (after a compaction in a run it started) gets the recorded sections; the
|
|
38
|
+
// head the recording saw stays as it is, so the prompt's start never changes.
|
|
39
|
+
if (first) {
|
|
40
|
+
first = false;
|
|
41
|
+
const rebuilt = !head || !sameSections(sections, head);
|
|
42
|
+
const missing = rebuilt ? Object.entries(own).filter(([name, value]) => sections[name] !== value) : [];
|
|
43
|
+
for (const [name, value] of Object.entries(sections)) current[name] = value;
|
|
44
|
+
if (!missing.length) {
|
|
45
|
+
out.push(message);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
for (const [name, value] of missing) current[name] = value;
|
|
49
|
+
out.push(reused(message, key, { ...sections, ...Object.fromEntries(missing) }));
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
let kept;
|
|
53
|
+
for (const [name, value] of Object.entries(sections)) {
|
|
54
|
+
const wanted = own[name];
|
|
55
|
+
const reverts = wanted !== undefined && current[name] === wanted && value !== wanted;
|
|
56
|
+
const restates = value === current[name];
|
|
57
|
+
if (wanted !== undefined && (reverts || restates)) {
|
|
58
|
+
const trimmed = (kept ??= { ...sections });
|
|
59
|
+
delete trimmed[name];
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
current[name] = value;
|
|
63
|
+
}
|
|
64
|
+
if (!kept) {
|
|
65
|
+
out.push(message);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const empty =
|
|
69
|
+
!Object.keys(kept).length &&
|
|
70
|
+
!message.content?.length &&
|
|
71
|
+
!message.toolsAdded?.length &&
|
|
72
|
+
!message.toolsRemoved?.length;
|
|
73
|
+
if (empty) continue;
|
|
74
|
+
out.push(reused(message, key, kept));
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
/** The same copy for the same message and section set, so later requests carry the identical object. */
|
|
79
|
+
function reused(message, key, sections) {
|
|
80
|
+
let byOwn = copies.get(message);
|
|
81
|
+
if (!byOwn) copies.set(message, (byOwn = new Map()));
|
|
82
|
+
let copy = byOwn.get(key);
|
|
83
|
+
if (!copy) byOwn.set(key, (copy = { ...message, sections }));
|
|
84
|
+
return copy;
|
|
85
|
+
}
|