@unson/brainbase-mcp 0.43.0 → 0.44.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/contracts/canonical-task/operation-schema.sql +58 -0
  2. package/contracts/canonical-task/search-indexes.sql +10 -0
  3. package/contracts/canonical-task/store-schema.sql +77 -0
  4. package/dist/automation-run.d.ts +213 -0
  5. package/dist/automation-run.js +479 -0
  6. package/dist/canonical-task-operations.d.ts +205 -0
  7. package/dist/canonical-task-operations.js +700 -0
  8. package/dist/canonical-task-postgres.d.ts +139 -0
  9. package/dist/canonical-task-postgres.js +554 -0
  10. package/dist/canonical-task-service.d.ts +13 -0
  11. package/dist/canonical-task-service.js +55 -3
  12. package/dist/canonical-task-store-config.d.ts +31 -0
  13. package/dist/canonical-task-store-config.js +53 -0
  14. package/dist/config-parser.d.ts +298 -0
  15. package/dist/config-parser.js +593 -0
  16. package/dist/connected-onboarding.d.ts +57 -0
  17. package/dist/connected-onboarding.js +37 -0
  18. package/dist/durable-wait-http.d.ts +3 -1
  19. package/dist/durable-wait-http.js +73 -6
  20. package/dist/durable-waits.d.ts +93 -3
  21. package/dist/durable-waits.js +320 -8
  22. package/dist/honcho.d.ts +25 -0
  23. package/dist/honcho.js +81 -0
  24. package/dist/judgment-event-adapter.d.ts +72 -0
  25. package/dist/judgment-event-adapter.js +114 -0
  26. package/dist/judgment-event-outbox.d.ts +182 -0
  27. package/dist/judgment-event-outbox.js +568 -0
  28. package/dist/judgment-execution-outcome.d.ts +46 -0
  29. package/dist/judgment-execution-outcome.js +149 -0
  30. package/dist/judgment-host.d.ts +18 -7
  31. package/dist/judgment-host.js +82 -34
  32. package/dist/judgment-lifecycle-core.d.ts +198 -0
  33. package/dist/judgment-lifecycle-core.js +464 -0
  34. package/dist/judgment-node-evidence.d.ts +57 -0
  35. package/dist/judgment-node-evidence.js +297 -0
  36. package/dist/judgment-runtime-resolution.d.ts +264 -0
  37. package/dist/judgment-runtime-resolution.js +1248 -0
  38. package/dist/judgment-runtime-resolution.public.d.ts +255 -0
  39. package/dist/knowledge-event.d.ts +28 -0
  40. package/dist/knowledge-event.js +114 -0
  41. package/dist/mcp-runtime.d.ts +75 -0
  42. package/dist/mcp-runtime.js +188 -0
  43. package/dist/meeting-automation-runtime.d.ts +102 -0
  44. package/dist/meeting-automation-runtime.js +249 -0
  45. package/dist/meeting-automation.d.ts +155 -0
  46. package/dist/meeting-automation.js +341 -0
  47. package/dist/meeting-minutes-context-receipt.d.ts +91 -0
  48. package/dist/meeting-minutes-context-receipt.js +280 -0
  49. package/dist/meeting-source-sync.d.ts +89 -0
  50. package/dist/meeting-source-sync.js +230 -0
  51. package/dist/mesh.d.ts +58 -0
  52. package/dist/mesh.js +134 -0
  53. package/dist/onboarding-runtime.d.ts +47 -0
  54. package/dist/onboarding-runtime.js +4719 -0
  55. package/dist/routine-cycle.d.ts +51 -0
  56. package/dist/routine-cycle.js +153 -0
  57. package/dist/routine-liveness.d.ts +49 -0
  58. package/dist/routine-liveness.js +227 -0
  59. package/dist/routine-runtime.d.ts +101 -0
  60. package/dist/routine-runtime.js +237 -0
  61. package/dist/routines.d.ts +3 -0
  62. package/dist/routines.js +3 -0
  63. package/dist/run-receipt.d.ts +40 -0
  64. package/dist/run-receipt.js +635 -0
  65. package/dist/runtime-host.d.ts +64 -0
  66. package/dist/runtime-host.js +70 -0
  67. package/dist/world-work.d.ts +16 -0
  68. package/package.json +115 -3
  69. package/ui/meeting-minutes.css +122 -0
  70. package/ui/meeting-minutes.js +316 -0
  71. package/ui/objective-editor.js +47 -11
  72. package/ui/world/world-district.js +18 -3
  73. package/ui/world/world-placement.js +13 -0
  74. package/ui/world/world-work-rail.js +7 -1
@@ -0,0 +1,280 @@
1
+ import { createHash } from 'node:crypto';
2
+ // Keep the persisted schema identifier byte-for-byte compatible with the
3
+ // existing Unson service. The package export has its own version constant.
4
+ export const MEETING_MINUTES_CONTEXT_RECEIPT_VERSION = 'meeting_minutes_context_receipt.v1';
5
+ export const MEETING_MINUTES_CONTEXT_RECEIPT_MAX_BYTES = 128 * 1024;
6
+ export class MeetingMinutesContextReceiptError extends Error {
7
+ code;
8
+ statusCode;
9
+ details;
10
+ constructor(code, message, statusCode = 400, details = {}) {
11
+ super(message);
12
+ this.name = 'MeetingMinutesContextReceiptError';
13
+ this.code = code;
14
+ this.statusCode = statusCode;
15
+ this.details = details;
16
+ }
17
+ }
18
+ function stable(value) {
19
+ if (Array.isArray(value))
20
+ return value.map(stable);
21
+ if (!value || typeof value !== 'object')
22
+ return value;
23
+ return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stable(value[key])]));
24
+ }
25
+ export function digestMeetingMinutesContext(value) {
26
+ return createHash('sha256').update(JSON.stringify(stable(value))).digest('hex');
27
+ }
28
+ function compact(value, depth = 0) {
29
+ if (value === null || value === undefined)
30
+ return null;
31
+ if (typeof value === 'string')
32
+ return value.slice(0, 4000);
33
+ if (typeof value === 'number' || typeof value === 'boolean')
34
+ return value;
35
+ if (depth >= 4)
36
+ return String(value).slice(0, 500);
37
+ if (Array.isArray(value))
38
+ return value.slice(0, 50).map((item) => compact(item, depth + 1));
39
+ if (typeof value === 'object')
40
+ return Object.fromEntries(Object.entries(value).slice(0, 40).map(([key, item]) => [key, compact(item, depth + 1)]));
41
+ return String(value).slice(0, 500);
42
+ }
43
+ export function normalizeMeetingMinutesContextIdentity(input = {}) {
44
+ const identity = {
45
+ run_id: typeof input.run_id === 'string' ? input.run_id.trim() : '',
46
+ project_code: typeof input.project_code === 'string' ? input.project_code.trim() : '',
47
+ transcript_sha256: typeof input.transcript_sha256 === 'string' ? input.transcript_sha256.trim().toLowerCase() : '',
48
+ };
49
+ if (!/^[a-zA-Z0-9._:-]{1,200}$/.test(identity.run_id))
50
+ throw new MeetingMinutesContextReceiptError('meeting_minutes_context_input_invalid', 'run_id is invalid');
51
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,99}$/.test(identity.project_code))
52
+ throw new MeetingMinutesContextReceiptError('meeting_minutes_context_input_invalid', 'project_code is invalid');
53
+ if (!/^[a-f0-9]{64}$/.test(identity.transcript_sha256))
54
+ throw new MeetingMinutesContextReceiptError('meeting_minutes_context_input_invalid', 'transcript_sha256 must be a lowercase SHA-256 digest');
55
+ return identity;
56
+ }
57
+ export function normalizeMeetingMinutesRetrievalContext(input = {}) {
58
+ const retrieval = input.retrieval_context ?? input.retrievalContext ?? {};
59
+ const raw = retrieval && typeof retrieval === 'object'
60
+ ? retrieval.mention_candidates ?? retrieval.mentionCandidates ?? retrieval.mentions
61
+ : undefined;
62
+ const mentions = (Array.isArray(raw)
63
+ ? raw.map((candidate) => {
64
+ if (typeof candidate === 'string')
65
+ return { surface_form: candidate.trim().slice(0, 160), entity_types: ['person', 'org', 'brand', 'glossary_term'] };
66
+ if (!candidate || typeof candidate !== 'object')
67
+ return null;
68
+ const item = candidate;
69
+ const surface = String(item.surface_form ?? item.surfaceForm ?? item.text ?? item.term ?? item.value ?? '').trim().slice(0, 160);
70
+ if (!surface)
71
+ return null;
72
+ const rawTypes = item.entity_types ?? item.entityTypes ?? item.types;
73
+ const types = Array.isArray(rawTypes) ? rawTypes.map((type) => String(type).trim().toLowerCase()).filter(Boolean) : ['person', 'org', 'brand', 'glossary_term'];
74
+ return { surface_form: surface, entity_types: [...new Set(types)].slice(0, 4) };
75
+ }).filter((candidate) => Boolean(candidate && typeof candidate === 'object' && 'surface_form' in candidate && candidate.surface_form))
76
+ : []);
77
+ const seen = new Set();
78
+ return {
79
+ mention_candidates: mentions.filter((mention) => {
80
+ const key = `${String(mention.surface_form).toLocaleLowerCase('ja-JP')}|${mention.entity_types.join(',')}`;
81
+ if (seen.has(key))
82
+ return false;
83
+ seen.add(key);
84
+ return true;
85
+ }).slice(0, 12),
86
+ };
87
+ }
88
+ function flattenEntities(graph) {
89
+ if (!graph)
90
+ return [];
91
+ const entities = graph.entities;
92
+ if (Array.isArray(entities))
93
+ return entities.map((item) => compact(item));
94
+ if (!entities || typeof entities !== 'object')
95
+ return [];
96
+ return Object.values(entities).flatMap((items) => Array.isArray(items) ? items.map((item) => compact(item)) : []);
97
+ }
98
+ function normalizeTasks(tasks) {
99
+ const taskRecord = tasks && !Array.isArray(tasks) ? tasks : null;
100
+ const items = Array.isArray(tasks)
101
+ ? tasks
102
+ : (Array.isArray(taskRecord?.items) ? taskRecord.items : []);
103
+ return items.filter((task) => task.status !== 'completed').slice(0, 50).map((task) => compact(task));
104
+ }
105
+ function sourceRefs(entities, tasks, minutes) {
106
+ return [
107
+ ...entities.map((entity) => ({ type: 'graph_entity', id: entity.id ?? entity.entity_id ?? null })),
108
+ ...tasks.map((task) => ({ type: 'canonical_task', id: task.id ?? task.task_id ?? null })),
109
+ ...minutes.map((minute) => ({ type: 'meeting_minutes', id: minute.id ?? minute.entity_id ?? null })),
110
+ ].filter((item) => Boolean(item.id));
111
+ }
112
+ function approvedMinutes(entities) {
113
+ return entities
114
+ .filter((entity) => entity.entity_type === 'meeting_minutes' && Boolean(entity.id ?? entity.entity_id))
115
+ .slice(0, 3)
116
+ .map((entity) => ({ id: entity.id ?? entity.entity_id, title: entity.title ?? entity.name ?? null }));
117
+ }
118
+ function boundedReceipt(receipt) {
119
+ const candidate = structuredClone(receipt);
120
+ const context = candidate.context;
121
+ while (Buffer.byteLength(JSON.stringify(candidate), 'utf8') > MEETING_MINUTES_CONTEXT_RECEIPT_MAX_BYTES) {
122
+ const entities = Array.isArray(context.entities) ? context.entities : [];
123
+ const tasks = Array.isArray(context.open_tasks) ? context.open_tasks : [];
124
+ const refs = Array.isArray(context.source_refs) ? context.source_refs : [];
125
+ if (entities.length)
126
+ entities.pop();
127
+ else if (tasks.length)
128
+ tasks.pop();
129
+ else if (refs.length)
130
+ refs.pop();
131
+ else
132
+ throw new MeetingMinutesContextReceiptError('meeting_minutes_context_too_large', 'meeting minutes context receipt exceeds 128 KiB', 503);
133
+ }
134
+ return candidate;
135
+ }
136
+ async function persistWithRetry(receipt, ports) {
137
+ const maxAttempts = Math.max(1, Math.min(5, ports.max_attempts ?? 2));
138
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
139
+ try {
140
+ const saved = await ports.repository.put(receipt, ports.scope);
141
+ return saved ?? receipt;
142
+ }
143
+ catch (error) {
144
+ if (!(ports.is_retryable?.(error) ?? Boolean(error?.retryable)) || attempt >= maxAttempts)
145
+ throw error;
146
+ }
147
+ }
148
+ throw new MeetingMinutesContextReceiptError('persist_failed', 'meeting minutes context receipt could not be persisted', 503);
149
+ }
150
+ export async function createMeetingMinutesContextReceipt(input, ports) {
151
+ const identity = normalizeMeetingMinutesContextIdentity(input);
152
+ const retrievalContext = normalizeMeetingMinutesRetrievalContext(input);
153
+ if (!ports?.actor?.resolve || !ports?.auth?.authorize || !ports?.tenant?.authorize || !ports?.project?.authorize) {
154
+ throw new MeetingMinutesContextReceiptError('meeting_minutes_context_invalid_ports', 'actor, auth, tenant, and project ports are required', 500);
155
+ }
156
+ if (!ports?.repository?.claimOrReplay || !ports?.repository?.get || !ports?.repository?.put) {
157
+ throw new MeetingMinutesContextReceiptError('meeting_minutes_context_invalid_ports', 'repository claimOrReplay, put, and durable get ports are required', 500);
158
+ }
159
+ const actor = await ports.actor.resolve({ identity, request: input });
160
+ await ports.auth.authorize({ actor, identity });
161
+ await ports.tenant.authorize({ actor, identity });
162
+ await ports.project.authorize({ actor, identity });
163
+ const errors = [];
164
+ const scope = ports.scope ?? null;
165
+ const receiptId = `mmctx_${digestMeetingMinutesContext({ identity, scope }).slice(0, 32)}`;
166
+ const fingerprint = digestMeetingMinutesContext({ identity, scope });
167
+ const claim = await ports.repository.claimOrReplay({
168
+ receipt_id: receiptId,
169
+ fingerprint,
170
+ scope,
171
+ execute: async () => {
172
+ let graph = null;
173
+ let graphRetrieval = null;
174
+ let graphStatus = 'unavailable';
175
+ let graphFetched = false;
176
+ let tasks = null;
177
+ let tasksFetched = false;
178
+ try {
179
+ graphFetched = typeof ports.graph === 'function';
180
+ const graphResult = await ports.graph?.({ identity, retrieval_context: retrievalContext }) ?? null;
181
+ if (graphResult && typeof graphResult === 'object' && 'graph' in graphResult) {
182
+ graph = (graphResult.graph && typeof graphResult.graph === 'object' ? graphResult.graph : null);
183
+ graphRetrieval = (graphResult.retrieval && typeof graphResult.retrieval === 'object' ? graphResult.retrieval : null);
184
+ graphStatus = typeof graphResult.status === 'string' ? graphResult.status : (graph ? 'resolved' : 'unavailable');
185
+ if (Array.isArray(graphResult.errors))
186
+ errors.push(...graphResult.errors.filter((item) => Boolean(item && typeof item === 'object')));
187
+ }
188
+ else {
189
+ graph = graphResult;
190
+ graphStatus = graph ? 'resolved' : 'unavailable';
191
+ }
192
+ }
193
+ catch (error) {
194
+ errors.push({ source: 'graph', code: 'graph_context_unavailable', message: error instanceof Error ? error.message : String(error) });
195
+ }
196
+ try {
197
+ tasksFetched = typeof ports.tasks === 'function';
198
+ tasks = await ports.tasks?.({ identity }) ?? null;
199
+ }
200
+ catch (error) {
201
+ errors.push({ source: 'tasks', code: 'canonical_tasks_unavailable', message: error instanceof Error ? error.message : String(error) });
202
+ }
203
+ const entities = flattenEntities(graph);
204
+ const openTasks = normalizeTasks(tasks);
205
+ const minutes = approvedMinutes(entities);
206
+ const context = {
207
+ project: entities.filter((entity) => entity.entity_type === 'project'),
208
+ people: entities.filter((entity) => entity.entity_type === 'person'),
209
+ organizations: entities.filter((entity) => entity.entity_type === 'org'),
210
+ decisions: entities.filter((entity) => entity.entity_type === 'decision'),
211
+ glossary: entities.filter((entity) => entity.entity_type === 'glossary_term'),
212
+ entities,
213
+ edges: compact(graph?.edges ?? []),
214
+ open_tasks: openTasks,
215
+ source_refs: sourceRefs(entities, openTasks, minutes),
216
+ approved_minutes_refs: minutes,
217
+ retrieval: graphRetrieval ?? retrievalContext,
218
+ };
219
+ const graphAvailable = graphFetched && Boolean(graph) && graphStatus !== 'unavailable';
220
+ const tasksAvailable = tasksFetched && tasks !== null;
221
+ const sourceStatus = {
222
+ graph: graphAvailable ? graphStatus : 'unavailable',
223
+ tasks: tasksAvailable ? 'resolved' : 'unavailable',
224
+ };
225
+ // Empty is a positive result only when both host sources were fetched
226
+ // successfully. Missing ports and unavailable responses must remain
227
+ // observable as unavailable/partial rather than confirmed_empty.
228
+ const status = !graphAvailable && !tasksAvailable
229
+ ? 'unavailable'
230
+ : errors.length || !graphAvailable || !tasksAvailable
231
+ ? 'partial'
232
+ : (entities.length || openTasks.length) ? 'resolved' : 'confirmed_empty';
233
+ const base = {
234
+ schema_version: MEETING_MINUTES_CONTEXT_RECEIPT_VERSION,
235
+ receipt_id: receiptId,
236
+ identity,
237
+ status,
238
+ source_status: sourceStatus,
239
+ searched_scope: {
240
+ project_code: identity.project_code,
241
+ entity_types: ['project', 'person', 'org', 'brand', 'decision', 'glossary_term', 'document'],
242
+ entity_limit: 80,
243
+ task_limit: 50,
244
+ approved_minutes_limit: 3,
245
+ },
246
+ resolved_at: (ports.now ?? (() => new Date()))().toISOString(),
247
+ context,
248
+ errors,
249
+ };
250
+ const bounded = boundedReceipt({ ...base, checksum: digestMeetingMinutesContext(base) });
251
+ const receipt = { ...bounded, checksum: digestMeetingMinutesContext({ ...bounded, checksum: undefined }) };
252
+ return persistWithRetry(receipt, ports);
253
+ },
254
+ });
255
+ const persisted = claim.receipt;
256
+ const readback = await ports.repository.get(persisted.receipt_id, ports.scope);
257
+ if (!readback || readback.checksum !== persisted.checksum) {
258
+ throw new MeetingMinutesContextReceiptError('meeting_minutes_context_readback_mismatch', 'meeting minutes context receipt readback did not match the persisted receipt', 503, { receipt_id: persisted.receipt_id });
259
+ }
260
+ return readback;
261
+ }
262
+ export async function readMeetingMinutesContextReceipt(receiptId, input, ports) {
263
+ const identity = normalizeMeetingMinutesContextIdentity(input);
264
+ if (!ports?.actor?.resolve || !ports?.auth?.authorize || !ports?.tenant?.authorize || !ports?.project?.authorize) {
265
+ throw new MeetingMinutesContextReceiptError('meeting_minutes_context_invalid_ports', 'actor, auth, tenant, and project ports are required', 500);
266
+ }
267
+ const actor = await ports.actor.resolve({ identity, request: input });
268
+ await ports.auth.authorize({ actor, identity });
269
+ await ports.tenant.authorize({ actor, identity });
270
+ await ports.project.authorize({ actor, identity });
271
+ if (!ports?.repository?.get) {
272
+ throw new MeetingMinutesContextReceiptError('meeting_minutes_context_invalid_ports', 'repository get port is required', 500);
273
+ }
274
+ const receipt = await ports.repository.get(receiptId, ports.scope);
275
+ if (!receipt)
276
+ throw new MeetingMinutesContextReceiptError('meeting_minutes_context_receipt_not_found', 'meeting minutes context receipt was not found', 404);
277
+ if (JSON.stringify(receipt.identity) !== JSON.stringify(identity))
278
+ throw new MeetingMinutesContextReceiptError('meeting_minutes_context_identity_mismatch', 'meeting minutes context receipt identity does not match', 409);
279
+ return receipt;
280
+ }
@@ -0,0 +1,89 @@
1
+ export declare const MEETING_SOURCE_SYNC_VERSION: "brainbase.meeting-source-sync.v1";
2
+ export declare const SUPPORTED_MEETING_SOURCE_PROVIDERS: readonly ["tactiq", "plaud"];
3
+ export type MeetingSourceProvider = typeof SUPPORTED_MEETING_SOURCE_PROVIDERS[number];
4
+ type UnknownRecord = Record<string, unknown>;
5
+ type MaybePromise<T> = T | Promise<T>;
6
+ export interface MeetingSourceArtifact extends UnknownRecord {
7
+ readonly provider: MeetingSourceProvider;
8
+ readonly external_id: string;
9
+ readonly title: string;
10
+ readonly meeting_mode: string;
11
+ readonly started_at: string | null;
12
+ readonly ended_at: string | null;
13
+ readonly updated_at: string | null;
14
+ readonly participants: readonly unknown[];
15
+ readonly mcp_resource_uri: string;
16
+ readonly transcript_hash: string | null;
17
+ readonly has_text: boolean;
18
+ readonly source_text: string;
19
+ readonly source_text_kind: string;
20
+ readonly source_text_length: number;
21
+ readonly text_preview: string;
22
+ readonly raw_metadata: UnknownRecord;
23
+ readonly dedupe_key: string;
24
+ }
25
+ export interface MeetingSourceCluster {
26
+ readonly source_cluster_id: string;
27
+ readonly dedupe_key: string;
28
+ readonly primary_source: MeetingSourceArtifact;
29
+ readonly supporting_sources: readonly MeetingSourceArtifact[];
30
+ readonly providers: readonly MeetingSourceProvider[];
31
+ readonly title: string;
32
+ readonly started_at: string | null;
33
+ readonly meeting_mode: string;
34
+ }
35
+ export interface MeetingSourceSyncClaimRequest {
36
+ readonly idempotency_key: string;
37
+ /** Fingerprint of the provider/window request, computed before provider I/O. */
38
+ readonly fingerprint: string;
39
+ readonly execute: () => MaybePromise<MeetingSourceSyncResult>;
40
+ }
41
+ export interface MeetingSourceSyncClaimResult {
42
+ readonly status: 'persisted' | 'replayed';
43
+ readonly result: MeetingSourceSyncResult;
44
+ }
45
+ export interface MeetingSourceSyncPorts {
46
+ readonly provider: (input: {
47
+ provider: MeetingSourceProvider;
48
+ cursor: UnknownRecord;
49
+ since?: string | null;
50
+ until?: string | null;
51
+ }) => MaybePromise<readonly UnknownRecord[]>;
52
+ readonly sink: {
53
+ /** Durable idempotency claim enclosing provider fetch, save and retry. */
54
+ readonly claimOrReplay: (request: MeetingSourceSyncClaimRequest) => MaybePromise<MeetingSourceSyncClaimResult>;
55
+ findByIdempotencyKey?: (key: string) => MaybePromise<MeetingSourceSyncResult | null>;
56
+ save: (result: MeetingSourceSyncResult) => MaybePromise<MeetingSourceSyncResult | void>;
57
+ readonly readback: (idempotencyKey: string) => MaybePromise<MeetingSourceSyncResult | null>;
58
+ };
59
+ readonly now?: () => Date;
60
+ readonly max_attempts?: number;
61
+ readonly is_retryable?: (error: unknown) => boolean;
62
+ }
63
+ export interface MeetingSourceSyncResult {
64
+ readonly schema_version: typeof MEETING_SOURCE_SYNC_VERSION;
65
+ readonly idempotency_key: string;
66
+ /** Fingerprint of provider/window input; excludes wall-clock ingestion time. */
67
+ readonly input_fingerprint?: string;
68
+ readonly provider: MeetingSourceProvider;
69
+ readonly artifacts: readonly MeetingSourceArtifact[];
70
+ readonly clusters: readonly MeetingSourceCluster[];
71
+ readonly cursor: UnknownRecord;
72
+ readonly synced_at: string;
73
+ readonly fingerprint: string;
74
+ }
75
+ export declare function stableMeetingSourceHash(value: string): string;
76
+ export declare function transcriptSegmentsToText(value: unknown): unknown;
77
+ export declare function normalizeMeetingSourceArtifact(raw?: UnknownRecord, providerInput?: unknown, _options?: {
78
+ readonly now?: () => Date;
79
+ }): MeetingSourceArtifact;
80
+ export declare function dedupeMeetingSourceArtifacts(artifacts: readonly MeetingSourceArtifact[]): MeetingSourceCluster[];
81
+ export declare function providerCursorAfterArtifacts(artifacts: readonly MeetingSourceArtifact[], previousCursor?: UnknownRecord): UnknownRecord;
82
+ export declare function runMeetingSourceSync(input: {
83
+ provider: MeetingSourceProvider;
84
+ cursor?: UnknownRecord;
85
+ since?: string | null;
86
+ until?: string | null;
87
+ idempotency_key?: string | null;
88
+ }, ports: MeetingSourceSyncPorts): Promise<MeetingSourceSyncResult>;
89
+ export {};
@@ -0,0 +1,230 @@
1
+ import { createHash } from 'node:crypto';
2
+ export const MEETING_SOURCE_SYNC_VERSION = 'brainbase.meeting-source-sync.v1';
3
+ export const SUPPORTED_MEETING_SOURCE_PROVIDERS = Object.freeze(['tactiq', 'plaud']);
4
+ function stable(value) {
5
+ if (Array.isArray(value))
6
+ return value.map(stable);
7
+ if (!value || typeof value !== 'object')
8
+ return value;
9
+ return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stable(value[key])]));
10
+ }
11
+ export function stableMeetingSourceHash(value) {
12
+ if (typeof value !== 'string') {
13
+ throw new TypeError('stableMeetingSourceHash accepts text only; use stableMeetingSourceFingerprint for objects');
14
+ }
15
+ return createHash('sha256').update(value).digest('hex');
16
+ }
17
+ function stableMeetingSourceFingerprint(value) {
18
+ return createHash('sha256').update(JSON.stringify(stable(value))).digest('hex');
19
+ }
20
+ function normalizeWhitespace(value) { return String(value || '').replace(/\s+/g, ' ').trim(); }
21
+ function normalizeMultilineText(value) { return String(value || '').replace(/\r\n/g, '\n').replace(/\r/g, '\n').replace(/[ \t]+/g, ' ').replace(/\n{3,}/g, '\n\n').trim(); }
22
+ export function transcriptSegmentsToText(value) {
23
+ if (typeof value !== 'string')
24
+ return value;
25
+ const trimmed = value.trim();
26
+ if (!trimmed.startsWith('[') || !trimmed.endsWith(']'))
27
+ return value;
28
+ let segments;
29
+ try {
30
+ segments = JSON.parse(trimmed);
31
+ }
32
+ catch {
33
+ return value;
34
+ }
35
+ if (!Array.isArray(segments) || segments.length === 0)
36
+ return value;
37
+ const lines = [];
38
+ for (const segment of segments) {
39
+ if (!segment || typeof segment !== 'object' || Array.isArray(segment))
40
+ return value;
41
+ const item = segment;
42
+ const content = typeof item.content === 'string' ? item.content : (typeof item.text === 'string' ? item.text : null);
43
+ if (content === null)
44
+ return value;
45
+ const speaker = normalizeWhitespace(item.speaker ?? item.original_speaker);
46
+ const line = normalizeWhitespace(content);
47
+ if (line)
48
+ lines.push(speaker ? `${speaker}: ${line}` : line);
49
+ }
50
+ return lines.length ? lines.join('\n') : value;
51
+ }
52
+ function safeProvider(input) {
53
+ const provider = String(input ?? '').toLowerCase();
54
+ if (!SUPPORTED_MEETING_SOURCE_PROVIDERS.includes(provider))
55
+ throw new Error(`unsupported meeting source provider: ${input}`);
56
+ return provider;
57
+ }
58
+ function minuteBucket(value) {
59
+ if (!value)
60
+ return '';
61
+ const date = new Date(value);
62
+ if (Number.isNaN(date.getTime()))
63
+ return value.slice(0, 16);
64
+ date.setSeconds(0, 0);
65
+ return date.toISOString().slice(0, 16);
66
+ }
67
+ function participantKey(participants) {
68
+ return participants.map((participant) => normalizeWhitespace(typeof participant === 'string' ? participant : participant?.name ?? participant?.email ?? participant?.id).toLowerCase()).filter(Boolean).sort().join('|');
69
+ }
70
+ function inferMeetingMode(raw) {
71
+ const explicit = String(raw.meeting_mode ?? raw.mode ?? raw.kind ?? '').toLowerCase();
72
+ if (['online', 'offline', 'call'].includes(explicit))
73
+ return explicit;
74
+ if (raw.calendar_event_id || raw.conference_url || raw.meet_url || raw.zoom_url)
75
+ return 'online';
76
+ if (raw.phone_number || raw.call_id || raw.is_call)
77
+ return 'call';
78
+ if (raw.location && !raw.conference_url)
79
+ return 'offline';
80
+ return 'unknown';
81
+ }
82
+ function sourcePriority(artifact) {
83
+ if (artifact.meeting_mode === 'online' && artifact.provider === 'tactiq')
84
+ return 100;
85
+ if (['offline', 'call'].includes(artifact.meeting_mode) && artifact.provider === 'plaud')
86
+ return 100;
87
+ if (artifact.provider === 'tactiq')
88
+ return 70;
89
+ if (artifact.provider === 'plaud')
90
+ return 60;
91
+ return 10;
92
+ }
93
+ function completenessScore(artifact) {
94
+ return (artifact.has_text ? 4 : 0) + (artifact.started_at ? 2 : 0) + (artifact.ended_at ? 1 : 0) + (artifact.participants.length ? 1 : 0) + (artifact.title ? 1 : 0) + (artifact.mcp_resource_uri ? 1 : 0);
95
+ }
96
+ export function normalizeMeetingSourceArtifact(raw = {}, providerInput = raw.provider, _options = {}) {
97
+ const provider = safeProvider(providerInput);
98
+ const transcript = transcriptSegmentsToText(raw.text ?? raw.transcript ?? raw.transcript_text ?? '');
99
+ const normalizedText = normalizeMultilineText(transcript);
100
+ const providerNote = normalizeWhitespace(raw.note_text ?? raw.content ?? raw.markdown ?? raw.summary ?? raw.ai_summary ?? raw.meeting_summary ?? '');
101
+ const startedAt = typeof (raw.started_at ?? raw.start_time ?? raw.created_at ?? raw.recorded_at) === 'string' ? String(raw.started_at ?? raw.start_time ?? raw.created_at ?? raw.recorded_at) : null;
102
+ const title = normalizeWhitespace(raw.title ?? raw.meeting_title ?? raw.name ?? 'Untitled meeting source');
103
+ const participants = Array.isArray(raw.participants) ? raw.participants : [];
104
+ const resourceUri = String(raw.mcp_resource_uri ?? raw.resource_uri ?? raw.url ?? raw.permalink ?? raw.file_url ?? `${provider}:${raw.id ?? raw.external_id ?? stableMeetingSourceFingerprint(raw).slice(0, 16)}`);
105
+ const externalId = String(raw.external_id ?? raw.id ?? stableMeetingSourceHash(`${provider}:${resourceUri}`).slice(0, 24));
106
+ const transcriptHash = normalizedText ? stableMeetingSourceHash(normalizedText) : null;
107
+ const sourceTextKind = normalizedText ? 'transcript' : (providerNote ? 'provider_note_available' : 'empty');
108
+ // A wall-clock fallback makes identical provider payloads conflict on a
109
+ // retry. Preserve provider/start timestamps and use a stable null when no
110
+ // source timestamp exists; the sync result's synced_at remains clock-owned.
111
+ const updatedAtValue = raw.updated_at ?? raw.modified_at ?? raw.created_at ?? startedAt ?? null;
112
+ const updatedAt = typeof updatedAtValue === 'string' && updatedAtValue.trim() ? updatedAtValue : null;
113
+ return {
114
+ provider,
115
+ external_id: externalId,
116
+ title,
117
+ meeting_mode: inferMeetingMode(raw),
118
+ started_at: startedAt,
119
+ ended_at: typeof raw.ended_at === 'string' ? raw.ended_at : (typeof raw.end_time === 'string' ? raw.end_time : null),
120
+ updated_at: updatedAt,
121
+ participants,
122
+ mcp_resource_uri: resourceUri,
123
+ transcript_hash: transcriptHash,
124
+ has_text: Boolean(normalizedText),
125
+ source_text: normalizedText,
126
+ source_text_kind: sourceTextKind,
127
+ source_text_length: normalizedText.length,
128
+ text_preview: normalizedText.slice(0, 280),
129
+ raw_metadata: {
130
+ calendar_event_id: raw.calendar_event_id ?? null,
131
+ source_url: raw.url ?? raw.permalink ?? null,
132
+ slack_permalink: raw.slack_permalink ?? raw.slack_permalink_url ?? null,
133
+ provider_role: provider === 'tactiq' ? 'online_primary' : 'offline_or_call_primary',
134
+ source_text_kind: sourceTextKind,
135
+ provider_note_authoritative: false,
136
+ provider_note_preview: providerNote ? providerNote.slice(0, 280) : null,
137
+ },
138
+ dedupe_key: transcriptHash ? `hash:${transcriptHash}` : `shape:${stableMeetingSourceHash(`${title.toLowerCase()}|${minuteBucket(startedAt)}|${participantKey(participants)}`)}`,
139
+ };
140
+ }
141
+ export function dedupeMeetingSourceArtifacts(artifacts) {
142
+ const groups = new Map();
143
+ for (const artifact of artifacts) {
144
+ const key = artifact.dedupe_key || `${artifact.provider}:${artifact.external_id}`;
145
+ groups.set(key, [...(groups.get(key) ?? []), artifact]);
146
+ }
147
+ return [...groups.entries()].map(([dedupeKey, members]) => {
148
+ const sorted = [...members].sort((left, right) => sourcePriority(right) - sourcePriority(left) || completenessScore(right) - completenessScore(left) || String(right.updated_at).localeCompare(String(left.updated_at)));
149
+ const primary = sorted[0];
150
+ return {
151
+ source_cluster_id: `msrc_${stableMeetingSourceHash(`${dedupeKey}:${primary.provider}:${primary.external_id}`).slice(0, 24)}`,
152
+ dedupe_key: dedupeKey,
153
+ primary_source: primary,
154
+ supporting_sources: sorted.slice(1),
155
+ providers: sorted.map((item) => item.provider),
156
+ title: primary.title,
157
+ started_at: primary.started_at,
158
+ meeting_mode: primary.meeting_mode,
159
+ };
160
+ });
161
+ }
162
+ export function providerCursorAfterArtifacts(artifacts, previousCursor = {}) {
163
+ const latest = [...artifacts].map((artifact) => ({ timestamp: artifact.updated_at || artifact.started_at, external_id: artifact.external_id })).filter((item) => item.timestamp).sort((a, b) => String(a.timestamp).localeCompare(String(b.timestamp)) || String(a.external_id).localeCompare(String(b.external_id))).at(-1);
164
+ const updatedSince = [previousCursor.updated_since, latest?.timestamp].filter(Boolean).sort().at(-1) ?? null;
165
+ return { ...previousCursor, updated_since: updatedSince, last_seen_external_id: latest && updatedSince === latest.timestamp ? latest.external_id : previousCursor.last_seen_external_id ?? null };
166
+ }
167
+ export async function runMeetingSourceSync(input, ports) {
168
+ const provider = safeProvider(input.provider);
169
+ const cursor = input.cursor ?? {};
170
+ if (!ports?.sink?.claimOrReplay || !ports?.sink?.readback || !ports?.sink?.save) {
171
+ throw new Error('meeting source sync requires durable claimOrReplay, save and readback ports');
172
+ }
173
+ // The request key is deterministic without reading the provider. Provider
174
+ // I/O therefore runs inside the durable claim and cannot be duplicated by
175
+ // two workers racing on the same cursor window.
176
+ const idempotencyKey = input.idempotency_key?.trim()
177
+ || `meeting-source:${provider}:${stableMeetingSourceFingerprint({ cursor, since: input.since ?? null, until: input.until ?? null })}`;
178
+ const inputFingerprint = stableMeetingSourceFingerprint({
179
+ provider,
180
+ cursor,
181
+ since: input.since ?? null,
182
+ until: input.until ?? null,
183
+ });
184
+ const claim = await ports.sink.claimOrReplay({
185
+ idempotency_key: idempotencyKey,
186
+ fingerprint: inputFingerprint,
187
+ execute: async () => {
188
+ const rawArtifacts = await ports.provider({ provider, cursor, since: input.since, until: input.until });
189
+ const artifacts = rawArtifacts.map((raw) => normalizeMeetingSourceArtifact(raw, provider, { now: ports.now }));
190
+ const clusters = dedupeMeetingSourceArtifacts(artifacts);
191
+ const nextCursor = providerCursorAfterArtifacts(artifacts, cursor);
192
+ // updated_at may be null when the provider does not expose a timestamp;
193
+ // it is intentionally excluded from a separate identity projection so
194
+ // a retry at a different wall-clock time keeps the same fingerprint.
195
+ const fingerprint = stableMeetingSourceFingerprint({ provider, artifacts, clusters, cursor: nextCursor });
196
+ const result = {
197
+ schema_version: MEETING_SOURCE_SYNC_VERSION,
198
+ idempotency_key: idempotencyKey,
199
+ input_fingerprint: inputFingerprint,
200
+ provider,
201
+ artifacts,
202
+ clusters,
203
+ cursor: nextCursor,
204
+ synced_at: (ports.now ?? (() => new Date()))().toISOString(),
205
+ fingerprint,
206
+ };
207
+ const maxAttempts = Math.max(1, Math.min(5, ports.max_attempts ?? 2));
208
+ let saved = undefined;
209
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
210
+ try {
211
+ saved = await ports.sink.save(result);
212
+ break;
213
+ }
214
+ catch (error) {
215
+ if (!(ports.is_retryable?.(error) ?? Boolean(error?.retryable)) || attempt >= maxAttempts)
216
+ throw error;
217
+ }
218
+ }
219
+ return saved ?? result;
220
+ },
221
+ });
222
+ const persisted = claim.result;
223
+ if (persisted.input_fingerprint && persisted.input_fingerprint !== inputFingerprint) {
224
+ throw new Error('meeting source sync idempotency conflict');
225
+ }
226
+ const readback = await ports.sink.readback(idempotencyKey);
227
+ if (!readback || readback.fingerprint !== persisted.fingerprint)
228
+ throw new Error('meeting source sync readback mismatch');
229
+ return readback;
230
+ }
package/dist/mesh.d.ts ADDED
@@ -0,0 +1,58 @@
1
+ /** Portable mesh envelope and host-port contracts. No network or auth dependency. */
2
+ export declare const MESH_CONTRACT_VERSION: "mesh.v1";
3
+ export declare const MESH_ENVELOPE_TYPES: readonly ["query", "response", "ping", "pong", "peer_joined", "peer_left"];
4
+ export type MeshEnvelopeType = typeof MESH_ENVELOPE_TYPES[number];
5
+ export type MeshJson = Record<string, unknown> | unknown[] | string | number | boolean | null;
6
+ export interface MeshEnvelope {
7
+ id: string;
8
+ from: string;
9
+ to: string;
10
+ type: MeshEnvelopeType;
11
+ payload: MeshJson;
12
+ ts: number;
13
+ nonce: string;
14
+ }
15
+ export interface MeshEnvelopeInput {
16
+ from: string;
17
+ to: string;
18
+ type: MeshEnvelopeType;
19
+ payload?: MeshJson;
20
+ }
21
+ export declare class MeshContractError extends Error {
22
+ readonly code = "mesh_contract_invalid";
23
+ constructor(field: string, message?: string);
24
+ }
25
+ export declare function createMeshEnvelope(input: MeshEnvelopeInput, options?: {
26
+ now?: number;
27
+ idFactory?: () => string;
28
+ nonceFactory?: () => string;
29
+ }): MeshEnvelope;
30
+ export declare function normalizeMeshEnvelope(value: unknown): MeshEnvelope;
31
+ export interface MeshProjectPort {
32
+ projectId: string;
33
+ localPath?: string;
34
+ globInclude?: string[];
35
+ ports?: Record<string, string | number>;
36
+ }
37
+ export interface MeshNodeProfile {
38
+ nodeId: string;
39
+ principalRef: string;
40
+ roleRank?: number;
41
+ agentRuntime?: string;
42
+ projects: MeshProjectPort[];
43
+ }
44
+ export declare function normalizeMeshNodeProfile(value: unknown): MeshNodeProfile;
45
+ export interface MeshQueryRequest {
46
+ queryId: string;
47
+ from: string;
48
+ to: string;
49
+ question: string;
50
+ scope?: string;
51
+ }
52
+ export declare function normalizeMeshQueryRequest(value: unknown): MeshQueryRequest;
53
+ export declare function createMeshSendReceipt(queryId: string): {
54
+ queryId: string;
55
+ status: 'sent';
56
+ receipt_kind: 'send_ack';
57
+ answer_status: 'unknown';
58
+ };