diff --git a/docs/architecture/adr-003-unified-ai-job-engine.md b/docs/architecture/adr-003-unified-ai-job-engine.md index 395252a..7009c87 100644 --- a/docs/architecture/adr-003-unified-ai-job-engine.md +++ b/docs/architecture/adr-003-unified-ai-job-engine.md @@ -48,6 +48,31 @@ M-AI-03 的目标是在 M-AI-02 Schema Expand 之上,纯代码层实现统一 | `failed` | 执行失败且重试耗尽 | 是 | `AiJobLifecycleRepository.markFailed()` | | `cancelled` | 用户/系统取消 | 是 | `AiJobLifecycleRepository.markCancelled()` | +#### 1.1.1 `cancel_requested` 不是独立状态 + +`cancel_requested` **不是** `lifecycleStatus` 枚举值。它是 `AiJob.cancelRequestedAt` 字段,作为 `running` 状态上的**取消信号**而非状态迁移目标。 + +流程: + +``` +用户调用 cancel API + │ + ├─ lifecycleStatus = 'queued' + │ → markCancelled(jobId) → cancelled(直接终态) + │ + └─ lifecycleStatus = 'running' + → 设置 cancelRequestedAt = now()(lifecycleStatus 保持 running) + → Worker Heartbeat / Engine 执行间隙检测到 cancelRequestedAt != null + → 抛出 JobCancelledError + → Engine catch → markCancelled(jobId) → cancelled +``` + +**设计理由**: +- 不引入中间状态 `cancel_requested` 避免状态机膨胀(5 状态 + 1 信号 vs 6 状态) +- `cancelRequestedAt` 是 `running` 上的 boolean flag,语义更轻量 +- Engine 在每次 Provider 调用前后检查此字段,实现协作式取消 +- §1.5 的 `STATUS_TO_LIFECYCLE` 中 `cancel_requested` 仅用于过渡期 Shadow Write(legacy `status` 列可能有此值,映射到新 `lifecycleStatus = 'cancelled'`) + #### 1.2 允许迁移 ``` @@ -367,11 +392,20 @@ export class AiJobService { async createJob(input: CreateJobInput): Promise; } +// 对齐 Issue #289:事务化 AiJob 创建、Snapshot 与 Enqueue Outbox interface CreateJobInput { + /** 用户 ID */ userId: string; + /** Job 类型(对应 JobDefinition.jobType) */ jobType: string; + /** 触发方式 */ + triggerType: 'user_api' | 'admin_manual' | 'system_scheduled' | 'parent_job'; + /** 分析目标类型 */ targetType: string; + /** 分析目标 ID */ targetId: string; + /** 父 Job ID(Job 链场景) */ + parentJobId?: string; /** 幂等 Key。提供时启用幂等;不提供时每次创建新 Job */ idempotencyKey?: string; /** 覆盖 Definition 默认值 */ @@ -638,7 +672,16 @@ try { | Execution Engine | 内部重试循环 | `JobDefinition.execution.maxRetries` | | Outbox | attemptCount < 3 → pending;>= 3 → dead | OutboxRepository | -**约束**:BullMQ retry 与 Engine 内部 retry 不叠加。Engine 内部错误不抛出(不触发 BullMQ retry),而是在 Engine 内完成所有重试后,一次性 `markFailed` 或 `markSucceeded`。 +**约束**:BullMQ retry 与 Engine 内部 retry 分工明确,不叠加。 + +| 层级 | 覆盖范围 | 触发条件 | +|------|---------|---------| +| **Engine 内部 retry** | 业务级错误(Provider 超时/5xx、输出校验失败) | `JobDefinition.execution.maxRetries` | +| **BullMQ retry** | 进程级故障(SIGKILL、OOM、Worker 崩溃、Redis 断连) | §3.1 `attempts` | + +Engine 内部完成所有业务重试后,一次性 `markFailed` 或 `markSucceeded`。Engine 内部错误**不向 BullMQ 抛出**——BullMQ 仅看到 Worker 进程退出(SIGKILL/OOM)或未响应(stalled),此时触发 BullMQ retry,Worker 重启后从 `markFailed` 或 `markSucceeded` 的幂等语义恢复。 + +**这解释了 §3.1 中 `ai-interactive` attempts=2 的设计**:交互式场景只需要 2 次机会覆盖进程崩溃;业务重试已在 Engine 内部闭环。 #### 5.6 ValidatedOutput 与 OutputHash