fix(worker): 恢复保留错误的第一因分类
build / build (push) Successful in 1m38s

恢复 terminalLibraryCaptureError,但仅限仍存在的 missing library generated
question index 和 empty library index content;已恢复写入者时走
jobs.Permanent(validation_failed),保留原消息。已取消的正文索引覆盖 marker
和网络超时仍重试。部署脚本另以新包提供旧基线稳定性采样;不恢复 R3f 已取消
的正文索引/父类型规则。

验证:worker 保留错误 RED→GREEN、v071、Go 全量、make verify 17 项。
This commit is contained in:
姚章浩
2026-11-05 18:48:53 +08:00
parent eaa388f76b
commit 77bbc3ac6b
3 changed files with 108 additions and 3 deletions
+21 -2
View File
@@ -1,5 +1,24 @@
# Changelog
## 开发快照 0.1.364(恢复保留错误的第一因分类;部署基线稳定性)
### Fixed
- worker 恢复 `terminalLibraryCaptureError`,仅保留仍存在的两个确定性 marker:
`missing library generated question index`、`empty library index content`。已恢复
写入者后走 `jobs.Permanent(validation_failed)`,保留原错误消息;已取消的正文
索引覆盖 marker 和网络超时仍可重试,不再恢复推断门槛。
- 本机部署 preflight 不再用 lifetime `RestartCount=0` 作为旧基线稳定性定义:
绑定容器 ID/image/env/mount/StartedAt/RestartCount,间隔采样确认 running、
StartedAt/RestartCount 不变;历史非零但稳定允许,采样变化/切换实例拒绝。
新 api/worker/web 容器仍要求 running、ready、零重启。
### Tests
- 保留错误分类 RED→GREEN:`r3g-terminal-red.txt` / `r3g-terminal-green.txt`。
- 部署 strict fake 增加旧基线非零稳定正例、RestartCount 递增拒绝、实例 ID 切换
拒绝;本机候选包重新绑定最终 SHA。
## 开发快照 0.1.363(取消封存索引推断规则,原样捕获源集合)
### Removed
@@ -10,8 +29,8 @@
- 删除 `parent_chunk_id` 的类型搭配门槛:不再要求 parent 只能是 `parent_text`,也不
对 summary/OCR/caption 指定 parent 类型。保留父引用存在、同 knowledge 等实际引用
完整性;不补关联、不改源数据。
- 同步移除该推断在 preflight/archive/load/replay/writeset 的共用校验和已无用途的
worker 永久失败分类;旧快照、旧失败记录原样保留,不篡改历史状态。
- 同步移除该推断在 preflight/archive/load/replay/writeset 的共用校验和当时已无
用途的 worker 永久失败分类;旧快照、旧失败记录原样保留,不篡改历史状态。
- 0.1.362 的索引覆盖门槛未部署,现被本版本取消;0.1.362 原文保留为历史,不再作为
当前契约。
@@ -0,0 +1,65 @@
package main
import (
"errors"
"strings"
"testing"
"knowledge-governance/internal/jobs"
)
// R3g: only retained deterministic capture failures become permanent after the
// agent has recovered the writer. Cancelled body-index coverage markers are
// deliberately absent.
func TestRetainedLibraryCaptureFailuresArePermanent(t *testing.T) {
for _, marker := range []string{
"missing library generated question index",
"empty library index content",
} {
t.Run(marker, func(t *testing.T) {
original := errors.New("LIBRARYCTL_FAILED: " + marker + ";已执行 recover 并确认写入者恢复")
got := terminalLibraryCaptureError(original)
terminal, code := jobs.Terminal(got)
if !terminal || code != jobs.ErrCodeValidationFailed {
t.Fatalf("retained validation failure must park immediately: terminal=%v code=%q err=%v", terminal, code, got)
}
if got.Error() != original.Error() {
t.Fatalf("original message changed: got %q want %q", got.Error(), original.Error())
}
})
}
}
func TestCancelledCoverageFailureStaysRetryable(t *testing.T) {
for _, marker := range []string{
"unverified legacy library chunk index coverage",
"missing indexed library chunk",
} {
err := terminalLibraryCaptureError(errors.New(marker + ";已执行 recover 并确认写入者恢复"))
if terminal, code := jobs.Terminal(err); terminal {
t.Fatalf("cancelled marker %q must not be terminal; code=%q", marker, code)
}
}
}
func TestTransientLibraryCaptureFailureStaysRetryable(t *testing.T) {
for _, message := range []string{
"library agent is unreachable",
"context deadline exceeded",
`LIBRARYCTL_FAILED: job "lib-internal-x" already exists with state "failed";已执行 recover 并确认写入者恢复`,
"missing library generated question index; writer restore not confirmed",
} {
got := terminalLibraryCaptureError(errors.New(message))
if terminal, code := jobs.Terminal(got); terminal {
t.Fatalf("%q must stay retryable, got terminal code=%q", message, code)
}
}
}
func TestRetainedFailureDoesNotChangeMessage(t *testing.T) {
original := errors.New("LIBRARYCTL_FAILED: missing library generated question index;已执行 recover 并确认写入者恢复")
got := terminalLibraryCaptureError(original)
if !strings.Contains(got.Error(), "missing library generated question index") {
t.Fatalf("lost original cause: %v", got)
}
}
+22 -1
View File
@@ -101,6 +101,27 @@ func stageSizesFrom(manifest librarysnapshot.Manifest) map[string]stageSize {
// libraryCaptureJobType is shared with the API which enqueues it.
const libraryCaptureJobType = "library.capture"
// Retained deterministic source-content failures: the agent has already
// restored the writer, so retrying the same journal only replaces the first
// cause with "already exists". Body-index coverage markers are intentionally
// absent; that inferred rule was removed in R3f.
var terminalLibraryCaptureMarkers = []string{
"missing library generated question index",
"empty library index content",
}
func terminalLibraryCaptureError(err error) error {
if err == nil || !strings.Contains(err.Error(), "已执行 recover 并确认写入者恢复") {
return err
}
for _, marker := range terminalLibraryCaptureMarkers {
if strings.Contains(err.Error(), marker) {
return jobs.Permanent(jobs.ErrCodeValidationFailed, err)
}
}
return err
}
// librarySyncJobType 是生产同步作业的队列类型;与封存严格分开,状态不共用。
const librarySyncJobType = "library.sync"
@@ -365,7 +386,7 @@ func workerHandlers(store *postgresstore.Store, config appconfig.Config) (map[st
_ = store.AdvanceLibraryJob(ctx, libraryJobID, governance.LibraryJobFailed, "export", err.Error(),
"agent 已执行恢复检查;确认来源 healthy 后可重试同一作业号", "", "", finishedAt)
log.Printf("library capture failed scope=%s capture_job=%s: %v", payload.Scope, payload.CaptureJob, err)
return err
return terminalLibraryCaptureError(err)
}
// 执行后复核:结果里 agent 自报的身份必须还是期望实例,否则这次封存
// 可能来自别的栈,绝不能登记成"本实例的测试封存"。