| 1 | package taskmonitor |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "errors" |
| 6 | "time" |
| 7 | ) |
| 8 | |
| 9 | // ErrStoreVersionConflict reports that a snapshot CAS lost to another writer. |
| 10 | // Callers may re-read and retry a derived update, or return a stable client |
| 11 | // conflict without parsing implementation-specific error text. |
| 12 | var ErrStoreVersionConflict = errors.New("task store version conflict") |
| 13 | |
| 14 | // Store is the read-only query surface for task monitoring. |
| 15 | type Store interface { |
| 16 | ListTasks(ctx context.Context, projectDir string) ([]TaskSnapshot, error) |
| 17 | GetTask(ctx context.Context, projectDir string, taskID string) (*TaskSnapshot, error) |
| 18 | ListEvents(ctx context.Context, projectDir string, taskID string, afterSequence int) ([]TaskEvent, error) |
| 19 | } |
| 20 | |
| 21 | // ProjectionSink receives post-commit hints. Implementations must enqueue and |
| 22 | // return immediately; FileStore remains the only authority for task control. |
| 23 | type ProjectionSink interface { |
| 24 | SnapshotChanged(projectRoot, taskID string) |
| 25 | EventsChanged(projectRoot, taskID string) |
| 26 | } |
| 27 | |
| 28 | // IdempotencyRecord captures the binding between an idempotency key and the |
| 29 | // operation it was used for. |
| 30 | type IdempotencyRecord struct { |
| 31 | Key string `json:"key"` |
| 32 | Op string `json:"op"` |
| 33 | TaskID string `json:"task_id"` |
| 34 | Version uint64 `json:"version"` |
| 35 | Pending bool `json:"pending,omitempty"` |
| 36 | ClaimedAt time.Time `json:"claimed_at,omitempty"` |
| 37 | } |
| 38 | |
| 39 | // IdempotencyClaimer atomically reserves a key before a control operation |
| 40 | // performs any side effect. Pending claims can be finalized or released. |
| 41 | type IdempotencyClaimer interface { |
| 42 | ClaimIdempotency(ctx context.Context, projectDir string, r IdempotencyRecord) (*IdempotencyRecord, error) |
| 43 | FinalizeIdempotency(ctx context.Context, projectDir string, r IdempotencyRecord) error |
| 44 | ReleaseIdempotency(ctx context.Context, projectDir, key string) error |
| 45 | } |
| 46 | |
| 47 | // WriteStore extends Store with atomic write operations for control |
| 48 | // commands, persistent idempotency, and event sequencing. |
| 49 | // |
| 50 | // Transaction ordering for control operations: |
| 51 | // 1. ClaimIdempotency — reserve the key before runtime/state side effects |
| 52 | // 2. SaveTask — persist state with version CAS |
| 53 | // 3. AppendAuditEvent — atomically assign sequence + write event |
| 54 | // 4. FinalizeIdempotency — mark the claim complete |
| 55 | // |
| 56 | // Steps 2-3 failures after a successful SaveTask leave the task in the new |
| 57 | // state with a potentially incomplete audit log. This is acceptable for a |
| 58 | // file-based store; a transactional store would provide stronger guarantees. |
| 59 | type WriteStore interface { |
| 60 | Store |
| 61 | |
| 62 | // SaveTask atomically persists snap with version-based CAS. |
| 63 | SaveTask(ctx context.Context, projectDir string, snap TaskSnapshot) error |
| 64 | |
| 65 | // RenewRuntimeLease extends an alive task lease only when ownerID still |
| 66 | // owns the persisted runtime generation. Implementations must read the raw |
| 67 | // stored snapshot rather than a liveness-reconciled observation. |
| 68 | RenewRuntimeLease(ctx context.Context, projectDir, taskID, ownerID string, leaseUntil time.Time) (bool, error) |
| 69 | |
| 70 | // AppendAuditEvent atomically assigns the next monotonic sequence |
| 71 | // number and appends the event to taskID's event log. Implementations |
| 72 | // must be safe for concurrent use across processes. |
| 73 | AppendAuditEvent(ctx context.Context, projectDir string, ev TaskEvent) error |
| 74 | |
| 75 | // CheckIdempotency returns the recorded key if it exists, or nil. |
| 76 | CheckIdempotency(ctx context.Context, projectDir string, key string) (*IdempotencyRecord, error) |
| 77 | |
| 78 | // RecordIdempotency atomically claims key for r. If key already exists |
| 79 | // with identical parameters, it is a no-op. If key exists with different |
| 80 | // parameters, it must return an error. Implementations must be safe |
| 81 | // across process restarts. |
| 82 | RecordIdempotency(ctx context.Context, projectDir string, r IdempotencyRecord) error |
| 83 | } |
| 84 |