@xfey/tutti 0.1.103 → 0.1.104

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 (106) hide show
  1. package/README.md +9 -4
  2. package/dist/chat-assistant/index.d.ts +6 -1
  3. package/dist/chat-assistant/index.js +22 -12
  4. package/dist/collaboration-ingestion/external-source.d.ts +15 -0
  5. package/dist/collaboration-ingestion/external-source.js +120 -0
  6. package/dist/collaboration-ingestion/index.d.ts +5 -0
  7. package/dist/collaboration-ingestion/index.js +4 -0
  8. package/dist/collaboration-ingestion/invalidations.d.ts +4 -0
  9. package/dist/collaboration-ingestion/invalidations.js +15 -0
  10. package/dist/collaboration-ingestion/main-chat.d.ts +33 -0
  11. package/dist/collaboration-ingestion/main-chat.js +67 -0
  12. package/dist/collaboration-ingestion/reference-files.d.ts +29 -0
  13. package/dist/collaboration-ingestion/reference-files.js +158 -0
  14. package/dist/collaboration-ingestion/types.d.ts +19 -0
  15. package/dist/collaboration-ingestion/types.js +2 -0
  16. package/dist/platform-outbound/coordinator.d.ts +17 -0
  17. package/dist/platform-outbound/coordinator.js +48 -0
  18. package/dist/platform-outbound/index.d.ts +5 -0
  19. package/dist/platform-outbound/index.js +5 -0
  20. package/dist/platform-outbound/repository.d.ts +30 -0
  21. package/dist/platform-outbound/repository.js +108 -0
  22. package/dist/platform-outbound/types.d.ts +38 -0
  23. package/dist/platform-outbound/types.js +2 -0
  24. package/dist/platform-outbound/worker.d.ts +36 -0
  25. package/dist/platform-outbound/worker.js +164 -0
  26. package/dist/server-shell/cli/cli.js +9 -0
  27. package/dist/server-shell/cli/errors.d.ts +1 -1
  28. package/dist/server-shell/cli/host-lifecycle.d.ts +2 -0
  29. package/dist/server-shell/cli/host-lifecycle.js +34 -1
  30. package/dist/server-shell/cli/host-relay-connection-manager.js +22 -11
  31. package/dist/server-shell/cli/host-relay-status.js +1 -0
  32. package/dist/server-shell/cli/host-runtime-endpoint.js +17 -1
  33. package/dist/server-shell/cli/host-server-runtime.d.ts +2 -0
  34. package/dist/server-shell/cli/host-server-runtime.js +88 -14
  35. package/dist/server-shell/cli/launch-command.d.ts +7 -0
  36. package/dist/server-shell/cli/launch-command.js +39 -1
  37. package/dist/server-shell/cli/launch.d.ts +2 -0
  38. package/dist/server-shell/cli/launch.js +3 -0
  39. package/dist/server-shell/cli/machine-local.d.ts +20 -0
  40. package/dist/server-shell/cli/machine-local.js +61 -1
  41. package/dist/server-shell/cli/managed-host.d.ts +16 -1
  42. package/dist/server-shell/cli/managed-host.js +191 -23
  43. package/dist/server-shell/cli/relay-registration.d.ts +4 -1
  44. package/dist/server-shell/cli/relay-registration.js +36 -7
  45. package/dist/server-shell/cli/runtime-commands.d.ts +5 -1
  46. package/dist/server-shell/cli/runtime-commands.js +9 -3
  47. package/dist/server-shell/http/host-tunnel.d.ts +5 -0
  48. package/dist/server-shell/http/host-tunnel.js +109 -0
  49. package/dist/server-shell/http/routes/local-control.d.ts +2 -0
  50. package/dist/server-shell/http/routes/local-control.js +2 -0
  51. package/dist/server-shell/http/routes/project-api/invalidations.d.ts +0 -1
  52. package/dist/server-shell/http/routes/project-api/invalidations.js +0 -3
  53. package/dist/server-shell/http/routes/project-api/messages-routes.js +32 -63
  54. package/dist/server-shell/http/routes/project-api/openapi-workspace-routes.d.ts +8 -4
  55. package/dist/server-shell/http/routes/project-api/openapi.d.ts +8 -4
  56. package/dist/server-shell/http/routes/project-api/reference-files-routes.js +25 -94
  57. package/dist/server-shell/http/routes/project-api/reference-files.d.ts +1 -11
  58. package/dist/server-shell/http/routes/project-api/reference-files.js +2 -87
  59. package/dist/server-shell/http/routes/project-api/schemas.d.ts +8 -4
  60. package/dist/server-shell/http/routes/project-api/schemas.js +8 -3
  61. package/dist/server-shell/http/routes/project-api/session-requirements.js +34 -2
  62. package/dist/server-shell/http/routes/project-api/types.d.ts +3 -1
  63. package/dist/server-shell/http/routes/project-api/workspace-projections.js +8 -2
  64. package/dist/server-shell/local-console/package-update-hosts.js +4 -1
  65. package/dist/server-shell/local-console/project-service.d.ts +11 -2
  66. package/dist/server-shell/local-console/project-service.js +36 -8
  67. package/dist/server-shell/local-console/server.js +6 -1
  68. package/dist/server-shell/local-console/session.d.ts +4 -0
  69. package/dist/server-shell/local-console/session.js +14 -0
  70. package/dist/server-shell/session/relay-session-context.d.ts +12 -1
  71. package/dist/server-shell/session/relay-session-context.js +19 -1
  72. package/migrations/0016_external_source_mappings.sql +141 -0
  73. package/migrations/0017_platform_outbound_intents.sql +31 -0
  74. package/migrations/README.md +3 -1
  75. package/node_modules/@tutti/relay-client/dist/host-control.d.ts +26 -1
  76. package/node_modules/@tutti/relay-client/dist/host-control.js +62 -0
  77. package/node_modules/@tutti/relay-client/dist/tunnel-frames.js +32 -6
  78. package/node_modules/@tutti/relay-client/dist/tunnel-types.d.ts +5 -1
  79. package/node_modules/@tutti/shared/dist/domain/index.d.ts +9 -0
  80. package/node_modules/@tutti/shared/dist/domain/index.js +14 -0
  81. package/node_modules/@tutti/shared/dist/ids/index.d.ts +6 -0
  82. package/node_modules/@tutti/shared/dist/ids/index.js +4 -0
  83. package/node_modules/@tutti/shared/dist/schemas/api/index.d.ts +4 -0
  84. package/node_modules/@tutti/shared/dist/schemas/api/index.js +2 -0
  85. package/node_modules/@tutti/shared/dist/schemas/api/local-console-account-handoffs.d.ts +13 -0
  86. package/node_modules/@tutti/shared/dist/schemas/api/local-console-account-handoffs.js +16 -0
  87. package/node_modules/@tutti/shared/dist/schemas/api/platform-sessions.d.ts +43 -0
  88. package/node_modules/@tutti/shared/dist/schemas/api/platform-sessions.js +44 -0
  89. package/node_modules/@tutti/shared/dist/schemas/domain/index.d.ts +7 -0
  90. package/node_modules/@tutti/shared/dist/schemas/domain/index.js +10 -0
  91. package/node_modules/@tutti/shared/dist/schemas/internal/index.d.ts +29 -0
  92. package/node_modules/@tutti/shared/dist/schemas/internal/index.js +24 -1
  93. package/node_modules/@tutti/shared/dist/schemas/internal/platform-integration.d.ts +1346 -0
  94. package/node_modules/@tutti/shared/dist/schemas/internal/platform-integration.js +516 -0
  95. package/node_modules/@tutti/shared/dist/utils/feishu-diagnostics/index.d.ts +10 -0
  96. package/node_modules/@tutti/shared/dist/utils/feishu-diagnostics/index.js +19 -0
  97. package/node_modules/@tutti/shared/dist/utils/index.d.ts +1 -0
  98. package/node_modules/@tutti/shared/dist/utils/index.js +1 -0
  99. package/node_modules/@tutti/shared/dist/utils/redaction/index.d.ts +1 -1
  100. package/node_modules/@tutti/shared/dist/utils/redaction/index.js +18 -0
  101. package/package.json +1 -1
  102. package/web/assets/{homepage-motion-scene-CPs4EYQq.js → homepage-motion-scene-DEVrQ9aj.js} +1 -1
  103. package/web/assets/{index-CxCTqGFO.css → index-6i9Uk7fS.css} +1 -1
  104. package/web/assets/index-Lxtst2sl.js +69 -0
  105. package/web/index.html +2 -2
  106. package/web/assets/index-D3lwP_UR.js +0 -69
package/README.md CHANGED
@@ -13,6 +13,8 @@
13
13
  - SQLite store、migrations、repository 与协作真相读写
14
14
  - `control-plane`、scheduler、`procedure-engine`、`run-engine`、`workspace-ops` 与 OpenAI provider 接入
15
15
  - host 到 Relay 的连接与工作区加入协作
16
+ - machine-local `standalone | feishu` Project Surface authority,以及Standalone browser session / Feishu trusted integration的双向Host gate
17
+ - Feishu主群聊Chat Assistant canonical response与Host-owned durable outbound handoff intent
16
18
 
17
19
  ## 结构
18
20
 
@@ -33,12 +35,14 @@
33
35
 
34
36
  - 已具备 Fastify host shell、后台 host manager CLI 套件、用户级 desktop integration / strict `tutti://projects` bridge、带 `service status / stop / restart`、卸载前 `shutdown`、并发启动锁、版本替换、strict terminal / desktop browser context、当前 runtime version bootstrap projection、idle / package update cleanup、全机readiness、exact npm global install、跨版本worker / recovery与私有日志的机器级 Local Console、Dev Runner、Host Project API、SSE、SQLite store、collaboration-state、workspace read-state、project timeline observability helper、Control Plane、Procedure / Run、workspace-ops、checks、Relay staged upload finalize、user skills 文件服务、artifacts 可视化产物查看层、current-generation Artifact feedback message / model formatter、Owner-only provider model discovery / update 和 OpenAI / Codex provider 接入。
35
37
  - 新 Host project 的空群聊会由 collaboration-state 幂等写入一条 Tutti 欢迎消息;它不经过 browser message command,不触发 `@tutti`、Chat read-state、Scratchpad 或 Timeline / execution 流程。
38
+ - Feishu external source触发的主群聊Chat Assistant回复会在canonical message transaction中建立`platform_outbound_intents`,后台通过当前Host credential交给Relay;`handed_off`只表示Integration durable outbox已接受,不表示飞书最终delivered。
36
39
  - Host server 与 Local Console 支持在显式 `webStatic.root` 或 packaged `web/` 存在时服务 Web 静态产物;`/api/*`、SSE、health、host-local control 和 `/local-console/*` API 不被 SPA fallback 吞掉。
37
40
  - Root `pack:dry-run` 会先清理 build output、重新构建,再复制 `apps/web/dist` 到 package-local `web/`,生成临时 staging `@xfey/tutti` artifact,并校验 npm pack 文件列表包含 CLI、migrations、prompts、Web index、bundled `@tutti/shared` / `@tutti/relay-client` 以及它们的外部 runtime dependencies,同时排除源码、source map、日志、SQLite、`.env`、`.tutti-dev` 与测试产物。
38
41
  - Root `smoke:packed-cli` 同样从干净 build output 生成 staging `@xfey/tutti` tarball、在临时目录安装,并验证 SQLite native binding、`tutti --help` / desktop help / `tutti service --help` / `tutti service status` / `tutti shutdown --help` / `tutti launch --help` / `tutti archive --help` / `tutti --version`、desktop assets、migrations、prompts、Web static index、bundled internal dependencies、bundled external runtime dependencies、CLI QR dependency、仓库锁定的 `@openai/codex` binary 可执行性和临时全局安装路径;该 smoke 不需要 Tutti source repo 存在。
39
42
  - 对外发布必须把同一 staging package 通过 `npm pack` 生成的已核验 `.tgz` 交给 `npm publish`,不得直接发布 staging 目录;npm 的目录发布路径可能重新收集文件并遗漏 staging 内的 bundled dependencies。发布后必须从 registry 临时执行 CLI version / help smoke。
40
43
  - `prompts/` 是所有模型-facing prompt、指令模板和 Codex 执行模板的统一落点;长 prompt 不内联在 TypeScript 中。
41
- - Host Project API 只信任 Relay tunnel metadata 注入的 `relay_session_context`;direct loopback 缺少可信 context 时不作为浏览器工作区入口。
44
+ - Host Project API只信任Relay tunnel metadata注入的Surface-matched browser context;direct loopback缺少可信context时不作为浏览器工作区入口。
45
+ - `binding.json.surface_profile`是Project Surface的durable authority,legacy binding默认Standalone;已有project不能通过普通launch或Local Console隐式切换Surface。Relay只保存不可变mirror用于边缘gate。
42
46
  - 详细模块落点见 `src/README.md`;logger redaction、command idempotency、schema、provider 与 Codex 边界以 `docs/system/` 对应 canonical 文档为准。
43
47
 
44
48
  ## 约定
@@ -51,10 +55,11 @@
51
55
  - 不实现绕过 Relay 的单人工作区路径。
52
56
  - packaged / production `tutti launch` 默认连接 `https://tutti.now`;`TUTTI_RELAY_URL` 可覆盖 Relay URL,本地开发 dev runner 默认使用 `http://127.0.0.1:4370`。
53
57
  - host 注册使用 machine-local `host_registration_secret`;该 secret 明文只可写入受限权限的 machine-local secret store,不写入目标项目 repo、Git local config 项目身份、SQLite 协作真相或日志,`binding.json` 只保存引用。
54
- - Relay registration 成功后只把 `relay_project_ref` 写回 machine-local binding;join URL 只在 host 本机终端或当前 host-local control 内存状态中展示,不写入 binding、runtime endpoint、SQLite 协作真相或日志。
55
- - 普通 `tutti launch` 默认后台运行 host,CLI 完成页展示 join URL 后直接退出;`tutti launch --foreground` 保留前台诊断模式。Host HTTP request log 与 Control Plane / Run Pipeline runtime log 写入 `TUTTI_HOME/logs/host.log`,不写入目标项目 repo 或常规 CLI stdout。
58
+ - Relay registration 成功后只把 `relay_project_ref` 写回 machine-local binding;Standalone join URL只在host本机终端或当前host-local control内存状态中展示,不写入binding、runtime endpoint、SQLite协作真相或日志。Feishu project不签发join URL。
59
+ - 普通 `tutti launch` 默认后台运行 host;Standalone完成页展示join URL,Feishu完成页只确认collaboration surface,然后CLI退出。`tutti launch --foreground` 保留前台诊断模式。后台spawn为每次启动生成唯一`startup_attempt_ref`,源码入口会先把`tsx` loader解析为不受目标project cwd影响的绝对URL;spawn、endpoint等待、Relay readiness、内部启动失败与timeout使用同一ref写入`TUTTI_HOME/logs/host.log`。Host HTTP request log 与 Control Plane / Run Pipeline runtime log也写入该文件,不写入目标项目 repo 或常规 CLI stdout。
60
+ - Standalone Host只接受普通`relay_session_context`工作区请求;Feishu Host接受`feishu_platform_session_context` browser请求和固定internal path的`integration_delivery`。三种metadata严格三选一,cross-Surface browser request在进入Fastify业务route前403。Feishu browser只开放Project / Work精确只读allowlist;Host outbound使用独立host-control route,不借用browser transport。无状态`/api/v1/openapi.json`诊断仍可生成contract。
56
61
  - Reference 和 Skills 上传的最终写入由 Host Project API 完成;Host 只通过 Relay host-control resolve 获取短期 R2 URL 下载 staged object,不接收浏览器 base64 文件正文,也不持久化 presigned URL。
57
- - Host Project API command browser session context 只信任 Relay tunnel metadata 中的 `relay_session_context`;route handler 不读取浏览器身份 header,也不接受 payload 内身份字段。
62
+ - Host Project API的browser session context只信任Relay tunnel metadata中的Surface-matched variant;route handler不读取浏览器身份header,也不接受payload内身份字段。Feishu session在兼容projection中使用`client_session.transport = "feishu_platform_session"`,不代表创建Standalone Relay client session。
58
63
  - Artifact feedback 仍写入现有 `messages` truth并复用command idempotency;Host只在当前ready generation完全匹配时接受,Anchor技术字段仅保留审计ref,模型上下文使用bounded semantic formatter。
59
64
  - Fastify request log、debug log、SSE payload、activity event 和 run result 都必须经过 redaction,不得泄露 provider secret、host registration secret、host connection token、join token、cookie、host 绝对路径或 run workspace path。
60
65
  - Repo path metadata policy 与更严格的 Viewer content path policy 已拆分为可测试 helper:Phase 6 Viewer / Reference Files API 继续拒绝 secret-like 内容读取,Run / merge changed paths 可以完整报告结构性安全的 repo-relative 文件名;Phase 7 checks executor 已以 safe npm script discovery、结构化 `spawn`、timeout、stdout / stderr redaction 与 no-shell 执行为首批命令安全边界。
@@ -1,6 +1,6 @@
1
1
  import type { ProjectId } from "@tutti/shared/ids";
2
2
  import type { MessageProjection } from "@tutti/shared/schemas/api";
3
- import type { HostProjectStore } from "../store/index.js";
3
+ import { type HostProjectStore, type SqliteDatabase } from "../store/index.js";
4
4
  import type { WorkspaceEventBus } from "../server-shell/http/workspace-events.js";
5
5
  export declare const CHAT_ASSISTANT_OUTPUT_SCHEMA: {
6
6
  readonly type: "object";
@@ -45,6 +45,11 @@ export type ReadOnlyChatAssistantOptions = {
45
45
  model: ReadOnlyChatAssistantModel;
46
46
  promptsRoot?: string;
47
47
  onScratchpadSourceChanged?: (message: MessageProjection) => void;
48
+ onMainChatAgentMessageCreated?: (input: {
49
+ tx: SqliteDatabase;
50
+ triggerMessage: MessageProjection;
51
+ responseMessage: MessageProjection;
52
+ }) => void | (() => void);
48
53
  waitForInspectionAvailability?: () => Promise<void>;
49
54
  maxQueuedMentions?: number;
50
55
  now?: () => Date;
@@ -1,6 +1,7 @@
1
1
  import { createClarificationRoundAgentMessage, createMainChatAgentMessage, readClarificationRoundMessagesWindow, readClarificationRoundProjection, readMainChatMessageWindow, readMainChatMessagesBeforeCursor, readMessageProjectionById, } from "../collaboration-state/index.js";
2
2
  import { renderPromptTemplate } from "../prompt-templates/index.js";
3
- import { messageTextForModel, messageTextForTuttiMention, } from "../artifact-feedback/index.js";
3
+ import { withHostStoreTransaction, } from "../store/index.js";
4
+ import { messageTextForModel, messageTextForTuttiMention } from "../artifact-feedback/index.js";
4
5
  const CHAT_ASSISTANT_CONTEXT_MESSAGE_LIMIT = 3;
5
6
  const CHAT_ASSISTANT_ROUND_MESSAGE_LIMIT = 50;
6
7
  const CHAT_ASSISTANT_ROUND_MESSAGE_MAX_CHARS = 2_000;
@@ -30,11 +31,7 @@ export function containsTuttiMention(value) {
30
31
  return /(^|[^\p{L}\p{N}_])@tutti(?=$|[^\p{L}\p{N}_-])/iu.test(value);
31
32
  }
32
33
  function mainChatMessageInvalidates() {
33
- return [
34
- { kind: "messages", scope: "main_chat" },
35
- { kind: "read_state" },
36
- { kind: "bootstrap" },
37
- ];
34
+ return [{ kind: "messages", scope: "main_chat" }, { kind: "read_state" }, { kind: "bootstrap" }];
38
35
  }
39
36
  function normalizeAnswer(answer) {
40
37
  const trimmed = answer.trim();
@@ -246,12 +243,25 @@ export class ReadOnlyChatAssistant {
246
243
  },
247
244
  });
248
245
  }
249
- appendMainChatAssistantMessage(body, refs) {
250
- const message = createMainChatAgentMessage(this.options.store.db, {
251
- body,
252
- ...(refs === undefined ? {} : { refs }),
253
- ...(this.options.now === undefined ? {} : { now: this.options.now }),
246
+ appendMainChatAssistantMessage(triggerMessage, body, refs) {
247
+ let afterCommit;
248
+ const message = withHostStoreTransaction(this.options.store.db, (tx) => {
249
+ const responseMessage = createMainChatAgentMessage(tx, {
250
+ body,
251
+ ...(refs === undefined ? {} : { refs }),
252
+ ...(this.options.now === undefined ? {} : { now: this.options.now }),
253
+ });
254
+ const callback = this.options.onMainChatAgentMessageCreated?.({
255
+ tx,
256
+ triggerMessage,
257
+ responseMessage,
258
+ });
259
+ if (typeof callback === "function") {
260
+ afterCommit = callback;
261
+ }
262
+ return responseMessage;
254
263
  });
264
+ afterCommit?.();
255
265
  this.options.events.publish({
256
266
  event_type: "message.created",
257
267
  payload: {
@@ -299,7 +309,7 @@ export class ReadOnlyChatAssistant {
299
309
  });
300
310
  return;
301
311
  }
302
- this.appendMainChatAssistantMessage(body, refs);
312
+ this.appendMainChatAssistantMessage(triggerMessage, body, refs);
303
313
  }
304
314
  async respond(triggerMessage) {
305
315
  try {
@@ -0,0 +1,15 @@
1
+ import type { IntegrationDeliveryDisposition, TrustedConversationSourceIngestionCommand } from "@tutti/shared/schemas/internal";
2
+ import type { HostProjectStore } from "../store/index.js";
3
+ import type { WorkspaceEventBus } from "../server-shell/http/workspace-events.js";
4
+ import type { ChatAssistantEnqueuer, ScratchpadSourceNotifier } from "./types.js";
5
+ export declare const TRUSTED_INTEGRATION_INGESTION_PATH = "/internal/integration/v1/conversation-sources";
6
+ export type IngestTrustedConversationSourceOptions = {
7
+ store: HostProjectStore;
8
+ events: WorkspaceEventBus;
9
+ command: TrustedConversationSourceIngestionCommand;
10
+ scratchpadSource?: ScratchpadSourceNotifier;
11
+ chatAssistant?: ChatAssistantEnqueuer;
12
+ now?: () => Date;
13
+ };
14
+ export declare function ingestTrustedConversationSource(options: IngestTrustedConversationSourceOptions): IntegrationDeliveryDisposition;
15
+ //# sourceMappingURL=external-source.d.ts.map
@@ -0,0 +1,120 @@
1
+ import { createHash } from "node:crypto";
2
+ import { INTEGRATION_CONVERSATION_SOURCE_TUNNEL_PATH, isTrustedConversationSourceIngestionCommand, } from "@tutti/shared/schemas/internal";
3
+ import { ingestMainChatMessage } from "./main-chat.js";
4
+ export const TRUSTED_INTEGRATION_INGESTION_PATH = INTEGRATION_CONVERSATION_SOURCE_TUNNEL_PATH;
5
+ function sha256Text(value) {
6
+ return `sha256:${createHash("sha256").update(value, "utf8").digest("hex")}`;
7
+ }
8
+ function readExternalSourceMapping(db, command) {
9
+ const installation = command.source.locator.conversation.installation;
10
+ return (db
11
+ .prepare(`
12
+ SELECT message_id
13
+ FROM external_source_mappings
14
+ WHERE platform = ?
15
+ AND installation_ref = ?
16
+ AND external_message_ref = ?
17
+ `)
18
+ .get(installation.platform, installation.installation_ref, command.source.locator.message_ref) ?? null);
19
+ }
20
+ function insertExternalSourceEvidence(tx, command, message) {
21
+ const source = command.source;
22
+ const installation = source.locator.conversation.installation;
23
+ const acceptedAt = message.created_at;
24
+ const contentSha256 = sha256Text(source.content.text);
25
+ tx.prepare(`
26
+ INSERT INTO external_source_mappings (
27
+ platform,
28
+ installation_ref,
29
+ conversation_ref,
30
+ thread_ref,
31
+ parent_message_ref,
32
+ external_message_ref,
33
+ message_id,
34
+ binding_ref,
35
+ receipt_key,
36
+ external_subject_ref,
37
+ author_account_ref,
38
+ author_display_name,
39
+ external_created_at,
40
+ accepted_at,
41
+ initial_revision_ref,
42
+ initial_observed_at,
43
+ content_sha256,
44
+ adapter_version,
45
+ latest_revision_ref,
46
+ latest_external_updated_at
47
+ )
48
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
49
+ `).run(installation.platform, installation.installation_ref, source.locator.conversation.conversation_ref, source.locator.conversation.thread_ref ?? null, source.locator.conversation.parent_message_ref ?? null, source.locator.message_ref, message.id, command.binding_ref, command.receipt.receipt_key, source.author.external_subject.subject_ref, source.author.account_ref, source.author.display_name, source.external_created_at, acceptedAt, source.revision_ref, source.observed_at, contentSha256, source.adapter_version, source.revision_ref, source.external_created_at);
50
+ tx.prepare(`
51
+ INSERT INTO external_source_revision_observations (
52
+ platform,
53
+ installation_ref,
54
+ external_message_ref,
55
+ revision_ref,
56
+ observation_kind,
57
+ content_sha256,
58
+ external_updated_at,
59
+ observed_at,
60
+ adapter_version
61
+ )
62
+ VALUES (?, ?, ?, ?, 'created', ?, ?, ?, ?)
63
+ `).run(installation.platform, installation.installation_ref, source.locator.message_ref, source.revision_ref, contentSha256, source.external_created_at, source.observed_at, source.adapter_version);
64
+ }
65
+ export function ingestTrustedConversationSource(options) {
66
+ if (!isTrustedConversationSourceIngestionCommand(options.command)) {
67
+ return { kind: "invalid", reason_code: "trusted_command_invalid" };
68
+ }
69
+ const existing = readExternalSourceMapping(options.store.db, options.command);
70
+ if (existing !== null) {
71
+ return { kind: "duplicate", message_id: existing.message_id };
72
+ }
73
+ const source = options.command.source;
74
+ if (source.attachments.length > 0) {
75
+ return { kind: "unsupported", reason_code: "attachments_unsupported" };
76
+ }
77
+ if (source.content.text.length > 8_000) {
78
+ return { kind: "unsupported", reason_code: "message_too_long" };
79
+ }
80
+ const response = ingestMainChatMessage({
81
+ store: options.store,
82
+ events: options.events,
83
+ commandId: options.command.command_id,
84
+ payload: { body: source.content.text },
85
+ author: {
86
+ account_ref: options.command.actor.account_ref,
87
+ display_name: options.command.actor.display_name,
88
+ },
89
+ materialize: () => ({ kind: "message", body: source.content.text }),
90
+ idempotencyContext: {
91
+ kind: "external_conversation_source",
92
+ platform: source.locator.conversation.installation.platform,
93
+ installation_ref: source.locator.conversation.installation.installation_ref,
94
+ external_message_ref: source.locator.message_ref,
95
+ binding_ref: options.command.binding_ref,
96
+ receipt_key: options.command.receipt.receipt_key,
97
+ },
98
+ afterCreate: (tx, message) => insertExternalSourceEvidence(tx, options.command, message),
99
+ ...(options.scratchpadSource === undefined
100
+ ? {}
101
+ : { scratchpadSource: options.scratchpadSource }),
102
+ ...(options.chatAssistant === undefined ? {} : { chatAssistant: options.chatAssistant }),
103
+ ...(options.now === undefined ? {} : { now: options.now }),
104
+ });
105
+ if (response.disposition.kind === "interrupted") {
106
+ return {
107
+ kind: "host_unavailable",
108
+ retryable: true,
109
+ reason_code: "command_interrupted",
110
+ };
111
+ }
112
+ if (response.disposition.kind !== "created") {
113
+ return { kind: "invalid", reason_code: "main_chat_ingestion_rejected" };
114
+ }
115
+ const messageId = response.disposition.message_id ?? response.result?.message.id;
116
+ return messageId === undefined
117
+ ? { kind: "invalid", reason_code: "message_identity_missing" }
118
+ : { kind: "created", message_id: messageId };
119
+ }
120
+ //# sourceMappingURL=external-source.js.map
@@ -0,0 +1,5 @@
1
+ export { ingestTrustedConversationSource, TRUSTED_INTEGRATION_INGESTION_PATH, type IngestTrustedConversationSourceOptions, } from "./external-source.js";
2
+ export { ingestMainChatMessage, type IngestMainChatMessageOptions, type MainChatMessageMaterialization, } from "./main-chat.js";
3
+ export { ingestReferenceFile, REFERENCE_FILE_INGESTION_COMMAND_NAME, REFERENCE_FILE_INGESTION_TARGET_REF, toReferenceFileProjection, type IngestReferenceFileOptions, } from "./reference-files.js";
4
+ export type { ChatAssistantEnqueuer, ReferenceSummaryStarter, ScratchpadSourceNotifier, TrustedCollaborationAuthor, } from "./types.js";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,4 @@
1
+ export { ingestTrustedConversationSource, TRUSTED_INTEGRATION_INGESTION_PATH, } from "./external-source.js";
2
+ export { ingestMainChatMessage, } from "./main-chat.js";
3
+ export { ingestReferenceFile, REFERENCE_FILE_INGESTION_COMMAND_NAME, REFERENCE_FILE_INGESTION_TARGET_REF, toReferenceFileProjection, } from "./reference-files.js";
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,4 @@
1
+ import type { QueryInvalidationHint } from "@tutti/shared/schemas/api";
2
+ export declare function mainChatIngestionInvalidates(): QueryInvalidationHint[];
3
+ export declare function referenceIngestionInvalidates(): QueryInvalidationHint[];
4
+ //# sourceMappingURL=invalidations.d.ts.map
@@ -0,0 +1,15 @@
1
+ import { REFERENCE_DIRECTORY_PATH } from "../workspace-ops/index.js";
2
+ export function mainChatIngestionInvalidates() {
3
+ return [{ kind: "messages", scope: "main_chat" }, { kind: "read_state" }, { kind: "bootstrap" }];
4
+ }
5
+ export function referenceIngestionInvalidates() {
6
+ return [
7
+ { kind: "viewer", path: REFERENCE_DIRECTORY_PATH },
8
+ { kind: "messages", scope: "main_chat" },
9
+ { kind: "execution_status" },
10
+ { kind: "workspace" },
11
+ { kind: "read_state" },
12
+ { kind: "bootstrap" },
13
+ ];
14
+ }
15
+ //# sourceMappingURL=invalidations.js.map
@@ -0,0 +1,33 @@
1
+ import type { IdempotencyKey } from "@tutti/shared/ids";
2
+ import type { MessageProjection, SendMainChatMessagePayload, SendMainChatMessageResponse } from "@tutti/shared/schemas/api";
3
+ import type { HostProjectStore } from "../store/index.js";
4
+ import type { SqliteDatabase } from "../store/index.js";
5
+ import type { WorkspaceEventBus } from "../server-shell/http/workspace-events.js";
6
+ import type { ChatAssistantEnqueuer, ScratchpadSourceNotifier, TrustedCollaborationAuthor } from "./types.js";
7
+ export type MainChatMessageMaterialization = {
8
+ kind: "message";
9
+ body: string;
10
+ refs?: MessageProjection["refs"];
11
+ } | {
12
+ kind: "rejected";
13
+ disposition: {
14
+ kind: "artifact_changed";
15
+ } | {
16
+ kind: "artifact_unavailable";
17
+ };
18
+ };
19
+ export type IngestMainChatMessageOptions = {
20
+ store: HostProjectStore;
21
+ events: WorkspaceEventBus;
22
+ commandId: IdempotencyKey;
23
+ payload: SendMainChatMessagePayload;
24
+ author: TrustedCollaborationAuthor;
25
+ materialize: () => MainChatMessageMaterialization;
26
+ scratchpadSource?: ScratchpadSourceNotifier;
27
+ chatAssistant?: ChatAssistantEnqueuer;
28
+ idempotencyContext?: unknown;
29
+ afterCreate?: (tx: SqliteDatabase, message: MessageProjection) => void;
30
+ now?: () => Date;
31
+ };
32
+ export declare function ingestMainChatMessage(options: IngestMainChatMessageOptions): SendMainChatMessageResponse;
33
+ //# sourceMappingURL=main-chat.d.ts.map
@@ -0,0 +1,67 @@
1
+ import { createMainChatMessage, readMessageProjectionById } from "../collaboration-state/index.js";
2
+ import { executeIdempotentCommand } from "../server-shell/command-idempotency/index.js";
3
+ import { mainChatIngestionInvalidates } from "./invalidations.js";
4
+ export function ingestMainChatMessage(options) {
5
+ const execution = executeIdempotentCommand({
6
+ db: options.store.db,
7
+ command_id: options.commandId,
8
+ command_name: "messages.create",
9
+ target_ref: "main_chat",
10
+ payload: {
11
+ ...options.payload,
12
+ author_ref: options.author.account_ref,
13
+ ...(options.idempotencyContext === undefined
14
+ ? {}
15
+ : { ingestion_context: options.idempotencyContext }),
16
+ },
17
+ ...(options.now === undefined ? {} : { now: options.now }),
18
+ execute: (tx) => {
19
+ const materialized = options.materialize();
20
+ if (materialized.kind === "rejected") {
21
+ return { disposition: materialized.disposition };
22
+ }
23
+ const message = createMainChatMessage(tx, {
24
+ body: materialized.body,
25
+ author: {
26
+ kind: "human",
27
+ ref: options.author.account_ref,
28
+ display_name: options.author.display_name,
29
+ ...(options.author.avatar === undefined ? {} : { avatar: options.author.avatar }),
30
+ },
31
+ ...(materialized.refs === undefined ? {} : { refs: materialized.refs }),
32
+ ...(options.now === undefined ? {} : { now: options.now }),
33
+ });
34
+ options.afterCreate?.(tx, message);
35
+ return {
36
+ disposition: { kind: "created", message_id: message.id },
37
+ result: { message },
38
+ };
39
+ },
40
+ });
41
+ const replayedMessage = execution.result === undefined &&
42
+ execution.disposition.kind === "created" &&
43
+ execution.disposition.message_id !== undefined
44
+ ? readMessageProjectionById(options.store.db, execution.disposition.message_id)
45
+ : null;
46
+ const result = execution.result ?? (replayedMessage === null ? undefined : { message: replayedMessage });
47
+ const invalidates = mainChatIngestionInvalidates();
48
+ const response = {
49
+ command_id: execution.command_id,
50
+ disposition: execution.disposition,
51
+ ...(execution.disposition.kind === "created" ? { invalidates } : {}),
52
+ };
53
+ if (result !== undefined) {
54
+ response.result = result;
55
+ }
56
+ if (!execution.replayed && result !== undefined) {
57
+ options.events.publish({
58
+ event_type: "message.created",
59
+ payload: { message: result.message },
60
+ invalidates,
61
+ });
62
+ options.scratchpadSource?.notify(result.message);
63
+ options.chatAssistant?.enqueue({ message: result.message });
64
+ }
65
+ return response;
66
+ }
67
+ //# sourceMappingURL=main-chat.js.map
@@ -0,0 +1,29 @@
1
+ import type { IdempotencyKey } from "@tutti/shared/ids";
2
+ import type { ReferenceFileProjection, UploadReferenceFilePayload, UploadReferenceFileResponse } from "@tutti/shared/schemas/api";
3
+ import type { HostProjectStore } from "../store/index.js";
4
+ import { readReferenceSummaryIndex, type ViewerTreeEntry } from "../workspace-ops/index.js";
5
+ import type { WorkspaceEventBus } from "../server-shell/http/workspace-events.js";
6
+ import type { ReferenceSummaryStarter, ScratchpadSourceNotifier, TrustedCollaborationAuthor } from "./types.js";
7
+ export declare const REFERENCE_FILE_INGESTION_COMMAND_NAME: "references.files.upload";
8
+ export declare const REFERENCE_FILE_INGESTION_TARGET_REF: "docs/reference";
9
+ export declare function toReferenceFileProjection(options: {
10
+ workspaceRoot: string;
11
+ entry: ViewerTreeEntry;
12
+ summaryIndex?: ReturnType<typeof readReferenceSummaryIndex>;
13
+ now?: () => Date;
14
+ }): ReferenceFileProjection;
15
+ export type IngestReferenceFileOptions = {
16
+ store: HostProjectStore;
17
+ workspaceRoot: string;
18
+ events: WorkspaceEventBus;
19
+ commandId: IdempotencyKey;
20
+ payload: UploadReferenceFilePayload;
21
+ content: Buffer | undefined;
22
+ author: TrustedCollaborationAuthor;
23
+ afterCommit?: () => Promise<void>;
24
+ scratchpadSource?: ScratchpadSourceNotifier;
25
+ referenceSummaries?: ReferenceSummaryStarter;
26
+ now?: () => Date;
27
+ };
28
+ export declare function ingestReferenceFile(options: IngestReferenceFileOptions): Promise<UploadReferenceFileResponse>;
29
+ //# sourceMappingURL=reference-files.d.ts.map
@@ -0,0 +1,158 @@
1
+ import { createMainChatMessage, recordReadNotificationEvent, } from "../collaboration-state/index.js";
2
+ import { REFERENCE_DIRECTORY_PATH, VIEWER_HARD_READ_LIMIT_BYTES, readReferenceSummaryIndex, readViewerFile, referenceFileCategoryForPath, referenceMediaTypeForPath, referenceSummaryProjectionForEntry, uploadReferenceFile, } from "../workspace-ops/index.js";
3
+ import { executeIdempotentCommand } from "../server-shell/command-idempotency/index.js";
4
+ import { referenceIngestionInvalidates } from "./invalidations.js";
5
+ export const REFERENCE_FILE_INGESTION_COMMAND_NAME = "references.files.upload";
6
+ export const REFERENCE_FILE_INGESTION_TARGET_REF = REFERENCE_DIRECTORY_PATH;
7
+ function unreadableReasonForReferenceContent(content) {
8
+ return content.kind === "text" ? undefined : content.reason;
9
+ }
10
+ function referenceTextReadProjection(workspaceRoot, entry, now) {
11
+ if (entry.size_bytes !== undefined && entry.size_bytes > VIEWER_HARD_READ_LIMIT_BYTES) {
12
+ return { can_read: false, blocked_reason: "too_large" };
13
+ }
14
+ try {
15
+ const file = readViewerFile({
16
+ workspaceRoot,
17
+ path: entry.path,
18
+ maxBytes: 1,
19
+ ...(now === undefined ? {} : { now }),
20
+ });
21
+ const reason = unreadableReasonForReferenceContent(file.content);
22
+ return reason === undefined ? { can_read: true } : { can_read: false, blocked_reason: reason };
23
+ }
24
+ catch {
25
+ return { can_read: false, blocked_reason: "unknown" };
26
+ }
27
+ }
28
+ export function toReferenceFileProjection(options) {
29
+ const category = referenceFileCategoryForPath(options.entry.path) ?? "file";
30
+ const summaryIndex = options.summaryIndex ?? readReferenceSummaryIndex({ workspaceRoot: options.workspaceRoot });
31
+ const projection = {
32
+ path: options.entry.path,
33
+ name: options.entry.name,
34
+ category,
35
+ size_bytes: options.entry.size_bytes ?? 0,
36
+ text_read: referenceTextReadProjection(options.workspaceRoot, options.entry, options.now),
37
+ summary: referenceSummaryProjectionForEntry({
38
+ entry: options.entry,
39
+ summaries: summaryIndex,
40
+ }),
41
+ };
42
+ if (options.entry.blob_oid !== undefined) {
43
+ projection.blob_oid = options.entry.blob_oid;
44
+ }
45
+ if (options.entry.extension !== undefined) {
46
+ projection.extension = options.entry.extension;
47
+ }
48
+ if (options.entry.language_hint !== undefined) {
49
+ projection.language_hint = options.entry.language_hint;
50
+ }
51
+ const mediaType = referenceMediaTypeForPath(options.entry.path);
52
+ if (mediaType !== undefined) {
53
+ projection.media_type = mediaType;
54
+ }
55
+ return projection;
56
+ }
57
+ function referenceUploadMessageBody(file) {
58
+ return file.category === "image" ? `Uploaded image: ${file.name}` : `Uploaded file: ${file.name}`;
59
+ }
60
+ function referenceFileMessageRef(file) {
61
+ const ref = {
62
+ path: file.path,
63
+ name: file.name,
64
+ source: "human_upload",
65
+ category: file.category,
66
+ size_bytes: file.size_bytes,
67
+ text_read: file.text_read,
68
+ };
69
+ if (file.media_type !== undefined) {
70
+ ref.media_type = file.media_type;
71
+ }
72
+ if (file.blob_oid !== undefined) {
73
+ ref.blob_oid = file.blob_oid;
74
+ }
75
+ return ref;
76
+ }
77
+ export async function ingestReferenceFile(options) {
78
+ const invalidates = referenceIngestionInvalidates();
79
+ const execution = executeIdempotentCommand({
80
+ db: options.store.db,
81
+ command_id: options.commandId,
82
+ command_name: REFERENCE_FILE_INGESTION_COMMAND_NAME,
83
+ target_ref: REFERENCE_FILE_INGESTION_TARGET_REF,
84
+ payload: options.payload,
85
+ ...(options.now === undefined ? {} : { now: options.now }),
86
+ execute: (tx) => {
87
+ if (options.content === undefined) {
88
+ throw new Error("Staged reference upload was not downloaded");
89
+ }
90
+ const uploaded = uploadReferenceFile({
91
+ workspaceRoot: options.workspaceRoot,
92
+ commandId: options.commandId,
93
+ fileName: options.payload.file_name,
94
+ content: options.content,
95
+ ...(options.payload.media_type === undefined
96
+ ? {}
97
+ : { mediaType: options.payload.media_type }),
98
+ ...(options.payload.note === undefined ? {} : { note: options.payload.note }),
99
+ ...(options.now === undefined ? {} : { now: options.now }),
100
+ });
101
+ const file = toReferenceFileProjection({
102
+ workspaceRoot: options.workspaceRoot,
103
+ entry: uploaded.file,
104
+ ...(options.now === undefined ? {} : { now: options.now }),
105
+ });
106
+ const message = createMainChatMessage(tx, {
107
+ body: referenceUploadMessageBody(file),
108
+ author: {
109
+ kind: "human",
110
+ ref: options.author.account_ref,
111
+ display_name: options.author.display_name,
112
+ ...(options.author.avatar === undefined ? {} : { avatar: options.author.avatar }),
113
+ },
114
+ refs: { reference_file: referenceFileMessageRef(file) },
115
+ ...(options.now === undefined ? {} : { now: options.now }),
116
+ });
117
+ recordReadNotificationEvent(tx, {
118
+ scope: "references",
119
+ source_kind: "reference_file",
120
+ source_ref: file.path,
121
+ actor: { kind: "human", ref: options.author.account_ref },
122
+ occurred_at: message.created_at,
123
+ });
124
+ return {
125
+ disposition: { kind: "created" },
126
+ result: { file, message, index_path: uploaded.index_path },
127
+ };
128
+ },
129
+ });
130
+ const response = {
131
+ command_id: execution.command_id,
132
+ disposition: execution.disposition,
133
+ invalidates,
134
+ };
135
+ if (execution.result !== undefined) {
136
+ response.result = execution.result;
137
+ }
138
+ if (!execution.replayed && execution.result !== undefined) {
139
+ await options.afterCommit?.();
140
+ options.events.publish({
141
+ event_type: "message.created",
142
+ payload: { message: execution.result.message },
143
+ invalidates,
144
+ });
145
+ options.events.publish({
146
+ event_type: "query.invalidate",
147
+ payload: {
148
+ reason: "viewer_changed",
149
+ message: "Reference files changed.",
150
+ },
151
+ invalidates,
152
+ });
153
+ options.scratchpadSource?.notify(execution.result.message);
154
+ options.referenceSummaries?.start([execution.result.file.path]);
155
+ }
156
+ return response;
157
+ }
158
+ //# sourceMappingURL=reference-files.js.map
@@ -0,0 +1,19 @@
1
+ import type { AccountAvatarProjection } from "@tutti/shared/domain";
2
+ import type { MessageProjection } from "@tutti/shared/schemas/api";
3
+ export type TrustedCollaborationAuthor = {
4
+ account_ref: string;
5
+ display_name: string;
6
+ avatar?: AccountAvatarProjection;
7
+ };
8
+ export type ScratchpadSourceNotifier = {
9
+ notify: (message: MessageProjection) => void;
10
+ };
11
+ export type ChatAssistantEnqueuer = {
12
+ enqueue: (input: {
13
+ message: MessageProjection;
14
+ }) => unknown;
15
+ };
16
+ export type ReferenceSummaryStarter = {
17
+ start: (paths: string[]) => unknown;
18
+ };
19
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,17 @@
1
+ import type { MessageProjection } from "@tutti/shared/schemas/api";
2
+ import type { SqliteDatabase } from "../store/index.js";
3
+ import type { HostPlatformOutboundLogger } from "./types.js";
4
+ import type { HostPlatformOutboundWorker } from "./worker.js";
5
+ export declare class HostPlatformOutboundCoordinator {
6
+ private readonly worker;
7
+ private readonly logger;
8
+ private readonly now;
9
+ constructor(worker: HostPlatformOutboundWorker, logger: HostPlatformOutboundLogger, now?: () => Date);
10
+ captureAgentResponse(tx: SqliteDatabase, input: {
11
+ triggerMessage: MessageProjection;
12
+ responseMessage: MessageProjection;
13
+ }): (() => void) | undefined;
14
+ start(): void;
15
+ stop(): Promise<void>;
16
+ }
17
+ //# sourceMappingURL=coordinator.d.ts.map