sync
Some checks are pending
format / format (push) Waiting to run
test / test (push) Waiting to run

This commit is contained in:
Dax Raad 2025-11-12 21:56:21 -05:00
parent d6f2864622
commit 7718aec891
8 changed files with 31 additions and 246 deletions

View file

@ -1,5 +1,6 @@
---
description: Git commit and push
subtask: true
---
commit and push

View file

@ -3,9 +3,7 @@
"plugin": ["opencode-openai-codex-auth"],
"provider": {
"opencode": {
"options": {
"baseURL": "http://localhost:8080"
}
"options": {}
}
}
}

View file

@ -27,7 +27,6 @@ import { Global } from "../global"
import { ProjectRoute } from "./project"
import { ToolRegistry } from "../tool/registry"
import { zodToJsonSchema } from "zod-to-json-schema"
import { SessionLock } from "../session/lock"
import { SessionPrompt } from "../session/prompt"
import { SessionCompaction } from "../session/compaction"
import { SessionRevert } from "../session/revert"

View file

@ -13,7 +13,6 @@ import { SessionPrompt } from "./prompt"
import { Flag } from "../flag/flag"
import { Token } from "../util/token"
import { Log } from "../util/log"
import { SessionLock } from "./lock"
import { ProviderTransform } from "@/provider/transform"
import { SessionRetry } from "./retry"
import { Config } from "@/config/config"
@ -88,7 +87,6 @@ export namespace SessionCompaction {
}
export async function run(input: { sessionID: string; providerID: string; modelID: string; signal?: AbortSignal }) {
if (!input.signal) SessionLock.assertUnlocked(input.sessionID)
await using lock = input.signal === undefined ? SessionLock.acquire({ sessionID: input.sessionID }) : undefined
const signal = input.signal ?? lock!.signal

View file

@ -1,97 +0,0 @@
import z from "zod"
import { Instance } from "../project/instance"
import { Log } from "../util/log"
import { NamedError } from "../util/error"
export namespace SessionLock {
const log = Log.create({ service: "session.lock" })
export const LockedError = NamedError.create(
"SessionLockedError",
z.object({
sessionID: z.string(),
message: z.string(),
}),
)
type LockState = {
controller: AbortController
created: number
}
const state = Instance.state(
() => {
const locks = new Map<string, LockState>()
return {
locks,
}
},
async (current) => {
for (const [sessionID, lock] of current.locks) {
log.info("force abort", { sessionID })
lock.controller.abort()
}
current.locks.clear()
},
)
function get(sessionID: string) {
return state().locks.get(sessionID)
}
function unset(input: { sessionID: string; controller: AbortController }) {
const lock = get(input.sessionID)
if (!lock) return false
if (lock.controller !== input.controller) return false
state().locks.delete(input.sessionID)
return true
}
export function acquire(input: { sessionID: string }) {
const lock = get(input.sessionID)
if (lock) {
throw new LockedError({
sessionID: input.sessionID,
message: `Session ${input.sessionID} is locked`,
})
}
const controller = new AbortController()
state().locks.set(input.sessionID, {
controller,
created: Date.now(),
})
log.info("locked", { sessionID: input.sessionID })
return {
signal: controller.signal,
abort() {
controller.abort()
unset({ sessionID: input.sessionID, controller })
},
async [Symbol.dispose]() {
const removed = unset({ sessionID: input.sessionID, controller })
if (removed) {
log.info("unlocked", { sessionID: input.sessionID })
}
},
}
}
export function abort(sessionID: string) {
const lock = get(sessionID)
if (!lock) return false
log.info("abort", { sessionID })
lock.controller.abort()
state().locks.delete(sessionID)
return true
}
export function isLocked(sessionID: string) {
return get(sessionID) !== undefined
}
export function assertUnlocked(sessionID: string) {
const lock = get(sessionID)
if (!lock) return
throw new LockedError({ sessionID, message: `Session ${sessionID} is locked` })
}
}

View file

@ -49,14 +49,12 @@ import { $, fileURLToPath } from "bun"
import { ConfigMarkdown } from "../config/markdown"
import { SessionSummary } from "./summary"
import { NamedError } from "@/util/error"
import { SessionLock } from "./lock"
import { fn } from "@/util/fn"
import { SessionRetry } from "./retry"
export namespace SessionPrompt {
const log = Log.create({ service: "session.prompt" })
export const OUTPUT_TOKEN_MAX = 32_000
const MAX_RETRIES = 10
const DOOM_LOOP_THRESHOLD = 3
export const Status = z
@ -132,6 +130,19 @@ export namespace SessionPrompt {
return state().status
}
export function getStatus(sessionID: string) {
return (
state().status[sessionID] ?? {
type: "idle",
}
)
}
export function assertNotBusy(sessionID: string) {
const status = getStatus(sessionID)
if (status?.type !== "idle") throw new Session.BusyError(sessionID)
}
export const setStatus = fn(z.object({ sessionID: Identifier.schema("session"), status: Status }), (input) => {
Bus.publish(Event.Status, { sessionID: input.sessionID, status: input.status })
if (input.status.type === "idle") {
@ -1387,7 +1398,6 @@ export namespace SessionPrompt {
})
export type ShellInput = z.infer<typeof ShellInput>
export async function shell(input: ShellInput) {
using abort = SessionLock.acquire({ sessionID: input.sessionID })
const session = await Session.get(input.sessionID)
if (session.revert) {
SessionRevert.cleanup(session)
@ -1502,7 +1512,6 @@ export namespace SessionPrompt {
const proc = spawn(shell, args, {
cwd: Instance.directory,
signal: abort.signal,
detached: true,
stdio: ["ignore", "pipe", "pipe"],
env: {
@ -1511,11 +1520,6 @@ export namespace SessionPrompt {
},
})
abort.signal.addEventListener("abort", () => {
if (!proc.pid) return
process.kill(-proc.pid)
})
let output = ""
proc.stdout?.on("data", (chunk) => {
@ -1646,132 +1650,22 @@ export namespace SessionPrompt {
})()
const agent = await Agent.get(agentName)
let result: MessageV2.WithParts
if ((agent.mode === "subagent" && command.subtask !== false) || command.subtask === true) {
using abort = SessionLock.acquire({ sessionID: input.sessionID })
const userMsg: MessageV2.User = {
id: Identifier.ascending("message"),
sessionID: input.sessionID,
time: {
created: Date.now(),
},
role: "user",
agent: agentName,
model: {
providerID: model.providerID,
modelID: model.modelID,
},
}
await Session.updateMessage(userMsg)
const userPart: MessageV2.Part = {
type: "text",
id: Identifier.ascending("part"),
messageID: userMsg.id,
sessionID: input.sessionID,
text: "The following tool was executed by the user",
synthetic: true,
}
await Session.updatePart(userPart)
const assistantMsg: MessageV2.Assistant = {
id: Identifier.ascending("message"),
sessionID: input.sessionID,
parentID: userMsg.id,
mode: agentName,
cost: 0,
path: {
cwd: Instance.directory,
root: Instance.worktree,
},
time: {
created: Date.now(),
},
role: "assistant",
tokens: {
input: 0,
output: 0,
reasoning: 0,
cache: { read: 0, write: 0 },
},
modelID: model.modelID,
providerID: model.providerID,
}
await Session.updateMessage(assistantMsg)
const args = {
description: "Consulting " + agent.name,
subagent_type: agent.name,
prompt: template,
}
const toolPart: MessageV2.ToolPart = {
type: "tool",
id: Identifier.ascending("part"),
messageID: assistantMsg.id,
sessionID: input.sessionID,
tool: "task",
callID: ulid(),
state: {
status: "running",
time: {
start: Date.now(),
},
input: {
description: args.description,
subagent_type: args.subagent_type,
// truncate prompt to preserve context
prompt: args.prompt.length > 100 ? args.prompt.substring(0, 97) + "..." : args.prompt,
},
},
}
await Session.updatePart(toolPart)
const taskResult = await TaskTool.init().then((t) =>
t.execute(args, {
sessionID: input.sessionID,
abort: abort.signal,
agent: agent.name,
messageID: assistantMsg.id,
extra: {},
metadata: async (metadata) => {
if (toolPart.state.status === "running") {
toolPart.state.metadata = metadata.metadata
toolPart.state.title = metadata.title
await Session.updatePart(toolPart)
}
},
}),
)
assistantMsg.time.completed = Date.now()
await Session.updateMessage(assistantMsg)
if (toolPart.state.status === "running") {
toolPart.state = {
status: "completed",
time: {
...toolPart.state.time,
end: Date.now(),
},
input: toolPart.state.input,
title: "",
metadata: taskResult.metadata,
output: taskResult.output,
}
await Session.updatePart(toolPart)
}
result = { info: assistantMsg, parts: [toolPart] }
} else {
result = (await prompt({
sessionID: input.sessionID,
messageID: input.messageID,
model,
agent: agentName,
parts,
})) as MessageV2.WithParts
parts.push({
type: "agent",
name: agent.name,
})
}
const result = (await prompt({
sessionID: input.sessionID,
messageID: input.messageID,
model,
agent: agentName,
parts,
})) as MessageV2.WithParts
Bus.publish(Command.Event.Executed, {
name: input.command,
sessionID: input.sessionID,

View file

@ -7,7 +7,7 @@ import { Log } from "../util/log"
import { splitWhen } from "remeda"
import { Storage } from "../storage/storage"
import { Bus } from "../bus"
import { SessionLock } from "./lock"
import { SessionPrompt } from "./prompt"
export namespace SessionRevert {
const log = Log.create({ service: "session.revert" })
@ -20,11 +20,7 @@ export namespace SessionRevert {
export type RevertInput = z.infer<typeof RevertInput>
export async function revert(input: RevertInput) {
SessionLock.assertUnlocked(input.sessionID)
using _ = SessionLock.acquire({
sessionID: input.sessionID,
})
SessionPrompt.assertNotBusy(input.sessionID)
const all = await Session.messages({ sessionID: input.sessionID })
let lastUser: MessageV2.User | undefined
const session = await Session.get(input.sessionID)
@ -70,10 +66,7 @@ export namespace SessionRevert {
export async function unrevert(input: { sessionID: string }) {
log.info("unreverting", input)
SessionLock.assertUnlocked(input.sessionID)
using _ = SessionLock.acquire({
sessionID: input.sessionID,
})
SessionPrompt.assertNotBusy(input.sessionID)
const session = await Session.get(input.sessionID)
if (!session.revert) return session
if (session.revert.snapshot) await Snapshot.restore(session.revert.snapshot)

View file

@ -6,7 +6,6 @@ import { Bus } from "../bus"
import { MessageV2 } from "../session/message-v2"
import { Identifier } from "../id/id"
import { Agent } from "../agent/agent"
import { SessionLock } from "../session/lock"
import { SessionPrompt } from "../session/prompt"
export const TaskTool = Tool.define("task", async () => {
@ -63,7 +62,7 @@ export const TaskTool = Tool.define("task", async () => {
}
ctx.abort.addEventListener("abort", () => {
SessionLock.abort(session.id)
SessionPrompt.cancel(session.id)
})
const promptParts = await SessionPrompt.resolvePromptParts(params.prompt)
const result = await SessionPrompt.prompt({