({
isLoading,
assistantMode = false,
setMessages,
}: Props)
| 38 | * so SDK/-p mode can share it — see print.ts for the headless wiring. |
| 39 | */ |
| 40 | export function useScheduledTasks({ |
| 41 | isLoading, |
| 42 | assistantMode = false, |
| 43 | setMessages, |
| 44 | }: Props): void { |
| 45 | // Latest-value ref so the scheduler's isLoading() getter doesn't capture |
| 46 | // a stale closure. The effect mounts once; isLoading changes every turn. |
| 47 | const isLoadingRef = useRef(isLoading) |
| 48 | isLoadingRef.current = isLoading |
| 49 | |
| 50 | const store = useAppStateStore() |
| 51 | const setAppState = useSetAppState() |
| 52 | |
| 53 | useEffect(() => { |
| 54 | // Runtime gate checked here (not at the hook call site) so the hook |
| 55 | // stays unconditionally mounted — rules-of-hooks forbid wrapping the |
| 56 | // call in a dynamic condition. getFeatureValue_CACHED_WITH_REFRESH |
| 57 | // reads from disk; the 5-min TTL fires a background refetch but the |
| 58 | // effect won't re-run on value flip (assistantMode is the only dep), |
| 59 | // so this guard alone is launch-grain. The mid-session killswitch is |
| 60 | // the isKilled option below — check() polls it every tick. |
| 61 | if (!isKairosCronEnabled()) return |
| 62 | |
| 63 | // System-generated — hidden from queue preview and transcript UI. |
| 64 | // In brief mode, executeForkedSlashCommand runs as a background |
| 65 | // subagent and returns no visible messages. In normal mode, |
| 66 | // isMeta is only propagated for plain-text prompts (via |
| 67 | // processTextPrompt); slash commands like /context:fork do not |
| 68 | // forward isMeta, so their messages remain visible in the |
| 69 | // transcript. This is acceptable since normal mode is not the |
| 70 | // primary use case for scheduled tasks. |
| 71 | const enqueueForLead = (prompt: string) => |
| 72 | enqueuePendingNotification({ |
| 73 | value: prompt, |
| 74 | mode: 'prompt', |
| 75 | priority: 'later', |
| 76 | isMeta: true, |
| 77 | // Threaded through to cc_workload= in the billing-header |
| 78 | // attribution block so the API can serve cron-initiated requests |
| 79 | // at lower QoS when capacity is tight. No human is actively |
| 80 | // waiting on this response. |
| 81 | workload: WORKLOAD_CRON, |
| 82 | }) |
| 83 | |
| 84 | const scheduler = createCronScheduler({ |
| 85 | // Missed-task surfacing (onFire fallback). Teammate crons are always |
| 86 | // session-only (durable:false) so they never appear in the missed list, |
| 87 | // which is populated from disk at scheduler startup — this path only |
| 88 | // handles team-lead durable crons. |
| 89 | onFire: enqueueForLead, |
| 90 | // Normal fires receive the full CronTask so we can route by agentId. |
| 91 | onFireTask: task => { |
| 92 | if (task.agentId) { |
| 93 | const teammate = findTeammateTaskByAgentId( |
| 94 | task.agentId, |
| 95 | store.getState().tasks, |
| 96 | ) |
| 97 | if (teammate && !isTerminalTaskStatus(teammate.status)) { |
nothing calls this directly
no test coverage detected