@xfey/tutti 0.1.109 → 0.1.111

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 (56) hide show
  1. package/README.md +4 -2
  2. package/dist/chat-assistant/diagnostics.d.ts +1 -0
  3. package/dist/chat-assistant/index.js +25 -17
  4. package/dist/chat-assistant/reference-freshness-smoke-cli.d.ts +2 -0
  5. package/dist/chat-assistant/reference-freshness-smoke-cli.js +40 -0
  6. package/dist/chat-assistant/reference-freshness-smoke.d.ts +23 -0
  7. package/dist/chat-assistant/reference-freshness-smoke.js +274 -0
  8. package/dist/collaboration-ingestion/external-source.js +2 -0
  9. package/dist/collaboration-ingestion/index.d.ts +1 -1
  10. package/dist/collaboration-ingestion/index.js +1 -1
  11. package/dist/collaboration-ingestion/main-chat.js +9 -2
  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-ingestion/types.d.ts +1 -0
  15. package/dist/collaboration-state/task-compile-context.d.ts +1 -1
  16. package/dist/control-plane/scratchpad-source-messages.js +9 -3
  17. package/dist/control-plane/task-source-context-types.d.ts +1 -1
  18. package/dist/control-plane/task-source-context.js +4 -2
  19. package/dist/reference-access/message-context.d.ts +6 -0
  20. package/dist/reference-access/message-context.js +11 -0
  21. package/dist/reference-access/read-evidence.d.ts +2 -0
  22. package/dist/reference-access/read-evidence.js +8 -0
  23. package/dist/server-shell/cli/external-reference-runtime.js +1 -0
  24. package/dist/server-shell/cli/relay-registration.js +1 -0
  25. package/dist/server-shell/http/routes/agent-context-external-reference-routes.js +1 -0
  26. package/dist/server-shell/http/routes/agent-context-messages-routes.js +2 -0
  27. package/dist/server-shell/http/routes/agent-context-reference-routes.js +37 -3
  28. package/dist/server-shell/http/routes/agent-context-references.d.ts +11 -0
  29. package/dist/server-shell/http/routes/agent-context-references.js +14 -0
  30. package/node_modules/@tutti/relay-client/dist/host-control.d.ts +1 -0
  31. package/node_modules/@tutti/relay-client/dist/host-control.js +3 -0
  32. package/node_modules/@tutti/shared/dist/schemas/api/clarifications.d.ts +3 -1
  33. package/node_modules/@tutti/shared/dist/schemas/api/external-references.d.ts +27 -4
  34. package/node_modules/@tutti/shared/dist/schemas/api/external-references.js +32 -3
  35. package/node_modules/@tutti/shared/dist/schemas/api/index.d.ts +1 -1
  36. package/node_modules/@tutti/shared/dist/schemas/api/index.js +1 -1
  37. package/node_modules/@tutti/shared/dist/schemas/api/messages.d.ts +12 -4
  38. package/node_modules/@tutti/shared/dist/schemas/api/messages.js +3 -1
  39. package/node_modules/@tutti/shared/dist/schemas/api/platform-sessions.d.ts +5 -3
  40. package/node_modules/@tutti/shared/dist/schemas/api/platform-sessions.js +16 -1
  41. package/node_modules/@tutti/shared/dist/schemas/api/viewer-reference.d.ts +3 -1
  42. package/node_modules/@tutti/shared/dist/schemas/internal/external-references.d.ts +15 -2
  43. package/node_modules/@tutti/shared/dist/schemas/internal/external-references.js +4 -2
  44. package/node_modules/@tutti/shared/dist/schemas/internal/platform-integration.d.ts +49 -8
  45. package/node_modules/@tutti/shared/dist/schemas/internal/platform-integration.js +14 -2
  46. package/package.json +1 -1
  47. package/prompts/chat-assistant.md +17 -15
  48. package/prompts/procedures/scratchpad-refresh.md +22 -2
  49. package/prompts/procedures/task-compile.md +1 -0
  50. package/prompts/runs/task-execute.md +2 -0
  51. package/prompts/skills/README.md +1 -1
  52. package/prompts/skills/read-references/README.md +2 -2
  53. package/prompts/skills/read-references/SKILL.md +24 -37
  54. package/web/assets/{homepage-motion-scene-B6yPJnY8.js → homepage-motion-scene-TmkKf8wQ.js} +1 -1
  55. package/web/assets/{index-aQnJ5JoI.js → index-B1oL1p9_.js} +3 -3
  56. package/web/index.html +1 -1
package/README.md CHANGED
@@ -4,7 +4,9 @@
4
4
 
5
5
  `apps/server` 是 Tutti 的项目 host service,也是机器级 Local Console 的进程入口。
6
6
 
7
- `0.1.109` Codex skill 接入统一验证 `skills/extraRoots/set` / `skills/list` 与实际 `SKILL.md` 文件身份;Chat 的安全阶段日志可通过 invocation ref 关联外部 Reference 读取。版本发布、测试 Host 切换和飞书真人门禁分别记录于[按需资料计划](../../docs/roadmap/feishu-on-demand-reference-plan.md),不以安装版本代替实际读取证据。
7
+ `0.1.110` 已发布每次 Chat invocation 的外部资料核验规则、本地/外部 provenance、从实际读取证据派生的回答来源线索,以及隔离多轮模型 smoke;配套 F.3 增量支持群资料确认与普通云盘文件。Web/Relay/Integration 已先行同步,registry byte match 与本机 CLI/Console/原在线 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),不以安装版本代替实际读取证据。
8
10
 
9
11
  它负责:
10
12
 
@@ -43,7 +45,7 @@
43
45
  - Host server 与 Local Console 支持在显式 `webStatic.root` 或 packaged `web/` 存在时服务 Web 静态产物;`/api/*`、SSE、health、host-local control 和 `/local-console/*` API 不被 SPA fallback 吞掉。
44
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` 与测试产物。
45
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 存在。
46
- - 对外发布必须把同一 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。
47
49
  - `prompts/` 是所有模型-facing prompt、指令模板和 Codex 执行模板的统一落点;长 prompt 不内联在 TypeScript 中。
48
50
  - Host Project API只信任Relay tunnel metadata注入的Surface-matched browser context;direct loopback缺少可信context时不作为浏览器工作区入口。
49
51
  - `binding.json.surface_profile`是Project Surface的durable authority,legacy binding默认Standalone;已有project不能通过普通launch或Local Console隐式切换Surface。Relay只保存不可变mirror用于边缘gate。
@@ -6,6 +6,7 @@ export type ChatAssistantLogger = {
6
6
  type ChatAssistantProgress = ProviderInvocationProgress | {
7
7
  stage: "queued" | "inspection_wait" | "model_requested" | "reply_persisted" | "failed" | "skipped";
8
8
  reason_code?: "queue_full" | "stopping" | "context_unavailable" | "answer_failed" | "reply_failed";
9
+ reference_source_count?: number;
9
10
  };
10
11
  export declare function createChatAssistantDiagnostics(options: {
11
12
  logger?: ChatAssistantLogger;
@@ -3,6 +3,8 @@ import { createClarificationRoundAgentMessage, createMainChatAgentMessage, readC
3
3
  import { renderPromptTemplate } from "../prompt-templates/index.js";
4
4
  import { withHostStoreTransaction, } from "../store/index.js";
5
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";
6
8
  const CHAT_ASSISTANT_CONTEXT_MESSAGE_LIMIT = 3;
7
9
  const CHAT_ASSISTANT_ROUND_MESSAGE_LIMIT = 50;
8
10
  const CHAT_ASSISTANT_ROUND_MESSAGE_MAX_CHARS = 2_000;
@@ -47,13 +49,17 @@ function assistantFailureMessage() {
47
49
  function messageToContext(message) {
48
50
  const authorDisplayName = message.author.display_name?.trim();
49
51
  return {
52
+ ...(message.refs?.development_operator === true ? { source_role: "operator" } : {}),
50
53
  ...(authorDisplayName === undefined || authorDisplayName === ""
51
54
  ? {}
52
55
  : { author_display_name: authorDisplayName }),
53
56
  text: messageTextForModel(message),
57
+ ...externalReferenceMessageContext(message.refs),
54
58
  };
55
59
  }
56
60
  function messageRole(message) {
61
+ if (message.author.kind === "system" && message.refs?.development_operator === true)
62
+ return "operator";
57
63
  if (message.author.kind === "agent") {
58
64
  return "tutti";
59
65
  }
@@ -90,6 +96,7 @@ function roundMessagesToContext(messages) {
90
96
  ? {}
91
97
  : { author_display_name: authorDisplayName }),
92
98
  text,
99
+ ...externalReferenceMessageContext(message.refs),
93
100
  });
94
101
  }
95
102
  return result;
@@ -106,6 +113,10 @@ export class ReadOnlyChatAssistant {
106
113
  this.maxQueuedMentions = options.maxQueuedMentions ?? 8;
107
114
  }
108
115
  enqueue(input) {
116
+ if (input.message.author.kind !== "human" &&
117
+ !(input.message.author.kind === "system" && input.message.refs?.development_operator === true)) {
118
+ return { triggered: false };
119
+ }
109
120
  if (!containsTuttiMention(messageTextForTuttiMention(input.message))) {
110
121
  return { triggered: false };
111
122
  }
@@ -179,18 +190,12 @@ export class ReadOnlyChatAssistant {
179
190
  this.idleWaiters.clear();
180
191
  }
181
192
  buildMainChatContext(triggerMessage) {
182
- const authorDisplayName = triggerMessage.author.display_name?.trim();
183
193
  return {
184
194
  mode: "main_chat",
185
195
  project: {
186
196
  display_name: this.options.project.display_name,
187
197
  },
188
- trigger: {
189
- ...(authorDisplayName === undefined || authorDisplayName === ""
190
- ? {}
191
- : { author_display_name: authorDisplayName }),
192
- text: messageTextForModel(triggerMessage),
193
- },
198
+ trigger: messageToContext(triggerMessage),
194
199
  recent_messages: readMainChatMessageWindow(this.options.store.db, {
195
200
  limit: CHAT_ASSISTANT_CONTEXT_MESSAGE_LIMIT,
196
201
  }).items.map(messageToContext),
@@ -213,18 +218,12 @@ export class ReadOnlyChatAssistant {
213
218
  before_id: cardMessage.id,
214
219
  limit: CHAT_ASSISTANT_ROUND_MAIN_CHAT_CONTEXT_LIMIT,
215
220
  }).map(messageToContext);
216
- const authorDisplayName = triggerMessage.author.display_name?.trim();
217
221
  return {
218
222
  mode: "clarification_round",
219
223
  project: {
220
224
  display_name: this.options.project.display_name,
221
225
  },
222
- trigger: {
223
- ...(authorDisplayName === undefined || authorDisplayName === ""
224
- ? {}
225
- : { author_display_name: authorDisplayName }),
226
- text: messageTextForModel(triggerMessage),
227
- },
226
+ trigger: messageToContext(triggerMessage),
228
227
  round_request: {
229
228
  ...(round.request_payload.title === undefined
230
229
  ? {}
@@ -325,6 +324,8 @@ export class ReadOnlyChatAssistant {
325
324
  }
326
325
  async respond(triggerMessage, report) {
327
326
  let answer;
327
+ let invocationRef;
328
+ let readSources = [];
328
329
  try {
329
330
  report({ stage: "inspection_wait" });
330
331
  await this.options.waitForInspectionAvailability?.();
@@ -336,9 +337,16 @@ export class ReadOnlyChatAssistant {
336
337
  report({ stage: "model_requested" });
337
338
  const output = await this.options.model.answer({
338
339
  prompt,
339
- onProgress: report,
340
+ onProgress: (event) => {
341
+ if (event.stage === "context_ready")
342
+ invocationRef = event.invocation_ref;
343
+ report(event);
344
+ },
340
345
  });
341
346
  answer = normalizeAnswer(output.answer);
347
+ if (invocationRef !== undefined) {
348
+ readSources = readExternalReferenceReadSources(this.options.store.db, invocationRef);
349
+ }
342
350
  }
343
351
  catch {
344
352
  report({ stage: "failed", reason_code: "answer_failed" });
@@ -349,8 +357,8 @@ export class ReadOnlyChatAssistant {
349
357
  return;
350
358
  }
351
359
  // Persistence/outbound-hook failure must not retry by creating a second reply.
352
- this.appendAssistantMessageForTrigger(triggerMessage, answer);
353
- report({ stage: "reply_persisted" });
360
+ this.appendAssistantMessageForTrigger(triggerMessage, answer, readSources.length === 0 ? undefined : { external_reference_reads: readSources });
361
+ report({ stage: "reply_persisted", reference_source_count: readSources.length });
354
362
  }
355
363
  }
356
364
  //# 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
@@ -83,6 +83,7 @@ export function ingestTrustedConversationSource(options) {
83
83
  commandId: options.command.command_id,
84
84
  payload: { body: source.content.text },
85
85
  author: {
86
+ ...(options.command.actor.kind === "bot" ? { kind: "development_operator" } : {}),
86
87
  account_ref: options.command.actor.account_ref,
87
88
  display_name: options.command.actor.display_name,
88
89
  },
@@ -100,6 +101,7 @@ export function ingestTrustedConversationSource(options) {
100
101
  external_message_ref: source.locator.message_ref,
101
102
  binding_ref: options.command.binding_ref,
102
103
  receipt_key: options.command.receipt.receipt_key,
104
+ ...(options.command.actor.kind === "bot" ? { actor_kind: "bot" } : {}),
103
105
  ...(source.external_references === undefined
104
106
  ? {}
105
107
  : { external_references: source.external_references }),
@@ -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
@@ -10,6 +10,9 @@ export function ingestMainChatMessage(options) {
10
10
  payload: {
11
11
  ...options.payload,
12
12
  author_ref: options.author.account_ref,
13
+ ...(options.author.kind === "development_operator"
14
+ ? { author_kind: "development_operator" }
15
+ : {}),
13
16
  ...(options.idempotencyContext === undefined
14
17
  ? {}
15
18
  : { ingestion_context: options.idempotencyContext }),
@@ -23,12 +26,16 @@ export function ingestMainChatMessage(options) {
23
26
  const message = createMainChatMessage(tx, {
24
27
  body: materialized.body,
25
28
  author: {
26
- kind: "human",
29
+ kind: options.author.kind === "development_operator" ? "system" : "human",
27
30
  ref: options.author.account_ref,
28
31
  display_name: options.author.display_name,
29
32
  ...(options.author.avatar === undefined ? {} : { avatar: options.author.avatar }),
30
33
  },
31
- ...(materialized.refs === undefined ? {} : { refs: materialized.refs }),
34
+ ...(options.author.kind === "development_operator"
35
+ ? { refs: { ...materialized.refs, development_operator: true } }
36
+ : materialized.refs === undefined
37
+ ? {}
38
+ : { refs: materialized.refs }),
32
39
  ...(options.now === undefined ? {} : { now: options.now }),
33
40
  });
34
41
  options.afterCreate?.(tx, message);
@@ -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 = ?")
@@ -1,6 +1,7 @@
1
1
  import type { AccountAvatarProjection } from "@tutti/shared/domain";
2
2
  import type { MessageProjection } from "@tutti/shared/schemas/api";
3
3
  export type TrustedCollaborationAuthor = {
4
+ kind?: "human" | "development_operator";
4
5
  account_ref: string;
5
6
  display_name: string;
6
7
  avatar?: AccountAvatarProjection;
@@ -24,7 +24,7 @@ export declare function completeTaskCompileContext(db: SqliteDatabase, input: Co
24
24
  task_changes: string[];
25
25
  receipts: {
26
26
  compiled_tasks?: {
27
- status: "failed" | "done" | "todo" | "running" | "pending";
27
+ status: "failed" | "done" | "pending" | "todo" | "running";
28
28
  task_id: string;
29
29
  title: string;
30
30
  summary: string;
@@ -19,7 +19,9 @@ export function shouldUseMessageAsScratchpadSource(message) {
19
19
  if (message.scope.kind !== "main_chat") {
20
20
  return false;
21
21
  }
22
- if (message.message_kind === "user_text" && message.author.kind === "human") {
22
+ if (message.message_kind === "user_text" &&
23
+ (message.author.kind === "human" ||
24
+ (message.author.kind === "system" && message.refs?.development_operator === true))) {
23
25
  return true;
24
26
  }
25
27
  if (message.message_kind === "agent_text" && message.author.kind === "agent") {
@@ -31,7 +33,11 @@ export function filterScratchpadSourceMessages(messages) {
31
33
  return messages.filter(shouldUseMessageAsScratchpadSource);
32
34
  }
33
35
  function compactRole(message) {
34
- return message.author.kind === "agent" ? "tutti" : "human";
36
+ return message.refs?.development_operator === true
37
+ ? "operator"
38
+ : message.author.kind === "agent"
39
+ ? "tutti"
40
+ : "human";
35
41
  }
36
42
  export function truncateTextMiddle(text, maxChars) {
37
43
  const trimmed = text.trim();
@@ -54,7 +60,7 @@ function sourceText(message) {
54
60
  return `Reference file (${reference.category}:${reference.name})\n${message.body}`;
55
61
  }
56
62
  function sourceAuthorDisplayName(message) {
57
- if (message.author.kind !== "human") {
63
+ if (message.author.kind !== "human" && message.refs?.development_operator !== true) {
58
64
  return undefined;
59
65
  }
60
66
  const displayName = message.author.display_name?.trim();