Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion doc/architecture/SPEC/kb_service.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# kb_service/ —— 规范

<!-- verified-against: 2026-10-05 -->
<!-- verified-against: 2026-10-06 -->

`知识服务核心:仓库配置、账本、outbox、CLI · refactor-status: new`

Expand Down Expand Up @@ -886,3 +886,26 @@ Zcode 模型角色中的 `effort` 被 transport 忽略,不能代替该设置
未发布的旧检查点缺少此审计时只补审,复用完整扫描和首次评审;prepared 产物缺少审计时
阻止发布,保留原产物。已发布历史目录保持不可变。审计预算中断保存已完成批次,恢复时
不重复已完成的审计。显式 `--retry-unfinished` 不重置已用完的三次内容修正,仅恢复通道故障。

## 2026-10-06 订阅模式基础知识并发

仅显式 `knowledge --unlimited-subscription` 启用基础知识工作池。
`KB_KNOWLEDGE_CONCURRENCY` 默认为 13,允许 1–13,并纳入阶段输入身份;
旧有有限预算模式保持顺序执行。每个 owner/feature 的 GLM 提取及原六个独立
Codex 区块评审在同一 worker 中顺序运行,整个池同时最多 13 个模型调用。原生 Zcode 调用复用共享节流器,默认每 5 秒启动一次;
`KB_KNOWLEDGE_START_INTERVAL_S` 可选有限正数且不超过 60 秒,同样冻结在输入身份中。
worker 使用独立知识视图、阶段记录和订阅传输;唯一协调器保存检查点,
按既定任务顺序追加精确获批区块,重新核对源码证据、正文摘要及原生评审记录,
不会重新调用模型来组装。单项通道失败保留其真实状态和已认可成果,其余任务继续。
恢复绑定原阶段输入身份与原生归档,不生成任何替代前置阶段记录。

新发现的正式功能优先使用已冻结发现报告中的完整源码行段,仍受提示词字节
预算限制;不可完整展示的行段显式保留为缺口。引用必须落在实际展示的单个区间
中,不以最大行号冒充中间未读取的内容。旧功能仍使用原来源选择及覆盖判定,
已覆盖页不重新提取。预算预留/结算采用原子操作;订阅费用未报告仍为未知。

完成但尚未发布的订阅知识缓存及已准备发布的恢复也必须先做只读原生证明复核。
复核绑定原输入身份、源码 pin、KB 基点、有效 owner、两个模型的完整归档及
精确正文/证据。失败在任何记录或发布写入前拒绝,恢复原归档后可以继续同一
已准备的发布;已发布的历史记录保持不变。已有订阅工作检查点的输入发生变化
时须建立新批次,不能静默覆盖或重新抽取。
19 changes: 19 additions & 0 deletions plugins/infermatrix-copilot/skills/imkbinit/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -326,3 +326,22 @@ a dry run, otherwise the opened PR). Summarise:

Without explicit authorization for a broader batch or merge, stop after one stage. The owner reviews and merges its PR before the next
stage can run. Never run two stages in one invocation, and never merge a PR.

### Foundation concurrency (2026-10-06)

Explicit `--stage knowledge --unlimited-subscription` shares at most 13 worker
slots across extraction and all existing independent section judgments. Set
`KB_KNOWLEDGE_CONCURRENCY` to 1–13 before starting; it is frozen in the input
identity. Native Zcode starts share a default 5-second pacing interval;
`KB_KNOWLEDGE_START_INTERVAL_S` may override it with a finite positive value
up to 60 seconds and is also bound to the stage identity. The CLI coordinator alone saves real worker checkpoints and appends
approved sections in deterministic order. Resume the same command/state to reuse
completed native approvals; keep their external trace archives. Finite-budget
runs retain the previous serial flow. New discovery features preferentially read
the frozen report’s exact complete source spans; unread gaps cannot be cited.

Completed unpublished caches and prepared-publication resumes also replay the
original native proofs before any write or model call. Missing or changed
archives reject reuse without changing the genuine record; restore the original
archive to continue. Changed frozen inputs require a new batch. Published
historical records remain unchanged.
25 changes: 15 additions & 10 deletions src/infermatrix_copilot/kb_service/init_budget.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import json
import math
import os
import threading
from contextlib import contextmanager
from dataclasses import dataclass
from typing import Iterator, Mapping
Expand Down Expand Up @@ -128,7 +129,7 @@ def charge(self, actual: float | None) -> None:

class Budget:
"""A finite stage ceiling, or explicit None for uncapped accounting.
Not thread-safe (stages call models one at a time)."""
Reservations are atomic; independent native calls may share one budget."""

def __init__(self, limit_usd: float | None, *, spent_usd: float = 0.0):
if limit_usd is not None and (not math.isfinite(limit_usd) or limit_usd <= 0):
Expand All @@ -138,10 +139,12 @@ def __init__(self, limit_usd: float | None, *, spent_usd: float = 0.0):
self.limit_usd = float(limit_usd) if limit_usd is not None else None
self.spent_usd = float(spent_usd)
self._reserved = 0.0
self._lock = threading.RLock()

@property
def remaining_usd(self) -> float | None:
return None if self.limit_usd is None else self.limit_usd - self.spent_usd - self._reserved
with self._lock:
return None if self.limit_usd is None else self.limit_usd - self.spent_usd - self._reserved

def can_reserve(self, amount: float) -> bool:
"""An explicit None ceiling keeps accounting without limiting calls."""
Expand All @@ -156,14 +159,16 @@ def reserve(self, amount: float) -> Iterator[Reservation]:
call when it does not fit. On exit the charge (or, when the block did
not charge, e.g. because the call raised, the whole reservation) is
added to what was spent."""
if not self.can_reserve(amount):
raise BudgetExhausted(
f"reserving ${amount:.4f} would exceed the budget "
f"(spent ${self.spent_usd:.4f} of ${self.limit_usd:.2f})")
reservation = Reservation(amount)
self._reserved += amount
with self._lock:
if not self.can_reserve(amount):
raise BudgetExhausted(
f"reserving ${amount:.4f} would exceed the budget "
f"(spent ${self.spent_usd:.4f} of ${self.limit_usd:.2f})")
reservation = Reservation(amount)
self._reserved += amount
try:
yield reservation
finally:
self._reserved -= amount
self.spent_usd += reservation.charged if reservation.charged is not None else amount
with self._lock:
self._reserved -= amount
self.spent_usd += reservation.charged if reservation.charged is not None else amount
Loading
Loading