@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
@@ -1,4 +1,4 @@
1
- import { createHash, randomUUID } from 'node:crypto';
1
+ import { createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from 'node:crypto';
2
2
  import { defaultJudgmentProblemSnapshotAccessProvider } from './judgment-problem-snapshot.js';
3
3
  import { initializePersonalOs, loadPersonalOs, mutatePersonalOsWithSidecar, readPersonalOsSidecar } from './ssot.js';
4
4
  /** Story13's persisted contract. The ledger is deliberately separate from the
@@ -26,7 +26,7 @@ class DurableWaitPreflightConflict extends DurableWaitError {
26
26
  const SNAPSHOT_ID_PATTERN = /^sha256:[0-9a-f]{64}$/u;
27
27
  const DEFAULT_LEASE_MS = 5 * 60 * 1000;
28
28
  const WAIT_STATES = [
29
- 'waiting', 'claimed', 'resumed', 'handoff_required', 'reconciliation_wait', 'failed', 'cancelled'
29
+ 'waiting', 'claimed', 'resumed', 'handoff_required', 'reconciliation_wait', 'effect_confirmed', 'failed', 'cancelled'
30
30
  ];
31
31
  const RESUME_METHODS = [
32
32
  'resume_run', 'new_problem', 'reconcile_external_effect', 'manual_review'
@@ -35,6 +35,43 @@ const FAILURE_POLICIES = [
35
35
  'handoff', 'new_problem', 'reconciliation_wait', 'fail'
36
36
  ];
37
37
  const TRIGGERS = ['event', 'timer', 'manual'];
38
+ const DUE_CURSOR_KEY = randomBytes(32);
39
+ function earliestDueAt(deadline) {
40
+ const values = [deadline?.due_at, deadline?.next_review_at].filter((value) => value !== undefined);
41
+ return values.sort((a, b) => Date.parse(a) - Date.parse(b))[0];
42
+ }
43
+ function encodeDueCursor(after, tenantId, principal, scopeType, scopeId) {
44
+ const payload = Buffer.from(JSON.stringify({ v: 1, after, tenantId, principal, scopeType, scopeId }), 'utf8').toString('base64url');
45
+ const signature = createHmac('sha256', DUE_CURSOR_KEY).update(payload).digest('base64url');
46
+ return `${payload}.${signature}`;
47
+ }
48
+ function decodeDueCursor(cursor, tenantId, principal, scopeType, scopeId) {
49
+ if (cursor === undefined)
50
+ return '';
51
+ if (typeof cursor !== 'string' || cursor.length === 0 || cursor.length > 4096 || !/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/u.test(cursor)) {
52
+ throw new DurableWaitError('invalid_request', 'cursor is invalid');
53
+ }
54
+ try {
55
+ const [payload, signature] = cursor.split('.');
56
+ const expected = createHmac('sha256', DUE_CURSOR_KEY).update(payload).digest();
57
+ const received = Buffer.from(signature, 'base64url');
58
+ if (expected.length !== received.length || !timingSafeEqual(expected, received))
59
+ throw new Error('cursor signature is invalid');
60
+ const decoded = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
61
+ if (decoded === null || typeof decoded !== 'object' || Array.isArray(decoded))
62
+ throw new Error('cursor is not an object');
63
+ const value = decoded;
64
+ if (value.v !== 1 || value.tenantId !== tenantId || value.principal !== principal
65
+ || value.scopeType !== scopeType || value.scopeId !== scopeId
66
+ || typeof value.after !== 'string' || value.after.length === 0 || value.after.length > 512) {
67
+ throw new Error('cursor binding is invalid');
68
+ }
69
+ return value.after;
70
+ }
71
+ catch {
72
+ throw new DurableWaitError('invalid_request', 'cursor is invalid for this principal and scope');
73
+ }
74
+ }
38
75
  export const defaultDurableWaitAccessProvider = {
39
76
  authorize({ action, principal, acl }) {
40
77
  return defaultJudgmentProblemSnapshotAccessProvider.authorize({
@@ -48,6 +85,8 @@ export class DurableWaitStore {
48
85
  dataDir;
49
86
  problemSnapshot;
50
87
  access;
88
+ externalEffect;
89
+ unknownEffect;
51
90
  clock;
52
91
  sidecarPath;
53
92
  defaultLeaseMs;
@@ -59,6 +98,8 @@ export class DurableWaitStore {
59
98
  this.dataDir = options.dataDir;
60
99
  this.problemSnapshot = options.problemSnapshot;
61
100
  this.access = options.access ?? defaultDurableWaitAccessProvider;
101
+ this.externalEffect = options.externalEffect;
102
+ this.unknownEffect = options.unknownEffect;
62
103
  this.clock = options.clock ?? (() => new Date());
63
104
  this.sidecarPath = options.sidecarPath ?? DURABLE_WAIT_SIDECAR_PATH;
64
105
  assertSidecarPath(this.sidecarPath);
@@ -84,6 +125,7 @@ export class DurableWaitStore {
84
125
  const record = freezeClone({
85
126
  wait_id: normalized.wait_id,
86
127
  version: DURABLE_WAIT_VERSION,
128
+ ...(normalized.tenant_id === undefined ? {} : { tenant_id: normalized.tenant_id }),
87
129
  owner_scope: normalized.owner_scope,
88
130
  read_policy: normalized.read_policy,
89
131
  problem_snapshot: normalized.problem_snapshot,
@@ -141,6 +183,56 @@ export class DurableWaitStore {
141
183
  }
142
184
  return result;
143
185
  }
186
+ /** Discovery only: claim repeats all live authorization and trigger checks. */
187
+ async listDueCandidates(input) {
188
+ assertText(input?.tenant_id, 'tenant_id');
189
+ assertText(input?.principal, 'principal');
190
+ if (!['personal', 'project', 'organization'].includes(input?.owner_scope_type)) {
191
+ throw new DurableWaitError('invalid_request', 'owner_scope_type is invalid');
192
+ }
193
+ assertText(input?.owner_scope_id, 'owner_scope_id');
194
+ const limit = input.limit ?? 50;
195
+ if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
196
+ throw new DurableWaitError('invalid_request', 'limit must be an integer from 1 to 100');
197
+ }
198
+ const after = decodeDueCursor(input.cursor, input.tenant_id, input.principal, input.owner_scope_type, input.owner_scope_id);
199
+ const nowMs = this.now().getTime();
200
+ await this.initialize();
201
+ const ledger = parseLedger(await readPersonalOsSidecar(this.dataDir, this.sidecarPath));
202
+ const candidates = [];
203
+ let hasMore = false;
204
+ for (const record of [...ledger.waits].sort((a, b) => a.wait_id < b.wait_id ? -1 : a.wait_id > b.wait_id ? 1 : 0)) {
205
+ if (record.wait_id <= after || record.tenant_id !== input.tenant_id
206
+ || record.owner_scope.type !== input.owner_scope_type
207
+ || record.owner_scope.id !== input.owner_scope_id || record.state !== 'waiting')
208
+ continue;
209
+ const dueAt = earliestDueAt(record.deadline);
210
+ if (dueAt === undefined || Date.parse(dueAt) > nowMs)
211
+ continue;
212
+ let allowed;
213
+ try {
214
+ allowed = await this.access.authorize({ action: 'claim', principal: input.principal, acl: record.read_policy, wait: record });
215
+ }
216
+ catch {
217
+ throw new DurableWaitError('authorization_provider_failed', 'Claim access could not be verified');
218
+ }
219
+ if (allowed === false)
220
+ continue;
221
+ await this.verifyProblem(input.principal, record.problem_snapshot);
222
+ if (candidates.length === limit) {
223
+ hasMore = true;
224
+ break;
225
+ }
226
+ candidates.push({ wait_id: record.wait_id, due_at: dueAt });
227
+ }
228
+ const last = candidates.at(-1);
229
+ return {
230
+ candidates,
231
+ ...(hasMore && last !== undefined
232
+ ? { next_cursor: encodeDueCursor(last.wait_id, input.tenant_id, input.principal, input.owner_scope_type, input.owner_scope_id) }
233
+ : {})
234
+ };
235
+ }
144
236
  async claim(input) {
145
237
  const normalized = normalizeClaimInput(input, this.now(), this.defaultLeaseMs);
146
238
  await this.initialize();
@@ -155,9 +247,12 @@ export class DurableWaitStore {
155
247
  if (record.state === 'reconciliation_wait') {
156
248
  throw new DurableWaitError('reconciliation_required', `Wait ${record.wait_id} requires external-effect reconciliation`, record.wait_id);
157
249
  }
158
- if (record.state === 'handoff_required' || record.state === 'resumed' || record.state === 'failed' || record.state === 'cancelled') {
250
+ if (record.state === 'handoff_required' || record.state === 'resumed' || record.state === 'effect_confirmed' || record.state === 'failed' || record.state === 'cancelled') {
159
251
  throw new DurableWaitError('state_conflict', `Wait ${record.wait_id} is ${record.state}`, record.wait_id);
160
252
  }
253
+ if (record.reconciliation_receipts?.some((receipt) => receipt.previous_claim_request_id === normalized.request_id)) {
254
+ throw new DurableWaitError('state_conflict', 'A reconciled effect requires a fresh claim request_id', record.wait_id);
255
+ }
161
256
  // Validate the incoming event/timer before treating an existing claim as
162
257
  // a duplicate. A different, not-yet-ready trigger must not receive a
163
258
  // claim it cannot legitimately resume.
@@ -319,7 +414,10 @@ export class DurableWaitStore {
319
414
  return mutatePersonalOsWithSidecar(this.dataDir, this.sidecarPath, (current, content) => {
320
415
  const ledger = this.assertMutationPreflight(current, content, preflight, normalized.wait_id);
321
416
  const record = findWait(ledger, normalized.wait_id);
322
- if (record.state === 'resumed' || record.state === 'cancelled' || record.state === 'failed') {
417
+ if (record.state === 'reconciliation_wait') {
418
+ throw new DurableWaitError('reconciliation_required', `Wait ${record.wait_id} requires external-effect reconciliation`, record.wait_id);
419
+ }
420
+ if (record.state === 'resumed' || record.state === 'effect_confirmed' || record.state === 'cancelled' || record.state === 'failed') {
323
421
  throw new DurableWaitError('state_conflict', `Wait ${record.wait_id} is ${record.state}`, record.wait_id);
324
422
  }
325
423
  const now = this.now().toISOString();
@@ -344,6 +442,67 @@ export class DurableWaitStore {
344
442
  }
345
443
  async markEffectUnknown(input) {
346
444
  const normalized = normalizeEffectUnknownInput(input);
445
+ const externalOperationId = normalized.external_operation_id;
446
+ if (externalOperationId === undefined)
447
+ return this.markEffectUnknownUnbound(normalized);
448
+ await this.initialize();
449
+ const preflight = await this.captureMutationPreflight(normalized.wait_id);
450
+ const preflightRecord = requireMutationRecord(preflight, normalized.wait_id);
451
+ await this.authorize('write', normalized.principal, preflightRecord.read_policy, preflightRecord);
452
+ await this.verifyProblem(normalized.principal, preflightRecord.problem_snapshot);
453
+ if ((preflightRecord.state !== 'claimed' && preflightRecord.state !== 'handoff_required') || preflightRecord.claim === undefined) {
454
+ throw new DurableWaitError('state_conflict', `Wait ${normalized.wait_id} has no claim to bind the uncertain effect`, normalized.wait_id);
455
+ }
456
+ let bindingVerification;
457
+ if (this.unknownEffect && typeof this.unknownEffect.verify === 'function') {
458
+ try {
459
+ bindingVerification = await this.unknownEffect.verify({
460
+ principal: normalized.principal,
461
+ wait: freezeClone(preflightRecord),
462
+ claim_id: preflightRecord.claim.claim_id,
463
+ external_operation_id: externalOperationId
464
+ });
465
+ }
466
+ catch {
467
+ // The effect is still uncertain. Persist a quarantine rather than
468
+ // leaving a claimed lease eligible for handoff and duplicate execution.
469
+ }
470
+ }
471
+ const bindingVerified = !!bindingVerification && bindingVerification.wait_id === normalized.wait_id
472
+ && bindingVerification.claim_id === preflightRecord.claim.claim_id
473
+ && bindingVerification.run_id === preflightRecord.run_ref.run_id
474
+ && bindingVerification.external_operation_id === externalOperationId
475
+ && bindingVerification.status === 'unknown';
476
+ await this.authorize('write', normalized.principal, preflightRecord.read_policy, preflightRecord);
477
+ return mutatePersonalOsWithSidecar(this.dataDir, this.sidecarPath, (current, content) => {
478
+ const ledger = this.assertMutationPreflight(current, content, preflight, normalized.wait_id);
479
+ const record = findWait(ledger, normalized.wait_id);
480
+ if ((record.state !== 'claimed' && record.state !== 'handoff_required') || record.claim === undefined) {
481
+ throw new DurableWaitError('state_conflict', `Wait ${record.wait_id} has no claim to bind the uncertain effect`, record.wait_id);
482
+ }
483
+ const now = this.now().toISOString();
484
+ const responsible = normalized.responsible ?? record.responsible;
485
+ const updated = updateRecord(record, {
486
+ state: 'reconciliation_wait',
487
+ responsible,
488
+ reconciliation: { reason: normalized.reason, recorded_at: now, responsible, external_operation_id: externalOperationId, claim_id: record.claim.claim_id, binding_status: bindingVerified ? 'verified' : 'unverified' },
489
+ updated_at: now,
490
+ history: appendHistory(record, {
491
+ action: 'effect_unknown',
492
+ occurred_at: now,
493
+ principal: normalized.principal,
494
+ from_state: record.state,
495
+ to_state: 'reconciliation_wait',
496
+ detail: `${normalized.reason}:${externalOperationId}`,
497
+ responsible
498
+ })
499
+ });
500
+ replaceWait(ledger, updated);
501
+ return { next: current, sidecarContent: serializeLedger(ledger), result: freezeClone(updated) };
502
+ });
503
+ }
504
+ /** Previous behavior, kept for callers that do not send external_operation_id. */
505
+ async markEffectUnknownUnbound(normalized) {
347
506
  await this.initialize();
348
507
  const preflight = await this.captureMutationPreflight(normalized.wait_id);
349
508
  const preflightRecord = requireMutationRecord(preflight, normalized.wait_id);
@@ -352,7 +511,7 @@ export class DurableWaitStore {
352
511
  return mutatePersonalOsWithSidecar(this.dataDir, this.sidecarPath, (current, content) => {
353
512
  const ledger = this.assertMutationPreflight(current, content, preflight, normalized.wait_id);
354
513
  const record = findWait(ledger, normalized.wait_id);
355
- if (record.state === 'resumed' || record.state === 'cancelled' || record.state === 'failed') {
514
+ if (record.state === 'resumed' || record.state === 'cancelled' || record.state === 'failed' || record.state === 'effect_confirmed') {
356
515
  throw new DurableWaitError('state_conflict', `Wait ${record.wait_id} is ${record.state}`, record.wait_id);
357
516
  }
358
517
  const now = this.now().toISOString();
@@ -376,6 +535,103 @@ export class DurableWaitStore {
376
535
  return { next: current, sidecarContent: serializeLedger(ledger), result: freezeClone(updated) };
377
536
  });
378
537
  }
538
+ async reconcileExternalEffect(input) {
539
+ const normalized = normalizeReconcileExternalEffectInput(input);
540
+ await this.initialize();
541
+ const preflight = await this.captureMutationPreflight(normalized.wait_id);
542
+ const preflightRecord = requireMutationRecord(preflight, normalized.wait_id);
543
+ await this.authorize('write', normalized.principal, preflightRecord.read_policy, preflightRecord);
544
+ await this.verifyProblem(normalized.principal, preflightRecord.problem_snapshot);
545
+ const prior = preflightRecord.reconciliation_receipts?.find((receipt) => receipt.request_id === normalized.request_id);
546
+ if (prior !== undefined) {
547
+ assertReconciliationReplay(prior, normalized);
548
+ return freezeClone(preflightRecord);
549
+ }
550
+ if (preflightRecord.state !== 'reconciliation_wait') {
551
+ throw new DurableWaitError('state_conflict', `Wait ${normalized.wait_id} is ${preflightRecord.state}`, normalized.wait_id);
552
+ }
553
+ const binding = preflightRecord.reconciliation;
554
+ if (!binding?.external_operation_id || !binding.claim_id || binding.binding_status !== 'verified' || !preflightRecord.claim || binding.claim_id !== preflightRecord.claim.claim_id) {
555
+ throw new DurableWaitError('reconciliation_required', 'The uncertain effect has no bound operation and claim identity', normalized.wait_id);
556
+ }
557
+ if (normalized.external_operation_id !== binding.external_operation_id) {
558
+ throw new DurableWaitError('state_conflict', 'Evidence targets a different external operation', normalized.wait_id);
559
+ }
560
+ if (!this.externalEffect || typeof this.externalEffect.verify !== 'function') {
561
+ throw new DurableWaitError('reconciliation_required', 'An authoritative external-effect verifier is required', normalized.wait_id);
562
+ }
563
+ let verification;
564
+ try {
565
+ verification = await this.externalEffect.verify({
566
+ principal: normalized.principal,
567
+ wait: freezeClone(preflightRecord),
568
+ claim_id: binding.claim_id,
569
+ external_operation_id: normalized.external_operation_id,
570
+ evidence_ref: normalized.evidence_ref
571
+ });
572
+ }
573
+ catch {
574
+ throw new DurableWaitError('reconciliation_required', 'External-effect evidence could not be verified', normalized.wait_id);
575
+ }
576
+ if (!verification || verification.wait_id !== normalized.wait_id
577
+ || verification.claim_id !== binding.claim_id
578
+ || verification.external_operation_id !== binding.external_operation_id
579
+ || verification.evidence_ref !== normalized.evidence_ref
580
+ || !['performed', 'not_performed', 'unknown'].includes(verification.outcome)) {
581
+ throw new DurableWaitError('reconciliation_required', 'External-effect verifier returned mismatched evidence', normalized.wait_id);
582
+ }
583
+ const outcome = verification.outcome;
584
+ // The evidence lookup may be slow; do not commit under permission that
585
+ // disappeared while it was running. The ledger CAS covers SSOT changes.
586
+ await this.authorize('write', normalized.principal, preflightRecord.read_policy, preflightRecord);
587
+ try {
588
+ return await mutatePersonalOsWithSidecar(this.dataDir, this.sidecarPath, (current, content) => {
589
+ const ledger = this.assertMutationPreflight(current, content, preflight, normalized.wait_id);
590
+ const record = findWait(ledger, normalized.wait_id);
591
+ if (record.state !== 'reconciliation_wait') {
592
+ throw new DurableWaitError('state_conflict', `Wait ${record.wait_id} is ${record.state}`, record.wait_id);
593
+ }
594
+ const now = this.now().toISOString();
595
+ const receipt = {
596
+ request_id: normalized.request_id,
597
+ external_operation_id: normalized.external_operation_id,
598
+ evidence_ref: normalized.evidence_ref,
599
+ outcome,
600
+ principal: normalized.principal,
601
+ recorded_at: now,
602
+ previous_claim_request_id: record.claim.request_id,
603
+ claim_id: binding.claim_id
604
+ };
605
+ const state = outcome === 'performed' ? 'effect_confirmed' : outcome === 'not_performed' ? 'waiting' : 'reconciliation_wait';
606
+ const updated = updateRecord(record, {
607
+ state,
608
+ updated_at: now,
609
+ reconciliation_receipts: [...(record.reconciliation_receipts ?? []), receipt],
610
+ history: appendHistory(record, {
611
+ action: 'effect_reconciled', occurred_at: now, principal: normalized.principal,
612
+ from_state: 'reconciliation_wait', to_state: state, detail: `${outcome}:${normalized.external_operation_id}`
613
+ })
614
+ });
615
+ // A proven non-effect can be claimed again, but never with the old lease.
616
+ if (outcome === 'not_performed')
617
+ delete updated.claim;
618
+ replaceWait(ledger, updated);
619
+ return { next: current, sidecarContent: serializeLedger(ledger), result: freezeClone(updated) };
620
+ });
621
+ }
622
+ catch (error) {
623
+ if (!(error instanceof DurableWaitPreflightConflict))
624
+ throw error;
625
+ const current = await this.readStored(normalized.wait_id);
626
+ await this.authorize('write', normalized.principal, current.read_policy, current);
627
+ await this.verifyProblem(normalized.principal, current.problem_snapshot);
628
+ const winner = current.reconciliation_receipts?.find((receipt) => receipt.request_id === normalized.request_id);
629
+ if (winner === undefined)
630
+ throw error;
631
+ assertReconciliationReplay(winner, normalized);
632
+ return freezeClone(current);
633
+ }
634
+ }
379
635
  async captureMutationPreflight(waitId) {
380
636
  // These reads intentionally happen outside mutatePersonalOsWithSidecar's
381
637
  // lock. A trusted provider may use GraphFoundationRevisionStore or read
@@ -482,6 +738,8 @@ export function createDurableWaitStore(options) {
482
738
  }
483
739
  function normalizeCreateInput(input, now) {
484
740
  assertText(input?.principal, 'principal');
741
+ if (input.tenant_id !== undefined)
742
+ assertText(input.tenant_id, 'tenant_id');
485
743
  const waitId = input.wait_id ?? `wait-${randomUUID()}`;
486
744
  assertText(waitId, 'wait_id');
487
745
  const ownerScope = normalizeScope(input.owner_scope, 'owner_scope');
@@ -506,6 +764,7 @@ function normalizeCreateInput(input, now) {
506
764
  assertTimestamp(createdAt, 'created_at');
507
765
  return {
508
766
  wait_id: waitId,
767
+ ...(input.tenant_id === undefined ? {} : { tenant_id: input.tenant_id }),
509
768
  principal: input.principal,
510
769
  owner_scope: ownerScope,
511
770
  read_policy: readPolicy,
@@ -577,7 +836,28 @@ function normalizeEffectUnknownInput(input) {
577
836
  const base = normalizeReadInput(input);
578
837
  assertText(input.reason, 'reason');
579
838
  const responsible = input.responsible === undefined ? undefined : normalizeResponsible(input.responsible, 'responsible');
580
- return { ...base, reason: input.reason, ...(responsible === undefined ? {} : { responsible }) };
839
+ return {
840
+ ...base,
841
+ reason: input.reason,
842
+ ...(input.external_operation_id === undefined ? {} : { external_operation_id: boundedText(input.external_operation_id, 'external_operation_id') }),
843
+ ...(responsible === undefined ? {} : { responsible })
844
+ };
845
+ }
846
+ function normalizeReconcileExternalEffectInput(input) {
847
+ const base = normalizeReadInput(input);
848
+ return {
849
+ ...base,
850
+ request_id: boundedText(input.request_id, 'request_id'),
851
+ external_operation_id: boundedText(input.external_operation_id, 'external_operation_id'),
852
+ evidence_ref: boundedText(input.evidence_ref, 'evidence_ref')
853
+ };
854
+ }
855
+ function assertReconciliationReplay(receipt, input) {
856
+ if (receipt.principal !== input.principal
857
+ || receipt.external_operation_id !== input.external_operation_id
858
+ || receipt.evidence_ref !== input.evidence_ref) {
859
+ throw new DurableWaitError('state_conflict', 'Reconciliation request_id was already used with different evidence', input.wait_id);
860
+ }
581
861
  }
582
862
  function normalizeCondition(value) {
583
863
  if (value === undefined || value === null || typeof value !== 'object')
@@ -755,6 +1035,8 @@ function validateStoredRecord(value) {
755
1035
  if (value.version !== DURABLE_WAIT_VERSION)
756
1036
  throw new Error('wait.version is unsupported');
757
1037
  assertText(value.wait_id, 'wait.wait_id');
1038
+ if (value.tenant_id !== undefined)
1039
+ assertText(value.tenant_id, 'wait.tenant_id');
758
1040
  const ownerScope = normalizeScope(value.owner_scope, 'wait.owner_scope');
759
1041
  const acl = normalizeAcl(value.read_policy, 'wait.read_policy');
760
1042
  const problem = normalizeProblemReference(value.problem_snapshot, 'wait.problem_snapshot');
@@ -773,9 +1055,11 @@ function validateStoredRecord(value) {
773
1055
  const resumeReceipt = value.resume_receipt === undefined ? undefined : validateResumeReceipt(value.resume_receipt);
774
1056
  const nextProblem = value.next_problem === undefined ? undefined : normalizeProblemReference(value.next_problem, 'wait.next_problem');
775
1057
  const reconciliation = value.reconciliation === undefined ? undefined : validateReconciliation(value.reconciliation);
1058
+ const reconciliationReceipts = value.reconciliation_receipts === undefined ? undefined : validateReconciliationReceipts(value.reconciliation_receipts);
776
1059
  return {
777
1060
  wait_id: value.wait_id,
778
1061
  version: DURABLE_WAIT_VERSION,
1062
+ ...(value.tenant_id === undefined ? {} : { tenant_id: value.tenant_id }),
779
1063
  owner_scope: ownerScope,
780
1064
  read_policy: acl,
781
1065
  problem_snapshot: problem,
@@ -792,6 +1076,7 @@ function validateStoredRecord(value) {
792
1076
  ...(resumeReceipt === undefined ? {} : { resume_receipt: resumeReceipt }),
793
1077
  ...(nextProblem === undefined ? {} : { next_problem: nextProblem }),
794
1078
  ...(reconciliation === undefined ? {} : { reconciliation }),
1079
+ ...(reconciliationReceipts === undefined ? {} : { reconciliation_receipts: reconciliationReceipts }),
795
1080
  history
796
1081
  };
797
1082
  }
@@ -846,7 +1131,7 @@ function validateResumeReceipt(value) {
846
1131
  function validateHistory(value) {
847
1132
  if (!isRecord(value))
848
1133
  throw new Error('wait.history entry must be an object');
849
- const action = enumValue(value.action, ['create', 'claim', 'handoff', 'premise_changed', 'effect_unknown', 'resume'], 'wait.history.action');
1134
+ const action = enumValue(value.action, ['create', 'claim', 'handoff', 'premise_changed', 'effect_unknown', 'effect_reconciled', 'resume'], 'wait.history.action');
850
1135
  assertTimestamp(value.occurred_at, 'wait.history.occurred_at');
851
1136
  assertText(value.principal, 'wait.history.principal');
852
1137
  const toState = enumValue(value.to_state, WAIT_STATES, 'wait.history.to_state');
@@ -862,7 +1147,34 @@ function validateReconciliation(value) {
862
1147
  const recordedAt = boundedText(value.recorded_at, 'wait.reconciliation.recorded_at');
863
1148
  assertTimestamp(recordedAt, 'wait.reconciliation.recorded_at');
864
1149
  const responsible = normalizeResponsible(value.responsible, 'wait.reconciliation.responsible');
865
- return { reason, recorded_at: recordedAt, responsible };
1150
+ const externalOperationId = value.external_operation_id === undefined ? undefined : boundedText(value.external_operation_id, 'wait.reconciliation.external_operation_id');
1151
+ const claimId = value.claim_id === undefined ? undefined : boundedText(value.claim_id, 'wait.reconciliation.claim_id');
1152
+ const bindingStatus = value.binding_status === undefined ? undefined : enumValue(value.binding_status, ['verified', 'unverified'], 'wait.reconciliation.binding_status');
1153
+ if ((externalOperationId === undefined) !== (claimId === undefined))
1154
+ throw new Error('wait.reconciliation binding is incomplete');
1155
+ if (bindingStatus !== undefined && externalOperationId === undefined)
1156
+ throw new Error('wait.reconciliation binding status has no operation');
1157
+ return { reason, recorded_at: recordedAt, responsible, ...(externalOperationId === undefined ? {} : { external_operation_id: externalOperationId, claim_id: claimId }), ...(bindingStatus === undefined ? {} : { binding_status: bindingStatus }) };
1158
+ }
1159
+ function validateReconciliationReceipts(value) {
1160
+ if (!Array.isArray(value))
1161
+ throw new Error('wait.reconciliation_receipts must be an array');
1162
+ const receipts = value.map((item) => {
1163
+ if (!isRecord(item))
1164
+ throw new Error('wait.reconciliation_receipt must be an object');
1165
+ const requestId = boundedText(item.request_id, 'wait.reconciliation_receipt.request_id');
1166
+ const externalOperationId = boundedText(item.external_operation_id, 'wait.reconciliation_receipt.external_operation_id');
1167
+ const evidenceRef = boundedText(item.evidence_ref, 'wait.reconciliation_receipt.evidence_ref');
1168
+ const outcome = enumValue(item.outcome, ['performed', 'not_performed', 'unknown'], 'wait.reconciliation_receipt.outcome');
1169
+ const principal = boundedText(item.principal, 'wait.reconciliation_receipt.principal');
1170
+ assertTimestamp(item.recorded_at, 'wait.reconciliation_receipt.recorded_at');
1171
+ const previousClaimRequestId = item.previous_claim_request_id === undefined ? undefined : boundedText(item.previous_claim_request_id, 'wait.reconciliation_receipt.previous_claim_request_id');
1172
+ const claimId = item.claim_id === undefined ? undefined : boundedText(item.claim_id, 'wait.reconciliation_receipt.claim_id');
1173
+ return { request_id: requestId, external_operation_id: externalOperationId, evidence_ref: evidenceRef, outcome, principal, recorded_at: item.recorded_at, ...(previousClaimRequestId === undefined ? {} : { previous_claim_request_id: previousClaimRequestId }), ...(claimId === undefined ? {} : { claim_id: claimId }) };
1174
+ });
1175
+ if (new Set(receipts.map((receipt) => receipt.request_id)).size !== receipts.length)
1176
+ throw new Error('duplicate reconciliation request_id');
1177
+ return receipts;
866
1178
  }
867
1179
  function serializeLedger(ledger) {
868
1180
  const sorted = {
@@ -0,0 +1,25 @@
1
+ /** Portable Honcho v3 HTTP boundary. Credentials and identity are host ports. */
2
+ export declare const HONCHO_CONTRACT_VERSION: "honcho.v1";
3
+ export interface HonchoConfig {
4
+ apiUrl: string;
5
+ workspaceId: string;
6
+ }
7
+ export interface HonchoRequest {
8
+ method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
9
+ path: string;
10
+ query?: Record<string, string | number | boolean>;
11
+ body?: Record<string, unknown> | unknown[] | null;
12
+ }
13
+ export declare class HonchoContractError extends Error {
14
+ readonly code = "honcho_contract_invalid";
15
+ constructor(field: string, message?: string);
16
+ }
17
+ export declare function normalizeHonchoConfig(value: unknown): HonchoConfig;
18
+ export declare function honchoWorkspacePath(workspaceId: string): string;
19
+ export declare function createHonchoRequest(input: HonchoRequest): HonchoRequest;
20
+ export declare function honchoRequestUrl(config: HonchoConfig, request: HonchoRequest): string;
21
+ export interface HonchoMessageInput {
22
+ peer: string;
23
+ content: string;
24
+ }
25
+ export declare function normalizeHonchoMessages(value: unknown): HonchoMessageInput[];
package/dist/honcho.js ADDED
@@ -0,0 +1,81 @@
1
+ /** Portable Honcho v3 HTTP boundary. Credentials and identity are host ports. */
2
+ export const HONCHO_CONTRACT_VERSION = 'honcho.v1';
3
+ export class HonchoContractError extends Error {
4
+ code = 'honcho_contract_invalid';
5
+ constructor(field, message = `Honcho field is invalid: ${field}`) {
6
+ super(message);
7
+ this.name = 'HonchoContractError';
8
+ }
9
+ }
10
+ function requiredText(value, field) {
11
+ if (typeof value !== 'string' || !value.trim())
12
+ throw new HonchoContractError(field);
13
+ return value.trim();
14
+ }
15
+ export function normalizeHonchoConfig(value) {
16
+ if (!value || typeof value !== 'object' || Array.isArray(value))
17
+ throw new HonchoContractError('config');
18
+ const candidate = value;
19
+ const rawUrl = requiredText(candidate.apiUrl, 'apiUrl');
20
+ let url;
21
+ try {
22
+ url = new URL(rawUrl);
23
+ }
24
+ catch {
25
+ throw new HonchoContractError('apiUrl', 'apiUrl must be an absolute URL');
26
+ }
27
+ if (!['http:', 'https:'].includes(url.protocol))
28
+ throw new HonchoContractError('apiUrl', 'apiUrl must use http or https');
29
+ if (url.username || url.password || url.search || url.hash) {
30
+ throw new HonchoContractError('apiUrl', 'apiUrl must not contain credentials, query, or hash components');
31
+ }
32
+ return {
33
+ apiUrl: url.toString().replace(/\/+$/u, ''),
34
+ workspaceId: requiredText(candidate.workspaceId ?? 'brainbase', 'workspaceId')
35
+ };
36
+ }
37
+ export function honchoWorkspacePath(workspaceId) {
38
+ return `/v3/workspaces/${encodeURIComponent(requiredText(workspaceId, 'workspaceId'))}`;
39
+ }
40
+ export function createHonchoRequest(input) {
41
+ if (!['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].includes(input.method)) {
42
+ throw new HonchoContractError('method');
43
+ }
44
+ const path = requiredText(input.path, 'path');
45
+ if (!path.startsWith('/') || path.includes('://') || path.includes('\n') || path.includes('\r')) {
46
+ throw new HonchoContractError('path');
47
+ }
48
+ if (input.query) {
49
+ for (const [key, value] of Object.entries(input.query)) {
50
+ if (!key || value === undefined || value === null)
51
+ throw new HonchoContractError('query');
52
+ }
53
+ }
54
+ return {
55
+ method: input.method,
56
+ path,
57
+ query: input.query ? { ...input.query } : undefined,
58
+ body: input.body === undefined ? undefined : input.body === null ? null : structuredClone(input.body)
59
+ };
60
+ }
61
+ export function honchoRequestUrl(config, request) {
62
+ const normalized = normalizeHonchoConfig(config);
63
+ const safeRequest = createHonchoRequest(request);
64
+ const url = new URL(`${normalized.apiUrl}${safeRequest.path}`);
65
+ for (const [key, value] of Object.entries(safeRequest.query ?? {}))
66
+ url.searchParams.set(key, String(value));
67
+ return url.toString();
68
+ }
69
+ export function normalizeHonchoMessages(value) {
70
+ if (!Array.isArray(value) || value.length === 0)
71
+ throw new HonchoContractError('messages');
72
+ return value.map((item, index) => {
73
+ if (!item || typeof item !== 'object' || Array.isArray(item))
74
+ throw new HonchoContractError(`messages.${index}`);
75
+ const candidate = item;
76
+ return {
77
+ peer: requiredText(candidate.peer, `messages.${index}.peer`),
78
+ content: requiredText(candidate.content, `messages.${index}.content`)
79
+ };
80
+ });
81
+ }
@@ -0,0 +1,72 @@
1
+ import { type JudgmentExecutionOutcome } from './judgment-execution-outcome.js';
2
+ /** Input boundary for a completed Host episode. Private runtime fields are ignored. */
3
+ export interface JudgmentEpisode {
4
+ episode_id?: unknown;
5
+ session_id?: unknown;
6
+ turn_id?: unknown;
7
+ finalized_at?: unknown;
8
+ answer_digest?: unknown;
9
+ completion_status?: unknown;
10
+ final_summary?: unknown;
11
+ final_answer?: unknown;
12
+ summary?: unknown;
13
+ payload?: unknown;
14
+ redaction_status?: unknown;
15
+ execution_outcome?: unknown;
16
+ [key: string]: unknown;
17
+ }
18
+ export interface JudgmentKnowledgeEventPayload {
19
+ summary?: string;
20
+ redaction_status?: 'needs_redaction';
21
+ execution_outcome?: JudgmentExecutionOutcome;
22
+ }
23
+ export interface JudgmentKnowledgeEvent {
24
+ schema_version: 'knowledge_event.v1';
25
+ contract_version: 'knowledge_event.v1';
26
+ event_id: string;
27
+ occurred_at: string;
28
+ captured_at: string;
29
+ source: {
30
+ type: 'agent_judgment' | 'codex_judgment';
31
+ ref: string;
32
+ };
33
+ subject: {
34
+ type: 'judgment_episode';
35
+ id: string;
36
+ };
37
+ decision_authority: {
38
+ kind: 'judgment_receipt';
39
+ authorized: false;
40
+ graph_promotion_allowed: false;
41
+ };
42
+ applicability_scope: {
43
+ scope: 'judgment_episode';
44
+ project_code: 'brainbase';
45
+ };
46
+ permission_snapshot: {
47
+ knowledge_registration: true;
48
+ external_action: false;
49
+ };
50
+ source_pointer: {
51
+ uri: string;
52
+ };
53
+ body_hash: string;
54
+ parent_episode_id: string;
55
+ semantic_state?: 'quarantined';
56
+ payload: JudgmentKnowledgeEventPayload;
57
+ }
58
+ /** Remove audit lines and quarantine likely secrets from a judgment answer. */
59
+ export declare function sanitizeJudgmentAnswer(value: unknown): {
60
+ sensitive: true;
61
+ redaction_status: 'needs_redaction';
62
+ } | {
63
+ sensitive: false;
64
+ summary: string;
65
+ } | null;
66
+ /**
67
+ * Project a completed Host episode into a non-authoritative knowledge event.
68
+ *
69
+ * This is a portable projection only. Organization identity, Task binding,
70
+ * transcript storage, and delivery/outbox behavior remain adapter concerns.
71
+ */
72
+ export declare function toKnowledgeEventFromJudgmentEpisode(episode: JudgmentEpisode | null | undefined): JudgmentKnowledgeEvent | null;