1313// pipe stdout+stderr back. BU_NAME is namespaced by sessionID so parallel
1414// sub-agents (each with their own session) get isolated daemons + browsers.
1515//
16+ // BH_TMP_DIR points at a per-session scratch dir so sock/port/pid/log + screenshot
17+ // output land somewhere predictable per session, instead of all sessions sharing
18+ // /tmp. The Level-2 wrapper supplies the cache root; we own the layout convention.
19+ //
1620// Level 1 per decisions.md §1c — substantial implementation lives here. The
1721// Level-2 hook in packages/opencode is a one-line wrapper.
1822
23+ import fs from "fs/promises"
24+ import path from "path"
1925import { Effect , Stream } from "effect"
2026import { ChildProcess , ChildProcessSpawner } from "effect/unstable/process"
2127import z from "zod"
2228import { resolveHarnessDir } from "./harness"
2329import { uvLocate } from "./uv-locate"
2430
31+ // Canonical per-session scratch dir layout. Caller supplies dataDir
32+ // (e.g. opencode's Global.Path.data); we own the `sessions/<id>` shape.
33+ // AF_UNIX sun_path is 104 bytes on macOS — `<dataDir>/sessions/<sessionID>/bu-<sessionID>.sock`
34+ // must fit. SessionID is `ses_` + 26 chars (30 chars). The literal suffix is
35+ // `/sessions/` (10) + 30 + `/bu-` (4) + 30 + `.sock` (5) = 79 chars, leaving
36+ // 25 chars of headroom for dataDir. Typical XDG dataDir is well under that.
37+ export const sessionScratchDir = ( dataDir : string , sessionID : string ) =>
38+ path . join ( dataDir , "sessions" , sessionID )
39+
2540const DEFAULT_TIMEOUT_MS = 60 * 1000
2641const MAX_TIMEOUT_MS = 10 * 60 * 1000
2742
@@ -37,6 +52,11 @@ export type Parameters = z.infer<typeof parameters>
3752
3853export interface ExecuteContext {
3954 readonly sessionID : string
55+ // Per-session scratch dir, passed to the harness as BH_TMP_DIR. The harness
56+ // mkdirs it on import, but we mkdir-p here too so failures surface as a
57+ // direct effect error rather than a child-process exit. Pre-compute via
58+ // sessionScratchDir(dataDir, sessionID).
59+ readonly bhTmpDir : string
4060 // Optional progress callback invoked per output chunk (combined stdout+stderr).
4161 // Level-2 supplies this to drive TUI streaming via opencode's `ctx.metadata`.
4262 // The callback receives the fully accumulated output so far, not just the
@@ -72,14 +92,15 @@ export const make = Effect.fn("BrowserExecute.make")(function* () {
7292 const execute = ( args : Parameters , ctx : ExecuteContext ) =>
7393 Effect . gen ( function * ( ) {
7494 const harnessDir = yield * Effect . promise ( ( ) => resolveHarnessDir ( ) )
95+ yield * Effect . promise ( ( ) => fs . mkdir ( ctx . bhTmpDir , { recursive : true } ) )
7596 const uv = yield * locate
7697 const proc = ChildProcess . make (
7798 uv ,
7899 [ "run" , "--project" , harnessDir , "browser-harness" , "-c" , args . python ] ,
79100 {
80101 cwd : harnessDir ,
81102 extendEnv : true ,
82- env : { BU_NAME : ctx . sessionID } ,
103+ env : { BU_NAME : ctx . sessionID , BH_TMP_DIR : ctx . bhTmpDir } ,
83104 stdin : "ignore" ,
84105 } ,
85106 )
0 commit comments