nodebench-mcp 2.69.0 → 3.0.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/README.md +95 -39
- package/dist/agents/alertRouter.d.ts +38 -0
- package/dist/agents/alertRouter.js +151 -0
- package/dist/agents/alertRouter.js.map +1 -0
- package/dist/agents/entityMemory.d.ts +40 -0
- package/dist/agents/entityMemory.js +64 -0
- package/dist/agents/entityMemory.js.map +1 -0
- package/dist/agents/subAgents.d.ts +35 -0
- package/dist/agents/subAgents.js +62 -0
- package/dist/agents/subAgents.js.map +1 -0
- package/dist/benchmarks/benchmarkRunner.js +14 -0
- package/dist/benchmarks/benchmarkRunner.js.map +1 -1
- package/dist/benchmarks/chainEval.js +107 -0
- package/dist/benchmarks/chainEval.js.map +1 -1
- package/dist/benchmarks/llmJudgeEval.js +85 -0
- package/dist/benchmarks/llmJudgeEval.js.map +1 -1
- package/dist/benchmarks/searchQualityEval.js +118 -5
- package/dist/benchmarks/searchQualityEval.js.map +1 -1
- package/dist/cli/search.d.ts +13 -0
- package/dist/cli/search.js +130 -0
- package/dist/cli/search.js.map +1 -0
- package/dist/db.d.ts +6 -2
- package/dist/db.js +470 -3
- package/dist/db.js.map +1 -1
- package/dist/index.js +349 -64
- package/dist/index.js.map +1 -1
- package/dist/profiler/behaviorStore.d.ts +97 -0
- package/dist/profiler/behaviorStore.js +276 -0
- package/dist/profiler/behaviorStore.js.map +1 -0
- package/dist/profiler/eventCollector.d.ts +119 -0
- package/dist/profiler/eventCollector.js +267 -0
- package/dist/profiler/eventCollector.js.map +1 -0
- package/dist/profiler/index.d.ts +15 -0
- package/dist/profiler/index.js +16 -0
- package/dist/profiler/index.js.map +1 -0
- package/dist/profiler/mcpProxy.d.ts +49 -0
- package/dist/profiler/mcpProxy.js +123 -0
- package/dist/profiler/mcpProxy.js.map +1 -0
- package/dist/profiler/modelRouter.d.ts +30 -0
- package/dist/profiler/modelRouter.js +99 -0
- package/dist/profiler/modelRouter.js.map +1 -0
- package/dist/profiler/otelReceiver.d.ts +17 -0
- package/dist/profiler/otelReceiver.js +62 -0
- package/dist/profiler/otelReceiver.js.map +1 -0
- package/dist/profiler/proofEngine.d.ts +41 -0
- package/dist/profiler/proofEngine.js +93 -0
- package/dist/profiler/proofEngine.js.map +1 -0
- package/dist/profiler/workflowTemplates.d.ts +41 -0
- package/dist/profiler/workflowTemplates.js +95 -0
- package/dist/profiler/workflowTemplates.js.map +1 -0
- package/dist/providers/localMemoryProvider.js +3 -2
- package/dist/providers/localMemoryProvider.js.map +1 -1
- package/dist/runtimeConfig.d.ts +11 -0
- package/dist/runtimeConfig.js +27 -0
- package/dist/runtimeConfig.js.map +1 -0
- package/dist/security/auditLog.js +8 -3
- package/dist/security/auditLog.js.map +1 -1
- package/dist/subconscious/blocks.d.ts +43 -0
- package/dist/subconscious/blocks.js +158 -0
- package/dist/subconscious/blocks.js.map +1 -0
- package/dist/subconscious/classifier.d.ts +22 -0
- package/dist/subconscious/classifier.js +118 -0
- package/dist/subconscious/classifier.js.map +1 -0
- package/dist/subconscious/graphEngine.d.ts +65 -0
- package/dist/subconscious/graphEngine.js +234 -0
- package/dist/subconscious/graphEngine.js.map +1 -0
- package/dist/subconscious/index.d.ts +19 -0
- package/dist/subconscious/index.js +20 -0
- package/dist/subconscious/index.js.map +1 -0
- package/dist/subconscious/tools.d.ts +5 -0
- package/dist/subconscious/tools.js +255 -0
- package/dist/subconscious/tools.js.map +1 -0
- package/dist/subconscious/whisperPolicy.d.ts +20 -0
- package/dist/subconscious/whisperPolicy.js +171 -0
- package/dist/subconscious/whisperPolicy.js.map +1 -0
- package/dist/sweep/engine.d.ts +27 -0
- package/dist/sweep/engine.js +244 -0
- package/dist/sweep/engine.js.map +1 -0
- package/dist/sweep/index.d.ts +9 -0
- package/dist/sweep/index.js +8 -0
- package/dist/sweep/index.js.map +1 -0
- package/dist/sweep/sources/github_trending.d.ts +6 -0
- package/dist/sweep/sources/github_trending.js +37 -0
- package/dist/sweep/sources/github_trending.js.map +1 -0
- package/dist/sweep/sources/hackernews.d.ts +7 -0
- package/dist/sweep/sources/hackernews.js +57 -0
- package/dist/sweep/sources/hackernews.js.map +1 -0
- package/dist/sweep/sources/openbb_finance.d.ts +9 -0
- package/dist/sweep/sources/openbb_finance.js +46 -0
- package/dist/sweep/sources/openbb_finance.js.map +1 -0
- package/dist/sweep/sources/producthunt.d.ts +6 -0
- package/dist/sweep/sources/producthunt.js +41 -0
- package/dist/sweep/sources/producthunt.js.map +1 -0
- package/dist/sweep/sources/web_signals.d.ts +7 -0
- package/dist/sweep/sources/web_signals.js +63 -0
- package/dist/sweep/sources/web_signals.js.map +1 -0
- package/dist/sweep/sources/yahoo_finance.d.ts +6 -0
- package/dist/sweep/sources/yahoo_finance.js +47 -0
- package/dist/sweep/sources/yahoo_finance.js.map +1 -0
- package/dist/sweep/types.d.ts +50 -0
- package/dist/sweep/types.js +9 -0
- package/dist/sweep/types.js.map +1 -0
- package/dist/sync/founderEpisodeStore.d.ts +98 -0
- package/dist/sync/founderEpisodeStore.js +230 -0
- package/dist/sync/founderEpisodeStore.js.map +1 -0
- package/dist/sync/hyperloopArchive.d.ts +51 -0
- package/dist/sync/hyperloopArchive.js +153 -0
- package/dist/sync/hyperloopArchive.js.map +1 -0
- package/dist/sync/hyperloopEval.d.ts +123 -0
- package/dist/sync/hyperloopEval.js +389 -0
- package/dist/sync/hyperloopEval.js.map +1 -0
- package/dist/sync/hyperloopEval.test.d.ts +4 -0
- package/dist/sync/hyperloopEval.test.js +60 -0
- package/dist/sync/hyperloopEval.test.js.map +1 -0
- package/dist/sync/protocol.d.ts +172 -0
- package/dist/sync/protocol.js +9 -0
- package/dist/sync/protocol.js.map +1 -0
- package/dist/sync/sessionMemory.d.ts +47 -0
- package/dist/sync/sessionMemory.js +138 -0
- package/dist/sync/sessionMemory.js.map +1 -0
- package/dist/sync/store.d.ts +384 -0
- package/dist/sync/store.js +1435 -0
- package/dist/sync/store.js.map +1 -0
- package/dist/sync/store.test.d.ts +4 -0
- package/dist/sync/store.test.js +43 -0
- package/dist/sync/store.test.js.map +1 -0
- package/dist/sync/syncBridgeClient.d.ts +30 -0
- package/dist/sync/syncBridgeClient.js +172 -0
- package/dist/sync/syncBridgeClient.js.map +1 -0
- package/dist/tools/autonomousDeliveryTools.d.ts +2 -0
- package/dist/tools/autonomousDeliveryTools.js +1104 -0
- package/dist/tools/autonomousDeliveryTools.js.map +1 -0
- package/dist/tools/claudeCodeIngestTools.d.ts +10 -0
- package/dist/tools/claudeCodeIngestTools.js +347 -0
- package/dist/tools/claudeCodeIngestTools.js.map +1 -0
- package/dist/tools/coreWorkflowTools.d.ts +2 -0
- package/dist/tools/coreWorkflowTools.js +488 -0
- package/dist/tools/coreWorkflowTools.js.map +1 -0
- package/dist/tools/deltaTools.d.ts +15 -0
- package/dist/tools/deltaTools.js +1522 -0
- package/dist/tools/deltaTools.js.map +1 -0
- package/dist/tools/entityLookupTools.d.ts +14 -0
- package/dist/tools/entityLookupTools.js +159 -0
- package/dist/tools/entityLookupTools.js.map +1 -0
- package/dist/tools/entityTemporalTools.d.ts +12 -0
- package/dist/tools/entityTemporalTools.js +330 -0
- package/dist/tools/entityTemporalTools.js.map +1 -0
- package/dist/tools/founderLocalPipeline.d.ts +215 -0
- package/dist/tools/founderLocalPipeline.js +1516 -2
- package/dist/tools/founderLocalPipeline.js.map +1 -1
- package/dist/tools/founderOperatingModel.d.ts +120 -0
- package/dist/tools/founderOperatingModel.js +469 -0
- package/dist/tools/founderOperatingModel.js.map +1 -0
- package/dist/tools/founderOperatingModelTools.d.ts +2 -0
- package/dist/tools/founderOperatingModelTools.js +169 -0
- package/dist/tools/founderOperatingModelTools.js.map +1 -0
- package/dist/tools/founderStrategicOpsTools.d.ts +2 -0
- package/dist/tools/founderStrategicOpsTools.js +1310 -0
- package/dist/tools/founderStrategicOpsTools.js.map +1 -0
- package/dist/tools/graphifyTools.d.ts +19 -0
- package/dist/tools/graphifyTools.js +375 -0
- package/dist/tools/graphifyTools.js.map +1 -0
- package/dist/tools/index.d.ts +3 -0
- package/dist/tools/index.js +4 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/monteCarloTools.d.ts +16 -0
- package/dist/tools/monteCarloTools.js +225 -0
- package/dist/tools/monteCarloTools.js.map +1 -0
- package/dist/tools/packetCompilerTools.d.ts +12 -0
- package/dist/tools/packetCompilerTools.js +322 -0
- package/dist/tools/packetCompilerTools.js.map +1 -0
- package/dist/tools/planSynthesisTools.d.ts +15 -0
- package/dist/tools/planSynthesisTools.js +455 -0
- package/dist/tools/planSynthesisTools.js.map +1 -0
- package/dist/tools/profilerTools.d.ts +20 -0
- package/dist/tools/profilerTools.js +364 -0
- package/dist/tools/profilerTools.js.map +1 -0
- package/dist/tools/savingsTools.d.ts +11 -0
- package/dist/tools/savingsTools.js +155 -0
- package/dist/tools/savingsTools.js.map +1 -0
- package/dist/tools/scenarioCompilerTools.d.ts +14 -0
- package/dist/tools/scenarioCompilerTools.js +290 -0
- package/dist/tools/scenarioCompilerTools.js.map +1 -0
- package/dist/tools/sharedContextTools.d.ts +2 -0
- package/dist/tools/sharedContextTools.js +423 -0
- package/dist/tools/sharedContextTools.js.map +1 -0
- package/dist/tools/sitemapTools.d.ts +15 -0
- package/dist/tools/sitemapTools.js +560 -0
- package/dist/tools/sitemapTools.js.map +1 -0
- package/dist/tools/sweepTools.d.ts +9 -0
- package/dist/tools/sweepTools.js +112 -0
- package/dist/tools/sweepTools.js.map +1 -0
- package/dist/tools/syncBridgeTools.d.ts +2 -0
- package/dist/tools/syncBridgeTools.js +258 -0
- package/dist/tools/syncBridgeTools.js.map +1 -0
- package/dist/tools/toolRegistry.js +1216 -49
- package/dist/tools/toolRegistry.js.map +1 -1
- package/dist/tools/workspaceTools.d.ts +19 -0
- package/dist/tools/workspaceTools.js +762 -0
- package/dist/tools/workspaceTools.js.map +1 -0
- package/dist/toolsetRegistry.js +88 -2
- package/dist/toolsetRegistry.js.map +1 -1
- package/package.json +36 -36
- package/rules/nodebench-agentic-reliability.md +32 -0
- package/rules/nodebench-analyst-diagnostic.md +25 -0
- package/rules/nodebench-auto-qa.md +31 -0
- package/rules/nodebench-completion-traceability.md +22 -0
- package/rules/nodebench-flywheel-continuous.md +25 -0
- package/rules/nodebench-pre-release-review.md +24 -0
- package/rules/nodebench-qa-dogfood.md +26 -0
- package/rules/nodebench-scenario-testing.md +30 -0
- package/rules/nodebench-self-direction.md +23 -0
- package/rules/nodebench-self-judge-loop.md +24 -0
- package/scripts/install.sh +215 -0
|
@@ -0,0 +1,1435 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { EventEmitter } from "node:events";
|
|
3
|
+
import { genId, getDb, withObjectNodesFtsRepair } from "../db.js";
|
|
4
|
+
function json(value, fallback = {}) {
|
|
5
|
+
return JSON.stringify(value ?? fallback);
|
|
6
|
+
}
|
|
7
|
+
function parseJson(value, fallback) {
|
|
8
|
+
if (!value)
|
|
9
|
+
return fallback;
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(value);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return fallback;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function hashPayload(payload) {
|
|
18
|
+
return createHash("sha256").update(JSON.stringify(payload ?? null)).digest("hex");
|
|
19
|
+
}
|
|
20
|
+
const sharedContextEventBus = new EventEmitter();
|
|
21
|
+
sharedContextEventBus.setMaxListeners(100);
|
|
22
|
+
export function getSharedContextEventBus() {
|
|
23
|
+
return sharedContextEventBus;
|
|
24
|
+
}
|
|
25
|
+
function emitSharedContextEvent(type, payload) {
|
|
26
|
+
sharedContextEventBus.emit("shared_context", {
|
|
27
|
+
type,
|
|
28
|
+
payload,
|
|
29
|
+
timestamp: new Date().toISOString(),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function parseSharedContextPeerRow(row) {
|
|
33
|
+
return {
|
|
34
|
+
peerId: row.peer_id,
|
|
35
|
+
product: row.product,
|
|
36
|
+
tenantId: row.tenant_id ?? null,
|
|
37
|
+
workspaceId: row.workspace_id ?? null,
|
|
38
|
+
surface: row.surface,
|
|
39
|
+
role: row.role,
|
|
40
|
+
capabilities: parseJson(row.capabilities_json, []),
|
|
41
|
+
contextScopes: parseJson(row.context_scopes_json, []),
|
|
42
|
+
status: row.status,
|
|
43
|
+
summary: parseJson(row.summary_json, {}),
|
|
44
|
+
metadata: parseJson(row.metadata_json, {}),
|
|
45
|
+
lastHeartbeatAt: row.last_heartbeat_at,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function parseSharedContextPacketRow(row) {
|
|
49
|
+
return {
|
|
50
|
+
contextId: row.context_id,
|
|
51
|
+
contextType: row.context_type,
|
|
52
|
+
producerPeerId: row.producer_peer_id,
|
|
53
|
+
tenantId: row.tenant_id ?? null,
|
|
54
|
+
workspaceId: row.workspace_id ?? null,
|
|
55
|
+
scope: parseJson(row.scope_json, []),
|
|
56
|
+
subject: row.subject,
|
|
57
|
+
summary: row.summary,
|
|
58
|
+
claims: parseJson(row.claims_json, []),
|
|
59
|
+
evidenceRefs: parseJson(row.evidence_refs_json, []),
|
|
60
|
+
stateSnapshot: parseJson(row.state_snapshot_json, {}),
|
|
61
|
+
timeWindow: parseJson(row.time_window_json, {}),
|
|
62
|
+
freshness: parseJson(row.freshness_json, {}),
|
|
63
|
+
permissions: parseJson(row.permissions_json, {}),
|
|
64
|
+
confidence: row.confidence ?? undefined,
|
|
65
|
+
lineage: parseJson(row.lineage_json, {}),
|
|
66
|
+
invalidates: parseJson(row.invalidates_json, []),
|
|
67
|
+
nextActions: parseJson(row.next_actions_json, []),
|
|
68
|
+
version: row.version,
|
|
69
|
+
status: row.status,
|
|
70
|
+
metadata: parseJson(row.metadata_json, {}),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function parseSharedContextTaskRow(row) {
|
|
74
|
+
return {
|
|
75
|
+
taskId: row.task_id,
|
|
76
|
+
taskType: row.task_type,
|
|
77
|
+
proposerPeerId: row.proposer_peer_id,
|
|
78
|
+
assigneePeerId: row.assignee_peer_id,
|
|
79
|
+
status: row.status,
|
|
80
|
+
taskSpec: parseJson(row.task_spec_json, {}),
|
|
81
|
+
inputContextIds: parseJson(row.input_context_ids_json, []),
|
|
82
|
+
outputContextId: row.output_context_id ?? null,
|
|
83
|
+
reason: row.reason ?? null,
|
|
84
|
+
createdAt: row.created_at,
|
|
85
|
+
updatedAt: row.updated_at,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function requireSharedContextPeer(peerId) {
|
|
89
|
+
const peer = getSharedContextPeer(peerId);
|
|
90
|
+
if (!peer) {
|
|
91
|
+
throw new Error(`Shared context peer not found: ${peerId}`);
|
|
92
|
+
}
|
|
93
|
+
return peer;
|
|
94
|
+
}
|
|
95
|
+
function normalizeVisibility(permissions) {
|
|
96
|
+
const visibility = permissions?.visibility;
|
|
97
|
+
if (visibility === "tenant" || visibility === "workspace" || visibility === "internal") {
|
|
98
|
+
return visibility;
|
|
99
|
+
}
|
|
100
|
+
return "workspace";
|
|
101
|
+
}
|
|
102
|
+
function assertPeersShareScope(sourcePeer, destinationPeer, action) {
|
|
103
|
+
const sourceWorkspace = sourcePeer.workspaceId ?? null;
|
|
104
|
+
const destinationWorkspace = destinationPeer.workspaceId ?? null;
|
|
105
|
+
const sourceTenant = sourcePeer.tenantId ?? null;
|
|
106
|
+
const destinationTenant = destinationPeer.tenantId ?? null;
|
|
107
|
+
if (sourceTenant && destinationTenant && sourceTenant !== destinationTenant) {
|
|
108
|
+
throw new Error(`${action} denied: peers do not share tenant scope`);
|
|
109
|
+
}
|
|
110
|
+
if (sourceWorkspace && destinationWorkspace && sourceWorkspace !== destinationWorkspace) {
|
|
111
|
+
throw new Error(`${action} denied: peers do not share workspace scope`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function canPeerAccessPacket(peer, packet) {
|
|
115
|
+
const visibility = normalizeVisibility(packet.permissions);
|
|
116
|
+
const allowedRoles = Array.isArray(packet.permissions?.allowedRoles)
|
|
117
|
+
? packet.permissions?.allowedRoles
|
|
118
|
+
: [];
|
|
119
|
+
if (packet.status === "invalidated")
|
|
120
|
+
return false;
|
|
121
|
+
if (allowedRoles.length > 0 && !allowedRoles.includes(peer.role))
|
|
122
|
+
return false;
|
|
123
|
+
if (packet.tenantId && peer.tenantId && packet.tenantId !== peer.tenantId)
|
|
124
|
+
return false;
|
|
125
|
+
if (packet.workspaceId && peer.workspaceId && packet.workspaceId !== peer.workspaceId)
|
|
126
|
+
return false;
|
|
127
|
+
if (visibility === "internal") {
|
|
128
|
+
return packet.producerPeerId === peer.peerId || ((!packet.workspaceId || packet.workspaceId === peer.workspaceId) &&
|
|
129
|
+
(!packet.tenantId || packet.tenantId === peer.tenantId) &&
|
|
130
|
+
peer.product === requireSharedContextPeer(packet.producerPeerId).product);
|
|
131
|
+
}
|
|
132
|
+
if (visibility === "tenant") {
|
|
133
|
+
if (packet.tenantId && peer.tenantId)
|
|
134
|
+
return packet.tenantId === peer.tenantId;
|
|
135
|
+
return !packet.workspaceId || packet.workspaceId === peer.workspaceId;
|
|
136
|
+
}
|
|
137
|
+
return !packet.workspaceId || packet.workspaceId === peer.workspaceId;
|
|
138
|
+
}
|
|
139
|
+
function requirePacketForPeer(contextId, peerId, action) {
|
|
140
|
+
const peer = requireSharedContextPeer(peerId);
|
|
141
|
+
const row = getDb().prepare(`
|
|
142
|
+
SELECT *
|
|
143
|
+
FROM shared_context_packets
|
|
144
|
+
WHERE context_id = ?
|
|
145
|
+
LIMIT 1
|
|
146
|
+
`).get(contextId);
|
|
147
|
+
if (!row) {
|
|
148
|
+
throw new Error(`Shared context packet not found: ${contextId}`);
|
|
149
|
+
}
|
|
150
|
+
const packet = parseSharedContextPacketRow(row);
|
|
151
|
+
if (!canPeerAccessPacket(peer, packet)) {
|
|
152
|
+
throw new Error(`${action} denied: peer ${peerId} cannot access packet ${contextId}`);
|
|
153
|
+
}
|
|
154
|
+
return packet;
|
|
155
|
+
}
|
|
156
|
+
export function upsertDurableObject(input) {
|
|
157
|
+
const db = getDb();
|
|
158
|
+
const now = new Date().toISOString();
|
|
159
|
+
const objectId = input.id ?? genId(String(input.kind));
|
|
160
|
+
const metadataJson = json(input.metadata);
|
|
161
|
+
withObjectNodesFtsRepair(() => {
|
|
162
|
+
db.prepare(`
|
|
163
|
+
INSERT INTO object_nodes (id, kind, label, source, status, metadata_json, created_at, updated_at)
|
|
164
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
165
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
166
|
+
kind = excluded.kind,
|
|
167
|
+
label = excluded.label,
|
|
168
|
+
source = excluded.source,
|
|
169
|
+
status = excluded.status,
|
|
170
|
+
metadata_json = excluded.metadata_json,
|
|
171
|
+
updated_at = excluded.updated_at
|
|
172
|
+
`).run(objectId, input.kind, input.label, input.source ?? "local", input.status ?? "active", metadataJson, now, now);
|
|
173
|
+
});
|
|
174
|
+
const queuedSyncId = input.queueForSync === false
|
|
175
|
+
? undefined
|
|
176
|
+
: enqueueSyncOperation({
|
|
177
|
+
objectId,
|
|
178
|
+
objectKind: input.kind,
|
|
179
|
+
opType: "upsert_object",
|
|
180
|
+
payload: {
|
|
181
|
+
id: objectId,
|
|
182
|
+
kind: input.kind,
|
|
183
|
+
label: input.label,
|
|
184
|
+
source: input.source ?? "local",
|
|
185
|
+
status: input.status ?? "active",
|
|
186
|
+
metadata: input.metadata ?? {},
|
|
187
|
+
},
|
|
188
|
+
}).queueId;
|
|
189
|
+
return { objectId, queuedSyncId };
|
|
190
|
+
}
|
|
191
|
+
export function linkDurableObjects(input) {
|
|
192
|
+
const db = getDb();
|
|
193
|
+
const edgeId = input.id ?? genId("edge");
|
|
194
|
+
db.prepare(`
|
|
195
|
+
INSERT INTO object_edges (id, from_id, to_id, edge_type, confidence, metadata_json, created_at)
|
|
196
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
197
|
+
ON CONFLICT(from_id, to_id, edge_type) DO UPDATE SET
|
|
198
|
+
confidence = excluded.confidence,
|
|
199
|
+
metadata_json = excluded.metadata_json
|
|
200
|
+
`).run(edgeId, input.fromId, input.toId, input.edgeType, input.confidence ?? 1, json(input.metadata), new Date().toISOString());
|
|
201
|
+
const queuedSyncId = input.queueForSync === false
|
|
202
|
+
? undefined
|
|
203
|
+
: enqueueSyncOperation({
|
|
204
|
+
objectId: input.fromId,
|
|
205
|
+
objectKind: "workflow",
|
|
206
|
+
opType: "link_object",
|
|
207
|
+
payload: {
|
|
208
|
+
id: edgeId,
|
|
209
|
+
fromId: input.fromId,
|
|
210
|
+
toId: input.toId,
|
|
211
|
+
edgeType: input.edgeType,
|
|
212
|
+
confidence: input.confidence ?? 1,
|
|
213
|
+
metadata: input.metadata ?? {},
|
|
214
|
+
},
|
|
215
|
+
}).queueId;
|
|
216
|
+
return { edgeId, queuedSyncId };
|
|
217
|
+
}
|
|
218
|
+
export function recordExecutionReceipt(input) {
|
|
219
|
+
const db = getDb();
|
|
220
|
+
const receiptId = input.id ?? genId("receipt");
|
|
221
|
+
const inputHash = input.input === undefined ? null : hashPayload(input.input);
|
|
222
|
+
const outputHash = input.output === undefined ? null : hashPayload(input.output);
|
|
223
|
+
db.prepare(`
|
|
224
|
+
INSERT INTO execution_receipts (id, run_id, trace_id, step_id, object_id, tool_name, action_type, summary, input_hash, output_hash, status, metadata_json, created_at)
|
|
225
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
226
|
+
`).run(receiptId, input.runId ?? null, input.traceId ?? null, input.stepId ?? null, input.objectId ?? null, input.toolName ?? null, input.actionType, input.summary, inputHash, outputHash, input.status ?? "recorded", json({
|
|
227
|
+
...(input.metadata ?? {}),
|
|
228
|
+
input: input.input ?? null,
|
|
229
|
+
output: input.output ?? null,
|
|
230
|
+
}), new Date().toISOString());
|
|
231
|
+
const queuedSyncId = input.queueForSync === false
|
|
232
|
+
? undefined
|
|
233
|
+
: enqueueSyncOperation({
|
|
234
|
+
objectId: input.objectId ?? input.runId ?? receiptId,
|
|
235
|
+
objectKind: "trace",
|
|
236
|
+
opType: "record_receipt",
|
|
237
|
+
payload: {
|
|
238
|
+
id: receiptId,
|
|
239
|
+
runId: input.runId ?? null,
|
|
240
|
+
traceId: input.traceId ?? null,
|
|
241
|
+
stepId: input.stepId ?? null,
|
|
242
|
+
objectId: input.objectId ?? null,
|
|
243
|
+
toolName: input.toolName ?? null,
|
|
244
|
+
actionType: input.actionType,
|
|
245
|
+
summary: input.summary,
|
|
246
|
+
status: input.status ?? "recorded",
|
|
247
|
+
inputHash,
|
|
248
|
+
outputHash,
|
|
249
|
+
metadata: input.metadata ?? {},
|
|
250
|
+
},
|
|
251
|
+
}).queueId;
|
|
252
|
+
return { receiptId, queuedSyncId };
|
|
253
|
+
}
|
|
254
|
+
export function recordLocalArtifact(input) {
|
|
255
|
+
const db = getDb();
|
|
256
|
+
const artifactId = input.id ?? genId("artifact");
|
|
257
|
+
const contentHash = input.content ? hashPayload(input.content) : null;
|
|
258
|
+
db.prepare(`
|
|
259
|
+
INSERT INTO local_artifacts (id, run_id, object_id, kind, path, content_hash, summary, verification_status, metadata_json, created_at)
|
|
260
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
261
|
+
`).run(artifactId, input.runId ?? null, input.objectId ?? null, input.kind, input.path ?? null, contentHash, input.summary ?? null, input.verificationStatus ?? "unverified", json({
|
|
262
|
+
...(input.metadata ?? {}),
|
|
263
|
+
contentPreview: typeof input.content === "string" ? input.content.slice(0, 500) : null,
|
|
264
|
+
}), new Date().toISOString());
|
|
265
|
+
const queuedSyncId = input.queueForSync === false
|
|
266
|
+
? undefined
|
|
267
|
+
: enqueueSyncOperation({
|
|
268
|
+
objectId: input.objectId ?? artifactId,
|
|
269
|
+
objectKind: "artifact",
|
|
270
|
+
opType: "record_artifact",
|
|
271
|
+
payload: {
|
|
272
|
+
id: artifactId,
|
|
273
|
+
runId: input.runId ?? null,
|
|
274
|
+
objectId: input.objectId ?? null,
|
|
275
|
+
kind: input.kind,
|
|
276
|
+
path: input.path ?? null,
|
|
277
|
+
contentHash,
|
|
278
|
+
summary: input.summary ?? null,
|
|
279
|
+
verificationStatus: input.verificationStatus ?? "unverified",
|
|
280
|
+
metadata: input.metadata ?? {},
|
|
281
|
+
},
|
|
282
|
+
}).queueId;
|
|
283
|
+
return { artifactId, queuedSyncId };
|
|
284
|
+
}
|
|
285
|
+
export function recordLocalOutcome(input) {
|
|
286
|
+
const db = getDb();
|
|
287
|
+
const outcomeId = input.id ?? genId("outcome");
|
|
288
|
+
db.prepare(`
|
|
289
|
+
INSERT INTO local_outcomes (id, run_id, object_id, outcome_type, headline, user_value, stakeholder_value, status, evidence_json, metadata_json, created_at)
|
|
290
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
291
|
+
`).run(outcomeId, input.runId ?? null, input.objectId ?? null, input.outcomeType, input.headline, input.userValue ?? null, input.stakeholderValue ?? null, input.status ?? "draft", json(input.evidence ?? [], []), json(input.metadata), new Date().toISOString());
|
|
292
|
+
const queuedSyncId = input.queueForSync === false
|
|
293
|
+
? undefined
|
|
294
|
+
: enqueueSyncOperation({
|
|
295
|
+
objectId: input.objectId ?? outcomeId,
|
|
296
|
+
objectKind: "outcome",
|
|
297
|
+
opType: "record_outcome",
|
|
298
|
+
payload: {
|
|
299
|
+
id: outcomeId,
|
|
300
|
+
runId: input.runId ?? null,
|
|
301
|
+
objectId: input.objectId ?? null,
|
|
302
|
+
outcomeType: input.outcomeType,
|
|
303
|
+
headline: input.headline,
|
|
304
|
+
userValue: input.userValue ?? null,
|
|
305
|
+
stakeholderValue: input.stakeholderValue ?? null,
|
|
306
|
+
status: input.status ?? "draft",
|
|
307
|
+
evidence: input.evidence ?? [],
|
|
308
|
+
metadata: input.metadata ?? {},
|
|
309
|
+
},
|
|
310
|
+
}).queueId;
|
|
311
|
+
return { outcomeId, queuedSyncId };
|
|
312
|
+
}
|
|
313
|
+
export function upsertDeviceBinding(input) {
|
|
314
|
+
const now = new Date().toISOString();
|
|
315
|
+
getDb().prepare(`
|
|
316
|
+
INSERT INTO device_bindings (device_id, device_name, platform, app_version, bridge_url, device_token, binding_status, metadata_json, paired_at, last_seen_at, created_at, updated_at)
|
|
317
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
318
|
+
ON CONFLICT(device_id) DO UPDATE SET
|
|
319
|
+
device_name = excluded.device_name,
|
|
320
|
+
platform = excluded.platform,
|
|
321
|
+
app_version = excluded.app_version,
|
|
322
|
+
bridge_url = excluded.bridge_url,
|
|
323
|
+
device_token = excluded.device_token,
|
|
324
|
+
binding_status = excluded.binding_status,
|
|
325
|
+
metadata_json = excluded.metadata_json,
|
|
326
|
+
paired_at = COALESCE(excluded.paired_at, device_bindings.paired_at),
|
|
327
|
+
last_seen_at = excluded.last_seen_at,
|
|
328
|
+
updated_at = excluded.updated_at
|
|
329
|
+
`).run(input.deviceId, input.deviceName, input.platform ?? null, input.appVersion ?? null, input.bridgeUrl ?? null, input.deviceToken ?? null, input.bindingStatus ?? "unpaired", json(input.metadata), input.bindingStatus === "paired" ? now : null, now, now, now);
|
|
330
|
+
}
|
|
331
|
+
export function bindDeviceToAccount(input) {
|
|
332
|
+
const bindingId = genId("acct");
|
|
333
|
+
const now = new Date().toISOString();
|
|
334
|
+
getDb().prepare(`
|
|
335
|
+
INSERT INTO account_bindings (id, device_id, user_id, workspace_id, scopes_json, sync_enabled, sync_mode, metadata_json, paired_at, created_at, updated_at)
|
|
336
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
337
|
+
`).run(bindingId, input.deviceId, input.userId, input.workspaceId ?? null, json(input.scopes, []), input.syncEnabled === false ? 0 : 1, input.syncMode ?? "connected", json(input.metadata), now, now, now);
|
|
338
|
+
return { bindingId };
|
|
339
|
+
}
|
|
340
|
+
export function getActiveAccountBinding(deviceId) {
|
|
341
|
+
const row = getDb().prepare(`
|
|
342
|
+
SELECT id, device_id, user_id, workspace_id, scopes_json, sync_enabled, sync_mode, last_synced_at, revoked_at
|
|
343
|
+
FROM account_bindings
|
|
344
|
+
WHERE (? IS NULL OR device_id = ?)
|
|
345
|
+
AND revoked_at IS NULL
|
|
346
|
+
ORDER BY created_at DESC
|
|
347
|
+
LIMIT 1
|
|
348
|
+
`).get(deviceId ?? null, deviceId ?? null);
|
|
349
|
+
if (!row)
|
|
350
|
+
return null;
|
|
351
|
+
return {
|
|
352
|
+
bindingId: row.id,
|
|
353
|
+
deviceId: row.device_id,
|
|
354
|
+
userId: row.user_id,
|
|
355
|
+
workspaceId: row.workspace_id ?? null,
|
|
356
|
+
scopes: parseJson(row.scopes_json, []),
|
|
357
|
+
syncEnabled: Boolean(row.sync_enabled),
|
|
358
|
+
syncMode: row.sync_mode,
|
|
359
|
+
lastSyncedAt: row.last_synced_at ?? null,
|
|
360
|
+
revokedAt: row.revoked_at ?? null,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
export function getDeviceBinding(deviceId) {
|
|
364
|
+
const row = getDb().prepare(`
|
|
365
|
+
SELECT device_id, device_name, platform, app_version, bridge_url, device_token, binding_status, last_seen_at
|
|
366
|
+
FROM device_bindings
|
|
367
|
+
WHERE device_id = ?
|
|
368
|
+
LIMIT 1
|
|
369
|
+
`).get(deviceId);
|
|
370
|
+
if (!row)
|
|
371
|
+
return null;
|
|
372
|
+
return {
|
|
373
|
+
deviceId: row.device_id,
|
|
374
|
+
deviceName: row.device_name,
|
|
375
|
+
platform: row.platform ?? null,
|
|
376
|
+
appVersion: row.app_version ?? null,
|
|
377
|
+
bridgeUrl: row.bridge_url ?? null,
|
|
378
|
+
deviceToken: row.device_token ?? null,
|
|
379
|
+
bindingStatus: row.binding_status,
|
|
380
|
+
lastSeenAt: row.last_seen_at ?? null,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
export function enqueueSyncOperation(input) {
|
|
384
|
+
const queueId = genId("sync");
|
|
385
|
+
const payloadHash = hashPayload(input.payload);
|
|
386
|
+
const now = new Date().toISOString();
|
|
387
|
+
getDb().prepare(`
|
|
388
|
+
INSERT INTO sync_queue (id, object_id, object_kind, op_type, payload_json, payload_hash, sync_status, created_at, updated_at)
|
|
389
|
+
VALUES (?, ?, ?, ?, ?, ?, 'pending', ?, ?)
|
|
390
|
+
`).run(queueId, input.objectId, input.objectKind, input.opType, json(input.payload), payloadHash, now, now);
|
|
391
|
+
return { queueId, payloadHash };
|
|
392
|
+
}
|
|
393
|
+
export function listPendingSyncOperations(limit = 50) {
|
|
394
|
+
const rows = getDb().prepare(`
|
|
395
|
+
SELECT id, object_id, object_kind, op_type, payload_json, payload_hash, created_at
|
|
396
|
+
FROM sync_queue
|
|
397
|
+
WHERE sync_status IN ('pending', 'retry')
|
|
398
|
+
ORDER BY created_at ASC
|
|
399
|
+
LIMIT ?
|
|
400
|
+
`).all(limit);
|
|
401
|
+
return rows.map((row) => ({
|
|
402
|
+
id: row.id,
|
|
403
|
+
objectId: row.object_id ?? null,
|
|
404
|
+
objectKind: row.object_kind,
|
|
405
|
+
opType: row.op_type,
|
|
406
|
+
payload: parseJson(row.payload_json, {}),
|
|
407
|
+
payloadHash: row.payload_hash,
|
|
408
|
+
createdAt: row.created_at,
|
|
409
|
+
}));
|
|
410
|
+
}
|
|
411
|
+
export function markSyncAttempt(queueIds, error) {
|
|
412
|
+
if (queueIds.length === 0)
|
|
413
|
+
return;
|
|
414
|
+
const db = getDb();
|
|
415
|
+
const placeholders = queueIds.map(() => "?").join(", ");
|
|
416
|
+
const now = new Date().toISOString();
|
|
417
|
+
db.prepare(`
|
|
418
|
+
UPDATE sync_queue
|
|
419
|
+
SET sync_status = ?, retry_count = retry_count + 1, last_error = ?, last_attempt_at = ?, updated_at = ?
|
|
420
|
+
WHERE id IN (${placeholders})
|
|
421
|
+
`).run(error ? "retry" : "in_flight", error ?? null, now, now, ...queueIds);
|
|
422
|
+
}
|
|
423
|
+
export function acknowledgeSyncOperations(args) {
|
|
424
|
+
if (args.queueIds.length === 0)
|
|
425
|
+
return;
|
|
426
|
+
const db = getDb();
|
|
427
|
+
const placeholders = args.queueIds.map(() => "?").join(", ");
|
|
428
|
+
const now = new Date().toISOString();
|
|
429
|
+
db.prepare(`
|
|
430
|
+
UPDATE sync_queue
|
|
431
|
+
SET sync_status = 'acknowledged', acknowledged_at = ?, last_attempt_at = ?, updated_at = ?, last_error = NULL
|
|
432
|
+
WHERE id IN (${placeholders})
|
|
433
|
+
`).run(now, now, now, ...args.queueIds);
|
|
434
|
+
const insertReceipt = db.prepare(`
|
|
435
|
+
INSERT INTO sync_receipts (id, queue_id, server_receipt_id, device_id, user_id, workspace_id, status, detail_json, created_at)
|
|
436
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
437
|
+
`);
|
|
438
|
+
for (const queueId of args.queueIds) {
|
|
439
|
+
insertReceipt.run(genId("sync_receipt"), queueId, args.serverReceiptId ?? null, args.deviceId ?? null, args.userId ?? null, args.workspaceId ?? null, "acknowledged", json(args.detail), now);
|
|
440
|
+
}
|
|
441
|
+
if (args.deviceId && args.userId) {
|
|
442
|
+
db.prepare(`
|
|
443
|
+
UPDATE account_bindings
|
|
444
|
+
SET last_synced_at = ?, updated_at = ?
|
|
445
|
+
WHERE device_id = ? AND user_id = ? AND revoked_at IS NULL
|
|
446
|
+
`).run(now, now, args.deviceId, args.userId);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
export function failSyncOperations(args) {
|
|
450
|
+
if (args.rejected.length === 0)
|
|
451
|
+
return;
|
|
452
|
+
const db = getDb();
|
|
453
|
+
const now = new Date().toISOString();
|
|
454
|
+
const update = db.prepare(`
|
|
455
|
+
UPDATE sync_queue
|
|
456
|
+
SET sync_status = 'retry', retry_count = retry_count + 1, last_error = ?, last_attempt_at = ?, updated_at = ?
|
|
457
|
+
WHERE id = ?
|
|
458
|
+
`);
|
|
459
|
+
const insertReceipt = db.prepare(`
|
|
460
|
+
INSERT INTO sync_receipts (id, queue_id, device_id, user_id, workspace_id, status, detail_json, created_at)
|
|
461
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
462
|
+
`);
|
|
463
|
+
for (const rejected of args.rejected) {
|
|
464
|
+
update.run(rejected.reason, now, now, rejected.id);
|
|
465
|
+
insertReceipt.run(genId("sync_receipt"), rejected.id, args.deviceId ?? null, args.userId ?? null, args.workspaceId ?? null, "rejected", json({ reason: rejected.reason }), now);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
export function getSyncBridgeStatus(deviceId) {
|
|
469
|
+
const db = getDb();
|
|
470
|
+
const counts = db.prepare(`
|
|
471
|
+
SELECT
|
|
472
|
+
SUM(CASE WHEN sync_status = 'pending' THEN 1 ELSE 0 END) AS pending_count,
|
|
473
|
+
SUM(CASE WHEN sync_status = 'retry' THEN 1 ELSE 0 END) AS retry_count,
|
|
474
|
+
SUM(CASE WHEN sync_status = 'acknowledged' THEN 1 ELSE 0 END) AS acknowledged_count,
|
|
475
|
+
MAX(acknowledged_at) AS last_acknowledged_at
|
|
476
|
+
FROM sync_queue
|
|
477
|
+
`).get();
|
|
478
|
+
const activeBinding = getActiveAccountBinding(deviceId);
|
|
479
|
+
return {
|
|
480
|
+
mode: activeBinding && activeBinding.syncEnabled ? "connected" : "offline",
|
|
481
|
+
activeBinding,
|
|
482
|
+
pendingCount: counts?.pending_count ?? 0,
|
|
483
|
+
retryCount: counts?.retry_count ?? 0,
|
|
484
|
+
acknowledgedCount: counts?.acknowledged_count ?? 0,
|
|
485
|
+
lastAcknowledgedAt: counts?.last_acknowledged_at ?? null,
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
export function registerSharedContextPeer(input) {
|
|
489
|
+
const db = getDb();
|
|
490
|
+
const now = new Date().toISOString();
|
|
491
|
+
const peerId = input.peerId ?? genId("peer");
|
|
492
|
+
db.prepare(`
|
|
493
|
+
INSERT INTO shared_context_peers (
|
|
494
|
+
peer_id, product, tenant_id, workspace_id, surface, role, capabilities_json,
|
|
495
|
+
context_scopes_json, status, summary_json, metadata_json, last_heartbeat_at, created_at, updated_at
|
|
496
|
+
)
|
|
497
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
498
|
+
ON CONFLICT(peer_id) DO UPDATE SET
|
|
499
|
+
product = excluded.product,
|
|
500
|
+
tenant_id = excluded.tenant_id,
|
|
501
|
+
workspace_id = excluded.workspace_id,
|
|
502
|
+
surface = excluded.surface,
|
|
503
|
+
role = excluded.role,
|
|
504
|
+
capabilities_json = excluded.capabilities_json,
|
|
505
|
+
context_scopes_json = excluded.context_scopes_json,
|
|
506
|
+
status = excluded.status,
|
|
507
|
+
summary_json = excluded.summary_json,
|
|
508
|
+
metadata_json = excluded.metadata_json,
|
|
509
|
+
last_heartbeat_at = excluded.last_heartbeat_at,
|
|
510
|
+
updated_at = excluded.updated_at
|
|
511
|
+
`).run(peerId, input.product, input.tenantId ?? null, input.workspaceId ?? null, input.surface, input.role, json(input.capabilities ?? [], []), json(input.contextScopes ?? [], []), input.status ?? "active", json(input.summary ?? {}, {}), json(input.metadata), now, now, now);
|
|
512
|
+
upsertDurableObject({
|
|
513
|
+
id: peerId,
|
|
514
|
+
kind: "peer",
|
|
515
|
+
label: `${input.product}:${input.role}:${input.surface}`,
|
|
516
|
+
source: "local",
|
|
517
|
+
status: input.status ?? "active",
|
|
518
|
+
metadata: {
|
|
519
|
+
product: input.product,
|
|
520
|
+
tenantId: input.tenantId ?? null,
|
|
521
|
+
workspaceId: input.workspaceId ?? null,
|
|
522
|
+
surface: input.surface,
|
|
523
|
+
role: input.role,
|
|
524
|
+
capabilities: input.capabilities ?? [],
|
|
525
|
+
contextScopes: input.contextScopes ?? [],
|
|
526
|
+
summary: input.summary ?? {},
|
|
527
|
+
...(input.metadata ?? {}),
|
|
528
|
+
},
|
|
529
|
+
queueForSync: false,
|
|
530
|
+
});
|
|
531
|
+
const queuedSyncId = input.queueForSync === false
|
|
532
|
+
? undefined
|
|
533
|
+
: enqueueSyncOperation({
|
|
534
|
+
objectId: peerId,
|
|
535
|
+
objectKind: "peer",
|
|
536
|
+
opType: "register_peer",
|
|
537
|
+
payload: {
|
|
538
|
+
peerId,
|
|
539
|
+
product: input.product,
|
|
540
|
+
tenantId: input.tenantId ?? null,
|
|
541
|
+
workspaceId: input.workspaceId ?? null,
|
|
542
|
+
surface: input.surface,
|
|
543
|
+
role: input.role,
|
|
544
|
+
capabilities: input.capabilities ?? [],
|
|
545
|
+
contextScopes: input.contextScopes ?? [],
|
|
546
|
+
status: input.status ?? "active",
|
|
547
|
+
summary: input.summary ?? {},
|
|
548
|
+
metadata: input.metadata ?? {},
|
|
549
|
+
lastHeartbeatAt: now,
|
|
550
|
+
},
|
|
551
|
+
}).queueId;
|
|
552
|
+
emitSharedContextEvent("peer_registered", {
|
|
553
|
+
peerId,
|
|
554
|
+
product: input.product,
|
|
555
|
+
workspaceId: input.workspaceId ?? null,
|
|
556
|
+
role: input.role,
|
|
557
|
+
surface: input.surface,
|
|
558
|
+
status: input.status ?? "active",
|
|
559
|
+
});
|
|
560
|
+
return { peerId, queuedSyncId };
|
|
561
|
+
}
|
|
562
|
+
export function heartbeatSharedContextPeer(peerId, summary) {
|
|
563
|
+
const now = new Date().toISOString();
|
|
564
|
+
getDb().prepare(`
|
|
565
|
+
UPDATE shared_context_peers
|
|
566
|
+
SET status = 'active',
|
|
567
|
+
summary_json = COALESCE(?, summary_json),
|
|
568
|
+
last_heartbeat_at = ?,
|
|
569
|
+
updated_at = ?
|
|
570
|
+
WHERE peer_id = ?
|
|
571
|
+
`).run(summary ? json(summary, {}) : null, now, now, peerId);
|
|
572
|
+
const existing = getSharedContextPeer(peerId);
|
|
573
|
+
if (existing) {
|
|
574
|
+
upsertDurableObject({
|
|
575
|
+
id: peerId,
|
|
576
|
+
kind: "peer",
|
|
577
|
+
label: `${existing.product}:${existing.role}:${existing.surface}`,
|
|
578
|
+
source: "local",
|
|
579
|
+
status: "active",
|
|
580
|
+
metadata: {
|
|
581
|
+
product: existing.product,
|
|
582
|
+
tenantId: existing.tenantId ?? null,
|
|
583
|
+
workspaceId: existing.workspaceId ?? null,
|
|
584
|
+
surface: existing.surface,
|
|
585
|
+
role: existing.role,
|
|
586
|
+
capabilities: existing.capabilities,
|
|
587
|
+
contextScopes: existing.contextScopes,
|
|
588
|
+
summary: summary ?? existing.summary,
|
|
589
|
+
...(existing.metadata ?? {}),
|
|
590
|
+
},
|
|
591
|
+
queueForSync: false,
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
emitSharedContextEvent("peer_heartbeat", {
|
|
595
|
+
peerId,
|
|
596
|
+
summary: summary ?? existing?.summary ?? {},
|
|
597
|
+
lastHeartbeatAt: now,
|
|
598
|
+
});
|
|
599
|
+
return { peerId, lastHeartbeatAt: now };
|
|
600
|
+
}
|
|
601
|
+
export function listSharedContextPeers(filters = {}) {
|
|
602
|
+
const rows = getDb().prepare(`
|
|
603
|
+
SELECT *
|
|
604
|
+
FROM shared_context_peers
|
|
605
|
+
WHERE (? IS NULL OR product = ?)
|
|
606
|
+
AND (? IS NULL OR workspace_id = ?)
|
|
607
|
+
AND (? IS NULL OR tenant_id = ?)
|
|
608
|
+
AND (? IS NULL OR role = ?)
|
|
609
|
+
AND (? IS NULL OR surface = ?)
|
|
610
|
+
AND (? IS NULL OR status = ?)
|
|
611
|
+
AND (? IS NULL OR capabilities_json LIKE '%' || ? || '%')
|
|
612
|
+
AND (? IS NULL OR context_scopes_json LIKE '%' || ? || '%')
|
|
613
|
+
ORDER BY last_heartbeat_at DESC
|
|
614
|
+
LIMIT ?
|
|
615
|
+
`).all(filters.product ?? null, filters.product ?? null, filters.workspaceId ?? null, filters.workspaceId ?? null, filters.tenantId ?? null, filters.tenantId ?? null, filters.role ?? null, filters.role ?? null, filters.surface ?? null, filters.surface ?? null, filters.status ?? null, filters.status ?? null, filters.capability ?? null, filters.capability ?? null, filters.scope ?? null, filters.scope ?? null, filters.limit ?? 50);
|
|
616
|
+
return rows.map(parseSharedContextPeerRow);
|
|
617
|
+
}
|
|
618
|
+
export function getSharedContextPeer(peerId) {
|
|
619
|
+
const row = getDb().prepare(`
|
|
620
|
+
SELECT *
|
|
621
|
+
FROM shared_context_peers
|
|
622
|
+
WHERE peer_id = ?
|
|
623
|
+
LIMIT 1
|
|
624
|
+
`).get(peerId);
|
|
625
|
+
return row ? parseSharedContextPeerRow(row) : null;
|
|
626
|
+
}
|
|
627
|
+
export function getSharedContextPacket(contextId, requestingPeerId) {
|
|
628
|
+
const row = getDb().prepare(`
|
|
629
|
+
SELECT *
|
|
630
|
+
FROM shared_context_packets
|
|
631
|
+
WHERE context_id = ?
|
|
632
|
+
LIMIT 1
|
|
633
|
+
`).get(contextId);
|
|
634
|
+
if (!row)
|
|
635
|
+
return null;
|
|
636
|
+
const packet = parseSharedContextPacketRow(row);
|
|
637
|
+
if (!requestingPeerId)
|
|
638
|
+
return packet;
|
|
639
|
+
const peer = requireSharedContextPeer(requestingPeerId);
|
|
640
|
+
return canPeerAccessPacket(peer, packet) ? packet : null;
|
|
641
|
+
}
|
|
642
|
+
function getPrimaryPacketScope(packet) {
|
|
643
|
+
return packet.scope.find((scope) => scope !== "workspace");
|
|
644
|
+
}
|
|
645
|
+
export function buildSharedContextPacketResource(packet, requestingPeerId) {
|
|
646
|
+
const primaryScope = getPrimaryPacketScope(packet);
|
|
647
|
+
return {
|
|
648
|
+
packet,
|
|
649
|
+
resourceUri: `shared-context://packet/${encodeURIComponent(packet.contextId)}`,
|
|
650
|
+
pullQuery: {
|
|
651
|
+
contextType: packet.contextType,
|
|
652
|
+
producerPeerId: packet.producerPeerId,
|
|
653
|
+
workspaceId: packet.workspaceId ?? undefined,
|
|
654
|
+
tenantId: packet.tenantId ?? undefined,
|
|
655
|
+
scopeIncludes: primaryScope,
|
|
656
|
+
subjectIncludes: packet.subject,
|
|
657
|
+
},
|
|
658
|
+
subscriptionQuery: {
|
|
659
|
+
peerId: requestingPeerId ?? undefined,
|
|
660
|
+
workspaceId: packet.workspaceId ?? undefined,
|
|
661
|
+
contextType: packet.contextType,
|
|
662
|
+
producerPeerId: packet.producerPeerId,
|
|
663
|
+
scopeIncludes: primaryScope,
|
|
664
|
+
subjectIncludes: packet.subject,
|
|
665
|
+
eventTypes: [
|
|
666
|
+
"packet_published",
|
|
667
|
+
"packet_invalidated",
|
|
668
|
+
"packet_acknowledged",
|
|
669
|
+
"task_proposed",
|
|
670
|
+
"task_status_changed",
|
|
671
|
+
],
|
|
672
|
+
},
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
export function getSharedContextPacketResource(contextId, requestingPeerId) {
|
|
676
|
+
const packet = getSharedContextPacket(contextId, requestingPeerId);
|
|
677
|
+
if (!packet)
|
|
678
|
+
return null;
|
|
679
|
+
return buildSharedContextPacketResource(packet, requestingPeerId);
|
|
680
|
+
}
|
|
681
|
+
function normalizeSubscriptionEventTypes(eventTypes) {
|
|
682
|
+
return eventTypes && eventTypes.length > 0
|
|
683
|
+
? Array.from(new Set(eventTypes))
|
|
684
|
+
: [
|
|
685
|
+
"packet_published",
|
|
686
|
+
"packet_invalidated",
|
|
687
|
+
"packet_acknowledged",
|
|
688
|
+
"task_proposed",
|
|
689
|
+
"task_status_changed",
|
|
690
|
+
"message_sent",
|
|
691
|
+
];
|
|
692
|
+
}
|
|
693
|
+
function taskMatchesSubscription(task, packets, filters) {
|
|
694
|
+
if (filters.taskType && task.taskType !== filters.taskType)
|
|
695
|
+
return false;
|
|
696
|
+
if (filters.peerId && task.proposerPeerId !== filters.peerId && task.assigneePeerId !== filters.peerId) {
|
|
697
|
+
const relatedPacket = task.outputContextId
|
|
698
|
+
? packets.some((packet) => packet.contextId === task.outputContextId)
|
|
699
|
+
: false;
|
|
700
|
+
if (!relatedPacket)
|
|
701
|
+
return false;
|
|
702
|
+
}
|
|
703
|
+
if (!filters.workspaceId)
|
|
704
|
+
return true;
|
|
705
|
+
const packetWorkspaceMatch = packets.some((packet) => packet.contextId === task.outputContextId || task.inputContextIds.includes(packet.contextId));
|
|
706
|
+
if (packetWorkspaceMatch)
|
|
707
|
+
return true;
|
|
708
|
+
return false;
|
|
709
|
+
}
|
|
710
|
+
function messageMatchesSubscription(message, peers, filters) {
|
|
711
|
+
if (filters.messageClass && message.messageClass !== filters.messageClass)
|
|
712
|
+
return false;
|
|
713
|
+
if (filters.peerId && message.fromPeerId !== filters.peerId && message.toPeerId !== filters.peerId) {
|
|
714
|
+
return false;
|
|
715
|
+
}
|
|
716
|
+
if (!filters.workspaceId)
|
|
717
|
+
return true;
|
|
718
|
+
return peers.some((peer) => peer.workspaceId === filters.workspaceId &&
|
|
719
|
+
(peer.peerId === message.fromPeerId || peer.peerId === message.toPeerId));
|
|
720
|
+
}
|
|
721
|
+
export function buildSharedContextSubscriptionManifest(query = {}) {
|
|
722
|
+
const limit = query.limit ?? 10;
|
|
723
|
+
const packets = pullSharedContextPackets({
|
|
724
|
+
contextType: query.contextType,
|
|
725
|
+
producerPeerId: query.producerPeerId,
|
|
726
|
+
requestingPeerId: query.peerId ?? query.requestingPeerId,
|
|
727
|
+
tenantId: query.tenantId,
|
|
728
|
+
workspaceId: query.workspaceId,
|
|
729
|
+
status: query.status,
|
|
730
|
+
scopeIncludes: query.scopeIncludes,
|
|
731
|
+
subjectIncludes: query.subjectIncludes,
|
|
732
|
+
limit,
|
|
733
|
+
});
|
|
734
|
+
const packetResources = packets.slice(0, limit).map((packet) => {
|
|
735
|
+
const resource = buildSharedContextPacketResource(packet, query.peerId ?? query.requestingPeerId);
|
|
736
|
+
return {
|
|
737
|
+
contextId: packet.contextId,
|
|
738
|
+
contextType: packet.contextType,
|
|
739
|
+
subject: packet.subject,
|
|
740
|
+
resourceUri: resource.resourceUri,
|
|
741
|
+
};
|
|
742
|
+
});
|
|
743
|
+
return {
|
|
744
|
+
peerId: query.peerId ?? query.requestingPeerId,
|
|
745
|
+
snapshotQuery: {
|
|
746
|
+
limit,
|
|
747
|
+
peerId: query.peerId ?? query.requestingPeerId,
|
|
748
|
+
workspaceId: query.workspaceId ?? undefined,
|
|
749
|
+
contextType: query.contextType,
|
|
750
|
+
producerPeerId: query.producerPeerId,
|
|
751
|
+
scopeIncludes: query.scopeIncludes,
|
|
752
|
+
subjectIncludes: query.subjectIncludes,
|
|
753
|
+
taskType: query.taskType,
|
|
754
|
+
messageClass: query.messageClass,
|
|
755
|
+
},
|
|
756
|
+
pullQuery: {
|
|
757
|
+
contextType: query.contextType,
|
|
758
|
+
producerPeerId: query.producerPeerId,
|
|
759
|
+
requestingPeerId: query.peerId ?? query.requestingPeerId,
|
|
760
|
+
tenantId: query.tenantId,
|
|
761
|
+
workspaceId: query.workspaceId,
|
|
762
|
+
status: query.status,
|
|
763
|
+
scopeIncludes: query.scopeIncludes,
|
|
764
|
+
subjectIncludes: query.subjectIncludes,
|
|
765
|
+
limit,
|
|
766
|
+
},
|
|
767
|
+
subscriptionQuery: {
|
|
768
|
+
peerId: query.peerId ?? query.requestingPeerId,
|
|
769
|
+
workspaceId: query.workspaceId ?? undefined,
|
|
770
|
+
contextType: query.contextType,
|
|
771
|
+
producerPeerId: query.producerPeerId,
|
|
772
|
+
scopeIncludes: query.scopeIncludes,
|
|
773
|
+
subjectIncludes: query.subjectIncludes,
|
|
774
|
+
taskType: query.taskType,
|
|
775
|
+
messageClass: query.messageClass,
|
|
776
|
+
eventTypes: normalizeSubscriptionEventTypes(query.eventTypes),
|
|
777
|
+
},
|
|
778
|
+
packetResources,
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
export function publishSharedContextPacket(input) {
|
|
782
|
+
const db = getDb();
|
|
783
|
+
const now = new Date().toISOString();
|
|
784
|
+
const contextId = input.contextId ?? genId("context");
|
|
785
|
+
const producerPeer = requireSharedContextPeer(input.producerPeerId);
|
|
786
|
+
const workspaceId = input.workspaceId ?? producerPeer.workspaceId ?? null;
|
|
787
|
+
const tenantId = input.tenantId ?? producerPeer.tenantId ?? null;
|
|
788
|
+
const permissions = {
|
|
789
|
+
visibility: "workspace",
|
|
790
|
+
...(input.permissions ?? {}),
|
|
791
|
+
};
|
|
792
|
+
if (producerPeer.workspaceId && workspaceId && producerPeer.workspaceId !== workspaceId) {
|
|
793
|
+
throw new Error(`publish_shared_context denied: producer peer ${input.producerPeerId} cannot publish outside its workspace`);
|
|
794
|
+
}
|
|
795
|
+
if (producerPeer.tenantId && tenantId && producerPeer.tenantId !== tenantId) {
|
|
796
|
+
throw new Error(`publish_shared_context denied: producer peer ${input.producerPeerId} cannot publish outside its tenant`);
|
|
797
|
+
}
|
|
798
|
+
db.prepare(`
|
|
799
|
+
INSERT INTO shared_context_packets (
|
|
800
|
+
context_id, context_type, producer_peer_id, tenant_id, workspace_id, scope_json, subject, summary,
|
|
801
|
+
claims_json, evidence_refs_json, state_snapshot_json, time_window_json, freshness_json, permissions_json,
|
|
802
|
+
confidence, lineage_json, invalidates_json, next_actions_json, version, status, metadata_json, created_at, updated_at
|
|
803
|
+
)
|
|
804
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
805
|
+
ON CONFLICT(context_id) DO UPDATE SET
|
|
806
|
+
context_type = excluded.context_type,
|
|
807
|
+
producer_peer_id = excluded.producer_peer_id,
|
|
808
|
+
tenant_id = excluded.tenant_id,
|
|
809
|
+
workspace_id = excluded.workspace_id,
|
|
810
|
+
scope_json = excluded.scope_json,
|
|
811
|
+
subject = excluded.subject,
|
|
812
|
+
summary = excluded.summary,
|
|
813
|
+
claims_json = excluded.claims_json,
|
|
814
|
+
evidence_refs_json = excluded.evidence_refs_json,
|
|
815
|
+
state_snapshot_json = excluded.state_snapshot_json,
|
|
816
|
+
time_window_json = excluded.time_window_json,
|
|
817
|
+
freshness_json = excluded.freshness_json,
|
|
818
|
+
permissions_json = excluded.permissions_json,
|
|
819
|
+
confidence = excluded.confidence,
|
|
820
|
+
lineage_json = excluded.lineage_json,
|
|
821
|
+
invalidates_json = excluded.invalidates_json,
|
|
822
|
+
next_actions_json = excluded.next_actions_json,
|
|
823
|
+
version = excluded.version,
|
|
824
|
+
status = excluded.status,
|
|
825
|
+
metadata_json = excluded.metadata_json,
|
|
826
|
+
updated_at = excluded.updated_at
|
|
827
|
+
`).run(contextId, input.contextType, input.producerPeerId, tenantId, workspaceId, json(input.scope ?? [], []), input.subject, input.summary, json(input.claims ?? [], []), json(input.evidenceRefs ?? [], []), json(input.stateSnapshot ?? {}, {}), json(input.timeWindow ?? {}, {}), json(input.freshness ?? {}, {}), json(permissions, {}), input.confidence ?? null, json(input.lineage ?? {}, {}), json(input.invalidates ?? [], []), json(input.nextActions ?? [], []), input.version ?? 1, input.status ?? "active", json(input.metadata), now, now);
|
|
828
|
+
if ((input.invalidates ?? []).length > 0) {
|
|
829
|
+
const invalidate = db.prepare(`
|
|
830
|
+
UPDATE shared_context_packets
|
|
831
|
+
SET status = 'invalidated', updated_at = ?
|
|
832
|
+
WHERE context_id = ?
|
|
833
|
+
`);
|
|
834
|
+
for (const invalidatedId of input.invalidates ?? []) {
|
|
835
|
+
invalidate.run(now, invalidatedId);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
upsertDurableObject({
|
|
839
|
+
id: contextId,
|
|
840
|
+
kind: "context_packet",
|
|
841
|
+
label: input.subject,
|
|
842
|
+
source: "local",
|
|
843
|
+
status: input.status ?? "active",
|
|
844
|
+
metadata: {
|
|
845
|
+
contextType: input.contextType,
|
|
846
|
+
producerPeerId: input.producerPeerId,
|
|
847
|
+
tenantId,
|
|
848
|
+
workspaceId,
|
|
849
|
+
scope: input.scope ?? [],
|
|
850
|
+
summary: input.summary,
|
|
851
|
+
claims: input.claims ?? [],
|
|
852
|
+
evidenceRefs: input.evidenceRefs ?? [],
|
|
853
|
+
freshness: input.freshness ?? {},
|
|
854
|
+
permissions,
|
|
855
|
+
confidence: input.confidence ?? null,
|
|
856
|
+
lineage: input.lineage ?? {},
|
|
857
|
+
invalidates: input.invalidates ?? [],
|
|
858
|
+
nextActions: input.nextActions ?? [],
|
|
859
|
+
version: input.version ?? 1,
|
|
860
|
+
...(input.metadata ?? {}),
|
|
861
|
+
},
|
|
862
|
+
queueForSync: false,
|
|
863
|
+
});
|
|
864
|
+
linkDurableObjects({
|
|
865
|
+
fromId: input.producerPeerId,
|
|
866
|
+
toId: contextId,
|
|
867
|
+
edgeType: "produced_context",
|
|
868
|
+
metadata: { contextType: input.contextType },
|
|
869
|
+
queueForSync: false,
|
|
870
|
+
});
|
|
871
|
+
for (const invalidatedId of input.invalidates ?? []) {
|
|
872
|
+
linkDurableObjects({
|
|
873
|
+
fromId: contextId,
|
|
874
|
+
toId: invalidatedId,
|
|
875
|
+
edgeType: "invalidates",
|
|
876
|
+
queueForSync: false,
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
const queuedSyncId = input.queueForSync === false
|
|
880
|
+
? undefined
|
|
881
|
+
: enqueueSyncOperation({
|
|
882
|
+
objectId: contextId,
|
|
883
|
+
objectKind: "context_packet",
|
|
884
|
+
opType: "publish_context",
|
|
885
|
+
payload: {
|
|
886
|
+
contextId,
|
|
887
|
+
contextType: input.contextType,
|
|
888
|
+
producerPeerId: input.producerPeerId,
|
|
889
|
+
tenantId,
|
|
890
|
+
workspaceId,
|
|
891
|
+
scope: input.scope ?? [],
|
|
892
|
+
subject: input.subject,
|
|
893
|
+
summary: input.summary,
|
|
894
|
+
claims: input.claims ?? [],
|
|
895
|
+
evidenceRefs: input.evidenceRefs ?? [],
|
|
896
|
+
stateSnapshot: input.stateSnapshot ?? {},
|
|
897
|
+
timeWindow: input.timeWindow ?? {},
|
|
898
|
+
freshness: input.freshness ?? {},
|
|
899
|
+
permissions,
|
|
900
|
+
confidence: input.confidence ?? null,
|
|
901
|
+
lineage: input.lineage ?? {},
|
|
902
|
+
invalidates: input.invalidates ?? [],
|
|
903
|
+
nextActions: input.nextActions ?? [],
|
|
904
|
+
version: input.version ?? 1,
|
|
905
|
+
status: input.status ?? "active",
|
|
906
|
+
metadata: input.metadata ?? {},
|
|
907
|
+
},
|
|
908
|
+
}).queueId;
|
|
909
|
+
emitSharedContextEvent("packet_published", {
|
|
910
|
+
contextId,
|
|
911
|
+
producerPeerId: input.producerPeerId,
|
|
912
|
+
contextType: input.contextType,
|
|
913
|
+
workspaceId,
|
|
914
|
+
tenantId,
|
|
915
|
+
status: input.status ?? "active",
|
|
916
|
+
});
|
|
917
|
+
return { contextId, queuedSyncId };
|
|
918
|
+
}
|
|
919
|
+
export function pullSharedContextPackets(query = {}) {
|
|
920
|
+
const rows = getDb().prepare(`
|
|
921
|
+
SELECT *
|
|
922
|
+
FROM shared_context_packets
|
|
923
|
+
WHERE (? IS NULL OR context_type = ?)
|
|
924
|
+
AND (? IS NULL OR producer_peer_id = ?)
|
|
925
|
+
AND (? IS NULL OR tenant_id = ?)
|
|
926
|
+
AND (? IS NULL OR workspace_id = ?)
|
|
927
|
+
AND (? IS NULL OR status = ?)
|
|
928
|
+
AND (? IS NULL OR scope_json LIKE '%' || ? || '%')
|
|
929
|
+
AND (? IS NULL OR subject LIKE '%' || ? || '%')
|
|
930
|
+
ORDER BY created_at DESC
|
|
931
|
+
LIMIT ?
|
|
932
|
+
`).all(query.contextType ?? null, query.contextType ?? null, query.producerPeerId ?? null, query.producerPeerId ?? null, query.tenantId ?? null, query.tenantId ?? null, query.workspaceId ?? null, query.workspaceId ?? null, query.status ?? null, query.status ?? null, query.scopeIncludes ?? null, query.scopeIncludes ?? null, query.subjectIncludes ?? null, query.subjectIncludes ?? null, query.limit ?? 50);
|
|
933
|
+
const packets = rows.map(parseSharedContextPacketRow);
|
|
934
|
+
if (!query.requestingPeerId)
|
|
935
|
+
return packets;
|
|
936
|
+
const peer = requireSharedContextPeer(query.requestingPeerId);
|
|
937
|
+
return packets.filter((packet) => canPeerAccessPacket(peer, packet));
|
|
938
|
+
}
|
|
939
|
+
export function acknowledgeSharedContextPacket(contextId, peerId, detail) {
|
|
940
|
+
requirePacketForPeer(contextId, peerId, "ack_shared_context");
|
|
941
|
+
const ackId = genId("context_ack");
|
|
942
|
+
const now = new Date().toISOString();
|
|
943
|
+
getDb().prepare(`
|
|
944
|
+
INSERT INTO shared_context_packet_acks (id, context_id, peer_id, ack_status, detail_json, created_at)
|
|
945
|
+
VALUES (?, ?, ?, 'acknowledged', ?, ?)
|
|
946
|
+
ON CONFLICT(context_id, peer_id) DO UPDATE SET
|
|
947
|
+
ack_status = 'acknowledged',
|
|
948
|
+
detail_json = excluded.detail_json,
|
|
949
|
+
created_at = excluded.created_at
|
|
950
|
+
`).run(ackId, contextId, peerId, json(detail), now);
|
|
951
|
+
const queuedSyncId = enqueueSyncOperation({
|
|
952
|
+
objectId: contextId,
|
|
953
|
+
objectKind: "context_packet",
|
|
954
|
+
opType: "ack_context",
|
|
955
|
+
payload: {
|
|
956
|
+
ackId,
|
|
957
|
+
contextId,
|
|
958
|
+
peerId,
|
|
959
|
+
detail: detail ?? {},
|
|
960
|
+
acknowledgedAt: now,
|
|
961
|
+
},
|
|
962
|
+
}).queueId;
|
|
963
|
+
emitSharedContextEvent("packet_acknowledged", {
|
|
964
|
+
contextId,
|
|
965
|
+
peerId,
|
|
966
|
+
acknowledgedAt: now,
|
|
967
|
+
});
|
|
968
|
+
return { ackId, queuedSyncId };
|
|
969
|
+
}
|
|
970
|
+
export function invalidateSharedContextPacket(contextId, producerPeerId, reason, invalidates = []) {
|
|
971
|
+
const now = new Date().toISOString();
|
|
972
|
+
const packet = requirePacketForPeer(contextId, producerPeerId, "invalidate_shared_context");
|
|
973
|
+
if (packet.producerPeerId !== producerPeerId) {
|
|
974
|
+
throw new Error(`invalidate_shared_context denied: only the producer peer can invalidate packet ${contextId}`);
|
|
975
|
+
}
|
|
976
|
+
getDb().prepare(`
|
|
977
|
+
UPDATE shared_context_packets
|
|
978
|
+
SET status = 'invalidated',
|
|
979
|
+
invalidates_json = ?,
|
|
980
|
+
metadata_json = json_set(metadata_json, '$.invalidationReason', ?),
|
|
981
|
+
updated_at = ?
|
|
982
|
+
WHERE context_id = ? AND producer_peer_id = ?
|
|
983
|
+
`).run(json(invalidates, []), reason, now, contextId, producerPeerId);
|
|
984
|
+
upsertDurableObject({
|
|
985
|
+
id: contextId,
|
|
986
|
+
kind: "context_packet",
|
|
987
|
+
label: getDb().prepare("SELECT subject FROM shared_context_packets WHERE context_id = ?").get(contextId)?.subject ?? contextId,
|
|
988
|
+
source: "local",
|
|
989
|
+
status: "invalidated",
|
|
990
|
+
metadata: {
|
|
991
|
+
invalidationReason: reason,
|
|
992
|
+
invalidates,
|
|
993
|
+
},
|
|
994
|
+
queueForSync: false,
|
|
995
|
+
});
|
|
996
|
+
const queuedSyncId = enqueueSyncOperation({
|
|
997
|
+
objectId: contextId,
|
|
998
|
+
objectKind: "context_packet",
|
|
999
|
+
opType: "invalidate_context",
|
|
1000
|
+
payload: {
|
|
1001
|
+
contextId,
|
|
1002
|
+
producerPeerId,
|
|
1003
|
+
reason,
|
|
1004
|
+
invalidates,
|
|
1005
|
+
invalidatedAt: now,
|
|
1006
|
+
},
|
|
1007
|
+
}).queueId;
|
|
1008
|
+
emitSharedContextEvent("packet_invalidated", {
|
|
1009
|
+
contextId,
|
|
1010
|
+
producerPeerId,
|
|
1011
|
+
reason,
|
|
1012
|
+
invalidates,
|
|
1013
|
+
invalidatedAt: now,
|
|
1014
|
+
});
|
|
1015
|
+
return { contextId, queuedSyncId };
|
|
1016
|
+
}
|
|
1017
|
+
export function sendSharedContextMessage(input) {
|
|
1018
|
+
const fromPeer = requireSharedContextPeer(input.fromPeerId);
|
|
1019
|
+
const toPeer = requireSharedContextPeer(input.toPeerId);
|
|
1020
|
+
assertPeersShareScope(fromPeer, toPeer, "send_peer_message");
|
|
1021
|
+
const referencedContextId = typeof input.payload.contextId === "string" ? input.payload.contextId : null;
|
|
1022
|
+
if (referencedContextId) {
|
|
1023
|
+
requirePacketForPeer(referencedContextId, input.fromPeerId, "send_peer_message");
|
|
1024
|
+
requirePacketForPeer(referencedContextId, input.toPeerId, "send_peer_message");
|
|
1025
|
+
}
|
|
1026
|
+
const messageId = genId("message");
|
|
1027
|
+
const now = new Date().toISOString();
|
|
1028
|
+
getDb().prepare(`
|
|
1029
|
+
INSERT INTO shared_context_messages (id, from_peer_id, to_peer_id, message_class, payload_json, status, created_at)
|
|
1030
|
+
VALUES (?, ?, ?, ?, ?, 'unread', ?)
|
|
1031
|
+
`).run(messageId, input.fromPeerId, input.toPeerId, input.messageClass, json(input.payload), now);
|
|
1032
|
+
upsertDurableObject({
|
|
1033
|
+
id: messageId,
|
|
1034
|
+
kind: "message",
|
|
1035
|
+
label: `${input.messageClass}:${input.fromPeerId}->${input.toPeerId}`,
|
|
1036
|
+
source: "local",
|
|
1037
|
+
status: "unread",
|
|
1038
|
+
metadata: {
|
|
1039
|
+
fromPeerId: input.fromPeerId,
|
|
1040
|
+
toPeerId: input.toPeerId,
|
|
1041
|
+
messageClass: input.messageClass,
|
|
1042
|
+
payload: input.payload,
|
|
1043
|
+
},
|
|
1044
|
+
queueForSync: false,
|
|
1045
|
+
});
|
|
1046
|
+
linkDurableObjects({
|
|
1047
|
+
fromId: input.fromPeerId,
|
|
1048
|
+
toId: messageId,
|
|
1049
|
+
edgeType: "sent_message",
|
|
1050
|
+
queueForSync: false,
|
|
1051
|
+
});
|
|
1052
|
+
linkDurableObjects({
|
|
1053
|
+
fromId: messageId,
|
|
1054
|
+
toId: input.toPeerId,
|
|
1055
|
+
edgeType: "addressed_to",
|
|
1056
|
+
queueForSync: false,
|
|
1057
|
+
});
|
|
1058
|
+
const queuedSyncId = input.queueForSync === false
|
|
1059
|
+
? undefined
|
|
1060
|
+
: enqueueSyncOperation({
|
|
1061
|
+
objectId: messageId,
|
|
1062
|
+
objectKind: "message",
|
|
1063
|
+
opType: "send_peer_message",
|
|
1064
|
+
payload: {
|
|
1065
|
+
messageId,
|
|
1066
|
+
fromPeerId: input.fromPeerId,
|
|
1067
|
+
toPeerId: input.toPeerId,
|
|
1068
|
+
messageClass: input.messageClass,
|
|
1069
|
+
payload: input.payload,
|
|
1070
|
+
createdAt: now,
|
|
1071
|
+
},
|
|
1072
|
+
}).queueId;
|
|
1073
|
+
emitSharedContextEvent("message_sent", {
|
|
1074
|
+
messageId,
|
|
1075
|
+
fromPeerId: input.fromPeerId,
|
|
1076
|
+
toPeerId: input.toPeerId,
|
|
1077
|
+
messageClass: input.messageClass,
|
|
1078
|
+
createdAt: now,
|
|
1079
|
+
});
|
|
1080
|
+
return { messageId, queuedSyncId };
|
|
1081
|
+
}
|
|
1082
|
+
export function listSharedContextMessages(peerId, unreadOnly = false) {
|
|
1083
|
+
const rows = getDb().prepare(`
|
|
1084
|
+
SELECT id, from_peer_id, to_peer_id, message_class, payload_json, status, created_at
|
|
1085
|
+
FROM shared_context_messages
|
|
1086
|
+
WHERE to_peer_id = ?
|
|
1087
|
+
AND (? = 0 OR status = 'unread')
|
|
1088
|
+
ORDER BY created_at DESC
|
|
1089
|
+
`).all(peerId, unreadOnly ? 1 : 0);
|
|
1090
|
+
return rows.map((row) => ({
|
|
1091
|
+
messageId: row.id,
|
|
1092
|
+
fromPeerId: row.from_peer_id,
|
|
1093
|
+
toPeerId: row.to_peer_id,
|
|
1094
|
+
messageClass: row.message_class,
|
|
1095
|
+
payload: parseJson(row.payload_json, {}),
|
|
1096
|
+
status: row.status,
|
|
1097
|
+
createdAt: row.created_at,
|
|
1098
|
+
}));
|
|
1099
|
+
}
|
|
1100
|
+
export function proposeSharedContextTask(input) {
|
|
1101
|
+
const db = getDb();
|
|
1102
|
+
const now = new Date().toISOString();
|
|
1103
|
+
const taskId = input.taskId ?? genId("task");
|
|
1104
|
+
const proposerPeer = requireSharedContextPeer(input.proposerPeerId);
|
|
1105
|
+
const assigneePeer = requireSharedContextPeer(input.assigneePeerId);
|
|
1106
|
+
assertPeersShareScope(proposerPeer, assigneePeer, "propose_shared_task");
|
|
1107
|
+
for (const contextId of input.inputContextIds ?? []) {
|
|
1108
|
+
requirePacketForPeer(contextId, input.proposerPeerId, "propose_shared_task");
|
|
1109
|
+
requirePacketForPeer(contextId, input.assigneePeerId, "propose_shared_task");
|
|
1110
|
+
}
|
|
1111
|
+
db.prepare(`
|
|
1112
|
+
INSERT INTO shared_context_tasks (
|
|
1113
|
+
task_id, task_type, proposer_peer_id, assignee_peer_id, status,
|
|
1114
|
+
task_spec_json, input_context_ids_json, output_context_id, reason, metadata_json, created_at, updated_at
|
|
1115
|
+
)
|
|
1116
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1117
|
+
ON CONFLICT(task_id) DO UPDATE SET
|
|
1118
|
+
task_type = excluded.task_type,
|
|
1119
|
+
proposer_peer_id = excluded.proposer_peer_id,
|
|
1120
|
+
assignee_peer_id = excluded.assignee_peer_id,
|
|
1121
|
+
status = excluded.status,
|
|
1122
|
+
task_spec_json = excluded.task_spec_json,
|
|
1123
|
+
input_context_ids_json = excluded.input_context_ids_json,
|
|
1124
|
+
output_context_id = excluded.output_context_id,
|
|
1125
|
+
reason = excluded.reason,
|
|
1126
|
+
metadata_json = excluded.metadata_json,
|
|
1127
|
+
updated_at = excluded.updated_at
|
|
1128
|
+
`).run(taskId, input.taskType, input.proposerPeerId, input.assigneePeerId, input.status ?? "proposed", json(input.taskSpec ?? {}, {}), json(input.inputContextIds ?? [], []), input.outputContextId ?? null, input.reason ?? null, json(input.metadata), now, now);
|
|
1129
|
+
upsertDurableObject({
|
|
1130
|
+
id: taskId,
|
|
1131
|
+
kind: "task",
|
|
1132
|
+
label: input.taskType,
|
|
1133
|
+
source: "local",
|
|
1134
|
+
status: input.status ?? "proposed",
|
|
1135
|
+
metadata: {
|
|
1136
|
+
proposerPeerId: input.proposerPeerId,
|
|
1137
|
+
assigneePeerId: input.assigneePeerId,
|
|
1138
|
+
taskSpec: input.taskSpec ?? {},
|
|
1139
|
+
inputContextIds: input.inputContextIds ?? [],
|
|
1140
|
+
outputContextId: input.outputContextId ?? null,
|
|
1141
|
+
reason: input.reason ?? null,
|
|
1142
|
+
...(input.metadata ?? {}),
|
|
1143
|
+
},
|
|
1144
|
+
queueForSync: false,
|
|
1145
|
+
});
|
|
1146
|
+
linkDurableObjects({
|
|
1147
|
+
fromId: input.proposerPeerId,
|
|
1148
|
+
toId: taskId,
|
|
1149
|
+
edgeType: "proposed_task",
|
|
1150
|
+
queueForSync: false,
|
|
1151
|
+
});
|
|
1152
|
+
linkDurableObjects({
|
|
1153
|
+
fromId: taskId,
|
|
1154
|
+
toId: input.assigneePeerId,
|
|
1155
|
+
edgeType: "assigned_to",
|
|
1156
|
+
queueForSync: false,
|
|
1157
|
+
});
|
|
1158
|
+
for (const contextId of input.inputContextIds ?? []) {
|
|
1159
|
+
linkDurableObjects({
|
|
1160
|
+
fromId: taskId,
|
|
1161
|
+
toId: contextId,
|
|
1162
|
+
edgeType: "input_context",
|
|
1163
|
+
queueForSync: false,
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
const queuedSyncId = input.queueForSync === false
|
|
1167
|
+
? undefined
|
|
1168
|
+
: enqueueSyncOperation({
|
|
1169
|
+
objectId: taskId,
|
|
1170
|
+
objectKind: "task",
|
|
1171
|
+
opType: "task_handoff",
|
|
1172
|
+
payload: {
|
|
1173
|
+
taskId,
|
|
1174
|
+
taskType: input.taskType,
|
|
1175
|
+
proposerPeerId: input.proposerPeerId,
|
|
1176
|
+
assigneePeerId: input.assigneePeerId,
|
|
1177
|
+
status: input.status ?? "proposed",
|
|
1178
|
+
taskSpec: input.taskSpec ?? {},
|
|
1179
|
+
inputContextIds: input.inputContextIds ?? [],
|
|
1180
|
+
outputContextId: input.outputContextId ?? null,
|
|
1181
|
+
reason: input.reason ?? null,
|
|
1182
|
+
metadata: input.metadata ?? {},
|
|
1183
|
+
createdAt: now,
|
|
1184
|
+
},
|
|
1185
|
+
}).queueId;
|
|
1186
|
+
emitSharedContextEvent("task_proposed", {
|
|
1187
|
+
taskId,
|
|
1188
|
+
taskType: input.taskType,
|
|
1189
|
+
proposerPeerId: input.proposerPeerId,
|
|
1190
|
+
assigneePeerId: input.assigneePeerId,
|
|
1191
|
+
status: input.status ?? "proposed",
|
|
1192
|
+
createdAt: now,
|
|
1193
|
+
});
|
|
1194
|
+
return { taskId, queuedSyncId };
|
|
1195
|
+
}
|
|
1196
|
+
function updateSharedContextTaskStatus(taskId, expectedPeerId, status, reason, outputContextId) {
|
|
1197
|
+
const now = new Date().toISOString();
|
|
1198
|
+
getDb().prepare(`
|
|
1199
|
+
UPDATE shared_context_tasks
|
|
1200
|
+
SET status = ?,
|
|
1201
|
+
reason = COALESCE(?, reason),
|
|
1202
|
+
output_context_id = COALESCE(?, output_context_id),
|
|
1203
|
+
updated_at = ?
|
|
1204
|
+
WHERE task_id = ?
|
|
1205
|
+
AND assignee_peer_id = ?
|
|
1206
|
+
`).run(status, reason ?? null, outputContextId ?? null, now, taskId, expectedPeerId);
|
|
1207
|
+
const row = getDb().prepare(`
|
|
1208
|
+
SELECT *
|
|
1209
|
+
FROM shared_context_tasks
|
|
1210
|
+
WHERE task_id = ?
|
|
1211
|
+
LIMIT 1
|
|
1212
|
+
`).get(taskId);
|
|
1213
|
+
if (!row) {
|
|
1214
|
+
throw new Error(`Shared context task not found: ${taskId}`);
|
|
1215
|
+
}
|
|
1216
|
+
const task = parseSharedContextTaskRow(row);
|
|
1217
|
+
const assigneePeer = requireSharedContextPeer(task.assigneePeerId);
|
|
1218
|
+
const proposerPeer = requireSharedContextPeer(task.proposerPeerId);
|
|
1219
|
+
assertPeersShareScope(proposerPeer, assigneePeer, "update_shared_task");
|
|
1220
|
+
if (outputContextId) {
|
|
1221
|
+
requirePacketForPeer(outputContextId, expectedPeerId, "update_shared_task");
|
|
1222
|
+
}
|
|
1223
|
+
upsertDurableObject({
|
|
1224
|
+
id: taskId,
|
|
1225
|
+
kind: "task",
|
|
1226
|
+
label: task.taskType,
|
|
1227
|
+
source: "local",
|
|
1228
|
+
status,
|
|
1229
|
+
metadata: {
|
|
1230
|
+
proposerPeerId: task.proposerPeerId,
|
|
1231
|
+
assigneePeerId: task.assigneePeerId,
|
|
1232
|
+
taskSpec: task.taskSpec,
|
|
1233
|
+
inputContextIds: task.inputContextIds,
|
|
1234
|
+
outputContextId: task.outputContextId ?? null,
|
|
1235
|
+
reason: task.reason ?? null,
|
|
1236
|
+
},
|
|
1237
|
+
queueForSync: false,
|
|
1238
|
+
});
|
|
1239
|
+
emitSharedContextEvent("task_status_changed", {
|
|
1240
|
+
taskId,
|
|
1241
|
+
peerId: expectedPeerId,
|
|
1242
|
+
status,
|
|
1243
|
+
outputContextId: outputContextId ?? null,
|
|
1244
|
+
updatedAt: task.updatedAt,
|
|
1245
|
+
});
|
|
1246
|
+
return task;
|
|
1247
|
+
}
|
|
1248
|
+
export function acceptSharedContextTask(taskId, peerId) {
|
|
1249
|
+
const task = updateSharedContextTaskStatus(taskId, peerId, "accepted");
|
|
1250
|
+
const queuedSyncId = enqueueSyncOperation({
|
|
1251
|
+
objectId: taskId,
|
|
1252
|
+
objectKind: "task",
|
|
1253
|
+
opType: "task_handoff",
|
|
1254
|
+
payload: {
|
|
1255
|
+
taskId,
|
|
1256
|
+
peerId,
|
|
1257
|
+
status: "accepted",
|
|
1258
|
+
updatedAt: task.updatedAt,
|
|
1259
|
+
},
|
|
1260
|
+
}).queueId;
|
|
1261
|
+
return { task, queuedSyncId };
|
|
1262
|
+
}
|
|
1263
|
+
export function rejectSharedContextTask(taskId, peerId, reason) {
|
|
1264
|
+
const task = updateSharedContextTaskStatus(taskId, peerId, "rejected", reason);
|
|
1265
|
+
const queuedSyncId = enqueueSyncOperation({
|
|
1266
|
+
objectId: taskId,
|
|
1267
|
+
objectKind: "task",
|
|
1268
|
+
opType: "task_handoff",
|
|
1269
|
+
payload: {
|
|
1270
|
+
taskId,
|
|
1271
|
+
peerId,
|
|
1272
|
+
status: "rejected",
|
|
1273
|
+
reason,
|
|
1274
|
+
updatedAt: task.updatedAt,
|
|
1275
|
+
},
|
|
1276
|
+
}).queueId;
|
|
1277
|
+
return { task, queuedSyncId };
|
|
1278
|
+
}
|
|
1279
|
+
export function completeSharedContextTask(taskId, peerId, outputContextId) {
|
|
1280
|
+
const task = updateSharedContextTaskStatus(taskId, peerId, "completed", null, outputContextId ?? null);
|
|
1281
|
+
if (outputContextId) {
|
|
1282
|
+
linkDurableObjects({
|
|
1283
|
+
fromId: taskId,
|
|
1284
|
+
toId: outputContextId,
|
|
1285
|
+
edgeType: "completed_with",
|
|
1286
|
+
queueForSync: false,
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1289
|
+
const queuedSyncId = enqueueSyncOperation({
|
|
1290
|
+
objectId: taskId,
|
|
1291
|
+
objectKind: "task",
|
|
1292
|
+
opType: "task_handoff",
|
|
1293
|
+
payload: {
|
|
1294
|
+
taskId,
|
|
1295
|
+
peerId,
|
|
1296
|
+
status: "completed",
|
|
1297
|
+
outputContextId: outputContextId ?? null,
|
|
1298
|
+
updatedAt: task.updatedAt,
|
|
1299
|
+
},
|
|
1300
|
+
}).queueId;
|
|
1301
|
+
return { task, queuedSyncId };
|
|
1302
|
+
}
|
|
1303
|
+
export function escalateSharedContextTask(taskId, peerId, reason, outputContextId) {
|
|
1304
|
+
const task = updateSharedContextTaskStatus(taskId, peerId, "escalated", reason, outputContextId ?? null);
|
|
1305
|
+
const queuedSyncId = enqueueSyncOperation({
|
|
1306
|
+
objectId: taskId,
|
|
1307
|
+
objectKind: "task",
|
|
1308
|
+
opType: "task_handoff",
|
|
1309
|
+
payload: {
|
|
1310
|
+
taskId,
|
|
1311
|
+
peerId,
|
|
1312
|
+
status: "escalated",
|
|
1313
|
+
reason,
|
|
1314
|
+
outputContextId: outputContextId ?? null,
|
|
1315
|
+
updatedAt: task.updatedAt,
|
|
1316
|
+
},
|
|
1317
|
+
}).queueId;
|
|
1318
|
+
return { task, queuedSyncId };
|
|
1319
|
+
}
|
|
1320
|
+
export function getSharedContextSnapshot(limit = 10, requestingPeerId) {
|
|
1321
|
+
const db = getDb();
|
|
1322
|
+
const counts = db.prepare(`
|
|
1323
|
+
SELECT
|
|
1324
|
+
(SELECT COUNT(*) FROM shared_context_peers WHERE status = 'active') AS active_peers,
|
|
1325
|
+
(SELECT COUNT(*) FROM shared_context_packets WHERE status = 'active') AS active_packets,
|
|
1326
|
+
(SELECT COUNT(*) FROM shared_context_packets WHERE status = 'invalidated') AS invalidated_packets,
|
|
1327
|
+
(SELECT COUNT(*) FROM shared_context_tasks WHERE status IN ('proposed', 'accepted')) AS open_tasks,
|
|
1328
|
+
(SELECT COUNT(*) FROM shared_context_messages WHERE status = 'unread') AS unread_messages
|
|
1329
|
+
`).get();
|
|
1330
|
+
const recentMessages = db.prepare(`
|
|
1331
|
+
SELECT id, from_peer_id, to_peer_id, message_class, payload_json, status, created_at
|
|
1332
|
+
FROM shared_context_messages
|
|
1333
|
+
ORDER BY created_at DESC
|
|
1334
|
+
LIMIT ?
|
|
1335
|
+
`).all(limit).map((row) => ({
|
|
1336
|
+
messageId: row.id,
|
|
1337
|
+
fromPeerId: row.from_peer_id,
|
|
1338
|
+
toPeerId: row.to_peer_id,
|
|
1339
|
+
messageClass: row.message_class,
|
|
1340
|
+
payload: parseJson(row.payload_json, {}),
|
|
1341
|
+
status: row.status,
|
|
1342
|
+
createdAt: row.created_at,
|
|
1343
|
+
}));
|
|
1344
|
+
const allPeers = listSharedContextPeers({ limit });
|
|
1345
|
+
const allPackets = pullSharedContextPackets({ limit });
|
|
1346
|
+
const allTasks = db.prepare(`
|
|
1347
|
+
SELECT *
|
|
1348
|
+
FROM shared_context_tasks
|
|
1349
|
+
ORDER BY updated_at DESC
|
|
1350
|
+
LIMIT ?
|
|
1351
|
+
`).all(limit).map(parseSharedContextTaskRow);
|
|
1352
|
+
if (!requestingPeerId) {
|
|
1353
|
+
return {
|
|
1354
|
+
peers: allPeers,
|
|
1355
|
+
recentPackets: allPackets,
|
|
1356
|
+
recentTasks: allTasks,
|
|
1357
|
+
recentMessages,
|
|
1358
|
+
counts: {
|
|
1359
|
+
activePeers: counts?.active_peers ?? 0,
|
|
1360
|
+
activePackets: counts?.active_packets ?? 0,
|
|
1361
|
+
invalidatedPackets: counts?.invalidated_packets ?? 0,
|
|
1362
|
+
openTasks: counts?.open_tasks ?? 0,
|
|
1363
|
+
unreadMessages: counts?.unread_messages ?? 0,
|
|
1364
|
+
},
|
|
1365
|
+
};
|
|
1366
|
+
}
|
|
1367
|
+
const requestingPeer = requireSharedContextPeer(requestingPeerId);
|
|
1368
|
+
const filteredPeers = allPeers.filter((peer) => {
|
|
1369
|
+
try {
|
|
1370
|
+
assertPeersShareScope(requestingPeer, peer, "get_shared_context_snapshot");
|
|
1371
|
+
return true;
|
|
1372
|
+
}
|
|
1373
|
+
catch {
|
|
1374
|
+
return peer.peerId === requestingPeer.peerId;
|
|
1375
|
+
}
|
|
1376
|
+
});
|
|
1377
|
+
const filteredPackets = allPackets.filter((packet) => canPeerAccessPacket(requestingPeer, packet));
|
|
1378
|
+
const filteredTasks = allTasks.filter((task) => {
|
|
1379
|
+
if (task.proposerPeerId === requestingPeer.peerId || task.assigneePeerId === requestingPeer.peerId)
|
|
1380
|
+
return true;
|
|
1381
|
+
const relatedOutput = task.outputContextId ? filteredPackets.some((packet) => packet.contextId === task.outputContextId) : false;
|
|
1382
|
+
return relatedOutput;
|
|
1383
|
+
});
|
|
1384
|
+
const filteredMessages = recentMessages.filter((message) => message.toPeerId === requestingPeer.peerId || message.fromPeerId === requestingPeer.peerId);
|
|
1385
|
+
return {
|
|
1386
|
+
peers: filteredPeers,
|
|
1387
|
+
recentPackets: filteredPackets,
|
|
1388
|
+
recentTasks: filteredTasks,
|
|
1389
|
+
recentMessages: filteredMessages,
|
|
1390
|
+
counts: {
|
|
1391
|
+
activePeers: filteredPeers.filter((peer) => peer.status === "active").length,
|
|
1392
|
+
activePackets: filteredPackets.filter((packet) => packet.status === "active").length,
|
|
1393
|
+
invalidatedPackets: filteredPackets.filter((packet) => packet.status === "invalidated").length,
|
|
1394
|
+
openTasks: filteredTasks.filter((task) => task.status === "proposed" || task.status === "accepted").length,
|
|
1395
|
+
unreadMessages: filteredMessages.filter((message) => message.status === "unread").length,
|
|
1396
|
+
},
|
|
1397
|
+
};
|
|
1398
|
+
}
|
|
1399
|
+
export function getSharedContextScopedSnapshot(filters = {}) {
|
|
1400
|
+
const snapshot = getSharedContextSnapshot(filters.limit ?? 10, filters.peerId ?? filters.requestingPeerId);
|
|
1401
|
+
const peers = filters.workspaceId
|
|
1402
|
+
? snapshot.peers.filter((peer) => peer.workspaceId === filters.workspaceId)
|
|
1403
|
+
: snapshot.peers;
|
|
1404
|
+
const recentPackets = snapshot.recentPackets.filter((packet) => {
|
|
1405
|
+
if (filters.workspaceId && packet.workspaceId !== filters.workspaceId)
|
|
1406
|
+
return false;
|
|
1407
|
+
if (filters.contextType && packet.contextType !== filters.contextType)
|
|
1408
|
+
return false;
|
|
1409
|
+
if (filters.producerPeerId && packet.producerPeerId !== filters.producerPeerId)
|
|
1410
|
+
return false;
|
|
1411
|
+
if (filters.scopeIncludes && !packet.scope.includes(filters.scopeIncludes))
|
|
1412
|
+
return false;
|
|
1413
|
+
if (filters.subjectIncludes &&
|
|
1414
|
+
!packet.subject.toLowerCase().includes(filters.subjectIncludes.toLowerCase())) {
|
|
1415
|
+
return false;
|
|
1416
|
+
}
|
|
1417
|
+
return true;
|
|
1418
|
+
});
|
|
1419
|
+
const recentTasks = snapshot.recentTasks.filter((task) => taskMatchesSubscription(task, recentPackets, filters));
|
|
1420
|
+
const recentMessages = snapshot.recentMessages.filter((message) => messageMatchesSubscription(message, peers, filters));
|
|
1421
|
+
return {
|
|
1422
|
+
peers,
|
|
1423
|
+
recentPackets,
|
|
1424
|
+
recentTasks,
|
|
1425
|
+
recentMessages,
|
|
1426
|
+
counts: {
|
|
1427
|
+
activePeers: peers.filter((peer) => peer.status === "active").length,
|
|
1428
|
+
activePackets: recentPackets.filter((packet) => packet.status === "active").length,
|
|
1429
|
+
invalidatedPackets: recentPackets.filter((packet) => packet.status === "invalidated").length,
|
|
1430
|
+
openTasks: recentTasks.filter((task) => task.status === "proposed" || task.status === "accepted").length,
|
|
1431
|
+
unreadMessages: recentMessages.filter((message) => message.status === "unread").length,
|
|
1432
|
+
},
|
|
1433
|
+
};
|
|
1434
|
+
}
|
|
1435
|
+
//# sourceMappingURL=store.js.map
|