agentradar-cli 0.3.20 → 0.3.22

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.
@@ -30,7 +30,7 @@ Your primary objective is to reach your quotas and drive the company forward in
30
30
 
31
31
  ## Talking to humans
32
32
 
33
- Write to humans so a 13-year-old would understand: a few short plain lines, then one next step, not a list of ideas. No ids, paths, tool names, task codes or internal terms unless asked (say "your team", "company memory"; name a plan with its monthly price). Say money in dollars, never cents; give every link with what it opens. Answer only what was asked, and say it once. Talk to the event's `sender` as "you", never by name or as "the owner"; no persona from another chat carries over. Keep to what a human told you all chat, say so before a step they may not expect (a web search, sending, hiring), and never offer to contact anyone unless asked. Never talk about how messages reach you. Do the obvious thing for "undo that"; if unclear, offer short choices. Check your tools before saying what is scheduled or done, never from memory.
33
+ Write to humans so a 13-year-old would understand: a few short lines, then one next step, not a list of ideas. No ids, paths, tool names, task codes or internal terms unless asked (say "your team", "company memory"; name a plan with its monthly price). Say "human" for a human. Say money in dollars, never cents; give every link with what it opens. Answer only what was asked, and say it once. Talk to the event's `sender` as "you", never by name or as "the owner"; no persona from another chat carries over. Keep to what a human told you all chat, say so before a step they may not expect (a web search, sending, hiring), and never offer to contact anyone unless asked. Never talk about how messages reach you. Do the obvious thing for "undo that"; if unclear, offer short choices. Check your tools before saying what is scheduled or done, never from memory.
34
34
 
35
35
  ## Work loop
36
36
 
package/dist/channel.js CHANGED
@@ -122,6 +122,7 @@ export async function startChannel(runnerId, deps = {}) {
122
122
  // Vadym 10342: an unconfirmed channel is reported to AgentRadar (runnerState channel_unconfirmed), so the owner sees
123
123
  // the agent is not receiving messages; "ready" again once the session does run `inbox next`.
124
124
  let unconfirmed = null;
125
+ let reportedAt = 0;
125
126
  let warning;
126
127
  let skillSync;
127
128
  let teammateNotices;
@@ -203,13 +204,17 @@ export async function startChannel(runnerId, deps = {}) {
203
204
  if (unconfirmed === "pending") {
204
205
  await next({ runnerState: "channel_unconfirmed" });
205
206
  unconfirmed = "reported";
207
+ reportedAt = Date.now();
206
208
  }
207
209
  if (unconfirmed === "reported" && polled())
208
210
  unconfirmed = null;
209
211
  // While reported, no runnerState: the stored state stands, and the poll still drains for wake signals.
210
212
  // Receipt only: a preview still needs read_message before any reply (server gate). A "starting" poll cannot ack.
211
213
  const ack = guarded && unacked.length && calledSince(lastSent) ? unacked : null;
212
- const response = await next({ ...(unconfirmed ? {} : { runnerState: guarded ? "ready" : "starting" }), ...(ack ? { ackEventIds: ack } : {}) });
214
+ // While reported, a short hold: the session's first `inbox next` clears the flag within seconds, not after the 200 s empty-queue hold.
215
+ // After 10 min still unconfirmed, back to the full hold (the session is likely never confirming; ~450 polls/h otherwise).
216
+ const fast = unconfirmed && Date.now() - reportedAt < (deps.unconfirmedFastMs ?? 600_000);
217
+ const response = await next({ ...(unconfirmed ? (fast ? { waitMs: 5_000 } : {}) : { runnerState: guarded ? "ready" : "starting" }), ...(ack ? { ackEventIds: ack } : {}) });
213
218
  if (response.error || response.result?.isError)
214
219
  throw new Error("AgentRadar refused `agentradar run $AR_CLI_RUNNER_ID inbox next`; check the runner key and its poll permission");
215
220
  const content = response.result?.content;
package/dist/runner.js CHANGED
@@ -450,8 +450,22 @@ export async function startVendorRunner(runnerId, home = homedir(), deps = {}) {
450
450
  return false;
451
451
  };
452
452
  const shared = spawnChild("bash", [supervisor, envPath]);
453
- shared.once("error", (error) => { stop(); reject(error); });
453
+ // RUNNER-KILL-LEAVES-FEEDER: a `kill` reaches only this process, so pass it on to the supervisors, whose EXIT
454
+ // trap stops their feeders. Otherwise they outlive the runner and a restarted one delivers every event twice.
455
+ // SIGINT too: `kill -INT <pid>` reaches only the runner (Ctrl-C reaches the whole group anyway). SIGHUP (the
456
+ // vendor closes the terminal) only when a terminal runs it: a listener would undo a nohup start's ignored SIGHUP.
457
+ // The runner exits once the shared supervisor has; a second signal kills it at once.
458
+ const signals = deps.signals ?? process;
459
+ const names = (deps.hangup ?? process.stdout.isTTY === true)
460
+ ? ["SIGTERM", "SIGINT", "SIGHUP"] : ["SIGTERM", "SIGINT"];
461
+ const unlisten = () => { for (const name of names)
462
+ signals.off(name, forward); };
463
+ const forward = () => { unlisten(); stop(); shared.kill("SIGTERM"); };
464
+ for (const name of names)
465
+ signals.once(name, forward);
466
+ shared.once("error", (error) => { unlisten(); stop(); reject(error); });
454
467
  shared.once("exit", (code, signal) => {
468
+ unlisten();
455
469
  stop();
456
470
  if (!updating)
457
471
  return resolvePromise(code ?? (signal ? 1 : 0));
package/dist/tool-run.js CHANGED
@@ -19,6 +19,7 @@ export function envValue(contents, key) {
19
19
  const quoted = raw.startsWith("'") && raw.endsWith("'") && raw.length >= 2;
20
20
  return (quoted ? raw.slice(1, -1).replaceAll("'\\''", "'") : raw) || null;
21
21
  }
22
+ const POSITIONAL_SUBJECT = { "feedback status": "feedback-id" };
22
23
  /**
23
24
  * #1871 slice 1: `agentradar <noun> [<sub-noun>] <verb> [--<argument> <value> ...]`, every path read from the server's
24
25
  * tools/list (`_meta.cli.path`), so a new tool or a renamed path needs no CLI release. `command` is that path, or a
@@ -28,6 +29,12 @@ export function envValue(contents, key) {
28
29
  export async function callToolOverCli(runnerId, command, argumentsJson, deps = {}, flags = []) {
29
30
  const switches = new Set(flags.filter(([flag]) => ["help", "dry-run", "json"].includes(flag)).map(([flag]) => flag));
30
31
  flags = flags.filter(([flag]) => !switches.has(flag));
32
+ // FEEDBACK-OUTCOME-NOTIFY-19720: the notice teaches `feedback status <ticket id>`; the one subject goes in as the flag it fills.
33
+ const subject = POSITIONAL_SUBJECT[command.slice(0, command.lastIndexOf(" "))];
34
+ if (subject) {
35
+ flags = [[subject, command.slice(command.lastIndexOf(" ") + 1)], ...flags];
36
+ command = command.slice(0, command.lastIndexOf(" "));
37
+ }
31
38
  // Every request of a --json run, tools/list included, keeps the upgrade line off stderr.
32
39
  if (switches.has("json"))
33
40
  deps = { ...deps, json: true };
@@ -240,7 +247,7 @@ const serviceTools = (runnerId, tools) => {
240
247
  /** Top-level `--help` headings (main 10-15: ~6 sections so 37 groups scan). */
241
248
  const SECTIONS = {
242
249
  Work: ["inbox", "threads", "tasks", "projects", "outcomes", "escalations", "interventions", "incidents", "briefs", "feedback", "me", "team", "browser", "files"],
243
- Channels: ["channels", "contacts", "webhooks", "integrations", "join-links", "onboarding"],
250
+ Channels: ["channels", "contacts", "webhooks", "integrations", "join-links", "onboarding", "telegram"],
244
251
  Access: ["permissions", "approvals", "secrets", "roles", "org-chart", "org"],
245
252
  Data: ["vaults", "memory", "skills", "metrics", "model", "hosting", "subscriptions", "selftest"],
246
253
  Money: ["spend", "wallet", "usage", "marketplace", "placement", "karma"],
@@ -790,7 +790,9 @@ export function paneInputReady(text, harness = AR_AGENT_HARNESS, readyPattern =
790
790
  * at 50, where the ❯ prompt and the bypass footer still show; the spinner line "✽ Sautéing… (24s · ↓ 105 tokens)"
791
791
  * shows at every width and turns into "✻ Worked for 13s" when the turn ends.
792
792
  */
793
- export const paneBusy = (text) => /\besc to (?:interrupt|cancel)\b/i.test(text ?? "")
793
+ // Hermes (hermes-agent 0.19.0) swaps its idle "❯" composer for "⚕ ❯ msg=interrupt · /queue · … Ctrl+C cancel" mid-turn,
794
+ // and text typed there interrupts the running turn, so the next event of a batch must wait for it.
795
+ export const paneBusy = (text) => /\besc to (?:interrupt|cancel)\b|\bmsg=interrupt\b/i.test(text ?? "")
794
796
  || /^\s*[^\w\s]\s+\S+…\s*\(\s*(?:\d+h\s*)?(?:\d+m\s*)?\d+s\b/m.test(text ?? "");
795
797
 
796
798
  /**
@@ -380,10 +380,12 @@ compact_in_place() {
380
380
  AR_COMPACT_JS="$(cd "$(dirname "$0")/.." && pwd)/pre-stop-compact.mjs" TMUX_BIN="${TMUX_BIN}" TMUX_SESSION="${TMUX_SESSION}" \
381
381
  node --input-type=module -e '
382
382
  import { spawn, spawnSync } from "node:child_process"; import { join } from "node:path";
383
- const { compactBeforeStop, paneTail, tmux, sessionProjectsDir } = await import(process.env.AR_COMPACT_JS);
383
+ const { compactBeforeStop, paneTail, tmux, sessionProjectsDir, reportToServer } = await import(process.env.AR_COMPACT_JS);
384
384
  const bin = process.env.TMUX_BIN, session = `=${process.env.TMUX_SESSION}:`;
385
385
  const dir = process.env.CLAUDE_CONFIG_DIR ? join(process.env.CLAUDE_CONFIG_DIR, "projects") : sessionProjectsDir();
386
- const r = await compactBeforeStop({ dir, session, workloadBusy: () => false, report: () => {}, minTokens: 0,
386
+ // A failed heal is reported as its end, or the next /compact reads as an overlap (COMPACTION-RACE-PARK, Pablo 11-07).
387
+ const report = (r) => reportToServer(r, process.env.AR_SESSION_DIR ? { dir: process.env.AR_SESSION_DIR } : {});
388
+ const r = await compactBeforeStop({ dir, session, workloadBusy: () => false, report, minTokens: 0,
387
389
  run: (argv) => tmux(argv, { uid: -1, spawnFn: (_c, a, o) => spawn(bin, a, o) }),
388
390
  capture: () => paneTail(session, { uid: -1, run: (_c, a, o) => spawnSync(bin, a, o) }) });
389
391
  process.exit(r.compacted ? 0 : 1);' && break
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentradar-cli",
3
- "version": "0.3.20",
3
+ "version": "0.3.22",
4
4
  "engines": {
5
5
  "node": ">=20"
6
6
  },