@lqc123qwe/car-runtime 1.12.0 → 1.14.0

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 (45) hide show
  1. package/README.md +10 -1
  2. package/dist/SHA256SUMS +1 -1
  3. package/dist/authz/authz.d.ts +9 -0
  4. package/dist/authz/authz.js +15 -0
  5. package/dist/car-runtime-1.14.0.tgz +0 -0
  6. package/dist/car-runtime-1.14.0.tgz.sig.bundle +1 -0
  7. package/dist/cli.js +48 -16
  8. package/dist/load/config.d.ts +45 -0
  9. package/dist/load/config.js +84 -1
  10. package/dist/load/pool.d.ts +97 -0
  11. package/dist/load/pool.js +261 -0
  12. package/dist/loop/stop.d.ts +6 -0
  13. package/dist/loop/stop.js +14 -3
  14. package/dist/release-audit.jsonl +4 -4
  15. package/dist/runtime-core/chatStep.js +2 -0
  16. package/dist/runtime-core/credentials.d.ts +2 -0
  17. package/dist/runtime-core/credentials.js +2 -1
  18. package/dist/runtime-core/llm.d.ts +6 -0
  19. package/dist/runtime-core/llm.js +54 -3
  20. package/dist/runtime-core/types.d.ts +15 -0
  21. package/dist/sandbox/sandbox.d.ts +7 -1
  22. package/dist/sandbox/sandbox.js +10 -4
  23. package/dist/sbom.json +24 -14
  24. package/dist/seam.d.ts +15 -1
  25. package/dist/seam.js +17 -1
  26. package/dist/session/compact.d.ts +57 -0
  27. package/dist/session/compact.js +128 -0
  28. package/dist/session/log.d.ts +5 -1
  29. package/dist/session/log.js +22 -2
  30. package/package.json +1 -1
  31. package/src/authz/authz.ts +16 -0
  32. package/src/cli.ts +48 -17
  33. package/src/load/config.ts +135 -1
  34. package/src/load/pool.ts +339 -0
  35. package/src/loop/stop.ts +19 -4
  36. package/src/runtime-core/chatStep.ts +2 -0
  37. package/src/runtime-core/credentials.ts +4 -1
  38. package/src/runtime-core/llm.ts +59 -3
  39. package/src/runtime-core/types.ts +11 -0
  40. package/src/sandbox/sandbox.ts +20 -5
  41. package/src/seam.ts +17 -1
  42. package/src/session/compact.ts +168 -0
  43. package/src/session/log.ts +19 -3
  44. package/dist/car-runtime-1.12.0.tgz +0 -0
  45. package/dist/car-runtime-1.12.0.tgz.sig.bundle +0 -1
@@ -25,6 +25,11 @@
25
25
  * 解析「先摘 evt.usage 再判 choice」(空 choices 尾包不再被吞);anthropic message_start(input 侧)
26
26
  * + message_delta(output 侧)两包合并(message_delta 缺席 = output 不完整,整条不落);
27
27
  * 归一口径 §1.1(openai 减法 / anthropic 透传 + cache_creation),畸形/缺失整条不落(禁补造)。
28
+ * - 1.14-S1(K-MP-1 / D-39):思考参数透传——LlmRequest.reasoning.effort 按家族映射:
29
+ * openai-compat = `reasoning_effort`;anthropic = `thinking:{type:'enabled',budget_tokens}`
30
+ * (ANTHROPIC_THINKING_BUDGET 表);**off / 缺席 = 字节面不下发**;未知 effort 值显式报错
31
+ * (CAR-E-LLM-REASONING,fail-visible 禁静默忽略——宿主越型传入防线);anthropic 侧
32
+ * budget ≥ max_tokens 违反 API 约束 → 显式报错提示调高(禁静默改写 max_tokens)。
28
33
  */
29
34
  import { CarM8Error, providerUnreachable } from './errors.ts'
30
35
  import { CredentialService } from './credentials.ts'
@@ -72,6 +77,22 @@ export class AdapterRegistry {
72
77
  }
73
78
  }
74
79
 
80
+ // ==================== 1.14-S1(K-MP-1 / D-39)思考参数映射 ====================
81
+
82
+ /** 归一思考档位:缺席/off → undefined(不下发);low/medium/high 原样;未知值 fail-visible(宿主越型传入防线) */
83
+ export function normalizeReasoningEffort(effort: unknown): 'off' | 'low' | 'medium' | 'high' | undefined {
84
+ if (effort === undefined || effort === null) return undefined
85
+ if (effort === 'off' || effort === 'low' || effort === 'medium' || effort === 'high') return effort
86
+ throw new Error(`CAR-E-LLM-REASONING: 未知思考档位 "${String(effort)}"(合法:off/low/medium/high;off/缺席 = 不下发)`)
87
+ }
88
+
89
+ /** anthropic effort → thinking.budget_tokens 映射(D-39 登记表);off/缺席不下发 */
90
+ const ANTHROPIC_THINKING_BUDGET: Record<'low' | 'medium' | 'high', number> = {
91
+ low: 2048,
92
+ medium: 8192,
93
+ high: 16384,
94
+ }
95
+
75
96
  // ==================== finishReason 不可变守卫(SDK 边界) ====================
76
97
 
77
98
  /** 首个 finishReason 定格;适配器后续给出不同值 = 契约违规显式抛错(不改写不吞没) */
@@ -264,6 +285,8 @@ export interface OpenAICompatOptions {
264
285
  /** 凭据服务与 provider 名(Bearer token 来源) */
265
286
  credentials?: CredentialService
266
287
  provider?: string
288
+ /** 1.14-S4(K-POOL-1):成员级凭据 env 变量名引用(modelPool keyEnv——resolve 精确候选;keychain 链保持在前) */
289
+ credentialEnvVars?: string[]
267
290
  fetchImpl?: typeof fetch
268
291
  /** §3.5.4 基线:总 120s / 首字节 30s / 2 次退避 1s-2s(测试可注入缩短) */
269
292
  timeoutMs?: number
@@ -288,6 +311,9 @@ class Retryable extends Error {}
288
311
  * 1.11-S1(D-33 / 计量设计 §1.1):openai usage 归一——prompt_tokens 含 cached,inputTokens
289
312
  * 必须做减法(prompt − prompt_tokens_details.cached_tokens);cache_creation 恒 0(无对应字段,
290
313
  * 登记不估算)。usage 对象缺席/畸形(两侧主字段任一非数值)= 整条不落(缺失口径,禁补造)。
314
+ * 1.14-S3(K-MP-2b / D-41):分段计量——completion_tokens_details.reasoning_tokens(思考输出
315
+ * 细分,provider 真实事实)数值才采(segments.reasoningTokens);details/字段缺席或非数值 =
316
+ * segments 不带键(禁估算补造)。
291
317
  */
292
318
  function normalizeOpenAIUsage(u: unknown, model: string, adapterId: string): LlmUsage | undefined {
293
319
  if (!u || typeof u !== 'object') return undefined
@@ -295,6 +321,8 @@ function normalizeOpenAIUsage(u: unknown, model: string, adapterId: string): Llm
295
321
  if (typeof r.prompt_tokens !== 'number' || typeof r.completion_tokens !== 'number') return undefined
296
322
  const details = (r.prompt_tokens_details ?? null) as Record<string, unknown> | null
297
323
  const cached = details && typeof details.cached_tokens === 'number' ? details.cached_tokens : 0
324
+ const compDetails = (r.completion_tokens_details ?? null) as Record<string, unknown> | null
325
+ const reasoning = compDetails && typeof compDetails.reasoning_tokens === 'number' ? compDetails.reasoning_tokens : undefined
298
326
  return {
299
327
  inputTokens: r.prompt_tokens - cached,
300
328
  outputTokens: r.completion_tokens,
@@ -303,6 +331,7 @@ function normalizeOpenAIUsage(u: unknown, model: string, adapterId: string): Llm
303
331
  model,
304
332
  adapterId,
305
333
  providerRaw: u,
334
+ ...(reasoning !== undefined ? { segments: { reasoningTokens: reasoning } } : {}),
306
335
  }
307
336
  }
308
337
 
@@ -394,6 +423,10 @@ export function createOpenAICompatAdapter(opts: OpenAICompatOptions): LlmAdapter
394
423
  const firstByteMs = opts.firstByteMs ?? 30_000
395
424
  const retries = opts.retries ?? 2
396
425
  const backoffMs = opts.backoffMs ?? [1_000, 2_000]
426
+ // 1.14-S4(K-POOL-1):成员级 keyEnv 精确候选——keyEnv 引用即显式意见(env 通道开启;候选=仅该变量,keychain 链保持在前)
427
+ const credResolveOpts = opts.credentialEnvVars?.length
428
+ ? { envVarCandidates: opts.credentialEnvVars, allowEnvFallback: true }
429
+ : {}
397
430
 
398
431
  const toOpenAI = (req: LlmRequest, apiKey: string) => ({
399
432
  // 1.4-BUG-1:method: 'POST' 缺失——M8 最小实现潜伏缺陷(注入式 fetch 的单测忽略 method,
@@ -406,6 +439,11 @@ export function createOpenAICompatAdapter(opts: OpenAICompatOptions): LlmAdapter
406
439
  body: JSON.stringify({
407
440
  model: req.model,
408
441
  stream: true,
442
+ // 1.14-S1(K-MP-1 / D-39):思考参数——effort 非 off/缺席才下发 reasoning_effort(off = 字节面零痕迹)
443
+ ...(normalizeReasoningEffort(req.reasoning?.effort) !== undefined &&
444
+ normalizeReasoningEffort(req.reasoning?.effort) !== 'off'
445
+ ? { reasoning_effort: normalizeReasoningEffort(req.reasoning?.effort) }
446
+ : {}),
409
447
  // 1.11-S1(D-33 / 计量设计 §9-2 推荐A):usage 采集默认开启;第三方网关不兼容时的
410
448
  // 回退方案(baseUrl 白名单)登记可回退项——不支持网关走缺失口径(禁估算补造)
411
449
  stream_options: { include_usage: true },
@@ -439,7 +477,7 @@ export function createOpenAICompatAdapter(opts: OpenAICompatOptions): LlmAdapter
439
477
  buildRequest: async () => {
440
478
  let apiKey = ''
441
479
  if (opts.credentials) {
442
- const ref = opts.credentials.resolve(opts.provider ?? id, {})
480
+ const ref = opts.credentials.resolve(opts.provider ?? id, credResolveOpts)
443
481
  apiKey = opts.credentials.reveal(ref, {})
444
482
  }
445
483
  return toOpenAI(req, apiKey)
@@ -470,6 +508,8 @@ export interface AnthropicAdapterOptions {
470
508
  sleep?: (ms: number) => Promise<void>
471
509
  /** D-20:Anthropic API max_tokens 必填——req.maxTokens 缺席时取此缺省(provider 差异登记,缺省 4096) */
472
510
  defaultMaxTokens?: number
511
+ /** 1.14-S4(K-POOL-1):成员级凭据 env 变量名引用(modelPool keyEnv——resolve 精确候选;keychain 链保持在前) */
512
+ credentialEnvVars?: string[]
473
513
  }
474
514
 
475
515
  /** stop_reason → CAR finishReason(未映射 fail-visible——openai FINISH_MAP 同纪律) */
@@ -495,6 +535,8 @@ function mergeAnthropicUsage(
495
535
  ): LlmUsage | undefined {
496
536
  if (!start || !delta) return undefined
497
537
  if (typeof start.input_tokens !== 'number' || typeof delta.output_tokens !== 'number') return undefined
538
+ // 1.14-S3(K-MP-2b / D-41):anthropic 无输出细分事实(thinking 计入 output_tokens)→
539
+ // segments 不带键(禁估算补造——桌面色条退化单色,登记口径)
498
540
  return {
499
541
  inputTokens: start.input_tokens,
500
542
  outputTokens: delta.output_tokens,
@@ -545,6 +587,15 @@ function toAnthropic(req: LlmRequest, apiKey: string) {
545
587
  }
546
588
  }
547
589
  flushToolResults()
590
+ // 1.14-S1(K-MP-1 / D-39):思考参数——effort → thinking.budget_tokens(ANTHROPIC_THINKING_BUDGET 表);
591
+ // off/缺席不下发;budget ≥ max_tokens 违反 Messages API 约束(max_tokens 必须大于 budget_tokens)→
592
+ // 显式报错提示调高(fail-visible,禁静默改写 max_tokens)
593
+ const reasoningEffort = normalizeReasoningEffort(req.reasoning?.effort)
594
+ const thinkingBudget = reasoningEffort && reasoningEffort !== 'off' ? ANTHROPIC_THINKING_BUDGET[reasoningEffort] : undefined
595
+ const maxTokens = req.maxTokens ?? 4096
596
+ if (thinkingBudget !== undefined && thinkingBudget >= maxTokens) {
597
+ throw new Error(`CAR-E-LLM-REASONING: thinking budget_tokens(${thinkingBudget}) 必须小于 max_tokens(${maxTokens})——请调高 maxTokens(禁静默改写请求参数)`)
598
+ }
548
599
  return {
549
600
  method: 'POST' as const,
550
601
  headers: {
@@ -555,7 +606,8 @@ function toAnthropic(req: LlmRequest, apiKey: string) {
555
606
  body: JSON.stringify({
556
607
  model: req.model,
557
608
  stream: true,
558
- max_tokens: req.maxTokens ?? 4096,
609
+ max_tokens: maxTokens,
610
+ ...(thinkingBudget !== undefined ? { thinking: { type: 'enabled', budget_tokens: thinkingBudget } } : {}),
559
611
  ...(system.length ? { system: system.join('\n\n') } : {}),
560
612
  messages: turns,
561
613
  ...(req.tools.length
@@ -639,6 +691,10 @@ export function createAnthropicAdapter(opts: AnthropicAdapterOptions): LlmAdapte
639
691
  const firstByteMs = opts.firstByteMs ?? 30_000
640
692
  const retries = opts.retries ?? 2
641
693
  const backoffMs = opts.backoffMs ?? [1_000, 2_000]
694
+ // 1.14-S4(K-POOL-1):成员级 keyEnv 精确候选(同 openai-compat 口径)
695
+ const credResolveOpts = opts.credentialEnvVars?.length
696
+ ? { envVarCandidates: opts.credentialEnvVars, allowEnvFallback: true }
697
+ : {}
642
698
 
643
699
  return {
644
700
  id,
@@ -650,7 +706,7 @@ export function createAnthropicAdapter(opts: AnthropicAdapterOptions): LlmAdapte
650
706
  buildRequest: async () => {
651
707
  let apiKey = ''
652
708
  if (opts.credentials) {
653
- const ref = opts.credentials.resolve(opts.provider ?? id, {})
709
+ const ref = opts.credentials.resolve(opts.provider ?? id, credResolveOpts)
654
710
  apiKey = opts.credentials.reveal(ref, {})
655
711
  }
656
712
  return toAnthropic(req, apiKey)
@@ -25,6 +25,10 @@ export interface LlmRequest {
25
25
  /** 1.4-S4(D-19):turn 取消信号(runTurn 的 {aborted} 形态透传)——适配器在 attempt 前 /
26
26
  * chunk 间隙检查;外部取消 ≠ 失败(不 retry、不产 error chunk,静默收口由 chatStep 抛 TurnAborted) */
27
27
  signal?: { aborted: boolean }
28
+ /** 1.14-S1(K-MP-1 / D-39):思考参数——适配器按家族映射(openai-compat = reasoning_effort;
29
+ * anthropic = thinking.budget_tokens);**off / 缺席 = 不下发该字段**(字节面零痕迹,非「effort:none」);
30
+ * 模型思考档位(模型级思考如 R1)为宿主侧模型元数据事实源(桌面 models:meta),内核不做档位判断 */
31
+ reasoning?: { effort: 'off' | 'low' | 'medium' | 'high' }
28
32
  }
29
33
 
30
34
  /** 工具调用流式增量(跨 chunk 聚合出完整 ToolCall) */
@@ -53,6 +57,13 @@ export interface LlmUsage {
53
57
  adapterId: string
54
58
  /** provider 原始 usage 对象(anthropic 为 messageStart/messageDelta 两原始片段) */
55
59
  providerRaw: unknown
60
+ /**
61
+ * 1.14-S3(K-MP-2b / D-41):请求分段计量——仅采 provider 真实返回的分段事实(归一化后),
62
+ * **无分段事实 = 不带键(禁静态估算,桌面色条退化单色不造假)**。v1 分段 = reasoningTokens
63
+ * (openai completion_tokens_details.reasoning_tokens——K-MP-1 思考参数的计量配套,thinking
64
+ * 输出细分;anthropic thinking 计入 output_tokens 无细分事实 → 缺省不带键,登记)。
65
+ */
66
+ segments?: { reasoningTokens?: number }
56
67
  }
57
68
 
58
69
  /** 流式块(finishReason 一经产生不可被任何中间层改写) */
@@ -14,7 +14,9 @@
14
14
  import { spawn } from 'node:child_process'
15
15
 
16
16
  export type SandboxMode = 'read-only' | 'workspace-write' | 'danger-full-access'
17
- export type Capability = 'fs-write' | 'net' | 'exec' | 'mcp' | 'env-read'
17
+ // 1.13.0(K-FS-3):加法 'fs-read' | 'fs-delete'——文件安全三轴的审计对象保真(能力×资源);
18
+ // 分类纪律:fs-delete 为写类(变更面,降级态 Q-04 强制 confirm),fs-read 为只读类(五枚举既有语义不动)
19
+ export type Capability = 'fs-write' | 'net' | 'exec' | 'mcp' | 'env-read' | 'fs-read' | 'fs-delete'
18
20
 
19
21
  export interface ProbeResult {
20
22
  platform: NodeJS.Platform
@@ -64,11 +66,22 @@ export interface ExecRequest {
64
66
  /** 运行权限模式(F4):readonly/confirm/full——降级态下 full 亦受限为 confirm */
65
67
  permissionMode: 'readonly' | 'confirm' | 'full'
66
68
  authorize?: (req: ExecRequest) => Promise<boolean>
69
+ /**
70
+ * 1.13.0(K-SC-1):authorizeAll=true 将 confirm 模式的授权门扩展到非写类 argv
71
+ * (文件/命令规则轴的只读命令面——询问/拒绝规则可达);缺省 false = 既有行为零变化
72
+ * (readonly 模式契约不变:写类一律拒、只读类照常放行;full 整体旁路)。
73
+ */
74
+ authorizeAll?: boolean
67
75
  timeoutMs?: number
68
76
  }
69
77
 
70
78
  export interface ExecResult { ok: boolean; stdout: string; stderr: string; code: number | null; degraded: boolean; denied?: string }
71
79
 
80
+ /** 写类能力判定(1.13.0 收敛单点):fs-delete 为变更面归写类;fs-read 为只读类不归(K-FS-3 分类纪律) */
81
+ function isWriteish(c: Capability): boolean {
82
+ return c === 'fs-write' || c === 'net' || c === 'exec' || c === 'mcp' || c === 'fs-delete'
83
+ }
84
+
72
85
  /**
73
86
  * M2-S7 · Seatbelt profile 生成器(darwin 后端)
74
87
  *
@@ -140,7 +153,7 @@ export class SandboxExecutor {
140
153
  /** Q-04 定稿:降级态运行约束判定(在授权门之前生效) */
141
154
  effectivePermission(permissionMode: ExecRequest['permissionMode'], capabilities: Capability[]): { mode: 'readonly' | 'confirm' | 'full'; denied: string | null } {
142
155
  if (!this.degraded) return { mode: permissionMode, denied: null }
143
- const writeish = capabilities.some(c => c === 'fs-write' || c === 'net' || c === 'exec' || c === 'mcp')
156
+ const writeish = capabilities.some(c => isWriteish(c))
144
157
  if (capabilities.includes('env-read')) return { mode: 'confirm', denied: 'env-read denied in degraded sandbox(Q-04:一律拒绝)' }
145
158
  if (writeish) return { mode: 'confirm', denied: null } // full 亦受限为 confirm
146
159
  return { mode: permissionMode === 'full' ? 'confirm' : permissionMode, denied: null }
@@ -153,9 +166,11 @@ export class SandboxExecutor {
153
166
  this.#audit({ kind: 'sandbox-denied', detail: { argv: req.argv, reason: degradedDenial, degraded: true }, ts: Date.now() })
154
167
  return { ok: false, stdout: '', stderr: degradedDenial, code: -1, degraded: true, denied: degradedDenial }
155
168
  }
156
- // 权限门:写类能力在 readonly 一律拒;confirm 需审批(降级态强制 confirm)
157
- const writeish = req.capabilities.some(c => c === 'fs-write' || c === 'net' || c === 'exec' || c === 'mcp')
158
- if (writeish && effMode !== 'full') {
169
+ // 权限门:写类能力在 readonly 一律拒;confirm 需审批(降级态强制 confirm)。
170
+ // 1.13.0(K-SC-1):authorizeAll=true 且 confirm 时非写类 argv 同样进门(命令规则轴只读面)
171
+ const writeish = req.capabilities.some(c => isWriteish(c))
172
+ const gated = effMode !== 'full' && (writeish || (req.authorizeAll === true && effMode === 'confirm'))
173
+ if (gated) {
159
174
  const granted = effMode === 'confirm' ? await (req.authorize?.(req) ?? Promise.resolve(false)) : false
160
175
  if (!granted) {
161
176
  this.#audit({ kind: 'sandbox-denied', detail: { argv: req.argv, mode: effMode, degraded: this.degraded }, ts: Date.now() })
package/src/seam.ts CHANGED
@@ -12,8 +12,18 @@
12
12
  * runCode(PTC 红线单点——erasable-only/预算/授权门仅经 makePtcToolDefinition 行为面可达)、JsonRpc 编码层、
13
13
  * rawPost/parseTraceparent/formatTraceparent(trace 通道内部件)。jiti epoch 面(D-37):mountPlugin.reloadEpoch
14
14
  * 自 1.10.0 起已随本面可达,零加法登记确认。
15
+ * 加法登记:1.13.0 —— 桌面 S3 治理面(文件安全设计 §7 / S3 迭代规划 D-S3-1):K-FS-4 redact 出面(fs_read 落链前
16
+ * 脱敏——链上不得进密钥原文)+ K-S3-1 loadSessionLog/deriveSessionId 出面(审计页历史会话链校验/取证导出的
17
+ * 只读载入面;载入/校验单一权威在内核,桌面禁自实现哈希重算)+ K-S3-3 exportForensicsBundle/verifyBundle 出面
18
+ * (取证包导出 + 读回重验;断链即中止语义随本面)。恢复面 K-S3-2 零登记确认:SessionLog.fromEvents 自 M5 DEC-4
19
+ * 起已随本面可达(fork.ts 生产调用点),S2 D-S2-6 前提修正登记(S3 执行期发现)。
20
+ * 加法登记:1.14.0 —— PluginManager 装载报告面(K-S4-1 / D-43):loadPlugins 出面(六阶段流水线
21
+ * discover/verify/parse/validate/topo/register——LoadReportVO 结构化 VO,verify/topo 真值取代 v0.1 桌面
22
+ * mountPlugin 组合呈现的 SKIPPED 标注)+ ReloadManager 出面(热重载 epoch 击穿 + invalidate 语义随本面)+
23
+ * verifyPluginFile 出面(签名校验挂载点——宿主自有装载路径可复用同一签名门;minisign 轨唯一实装,sigstore
24
+ * 主轨维持 verifier 骨架期预留——D-43 裁决:登记 strict 期限不引入 sigstore 依赖,engines 硬不兼容见评估报告)。
15
25
  */
16
- export { SessionLog, type SessionEvent, type EventKind, type Actor } from './session/log.ts'
26
+ export { SessionLog, loadSessionLog, deriveSessionId, type SessionEvent, type EventKind, type Actor } from './session/log.ts'
17
27
  export { SessionFileStore } from './session/store.ts'
18
28
  export { runTurn, type TurnEndReason, type TurnPreset, type TurnResult } from './loop/stop.ts'
19
29
  export { chatStep } from './runtime-core/chatStep.ts'
@@ -34,3 +44,9 @@ export { createHttpMcpTransport, resolveMcpHeaders, type HttpMcpServerSpec } fro
34
44
  export { makePtcToolDefinition, type ToolBridge } from './ptc/runCode.ts'
35
45
  export { renderSdkFromRegistry } from './ptc/sdk.ts'
36
46
  export type { McpClientSpanHandle, PtcRelayHandle } from './runtime-core/trace.ts'
47
+ // —— 1.13.0:桌面 S3 治理面(文件安全/审计/取证)——
48
+ export { redact, type RedactionResult } from './runtime-core/redaction.ts'
49
+ export { exportForensicsBundle, verifyBundle, type ForensicsBundle, type ExportMeta } from './session/export.ts'
50
+ // —— 1.14.0(K-S4-1 / D-43):PluginManager 装载报告面 + 签名校验挂载点 ——
51
+ export { loadPlugins, ReloadManager, type LoadReportVO, type LoadPluginsResult, type LoadPluginsOptions, type LoadStage, type StageStatus, type PeerConflictChain } from './load/report.ts'
52
+ export { verifyPluginFile, type SignatureGateOptions, type SigGateResult } from './load/sigGate.ts'
@@ -0,0 +1,168 @@
1
+ /**
2
+ * 1.14-S2 · 历史压缩面(K-MP-2 / D-40)——策略器 + 执行器 + 确定性摘要
3
+ *
4
+ * 口径(红线先立后码):
5
+ * - **append-only 红线**:压缩 = 追加一条 'compact' 事件(payload 携带摘要 + 折叠前缀边界
6
+ * compactedUpTo),**不改写 / 不删除任何已落链事件**——SessionLog 无 update/delete API,
7
+ * 压缩只走 append;node:test 断言压缩前后 verifyChain() 均 null 且旧事件逐字节不变;
8
+ * - **投影语义**:compact 只影响其后请求的投影(deriveMessages last-wins),压缩前快照
9
+ * (atSeq ≤ compact.seq)投影仍为全量形态——N1 不变量(Model-visible means logged)
10
+ * 逐快照自洽,无需豁免;
11
+ * - **保护段**:首个 system 事件(会话级提示)+ 最近 N turn(protectLastTurns,缺省 4)
12
+ * 不进折叠区间;中途 system 事件按普通事件折叠(内容保留进摘要,信息不丢——D-40 执行期细化);
13
+ * - **工具对完整性(toolPairIntegrity)**:折叠边界若落在 toolCall↔toolResult 配对中间
14
+ * (call 在区间内、result 在保护段)→ 边界回退到该 toolCall(成对整体移出),循环至不动点;
15
+ * - **fail-visible**:无可压缩区间(全部在保护段 / 已压缩过)→ CAR-E-COMPACT-NORANGE 显式报错,
16
+ * 禁静默成功;摘要生成产物非非空字符串 → CAR-E-COMPACT-SUMMARY 显式报错;
17
+ * - **零新依赖**:摘要缺省为确定性生成器(轮次意图 + 工具调用统计 + 收口状态——事实拼装,
18
+ * 禁估算);宿主可注入 LLM 摘要(summarize 缝——内核不依赖 LLM 做压缩);
19
+ * - **触发面**:shouldCompact 判定(thresholdEvents 事实计数,非 token 估算)+ applyCompaction
20
+ * 立即 API;自动挂载点在宿主装配层(runTurn 之间调用,内核不改 runTurn 语义)。
21
+ */
22
+ import type { SessionEvent, SessionLog } from './log.ts'
23
+
24
+ export interface CompactionConfig {
25
+ /** 缺省 false = 行为零变化(1.13 兼容线) */
26
+ enabled?: boolean
27
+ /** 距上次压缩以来的新增事件数阈值(事实计数,非 token 估算);缺省 200 */
28
+ thresholdEvents?: number
29
+ /** 保护最近 N 个 turn 不折叠;缺省 4 */
30
+ protectLastTurns?: number
31
+ }
32
+
33
+ export interface CompactionPlan {
34
+ /** 折叠前缀边界 [0, compactedUpTo)——compact 事件 payload 原样携带 */
35
+ compactedUpTo: number
36
+ /** 折叠区间覆盖的 turn 数(去重) */
37
+ compactedTurns: number
38
+ /** 折叠区间事件引用(供摘要生成;只读) */
39
+ events: readonly SessionEvent[]
40
+ }
41
+
42
+ /** 上次压缩边界(无 compact 事件 = 0)——重复压缩不越界(幂等区间的下界) */
43
+ function lastCompactedUpTo(log: SessionLog): number {
44
+ let last = 0
45
+ for (const e of log.events) {
46
+ if (e.kind !== 'compact') continue
47
+ const p = (e.payload ?? {}) as { compactedUpTo?: unknown }
48
+ if (typeof p.compactedUpTo === 'number' && p.compactedUpTo > last) last = p.compactedUpTo
49
+ }
50
+ return last
51
+ }
52
+
53
+ /** shouldCompact:距上次压缩的新增事件数 ≥ thresholdEvents(enabled 缺省关 = 零变化) */
54
+ export function shouldCompact(log: SessionLog, cfg: CompactionConfig | undefined): boolean {
55
+ if (!cfg || cfg.enabled !== true) return false
56
+ const threshold = cfg.thresholdEvents ?? 200
57
+ return log.events.length - lastCompactedUpTo(log) >= threshold
58
+ }
59
+
60
+ /**
61
+ * planCompaction:计算折叠区间(纯函数,不落链)。
62
+ * 保护段 = 首个 system 事件 + 最近 protectLastTurns 个 turn(按 turnId 首次出现序);
63
+ * 边界回退保证 toolCall↔toolResult 成对(toolPairIntegrity);无可压缩区间显式报错。
64
+ */
65
+ export function planCompaction(log: SessionLog, protectLastTurns = 4): CompactionPlan {
66
+ const events = log.events
67
+ const floor = lastCompactedUpTo(log) // 上界不越过上次压缩边界(幂等)
68
+
69
+ // 保护起点候选:最近 N 个 turn 的首事件 seq
70
+ const turnFirst = new Map<string, number>()
71
+ for (const e of events) {
72
+ if (e.turnId === 'compaction') continue // 压缩事件自身不构成 turn
73
+ if (!turnFirst.has(e.turnId)) turnFirst.set(e.turnId, e.seq)
74
+ }
75
+ const recentTurns = [...turnFirst.entries()].slice(-protectLastTurns)
76
+ let protectStart = recentTurns.length ? Math.min(...recentTurns.map(([, seq]) => seq)) : events.length
77
+
78
+ // 统一规则:折叠区间 = [lowerBound, protectStart),lowerBound = max(上次压缩边界, 首个 system.seq + 1)
79
+ // ——首个 system(会话级提示)恒不被折叠;floor 已含它时(上次压缩边界在其后)不受限
80
+ const firstSystem = events.find(e => e.kind === 'system')
81
+ const lowerBound = firstSystem ? Math.max(floor, firstSystem.seq + 1) : floor
82
+ if (protectStart <= lowerBound) {
83
+ throw new Error(`CAR-E-COMPACT-NORANGE: 无可压缩区间(保护段 = 首个 system + 最近 ${protectLastTurns} turn,上次压缩边界 = ${floor})——禁静默成功`)
84
+ }
85
+
86
+ // toolPairIntegrity:边界落在配对中间 → 回退到 toolCall(成对整体移出),循环至不动点
87
+ for (let guard = 0; guard < 1000; guard++) {
88
+ const callIds = new Map<string, number>() // id → call seq(区间内)
89
+ for (let i = lowerBound; i < protectStart; i++) {
90
+ const e = events[i]!
91
+ if (e.kind === 'toolCall') {
92
+ const id = (e.payload as { id?: unknown } | null)?.id
93
+ if (typeof id === 'string') callIds.set(id, e.seq)
94
+ }
95
+ }
96
+ let earliestCallSeq: number | undefined
97
+ for (let i = protectStart; i < events.length; i++) {
98
+ const e = events[i]!
99
+ if (e.kind !== 'toolResult') continue
100
+ const id = (e.payload as { id?: unknown } | null)?.id
101
+ if (typeof id === 'string' && callIds.has(id)) {
102
+ const callSeq = callIds.get(id)!
103
+ if (earliestCallSeq === undefined || callSeq < earliestCallSeq) earliestCallSeq = callSeq
104
+ }
105
+ }
106
+ if (earliestCallSeq === undefined) break // 无破裂配对——边界成立
107
+ protectStart = earliestCallSeq // 成对整体移出
108
+ if (protectStart <= lowerBound) {
109
+ throw new Error(`CAR-E-COMPACT-NORANGE: 工具对回退后无区间(toolCall↔toolResult 成对完整性不可满足)——禁静默成功`)
110
+ }
111
+ }
112
+
113
+ const compacted = events.slice(lowerBound, protectStart)
114
+ const turns = new Set(compacted.map(e => e.turnId))
115
+ return { compactedUpTo: protectStart, compactedTurns: turns.size, events: compacted }
116
+ }
117
+
118
+ /** 确定性摘要(缺省生成器):事实拼装(轮次意图 + 工具统计 + 收口状态),禁估算禁 LLM 依赖 */
119
+ export function defaultSummarizer(events: readonly SessionEvent[]): string {
120
+ const cut = (v: unknown, n: number) => {
121
+ const s = typeof v === 'string' ? v : JSON.stringify(v)
122
+ return s.length > n ? s.slice(0, n) + '…' : s
123
+ }
124
+ const lines: string[] = [`[会话压缩摘要] 折叠事件 ${events.length} 条(${events[0]?.seq ?? 0}–${events.at(-1)?.seq ?? 0}):`]
125
+ const toolCounts = new Map<string, number>()
126
+ const endReasons = new Map<string, number>()
127
+ for (const e of events) {
128
+ if (e.kind === 'user') lines.push(`- [${e.turnId}] 用户:${cut(e.payload, 200)}`)
129
+ else if (e.kind === 'assistant' && typeof e.payload === 'string' && e.payload.trim()) lines.push(`- [${e.turnId}] 助手:${cut(e.payload, 160)}`)
130
+ else if (e.kind === 'toolCall') {
131
+ const p = e.payload as { tool?: unknown }
132
+ const name = typeof p?.tool === 'string' ? p.tool : '?'
133
+ toolCounts.set(name, (toolCounts.get(name) ?? 0) + 1)
134
+ } else if (e.kind === 'turnEnd') {
135
+ const r = (e.meta as { reason?: unknown } | null)?.reason
136
+ const key = typeof r === 'string' ? r : 'unknown'
137
+ endReasons.set(key, (endReasons.get(key) ?? 0) + 1)
138
+ }
139
+ }
140
+ if (toolCounts.size) {
141
+ lines.push(`工具调用:${[...toolCounts.entries()].map(([t, n]) => `${t} ×${n}`).join('、')}`)
142
+ }
143
+ if (endReasons.size) {
144
+ lines.push(`turn 收口:${[...endReasons.entries()].map(([r, n]) => `${r} ×${n}`).join('、')}`)
145
+ }
146
+ return lines.join('\n')
147
+ }
148
+
149
+ /**
150
+ * applyCompaction:执行压缩(唯一落链动作 = append 一条 'compact' 事件——原事件链不改写)。
151
+ * summarize 缝:宿主可注入 LLM 摘要(返回非空字符串;缺省确定性生成器)。
152
+ */
153
+ export function applyCompaction(
154
+ log: SessionLog,
155
+ opts: { protectLastTurns?: number; turnId?: string; summarize?: (events: readonly SessionEvent[]) => string } = {},
156
+ ): SessionEvent {
157
+ const plan = planCompaction(log, opts.protectLastTurns ?? 4)
158
+ const summary = opts.summarize ? opts.summarize(plan.events) : defaultSummarizer(plan.events)
159
+ if (typeof summary !== 'string' || !summary.trim().length) {
160
+ throw new Error(`CAR-E-COMPACT-SUMMARY: 摘要生成产物为空(fail-visible,禁静默折叠空摘要)`)
161
+ }
162
+ return log.append('runtime', 'compact', opts.turnId ?? 'compaction', {
163
+ summary,
164
+ compactedUpTo: plan.compactedUpTo,
165
+ compactedTurns: plan.compactedTurns,
166
+ compactedEvents: plan.events.length,
167
+ })
168
+ }
@@ -11,7 +11,7 @@ import { createHash } from 'node:crypto'
11
11
  import { readFileSync } from 'node:fs'
12
12
  import { decodeLogBuffer, splitJsonlLines } from './format.ts'
13
13
 
14
- export type EventKind = 'user' | 'assistant' | 'toolCall' | 'toolResult' | 'turnEnd' | 'goalUpdate' | 'hostRaw' | 'fork' | 'system' | 'usage' // M3-S11 加法扩展:多宿主归一化降级通道(pattern 未命中留痕,零静默);M5-DEC4 加法扩展:跨宿主 fork 标记事件;1.4-S2 加法扩展:system prompt 落链(§3.5.5:新模型可见输入 = 新事件类型);1.11-S2 加法扩展:usage 计量事实(D-33,actor='runtime'、model_visible=0 不进模型消息流,deriveMessages 不投影);六值 TurnEndReason 不变
14
+ export type EventKind = 'user' | 'assistant' | 'toolCall' | 'toolResult' | 'turnEnd' | 'goalUpdate' | 'hostRaw' | 'fork' | 'system' | 'usage' | 'compact' | 'routing' // M3-S11 加法扩展:多宿主归一化降级通道(pattern 未命中留痕,零静默);M5-DEC4 加法扩展:跨宿主 fork 标记事件;1.4-S2 加法扩展:system prompt 落链(§3.5.5:新模型可见输入 = 新事件类型);1.11-S2 加法扩展:usage 计量事实(D-33,actor='runtime'、model_visible=0 不进模型消息流,deriveMessages 不投影);六值 TurnEndReason 不变;1.14-S2 加法扩展(K-MP-2 / D-40):历史压缩事件(actor='runtime',append-only 只追加——payload={summary, compactedUpTo, ...},投影按 compactedUpTo 前缀折叠为摘要,原事件链不改写);1.14-S4 加法扩展(K-POOL-1 / D-42):模型池路由决策(actor='runtime',payload.type='decide',selected/reason/retries/attempts——禁静默换模型;model_visible=0 不进模型消息流,deriveMessages 不投影——同 usage 纪律)
15
15
  export type Actor = 'user' | 'model' | 'plugin' | 'runtime'
16
16
  export interface SessionEvent {
17
17
  seq: number
@@ -91,17 +91,33 @@ export class SessionLog {
91
91
  * 模型可见流只保留最终结果(真实 provider 对重复 tool_call_id/tool_use_id 拒绝或未定义,
92
92
  * 1.4 观察项经真实适配器落地升级为缺陷面)。id 缺席不收敛(hostRaw/无 id 载荷形态不变,
93
93
  * golden ④ 投影语义兼容);授权拒绝单条即唯一结果原样保留。适配器侧零改动(消费已收敛上游)。
94
+ * 1.14-S2(K-MP-2 / D-40):compact 事件投影——scoped 内最后一条 compact 生效(last-wins),
95
+ * 其 compactedUpTo 前缀折叠为单条 system 摘要(事件原文不改写,仅投影层折叠);compact 之前的
96
+ * 请求快照(atSeq ≤ compact.seq)不含 compact 事件 → 投影仍为全量形态(N1 不变量自洽——
97
+ * 「请求时发出 = 日志前缀投影」逐快照成立);无 compact 事件的日志投影与 1.13 逐字节一致。
94
98
  */
95
99
  deriveMessages(upTo?: number): Array<{ role: string; [k: string]: unknown }> {
96
100
  const scoped = upTo !== undefined ? this.events.slice(0, upTo) : this.events
101
+ let compactEvent: SessionEvent | undefined
102
+ for (const e of scoped) { if (e.kind === 'compact') compactEvent = e } // last-wins
103
+ let compactedUpTo: number | undefined
104
+ if (compactEvent) {
105
+ const cp = (compactEvent.payload ?? {}) as { summary?: unknown; compactedUpTo?: unknown }
106
+ if (typeof cp.summary !== 'string' || !cp.summary.length || typeof cp.compactedUpTo !== 'number') {
107
+ throw new Error(`CAR-E-COMPACT-PAYLOAD: compact 事件 seq=${compactEvent.seq} 缺有效 summary/compactedUpTo(投影拒绝——fail-visible)`)
108
+ }
109
+ compactedUpTo = cp.compactedUpTo // 折叠边界 = payload 声明的前缀边界(非 compact 事件自身 seq)
110
+ }
111
+ const projected = compactedUpTo !== undefined ? scoped.filter(e => e.seq >= compactedUpTo!) : scoped
97
112
  const lastSeqById = new Map<string, number>()
98
- for (const e of scoped) {
113
+ for (const e of projected) {
99
114
  if (e.kind !== 'toolResult') continue
100
115
  const id = (e.payload as { id?: unknown } | null)?.id
101
116
  if (typeof id === 'string') lastSeqById.set(id, e.seq)
102
117
  }
103
118
  const out: Array<{ role: string; [k: string]: unknown }> = []
104
- for (const e of scoped) {
119
+ if (compactEvent) out.push({ role: 'system', content: ((compactEvent.payload as { summary: string }).summary) })
120
+ for (const e of projected) {
105
121
  if (e.kind === 'system') out.push({ role: 'system', content: e.payload })
106
122
  else if (e.kind === 'user') out.push({ role: 'user', content: e.payload })
107
123
  else if (e.kind === 'assistant') out.push({ role: 'assistant', content: e.payload })
Binary file
@@ -1 +0,0 @@
1
- {"base64Signature":"MEUCIA0UBnqeM+TPTKLaxwsFDiHv1WnpL4aJ5GHqxXzLSbAyAiEAneTvQOA349AWwfG/eCWATHXznws2AYYf/EXE/cVQrGU=","cert":"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUc1akNDQm0yZ0F3SUJBZ0lVV204L2E1cGFQWmpYcXRXVmRoUnlDeWMrZk1Jd0NnWUlLb1pJemowRUF3TXcKTnpFVk1CTUdBMVVFQ2hNTWMybG5jM1J2Y21VdVpHVjJNUjR3SEFZRFZRUURFeFZ6YVdkemRHOXlaUzFwYm5SbApjbTFsWkdsaGRHVXdIaGNOTWpZeE1EQTNNRGswTURVMFdoY05Nall4TURBM01EazFNRFUwV2pBQU1Ga3dFd1lICktvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUVXMFVLYVFXcUF5NjMvZlB4alo4ak83Yndtekk5WmlUNkEwN1YKTXZxTzlxdnJZQjloR2tZaGNWRDcza25jdUpacmQrVkhNL3BJNmEwWDZjR290QzNjNGFPQ0JZd3dnZ1dJTUE0RwpBMVVkRHdFQi93UUVBd0lIZ0RBVEJnTlZIU1VFRERBS0JnZ3JCZ0VGQlFjREF6QWZCZ05WSFNNRUdEQVdnQlRmCjArblBWaVFSbHZtbzJPa29WYUxHTGhoa1B6QmlCZ05WSFJFQkFmOEVXREJXaGxSb2RIUndjem92TDJkcGRHaDEKWWk1amIyMHZiSEZqTmpJMEwyTmhjaTF5ZFc1MGFXMWxMeTVuYVhSb2RXSXZkMjl5YTJac2IzZHpMM0psYkdWaApjMlV1ZVcxc1FISmxabk12ZEdGbmN5OHhMakV5TGpBd09RWUtLd1lCQkFHRHZ6QUJBUVFyYUhSMGNITTZMeTkwCmIydGxiaTVoWTNScGIyNXpMbWRwZEdoMVluVnpaWEpqYjI1MFpXNTBMbU52YlRBVkJnb3JCZ0VFQVlPL01BRUMKQkFkeVpXeGxZWE5sTURZR0Npc0dBUVFCZzc4d0FRTUVLRFJtWldVMlpHRXpNbVZtT1dReU9EbGpaV0k0TUdObQpZekZsTnpNNE0yUXdPVFZpWVRoa05qa3dGUVlLS3dZQkJBR0R2ekFCQkFRSGNtVnNaV0Z6WlRBZ0Jnb3JCZ0VFCkFZTy9NQUVGQkJKc2NXTTJNalF2WTJGeUxYSjFiblJwYldVd0hnWUtLd1lCQkFHRHZ6QUJCZ1FRY21WbWN5OTAKWVdkekx6RXVNVEl1TURBN0Jnb3JCZ0VFQVlPL01BRUlCQzBNSzJoMGRIQnpPaTh2ZEc5clpXNHVZV04wYVc5dQpjeTVuYVhSb2RXSjFjMlZ5WTI5dWRHVnVkQzVqYjIwd1pBWUtLd1lCQkFHRHZ6QUJDUVJXREZSb2RIUndjem92CkwyZHBkR2gxWWk1amIyMHZiSEZqTmpJMEwyTmhjaTF5ZFc1MGFXMWxMeTVuYVhSb2RXSXZkMjl5YTJac2IzZHoKTDNKbGJHVmhjMlV1ZVcxc1FISmxabk12ZEdGbmN5OHhMakV5TGpBd09BWUtLd1lCQkFHRHZ6QUJDZ1FxRENnMApabVZsTm1SaE16SmxaamxrTWpnNVkyVmlPREJqWm1NeFpUY3pPRE5rTURrMVltRTRaRFk1TUIwR0Npc0dBUVFCCmc3OHdBUXNFRHd3TloybDBhSFZpTFdodmMzUmxaREExQmdvckJnRUVBWU8vTUFFTUJDY01KV2gwZEhCek9pOHYKWjJsMGFIVmlMbU52YlM5c2NXTTJNalF2WTJGeUxYSjFiblJwYldVd09BWUtLd1lCQkFHRHZ6QUJEUVFxRENnMApabVZsTm1SaE16SmxaamxrTWpnNVkyVmlPREJqWm1NeFpUY3pPRE5rTURrMVltRTRaRFk1TUNBR0Npc0dBUVFCCmc3OHdBUTRFRWd3UWNtVm1jeTkwWVdkekx6RXVNVEl1TURBYUJnb3JCZ0VFQVlPL01BRVBCQXdNQ2pFek5qRXgKT1RJM01UUXdLUVlLS3dZQkJBR0R2ekFCRUFRYkRCbG9kSFJ3Y3pvdkwyZHBkR2gxWWk1amIyMHZiSEZqTmpJMApNQmdHQ2lzR0FRUUJnNzh3QVJFRUNnd0lOamt5TXpRMU5qUXdaQVlLS3dZQkJBR0R2ekFCRWdSV0RGUm9kSFJ3CmN6b3ZMMmRwZEdoMVlpNWpiMjB2YkhGak5qSTBMMk5oY2kxeWRXNTBhVzFsTHk1bmFYUm9kV0l2ZDI5eWEyWnMKYjNkekwzSmxiR1ZoYzJVdWVXMXNRSEpsWm5NdmRHRm5jeTh4TGpFeUxqQXdPQVlLS3dZQkJBR0R2ekFCRXdRcQpEQ2cwWm1WbE5tUmhNekpsWmpsa01qZzVZMlZpT0RCalptTXhaVGN6T0ROa01EazFZbUU0WkRZNU1CY0dDaXNHCkFRUUJnNzh3QVJRRUNRd0hjbVZzWldGelpUQlpCZ29yQmdFRUFZTy9NQUVWQkVzTVNXaDBkSEJ6T2k4dloybDAKYUhWaUxtTnZiUzlzY1dNMk1qUXZZMkZ5TFhKMWJuUnBiV1V2WVdOMGFXOXVjeTl5ZFc1ekx6TTNOakF5TWpZeQpOakl4TDJGMGRHVnRjSFJ6THpFd0ZnWUtLd1lCQkFHRHZ6QUJGZ1FJREFad2RXSnNhV013VUFZS0t3WUJCQUdECnZ6QUJHQVJDREVCeVpYQnZPbXh4WXpZeU5FQTJPVEl6TkRVMk5DOWpZWEl0Y25WdWRHbHRaVUF4TXpZeE1Ua3kKTnpFME9uSmxaanB5WldaekwzUmhaM012TVM0eE1pNHdNSUdLQmdvckJnRUVBZFo1QWdRQ0JId0VlZ0I0QUhZQQozVDB3YXNiSEVUSmpHUjRjbVdjM0FxSktYcmplUEszL2g0cHlnQzhwN280QUFBR2hGYnpBd1FBQUJBTUFSekJGCkFpQTNSUk0xdk1iZDBmVUU0VUlvMjFYaUo5a0lMYThlUnE1VlBpaHpHcGNybmdJaEFKZlNzQ01GVGpFYk11L0kKdmh1L3VnR1l4d3J2S0dZcGpXVytKUnBtUDF3WU1Bb0dDQ3FHU000OUJBTURBMmNBTUdRQ01Id3FjdWp4Nm96KwpibTlwZ1E1SWZoTXZmVW1qWUdlaG5vL3EvSU9wSnhsU0hESStxSTljc0lENFltUDhGLzdVa1FJd1ZnMCs3UmFJClRncnR1V2laaFhTQWprdmgvdUU1clhZMUdiZ0FQOGd3RUxhdmFCMTdhbXBORXBhU3Q3Nkw0Tzk0Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K","rekorBundle":{"SignedEntryTimestamp":"MEUCIQDK0u1UYH5gd1wwowDNOGkRWm7g3kMOt76q+73QtUfU1gIgI8/8XsM4m3rnPXnGT9A9PmHLMpheIYj76cODbAeO9Ow=","Payload":{"body":"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiJhM2Q5N2YzZmE3ZWI0NTViYmEwZmVkNzllZDY4NGUwYTMxNGQ5YjMzZWQ3NWVjNGNhN2EzMDAzNzA1ODgxZjEyIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FVUNJQTBVQm5xZU0rVFBUS0xheHdzRkRpSHYxV25wTDRhSjVHSHF4WHpMU2JBeUFpRUFuZVR2UU9BMzQ5QVd3ZkcvZUNXQVRIWHpud3MyQVlZZi9FWEUvY1ZRckdVPSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVYzFha05EUW0weVowRjNTVUpCWjBsVlYyMDRMMkUxY0dGUVdtcFljWFJYVm1Sb1VubERlV01yWmsxSmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcFplRTFFUVROTlJHc3dUVVJWTUZkb1kwNU5hbGw0VFVSQk0wMUVhekZOUkZVd1YycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZYTUZWTFlWRlhjVUY1TmpNdlpsQjRhbG80YWs4M1luZHRla2s1V21sVU5rRXdOMVlLVFhaeFR6bHhkbkpaUWpsb1IydFphR05XUkRjemEyNWpkVXBhY21RclZraE5MM0JKTm1Fd1dEWmpSMjkwUXpOak5HRlBRMEpaZDNkbloxZEpUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXWkNaMDVXU0ZOTlJVZEVRVmRuUWxSbUNqQXJibEJXYVZGU2JIWnRiekpQYTI5V1lVeEhUR2hvYTFCNlFtbENaMDVXU0ZKRlFrRm1PRVZYUkVKWGFHeFNiMlJJVW5kamVtOTJUREprY0dSSGFERUtXV2sxYW1JeU1IWmlTRVpxVG1wSk1Fd3lUbWhqYVRGNVpGYzFNR0ZYTVd4TWVUVnVZVmhTYjJSWFNYWmtNamw1WVRKYWMySXpaSHBNTTBwc1lrZFdhQXBqTWxWMVpWY3hjMUZJU214YWJrMTJaRWRHYm1ONU9IaE1ha1Y1VEdwQmQwOVJXVXRMZDFsQ1FrRkhSSFo2UVVKQlVWRnlZVWhTTUdOSVRUWk1lVGt3Q21JeWRHeGlhVFZvV1ROU2NHSXlOWHBNYldSd1pFZG9NVmx1Vm5wYVdFcHFZakkxTUZwWE5UQk1iVTUyWWxSQlZrSm5iM0pDWjBWRlFWbFBMMDFCUlVNS1FrRmtlVnBYZUd4WldFNXNUVVJaUjBOcGMwZEJVVkZDWnpjNGQwRlJUVVZMUkZKdFdsZFZNbHBIUlhwTmJWWnRUMWRSZVU5RWJHcGFWMGswVFVkT2JRcFpla1pzVG5wTk5FMHlVWGRQVkZacFdWUm9hMDVxYTNkR1VWbExTM2RaUWtKQlIwUjJla0ZDUWtGUlNHTnRWbk5hVjBaNldsUkJaMEpuYjNKQ1owVkZDa0ZaVHk5TlFVVkdRa0pLYzJOWFRUSk5hbEYyV1RKR2VVeFlTakZpYmxKd1lsZFZkMGhuV1V0TGQxbENRa0ZIUkhaNlFVSkNaMUZSWTIxV2JXTjVPVEFLV1Zka2VreDZSWFZOVkVsMVRVUkJOMEpuYjNKQ1owVkZRVmxQTDAxQlJVbENRekJOU3pKb01HUklRbnBQYVRoMlpFYzVjbHBYTkhWWlYwNHdZVmM1ZFFwamVUVnVZVmhTYjJSWFNqRmpNbFo1V1RJNWRXUkhWblZrUXpWcVlqSXdkMXBCV1V0TGQxbENRa0ZIUkhaNlFVSkRVVkpYUkVaU2IyUklVbmRqZW05MkNrd3laSEJrUjJneFdXazFhbUl5TUhaaVNFWnFUbXBKTUV3eVRtaGphVEY1WkZjMU1HRlhNV3hNZVRWdVlWaFNiMlJYU1haa01qbDVZVEphYzJJelpIb0tURE5LYkdKSFZtaGpNbFYxWlZjeGMxRklTbXhhYmsxMlpFZEdibU41T0hoTWFrVjVUR3BCZDA5QldVdExkMWxDUWtGSFJIWjZRVUpEWjFGeFJFTm5NQXBhYlZac1RtMVNhRTE2U214YWFteHJUV3BuTlZreVZtbFBSRUpxV20xTmVGcFVZM3BQUkU1clRVUnJNVmx0UlRSYVJGazFUVUl3UjBOcGMwZEJVVkZDQ21jM09IZEJVWE5GUkhkM1Rsb3liREJoU0ZacFRGZG9kbU16VW14YVJFRXhRbWR2Y2tKblJVVkJXVTh2VFVGRlRVSkRZMDFLVjJnd1pFaENlazlwT0hZS1dqSnNNR0ZJVm1sTWJVNTJZbE01YzJOWFRUSk5hbEYyV1RKR2VVeFlTakZpYmxKd1lsZFZkMDlCV1V0TGQxbENRa0ZIUkhaNlFVSkVVVkZ4UkVObk1BcGFiVlpzVG0xU2FFMTZTbXhhYW14clRXcG5OVmt5Vm1sUFJFSnFXbTFOZUZwVVkzcFBSRTVyVFVSck1WbHRSVFJhUkZrMVRVTkJSME5wYzBkQlVWRkNDbWMzT0hkQlVUUkZSV2QzVVdOdFZtMWplVGt3V1Zka2VreDZSWFZOVkVsMVRVUkJZVUpuYjNKQ1owVkZRVmxQTDAxQlJWQkNRWGROUTJwRmVrNXFSWGdLVDFSSk0wMVVVWGRMVVZsTFMzZFpRa0pCUjBSMmVrRkNSVUZSWWtSQ2JHOWtTRkozWTNwdmRrd3laSEJrUjJneFdXazFhbUl5TUhaaVNFWnFUbXBKTUFwTlFtZEhRMmx6UjBGUlVVSm5OemgzUVZKRlJVTm5kMGxPYW10NVRYcFJNVTVxVVhkYVFWbExTM2RaUWtKQlIwUjJla0ZDUldkU1YwUkdVbTlrU0ZKM0NtTjZiM1pNTW1Sd1pFZG9NVmxwTldwaU1qQjJZa2hHYWs1cVNUQk1NazVvWTJreGVXUlhOVEJoVnpGc1RIazFibUZZVW05a1YwbDJaREk1ZVdFeVduTUtZak5rZWt3elNteGlSMVpvWXpKVmRXVlhNWE5SU0Vwc1dtNU5kbVJIUm01amVUaDRUR3BGZVV4cVFYZFBRVmxMUzNkWlFrSkJSMFIyZWtGQ1JYZFJjUXBFUTJjd1dtMVdiRTV0VW1oTmVrcHNXbXBzYTAxcVp6VlpNbFpwVDBSQ2FscHRUWGhhVkdONlQwUk9hMDFFYXpGWmJVVTBXa1JaTlUxQ1kwZERhWE5IQ2tGUlVVSm5OemgzUVZKUlJVTlJkMGhqYlZaeldsZEdlbHBVUWxwQ1oyOXlRbWRGUlVGWlR5OU5RVVZXUWtWelRWTlhhREJrU0VKNlQyazRkbG95YkRBS1lVaFdhVXh0VG5aaVV6bHpZMWROTWsxcVVYWlpNa1o1VEZoS01XSnVVbkJpVjFWMldWZE9NR0ZYT1hWamVUbDVaRmMxZWt4NlRUTk9ha0Y1VFdwWmVRcE9ha2w0VERKR01HUkhWblJqU0ZKNlRIcEZkMFpuV1V0TGQxbENRa0ZIUkhaNlFVSkdaMUZKUkVGYWQyUlhTbk5oVjAxM1ZVRlpTMHQzV1VKQ1FVZEVDblo2UVVKSFFWSkRSRVZDZVZwWVFuWlBiWGg0V1hwWmVVNUZRVEpQVkVsNlRrUlZNazVET1dwWldFbDBZMjVXZFdSSGJIUmFWVUY0VFhwWmVFMVVhM2tLVG5wRk1FOXVTbXhhYW5CNVdsZGFla3d6VW1oYU0wMTJUVk0wZUUxcE5IZE5TVWRMUW1kdmNrSm5SVVZCWkZvMVFXZFJRMEpJZDBWbFowSTBRVWhaUVFvelZEQjNZWE5pU0VWVVNtcEhValJqYlZkak0wRnhTa3RZY21wbFVFc3pMMmcwY0hsblF6aHdOMjgwUVVGQlIyaEdZbnBCZDFGQlFVSkJUVUZTZWtKR0NrRnBRVE5TVWsweGRrMWlaREJtVlVVMFZVbHZNakZZYVVvNWEwbE1ZVGhsVW5FMVZsQnBhSHBIY0dOeWJtZEphRUZLWmxOelEwMUdWR3BGWWsxMUwwa0tkbWgxTDNWblIxbDRkM0oyUzBkWmNHcFhWeXRLVW5CdFVERjNXVTFCYjBkRFEzRkhVMDAwT1VKQlRVUkJNbU5CVFVkUlEwMUlkM0ZqZFdwNE5tOTZLd3BpYlRsd1oxRTFTV1pvVFhabVZXMXFXVWRsYUc1dkwzRXZTVTl3U25oc1UwaEVTU3R4U1RsamMwbEVORmx0VURoR0x6ZFZhMUZKZDFabk1DczNVbUZKQ2xSbmNuUjFWMmxhYUZoVFFXcHJkbWd2ZFVVMWNsaFpNVWRpWjBGUU9HZDNSVXhoZG1GQ01UZGhiWEJPUlhCaFUzUTNOa3cwVHprMENpMHRMUzB0UlU1RUlFTkZVbFJKUmtsRFFWUkZMUzB0TFMwSyJ9fX19","integratedTime":1791366054,"logIndex":3128454895,"logID":"c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d"}}}