@penclipai/adapter-utils 2026.704.0 → 2026.714.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.
Files changed (55) hide show
  1. package/README.md +1 -1
  2. package/dist/acpx-engine/cli.d.ts +2 -0
  3. package/dist/acpx-engine/cli.d.ts.map +1 -0
  4. package/dist/acpx-engine/cli.js +128 -0
  5. package/dist/acpx-engine/cli.js.map +1 -0
  6. package/dist/acpx-engine/constants.d.ts +16 -0
  7. package/dist/acpx-engine/constants.d.ts.map +1 -0
  8. package/dist/acpx-engine/constants.js +18 -0
  9. package/dist/acpx-engine/constants.js.map +1 -0
  10. package/dist/acpx-engine/execute.d.ts +57 -0
  11. package/dist/acpx-engine/execute.d.ts.map +1 -0
  12. package/dist/acpx-engine/execute.js +1849 -0
  13. package/dist/acpx-engine/execute.js.map +1 -0
  14. package/dist/acpx-engine/execute.test.d.ts +2 -0
  15. package/dist/acpx-engine/execute.test.d.ts.map +1 -0
  16. package/dist/acpx-engine/execute.test.js +1332 -0
  17. package/dist/acpx-engine/execute.test.js.map +1 -0
  18. package/dist/acpx-engine/index.d.ts +6 -0
  19. package/dist/acpx-engine/index.d.ts.map +1 -0
  20. package/dist/acpx-engine/index.js +6 -0
  21. package/dist/acpx-engine/index.js.map +1 -0
  22. package/dist/acpx-engine/session-codec.d.ts +3 -0
  23. package/dist/acpx-engine/session-codec.d.ts.map +1 -0
  24. package/dist/acpx-engine/session-codec.js +48 -0
  25. package/dist/acpx-engine/session-codec.js.map +1 -0
  26. package/dist/acpx-engine/ui.d.ts +3 -0
  27. package/dist/acpx-engine/ui.d.ts.map +1 -0
  28. package/dist/acpx-engine/ui.js +162 -0
  29. package/dist/acpx-engine/ui.js.map +1 -0
  30. package/dist/execution-target-sandbox.test.js +416 -5
  31. package/dist/execution-target-sandbox.test.js.map +1 -1
  32. package/dist/execution-target.d.ts +37 -1
  33. package/dist/execution-target.d.ts.map +1 -1
  34. package/dist/execution-target.js +488 -9
  35. package/dist/execution-target.js.map +1 -1
  36. package/dist/index.d.ts +1 -1
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js.map +1 -1
  39. package/dist/sandbox-callback-bridge.d.ts.map +1 -1
  40. package/dist/sandbox-callback-bridge.js +9 -4
  41. package/dist/sandbox-callback-bridge.js.map +1 -1
  42. package/dist/sandbox-callback-bridge.test.js +4 -0
  43. package/dist/sandbox-callback-bridge.test.js.map +1 -1
  44. package/dist/server-utils.d.ts +18 -0
  45. package/dist/server-utils.d.ts.map +1 -1
  46. package/dist/server-utils.js +85 -19
  47. package/dist/server-utils.js.map +1 -1
  48. package/dist/server-utils.test.js +256 -8
  49. package/dist/server-utils.test.js.map +1 -1
  50. package/dist/session-compaction.d.ts.map +1 -1
  51. package/dist/session-compaction.js +0 -6
  52. package/dist/session-compaction.js.map +1 -1
  53. package/dist/types.d.ts +35 -1
  54. package/dist/types.d.ts.map +1 -1
  55. package/package.json +5 -1
@@ -1,4 +1,8 @@
1
+ import fs from "node:fs/promises";
2
+ import net from "node:net";
3
+ import os from "node:os";
1
4
  import path from "node:path";
5
+ import { randomUUID } from "node:crypto";
2
6
  import { prepareCommandManagedRuntime, } from "./command-managed-runtime.js";
3
7
  import { buildRemoteExecutionSessionIdentity, prepareRemoteManagedRuntime, remoteExecutionSessionMatches, } from "./remote-managed-runtime.js";
4
8
  import { createCommandManagedSandboxCallbackBridgeQueueClient, createSandboxCallbackBridgeAsset, createSandboxCallbackBridgeToken, DEFAULT_SANDBOX_CALLBACK_BRIDGE_MAX_BODY_BYTES, sandboxCallbackBridgeDirectories, startSandboxCallbackBridgeServer, startSandboxCallbackBridgeWorker, } from "./sandbox-callback-bridge.js";
@@ -8,7 +12,14 @@ import { ensureCommandResolvable, resolveCommandForLogs, runChildProcess, } from
8
12
  import { sanitizeRemoteExecutionEnv } from "./remote-execution-env.js";
9
13
  import { preferredShellForSandbox, shellCommandArgs } from "./sandbox-shell.js";
10
14
  export { sanitizeRemoteExecutionEnv } from "./remote-execution-env.js";
11
- export const DEFAULT_REMOTE_SANDBOX_ADAPTER_TIMEOUT_SEC = 1_800;
15
+ // 4-hour wall-clock backstop for sandbox-backed adapter runs. This is a
16
+ // last-resort kill switch, not the primary hang detector: genuinely hung runs
17
+ // are caught much earlier by the adapters' output-inactivity monitors (e.g.
18
+ // codex-local's 7-minute monitor). The value intentionally matches the
19
+ // recovery watchdog's ACTIVE_RUN_OUTPUT_CRITICAL_THRESHOLD_MS (4h) in
20
+ // server/src/services/recovery/service.ts so healthy long runs are never
21
+ // killed by the adapter before the watchdog would even consider them stuck.
22
+ export const DEFAULT_REMOTE_SANDBOX_ADAPTER_TIMEOUT_SEC = 14_400;
12
23
  function parseObject(value) {
13
24
  return value && typeof value === "object" && !Array.isArray(value)
14
25
  ? value
@@ -102,21 +113,71 @@ export function describeAdapterExecutionTarget(target) {
102
113
  }
103
114
  return `sandbox environment${target.providerKey ? ` (${target.providerKey})` : ""}`;
104
115
  }
105
- export function resolveAdapterExecutionTargetTimeoutSec(target, configuredTimeoutSec) {
106
- const normalizedConfiguredTimeoutSec = typeof configuredTimeoutSec === "number" && Number.isFinite(configuredTimeoutSec) && configuredTimeoutSec > 0
107
- ? Math.floor(configuredTimeoutSec)
108
- : 0;
109
- if (normalizedConfiguredTimeoutSec > 0)
110
- return normalizedConfiguredTimeoutSec;
116
+ export function resolveAdapterExecutionTargetTimeout(target, configuredTimeoutSec) {
117
+ if (typeof configuredTimeoutSec === "number" && Number.isFinite(configuredTimeoutSec)) {
118
+ // Preserve fractional (sub-second) configured values instead of flooring:
119
+ // adapters historically honored e.g. timeoutSec=0.5, and flooring would
120
+ // silently turn it into "no timeout".
121
+ if (configuredTimeoutSec > 0) {
122
+ return { timeoutSec: configuredTimeoutSec, source: "configured" };
123
+ }
124
+ // A negative timeoutSec is the explicit "no adapter wall-clock timeout"
125
+ // opt-out, honored even on sandbox targets. Zero cannot carry that
126
+ // meaning: the adapter config UI persists the schema default of 0 for
127
+ // untouched fields, so timeoutSec=0 in stored config does not signal
128
+ // operator intent and falls through to target defaults below.
129
+ if (configuredTimeoutSec < 0) {
130
+ return { timeoutSec: 0, source: "configured" };
131
+ }
132
+ }
111
133
  // Local and SSH adapters preserve the historical "0 means no adapter
112
134
  // timeout" behavior. Sandbox-backed runs execute through provider RPCs
113
135
  // that usually apply their own shorter command defaults, so request an
114
136
  // explicit longer timeout for full adapter runs when the adapter leaves
115
137
  // timeoutSec unset.
116
138
  if (target?.kind === "remote" && target.transport === "sandbox") {
117
- return DEFAULT_REMOTE_SANDBOX_ADAPTER_TIMEOUT_SEC;
139
+ return { timeoutSec: DEFAULT_REMOTE_SANDBOX_ADAPTER_TIMEOUT_SEC, source: "sandbox_default" };
140
+ }
141
+ return { timeoutSec: 0, source: "unlimited" };
142
+ }
143
+ export function resolveAdapterExecutionTargetTimeoutSec(target, configuredTimeoutSec) {
144
+ return resolveAdapterExecutionTargetTimeout(target, configuredTimeoutSec).timeoutSec;
145
+ }
146
+ function describeAdapterExecutionTimeoutSource(source) {
147
+ switch (source) {
148
+ case "configured":
149
+ return "configured via adapterConfig.timeoutSec";
150
+ case "sandbox_default":
151
+ return "sandbox default";
152
+ case "unlimited":
153
+ return "no adapter wall-clock timeout";
118
154
  }
119
- return 0;
155
+ }
156
+ /**
157
+ * Self-describing error message for when the adapter wall-clock execution
158
+ * timeout kills a run. Names the timer that fired and the knob that controls
159
+ * it so run failures never surface as a bare "Timed out".
160
+ */
161
+ export function formatAdapterExecutionTimeoutErrorMessage(resolution) {
162
+ return (`Run exceeded the adapter execution timeout ` +
163
+ `(timeoutSec=${resolution.timeoutSec}, ${describeAdapterExecutionTimeoutSource(resolution.source)}). ` +
164
+ `Set adapterConfig.timeoutSec to raise it.`);
165
+ }
166
+ /**
167
+ * One-line start-of-run statement of the effective wall-clock timeout and its
168
+ * source. Callers prefix with `[paperclip] ` and append a newline.
169
+ */
170
+ export function formatAdapterExecutionTimeoutStartLogLine(resolution) {
171
+ if (resolution.timeoutSec <= 0) {
172
+ if (resolution.source === "configured") {
173
+ return ("Adapter execution timeout: none " +
174
+ "(explicitly disabled via adapterConfig.timeoutSec; set it to a positive value to add one).");
175
+ }
176
+ return ("Adapter execution timeout: none " +
177
+ "(no adapter wall-clock timeout for this target; set adapterConfig.timeoutSec to add one).");
178
+ }
179
+ return (`Adapter execution timeout: timeoutSec=${resolution.timeoutSec} ` +
180
+ `(${describeAdapterExecutionTimeoutSource(resolution.source)}; set adapterConfig.timeoutSec to override).`);
120
181
  }
121
182
  function requireSandboxRunner(target) {
122
183
  if (target.runner)
@@ -752,6 +813,424 @@ async function readBridgeForwardResponseBody(response, maxBodyBytes) {
752
813
  }
753
814
  return Buffer.concat(chunks, totalBytes).toString("utf8");
754
815
  }
816
+ const PROCESS_SESSION_PROXY_SCRIPT = "paperclip-process-session-proxy.mjs";
817
+ const PROCESS_SESSION_REMOTE_SCRIPT = "paperclip-process-session-remote.mjs";
818
+ const PROCESS_SESSION_AUTH_TIMEOUT_MS = 5_000;
819
+ function jsonLine(value) {
820
+ return `${JSON.stringify(value)}\n`;
821
+ }
822
+ function splitJsonLines(buffer) {
823
+ const parts = buffer.split(/\n/);
824
+ return { lines: parts.slice(0, -1), rest: parts.at(-1) ?? "" };
825
+ }
826
+ async function writeProcessSessionProxyScript(dir, port, token) {
827
+ await fs.mkdir(dir, { recursive: true });
828
+ const proxyPath = path.join(dir, PROCESS_SESSION_PROXY_SCRIPT);
829
+ await fs.writeFile(proxyPath, getProcessSessionProxySource({ port, token }), { mode: 0o700 });
830
+ if (process.platform === "win32") {
831
+ // ACPX splits override commands before spawning them. Use native Node
832
+ // directly so a .cmd shell layer cannot buffer the proxy's streaming stdin.
833
+ const nodePath = process.execPath.replace(/\\/g, "/");
834
+ const scriptPath = proxyPath.replace(/\\/g, "/");
835
+ return `${JSON.stringify(nodePath)} ${JSON.stringify(scriptPath)}`;
836
+ }
837
+ return proxyPath;
838
+ }
839
+ async function syncProcessSessionRemoteScript(input) {
840
+ await input.client.writeTextFile(input.remoteScriptPath, getProcessSessionRemoteSource());
841
+ }
842
+ async function readRemoteJsonFiles(input) {
843
+ const names = await input.client.listJsonFiles(input.dir);
844
+ const out = [];
845
+ for (const name of names) {
846
+ const filePath = path.posix.join(input.dir, name);
847
+ const body = await input.client.readTextFile(filePath);
848
+ await input.client.remove(filePath).catch(() => undefined);
849
+ out.push({ name, body });
850
+ }
851
+ return out;
852
+ }
853
+ async function waitForLocalServerListen(server) {
854
+ await new Promise((resolve, reject) => {
855
+ server.once("error", reject);
856
+ server.listen(0, "127.0.0.1", () => {
857
+ server.off("error", reject);
858
+ resolve();
859
+ });
860
+ });
861
+ const address = server.address();
862
+ if (!address || typeof address === "string") {
863
+ throw new Error("Process session bridge did not expose a TCP port.");
864
+ }
865
+ return address.port;
866
+ }
867
+ export async function startAdapterExecutionTargetProcessSessionBridge(input) {
868
+ if (!input.target || input.target.kind !== "remote" || input.target.transport !== "sandbox") {
869
+ return null;
870
+ }
871
+ const target = input.target;
872
+ const onLog = input.onLog ?? (async () => { });
873
+ const runner = requireSandboxRunner(target);
874
+ const shellCommand = preferredSandboxShell(target);
875
+ const timeoutMs = typeof input.timeoutSec === "number" && Number.isFinite(input.timeoutSec) && input.timeoutSec > 0
876
+ ? Math.trunc(input.timeoutSec * 1000)
877
+ : target.timeoutMs ?? undefined;
878
+ const bridgeRuntimeDir = path.posix.join(input.runtimeRootDir?.trim() || path.posix.join(target.remoteCwd, ".paperclip-runtime", input.adapterKey), "process-sessions");
879
+ const sessionId = randomUUID();
880
+ const sessionDir = path.posix.join(bridgeRuntimeDir, sessionId);
881
+ const stdinDir = path.posix.join(sessionDir, "stdin");
882
+ const eventsDir = path.posix.join(sessionDir, "events");
883
+ const commandFilePath = path.posix.join(sessionDir, "command.json");
884
+ const remoteScriptPath = path.posix.join(bridgeRuntimeDir, PROCESS_SESSION_REMOTE_SCRIPT);
885
+ const client = createCommandManagedSandboxCallbackBridgeQueueClient({
886
+ runner,
887
+ remoteCwd: target.remoteCwd,
888
+ timeoutMs,
889
+ shellCommand,
890
+ });
891
+ await client.makeDir(stdinDir);
892
+ await client.makeDir(eventsDir);
893
+ await syncProcessSessionRemoteScript({ client, remoteScriptPath });
894
+ const commandPayload = JSON.stringify({
895
+ command: input.command,
896
+ args: input.args,
897
+ cwd: input.cwd || target.remoteCwd,
898
+ env: sanitizeRemoteExecutionEnv(input.env),
899
+ });
900
+ await client.writeTextFile(commandFilePath, commandPayload);
901
+ await onLog("stdout", `[paperclip] Starting ACP process session bridge in sandbox (${target.providerKey ?? "provider"}).\n`);
902
+ const startResult = await runner.execute({
903
+ command: shellCommand,
904
+ args: shellCommandArgs([
905
+ `mkdir -p ${shellQuote(stdinDir)} ${shellQuote(eventsDir)}`,
906
+ `chmod 600 ${shellQuote(commandFilePath)}`,
907
+ `PAPERCLIP_PROCESS_SESSION_DIR=${shellQuote(sessionDir)} ` +
908
+ `nohup node ${shellQuote(remoteScriptPath)} >/dev/null 2>&1 < /dev/null &`,
909
+ "printf '%s\\n' \"$!\"",
910
+ ].join("\n")),
911
+ cwd: target.remoteCwd,
912
+ env: {
913
+ PAPERCLIP_SANDBOX_EXEC_CHANNEL: "bridge",
914
+ },
915
+ timeoutMs,
916
+ });
917
+ if (startResult.timedOut || (startResult.exitCode ?? 1) !== 0) {
918
+ throw new Error(`Failed to start sandbox ACP process session bridge: ${startResult.stderr || startResult.stdout}`);
919
+ }
920
+ let socket = null;
921
+ let stopping = false;
922
+ let stdinSeq = 0;
923
+ let pollTimer = null;
924
+ let markReady = () => { };
925
+ const ready = new Promise((resolve) => {
926
+ markReady = resolve;
927
+ });
928
+ const pendingRemoteEvents = [];
929
+ const token = createSandboxCallbackBridgeToken(18);
930
+ const proxyDir = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-process-session-proxy-"));
931
+ const writeRemoteEventToSocket = (event) => {
932
+ if (!socket)
933
+ return false;
934
+ socket.write(jsonLine(event));
935
+ if (event.type === "exit") {
936
+ stopping = true;
937
+ socket.end();
938
+ }
939
+ else if (event.type === "error") {
940
+ stopping = true;
941
+ socket.destroy();
942
+ }
943
+ return true;
944
+ };
945
+ const deliverRemoteEvent = (event) => {
946
+ if (socket) {
947
+ writeRemoteEventToSocket(event);
948
+ return;
949
+ }
950
+ pendingRemoteEvents.push(event);
951
+ if (event.type === "exit" || event.type === "error") {
952
+ stopping = true;
953
+ }
954
+ };
955
+ const flushPendingRemoteEvents = () => {
956
+ if (!socket)
957
+ return;
958
+ while (pendingRemoteEvents.length > 0 && socket) {
959
+ const event = pendingRemoteEvents.shift();
960
+ if (event)
961
+ writeRemoteEventToSocket(event);
962
+ }
963
+ };
964
+ const liveSockets = new Set();
965
+ let stdinWriteChain = Promise.resolve();
966
+ const server = net.createServer((nextSocket) => {
967
+ liveSockets.add(nextSocket);
968
+ nextSocket.setEncoding("utf8");
969
+ nextSocket.on("error", () => undefined);
970
+ let connectionBuffer = "";
971
+ let authenticated = false;
972
+ // Connections own the session (and receive buffered process output) only
973
+ // after presenting the bridge token; idle unauthenticated peers are dropped.
974
+ const authTimer = setTimeout(() => {
975
+ if (!authenticated)
976
+ nextSocket.destroy();
977
+ }, PROCESS_SESSION_AUTH_TIMEOUT_MS);
978
+ authTimer.unref?.();
979
+ nextSocket.on("close", () => {
980
+ clearTimeout(authTimer);
981
+ liveSockets.delete(nextSocket);
982
+ });
983
+ nextSocket.on("data", (chunk) => {
984
+ connectionBuffer += chunk;
985
+ const split = splitJsonLines(connectionBuffer);
986
+ connectionBuffer = split.rest;
987
+ for (const line of split.lines) {
988
+ if (!line.trim())
989
+ continue;
990
+ let message;
991
+ try {
992
+ message = JSON.parse(line);
993
+ }
994
+ catch {
995
+ nextSocket.destroy();
996
+ return;
997
+ }
998
+ if (message.token !== token) {
999
+ nextSocket.destroy();
1000
+ return;
1001
+ }
1002
+ if (!authenticated) {
1003
+ if (socket) {
1004
+ nextSocket.destroy();
1005
+ return;
1006
+ }
1007
+ authenticated = true;
1008
+ clearTimeout(authTimer);
1009
+ socket = nextSocket;
1010
+ markReady();
1011
+ flushPendingRemoteEvents();
1012
+ }
1013
+ stdinWriteChain = stdinWriteChain.then(async () => {
1014
+ if (message.type === "stdin" && typeof message.data === "string") {
1015
+ stdinSeq += 1;
1016
+ const name = `${String(stdinSeq).padStart(12, "0")}.json`;
1017
+ await client.writeTextFile(path.posix.join(stdinDir, name), jsonLine({ type: "stdin", data: message.data }));
1018
+ }
1019
+ else if (message.type === "stdinEnd") {
1020
+ stdinSeq += 1;
1021
+ const name = `${String(stdinSeq).padStart(12, "0")}.json`;
1022
+ await client.writeTextFile(path.posix.join(stdinDir, name), jsonLine({ type: "stdinEnd" }));
1023
+ }
1024
+ }).catch((error) => {
1025
+ if (!nextSocket.destroyed) {
1026
+ nextSocket.write(jsonLine({ type: "error", message: error instanceof Error ? error.message : String(error) }));
1027
+ }
1028
+ nextSocket.destroy();
1029
+ });
1030
+ }
1031
+ });
1032
+ });
1033
+ const poll = async () => {
1034
+ if (stopping)
1035
+ return;
1036
+ try {
1037
+ const events = await readRemoteJsonFiles({ client, dir: eventsDir });
1038
+ for (const event of events) {
1039
+ const parsed = JSON.parse(event.body);
1040
+ deliverRemoteEvent(parsed);
1041
+ if (parsed.type === "exit" || parsed.type === "error")
1042
+ return;
1043
+ }
1044
+ }
1045
+ catch (error) {
1046
+ const message = error instanceof Error ? error.message : String(error);
1047
+ await onLog("stderr", `[paperclip] ACP process session bridge poll failed: ${message}\n`);
1048
+ deliverRemoteEvent({ type: "error", message });
1049
+ return;
1050
+ }
1051
+ finally {
1052
+ if (!stopping) {
1053
+ pollTimer = setTimeout(() => void poll(), 100);
1054
+ pollTimer.unref?.();
1055
+ }
1056
+ }
1057
+ };
1058
+ const port = await waitForLocalServerListen(server);
1059
+ const agentCommand = await writeProcessSessionProxyScript(proxyDir, port, token);
1060
+ pollTimer = setTimeout(() => void poll(), 100);
1061
+ pollTimer.unref?.();
1062
+ return {
1063
+ agentCommand,
1064
+ ready,
1065
+ stop: async () => {
1066
+ stopping = true;
1067
+ markReady();
1068
+ if (pollTimer)
1069
+ clearTimeout(pollTimer);
1070
+ for (const liveSocket of liveSockets)
1071
+ liveSocket.destroy();
1072
+ await new Promise((resolve) => server.close(() => resolve())).catch(() => undefined);
1073
+ await stdinWriteChain.catch(() => undefined);
1074
+ await client.writeTextFile(path.posix.join(stdinDir, `${String(stdinSeq + 1).padStart(12, "0")}.json`), jsonLine({ type: "stdinEnd" })).catch(() => undefined);
1075
+ await client.remove(sessionDir).catch(() => undefined);
1076
+ await fs.rm(proxyDir, { recursive: true, force: true }).catch(() => undefined);
1077
+ },
1078
+ };
1079
+ }
1080
+ function getProcessSessionProxySource(input) {
1081
+ return `#!/usr/bin/env node
1082
+ import net from "node:net";
1083
+
1084
+ const socket = net.createConnection({ host: "127.0.0.1", port: ${input.port} });
1085
+ const token = ${JSON.stringify(input.token)};
1086
+ let buffer = "";
1087
+ let exiting = false;
1088
+ let outputChain = Promise.resolve();
1089
+ let connected = false;
1090
+ let stdinEnded = false;
1091
+ const pendingStdin = [];
1092
+
1093
+ function send(message) {
1094
+ socket.write(JSON.stringify({ token, ...message }) + "\\n");
1095
+ }
1096
+
1097
+ function writeOutput(stream, chunk) {
1098
+ const output = stream === "stderr" ? process.stderr : process.stdout;
1099
+ outputChain = outputChain.then(() => new Promise((resolve) => {
1100
+ if (output.write(chunk)) resolve();
1101
+ else output.once("drain", resolve);
1102
+ }));
1103
+ }
1104
+
1105
+ function finish(code) {
1106
+ if (exiting) return;
1107
+ exiting = true;
1108
+ void outputChain.then(
1109
+ () => {
1110
+ process.exitCode = code;
1111
+ socket.end();
1112
+ },
1113
+ () => {
1114
+ process.exitCode = 1;
1115
+ socket.destroy();
1116
+ },
1117
+ );
1118
+ }
1119
+
1120
+ socket.on("connect", () => {
1121
+ connected = true;
1122
+ send({ type: "hello" });
1123
+ for (const data of pendingStdin.splice(0)) send({ type: "stdin", data });
1124
+ if (stdinEnded) send({ type: "stdinEnd" });
1125
+ });
1126
+ process.stdin.on("data", (chunk) => {
1127
+ const data = Buffer.from(chunk).toString("base64");
1128
+ if (connected) send({ type: "stdin", data });
1129
+ else pendingStdin.push(data);
1130
+ });
1131
+ process.stdin.on("end", () => {
1132
+ stdinEnded = true;
1133
+ if (connected) send({ type: "stdinEnd" });
1134
+ });
1135
+
1136
+ socket.setEncoding("utf8");
1137
+ socket.on("data", (chunk) => {
1138
+ buffer += chunk;
1139
+ const parts = buffer.split(/\\n/);
1140
+ buffer = parts.pop() || "";
1141
+ for (const line of parts) {
1142
+ if (!line.trim()) continue;
1143
+ const message = JSON.parse(line);
1144
+ if (message.type === "data") {
1145
+ const out = Buffer.from(message.data || "", "base64");
1146
+ writeOutput(message.stream, out);
1147
+ } else if (message.type === "error") {
1148
+ writeOutput("stderr", String(message.message || "Process session bridge failed.") + "\\n");
1149
+ finish(1);
1150
+ } else if (message.type === "exit") {
1151
+ finish(typeof message.code === "number" ? message.code : 1);
1152
+ }
1153
+ }
1154
+ });
1155
+ socket.on("close", () => {
1156
+ if (!exiting) process.exit(1);
1157
+ });
1158
+ `;
1159
+ }
1160
+ function getProcessSessionRemoteSource() {
1161
+ return `import { spawn } from "node:child_process";
1162
+ import { promises as fs } from "node:fs";
1163
+ import path from "node:path";
1164
+
1165
+ const sessionDir = process.env.PAPERCLIP_PROCESS_SESSION_DIR;
1166
+ if (!sessionDir) throw new Error("Missing process session bridge env.");
1167
+
1168
+ const stdinDir = path.posix.join(sessionDir, "stdin");
1169
+ const eventsDir = path.posix.join(sessionDir, "events");
1170
+ const commandFilePath = path.posix.join(sessionDir, "command.json");
1171
+ let seq = 0;
1172
+ let stdinClosed = false;
1173
+
1174
+ const config = JSON.parse(await fs.readFile(commandFilePath, "utf8"));
1175
+ await fs.mkdir(stdinDir, { recursive: true });
1176
+ await fs.mkdir(eventsDir, { recursive: true });
1177
+
1178
+ let writeChain = Promise.resolve();
1179
+
1180
+ function writeEvent(event) {
1181
+ seq += 1;
1182
+ const file = path.posix.join(eventsDir, String(seq).padStart(12, "0") + ".json");
1183
+ const write = writeChain.then(async () => {
1184
+ await fs.writeFile(file + ".tmp", JSON.stringify(event) + "\\n", "utf8");
1185
+ await fs.rename(file + ".tmp", file);
1186
+ });
1187
+ writeChain = write.catch(() => undefined);
1188
+ return write;
1189
+ }
1190
+
1191
+ const child = spawn(config.command, Array.isArray(config.args) ? config.args : [], {
1192
+ cwd: config.cwd || process.cwd(),
1193
+ env: { ...process.env, ...(config.env || {}) },
1194
+ stdio: ["pipe", "pipe", "pipe"],
1195
+ });
1196
+
1197
+ child.stdout.on("data", (chunk) => void writeEvent({ type: "data", stream: "stdout", data: Buffer.from(chunk).toString("base64") }));
1198
+ child.stderr.on("data", (chunk) => void writeEvent({ type: "data", stream: "stderr", data: Buffer.from(chunk).toString("base64") }));
1199
+ child.on("error", (error) => void writeEvent({ type: "error", message: error.message }));
1200
+ // "close" (not "exit") so stdout/stderr fully drain before the exit event;
1201
+ // the write chain then guarantees the exit file lands after every data file.
1202
+ child.on("close", (code, signal) => void writeEvent({ type: "exit", code, signal }));
1203
+
1204
+ async function pollStdin() {
1205
+ while (!stdinClosed) {
1206
+ const stdinEntries = await fs.readdir(stdinDir).catch(() => null);
1207
+ if (!stdinEntries) {
1208
+ stdinClosed = true;
1209
+ child.stdin.end();
1210
+ break;
1211
+ }
1212
+ const entries = stdinEntries.filter((name) => name.endsWith(".json")).sort();
1213
+ for (const name of entries) {
1214
+ const file = path.posix.join(stdinDir, name);
1215
+ const raw = await fs.readFile(file, "utf8").catch(() => null);
1216
+ await fs.rm(file, { force: true }).catch(() => undefined);
1217
+ if (!raw) continue;
1218
+ const message = JSON.parse(raw);
1219
+ if (message.type === "stdin" && typeof message.data === "string") {
1220
+ child.stdin.write(Buffer.from(message.data, "base64"));
1221
+ } else if (message.type === "stdinEnd") {
1222
+ stdinClosed = true;
1223
+ child.stdin.end();
1224
+ break;
1225
+ }
1226
+ }
1227
+ if (!stdinClosed) await new Promise((resolve) => setTimeout(resolve, 50));
1228
+ }
1229
+ }
1230
+
1231
+ void pollStdin().catch((error) => void writeEvent({ type: "error", message: error instanceof Error ? error.message : String(error) }));
1232
+ `;
1233
+ }
755
1234
  export async function startAdapterExecutionTargetPaperclipBridge(input) {
756
1235
  if (!adapterExecutionTargetUsesPaperclipBridge(input.target)) {
757
1236
  return null;