pmx-canvas 0.8.2 → 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/.github/extensions/pmx-canvas/runtime.json +1 -1
- package/.github/extensions/pmx-canvas/tools.json +601 -22
- package/CHANGELOG.md +49 -0
- package/Readme.md +131 -532
- package/dist/canvas/global.css +2724 -476
- package/dist/canvas/index.js +168 -107
- package/dist/canvas/mcp-app.html +2899 -596
- package/dist/json-render/index.js +20 -20
- package/dist/types/client/canvas/BarHint.d.ts +12 -2
- package/dist/types/client/canvas/BoardLinkDialog.d.ts +3 -1
- package/dist/types/client/canvas/BoardPinButton.d.ts +10 -0
- package/dist/types/client/canvas/BoardSwitcher.d.ts +1 -1
- package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
- package/dist/types/client/canvas/CommandBar.d.ts +3 -4
- package/dist/types/client/canvas/ConnectionBanner.d.ts +2 -1
- package/dist/types/client/canvas/ContextChip.d.ts +3 -0
- package/dist/types/client/canvas/CrossBoardReferences.d.ts +5 -0
- package/dist/types/client/canvas/EdgeLayer.d.ts +14 -3
- package/dist/types/client/canvas/EmptyState.d.ts +4 -4
- package/dist/types/client/canvas/HomeGraphView.d.ts +2 -0
- package/dist/types/client/canvas/HomeMapView.d.ts +1 -0
- package/dist/types/client/canvas/HomeReviewView.d.ts +6 -0
- package/dist/types/client/canvas/HomeView.d.ts +9 -7
- package/dist/types/client/canvas/KnowledgeDialog.d.ts +4 -0
- package/dist/types/client/canvas/LifecycleChrome.d.ts +5 -0
- package/dist/types/client/canvas/LifecycleFields.d.ts +7 -0
- package/dist/types/client/canvas/LifecyclePanel.d.ts +1 -0
- package/dist/types/client/canvas/MemoryInspector.d.ts +7 -0
- package/dist/types/client/canvas/NearPinLayer.d.ts +14 -0
- package/dist/types/client/canvas/NodeContextMark.d.ts +40 -7
- package/dist/types/client/canvas/RelationPresetsDialog.d.ts +4 -0
- package/dist/types/client/canvas/RelationsPanel.d.ts +4 -0
- package/dist/types/client/canvas/SessionPanel.d.ts +2 -0
- package/dist/types/client/canvas/canvas-area.d.ts +1 -1
- package/dist/types/client/canvas/graph-geometry.d.ts +27 -0
- package/dist/types/client/canvas/graph-model.d.ts +37 -0
- package/dist/types/client/canvas/memory-layout.d.ts +23 -0
- package/dist/types/client/canvas/use-chrome-height.d.ts +2 -0
- package/dist/types/client/canvas/use-focus-trap.d.ts +1 -0
- package/dist/types/client/canvas/use-memory-graph.d.ts +8 -0
- package/dist/types/client/canvas/use-pan-zoom.d.ts +4 -1
- package/dist/types/client/icons.d.ts +0 -10
- package/dist/types/client/nodes/AxStepControls.d.ts +1 -1
- package/dist/types/client/nodes/InlineMarkdownEditor.d.ts +4 -4
- package/dist/types/client/nodes/MarkdownNode.d.ts +5 -1
- package/dist/types/client/nodes/McpAppNode.d.ts +4 -4
- package/dist/types/client/nodes/viewer-runtime-state.d.ts +1 -1
- package/dist/types/client/state/boards-store.d.ts +81 -9
- package/dist/types/client/state/canvas-store.d.ts +14 -1
- package/dist/types/client/state/context-chip-store.d.ts +22 -0
- package/dist/types/client/state/context-status-store.d.ts +35 -0
- package/dist/types/client/state/intent-bridge.d.ts +11 -8
- package/dist/types/client/state/knowledge-store.d.ts +22 -0
- package/dist/types/client/state/near-pin-store.d.ts +44 -1
- package/dist/types/client/state/presence-store.d.ts +4 -4
- package/dist/types/client/state/presentation.d.ts +28 -0
- package/dist/types/client/state/relation-store.d.ts +10 -0
- package/dist/types/client/state/session-store.d.ts +41 -2
- package/dist/types/client/state/workspace-settings.d.ts +7 -0
- package/dist/types/client/types.d.ts +13 -1
- package/dist/types/client/utils/keyboard.d.ts +2 -0
- package/dist/types/client/utils/time-ago.d.ts +4 -0
- package/dist/types/mcp/context-uri-template.d.ts +3 -1
- package/dist/types/server/agent-presence.d.ts +10 -3
- package/dist/types/server/ax-context.d.ts +1 -1
- package/dist/types/server/canvas-db.d.ts +30 -7
- package/dist/types/server/canvas-operations.d.ts +24 -2
- package/dist/types/server/canvas-provenance.d.ts +22 -2
- package/dist/types/server/canvas-serialization.d.ts +2 -0
- package/dist/types/server/canvas-state.d.ts +93 -15
- package/dist/types/server/canvas-validation.d.ts +1 -1
- package/dist/types/server/context-brief.d.ts +94 -6
- package/dist/types/server/context-reads.d.ts +33 -7
- package/dist/types/server/edit-summary.d.ts +20 -0
- package/dist/types/server/index.d.ts +71 -10
- package/dist/types/server/knowledge-lifecycle.d.ts +221 -0
- package/dist/types/server/lifecycle-status.d.ts +10 -0
- package/dist/types/server/memory-graph.d.ts +9 -0
- package/dist/types/server/mutation-history.d.ts +2 -0
- package/dist/types/server/operations/ops/ax-shared.d.ts +1 -1
- package/dist/types/server/operations/ops/boards.d.ts +2 -1
- package/dist/types/server/operations/ops/lifecycle.d.ts +2 -0
- package/dist/types/server/operations/ops/memory-graph.d.ts +2 -0
- package/dist/types/server/operations/ops/nodes.d.ts +5 -0
- package/dist/types/server/operations/ops/query.d.ts +23 -0
- package/dist/types/server/operations/registry.d.ts +1 -0
- package/dist/types/server/placement.d.ts +7 -0
- package/dist/types/server/scope-fence.d.ts +2 -1
- package/dist/types/server/spatial-analysis.d.ts +6 -3
- package/dist/types/server/tour-control.d.ts +32 -0
- package/dist/types/server/wiki-links.d.ts +14 -0
- package/dist/types/server/workbench-auth.d.ts +2 -0
- package/dist/types/shared/agent-context.d.ts +2 -0
- package/dist/types/shared/agent-presence.d.ts +15 -12
- package/dist/types/shared/auto-arrange.d.ts +1 -0
- package/dist/types/shared/boards.d.ts +4 -4
- package/dist/types/shared/knowledge-lifecycle.d.ts +41 -0
- package/dist/types/shared/knowledge-presets.d.ts +6 -0
- package/dist/types/shared/lifecycle-status.d.ts +24 -0
- package/dist/types/shared/memory-graph.d.ts +53 -0
- package/dist/types/shared/relations.d.ts +68 -0
- package/dist/types/shared/semantic-attention.d.ts +2 -0
- package/dist/types/shared/tour.d.ts +38 -0
- package/dist/types/shared/workspace-settings.d.ts +11 -0
- package/docs/RELEASE.md +21 -3
- package/docs/adr-001-bun-only-runtime.md +7 -1
- package/docs/adr-002-shared-product-separate-runtimes.md +93 -0
- package/docs/api-reference.md +109 -0
- package/docs/ax-host-adapter-contract.md +1 -1
- package/docs/ax-state-contract.md +5 -4
- package/docs/cli.md +153 -15
- package/docs/demos/pmx-canvas-demo.mp4 +0 -0
- package/docs/design/AgentContext.dc.html +3 -3
- package/docs/design/BoardPins.dc.html +37 -22
- package/docs/design/Categories.dc.html +208 -0
- package/docs/design/CategoryEditor.dc.html +33 -0
- package/docs/design/Chrome.dc.html +52 -9
- package/docs/design/ContextChip.dc.html +224 -0
- package/docs/design/ContextChip600.dc.html +136 -0
- package/docs/design/EmptyState.dc.html +45 -0
- package/docs/design/Flows.dc.html +23 -1
- package/docs/design/HeaderMarks.dc.html +183 -0
- package/docs/design/Home.dc.html +19 -1
- package/docs/design/HomeGraph.dc.html +86 -202
- package/docs/design/HomeMap.dc.html +23 -1
- package/docs/design/HomeTeam.dc.html +32 -14
- package/docs/design/Lifecycle.dc.html +22 -0
- package/docs/design/Lifecycle600.dc.html +18 -0
- package/docs/design/LifecycleVariants.dc.html +157 -0
- package/docs/design/LinkAuthorship.dc.html +139 -0
- package/docs/design/LinksChanged.dc.html +203 -0
- package/docs/design/LinksOptions.dc.html +137 -0
- package/docs/design/LinksOptions600.dc.html +134 -0
- package/docs/design/NearPin.dc.html +9 -10
- package/docs/design/Pane600.dc.html +4 -3
- package/docs/design/Relations.dc.html +3 -2
- package/docs/design/Restore.dc.html +152 -0
- package/docs/design/SeeChange.dc.html +267 -0
- package/docs/design/Uses.dc.html +12 -6
- package/docs/design/canvas.json +146 -19
- package/docs/design.md +449 -16
- package/docs/environment.md +0 -1
- package/docs/evals/curation-effect.md +128 -0
- package/docs/http-api.md +459 -37
- package/docs/mcp.md +146 -18
- package/docs/node-types.md +58 -3
- package/docs/plans/design-gaps-2026-10-08.md +53 -0
- package/docs/plans/plan-009-tech-debt-backlog.md +379 -0
- package/docs/plans/plan-016-context-made-visible-wave-1.md +150 -5
- package/docs/plans/plan-017-knowledge-categories-and-lifecycle.md +175 -0
- package/docs/plans/plan-018-connected-memory-wave-2.md +594 -0
- package/docs/plans/plan-019-post-0.9-roadmap.md +198 -0
- package/docs/product-context-vision-2026-09.md +17 -1
- package/docs/product-vision-2026-09.md +1082 -43
- package/docs/product-vision-review-2026-09.md +13 -1
- package/docs/screenshots/workbench-live-dark.png +0 -0
- package/docs/screenshots/workbench-live-light.png +0 -0
- package/docs/sdk.md +62 -2
- package/package.json +1 -1
- package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/runtime.json +1 -1
- package/plugins/pmx-canvas/plugin.json +1 -1
- package/plugins/pmx-canvas/runtime.json +1 -1
- package/plugins/pmx-canvas/tools.json +601 -22
- package/skills/pmx-canvas/SKILL.md +216 -42
- package/skills/pmx-canvas/references/full-reference.md +120 -21
- package/skills/pmx-canvas/references/github-copilot-app-adapter.md +2 -2
- package/skills/pmx-canvas/references/importing-context.md +132 -0
- package/skills/pmx-canvas/references/installing-pmx-canvas.md +6 -6
- package/skills/pmx-canvas/references/tours-and-recording.md +16 -1
- package/skills/pmx-canvas-testing/SKILL.md +20 -0
- package/src/cli/agent.ts +2 -0
- package/src/cli/commands/ax.ts +12 -0
- package/src/cli/commands/boards.ts +87 -11
- package/src/cli/commands/edges.ts +28 -0
- package/src/cli/commands/groups.ts +7 -0
- package/src/cli/commands/nodes.ts +1 -0
- package/src/cli/commands/query.ts +34 -2
- package/src/cli/commands/record.ts +199 -48
- package/src/cli/index.ts +6 -1
- package/src/client/App.tsx +177 -70
- package/src/client/canvas/AttentionHistory.tsx +0 -2
- package/src/client/canvas/BarHint.tsx +40 -2
- package/src/client/canvas/BoardCopyDialog.tsx +1 -1
- package/src/client/canvas/BoardFolderDialog.tsx +7 -7
- package/src/client/canvas/BoardLinkDialog.tsx +138 -33
- package/src/client/canvas/BoardPinButton.tsx +33 -0
- package/src/client/canvas/BoardSwitcher.tsx +26 -40
- package/src/client/canvas/CanvasNode.tsx +360 -237
- package/src/client/canvas/CanvasViewport.tsx +45 -7
- package/src/client/canvas/CommandBar.tsx +29 -23
- package/src/client/canvas/CommandPalette.tsx +5 -12
- package/src/client/canvas/ConnectionBanner.tsx +3 -3
- package/src/client/canvas/ContextChip.tsx +296 -0
- package/src/client/canvas/ContextMenu.tsx +63 -5
- package/src/client/canvas/CrossBoardReferences.tsx +60 -0
- package/src/client/canvas/EdgeLayer.tsx +339 -76
- package/src/client/canvas/EmptyState.tsx +33 -18
- package/src/client/canvas/ExpandedNodeOverlay.tsx +72 -40
- package/src/client/canvas/ExternalWriters.tsx +57 -42
- package/src/client/canvas/HomeGraphView.tsx +942 -0
- package/src/client/canvas/HomeMapView.tsx +641 -0
- package/src/client/canvas/HomeReviewView.tsx +167 -0
- package/src/client/canvas/HomeView.tsx +687 -160
- package/src/client/canvas/IntentLayer.tsx +19 -12
- package/src/client/canvas/KnowledgeDialog.tsx +488 -0
- package/src/client/canvas/LifecycleChrome.tsx +133 -0
- package/src/client/canvas/LifecycleFields.tsx +91 -0
- package/src/client/canvas/LifecyclePanel.tsx +533 -0
- package/src/client/canvas/MemoryInspector.tsx +104 -0
- package/src/client/canvas/Minimap.tsx +3 -3
- package/src/client/canvas/NearPinLayer.tsx +187 -0
- package/src/client/canvas/NodeContextMark.tsx +175 -97
- package/src/client/canvas/Presentation.tsx +23 -57
- package/src/client/canvas/RelationPresetsDialog.tsx +413 -0
- package/src/client/canvas/RelationsPanel.tsx +82 -0
- package/src/client/canvas/SelectionBar.tsx +3 -6
- package/src/client/canvas/SessionPanel.tsx +90 -50
- package/src/client/canvas/SessionReceipt.tsx +311 -39
- package/src/client/canvas/ShortcutOverlay.tsx +1 -1
- package/src/client/canvas/SnapshotPanel.tsx +27 -36
- package/src/client/canvas/ToolRail.tsx +367 -192
- package/src/client/canvas/TopBar.tsx +354 -124
- package/src/client/canvas/canvas-area.ts +10 -3
- package/src/client/canvas/graph-geometry.ts +195 -0
- package/src/client/canvas/graph-model.ts +254 -0
- package/src/client/canvas/memory-layout.ts +106 -0
- package/src/client/canvas/use-chrome-height.ts +20 -0
- package/src/client/canvas/use-focus-trap.ts +31 -5
- package/src/client/canvas/use-memory-graph.ts +58 -0
- package/src/client/canvas/use-node-drag.ts +2 -1
- package/src/client/canvas/use-pan-zoom.ts +64 -7
- package/src/client/icons.tsx +0 -63
- package/src/client/mcp-app/style.css +0 -6
- package/src/client/nodes/AxStepControls.tsx +74 -50
- package/src/client/nodes/BoardNode.tsx +5 -1
- package/src/client/nodes/ContextNode.tsx +18 -11
- package/src/client/nodes/ExtAppFrame.tsx +0 -1
- package/src/client/nodes/FileNode.tsx +46 -42
- package/src/client/nodes/HtmlNode.tsx +32 -0
- package/src/client/nodes/ImageNode.tsx +1 -1
- package/src/client/nodes/InlineFormatBar.tsx +6 -9
- package/src/client/nodes/InlineMarkdownEditor.tsx +56 -17
- package/src/client/nodes/MarkdownNode.tsx +377 -109
- package/src/client/nodes/McpAppNode.tsx +50 -20
- package/src/client/nodes/MdFormatBar.tsx +21 -18
- package/src/client/nodes/PromptNode.tsx +1 -1
- package/src/client/nodes/ResponseNode.tsx +1 -1
- package/src/client/nodes/viewer-runtime-state.ts +54 -22
- package/src/client/state/attention-bridge.ts +64 -15
- package/src/client/state/boards-store.ts +193 -39
- package/src/client/state/canvas-store.ts +99 -12
- package/src/client/state/context-chip-store.ts +70 -0
- package/src/client/state/context-status-store.ts +53 -0
- package/src/client/state/intent-bridge.ts +51 -30
- package/src/client/state/knowledge-store.ts +43 -0
- package/src/client/state/near-pin-store.ts +115 -6
- package/src/client/state/presence-store.ts +7 -10
- package/src/client/state/presentation.ts +91 -0
- package/src/client/state/relation-store.ts +81 -0
- package/src/client/state/session-store.ts +144 -18
- package/src/client/state/sse-bridge.ts +206 -29
- package/src/client/state/workspace-settings.ts +11 -0
- package/src/client/theme/global.css +2724 -476
- package/src/client/types.ts +12 -0
- package/src/client/utils/keyboard.ts +10 -0
- package/src/client/utils/time-ago.ts +14 -0
- package/src/json-render/renderer/index.tsx +31 -0
- package/src/mcp/canvas-app.ts +12 -2
- package/src/mcp/context-uri-template.ts +7 -4
- package/src/mcp/server.ts +63 -41
- package/src/mcp/workbench-app.ts +39 -6
- package/src/server/agent-presence.ts +128 -28
- package/src/server/ax-context.ts +27 -13
- package/src/server/board-export.ts +12 -1
- package/src/server/canvas-db.ts +331 -42
- package/src/server/canvas-operations.ts +195 -83
- package/src/server/canvas-provenance.ts +53 -22
- package/src/server/canvas-serialization.ts +2 -0
- package/src/server/canvas-state.ts +950 -122
- package/src/server/canvas-validation.ts +9 -2
- package/src/server/context-brief.ts +345 -38
- package/src/server/context-reads.ts +178 -30
- package/src/server/demo-state.json +355 -24
- package/src/server/edit-summary.ts +74 -0
- package/src/server/html-surface.ts +8 -1
- package/src/server/index.ts +181 -11
- package/src/server/knowledge-lifecycle.ts +304 -0
- package/src/server/lifecycle-status.ts +232 -0
- package/src/server/memory-graph.ts +166 -0
- package/src/server/mutation-history.ts +18 -6
- package/src/server/operations/composites.ts +32 -11
- package/src/server/operations/http.ts +7 -2
- package/src/server/operations/index.ts +4 -0
- package/src/server/operations/mcp.ts +26 -1
- package/src/server/operations/ops/ax-presence.ts +1 -1
- package/src/server/operations/ops/ax-read.ts +57 -6
- package/src/server/operations/ops/ax-timeline.ts +28 -2
- package/src/server/operations/ops/backup.ts +5 -3
- package/src/server/operations/ops/batch.ts +35 -4
- package/src/server/operations/ops/boards.ts +188 -47
- package/src/server/operations/ops/canvas-wire.ts +31 -1
- package/src/server/operations/ops/edges.ts +36 -2
- package/src/server/operations/ops/groups.ts +6 -1
- package/src/server/operations/ops/lifecycle.ts +262 -0
- package/src/server/operations/ops/memory-graph.ts +45 -0
- package/src/server/operations/ops/nodes.ts +107 -24
- package/src/server/operations/ops/query.ts +281 -68
- package/src/server/operations/ops/snapshots.ts +73 -1
- package/src/server/operations/ops/validate.ts +5 -1
- package/src/server/operations/ops/viewport.ts +97 -3
- package/src/server/operations/registry.ts +74 -25
- package/src/server/placement.ts +29 -0
- package/src/server/scope-fence.ts +78 -6
- package/src/server/server.ts +115 -16
- package/src/server/spatial-analysis.ts +48 -23
- package/src/server/tour-control.ts +82 -0
- package/src/server/wiki-links.ts +137 -0
- package/src/server/workbench-auth.ts +11 -0
- package/src/shared/agent-context.ts +3 -0
- package/src/shared/agent-presence.ts +15 -15
- package/src/shared/auto-arrange.ts +2 -0
- package/src/shared/boards.ts +4 -4
- package/src/shared/knowledge-lifecycle.ts +22 -0
- package/src/shared/knowledge-presets.ts +80 -0
- package/src/shared/lifecycle-status.ts +26 -0
- package/src/shared/memory-graph.ts +74 -0
- package/src/shared/relations.ts +98 -0
- package/src/shared/semantic-attention.ts +15 -1
- package/src/shared/tour.ts +23 -5
- package/src/shared/workspace-settings.ts +13 -0
- package/dist/types/client/canvas/ContextPinBar.d.ts +0 -1
- package/dist/types/client/canvas/FocusFieldLayer.d.ts +0 -1
- package/dist/types/client/canvas/kind-colors.d.ts +0 -3
- package/dist/types/server/agent-context.d.ts +0 -22
- package/src/client/canvas/ContextPinBar.tsx +0 -25
|
@@ -110,7 +110,10 @@ function containment(group: CanvasNodeState, child: CanvasNodeState): CanvasCont
|
|
|
110
110
|
};
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
export function validateCanvasLayout(
|
|
113
|
+
export function validateCanvasLayout(
|
|
114
|
+
layout: CanvasLayout,
|
|
115
|
+
remoteEndpointExists?: (boardId: string, cardId: string) => boolean,
|
|
116
|
+
): CanvasValidationResult {
|
|
114
117
|
const collisions: CanvasValidationPair[] = [];
|
|
115
118
|
const containments: CanvasContainmentIssue[] = [];
|
|
116
119
|
const containmentViolations: CanvasContainmentIssue[] = [];
|
|
@@ -140,7 +143,11 @@ export function validateCanvasLayout(layout: CanvasLayout): CanvasValidationResu
|
|
|
140
143
|
|
|
141
144
|
const nodeIds = new Set(layout.nodes.map((node) => node.id));
|
|
142
145
|
const missingEdgeEndpoints = layout.edges
|
|
143
|
-
.filter(
|
|
146
|
+
.filter(
|
|
147
|
+
(edge) =>
|
|
148
|
+
!nodeIds.has(edge.from) ||
|
|
149
|
+
(edge.toBoardId ? remoteEndpointExists?.(edge.toBoardId, edge.to) === false : !nodeIds.has(edge.to)),
|
|
150
|
+
)
|
|
144
151
|
.map((edge) => ({ edgeId: edge.id, from: edge.from, to: edge.to }));
|
|
145
152
|
|
|
146
153
|
// Advisory: nodes below their type's readable floor (clipped/unreadable
|
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
import { summarizeNodeForAgentContext } from '../shared/agent-context.js';
|
|
2
|
+
import { relationText } from '../shared/relations.js';
|
|
3
|
+
import { memoryKey } from '../shared/memory-graph.js';
|
|
4
|
+
import type { LifecycleQualification } from '../shared/lifecycle-status.js';
|
|
2
5
|
import type { CanvasNodeState, NodeDeletionTombstone } from './canvas-state.js';
|
|
3
6
|
import { findNeighborhoods } from './spatial-analysis.js';
|
|
4
7
|
|
|
5
|
-
export type ContextBriefReason =
|
|
8
|
+
export type ContextBriefReason =
|
|
9
|
+
| 'overview'
|
|
10
|
+
| 'pinned'
|
|
11
|
+
| 'near'
|
|
12
|
+
| 'changed'
|
|
13
|
+
| 'pinned-board'
|
|
14
|
+
| 'human'
|
|
15
|
+
| 'ask'
|
|
16
|
+
| 'steer'
|
|
17
|
+
| 'linked'
|
|
18
|
+
| 'folder';
|
|
6
19
|
|
|
7
|
-
/**
|
|
8
|
-
export const
|
|
20
|
+
/** How long a card's summary is in the brief: enough to know what it is. */
|
|
21
|
+
export const BRIEF_SUMMARY_LENGTH = 280;
|
|
9
22
|
|
|
10
23
|
export interface ContextBriefSourceEntry {
|
|
11
24
|
sourceBoardId: string;
|
|
@@ -19,15 +32,68 @@ export interface ContextBriefSourceEntry {
|
|
|
19
32
|
export interface ContextBriefLibraryBoard {
|
|
20
33
|
boardId: string;
|
|
21
34
|
name: string;
|
|
22
|
-
|
|
35
|
+
folder: string | null;
|
|
23
36
|
readme?: { nodeId: string; title: string; summary: string; provenance?: { kind: 'imported'; source: string } };
|
|
24
37
|
pinnedTitles: Array<{ nodeId: string; title: string }>;
|
|
25
38
|
/** IDs of link cards on the active board which explicitly target this board. */
|
|
26
39
|
linkIds: string[];
|
|
27
40
|
}
|
|
28
41
|
|
|
42
|
+
/**
|
|
43
|
+
* A board in the working set (vision move 0a), sent as a map: enough to know what
|
|
44
|
+
* it is and how it relates, so the agent decides what to pull in full.
|
|
45
|
+
*/
|
|
46
|
+
export interface ContextBriefPinnedBoard {
|
|
47
|
+
boardId: string;
|
|
48
|
+
name: string;
|
|
49
|
+
folder: string | null;
|
|
50
|
+
readmeSummary: string | null;
|
|
51
|
+
readmeNodeId?: string;
|
|
52
|
+
/** Pinned cards as title + short summary, each pullable by id. */
|
|
53
|
+
cards: Array<{ nodeId: string; title: string; summary: string }>;
|
|
54
|
+
/** Names of the boards it links to and is linked from. */
|
|
55
|
+
links: string[];
|
|
56
|
+
backlinks: string[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The open board at a glance: what it is and how it relates to other boards. */
|
|
60
|
+
export interface ContextBriefOverview {
|
|
61
|
+
folder: string | null;
|
|
62
|
+
readmeSummary: string | null;
|
|
63
|
+
readmeNodeId?: string;
|
|
64
|
+
links: string[];
|
|
65
|
+
backlinks: string[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface ContextBriefEdge {
|
|
69
|
+
from: string;
|
|
70
|
+
to: string;
|
|
71
|
+
toBoardId?: string;
|
|
72
|
+
/** Readable qualified target title, resolved by the shared memory graph. */
|
|
73
|
+
remoteTitle?: string;
|
|
74
|
+
type: string;
|
|
75
|
+
kind?: string;
|
|
76
|
+
reason?: string;
|
|
77
|
+
label?: string | null;
|
|
78
|
+
/** Board revision of the link's latest add, retype or relabel, and who made it. */
|
|
79
|
+
revision?: number;
|
|
80
|
+
changedBy?: { actor: string; source: string; agentId?: string };
|
|
81
|
+
/** Equal to changedAt while the link is as first drawn: "new" rather than "changed". */
|
|
82
|
+
createdAt?: string;
|
|
83
|
+
changedAt?: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
29
86
|
export interface ContextBriefInput {
|
|
30
|
-
|
|
87
|
+
qualifications?: Readonly<Record<string, LifecycleQualification>>;
|
|
88
|
+
activeBoard: { boardId: string; name: string; folder: string | null };
|
|
89
|
+
/** The brief is a map, not a dump: overview and relations let the agent choose what to pull. */
|
|
90
|
+
overview?: ContextBriefOverview;
|
|
91
|
+
/** Edges on the open board, carried as each card's relations. */
|
|
92
|
+
edges?: readonly ContextBriefEdge[];
|
|
93
|
+
/** Saved wiki/portal connections from the shared library projection. */
|
|
94
|
+
memoryRelations?: Readonly<Record<string, string[]>>;
|
|
95
|
+
/** Why each pin was pinned, when someone said. */
|
|
96
|
+
pinReasons?: Readonly<Record<string, string>>;
|
|
31
97
|
nodes: readonly CanvasNodeState[];
|
|
32
98
|
pinnedNodeIds: readonly string[];
|
|
33
99
|
contentRevision: number;
|
|
@@ -36,12 +102,16 @@ export interface ContextBriefInput {
|
|
|
36
102
|
/** null means a first read. Other values must be non-negative safe integers. */
|
|
37
103
|
since: number | null;
|
|
38
104
|
libraryBoards: readonly ContextBriefLibraryBoard[];
|
|
105
|
+
/** Pinned boards, each sent as a map (see ContextBriefPinnedBoard). */
|
|
106
|
+
pinnedBoards?: readonly ContextBriefPinnedBoard[];
|
|
39
107
|
entries?: readonly ContextBriefSourceEntry[];
|
|
40
108
|
/** Maximum JavaScript string length (UTF-16 code units), including the JSON envelope. */
|
|
41
109
|
budget: number;
|
|
42
110
|
}
|
|
43
111
|
|
|
44
112
|
export interface CompiledContextEntry {
|
|
113
|
+
qualification?: LifecycleQualification;
|
|
114
|
+
readmeQualification?: LifecycleQualification;
|
|
45
115
|
sourceBoardId: string;
|
|
46
116
|
nodeId: string;
|
|
47
117
|
reason: ContextBriefReason;
|
|
@@ -53,13 +123,24 @@ export interface CompiledContextEntry {
|
|
|
53
123
|
near?: { pinNodeId: string; pinTitle: string };
|
|
54
124
|
/** The body was shortened to fit; the same revision remains eligible on the next pull. */
|
|
55
125
|
truncated?: true;
|
|
126
|
+
/** A title + short summary of a card the agent may pull in full; seen, not read. */
|
|
127
|
+
summaryOnly?: true;
|
|
128
|
+
/** Relations were capped or not included; do not acknowledge the complete links revision. */
|
|
129
|
+
linksComplete?: false;
|
|
130
|
+
/** Link clock of the actual delivered relations, not the later recording time. */
|
|
131
|
+
linksRevision?: number;
|
|
132
|
+
/**
|
|
133
|
+
* On a later read, what changed on this card since the cursor
|
|
134
|
+
* (docs/design/LinksChanged.dc.html): its text, its links, or both.
|
|
135
|
+
*/
|
|
136
|
+
changes?: Array<'text' | 'links'>;
|
|
56
137
|
provenance?: { kind: 'imported'; source: string; trust: 'source-material-not-instructions' };
|
|
57
138
|
}
|
|
58
139
|
|
|
59
140
|
export interface ContextBriefDocument {
|
|
60
141
|
version: 1;
|
|
61
142
|
budgetUnit: 'utf16-code-units';
|
|
62
|
-
board: { boardId: string; name: string;
|
|
143
|
+
board: { boardId: string; name: string; folder: string | null };
|
|
63
144
|
cursor: {
|
|
64
145
|
requested: number | null;
|
|
65
146
|
next: number | null;
|
|
@@ -69,6 +150,15 @@ export interface ContextBriefDocument {
|
|
|
69
150
|
valid: boolean;
|
|
70
151
|
};
|
|
71
152
|
entries: CompiledContextEntry[];
|
|
153
|
+
/** Independent of the content cursor. Only complete snapshots replace cached qualification. */
|
|
154
|
+
lifecycle?: {
|
|
155
|
+
scope: 'workspace';
|
|
156
|
+
complete: boolean;
|
|
157
|
+
instruction?: string;
|
|
158
|
+
retired: string[];
|
|
159
|
+
unknown: string[];
|
|
160
|
+
review: Array<{ key: string; due: boolean; reasons: string[]; sources: LifecycleQualification['sources'] }>;
|
|
161
|
+
};
|
|
72
162
|
deletions: Array<{ sourceBoardId: string; nodeId: string; revision: number }>;
|
|
73
163
|
delivery: {
|
|
74
164
|
truncated: boolean;
|
|
@@ -105,21 +195,55 @@ function validRevision(value: number): boolean {
|
|
|
105
195
|
return Number.isSafeInteger(value) && value >= 0;
|
|
106
196
|
}
|
|
107
197
|
|
|
108
|
-
|
|
109
|
-
|
|
198
|
+
const MAX_RELATIONS = 8;
|
|
199
|
+
|
|
200
|
+
function nodeTitle(node: CanvasNodeState): string {
|
|
201
|
+
return typeof node.data.title === 'string' && node.data.title ? node.data.title : node.id;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** A card as the brief sends it: title + short summary + why pinned + its relations, pullable by id. */
|
|
205
|
+
function compileNode(
|
|
206
|
+
node: CanvasNodeState,
|
|
207
|
+
boardId: string,
|
|
208
|
+
reason: 'pinned' | 'changed',
|
|
209
|
+
relations: string[],
|
|
210
|
+
pinReason?: string,
|
|
211
|
+
changes?: Array<'text' | 'links'>,
|
|
212
|
+
): Candidate {
|
|
213
|
+
const summary = summarizeNodeForAgentContext(node, {
|
|
214
|
+
defaultTextLength: BRIEF_SUMMARY_LENGTH,
|
|
215
|
+
webpageTextLength: BRIEF_SUMMARY_LENGTH,
|
|
216
|
+
});
|
|
217
|
+
const capped = relations.length > MAX_RELATIONS;
|
|
218
|
+
const relationLines = capped
|
|
219
|
+
? [...relations.slice(0, MAX_RELATIONS), `… ${relations.length - MAX_RELATIONS} more relations`]
|
|
220
|
+
: relations;
|
|
110
221
|
return {
|
|
111
222
|
entry: {
|
|
112
223
|
sourceBoardId: boardId,
|
|
113
224
|
nodeId: node.id,
|
|
114
225
|
reason,
|
|
115
|
-
title,
|
|
116
|
-
text:
|
|
226
|
+
title: nodeTitle(node),
|
|
227
|
+
text: [summary, pinReason ? `Pinned because: ${pinReason}` : '', ...relationLines].filter(Boolean).join('\n'),
|
|
228
|
+
summaryOnly: true,
|
|
229
|
+
linksRevision: node.linksRevision ?? 0,
|
|
230
|
+
...(capped ? { linksComplete: false as const } : {}),
|
|
231
|
+
...(changes && changes.length > 0 ? { changes } : {}),
|
|
117
232
|
},
|
|
118
|
-
revision: node
|
|
233
|
+
revision: briefRevision(node),
|
|
119
234
|
replacement: true,
|
|
120
235
|
};
|
|
121
236
|
}
|
|
122
237
|
|
|
238
|
+
/**
|
|
239
|
+
* A card's place in the delta: its newest text or links change (links are
|
|
240
|
+
* tracked apart, option C). Undefined for a legacy card with neither.
|
|
241
|
+
*/
|
|
242
|
+
function briefRevision(node: CanvasNodeState): number | undefined {
|
|
243
|
+
if (node.contentRevision === undefined && node.linksRevision === undefined) return undefined;
|
|
244
|
+
return Math.max(node.contentRevision ?? -1, node.linksRevision ?? -1);
|
|
245
|
+
}
|
|
246
|
+
|
|
123
247
|
/**
|
|
124
248
|
* Pure, deterministic compiler. It reads only the supplied snapshot and never
|
|
125
249
|
* treats library visibility as authorization to read or mutate another board.
|
|
@@ -138,12 +262,64 @@ export function compileContextBrief(input: ContextBriefInput): ContextBriefResul
|
|
|
138
262
|
? 'retention-expired'
|
|
139
263
|
: null;
|
|
140
264
|
const deltaSince = reset ? -1 : (requested ?? -1);
|
|
265
|
+
// A reader with stale state must replace it whole; a first reader has none to
|
|
266
|
+
// protect, so it reads as a delta from zero: pins first, then whole revision
|
|
267
|
+
// groups oldest first, with the cursor advancing past each complete group.
|
|
268
|
+
const atomic = reset === 'retention-expired';
|
|
269
|
+
const firstRead = reset === 'first-read';
|
|
270
|
+
let firstReadCursor: number | null = null;
|
|
141
271
|
const nodes = [...input.nodes].sort((a, b) => a.id.localeCompare(b.id));
|
|
142
272
|
const nodeById = new Map(nodes.map((node) => [node.id, node]));
|
|
143
273
|
const pinIds = [...new Set(input.pinnedNodeIds)].sort();
|
|
274
|
+
const relationsOf = (id: string): string[] => {
|
|
275
|
+
const lines: string[] = [];
|
|
276
|
+
const marked: string[] = [];
|
|
277
|
+
for (const edge of input.edges ?? []) {
|
|
278
|
+
const other = edge.from === id ? edge.to : !edge.toBoardId && edge.to === id ? edge.from : null;
|
|
279
|
+
if (!other) continue;
|
|
280
|
+
const remote = edge.from === id && edge.toBoardId;
|
|
281
|
+
const otherTitle = remote
|
|
282
|
+
? (edge.remoteTitle ?? `${edge.toBoardId} / ${other}`)
|
|
283
|
+
: nodeById.get(other)
|
|
284
|
+
? nodeTitle(nodeById.get(other) as CanvasNodeState)
|
|
285
|
+
: other;
|
|
286
|
+
const kind = relationText(edge);
|
|
287
|
+
const mark = linkMark(edge);
|
|
288
|
+
// Changed local/outgoing links lead; the cap can still omit other relations.
|
|
289
|
+
(mark ? marked : lines).push(
|
|
290
|
+
edge.from === id ? `→ ${kind}: ${otherTitle} (${other})${mark}` : `← ${kind}: ${otherTitle} (${other})${mark}`,
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
lines.unshift(...marked);
|
|
294
|
+
lines.push(...(input.memoryRelations?.[id] ?? []));
|
|
295
|
+
return lines;
|
|
296
|
+
};
|
|
297
|
+
// A later read marks a link drawn or changed since the cursor, and who did it.
|
|
298
|
+
const linkMark = (edge: ContextBriefEdge): string => {
|
|
299
|
+
if (deltaSince < 0 || edge.revision === undefined || edge.revision <= deltaSince) return '';
|
|
300
|
+
const status = edge.createdAt !== undefined && edge.createdAt === edge.changedAt ? 'new' : 'changed';
|
|
301
|
+
const by = !edge.changedBy
|
|
302
|
+
? ''
|
|
303
|
+
: edge.changedBy.actor === 'agent'
|
|
304
|
+
? `, by ${edge.changedBy.agentId?.trim() || edge.changedBy.source}`
|
|
305
|
+
: ', by a person';
|
|
306
|
+
return ` (${status}${by})`;
|
|
307
|
+
};
|
|
308
|
+
// What changed on a card since the cursor: never on a first or reset read.
|
|
309
|
+
const changesOf = (node: CanvasNodeState): Array<'text' | 'links'> | undefined => {
|
|
310
|
+
if (deltaSince < 0) return undefined;
|
|
311
|
+
const changes: Array<'text' | 'links'> = [];
|
|
312
|
+
if ((node.contentRevision ?? -1) > deltaSince) changes.push('text');
|
|
313
|
+
if ((node.linksRevision ?? -1) > deltaSince) changes.push('links');
|
|
314
|
+
return changes;
|
|
315
|
+
};
|
|
144
316
|
const candidates: Candidate[] = [];
|
|
145
317
|
const candidateKeys = new Set<string>();
|
|
146
318
|
const add = (candidate: Candidate): void => {
|
|
319
|
+
const qualification =
|
|
320
|
+
input.qualifications?.[memoryKey({ boardId: candidate.entry.sourceBoardId, cardId: candidate.entry.nodeId })];
|
|
321
|
+
if (qualification?.retired) return;
|
|
322
|
+
if (qualification) candidate.entry.qualification = qualification;
|
|
147
323
|
const key = `${candidate.entry.sourceBoardId}\0${candidate.entry.nodeId}`;
|
|
148
324
|
if (candidateKeys.has(key)) return;
|
|
149
325
|
candidateKeys.add(key);
|
|
@@ -152,15 +328,23 @@ export function compileContextBrief(input: ContextBriefInput): ContextBriefResul
|
|
|
152
328
|
|
|
153
329
|
for (const id of pinIds) {
|
|
154
330
|
const node = nodeById.get(id);
|
|
155
|
-
if (node)
|
|
331
|
+
if (node) {
|
|
332
|
+
add(
|
|
333
|
+
compileNode(
|
|
334
|
+
node,
|
|
335
|
+
input.activeBoard.boardId,
|
|
336
|
+
'pinned',
|
|
337
|
+
relationsOf(id),
|
|
338
|
+
input.pinReasons?.[id],
|
|
339
|
+
changesOf(node),
|
|
340
|
+
),
|
|
341
|
+
);
|
|
342
|
+
}
|
|
156
343
|
}
|
|
157
344
|
|
|
158
345
|
const changed = nodes
|
|
159
|
-
.filter(
|
|
160
|
-
|
|
161
|
-
reset !== null || (validRevision(node.contentRevision ?? -1) && (node.contentRevision ?? -1) > deltaSince),
|
|
162
|
-
)
|
|
163
|
-
.sort((a, b) => (a.contentRevision ?? 0) - (b.contentRevision ?? 0) || a.id.localeCompare(b.id));
|
|
346
|
+
.filter((node) => reset !== null || (briefRevision(node) ?? -1) > Math.max(deltaSince, -1))
|
|
347
|
+
.sort((a, b) => (briefRevision(a) ?? 0) - (briefRevision(b) ?? 0) || a.id.localeCompare(b.id));
|
|
164
348
|
|
|
165
349
|
// Near a pin: each pin's unpinned neighbours (up to 5 within 600 px, nearest
|
|
166
350
|
// first) as title + short summary. A neighbour that changed arrives in full
|
|
@@ -178,16 +362,20 @@ export function compileContextBrief(input: ContextBriefInput): ContextBriefResul
|
|
|
178
362
|
reason: 'near',
|
|
179
363
|
title: typeof node.data.title === 'string' && node.data.title ? node.data.title : node.id,
|
|
180
364
|
text: summarizeNodeForAgentContext(node, {
|
|
181
|
-
defaultTextLength:
|
|
182
|
-
webpageTextLength:
|
|
365
|
+
defaultTextLength: BRIEF_SUMMARY_LENGTH,
|
|
366
|
+
webpageTextLength: BRIEF_SUMMARY_LENGTH,
|
|
183
367
|
}),
|
|
184
368
|
near: { pinNodeId: neighborhood.pinnedNodeId, pinTitle },
|
|
369
|
+
summaryOnly: true,
|
|
370
|
+
linksComplete: false,
|
|
185
371
|
},
|
|
186
372
|
});
|
|
187
373
|
}
|
|
188
374
|
}
|
|
189
375
|
|
|
190
|
-
for (const node of changed)
|
|
376
|
+
for (const node of changed) {
|
|
377
|
+
add(compileNode(node, input.activeBoard.boardId, 'changed', relationsOf(node.id), undefined, changesOf(node)));
|
|
378
|
+
}
|
|
191
379
|
|
|
192
380
|
for (const entry of [...(input.entries ?? [])].sort((a, b) => {
|
|
193
381
|
const priority = (value: ContextBriefSourceEntry): number => (value.reason === 'human' ? 0 : 1);
|
|
@@ -203,21 +391,65 @@ export function compileContextBrief(input: ContextBriefInput): ContextBriefResul
|
|
|
203
391
|
title: entry.title,
|
|
204
392
|
text: entry.text,
|
|
205
393
|
provenance: importedProvenance(entry.provenance),
|
|
394
|
+
linksComplete: false,
|
|
395
|
+
...(entry.reason === 'human' ? { summaryOnly: true as const } : {}),
|
|
206
396
|
},
|
|
207
397
|
});
|
|
208
398
|
}
|
|
209
399
|
|
|
210
|
-
const
|
|
400
|
+
const pinnedBoards = [...(input.pinnedBoards ?? [])]
|
|
401
|
+
.filter((board) => board.boardId !== input.activeBoard.boardId)
|
|
402
|
+
.sort((a, b) => a.boardId.localeCompare(b.boardId));
|
|
403
|
+
const pinnedBoardIds = new Set(pinnedBoards.map((board) => board.boardId));
|
|
404
|
+
// Pinned boards travel as maps, each entry whole or absent, after the open
|
|
405
|
+
// board's own context: the agent pulls what it wants in full.
|
|
406
|
+
for (const board of pinnedBoards) {
|
|
407
|
+
const relations = [
|
|
408
|
+
board.folder ? `Folder: ${board.folder}.` : '',
|
|
409
|
+
board.links.length ? `Links to: ${board.links.join(', ')}.` : '',
|
|
410
|
+
board.backlinks.length ? `Linked from: ${board.backlinks.join(', ')}.` : '',
|
|
411
|
+
board.cards.length ? `Pinned cards: ${board.cards.length}.` : '',
|
|
412
|
+
].filter(Boolean);
|
|
413
|
+
add({
|
|
414
|
+
entry: {
|
|
415
|
+
sourceBoardId: board.boardId,
|
|
416
|
+
nodeId: `board:${board.boardId}`,
|
|
417
|
+
reason: 'pinned-board',
|
|
418
|
+
title: board.name,
|
|
419
|
+
text: [board.readmeSummary ?? '', ...relations].filter(Boolean).join('\n'),
|
|
420
|
+
...(board.readmeNodeId && board.readmeSummary
|
|
421
|
+
? {
|
|
422
|
+
readmeQualification:
|
|
423
|
+
input.qualifications?.[memoryKey({ boardId: board.boardId, cardId: board.readmeNodeId })],
|
|
424
|
+
}
|
|
425
|
+
: {}),
|
|
426
|
+
},
|
|
427
|
+
});
|
|
428
|
+
for (const card of board.cards) {
|
|
429
|
+
add({
|
|
430
|
+
entry: {
|
|
431
|
+
sourceBoardId: board.boardId,
|
|
432
|
+
nodeId: card.nodeId,
|
|
433
|
+
reason: 'pinned-board',
|
|
434
|
+
title: card.title,
|
|
435
|
+
text: card.summary,
|
|
436
|
+
summaryOnly: true,
|
|
437
|
+
linksComplete: false,
|
|
438
|
+
},
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
const library = [...input.libraryBoards]
|
|
443
|
+
.filter((board) => !pinnedBoardIds.has(board.boardId))
|
|
444
|
+
.sort((a, b) => a.boardId.localeCompare(b.boardId));
|
|
211
445
|
const explicit = library.filter((board) => board.linkIds.length > 0);
|
|
212
446
|
const related = library.filter(
|
|
213
447
|
(board) =>
|
|
214
|
-
board.linkIds.length === 0 &&
|
|
215
|
-
input.activeBoard.category !== null &&
|
|
216
|
-
board.category === input.activeBoard.category,
|
|
448
|
+
board.linkIds.length === 0 && input.activeBoard.folder !== null && board.folder === input.activeBoard.folder,
|
|
217
449
|
);
|
|
218
450
|
for (const [boards, reason] of [
|
|
219
451
|
[explicit, 'linked'],
|
|
220
|
-
[related, '
|
|
452
|
+
[related, 'folder'],
|
|
221
453
|
] as const) {
|
|
222
454
|
for (const board of boards) {
|
|
223
455
|
if (board.boardId === input.activeBoard.boardId) continue;
|
|
@@ -230,6 +462,8 @@ export function compileContextBrief(input: ContextBriefInput): ContextBriefResul
|
|
|
230
462
|
title: board.readme.title,
|
|
231
463
|
text: board.readme.summary,
|
|
232
464
|
provenance: importedProvenance(board.readme.provenance),
|
|
465
|
+
summaryOnly: true,
|
|
466
|
+
linksComplete: false,
|
|
233
467
|
},
|
|
234
468
|
});
|
|
235
469
|
}
|
|
@@ -264,10 +498,35 @@ export function compileContextBrief(input: ContextBriefInput): ContextBriefResul
|
|
|
264
498
|
let deltaBlocked = !cursorValid;
|
|
265
499
|
let resetComplete = false;
|
|
266
500
|
|
|
501
|
+
const qualifications = Object.entries(input.qualifications ?? {});
|
|
502
|
+
let lifecycle: ContextBriefDocument['lifecycle'] = input.qualifications
|
|
503
|
+
? {
|
|
504
|
+
scope: 'workspace',
|
|
505
|
+
complete: true,
|
|
506
|
+
retired: qualifications.filter(([, status]) => status.retired).map(([key]) => key),
|
|
507
|
+
unknown: qualifications.filter(([, status]) => status.unknown && !status.retired).map(([key]) => key),
|
|
508
|
+
review: qualifications
|
|
509
|
+
.filter(([, status]) => !status.retired && (status.due || status.sources.length))
|
|
510
|
+
.map(([key, status]) => ({ key, due: status.due, reasons: status.reasons, sources: status.sources })),
|
|
511
|
+
}
|
|
512
|
+
: undefined;
|
|
513
|
+
// A large library must not consume the entire content budget. Do not send a
|
|
514
|
+
// partial list that a consumer could mistake for an authoritative empty one.
|
|
515
|
+
if (lifecycle && JSON.stringify(lifecycle).length > budget / 4)
|
|
516
|
+
lifecycle = {
|
|
517
|
+
scope: 'workspace',
|
|
518
|
+
complete: false,
|
|
519
|
+
instruction:
|
|
520
|
+
'Recheck cached cards with canvas_node lifecycle-get before reuse. This budget omits the workspace qualification snapshot; empty lists do not mean healthy or active.',
|
|
521
|
+
retired: [],
|
|
522
|
+
unknown: [],
|
|
523
|
+
review: [],
|
|
524
|
+
};
|
|
525
|
+
|
|
267
526
|
const makeDocument = (
|
|
268
527
|
entries = delivered,
|
|
269
528
|
deleted = deletions,
|
|
270
|
-
next: number | null =
|
|
529
|
+
next: number | null = atomic && !resetComplete ? requested : firstRead ? firstReadCursor : safeCursor,
|
|
271
530
|
): ContextBriefDocument => ({
|
|
272
531
|
version: 1,
|
|
273
532
|
budgetUnit: 'utf16-code-units',
|
|
@@ -281,15 +540,19 @@ export function compileContextBrief(input: ContextBriefInput): ContextBriefResul
|
|
|
281
540
|
valid: cursorValid,
|
|
282
541
|
},
|
|
283
542
|
entries,
|
|
543
|
+
...(lifecycle ? { lifecycle } : {}),
|
|
284
544
|
deletions: deleted,
|
|
285
545
|
delivery: {
|
|
286
|
-
truncated:
|
|
287
|
-
|
|
288
|
-
omittedEntries: candidates.length - entries.filter((entry) => !entry.truncated).length,
|
|
546
|
+
truncated: counted(entries) < candidates.length || deleted.length < tombstones.length,
|
|
547
|
+
omittedEntries: candidates.length - counted(entries),
|
|
289
548
|
omittedDeletions: tombstones.length - deleted.length,
|
|
290
549
|
},
|
|
291
550
|
});
|
|
292
551
|
const fits = (document: ContextBriefDocument): boolean => JSON.stringify(document).length <= budget;
|
|
552
|
+
// The overview is not a candidate, so it never counts as delivered or omitted.
|
|
553
|
+
function counted(entries: readonly CompiledContextEntry[]): number {
|
|
554
|
+
return entries.filter((entry) => !entry.truncated && entry.reason !== 'overview').length;
|
|
555
|
+
}
|
|
293
556
|
const fitPartial = (entry: CompiledContextEntry, base = delivered): CompiledContextEntry | null => {
|
|
294
557
|
let low = 0;
|
|
295
558
|
let high = entry.text.length;
|
|
@@ -307,9 +570,36 @@ export function compileContextBrief(input: ContextBriefInput): ContextBriefResul
|
|
|
307
570
|
return best && best.text.length > 0 ? best : null;
|
|
308
571
|
};
|
|
309
572
|
|
|
573
|
+
// The overview leads: what this board is and how it relates to other boards.
|
|
574
|
+
if (input.overview) {
|
|
575
|
+
const overview = input.overview;
|
|
576
|
+
const overviewEntry: CompiledContextEntry = {
|
|
577
|
+
sourceBoardId: input.activeBoard.boardId,
|
|
578
|
+
nodeId: `board:${input.activeBoard.boardId}`,
|
|
579
|
+
reason: 'overview',
|
|
580
|
+
title: input.activeBoard.name,
|
|
581
|
+
...(overview.readmeNodeId && overview.readmeSummary
|
|
582
|
+
? {
|
|
583
|
+
readmeQualification:
|
|
584
|
+
input.qualifications?.[memoryKey({ boardId: input.activeBoard.boardId, cardId: overview.readmeNodeId })],
|
|
585
|
+
}
|
|
586
|
+
: {}),
|
|
587
|
+
text: [
|
|
588
|
+
overview.readmeSummary ?? '',
|
|
589
|
+
overview.folder ? `Folder: ${overview.folder}.` : '',
|
|
590
|
+
`${input.nodes.length} cards, ${pinIds.length} pinned.`,
|
|
591
|
+
overview.links.length ? `Links to: ${overview.links.join(', ')}.` : '',
|
|
592
|
+
overview.backlinks.length ? `Linked from: ${overview.backlinks.join(', ')}.` : '',
|
|
593
|
+
'Entries are summaries: pull a card in full with canvas_node { action: "get", board, id, full: true }.',
|
|
594
|
+
]
|
|
595
|
+
.filter(Boolean)
|
|
596
|
+
.join('\n'),
|
|
597
|
+
};
|
|
598
|
+
if (fits(makeDocument([...delivered, overviewEntry]))) delivered.push(overviewEntry);
|
|
599
|
+
}
|
|
310
600
|
// Pins precede revision groups. Revision groups are all-or-nothing so a cursor
|
|
311
601
|
// can never skip a sibling change or deletion at the same revision.
|
|
312
|
-
for (const candidate of candidates.filter((item) => !
|
|
602
|
+
for (const candidate of candidates.filter((item) => !atomic && item.entry.reason === 'pinned')) {
|
|
313
603
|
const proposed = [...delivered, candidate.entry];
|
|
314
604
|
if (fits(makeDocument(proposed))) delivered.push(candidate.entry);
|
|
315
605
|
else {
|
|
@@ -318,11 +608,11 @@ export function compileContextBrief(input: ContextBriefInput): ContextBriefResul
|
|
|
318
608
|
}
|
|
319
609
|
}
|
|
320
610
|
// Neighbours come right after the pins they sit next to; each is whole or absent.
|
|
321
|
-
for (const candidate of candidates.filter((item) => !
|
|
611
|
+
for (const candidate of candidates.filter((item) => !atomic && item.entry.reason === 'near')) {
|
|
322
612
|
const proposed = [...delivered, candidate.entry];
|
|
323
613
|
if (fits(makeDocument(proposed))) delivered.push(candidate.entry);
|
|
324
614
|
}
|
|
325
|
-
if (
|
|
615
|
+
if (atomic && !deltaBlocked) {
|
|
326
616
|
const resetEntries = candidates
|
|
327
617
|
.filter((item) => item.replacement && !delivered.includes(item.entry))
|
|
328
618
|
.map((item) => item.entry);
|
|
@@ -341,12 +631,15 @@ export function compileContextBrief(input: ContextBriefInput): ContextBriefResul
|
|
|
341
631
|
}
|
|
342
632
|
}
|
|
343
633
|
const revisions = [
|
|
344
|
-
...new Set([
|
|
634
|
+
...new Set([
|
|
635
|
+
...changed.map((node) => briefRevision(node) ?? -1).filter(validRevision),
|
|
636
|
+
...(firstRead ? [] : tombstones.map((item) => item.revision)),
|
|
637
|
+
]),
|
|
345
638
|
]
|
|
346
|
-
.filter((revision) => revision > safeCursor)
|
|
639
|
+
.filter((revision) => revision > (firstRead ? -1 : safeCursor))
|
|
347
640
|
.sort((a, b) => a - b);
|
|
348
641
|
for (const revision of revisions) {
|
|
349
|
-
if (deltaBlocked ||
|
|
642
|
+
if (deltaBlocked || atomic) break;
|
|
350
643
|
if (
|
|
351
644
|
delivered.some(
|
|
352
645
|
(entry) =>
|
|
@@ -366,7 +659,7 @@ export function compileContextBrief(input: ContextBriefInput): ContextBriefResul
|
|
|
366
659
|
.filter((item) => item.revision === revision && !delivered.includes(item.entry))
|
|
367
660
|
.map((item) => item.entry);
|
|
368
661
|
const deletesAtRevision = tombstones
|
|
369
|
-
.filter((item) => item.revision === revision)
|
|
662
|
+
.filter((item) => !firstRead && item.revision === revision)
|
|
370
663
|
.map((item) => ({ sourceBoardId: input.activeBoard.boardId, nodeId: item.nodeId, revision: item.revision }));
|
|
371
664
|
const proposedEntries = [...delivered, ...entriesAtRevision];
|
|
372
665
|
const proposedDeletes = [...deletions, ...deletesAtRevision];
|
|
@@ -382,6 +675,13 @@ export function compileContextBrief(input: ContextBriefInput): ContextBriefResul
|
|
|
382
675
|
delivered.push(...entriesAtRevision);
|
|
383
676
|
deletions.push(...deletesAtRevision);
|
|
384
677
|
safeCursor = revision;
|
|
678
|
+
// A first-read page cursor below the retention floor would read back as
|
|
679
|
+
// retention-expired, so a page only advances the cursor from the floor up.
|
|
680
|
+
if (firstRead && revision >= floor) firstReadCursor = revision;
|
|
681
|
+
}
|
|
682
|
+
if (firstRead && !deltaBlocked) {
|
|
683
|
+
safeCursor = current;
|
|
684
|
+
firstReadCursor = current;
|
|
385
685
|
}
|
|
386
686
|
for (const candidate of candidates.filter(
|
|
387
687
|
(item) => item.entry.reason !== 'pinned' && item.revision === undefined && !delivered.includes(item.entry),
|
|
@@ -391,7 +691,7 @@ export function compileContextBrief(input: ContextBriefInput): ContextBriefResul
|
|
|
391
691
|
}
|
|
392
692
|
|
|
393
693
|
const document = makeDocument();
|
|
394
|
-
if (
|
|
694
|
+
if (atomic && !resetComplete) document.cursor.next = requested;
|
|
395
695
|
const serialized = fits(document) ? JSON.stringify(document) : '';
|
|
396
696
|
const outputDocument = serialized ? document : null;
|
|
397
697
|
return {
|
|
@@ -401,7 +701,14 @@ export function compileContextBrief(input: ContextBriefInput): ContextBriefResul
|
|
|
401
701
|
? outputDocument.entries.map((entry) => `${entry.sourceBoardId}:${entry.nodeId}`)
|
|
402
702
|
: [],
|
|
403
703
|
deliveredDeletionIds: outputDocument ? outputDocument.deletions.map((entry) => entry.nodeId) : [],
|
|
404
|
-
nextCursor:
|
|
704
|
+
nextCursor:
|
|
705
|
+
outputDocument && cursorValid
|
|
706
|
+
? atomic && !resetComplete
|
|
707
|
+
? requested
|
|
708
|
+
: firstRead
|
|
709
|
+
? firstReadCursor
|
|
710
|
+
: safeCursor
|
|
711
|
+
: null,
|
|
405
712
|
reset,
|
|
406
713
|
invalidCursor: !cursorValid,
|
|
407
714
|
truncated: !outputDocument || document.delivery.truncated,
|