@sorenllm/opencode-forge 0.8.0 → 0.9.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.
package/README.md CHANGED
@@ -265,7 +265,7 @@ the stdio-EOF bug):
265
265
  | --- | --- | --- | --- |
266
266
  | process exit | — | final; exit code + output tail returned | **terminal evidence** — output delivered in full; the job self-clears from the registry (no wake, no clear needed) |
267
267
  | `success_pattern` regex matches new output | opt-in | completes as success; process kept alive by default (server semantics), `keep_alive: false` kills its tree | kept alive → later exit still wakes (the server dying is real news); killed inline → self-clears |
268
- | `idle_ms` with no new output | default 60000 | early return `still-running` + `jobId`; process stays alive | later exit queues a wake; entry stays listed until cleared |
268
+ | `idle_ms` with no new output | default 60000 | early return `still-running` + `jobId`; process stays alive | later exit queues a wake — unless a poll has already read the terminal state (a terminal poll consumes the entry); stays listed until then |
269
269
  | `max_wait_ms` hard cap | default 120000, max 600000 | early return `still-running`; never kills | same as idle |
270
270
 
271
271
  `run_in_background: true` skips all waiting and returns
@@ -292,7 +292,11 @@ idle (exactly once; `notify: false` opts out per job). A foreground call
292
292
  resolved on the exit event is terminal evidence: the caller already holds
293
293
  the complete output, so no wake fires and the registry entry self-clears —
294
294
  a stale `poll`/`kill`/`clear` on such an id answers "already consumed"
295
- instead of "unknown job".
295
+ instead of "unknown job". The same consumption applies to a poll that
296
+ observes a finished job: the poll itself delivered the exit status and the
297
+ drained output, so the queued wake is dropped and the entry self-clears —
298
+ no duplicate notification, no manual clear. Wakes fire only for
299
+ completions that were never read.
296
300
 
297
301
  **Ownership.** Jobs belong to the session that created them. Session
298
302
  deleted → its live session-scoped jobs are killed and the event is recorded
package/dist/index.js CHANGED
@@ -13419,6 +13419,9 @@ function createJobManager(opts = {}) {
13419
13419
  if (job.notify && job.wakeState === "none" && !job.terminalEvidence) {
13420
13420
  job.wakeState = "queued";
13421
13421
  job.wakeQueuedAt = now();
13422
+ try {
13423
+ opts.onWakeQueued?.(job);
13424
+ } catch {}
13422
13425
  }
13423
13426
  if (job.terminalEvidence) {
13424
13427
  jobs.delete(job.id);
@@ -13449,8 +13452,11 @@ function createJobManager(opts = {}) {
13449
13452
  markTerminal(job, "killed", null);
13450
13453
  }
13451
13454
  function poll(job) {
13452
- if (isTerminal2(job))
13455
+ if (isTerminal2(job)) {
13453
13456
  job.readAfterEnd = true;
13457
+ jobs.delete(job.id);
13458
+ rememberConsumed(job.id);
13459
+ }
13454
13460
  const cursor = job.outLen;
13455
13461
  const start = job.pollCursor;
13456
13462
  const newOutput = sliceFromCursor(job, start);
@@ -15600,8 +15606,15 @@ function jobLedgerSink(entry) {
15600
15606
  `);
15601
15607
  } catch {}
15602
15608
  }
15603
- var jobManager = createJobManager({ sink: jobLedgerSink });
15609
+ var jobManager = createJobManager({
15610
+ sink: jobLedgerSink,
15611
+ onWakeQueued: (job) => {
15612
+ if (idleJobSessions.has(job.ownerSession))
15613
+ deliverJobWakes(job.ownerSession);
15614
+ }
15615
+ });
15604
15616
  var jobClient = null;
15617
+ var idleJobSessions = new Set;
15605
15618
  function jobWakeText(job) {
15606
15619
  return [
15607
15620
  `[forge:job-complete] Background job ${job.id} finished (${job.state}${job.exitCode !== null ? `, exit ${job.exitCode}` : ""}).`,
@@ -16242,7 +16255,7 @@ ${r.outputTail || "(none yet)"}`,
16242
16255
  });
16243
16256
  var FORGE_JOBS_ACTIONS = ["list", "poll", "log", "kill", "clear", "handoff"];
16244
16257
  var forgeJobsTool = tool({
16245
- description: "Manage forge_shell jobs. Actions: list (all jobs, newest first); poll {jobId, waitMs<=30000} — bounded wait for NEW output or exit, drains it; log {jobId, offset?, limit?} — line paging over the on-disk log (omitted offset = tail window, default 200 lines); kill {jobId} — terminate the job's whole process tree; clear {jobId} — drop a finished job from the registry (only early-returned/background jobs need this — synchronously consumed jobs self-clear at completion); handoff {jobId} — rebind ownership to the root session so the job survives this (sub)session's end. A delegated agent MUST poll its jobs before yielding its conclusion.",
16258
+ description: "Manage forge_shell jobs. Actions: list (all jobs, newest first); poll {jobId, waitMs<=30000} — bounded wait for NEW output or exit, drains it (polling a finished job consumes it: the entry self-clears and no wake fires); log {jobId, offset?, limit?} — line paging over the on-disk log (omitted offset = tail window, default 200 lines); kill {jobId} — terminate the job's whole process tree; clear {jobId} — drop a finished job from the registry (only un-polled early-returned/background jobs need this — synchronously consumed jobs self-clear at completion, and a terminal poll consumes the entry); handoff {jobId} — rebind ownership to the root session so the job survives this (sub)session's end. A delegated agent MUST poll its jobs before yielding its conclusion.",
16246
16259
  args: {
16247
16260
  action: tool.schema.string().describe(`One of: ${FORGE_JOBS_ACTIONS.join(", ")}`),
16248
16261
  jobId: tool.schema.string().optional().describe("Job id from forge_shell (required for every action except list)"),
@@ -16943,8 +16956,11 @@ var server = async (input, options) => {
16943
16956
  if (forgeDisabled)
16944
16957
  return;
16945
16958
  const raw = input2;
16946
- if (typeof raw.sessionID === "string" && raw.sessionID && typeof raw.agent === "string" && raw.agent) {
16947
- sessionAgents.set(raw.sessionID, raw.agent);
16959
+ if (typeof raw.sessionID === "string" && raw.sessionID) {
16960
+ idleJobSessions.delete(raw.sessionID);
16961
+ if (typeof raw.agent === "string" && raw.agent) {
16962
+ sessionAgents.set(raw.sessionID, raw.agent);
16963
+ }
16948
16964
  }
16949
16965
  },
16950
16966
  "chat.params": async (input2, output) => {
@@ -17030,6 +17046,7 @@ var server = async (input, options) => {
17030
17046
  const sessionID = event.properties.sessionID;
17031
17047
  if (typeof sessionID === "string" && sessionID) {
17032
17048
  goalProbe(`idle event session=${sessionID}`);
17049
+ idleJobSessions.add(sessionID);
17033
17050
  scheduleIdleContinuation(client, sessionID);
17034
17051
  deliverJobWakes(sessionID);
17035
17052
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sorenllm/opencode-forge",
3
- "version": "0.8.0",
3
+ "version": "0.9.1",
4
4
  "description": "Single general-purpose forge agent for opencode: static worker subagents defined in forge.json (pinned model + thoughtLevel pair, or an Auto worker inheriting the parent model; dispatched through the native task tool — TUI-visible), a plan harness (file-backed plans with approve/close confirmation gates and a hard draft write-ban), a goal harness (autonomous, host-verified objectives), a /crew orchestration command (declared-plan waves with an evidence-checked close gate), a job supervisor (forge_shell with exit-bound completion, idle/success early return, background jobs with poll/log/kill/handoff), and a builtin-shell hang watchdog.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",