@tickernelz/paperclip-pro-adapter-utils 2026.1011.3 → 2026.1012.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.
Files changed (102) hide show
  1. package/dist/acpx-engine/execute.d.ts.map +1 -1
  2. package/dist/acpx-engine/execute.js +77 -8
  3. package/dist/acpx-engine/execute.js.map +1 -1
  4. package/dist/acpx-engine/execute.test.js +186 -14
  5. package/dist/acpx-engine/execute.test.js.map +1 -1
  6. package/dist/acpx-engine/filesystem-protocol.test.d.ts +2 -0
  7. package/dist/acpx-engine/filesystem-protocol.test.d.ts.map +1 -0
  8. package/dist/acpx-engine/filesystem-protocol.test.js +99 -0
  9. package/dist/acpx-engine/filesystem-protocol.test.js.map +1 -0
  10. package/dist/acpx-engine/run-contracts.d.ts +1 -0
  11. package/dist/acpx-engine/run-contracts.d.ts.map +1 -1
  12. package/dist/billing.d.ts +7 -0
  13. package/dist/billing.d.ts.map +1 -1
  14. package/dist/billing.js +25 -2
  15. package/dist/billing.js.map +1 -1
  16. package/dist/billing.test.js +17 -1
  17. package/dist/billing.test.js.map +1 -1
  18. package/dist/chat-file-delivery.d.ts +1 -1
  19. package/dist/chat-file-delivery.d.ts.map +1 -1
  20. package/dist/chat-file-delivery.js +4 -3
  21. package/dist/chat-file-delivery.js.map +1 -1
  22. package/dist/chat-file-delivery.test.js +2 -2
  23. package/dist/chat-file-delivery.test.js.map +1 -1
  24. package/dist/command-managed-runtime.d.ts +1 -0
  25. package/dist/command-managed-runtime.d.ts.map +1 -1
  26. package/dist/command-managed-runtime.js +3 -1
  27. package/dist/command-managed-runtime.js.map +1 -1
  28. package/dist/connection-failure.d.ts +33 -0
  29. package/dist/connection-failure.d.ts.map +1 -0
  30. package/dist/connection-failure.js +43 -0
  31. package/dist/connection-failure.js.map +1 -0
  32. package/dist/connection-failure.test.d.ts +2 -0
  33. package/dist/connection-failure.test.d.ts.map +1 -0
  34. package/dist/connection-failure.test.js +94 -0
  35. package/dist/connection-failure.test.js.map +1 -0
  36. package/dist/directory-merge-lock.test.js +4 -2
  37. package/dist/directory-merge-lock.test.js.map +1 -1
  38. package/dist/execution-target-sandbox.test.js +228 -0
  39. package/dist/execution-target-sandbox.test.js.map +1 -1
  40. package/dist/execution-target.d.ts +4 -0
  41. package/dist/execution-target.d.ts.map +1 -1
  42. package/dist/execution-target.js +46 -13
  43. package/dist/execution-target.js.map +1 -1
  44. package/dist/execution-target.test.js +1 -1
  45. package/dist/execution-target.test.js.map +1 -1
  46. package/dist/github-launcher-environment.test.js +37 -0
  47. package/dist/github-launcher-environment.test.js.map +1 -1
  48. package/dist/index.d.ts +3 -3
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +2 -2
  51. package/dist/index.js.map +1 -1
  52. package/dist/paperclip-runner-permissions.d.ts +15 -4
  53. package/dist/paperclip-runner-permissions.d.ts.map +1 -1
  54. package/dist/paperclip-runner-permissions.js +29 -3
  55. package/dist/paperclip-runner-permissions.js.map +1 -1
  56. package/dist/paperclip-runner-permissions.test.js +34 -1
  57. package/dist/paperclip-runner-permissions.test.js.map +1 -1
  58. package/dist/remote-managed-runtime.d.ts.map +1 -1
  59. package/dist/remote-managed-runtime.js +18 -2
  60. package/dist/remote-managed-runtime.js.map +1 -1
  61. package/dist/sandbox-callback-bridge.d.ts.map +1 -1
  62. package/dist/sandbox-callback-bridge.js +1 -0
  63. package/dist/sandbox-callback-bridge.js.map +1 -1
  64. package/dist/sandbox-callback-bridge.test.js +113 -16
  65. package/dist/sandbox-callback-bridge.test.js.map +1 -1
  66. package/dist/sandbox-managed-runtime.d.ts +3 -0
  67. package/dist/sandbox-managed-runtime.d.ts.map +1 -1
  68. package/dist/sandbox-managed-runtime.js +8 -1
  69. package/dist/sandbox-managed-runtime.js.map +1 -1
  70. package/dist/server-utils.d.ts +6 -3
  71. package/dist/server-utils.d.ts.map +1 -1
  72. package/dist/server-utils.js +39 -9
  73. package/dist/server-utils.js.map +1 -1
  74. package/dist/server-utils.test.js +35 -1
  75. package/dist/server-utils.test.js.map +1 -1
  76. package/dist/ssh-fixture.test.js +172 -0
  77. package/dist/ssh-fixture.test.js.map +1 -1
  78. package/dist/ssh.d.ts +5 -0
  79. package/dist/ssh.d.ts.map +1 -1
  80. package/dist/ssh.js +138 -20
  81. package/dist/ssh.js.map +1 -1
  82. package/dist/types.d.ts +76 -1
  83. package/dist/types.d.ts.map +1 -1
  84. package/dist/usage-checkpoint.d.ts +11 -0
  85. package/dist/usage-checkpoint.d.ts.map +1 -0
  86. package/dist/usage-checkpoint.js +165 -0
  87. package/dist/usage-checkpoint.js.map +1 -0
  88. package/dist/usage-checkpoint.test.d.ts +2 -0
  89. package/dist/usage-checkpoint.test.d.ts.map +1 -0
  90. package/dist/usage-checkpoint.test.js +131 -0
  91. package/dist/usage-checkpoint.test.js.map +1 -0
  92. package/dist/workspace-restore-diagnostics.d.ts +8 -1
  93. package/dist/workspace-restore-diagnostics.d.ts.map +1 -1
  94. package/dist/workspace-restore-diagnostics.js +35 -8
  95. package/dist/workspace-restore-diagnostics.js.map +1 -1
  96. package/dist/workspace-restore-diagnostics.test.js +31 -1
  97. package/dist/workspace-restore-diagnostics.test.js.map +1 -1
  98. package/node_modules/acpx/dist/live-checkpoint-ClPCSdrW.js +3 -0
  99. package/package.json +31 -3
  100. package/node_modules/@esbuild/linux-x64/README.md +0 -3
  101. package/node_modules/@esbuild/linux-x64/bin/esbuild +0 -0
  102. package/node_modules/@esbuild/linux-x64/package.json +0 -20
@@ -29,12 +29,14 @@ import { withWorkspaceRestoreDiagnostics, withWorkspaceRestoreStep } from "../wo
29
29
  import { resolveReferencedSourceIgnore } from "../sandbox-managed-runtime.js";
30
30
  import { getActiveStepContext, runWithRuntimeParent, SANDBOX_STARTUP_SPAN_ATTRS, } from "./startup-timing.js";
31
31
  const tempRoots = [];
32
+ let defaultGeminiTestHome;
32
33
  async function makeTempRoot() {
33
34
  const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-acpx-skills-"));
34
35
  tempRoots.push(root);
35
36
  return root;
36
37
  }
37
38
  afterEach(async () => {
39
+ defaultGeminiTestHome = undefined;
38
40
  // A remote run stages a process-session bridge whose detached event writer can
39
41
  // still be flushing a trailing event file into `.../process-sessions/<id>/events`
40
42
  // when the run's own best-effort `client.remove(sessionDir)` (which production
@@ -114,6 +116,13 @@ function buildRuntime(onSetConfigOption, onEnsureSession, onStartTurn, onSetMode
114
116
  };
115
117
  }
116
118
  async function runExecutor(config, options = {}) {
119
+ // Skill reconciliation must never inspect or modify the developer's real
120
+ // Gemini home. Keep one private home across this test's resumed turns.
121
+ const configuredEnv = config.env;
122
+ if (config.agent === "gemini" && !(typeof configuredEnv?.HOME === "string" && configuredEnv.HOME.trim())) {
123
+ defaultGeminiTestHome ??= await makeTempRoot();
124
+ config = { ...config, env: { ...configuredEnv, HOME: defaultGeminiTestHome } };
125
+ }
117
126
  const runtimeOptions = [];
118
127
  const configOptions = [];
119
128
  const sessionModes = [];
@@ -122,6 +131,7 @@ async function runExecutor(config, options = {}) {
122
131
  const meta = [];
123
132
  const logs = [];
124
133
  const events = [];
134
+ const receipts = [];
125
135
  const execute = createAcpxEngineExecutor({
126
136
  ...(options.prepareRemoteManagedHome
127
137
  ? { prepareRemoteManagedHome: options.prepareRemoteManagedHome }
@@ -142,6 +152,7 @@ async function runExecutor(config, options = {}) {
142
152
  context: options.context ?? {},
143
153
  executionTransport: options.executionTransport,
144
154
  authToken: options.authToken,
155
+ agentIdentity: options.agentIdentity,
145
156
  executionTarget: options.executionTarget,
146
157
  runtimeMcp: options.runtimeMcp,
147
158
  startupTraceContext: options.startupTraceContext,
@@ -154,9 +165,10 @@ async function runExecutor(config, options = {}) {
154
165
  onEvent: async (event) => {
155
166
  events.push(event);
156
167
  },
168
+ onUsage: async (receipt) => { receipts.push(receipt); },
157
169
  });
158
170
  expect(result.exitCode).toBe(0);
159
- return { logs, meta, events, runtimeOptions, configOptions, sessionModes, sessionInputs, turnInputs, result };
171
+ return { logs, meta, events, receipts, runtimeOptions, configOptions, sessionModes, sessionInputs, turnInputs, result };
160
172
  }
161
173
  // Under `vi.useFakeTimers()`, setup before `ensureSession` still performs real
162
174
  // filesystem work. Advancing the fake clock before that work reaches the
@@ -438,7 +450,7 @@ describe("shared ACPX engine runtime behavior", () => {
438
450
  expect(turnStartedBeforeProcessIdentity).toBe(false);
439
451
  });
440
452
  it("sets Codex model, effort, and fast mode through CODEX_CONFIG without session config calls", async () => {
441
- const { configOptions, meta } = await runExecutor({
453
+ const { configOptions, meta, receipts, result } = await runExecutor({
442
454
  agent: "codex",
443
455
  model: "gpt-5.6-sol",
444
456
  modelReasoningEffort: "high",
@@ -451,8 +463,23 @@ describe("shared ACPX engine runtime behavior", () => {
451
463
  features: { fast_mode: true },
452
464
  });
453
465
  expect(configOptions).toEqual([]);
466
+ expect(result.pricingContext).toEqual({ serviceTier: "fast" });
467
+ expect(receipts).toContainEqual(expect.objectContaining({ pricingContext: { serviceTier: "fast" }, complete: true }));
454
468
  expect(meta[0]?.commandNotes).toContain("Requested ACPX model: gpt-5.6-sol (set via CODEX_CONFIG at startup).");
455
469
  });
470
+ it("keeps identity and scoped API access without exposing configured service tokens to Codex shells", async () => {
471
+ const { meta } = await runExecutor({ agent: "codex", env: { MY_SERVICE_TOKEN: "assigned-tool-token" } }, {
472
+ authToken: "assigned-run-token",
473
+ agentIdentity: { keyId: "sha256:test", publicKeyPem: "public", privateKeyPem: "private" },
474
+ });
475
+ const config = JSON.parse(String((meta[0]?.env).CODEX_CONFIG));
476
+ expect(config.shell_environment_policy.include_only).toEqual(expect.arrayContaining([
477
+ "PAPERCLIP_API_KEY", "PAPERCLIP_AGENT_PRIVATE_KEY",
478
+ ]));
479
+ expect(config.shell_environment_policy.include_only).not.toContain("MY_SERVICE_TOKEN");
480
+ expect(JSON.stringify(config)).not.toContain("assigned-run-token");
481
+ expect(JSON.stringify(config)).not.toContain("assigned-tool-token");
482
+ });
456
483
  it("forwards arbitrary Codex model IDs verbatim without picker-dependent session config", async () => {
457
484
  const arbitraryModel = "gpt-999-test-does-not-exist";
458
485
  const { configOptions, meta } = await runExecutor({
@@ -500,7 +527,7 @@ describe("shared ACPX engine runtime behavior", () => {
500
527
  text: "[paperclip] Ignoring invalid user CODEX_CONFIG while applying runtime Codex settings; expected a JSON object.\n",
501
528
  });
502
529
  });
503
- it("keeps Claude startup model handling and Gemini session config handling unchanged", async () => {
530
+ it("sets Claude and Gemini models at startup without unsupported picker calls", async () => {
504
531
  const claude = await runExecutor({ agent: "claude", model: "claude-opus-4-7" });
505
532
  expect((claude.meta[0]?.env).ANTHROPIC_MODEL).toBe("claude-opus-4-7");
506
533
  expect(claude.configOptions).toEqual([]);
@@ -508,9 +535,10 @@ describe("shared ACPX engine runtime behavior", () => {
508
535
  agent: "gemini",
509
536
  model: "gemini-2.5-pro",
510
537
  thinkingEffort: "high",
538
+ env: { GEMINI_MODEL: "stale-model" },
511
539
  });
540
+ expect((gemini.meta[0]?.env).GEMINI_MODEL).toBe("gemini-2.5-pro");
512
541
  expect(gemini.configOptions).toEqual([
513
- { key: "model", value: "gemini-2.5-pro" },
514
542
  { key: "effort", value: "high" },
515
543
  ]);
516
544
  });
@@ -1293,6 +1321,45 @@ describe("shared ACPX engine runtime behavior", () => {
1293
1321
  const statusLine = logs.find((entry) => entry.text.includes('"acpx.status"'));
1294
1322
  expect(statusLine?.text).toContain('"cost"');
1295
1323
  });
1324
+ it.each(["checkpoint", "terminal"])("returns received usage after the %s receipt sink rejects and still closes the runtime", async (failedSave) => {
1325
+ const root = await makeTempRoot();
1326
+ const close = vi.fn(async () => { });
1327
+ const cancel = vi.fn(async () => { });
1328
+ let reads = 0;
1329
+ const execute = createAcpxEngineExecutor({
1330
+ resolveBillingIdentity: () => ({ provider: "anthropic", biller: "anthropic", billingType: "api" }),
1331
+ createRuntime: () => ({
1332
+ ...buildRuntime(),
1333
+ getStatus: async () => ++reads === 1
1334
+ ? { usage: { cost: { amount: 0.4, currency: "USD" } } }
1335
+ : { usage: { cumulative: { inputTokens: 120, outputTokens: 4500, cachedReadTokens: 900, cachedWriteTokens: 30 }, cost: { amount: 1.15, currency: "USD" } } },
1336
+ startTurn: () => ({
1337
+ events: (async function* () {
1338
+ yield { type: "status", text: "usage", tag: "usage_update",
1339
+ breakdown: { inputTokens: 100, outputTokens: 4000, cachedReadTokens: 800, cachedWriteTokens: 20 }, cost: { amount: 1.1, currency: "USD" } };
1340
+ })(),
1341
+ result: Promise.resolve({ status: "completed", stopReason: "end_turn" }), cancel,
1342
+ }), close,
1343
+ }),
1344
+ });
1345
+ const onUsage = vi.fn(async (receipt) => {
1346
+ if (failedSave === "checkpoint" || receipt.complete)
1347
+ throw new Error("Receipt storage unavailable");
1348
+ });
1349
+ const result = await execute({ runId: `failed-${failedSave}-save`, agent: { id: "agent-1", companyId: "company-1" },
1350
+ runtime: {}, config: { agent: "custom", agentCommand: "node ./fake-acp.js", stateDir: path.join(root, "state") },
1351
+ context: {}, onMeta: async () => { }, onLog: async () => { }, onUsage });
1352
+ expect(result).toMatchObject({ exitCode: 1, usageBasis: "per_run", usageComplete: failedSave === "terminal",
1353
+ provider: "anthropic", biller: "anthropic", billingType: "api" });
1354
+ expect(result.errorMessage).toContain("Receipt storage unavailable");
1355
+ expect(result.usage).toEqual(failedSave === "terminal"
1356
+ ? { inputTokens: 150, outputTokens: 4500, cachedInputTokens: 900 }
1357
+ : { inputTokens: 120, outputTokens: 4000, cachedInputTokens: 800 });
1358
+ expect(result.costUsd).toBeCloseTo(failedSave === "terminal" ? 0.75 : 0.7);
1359
+ expect(onUsage).toHaveBeenCalledTimes(failedSave === "terminal" ? 2 : 1);
1360
+ expect(cancel).toHaveBeenCalledTimes(1);
1361
+ expect(close).toHaveBeenCalledTimes(1);
1362
+ });
1296
1363
  it("falls back to usage_update events when the runtime lacks getStatus", async () => {
1297
1364
  const root = await makeTempRoot();
1298
1365
  const stateDir = path.join(root, "state");
@@ -1339,6 +1406,55 @@ describe("shared ACPX engine runtime behavior", () => {
1339
1406
  expect(result.provider).toBe("acpx");
1340
1407
  expect(result.billingType).toBe("unknown");
1341
1408
  });
1409
+ it.each(["failed", "cancelled"])("keeps %s in-stream usage provisional without a final receipt", async (status) => {
1410
+ const root = await makeTempRoot();
1411
+ const stateDir = path.join(root, "state");
1412
+ const execute = createAcpxEngineExecutor({
1413
+ createRuntime: () => ({
1414
+ ensureSession: async () => ({
1415
+ backendSessionId: "backend-session",
1416
+ agentSessionId: "agent-session",
1417
+ runtimeSessionName: "runtime-session",
1418
+ }),
1419
+ startTurn: () => ({
1420
+ events: (async function* () {
1421
+ yield {
1422
+ type: "status",
1423
+ text: "usage",
1424
+ tag: "usage_update",
1425
+ cost: { amount: 0.31, currency: "USD" },
1426
+ breakdown: { inputTokens: 40, outputTokens: 700, cachedReadTokens: 60 },
1427
+ };
1428
+ })(),
1429
+ result: Promise.resolve({ status, error: { code: "INTERNAL", message: "fixture failure" } }),
1430
+ cancel: async () => { },
1431
+ }),
1432
+ close: async () => { },
1433
+ }),
1434
+ });
1435
+ const onUsage = vi.fn();
1436
+ const result = await execute({
1437
+ runId: "run-usage-event-fallback",
1438
+ agent: {
1439
+ id: "agent-1",
1440
+ companyId: "company-1",
1441
+ },
1442
+ runtime: {},
1443
+ config: { agent: "custom", agentCommand: "node ./fake-acp.js", stateDir },
1444
+ context: {},
1445
+ onLog: async () => { },
1446
+ onMeta: async () => { },
1447
+ onUsage,
1448
+ });
1449
+ expect(result.exitCode).not.toBe(0);
1450
+ expect(result.usageComplete).toBe(false);
1451
+ expect(onUsage).toHaveBeenLastCalledWith(expect.objectContaining({ complete: false, costUsd: 0.31 }));
1452
+ expect(result.usage).toEqual({ inputTokens: 40, outputTokens: 700, cachedInputTokens: 60 });
1453
+ expect(result.usageBasis).toBe("per_run");
1454
+ expect(result.costUsd).toBeCloseTo(0.31);
1455
+ expect(result.provider).toBe("acpx");
1456
+ expect(result.billingType).toBe("unknown");
1457
+ });
1342
1458
  it.skipIf(process.platform === "win32")("materializes ACPX Claude skills without symlinked descendants", async () => {
1343
1459
  const root = await makeTempRoot();
1344
1460
  const skillRoot = path.join(root, "skills");
@@ -1411,6 +1527,29 @@ describe("shared ACPX engine runtime behavior", () => {
1411
1527
  });
1412
1528
  expect(await pathExists(path.join(codexHome, "skills", operational.runtimeName, "SKILL.md"))).toBe(true);
1413
1529
  });
1530
+ it.each(["codex", "gemini"])("refreshes the %s skill root when a connection home changes between turns", async (agent) => {
1531
+ const root = await makeTempRoot();
1532
+ const operational = {
1533
+ ...await createSkill(path.join(root, "skills"), "paperclip"),
1534
+ key: "paperclipai/paperclip/paperclip",
1535
+ };
1536
+ const homes = [path.join(root, "first-home"), path.join(root, "next-home")];
1537
+ for (const home of homes) {
1538
+ const { meta } = await runExecutor({
1539
+ agent,
1540
+ stateDir: path.join(root, "state"),
1541
+ env: agent === "codex" ? { CODEX_HOME: home } : { HOME: home },
1542
+ paperclipRuntimeSkills: [operational],
1543
+ paperclipSkillSync: { desiredSkills: [] },
1544
+ });
1545
+ const skillsHome = agent === "codex" ? path.join(home, "skills") : path.join(home, ".gemini", "skills");
1546
+ expect(String(meta[0]?.prompt ?? "")).toContain(`Skill root for this run: ${skillsHome}`);
1547
+ expect(String(meta[0]?.prompt ?? "")).toContain("Resolve referenced scripts and files relative to that skill");
1548
+ if (home === homes[1])
1549
+ expect(String(meta[0]?.prompt ?? "")).not.toContain(homes[0]);
1550
+ await fs.rm(home, { recursive: true, force: true });
1551
+ }
1552
+ });
1414
1553
  it.skipIf(process.platform === "win32")("removes legacy ACPX Codex skill symlinks when a skill is no longer desired", async () => {
1415
1554
  const root = await makeTempRoot();
1416
1555
  const skillRoot = path.join(root, "skills");
@@ -2378,7 +2517,9 @@ describe("shared ACPX engine runtime behavior", () => {
2378
2517
  });
2379
2518
  it.each(["claude", "codex", "grok", "gemini", "kimi"])("resumes %s with added and removed MCP servers and current credentials", async (agent) => {
2380
2519
  const root = await makeTempRoot();
2381
- const config = { agent, cwd: root, stateDir: path.join(root, "state"), paperclipRuntimeSkills: [], paperclipSkillSync: { desiredSkills: [] } };
2520
+ // This fixture injects its runtime. Keep session identity independent of
2521
+ // host CLI discovery/version probes while testing MCP credential changes.
2522
+ const config = { agent, agentCommand: "node ./fake-acp.js", cwd: root, stateDir: path.join(root, "state"), paperclipRuntimeSkills: [], paperclipSkillSync: { desiredSkills: [] } };
2382
2523
  const first = await runExecutor(config);
2383
2524
  const server = { name: "github", url: "https://example.test/github/mcp", connectionId: "github", token: "current-token" };
2384
2525
  const second = await runExecutor(config, {
@@ -2469,6 +2610,20 @@ describe("findAncestorBin", () => {
2469
2610
  });
2470
2611
  });
2471
2612
  describe("gemini ACP flag selection", () => {
2613
+ it("uses the canonical local Gemini workspace for ACP filesystem requests", async () => {
2614
+ const root = await makeTempRoot();
2615
+ const workspace = path.join(root, "workspace");
2616
+ const alias = path.join(root, "workspace-alias");
2617
+ await fs.mkdir(workspace);
2618
+ await fs.symlink(workspace, alias, process.platform === "win32" ? "junction" : "dir");
2619
+ const { runtimeOptions, sessionInputs } = await runExecutor({
2620
+ agent: "gemini", agentCommand: "node ./fake-acp.js", cwd: alias,
2621
+ stateDir: path.join(root, "state"),
2622
+ });
2623
+ const canonical = await fs.realpath(workspace);
2624
+ expect(runtimeOptions[0].cwd).toBe(canonical);
2625
+ expect(sessionInputs[0].cwd).toBe(canonical);
2626
+ });
2472
2627
  it("parses semantic version parts from gemini --version output", () => {
2473
2628
  expect(parseGeminiVersionParts("0.30.0")).toEqual([0, 30, 0]);
2474
2629
  expect(parseGeminiVersionParts("gemini-cli v1.2.3\n")).toEqual([1, 2, 3]);
@@ -3043,6 +3198,12 @@ describe("ACPX engine remote sandbox staging seam (PR 1: workspace + cwd)", () =
3043
3198
  expect(sessionInputs[0]?.cwd).toBe(remoteCwd);
3044
3199
  expect(sessionInputs[0]?.cwd).not.toBe(localCwd);
3045
3200
  });
3201
+ it("preserves Gemini's in-sandbox workspace without host canonicalization", async () => {
3202
+ const { stateDir, localCwd, remoteCwd, executionTarget } = await setupRemoteSandbox();
3203
+ const { sessionInputs, runtimeOptions } = await runExecutor({ agent: "gemini", agentCommand: "node ./fake-acp.js", stateDir, cwd: localCwd }, { authToken: "real-run-jwt", executionTarget });
3204
+ expect(runtimeOptions[0]?.cwd).toBe(remoteCwd);
3205
+ expect(sessionInputs[0]?.cwd).toBe(remoteCwd);
3206
+ });
3046
3207
  it("test_remote_warm_handle_reused_after_cwd_change", async () => {
3047
3208
  const { stateDir, localCwd, remoteCwd, executionTarget } = await setupRemoteSandbox();
3048
3209
  const ensureInputs = [];
@@ -5388,9 +5549,10 @@ describe("ACPX engine run lifecycle corrections (F1: settle every failure after
5388
5549
  // next same-session run.
5389
5550
  expect(stagingLocks.size).toBe(0);
5390
5551
  });
5391
- it("test_prompt_build_failure_returns_error_result_with_phase_prepare_turn", async () => {
5552
+ it.each(["prompt", "metadata", "dispatch"])("records whether provider work could have started on %s failure", async (failure) => {
5392
5553
  const root = await makeTempRoot();
5393
5554
  const close = vi.fn(async () => { });
5555
+ const startTurn = vi.fn(() => { throw new Error("dispatch failed"); });
5394
5556
  const execute = createAcpxEngineExecutor({
5395
5557
  createRuntime: () => ({
5396
5558
  ensureSession: async () => ({
@@ -5398,11 +5560,7 @@ describe("ACPX engine run lifecycle corrections (F1: settle every failure after
5398
5560
  agentSessionId: "agent-session",
5399
5561
  runtimeSessionName: "runtime-session",
5400
5562
  }),
5401
- startTurn: () => ({
5402
- events: (async function* () { })(),
5403
- result: Promise.resolve({ status: "completed", stopReason: "end_turn" }),
5404
- cancel: async () => { },
5405
- }),
5563
+ startTurn,
5406
5564
  close,
5407
5565
  }),
5408
5566
  });
@@ -5412,7 +5570,9 @@ describe("ACPX engine run lifecycle corrections (F1: settle every failure after
5412
5570
  Object.defineProperty(context, "paperclipSessionHandoffMarkdown", {
5413
5571
  enumerable: false,
5414
5572
  get() {
5415
- throw new Error("prompt build boom");
5573
+ if (failure === "prompt")
5574
+ throw new Error("prompt build boom");
5575
+ return undefined;
5416
5576
  },
5417
5577
  });
5418
5578
  const result = await execute({
@@ -5422,10 +5582,20 @@ describe("ACPX engine run lifecycle corrections (F1: settle every failure after
5422
5582
  config: { agent: "custom", agentCommand: "node ./fake-acp.js", stateDir: path.join(root, "state") },
5423
5583
  context,
5424
5584
  onLog: async () => { },
5425
- onMeta: async () => { },
5585
+ onMeta: async () => { if (failure === "metadata")
5586
+ throw new Error("metadata failed"); },
5426
5587
  });
5427
5588
  expect(result.exitCode).toBe(1);
5428
- expect(result.resultJson?.phase).toBe("prepare_turn");
5589
+ if (failure === "dispatch") {
5590
+ expect(startTurn).toHaveBeenCalledTimes(1);
5591
+ expect(result.executionRecovery).toBeUndefined();
5592
+ expect(result.usageComplete).toBe(false);
5593
+ }
5594
+ else {
5595
+ expect(result.resultJson?.phase).toBe("prepare_turn");
5596
+ expect(startTurn).not.toHaveBeenCalled();
5597
+ expect(result.executionRecovery).toEqual({ kind: "bootstrap", providerWorkStarted: false });
5598
+ }
5429
5599
  // The established runtime is closed on the pre-turn failure, so no session
5430
5600
  // leaks.
5431
5601
  expect(close).toHaveBeenCalledTimes(1);
@@ -6284,6 +6454,7 @@ describe("ACPX engine sandbox bridge run-disposition seam (fail-closed)", () =>
6284
6454
  // The lost channel overrides the nominally completed terminal to a failure.
6285
6455
  expect(result.exitCode).not.toBe(0);
6286
6456
  expect(result.errorCode).toBe("duplex_channel_lost");
6457
+ expect(result.usageComplete).toBe(false);
6287
6458
  // The message carries only the typed loss reason, not raw provider text.
6288
6459
  expect(result.errorMessage).toContain("provider_exit");
6289
6460
  expect(result.resultJson).toMatchObject({ status: "failed" });
@@ -6300,6 +6471,7 @@ describe("ACPX engine sandbox bridge run-disposition seam (fail-closed)", () =>
6300
6471
  const result = await runRemote(fake.handle, runtime, sandbox);
6301
6472
  expect(result.exitCode).toBe(0);
6302
6473
  expect(result.errorCode ?? null).toBeNull();
6474
+ expect(result.usageComplete).toBe(true);
6303
6475
  // The atomic settle step marked the orderly completion for the
6304
6476
  // success-eligible terminal.
6305
6477
  expect(fake.settleRunDisposition).toHaveBeenCalledTimes(1);