opencara 0.104.1 → 0.105.1

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/bin.js CHANGED
@@ -85,7 +85,8 @@ var AcpSpecSchema = z3.object({
85
85
  systemPromptMd: z3.string(),
86
86
  userPromptMd: z3.string(),
87
87
  history: z3.array(AcpHistoryTurnSchema).default([]),
88
- pageContextJson: z3.string().optional()
88
+ pageContextJson: z3.string().optional(),
89
+ priorSessionId: z3.string().optional()
89
90
  });
90
91
  var AgentSpecSchema = z3.object({
91
92
  kind: z3.string(),
@@ -184,7 +185,12 @@ var RunDoneSchema = z4.object({
184
185
  runId: z4.string(),
185
186
  status: z4.enum(["succeeded", "failed", "cancelled"]),
186
187
  exitCode: z4.number().int().nullable().optional(),
187
- errorMessage: z4.string().optional()
188
+ errorMessage: z4.string().optional(),
189
+ /** ACP session id the agent ran under (fresh from session/new, or
190
+ * echoed from session/load). The orchestrator persists this per
191
+ * (repo, branch) so the next iteration can resume via session/load.
192
+ * Null/absent for non-ACP runs (worktree-allocate, write-session). */
193
+ acpSessionId: z4.string().nullable().optional()
188
194
  });
189
195
  var HelloAckSchema = z4.object({
190
196
  type: z4.literal("hello-ack"),
@@ -273,7 +279,6 @@ var DeviceToServerMessageSchema = z4.union([
273
279
  LogFrameSchema,
274
280
  RunDoneSchema,
275
281
  PongSchema,
276
- AgentCallSchema,
277
282
  AgentCallRequestSchema
278
283
  ]);
279
284
  var HostRegisterRequestSchema = z4.object({
@@ -455,9 +460,6 @@ var WsClient = class {
455
460
  }
456
461
  };
457
462
 
458
- // src/runner/spawn.ts
459
- import { spawn as spawn3 } from "node:child_process";
460
-
461
463
  // src/runner/acpRunner.ts
462
464
  import { existsSync as existsSync3 } from "node:fs";
463
465
  import { fileURLToPath as fileURLToPath2 } from "node:url";
@@ -565,6 +567,9 @@ var AcpConnection = class {
565
567
  newSession(req) {
566
568
  return this.request(ACP_METHODS.session_new, req);
567
569
  }
570
+ loadSession(req) {
571
+ return this.request(ACP_METHODS.session_load, req);
572
+ }
568
573
  prompt(req) {
569
574
  return this.request(ACP_METHODS.session_prompt, req);
570
575
  }
@@ -726,6 +731,9 @@ var AcpClient = class {
726
731
  newSession(req) {
727
732
  return this.must().newSession(req);
728
733
  }
734
+ loadSession(req) {
735
+ return this.must().loadSession(req);
736
+ }
729
737
  prompt(req) {
730
738
  return this.must().prompt(req);
731
739
  }
@@ -1069,26 +1077,35 @@ function runAcpJob(opts) {
1069
1077
  }
1070
1078
  };
1071
1079
  const promise = (async () => {
1072
- let result = { exitCode: 1, stopReason: "uninitialized" };
1080
+ let result = { exitCode: 1, stopReason: "uninitialized", sessionId: "" };
1073
1081
  try {
1074
1082
  await host.start();
1075
1083
  client.start();
1076
- await client.initialize({
1084
+ const initResult = await client.initialize({
1077
1085
  protocolVersion: ACP_PROTOCOL_VERSION,
1078
1086
  clientCapabilities: {}
1079
1087
  });
1080
- const session = await client.newSession({
1081
- cwd: spec.cwd ?? process.cwd(),
1082
- mcpServers: [host.acpServerEntry()]
1083
- });
1088
+ const cwd = spec.cwd ?? process.cwd();
1089
+ const mcpServers = [host.acpServerEntry()];
1090
+ const shimSupportsLoad = initResult.agentCapabilities?.loadSession === true;
1091
+ let sessionId;
1092
+ if (acpSpec.priorSessionId && shimSupportsLoad) {
1093
+ await client.loadSession({
1094
+ sessionId: acpSpec.priorSessionId,
1095
+ cwd,
1096
+ mcpServers
1097
+ });
1098
+ sessionId = acpSpec.priorSessionId;
1099
+ } else {
1100
+ const session = await client.newSession({ cwd, mcpServers });
1101
+ sessionId = session.sessionId;
1102
+ }
1084
1103
  const prompt = buildPromptContent(acpSpec);
1085
- const promptResult = await client.prompt({
1086
- sessionId: session.sessionId,
1087
- prompt
1088
- });
1104
+ const promptResult = await client.prompt({ sessionId, prompt });
1089
1105
  result = {
1090
1106
  exitCode: promptResult.stopReason === "end_turn" ? 0 : 1,
1091
- stopReason: promptResult.stopReason
1107
+ stopReason: promptResult.stopReason,
1108
+ sessionId
1092
1109
  };
1093
1110
  return result;
1094
1111
  } finally {
@@ -1187,100 +1204,8 @@ function resolveLocalAcpAdapter(command, args) {
1187
1204
  return { command, args: [...args] };
1188
1205
  }
1189
1206
 
1190
- // src/runner/spawn.ts
1191
- function runJob(spec, stdinJson, handlers) {
1192
- return new Promise((resolve, reject) => {
1193
- const child = spawn3(spec.command, spec.args ?? [], {
1194
- env: { ...process.env, ...spec.env ?? {} },
1195
- cwd: spec.cwd,
1196
- stdio: ["pipe", "pipe", "pipe"]
1197
- });
1198
- child.stdout.setEncoding("utf8");
1199
- child.stderr.setEncoding("utf8");
1200
- child.stdout.on("data", (c) => handlers.onLog("stdout", c));
1201
- child.stderr.on("data", (c) => handlers.onLog("stderr", c));
1202
- child.on("error", reject);
1203
- child.on("close", (code) => resolve({ exitCode: code ?? -1 }));
1204
- if (stdinJson !== void 0) {
1205
- try {
1206
- child.stdin.end(JSON.stringify(stdinJson));
1207
- } catch (err) {
1208
- child.kill();
1209
- reject(err);
1210
- return;
1211
- }
1212
- } else {
1213
- child.stdin.end();
1214
- }
1215
- });
1216
- }
1217
-
1218
- // src/runner/agentCallParser.ts
1219
- import { z as z6 } from "zod";
1220
- var MAX_BUFFER_BYTES = 64 * 1024;
1221
- var FENCE_RE = /```opencara-call\r?\n([\s\S]*?)\r?\n```/;
1222
- var VARIANT_SCHEMAS = {
1223
- "issue.body.set": IssueBodySetCallSchema.omit({ type: true, runId: true }),
1224
- "flow.node.config.set": FlowNodeConfigSetCallSchema.omit({
1225
- type: true,
1226
- runId: true
1227
- }),
1228
- "template.node.config.set": TemplateNodeConfigSetCallSchema.omit({
1229
- type: true,
1230
- runId: true
1231
- })
1232
- };
1233
- var KindSchema = z6.enum([
1234
- "issue.body.set",
1235
- "flow.node.config.set",
1236
- "template.node.config.set"
1237
- ]);
1238
- var AgentCallParser = class {
1239
- constructor(emit) {
1240
- this.emit = emit;
1241
- }
1242
- emit;
1243
- buffer = "";
1244
- feed(chunk) {
1245
- this.buffer += chunk;
1246
- if (this.buffer.length > MAX_BUFFER_BYTES) {
1247
- const cutoff = Math.floor(MAX_BUFFER_BYTES / 2);
1248
- const nl = this.buffer.indexOf("\n", this.buffer.length - cutoff);
1249
- this.buffer = nl >= 0 ? this.buffer.slice(nl + 1) : this.buffer.slice(-cutoff);
1250
- }
1251
- while (true) {
1252
- const m = FENCE_RE.exec(this.buffer);
1253
- if (!m) break;
1254
- const inner = m[1] ?? "";
1255
- const consumedTo = m.index + m[0].length;
1256
- this.buffer = this.buffer.slice(consumedTo);
1257
- this.tryEmit(inner);
1258
- }
1259
- }
1260
- tryEmit(inner) {
1261
- let parsed;
1262
- try {
1263
- parsed = JSON.parse(inner);
1264
- } catch {
1265
- return;
1266
- }
1267
- if (!parsed || typeof parsed !== "object") return;
1268
- const raw = parsed;
1269
- const kindResult = KindSchema.safeParse(raw.kind);
1270
- if (!kindResult.success) return;
1271
- const withCallId = {
1272
- ...raw,
1273
- callId: typeof raw.callId === "string" && raw.callId.length > 0 ? raw.callId : `call_${Date.now().toString(36)}`
1274
- };
1275
- const schema = VARIANT_SCHEMAS[kindResult.data];
1276
- const result = schema.safeParse(withCallId);
1277
- if (!result.success) return;
1278
- this.emit(result.data);
1279
- }
1280
- };
1281
-
1282
1207
  // src/commands/run.ts
1283
- var PKG_VERSION = "0.104.1";
1208
+ var PKG_VERSION = "0.105.1";
1284
1209
  var LOG_FLUSH_MS = 800;
1285
1210
  var MAX_CHUNK_SIZE = 4 * 1024;
1286
1211
  async function run(opts = {}) {
@@ -1300,12 +1225,11 @@ async function run(opts = {}) {
1300
1225
  type: "hello",
1301
1226
  platform: platform(),
1302
1227
  version: PKG_VERSION,
1303
- // Advertise the new opencara-call stdout protocol so the
1304
- // server can later gate the skill prompt to capable CLIs.
1305
- // Older CLIs without this capability would still be sent the
1306
- // skill markdown today (it doesn't crash; the fenced block
1307
- // just shows up in stdout unparsed).
1308
- capabilities: ["agent-call"],
1228
+ // Advertise ACP transport support. Pre-v0.30 devices reported
1229
+ // "agent-call" (the fenced-stdout-block protocol); since the
1230
+ // legacy path was removed, this version reports "acp" so the
1231
+ // orchestrator knows the device can handle `spec.acp` jobs.
1232
+ capabilities: ["acp"],
1309
1233
  systemInfo: collectSystemInfo()
1310
1234
  });
1311
1235
  },
@@ -1357,79 +1281,45 @@ async function executeJob(job, client) {
1357
1281
  if (flushTimer) return;
1358
1282
  flushTimer = setTimeout(flush, LOG_FLUSH_MS);
1359
1283
  };
1360
- if (job.spec.acp) {
1361
- const handle = runAcpJob({
1362
- runId,
1363
- spec: job.spec,
1364
- handlers: {
1365
- onLog: (stream, chunk) => {
1366
- pending[stream] += chunk;
1367
- scheduleFlush();
1368
- },
1369
- sendAgentCall: (req) => client.send(req)
1370
- }
1371
- });
1372
- acpControllers.set(runId, handle.controller);
1373
- try {
1374
- const result = await handle.promise;
1375
- flush();
1376
- client.send({
1377
- type: "done",
1378
- runId,
1379
- status: result.exitCode === 0 ? "succeeded" : "failed",
1380
- exitCode: result.exitCode
1381
- });
1382
- console.log(
1383
- `[opencara] job ${runId.slice(-8)} (acp) \u2192 ${result.stopReason} exit=${result.exitCode}`
1384
- );
1385
- } catch (err) {
1386
- flush();
1387
- const message = err instanceof Error ? err.message : String(err);
1388
- client.send({ type: "done", runId, status: "failed", errorMessage: message });
1389
- console.error(`[opencara] job ${runId.slice(-8)} (acp) failed`, message);
1390
- } finally {
1391
- acpControllers.delete(runId);
1392
- }
1284
+ if (!job.spec.acp) {
1285
+ flush();
1286
+ const message = `legacy stdin-JSON dispatch removed in v0.30 \u2014 orchestrator must send spec.acp. Got command: ${job.spec.command}.`;
1287
+ client.send({ type: "done", runId, status: "failed", errorMessage: message });
1288
+ console.error(`[opencara] job ${runId.slice(-8)} rejected: ${message}`);
1393
1289
  return;
1394
1290
  }
1395
- const callParser = new AgentCallParser((call) => {
1396
- switch (call.kind) {
1397
- case "issue.body.set":
1398
- client.send({ type: "agent-call", runId, ...call });
1399
- return;
1400
- case "flow.node.config.set":
1401
- client.send({ type: "agent-call", runId, ...call });
1402
- return;
1403
- case "template.node.config.set":
1404
- client.send({ type: "agent-call", runId, ...call });
1405
- return;
1406
- default: {
1407
- const exhaustive = call;
1408
- void exhaustive;
1409
- }
1410
- }
1411
- });
1412
- try {
1413
- const result = await runJob(job.spec, job.stdinJson, {
1291
+ const handle = runAcpJob({
1292
+ runId,
1293
+ spec: job.spec,
1294
+ handlers: {
1414
1295
  onLog: (stream, chunk) => {
1415
1296
  pending[stream] += chunk;
1416
1297
  scheduleFlush();
1417
- if (stream === "stdout") callParser.feed(chunk);
1418
- }
1419
- });
1298
+ },
1299
+ sendAgentCall: (req) => client.send(req)
1300
+ }
1301
+ });
1302
+ acpControllers.set(runId, handle.controller);
1303
+ try {
1304
+ const result = await handle.promise;
1420
1305
  flush();
1421
1306
  client.send({
1422
1307
  type: "done",
1423
1308
  runId,
1424
1309
  status: result.exitCode === 0 ? "succeeded" : "failed",
1425
- exitCode: result.exitCode
1310
+ exitCode: result.exitCode,
1311
+ acpSessionId: result.sessionId || null
1426
1312
  });
1427
- console.log(`[opencara] job ${runId.slice(-8)} \u2192 exit ${result.exitCode}`);
1313
+ console.log(
1314
+ `[opencara] job ${runId.slice(-8)} (acp) \u2192 ${result.stopReason} exit=${result.exitCode}`
1315
+ );
1428
1316
  } catch (err) {
1429
1317
  flush();
1430
1318
  const message = err instanceof Error ? err.message : String(err);
1431
1319
  client.send({ type: "done", runId, status: "failed", errorMessage: message });
1432
- console.error(`[opencara] job ${runId.slice(-8)} failed`, message);
1320
+ console.error(`[opencara] job ${runId.slice(-8)} (acp) failed`, message);
1321
+ } finally {
1322
+ acpControllers.delete(runId);
1433
1323
  }
1434
1324
  }
1435
1325
  function collectSystemInfo() {
@@ -74,23 +74,25 @@ async function runClaudeTurn(sessionId, state, promptText) {
74
74
  "--include-partial-messages",
75
75
  "--verbose",
76
76
  "--session-id",
77
- state.claudeSessionId,
77
+ sessionId,
78
78
  // Headless: no human in the loop to approve tool use. Matches the
79
79
  // legacy `claudeAdapter` posture in agents/kinds.ts.
80
- "--dangerously-skip-permissions",
81
- promptText
80
+ "--dangerously-skip-permissions"
82
81
  ];
83
82
  const child = spawn("claude", args, {
84
83
  cwd: state.cwd,
85
84
  env: process.env,
86
- stdio: ["ignore", "pipe", "pipe"]
85
+ stdio: ["pipe", "pipe", "pipe"]
86
+ });
87
+ child.stdin.on("error", () => {
87
88
  });
88
- const decoder2 = new FrameDecoder();
89
+ child.stdin.end(promptText);
90
+ const decoder = new FrameDecoder();
89
91
  let resolved = false;
90
92
  let stopReason = "end_turn";
91
93
  child.stdout.setEncoding("utf8");
92
94
  child.stdout.on("data", (chunk) => {
93
- const { messages, malformed } = decoder2.feed(chunk);
95
+ const { messages, malformed } = decoder.feed(chunk);
94
96
  for (const line of malformed) {
95
97
  stderr.write(`[claude-acp] malformed: ${line}
96
98
  `);
@@ -189,10 +191,12 @@ function handleInitialize(_params) {
189
191
  version: "0.0.1"
190
192
  },
191
193
  agentCapabilities: {
192
- // No session resume yet (follow-up). MCP via stdio works because
193
- // the `claude` CLI itself supports `mcpServers` in settings.json
194
- // but we don't propagate ACP's mcpServers config in this MVP.
195
- loadSession: false,
194
+ // Session resume works by passing the ACP sessionId back as
195
+ // `claude --session-id <uuid>` on the next prompt — Claude CLI
196
+ // replays its own JSONL internally. MCP via stdio is not yet
197
+ // propagated from ACP's mcpServers config (the `claude` CLI uses
198
+ // settings.json for that today; bridging is a separate change).
199
+ loadSession: true,
196
200
  mcpCapabilities: {},
197
201
  promptCapabilities: { embeddedContext: false, image: false, audio: false }
198
202
  },
@@ -201,12 +205,16 @@ function handleInitialize(_params) {
201
205
  }
202
206
  function handleNewSession(params) {
203
207
  const sessionId = randomUUID();
204
- sessions.set(sessionId, {
205
- claudeSessionId: randomUUID(),
206
- cwd: params.cwd ?? process.cwd()
207
- });
208
+ sessions.set(sessionId, { cwd: params.cwd ?? process.cwd() });
208
209
  return { sessionId };
209
210
  }
211
+ function handleLoadSession(params) {
212
+ if (typeof params.sessionId !== "string" || params.sessionId.length === 0) {
213
+ throw new Error("session/load: sessionId required");
214
+ }
215
+ sessions.set(params.sessionId, { cwd: params.cwd ?? process.cwd() });
216
+ return {};
217
+ }
210
218
  async function handlePrompt(params) {
211
219
  const state = sessions.get(params.sessionId);
212
220
  if (!state) {
@@ -219,19 +227,22 @@ async function handlePrompt(params) {
219
227
  const result = await runClaudeTurn(params.sessionId, state, promptText);
220
228
  return { stopReason: result.stopReason };
221
229
  }
222
- var decoder = new FrameDecoder();
223
- stdin.setEncoding("utf8");
224
- stdin.on("data", (chunk) => {
225
- const { messages, malformed } = decoder.feed(chunk);
226
- for (const line of malformed) {
227
- stderr.write(`[claude-acp] malformed inbound: ${line}
230
+ var isMainModule = import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.endsWith("claude-acp.ts") === true || process.argv[1]?.endsWith("claude-acp.js") === true;
231
+ if (isMainModule) {
232
+ const decoder = new FrameDecoder();
233
+ stdin.setEncoding("utf8");
234
+ stdin.on("data", (chunk) => {
235
+ const { messages, malformed } = decoder.feed(chunk);
236
+ for (const line of malformed) {
237
+ stderr.write(`[claude-acp] malformed inbound: ${line}
228
238
  `);
229
- }
230
- for (const msg of messages) void dispatch(msg);
231
- });
232
- stdin.on("end", () => {
233
- exit(0);
234
- });
239
+ }
240
+ for (const msg of messages) void dispatch(msg);
241
+ });
242
+ stdin.on("end", () => {
243
+ exit(0);
244
+ });
245
+ }
235
246
  async function dispatch(msg) {
236
247
  if (!("id" in msg) || msg.id == null) return;
237
248
  if ("result" in msg || "error" in msg) return;
@@ -244,6 +255,9 @@ async function dispatch(msg) {
244
255
  case "session/new":
245
256
  reply(req.id, handleNewSession(req.params));
246
257
  return;
258
+ case "session/load":
259
+ reply(req.id, handleLoadSession(req.params));
260
+ return;
247
261
  case "session/prompt": {
248
262
  const result = await handlePrompt(req.params);
249
263
  reply(req.id, result);
@@ -258,10 +272,17 @@ async function dispatch(msg) {
258
272
  }
259
273
  } catch (err) {
260
274
  const message = err instanceof Error ? err.message : String(err);
275
+ const isParamsError = err instanceof Error && (message.startsWith("session/prompt:") || message.startsWith("session/load:"));
261
276
  replyError(
262
277
  req.id,
263
- err instanceof Error && message.startsWith("session/prompt:") ? JSON_RPC_ERROR_INVALID_PARAMS : JSON_RPC_ERROR_INTERNAL,
278
+ isParamsError ? JSON_RPC_ERROR_INVALID_PARAMS : JSON_RPC_ERROR_INTERNAL,
264
279
  message
265
280
  );
266
281
  }
267
282
  }
283
+ export {
284
+ handleInitialize,
285
+ handleLoadSession,
286
+ handleNewSession,
287
+ sessions
288
+ };
@@ -127,7 +127,8 @@ var AcpSpecSchema = z2.object({
127
127
  systemPromptMd: z2.string(),
128
128
  userPromptMd: z2.string(),
129
129
  history: z2.array(AcpHistoryTurnSchema).default([]),
130
- pageContextJson: z2.string().optional()
130
+ pageContextJson: z2.string().optional(),
131
+ priorSessionId: z2.string().optional()
131
132
  });
132
133
  var AgentSpecSchema = z2.object({
133
134
  kind: z2.string(),
@@ -226,7 +227,12 @@ var RunDoneSchema = z3.object({
226
227
  runId: z3.string(),
227
228
  status: z3.enum(["succeeded", "failed", "cancelled"]),
228
229
  exitCode: z3.number().int().nullable().optional(),
229
- errorMessage: z3.string().optional()
230
+ errorMessage: z3.string().optional(),
231
+ /** ACP session id the agent ran under (fresh from session/new, or
232
+ * echoed from session/load). The orchestrator persists this per
233
+ * (repo, branch) so the next iteration can resume via session/load.
234
+ * Null/absent for non-ACP runs (worktree-allocate, write-session). */
235
+ acpSessionId: z3.string().nullable().optional()
230
236
  });
231
237
  var HelloAckSchema = z3.object({
232
238
  type: z3.literal("hello-ack"),
@@ -315,7 +321,6 @@ var DeviceToServerMessageSchema = z3.union([
315
321
  LogFrameSchema,
316
322
  RunDoneSchema,
317
323
  PongSchema,
318
- AgentCallSchema,
319
324
  AgentCallRequestSchema
320
325
  ]);
321
326
  var HostRegisterRequestSchema = z3.object({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencara",
3
- "version": "0.104.1",
3
+ "version": "0.105.1",
4
4
  "description": "OpenCara agent-host CLI: register a machine as an agent host and run dispatched agents.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -38,7 +38,7 @@
38
38
  "dev": "tsx watch src/bin.ts",
39
39
  "start": "node dist/bin.js",
40
40
  "typecheck": "tsc -b",
41
- "test": "node --import tsx --test --test-reporter=spec src/acp/__tests__/*.test.ts src/mcp/__tests__/*.test.ts src/runner/__tests__/*.test.ts",
41
+ "test": "node --import tsx --test --test-reporter=spec src/acp/__tests__/*.test.ts src/mcp/__tests__/*.test.ts src/runner/__tests__/*.test.ts src/bin/__tests__/*.test.ts",
42
42
  "acp:spike": "tsx src/acp/spike.ts",
43
43
  "mcp:smoke": "tsx src/mcp/smoke.ts",
44
44
  "clean": "rm -rf dist *.tsbuildinfo"