@bybrawe/opencode-loop 0.5.28 → 0.5.29

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 (56) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +1 -1
  3. package/package.json +4 -4
  4. package/scripts/command-lifecycle-test.mjs +47 -0
  5. package/scripts/command-router-test.mjs +175 -0
  6. package/scripts/compaction-runtime-test.mjs +128 -0
  7. package/scripts/comprehensive-test.mjs +11 -3
  8. package/scripts/executor-runtime-test.mjs +310 -0
  9. package/scripts/fixtures/opencode2-real-adapter-probe.js +70 -0
  10. package/scripts/goal-command-handlers-test.mjs +236 -0
  11. package/scripts/goal-policy-test.mjs +143 -0
  12. package/scripts/goal-runtime-test.mjs +203 -0
  13. package/scripts/goal-steering-test.mjs +153 -0
  14. package/scripts/host-goal-steering-canary.mjs +443 -0
  15. package/scripts/host-loop-canary.mjs +54 -12
  16. package/scripts/job-workspace-test.mjs +164 -0
  17. package/scripts/loop-command-handlers-test.mjs +279 -0
  18. package/scripts/loop-registration-test.mjs +220 -0
  19. package/scripts/opencode2-loop-canary.mjs +433 -0
  20. package/scripts/scheduler-runtime-test.mjs +147 -0
  21. package/scripts/session-activity-test.mjs +95 -0
  22. package/scripts/session-status-test.mjs +220 -0
  23. package/scripts/v2-command-adapter-test.mjs +113 -0
  24. package/scripts/v2-command-runtime-test.mjs +98 -0
  25. package/scripts/v2-host-contract-test.mjs +27 -0
  26. package/scripts/v2-plugin-sentinel-test.mjs +57 -8
  27. package/scripts/v2-prompt-interval-test.mjs +171 -0
  28. package/scripts/v2-prompt-runtime-test.mjs +267 -0
  29. package/scripts/v2-runtime-adapter-test.mjs +326 -0
  30. package/src/index.js +2751 -1594
  31. package/src/source/legacy-v1.js +168 -1860
  32. package/src/source/opencode/command-router.js +103 -0
  33. package/src/source/opencode/commands.js +83 -0
  34. package/src/source/opencode/goal-commands.js +107 -0
  35. package/src/source/opencode/loop-commands.js +195 -0
  36. package/src/source/opencode/loop-registration.js +100 -0
  37. package/src/source/opencode/messages.js +55 -0
  38. package/src/source/opencode2/capabilities.js +3 -1
  39. package/src/source/opencode2/commands.js +60 -0
  40. package/src/source/opencode2/event-bridge.js +19 -1
  41. package/src/source/opencode2/events.js +64 -0
  42. package/src/source/opencode2/experimental.js +32 -1
  43. package/src/source/opencode2/host-contract.js +26 -0
  44. package/src/source/opencode2/prompt-runtime.js +381 -0
  45. package/src/source/opencode2/runtime-adapter.js +56 -0
  46. package/src/source/opencode2/status.js +40 -0
  47. package/src/source/runtime/compaction.js +122 -0
  48. package/src/source/runtime/executor.js +558 -0
  49. package/src/source/runtime/goal-policy.js +77 -0
  50. package/src/source/runtime/goal-runtime.js +231 -0
  51. package/src/source/runtime/goal-steering.js +198 -0
  52. package/src/source/runtime/job-workspace.js +148 -0
  53. package/src/source/runtime/scheduler.js +212 -0
  54. package/src/source/runtime/session-activity.js +112 -0
  55. package/src/source/runtime/session-status.js +197 -0
  56. package/scripts/host-bridge-parity-test.mjs +0 -22
@@ -0,0 +1,56 @@
1
+ import { inspectOpenCode2Context } from "./capabilities.js"
2
+ import { createOpenCode2HostContract } from "./host-contract.js"
3
+
4
+ function promptRequest(request) {
5
+ const value = {
6
+ sessionID: request.sessionID,
7
+ }
8
+ if (request.noReply === true) {
9
+ value.noReply = true
10
+ value.parts = [{ type: "text", text: request.text }]
11
+ } else {
12
+ value.text = request.text
13
+ }
14
+ if (request.agent !== undefined) value.agent = request.agent
15
+ if (request.model !== undefined) value.model = request.model
16
+ return value
17
+ }
18
+
19
+ function commandRequest(request) {
20
+ const value = {
21
+ sessionID: request.sessionID,
22
+ command: request.command,
23
+ }
24
+ if (request.arguments !== undefined) value.arguments = request.arguments
25
+ if (request.agent !== undefined) value.agent = request.agent
26
+ if (request.model !== undefined) value.model = request.model
27
+ if (request.delivery !== undefined) value.delivery = request.delivery
28
+ if (request.resume !== undefined) value.resume = request.resume
29
+ return value
30
+ }
31
+
32
+ export function createOpenCode2RuntimeAdapter(ctx, options = {}) {
33
+ const capabilities = inspectOpenCode2Context(ctx)
34
+ if (!capabilities.eventSubscribe) throw new Error("OpenCode 2 event.subscribe capability is unavailable")
35
+ if (!capabilities.sessionPrompt) throw new Error("OpenCode 2 session.prompt capability is unavailable")
36
+
37
+ const host = createOpenCode2HostContract({
38
+ directory: options.directory,
39
+ subscribe: ctx.event.subscribe.bind(ctx.event),
40
+ sendPrompt: (request) => ctx.session.prompt(promptRequest(request)),
41
+ sendCommand: capabilities.sessionCommand ? (request) => ctx.session.command(commandRequest(request)) : undefined,
42
+ onEvent: options.onEvent,
43
+ onError: options.onError,
44
+ })
45
+
46
+ return Object.freeze({
47
+ start: () => host.start(),
48
+ prompt: (request) => host.prompt(request),
49
+ command: (request) => host.command(request),
50
+ dispose: (reason = "runtime-adapter-disposed") => host.dispose(reason),
51
+ runtimeManager: host.runtimeManager,
52
+ isStarted: host.isStarted,
53
+ isDisposed: host.isDisposed,
54
+ isHostDisposed: host.isHostDisposed,
55
+ })
56
+ }
@@ -0,0 +1,40 @@
1
+ import { durationToText } from "../core/args.js"
2
+ import { goalStatusText, isGoalJob, jobLabel } from "../core/jobs.js"
3
+
4
+ function dueAt(job, current) {
5
+ if (Number(job?.runNowRequestedAt || 0) > 0) return current
6
+ const intervalMs = Math.max(0, Number(job?.intervalMs || 0))
7
+ const lastRunAt = Number(job?.lastRunAt || 0)
8
+ if (intervalMs === 0) return current
9
+ if (lastRunAt > 0) return lastRunAt + intervalMs
10
+ if (job?.immediate === false) {
11
+ const createdAt = Date.parse(job?.createdAt || "")
12
+ return (Number.isFinite(createdAt) ? createdAt : current) + intervalMs
13
+ }
14
+ return current
15
+ }
16
+
17
+ export function formatOpenCode2LoopStatus(state, current = Date.now()) {
18
+ const jobs = Array.isArray(state?.jobs) ? state.jobs : []
19
+ const lines = jobs.length
20
+ ? jobs.map((job, index) => {
21
+ const dueIn = Math.max(0, dueAt(job, current) - current)
22
+ const flags = [
23
+ isGoalJob(job) ? `goal:${goalStatusText(job)}` : undefined,
24
+ job.paused ? "paused" : "active",
25
+ Number(job.runNowRequestedAt || 0) > 0 ? "run-now" : undefined,
26
+ job.safe ? "safe" : undefined,
27
+ job.askNever ? "ask-never" : undefined,
28
+ job.noOverlap ? "no-overlap" : undefined,
29
+ job.checkpointOnly ? "checkpoint-only" : undefined,
30
+ job.gitCheckpoint ? "git-checkpoint" : undefined,
31
+ ].filter(Boolean).join(",")
32
+ return `${index + 1}. ${job.id}${job.name ? ` (${job.name})` : ""}: ${jobLabel(job)} | runs=${job.runCount || 0} | failures=${job.failureCount || 0} | due in ${durationToText(dueIn)} | ${flags}`
33
+ })
34
+ : ["No active loop jobs."]
35
+
36
+ return Object.freeze({
37
+ jobs,
38
+ text: `OpenCode loop status:\n${lines.join("\n")}`,
39
+ })
40
+ }
@@ -0,0 +1,122 @@
1
+ import { now as defaultNow } from "../core/args.js"
2
+ import { appendLoopLog as defaultAppendLoopLog } from "../core/process.js"
3
+ import { compactSession as defaultCompactSession, log as defaultLog } from "../opencode/host.js"
4
+ import { sdkErrorMessage as defaultErrorMessage } from "../opencode/sdk.js"
5
+
6
+ export function createCompactionRuntime(options = {}) {
7
+ const activeRuns = options.activeRuns
8
+ if (!(activeRuns instanceof Map)) throw new TypeError("createCompactionRuntime requires activeRuns Map")
9
+ if (typeof options.finalizeActiveRun !== "function") throw new TypeError("createCompactionRuntime requires finalizeActiveRun")
10
+
11
+ const now = typeof options.now === "function" ? options.now : defaultNow
12
+ const appendLoopLog = typeof options.appendLoopLog === "function" ? options.appendLoopLog : defaultAppendLoopLog
13
+ const compactSession = typeof options.compactSession === "function" ? options.compactSession : defaultCompactSession
14
+ const log = typeof options.log === "function" ? options.log : defaultLog
15
+ const errorMessage = typeof options.errorMessage === "function" ? options.errorMessage : defaultErrorMessage
16
+ const finalizeActiveRun = options.finalizeActiveRun
17
+ const requests = new Map()
18
+
19
+ function begin(sessionID, jobId, resumeAfter = false) {
20
+ if (typeof sessionID !== "string" || typeof jobId !== "string") return undefined
21
+ const request = {
22
+ jobId,
23
+ resumeAfter: Boolean(resumeAfter),
24
+ requestedAt: now(),
25
+ startedAt: 0,
26
+ completedAt: 0,
27
+ }
28
+ requests.set(sessionID, request)
29
+ return request
30
+ }
31
+
32
+ function getPending(sessionID) {
33
+ return requests.get(sessionID)
34
+ }
35
+
36
+ function clear(sessionID) {
37
+ return requests.delete(sessionID)
38
+ }
39
+
40
+ function clearForActiveRun(sessionID, active) {
41
+ const pending = requests.get(sessionID)
42
+ if (!pending || !active || pending.jobId === active.jobId) requests.delete(sessionID)
43
+ }
44
+
45
+ function isCompleted(sessionID, jobId) {
46
+ const pending = requests.get(sessionID)
47
+ return Boolean(pending && pending.jobId === jobId && pending.completedAt)
48
+ }
49
+
50
+ async function start(directory, client, sessionID, jobId, model, resumeAfter = false) {
51
+ begin(sessionID, jobId, resumeAfter)
52
+ const ok = await compactSession(directory, client, sessionID, model)
53
+ if (!ok) clear(sessionID)
54
+ return ok
55
+ }
56
+
57
+ async function maybeCompact(directory, client, sessionID, job) {
58
+ const dueRuns = job.compactEveryRuns > 0 && (job.runCount || 0) > 0 && (job.runCount || 0) % job.compactEveryRuns === 0 && job.lastCompactRunCount !== job.runCount
59
+ const dueTime = job.compactEveryMs > 0 && (!job.lastCompactAt || now() - job.lastCompactAt >= job.compactEveryMs)
60
+ if (!dueRuns && !dueTime) return { job, started: false }
61
+
62
+ if (await start(directory, client, sessionID, job.id, job.model, true)) {
63
+ job.lastCompactAt = now()
64
+ job.lastCompactRunCount = job.runCount || 0
65
+ return { job, started: true }
66
+ }
67
+ return { job, started: false }
68
+ }
69
+
70
+ async function noteStarted(directory, sessionID) {
71
+ const pending = requests.get(sessionID)
72
+ if (!pending) return false
73
+ if (!pending.startedAt) {
74
+ pending.startedAt = now()
75
+ requests.set(sessionID, pending)
76
+ await appendLoopLog(directory, "compact-started", {
77
+ sessionID,
78
+ job: pending.jobId,
79
+ resumeAfter: pending.resumeAfter,
80
+ })
81
+ }
82
+ return true
83
+ }
84
+
85
+ async function finalize(directory, client, sessionID) {
86
+ const pending = requests.get(sessionID)
87
+ const active = activeRuns.get(sessionID)
88
+ if (!pending || !active || pending.jobId !== active.jobId) return false
89
+ return await finalizeActiveRun(directory, client, sessionID)
90
+ }
91
+
92
+ async function noteCompleted(directory, client, sessionID) {
93
+ const pending = requests.get(sessionID)
94
+ if (!pending) return false
95
+ pending.completedAt = now()
96
+ requests.set(sessionID, pending)
97
+ await appendLoopLog(directory, "compact-event", {
98
+ sessionID,
99
+ job: pending.jobId,
100
+ resumeAfter: pending.resumeAfter,
101
+ })
102
+ const timer = setTimeout(() => {
103
+ finalize(directory, client, sessionID)
104
+ .catch((error) => log(client, "error", "compaction finalization failed", { error: errorMessage(error) }))
105
+ }, 0)
106
+ timer.unref?.()
107
+ return true
108
+ }
109
+
110
+ return {
111
+ begin,
112
+ getPending,
113
+ clear,
114
+ clearForActiveRun,
115
+ isCompleted,
116
+ start,
117
+ maybeCompact,
118
+ noteStarted,
119
+ finalize,
120
+ noteCompleted,
121
+ }
122
+ }