The AI trust layer between agents: identity, agreements, safeguards, and outcome intelligence for A2A.

Viewing files at commit 66a0492edc4aCommit detailsView latest

packages/persistence/src/hosted.ts 4347 lines · 170.4 KB · typescript
1
import { neon, type NeonQueryFunction } from '@neondatabase/serverless';
2
import { createHash, randomBytes } from 'node:crypto';
3
import {
4
  buildCounterpartyBrief,
5
  buildInteractionConclusion,
6
  deriveBehaviouralObservations,
7
  evaluateLearningEligibility,
8
  summarizeContextualReliability,
9
  updateContextualBehaviouralProfile,
10
} from '../../core/src/index.js';
11
import {
12
  AgentIdentitySchema,
13
  AgentResolutionSchema,
14
  AssuranceDecisionSchema,
15
  AssuranceEffectivenessEvaluationSchema,
16
  AssuranceClaimOutcomeComparisonSchema,
17
  AssurancePredictionSnapshotSchema,
18
  AssuranceProbePlanSchema,
19
  AssuranceProbeResponseSchema,
20
  AssuranceSafeguardSchema,
21
  BehaviouralProfileDeltaSchema,
22
  DEFAULT_AGENT_AUTH_SCOPES,
23
  DEFAULT_EXTENSION_URI,
24
  CounterpartyBriefSchema,
25
  ContractRevisionSchema,
26
  FederatedInteractionSchema,
27
  FeedbackDimensionSchema,
28
  FeedbackRequestSchema,
29
  LiveSessionAcceptanceSchema,
30
  LiveSessionActivationSchema,
31
  LiveSessionEventSchema,
32
  LiveSessionOfferSchema,
33
  LiveSessionStateRecordSchema,
34
  InteractionCompletionReportSchema,
35
  InteractionContractSchema,
36
  InteractionConclusionSchema,
37
  InteractionFeedbackSchema,
38
  LearningEligibilityDecisionSchema,
39
  PublicAgentCardSchema,
40
  ReceiptSchema,
41
  canonicalHash,
42
  verifyObject,
43
  type FederatedInteraction,
44
  type AgentPresence,
45
  type AgentResolution,
46
  type AssuranceDecision,
47
  type AssuranceEffectivenessEvaluation,
48
  type AssuranceClaimOutcomeComparison,
49
  type AssurancePredictionSnapshot,
50
  type AssuranceProbePlan,
51
  type AssuranceProbeResponse,
52
  type BehaviouralProfileDelta,
53
  type CounterpartyBrief,
54
  type ContractRevision,
55
  type LiveSessionActivation,
56
  type LiveSessionEvent,
57
  type LiveSessionInsight,
58
  type InteractionCompletionReport,
59
  type InteractionContract,
60
  type InteractionFeedback,
61
  type InteractionConclusion,
62
  type PublicAgentCard,
63
} from '../../protocol/src/index.js';
64
import type { AgentProfile } from './onboarding.js';
65
import type { AgentInstallation } from './onboarding.js';
66
import {
67
  agentAccessTokenClientId,
68
  agentAccessTokenId,
69
  createAgentAccessToken,
70
  matchesAgentAccessToken,
71
  type AgentAccessTokenMetadata,
72
} from './access-token.js';
73
import {
74
  decryptGatewayPayload,
75
  encryptGatewayPayload,
76
  attestSessionRecord,
77
  getSessionKeyId,
78
  getSessionVerificationKey,
79
  issueSessionGrant,
80
  signSessionControl,
81
  verifySessionGrant,
82
} from './relay.js';
83
import { postRuntimeJson, resolvePublicRuntimeEndpoint } from './runtime.js';
84
import { verifyHostedMigrations } from './hosted-migrations.js';
85
import {
86
  buildSourceRecordEnvelope,
87
  shouldJournalSourceRecord,
88
  type SourceRecordWriteMetadata,
89
} from './source-record.js';
90
91
export type HostedRecordKind =
92
  | 'agent'
93
  | 'delegation'
94
  | 'contract'
95
  | 'interaction'
96
  | 'event'
97
  | 'receipt'
98
  | 'feedback'
99
  | 'counterparty_brief'
100
  | 'completion_report'
101
  | 'feedback_request'
102
  | 'interaction_feedback'
103
  | 'interaction_conclusion'
104
  | 'learning_eligibility'
105
  | 'profile_delta'
106
  | 'conflict'
107
  | 'profile'
108
  | 'consent'
109
  | 'revocation'
110
  | 'project'
111
  | 'agent_profile'
112
  | 'installation'
113
  | 'setup_request'
114
  | 'publication'
115
  | 'presence';
116
117
export type { PublicAgentCard, FederatedInteraction } from '../../protocol/src/index.js';
118
119
type ContextualProfile = {
120
  recordId: string;
121
  agentId: string;
122
  agentVersion: string;
123
  taskCategory: string;
124
  sampleSize: number;
125
  effectiveSampleSize?: number;
126
  dimensionSampleSizes?: Partial<
127
    Record<
128
      | 'completion'
129
      | 'acceptance'
130
      | 'specification'
131
      | 'deadline'
132
      | 'communication'
133
      | 'evidence'
134
      | 'scope'
135
      | 'correction'
136
      | 'limitations'
137
      | 'disputes',
138
      number
139
    >
140
  >;
141
  updatedAt: string;
142
  completion: number;
143
  acceptance: number;
144
  specification: number;
145
  deadline: number;
146
  communication: number;
147
  evidence: number;
148
  scope: number;
149
  correction: number;
150
  limitations: number;
151
  disputes: number;
152
};
153
154
const FEEDBACK_DIMENSIONS = FeedbackDimensionSchema.options;
155
156
function feedbackWindowMilliseconds() {
157
  const configured = Number(process.env.OPENCLASP_FEEDBACK_WINDOW_MINUTES ?? 120);
158
  const minutes = Number.isFinite(configured) ? Math.max(15, Math.min(1440, configured)) : 120;
159
  return minutes * 60_000;
160
}
161
162
function reviewerCredibility(feedback: InteractionFeedback): number {
163
  const provenance =
164
    feedback.submissionMethod === 'agent_signature'
165
      ? 0.9
166
      : feedback.submissionMethod === 'runtime_session'
167
        ? 0.8
168
        : feedback.submissionMethod === 'agent_access_token'
169
          ? 0.75
170
          : 0.65;
171
  return Math.min(1, provenance + (feedback.evidenceReferences.length ? 0.1 : 0));
172
}
173
174
function deterministicUuid(value: string): string {
175
  const bytes = createHash('sha256').update(value).digest().subarray(0, 16);
176
  bytes[6] = (bytes[6]! & 0x0f) | 0x50;
177
  bytes[8] = (bytes[8]! & 0x3f) | 0x80;
178
  const hex = bytes.toString('hex');
179
  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
180
}
181
182
type AssuranceLearningSummary = {
183
  sampleSize: number;
184
  averageBrierScore?: number;
185
  questionFamilies: Array<{
186
    questionFamily: AssuranceEffectivenessEvaluation['questionScores'][number]['questionFamily'];
187
    sampleSize: number;
188
    riskRevealRate: number;
189
    predictiveAccuracy: number;
190
    averageAbsolutePredictionDelta: number;
191
    utilityScore: number;
192
  }>;
193
  safeguardTypes: Array<{
194
    type: AssuranceEffectivenessEvaluation['safeguardScores'][number]['type'];
195
    sampleSize: number;
196
    positiveOutcomeRate: number;
197
  }>;
198
};
199
200
function summarizeAssuranceLearning(
201
  evaluations: AssuranceEffectivenessEvaluation[],
202
): AssuranceLearningSummary {
203
  const predictionScores = evaluations.flatMap((evaluation) => evaluation.predictionScores);
204
  const questionGroups = new Map<
205
    string,
206
    {
207
      questionFamily: AssuranceLearningSummary['questionFamilies'][number]['questionFamily'];
208
      sampleSize: number;
209
      riskReveals: number;
210
      predictiveScore: number;
211
      absolutePredictionDelta: number;
212
    }
213
  >();
214
  for (const evaluation of evaluations) {
215
    for (const question of evaluation.questionScores) {
216
      const group = questionGroups.get(question.questionFamily) ?? {
217
        questionFamily: question.questionFamily,
218
        sampleSize: 0,
219
        riskReveals: 0,
220
        predictiveScore: 0,
221
        absolutePredictionDelta: 0,
222
      };
223
      group.sampleSize += 1;
224
      if (question.exposedMaterialRisk) group.riskReveals += 1;
225
      const predictedOutcome = question.exposedMaterialRisk ? 0 : 1;
226
      group.predictiveScore += 1 - Math.abs(predictedOutcome - evaluation.outcomeValue);
227
      group.absolutePredictionDelta += Math.abs(question.predictionDelta);
228
      questionGroups.set(question.questionFamily, group);
229
    }
230
  }
231
  const safeguardGroups = new Map<
232
    string,
233
    {
234
      type: AssuranceLearningSummary['safeguardTypes'][number]['type'];
235
      sampleSize: number;
236
      positive: number;
237
    }
238
  >();
239
  for (const safeguard of evaluations.flatMap((evaluation) => evaluation.safeguardScores)) {
240
    if (safeguard.status !== 'accepted' && safeguard.status !== 'modified') continue;
241
    const group = safeguardGroups.get(safeguard.type) ?? {
242
      type: safeguard.type,
243
      sampleSize: 0,
244
      positive: 0,
245
    };
246
    group.sampleSize += 1;
247
    if (safeguard.outcomeAssociation === 'positive') group.positive += 1;
248
    safeguardGroups.set(safeguard.type, group);
249
  }
250
  return {
251
    sampleSize: evaluations.length,
252
    ...(predictionScores.length
253
      ? {
254
          averageBrierScore:
255
            predictionScores.reduce((sum, score) => sum + score.brierScore, 0) /
256
            predictionScores.length,
257
        }
258
      : {}),
259
    questionFamilies: [...questionGroups.values()]
260
      .map((group) => {
261
        const riskRevealRate = (group.riskReveals + 1) / (group.sampleSize + 2);
262
        const predictiveAccuracy = (group.predictiveScore + 1) / (group.sampleSize + 2);
263
        const averageAbsolutePredictionDelta = group.absolutePredictionDelta / group.sampleSize;
264
        return {
265
          questionFamily: group.questionFamily,
266
          sampleSize: group.sampleSize,
267
          riskRevealRate,
268
          predictiveAccuracy,
269
          averageAbsolutePredictionDelta,
270
          utilityScore: Math.min(
271
            1,
272
            riskRevealRate * 0.4 +
273
              predictiveAccuracy * 0.5 +
274
              Math.min(1, averageAbsolutePredictionDelta * 4) * 0.1,
275
          ),
276
        };
277
      })
278
      .sort((left, right) => right.utilityScore - left.utilityScore),
279
    safeguardTypes: [...safeguardGroups.values()]
280
      .map((group) => ({
281
        type: group.type,
282
        sampleSize: group.sampleSize,
283
        positiveOutcomeRate: (group.positive + 1) / (group.sampleSize + 2),
284
      }))
285
      .sort((left, right) => right.positiveOutcomeRate - left.positiveOutcomeRate),
286
  };
287
}
288
289
function assuranceClaimComparison(
290
  answer: AssuranceProbeResponse['answers'][number],
291
  report: InteractionCompletionReport,
292
) {
293
  const code = answer.questionCode;
294
  const claim = answer.answer;
295
  const normalized = String(claim).toLowerCase();
296
  const positive = ['true', 'yes', 'complete', 'all', 'accurate'].includes(normalized);
297
  const partial = ['partially', 'partial', 'some'].includes(normalized);
298
  const negative = ['false', 'no', 'none'].includes(normalized);
299
  const observedOutcome = `${report.outcome}: ${report.summary}`.slice(0, 500);
300
  let status: 'aligned' | 'partially_aligned' | 'contradicted' | 'unverifiable' = 'unverifiable';
301
  if (/capability|commitment|complete|deliver|deadline/.test(code)) {
302
    if (report.outcome === 'success')
303
      status = positive
304
        ? 'aligned'
305
        : partial
306
          ? 'partially_aligned'
307
          : negative
308
            ? 'contradicted'
309
            : 'unverifiable';
310
    if (report.outcome === 'partial')
311
      status = partial ? 'aligned' : positive || negative ? 'partially_aligned' : 'unverifiable';
312
    if (report.outcome === 'failure' || report.outcome === 'cancelled')
313
      status = negative
314
        ? 'aligned'
315
        : partial
316
          ? 'partially_aligned'
317
          : positive
318
            ? 'contradicted'
319
            : 'unverifiable';
320
  } else if (/evidence/.test(code)) {
321
    const evidenceProvided = report.evidenceReferences.length > 0;
322
    status = evidenceProvided
323
      ? negative
324
        ? 'contradicted'
325
        : positive
326
          ? 'aligned'
327
          : 'partially_aligned'
328
      : positive
329
        ? 'contradicted'
330
        : negative
331
          ? 'aligned'
332
          : 'unverifiable';
333
  } else if (/depend|blocker|approval|tool/.test(code)) {
334
    const blockersObserved = report.blockers.length > 0;
335
    status =
336
      normalized === 'none'
337
        ? blockersObserved
338
          ? 'contradicted'
339
          : 'aligned'
340
        : blockersObserved
341
          ? 'aligned'
342
          : 'unverifiable';
343
  }
344
  return {
345
    questionCode: code,
346
    preTaskClaim: claim,
347
    observedOutcome,
348
    status,
349
    evidenceReferences: [
350
      ...new Set([...answer.evidenceReferences, ...report.evidenceReferences]),
351
    ].slice(0, 10),
352
  };
353
}
354
355
function normalizePublicAgentCard(value: unknown): PublicAgentCard {
356
  const current = PublicAgentCardSchema.safeParse(value);
357
  const baseUrl = (process.env.OPENCLASP_PUBLIC_URL ?? 'https://openclasp.vercel.app').replace(
358
    /\/$/,
359
    '',
360
  );
361
  if (current.success) {
362
    const slug = current.data.slug ?? publicAgentSlug(current.data.name, current.data.agentId);
363
    return PublicAgentCardSchema.parse({
364
      ...current.data,
365
      slug,
366
      profileUrl: `${baseUrl}/a/${encodeURIComponent(slug)}`,
367
      cardUrl: `${baseUrl}/agents/${encodeURIComponent(current.data.agentId)}/card.json`,
368
      a2aAgentCardUrl: `${baseUrl}/agents/${encodeURIComponent(current.data.agentId)}/a2a-agent-card.json`,
369
      extensionUri: current.data.extensionUri ?? DEFAULT_EXTENSION_URI,
370
      verification: {
371
        ...(current.data.verification ?? {
372
          status: 'verified',
373
          method: 'openclasp_oauth_account',
374
          verifiedAt: current.data.publishedAt,
375
        }),
376
        verificationKeyUrl: `${baseUrl}/.well-known/openclasp-session-key`,
377
      },
378
    });
379
  }
380
  const legacy = value as Record<string, unknown>;
381
  const agentId = String(legacy.agentId ?? '');
382
  const slug = publicAgentSlug(String(legacy.name ?? agentId), agentId);
383
  return PublicAgentCardSchema.parse({
384
    protocolVersion: '0.1',
385
    agentId,
386
    name: legacy.name,
387
    description: '',
388
    framework: legacy.framework,
389
    agentVersion: '1.0.0',
390
    capabilities: legacy.capabilities,
391
    limitations: legacy.limitations,
392
    assurance: legacy.assurance,
393
    transports:
394
      typeof legacy.a2aEndpoint === 'string'
395
        ? [
396
            {
397
              protocol: 'A2A/1.0',
398
              protocolBinding: 'JSONRPC',
399
              endpoint: legacy.a2aEndpoint,
400
              managedBy: 'agent',
401
            },
402
          ]
403
        : [],
404
    slug,
405
    profileUrl: `${baseUrl}/a/${encodeURIComponent(slug)}`,
406
    cardUrl: `${baseUrl}/agents/${encodeURIComponent(agentId)}/card.json`,
407
    a2aAgentCardUrl: `${baseUrl}/agents/${encodeURIComponent(agentId)}/a2a-agent-card.json`,
408
    extensionUri: DEFAULT_EXTENSION_URI,
409
    verification: {
410
      status: 'verified',
411
      method: 'openclasp_oauth_account',
412
      verifiedAt: legacy.publishedAt,
413
      verificationKeyUrl: `${baseUrl}/.well-known/openclasp-session-key`,
414
    },
415
    publishedAt: legacy.publishedAt,
416
    updatedAt: legacy.updatedAt,
417
  });
418
}
419
420
function publicAgentSlug(name: string, agentId: string) {
421
  const prefix =
422
    name
423
      .trim()
424
      .toLowerCase()
425
      .replace(/[^a-z0-9]+/g, '-')
426
      .replace(/^-|-$/g, '')
427
      .slice(0, 48) || 'agent';
428
  const suffix = createHash('sha256').update(agentId).digest('hex').slice(0, 8);
429
  return `${prefix}-${suffix}`;
430
}
431
432
export function buildPublicAgentCard(
433
  agent: AgentProfile,
434
  baseUrl: string,
435
  previous?: PublicAgentCard,
436
): PublicAgentCard {
437
  const now = new Date().toISOString();
438
  const root = baseUrl.replace(/\/$/, '');
439
  const slug = previous?.slug ?? publicAgentSlug(agent.name, agent.agentId);
440
  return PublicAgentCardSchema.parse({
441
    protocolVersion: '0.1',
442
    agentId: agent.agentId,
443
    name: agent.name,
444
    description: agent.description ?? '',
445
    framework: agent.framework,
446
    agentVersion: agent.agentVersion ?? '1.0.0',
447
    capabilities: agent.capabilities,
448
    limitations: agent.limitations,
449
    assurance: 'oauth_authenticated',
450
    agentMode: 'persistent_runtime',
451
    transports: agent.a2aEndpoint
452
      ? [
453
          {
454
            protocol: 'A2A/1.0',
455
            protocolBinding: 'JSONRPC',
456
            endpoint: agent.a2aEndpoint,
457
            managedBy: 'agent',
458
          },
459
        ]
460
      : [],
461
    slug,
462
    profileUrl: `${root}/a/${encodeURIComponent(slug)}`,
463
    cardUrl: `${root}/agents/${encodeURIComponent(agent.agentId)}/card.json`,
464
    a2aAgentCardUrl: `${root}/agents/${encodeURIComponent(agent.agentId)}/a2a-agent-card.json`,
465
    extensionUri: DEFAULT_EXTENSION_URI,
466
    verification: {
467
      status: 'verified',
468
      method: 'openclasp_oauth_account',
469
      verifiedAt: previous?.verification?.verifiedAt ?? previous?.publishedAt ?? now,
470
      verificationKeyUrl: `${root}/.well-known/openclasp-session-key`,
471
    },
472
    publishedAt: previous?.publishedAt ?? now,
473
    updatedAt: now,
474
  });
475
}
476
477
const normalized = (value: string) =>
478
  value
479
    .trim()
480
    .toLowerCase()
481
    .replace(/[^a-z0-9]+/g, '_');
482
483
export const AGENT_ONLINE_WINDOW_MS = 2 * 60_000;
484
485
export function resolveAgentPresence(lastSeenAt?: string, now = new Date()): AgentPresence {
486
  const online = Boolean(
487
    lastSeenAt && now.getTime() - Date.parse(lastSeenAt) <= AGENT_ONLINE_WINDOW_MS,
488
  );
489
  return {
490
    status: online ? 'online' : 'offline',
491
    ...(lastSeenAt ? { lastSeenAt } : {}),
492
    checkedAt: now.toISOString(),
493
  };
494
}
495
496
export function canAutoAcceptInteraction(
497
  agent: AgentProfile,
498
  interaction: FederatedInteraction,
499
): boolean {
500
  if (agent.autoAcceptPolicy !== 'safe_matching' || agent.status !== 'active') return false;
501
  const categories = new Set(
502
    [...(agent.autoAcceptTaskCategories ?? []), ...(agent.capabilities ?? [])].map(normalized),
503
  );
504
  const taskCategory = normalized(interaction.contract.taskCategory);
505
  if (!taskCategory || !categories.has(taskCategory)) return false;
506
  if (interaction.contract.allowedData.length > 0) return false;
507
  if (
508
    /\b(password|secret|api[_ -]?key|access[_ -]?token|credential|bank|payment|medical|health|ssn|personal[_ -]?data|customer[_ -]?record)\b/i.test(
509
      `${interaction.contract.purpose} ${interaction.contract.requestedOutcome}`,
510
    )
511
  )
512
    return false;
513
  if (interaction.contract.humanApprovalRequirements.length > 0) return false;
514
  if (interaction.contract.retentionDays > 30) return false;
515
  if (interaction.contract.allowedActions.length > 0) {
516
    const capabilities = new Set((agent.capabilities ?? []).map(normalized));
517
    if (
518
      !interaction.contract.allowedActions.every((action) => capabilities.has(normalized(action)))
519
    )
520
      return false;
521
  }
522
  return true;
523
}
524
525
function withContractRevisionHistory(value: FederatedInteraction): FederatedInteraction {
526
  if (value.contractRevisions.length) return value;
527
  const status =
528
    value.status === 'pending'
529
      ? 'proposed'
530
      : ['active', 'completed'].includes(value.status)
531
        ? 'accepted'
532
        : 'rejected';
533
  const revision = ContractRevisionSchema.parse({
534
    revisionId: deterministicUuid(`${value.interactionId}:contract:1:${value.termsHash}`),
535
    interactionId: value.interactionId,
536
    revision: value.contractRevision,
537
    termsHash: value.termsHash,
538
    contract: value.contract,
539
    proposedByAgentId: value.initiatorAgentId,
540
    status,
541
    acceptances: value.acceptances,
542
    createdAt: value.createdAt,
543
    updatedAt: value.updatedAt,
544
    expiresAt: value.expiresAt,
545
  });
546
  return FederatedInteractionSchema.parse({ ...value, contractRevisions: [revision] });
547
}
548
549
function openContractRevision(value: FederatedInteraction): ContractRevision | undefined {
550
  return [...value.contractRevisions].reverse().find((revision) => revision.status === 'proposed');
551
}
552
553
export type AccountSettings = {
554
  displayName: string;
555
  contributionEnabled: boolean;
556
  rawConversationsStored: false;
557
};
558
559
const defaults: AccountSettings = {
560
  displayName: '',
561
  contributionEnabled: false,
562
  rawConversationsStored: false,
563
};
564
565
export class HostedRepository {
566
  private readonly sql: NeonQueryFunction<false, false>;
567
  private initialized?: Promise<void>;
568
569
  constructor(databaseUrl: string) {
570
    this.sql = neon(databaseUrl);
571
  }
572
573
  private gatewaySecret() {
574
    const value = process.env.OPENCLASP_RELAY_ENCRYPTION_KEY;
575
    if (!value || value.length < 32) throw new Error('Gateway encryption is not configured');
576
    return value;
577
  }
578
579
  private gatewaySecrets() {
580
    const active = this.gatewaySecret();
581
    const previous = (process.env.OPENCLASP_RELAY_PREVIOUS_KEYS ?? '')
582
      .split(',')
583
      .map((value) => value.trim())
584
      .filter(Boolean);
585
    if (previous.some((value) => value.length < 32))
586
      throw new Error('A previous gateway key is too short');
587
    if (previous.length > 3) throw new Error('At most three previous gateway keys are supported');
588
    return [...new Set([active, ...previous])];
589
  }
590
591
  private decryptGatewayPayload(encrypted: { ciphertext: string; iv: string; authTag: string }) {
592
    for (const secret of this.gatewaySecrets()) {
593
      try {
594
        return decryptGatewayPayload(secret, encrypted);
595
      } catch {
596
        // Try retained rotation keys without exposing which key failed.
597
      }
598
    }
599
    throw new Error('Encrypted gateway payload could not be decrypted');
600
  }
601
602
  private verifySessionGrant(token: string) {
603
    for (const secret of this.gatewaySecrets()) {
604
      try {
605
        return verifySessionGrant(secret, token);
606
      } catch {
607
        // Short-lived sessions remain valid during an orderly key rotation.
608
      }
609
    }
610
    throw new Error('Invalid or expired live-session credential');
611
  }
612
613
  private attestPublicAgentCard(value: PublicAgentCard): PublicAgentCard {
614
    const unsigned = { ...value };
615
    delete unsigned.platformAttestation;
616
    return PublicAgentCardSchema.parse({
617
      ...unsigned,
618
      platformAttestation: attestSessionRecord(this.gatewaySecret(), unsigned),
619
    });
620
  }
621
622
  ensureSchema(): Promise<void> {
623
    return (this.initialized ??= verifyHostedMigrations(this.sql));
624
  }
625
626
  private async appendSourceRecord(
627
    operatorId: string,
628
    kind: string,
629
    recordId: string,
630
    payload: unknown,
631
    metadata?: SourceRecordWriteMetadata,
632
  ) {
633
    const source = buildSourceRecordEnvelope({
634
      operatorId,
635
      kind,
636
      recordId,
637
      payload,
638
      ...(metadata ? { metadata } : {}),
639
    });
640
    await this.sql`
641
      INSERT INTO openclasp_source_records(
642
        event_id, operator_id, kind, record_id, schema_name, schema_version, payload,
643
        payload_digest, entity_refs, provenance, visibility, retention_class, learning_scope,
644
        reported_at, ingested_at
645
      ) VALUES (
646
        ${source.eventId}, ${source.operatorId}, ${source.kind}, ${source.recordId},
647
        ${source.schemaName}, ${source.schemaVersion}, ${JSON.stringify(source.payload)}::jsonb,
648
        ${source.payloadDigest}, ${JSON.stringify(source.entityRefs)}::jsonb,
649
        ${source.provenance}, ${source.visibility}, ${source.retentionClass},
650
        ${source.learningScope}, ${source.reportedAt}, ${source.ingestedAt}
651
      )
652
      ON CONFLICT (operator_id, kind, record_id, payload_digest) DO NOTHING
653
    `;
654
  }
655
656
  private async interactionOperatorIds(interactionId: string): Promise<string[]> {
657
    const rows = await this.sql`
658
      SELECT initiator_operator_id, responder_operator_id
659
      FROM openclasp_federated_interactions
660
      WHERE interaction_id = ${interactionId}
661
      LIMIT 1
662
    `;
663
    if (!rows[0]) return [];
664
    return [
665
      ...new Set([String(rows[0].initiator_operator_id), String(rows[0].responder_operator_id)]),
666
    ];
667
  }
668
669
  private async journalFederatedInteraction(interactionId: string) {
670
    const rows = await this.sql`
671
      SELECT payload, initiator_operator_id, responder_operator_id
672
      FROM openclasp_federated_interactions
673
      WHERE interaction_id = ${interactionId}
674
      LIMIT 1
675
    `;
676
    if (!rows[0]) return;
677
    const payload = FederatedInteractionSchema.parse(rows[0].payload);
678
    const operatorIds = [
679
      ...new Set([String(rows[0].initiator_operator_id), String(rows[0].responder_operator_id)]),
680
    ];
681
    await Promise.all(
682
      operatorIds.map((operatorId) =>
683
        this.appendSourceRecord(operatorId, 'federated_interaction', interactionId, payload, {
684
          schemaName: 'openclasp.federated_interaction',
685
          schemaVersion: '0.1',
686
          visibility: 'shared_participants',
687
          retentionClass: 'audit',
688
        }),
689
      ),
690
    );
691
  }
692
693
  private async journalLiveSessionState(interactionId: string) {
694
    const rows = await this.sql`
695
      SELECT session.interaction_id, session.initiator_agent_id, session.responder_agent_id,
696
        session.status, session.expires_at, session.created_at, session.activated_at,
697
        session.completed_at, interaction.initiator_operator_id,
698
        interaction.responder_operator_id
699
      FROM openclasp_live_sessions session
700
      INNER JOIN openclasp_federated_interactions interaction
701
        ON interaction.interaction_id = session.interaction_id
702
      WHERE session.interaction_id = ${interactionId}
703
      LIMIT 1
704
    `;
705
    if (!rows[0]) return;
706
    const row = rows[0];
707
    const payload = LiveSessionStateRecordSchema.parse({
708
      interactionId: String(row.interaction_id),
709
      initiatorAgentId: String(row.initiator_agent_id),
710
      responderAgentId: String(row.responder_agent_id),
711
      status: String(row.status),
712
      expiresAt: new Date(String(row.expires_at)).toISOString(),
713
      createdAt: new Date(String(row.created_at)).toISOString(),
714
      ...(row.activated_at
715
        ? { activatedAt: new Date(String(row.activated_at)).toISOString() }
716
        : {}),
717
      ...(row.completed_at
718
        ? { completedAt: new Date(String(row.completed_at)).toISOString() }
719
        : {}),
720
      ...(row.status === 'failed' ? { failureCode: 'session_failed' } : {}),
721
    });
722
    const operatorIds = [
723
      ...new Set([String(row.initiator_operator_id), String(row.responder_operator_id)]),
724
    ];
725
    await Promise.all(
726
      operatorIds.map((operatorId) =>
727
        this.appendSourceRecord(operatorId, 'live_session_state', interactionId, payload, {
728
          schemaName: 'openclasp.live_session_state',
729
          schemaVersion: '0.1',
730
          visibility: 'shared_participants',
731
          retentionClass: 'audit',
732
          reportedAt: payload.completedAt ?? payload.activatedAt ?? payload.createdAt,
733
        }),
734
      ),
735
    );
736
  }
737
738
  private async journalLiveSessionEvent(payload: Record<string, unknown>) {
739
    const interactionId = String(payload.interactionId ?? '');
740
    const eventId = String(payload.eventId ?? '');
741
    if (!interactionId || !eventId) return;
742
    const operatorIds = await this.interactionOperatorIds(interactionId);
743
    await Promise.all(
744
      operatorIds.map((operatorId) =>
745
        this.appendSourceRecord(operatorId, 'live_session_event', eventId, payload, {
746
          schemaName: 'openclasp.live_session_event',
747
          schemaVersion: '0.1',
748
          visibility: 'shared_participants',
749
          retentionClass: 'audit',
750
        }),
751
      ),
752
    );
753
  }
754
755
  getRuntimeVerificationKey() {
756
    const [active, ...previous] = this.gatewaySecrets();
757
    return {
758
      algorithm: 'Ed25519' as const,
759
      keyId: getSessionKeyId(active!),
760
      publicKey: getSessionVerificationKey(active!),
761
      previousKeys: previous.map((secret) => ({
762
        algorithm: 'Ed25519' as const,
763
        keyId: getSessionKeyId(secret),
764
        publicKey: getSessionVerificationKey(secret),
765
      })),
766
    };
767
  }
768
769
  async upsert(
770
    operatorId: string,
771
    kind: HostedRecordKind,
772
    recordId: string,
773
    payload: unknown,
774
    metadata?: SourceRecordWriteMetadata,
775
  ): Promise<void> {
776
    await this.ensureSchema();
777
    const encoded = JSON.stringify(payload);
778
    if (!(metadata?.journal ?? shouldJournalSourceRecord(kind))) {
779
      await this.sql`
780
        INSERT INTO openclasp_records(operator_id, kind, record_id, payload)
781
        VALUES (${operatorId}, ${kind}, ${recordId}, ${encoded}::jsonb)
782
        ON CONFLICT (operator_id, kind, record_id)
783
        DO UPDATE SET payload = EXCLUDED.payload, updated_at = NOW()
784
      `;
785
      return;
786
    }
787
    const source = buildSourceRecordEnvelope({
788
      operatorId,
789
      kind,
790
      recordId,
791
      payload,
792
      ...(metadata ? { metadata } : {}),
793
    });
794
    const sourcePayload = JSON.stringify(source.payload);
795
    const sourceEntityRefs = JSON.stringify(source.entityRefs);
796
    await this.sql.transaction((transaction) => [
797
      transaction`
798
        INSERT INTO openclasp_records(operator_id, kind, record_id, payload)
799
        VALUES (${operatorId}, ${kind}, ${recordId}, ${encoded}::jsonb)
800
        ON CONFLICT (operator_id, kind, record_id)
801
        DO UPDATE SET payload = EXCLUDED.payload, updated_at = NOW()
802
      `,
803
      transaction`
804
        INSERT INTO openclasp_source_records(
805
          event_id,
806
          operator_id,
807
          kind,
808
          record_id,
809
          schema_name,
810
          schema_version,
811
          payload,
812
          payload_digest,
813
          entity_refs,
814
          provenance,
815
          visibility,
816
          retention_class,
817
          learning_scope,
818
          reported_at,
819
          ingested_at
820
        ) VALUES (
821
          ${source.eventId},
822
          ${source.operatorId},
823
          ${source.kind},
824
          ${source.recordId},
825
          ${source.schemaName},
826
          ${source.schemaVersion},
827
          ${sourcePayload}::jsonb,
828
          ${source.payloadDigest},
829
          ${sourceEntityRefs}::jsonb,
830
          ${source.provenance},
831
          ${source.visibility},
832
          ${source.retentionClass},
833
          ${source.learningScope},
834
          ${source.reportedAt},
835
          ${source.ingestedAt}
836
        )
837
        ON CONFLICT (operator_id, kind, record_id, payload_digest) DO NOTHING
838
      `,
839
    ]);
840
  }
841
842
  async list(
843
    operatorId: string,
844
  ): Promise<{ kind: HostedRecordKind; recordId: string; payload: any }[]> {
845
    await this.ensureSchema();
846
    const rows = await this.sql`
847
      SELECT kind, record_id, payload
848
      FROM openclasp_records
849
      WHERE operator_id = ${operatorId}
850
      ORDER BY created_at ASC
851
    `;
852
    return rows.map((row) => ({
853
      kind: row.kind as HostedRecordKind,
854
      recordId: String(row.record_id),
855
      payload: row.payload,
856
    }));
857
  }
858
859
  async dashboard(operatorId: string) {
860
    await this.ensureSchema();
861
    const dueFeedback = await this.sql`
862
      SELECT 1 FROM openclasp_records
863
      WHERE operator_id = ${operatorId}
864
        AND kind = 'feedback_request'
865
        AND payload->>'status' = 'pending'
866
        AND (payload->>'dueAt')::timestamptz <= NOW()
867
      LIMIT 1
868
    `;
869
    if (dueFeedback.length) await this.processDueFeedback().catch(() => undefined);
870
    const outstanding = await this.sql`
871
      SELECT DISTINCT report.payload->>'interactionId' AS interaction_id
872
      FROM openclasp_records report
873
      INNER JOIN openclasp_federated_interactions interaction
874
        ON interaction.interaction_id::text = report.payload->>'interactionId'
875
      WHERE report.kind = 'completion_report'
876
        AND (
877
          interaction.initiator_operator_id = ${operatorId}
878
          OR interaction.responder_operator_id = ${operatorId}
879
        )
880
        AND NOT EXISTS (
881
          SELECT 1 FROM openclasp_records conclusion
882
          WHERE conclusion.operator_id = ${operatorId}
883
            AND conclusion.kind = 'interaction_conclusion'
884
            AND conclusion.payload->>'interactionId' = report.payload->>'interactionId'
885
        )
886
      LIMIT 20
887
    `;
888
    for (const row of outstanding) {
889
      const interactionId = String(row.interaction_id ?? '');
890
      if (interactionId)
891
        await this.finalizeInteractionConclusion(interactionId).catch(() => undefined);
892
    }
893
    const intelligenceBackfill = await this.sql`
894
      SELECT DISTINCT interaction.interaction_id
895
      FROM openclasp_federated_interactions interaction
896
      INNER JOIN openclasp_records conclusion
897
        ON conclusion.payload->>'interactionId' = interaction.interaction_id::text
898
        AND conclusion.kind = 'interaction_conclusion'
899
        AND conclusion.payload->>'lifecycle' = 'final'
900
      WHERE (
901
          interaction.initiator_operator_id = ${operatorId}
902
          OR interaction.responder_operator_id = ${operatorId}
903
        )
904
        AND EXISTS (
905
          SELECT 1 FROM openclasp_records eligibility
906
          WHERE eligibility.kind = 'learning_eligibility'
907
            AND eligibility.payload->>'interactionId' = interaction.interaction_id::text
908
            AND eligibility.payload->>'eligible' = 'true'
909
        )
910
        AND NOT EXISTS (
911
          SELECT 1 FROM openclasp_records delta
912
          WHERE delta.operator_id = ${operatorId}
913
            AND delta.kind = 'profile_delta'
914
            AND delta.payload->>'interactionId' = interaction.interaction_id::text
915
            AND delta.payload->>'agentId' = CASE
916
              WHEN interaction.initiator_operator_id = ${operatorId}
917
                THEN interaction.initiator_agent_id::text
918
              ELSE interaction.responder_agent_id::text
919
            END
920
        )
921
      ORDER BY interaction.interaction_id DESC
922
      LIMIT 20
923
    `;
924
    for (const row of intelligenceBackfill) {
925
      const interactionId = String(row.interaction_id ?? '');
926
      if (interactionId)
927
        await this.finalizeInteractionConclusion(interactionId).catch(() => undefined);
928
    }
929
    const rows = await this.list(operatorId);
930
    const [
931
      federatedInteractions,
932
      runtimes,
933
      accessTokens,
934
      liveSessionRows,
935
      liveEventRows,
936
      assuranceAssessmentRows,
937
      assurancePredictionRows,
938
      assuranceSafeguardRows,
939
      assuranceEvaluationRows,
940
      assurancePlanRows,
941
      assuranceResponseRows,
942
    ] = await Promise.all([
943
      this.listFederatedInteractions(operatorId),
944
      this.listAgentRuntimes(operatorId),
945
      this.listAgentAccessTokens(operatorId),
946
      this.sql`
947
        SELECT session.interaction_id, session.initiator_agent_id, session.responder_agent_id,
948
          session.status, session.created_at, session.activated_at, session.completed_at,
949
          session.expires_at, session.last_error
950
        FROM openclasp_live_sessions session
951
        INNER JOIN openclasp_federated_interactions interaction
952
          ON interaction.interaction_id = session.interaction_id
953
        WHERE interaction.initiator_operator_id = ${operatorId}
954
           OR interaction.responder_operator_id = ${operatorId}
955
        ORDER BY session.created_at DESC
956
      `,
957
      this.sql`
958
        SELECT events.event
959
        FROM openclasp_live_session_events events
960
        INNER JOIN openclasp_federated_interactions interaction
961
          ON interaction.interaction_id = events.interaction_id
962
        WHERE interaction.initiator_operator_id = ${operatorId}
963
           OR interaction.responder_operator_id = ${operatorId}
964
        ORDER BY events.created_at ASC
965
      `,
966
      this
967
        .sql`SELECT payload FROM openclasp_assurance_assessments WHERE operator_id = ${operatorId} ORDER BY created_at ASC`,
968
      this
969
        .sql`SELECT payload FROM openclasp_assurance_predictions WHERE operator_id = ${operatorId} ORDER BY created_at ASC`,
970
      this
971
        .sql`SELECT payload FROM openclasp_assurance_safeguards WHERE operator_id = ${operatorId} ORDER BY created_at ASC`,
972
      this
973
        .sql`SELECT payload FROM openclasp_assurance_evaluations WHERE operator_id = ${operatorId} ORDER BY created_at ASC`,
974
      this
975
        .sql`SELECT payload FROM openclasp_assurance_probe_plans WHERE operator_id = ${operatorId} ORDER BY created_at ASC`,
976
      this
977
        .sql`SELECT responses.payload FROM openclasp_assurance_probe_responses responses INNER JOIN openclasp_assurance_probe_plans plans ON plans.plan_id = responses.plan_id WHERE plans.operator_id = ${operatorId} ORDER BY responses.created_at ASC`,
978
    ]);
979
    const ofKind = (kind: HostedRecordKind) =>
980
      rows.filter((row) => row.kind === kind).map((row) => row.payload);
981
    const presence = new Map(
982
      rows
983
        .filter((row) => row.kind === 'presence')
984
        .map((row) => [row.recordId, String(row.payload.lastSeenAt)]),
985
    );
986
    const intelligenceSummaries = await this.listContextualIntelligence(operatorId);
987
    return {
988
      agents: [...ofKind('agent_profile'), ...ofKind('agent')].map((agent) => ({
989
        ...agent,
990
        presence: resolveAgentPresence(presence.get(String(agent.agentId))),
991
      })),
992
      projects: ofKind('project'),
993
      installations: ofKind('installation'),
994
      setupRequests: ofKind('setup_request'),
995
      publications: ofKind('publication'),
996
      interactions: ofKind('interaction'),
997
      events: [...ofKind('event'), ...liveEventRows.map((row) => row.event)],
998
      conflicts: ofKind('conflict'),
999
      receipts: ofKind('receipt'),
1000
      profiles: ofKind('profile'),
1001
      counterpartyBriefs: ofKind('counterparty_brief'),
1002
      completionReports: ofKind('completion_report'),
1003
      feedbackRequests: ofKind('feedback_request'),
1004
      interactionFeedback: ofKind('interaction_feedback'),
1005
      interactionConclusions: ofKind('interaction_conclusion'),
1006
      learningEligibility: ofKind('learning_eligibility'),
1007
      profileDeltas: ofKind('profile_delta'),
1008
      intelligenceSummaries,
1009
      assuranceAssessments: assuranceAssessmentRows.map((row) => row.payload),
1010
      assurancePredictions: assurancePredictionRows.map((row) => row.payload),
1011
      assuranceSafeguards: assuranceSafeguardRows.map((row) => row.payload),
1012
      assuranceEvaluations: assuranceEvaluationRows.map((row) => row.payload),
1013
      assuranceProbePlans: assurancePlanRows.map((row) => row.payload),
1014
      assuranceProbeResponses: assuranceResponseRows.map((row) => row.payload),
1015
      federatedInteractions,
1016
      liveSessions: liveSessionRows.map((row) => ({
1017
        interactionId: String(row.interaction_id),
1018
        initiatorAgentId: String(row.initiator_agent_id),
1019
        responderAgentId: String(row.responder_agent_id),
1020
        status: String(row.status),
1021
        createdAt: new Date(String(row.created_at)).toISOString(),
1022
        ...(row.activated_at
1023
          ? { activatedAt: new Date(String(row.activated_at)).toISOString() }
1024
          : {}),
1025
        ...(row.completed_at
1026
          ? { completedAt: new Date(String(row.completed_at)).toISOString() }
1027
          : {}),
1028
        expiresAt: new Date(String(row.expires_at)).toISOString(),
1029
        ...(row.last_error ? { lastError: String(row.last_error) } : {}),
1030
      })),
1031
      runtimes,
1032
      accessTokens,
1033
    };
1034
  }
1035
1036
  async listContextualIntelligence(
1037
    operatorId: string,
1038
    input: { agentId?: string; taskCategory?: string } = {},
1039
  ) {
1040
    await this.ensureSchema();
1041
    const [records, publicRows] = await Promise.all([
1042
      this.list(operatorId),
1043
      this
1044
        .sql`SELECT agent_id, card FROM openclasp_public_agents ORDER BY updated_at DESC LIMIT 1000`,
1045
    ]);
1046
    const currentVersions = new Map<string, string>();
1047
    for (const row of publicRows) {
1048
      const card = PublicAgentCardSchema.safeParse(normalizePublicAgentCard(row.card));
1049
      if (card.success) currentVersions.set(String(row.agent_id), card.data.agentVersion);
1050
    }
1051
    for (const record of records.filter((item) => item.kind === 'agent_profile')) {
1052
      const profile = record.payload as Partial<AgentProfile>;
1053
      if (typeof profile.agentId === 'string' && typeof profile.agentVersion === 'string')
1054
        currentVersions.set(profile.agentId, profile.agentVersion);
1055
    }
1056
    const deltas = records
1057
      .filter((record) => record.kind === 'profile_delta')
1058
      .map((record) => BehaviouralProfileDeltaSchema.safeParse(record.payload))
1059
      .filter((result) => result.success)
1060
      .map((result) => result.data);
1061
    return records
1062
      .filter((record) => record.kind === 'profile')
1063
      .map((record) => ({ recordId: record.recordId, ...record.payload }) as ContextualProfile)
1064
      .filter(
1065
        (profile) =>
1066
          (!input.agentId || profile.agentId === input.agentId) &&
1067
          (!input.taskCategory || profile.taskCategory === input.taskCategory),
1068
      )
1069
      .map((profile) =>
1070
        summarizeContextualReliability({
1071
          profile: {
1072
            ...profile,
1073
            effectiveSampleSize: profile.effectiveSampleSize ?? profile.sampleSize,
1074
          },
1075
          deltas: deltas.filter(
1076
            (delta) =>
1077
              delta.agentId === profile.agentId &&
1078
              delta.agentVersion === profile.agentVersion &&
1079
              delta.taskCategory === profile.taskCategory,
1080
          ),
1081
          currentAgentVersion: currentVersions.get(profile.agentId) ?? profile.agentVersion,
1082
        }),
1083
      )
1084
      .sort((left, right) => {
1085
        if (left.agentId !== right.agentId) return left.agentId.localeCompare(right.agentId);
1086
        if (left.versionStatus.status !== right.versionStatus.status)
1087
          return left.versionStatus.status === 'current' ? -1 : 1;
1088
        return right.confidence.value - left.confidence.value;
1089
      });
1090
  }
1091
1092
  async issueAgentAccessToken(
1093
    operatorId: string,
1094
    agentId: string,
1095
    input: { name: string; expiresInDays: number },
1096
  ): Promise<AgentAccessTokenMetadata & { token: string }> {
1097
    await this.ensureSchema();
1098
    const agents = await this.sql`
1099
      SELECT payload FROM openclasp_records
1100
      WHERE operator_id = ${operatorId} AND kind = 'agent_profile' AND record_id = ${agentId}
1101
      LIMIT 1
1102
    `;
1103
    const agent = agents[0]?.payload as AgentProfile | undefined;
1104
    if (!agent) throw new Error('Owned agent not found');
1105
    if (agent.status !== 'active') throw new Error('Revoked agents cannot receive access tokens');
1106
    const name = input.name.trim();
1107
    if (!name) throw new Error('Token name is required');
1108
    const { tokenId, token, tokenHash } = createAgentAccessToken();
1109
    const createdAt = new Date();
1110
    const expiresAt = new Date(createdAt.getTime() + input.expiresInDays * 86_400_000);
1111
    // The credential is bound to one agent. It may use MCP outbound and let that
1112
    // same agent register its own inbound runtime; it cannot manage other agents.
1113
    const scopes = DEFAULT_AGENT_AUTH_SCOPES;
1114
    await this.sql`
1115
      INSERT INTO openclasp_agent_access_tokens(
1116
        token_id, operator_id, agent_id, name, token_hash, scopes, expires_at, created_at
1117
      ) VALUES (
1118
        ${tokenId}, ${operatorId}, ${agentId}, ${name}, ${tokenHash},
1119
        ${JSON.stringify(scopes)}::jsonb, ${expiresAt.toISOString()}, ${createdAt.toISOString()}
1120
      )
1121
    `;
1122
    const clientId = agentAccessTokenClientId(tokenId);
1123
    await this.upsert(operatorId, 'installation', clientId, {
1124
      installationId: `install_${tokenId}`,
1125
      clientId,
1126
      agentId,
1127
      projectId: agent.projectId,
1128
      connectedAt: createdAt.toISOString(),
1129
      updatedAt: createdAt.toISOString(),
1130
    } satisfies AgentInstallation);
1131
    return {
1132
      tokenId,
1133
      token,
1134
      agentId,
1135
      name,
1136
      scopes,
1137
      createdAt: createdAt.toISOString(),
1138
      expiresAt: expiresAt.toISOString(),
1139
    };
1140
  }
1141
1142
  async listAgentAccessTokens(
1143
    operatorId: string,
1144
    agentId?: string,
1145
  ): Promise<AgentAccessTokenMetadata[]> {
1146
    await this.ensureSchema();
1147
    const rows = agentId
1148
      ? await this.sql`
1149
          SELECT token_id, agent_id, name, scopes, expires_at, last_used_at, revoked_at, created_at
1150
          FROM openclasp_agent_access_tokens
1151
          WHERE operator_id = ${operatorId} AND agent_id = ${agentId}
1152
          ORDER BY created_at DESC
1153
        `
1154
      : await this.sql`
1155
          SELECT token_id, agent_id, name, scopes, expires_at, last_used_at, revoked_at, created_at
1156
          FROM openclasp_agent_access_tokens
1157
          WHERE operator_id = ${operatorId}
1158
          ORDER BY created_at DESC
1159
        `;
1160
    return rows.map((row) => ({
1161
      tokenId: String(row.token_id),
1162
      agentId: String(row.agent_id),
1163
      name: String(row.name),
1164
      scopes: Array.isArray(row.scopes)
1165
        ? row.scopes.filter((scope): scope is string => typeof scope === 'string')
1166
        : [],
1167
      createdAt: new Date(String(row.created_at)).toISOString(),
1168
      expiresAt: new Date(String(row.expires_at)).toISOString(),
1169
      ...(row.last_used_at ? { lastUsedAt: new Date(String(row.last_used_at)).toISOString() } : {}),
1170
      ...(row.revoked_at ? { revokedAt: new Date(String(row.revoked_at)).toISOString() } : {}),
1171
    }));
1172
  }
1173
1174
  async revokeAgentAccessToken(operatorId: string, agentId: string, tokenId: string) {
1175
    await this.ensureSchema();
1176
    const rows = await this.sql`
1177
      UPDATE openclasp_agent_access_tokens
1178
      SET revoked_at = COALESCE(revoked_at, NOW())
1179
      WHERE token_id = ${tokenId} AND operator_id = ${operatorId} AND agent_id = ${agentId}
1180
      RETURNING revoked_at
1181
    `;
1182
    if (!rows.length) throw new Error('Agent access token not found');
1183
    const clientId = agentAccessTokenClientId(tokenId);
1184
    await this.sql`
1185
      DELETE FROM openclasp_records
1186
      WHERE operator_id = ${operatorId} AND kind = 'installation' AND record_id = ${clientId}
1187
    `;
1188
    return {
1189
      tokenId,
1190
      agentId,
1191
      revokedAt: new Date(String(rows[0]?.revoked_at)).toISOString(),
1192
    };
1193
  }
1194
1195
  async verifyAgentAccessToken(token: string) {
1196
    await this.ensureSchema();
1197
    const tokenId = agentAccessTokenId(token);
1198
    if (!tokenId) throw new Error('Invalid agent access token');
1199
    const rows = await this.sql`
1200
      SELECT tokens.operator_id, tokens.agent_id, tokens.token_hash, tokens.scopes,
1201
        tokens.expires_at, profile.payload AS profile
1202
      FROM openclasp_agent_access_tokens tokens
1203
      INNER JOIN openclasp_records profile
1204
        ON profile.operator_id = tokens.operator_id
1205
        AND profile.kind = 'agent_profile'
1206
        AND profile.record_id = tokens.agent_id
1207
      WHERE tokens.token_id = ${tokenId} AND tokens.revoked_at IS NULL
1208
      LIMIT 1
1209
    `;
1210
    const row = rows[0];
1211
    if (!row || !matchesAgentAccessToken(token, String(row.token_hash)))
1212
      throw new Error('Invalid agent access token');
1213
    if (Date.parse(String(row.expires_at)) <= Date.now())
1214
      throw new Error('Agent access token has expired');
1215
    const profile = row.profile as Partial<AgentProfile>;
1216
    if (profile.status !== 'active') throw new Error('Agent access token is disabled');
1217
    await this.sql`
1218
      UPDATE openclasp_agent_access_tokens
1219
      SET last_used_at = NOW()
1220
      WHERE token_id = ${tokenId}
1221
        AND (last_used_at IS NULL OR last_used_at < NOW() - INTERVAL '5 minutes')
1222
    `;
1223
    return {
1224
      tokenId,
1225
      operatorId: String(row.operator_id),
1226
      agentId: String(row.agent_id),
1227
      clientId: agentAccessTokenClientId(tokenId),
1228
      scopes: Array.isArray(row.scopes)
1229
        ? row.scopes.filter((scope): scope is string => typeof scope === 'string')
1230
        : [],
1231
      expiresAt: new Date(String(row.expires_at)).toISOString(),
1232
    };
1233
  }
1234
1235
  async publishAgent(operatorId: string, card: PublicAgentCard): Promise<PublicAgentCard> {
1236
    await this.ensureSchema();
1237
    card = PublicAgentCardSchema.parse(card);
1238
    const runtimes = await this.sql`
1239
      SELECT a2a_endpoint, endpoint FROM openclasp_agent_runtimes
1240
      WHERE operator_id = ${operatorId} AND agent_id = ${card.agentId} AND status = 'verified'
1241
    `;
1242
    if (!runtimes[0]) throw new Error('Connect and verify the agent runtime before publishing');
1243
    card = PublicAgentCardSchema.parse({
1244
      ...card,
1245
      agentMode: 'persistent_runtime',
1246
      transports: [
1247
        {
1248
          protocol: 'A2A/1.0',
1249
          protocolBinding: 'JSONRPC',
1250
          endpoint: String(runtimes[0].a2a_endpoint ?? runtimes[0].endpoint),
1251
          managedBy: 'agent',
1252
        },
1253
      ],
1254
    });
1255
    card = this.attestPublicAgentCard(card);
1256
    const encoded = JSON.stringify(card);
1257
    const rows = await this.sql`
1258
      INSERT INTO openclasp_public_agents(agent_id, operator_id, card, published_at, updated_at)
1259
      VALUES (${card.agentId}, ${operatorId}, ${encoded}::jsonb, NOW(), NOW())
1260
      ON CONFLICT (agent_id) DO UPDATE SET
1261
        card = EXCLUDED.card,
1262
        updated_at = NOW()
1263
      WHERE openclasp_public_agents.operator_id = ${operatorId}
1264
      RETURNING card
1265
    `;
1266
    const published = rows[0]?.card as PublicAgentCard | undefined;
1267
    if (!published) throw new Error('Agent ID is already owned by another operator');
1268
    return PublicAgentCardSchema.parse(published);
1269
  }
1270
1271
  async unpublishAgent(operatorId: string, agentId: string): Promise<boolean> {
1272
    await this.ensureSchema();
1273
    const rows = await this.sql`
1274
      DELETE FROM openclasp_public_agents
1275
      WHERE agent_id = ${agentId} AND operator_id = ${operatorId}
1276
      RETURNING agent_id
1277
    `;
1278
    return rows.length > 0;
1279
  }
1280
1281
  async getPublishedAgent(agentId: string): Promise<PublicAgentCard | undefined> {
1282
    await this.ensureSchema();
1283
    const rows = await this.sql`
1284
      SELECT agents.card, runtime.a2a_endpoint, runtime.endpoint, profile.payload AS profile
1285
      FROM openclasp_public_agents agents
1286
      INNER JOIN openclasp_agent_runtimes runtime
1287
        ON runtime.agent_id = agents.agent_id AND runtime.status = 'verified'
1288
      LEFT JOIN openclasp_records profile
1289
        ON profile.operator_id = agents.operator_id AND profile.kind = 'agent_profile'
1290
        AND profile.record_id = agents.agent_id
1291
      WHERE agents.agent_id = ${agentId}
1292
    `;
1293
    if (!rows[0]?.card) return undefined;
1294
    const card = PublicAgentCardSchema.parse({
1295
      ...normalizePublicAgentCard(rows[0].card),
1296
      agentMode: 'persistent_runtime',
1297
      transports: [
1298
        {
1299
          protocol: 'A2A/1.0',
1300
          protocolBinding: 'JSONRPC',
1301
          endpoint: String(rows[0].a2a_endpoint ?? rows[0].endpoint),
1302
          managedBy: 'agent',
1303
        },
1304
      ],
1305
    });
1306
    return this.attestPublicAgentCard({ ...card, presence: await this.getAgentPresence(agentId) });
1307
  }
1308
1309
  async resolveAgentReference(reference: string): Promise<AgentResolution | undefined> {
1310
    await this.ensureSchema();
1311
    const normalizedReference = reference.trim().replace(/\/$/, '');
1312
    if (!normalizedReference || normalizedReference.length > 2048)
1313
      throw new Error('Agent reference is invalid');
1314
    const rows = await this.sql`
1315
      SELECT agent_id, card
1316
      FROM openclasp_public_agents
1317
      WHERE agent_id = ${normalizedReference}
1318
        OR card->>'slug' = ${normalizedReference}
1319
        OR RTRIM(card->>'profileUrl', '/') = ${normalizedReference}
1320
        OR RTRIM(card->>'cardUrl', '/') = ${normalizedReference}
1321
        OR RTRIM(card->>'a2aAgentCardUrl', '/') = ${normalizedReference}
1322
      LIMIT 1
1323
    `;
1324
    let row = rows[0];
1325
    if (!row) {
1326
      const legacyRows = await this.sql`
1327
        SELECT agent_id, card
1328
        FROM openclasp_public_agents
1329
        ORDER BY updated_at DESC
1330
        LIMIT 1000
1331
      `;
1332
      row = legacyRows.find((candidate) => {
1333
        const card = normalizePublicAgentCard(candidate.card);
1334
        return [card.slug, card.profileUrl, card.cardUrl, card.a2aAgentCardUrl]
1335
          .filter((value): value is string => typeof value === 'string')
1336
          .some((value) => value.replace(/\/$/, '') === normalizedReference);
1337
      });
1338
    }
1339
    if (!row) return undefined;
1340
    const stored = normalizePublicAgentCard(row.card);
1341
    const matchedBy =
1342
      String(row.agent_id) === normalizedReference
1343
        ? 'agent_id'
1344
        : stored.slug === normalizedReference
1345
          ? 'slug'
1346
          : stored.profileUrl?.replace(/\/$/, '') === normalizedReference
1347
            ? 'profile_url'
1348
            : stored.cardUrl.replace(/\/$/, '') === normalizedReference
1349
              ? 'card_url'
1350
              : 'a2a_card_url';
1351
    const card = await this.getPublishedAgent(String(row.agent_id));
1352
    if (!card) return undefined;
1353
    return AgentResolutionSchema.parse({
1354
      reference,
1355
      matchedBy,
1356
      verified: true,
1357
      card,
1358
      resolvedAt: new Date().toISOString(),
1359
    });
1360
  }
1361
1362
  async searchPublishedAgents(input: {
1363
    query?: string | undefined;
1364
    capability?: string | undefined;
1365
    limit?: number | undefined;
1366
  }): Promise<PublicAgentCard[]> {
1367
    await this.ensureSchema();
1368
    const rows = await this.sql`
1369
      SELECT agents.card, runtime.a2a_endpoint, runtime.endpoint, profile.payload AS profile
1370
      FROM openclasp_public_agents agents
1371
      INNER JOIN openclasp_agent_runtimes runtime
1372
        ON runtime.agent_id = agents.agent_id AND runtime.status = 'verified'
1373
      LEFT JOIN openclasp_records profile
1374
        ON profile.operator_id = agents.operator_id AND profile.kind = 'agent_profile'
1375
        AND profile.record_id = agents.agent_id
1376
      ORDER BY agents.updated_at DESC LIMIT 100
1377
    `;
1378
    const query = input.query?.trim().toLowerCase();
1379
    const capability = input.capability?.trim().toLowerCase();
1380
    const presenceRows = await this.sql`
1381
      SELECT records.record_id, records.payload
1382
      FROM openclasp_records records
1383
      INNER JOIN openclasp_public_agents agents ON agents.agent_id = records.record_id
1384
      WHERE records.kind = 'presence'
1385
    `;
1386
    const presence = new Map(
1387
      presenceRows.map((row) => [String(row.record_id), String(row.payload.lastSeenAt)]),
1388
    );
1389
    return rows
1390
      .map((row) => {
1391
        const card = PublicAgentCardSchema.parse({
1392
          ...normalizePublicAgentCard(row.card),
1393
          agentMode: 'persistent_runtime',
1394
          transports: [
1395
            {
1396
              protocol: 'A2A/1.0',
1397
              protocolBinding: 'JSONRPC',
1398
              endpoint: String(row.a2a_endpoint ?? row.endpoint),
1399
              managedBy: 'agent',
1400
            },
1401
          ],
1402
        });
1403
        return this.attestPublicAgentCard({
1404
          ...card,
1405
          presence: resolveAgentPresence(presence.get(card.agentId)),
1406
        });
1407
      })
1408
      .filter(
1409
        (card) =>
1410
          (!query ||
1411
            card.agentId.toLowerCase().includes(query) ||
1412
            card.slug?.includes(query) ||
1413
            card.name.toLowerCase().includes(query) ||
1414
            card.description.toLowerCase().includes(query) ||
1415
            card.framework.toLowerCase().includes(query) ||
1416
            card.capabilities.some((value) => value.toLowerCase().includes(query)) ||
1417
            card.limitations.some((value) => value.toLowerCase().includes(query))) &&
1418
          (!capability ||
1419
            card.capabilities.some((value) => value.toLowerCase().includes(capability))),
1420
      )
1421
      .slice(0, Math.min(Math.max(input.limit ?? 20, 1), 50));
1422
  }
1423
1424
  async searchPersonalizedMarketplace(
1425
    operatorId: string,
1426
    input: {
1427
      agentId?: string;
1428
      taskCategory?: string;
1429
      query?: string;
1430
      limit?: number;
1431
    } = {},
1432
  ) {
1433
    const [cards, records, summaries] = await Promise.all([
1434
      this.searchPublishedAgents({ query: input.query, limit: input.limit }),
1435
      this.list(operatorId),
1436
      this.listContextualIntelligence(operatorId, {
1437
        ...(input.taskCategory ? { taskCategory: input.taskCategory } : {}),
1438
      }),
1439
    ]);
1440
    const ownedAgents = records
1441
      .filter((record) => record.kind === 'agent_profile')
1442
      .map((record) => record.payload as Partial<AgentProfile>);
1443
    const ownedIds = new Set(ownedAgents.map((agent) => agent.agentId));
1444
    const selected = ownedAgents.find((agent) => agent.agentId === input.agentId);
1445
    const taskCategory = input.taskCategory ?? selected?.capabilities?.[0] ?? 'general';
1446
    const normalizedCategory = taskCategory.toLowerCase();
1447
    const summaryFor = (agentId: string) =>
1448
      summaries.find(
1449
        (summary) =>
1450
          summary.agentId === agentId &&
1451
          summary.taskCategory.toLowerCase() === normalizedCategory &&
1452
          summary.versionStatus.status === 'current',
1453
      ) ?? summaries.find((summary) => summary.agentId === agentId);
1454
    return cards
1455
      .filter(
1456
        (card) =>
1457
          card.agentMode === 'persistent_runtime' &&
1458
          card.transports.some((transport) => transport.managedBy === 'agent'),
1459
      )
1460
      .filter((card) => !ownedIds.has(card.agentId))
1461
      .map((card) => {
1462
        const intelligence = summaryFor(card.agentId);
1463
        const capabilityMatch = card.capabilities.some((capability) =>
1464
          capability.toLowerCase().includes(normalizedCategory),
1465
        );
1466
        const online = card.presence?.status === 'online';
1467
        const fitScore = Math.min(
1468
          1,
1469
          0.25 +
1470
            (capabilityMatch ? 0.35 : 0) +
1471
            (online ? 0.1 : 0) +
1472
            (intelligence ? intelligence.score * intelligence.confidence.value * 0.3 : 0),
1473
        );
1474
        return {
1475
          card,
1476
          taskCategory,
1477
          ...(intelligence ? { contextualReliability: intelligence } : {}),
1478
          match: {
1479
            score: fitScore,
1480
            label: fitScore >= 0.72 ? 'strong' : fitScore >= 0.48 ? 'possible' : 'unproven',
1481
            reasons: [
1482
              capabilityMatch
1483
                ? `Published capability matches ${taskCategory}`
1484
                : `No explicit ${taskCategory} capability`,
1485
              intelligence
1486
                ? `${intelligence.confidence.evidenceCount} verified outcomes in private history`
1487
                : 'No verified private history for this task',
1488
              online ? 'Currently online' : 'Currently offline',
1489
            ],
1490
          },
1491
        };
1492
      })
1493
      .sort((left, right) => right.match.score - left.match.score);
1494
  }
1495
1496
  async touchAgentPresence(operatorId: string, agentId: string): Promise<AgentPresence> {
1497
    await this.ensureSchema();
1498
    const owned = await this.sql`
1499
      SELECT 1 FROM openclasp_records
1500
      WHERE operator_id = ${operatorId} AND kind = 'agent_profile' AND record_id = ${agentId}
1501
    `;
1502
    if (!owned.length) throw new Error('Agent is not owned by this account');
1503
    const lastSeenAt = new Date().toISOString();
1504
    await this.upsert(operatorId, 'presence', agentId, { lastSeenAt });
1505
    await this.sql`
1506
      UPDATE openclasp_agent_runtimes SET last_seen_at = ${lastSeenAt}, updated_at = NOW()
1507
      WHERE operator_id = ${operatorId} AND agent_id = ${agentId} AND status = 'verified'
1508
    `;
1509
    return resolveAgentPresence(lastSeenAt);
1510
  }
1511
1512
  async getAgentPresence(agentId: string): Promise<AgentPresence> {
1513
    await this.ensureSchema();
1514
    const rows = await this.sql`
1515
      SELECT records.payload
1516
      FROM openclasp_public_agents agents
1517
      LEFT JOIN openclasp_records records
1518
        ON records.operator_id = agents.operator_id
1519
        AND records.kind = 'presence'
1520
        AND records.record_id = agents.agent_id
1521
      WHERE agents.agent_id = ${agentId}
1522
      LIMIT 1
1523
    `;
1524
    if (!rows.length) throw new Error('Published agent not found');
1525
    const lastSeenAt = rows[0]?.payload?.lastSeenAt;
1526
    return resolveAgentPresence(typeof lastSeenAt === 'string' ? lastSeenAt : undefined);
1527
  }
1528
1529
  async listAgentRuntimes(operatorId: string) {
1530
    await this.ensureSchema();
1531
    const rows = await this.sql`
1532
      SELECT agent_id, endpoint, a2a_endpoint, status, verified_at, last_seen_at, last_error, updated_at
1533
      FROM openclasp_agent_runtimes
1534
      WHERE operator_id = ${operatorId}
1535
      ORDER BY updated_at DESC
1536
    `;
1537
    return rows.map((row) => ({
1538
      agentId: String(row.agent_id),
1539
      endpoint: String(row.endpoint),
1540
      a2aEndpoint: String(row.a2a_endpoint ?? row.endpoint),
1541
      status: row.status as 'verified' | 'disabled',
1542
      verifiedAt: new Date(String(row.verified_at)).toISOString(),
1543
      ...(row.last_seen_at ? { lastSeenAt: new Date(String(row.last_seen_at)).toISOString() } : {}),
1544
      ...(row.last_error ? { lastError: String(row.last_error) } : {}),
1545
      updatedAt: new Date(String(row.updated_at)).toISOString(),
1546
    }));
1547
  }
1548
1549
  async registerAgentRuntime(operatorId: string, agentId: string, endpoint: string) {
1550
    await this.ensureSchema();
1551
    const owned = await this.sql`
1552
      SELECT 1 FROM openclasp_records
1553
      WHERE operator_id = ${operatorId} AND kind = 'agent_profile' AND record_id = ${agentId}
1554
    `;
1555
    if (!owned.length) throw new Error('Agent is not owned by this account');
1556
    const challenge = crypto.randomUUID();
1557
    const verification = await postRuntimeJson(endpoint, {
1558
      type: 'openclasp.runtime.verify',
1559
      version: '1',
1560
      agentId,
1561
      challenge,
1562
    });
1563
    if (
1564
      verification.status < 200 ||
1565
      verification.status >= 300 ||
1566
      !verification.body ||
1567
      typeof verification.body !== 'object' ||
1568
      (verification.body as { type?: unknown }).type !== 'openclasp.runtime.verified' ||
1569
      (verification.body as { version?: unknown }).version !== '1' ||
1570
      (verification.body as { agentId?: unknown }).agentId !== agentId ||
1571
      (verification.body as { challenge?: unknown }).challenge !== challenge
1572
    )
1573
      throw new Error('Runtime endpoint did not return the verification challenge');
1574
    const a2aEndpointValue = (verification.body as { a2aEndpoint?: unknown }).a2aEndpoint;
1575
    const a2aEndpoint =
1576
      typeof a2aEndpointValue === 'string' && a2aEndpointValue ? a2aEndpointValue : endpoint;
1577
    await resolvePublicRuntimeEndpoint(a2aEndpoint);
1578
    const runtimeSecret = randomBytes(32).toString('base64url');
1579
    const encrypted = encryptGatewayPayload(this.gatewaySecret(), runtimeSecret);
1580
    const verifiedAt = new Date().toISOString();
1581
    const saved = await this.sql`
1582
      INSERT INTO openclasp_agent_runtimes(
1583
        agent_id, operator_id, endpoint, a2a_endpoint, secret_ciphertext, secret_iv, secret_auth_tag,
1584
        status, verified_at, last_seen_at, updated_at
1585
      ) VALUES (
1586
        ${agentId}, ${operatorId}, ${endpoint}, ${a2aEndpoint}, ${encrypted.ciphertext},
1587
        ${encrypted.iv}, ${encrypted.authTag}, 'verified', ${verifiedAt}, ${verifiedAt}, NOW()
1588
      )
1589
      ON CONFLICT (agent_id) DO UPDATE SET
1590
        operator_id = EXCLUDED.operator_id,
1591
        endpoint = EXCLUDED.endpoint,
1592
        a2a_endpoint = EXCLUDED.a2a_endpoint,
1593
        secret_ciphertext = EXCLUDED.secret_ciphertext,
1594
        secret_iv = EXCLUDED.secret_iv,
1595
        secret_auth_tag = EXCLUDED.secret_auth_tag,
1596
        status = 'verified',
1597
        verified_at = EXCLUDED.verified_at,
1598
        last_seen_at = EXCLUDED.last_seen_at,
1599
        last_error = NULL,
1600
        updated_at = NOW()
1601
      WHERE openclasp_agent_runtimes.operator_id = ${operatorId}
1602
      RETURNING agent_id
1603
    `;
1604
    if (!saved.length) throw new Error('Agent runtime is owned by another account');
1605
    await this.sql`
1606
      UPDATE openclasp_public_agents
1607
      SET card = jsonb_set(
1608
          jsonb_set(
1609
            card,
1610
            '{transports}',
1611
            ${JSON.stringify([
1612
              {
1613
                protocol: 'A2A/1.0',
1614
                protocolBinding: 'JSONRPC',
1615
                endpoint: a2aEndpoint,
1616
                managedBy: 'agent',
1617
              },
1618
            ])}::jsonb
1619
          ),
1620
          '{agentMode}', '"persistent_runtime"'::jsonb
1621
        ), updated_at = NOW()
1622
      WHERE agent_id = ${agentId} AND operator_id = ${operatorId}
1623
    `;
1624
    await this.sql`
1625
      UPDATE openclasp_records
1626
      SET payload = jsonb_set(
1627
          jsonb_set(payload, '{a2aEndpoint}', to_jsonb(${a2aEndpoint}::text)),
1628
          '{agentMode}', '"persistent_runtime"'::jsonb
1629
        ),
1630
        updated_at = NOW()
1631
      WHERE operator_id = ${operatorId} AND kind = 'agent_profile' AND record_id = ${agentId}
1632
    `;
1633
    await this.touchAgentPresence(operatorId, agentId);
1634
    return {
1635
      agentId,
1636
      endpoint,
1637
      a2aEndpoint,
1638
      status: 'verified' as const,
1639
      verifiedAt,
1640
      verificationKey: getSessionVerificationKey(this.gatewaySecret()),
1641
    };
1642
  }
1643
1644
  async disableAgentRuntime(operatorId: string, agentId: string) {
1645
    await this.ensureSchema();
1646
    const rows = await this.sql`
1647
      UPDATE openclasp_agent_runtimes
1648
      SET status = 'disabled', updated_at = NOW()
1649
      WHERE operator_id = ${operatorId} AND agent_id = ${agentId}
1650
      RETURNING agent_id
1651
    `;
1652
    if (!rows.length) throw new Error('Agent runtime not found');
1653
    await this.sql`
1654
      DELETE FROM openclasp_public_agents
1655
      WHERE operator_id = ${operatorId} AND agent_id = ${agentId}
1656
    `;
1657
    await this.sql`
1658
      UPDATE openclasp_records
1659
      SET payload = jsonb_set(
1660
        jsonb_set(payload, '{published}', 'false'::jsonb),
1661
        '{updatedAt}', to_jsonb(${new Date().toISOString()}::text)
1662
      ), updated_at = NOW()
1663
      WHERE operator_id = ${operatorId} AND kind = 'publication' AND record_id = ${agentId}
1664
    `;
1665
    return { agentId, status: 'disabled' as const, unpublished: true as const };
1666
  }
1667
1668
  async deleteAgent(operatorId: string, agentId: string) {
1669
    await this.ensureSchema();
1670
    const owned = await this.sql`
1671
      SELECT 1 FROM openclasp_records
1672
      WHERE operator_id = ${operatorId}
1673
        AND kind = 'agent_profile'
1674
        AND record_id = ${agentId}
1675
      LIMIT 1
1676
    `;
1677
    if (!owned.length) throw new Error('Owned agent not found');
1678
    const openInteractions = await this.sql`
1679
      SELECT interaction_id FROM openclasp_federated_interactions
1680
      WHERE (initiator_agent_id = ${agentId} OR responder_agent_id = ${agentId})
1681
        AND status IN ('pending', 'active')
1682
      LIMIT 1
1683
    `;
1684
    if (openInteractions.length) {
1685
      const error = new Error('Finish or reject the agent’s open interactions before deleting it');
1686
      Object.assign(error, { statusCode: 409 });
1687
      throw error;
1688
    }
1689
    await this.sql`
1690
      DELETE FROM openclasp_public_agents
1691
      WHERE operator_id = ${operatorId} AND agent_id = ${agentId}
1692
    `;
1693
    await this.sql`
1694
      DELETE FROM openclasp_agent_runtimes
1695
      WHERE operator_id = ${operatorId} AND agent_id = ${agentId}
1696
    `;
1697
    await this.sql`
1698
      DELETE FROM openclasp_agent_access_tokens
1699
      WHERE operator_id = ${operatorId} AND agent_id = ${agentId}
1700
    `;
1701
    await this.sql`
1702
      DELETE FROM openclasp_records
1703
      WHERE operator_id = ${operatorId}
1704
        AND (
1705
          (record_id = ${agentId} AND kind IN ('agent', 'agent_profile', 'publication', 'presence'))
1706
          OR (kind = 'installation' AND payload->>'agentId' = ${agentId})
1707
          OR (kind = 'setup_request' AND payload->>'existingAgentId' = ${agentId})
1708
        )
1709
    `;
1710
    return {
1711
      agentId,
1712
      deleted: true as const,
1713
      historyRetained: true as const,
1714
    };
1715
  }
1716
1717
  private async sessionParticipant(agentId: string) {
1718
    const rows = await this.sql`
1719
      SELECT agents.operator_id, agents.card, profile.payload AS profile,
1720
        runtime.endpoint, runtime.a2a_endpoint
1721
      FROM openclasp_public_agents agents
1722
      LEFT JOIN openclasp_records profile
1723
        ON profile.operator_id = agents.operator_id
1724
        AND profile.kind = 'agent_profile'
1725
        AND profile.record_id = agents.agent_id
1726
      INNER JOIN openclasp_agent_runtimes runtime
1727
        ON runtime.agent_id = agents.agent_id AND runtime.status = 'verified'
1728
      WHERE agents.agent_id = ${agentId}
1729
      LIMIT 1
1730
    `;
1731
    const row = rows[0];
1732
    if (!row) throw new Error(`Published agent ${agentId} was not found`);
1733
    const card = normalizePublicAgentCard(row.card);
1734
    if (!row.endpoint) throw new Error(`Agent ${agentId} does not have a verified live runtime`);
1735
    return {
1736
      agentId,
1737
      operatorId: String(row.operator_id),
1738
      mode: 'persistent_runtime' as const,
1739
      callbackEndpoint: String(row.endpoint),
1740
      a2aEndpoint: String(row.a2a_endpoint ?? row.endpoint),
1741
      card: PublicAgentCardSchema.parse({ ...card, agentMode: 'persistent_runtime' }),
1742
    };
1743
  }
1744
1745
  private async signedRuntimeRequest(
1746
    runtime: { callbackEndpoint: string },
1747
    requestId: string,
1748
    value: unknown,
1749
  ) {
1750
    const body = JSON.stringify(value);
1751
    const timestamp = new Date().toISOString();
1752
    return postRuntimeJson(runtime.callbackEndpoint, value, {
1753
      'openclasp-request-id': requestId,
1754
      'openclasp-timestamp': timestamp,
1755
      'openclasp-signature': `v1=${signSessionControl(this.gatewaySecret(), requestId, timestamp, body)}`,
1756
    });
1757
  }
1758
1759
  private sessionGrant(
1760
    interaction: FederatedInteraction,
1761
    senderAgentId: string,
1762
    recipientAgentId: string,
1763
  ) {
1764
    return issueSessionGrant(this.gatewaySecret(), {
1765
      interactionId: interaction.interactionId,
1766
      senderAgentId,
1767
      recipientAgentId,
1768
      expiresAt: Math.min(Date.parse(interaction.expiresAt), Date.now() + 60 * 60_000),
1769
    });
1770
  }
1771
1772
  private async privateCounterpartyInsights(
1773
    viewerOperatorId: string,
1774
    counterparty: { agentId: string; card: PublicAgentCard },
1775
    taskCategory: string,
1776
  ): Promise<{
1777
    insights: LiveSessionInsight[];
1778
    relevantSampleSize: number;
1779
    historyConfidence: number;
1780
  }> {
1781
    const rows = await this.sql`
1782
      SELECT record_id, payload
1783
      FROM openclasp_records
1784
      WHERE operator_id = ${viewerOperatorId}
1785
        AND kind = 'profile'
1786
        AND payload->>'agentId' = ${counterparty.agentId}
1787
        AND payload->>'taskCategory' = ${taskCategory}
1788
      ORDER BY (payload->>'sampleSize')::integer DESC, updated_at DESC
1789
    `;
1790
    const profiles: ContextualProfile[] = rows
1791
      .map<Record<string, unknown>>((row) => ({
1792
        recordId: String(row.record_id),
1793
        ...(row.payload as Record<string, unknown>),
1794
      }))
1795
      .filter(
1796
        (profile) =>
1797
          typeof profile.agentVersion === 'string' &&
1798
          typeof profile.sampleSize === 'number' &&
1799
          typeof profile.updatedAt === 'string' &&
1800
          typeof profile.completion === 'number' &&
1801
          typeof profile.acceptance === 'number' &&
1802
          typeof profile.specification === 'number' &&
1803
          typeof profile.deadline === 'number' &&
1804
          typeof profile.communication === 'number' &&
1805
          typeof profile.evidence === 'number' &&
1806
          typeof profile.scope === 'number' &&
1807
          typeof profile.disputes === 'number',
1808
      ) as ContextualProfile[];
1809
    const exact = profiles.find(
1810
      (profile) => profile.agentVersion === counterparty.card.agentVersion,
1811
    );
1812
    const current = exact ?? profiles[0];
1813
    const insights: LiveSessionInsight[] = [];
1814
    let relevantSampleSize = 0;
1815
    let historyConfidence = 0;
1816
    if (!current) {
1817
      insights.push({
1818
        code: 'limited_verified_history',
1819
        severity: 'caution',
1820
        message: `No eligible ${taskCategory} history is available for this agent version. Ask for task-relevant evidence.`,
1821
        evidenceReferences: [],
1822
        requirementReferences: [],
1823
      });
1824
    } else {
1825
      const ageDays = Math.max(
1826
        0,
1827
        (Date.now() - Date.parse(String(current.updatedAt))) / 86_400_000,
1828
      );
1829
      const freshness = Math.exp(-ageDays / 180);
1830
      const sampleSize = Number(current.sampleSize);
1831
      const effectiveSampleSize = Number(current.effectiveSampleSize ?? sampleSize);
1832
      const versionChanged = current.agentVersion !== counterparty.card.agentVersion;
1833
      const confidence = Math.min(
1834
        0.95,
1835
        (effectiveSampleSize / (effectiveSampleSize + 5)) * freshness * (versionChanged ? 0.35 : 1),
1836
      );
1837
      relevantSampleSize = sampleSize;
1838
      historyConfidence = confidence;
1839
      const reference = `openclasp:profile:${current.recordId}`;
1840
      insights.push({
1841
        code: versionChanged ? 'version_history_only' : 'contextual_history',
1842
        severity: versionChanged || confidence < 0.25 ? 'caution' : 'info',
1843
        message: versionChanged
1844
          ? `Only prior-version ${taskCategory} history is available (${sampleSize} eligible interaction${sampleSize === 1 ? '' : 's'}); confidence is reduced for version ${counterparty.card.agentVersion}.`
1845
          : `Based on ${sampleSize} eligible ${taskCategory} interaction${sampleSize === 1 ? '' : 's'}; confidence ${(confidence * 100).toFixed(0)}%.`,
1846
        evidenceReferences: [reference],
1847
        requirementReferences: [],
1848
      });
1849
      const weakDimensions = [
1850
        ['completion', Number(current.completion)],
1851
        ['output acceptance', Number(current.acceptance)],
1852
        ['specification adherence', Number(current.specification)],
1853
        ['deadline reliability', Number(current.deadline)],
1854
        ['communication quality', Number(current.communication)],
1855
        ['evidence quality', Number(current.evidence)],
1856
        ['scope adherence', Number(current.scope)],
1857
      ].filter(([, score]) => Number(score) < 0.7);
1858
      if (weakDimensions.length)
1859
        insights.push({
1860
          code: 'historical_weaknesses',
1861
          severity: weakDimensions.some(([, score]) => Number(score) < 0.5) ? 'high' : 'caution',
1862
          message: `Observed weaker areas: ${weakDimensions
1863
            .map(([name, score]) => `${String(name)} ${(Number(score) * 100).toFixed(0)}%`)
1864
            .join(', ')}.`,
1865
          evidenceReferences: [reference],
1866
          requirementReferences: [],
1867
        });
1868
      if (Number(current.disputes) > 0.25)
1869
        insights.push({
1870
          code: 'elevated_dispute_history',
1871
          severity: Number(current.disputes) >= 0.5 ? 'high' : 'caution',
1872
          message: `Eligible feedback shows a ${(Number(current.disputes) * 100).toFixed(0)}% dispute rate in this task category.`,
1873
          evidenceReferences: [reference],
1874
          requirementReferences: [],
1875
        });
1876
    }
1877
    if (counterparty.card.limitations.length)
1878
      insights.push({
1879
        code: 'declared_limitations',
1880
        severity: 'info',
1881
        message: `Counterparty declares: ${counterparty.card.limitations.join('; ')}`,
1882
        evidenceReferences: [counterparty.card.cardUrl],
1883
        requirementReferences: [],
1884
      });
1885
    return { insights, relevantSampleSize, historyConfidence };
1886
  }
1887
1888
  async brokerLiveSession(interaction: FederatedInteraction) {
1889
    await this.ensureSchema();
1890
    const existing = await this.sql`
1891
      SELECT status FROM openclasp_live_sessions
1892
      WHERE interaction_id = ${interaction.interactionId}
1893
    `;
1894
    if (existing[0]?.status === 'active') return;
1895
    const [initiator, responder] = await Promise.all([
1896
      this.sessionParticipant(interaction.initiatorAgentId),
1897
      this.sessionParticipant(interaction.responderAgentId),
1898
    ]);
1899
    const [initiatorContext, responderContext] = await Promise.all([
1900
      this.privateCounterpartyInsights(
1901
        initiator.operatorId,
1902
        responder,
1903
        interaction.contract.taskCategory,
1904
      ),
1905
      this.privateCounterpartyInsights(
1906
        responder.operatorId,
1907
        initiator,
1908
        interaction.contract.taskCategory,
1909
      ),
1910
    ]);
1911
    const issuedAt = new Date().toISOString();
1912
    const initiatorBrief = buildCounterpartyBrief({
1913
      interactionId: interaction.interactionId,
1914
      contractHash: interaction.termsHash,
1915
      contract: interaction.contract,
1916
      recipientAgentId: initiator.agentId,
1917
      subject: responder.card,
1918
      historyInsights: initiatorContext.insights,
1919
      relevantSampleSize: initiatorContext.relevantSampleSize,
1920
      historyConfidence: initiatorContext.historyConfidence,
1921
      generatedAt: issuedAt,
1922
      expiresAt: interaction.expiresAt,
1923
    });
1924
    const responderBrief = buildCounterpartyBrief({
1925
      interactionId: interaction.interactionId,
1926
      contractHash: interaction.termsHash,
1927
      contract: interaction.contract,
1928
      recipientAgentId: responder.agentId,
1929
      subject: initiator.card,
1930
      historyInsights: responderContext.insights,
1931
      relevantSampleSize: responderContext.relevantSampleSize,
1932
      historyConfidence: responderContext.historyConfidence,
1933
      generatedAt: issuedAt,
1934
      expiresAt: interaction.expiresAt,
1935
    });
1936
    await Promise.all([
1937
      this.upsert(
1938
        initiator.operatorId,
1939
        'counterparty_brief',
1940
        initiatorBrief.briefId,
1941
        initiatorBrief,
1942
      ),
1943
      this.upsert(
1944
        responder.operatorId,
1945
        'counterparty_brief',
1946
        responderBrief.briefId,
1947
        responderBrief,
1948
      ),
1949
    ]);
1950
    const makeOffer = (
1951
      runtime: typeof initiator,
1952
      counterparty: typeof responder,
1953
      role: 'initiator' | 'responder',
1954
      privateInsights: LiveSessionInsight[],
1955
      counterpartyBrief: CounterpartyBrief,
1956
    ) =>
1957
      LiveSessionOfferSchema.parse({
1958
        type: 'openclasp.session.offer',
1959
        version: '1',
1960
        offerId: crypto.randomUUID(),
1961
        interactionId: interaction.interactionId,
1962
        agentId: runtime.agentId,
1963
        role,
1964
        counterparty: {
1965
          agentId: counterparty.agentId,
1966
          name: counterparty.card.name,
1967
          agentVersion: counterparty.card.agentVersion,
1968
          capabilities: counterparty.card.capabilities,
1969
        },
1970
        contract: interaction.contract,
1971
        contractHash: interaction.termsHash,
1972
        privateInsights,
1973
        counterpartyBrief,
1974
        issuedAt,
1975
        expiresAt: interaction.expiresAt,
1976
      });
1977
    const initiatorOffer = makeOffer(
1978
      initiator,
1979
      responder,
1980
      'initiator',
1981
      initiatorBrief.insights,
1982
      initiatorBrief,
1983
    );
1984
    const responderOffer = makeOffer(
1985
      responder,
1986
      initiator,
1987
      'responder',
1988
      responderBrief.insights,
1989
      responderBrief,
1990
    );
1991
    const prepare = async (
1992
      participant: typeof initiator,
1993
      offer: typeof initiatorOffer,
1994
      label: 'Initiator' | 'Responder',
1995
    ) => {
1996
      if (!participant.callbackEndpoint) throw new Error(`${label} runtime is not configured`);
1997
      const response = await this.signedRuntimeRequest(
1998
        { callbackEndpoint: participant.callbackEndpoint },
1999
        offer.offerId,
2000
        offer,
2001
      );
2002
      if (response.status < 200 || response.status >= 300)
2003
        throw new Error(`${label} runtime is not live (HTTP ${response.status})`);
2004
      return LiveSessionAcceptanceSchema.parse(response.body);
2005
    };
2006
    const [initiatorAcceptance, responderAcceptance] = await Promise.all([
2007
      prepare(initiator, initiatorOffer, 'Initiator'),
2008
      prepare(responder, responderOffer, 'Responder'),
2009
    ]);
2010
    if (
2011
      initiatorAcceptance.offerId !== initiatorOffer.offerId ||
2012
      initiatorAcceptance.agentId !== initiator.agentId ||
2013
      responderAcceptance.offerId !== responderOffer.offerId ||
2014
      responderAcceptance.agentId !== responder.agentId
2015
    )
2016
      throw new Error('Runtime returned a mismatched live-session acceptance');
2017
    await Promise.all([
2018
      ...(initiator.mode === 'persistent_runtime'
2019
        ? [resolvePublicRuntimeEndpoint(initiatorAcceptance.a2aEndpoint)]
2020
        : []),
2021
      ...(responder.mode === 'persistent_runtime'
2022
        ? [resolvePublicRuntimeEndpoint(responderAcceptance.a2aEndpoint)]
2023
        : []),
2024
    ]);
2025
    await this.sql`
2026
      INSERT INTO openclasp_live_sessions(
2027
        interaction_id, initiator_agent_id, responder_agent_id,
2028
        initiator_session_id, responder_session_id,
2029
        initiator_endpoint, responder_endpoint, status, expires_at
2030
      ) VALUES (
2031
        ${interaction.interactionId}, ${interaction.initiatorAgentId}, ${interaction.responderAgentId},
2032
        ${initiatorAcceptance.sessionId}, ${responderAcceptance.sessionId},
2033
        ${initiatorAcceptance.a2aEndpoint}, ${responderAcceptance.a2aEndpoint},
2034
        'preparing', ${interaction.expiresAt}
2035
      )
2036
      ON CONFLICT (interaction_id) DO UPDATE SET
2037
        initiator_session_id = EXCLUDED.initiator_session_id,
2038
        responder_session_id = EXCLUDED.responder_session_id,
2039
        initiator_endpoint = EXCLUDED.initiator_endpoint,
2040
        responder_endpoint = EXCLUDED.responder_endpoint,
2041
        status = 'preparing', expires_at = EXCLUDED.expires_at,
2042
        activated_at = NULL, completed_at = NULL, last_error = NULL
2043
    `;
2044
    await this.journalLiveSessionState(interaction.interactionId);
2045
    const baseUrl = (process.env.OPENCLASP_PUBLIC_URL ?? 'https://openclasp.vercel.app').replace(
2046
      /\/$/,
2047
      '',
2048
    );
2049
    const activatedAt = new Date().toISOString();
2050
    const verificationKey = getSessionVerificationKey(this.gatewaySecret());
2051
    const makeActivation = (
2052
      agentId: string,
2053
      sessionId: string,
2054
      role: 'initiator' | 'responder',
2055
      peerAgentId: string,
2056
      peerSessionId: string,
2057
      peerEndpoint: string,
2058
      privateInsights: LiveSessionInsight[],
2059
      counterpartyBrief: CounterpartyBrief,
2060
    ): LiveSessionActivation => {
2061
      const bearerToken = this.sessionGrant(interaction, agentId, peerAgentId);
2062
      return LiveSessionActivationSchema.parse({
2063
        type: 'openclasp.session.activation',
2064
        version: '1',
2065
        activationId: crypto.randomUUID(),
2066
        interactionId: interaction.interactionId,
2067
        agentId,
2068
        sessionId,
2069
        role,
2070
        peer: {
2071
          agentId: peerAgentId,
2072
          sessionId: peerSessionId,
2073
          endpoint: peerEndpoint,
2074
          bearerToken,
2075
          verificationKey,
2076
        },
2077
        reporting: {
2078
          endpoint: `${baseUrl}/sessions/${encodeURIComponent(interaction.interactionId)}/events`,
2079
          completionEndpoint: `${baseUrl}/sessions/${encodeURIComponent(interaction.interactionId)}/completion-reports`,
2080
          feedbackEndpoint: `${baseUrl}/sessions/${encodeURIComponent(interaction.interactionId)}/feedback`,
2081
          assuranceResponseEndpoint: `${baseUrl}/sessions/${encodeURIComponent(interaction.interactionId)}/assurance-responses`,
2082
          bearerToken,
2083
        },
2084
        privateInsights,
2085
        counterpartyBrief,
2086
        contractHash: interaction.termsHash,
2087
        activatedAt,
2088
        expiresAt: interaction.expiresAt,
2089
      });
2090
    };
2091
    const initiatorActivation = makeActivation(
2092
      initiator.agentId,
2093
      initiatorAcceptance.sessionId,
2094
      'initiator',
2095
      responder.agentId,
2096
      responderAcceptance.sessionId,
2097
      responderAcceptance.a2aEndpoint,
2098
      initiatorBrief.insights,
2099
      initiatorBrief,
2100
    );
2101
    const responderActivation = makeActivation(
2102
      responder.agentId,
2103
      responderAcceptance.sessionId,
2104
      'responder',
2105
      initiator.agentId,
2106
      initiatorAcceptance.sessionId,
2107
      initiatorAcceptance.a2aEndpoint,
2108
      responderBrief.insights,
2109
      responderBrief,
2110
    );
2111
    try {
2112
      const activate = async (
2113
        participant: typeof initiator,
2114
        activation: LiveSessionActivation,
2115
        label: 'Initiator' | 'Responder',
2116
      ) => {
2117
        if (!participant.callbackEndpoint) throw new Error(`${label} runtime is not configured`);
2118
        const response = await this.signedRuntimeRequest(
2119
          { callbackEndpoint: participant.callbackEndpoint },
2120
          activation.activationId,
2121
          activation,
2122
        );
2123
        if (response.status < 200 || response.status >= 300)
2124
          throw new Error(`${label} activation failed with HTTP ${response.status}`);
2125
      };
2126
      await activate(responder, responderActivation, 'Responder');
2127
      await activate(initiator, initiatorActivation, 'Initiator');
2128
      await this.sql`
2129
        UPDATE openclasp_live_sessions
2130
        SET status = 'active', activated_at = NOW(), last_error = NULL
2131
        WHERE interaction_id = ${interaction.interactionId}
2132
      `;
2133
      await this.journalLiveSessionState(interaction.interactionId);
2134
      await this.sql`
2135
        UPDATE openclasp_agent_runtimes
2136
        SET last_seen_at = NOW(), last_error = NULL, updated_at = NOW()
2137
        WHERE agent_id IN (${initiator.agentId}, ${responder.agentId})
2138
      `;
2139
      await Promise.all([
2140
        this.touchAgentPresence(initiator.operatorId, initiator.agentId),
2141
        this.touchAgentPresence(responder.operatorId, responder.agentId),
2142
      ]);
2143
    } catch (error) {
2144
      const reason =
2145
        error instanceof Error ? error.message.slice(0, 500) : 'Session activation failed';
2146
      await this.sql`
2147
        UPDATE openclasp_live_sessions SET status = 'failed', last_error = ${reason}
2148
        WHERE interaction_id = ${interaction.interactionId}
2149
      `;
2150
      await this.journalLiveSessionState(interaction.interactionId);
2151
      throw error;
2152
    }
2153
  }
2154
2155
  async getLiveSession(operatorId: string, interactionId: string, agentId: string) {
2156
    await this.ensureSchema();
2157
    const rows = await this.sql`
2158
      SELECT session.*, interaction.payload,
2159
        initiator.operator_id AS initiator_operator_id,
2160
        responder.operator_id AS responder_operator_id
2161
      FROM openclasp_live_sessions session
2162
      INNER JOIN openclasp_federated_interactions interaction
2163
        ON interaction.interaction_id = session.interaction_id
2164
      INNER JOIN openclasp_public_agents initiator
2165
        ON initiator.agent_id = session.initiator_agent_id
2166
      INNER JOIN openclasp_public_agents responder
2167
        ON responder.agent_id = session.responder_agent_id
2168
      WHERE session.interaction_id = ${interactionId} AND session.status = 'active'
2169
      LIMIT 1
2170
    `;
2171
    const row = rows[0];
2172
    if (!row) throw new Error('Active live session not found');
2173
    const isInitiator = row.initiator_agent_id === agentId;
2174
    const isResponder = row.responder_agent_id === agentId;
2175
    if (!isInitiator && !isResponder) throw new Error('Agent is not a session participant');
2176
    const owner = isInitiator ? row.initiator_operator_id : row.responder_operator_id;
2177
    if (owner !== operatorId) throw new Error('Agent is not owned by this account');
2178
    const interaction = FederatedInteractionSchema.parse(row.payload);
2179
    const peerAgentId = String(isInitiator ? row.responder_agent_id : row.initiator_agent_id);
2180
    const peer = await this.sessionParticipant(peerAgentId);
2181
    const privateContext = await this.privateCounterpartyInsights(
2182
      String(owner),
2183
      peer,
2184
      interaction.contract.taskCategory,
2185
    );
2186
    const briefRows = await this.sql`
2187
      SELECT payload FROM openclasp_records
2188
      WHERE operator_id = ${String(owner)}
2189
        AND kind = 'counterparty_brief'
2190
        AND payload->>'interactionId' = ${interactionId}
2191
        AND payload->>'recipientAgentId' = ${agentId}
2192
      ORDER BY updated_at DESC
2193
      LIMIT 1
2194
    `;
2195
    const storedBrief = CounterpartyBriefSchema.safeParse(briefRows[0]?.payload);
2196
    const counterpartyBrief = storedBrief.success
2197
      ? storedBrief.data
2198
      : buildCounterpartyBrief({
2199
          interactionId,
2200
          contractHash: interaction.termsHash,
2201
          contract: interaction.contract,
2202
          recipientAgentId: agentId,
2203
          subject: peer.card,
2204
          historyInsights: privateContext.insights,
2205
          relevantSampleSize: privateContext.relevantSampleSize,
2206
          historyConfidence: privateContext.historyConfidence,
2207
          expiresAt: new Date(String(row.expires_at)).toISOString(),
2208
        });
2209
    const bearerToken = this.sessionGrant(interaction, agentId, peerAgentId);
2210
    return {
2211
      type: 'openclasp.session.activation' as const,
2212
      version: '1' as const,
2213
      activationId: crypto.randomUUID(),
2214
      interactionId,
2215
      agentId,
2216
      sessionId: String(isInitiator ? row.initiator_session_id : row.responder_session_id),
2217
      role: isInitiator ? ('initiator' as const) : ('responder' as const),
2218
      peer: {
2219
        agentId: peerAgentId,
2220
        sessionId: String(isInitiator ? row.responder_session_id : row.initiator_session_id),
2221
        endpoint: String(isInitiator ? row.responder_endpoint : row.initiator_endpoint),
2222
        bearerToken,
2223
        verificationKey: getSessionVerificationKey(this.gatewaySecret()),
2224
      },
2225
      reporting: {
2226
        endpoint: `${(process.env.OPENCLASP_PUBLIC_URL ?? 'https://openclasp.vercel.app').replace(/\/$/, '')}/sessions/${encodeURIComponent(interactionId)}/events`,
2227
        completionEndpoint: `${(process.env.OPENCLASP_PUBLIC_URL ?? 'https://openclasp.vercel.app').replace(/\/$/, '')}/sessions/${encodeURIComponent(interactionId)}/completion-reports`,
2228
        feedbackEndpoint: `${(process.env.OPENCLASP_PUBLIC_URL ?? 'https://openclasp.vercel.app').replace(/\/$/, '')}/sessions/${encodeURIComponent(interactionId)}/feedback`,
2229
        assuranceResponseEndpoint: `${(process.env.OPENCLASP_PUBLIC_URL ?? 'https://openclasp.vercel.app').replace(/\/$/, '')}/sessions/${encodeURIComponent(interactionId)}/assurance-responses`,
2230
        bearerToken,
2231
      },
2232
      privateInsights: privateContext.insights,
2233
      counterpartyBrief,
2234
      contractHash: interaction.termsHash,
2235
      activatedAt: row.activated_at
2236
        ? new Date(String(row.activated_at)).toISOString()
2237
        : new Date().toISOString(),
2238
      expiresAt: new Date(String(row.expires_at)).toISOString(),
2239
    };
2240
  }
2241
2242
  private async interactionParticipant(operatorId: string, interactionId: string, agentId: string) {
2243
    await this.ensureSchema();
2244
    const rows = await this.sql`
2245
      SELECT payload, initiator_operator_id, responder_operator_id,
2246
        initiator_agent_id, responder_agent_id
2247
      FROM openclasp_federated_interactions
2248
      WHERE interaction_id = ${interactionId}
2249
        AND (
2250
          (initiator_operator_id = ${operatorId} AND initiator_agent_id = ${agentId})
2251
          OR (responder_operator_id = ${operatorId} AND responder_agent_id = ${agentId})
2252
        )
2253
      LIMIT 1
2254
    `;
2255
    const row = rows[0];
2256
    if (!row) throw new Error('Interaction participant not found for this account');
2257
    const interaction = FederatedInteractionSchema.parse(row.payload);
2258
    const isInitiator = interaction.initiatorAgentId === agentId;
2259
    return {
2260
      interaction,
2261
      counterpartyAgentId: isInitiator
2262
        ? interaction.responderAgentId
2263
        : interaction.initiatorAgentId,
2264
      participantOperatorId: String(
2265
        isInitiator ? row.initiator_operator_id : row.responder_operator_id,
2266
      ),
2267
      counterpartyOperatorId: String(
2268
        isInitiator ? row.responder_operator_id : row.initiator_operator_id,
2269
      ),
2270
    };
2271
  }
2272
2273
  async getCounterpartyBrief(operatorId: string, interactionId: string, agentId: string) {
2274
    await this.interactionParticipant(operatorId, interactionId, agentId);
2275
    const rows = await this.sql`
2276
      SELECT payload FROM openclasp_records
2277
      WHERE operator_id = ${operatorId}
2278
        AND kind = 'counterparty_brief'
2279
        AND payload->>'interactionId' = ${interactionId}
2280
        AND payload->>'recipientAgentId' = ${agentId}
2281
      ORDER BY updated_at DESC
2282
      LIMIT 1
2283
    `;
2284
    const parsed = CounterpartyBriefSchema.safeParse(rows[0]?.payload);
2285
    if (!parsed.success) throw new Error('Counterparty brief is not available');
2286
    return parsed.data;
2287
  }
2288
2289
  private async getAssuranceLearning(
2290
    operatorId: string,
2291
    targetAgentId: string,
2292
    targetAgentVersion: string,
2293
    taskCategory: string,
2294
  ) {
2295
    const rows = await this.sql`
2296
      SELECT payload FROM openclasp_assurance_evaluations
2297
      WHERE operator_id = ${operatorId}
2298
        AND target_agent_id = ${targetAgentId}
2299
        AND target_agent_version = ${targetAgentVersion}
2300
        AND task_category = ${taskCategory}
2301
      ORDER BY created_at DESC
2302
      LIMIT 500
2303
    `;
2304
    const evaluations = rows
2305
      .map((row) => AssuranceEffectivenessEvaluationSchema.safeParse(row.payload))
2306
      .filter((parsed) => parsed.success)
2307
      .map((parsed) => parsed.data);
2308
    return summarizeAssuranceLearning(evaluations);
2309
  }
2310
2311
  async getAssuranceProbeContext(
2312
    operatorId: string,
2313
    interactionId: string,
2314
    generatedForAgentId: string,
2315
    targetAgentId: string,
2316
  ) {
2317
    const participant = await this.interactionParticipant(
2318
      operatorId,
2319
      interactionId,
2320
      generatedForAgentId,
2321
    );
2322
    if (participant.counterpartyAgentId !== targetAgentId)
2323
      throw new Error('Assurance probe target must be the authenticated agent’s counterparty');
2324
    if (participant.interaction.status !== 'active')
2325
      throw new Error('Assurance probes require an active interaction');
2326
    const targetCard = await this.getPublishedAgent(targetAgentId);
2327
    if (!targetCard) throw new Error('Assurance probe target is no longer published');
2328
    const brief = await this.getCounterpartyBrief(
2329
      operatorId,
2330
      interactionId,
2331
      generatedForAgentId,
2332
    ).catch(() => undefined);
2333
    const reportRows = await this.sql`
2334
      SELECT payload FROM openclasp_records
2335
      WHERE operator_id = ${operatorId}
2336
        AND kind = 'completion_report'
2337
        AND payload->>'interactionId' = ${interactionId}
2338
      ORDER BY updated_at DESC
2339
    `;
2340
    const completionReports = reportRows
2341
      .map((row) => InteractionCompletionReportSchema.safeParse(row.payload))
2342
      .filter((parsed) => parsed.success)
2343
      .map((parsed) => parsed.data);
2344
    const eventRows = await this.sql`
2345
      SELECT event FROM openclasp_live_session_events
2346
      WHERE interaction_id = ${interactionId}
2347
      ORDER BY created_at ASC
2348
      LIMIT 100
2349
    `;
2350
    const sessionEvents = eventRows
2351
      .map((row) => LiveSessionEventSchema.safeParse(row.event))
2352
      .filter((parsed) => parsed.success)
2353
      .map((parsed) => parsed.data);
2354
    const planRows = await this.sql`
2355
      SELECT payload FROM openclasp_assurance_probe_plans
2356
      WHERE operator_id = ${operatorId} AND interaction_id = ${interactionId}
2357
        AND generated_for_agent_id = ${generatedForAgentId} AND target_agent_id = ${targetAgentId}
2358
      ORDER BY created_at ASC
2359
    `;
2360
    const previousPlans = planRows.map((row) => AssuranceProbePlanSchema.parse(row.payload));
2361
    const responseRows = await this.sql`
2362
      SELECT responses.payload FROM openclasp_assurance_probe_responses responses
2363
      INNER JOIN openclasp_assurance_probe_plans plans ON plans.plan_id = responses.plan_id
2364
      WHERE plans.operator_id = ${operatorId} AND responses.interaction_id = ${interactionId}
2365
        AND responses.agent_id = ${targetAgentId}
2366
      ORDER BY responses.created_at ASC
2367
    `;
2368
    const previousResponses = responseRows.map((row) =>
2369
      AssuranceProbeResponseSchema.parse(row.payload),
2370
    );
2371
    const predictionRows = await this.sql`
2372
      SELECT payload FROM openclasp_assurance_predictions
2373
      WHERE operator_id = ${operatorId} AND interaction_id = ${interactionId}
2374
        AND target_agent_id = ${targetAgentId}
2375
      ORDER BY created_at ASC
2376
    `;
2377
    const previousPredictions = predictionRows.map((row) =>
2378
      AssurancePredictionSnapshotSchema.parse(row.payload),
2379
    );
2380
    const assuranceLearning = await this.getAssuranceLearning(
2381
      operatorId,
2382
      targetAgentId,
2383
      targetCard.agentVersion,
2384
      participant.interaction.contract.taskCategory,
2385
    );
2386
    return {
2387
      interaction: participant.interaction,
2388
      targetCard,
2389
      brief,
2390
      completionReports,
2391
      sessionEvents,
2392
      previousPlans,
2393
      previousResponses,
2394
      previousPredictions,
2395
      assuranceLearning,
2396
    };
2397
  }
2398
2399
  async beginAssuranceGeneration(record: {
2400
    generationId: string;
2401
    operatorId: string;
2402
    interactionId: string;
2403
    phase: 'pre_task' | 'post_task';
2404
    model: string;
2405
    promptVersion: string;
2406
    input: Record<string, unknown>;
2407
  }) {
2408
    await this.ensureSchema();
2409
    const participants = await this.sql`
2410
      SELECT 1 FROM openclasp_federated_interactions
2411
      WHERE interaction_id = ${record.interactionId}
2412
        AND ${record.operatorId} IN (initiator_operator_id, responder_operator_id)
2413
      LIMIT 1
2414
    `;
2415
    if (!participants.length) throw new Error('Assurance generation interaction is not accessible');
2416
    await this.sql`
2417
      INSERT INTO openclasp_ai_generations(
2418
        generation_id, operator_id, interaction_id, phase, model, prompt_version,
2419
        status, input, input_digest
2420
      ) VALUES (
2421
        ${record.generationId}, ${record.operatorId}, ${record.interactionId}, ${record.phase},
2422
        ${record.model}, ${record.promptVersion}, 'pending', ${JSON.stringify(record.input)}::jsonb,
2423
        ${canonicalHash(record.input)}
2424
      )
2425
    `;
2426
  }
2427
2428
  async finishAssuranceGeneration(
2429
    operatorId: string,
2430
    generationId: string,
2431
    value: {
2432
      status: 'complete' | 'fallback' | 'error';
2433
      output: AssuranceDecision;
2434
      tokenUsage?: { inputTokens?: number; outputTokens?: number; totalTokens?: number };
2435
      errorCode?: string;
2436
    },
2437
  ) {
2438
    const rows = await this.sql`
2439
      UPDATE openclasp_ai_generations
2440
      SET status = ${value.status}, output = ${JSON.stringify(value.output)}::jsonb,
2441
        token_usage = ${JSON.stringify(value.tokenUsage ?? {})}::jsonb,
2442
        error_code = ${value.errorCode ?? null}, completed_at = NOW()
2443
      WHERE generation_id = ${generationId} AND operator_id = ${operatorId} AND status = 'pending'
2444
      RETURNING generation_id
2445
    `;
2446
    if (!rows.length) throw new Error('Pending assurance generation not found');
2447
  }
2448
2449
  async saveAssuranceProbePlan(operatorId: string, value: AssuranceProbePlan) {
2450
    const plan = AssuranceProbePlanSchema.parse(value);
2451
    const participant = await this.interactionParticipant(
2452
      operatorId,
2453
      plan.interactionId,
2454
      plan.generatedForAgentId,
2455
    );
2456
    if (participant.counterpartyAgentId !== plan.targetAgentId)
2457
      throw new Error('Assurance probe target does not match the interaction counterparty');
2458
    if (participant.interaction.termsHash !== plan.contractHash)
2459
      throw new Error('Assurance probe contract hash is stale');
2460
    const rows = await this.sql`
2461
      INSERT INTO openclasp_assurance_probe_plans(
2462
        plan_id, generation_id, operator_id, interaction_id, contract_hash, phase,
2463
        generated_for_agent_id, target_agent_id, payload, expires_at
2464
      ) VALUES (
2465
        ${plan.planId}, ${plan.generation.generationId}, ${operatorId}, ${plan.interactionId},
2466
        ${plan.contractHash}, ${plan.phase}, ${plan.generatedForAgentId}, ${plan.targetAgentId},
2467
        ${JSON.stringify(plan)}::jsonb, ${plan.expiresAt}
2468
      )
2469
      ON CONFLICT (plan_id) DO NOTHING
2470
      RETURNING payload
2471
    `;
2472
    if (!rows.length) throw new Error('Assurance probe plan already exists');
2473
    return AssuranceProbePlanSchema.parse(rows[0]!.payload);
2474
  }
2475
2476
  async saveAssuranceDecision(
2477
    operatorId: string,
2478
    decisionValue: AssuranceDecision,
2479
    planValue: AssuranceProbePlan,
2480
  ) {
2481
    const decision = AssuranceDecisionSchema.parse(decisionValue);
2482
    const plan = AssuranceProbePlanSchema.parse(planValue);
2483
    if (
2484
      decision.assessmentId !== plan.assessmentId ||
2485
      decision.prediction.predictionId !== plan.predictionBeforeId ||
2486
      decision.generation.generationId !== plan.generation.generationId
2487
    )
2488
      throw new Error('Assurance decision and probe plan are not linked');
2489
    await this.interactionParticipant(
2490
      operatorId,
2491
      decision.interactionId,
2492
      decision.generatedForAgentId,
2493
    );
2494
    await this.sql`
2495
      INSERT INTO openclasp_assurance_assessments(
2496
        assessment_id, generation_id, operator_id, interaction_id, phase, round,
2497
        target_agent_id, target_agent_version, payload
2498
      ) VALUES (
2499
        ${decision.assessmentId}, ${decision.generation.generationId}, ${operatorId},
2500
        ${decision.interactionId}, ${decision.phase}, ${decision.round}, ${decision.targetAgentId},
2501
        ${decision.targetAgentVersion}, ${JSON.stringify(decision)}::jsonb
2502
      )
2503
    `;
2504
    await this.sql`
2505
      INSERT INTO openclasp_assurance_predictions(
2506
        prediction_id, assessment_id, operator_id, interaction_id, target_agent_id,
2507
        target_agent_version, task_category, stage, success_probability, payload
2508
      ) VALUES (
2509
        ${decision.prediction.predictionId}, ${decision.assessmentId}, ${operatorId},
2510
        ${decision.interactionId}, ${decision.targetAgentId}, ${decision.targetAgentVersion},
2511
        ${decision.prediction.taskCategory}, ${decision.prediction.stage},
2512
        ${decision.prediction.successProbability}, ${JSON.stringify(decision.prediction)}::jsonb
2513
      )
2514
    `;
2515
    for (const safeguard of decision.safeguards) {
2516
      await this.sql`
2517
        INSERT INTO openclasp_assurance_safeguards(
2518
          safeguard_id, assessment_id, operator_id, interaction_id, target_agent_id, status, payload
2519
        ) VALUES (
2520
          ${safeguard.safeguardId}, ${decision.assessmentId}, ${operatorId},
2521
          ${decision.interactionId}, ${decision.targetAgentId}, ${safeguard.status},
2522
          ${JSON.stringify(safeguard)}::jsonb
2523
        )
2524
      `;
2525
    }
2526
    const savedPlan = await this.saveAssuranceProbePlan(operatorId, plan);
2527
    return { decision, plan: savedPlan };
2528
  }
2529
2530
  async listAssuranceProbePlans(operatorId: string, interactionId: string, agentId: string) {
2531
    await this.interactionParticipant(operatorId, interactionId, agentId);
2532
    const rows = await this.sql`
2533
      SELECT payload FROM openclasp_assurance_probe_plans
2534
      WHERE interaction_id = ${interactionId}
2535
        AND (generated_for_agent_id = ${agentId} OR target_agent_id = ${agentId})
2536
      ORDER BY created_at DESC
2537
    `;
2538
    return rows.map((row) => AssuranceProbePlanSchema.parse(row.payload));
2539
  }
2540
2541
  async getAssuranceBrief(operatorId: string, interactionId: string, agentId: string) {
2542
    await this.interactionParticipant(operatorId, interactionId, agentId);
2543
    const [assessments, predictions, safeguards, evaluations, plans, responses] = await Promise.all(
2544
      [
2545
        this
2546
          .sql`SELECT payload FROM openclasp_assurance_assessments WHERE operator_id = ${operatorId} AND interaction_id = ${interactionId} ORDER BY created_at ASC`,
2547
        this
2548
          .sql`SELECT payload FROM openclasp_assurance_predictions WHERE operator_id = ${operatorId} AND interaction_id = ${interactionId} ORDER BY created_at ASC`,
2549
        this
2550
          .sql`SELECT payload FROM openclasp_assurance_safeguards WHERE operator_id = ${operatorId} AND interaction_id = ${interactionId} ORDER BY created_at ASC`,
2551
        this
2552
          .sql`SELECT payload FROM openclasp_assurance_evaluations WHERE operator_id = ${operatorId} AND interaction_id = ${interactionId} ORDER BY created_at ASC`,
2553
        this
2554
          .sql`SELECT payload FROM openclasp_assurance_probe_plans WHERE operator_id = ${operatorId} AND interaction_id = ${interactionId} ORDER BY created_at ASC`,
2555
        this
2556
          .sql`SELECT responses.payload FROM openclasp_assurance_probe_responses responses INNER JOIN openclasp_assurance_probe_plans plans ON plans.plan_id = responses.plan_id WHERE plans.operator_id = ${operatorId} AND responses.interaction_id = ${interactionId} ORDER BY responses.created_at ASC`,
2557
      ],
2558
    );
2559
    const parsedAssessments = assessments.map((row) => AssuranceDecisionSchema.parse(row.payload));
2560
    const latestAssessment = parsedAssessments.at(-1);
2561
    const assuranceLearning = latestAssessment
2562
      ? await this.getAssuranceLearning(
2563
          operatorId,
2564
          latestAssessment.targetAgentId,
2565
          latestAssessment.targetAgentVersion,
2566
          latestAssessment.prediction.taskCategory,
2567
        )
2568
      : summarizeAssuranceLearning([]);
2569
    return {
2570
      assessments: parsedAssessments,
2571
      predictions: predictions.map((row) => AssurancePredictionSnapshotSchema.parse(row.payload)),
2572
      safeguards: safeguards.map((row) => AssuranceSafeguardSchema.parse(row.payload)),
2573
      evaluations: evaluations.map((row) =>
2574
        AssuranceEffectivenessEvaluationSchema.parse(row.payload),
2575
      ),
2576
      plans: plans.map((row) => AssuranceProbePlanSchema.parse(row.payload)),
2577
      responses: responses.map((row) => AssuranceProbeResponseSchema.parse(row.payload)),
2578
      assuranceLearning,
2579
      advisoryNotice: 'experimental_estimate_not_a_guarantee' as const,
2580
    };
2581
  }
2582
2583
  private async predictionAfterProbe(
2584
    operatorId: string,
2585
    plan: AssuranceProbePlan,
2586
    response: AssuranceProbeResponse,
2587
  ) {
2588
    const rows = await this.sql`
2589
      SELECT payload FROM openclasp_assurance_predictions
2590
      WHERE operator_id = ${operatorId} AND interaction_id = ${plan.interactionId}
2591
        AND target_agent_id = ${plan.targetAgentId}
2592
      ORDER BY created_at DESC LIMIT 1
2593
    `;
2594
    const prior = AssurancePredictionSnapshotSchema.safeParse(rows[0]?.payload);
2595
    if (!prior.success) return undefined;
2596
    const question = plan.questions[0];
2597
    const answer = response.answers.find((candidate) => candidate.probeId === question?.probeId);
2598
    if (!question || !answer) return undefined;
2599
    const normalized = String(answer.answer).trim().toLowerCase();
2600
    const signal = question.expectedSignals.find(
2601
      (candidate) => candidate.answer.trim().toLowerCase() === normalized,
2602
    );
2603
    const delta = signal?.probabilityDelta ?? 0;
2604
    const prediction = AssurancePredictionSnapshotSchema.parse({
2605
      ...prior.data,
2606
      predictionId: crypto.randomUUID(),
2607
      stage: 'after_probe',
2608
      successProbability: Math.max(0.05, Math.min(0.95, prior.data.successProbability + delta)),
2609
      confidence: Math.min(0.9, prior.data.confidence + 0.05 * answer.confidence),
2610
      priorPredictionId: prior.data.predictionId,
2611
      triggerResponseId: response.responseId,
2612
      createdAt: new Date().toISOString(),
2613
    });
2614
    await this.sql`
2615
      INSERT INTO openclasp_assurance_predictions(
2616
        prediction_id, assessment_id, operator_id, interaction_id, target_agent_id,
2617
        target_agent_version, task_category, stage, success_probability, payload
2618
      ) VALUES (
2619
        ${prediction.predictionId}, ${plan.assessmentId}, ${operatorId}, ${prediction.interactionId},
2620
        ${prediction.targetAgentId}, ${prediction.targetAgentVersion}, ${prediction.taskCategory},
2621
        ${prediction.stage}, ${prediction.successProbability}, ${JSON.stringify(prediction)}::jsonb
2622
      )
2623
    `;
2624
    return prediction;
2625
  }
2626
2627
  async decideAssuranceSafeguard(
2628
    operatorId: string,
2629
    interactionId: string,
2630
    agentId: string,
2631
    safeguardId: string,
2632
    status: 'accepted' | 'rejected' | 'modified',
2633
    decisionReason?: string,
2634
  ) {
2635
    await this.interactionParticipant(operatorId, interactionId, agentId);
2636
    const rows = await this.sql`
2637
      SELECT safeguard.payload, safeguard.assessment_id, assessment.payload AS assessment
2638
      FROM openclasp_assurance_safeguards safeguard
2639
      INNER JOIN openclasp_assurance_assessments assessment
2640
        ON assessment.assessment_id = safeguard.assessment_id
2641
      WHERE safeguard.operator_id = ${operatorId} AND safeguard.interaction_id = ${interactionId}
2642
        AND safeguard.safeguard_id = ${safeguardId}
2643
      LIMIT 1
2644
    `;
2645
    const row = rows[0];
2646
    if (!row) throw new Error('Assurance safeguard not found');
2647
    const assessment = AssuranceDecisionSchema.parse(row.assessment);
2648
    if (assessment.generatedForAgentId !== agentId)
2649
      throw new Error('Only the agent that requested the assessment may decide its safeguard');
2650
    const current = AssuranceSafeguardSchema.parse(row.payload);
2651
    if (current.status !== 'recommended')
2652
      throw new Error('Assurance safeguard was already decided');
2653
    const safeguard = AssuranceSafeguardSchema.parse({
2654
      ...current,
2655
      status,
2656
      ...(decisionReason ? { decisionReason } : {}),
2657
      decidedAt: new Date().toISOString(),
2658
    });
2659
    await this.sql`
2660
      UPDATE openclasp_assurance_safeguards
2661
      SET status = ${status}, payload = ${JSON.stringify(safeguard)}::jsonb, updated_at = NOW()
2662
      WHERE safeguard_id = ${safeguardId} AND operator_id = ${operatorId}
2663
    `;
2664
    let prediction: AssurancePredictionSnapshot | undefined;
2665
    if (status !== 'rejected') {
2666
      const priorRows = await this.sql`
2667
        SELECT payload FROM openclasp_assurance_predictions
2668
        WHERE operator_id = ${operatorId} AND interaction_id = ${interactionId}
2669
          AND target_agent_id = ${safeguard.targetAgentId}
2670
        ORDER BY created_at DESC LIMIT 1
2671
      `;
2672
      const prior = AssurancePredictionSnapshotSchema.safeParse(priorRows[0]?.payload);
2673
      if (prior.success) {
2674
        prediction = AssurancePredictionSnapshotSchema.parse({
2675
          ...prior.data,
2676
          predictionId: crypto.randomUUID(),
2677
          stage: 'after_safeguard',
2678
          successProbability: Math.min(
2679
            0.95,
2680
            prior.data.successProbability + Math.min(0.15, safeguard.expectedImpact),
2681
          ),
2682
          confidence: Math.min(0.9, prior.data.confidence + 0.03),
2683
          priorPredictionId: prior.data.predictionId,
2684
          createdAt: new Date().toISOString(),
2685
        });
2686
        await this.sql`
2687
          INSERT INTO openclasp_assurance_predictions(
2688
            prediction_id, assessment_id, operator_id, interaction_id, target_agent_id,
2689
            target_agent_version, task_category, stage, success_probability, payload
2690
          ) VALUES (
2691
            ${prediction.predictionId}, ${String(row.assessment_id)}, ${operatorId},
2692
            ${prediction.interactionId}, ${prediction.targetAgentId}, ${prediction.targetAgentVersion},
2693
            ${prediction.taskCategory}, ${prediction.stage}, ${prediction.successProbability},
2694
            ${JSON.stringify(prediction)}::jsonb
2695
          )
2696
        `;
2697
      }
2698
    }
2699
    return { safeguard, prediction, contractRevisionRequired: status !== 'rejected' };
2700
  }
2701
2702
  private async recalculateAssuranceComparison(
2703
    operatorId: string,
2704
    interactionId: string,
2705
    targetAgentId: string,
2706
  ): Promise<AssuranceClaimOutcomeComparison | undefined> {
2707
    const responseRows = await this.sql`
2708
      SELECT responses.response_id, responses.phase, responses.payload
2709
      FROM openclasp_assurance_probe_responses responses
2710
      INNER JOIN openclasp_assurance_probe_plans plans ON plans.plan_id = responses.plan_id
2711
      WHERE responses.interaction_id = ${interactionId}
2712
        AND responses.agent_id = ${targetAgentId}
2713
        AND plans.operator_id = ${operatorId}
2714
      ORDER BY responses.created_at DESC
2715
    `;
2716
    const preRow = responseRows.find((row) => row.phase === 'pre_task');
2717
    if (!preRow) return undefined;
2718
    const pre = AssuranceProbeResponseSchema.parse(preRow.payload);
2719
    const postRow = responseRows.find((row) => row.phase === 'post_task');
2720
    const post = postRow ? AssuranceProbeResponseSchema.parse(postRow.payload) : undefined;
2721
    const reportRows = await this.sql`
2722
      SELECT payload FROM openclasp_records
2723
      WHERE operator_id = ${operatorId}
2724
        AND kind = 'completion_report'
2725
        AND payload->>'interactionId' = ${interactionId}
2726
        AND payload->>'reportingAgentId' = ${targetAgentId}
2727
      ORDER BY updated_at DESC
2728
      LIMIT 1
2729
    `;
2730
    const report = InteractionCompletionReportSchema.safeParse(reportRows[0]?.payload);
2731
    if (!report.success) return undefined;
2732
    const comparison = AssuranceClaimOutcomeComparisonSchema.parse({
2733
      protocolVersion: '0.1',
2734
      comparisonId: deterministicUuid(`assurance-comparison:${operatorId}:${pre.responseId}`),
2735
      interactionId,
2736
      contractHash: pre.contractHash,
2737
      targetAgentId,
2738
      preTaskResponseId: pre.responseId,
2739
      ...(post ? { postTaskResponseId: post.responseId } : {}),
2740
      completionReportIds: [report.data.reportId],
2741
      comparisons: pre.answers.map((answer) => assuranceClaimComparison(answer, report.data)),
2742
      calculatedAt: new Date().toISOString(),
2743
    });
2744
    await this.sql`
2745
      INSERT INTO openclasp_assurance_claim_comparisons(
2746
        comparison_id, operator_id, interaction_id, target_agent_id,
2747
        pre_task_response_id, post_task_response_id, payload
2748
      ) VALUES (
2749
        ${comparison.comparisonId}, ${operatorId}, ${interactionId}, ${targetAgentId},
2750
        ${comparison.preTaskResponseId}, ${comparison.postTaskResponseId ?? null},
2751
        ${JSON.stringify(comparison)}::jsonb
2752
      )
2753
      ON CONFLICT (operator_id, interaction_id, target_agent_id, pre_task_response_id)
2754
      DO UPDATE SET post_task_response_id = EXCLUDED.post_task_response_id,
2755
        payload = EXCLUDED.payload, created_at = NOW()
2756
    `;
2757
    return comparison;
2758
  }
2759
2760
  async submitAssuranceProbeResponse(
2761
    operatorId: string,
2762
    agentId: string,
2763
    value: AssuranceProbeResponse,
2764
  ) {
2765
    const response = AssuranceProbeResponseSchema.parse(value);
2766
    const participant = await this.interactionParticipant(
2767
      operatorId,
2768
      response.interactionId,
2769
      agentId,
2770
    );
2771
    if (response.agentId !== agentId)
2772
      throw new Error('Assurance response identity does not match the authenticated agent');
2773
    if (participant.interaction.termsHash !== response.contractHash)
2774
      throw new Error('Assurance response contract hash is stale');
2775
    const planRows = await this.sql`
2776
      SELECT operator_id, target_agent_id, phase, payload, expires_at
2777
      FROM openclasp_assurance_probe_plans
2778
      WHERE plan_id = ${response.planId} AND interaction_id = ${response.interactionId}
2779
      LIMIT 1
2780
    `;
2781
    const row = planRows[0];
2782
    if (!row) throw new Error('Assurance probe plan not found');
2783
    const plan = AssuranceProbePlanSchema.parse(row.payload);
2784
    if (row.target_agent_id !== agentId || response.phase !== row.phase)
2785
      throw new Error('Assurance response does not match the targeted probe plan');
2786
    if (response.agentVersion !== plan.targetAgentVersion)
2787
      throw new Error('Assurance response agent version does not match the probe plan');
2788
    if (Date.parse(String(row.expires_at)) <= Date.now())
2789
      throw new Error('Assurance probe plan expired');
2790
    const answerIds = new Set(response.answers.map((answer) => answer.probeId));
2791
    for (const question of plan.questions) {
2792
      const answer = response.answers.find((candidate) => candidate.probeId === question.probeId);
2793
      if (question.required && !answer)
2794
        throw new Error(`Required assurance probe ${question.questionCode} is unanswered`);
2795
      if (!answer) continue;
2796
      if (
2797
        answer.questionCode !== question.questionCode ||
2798
        answer.responseType !== question.responseType ||
2799
        (question.responseType === 'enum' && !question.choices?.includes(String(answer.answer)))
2800
      )
2801
        throw new Error(`Assurance answer does not match probe ${question.questionCode}`);
2802
    }
2803
    if (
2804
      answerIds.size !== response.answers.length ||
2805
      response.answers.some(
2806
        (answer) => !plan.questions.some((question) => question.probeId === answer.probeId),
2807
      )
2808
    )
2809
      throw new Error('Assurance response contains duplicate or unknown probes');
2810
    const rows = await this.sql`
2811
      INSERT INTO openclasp_assurance_probe_responses(
2812
        response_id, plan_id, operator_id, interaction_id, contract_hash, phase, agent_id, payload
2813
      ) VALUES (
2814
        ${response.responseId}, ${response.planId}, ${operatorId}, ${response.interactionId},
2815
        ${response.contractHash}, ${response.phase}, ${agentId}, ${JSON.stringify(response)}::jsonb
2816
      )
2817
      ON CONFLICT (plan_id, agent_id) DO NOTHING
2818
      RETURNING payload
2819
    `;
2820
    if (!rows.length) throw new Error('This agent already answered the assurance probe plan');
2821
    const storedResponse = AssuranceProbeResponseSchema.parse(rows[0]!.payload);
2822
    const prediction = await this.predictionAfterProbe(
2823
      String(row.operator_id),
2824
      plan,
2825
      storedResponse,
2826
    );
2827
    const comparison = await this.recalculateAssuranceComparison(
2828
      String(row.operator_id),
2829
      response.interactionId,
2830
      agentId,
2831
    );
2832
    return { response: storedResponse, prediction, comparison };
2833
  }
2834
2835
  async recordSessionAssuranceResponse(token: string, value: AssuranceProbeResponse) {
2836
    const response = AssuranceProbeResponseSchema.parse(value);
2837
    const grant = this.verifySessionGrant(token);
2838
    if (grant.interactionId !== response.interactionId || grant.senderAgentId !== response.agentId)
2839
      throw new Error('Session credential does not match the assurance response');
2840
    const owners = await this.sql`
2841
      SELECT initiator_operator_id, responder_operator_id, initiator_agent_id
2842
      FROM openclasp_federated_interactions
2843
      WHERE interaction_id = ${response.interactionId}
2844
      LIMIT 1
2845
    `;
2846
    const row = owners[0];
2847
    if (!row) throw new Error('Interaction not found');
2848
    const operatorId = String(
2849
      row.initiator_agent_id === response.agentId
2850
        ? row.initiator_operator_id
2851
        : row.responder_operator_id,
2852
    );
2853
    return this.submitAssuranceProbeResponse(operatorId, response.agentId, response);
2854
  }
2855
2856
  async listAssuranceComparisons(operatorId: string, interactionId: string, agentId: string) {
2857
    await this.interactionParticipant(operatorId, interactionId, agentId);
2858
    const rows = await this.sql`
2859
      SELECT payload FROM openclasp_assurance_claim_comparisons
2860
      WHERE operator_id = ${operatorId} AND interaction_id = ${interactionId}
2861
      ORDER BY created_at DESC
2862
    `;
2863
    return rows.map((row) => AssuranceClaimOutcomeComparisonSchema.parse(row.payload));
2864
  }
2865
2866
  private async evaluateAssuranceOutcome(
2867
    operatorId: string,
2868
    report: InteractionCompletionReport,
2869
  ): Promise<AssuranceEffectivenessEvaluation | undefined> {
2870
    const [predictionRows, planRows, responseRows, safeguardRows] = await Promise.all([
2871
      this
2872
        .sql`SELECT payload FROM openclasp_assurance_predictions WHERE operator_id = ${operatorId} AND interaction_id = ${report.interactionId} AND target_agent_id = ${report.reportingAgentId} ORDER BY created_at ASC`,
2873
      this
2874
        .sql`SELECT payload FROM openclasp_assurance_probe_plans WHERE operator_id = ${operatorId} AND interaction_id = ${report.interactionId} AND target_agent_id = ${report.reportingAgentId} ORDER BY created_at ASC`,
2875
      this
2876
        .sql`SELECT responses.payload FROM openclasp_assurance_probe_responses responses INNER JOIN openclasp_assurance_probe_plans plans ON plans.plan_id = responses.plan_id WHERE plans.operator_id = ${operatorId} AND responses.interaction_id = ${report.interactionId} AND responses.agent_id = ${report.reportingAgentId} ORDER BY responses.created_at ASC`,
2877
      this
2878
        .sql`SELECT payload FROM openclasp_assurance_safeguards WHERE operator_id = ${operatorId} AND interaction_id = ${report.interactionId} AND target_agent_id = ${report.reportingAgentId} ORDER BY created_at ASC`,
2879
    ]);
2880
    if (!predictionRows.length && !planRows.length) return undefined;
2881
    const predictions = predictionRows.map((row) =>
2882
      AssurancePredictionSnapshotSchema.parse(row.payload),
2883
    );
2884
    const plans = planRows.map((row) => AssuranceProbePlanSchema.parse(row.payload));
2885
    const responses = responseRows.map((row) => AssuranceProbeResponseSchema.parse(row.payload));
2886
    const safeguards = safeguardRows.map((row) => AssuranceSafeguardSchema.parse(row.payload));
2887
    const outcomeValue = report.outcome === 'success' ? 1 : report.outcome === 'partial' ? 0.5 : 0;
2888
    const evaluation = AssuranceEffectivenessEvaluationSchema.parse({
2889
      protocolVersion: '0.1',
2890
      evaluationId: deterministicUuid(`assurance-evaluation:${operatorId}:${report.reportId}`),
2891
      interactionId: report.interactionId,
2892
      contractHash: report.contractHash,
2893
      targetAgentId: report.reportingAgentId,
2894
      targetAgentVersion: report.agentVersion,
2895
      taskCategory: predictions[0]?.taskCategory ?? 'general',
2896
      completionReportId: report.reportId,
2897
      outcomeValue,
2898
      predictionScores: predictions.map((prediction) => ({
2899
        predictionId: prediction.predictionId,
2900
        stage: prediction.stage,
2901
        probability: prediction.successProbability,
2902
        brierScore: (prediction.successProbability - outcomeValue) ** 2,
2903
      })),
2904
      questionScores: plans.flatMap((plan) =>
2905
        plan.questions.map((question) => {
2906
          const response = responses.find((candidate) => candidate.planId === plan.planId);
2907
          const answer = response?.answers.find(
2908
            (candidate) => candidate.probeId === question.probeId,
2909
          );
2910
          const signal = answer
2911
            ? question.expectedSignals.find(
2912
                (candidate) =>
2913
                  candidate.answer.toLowerCase() === String(answer.answer).toLowerCase(),
2914
              )
2915
            : undefined;
2916
          const before = predictions.find(
2917
            (prediction) => prediction.predictionId === plan.predictionBeforeId,
2918
          );
2919
          const after = response
2920
            ? predictions.find((prediction) => prediction.triggerResponseId === response.responseId)
2921
            : undefined;
2922
          return {
2923
            probeId: question.probeId,
2924
            questionCode: question.questionCode,
2925
            questionFamily: question.questionFamily,
2926
            answered: Boolean(answer),
2927
            exposedMaterialRisk:
2928
              signal?.effect === 'reduce_success' ||
2929
              Boolean(answer?.limitations.length) ||
2930
              report.blockers.length > 0,
2931
            predictionDelta:
2932
              before && after ? after.successProbability - before.successProbability : 0,
2933
          };
2934
        }),
2935
      ),
2936
      safeguardScores: safeguards.map((safeguard) => ({
2937
        safeguardId: safeguard.safeguardId,
2938
        type: safeguard.type,
2939
        status: safeguard.status,
2940
        outcomeAssociation:
2941
          safeguard.status === 'accepted' || safeguard.status === 'modified'
2942
            ? outcomeValue >= 0.75
2943
              ? 'positive'
2944
              : outcomeValue <= 0.25
2945
                ? 'negative'
2946
                : 'unclear'
2947
            : 'unclear',
2948
        causalClaim: false,
2949
      })),
2950
      evaluatedAt: new Date().toISOString(),
2951
    });
2952
    await this.sql`
2953
      INSERT INTO openclasp_assurance_evaluations(
2954
        evaluation_id, operator_id, interaction_id, target_agent_id, target_agent_version,
2955
        task_category, completion_report_id, payload
2956
      ) VALUES (
2957
        ${evaluation.evaluationId}, ${operatorId}, ${evaluation.interactionId},
2958
        ${evaluation.targetAgentId}, ${evaluation.targetAgentVersion}, ${evaluation.taskCategory},
2959
        ${evaluation.completionReportId}, ${JSON.stringify(evaluation)}::jsonb
2960
      )
2961
      ON CONFLICT (operator_id, completion_report_id)
2962
      DO UPDATE SET payload = EXCLUDED.payload, created_at = NOW()
2963
    `;
2964
    return evaluation;
2965
  }
2966
2967
  private async ensureFeedbackRequest(
2968
    operatorId: string,
2969
    interactionId: string,
2970
    reviewerAgentId: string,
2971
    subjectAgentId: string,
2972
  ) {
2973
    const existing = await this.sql`
2974
      SELECT payload FROM openclasp_records
2975
      WHERE operator_id = ${operatorId}
2976
        AND kind = 'feedback_request'
2977
        AND payload->>'interactionId' = ${interactionId}
2978
        AND payload->>'reviewerAgentId' = ${reviewerAgentId}
2979
      ORDER BY updated_at DESC
2980
      LIMIT 1
2981
    `;
2982
    const parsed = FeedbackRequestSchema.safeParse(existing[0]?.payload);
2983
    if (parsed.success) return parsed.data;
2984
    const requestedAt = new Date();
2985
    const base = FeedbackRequestSchema.parse({
2986
      requestId: deterministicUuid(`feedback-request:${interactionId}:${reviewerAgentId}`),
2987
      interactionId,
2988
      reviewerAgentId,
2989
      subjectAgentId,
2990
      status: 'pending',
2991
      requestedDimensions: FEEDBACK_DIMENSIONS,
2992
      requestedAt: requestedAt.toISOString(),
2993
      dueAt: new Date(requestedAt.getTime() + feedbackWindowMilliseconds()).toISOString(),
2994
    });
2995
    const request = FeedbackRequestSchema.parse({
2996
      ...base,
2997
      platformAttestation: attestSessionRecord(this.gatewaySecret(), base),
2998
    });
2999
    await this.upsert(operatorId, 'feedback_request', request.requestId, request);
3000
    return request;
3001
  }
3002
3003
  private async requestRuntimeFinalization(
3004
    interactionId: string,
3005
    reportingAgentId: string,
3006
    counterpartyAgentId: string,
3007
    contractHash: string,
3008
  ) {
3009
    const rows = await this.sql`
3010
      SELECT endpoint FROM openclasp_agent_runtimes
3011
      WHERE agent_id = ${counterpartyAgentId} AND status = 'verified'
3012
      LIMIT 1
3013
    `;
3014
    if (!rows[0]?.endpoint) return false;
3015
    const requestId = crypto.randomUUID();
3016
    const value = {
3017
      type: 'openclasp.session.finalization_request',
3018
      version: '1',
3019
      requestId,
3020
      interactionId,
3021
      agentId: counterpartyAgentId,
3022
      peerAgentId: reportingAgentId,
3023
      contractHash,
3024
      requestedAt: new Date().toISOString(),
3025
    };
3026
    const response = await this.signedRuntimeRequest(
3027
      { callbackEndpoint: String(rows[0].endpoint) },
3028
      requestId,
3029
      value,
3030
    );
3031
    return response.status >= 200 && response.status < 300;
3032
  }
3033
3034
  async listFeedbackRequests(operatorId: string, agentId: string) {
3035
    const rows = await this.sql`
3036
      SELECT payload FROM openclasp_records
3037
      WHERE operator_id = ${operatorId}
3038
        AND kind = 'feedback_request'
3039
        AND payload->>'reviewerAgentId' = ${agentId}
3040
      ORDER BY updated_at DESC
3041
    `;
3042
    return rows.map((row) => FeedbackRequestSchema.parse(row.payload));
3043
  }
3044
3045
  async submitCompletionReport(
3046
    operatorId: string,
3047
    agentId: string,
3048
    value: InteractionCompletionReport,
3049
    submissionMethod:
3050
      'oauth_account' | 'oauth_installation' | 'agent_access_token' | 'runtime_session',
3051
  ) {
3052
    const participant = await this.interactionParticipant(operatorId, value.interactionId, agentId);
3053
    const report = InteractionCompletionReportSchema.parse(value);
3054
    if (report.platformAttestation)
3055
      throw new Error('Platform attestation is assigned by OpenClasp');
3056
    if (report.reportingAgentId !== agentId)
3057
      throw new Error('Completion report identity does not match the authenticated agent');
3058
    if (report.counterpartyAgentId !== participant.counterpartyAgentId)
3059
      throw new Error('Completion report counterparty does not match the interaction');
3060
    if (report.contractHash !== participant.interaction.termsHash)
3061
      throw new Error('Completion report contract hash does not match the interaction');
3062
    if (report.requestedOutcome !== participant.interaction.contract.requestedOutcome)
3063
      throw new Error('Completion report requested outcome does not match the contract');
3064
    if (
3065
      report.criteria.some(
3066
        (criterion) =>
3067
          !participant.interaction.contract.successCriteria.includes(criterion.criterion),
3068
      )
3069
    )
3070
      throw new Error('Completion report contains a criterion outside the contract');
3071
    if (!['active', 'completed'].includes(participant.interaction.status))
3072
      throw new Error('Only active or completed interactions accept completion reports');
3073
    const profiles = await this.sql`
3074
      SELECT payload FROM openclasp_records
3075
      WHERE operator_id = ${operatorId} AND kind = 'agent_profile' AND record_id = ${agentId}
3076
      LIMIT 1
3077
    `;
3078
    const profile = profiles[0]?.payload as Partial<AgentProfile> | undefined;
3079
    if (!profile || profile.agentVersion !== report.agentVersion)
3080
      throw new Error('Completion report agent version does not match the registered agent');
3081
    let verifiedSubmissionMethod: InteractionCompletionReport['submissionMethod'] =
3082
      submissionMethod;
3083
    if (report.signature) {
3084
      if (report.submissionMethod !== 'agent_signature')
3085
        throw new Error('Signed completion reports must declare agent_signature submission');
3086
      const identities = await this.sql`
3087
        SELECT payload FROM openclasp_records
3088
        WHERE operator_id = ${operatorId} AND kind = 'agent' AND record_id = ${agentId}
3089
        LIMIT 1
3090
      `;
3091
      const identity = AgentIdentitySchema.safeParse(identities[0]?.payload);
3092
      if (
3093
        !identity.success ||
3094
        !verifyObject(report as unknown as Record<string, unknown>, identity.data.publicKey)
3095
      )
3096
        throw new Error('Completion report agent signature is invalid or unverifiable');
3097
      verifiedSubmissionMethod = 'agent_signature';
3098
    }
3099
    const unsigned = InteractionCompletionReportSchema.parse({
3100
      ...report,
3101
      submissionMethod: verifiedSubmissionMethod,
3102
    });
3103
    const stored = InteractionCompletionReportSchema.parse({
3104
      ...unsigned,
3105
      platformAttestation: attestSessionRecord(this.gatewaySecret(), unsigned),
3106
    });
3107
    const existing = await this.sql`
3108
      SELECT payload FROM openclasp_records
3109
      WHERE kind = 'completion_report' AND record_id = ${stored.reportId}
3110
        AND payload->>'interactionId' = ${stored.interactionId}
3111
      LIMIT 1
3112
    `;
3113
    if (existing[0] && canonicalHash(existing[0].payload) !== canonicalHash(stored))
3114
      throw new Error('Conflicting completion report ID');
3115
    await Promise.all([
3116
      this.upsert(participant.participantOperatorId, 'completion_report', stored.reportId, stored),
3117
      this.upsert(participant.counterpartyOperatorId, 'completion_report', stored.reportId, stored),
3118
    ]);
3119
    const assuranceComparisons = (
3120
      await Promise.all([
3121
        this.recalculateAssuranceComparison(
3122
          participant.participantOperatorId,
3123
          stored.interactionId,
3124
          stored.reportingAgentId,
3125
        ).catch(() => undefined),
3126
        this.recalculateAssuranceComparison(
3127
          participant.counterpartyOperatorId,
3128
          stored.interactionId,
3129
          stored.reportingAgentId,
3130
        ).catch(() => undefined),
3131
      ])
3132
    ).filter((comparison) => comparison !== undefined);
3133
    const assuranceEvaluations = (
3134
      await Promise.all([
3135
        this.evaluateAssuranceOutcome(participant.participantOperatorId, stored).catch(
3136
          () => undefined,
3137
        ),
3138
        this.evaluateAssuranceOutcome(participant.counterpartyOperatorId, stored).catch(
3139
          () => undefined,
3140
        ),
3141
      ])
3142
    ).filter((evaluation) => evaluation !== undefined);
3143
    const [feedbackRequest] = await Promise.all([
3144
      this.ensureFeedbackRequest(
3145
        participant.participantOperatorId,
3146
        stored.interactionId,
3147
        stored.reportingAgentId,
3148
        stored.counterpartyAgentId,
3149
      ),
3150
      this.ensureFeedbackRequest(
3151
        participant.counterpartyOperatorId,
3152
        stored.interactionId,
3153
        stored.counterpartyAgentId,
3154
        stored.reportingAgentId,
3155
      ),
3156
    ]);
3157
    const completed = await this.sql`
3158
      SELECT COUNT(DISTINCT payload->>'reportingAgentId') AS count
3159
      FROM openclasp_records
3160
      WHERE kind = 'completion_report'
3161
        AND payload->>'interactionId' = ${stored.interactionId}
3162
    `;
3163
    let peerReportStatus: InteractionConclusion['peerReportStatus'] = 'received';
3164
    if (Number(completed[0]?.count ?? 0) >= 2) {
3165
      const completedAt = new Date().toISOString();
3166
      await Promise.all([
3167
        this.sql`
3168
          UPDATE openclasp_live_sessions SET status = 'completed', completed_at = NOW()
3169
          WHERE interaction_id = ${stored.interactionId} AND status = 'active'
3170
        `,
3171
        this.sql`
3172
          UPDATE openclasp_federated_interactions
3173
          SET status = 'completed',
3174
            payload = jsonb_set(
3175
              jsonb_set(payload, '{status}', '"completed"'::jsonb),
3176
              '{updatedAt}', to_jsonb(${completedAt}::text)
3177
            ),
3178
            updated_at = NOW()
3179
          WHERE interaction_id = ${stored.interactionId} AND status = 'active'
3180
        `,
3181
      ]);
3182
      await Promise.all([
3183
        this.journalLiveSessionState(stored.interactionId),
3184
        this.journalFederatedInteraction(stored.interactionId),
3185
      ]);
3186
    } else {
3187
      const requested = await this.requestRuntimeFinalization(
3188
        stored.interactionId,
3189
        stored.reportingAgentId,
3190
        stored.counterpartyAgentId,
3191
        stored.contractHash,
3192
      ).catch(() => false);
3193
      peerReportStatus = requested ? 'awaiting' : 'unreachable';
3194
    }
3195
    const conclusion = await this.finalizeInteractionConclusion(stored.interactionId, {
3196
      peerReportStatus,
3197
    });
3198
    return {
3199
      report: stored,
3200
      feedbackRequest,
3201
      peerReportRequested: peerReportStatus === 'awaiting',
3202
      assuranceComparisons,
3203
      assuranceEvaluations,
3204
      ...(conclusion.released ? { conclusion: conclusion.conclusion } : {}),
3205
    };
3206
  }
3207
3208
  async recordSessionCompletionReport(token: string, value: InteractionCompletionReport) {
3209
    const report = InteractionCompletionReportSchema.parse(value);
3210
    const grant = this.verifySessionGrant(token);
3211
    if (
3212
      grant.interactionId !== report.interactionId ||
3213
      grant.senderAgentId !== report.reportingAgentId ||
3214
      grant.recipientAgentId !== report.counterpartyAgentId
3215
    )
3216
      throw new Error('Session credential does not match the completion report');
3217
    const owners = await this.sql`
3218
      SELECT initiator_operator_id, responder_operator_id,
3219
        initiator_agent_id, responder_agent_id
3220
      FROM openclasp_federated_interactions
3221
      WHERE interaction_id = ${report.interactionId}
3222
      LIMIT 1
3223
    `;
3224
    const row = owners[0];
3225
    if (!row) throw new Error('Interaction not found');
3226
    const operatorId = String(
3227
      row.initiator_agent_id === report.reportingAgentId
3228
        ? row.initiator_operator_id
3229
        : row.responder_operator_id,
3230
    );
3231
    return this.submitCompletionReport(
3232
      operatorId,
3233
      report.reportingAgentId,
3234
      report,
3235
      'runtime_session',
3236
    );
3237
  }
3238
3239
  private async applyInteractionLearning(input: {
3240
    interaction: FederatedInteraction;
3241
    initiatorOperatorId: string;
3242
    responderOperatorId: string;
3243
    reports: InteractionCompletionReport[];
3244
    feedback: InteractionFeedback[];
3245
    conclusion: InteractionConclusion;
3246
  }) {
3247
    const { interaction, reports, feedback, conclusion } = input;
3248
    const [initiatorSettings, responderSettings, cards, pairActivity] = await Promise.all([
3249
      this.getSettings(input.initiatorOperatorId),
3250
      this.getSettings(input.responderOperatorId),
3251
      this.sql`
3252
        SELECT agent_id, card FROM openclasp_public_agents
3253
        WHERE agent_id IN (${interaction.initiatorAgentId}, ${interaction.responderAgentId})
3254
      `,
3255
      this.sql`
3256
        SELECT COUNT(*)::integer AS count
3257
        FROM openclasp_federated_interactions
3258
        WHERE created_at >= NOW() - INTERVAL '24 hours'
3259
          AND (
3260
            (initiator_agent_id = ${interaction.initiatorAgentId}
3261
              AND responder_agent_id = ${interaction.responderAgentId})
3262
            OR
3263
            (initiator_agent_id = ${interaction.responderAgentId}
3264
              AND responder_agent_id = ${interaction.initiatorAgentId})
3265
          )
3266
      `,
3267
    ]);
3268
    const contributionMode =
3269
      initiatorSettings.contributionEnabled && responderSettings.contributionEnabled
3270
        ? ('network_aggregate' as const)
3271
        : ('local_only' as const);
3272
    const eligibilityBase = evaluateLearningEligibility({
3273
      interactionId: interaction.interactionId,
3274
      reports,
3275
      feedback,
3276
      consensus: conclusion.consensus,
3277
      contributionMode,
3278
      reviewerCredibility: Object.fromEntries(
3279
        feedback.map((item) => [item.reviewerAgentId, reviewerCredibility(item)]),
3280
      ),
3281
      manipulationSignals: [
3282
        ...(input.initiatorOperatorId === input.responderOperatorId ? ['same_operator_pair'] : []),
3283
        ...(Number(pairActivity[0]?.count ?? 0) > 10 ? ['rapid_reciprocal_pair_activity'] : []),
3284
      ],
3285
      decisionId: deterministicUuid(`learning-eligibility:${interaction.interactionId}`),
3286
      decidedAt: conclusion.generatedAt,
3287
    });
3288
    const eligibility = LearningEligibilityDecisionSchema.parse({
3289
      ...eligibilityBase,
3290
      platformAttestation: attestSessionRecord(this.gatewaySecret(), eligibilityBase),
3291
    });
3292
    await Promise.all([
3293
      this.upsert(
3294
        input.initiatorOperatorId,
3295
        'learning_eligibility',
3296
        eligibility.decisionId,
3297
        eligibility,
3298
      ),
3299
      this.upsert(
3300
        input.responderOperatorId,
3301
        'learning_eligibility',
3302
        eligibility.decisionId,
3303
        eligibility,
3304
      ),
3305
    ]);
3306
    if (!eligibility.eligible || eligibility.sampleWeight <= 0)
3307
      return { eligibility, profileDeltas: [] as BehaviouralProfileDelta[] };
3308
3309
    const appliedAt = new Date().toISOString();
3310
    const versions = new Map<string, string>();
3311
    for (const report of reports) versions.set(report.reportingAgentId, report.agentVersion);
3312
    for (const row of cards) {
3313
      const card = PublicAgentCardSchema.safeParse(row.card);
3314
      if (card.success && !versions.has(String(row.agent_id)))
3315
        versions.set(String(row.agent_id), card.data.agentVersion);
3316
    }
3317
    const participants = [
3318
      {
3319
        viewerOperatorId: input.initiatorOperatorId,
3320
        reviewerAgentId: interaction.initiatorAgentId,
3321
        subjectAgentId: interaction.responderAgentId,
3322
      },
3323
      {
3324
        viewerOperatorId: input.responderOperatorId,
3325
        reviewerAgentId: interaction.responderAgentId,
3326
        subjectAgentId: interaction.initiatorAgentId,
3327
      },
3328
      {
3329
        viewerOperatorId: input.initiatorOperatorId,
3330
        reviewerAgentId: interaction.responderAgentId,
3331
        subjectAgentId: interaction.initiatorAgentId,
3332
      },
3333
      {
3334
        viewerOperatorId: input.responderOperatorId,
3335
        reviewerAgentId: interaction.initiatorAgentId,
3336
        subjectAgentId: interaction.responderAgentId,
3337
      },
3338
    ];
3339
    const profileDeltas: Array<BehaviouralProfileDelta | undefined> = [];
3340
    for (const participant of participants) {
3341
      const agentVersion = versions.get(participant.subjectAgentId);
3342
      if (!agentVersion) {
3343
        profileDeltas.push(undefined);
3344
        continue;
3345
      }
3346
      const deltaId = deterministicUuid(
3347
        `profile-delta:${interaction.interactionId}:${participant.viewerOperatorId}:${participant.reviewerAgentId}:${participant.subjectAgentId}`,
3348
      );
3349
      const legacyDeltaId = deterministicUuid(
3350
        `profile-delta:${interaction.interactionId}:${participant.viewerOperatorId}:${participant.subjectAgentId}`,
3351
      );
3352
      const existingDelta = await this.sql`
3353
          SELECT payload FROM openclasp_records
3354
          WHERE operator_id = ${participant.viewerOperatorId}
3355
            AND kind = 'profile_delta'
3356
            AND (record_id = ${deltaId} OR record_id = ${legacyDeltaId})
3357
          LIMIT 1
3358
        `;
3359
      const parsedDelta = BehaviouralProfileDeltaSchema.safeParse(existingDelta[0]?.payload);
3360
      if (parsedDelta.success) {
3361
        profileDeltas.push(parsedDelta.data);
3362
        continue;
3363
      }
3364
      const profileId = deterministicUuid(
3365
        `contextual-profile:${participant.subjectAgentId}:${agentVersion}:${interaction.contract.taskCategory}`,
3366
      );
3367
      const currentRows = await this.sql`
3368
          SELECT payload FROM openclasp_records
3369
          WHERE operator_id = ${participant.viewerOperatorId}
3370
            AND kind = 'profile' AND record_id = ${profileId}
3371
          LIMIT 1
3372
        `;
3373
      const current = currentRows[0]?.payload as Partial<ContextualProfile> | undefined;
3374
      const reviewerFeedback = feedback.find(
3375
        (item) =>
3376
          item.reviewerAgentId === participant.reviewerAgentId &&
3377
          item.subjectAgentId === participant.subjectAgentId,
3378
      );
3379
      const observations = deriveBehaviouralObservations({
3380
        subjectAgentId: participant.subjectAgentId,
3381
        reviewerAgentId: participant.reviewerAgentId,
3382
        reports,
3383
        ...(reviewerFeedback ? { reviewerFeedback } : {}),
3384
        conclusion,
3385
      });
3386
      if (!Object.keys(observations).length) {
3387
        profileDeltas.push(undefined);
3388
        continue;
3389
      }
3390
      const applied = updateContextualBehaviouralProfile({
3391
        ...(current ? { current } : {}),
3392
        observations,
3393
        sampleWeight: eligibility.sampleWeight,
3394
        appliedAt,
3395
      });
3396
      const profile: ContextualProfile = {
3397
        recordId: profileId,
3398
        agentId: participant.subjectAgentId,
3399
        agentVersion,
3400
        taskCategory: interaction.contract.taskCategory,
3401
        ...applied.profile,
3402
      };
3403
      const deltaBase = BehaviouralProfileDeltaSchema.parse({
3404
        deltaId,
3405
        interactionId: interaction.interactionId,
3406
        agentId: participant.subjectAgentId,
3407
        agentVersion,
3408
        taskCategory: interaction.contract.taskCategory,
3409
        sampleWeight: eligibility.sampleWeight,
3410
        dimensionDeltas: applied.dimensionDeltas,
3411
        explanation:
3412
          'Applied platform-attested structured outcome and eligible reviewer signals; private comments and raw conversation content were excluded.',
3413
        appliedAt,
3414
      });
3415
      const delta = BehaviouralProfileDeltaSchema.parse({
3416
        ...deltaBase,
3417
        platformAttestation: attestSessionRecord(this.gatewaySecret(), deltaBase),
3418
      });
3419
      await Promise.all([
3420
        this.upsert(participant.viewerOperatorId, 'profile', profileId, profile),
3421
        this.upsert(participant.viewerOperatorId, 'profile_delta', deltaId, delta),
3422
      ]);
3423
      profileDeltas.push(delta);
3424
    }
3425
    return {
3426
      eligibility,
3427
      profileDeltas: profileDeltas.filter(
3428
        (delta): delta is BehaviouralProfileDelta => delta !== undefined,
3429
      ),
3430
    };
3431
  }
3432
3433
  private async finalizeInteractionConclusion(
3434
    interactionId: string,
3435
    options: { peerReportStatus?: InteractionConclusion['peerReportStatus'] } = {},
3436
  ) {
3437
    const [interactionRows, reportRows, feedbackRows, requestRows, existingRows] =
3438
      await Promise.all([
3439
        this.sql`
3440
        SELECT payload, initiator_operator_id, responder_operator_id
3441
        FROM openclasp_federated_interactions
3442
        WHERE interaction_id = ${interactionId}
3443
        LIMIT 1
3444
      `,
3445
        this.sql`
3446
        SELECT record_id, payload FROM openclasp_records
3447
        WHERE kind = 'completion_report' AND payload->>'interactionId' = ${interactionId}
3448
      `,
3449
        this.sql`
3450
        SELECT record_id, payload FROM openclasp_records
3451
        WHERE kind = 'interaction_feedback' AND payload->>'interactionId' = ${interactionId}
3452
      `,
3453
        this.sql`
3454
        SELECT record_id, payload FROM openclasp_records
3455
        WHERE kind = 'feedback_request' AND payload->>'interactionId' = ${interactionId}
3456
      `,
3457
        this.sql`
3458
        SELECT payload FROM openclasp_records
3459
        WHERE kind = 'interaction_conclusion' AND payload->>'interactionId' = ${interactionId}
3460
        LIMIT 1
3461
      `,
3462
      ]);
3463
    const interactionRow = interactionRows[0];
3464
    if (!interactionRow) throw new Error('Interaction not found');
3465
    const interaction = FederatedInteractionSchema.parse(interactionRow.payload);
3466
    const reports = [
3467
      ...new Map(
3468
        reportRows
3469
          .map((row) => InteractionCompletionReportSchema.parse(row.payload))
3470
          .map((report) => [report.reportId, report]),
3471
      ).values(),
3472
    ];
3473
    const feedback = [
3474
      ...new Map(
3475
        feedbackRows
3476
          .map((row) => InteractionFeedbackSchema.parse(row.payload))
3477
          .map((item) => [item.feedbackId, item]),
3478
      ).values(),
3479
    ];
3480
    const requests = [
3481
      ...new Map(
3482
        requestRows
3483
          .map((row) => FeedbackRequestSchema.parse(row.payload))
3484
          .map((request) => [request.requestId, request]),
3485
      ).values(),
3486
    ];
3487
    if (!reports.length) return { released: false as const, feedbackRevealed: false as const };
3488
    const existing = InteractionConclusionSchema.safeParse(existingRows[0]?.payload);
3489
    const reportingAgentIds = new Set(reports.map((report) => report.reportingAgentId));
3490
    const missingReportAgentIds = [
3491
      interaction.initiatorAgentId,
3492
      interaction.responderAgentId,
3493
    ].filter((agentId) => !reportingAgentIds.has(agentId));
3494
    const feedbackWindowClosed =
3495
      requests.length >= 2 && requests.every((request) => request.status !== 'pending');
3496
    const lifecycle =
3497
      missingReportAgentIds.length === 0 || feedbackWindowClosed
3498
        ? ('final' as const)
3499
        : ('provisional' as const);
3500
    const visibleFeedback = feedbackWindowClosed ? feedback : [];
3501
    const peerReportStatus: InteractionConclusion['peerReportStatus'] =
3502
      missingReportAgentIds.length === 0
3503
        ? 'received'
3504
        : feedbackWindowClosed
3505
          ? 'timed_out'
3506
          : (options.peerReportStatus ?? existing.data?.peerReportStatus ?? 'awaiting');
3507
    const pendingFeedbackAgentIds = requests
3508
      .filter((request) => request.status === 'pending')
3509
      .map((request) => request.reviewerAgentId);
3510
    if (requests.length < 2) return { released: false as const };
3511
    const base = buildInteractionConclusion({
3512
      interaction,
3513
      reports,
3514
      feedback: visibleFeedback,
3515
      conclusionId: deterministicUuid(`interaction-conclusion:${interactionId}`),
3516
      lifecycle,
3517
      pendingFeedbackAgentIds,
3518
      peerReportStatus,
3519
    });
3520
    const conclusion = InteractionConclusionSchema.parse({
3521
      ...base,
3522
      platformAttestation: attestSessionRecord(this.gatewaySecret(), base),
3523
    });
3524
    const startedAt = reports
3525
      .map((report) => report.startedAt)
3526
      .filter((value): value is string => typeof value === 'string')
3527
      .sort()[0];
3528
    const receiptBase = {
3529
      receiptId: deterministicUuid(`interaction-receipt:${interactionId}`),
3530
      interactionId,
3531
      participants: [interaction.initiatorAgentId, interaction.responderAgentId],
3532
      agentVersions: Object.fromEntries(
3533
        reports.map((report) => [report.reportingAgentId, report.agentVersion]),
3534
      ),
3535
      contractHash: interaction.termsHash,
3536
      startedAt: startedAt ?? interaction.createdAt,
3537
      completedAt:
3538
        reports
3539
          .map((report) => report.completedAt)
3540
          .sort()
3541
          .at(-1) ?? conclusion.generatedAt,
3542
      outcome: conclusion.outcome,
3543
      commitmentsFulfilled: conclusion.criteria
3544
        .filter((criterion) => criterion.status === 'met')
3545
        .map((criterion) => criterion.criterion),
3546
      commitmentsMissed: conclusion.criteria
3547
        .filter((criterion) => criterion.status !== 'met')
3548
        .map((criterion) => criterion.criterion),
3549
      evidenceHashes: conclusion.evidenceReferences.map((reference) => canonicalHash(reference)),
3550
      policyWarnings: reports.flatMap((report) => report.blockers),
3551
      policyViolations: [],
3552
      disputeStatus: conclusion.consensus === 'conflicting' ? ('open' as const) : ('none' as const),
3553
      delegationChainHash: canonicalHash(interaction.contract.delegationRules),
3554
      unilateral: reports.length < 2,
3555
      provisional: lifecycle === 'provisional',
3556
      confidence: conclusion.confidence,
3557
      signatures: {},
3558
      completionReportIds: conclusion.reportIds,
3559
      conclusionId: conclusion.conclusionId,
3560
    };
3561
    const receipt = ReceiptSchema.parse({
3562
      ...receiptBase,
3563
      platformAttestation: attestSessionRecord(this.gatewaySecret(), receiptBase),
3564
    });
3565
    await Promise.all([
3566
      this.upsert(
3567
        String(interactionRow.initiator_operator_id),
3568
        'interaction_conclusion',
3569
        conclusion.conclusionId,
3570
        conclusion,
3571
      ),
3572
      this.upsert(
3573
        String(interactionRow.responder_operator_id),
3574
        'interaction_conclusion',
3575
        conclusion.conclusionId,
3576
        conclusion,
3577
      ),
3578
      this.upsert(
3579
        String(interactionRow.initiator_operator_id),
3580
        'receipt',
3581
        receipt.receiptId,
3582
        receipt,
3583
      ),
3584
      this.upsert(
3585
        String(interactionRow.responder_operator_id),
3586
        'receipt',
3587
        receipt.receiptId,
3588
        receipt,
3589
      ),
3590
    ]);
3591
    const learning =
3592
      lifecycle === 'final' && feedbackWindowClosed
3593
        ? await this.applyInteractionLearning({
3594
            interaction,
3595
            initiatorOperatorId: String(interactionRow.initiator_operator_id),
3596
            responderOperatorId: String(interactionRow.responder_operator_id),
3597
            reports,
3598
            feedback: visibleFeedback,
3599
            conclusion,
3600
          })
3601
        : undefined;
3602
    return {
3603
      released: true as const,
3604
      feedbackRevealed: feedbackWindowClosed,
3605
      conclusion,
3606
      receipt,
3607
      ...(learning ? { learning } : {}),
3608
    };
3609
  }
3610
3611
  async submitInteractionFeedback(
3612
    operatorId: string,
3613
    agentId: string,
3614
    value: InteractionFeedback,
3615
    submissionMethod:
3616
      'oauth_account' | 'oauth_installation' | 'agent_access_token' | 'runtime_session',
3617
  ) {
3618
    const feedback = InteractionFeedbackSchema.parse(value);
3619
    if (feedback.platformAttestation)
3620
      throw new Error('Platform attestation is assigned by OpenClasp');
3621
    if (feedback.reviewerAgentId !== agentId)
3622
      throw new Error('Feedback reviewer does not match the authenticated agent');
3623
    const participant = await this.interactionParticipant(
3624
      operatorId,
3625
      feedback.interactionId,
3626
      agentId,
3627
    );
3628
    if (feedback.subjectAgentId !== participant.counterpartyAgentId)
3629
      throw new Error('Feedback subject does not match the interaction counterparty');
3630
    const requestRows = await this.sql`
3631
      SELECT payload FROM openclasp_records
3632
      WHERE operator_id = ${operatorId}
3633
        AND kind = 'feedback_request'
3634
        AND record_id = ${feedback.requestId}
3635
      LIMIT 1
3636
    `;
3637
    const request = FeedbackRequestSchema.parse(requestRows[0]?.payload);
3638
    if (
3639
      request.interactionId !== feedback.interactionId ||
3640
      request.reviewerAgentId !== feedback.reviewerAgentId ||
3641
      request.subjectAgentId !== feedback.subjectAgentId
3642
    )
3643
      throw new Error('Feedback does not match its request');
3644
    if (request.status !== 'pending') throw new Error('Feedback request is no longer pending');
3645
    if (Date.parse(request.dueAt) <= Date.now()) throw new Error('Feedback request has expired');
3646
    if (
3647
      request.requestedDimensions.some(
3648
        (dimension) => typeof feedback.ratings[dimension] !== 'number',
3649
      )
3650
    )
3651
      throw new Error('Feedback must rate every requested dimension');
3652
    const profiles = await this.sql`
3653
      SELECT payload FROM openclasp_records
3654
      WHERE operator_id = ${operatorId} AND kind = 'agent_profile' AND record_id = ${agentId}
3655
      LIMIT 1
3656
    `;
3657
    const profile = profiles[0]?.payload as Partial<AgentProfile> | undefined;
3658
    if (!profile || profile.agentVersion !== feedback.reviewerAgentVersion)
3659
      throw new Error('Feedback reviewer version does not match the registered agent');
3660
    let verifiedSubmissionMethod: InteractionFeedback['submissionMethod'] = submissionMethod;
3661
    if (feedback.signature) {
3662
      if (feedback.submissionMethod !== 'agent_signature')
3663
        throw new Error('Signed feedback must declare agent_signature submission');
3664
      const identities = await this.sql`
3665
        SELECT payload FROM openclasp_records
3666
        WHERE operator_id = ${operatorId} AND kind = 'agent' AND record_id = ${agentId}
3667
        LIMIT 1
3668
      `;
3669
      const identity = AgentIdentitySchema.safeParse(identities[0]?.payload);
3670
      if (
3671
        !identity.success ||
3672
        !verifyObject(feedback as unknown as Record<string, unknown>, identity.data.publicKey)
3673
      )
3674
        throw new Error('Feedback agent signature is invalid or unverifiable');
3675
      verifiedSubmissionMethod = 'agent_signature';
3676
    }
3677
    const unattested = InteractionFeedbackSchema.parse({
3678
      ...feedback,
3679
      submissionMethod: verifiedSubmissionMethod,
3680
    });
3681
    const stored = InteractionFeedbackSchema.parse({
3682
      ...unattested,
3683
      platformAttestation: attestSessionRecord(this.gatewaySecret(), unattested),
3684
    });
3685
    const existing = await this.sql`
3686
      SELECT payload FROM openclasp_records
3687
      WHERE operator_id = ${operatorId}
3688
        AND kind = 'interaction_feedback'
3689
        AND record_id = ${stored.feedbackId}
3690
      LIMIT 1
3691
    `;
3692
    if (existing[0] && canonicalHash(existing[0].payload) !== canonicalHash(stored))
3693
      throw new Error('Conflicting feedback ID');
3694
    await this.upsert(operatorId, 'interaction_feedback', stored.feedbackId, stored);
3695
    const requestBase = { ...request, status: 'submitted' as const };
3696
    delete requestBase.platformAttestation;
3697
    const updatedRequest = FeedbackRequestSchema.parse({
3698
      ...requestBase,
3699
      platformAttestation: attestSessionRecord(this.gatewaySecret(), requestBase),
3700
    });
3701
    await this.upsert(operatorId, 'feedback_request', request.requestId, updatedRequest);
3702
    const release = await this.finalizeInteractionConclusion(feedback.interactionId);
3703
    return {
3704
      feedbackId: stored.feedbackId,
3705
      status: 'submitted' as const,
3706
      revealed: release.released && release.feedbackRevealed,
3707
      ...(release.released ? { conclusion: release.conclusion } : {}),
3708
    };
3709
  }
3710
3711
  async processDueFeedback(now = new Date()) {
3712
    await this.ensureSchema();
3713
    const expiredInteractions = await this.sql`
3714
      UPDATE openclasp_federated_interactions
3715
      SET status = 'expired',
3716
        payload = jsonb_set(
3717
          jsonb_set(payload, '{status}', '"expired"'::jsonb),
3718
          '{updatedAt}', to_jsonb(${now.toISOString()}::text)
3719
        ),
3720
        updated_at = NOW()
3721
      WHERE status IN ('pending', 'active') AND expires_at <= ${now.toISOString()}
3722
      RETURNING interaction_id
3723
    `;
3724
    if (expiredInteractions.length) {
3725
      await this.sql`
3726
        UPDATE openclasp_live_sessions
3727
        SET status = 'failed', completed_at = COALESCE(completed_at, NOW()),
3728
          last_error = COALESCE(last_error, 'Interaction expired before bilateral completion')
3729
        WHERE status IN ('preparing', 'active')
3730
          AND interaction_id IN (
3731
            SELECT interaction_id FROM openclasp_federated_interactions
3732
            WHERE status = 'expired' AND expires_at <= ${now.toISOString()}
3733
          )
3734
      `;
3735
      await Promise.all(
3736
        expiredInteractions.flatMap((row) => {
3737
          const interactionId = String(row.interaction_id);
3738
          return [
3739
            this.journalFederatedInteraction(interactionId),
3740
            this.journalLiveSessionState(interactionId),
3741
          ];
3742
        }),
3743
      );
3744
    }
3745
    const [rows, backfillRows] = await Promise.all([
3746
      this.sql`
3747
        SELECT operator_id, record_id, payload FROM openclasp_records
3748
        WHERE kind = 'feedback_request'
3749
          AND payload->>'status' = 'pending'
3750
          AND (payload->>'dueAt')::timestamptz <= ${now.toISOString()}
3751
        LIMIT 500
3752
      `,
3753
      this.sql`
3754
        SELECT source.interaction_id
3755
        FROM (
3756
          SELECT conclusion.payload->>'interactionId' AS interaction_id
3757
          FROM openclasp_records conclusion
3758
          WHERE conclusion.kind = 'interaction_conclusion'
3759
            AND NOT EXISTS (
3760
              SELECT 1 FROM openclasp_records eligibility
3761
              WHERE eligibility.operator_id = conclusion.operator_id
3762
                AND eligibility.kind = 'learning_eligibility'
3763
                AND eligibility.payload->>'interactionId' = conclusion.payload->>'interactionId'
3764
            )
3765
          UNION
3766
          SELECT report.payload->>'interactionId' AS interaction_id
3767
          FROM openclasp_records report
3768
          WHERE report.kind = 'completion_report'
3769
            AND NOT EXISTS (
3770
              SELECT 1 FROM openclasp_records conclusion
3771
              WHERE conclusion.kind = 'interaction_conclusion'
3772
                AND conclusion.payload->>'interactionId' = report.payload->>'interactionId'
3773
            )
3774
        ) source
3775
        WHERE source.interaction_id IS NOT NULL
3776
        GROUP BY source.interaction_id
3777
        ORDER BY source.interaction_id ASC
3778
        LIMIT 200
3779
      `,
3780
    ]);
3781
    const interactions = new Set<string>();
3782
    for (const row of rows) {
3783
      const request = FeedbackRequestSchema.parse(row.payload);
3784
      const base = { ...request, status: 'expired' as const };
3785
      delete base.platformAttestation;
3786
      const expired = FeedbackRequestSchema.parse({
3787
        ...base,
3788
        platformAttestation: attestSessionRecord(this.gatewaySecret(), base),
3789
      });
3790
      await this.upsert(
3791
        String(row.operator_id),
3792
        'feedback_request',
3793
        String(row.record_id),
3794
        expired,
3795
      );
3796
      interactions.add(request.interactionId);
3797
    }
3798
    for (const row of backfillRows) interactions.add(String(row.interaction_id));
3799
    let released = 0;
3800
    for (const interactionId of interactions) {
3801
      const result = await this.finalizeInteractionConclusion(interactionId);
3802
      if (result.released) released += 1;
3803
    }
3804
    return {
3805
      expired: rows.length,
3806
      interactionsExpired: expiredInteractions.length,
3807
      released,
3808
      backfilled: backfillRows.length,
3809
    };
3810
  }
3811
3812
  async recordSessionFeedback(token: string, value: InteractionFeedback) {
3813
    const feedback = InteractionFeedbackSchema.parse(value);
3814
    const grant = this.verifySessionGrant(token);
3815
    if (
3816
      grant.interactionId !== feedback.interactionId ||
3817
      grant.senderAgentId !== feedback.reviewerAgentId ||
3818
      grant.recipientAgentId !== feedback.subjectAgentId
3819
    )
3820
      throw new Error('Session credential does not match the feedback');
3821
    const owners = await this.sql`
3822
      SELECT initiator_operator_id, responder_operator_id,
3823
        initiator_agent_id, responder_agent_id
3824
      FROM openclasp_federated_interactions
3825
      WHERE interaction_id = ${feedback.interactionId}
3826
      LIMIT 1
3827
    `;
3828
    const row = owners[0];
3829
    if (!row) throw new Error('Interaction not found');
3830
    const operatorId = String(
3831
      row.initiator_agent_id === feedback.reviewerAgentId
3832
        ? row.initiator_operator_id
3833
        : row.responder_operator_id,
3834
    );
3835
    return this.submitInteractionFeedback(
3836
      operatorId,
3837
      feedback.reviewerAgentId,
3838
      feedback,
3839
      'runtime_session',
3840
    );
3841
  }
3842
3843
  async recordLiveSessionEvent(token: string, value: LiveSessionEvent) {
3844
    await this.ensureSchema();
3845
    const event = LiveSessionEventSchema.parse(value);
3846
    const grant = this.verifySessionGrant(token);
3847
    if (grant.interactionId !== event.interactionId || grant.senderAgentId !== event.agentId)
3848
      throw new Error('Session credential does not match the event');
3849
    const sessions = await this.sql`
3850
      SELECT 1 FROM openclasp_live_sessions
3851
      WHERE interaction_id = ${event.interactionId}
3852
        AND status = 'active'
3853
        AND ${event.agentId} IN (initiator_agent_id, responder_agent_id)
3854
    `;
3855
    if (!sessions.length) throw new Error('Active live session not found');
3856
    const attestation = attestSessionRecord(this.gatewaySecret(), event);
3857
    const encoded = JSON.stringify({ ...event, attestation });
3858
    const rows = await this.sql`
3859
      INSERT INTO openclasp_live_session_events(
3860
        interaction_id, event_id, agent_id, sequence, event
3861
      ) SELECT ${event.interactionId}, ${event.eventId}, ${event.agentId}, ${event.sequence}, ${encoded}::jsonb
3862
      WHERE (
3863
        SELECT COUNT(*) FROM openclasp_live_session_events
3864
        WHERE interaction_id = ${event.interactionId}
3865
      ) < 1000
3866
      ON CONFLICT DO NOTHING
3867
      RETURNING event_id
3868
    `;
3869
    if (rows.length)
3870
      await this.journalLiveSessionEvent({
3871
        ...event,
3872
        attestation,
3873
      });
3874
    if (event.type === 'session_completed' || event.type === 'session_failed') {
3875
      const terminal = await this.sql`
3876
        SELECT COUNT(DISTINCT agent_id) AS count
3877
        FROM openclasp_live_session_events
3878
        WHERE interaction_id = ${event.interactionId}
3879
          AND event->>'type' IN ('session_completed', 'session_failed')
3880
      `;
3881
      if (Number(terminal[0]?.count ?? 0) >= 2) {
3882
        await this.sql`
3883
          UPDATE openclasp_live_sessions SET status = 'completed', completed_at = NOW()
3884
          WHERE interaction_id = ${event.interactionId} AND status = 'active'
3885
        `;
3886
        const completedAt = new Date().toISOString();
3887
        await this.sql`
3888
          UPDATE openclasp_federated_interactions
3889
          SET status = 'completed',
3890
            payload = jsonb_set(
3891
              jsonb_set(payload, '{status}', '"completed"'::jsonb),
3892
              '{updatedAt}', to_jsonb(${completedAt}::text)
3893
            ),
3894
            updated_at = NOW()
3895
          WHERE interaction_id = ${event.interactionId} AND status = 'active'
3896
        `;
3897
        await Promise.all([
3898
          this.journalLiveSessionState(event.interactionId),
3899
          this.journalFederatedInteraction(event.interactionId),
3900
        ]);
3901
      }
3902
    }
3903
    return {
3904
      recorded: rows.length > 0,
3905
      deduplicated: rows.length === 0,
3906
      eventId: event.eventId,
3907
      attestation,
3908
    };
3909
  }
3910
3911
  async createFederatedInteraction(
3912
    operatorId: string,
3913
    value: FederatedInteraction,
3914
  ): Promise<FederatedInteraction> {
3915
    await this.ensureSchema();
3916
    let interaction = FederatedInteractionSchema.parse(value);
3917
    if (interaction.status !== 'pending') throw new Error('New interactions must be pending');
3918
    if (interaction.contractRevision !== 1 || interaction.contractRevisions.length)
3919
      throw new Error('New interactions cannot supply contract revision history');
3920
    if (interaction.contract.interactionId !== interaction.interactionId)
3921
      throw new Error('Contract interaction ID does not match');
3922
    if (canonicalHash(interaction.contract) !== interaction.termsHash)
3923
      throw new Error('Contract hash does not match the immutable terms');
3924
    if (
3925
      interaction.contract.parties.length !== 2 ||
3926
      interaction.contract.parties[0] !== interaction.initiatorAgentId ||
3927
      interaction.contract.parties[1] !== interaction.responderAgentId
3928
    )
3929
      throw new Error('Contract parties do not match the interaction participants');
3930
    const acceptanceEntries = Object.entries(interaction.acceptances);
3931
    if (
3932
      acceptanceEntries.length !== 1 ||
3933
      acceptanceEntries[0]?.[0] !== interaction.initiatorAgentId ||
3934
      acceptanceEntries[0]?.[1].agentId !== interaction.initiatorAgentId ||
3935
      acceptanceEntries[0]?.[1].termsHash !== interaction.termsHash ||
3936
      !['oauth_installation', 'oauth_account'].includes(acceptanceEntries[0]?.[1].method ?? '')
3937
    )
3938
      throw new Error('A pending interaction requires only the initiator acceptance');
3939
    if (Date.parse(interaction.expiresAt) <= Date.now())
3940
      throw new Error('Interaction expiry must be in the future');
3941
    interaction = withContractRevisionHistory(interaction);
3942
    const owners = await this.sql`
3943
      SELECT agent_id, operator_id, card FROM openclasp_public_agents
3944
      WHERE agent_id IN (${interaction.initiatorAgentId}, ${interaction.responderAgentId})
3945
    `;
3946
    const initiator = owners.find((row) => row.agent_id === interaction.initiatorAgentId);
3947
    const responder = owners.find((row) => row.agent_id === interaction.responderAgentId);
3948
    if (!initiator || initiator.operator_id !== operatorId)
3949
      throw new Error('The initiating agent must be published and owned by this account');
3950
    if (!responder) throw new Error('Responder is not a published OpenClasp agent');
3951
    if (responder.operator_id === operatorId)
3952
      throw new Error('Federated interactions require agents from different accounts');
3953
    const responderCard = normalizePublicAgentCard(responder.card);
3954
    if (
3955
      !responderCard.transports.some(
3956
        (transport) =>
3957
          transport.endpoint === interaction.responderTransport.endpoint &&
3958
          transport.protocolBinding === interaction.responderTransport.protocolBinding,
3959
      )
3960
    )
3961
      throw new Error('Responder transport does not match its published Agent Card');
3962
    const encoded = JSON.stringify(interaction);
3963
    const rows = await this.sql`
3964
      INSERT INTO openclasp_federated_interactions(
3965
        interaction_id, initiator_operator_id, responder_operator_id,
3966
        initiator_agent_id, responder_agent_id, status, payload, expires_at
3967
      ) VALUES (
3968
        ${interaction.interactionId}, ${operatorId}, ${responder.operator_id},
3969
        ${interaction.initiatorAgentId}, ${interaction.responderAgentId},
3970
        ${interaction.status}, ${encoded}::jsonb, ${interaction.expiresAt}
3971
      )
3972
      RETURNING payload
3973
    `;
3974
    const stored = FederatedInteractionSchema.parse(rows[0]?.payload);
3975
    await this.journalFederatedInteraction(stored.interactionId);
3976
    const profiles = await this.sql`
3977
      SELECT payload FROM openclasp_records
3978
      WHERE operator_id = ${responder.operator_id}
3979
        AND kind = 'agent_profile'
3980
        AND record_id = ${interaction.responderAgentId}
3981
      LIMIT 1
3982
    `;
3983
    const responderProfile = profiles[0]?.payload as AgentProfile | undefined;
3984
    if (responderProfile && canAutoAcceptInteraction(responderProfile, stored))
3985
      return this.respondToFederatedInteraction(
3986
        String(responder.operator_id),
3987
        stored.interactionId,
3988
        stored.responderAgentId,
3989
        'accept',
3990
        'policy_auto_accept',
3991
      );
3992
    return stored;
3993
  }
3994
3995
  async listFederatedInteractions(operatorId: string): Promise<FederatedInteraction[]> {
3996
    await this.ensureSchema();
3997
    const expired = await this.sql`
3998
      UPDATE openclasp_federated_interactions
3999
      SET status = 'expired', payload = jsonb_set(payload, '{status}', '"expired"'::jsonb), updated_at = NOW()
4000
      WHERE status = 'pending' AND expires_at <= NOW()
4001
      RETURNING interaction_id
4002
    `;
4003
    await Promise.all(
4004
      expired.map((row) => this.journalFederatedInteraction(String(row.interaction_id))),
4005
    );
4006
    const rows = await this.sql`
4007
      SELECT payload FROM openclasp_federated_interactions
4008
      WHERE initiator_operator_id = ${operatorId} OR responder_operator_id = ${operatorId}
4009
      ORDER BY updated_at DESC
4010
    `;
4011
    return rows.map((row) =>
4012
      withContractRevisionHistory(FederatedInteractionSchema.parse(row.payload)),
4013
    );
4014
  }
4015
4016
  async getFederatedInteraction(
4017
    operatorId: string,
4018
    interactionId: string,
4019
  ): Promise<FederatedInteraction | undefined> {
4020
    const values = await this.listFederatedInteractions(operatorId);
4021
    return values.find((value) => value.interactionId === interactionId);
4022
  }
4023
4024
  async respondToFederatedInteraction(
4025
    operatorId: string,
4026
    interactionId: string,
4027
    agentId: string,
4028
    decision: 'accept' | 'reject',
4029
    method: 'oauth_installation' | 'oauth_account' | 'policy_auto_accept' = 'oauth_account',
4030
  ): Promise<FederatedInteraction> {
4031
    await this.ensureSchema();
4032
    const rows = await this.sql`
4033
      SELECT payload, responder_operator_id, responder_agent_id
4034
      FROM openclasp_federated_interactions WHERE interaction_id = ${interactionId}
4035
    `;
4036
    const row = rows[0];
4037
    if (!row) throw new Error('Interaction invitation not found');
4038
    if (row.responder_operator_id !== operatorId || row.responder_agent_id !== agentId)
4039
      throw new Error('Only the invited agent may respond');
4040
    const current = withContractRevisionHistory(FederatedInteractionSchema.parse(row.payload));
4041
    if (current.status !== 'pending') throw new Error('Invitation is no longer pending');
4042
    const now = new Date().toISOString();
4043
    if (Date.parse(current.expiresAt) <= Date.now()) {
4044
      const expired = { ...current, status: 'expired' as const, updatedAt: now };
4045
      await this.updateFederatedInteraction(
4046
        interactionId,
4047
        current.status,
4048
        expired,
4049
        current.updatedAt,
4050
      );
4051
      throw new Error('Invitation has expired');
4052
    }
4053
    const proposed = openContractRevision(current);
4054
    if (!proposed || proposed.termsHash !== current.termsHash)
4055
      throw new Error('No current contract proposal is available');
4056
    const acceptance = {
4057
      agentId,
4058
      method,
4059
      termsHash: current.termsHash,
4060
      acceptedAt: now,
4061
    };
4062
    const proposalBase: ContractRevision = {
4063
      ...proposed,
4064
      status: decision === 'accept' ? 'accepted' : 'rejected',
4065
      acceptances:
4066
        decision === 'accept'
4067
          ? { ...proposed.acceptances, [agentId]: acceptance }
4068
          : proposed.acceptances,
4069
      updatedAt: now,
4070
    };
4071
    const resolvedProposal =
4072
      decision === 'accept'
4073
        ? ContractRevisionSchema.parse({
4074
            ...proposalBase,
4075
            platformAttestation: attestSessionRecord(this.gatewaySecret(), proposalBase),
4076
          })
4077
        : proposalBase;
4078
    const next = FederatedInteractionSchema.parse({
4079
      ...current,
4080
      status: decision === 'accept' ? 'active' : 'rejected',
4081
      updatedAt: now,
4082
      acceptances: decision === 'accept' ? resolvedProposal.acceptances : current.acceptances,
4083
      contractRevisions: current.contractRevisions.map((revision) =>
4084
        revision.revisionId === resolvedProposal.revisionId ? resolvedProposal : revision,
4085
      ),
4086
    });
4087
    if (decision === 'accept') {
4088
      try {
4089
        await this.brokerLiveSession(next);
4090
      } catch (error) {
4091
        const reason = error instanceof Error ? error.message.slice(0, 500) : 'Live session failed';
4092
        await this.sql`
4093
          UPDATE openclasp_agent_runtimes SET last_error = ${reason}, updated_at = NOW()
4094
          WHERE agent_id IN (${current.initiatorAgentId}, ${current.responderAgentId})
4095
        `;
4096
        throw error;
4097
      }
4098
    }
4099
    const updated = await this.updateFederatedInteraction(
4100
      interactionId,
4101
      current.status,
4102
      next,
4103
      current.updatedAt,
4104
    );
4105
    if (!updated) throw new Error('Invitation was already handled');
4106
    return next;
4107
  }
4108
4109
  async proposeContractRevision(
4110
    operatorId: string,
4111
    interactionId: string,
4112
    agentId: string,
4113
    contract: InteractionContract,
4114
    expectedTermsHash?: string,
4115
    method: 'oauth_installation' | 'oauth_account' = 'oauth_account',
4116
  ): Promise<FederatedInteraction> {
4117
    await this.ensureSchema();
4118
    const rows = await this.sql`
4119
      SELECT payload, initiator_operator_id, responder_operator_id,
4120
        initiator_agent_id, responder_agent_id
4121
      FROM openclasp_federated_interactions
4122
      WHERE interaction_id = ${interactionId}
4123
    `;
4124
    const row = rows[0];
4125
    if (!row) throw new Error('Interaction not found');
4126
    const ownsParticipant =
4127
      (row.initiator_operator_id === operatorId && row.initiator_agent_id === agentId) ||
4128
      (row.responder_operator_id === operatorId && row.responder_agent_id === agentId);
4129
    if (!ownsParticipant) throw new Error('Only a participating agent may propose contract terms');
4130
    const current = withContractRevisionHistory(FederatedInteractionSchema.parse(row.payload));
4131
    if (!['pending', 'active'].includes(current.status))
4132
      throw new Error('This interaction no longer accepts contract proposals');
4133
    const parsedContract = InteractionContractSchema.parse(contract);
4134
    if (parsedContract.interactionId !== interactionId)
4135
      throw new Error('Contract interaction ID does not match');
4136
    if (
4137
      parsedContract.parties.length !== 2 ||
4138
      parsedContract.parties[0] !== current.initiatorAgentId ||
4139
      parsedContract.parties[1] !== current.responderAgentId
4140
    )
4141
      throw new Error('Contract parties cannot change during negotiation');
4142
    const open = openContractRevision(current);
4143
    const expected = open?.termsHash ?? current.termsHash;
4144
    if (expectedTermsHash && expectedTermsHash !== expected)
4145
      throw new Error('Contract terms changed; fetch the interaction before countering');
4146
    const termsHash = canonicalHash(parsedContract);
4147
    if (termsHash === expected) throw new Error('Proposed terms are unchanged');
4148
    const now = new Date().toISOString();
4149
    const revisionNumber =
4150
      Math.max(
4151
        current.contractRevision,
4152
        ...current.contractRevisions.map((item) => item.revision),
4153
      ) + 1;
4154
    const acceptance = {
4155
      agentId,
4156
      method,
4157
      termsHash,
4158
      acceptedAt: now,
4159
    } as const;
4160
    const revision = ContractRevisionSchema.parse({
4161
      revisionId: crypto.randomUUID(),
4162
      interactionId,
4163
      revision: revisionNumber,
4164
      previousTermsHash: expected,
4165
      termsHash,
4166
      contract: parsedContract,
4167
      proposedByAgentId: agentId,
4168
      status: 'proposed',
4169
      acceptances: { [agentId]: acceptance },
4170
      createdAt: now,
4171
      updatedAt: now,
4172
    });
4173
    const revisions = [
4174
      ...current.contractRevisions.map((item) =>
4175
        item.status === 'proposed'
4176
          ? ContractRevisionSchema.parse({ ...item, status: 'superseded', updatedAt: now })
4177
          : item,
4178
      ),
4179
      revision,
4180
    ];
4181
    const next = FederatedInteractionSchema.parse({
4182
      ...current,
4183
      ...(current.status === 'pending'
4184
        ? {
4185
            contract: parsedContract,
4186
            termsHash,
4187
            acceptances: revision.acceptances,
4188
            contractRevision: revisionNumber,
4189
          }
4190
        : {}),
4191
      contractRevisions: revisions,
4192
      updatedAt: now,
4193
    });
4194
    const updated = await this.updateFederatedInteraction(
4195
      interactionId,
4196
      current.status,
4197
      next,
4198
      current.updatedAt,
4199
    );
4200
    if (!updated) throw new Error('Contract changed concurrently; fetch it and retry');
4201
    return next;
4202
  }
4203
4204
  async respondToContractRevision(
4205
    operatorId: string,
4206
    interactionId: string,
4207
    agentId: string,
4208
    revisionId: string,
4209
    decision: 'accept' | 'reject',
4210
    method: 'oauth_installation' | 'oauth_account' = 'oauth_account',
4211
  ): Promise<FederatedInteraction> {
4212
    await this.ensureSchema();
4213
    const rows = await this.sql`
4214
      SELECT payload, initiator_operator_id, responder_operator_id,
4215
        initiator_agent_id, responder_agent_id
4216
      FROM openclasp_federated_interactions
4217
      WHERE interaction_id = ${interactionId}
4218
    `;
4219
    const row = rows[0];
4220
    if (!row) throw new Error('Interaction not found');
4221
    const ownsParticipant =
4222
      (row.initiator_operator_id === operatorId && row.initiator_agent_id === agentId) ||
4223
      (row.responder_operator_id === operatorId && row.responder_agent_id === agentId);
4224
    if (!ownsParticipant) throw new Error('Only a participating agent may respond to terms');
4225
    const current = withContractRevisionHistory(FederatedInteractionSchema.parse(row.payload));
4226
    if (!['pending', 'active'].includes(current.status))
4227
      throw new Error('This interaction no longer accepts contract responses');
4228
    const proposal = openContractRevision(current);
4229
    if (!proposal || proposal.revisionId !== revisionId)
4230
      throw new Error('Contract proposal is no longer current');
4231
    if (proposal.acceptances[agentId]) throw new Error('Agent already accepted this proposal');
4232
    const now = new Date().toISOString();
4233
    const acceptances =
4234
      decision === 'accept'
4235
        ? {
4236
            ...proposal.acceptances,
4237
            [agentId]: { agentId, method, termsHash: proposal.termsHash, acceptedAt: now },
4238
          }
4239
        : proposal.acceptances;
4240
    const fullyAccepted = current.contract.parties.every((party) => party in acceptances);
4241
    const revisionBase = ContractRevisionSchema.parse({
4242
      ...proposal,
4243
      status: decision === 'reject' ? 'rejected' : fullyAccepted ? 'accepted' : 'proposed',
4244
      acceptances,
4245
      updatedAt: now,
4246
    });
4247
    const revision =
4248
      fullyAccepted && decision === 'accept'
4249
        ? ContractRevisionSchema.parse({
4250
            ...revisionBase,
4251
            platformAttestation: attestSessionRecord(this.gatewaySecret(), revisionBase),
4252
          })
4253
        : revisionBase;
4254
    const next = FederatedInteractionSchema.parse({
4255
      ...current,
4256
      status:
4257
        current.status === 'pending'
4258
          ? decision === 'reject'
4259
            ? 'rejected'
4260
            : fullyAccepted
4261
              ? 'active'
4262
              : 'pending'
4263
          : 'active',
4264
      ...(fullyAccepted
4265
        ? {
4266
            contract: revision.contract,
4267
            termsHash: revision.termsHash,
4268
            acceptances: revision.acceptances,
4269
            contractRevision: revision.revision,
4270
          }
4271
        : {}),
4272
      contractRevisions: current.contractRevisions.map((item) =>
4273
        item.revisionId === revision.revisionId ? revision : item,
4274
      ),
4275
      updatedAt: now,
4276
    });
4277
    if (current.status === 'pending' && decision === 'accept' && fullyAccepted)
4278
      await this.brokerLiveSession(next);
4279
    const updated = await this.updateFederatedInteraction(
4280
      interactionId,
4281
      current.status,
4282
      next,
4283
      current.updatedAt,
4284
    );
4285
    if (!updated) throw new Error('Contract changed concurrently; fetch it and retry');
4286
    return next;
4287
  }
4288
4289
  private async updateFederatedInteraction(
4290
    interactionId: string,
4291
    expectedStatus: string,
4292
    value: FederatedInteraction,
4293
    expectedUpdatedAt?: string,
4294
  ): Promise<boolean> {
4295
    const encoded = JSON.stringify(FederatedInteractionSchema.parse(value));
4296
    const rows = await this.sql`
4297
      UPDATE openclasp_federated_interactions
4298
      SET status = ${value.status}, payload = ${encoded}::jsonb, updated_at = NOW()
4299
      WHERE interaction_id = ${interactionId} AND status = ${expectedStatus}
4300
        AND (${expectedUpdatedAt ?? null}::text IS NULL OR payload->>'updatedAt' = ${expectedUpdatedAt ?? null})
4301
      RETURNING interaction_id
4302
    `;
4303
    if (rows.length) await this.journalFederatedInteraction(interactionId);
4304
    return rows.length > 0;
4305
  }
4306
4307
  async getSettings(operatorId: string): Promise<AccountSettings> {
4308
    await this.ensureSchema();
4309
    const rows = await this.sql`
4310
      SELECT display_name, contribution_enabled
4311
      FROM openclasp_account_settings
4312
      WHERE operator_id = ${operatorId}
4313
    `;
4314
    const row = rows[0] as
4315
      | {
4316
          display_name: string;
4317
          contribution_enabled: boolean;
4318
        }
4319
      | undefined;
4320
    return row
4321
      ? {
4322
          displayName: row.display_name,
4323
          contributionEnabled: row.contribution_enabled,
4324
          rawConversationsStored: false,
4325
        }
4326
      : defaults;
4327
  }
4328
4329
  async saveSettings(
4330
    operatorId: string,
4331
    settings: Omit<AccountSettings, 'rawConversationsStored'>,
4332
  ): Promise<AccountSettings> {
4333
    await this.ensureSchema();
4334
    await this.sql`
4335
      INSERT INTO openclasp_account_settings(
4336
        operator_id, display_name, contribution_enabled
4337
      ) VALUES (
4338
        ${operatorId}, ${settings.displayName}, ${settings.contributionEnabled}
4339
      )
4340
      ON CONFLICT (operator_id) DO UPDATE SET
4341
        display_name = EXCLUDED.display_name,
4342
        contribution_enabled = EXCLUDED.contribution_enabled,
4343
        updated_at = NOW()
4344
    `;
4345
    return { ...settings, rawConversationsStored: false };
4346
  }
4347
}