Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/core/src/v1/config/permission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const InputObject = Schema.StructWithRest(
question: Schema.optional(Action),
webfetch: Schema.optional(Action),
websearch: Schema.optional(Action),
model_override: Schema.optional(Rule),
lsp: Schema.optional(Rule),
doom_loop: Schema.optional(Action),
skill: Schema.optional(Rule),
Expand Down
1 change: 1 addition & 0 deletions packages/opencode/src/agent/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ const layer = Layer.effect(
const defaults = Permission.fromConfig({
"*": "allow",
doom_loop: "ask",
model_override: "deny",
external_directory: {
"*": "ask",
...Object.fromEntries(whitelistedDirs.map((dir) => [dir, "allow"])),
Expand Down
11 changes: 9 additions & 2 deletions packages/opencode/src/provider/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -926,11 +926,18 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
if (model.api.id.toLowerCase().includes("north-mini-code")) {
return Object.fromEntries(["none", "high"].map((effort) => [effort, { reasoningEffort: effort }]))
}
const isDeepseekV4 = model.api.id.toLowerCase().includes("deepseek-v4")
const efforts = [...WIDELY_SUPPORTED_EFFORTS]
if (model.api.id.toLowerCase().includes("deepseek-v4")) {
if (isDeepseekV4) {
efforts.push("max")
}
return Object.fromEntries(efforts.map((effort) => [effort, { reasoningEffort: effort }]))
const result: Record<string, Record<string, any>> = Object.fromEntries(
efforts.map((effort) => [effort, { reasoningEffort: effort }]),
)
if (isDeepseekV4) {
result.none = { thinking: { type: "disabled" } }
}
return result

case "@ai-sdk/azure":
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/azure
Expand Down
5 changes: 5 additions & 0 deletions packages/opencode/src/session/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ const layer = Layer.effect(
const ops = Effect.fn("SessionPrompt.ops")(function* () {
return {
cancel: (sessionID: SessionID) => cancel(sessionID),
cancelRun: (sessionID: SessionID) => cancelRun(sessionID),
resolvePromptParts: (template: string) => resolvePromptParts(template),
prompt: (input: PromptInput) => prompt(input).pipe(Effect.catch(Effect.die)),
} satisfies TaskPromptOps
Expand All @@ -154,6 +155,10 @@ const layer = Layer.effect(
yield* state.cancel(sessionID)
})

const cancelRun = Effect.fn("SessionPrompt.cancelRun")(function* (sessionID: SessionID) {
yield* state.cancelRun(sessionID)
})

const resolvePromptParts = Effect.fn("SessionPrompt.resolvePromptParts")(function* (template: string) {
const ctx = yield* InstanceState.context
const parts: Types.DeepMutable<PromptInput["parts"]> = [{ type: "text", text: template }]
Expand Down
17 changes: 16 additions & 1 deletion packages/opencode/src/session/run-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { SessionStatus } from "./status"
export interface Interface {
readonly assertNotBusy: (sessionID: SessionID) => Effect.Effect<void, Session.BusyError>
readonly cancel: (sessionID: SessionID) => Effect.Effect<void>
readonly cancelRun: (sessionID: SessionID) => Effect.Effect<void>
readonly ensureRunning: (
sessionID: SessionID,
onInterrupt: Effect.Effect<SessionV1.WithParts>,
Expand Down Expand Up @@ -85,6 +86,20 @@ const layer = Layer.effect(
yield* existing.cancel
})

// Runner-only interrupt without cancelling background jobs.
// Exists so the Task tool fallback path can clear a stuck child prompt
// runner before re-prompting the same session, without self-cancelling
// the enclosing background job (the job's id equals the child session id).
const cancelRun = Effect.fn("SessionRunState.cancelRun")(function* (sessionID: SessionID) {
const data = yield* InstanceState.get(state)
const existing = data.runners.get(sessionID)
if (!existing) {
yield* status.set(sessionID, { type: "idle" })
return
}
yield* existing.cancel
})

const ensureRunning = Effect.fn("SessionRunState.ensureRunning")(function* (
sessionID: SessionID,
onInterrupt: Effect.Effect<SessionV1.WithParts>,
Expand All @@ -104,7 +119,7 @@ const layer = Layer.effect(
.pipe(Effect.catchTag("RunnerBusy", () => Effect.fail(busyError(sessionID))))
})

return Service.of({ assertNotBusy, cancel, ensureRunning, startShell })
return Service.of({ assertNotBusy, cancel, cancelRun, ensureRunning, startShell })
}),
)

Expand Down
27 changes: 26 additions & 1 deletion packages/opencode/src/session/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,10 +257,15 @@ export const GlobalInfo = Schema.Struct({
}).annotate({ identifier: "GlobalSession" })
export type GlobalInfo = Types.DeepMutable<Schema.Schema.Type<typeof GlobalInfo>>

// session.slug feeds filesystem paths (see plan()); restrict to id/path-safe characters.
export const SESSION_SLUG_PATTERN = /^[a-z0-9][a-z0-9-_]{0,63}$/

export const CreateInput = Schema.optional(
Schema.Struct({
id: Schema.optional(SessionID),
parentID: Schema.optional(SessionID),
title: Schema.optional(Schema.String),
slug: Schema.optional(Schema.String),
agent: Schema.optional(Schema.String),
model: Schema.optional(Model),
metadata: Schema.optional(Metadata),
Expand Down Expand Up @@ -416,15 +421,18 @@ export interface Interface {
readonly list: (input?: ListInput) => Effect.Effect<Info[]>
readonly listGlobal: (input?: GlobalListInput) => Effect.Effect<GlobalInfo[]>
readonly create: (input?: {
id?: SessionID
parentID?: SessionID
title?: string
slug?: string
agent?: string
model?: Schema.Schema.Type<typeof Model>
metadata?: typeof Metadata.Type
permission?: PermissionV1.Ruleset
workspaceID?: WorkspaceV2.ID
}) => Effect.Effect<Info>
readonly fork: (input: { sessionID: SessionID; messageID?: MessageID }) => Effect.Effect<Info, NotFound>
readonly root: (sessionID: SessionID) => Effect.Effect<SessionID, NotFound>
readonly touch: (sessionID: SessionID) => Effect.Effect<void>
readonly get: (id: SessionID) => Effect.Effect<Info, NotFound>
readonly setTitle: (input: { sessionID: SessionID; title: string }) => Effect.Effect<void>
Expand Down Expand Up @@ -509,11 +517,14 @@ const layer: Layer.Layer<
path?: string
metadata?: typeof Metadata.Type
permission?: PermissionV1.Ruleset
slug?: string
}) {
if (input.slug !== undefined && !SESSION_SLUG_PATTERN.test(input.slug))
return yield* Effect.die(new Error(`Invalid session slug: "${input.slug}"`))
const ctx = yield* InstanceState.context
const result: Info = {
id: SessionID.descending(input.id),
slug: Slug.create(),
slug: input.slug ?? Slug.create(),
version: InstallationVersion,
projectID: ctx.project.id,
directory: input.directory,
Expand Down Expand Up @@ -667,8 +678,10 @@ const layer: Layer.Layer<
})

const create = Effect.fn("Session.create")(function* (input?: {
id?: SessionID
parentID?: SessionID
title?: string
slug?: string
agent?: string
model?: Schema.Schema.Type<typeof Model>
metadata?: typeof Metadata.Type
Expand All @@ -678,10 +691,12 @@ const layer: Layer.Layer<
const ctx = yield* InstanceState.context
const workspace = yield* InstanceState.workspaceID
return yield* createNext({
id: input?.id,
parentID: input?.parentID,
directory: ctx.directory,
path: sessionPath(ctx.worktree, ctx.directory),
title: input?.title,
slug: input?.slug,
agent: input?.agent,
model: input?.model,
metadata: input?.metadata,
Expand Down Expand Up @@ -733,6 +748,15 @@ const layer: Layer.Layer<
return session
})

const root = Effect.fn("Session.root")(function* (sessionID: SessionID) {
let current = sessionID
while (true) {
const s = yield* get(current)
if (!s.parentID) return current
current = s.parentID
}
})

const patch = (sessionID: SessionID, info: Patch) =>
Effect.gen(function* () {
const current = yield* get(sessionID)
Expand Down Expand Up @@ -910,6 +934,7 @@ const layer: Layer.Layer<
listGlobal,
create,
fork,
root,
touch,
get,
setTitle,
Expand Down
Loading
Loading