@livx.cc/agentx 0.99.48 → 0.99.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -92,7 +92,7 @@ Beyond file tools, the runtime ships the higher-altitude pieces too — each an
92
92
  - **DuplexAgent** (`src/duplex.ts`) — voice-optimized three-tier engine (reflex/act/think): a fast reflex agent streams instant replies and self-selects escalation — `Act` for standard tool work (Sonnet-class), `Think` for deep reasoning (Opus-class, configurable, default on). Results are pushed back and re-voiced by the reflex (turn mutex, coalesced completions, `TaskStatus`/`CancelTask`). See [`mind/10`](./mind/10-duplex.md).
93
93
  - **Scheduler** (`src/scheduler.ts` + `cli/osScheduler.ts`) — one-off (`{at}`), interval (`{everyMs}`), cron (`{cron}`) via `ScheduleTask`/`ScheduleList`/`ScheduleCancel`/`Wakeup`. In-session jobs fire while the session is alive (persisted, re-armed on `--resume`); far one-offs (or `backend:'os'`) register with the OS scheduler (launchd / crontab / at) and **survive quitting** — the fired job headless-resumes the session (`agentx -p … --resume <id> --yes`). The `PushNotification` tool (osascript / notify-send) alerts the user out-of-band; `Read` on a `.pdf` returns extracted text (poppler's pdftotext, disk mode). **`RemoteTrigger`** invokes another agentx session on this machine: a session open in a live terminal receives the prompt as an injected turn (per-session unix socket, same-user only); otherwise it's resumed headless and the final answer comes back. See [`mind/12`](./mind/12-scheduler.md).
94
94
  - **Budget kill-switches** — always-on per-run guards (`maxTokens`/`timeoutMs`/`maxRepeats`/`maxToolCalls`/`signal` → `finishReason` `budget`/`timeout`/`loop`/`max_tool_calls`/`aborted`) protect the API spend against runaway loops. The *enforceable* billing cap is server-side in the web key-proxy: a VFS-backed budget config (`/.agent/budget.json`, USD-metered, hot-reloaded, $100/wk default) a browser client can't bypass. See [`web/`](./web) and [`mind/06`](./mind/06-agent-features.md).
95
- - **Jobs & liveness (never kill a working tool)** — every long-lived tool call is a job in `agent.jobs` (`JobRegistry`: id, kind `shell|mcp|host|native|bash|task`, status, `startedAt`, `lastActivityAt`, output tail, result). The model follows jobs with `JobStatus` / `JobOutput` (tail or `offset`) / `JobWait` (bounded — use instead of sleeping) / `JobKill`, and a job that finishes after its caller stopped waiting is **reported back** (re-opening a stopped turn). Liveness comes from output chunks, MCP `notifications/progress`, delegated tool events, and (for adopted shell jobs) process-tree CPU time via `ps` (group + descendants that left it). A job is killed only when it is **stuck** — silent past its kind's idle window with no CPU progress (`shell`: 10 min; unknown CPU never counts as idle) — for `mcp` only after the server has sent progress and then stopped, since most servers never report progress — or past its hard cap (`mcp`: 30 min; delegated host/native tools: the transport tool deadline) — and the model sees `killed: stuck: idle 600s`; the run continues, and every kill is logged with its evidence. Explicit `Shell({background:true})` jobs are never idle-reaped (a quiet server is healthy). The model **stall watchdog** (`stallMs`, 60s of stream silence) only fires when no job is holding the stream; a detached job does not mask a silent model, and a step whose tools already ran is never replayed. Knobs: `new Agent({ jobs: new JobRegistry({ idleMs, hardCapMs, foregroundMs, sweepMs, killGraceMs }) })`; share the same registry with `new ShellJobRegistry({ jobs })` (the CLI and `fullAgentOptions` do).
95
+ - **Jobs & liveness (never kill a working tool)** — every long-lived tool call is a job in `agent.jobs` (`JobRegistry`: id, kind `shell|mcp|host|native|bash|task`, status, `startedAt`, `lastActivityAt`, output tail, result). The model follows jobs with `JobStatus` / `JobOutput` (tail or `offset`) / `JobWait` (bounded — use instead of sleeping) / `JobKill`, and a job that finishes after its caller stopped waiting is **reported back** (re-opening a stopped turn). Liveness comes from output chunks, MCP `notifications/progress`, delegated tool events, and (for adopted shell jobs) process-tree CPU time via `ps` (group + descendants that left it). A job is killed only when it is **stuck** — silent past its kind's idle window with no CPU progress (`shell`: 10 min; unknown CPU never counts as idle) — for `mcp` only after the server has sent progress and then stopped, since most servers never report progress — or past its hard cap (`mcp`: 30 min; delegated host/native tools: the transport tool deadline). A delegated **`native`** tool (cursor's own shell/mcp/task) additionally forfeits its hold after `stallMs`-scale silence (`AgentOptions.nativeHoldMs`, 5 min): unlike a `host` job — whose pending dispatch promise is independent proof it is alive — a native job is only the provider's own claim, and it suppresses the very stall detection that would catch that provider going quiet. Further `running` events `touch()` it, so a native tool that keeps reporting progress holds indefinitely — and the model sees `killed: stuck: idle 600s`; the run continues, and every kill is logged with its evidence. Explicit `Shell({background:true})` jobs are never idle-reaped (a quiet server is healthy). The model **stall watchdog** (`stallMs`, 60s of stream silence) only fires when no job is holding the stream; a detached job does not mask a silent model, and a step whose tools already ran is never replayed. A hold is never silent — every suppressed window logs and notifies `waiting on <kind> "<label>" (<age>, quiet <n>s)`, so a wedged tool is visible in seconds instead of stalling the run unseen. Knobs: `new Agent({ jobs: new JobRegistry({ idleMs, hardCapMs, foregroundMs, sweepMs, killGraceMs }) })`; share the same registry with `new ShellJobRegistry({ jobs })` (the CLI and `fullAgentOptions` do).
96
96
 
97
97
  ## The `agentx` CLI
98
98
 
@@ -1,5 +1,5 @@
1
1
  import { IFilesystem } from '@livx.cc/wcli/core';
2
- import { M as Message, H as HostBridge, A as AgentTool, C as ChatLike, k as JobRegistry, o as MessageContent, U as UserQuestion } from './tools-MNdqlJIa.js';
2
+ import { M as Message, H as HostBridge, A as AgentTool, C as ChatLike, k as JobRegistry, o as MessageContent, U as UserQuestion } from './tools-HsbxgqjF.js';
3
3
 
4
4
  /**
5
5
  * Hooks — deterministic interception points around tool execution, run by the
@@ -244,6 +244,15 @@ declare class AgentOptions {
244
244
  * arrives for this long. The between-steps `timeoutMs` can't preempt an in-flight `chat()`, so a
245
245
  * provider that goes silent mid-stream would otherwise park the loop forever. 0 = off. */
246
246
  stallMs: number;
247
+ /** IDLE bound on a NATIVE delegated tool's hold over the stall watchdog (cursor's own shell/mcp/task:
248
+ * `running` → silence → `completed`). Such a hold is the provider's own unverified CLAIM that it is
249
+ * busy, and it SUSPENDS the very stall detection that would catch that provider going silent — so its
250
+ * only backstop was `hardCapMs = toolHoldMs`, the TRANSPORT deadline (≥30m, sized for host-tool
251
+ * round-trips a native call never makes). A wedged native call therefore bought 30m of total silence,
252
+ * ×2 attempts = the 61-minute zero-output run (blank 2026-09-18). Idle, not absolute: every further
253
+ * `running` event `touch()`es the job, so a native tool that keeps reporting progress holds
254
+ * indefinitely — what expires is silence, not work. 0 = off (hard cap only). */
255
+ nativeHoldMs: number;
247
256
  /** Stop if the identical tool-call batch (name+args) repeats this many times in a row. 0 = off. */
248
257
  maxRepeats: number;
249
258
  /** Cumulative cap on tool calls dispatched across the run. 0 = unbounded. */
@@ -431,11 +440,14 @@ declare class Agent {
431
440
  * toolExecutor) and NATIVE tools (cursor's own shell/mcp/task, `running` → `completed`) are tracked as
432
441
  * `holds` jobs: while one runs the provider is not silent — it is waiting on a tool — so the idle-stall
433
442
  * watchdog must not fire. Each is bounded by the transport tool deadline (`hardCapMs = toolHoldMs`), so a
434
- * tool that never settles (or a lost `completed` event) is reaped and cannot disable stall protection. */
443
+ * tool that never settles (or a lost `completed` event) is reaped and cannot disable stall protection.
444
+ * A NATIVE job carries a second, much tighter bound (`idleMs = nativeHoldMs`): the transport deadline is
445
+ * sized for a host-tool round-trip it never makes, and silence is the only thing it can actually prove. */
435
446
  readonly jobs: JobRegistry;
436
447
  /** Native delegated tool activity id → its job (per step attempt). */
437
448
  private nativeJobs;
438
449
  private toolHoldMs;
450
+ private nativeHoldMs;
439
451
  /** A tool (host or native) already executed in the current step attempt: a stall retry replays the step
440
452
  * from the pre-step transcript, which would re-run it — so such a stall is not retried. */
441
453
  private toolRanThisAttempt;
package/dist/cli.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bun
2
- import { H as Hooks, i as RunResult, R as ReasoningEffort, A as Agent } from './Agent-BeoEFl2T.js';
2
+ import { H as Hooks, i as RunResult, R as ReasoningEffort, A as Agent } from './Agent-CXkPfYX2.js';
3
3
  import { IFilesystem } from '@livx.cc/wcli/core';
4
- import { M as Message, U as UserQuestion, H as HostBridge, c as ContentPart, o as MessageContent } from './tools-MNdqlJIa.js';
4
+ import { M as Message, U as UserQuestion, H as HostBridge, c as ContentPart, o as MessageContent } from './tools-HsbxgqjF.js';
5
5
 
6
6
  /**
7
7
  * On-disk session store for the CLI: each conversation is one JSON file at
package/dist/cli.js CHANGED
@@ -1577,7 +1577,12 @@ var init_tools_jobs = __esm({
1577
1577
  maxBuffer = 256 * 1024;
1578
1578
  /** Idle window per kind: no output/progress/CPU for this long ⇒ stuck. Kinds without an activity channel
1579
1579
  * (host, native) are absent on purpose — silence is all they can ever show, so it proves nothing; their
1580
- * bound is the hard cap. Explicit `background:true` shell jobs opt out per job (a quiet server is healthy). */
1580
+ * bound is the hard cap. Explicit `background:true` shell jobs opt out per job (a quiet server is healthy).
1581
+ * NOTE the asymmetry that default hides: for a HOST job silence really is uninformative (the caller holds
1582
+ * the pending dispatch promise — independent proof it is alive), but a NATIVE job has no such proof AND it
1583
+ * suppresses the stall watchdog, i.e. it turns the provider's own silence into permission to keep ignoring
1584
+ * that provider's silence. So Agent passes an explicit per-job `idleMs` (nativeHoldMs) for `native`: the
1585
+ * provider must keep re-proving liveness with `running` events or forfeit the hold. See Agent.nativeHoldMs. */
1581
1586
  idleMs = { shell: 10 * 6e4, mcp: 10 * 6e4 };
1582
1587
  /** Absolute ceiling per kind (0/absent = none). */
1583
1588
  hardCapMs = { mcp: 30 * 6e4 };
@@ -4180,6 +4185,15 @@ var AgentOptions = class {
4180
4185
  * arrives for this long. The between-steps `timeoutMs` can't preempt an in-flight `chat()`, so a
4181
4186
  * provider that goes silent mid-stream would otherwise park the loop forever. 0 = off. */
4182
4187
  stallMs = 6e4;
4188
+ /** IDLE bound on a NATIVE delegated tool's hold over the stall watchdog (cursor's own shell/mcp/task:
4189
+ * `running` → silence → `completed`). Such a hold is the provider's own unverified CLAIM that it is
4190
+ * busy, and it SUSPENDS the very stall detection that would catch that provider going silent — so its
4191
+ * only backstop was `hardCapMs = toolHoldMs`, the TRANSPORT deadline (≥30m, sized for host-tool
4192
+ * round-trips a native call never makes). A wedged native call therefore bought 30m of total silence,
4193
+ * ×2 attempts = the 61-minute zero-output run (blank 2026-09-18). Idle, not absolute: every further
4194
+ * `running` event `touch()`es the job, so a native tool that keeps reporting progress holds
4195
+ * indefinitely — what expires is silence, not work. 0 = off (hard cap only). */
4196
+ nativeHoldMs = 3e5;
4183
4197
  /** Stop if the identical tool-call batch (name+args) repeats this many times in a row. 0 = off. */
4184
4198
  maxRepeats = 3;
4185
4199
  /** Cumulative cap on tool calls dispatched across the run. 0 = unbounded. */
@@ -4371,11 +4385,14 @@ var Agent = class _Agent {
4371
4385
  * toolExecutor) and NATIVE tools (cursor's own shell/mcp/task, `running` → `completed`) are tracked as
4372
4386
  * `holds` jobs: while one runs the provider is not silent — it is waiting on a tool — so the idle-stall
4373
4387
  * watchdog must not fire. Each is bounded by the transport tool deadline (`hardCapMs = toolHoldMs`), so a
4374
- * tool that never settles (or a lost `completed` event) is reaped and cannot disable stall protection. */
4388
+ * tool that never settles (or a lost `completed` event) is reaped and cannot disable stall protection.
4389
+ * A NATIVE job carries a second, much tighter bound (`idleMs = nativeHoldMs`): the transport deadline is
4390
+ * sized for a host-tool round-trip it never makes, and silence is the only thing it can actually prove. */
4375
4391
  jobs;
4376
4392
  /** Native delegated tool activity id → its job (per step attempt). */
4377
4393
  nativeJobs = /* @__PURE__ */ new Map();
4378
4394
  toolHoldMs = 0;
4395
+ nativeHoldMs = 0;
4379
4396
  /** A tool (host or native) already executed in the current step attempt: a stall retry replays the step
4380
4397
  * from the pre-step transcript, which would re-run it — so such a stall is not retried. */
4381
4398
  toolRanThisAttempt = false;
@@ -4717,6 +4734,7 @@ var Agent = class _Agent {
4717
4734
  const derivedToolTimeoutMs = Math.max(TRANSPORT_TOOL_TIMEOUT_FLOOR_MS, ...this.activeTools.map(transportHeadroomMs));
4718
4735
  const effectiveToolTimeoutMs = typeof hostToolTimeoutMs === "number" ? Math.max(hostToolTimeoutMs, derivedToolTimeoutMs) : derivedToolTimeoutMs;
4719
4736
  this.toolHoldMs = effectiveToolTimeoutMs;
4737
+ this.nativeHoldMs = o.nativeHoldMs;
4720
4738
  if (isCursorWithTools) {
4721
4739
  if (typeof hostToolTimeoutMs === "number" && effectiveToolTimeoutMs !== hostToolTimeoutMs) {
4722
4740
  log7.warn(`providerOptions.toolTimeoutMs=${hostToolTimeoutMs}ms is below what the registered tools need \u2014 RAISED to ${effectiveToolTimeoutMs}ms. ${toolTimeoutViolations(this.activeTools, hostToolTimeoutMs).length} tool(s) could outlive it, and the transport would have destroyed their results. To lower it, lower the tools' own maxDurationMs (e.g. the Shell tool's maxTimeoutMs) instead.`);
@@ -4970,7 +4988,13 @@ var Agent = class _Agent {
4970
4988
  if (!stallMs) return;
4971
4989
  if (timer) clearTimeout(timer);
4972
4990
  timer = setTimeout(() => {
4973
- if (this.toolHoldActive()) return poke();
4991
+ if (this.toolHoldActive()) {
4992
+ const now4 = Date.now();
4993
+ const who = this.jobs.holding().map((j) => `${j.kind} "${j.label}" (${Math.round((now4 - j.startedAt) / 1e3)}s, quiet ${Math.round((now4 - j.lastActivityAt) / 1e3)}s)`).join(", ");
4994
+ log7.warn(`stall watchdog: no stream output for ${stallMs}ms \u2014 held by ${who}`);
4995
+ this.options.host?.notify?.({ kind: "tool_hold", message: `waiting on ${who}` });
4996
+ return poke();
4997
+ }
4974
4998
  const inFlight = this.jobs.list().filter((j) => j.status === "running");
4975
4999
  log7.warn(`stall watchdog: no stream output for ${stallMs}ms and no tool holding the stream \u2014 aborting the request` + (inFlight.length ? ` (${inFlight.length} detached job(s) running, not awaited by the stream: ${inFlight.map((j) => `${j.id}/${j.kind}`).join(", ")})` : ""));
4976
5000
  fired = true;
@@ -5039,7 +5063,7 @@ var Agent = class _Agent {
5039
5063
  const nj = a.id ? this.nativeJobs.get(a.id) : void 0;
5040
5064
  if (a.status === "running") {
5041
5065
  if (nj) nj.touch();
5042
- else if (a.id) this.nativeJobs.set(a.id, this.jobs.track({ kind: "native", label: a.name, holds: true, hardCapMs: this.toolHoldMs }));
5066
+ else if (a.id) this.nativeJobs.set(a.id, this.jobs.track({ kind: "native", label: a.name, holds: true, idleMs: this.nativeHoldMs, hardCapMs: this.toolHoldMs }));
5043
5067
  } else {
5044
5068
  if (nj) {
5045
5069
  if (typeof a.output === "string") nj.chunk(a.output.slice(-4e3));