{"version":3,"file":"todo-engine.d.ts","sourceRoot":"","sources":["../../../src/core/todo/todo-engine.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Durable TODO coordination engine.\n *\n * The TODO subsystem is a coordination and progress-reporting facility. It is\n * NOT execution authority. This engine provides:\n *\n * - optimistic concurrency with a monotonically increasing revision;\n * - deterministic state hashing;\n * - internal bounded stale-revision read/rebase/retry;\n * - idempotent mutation intents;\n * - typed error taxonomy;\n * - progress-aware loop detection;\n * - a bounded, sanitized event log for operability and replay.\n *\n * It deliberately does not introduce a second TODO authority: it coordinates\n * around a single set of items/revision owned by the session.\n */\n\nimport { createHash } from \"node:crypto\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Canonical status set. Tool surfaces expose the subset pending/in_progress/completed. */\nexport type TodoStatus = \"pending\" | \"in_progress\" | \"completed\" | \"cancelled\" | \"blocked\";\n\n/** The runtime status stored on items (compatible with the existing session TodoItem). */\nexport type TodoItemStatus = \"pending\" | \"in_progress\" | \"completed\";\n\n/** Item as stored/coordinated by the engine. Structurally compatible with the session TodoItem. */\nexport interface TodoItem {\n\tid?: string;\n\tcontent: string;\n\tactiveForm: string;\n\tstatus: TodoItemStatus;\n\tversion?: number;\n\tcreatedAt?: number;\n\tupdatedAt?: number;\n\tcompletedAt?: number;\n\townerRunId?: string;\n\townerAgentId?: string;\n}\n\n/** Patch-style operation for todo_update. */\nexport interface TodoPatchOp {\n\tid: string;\n\tstatus?: TodoItemStatus;\n\tactiveForm?: string;\n\tcontent?: string;\n}\n\n/** A fully-typed mutation intent. */\nexport interface TodoMutationIntent {\n\tintentId: string;\n\tidempotencyKey: string;\n\tscopeId: string;\n\tbaseRevision: number;\n\trequestedBy: { toolCallId: string };\n\toperations: TodoPatchOp[];\n\tkind: \"patch\" | \"replace_all\";\n\treplaceItems?: TodoItem[];\n\treplaceStateHash?: string;\n}\n\n/** Typed recovery action for a TODO error. */\nexport type TodoRecoveryAction =\n\t| \"internal_read_and_rebase\"\n\t| \"return_current_snapshot\"\n\t| \"manual_reconciliation\"\n\t| \"disable_todo_for_turn\"\n\t| \"none\";\n\n/** Typed TODO mutation error. */\nexport interface TodoMutationErrorInput {\n\tcode:\n\t\t| \"TODO_REVISION_STALE\"\n\t\t| \"TODO_REBASE_REQUIRED\"\n\t\t| \"TODO_REBASE_CONFLICT\"\n\t\t| \"TODO_ITEM_NOT_FOUND\"\n\t\t| \"TODO_ITEM_VERSION_CONFLICT\"\n\t\t| \"TODO_INVALID_STATUS_TRANSITION\"\n\t\t| \"TODO_DUPLICATE_INTENT\"\n\t\t| \"TODO_ALREADY_APPLIED\"\n\t\t| \"TODO_STATE_CORRUPT\"\n\t\t| \"TODO_SCOPE_MISMATCH\"\n\t\t| \"TODO_PERMISSION_DENIED\"\n\t\t| \"TODO_NO_PROGRESS_LOOP\"\n\t\t| \"TODO_TOOL_TEMPORARILY_DEGRADED\";\n\trequestedRevision?: number;\n\tcurrentRevision?: number;\n\tintentId?: string;\n\tconflictItemIds?: string[];\n\tmessage?: string;\n}\n\nexport interface TodoMutationError {\n\tcode: TodoMutationErrorInput[\"code\"];\n\trecoverable: boolean;\n\trunMustContinue: boolean;\n\trequestedRevision?: number;\n\tcurrentRevision?: number;\n\tintentId?: string;\n\tconflictItemIds?: string[];\n\trecoveryAction?: TodoRecoveryAction;\n\tmessage?: string;\n}\n\nexport interface TodoRebaseResult {\n\tstatus: \"not_needed\" | \"rebased\" | \"already_applied\" | \"conflict\";\n\toriginalRevision: number;\n\tcurrentRevision: number;\n\tappliedRevision?: number;\n\tpreservedConcurrentChanges: string[];\n\tconflictItemIds: string[];\n\treasonCodes: string[];\n}\n\nexport interface TodoFailureFingerprint {\n\tscopeId: string;\n\terrorCode: string;\n\tintentHash: string;\n\trequestedRevision?: number;\n\tcurrentRevision?: number;\n\tconflictItemIds?: string[];\n}\n\nexport type TodoEventType =\n\t| \"TODO_STATE_READ\"\n\t| \"TODO_MUTATION_INTENT_CREATED\"\n\t| \"TODO_MUTATION_APPLY_STARTED\"\n\t| \"TODO_REVISION_STALE_DETECTED\"\n\t| \"TODO_INTERNAL_READ_COMPLETED\"\n\t| \"TODO_REBASE_STARTED\"\n\t| \"TODO_REBASE_SUCCEEDED\"\n\t| \"TODO_REBASE_CONFLICT\"\n\t| \"TODO_INTENT_ALREADY_APPLIED\"\n\t| \"TODO_MUTATION_COMMITTED\"\n\t| \"TODO_MUTATION_REJECTED\"\n\t| \"TODO_LOOP_CHAIN_STARTED\"\n\t| \"TODO_LOOP_CHAIN_RESET_BY_PROGRESS\"\n\t| \"TODO_NO_PROGRESS_LOOP_DETECTED\"\n\t| \"TODO_TOOL_DEGRADED\"\n\t| \"TODO_PROJECTION_DRIFT_DETECTED\"\n\t| \"TODO_PROJECTION_RECONCILED\";\n\nexport interface TodoEvent {\n\ttype: TodoEventType;\n\tat: number;\n\trevision?: number;\n\tintentId?: string;\n\trequestedRevision?: number;\n\tcurrentRevision?: number;\n\tconflictItemIds?: string[];\n\trecoveryAction?: TodoRecoveryAction;\n}\n\nexport interface TodoEngineState {\n\tcurrentRevision: number;\n\tprogressEpoch: number;\n\tlastTodoReadRevision: number;\n\tsnapshots: Array<{ revision: number; items: TodoItem[] }>;\n\tledger: Array<{ idempotencyKey: string; applicationRevision: number; applied: boolean }>;\n\tevents: TodoEvent[];\n\tchain: { fingerprint: TodoFailureFingerprint; consecutive: number; lastAt: number } | null;\n}\n\nexport interface TodoEnginePersistence {\n\tload: () => TodoEngineState | undefined;\n\tsave: (state: TodoEngineState) => void;\n}\n\n// ---------------------------------------------------------------------------\n// Limits / bounds\n// ---------------------------------------------------------------------------\n\nexport interface TodoEngineLimits {\n\t/** Retained snapshot history (for rebase base lookup). */\n\tmaxSnapshots: number;\n\t/** Bounded idempotency ledger entries. */\n\tmaxLedger: number;\n\t/** Bounded event log length. */\n\tmaxEvents: number;\n\t/** Maximum internal rebase attempts per tool call. */\n\tmaxInternalRebaseAttempts: number;\n\t/** Bounded excessive-failure threshold for a single chain. */\n\tmaxNoProgressFailures: number;\n}\n\nexport const DEFAULT_TODO_ENGINE_LIMITS: TodoEngineLimits = {\n\tmaxSnapshots: 16,\n\tmaxLedger: 256,\n\tmaxEvents: 512,\n\tmaxInternalRebaseAttempts: 1,\n\tmaxNoProgressFailures: 3,\n};\n\n// ---------------------------------------------------------------------------\n// Pure helpers\n// ---------------------------------------------------------------------------\n\nfunction stableItemKey(items: TodoItem[]): string {\n\treturn JSON.stringify(\n\t\t[...items]\n\t\t\t.sort((a, b) => (a.id ?? \"\").localeCompare(b.id ?? \"\"))\n\t\t\t.map((t) => ({\n\t\t\t\tid: t.id ?? \"\",\n\t\t\t\tcontent: t.content,\n\t\t\t\tactiveForm: t.activeForm,\n\t\t\t\tstatus: t.status,\n\t\t\t})),\n\t);\n}\n\n/** Deterministic state hash of a set of items. */\nexport function computeStateHash(items: TodoItem[]): string {\n\treturn createHash(\"sha256\").update(stableItemKey(items)).digest(\"hex\");\n}\n\n/** Allowed transitions from a status. */\nexport function allowedTransitions(from: TodoStatus): ReadonlySet<TodoStatus> {\n\tswitch (from) {\n\t\tcase \"pending\":\n\t\t\treturn new Set([\"pending\", \"in_progress\", \"completed\", \"cancelled\", \"blocked\"]);\n\t\tcase \"in_progress\":\n\t\t\treturn new Set([\"in_progress\", \"pending\", \"completed\", \"blocked\", \"cancelled\"]);\n\t\tcase \"blocked\":\n\t\t\treturn new Set([\"blocked\", \"pending\", \"in_progress\", \"cancelled\"]);\n\t\tcase \"completed\":\n\t\t\treturn new Set([\"completed\"]);\n\t\tcase \"cancelled\":\n\t\t\treturn new Set([\"cancelled\"]);\n\t\tdefault:\n\t\t\treturn new Set([from]);\n\t}\n}\n\n/**\n * Validate a single status transition.\n * Repeating the current status is idempotent (allowed).\n */\nexport function validateTransition(from: TodoStatus, to: TodoStatus): { ok: boolean; reason?: string } {\n\tif (from === to) return { ok: true };\n\tif (allowedTransitions(from).has(to)) return { ok: true };\n\treturn {\n\t\tok: false,\n\t\treason: `cannot transition ${from} -> ${to} (${from} is terminal)`,\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// Failure fingerprint\n// ---------------------------------------------------------------------------\n\nexport function fingerprintError(\n\tfp: Omit<TodoFailureFingerprint, \"intentHash\"> & { intentHash?: string },\n): TodoFailureFingerprint {\n\treturn {\n\t\tscopeId: fp.scopeId,\n\t\terrorCode: fp.errorCode,\n\t\tintentHash: fp.intentHash ?? \"\",\n\t\trequestedRevision: fp.requestedRevision,\n\t\tcurrentRevision: fp.currentRevision,\n\t\tconflictItemIds: fp.conflictItemIds,\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// Loop detection chain\n// ---------------------------------------------------------------------------\n\ninterface FailureChain {\n\tfingerprint: TodoFailureFingerprint;\n\tconsecutive: number;\n\tlastAt: number;\n}\n\n// ---------------------------------------------------------------------------\n// Engine\n// ---------------------------------------------------------------------------\n\nexport class TodoEngine {\n\treadonly scopeId: string;\n\treadonly limits: TodoEngineLimits;\n\n\tprivate snapshots = new Map<number, TodoItem[]>();\n\tprivate lastTodoReadRevision = -1;\n\tprivate ledger = new Map<string, { applicationRevision: number; applied: boolean }>();\n\tprivate events: TodoEvent[] = [];\n\tprivate chain: FailureChain | null = null;\n\tprivate progressEpoch = 0;\n\tprivate currentRevision = 0;\n\tprivate readonly persistence?: TodoEnginePersistence;\n\n\tconstructor(\n\t\tscopeId: string,\n\t\tlimits: Partial<TodoEngineLimits> = {},\n\t\tprivate readonly now: () => number = Date.now,\n\t\tpersistence?: TodoEnginePersistence,\n\t) {\n\t\tthis.scopeId = scopeId;\n\t\tthis.limits = { ...DEFAULT_TODO_ENGINE_LIMITS, ...limits };\n\t\tthis.persistence = persistence;\n\t\tthis.restore(persistence?.load());\n\t}\n\n\tprivate restore(state: TodoEngineState | undefined): void {\n\t\tif (!state) return;\n\t\tthis.currentRevision = state.currentRevision;\n\t\tthis.progressEpoch = state.progressEpoch;\n\t\tthis.lastTodoReadRevision = state.lastTodoReadRevision;\n\t\tfor (const snapshot of state.snapshots.slice(-this.limits.maxSnapshots)) {\n\t\t\tthis.snapshots.set(\n\t\t\t\tsnapshot.revision,\n\t\t\t\tsnapshot.items.map((item) => ({ ...item })),\n\t\t\t);\n\t\t}\n\t\tfor (const entry of state.ledger.slice(-this.limits.maxLedger)) {\n\t\t\tthis.ledger.set(entry.idempotencyKey, {\n\t\t\t\tapplicationRevision: entry.applicationRevision,\n\t\t\t\tapplied: entry.applied,\n\t\t\t});\n\t\t}\n\t\tthis.events = state.events.slice(-this.limits.maxEvents).map((event) => ({ ...event }));\n\t\tthis.chain = state.chain ? { ...state.chain, fingerprint: { ...state.chain.fingerprint } } : null;\n\t}\n\n\tprivate persist(): void {\n\t\tthis.persistence?.save({\n\t\t\tcurrentRevision: this.currentRevision,\n\t\t\tprogressEpoch: this.progressEpoch,\n\t\t\tlastTodoReadRevision: this.lastTodoReadRevision,\n\t\t\tsnapshots: [...this.snapshots.entries()].map(([revision, items]) => ({\n\t\t\t\trevision,\n\t\t\t\titems: items.map((item) => ({ ...item })),\n\t\t\t})),\n\t\t\tledger: [...this.ledger.entries()].map(([idempotencyKey, value]) => ({ idempotencyKey, ...value })),\n\t\t\tevents: this.events.map((event) => ({ ...event })),\n\t\t\tchain: this.chain ? { ...this.chain, fingerprint: { ...this.chain.fingerprint } } : null,\n\t\t});\n\t}\n\n\t// -- progress epoch ------------------------------------------------------\n\n\t/** Record authoritative non-TODO progress; breaks any failure chain. */\n\trecordProgress(): void {\n\t\tthis.progressEpoch++;\n\t\tif (this.chain) {\n\t\t\tthis.emit(\"TODO_LOOP_CHAIN_RESET_BY_PROGRESS\");\n\t\t\tthis.chain = null;\n\t\t}\n\t\tthis.persist();\n\t}\n\n\t/**\n\t * Record a todo read. Only counts as progress if the read returned a\n\t * newer revision than the last observed revision (identical reads do not\n\t * reset the chain).\n\t */\n\trecordTodoRead(revision: number): void {\n\t\tif (revision > this.lastTodoReadRevision) {\n\t\t\tthis.lastTodoReadRevision = revision;\n\t\t\tthis.currentRevision = Math.max(this.currentRevision, revision);\n\t\t\tthis.recordProgress();\n\t\t}\n\t}\n\n\tgetProgressEpoch(): number {\n\t\treturn this.progressEpoch;\n\t}\n\n\t// -- snapshot history ----------------------------------------------------\n\n\trecordReadSnapshot(revision: number, items: TodoItem[]): void {\n\t\tthis.currentRevision = Math.max(this.currentRevision, revision);\n\t\tif (this.snapshots.size >= this.limits.maxSnapshots) {\n\t\t\tconst oldest = [...this.snapshots.keys()].sort((a, b) => a - b)[0];\n\t\t\tif (oldest !== undefined) this.snapshots.delete(oldest);\n\t\t}\n\t\tthis.snapshots.set(\n\t\t\trevision,\n\t\t\titems.map((t) => ({ ...t })),\n\t\t);\n\t\tthis.persist();\n\t}\n\n\trecordState(revision: number, items: TodoItem[]): void {\n\t\tthis.currentRevision = revision;\n\t\tthis.recordReadSnapshot(revision, items);\n\t}\n\n\tgetCurrentRevision(): number {\n\t\treturn this.currentRevision;\n\t}\n\n\tprivate getSnapshot(revision: number): TodoItem[] | undefined {\n\t\treturn this.snapshots.get(revision)?.map((t) => ({ ...t }));\n\t}\n\n\t// -- idempotency ---------------------------------------------------------\n\n\t/**\n\t * Look up a previously applied intent by idempotency key.\n\t * Returns true if the exact key was already applied.\n\t */\n\tlookupApplied(idempotencyKey: string): boolean {\n\t\treturn this.ledger.get(idempotencyKey)?.applied === true;\n\t}\n\n\trecordApplied(idempotencyKey: string, applicationRevision: number): void {\n\t\tif (this.ledger.size >= this.limits.maxLedger) {\n\t\t\tconst oldest = this.ledger.keys().next().value;\n\t\t\tif (oldest !== undefined) this.ledger.delete(oldest as string);\n\t\t}\n\t\tthis.ledger.set(idempotencyKey, { applicationRevision, applied: true });\n\t\tthis.currentRevision = Math.max(this.currentRevision, applicationRevision);\n\t\tthis.persist();\n\t}\n\n\t// -- events --------------------------------------------------------------\n\n\temit(event: Omit<TodoEvent, \"at\"> | TodoEventType): void {\n\t\tconst resolved: Omit<TodoEvent, \"at\"> = typeof event === \"string\" ? { type: event } : event;\n\t\tthis.events.push({ ...resolved, at: this.now() });\n\t\tif (this.events.length > this.limits.maxEvents) {\n\t\t\tthis.events = this.events.slice(-this.limits.maxEvents);\n\t\t}\n\t\tthis.persist();\n\t}\n\n\tgetEvents(limit = 50): TodoEvent[] {\n\t\treturn this.events.slice(-limit).map((e) => ({ ...e }));\n\t}\n\n\t// -- loop detection ------------------------------------------------------\n\n\t/**\n\t * Register a failure and return whether the chain should be treated as a\n\t * genuine no-progress loop (model-requested, consecutive, same fingerprint).\n\t */\n\tregisterFailure(fp: TodoFailureFingerprint): { blocked: boolean; consecutive: number } {\n\t\tif (this.chain && this.sameFingerprint(this.chain.fingerprint, fp)) {\n\t\t\tthis.chain.consecutive++;\n\t\t\tthis.chain.lastAt = this.now();\n\t\t} else {\n\t\t\tthis.chain = {\n\t\t\t\tfingerprint: { ...fp },\n\t\t\t\tconsecutive: 1,\n\t\t\t\tlastAt: this.now(),\n\t\t\t};\n\t\t\tthis.emit({\n\t\t\t\ttype: \"TODO_LOOP_CHAIN_STARTED\",\n\t\t\t\tintentId: fp.intentHash,\n\t\t\t\tcurrentRevision: fp.currentRevision,\n\t\t\t});\n\t\t}\n\n\t\tconst blocked = this.chain.consecutive >= this.limits.maxNoProgressFailures;\n\t\tif (blocked) {\n\t\t\tthis.emit({\n\t\t\t\ttype: \"TODO_NO_PROGRESS_LOOP_DETECTED\",\n\t\t\t\tcurrentRevision: fp.currentRevision,\n\t\t\t\trequestedRevision: fp.requestedRevision,\n\t\t\t\tintentId: fp.intentHash,\n\t\t\t});\n\t\t}\n\t\tthis.persist();\n\t\treturn { blocked, consecutive: this.chain.consecutive };\n\t}\n\n\t/** Reset the failure chain (self-healing path, e.g. internal rebase success). */\n\tresetFailureChain(): void {\n\t\tif (this.chain) {\n\t\t\tthis.emit(\"TODO_LOOP_CHAIN_RESET_BY_PROGRESS\");\n\t\t\tthis.chain = null;\n\t\t\tthis.persist();\n\t\t}\n\t}\n\n\tprivate sameFingerprint(a: TodoFailureFingerprint, b: TodoFailureFingerprint): boolean {\n\t\treturn (\n\t\t\ta.scopeId === b.scopeId &&\n\t\t\ta.errorCode === b.errorCode &&\n\t\t\ta.intentHash === b.intentHash &&\n\t\t\ta.requestedRevision === b.requestedRevision &&\n\t\t\ta.currentRevision === b.currentRevision &&\n\t\t\tJSON.stringify(a.conflictItemIds ?? []) === JSON.stringify(b.conflictItemIds ?? [])\n\t\t);\n\t}\n\n\tisBlocked(): boolean {\n\t\treturn this.chain !== null && this.chain.consecutive >= this.limits.maxNoProgressFailures;\n\t}\n\n\t// -- rebase --------------------------------------------------------------\n\n\t/**\n\t * Deterministic, operation-aware rebase of a patch intent against the\n\t * current state. Uses the retained base snapshot when available to avoid\n\t * clobbering concurrent edits; otherwise applies conservative rules.\n\t */\n\trebase(\n\t\tbaseRevision: number,\n\t\tcurrentRevision: number,\n\t\tcurrentItems: TodoItem[],\n\t\tops: TodoPatchOp[],\n\t): TodoRebaseResult {\n\t\tconst base = this.getSnapshot(baseRevision);\n\t\tconst preserved: string[] = [];\n\t\tconst conflicts: string[] = [];\n\t\tconst reasons: string[] = [];\n\t\tconst byId = new Map<string, TodoItem>();\n\t\tfor (const item of currentItems) {\n\t\t\tif (item.id) byId.set(item.id, item);\n\t\t}\n\t\tconst baseById = new Map<string, TodoItem>();\n\t\tfor (const item of base ?? []) {\n\t\t\tif (item.id) baseById.set(item.id, item);\n\t\t}\n\n\t\tfor (const op of ops) {\n\t\t\tconst current = byId.get(op.id);\n\t\t\tif (!current) {\n\t\t\t\tconflicts.push(op.id);\n\t\t\t\treasons.push(\"TODO_ITEM_NOT_FOUND\");\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t// Status\n\t\t\tif (op.status !== undefined) {\n\t\t\t\tif (current.status === op.status) {\n\t\t\t\t\t// repeated transition -> idempotent\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tconst baseItem = baseById.get(op.id);\n\t\t\t\tconst baseStatus = baseItem?.status;\n\t\t\t\t// If the status field was not concurrently changed (base == current\n\t\t\t\t// for that field) we can safely apply the transition; otherwise the\n\t\t\t\t// item was concurrently moved and we treat it as a conflict.\n\t\t\t\tif (baseStatus !== undefined && baseStatus !== current.status && baseStatus !== op.status) {\n\t\t\t\t\tconflicts.push(op.id);\n\t\t\t\t\treasons.push(\"TODO_ITEM_VERSION_CONFLICT\");\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tconst allowed = validateTransition(current.status, op.status);\n\t\t\t\tif (!allowed.ok) {\n\t\t\t\t\tconflicts.push(op.id);\n\t\t\t\t\treasons.push(allowed.reason ?? \"TODO_INVALID_STATUS_TRANSITION\");\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (baseStatus === undefined && current.status !== op.status) {\n\t\t\t\t\t// No base: apply only if current unchanged since it's the only writer\n\t\t\t\t\t// path. Conservative: allow forward transitions.\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Content\n\t\t\tif (op.content !== undefined) {\n\t\t\t\tif (current.content === op.content) {\n\t\t\t\t\tcontinue; // already applied\n\t\t\t\t}\n\t\t\t\tconst baseItem = baseById.get(op.id);\n\t\t\t\tconst baseContent = baseItem?.content;\n\t\t\t\tif (baseContent !== undefined && baseContent === current.content) {\n\t\t\t\t\t// content unchanged by any concurrent writer -> model may set it\n\t\t\t\t\tpreserved.push(op.id);\n\t\t\t\t} else if (baseContent !== undefined && baseContent !== current.content) {\n\t\t\t\t\t// content was edited concurrently -> conflict\n\t\t\t\t\tconflicts.push(op.id);\n\t\t\t\t\treasons.push(\"TODO_ITEM_VERSION_CONFLICT\");\n\t\t\t\t\tcontinue;\n\t\t\t\t} else {\n\t\t\t\t\t// No base: conservative conflict for content edits we cannot verify\n\t\t\t\t\tconflicts.push(op.id);\n\t\t\t\t\treasons.push(\"TODO_REBASE_CONFLICT\");\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\t\t\t// activeForm: applies unless concurrently changed on the same item\n\t\t\tif (op.activeForm !== undefined && op.activeForm !== current.activeForm) {\n\t\t\t\tconst baseItem = baseById.get(op.id);\n\t\t\t\tconst baseActive = baseItem?.activeForm;\n\t\t\t\tif (baseActive !== undefined && baseActive !== current.activeForm && baseActive !== op.activeForm) {\n\t\t\t\t\tconflicts.push(op.id);\n\t\t\t\t\treasons.push(\"TODO_ITEM_VERSION_CONFLICT\");\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tpreserved.push(op.id);\n\t\t\t}\n\t\t}\n\n\t\tif (conflicts.length > 0) {\n\t\t\treturn {\n\t\t\t\tstatus: \"conflict\",\n\t\t\t\toriginalRevision: baseRevision,\n\t\t\t\tcurrentRevision,\n\t\t\t\tpreservedConcurrentChanges: preserved,\n\t\t\t\tconflictItemIds: conflicts,\n\t\t\t\treasonCodes: reasons,\n\t\t\t};\n\t\t}\n\n\t\tconst hasActualChange = ops.some((op) => {\n\t\t\tconst cur = byId.get(op.id);\n\t\t\tif (!cur) return false;\n\t\t\tif (op.status !== undefined && op.status !== cur.status) return true;\n\t\t\tif (op.activeForm !== undefined && op.activeForm !== cur.activeForm) return true;\n\t\t\tif (op.content !== undefined && op.content !== cur.content) return true;\n\t\t\treturn false;\n\t\t});\n\n\t\treturn {\n\t\t\tstatus: hasActualChange ? \"rebased\" : \"already_applied\",\n\t\t\toriginalRevision: baseRevision,\n\t\t\tcurrentRevision,\n\t\t\tappliedRevision: currentRevision + (hasActualChange ? 1 : 0),\n\t\t\tpreservedConcurrentChanges: preserved,\n\t\t\tconflictItemIds: [],\n\t\t\treasonCodes: [],\n\t\t};\n\t}\n\n\t// -- typed errors --------------------------------------------------------\n\n\ttypedError(input: TodoMutationErrorInput): TodoMutationError {\n\t\treturn {\n\t\t\tcode: input.code,\n\t\t\trecoverable: codeRecoverable(input.code),\n\t\t\trunMustContinue: true,\n\t\t\trequestedRevision: input.requestedRevision,\n\t\t\tcurrentRevision: input.currentRevision,\n\t\t\tintentId: input.intentId,\n\t\t\tconflictItemIds: input.conflictItemIds,\n\t\t\trecoveryAction: recoveryFor(input),\n\t\t\tmessage: input.message,\n\t\t};\n\t}\n\n\tgetDiagnostics() {\n\t\treturn {\n\t\t\tscopeId: this.scopeId,\n\t\t\tprogressEpoch: this.progressEpoch,\n\t\t\tlastTodoReadRevision: this.lastTodoReadRevision,\n\t\t\tsnapshotCount: this.snapshots.size,\n\t\t\tledgerCount: this.ledger.size,\n\t\t\teventCount: this.events.length,\n\t\t\tchainActive: this.chain !== null,\n\t\t\tchainConsecutive: this.chain?.consecutive ?? 0,\n\t\t\tisLoopBlocked: this.isBlocked(),\n\t\t};\n\t}\n}\n\nfunction codeRecoverable(code: TodoMutationErrorInput[\"code\"]): boolean {\n\tswitch (code) {\n\t\tcase \"TODO_STATE_CORRUPT\":\n\t\t\treturn false;\n\t\tdefault:\n\t\t\treturn true;\n\t}\n}\n\nfunction recoveryFor(input: TodoMutationErrorInput): TodoRecoveryAction {\n\tswitch (input.code) {\n\t\tcase \"TODO_REVISION_STALE\":\n\t\t\treturn \"internal_read_and_rebase\";\n\t\tcase \"TODO_REBASE_CONFLICT\":\n\t\tcase \"TODO_ITEM_NOT_FOUND\":\n\t\tcase \"TODO_ITEM_VERSION_CONFLICT\":\n\t\tcase \"TODO_SCOPE_MISMATCH\":\n\t\t\treturn \"return_current_snapshot\";\n\t\tcase \"TODO_INVALID_STATUS_TRANSITION\":\n\t\t\treturn \"return_current_snapshot\";\n\t\tcase \"TODO_ALREADY_APPLIED\":\n\t\t\treturn \"none\";\n\t\tcase \"TODO_NO_PROGRESS_LOOP\":\n\t\tcase \"TODO_TOOL_TEMPORARILY_DEGRADED\":\n\t\t\treturn \"disable_todo_for_turn\";\n\t\tdefault:\n\t\t\treturn \"none\";\n\t}\n}\n\n/** Derive a stable intent hash from the operations for fingerprinting. */\nexport function hashIntent(ops: TodoPatchOp[]): string {\n\treturn createHash(\"sha256\")\n\t\t.update(JSON.stringify(ops.map((o) => ({ id: o.id, s: o.status, a: o.activeForm, c: o.content }))))\n\t\t.digest(\"hex\");\n}\n\n/** Generate a bounded, non-secret intent id. */\nexport function generateIntentId(seed: string, now: number): string {\n\treturn createHash(\"sha256\").update(`${seed}|${now}`).digest(\"hex\").slice(0, 16);\n}\n"]}