@musnows/scriverse 0.4.12 → 0.5.1

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/dist/ai.js CHANGED
@@ -33,6 +33,119 @@ function thinkingParameters(provider, model) {
33
33
  return { thinking: { type: boolValue(model, "thinking_enabled") ? "enabled" : "disabled" } };
34
34
  }
35
35
  const AGENT_TOOL_IDS = ["story_index", "read_chapters", "grep", "search_story_entities", "read_character_sections"];
36
+ const TASK_TRACE_PREVIEW_CHARACTER_LIMIT = 3_000;
37
+ function traceRecord(value) {
38
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
39
+ }
40
+ function previewTaskTraceMessages(messages) {
41
+ const records = messages.map(traceRecord);
42
+ const contents = records.map((message) => message.content === null ? "" : String(message.content ?? ""));
43
+ const allocations = contents.map(() => 0);
44
+ let remaining = TASK_TRACE_PREVIEW_CHARACTER_LIMIT;
45
+ let active = contents.map((_, index) => index).filter((index) => contents[index].length > 0);
46
+ while (remaining > 0 && active.length > 0) {
47
+ const share = Math.max(1, Math.floor(remaining / active.length));
48
+ const next = [];
49
+ for (const index of active) {
50
+ if (remaining <= 0)
51
+ break;
52
+ const available = contents[index].length - allocations[index];
53
+ const granted = Math.min(available, share, remaining);
54
+ allocations[index] = allocations[index] + granted;
55
+ remaining -= granted;
56
+ if (allocations[index] < contents[index].length)
57
+ next.push(index);
58
+ }
59
+ active = next;
60
+ }
61
+ return {
62
+ messages: records.map((message, index) => {
63
+ const content = contents[index];
64
+ const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
65
+ return {
66
+ role: typeof message.role === "string" ? message.role : "user",
67
+ content: content.slice(0, allocations[index]),
68
+ contentChars: content.length,
69
+ contentTruncated: allocations[index] < content.length,
70
+ toolCallCount: toolCalls.length,
71
+ ...(typeof message.tool_call_id === "string" ? { tool_call_id: message.tool_call_id } : {})
72
+ };
73
+ }),
74
+ totalChars: contents.reduce((total, content) => total + content.length, 0),
75
+ truncated: contents.some((content, index) => allocations[index] < content.length)
76
+ };
77
+ }
78
+ function summarizeTaskTraceRound(value) {
79
+ const round = traceRecord(value);
80
+ const request = traceRecord(round.request);
81
+ const messages = Array.isArray(request.messages) ? request.messages : [];
82
+ const attempts = Array.isArray(round.attempts) ? round.attempts : [];
83
+ const toolExecutions = Array.isArray(round.toolExecutions) ? round.toolExecutions : [];
84
+ return {
85
+ round: typeof round.round === "number" ? round.round : 1,
86
+ requestedAt: typeof round.requestedAt === "string" ? round.requestedAt : "",
87
+ messageCount: messages.length,
88
+ promptChars: messages.reduce((total, message) => {
89
+ const content = traceRecord(message).content;
90
+ return total + (content === null ? 0 : String(content ?? "").length);
91
+ }, 0),
92
+ attemptCount: attempts.length,
93
+ toolExecutionCount: toolExecutions.length
94
+ };
95
+ }
96
+ function redactProviderSecret(value, apiKey) {
97
+ if (!apiKey)
98
+ return value;
99
+ return value.split(apiKey).join("[REDACTED]");
100
+ }
101
+ function redactProviderSecrets(value, apiKey, depth = 0) {
102
+ if (typeof value === "string")
103
+ return redactProviderSecret(value, apiKey);
104
+ if (value === null || typeof value === "number" || typeof value === "boolean")
105
+ return value;
106
+ if (depth >= 32)
107
+ return "[REDACTED_DEPTH_LIMIT]";
108
+ if (Array.isArray(value))
109
+ return value.map((item) => redactProviderSecrets(item, apiKey, depth + 1));
110
+ if (!value || typeof value !== "object")
111
+ return null;
112
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, redactProviderSecrets(item, apiKey, depth + 1)]));
113
+ }
114
+ function sanitizeCompletionTraceResponse(value) {
115
+ const response = value && typeof value === "object" && !Array.isArray(value) ? value : {};
116
+ const choices = Array.isArray(response.choices) ? response.choices : [];
117
+ return {
118
+ choices: choices.map((choice) => {
119
+ const choiceRecord = choice && typeof choice === "object" && !Array.isArray(choice) ? choice : {};
120
+ const message = choiceRecord.message && typeof choiceRecord.message === "object" && !Array.isArray(choiceRecord.message)
121
+ ? choiceRecord.message
122
+ : {};
123
+ const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
124
+ return {
125
+ finish_reason: typeof choiceRecord.finish_reason === "string" || choiceRecord.finish_reason === null ? choiceRecord.finish_reason : null,
126
+ message: {
127
+ content: typeof message.content === "string" || message.content === null ? message.content : null,
128
+ reasoning_content: typeof message.reasoning_content === "string" || message.reasoning_content === null ? message.reasoning_content : null,
129
+ tool_calls: toolCalls.map((toolCall) => {
130
+ const toolCallRecord = toolCall && typeof toolCall === "object" && !Array.isArray(toolCall) ? toolCall : {};
131
+ const fn = toolCallRecord.function && typeof toolCallRecord.function === "object" && !Array.isArray(toolCallRecord.function)
132
+ ? toolCallRecord.function
133
+ : {};
134
+ return {
135
+ id: typeof toolCallRecord.id === "string" ? toolCallRecord.id : "",
136
+ type: "function",
137
+ function: {
138
+ name: typeof fn.name === "string" ? fn.name : "",
139
+ arguments: fn.arguments ?? ""
140
+ }
141
+ };
142
+ })
143
+ }
144
+ };
145
+ }),
146
+ ...(response.usage && typeof response.usage === "object" && !Array.isArray(response.usage) ? { usage: response.usage } : {})
147
+ };
148
+ }
36
149
  const MAX_AGENT_TOOL_ROUNDS = 6;
37
150
  const MAX_AGENT_TOOL_CALLS = 12;
38
151
  const MAX_CONFIGURED_AGENT_TOOL_CALLS = 48;
@@ -1223,6 +1336,7 @@ export class AiManager {
1223
1336
  return this.store.db.all("SELECT * FROM ai_calls WHERE work_id = ? ORDER BY created_at DESC LIMIT 200", workId).map((row) => ({
1224
1337
  id: stringValue(row, "id"),
1225
1338
  workId: stringValue(row, "work_id"),
1339
+ taskId: row.task_id === null ? null : stringValue(row, "task_id"),
1226
1340
  taskType: stringValue(row, "task_type"),
1227
1341
  provider: this.getProvider(stringValue(row, "provider_id")),
1228
1342
  model: this.getModel(stringValue(row, "model_id")),
@@ -1243,6 +1357,7 @@ export class AiManager {
1243
1357
  return paginated(rows.map((row) => ({
1244
1358
  id: stringValue(row, "id"),
1245
1359
  workId: stringValue(row, "work_id"),
1360
+ taskId: row.task_id === null ? null : stringValue(row, "task_id"),
1246
1361
  taskType: stringValue(row, "task_type"),
1247
1362
  provider: this.getProvider(stringValue(row, "provider_id")),
1248
1363
  model: this.getModel(stringValue(row, "model_id")),
@@ -1256,6 +1371,99 @@ export class AiManager {
1256
1371
  completedAt: row.completed_at === null ? null : stringValue(row, "completed_at")
1257
1372
  })), pagination);
1258
1373
  }
1374
+ getTaskTrace(taskId) {
1375
+ this.store.getTask(taskId);
1376
+ const rows = this.store.db.all(`SELECT call.id, call.task_type, call.provider_id, call.model_id, call.status, call.failure,
1377
+ call.input_chars, call.output_chars, call.created_at, call.completed_at, trace.call_id AS trace_call_id,
1378
+ CASE WHEN trace.call_id IS NULL THEN 0 ELSE json_array_length(trace.initial_messages_json) END AS initial_message_count,
1379
+ CASE WHEN trace.call_id IS NULL THEN 0 ELSE json_array_length(trace.rounds_json) END AS round_count,
1380
+ CASE WHEN trace.call_id IS NULL THEN 0 ELSE length(trace.initial_messages_json) + length(trace.rounds_json) END AS trace_chars,
1381
+ trace.created_at AS trace_created_at, trace.updated_at AS trace_updated_at, provider.name AS provider_name,
1382
+ model.display_name AS model_display_name, model.model_id AS external_model_id
1383
+ FROM ai_calls call
1384
+ LEFT JOIN ai_call_traces trace ON trace.call_id = call.id
1385
+ LEFT JOIN providers provider ON provider.id = call.provider_id
1386
+ LEFT JOIN models model ON model.id = call.model_id
1387
+ WHERE call.task_id = ?
1388
+ ORDER BY call.created_at ASC, call.id ASC`, taskId);
1389
+ const calls = rows.map((row) => {
1390
+ const hasTrace = row.trace_call_id !== null && row.trace_call_id !== undefined;
1391
+ const failure = row.failure === null ? null : stringValue(row, "failure");
1392
+ return {
1393
+ id: stringValue(row, "id"),
1394
+ taskType: stringValue(row, "task_type"),
1395
+ provider: {
1396
+ id: stringValue(row, "provider_id"),
1397
+ name: row.provider_name === null ? "已删除的供应商" : stringValue(row, "provider_name"),
1398
+ deleted: row.provider_name === null
1399
+ },
1400
+ model: {
1401
+ id: stringValue(row, "model_id"),
1402
+ displayName: row.model_display_name === null ? "已删除的模型" : stringValue(row, "model_display_name"),
1403
+ modelId: row.external_model_id === null ? null : stringValue(row, "external_model_id"),
1404
+ deleted: row.model_display_name === null
1405
+ },
1406
+ status: stringValue(row, "status"),
1407
+ failure: failure === null ? null : failure.slice(0, 1_000),
1408
+ failureTruncated: failure !== null && failure.length > 1_000,
1409
+ inputChars: numberValue(row, "input_chars"),
1410
+ outputChars: numberValue(row, "output_chars"),
1411
+ createdAt: stringValue(row, "created_at"),
1412
+ completedAt: row.completed_at === null ? null : stringValue(row, "completed_at"),
1413
+ trace: hasTrace ? {
1414
+ available: true,
1415
+ initialMessageCount: numberValue(row, "initial_message_count"),
1416
+ roundCount: numberValue(row, "round_count"),
1417
+ serializedChars: numberValue(row, "trace_chars"),
1418
+ createdAt: stringValue(row, "trace_created_at"),
1419
+ updatedAt: stringValue(row, "trace_updated_at")
1420
+ } : null
1421
+ };
1422
+ });
1423
+ return {
1424
+ taskId,
1425
+ captured: calls.some((call) => call.trace !== null),
1426
+ calls
1427
+ };
1428
+ }
1429
+ getTaskTraceCall(taskId, callId, full = false) {
1430
+ this.store.getTask(taskId);
1431
+ const row = this.store.db.get(`SELECT trace.initial_messages_json, trace.rounds_json, trace.created_at, trace.updated_at
1432
+ FROM ai_calls call JOIN ai_call_traces trace ON trace.call_id = call.id AND trace.task_id = call.task_id
1433
+ WHERE call.id = ? AND call.task_id = ?`, callId, taskId);
1434
+ if (!row)
1435
+ throw notFound("AI 调用追踪");
1436
+ const initialMessages = json(stringValue(row, "initial_messages_json"), []);
1437
+ const rounds = json(stringValue(row, "rounds_json"), []);
1438
+ if (full) {
1439
+ return {
1440
+ taskId,
1441
+ callId,
1442
+ mode: "full",
1443
+ trace: {
1444
+ initialMessages,
1445
+ rounds,
1446
+ createdAt: stringValue(row, "created_at"),
1447
+ updatedAt: stringValue(row, "updated_at")
1448
+ }
1449
+ };
1450
+ }
1451
+ const preview = previewTaskTraceMessages(initialMessages);
1452
+ return {
1453
+ taskId,
1454
+ callId,
1455
+ mode: "preview",
1456
+ previewLimit: TASK_TRACE_PREVIEW_CHARACTER_LIMIT,
1457
+ truncated: preview.truncated,
1458
+ totalPromptChars: preview.totalChars,
1459
+ trace: {
1460
+ initialMessages: preview.messages,
1461
+ rounds: rounds.map(summarizeTaskTraceRound),
1462
+ createdAt: stringValue(row, "created_at"),
1463
+ updatedAt: stringValue(row, "updated_at")
1464
+ }
1465
+ };
1466
+ }
1259
1467
  async runTask(taskId, modelId) {
1260
1468
  const task = this.store.getTask(taskId);
1261
1469
  const workId = String(task.workId);
@@ -1310,6 +1518,7 @@ export class AiManager {
1310
1518
  else {
1311
1519
  const generated = await this.generate({
1312
1520
  workId,
1521
+ taskId,
1313
1522
  taskType: taskType === "book-analysis" ? "book-analysis" : "chapter-analysis",
1314
1523
  instruction: "请基于上下文完成分析,给出有原文依据的中文结论。",
1315
1524
  scope,
@@ -1771,8 +1980,20 @@ export class AiManager {
1771
1980
  });
1772
1981
  const callId = id("call");
1773
1982
  const timestamp = now();
1774
- this.store.db.run(`INSERT INTO ai_calls (id, work_id, task_type, provider_id, model_id, context_scope_json, parameters_json,
1775
- status, input_chars, created_at, created_by_user_id) VALUES (?, ?, ?, ?, ?, ?, ?, 'running', ?, ?, ?)`, callId, input.workId, input.taskType, stringValue(provider, "id"), stringValue(model, "id"), JSON.stringify(input.scope), JSON.stringify(parameters), context.length + input.instruction.length, timestamp, currentRequestActor()?.userId ?? null);
1983
+ const traceRounds = [];
1984
+ this.store.db.transaction(() => {
1985
+ this.store.db.run(`INSERT INTO ai_calls (id, work_id, task_id, task_type, provider_id, model_id, context_scope_json, parameters_json,
1986
+ status, input_chars, created_at, created_by_user_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'running', ?, ?, ?)`, callId, input.workId, input.taskId ?? null, input.taskType, stringValue(provider, "id"), stringValue(model, "id"), JSON.stringify(input.scope), JSON.stringify(parameters), context.length + input.instruction.length, timestamp, currentRequestActor()?.userId ?? null);
1987
+ if (input.taskId) {
1988
+ this.store.db.run(`INSERT INTO ai_call_traces (call_id, task_id, initial_messages_json, rounds_json, created_at, updated_at)
1989
+ VALUES (?, ?, ?, '[]', ?, ?)`, callId, input.taskId, JSON.stringify(messages), timestamp, timestamp);
1990
+ }
1991
+ });
1992
+ const saveTrace = () => {
1993
+ if (!input.taskId)
1994
+ return;
1995
+ this.store.db.run("UPDATE ai_call_traces SET rounds_json = ?, updated_at = ? WHERE call_id = ?", JSON.stringify(traceRounds), now(), callId);
1996
+ };
1776
1997
  const callStartedAt = process.hrtime.bigint();
1777
1998
  logger.info("ai.call.started", {
1778
1999
  callId,
@@ -1785,8 +2006,10 @@ export class AiManager {
1785
2006
  instructionChars: input.instruction.length,
1786
2007
  toolCount: tools.length
1787
2008
  });
2009
+ let activeApiKey = "";
1788
2010
  try {
1789
2011
  const apiKey = this.decryptKey(provider);
2012
+ activeApiKey = apiKey;
1790
2013
  const endpoint = `${normalizeBaseUrl(stringValue(provider, "base_url"))}/chat/completions`;
1791
2014
  const timeoutMs = input.taskType === "book-analysis" || input.taskType === "relationship-analysis" ? 300_000 : 60_000;
1792
2015
  const maximumAttempts = Math.round(clamp(input.maxAttempts ?? 3, 1, 5));
@@ -1795,10 +2018,32 @@ export class AiManager {
1795
2018
  let totalInputTokens = 0;
1796
2019
  let totalCachedInputTokens = 0;
1797
2020
  const requestCompletion = async (toolChoice) => {
2021
+ const traceRound = {
2022
+ round: traceRounds.length + 1,
2023
+ requestedAt: now(),
2024
+ request: {
2025
+ model: stringValue(model, "model_id"),
2026
+ messages: structuredClone(completionMessages),
2027
+ parameters: structuredClone(parameters),
2028
+ tools: toolChoice === "auto" ? structuredClone(tools) : [],
2029
+ toolChoice
2030
+ },
2031
+ attempts: [],
2032
+ toolExecutions: []
2033
+ };
2034
+ traceRounds.push(traceRound);
2035
+ saveTrace();
1798
2036
  let lastFailure = null;
1799
2037
  for (let attempt = 1; attempt <= maximumAttempts; attempt += 1) {
1800
2038
  let retryable = true;
1801
2039
  const attemptStartedAt = process.hrtime.bigint();
2040
+ const traceAttempt = {
2041
+ attempt,
2042
+ startedAt: now(),
2043
+ status: "running"
2044
+ };
2045
+ traceRound.attempts.push(traceAttempt);
2046
+ saveTrace();
1802
2047
  logger.info("ai.call.attempt_started", { callId, attempt, maximumAttempts, toolChoice });
1803
2048
  try {
1804
2049
  const candidate = await this.scheduleProviderRequest(provider, input.signal, async () => {
@@ -1837,7 +2082,12 @@ export class AiManager {
1837
2082
  });
1838
2083
  if (candidate.ok) {
1839
2084
  try {
1840
- const parsed = JSON.parse(candidate.body);
2085
+ const parsed = redactProviderSecrets(JSON.parse(candidate.body), apiKey);
2086
+ traceAttempt.completedAt = now();
2087
+ traceAttempt.status = "completed";
2088
+ traceAttempt.httpStatus = candidate.status;
2089
+ traceAttempt.response = sanitizeCompletionTraceResponse(parsed);
2090
+ saveTrace();
1841
2091
  completionRequestCount += 1;
1842
2092
  const cacheUsage = resolveInputCacheUsage(parsed.usage);
1843
2093
  if (!cacheUsage)
@@ -1853,6 +2103,11 @@ export class AiManager {
1853
2103
  }
1854
2104
  }
1855
2105
  lastFailure = new Error(`HTTP ${candidate.status}: ${candidate.body.slice(0, 500)}`);
2106
+ traceAttempt.completedAt = now();
2107
+ traceAttempt.status = "failed";
2108
+ traceAttempt.httpStatus = candidate.status;
2109
+ traceAttempt.failure = redactProviderSecret(`HTTP ${candidate.status}: ${candidate.body.slice(0, 2_000)}`, apiKey);
2110
+ saveTrace();
1856
2111
  if (candidate.status !== 429 && candidate.status < 500) {
1857
2112
  retryable = false;
1858
2113
  throw lastFailure;
@@ -1860,6 +2115,14 @@ export class AiManager {
1860
2115
  }
1861
2116
  catch (error) {
1862
2117
  lastFailure = error;
2118
+ if (traceAttempt.status === "running") {
2119
+ traceAttempt.completedAt = now();
2120
+ traceAttempt.status = "failed";
2121
+ traceAttempt.failure = error instanceof Error
2122
+ ? redactProviderSecret(error.message.slice(0, 2_000), apiKey)
2123
+ : "AI request failed";
2124
+ saveTrace();
2125
+ }
1863
2126
  logger.warn("ai.call.attempt_failed", {
1864
2127
  callId,
1865
2128
  attempt,
@@ -1921,6 +2184,8 @@ export class AiManager {
1921
2184
  const execution = this.executeAgentTool(input.workId, toolCall);
1922
2185
  logger.info("ai.tool_call.completed", { callId, toolName: execution.name, status: execution.status, round });
1923
2186
  executedToolCalls.push(execution);
2187
+ traceRounds.at(-1)?.toolExecutions.push(execution);
2188
+ saveTrace();
1924
2189
  processSteps.push({ id: id("process"), type: "tool", round, toolCall: execution, createdAt: execution.calledAt });
1925
2190
  input.onToolCall?.(execution, round);
1926
2191
  completionMessages.push({ role: "tool", tool_call_id: toolCall.id, content: JSON.stringify(execution.result) });
@@ -1966,7 +2231,7 @@ export class AiManager {
1966
2231
  return { callId, content, outputTokens, ...(cacheHitPercent === undefined ? {} : { cacheHitPercent }), provider: this.mapProvider(provider), model: this.mapModel(model), context, toolCalls: executedToolCalls, processSteps };
1967
2232
  }
1968
2233
  catch (error) {
1969
- const message = error instanceof Error ? error.message : "AI 调用失败";
2234
+ const message = error instanceof Error ? redactProviderSecret(error.message, activeApiKey) : "AI 调用失败";
1970
2235
  this.store.db.run("UPDATE ai_calls SET status = 'failed', failure = ?, completed_at = ? WHERE id = ?", message, now(), callId);
1971
2236
  logger.error("ai.call.failed", {
1972
2237
  callId,
@@ -2170,6 +2435,7 @@ export class AiManager {
2170
2435
  const chapter = this.store.getChapter(scope.chapterId);
2171
2436
  const generated = await this.generateTaggedJson({
2172
2437
  workId,
2438
+ taskId,
2173
2439
  taskType: "chapter-analysis",
2174
2440
  signal: this.taskSignal(taskId),
2175
2441
  instruction: "分析本章并输出 JSON 对象,字段为 summary(1至3句)、events(数组)、characters(数组)、settings(数组)、evidence(数组,每项含 conclusion 和 quote)、uncertainties(数组)。",
@@ -2189,6 +2455,7 @@ export class AiManager {
2189
2455
  async runTimelineAnalysis(workId, scope, modelId, taskId) {
2190
2456
  const generated = await this.generateTaggedJson({
2191
2457
  workId,
2458
+ taskId,
2192
2459
  taskType: "timeline-analysis",
2193
2460
  signal: this.taskSignal(taskId),
2194
2461
  instruction: "抽取大事件候选并输出 JSON 数组。每项字段:name、description、eventType、timeLabel、timeSort(无法确定为 null)、location、impactScope、chapterIds、participantIds、evidence。必须区分发生时间与叙述时间;不确定时使用‘时间待定’。",
@@ -2228,6 +2495,7 @@ export class AiManager {
2228
2495
  throw new AppError(409, "CHAPTERS_REQUIRED", "世界观分析范围内没有章节");
2229
2496
  const generated = await this.generateTaggedJson({
2230
2497
  workId,
2498
+ taskId,
2231
2499
  taskType: "book-analysis",
2232
2500
  signal: this.taskSignal(taskId),
2233
2501
  instruction: [
@@ -2325,6 +2593,7 @@ export class AiManager {
2325
2593
  return { candidates: [], callId: null };
2326
2594
  const generated = await this.generateTaggedJson({
2327
2595
  workId,
2596
+ taskId,
2328
2597
  taskType: "book-analysis",
2329
2598
  signal: this.taskSignal(taskId),
2330
2599
  maxAttempts: 2,
@@ -2468,6 +2737,7 @@ export class AiManager {
2468
2737
  async runConsistencyCheck(workId, scope, modelId, taskId) {
2469
2738
  const generated = await this.generateTaggedJson({
2470
2739
  workId,
2740
+ taskId,
2471
2741
  taskType: "consistency-check",
2472
2742
  signal: this.taskSignal(taskId),
2473
2743
  instruction: "检查设定、人物状态、关系和时间是否冲突,输出 JSON 数组。每项字段:itemType、severity(low/medium/high)、title、description、entityRefs、evidence、suggestion。没有问题时输出 []。",
@@ -2523,6 +2793,7 @@ export class AiManager {
2523
2793
  }).join("\n");
2524
2794
  const generated = await this.generateTaggedJson({
2525
2795
  workId,
2796
+ taskId,
2526
2797
  taskType: "book-analysis",
2527
2798
  signal: this.taskSignal(taskId),
2528
2799
  scope: scope.type === "none" ? scope : { type: "none" },
@@ -2643,6 +2914,7 @@ export class AiManager {
2643
2914
  async verifyCharacterTitlePairs(workId, pairs, modelId, taskId) {
2644
2915
  const generated = await this.generateTaggedJson({
2645
2916
  workId,
2917
+ taskId,
2646
2918
  taskType: "book-analysis",
2647
2919
  signal: this.taskSignal(taskId),
2648
2920
  scope: { type: "none" },
@@ -2696,6 +2968,7 @@ export class AiManager {
2696
2968
  const extractChunk = async (text, maxAttempts = 3) => {
2697
2969
  const generated = await this.generateTaggedJson({
2698
2970
  workId,
2971
+ taskId,
2699
2972
  taskType: "book-analysis",
2700
2973
  signal: this.taskSignal(taskId),
2701
2974
  maxAttempts,
@@ -3073,6 +3346,7 @@ export class AiManager {
3073
3346
  const extractChunk = async (text, maxAttempts = 3) => {
3074
3347
  const generated = await this.generateTaggedJson({
3075
3348
  workId,
3349
+ taskId,
3076
3350
  taskType: "relationship-analysis",
3077
3351
  signal: this.taskSignal(taskId),
3078
3352
  maxAttempts,
@@ -3210,6 +3484,7 @@ export class AiManager {
3210
3484
  const aggregationResults = await this.processChunks(evidenceBatches, Math.min(concurrency, 4), async (evidenceBatch) => {
3211
3485
  const generated = await this.generateTaggedJson({
3212
3486
  workId,
3487
+ taskId,
3213
3488
  taskType: "relationship-analysis",
3214
3489
  signal: this.taskSignal(taskId),
3215
3490
  maxAttempts: 2,