diff --git a/CHANGELOG.md b/CHANGELOG.md index 960c548..92a7898 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 原文保留为历史,不再作为 当前契约。 diff --git a/cmd/worker/library_capture_terminal_test.go b/cmd/worker/library_capture_terminal_test.go new file mode 100644 index 0000000..e2704d0 --- /dev/null +++ b/cmd/worker/library_capture_terminal_test.go @@ -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) + } +} diff --git a/cmd/worker/main.go b/cmd/worker/main.go index 022d715..c65e784 100644 --- a/cmd/worker/main.go +++ b/cmd/worker/main.go @@ -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 自报的身份必须还是期望实例,否则这次封存 // 可能来自别的栈,绝不能登记成"本实例的测试封存"。