@xfey/tutti 0.1.110 → 0.1.112

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/README.md +1 -1
  2. package/dist/approvals/decision-command.d.ts +2 -2
  3. package/dist/chat-assistant/index.d.ts +1 -1
  4. package/dist/chat-assistant/index.js +26 -6
  5. package/dist/collaboration-ingestion/clarification.d.ts +25 -0
  6. package/dist/collaboration-ingestion/clarification.js +63 -0
  7. package/dist/collaboration-ingestion/external-source.d.ts +2 -0
  8. package/dist/collaboration-ingestion/external-source.js +35 -0
  9. package/dist/collaboration-ingestion/index.d.ts +2 -0
  10. package/dist/collaboration-ingestion/index.js +2 -0
  11. package/dist/collaboration-ingestion/main-chat.js +9 -2
  12. package/dist/collaboration-ingestion/platform-clarification.d.ts +10 -0
  13. package/dist/collaboration-ingestion/platform-clarification.js +42 -0
  14. package/dist/collaboration-ingestion/types.d.ts +1 -0
  15. package/dist/collaboration-state/clarification-messages.js +3 -2
  16. package/dist/collaboration-state/clarification-records.d.ts +1 -1
  17. package/dist/collaboration-state/clarification-records.js +7 -5
  18. package/dist/collaboration-state/clarification-rounds.js +5 -5
  19. package/dist/collaboration-state/clarification-snapshot.d.ts +228 -0
  20. package/dist/collaboration-state/clarification-snapshot.js +29 -0
  21. package/dist/collaboration-state/clarification-successors.js +6 -6
  22. package/dist/collaboration-state/index.d.ts +1 -0
  23. package/dist/collaboration-state/index.js +1 -0
  24. package/dist/collaboration-state/run-recording.js +1 -1
  25. package/dist/collaboration-state/task-compile-context.d.ts +1 -1
  26. package/dist/collaboration-state/types.d.ts +1 -0
  27. package/dist/control-plane/clarification-commands.d.ts +3 -2
  28. package/dist/control-plane/clarification-commands.js +13 -4
  29. package/dist/control-plane/follow-up-context.js +8 -14
  30. package/dist/control-plane/index.d.ts +3 -2
  31. package/dist/control-plane/scratchpad-source-messages.js +9 -3
  32. package/dist/control-plane/task-compile-clarification-context.js +7 -2
  33. package/dist/control-plane/task-source-context-types.d.ts +1 -1
  34. package/dist/control-plane/task-source-context.js +4 -2
  35. package/dist/control-plane/types.d.ts +1 -0
  36. package/dist/control-plane/workflows/openai.js +5 -4
  37. package/dist/platform-outbound/repository.js +83 -4
  38. package/dist/server-shell/cli/host-lifecycle.js +1 -0
  39. package/dist/server-shell/cli/host-server-runtime.d.ts +2 -1
  40. package/dist/server-shell/cli/host-server-runtime.js +4 -2
  41. package/dist/server-shell/cli/relay-registration.js +2 -0
  42. package/dist/server-shell/http/host-tunnel.d.ts +2 -1
  43. package/dist/server-shell/http/host-tunnel.js +4 -1
  44. package/dist/server-shell/http/integration-clarification-dispatch.d.ts +6 -0
  45. package/dist/server-shell/http/integration-clarification-dispatch.js +43 -0
  46. package/dist/server-shell/http/routes/agent-context-reference-routes.js +1 -3
  47. package/dist/server-shell/http/routes/project-api/clarification-routes.js +16 -42
  48. package/dist/server-shell/http/routes/project-api/openapi-collaboration-routes.d.ts +2 -0
  49. package/dist/server-shell/http/routes/project-api/openapi-workspace-routes.d.ts +1 -0
  50. package/dist/server-shell/http/routes/project-api/openapi.d.ts +3 -0
  51. package/dist/server-shell/http/routes/project-api/schemas.d.ts +1 -0
  52. package/migrations/0022_clarification_projection_cursors.sql +7 -0
  53. package/node_modules/@tutti/relay-client/dist/host-control.d.ts +2 -0
  54. package/node_modules/@tutti/relay-client/dist/host-control.js +6 -0
  55. package/node_modules/@tutti/shared/dist/schemas/api/clarifications.d.ts +14 -1
  56. package/node_modules/@tutti/shared/dist/schemas/api/clarifications.js +9 -2
  57. package/node_modules/@tutti/shared/dist/schemas/api/messages.d.ts +4 -0
  58. package/node_modules/@tutti/shared/dist/schemas/api/messages.js +1 -0
  59. package/node_modules/@tutti/shared/dist/schemas/api/viewer-reference.d.ts +1 -0
  60. package/node_modules/@tutti/shared/dist/schemas/internal/index.d.ts +2 -0
  61. package/node_modules/@tutti/shared/dist/schemas/internal/index.js +2 -0
  62. package/node_modules/@tutti/shared/dist/schemas/internal/platform-clarification-content.d.ts +15 -0
  63. package/node_modules/@tutti/shared/dist/schemas/internal/platform-clarification-content.js +24 -0
  64. package/node_modules/@tutti/shared/dist/schemas/internal/platform-clarification.d.ts +354 -0
  65. package/node_modules/@tutti/shared/dist/schemas/internal/platform-clarification.js +94 -0
  66. package/node_modules/@tutti/shared/dist/schemas/internal/platform-integration.d.ts +132 -5
  67. package/node_modules/@tutti/shared/dist/schemas/internal/platform-integration.js +29 -5
  68. package/node_modules/@tutti/shared/dist/utils/clarification-participants.d.ts +4 -0
  69. package/node_modules/@tutti/shared/dist/utils/clarification-participants.js +11 -0
  70. package/node_modules/@tutti/shared/dist/utils/index.d.ts +1 -0
  71. package/node_modules/@tutti/shared/dist/utils/index.js +1 -0
  72. package/package.json +1 -1
  73. package/prompts/chat-assistant.md +1 -0
  74. package/prompts/procedures/follow-up-check.md +4 -4
  75. package/prompts/procedures/scratchpad-refresh.md +22 -2
  76. package/prompts/procedures/task-compile.md +1 -0
  77. package/prompts/runs/task-execute.md +2 -0
  78. package/web/assets/{homepage-motion-scene-rxYjg-zc.js → homepage-motion-scene-BwdH6Iss.js} +1 -1
  79. package/web/assets/index-B29RuNsd.js +69 -0
  80. package/web/index.html +1 -1
  81. package/web/assets/index-BrKWdtKZ.js +0 -69
@@ -1,6 +1,7 @@
1
- import { createIdempotencyKey } from "@tutti/shared/ids";
1
+ import { createIdempotencyKey, } from "@tutti/shared/ids";
2
2
  import { Value } from "@sinclair/typebox/value";
3
3
  import { PlatformOutboundContentSchema, } from "@tutti/shared/schemas/internal";
4
+ import { readClarificationRoundProjection, readClarificationThreadResponse, } from "../collaboration-state/index.js";
4
5
  const MAX_PLATFORM_OUTBOUND_TEXT_LENGTH = 4_000;
5
6
  function boundedPlatformOutboundText(value) {
6
7
  const characters = [...value.trim()];
@@ -134,10 +135,13 @@ export function materializePlatformStatusOutboundIntents(db, now = () => new Dat
134
135
  subjectRef: roundRef,
135
136
  targetKind: "project",
136
137
  canonicalMessageId: candidate.id,
138
+ content: clarificationContent(db, roundRef),
137
139
  now,
138
140
  });
139
141
  if (result?.created === true) {
140
142
  created.push(result.intent);
143
+ const content = clarificationContent(db, roundRef);
144
+ db.prepare("INSERT OR IGNORE INTO platform_clarification_projection_cursors (round_id, binding_ref, version) VALUES (?, ?, ?)").run(roundRef, result.intent.binding_ref, content.version);
141
145
  }
142
146
  continue;
143
147
  }
@@ -162,9 +166,61 @@ export function materializePlatformStatusOutboundIntents(db, now = () => new Dat
162
166
  created.push(result.intent);
163
167
  }
164
168
  }
169
+ for (const cursor of db
170
+ .prepare("SELECT round_id, binding_ref, version FROM platform_clarification_projection_cursors WHERE closed = 0")
171
+ .all()) {
172
+ const content = clarificationContent(db, cursor.round_id);
173
+ if (content === null || readLatestFeishuDestination(db)?.binding_ref !== cursor.binding_ref) {
174
+ db.prepare("UPDATE platform_clarification_projection_cursors SET closed = 1 WHERE round_id = ?").run(cursor.round_id);
175
+ continue;
176
+ }
177
+ if (content.version <= cursor.version)
178
+ continue;
179
+ const result = createPlatformStatusOutboundIntent(db, {
180
+ eventKind: "needs_input",
181
+ eventKey: `clarification_round:${cursor.round_id}:state:${content.version}`,
182
+ subjectKind: "clarification_round",
183
+ subjectRef: cursor.round_id,
184
+ targetKind: "project",
185
+ content,
186
+ now,
187
+ });
188
+ if (result !== null) {
189
+ db.prepare("UPDATE platform_clarification_projection_cursors SET version = ?, closed = ? WHERE round_id = ?").run(content.version, content.state === "closed" ? 1 : 0, cursor.round_id);
190
+ if (result.created)
191
+ created.push(result.intent);
192
+ }
193
+ }
165
194
  return created;
166
195
  })();
167
196
  }
197
+ function clarificationContent(db, roundId) {
198
+ const round = readClarificationRoundProjection(db, roundId);
199
+ if (round === null)
200
+ return null;
201
+ const thread = readClarificationThreadResponse(db, round.thread_id)?.thread;
202
+ const state = thread?.status !== "active" || thread.current_round_id !== round.id
203
+ ? "closed"
204
+ : round.successor_status === "materialized"
205
+ ? "continued"
206
+ : round.status === "sealed"
207
+ ? "submitted"
208
+ : "writable";
209
+ const answerRevision = round.answer_revision ?? 0;
210
+ return {
211
+ kind: "clarification",
212
+ round_id: round.id,
213
+ answer_revision: answerRevision,
214
+ version: answerRevision * 4 + { writable: 0, submitted: 1, continued: 2, closed: 3 }[state],
215
+ state,
216
+ ...(state === "closed" && thread?.closed_reason !== undefined
217
+ ? { closed_reason: thread.closed_reason }
218
+ : {}),
219
+ title: (round.request_payload.title ?? "Needs your input").slice(0, 512),
220
+ request: round.request_payload.request.slice(0, 4_000),
221
+ opened_at: round.created_at,
222
+ };
223
+ }
168
224
  export function createPlatformOutboundIntent(tx, input) {
169
225
  const existing = tx
170
226
  .prepare("SELECT * FROM platform_outbound_intents WHERE canonical_message_id = ?")
@@ -172,13 +228,27 @@ export function createPlatformOutboundIntent(tx, input) {
172
228
  if (existing !== undefined) {
173
229
  return { created: false, intent: existing };
174
230
  }
231
+ // An H5 round answer can finish before the periodic status worker observes the
232
+ // opening card. Capture its destination first, in this same transaction.
233
+ if (tx
234
+ .prepare("SELECT 1 FROM messages WHERE id = ? AND scope_kind = 'clarification_round'")
235
+ .get(input.triggerMessageId) !== undefined)
236
+ materializePlatformStatusOutboundIntents(tx, input.now);
175
237
  const destination = tx
176
238
  .prepare(`
177
239
  SELECT platform, installation_ref, binding_ref
178
240
  FROM external_source_mappings
179
241
  WHERE message_id = ? AND platform = 'feishu'
180
242
  `)
181
- .get(input.triggerMessageId);
243
+ .get(input.triggerMessageId) ??
244
+ tx
245
+ .prepare(`
246
+ SELECT i.platform, i.installation_ref, i.binding_ref FROM messages m
247
+ JOIN platform_clarification_projection_cursors c ON c.round_id = m.clarification_round_id
248
+ JOIN platform_outbound_intents i ON i.event_key = 'clarification_round:' || c.round_id || ':opened'
249
+ WHERE m.id = ? AND m.scope_kind = 'clarification_round'
250
+ `)
251
+ .get(input.triggerMessageId);
182
252
  if (destination === undefined) {
183
253
  return null;
184
254
  }
@@ -216,7 +286,7 @@ export function claimDuePlatformOutboundIntent(db, now = () => new Date()) {
216
286
  const row = db
217
287
  .prepare(`
218
288
  SELECT platform_outbound_intents.*,
219
- COALESCE(platform_outbound_intents.content_text, messages.body) AS body
289
+ COALESCE(platform_outbound_intents.content_text, messages.body) AS body, messages.clarification_round_id
220
290
  FROM platform_outbound_intents
221
291
  LEFT JOIN messages ON messages.id = platform_outbound_intents.canonical_message_id
222
292
  WHERE platform_outbound_intents.disposition IN ('pending', 'retry_wait')
@@ -243,7 +313,16 @@ export function claimDuePlatformOutboundIntent(db, now = () => new Date()) {
243
313
  }
244
314
  else if (row.body !== null) {
245
315
  const body = boundedPlatformOutboundText(row.body);
246
- content = body.length === 0 ? null : { kind: "text", text: body };
316
+ content =
317
+ body.length === 0
318
+ ? null
319
+ : {
320
+ kind: "text",
321
+ text: body,
322
+ ...(row.clarification_round_id == null
323
+ ? {}
324
+ : { clarification_round_ref: row.clarification_round_id }),
325
+ };
247
326
  }
248
327
  if (content === null) {
249
328
  return null;
@@ -311,6 +311,7 @@ export async function startLaunchProject(options) {
311
311
  projectSurface: preparation.machine_local.binding.surface_profile.kind,
312
312
  trustedMetadataStore: host.trusted_relay_metadata_store,
313
313
  integrationDelivery: host.integration_delivery,
314
+ integrationClarification: host.integration_clarification,
314
315
  integrationReferenceAttachment: host.integration_reference_attachment,
315
316
  integrationManagedReference: host.integration_managed_reference,
316
317
  integrationApprovalDecision: host.integration_approval_decision,
@@ -1,6 +1,6 @@
1
1
  import type { FastifyInstance } from "fastify";
2
2
  import { type ExecutionStatusProjection } from "@tutti/shared/schemas/api";
3
- import type { IntegrationApprovalDecisionDisposition, IntegrationDeliveryDisposition, IntegrationManagedReferenceDisposition, TrustedApprovalDecisionCommand, TrustedConversationSourceIngestionCommand, TrustedManagedReferenceIngestionCommand, TrustedReferenceAttachmentIngestionCommand } from "@tutti/shared/schemas/internal";
3
+ import type { IntegrationApprovalDecisionDisposition, IntegrationClarificationResponse, TrustedClarificationCommand, IntegrationDeliveryDisposition, IntegrationManagedReferenceDisposition, TrustedApprovalDecisionCommand, TrustedConversationSourceIngestionCommand, TrustedManagedReferenceIngestionCommand, TrustedReferenceAttachmentIngestionCommand } from "@tutti/shared/schemas/internal";
4
4
  import { type HostProjectStore } from "../../store/index.js";
5
5
  import type { createHostServer } from "../http/create-server.js";
6
6
  import type { HostLocalLaunchStatus } from "../http/routes/local-control.js";
@@ -19,6 +19,7 @@ export type HostServerHandle = {
19
19
  runtime_endpoint: MachineRuntimeEndpointRecord;
20
20
  trusted_relay_metadata_store: TrustedRelaySessionMetadataStore;
21
21
  integration_delivery: (command: TrustedConversationSourceIngestionCommand) => IntegrationDeliveryDisposition;
22
+ integration_clarification: (command: TrustedClarificationCommand) => IntegrationClarificationResponse;
22
23
  integration_approval_decision: (command: TrustedApprovalDecisionCommand) => IntegrationApprovalDecisionDisposition;
23
24
  integration_reference_attachment: (command: TrustedReferenceAttachmentIngestionCommand) => Promise<IntegrationDeliveryDisposition>;
24
25
  integration_managed_reference: (command: TrustedManagedReferenceIngestionCommand) => Promise<IntegrationManagedReferenceDisposition>;
@@ -6,7 +6,7 @@ import { ApprovalRequestManager, executeApprovalDecisionCommand } from "../../ap
6
6
  import { ArtifactPreviewManager } from "../../artifacts/index.js";
7
7
  import { ReadOnlyChatAssistant } from "../../chat-assistant/index.js";
8
8
  import { CheckProcessCoordinator } from "../../checks/index.js";
9
- import { ingestTrustedConversationSource, ingestTrustedManagedReference, ingestTrustedReferenceAttachment, } from "../../collaboration-ingestion/index.js";
9
+ import { ingestTrustedConversationSource, executeTrustedClarification, ingestTrustedManagedReference, ingestTrustedReferenceAttachment, } from "../../collaboration-ingestion/index.js";
10
10
  import { ensureInitialProjectWelcomeMessage } from "../../collaboration-state/index.js";
11
11
  import { Phase5ControlPlane } from "../../control-plane/index.js";
12
12
  import { resolveProjectOpenAiProcedureWorkflowRunner } from "../../control-plane/workflows/index.js";
@@ -447,7 +447,7 @@ export async function startForegroundHostServer(options) {
447
447
  ...(platformOutboundCoordinator === undefined
448
448
  ? {}
449
449
  : {
450
- onMainChatAgentMessageCreated: ({ tx, triggerMessage, responseMessage }) => platformOutboundCoordinator.captureAgentResponse(tx, {
450
+ onAgentMessageCreated: ({ tx, triggerMessage, responseMessage }) => platformOutboundCoordinator.captureAgentResponse(tx, {
451
451
  triggerMessage,
452
452
  responseMessage,
453
453
  }),
@@ -714,6 +714,7 @@ export async function startForegroundHostServer(options) {
714
714
  trusted_relay_metadata_store: trustedMetadataStore,
715
715
  integration_delivery: (command) => ingestTrustedConversationSource({
716
716
  store,
717
+ controlPlane,
717
718
  events: workspaceEvents,
718
719
  command,
719
720
  scratchpadSource: {
@@ -722,6 +723,7 @@ export async function startForegroundHostServer(options) {
722
723
  chatAssistant,
723
724
  now,
724
725
  }),
726
+ integration_clarification: (command) => executeTrustedClarification({ store, controlPlane, command, now }),
725
727
  integration_reference_attachment: async (command) => await ingestTrustedReferenceAttachment({
726
728
  store,
727
729
  workspaceRoot: options.preparation.workspace_root,
@@ -37,6 +37,8 @@ export async function registerHostWithRelay(options) {
37
37
  joinToken: { mode: options.joinTokenMode ?? "rotate" },
38
38
  capabilities: {
39
39
  integration_source_ingestion_v1: true,
40
+ integration_development_bot_v1: true,
41
+ integration_clarification_v1: true,
40
42
  integration_external_references_v1: true,
41
43
  integration_attachment_ingestion_v1: true,
42
44
  integration_managed_reference_ingestion_v1: true,
@@ -1,12 +1,13 @@
1
1
  import type { FastifyInstance } from "fastify";
2
2
  import type { RelayHostTunnelRequestHandler, RelayHostTunnelStreamRequestHandler } from "@tutti/relay-client";
3
- import type { IntegrationApprovalDecisionDisposition, IntegrationDeliveryDisposition, IntegrationManagedReferenceDisposition, TrustedApprovalDecisionCommand, TrustedConversationSourceIngestionCommand, TrustedManagedReferenceIngestionCommand, TrustedReferenceAttachmentIngestionCommand } from "@tutti/shared/schemas/internal";
3
+ import type { IntegrationApprovalDecisionDisposition, IntegrationClarificationResponse, TrustedClarificationCommand, IntegrationDeliveryDisposition, IntegrationManagedReferenceDisposition, TrustedApprovalDecisionCommand, TrustedConversationSourceIngestionCommand, TrustedManagedReferenceIngestionCommand, TrustedReferenceAttachmentIngestionCommand } from "@tutti/shared/schemas/internal";
4
4
  import type { ProjectSurface } from "@tutti/shared/domain";
5
5
  import { type TrustedRelaySessionMetadataStore } from "../session/relay-session-context.js";
6
6
  export type HostProjectApiTunnelDispatchOptions = {
7
7
  app: FastifyInstance;
8
8
  projectSurface?: ProjectSurface;
9
9
  trustedMetadataStore: TrustedRelaySessionMetadataStore;
10
+ integrationClarification?: (command: TrustedClarificationCommand) => IntegrationClarificationResponse;
10
11
  integrationDelivery?: (command: TrustedConversationSourceIngestionCommand) => IntegrationDeliveryDisposition;
11
12
  integrationApprovalDecision?: (command: TrustedApprovalDecisionCommand) => IntegrationApprovalDecisionDisposition;
12
13
  integrationReferenceAttachment?: (command: TrustedReferenceAttachmentIngestionCommand) => Promise<IntegrationDeliveryDisposition>;
@@ -1,4 +1,5 @@
1
- import { INTEGRATION_APPROVAL_DECISION_TUNNEL_PATH, INTEGRATION_MANAGED_REFERENCE_TUNNEL_PATH, INTEGRATION_REFERENCE_ATTACHMENT_TUNNEL_PATH, isTrustedApprovalDecisionCommand, isTrustedConversationSourceIngestionCommand, isTrustedManagedReferenceIngestionCommand, isTrustedReferenceAttachmentIngestionCommand, } from "@tutti/shared/schemas/internal";
1
+ import { dispatchIntegrationClarification } from "./integration-clarification-dispatch.js";
2
+ import { INTEGRATION_APPROVAL_DECISION_TUNNEL_PATH, INTEGRATION_CLARIFICATION_TUNNEL_PATH, INTEGRATION_MANAGED_REFERENCE_TUNNEL_PATH, INTEGRATION_REFERENCE_ATTACHMENT_TUNNEL_PATH, isTrustedApprovalDecisionCommand, isTrustedConversationSourceIngestionCommand, isTrustedManagedReferenceIngestionCommand, isTrustedReferenceAttachmentIngestionCommand, } from "@tutti/shared/schemas/internal";
2
3
  import { TRUSTED_INTEGRATION_INGESTION_PATH, TRUSTED_INTEGRATION_REFERENCE_ATTACHMENT_PATH, } from "../../collaboration-ingestion/index.js";
3
4
  import { TRUSTED_RELAY_METADATA_REQUEST_HEADER, } from "../session/relay-session-context.js";
4
5
  function browserMetadataMatchesSurface(projectSurface, request) {
@@ -78,6 +79,8 @@ function dispatchIntegrationDelivery(options, request) {
78
79
  if (request.path === INTEGRATION_APPROVAL_DECISION_TUNNEL_PATH) {
79
80
  return dispatchIntegrationApprovalDecision(options, request);
80
81
  }
82
+ if (request.path === INTEGRATION_CLARIFICATION_TUNNEL_PATH)
83
+ return Promise.resolve(dispatchIntegrationClarification(options, request));
81
84
  if (request.path === INTEGRATION_REFERENCE_ATTACHMENT_TUNNEL_PATH) {
82
85
  return dispatchIntegrationReferenceAttachment(options, request);
83
86
  }
@@ -0,0 +1,6 @@
1
+ import type { RelayHostTunnelRequest, RelayHostTunnelResponse } from "@tutti/relay-client";
2
+ import { type IntegrationClarificationResponse, type TrustedClarificationCommand } from "@tutti/shared/schemas/internal";
3
+ export declare function dispatchIntegrationClarification(options: {
4
+ integrationClarification?: (command: TrustedClarificationCommand) => IntegrationClarificationResponse;
5
+ }, request: RelayHostTunnelRequest): RelayHostTunnelResponse;
6
+ //# sourceMappingURL=integration-clarification-dispatch.d.ts.map
@@ -0,0 +1,43 @@
1
+ import { INTEGRATION_CLARIFICATION_TUNNEL_PATH, isTrustedClarificationCommand, } from "@tutti/shared/schemas/internal";
2
+ export function dispatchIntegrationClarification(options, request) {
3
+ const finish = (body) => ({
4
+ request_id: request.request_id,
5
+ status_code: 200,
6
+ headers: { "content-type": "application/json; charset=utf-8" },
7
+ body_base64: Buffer.from(JSON.stringify(body), "utf8").toString("base64"),
8
+ });
9
+ const invalid = (reason_code) => finish({ protocol_version: 1, disposition: { kind: "invalid", reason_code } });
10
+ if (request.method !== "POST" ||
11
+ request.path !== INTEGRATION_CLARIFICATION_TUNNEL_PATH ||
12
+ !("integration_delivery" in request.metadata))
13
+ return invalid("internal_route_invalid");
14
+ let command;
15
+ try {
16
+ command = JSON.parse(Buffer.from(request.body_base64 ?? "", "base64").toString("utf8"));
17
+ }
18
+ catch {
19
+ return invalid("trusted_command_invalid");
20
+ }
21
+ if (!isTrustedClarificationCommand(command))
22
+ return invalid("trusted_command_invalid");
23
+ const metadata = request.metadata.integration_delivery;
24
+ if (!("operation_ref" in metadata) ||
25
+ command.operation_ref !== metadata.operation_ref ||
26
+ command.binding_ref !== metadata.binding_ref ||
27
+ command.relay_project_ref !== metadata.relay_project_ref ||
28
+ command.actor.verified_at !== metadata.verified_at ||
29
+ command.conversation.installation.platform !== metadata.installation.platform ||
30
+ command.conversation.installation.installation_ref !== metadata.installation.installation_ref)
31
+ return invalid("trusted_metadata_mismatch");
32
+ if (options.integrationClarification === undefined)
33
+ return finish({
34
+ protocol_version: 1,
35
+ disposition: {
36
+ kind: "unavailable",
37
+ retryable: true,
38
+ reason_code: "integration_clarification_unsupported",
39
+ },
40
+ });
41
+ return finish(options.integrationClarification(command));
42
+ }
43
+ //# sourceMappingURL=integration-clarification-dispatch.js.map
@@ -99,9 +99,7 @@ export function registerAgentContextReferenceRoutes(app, options) {
99
99
  request.log.warn({
100
100
  ...diagnosticFields,
101
101
  stage: "local_reference_unavailable",
102
- reason_code: error instanceof WorkspaceOpsError
103
- ? error.code
104
- : "reference_read_unavailable",
102
+ reason_code: error instanceof WorkspaceOpsError ? error.code : "reference_read_unavailable",
105
103
  elapsed_ms: Math.round(performance.now() - started),
106
104
  }, "Local Reference read unavailable");
107
105
  if (error instanceof WorkspaceOpsError) {
@@ -1,9 +1,8 @@
1
1
  import { ActiveClarificationProjectionSchema, GetClarificationRoundMessagesRequestSchema, GetClarificationRoundResponseSchema, GetClarificationThreadResponseSchema, MessageWindowSchema, SendClarificationRoundMessageBodySchema, SendClarificationRoundMessageResponseSchema, SubmitClarificationRoundBodySchema, SubmitClarificationRoundResponseSchema, } from "@tutti/shared/schemas/api";
2
2
  import { readActiveClarificationProjection, readClarificationRoundMessagesWindow, readClarificationRoundProjection, readClarificationThreadResponse, } from "../../../../collaboration-state/index.js";
3
- import { executeIdempotentCommand } from "../../../command-idempotency/index.js";
3
+ import { ingestClarificationMessage, submitClarificationAnswers, } from "../../../../collaboration-ingestion/index.js";
4
4
  import { HOST_PROJECT_API_BASE_PATH } from "./constants.js";
5
5
  import { HostProjectNotFoundError } from "./errors.js";
6
- import { clarificationRoundInvalidates } from "./invalidations.js";
7
6
  import { requireValidMessageWindowRequest, requireValidRoundMessagePayload, } from "./payload-validation.js";
8
7
  import { ClarificationRoundParamsSchema, ClarificationThreadParamsSchema, } from "./schemas.js";
9
8
  import { requireControlPlane, requireHostProjectSession, requireProjectStore, } from "./session-requirements.js";
@@ -85,35 +84,20 @@ export function registerClarificationRoutes(app, options) {
85
84
  const store = requireProjectStore(options);
86
85
  const controlPlane = requireControlPlane(options);
87
86
  const payload = requireValidRoundMessagePayload(request.body.payload);
88
- const execution = executeIdempotentCommand({
89
- db: store.db,
90
- command_id: request.body.command_id,
91
- command_name: "clarification.message.create",
92
- target_ref: request.params.roundId,
93
- payload: {
94
- body: payload.body,
95
- author_ref: session.account.account_ref,
96
- },
97
- ...(options.now === undefined ? {} : { now: options.now }),
98
- execute: () => controlPlane.sendClarificationRoundMessage(request.params.roundId, payload, {
87
+ return ingestClarificationMessage({
88
+ store,
89
+ controlPlane,
90
+ commandId: request.body.command_id,
91
+ roundId: request.params.roundId,
92
+ payload,
93
+ author: {
99
94
  account_ref: session.account.account_ref,
100
95
  display_name: session.account.display_name,
101
96
  avatar: session.account.avatar,
102
- }),
97
+ },
98
+ ...(options.now === undefined ? {} : { now: options.now }),
99
+ ...(options.chatAssistant === undefined ? {} : { chatAssistant: options.chatAssistant }),
103
100
  });
104
- const response = {
105
- command_id: execution.command_id,
106
- disposition: execution.disposition,
107
- invalidates: clarificationRoundInvalidates(request.params.roundId),
108
- };
109
- if (execution.result !== undefined) {
110
- response.result = execution.result;
111
- }
112
- const message = execution.result?.message;
113
- if (!execution.replayed && message !== undefined) {
114
- options.chatAssistant?.enqueue({ message });
115
- }
116
- return response;
117
101
  });
118
102
  app.post(`${HOST_PROJECT_API_BASE_PATH}/clarifications/rounds/:roundId/submit`, {
119
103
  schema: {
@@ -127,24 +111,14 @@ export function registerClarificationRoutes(app, options) {
127
111
  requireHostProjectSession(request, options);
128
112
  const store = requireProjectStore(options);
129
113
  const controlPlane = requireControlPlane(options);
130
- const execution = executeIdempotentCommand({
131
- db: store.db,
132
- command_id: request.body.command_id,
133
- command_name: "clarification.round.submit",
134
- target_ref: request.params.roundId,
114
+ return submitClarificationAnswers({
115
+ store,
116
+ controlPlane,
117
+ commandId: request.body.command_id,
118
+ roundId: request.params.roundId,
135
119
  payload: request.body.payload,
136
120
  ...(options.now === undefined ? {} : { now: options.now }),
137
- execute: () => controlPlane.submitClarificationRound(request.params.roundId, request.body.payload, request.body.command_id),
138
121
  });
139
- const response = {
140
- command_id: execution.command_id,
141
- disposition: execution.disposition,
142
- invalidates: clarificationRoundInvalidates(request.params.roundId),
143
- };
144
- if (execution.result !== undefined) {
145
- response.result = execution.result;
146
- }
147
- return response;
148
122
  });
149
123
  }
150
124
  //# sourceMappingURL=clarification-routes.js.map
@@ -266,6 +266,7 @@ export declare const HOST_PROJECT_COLLABORATION_OPENAPI_ROUTES: ({
266
266
  }>;
267
267
  card_message_id: import("@sinclair/typebox").TString;
268
268
  opening_message_id: import("@sinclair/typebox").TString;
269
+ answer_revision: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TInteger>;
269
270
  submitted_at: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
270
271
  submitted_message_refs: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
271
272
  successor_status: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"not_materialized">, import("@sinclair/typebox").TLiteral<"materialized">]>;
@@ -421,6 +422,7 @@ export declare const HOST_PROJECT_COLLABORATION_OPENAPI_ROUTES: ({
421
422
  }>;
422
423
  card_message_id: import("@sinclair/typebox").TString;
423
424
  opening_message_id: import("@sinclair/typebox").TString;
425
+ answer_revision: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TInteger>;
424
426
  submitted_at: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
425
427
  submitted_message_refs: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
426
428
  successor_status: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"not_materialized">, import("@sinclair/typebox").TLiteral<"materialized">]>;
@@ -1117,6 +1117,7 @@ export declare const HOST_PROJECT_WORKSPACE_OPENAPI_ROUTES: ({
1117
1117
  }>;
1118
1118
  card_message_id: import("@sinclair/typebox").TString;
1119
1119
  opening_message_id: import("@sinclair/typebox").TString;
1120
+ answer_revision: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TInteger>;
1120
1121
  submitted_at: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
1121
1122
  submitted_message_refs: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
1122
1123
  successor_status: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"not_materialized">, import("@sinclair/typebox").TLiteral<"materialized">]>;
@@ -410,6 +410,7 @@ export declare const HOST_PROJECT_OPENAPI_ROUTES: ({
410
410
  }>;
411
411
  card_message_id: import("@sinclair/typebox").TString;
412
412
  opening_message_id: import("@sinclair/typebox").TString;
413
+ answer_revision: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TInteger>;
413
414
  submitted_at: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
414
415
  submitted_message_refs: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
415
416
  successor_status: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"not_materialized">, import("@sinclair/typebox").TLiteral<"materialized">]>;
@@ -565,6 +566,7 @@ export declare const HOST_PROJECT_OPENAPI_ROUTES: ({
565
566
  }>;
566
567
  card_message_id: import("@sinclair/typebox").TString;
567
568
  opening_message_id: import("@sinclair/typebox").TString;
569
+ answer_revision: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TInteger>;
568
570
  submitted_at: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
569
571
  submitted_message_refs: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
570
572
  successor_status: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"not_materialized">, import("@sinclair/typebox").TLiteral<"materialized">]>;
@@ -2400,6 +2402,7 @@ export declare const HOST_PROJECT_OPENAPI_ROUTES: ({
2400
2402
  }>;
2401
2403
  card_message_id: import("@sinclair/typebox").TString;
2402
2404
  opening_message_id: import("@sinclair/typebox").TString;
2405
+ answer_revision: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TInteger>;
2403
2406
  submitted_at: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
2404
2407
  submitted_message_refs: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
2405
2408
  successor_status: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"not_materialized">, import("@sinclair/typebox").TLiteral<"materialized">]>;
@@ -1081,6 +1081,7 @@ export declare const BootstrapResponseSchema: {
1081
1081
  }>;
1082
1082
  card_message_id: import("@sinclair/typebox").TString;
1083
1083
  opening_message_id: import("@sinclair/typebox").TString;
1084
+ answer_revision: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TInteger>;
1084
1085
  submitted_at: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
1085
1086
  submitted_message_refs: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
1086
1087
  successor_status: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"not_materialized">, import("@sinclair/typebox").TLiteral<"materialized">]>;
@@ -0,0 +1,7 @@
1
+ CREATE TABLE platform_clarification_projection_cursors (
2
+ round_id TEXT PRIMARY KEY REFERENCES clarification_rounds(id),
3
+ binding_ref TEXT NOT NULL,
4
+ version INTEGER NOT NULL CHECK (version >= 0),
5
+ closed INTEGER NOT NULL DEFAULT 0 CHECK (closed IN (0, 1))
6
+ );
7
+ CREATE INDEX idx_platform_clarification_live ON platform_clarification_projection_cursors(closed);
@@ -44,6 +44,8 @@ export type RegisterRelayHostConnectionPayload = {
44
44
  browser_secret_write: false;
45
45
  repo_viewer: boolean;
46
46
  integration_source_ingestion_v1?: boolean;
47
+ integration_development_bot_v1?: boolean;
48
+ integration_clarification_v1?: boolean;
47
49
  integration_external_references_v1?: boolean;
48
50
  integration_attachment_ingestion_v1?: boolean;
49
51
  integration_managed_reference_ingestion_v1?: boolean;
@@ -57,11 +57,17 @@ function createRegisterPayload(options) {
57
57
  browser_secret_write: false,
58
58
  repo_viewer: capabilities.repo_viewer ?? false,
59
59
  integration_source_ingestion_v1: capabilities.integration_source_ingestion_v1 ?? false,
60
+ ...(capabilities.integration_development_bot_v1 === true
61
+ ? { integration_development_bot_v1: true }
62
+ : {}),
60
63
  integration_external_references_v1: capabilities.integration_external_references_v1 ?? false,
61
64
  integration_attachment_ingestion_v1: capabilities.integration_attachment_ingestion_v1 ?? false,
62
65
  integration_managed_reference_ingestion_v1: capabilities.integration_managed_reference_ingestion_v1 ?? false,
63
66
  integration_outbound_v1: capabilities.integration_outbound_v1 ?? false,
64
67
  integration_approval_decision_v1: capabilities.integration_approval_decision_v1 ?? false,
68
+ ...(capabilities.integration_clarification_v1 === true
69
+ ? { integration_clarification_v1: true }
70
+ : {}),
65
71
  },
66
72
  },
67
73
  };
@@ -201,6 +201,7 @@ export declare const ClarificationRoundProjectionSchema: import("@sinclair/typeb
201
201
  }>;
202
202
  card_message_id: import("@sinclair/typebox").TString;
203
203
  opening_message_id: import("@sinclair/typebox").TString;
204
+ answer_revision: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TInteger>;
204
205
  submitted_at: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
205
206
  submitted_message_refs: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
206
207
  successor_status: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"not_materialized">, import("@sinclair/typebox").TLiteral<"materialized">]>;
@@ -299,6 +300,7 @@ export declare const ActiveClarificationProjectionSchema: import("@sinclair/type
299
300
  }>;
300
301
  card_message_id: import("@sinclair/typebox").TString;
301
302
  opening_message_id: import("@sinclair/typebox").TString;
303
+ answer_revision: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TInteger>;
302
304
  submitted_at: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
303
305
  submitted_message_refs: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
304
306
  successor_status: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"not_materialized">, import("@sinclair/typebox").TLiteral<"materialized">]>;
@@ -437,6 +439,7 @@ export declare const GetClarificationRoundResponseSchema: import("@sinclair/type
437
439
  }>;
438
440
  card_message_id: import("@sinclair/typebox").TString;
439
441
  opening_message_id: import("@sinclair/typebox").TString;
442
+ answer_revision: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TInteger>;
440
443
  submitted_at: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
441
444
  submitted_message_refs: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
442
445
  successor_status: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"not_materialized">, import("@sinclair/typebox").TLiteral<"materialized">]>;
@@ -471,6 +474,7 @@ export declare const SendClarificationRoundMessagePayloadSchema: import("@sincla
471
474
  }>;
472
475
  export declare const SendClarificationRoundMessageDispositionSchema: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TObject<{
473
476
  kind: import("@sinclair/typebox").TLiteral<"created">;
477
+ message_id: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
474
478
  }>, import("@sinclair/typebox").TObject<{
475
479
  kind: import("@sinclair/typebox").TLiteral<"stale_round">;
476
480
  current_round_id: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
@@ -507,6 +511,7 @@ export declare const SendClarificationRoundMessageResultSchema: import("@sinclai
507
511
  message_kind: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"user_text">, import("@sinclair/typebox").TLiteral<"agent_text">, import("@sinclair/typebox").TLiteral<"system_notice">, import("@sinclair/typebox").TLiteral<"clarification_request">, import("@sinclair/typebox").TLiteral<"clarification_card">]>;
508
512
  body: import("@sinclair/typebox").TString;
509
513
  refs: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
514
+ development_operator: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TLiteral<true>>;
510
515
  external_reference_reads: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
511
516
  external_references: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
512
517
  source_ref: import("@sinclair/typebox").TString;
@@ -705,9 +710,16 @@ export declare const SendClarificationRoundMessageResultSchema: import("@sinclai
705
710
  }>;
706
711
  export declare const SendClarificationRoundMessageBodySchema: import("@sinclair/typebox").TSchema;
707
712
  export declare const SendClarificationRoundMessageResponseSchema: import("@sinclair/typebox").TSchema;
708
- export declare const SubmitClarificationRoundPayloadSchema: import("@sinclair/typebox").TObject<{}>;
713
+ export declare const SubmitClarificationRoundPayloadSchema: import("@sinclair/typebox").TObject<{
714
+ expected_answer_revision: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TInteger>;
715
+ }>;
709
716
  export declare const SubmitClarificationRoundDispositionSchema: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TObject<{
710
717
  kind: import("@sinclair/typebox").TLiteral<"accepted">;
718
+ }>, import("@sinclair/typebox").TObject<{
719
+ kind: import("@sinclair/typebox").TLiteral<"no_answers">;
720
+ }>, import("@sinclair/typebox").TObject<{
721
+ kind: import("@sinclair/typebox").TLiteral<"answers_changed">;
722
+ current_answer_revision: import("@sinclair/typebox").TInteger;
711
723
  }>, import("@sinclair/typebox").TObject<{
712
724
  kind: import("@sinclair/typebox").TLiteral<"already_submitted">;
713
725
  successor_status: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"not_materialized">, import("@sinclair/typebox").TLiteral<"materialized">]>;
@@ -789,6 +801,7 @@ export declare const SubmitClarificationRoundResultSchema: import("@sinclair/typ
789
801
  }>;
790
802
  card_message_id: import("@sinclair/typebox").TString;
791
803
  opening_message_id: import("@sinclair/typebox").TString;
804
+ answer_revision: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TInteger>;
792
805
  submitted_at: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
793
806
  submitted_message_refs: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
794
807
  successor_status: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"not_materialized">, import("@sinclair/typebox").TLiteral<"materialized">]>;
@@ -119,6 +119,8 @@ export const ClarificationRoundProjectionSchema = Type.Object({
119
119
  request_payload: ClarificationRequestPayloadSchema,
120
120
  card_message_id: MessageIdSchema,
121
121
  opening_message_id: MessageIdSchema,
122
+ // Append-only eligible answers. Optional for compatibility with older Hosts.
123
+ answer_revision: Type.Optional(Type.Integer({ minimum: 0 })),
122
124
  submitted_at: Type.Optional(IsoDateTimeStringSchema),
123
125
  submitted_message_refs: Type.Optional(Type.Array(MessageIdSchema)),
124
126
  successor_status: Type.Union([Type.Literal("not_materialized"), Type.Literal("materialized")]),
@@ -168,7 +170,7 @@ export const SendClarificationRoundMessagePayloadSchema = Type.Object({
168
170
  body: Type.String({ minLength: 1, maxLength: 8000 }),
169
171
  }, { additionalProperties: false });
170
172
  export const SendClarificationRoundMessageDispositionSchema = Type.Union([
171
- Type.Object({ kind: Type.Literal("created") }, { additionalProperties: false }),
173
+ Type.Object({ kind: Type.Literal("created"), message_id: Type.Optional(MessageIdSchema) }, { additionalProperties: false }),
172
174
  Type.Object({
173
175
  kind: Type.Literal("stale_round"),
174
176
  current_round_id: Type.Optional(ClarificationRoundRefSchema),
@@ -181,9 +183,14 @@ export const SendClarificationRoundMessageResultSchema = Type.Object({
181
183
  }, { additionalProperties: false });
182
184
  export const SendClarificationRoundMessageBodySchema = commandEnvelopeSchema(SendClarificationRoundMessagePayloadSchema);
183
185
  export const SendClarificationRoundMessageResponseSchema = commandResponseWithResultSchema(SendClarificationRoundMessageDispositionSchema, SendClarificationRoundMessageResultSchema);
184
- export const SubmitClarificationRoundPayloadSchema = Type.Object({}, { additionalProperties: false });
186
+ export const SubmitClarificationRoundPayloadSchema = Type.Object({ expected_answer_revision: Type.Optional(Type.Integer({ minimum: 0 })) }, { additionalProperties: false });
185
187
  export const SubmitClarificationRoundDispositionSchema = Type.Union([
186
188
  Type.Object({ kind: Type.Literal("accepted") }, { additionalProperties: false }),
189
+ Type.Object({ kind: Type.Literal("no_answers") }, { additionalProperties: false }),
190
+ Type.Object({
191
+ kind: Type.Literal("answers_changed"),
192
+ current_answer_revision: Type.Integer({ minimum: 0 }),
193
+ }, { additionalProperties: false }),
187
194
  Type.Object({
188
195
  kind: Type.Literal("already_submitted"),
189
196
  successor_status: Type.Union([