sessionpipe 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/hook.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/hook.ts", "../src/control/local.ts"],
4
- "sourcesContent": ["#!/usr/bin/env node\n// SPDX-License-Identifier: Apache-2.0\n// The hook path. A harness runs `node hook.js <harness> <event>`; this reads stdin,\n// writes one job file, spawns the worker detached, prints what the harness needs\n// (Antigravity's answer) and exits 0 \u2014 in milliseconds, with nothing but node:\n// builtins imported (the build checks that). No network, no schema library, no\n// config parsing beyond one small file read for the throttle.\nimport { spawn } from \"node:child_process\";\nimport { createHash } from \"node:crypto\";\nimport { appendFileSync, existsSync, mkdirSync, readFileSync, realpathSync, statSync, writeFileSync } from \"node:fs\";\nimport os from \"node:os\";\nimport path from \"node:path\";\nimport { fileURLToPath, pathToFileURL } from \"node:url\";\nimport { ask, socketPath } from \"./control/local.js\";\n\n/** A Claude Code session id (the same test as core's notClaudeCode). */\nconst UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\n/** The two streams the hook touches: stdin once, whole; stdout for an answer. */\nexport interface HookIO {\n stdin(): Buffer;\n stdout(text: string): void;\n}\n\n/** One hook run: `argv` is [harness, event, ...] as the harness invoked it, `env` the\n * process environment. Resolves once the job is on its way and any answer printed;\n * the process then exits 0, whatever happened (the hook is invisible to the harness). */\nexport async function hookMain(argv: string[], env: NodeJS.ProcessEnv, io: HookIO): Promise<void> {\n const t0 = process.hrtime.bigint();\n /** The hook's own work, when a control wait follows it (the wait is not the hook's cost). */\n let workMs: number | null = null;\n const [harness = \"\", event = \"\", ...rest] = argv;\n\n function stateDir(): string {\n if (env.SESSIONPIPE_STATE) return path.resolve(env.SESSIONPIPE_STATE);\n const home = os.homedir();\n if (process.platform === \"win32\")\n return path.join(env.LOCALAPPDATA || path.join(home, \"AppData\", \"Local\"), \"sessionpipe\");\n return path.join(env.XDG_STATE_HOME || path.join(home, \".local\", \"state\"), \"sessionpipe\");\n }\n function configFile(): string {\n if (env.SESSIONPIPE_CONFIG) return path.resolve(env.SESSIONPIPE_CONFIG);\n const home = os.homedir();\n if (process.platform === \"win32\")\n return path.join(env.APPDATA || path.join(home, \"AppData\", \"Roaming\"), \"sessionpipe\", \"config.json\");\n return path.join(env.XDG_CONFIG_HOME || path.join(home, \".config\"), \"sessionpipe\", \"config.json\");\n }\n\n // Antigravity parses stdout as the hook's answer; `{}` is \"carry on\" for every event we\n // register. PreToolUse is not one of them: `{}` there is a deny, so an entry an older\n // install left behind asks the person instead (core's AG_STALE_PRETOOL, same bytes).\n if (harness === \"antigravity\") {\n try {\n io.stdout(\n event === \"PreToolUse\"\n ? '{\"decision\":\"ask\",\"reason\":\"sessionpipe: an outdated PreToolUse hook is still loaded. Restart this conversation (or run sessionpipe install) to drop it.\"}\\n'\n : \"{}\\n\",\n );\n } catch {}\n }\n\n const state = stateDir();\n try {\n if (!harness || !event) throw new Error(\"usage: hook <harness> <event>\");\n let stdin = \"\";\n if (harness !== \"codex\" || event !== \"notify\") {\n try {\n const buf = io.stdin();\n stdin = buf.subarray(0, 1024 * 1024).toString(\"utf8\");\n } catch {}\n }\n // Tool events are the bulk of what a harness fires. When no sink takes tier \u2265 1\n // they are only a \"still working\" signal: one heartbeat a minute per session.\n const TOOL = /^(PreToolUse|PostToolUse|PostToolUseFailure|BeforeTool|AfterTool|PostInvocation)$/;\n if (TOOL.test(event) && !anySinkAbove(0)) {\n const id = sessionIdOf(stdin);\n if (!id || !beatDue(id)) return finish(false);\n }\n // A Stop asks the daemon BEFORE its event goes to the worker: the worker reports the\n // Stop to the daemon too, and a message queued for this Stop must be taken here,\n // not rerouted because the event got there first.\n const stopAnswer = event === \"Stop\" ? await (controlAsk(stdin) ?? Promise.resolve(null)) : null;\n mkdirSync(path.join(state, \"jobs\"), { recursive: true, mode: 0o700 });\n const job = {\n harness,\n event,\n argv: [harness, event, ...rest],\n stdin,\n cwd: process.cwd(),\n env: pick([\n // Where a harness keeps its config and its sessions, so a job swept by a worker\n // some other harness's hook spawned still resolves this harness's own homes\n // (run.ts sweepJobs runs any stale job file, whatever spawned the worker).\n \"APPDATA\",\n \"CLAUDE_CODE_HOST_SESSION_ID\",\n \"CLAUDE_CONFIG_DIR\",\n \"CLAUDE_PROJECT_DIR\",\n \"CODEX_HOME\",\n // Devin's payload names no folder at all; this is the only thing that does.\n \"DEVIN_PROJECT_DIR\",\n \"GEMINI_CLI_HOME\",\n // Devin's config file and session store are both resolved through these.\n \"XDG_CONFIG_HOME\",\n \"XDG_DATA_HOME\",\n ]),\n ppid: process.ppid,\n at: Date.now(),\n };\n const jobFile = path.join(\n state,\n \"jobs\",\n `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}.json`,\n );\n writeFileSync(jobFile, JSON.stringify(job), { mode: 0o600 });\n if (!env.SESSIONPIPE_NO_WORKER) {\n const worker = path.join(path.dirname(fileURLToPath(import.meta.url)), \"worker.js\");\n const p = spawn(process.execPath, [worker, jobFile], { detached: true, stdio: \"ignore\", windowsHide: true, env });\n p.unref();\n }\n if (stopAnswer) {\n try {\n io.stdout(`${stopAnswer}\\n`);\n } catch {}\n }\n // A PermissionRequest waits AFTER its event is on its way, so the receiver already\n // shows the Allow / Deny this answer is for.\n const ctl = event === \"PermissionRequest\" ? controlAsk(stdin) : null;\n if (ctl) {\n workMs = Number(process.hrtime.bigint() - t0) / 1e6;\n const out = await ctl;\n if (out) {\n try {\n io.stdout(`${out}\\n`);\n } catch {}\n }\n }\n return finish(true);\n } catch {\n return finish(false); // the hook must be invisible to the harness\n }\n\n /** Control (spec/CONTROL.md \u00A76, \u00A79): only on a machine with control paired, and only\n * for a message the daemon itself verified. A Stop hook asks for a queued message and\n * answers a block with it; a PermissionRequest hook waits for the person's signed\n * answer while the terminal shows its own prompt, first answer wins. Anything that\n * goes wrong (no daemon, a timeout) prints nothing: the harness carries on as usual. */\n function controlAsk(stdin: string): Promise<string | null> | null {\n if (harness !== \"claude-code\" || (event !== \"Stop\" && event !== \"PermissionRequest\")) return null;\n if (env.SESSIONPIPE_CONTROL_JOB) return null; // a headless turn the daemon itself runs\n const sock = socketPath(state, env);\n if (\n process.platform === \"win32\"\n ? !existsSync(path.join(path.dirname(configFile()), \"control.json\"))\n : !existsSync(sock)\n )\n return null;\n let s: Record<string, unknown>;\n try {\n s = JSON.parse(stdin) as Record<string, unknown>;\n } catch {\n return null;\n }\n const id = typeof s.session_id === \"string\" ? s.session_id : \"\";\n if (!id) return null;\n // Our entry in ~/.claude/settings.json is run by every harness that imports that file\n // \u2014 Devin's `claude` hooks provider is live, not a stub \u2014 and it arrives under the\n // claude-code name. Every Claude Code payload names a transcript and a UUID session;\n // a payload with neither is somebody else's, and must not be parked for up to 125 s\n // waiting on an answer that is not coming. Same test as core's notClaudeCode\n // (claude-code.ts), which the worker applies; repeated here because this file imports\n // nothing but node builtins.\n if (typeof s.transcript_path !== \"string\" && !UUID.test(id)) return null;\n const session = `claude-code:${id}`;\n if (event === \"Stop\")\n return ask(sock, { op: \"stop\", session, active: s.stop_hook_active === true }, 300).then((r) =>\n r?.op === \"block\" ? JSON.stringify({ decision: \"block\", reason: r.reason }) : null,\n );\n // The attention id the adapter puts on attention.needed (claude-code.ts), so the\n // receiver's Allow / Deny names this very prompt.\n const tool =\n typeof s.tool_name === \"string\" && /^[A-Za-z0-9_.:-]{1,120}$/.test(s.tool_name) ? s.tool_name : undefined;\n const turn = typeof s.prompt_id === \"string\" ? s.prompt_id : undefined;\n const attention = `perm-${createHash(\"sha256\")\n .update(`${turn ?? \"\"}|${tool ?? \"\"}|${JSON.stringify(s.tool_input ?? null)}`)\n .digest(\"hex\")\n .slice(0, 16)}`;\n return ask(sock, { op: \"permission\", session, attention, ...(tool ? { tool } : {}) }, 125_000).then((r) =>\n r?.op === \"decision\"\n ? JSON.stringify({\n hookSpecificOutput: {\n hookEventName: \"PermissionRequest\",\n decision:\n r.behavior === \"allow\"\n ? { behavior: \"allow\" }\n : { behavior: \"deny\", message: r.message || \"Denied by the owner (sessionpipe control).\" },\n },\n })\n : null,\n );\n }\n\n /** The timing line; the process exits 0 right after. */\n function finish(spawned: boolean): void {\n const total = Number(process.hrtime.bigint() - t0) / 1e6;\n const ms = workMs ?? total;\n try {\n mkdirSync(state, { recursive: true });\n appendFileSync(\n path.join(state, \"timing.jsonl\"),\n `${JSON.stringify({ at: Date.now(), harness, event, ms: Math.round(ms * 10) / 10, spawned, ...(workMs !== null ? { control_wait_ms: Math.round(total - workMs) } : {}) })}\\n`,\n );\n } catch {}\n }\n function pick(names: string[]): Record<string, string> {\n const out: Record<string, string> = {};\n for (const n of names) if (env[n] !== undefined) out[n] = env[n] as string;\n return out;\n }\n function anySinkAbove(tier: number): boolean {\n try {\n const c = JSON.parse(readFileSync(configFile(), \"utf8\")) as { sinks?: { tier?: number; paused?: string }[] };\n return (c.sinks ?? []).some((s) => Number(s.tier) > tier && !s.paused);\n } catch {\n return false;\n }\n }\n function sessionIdOf(stdin: string): string {\n const m = /\"(?:session_id|sessionId|conversationId)\"\\s*:\\s*\"([^\"]{1,128})\"/.exec(stdin);\n return m?.[1] ?? \"\";\n }\n /** One heartbeat per minute per session: the stamp file's mtime is the clock. */\n function beatDue(id: string): boolean {\n const stamp = path.join(state, \"beats\", `${harness}-${id.replace(/[^A-Za-z0-9_-]/g, \"_\")}`);\n try {\n if (Date.now() - statSync(stamp).mtimeMs < 60_000) return false;\n } catch {}\n try {\n mkdirSync(path.dirname(stamp), { recursive: true, mode: 0o700 });\n writeFileSync(stamp, \"\", { mode: 0o600 });\n } catch {}\n return true;\n }\n}\n\n// Run as `node hook.js <harness> <event>`; importing this module runs nothing. The main\n// module is known by its real path: node resolves symlinks for import.meta.url, argv[1]\n// keeps the spelling it was given (/var \u2192 /private/var on macOS).\nif (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href)\n hookMain(process.argv.slice(2), process.env, {\n // fd 0 and 1 are the test runner's own (a read of 0 would block it); launcher.test.ts\n // and the bench run these two through the real binary.\n /* v8 ignore next -- see above */\n stdin: () => readFileSync(0),\n /* v8 ignore next -- see above */\n stdout: (text) => writeFileSync(1, text),\n }).then(() => process.exit(0));\n", "// SPDX-License-Identifier: Apache-2.0\n// The daemon's local socket (spec/CONTROL.md \u00A78): how the waiter, the Stop and\n// PermissionRequest hooks and the worker talk to the one daemon on this machine.\n// Line-delimited JSON, one request per connection. A Unix socket in the state\n// directory (0600, in a 0700 dir); a per-user named pipe on Windows. Only node:\n// builtins \u2014 the hook bundle imports this.\nimport net from \"node:net\";\nimport os from \"node:os\";\nimport path from \"node:path\";\n\nexport type LocalRequest =\n /** A waiter parks until a message for its session arrives. */\n | { op: \"wait\"; session: string; pid?: number }\n /** A Stop hook asks for a queued message (turn mode). */\n | { op: \"stop\"; session: string; active: boolean }\n /** A PermissionRequest hook parks until a verified answer, or until it's released. */\n | { op: \"permission\"; session: string; attention: string; tool?: string }\n /** The worker reports a hook event: turn state and attention closure. */\n | { op: \"event\"; session: string; event: string; transcript?: string; cwd?: string }\n /** `sessionpipe control status`. */\n | { op: \"status\" }\n /** `sessionpipe update` installed a new copy: restart onto it once nothing is in hand. */\n | { op: \"restart\" };\n\nexport type LocalReply =\n | { op: \"message\"; text: string; id: string }\n | { op: \"block\"; reason: string; id: string }\n | { op: \"decision\"; behavior: \"allow\" | \"deny\"; message?: string; id: string }\n | { op: \"none\" }\n /** A newer waiter for the same session took over; this one stops. */\n | { op: \"replaced\" }\n | { op: \"ok\" }\n | { op: \"status\"; status: unknown };\n\n/** Where the daemon listens. `SESSIONPIPE_SOCKET` overrides (tests). */\nexport function socketPath(state: string, env: NodeJS.ProcessEnv = process.env): string {\n if (env.SESSIONPIPE_SOCKET) return env.SESSIONPIPE_SOCKET;\n if (process.platform === \"win32\") return `\\\\\\\\.\\\\pipe\\\\sessionpipe-control-${os.userInfo().username}`;\n return path.join(state, \"control.sock\");\n}\n\n/** One request, one reply. `timeoutMs` bounds the whole exchange; null on any\n * failure (no daemon, a timeout, a garbled reply) \u2014 callers treat that as \"none\". */\nexport function ask(sock: string, req: LocalRequest, timeoutMs: number): Promise<LocalReply | null> {\n return new Promise((resolve) => {\n let buf = \"\";\n let done = false;\n const s = net.connect(sock);\n const finish = (r: LocalReply | null) => {\n if (done) return;\n done = true;\n clearTimeout(t);\n s.destroy();\n resolve(r);\n };\n const t = setTimeout(() => finish(null), timeoutMs);\n s.on(\"connect\", () => s.write(`${JSON.stringify(req)}\\n`));\n s.on(\"data\", (d) => {\n buf += d.toString(\"utf8\");\n const i = buf.indexOf(\"\\n\");\n if (i < 0) return;\n try {\n finish(JSON.parse(buf.slice(0, i)) as LocalReply);\n } catch {\n finish(null);\n }\n });\n s.on(\"error\", () => finish(null));\n s.on(\"close\", () => finish(null));\n });\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["#!/usr/bin/env node\n// SPDX-License-Identifier: Apache-2.0\n// The hook path. A harness runs `node hook.js <harness> <event>`; this reads stdin,\n// writes one job file, spawns the worker detached, prints what the harness needs\n// (Antigravity's answer) and exits 0 \u2014 in milliseconds, with nothing but node:\n// builtins imported (the build checks that). No network, no schema library, no\n// config parsing beyond one small file read for the throttle.\nimport { spawn } from \"node:child_process\";\nimport { createHash } from \"node:crypto\";\nimport { appendFileSync, existsSync, mkdirSync, readFileSync, realpathSync, statSync, writeFileSync } from \"node:fs\";\nimport os from \"node:os\";\nimport path from \"node:path\";\nimport { fileURLToPath, pathToFileURL } from \"node:url\";\nimport { ask, socketPath } from \"./control/local.js\";\n\n/** A Claude Code session id (the same test as core's notClaudeCode). */\nconst UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\n/** The two streams the hook touches: stdin once, whole; stdout for an answer. */\nexport interface HookIO {\n stdin(): Buffer;\n stdout(text: string): void;\n}\n\n/** One hook run: `argv` is [harness, event, ...] as the harness invoked it, `env` the\n * process environment. Resolves once the job is on its way and any answer printed;\n * the process then exits 0, whatever happened (the hook is invisible to the harness). */\nexport async function hookMain(argv: string[], env: NodeJS.ProcessEnv, io: HookIO): Promise<void> {\n const t0 = process.hrtime.bigint();\n /** The hook's own work, when a control wait follows it (the wait is not the hook's cost). */\n let workMs: number | null = null;\n const [harness = \"\", event = \"\", ...rest] = argv;\n\n function stateDir(): string {\n if (env.SESSIONPIPE_STATE) return path.resolve(env.SESSIONPIPE_STATE);\n const home = os.homedir();\n if (process.platform === \"win32\")\n return path.join(env.LOCALAPPDATA || path.join(home, \"AppData\", \"Local\"), \"sessionpipe\");\n return path.join(env.XDG_STATE_HOME || path.join(home, \".local\", \"state\"), \"sessionpipe\");\n }\n function configFile(): string {\n if (env.SESSIONPIPE_CONFIG) return path.resolve(env.SESSIONPIPE_CONFIG);\n const home = os.homedir();\n if (process.platform === \"win32\")\n return path.join(env.APPDATA || path.join(home, \"AppData\", \"Roaming\"), \"sessionpipe\", \"config.json\");\n return path.join(env.XDG_CONFIG_HOME || path.join(home, \".config\"), \"sessionpipe\", \"config.json\");\n }\n\n // Antigravity parses stdout as the hook's answer; `{}` is \"carry on\" for every event we\n // register. PreToolUse is not one of them: `{}` there is a deny, so an entry an older\n // install left behind asks the person instead (core's AG_STALE_PRETOOL, same bytes).\n if (harness === \"antigravity\") {\n try {\n io.stdout(\n event === \"PreToolUse\"\n ? '{\"decision\":\"ask\",\"reason\":\"sessionpipe: an outdated PreToolUse hook is still loaded. Restart this conversation (or run sessionpipe install) to drop it.\"}\\n'\n : \"{}\\n\",\n );\n } catch {}\n }\n\n const state = stateDir();\n try {\n if (!harness || !event) throw new Error(\"usage: hook <harness> <event>\");\n let stdin = \"\";\n if (harness !== \"codex\" || event !== \"notify\") {\n try {\n const buf = io.stdin();\n stdin = buf.subarray(0, 1024 * 1024).toString(\"utf8\");\n } catch {}\n }\n // Tool events are the bulk of what a harness fires. When no sink takes tier \u2265 1\n // they are only a \"still working\" signal: one heartbeat a minute per session.\n const TOOL = /^(PreToolUse|PostToolUse|PostToolUseFailure|BeforeTool|AfterTool|PostInvocation)$/;\n if (TOOL.test(event) && !anySinkAbove(0)) {\n const id = sessionIdOf(stdin);\n if (!id || !beatDue(id)) return finish(false);\n }\n // A Stop asks the daemon BEFORE its event goes to the worker: the worker reports the\n // Stop to the daemon too, and a message queued for this Stop must be taken here,\n // not rerouted because the event got there first.\n const stopAnswer = event === \"Stop\" ? await (controlAsk(stdin) ?? Promise.resolve(null)) : null;\n mkdirSync(path.join(state, \"jobs\"), { recursive: true, mode: 0o700 });\n const job = {\n harness,\n event,\n argv: [harness, event, ...rest],\n stdin,\n cwd: process.cwd(),\n env: pick([\n // Where a harness keeps its config and its sessions, so a job swept by a worker\n // some other harness's hook spawned still resolves this harness's own homes\n // (run.ts sweepJobs runs any stale job file, whatever spawned the worker).\n \"APPDATA\",\n \"CLAUDE_CODE_HOST_SESSION_ID\",\n // session.attended: Claude Code's own word, and SESSIONPIPE_CONTROL_JOB below, the\n // control daemon's mark on a turn it runs for a person's message.\n \"CLAUDE_CODE_SESSION_ATTENDED\",\n \"CLAUDE_CONFIG_DIR\",\n \"CLAUDE_PROJECT_DIR\",\n \"CODEX_HOME\",\n // Devin's payload names no folder at all; this is the only thing that does.\n \"DEVIN_PROJECT_DIR\",\n \"GEMINI_CLI_HOME\",\n \"SESSIONPIPE_CONTROL_JOB\",\n // The launcher a script named for a session it starts (PROTOCOL.md \u00A73 launched_by).\n \"SESSIONPIPE_LAUNCHED_BY\",\n // Devin's config file and session store are both resolved through these.\n \"XDG_CONFIG_HOME\",\n \"XDG_DATA_HOME\",\n ]),\n ppid: process.ppid,\n at: Date.now(),\n };\n const jobFile = path.join(\n state,\n \"jobs\",\n `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}.json`,\n );\n writeFileSync(jobFile, JSON.stringify(job), { mode: 0o600 });\n if (!env.SESSIONPIPE_NO_WORKER) {\n const worker = path.join(path.dirname(fileURLToPath(import.meta.url)), \"worker.js\");\n const p = spawn(process.execPath, [worker, jobFile], { detached: true, stdio: \"ignore\", windowsHide: true, env });\n p.unref();\n }\n if (stopAnswer) {\n try {\n io.stdout(`${stopAnswer}\\n`);\n } catch {}\n }\n // A PermissionRequest waits AFTER its event is on its way, so the receiver already\n // shows the Allow / Deny this answer is for.\n const ctl = event === \"PermissionRequest\" ? controlAsk(stdin) : null;\n if (ctl) {\n workMs = Number(process.hrtime.bigint() - t0) / 1e6;\n const out = await ctl;\n if (out) {\n try {\n io.stdout(`${out}\\n`);\n } catch {}\n }\n }\n return finish(true);\n } catch {\n return finish(false); // the hook must be invisible to the harness\n }\n\n /** Control (spec/CONTROL.md \u00A76, \u00A79): only on a machine with control paired, and only\n * for a message the daemon itself verified. A Stop hook asks for a queued message and\n * answers a block with it; a PermissionRequest hook waits for the person's signed\n * answer while the terminal shows its own prompt, first answer wins. Anything that\n * goes wrong (no daemon, a timeout) prints nothing: the harness carries on as usual. */\n function controlAsk(stdin: string): Promise<string | null> | null {\n if (harness !== \"claude-code\" || (event !== \"Stop\" && event !== \"PermissionRequest\")) return null;\n if (env.SESSIONPIPE_CONTROL_JOB) return null; // a headless turn the daemon itself runs\n const sock = socketPath(state, env);\n if (\n process.platform === \"win32\"\n ? !existsSync(path.join(path.dirname(configFile()), \"control.json\"))\n : !existsSync(sock)\n )\n return null;\n let s: Record<string, unknown>;\n try {\n s = JSON.parse(stdin) as Record<string, unknown>;\n } catch {\n return null;\n }\n const id = typeof s.session_id === \"string\" ? s.session_id : \"\";\n if (!id) return null;\n // Our entry in ~/.claude/settings.json is run by every harness that imports that file\n // \u2014 Devin's `claude` hooks provider is live, not a stub \u2014 and it arrives under the\n // claude-code name. Every Claude Code payload names a transcript and a UUID session;\n // a payload with neither is somebody else's, and must not be parked for up to 125 s\n // waiting on an answer that is not coming. Same test as core's notClaudeCode\n // (claude-code.ts), which the worker applies; repeated here because this file imports\n // nothing but node builtins.\n if (typeof s.transcript_path !== \"string\" && !UUID.test(id)) return null;\n const session = `claude-code:${id}`;\n if (event === \"Stop\")\n return ask(sock, { op: \"stop\", session, active: s.stop_hook_active === true }, 300).then((r) =>\n r?.op === \"block\" ? JSON.stringify({ decision: \"block\", reason: r.reason }) : null,\n );\n // The attention id the adapter puts on attention.needed (claude-code.ts), so the\n // receiver's Allow / Deny names this very prompt.\n const tool =\n typeof s.tool_name === \"string\" && /^[A-Za-z0-9_.:-]{1,120}$/.test(s.tool_name) ? s.tool_name : undefined;\n const turn = typeof s.prompt_id === \"string\" ? s.prompt_id : undefined;\n const attention = `perm-${createHash(\"sha256\")\n .update(`${turn ?? \"\"}|${tool ?? \"\"}|${JSON.stringify(s.tool_input ?? null)}`)\n .digest(\"hex\")\n .slice(0, 16)}`;\n return ask(sock, { op: \"permission\", session, attention, ...(tool ? { tool } : {}) }, 125_000).then((r) =>\n r?.op === \"decision\"\n ? JSON.stringify({\n hookSpecificOutput: {\n hookEventName: \"PermissionRequest\",\n decision:\n r.behavior === \"allow\"\n ? { behavior: \"allow\" }\n : { behavior: \"deny\", message: r.message || \"Denied by the owner (sessionpipe control).\" },\n },\n })\n : null,\n );\n }\n\n /** The timing line; the process exits 0 right after. */\n function finish(spawned: boolean): void {\n const total = Number(process.hrtime.bigint() - t0) / 1e6;\n const ms = workMs ?? total;\n try {\n mkdirSync(state, { recursive: true });\n appendFileSync(\n path.join(state, \"timing.jsonl\"),\n `${JSON.stringify({ at: Date.now(), harness, event, ms: Math.round(ms * 10) / 10, spawned, ...(workMs !== null ? { control_wait_ms: Math.round(total - workMs) } : {}) })}\\n`,\n );\n } catch {}\n }\n function pick(names: string[]): Record<string, string> {\n const out: Record<string, string> = {};\n for (const n of names) if (env[n] !== undefined) out[n] = env[n] as string;\n return out;\n }\n function anySinkAbove(tier: number): boolean {\n try {\n const c = JSON.parse(readFileSync(configFile(), \"utf8\")) as { sinks?: { tier?: number; paused?: string }[] };\n return (c.sinks ?? []).some((s) => Number(s.tier) > tier && !s.paused);\n } catch {\n return false;\n }\n }\n function sessionIdOf(stdin: string): string {\n const m = /\"(?:session_id|sessionId|conversationId)\"\\s*:\\s*\"([^\"]{1,128})\"/.exec(stdin);\n return m?.[1] ?? \"\";\n }\n /** One heartbeat per minute per session: the stamp file's mtime is the clock. */\n function beatDue(id: string): boolean {\n const stamp = path.join(state, \"beats\", `${harness}-${id.replace(/[^A-Za-z0-9_-]/g, \"_\")}`);\n try {\n if (Date.now() - statSync(stamp).mtimeMs < 60_000) return false;\n } catch {}\n try {\n mkdirSync(path.dirname(stamp), { recursive: true, mode: 0o700 });\n writeFileSync(stamp, \"\", { mode: 0o600 });\n } catch {}\n return true;\n }\n}\n\n// Run as `node hook.js <harness> <event>`; importing this module runs nothing. The main\n// module is known by its real path: node resolves symlinks for import.meta.url, argv[1]\n// keeps the spelling it was given (/var \u2192 /private/var on macOS).\nif (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href)\n hookMain(process.argv.slice(2), process.env, {\n // fd 0 and 1 are the test runner's own (a read of 0 would block it); launcher.test.ts\n // and the bench run these two through the real binary.\n /* v8 ignore next -- see above */\n stdin: () => readFileSync(0),\n /* v8 ignore next -- see above */\n stdout: (text) => writeFileSync(1, text),\n }).then(() => process.exit(0));\n", "// SPDX-License-Identifier: Apache-2.0\n// The daemon's local socket (spec/CONTROL.md \u00A78): how the waiter, the Stop and\n// PermissionRequest hooks and the worker talk to the one daemon on this machine.\n// Line-delimited JSON, one request per connection. A Unix socket in the state\n// directory (0600, in a 0700 dir); a per-user named pipe on Windows. Only node:\n// builtins \u2014 the hook bundle imports this.\nimport net from \"node:net\";\nimport os from \"node:os\";\nimport path from \"node:path\";\n\nexport type LocalRequest =\n /** A waiter parks until a message for its session arrives. */\n | { op: \"wait\"; session: string; pid?: number }\n /** A Stop hook asks for a queued message (turn mode). */\n | { op: \"stop\"; session: string; active: boolean }\n /** A PermissionRequest hook parks until a verified answer, or until it's released. */\n | { op: \"permission\"; session: string; attention: string; tool?: string }\n /** The worker reports a hook event: turn state and attention closure. */\n | { op: \"event\"; session: string; event: string; transcript?: string; cwd?: string }\n /** `sessionpipe control status`. */\n | { op: \"status\" }\n /** `sessionpipe update` installed a new copy: restart onto it once nothing is in hand. */\n | { op: \"restart\" };\n\nexport type LocalReply =\n | { op: \"message\"; text: string; id: string }\n | { op: \"block\"; reason: string; id: string }\n | { op: \"decision\"; behavior: \"allow\" | \"deny\"; message?: string; id: string }\n | { op: \"none\" }\n /** A newer waiter for the same session took over; this one stops. */\n | { op: \"replaced\" }\n | { op: \"ok\" }\n | { op: \"status\"; status: unknown };\n\n/** Where the daemon listens. `SESSIONPIPE_SOCKET` overrides (tests). */\nexport function socketPath(state: string, env: NodeJS.ProcessEnv = process.env): string {\n if (env.SESSIONPIPE_SOCKET) return env.SESSIONPIPE_SOCKET;\n if (process.platform === \"win32\") return `\\\\\\\\.\\\\pipe\\\\sessionpipe-control-${os.userInfo().username}`;\n return path.join(state, \"control.sock\");\n}\n\n/** One request, one reply. `timeoutMs` bounds the whole exchange; null on any\n * failure (no daemon, a timeout, a garbled reply) \u2014 callers treat that as \"none\". */\nexport function ask(sock: string, req: LocalRequest, timeoutMs: number): Promise<LocalReply | null> {\n return new Promise((resolve) => {\n let buf = \"\";\n let done = false;\n const s = net.connect(sock);\n const finish = (r: LocalReply | null) => {\n if (done) return;\n done = true;\n clearTimeout(t);\n s.destroy();\n resolve(r);\n };\n const t = setTimeout(() => finish(null), timeoutMs);\n s.on(\"connect\", () => s.write(`${JSON.stringify(req)}\\n`));\n s.on(\"data\", (d) => {\n buf += d.toString(\"utf8\");\n const i = buf.indexOf(\"\\n\");\n if (i < 0) return;\n try {\n finish(JSON.parse(buf.slice(0, i)) as LocalReply);\n } catch {\n finish(null);\n }\n });\n s.on(\"error\", () => finish(null));\n s.on(\"close\", () => finish(null));\n });\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["os", "path", "path", "os"]
7
7
  }
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- // sessionpipe 0.9.0 · Apache-2.0 · https://sessionpipe.org
1
+ // sessionpipe 0.10.0 · Apache-2.0 · https://sessionpipe.org
2
2
 
3
3
  // src/http-sink.ts
4
4
  import { createHash, createHmac } from "node:crypto";
@@ -924,14 +924,28 @@ var claudeCode = {
924
924
  case "SessionStart":
925
925
  events.push(ev("session.started", { source: pickSource(s.source ?? s.trigger) }));
926
926
  break;
927
- case "UserPromptSubmit":
927
+ case "UserPromptSubmit": {
928
+ const prompt = typeof s.prompt === "string" ? s.prompt : void 0;
928
929
  events.push(
929
930
  ev("turn.started", {
930
931
  turn_id,
931
- prompt_chars: typeof s.prompt === "string" ? [...s.prompt].length : void 0
932
+ prompt_chars: prompt === void 0 ? void 0 : [...prompt].length,
933
+ // Claude Code's hook schema documents `source`
934
+ // (user | sdk | system | loop_wakeup | schedule_wakeup | poll_event) and says a
935
+ // payload may omit it while the field rolls out; 2.1.304's payload has no such
936
+ // key at all. Forwarded verbatim, shape-guarded only: the list is the harness's
937
+ // to extend.
938
+ submitted_by: harnessWord(s.source),
939
+ // One of Claude Code's own prompt wrappers: the prompt is a block the harness
940
+ // wrote, so it is not the person's words. Absent is unknown, never "the person" —
941
+ // and `true` is never "no person caused this turn", because a Talk or
942
+ // `sessionpipe wait` delivery hands the person's own message over inside a
943
+ // <task-notification> (PROTOCOL.md §4).
944
+ injected: prompt !== void 0 && injectedBlock(prompt) ? true : void 0
932
945
  })
933
946
  );
934
947
  break;
948
+ }
935
949
  case "PreToolUse":
936
950
  if (tool) events.push(ev("tool.started", { tool, call_id, turn_id, agent_id, input: s.tool_input }));
937
951
  break;
@@ -986,7 +1000,13 @@ var claudeCode = {
986
1000
  );
987
1001
  break;
988
1002
  case "Stop":
989
- events.push(ev("turn.ended", { turn_id, reason: "stop" }));
1003
+ events.push(
1004
+ ev("turn.ended", {
1005
+ turn_id,
1006
+ reason: "stop",
1007
+ background: Array.isArray(s.background_tasks) ? s.background_tasks.length : void 0
1008
+ })
1009
+ );
990
1010
  break;
991
1011
  case "StopFailure":
992
1012
  events.push(
@@ -1029,6 +1049,8 @@ var claudeCode = {
1029
1049
  if (event === "UserPromptSubmit" && typeof s.prompt === "string") hints.prompt = s.prompt;
1030
1050
  const host = input.env.CLAUDE_CODE_HOST_SESSION_ID;
1031
1051
  if (host && DESKTOP_ID.test(host)) hints.desktop_id = host;
1052
+ const attended = input.env.CLAUDE_CODE_SESSION_ATTENDED;
1053
+ if (attended === "1" || attended === "0") hints.attended = attended === "1" || !!input.env.SESSIONPIPE_CONTROL_JOB;
1032
1054
  if (event !== "PreToolUse" && event !== "PostToolUse") hints.claude_pid = claudePid(input.env);
1033
1055
  const r = { session, events, hints };
1034
1056
  if (typeof s.transcript_path === "string") r.transcript = s.transcript_path;
@@ -1049,6 +1071,7 @@ var claudeCode = {
1049
1071
  const cwd = out.cwd ?? session.cwd;
1050
1072
  Object.assign(out, gitFacts(cwd), out.branch ? { branch: out.branch } : {});
1051
1073
  if (!out.url && typeof hints.desktop_id === "string") out.url = `claude://code/continue/${hints.desktop_id}`;
1074
+ if (typeof hints.attended === "boolean") out.attended = hints.attended || !!out.url;
1052
1075
  return out;
1053
1076
  },
1054
1077
  backfill(sinceMs, env = process.env) {
@@ -1084,6 +1107,9 @@ function notClaudeCode(id, s) {
1084
1107
  function pickSource(v) {
1085
1108
  return typeof v === "string" && ["startup", "resume", "clear", "compact", "fork"].includes(v) ? v : "unknown";
1086
1109
  }
1110
+ var harnessWord = (v) => typeof v === "string" && /^[A-Za-z0-9][A-Za-z0-9_.-]{0,39}$/.test(v) ? v : void 0;
1111
+ var WRAPPER = /^<(?:task-notification|cross-session-message|system-reminder|local-command-stdout|command-[a-z-]+)[\s/>]/i;
1112
+ var injectedBlock = (prompt) => WRAPPER.test(prompt.trim());
1087
1113
  function errText(v) {
1088
1114
  if (typeof v === "string") return v.slice(0, 2e3);
1089
1115
  if (v && typeof v === "object") {
@@ -1200,7 +1226,27 @@ function claudeLogin(dir, home = homeDir()) {
1200
1226
  } catch {
1201
1227
  }
1202
1228
  }
1203
- return null;
1229
+ return apiAccount(dir);
1230
+ }
1231
+ var ANTHROPIC_HOST = /(^|\.)anthropic\.com$/i;
1232
+ function apiAccount(dir) {
1233
+ let base;
1234
+ try {
1235
+ base = JSON.parse(readFileSync3(settingsIn(dir), "utf8")).env?.ANTHROPIC_BASE_URL;
1236
+ } catch {
1237
+ return null;
1238
+ }
1239
+ if (typeof base !== "string") return null;
1240
+ let u;
1241
+ try {
1242
+ u = new URL(base.trim());
1243
+ } catch {
1244
+ return null;
1245
+ }
1246
+ if (u.protocol !== "https:" && u.protocol !== "http:" || !u.hostname || ANTHROPIC_HOST.test(u.hostname))
1247
+ return null;
1248
+ const origin = `${u.protocol}//${u.host}`;
1249
+ return { id: `api-${createHash2("sha256").update(origin).digest("hex").slice(0, 16)}`, name: u.host };
1204
1250
  }
1205
1251
  var accountCache = /* @__PURE__ */ new Map();
1206
1252
  function claudeAccount(dir) {
@@ -1360,6 +1406,18 @@ function claudeFacts(file, sessionId, hints, state) {
1360
1406
  return out;
1361
1407
  }
1362
1408
 
1409
+ // ../core/src/control/verify.ts
1410
+ var GRANT_MAX_MS = 24 * 36e5;
1411
+ var SKEW_MS = 5 * 6e4;
1412
+ var MACHINE_MAX_AGE_MS = 24 * 36e5;
1413
+ var RECEIVER_MAX_AGE_MS = 10 * 6e4;
1414
+ var NONCE_TTL_MS = 25 * 36e5;
1415
+ var MODEL_RE = /^[A-Za-z0-9][A-Za-z0-9._:/@[\]-]*$/;
1416
+ var START_PERMISSIONS = ["read", "safe", "edits", "auto"];
1417
+ var levelRank = (l) => START_PERMISSIONS.indexOf(l);
1418
+ var enc = new TextEncoder();
1419
+ var dec = new TextDecoder();
1420
+
1363
1421
  // ../core/src/adapters/codex.ts
1364
1422
  import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
1365
1423
  import path6 from "node:path";
@@ -1681,7 +1739,7 @@ var codex = {
1681
1739
  if (j && typeof j === "object" && !Array.isArray(j)) s = j;
1682
1740
  } catch {
1683
1741
  }
1684
- const id = s.session_id;
1742
+ const id = s.session_id ?? s.thread_id ?? s.conversation_id ?? s["thread-id"] ?? s["session-id"];
1685
1743
  if (!id) return null;
1686
1744
  if (s.transcript_path === null) return null;
1687
1745
  const session = { id: String(id), cwd: typeof s.cwd === "string" ? s.cwd : input.cwd };
@@ -2525,6 +2583,22 @@ function makeEvent(a, ctx) {
2525
2583
  };
2526
2584
  }
2527
2585
 
2586
+ // ../core/src/launched-by.ts
2587
+ var LAUNCHED_BY_ENV = "SESSIONPIPE_LAUNCHED_BY";
2588
+ var LAUNCHED_BY = /^[a-z][a-z0-9-]{0,39}:[A-Za-z0-9._:-]{1,128}$/;
2589
+ function launchedBy(env) {
2590
+ const v = env[LAUNCHED_BY_ENV]?.trim();
2591
+ return v && LAUNCHED_BY.test(v) ? v : void 0;
2592
+ }
2593
+ function readHook(adapter, input) {
2594
+ const r = adapter.fromHook(input);
2595
+ if (r && !r.session.parent_id) {
2596
+ const launcher = launchedBy(input.env);
2597
+ if (launcher && launcher !== `${adapter.name}:${r.session.id}`) r.session.launched_by = launcher;
2598
+ }
2599
+ return r;
2600
+ }
2601
+
2528
2602
  // ../core/src/outbox.ts
2529
2603
  import {
2530
2604
  appendFileSync,
@@ -2967,8 +3041,8 @@ var DATA_ALLOW = {
2967
3041
  "session.ended": { 0: ["reason"] },
2968
3042
  "session.backfill": { 0: ["started_at", "last_at", "turns"] },
2969
3043
  "session.forgotten": { 0: [] },
2970
- "turn.started": { 0: ["turn_id"], 1: ["prompt_chars"] },
2971
- "turn.ended": { 0: ["turn_id", "reason", "ms"], 1: ["error"] },
3044
+ "turn.started": { 0: ["turn_id", "submitted_by", "injected"], 1: ["prompt_chars"] },
3045
+ "turn.ended": { 0: ["turn_id", "reason", "ms", "background"], 1: ["error"] },
2972
3046
  "turn.transcript": { 2: ["turn", "user", "assistant", "at"] },
2973
3047
  "turn.progress": { 2: ["turn", "user", "assistant", "at"] },
2974
3048
  "tool.started": { 1: ["tool", "call_id", "turn_id", "agent_id"], 3: ["input"] },
@@ -2992,7 +3066,9 @@ var SESSION_ALLOW = [
2992
3066
  "title_source",
2993
3067
  "url",
2994
3068
  "account_id",
2995
- "parent_id"
3069
+ "attended",
3070
+ "parent_id",
3071
+ "launched_by"
2996
3072
  ];
2997
3073
  var MAX_SIDE = 2e4;
2998
3074
  var MAX_IO = 64 * 1024;
@@ -3106,6 +3182,49 @@ var StdoutSink = class {
3106
3182
  // src/control/aliases.ts
3107
3183
  import { chmodSync as chmodSync2, mkdirSync as mkdirSync7, readFileSync as readFileSync8, renameSync as renameSync2, writeFileSync as writeFileSync6 } from "node:fs";
3108
3184
  import path12 from "node:path";
3185
+
3186
+ // src/secrets.ts
3187
+ import { spawnSync } from "node:child_process";
3188
+ import { createHash as createHash6, randomBytes as randomBytes2 } from "node:crypto";
3189
+ var SERVICE = "sessionpipe";
3190
+ var defaultRunner = (cmd, args, input) => {
3191
+ const r = spawnSync(cmd, args, {
3192
+ input: input ?? "",
3193
+ encoding: "utf8",
3194
+ timeout: 8e3,
3195
+ stdio: ["pipe", "pipe", "ignore"]
3196
+ });
3197
+ return { status: r.error ? null : r.status, stdout: r.stdout ?? "" };
3198
+ };
3199
+ var sinkAccount = (name, what = "token") => {
3200
+ const scope = process.env.SESSIONPIPE_CONFIG ? `@${createHash6("sha256").update(configFile()).digest("hex").slice(0, 8)}` : "";
3201
+ return `sink:${name}:${what}${scope}`;
3202
+ };
3203
+ function secretGet(store, account, run = defaultRunner) {
3204
+ const r = store === "keychain" ? run("security", ["find-generic-password", "-s", SERVICE, "-a", account, "-w"]) : run("secret-tool", ["lookup", "service", SERVICE, "account", account]);
3205
+ if (r.status !== 0) return null;
3206
+ const v = r.stdout.replace(/\r?\n$/, "");
3207
+ return v || null;
3208
+ }
3209
+ function sinkWithSecrets(s, run = defaultRunner) {
3210
+ if (!s.token_in) return s;
3211
+ const token = secretGet(s.token_in, sinkAccount(s.name), run);
3212
+ if (!token) return null;
3213
+ const secret = secretGet(s.token_in, sinkAccount(s.name, "secret"), run);
3214
+ return { ...s, token, ...secret ? { secret } : {} };
3215
+ }
3216
+
3217
+ // src/control/store.ts
3218
+ function startOptionsOf(v) {
3219
+ const o = v;
3220
+ if (!o || typeof o !== "object" || levelRank(o.permission) < 0 && o.permission !== "inherit") return null;
3221
+ return {
3222
+ permission: o.permission,
3223
+ ...typeof o.model === "string" && MODEL_RE.test(o.model) ? { model: o.model } : {}
3224
+ };
3225
+ }
3226
+
3227
+ // src/control/aliases.ts
3109
3228
  var aliasFile = (controlDir) => path12.join(controlDir, "aliases.json");
3110
3229
  function readAliases(file) {
3111
3230
  try {
@@ -3115,7 +3234,7 @@ function readAliases(file) {
3115
3234
  for (const [k, v] of Object.entries(j)) {
3116
3235
  const a = v;
3117
3236
  if (a && typeof a.id === "string" && a.id && typeof a.cwd === "string")
3118
- out[k] = { id: a.id, cwd: a.cwd, at: typeof a.at === "string" ? a.at : "" };
3237
+ out[k] = { id: a.id, cwd: a.cwd, at: typeof a.at === "string" ? a.at : "", ...startOptionsOf(a) };
3119
3238
  }
3120
3239
  return out;
3121
3240
  } catch {
@@ -3167,37 +3286,6 @@ function ask(sock, req, timeoutMs) {
3167
3286
  });
3168
3287
  }
3169
3288
 
3170
- // src/secrets.ts
3171
- import { spawnSync } from "node:child_process";
3172
- import { createHash as createHash6, randomBytes as randomBytes2 } from "node:crypto";
3173
- var SERVICE = "sessionpipe";
3174
- var defaultRunner = (cmd, args, input) => {
3175
- const r = spawnSync(cmd, args, {
3176
- input: input ?? "",
3177
- encoding: "utf8",
3178
- timeout: 8e3,
3179
- stdio: ["pipe", "pipe", "ignore"]
3180
- });
3181
- return { status: r.error ? null : r.status, stdout: r.stdout ?? "" };
3182
- };
3183
- var sinkAccount = (name, what = "token") => {
3184
- const scope = process.env.SESSIONPIPE_CONFIG ? `@${createHash6("sha256").update(configFile()).digest("hex").slice(0, 8)}` : "";
3185
- return `sink:${name}:${what}${scope}`;
3186
- };
3187
- function secretGet(store, account, run = defaultRunner) {
3188
- const r = store === "keychain" ? run("security", ["find-generic-password", "-s", SERVICE, "-a", account, "-w"]) : run("secret-tool", ["lookup", "service", SERVICE, "account", account]);
3189
- if (r.status !== 0) return null;
3190
- const v = r.stdout.replace(/\r?\n$/, "");
3191
- return v || null;
3192
- }
3193
- function sinkWithSecrets(s, run = defaultRunner) {
3194
- if (!s.token_in) return s;
3195
- const token = secretGet(s.token_in, sinkAccount(s.name), run);
3196
- if (!token) return null;
3197
- const secret = secretGet(s.token_in, sinkAccount(s.name, "secret"), run);
3198
- return { ...s, token, ...secret ? { secret } : {} };
3199
- }
3200
-
3201
3289
  // src/run.ts
3202
3290
  async function tellDaemon(state, job, session, cwd, transcript) {
3203
3291
  const sock = socketPath(state);
@@ -3298,6 +3386,12 @@ function openTurn(read, o) {
3298
3386
  };
3299
3387
  }
3300
3388
  var MAX_TURN_SIDE = 2e4;
3389
+ var JOB_OWN_ENV = [
3390
+ "CLAUDE_CODE_SESSION_ATTENDED",
3391
+ "CLAUDE_CODE_HOST_SESSION_ID",
3392
+ "SESSIONPIPE_CONTROL_JOB",
3393
+ "SESSIONPIPE_LAUNCHED_BY"
3394
+ ];
3301
3395
  async function runJob(job, opts = {}) {
3302
3396
  const state = opts.state ?? stateDir();
3303
3397
  const cfg = readConfig();
@@ -3306,9 +3400,20 @@ async function runJob(job, opts = {}) {
3306
3400
  log(state, `${job.harness} ${job.event}: no adapter for harness "${job.harness}"; event dropped`);
3307
3401
  return { events: [], delivered: {} };
3308
3402
  }
3309
- const r = adapter.fromHook({ argv: job.argv, stdin: job.stdin, env: { ...process.env, ...job.env }, cwd: job.cwd });
3403
+ const env = { ...process.env };
3404
+ for (const k of JOB_OWN_ENV) delete env[k];
3405
+ const r = readHook(adapter, { argv: job.argv, stdin: job.stdin, env: { ...env, ...job.env }, cwd: job.cwd });
3310
3406
  if (!r || !r.session.id) {
3311
- log(state, `${job.harness} ${job.event}: the ${job.harness} adapter did not claim this payload; event dropped`);
3407
+ let keys = [];
3408
+ try {
3409
+ const payload = JSON.parse(job.argv.find((a) => a.startsWith("{")) ?? job.stdin);
3410
+ if (payload && typeof payload === "object" && !Array.isArray(payload)) keys = Object.keys(payload);
3411
+ } catch {
3412
+ }
3413
+ log(
3414
+ state,
3415
+ `${job.harness} ${job.event}: the ${job.harness} adapter did not claim this payload; event dropped; keys: ${keys.join(", ") || "(none)"}`
3416
+ );
3312
3417
  return { events: [], delivered: {} };
3313
3418
  }
3314
3419
  const id = receiverIdOf(readAliases(aliasFile(path14.join(state, "control"))), job.harness, r.session.id) ?? r.session.id;
@@ -3360,7 +3465,8 @@ async function runJob(job, opts = {}) {
3360
3465
  seq: 0,
3361
3466
  machine: machineName(cfg, os3.hostname()),
3362
3467
  ...cwdRaw ? { cwd: tilde(cwdRaw) } : {},
3363
- ...r.session.model ? { model: r.session.model } : {}
3468
+ ...r.session.model ? { model: r.session.model } : {},
3469
+ ...r.session.launched_by ? { launched_by: r.session.launched_by } : {}
3364
3470
  };
3365
3471
  const version = facts.get(job.harness, id).harness_version ?? void 0;
3366
3472
  for (const a of r.events) {