- commit
- a02204e
- parent
- 0874a72
- author
- im_wower
- date
- 2026-03-28 18:28:17 +0800 CST
feat: wire artifact persistence into baa pipeline
11 files changed,
+625,
-75
+37,
-15
1@@ -2,12 +2,14 @@ import assert from "node:assert/strict";
2 import test from "node:test";
3
4 import {
5- DEFAULT_BAA_DELIVERY_LINE_LIMIT,
6+ DEFAULT_BAA_DELIVERY_INLINE_THRESHOLD,
7+ DEFAULT_BAA_DELIVERY_SUMMARY_LENGTH,
8 renderBaaDeliveryMessageText
9 } from "../dist/index.js";
10
11 function createExecutionResult(overrides = {}) {
12 return {
13+ artifact: null,
14 data: null,
15 dedupeKey: "dedupe-default",
16 details: null,
17@@ -47,6 +49,20 @@ test("renderBaaDeliveryMessageText renders plain-text execution details", () =>
18 processResult: createProcessResult(
19 [
20 createExecutionResult({
21+ artifact: {
22+ resultText: JSON.stringify({
23+ data: {
24+ a: 1,
25+ b: 2
26+ },
27+ details: {
28+ retryable: false
29+ },
30+ message: "command completed",
31+ ok: true
32+ }, null, 2),
33+ url: "https://conductor.makefile.so/artifact/exec/inst_render_01.txt"
34+ },
35 data: {
36 b: 2,
37 a: 1
38@@ -99,16 +115,16 @@ test("renderBaaDeliveryMessageText renders plain-text execution details", () =>
39 assert.equal(rendered.messageTruncated, false);
40 assert.match(rendered.messageText, /\[BAA 执行结果\]/u);
41 assert.match(rendered.messageText, /assistant_message_id: msg-render-plain/u);
42- assert.match(rendered.messageText, /block_index: 3/u);
43- assert.match(rendered.messageText, /message:\ncommand completed/u);
44- assert.match(rendered.messageText, /data:\n a: 1\n b: 2/u);
45- assert.match(rendered.messageText, /details:\n retryable: false/u);
46+ assert.match(rendered.messageText, /instruction_id: inst_render_01/u);
47+ assert.match(rendered.messageText, /"retryable": false/u);
48+ assert.match(
49+ rendered.messageText,
50+ /完整结果:https:\/\/conductor\.makefile\.so\/artifact\/exec\/inst_render_01\.txt/u
51+ );
52 });
53
54-test("renderBaaDeliveryMessageText truncates overlong output and appends marker", () => {
55- const stdout = Array.from({
56- length: DEFAULT_BAA_DELIVERY_LINE_LIMIT + 20
57- }, (_, index) => `line-${index + 1}`).join("\n");
58+test("renderBaaDeliveryMessageText truncates overlong output and appends artifact URL", () => {
59+ const stdout = `${"A".repeat(DEFAULT_BAA_DELIVERY_INLINE_THRESHOLD)}TRUNCATED_TAIL_MARKER`;
60 const rendered = renderBaaDeliveryMessageText(
61 {
62 assistantMessageId: "msg-render-truncated",
63@@ -116,6 +132,10 @@ test("renderBaaDeliveryMessageText truncates overlong output and appends marker"
64 platform: "claude",
65 processResult: createProcessResult([
66 createExecutionResult({
67+ artifact: {
68+ resultText: stdout,
69+ url: "https://conductor.makefile.so/artifact/exec/inst_render_truncated.txt"
70+ },
71 data: {
72 result: {
73 stdout
74@@ -133,11 +153,13 @@ test("renderBaaDeliveryMessageText truncates overlong output and appends marker"
75 }
76 );
77
78- assert.equal(rendered.messageLineLimit, DEFAULT_BAA_DELIVERY_LINE_LIMIT);
79+ assert.equal(rendered.messageLineLimit, DEFAULT_BAA_DELIVERY_INLINE_THRESHOLD);
80 assert.equal(rendered.messageTruncated, true);
81- assert.equal(rendered.messageLineCount, DEFAULT_BAA_DELIVERY_LINE_LIMIT + 1);
82- assert.ok(rendered.sourceLineCount > rendered.messageLineCount);
83- assert.match(rendered.messageText, /line-1/u);
84- assert.doesNotMatch(rendered.messageText, /line-220/u);
85- assert.match(rendered.messageText, /超长截断$/u);
86+ assert.ok(rendered.messageCharCount < stdout.length);
87+ assert.match(rendered.messageText, new RegExp(`A{${DEFAULT_BAA_DELIVERY_SUMMARY_LENGTH}}`, "u"));
88+ assert.doesNotMatch(rendered.messageText, /TRUNCATED_TAIL_MARKER/u);
89+ assert.match(
90+ rendered.messageText,
91+ /完整结果:https:\/\/conductor\.makefile\.so\/artifact\/exec\/inst_render_truncated\.txt/u
92+ );
93 });
1@@ -1,5 +1,7 @@
2+import { DEFAULT_SUMMARY_LENGTH } from "../../../../packages/artifact-db/dist/index.js";
3 import type { BrowserBridgeController } from "../browser-types.js";
4 import {
5+ type BaaInstructionExecutionResult,
6 sortBaaJsonValue,
7 type BaaInstructionProcessResult,
8 type BaaJsonValue
9@@ -15,6 +17,8 @@ const DEFAULT_COMPLETED_SESSION_TTL_MS = 10 * 60_000;
10 const DEFAULT_DELIVERY_ACTION_RESULT_TIMEOUT_MS = 20_000;
11 const DEFAULT_DELIVERY_ACTION_TIMEOUT_MS = 15_000;
12 export const DEFAULT_BAA_DELIVERY_LINE_LIMIT = 200;
13+export const DEFAULT_BAA_DELIVERY_INLINE_THRESHOLD = 2_000;
14+export const DEFAULT_BAA_DELIVERY_SUMMARY_LENGTH = DEFAULT_SUMMARY_LENGTH;
15 const DEFAULT_DELIVERY_POLL_INTERVAL_MS = 150;
16 const DEFAULT_DELIVERY_RETRY_ATTEMPTS = 2;
17 const DEFAULT_DELIVERY_RETRY_DELAY_MS = 250;
18@@ -37,9 +41,11 @@ interface BaaDeliverySessionRecord {
19
20 export interface BaaBrowserDeliveryBridgeOptions {
21 bridge: BrowserBridgeController;
22+ inlineThreshold?: number | null;
23 lineLimit?: number | null;
24 now?: () => number;
25 onChange?: (() => Promise<void> | void) | null;
26+ summaryLength?: number | null;
27 }
28
29 export interface BaaBrowserDeliveryInput {
30@@ -250,10 +256,100 @@ function buildExecutionSection(
31 return lines;
32 }
33
34+function appendArtifactUrlLine(lines: string[], artifactUrl: string | null): void {
35+ if (artifactUrl == null) {
36+ return;
37+ }
38+
39+ lines.push("");
40+ lines.push(`完整结果:${artifactUrl}`);
41+}
42+
43+function buildExecutionResultText(execution: BaaInstructionExecutionResult): string | null {
44+ const artifactResultText = execution.artifact?.resultText;
45+
46+ if (typeof artifactResultText === "string" && artifactResultText.trim() !== "") {
47+ return artifactResultText;
48+ }
49+ const payload: Record<string, BaaJsonValue> = {
50+ http_status: execution.httpStatus,
51+ ok: execution.ok,
52+ route: {
53+ key: execution.route.key,
54+ method: execution.route.method,
55+ path: execution.route.path
56+ }
57+ };
58+
59+ if (execution.data != null) {
60+ payload.data = execution.data;
61+ }
62+
63+ if (execution.details != null) {
64+ payload.details = execution.details;
65+ }
66+
67+ if (execution.error != null) {
68+ payload.error = execution.error;
69+ }
70+
71+ if (execution.message != null) {
72+ payload.message = execution.message;
73+ }
74+
75+ if (execution.requestId != null) {
76+ payload.request_id = execution.requestId;
77+ }
78+
79+ return JSON.stringify(payload, null, 2);
80+}
81+
82+function buildRenderedExecutionSection(
83+ execution: BaaInstructionExecutionResult,
84+ executionIndex: number,
85+ inlineThreshold: number,
86+ summaryLength: number
87+): {
88+ sourceLines: string[];
89+ truncated: boolean;
90+ visibleLines: string[];
91+} {
92+ const fullResultText = buildExecutionResultText(execution);
93+ const summaryText =
94+ fullResultText != null && fullResultText.length > inlineThreshold
95+ ? fullResultText.slice(0, summaryLength)
96+ : fullResultText;
97+ const truncated = fullResultText != null && summaryText !== fullResultText;
98+ const sourceLines = [
99+ `[执行 ${executionIndex + 1}]`,
100+ `instruction_id: ${execution.instructionId}`,
101+ `tool: ${execution.tool}`,
102+ `target: ${execution.target}`,
103+ `status: ${execution.ok ? "ok" : "error"}`,
104+ `http_status: ${String(execution.httpStatus)}`
105+ ];
106+ const visibleLines = [...sourceLines];
107+ const sourceResultText = fullResultText ?? "(empty)";
108+ const visibleResultText = summaryText ?? "(empty)";
109+
110+ appendSection(sourceLines, "result:", sourceResultText);
111+ appendSection(visibleLines, "result:", visibleResultText);
112+ appendArtifactUrlLine(sourceLines, execution.artifact?.url ?? null);
113+ appendArtifactUrlLine(visibleLines, execution.artifact?.url ?? null);
114+
115+ return {
116+ sourceLines,
117+ truncated,
118+ visibleLines
119+ };
120+}
121+
122 export function renderBaaDeliveryMessageText(
123 input: Pick<BaaBrowserDeliveryInput, "assistantMessageId" | "conversationId" | "platform" | "processResult">,
124 options: {
125+ inlineThreshold?: number | null;
126 lineLimit?: number | null;
127+ summaryLength?: number | null;
128 } = {}
129 ): BaaDeliveryMessageRenderResult {
130 const processResult = input.processResult;
131@@ -263,48 +359,51 @@ export function renderBaaDeliveryMessageText(
132 executionCount: 0,
133 messageCharCount: 0,
134 messageLineCount: 0,
135- messageLineLimit: normalizePositiveInteger(options.lineLimit, DEFAULT_BAA_DELIVERY_LINE_LIMIT),
136+ messageLineLimit: normalizePositiveInteger(options.inlineThreshold, DEFAULT_BAA_DELIVERY_INLINE_THRESHOLD),
137 messageText: "",
138 messageTruncated: false,
139 sourceLineCount: 0
140 };
141 }
142
143- const lineLimit = normalizePositiveInteger(options.lineLimit, DEFAULT_BAA_DELIVERY_LINE_LIMIT);
144- const lines = [
145+ const inlineThreshold = normalizePositiveInteger(options.inlineThreshold, DEFAULT_BAA_DELIVERY_INLINE_THRESHOLD);
146+ const summaryLength = normalizePositiveInteger(options.summaryLength, DEFAULT_BAA_DELIVERY_SUMMARY_LENGTH);
147+ const sourceLines = [
148 "[BAA 执行结果]",
149 `assistant_message_id: ${input.assistantMessageId}`,
150 `platform: ${input.platform}`,
151 `conversation_id: ${input.conversationId ?? "-"}`,
152 `execution_count: ${String(processResult.executions.length)}`
153 ];
154-
155- for (let index = 0; index < processResult.executions.length; index += 1) {
156- lines.push("");
157- lines.push(...buildExecutionSection(processResult, index));
158- }
159-
160- const sourceLines = normalizeDeliveryLines(lines.join("\n"));
161- let visibleLines = sourceLines;
162+ const visibleLines = [...sourceLines];
163 let truncated = false;
164
165- if (sourceLines.length > lineLimit) {
166- visibleLines = [
167- ...sourceLines.slice(0, lineLimit),
168- "超长截断"
169- ];
170- truncated = true;
171+ for (let index = 0; index < processResult.executions.length; index += 1) {
172+ const renderedSection = buildRenderedExecutionSection(
173+ processResult.executions[index]!,
174+ index,
175+ inlineThreshold,
176+ summaryLength
177+ );
178+
179+ sourceLines.push("");
180+ sourceLines.push(...renderedSection.sourceLines);
181+ visibleLines.push("");
182+ visibleLines.push(...renderedSection.visibleLines);
183+ truncated = truncated || renderedSection.truncated;
184 }
185
186- const messageText = visibleLines.join("\n");
187+ const normalizedSourceLines = normalizeDeliveryLines(sourceLines.join("\n"));
188+ const normalizedVisibleLines = normalizeDeliveryLines(visibleLines.join("\n"));
189+ const messageText = normalizedVisibleLines.join("\n");
190 return {
191 executionCount: processResult.executions.length,
192 messageCharCount: messageText.length,
193- messageLineCount: visibleLines.length,
194- messageLineLimit: lineLimit,
195+ messageLineCount: normalizedVisibleLines.length,
196+ messageLineLimit: inlineThreshold,
197 messageText,
198 messageTruncated: truncated,
199- sourceLineCount: sourceLines.length
200+ sourceLineCount: normalizedSourceLines.length
201 };
202 }
203
204@@ -358,18 +457,22 @@ function shouldFailClosedWithoutFallback(reason: string): boolean {
205
206 export class BaaBrowserDeliveryBridge {
207 private readonly bridge: BrowserBridgeController;
208+ private readonly inlineThreshold: number;
209 private lastRoute: BaaDeliveryRouteSnapshot | null = null;
210 private lastSession: BaaDeliverySessionSnapshot | null = null;
211 private readonly lineLimit: number;
212 private readonly now: () => number;
213 private readonly onChange: (() => Promise<void> | void) | null;
214+ private readonly summaryLength: number;
215 private readonly sessions = new Map<string, BaaDeliverySessionRecord>();
216
217 constructor(options: BaaBrowserDeliveryBridgeOptions) {
218 this.bridge = options.bridge;
219+ this.inlineThreshold = normalizePositiveInteger(options.inlineThreshold, DEFAULT_BAA_DELIVERY_INLINE_THRESHOLD);
220 this.lineLimit = normalizePositiveInteger(options.lineLimit, DEFAULT_BAA_DELIVERY_LINE_LIMIT);
221 this.now = options.now ?? (() => Date.now());
222 this.onChange = options.onChange ?? null;
223+ this.summaryLength = normalizePositiveInteger(options.summaryLength, DEFAULT_BAA_DELIVERY_SUMMARY_LENGTH);
224 }
225
226 getSnapshot(): BaaDeliveryBridgeSnapshot {
227@@ -423,7 +526,9 @@ export class BaaBrowserDeliveryBridge {
228 }
229
230 const rendered = renderBaaDeliveryMessageText(input, {
231- lineLimit: this.lineLimit
232+ inlineThreshold: this.inlineThreshold,
233+ lineLimit: this.lineLimit,
234+ summaryLength: this.summaryLength
235 });
236
237 if (rendered.messageText.trim() === "") {
1@@ -45,6 +45,8 @@ type IntervalHandle = ReturnType<typeof globalThis.setInterval>;
2 type FirefoxWsAction = "pause" | "resume" | "drain";
3
4 interface FirefoxWebSocketServerOptions {
5+ artifactInlineThreshold?: number | null;
6+ artifactSummaryLength?: number | null;
7 baseUrlLoader: () => string;
8 instructionIngest?: BaaLiveInstructionIngest | null;
9 now?: () => number;
10@@ -1006,11 +1008,13 @@ export class ConductorFirefoxWebSocketServer {
11 });
12 this.bridgeService = new FirefoxBridgeService(commandBroker);
13 this.deliveryBridge = new BaaBrowserDeliveryBridge({
14+ inlineThreshold: options.artifactInlineThreshold,
15 bridge: this.bridgeService,
16 now: () => this.getNextTimestampMilliseconds(),
17 onChange: () => this.broadcastStateSnapshot("delivery_session", {
18 force: true
19- })
20+ }),
21+ summaryLength: options.artifactSummaryLength
22 });
23 }
24
25@@ -1631,6 +1635,9 @@ export class ConductorFirefoxWebSocketServer {
26 assistantMessageId: finalMessage.assistant_message_id,
27 conversationId: finalMessage.conversation_id,
28 observedAt: finalMessage.observed_at,
29+ organizationId: finalMessage.organization_id,
30+ pageTitle: finalMessage.page_title,
31+ pageUrl: finalMessage.page_url,
32 platform: finalMessage.platform,
33 source: "browser.final_message",
34 text: finalMessage.raw_text
+35,
-2
1@@ -4799,6 +4799,32 @@ test("ConductorRuntime routes browser.final_message into live instruction ingest
2 );
3 assert.equal(readFileSync(join(hostOpsDir, "final-message-ingest.txt"), "utf8"), "ws-live\n");
4
5+ const persistedStore = new ArtifactStore({
6+ artifactDir: join(stateDir, ARTIFACTS_DIRNAME),
7+ databasePath: join(stateDir, ARTIFACT_DB_FILENAME),
8+ publicBaseUrl: "https://control.example.test"
9+ });
10+
11+ try {
12+ const persistedMessage = await persistedStore.getMessage("msg-final-message-ingest");
13+ const persistedExecutions = await persistedStore.listExecutions({
14+ messageId: "msg-final-message-ingest"
15+ });
16+ const persistedSessions = await persistedStore.getLatestSessions(1);
17+
18+ assert.equal(persistedMessage?.rawText, messageText);
19+ assert.equal(existsSync(join(stateDir, ARTIFACTS_DIRNAME, "msg", "msg-final-message-ingest.txt")), true);
20+ assert.equal(persistedExecutions.length, 2);
21+ assert.ok(persistedExecutions.every((execution) =>
22+ existsSync(join(stateDir, ARTIFACTS_DIRNAME, execution.staticPath))
23+ ));
24+ assert.equal(persistedSessions[0]?.messageCount, 1);
25+ assert.equal(persistedSessions[0]?.executionCount, 2);
26+ assert.equal(existsSync(join(stateDir, ARTIFACTS_DIRNAME, "session", "latest.txt")), true);
27+ } finally {
28+ persistedStore.close();
29+ }
30+
31 const browserStatusResponse = await fetch(`${baseUrl}/v1/browser`);
32 assert.equal(browserStatusResponse.status, 200);
33 const browserStatusPayload = await browserStatusResponse.json();
34@@ -5077,8 +5103,15 @@ test("ConductorRuntime exposes proxy-delivery browser snapshots with routed busi
35 (message) => message.type === "browser.proxy_delivery"
36 );
37 assert.match(proxyDelivery.message_text, /\[BAA 执行结果\]/u);
38- assert.match(proxyDelivery.message_text, /line-1/u);
39- assert.match(proxyDelivery.message_text, /超长截断$/u);
40+ assert.equal(
41+ proxyDelivery.message_text.includes("\"command\": \"i=1; while [ $i -le 260"),
42+ true
43+ );
44+ assert.match(
45+ proxyDelivery.message_text,
46+ /完整结果:https:\/\/control\.example\.test\/artifact\/exec\/[^ \n]+\.txt/u
47+ );
48+ assert.doesNotMatch(proxyDelivery.message_text, /line-260/u);
49 assert.equal(proxyDelivery.page_url, "https://chatgpt.com/c/conv-delivery-artifact");
50 assert.equal(proxyDelivery.target_tab_id, 71);
51
+54,
-2
1@@ -9,7 +9,8 @@ import { join } from "node:path";
2 import {
3 ARTIFACTS_DIRNAME,
4 ARTIFACT_DB_FILENAME,
5- ArtifactStore
6+ ArtifactStore,
7+ DEFAULT_SUMMARY_LENGTH
8 } from "../../../packages/artifact-db/dist/index.js";
9 import {
10 DEFAULT_BAA_EXECUTION_JOURNAL_LIMIT,
11@@ -24,6 +25,7 @@ import {
12 ConductorFirefoxWebSocketServer,
13 buildFirefoxWebSocketUrl
14 } from "./firefox-ws.js";
15+import { DEFAULT_BAA_DELIVERY_INLINE_THRESHOLD } from "./artifacts/upload-session.js";
16 import {
17 BrowserRequestPolicyController,
18 type BrowserRequestPolicyControllerOptions
19@@ -154,6 +156,8 @@ export interface ConductorRuntimePaths {
20 }
21
22 export interface ConductorRuntimeConfig extends ConductorConfig {
23+ artifactInlineThreshold?: number | null;
24+ artifactSummaryLength?: number | null;
25 codexdLocalApiBase?: string | null;
26 localApiAllowedHosts?: readonly string[] | string | null;
27 localApiBase?: string | null;
28@@ -163,6 +167,8 @@ export interface ConductorRuntimeConfig extends ConductorConfig {
29
30 export interface ResolvedConductorRuntimeConfig
31 extends Omit<ConductorConfig, "controlApiBase" | "publicApiBase"> {
32+ artifactInlineThreshold: number;
33+ artifactSummaryLength: number;
34 controlApiBase: string;
35 heartbeatIntervalMs: number;
36 leaseRenewIntervalMs: number;
37@@ -346,6 +352,8 @@ interface LocalApiListenConfig {
38 }
39
40 interface CliValueOverrides {
41+ artifactInlineThreshold?: string;
42+ artifactSummaryLength?: string;
43 codexdLocalApiBase?: string;
44 controlApiBase?: string;
45 heartbeatIntervalMs?: string;
46@@ -724,6 +732,8 @@ class ConductorLocalHttpServer {
47 sharedToken: string | null,
48 version: string | null,
49 now: () => number,
50+ artifactInlineThreshold: number,
51+ artifactSummaryLength: number,
52 browserRequestPolicyOptions: BrowserRequestPolicyControllerOptions = {}
53 ) {
54 this.artifactStore = artifactStore;
55@@ -739,6 +749,7 @@ class ConductorLocalHttpServer {
56 this.resolvedBaseUrl = localApiBase;
57 const nowMs = () => this.now() * 1000;
58 const localApiContext = {
59+ artifactStore: this.artifactStore,
60 fetchImpl: this.fetchImpl,
61 now: this.now,
62 repository: this.repository,
63@@ -764,6 +775,8 @@ class ConductorLocalHttpServer {
64 });
65 this.instructionIngest = instructionIngest;
66 this.firefoxWebSocketServer = new ConductorFirefoxWebSocketServer({
67+ artifactInlineThreshold,
68+ artifactSummaryLength,
69 baseUrlLoader: () => this.resolvedBaseUrl,
70 instructionIngest,
71 now: this.now,
72@@ -1621,6 +1634,8 @@ export function resolveConductorRuntimeConfig(
73 const localApiAllowedHosts = parseLocalApiAllowedHosts(config.localApiAllowedHosts);
74 const leaseTtlSec = config.leaseTtlSec ?? DEFAULT_LEASE_TTL_SEC;
75 const renewFailureThreshold = config.renewFailureThreshold ?? DEFAULT_RENEW_FAILURE_THRESHOLD;
76+ const artifactInlineThreshold = config.artifactInlineThreshold ?? DEFAULT_BAA_DELIVERY_INLINE_THRESHOLD;
77+ const artifactSummaryLength = config.artifactSummaryLength ?? DEFAULT_SUMMARY_LENGTH;
78 const priority = config.priority ?? (config.role === "primary" ? 100 : 50);
79
80 if (heartbeatIntervalMs <= 0) {
81@@ -1639,8 +1654,18 @@ export function resolveConductorRuntimeConfig(
82 throw new Error("Conductor renewFailureThreshold must be > 0.");
83 }
84
85+ if (!Number.isInteger(artifactInlineThreshold) || artifactInlineThreshold <= 0) {
86+ throw new Error("Conductor artifactInlineThreshold must be a positive integer.");
87+ }
88+
89+ if (!Number.isInteger(artifactSummaryLength) || artifactSummaryLength <= 0) {
90+ throw new Error("Conductor artifactSummaryLength must be a positive integer.");
91+ }
92+
93 return {
94 ...config,
95+ artifactInlineThreshold,
96+ artifactSummaryLength,
97 nodeId,
98 host,
99 role: parseConductorRole("Conductor role", config.role),
100@@ -1700,6 +1725,16 @@ function resolveRuntimeConfigFromSources(
101 role,
102 publicApiBase,
103 controlApiBase: publicApiBase,
104+ artifactInlineThreshold: parseIntegerValue(
105+ "Conductor artifact inline threshold",
106+ overrides.artifactInlineThreshold ?? env.BAA_ARTIFACT_INLINE_THRESHOLD,
107+ { minimum: 1 }
108+ ),
109+ artifactSummaryLength: parseIntegerValue(
110+ "Conductor artifact summary length",
111+ overrides.artifactSummaryLength ?? env.BAA_ARTIFACT_SUMMARY_LENGTH,
112+ { minimum: 1 }
113+ ),
114 priority: parseIntegerValue("Conductor priority", overrides.priority ?? env.BAA_CONDUCTOR_PRIORITY, {
115 minimum: 0
116 }),
117@@ -1819,6 +1854,14 @@ export function parseConductorCliRequest(
118 overrides.sharedToken = readOptionValue(tokens, token, index);
119 index += 1;
120 break;
121+ case "--artifact-inline-threshold":
122+ overrides.artifactInlineThreshold = readOptionValue(tokens, token, index);
123+ index += 1;
124+ break;
125+ case "--artifact-summary-length":
126+ overrides.artifactSummaryLength = readOptionValue(tokens, token, index);
127+ index += 1;
128+ break;
129 case "--priority":
130 overrides.priority = readOptionValue(tokens, token, index);
131 index += 1;
132@@ -1952,6 +1995,8 @@ function formatConfigText(config: ResolvedConductorRuntimeConfig): string {
133 `local_api_allowed_hosts: ${config.localApiAllowedHosts.join(",") || "loopback-only"}`,
134 `priority: ${config.priority}`,
135 `preferred: ${String(config.preferred)}`,
136+ `artifact_inline_threshold: ${config.artifactInlineThreshold}`,
137+ `artifact_summary_length: ${config.artifactSummaryLength}`,
138 `heartbeat_interval_ms: ${config.heartbeatIntervalMs}`,
139 `lease_renew_interval_ms: ${config.leaseRenewIntervalMs}`,
140 `lease_ttl_sec: ${config.leaseTtlSec}`,
141@@ -1995,6 +2040,8 @@ function getUsageText(): string {
142 " --codexd-local-api <url>",
143 " --local-api <url>",
144 " --shared-token <token>",
145+ " --artifact-inline-threshold <integer>",
146+ " --artifact-summary-length <integer>",
147 " --priority <integer>",
148 " --version <string>",
149 " --preferred | --no-preferred",
150@@ -2021,6 +2068,8 @@ function getUsageText(): string {
151 " BAA_CONDUCTOR_LOCAL_API",
152 " BAA_CONDUCTOR_LOCAL_API_ALLOWED_HOSTS",
153 " BAA_SHARED_TOKEN",
154+ " BAA_ARTIFACT_INLINE_THRESHOLD",
155+ " BAA_ARTIFACT_SUMMARY_LENGTH",
156 " BAA_CONDUCTOR_PRIORITY",
157 " BAA_CONDUCTOR_VERSION",
158 " BAA_CONDUCTOR_PREFERRED",
159@@ -2065,7 +2114,8 @@ export class ConductorRuntime {
160 this.artifactStore = new ArtifactStore({
161 artifactDir: join(resolvedStateDir, ARTIFACTS_DIRNAME),
162 databasePath: join(resolvedStateDir, ARTIFACT_DB_FILENAME),
163- publicBaseUrl: this.config.publicApiBase
164+ publicBaseUrl: this.config.publicApiBase,
165+ summaryLength: this.config.artifactSummaryLength
166 });
167 this.daemon = new ConductorDaemon(this.config, {
168 ...options,
169@@ -2087,6 +2137,8 @@ export class ConductorRuntime {
170 this.config.sharedToken,
171 this.config.version,
172 this.now,
173+ this.config.artifactInlineThreshold,
174+ this.config.artifactSummaryLength,
175 options.browserRequestPolicyOptions
176 );
177 }
1@@ -1,3 +1,4 @@
2+import { buildArtifactPublicUrl } from "../../../../packages/artifact-db/dist/index.js";
3 import {
4 handleConductorHttpRequest,
5 type ConductorLocalApiContext
6@@ -6,6 +7,7 @@ import type {
7 BaaInstructionEnvelope,
8 BaaInstructionExecutionResult,
9 BaaInstructionRoute,
10+ BaaJsonObject,
11 BaaJsonValue
12 } from "./types.js";
13 import { isBaaJsonValue } from "./types.js";
14@@ -18,6 +20,7 @@ function toExecutionFailure(
15 details: BaaJsonValue | null = null
16 ): BaaInstructionExecutionResult {
17 return {
18+ artifact: null,
19 data: null,
20 dedupeKey: instruction.dedupeKey,
21 details,
22@@ -41,6 +44,125 @@ function normalizeJsonBodyValue(value: unknown): BaaJsonValue | null {
23 return isBaaJsonValue(value) ? value : null;
24 }
25
26+function normalizeOptionalString(value: string | null | undefined): string | null {
27+ if (typeof value !== "string") {
28+ return null;
29+ }
30+
31+ const normalized = value.trim();
32+ return normalized === "" ? null : normalized;
33+}
34+
35+function stringifyArtifactValue(value: BaaJsonValue | null): string | null {
36+ if (value == null) {
37+ return null;
38+ }
39+
40+ if (typeof value === "string") {
41+ return value.trim() === "" ? null : value;
42+ }
43+
44+ return JSON.stringify(value, null, 2);
45+}
46+
47+function buildExecutionArtifactPayload(result: BaaInstructionExecutionResult): BaaJsonObject {
48+ const payload: BaaJsonObject = {
49+ http_status: result.httpStatus,
50+ ok: result.ok,
51+ route: {
52+ key: result.route.key,
53+ method: result.route.method,
54+ path: result.route.path
55+ }
56+ };
57+
58+ if (result.data != null) {
59+ payload.data = result.data;
60+ }
61+
62+ if (result.details != null) {
63+ payload.details = result.details;
64+ }
65+
66+ if (result.error != null) {
67+ payload.error = result.error;
68+ }
69+
70+ if (result.message != null) {
71+ payload.message = result.message;
72+ }
73+
74+ if (result.requestId != null) {
75+ payload.request_id = result.requestId;
76+ }
77+
78+ return payload;
79+}
80+
81+function buildExecutionArtifactError(result: BaaInstructionExecutionResult): string | null {
82+ const values = [normalizeOptionalString(result.message), normalizeOptionalString(result.error)]
83+ .filter((value, index, all): value is string => value != null && all.indexOf(value) === index);
84+
85+ return values.length === 0 ? null : values.join("\n");
86+}
87+
88+function logArtifactPersistenceFailure(instructionId: string, error: unknown): void {
89+ const message = error instanceof Error ? error.stack ?? error.message : String(error);
90+ console.error(`[artifact] failed to persist execution ${instructionId}: ${message}`);
91+}
92+
93+async function withExecutionArtifact(
94+ result: BaaInstructionExecutionResult,
95+ instruction: BaaInstructionEnvelope,
96+ context: ConductorLocalApiContext
97+): Promise<BaaInstructionExecutionResult> {
98+ const resultData = buildExecutionArtifactPayload(result);
99+ const resultError = buildExecutionArtifactError(result);
100+ const fallbackArtifact = {
101+ resultText: stringifyArtifactValue(resultData) ?? resultError,
102+ url: null
103+ };
104+ const artifactStore = context.artifactStore ?? null;
105+
106+ if (artifactStore == null) {
107+ return {
108+ ...result,
109+ artifact: fallbackArtifact
110+ };
111+ }
112+
113+ try {
114+ const record = await artifactStore.insertExecution({
115+ executedAt: Date.now(),
116+ httpStatus: result.httpStatus,
117+ instructionId: result.instructionId,
118+ messageId: instruction.assistantMessageId,
119+ params: instruction.params,
120+ paramsKind: instruction.paramsKind,
121+ resultData,
122+ resultError,
123+ resultOk: result.ok,
124+ target: instruction.target,
125+ tool: instruction.tool
126+ });
127+
128+ return {
129+ ...result,
130+ artifact: {
131+ resultText: record.resultData ?? record.resultError,
132+ url: buildArtifactPublicUrl(artifactStore.getPublicBaseUrl(), record.staticPath)
133+ }
134+ };
135+ } catch (error) {
136+ logArtifactPersistenceFailure(result.instructionId, error);
137+
138+ return {
139+ ...result,
140+ artifact: fallbackArtifact
141+ };
142+ }
143+}
144+
145 export async function executeBaaInstruction(
146 instruction: BaaInstructionEnvelope,
147 route: BaaInstructionRoute,
148@@ -75,12 +197,16 @@ export async function executeBaaInstruction(
149 parsedBody = responseText.trim() === "" ? null : JSON.parse(responseText);
150 } catch (error) {
151 const message = error instanceof Error ? error.message : String(error);
152- return toExecutionFailure(
153+ return withExecutionArtifact(
154+ toExecutionFailure(
155+ instruction,
156+ route,
157+ `Failed to parse local API response JSON: ${message}`,
158+ "invalid_local_api_response",
159+ normalizeJsonBodyValue(responseText)
160+ ),
161 instruction,
162- route,
163- `Failed to parse local API response JSON: ${message}`,
164- "invalid_local_api_response",
165- normalizeJsonBodyValue(responseText)
166+ context
167 );
168 }
169
170@@ -89,7 +215,8 @@ export async function executeBaaInstruction(
171 ? (parsedBody as Record<string, unknown>)
172 : null;
173
174- return {
175+ return withExecutionArtifact({
176+ artifact: null,
177 data: payload?.ok === true ? normalizeJsonBodyValue(payload.data) : null,
178 dedupeKey: instruction.dedupeKey,
179 details: normalizeJsonBodyValue(payload?.details),
180@@ -106,9 +233,9 @@ export async function executeBaaInstruction(
181 },
182 target: instruction.target,
183 tool: instruction.tool
184- };
185+ }, instruction, context);
186 } catch (error) {
187 const message = error instanceof Error ? error.message : String(error);
188- return toExecutionFailure(instruction, route, message);
189+ return withExecutionArtifact(toExecutionFailure(instruction, route, message), instruction, context);
190 }
191 }
1@@ -1,4 +1,5 @@
2 import { createHash } from "node:crypto";
3+import type { ArtifactStore } from "../../../../packages/artifact-db/dist/index.js";
4
5 import type { ConductorLocalApiContext } from "../local-api.js";
6
7@@ -23,6 +24,9 @@ export interface BaaLiveInstructionIngestInput {
8 assistantMessageId: string;
9 conversationId?: string | null;
10 observedAt?: number | null;
11+ organizationId?: string | null;
12+ pageTitle?: string | null;
13+ pageUrl?: string | null;
14 platform: string;
15 source: "browser.final_message";
16 text: string;
17@@ -172,6 +176,13 @@ function normalizeOptionalString(value: string | null | undefined): string | nul
18 return normalized === "" ? null : normalized;
19 }
20
21+function isDuplicateArtifactMessageError(error: unknown): boolean {
22+ return (
23+ error instanceof Error
24+ && error.message.includes("UNIQUE constraint failed: messages.id")
25+ );
26+}
27+
28 function cloneSummary(summary: BaaLiveInstructionIngestSummary): BaaLiveInstructionIngestSummary {
29 return {
30 ...summary,
31@@ -214,6 +225,7 @@ export function buildBaaLiveInstructionMessageDedupeKey(input: {
32 }
33
34 export class BaaLiveInstructionIngest {
35+ private readonly artifactStore: ArtifactStore | null;
36 private readonly center: BaaInstructionCenter;
37 private readonly historyLimit: number;
38 private readonly messageDeduper: BaaLiveInstructionMessageDeduper;
39@@ -236,6 +248,7 @@ export class BaaLiveInstructionIngest {
40 ?? new BaaInstructionCenter({
41 localApiContext: options.localApiContext as BaaInstructionCenterOptions["localApiContext"]
42 });
43+ this.artifactStore = options.localApiContext?.artifactStore ?? null;
44 this.historyLimit = normalizeHistoryLimit(options.historyLimit);
45 this.messageDeduper = options.messageDeduper ?? new InMemoryBaaLiveInstructionMessageDeduper();
46 this.now = options.now ?? Date.now;
47@@ -267,6 +280,7 @@ export class BaaLiveInstructionIngest {
48 input: BaaLiveInstructionIngestInput
49 ): Promise<BaaLiveInstructionIngestResult> {
50 await this.initialize();
51+ await this.persistMessageArtifact(input);
52
53 const messageDedupeKey = buildBaaLiveInstructionMessageDedupeKey({
54 assistantMessageId: input.assistantMessageId,
55@@ -348,6 +362,36 @@ export class BaaLiveInstructionIngest {
56 }
57 }
58
59+ private async persistMessageArtifact(input: BaaLiveInstructionIngestInput): Promise<void> {
60+ if (this.artifactStore == null) {
61+ return;
62+ }
63+
64+ try {
65+ await this.artifactStore.insertMessage({
66+ conversationId: normalizeOptionalString(input.conversationId),
67+ id: input.assistantMessageId,
68+ observedAt:
69+ typeof input.observedAt === "number" && Number.isFinite(input.observedAt)
70+ ? input.observedAt
71+ : this.now(),
72+ organizationId: normalizeOptionalString(input.organizationId),
73+ pageTitle: normalizeOptionalString(input.pageTitle),
74+ pageUrl: normalizeOptionalString(input.pageUrl),
75+ platform: input.platform,
76+ rawText: input.text,
77+ role: "assistant"
78+ });
79+ } catch (error) {
80+ if (isDuplicateArtifactMessageError(error)) {
81+ return;
82+ }
83+
84+ const message = error instanceof Error ? error.stack ?? error.message : String(error);
85+ console.error(`[artifact] failed to persist message ${input.assistantMessageId}: ${message}`);
86+ }
87+ }
88+
89 private async loadPersistedSnapshot(): Promise<void> {
90 if (this.snapshotStore == null) {
91 return;
1@@ -78,6 +78,10 @@ export interface BaaInstructionDeniedResult {
2 }
3
4 export interface BaaInstructionExecutionResult {
5+ artifact: {
6+ resultText: string | null;
7+ url: string | null;
8+ } | null;
9 data: BaaJsonValue | null;
10 dedupeKey: string;
11 details: BaaJsonValue | null;
+13,
-17
1@@ -54,24 +54,21 @@ test("ArtifactStore writes message, execution, session, and index artifacts sync
2 target: "conductor",
3 tool: "exec"
4 });
5- const session = await store.upsertSession({
6- conversationId: "conv_123",
7- executionCount: 1,
8- id: "session_123",
9- lastActivityAt: Date.UTC(2026, 2, 28, 8, 10, 0),
10- messageCount: 1,
11- platform: "claude",
12- startedAt: Date.UTC(2026, 2, 28, 8, 9, 0),
13- summary: "会话摘要"
14- });
15+ const [session] = await store.getLatestSessions(1);
16+
17+ assert.ok(session);
18+ assert.equal(session.conversationId, "conv_123");
19+ assert.equal(session.executionCount, 1);
20+ assert.equal(session.messageCount, 1);
21+ assert.equal(session.platform, "claude");
22
23 assert.equal(existsSync(databasePath), true);
24 assert.equal(existsSync(join(artifactDir, "msg", "msg_123.txt")), true);
25 assert.equal(existsSync(join(artifactDir, "msg", "msg_123.json")), true);
26 assert.equal(existsSync(join(artifactDir, "exec", "inst_123.txt")), true);
27 assert.equal(existsSync(join(artifactDir, "exec", "inst_123.json")), true);
28- assert.equal(existsSync(join(artifactDir, "session", "session_123.txt")), true);
29- assert.equal(existsSync(join(artifactDir, "session", "session_123.json")), true);
30+ assert.equal(existsSync(join(artifactDir, session.staticPath)), true);
31+ assert.equal(existsSync(join(artifactDir, session.staticPath.replace(/\.txt$/u, ".json"))), true);
32 assert.equal(existsSync(join(artifactDir, "session", "latest.txt")), true);
33 assert.equal(existsSync(join(artifactDir, "session", "latest.json")), true);
34
35@@ -83,17 +80,16 @@ test("ArtifactStore writes message, execution, session, and index artifacts sync
36 );
37 assert.match(readFileSync(join(artifactDir, "exec", "inst_123.txt"), "utf8"), /params:/u);
38 assert.match(readFileSync(join(artifactDir, "exec", "inst_123.txt"), "utf8"), /pnpm test/u);
39- assert.match(readFileSync(join(artifactDir, "session", "session_123.txt"), "utf8"), /### message msg_123/u);
40- assert.match(readFileSync(join(artifactDir, "session", "session_123.txt"), "utf8"), /### execution inst_123/u);
41- assert.match(readFileSync(join(artifactDir, "session", "latest.txt"), "utf8"), /session_123/u);
42+ assert.match(readFileSync(join(artifactDir, session.staticPath), "utf8"), /### message msg_123/u);
43+ assert.match(readFileSync(join(artifactDir, session.staticPath), "utf8"), /### execution inst_123/u);
44+ assert.match(readFileSync(join(artifactDir, "session", "latest.txt"), "utf8"), new RegExp(session.id, "u"));
45 assert.match(
46 readFileSync(join(artifactDir, "session", "latest.json"), "utf8"),
47- /https:\/\/conductor\.makefile\.so\/artifact\/session\/session_123\.txt/u
48+ new RegExp(`https://conductor\\.makefile\\.so/artifact/session/${session.id}\\.txt`, "u")
49 );
50
51 assert.deepEqual(await store.getMessage(message.id), message);
52 assert.deepEqual(await store.getExecution(execution.instructionId), execution);
53- assert.deepEqual(await store.getLatestSessions(1), [session]);
54 assert.deepEqual(await store.listMessages({ conversationId: "conv_123" }), [message]);
55 assert.deepEqual(await store.listExecutions({ messageId: message.id }), [execution]);
56 assert.deepEqual(await store.listSessions({ platform: "claude" }), [session]);
+148,
-5
1@@ -173,6 +173,10 @@ interface LatestMessageRow {
2 static_path: string;
3 }
4
5+interface CountRow {
6+ count: number;
7+}
8+
9 export class ArtifactStore {
10 private readonly artifactDir: string;
11 private readonly db: DatabaseSync;
12@@ -216,6 +220,10 @@ export class ArtifactStore {
13 return this.artifactDir;
14 }
15
16+ getPublicBaseUrl(): string | null {
17+ return this.publicBaseUrl;
18+ }
19+
20 async getExecution(instructionId: string): Promise<ExecutionRecord | null> {
21 const row = this.getRow<ExecutionRow>(
22 "SELECT * FROM executions WHERE instruction_id = ? LIMIT 1;",
23@@ -256,6 +264,17 @@ export class ArtifactStore {
24 buildExecutionArtifactFiles(record, this.renderConfig(), message.staticPath),
25 mutations
26 );
27+ this.writeSessionArtifacts(
28+ this.buildDerivedSessionRecord({
29+ conversationId: message.conversationId,
30+ createdAtCandidate: message.createdAt,
31+ lastActivityAt: record.executedAt,
32+ platform: message.platform,
33+ startedAtCandidate: message.observedAt,
34+ summaryCandidate: record.resultSummary ?? message.summary
35+ }),
36+ mutations
37+ );
38 this.writeArtifactFiles(
39 buildSessionIndexArtifactFiles(this.readSessionIndexEntries(this.sessionIndexLimit), Date.now(), this.renderConfig()),
40 mutations
41@@ -271,6 +290,17 @@ export class ArtifactStore {
42 this.executeWrite((mutations) => {
43 this.run(INSERT_MESSAGE_SQL, messageParams(record));
44 this.writeArtifactFiles(buildMessageArtifactFiles(record, this.renderConfig()), mutations);
45+ this.writeSessionArtifacts(
46+ this.buildDerivedSessionRecord({
47+ conversationId: record.conversationId,
48+ createdAtCandidate: record.createdAt,
49+ lastActivityAt: record.observedAt,
50+ platform: record.platform,
51+ startedAtCandidate: record.observedAt,
52+ summaryCandidate: record.summary
53+ }),
54+ mutations
55+ );
56 this.writeArtifactFiles(
57 buildSessionIndexArtifactFiles(this.readSessionIndexEntries(this.sessionIndexLimit), Date.now(), this.renderConfig()),
58 mutations
59@@ -354,11 +384,7 @@ export class ArtifactStore {
60 const record = buildSessionRecord(input, this.summaryLength);
61
62 this.executeWrite((mutations) => {
63- this.run(UPSERT_SESSION_SQL, sessionParams(record));
64- this.writeArtifactFiles(
65- buildSessionArtifactFiles(record, this.readSessionTimeline(record), this.renderConfig()),
66- mutations
67- );
68+ this.writeSessionArtifacts(record, mutations);
69 this.writeArtifactFiles(
70 buildSessionIndexArtifactFiles(this.readSessionIndexEntries(this.sessionIndexLimit), Date.now(), this.renderConfig()),
71 mutations
72@@ -408,6 +434,81 @@ export class ArtifactStore {
73 );
74 }
75
76+ private buildDerivedSessionRecord(input: {
77+ conversationId: string | null;
78+ createdAtCandidate: number;
79+ lastActivityAt: number;
80+ platform: string;
81+ startedAtCandidate: number;
82+ summaryCandidate: string | null;
83+ }): SessionRecord {
84+ const id = buildSessionId(input.platform, input.conversationId);
85+ const existing = this.getRow<SessionRow>("SELECT * FROM sessions WHERE id = ? LIMIT 1;", id);
86+ const existingRecord = existing == null ? null : mapSessionRow(existing);
87+ const messageCount = this.countMessages(input.platform, input.conversationId);
88+ const executionCount = this.countExecutions(input.platform, input.conversationId);
89+
90+ return buildSessionRecord(
91+ {
92+ conversationId: input.conversationId,
93+ createdAt: existingRecord?.createdAt ?? input.createdAtCandidate,
94+ executionCount,
95+ id,
96+ lastActivityAt:
97+ existingRecord == null
98+ ? input.lastActivityAt
99+ : Math.max(existingRecord.lastActivityAt, input.lastActivityAt),
100+ messageCount,
101+ platform: input.platform,
102+ startedAt:
103+ existingRecord == null
104+ ? input.startedAtCandidate
105+ : Math.min(existingRecord.startedAt, input.startedAtCandidate),
106+ summary: resolveSessionSummary(existingRecord, input.summaryCandidate, input.lastActivityAt)
107+ },
108+ this.summaryLength
109+ );
110+ }
111+
112+ private countExecutions(platform: string, conversationId: string | null): number {
113+ const { clause, params } = buildConversationFilters(platform, conversationId);
114+ const executionClause =
115+ clause === ""
116+ ? ""
117+ : `WHERE ${clause
118+ .replaceAll("platform", "m.platform")
119+ .replaceAll("conversation_id", "m.conversation_id")}`;
120+ const row = this.getRow<CountRow>(
121+ [
122+ "SELECT COUNT(*) AS count",
123+ "FROM executions e",
124+ "INNER JOIN messages m ON m.id = e.message_id",
125+ executionClause
126+ ]
127+ .filter(Boolean)
128+ .join(" "),
129+ ...params
130+ );
131+
132+ return row?.count ?? 0;
133+ }
134+
135+ private countMessages(platform: string, conversationId: string | null): number {
136+ const { clause, params } = buildConversationFilters(platform, conversationId);
137+ const row = this.getRow<CountRow>(
138+ [
139+ "SELECT COUNT(*) AS count",
140+ "FROM messages",
141+ clause === "" ? "" : `WHERE ${clause}`
142+ ]
143+ .filter(Boolean)
144+ .join(" "),
145+ ...params
146+ );
147+
148+ return row?.count ?? 0;
149+ }
150+
151 private readSessionIndexEntries(limit: number): SessionIndexEntry[] {
152 const sessions = this.getRows<SessionRow>(
153 `
154@@ -500,6 +601,14 @@ export class ArtifactStore {
155 };
156 }
157
158+ private writeSessionArtifacts(record: SessionRecord, mutations: FileMutation[]): void {
159+ this.run(UPSERT_SESSION_SQL, sessionParams(record));
160+ this.writeArtifactFiles(
161+ buildSessionArtifactFiles(record, this.readSessionTimeline(record), this.renderConfig()),
162+ mutations
163+ );
164+ }
165+
166 private restoreFiles(mutations: FileMutation[]): void {
167 for (let index = mutations.length - 1; index >= 0; index -= 1) {
168 const mutation = mutations[index];
169@@ -838,3 +947,37 @@ function summarizeText(value: string, summaryLength: number): string | null {
170
171 return normalized.slice(0, summaryLength);
172 }
173+
174+function buildSessionId(platform: string, conversationId: string | null): string {
175+ const key = `${normalizeRequiredString(platform, "platform")}\u0000${conversationId ?? ""}`;
176+ return `session_${buildStableHash(key)}${buildStableHash(`${key}\u0001session`)}`;
177+}
178+
179+function resolveSessionSummary(
180+ existing: SessionRecord | null,
181+ summaryCandidate: string | null,
182+ activityAt: number
183+): string | null {
184+ const normalizedCandidate = normalizeOptionalString(summaryCandidate);
185+
186+ if (existing == null) {
187+ return normalizedCandidate;
188+ }
189+
190+ if (activityAt >= existing.lastActivityAt) {
191+ return normalizedCandidate ?? existing.summary;
192+ }
193+
194+ return existing.summary;
195+}
196+
197+function buildStableHash(value: string): string {
198+ let hash = 2_166_136_261;
199+
200+ for (let index = 0; index < value.length; index += 1) {
201+ hash ^= value.charCodeAt(index);
202+ hash = Math.imul(hash, 16_777_619);
203+ }
204+
205+ return (hash >>> 0).toString(16).padStart(8, "0");
206+}
+20,
-3
1@@ -145,17 +145,33 @@ T-S039 建好了数据库和静态文件生成能力,但还没有接入实际
2
3 ### 开始执行
4
5-- 执行者:
6-- 开始时间:
7+- 执行者:`Codex`
8+- 开始时间:`2026-03-28 17:35:00 +0800`
9 - 状态变更:`待开始` → `进行中`
10
11 ### 完成摘要
12
13-- 完成时间:
14+- 完成时间:`2026-03-28 18:27:35 +0800`
15 - 状态变更:`进行中` → `已完成`
16 - 修改了哪些文件:
17+ - `packages/artifact-db/src/store.ts`
18+ - `packages/artifact-db/src/index.test.js`
19+ - `apps/conductor-daemon/src/instructions/ingest.ts`
20+ - `apps/conductor-daemon/src/instructions/executor.ts`
21+ - `apps/conductor-daemon/src/instructions/types.ts`
22+ - `apps/conductor-daemon/src/artifacts/upload-session.ts`
23+ - `apps/conductor-daemon/src/firefox-ws.ts`
24+ - `apps/conductor-daemon/src/index.ts`
25+ - `apps/conductor-daemon/src/artifacts.test.js`
26+ - `apps/conductor-daemon/src/index.test.js`
27+ - `tasks/T-S040.md`
28 - 核心实现思路:
29+ - 在 `ArtifactStore` 内部把 `insertMessage()` / `insertExecution()` 扩展为自动维护 session 记录、timeline 静态文件和 `session/latest.txt`,避免上层链路重复拼 session 逻辑。
30+ - `browser.final_message` 进入 ingest 时先 best-effort 写入 messages 表与静态文件,失败只记录日志;指令执行结束后在 executor 中写入 executions 表,并把 artifact URL / 完整结果文本挂回执行结果。
31+ - delivery 渲染从“按行截断”改为“按字符阈值 + exact URL”:短结果内联全文并附 URL,长结果截断前 `summaryLength` 字符并附 `完整结果:{url}`;阈值通过 conductor config / 环境变量注入。
32 - 跑了哪些测试:
33+ - `pnpm -C /Users/george/code/baa-conductor-artifact-pipeline -F @baa-conductor/artifact-db test`
34+ - `pnpm -C /Users/george/code/baa-conductor-artifact-pipeline -F @baa-conductor/conductor-daemon test`
35
36 ### 执行过程中遇到的问题
37
38@@ -163,3 +179,4 @@ T-S039 建好了数据库和静态文件生成能力,但还没有接入实际
39
40 ### 剩余风险
41
42+- artifact URL 依赖 `publicApiBase` / `BAA_CONDUCTOR_PUBLIC_API_BASE` 配置正确;如果公网域名或反代未同步,AI 能拿到 URL,但外部访问仍会失败。