diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/session.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/session.ts
index 3d88db60db..c4def3e742 100644
--- a/packages/opencode/src/server/routes/instance/httpapi/handlers/session.ts
+++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/session.ts
@@ -1,5 +1,5 @@
import * as InstanceState from "@/effect/instance-state"
-import { AppRuntime } from "@/effect/app-runtime"
+import { EffectBridge } from "@/effect/bridge"
import { Agent } from "@/agent/agent"
import { Bus } from "@/bus"
import { Command } from "@/command"
@@ -53,6 +53,13 @@ const mapNotFound = (self: Effect.Effect) =>
export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session", (handlers) =>
Effect.gen(function* () {
const session = yield* Session.Service
+ const shareSvc = yield* SessionShare.Service
+ const promptSvc = yield* SessionPrompt.Service
+ const revertSvc = yield* SessionRevert.Service
+ const compactSvc = yield* SessionCompaction.Service
+ const runState = yield* SessionRunState.Service
+ const agentSvc = yield* Agent.Service
+ const permissionSvc = yield* Permission.Service
const statusSvc = yield* SessionStatus.Service
const todoSvc = yield* Todo.Service
const summary = yield* SessionSummary.Service
@@ -148,14 +155,7 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
})
const create = Effect.fn("SessionHttpApi.create")(function* (ctx: { payload?: Session.CreateInput }) {
- const instance = yield* InstanceState.context
- return yield* Effect.promise(() =>
- Instance.restore(instance, () =>
- AppRuntime.runPromise(
- SessionShare.Service.use((svc) => svc.create(ctx.payload)).pipe(Effect.provide(SessionShare.defaultLayer)),
- ),
- ),
- )
+ return yield* shareSvc.create(ctx.payload)
})
const createRaw = Effect.fn("SessionHttpApi.createRaw")(function* (ctx: {
@@ -175,14 +175,7 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
})
const remove = Effect.fn("SessionHttpApi.remove")(function* (ctx: { params: { sessionID: SessionID } }) {
- const instance = yield* InstanceState.context
- yield* Effect.promise(() =>
- Instance.restore(instance, () =>
- AppRuntime.runPromise(
- Session.Service.use((svc) => svc.remove(ctx.params.sessionID)).pipe(Effect.provide(Session.defaultLayer)),
- ),
- ),
- )
+ yield* session.remove(ctx.params.sessionID)
return true
})
@@ -190,60 +183,31 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
params: { sessionID: SessionID }
payload: typeof UpdatePayload.Type
}) {
- const instance = yield* InstanceState.context
- return yield* Effect.promise(() =>
- Instance.restore(instance, () =>
- AppRuntime.runPromise(
- Session.Service.use((svc) =>
- Effect.gen(function* () {
- const current = yield* svc.get(ctx.params.sessionID)
- if (ctx.payload.title !== undefined) {
- yield* svc.setTitle({ sessionID: ctx.params.sessionID, title: ctx.payload.title })
- }
- if (ctx.payload.permission !== undefined) {
- yield* svc.setPermission({
- sessionID: ctx.params.sessionID,
- permission: Permission.merge(current.permission ?? [], ctx.payload.permission),
- })
- }
- if (ctx.payload.time?.archived !== undefined) {
- yield* svc.setArchived({ sessionID: ctx.params.sessionID, time: ctx.payload.time.archived })
- }
- return yield* svc.get(ctx.params.sessionID)
- }),
- ).pipe(Effect.provide(Session.defaultLayer)),
- ),
- ),
- )
+ const current = yield* session.get(ctx.params.sessionID)
+ if (ctx.payload.title !== undefined) {
+ yield* session.setTitle({ sessionID: ctx.params.sessionID, title: ctx.payload.title })
+ }
+ if (ctx.payload.permission !== undefined) {
+ yield* session.setPermission({
+ sessionID: ctx.params.sessionID,
+ permission: Permission.merge(current.permission ?? [], ctx.payload.permission),
+ })
+ }
+ if (ctx.payload.time?.archived !== undefined) {
+ yield* session.setArchived({ sessionID: ctx.params.sessionID, time: ctx.payload.time.archived })
+ }
+ return yield* session.get(ctx.params.sessionID)
})
const fork = Effect.fn("SessionHttpApi.fork")(function* (ctx: {
params: { sessionID: SessionID }
payload: typeof ForkPayload.Type
}) {
- const instance = yield* InstanceState.context
- return yield* Effect.promise(() =>
- Instance.restore(instance, () =>
- AppRuntime.runPromise(
- Session.Service.use((svc) =>
- svc.fork({ sessionID: ctx.params.sessionID, messageID: ctx.payload.messageID }),
- ).pipe(Effect.provide(Session.defaultLayer)),
- ),
- ),
- )
+ return yield* session.fork({ sessionID: ctx.params.sessionID, messageID: ctx.payload.messageID })
})
const abort = Effect.fn("SessionHttpApi.abort")(function* (ctx: { params: { sessionID: SessionID } }) {
- const instance = yield* InstanceState.context
- yield* Effect.promise(() =>
- Instance.restore(instance, () =>
- AppRuntime.runPromise(
- SessionPrompt.Service.use((svc) => svc.cancel(ctx.params.sessionID)).pipe(
- Effect.provide(SessionPrompt.defaultLayer),
- ),
- ),
- ),
- )
+ yield* promptSvc.cancel(ctx.params.sessionID)
return true
})
@@ -251,98 +215,45 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
params: { sessionID: SessionID }
payload: typeof InitPayload.Type
}) {
- const instance = yield* InstanceState.context
- yield* Effect.promise(() =>
- Instance.restore(instance, () =>
- AppRuntime.runPromise(
- SessionPrompt.Service.use((svc) =>
- svc.command({
- sessionID: ctx.params.sessionID,
- messageID: ctx.payload.messageID,
- model: `${ctx.payload.providerID}/${ctx.payload.modelID}`,
- command: Command.Default.INIT,
- arguments: "",
- }),
- ).pipe(Effect.provide(SessionPrompt.defaultLayer)),
- ),
- ),
- )
+ yield* promptSvc.command({
+ sessionID: ctx.params.sessionID,
+ messageID: ctx.payload.messageID,
+ model: `${ctx.payload.providerID}/${ctx.payload.modelID}`,
+ command: Command.Default.INIT,
+ arguments: "",
+ })
return true
})
const share = Effect.fn("SessionHttpApi.share")(function* (ctx: { params: { sessionID: SessionID } }) {
- const instance = yield* InstanceState.context
- return yield* Effect.promise(() =>
- Instance.restore(instance, () =>
- AppRuntime.runPromise(
- Effect.gen(function* () {
- const share = yield* SessionShare.Service
- const session = yield* Session.Service
- yield* share.share(ctx.params.sessionID)
- return yield* session.get(ctx.params.sessionID)
- }).pipe(Effect.provide(SessionShare.defaultLayer)),
- ),
- ),
- )
+ yield* shareSvc.share(ctx.params.sessionID).pipe(Effect.mapError(() => new HttpApiError.BadRequest({})))
+ return yield* session.get(ctx.params.sessionID)
})
const unshare = Effect.fn("SessionHttpApi.unshare")(function* (ctx: { params: { sessionID: SessionID } }) {
- const instance = yield* InstanceState.context
- return yield* Effect.promise(() =>
- Instance.restore(instance, () =>
- AppRuntime.runPromise(
- Effect.gen(function* () {
- const share = yield* SessionShare.Service
- const session = yield* Session.Service
- yield* share.unshare(ctx.params.sessionID)
- return yield* session.get(ctx.params.sessionID)
- }).pipe(Effect.provide(SessionShare.defaultLayer)),
- ),
- ),
- )
+ yield* shareSvc.unshare(ctx.params.sessionID).pipe(Effect.mapError(() => new HttpApiError.BadRequest({})))
+ return yield* session.get(ctx.params.sessionID)
})
const summarize = Effect.fn("SessionHttpApi.summarize")(function* (ctx: {
params: { sessionID: SessionID }
payload: typeof SummarizePayload.Type
}) {
- const instance = yield* InstanceState.context
- yield* Effect.promise(() =>
- Instance.restore(instance, () =>
- AppRuntime.runPromise(
- Effect.gen(function* () {
- const session = yield* Session.Service
- const revert = yield* SessionRevert.Service
- const compact = yield* SessionCompaction.Service
- const prompt = yield* SessionPrompt.Service
- const agent = yield* Agent.Service
+ yield* revertSvc.cleanup(yield* session.get(ctx.params.sessionID))
+ const messages = yield* session.messages({ sessionID: ctx.params.sessionID })
+ const defaultAgent = yield* agentSvc.defaultAgent()
+ const currentAgent = messages.findLast((message) => message.info.role === "user")?.info.agent ?? defaultAgent
- yield* revert.cleanup(yield* session.get(ctx.params.sessionID))
- const messages = yield* session.messages({ sessionID: ctx.params.sessionID })
- const defaultAgent = yield* agent.defaultAgent()
- const currentAgent =
- messages.findLast((message) => message.info.role === "user")?.info.agent ?? defaultAgent
-
- yield* compact.create({
- sessionID: ctx.params.sessionID,
- agent: currentAgent,
- model: {
- providerID: ctx.payload.providerID,
- modelID: ctx.payload.modelID,
- },
- auto: ctx.payload.auto ?? false,
- })
- yield* prompt.loop({ sessionID: ctx.params.sessionID })
- }).pipe(
- Effect.provide(SessionRevert.defaultLayer),
- Effect.provide(SessionCompaction.defaultLayer),
- Effect.provide(SessionPrompt.defaultLayer),
- Effect.provide(Agent.defaultLayer),
- Effect.provide(Session.defaultLayer),
- ),
- ),
- ),
- )
+ yield* compactSvc.create({
+ sessionID: ctx.params.sessionID,
+ agent: currentAgent,
+ model: {
+ providerID: ctx.payload.providerID,
+ modelID: ctx.payload.modelID,
+ },
+ auto: ctx.payload.auto ?? false,
+ })
+ yield* promptSvc.loop({ sessionID: ctx.params.sessionID })
return true
})
@@ -350,19 +261,15 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
params: { sessionID: SessionID }
payload: typeof PromptPayload.Type
}) {
- const instance = yield* InstanceState.context
+ const bridge = yield* EffectBridge.make()
return HttpServerResponse.stream(
Stream.fromEffect(
Effect.promise(() =>
- Instance.restore(instance, () =>
- AppRuntime.runPromise(
- SessionPrompt.Service.use((svc) =>
- svc.prompt({
- ...ctx.payload,
- sessionID: ctx.params.sessionID,
- } as unknown as SessionPrompt.PromptInput),
- ).pipe(Effect.provide(SessionPrompt.defaultLayer)),
- ),
+ bridge.promise(
+ promptSvc.prompt({
+ ...ctx.payload,
+ sessionID: ctx.params.sessionID,
+ } as unknown as SessionPrompt.PromptInput),
),
),
).pipe(
@@ -377,23 +284,23 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
params: { sessionID: SessionID }
payload: typeof PromptPayload.Type
}) {
- const instance = yield* InstanceState.context
+ const bridge = yield* EffectBridge.make()
yield* Effect.sync(() => {
- Instance.restore(instance, () => {
- void AppRuntime.runPromise(
- SessionPrompt.Service.use((svc) =>
- svc.prompt({ ...ctx.payload, sessionID: ctx.params.sessionID } as unknown as SessionPrompt.PromptInput),
- ).pipe(Effect.provide(SessionPrompt.defaultLayer)),
- ).catch((error) => {
- log.error("prompt_async failed", { sessionID: ctx.params.sessionID, error })
- void Bus.publish(Session.Event.Error, {
- sessionID: ctx.params.sessionID,
- error: new NamedError.Unknown({
- message: error instanceof Error ? error.message : String(error),
- }).toObject(),
- })
- })
- })
+ bridge.fork(
+ promptSvc
+ .prompt({ ...ctx.payload, sessionID: ctx.params.sessionID } as unknown as SessionPrompt.PromptInput)
+ .pipe(
+ Effect.catchCause((error) =>
+ Effect.sync(() => {
+ log.error("prompt_async failed", { sessionID: ctx.params.sessionID, error })
+ void Bus.publish(Session.Event.Error, {
+ sessionID: ctx.params.sessionID,
+ error: new NamedError.Unknown({ message: String(error) }).toObject(),
+ })
+ }),
+ ),
+ ),
+ )
})
return HttpApiSchema.NoContent.make()
})
@@ -402,111 +309,47 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
params: { sessionID: SessionID }
payload: typeof CommandPayload.Type
}) {
- const instance = yield* InstanceState.context
- return yield* Effect.promise(() =>
- Instance.restore(instance, () =>
- AppRuntime.runPromise(
- SessionPrompt.Service.use((svc) =>
- svc.command({ ...ctx.payload, sessionID: ctx.params.sessionID } as SessionPrompt.CommandInput),
- ).pipe(Effect.provide(SessionPrompt.defaultLayer)),
- ),
- ),
- )
+ return yield* promptSvc.command({ ...ctx.payload, sessionID: ctx.params.sessionID } as SessionPrompt.CommandInput)
})
const shell = Effect.fn("SessionHttpApi.shell")(function* (ctx: {
params: { sessionID: SessionID }
payload: typeof ShellPayload.Type
}) {
- const instance = yield* InstanceState.context
- return yield* Effect.promise(() =>
- Instance.restore(instance, () =>
- AppRuntime.runPromise(
- SessionPrompt.Service.use((svc) =>
- svc.shell({ ...ctx.payload, sessionID: ctx.params.sessionID } as SessionPrompt.ShellInput),
- ).pipe(Effect.provide(SessionPrompt.defaultLayer)),
- ),
- ),
- )
+ return yield* promptSvc.shell({ ...ctx.payload, sessionID: ctx.params.sessionID } as SessionPrompt.ShellInput)
})
const revert = Effect.fn("SessionHttpApi.revert")(function* (ctx: {
params: { sessionID: SessionID }
payload: typeof RevertPayload.Type
}) {
- const instance = yield* InstanceState.context
- log.info("revert", ctx.payload)
- return yield* Effect.promise(() =>
- Instance.restore(instance, () =>
- AppRuntime.runPromise(
- SessionRevert.Service.use((svc) => svc.revert({ sessionID: ctx.params.sessionID, ...ctx.payload })).pipe(
- Effect.provide(SessionRevert.defaultLayer),
- ),
- ),
- ),
- )
+ return yield* revertSvc.revert({ sessionID: ctx.params.sessionID, ...ctx.payload })
})
const unrevert = Effect.fn("SessionHttpApi.unrevert")(function* (ctx: { params: { sessionID: SessionID } }) {
- const instance = yield* InstanceState.context
- return yield* Effect.promise(() =>
- Instance.restore(instance, () =>
- AppRuntime.runPromise(
- SessionRevert.Service.use((svc) => svc.unrevert({ sessionID: ctx.params.sessionID })).pipe(
- Effect.provide(SessionRevert.defaultLayer),
- ),
- ),
- ),
- )
+ return yield* revertSvc.unrevert({ sessionID: ctx.params.sessionID })
})
const permissionRespond = Effect.fn("SessionHttpApi.permissionRespond")(function* (ctx: {
params: { permissionID: PermissionID }
payload: typeof PermissionResponsePayload.Type
}) {
- const instance = yield* InstanceState.context
- yield* Effect.promise(() =>
- Instance.restore(instance, () =>
- AppRuntime.runPromise(
- Permission.Service.use((svc) =>
- svc.reply({ requestID: ctx.params.permissionID, reply: ctx.payload.response }),
- ).pipe(Effect.provide(Permission.defaultLayer)),
- ),
- ),
- )
+ yield* permissionSvc.reply({ requestID: ctx.params.permissionID, reply: ctx.payload.response })
return true
})
const deleteMessage = Effect.fn("SessionHttpApi.deleteMessage")(function* (ctx: {
params: { sessionID: SessionID; messageID: MessageID }
}) {
- const instance = yield* InstanceState.context
- yield* Effect.promise(() =>
- Instance.restore(instance, () =>
- AppRuntime.runPromise(
- Effect.gen(function* () {
- const state = yield* SessionRunState.Service
- const session = yield* Session.Service
- yield* state.assertNotBusy(ctx.params.sessionID)
- yield* session.removeMessage(ctx.params)
- }).pipe(Effect.provide(SessionRunState.defaultLayer), Effect.provide(Session.defaultLayer)),
- ),
- ),
- )
+ yield* runState.assertNotBusy(ctx.params.sessionID)
+ yield* session.removeMessage(ctx.params)
return true
})
const deletePart = Effect.fn("SessionHttpApi.deletePart")(function* (ctx: {
params: { sessionID: SessionID; messageID: MessageID; partID: PartID }
}) {
- const instance = yield* InstanceState.context
- yield* Effect.promise(() =>
- Instance.restore(instance, () =>
- AppRuntime.runPromise(
- Session.Service.use((svc) => svc.removePart(ctx.params)).pipe(Effect.provide(Session.defaultLayer)),
- ),
- ),
- )
+ yield* session.removePart(ctx.params)
return true
})
@@ -524,14 +367,7 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
`Part mismatch: body.id='${payload.id}' vs partID='${ctx.params.partID}', body.messageID='${payload.messageID}' vs messageID='${ctx.params.messageID}', body.sessionID='${payload.sessionID}' vs sessionID='${ctx.params.sessionID}'`,
)
}
- const instance = yield* InstanceState.context
- return yield* Effect.promise(() =>
- Instance.restore(instance, () =>
- AppRuntime.runPromise(
- Session.Service.use((svc) => svc.updatePart(payload)).pipe(Effect.provide(Session.defaultLayer)),
- ),
- ),
- )
+ return yield* session.updatePart(payload)
})
return handlers
diff --git a/packages/opencode/src/server/routes/instance/httpapi/server.ts b/packages/opencode/src/server/routes/instance/httpapi/server.ts
index d8208c7657..600b4f6087 100644
--- a/packages/opencode/src/server/routes/instance/httpapi/server.ts
+++ b/packages/opencode/src/server/routes/instance/httpapi/server.ts
@@ -22,10 +22,14 @@ import { Provider } from "@/provider/provider"
import { Pty } from "@/pty"
import { Question } from "@/question"
import { Session } from "@/session/session"
+import { SessionCompaction } from "@/session/compaction"
+import { SessionPrompt } from "@/session/prompt"
+import { SessionRevert } from "@/session/revert"
import { SessionRunState } from "@/session/run-state"
import { SessionStatus } from "@/session/status"
import { SessionSummary } from "@/session/summary"
import { Todo } from "@/session/todo"
+import { SessionShare } from "@/share/session"
import { Skill } from "@/skill"
import { ToolRegistry } from "@/tool/registry"
import { lazy } from "@/util/lazy"
@@ -134,6 +138,10 @@ export const routes = Layer.mergeAll(rootApiRoutes, instanceRoutes).pipe(
Question.defaultLayer,
Ripgrep.defaultLayer,
Session.defaultLayer,
+ SessionCompaction.defaultLayer,
+ SessionPrompt.defaultLayer,
+ SessionRevert.defaultLayer,
+ SessionShare.defaultLayer,
SessionRunState.defaultLayer,
SessionStatus.defaultLayer,
SessionSummary.defaultLayer,
diff --git a/packages/opencode/src/session/session.ts b/packages/opencode/src/session/session.ts
index 9a50a9a980..72c4d241eb 100644
--- a/packages/opencode/src/session/session.ts
+++ b/packages/opencode/src/session/session.ts
@@ -589,7 +589,7 @@ export const layer: Layer.Layer =
path: sessionPath(ctx.worktree, ctx.directory),
title: input?.title,
permission: input?.permission,
- workspaceID: workspace,
+ workspaceID: input?.workspaceID ?? workspace,
})
})
diff --git a/packages/opencode/test/server/httpapi-session.test.ts b/packages/opencode/test/server/httpapi-session.test.ts
index 75e4a3ac9b..c7d0945436 100644
--- a/packages/opencode/test/server/httpapi-session.test.ts
+++ b/packages/opencode/test/server/httpapi-session.test.ts
@@ -3,9 +3,13 @@ import { mkdir } from "node:fs/promises"
import path from "node:path"
import { Effect } from "effect"
import { Flag } from "@opencode-ai/core/flag/flag"
+import { registerAdaptor } from "../../src/control-plane/adaptors"
+import type { WorkspaceAdaptor } from "../../src/control-plane/types"
+import { Workspace } from "../../src/control-plane/workspace"
import { PermissionID } from "../../src/permission/schema"
import { ModelID, ProviderID } from "../../src/provider/schema"
import { Instance } from "../../src/project/instance"
+import { Project } from "../../src/project/project"
import { Server } from "../../src/server/server"
import { SessionPaths } from "../../src/server/routes/instance/httpapi/groups/session"
import { Session } from "@/session/session"
@@ -22,6 +26,7 @@ import { it } from "../lib/effect"
void Log.init({ print: false })
const original = Flag.OPENCODE_EXPERIMENTAL_HTTPAPI
+const originalWorkspaces = Flag.OPENCODE_EXPERIMENTAL_WORKSPACES
function app(experimental = true) {
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = experimental
@@ -77,6 +82,28 @@ function createTextMessage(directory: string, sessionID: SessionID, text: string
)
}
+const localAdaptor = (directory: string): WorkspaceAdaptor => ({
+ name: "Local Test",
+ description: "Create a local test workspace",
+ configure: (info) => ({ ...info, name: "local-test", directory }),
+ create: async () => {
+ await mkdir(directory, { recursive: true })
+ },
+ async remove() {},
+ target: () => ({ type: "local" as const, directory }),
+})
+
+const createLocalWorkspace = (input: { projectID: Project.Info["id"]; type: string; directory: string }) =>
+ Effect.promise(async () => {
+ registerAdaptor(input.projectID, input.type, localAdaptor(input.directory))
+ return Workspace.create({
+ type: input.type,
+ branch: null,
+ extra: null,
+ projectID: input.projectID,
+ })
+ })
+
function request(path: string, init?: RequestInit) {
return Effect.promise(async () => app().request(path, init))
}
@@ -108,6 +135,7 @@ function withTmp(
afterEach(async () => {
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original
+ Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = originalWorkspaces
await Instance.disposeAll()
await resetDatabase()
})
@@ -226,6 +254,40 @@ describe("session HttpApi", () => {
),
)
+ it.live(
+ "persists selected workspace id when creating a session",
+ withTmp({ git: true, config: { formatter: false, lsp: false, share: "disabled" } }, (tmp) =>
+ Effect.gen(function* () {
+ Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = true
+ const project = yield* Project.use.fromDirectory(tmp.path).pipe(Effect.provide(Project.defaultLayer))
+ const workspace = yield* createLocalWorkspace({
+ projectID: project.project.id,
+ type: "session-create-workspace",
+ directory: path.join(tmp.path, ".workspace-local"),
+ })
+
+ const created = yield* requestJson(`${SessionPaths.create}?workspace=${workspace.id}`, {
+ method: "POST",
+ headers: { "x-opencode-directory": tmp.path, "content-type": "application/json" },
+ body: JSON.stringify({ title: "workspace session" }),
+ })
+
+ expect(created).toMatchObject({ id: created.id, workspaceID: workspace.id })
+ expect(
+ yield* Effect.sync(() =>
+ Database.use((db) =>
+ db
+ .select({ workspaceID: SessionTable.workspace_id })
+ .from(SessionTable)
+ .where(eq(SessionTable.id, created.id))
+ .get(),
+ ),
+ ),
+ ).toEqual({ workspaceID: workspace.id })
+ }),
+ ),
+ )
+
it.live(
"matches legacy archived timestamp validation",
withTmp({ git: true, config: { formatter: false, lsp: false } }, (tmp) =>