@xfey/tutti 0.1.111 → 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 (67) hide show
  1. package/dist/approvals/decision-command.d.ts +2 -2
  2. package/dist/chat-assistant/index.d.ts +1 -1
  3. package/dist/chat-assistant/index.js +19 -6
  4. package/dist/collaboration-ingestion/clarification.d.ts +25 -0
  5. package/dist/collaboration-ingestion/clarification.js +63 -0
  6. package/dist/collaboration-ingestion/external-source.d.ts +2 -0
  7. package/dist/collaboration-ingestion/external-source.js +33 -0
  8. package/dist/collaboration-ingestion/index.d.ts +2 -0
  9. package/dist/collaboration-ingestion/index.js +2 -0
  10. package/dist/collaboration-ingestion/platform-clarification.d.ts +10 -0
  11. package/dist/collaboration-ingestion/platform-clarification.js +42 -0
  12. package/dist/collaboration-state/clarification-messages.js +3 -2
  13. package/dist/collaboration-state/clarification-records.d.ts +1 -1
  14. package/dist/collaboration-state/clarification-records.js +7 -5
  15. package/dist/collaboration-state/clarification-rounds.js +5 -5
  16. package/dist/collaboration-state/clarification-snapshot.d.ts +228 -0
  17. package/dist/collaboration-state/clarification-snapshot.js +29 -0
  18. package/dist/collaboration-state/clarification-successors.js +6 -6
  19. package/dist/collaboration-state/index.d.ts +1 -0
  20. package/dist/collaboration-state/index.js +1 -0
  21. package/dist/collaboration-state/run-recording.js +1 -1
  22. package/dist/collaboration-state/task-compile-context.d.ts +1 -1
  23. package/dist/collaboration-state/types.d.ts +1 -0
  24. package/dist/control-plane/clarification-commands.d.ts +3 -2
  25. package/dist/control-plane/clarification-commands.js +13 -4
  26. package/dist/control-plane/follow-up-context.js +8 -14
  27. package/dist/control-plane/index.d.ts +3 -2
  28. package/dist/control-plane/task-compile-clarification-context.js +7 -2
  29. package/dist/control-plane/types.d.ts +1 -0
  30. package/dist/control-plane/workflows/openai.js +5 -4
  31. package/dist/platform-outbound/repository.js +83 -4
  32. package/dist/server-shell/cli/host-lifecycle.js +1 -0
  33. package/dist/server-shell/cli/host-server-runtime.d.ts +2 -1
  34. package/dist/server-shell/cli/host-server-runtime.js +4 -2
  35. package/dist/server-shell/cli/relay-registration.js +1 -0
  36. package/dist/server-shell/http/host-tunnel.d.ts +2 -1
  37. package/dist/server-shell/http/host-tunnel.js +4 -1
  38. package/dist/server-shell/http/integration-clarification-dispatch.d.ts +6 -0
  39. package/dist/server-shell/http/integration-clarification-dispatch.js +43 -0
  40. package/dist/server-shell/http/routes/project-api/clarification-routes.js +16 -42
  41. package/dist/server-shell/http/routes/project-api/openapi-collaboration-routes.d.ts +2 -0
  42. package/dist/server-shell/http/routes/project-api/openapi-workspace-routes.d.ts +1 -0
  43. package/dist/server-shell/http/routes/project-api/openapi.d.ts +3 -0
  44. package/dist/server-shell/http/routes/project-api/schemas.d.ts +1 -0
  45. package/migrations/0022_clarification_projection_cursors.sql +7 -0
  46. package/node_modules/@tutti/relay-client/dist/host-control.d.ts +1 -0
  47. package/node_modules/@tutti/relay-client/dist/host-control.js +3 -0
  48. package/node_modules/@tutti/shared/dist/schemas/api/clarifications.d.ts +13 -1
  49. package/node_modules/@tutti/shared/dist/schemas/api/clarifications.js +9 -2
  50. package/node_modules/@tutti/shared/dist/schemas/internal/index.d.ts +2 -0
  51. package/node_modules/@tutti/shared/dist/schemas/internal/index.js +2 -0
  52. package/node_modules/@tutti/shared/dist/schemas/internal/platform-clarification-content.d.ts +15 -0
  53. package/node_modules/@tutti/shared/dist/schemas/internal/platform-clarification-content.js +24 -0
  54. package/node_modules/@tutti/shared/dist/schemas/internal/platform-clarification.d.ts +354 -0
  55. package/node_modules/@tutti/shared/dist/schemas/internal/platform-clarification.js +94 -0
  56. package/node_modules/@tutti/shared/dist/schemas/internal/platform-integration.d.ts +89 -3
  57. package/node_modules/@tutti/shared/dist/schemas/internal/platform-integration.js +15 -3
  58. package/node_modules/@tutti/shared/dist/utils/clarification-participants.d.ts +4 -0
  59. package/node_modules/@tutti/shared/dist/utils/clarification-participants.js +11 -0
  60. package/node_modules/@tutti/shared/dist/utils/index.d.ts +1 -0
  61. package/node_modules/@tutti/shared/dist/utils/index.js +1 -0
  62. package/package.json +1 -1
  63. package/prompts/procedures/follow-up-check.md +4 -4
  64. package/web/assets/{homepage-motion-scene-TmkKf8wQ.js → homepage-motion-scene-BwdH6Iss.js} +1 -1
  65. package/web/assets/index-B29RuNsd.js +69 -0
  66. package/web/index.html +1 -1
  67. package/web/assets/index-B1oL1p9_.js +0 -69
@@ -0,0 +1,29 @@
1
+ import { mapMessageRow } from "./messages.js";
2
+ /** Round messages are append-only; the eligible answer count is a stable CAS revision. */
3
+ export function readClarificationAnswerRevision(db, roundId) {
4
+ return db
5
+ .prepare(`
6
+ SELECT COUNT(*) AS count FROM messages
7
+ WHERE scope_kind = 'clarification_round' AND clarification_round_id = ?
8
+ AND message_kind = 'user_text'
9
+ AND (author_kind = 'human' OR
10
+ (author_kind = 'system' AND json_extract(refs_json, '$.development_operator') = 1))
11
+ `)
12
+ .get(roundId).count;
13
+ }
14
+ /** Read the immutable submitted refs, not a first page or messages appended after sealing. */
15
+ export function readSubmittedClarificationMessages(db, roundId) {
16
+ return db
17
+ .prepare(`
18
+ SELECT messages.* FROM clarification_rounds AS rounds
19
+ JOIN json_each(rounds.submitted_message_refs_json) AS submitted
20
+ JOIN messages ON messages.id = submitted.value
21
+ WHERE rounds.id = ? AND rounds.status = 'sealed'
22
+ AND messages.scope_kind = 'clarification_round'
23
+ AND messages.clarification_round_id = rounds.id
24
+ ORDER BY CAST(submitted.key AS INTEGER)
25
+ `)
26
+ .all(roundId)
27
+ .map((row) => mapMessageRow(row));
28
+ }
29
+ //# sourceMappingURL=clarification-snapshot.js.map
@@ -15,7 +15,7 @@ export function submitClarificationRound(db, input) {
15
15
  return;
16
16
  }
17
17
  if (row.status === "sealed") {
18
- round = mapClarificationRoundRow(row);
18
+ round = mapClarificationRoundRow(row, db);
19
19
  return;
20
20
  }
21
21
  const messageRows = db
@@ -64,7 +64,7 @@ export function materializeClarificationSuccessor(db, input) {
64
64
  return;
65
65
  }
66
66
  if (row.successor_status === "materialized") {
67
- round = mapClarificationRoundRow(row);
67
+ round = mapClarificationRoundRow(row, db);
68
68
  return;
69
69
  }
70
70
  db.prepare(`
@@ -120,7 +120,7 @@ export function materializeTaskBoundRunSuccessor(db, input) {
120
120
  result = {
121
121
  kind: "already_materialized",
122
122
  thread: mapClarificationThreadRow(thread),
123
- round: mapClarificationRoundRow(row),
123
+ round: mapClarificationRoundRow(row, db),
124
124
  task: readTaskProjection(db, thread.owner_task_id),
125
125
  };
126
126
  return;
@@ -182,7 +182,7 @@ export function materializeTaskBoundRunSuccessor(db, input) {
182
182
  result = {
183
183
  kind: "materialized",
184
184
  thread: mapClarificationThreadRow(updatedThread),
185
- round: mapClarificationRoundRow(updatedRound),
185
+ round: mapClarificationRoundRow(updatedRound, db),
186
186
  task: updatedTask,
187
187
  };
188
188
  })();
@@ -210,7 +210,7 @@ export function closeTaskBoundClarificationAsRetaskingRequired(db, input) {
210
210
  return {
211
211
  kind: "already_materialized",
212
212
  thread: mapClarificationThreadRow(thread),
213
- round: mapClarificationRoundRow(row),
213
+ round: mapClarificationRoundRow(row, db),
214
214
  task: readTaskProjection(tx, taskId),
215
215
  task_detail: readTaskDetailProjection(tx, taskId),
216
216
  };
@@ -308,7 +308,7 @@ export function closeTaskBoundClarificationAsRetaskingRequired(db, input) {
308
308
  return {
309
309
  kind: "closed",
310
310
  thread: mapClarificationThreadRow(updatedThread),
311
- round: mapClarificationRoundRow(updatedRound),
311
+ round: mapClarificationRoundRow(updatedRound, db),
312
312
  task,
313
313
  task_detail: taskDetail,
314
314
  message,
@@ -1,4 +1,5 @@
1
1
  export { CollaborationStateCursorError } from "./serialization.js";
2
+ export { readClarificationAnswerRevision, readSubmittedClarificationMessages, } from "./clarification-snapshot.js";
2
3
  export { createIdleExecutionStatusProjection } from "./execution-status.js";
3
4
  export { createMainChatAgentMessage, createMainChatMessage, createMainChatSystemMessage, ensureInitialProjectWelcomeMessage, hasMainChatArtifactPreviewMessage, hasMainChatTaskResultMessage, hasMainChatWorklistCompileMessage, hasMainChatWorklistFeedbackMessage, readCompletedWorklistFeedbackTaskIds, readMessageProjectionById, readMainChatMessageWindow, readMainChatMessagesBeforeCursor, readMainChatMessagesForScratchpadCycle, } from "./messages.js";
4
5
  export { createEmptyScratchpadProjection, readScratchpadReceiptSourceRefs, readScratchpadProjection, upsertScratchpadProjection, } from "./scratchpad.js";
@@ -1,4 +1,5 @@
1
1
  export { CollaborationStateCursorError } from "./serialization.js";
2
+ export { readClarificationAnswerRevision, readSubmittedClarificationMessages, } from "./clarification-snapshot.js";
2
3
  export { createIdleExecutionStatusProjection } from "./execution-status.js";
3
4
  export { createMainChatAgentMessage, createMainChatMessage, createMainChatSystemMessage, ensureInitialProjectWelcomeMessage, hasMainChatArtifactPreviewMessage, hasMainChatTaskResultMessage, hasMainChatWorklistCompileMessage, hasMainChatWorklistFeedbackMessage, readCompletedWorklistFeedbackTaskIds, readMessageProjectionById, readMainChatMessageWindow, readMainChatMessagesBeforeCursor, readMainChatMessagesForScratchpadCycle, } from "./messages.js";
4
5
  export { createEmptyScratchpadProjection, readScratchpadReceiptSourceRefs, readScratchpadProjection, upsertScratchpadProjection, } from "./scratchpad.js";
@@ -227,7 +227,7 @@ export function recordRunNeedsHumanResult(db, input) {
227
227
  task: updatedTask,
228
228
  task_detail: taskDetail,
229
229
  thread: mapClarificationThreadRow(thread),
230
- round: mapClarificationRoundRow(round),
230
+ round: mapClarificationRoundRow(round, db),
231
231
  card_message: mapMessageRow(cardMessage, tx),
232
232
  opening_message: mapMessageRow(openingMessage),
233
233
  };
@@ -15,8 +15,8 @@ export declare function completeTaskCompileContext(db: SqliteDatabase, input: Co
15
15
  updated_by_activity_ref?: string;
16
16
  submitted_by_activity_ref?: string;
17
17
  submitted_by_workflow_ref?: string;
18
- id: "current";
19
18
  state: "ready" | "empty" | "submitted";
19
+ id: "current";
20
20
  topic: string;
21
21
  background_summary: string;
22
22
  current_consensus: string[];
@@ -279,6 +279,7 @@ export type CreateClarificationRoundMessageInput = {
279
279
  round_id: ClarificationRoundRef;
280
280
  body: string;
281
281
  author: MessageAuthorInput;
282
+ refs?: MessageProjection["refs"];
282
283
  now?: () => Date;
283
284
  };
284
285
  export type CreateClarificationRoundAgentMessageInput = {
@@ -2,13 +2,14 @@ import type { ClarificationRoundRef, IdempotencyKey } from "@tutti/shared/ids";
2
2
  import type { SendClarificationRoundMessageDisposition, SendClarificationRoundMessagePayload, SendClarificationRoundMessageResult, SubmitClarificationRoundDisposition, SubmitClarificationRoundPayload, SubmitClarificationRoundResult } from "@tutti/shared/schemas/api";
3
3
  import type { WorkspaceEventBus } from "../server-shell/http/workspace-events.js";
4
4
  import { type HostProjectStore } from "../store/index.js";
5
- import type { ControlPlaneCommandResult, TrustedHumanAuthor } from "./types.js";
5
+ import type { ControlPlaneCommandResult } from "./types.js";
6
+ import type { TrustedCollaborationAuthor } from "../collaboration-ingestion/types.js";
6
7
  export declare function runSendClarificationRoundMessage(options: {
7
8
  store: HostProjectStore;
8
9
  events: WorkspaceEventBus;
9
10
  roundId: ClarificationRoundRef;
10
11
  payload: SendClarificationRoundMessagePayload;
11
- author: TrustedHumanAuthor;
12
+ author: TrustedCollaborationAuthor;
12
13
  now: () => Date;
13
14
  }): ControlPlaneCommandResult<SendClarificationRoundMessageDisposition, SendClarificationRoundMessageResult>;
14
15
  export declare function runSubmitClarificationRound(options: {
@@ -1,4 +1,4 @@
1
- import { createClarificationRoundMessage, readActiveClarificationProjection, submitClarificationRound, } from "../collaboration-state/index.js";
1
+ import { createClarificationRoundMessage, readActiveClarificationProjection, readClarificationAnswerRevision, submitClarificationRound, } from "../collaboration-state/index.js";
2
2
  import { withHostStoreTransaction } from "../store/index.js";
3
3
  import { enqueueControlPlaneIntent } from "./dispatch-intents.js";
4
4
  import { clarificationInvalidates } from "./invalidations.js";
@@ -19,11 +19,14 @@ export function runSendClarificationRoundMessage(options) {
19
19
  round_id: options.roundId,
20
20
  body: options.payload.body,
21
21
  author: {
22
- kind: "human",
22
+ kind: options.author.kind === "development_operator" ? "system" : "human",
23
23
  ref: options.author.account_ref,
24
24
  display_name: options.author.display_name,
25
25
  ...(options.author.avatar === undefined ? {} : { avatar: options.author.avatar }),
26
26
  },
27
+ ...(options.author.kind === "development_operator"
28
+ ? { refs: { development_operator: true } }
29
+ : {}),
27
30
  now: options.now,
28
31
  });
29
32
  options.events.publish({
@@ -41,12 +44,11 @@ export function runSendClarificationRoundMessage(options) {
41
44
  invalidates: clarificationInvalidates(options.roundId, active.thread.id),
42
45
  });
43
46
  return {
44
- disposition: { kind: "created" },
47
+ disposition: { kind: "created", message_id: message.id },
45
48
  result: { message },
46
49
  };
47
50
  }
48
51
  export function runSubmitClarificationRound(options) {
49
- void options.payload;
50
52
  const active = readActiveClarificationProjection(options.store.db);
51
53
  if (active.state === "none") {
52
54
  return { disposition: { kind: "chain_closed" } };
@@ -71,6 +73,13 @@ export function runSubmitClarificationRound(options) {
71
73
  result: { round: active.round },
72
74
  };
73
75
  }
76
+ const answerRevision = readClarificationAnswerRevision(options.store.db, options.roundId);
77
+ if (options.payload.expected_answer_revision !== undefined &&
78
+ options.payload.expected_answer_revision !== answerRevision) {
79
+ return { disposition: { kind: "answers_changed", current_answer_revision: answerRevision } };
80
+ }
81
+ if (answerRevision === 0)
82
+ return { disposition: { kind: "no_answers" } };
74
83
  const sealed = withHostStoreTransaction(options.store.db, (tx) => {
75
84
  const submitted = submitClarificationRound(tx, {
76
85
  round_id: options.roundId,
@@ -1,12 +1,10 @@
1
- import { readClarificationRoundMessagesWindow, readClarificationRoundProjection, readClarificationRoundsForThread, readTaskDetailProjection, readTaskRunResultsPage, } from "../collaboration-state/index.js";
1
+ import { clarificationAnswerRole } from "@tutti/shared/utils";
2
+ import { readSubmittedClarificationMessages, readClarificationRoundProjection, readClarificationRoundsForThread, readTaskDetailProjection, readTaskRunResultsPage, } from "../collaboration-state/index.js";
2
3
  import { toDomainRunLineage } from "./run-lineage.js";
3
4
  const CONTINUATION_CLARIFICATION_MESSAGE_LIMIT = 200;
4
5
  export function readFollowUpCheckInputFromStore(input) {
5
6
  const taskDetail = readTaskDetailProjection(input.db, input.task_id);
6
7
  const round = readClarificationRoundProjection(input.db, input.round_id);
7
- const messagesWindow = readClarificationRoundMessagesWindow(input.db, input.round_id, {
8
- anchor: "start",
9
- });
10
8
  const latestRunResult = readTaskRunResultsPage(input.db, input.task_id, {
11
9
  limit: 1,
12
10
  })?.items[0];
@@ -29,7 +27,7 @@ export function readFollowUpCheckInputFromStore(input) {
29
27
  clarification_round_ref: input.round_id,
30
28
  clarification_request_payload: round.request_payload,
31
29
  submitted_message_refs: (round.submitted_message_refs ?? []),
32
- round_messages: messagesWindow?.items ?? [],
30
+ round_messages: readSubmittedClarificationMessages(input.db, input.round_id),
33
31
  latest_run_result: latestRunResult,
34
32
  run_lineage: toDomainRunLineage(latestRunResult.run_lineage),
35
33
  },
@@ -51,18 +49,14 @@ export function readRunContinuationContextFromStore(input) {
51
49
  }
52
50
  let messagesTruncated = false;
53
51
  const resolvedRounds = rounds.map((round) => {
54
- const messagesWindow = readClarificationRoundMessagesWindow(input.db, round.id, { anchor: "start", limit: CONTINUATION_CLARIFICATION_MESSAGE_LIMIT });
55
- if (messagesWindow === null ||
56
- messagesWindow.window.has_older ||
57
- messagesWindow.window.has_newer) {
52
+ const submitted = readSubmittedClarificationMessages(input.db, round.id).filter((message) => clarificationAnswerRole(message) !== null);
53
+ if (submitted.length > CONTINUATION_CLARIFICATION_MESSAGE_LIMIT)
58
54
  messagesTruncated = true;
59
- }
60
- const submittedMessageRefs = new Set((round.submitted_message_refs ?? []));
61
- const answerMessages = (messagesWindow?.items ?? [])
62
- .filter((message) => message.author.kind === "human")
63
- .filter((message) => submittedMessageRefs.size === 0 || submittedMessageRefs.has(message.id))
55
+ const answerMessages = submitted
56
+ .slice(0, CONTINUATION_CLARIFICATION_MESSAGE_LIMIT)
64
57
  .map((message) => ({
65
58
  author: message.author,
59
+ role: clarificationAnswerRole(message),
66
60
  text: message.body,
67
61
  created_at: message.created_at,
68
62
  }));
@@ -1,7 +1,8 @@
1
1
  import { type ActivityRef, type ClarificationRoundRef, type IdempotencyKey, type TaskId, type WorkflowInvocationRef } from "@tutti/shared/ids";
2
+ import type { TrustedCollaborationAuthor } from "../collaboration-ingestion/types.js";
2
3
  import type { ExecutionStatusProjection, MessageProjection, ContextSyncDisposition, ContextSyncResult, RefreshScratchpadDisposition, RefreshScratchpadResult, RunSchedulerNowDisposition, RunSchedulerNowPayload, RetryTaskDisposition, RetryTaskPayload, RefreshReferenceSummariesDisposition, RefreshReferenceSummariesPayload, RefreshReferenceSummariesResult, SendClarificationRoundMessageDisposition, SendClarificationRoundMessagePayload, SendClarificationRoundMessageResult, SubmitClarificationRoundDisposition, SubmitClarificationRoundPayload, SubmitClarificationRoundResult, SubmitWorklistDisposition, SubmitWorklistPayload, SubmitWorklistResult, ProcedureLane } from "@tutti/shared/schemas/api";
3
4
  import type { ContextSyncWorkflowTrigger } from "./workflows/index.js";
4
- import type { ControlPlaneCommandResult, Phase5ControlPlaneOptions, StartupRecoveryOptions, StartupRecoveryResult, TrustedHumanAuthor } from "./types.js";
5
+ import type { ControlPlaneCommandResult, Phase5ControlPlaneOptions, StartupRecoveryOptions, StartupRecoveryResult } from "./types.js";
5
6
  import { type ProjectBriefRefreshReason, type RefreshProjectBriefProjectionResult } from "./project-brief-refresh.js";
6
7
  export type { ControlPlaneCommandResult, ControlPlaneLogger, Phase5ControlPlaneOptions, RunCorrectionContext, RunContinuationContext, RunDispatchContext, RunDispatchTrigger, RunPipelineInput, RunPipelineResolution, RunPipelineResolver, RunPipelineResult, RunPipelineRunner, StartupRecoveryOptions, StartupRecoveryResult, TrustedHumanAuthor, } from "./types.js";
7
8
  export declare class Phase5ControlPlane {
@@ -50,7 +51,7 @@ export declare class Phase5ControlPlane {
50
51
  startContextSyncAnalysis(trigger?: ContextSyncWorkflowTrigger): ControlPlaneCommandResult<ContextSyncDisposition, ContextSyncResult>;
51
52
  startReferenceSummaryRefresh(payload?: RefreshReferenceSummariesPayload): ControlPlaneCommandResult<RefreshReferenceSummariesDisposition, RefreshReferenceSummariesResult>;
52
53
  private tryStartPendingReferenceSummaryRefresh;
53
- sendClarificationRoundMessage(roundId: ClarificationRoundRef, payload: SendClarificationRoundMessagePayload, author: TrustedHumanAuthor): ControlPlaneCommandResult<SendClarificationRoundMessageDisposition, SendClarificationRoundMessageResult>;
54
+ sendClarificationRoundMessage(roundId: ClarificationRoundRef, payload: SendClarificationRoundMessagePayload, author: TrustedCollaborationAuthor): ControlPlaneCommandResult<SendClarificationRoundMessageDisposition, SendClarificationRoundMessageResult>;
54
55
  submitClarificationRound(roundId: ClarificationRoundRef, payload: SubmitClarificationRoundPayload, commandId: IdempotencyKey): ControlPlaneCommandResult<SubmitClarificationRoundDisposition, SubmitClarificationRoundResult>;
55
56
  waitForIdle(): Promise<void>;
56
57
  private trackAsyncOwner;
@@ -1,3 +1,4 @@
1
+ import { clarificationAnswerRole } from "@tutti/shared/utils";
1
2
  import { readActiveClarificationProjection, readClarificationRoundProjection, readClarificationRoundsForThread, readMessageProjectionById, readTaskCompileClarificationRoundsByWorkflowRef, } from "../collaboration-state/index.js";
2
3
  function answerMessage(store, messageId, roundId) {
3
4
  const message = readMessageProjectionById(store.db, messageId);
@@ -7,11 +8,15 @@ function answerMessage(store, messageId, roundId) {
7
8
  (message.message_kind !== "user_text" && message.message_kind !== "agent_text")) {
8
9
  return null;
9
10
  }
10
- const role = message.author.kind === "agent" ? "tutti" : "human";
11
+ const role = message.author.kind === "agent" && message.message_kind === "agent_text"
12
+ ? "tutti"
13
+ : clarificationAnswerRole(message);
14
+ if (role === null)
15
+ return null;
11
16
  const displayName = message.author.display_name?.trim();
12
17
  return {
13
18
  role,
14
- ...(role === "human" && displayName !== undefined && displayName !== ""
19
+ ...(role !== "tutti" && displayName !== undefined && displayName !== ""
15
20
  ? { author_display_name: displayName }
16
21
  : {}),
17
22
  text: message.body,
@@ -75,6 +75,7 @@ export type RunContinuationContext = {
75
75
  request_payload: ClarificationRequestPayload;
76
76
  answer_messages: Array<{
77
77
  author: MessageProjection["author"];
78
+ role?: "human" | "operator";
78
79
  text: string;
79
80
  created_at: string;
80
81
  }>;
@@ -2,6 +2,7 @@ import { copyFileSync, mkdtempSync } from "node:fs";
2
2
  import { rm } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import { extname, isAbsolute, join, relative, resolve } from "node:path";
5
+ import { clarificationAnswerRole } from "@tutti/shared/utils";
5
6
  import { renderPromptTemplate } from "../../prompt-templates/index.js";
6
7
  import { createOpenAiStructuredOutputClient, runOpenAiStructuredOutput, } from "../../providers/openai/sdk-procedure-runner.js";
7
8
  import { readOpenAiApiKeyCredential } from "../../providers/openai/credential-store.js";
@@ -162,10 +163,10 @@ function referenceFileSummaryWorkflowOutput(output, originalPath) {
162
163
  function followUpCheckPromptInput(input) {
163
164
  const submittedMessageRefs = new Set(input.submitted_message_refs);
164
165
  const answers = input.round_messages
165
- .filter((message) => message.author.kind === "human")
166
- .filter((message) => submittedMessageRefs.size === 0 || submittedMessageRefs.has(message.id))
167
- .map((message) => message.body.trim())
168
- .filter((text) => text.length > 0);
166
+ .filter((message) => clarificationAnswerRole(message) !== null)
167
+ .filter((message) => submittedMessageRefs.has(message.id))
168
+ .filter((message) => message.body.trim().length > 0)
169
+ .map((message) => ({ role: clarificationAnswerRole(message), text: message.body.trim() }));
169
170
  return {
170
171
  title: input.task_detail.task.title,
171
172
  goal: input.task_detail.detail.contract.goal,
@@ -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,
@@ -38,6 +38,7 @@ export async function registerHostWithRelay(options) {
38
38
  capabilities: {
39
39
  integration_source_ingestion_v1: true,
40
40
  integration_development_bot_v1: true,
41
+ integration_clarification_v1: true,
41
42
  integration_external_references_v1: true,
42
43
  integration_attachment_ingestion_v1: true,
43
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