opencode-usage-coach 0.8.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +62 -33
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1682,7 +1682,7 @@ ${priorNotes}
1682
1682
  }
1683
1683
  }),
1684
1684
  generate_batch: tool({
1685
- description: "Run the GENERATOR model on MULTIPLE tasks. Quota-aware: GO = full parallel; THROTTLE = lighter model + concurrency capped at 2; STOP = refused. Use for INDEPENDENT tasks. Step-limited: each sub-session aborts after max_steps (default 30).",
1685
+ description: "Run the GENERATOR model on MULTIPLE tasks. Quota-aware: GO = full parallel; THROTTLE = lighter model + concurrency capped at 2; STOP = refused. Use for INDEPENDENT tasks. Step-limited: each sub-session aborts after max_steps (default 30). Resilient: failed tasks are retried sequentially (once); if retry also fails, re-run them individually with generate().",
1686
1686
  args: { tasks: tool.schema.array(tool.schema.object({ id: tool.schema.number(), prompt: tool.schema.string() })), max_steps: tool.schema.number().optional().describe("Maximum sub-session steps per task before timeout (default 30).") },
1687
1687
  async execute(args, ctx) {
1688
1688
  const cfg = readHarnessCfg(ctx.directory);
@@ -1698,54 +1698,83 @@ ${priorNotes}
1698
1698
  const limit = decision === "THROTTLE" ? 2 : args.tasks.length;
1699
1699
  const rules = readRules();
1700
1700
  const priorNotes = readImplNotes(5);
1701
- const results = [];
1702
- for (let i = 0; i < args.tasks.length; i += limit) {
1703
- const batch = args.tasks.slice(i, i + limit);
1704
- const out = await Promise.all(batch.map(async (t) => {
1705
- let prefix = rules ? `Lessons learned from previous failures (apply where relevant):
1701
+ const maxSteps = args.max_steps ?? DEFAULT_MAX_STEPS;
1702
+ const runOne = async (t) => {
1703
+ let prefix = rules ? `Lessons learned from previous failures (apply where relevant):
1706
1704
  ${rules}
1707
1705
 
1708
1706
  ---
1709
1707
 
1710
1708
  ` : "";
1711
- if (priorNotes) prefix = `Notes from previous runs (context for this task):
1709
+ if (priorNotes) prefix = `Notes from previous runs (context for this task):
1712
1710
  ${priorNotes}
1713
1711
 
1714
1712
  ---
1715
1713
 
1716
1714
  ` + prefix;
1717
- prefix += IMPL_NOTE_INSTRUCTION;
1718
- const r = await runModel(
1719
- input.client,
1720
- model,
1721
- prefix + t.prompt,
1722
- ctx.directory,
1723
- { sessionID: ctx.sessionID, taskId: t.id },
1724
- args.max_steps ?? DEFAULT_MAX_STEPS
1725
- );
1726
- const isTimeoutOrError = r.startsWith("Task appears too large") || r.startsWith("ERROR:");
1727
- if (!isTimeoutOrError) {
1728
- try {
1729
- const { notes } = extractImplNotes(r);
1730
- if (notes) {
1731
- appendImplNotes(notes, t.prompt);
1732
- const kw = extractKeywords(t.prompt);
1733
- if (kw.length) {
1734
- const noteNodeId = saveInvestigationResult(kw, notes, "impl-note", 0.5);
1735
- if (noteNodeId) linkImplNoteToDomain(noteNodeId, notes);
1736
- }
1715
+ prefix += IMPL_NOTE_INSTRUCTION;
1716
+ const r = await runModel(
1717
+ input.client,
1718
+ model,
1719
+ prefix + t.prompt,
1720
+ ctx.directory,
1721
+ { sessionID: ctx.sessionID, taskId: t.id },
1722
+ maxSteps
1723
+ );
1724
+ const isTimeoutOrError = r.startsWith("Task appears too large") || r.startsWith("ERROR:");
1725
+ if (!isTimeoutOrError) {
1726
+ try {
1727
+ const { notes } = extractImplNotes(r);
1728
+ if (notes) {
1729
+ appendImplNotes(notes, t.prompt);
1730
+ const kw = extractKeywords(t.prompt);
1731
+ if (kw.length) {
1732
+ const noteNodeId = saveInvestigationResult(kw, notes, "impl-note", 0.5);
1733
+ if (noteNodeId) linkImplNoteToDomain(noteNodeId, notes);
1737
1734
  }
1738
- } catch (e) {
1739
- log(`generate_batch impl-notes extract err: ${String(e)}`);
1740
1735
  }
1736
+ } catch (e) {
1737
+ log(`generate_batch impl-notes extract err: ${String(e)}`);
1738
+ }
1739
+ }
1740
+ return { id: t.id, result: r };
1741
+ };
1742
+ const results = [];
1743
+ const failed = [];
1744
+ for (let i = 0; i < args.tasks.length; i += limit) {
1745
+ const batch = args.tasks.slice(i, i + limit);
1746
+ const settled = await Promise.allSettled(batch.map((t) => runOne(t)));
1747
+ for (let j = 0; j < settled.length; j++) {
1748
+ const s = settled[j];
1749
+ const t = batch[j];
1750
+ if (s.status === "fulfilled") {
1751
+ results.push(`[task ${s.value.id}] ${s.value.result}`);
1752
+ } else {
1753
+ const err = String(s.reason ?? "unknown rejection");
1754
+ log(`generate_batch task ${t.id} REJECTED: ${err}`);
1755
+ failed.push({ id: t.id, prompt: t.prompt, error: err });
1741
1756
  }
1742
- return `[task ${t.id}] ${r}`;
1743
- }));
1744
- results.push(...out);
1757
+ }
1758
+ }
1759
+ if (failed.length > 0) {
1760
+ log(`generate_batch: retrying ${failed.length} failed task(s) sequentially`);
1761
+ for (const f of failed) {
1762
+ try {
1763
+ const r = await runOne(f);
1764
+ results.push(`[task ${r.id}] (retry) ${r.result}`);
1765
+ } catch (e2) {
1766
+ const err2 = String(e2 ?? "unknown rejection on retry");
1767
+ log(`generate_batch task ${f.id} retry ALSO FAILED: ${err2}`);
1768
+ results.push(`[task ${f.id}] ERROR: task failed on both batch and retry. Error: ${err2}`);
1769
+ }
1770
+ }
1745
1771
  }
1772
+ const failedCount = results.filter((r) => r.includes("ERROR:")).length;
1746
1773
  const note = throttle ? `
1747
1774
  [usage-coach] quota THROTTLE \u2014 lighter model ${cfg.lighterModel}, concurrency capped at ${limit}` : "";
1748
- return results.join("\n\n") + note;
1775
+ const failNext = failedCount > 0 ? `
1776
+ [usage-coach NEXT] ${failedCount} task(s) failed even after retry. Re-run each failed task individually with generate() (sequential), NOT generate_batch. This isolates failures and avoids batch-level abort.` : "";
1777
+ return results.join("\n\n") + note + failNext;
1749
1778
  }
1750
1779
  }),
1751
1780
  grade: tool({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-usage-coach",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "opencode closed-loop usage coach — quota SENSE -> coaching DECIDE -> loop ACT + TUI integration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",