fix(threads): unify wait-state ownership across thread types

`hasUnresolvedWaitStatesForAgent` only joined through tasks, so
approval-only threads and ad-hoc threads where the agent is the
requesting party or an explicit participant were invisible. Result:
the heartbeat scheduler silently resumed agents that the board had
asked to wait on, Co-Founder approval chains deadlocked on the
agent side, and any new gate that relies on the query inherits the
same hole.

Root-fix the query at the SQL layer. `hasUnresolvedWaitStateForAgent`
now returns true when any of the three ownership predicates hold:

  - task thread whose task.assignee_agent_id = agent
  - approval thread whose approval.requested_by_agent_id = agent
  - any thread on which the agent is a recorded participant

Each branch is an EXISTS subquery so the query short-circuits on
first match. The thread-integration helper delegates here so the
heartbeat scheduler and the `beforeToolCall` gate share one source
of truth rather than reimplementing half-policies in parallel.
This commit is contained in:
ZacharyZhang-NY committed 2026-04-19 00:51:40 -04:00
1 parent e1693899a2
commit 6a37f41dfd
3 files changed
+46 -13

No files matched your search

+5 -13
View File
@@ -330,22 +330,14 @@ export function createThreadIntegration({ db, logger, emitEvent, publishDomainCh
},
/**
* Checks whether a specific agent has unresolved thread wait states.
* Joins through conversation_threads → tasks to filter by assignee.
* Used by heartbeat scheduler to pause only the affected agent, not all agents.
* Checks whether a specific agent has unresolved thread wait states
* across task, approval, and freeform threads. The query lives on
* `db.hasUnresolvedWaitStateForAgent` so the heartbeat scheduler and
* the pi `beforeToolCall` hook share one source of truth.
*/
hasUnresolvedWaitStatesForAgent(companyId: string, agentId: string): boolean {
try {
const rows = db.queryAll<{ id: string }>(
`SELECT tws.id FROM thread_wait_states tws
JOIN conversation_threads ct ON tws.thread_id = ct.id
JOIN tasks t ON ct.related_entity_id = t.id AND ct.related_entity_type = 'task'
WHERE tws.company_id = ? AND tws.resolved_at IS NULL
AND t.assignee_agent_id = ?
LIMIT 1`,
companyId, agentId,
);
return rows.length > 0;
return db.hasUnresolvedWaitStateForAgent(companyId, agentId);
} catch (err) {
logger.warn(`[thread-integration] hasUnresolvedWaitStatesForAgent query failed (company=${companyId} agent=${agentId}):`, err instanceof Error ? err.message : String(err));
return false;