@xfey/tutti 0.1.108 → 0.1.110

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 (57) hide show
  1. package/README.md +5 -1
  2. package/dist/chat-assistant/diagnostics.d.ts +18 -0
  3. package/dist/chat-assistant/diagnostics.js +27 -0
  4. package/dist/chat-assistant/index.d.ts +4 -0
  5. package/dist/chat-assistant/index.js +45 -21
  6. package/dist/chat-assistant/reference-freshness-smoke-cli.d.ts +2 -0
  7. package/dist/chat-assistant/reference-freshness-smoke-cli.js +40 -0
  8. package/dist/chat-assistant/reference-freshness-smoke.d.ts +23 -0
  9. package/dist/chat-assistant/reference-freshness-smoke.js +274 -0
  10. package/dist/collaboration-ingestion/index.d.ts +1 -1
  11. package/dist/collaboration-ingestion/index.js +1 -1
  12. package/dist/collaboration-ingestion/managed-reference-sources.d.ts +3 -1
  13. package/dist/collaboration-ingestion/managed-reference-sources.js +7 -0
  14. package/dist/collaboration-state/task-compile-context.d.ts +1 -1
  15. package/dist/providers/invocation-progress.d.ts +10 -0
  16. package/dist/providers/invocation-progress.js +9 -0
  17. package/dist/providers/openai/app-server/read-only-procedure.d.ts +2 -0
  18. package/dist/providers/openai/app-server/read-only-procedure.js +31 -2
  19. package/dist/providers/openai/app-server/skill-discovery.d.ts +10 -0
  20. package/dist/providers/openai/app-server/skill-discovery.js +29 -0
  21. package/dist/providers/openai/app-server/skills.d.ts +4 -16
  22. package/dist/providers/openai/app-server/skills.js +4 -29
  23. package/dist/providers/openai/app-server/workspace-write-run.js +13 -19
  24. package/dist/providers/openai/chat-assistant.js +12 -2
  25. package/dist/reference-access/message-context.d.ts +6 -0
  26. package/dist/reference-access/message-context.js +11 -0
  27. package/dist/reference-access/read-evidence.d.ts +2 -0
  28. package/dist/reference-access/read-evidence.js +8 -0
  29. package/dist/server-shell/cli/external-reference-runtime.js +1 -0
  30. package/dist/server-shell/cli/host-server-runtime.js +2 -0
  31. package/dist/server-shell/http/routes/agent-context-external-reference-routes.js +30 -4
  32. package/dist/server-shell/http/routes/agent-context-messages-routes.js +2 -0
  33. package/dist/server-shell/http/routes/agent-context-reference-routes.js +39 -3
  34. package/dist/server-shell/http/routes/agent-context-references.d.ts +11 -0
  35. package/dist/server-shell/http/routes/agent-context-references.js +14 -0
  36. package/dist/server-shell/http/static-web.js +10 -6
  37. package/node_modules/@tutti/shared/dist/schemas/api/clarifications.d.ts +2 -1
  38. package/node_modules/@tutti/shared/dist/schemas/api/external-references.d.ts +27 -4
  39. package/node_modules/@tutti/shared/dist/schemas/api/external-references.js +32 -3
  40. package/node_modules/@tutti/shared/dist/schemas/api/index.d.ts +1 -1
  41. package/node_modules/@tutti/shared/dist/schemas/api/index.js +1 -1
  42. package/node_modules/@tutti/shared/dist/schemas/api/messages.d.ts +8 -4
  43. package/node_modules/@tutti/shared/dist/schemas/api/messages.js +2 -1
  44. package/node_modules/@tutti/shared/dist/schemas/api/platform-sessions.d.ts +5 -3
  45. package/node_modules/@tutti/shared/dist/schemas/api/platform-sessions.js +16 -1
  46. package/node_modules/@tutti/shared/dist/schemas/api/viewer-reference.d.ts +2 -1
  47. package/node_modules/@tutti/shared/dist/schemas/internal/external-references.d.ts +15 -2
  48. package/node_modules/@tutti/shared/dist/schemas/internal/external-references.js +4 -2
  49. package/node_modules/@tutti/shared/dist/schemas/internal/platform-integration.d.ts +6 -6
  50. package/package.json +1 -1
  51. package/prompts/chat-assistant.md +16 -15
  52. package/prompts/skills/README.md +1 -1
  53. package/prompts/skills/read-references/README.md +2 -2
  54. package/prompts/skills/read-references/SKILL.md +24 -37
  55. package/web/assets/{homepage-motion-scene-B6yPJnY8.js → homepage-motion-scene-rxYjg-zc.js} +1 -1
  56. package/web/assets/{index-aQnJ5JoI.js → index-BrKWdtKZ.js} +3 -3
  57. package/web/index.html +1 -1
package/README.md CHANGED
@@ -4,6 +4,10 @@
4
4
 
5
5
  `apps/server` 是 Tutti 的项目 host service,也是机器级 Local Console 的进程入口。
6
6
 
7
+ `0.1.110` 发布候选加入每次 Chat invocation 的外部资料核验规则、本地/外部 provenance、从实际读取证据派生的回答来源线索,以及隔离多轮模型 smoke;配套 F.3 增量支持群资料确认与普通云盘文件。需要同步 Web/Relay/Integration 后再升级 Host;实际发布与真人验收状态见同一[按需资料计划](../../docs/roadmap/feishu-on-demand-reference-plan.md)。历史快照不删除,读取线索不继承新鲜度或权限。
8
+
9
+ 已发布的 `0.1.109` 统一验证 Codex `skills/extraRoots/set` / `skills/list` 与实际 `SKILL.md` 文件身份;Chat 的安全阶段日志可通过 invocation ref 关联外部 Reference 读取。候选安装/升级、registry byte match、本机 CLI/Console/测试 Host 更新及更新后 Docx 真人读取闭环已通过;连续更新/同次 pin/附件/失权 live 矩阵仍独立跟踪。证据见[按需资料计划](../../docs/roadmap/feishu-on-demand-reference-plan.md),不以安装版本代替实际读取证据。
10
+
7
11
  它负责:
8
12
 
9
13
  - `tutti launch` 后的项目绑定、恢复和接管入口
@@ -41,7 +45,7 @@
41
45
  - Host server 与 Local Console 支持在显式 `webStatic.root` 或 packaged `web/` 存在时服务 Web 静态产物;`/api/*`、SSE、health、host-local control 和 `/local-console/*` API 不被 SPA fallback 吞掉。
42
46
  - 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` 与测试产物。
43
47
  - 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 存在。
44
- - 对外发布必须把同一 staging package 通过 `npm pack` 生成的已核验 `.tgz` 交给 `npm publish`,不得直接发布 staging 目录;npm 的目录发布路径可能重新收集文件并遗漏 staging 内的 bundled dependencies。发布后必须从 registry 临时执行 CLI version / help smoke。
48
+ - 对外发布必须把同一 staging package 通过 `npm pack` 生成的已核验 `.tgz` 交给 `npm publish --auth-type=web`,不得直接发布 staging 目录;npm 的目录发布路径可能重新收集文件并遗漏 staging 内的 bundled dependencies。发布后核对 registry metadata 并回下载;若与已完成安装/CLI smoke candidate 逐字节一致,复用候选证据,不重复安装。未被相同候选覆盖的 registry artifact 仍须补齐安装 smoke。
45
49
  - `prompts/` 是所有模型-facing prompt、指令模板和 Codex 执行模板的统一落点;长 prompt 不内联在 TypeScript 中。
46
50
  - Host Project API只信任Relay tunnel metadata注入的Surface-matched browser context;direct loopback缺少可信context时不作为浏览器工作区入口。
47
51
  - `binding.json.surface_profile`是Project Surface的durable authority,legacy binding默认Standalone;已有project不能通过普通launch或Local Console隐式切换Surface。Relay只保存不可变mirror用于边缘gate。
@@ -0,0 +1,18 @@
1
+ import type { ProviderInvocationProgress } from "../providers/invocation-progress.js";
2
+ export type ChatAssistantLogger = {
3
+ info: (fields: Record<string, unknown>, message: string) => void;
4
+ warn: (fields: Record<string, unknown>, message: string) => void;
5
+ };
6
+ type ChatAssistantProgress = ProviderInvocationProgress | {
7
+ stage: "queued" | "inspection_wait" | "model_requested" | "reply_persisted" | "failed" | "skipped";
8
+ reason_code?: "queue_full" | "stopping" | "context_unavailable" | "answer_failed" | "reply_failed";
9
+ reference_source_count?: number;
10
+ };
11
+ export declare function createChatAssistantDiagnostics(options: {
12
+ logger?: ChatAssistantLogger;
13
+ projectId: string;
14
+ triggerMessageId: string;
15
+ scope: string;
16
+ }): (event: ChatAssistantProgress) => void;
17
+ export {};
18
+ //# sourceMappingURL=diagnostics.d.ts.map
@@ -0,0 +1,27 @@
1
+ import { createEventRef } from "@tutti/shared/ids";
2
+ export function createChatAssistantDiagnostics(options) {
3
+ const requestRef = createEventRef();
4
+ const started = performance.now();
5
+ return (event) => {
6
+ try {
7
+ const fields = {
8
+ project_id: options.projectId,
9
+ chat_request_ref: requestRef,
10
+ trigger_message_ref: options.triggerMessageId,
11
+ scope: options.scope,
12
+ elapsed_ms: Math.round(performance.now() - started),
13
+ ...event,
14
+ };
15
+ if (event.stage === "failed" || event.stage === "provider_failed") {
16
+ options.logger?.warn(fields, "Chat assistant progress");
17
+ }
18
+ else {
19
+ options.logger?.info(fields, "Chat assistant progress");
20
+ }
21
+ }
22
+ catch {
23
+ /* Logging must not replace the answer or interrupt queue draining. */
24
+ }
25
+ };
26
+ }
27
+ //# sourceMappingURL=diagnostics.js.map
@@ -1,5 +1,7 @@
1
1
  import type { ProjectId } from "@tutti/shared/ids";
2
2
  import type { MessageProjection } from "@tutti/shared/schemas/api";
3
+ import { type ChatAssistantLogger } from "./diagnostics.js";
4
+ import type { ProviderInvocationProgressObserver } from "../providers/invocation-progress.js";
3
5
  import { type HostProjectStore, type SqliteDatabase } from "../store/index.js";
4
6
  import type { WorkspaceEventBus } from "../server-shell/http/workspace-events.js";
5
7
  export declare const CHAT_ASSISTANT_OUTPUT_SCHEMA: {
@@ -19,6 +21,7 @@ export type ChatAssistantOutput = {
19
21
  };
20
22
  export type ChatAssistantPromptInput = {
21
23
  prompt: string;
24
+ onProgress?: ProviderInvocationProgressObserver;
22
25
  };
23
26
  export type ReadOnlyChatAssistantModel = {
24
27
  answer: (input: ChatAssistantPromptInput) => Promise<ChatAssistantOutput>;
@@ -35,6 +38,7 @@ export type ReadOnlyChatAssistantHandle = {
35
38
  stopAccepting: () => void;
36
39
  };
37
40
  export type ReadOnlyChatAssistantOptions = {
41
+ logger?: ChatAssistantLogger;
38
42
  project: {
39
43
  project_id: ProjectId;
40
44
  display_name: string;
@@ -1,7 +1,10 @@
1
+ import { createChatAssistantDiagnostics } from "./diagnostics.js";
1
2
  import { createClarificationRoundAgentMessage, createMainChatAgentMessage, readClarificationRoundMessagesWindow, readClarificationRoundProjection, readMainChatMessageWindow, readMainChatMessagesBeforeCursor, readMessageProjectionById, } from "../collaboration-state/index.js";
2
3
  import { renderPromptTemplate } from "../prompt-templates/index.js";
3
4
  import { withHostStoreTransaction, } from "../store/index.js";
4
5
  import { messageTextForModel, messageTextForTuttiMention } from "../artifact-feedback/index.js";
6
+ import { externalReferenceMessageContext } from "../reference-access/message-context.js";
7
+ import { readExternalReferenceReadSources } from "../reference-access/read-evidence.js";
5
8
  const CHAT_ASSISTANT_CONTEXT_MESSAGE_LIMIT = 3;
6
9
  const CHAT_ASSISTANT_ROUND_MESSAGE_LIMIT = 50;
7
10
  const CHAT_ASSISTANT_ROUND_MESSAGE_MAX_CHARS = 2_000;
@@ -50,6 +53,7 @@ function messageToContext(message) {
50
53
  ? {}
51
54
  : { author_display_name: authorDisplayName }),
52
55
  text: messageTextForModel(message),
56
+ ...externalReferenceMessageContext(message.refs),
53
57
  };
54
58
  }
55
59
  function messageRole(message) {
@@ -89,6 +93,7 @@ function roundMessagesToContext(messages) {
89
93
  ? {}
90
94
  : { author_display_name: authorDisplayName }),
91
95
  text,
96
+ ...externalReferenceMessageContext(message.refs),
92
97
  });
93
98
  }
94
99
  return result;
@@ -111,13 +116,21 @@ export class ReadOnlyChatAssistant {
111
116
  if (!this.accepting) {
112
117
  return { triggered: false };
113
118
  }
119
+ const report = createChatAssistantDiagnostics({
120
+ ...(this.options.logger === undefined ? {} : { logger: this.options.logger }),
121
+ projectId: this.options.project.project_id,
122
+ triggerMessageId: input.message.id,
123
+ scope: input.message.scope.kind,
124
+ });
114
125
  if (this.pending.length >= this.maxQueuedMentions) {
126
+ report({ stage: "failed", reason_code: "queue_full" });
115
127
  this.appendAssistantMessageForTrigger(input.message, assistantFailureMessage(), {
116
128
  scratchpad_source: "exclude",
117
129
  });
118
130
  return { triggered: true };
119
131
  }
120
- this.pending.push(input.message);
132
+ report({ stage: "queued" });
133
+ this.pending.push({ message: input.message, report });
121
134
  this.pump();
122
135
  return { triggered: true };
123
136
  }
@@ -132,7 +145,8 @@ export class ReadOnlyChatAssistant {
132
145
  return;
133
146
  }
134
147
  this.accepting = false;
135
- for (const message of this.pending.splice(0)) {
148
+ for (const { message, report } of this.pending.splice(0)) {
149
+ report({ stage: "skipped", reason_code: "stopping" });
136
150
  this.appendAssistantMessageForTrigger(message, assistantFailureMessage(), {
137
151
  scratchpad_source: "exclude",
138
152
  });
@@ -143,12 +157,14 @@ export class ReadOnlyChatAssistant {
143
157
  if (this.active !== null) {
144
158
  return;
145
159
  }
146
- const message = this.pending.shift();
147
- if (message === undefined) {
160
+ const pending = this.pending.shift();
161
+ if (pending === undefined) {
148
162
  this.resolveIdleIfNeeded();
149
163
  return;
150
164
  }
151
- const task = this.respond(message).catch(() => undefined);
165
+ const task = this.respond(pending.message, pending.report).catch(() => {
166
+ pending.report({ stage: "failed", reason_code: "reply_failed" });
167
+ });
152
168
  this.active = task;
153
169
  void task.finally(() => {
154
170
  if (this.active === task) {
@@ -167,18 +183,12 @@ export class ReadOnlyChatAssistant {
167
183
  this.idleWaiters.clear();
168
184
  }
169
185
  buildMainChatContext(triggerMessage) {
170
- const authorDisplayName = triggerMessage.author.display_name?.trim();
171
186
  return {
172
187
  mode: "main_chat",
173
188
  project: {
174
189
  display_name: this.options.project.display_name,
175
190
  },
176
- trigger: {
177
- ...(authorDisplayName === undefined || authorDisplayName === ""
178
- ? {}
179
- : { author_display_name: authorDisplayName }),
180
- text: messageTextForModel(triggerMessage),
181
- },
191
+ trigger: messageToContext(triggerMessage),
182
192
  recent_messages: readMainChatMessageWindow(this.options.store.db, {
183
193
  limit: CHAT_ASSISTANT_CONTEXT_MESSAGE_LIMIT,
184
194
  }).items.map(messageToContext),
@@ -201,18 +211,12 @@ export class ReadOnlyChatAssistant {
201
211
  before_id: cardMessage.id,
202
212
  limit: CHAT_ASSISTANT_ROUND_MAIN_CHAT_CONTEXT_LIMIT,
203
213
  }).map(messageToContext);
204
- const authorDisplayName = triggerMessage.author.display_name?.trim();
205
214
  return {
206
215
  mode: "clarification_round",
207
216
  project: {
208
217
  display_name: this.options.project.display_name,
209
218
  },
210
- trigger: {
211
- ...(authorDisplayName === undefined || authorDisplayName === ""
212
- ? {}
213
- : { author_display_name: authorDisplayName }),
214
- text: messageTextForModel(triggerMessage),
215
- },
219
+ trigger: messageToContext(triggerMessage),
216
220
  round_request: {
217
221
  ...(round.request_payload.title === undefined
218
222
  ? {}
@@ -311,23 +315,43 @@ export class ReadOnlyChatAssistant {
311
315
  }
312
316
  this.appendMainChatAssistantMessage(triggerMessage, body, refs);
313
317
  }
314
- async respond(triggerMessage) {
318
+ async respond(triggerMessage, report) {
319
+ let answer;
320
+ let invocationRef;
321
+ let readSources = [];
315
322
  try {
323
+ report({ stage: "inspection_wait" });
316
324
  await this.options.waitForInspectionAvailability?.();
317
325
  const prompt = this.renderPrompt(triggerMessage);
318
326
  if (prompt === null) {
327
+ report({ stage: "skipped", reason_code: "context_unavailable" });
319
328
  return;
320
329
  }
330
+ report({ stage: "model_requested" });
321
331
  const output = await this.options.model.answer({
322
332
  prompt,
333
+ onProgress: (event) => {
334
+ if (event.stage === "context_ready")
335
+ invocationRef = event.invocation_ref;
336
+ report(event);
337
+ },
323
338
  });
324
- this.appendAssistantMessageForTrigger(triggerMessage, normalizeAnswer(output.answer));
339
+ answer = normalizeAnswer(output.answer);
340
+ if (invocationRef !== undefined) {
341
+ readSources = readExternalReferenceReadSources(this.options.store.db, invocationRef);
342
+ }
325
343
  }
326
344
  catch {
345
+ report({ stage: "failed", reason_code: "answer_failed" });
327
346
  this.appendAssistantMessageForTrigger(triggerMessage, assistantFailureMessage(), {
328
347
  scratchpad_source: "exclude",
329
348
  });
349
+ report({ stage: "reply_persisted" });
350
+ return;
330
351
  }
352
+ // Persistence/outbound-hook failure must not retry by creating a second reply.
353
+ this.appendAssistantMessageForTrigger(triggerMessage, answer, readSources.length === 0 ? undefined : { external_reference_reads: readSources });
354
+ report({ stage: "reply_persisted", reference_source_count: readSources.length });
331
355
  }
332
356
  }
333
357
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=reference-freshness-smoke-cli.d.ts.map
@@ -0,0 +1,40 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+ import { parseArgs } from "node:util";
4
+ import { resolveOpenAiProviderConfig } from "../providers/openai/provider-config.js";
5
+ import { readOpenAiApiKeyCredential } from "../providers/openai/credential-store.js";
6
+ import { runReferenceFreshnessSmoke } from "./reference-freshness-smoke.js";
7
+ async function main() {
8
+ const { values } = parseArgs({
9
+ options: {
10
+ "project-id": { type: "string" },
11
+ "tutti-home": { type: "string" },
12
+ "reread-only": { type: "boolean" },
13
+ },
14
+ });
15
+ if (!/^prj_[0-9A-Z]{26}$/u.test(values["project-id"] ?? ""))
16
+ throw new Error("invalid_project_id");
17
+ const projectId = values["project-id"];
18
+ const tuttiHome = values["tutti-home"] ?? join(homedir(), ".tutti");
19
+ const provider = resolveOpenAiProviderConfig({ tuttiHome, projectId });
20
+ if (provider.kind === "unavailable")
21
+ throw new Error("provider_unavailable");
22
+ const credential = readOpenAiApiKeyCredential(tuttiHome, projectId, provider.config.credential_ref);
23
+ // Read only the selected project's provider configuration; never its documents or database.
24
+ await runReferenceFreshnessSmoke({
25
+ apiKey: credential.secret,
26
+ model: provider.config.default_model,
27
+ rereadOnly: values["reread-only"] ?? false,
28
+ ...(provider.config.api_base_url === undefined
29
+ ? {}
30
+ : { apiBaseUrl: provider.config.api_base_url }),
31
+ report: (result) => console.log(JSON.stringify(result)),
32
+ });
33
+ console.log(JSON.stringify({ status: "passed", cases: values["reread-only"] ? 2 : 7 }));
34
+ }
35
+ void main().catch(() => {
36
+ // Provider errors and assertion messages can contain private runtime details.
37
+ console.error(JSON.stringify({ status: "failed", reason_code: "reference_freshness_smoke_failed" }));
38
+ process.exitCode = 1;
39
+ });
40
+ //# sourceMappingURL=reference-freshness-smoke-cli.js.map
@@ -0,0 +1,23 @@
1
+ /** Fixture-only wording check, not a production semantic validator. */
2
+ export declare function labelsPriorReferenceAsUnverified(answer: string): boolean;
3
+ /** Explicit live-model gate. All documents, project state and platform transport are synthetic. */
4
+ export declare function runReferenceFreshnessSmoke(input: {
5
+ apiKey: string;
6
+ apiBaseUrl?: string;
7
+ model: string;
8
+ rereadOnly?: boolean;
9
+ /** Optional inspection of synthetic fixture answers; the CLI never logs answer bodies. */
10
+ inspectSyntheticAnswer?: (result: {
11
+ case: string;
12
+ answer: string;
13
+ }) => void;
14
+ report: (result: {
15
+ case: string;
16
+ status: "started" | "passed" | "failed";
17
+ reason_code?: string;
18
+ external_reads: number;
19
+ history_reads: number;
20
+ local_reads: number;
21
+ }) => void;
22
+ }): Promise<void>;
23
+ //# sourceMappingURL=reference-freshness-smoke.d.ts.map
@@ -0,0 +1,274 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtempSync, mkdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import Fastify from "fastify";
7
+ import { ExternalReferenceTransportError } from "@tutti/relay-client";
8
+ import { createProjectId } from "@tutti/shared/ids";
9
+ import { createMainChatMessage, readMainChatMessageWindow } from "../collaboration-state/index.js";
10
+ import { openHostProjectStore } from "../store/index.js";
11
+ import { runGit } from "../workspace-ops/git.js";
12
+ import { ExternalReferenceCache } from "../reference-access/cache.js";
13
+ import { ExternalReferenceAccess } from "../reference-access/service.js";
14
+ import { recordExternalReferenceRead } from "../reference-access/read-evidence.js";
15
+ import { WorkspaceEventBus } from "../server-shell/http/workspace-events.js";
16
+ import { AgentContextTokenRegistry } from "../server-shell/http/routes/agent-context-auth.js";
17
+ import { registerAgentContextReferenceRoutes } from "../server-shell/http/routes/agent-context-reference-routes.js";
18
+ import { registerAgentContextExternalReferenceRoutes } from "../server-shell/http/routes/agent-context-external-reference-routes.js";
19
+ import { registerAgentContextMessageRoutes } from "../server-shell/http/routes/agent-context-messages-routes.js";
20
+ import { runCodexAppServerReadOnlyProcedure } from "../providers/openai/app-server/read-only-procedure.js";
21
+ import { CHAT_ASSISTANT_OUTPUT_SCHEMA, isChatAssistantOutput, ReadOnlyChatAssistant, } from "./index.js";
22
+ /** Fixture-only wording check, not a production semantic validator. */
23
+ export function labelsPriorReferenceAsUnverified(answer) {
24
+ return (/先前|此前|上次|之前|历史|previous|historical|last (?:read|known)/iu.test(answer) &&
25
+ /无法确认|不能|不代表|并非|不是|未能确认|cannot|not (?:current|verified)|does not (?:prove|establish)/iu.test(answer));
26
+ }
27
+ /** Explicit live-model gate. All documents, project state and platform transport are synthetic. */
28
+ export async function runReferenceFreshnessSmoke(input) {
29
+ const root = realpathSync(mkdtempSync(join(tmpdir(), "tutti-reference-freshness-")));
30
+ const workspaceRoot = join(root, "repo");
31
+ mkdirSync(workspaceRoot);
32
+ const projectId = createProjectId();
33
+ const store = openHostProjectStore({
34
+ projectId,
35
+ localStoreRoot: join(root, "store"),
36
+ migrationsDirectory: fileURLToPath(new URL("../../migrations", import.meta.url)),
37
+ });
38
+ const app = Fastify({ logger: false });
39
+ let processCleanupUncertain = false;
40
+ try {
41
+ runGit(["init", "-q", "-b", "tutti/mainline"], workspaceRoot);
42
+ runGit(["config", "user.name", "Reference Smoke"], workspaceRoot);
43
+ runGit(["config", "user.email", "smoke@example.test"], workspaceRoot);
44
+ const legacyPath = "docs/reference/files/project-brief.md";
45
+ mkdirSync(join(workspaceRoot, "docs/reference/files"), { recursive: true });
46
+ writeFileSync(join(workspaceRoot, legacyPath), "# Project brief\nHistorical marker: ORCHID_17\n");
47
+ runGit(["add", "."], workspaceRoot);
48
+ runGit(["commit", "-qm", "Synthetic historical snapshot"], workspaceRoot);
49
+ const nowIso = new Date().toISOString();
50
+ store.db
51
+ .prepare(`INSERT INTO managed_reference_sources
52
+ (source_ref, adapter_key, reference_path, accepted_revision_ref, accepted_blob_oid,
53
+ accepted_content_sha256, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
54
+ .run("managed_smoke", "feishu", legacyPath, "1", "a".repeat(40), "b".repeat(64), nowIso, nowIso);
55
+ const source = {
56
+ source_ref: `external_ref_${"a".repeat(32)}`,
57
+ kind: "feishu_docx",
58
+ name: "Project brief",
59
+ created_at: nowIso,
60
+ };
61
+ const other = {
62
+ ...source,
63
+ source_ref: `external_ref_${"b".repeat(32)}`,
64
+ kind: "feishu_file",
65
+ name: "Another team's attachment",
66
+ };
67
+ const sources = [source];
68
+ let revision = "1";
69
+ let marker = "CEDAR_29";
70
+ let deny = false;
71
+ let remoteReads = 0;
72
+ let historyReads = 0;
73
+ let localReads = 0;
74
+ const registry = new AgentContextTokenRegistry();
75
+ const service = new ExternalReferenceAccess({
76
+ createCache: () => new ExternalReferenceCache({ workspaceRoot, ledgerRoot: join(root, "cache") }),
77
+ list: () => Promise.resolve(sources.map((item) => ({ ...item }))),
78
+ read: (sourceRef) => {
79
+ remoteReads++;
80
+ if (deny)
81
+ return Promise.reject(new ExternalReferenceTransportError("feishu_docx_access_denied"));
82
+ assert.equal(sourceRef, source.source_ref, "wrong_source_selected");
83
+ return Promise.resolve({
84
+ content: Buffer.from(`# Project brief\nCurrent marker: ${marker}\n`),
85
+ revision,
86
+ media_type: "text/plain",
87
+ });
88
+ },
89
+ recordRead: (invocation, copy) => recordExternalReferenceRead(store.db, invocation, copy),
90
+ });
91
+ const routes = {
92
+ project: {
93
+ project_id: projectId,
94
+ display_name: "Reference smoke",
95
+ identity_source: "git_config",
96
+ },
97
+ store,
98
+ workspaceOps: { workspaceRoot, viewerAssetCacheRoot: join(root, "viewer-cache") },
99
+ tokenRegistry: registry,
100
+ externalReferences: service,
101
+ };
102
+ app.addHook("onRequest", (request, _reply, done) => {
103
+ const path = request.url.split("?")[0];
104
+ if (path === "/internal/agent-context/messages")
105
+ historyReads++;
106
+ if (path === "/internal/agent-context/references/file")
107
+ localReads++;
108
+ done();
109
+ });
110
+ registerAgentContextReferenceRoutes(app, routes);
111
+ registerAgentContextExternalReferenceRoutes(app, routes);
112
+ registerAgentContextMessageRoutes(app, routes);
113
+ const address = await app.listen({ port: 0, host: "127.0.0.1" });
114
+ const agentContext = {
115
+ baseUrl: `${address}/internal/agent-context`,
116
+ issueToken: (request) => {
117
+ const token = registry.issueToken({
118
+ projectId,
119
+ selection: "chat_assistant",
120
+ scopes: request.scopes,
121
+ });
122
+ return {
123
+ token: token.token,
124
+ invocation_ref: token.reference_invocation_ref,
125
+ expires_at: token.expires_at,
126
+ };
127
+ },
128
+ };
129
+ const say = (body, descriptors) => createMainChatMessage(store.db, {
130
+ author: { kind: "human", display_name: "Tester" },
131
+ body,
132
+ ...(descriptors === undefined ? {} : { refs: { external_references: descriptors } }),
133
+ });
134
+ const ask = async (name, question, expected) => {
135
+ const before = { remote: remoteReads, history: historyReads, local: localReads };
136
+ input.report({
137
+ case: name,
138
+ status: "started",
139
+ external_reads: 0,
140
+ history_reads: 0,
141
+ local_reads: 0,
142
+ });
143
+ const check = (condition, reason) => {
144
+ if (condition)
145
+ return;
146
+ input.report({
147
+ case: name,
148
+ status: "failed",
149
+ reason_code: reason,
150
+ external_reads: remoteReads - before.remote,
151
+ history_reads: historyReads - before.history,
152
+ local_reads: localReads - before.local,
153
+ });
154
+ throw new Error("reference_freshness_assertion_failed");
155
+ };
156
+ let providerFailed = false;
157
+ // Each answer uses a new assistant instance and ephemeral app-server Thread.
158
+ const assistant = new ReadOnlyChatAssistant({
159
+ project: routes.project,
160
+ store,
161
+ workspaceRoot,
162
+ events: new WorkspaceEventBus(),
163
+ model: {
164
+ answer: async (turn) => {
165
+ try {
166
+ const result = await runCodexAppServerReadOnlyProcedure({
167
+ apiKey: input.apiKey,
168
+ ...(input.apiBaseUrl === undefined ? {} : { apiBaseUrl: input.apiBaseUrl }),
169
+ defaultModel: input.model,
170
+ workspaceRoot,
171
+ prompt: turn.prompt,
172
+ outputSchema: CHAT_ASSISTANT_OUTPUT_SCHEMA,
173
+ validateOutput: isChatAssistantOutput,
174
+ skillSelectionName: "chat_assistant",
175
+ agentContext,
176
+ baseEnv: { PATH: process.env.PATH, HOME: root },
177
+ webSearchMode: "disabled",
178
+ requestTimeoutMs: 60_000,
179
+ turnTimeoutMs: 180_000,
180
+ ...(turn.onProgress === undefined ? {} : { onProgress: turn.onProgress }),
181
+ onLifecycleIncident: () => {
182
+ processCleanupUncertain = true;
183
+ },
184
+ });
185
+ return result.output;
186
+ }
187
+ catch (error) {
188
+ providerFailed = true;
189
+ throw error;
190
+ }
191
+ },
192
+ },
193
+ });
194
+ assistant.enqueue({ message: say(`@tutti ${question}`) });
195
+ await assistant.waitForIdle();
196
+ check(!providerFailed, "provider_failed");
197
+ const reply = readMainChatMessageWindow(store.db).items.at(-1);
198
+ input.inspectSyntheticAnswer?.({ case: name, answer: reply.body });
199
+ check(reply.author.kind === "agent", "reply_missing");
200
+ if (expected.marker)
201
+ check(reply.body.includes(expected.marker), "expected_content_missing");
202
+ if (expected.current) {
203
+ check(remoteReads - before.remote === 1, "current_read_missing_or_repeated");
204
+ check(JSON.stringify(reply.refs?.external_reference_reads) ===
205
+ JSON.stringify([source.source_ref]), "evidence_missing");
206
+ check(!reply.body.includes("ORCHID_17"), "legacy_content_used");
207
+ }
208
+ else {
209
+ check(reply.refs?.external_reference_reads === undefined, "unverified_source_attached");
210
+ check(remoteReads - before.remote === (expected.denied ? 1 : 0), "unexpected_read_count");
211
+ }
212
+ if (expected.history)
213
+ check(historyReads > before.history, "history_not_consulted");
214
+ if (expected.local)
215
+ check(localReads > before.local, "historical_read_missing");
216
+ if (expected.denied) {
217
+ check(localReads === before.local, "legacy_fallback");
218
+ if (/ORCHID_17|CEDAR_29|MAPLE_43|BIRCH_61/u.test(reply.body)) {
219
+ check(labelsPriorReferenceAsUnverified(reply.body), "unlabelled_historical_content");
220
+ }
221
+ check(/无法|不能|失败|权限|不可|未能|无法确认|unable|cannot|access|unavailable/iu.test(reply.body), "missing_access_limitation");
222
+ }
223
+ input.report({
224
+ case: name,
225
+ status: "passed",
226
+ external_reads: remoteReads - before.remote,
227
+ history_reads: historyReads - before.history,
228
+ local_reads: localReads - before.local,
229
+ });
230
+ };
231
+ say("这是本项目要使用的文档。", [source]);
232
+ await ask("initial_external", "读取这份文档,告诉我当前标记。", { marker, current: true });
233
+ await ask("reread_without_update", "请重新读取刚才的文档,核对当前内容。", {
234
+ marker,
235
+ current: true,
236
+ });
237
+ if (input.rereadOnly) {
238
+ assert.equal(processCleanupUncertain, false, "process_cleanup_unverified");
239
+ return;
240
+ }
241
+ revision = "2";
242
+ marker = "MAPLE_43";
243
+ source.name = "Renamed project plan";
244
+ say("我已经修改了文档内容,也改了文件名。");
245
+ await ask("reread_after_update_and_rename", "请重新读取刚才的文档,核对当前内容。", {
246
+ marker,
247
+ current: true,
248
+ });
249
+ sources.push(other);
250
+ say("这是另一个团队的附件,先不用处理。", [other]);
251
+ say("我们下午再讨论排期。");
252
+ say("会议室已经预订好了。");
253
+ say("好的,收到。");
254
+ revision = "3";
255
+ marker = "BIRCH_61";
256
+ await ask("reread_beyond_compact_window", "最开始那份项目文档又更新了。请重新读取并核对当前标记,不是另一个团队的附件。", { marker, current: true, history: true });
257
+ deny = true;
258
+ await ask("revoked_access_no_legacy_fallback", "请再次读取刚才那份项目文档,确认现在的标记。", {
259
+ current: false,
260
+ denied: true,
261
+ });
262
+ await ask("explicit_historical_read", `请读取本地历史快照 ${legacyPath},只说明其中保存的旧标记,不要查在线版本。`, { marker: "ORCHID_17", current: false, local: true });
263
+ await ask("ordinary_question", "一加一等于几?", { current: false });
264
+ assert.equal(processCleanupUncertain, false, "process_cleanup_unverified");
265
+ }
266
+ finally {
267
+ await app.close();
268
+ store.close();
269
+ // Never remove a temp root while an unconfirmed app-server process may use it.
270
+ if (!processCleanupUncertain)
271
+ rmSync(root, { recursive: true, force: true });
272
+ }
273
+ }
274
+ //# sourceMappingURL=reference-freshness-smoke.js.map
@@ -1,7 +1,7 @@
1
1
  export { ingestTrustedConversationSource, TRUSTED_INTEGRATION_INGESTION_PATH, type IngestTrustedConversationSourceOptions, } from "./external-source.js";
2
2
  export { ingestMainChatMessage, type IngestMainChatMessageOptions, type MainChatMessageMaterialization, } from "./main-chat.js";
3
3
  export { ingestManagedReferenceSnapshot, publishManagedReferenceSnapshotChanged, type IngestManagedReferenceSnapshotOptions, } from "./managed-reference-snapshots.js";
4
- export { ingestTrustedManagedReference, type IngestTrustedManagedReferenceOptions, type ResolvedManagedReferenceContent, } from "./managed-reference-sources.js";
4
+ export { ingestTrustedManagedReference, readManagedReferenceSourcePaths, type IngestTrustedManagedReferenceOptions, type ResolvedManagedReferenceContent, } from "./managed-reference-sources.js";
5
5
  export { ingestReferenceFile, REFERENCE_FILE_INGESTION_COMMAND_NAME, REFERENCE_FILE_INGESTION_TARGET_REF, toReferenceFileProjection, type IngestReferenceFileOptions, } from "./reference-files.js";
6
6
  export { ingestTrustedReferenceAttachment, TRUSTED_INTEGRATION_REFERENCE_ATTACHMENT_PATH, type IngestTrustedReferenceAttachmentOptions, type ResolvedReferenceAttachmentContent, } from "./reference-attachments.js";
7
7
  export type { ChatAssistantEnqueuer, ReferenceSummaryStarter, ScratchpadSourceNotifier, TrustedCollaborationAuthor, } from "./types.js";
@@ -1,7 +1,7 @@
1
1
  export { ingestTrustedConversationSource, TRUSTED_INTEGRATION_INGESTION_PATH, } from "./external-source.js";
2
2
  export { ingestMainChatMessage, } from "./main-chat.js";
3
3
  export { ingestManagedReferenceSnapshot, publishManagedReferenceSnapshotChanged, } from "./managed-reference-snapshots.js";
4
- export { ingestTrustedManagedReference, } from "./managed-reference-sources.js";
4
+ export { ingestTrustedManagedReference, readManagedReferenceSourcePaths, } from "./managed-reference-sources.js";
5
5
  export { ingestReferenceFile, REFERENCE_FILE_INGESTION_COMMAND_NAME, REFERENCE_FILE_INGESTION_TARGET_REF, toReferenceFileProjection, } from "./reference-files.js";
6
6
  export { ingestTrustedReferenceAttachment, TRUSTED_INTEGRATION_REFERENCE_ATTACHMENT_PATH, } from "./reference-attachments.js";
7
7
  //# sourceMappingURL=index.js.map
@@ -1,7 +1,9 @@
1
1
  import type { IntegrationManagedReferenceDisposition, TrustedManagedReferenceIngestionCommand } from "@tutti/shared/schemas/internal";
2
- import type { HostProjectStore } from "../store/index.js";
2
+ import type { HostProjectStore, SqliteDatabase } from "../store/index.js";
3
3
  import type { WorkspaceEventBus } from "../server-shell/http/workspace-events.js";
4
4
  import type { ReferenceSummaryStarter } from "./types.js";
5
+ /** Dedicated source records identify snapshots; filenames and text are not provenance. */
6
+ export declare function readManagedReferenceSourcePaths(db: SqliteDatabase): ReadonlySet<string>;
5
7
  export type ResolvedManagedReferenceContent = {
6
8
  content: Buffer;
7
9
  complete: () => Promise<void>;
@@ -2,6 +2,13 @@ import { createHash } from "node:crypto";
2
2
  import { isTrustedManagedReferenceIngestionCommand } from "@tutti/shared/schemas/internal";
3
3
  import { readManagedReferenceSnapshotState, WorkspaceOpsError, writeManagedReferenceSnapshot, } from "../workspace-ops/index.js";
4
4
  import { publishManagedReferenceSnapshotChanged } from "./managed-reference-snapshots.js";
5
+ /** Dedicated source records identify snapshots; filenames and text are not provenance. */
6
+ export function readManagedReferenceSourcePaths(db) {
7
+ return new Set(db
8
+ .prepare("SELECT reference_path FROM managed_reference_sources WHERE accepted_revision_ref IS NOT NULL")
9
+ .all()
10
+ .map((row) => row.reference_path));
11
+ }
5
12
  function readSource(db, sourceRef) {
6
13
  return (db
7
14
  .prepare("SELECT * FROM managed_reference_sources WHERE source_ref = ?")