@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.
- package/README.md +4 -2
- package/dist/chat-assistant/diagnostics.d.ts +1 -0
- package/dist/chat-assistant/index.js +25 -17
- package/dist/chat-assistant/reference-freshness-smoke-cli.d.ts +2 -0
- package/dist/chat-assistant/reference-freshness-smoke-cli.js +40 -0
- package/dist/chat-assistant/reference-freshness-smoke.d.ts +23 -0
- package/dist/chat-assistant/reference-freshness-smoke.js +274 -0
- package/dist/collaboration-ingestion/external-source.js +2 -0
- package/dist/collaboration-ingestion/index.d.ts +1 -1
- package/dist/collaboration-ingestion/index.js +1 -1
- package/dist/collaboration-ingestion/main-chat.js +9 -2
- package/dist/collaboration-ingestion/managed-reference-sources.d.ts +3 -1
- package/dist/collaboration-ingestion/managed-reference-sources.js +7 -0
- package/dist/collaboration-ingestion/types.d.ts +1 -0
- package/dist/collaboration-state/task-compile-context.d.ts +1 -1
- package/dist/control-plane/scratchpad-source-messages.js +9 -3
- package/dist/control-plane/task-source-context-types.d.ts +1 -1
- package/dist/control-plane/task-source-context.js +4 -2
- package/dist/reference-access/message-context.d.ts +6 -0
- package/dist/reference-access/message-context.js +11 -0
- package/dist/reference-access/read-evidence.d.ts +2 -0
- package/dist/reference-access/read-evidence.js +8 -0
- package/dist/server-shell/cli/external-reference-runtime.js +1 -0
- package/dist/server-shell/cli/relay-registration.js +1 -0
- package/dist/server-shell/http/routes/agent-context-external-reference-routes.js +1 -0
- package/dist/server-shell/http/routes/agent-context-messages-routes.js +2 -0
- package/dist/server-shell/http/routes/agent-context-reference-routes.js +37 -3
- package/dist/server-shell/http/routes/agent-context-references.d.ts +11 -0
- package/dist/server-shell/http/routes/agent-context-references.js +14 -0
- package/node_modules/@tutti/relay-client/dist/host-control.d.ts +1 -0
- package/node_modules/@tutti/relay-client/dist/host-control.js +3 -0
- package/node_modules/@tutti/shared/dist/schemas/api/clarifications.d.ts +3 -1
- package/node_modules/@tutti/shared/dist/schemas/api/external-references.d.ts +27 -4
- package/node_modules/@tutti/shared/dist/schemas/api/external-references.js +32 -3
- package/node_modules/@tutti/shared/dist/schemas/api/index.d.ts +1 -1
- package/node_modules/@tutti/shared/dist/schemas/api/index.js +1 -1
- package/node_modules/@tutti/shared/dist/schemas/api/messages.d.ts +12 -4
- package/node_modules/@tutti/shared/dist/schemas/api/messages.js +3 -1
- package/node_modules/@tutti/shared/dist/schemas/api/platform-sessions.d.ts +5 -3
- package/node_modules/@tutti/shared/dist/schemas/api/platform-sessions.js +16 -1
- package/node_modules/@tutti/shared/dist/schemas/api/viewer-reference.d.ts +3 -1
- package/node_modules/@tutti/shared/dist/schemas/internal/external-references.d.ts +15 -2
- package/node_modules/@tutti/shared/dist/schemas/internal/external-references.js +4 -2
- package/node_modules/@tutti/shared/dist/schemas/internal/platform-integration.d.ts +49 -8
- package/node_modules/@tutti/shared/dist/schemas/internal/platform-integration.js +14 -2
- package/package.json +1 -1
- package/prompts/chat-assistant.md +17 -15
- package/prompts/procedures/scratchpad-refresh.md +22 -2
- package/prompts/procedures/task-compile.md +1 -0
- package/prompts/runs/task-execute.md +2 -0
- package/prompts/skills/README.md +1 -1
- package/prompts/skills/read-references/README.md +2 -2
- package/prompts/skills/read-references/SKILL.md +24 -37
- package/web/assets/{homepage-motion-scene-B6yPJnY8.js → homepage-motion-scene-TmkKf8wQ.js} +1 -1
- package/web/assets/{index-aQnJ5JoI.js → index-B1oL1p9_.js} +3 -3
- 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.
|
|
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
|
|
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:
|
|
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,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
|
-
...(
|
|
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" | "
|
|
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" &&
|
|
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.
|
|
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();
|