@sorenllm/opencode-forge 0.3.3 → 0.3.4
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 +18 -4
- package/dist/index.js +51 -13
- package/package.json +61 -61
package/README.md
CHANGED
|
@@ -130,7 +130,7 @@ What this plugin touches, exhaustively:
|
|
|
130
130
|
| `<tmp>/opencode-forge/jobs/ledger.jsonl` | job registry ledger (bounded: 1 MB reset, 200 entries) | runtime debris — delete freely, also after uninstall |
|
|
131
131
|
| `<tmp>/opencode-forge/jobs/registry.json` | persistent survivor registry (bounded: 100 entries) | runtime debris — after uninstall, kill any still-running `survive` jobs yourself first |
|
|
132
132
|
| `<tmp>/opencode-forge/watchdog/log.jsonl` | watchdog interventions ledger (bounded: 200 entries, oldest rotated) | runtime debris — delete freely, also after uninstall |
|
|
133
|
-
| `<tmp>/opencode-forge/dispatch/ledger.jsonl` | dispatch ledger: resolved identity/depth, outcomes, tokens/cost, timeouts, lost-on-exit, crew summaries (bounded, capped rotation) | runtime debris — delete freely, also after uninstall |
|
|
133
|
+
| `<tmp>/opencode-forge/dispatch/ledger.jsonl` | dispatch ledger: resolved identity/depth, outcomes, tokens/cost, timeouts, transport interruptions, lost-on-exit, crew summaries (bounded, capped rotation) | runtime debris — delete freely, also after uninstall |
|
|
134
134
|
| `<project>/.opencode/forge.json`, `~/.config/opencode/forge.json` | your dispatch agent definitions (JSONC). **User data — the plugin only reads it, never writes or migrates it** | yours — version the project one, keep the global one out of sync tools if it holds machine-specific models |
|
|
135
135
|
| `~/.cache/opencode/packages/...` | installed package copy | written by the `opencode plugin` installer, not the plugin |
|
|
136
136
|
| `~/.config/opencode/opencode.json` | `plugin` array entry | written by the installer |
|
|
@@ -420,6 +420,18 @@ stops polling, suppresses the brief, and ledgers `killed`. On host exit,
|
|
|
420
420
|
in-flight dispatches are ledgered `lost-on-exit` — sessions are host memory
|
|
421
421
|
objects, there are no orphan processes and no survive semantics.
|
|
422
422
|
|
|
423
|
+
**Transport-interruption recovery**: the child's prompt is delivered by a
|
|
424
|
+
turn-synchronous HTTP POST that stays open for the whole first turn. If that
|
|
425
|
+
POST dies at the transport layer (fetch abort / network failure — anything
|
|
426
|
+
that is not an HTTP error response), the dispatch does **not** fail: the
|
|
427
|
+
engine ledgers `transport-interrupted` (with the child sessionID) and falls
|
|
428
|
+
back to completion polling under the same deadline — the child session is a
|
|
429
|
+
host-side object and usually keeps running, so its result is still collected
|
|
430
|
+
and delivered normally. The message is never re-POSTed (the prompt may
|
|
431
|
+
already be delivering); a message that never arrived polls as waiting and
|
|
432
|
+
ends in the honest deadline `timeout`. Terminal reports and terminal ledger
|
|
433
|
+
rows carry the child sessionID, dispatchId, parentSessionID, and durationMs.
|
|
434
|
+
|
|
423
435
|
### Crew workflow (`/crew`)
|
|
424
436
|
|
|
425
437
|
`/crew <objective>` registers a crew: decompose the objective into
|
|
@@ -447,9 +459,11 @@ ends it honestly and the ledger keeps the history.
|
|
|
447
459
|
live TUI sessions; prefer sync under `opencode run`.
|
|
448
460
|
- **Host tool ceiling (~262s observed on 1.18.32)**: the host may kill a
|
|
449
461
|
sync tool call that exceeds roughly 262 seconds even though the plugin's
|
|
450
|
-
own deadline is higher.
|
|
451
|
-
|
|
452
|
-
|
|
462
|
+
own deadline is higher. A same-family transport abort (~281s observed,
|
|
463
|
+
2026-09-27 incident) can also kill the background dispatch's turn POST.
|
|
464
|
+
Transport-interruption recovery (above) absorbs both: the dispatch falls
|
|
465
|
+
back to polling and still delivers the child's result; keep `timeoutMs`
|
|
466
|
+
modest anyway for sync-heavy workflows.
|
|
453
467
|
- **Child permission asks**: a worker that hits a permission ask surfaces the
|
|
454
468
|
ask on the CHILD's session in the TUI. An unanswered ask stalls the child
|
|
455
469
|
until the dispatch deadline — scope worker prompts to avoid permission
|
package/dist/index.js
CHANGED
|
@@ -15331,23 +15331,41 @@ Vocabulary: ${r.error.vocabulary.join(", ") || "(none)"}`, r.error.code);
|
|
|
15331
15331
|
parts: [{ type: "text", text: composeWorkerPrompt({ prompt: req.prompt, agent: req.agent, shape: r.shape, ...r.role ? { role: r.role } : {} }) }]
|
|
15332
15332
|
})
|
|
15333
15333
|
});
|
|
15334
|
-
|
|
15335
|
-
|
|
15336
|
-
|
|
15334
|
+
turnPost.catch(() => {});
|
|
15335
|
+
try {
|
|
15336
|
+
await Promise.race([turnPost, deadlinePromise]);
|
|
15337
|
+
if (deadlineHit && deps.now() - t0 >= timeoutMs) {
|
|
15338
|
+
const elapsed = deps.now() - t0;
|
|
15339
|
+
deps.sink({
|
|
15340
|
+
ts: new Date().toISOString(),
|
|
15341
|
+
event: "timeout",
|
|
15342
|
+
tier: req.agent,
|
|
15343
|
+
agent: req.agent,
|
|
15344
|
+
identity: r.model,
|
|
15345
|
+
depth: r.depth,
|
|
15346
|
+
sessionID: childID,
|
|
15347
|
+
durationMs: elapsed,
|
|
15348
|
+
...dispatchId !== undefined ? { dispatchId, parentSessionID: parentSessionID ?? "" } : {}
|
|
15349
|
+
});
|
|
15350
|
+
throw new DispatchError(`forge_dispatch timed out after ${elapsed}ms (deadline ${timeoutMs}ms). Partial state: child session ${childID} is left for the host to reclaim; its transcript remains queryable via the host API.`, "timeout");
|
|
15351
|
+
}
|
|
15352
|
+
await turnPost;
|
|
15353
|
+
} catch (e) {
|
|
15354
|
+
if (e instanceof DispatchError)
|
|
15355
|
+
throw e;
|
|
15337
15356
|
deps.sink({
|
|
15338
15357
|
ts: new Date().toISOString(),
|
|
15339
|
-
event: "
|
|
15358
|
+
event: "transport-interrupted",
|
|
15340
15359
|
tier: req.agent,
|
|
15341
15360
|
agent: req.agent,
|
|
15342
15361
|
identity: r.model,
|
|
15343
15362
|
depth: r.depth,
|
|
15344
15363
|
sessionID: childID,
|
|
15345
|
-
durationMs:
|
|
15364
|
+
durationMs: deps.now() - t0,
|
|
15365
|
+
note: e instanceof Error ? e.message : String(e),
|
|
15346
15366
|
...dispatchId !== undefined ? { dispatchId, parentSessionID: parentSessionID ?? "" } : {}
|
|
15347
15367
|
});
|
|
15348
|
-
throw new DispatchError(`forge_dispatch timed out after ${elapsed}ms (deadline ${timeoutMs}ms). Partial state: child session ${childID} is left for the host to reclaim; its transcript remains queryable via the host API.`, "timeout");
|
|
15349
15368
|
}
|
|
15350
|
-
await turnPost;
|
|
15351
15369
|
let poll = null;
|
|
15352
15370
|
let messages = [];
|
|
15353
15371
|
for (;; ) {
|
|
@@ -15424,24 +15442,44 @@ Vocabulary: ${r.error.vocabulary.join(", ") || "(none)"}`, r.error.code);
|
|
|
15424
15442
|
return result;
|
|
15425
15443
|
}
|
|
15426
15444
|
function bgTerminal(reg, dispatchId, identity, req, t0, e) {
|
|
15445
|
+
let entrySessionID = "";
|
|
15446
|
+
let parentSessionID = "";
|
|
15447
|
+
try {
|
|
15448
|
+
const entry = reg.get(dispatchId);
|
|
15449
|
+
entrySessionID = entry.sessionID;
|
|
15450
|
+
parentSessionID = entry.parentSessionID;
|
|
15451
|
+
} catch {}
|
|
15452
|
+
const durationMs = deps.now() - t0;
|
|
15427
15453
|
const report = {
|
|
15428
15454
|
agent: req.agent,
|
|
15429
15455
|
requested: { agent: req.agent, depth: req.depth },
|
|
15430
15456
|
actual: { model: identity, depth: null },
|
|
15431
|
-
sessionID:
|
|
15457
|
+
sessionID: entrySessionID,
|
|
15432
15458
|
outcome: "error",
|
|
15433
|
-
durationMs
|
|
15459
|
+
durationMs,
|
|
15434
15460
|
error: e instanceof Error ? e.message : String(e)
|
|
15435
15461
|
};
|
|
15436
15462
|
if (e instanceof DispatchError && e.code === "timeout") {
|
|
15437
15463
|
report.outcome = "timeout";
|
|
15438
15464
|
reg.markTimeout(dispatchId, report);
|
|
15439
|
-
deps.onTerminal?.(
|
|
15465
|
+
deps.onTerminal?.(parentSessionID);
|
|
15440
15466
|
return;
|
|
15441
15467
|
}
|
|
15442
|
-
deps.sink({
|
|
15468
|
+
deps.sink({
|
|
15469
|
+
ts: new Date().toISOString(),
|
|
15470
|
+
event: "error",
|
|
15471
|
+
tier: req.agent,
|
|
15472
|
+
agent: req.agent,
|
|
15473
|
+
identity,
|
|
15474
|
+
outcome: "error",
|
|
15475
|
+
sessionID: entrySessionID,
|
|
15476
|
+
durationMs,
|
|
15477
|
+
note: report.error,
|
|
15478
|
+
dispatchId,
|
|
15479
|
+
parentSessionID
|
|
15480
|
+
});
|
|
15443
15481
|
reg.markError(dispatchId, report);
|
|
15444
|
-
deps.onTerminal?.(
|
|
15482
|
+
deps.onTerminal?.(parentSessionID);
|
|
15445
15483
|
}
|
|
15446
15484
|
async function dispatch(req, parentSessionID, opts) {
|
|
15447
15485
|
const t0 = deps.now();
|
|
@@ -15736,7 +15774,7 @@ function validateCrewReport(report, ledgerRows, crew) {
|
|
|
15736
15774
|
continue;
|
|
15737
15775
|
if (row.ts < crew.startedAt)
|
|
15738
15776
|
continue;
|
|
15739
|
-
if (row.event === "validation" || row.event === "resolve-error" || row.event === "refused-cap" || row.event === "models-dev-degraded" || row.event === "retry-excluded" || row.event === "lost-on-exit" || row.event === "kill-late-completion")
|
|
15777
|
+
if (row.event === "validation" || row.event === "resolve-error" || row.event === "refused-cap" || row.event === "models-dev-degraded" || row.event === "retry-excluded" || row.event === "lost-on-exit" || row.event === "kill-late-completion" || row.event === "transport-interrupted")
|
|
15740
15778
|
continue;
|
|
15741
15779
|
if (CREW_DISPATCH_EVENTS.has(row.event) && row.dispatchId)
|
|
15742
15780
|
rowById.set(row.dispatchId, row);
|
package/package.json
CHANGED
|
@@ -1,61 +1,61 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@sorenllm/opencode-forge",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"description": "Single general-purpose forge agent for opencode with two orthogonal harnesses and two hang backstops: a plan harness (file-backed plans in .opencode/plan/, approve/close confirmation gates, tick discipline, hard write-ban while planning), a goal harness (autonomous, host-verified objectives in .opencode/goal/), a job supervisor (forge_shell with exit-bound completion — immune to stdio-EOF hangs — idle/success early return, background jobs with poll/log/kill/handoff, exit wakes on session idle), and a builtin-shell hang watchdog (marked timing across all sessions, graded tree-kill with dry-run/off modes, auditable ledger).",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "./dist/index.js",
|
|
7
|
-
"exports": {
|
|
8
|
-
".": {
|
|
9
|
-
"import": "./dist/index.js"
|
|
10
|
-
},
|
|
11
|
-
"./server": {
|
|
12
|
-
"import": "./dist/index.js"
|
|
13
|
-
}
|
|
14
|
-
},
|
|
15
|
-
"files": [
|
|
16
|
-
"dist",
|
|
17
|
-
"README.md"
|
|
18
|
-
],
|
|
19
|
-
"scripts": {
|
|
20
|
-
"bundle": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && bun build ./plugin.ts --outfile ./dist/index.js --target node --format esm",
|
|
21
|
-
"test": "node --test tests/*.test.mjs",
|
|
22
|
-
"typecheck": "tsc --noEmit"
|
|
23
|
-
},
|
|
24
|
-
"keywords": [
|
|
25
|
-
"opencode",
|
|
26
|
-
"opencode-plugin",
|
|
27
|
-
"plan",
|
|
28
|
-
"planning",
|
|
29
|
-
"workflow",
|
|
30
|
-
"single-agent",
|
|
31
|
-
"goal",
|
|
32
|
-
"background-jobs",
|
|
33
|
-
"shell",
|
|
34
|
-
"watchdog"
|
|
35
|
-
],
|
|
36
|
-
"license": "MIT",
|
|
37
|
-
"author": "chengsongren",
|
|
38
|
-
"homepage": "https://github.com/ChengZiiii/opencode-forge#readme",
|
|
39
|
-
"repository": {
|
|
40
|
-
"type": "git",
|
|
41
|
-
"url": "git+https://github.com/ChengZiiii/opencode-forge.git"
|
|
42
|
-
},
|
|
43
|
-
"bugs": {
|
|
44
|
-
"url": "https://github.com/ChengZiiii/opencode-forge/issues"
|
|
45
|
-
},
|
|
46
|
-
"peerDependencies": {
|
|
47
|
-
"@opencode-ai/plugin": "^1.18.0"
|
|
48
|
-
},
|
|
49
|
-
"devDependencies": {
|
|
50
|
-
"@opencode-ai/plugin": "^1.18.0",
|
|
51
|
-
"@types/node": "^22.13.9",
|
|
52
|
-
"typescript": "^5.8.2"
|
|
53
|
-
},
|
|
54
|
-
"engines": {
|
|
55
|
-
"opencode": "^1.18.0"
|
|
56
|
-
},
|
|
57
|
-
"publishConfig": {
|
|
58
|
-
"access": "public",
|
|
59
|
-
"registry": "https://registry.npmjs.org/"
|
|
60
|
-
}
|
|
61
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@sorenllm/opencode-forge",
|
|
3
|
+
"version": "0.3.4",
|
|
4
|
+
"description": "Single general-purpose forge agent for opencode with two orthogonal harnesses and two hang backstops: a plan harness (file-backed plans in .opencode/plan/, approve/close confirmation gates, tick discipline, hard write-ban while planning), a goal harness (autonomous, host-verified objectives in .opencode/goal/), a job supervisor (forge_shell with exit-bound completion — immune to stdio-EOF hangs — idle/success early return, background jobs with poll/log/kill/handoff, exit wakes on session idle), and a builtin-shell hang watchdog (marked timing across all sessions, graded tree-kill with dry-run/off modes, auditable ledger).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"./server": {
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"bundle": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && bun build ./plugin.ts --outfile ./dist/index.js --target node --format esm",
|
|
21
|
+
"test": "node --test tests/*.test.mjs",
|
|
22
|
+
"typecheck": "tsc --noEmit"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"opencode",
|
|
26
|
+
"opencode-plugin",
|
|
27
|
+
"plan",
|
|
28
|
+
"planning",
|
|
29
|
+
"workflow",
|
|
30
|
+
"single-agent",
|
|
31
|
+
"goal",
|
|
32
|
+
"background-jobs",
|
|
33
|
+
"shell",
|
|
34
|
+
"watchdog"
|
|
35
|
+
],
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"author": "chengsongren",
|
|
38
|
+
"homepage": "https://github.com/ChengZiiii/opencode-forge#readme",
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "git+https://github.com/ChengZiiii/opencode-forge.git"
|
|
42
|
+
},
|
|
43
|
+
"bugs": {
|
|
44
|
+
"url": "https://github.com/ChengZiiii/opencode-forge/issues"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"@opencode-ai/plugin": "^1.18.0"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@opencode-ai/plugin": "^1.18.0",
|
|
51
|
+
"@types/node": "^22.13.9",
|
|
52
|
+
"typescript": "^5.8.2"
|
|
53
|
+
},
|
|
54
|
+
"engines": {
|
|
55
|
+
"opencode": "^1.18.0"
|
|
56
|
+
},
|
|
57
|
+
"publishConfig": {
|
|
58
|
+
"access": "public",
|
|
59
|
+
"registry": "https://registry.npmjs.org/"
|
|
60
|
+
}
|
|
61
|
+
}
|