[FEATURE]: On-idle background processing #3721

Open
opened 2026-02-16 17:41:14 -05:00 by yindo · 2 comments
Owner

Originally created by @synergiator on GitHub (Dec 21, 2025).

Originally assigned to: @thdxr on GitHub.

Feature hasn't been suggested before.

  • I have verified this feature I'm about to request hasn't been suggested before.

Describe the enhancement you want to request

It happens quite offen that the TUI becomes idle as it waits for the user to return and follow up.
If open ("ready") beads exist of some chore-like "unattended processing is desired", there shall be an option so that the system should start processing them in background and commit local change requests upon success which could be reviewed on later time.

One important component for doing so exists already, like the beads (bd) local issue storage.

This would streamline productive work and establish users' well being due to UX risk research.

Note: the following content is AI sourced.

What is possible as a workaround

OpenCode “timing awareness” and background tasks (practical view)

OpenCode does not automatically “notice” you stepped away and then proactively run work; by default it runs when you trigger a prompt/command/tool action (SST et al., n.d.). However, OpenCode exposes enough lifecycle and API surface that you can implement small background tasks during “waiting” states (SST et al., n.d.).

1) In-process option: plugin events (e.g., session.idle)

  • OpenCode plugins can subscribe to session lifecycle events (including an idle signal) and can execute local commands from the plugin runtime (SST et al., n.d.).
  • This supports “cheap” background tasks such as: quick linters, formatting checks, small unit-test subsets, index refresh, or generating a short status summary while you are away (SST et al., n.d.).

Operational caveat: In practice, an “idle” event may not always mean “the agent is fully done and waiting for user input.” Some reports/requests indicate timing semantics can be ambiguous and a stronger “finished” signal may be desirable (SST et al., n.d.).

2) Out-of-process option: server mode + event stream + async dispatch

  • OpenCode can run as a server (opencode serve) exposing an HTTP API and event streams (SST et al., n.d.).
  • A sidecar/controller can subscribe to the server’s event stream(s) and, upon “idle” (or a more conservative composite condition), trigger:
    • local tasks in your environment, and/or
    • an asynchronous LLM job via an async prompt endpoint (SST et al., n.d.).
  • This pattern is well-suited for throttling, budgeting, and observability (you can decide “only run X when idle for >N seconds,” “only run once per commit,” etc.) (SST et al., n.d.).

3) MVP guidance (low risk)

  • Start with plugin-driven background tasks that are deterministic and fast (lint/format/quick tests) on an idle-style signal (SST et al., n.d.).
  • If you want LLM work while you are away, prefer a sidecar using server mode + async prompt dispatch so you can rate-limit and avoid surprising interactive UX behavior (SST et al., n.d.).
  • Design tasks to be idempotent and safe to re-run because “idle” semantics may not be perfectly aligned to “finished” in every integration (SST et al., n.d.).

4) Related execution mode: CI/workflow triggers

OpenCode can also be invoked in GitHub workflow contexts (triggered explicitly by comments/events rather than inferred “user absence���), which is another way to “run in the background,” but it is not the same as session-idle opportunistic work (SST et al., n.d.).


References

UX Risks in Agentic Coding Workflows Under Variable Turnaround Time — Compiled Insights

Why this is not just subjective

  1. Delay and delay-variability degrade perceived control and satisfaction.
    Interactive systems research shows users form strong expectations about response time; unpredictable delays reduce perceived control and can increase frustration and error likelihood. (Shneiderman 1984; Nielsen 1993; Kohrs et al. 2014)

  2. Interruptions impose measurable cognitive “resumption costs.”
    When you switch away during an agent run, you later pay a context reconstruction cost; interruptions are associated with increased stress/frustration even when people compensate by working faster. (Mark et al. 2008; Parnin & Rugaber 2009)

  3. Developer work is already fragmented; agent latency variance compounds it.
    Empirical software engineering studies indicate frequent task switching and coordination overhead in real developer days; adding irregular “wait → return → verify” loops increases fragmentation. (Meyer et al. 2017; Meyer et al. 2019)

  4. AI coding assistance can shift time into prompting, waiting, and verification.
    Evidence is mixed: some controlled results show speedups, while other rigorous evaluations report slowdowns for experienced developers in familiar codebases, with contributors including overhead and verification. (Peng et al. 2023; Becker et al. 2025; METR 2025; Reuters 2025)

  5. Proactive/agentic designs are explicitly a trade-off between assistance and disruption.
    Work on proactive programming support highlights that assistance can disrupt flow unless users get clear signals, context, and control mechanisms (e.g., progress/presence indicators). (Pu et al. 2025)


The failure mode you described (canonical loop)

  • You delegate intent to the agent (prompt).
  • The agent returns after variable delay (sometimes 1 minute, sometimes 6).
  • You either wait (costly) or context-switch (also costly).
  • When output arrives, you must rebuild context, interpret changes, verify correctness, decide next steps.
  • Net result: attention fragmentation + stress + lost follow-up windows, especially under time pressure. (Mark et al. 2008; Parnin & Rugaber 2009; Shneiderman 1984)

Known scenarios that predictably worsen the experience

  • High variance turnaround time (not just “slow,” but unpredictable) → planning attention becomes hard. (Shneiderman 1984; Nielsen 1993)
  • Long multi-step agent runs without intermediate checkpoints → all-or-nothing waiting. (Pu et al. 2025)
  • Large diffs / high verification burden → time shifts from implementation to review/rework. (METR 2025; Becker et al. 2025)
  • High time-pressure contexts → missed follow-ups feel costly; stress increases. (Mark et al. 2008; Kohrs et al. 2014)

Compact mitigation playbook (patterns that map to the mechanisms)

A) Make waiting predictable and actionable

  • Short checkpoints over long runs: split work into staged outputs (plan → patch → tests → finalize). (Pu et al. 2025)
  • Visible progress/presence cues: treat “what stage is it in” as first-class; reduces disruption in proactive designs. (Pu et al. 2025)
  • Explicit “return points”: decide in advance when you will check back (e.g., after unit tests), to avoid constant vigilance. (Nielsen 1993)

B) Reduce iterations by raising prompt specificity and acceptance criteria

  • Predefine verification contract: required tests, lint, acceptance criteria, performance constraints.
  • Constrain diff surface: specify files/modules, boundaries, and “do not touch” areas to reduce review load. (METR 2025; Becker et al. 2025)

C) Optimize for lower variance (not only lower mean latency)

  • Stabilize runtime: warm caches, consistent model/tool selection, avoid unpredictable tool chains.
  • Deterministic decomposition: prefer predictable sub-steps with explicit stop conditions when uncertain.

D) Use agents selectively by task type

  • Delegate bounded/mechanical tasks (boilerplate, repetitive refactors, test scaffolds) where benefits are more consistent. (Peng et al. 2023)
  • Be cautious delegating high-context work in familiar repos if verification/rework is likely to dominate. (METR 2025; Becker et al. 2025)

Practical operating modes (choose deliberately)

  1. Tight-loop mode (time pressure):

    • Keep runs short; require checkpoints; stop on uncertainty; decide quickly per checkpoint.
    • Goal: minimize resumption lag and missed follow-up windows. (Parnin & Rugaber 2009; Mark et al. 2008)
  2. Batch mode (low time pressure):

    • Allow longer runs, but schedule explicit return points and do only low-context parallel tasks.
    • Goal: avoid expensive context switches.

References

  • (Becker et al. 2025) Becker, J. and others. Measuring the Impact of Early-2025 AI on Experienced Open-Source Developer Productivity. arXiv:2507.09089.
  • (Kohrs et al. 2014) Kohrs, C., Hrabal, D., Angenstein, N., Boucsein, W. Delayed system response times affect immediate physiology and the dynamics of subsequent button press behavior. Psychophysiology, 2014.
  • (Mark et al. 2008) Mark, G., Gudith, D., Klocke, U. The Cost of Interrupted Work: More Speed and Stress. CHI 2008.
  • (METR 2025) METR. Measuring the Impact of Early-2025 AI on Experienced Open-Source Developer Productivity. Blog post, retrieved on 2025-12-21.
  • (Meyer et al. 2017) Meyer, A. N. et al. The Work Life of Developers: Activities, Switches and Perceived Productivity. IEEE TSE, 2017.
  • (Meyer et al. 2019) Meyer, A. N. et al. Today Was a Good Day: The Daily Life of Software Developers. IEEE TSE, 2019.
  • (Nielsen 1993) Nielsen, J. Response Times: The 3 Important Limits. Nielsen Norman Group, retrieved on 2025-12-21.
  • (Parnin & Rugaber 2009) Parnin, C., Rugaber, S. Resumption Strategies for Interrupted Programming Tasks. ICPC 2009.
  • (Peng et al. 2023) Peng, S. et al. The Impact of AI on Developer Productivity: Evidence from GitHub Copilot. arXiv:2302.06590.
  • (Pu et al. 2025) Pu, K. et al. Assistance or Disruption? Exploring and Evaluating the Design and Trade-offs of Proactive AI Programming Support. arXiv:2502.18658.
  • (Reuters 2025) Reuters. AI slows down some experienced software developers, study finds. retrieved on 2025-12-21.
  • (Shneiderman 1984) Shneiderman, B. Response Time and Display Rate in Human Performance with Computers. ACM Computing Surveys, 1984.
Originally created by @synergiator on GitHub (Dec 21, 2025). Originally assigned to: @thdxr on GitHub. ### Feature hasn't been suggested before. - [x] I have verified this feature I'm about to request hasn't been suggested before. ### Describe the enhancement you want to request It happens quite offen that the TUI becomes idle as it waits for the user to return and follow up. If open ("ready") beads exist of some chore-like "unattended processing is desired", there shall be an option so that the system should start processing them in background and commit local change requests upon success which could be reviewed on later time. One important component for doing so exists already, like the beads (bd) local issue storage. This would streamline productive work and establish users' well being due to UX risk research. Note: the following content is AI sourced. # What is possible as a workaround ## OpenCode “timing awareness” and background tasks (practical view) OpenCode does not automatically “notice” you stepped away and then proactively run work; by default it runs when you trigger a prompt/command/tool action (SST et al., n.d.). However, OpenCode exposes enough lifecycle and API surface that you can *implement* small background tasks during “waiting” states (SST et al., n.d.). ## 1) In-process option: plugin events (e.g., `session.idle`) - OpenCode plugins can subscribe to session lifecycle events (including an idle signal) and can execute local commands from the plugin runtime (SST et al., n.d.). - This supports “cheap” background tasks such as: quick linters, formatting checks, small unit-test subsets, index refresh, or generating a short status summary while you are away (SST et al., n.d.). **Operational caveat:** In practice, an “idle” event may not always mean “the agent is fully done and waiting for user input.” Some reports/requests indicate timing semantics can be ambiguous and a stronger “finished” signal may be desirable (SST et al., n.d.). ## 2) Out-of-process option: server mode + event stream + async dispatch - OpenCode can run as a server (`opencode serve`) exposing an HTTP API and event streams (SST et al., n.d.). - A sidecar/controller can subscribe to the server’s event stream(s) and, upon “idle” (or a more conservative composite condition), trigger: - local tasks in your environment, and/or - an asynchronous LLM job via an async prompt endpoint (SST et al., n.d.). - This pattern is well-suited for throttling, budgeting, and observability (you can decide “only run X when idle for >N seconds,” “only run once per commit,” etc.) (SST et al., n.d.). ## 3) MVP guidance (low risk) - Start with plugin-driven background tasks that are deterministic and fast (lint/format/quick tests) on an idle-style signal (SST et al., n.d.). - If you want LLM work while you are away, prefer a sidecar using server mode + async prompt dispatch so you can rate-limit and avoid surprising interactive UX behavior (SST et al., n.d.). - Design tasks to be idempotent and safe to re-run because “idle” semantics may not be perfectly aligned to “finished” in every integration (SST et al., n.d.). ## 4) Related execution mode: CI/workflow triggers OpenCode can also be invoked in GitHub workflow contexts (triggered explicitly by comments/events rather than inferred “user absence���), which is another way to “run in the background,” but it is not the same as session-idle opportunistic work (SST et al., n.d.). --- ## References - SST et al. (n.d.). *OpenCode Plugins Documentation*. Retrieved on 2025-12-21 from https://opencode.ai/docs/plugins/ - SST et al. (n.d.). *OpenCode Server (HTTP API & Events) Documentation*. Retrieved on 2025-12-21 from https://opencode.ai/docs/server - SST et al. (n.d.). *OpenCode SDK Documentation*. Retrieved on 2025-12-21 from https://opencode.ai/docs/sdk/ - SST et al. (n.d.). *OpenCode GitHub Integrations / Workflows Documentation*. Retrieved on 2025-12-21 from https://opencode.ai/docs/github/ - SST et al. (n.d.). *OpenCode GitHub Issue: idle/finished event semantics discussion*. Retrieved on 2025-12-21 from https://github.com/sst/opencode/issues/3815 - SST et al. (n.d.). *OpenCode GitHub Issue: event/lifecycle request (finished signal)*. Retrieved on 2025-12-21 from https://github.com/sst/opencode/issues/2021 - SST et al. (n.d.). *OpenCode GitHub Issue: additional event semantics/behavior discussion*. Retrieved on 2025-12-21 from https://github.com/sst/opencode/issues/3534 - Boud, C. (n.d.). *Coding Agents Internals: OpenCode Deep Dive (blog)*. Retrieved on 2025-12-21 from https://cefboud.com/posts/coding-agents-internals-opencode-deepdive/ - SST et al. (n.d.). *opencode-sdk-python (repository)*. Retrieved on 2025-12-21 from https://github.com/sst/opencode-sdk-python - SST et al. (n.d.). *opencode-sdk-go (repository)*. Retrieved on 2025-12-21 from https://github.com/sst/opencode-sdk-go # UX Risks in Agentic Coding Workflows Under Variable Turnaround Time — Compiled Insights ## Why this is not just subjective 1) **Delay and delay-variability degrade perceived control and satisfaction.** Interactive systems research shows users form strong expectations about response time; unpredictable delays reduce perceived control and can increase frustration and error likelihood. (Shneiderman 1984; Nielsen 1993; Kohrs et al. 2014) 2) **Interruptions impose measurable cognitive “resumption costs.”** When you switch away during an agent run, you later pay a context reconstruction cost; interruptions are associated with increased stress/frustration even when people compensate by working faster. (Mark et al. 2008; Parnin & Rugaber 2009) 3) **Developer work is already fragmented; agent latency variance compounds it.** Empirical software engineering studies indicate frequent task switching and coordination overhead in real developer days; adding irregular “wait → return → verify” loops increases fragmentation. (Meyer et al. 2017; Meyer et al. 2019) 4) **AI coding assistance can shift time into prompting, waiting, and verification.** Evidence is mixed: some controlled results show speedups, while other rigorous evaluations report slowdowns for experienced developers in familiar codebases, with contributors including overhead and verification. (Peng et al. 2023; Becker et al. 2025; METR 2025; Reuters 2025) 5) **Proactive/agentic designs are explicitly a trade-off between assistance and disruption.** Work on proactive programming support highlights that assistance can disrupt flow unless users get clear signals, context, and control mechanisms (e.g., progress/presence indicators). (Pu et al. 2025) --- ## The failure mode you described (canonical loop) - You delegate intent to the agent (prompt). - The agent returns after **variable delay** (sometimes 1 minute, sometimes 6). - You either wait (costly) or context-switch (also costly). - When output arrives, you must **rebuild context**, interpret changes, verify correctness, decide next steps. - Net result: attention fragmentation + stress + lost follow-up windows, especially under time pressure. (Mark et al. 2008; Parnin & Rugaber 2009; Shneiderman 1984) --- ## Known scenarios that predictably worsen the experience - **High variance turnaround time** (not just “slow,” but unpredictable) → planning attention becomes hard. (Shneiderman 1984; Nielsen 1993) - **Long multi-step agent runs without intermediate checkpoints** → all-or-nothing waiting. (Pu et al. 2025) - **Large diffs / high verification burden** → time shifts from implementation to review/rework. (METR 2025; Becker et al. 2025) - **High time-pressure contexts** → missed follow-ups feel costly; stress increases. (Mark et al. 2008; Kohrs et al. 2014) --- ## Compact mitigation playbook (patterns that map to the mechanisms) ### A) Make waiting predictable and actionable - **Short checkpoints over long runs:** split work into staged outputs (plan → patch → tests → finalize). (Pu et al. 2025) - **Visible progress/presence cues:** treat “what stage is it in” as first-class; reduces disruption in proactive designs. (Pu et al. 2025) - **Explicit “return points”:** decide in advance when you will check back (e.g., after unit tests), to avoid constant vigilance. (Nielsen 1993) ### B) Reduce iterations by raising prompt specificity and acceptance criteria - **Predefine verification contract:** required tests, lint, acceptance criteria, performance constraints. - **Constrain diff surface:** specify files/modules, boundaries, and “do not touch” areas to reduce review load. (METR 2025; Becker et al. 2025) ### C) Optimize for lower variance (not only lower mean latency) - **Stabilize runtime:** warm caches, consistent model/tool selection, avoid unpredictable tool chains. - **Deterministic decomposition:** prefer predictable sub-steps with explicit stop conditions when uncertain. ### D) Use agents selectively by task type - **Delegate bounded/mechanical tasks** (boilerplate, repetitive refactors, test scaffolds) where benefits are more consistent. (Peng et al. 2023) - **Be cautious delegating high-context work** in familiar repos if verification/rework is likely to dominate. (METR 2025; Becker et al. 2025) --- ## Practical operating modes (choose deliberately) 1) **Tight-loop mode (time pressure):** - Keep runs short; require checkpoints; stop on uncertainty; decide quickly per checkpoint. - Goal: minimize resumption lag and missed follow-up windows. (Parnin & Rugaber 2009; Mark et al. 2008) 2) **Batch mode (low time pressure):** - Allow longer runs, but schedule explicit return points and do only low-context parallel tasks. - Goal: avoid expensive context switches. --- # References - (Becker et al. 2025) Becker, J. and others. *Measuring the Impact of Early-2025 AI on Experienced Open-Source Developer Productivity.* arXiv:2507.09089. - (Kohrs et al. 2014) Kohrs, C., Hrabal, D., Angenstein, N., Boucsein, W. *Delayed system response times affect immediate physiology and the dynamics of subsequent button press behavior.* Psychophysiology, 2014. - (Mark et al. 2008) Mark, G., Gudith, D., Klocke, U. *The Cost of Interrupted Work: More Speed and Stress.* CHI 2008. - (METR 2025) METR. *Measuring the Impact of Early-2025 AI on Experienced Open-Source Developer Productivity.* Blog post, retrieved on 2025-12-21. - (Meyer et al. 2017) Meyer, A. N. et al. *The Work Life of Developers: Activities, Switches and Perceived Productivity.* IEEE TSE, 2017. - (Meyer et al. 2019) Meyer, A. N. et al. *Today Was a Good Day: The Daily Life of Software Developers.* IEEE TSE, 2019. - (Nielsen 1993) Nielsen, J. *Response Times: The 3 Important Limits.* Nielsen Norman Group, retrieved on 2025-12-21. - (Parnin & Rugaber 2009) Parnin, C., Rugaber, S. *Resumption Strategies for Interrupted Programming Tasks.* ICPC 2009. - (Peng et al. 2023) Peng, S. et al. *The Impact of AI on Developer Productivity: Evidence from GitHub Copilot.* arXiv:2302.06590. - (Pu et al. 2025) Pu, K. et al. *Assistance or Disruption? Exploring and Evaluating the Design and Trade-offs of Proactive AI Programming Support.* arXiv:2502.18658. - (Reuters 2025) Reuters. *AI slows down some experienced software developers, study finds.* retrieved on 2025-12-21. - (Shneiderman 1984) Shneiderman, B. *Response Time and Display Rate in Human Performance with Computers.* ACM Computing Surveys, 1984.
yindo added the discussion label 2026-02-16 17:41:14 -05:00
Author
Owner

@github-actions[bot] commented on GitHub (Dec 21, 2025):

This issue might be a duplicate of existing issues. Please check:

  • #5887: [feat] True Async/Background Sub-Agent Delegation - Addresses async/background execution for delegated tasks while maintaining interactive control
  • #5408: [FEATURE]: Delayed queue feature - Addresses queuing work to run when OpenCode is waiting for user input
  • #5333: [FEATURE]: Graceful handling of queued messages after session interrupt - Addresses queue persistence and message handling during idle states
  • #3815: What is the deterministic way to monitor a session is completed - Discusses event semantics and finished signal detection mentioned in your references

Feel free to ignore if none of these address your specific case.

@github-actions[bot] commented on GitHub (Dec 21, 2025): This issue might be a duplicate of existing issues. Please check: - #5887: [feat] True Async/Background Sub-Agent Delegation - Addresses async/background execution for delegated tasks while maintaining interactive control - #5408: [FEATURE]: Delayed queue feature - Addresses queuing work to run when OpenCode is waiting for user input - #5333: [FEATURE]: Graceful handling of queued messages after session interrupt - Addresses queue persistence and message handling during idle states - #3815: What is the deterministic way to monitor a session is completed - Discusses event semantics and finished signal detection mentioned in your references Feel free to ignore if none of these address your specific case.
Author
Owner

@synergiator commented on GitHub (Dec 21, 2025):

TWIMC - after some iterations, I have now something I can work with, though I think that on long term we should aim to have an opencode bot to do chores inside CICD just like the renovate bot.

This opencode plugins allows to process open beads in background.

import type { Plugin } from "@opencode-ai/plugin";
import { appendFile } from "node:fs/promises";

type Bead = {
  id: string;
  title?: string;
  description?: string;
  priority?: number;
  created_at?: string;
  updated_at?: string;
  status?: string;
  issue_type?: string;
};

const LOG_FILE = process.env.AUTO_BEAD_LOG ?? "/tmp/opencode-auto-bead-runner.log";

const ENABLED = (process.env.AUTO_BEAD_ENABLED ?? "1") === "1";
const IDLE_DELAY_MS = Number(process.env.AUTO_BEAD_IDLE_DELAY_MS ?? "2000");
const COOLDOWN_MS = Number(process.env.AUTO_BEAD_COOLDOWN_MS ?? "1000");

const ENABLE_TOAST = (process.env.AUTO_BEAD_TOAST ?? "0") === "1";

// Claim = move bead to in_progress before running it
const CLAIM = (process.env.AUTO_BEAD_CLAIM ?? "1") === "1";

// Autorun = dispatch a prompt to OpenCode so it actually edits files & completes the bead
const AUTORUN = (process.env.AUTO_BEAD_AUTORUN ?? "1") === "1";

// Hard stop if a worker prompt never returns (eg permission gate / waiting for human)
const PROMPT_TIMEOUT_MS = Number(process.env.AUTO_BEAD_PROMPT_TIMEOUT_MS ?? "90000");

const AGENT = "autobead" //  process.env.AUTO_BEAD_AGENT || undefined;
const MODEL_PROVIDER = process.env.AUTO_BEAD_MODEL_PROVIDER || undefined;
const MODEL_ID = process.env.AUTO_BEAD_MODEL_ID || undefined;

let crashHooksInstalled = false;
let logWriteFailedOnce = false;

function ts() {
  return new Date().toISOString();
}

async function log(line: string) {
  try {
    await appendFile(LOG_FILE, `${ts()} ${line}\n`);
  } catch (e: any) {
    if (!logWriteFailedOnce) {
      logWriteFailedOnce = true;
      console.error("[AutoBeadRunner] log write failed:", e?.message ?? e);
    }
  }
}

function fireAndForget(p: Promise<any>) {
  p.catch((e) => void log(`[async-error] ${String(e?.message ?? e)}`));
}

function sleep(ms: number) {
  return new Promise<void>((r) => setTimeout(r, ms));
}

// Known noisy rejection you observed: session_share file missing.
// This is unrelated to beads; swallow it to keep logs clean.
function isBenignSessionShareNotFound(reason: any): boolean {
  const msg = String(reason?.data?.message ?? reason?.message ?? reason ?? "");
  return msg.includes("session_share") && msg.includes("Resource not found:");
}

function ensureCrashHooksInstalled() {
  if (crashHooksInstalled) return;
  crashHooksInstalled = true;

  process.on("unhandledRejection", (reason: any) => {
    if (isBenignSessionShareNotFound(reason)) {
      void log(
        `[unhandledRejection][swallowed] ${String(
          reason?.data?.message ?? reason?.message ?? reason
        )}`
      );
      return;
    }
    void log(
      `[unhandledRejection] ${String(reason?.data?.message ?? reason?.message ?? reason)}`
    );
  });

  process.on("uncaughtException", (err: any) => {
    void log(`[uncaughtException] ${String(err?.message ?? err)}`);
  });
}

// The SDK may return { data, request, response } (responseStyle="fields").
// Unwrap defensively. (If already flat, return as-is.)
function unwrap<T>(v: any): T {
  return (v && typeof v === "object" && "data" in v ? v.data : v) as T;
}

function parseBeads(jsonText: string): Bead[] {
  const v = JSON.parse(jsonText);
  return Array.isArray(v) ? (v as Bead[]) : [];
}

function beadSortKey(b: Bead) {
  const prio = Number.isFinite(b.priority) ? (b.priority as number) : 999999;

  const created = b.created_at ? Date.parse(b.created_at) : Number.POSITIVE_INFINITY;
  const createdKey = Number.isFinite(created) ? created : Number.POSITIVE_INFINITY;

  const id = b.id ?? "";
  return { prio, createdKey, id };
}

function pickNextBead(beads: Bead[]): Bead | null {
  const sorted = [...beads].sort((a, b) => {
    const ka = beadSortKey(a);
    const kb = beadSortKey(b);
    if (ka.prio !== kb.prio) return ka.prio - kb.prio;
    if (ka.createdKey !== kb.createdKey) return ka.createdKey - kb.createdKey;
    return ka.id.localeCompare(kb.id);
  });
  return sorted.length ? sorted[0] : null;
}

function singleLine(s: string, max = 180) {
  const v = (s ?? "").replace(/\s+/g, " ").trim();
  return v.length > max ? v.slice(0, max - 1) + "…" : v;
}

function textFromParts(msg: any): string {
  const parts = Array.isArray(msg?.parts) ? msg.parts : [];
  return parts
    .filter((p: any) => p && p.type === "text" && typeof p.text === "string")
    .map((p: any) => p.text)
    .join("\n")
    .trim();
}

function buildPrompt(bead: Bead, cwd: string) {
  const title = bead.title ?? "";
  const desc = bead.description ?? "";

  return [
    "You were triggered by an idle-time automation in OpenCode.",
    "",
    "Process exactly ONE bead (the one below) in an isolated worker session.",
    "",
    `Bead ID: ${bead.id}`,
    `Title: ${title}`,
    `Description: ${desc}`,
    "",
    "Constraints:",
    `- Work in repository worktree: ${cwd}`,
    "- Prefer minimal, safe edits.",
    "- Avoid using shell commands unless absolutely required (assume they may require human approval).",
    "- If you need consent/clarification: ask ONE concise question and STOP.",
    "",
    "Output contract (MANDATORY):",
    "End your final response with exactly ONE of:",
    "  AUTO_BEAD_RESULT: SUCCESS",
    "  AUTO_BEAD_RESULT: NEEDS_HUMAN",
    "",
    "If SUCCESS, include a short bullet list of changed files above the marker.",
    "If NEEDS_HUMAN, include your ONE question above the marker.",
  ].join("\n");
}

async function withTimeout<T>(
  p: Promise<T>,
  ms: number,
  onTimeout: () => Promise<void>
): Promise<T> {
  let timer: ReturnType<typeof setTimeout> | null = null;

  const timeoutP = new Promise<T>((_, reject) => {
    timer = setTimeout(() => reject(new Error(`timeout after ${ms}ms`)), ms);
  });

  try {
    return await Promise.race([p, timeoutP]);
  } catch (e: any) {
    const msg = String(e?.message ?? e);
    if (msg.includes("timeout after")) {
      await onTimeout();
    }
    throw e;
  } finally {
    if (timer) clearTimeout(timer);
  }
}

export const AutoBeadRunner: Plugin = async ({ client, $, directory, worktree }) => {
  ensureCrashHooksInstalled();

  const cwd = process.env.AUTO_BEAD_CWD ?? (worktree || directory);
  fireAndForget(log(`[loaded] cwd=${cwd} log=${LOG_FILE}`));

  if (!ENABLED) {
    fireAndForget(log(`[disabled] AUTO_BEAD_ENABLED=0`));
    return { event: async () => {} };
  }

  if (ENABLE_TOAST) {
    fireAndForget(
      client.tui.showToast?.({ body: { message: "[AutoBeadRunner] loaded", variant: "info" } }) ??
        Promise.resolve()
    );
  }

  let lastIdleAt = 0;
  let inFlight = false;
  let scheduled: ReturnType<typeof setTimeout> | null = null;

  async function runBdReady(): Promise<Bead[]> {
    const r = await $`bd ready --json`
      .cwd(cwd)
      .env({ ...process.env, NO_COLOR: "1", TERM: "dumb" })
      .quiet()
      .nothrow();

    const exitCode = Number(r.exitCode ?? -1);
    const stdout = String(r.stdout ?? "").trim();
    const stderr = String(r.stderr ?? "").trim();

    await log(`[bd.ready] exit=${exitCode}`);
    if (stderr) await log(`[bd.ready] stderr=${stderr}`);
    if (!stdout) return [];
    if (exitCode !== 0) return [];

    try {
      return parseBeads(stdout);
    } catch (e: any) {
      await log(`[bd.ready] parse error: ${String(e?.message ?? e)}`);
      await log(`[bd.ready] raw=${stdout}`);
      return [];
    }
  }

  async function bdUpdateStatus(id: string, status: string, notes?: string) {
    const note = notes ? singleLine(notes) : "";
    // Try with notes first; fall back if flag unsupported.
    if (note) {
      const u1 = await $`bd update ${id} --status ${status} --notes ${note} --json`
        .cwd(cwd)
        .env({ ...process.env, NO_COLOR: "1", TERM: "dumb" })
        .quiet()
        .nothrow();
      if (Number(u1.exitCode ?? -1) === 0) {
        await log(`[bd.update] id=${id} status=${status} notes=1 exit=0`);
        return;
      }
      await log(
        `[bd.update] id=${id} status=${status} notes=1 exit=${Number(u1.exitCode ?? -1)} (fallback without notes)`
      );
    }

    const u2 = await $`bd update ${id} --status ${status} --json`
      .cwd(cwd)
      .env({ ...process.env, NO_COLOR: "1", TERM: "dumb" })
      .quiet()
      .nothrow();

    await log(
      `[bd.update] id=${id} status=${status} notes=0 exit=${Number(u2.exitCode ?? -1)} stdout=${singleLine(
        String(u2.stdout ?? "")
      )} stderr=${singleLine(String(u2.stderr ?? ""))}`
    );
  }

  async function abortSession(sessionId: string) {
    const anyClient = client as any;

    try {
      if (typeof (client as any).session?.abort === "function") {
        await log(`[session.abort] via client.session.abort id=${sessionId}`);
        await (client as any).session.abort({ path: { id: sessionId } });
        return;
      }
      if (typeof anyClient.postSessionByIdAbort === "function") {
        await log(`[session.abort] via postSessionByIdAbort id=${sessionId}`);
        await anyClient.postSessionByIdAbort({ path: { id: sessionId } });
        return;
      }
      await log(`[session.abort] not available in client; cannot abort id=${sessionId}`);
    } catch (e: any) {
      await log(`[session.abort ERROR] id=${sessionId} ${String(e?.message ?? e)}`);
    }
  }

  async function createWorkerSession(parentSessionId: string | undefined, bead: Bead): Promise<string> {
    const title = `[AUTO-BEAD] ${bead.id} — ${bead.title ?? ""}`.trim();

    const res = await client.session.create({
      body: {
        title,
        // Keeping linkage optional; if you don’t want any coupling, remove parentID.
        ...(parentSessionId ? { parentID: parentSessionId } : {}),
      } as any,
    });

    const s = unwrap<any>(res);
    const id = s?.id as string | undefined;

    if (!id) {
      throw new Error(`session.create returned no id (keys=${Object.keys(s ?? {}).join(",")})`);
    }

    await log(`[session.create] id=${id} title=${JSON.stringify(title)}`);
    return id;
  }

  async function promptInSession(sessionId: string, promptText: string) {
    const body: any = { parts: [{ type: "text", text: promptText }] };
    if (AGENT) body.agent = AGENT;
    if (MODEL_PROVIDER && MODEL_ID) body.model = { providerID: MODEL_PROVIDER, modelID: MODEL_ID };

    await log(`[dispatch] session.prompt session=${sessionId}`);
    const res = await client.session.prompt({ path: { id: sessionId }, body } as any);
    return unwrap<any>(res);
  }

  async function handleTrigger(sessionId: string | undefined) {
    if (inFlight) return;
    inFlight = true;

    let bead: Bead | null = null;
    let workerSessionId: string | null = null;

    try {
      await log(`[handleTrigger] idle; delay=${IDLE_DELAY_MS}ms sessionId=${sessionId ?? "?"}`);
      if (IDLE_DELAY_MS > 0) await sleep(IDLE_DELAY_MS);

      const beads = await runBdReady();
      bead = pickNextBead(beads);

      if (!bead) {
        await log(`[handleTrigger] no ready beads`);
        if (ENABLE_TOAST) {
          fireAndForget(
            client.tui.showToast?.({ body: { message: "[AutoBeadRunner] no ready beads", variant: "info" } }) ??
              Promise.resolve()
          );
        }
        return;
      }

      await log(`[handleTrigger] nextReady=${bead.id}`);

      if (ENABLE_TOAST) {
        fireAndForget(
          client.tui.showToast?.({
            body: { message: `[AutoBeadRunner] next bead: ${bead.id}`, variant: "success" },
          }) ?? Promise.resolve()
        );
      }

      if (CLAIM) {
        await bdUpdateStatus(bead.id, "in_progress");
      } else {
        await log(`[claim] disabled (AUTO_BEAD_CLAIM=0)`);
      }

      if (!AUTORUN) {
        await log(`[autorun] disabled (AUTO_BEAD_AUTORUN=0)`);
        return;
      }

      // Create isolated worker session (NOT the idle TUI session).
      workerSessionId = await createWorkerSession(sessionId, bead);

      const promptText = buildPrompt(bead, cwd);
      await log(`[autorun] dispatching bead=${bead.id} targetSession=${workerSessionId}`);

      const msg = await withTimeout(
        promptInSession(workerSessionId, promptText),
        PROMPT_TIMEOUT_MS,
        async () => {
          await log(`[autorun][timeout] bead=${bead?.id ?? "?"} session=${workerSessionId}`);
          if (workerSessionId) await abortSession(workerSessionId);
        }
      );

      const text = textFromParts(msg);
      await log(`[autorun] response.len=${text.length}`);

      // Decide outcome
      if (text.includes("AUTO_BEAD_RESULT: SUCCESS")) {
        await bdUpdateStatus(
          bead.id,
          "closed",
          `Auto-run SUCCESS in session ${workerSessionId}`
        );
        await log(`[autorun] bead=${bead.id} -> closed`);
      } else if (text.includes("AUTO_BEAD_RESULT: NEEDS_HUMAN")) {
        await bdUpdateStatus(
          bead.id,
          "open",
          `Auto-run needs human. Session=${workerSessionId}. ${singleLine(text)}`
        );
        await log(`[autorun] bead=${bead.id} -> open (needs human)`);
      } else {
        // Unclassified => do not leave in_progress forever
        await bdUpdateStatus(
          bead.id,
          "open",
          `Auto-run unclassified result. Session=${workerSessionId}`
        );
        await log(`[autorun] bead=${bead.id} -> open (unclassified)`);
      }

      if (ENABLE_TOAST) {
        fireAndForget(
          client.tui.showToast?.({
            body: { message: `[AutoBeadRunner] finished: ${bead.id}`, variant: "info" },
          }) ?? Promise.resolve()
        );
      }
    } catch (e: any) {
      const msg = String(e?.message ?? e);
      await log(`[handleTrigger ERROR] ${msg}`);

      // Safety: if we had claimed, unstick the bead.
      if (bead?.id) {
        await bdUpdateStatus(
          bead.id,
          "open",
          `Auto-run error; waiting for human. ${workerSessionId ? `Session=${workerSessionId}. ` : ""}${singleLine(msg)}`
        );
      }
    } finally {
      inFlight = false;
    }
  }

  return {
    event: async ({ event }) => {
      try {
        if (event?.type !== "session.idle") return;

        const now = Date.now();
        if (now - lastIdleAt < COOLDOWN_MS) return;
        lastIdleAt = now;

        // Property names come from the event payload.
        const sessionId = (event as any)?.properties?.sessionID as string | undefined;

        if (scheduled) clearTimeout(scheduled);
        scheduled = setTimeout(() => {
          scheduled = null;
          fireAndForget(handleTrigger(sessionId));
        }, 0);
      } catch (e: any) {
        await log(`[event ERROR] ${String(e?.message ?? e)}`);
      }
    },
  };
};


@synergiator commented on GitHub (Dec 21, 2025): TWIMC - after some iterations, I have now something I can work with, though I think that on long term we should aim to have an opencode bot to do chores inside CICD just like the renovate bot. This opencode plugins allows to process open beads in background. ``` import type { Plugin } from "@opencode-ai/plugin"; import { appendFile } from "node:fs/promises"; type Bead = { id: string; title?: string; description?: string; priority?: number; created_at?: string; updated_at?: string; status?: string; issue_type?: string; }; const LOG_FILE = process.env.AUTO_BEAD_LOG ?? "/tmp/opencode-auto-bead-runner.log"; const ENABLED = (process.env.AUTO_BEAD_ENABLED ?? "1") === "1"; const IDLE_DELAY_MS = Number(process.env.AUTO_BEAD_IDLE_DELAY_MS ?? "2000"); const COOLDOWN_MS = Number(process.env.AUTO_BEAD_COOLDOWN_MS ?? "1000"); const ENABLE_TOAST = (process.env.AUTO_BEAD_TOAST ?? "0") === "1"; // Claim = move bead to in_progress before running it const CLAIM = (process.env.AUTO_BEAD_CLAIM ?? "1") === "1"; // Autorun = dispatch a prompt to OpenCode so it actually edits files & completes the bead const AUTORUN = (process.env.AUTO_BEAD_AUTORUN ?? "1") === "1"; // Hard stop if a worker prompt never returns (eg permission gate / waiting for human) const PROMPT_TIMEOUT_MS = Number(process.env.AUTO_BEAD_PROMPT_TIMEOUT_MS ?? "90000"); const AGENT = "autobead" // process.env.AUTO_BEAD_AGENT || undefined; const MODEL_PROVIDER = process.env.AUTO_BEAD_MODEL_PROVIDER || undefined; const MODEL_ID = process.env.AUTO_BEAD_MODEL_ID || undefined; let crashHooksInstalled = false; let logWriteFailedOnce = false; function ts() { return new Date().toISOString(); } async function log(line: string) { try { await appendFile(LOG_FILE, `${ts()} ${line}\n`); } catch (e: any) { if (!logWriteFailedOnce) { logWriteFailedOnce = true; console.error("[AutoBeadRunner] log write failed:", e?.message ?? e); } } } function fireAndForget(p: Promise<any>) { p.catch((e) => void log(`[async-error] ${String(e?.message ?? e)}`)); } function sleep(ms: number) { return new Promise<void>((r) => setTimeout(r, ms)); } // Known noisy rejection you observed: session_share file missing. // This is unrelated to beads; swallow it to keep logs clean. function isBenignSessionShareNotFound(reason: any): boolean { const msg = String(reason?.data?.message ?? reason?.message ?? reason ?? ""); return msg.includes("session_share") && msg.includes("Resource not found:"); } function ensureCrashHooksInstalled() { if (crashHooksInstalled) return; crashHooksInstalled = true; process.on("unhandledRejection", (reason: any) => { if (isBenignSessionShareNotFound(reason)) { void log( `[unhandledRejection][swallowed] ${String( reason?.data?.message ?? reason?.message ?? reason )}` ); return; } void log( `[unhandledRejection] ${String(reason?.data?.message ?? reason?.message ?? reason)}` ); }); process.on("uncaughtException", (err: any) => { void log(`[uncaughtException] ${String(err?.message ?? err)}`); }); } // The SDK may return { data, request, response } (responseStyle="fields"). // Unwrap defensively. (If already flat, return as-is.) function unwrap<T>(v: any): T { return (v && typeof v === "object" && "data" in v ? v.data : v) as T; } function parseBeads(jsonText: string): Bead[] { const v = JSON.parse(jsonText); return Array.isArray(v) ? (v as Bead[]) : []; } function beadSortKey(b: Bead) { const prio = Number.isFinite(b.priority) ? (b.priority as number) : 999999; const created = b.created_at ? Date.parse(b.created_at) : Number.POSITIVE_INFINITY; const createdKey = Number.isFinite(created) ? created : Number.POSITIVE_INFINITY; const id = b.id ?? ""; return { prio, createdKey, id }; } function pickNextBead(beads: Bead[]): Bead | null { const sorted = [...beads].sort((a, b) => { const ka = beadSortKey(a); const kb = beadSortKey(b); if (ka.prio !== kb.prio) return ka.prio - kb.prio; if (ka.createdKey !== kb.createdKey) return ka.createdKey - kb.createdKey; return ka.id.localeCompare(kb.id); }); return sorted.length ? sorted[0] : null; } function singleLine(s: string, max = 180) { const v = (s ?? "").replace(/\s+/g, " ").trim(); return v.length > max ? v.slice(0, max - 1) + "…" : v; } function textFromParts(msg: any): string { const parts = Array.isArray(msg?.parts) ? msg.parts : []; return parts .filter((p: any) => p && p.type === "text" && typeof p.text === "string") .map((p: any) => p.text) .join("\n") .trim(); } function buildPrompt(bead: Bead, cwd: string) { const title = bead.title ?? ""; const desc = bead.description ?? ""; return [ "You were triggered by an idle-time automation in OpenCode.", "", "Process exactly ONE bead (the one below) in an isolated worker session.", "", `Bead ID: ${bead.id}`, `Title: ${title}`, `Description: ${desc}`, "", "Constraints:", `- Work in repository worktree: ${cwd}`, "- Prefer minimal, safe edits.", "- Avoid using shell commands unless absolutely required (assume they may require human approval).", "- If you need consent/clarification: ask ONE concise question and STOP.", "", "Output contract (MANDATORY):", "End your final response with exactly ONE of:", " AUTO_BEAD_RESULT: SUCCESS", " AUTO_BEAD_RESULT: NEEDS_HUMAN", "", "If SUCCESS, include a short bullet list of changed files above the marker.", "If NEEDS_HUMAN, include your ONE question above the marker.", ].join("\n"); } async function withTimeout<T>( p: Promise<T>, ms: number, onTimeout: () => Promise<void> ): Promise<T> { let timer: ReturnType<typeof setTimeout> | null = null; const timeoutP = new Promise<T>((_, reject) => { timer = setTimeout(() => reject(new Error(`timeout after ${ms}ms`)), ms); }); try { return await Promise.race([p, timeoutP]); } catch (e: any) { const msg = String(e?.message ?? e); if (msg.includes("timeout after")) { await onTimeout(); } throw e; } finally { if (timer) clearTimeout(timer); } } export const AutoBeadRunner: Plugin = async ({ client, $, directory, worktree }) => { ensureCrashHooksInstalled(); const cwd = process.env.AUTO_BEAD_CWD ?? (worktree || directory); fireAndForget(log(`[loaded] cwd=${cwd} log=${LOG_FILE}`)); if (!ENABLED) { fireAndForget(log(`[disabled] AUTO_BEAD_ENABLED=0`)); return { event: async () => {} }; } if (ENABLE_TOAST) { fireAndForget( client.tui.showToast?.({ body: { message: "[AutoBeadRunner] loaded", variant: "info" } }) ?? Promise.resolve() ); } let lastIdleAt = 0; let inFlight = false; let scheduled: ReturnType<typeof setTimeout> | null = null; async function runBdReady(): Promise<Bead[]> { const r = await $`bd ready --json` .cwd(cwd) .env({ ...process.env, NO_COLOR: "1", TERM: "dumb" }) .quiet() .nothrow(); const exitCode = Number(r.exitCode ?? -1); const stdout = String(r.stdout ?? "").trim(); const stderr = String(r.stderr ?? "").trim(); await log(`[bd.ready] exit=${exitCode}`); if (stderr) await log(`[bd.ready] stderr=${stderr}`); if (!stdout) return []; if (exitCode !== 0) return []; try { return parseBeads(stdout); } catch (e: any) { await log(`[bd.ready] parse error: ${String(e?.message ?? e)}`); await log(`[bd.ready] raw=${stdout}`); return []; } } async function bdUpdateStatus(id: string, status: string, notes?: string) { const note = notes ? singleLine(notes) : ""; // Try with notes first; fall back if flag unsupported. if (note) { const u1 = await $`bd update ${id} --status ${status} --notes ${note} --json` .cwd(cwd) .env({ ...process.env, NO_COLOR: "1", TERM: "dumb" }) .quiet() .nothrow(); if (Number(u1.exitCode ?? -1) === 0) { await log(`[bd.update] id=${id} status=${status} notes=1 exit=0`); return; } await log( `[bd.update] id=${id} status=${status} notes=1 exit=${Number(u1.exitCode ?? -1)} (fallback without notes)` ); } const u2 = await $`bd update ${id} --status ${status} --json` .cwd(cwd) .env({ ...process.env, NO_COLOR: "1", TERM: "dumb" }) .quiet() .nothrow(); await log( `[bd.update] id=${id} status=${status} notes=0 exit=${Number(u2.exitCode ?? -1)} stdout=${singleLine( String(u2.stdout ?? "") )} stderr=${singleLine(String(u2.stderr ?? ""))}` ); } async function abortSession(sessionId: string) { const anyClient = client as any; try { if (typeof (client as any).session?.abort === "function") { await log(`[session.abort] via client.session.abort id=${sessionId}`); await (client as any).session.abort({ path: { id: sessionId } }); return; } if (typeof anyClient.postSessionByIdAbort === "function") { await log(`[session.abort] via postSessionByIdAbort id=${sessionId}`); await anyClient.postSessionByIdAbort({ path: { id: sessionId } }); return; } await log(`[session.abort] not available in client; cannot abort id=${sessionId}`); } catch (e: any) { await log(`[session.abort ERROR] id=${sessionId} ${String(e?.message ?? e)}`); } } async function createWorkerSession(parentSessionId: string | undefined, bead: Bead): Promise<string> { const title = `[AUTO-BEAD] ${bead.id} — ${bead.title ?? ""}`.trim(); const res = await client.session.create({ body: { title, // Keeping linkage optional; if you don’t want any coupling, remove parentID. ...(parentSessionId ? { parentID: parentSessionId } : {}), } as any, }); const s = unwrap<any>(res); const id = s?.id as string | undefined; if (!id) { throw new Error(`session.create returned no id (keys=${Object.keys(s ?? {}).join(",")})`); } await log(`[session.create] id=${id} title=${JSON.stringify(title)}`); return id; } async function promptInSession(sessionId: string, promptText: string) { const body: any = { parts: [{ type: "text", text: promptText }] }; if (AGENT) body.agent = AGENT; if (MODEL_PROVIDER && MODEL_ID) body.model = { providerID: MODEL_PROVIDER, modelID: MODEL_ID }; await log(`[dispatch] session.prompt session=${sessionId}`); const res = await client.session.prompt({ path: { id: sessionId }, body } as any); return unwrap<any>(res); } async function handleTrigger(sessionId: string | undefined) { if (inFlight) return; inFlight = true; let bead: Bead | null = null; let workerSessionId: string | null = null; try { await log(`[handleTrigger] idle; delay=${IDLE_DELAY_MS}ms sessionId=${sessionId ?? "?"}`); if (IDLE_DELAY_MS > 0) await sleep(IDLE_DELAY_MS); const beads = await runBdReady(); bead = pickNextBead(beads); if (!bead) { await log(`[handleTrigger] no ready beads`); if (ENABLE_TOAST) { fireAndForget( client.tui.showToast?.({ body: { message: "[AutoBeadRunner] no ready beads", variant: "info" } }) ?? Promise.resolve() ); } return; } await log(`[handleTrigger] nextReady=${bead.id}`); if (ENABLE_TOAST) { fireAndForget( client.tui.showToast?.({ body: { message: `[AutoBeadRunner] next bead: ${bead.id}`, variant: "success" }, }) ?? Promise.resolve() ); } if (CLAIM) { await bdUpdateStatus(bead.id, "in_progress"); } else { await log(`[claim] disabled (AUTO_BEAD_CLAIM=0)`); } if (!AUTORUN) { await log(`[autorun] disabled (AUTO_BEAD_AUTORUN=0)`); return; } // Create isolated worker session (NOT the idle TUI session). workerSessionId = await createWorkerSession(sessionId, bead); const promptText = buildPrompt(bead, cwd); await log(`[autorun] dispatching bead=${bead.id} targetSession=${workerSessionId}`); const msg = await withTimeout( promptInSession(workerSessionId, promptText), PROMPT_TIMEOUT_MS, async () => { await log(`[autorun][timeout] bead=${bead?.id ?? "?"} session=${workerSessionId}`); if (workerSessionId) await abortSession(workerSessionId); } ); const text = textFromParts(msg); await log(`[autorun] response.len=${text.length}`); // Decide outcome if (text.includes("AUTO_BEAD_RESULT: SUCCESS")) { await bdUpdateStatus( bead.id, "closed", `Auto-run SUCCESS in session ${workerSessionId}` ); await log(`[autorun] bead=${bead.id} -> closed`); } else if (text.includes("AUTO_BEAD_RESULT: NEEDS_HUMAN")) { await bdUpdateStatus( bead.id, "open", `Auto-run needs human. Session=${workerSessionId}. ${singleLine(text)}` ); await log(`[autorun] bead=${bead.id} -> open (needs human)`); } else { // Unclassified => do not leave in_progress forever await bdUpdateStatus( bead.id, "open", `Auto-run unclassified result. Session=${workerSessionId}` ); await log(`[autorun] bead=${bead.id} -> open (unclassified)`); } if (ENABLE_TOAST) { fireAndForget( client.tui.showToast?.({ body: { message: `[AutoBeadRunner] finished: ${bead.id}`, variant: "info" }, }) ?? Promise.resolve() ); } } catch (e: any) { const msg = String(e?.message ?? e); await log(`[handleTrigger ERROR] ${msg}`); // Safety: if we had claimed, unstick the bead. if (bead?.id) { await bdUpdateStatus( bead.id, "open", `Auto-run error; waiting for human. ${workerSessionId ? `Session=${workerSessionId}. ` : ""}${singleLine(msg)}` ); } } finally { inFlight = false; } } return { event: async ({ event }) => { try { if (event?.type !== "session.idle") return; const now = Date.now(); if (now - lastIdleAt < COOLDOWN_MS) return; lastIdleAt = now; // Property names come from the event payload. const sessionId = (event as any)?.properties?.sessionID as string | undefined; if (scheduled) clearTimeout(scheduled); scheduled = setTimeout(() => { scheduled = null; fireAndForget(handleTrigger(sessionId)); }, 0); } catch (e: any) { await log(`[event ERROR] ${String(e?.message ?? e)}`); } }, }; }; ```
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: anomalyco/opencode#3721