Guide for working with the process_book job in the shelf book digitization pipeline...
The process_book job orchestrates the book digitization pipeline using a work unit pattern. All stages are consolidated into this single job - there are no separate job packages.
| Task | Reference |
|---|---|
| Understand pipeline architecture | architecture.md |
| Stage implementation patterns | stages.md |
| Debug issues | debugging.md |
| Add/modify stages | adding-stages.md |
internal/jobs/process_book/
āāā process_book.go # Package entry, NewJob(), factory
āāā job/
āāā job.go # Job struct, Start(), OnComplete()
āāā types.go # WorkUnitInfo, constants
āāā state.go # MaybeStartBookOperations(), triggers
āāā finalize.go # Finalize-ToC inline (discover, gap analysis)
āāā structure.go # Common-structure inline (extract, polish)
āāā link_toc.go # ToC linking agents
āāā [stage].go # One file per stage
internal/jobs/common/
āāā state.go # BookState, PageState, OperationState
āāā load.go # LoadBook()
āāā persist.go # Persistence helpers (async-first)
āāā reset.go # Reset helpers for crash recovery
āāā [helpers].go # Shared utilities
// 1. Create and register
unit := j.CreateXWorkUnit(ctx, pageNum, state)
j.RegisterWorkUnit(unit.ID, WorkUnitInfo{...})
// 2. Worker executes (external)
// 3. Handle completion
func (j *Job) OnComplete(ctx, result) {
info, _ := j.GetWorkUnit(result.WorkUnitID)
switch info.UnitType {
case WorkUnitTypeX:
return j.HandleXComplete(ctx, info, result)
}
}
// 4. Clean up
j.RemoveWorkUnit(result.WorkUnitID)
// Async writes (default - non-blocking, fire-and-forget)
state.SetComplete(true) // In-memory first
sink.Send(defra.WriteOp{...}) // Async to DB
// Sync only for critical creates (need DocID back)
result, _ := sink.SendSync(ctx, op) // Blocking
j.TocDocID = result.DocID
Important: Agent state persistence is fully async. No intermediate saves during agent loops - crash recovery restarts agents from scratch.
func (j *Job) MaybeStartBookOperations(ctx) []jobs.WorkUnit {
if j.SomeThreshold() && j.Book.SomeOp.CanStart() {
j.Book.SomeOp.Start()
return []jobs.WorkUnit{j.CreateSomeWorkUnit(ctx)}
}
// ... more triggers
}
op.CanStart() // true if not started
op.Start() // NotStarted ā InProgress
op.Complete() // InProgress ā Complete
op.Fail(max) // Increment retry, maybe ā Failed
op.IsDone() // Complete or permanently Failed
| Category | Pattern | State | Example |
|---|---|---|---|
| Page-level | Per-page parallel | PageState | OCR, blend, label |
| Book-level | Threshold trigger | OperationState | Metadata |
| Agent-based | Multi-turn loop | Agent struct (no intermediate persist) | ToC finder, chapter finder |
| Inline sub-job | Embedded in parent | Sub-job state | Finalize, Structure |
See stages.md for implementation patterns.
shelf api jobs status <book-id> # Stage progress
shelf api books get <book-id> # Book state
shelf api agent-logs list --job-id X # Agent logs (if debug enabled)
{ Page(filter: {book_id: {_eq: "<id>"}}) { page_num ocr_complete } }
{ Book(filter: {_docID: {_eq: "<id>"}}) { metadata_complete toc_finalize_complete } }
{ AgentState(filter: {book_id: {_eq: "<id>"}}) { agent_id agent_type complete } }
See debugging.md for full query reference.
WorkUnitTypeX constant in types.goCreateX and HandleXCompleteOnComplete() switchMaybeStartBookOperations()SendToSink)See adding-stages.md for full guide.
*_started / *_complete flagsConfig.DebugAgents = trueDeleteAgentStateByAgentID for cleanup (not DocID-based)