fix: restore changes dropped by the upstream sync merge

The manual v0.3.6 sync (22132a4) was incomplete, and the follow-up merge
(d803ae2) resolved conflicts in favour of that copy, silently reverting three
shipped changes. main is already an ancestor of this branch, so re-merging
cannot bring them back; restore them explicitly.

- schema.sql: re-add the idempotent llm_profiles_format_check migration.
  CREATE TABLE IF NOT EXISTS leaves an existing table alone, so databases
  created before openai-responses kept the old two-value CHECK and rejected
  every openai-responses profile.
- orchestration.go: re-add the reseedPlannerPrompt/reseedWorkerPrompt calls,
  dropped by a comment-alignment hunk while the functions stayed behind as
  dead code. Without them existing installs never receive the quantified
  acceptance check or the negative-conclusion evidence threshold.
- CHANGELOG.md: restore the 0.3.6 entries, the @neouks credit and the correct
  norma version range.

Also close three recovery gaps in the new cold-archive path:

- traffic: write the staging journal even when no legacy host directory
  exists. Modern installs keep traffic in SQLite only, so a crash between the
  PostgreSQL commit and the SQLite commit left the archived task's exclusive
  traffic in the hot store with nothing for recovery to act on.
- recoverTaskArchiveStages: treat a missing journal.json as a discardable
  directory instead of an error, which used to set recoveryFailed and stop the
  archive worker from starting for the rest of the process lifetime.
- stageTaskArchiveFiles: replay the journal's renames when resuming an
  existing staging root. The journal is written before the first rename, so
  its presence did not guarantee a complete payload, and a resumed round could
  package an archive missing transcripts or workspace files.
This commit is contained in:
Autumn-27 committed 2026-08-29 01:37:46 -04:00
1 parent 0ec45bb45d
commit 21656cc2e3
5 files changed
+107 -13

No files matched your search

+2
View File
@@ -452,6 +452,8 @@ func (s *Server) seedOrchestrationTools() {
s.unbindGoalMetDefault()
s.reseedGoalsPrompt() // goals 提示词加入「抽操作约束」步 → 旧库追加一版新默认(一次性)
s.reseedMainAgentPrompt() // mainagent 提示词加入「目标达成后 add_intent 反问是否建目标」(一次性)
s.reseedPlannerPrompt() // planner 提示词:重写「0 意图」正当理由 + 加量化验收核对(一次性)
s.reseedWorkerPrompt() // worker 提示词:加否定结论证据门槛(一次性)
s.seedReporterAgent() // 预置「报告撰写」agent + 工具绑定 + finding 触发器(一次性)
// 注:pentest 的默认工具绑定无需迁移——BuiltinToolSeeds 在全新初始化时就把
// list_assets/insert_assets/report_finding/list_findings/list_companies 连同
+55 -10
View File
@@ -96,6 +96,17 @@ func stageTaskArchiveFiles(dataDir string, archiveID int64, taskID string, explo
if err := json.Unmarshal(raw, &stage.journal); err != nil {
return nil, fmt.Errorf("read task archive staging journal: %w", err)
}
// The journal is written before the first rename, so its presence does not mean
// the payload is complete: a previous round may have failed mid-loop with a
// rollback that itself errored, which leaves the root in place. Replay the moves
// rather than packaging a payload that is missing transcripts or workspace files.
if err := os.MkdirAll(filepath.Join(stage.payload, "files"), archiveDirMode); err != nil {
return nil, err
}
if err := stage.applyMoves(); err != nil {
_ = stage.rollback()
return nil, err
}
return stage, nil
} else if !os.IsNotExist(err) {
return nil, err
@@ -133,20 +144,42 @@ func stageTaskArchiveFiles(dataDir string, archiveID int64, taskID string, explo
_ = stage.rollback()
return nil, err
}
for _, move := range targets {
destination := filepath.Join(stage.payload, move.Relative)
if err := os.MkdirAll(filepath.Dir(destination), archiveDirMode); err != nil {
_ = stage.rollback()
return nil, err
}
if err := os.Rename(move.Source, destination); err != nil {
_ = stage.rollback()
return nil, fmt.Errorf("stage task archive path %s: %w", move.Source, err)
}
if err := stage.applyMoves(); err != nil {
_ = stage.rollback()
return nil, err
}
return stage, nil
}
// applyMoves performs the renames recorded in the journal. Entries already sitting
// in the payload are skipped, so an interrupted staging round can be resumed in
// place without moving anything twice.
func (s *taskArchiveFileStage) applyMoves() error {
for _, move := range s.journal.Moves {
destination := filepath.Join(s.payload, move.Relative)
if _, err := os.Lstat(destination); err == nil {
continue
} else if !os.IsNotExist(err) {
return err
}
if _, err := os.Lstat(move.Source); err != nil {
if os.IsNotExist(err) {
// Neither side exists: the path was removed outside the archive flow
// after the journal was written. Nothing can be staged for it.
continue
}
return err
}
if err := os.MkdirAll(filepath.Dir(destination), archiveDirMode); err != nil {
return err
}
if err := os.Rename(move.Source, destination); err != nil {
return fmt.Errorf("stage task archive path %s: %w", move.Source, err)
}
}
return nil
}
func (s *taskArchiveFileStage) rollback() error {
if s == nil || s.done {
return nil
@@ -368,6 +401,18 @@ func recoverTaskArchiveStages(dataDir string, pg *pgdb.DB) error {
}
root := filepath.Join(parent, entry.Name())
raw, err := os.ReadFile(filepath.Join(root, "journal.json"))
if os.IsNotExist(err) {
// Either staging died between creating the directory tree and writing the
// journal — no rename had run, so payload holds nothing — or commit/rollback
// failed part-way through removing the root, in which case payload only holds
// copies already inside the package. Without a journal there is nothing to
// roll back, and reporting an error here keeps the archive worker from ever
// starting, so discard the directory instead.
if err := os.RemoveAll(root); err != nil {
errs = append(errs, err)
}
continue
}
if err != nil {
errs = append(errs, err)
continue