@agent-compose/sdk 0.8.1 → 0.8.3

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 (48) 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 +5 -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 +189 -15
  9. package/dist/display.d.ts +17 -0
  10. package/dist/index.d.ts +14 -5
  11. package/dist/index.js +1625 -120
  12. package/dist/runtimes/_cli-agent.d.ts +372 -2
  13. package/dist/runtimes/claude-code.d.ts +12 -0
  14. package/dist/runtimes/codex.buildcommand.test.d.ts +9 -0
  15. package/dist/runtimes/codex.d.ts +8 -0
  16. package/dist/runtimes/openai-desktop.js +1555 -120
  17. package/dist/runtimes/session-env.test.d.ts +14 -0
  18. package/dist/sandbox/sizes.d.ts +120 -30
  19. package/dist/sandbox.d.ts +1 -1
  20. package/dist/types/api-conversations.d.ts +476 -1
  21. package/dist/types/api-factory.d.ts +164 -7
  22. package/dist/types/api-runs.d.ts +23 -1
  23. package/dist/types/protocol.d.ts +32 -1
  24. package/dist/types/runtime.d.ts +120 -0
  25. package/dist/types/workflow-metadata.d.ts +6 -5
  26. package/package.json +1 -1
  27. package/src/agent/agent-context.ts +128 -28
  28. package/src/agent/agent-loop.ts +10 -3
  29. package/src/agent/desktop-open.ts +418 -0
  30. package/src/agent/perf-sampler.ts +202 -0
  31. package/src/agent/services-manifest.ts +356 -0
  32. package/src/agent/services-restore.ts +195 -0
  33. package/src/client.ts +384 -32
  34. package/src/display.ts +44 -1
  35. package/src/index.ts +74 -7
  36. package/src/runtimes/_cli-agent.ts +1160 -67
  37. package/src/runtimes/claude-code.ts +187 -12
  38. package/src/runtimes/codex.ts +65 -2
  39. package/src/sandbox/providers/e2b.ts +8 -4
  40. package/src/sandbox/providers/local.ts +16 -4
  41. package/src/sandbox/sizes.ts +127 -44
  42. package/src/sandbox.ts +8 -0
  43. package/src/types/api-conversations.ts +461 -2
  44. package/src/types/api-factory.ts +165 -7
  45. package/src/types/api-runs.ts +25 -1
  46. package/src/types/protocol.ts +30 -1
  47. package/src/types/runtime.ts +122 -0
  48. package/src/types/workflow-metadata.ts +6 -5
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",
@@ -1756,11 +1798,43 @@ class AgentComposeClient {
1756
1798
  const body = await this.fetch(`/api/v1/conversations/${encodeURIComponent(conversationId)}/preview/${port}`, { method: "DELETE" });
1757
1799
  return body.closed;
1758
1800
  }
1801
+ holdBackgroundWork(conversationId, input) {
1802
+ return this.fetch(`/api/v1/conversations/${encodeURIComponent(conversationId)}/background-work`, { method: "POST", body: { ...input?.minutes !== undefined ? { minutes: input.minutes } : {} } });
1803
+ }
1804
+ async releaseBackgroundWork(conversationId) {
1805
+ const body = await this.fetch(`/api/v1/conversations/${encodeURIComponent(conversationId)}/background-work`, { method: "DELETE" });
1806
+ return body.released;
1807
+ }
1759
1808
  forkSession(conversationId, input) {
1760
1809
  return this.fetch(`/api/v1/conversations/${encodeURIComponent(conversationId)}/fork`, { method: "POST", body: input?.title ? { title: input.title } : {} });
1761
1810
  }
1762
- getSessionChanges(conversationId) {
1763
- 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" });
1764
1838
  }
1765
1839
  mergeSessionChanges(conversationId, opts = {}) {
1766
1840
  return this.fetch(`/api/v1/conversations/${encodeURIComponent(conversationId)}/changes/merge`, { method: "POST", body: opts.branch ? { branch: opts.branch } : {} });
@@ -1768,6 +1842,15 @@ class AgentComposeClient {
1768
1842
  discardSessionChanges(conversationId, opts = {}) {
1769
1843
  return this.fetch(`/api/v1/conversations/${encodeURIComponent(conversationId)}/changes/discard`, { method: "POST", body: opts.branch ? { branch: opts.branch } : {} });
1770
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
+ }
1771
1854
  async getConversationMembers(conversationId) {
1772
1855
  const body = await this.fetch(`/api/v1/conversations/${encodeURIComponent(conversationId)}/members`);
1773
1856
  return body.members;
@@ -1822,6 +1905,28 @@ class AgentComposeClient {
1822
1905
  q2.set("cursor", opts.cursor);
1823
1906
  return this.fetch(`/api/v1/factories/${encodeURIComponent(factorySlug)}/files/search?${q2}`);
1824
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
+ }
1825
1930
  async listMembers() {
1826
1931
  const body = await this.fetch("/api/v1/team/members");
1827
1932
  return body.members;
@@ -2005,7 +2110,7 @@ class AgentComposeClient {
2005
2110
  async createRepoLink(input, opts) {
2006
2111
  const slug = opts?.factorySlug ?? DEFAULT_FACTORY;
2007
2112
  const body = await this.fetch(`/api/v1/factories/${encodeURIComponent(slug)}/repo-links`, { method: "POST", body: input });
2008
- return body.link;
2113
+ return body.links;
2009
2114
  }
2010
2115
  deleteRepoLink(linkId, opts) {
2011
2116
  const slug = opts?.factorySlug ?? DEFAULT_FACTORY;
@@ -2026,12 +2131,37 @@ class AgentComposeClient {
2026
2131
  const factorySlug = opts?.factorySlug ?? DEFAULT_FACTORY;
2027
2132
  return this.fetch(`/api/v1/factories/${encodeURIComponent(factorySlug)}/secrets/${encodeURIComponent(key)}`, { method: "DELETE" });
2028
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
+ }
2029
2151
  createApiKey(input) {
2030
2152
  return this.fetch("/api-keys", { method: "POST", body: input });
2031
2153
  }
2032
- async listApiKeys() {
2033
- const body = await this.fetch("/api-keys");
2034
- return body.data;
2154
+ async listApiKeys(opts) {
2155
+ const qs = new URLSearchParams;
2156
+ if (opts?.limit !== undefined)
2157
+ qs.set("limit", String(opts.limit));
2158
+ if (opts?.status !== undefined)
2159
+ qs.set("status", opts.status);
2160
+ if (opts?.cursor !== undefined)
2161
+ qs.set("cursor", opts.cursor);
2162
+ const suffix = qs.toString() ? `?${qs.toString()}` : "";
2163
+ const body = await this.fetch(`/api-keys${suffix}`);
2164
+ return { data: body.data, hasMore: body.has_more, nextCursor: body.next_cursor ?? null };
2035
2165
  }
2036
2166
  getUsage(from, to) {
2037
2167
  const qs = `?from=${encodeURIComponent(from.toISOString())}&to=${encodeURIComponent(to.toISOString())}`;
@@ -3283,7 +3413,7 @@ Re-emit the COMPLETE corrected <response> JSON now: every required field present
3283
3413
  continue;
3284
3414
  }
3285
3415
  const msg = outputVerdict.value;
3286
- if (msg.type === "text_delta" || msg.type === "usage_delta")
3416
+ if (msg.type === "text_delta" || msg.type === "usage_delta" || msg.type === "task_notification")
3287
3417
  continue;
3288
3418
  opts.onAgentEvent?.(iteration, msg);
3289
3419
  const summary = summarizeAgentMessage(msg);
@@ -3668,6 +3798,73 @@ function createClaudeRuntime(config = {}) {
3668
3798
  });
3669
3799
  }
3670
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
+
3671
3868
  // src/runtimes/_cli-agent.ts
3672
3869
  import { ndJsonStream } from "@agentclientprotocol/sdk";
3673
3870
 
@@ -3783,9 +3980,152 @@ function wrapJsonlCommand(args) {
3783
3980
  function now4() {
3784
3981
  return new Date().toISOString();
3785
3982
  }
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 injectAppendAndAckCommand(args) {
4042
+ const q2 = shellQuote;
4043
+ 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`;
4044
+ }
4045
+ var REAP_TERM_WAIT_ATTEMPTS = 20;
4046
+ var REAP_KILL_WAIT_ATTEMPTS = 8;
4047
+ var REAP_POLL_SECONDS = "0.25";
4048
+ var REAP_STILL_ALIVE_EXIT = 7;
4049
+ var REAP_EXEC_TIMEOUT_MS = 15000;
4050
+ function reapKillAndConfirmCommand(pid) {
4051
+ const alive = `kill -0 ${pid} 2>/dev/null`;
4052
+ 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}`;
4053
+ }
4054
+ var RESIDENT_IDLE_TTL_S = 240;
4055
+ var RESIDENT_WATCHDOG_POLL_S = 15;
4056
+ var RESIDENT_RESULT_LINE_PREFIX = '{"type":"result"';
4057
+ function residentResultCountCommand(outPath, fromByte = 0) {
4058
+ const q2 = shellQuote;
4059
+ const src = fromByte > 0 ? `tail -c +${fromByte + 1} ${q2(outPath)} 2>/dev/null` : `cat ${q2(outPath)} 2>/dev/null`;
4060
+ return `ac_res=$(${src} | grep -c ${q2(`^${RESIDENT_RESULT_LINE_PREFIX}`)}); echo "\${ac_res:-0}"`;
4061
+ }
4062
+ function residentFeederFragment(paths) {
4063
+ const q2 = shellQuote;
4064
+ 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 & `;
4065
+ }
4066
+ function residentIdleCheckFragment(paths) {
4067
+ const q2 = shellQuote;
4068
+ 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; `;
4069
+ }
4070
+ function residentIdleWatchdogFragment(paths) {
4071
+ 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 & `;
4072
+ }
4073
+ function deferredExitEventPushCommand(notify, turnIdFile) {
4074
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(notify.tokenEnv))
4075
+ return "";
4076
+ const q2 = shellQuote;
4077
+ 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`;
4078
+ }
4079
+ var LAUNCH_EXEC_TIMEOUT_MS = 30000;
4080
+ var LAUNCH_PID_RECOVERY_ATTEMPTS = 3;
4081
+ var LAUNCH_PID_RECOVERY_DELAY_MS = 1000;
4082
+ var INSTALL_PROBE_TIMEOUT_MS = 30000;
4083
+ var CLI_INSTALL_TIMEOUT_MS = 300000;
4084
+ function parsePid(raw) {
4085
+ const pid = Number.parseInt(raw.trim().split(`
4086
+ `).pop() ?? "", 10);
4087
+ return Number.isInteger(pid) && pid > 0 ? pid : Number.NaN;
4088
+ }
4089
+ async function recoverLaunchPid(read, pidPath) {
4090
+ for (let attempt = 0;attempt < LAUNCH_PID_RECOVERY_ATTEMPTS; attempt++) {
4091
+ try {
4092
+ const pid = parsePid(await read(pidPath));
4093
+ if (!Number.isNaN(pid))
4094
+ return pid;
4095
+ } catch {}
4096
+ if (attempt < LAUNCH_PID_RECOVERY_ATTEMPTS - 1) {
4097
+ await new Promise((r) => setTimeout(r, LAUNCH_PID_RECOVERY_DELAY_MS));
4098
+ }
4099
+ }
4100
+ return Number.NaN;
4101
+ }
3786
4102
  function shellQuote(value) {
3787
4103
  return `'${value.replace(/'/g, `'\\''`)}'`;
3788
4104
  }
4105
+ var SESSION_ENV_FILE_RELPATH = ".agent-compose/session-env.sh";
4106
+ var SESSION_ENV_FILE_SAFE = /^[A-Za-z0-9._\/-]+$/;
4107
+ function sessionEnvSourceFragment(relPath) {
4108
+ if (!relPath || !SESSION_ENV_FILE_SAFE.test(relPath) || relPath.includes(".."))
4109
+ return "";
4110
+ return `[ -f "$HOME/${relPath}" ] && . "$HOME/${relPath}" >/dev/null 2>&1 || true; `;
4111
+ }
4112
+ function credEnvSourceFragment(absPath) {
4113
+ if (!absPath || !absPath.startsWith("/") || !SESSION_ENV_FILE_SAFE.test(absPath.slice(1)) || absPath.includes(".."))
4114
+ return "";
4115
+ return `[ -f "${absPath}" ] && . "${absPath}" >/dev/null 2>&1 || true; `;
4116
+ }
4117
+ var EXIT_PUSH_ATTEMPT_TIMEOUT_SECONDS = 5;
4118
+ var EXIT_PUSH_RETRY_DELAY_SECONDS = 1;
4119
+ function exitEventPushCommand(notify) {
4120
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(notify.tokenEnv))
4121
+ return "";
4122
+ if (!/^[A-Za-z0-9-]{1,64}$/.test(notify.turnId))
4123
+ return "";
4124
+ const token = `"$${notify.tokenEnv}"`;
4125
+ const body = `"{\\"turnId\\":\\"${notify.turnId}\\",\\"exitCode\\":$ac_rc}"`;
4126
+ 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)}`;
4127
+ 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`;
4128
+ }
3789
4129
  var promptFileCounter = 0;
3790
4130
  function uniquePromptPath(kind, agentId, iteration) {
3791
4131
  const unique = `${Date.now().toString(36)}-${(promptFileCounter++).toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
@@ -3825,6 +4165,60 @@ class CliAgentRunner {
3825
4165
  acpSession;
3826
4166
  currentAcpProcCtx;
3827
4167
  acpTransportReady = ACP_TRANSPORT_READY;
4168
+ turnProbe = null;
4169
+ turnInject = null;
4170
+ async injectUserMessage(text) {
4171
+ const inject = this.turnInject;
4172
+ const streamInput = this.spec.streamInput;
4173
+ if (!inject || !streamInput || this.guestDetached)
4174
+ return "unsupported";
4175
+ const line = streamInput.messageLine(text);
4176
+ const attempt = inject.chain.then(async () => {
4177
+ if (this.turnInject !== inject)
4178
+ return "closed";
4179
+ const target = inject.appended + 1;
4180
+ inject.appended = target;
4181
+ try {
4182
+ const res = await this.sandbox.commands.run(injectAppendAndAckCommand({
4183
+ line,
4184
+ target,
4185
+ pid: inject.pid,
4186
+ inboxPath: inject.inboxPath,
4187
+ deliveredPath: inject.deliveredPath
4188
+ }), { timeoutMs: INJECT_ACK_EXEC_TIMEOUT_MS });
4189
+ if (res.exitCode === 0)
4190
+ return "delivered";
4191
+ return res.exitCode === 5 ? "pending" : "closed";
4192
+ } catch {
4193
+ return "closed";
4194
+ }
4195
+ });
4196
+ inject.chain = attempt.catch(() => {
4197
+ return;
4198
+ });
4199
+ return attempt;
4200
+ }
4201
+ async probeTurnLiveness() {
4202
+ const probe = this.turnProbe;
4203
+ if (!probe)
4204
+ return null;
4205
+ return livenessVerdictFromReading(await probe());
4206
+ }
4207
+ wakeTailWatchdog = null;
4208
+ nudgePending = false;
4209
+ nudgeTurnProbe() {
4210
+ const wake = this.wakeTailWatchdog;
4211
+ if (wake) {
4212
+ this.wakeTailWatchdog = null;
4213
+ wake();
4214
+ } else {
4215
+ this.nudgePending = true;
4216
+ }
4217
+ }
4218
+ guestDetached = false;
4219
+ detachGuest() {
4220
+ this.guestDetached = true;
4221
+ }
3828
4222
  constructor(sandbox, options, spec, configModel, configEffort) {
3829
4223
  this.sandbox = sandbox;
3830
4224
  this.options = options;
@@ -3868,12 +4262,12 @@ class CliAgentRunner {
3868
4262
  async ensureInstalled() {
3869
4263
  if (this.installed)
3870
4264
  return;
3871
- const probe = await this.sandbox.commands.run(`command -v ${this.spec.bin}`);
4265
+ const probe = await this.sandbox.commands.run(`command -v ${this.spec.bin}`, { timeoutMs: INSTALL_PROBE_TIMEOUT_MS });
3872
4266
  if (probe.exitCode === 0) {
3873
4267
  this.installed = true;
3874
4268
  return;
3875
4269
  }
3876
- const res = await this.sandbox.commands.run(this.spec.install, { timeoutMs: 300000 });
4270
+ const res = await this.sandbox.commands.run(this.spec.install, { timeoutMs: CLI_INSTALL_TIMEOUT_MS });
3877
4271
  if (res.exitCode !== 0) {
3878
4272
  throw new Error(`failed to install ${this.spec.bin}: ${(res.stderr || res.stdout || "").slice(-500)}`);
3879
4273
  }
@@ -4014,17 +4408,25 @@ class CliAgentRunner {
4014
4408
  return;
4015
4409
  await this.ensureInstalled();
4016
4410
  const promptPath = uniquePromptPath(this.spec.kind, this.options.agentId ?? "agent", opts.iteration ?? 0);
4017
- await this.sandbox.files.write(promptPath, this.spec.promptPayload(opts.prompt));
4411
+ this.turnProbe = null;
4412
+ this.turnInject = null;
4413
+ this.nudgePending = false;
4414
+ this.wakeTailWatchdog = null;
4415
+ const streamInput = this.spec.streamInput !== undefined && this.sandbox.commands.runBackground !== undefined;
4416
+ const fifoPath = `${promptPath}.fifo`;
4417
+ const promptWrite = this.sandbox.files.write(promptPath, streamInput ? `${this.spec.streamInput.promptLine(opts.prompt)}
4418
+ ` : this.spec.promptPayload(opts.prompt));
4018
4419
  const cmd = this.spec.buildCommand({
4019
- promptPath,
4420
+ promptPath: streamInput ? fifoPath : promptPath,
4020
4421
  sessionId: opts.sessionId,
4021
4422
  model: this.model,
4022
4423
  cwd: this.options.cwd,
4023
- effort: this.configEffort
4424
+ effort: this.configEffort,
4425
+ ...streamInput ? { streamInput: true } : {}
4024
4426
  });
4025
4427
  const guardPath = `${promptPath}.guard.js`;
4026
4428
  const sentinel = `__AC_JSONL_EXIT_${Math.random().toString(36).slice(2, 10)}__`;
4027
- await this.sandbox.files.write(guardPath, jsonlGuardScript());
4429
+ await Promise.all([promptWrite, this.sandbox.files.write(guardPath, jsonlGuardScript())]);
4028
4430
  const guardedCmd = wrapJsonlCommand({
4029
4431
  cmd,
4030
4432
  guardPath,
@@ -4033,58 +4435,252 @@ class CliAgentRunner {
4033
4435
  keepBytes: JSONL_GUARD_KEEP_BYTES
4034
4436
  });
4035
4437
  const lines = new AsyncQueue;
4036
- let buf = "";
4037
- const onStdout = (data) => {
4038
- buf += data;
4039
- let nl;
4040
- while ((nl = buf.indexOf(`
4041
- `)) >= 0) {
4042
- const line = buf.slice(0, nl).trim();
4043
- buf = buf.slice(nl + 1);
4044
- if (line)
4045
- lines.push(line);
4046
- }
4047
- };
4048
- const runOpts = {
4049
- ...this.options.cwd ? { cwd: this.options.cwd } : {},
4050
- onStdout,
4051
- timeoutMs: 0
4052
- };
4053
- let exited = false;
4054
- const settle = (res) => {
4055
- exited = true;
4056
- const tail = buf.trim();
4057
- if (tail)
4058
- lines.push(tail);
4059
- lines.close();
4060
- return res;
4061
- };
4062
- const fail = (err) => {
4063
- exited = true;
4064
- lines.close();
4065
- throw err;
4066
- };
4067
- let kill;
4068
- let runPromise;
4438
+ let exitCode = null;
4439
+ let exitStderr = "";
4440
+ let transportError = null;
4441
+ let consumerStopped = false;
4442
+ let reap = () => {};
4443
+ let reapConfirm = null;
4444
+ let transport;
4445
+ const durableRead = this.sandbox.files.read?.bind(this.sandbox.files);
4069
4446
  if (this.sandbox.commands.runBackground) {
4070
- const handle = await this.sandbox.commands.runBackground(guardedCmd, runOpts);
4071
- kill = () => handle.kill();
4072
- runPromise = handle.wait().then(settle, fail);
4447
+ const outPath = `${promptPath}.out`;
4448
+ const errPath = `${promptPath}.err`;
4449
+ const hbPath = `${promptPath}.hb`;
4450
+ const exitPush = this.options.turnExitNotify ? exitEventPushCommand(this.options.turnExitNotify) : "";
4451
+ const busyPath = `${promptPath}.busy`;
4452
+ const tasksPath = `${promptPath}.tasks`;
4453
+ const inboxPath = `${promptPath}.inbox`;
4454
+ const deliveredPath = `${promptPath}.delivered`;
4455
+ const feeder = streamInput ? streamInputFeederFragment({ promptPath, fifoPath, inboxPath, deliveredPath, outPath }) : "";
4456
+ const perfPath = `${promptPath}.perf`;
4457
+ 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}` : "");
4458
+ const pidPath = `${promptPath}.pid`;
4459
+ 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 "$!"`;
4460
+ let pid = Number.NaN;
4461
+ let launched = null;
4462
+ let launchErr = null;
4463
+ try {
4464
+ launched = await this.sandbox.commands.run(launchCmd, {
4465
+ ...this.options.cwd ? { cwd: this.options.cwd } : {},
4466
+ timeoutMs: LAUNCH_EXEC_TIMEOUT_MS
4467
+ });
4468
+ } catch (err) {
4469
+ launchErr = err;
4470
+ }
4471
+ if (launched) {
4472
+ pid = parsePid(launched.stdout ?? "");
4473
+ if (launched.exitCode !== 0 || Number.isNaN(pid)) {
4474
+ throw new Error(`failed to launch ${this.spec.kind}: ` + `exit ${launched.exitCode}${launched.stderr ? `: ${launched.stderr.slice(-500)}` : ""}`);
4475
+ }
4476
+ } else {
4477
+ pid = durableRead ? await recoverLaunchPid(durableRead, pidPath) : Number.NaN;
4478
+ if (Number.isNaN(pid))
4479
+ throw launchErr;
4480
+ 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)}`);
4481
+ }
4482
+ const probeCmd = turnLivenessProbeCommand({ outPath, hbPath, perfPath, pid, sentinel });
4483
+ const runTurnProbe = async () => {
4484
+ try {
4485
+ const res = await this.sandbox.commands.run(probeCmd, { timeoutMs: TURN_PROBE_TIMEOUT_MS });
4486
+ return parseTurnProbeOutput(res.stdout ?? "");
4487
+ } catch {
4488
+ return null;
4489
+ }
4490
+ };
4491
+ this.turnProbe = runTurnProbe;
4492
+ if (streamInput) {
4493
+ this.turnInject = { inboxPath, deliveredPath, pid, appended: 0, chain: Promise.resolve() };
4494
+ }
4495
+ let offset = 0;
4496
+ let currentTail = null;
4497
+ const consumeLine = (raw) => {
4498
+ offset += Buffer.byteLength(raw, "utf8") + 1;
4499
+ const line = raw.trim();
4500
+ if (!line)
4501
+ return;
4502
+ if (line.startsWith(sentinel)) {
4503
+ const code = Number(line.slice(sentinel.length).trim());
4504
+ exitCode = Number.isFinite(code) ? code : 0;
4505
+ return;
4506
+ }
4507
+ lines.push(line);
4508
+ };
4509
+ reap = () => {
4510
+ reapConfirm ??= this.sandbox.commands.run(reapKillAndConfirmCommand(pid), { timeoutMs: REAP_EXEC_TIMEOUT_MS }).then((res) => {
4511
+ if (res.exitCode === REAP_STILL_ALIVE_EXIT) {
4512
+ console.warn(`[cli-agent] ${this.spec.kind} runner ${pid} survived SIGKILL's confirm window ` + "— the next launch's preflight is the backstop");
4513
+ }
4514
+ }).catch(() => {});
4515
+ currentTail?.kill().catch(() => {});
4516
+ };
4517
+ transport = (async () => {
4518
+ let streamDeaths = 0;
4519
+ while (exitCode === null && !consumerStopped && !opts.signal?.aborted) {
4520
+ let buf = "";
4521
+ let handle = null;
4522
+ const onStdout = (data) => {
4523
+ buf += data;
4524
+ let nl;
4525
+ while ((nl = buf.indexOf(`
4526
+ `)) >= 0) {
4527
+ consumeLine(buf.slice(0, nl));
4528
+ buf = buf.slice(nl + 1);
4529
+ if (exitCode !== null) {
4530
+ handle?.kill().catch(() => {});
4531
+ return;
4532
+ }
4533
+ }
4534
+ };
4535
+ if (streamDeaths < TAIL_ABANDON_AFTER_STREAM_DEATHS) {
4536
+ try {
4537
+ handle = await this.sandbox.commands.runBackground(`tail -s 0.05 -c +${offset + 1} -f ${shellQuote(outPath)}`, { timeoutMs: 0, onStdout });
4538
+ } catch {}
4539
+ }
4540
+ if (handle) {
4541
+ if (exitCode !== null)
4542
+ handle.kill().catch(() => {});
4543
+ currentTail = handle;
4544
+ await (async (tail) => {
4545
+ let waitSettled = false;
4546
+ const waitP = tail.wait().catch(() => {
4547
+ return;
4548
+ }).then(() => {
4549
+ waitSettled = true;
4550
+ });
4551
+ let lastTickOffset = -1;
4552
+ while (!waitSettled) {
4553
+ let onAbort;
4554
+ const wake = new Promise((resolve) => {
4555
+ const t = setTimeout(resolve, TAIL_WATCHDOG_INTERVAL_MS);
4556
+ t.unref?.();
4557
+ const fire = () => {
4558
+ clearTimeout(t);
4559
+ resolve();
4560
+ };
4561
+ if (this.nudgePending) {
4562
+ this.nudgePending = false;
4563
+ fire();
4564
+ } else {
4565
+ this.wakeTailWatchdog = fire;
4566
+ }
4567
+ if (opts.signal) {
4568
+ onAbort = fire;
4569
+ opts.signal.addEventListener("abort", onAbort, { once: true });
4570
+ }
4571
+ });
4572
+ await Promise.race([waitP, wake]);
4573
+ this.wakeTailWatchdog = null;
4574
+ if (onAbort)
4575
+ opts.signal?.removeEventListener("abort", onAbort);
4576
+ if (waitSettled || exitCode !== null || consumerStopped || opts.signal?.aborted)
4577
+ return;
4578
+ const tickOffset = offset;
4579
+ const reading = await runTurnProbe();
4580
+ if (offset !== tickOffset) {
4581
+ lastTickOffset = offset;
4582
+ continue;
4583
+ }
4584
+ if (reading === null) {
4585
+ lastTickOffset = tickOffset;
4586
+ continue;
4587
+ }
4588
+ const streamStarved = reading.size > offset && tickOffset === lastTickOffset;
4589
+ if (reading.sentinelSeen || !reading.alive || streamStarved) {
4590
+ 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");
4591
+ tail.kill().catch(() => {});
4592
+ return;
4593
+ }
4594
+ lastTickOffset = tickOffset;
4595
+ }
4596
+ })(handle);
4597
+ currentTail = null;
4598
+ }
4599
+ if (exitCode !== null || consumerStopped || opts.signal?.aborted)
4600
+ break;
4601
+ if (durableRead) {
4602
+ try {
4603
+ const bytes = Buffer.from(await durableRead(outPath), "utf8");
4604
+ let rest = bytes.subarray(offset).toString("utf8");
4605
+ let nl;
4606
+ while (exitCode === null && (nl = rest.indexOf(`
4607
+ `)) >= 0) {
4608
+ consumeLine(rest.slice(0, nl));
4609
+ rest = rest.slice(nl + 1);
4610
+ }
4611
+ } catch {}
4612
+ }
4613
+ if (exitCode !== null)
4614
+ break;
4615
+ let alive = null;
4616
+ try {
4617
+ const probe = await this.sandbox.commands.run(`kill -0 ${pid} 2>/dev/null`, { timeoutMs: 1e4 });
4618
+ alive = probe.exitCode === 0;
4619
+ } catch {
4620
+ alive = null;
4621
+ }
4622
+ if (alive === false) {
4623
+ exitCode = JSONL_GUARD_NO_SENTINEL_EXIT;
4624
+ break;
4625
+ }
4626
+ streamDeaths += 1;
4627
+ if (streamDeaths < TAIL_ABANDON_AFTER_STREAM_DEATHS) {
4628
+ console.warn(`[cli-agent] ${this.spec.kind} turn stream lost with the runner alive — re-tailing ${outPath} from byte ${offset}`);
4629
+ await new Promise((r) => setTimeout(r, TAIL_REATTACH_DELAY_MS));
4630
+ } else {
4631
+ if (streamDeaths === TAIL_ABANDON_AFTER_STREAM_DEATHS) {
4632
+ console.warn(`[cli-agent] ${this.spec.kind} turn abandoning streaming after ` + `${streamDeaths} stream deaths — finishing on durable polling from byte ${offset}`);
4633
+ }
4634
+ await new Promise((r) => setTimeout(r, DURABLE_POLL_INTERVAL_MS));
4635
+ }
4636
+ }
4637
+ if (exitCode !== null && exitCode !== 0 && durableRead) {
4638
+ try {
4639
+ exitStderr = (await durableRead(errPath)).slice(-2000);
4640
+ } catch {}
4641
+ }
4642
+ })().finally(() => lines.close());
4073
4643
  } else {
4074
- runPromise = this.sandbox.commands.run(guardedCmd, runOpts).then(settle, fail);
4644
+ let buf = "";
4645
+ const onStdout = (data) => {
4646
+ buf += data;
4647
+ let nl;
4648
+ while ((nl = buf.indexOf(`
4649
+ `)) >= 0) {
4650
+ const line = buf.slice(0, nl).trim();
4651
+ buf = buf.slice(nl + 1);
4652
+ if (line)
4653
+ lines.push(line);
4654
+ }
4655
+ };
4656
+ const runOpts = {
4657
+ ...this.options.cwd ? { cwd: this.options.cwd } : {},
4658
+ onStdout,
4659
+ timeoutMs: 0
4660
+ };
4661
+ transport = this.sandbox.commands.run(credEnvSourceFragment(this.options.credEnvFile) + guardedCmd, runOpts).then((res) => {
4662
+ const tail = buf.trim();
4663
+ if (tail)
4664
+ lines.push(tail);
4665
+ exitCode = res.exitCode;
4666
+ exitStderr = (res.stderr ?? "").slice(-2000);
4667
+ }, (err) => {
4668
+ transportError = err;
4669
+ }).finally(() => lines.close());
4075
4670
  }
4076
- runPromise.catch(() => {});
4077
- const reap = () => {
4078
- if (!exited)
4079
- kill?.().catch(() => {});
4671
+ transport.catch(() => {});
4672
+ const reapUnlessDetached = () => {
4673
+ if (!this.guestDetached)
4674
+ reap();
4080
4675
  };
4081
4676
  if (opts.signal) {
4082
4677
  if (opts.signal.aborted)
4083
- reap();
4678
+ reapUnlessDetached();
4084
4679
  else
4085
- opts.signal.addEventListener("abort", reap, { once: true });
4680
+ opts.signal.addEventListener("abort", reapUnlessDetached, { once: true });
4086
4681
  }
4087
4682
  try {
4683
+ let announcedSessionId = opts.sessionId ?? "";
4088
4684
  for await (const line of lines) {
4089
4685
  let parsed;
4090
4686
  try {
@@ -4095,24 +4691,47 @@ class CliAgentRunner {
4095
4691
  const sid = this.spec.extractSessionId(parsed);
4096
4692
  if (sid)
4097
4693
  sessionId = sid;
4694
+ if (sid && sid !== announcedSessionId) {
4695
+ announcedSessionId = sid;
4696
+ yield { type: "init", sessionId: sid, timestamp: now4() };
4697
+ }
4098
4698
  for (const msg of this.spec.mapEvent(parsed)) {
4099
4699
  if (msg.type === "error")
4100
4700
  sawError = true;
4101
4701
  yield msg;
4102
4702
  }
4103
4703
  }
4104
- const res = await runPromise;
4105
- if (res.exitCode !== 0 && !sawError) {
4106
- const tail = (res.stderr ?? "").slice(-2000);
4107
- yield { type: "error", text: `${this.spec.kind} exited with code ${res.exitCode}${tail ? `: ${tail}` : ""}`, timestamp: now4() };
4704
+ await transport;
4705
+ if (transportError)
4706
+ throw transportError;
4707
+ if (exitCode === null) {
4708
+ if (!sawError) {
4709
+ yield { type: "error", text: `${this.spec.kind} aborted — the in-sandbox runner was terminated`, timestamp: now4() };
4710
+ }
4711
+ return;
4712
+ }
4713
+ if (exitCode !== 0 && !sawError) {
4714
+ const detail = exitCode === JSONL_GUARD_NO_SENTINEL_EXIT ? " (the runner died without reporting an exit code)" : "";
4715
+ yield { type: "error", text: `${this.spec.kind} exited with code ${exitCode}${detail}${exitStderr ? `: ${exitStderr}` : ""}`, timestamp: now4() };
4716
+ return;
4717
+ }
4718
+ if (exitCode !== 0 && sawError && RESUME_TARGET_MISSING_STDERR.test(exitStderr)) {
4719
+ yield { type: "error", text: `${this.spec.kind} exited with code ${exitCode}: ${exitStderr}`, timestamp: now4() };
4108
4720
  return;
4109
4721
  }
4110
4722
  if (!sawError)
4111
4723
  yield { type: "done", sessionId: sessionId ?? "", timestamp: now4() };
4112
4724
  } finally {
4113
- opts.signal?.removeEventListener("abort", reap);
4114
- reap();
4115
- this.sandbox.commands.run(`rm -f ${shellQuote(promptPath)} ${shellQuote(guardPath)}`, { timeoutMs: 1e4 }).catch(() => {});
4725
+ opts.signal?.removeEventListener("abort", reapUnlessDetached);
4726
+ consumerStopped = true;
4727
+ if (exitCode === null && !this.guestDetached)
4728
+ reap();
4729
+ if (reapConfirm)
4730
+ await reapConfirm;
4731
+ this.turnInject = null;
4732
+ if (!this.guestDetached) {
4733
+ 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(() => {});
4734
+ }
4116
4735
  }
4117
4736
  } catch (err) {
4118
4737
  yield { type: "error", text: formatError(err), timestamp: now4() };
@@ -4136,10 +4755,19 @@ var CODEX_ADVISORY_PATTERNS = [
4136
4755
  function isCodexAdvisoryNoise(text) {
4137
4756
  return CODEX_ADVISORY_PATTERNS.some((re) => re.test(text));
4138
4757
  }
4758
+ var CODEX_LOCK_WAIT_ATTEMPTS = 20;
4759
+ var CODEX_LOCK_WAIT_SECONDS = "0.25";
4760
+ var CODEX_THREAD_ID_SAFE = /^[A-Za-z0-9_-]{1,64}$/;
4761
+ function codexWriterLockPreflight(threadId, waitAttempts = CODEX_LOCK_WAIT_ATTEMPTS) {
4762
+ if (!CODEX_THREAD_ID_SAFE.test(threadId))
4763
+ return "";
4764
+ 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; `;
4765
+ }
4139
4766
  var codexSpec = {
4140
4767
  kind: "codex",
4141
4768
  authEnv: "CODEX_API_KEY",
4142
4769
  bin: "codex",
4770
+ exclusiveSessionWriter: true,
4143
4771
  acp: {
4144
4772
  command: "npx",
4145
4773
  args: ["-y", CODEX_ACP_ADAPTER],
@@ -4153,11 +4781,12 @@ var codexSpec = {
4153
4781
  "--skip-git-repo-check",
4154
4782
  "--dangerously-bypass-approvals-and-sandbox",
4155
4783
  ...model ? ["-m", shellQuote(model)] : [],
4156
- ...effort ? ["-c", `model_reasoning_effort=${effort === "max" ? "xhigh" : effort}`] : [],
4157
- ...cwd ? ["-C", shellQuote(cwd)] : []
4784
+ ...effort ? ["-c", `model_reasoning_effort=${effort === "max" ? "xhigh" : effort}`] : []
4158
4785
  ].join(" ");
4786
+ const cd = cwd ? `cd ${shellQuote(cwd)} && ` : "";
4787
+ const preflight = sessionId ? codexWriterLockPreflight(sessionId) : "";
4159
4788
  const exec = sessionId ? `codex exec resume ${shellQuote(sessionId)} ${flags}` : `codex exec ${flags}`;
4160
- return `${exec} - < ${shellQuote(promptPath)}`;
4789
+ return `${preflight}${cd}${exec} - < ${shellQuote(promptPath)}`;
4161
4790
  },
4162
4791
  extractSessionId: (p) => p.type === "thread.started" && typeof p.thread_id === "string" ? p.thread_id : undefined,
4163
4792
  mapEvent: (p) => {
@@ -4375,6 +5004,84 @@ function toolResultText(content) {
4375
5004
  }
4376
5005
  return content == null ? "" : JSON.stringify(content) ?? "";
4377
5006
  }
5007
+ var TASK_NOTIFICATION_RE = /<task-notification>([\s\S]*?)<\/task-notification>/g;
5008
+ var NOTIFICATION_SUMMARY_MAX = 500;
5009
+ var NOTIFICATION_REPORT_MAX = 20000;
5010
+ function innerTag(body, tag) {
5011
+ const m = new RegExp(`<${tag}>([\\s\\S]*?)</${tag}>`).exec(body);
5012
+ const text = m?.[1]?.trim() ?? "";
5013
+ return text.length > 0 ? text : null;
5014
+ }
5015
+ function unescapeEntities(s) {
5016
+ return s.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&amp;/g, "&");
5017
+ }
5018
+ var clip = (s, max) => s.length > max ? `${s.slice(0, max - 1)}…` : s;
5019
+ function intTag(body, tag) {
5020
+ const raw = innerTag(body, tag);
5021
+ if (!raw)
5022
+ return;
5023
+ const n = Number.parseInt(raw, 10);
5024
+ return Number.isFinite(n) && n >= 0 ? n : undefined;
5025
+ }
5026
+ function parseTaskNotifications(text, timestamp) {
5027
+ if (!text.includes("<task-notification>"))
5028
+ return [];
5029
+ const out = [];
5030
+ TASK_NOTIFICATION_RE.lastIndex = 0;
5031
+ for (let m = TASK_NOTIFICATION_RE.exec(text);m !== null; m = TASK_NOTIFICATION_RE.exec(text)) {
5032
+ const body = m[1] ?? "";
5033
+ const taskId = innerTag(body, "task-id");
5034
+ if (!taskId)
5035
+ continue;
5036
+ const toolUseId = innerTag(body, "tool-use-id");
5037
+ const summary = innerTag(body, "summary");
5038
+ const report = innerTag(body, "result");
5039
+ const usageBody = /<usage>([\s\S]*?)<\/usage>/.exec(body)?.[1] ?? "";
5040
+ const tokens = intTag(usageBody, "subagent_tokens");
5041
+ const toolUses = intTag(usageBody, "tool_uses");
5042
+ const durationMs = intTag(usageBody, "duration_ms");
5043
+ out.push({
5044
+ type: "task_notification",
5045
+ taskId: clip(taskId, 128),
5046
+ ...toolUseId ? { toolUseId: clip(toolUseId, 128) } : {},
5047
+ status: innerTag(body, "status") ?? "finished",
5048
+ summary: summary ? clip(unescapeEntities(summary.replace(/\s+/g, " ")), NOTIFICATION_SUMMARY_MAX) : "",
5049
+ ...report ? { report: clip(unescapeEntities(report), NOTIFICATION_REPORT_MAX) } : {},
5050
+ ...tokens !== undefined || toolUses !== undefined || durationMs !== undefined ? { usage: {
5051
+ ...tokens !== undefined ? { tokens } : {},
5052
+ ...toolUses !== undefined ? { toolUses } : {},
5053
+ ...durationMs !== undefined ? { durationMs } : {}
5054
+ } } : {},
5055
+ timestamp
5056
+ });
5057
+ }
5058
+ return out;
5059
+ }
5060
+ function parseSystemTaskNotification(p, timestamp) {
5061
+ const taskId = typeof p.task_id === "string" && p.task_id.trim().length > 0 ? p.task_id.trim() : null;
5062
+ if (!taskId)
5063
+ return null;
5064
+ const toolUseId = typeof p.tool_use_id === "string" && p.tool_use_id.length > 0 ? p.tool_use_id : null;
5065
+ const summary = typeof p.summary === "string" ? p.summary : "";
5066
+ const nonneg = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0 ? Math.floor(v) : undefined;
5067
+ const u = typeof p.usage === "object" && p.usage !== null ? p.usage : {};
5068
+ const tokens = nonneg(u.total_tokens);
5069
+ const toolUses = nonneg(u.tool_uses);
5070
+ const durationMs = nonneg(u.duration_ms);
5071
+ return {
5072
+ type: "task_notification",
5073
+ taskId: clip(taskId, 128),
5074
+ ...toolUseId ? { toolUseId: clip(toolUseId, 128) } : {},
5075
+ status: typeof p.status === "string" && p.status.length > 0 ? p.status : "finished",
5076
+ summary: clip(summary.replace(/\s+/g, " ").trim(), NOTIFICATION_SUMMARY_MAX),
5077
+ ...tokens !== undefined || toolUses !== undefined || durationMs !== undefined ? { usage: {
5078
+ ...tokens !== undefined ? { tokens } : {},
5079
+ ...toolUses !== undefined ? { toolUses } : {},
5080
+ ...durationMs !== undefined ? { durationMs } : {}
5081
+ } } : {},
5082
+ timestamp
5083
+ };
5084
+ }
4378
5085
  var CLAUDE_CODE_EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
4379
5086
  var claudeCodeSpec = {
4380
5087
  kind: "claude-code",
@@ -4383,11 +5090,16 @@ var claudeCodeSpec = {
4383
5090
  acp: { command: "npx", args: ["--yes", CLAUDE_CODE_ACP_ADAPTER] },
4384
5091
  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',
4385
5092
  promptPayload: (prompt) => prompt,
4386
- buildCommand: ({ promptPath, sessionId, model, cwd, effort }) => {
5093
+ streamInput: {
5094
+ promptLine: (prompt) => JSON.stringify({ type: "user", message: { role: "user", content: prompt } }),
5095
+ messageLine: (text) => JSON.stringify({ type: "user", message: { role: "user", content: text } })
5096
+ },
5097
+ buildCommand: ({ promptPath, sessionId, model, cwd, effort, streamInput }) => {
4387
5098
  const flags = [
4388
5099
  "-p",
4389
5100
  "--output-format stream-json",
4390
5101
  "--verbose",
5102
+ ...streamInput ? ["--input-format stream-json"] : [],
4391
5103
  "--include-partial-messages",
4392
5104
  "--dangerously-skip-permissions",
4393
5105
  ...model ? [`--model ${shellQuote(model)}`] : [],
@@ -4426,15 +5138,26 @@ var claudeCodeSpec = {
4426
5138
  }
4427
5139
  case "user": {
4428
5140
  const message = p.message;
5141
+ if (typeof message?.content === "string") {
5142
+ return parseTaskNotifications(message.content, ts);
5143
+ }
4429
5144
  const blocks2 = Array.isArray(message?.content) ? message.content : [];
4430
- return blocks2.flatMap((b) => b.type === "tool_result" ? [{
4431
- type: "tool_result",
4432
- toolUseId: String(b.tool_use_id ?? ""),
4433
- output: toolResultText(b.content),
4434
- isError: b.is_error === true,
4435
- ...parent,
4436
- timestamp: ts
4437
- }] : []);
5145
+ return blocks2.flatMap((b) => {
5146
+ if (b.type === "tool_result") {
5147
+ return [{
5148
+ type: "tool_result",
5149
+ toolUseId: String(b.tool_use_id ?? ""),
5150
+ output: toolResultText(b.content),
5151
+ isError: b.is_error === true,
5152
+ ...parent,
5153
+ timestamp: ts
5154
+ }];
5155
+ }
5156
+ if (b.type === "text" && typeof b.text === "string") {
5157
+ return parseTaskNotifications(b.text, ts);
5158
+ }
5159
+ return [];
5160
+ });
4438
5161
  }
4439
5162
  case "stream_event": {
4440
5163
  const ev = p.event;
@@ -4484,6 +5207,12 @@ var claudeCodeSpec = {
4484
5207
  timestamp: ts
4485
5208
  }];
4486
5209
  }
5210
+ case "system": {
5211
+ if (p.subtype !== "task_notification")
5212
+ return [];
5213
+ const notification = parseSystemTaskNotification(p, ts);
5214
+ return notification ? [notification] : [];
5215
+ }
4487
5216
  default:
4488
5217
  return [];
4489
5218
  }
@@ -4552,23 +5281,36 @@ function toE2bNetwork(policy) {
4552
5281
  }
4553
5282
 
4554
5283
  // src/sandbox/sizes.ts
4555
- var SANDBOX_VCPUS = {
4556
- "2vcpu-4gb": 2,
4557
- "4vcpu-8gb": 4,
4558
- "8vcpu-16gb": 8,
4559
- "32vcpu-64gb": 32
5284
+ var SANDBOX_MACHINES = {
5285
+ "1vcpu-2gb": { vcpus: 1, memoryMB: 2048 },
5286
+ "2vcpu-4gb": { vcpus: 2, memoryMB: 4096 },
5287
+ "4vcpu-8gb": { vcpus: 4, memoryMB: 8192 },
5288
+ "8vcpu-8gb": { vcpus: 8, memoryMB: 8192 },
5289
+ "8vcpu-16gb": { vcpus: 8, memoryMB: 16384 },
5290
+ "32vcpu-64gb": { vcpus: 32, memoryMB: 65536 }
4560
5291
  };
5292
+ var SANDBOX_SIZES = Object.keys(SANDBOX_MACHINES);
5293
+ var SANDBOX_VCPUS = Object.fromEntries(SANDBOX_SIZES.map((s) => [s, SANDBOX_MACHINES[s].vcpus]));
4561
5294
  var DEFAULT_SANDBOX_SIZE = "2vcpu-4gb";
4562
- var E2B_TEMPLATE_SIZES = [
4563
- "2vcpu-4gb",
4564
- "4vcpu-8gb"
4565
- ];
5295
+ var SESSION_DEFAULT_SANDBOX_SIZE = "4vcpu-8gb";
5296
+ var E2B_MAX_VCPUS = 8;
5297
+ var E2B_MAX_MEMORY_MB = 8192;
5298
+ var E2B_TEMPLATE_SIZES = SANDBOX_SIZES.filter((s) => SANDBOX_MACHINES[s].vcpus <= E2B_MAX_VCPUS && SANDBOX_MACHINES[s].memoryMB <= E2B_MAX_MEMORY_MB);
4566
5299
  function isE2bSupportedSize(size) {
4567
5300
  return E2B_TEMPLATE_SIZES.includes(size);
4568
5301
  }
5302
+ var VERCEL_MEMORY_MB_PER_VCPU = 2048;
5303
+ function isVercelSupportedSize(size) {
5304
+ const m = SANDBOX_MACHINES[size];
5305
+ return m.memoryMB === m.vcpus * VERCEL_MEMORY_MB_PER_VCPU;
5306
+ }
4569
5307
  function e2bMachineSpec(size) {
4570
- const cpuCount = SANDBOX_VCPUS[size];
4571
- return { cpuCount, memoryMB: cpuCount * 2048 };
5308
+ const m = SANDBOX_MACHINES[size];
5309
+ return { cpuCount: m.vcpus, memoryMB: m.memoryMB };
5310
+ }
5311
+ function sandboxSizeLabel(size) {
5312
+ const m = SANDBOX_MACHINES[size];
5313
+ return `${m.vcpus} vCPU · ${Math.round(m.memoryMB / 1024)} GB`;
4572
5314
  }
4573
5315
  function e2bBaseTemplate(size) {
4574
5316
  return `agent-compose-base-${size}`;
@@ -4785,7 +5527,7 @@ function makeE2bSandboxProvider(sb) {
4785
5527
  return { resumeHandle: sb.sandboxId };
4786
5528
  },
4787
5529
  async extendLifetime(ms) {
4788
- await sb.setTimeout(ms);
5530
+ await sb.setTimeout(Math.min(ms, e2bMaxSandboxMs()));
4789
5531
  },
4790
5532
  async updateNetworkPolicy(policy) {
4791
5533
  await sb.updateNetwork(toE2bNetwork(policy));
@@ -5089,8 +5831,9 @@ function makeLocalSandboxProvider() {
5089
5831
  stderr += chunk;
5090
5832
  opts?.onStderr?.(chunk);
5091
5833
  });
5092
- proc.on("error", reject);
5093
- proc.on("close", (code) => resolve({ exitCode: code ?? 0, stdout, stderr }));
5834
+ const ev = proc;
5835
+ ev.on("error", reject);
5836
+ ev.on("close", (code) => resolve({ exitCode: code ?? 0, stdout, stderr }));
5094
5837
  });
5095
5838
  },
5096
5839
  spawnDuplex(cmd, opts) {
@@ -5106,8 +5849,9 @@ function makeLocalSandboxProvider() {
5106
5849
  stderr += chunk;
5107
5850
  });
5108
5851
  const exited = new Promise((resolve, reject) => {
5109
- proc.on("error", reject);
5110
- proc.on("close", (code) => resolve({ exitCode: code ?? 0, stderr }));
5852
+ const ev = proc;
5853
+ ev.on("error", reject);
5854
+ ev.on("close", (code) => resolve({ exitCode: code ?? 0, stderr }));
5111
5855
  });
5112
5856
  return {
5113
5857
  stdin: Writable.toWeb(proc.stdin),
@@ -6411,27 +7155,81 @@ like?" is answered by opening it on this desktop and screenshotting it — not b
6411
7155
  reasoning about the code, and not by a headless render (which proves the process
6412
7156
  starts, not that the thing draws). Verify visually before you report visually.
6413
7157
 
7158
+ **This is how you ACT on the web.** When the task is to DO something on a
7159
+ website — book, order, reserve, sign up, fill a form, operate a dashboard —
7160
+ and no connector or API covers it, the desktop browser IS the tool: \`ac-open\`
7161
+ the site, do the errand there, and show the human the screen at decision
7162
+ points (\`agentc display desktop\` in a cloud session). Research/search tools
7163
+ answer QUESTIONS; an errand is an ACTION — "book me a table" means open the
7164
+ booking site and book it, never a research report of options.
7165
+
6414
7166
  - **Input** — \`xdotool\` against \`DISPLAY=:0\`: \`DISPLAY=:0 xdotool mousemove <x> <y>\`,
6415
7167
  \`DISPLAY=:0 xdotool click 1\` (1=left, 3=right), \`DISPLAY=:0 xdotool type 'text'\`,
6416
7168
  \`DISPLAY=:0 xdotool key Return\` (also \`ctrl+c\`, \`Tab\`, \`super\`, …).
6417
7169
  - **Screenshots** — \`scrot\` (or ImageMagick's \`import\`):
6418
7170
  \`DISPLAY=:0 scrot /tmp/screen.png\`, then READ the PNG to see the screen,
6419
7171
  before and after you act. A screenshot is your only eyes here.
6420
- - **Apps + windows** — a plain X session. Launch in the background:
6421
- \`DISPLAY=:0 <app> &\`. Two things that trip agents up, both normal:
7172
+ - **The browser is chromium, preinstalled** — headful, on this display
7173
+ (\`command -v chromium\` to confirm on an older machine). If an older machine
7174
+ is missing it, the platform is already installing it in the background from
7175
+ boot — \`ac-open <url>\` tells you when that is the case; retry it in ~30s.
7176
+ Only if \`ac-open\` reports the background install FAILED do you relay that
7177
+ one line to the human — never an apt-get expedition of your own.
7178
+ - **Launching apps — use \`ac-open\`, never a plain \`&\`.** A GUI process
7179
+ launched with \`<app> &\` DIES the moment your shell command returns — the
7180
+ sandbox reaps each command's process group, so "the window vanished when
7181
+ the shell finished" is that reaping, not a broken app. \`ac-open\` is the
7182
+ platform launcher that survives it (\`command -v ac-open\` on older machines):
7183
+
7184
+ ac-open https://github.com # the browser — a running instance gets a tab
7185
+ ac-open ./report.html # a local file, in the browser
7186
+ ac-open . # a directory, in the file manager
7187
+ ac-open gimp # any GUI app by command name
7188
+
7189
+ It detaches the app into its own session (setsid, stdio off your command's
7190
+ pipes), records a pidfile + log under \`/tmp/.ac-desktop-open.<uid>/\`
7191
+ (per-uid — yours is \`/tmp/.ac-desktop-open.$(id -u)\`), and
7192
+ re-invoking it for a running app FOCUSES the existing window instead of
7193
+ spawning a second copy. \`xdg-open\` and \`sensible-browser\` route through
7194
+ it too. The whole recipe for looking at a page: \`ac-open <url>\`, then
7195
+ \`sleep 5\`, then \`DISPLAY=:0 scrot /tmp/screen.png\` and read it. Without
7196
+ \`ac-open\` (older machine), detach by hand:
7197
+ \`setsid <app> </dev/null >/tmp/app.log 2>&1 &\` — and note **chromium as
7198
+ root also needs \`--no-sandbox\`** (nested sandbox; \`ac-open\` and the baked
7199
+ chromium defaults already handle it).
7200
+ - **Two things that trip agents up, both normal:**
6422
7201
  - a GUI app needs a **beat to map its window** — screenshot, and if you see
6423
7202
  only wallpaper, wait a couple of seconds and screenshot again before
6424
7203
  concluding anything;
6425
- - **Chromium needs \`--no-sandbox\`** in this environment (nested sandbox).
6426
- The whole recipe for looking at a local page:
6427
- \`DISPLAY=:0 chromium --no-sandbox --disable-gpu --start-maximized <url> &\`
6428
- then \`sleep 5\`, then \`DISPLAY=:0 scrot /tmp/screen.png\` and read it.
6429
- If a window still never appears, read the app's own log (\`/tmp/*.log\`) — the
6430
- desktop is not the thing that failed. Do NOT abandon it for a headless
6431
- screenshot: headless cannot tell you what the human will see.
7204
+ - if a window still never appears, read the app's own log
7205
+ (\`/tmp/.ac-desktop-open.$(id -u)/*.log\`, \`/tmp/*.log\`) — the desktop is not the
7206
+ thing that failed. Do NOT abandon it for a headless
7207
+ screenshot: headless cannot tell you what the human will see.
6432
7208
  - **A human can watch** — the session header carries a **Desktop** button in the
6433
7209
  dashboard, and what a teammate sees there is exactly this display. The desktop
6434
7210
  runs whether or not anyone is looking; never wait for a viewer.
7211
+ - **Show the human the screen** — in a cloud session,
7212
+ \`agentc display desktop --note "<caption>"\` captures this display and posts
7213
+ it into the conversation as a snapshot card with an "Open desktop" door to
7214
+ the live view. Use it to report visual results, and ALWAYS when you hit a
7215
+ wall on the desktop that only a human can clear — a login form, a 2FA
7216
+ prompt, a CAPTCHA, an unexpected dialog: snapshot it so they SEE the wall,
7217
+ then ask (AskUserQuestion when you have it) and wait; never guess
7218
+ credentials or click around a wall. The rule is SCREEN FOR ACTIONS,
7219
+ VAULT FOR SECRETS. For non-sensitive interaction that needs the human's
7220
+ own hands or judgment — pick an option, review a page, solve a CAPTCHA —
7221
+ the display + ask pair is right: the platform merges them into ONE live
7222
+ desktop card — the human clicks in, acts on the live screen, and answers
7223
+ "I'm done" to hand it back; treat that answer as the wall being cleared,
7224
+ re-check the screen, and continue. For SECRETS — a password, payment
7225
+ details, any sensitive value —
7226
+ \`agentc secrets session request <KEY...> --reason "<why>" --wait\` mints a
7227
+ secure vault link (a one-tap approval when the user has these saved as a
7228
+ personal set); the values land in the session env and YOU type them into
7229
+ the site on the user's behalf. Never ask the human to type a password or
7230
+ card number into this machine's browser, and never suggest they "log in
7231
+ on the Desktop view" — the vault carries the secret, then you act with
7232
+ it. A one-time 2FA code from their phone is the chat-OK exception.
6435
7233
 
6436
7234
  Nothing here changes the credentials rule above: tokens are injected at the
6437
7235
  network layer, never present on the desktop or in any file you can read — so
@@ -6439,38 +7237,49 @@ there is nothing to type, paste, or screenshot a credential from.
6439
7237
 
6440
7238
  ## Recording a demo — the desktop, captured to a video the human can play
6441
7239
 
6442
- "Record a demo of you using X" is a normal ask, and this machine does it:
6443
- start a screen recording, drive the app with \`xdotool\` exactly as in Computer
6444
- Use, stop the recording, and report the file. (For a LIVE view no recording is
6445
- needed — the session header's **Desktop** button already streams this display
6446
- to any teammate watching; a recording is the durable, replayable artifact.
6447
- Both modes exist; say so when it matters.)
7240
+ "Record a demo of you using X" is a normal ask, and this machine does it.
7241
+ (For a LIVE view no recording is needed — the session header's **Desktop**
7242
+ button already streams this display to any teammate watching; a recording is
7243
+ the durable, replayable artifact. Both modes exist; say so when it matters.)
6448
7244
 
6449
- **ffmpeg is NOT pre-installed** — install it first, once per machine:
7245
+ **Use \`ac-record\` — the platform recorder is already on PATH** (cloud
7246
+ sessions; \`command -v ac-record\` to confirm on older machines):
7247
+
7248
+ ac-record start # begins capturing the desktop (display :0)
7249
+ # ... drive the app with xdotool, screenshotting as you go ...
7250
+ ac-record stop # finishes + saves to recordings/ in your workspace
7251
+ ac-record status # one JSON line: {"recording":true,...}
6450
7252
 
6451
- sudo apt-get update -q && sudo apt-get install -y -q ffmpeg
7253
+ It records the whole display (with desktop audio when the machine has a
7254
+ PulseAudio monitor), enforces sane caps (5 min / 200 MB — start a fresh
7255
+ recording per scene rather than one long take), keeps the file playable even
7256
+ if the machine dies mid-take, and \`stop\` prints the saved path — the file
7257
+ lands ON THE DRIVE in \`recordings/\`, visible in Files and playable in the
7258
+ dashboard. A human watching the Desktop pane sees the recording indicator
7259
+ while you record.
6452
7260
 
6453
- (drop \`sudo\` if you are already root). Then the whole recipe:
7261
+ If \`ac-record\` is missing (older machine), record by hand.
7262
+ **ffmpeg IS pre-installed** on platform images (\`command -v ffmpeg\`; only
7263
+ if absent: \`sudo apt-get update -q && sudo apt-get install -y -q ffmpeg\`):
6454
7264
 
6455
7265
  DISPLAY=:0 ffmpeg -f x11grab \\
6456
7266
  -video_size "$(DISPLAY=:0 xdotool getdisplaygeometry | tr ' ' x)" \\
6457
7267
  -framerate 10 -i :0 -c:v libvpx -b:v 1M -deadline realtime -cpu-used 8 \\
6458
7268
  demo.webm &
6459
7269
  FFMPEG_PID=$!
6460
- # ... drive the app with xdotool, screenshotting as you go ...
7270
+ # ... drive the app with xdotool ...
6461
7271
  kill -INT "$FFMPEG_PID" && wait "$FFMPEG_PID"
6462
7272
 
6463
- The gotchas, each one earned:
7273
+ The hand-rolled gotchas, each one earned:
6464
7274
  - **Stop with SIGINT (\`kill -INT\`), never SIGKILL** — ffmpeg finalizes the
6465
7275
  file on SIGINT; a hard kill truncates the encode mid-write.
6466
- - **Record WebM (matroska-family), not MP4** — mp4 writes its moov atom at the
6467
- END, so a killed or crashed encode leaves an UNPLAYABLE file; webm stays
6468
- playable up to the last written frame and plays natively in the browser.
6469
- MP4's only edge is compatibility with some external players — transcode
6470
- afterwards if you truly need it, never record straight to it.
7276
+ - **Record WebM (matroska-family), not plain MP4** — mp4 writes its moov atom
7277
+ at the END, so a killed or crashed encode leaves an UNPLAYABLE file; webm
7278
+ stays playable up to the last written frame and plays natively in the
7279
+ browser. (\`ac-record\` sidesteps this with fragmented mp4.)
6471
7280
  - **\`-video_size\` must match the real screen** — x11grab does not default to
6472
7281
  it; read the geometry from \`xdotool getdisplaygeometry\` as above.
6473
- - **10–12 fps is right for a screen demo** — small files, legible UI motion;
7282
+ - **10–15 fps is right for a screen demo** — small files, legible UI motion;
6474
7283
  this is not video production.
6475
7284
  - **Write to the drive, not /tmp** — the recording must land in your working
6476
7285
  directory to persist and show up in Files; a file in /tmp dies with the
@@ -6511,14 +7320,49 @@ origin of its own — absolute asset paths and client-side routing work, the
6511
7320
  whole app is navigable — so serve normally and let the platform address it;
6512
7321
  never rewrite your app to a path prefix.
6513
7322
 
7323
+ ## Durable services — the machine is cattle, the manifest is the pet (cloud sessions)
7324
+
7325
+ Parking preserves detached processes; a machine RECYCLE (resize, eviction,
7326
+ failed reconnect) does not — every process and every byte off the drive is
7327
+ discarded, and recycles are normal. When you start a long-running service the
7328
+ human will rely on across turns (a dev server, a docker compose stack, a
7329
+ database), record it in \`.ac/services.yml\` at the drive root so the platform
7330
+ relaunches it automatically on the next fresh machine:
7331
+
7332
+ agentc services add <name> --command '<cmd>' # record a service
7333
+ agentc services list # manifest + live status
7334
+ agentc services restore # run the manifest now
7335
+ agentc services remove <name>
7336
+
7337
+ Each entry can carry \`cwd\`, \`port\`, a bounded \`health\` probe (cmd or
7338
+ http), one-time \`setup\` (e.g. \`docker compose pull\`), and \`data\` hooks.
7339
+ After a recycle the platform posts "Machine restarted — restored N services"
7340
+ into the conversation; on seeing it, VERIFY health rather than rebuilding —
7341
+ logs live at \`/tmp/ac-services/<name>.log\`. Data honesty: sandbox-local
7342
+ database state dies with the machine. Keep seeds/dumps ON THE DRIVE; declare
7343
+ \`data.restore\` (reload on fresh boot) and \`data.dump\` (written before a
7344
+ DELIBERATE recycle such as a resize — evictions give no warning, so treat the
7345
+ drive copy as the truth).
7346
+
6514
7347
  ## Tools in this environment
6515
7348
 
6516
7349
  - \`agentc\` — Agent Compose CLI (your primary interface; authed from env)
6517
7350
  - \`@agent-compose/sdk\` — installed in /workspace for writing workflows
6518
7351
  - \`/ac:*\` Claude Code skills — slash commands for the above
6519
- - \`archil\` (factory drive), \`rtk\`, \`bun\`
7352
+ - \`rtk\`, \`bun\`
6520
7353
  - \`xdotool\` / \`scrot\` — drive + screenshot the desktop (if this machine has one; see Computer Use)
6521
- - A world-writable \`/workspace\` working directory`;
7354
+ - \`chromium\` — the desktop browser; \`ac-open <url|file|app>\` — open it on the
7355
+ desktop, detached (survives your command; see Computer Use)
7356
+ - A world-writable \`/workspace\` working directory
7357
+
7358
+ If a system capability you need is genuinely missing — no browser, no display,
7359
+ no \`ac-open\`, a daemon that isn't there — say so to the human in ONE honest
7360
+ line (what is missing and what it blocks) instead of mounting a
7361
+ package-manager expedition. An in-session \`apt-get install\` dies with the
7362
+ sandbox, burns turns, and hides the real gap; missing platform capabilities
7363
+ are the platform's to bake in, and \`agentc pause\` is the door to ask through.
7364
+ (Your own project's dependencies are different — installing those is normal
7365
+ work.)`;
6522
7366
  function buildAddedSessionBrief(p) {
6523
7367
  const dashboardSection = p.dashboardUrl === null ? "" : `
6524
7368
 
@@ -6825,6 +7669,565 @@ async function agent(opts) {
6825
7669
  inboxAbort.abort();
6826
7670
  }
6827
7671
  }
7672
+ // src/agent/desktop-open.ts
7673
+ var AC_OPEN_PATH = "/usr/local/bin/ac-open";
7674
+ var AC_OPEN_STATE_DIR = "/tmp/.ac-desktop-open";
7675
+ var AC_OPEN_SHIM_PATHS = [
7676
+ "/usr/local/bin/xdg-open",
7677
+ "/usr/local/bin/sensible-browser"
7678
+ ];
7679
+ var BROWSER_BACKFILL_PIDFILE = `${AC_OPEN_STATE_DIR}/browser-install.pid`;
7680
+ var BROWSER_BACKFILL_LOG = `${AC_OPEN_STATE_DIR}/browser-install.log`;
7681
+ var BROWSER_BACKFILL_FAILED = `${AC_OPEN_STATE_DIR}/browser-install.failed`;
7682
+ 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";
7683
+ var AC_OPEN_SCRIPT = `#!/bin/sh
7684
+ # ac-open — open a URL, file, or GUI app on this machine's desktop, DETACHED.
7685
+ #
7686
+ # ac-open https://github.com the browser (a running instance gets a tab)
7687
+ # ac-open ./report.html a local file, in the browser
7688
+ # ac-open . a directory, in the file manager
7689
+ # ac-open gimp [args...] any GUI app by command name
7690
+ #
7691
+ # Why: the sandbox reaps each exec'd command's process group when the command
7692
+ # returns, so a GUI app launched with a plain \`&\` dies the moment the
7693
+ # launching shell finishes. ac-open detaches the app into its own session
7694
+ # (setsid, stdio off the exec's pipes), records a pidfile, and returns at
7695
+ # once; re-invoking it for a running app focuses the existing window instead
7696
+ # of spawning a second copy. Logs + pidfiles: /tmp/.ac-desktop-open.<uid>/.
7697
+ # Written by agent-compose (sdk/src/agent/desktop-open.ts) — do not edit in place.
7698
+ set -u
7699
+ export DISPLAY="\${DISPLAY:-:0}"
7700
+ # Per-UID state (pidfiles + logs): the shared dir is root-owned when the
7701
+ # backfill created it, and a user-side >>$LOG into a root 0755 dir fails
7702
+ # BEFORE the app runs — the false "exited immediately" with an empty log.
7703
+ D="/tmp/.ac-desktop-open.$(id -u)"
7704
+ # Shared, root-written backfill markers — READ-ONLY here.
7705
+ M=/tmp/.ac-desktop-open
7706
+ mkdir -p "$D" 2>/dev/null || true
7707
+ [ -w "$D" ] || { echo "ac-open: state dir $D is not writable by uid $(id -u)" >&2; exit 1; }
7708
+
7709
+ [ $# -ge 1 ] || { echo 'usage: ac-open <url|file|dir|app> [args...]' >&2; exit 2; }
7710
+ TARGET=$1; shift
7711
+
7712
+ # No display = no desktop on this image. One honest line, no expedition.
7713
+ if ! timeout 3 xdotool getdisplaygeometry >/dev/null 2>&1; then
7714
+ echo "ac-open: no desktop display on $DISPLAY — this machine has no GUI session up" >&2
7715
+ exit 1
7716
+ fi
7717
+
7718
+ pick_browser() {
7719
+ for b in chromium chromium-browser x-www-browser google-chrome; do
7720
+ command -v "$b" >/dev/null 2>&1 && { echo "$b"; return 0; }
7721
+ done
7722
+ return 1
7723
+ }
7724
+
7725
+ # Best-effort: raise the newest visible window whose class matches $1.
7726
+ focus_class() {
7727
+ WID=$(xdotool search --onlyvisible --class "$1" 2>/dev/null | tail -1) || WID=''
7728
+ [ -n "\${WID:-}" ] && xdotool windowactivate "$WID" 2>/dev/null || true
7729
+ }
7730
+
7731
+ # Same-UID instance probe: another identity's instance shares neither profile
7732
+ # nor single-instance socket, so only our own uid's counts as "running".
7733
+ alive_same_uid() { pgrep -x -u "$(id -u)" "$1" >/dev/null 2>&1; }
7734
+
7735
+ # Singleton apps (browser, file manager): the second invocation hands the
7736
+ # target to the running instance and exits — that IS the idempotent path, so
7737
+ # spawn detached every time and only liveness-check a FRESH instance.
7738
+ singleton_open() { # $1 = state key, rest = command + target
7739
+ NAME=$1; shift
7740
+ BIN=$(basename "$1")
7741
+ PIDFILE=$D/$NAME.pid; LOG=$D/$NAME.log
7742
+ RUNNING=0
7743
+ alive_same_uid "$BIN" && RUNNING=1
7744
+ [ "$RUNNING" = 0 ] && [ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE" 2>/dev/null)" 2>/dev/null && RUNNING=1
7745
+ setsid "$@" </dev/null >>"$LOG" 2>&1 &
7746
+ PID=$!
7747
+ if [ "$RUNNING" = 1 ]; then
7748
+ sleep 1
7749
+ focus_class "$BIN"
7750
+ echo "ac-open: handed off to the running $BIN — no second window"
7751
+ return 0
7752
+ fi
7753
+ echo "$PID" > "$PIDFILE"
7754
+ sleep 1
7755
+ if ! kill -0 "$PID" 2>/dev/null; then
7756
+ # Died within the liveness window — but if OUR instance of $BIN is alive
7757
+ # NOW, this was the single-instance HAND-OFF: the pre-check raced a
7758
+ # still-starting instance, the fresh process handed the target over and
7759
+ # exited 0. That is success (N calls = N tabs), never a crash report.
7760
+ if alive_same_uid "$BIN"; then
7761
+ focus_class "$BIN"
7762
+ echo "ac-open: handed off to the running $BIN — no second window"
7763
+ return 0
7764
+ fi
7765
+ TAIL=$(tail -c 300 "$LOG" 2>/dev/null | tr '\\n' ' ')
7766
+ echo "ac-open: $BIN exited immediately: \${TAIL:-no output captured — see $LOG}" >&2
7767
+ exit 1
7768
+ fi
7769
+ focus_class "$BIN"
7770
+ echo "ac-open: launched $BIN (pid $PID), detached — it survives this command"
7771
+ }
7772
+
7773
+ open_url() { # $1 = url
7774
+ B=$(pick_browser) || {
7775
+ # Grandfathered image mid-heal: the boot-time desktop-ensure found no
7776
+ # browser and detached an apt install (browserBackfillCmd — the pidfile
7777
+ # below is ITS single-flight marker, root-written in the SHARED dir).
7778
+ # Third honest state: arriving.
7779
+ if [ -f "$M/browser-install.pid" ] && kill -0 "$(cat "$M/browser-install.pid" 2>/dev/null)" 2>/dev/null; then
7780
+ echo "${BROWSER_INSTALL_IN_PROGRESS_MSG}" >&2
7781
+ exit 75
7782
+ fi
7783
+ if [ -f "$M/browser-install.failed" ]; then
7784
+ TAIL=$(tail -c 300 "$M/browser-install.log" 2>/dev/null | tr '\\n' ' ')
7785
+ 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
7786
+ exit 1
7787
+ fi
7788
+ 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
7789
+ exit 1
7790
+ }
7791
+ singleton_open browser "$B" "$1"
7792
+ }
7793
+
7794
+ open_app() { # $1 = command, rest = args
7795
+ command -v "$1" >/dev/null 2>&1 || {
7796
+ echo "ac-open: '$1' is not installed on this machine — report the missing tool in one line instead of package-hunting for it" >&2
7797
+ exit 127
7798
+ }
7799
+ BIN=$(basename "$1"); PIDFILE=$D/app-$BIN.pid; LOG=$D/app-$BIN.log
7800
+ OLD=$(cat "$PIDFILE" 2>/dev/null || true)
7801
+ if [ -n "\${OLD:-}" ] && kill -0 "$OLD" 2>/dev/null; then
7802
+ WID=$(xdotool search --onlyvisible --pid "$OLD" 2>/dev/null | tail -1) || WID=''
7803
+ [ -z "\${WID:-}" ] && { WID=$(xdotool search --onlyvisible --class "$BIN" 2>/dev/null | tail -1) || WID=''; }
7804
+ [ -n "\${WID:-}" ] && xdotool windowactivate "$WID" 2>/dev/null || true
7805
+ echo "ac-open: $BIN is already running (pid $OLD) — focused it instead of starting a second copy"
7806
+ exit 0
7807
+ fi
7808
+ CMD=$1; shift
7809
+ setsid "$CMD" "$@" </dev/null >>"$LOG" 2>&1 &
7810
+ PID=$!
7811
+ echo "$PID" > "$PIDFILE"
7812
+ sleep 1
7813
+ if ! kill -0 "$PID" 2>/dev/null; then
7814
+ # Same hand-off grace as the singletons: VS Code and friends are
7815
+ # single-instance too — a fresh spawn that exits at once while our own
7816
+ # instance is alive HANDED OFF, it did not crash.
7817
+ if alive_same_uid "$BIN"; then
7818
+ focus_class "$BIN"
7819
+ echo "ac-open: handed off to the running $BIN — no second window"
7820
+ exit 0
7821
+ fi
7822
+ TAIL=$(tail -c 300 "$LOG" 2>/dev/null | tr '\\n' ' ')
7823
+ echo "ac-open: $BIN exited immediately: \${TAIL:-no output captured — see $LOG}" >&2
7824
+ exit 1
7825
+ fi
7826
+ echo "ac-open: launched $BIN (pid $PID), detached — it survives this command"
7827
+ }
7828
+
7829
+ case "$TARGET" in
7830
+ http://*|https://*|file://*|about:*|chrome://*)
7831
+ open_url "$TARGET" ;;
7832
+ localhost|localhost:*|localhost/*|127.0.0.1|127.0.0.1:*|127.0.0.1/*)
7833
+ open_url "http://$TARGET" ;;
7834
+ *)
7835
+ if [ -d "$TARGET" ]; then
7836
+ command -v pcmanfm >/dev/null 2>&1 || {
7837
+ echo "ac-open: no file manager on this image (expected pcmanfm) — report it in one line" >&2
7838
+ exit 1
7839
+ }
7840
+ case "$TARGET" in /*) ABS=$TARGET ;; *) ABS=$PWD/$TARGET ;; esac
7841
+ singleton_open files pcmanfm "$ABS"
7842
+ elif [ -f "$TARGET" ]; then
7843
+ case "$TARGET" in /*) ABS=$TARGET ;; *) ABS=$PWD/$TARGET ;; esac
7844
+ open_url "file://$ABS"
7845
+ else
7846
+ open_app "$TARGET" "$@"
7847
+ fi ;;
7848
+ esac
7849
+ `;
7850
+ var AC_OPEN_SHIM = `#!/bin/sh
7851
+ # agent-compose: generic open routes through ac-open (the detaching launcher).
7852
+ exec ${AC_OPEN_PATH} "$@"
7853
+ `;
7854
+ function installDesktopOpenCmd() {
7855
+ const scriptB64 = Buffer.from(AC_OPEN_SCRIPT, "utf8").toString("base64");
7856
+ const shimB64 = Buffer.from(AC_OPEN_SHIM, "utf8").toString("base64");
7857
+ return [
7858
+ `printf %s '${scriptB64}' | base64 -d > ${AC_OPEN_PATH}`,
7859
+ `chmod 0755 ${AC_OPEN_PATH}`,
7860
+ `sh -n ${AC_OPEN_PATH}`,
7861
+ ...AC_OPEN_SHIM_PATHS.flatMap((p) => [
7862
+ `printf %s '${shimB64}' | base64 -d > ${p}`,
7863
+ `chmod 0755 ${p}`,
7864
+ `sh -n ${p}`
7865
+ ])
7866
+ ].join(" && ");
7867
+ }
7868
+ var CHROMIUM_DEFAULTS_PATH = "/etc/chromium.d/00-ac-defaults";
7869
+ var CHROMIUM_DEFAULTS = `# agent-compose session-desktop chromium defaults
7870
+ # (single-sourced in sdk/src/agent/desktop-open.ts — sourced by /usr/bin/chromium)
7871
+ 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"
7872
+ if [ "$(id -u)" = "0" ]; then CHROMIUM_FLAGS="$CHROMIUM_FLAGS --no-sandbox"; fi
7873
+ `;
7874
+ function installChromiumDefaultsCmd() {
7875
+ const b64 = Buffer.from(CHROMIUM_DEFAULTS, "utf8").toString("base64");
7876
+ return [
7877
+ "mkdir -p /etc/chromium.d",
7878
+ `printf %s ${b64} | base64 -d > ${CHROMIUM_DEFAULTS_PATH}`,
7879
+ `chmod 0644 ${CHROMIUM_DEFAULTS_PATH}`,
7880
+ `sh -n ${CHROMIUM_DEFAULTS_PATH}`
7881
+ ].join(" && ");
7882
+ }
7883
+ function browserBackfillCmd() {
7884
+ const pid = BROWSER_BACKFILL_PIDFILE;
7885
+ 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`;
7886
+ }
7887
+ function browserBackfillStatusCmd() {
7888
+ 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`;
7889
+ }
7890
+ // src/agent/services-manifest.ts
7891
+ var SERVICES_MANIFEST_RELPATH = ".ac/services.yml";
7892
+ var SERVICES_MAX = 16;
7893
+ var SERVICE_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,31}$/;
7894
+ var TOP_KEYS = new Set(["version", "services"]);
7895
+ var ENTRY_KEYS = new Set(["name", "command", "cwd", "port", "env", "setup", "health", "data"]);
7896
+ var HEALTH_KEYS = new Set(["cmd", "http"]);
7897
+ var DATA_KEYS = new Set(["dump", "restore"]);
7898
+ function lex(text) {
7899
+ const out = [];
7900
+ const raw = text.split(/\r?\n/);
7901
+ for (let i = 0;i < raw.length; i++) {
7902
+ const line = raw[i];
7903
+ const trimmed = line.trim();
7904
+ if (trimmed === "" || trimmed.startsWith("#"))
7905
+ continue;
7906
+ const indent = line.length - line.trimStart().length;
7907
+ const dash = trimmed.startsWith("- ") || trimmed === "-";
7908
+ out.push({
7909
+ no: i + 1,
7910
+ indent,
7911
+ dash,
7912
+ text: dash ? trimmed.slice(1).trim() : trimmed
7913
+ });
7914
+ }
7915
+ return out;
7916
+ }
7917
+ function splitKey(text) {
7918
+ if (text.endsWith(":") && !text.includes(": ")) {
7919
+ const key2 = text.slice(0, -1).trim();
7920
+ return /^[A-Za-z_][\w-]*$/.test(key2) ? { key: key2, value: "" } : null;
7921
+ }
7922
+ const idx = text.indexOf(": ");
7923
+ if (idx === -1)
7924
+ return null;
7925
+ const key = text.slice(0, idx).trim();
7926
+ if (!/^[A-Za-z_][\w-]*$/.test(key))
7927
+ return null;
7928
+ return { key, value: text.slice(idx + 2).trim() };
7929
+ }
7930
+ function scalar(value, no, errors) {
7931
+ if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
7932
+ return value.slice(1, -1).replace(/\\(["\\])/g, "$1");
7933
+ }
7934
+ if (value.length >= 2 && value.startsWith("'") && value.endsWith("'")) {
7935
+ return value.slice(1, -1).replace(/''/g, "'");
7936
+ }
7937
+ if (value === "|" || value === ">" || value.startsWith("| ") || value.startsWith("> ")) {
7938
+ errors.push(`line ${no}: block scalars (| / >) are not supported — keep commands single-line`);
7939
+ return "";
7940
+ }
7941
+ return value;
7942
+ }
7943
+ function parseServicesManifest(text) {
7944
+ const errors = [];
7945
+ if (text.length > 64 * 1024) {
7946
+ return { manifest: null, errors: ["manifest exceeds 64KB — this file is a service list, not a data store"] };
7947
+ }
7948
+ const lines = lex(text);
7949
+ if (lines.length === 0)
7950
+ return { manifest: null, errors: ["manifest is empty"] };
7951
+ let version = null;
7952
+ const services = [];
7953
+ let current = null;
7954
+ let currentIndent = -1;
7955
+ let nested = null;
7956
+ let nestedIndent = -1;
7957
+ let inServices = false;
7958
+ const finish = (entry) => {
7959
+ if (!entry)
7960
+ return;
7961
+ services.push(entry);
7962
+ };
7963
+ for (const line of lines) {
7964
+ const kv = splitKey(line.text);
7965
+ if (!kv) {
7966
+ errors.push(`line ${line.no}: not a \`key: value\` mapping — flow collections and bare scalars are not supported`);
7967
+ continue;
7968
+ }
7969
+ if (line.indent === 0 && !line.dash) {
7970
+ inServices = false;
7971
+ nested = null;
7972
+ finish(current);
7973
+ current = null;
7974
+ if (!TOP_KEYS.has(kv.key)) {
7975
+ errors.push(`line ${line.no}: unknown top-level key \`${kv.key}\` (allowed: version, services)`);
7976
+ continue;
7977
+ }
7978
+ if (kv.key === "version") {
7979
+ const v = Number(scalar(kv.value, line.no, errors));
7980
+ if (v !== 1)
7981
+ errors.push(`line ${line.no}: unsupported manifest version \`${kv.value}\` (this build understands version 1)`);
7982
+ else
7983
+ version = 1;
7984
+ } else {
7985
+ if (kv.value !== "")
7986
+ errors.push(`line ${line.no}: \`services:\` must open a list, not carry a value`);
7987
+ inServices = true;
7988
+ }
7989
+ continue;
7990
+ }
7991
+ if (!inServices) {
7992
+ errors.push(`line ${line.no}: unexpected indented line outside \`services:\``);
7993
+ continue;
7994
+ }
7995
+ if (line.dash) {
7996
+ finish(current);
7997
+ current = { name: "", command: "" };
7998
+ currentIndent = line.indent;
7999
+ nested = null;
8000
+ if (kv.key !== "name") {
8001
+ errors.push(`line ${line.no}: each service entry must start with \`- name: <name>\``);
8002
+ continue;
8003
+ }
8004
+ current.name = scalar(kv.value, line.no, errors);
8005
+ continue;
8006
+ }
8007
+ if (!current) {
8008
+ errors.push(`line ${line.no}: expected \`- name: <name>\` to open a service entry`);
8009
+ continue;
8010
+ }
8011
+ if (nested && line.indent > nestedIndent) {
8012
+ const allowed = nested === "health" ? HEALTH_KEYS : DATA_KEYS;
8013
+ if (!allowed.has(kv.key)) {
8014
+ errors.push(`line ${line.no}: unknown \`${nested}\` key \`${kv.key}\` (allowed: ${[...allowed].join(", ")})`);
8015
+ continue;
8016
+ }
8017
+ const value2 = scalar(kv.value, line.no, errors);
8018
+ if (value2 === "") {
8019
+ errors.push(`line ${line.no}: \`${nested}.${kv.key}\` needs a value`);
8020
+ continue;
8021
+ }
8022
+ const bucket = current[nested] ??= {};
8023
+ bucket[kv.key] = value2;
8024
+ continue;
8025
+ }
8026
+ nested = null;
8027
+ if (line.indent <= currentIndent) {
8028
+ errors.push(`line ${line.no}: bad indentation — service fields must be indented under their \`- name:\` line`);
8029
+ continue;
8030
+ }
8031
+ if (!ENTRY_KEYS.has(kv.key)) {
8032
+ errors.push(`line ${line.no}: unknown service key \`${kv.key}\` (allowed: ${[...ENTRY_KEYS].join(", ")})`);
8033
+ continue;
8034
+ }
8035
+ if (kv.key === "health" || kv.key === "data") {
8036
+ if (kv.value !== "") {
8037
+ errors.push(`line ${line.no}: \`${kv.key}:\` opens a nested map — put cmd/http (or dump/restore) on indented lines below`);
8038
+ continue;
8039
+ }
8040
+ nested = kv.key;
8041
+ nestedIndent = line.indent;
8042
+ continue;
8043
+ }
8044
+ const value = scalar(kv.value, line.no, errors);
8045
+ if (kv.key === "port") {
8046
+ const port = Number(value);
8047
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
8048
+ errors.push(`line ${line.no}: port must be an integer 1-65535, got \`${kv.value}\``);
8049
+ } else {
8050
+ current.port = port;
8051
+ }
8052
+ continue;
8053
+ }
8054
+ if (value === "") {
8055
+ errors.push(`line ${line.no}: \`${kv.key}\` needs a value`);
8056
+ continue;
8057
+ }
8058
+ if (kv.key === "cwd" && (value.startsWith("/") || value.includes(".."))) {
8059
+ errors.push(`line ${line.no}: cwd must be a relative path under the drive root (no leading / and no ..)`);
8060
+ continue;
8061
+ }
8062
+ current[kv.key] = value;
8063
+ }
8064
+ finish(current);
8065
+ if (version === null)
8066
+ errors.push("missing `version: 1`");
8067
+ if (services.length === 0 && errors.length === 0)
8068
+ errors.push("`services:` lists no entries");
8069
+ if (services.length > SERVICES_MAX)
8070
+ errors.push(`too many services (${services.length}) — the cap is ${SERVICES_MAX}`);
8071
+ const seen = new Set;
8072
+ for (const svc of services) {
8073
+ const label = svc.name || "<unnamed>";
8074
+ if (!SERVICE_NAME_RE.test(svc.name)) {
8075
+ errors.push(`service \`${label}\`: name must match ${SERVICE_NAME_RE} (alphanumeric start, then [A-Za-z0-9._-], max 32)`);
8076
+ } else if (seen.has(svc.name)) {
8077
+ errors.push(`service \`${label}\`: duplicate name`);
8078
+ }
8079
+ seen.add(svc.name);
8080
+ if (!svc.command)
8081
+ errors.push(`service \`${label}\`: missing \`command\``);
8082
+ if (svc.health?.cmd && svc.health.http) {
8083
+ errors.push(`service \`${label}\`: health takes \`cmd\` OR \`http\`, not both`);
8084
+ }
8085
+ if (svc.health?.http && !/^https?:\/\//.test(svc.health.http)) {
8086
+ errors.push(`service \`${label}\`: health.http must be an http(s):// URL`);
8087
+ }
8088
+ }
8089
+ return errors.length > 0 ? { manifest: null, errors } : { manifest: { version: 1, services }, errors: [] };
8090
+ }
8091
+ function emitScalar(value) {
8092
+ const needsQuote = value !== value.trim() || value.startsWith("'") || value.startsWith('"') || value === "|" || value === ">" || value.startsWith("| ") || value.startsWith("> ");
8093
+ if (!needsQuote)
8094
+ return value;
8095
+ return `"${value.replace(/([\\"])/g, "\\$1")}"`;
8096
+ }
8097
+ function renderServicesManifest(manifest) {
8098
+ const out = [
8099
+ "# Durable services manifest — restored automatically after a machine recycle.",
8100
+ "# Docs: agentc services --help",
8101
+ "version: 1",
8102
+ "services:"
8103
+ ];
8104
+ for (const svc of manifest.services) {
8105
+ out.push(` - name: ${emitScalar(svc.name)}`);
8106
+ out.push(` command: ${emitScalar(svc.command)}`);
8107
+ if (svc.cwd)
8108
+ out.push(` cwd: ${emitScalar(svc.cwd)}`);
8109
+ if (svc.port !== undefined)
8110
+ out.push(` port: ${svc.port}`);
8111
+ if (svc.env)
8112
+ out.push(` env: ${emitScalar(svc.env)}`);
8113
+ if (svc.setup)
8114
+ out.push(` setup: ${emitScalar(svc.setup)}`);
8115
+ if (svc.health && (svc.health.cmd || svc.health.http)) {
8116
+ out.push(" health:");
8117
+ if (svc.health.cmd)
8118
+ out.push(` cmd: ${emitScalar(svc.health.cmd)}`);
8119
+ if (svc.health.http)
8120
+ out.push(` http: ${emitScalar(svc.health.http)}`);
8121
+ }
8122
+ if (svc.data && (svc.data.dump || svc.data.restore)) {
8123
+ out.push(" data:");
8124
+ if (svc.data.dump)
8125
+ out.push(` dump: ${emitScalar(svc.data.dump)}`);
8126
+ if (svc.data.restore)
8127
+ out.push(` restore: ${emitScalar(svc.data.restore)}`);
8128
+ }
8129
+ }
8130
+ return out.join(`
8131
+ `) + `
8132
+ `;
8133
+ }
8134
+ // src/agent/services-restore.ts
8135
+ var SERVICES_STATE_DIR = "/tmp/ac-services";
8136
+ var SERVICE_STATUS_PREFIX = "##ac-service";
8137
+ var SERVICE_HEALTH_TIMEOUT_S = 45;
8138
+ function shq(s) {
8139
+ return `'${s.replace(/'/g, `'\\''`)}'`;
8140
+ }
8141
+ function shc(cmd) {
8142
+ return `sh -c ${shq(cmd)}`;
8143
+ }
8144
+ function serviceCwd(driveRoot, svc) {
8145
+ return svc.cwd ? `${driveRoot.replace(/\/+$/, "")}/${svc.cwd}` : driveRoot;
8146
+ }
8147
+ function restoreOneService(driveRoot, svc) {
8148
+ const n = svc.name;
8149
+ const cwd = serviceCwd(driveRoot, svc);
8150
+ const lines = [
8151
+ ``,
8152
+ `# --- ${n} ---`,
8153
+ `(`,
8154
+ ` cd ${shq(cwd)} || { status ${shq(n)} failed cwd-missing; exit 0; }`
8155
+ ];
8156
+ if (svc.setup) {
8157
+ lines.push(` if ! ${shc(svc.setup)} >> "$D/${n}.setup.log" 2>&1; then`, ` status ${shq(n)} failed setup; exit 0`, ` fi`);
8158
+ }
8159
+ if (svc.data?.restore) {
8160
+ lines.push(` if ! ${shc(svc.data.restore)} >> "$D/${n}.setup.log" 2>&1; then`, ` status ${shq(n)} failed data-restore; exit 0`, ` fi`);
8161
+ }
8162
+ lines.push(` $SETSID ${shc(svc.command)} >> "$D/${n}.log" 2>&1 < /dev/null &`, ` echo $! > "$D/${n}.pid"`);
8163
+ const health = svc.health?.cmd ? shc(svc.health.cmd) : svc.health?.http ? `curl -fsS -m 2 -o /dev/null ${shq(svc.health.http)}` : null;
8164
+ if (health) {
8165
+ 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`);
8166
+ } else {
8167
+ 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`);
8168
+ }
8169
+ lines.push(`)`);
8170
+ return lines;
8171
+ }
8172
+ function servicesRestoreScript(manifest, driveRoot) {
8173
+ const head = [
8174
+ `#!/bin/sh`,
8175
+ `# generated by agent-compose — restores services from ${driveRoot}/.ac/services.yml`,
8176
+ `# logs + pidfiles: ${SERVICES_STATE_DIR}/<name>.{log,pid}`,
8177
+ `D=${shq(SERVICES_STATE_DIR)}`,
8178
+ `mkdir -p "$D"`,
8179
+ `status() { printf '%s %s %s %s\\n' ${shq(SERVICE_STATUS_PREFIX)} "$1" "$2" "$3"; }`,
8180
+ `# setsid detaches from the boot shell's session (Linux guests); on hosts`,
8181
+ `# without it (macOS running \`agentc services restore\`) plain & still works.`,
8182
+ `SETSID=""`,
8183
+ `command -v setsid >/dev/null 2>&1 && SETSID=setsid`
8184
+ ];
8185
+ const body = manifest.services.flatMap((svc) => restoreOneService(driveRoot, svc));
8186
+ return [...head, ...body, ``].join(`
8187
+ `);
8188
+ }
8189
+ function servicesDumpScript(manifest, driveRoot) {
8190
+ const withDump = manifest.services.filter((svc) => svc.data?.dump);
8191
+ if (withDump.length === 0)
8192
+ return null;
8193
+ const lines = [
8194
+ `#!/bin/sh`,
8195
+ `# generated by agent-compose — pre-recycle data dumps from .ac/services.yml`,
8196
+ `status() { printf '%s %s %s %s\\n' ${shq(SERVICE_STATUS_PREFIX)} "$1" "$2" "$3"; }`
8197
+ ];
8198
+ for (const svc of withDump) {
8199
+ const cwd = serviceCwd(driveRoot, svc);
8200
+ 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`, `)`);
8201
+ }
8202
+ lines.push(``);
8203
+ return lines.join(`
8204
+ `);
8205
+ }
8206
+ function parseServicesRestoreOutput(stdout) {
8207
+ const out = [];
8208
+ for (const raw of stdout.split(/\r?\n/)) {
8209
+ const line = raw.trim();
8210
+ if (!line.startsWith(`${SERVICE_STATUS_PREFIX} `))
8211
+ continue;
8212
+ const rest = line.slice(SERVICE_STATUS_PREFIX.length + 1).trim();
8213
+ const [name, status, ...detail] = rest.split(/\s+/);
8214
+ if (!name || !status)
8215
+ continue;
8216
+ if (!["ok", "unchecked", "failed", "dumped", "dump-failed"].includes(status))
8217
+ continue;
8218
+ const d = detail.join(" ").trim();
8219
+ out.push({ name, status, ...d ? { detail: d } : {} });
8220
+ }
8221
+ return out;
8222
+ }
8223
+ function formatServicesRestoreSummary(outcomes) {
8224
+ return outcomes.map((o) => {
8225
+ if (o.status === "ok" || o.status === "unchecked" || o.status === "dumped")
8226
+ return `${o.name} ✓`;
8227
+ const why = o.detail ? `${o.detail} — see ${SERVICES_STATE_DIR}/${o.name}.log` : `see ${SERVICES_STATE_DIR}/${o.name}.log`;
8228
+ return `${o.name} ✗ (${why})`;
8229
+ }).join(" ");
8230
+ }
6828
8231
  // src/display.ts
6829
8232
  var DISPLAY_MARKER_KEY = "acDisplay";
6830
8233
  var DISPLAY_MARKER_VERSION = "v1";
@@ -7018,6 +8421,7 @@ function clampAskOptions(raw) {
7018
8421
  }
7019
8422
  return options;
7020
8423
  }
8424
+ var DESKTOP_NOTE_MAX_CHARS = 200;
7021
8425
  function serializeDisplayMarker(marker) {
7022
8426
  return JSON.stringify({ [DISPLAY_MARKER_KEY]: DISPLAY_MARKER_VERSION, ...marker });
7023
8427
  }
@@ -7103,6 +8507,21 @@ function parseDisplayMarker(line) {
7103
8507
  ...yLabel !== undefined ? { yLabel } : {}
7104
8508
  };
7105
8509
  }
8510
+ case "desktop": {
8511
+ const path = str("path");
8512
+ if (!path || !isPlausibleDrivePath(path))
8513
+ return null;
8514
+ const capturedAt = str("capturedAt");
8515
+ const capturedOk = capturedAt !== undefined && Number.isFinite(Date.parse(capturedAt));
8516
+ const note = str("note");
8517
+ return {
8518
+ kind: "desktop",
8519
+ path,
8520
+ ...str("factorySlug") ? { factorySlug: str("factorySlug") } : {},
8521
+ ...capturedOk ? { capturedAt } : {},
8522
+ ...note !== undefined ? { note: note.slice(0, DESKTOP_NOTE_MAX_CHARS) } : {}
8523
+ };
8524
+ }
7106
8525
  case "ask": {
7107
8526
  const askId = str("askId");
7108
8527
  const rawPrompt = str("prompt");
@@ -7185,7 +8604,8 @@ var VALUE_FLAGS = new Set([
7185
8604
  "--to",
7186
8605
  "--prompt",
7187
8606
  "--options",
7188
- "--approver"
8607
+ "--approver",
8608
+ "--note"
7189
8609
  ]);
7190
8610
  function isShellControlWord(w) {
7191
8611
  if (/^\d*>&\d+$/.test(w))
@@ -7247,6 +8667,8 @@ function detectAgentcInvocation(command) {
7247
8667
  const path = drivePath(args[2]);
7248
8668
  return path ? { verb: "display-image", path, ...withSlug } : null;
7249
8669
  }
8670
+ case "desktop":
8671
+ return { verb: "display-desktop" };
7250
8672
  case "preview": {
7251
8673
  const path = drivePath(args[2]);
7252
8674
  return path ? { verb: "display-preview", path } : null;
@@ -7400,6 +8822,8 @@ function createDisplayPromoter(ctx = {}) {
7400
8822
  return call("display_plan", {});
7401
8823
  case "display-image":
7402
8824
  return call("display_image", { path: invocation.path, ...factoryRef(invocation.factorySlug) });
8825
+ case "display-desktop":
8826
+ return call("display_desktop", {});
7403
8827
  case "display-preview":
7404
8828
  return call("display_preview", { path: invocation.path, ...factoryRef(undefined) });
7405
8829
  case "display-table":
@@ -7493,6 +8917,17 @@ function createDisplayPromoter(ctx = {}) {
7493
8917
  ...factoryRef(m?.factorySlug ?? inv.factorySlug)
7494
8918
  })];
7495
8919
  }
8920
+ case "display-desktop": {
8921
+ const marker = findDisplayMarker(text);
8922
+ if (marker?.kind !== "desktop")
8923
+ return [result(msg.output)];
8924
+ return [result({
8925
+ path: marker.path,
8926
+ ...factoryRef(marker.factorySlug),
8927
+ capturedAt: marker.capturedAt ?? null,
8928
+ note: marker.note ?? null
8929
+ })];
8930
+ }
7496
8931
  case "display-table": {
7497
8932
  if (inv.source === "file")
7498
8933
  return [result(msg.output)];
@@ -7541,27 +8976,48 @@ function createDisplayPromoter(ctx = {}) {
7541
8976
  export {
7542
8977
  writeTool,
7543
8978
  writeAgentContext,
8979
+ uniquePromptPath,
8980
+ turnLivenessProbeCommand,
8981
+ streamInputFeederFragment,
7544
8982
  stepInputPath,
8983
+ standalonePerfProbeCommand,
7545
8984
  snapshotResolves,
7546
8985
  shellWords,
8986
+ shellQuote,
8987
+ sessionEnvSourceFragment,
8988
+ servicesRestoreScript,
8989
+ servicesDumpScript,
7547
8990
  serveStep,
7548
8991
  serializeDisplayMarker,
8992
+ sandboxSizeLabel,
8993
+ runnerBusySentinelFragment,
7549
8994
  runWorkflowSteps,
7550
8995
  runWorkflowSingleStep,
7551
8996
  runWorkflow,
7552
8997
  runProcessorChain,
7553
8998
  resolveSdkAlias,
8999
+ residentResultCountCommand,
9000
+ residentIdleWatchdogFragment,
9001
+ residentIdleCheckFragment,
9002
+ residentFeederFragment,
7554
9003
  requireScope,
7555
9004
  requestPauseAndAwait,
7556
9005
  requestContextPath,
9006
+ renderServicesManifest,
7557
9007
  renderDirectiveMarker,
7558
9008
  redactPattern,
7559
9009
  reconnectStep,
7560
9010
  reconnectSandbox,
7561
9011
  readTool,
9012
+ perfSamplerFunctionFragment,
9013
+ parseTurnProbeOutput,
7562
9014
  parseStepResult,
7563
9015
  parseSseStream,
7564
9016
  parseSseExecStream,
9017
+ parseServicesRestoreOutput,
9018
+ parseServicesManifest,
9019
+ parsePerfToken,
9020
+ parsePerfProbeOutput,
7565
9021
  parseNameVersion,
7566
9022
  parseDisplayMarker,
7567
9023
  parseDirectiveMarkers,
@@ -7573,22 +9029,29 @@ export {
7573
9029
  normalizeConversationStreamEvent,
7574
9030
  makeSandboxProvider,
7575
9031
  makeDesktopSandboxProvider,
9032
+ livenessVerdictFromReading,
7576
9033
  listVercelRuntimeModels,
7577
9034
  listOwnedSandboxes,
7578
9035
  launchStep,
7579
9036
  killSandboxById,
7580
9037
  killAllSandboxes,
7581
9038
  isWorkflow,
9039
+ isVercelSupportedSize,
7582
9040
  isPlausibleDrivePath,
7583
9041
  isPlatformE2bTemplateAlias,
7584
9042
  isE2bSupportedSize,
7585
9043
  invokeStep,
9044
+ installDesktopOpenCmd,
9045
+ installChromiumDefaultsCmd,
9046
+ injectAppendAndAckCommand,
7586
9047
  importSourceModule,
7587
9048
  humanApproval,
7588
9049
  getSandboxQuotas,
9050
+ formatServicesRestoreSummary,
7589
9051
  formatError,
7590
9052
  findDisplayMarker,
7591
9053
  explainBundleFailure,
9054
+ exitEventPushCommand,
7592
9055
  editTool,
7593
9056
  e2bMachineSpec,
7594
9057
  e2bDevboxTemplateRef,
@@ -7603,8 +9066,10 @@ export {
7603
9066
  defineStep,
7604
9067
  defineSandboxEnvironment,
7605
9068
  defineRuntime,
9069
+ deferredExitEventPushCommand,
7606
9070
  cursorSpec,
7607
9071
  cursor_default as cursorRuntime,
9072
+ credEnvSourceFragment,
7608
9073
  createVercelRuntime,
7609
9074
  createSandbox,
7610
9075
  createOpencodeRuntime,
@@ -7634,6 +9099,8 @@ export {
7634
9099
  buildInvokeChild,
7635
9100
  buildAgentContextDoc,
7636
9101
  buildAddedSessionBrief,
9102
+ browserBackfillStatusCmd,
9103
+ browserBackfillCmd,
7637
9104
  bashTool,
7638
9105
  assertDefaultExportIsDefineWorkflow,
7639
9106
  amp_default as ampRuntime,
@@ -7645,6 +9112,8 @@ export {
7645
9112
  WorkflowError,
7646
9113
  Verdict,
7647
9114
  VercelRunner,
9115
+ VERCEL_MEMORY_MB_PER_VCPU,
9116
+ TURN_PROBE_TIMEOUT_MS,
7648
9117
  TMP_DIR,
7649
9118
  TABLE_MAX_ROWS,
7650
9119
  TABLE_MAX_COLUMNS,
@@ -7655,26 +9124,49 @@ export {
7655
9124
  STEP_RESULT_PREFIX,
7656
9125
  STEP_PAUSE_PREFIX,
7657
9126
  STEP_ENV,
9127
+ SESSION_ENV_FILE_RELPATH,
9128
+ SESSION_DEFAULT_SANDBOX_SIZE,
9129
+ SERVICE_STATUS_PREFIX,
9130
+ SERVICE_NAME_RE,
9131
+ SERVICE_HEALTH_TIMEOUT_S,
9132
+ SERVICES_STATE_DIR,
9133
+ SERVICES_MAX,
9134
+ SERVICES_MANIFEST_RELPATH,
7658
9135
  SDK_SPECIFIER_ALIASES,
7659
9136
  SDK_PACKAGE,
7660
9137
  SANDBOX_VCPUS,
7661
9138
  SANDBOX_UNAVAILABLE_PREFIX,
9139
+ SANDBOX_SIZES,
9140
+ SANDBOX_MACHINES,
7662
9141
  ReservedKeyError,
7663
9142
  RequestContext,
9143
+ RUNNER_HEARTBEAT_STALE_MS,
9144
+ RUNNER_HEARTBEAT_INTERVAL_SECONDS,
7664
9145
  RUNNER_COMMAND,
7665
9146
  RUNNER_BUNDLE_PATH,
7666
9147
  REVISION_SELECTOR_RE,
9148
+ RESIDENT_WATCHDOG_POLL_S,
9149
+ RESIDENT_RESULT_LINE_PREFIX,
9150
+ RESIDENT_IDLE_TTL_S,
7667
9151
  PauseSchemaError,
7668
9152
  PauseRequestError,
7669
9153
  PauseExpiredError,
7670
9154
  PauseError,
7671
9155
  PLAN_MAX_ENTRIES,
7672
9156
  PLAN_ENTRY_MAX_CHARS,
9157
+ PERF_TOKEN_MAX_CHARS,
9158
+ PERF_SAMPLE_EVERY_BEATS,
9159
+ PERF_PROBE_WINDOW_SECONDS,
7673
9160
  NonSerialisableValueError,
7674
9161
  MAX_DIRECTIVES_PER_OUTPUT,
9162
+ LAUNCH_EXEC_TIMEOUT_MS,
7675
9163
  LATEST_VERSION,
9164
+ INSTALL_PROBE_TIMEOUT_MS,
9165
+ INJECT_ACK_EXEC_TIMEOUT_MS,
7676
9166
  EngineError,
7677
9167
  E2B_TEMPLATE_SIZES,
9168
+ E2B_MAX_VCPUS,
9169
+ E2B_MAX_MEMORY_MB,
7678
9170
  E2B_DEVBOX_TEMPLATE,
7679
9171
  E2B_DEVBOX_SPEC,
7680
9172
  E2B_DEVBOX_RECIPE_VERSION,
@@ -7683,15 +9175,23 @@ export {
7683
9175
  DISPLAY_MARKER_KEY,
7684
9176
  DIRECTIVE_MARKER_SUFFIX,
7685
9177
  DIRECTIVE_MARKER_PREFIX,
9178
+ DESKTOP_NOTE_MAX_CHARS,
7686
9179
  DEFAULT_SANDBOX_SIZE,
7687
9180
  DEFAULT_CLAUDE_MODEL,
7688
9181
  ClaudeRunner,
9182
+ CLI_INSTALL_TIMEOUT_MS,
7689
9183
  CLAUDE_CODE_EFFORT_LEVELS,
7690
9184
  CLAUDE_CODE_ACP_ADAPTER,
9185
+ CHROMIUM_DEFAULTS_PATH,
9186
+ CHROMIUM_DEFAULTS,
7691
9187
  CHART_MAX_SERIES,
7692
9188
  CHART_MAX_POINTS_PER_SERIES,
7693
9189
  CHART_LABEL_MAX_CHARS,
7694
9190
  BUNDLER_VERSION,
9191
+ BROWSER_INSTALL_IN_PROGRESS_MSG,
9192
+ BROWSER_BACKFILL_PIDFILE,
9193
+ BROWSER_BACKFILL_LOG,
9194
+ BROWSER_BACKFILL_FAILED,
7695
9195
  AgentStatusSchema,
7696
9196
  AgentMessageSchema,
7697
9197
  AgentComposeError,
@@ -7715,6 +9215,11 @@ export {
7715
9215
  AC_RUN_ID,
7716
9216
  AC_RESERVED_PREFIX,
7717
9217
  AC_PARENT_RUN_ID,
9218
+ AC_OPEN_STATE_DIR,
9219
+ AC_OPEN_SHIM_PATHS,
9220
+ AC_OPEN_SHIM,
9221
+ AC_OPEN_SCRIPT,
9222
+ AC_OPEN_PATH,
7718
9223
  AC_FACTORY_ID,
7719
9224
  AC_API_KEY_SCOPES,
7720
9225
  AC_ABORT_SIGNAL,