{"_id":"@background-agents/sandbox-jobs","name":"@background-agents/sandbox-jobs","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@background-agents/sandbox-jobs","version":"0.1.0","description":"Run, observe, and reconnect to long-running shell processes in a Daytona sandbox via the filesystem. Byte-offset incremental reads, real exit codes, and cold-reconnect-by-id.","type":"module","main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./package.json":"./package.json"},"scripts":{"build":"tsup src/index.ts --format esm,cjs --dts --clean --sourcemap","clean":"rm -rf dist","typecheck":"tsc --noEmit","test":"vitest run","test:watch":"vitest","prepare":"npm run build"},"peerDependencies":{"@daytonaio/sdk":">=0.8.0"},"peerDependenciesMeta":{"@daytonaio/sdk":{"optional":true}},"devDependencies":{"@daytonaio/sdk":"^0.170.0","@types/node":"^20.10.0","dotenv":"^17.3.1","tsup":"^8.0.0","typescript":"^5.3.0","vitest":"^1.0.0"},"keywords":["daytona","sandbox","background","process","long-running"],"license":"MIT","publishConfig":{"access":"public"},"_id":"@background-agents/sandbox-jobs@0.1.0","gitHead":"803bd192a97990ac03f5d559b923f7633a853c96","_nodeVersion":"20.20.2","_npmVersion":"10.8.2","dist":{"integrity":"sha512-uxWsWwhPt/wCfbUXX+juAein454dUp5N6stg1aySqx+HEng0liw8Bs+eF2igMT3jwt9gy90O/PHOR2EIfyAkZw==","shasum":"5a04f6afe0b1247b85c374b7a59d33c10caedba4","tarball":"https://registry.npmjs.org/@background-agents/sandbox-jobs/-/sandbox-jobs-0.1.0.tgz","fileCount":8,"unpackedSize":72756,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQDX64xqCbbXIjdYu1WvmKYXy58HlCydY/2p/yhoyhphjwIhAOBNvXCIbz3mx4UNdA2oR+MFys5OJbaAOmGwZa59daJt"}]},"_npmUser":{"name":"jamesmurdza","email":"jamesmurdza@gmail.com"},"directories":{},"maintainers":[{"name":"jamesmurdza","email":"jamesmurdza@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/sandbox-jobs_0.1.0_1782598693710_0.8321408993795774"},"_hasShrinkwrap":false}},"time":{"created":"2026-06-27T22:18:13.592Z","0.1.0":"2026-06-27T22:18:13.877Z","modified":"2026-06-27T22:18:14.071Z"},"maintainers":[{"name":"jamesmurdza","email":"jamesmurdza@gmail.com"}],"description":"Run, observe, and reconnect to long-running shell processes in a Daytona sandbox via the filesystem. Byte-offset incremental reads, real exit codes, and cold-reconnect-by-id.","keywords":["daytona","sandbox","background","process","long-running"],"license":"MIT","readme":"# @background-agents/sandbox-jobs\n\nRun, observe, and reconnect to **long-running shell processes** in a Daytona\nsandbox — using the sandbox filesystem as the durable source of truth.\n\nThe problem it solves: a sandbox's `executeCommand` is request/response and\nshort-lived, but a real job (an agent run, a build, a test suite) can run for\nminutes. This package detaches the process inside the sandbox and represents the\nentire run as files, so a **cold caller** — a serverless function, a restarted\nserver, a different process — can reattach by id and read output incrementally\nwithout ever holding a connection open.\n\n## Model\n\nOne job = one process = one directory:\n\n```\n<root>/<jobId>/\n  meta.json     { jobId, pgid, processName, outputFile, exitFile, dir, createdAt, version }\n  output.log    combined stdout+stderr, byte-exact, append-only\n  exit          integer $?, present ONLY once the process finishes\n```\n\n- **Detached + fully reapable.** Launched with `setsid` and placed in its own\n  cgroup-v2. `cancel()` writes the cgroup's `cgroup.kill`, which SIGKILLs\n  *every* descendant — including a child that re-sessions itself with `setsid()`\n  (e.g. a daemonized MCP server) and so escapes the process group. A\n  process-group kill alone misses those and leaks them. Requires cgroup-v2 and\n  privilege to create a cgroup (the sandbox image grants this via `sudo`).\n- **Real exit codes.** The wrapper records the true `$?`; completion is never\n  guessed. A process killed before it could write `$?` (SIGKILL/OOM) is detected\n  as `crashed` via process-group liveness.\n- **Incremental, UTF-8-safe reads.** `read(handle, cursor)` returns only bytes\n  after the cursor, truncated to the last complete line — so the cursor never\n  splits a multi-byte character and you never re-read the whole log.\n- **Cold reconnect.** Everything needed to reattach is the serializable\n  `JobHandle` + an integer cursor, or just the job id via `attach()`.\n\n## Why not Daytona's session API (`executeSessionCommand`)?\n\nDaytona ships a native way to run a detached command: `createSession` +\n`executeSessionCommand({ runAsync: true })`, then `getSessionCommand` /\n`getSessionCommandLogs`. It looks like it should replace this package — the\ndaemon supervises the process and even returns a real exit code. We evaluated\nit directly; for the **cold-serverless-poller** use case (a function that starts\na job, dies, and reconnects later to stream output into a DB) the file approach\nwins on the things that actually bite:\n\n| | This package (files) | `executeSessionCommand` |\n|---|---|---|\n| **Incremental reads** | byte-offset `tail` → only new bytes, **O(n)** over a run | `getSessionCommandLogs` has **no offset param**: full-dump every poll (**O(n²)**), *or* a streaming callback that forces a held-open connection |\n| **Connectionless polling** | any cold caller reads the filesystem; nothing to keep alive | the streaming variant needs a live socket; the dump variant re-sends everything |\n| **Output fidelity** | `output.log` is **byte-exact**, so a byte cursor is reliable | the log stream is wrapped in control-byte framing (e.g. `\\x01` markers) — not byte-exact, which breaks offset cursors |\n| **Cancellation** | `cgroup.kill` reaps the whole job cgroup, incl. `setsid()` escapees | no documented kill for an async session command — you shell out to `pkill` anyway |\n| **Lifecycle to manage** | none — a dead process just leaves files; cleanup is `rm -rf <dir>` | a **session** outlives the command and must be torn down; deleting a live session reaps the process (a real footgun), and sessions accumulate |\n| **Isolation** | each job is its own process, dir, and cursor | a session is a **stateful shell** — env/cwd bleed across commands |\n| **Full-transcript retention** | the whole log until the disk fills | the daemon's log buffer may be capped (undocumented), which would break replay-from-zero |\n| **Backend surface** | only needs `executeCommand` — the most basic primitive | tied to the full session/command API |\n\nNote one thing it does **not** beat the session API on: exit codes.\n`getSessionCommand` returns a real `exitCode` too. The exit-code win here is over\nthe older `nohup` + `.done`-sentinel approach this package replaces, not over the\nsession API.\n\n**When the session API is the better choice:** when you *want* Daytona to own\nprocess supervision (server-side observability), or when you have a long-lived\nserver holding a socket and want live push rather than polling — e.g. an\ninteractive terminal/PTY. That's a different shape than \"reliably get every line\nand the exit code into a database from an intermittent caller,\" which is what\nthis package is for.\n\n## Usage\n\n```ts\nimport { createSandboxJobs } from \"@background-agents/sandbox-jobs\"\n\nconst jobs = createSandboxJobs(sandbox) // a @daytonaio/sdk Sandbox\n\nconst handle = await jobs.start({\n  command: `for i in $(seq 1 100); do echo \"tick $i\"; sleep 1; done`,\n  cwd: \"/home/daytona/project\",\n  env: { FOO: \"bar\" },\n  timeoutSeconds: 600, // optional hard limit (coreutils `timeout`)\n})\n\n// Poll incrementally (cold-start safe — rebuild `jobs`/`handle` each time):\nlet cursor = 0\nfor (;;) {\n  const r = await jobs.read(handle, cursor)\n  cursor = r.cursor\n  process.stdout.write(r.raw)\n  if (r.status.state !== \"running\") {\n    console.log(\"done\", r.status) // { state: \"exited\", exitCode: 0, alive: false }\n    break\n  }\n}\n\n// Or reattach later from just the id:\nconst reattached = await jobs.attach(handle.jobId)\n```\n\n## Tests\n\n```bash\nnpm run typecheck\nnpx vitest run tests/parse.test.ts        # pure unit tests, instant\nDAYTONA_API_KEY=... npx vitest run        # + integration (creates a sandbox)\n```\n","readmeFilename":"README.md","_rev":"1-a2ce0afa63b2ae9a0c96ce70033ed70f"}