opencode-queue 0.13.0 → 0.13.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 +4 -4
- package/index.ts +54 -27
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -72,9 +72,9 @@ do this next /queue front
|
|
|
72
72
|
| `/queue list` | Show the current queue. |
|
|
73
73
|
| `/queue stop` | Pause automatic sending of queued entries. |
|
|
74
74
|
| `/queue start` | Resume automatic sending of queued entries. |
|
|
75
|
-
| `/queue always` | Show whether automatic queueing is enabled
|
|
76
|
-
| `/queue always on` |
|
|
77
|
-
| `/queue always off` |
|
|
75
|
+
| `/queue always` | Show whether automatic queueing is enabled globally. |
|
|
76
|
+
| `/queue always on` | Enable automatic queueing in every project. |
|
|
77
|
+
| `/queue always off` | Disable automatic queueing in every project. |
|
|
78
78
|
| `/queue flush` | Send all queued entries immediately. |
|
|
79
79
|
| `/queue clear` | Clear the current queue. |
|
|
80
80
|
| `/queue clear 1` | Clear item 1 from the current queue. |
|
|
@@ -109,7 +109,7 @@ When the session is idle:
|
|
|
109
109
|
- `/queue flush` sends all queued entries immediately in one batch.
|
|
110
110
|
- `/queue clear` clears the current queue, and `/queue clear 1` clears a specific queued item.
|
|
111
111
|
|
|
112
|
-
Queues are scoped to the current project and session. They are stored in OpenCode's user data directory and restored with their previous running or stopped state after OpenCode restarts or crashes. The `always` setting applies to
|
|
112
|
+
Queues are scoped to the current project and session. They are stored in OpenCode's user data directory and restored with their previous running or stopped state after OpenCode restarts or crashes. The `always` setting applies to every OpenCode project. Restored queues do not replay just because the session starts idle; a running queue resumes after the session becomes busy and then finishes successfully. A send interrupted by a crash remains queued because the plugin cannot know whether OpenCode accepted it before exiting.
|
|
113
113
|
|
|
114
114
|
## Notes
|
|
115
115
|
|
package/index.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { Plugin } from "@opencode-ai/plugin"
|
|
|
2
2
|
import type { AgentPartInput, FilePart, FilePartInput, SubtaskPartInput, TextPart, TextPartInput } from "@opencode-ai/sdk"
|
|
3
3
|
import { HttpServerResponse } from "effect/unstable/http"
|
|
4
4
|
import { createHash, randomUUID } from "node:crypto"
|
|
5
|
-
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises"
|
|
5
|
+
import { link, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises"
|
|
6
6
|
import { homedir } from "node:os"
|
|
7
7
|
import { dirname, join } from "node:path"
|
|
8
8
|
|
|
@@ -43,8 +43,8 @@ type ControlOp =
|
|
|
43
43
|
|
|
44
44
|
type Activity = { kind: "idle" } | { kind: "restored" } | { kind: "busy" } | { kind: "sending"; idle: boolean; items: Item[] }
|
|
45
45
|
type State = { items: Item[]; activity: Activity; stopped: boolean; failed: boolean; hidden: Set<string> }
|
|
46
|
-
type Draft = Pick<State, "items" | "stopped" | "hidden">
|
|
47
|
-
type Store = { version: 1; projectID: string;
|
|
46
|
+
type Draft = Pick<State, "items" | "stopped" | "hidden">
|
|
47
|
+
type Store = { version: 1; projectID: string; sessions: Record<string, { items: Item[]; stopped: boolean; hidden: string[] }> }
|
|
48
48
|
type Placeholder = { id: string; part: TextPart }
|
|
49
49
|
|
|
50
50
|
type Op =
|
|
@@ -197,6 +197,17 @@ const dataHome = () => {
|
|
|
197
197
|
return join(homedir(), ".local", "share")
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
+
const writeJson = async (path: string, value: unknown, commit = rename) => {
|
|
201
|
+
await mkdir(dirname(path), { recursive: true })
|
|
202
|
+
const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`
|
|
203
|
+
try {
|
|
204
|
+
await writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 })
|
|
205
|
+
await commit(temporary, path)
|
|
206
|
+
} finally {
|
|
207
|
+
await rm(temporary, { force: true }).catch((error) => console.warn("QueuePlugin failed to remove temporary storage", error))
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
200
211
|
export const QueuePlugin: Plugin = async ({ client, project, directory }) => {
|
|
201
212
|
const sessions = new Map<string, State>()
|
|
202
213
|
const deleted = new Set<string>()
|
|
@@ -204,17 +215,28 @@ export const QueuePlugin: Plugin = async ({ client, project, directory }) => {
|
|
|
204
215
|
const internalCommands: { sid: string; command: string; args: string; used: boolean }[] = []
|
|
205
216
|
const origin = randomUUID()
|
|
206
217
|
const post = (client as unknown as { _client?: { post?: Post } })._client?.post
|
|
207
|
-
const
|
|
208
|
-
|
|
218
|
+
const root = join(dataHome(), "opencode", "opencode-queue")
|
|
219
|
+
const path = join(root, `${createHash("sha256").update(project.id).digest("hex")}.json`)
|
|
220
|
+
const settingsPath = join(root, "settings.json")
|
|
209
221
|
let writes = Promise.resolve()
|
|
210
222
|
|
|
223
|
+
const readAlways = async () => {
|
|
224
|
+
try {
|
|
225
|
+
const parsed: unknown = JSON.parse(await readFile(settingsPath, "utf8"))
|
|
226
|
+
if (record(parsed) && typeof parsed.always === "boolean") return parsed.always
|
|
227
|
+
console.warn("QueuePlugin ignored invalid global settings", settingsPath)
|
|
228
|
+
} catch (error) {
|
|
229
|
+
if (!record(error) || error.code !== "ENOENT") console.error("QueuePlugin failed to load global settings", error)
|
|
230
|
+
}
|
|
231
|
+
return false
|
|
232
|
+
}
|
|
233
|
+
let legacyAlways = false
|
|
234
|
+
|
|
211
235
|
try {
|
|
212
236
|
const parsed: unknown = JSON.parse(await readFile(path, "utf8"))
|
|
213
237
|
if (!record(parsed) || parsed.version !== 1 || parsed.projectID !== project.id || !record(parsed.sessions)) {
|
|
214
238
|
console.warn("QueuePlugin ignored invalid queue storage", path)
|
|
215
239
|
} else {
|
|
216
|
-
if (typeof parsed.always === "boolean") alwaysQueue = parsed.always
|
|
217
|
-
else if (parsed.always !== undefined) console.warn("QueuePlugin ignored invalid always queue setting", path)
|
|
218
240
|
for (const [sid, value] of Object.entries(parsed.sessions)) {
|
|
219
241
|
if (!record(value) || typeof value.stopped !== "boolean" || !Array.isArray(value.items)) {
|
|
220
242
|
console.warn("QueuePlugin skipped invalid stored session", sid)
|
|
@@ -228,28 +250,27 @@ export const QueuePlugin: Plugin = async ({ client, project, directory }) => {
|
|
|
228
250
|
const activity: Activity = { kind: items.length && !value.stopped ? "restored" : "idle" }
|
|
229
251
|
if (items.length || value.stopped || hidden.size) sessions.set(sid, { items, activity, stopped: value.stopped, failed: false, hidden })
|
|
230
252
|
}
|
|
253
|
+
legacyAlways = parsed.always === true
|
|
231
254
|
}
|
|
232
255
|
} catch (error) {
|
|
233
256
|
if (!record(error) || error.code !== "ENOENT") console.error("QueuePlugin failed to load queue storage", error)
|
|
234
257
|
}
|
|
235
|
-
|
|
258
|
+
if (legacyAlways) {
|
|
259
|
+
try {
|
|
260
|
+
await writeJson(settingsPath, { always: true }, link)
|
|
261
|
+
} catch (error) {
|
|
262
|
+
if (!record(error) || error.code !== "EEXIST") throw error
|
|
263
|
+
}
|
|
264
|
+
}
|
|
236
265
|
const save = async (sid?: string, draft?: Draft) => {
|
|
237
|
-
const stored: Store = { version: 1, projectID: project.id,
|
|
266
|
+
const stored: Store = { version: 1, projectID: project.id, sessions: {} }
|
|
238
267
|
for (const [id, current] of sessions) {
|
|
239
268
|
if (deleted.has(id) || (id === sid && !draft)) continue
|
|
240
269
|
const durable = id === sid ? draft! : current
|
|
241
270
|
const items = current.activity.kind === "sending" ? [...current.activity.items, ...durable.items] : durable.items
|
|
242
271
|
if (items.length || durable.stopped || durable.hidden.size) stored.sessions[id] = { items, stopped: durable.stopped, hidden: [...durable.hidden] }
|
|
243
272
|
}
|
|
244
|
-
|
|
245
|
-
await mkdir(dirname(path), { recursive: true })
|
|
246
|
-
const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`
|
|
247
|
-
try {
|
|
248
|
-
await writeFile(temporary, contents, { mode: 0o600 })
|
|
249
|
-
await rename(temporary, path)
|
|
250
|
-
} finally {
|
|
251
|
-
await rm(temporary, { force: true }).catch((error) => console.warn("QueuePlugin failed to remove temporary queue storage", error))
|
|
252
|
-
}
|
|
273
|
+
await writeJson(path, stored)
|
|
253
274
|
}
|
|
254
275
|
|
|
255
276
|
const serialize = <T>(action: () => Promise<T>) => {
|
|
@@ -267,7 +288,7 @@ export const QueuePlugin: Plugin = async ({ client, project, directory }) => {
|
|
|
267
288
|
return current
|
|
268
289
|
}
|
|
269
290
|
|
|
270
|
-
const automaticallyQueue = (sid: string) =>
|
|
291
|
+
const automaticallyQueue = async (sid: string) => shouldQueue(sessions.get(sid)) && (await readAlways())
|
|
271
292
|
|
|
272
293
|
const store = async (sid: string, current: State, draft: Draft, placeholder?: Placeholder) => {
|
|
273
294
|
try {
|
|
@@ -279,7 +300,6 @@ export const QueuePlugin: Plugin = async ({ client, project, directory }) => {
|
|
|
279
300
|
current.items.splice(0, current.items.length, ...draft.items)
|
|
280
301
|
current.stopped = draft.stopped
|
|
281
302
|
current.hidden = draft.hidden
|
|
282
|
-
alwaysQueue = draft.always
|
|
283
303
|
if (placeholder) Object.assign(placeholder.part, { text: "", synthetic: true, ignored: true })
|
|
284
304
|
}
|
|
285
305
|
|
|
@@ -287,7 +307,7 @@ export const QueuePlugin: Plugin = async ({ client, project, directory }) => {
|
|
|
287
307
|
serialize(async () => {
|
|
288
308
|
if (deleted.has(sid)) throw new Error(`QueuePlugin cannot persist queue state for deleted session ${sid}`)
|
|
289
309
|
const current = state(sid)
|
|
290
|
-
const draft: Draft = { items: [...current.items], stopped: current.stopped, hidden: new Set(current.hidden)
|
|
310
|
+
const draft: Draft = { items: [...current.items], stopped: current.stopped, hidden: new Set(current.hidden) }
|
|
291
311
|
if (placeholder) draft.hidden.add(placeholder.id)
|
|
292
312
|
const value = mutate(draft)
|
|
293
313
|
await store(sid, current, draft, placeholder)
|
|
@@ -451,7 +471,7 @@ export const QueuePlugin: Plugin = async ({ client, project, directory }) => {
|
|
|
451
471
|
|
|
452
472
|
const items = current.items.slice(0, count)
|
|
453
473
|
if (placeholder) {
|
|
454
|
-
const draft: Draft = { items: [...current.items], stopped: current.stopped, hidden: new Set(current.hidden).add(placeholder.id)
|
|
474
|
+
const draft: Draft = { items: [...current.items], stopped: current.stopped, hidden: new Set(current.hidden).add(placeholder.id) }
|
|
455
475
|
await store(sid, current, draft, placeholder)
|
|
456
476
|
if (deleted.has(sid)) return undefined
|
|
457
477
|
}
|
|
@@ -519,6 +539,16 @@ export const QueuePlugin: Plugin = async ({ client, project, directory }) => {
|
|
|
519
539
|
return result.failed ? `${message}; ${result.failed} failed` : message
|
|
520
540
|
}
|
|
521
541
|
|
|
542
|
+
if (op.kind === "always") {
|
|
543
|
+
const enabled = await serialize(async () => {
|
|
544
|
+
if (op.enabled === undefined) return readAlways()
|
|
545
|
+
await writeJson(settingsPath, { always: op.enabled })
|
|
546
|
+
return op.enabled
|
|
547
|
+
})
|
|
548
|
+
if (placeholder) await persist(sid, placeholder, () => undefined)
|
|
549
|
+
return `Always queue is ${enabled ? "on" : "off"} globally`
|
|
550
|
+
}
|
|
551
|
+
|
|
522
552
|
const message = await persist(sid, placeholder, (draft) => {
|
|
523
553
|
switch (op.kind) {
|
|
524
554
|
case "list": {
|
|
@@ -533,9 +563,6 @@ export const QueuePlugin: Plugin = async ({ client, project, directory }) => {
|
|
|
533
563
|
case "start":
|
|
534
564
|
draft.stopped = false
|
|
535
565
|
return "Queue started"
|
|
536
|
-
case "always":
|
|
537
|
-
if (op.enabled !== undefined) draft.always = op.enabled
|
|
538
|
-
return `Always queue is ${draft.always ? "on" : "off"} for this project`
|
|
539
566
|
}
|
|
540
567
|
})
|
|
541
568
|
if (op.kind === "start") {
|
|
@@ -618,7 +645,7 @@ export const QueuePlugin: Plugin = async ({ client, project, directory }) => {
|
|
|
618
645
|
}
|
|
619
646
|
|
|
620
647
|
if (!isQueue(input.command)) {
|
|
621
|
-
const queued = parseSuffix(body) ?? (automaticallyQueue(sid) ? { body } : undefined)
|
|
648
|
+
const queued = parseSuffix(body) ?? ((await automaticallyQueue(sid)) ? { body } : undefined)
|
|
622
649
|
if (!queued) return
|
|
623
650
|
|
|
624
651
|
if (!shouldQueue(sessions.get(sid))) {
|
|
@@ -672,7 +699,7 @@ export const QueuePlugin: Plugin = async ({ client, project, directory }) => {
|
|
|
672
699
|
const text = output.parts.find((part): part is TextPart => part.type === "text" && !part.synthetic)
|
|
673
700
|
if (!text) return
|
|
674
701
|
|
|
675
|
-
const request = parseInput(text.text) ?? (automaticallyQueue(sid) ? { body: text.text } : undefined)
|
|
702
|
+
const request = parseInput(text.text) ?? ((await automaticallyQueue(sid)) ? { body: text.text } : undefined)
|
|
676
703
|
if (!request) return
|
|
677
704
|
|
|
678
705
|
const current = state(sid)
|
package/package.json
CHANGED