@xfey/tutti 0.1.114 → 0.1.116

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 (39) hide show
  1. package/dist/approvals/decision-command.d.ts +1 -1
  2. package/dist/artifact-feedback/index.d.ts +1 -3
  3. package/dist/artifact-feedback/index.js +1 -15
  4. package/dist/chat-assistant/index.js +1 -1
  5. package/dist/collaboration-ingestion/external-source.d.ts +3 -0
  6. package/dist/collaboration-ingestion/external-source.js +33 -7
  7. package/dist/collaboration-ingestion/reply-context.d.ts +10 -0
  8. package/dist/collaboration-ingestion/reply-context.js +43 -0
  9. package/dist/collaboration-state/clarification-snapshot.d.ts +11 -0
  10. package/dist/collaboration-state/message-context.d.ts +5 -0
  11. package/dist/collaboration-state/message-context.js +33 -0
  12. package/dist/control-plane/scratchpad-source-messages.js +1 -1
  13. package/dist/control-plane/task-source-context.js +1 -1
  14. package/dist/reference-access/message-context.js +3 -0
  15. package/dist/reference-access/message-links.d.ts +4 -0
  16. package/dist/reference-access/message-links.js +13 -0
  17. package/dist/reference-access/service.d.ts +8 -1
  18. package/dist/reference-access/service.js +49 -2
  19. package/dist/server-shell/cli/external-reference-runtime.js +14 -4
  20. package/dist/server-shell/cli/host-server-runtime.js +1 -0
  21. package/dist/server-shell/http/routes/agent-context-external-reference-routes.js +76 -0
  22. package/dist/server-shell/http/routes/agent-context-messages-routes.js +2 -1
  23. package/node_modules/@tutti/relay-client/dist/external-references.js +6 -2
  24. package/node_modules/@tutti/shared/dist/schemas/api/clarifications.d.ts +11 -0
  25. package/node_modules/@tutti/shared/dist/schemas/api/external-references.d.ts +21 -0
  26. package/node_modules/@tutti/shared/dist/schemas/api/external-references.js +21 -0
  27. package/node_modules/@tutti/shared/dist/schemas/api/messages.d.ts +55 -0
  28. package/node_modules/@tutti/shared/dist/schemas/api/messages.js +23 -0
  29. package/node_modules/@tutti/shared/dist/schemas/api/viewer-reference.d.ts +11 -0
  30. package/node_modules/@tutti/shared/dist/schemas/internal/external-references.d.ts +28 -2
  31. package/node_modules/@tutti/shared/dist/schemas/internal/external-references.js +19 -2
  32. package/package.json +1 -1
  33. package/prompts/chat-assistant.md +1 -0
  34. package/prompts/skills/read-references/SKILL.md +8 -5
  35. package/web/assets/{homepage-motion-scene-DwIRFWMo.js → homepage-motion-scene-BlQLQVfd.js} +1 -1
  36. package/web/assets/{index-C9grZQCJ.css → index-Ccf8i-Lx.css} +1 -1
  37. package/web/assets/index-gR0GJkAv.js +70 -0
  38. package/web/index.html +2 -2
  39. package/web/assets/index-Igpofaw2.js +0 -69
@@ -115,7 +115,7 @@ export declare function executeApprovalDecisionCommand(input: {
115
115
  account_ref: string;
116
116
  };
117
117
  label: string;
118
- outcome: "accepted" | "declined" | "cancelled" | "expired";
118
+ outcome: "expired" | "accepted" | "declined" | "cancelled";
119
119
  decision_id: string;
120
120
  decided_at: string;
121
121
  };
@@ -1,4 +1,4 @@
1
- import { type ArtifactFeedbackCommand, type ArtifactFeedbackRef, type ArtifactSemanticSnapshot, type MessageProjection } from "@tutti/shared/schemas/api";
1
+ import { type ArtifactFeedbackCommand, type ArtifactFeedbackRef, type ArtifactSemanticSnapshot } from "@tutti/shared/schemas/api";
2
2
  export declare class ArtifactFeedbackValidationError extends Error {
3
3
  constructor(message: string);
4
4
  }
@@ -14,6 +14,4 @@ export declare function createArtifactFeedbackMessage(options: {
14
14
  export declare function formatArtifactFeedbackForModel(ref: ArtifactFeedbackRef, options?: {
15
15
  maxChars?: number;
16
16
  }): string;
17
- export declare function messageTextForModel(message: MessageProjection): string;
18
- export declare function messageTextForTuttiMention(message: MessageProjection): string;
19
17
  //# sourceMappingURL=index.d.ts.map
@@ -61,8 +61,7 @@ export function normalizeArtifactFeedbackCommand(input) {
61
61
  annotations,
62
62
  };
63
63
  if (normalized.expected_preview_generation === "" ||
64
- Buffer.byteLength(JSON.stringify(normalized), "utf8") >
65
- ARTIFACT_FEEDBACK_MAX_SERIALIZED_BYTES) {
64
+ Buffer.byteLength(JSON.stringify(normalized), "utf8") > ARTIFACT_FEEDBACK_MAX_SERIALIZED_BYTES) {
66
65
  throw new ArtifactFeedbackValidationError("Artifact feedback payload is too large");
67
66
  }
68
67
  return normalized;
@@ -180,17 +179,4 @@ export function formatArtifactFeedbackForModel(ref, options = {}) {
180
179
  });
181
180
  return [header, ...(overall === undefined ? [] : [overall]), ...annotations].join("\n");
182
181
  }
183
- export function messageTextForModel(message) {
184
- const feedback = message.refs?.artifact_feedback;
185
- return feedback === undefined ? message.body : formatArtifactFeedbackForModel(feedback);
186
- }
187
- export function messageTextForTuttiMention(message) {
188
- const feedback = message.refs?.artifact_feedback;
189
- if (feedback === undefined) {
190
- return message.body;
191
- }
192
- return [feedback.summary, ...feedback.annotations.map((annotation) => annotation.comment)]
193
- .filter((value) => value !== undefined)
194
- .join("\n");
195
- }
196
182
  //# sourceMappingURL=index.js.map
@@ -2,7 +2,7 @@ import { createChatAssistantDiagnostics } from "./diagnostics.js";
2
2
  import { createClarificationRoundAgentMessage, createMainChatAgentMessage, readClarificationRoundMessagesWindow, readClarificationRoundProjection, readMainChatMessageWindow, readMainChatMessagesBeforeCursor, readMessageProjectionById, } from "../collaboration-state/index.js";
3
3
  import { renderPromptTemplate } from "../prompt-templates/index.js";
4
4
  import { withHostStoreTransaction, } from "../store/index.js";
5
- import { messageTextForModel, messageTextForTuttiMention } from "../artifact-feedback/index.js";
5
+ import { messageTextForModel, messageTextForTuttiMention, } from "../collaboration-state/message-context.js";
6
6
  import { externalReferenceMessageContext } from "../reference-access/message-context.js";
7
7
  import { readExternalReferenceReadSources } from "../reference-access/read-evidence.js";
8
8
  const CHAT_ASSISTANT_CONTEXT_MESSAGE_LIMIT = 3;
@@ -12,6 +12,9 @@ export type IngestTrustedConversationSourceOptions = {
12
12
  scratchpadSource?: ScratchpadSourceNotifier;
13
13
  chatAssistant?: ChatAssistantEnqueuer;
14
14
  now?: () => Date;
15
+ logger?: {
16
+ info: (fields: Record<string, unknown>, message: string) => void;
17
+ };
15
18
  };
16
19
  export declare function ingestTrustedConversationSource(options: IngestTrustedConversationSourceOptions): IntegrationDeliveryDisposition;
17
20
  //# sourceMappingURL=external-source.d.ts.map
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import { INTEGRATION_CONVERSATION_SOURCE_TUNNEL_PATH, isTrustedConversationSourceIngestionCommand, } from "@tutti/shared/schemas/internal";
3
3
  import { ingestMainChatMessage } from "./main-chat.js";
4
4
  import { ingestClarificationMessage } from "./clarification.js";
5
+ import { captureReplyContext } from "./reply-context.js";
5
6
  export const TRUSTED_INTEGRATION_INGESTION_PATH = INTEGRATION_CONVERSATION_SOURCE_TUNNEL_PATH;
6
7
  function sha256Text(value) {
7
8
  return `sha256:${createHash("sha256").update(value, "utf8").digest("hex")}`;
@@ -122,13 +123,23 @@ export function ingestTrustedConversationSource(options) {
122
123
  account_ref: options.command.actor.account_ref,
123
124
  display_name: options.command.actor.display_name,
124
125
  },
125
- materialize: () => ({
126
- kind: "message",
127
- body: source.content.text,
128
- ...(source.external_references === undefined
129
- ? {}
130
- : { refs: { external_references: source.external_references } }),
131
- }),
126
+ materialize: () => {
127
+ const reply = captureReplyContext(options.store.db, options.command);
128
+ return {
129
+ kind: "message",
130
+ body: source.content.text,
131
+ ...(source.external_references === undefined && reply === undefined
132
+ ? {}
133
+ : {
134
+ refs: {
135
+ ...(source.external_references === undefined
136
+ ? {}
137
+ : { external_references: source.external_references }),
138
+ ...(reply === undefined ? {} : { reply_context: reply }),
139
+ },
140
+ }),
141
+ };
142
+ },
132
143
  idempotencyContext: {
133
144
  kind: "external_conversation_source",
134
145
  platform: source.locator.conversation.installation.platform,
@@ -159,6 +170,21 @@ export function ingestTrustedConversationSource(options) {
159
170
  return { kind: "invalid", reason_code: "main_chat_ingestion_rejected" };
160
171
  }
161
172
  const messageId = response.disposition.message_id ?? response.result?.message.id;
173
+ try {
174
+ options.logger?.info({
175
+ stage: "integration_source_accepted",
176
+ command_id: options.command.command_id,
177
+ receipt_key: options.command.receipt.receipt_key,
178
+ binding_ref: options.command.binding_ref,
179
+ message_id: messageId,
180
+ adapter_version: source.adapter_version,
181
+ reply_resolution: response.result?.message.refs?.reply_context?.status ?? "none",
182
+ reference_count: source.external_references?.length ?? 0,
183
+ }, "Integration conversation source accepted");
184
+ }
185
+ catch {
186
+ /* Logging must not turn a committed source into a failed delivery. */
187
+ }
162
188
  return messageId === undefined
163
189
  ? { kind: "invalid", reason_code: "message_identity_missing" }
164
190
  : { kind: "created", message_id: messageId };
@@ -0,0 +1,10 @@
1
+ import type { MessageProjection } from "@tutti/shared/schemas/api";
2
+ import type { TrustedConversationSourceIngestionCommand } from "@tutti/shared/schemas/internal";
3
+ import type { SqliteDatabase } from "../store/index.js";
4
+ type ReplyContext = NonNullable<NonNullable<MessageProjection["refs"]>["reply_context"]>;
5
+ /** Capture only an already observed direct parent in this exact binding.
6
+ * No root/thread guessing, remote history request, recursive quote or late backfill.
7
+ */
8
+ export declare function captureReplyContext(db: SqliteDatabase, command: TrustedConversationSourceIngestionCommand): ReplyContext | undefined;
9
+ export {};
10
+ //# sourceMappingURL=reply-context.d.ts.map
@@ -0,0 +1,43 @@
1
+ import { redactText } from "@tutti/shared/utils";
2
+ import { readMessageProjectionById } from "../collaboration-state/index.js";
3
+ import { externalReferenceMessageContext } from "../reference-access/message-context.js";
4
+ /** Capture only an already observed direct parent in this exact binding.
5
+ * No root/thread guessing, remote history request, recursive quote or late backfill.
6
+ */
7
+ export function captureReplyContext(db, command) {
8
+ const conversation = command.source.locator.conversation;
9
+ if (conversation.parent_message_ref === undefined)
10
+ return undefined;
11
+ const row = db
12
+ .prepare(`
13
+ SELECT message_id FROM external_source_mappings
14
+ WHERE platform = ? AND installation_ref = ? AND conversation_ref = ?
15
+ AND binding_ref = ? AND external_message_ref = ?
16
+ `)
17
+ .get(conversation.installation.platform, conversation.installation.installation_ref, conversation.conversation_ref, command.binding_ref, conversation.parent_message_ref);
18
+ const parent = row === undefined ? null : readMessageProjectionById(db, row.message_id);
19
+ if (parent === null || parent.scope.kind !== "main_chat")
20
+ return { status: "unavailable", reason_code: "parent_not_observed_in_binding" };
21
+ const text = redactText(parent.body);
22
+ return {
23
+ status: "resolved",
24
+ message_id: parent.id,
25
+ role: parent.refs?.development_operator === true
26
+ ? "operator"
27
+ : parent.author.kind === "agent"
28
+ ? "tutti"
29
+ : parent.author.kind,
30
+ text: text.slice(0, 2_000),
31
+ truncated: text.length > 2_000,
32
+ // Do not recursively inherit an ancestor's hints from a reply to another reply.
33
+ external_reference_sources: externalReferenceMessageContext({
34
+ ...(parent.refs?.external_references === undefined
35
+ ? {}
36
+ : { external_references: parent.refs.external_references }),
37
+ ...(parent.refs?.external_reference_reads === undefined
38
+ ? {}
39
+ : { external_reference_reads: parent.refs.external_reference_reads }),
40
+ }).external_reference_sources ?? [],
41
+ };
42
+ }
43
+ //# sourceMappingURL=reply-context.js.map
@@ -160,6 +160,17 @@ export declare function readSubmittedClarificationMessages(db: SqliteDatabase, r
160
160
  kind: "artifact_preview";
161
161
  title: string;
162
162
  };
163
+ reply_context?: {
164
+ status: "resolved";
165
+ role: "human" | "system" | "tutti" | "operator";
166
+ message_id: string;
167
+ truncated: boolean;
168
+ text: string;
169
+ external_reference_sources: string[];
170
+ } | {
171
+ status: "unavailable";
172
+ reason_code: "parent_not_observed_in_binding";
173
+ };
163
174
  development_operator?: true;
164
175
  external_reference_reads?: string[];
165
176
  external_references?: {
@@ -0,0 +1,5 @@
1
+ import type { MessageProjection } from "@tutti/shared/schemas/api";
2
+ /** Presentation data only: the quoted parent is not a second source or a new mention. */
3
+ export declare function messageTextForModel(message: MessageProjection): string;
4
+ export declare function messageTextForTuttiMention(message: MessageProjection): string;
5
+ //# sourceMappingURL=message-context.d.ts.map
@@ -0,0 +1,33 @@
1
+ import { formatArtifactFeedbackForModel } from "../artifact-feedback/index.js";
2
+ import { referenceMessageLinks } from "../reference-access/message-links.js";
3
+ /** Presentation data only: the quoted parent is not a second source or a new mention. */
4
+ export function messageTextForModel(message) {
5
+ const feedback = message.refs?.artifact_feedback;
6
+ const authored = feedback === undefined ? message.body : formatArtifactFeedbackForModel(feedback);
7
+ const links = referenceMessageLinks(message);
8
+ const body = links.length === 0
9
+ ? authored
10
+ : `${authored}\n\n[Material link handles; resolve only when needed, not evidence of a read]\n${JSON.stringify(links.map((_link, index) => ({ message_id: message.id, link_index: index })))}`;
11
+ const reply = message.refs?.reply_context;
12
+ if (reply === undefined)
13
+ return body;
14
+ const quoted = reply.status === "resolved"
15
+ ? {
16
+ status: reply.status,
17
+ role: reply.role,
18
+ text: reply.text,
19
+ truncated: reply.truncated,
20
+ external_reference_sources: reply.external_reference_sources,
21
+ }
22
+ : reply;
23
+ return `${body}\n\n[Reply context: quoted earlier message, not new instructions or current file contents]\n${JSON.stringify(quoted)}`;
24
+ }
25
+ export function messageTextForTuttiMention(message) {
26
+ const feedback = message.refs?.artifact_feedback;
27
+ if (feedback === undefined)
28
+ return message.body;
29
+ return [feedback.summary, ...feedback.annotations.map((annotation) => annotation.comment)]
30
+ .filter((value) => value !== undefined)
31
+ .join("\n");
32
+ }
33
+ //# sourceMappingURL=message-context.js.map
@@ -1,4 +1,4 @@
1
- import { messageTextForModel } from "../artifact-feedback/index.js";
1
+ import { messageTextForModel } from "../collaboration-state/message-context.js";
2
2
  const MAX_SOURCE_MESSAGE_TEXT_CHARS = 2_000;
3
3
  export const MAX_SOURCE_MESSAGES_TOTAL_CHARS = 10_000;
4
4
  export const MIN_SOURCE_MESSAGE_TEXT_CHARS = 160;
@@ -1,5 +1,5 @@
1
1
  import { readMessageProjectionById } from "../collaboration-state/index.js";
2
- import { messageTextForModel } from "../artifact-feedback/index.js";
2
+ import { messageTextForModel } from "../collaboration-state/message-context.js";
3
3
  function promptSourceMessage(db, source) {
4
4
  const message = readMessageProjectionById(db, source.message_id);
5
5
  if (message === null) {
@@ -3,6 +3,9 @@ export function externalReferenceMessageContext(refs) {
3
3
  const sources = [
4
4
  ...(refs?.external_references?.map((source) => source.source_ref) ?? []),
5
5
  ...(refs?.external_reference_reads ?? []),
6
+ ...(refs?.reply_context?.status === "resolved"
7
+ ? refs.reply_context.external_reference_sources
8
+ : []),
6
9
  ];
7
10
  return sources.length === 0
8
11
  ? {}
@@ -0,0 +1,4 @@
1
+ import { type MessageProjection } from "@tutti/shared/schemas/api";
2
+ /** Only authored chat links. File/document contents cannot silently grant another locator. */
3
+ export declare function referenceMessageLinks(message: MessageProjection): string[];
4
+ //# sourceMappingURL=message-links.d.ts.map
@@ -0,0 +1,13 @@
1
+ import { parseFeishuExternalReferenceInput, } from "@tutti/shared/schemas/api";
2
+ /** Only authored chat links. File/document contents cannot silently grant another locator. */
3
+ export function referenceMessageLinks(message) {
4
+ if (message.author.kind !== "human" && message.refs?.development_operator !== true)
5
+ return [];
6
+ const links = message.body.match(/https:\/\/[^\s<>"'\u3000]+/giu) ?? [];
7
+ return [
8
+ ...new Set(links
9
+ .map((link) => link.replace(/[),.;!?\]}>,。;!?)]+$/u, ""))
10
+ .filter((link) => parseFeishuExternalReferenceInput(link) !== null)),
11
+ ].slice(0, 16);
12
+ }
13
+ //# sourceMappingURL=message-links.js.map
@@ -1,4 +1,4 @@
1
- import type { ExternalReferenceDescriptor } from "@tutti/shared/schemas/api";
1
+ import type { ExternalReferenceDescriptor, ReferenceAuthorizationRequired } from "@tutti/shared/schemas/api";
2
2
  import type { ReferenceCopy, ReferenceFetchResult } from "./cache.js";
3
3
  import { type ExternalReferenceCache } from "./cache.js";
4
4
  export type ReferenceInvocation = {
@@ -6,6 +6,10 @@ export type ReferenceInvocation = {
6
6
  activity_ref?: string;
7
7
  expires_at: string;
8
8
  };
9
+ export declare class ReferenceAuthorizationRequiredError extends Error {
10
+ readonly result: ReferenceAuthorizationRequired;
11
+ constructor(result: ReferenceAuthorizationRequired);
12
+ }
9
13
  export declare class ExternalReferenceAccess {
10
14
  private readonly options;
11
15
  private cache;
@@ -14,9 +18,12 @@ export declare class ExternalReferenceAccess {
14
18
  createCache: () => ExternalReferenceCache;
15
19
  list: (invocation: ReferenceInvocation) => Promise<ExternalReferenceDescriptor[]>;
16
20
  read: (sourceRef: string, invocation: ReferenceInvocation) => Promise<ReferenceFetchResult>;
21
+ resolve?: (url: string, invocation: ReferenceInvocation) => Promise<ExternalReferenceDescriptor>;
17
22
  recordRead: (invocation: ReferenceInvocation, copy: ReferenceCopy) => void;
18
23
  });
19
24
  list(invocation: ReferenceInvocation): Promise<ExternalReferenceDescriptor[]>;
25
+ /** Explicit, authenticated admission does not replace any previously observed version. */
26
+ resolve(invocation: ReferenceInvocation, url: string): Promise<ExternalReferenceDescriptor>;
20
27
  read(invocation: ReferenceInvocation, sourceRef: string): Promise<{
21
28
  copy: ReferenceCopy;
22
29
  cache_hit: boolean;
@@ -1,4 +1,11 @@
1
1
  import { ReferenceCacheError } from "./cache.js";
2
+ export class ReferenceAuthorizationRequiredError extends Error {
3
+ result;
4
+ constructor(result) {
5
+ super("reference_authorization_required");
6
+ this.result = result;
7
+ }
8
+ }
2
9
  export class ExternalReferenceAccess {
3
10
  options;
4
11
  cache;
@@ -8,6 +15,9 @@ export class ExternalReferenceAccess {
8
15
  }
9
16
  async list(invocation) {
10
17
  const now = Date.now();
18
+ if (!Number.isFinite(Date.parse(invocation.expires_at)) ||
19
+ Date.parse(invocation.expires_at) <= now)
20
+ throw new ReferenceCacheError("reference_invocation_expired");
11
21
  for (const [key, selection] of this.selections)
12
22
  if (selection.expires <= now)
13
23
  this.selections.delete(key);
@@ -18,11 +28,48 @@ export class ExternalReferenceAccess {
18
28
  selection = {
19
29
  expires: Date.parse(invocation.expires_at),
20
30
  sources: this.options.list(invocation),
31
+ admitted: new Map(),
32
+ resolutions: new Map(),
21
33
  };
22
34
  this.selections.set(invocation.reference_invocation_ref, selection);
23
- selection.sources.catch(() => this.selections.delete(invocation.reference_invocation_ref));
35
+ const created = selection;
36
+ selection.sources.catch(() => {
37
+ if (this.selections.get(invocation.reference_invocation_ref) === created)
38
+ this.selections.delete(invocation.reference_invocation_ref);
39
+ });
24
40
  }
25
- return selection.sources;
41
+ const sources = await selection.sources;
42
+ return [
43
+ ...new Map([...sources, ...selection.admitted.values()].map((source) => [source.source_ref, source])).values(),
44
+ ];
45
+ }
46
+ /** Explicit, authenticated admission does not replace any previously observed version. */
47
+ async resolve(invocation, url) {
48
+ if (!this.options.resolve)
49
+ throw new ReferenceCacheError("reference_resolve_unavailable");
50
+ await this.list(invocation);
51
+ const selection = this.selections.get(invocation.reference_invocation_ref);
52
+ if (selection.expires <= Date.now())
53
+ throw new ReferenceCacheError("reference_invocation_expired");
54
+ const existing = selection.resolutions.get(url);
55
+ if (existing)
56
+ return existing;
57
+ if (selection.resolutions.size >= 32)
58
+ throw new ReferenceCacheError("reference_resolution_limit_reached");
59
+ const operation = this.options
60
+ .resolve(url, invocation)
61
+ .then((source) => {
62
+ if (selection.expires <= Date.now())
63
+ throw new ReferenceCacheError("reference_invocation_expired");
64
+ selection.admitted.set(source.source_ref, source);
65
+ return source;
66
+ })
67
+ .catch((error) => {
68
+ selection.resolutions.delete(url);
69
+ throw error;
70
+ });
71
+ selection.resolutions.set(url, operation);
72
+ return operation;
26
73
  }
27
74
  async read(invocation, sourceRef) {
28
75
  const sources = await this.list(invocation);
@@ -1,20 +1,20 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { join } from "node:path";
3
3
  import { requestExternalReferences } from "@tutti/relay-client";
4
- import { isExternalReferenceList, isExternalReferenceReadResult, } from "@tutti/shared/schemas/internal";
4
+ import { isExternalReferenceList, isExternalReferenceReadResult, isExternalReferenceDescriptor, isReferenceAuthorizationRequired, } from "@tutti/shared/schemas/internal";
5
5
  import { ExternalReferenceCache } from "../../reference-access/cache.js";
6
- import { ExternalReferenceAccess, } from "../../reference-access/service.js";
6
+ import { ExternalReferenceAccess, ReferenceAuthorizationRequiredError, } from "../../reference-access/service.js";
7
7
  import { recordExternalReferenceRead } from "../../reference-access/read-evidence.js";
8
8
  import { downloadStagedUploadWithResolver, cleanupDownloadedStagedUpload, } from "../http/routes/project-api/staged-uploads.js";
9
9
  import { readHostRegistrationSecret } from "./machine-local.js";
10
10
  export function createExternalReferenceRuntime(options) {
11
11
  const { preparation } = options;
12
- async function request(action, invocation, sourceRef) {
12
+ async function request(action, invocation, sourceRef, documentUrl) {
13
13
  const relay = options.getLaunchStatus().relay;
14
14
  if (!relay)
15
15
  throw new Error("reference_host_offline");
16
16
  const secret = readHostRegistrationSecret(preparation.tutti_home, preparation.project_id);
17
- return requestExternalReferences({
17
+ const result = await requestExternalReferences({
18
18
  relayUrl: preparation.relay_url,
19
19
  body: {
20
20
  supports_drive_files: true,
@@ -26,8 +26,12 @@ export function createExternalReferenceRuntime(options) {
26
26
  invocation_ref: invocation.reference_invocation_ref,
27
27
  action,
28
28
  ...(sourceRef === undefined ? {} : { source_ref: sourceRef }),
29
+ ...(documentUrl === undefined ? {} : { document_url: documentUrl }),
29
30
  },
30
31
  });
32
+ if (isReferenceAuthorizationRequired(result))
33
+ throw new ReferenceAuthorizationRequiredError(result);
34
+ return result;
31
35
  }
32
36
  return new ExternalReferenceAccess({
33
37
  createCache: () => new ExternalReferenceCache({
@@ -40,6 +44,12 @@ export function createExternalReferenceRuntime(options) {
40
44
  throw new Error("reference_list_invalid");
41
45
  return result.items;
42
46
  },
47
+ resolve: async (url, invocation) => {
48
+ const result = await request("resolve", invocation, undefined, url);
49
+ if (!isExternalReferenceDescriptor(result))
50
+ throw new Error("reference_resolve_invalid");
51
+ return result;
52
+ },
43
53
  read: async (sourceRef, invocation) => {
44
54
  const result = await request("read", invocation, sourceRef);
45
55
  if (!isExternalReferenceReadResult(result) || result.source.source_ref !== sourceRef)
@@ -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
+ logger: app.log,
717
718
  controlPlane,
718
719
  events: workspaceEvents,
719
720
  command,
@@ -1,11 +1,79 @@
1
1
  import { Type } from "@sinclair/typebox";
2
+ import { Value } from "@sinclair/typebox/value";
2
3
  import { redactText } from "@tutti/shared/utils";
3
4
  import { ExternalReferenceTransportError } from "@tutti/relay-client";
4
5
  import { ReferenceCacheError } from "../../../reference-access/cache.js";
6
+ import { ReferenceAuthorizationRequiredError } from "../../../reference-access/service.js";
7
+ import { referenceMessageLinks } from "../../../reference-access/message-links.js";
8
+ import { readMessageProjectionById } from "../../../collaboration-state/index.js";
9
+ import { MessageIdSchema } from "@tutti/shared/schemas/domain";
10
+ import { requireProjectStore } from "./agent-context-shared.js";
11
+ import { randomUUID } from "node:crypto";
5
12
  import { WorkspaceOpsError } from "../../../workspace-ops/errors.js";
6
13
  import { authorizeAgentContextRequest } from "./agent-context-auth.js";
7
14
  import { AGENT_CONTEXT_BASE_PATH } from "./agent-context-shared.js";
15
+ const ResolveMessageLinkBody = Type.Object({ message_id: MessageIdSchema, link_index: Type.Integer({ minimum: 0, maximum: 15 }) }, { additionalProperties: false });
8
16
  export function registerAgentContextExternalReferenceRoutes(app, options) {
17
+ app.post(`${AGENT_CONTEXT_BASE_PATH}/references/external/resolve`, {
18
+ bodyLimit: 4096,
19
+ schema: { body: ResolveMessageLinkBody },
20
+ preValidation: async (request, reply) => {
21
+ reply.header("cache-control", "no-store");
22
+ if (!Value.Check(ResolveMessageLinkBody, request.body))
23
+ return reply
24
+ .code(400)
25
+ .send({ kind: "unavailable", reason: "reference_message_link_invalid" });
26
+ },
27
+ }, async (request, reply) => {
28
+ reply.header("cache-control", "no-store");
29
+ const invocation = authorizeAgentContextRequest(request, options, "references:external");
30
+ const requestRef = `eref_${randomUUID().replaceAll("-", "")}`;
31
+ const fields = {
32
+ request_ref: requestRef,
33
+ invocation_ref: invocation.reference_invocation_ref,
34
+ activity_ref: invocation.activity_ref,
35
+ message_ref: request.body.message_id,
36
+ link_index: request.body.link_index,
37
+ stage: "external_reference_resolve",
38
+ };
39
+ const started = performance.now();
40
+ try {
41
+ if (!options.externalReferences)
42
+ throw new ReferenceCacheError("external_references_not_enabled");
43
+ const message = readMessageProjectionById(requireProjectStore(options).db, request.body.message_id);
44
+ const url = message ? referenceMessageLinks(message)[request.body.link_index] : undefined;
45
+ if (!url)
46
+ throw new ReferenceCacheError("reference_message_link_not_found");
47
+ const source = await options.externalReferences.resolve(invocation, url);
48
+ request.log.info({
49
+ ...fields,
50
+ source_ref: source.source_ref,
51
+ elapsed_ms: Math.round(performance.now() - started),
52
+ outcome: "resolved",
53
+ }, "Message material resolved");
54
+ return { source, request_ref: requestRef };
55
+ }
56
+ catch (error) {
57
+ const reason = error instanceof ReferenceAuthorizationRequiredError
58
+ ? "reference_authorization_required"
59
+ : error instanceof ReferenceCacheError
60
+ ? error.code
61
+ : error instanceof ExternalReferenceTransportError
62
+ ? error.reasonCode
63
+ : "reference_resolve_unavailable";
64
+ request.log.warn({
65
+ ...fields,
66
+ reason_code: reason,
67
+ ...(error instanceof ReferenceAuthorizationRequiredError
68
+ ? { authorization_ref: error.result.authorization.authorization_ref }
69
+ : {}),
70
+ elapsed_ms: Math.round(performance.now() - started),
71
+ }, "Message material unavailable");
72
+ return error instanceof ReferenceAuthorizationRequiredError
73
+ ? { ...error.result, request_ref: requestRef }
74
+ : { kind: "unavailable", reason, request_ref: requestRef };
75
+ }
76
+ });
9
77
  for (const action of ["list", "read"])
10
78
  app.get(`${AGENT_CONTEXT_BASE_PATH}/references/external${action === "read" ? "/file" : ""}`, {
11
79
  schema: {
@@ -86,6 +154,14 @@ export function registerAgentContextExternalReferenceRoutes(app, options) {
86
154
  };
87
155
  }
88
156
  catch (error) {
157
+ if (error instanceof ReferenceAuthorizationRequiredError) {
158
+ request.log.info({
159
+ ...diagnosticFields,
160
+ stage: "external_reference_authorization_required",
161
+ authorization_ref: error.result.authorization.authorization_ref,
162
+ }, "Document access card requested");
163
+ return error.result;
164
+ }
89
165
  const reason = error instanceof ReferenceCacheError
90
166
  ? error.code
91
167
  : error instanceof ExternalReferenceTransportError
@@ -1,4 +1,5 @@
1
1
  import { readMainChatMessageWindow } from "../../../collaboration-state/index.js";
2
+ import { messageTextForModel } from "../../../collaboration-state/message-context.js";
2
3
  import { externalReferenceMessageContext } from "../../../reference-access/message-context.js";
3
4
  import { authorizeAgentContextRequest } from "./agent-context-auth.js";
4
5
  import { AgentContextBadRequestError } from "./agent-context-errors.js";
@@ -35,7 +36,7 @@ export function registerAgentContextMessageRoutes(app, options) {
35
36
  return {
36
37
  scope: "main_chat",
37
38
  items: window.items.map((message) => {
38
- const body = truncateText(message.body, MESSAGE_BODY_MAX_CHARS);
39
+ const body = truncateText(messageTextForModel(message), MESSAGE_BODY_MAX_CHARS);
39
40
  return {
40
41
  message_id: message.id,
41
42
  created_at: message.created_at,
@@ -1,4 +1,4 @@
1
- import { EXTERNAL_REFERENCE_HOST_PATH, isExternalReferenceList, isExternalReferenceReadResult, } from "@tutti/shared/schemas/internal";
1
+ import { EXTERNAL_REFERENCE_HOST_PATH, isExternalReferenceList, isExternalReferenceReadResult, isReferenceAuthorizationRequired, isExternalReferenceDescriptor, } from "@tutti/shared/schemas/internal";
2
2
  export class ExternalReferenceTransportError extends Error {
3
3
  reasonCode;
4
4
  constructor(reasonCode) {
@@ -48,9 +48,13 @@ export async function requestExternalReferences(options) {
48
48
  ? reason
49
49
  : "reference_transport_unavailable");
50
50
  }
51
+ if (options.body.action !== "list" && isReferenceAuthorizationRequired(value))
52
+ return value;
51
53
  if (options.body.action === "list"
52
54
  ? !isExternalReferenceList(value)
53
- : !isExternalReferenceReadResult(value))
55
+ : options.body.action === "resolve"
56
+ ? !isExternalReferenceDescriptor(value)
57
+ : !isExternalReferenceReadResult(value))
54
58
  throw new ExternalReferenceTransportError("reference_transport_invalid_response");
55
59
  return value;
56
60
  }
@@ -511,6 +511,17 @@ export declare const SendClarificationRoundMessageResultSchema: import("@sinclai
511
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">]>;
512
512
  body: import("@sinclair/typebox").TString;
513
513
  refs: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
514
+ reply_context: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TObject<{
515
+ status: import("@sinclair/typebox").TLiteral<"resolved">;
516
+ message_id: import("@sinclair/typebox").TString;
517
+ role: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"human">, import("@sinclair/typebox").TLiteral<"tutti">, import("@sinclair/typebox").TLiteral<"operator">, import("@sinclair/typebox").TLiteral<"system">]>;
518
+ text: import("@sinclair/typebox").TString;
519
+ truncated: import("@sinclair/typebox").TBoolean;
520
+ external_reference_sources: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>;
521
+ }>, import("@sinclair/typebox").TObject<{
522
+ status: import("@sinclair/typebox").TLiteral<"unavailable">;
523
+ reason_code: import("@sinclair/typebox").TLiteral<"parent_not_observed_in_binding">;
524
+ }>]>>;
514
525
  development_operator: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TLiteral<true>>;
515
526
  external_reference_reads: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
516
527
  external_references: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{