Dashboard › opencode › Distillation
3233b0c6-5bc0-49f6-9b01-7f062cca179e["lore_tm_v1_eMMiNgGZVVYU18Zd-BocXQMeclRiOwaPECLFl1hQ2VE","lore_tm_v1_l3yTBHK-CP7E3aEnLPIWfmx2jebOUqb1eXQalbVzcyw","lore_tm_v1_CJfRUzHijGaouvCeiQwhjhYlFcBriJS0VpqS02xouC8","lore_tm_v1_UTyvx6R77zaB8zVoHm27It8J4FFBeW_X4_K3qHoV76A","lore_tm_v1_n26Pyf55WJlPDzLYWSC2vTFRo6DOwA7RO2YyXFO5A1w","lore_tm_v1_JtdZBvyRePoWQGlljQ-uKHBstGAhZ9n-RJGjH151MxA","lore_tm_v1_V5BRdy6FQJWohj67XU0pTj4_8MPnuvRr1o1-h7g3Q9w","lore_tm_v1_fWH3g_nt6BTX1pNaRbeKYfTysIcqIuD3unYS70OSn6g"]
π΄ (19:18) /home/byk/.local/share/opencode-v2-pilot/config/opencode/test/pty.test.ts is 401 lines, imports createPtyPlugin from ../plugins/pty.ts, uses bun:test, sets setDefaultTimeout(15_000), and runs registered cleanup callbacks via Promise.allSettled(...) after each test.
π΄ (19:18) pty.test.ts contains 13 ordered PTY tests: 1. registers ["pty_spawn", "pty_write", "pty_read", "pty_list", "pty_kill"] in that order; 2. bounds stalled startup using spawnTimeoutMs: 20, expecting "PTY process startup timed out." in under 250 ms; 3. rejects malformed Unicode before permission/execution; 4. verifies an interactive commandβs fixed environment and pinned cwd; 5. preserves argv without shell evaluation; 6. kills the process group and emits one JSON notification; 7. kills a PTY after a 1-second timeout; 8. enforces UTF-8 command, argument, and write limits; 9. avoids phantom lines for empty/newline-terminated output; 10. enforces Session ownership and canonical permission sources; 11. authorizes the exact decoded write without a saved grant; 12. removes active sessions when their parent Session is deleted; 13. reserves per-Session capacity before asynchronous spawning; 14. closes a PTY that finishes opening after Session deletion.
π΄ (19:18) The PTY stalled-startup test injects openProcess that sleeps 100 ms then throws "fixture startup remained pending" and configures spawnTimeoutMs: 20; pty_spawn for /bin/true must reject with "PTY process startup timed out.", and elapsed time must be below 250 ms.
π΄ (19:18) The malformed-Unicode PTY test uses command `/bin/echo\uD800` and expects "well-formed Unicode" before any permission request; it then spawns /bin/cat, writes "\\uD800", expects the same Unicode rejection, and confirms only the original spawn permission exists.
π΄ (19:18) The PTY environment test runs /bin/sh -c "printf ...; read value; ..." and verifies output contains /home/byk|C.UTF-8|/usr/bin:/bin|xterm-256color|${process.cwd()} and got:hello after writing "hello\\n".
π΄ (19:18) The PTY argv-boundary test passes ["space value", "quote'value", "$(printf injected)", "semi;colon", "Γ©"] through /usr/bin/python3 and checks they remain literal, including escaped JSON output "\\u00e9".
π΄ (19:18) The PTY kill-escalation test runs /bin/sh with trap '' TERM; while :; do sleep 1; done, sets notifyOnExit: true, expects pty_kill status "killed", verifies process.kill(-session.pid, 0) throws, and expects exactly one notification matching { type: "pty.exit", id: session.id, description: "kill escalation" }; cleanup must not send another notification.
π΄ (19:18) The PTY timeout test runs the TERM-ignoring infinite shell loop with timeoutSeconds: 1, waits up to 5,000 ms for matching pty_list state status === "killed" and timedOut === true, and verifies the process group no longer exists.
π΄ (19:18) PTY byte-limit tests use "Γ©".repeat(2_049) to exceed the 4,096-byte command limit, "Γ©".repeat(8_193) to exceed the 16,384-byte per-argument limit, and "Γ©".repeat(32_769) to exceed the 65,536-byte decoded-write limit.
π΄ (19:18) PTY line-accounting tests expect /bin/true to return lines: [], totalLines: 0, and hasMore: false; /usr/bin/printf with "one\\ntwo\\n" must return exactly 2 lines, ["one", "two"] after removing terminal \r, and totalLines: 2.
π΄ (19:18) PTY ownership tests use canonical tool context { sessionID: "top", agent: "build", messageID: "msg", id: "call" }; reading from sessionID: "other" must yield PTY session not found: ${id}. The spawn permission must contain action: "shell", save: [], and source { type: "tool", messageID: "msg", id: "call" }.
π΄ (19:18) The exact decoded-write authorization test expects the second permission to contain action: "shell", resource ["'/bin/cat' <stdin> 'hello\n'"], save: [], the canonical Session/agent values, and canonical tool source identity.
π΄ (19:18) The parent-deletion PTY test pushes { type: "session.deleted", data: { sessionID: "top" } }, waits up to 5,000 ms for pty_list to become empty, expects the deleted PTY to be hidden as not found, and expects subsequent spawning to fail with Session has been deleted: top.
π΄ (19:18) The PTY capacity-reservation test starts 5 blocked asynchronous spawns, confirms all 5 reached openProcess, and requires a sixth spawn to fail with "PTY session limit reached for this Session (5)." before releasing and completing the original 5.
π΄ (19:18) The PTY in-flight-deletion test blocks openProcess, emits session.deleted, then releases opening; the pending spawn must reject with Session has been deleted: top, the opened fake process must close, and pty_list must remain empty.
π΄ (19:18) pty.test.ts defines setup(...), waitForExit(...), waitFor(...), deferred<T>(), fakeProcess(), and EventQueue; waitFor(...) polls every 10 ms and defaults to a 1,000 ms timeout, while fakeProcess().close() is idempotent and resolves exit with { exitCode: 0 }.
π΄ (19:19) /home/byk/.local/share/opencode-v2-pilot/config/opencode/plugins/followup.ts is 158 lines and defines plugin ID "followup" with 3 tools registered through ctx.tool.transform: schedule_followup, list_followups, and cancel_followup; each uses { codemode: false }.
π΄ (19:19) The follow-up pluginβs exact subagent refusal is "Error: scheduled follow-ups are disabled in subagents. A subagent must finish its task and return a result synchronously.".
π΄ (19:19) schedule_followup accepts string fields delay and message; supported examples include 30s, 2m, and 1h, while a bare number means seconds. parseDelay(...) accepts nonnegative integer/decimal values with optional units ms, s, m, h, sec, secs, min, mins, hour, hours, second, seconds, minute, or minutes, and rounds the converted milliseconds.
π΄ (19:19) schedule_followup rejects unparseable delays with Error: could not parse delay "${input.delay}". Use forms like 30s, 2m, or 1h. and rejects delays below 1,000 ms with Error: delay too short (${milliseconds}ms). Use at least 1 second..
π΄ (19:19) Follow-up IDs are generated with Math.random().toString(36).slice(2, 10). Pending timers are isolated by Session in Map<string, Map<string, { timer; message; fireAt }>>; timers call unref?.().
π΄ (19:19) When a scheduled follow-up fires, it removes itself from the current Sessionβs pending map and calls ctx.session.prompt({ sessionID, text: \[scheduled follow-up] ${input.message}`, delivery: "steer" }); delivery errors are logged as [followup] failed to deliver ${id}. π΄ (19:19) schedule_followupreturnsFollow-up scheduled in ${input.delay} (id ${id}). Ending turn now; the session will be re-prompted with: "${input.message}". π΄ (19:19) list_followupsreturns"No pending follow-ups for this session."when empty; otherwise it reportsPending follow-ups (${sessionTimers.size}):and one line per timer as${id}: in about ${seconds}s: "${info.message}", where seconds are rounded and clamped to at least 0. π΄ (19:19) cancel_followupaccepts an ID or exact value"all"; "all"clears every current-Session timer and returnsCancelled ${count} follow-up(s)., a valid individual ID returns Cancelled follow-up "${input.id}"., an unknown ID returns No pending follow-up with id "${input.id}"., and an empty Session returns "No pending follow-ups to cancel.". π΄ (19:19) isSubagent(sessionID)callsctx.session.get({ sessionID }), treats any defined parentIDas a subagent, and caches the Boolean by Session ID. All 3 follow-up tools returnSUBAGENT_REFUSALfor subagents. π΄ (19:19) The follow-up plugin subscribes to events with anAbortController; session.deletedinvokesclearSession(...), which clears all timers and removes both pending and subagent-cache entries. Plugin cleanup aborts the subscription and clears every remaining Session. π΄ (19:19) Follow-up helper result(text)returns both{ output: text, content: text }`.
π΄ (19:19) /home/byk/.local/share/opencode-v2-pilot/config/opencode/test/followup.test.ts is 66 lines and has one integrated test named "schedules, lists, cancels, delivers, and rejects subagents".
π΄ (19:19) The follow-up test schedules { delay: "1s", message: "deliver" }, extracts an alphanumeric ID, verifies listing includes it, cancels it, and confirms the exact empty response "No pending follow-ups for this session.".
π΄ (19:19) The follow-up test models Session "subagent" with parentID: "parent" and confirms scheduling "blocked" returns text containing "disabled in subagents".
π΄ (19:19) The follow-up delivery test waits 1,100 ms and expects exactly [{ sessionID: "top", text: "[scheduled follow-up] deliver", delivery: "steer" }].
π΄ (19:20) /home/byk/.local/share/opencode-v2-pilot/config/opencode/opencode.json contains only "$schema": "https://opencode.ai/config.json".
π΄ (19:20) /home/byk/.local/share/opencode-v2-pilot/config/opencode/service-v2-pilot.json configures hostname 127.0.0.1 and port 14104; the file also contains a service password, whose value is omitted as a secret.
π΄ (19:20) /home/byk/.local/share/opencode-v2-pilot/opencode-v2.service defines OpenCode V2 Server as a simple systemd service for user byk, working directory /home/byk, starting after network.target, with Restart=always, RestartSec=5, Nice=-5, LimitNOFILE=65535, LimitNPROC=4096, MemoryMax=13G, and MemoryHigh=12G.
π΄ (19:20) opencode-v2.service loads /home/byk/.opencode/env and /home/byk/.local/share/opencode-v2-pilot/server.env; sets OPENCODE_CONFIG_DIR=/home/byk/.local/share/opencode-v2-pilot/config/opencode, OPENCODE_DB=/home/byk/.local/share/opencode-v2-pilot/data/opencode/opencode.db, XDG_DATA_HOME=/home/byk/.local/share/opencode-v2-pilot/data, XDG_CACHE_HOME=/home/byk/.local/share/opencode-v2-pilot/cache, XDG_CONFIG_HOME=/home/byk/.local/share/opencode-v2-pilot/config, and XDG_STATE_HOME=/home/byk/.local/share/opencode-v2-pilot/state.
π΄ (19:20) opencode-v2.service runs /home/byk/Code/opencode-v2-pilot/packages/cli/dist-v2-pilot-byk/cli-node-linux-x64/bin/opencode2-node serve --hostname=0.0.0.0 --port=4096.
π΄ (19:20) opencode-v2.service hardening uses NoNewPrivileges=true, ProtectSystem=strict, ProtectHome=read-only, ReadWritePaths=/home/byk, and PrivateTmp=yes; it is installable under multi-user.target.
π΄ (19:21) /home/byk/.local/share/opencode-v2-pilot/CUTOVER.md states the V2 executable serves the API and web UI from one origin and nginx must not be added.
π΄ (19:21) CUTOVER preflight is ordered: 1. confirm production health on port 4096; 2. confirm candidate revision with git -C /home/byk/Code/opencode-v2-pilot rev-parse HEAD and smoke-build version with /home/byk/Code/opencode-v2-pilot/packages/cli/dist-v2-pilot-byk/cli-node-linux-x64/bin/opencode2-node --version; 3. confirm the isolated candidate on port 14104 using authenticated curls to /api/health and /; 4. call POST /api/plugin/await-activation and confirm both local plugins are active; 5. create /home/byk/.local/share/opencode-v2-pilot/server.env with mode 0600 and one OPENCODE_PASSWORD=<password> line; 6. run the PTY smoke harness at least 10 times; 7. confirm the PTYβs unprivileged in-process trust model and exclusion of dormant supervisor/; 8. confirm process-group cleanup scenarios and acknowledge setsid()/daemon escape limits; 9. obtain fresh independent correctness and security approval for the exact source revision and candidate binary hash; 10. run systemd-analyze verify without changing production.
π΄ (19:21) Each PTY smoke-harness preflight run must confirm all 5 tools, canonical permissions and source identity, Session ownership, interaction, JSON notifications, timeouts, TERM-to-KILL escalation, deletion and in-flight deletion, atomic reservations, byte limits, line paging, fixed environment, pinned cwd, and cleanup.
π΄ (19:21) The PTY must run in-process as byk with bykβs account groups and introduce no root helper, supervisor service, readiness marker, or root-owned runtime dependency; dormant supervisor/ source is excluded from activation and cutover.
π΄ (19:21) Explicit kill, timeout, Session deletion, plugin unload, and candidate shutdown must stop the PTY process group. A process deliberately calling setsid() or daemonizing can escape process-group cleanup and is outside the chosen byk trust model.
π΄ (19:21) User directive: Never describe this PTY as cgroup-contained or as a general sandbox.
π΄ (19:21) Fresh independent correctness and security approval must cover the exact source revision and candidate binary hash.
π΄ (19:21) User directive: Never cut over while smoke checks or independent reviews are incomplete; cutover requires explicit approval because it stops the running service.
π΄ (19:21) CUTOVER execution order is: 1. install the candidate as /etc/systemd/system/opencode-v2.service; 2. run sudo systemctl daemon-reload; 3. run sudo systemctl stop opencode.service; 4. run sudo systemctl start opencode-v2.service; 5. wait for socket binding and verify /api/health, /, /site.webmanifest, /sw.js, and /openapi.json on port 4096; 6. call POST /api/plugin/await-activation and confirm followup and local-pty via GET /api/plugin; 7. observe real browser traffic and run one follow-up plus one PTY smoke Session; 8. enable opencode-v2.service only after every check passes, while retaining opencode.service installed and disabled for rollback.
π΄ (19:21) CUTOVER rollback order is: 1. run sudo systemctl stop opencode-v2.service; 2. run sudo systemctl start opencode.service; 3. verify the legacy health endpoint and browser UI on port 4096; 4. disable opencode-v2.service only after legacy service health is confirmed.
π΄ (19:21) The V2 database remains isolated under /home/byk/.local/share/opencode-v2-pilot/data.
π΄ (19:21) User directive: Rollback never rewrites or deletes either database.
π΄ (19:21) /home/byk/Code/opencode-v2-pilot/packages/core/src/plugin/host.ts exports PluginHost, and lines 1β520 show make(...) constructing a Plugin.Context from services including App.Metadata, Agent.Service, AISDK.Service, Catalog.Service, Command.Service, Bus.Service, Integration.Service, KV.Service, Mcp.Service, Location.Service, Reference.Service, Rpc.Service, Skill.Service, Tool.Service, Vcs.Service, WebSearch.Service, Generate.Service, Permission.Service, PluginHooks.Service, Session.Service, PersistentPty.Service, LocationServiceMap.Service, and Worktree.Service.
π΄ (19:21) PluginHost.make defaults pluginID to "test", defines RpcEvent as events with type rpc.${string}, and uses isRpcEvent(event) via event.type.startsWith("rpc.").
π΄ (19:21) locationRef(...) maps optional plugin location input to Location.Ref, defaulting the directory and workspace to the current Location; isCurrentLocation(...) compares both directory and workspaceID.
π΄ (19:21) atWorktree(...) rejects refs with a workspaceID using Worktree.UnsupportedLocationError; current/no-location calls run directly, while a different worktree dynamically imports ../plugin.js, obtains Plugin.Service and Worktree.Service, awaits plugins.awaitActivation, and runs under locations.get(ref).
π΄ (19:21) Same-location plugin setup calls never wait on themselves.
π΄ (19:21) PluginHost event subscription forwards only EventManifest.ServerEvent and RpcEvent values from bus.subscribe().
π΄ (19:21) PluginHost exposes experimental.terminal.read as persistentPty.read(input.sessionID, input.lines).
π΄ (19:21) PluginHost wraps many service responses as { location: locationInfo(), data } and exposes context namespaces including agent, aisdk, catalog, command, event, experimental.terminal, generate, integration, mcp, permission, plugin, reference, skill, storage, shell, tool, vcs, websearch, worktree, and session.
π΄ (19:21) Cross-location agent.list and agent.get use LocationServiceMap; a missing requested agent fails with Agent not found: ${input.agentID}.
π΄ (19:21) aisdk.hook("sdk", ...) can replace event.sdk; the alternate language hook can replace event.language. Both optionally filter by exact providerID.
π΄ (19:21) Permission context methods include hook, assert, list, get, and reply; get and reply verify that the permission request belongs to the supplied Session and otherwise fail with Permission request not found: ${input.requestID}.
π΄ (19:21) PluginHost exposes shell hooks through hooks.register("shell", name, callback) and tool transforms/reloads/hooks through tools.transform, tools.reload, and hooks.register("tool", name, callback).
π΄ (19:21) PluginHost VCS methods are get, base, branches, status, diff, transform, and reload; branches accepts search and limit, while diff passes mode, context, and base.
π΄ (19:21) PluginHost worktree methods are list, create, refresh, remove, reload, and transform; transformed strategy IDs use Worktree.StrategyID.make(...), and returned worktree values are schema-decoded through Worktree.Info or Schema.Array(Worktree.ListEntry).
π΄ (19:21) The visible Session context methods in host.ts lines 505β520 are hook, create, get, switchAgent, switchModel, prompt, and generate; create defaults location to the current directory/workspace, and generate maps output to { text }.