Skip to content

Tasks: add blocked flow retry state#58204

Merged
mbelinky merged 1 commit into
mainfrom
workspace/flow-blocked-unblock-v1
Mar 31, 2026
Merged

Tasks: add blocked flow retry state#58204
mbelinky merged 1 commit into
mainfrom
workspace/flow-blocked-unblock-v1

Conversation

@mbelinky
Copy link
Copy Markdown
Contributor

Summary

  • persist blocked metadata on one-task flows so a parent flow can carry the blocker reason directly
  • let blocked one-task flows reopen cleanly by creating a replacement queued or running task on the same flow
  • keep the slice narrow: no multi-step orchestration yet, just one-task blocked state plus retry plumbing

Testing

  • pnpm exec oxlint src/tasks/flow-registry.types.ts src/tasks/flow-registry.ts src/tasks/flow-registry.store.sqlite.ts src/tasks/flow-registry.test.ts src/tasks/flow-registry.store.test.ts src/tasks/task-registry.ts src/tasks/task-executor.ts src/tasks/task-executor.test.ts
  • pnpm exec vitest run src/tasks/task-executor.test.ts src/tasks/flow-registry.test.ts src/tasks/flow-registry.store.test.ts src/tasks/task-registry.test.ts -t "blocked|retry|flow|preserves endedAt|minimal defaults|auto-creates|fallback" --maxWorkers=1

Manual checklist

  • run a detached ACP/subagent task that ends blocked and confirm the parent flow stores the blocker summary
  • retry that blocked flow and confirm the same flow reopens cleanly instead of fragmenting into a new job
  • try retrying a non-blocked flow and confirm it is refused
  • restart/restore and confirm blocked flow metadata survives

@openclaw-barnacle openclaw-barnacle Bot added size: M maintainer Maintainer-authored PR labels Mar 31, 2026
@mbelinky mbelinky merged commit 8d94200 into main Mar 31, 2026
23 of 28 checks passed
@mbelinky mbelinky deleted the workspace/flow-blocked-unblock-v1 branch March 31, 2026 07:33
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Mar 31, 2026

Greptile Summary

This PR wires up blocked-flow state persistence and clean retry plumbing for one-task flows. blockedTaskId/blockedSummary are propagated from the terminal task through syncFlowFromTaskupdateFlowRecordById → SQLite, and a new ensureColumn helper migrates existing databases non-destructively. The two new executor exports gate on both flow status and latest-task outcome before allowing a retry, and the flow is re-synced automatically by the new syncFlowFromTask call inside createTaskRecord.

  • No correctness or data-integrity bugs found
  • P2: ensureColumn interpolates table/column names into SQL DDL without an identifier-safety guard (safe today with hardcoded callers, worth hardening)
  • P2: listTasksForFlowId does a full O(n) scan; a taskIdsByFlowId index (matching taskIdsBySessionKey) would keep findLatestTaskForFlowId fast as flow task counts grow

Confidence Score: 5/5

Safe to merge — no correctness bugs found; all remaining findings are P2 style/performance suggestions.

The null/undefined sentinel pattern for clearing blocked fields and endedAt is consistent throughout. SQLite migration is safe with PRAGMA guard. Retry gating double-checks both flow and task status. All manual checklist paths are covered by automated tests.

No files require special attention; P2 notes on task-registry.ts and flow-registry.store.sqlite.ts do not block merge.

Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/tasks/flow-registry.store.sqlite.ts
Line: 191-202

Comment:
**SQL identifiers interpolated without sanitization in `ensureColumn`**

`tableName`, `columnName`, and `columnDefinition` are all interpolated directly into raw SQL strings via template literals:

```typescript
const rows = db.prepare(`PRAGMA table_info(${tableName})`).all()
db.exec(`ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${columnDefinition};`);
```

DDL statements don't support `?`-style parameterization, so the interpolation itself is unavoidable. However, the function currently has no guard against non-literal arguments. The two current call sites are both hardcoded and safe, but if this utility is reused elsewhere with a runtime-derived string, it becomes a SQL injection surface.

Consider adding a small allowlist check or asserting that the arguments match a safe identifier pattern (e.g. `/^[A-Za-z_][A-Za-z0-9_]*$/`) before executing.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/tasks/task-registry.ts
Line: 1370-1395

Comment:
**`listTasksForFlowId` does a full O(n) scan with no index**

`listTasksForFlowId` iterates the entire `tasks` Map to find matching entries:

```typescript
return [...tasks.values()]
  .map((task, insertionIndex) =>
    task.parentFlowId?.trim() === normalizedFlowId ? { ...cloneTaskRecord(task), insertionIndex } : null,
  )
  ...
```

The comparable `listTasksForSessionKey` avoids this by maintaining a dedicated `taskIdsBySessionKey` index (a `Map<string, Set<string>>`). When the task registry grows large, this becomes the only hot per-flow lookup that degrades linearly.

Since `findLatestTaskForFlowId` is on the retry critical path, it's worth introducing a `taskIdsByFlowId` index in the same style as `taskIdsBySessionKey` in a follow-up.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "Tasks: add blocked flow retry state" | Re-trigger Greptile

Comment on lines +191 to +202
function ensureColumn(
db: DatabaseSync,
tableName: string,
columnName: string,
columnDefinition: string,
) {
const rows = db.prepare(`PRAGMA table_info(${tableName})`).all() as Array<{ name?: string }>;
if (rows.some((row) => row.name === columnName)) {
return;
}
db.exec(`ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${columnDefinition};`);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 SQL identifiers interpolated without sanitization in ensureColumn

tableName, columnName, and columnDefinition are all interpolated directly into raw SQL strings via template literals:

const rows = db.prepare(`PRAGMA table_info(${tableName})`).all()
db.exec(`ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${columnDefinition};`);

DDL statements don't support ?-style parameterization, so the interpolation itself is unavoidable. However, the function currently has no guard against non-literal arguments. The two current call sites are both hardcoded and safe, but if this utility is reused elsewhere with a runtime-derived string, it becomes a SQL injection surface.

Consider adding a small allowlist check or asserting that the arguments match a safe identifier pattern (e.g. /^[A-Za-z_][A-Za-z0-9_]*$/) before executing.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/tasks/flow-registry.store.sqlite.ts
Line: 191-202

Comment:
**SQL identifiers interpolated without sanitization in `ensureColumn`**

`tableName`, `columnName`, and `columnDefinition` are all interpolated directly into raw SQL strings via template literals:

```typescript
const rows = db.prepare(`PRAGMA table_info(${tableName})`).all()
db.exec(`ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${columnDefinition};`);
```

DDL statements don't support `?`-style parameterization, so the interpolation itself is unavoidable. However, the function currently has no guard against non-literal arguments. The two current call sites are both hardcoded and safe, but if this utility is reused elsewhere with a runtime-derived string, it becomes a SQL injection surface.

Consider adding a small allowlist check or asserting that the arguments match a safe identifier pattern (e.g. `/^[A-Za-z_][A-Za-z0-9_]*$/`) before executing.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +1370 to +1395
export function listTasksForFlowId(flowId: string): TaskRecord[] {
ensureTaskRegistryReady();
const normalizedFlowId = flowId.trim();
if (!normalizedFlowId) {
return [];
}
return [...tasks.values()]
.map((task, insertionIndex) =>
task.parentFlowId?.trim() === normalizedFlowId
? { ...cloneTaskRecord(task), insertionIndex }
: null,
)
.filter(
(
task,
): task is TaskRecord & {
insertionIndex: number;
} => Boolean(task),
)
.toSorted(compareTasksNewestFirst)
.map(({ insertionIndex: _, ...task }) => task);
}

export function findLatestTaskForFlowId(flowId: string): TaskRecord | undefined {
const task = listTasksForFlowId(flowId)[0];
return task ? cloneTaskRecord(task) : undefined;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 listTasksForFlowId does a full O(n) scan with no index

listTasksForFlowId iterates the entire tasks Map to find matching entries:

return [...tasks.values()]
  .map((task, insertionIndex) =>
    task.parentFlowId?.trim() === normalizedFlowId ? { ...cloneTaskRecord(task), insertionIndex } : null,
  )
  ...

The comparable listTasksForSessionKey avoids this by maintaining a dedicated taskIdsBySessionKey index (a Map<string, Set<string>>). When the task registry grows large, this becomes the only hot per-flow lookup that degrades linearly.

Since findLatestTaskForFlowId is on the retry critical path, it's worth introducing a taskIdsByFlowId index in the same style as taskIdsBySessionKey in a follow-up.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/tasks/task-registry.ts
Line: 1370-1395

Comment:
**`listTasksForFlowId` does a full O(n) scan with no index**

`listTasksForFlowId` iterates the entire `tasks` Map to find matching entries:

```typescript
return [...tasks.values()]
  .map((task, insertionIndex) =>
    task.parentFlowId?.trim() === normalizedFlowId ? { ...cloneTaskRecord(task), insertionIndex } : null,
  )
  ...
```

The comparable `listTasksForSessionKey` avoids this by maintaining a dedicated `taskIdsBySessionKey` index (a `Map<string, Set<string>>`). When the task registry grows large, this becomes the only hot per-flow lookup that degrades linearly.

Since `findLatestTaskForFlowId` is on the retry critical path, it's worth introducing a `taskIdsByFlowId` index in the same style as `taskIdsBySessionKey` in a follow-up.

How can I resolve this? If you propose a fix, please make it concise.

pgondhi987 pushed a commit to pgondhi987/openclaw that referenced this pull request Mar 31, 2026
pgondhi987 pushed a commit to pgondhi987/openclaw that referenced this pull request Mar 31, 2026
lovewanwan pushed a commit to lovewanwan/openclaw that referenced this pull request Apr 28, 2026
ogt-redknie pushed a commit to ogt-redknie/OPENX that referenced this pull request May 2, 2026
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 9, 2026
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

maintainer Maintainer-authored PR size: M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant