maka-agent 0.2.0-dev.22.20260905 → 0.2.0-dev.23.20260906

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/native/runtime-host-peer/prebuilds/darwin-arm64/maka_runtime_host_peer.node +0 -0
  2. package/native/runtime-host-peer/prebuilds/linux-arm64/maka_runtime_host_peer.node +0 -0
  3. package/native/runtime-host-peer/prebuilds/linux-x64/maka_runtime_host_peer.node +0 -0
  4. package/native/runtime-host-peer/prebuilds/win32-x64/maka_runtime_host_peer.node +0 -0
  5. package/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
  6. package/node_modules/@maka/core/dist/bot-events.js +18 -63
  7. package/node_modules/@maka/core/dist/model-call-attempt.js +0 -82
  8. package/node_modules/@maka/core/dist/model-call-usage-projection.js +0 -186
  9. package/node_modules/@maka/core/dist/record-schema.js +13 -0
  10. package/node_modules/@maka/core/dist/redaction.js +41 -58
  11. package/node_modules/@maka/core/dist/session-trace.js +5 -1
  12. package/node_modules/@maka/core/dist/usage-ledger-merge.js +7 -9
  13. package/node_modules/@maka/core/dist/workhub-creation-intent.js +34 -20
  14. package/node_modules/@maka/eval/harbor/egress-proxy/entrypoint.sh +2 -1
  15. package/node_modules/@maka/runtime/dist/ai-sdk-turn.js +51 -24
  16. package/node_modules/@maka/runtime/dist/bots/base-adapter.js +17 -0
  17. package/node_modules/@maka/runtime/dist/bots/bot-test.js +38 -22
  18. package/node_modules/@maka/runtime/dist/bots/dingtalk-bridge.js +7 -5
  19. package/node_modules/@maka/runtime/dist/bots/discord-bridge.js +4 -4
  20. package/node_modules/@maka/runtime/dist/bots/feishu-bridge.js +4 -5
  21. package/node_modules/@maka/runtime/dist/bots/qq-bridge.js +6 -4
  22. package/node_modules/@maka/runtime/dist/bots/slack-bridge.js +3 -4
  23. package/node_modules/@maka/runtime/dist/bots/telegram-bridge.js +4 -5
  24. package/node_modules/@maka/runtime/dist/bots/wechat-bridge.js +17 -19
  25. package/node_modules/@maka/runtime/dist/bots/wecom-bridge.js +5 -6
  26. package/node_modules/@maka/runtime/dist/bots/ws-bridge-base.js +2 -3
  27. package/node_modules/@maka/runtime/dist/session-trace-projection.js +3 -30
  28. package/node_modules/@maka/runtime-host/dist/candidate-entry.js +4 -1
  29. package/node_modules/@maka/runtime-host/dist/client/index.js +1 -1
  30. package/node_modules/@maka/runtime-host/dist/client/peer-client.js +13 -0
  31. package/node_modules/@maka/runtime-host/dist/client/registered-host-termination.js +0 -59
  32. package/node_modules/@maka/runtime-host/dist/peer-mesh/node.js +28 -16
  33. package/node_modules/@maka/runtime-host/dist/protocol/external-session.js +4 -0
  34. package/node_modules/@maka/runtime-host/dist/protocol/host-status.js +8 -0
  35. package/node_modules/@maka/runtime-host/dist/protocol/index.js +13 -1
  36. package/node_modules/@maka/runtime-host/dist/protocol/operations.js +1 -0
  37. package/node_modules/@maka/runtime-host/dist/protocol/session-collaboration.js +28 -1
  38. package/node_modules/@maka/runtime-host/dist/protocol/workhub-coordination.js +33 -0
  39. package/node_modules/@maka/runtime-host/dist/server/access-authority.js +26 -1
  40. package/node_modules/@maka/runtime-host/dist/server/access-credential-store.js +5 -1
  41. package/node_modules/@maka/runtime-host/dist/server/canonical-usage-reader.js +24 -11
  42. package/node_modules/@maka/runtime-host/dist/server/daily-review-coordinator.js +7 -4
  43. package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +70 -6
  44. package/node_modules/@maka/runtime-host/dist/server/external-session-coordinator.js +6 -3
  45. package/node_modules/@maka/runtime-host/dist/server/host-kernel.js +12 -6
  46. package/node_modules/@maka/runtime-host/dist/server/message-coordinator.js +9 -2
  47. package/node_modules/@maka/runtime-host/dist/server/root-admission-owner.js +7 -0
  48. package/node_modules/@maka/runtime-host/dist/server/root-turn-coordinator.js +21 -0
  49. package/node_modules/@maka/runtime-host/dist/server/session-catalog-coordinator.js +151 -5
  50. package/node_modules/@maka/runtime-host/dist/server/usage-pricing-coordinator.js +4 -11
  51. package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-action-gate.js +44 -15
  52. package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-coordinator.js +6 -1
  53. package/node_modules/@maka/storage/dist/agent-run-store.js +44 -0
  54. package/node_modules/@maka/storage/dist/conversation-operational-state.js +4 -0
  55. package/node_modules/@maka/storage/dist/execution-stores.js +2 -0
  56. package/node_modules/@maka/storage/dist/model-call-ledger.js +199 -32
  57. package/node_modules/@maka/storage/dist/model-call-usage-sql.js +136 -0
  58. package/node_modules/@maka/storage/dist/operational-state-store-public.js +1 -1
  59. package/node_modules/@maka/storage/dist/operational-state-store.js +5 -3
  60. package/node_modules/@maka/storage/dist/sqlite-core-execution-schema.js +11 -1
  61. package/node_modules/@maka/storage/dist/sqlite-usage-schema.js +200 -20
  62. package/node_modules/@maka/storage/dist/usage-stores.js +6 -2
  63. package/package.json +1 -1
@@ -22,6 +22,7 @@
22
22
  * different copy without diluting the core message. Exported so the
23
23
  * handler can use it AND a contract test can pin it.
24
24
  */
25
+ // bot-channel notices follow the bot audience language; localization tracked under #2672
25
26
  export function nonTextMessageAck(kind) {
26
27
  switch (kind) {
27
28
  case 'photo':
@@ -139,74 +140,28 @@ function sanitizeBotUserName(value) {
139
140
  .trim() || 'unknown');
140
141
  }
141
142
  /**
142
- * PR-BOT-LASTERROR-FROM-SEND-0 (external bot research): translate the bridge's
143
- * machine-readable `BotStatus.reason` into a short user-readable string
144
- * suitable for persistence in `BotChannelSettings.lastError`. The Settings
145
- * page reads `lastError` from persisted settings (not live status), so
146
- * without this persistence step the user sees stale connection-test
147
- * errors instead of the actual send-path failure that happened minutes
148
- * ago.
149
- *
150
- * Returns `undefined` for non-error reasons (disabled/stopped/missing
151
- * credentials — those have their own UI surface) and for unrecognized
152
- * inputs whose pass-through risks leaking unredacted payloads.
153
- *
154
- * Length-capped at 200 chars defensively; a real Telegram error
155
- * description is typically well under 80 chars.
143
+ * Non-error `BotStatus.reason` values. These states have their own UI surface,
144
+ * so an error readout must not repeat them as failures.
156
145
  */
157
- const BOT_REASON_HUMANIZE = {
158
- 'rate-limited': '发送被节流(429);上一条回复可能截断,可以请用户再发一次',
159
- 'polling-timeout': '事件轮询超时;可能是网络抖动或代理失效',
160
- 'send-failed': '上一次发送失败,详细原因 Telegram 没有返回',
161
- 'get-me-failed': '凭据探测失败;请检查 Bot Token',
162
- // Non-error states surface elsewhere in the UI — return undefined so
163
- // we do not overwrite a real lastError with a benign status change.
164
- disabled: undefined,
165
- stopped: undefined,
166
- 'no-token': undefined,
167
- 'missing-feishu-credentials': undefined,
168
- 'feishu-domain-required': undefined,
169
- 'feishu-events-not-connected': undefined,
170
- 'scaffold-only': undefined,
171
- unimplemented: undefined,
172
- };
146
+ const BOT_BENIGN_STATUS_REASONS = new Set([
147
+ 'disabled',
148
+ 'stopped',
149
+ 'token_missing',
150
+ 'feishu_credentials_missing',
151
+ 'feishu-domain-required',
152
+ 'feishu-events-not-connected',
153
+ 'scaffold-only',
154
+ 'unimplemented',
155
+ ]);
173
156
  /**
174
- * PR-BOT-RUNTIME-REASON-HUMANIZE-0: Discord / DingTalk / QQ bridges
175
- * emit parameterized reason strings like `gateway-closed-4004` and
176
- * `connections-open-500`. Without these patterns the user sees the
177
- * raw machine code in `lastError`; with them they get a translated
178
- * description plus the diagnostic code preserved in parentheses.
179
- *
180
- * Each entry is a regex with one numeric capture group; the matched
181
- * code is preserved verbatim so support diagnostics still survive.
157
+ * `BotStatus.reason` stays a stable code; presenters own the copy. Benign
158
+ * states yield `undefined` so they never overwrite a real error readout.
182
159
  */
183
- const BOT_REASON_HUMANIZE_PATTERNS = [
184
- { pattern: /^gateway-bot-(\d+)$/, format: (code) => `获取 Gateway 失败(HTTP ${code})` },
185
- { pattern: /^gateway-closed-(\d+)$/, format: (code) => `Gateway 连接关闭(${code});正在重连` },
186
- { pattern: /^connections-open-(\d+)$/, format: (code) => `Stream 订阅打开失败(HTTP ${code})` },
187
- { pattern: /^stream-closed-(\d+)$/, format: (code) => `Stream 连接关闭(${code});正在重连` },
188
- { pattern: /^send-failed-(\d+)$/, format: (code) => `发送失败(HTTP ${code})` },
189
- {
190
- pattern: /^getAppAccessToken-(\d+)$/,
191
- format: (code) => `获取 access_token 失败(HTTP ${code})`,
192
- },
193
- ];
194
- export function humanizeBotStatusReason(reason) {
195
- if (typeof reason !== 'string' || reason.length === 0)
160
+ export function botStatusErrorReason(reason) {
161
+ if (typeof reason !== 'string')
196
162
  return undefined;
197
- if (reason in BOT_REASON_HUMANIZE) {
198
- return BOT_REASON_HUMANIZE[reason];
199
- }
200
- for (const { pattern, format } of BOT_REASON_HUMANIZE_PATTERNS) {
201
- const match = pattern.exec(reason);
202
- if (match)
203
- return format(match[1]);
204
- }
205
- // Pass-through for platform-supplied descriptions ("Bad Request:
206
- // chat not found", etc.). Trim + length-cap to keep `lastError`
207
- // bounded.
208
163
  const trimmed = reason.trim();
209
- if (trimmed.length === 0)
164
+ if (trimmed.length === 0 || BOT_BENIGN_STATUS_REASONS.has(trimmed))
210
165
  return undefined;
211
166
  return trimmed.length > 200 ? trimmed.slice(0, 200) : trimmed;
212
167
  }
@@ -349,15 +349,6 @@ export function decodeModelCallAttempt(value) {
349
349
  }
350
350
  return value;
351
351
  }
352
- export function isModelCallAttempt(value) {
353
- try {
354
- decodeModelCallAttempt(value);
355
- return true;
356
- }
357
- catch {
358
- return false;
359
- }
360
- }
361
352
  /**
362
353
  * Collapses re-appended records by `attemptId`, keeping the last occurrence.
363
354
  *
@@ -389,76 +380,3 @@ export function groupModelCallAttempts(attempts) {
389
380
  }
390
381
  return [...groups.values()];
391
382
  }
392
- /**
393
- * The attempt that settled a logical call: the highest `attempt` ordinal that
394
- * reached a provider outcome. Terminality is a projection concern, not a stored
395
- * field, so it is derived rather than recorded.
396
- */
397
- export function settledAttempt(group) {
398
- let settled;
399
- for (const attempt of group.attempts) {
400
- if (!settled || attempt.attempt > settled.attempt)
401
- settled = attempt;
402
- }
403
- return settled;
404
- }
405
- /**
406
- * Extracts the canonical attempts a run committed, from that run's AgentRun
407
- * events. This is the projection the Usage read model is rebuilt through, so it
408
- * has to be total: an event that cannot be decoded is counted, not thrown, or
409
- * one bad record would block every later one in the same run from ever being
410
- * projected.
411
- */
412
- export function modelCallAttemptsFromRunEvents(events) {
413
- const attempts = [];
414
- let unreadableEvents = 0;
415
- for (const event of events) {
416
- if (event.type !== MODEL_CALL_ATTEMPT_EVENT_TYPE)
417
- continue;
418
- try {
419
- attempts.push(decodeModelCallAttempt(event.data));
420
- }
421
- catch {
422
- unreadableEvents += 1;
423
- }
424
- }
425
- return { attempts, unreadableEvents };
426
- }
427
- export function summarizeModelCallCoverage(attempts) {
428
- const unique = dedupeModelCallAttempts(attempts);
429
- const coverage = {
430
- attempts: unique.length,
431
- pricedAttempts: 0,
432
- unpricedAttempts: 0,
433
- usageReportedAttempts: 0,
434
- usagePartialAttempts: 0,
435
- usageMissingAttempts: 0,
436
- };
437
- for (const attempt of unique) {
438
- if (attempt.costBasis === 'priced')
439
- coverage.pricedAttempts += 1;
440
- else
441
- coverage.unpricedAttempts += 1;
442
- if (attempt.usageBasis === 'reported')
443
- coverage.usageReportedAttempts += 1;
444
- else if (attempt.usageBasis === 'partial')
445
- coverage.usagePartialAttempts += 1;
446
- else
447
- coverage.usageMissingAttempts += 1;
448
- }
449
- return coverage;
450
- }
451
- /**
452
- * Sums cost across attempts. Returns the total alongside the coverage that
453
- * qualifies it, because a bare number cannot express "plus an unknown amount
454
- * from unpriced calls".
455
- */
456
- export function sumModelCallCostUsd(attempts) {
457
- const unique = dedupeModelCallAttempts(attempts);
458
- let costUsd = 0;
459
- for (const attempt of unique) {
460
- if (attempt.costBasis === 'priced' && attempt.costUsd !== undefined)
461
- costUsd += attempt.costUsd;
462
- }
463
- return { costUsd, coverage: summarizeModelCallCoverage(unique) };
464
- }
@@ -16,8 +16,6 @@
16
16
  * specific language governing permissions and limitations
17
17
  * under the License.
18
18
  */
19
- import { dedupeModelCallAttempts, summarizeModelCallCoverage, } from './model-call-attempt.js';
20
- import { usageBucketKey } from './usage-stats/bucket-key.js';
21
19
  const DAY_MS = 86_400_000;
22
20
  export function resolveUsageRange(range, now) {
23
21
  if (typeof range === 'object')
@@ -44,190 +42,6 @@ export function usageStatusForAttempt(status) {
44
42
  return 'error';
45
43
  return 'aborted';
46
44
  }
47
- function matchesQuery(attempt, query, range) {
48
- if (attempt.completedAt < range.from || attempt.completedAt > range.to)
49
- return false;
50
- if (query.sessionId !== undefined && attempt.sessionId !== query.sessionId)
51
- return false;
52
- if (query.providerId !== undefined && attempt.providerId !== query.providerId)
53
- return false;
54
- if (query.modelId !== undefined && attempt.modelId !== query.modelId)
55
- return false;
56
- if (query.connectionSlug !== undefined && attempt.connectionSlug !== query.connectionSlug) {
57
- return false;
58
- }
59
- if (query.status !== undefined && query.status !== 'all') {
60
- if (usageStatusForAttempt(attempt.status) !== query.status)
61
- return false;
62
- }
63
- return true;
64
- }
65
- /**
66
- * Selects the attempts a query addresses, in append order, deduped by
67
- * `attemptId` so a re-appended settlement counts once.
68
- */
69
- export function selectModelCallAttempts(attempts, query, now) {
70
- const range = resolveUsageRange(query.range, now);
71
- const rows = dedupeModelCallAttempts(attempts).filter((a) => matchesQuery(a, query, range));
72
- return { rows, range };
73
- }
74
- function tokens(attempt) {
75
- const reportedCacheRead = attempt.cacheReadInputTokens ?? 0;
76
- const input = attempt.inputTokens ?? 0;
77
- const output = attempt.outputTokens ?? 0;
78
- const cacheMiss = attempt.cacheMissInputTokens ?? 0;
79
- // With no reported prompt total there is no denominator to validate against,
80
- // but the provider's cache evidence remains authoritative. Presentation code
81
- // must leave ratios unavailable while Usage coverage is partial.
82
- const cacheRead = attempt.inputTokens === undefined
83
- ? reportedCacheRead
84
- : clampCacheReadTokens(input, reportedCacheRead);
85
- const cacheWrite = attempt.cacheWriteInputTokens ?? 0;
86
- const reasoning = attempt.reasoningTokens ?? 0;
87
- return { input, output, cacheMiss, cacheRead, cacheWrite, reasoning, total: input + output };
88
- }
89
45
  export function clampCacheReadTokens(inputTokens, cacheReadTokens) {
90
46
  return Math.min(cacheReadTokens, inputTokens);
91
47
  }
92
- /** Cost contributed by an attempt. Unpriced records contribute nothing to the
93
- * sum and are surfaced through coverage instead of being counted as zero. */
94
- function pricedCost(attempt) {
95
- return attempt.costBasis === 'priced' ? (attempt.costUsd ?? 0) : 0;
96
- }
97
- export function projectModelCallUsageSummary(attempts, query, now) {
98
- const { rows, range } = selectModelCallAttempts(attempts, query, now);
99
- const totals = {
100
- input: 0,
101
- output: 0,
102
- cacheMiss: 0,
103
- cacheRead: 0,
104
- cacheWrite: 0,
105
- reasoning: 0,
106
- total: 0,
107
- };
108
- let totalCostUsd = 0;
109
- let totalDurationMs = 0;
110
- let cacheHitRequests = 0;
111
- let cacheCreateRequests = 0;
112
- let errorRequests = 0;
113
- for (const attempt of rows) {
114
- const t = tokens(attempt);
115
- totals.input += t.input;
116
- totals.output += t.output;
117
- totals.cacheMiss += t.cacheMiss;
118
- totals.cacheRead += t.cacheRead;
119
- totals.cacheWrite += t.cacheWrite;
120
- totals.reasoning += t.reasoning;
121
- totals.total += t.total;
122
- totalCostUsd += pricedCost(attempt);
123
- totalDurationMs += attempt.latencyMs;
124
- if (t.cacheRead > 0)
125
- cacheHitRequests += 1;
126
- if (t.cacheWrite > 0)
127
- cacheCreateRequests += 1;
128
- if (usageStatusForAttempt(attempt.status) === 'error')
129
- errorRequests += 1;
130
- }
131
- return {
132
- range,
133
- totalRequests: rows.length,
134
- totalCostUsd,
135
- totalDurationMs,
136
- totalTokens: totals,
137
- cacheHitRequests,
138
- cacheCreateRequests,
139
- errorRequests,
140
- coverage: summarizeModelCallCoverage(rows),
141
- };
142
- }
143
- export function projectModelCallUsageBuckets(attempts, query, groupBy, now) {
144
- const { rows } = selectModelCallAttempts(attempts, query, now);
145
- const groups = new Map();
146
- for (const attempt of rows) {
147
- const key = usageBucketKey({ providerId: attempt.providerId, modelId: attempt.modelId, ts: attempt.completedAt }, groupBy);
148
- const group = groups.get(key);
149
- if (group)
150
- group.push(attempt);
151
- else
152
- groups.set(key, [attempt]);
153
- }
154
- return [...groups.entries()]
155
- .map(([key, group]) => {
156
- const agg = {
157
- input: 0,
158
- output: 0,
159
- cacheMiss: 0,
160
- cacheRead: 0,
161
- cacheWrite: 0,
162
- reasoning: 0,
163
- total: 0,
164
- };
165
- let costUsd = 0;
166
- let latency = 0;
167
- let errors = 0;
168
- for (const attempt of group) {
169
- const t = tokens(attempt);
170
- agg.input += t.input;
171
- agg.output += t.output;
172
- agg.cacheMiss += t.cacheMiss;
173
- agg.cacheRead += t.cacheRead;
174
- agg.cacheWrite += t.cacheWrite;
175
- agg.reasoning += t.reasoning;
176
- agg.total += t.total;
177
- costUsd += pricedCost(attempt);
178
- latency += attempt.latencyMs;
179
- if (usageStatusForAttempt(attempt.status) === 'error')
180
- errors += 1;
181
- }
182
- return {
183
- key,
184
- label: key,
185
- requests: group.length,
186
- inputTokens: agg.input,
187
- outputTokens: agg.output,
188
- cacheMissTokens: agg.cacheMiss,
189
- cacheReadTokens: agg.cacheRead,
190
- cacheWriteTokens: agg.cacheWrite,
191
- reasoningTokens: agg.reasoning,
192
- totalTokens: agg.total,
193
- costUsd,
194
- avgLatencyMs: group.length === 0 ? 0 : latency / group.length,
195
- errorRate: group.length === 0 ? 0 : errors / group.length,
196
- };
197
- })
198
- .sort((left, right) => right.requests - left.requests);
199
- }
200
- export function projectModelCallUsageLogs(attempts, query, now, offset = 0, limit = 100) {
201
- const { rows } = selectModelCallAttempts(attempts, query, now);
202
- const ordered = [...rows].sort((left, right) => right.completedAt - left.completedAt);
203
- const page = ordered.slice(offset, offset + limit).map((attempt) => {
204
- const t = tokens(attempt);
205
- return {
206
- id: attempt.attemptId,
207
- ts: attempt.completedAt,
208
- callKind: attempt.callKind,
209
- callId: attempt.logicalCallId,
210
- ...(attempt.connectionSlug !== undefined ? { connectionSlug: attempt.connectionSlug } : {}),
211
- providerId: attempt.providerId,
212
- modelId: attempt.modelId,
213
- inputTokens: t.input,
214
- outputTokens: t.output,
215
- cacheMissTokens: t.cacheMiss,
216
- cacheReadTokens: t.cacheRead,
217
- cacheWriteTokens: t.cacheWrite,
218
- reasoningTokens: t.reasoning,
219
- totalTokens: t.total,
220
- // A row keeps its basis, not just its number. Collapsing an unpriced call
221
- // to 0 here would reproduce, per row, exactly the ambiguity the coverage
222
- // breakdown removes from the totals.
223
- ...(attempt.costBasis === 'priced' ? { costUsd: attempt.costUsd ?? 0 } : {}),
224
- costBasis: attempt.costBasis,
225
- latencyMs: attempt.latencyMs,
226
- status: usageStatusForAttempt(attempt.status),
227
- ...(attempt.errorClass !== undefined ? { errorClass: attempt.errorClass } : {}),
228
- sessionId: attempt.sessionId,
229
- turnId: attempt.turnId,
230
- };
231
- });
232
- return { rows: page, total: ordered.length, coverage: summarizeModelCallCoverage(rows) };
233
- }
@@ -33,6 +33,19 @@ export function hasExactShape(value, shape) {
33
33
  return (shape.required.every((key) => Object.hasOwn(value, key)) &&
34
34
  Object.keys(value).every((key) => shape.allowed.has(key)));
35
35
  }
36
+ /**
37
+ * Narrows a record to the keys a shape allows. `undefined` entries are dropped
38
+ * so the result serializes the way {@link hasExactShape} reads it back.
39
+ */
40
+ export function pickShape(value, shape) {
41
+ const picked = {};
42
+ for (const key of shape.allowed) {
43
+ const entry = value[key];
44
+ if (entry !== undefined)
45
+ picked[key] = entry;
46
+ }
47
+ return picked;
48
+ }
36
49
  export function isFiniteNumber(value) {
37
50
  return typeof value === 'number' && Number.isFinite(value);
38
51
  }
@@ -174,76 +174,59 @@ function isAssignmentSensitiveKey(key) {
174
174
  const suffix = sensitiveKeySegments(key).at(-1);
175
175
  return suffix !== 'auth' && suffix !== 'authorization';
176
176
  }
177
- const GENERALIZED_ERROR_COPY = {
178
- 'zh-CN': {
179
- timeout: '请求超时',
180
- rateLimit: '触发模型速率限制',
181
- authentication: '鉴权失败',
182
- provider: '模型服务返回错误',
183
- network: '网络错误',
184
- },
185
- 'zh-TW': {
186
- timeout: '請求逾時',
187
- rateLimit: '已達模型速率限制',
188
- authentication: '驗證失敗',
189
- provider: '模型服務傳回錯誤',
190
- network: '網路錯誤',
191
- },
192
- en: {
193
- timeout: 'Request timed out',
194
- rateLimit: 'Rate limit exceeded',
195
- authentication: 'Authentication failed',
196
- provider: 'Provider returned an error',
197
- network: 'Network error',
198
- },
199
- };
200
- function classifyGeneralizedError(error) {
177
+ /**
178
+ * Keyword classification shared by the localized message helpers and by
179
+ * producers that emit a stable machine code instead of prose.
180
+ */
181
+ export function classifyGeneralizedError(error) {
201
182
  const message = error instanceof Error ? error.message : String(error);
202
- const redacted = redactSecrets(message);
203
- const lower = redacted.toLowerCase();
183
+ const lower = redactSecrets(message).toLowerCase();
204
184
  if (lower.includes('timeout'))
205
185
  return 'timeout';
206
186
  if (lower.includes('429') || lower.includes('rate'))
207
- return 'rateLimit';
187
+ return 'rate_limited';
208
188
  if (lower.includes('401') || lower.includes('403') || isAuthenticationErrorText(lower))
209
- return 'authentication';
210
- if (lower.includes('5') && /\b5\d\d\b/.test(lower))
211
- return 'provider';
189
+ return 'auth_failed';
190
+ if (/\b5\d\d\b/.test(lower))
191
+ return 'provider_error';
212
192
  if (lower.includes('network') ||
213
193
  lower.includes('fetch') ||
214
194
  lower.includes('econn') ||
215
195
  lower.includes('enotfound'))
216
- return 'network';
217
- return null;
196
+ return 'network_error';
197
+ return undefined;
218
198
  }
219
- function localizedGeneralizedErrorMessage(error, fallback, locale) {
220
- const category = classifyGeneralizedError(error);
221
- return category ? GENERALIZED_ERROR_COPY[locale][category] : fallback;
199
+ /** Locale copy for each {@link GeneralizedErrorClass}; catalog authors spread
200
+ * this per-locale block instead of restating the sentences. */
201
+ export const GENERALIZED_ERROR_COPY = {
202
+ 'zh-CN': {
203
+ timeout: '请求超时',
204
+ rate_limited: '触发模型速率限制',
205
+ auth_failed: '鉴权失败',
206
+ provider_error: '模型服务返回错误',
207
+ network_error: '网络错误',
208
+ },
209
+ 'zh-TW': {
210
+ timeout: '請求逾時',
211
+ rate_limited: '已達模型速率限制',
212
+ auth_failed: '驗證失敗',
213
+ provider_error: '模型服務傳回錯誤',
214
+ network_error: '網路錯誤',
215
+ },
216
+ en: {
217
+ timeout: 'Request timed out',
218
+ rate_limited: 'Rate limit exceeded',
219
+ auth_failed: 'Authentication failed',
220
+ provider_error: 'Provider returned an error',
221
+ network_error: 'Network error',
222
+ },
223
+ };
224
+ export function generalizedErrorMessageForLocale(error, fallback, locale) {
225
+ const classified = classifyGeneralizedError(error);
226
+ return classified ? GENERALIZED_ERROR_COPY[locale][classified] : fallback;
222
227
  }
223
228
  export function generalizedErrorMessage(error, fallback = 'Operation failed') {
224
- return localizedGeneralizedErrorMessage(error, fallback, 'en');
225
- }
226
- /**
227
- * Chinese-locale companion to `generalizedErrorMessage()` (PR110b
228
- * follow-up). Same classification rules; returns Chinese phrasing
229
- * instead of English. Used by surfaces that must enforce a
230
- * Chinese-only error copy contract (session start, onboarding setup
231
- * banners, etc.) — the English version would have leaked through any
232
- * matched category, breaking the gate.
233
- *
234
- * The fallback default is also Chinese so callers that don't supply
235
- * one still produce a Chinese-only result. Pass a more specific
236
- * Chinese fallback (e.g. "会话已创建但发送失败,请重试。") for better
237
- * UX when the classifier can't categorize.
238
- */
239
- export function generalizedErrorMessageChinese(error, fallback = '操作失败') {
240
- return localizedGeneralizedErrorMessage(error, fallback, 'zh-CN');
241
- }
242
- export function generalizedErrorMessageTraditionalChinese(error, fallback = '操作失敗') {
243
- return localizedGeneralizedErrorMessage(error, fallback, 'zh-TW');
244
- }
245
- export function generalizedErrorMessageForLocale(error, fallback, locale) {
246
- return localizedGeneralizedErrorMessage(error, fallback, locale);
229
+ return generalizedErrorMessageForLocale(error, fallback, 'en');
247
230
  }
248
231
  export function isAuthenticationErrorText(message) {
249
232
  return message.replace(/\bauthorit\w*/g, '').includes('auth');
@@ -65,7 +65,11 @@ const MODEL_CALL_STEP_SHAPE = defineObjectShape()([
65
65
  'attempts',
66
66
  'status',
67
67
  ], ['connectionSlug', 'historyCompactRoute', 'costUsd']);
68
- const MODEL_ATTEMPT_SHAPE = defineObjectShape()([
68
+ /**
69
+ * The Inspector's own view of an attempt. Exported so the projection narrows an
70
+ * authority record through this list rather than restating it by hand.
71
+ */
72
+ export const MODEL_ATTEMPT_SHAPE = defineObjectShape()([
69
73
  'attemptId',
70
74
  'attempt',
71
75
  'status',
@@ -16,7 +16,6 @@
16
16
  * specific language governing permissions and limitations
17
17
  * under the License.
18
18
  */
19
- import { projectModelCallUsageBuckets, projectModelCallUsageLogs, projectModelCallUsageSummary, } from './model-call-usage-projection.js';
20
19
  /**
21
20
  * The cost to present for a usage total, or `undefined` when it cannot be shown
22
21
  * honestly. Trust the total only when at least one canonical attempt was priced;
@@ -66,8 +65,8 @@ export function legacyUsageProvenance(legacyRecords) {
66
65
  pendingRepairs: 0,
67
66
  };
68
67
  }
69
- export function mergeUsageSummary(legacy, canonical, query, now) {
70
- const projected = projectModelCallUsageSummary(canonical.attempts, query, now);
68
+ export function mergeUsageSummary(legacy, canonical) {
69
+ const projected = canonical.projection;
71
70
  return {
72
71
  range: projected.range,
73
72
  totalRequests: legacy.totalRequests + projected.totalRequests,
@@ -95,17 +94,16 @@ export function mergeUsageSummary(legacy, canonical, query, now) {
95
94
  },
96
95
  };
97
96
  }
98
- export function mergeUsageBuckets(legacy, canonical, query, groupBy, now) {
99
- const projected = projectModelCallUsageBuckets(canonical.attempts, query, groupBy, now);
97
+ export function mergeUsageBuckets(legacy, canonical) {
100
98
  const merged = new Map();
101
- for (const bucket of [...legacy, ...projected]) {
99
+ for (const bucket of [...legacy, ...canonical.projection.buckets]) {
102
100
  const existing = merged.get(bucket.key);
103
101
  merged.set(bucket.key, existing ? combineBuckets(existing, bucket) : { ...bucket });
104
102
  }
105
103
  return {
106
104
  buckets: [...merged.values()].sort((left, right) => right.requests - left.requests),
107
105
  provenance: {
108
- coverage: projectModelCallUsageSummary(canonical.attempts, query, now).coverage,
106
+ coverage: canonical.projection.coverage,
109
107
  legacyRecords: legacy.reduce((total, bucket) => total + bucket.requests, 0),
110
108
  unreadableRecords: canonical.unreadableRecords,
111
109
  pendingRepairs: canonical.pendingRepairs,
@@ -117,8 +115,8 @@ export function mergeUsageBuckets(legacy, canonical, query, groupBy, now) {
117
115
  * the merged order can only come from the first `offset + limit` rows of each —
118
116
  * callers may bound their reads to that prefix and still page exactly.
119
117
  */
120
- export function mergeUsageLogs(legacy, canonical, query, now, offset, limit) {
121
- const projected = projectModelCallUsageLogs(canonical.attempts, query, now, 0, offset + limit);
118
+ export function mergeUsageLogs(legacy, canonical, offset, limit) {
119
+ const projected = canonical.projection;
122
120
  const rows = [];
123
121
  let left = 0;
124
122
  let right = 0;