@agent-compose/sdk 0.8.2 → 0.8.4

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 (43) hide show
  1. package/dist/agent/__tests__/perf-sampler.test.d.ts +10 -0
  2. package/dist/agent/agent-context.d.ts +1 -1
  3. package/dist/agent/agent-loop.d.ts +9 -1
  4. package/dist/agent/desktop-open.d.ts +184 -0
  5. package/dist/agent/perf-sampler.d.ts +99 -0
  6. package/dist/agent/services-manifest.d.ts +88 -0
  7. package/dist/agent/services-restore.d.ts +58 -0
  8. package/dist/client.d.ts +164 -8
  9. package/dist/display.d.ts +17 -0
  10. package/dist/index.d.ts +14 -5
  11. package/dist/index.js +1393 -53
  12. package/dist/runtimes/_cli-agent.d.ts +359 -2
  13. package/dist/runtimes/claude-code.d.ts +12 -0
  14. package/dist/runtimes/codex.d.ts +8 -0
  15. package/dist/runtimes/openai-desktop.js +1329 -53
  16. package/dist/runtimes/session-env.test.d.ts +14 -0
  17. package/dist/sandbox.d.ts +1 -1
  18. package/dist/types/api-conversations.d.ts +309 -1
  19. package/dist/types/api-factory.d.ts +115 -10
  20. package/dist/types/api-runs.d.ts +21 -0
  21. package/dist/types/protocol.d.ts +44 -1
  22. package/dist/types/runtime.d.ts +120 -0
  23. package/package.json +1 -1
  24. package/src/agent/agent-context.ts +100 -11
  25. package/src/agent/agent-loop.ts +15 -3
  26. package/src/agent/desktop-open.ts +418 -0
  27. package/src/agent/perf-sampler.ts +202 -0
  28. package/src/agent/services-manifest.ts +356 -0
  29. package/src/agent/services-restore.ts +195 -0
  30. package/src/client.ts +328 -12
  31. package/src/display.ts +44 -1
  32. package/src/index.ts +65 -2
  33. package/src/runtimes/_cli-agent.ts +911 -35
  34. package/src/runtimes/claude-code.ts +198 -14
  35. package/src/runtimes/codex.ts +58 -1
  36. package/src/sandbox/providers/e2b.ts +29 -1
  37. package/src/sandbox/providers/local.ts +16 -4
  38. package/src/sandbox.ts +1 -1
  39. package/src/types/api-conversations.ts +307 -3
  40. package/src/types/api-factory.ts +118 -10
  41. package/src/types/api-runs.ts +23 -0
  42. package/src/types/protocol.ts +44 -1
  43. package/src/types/runtime.ts +122 -0
package/dist/index.js CHANGED
@@ -1608,6 +1608,9 @@ class AgentComposeClient {
1608
1608
  async getRunFunding(runId) {
1609
1609
  return this.fetch(`/api/v1/workflows/${encodeURIComponent(runId)}/funding`);
1610
1610
  }
1611
+ async getRunStepUsage(runId) {
1612
+ return this.fetch(`/api/v1/workflows/${encodeURIComponent(runId)}/step-usage`);
1613
+ }
1611
1614
  async reportEvent(runId, input) {
1612
1615
  const body = {
1613
1616
  ...input,
@@ -1663,6 +1666,8 @@ class AgentComposeClient {
1663
1666
  q2.set("revision", String(opts.revision));
1664
1667
  if (opts?.branch !== undefined)
1665
1668
  q2.set("branch", opts.branch);
1669
+ if (opts?.at !== undefined)
1670
+ q2.set("at", opts.at);
1666
1671
  const buf = await this.fetch(`/api/v1/factories/${encodeURIComponent(factorySlug)}/files/content?${q2}`, { responseType: "arrayBuffer" });
1667
1672
  return new Uint8Array(buf);
1668
1673
  }
@@ -1695,6 +1700,22 @@ class AgentComposeClient {
1695
1700
  q2.set("cursor", opts.cursor);
1696
1701
  return this.fetch(`/api/v1/sessions?${q2}`);
1697
1702
  }
1703
+ getSessionPerfHistory(conversationId, opts) {
1704
+ const q2 = new URLSearchParams;
1705
+ if (opts?.limit !== undefined)
1706
+ q2.set("limit", String(opts.limit));
1707
+ if (opts?.cursor)
1708
+ q2.set("cursor", opts.cursor);
1709
+ return this.fetch(`/api/v1/conversations/${encodeURIComponent(conversationId)}/perf-history${q2.toString() ? `?${q2}` : ""}`);
1710
+ }
1711
+ getFactoryPerfSummary(factorySlug, opts) {
1712
+ const q2 = new URLSearchParams;
1713
+ if (opts?.from)
1714
+ q2.set("from", opts.from);
1715
+ if (opts?.to)
1716
+ q2.set("to", opts.to);
1717
+ return this.fetch(`/api/v1/factories/${encodeURIComponent(factorySlug)}/perf-summary${q2.toString() ? `?${q2}` : ""}`);
1718
+ }
1698
1719
  getConversation(id, opts) {
1699
1720
  const q2 = opts?.limit !== undefined ? `?limit=${opts.limit}` : "";
1700
1721
  return this.fetch(`/api/v1/conversations/${encodeURIComponent(id)}${q2}`);
@@ -1738,6 +1759,27 @@ class AgentComposeClient {
1738
1759
  postSessionChannelMessage(channelId, input) {
1739
1760
  return this.fetch(`/api/v1/conversations/${encodeURIComponent(channelId)}/session-messages`, { method: "POST", body: { text: input.text, ...input.threadRootId ? { threadRootId: input.threadRootId } : {} } });
1740
1761
  }
1762
+ sendSessionMessage(input) {
1763
+ return this.fetch("/api/v1/session-messages", {
1764
+ method: "POST",
1765
+ body: input
1766
+ });
1767
+ }
1768
+ claimSessionBranch(input) {
1769
+ return this.fetch("/api/v1/session-branch-claims", {
1770
+ method: "POST",
1771
+ body: { target: input.target, ...input.reason ? { reason: input.reason } : {} }
1772
+ });
1773
+ }
1774
+ releaseSessionBranchClaims(input = {}) {
1775
+ return this.fetch("/api/v1/session-branch-claims/release", {
1776
+ method: "POST",
1777
+ body: { ...input.target ? { target: input.target } : {} }
1778
+ });
1779
+ }
1780
+ getSessionBranchClaimState(conversationId) {
1781
+ return this.fetch(`/api/v1/conversations/${encodeURIComponent(conversationId)}/branch-claim`);
1782
+ }
1741
1783
  openPreview(conversationId, input) {
1742
1784
  return this.fetch(`/api/v1/conversations/${encodeURIComponent(conversationId)}/preview`, {
1743
1785
  method: "POST",
@@ -1766,8 +1808,33 @@ class AgentComposeClient {
1766
1808
  forkSession(conversationId, input) {
1767
1809
  return this.fetch(`/api/v1/conversations/${encodeURIComponent(conversationId)}/fork`, { method: "POST", body: input?.title ? { title: input.title } : {} });
1768
1810
  }
1769
- getSessionChanges(conversationId) {
1770
- return this.fetch(`/api/v1/conversations/${encodeURIComponent(conversationId)}/changes`);
1811
+ getSessionChanges(conversationId, opts = {}) {
1812
+ const q2 = new URLSearchParams;
1813
+ if (opts.after !== undefined)
1814
+ q2.set("after", opts.after);
1815
+ if (opts.limit !== undefined)
1816
+ q2.set("limit", String(opts.limit));
1817
+ const tail = q2.toString();
1818
+ return this.fetch(`/api/v1/conversations/${encodeURIComponent(conversationId)}/changes${tail ? `?${tail}` : ""}`);
1819
+ }
1820
+ async getSessionChangeContent(conversationId, path, opts = {}) {
1821
+ const q2 = new URLSearchParams({ path });
1822
+ if (opts.side !== undefined)
1823
+ q2.set("side", opts.side);
1824
+ const buf = await this.fetch(`/api/v1/conversations/${encodeURIComponent(conversationId)}/changes/content?${q2}`, { responseType: "arrayBuffer" });
1825
+ return new Uint8Array(buf);
1826
+ }
1827
+ publishReviewNotes(conversationId, input) {
1828
+ return this.fetch(`/api/v1/conversations/${encodeURIComponent(conversationId)}/review-notes`, {
1829
+ method: "POST",
1830
+ body: {
1831
+ notes: input.notes,
1832
+ ...input.suggestions !== undefined ? { suggestions: input.suggestions } : {}
1833
+ }
1834
+ });
1835
+ }
1836
+ getReviewGitCredential(conversationId) {
1837
+ return this.fetch(`/api/v1/conversations/${encodeURIComponent(conversationId)}/review-git-credential`, { method: "POST" });
1771
1838
  }
1772
1839
  mergeSessionChanges(conversationId, opts = {}) {
1773
1840
  return this.fetch(`/api/v1/conversations/${encodeURIComponent(conversationId)}/changes/merge`, { method: "POST", body: opts.branch ? { branch: opts.branch } : {} });
@@ -1775,6 +1842,15 @@ class AgentComposeClient {
1775
1842
  discardSessionChanges(conversationId, opts = {}) {
1776
1843
  return this.fetch(`/api/v1/conversations/${encodeURIComponent(conversationId)}/changes/discard`, { method: "POST", body: opts.branch ? { branch: opts.branch } : {} });
1777
1844
  }
1845
+ getSessionChangePreflight(conversationId) {
1846
+ return this.fetch(`/api/v1/conversations/${encodeURIComponent(conversationId)}/changes/preflight`);
1847
+ }
1848
+ rebaseSessionChanges(conversationId, opts = {}) {
1849
+ return this.fetch(`/api/v1/conversations/${encodeURIComponent(conversationId)}/changes/rebase`, { method: "POST", body: opts.branch ? { branch: opts.branch } : {} });
1850
+ }
1851
+ setSessionAutoRebase(conversationId, enabled) {
1852
+ return this.fetch(`/api/v1/conversations/${encodeURIComponent(conversationId)}/changes/autorebase`, { method: "POST", body: { enabled } });
1853
+ }
1778
1854
  async getConversationMembers(conversationId) {
1779
1855
  const body = await this.fetch(`/api/v1/conversations/${encodeURIComponent(conversationId)}/members`);
1780
1856
  return body.members;
@@ -1829,6 +1905,28 @@ class AgentComposeClient {
1829
1905
  q2.set("cursor", opts.cursor);
1830
1906
  return this.fetch(`/api/v1/factories/${encodeURIComponent(factorySlug)}/files/search?${q2}`);
1831
1907
  }
1908
+ async listFactoryFiles(opts) {
1909
+ const factorySlug = opts?.factorySlug ?? (typeof process !== "undefined" ? process.env?.AGENT_COMPOSE_FACTORY : undefined) ?? DEFAULT_FACTORY;
1910
+ const q2 = new URLSearchParams;
1911
+ if (opts?.prefix)
1912
+ q2.set("prefix", opts.prefix);
1913
+ if (opts?.cursor)
1914
+ q2.set("cursor", opts.cursor);
1915
+ if (opts?.limit != null)
1916
+ q2.set("limit", String(opts.limit));
1917
+ if (opts?.branch !== undefined)
1918
+ q2.set("branch", opts.branch);
1919
+ if (opts?.at !== undefined)
1920
+ q2.set("at", opts.at);
1921
+ const qs = q2.toString();
1922
+ const wire = await this.fetch(`/api/v1/factories/${encodeURIComponent(factorySlug)}/files${qs ? `?${qs}` : ""}`);
1923
+ return {
1924
+ data: wire.data,
1925
+ hasMore: wire.has_more,
1926
+ nextCursor: wire.next_cursor,
1927
+ at: wire.at ? { head: wire.at.head, committedAt: wire.at.committed_at } : null
1928
+ };
1929
+ }
1832
1930
  async listMembers() {
1833
1931
  const body = await this.fetch("/api/v1/team/members");
1834
1932
  return body.members;
@@ -2033,6 +2131,23 @@ class AgentComposeClient {
2033
2131
  const factorySlug = opts?.factorySlug ?? DEFAULT_FACTORY;
2034
2132
  return this.fetch(`/api/v1/factories/${encodeURIComponent(factorySlug)}/secrets/${encodeURIComponent(key)}`, { method: "DELETE" });
2035
2133
  }
2134
+ async listSessionSecrets(conversationId) {
2135
+ const body = await this.fetch(`/api/v1/conversations/${encodeURIComponent(conversationId)}/secrets`);
2136
+ return body.secrets;
2137
+ }
2138
+ async setSessionSecrets(conversationId, secrets) {
2139
+ const body = await this.fetch(`/api/v1/conversations/${encodeURIComponent(conversationId)}/secrets`, { method: "POST", body: { secrets } });
2140
+ return body.set;
2141
+ }
2142
+ deleteSessionSecret(conversationId, key) {
2143
+ return this.fetch(`/api/v1/conversations/${encodeURIComponent(conversationId)}/secrets/${encodeURIComponent(key)}`, { method: "DELETE" });
2144
+ }
2145
+ requestSessionSecrets(conversationId, keys, opts) {
2146
+ return this.fetch(`/api/v1/conversations/${encodeURIComponent(conversationId)}/secret-requests`, { method: "POST", body: { keys, ...opts?.reason ? { reason: opts.reason } : {} } });
2147
+ }
2148
+ getSessionSecretRequest(conversationId, requestId) {
2149
+ return this.fetch(`/api/v1/conversations/${encodeURIComponent(conversationId)}/secret-requests/${encodeURIComponent(requestId)}`);
2150
+ }
2036
2151
  createApiKey(input) {
2037
2152
  return this.fetch("/api-keys", { method: "POST", body: input });
2038
2153
  }
@@ -3298,7 +3413,7 @@ Re-emit the COMPLETE corrected <response> JSON now: every required field present
3298
3413
  continue;
3299
3414
  }
3300
3415
  const msg = outputVerdict.value;
3301
- if (msg.type === "text_delta" || msg.type === "usage_delta")
3416
+ if (msg.type === "text_delta" || msg.type === "usage_delta" || msg.type === "task_notification" || msg.type === "harness_notice")
3302
3417
  continue;
3303
3418
  opts.onAgentEvent?.(iteration, msg);
3304
3419
  const summary = summarizeAgentMessage(msg);
@@ -3683,6 +3798,73 @@ function createClaudeRuntime(config = {}) {
3683
3798
  });
3684
3799
  }
3685
3800
  var claude_default = createClaudeRuntime();
3801
+ // src/agent/perf-sampler.ts
3802
+ var PERF_SAMPLE_EVERY_BEATS = 6;
3803
+ var PERF_PROBE_WINDOW_SECONDS = 2;
3804
+ var PERF_PROBE_LINE_PREFIX = "perf ";
3805
+ var PERF_TOKEN_MAX_CHARS = 200;
3806
+ function perfReadFunctionFragment(procRoot, diskPath) {
3807
+ const stat = shellQuote(`${procRoot}/stat`);
3808
+ const loadavg = shellQuote(`${procRoot}/loadavg`);
3809
+ const meminfo = shellQuote(`${procRoot}/meminfo`);
3810
+ const disk = shellQuote(diskPath);
3811
+ return `ac_perf_read() { ` + `IFS=' ' read -r ac_pl ac_pu ac_pn2 ac_psy ac_pid ac_pio ac_pirq ac_psq ac_pst ac_prest 2>/dev/null < ${stat} || return 0; ` + `ac_pbz=$(( \${ac_pu:-0} + \${ac_pn2:-0} + \${ac_psy:-0} + \${ac_pirq:-0} + \${ac_psq:-0} + \${ac_pst:-0} )); ` + `ac_ptt=$(( ac_pbz + \${ac_pid:-0} + \${ac_pio:-0} )); ` + `ac_pcpu=-1; ` + `if [ "\${ac_ppt:-0}" -gt 0 ] && [ "$ac_ptt" -gt "\${ac_ppt:-0}" ]; then ` + `ac_pcpu=$(( 100 * (ac_pbz - \${ac_ppb:-0}) / (ac_ptt - ac_ppt) )); ` + `[ "$ac_pcpu" -lt 0 ] && ac_pcpu=0; [ "$ac_pcpu" -gt 100 ] && ac_pcpu=100; ` + `fi; ` + `ac_ppb=$ac_pbz; ac_ppt=$ac_ptt; ` + `IFS=' ' read -r ac_pl1 ac_plr 2>/dev/null < ${loadavg} || ac_pl1=-1; ` + `ac_pvc=$(grep -c '^cpu[0-9]' ${stat} 2>/dev/null); [ -n "$ac_pvc" ] || ac_pvc=0; ` + `ac_pmt=$(awk '$1=="MemTotal:"{print $2}' ${meminfo} 2>/dev/null); ` + `ac_pma=$(awk '$1=="MemAvailable:"{print $2}' ${meminfo} 2>/dev/null); ` + `ac_pmem=-1; ` + `if [ "\${ac_pmt:-0}" -gt 0 ] && [ -n "\${ac_pma:-}" ]; then ac_pmem=$(( 100 * (ac_pmt - ac_pma) / ac_pmt )); fi; ` + `[ "$ac_pmem" -gt 100 ] && ac_pmem=100; [ "$ac_pmem" -lt -1 ] && ac_pmem=-1; ` + `ac_pdsk=$(df -kP ${disk} 2>/dev/null | awk 'NR==2 && $2>0 {print int(100*$3/$2)}'); ` + `ac_tok="v=1,cpu=$ac_pcpu,l1=\${ac_pl1:--1},mem=$ac_pmem,dsk=\${ac_pdsk:--1},vc=\${ac_pvc:-0},ram=$(( \${ac_pmt:-0} / 1024 )),ts=$(date +%s)"; ` + `}; `;
3812
+ }
3813
+ function perfSamplerFunctionFragment(paths) {
3814
+ const perf = shellQuote(paths.perfPath);
3815
+ const perfTmp = shellQuote(`${paths.perfPath}.tmp`);
3816
+ return perfReadFunctionFragment(paths.procRoot ?? "/proc", paths.diskPath ?? "/") + `ac_perf_tick() { ` + `ac_pbeat=$(( \${ac_pbeat:-0} + 1 )); ` + `[ $(( (ac_pbeat - 1) % ${PERF_SAMPLE_EVERY_BEATS} )) -eq 0 ] || return 0; ` + `ac_perf_read; ` + `[ -n "\${ac_tok:-}" ] || return 0; ` + `printf '%s\\n' "$ac_tok" > ${perfTmp} 2>/dev/null && mv -f ${perfTmp} ${perf} 2>/dev/null; ` + `}; `;
3817
+ }
3818
+ function standalonePerfProbeCommand(opts = {}) {
3819
+ const win = opts.windowSeconds ?? PERF_PROBE_WINDOW_SECONDS;
3820
+ return perfReadFunctionFragment(opts.procRoot ?? "/proc", opts.diskPath ?? "/") + `ac_perf_read; sleep ${win}; ac_perf_read; ` + `printf '${PERF_PROBE_LINE_PREFIX}%s\\n' "\${ac_tok:--}"`;
3821
+ }
3822
+ function parsePerfToken(token) {
3823
+ const t = token.trim();
3824
+ if (t.length === 0 || t.length > PERF_TOKEN_MAX_CHARS)
3825
+ return null;
3826
+ if (!/^v=1(?:,|$)/.test(t))
3827
+ return null;
3828
+ const kv = new Map;
3829
+ for (const part of t.split(",")) {
3830
+ const i = part.indexOf("=");
3831
+ if (i > 0)
3832
+ kv.set(part.slice(0, i), part.slice(i + 1));
3833
+ }
3834
+ const pct = (k) => {
3835
+ const n = Number(kv.get(k));
3836
+ return Number.isFinite(n) && n >= 0 && n <= 100 ? Math.round(n) : null;
3837
+ };
3838
+ const bounded = (k, min, max) => {
3839
+ const n = Number(kv.get(k));
3840
+ return Number.isInteger(n) && n >= min && n <= max ? n : null;
3841
+ };
3842
+ const l1raw = Number(kv.get("l1"));
3843
+ const sample = {
3844
+ cpuBusyPct: pct("cpu"),
3845
+ load1: Number.isFinite(l1raw) && l1raw >= 0 && l1raw <= 1e5 ? l1raw : null,
3846
+ memUsedPct: pct("mem"),
3847
+ diskUsedPct: pct("dsk"),
3848
+ vcpus: bounded("vc", 1, 1024),
3849
+ ramMb: bounded("ram", 1, 16 * 1024 * 1024),
3850
+ sampledAtS: bounded("ts", 1577836800, 4102444800)
3851
+ };
3852
+ if (sample.cpuBusyPct === null && sample.load1 === null && sample.memUsedPct === null && sample.diskUsedPct === null)
3853
+ return null;
3854
+ return sample;
3855
+ }
3856
+ function parsePerfProbeOutput(stdout) {
3857
+ const lines = stdout.split(`
3858
+ `).filter((l) => l.startsWith(PERF_PROBE_LINE_PREFIX));
3859
+ const last = lines[lines.length - 1];
3860
+ if (!last)
3861
+ return null;
3862
+ const token = last.slice(PERF_PROBE_LINE_PREFIX.length).trim();
3863
+ if (token === "-")
3864
+ return null;
3865
+ return parsePerfToken(token);
3866
+ }
3867
+
3686
3868
  // src/runtimes/_cli-agent.ts
3687
3869
  import { ndJsonStream } from "@agentclientprotocol/sdk";
3688
3870
 
@@ -3799,10 +3981,113 @@ function now4() {
3799
3981
  return new Date().toISOString();
3800
3982
  }
3801
3983
  var TAIL_REATTACH_DELAY_MS = 2000;
3984
+ var RUNNER_HEARTBEAT_INTERVAL_SECONDS = 10;
3985
+ var RUNNER_BUSY_TASK_FRESH_MINUTES = 2;
3986
+ var RUNNER_BUSY_TASK_ID_LIMIT = 16;
3987
+ function runnerBusySentinelFragment(busyPath, tasksPath) {
3988
+ return `ac_task_files=$(find "$HOME/.claude/tasks" -type f -mmin -${RUNNER_BUSY_TASK_FRESH_MINUTES} 2>/dev/null); ` + `if [ -n "$ac_task_files" ]; then ac_busy=$(printf '%s\\n' "$ac_task_files" | wc -l | tr -cd 0-9); else ac_busy=0; fi; ` + `printf '%s' "$ac_busy" > ${shellQuote(busyPath)} 2>/dev/null; ` + `printf '%s\\n' "$ac_task_files" | sed 's|.*/tasks/||; s|/.*||' | grep . | sort -u ` + `| head -${RUNNER_BUSY_TASK_ID_LIMIT} > ${shellQuote(tasksPath)} 2>/dev/null`;
3989
+ }
3990
+ var RUNNER_HEARTBEAT_STALE_MS = RUNNER_HEARTBEAT_INTERVAL_SECONDS * 1000 * 3;
3991
+ var TAIL_WATCHDOG_INTERVAL_MS = 15000;
3992
+ var TURN_PROBE_TIMEOUT_MS = 1e4;
3993
+ var TAIL_ABANDON_AFTER_STREAM_DEATHS = 3;
3994
+ var DURABLE_POLL_INTERVAL_MS = 1000;
3995
+ var RESUME_TARGET_MISSING_STDERR = /no conversation found with session id/i;
3996
+ function turnLivenessProbeCommand(paths) {
3997
+ return `sz=$(wc -c < ${shellQuote(paths.outPath)} 2>/dev/null || echo -1); ` + `alive=0; kill -0 ${paths.pid} 2>/dev/null && alive=1; ` + `hb=$(cat ${shellQuote(paths.hbPath)} 2>/dev/null || echo 0); ` + `now=$(date +%s%3N); ` + `sent=$(grep -c -F ${shellQuote(paths.sentinel)} ${shellQuote(paths.outPath)} 2>/dev/null || echo 0); ` + `pf=$(head -c ${PERF_TOKEN_MAX_CHARS} ${shellQuote(paths.perfPath)} 2>/dev/null | tr -cd 'A-Za-z0-9=,.-'); ` + `printf '%s %s %s %s %s %s\\n' "$sz" "$alive" "$hb" "$now" "$sent" "\${pf:--}"`;
3998
+ }
3999
+ function parseTurnProbeOutput(raw) {
4000
+ const fields = raw.trim().split(/\s+/);
4001
+ if (fields.length < 5)
4002
+ return null;
4003
+ const size = Number.parseInt(fields[0], 10);
4004
+ const alive = fields[1] === "1" ? true : fields[1] === "0" ? false : null;
4005
+ const hbMs = Number.parseInt(fields[2], 10);
4006
+ const nowMs = Number.parseInt(fields[3], 10);
4007
+ const sentinelCount = Number.parseInt(fields[4], 10);
4008
+ if (!Number.isFinite(size) || alive === null || !Number.isFinite(hbMs) || !Number.isFinite(nowMs) || !Number.isFinite(sentinelCount))
4009
+ return null;
4010
+ const perfTok = fields[5];
4011
+ const perf = perfTok !== undefined && perfTok !== "-" ? parsePerfToken(perfTok) : null;
4012
+ return {
4013
+ size,
4014
+ alive,
4015
+ hbMs,
4016
+ nowMs,
4017
+ sentinelSeen: sentinelCount > 0,
4018
+ ...perf ? { perf } : {}
4019
+ };
4020
+ }
4021
+ function livenessVerdictFromReading(reading) {
4022
+ if (reading === null)
4023
+ return "probe-failed";
4024
+ if (reading.alive)
4025
+ return "alive";
4026
+ if (reading.sentinelSeen)
4027
+ return "alive";
4028
+ if (reading.hbMs > 0 && reading.nowMs - reading.hbMs <= RUNNER_HEARTBEAT_STALE_MS)
4029
+ return "alive";
4030
+ return "dead";
4031
+ }
4032
+ var INJECT_FEEDER_POLL_SECONDS = "0.3";
4033
+ var INJECT_FEEDER_RESULT_TAIL_BYTES = 262144;
4034
+ var INJECT_ACK_POLL_ATTEMPTS = 30;
4035
+ var INJECT_ACK_POLL_SECONDS = "0.5";
4036
+ var INJECT_ACK_EXEC_TIMEOUT_MS = 25000;
4037
+ function streamInputFeederFragment(paths) {
4038
+ const q2 = shellQuote;
4039
+ return `rm -f ${q2(paths.fifoPath)}; mkfifo ${q2(paths.fifoPath)}; ` + `: > ${q2(paths.inboxPath)}; echo 0 > ${q2(paths.deliveredPath)}; ` + `( cat ${q2(paths.promptPath)}; ac_fed=0; while kill -0 "$$" 2>/dev/null; do ` + `if tail -c ${INJECT_FEEDER_RESULT_TAIL_BYTES} ${q2(paths.outPath)} 2>/dev/null | grep -q '"type":"result"'; then break; fi; ` + `ac_lines=$(wc -l < ${q2(paths.inboxPath)} 2>/dev/null || echo 0); ac_lines=\${ac_lines:-0}; ` + `if [ "$ac_lines" -gt "$ac_fed" ]; then ` + `sed -n "$((ac_fed+1)),$((ac_lines))p" ${q2(paths.inboxPath)}; ac_fed=$ac_lines; ` + `echo "$ac_fed" > ${q2(paths.deliveredPath)}; fi; ` + `sleep ${INJECT_FEEDER_POLL_SECONDS}; done ) > ${q2(paths.fifoPath)} 2>/dev/null </dev/null & `;
4040
+ }
4041
+ function wrapMidTurnUserMessage(text) {
4042
+ return `The user sent a new message while you were working:
4043
+
4044
+ ` + text + `
4045
+
4046
+ Address the message above as you continue this turn.`;
4047
+ }
4048
+ function injectAppendAndAckCommand(args) {
4049
+ const q2 = shellQuote;
4050
+ return `printf '%s\\n' ${q2(args.line)} >> ${q2(args.inboxPath)}; i=0; ` + `while [ "$i" -lt ${INJECT_ACK_POLL_ATTEMPTS} ]; do ` + `d=$(cat ${q2(args.deliveredPath)} 2>/dev/null || echo 0); ` + `[ "\${d:-0}" -ge ${args.target} ] && exit 0; ` + `kill -0 ${args.pid} 2>/dev/null || exit 4; ` + `sleep ${INJECT_ACK_POLL_SECONDS}; i=$((i+1)); done; exit 5`;
4051
+ }
4052
+ var REAP_TERM_WAIT_ATTEMPTS = 20;
4053
+ var REAP_KILL_WAIT_ATTEMPTS = 8;
4054
+ var REAP_POLL_SECONDS = "0.25";
4055
+ var REAP_STILL_ALIVE_EXIT = 7;
4056
+ var REAP_EXEC_TIMEOUT_MS = 15000;
4057
+ function reapKillAndConfirmCommand(pid) {
4058
+ const alive = `kill -0 ${pid} 2>/dev/null`;
4059
+ return `kill -TERM -- -${pid} 2>/dev/null; kill -TERM ${pid} 2>/dev/null; ac_ri=0; ` + `while [ "$ac_ri" -lt ${REAP_TERM_WAIT_ATTEMPTS} ]; do ${alive} || exit 0; ` + `sleep ${REAP_POLL_SECONDS}; ac_ri=$((ac_ri+1)); done; ` + `kill -KILL -- -${pid} 2>/dev/null; kill -KILL ${pid} 2>/dev/null; ac_ri=0; ` + `while [ "$ac_ri" -lt ${REAP_KILL_WAIT_ATTEMPTS} ]; do ${alive} || exit 0; ` + `sleep ${REAP_POLL_SECONDS}; ac_ri=$((ac_ri+1)); done; ` + `exit ${REAP_STILL_ALIVE_EXIT}`;
4060
+ }
4061
+ var RESIDENT_IDLE_TTL_S = 240;
4062
+ var RESIDENT_WATCHDOG_POLL_S = 15;
4063
+ var RESIDENT_RESULT_LINE_PREFIX = '{"type":"result"';
4064
+ function residentResultCountCommand(outPath, fromByte = 0) {
4065
+ const q2 = shellQuote;
4066
+ const src = fromByte > 0 ? `tail -c +${fromByte + 1} ${q2(outPath)} 2>/dev/null` : `cat ${q2(outPath)} 2>/dev/null`;
4067
+ return `ac_res=$(${src} | grep -c ${q2(`^${RESIDENT_RESULT_LINE_PREFIX}`)}); echo "\${ac_res:-0}"`;
4068
+ }
4069
+ function residentFeederFragment(paths) {
4070
+ const q2 = shellQuote;
4071
+ return `rm -f ${q2(paths.fifoPath)}; mkfifo ${q2(paths.fifoPath)}; ` + `: > ${q2(paths.inboxPath)}; echo 0 > ${q2(paths.deliveredPath)}; rm -f ${q2(paths.endPath)}; ` + `( cat ${q2(paths.promptPath)}; ac_fed=0; while kill -0 "$$" 2>/dev/null; do ` + `if [ -f ${q2(paths.endPath)} ]; then break; fi; ` + `ac_lines=$(wc -l < ${q2(paths.inboxPath)} 2>/dev/null || echo 0); ac_lines=\${ac_lines:-0}; ` + `if [ "$ac_lines" -gt "$ac_fed" ]; then ` + `sed -n "$((ac_fed+1)),$((ac_lines))p" ${q2(paths.inboxPath)}; ac_fed=$ac_lines; ` + `echo "$ac_fed" > ${q2(paths.deliveredPath)}; fi; ` + `sleep ${INJECT_FEEDER_POLL_SECONDS}; done ) > ${q2(paths.fifoPath)} 2>/dev/null </dev/null & `;
4072
+ }
4073
+ function residentIdleCheckFragment(paths) {
4074
+ const q2 = shellQuote;
4075
+ return `ac_res=$(grep -c ${q2(`^${RESIDENT_RESULT_LINE_PREFIX}`)} ${q2(paths.outPath)} 2>/dev/null); ac_res=\${ac_res:-0}; ` + `ac_srv=$(cat ${q2(paths.servedPath)} 2>/dev/null); ac_srv=\${ac_srv:-1}; ` + `ac_in=$(wc -l < ${q2(paths.inboxPath)} 2>/dev/null); ac_in=\${ac_in:-0}; ` + `ac_del=$(cat ${q2(paths.deliveredPath)} 2>/dev/null); ac_del=\${ac_del:-0}; ` + `if [ "$ac_res" -ge "$ac_srv" ] && [ "$ac_in" -eq "$ac_del" ]; then ac_idle=1; else ac_idle=0; fi; `;
4076
+ }
4077
+ function residentIdleWatchdogFragment(paths) {
4078
+ return `( ac_idle_s=0; while kill -0 "$$" 2>/dev/null; do ` + residentIdleCheckFragment(paths) + `if [ "$ac_idle" = 1 ] && find "$HOME/.claude/tasks" -type f -mmin -${RUNNER_BUSY_TASK_FRESH_MINUTES} 2>/dev/null | grep -q .; then ac_idle=0; fi; ` + `if [ "$ac_idle" = 1 ]; then ac_idle_s=$((ac_idle_s+${RESIDENT_WATCHDOG_POLL_S})); else ac_idle_s=0; fi; ` + `if [ "$ac_idle_s" -ge ${RESIDENT_IDLE_TTL_S} ]; then kill -- -$$ 2>/dev/null; exit 0; fi; ` + `sleep ${RESIDENT_WATCHDOG_POLL_S}; done ) >/dev/null 2>&1 </dev/null & `;
4079
+ }
4080
+ function deferredExitEventPushCommand(notify, turnIdFile) {
4081
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(notify.tokenEnv))
4082
+ return "";
4083
+ const q2 = shellQuote;
4084
+ return `( ac_tid=$(cat ${q2(turnIdFile)} 2>/dev/null | tr -cd 'A-Za-z0-9-'); ` + `if [ -n "$ac_tid" ] && [ -n "$${notify.tokenEnv}" ] && command -v curl >/dev/null 2>&1; then ` + `curl -fsS -m ${EXIT_PUSH_ATTEMPT_TIMEOUT_SECONDS} -o /dev/null -X POST ` + `-H "Authorization: Bearer $${notify.tokenEnv}" ` + `-H 'Content-Type: application/json' --data "{\\"turnId\\":\\"$ac_tid\\",\\"exitCode\\":$ac_rc}" ${q2(notify.url)}; ` + `fi ) >/dev/null 2>&1 || true`;
4085
+ }
3802
4086
  var LAUNCH_EXEC_TIMEOUT_MS = 30000;
3803
4087
  var LAUNCH_PID_RECOVERY_ATTEMPTS = 3;
3804
4088
  var LAUNCH_PID_RECOVERY_DELAY_MS = 1000;
3805
4089
  var INSTALL_PROBE_TIMEOUT_MS = 30000;
4090
+ var CLI_INSTALL_TIMEOUT_MS = 300000;
3806
4091
  function parsePid(raw) {
3807
4092
  const pid = Number.parseInt(raw.trim().split(`
3808
4093
  `).pop() ?? "", 10);
@@ -3824,6 +4109,30 @@ async function recoverLaunchPid(read, pidPath) {
3824
4109
  function shellQuote(value) {
3825
4110
  return `'${value.replace(/'/g, `'\\''`)}'`;
3826
4111
  }
4112
+ var SESSION_ENV_FILE_RELPATH = ".agent-compose/session-env.sh";
4113
+ var SESSION_ENV_FILE_SAFE = /^[A-Za-z0-9._\/-]+$/;
4114
+ function sessionEnvSourceFragment(relPath) {
4115
+ if (!relPath || !SESSION_ENV_FILE_SAFE.test(relPath) || relPath.includes(".."))
4116
+ return "";
4117
+ return `[ -f "$HOME/${relPath}" ] && . "$HOME/${relPath}" >/dev/null 2>&1 || true; `;
4118
+ }
4119
+ function credEnvSourceFragment(absPath) {
4120
+ if (!absPath || !absPath.startsWith("/") || !SESSION_ENV_FILE_SAFE.test(absPath.slice(1)) || absPath.includes(".."))
4121
+ return "";
4122
+ return `[ -f "${absPath}" ] && . "${absPath}" >/dev/null 2>&1 || true; `;
4123
+ }
4124
+ var EXIT_PUSH_ATTEMPT_TIMEOUT_SECONDS = 5;
4125
+ var EXIT_PUSH_RETRY_DELAY_SECONDS = 1;
4126
+ function exitEventPushCommand(notify) {
4127
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(notify.tokenEnv))
4128
+ return "";
4129
+ if (!/^[A-Za-z0-9-]{1,64}$/.test(notify.turnId))
4130
+ return "";
4131
+ const token = `"$${notify.tokenEnv}"`;
4132
+ const body = `"{\\"turnId\\":\\"${notify.turnId}\\",\\"exitCode\\":$ac_rc}"`;
4133
+ const attempt = `curl -fsS -m ${EXIT_PUSH_ATTEMPT_TIMEOUT_SECONDS} -o /dev/null -X POST` + ` -H "Authorization: Bearer $${notify.tokenEnv}"` + ` -H 'Content-Type: application/json' --data ${body} ${shellQuote(notify.url)}`;
4134
+ return `( if [ -n ${token} ] && command -v curl >/dev/null 2>&1; then ` + `${attempt} || { sleep ${EXIT_PUSH_RETRY_DELAY_SECONDS}; ${attempt}; }; ` + `fi ) >/dev/null 2>&1 || true`;
4135
+ }
3827
4136
  var promptFileCounter = 0;
3828
4137
  function uniquePromptPath(kind, agentId, iteration) {
3829
4138
  const unique = `${Date.now().toString(36)}-${(promptFileCounter++).toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
@@ -3863,6 +4172,60 @@ class CliAgentRunner {
3863
4172
  acpSession;
3864
4173
  currentAcpProcCtx;
3865
4174
  acpTransportReady = ACP_TRANSPORT_READY;
4175
+ turnProbe = null;
4176
+ turnInject = null;
4177
+ async injectUserMessage(text) {
4178
+ const inject = this.turnInject;
4179
+ const streamInput = this.spec.streamInput;
4180
+ if (!inject || !streamInput || this.guestDetached)
4181
+ return "unsupported";
4182
+ const line = streamInput.messageLine(wrapMidTurnUserMessage(text));
4183
+ const attempt = inject.chain.then(async () => {
4184
+ if (this.turnInject !== inject)
4185
+ return "closed";
4186
+ const target = inject.appended + 1;
4187
+ inject.appended = target;
4188
+ try {
4189
+ const res = await this.sandbox.commands.run(injectAppendAndAckCommand({
4190
+ line,
4191
+ target,
4192
+ pid: inject.pid,
4193
+ inboxPath: inject.inboxPath,
4194
+ deliveredPath: inject.deliveredPath
4195
+ }), { timeoutMs: INJECT_ACK_EXEC_TIMEOUT_MS });
4196
+ if (res.exitCode === 0)
4197
+ return "delivered";
4198
+ return res.exitCode === 5 ? "pending" : "closed";
4199
+ } catch {
4200
+ return "closed";
4201
+ }
4202
+ });
4203
+ inject.chain = attempt.catch(() => {
4204
+ return;
4205
+ });
4206
+ return attempt;
4207
+ }
4208
+ async probeTurnLiveness() {
4209
+ const probe = this.turnProbe;
4210
+ if (!probe)
4211
+ return null;
4212
+ return livenessVerdictFromReading(await probe());
4213
+ }
4214
+ wakeTailWatchdog = null;
4215
+ nudgePending = false;
4216
+ nudgeTurnProbe() {
4217
+ const wake = this.wakeTailWatchdog;
4218
+ if (wake) {
4219
+ this.wakeTailWatchdog = null;
4220
+ wake();
4221
+ } else {
4222
+ this.nudgePending = true;
4223
+ }
4224
+ }
4225
+ guestDetached = false;
4226
+ detachGuest() {
4227
+ this.guestDetached = true;
4228
+ }
3866
4229
  constructor(sandbox, options, spec, configModel, configEffort) {
3867
4230
  this.sandbox = sandbox;
3868
4231
  this.options = options;
@@ -3911,7 +4274,7 @@ class CliAgentRunner {
3911
4274
  this.installed = true;
3912
4275
  return;
3913
4276
  }
3914
- const res = await this.sandbox.commands.run(this.spec.install, { timeoutMs: 300000 });
4277
+ const res = await this.sandbox.commands.run(this.spec.install, { timeoutMs: CLI_INSTALL_TIMEOUT_MS });
3915
4278
  if (res.exitCode !== 0) {
3916
4279
  throw new Error(`failed to install ${this.spec.bin}: ${(res.stderr || res.stdout || "").slice(-500)}`);
3917
4280
  }
@@ -4052,17 +4415,25 @@ class CliAgentRunner {
4052
4415
  return;
4053
4416
  await this.ensureInstalled();
4054
4417
  const promptPath = uniquePromptPath(this.spec.kind, this.options.agentId ?? "agent", opts.iteration ?? 0);
4055
- await this.sandbox.files.write(promptPath, this.spec.promptPayload(opts.prompt));
4418
+ this.turnProbe = null;
4419
+ this.turnInject = null;
4420
+ this.nudgePending = false;
4421
+ this.wakeTailWatchdog = null;
4422
+ const streamInput = this.spec.streamInput !== undefined && this.sandbox.commands.runBackground !== undefined;
4423
+ const fifoPath = `${promptPath}.fifo`;
4424
+ const promptWrite = this.sandbox.files.write(promptPath, streamInput ? `${this.spec.streamInput.promptLine(opts.prompt)}
4425
+ ` : this.spec.promptPayload(opts.prompt));
4056
4426
  const cmd = this.spec.buildCommand({
4057
- promptPath,
4427
+ promptPath: streamInput ? fifoPath : promptPath,
4058
4428
  sessionId: opts.sessionId,
4059
4429
  model: this.model,
4060
4430
  cwd: this.options.cwd,
4061
- effort: this.configEffort
4431
+ effort: this.configEffort,
4432
+ ...streamInput ? { streamInput: true } : {}
4062
4433
  });
4063
4434
  const guardPath = `${promptPath}.guard.js`;
4064
4435
  const sentinel = `__AC_JSONL_EXIT_${Math.random().toString(36).slice(2, 10)}__`;
4065
- await this.sandbox.files.write(guardPath, jsonlGuardScript());
4436
+ await Promise.all([promptWrite, this.sandbox.files.write(guardPath, jsonlGuardScript())]);
4066
4437
  const guardedCmd = wrapJsonlCommand({
4067
4438
  cmd,
4068
4439
  guardPath,
@@ -4076,14 +4447,23 @@ class CliAgentRunner {
4076
4447
  let transportError = null;
4077
4448
  let consumerStopped = false;
4078
4449
  let reap = () => {};
4450
+ let reapConfirm = null;
4079
4451
  let transport;
4080
4452
  const durableRead = this.sandbox.files.read?.bind(this.sandbox.files);
4081
4453
  if (this.sandbox.commands.runBackground) {
4082
4454
  const outPath = `${promptPath}.out`;
4083
4455
  const errPath = `${promptPath}.err`;
4084
- const detachedScript = `{ ${guardedCmd}; } >> ${shellQuote(outPath)} 2>> ${shellQuote(errPath)} </dev/null; ` + `printf '\\n%s %s\\n' ${shellQuote(sentinel)} "$?" >> ${shellQuote(outPath)}`;
4456
+ const hbPath = `${promptPath}.hb`;
4457
+ const exitPush = this.options.turnExitNotify ? exitEventPushCommand(this.options.turnExitNotify) : "";
4458
+ const busyPath = `${promptPath}.busy`;
4459
+ const tasksPath = `${promptPath}.tasks`;
4460
+ const inboxPath = `${promptPath}.inbox`;
4461
+ const deliveredPath = `${promptPath}.delivered`;
4462
+ const feeder = streamInput ? streamInputFeederFragment({ promptPath, fifoPath, inboxPath, deliveredPath, outPath }) : "";
4463
+ const perfPath = `${promptPath}.perf`;
4464
+ const detachedScript = feeder + `( ${perfSamplerFunctionFragment({ perfPath })}while kill -0 "$$" 2>/dev/null; do ` + `date +%s%3N > ${shellQuote(hbPath)}; ` + `${runnerBusySentinelFragment(busyPath, tasksPath)}; ` + `ac_perf_tick; ` + `sleep ${RUNNER_HEARTBEAT_INTERVAL_SECONDS}; done ) >/dev/null 2>&1 </dev/null & ` + sessionEnvSourceFragment(this.options.sessionEnvFile) + credEnvSourceFragment(this.options.credEnvFile) + `{ ${guardedCmd}; } >> ${shellQuote(outPath)} 2>> ${shellQuote(errPath)} </dev/null; ac_rc=$?; ` + `printf '\\n%s %s\\n' ${shellQuote(sentinel)} "$ac_rc" >> ${shellQuote(outPath)}` + (exitPush ? `; ${exitPush}` : "");
4085
4465
  const pidPath = `${promptPath}.pid`;
4086
- const launchCmd = `: > ${shellQuote(outPath)}; : > ${shellQuote(errPath)}; ` + `if command -v setsid >/dev/null 2>&1; then setsid sh -c ${shellQuote(detachedScript)} >/dev/null 2>&1 </dev/null & ` + `else sh -c ${shellQuote(detachedScript)} >/dev/null 2>&1 </dev/null & fi; ` + `echo "$!" > ${shellQuote(pidPath)}; echo "$!"`;
4466
+ const launchCmd = `: > ${shellQuote(outPath)}; : > ${shellQuote(errPath)}; date +%s%3N > ${shellQuote(hbPath)}; ` + `if command -v setsid >/dev/null 2>&1; then setsid sh -c ${shellQuote(detachedScript)} >/dev/null 2>&1 </dev/null & ` + `else sh -c ${shellQuote(detachedScript)} >/dev/null 2>&1 </dev/null & fi; ` + `echo "$!" > ${shellQuote(pidPath)}; echo "$!"`;
4087
4467
  let pid = Number.NaN;
4088
4468
  let launched = null;
4089
4469
  let launchErr = null;
@@ -4106,6 +4486,19 @@ class CliAgentRunner {
4106
4486
  throw launchErr;
4107
4487
  console.warn(`[cli-agent] ${this.spec.kind} launch exec stream died but the detached runner is up ` + `(pid ${pid} via ${pidPath}) — continuing on the durable transport: ${formatError(launchErr)}`);
4108
4488
  }
4489
+ const probeCmd = turnLivenessProbeCommand({ outPath, hbPath, perfPath, pid, sentinel });
4490
+ const runTurnProbe = async () => {
4491
+ try {
4492
+ const res = await this.sandbox.commands.run(probeCmd, { timeoutMs: TURN_PROBE_TIMEOUT_MS });
4493
+ return parseTurnProbeOutput(res.stdout ?? "");
4494
+ } catch {
4495
+ return null;
4496
+ }
4497
+ };
4498
+ this.turnProbe = runTurnProbe;
4499
+ if (streamInput) {
4500
+ this.turnInject = { inboxPath, deliveredPath, pid, appended: 0, chain: Promise.resolve() };
4501
+ }
4109
4502
  let offset = 0;
4110
4503
  let currentTail = null;
4111
4504
  const consumeLine = (raw) => {
@@ -4121,10 +4514,15 @@ class CliAgentRunner {
4121
4514
  lines.push(line);
4122
4515
  };
4123
4516
  reap = () => {
4124
- this.sandbox.commands.run(`kill -TERM -- -${pid} 2>/dev/null; kill -TERM ${pid} 2>/dev/null; true`, { timeoutMs: 1e4 }).catch(() => {});
4517
+ reapConfirm ??= this.sandbox.commands.run(reapKillAndConfirmCommand(pid), { timeoutMs: REAP_EXEC_TIMEOUT_MS }).then((res) => {
4518
+ if (res.exitCode === REAP_STILL_ALIVE_EXIT) {
4519
+ console.warn(`[cli-agent] ${this.spec.kind} runner ${pid} survived SIGKILL's confirm window ` + "— the next launch's preflight is the backstop");
4520
+ }
4521
+ }).catch(() => {});
4125
4522
  currentTail?.kill().catch(() => {});
4126
4523
  };
4127
4524
  transport = (async () => {
4525
+ let streamDeaths = 0;
4128
4526
  while (exitCode === null && !consumerStopped && !opts.signal?.aborted) {
4129
4527
  let buf = "";
4130
4528
  let handle = null;
@@ -4141,16 +4539,68 @@ class CliAgentRunner {
4141
4539
  }
4142
4540
  }
4143
4541
  };
4144
- try {
4145
- handle = await this.sandbox.commands.runBackground(`tail -c +${offset + 1} -f ${shellQuote(outPath)}`, { timeoutMs: 0, onStdout });
4146
- } catch {}
4542
+ if (streamDeaths < TAIL_ABANDON_AFTER_STREAM_DEATHS) {
4543
+ try {
4544
+ handle = await this.sandbox.commands.runBackground(`tail -s 0.05 -c +${offset + 1} -f ${shellQuote(outPath)}`, { timeoutMs: 0, onStdout });
4545
+ } catch {}
4546
+ }
4147
4547
  if (handle) {
4148
4548
  if (exitCode !== null)
4149
4549
  handle.kill().catch(() => {});
4150
4550
  currentTail = handle;
4151
- await handle.wait().catch(() => {
4152
- return;
4153
- });
4551
+ await (async (tail) => {
4552
+ let waitSettled = false;
4553
+ const waitP = tail.wait().catch(() => {
4554
+ return;
4555
+ }).then(() => {
4556
+ waitSettled = true;
4557
+ });
4558
+ let lastTickOffset = -1;
4559
+ while (!waitSettled) {
4560
+ let onAbort;
4561
+ const wake = new Promise((resolve) => {
4562
+ const t = setTimeout(resolve, TAIL_WATCHDOG_INTERVAL_MS);
4563
+ t.unref?.();
4564
+ const fire = () => {
4565
+ clearTimeout(t);
4566
+ resolve();
4567
+ };
4568
+ if (this.nudgePending) {
4569
+ this.nudgePending = false;
4570
+ fire();
4571
+ } else {
4572
+ this.wakeTailWatchdog = fire;
4573
+ }
4574
+ if (opts.signal) {
4575
+ onAbort = fire;
4576
+ opts.signal.addEventListener("abort", onAbort, { once: true });
4577
+ }
4578
+ });
4579
+ await Promise.race([waitP, wake]);
4580
+ this.wakeTailWatchdog = null;
4581
+ if (onAbort)
4582
+ opts.signal?.removeEventListener("abort", onAbort);
4583
+ if (waitSettled || exitCode !== null || consumerStopped || opts.signal?.aborted)
4584
+ return;
4585
+ const tickOffset = offset;
4586
+ const reading = await runTurnProbe();
4587
+ if (offset !== tickOffset) {
4588
+ lastTickOffset = offset;
4589
+ continue;
4590
+ }
4591
+ if (reading === null) {
4592
+ lastTickOffset = tickOffset;
4593
+ continue;
4594
+ }
4595
+ const streamStarved = reading.size > offset && tickOffset === lastTickOffset;
4596
+ if (reading.sentinelSeen || !reading.alive || streamStarved) {
4597
+ console.warn(`[cli-agent] ${this.spec.kind} tail stream ${reading.sentinelSeen ? "outlived the FINISHED turn (durable sentinel unharvested)" : !reading.alive ? "outlived the runner" : `starved (durable file at ${reading.size} bytes, stream stuck at ${offset})`}` + " — killing the tail and catching up from the durable file");
4598
+ tail.kill().catch(() => {});
4599
+ return;
4600
+ }
4601
+ lastTickOffset = tickOffset;
4602
+ }
4603
+ })(handle);
4154
4604
  currentTail = null;
4155
4605
  }
4156
4606
  if (exitCode !== null || consumerStopped || opts.signal?.aborted)
@@ -4180,8 +4630,16 @@ class CliAgentRunner {
4180
4630
  exitCode = JSONL_GUARD_NO_SENTINEL_EXIT;
4181
4631
  break;
4182
4632
  }
4183
- console.warn(`[cli-agent] ${this.spec.kind} turn stream lost with the runner alive — re-tailing ${outPath} from byte ${offset}`);
4184
- await new Promise((r) => setTimeout(r, TAIL_REATTACH_DELAY_MS));
4633
+ streamDeaths += 1;
4634
+ if (streamDeaths < TAIL_ABANDON_AFTER_STREAM_DEATHS) {
4635
+ console.warn(`[cli-agent] ${this.spec.kind} turn stream lost with the runner alive — re-tailing ${outPath} from byte ${offset}`);
4636
+ await new Promise((r) => setTimeout(r, TAIL_REATTACH_DELAY_MS));
4637
+ } else {
4638
+ if (streamDeaths === TAIL_ABANDON_AFTER_STREAM_DEATHS) {
4639
+ console.warn(`[cli-agent] ${this.spec.kind} turn abandoning streaming after ` + `${streamDeaths} stream deaths — finishing on durable polling from byte ${offset}`);
4640
+ }
4641
+ await new Promise((r) => setTimeout(r, DURABLE_POLL_INTERVAL_MS));
4642
+ }
4185
4643
  }
4186
4644
  if (exitCode !== null && exitCode !== 0 && durableRead) {
4187
4645
  try {
@@ -4207,7 +4665,7 @@ class CliAgentRunner {
4207
4665
  onStdout,
4208
4666
  timeoutMs: 0
4209
4667
  };
4210
- transport = this.sandbox.commands.run(guardedCmd, runOpts).then((res) => {
4668
+ transport = this.sandbox.commands.run(credEnvSourceFragment(this.options.credEnvFile) + guardedCmd, runOpts).then((res) => {
4211
4669
  const tail = buf.trim();
4212
4670
  if (tail)
4213
4671
  lines.push(tail);
@@ -4218,13 +4676,18 @@ class CliAgentRunner {
4218
4676
  }).finally(() => lines.close());
4219
4677
  }
4220
4678
  transport.catch(() => {});
4679
+ const reapUnlessDetached = () => {
4680
+ if (!this.guestDetached)
4681
+ reap();
4682
+ };
4221
4683
  if (opts.signal) {
4222
4684
  if (opts.signal.aborted)
4223
- reap();
4685
+ reapUnlessDetached();
4224
4686
  else
4225
- opts.signal.addEventListener("abort", reap, { once: true });
4687
+ opts.signal.addEventListener("abort", reapUnlessDetached, { once: true });
4226
4688
  }
4227
4689
  try {
4690
+ let announcedSessionId = opts.sessionId ?? "";
4228
4691
  for await (const line of lines) {
4229
4692
  let parsed;
4230
4693
  try {
@@ -4235,6 +4698,10 @@ class CliAgentRunner {
4235
4698
  const sid = this.spec.extractSessionId(parsed);
4236
4699
  if (sid)
4237
4700
  sessionId = sid;
4701
+ if (sid && sid !== announcedSessionId) {
4702
+ announcedSessionId = sid;
4703
+ yield { type: "init", sessionId: sid, timestamp: now4() };
4704
+ }
4238
4705
  for (const msg of this.spec.mapEvent(parsed)) {
4239
4706
  if (msg.type === "error")
4240
4707
  sawError = true;
@@ -4255,14 +4722,23 @@ class CliAgentRunner {
4255
4722
  yield { type: "error", text: `${this.spec.kind} exited with code ${exitCode}${detail}${exitStderr ? `: ${exitStderr}` : ""}`, timestamp: now4() };
4256
4723
  return;
4257
4724
  }
4725
+ if (exitCode !== 0 && sawError && RESUME_TARGET_MISSING_STDERR.test(exitStderr)) {
4726
+ yield { type: "error", text: `${this.spec.kind} exited with code ${exitCode}: ${exitStderr}`, timestamp: now4() };
4727
+ return;
4728
+ }
4258
4729
  if (!sawError)
4259
4730
  yield { type: "done", sessionId: sessionId ?? "", timestamp: now4() };
4260
4731
  } finally {
4261
- opts.signal?.removeEventListener("abort", reap);
4732
+ opts.signal?.removeEventListener("abort", reapUnlessDetached);
4262
4733
  consumerStopped = true;
4263
- if (exitCode === null)
4734
+ if (exitCode === null && !this.guestDetached)
4264
4735
  reap();
4265
- this.sandbox.commands.run(`rm -f ${shellQuote(promptPath)} ${shellQuote(guardPath)} ${shellQuote(`${promptPath}.out`)} ${shellQuote(`${promptPath}.err`)} ${shellQuote(`${promptPath}.pid`)}`, { timeoutMs: 1e4 }).catch(() => {});
4736
+ if (reapConfirm)
4737
+ await reapConfirm;
4738
+ this.turnInject = null;
4739
+ if (!this.guestDetached) {
4740
+ this.sandbox.commands.run(`rm -f ${shellQuote(promptPath)} ${shellQuote(guardPath)} ${shellQuote(`${promptPath}.out`)} ${shellQuote(`${promptPath}.err`)} ${shellQuote(`${promptPath}.pid`)} ${shellQuote(`${promptPath}.hb`)} ${shellQuote(`${promptPath}.busy`)} ${shellQuote(`${promptPath}.tasks`)} ${shellQuote(`${promptPath}.fifo`)} ${shellQuote(`${promptPath}.inbox`)} ${shellQuote(`${promptPath}.delivered`)}`, { timeoutMs: 1e4 }).catch(() => {});
4741
+ }
4266
4742
  }
4267
4743
  } catch (err) {
4268
4744
  yield { type: "error", text: formatError(err), timestamp: now4() };
@@ -4286,10 +4762,19 @@ var CODEX_ADVISORY_PATTERNS = [
4286
4762
  function isCodexAdvisoryNoise(text) {
4287
4763
  return CODEX_ADVISORY_PATTERNS.some((re) => re.test(text));
4288
4764
  }
4765
+ var CODEX_LOCK_WAIT_ATTEMPTS = 20;
4766
+ var CODEX_LOCK_WAIT_SECONDS = "0.25";
4767
+ var CODEX_THREAD_ID_SAFE = /^[A-Za-z0-9_-]{1,64}$/;
4768
+ function codexWriterLockPreflight(threadId, waitAttempts = CODEX_LOCK_WAIT_ATTEMPTS) {
4769
+ if (!CODEX_THREAD_ID_SAFE.test(threadId))
4770
+ return "";
4771
+ return `ac_lk="\${CODEX_HOME:-$HOME/.codex}/thread-writer-locks/${threadId}.lock"; ` + `if [ -e "$ac_lk" ] && command -v flock >/dev/null 2>&1; then ac_lki=0; ` + `while ! flock -n "$ac_lk" true 2>/dev/null && [ "$ac_lki" -lt ${waitAttempts} ]; do ` + `sleep ${CODEX_LOCK_WAIT_SECONDS}; ac_lki=$((ac_lki+1)); done; ` + `flock -n "$ac_lk" rm -f -- "$ac_lk" 2>/dev/null || true; fi; `;
4772
+ }
4289
4773
  var codexSpec = {
4290
4774
  kind: "codex",
4291
4775
  authEnv: "CODEX_API_KEY",
4292
4776
  bin: "codex",
4777
+ exclusiveSessionWriter: true,
4293
4778
  acp: {
4294
4779
  command: "npx",
4295
4780
  args: ["-y", CODEX_ACP_ADAPTER],
@@ -4306,8 +4791,9 @@ var codexSpec = {
4306
4791
  ...effort ? ["-c", `model_reasoning_effort=${effort === "max" ? "xhigh" : effort}`] : []
4307
4792
  ].join(" ");
4308
4793
  const cd = cwd ? `cd ${shellQuote(cwd)} && ` : "";
4794
+ const preflight = sessionId ? codexWriterLockPreflight(sessionId) : "";
4309
4795
  const exec = sessionId ? `codex exec resume ${shellQuote(sessionId)} ${flags}` : `codex exec ${flags}`;
4310
- return `${cd}${exec} - < ${shellQuote(promptPath)}`;
4796
+ return `${preflight}${cd}${exec} - < ${shellQuote(promptPath)}`;
4311
4797
  },
4312
4798
  extractSessionId: (p) => p.type === "thread.started" && typeof p.thread_id === "string" ? p.thread_id : undefined,
4313
4799
  mapEvent: (p) => {
@@ -4525,6 +5011,84 @@ function toolResultText(content) {
4525
5011
  }
4526
5012
  return content == null ? "" : JSON.stringify(content) ?? "";
4527
5013
  }
5014
+ var TASK_NOTIFICATION_RE = /<task-notification>([\s\S]*?)<\/task-notification>/g;
5015
+ var NOTIFICATION_SUMMARY_MAX = 500;
5016
+ var NOTIFICATION_REPORT_MAX = 20000;
5017
+ function innerTag(body, tag) {
5018
+ const m = new RegExp(`<${tag}>([\\s\\S]*?)</${tag}>`).exec(body);
5019
+ const text = m?.[1]?.trim() ?? "";
5020
+ return text.length > 0 ? text : null;
5021
+ }
5022
+ function unescapeEntities(s) {
5023
+ return s.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&amp;/g, "&");
5024
+ }
5025
+ var clip = (s, max) => s.length > max ? `${s.slice(0, max - 1)}…` : s;
5026
+ function intTag(body, tag) {
5027
+ const raw = innerTag(body, tag);
5028
+ if (!raw)
5029
+ return;
5030
+ const n = Number.parseInt(raw, 10);
5031
+ return Number.isFinite(n) && n >= 0 ? n : undefined;
5032
+ }
5033
+ function parseTaskNotifications(text, timestamp) {
5034
+ if (!text.includes("<task-notification>"))
5035
+ return [];
5036
+ const out = [];
5037
+ TASK_NOTIFICATION_RE.lastIndex = 0;
5038
+ for (let m = TASK_NOTIFICATION_RE.exec(text);m !== null; m = TASK_NOTIFICATION_RE.exec(text)) {
5039
+ const body = m[1] ?? "";
5040
+ const taskId = innerTag(body, "task-id");
5041
+ if (!taskId)
5042
+ continue;
5043
+ const toolUseId = innerTag(body, "tool-use-id");
5044
+ const summary = innerTag(body, "summary");
5045
+ const report = innerTag(body, "result");
5046
+ const usageBody = /<usage>([\s\S]*?)<\/usage>/.exec(body)?.[1] ?? "";
5047
+ const tokens = intTag(usageBody, "subagent_tokens");
5048
+ const toolUses = intTag(usageBody, "tool_uses");
5049
+ const durationMs = intTag(usageBody, "duration_ms");
5050
+ out.push({
5051
+ type: "task_notification",
5052
+ taskId: clip(taskId, 128),
5053
+ ...toolUseId ? { toolUseId: clip(toolUseId, 128) } : {},
5054
+ status: innerTag(body, "status") ?? "finished",
5055
+ summary: summary ? clip(unescapeEntities(summary.replace(/\s+/g, " ")), NOTIFICATION_SUMMARY_MAX) : "",
5056
+ ...report ? { report: clip(unescapeEntities(report), NOTIFICATION_REPORT_MAX) } : {},
5057
+ ...tokens !== undefined || toolUses !== undefined || durationMs !== undefined ? { usage: {
5058
+ ...tokens !== undefined ? { tokens } : {},
5059
+ ...toolUses !== undefined ? { toolUses } : {},
5060
+ ...durationMs !== undefined ? { durationMs } : {}
5061
+ } } : {},
5062
+ timestamp
5063
+ });
5064
+ }
5065
+ return out;
5066
+ }
5067
+ function parseSystemTaskNotification(p, timestamp) {
5068
+ const taskId = typeof p.task_id === "string" && p.task_id.trim().length > 0 ? p.task_id.trim() : null;
5069
+ if (!taskId)
5070
+ return null;
5071
+ const toolUseId = typeof p.tool_use_id === "string" && p.tool_use_id.length > 0 ? p.tool_use_id : null;
5072
+ const summary = typeof p.summary === "string" ? p.summary : "";
5073
+ const nonneg = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0 ? Math.floor(v) : undefined;
5074
+ const u = typeof p.usage === "object" && p.usage !== null ? p.usage : {};
5075
+ const tokens = nonneg(u.total_tokens);
5076
+ const toolUses = nonneg(u.tool_uses);
5077
+ const durationMs = nonneg(u.duration_ms);
5078
+ return {
5079
+ type: "task_notification",
5080
+ taskId: clip(taskId, 128),
5081
+ ...toolUseId ? { toolUseId: clip(toolUseId, 128) } : {},
5082
+ status: typeof p.status === "string" && p.status.length > 0 ? p.status : "finished",
5083
+ summary: clip(summary.replace(/\s+/g, " ").trim(), NOTIFICATION_SUMMARY_MAX),
5084
+ ...tokens !== undefined || toolUses !== undefined || durationMs !== undefined ? { usage: {
5085
+ ...tokens !== undefined ? { tokens } : {},
5086
+ ...toolUses !== undefined ? { toolUses } : {},
5087
+ ...durationMs !== undefined ? { durationMs } : {}
5088
+ } } : {},
5089
+ timestamp
5090
+ };
5091
+ }
4528
5092
  var CLAUDE_CODE_EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
4529
5093
  var claudeCodeSpec = {
4530
5094
  kind: "claude-code",
@@ -4533,11 +5097,16 @@ var claudeCodeSpec = {
4533
5097
  acp: { command: "npx", args: ["--yes", CLAUDE_CODE_ACP_ADAPTER] },
4534
5098
  install: "curl -fsSL https://claude.ai/install.sh | bash && " + 'REAL=$(readlink -f "$HOME/.local/bin/claude") && test -f "$REAL" && ' + 'sudo cp "$REAL" /usr/local/bin/claude && sudo chmod 0755 /usr/local/bin/claude',
4535
5099
  promptPayload: (prompt) => prompt,
4536
- buildCommand: ({ promptPath, sessionId, model, cwd, effort }) => {
5100
+ streamInput: {
5101
+ promptLine: (prompt) => JSON.stringify({ type: "user", message: { role: "user", content: prompt } }),
5102
+ messageLine: (text) => JSON.stringify({ type: "user", message: { role: "user", content: text } })
5103
+ },
5104
+ buildCommand: ({ promptPath, sessionId, model, cwd, effort, streamInput }) => {
4537
5105
  const flags = [
4538
5106
  "-p",
4539
5107
  "--output-format stream-json",
4540
5108
  "--verbose",
5109
+ ...streamInput ? ["--input-format stream-json"] : [],
4541
5110
  "--include-partial-messages",
4542
5111
  "--dangerously-skip-permissions",
4543
5112
  ...model ? [`--model ${shellQuote(model)}`] : [],
@@ -4553,10 +5122,11 @@ var claudeCodeSpec = {
4553
5122
  switch (p.type) {
4554
5123
  case "assistant": {
4555
5124
  const message = p.message;
5125
+ const synthetic = message?.model === "<synthetic>";
4556
5126
  const blocks2 = Array.isArray(message?.content) ? message.content : [];
4557
5127
  return blocks2.flatMap((b) => {
4558
5128
  if (b.type === "text" && typeof b.text === "string" && b.text.length > 0) {
4559
- return [{ type: "text", text: b.text, timestamp: ts }];
5129
+ return [{ type: synthetic ? "harness_notice" : "text", text: b.text, timestamp: ts }];
4560
5130
  }
4561
5131
  if (b.type === "thinking" && typeof b.thinking === "string" && b.thinking.length > 0) {
4562
5132
  return [{ type: "thinking", text: b.thinking, timestamp: ts }];
@@ -4576,15 +5146,26 @@ var claudeCodeSpec = {
4576
5146
  }
4577
5147
  case "user": {
4578
5148
  const message = p.message;
5149
+ if (typeof message?.content === "string") {
5150
+ return parseTaskNotifications(message.content, ts);
5151
+ }
4579
5152
  const blocks2 = Array.isArray(message?.content) ? message.content : [];
4580
- return blocks2.flatMap((b) => b.type === "tool_result" ? [{
4581
- type: "tool_result",
4582
- toolUseId: String(b.tool_use_id ?? ""),
4583
- output: toolResultText(b.content),
4584
- isError: b.is_error === true,
4585
- ...parent,
4586
- timestamp: ts
4587
- }] : []);
5153
+ return blocks2.flatMap((b) => {
5154
+ if (b.type === "tool_result") {
5155
+ return [{
5156
+ type: "tool_result",
5157
+ toolUseId: String(b.tool_use_id ?? ""),
5158
+ output: toolResultText(b.content),
5159
+ isError: b.is_error === true,
5160
+ ...parent,
5161
+ timestamp: ts
5162
+ }];
5163
+ }
5164
+ if (b.type === "text" && typeof b.text === "string") {
5165
+ return parseTaskNotifications(b.text, ts);
5166
+ }
5167
+ return [];
5168
+ });
4588
5169
  }
4589
5170
  case "stream_event": {
4590
5171
  const ev = p.event;
@@ -4634,6 +5215,12 @@ var claudeCodeSpec = {
4634
5215
  timestamp: ts
4635
5216
  }];
4636
5217
  }
5218
+ case "system": {
5219
+ if (p.subtype !== "task_notification")
5220
+ return [];
5221
+ const notification = parseSystemTaskNotification(p, ts);
5222
+ return notification ? [notification] : [];
5223
+ }
4637
5224
  default:
4638
5225
  return [];
4639
5226
  }
@@ -4855,6 +5442,7 @@ function makeSandboxProvider(sb) {
4855
5442
  function e2bMaxSandboxMs() {
4856
5443
  return Number(process.env.E2B_MAX_SANDBOX_MS) || 60 * 60 * 1000;
4857
5444
  }
5445
+ var e2bExtendClampWarned = false;
4858
5446
  function wrapE2bBackgroundProcess(handle) {
4859
5447
  return {
4860
5448
  pid: handle.pid,
@@ -4948,7 +5536,12 @@ function makeE2bSandboxProvider(sb) {
4948
5536
  return { resumeHandle: sb.sandboxId };
4949
5537
  },
4950
5538
  async extendLifetime(ms) {
4951
- await sb.setTimeout(Math.min(ms, e2bMaxSandboxMs()));
5539
+ const maxMs = e2bMaxSandboxMs();
5540
+ if (ms > maxMs && !e2bExtendClampWarned) {
5541
+ e2bExtendClampWarned = true;
5542
+ console.warn(`[sandbox] e2b lifetime extension clamped: requested ${ms}ms exceeds the plan cap ${maxMs}ms — ` + `the kill-clock backstop fires at the cap, not the requested horizon. Raise E2B_MAX_SANDBOX_MS on plans that allow more.`);
5543
+ }
5544
+ await sb.setTimeout(Math.min(ms, maxMs));
4952
5545
  },
4953
5546
  async updateNetworkPolicy(policy) {
4954
5547
  await sb.updateNetwork(toE2bNetwork(policy));
@@ -4980,6 +5573,7 @@ var e2bProviderDef = {
4980
5573
  const sandboxOpts = {
4981
5574
  ...rest,
4982
5575
  timeoutMs: Math.min(timeoutMs, maxMs),
5576
+ lifecycle: { onTimeout: "pause", autoResume: true },
4983
5577
  ...networkPolicy ? { network: toE2bNetwork(networkPolicy) } : {}
4984
5578
  };
4985
5579
  const resolvedSize = size ?? DEFAULT_SANDBOX_SIZE;
@@ -5252,8 +5846,9 @@ function makeLocalSandboxProvider() {
5252
5846
  stderr += chunk;
5253
5847
  opts?.onStderr?.(chunk);
5254
5848
  });
5255
- proc.on("error", reject);
5256
- proc.on("close", (code) => resolve({ exitCode: code ?? 0, stdout, stderr }));
5849
+ const ev = proc;
5850
+ ev.on("error", reject);
5851
+ ev.on("close", (code) => resolve({ exitCode: code ?? 0, stdout, stderr }));
5257
5852
  });
5258
5853
  },
5259
5854
  spawnDuplex(cmd, opts) {
@@ -5269,8 +5864,9 @@ function makeLocalSandboxProvider() {
5269
5864
  stderr += chunk;
5270
5865
  });
5271
5866
  const exited = new Promise((resolve, reject) => {
5272
- proc.on("error", reject);
5273
- proc.on("close", (code) => resolve({ exitCode: code ?? 0, stderr }));
5867
+ const ev = proc;
5868
+ ev.on("error", reject);
5869
+ ev.on("close", (code) => resolve({ exitCode: code ?? 0, stderr }));
5274
5870
  });
5275
5871
  return {
5276
5872
  stdin: Writable.toWeb(proc.stdin),
@@ -6574,27 +7170,81 @@ like?" is answered by opening it on this desktop and screenshotting it — not b
6574
7170
  reasoning about the code, and not by a headless render (which proves the process
6575
7171
  starts, not that the thing draws). Verify visually before you report visually.
6576
7172
 
7173
+ **This is how you ACT on the web.** When the task is to DO something on a
7174
+ website — book, order, reserve, sign up, fill a form, operate a dashboard —
7175
+ and no connector or API covers it, the desktop browser IS the tool: \`ac-open\`
7176
+ the site, do the errand there, and show the human the screen at decision
7177
+ points (\`agentc display desktop\` in a cloud session). Research/search tools
7178
+ answer QUESTIONS; an errand is an ACTION — "book me a table" means open the
7179
+ booking site and book it, never a research report of options.
7180
+
6577
7181
  - **Input** — \`xdotool\` against \`DISPLAY=:0\`: \`DISPLAY=:0 xdotool mousemove <x> <y>\`,
6578
7182
  \`DISPLAY=:0 xdotool click 1\` (1=left, 3=right), \`DISPLAY=:0 xdotool type 'text'\`,
6579
7183
  \`DISPLAY=:0 xdotool key Return\` (also \`ctrl+c\`, \`Tab\`, \`super\`, …).
6580
7184
  - **Screenshots** — \`scrot\` (or ImageMagick's \`import\`):
6581
7185
  \`DISPLAY=:0 scrot /tmp/screen.png\`, then READ the PNG to see the screen,
6582
7186
  before and after you act. A screenshot is your only eyes here.
6583
- - **Apps + windows** — a plain X session. Launch in the background:
6584
- \`DISPLAY=:0 <app> &\`. Two things that trip agents up, both normal:
7187
+ - **The browser is chromium, preinstalled** — headful, on this display
7188
+ (\`command -v chromium\` to confirm on an older machine). If an older machine
7189
+ is missing it, the platform is already installing it in the background from
7190
+ boot — \`ac-open <url>\` tells you when that is the case; retry it in ~30s.
7191
+ Only if \`ac-open\` reports the background install FAILED do you relay that
7192
+ one line to the human — never an apt-get expedition of your own.
7193
+ - **Launching apps — use \`ac-open\`, never a plain \`&\`.** A GUI process
7194
+ launched with \`<app> &\` DIES the moment your shell command returns — the
7195
+ sandbox reaps each command's process group, so "the window vanished when
7196
+ the shell finished" is that reaping, not a broken app. \`ac-open\` is the
7197
+ platform launcher that survives it (\`command -v ac-open\` on older machines):
7198
+
7199
+ ac-open https://github.com # the browser — a running instance gets a tab
7200
+ ac-open ./report.html # a local file, in the browser
7201
+ ac-open . # a directory, in the file manager
7202
+ ac-open gimp # any GUI app by command name
7203
+
7204
+ It detaches the app into its own session (setsid, stdio off your command's
7205
+ pipes), records a pidfile + log under \`/tmp/.ac-desktop-open.<uid>/\`
7206
+ (per-uid — yours is \`/tmp/.ac-desktop-open.$(id -u)\`), and
7207
+ re-invoking it for a running app FOCUSES the existing window instead of
7208
+ spawning a second copy. \`xdg-open\` and \`sensible-browser\` route through
7209
+ it too. The whole recipe for looking at a page: \`ac-open <url>\`, then
7210
+ \`sleep 5\`, then \`DISPLAY=:0 scrot /tmp/screen.png\` and read it. Without
7211
+ \`ac-open\` (older machine), detach by hand:
7212
+ \`setsid <app> </dev/null >/tmp/app.log 2>&1 &\` — and note **chromium as
7213
+ root also needs \`--no-sandbox\`** (nested sandbox; \`ac-open\` and the baked
7214
+ chromium defaults already handle it).
7215
+ - **Two things that trip agents up, both normal:**
6585
7216
  - a GUI app needs a **beat to map its window** — screenshot, and if you see
6586
7217
  only wallpaper, wait a couple of seconds and screenshot again before
6587
7218
  concluding anything;
6588
- - **Chromium needs \`--no-sandbox\`** in this environment (nested sandbox).
6589
- The whole recipe for looking at a local page:
6590
- \`DISPLAY=:0 chromium --no-sandbox --disable-gpu --start-maximized <url> &\`
6591
- then \`sleep 5\`, then \`DISPLAY=:0 scrot /tmp/screen.png\` and read it.
6592
- If a window still never appears, read the app's own log (\`/tmp/*.log\`) — the
6593
- desktop is not the thing that failed. Do NOT abandon it for a headless
6594
- screenshot: headless cannot tell you what the human will see.
7219
+ - if a window still never appears, read the app's own log
7220
+ (\`/tmp/.ac-desktop-open.$(id -u)/*.log\`, \`/tmp/*.log\`) — the desktop is not the
7221
+ thing that failed. Do NOT abandon it for a headless
7222
+ screenshot: headless cannot tell you what the human will see.
6595
7223
  - **A human can watch** — the session header carries a **Desktop** button in the
6596
7224
  dashboard, and what a teammate sees there is exactly this display. The desktop
6597
7225
  runs whether or not anyone is looking; never wait for a viewer.
7226
+ - **Show the human the screen** — in a cloud session,
7227
+ \`agentc display desktop --note "<caption>"\` captures this display and posts
7228
+ it into the conversation as a snapshot card with an "Open desktop" door to
7229
+ the live view. Use it to report visual results, and ALWAYS when you hit a
7230
+ wall on the desktop that only a human can clear — a login form, a 2FA
7231
+ prompt, a CAPTCHA, an unexpected dialog: snapshot it so they SEE the wall,
7232
+ then ask (AskUserQuestion when you have it) and wait; never guess
7233
+ credentials or click around a wall. The rule is SCREEN FOR ACTIONS,
7234
+ VAULT FOR SECRETS. For non-sensitive interaction that needs the human's
7235
+ own hands or judgment — pick an option, review a page, solve a CAPTCHA —
7236
+ the display + ask pair is right: the platform merges them into ONE live
7237
+ desktop card — the human clicks in, acts on the live screen, and answers
7238
+ "I'm done" to hand it back; treat that answer as the wall being cleared,
7239
+ re-check the screen, and continue. For SECRETS — a password, payment
7240
+ details, any sensitive value —
7241
+ \`agentc secrets session request <KEY...> --reason "<why>" --wait\` mints a
7242
+ secure vault link (a one-tap approval when the user has these saved as a
7243
+ personal set); the values land in the session env and YOU type them into
7244
+ the site on the user's behalf. Never ask the human to type a password or
7245
+ card number into this machine's browser, and never suggest they "log in
7246
+ on the Desktop view" — the vault carries the secret, then you act with
7247
+ it. A one-time 2FA code from their phone is the chat-OK exception.
6598
7248
 
6599
7249
  Nothing here changes the credentials rule above: tokens are injected at the
6600
7250
  network layer, never present on the desktop or in any file you can read — so
@@ -6685,14 +7335,49 @@ origin of its own — absolute asset paths and client-side routing work, the
6685
7335
  whole app is navigable — so serve normally and let the platform address it;
6686
7336
  never rewrite your app to a path prefix.
6687
7337
 
7338
+ ## Durable services — the machine is cattle, the manifest is the pet (cloud sessions)
7339
+
7340
+ Parking preserves detached processes; a machine RECYCLE (resize, eviction,
7341
+ failed reconnect) does not — every process and every byte off the drive is
7342
+ discarded, and recycles are normal. When you start a long-running service the
7343
+ human will rely on across turns (a dev server, a docker compose stack, a
7344
+ database), record it in \`.ac/services.yml\` at the drive root so the platform
7345
+ relaunches it automatically on the next fresh machine:
7346
+
7347
+ agentc services add <name> --command '<cmd>' # record a service
7348
+ agentc services list # manifest + live status
7349
+ agentc services restore # run the manifest now
7350
+ agentc services remove <name>
7351
+
7352
+ Each entry can carry \`cwd\`, \`port\`, a bounded \`health\` probe (cmd or
7353
+ http), one-time \`setup\` (e.g. \`docker compose pull\`), and \`data\` hooks.
7354
+ After a recycle the platform posts "Machine restarted — restored N services"
7355
+ into the conversation; on seeing it, VERIFY health rather than rebuilding —
7356
+ logs live at \`/tmp/ac-services/<name>.log\`. Data honesty: sandbox-local
7357
+ database state dies with the machine. Keep seeds/dumps ON THE DRIVE; declare
7358
+ \`data.restore\` (reload on fresh boot) and \`data.dump\` (written before a
7359
+ DELIBERATE recycle such as a resize — evictions give no warning, so treat the
7360
+ drive copy as the truth).
7361
+
6688
7362
  ## Tools in this environment
6689
7363
 
6690
7364
  - \`agentc\` — Agent Compose CLI (your primary interface; authed from env)
6691
7365
  - \`@agent-compose/sdk\` — installed in /workspace for writing workflows
6692
7366
  - \`/ac:*\` Claude Code skills — slash commands for the above
6693
- - \`archil\` (factory drive), \`rtk\`, \`bun\`
7367
+ - \`rtk\`, \`bun\`
6694
7368
  - \`xdotool\` / \`scrot\` — drive + screenshot the desktop (if this machine has one; see Computer Use)
6695
- - A world-writable \`/workspace\` working directory`;
7369
+ - \`chromium\` — the desktop browser; \`ac-open <url|file|app>\` — open it on the
7370
+ desktop, detached (survives your command; see Computer Use)
7371
+ - A world-writable \`/workspace\` working directory
7372
+
7373
+ If a system capability you need is genuinely missing — no browser, no display,
7374
+ no \`ac-open\`, a daemon that isn't there — say so to the human in ONE honest
7375
+ line (what is missing and what it blocks) instead of mounting a
7376
+ package-manager expedition. An in-session \`apt-get install\` dies with the
7377
+ sandbox, burns turns, and hides the real gap; missing platform capabilities
7378
+ are the platform's to bake in, and \`agentc pause\` is the door to ask through.
7379
+ (Your own project's dependencies are different — installing those is normal
7380
+ work.)`;
6696
7381
  function buildAddedSessionBrief(p) {
6697
7382
  const dashboardSection = p.dashboardUrl === null ? "" : `
6698
7383
 
@@ -6999,6 +7684,565 @@ async function agent(opts) {
6999
7684
  inboxAbort.abort();
7000
7685
  }
7001
7686
  }
7687
+ // src/agent/desktop-open.ts
7688
+ var AC_OPEN_PATH = "/usr/local/bin/ac-open";
7689
+ var AC_OPEN_STATE_DIR = "/tmp/.ac-desktop-open";
7690
+ var AC_OPEN_SHIM_PATHS = [
7691
+ "/usr/local/bin/xdg-open",
7692
+ "/usr/local/bin/sensible-browser"
7693
+ ];
7694
+ var BROWSER_BACKFILL_PIDFILE = `${AC_OPEN_STATE_DIR}/browser-install.pid`;
7695
+ var BROWSER_BACKFILL_LOG = `${AC_OPEN_STATE_DIR}/browser-install.log`;
7696
+ var BROWSER_BACKFILL_FAILED = `${AC_OPEN_STATE_DIR}/browser-install.failed`;
7697
+ var BROWSER_INSTALL_IN_PROGRESS_MSG = "ac-open: no browser YET — the platform is installing chromium in the background right now; retry this exact command in ~30s";
7698
+ var AC_OPEN_SCRIPT = `#!/bin/sh
7699
+ # ac-open — open a URL, file, or GUI app on this machine's desktop, DETACHED.
7700
+ #
7701
+ # ac-open https://github.com the browser (a running instance gets a tab)
7702
+ # ac-open ./report.html a local file, in the browser
7703
+ # ac-open . a directory, in the file manager
7704
+ # ac-open gimp [args...] any GUI app by command name
7705
+ #
7706
+ # Why: the sandbox reaps each exec'd command's process group when the command
7707
+ # returns, so a GUI app launched with a plain \`&\` dies the moment the
7708
+ # launching shell finishes. ac-open detaches the app into its own session
7709
+ # (setsid, stdio off the exec's pipes), records a pidfile, and returns at
7710
+ # once; re-invoking it for a running app focuses the existing window instead
7711
+ # of spawning a second copy. Logs + pidfiles: /tmp/.ac-desktop-open.<uid>/.
7712
+ # Written by agent-compose (sdk/src/agent/desktop-open.ts) — do not edit in place.
7713
+ set -u
7714
+ export DISPLAY="\${DISPLAY:-:0}"
7715
+ # Per-UID state (pidfiles + logs): the shared dir is root-owned when the
7716
+ # backfill created it, and a user-side >>$LOG into a root 0755 dir fails
7717
+ # BEFORE the app runs — the false "exited immediately" with an empty log.
7718
+ D="/tmp/.ac-desktop-open.$(id -u)"
7719
+ # Shared, root-written backfill markers — READ-ONLY here.
7720
+ M=/tmp/.ac-desktop-open
7721
+ mkdir -p "$D" 2>/dev/null || true
7722
+ [ -w "$D" ] || { echo "ac-open: state dir $D is not writable by uid $(id -u)" >&2; exit 1; }
7723
+
7724
+ [ $# -ge 1 ] || { echo 'usage: ac-open <url|file|dir|app> [args...]' >&2; exit 2; }
7725
+ TARGET=$1; shift
7726
+
7727
+ # No display = no desktop on this image. One honest line, no expedition.
7728
+ if ! timeout 3 xdotool getdisplaygeometry >/dev/null 2>&1; then
7729
+ echo "ac-open: no desktop display on $DISPLAY — this machine has no GUI session up" >&2
7730
+ exit 1
7731
+ fi
7732
+
7733
+ pick_browser() {
7734
+ for b in chromium chromium-browser x-www-browser google-chrome; do
7735
+ command -v "$b" >/dev/null 2>&1 && { echo "$b"; return 0; }
7736
+ done
7737
+ return 1
7738
+ }
7739
+
7740
+ # Best-effort: raise the newest visible window whose class matches $1.
7741
+ focus_class() {
7742
+ WID=$(xdotool search --onlyvisible --class "$1" 2>/dev/null | tail -1) || WID=''
7743
+ [ -n "\${WID:-}" ] && xdotool windowactivate "$WID" 2>/dev/null || true
7744
+ }
7745
+
7746
+ # Same-UID instance probe: another identity's instance shares neither profile
7747
+ # nor single-instance socket, so only our own uid's counts as "running".
7748
+ alive_same_uid() { pgrep -x -u "$(id -u)" "$1" >/dev/null 2>&1; }
7749
+
7750
+ # Singleton apps (browser, file manager): the second invocation hands the
7751
+ # target to the running instance and exits — that IS the idempotent path, so
7752
+ # spawn detached every time and only liveness-check a FRESH instance.
7753
+ singleton_open() { # $1 = state key, rest = command + target
7754
+ NAME=$1; shift
7755
+ BIN=$(basename "$1")
7756
+ PIDFILE=$D/$NAME.pid; LOG=$D/$NAME.log
7757
+ RUNNING=0
7758
+ alive_same_uid "$BIN" && RUNNING=1
7759
+ [ "$RUNNING" = 0 ] && [ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE" 2>/dev/null)" 2>/dev/null && RUNNING=1
7760
+ setsid "$@" </dev/null >>"$LOG" 2>&1 &
7761
+ PID=$!
7762
+ if [ "$RUNNING" = 1 ]; then
7763
+ sleep 1
7764
+ focus_class "$BIN"
7765
+ echo "ac-open: handed off to the running $BIN — no second window"
7766
+ return 0
7767
+ fi
7768
+ echo "$PID" > "$PIDFILE"
7769
+ sleep 1
7770
+ if ! kill -0 "$PID" 2>/dev/null; then
7771
+ # Died within the liveness window — but if OUR instance of $BIN is alive
7772
+ # NOW, this was the single-instance HAND-OFF: the pre-check raced a
7773
+ # still-starting instance, the fresh process handed the target over and
7774
+ # exited 0. That is success (N calls = N tabs), never a crash report.
7775
+ if alive_same_uid "$BIN"; then
7776
+ focus_class "$BIN"
7777
+ echo "ac-open: handed off to the running $BIN — no second window"
7778
+ return 0
7779
+ fi
7780
+ TAIL=$(tail -c 300 "$LOG" 2>/dev/null | tr '\\n' ' ')
7781
+ echo "ac-open: $BIN exited immediately: \${TAIL:-no output captured — see $LOG}" >&2
7782
+ exit 1
7783
+ fi
7784
+ focus_class "$BIN"
7785
+ echo "ac-open: launched $BIN (pid $PID), detached — it survives this command"
7786
+ }
7787
+
7788
+ open_url() { # $1 = url
7789
+ B=$(pick_browser) || {
7790
+ # Grandfathered image mid-heal: the boot-time desktop-ensure found no
7791
+ # browser and detached an apt install (browserBackfillCmd — the pidfile
7792
+ # below is ITS single-flight marker, root-written in the SHARED dir).
7793
+ # Third honest state: arriving.
7794
+ if [ -f "$M/browser-install.pid" ] && kill -0 "$(cat "$M/browser-install.pid" 2>/dev/null)" 2>/dev/null; then
7795
+ echo "${BROWSER_INSTALL_IN_PROGRESS_MSG}" >&2
7796
+ exit 75
7797
+ fi
7798
+ if [ -f "$M/browser-install.failed" ]; then
7799
+ TAIL=$(tail -c 300 "$M/browser-install.log" 2>/dev/null | tr '\\n' ' ')
7800
+ echo "ac-open: the platform's background chromium install FAILED: \${TAIL:-see $M/browser-install.log} — report that cause in one line; do not apt-get one yourself." >&2
7801
+ exit 1
7802
+ fi
7803
+ echo "ac-open: no graphical browser on this image (expected chromium) — cannot open $1. Report the missing browser in one line; do not apt-get one mid-session." >&2
7804
+ exit 1
7805
+ }
7806
+ singleton_open browser "$B" "$1"
7807
+ }
7808
+
7809
+ open_app() { # $1 = command, rest = args
7810
+ command -v "$1" >/dev/null 2>&1 || {
7811
+ echo "ac-open: '$1' is not installed on this machine — report the missing tool in one line instead of package-hunting for it" >&2
7812
+ exit 127
7813
+ }
7814
+ BIN=$(basename "$1"); PIDFILE=$D/app-$BIN.pid; LOG=$D/app-$BIN.log
7815
+ OLD=$(cat "$PIDFILE" 2>/dev/null || true)
7816
+ if [ -n "\${OLD:-}" ] && kill -0 "$OLD" 2>/dev/null; then
7817
+ WID=$(xdotool search --onlyvisible --pid "$OLD" 2>/dev/null | tail -1) || WID=''
7818
+ [ -z "\${WID:-}" ] && { WID=$(xdotool search --onlyvisible --class "$BIN" 2>/dev/null | tail -1) || WID=''; }
7819
+ [ -n "\${WID:-}" ] && xdotool windowactivate "$WID" 2>/dev/null || true
7820
+ echo "ac-open: $BIN is already running (pid $OLD) — focused it instead of starting a second copy"
7821
+ exit 0
7822
+ fi
7823
+ CMD=$1; shift
7824
+ setsid "$CMD" "$@" </dev/null >>"$LOG" 2>&1 &
7825
+ PID=$!
7826
+ echo "$PID" > "$PIDFILE"
7827
+ sleep 1
7828
+ if ! kill -0 "$PID" 2>/dev/null; then
7829
+ # Same hand-off grace as the singletons: VS Code and friends are
7830
+ # single-instance too — a fresh spawn that exits at once while our own
7831
+ # instance is alive HANDED OFF, it did not crash.
7832
+ if alive_same_uid "$BIN"; then
7833
+ focus_class "$BIN"
7834
+ echo "ac-open: handed off to the running $BIN — no second window"
7835
+ exit 0
7836
+ fi
7837
+ TAIL=$(tail -c 300 "$LOG" 2>/dev/null | tr '\\n' ' ')
7838
+ echo "ac-open: $BIN exited immediately: \${TAIL:-no output captured — see $LOG}" >&2
7839
+ exit 1
7840
+ fi
7841
+ echo "ac-open: launched $BIN (pid $PID), detached — it survives this command"
7842
+ }
7843
+
7844
+ case "$TARGET" in
7845
+ http://*|https://*|file://*|about:*|chrome://*)
7846
+ open_url "$TARGET" ;;
7847
+ localhost|localhost:*|localhost/*|127.0.0.1|127.0.0.1:*|127.0.0.1/*)
7848
+ open_url "http://$TARGET" ;;
7849
+ *)
7850
+ if [ -d "$TARGET" ]; then
7851
+ command -v pcmanfm >/dev/null 2>&1 || {
7852
+ echo "ac-open: no file manager on this image (expected pcmanfm) — report it in one line" >&2
7853
+ exit 1
7854
+ }
7855
+ case "$TARGET" in /*) ABS=$TARGET ;; *) ABS=$PWD/$TARGET ;; esac
7856
+ singleton_open files pcmanfm "$ABS"
7857
+ elif [ -f "$TARGET" ]; then
7858
+ case "$TARGET" in /*) ABS=$TARGET ;; *) ABS=$PWD/$TARGET ;; esac
7859
+ open_url "file://$ABS"
7860
+ else
7861
+ open_app "$TARGET" "$@"
7862
+ fi ;;
7863
+ esac
7864
+ `;
7865
+ var AC_OPEN_SHIM = `#!/bin/sh
7866
+ # agent-compose: generic open routes through ac-open (the detaching launcher).
7867
+ exec ${AC_OPEN_PATH} "$@"
7868
+ `;
7869
+ function installDesktopOpenCmd() {
7870
+ const scriptB64 = Buffer.from(AC_OPEN_SCRIPT, "utf8").toString("base64");
7871
+ const shimB64 = Buffer.from(AC_OPEN_SHIM, "utf8").toString("base64");
7872
+ return [
7873
+ `printf %s '${scriptB64}' | base64 -d > ${AC_OPEN_PATH}`,
7874
+ `chmod 0755 ${AC_OPEN_PATH}`,
7875
+ `sh -n ${AC_OPEN_PATH}`,
7876
+ ...AC_OPEN_SHIM_PATHS.flatMap((p) => [
7877
+ `printf %s '${shimB64}' | base64 -d > ${p}`,
7878
+ `chmod 0755 ${p}`,
7879
+ `sh -n ${p}`
7880
+ ])
7881
+ ].join(" && ");
7882
+ }
7883
+ var CHROMIUM_DEFAULTS_PATH = "/etc/chromium.d/00-ac-defaults";
7884
+ var CHROMIUM_DEFAULTS = `# agent-compose session-desktop chromium defaults
7885
+ # (single-sourced in sdk/src/agent/desktop-open.ts — sourced by /usr/bin/chromium)
7886
+ CHROMIUM_FLAGS="$CHROMIUM_FLAGS --no-first-run --no-default-browser-check --password-store=basic --disable-session-crashed-bubble --disable-dev-shm-usage --start-maximized --disable-gpu"
7887
+ if [ "$(id -u)" = "0" ]; then CHROMIUM_FLAGS="$CHROMIUM_FLAGS --no-sandbox"; fi
7888
+ `;
7889
+ function installChromiumDefaultsCmd() {
7890
+ const b64 = Buffer.from(CHROMIUM_DEFAULTS, "utf8").toString("base64");
7891
+ return [
7892
+ "mkdir -p /etc/chromium.d",
7893
+ `printf %s ${b64} | base64 -d > ${CHROMIUM_DEFAULTS_PATH}`,
7894
+ `chmod 0644 ${CHROMIUM_DEFAULTS_PATH}`,
7895
+ `sh -n ${CHROMIUM_DEFAULTS_PATH}`
7896
+ ].join(" && ");
7897
+ }
7898
+ function browserBackfillCmd() {
7899
+ const pid = BROWSER_BACKFILL_PIDFILE;
7900
+ return `mkdir -p ${AC_OPEN_STATE_DIR} && ` + `if command -v chromium >/dev/null 2>&1; then echo backfill=present; ` + `elif [ -f ${pid} ] && kill -0 "$(cat ${pid} 2>/dev/null)" 2>/dev/null; then echo backfill=in-progress; ` + `else rm -f ${BROWSER_BACKFILL_FAILED}; ` + `setsid sh -c 'if apt-get update -q && DEBIAN_FRONTEND=noninteractive apt-get install -y -q chromium && command -v chromium >/dev/null 2>&1 && ${installChromiumDefaultsCmd()}; then echo backfill-install=ok; else echo backfill-install=failed; touch ${BROWSER_BACKFILL_FAILED}; fi; rm -f ${pid}' </dev/null >>${BROWSER_BACKFILL_LOG} 2>&1 & ` + `echo $! > ${pid}; echo backfill=started; fi`;
7901
+ }
7902
+ function browserBackfillStatusCmd() {
7903
+ return `if command -v chromium >/dev/null 2>&1; then echo backfill=ok; ` + `elif [ -f ${BROWSER_BACKFILL_FAILED} ]; then echo backfill=failed; tail -c 300 ${BROWSER_BACKFILL_LOG} 2>/dev/null | tr '\\n' ' '; ` + `elif [ -f ${BROWSER_BACKFILL_PIDFILE} ] && kill -0 "$(cat ${BROWSER_BACKFILL_PIDFILE} 2>/dev/null)" 2>/dev/null; then echo backfill=in-progress; ` + `else echo backfill=lost; fi`;
7904
+ }
7905
+ // src/agent/services-manifest.ts
7906
+ var SERVICES_MANIFEST_RELPATH = ".ac/services.yml";
7907
+ var SERVICES_MAX = 16;
7908
+ var SERVICE_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,31}$/;
7909
+ var TOP_KEYS = new Set(["version", "services"]);
7910
+ var ENTRY_KEYS = new Set(["name", "command", "cwd", "port", "env", "setup", "health", "data"]);
7911
+ var HEALTH_KEYS = new Set(["cmd", "http"]);
7912
+ var DATA_KEYS = new Set(["dump", "restore"]);
7913
+ function lex(text) {
7914
+ const out = [];
7915
+ const raw = text.split(/\r?\n/);
7916
+ for (let i = 0;i < raw.length; i++) {
7917
+ const line = raw[i];
7918
+ const trimmed = line.trim();
7919
+ if (trimmed === "" || trimmed.startsWith("#"))
7920
+ continue;
7921
+ const indent = line.length - line.trimStart().length;
7922
+ const dash = trimmed.startsWith("- ") || trimmed === "-";
7923
+ out.push({
7924
+ no: i + 1,
7925
+ indent,
7926
+ dash,
7927
+ text: dash ? trimmed.slice(1).trim() : trimmed
7928
+ });
7929
+ }
7930
+ return out;
7931
+ }
7932
+ function splitKey(text) {
7933
+ if (text.endsWith(":") && !text.includes(": ")) {
7934
+ const key2 = text.slice(0, -1).trim();
7935
+ return /^[A-Za-z_][\w-]*$/.test(key2) ? { key: key2, value: "" } : null;
7936
+ }
7937
+ const idx = text.indexOf(": ");
7938
+ if (idx === -1)
7939
+ return null;
7940
+ const key = text.slice(0, idx).trim();
7941
+ if (!/^[A-Za-z_][\w-]*$/.test(key))
7942
+ return null;
7943
+ return { key, value: text.slice(idx + 2).trim() };
7944
+ }
7945
+ function scalar(value, no, errors) {
7946
+ if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
7947
+ return value.slice(1, -1).replace(/\\(["\\])/g, "$1");
7948
+ }
7949
+ if (value.length >= 2 && value.startsWith("'") && value.endsWith("'")) {
7950
+ return value.slice(1, -1).replace(/''/g, "'");
7951
+ }
7952
+ if (value === "|" || value === ">" || value.startsWith("| ") || value.startsWith("> ")) {
7953
+ errors.push(`line ${no}: block scalars (| / >) are not supported — keep commands single-line`);
7954
+ return "";
7955
+ }
7956
+ return value;
7957
+ }
7958
+ function parseServicesManifest(text) {
7959
+ const errors = [];
7960
+ if (text.length > 64 * 1024) {
7961
+ return { manifest: null, errors: ["manifest exceeds 64KB — this file is a service list, not a data store"] };
7962
+ }
7963
+ const lines = lex(text);
7964
+ if (lines.length === 0)
7965
+ return { manifest: null, errors: ["manifest is empty"] };
7966
+ let version = null;
7967
+ const services = [];
7968
+ let current = null;
7969
+ let currentIndent = -1;
7970
+ let nested = null;
7971
+ let nestedIndent = -1;
7972
+ let inServices = false;
7973
+ const finish = (entry) => {
7974
+ if (!entry)
7975
+ return;
7976
+ services.push(entry);
7977
+ };
7978
+ for (const line of lines) {
7979
+ const kv = splitKey(line.text);
7980
+ if (!kv) {
7981
+ errors.push(`line ${line.no}: not a \`key: value\` mapping — flow collections and bare scalars are not supported`);
7982
+ continue;
7983
+ }
7984
+ if (line.indent === 0 && !line.dash) {
7985
+ inServices = false;
7986
+ nested = null;
7987
+ finish(current);
7988
+ current = null;
7989
+ if (!TOP_KEYS.has(kv.key)) {
7990
+ errors.push(`line ${line.no}: unknown top-level key \`${kv.key}\` (allowed: version, services)`);
7991
+ continue;
7992
+ }
7993
+ if (kv.key === "version") {
7994
+ const v = Number(scalar(kv.value, line.no, errors));
7995
+ if (v !== 1)
7996
+ errors.push(`line ${line.no}: unsupported manifest version \`${kv.value}\` (this build understands version 1)`);
7997
+ else
7998
+ version = 1;
7999
+ } else {
8000
+ if (kv.value !== "")
8001
+ errors.push(`line ${line.no}: \`services:\` must open a list, not carry a value`);
8002
+ inServices = true;
8003
+ }
8004
+ continue;
8005
+ }
8006
+ if (!inServices) {
8007
+ errors.push(`line ${line.no}: unexpected indented line outside \`services:\``);
8008
+ continue;
8009
+ }
8010
+ if (line.dash) {
8011
+ finish(current);
8012
+ current = { name: "", command: "" };
8013
+ currentIndent = line.indent;
8014
+ nested = null;
8015
+ if (kv.key !== "name") {
8016
+ errors.push(`line ${line.no}: each service entry must start with \`- name: <name>\``);
8017
+ continue;
8018
+ }
8019
+ current.name = scalar(kv.value, line.no, errors);
8020
+ continue;
8021
+ }
8022
+ if (!current) {
8023
+ errors.push(`line ${line.no}: expected \`- name: <name>\` to open a service entry`);
8024
+ continue;
8025
+ }
8026
+ if (nested && line.indent > nestedIndent) {
8027
+ const allowed = nested === "health" ? HEALTH_KEYS : DATA_KEYS;
8028
+ if (!allowed.has(kv.key)) {
8029
+ errors.push(`line ${line.no}: unknown \`${nested}\` key \`${kv.key}\` (allowed: ${[...allowed].join(", ")})`);
8030
+ continue;
8031
+ }
8032
+ const value2 = scalar(kv.value, line.no, errors);
8033
+ if (value2 === "") {
8034
+ errors.push(`line ${line.no}: \`${nested}.${kv.key}\` needs a value`);
8035
+ continue;
8036
+ }
8037
+ const bucket = current[nested] ??= {};
8038
+ bucket[kv.key] = value2;
8039
+ continue;
8040
+ }
8041
+ nested = null;
8042
+ if (line.indent <= currentIndent) {
8043
+ errors.push(`line ${line.no}: bad indentation — service fields must be indented under their \`- name:\` line`);
8044
+ continue;
8045
+ }
8046
+ if (!ENTRY_KEYS.has(kv.key)) {
8047
+ errors.push(`line ${line.no}: unknown service key \`${kv.key}\` (allowed: ${[...ENTRY_KEYS].join(", ")})`);
8048
+ continue;
8049
+ }
8050
+ if (kv.key === "health" || kv.key === "data") {
8051
+ if (kv.value !== "") {
8052
+ errors.push(`line ${line.no}: \`${kv.key}:\` opens a nested map — put cmd/http (or dump/restore) on indented lines below`);
8053
+ continue;
8054
+ }
8055
+ nested = kv.key;
8056
+ nestedIndent = line.indent;
8057
+ continue;
8058
+ }
8059
+ const value = scalar(kv.value, line.no, errors);
8060
+ if (kv.key === "port") {
8061
+ const port = Number(value);
8062
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
8063
+ errors.push(`line ${line.no}: port must be an integer 1-65535, got \`${kv.value}\``);
8064
+ } else {
8065
+ current.port = port;
8066
+ }
8067
+ continue;
8068
+ }
8069
+ if (value === "") {
8070
+ errors.push(`line ${line.no}: \`${kv.key}\` needs a value`);
8071
+ continue;
8072
+ }
8073
+ if (kv.key === "cwd" && (value.startsWith("/") || value.includes(".."))) {
8074
+ errors.push(`line ${line.no}: cwd must be a relative path under the drive root (no leading / and no ..)`);
8075
+ continue;
8076
+ }
8077
+ current[kv.key] = value;
8078
+ }
8079
+ finish(current);
8080
+ if (version === null)
8081
+ errors.push("missing `version: 1`");
8082
+ if (services.length === 0 && errors.length === 0)
8083
+ errors.push("`services:` lists no entries");
8084
+ if (services.length > SERVICES_MAX)
8085
+ errors.push(`too many services (${services.length}) — the cap is ${SERVICES_MAX}`);
8086
+ const seen = new Set;
8087
+ for (const svc of services) {
8088
+ const label = svc.name || "<unnamed>";
8089
+ if (!SERVICE_NAME_RE.test(svc.name)) {
8090
+ errors.push(`service \`${label}\`: name must match ${SERVICE_NAME_RE} (alphanumeric start, then [A-Za-z0-9._-], max 32)`);
8091
+ } else if (seen.has(svc.name)) {
8092
+ errors.push(`service \`${label}\`: duplicate name`);
8093
+ }
8094
+ seen.add(svc.name);
8095
+ if (!svc.command)
8096
+ errors.push(`service \`${label}\`: missing \`command\``);
8097
+ if (svc.health?.cmd && svc.health.http) {
8098
+ errors.push(`service \`${label}\`: health takes \`cmd\` OR \`http\`, not both`);
8099
+ }
8100
+ if (svc.health?.http && !/^https?:\/\//.test(svc.health.http)) {
8101
+ errors.push(`service \`${label}\`: health.http must be an http(s):// URL`);
8102
+ }
8103
+ }
8104
+ return errors.length > 0 ? { manifest: null, errors } : { manifest: { version: 1, services }, errors: [] };
8105
+ }
8106
+ function emitScalar(value) {
8107
+ const needsQuote = value !== value.trim() || value.startsWith("'") || value.startsWith('"') || value === "|" || value === ">" || value.startsWith("| ") || value.startsWith("> ");
8108
+ if (!needsQuote)
8109
+ return value;
8110
+ return `"${value.replace(/([\\"])/g, "\\$1")}"`;
8111
+ }
8112
+ function renderServicesManifest(manifest) {
8113
+ const out = [
8114
+ "# Durable services manifest — restored automatically after a machine recycle.",
8115
+ "# Docs: agentc services --help",
8116
+ "version: 1",
8117
+ "services:"
8118
+ ];
8119
+ for (const svc of manifest.services) {
8120
+ out.push(` - name: ${emitScalar(svc.name)}`);
8121
+ out.push(` command: ${emitScalar(svc.command)}`);
8122
+ if (svc.cwd)
8123
+ out.push(` cwd: ${emitScalar(svc.cwd)}`);
8124
+ if (svc.port !== undefined)
8125
+ out.push(` port: ${svc.port}`);
8126
+ if (svc.env)
8127
+ out.push(` env: ${emitScalar(svc.env)}`);
8128
+ if (svc.setup)
8129
+ out.push(` setup: ${emitScalar(svc.setup)}`);
8130
+ if (svc.health && (svc.health.cmd || svc.health.http)) {
8131
+ out.push(" health:");
8132
+ if (svc.health.cmd)
8133
+ out.push(` cmd: ${emitScalar(svc.health.cmd)}`);
8134
+ if (svc.health.http)
8135
+ out.push(` http: ${emitScalar(svc.health.http)}`);
8136
+ }
8137
+ if (svc.data && (svc.data.dump || svc.data.restore)) {
8138
+ out.push(" data:");
8139
+ if (svc.data.dump)
8140
+ out.push(` dump: ${emitScalar(svc.data.dump)}`);
8141
+ if (svc.data.restore)
8142
+ out.push(` restore: ${emitScalar(svc.data.restore)}`);
8143
+ }
8144
+ }
8145
+ return out.join(`
8146
+ `) + `
8147
+ `;
8148
+ }
8149
+ // src/agent/services-restore.ts
8150
+ var SERVICES_STATE_DIR = "/tmp/ac-services";
8151
+ var SERVICE_STATUS_PREFIX = "##ac-service";
8152
+ var SERVICE_HEALTH_TIMEOUT_S = 45;
8153
+ function shq(s) {
8154
+ return `'${s.replace(/'/g, `'\\''`)}'`;
8155
+ }
8156
+ function shc(cmd) {
8157
+ return `sh -c ${shq(cmd)}`;
8158
+ }
8159
+ function serviceCwd(driveRoot, svc) {
8160
+ return svc.cwd ? `${driveRoot.replace(/\/+$/, "")}/${svc.cwd}` : driveRoot;
8161
+ }
8162
+ function restoreOneService(driveRoot, svc) {
8163
+ const n = svc.name;
8164
+ const cwd = serviceCwd(driveRoot, svc);
8165
+ const lines = [
8166
+ ``,
8167
+ `# --- ${n} ---`,
8168
+ `(`,
8169
+ ` cd ${shq(cwd)} || { status ${shq(n)} failed cwd-missing; exit 0; }`
8170
+ ];
8171
+ if (svc.setup) {
8172
+ lines.push(` if ! ${shc(svc.setup)} >> "$D/${n}.setup.log" 2>&1; then`, ` status ${shq(n)} failed setup; exit 0`, ` fi`);
8173
+ }
8174
+ if (svc.data?.restore) {
8175
+ lines.push(` if ! ${shc(svc.data.restore)} >> "$D/${n}.setup.log" 2>&1; then`, ` status ${shq(n)} failed data-restore; exit 0`, ` fi`);
8176
+ }
8177
+ lines.push(` $SETSID ${shc(svc.command)} >> "$D/${n}.log" 2>&1 < /dev/null &`, ` echo $! > "$D/${n}.pid"`);
8178
+ const health = svc.health?.cmd ? shc(svc.health.cmd) : svc.health?.http ? `curl -fsS -m 2 -o /dev/null ${shq(svc.health.http)}` : null;
8179
+ if (health) {
8180
+ lines.push(` i=0`, ` while [ "$i" -lt ${SERVICE_HEALTH_TIMEOUT_S} ]; do`, ` if ${health} >/dev/null 2>&1; then status ${shq(n)} ok ''; exit 0; fi`, ` kill -0 "$(cat "$D/${n}.pid")" 2>/dev/null || { status ${shq(n)} failed exited; exit 0; }`, ` i=$((i+1)); sleep 1`, ` done`, ` status ${shq(n)} failed health-timeout`);
8181
+ } else {
8182
+ lines.push(` sleep 1`, ` if kill -0 "$(cat "$D/${n}.pid")" 2>/dev/null; then status ${shq(n)} unchecked ''; else status ${shq(n)} failed exited; fi`);
8183
+ }
8184
+ lines.push(`)`);
8185
+ return lines;
8186
+ }
8187
+ function servicesRestoreScript(manifest, driveRoot) {
8188
+ const head = [
8189
+ `#!/bin/sh`,
8190
+ `# generated by agent-compose — restores services from ${driveRoot}/.ac/services.yml`,
8191
+ `# logs + pidfiles: ${SERVICES_STATE_DIR}/<name>.{log,pid}`,
8192
+ `D=${shq(SERVICES_STATE_DIR)}`,
8193
+ `mkdir -p "$D"`,
8194
+ `status() { printf '%s %s %s %s\\n' ${shq(SERVICE_STATUS_PREFIX)} "$1" "$2" "$3"; }`,
8195
+ `# setsid detaches from the boot shell's session (Linux guests); on hosts`,
8196
+ `# without it (macOS running \`agentc services restore\`) plain & still works.`,
8197
+ `SETSID=""`,
8198
+ `command -v setsid >/dev/null 2>&1 && SETSID=setsid`
8199
+ ];
8200
+ const body = manifest.services.flatMap((svc) => restoreOneService(driveRoot, svc));
8201
+ return [...head, ...body, ``].join(`
8202
+ `);
8203
+ }
8204
+ function servicesDumpScript(manifest, driveRoot) {
8205
+ const withDump = manifest.services.filter((svc) => svc.data?.dump);
8206
+ if (withDump.length === 0)
8207
+ return null;
8208
+ const lines = [
8209
+ `#!/bin/sh`,
8210
+ `# generated by agent-compose — pre-recycle data dumps from .ac/services.yml`,
8211
+ `status() { printf '%s %s %s %s\\n' ${shq(SERVICE_STATUS_PREFIX)} "$1" "$2" "$3"; }`
8212
+ ];
8213
+ for (const svc of withDump) {
8214
+ const cwd = serviceCwd(driveRoot, svc);
8215
+ lines.push(``, `# --- ${svc.name} ---`, `(`, ` cd ${shq(cwd)} || { status ${shq(svc.name)} dump-failed cwd-missing; exit 0; }`, ` if ${shc(svc.data.dump)} > /dev/null 2>&1; then`, ` status ${shq(svc.name)} dumped ''`, ` else`, ` status ${shq(svc.name)} dump-failed ''`, ` fi`, `)`);
8216
+ }
8217
+ lines.push(``);
8218
+ return lines.join(`
8219
+ `);
8220
+ }
8221
+ function parseServicesRestoreOutput(stdout) {
8222
+ const out = [];
8223
+ for (const raw of stdout.split(/\r?\n/)) {
8224
+ const line = raw.trim();
8225
+ if (!line.startsWith(`${SERVICE_STATUS_PREFIX} `))
8226
+ continue;
8227
+ const rest = line.slice(SERVICE_STATUS_PREFIX.length + 1).trim();
8228
+ const [name, status, ...detail] = rest.split(/\s+/);
8229
+ if (!name || !status)
8230
+ continue;
8231
+ if (!["ok", "unchecked", "failed", "dumped", "dump-failed"].includes(status))
8232
+ continue;
8233
+ const d = detail.join(" ").trim();
8234
+ out.push({ name, status, ...d ? { detail: d } : {} });
8235
+ }
8236
+ return out;
8237
+ }
8238
+ function formatServicesRestoreSummary(outcomes) {
8239
+ return outcomes.map((o) => {
8240
+ if (o.status === "ok" || o.status === "unchecked" || o.status === "dumped")
8241
+ return `${o.name} ✓`;
8242
+ const why = o.detail ? `${o.detail} — see ${SERVICES_STATE_DIR}/${o.name}.log` : `see ${SERVICES_STATE_DIR}/${o.name}.log`;
8243
+ return `${o.name} ✗ (${why})`;
8244
+ }).join(" ");
8245
+ }
7002
8246
  // src/display.ts
7003
8247
  var DISPLAY_MARKER_KEY = "acDisplay";
7004
8248
  var DISPLAY_MARKER_VERSION = "v1";
@@ -7192,6 +8436,7 @@ function clampAskOptions(raw) {
7192
8436
  }
7193
8437
  return options;
7194
8438
  }
8439
+ var DESKTOP_NOTE_MAX_CHARS = 200;
7195
8440
  function serializeDisplayMarker(marker) {
7196
8441
  return JSON.stringify({ [DISPLAY_MARKER_KEY]: DISPLAY_MARKER_VERSION, ...marker });
7197
8442
  }
@@ -7277,6 +8522,21 @@ function parseDisplayMarker(line) {
7277
8522
  ...yLabel !== undefined ? { yLabel } : {}
7278
8523
  };
7279
8524
  }
8525
+ case "desktop": {
8526
+ const path = str("path");
8527
+ if (!path || !isPlausibleDrivePath(path))
8528
+ return null;
8529
+ const capturedAt = str("capturedAt");
8530
+ const capturedOk = capturedAt !== undefined && Number.isFinite(Date.parse(capturedAt));
8531
+ const note = str("note");
8532
+ return {
8533
+ kind: "desktop",
8534
+ path,
8535
+ ...str("factorySlug") ? { factorySlug: str("factorySlug") } : {},
8536
+ ...capturedOk ? { capturedAt } : {},
8537
+ ...note !== undefined ? { note: note.slice(0, DESKTOP_NOTE_MAX_CHARS) } : {}
8538
+ };
8539
+ }
7280
8540
  case "ask": {
7281
8541
  const askId = str("askId");
7282
8542
  const rawPrompt = str("prompt");
@@ -7359,7 +8619,8 @@ var VALUE_FLAGS = new Set([
7359
8619
  "--to",
7360
8620
  "--prompt",
7361
8621
  "--options",
7362
- "--approver"
8622
+ "--approver",
8623
+ "--note"
7363
8624
  ]);
7364
8625
  function isShellControlWord(w) {
7365
8626
  if (/^\d*>&\d+$/.test(w))
@@ -7421,6 +8682,8 @@ function detectAgentcInvocation(command) {
7421
8682
  const path = drivePath(args[2]);
7422
8683
  return path ? { verb: "display-image", path, ...withSlug } : null;
7423
8684
  }
8685
+ case "desktop":
8686
+ return { verb: "display-desktop" };
7424
8687
  case "preview": {
7425
8688
  const path = drivePath(args[2]);
7426
8689
  return path ? { verb: "display-preview", path } : null;
@@ -7574,6 +8837,8 @@ function createDisplayPromoter(ctx = {}) {
7574
8837
  return call("display_plan", {});
7575
8838
  case "display-image":
7576
8839
  return call("display_image", { path: invocation.path, ...factoryRef(invocation.factorySlug) });
8840
+ case "display-desktop":
8841
+ return call("display_desktop", {});
7577
8842
  case "display-preview":
7578
8843
  return call("display_preview", { path: invocation.path, ...factoryRef(undefined) });
7579
8844
  case "display-table":
@@ -7667,6 +8932,17 @@ function createDisplayPromoter(ctx = {}) {
7667
8932
  ...factoryRef(m?.factorySlug ?? inv.factorySlug)
7668
8933
  })];
7669
8934
  }
8935
+ case "display-desktop": {
8936
+ const marker = findDisplayMarker(text);
8937
+ if (marker?.kind !== "desktop")
8938
+ return [result(msg.output)];
8939
+ return [result({
8940
+ path: marker.path,
8941
+ ...factoryRef(marker.factorySlug),
8942
+ capturedAt: marker.capturedAt ?? null,
8943
+ note: marker.note ?? null
8944
+ })];
8945
+ }
7670
8946
  case "display-table": {
7671
8947
  if (inv.source === "file")
7672
8948
  return [result(msg.output)];
@@ -7715,28 +8991,49 @@ function createDisplayPromoter(ctx = {}) {
7715
8991
  export {
7716
8992
  writeTool,
7717
8993
  writeAgentContext,
8994
+ wrapMidTurnUserMessage,
8995
+ uniquePromptPath,
8996
+ turnLivenessProbeCommand,
8997
+ streamInputFeederFragment,
7718
8998
  stepInputPath,
8999
+ standalonePerfProbeCommand,
7719
9000
  snapshotResolves,
7720
9001
  shellWords,
9002
+ shellQuote,
9003
+ sessionEnvSourceFragment,
9004
+ servicesRestoreScript,
9005
+ servicesDumpScript,
7721
9006
  serveStep,
7722
9007
  serializeDisplayMarker,
7723
9008
  sandboxSizeLabel,
9009
+ runnerBusySentinelFragment,
7724
9010
  runWorkflowSteps,
7725
9011
  runWorkflowSingleStep,
7726
9012
  runWorkflow,
7727
9013
  runProcessorChain,
7728
9014
  resolveSdkAlias,
9015
+ residentResultCountCommand,
9016
+ residentIdleWatchdogFragment,
9017
+ residentIdleCheckFragment,
9018
+ residentFeederFragment,
7729
9019
  requireScope,
7730
9020
  requestPauseAndAwait,
7731
9021
  requestContextPath,
9022
+ renderServicesManifest,
7732
9023
  renderDirectiveMarker,
7733
9024
  redactPattern,
7734
9025
  reconnectStep,
7735
9026
  reconnectSandbox,
7736
9027
  readTool,
9028
+ perfSamplerFunctionFragment,
9029
+ parseTurnProbeOutput,
7737
9030
  parseStepResult,
7738
9031
  parseSseStream,
7739
9032
  parseSseExecStream,
9033
+ parseServicesRestoreOutput,
9034
+ parseServicesManifest,
9035
+ parsePerfToken,
9036
+ parsePerfProbeOutput,
7740
9037
  parseNameVersion,
7741
9038
  parseDisplayMarker,
7742
9039
  parseDirectiveMarkers,
@@ -7748,6 +9045,7 @@ export {
7748
9045
  normalizeConversationStreamEvent,
7749
9046
  makeSandboxProvider,
7750
9047
  makeDesktopSandboxProvider,
9048
+ livenessVerdictFromReading,
7751
9049
  listVercelRuntimeModels,
7752
9050
  listOwnedSandboxes,
7753
9051
  launchStep,
@@ -7759,13 +9057,19 @@ export {
7759
9057
  isPlatformE2bTemplateAlias,
7760
9058
  isE2bSupportedSize,
7761
9059
  invokeStep,
9060
+ installDesktopOpenCmd,
9061
+ installChromiumDefaultsCmd,
9062
+ injectAppendAndAckCommand,
7762
9063
  importSourceModule,
7763
9064
  humanApproval,
7764
9065
  getSandboxQuotas,
9066
+ formatServicesRestoreSummary,
7765
9067
  formatError,
7766
9068
  findDisplayMarker,
7767
9069
  explainBundleFailure,
9070
+ exitEventPushCommand,
7768
9071
  editTool,
9072
+ e2bMaxSandboxMs,
7769
9073
  e2bMachineSpec,
7770
9074
  e2bDevboxTemplateRef,
7771
9075
  e2bBaseTemplate,
@@ -7779,8 +9083,10 @@ export {
7779
9083
  defineStep,
7780
9084
  defineSandboxEnvironment,
7781
9085
  defineRuntime,
9086
+ deferredExitEventPushCommand,
7782
9087
  cursorSpec,
7783
9088
  cursor_default as cursorRuntime,
9089
+ credEnvSourceFragment,
7784
9090
  createVercelRuntime,
7785
9091
  createSandbox,
7786
9092
  createOpencodeRuntime,
@@ -7810,6 +9116,8 @@ export {
7810
9116
  buildInvokeChild,
7811
9117
  buildAgentContextDoc,
7812
9118
  buildAddedSessionBrief,
9119
+ browserBackfillStatusCmd,
9120
+ browserBackfillCmd,
7813
9121
  bashTool,
7814
9122
  assertDefaultExportIsDefineWorkflow,
7815
9123
  amp_default as ampRuntime,
@@ -7822,6 +9130,7 @@ export {
7822
9130
  Verdict,
7823
9131
  VercelRunner,
7824
9132
  VERCEL_MEMORY_MB_PER_VCPU,
9133
+ TURN_PROBE_TIMEOUT_MS,
7825
9134
  TMP_DIR,
7826
9135
  TABLE_MAX_ROWS,
7827
9136
  TABLE_MAX_COLUMNS,
@@ -7832,7 +9141,14 @@ export {
7832
9141
  STEP_RESULT_PREFIX,
7833
9142
  STEP_PAUSE_PREFIX,
7834
9143
  STEP_ENV,
9144
+ SESSION_ENV_FILE_RELPATH,
7835
9145
  SESSION_DEFAULT_SANDBOX_SIZE,
9146
+ SERVICE_STATUS_PREFIX,
9147
+ SERVICE_NAME_RE,
9148
+ SERVICE_HEALTH_TIMEOUT_S,
9149
+ SERVICES_STATE_DIR,
9150
+ SERVICES_MAX,
9151
+ SERVICES_MANIFEST_RELPATH,
7836
9152
  SDK_SPECIFIER_ALIASES,
7837
9153
  SDK_PACKAGE,
7838
9154
  SANDBOX_VCPUS,
@@ -7841,18 +9157,29 @@ export {
7841
9157
  SANDBOX_MACHINES,
7842
9158
  ReservedKeyError,
7843
9159
  RequestContext,
9160
+ RUNNER_HEARTBEAT_STALE_MS,
9161
+ RUNNER_HEARTBEAT_INTERVAL_SECONDS,
7844
9162
  RUNNER_COMMAND,
7845
9163
  RUNNER_BUNDLE_PATH,
7846
9164
  REVISION_SELECTOR_RE,
9165
+ RESIDENT_WATCHDOG_POLL_S,
9166
+ RESIDENT_RESULT_LINE_PREFIX,
9167
+ RESIDENT_IDLE_TTL_S,
7847
9168
  PauseSchemaError,
7848
9169
  PauseRequestError,
7849
9170
  PauseExpiredError,
7850
9171
  PauseError,
7851
9172
  PLAN_MAX_ENTRIES,
7852
9173
  PLAN_ENTRY_MAX_CHARS,
9174
+ PERF_TOKEN_MAX_CHARS,
9175
+ PERF_SAMPLE_EVERY_BEATS,
9176
+ PERF_PROBE_WINDOW_SECONDS,
7853
9177
  NonSerialisableValueError,
7854
9178
  MAX_DIRECTIVES_PER_OUTPUT,
9179
+ LAUNCH_EXEC_TIMEOUT_MS,
7855
9180
  LATEST_VERSION,
9181
+ INSTALL_PROBE_TIMEOUT_MS,
9182
+ INJECT_ACK_EXEC_TIMEOUT_MS,
7856
9183
  EngineError,
7857
9184
  E2B_TEMPLATE_SIZES,
7858
9185
  E2B_MAX_VCPUS,
@@ -7865,15 +9192,23 @@ export {
7865
9192
  DISPLAY_MARKER_KEY,
7866
9193
  DIRECTIVE_MARKER_SUFFIX,
7867
9194
  DIRECTIVE_MARKER_PREFIX,
9195
+ DESKTOP_NOTE_MAX_CHARS,
7868
9196
  DEFAULT_SANDBOX_SIZE,
7869
9197
  DEFAULT_CLAUDE_MODEL,
7870
9198
  ClaudeRunner,
9199
+ CLI_INSTALL_TIMEOUT_MS,
7871
9200
  CLAUDE_CODE_EFFORT_LEVELS,
7872
9201
  CLAUDE_CODE_ACP_ADAPTER,
9202
+ CHROMIUM_DEFAULTS_PATH,
9203
+ CHROMIUM_DEFAULTS,
7873
9204
  CHART_MAX_SERIES,
7874
9205
  CHART_MAX_POINTS_PER_SERIES,
7875
9206
  CHART_LABEL_MAX_CHARS,
7876
9207
  BUNDLER_VERSION,
9208
+ BROWSER_INSTALL_IN_PROGRESS_MSG,
9209
+ BROWSER_BACKFILL_PIDFILE,
9210
+ BROWSER_BACKFILL_LOG,
9211
+ BROWSER_BACKFILL_FAILED,
7877
9212
  AgentStatusSchema,
7878
9213
  AgentMessageSchema,
7879
9214
  AgentComposeError,
@@ -7897,6 +9232,11 @@ export {
7897
9232
  AC_RUN_ID,
7898
9233
  AC_RESERVED_PREFIX,
7899
9234
  AC_PARENT_RUN_ID,
9235
+ AC_OPEN_STATE_DIR,
9236
+ AC_OPEN_SHIM_PATHS,
9237
+ AC_OPEN_SHIM,
9238
+ AC_OPEN_SCRIPT,
9239
+ AC_OPEN_PATH,
7900
9240
  AC_FACTORY_ID,
7901
9241
  AC_API_KEY_SCOPES,
7902
9242
  AC_ABORT_SIGNAL,