@phnx-labs/agents-cli 1.20.90 → 1.20.91
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/CHANGELOG.md +121 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/feed.js +77 -4
- package/dist/commands/hooks.js +22 -6
- package/dist/commands/perf.d.ts +14 -0
- package/dist/commands/perf.js +221 -0
- package/dist/commands/routines.js +30 -24
- package/dist/commands/secrets.d.ts +43 -4
- package/dist/commands/secrets.js +217 -32
- package/dist/commands/send.d.ts +5 -1
- package/dist/commands/send.js +1 -1
- package/dist/commands/sessions-picker.js +70 -1
- package/dist/index.js +18 -3
- package/dist/lib/activity.d.ts +11 -1
- package/dist/lib/activity.js +1 -0
- package/dist/lib/catchup.d.ts +105 -0
- package/dist/lib/catchup.js +160 -0
- package/dist/lib/channels/providers/desktop.d.ts +49 -0
- package/dist/lib/channels/providers/desktop.js +132 -0
- package/dist/lib/channels/providers/index.js +2 -0
- package/dist/lib/daemon.js +74 -13
- package/dist/lib/events.d.ts +12 -0
- package/dist/lib/events.js +122 -9
- package/dist/lib/exec.js +10 -0
- package/dist/lib/feed-broadcast.d.ts +47 -0
- package/dist/lib/feed-broadcast.js +65 -1
- package/dist/lib/feed-post.d.ts +10 -0
- package/dist/lib/feed-post.js +1 -1
- package/dist/lib/feed.d.ts +47 -1
- package/dist/lib/feed.js +38 -0
- package/dist/lib/hooks/cache.d.ts +2 -0
- package/dist/lib/hooks/cache.js +24 -4
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/overdue.d.ts +14 -0
- package/dist/lib/overdue.js +37 -1
- package/dist/lib/perf/db.d.ts +25 -0
- package/dist/lib/perf/db.js +290 -0
- package/dist/lib/perf/spool.d.ts +18 -0
- package/dist/lib/perf/spool.js +79 -0
- package/dist/lib/perf/types.d.ts +45 -0
- package/dist/lib/perf/types.js +2 -0
- package/dist/lib/routines-project.js +6 -0
- package/dist/lib/routines.d.ts +30 -1
- package/dist/lib/routines.js +11 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/list-filter.d.ts +94 -0
- package/dist/lib/secrets/list-filter.js +245 -0
- package/dist/lib/session/digest.d.ts +7 -0
- package/dist/lib/session/digest.js +29 -1
- package/dist/lib/session/discover.d.ts +1 -2
- package/dist/lib/session/discover.js +7 -24
- package/dist/lib/session/highlights.d.ts +82 -0
- package/dist/lib/session/highlights.js +251 -0
- package/dist/lib/session/parse.js +23 -1
- package/dist/lib/session/relative-time.d.ts +14 -0
- package/dist/lib/session/relative-time.js +36 -0
- package/dist/lib/session/render.d.ts +7 -0
- package/dist/lib/session/render.js +87 -17
- package/dist/lib/session/types.d.ts +4 -1
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/state.d.ts +9 -0
- package/dist/lib/state.js +11 -0
- package/package.json +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,126 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.20.91
|
|
4
|
+
|
|
5
|
+
- **An agent can now say it is stuck: `agents feed post --blocked` (RUSH-2110).** The
|
|
6
|
+
feed carried benign progress but had no way to signal "I cannot proceed", so agents
|
|
7
|
+
hand-rolled it into the status text (`NEEDS MUQSIT: …`) and it reached nobody. A
|
|
8
|
+
blocked post writes `status.blocked` to the shared activity stream *and* opens an
|
|
9
|
+
answerable block in the ledger, so the ask stays open until someone resolves it
|
|
10
|
+
instead of scrolling away. It is a flag on the existing verb, not a new command —
|
|
11
|
+
one thing for an agent to learn, and one stream where most posts are benign and
|
|
12
|
+
some need a human. Blocked is a state, not a volume: it always broadcasts at
|
|
13
|
+
`important`, so passing `--level` too is a usage error rather than a silent
|
|
14
|
+
override. Pair it with `--option` for an answerable choice or `--default` for a
|
|
15
|
+
safe fallback policy may apply. Source: `apps/cli/src/commands/feed.ts`,
|
|
16
|
+
`apps/cli/src/lib/feed.ts`.
|
|
17
|
+
- **Feed blocks are actually delivered.** `publishBlock` wrote every "needs you"
|
|
18
|
+
record to the ledger and stopped there — `broadcastPostedEvent` ran only for
|
|
19
|
+
`feed post`, so a block was durable and invisible at the same time. Blocks now
|
|
20
|
+
reach the configured `feed.broadcast` sinks, carrying the ask and the literal
|
|
21
|
+
`agents focus <id>` command that unblocks it, and a block that reaches nobody
|
|
22
|
+
exits non-zero instead of looking like a success. Source:
|
|
23
|
+
`apps/cli/src/lib/feed-broadcast.ts`.
|
|
24
|
+
- **New `desktop` channel provider.** `agents send --channel desktop` (and
|
|
25
|
+
`notify.owner.channel: desktop`) posts a native notification through the branded
|
|
26
|
+
menu-bar helper. It is the only channel with no external dependency — no network,
|
|
27
|
+
no login, no vendor CLI — so it still reaches you at your Mac when a messaging
|
|
28
|
+
gateway is down. It reports real deliverability rather than always succeeding:
|
|
29
|
+
on Linux it probes for `notify-send` instead of trusting the platform name.
|
|
30
|
+
Source: `apps/cli/src/lib/channels/providers/desktop.ts`.
|
|
31
|
+
|
|
32
|
+
- **`agents perf` — disposable SQLite latency warehouse.** Indexed p50/p99
|
|
33
|
+
rollups for hooks, CLI commands, and `agent.run` timings without scanning the
|
|
34
|
+
audit JSONL. Warehouse lives at `~/.agents/.cache/perf/perf.db` (safe to wipe);
|
|
35
|
+
identity columns reuse sessions/events string shapes (`session_id`, `agent`,
|
|
36
|
+
`machine`, …) for soft cross-reference — no foreign keys. Hook shims spool
|
|
37
|
+
into the same DB; `agents hooks profile` reads it first. Source:
|
|
38
|
+
`apps/cli/src/lib/perf/db.ts`, `apps/cli/src/commands/perf.ts`.
|
|
39
|
+
|
|
40
|
+
- **A routine that misses its fire now runs late instead of being silently lost.** Fires
|
|
41
|
+
are in-process croner timers, and croner only ever schedules forward from "now" — so a
|
|
42
|
+
daemon that was down, asleep, or wedged when a routine came due dropped that fire
|
|
43
|
+
outright, and `loadAll()` rebuilt every timer looking only at the future. Detection
|
|
44
|
+
existed but ran **once, at daemon startup**, and only logged a warning plus a
|
|
45
|
+
notification; catching up was a manual `agents routines catchup`. Observed cost: zion's
|
|
46
|
+
daemon was down from 02:03Z to 08:23Z while the laptop slept, `weekly-fleet-retro` was
|
|
47
|
+
armed for exactly 04:00Z, never ran, and the restart logged `2 routine(s) overdue` and
|
|
48
|
+
did nothing. The daemon now re-scans every 5 minutes as well as at startup and runs each
|
|
49
|
+
missed routine via the same detached path `catchup` already used. Source:
|
|
50
|
+
`apps/cli/src/lib/catchup.ts`, `apps/cli/src/lib/daemon.ts`.
|
|
51
|
+
- **New `catchup:` routine field, and `agents routines add --no-catchup`.** Defaults to
|
|
52
|
+
true — a routine you scheduled is one you expect to have run. Set `catchup: false` for a
|
|
53
|
+
routine whose worth expires with its slot (a 9am brief is useless at 3pm); the miss is
|
|
54
|
+
still recorded, it just is not re-run. `agents routines list --json` reports the
|
|
55
|
+
effective value as `catchup`.
|
|
56
|
+
- **New `missed` run status.** A missed fire previously left no trace anywhere — no run
|
|
57
|
+
record, no log line in the routine's history — so `agents routines list` kept showing the
|
|
58
|
+
previous run's `completed` as though it were current, sometimes for weeks. A miss is now
|
|
59
|
+
written as a real run stamped at the moment the fire was due, so `agents routines runs
|
|
60
|
+
<name>` shows the gap, and the listing renders it distinctly from `failed` (a miss is an
|
|
61
|
+
infrastructure problem, not a task failure). That record is also what makes catch-up
|
|
62
|
+
idempotent: it advances the overdue comparison, so the same missed fire is never
|
|
63
|
+
reconsidered across ticks or a daemon restart storm, and its directory is created with a
|
|
64
|
+
non-recursive `mkdir` — an atomic claim, so if the daemon's timer and a manual
|
|
65
|
+
`agents routines catchup` overlap, only one of them runs the routine. Source: `apps/cli/src/lib/routines.ts` (`RunMeta`),
|
|
66
|
+
`apps/cli/src/commands/routines.ts`.
|
|
67
|
+
- **A routine is never caught up for a fire that predates it.** `detectOverdueJobs` walks back
|
|
68
|
+
a week for the most recent expected occurrence, and a routine with no runs is overdue by
|
|
69
|
+
definition — so before this, `agents routines add` on any daily or weekly schedule whose slot
|
|
70
|
+
had already passed made the routine instantly "overdue". That was cosmetic while catch-up was
|
|
71
|
+
a manual command; with the daemon now catching up automatically it would have run every newly
|
|
72
|
+
created routine once, within five minutes of creating it. Routines gain a `createdAt` stamp
|
|
73
|
+
(written once, like `actor`), and overdue detection floors the expected fire at it — falling
|
|
74
|
+
back to the routine file's mtime for routines written before the field existed. Observed on
|
|
75
|
+
the live fleet: `agents-cli-updates`, created Aug 1 and never run, was flagged overdue for a
|
|
76
|
+
Jul 27 fire. Source: `apps/cli/src/lib/overdue.ts` (`routineEffectiveStart`),
|
|
77
|
+
`apps/cli/src/lib/routines.ts` (`writeJob`).
|
|
78
|
+
|
|
79
|
+
- **`agents secrets list` can be filtered.** It had no filtering at all —
|
|
80
|
+
`--host` picks a machine and `--json` picks a format, but nothing selected over
|
|
81
|
+
the bundles themselves, so "which of these read with no Touch ID?", "which
|
|
82
|
+
still store a raw value inline?", "what have I not touched in three months?"
|
|
83
|
+
meant piping the table through `grep` or went unanswered. There is now an axis
|
|
84
|
+
per question: a `[query]` positional over name and description, `--policy`,
|
|
85
|
+
`--backend`, `--type`, `--kind`, `--held`/`--not-held`, `--expired`,
|
|
86
|
+
`--expiring [days]`, `--unused <duration>`, plus `--sort` and `-n/--limit`.
|
|
87
|
+
Every axis narrows independently, so they compose. Following the `agents
|
|
88
|
+
sessions` house style, an unknown value is a loud error naming the valid set
|
|
89
|
+
rather than an empty list, filters apply before `--json` so the payload is the
|
|
90
|
+
exact twin of the table, and they are forwarded over `--host` so a remote list
|
|
91
|
+
narrows the same way. `--held`/`--not-held` read live broker state and so
|
|
92
|
+
refuse to run off macOS instead of reporting every bundle as unheld. An empty
|
|
93
|
+
result names the filters that emptied it and the total it started from. Source:
|
|
94
|
+
`apps/cli/src/lib/secrets/list-filter.ts`, `apps/cli/src/commands/secrets.ts`.
|
|
95
|
+
|
|
96
|
+
- **The EXPIRING column no longer hides keys that have already expired.**
|
|
97
|
+
`countExpiringSoon` counted only keys due in the next 30 days — the guard is
|
|
98
|
+
`d >= 0` — so a bundle whose token died last month rendered `-`, identical to
|
|
99
|
+
one with no expiry at all. The only places a lapsed key surfaced were
|
|
100
|
+
`agents secrets view` and a hard abort at inject time, i.e. after it had already
|
|
101
|
+
broken a run. The column now counts lapsed and upcoming together and turns red
|
|
102
|
+
once anything has lapsed, and `secrets list --json` gains an `expired` count
|
|
103
|
+
alongside the existing `expiringSoon`. Source: `apps/cli/src/commands/secrets.ts`.
|
|
104
|
+
|
|
105
|
+
- **`agents secrets list` now states the hold window instead of the bare word
|
|
106
|
+
`hold`.** The `hold` tier is a duration — prompt once, then stay silent for
|
|
107
|
+
`secrets.agent.holdMs` (7 days by default) — but the POLICY column printed only
|
|
108
|
+
the tier name, so a reader could not tell it meant a window, let alone which
|
|
109
|
+
one; finding out meant running `agents secrets status`. The column now reads
|
|
110
|
+
`hold 7d`, and `hold 7d · held 6d` while the broker is actually caching the
|
|
111
|
+
bundle. It follows the configured window, so a 24-hour hold reads `hold 1d`.
|
|
112
|
+
`always` and `never` are unchanged — neither has a window, and annotating one
|
|
113
|
+
would repeat the mistake the `daily` rename fixed. Two adjacent bugs go with
|
|
114
|
+
it: `agents secrets view` printed "7d by default" as a string literal and so
|
|
115
|
+
misstated the window for anyone who had configured `holdMs`, and a stale broker
|
|
116
|
+
entry past its expiry rendered as `hold · held expired` because the column
|
|
117
|
+
tested the entry for presence rather than liveness. `secrets list --json` and
|
|
118
|
+
`secrets view --json` gain an additive `holdMs` field (null on `always`/`never`)
|
|
119
|
+
so a machine caller gets the window too. Source:
|
|
120
|
+
`apps/cli/src/commands/secrets.ts`.
|
|
121
|
+
|
|
122
|
+
- **Richer session previews: skills, hooks, links, artifacts, repos, todo status.** The `agents sessions` quick preview and full summary now show the skills a session invoked (with counts), the hooks that fired (Claude transcripts, with repeat counts and failures), a clickable Links section (Linear/Jira/GitHub/GitLab URLs harvested from the conversation), the documents the session produced (`.agents/artifacts|plans|reports` and other `*.md`/`*.html` creations), the repos it worked in (via `.git` walk-up), and an error tally in the picker. The full summary's Plan section now marks checklist items `[x]`/`[>]`/`[ ]` and renders the checklist alongside the ExitPlanMode text instead of hiding it. Changes/Dirs lines collapse `.agents/worktrees/<slug>` prefixes to `⧉ <slug>/…`, are width-capped, and no longer list shell junk (`2>&1`, `$VAR` paths), `node_modules`, or agents-cli internal archives. Source: `apps/cli/src/lib/session/highlights.ts`, `apps/cli/src/lib/session/parse.ts`, `apps/cli/src/lib/session/render.ts`, `apps/cli/src/commands/sessions-picker.ts`.
|
|
123
|
+
|
|
3
124
|
## 1.20.90
|
|
4
125
|
|
|
5
126
|
- **`agents sessions --active` now shows one row per agent, not one per directory.**
|
package/dist/bin/agents
CHANGED
|
Binary file
|
package/dist/commands/feed.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { ensureFeedPublishHook, listAskStats, listBlocks, recordNotified } from '../lib/feed.js';
|
|
2
|
+
import { ensureFeedPublishHook, listAskStats, listBlocks, recordNotified, buildDeclaredBlock, publishBlock, } from '../lib/feed.js';
|
|
3
3
|
import { ensureActivityLogHook, readRecentActivity, formatActivityLine, formatProgressUpdate, mergeActivityEvents, parseActivityPayload, } from '../lib/activity.js';
|
|
4
4
|
import { postFeedStatus } from '../lib/feed-post.js';
|
|
5
|
-
import { parseFeedPostLevel, planFeedBroadcast, runFeedBroadcast, } from '../lib/feed-broadcast.js';
|
|
5
|
+
import { parseFeedPostLevel, planFeedBroadcast, runFeedBroadcast, blockBroadcastContext, blockDeliveryFailure, } from '../lib/feed-broadcast.js';
|
|
6
6
|
import { getSessionById } from '../lib/session/db.js';
|
|
7
7
|
import { readMeta } from '../lib/state.js';
|
|
8
8
|
import { enrichBlocksFromSessions, groupBlocksByOutcome, isUnambiguousOutcomeAnswer, openBlocksForOutcome, stampBlockOutcomes, } from '../lib/feed-outcome.js';
|
|
@@ -238,6 +238,9 @@ export function registerFeedCommand(program) {
|
|
|
238
238
|
.option('--session <id>', 'Session id escape hatch (default: auto from env / pid registry)')
|
|
239
239
|
.option('--attach <path-or-url...>', 'Attach an artifact (local file or URL); repeatable')
|
|
240
240
|
.option('--level <level>', 'How loudly to broadcast: milestone (default) or important. Configured sinks with minLevel: important only fire on the latter.', 'milestone')
|
|
241
|
+
.option('--blocked', 'You are STUCK and need the user. Opens an answerable block and always broadcasts at important — do not also pass --level.')
|
|
242
|
+
.option('--option <label...>', 'With --blocked: an answer the user can pick; repeatable')
|
|
243
|
+
.option('--default <answer>', 'With --blocked: a safe default policy may apply if nobody answers in time')
|
|
241
244
|
.option('--json', 'Emit the written event as JSON')
|
|
242
245
|
.addHelpText('after', `
|
|
243
246
|
Examples:
|
|
@@ -249,6 +252,17 @@ Examples:
|
|
|
249
252
|
# Worth interrupting someone over — reaches sinks gated on minLevel: important:
|
|
250
253
|
agents feed post "release blocked: npm token expired" --level important
|
|
251
254
|
|
|
255
|
+
# You are STUCK and cannot proceed. Opens an answerable block that stays in
|
|
256
|
+
# 'agents feed' until someone resolves it, and always reaches the owner —
|
|
257
|
+
# do NOT also pass --level:
|
|
258
|
+
agents feed post "force-push denied by git-guard on PR #1749" --blocked
|
|
259
|
+
agents feed post "publish to npm or wait for review?" --blocked --option publish --option wait
|
|
260
|
+
agents feed post "delete the stale preview env?" --blocked --default "leave it"
|
|
261
|
+
|
|
262
|
+
# Exhaust self-serve FIRST. A block is for what you genuinely cannot do:
|
|
263
|
+
# a credential only the user holds, a decision only they can make, an
|
|
264
|
+
# approval only they can give. Not "should I do the obvious next step?".
|
|
265
|
+
|
|
252
266
|
# Outside a run, pass the session explicitly:
|
|
253
267
|
agents feed post "manual note" --session 00998b0e-2d15-4d2f-a58b-974a886c9b47
|
|
254
268
|
|
|
@@ -268,22 +282,66 @@ docs/06-observability.md.
|
|
|
268
282
|
session: opts?.session ?? cmd?.opts?.()?.session,
|
|
269
283
|
attach: opts?.attach ?? cmd?.opts?.()?.attach,
|
|
270
284
|
level: opts?.level ?? cmd?.opts?.()?.level,
|
|
285
|
+
blocked: Boolean(opts?.blocked ?? cmd?.opts?.()?.blocked),
|
|
286
|
+
option: opts?.option ?? cmd?.opts?.()?.option,
|
|
287
|
+
default: opts?.default ?? cmd?.opts?.()?.default,
|
|
271
288
|
json: Boolean(opts?.json ?? cmd?.opts?.()?.json ?? cmd?.parent?.opts?.()?.json),
|
|
272
289
|
};
|
|
273
290
|
try {
|
|
274
|
-
|
|
291
|
+
// Blocked is a state, not a volume: it always broadcasts at `important`,
|
|
292
|
+
// so an agent has exactly one thing to say. Passing both is a usage
|
|
293
|
+
// error rather than a silent override -- an agent that thinks it chose
|
|
294
|
+
// the level should not be quietly ignored.
|
|
295
|
+
if (flags.blocked && flags.level && flags.level !== 'milestone') {
|
|
296
|
+
throw new Error('--blocked already broadcasts at important; drop --level.');
|
|
297
|
+
}
|
|
298
|
+
if (!flags.blocked && (flags.option?.length || flags.default)) {
|
|
299
|
+
throw new Error('--option/--default only apply with --blocked.');
|
|
300
|
+
}
|
|
301
|
+
const level = flags.blocked ? 'important' : parseFeedPostLevel(flags.level);
|
|
275
302
|
const { event } = postFeedStatus({
|
|
276
303
|
text: Array.isArray(textParts) ? textParts.join(' ') : String(textParts ?? ''),
|
|
277
304
|
sessionId: flags.session,
|
|
278
305
|
attach: flags.attach,
|
|
306
|
+
blocked: flags.blocked,
|
|
279
307
|
});
|
|
280
|
-
|
|
308
|
+
// A blocked post lands in BOTH stores: the event in the shared activity
|
|
309
|
+
// stream (what happened) and an OpenBlock in the ledger (what is still
|
|
310
|
+
// open). The ledger is what makes it answerable and clearable -- without
|
|
311
|
+
// it the ask would scroll away like any other update.
|
|
312
|
+
let outcomes;
|
|
313
|
+
if (flags.blocked) {
|
|
314
|
+
const block = buildDeclaredBlock(event, {
|
|
315
|
+
text: event.detail ?? '',
|
|
316
|
+
options: flags.option,
|
|
317
|
+
safeDefault: flags.default,
|
|
318
|
+
});
|
|
319
|
+
publishBlock(block);
|
|
320
|
+
outcomes = broadcastBlock(block, { project: event.project, agent: event.agent });
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
outcomes = broadcastPostedEvent(event, level);
|
|
324
|
+
}
|
|
325
|
+
// Fail loud when a block reached nobody. This is computed BEFORE the
|
|
326
|
+
// --json early return: a machine caller is exactly the one that reads the
|
|
327
|
+
// exit code, so returning 0 there while a human gets 1 would make the
|
|
328
|
+
// undelivered block invisible to the caller most likely to act on it —
|
|
329
|
+
// reintroducing, behind a flag, the silent failure this exists to remove.
|
|
330
|
+
// One sink failing among several stays a warning: the channels are
|
|
331
|
+
// redundant by design.
|
|
332
|
+
const undelivered = blockDeliveryFailure(flags.blocked, outcomes);
|
|
333
|
+
if (undelivered)
|
|
334
|
+
process.exitCode = 1;
|
|
281
335
|
if (flags.json) {
|
|
282
336
|
console.log(JSON.stringify(outcomes.length ? { ...event, broadcast: outcomes } : event, null, 2));
|
|
337
|
+
if (undelivered)
|
|
338
|
+
console.error(chalk.red(undelivered));
|
|
283
339
|
return;
|
|
284
340
|
}
|
|
285
341
|
console.log(formatProgressUpdate(event));
|
|
286
342
|
reportBroadcast(outcomes);
|
|
343
|
+
if (undelivered)
|
|
344
|
+
console.error(chalk.red(undelivered));
|
|
287
345
|
}
|
|
288
346
|
catch (err) {
|
|
289
347
|
console.error(chalk.red(err.message));
|
|
@@ -516,6 +574,21 @@ function broadcastPostedEvent(event, level) {
|
|
|
516
574
|
});
|
|
517
575
|
return runFeedBroadcast(planned);
|
|
518
576
|
}
|
|
577
|
+
/**
|
|
578
|
+
* Mirror a declared block to the same sinks a post reaches.
|
|
579
|
+
*
|
|
580
|
+
* Blocks previously never broadcast at all: `broadcastPostedEvent` ran only for
|
|
581
|
+
* `feed post`, while every `publishBlock` call wrote to the ledger and stopped
|
|
582
|
+
* there — so a "needs you" record was durable and invisible at the same time.
|
|
583
|
+
*/
|
|
584
|
+
function broadcastBlock(block, extras) {
|
|
585
|
+
const config = readMeta().feed?.broadcast;
|
|
586
|
+
if (!config || Object.keys(config).length === 0)
|
|
587
|
+
return [];
|
|
588
|
+
const ticket = getSessionById(block.sessionId)?.ticketId;
|
|
589
|
+
const ctx = blockBroadcastContext({ ...block, ticket: block.ticket ?? ticket }, extras);
|
|
590
|
+
return runFeedBroadcast(planFeedBroadcast(config, ctx));
|
|
591
|
+
}
|
|
519
592
|
/** One line per sink that ran. Silent when nothing is configured. */
|
|
520
593
|
function reportBroadcast(outcomes) {
|
|
521
594
|
for (const o of outcomes) {
|
package/dist/commands/hooks.js
CHANGED
|
@@ -603,30 +603,46 @@ Examples:
|
|
|
603
603
|
.option('--warn-ms <n>', 'p99 threshold above which a hook is flagged as slow', '2000')
|
|
604
604
|
.option('--json', 'Emit raw JSON rows instead of the table')
|
|
605
605
|
.addHelpText('after', `
|
|
606
|
-
Shows aggregated stats for every hook that
|
|
607
|
-
~/.agents/.cache/
|
|
608
|
-
|
|
606
|
+
Shows aggregated stats for every hook that fired through a generated shim.
|
|
607
|
+
Primary source: disposable SQLite warehouse ~/.agents/.cache/perf/perf.db
|
|
608
|
+
(same data as \`agents perf hooks\`). Falls back to the legacy daily JSONL under
|
|
609
|
+
~/.agents/.cache/logs/ when the warehouse is empty.
|
|
609
610
|
|
|
610
611
|
Examples:
|
|
611
612
|
agents hooks profile # last 7 days, table form
|
|
612
613
|
agents hooks profile --days 30 # roll up the full month
|
|
613
614
|
agents hooks profile --json | jq # pipe somewhere
|
|
615
|
+
agents perf hooks # same rollup under the perf surface
|
|
614
616
|
|
|
615
617
|
A hook whose p99 exceeds --warn-ms gets flagged in the cache column. Add
|
|
616
618
|
'cache: 5m' or 'cache: 5m-bg' to its hooks.yaml entry to fix it.
|
|
617
619
|
`)
|
|
618
620
|
.action(async (options) => {
|
|
619
621
|
const { aggregateHookProfile, loadHookFireEvents, formatMs, formatCacheColumn, DEFAULT_SLOW_HOOK_WARN_MS } = await import('../lib/hooks/profile.js');
|
|
622
|
+
const { aggregateSamples } = await import('../lib/perf/db.js');
|
|
620
623
|
const days = Math.max(1, parseInt(options.days, 10) || 7);
|
|
621
624
|
const warnMs = Math.max(0, parseInt(options.warnMs, 10) || DEFAULT_SLOW_HOOK_WARN_MS);
|
|
622
|
-
|
|
625
|
+
// Prefer the indexed warehouse; fall back to legacy JSONL for pre-warehouse shims.
|
|
626
|
+
const fromDb = aggregateSamples({ days, kinds: ['hook.fire'] }).map((r) => ({
|
|
627
|
+
hook: r.label,
|
|
628
|
+
n: r.n,
|
|
629
|
+
p50Ms: r.p50Ms,
|
|
630
|
+
p99Ms: r.p99Ms,
|
|
631
|
+
meanMs: r.meanMs,
|
|
632
|
+
maxMs: r.maxMs,
|
|
633
|
+
cacheHitPct: r.cacheHitPct ?? 0,
|
|
634
|
+
cacheStalePct: r.cacheStalePct ?? 0,
|
|
635
|
+
cacheMissPct: r.cacheMissPct ?? 0,
|
|
636
|
+
errorCount: r.errorCount ?? 0,
|
|
637
|
+
}));
|
|
638
|
+
const rows = fromDb.length > 0 ? fromDb : aggregateHookProfile(loadHookFireEvents(days));
|
|
623
639
|
if (options.json) {
|
|
624
640
|
console.log(JSON.stringify(rows, null, 2));
|
|
625
641
|
return;
|
|
626
642
|
}
|
|
627
643
|
if (rows.length === 0) {
|
|
628
|
-
console.log(chalk.gray(`No hook.fire
|
|
629
|
-
console.log(chalk.gray('Add \'cache: 5m\' to a hook
|
|
644
|
+
console.log(chalk.gray(`No hook.fire samples in the last ${days} day${days === 1 ? '' : 's'}.`));
|
|
645
|
+
console.log(chalk.gray('Add \'cache: 5m\' to a hook (or matches:) so a shim instruments it, then resync.'));
|
|
630
646
|
return;
|
|
631
647
|
}
|
|
632
648
|
const widths = { hook: 36, n: 5, p50: 7, p99: 7, mean: 7, max: 7, cache: 30 };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents perf` — latency rollups over the disposable perf SQLite warehouse.
|
|
3
|
+
*
|
|
4
|
+
* Subcommands:
|
|
5
|
+
* agents perf multi-section summary (commands + hooks + runs)
|
|
6
|
+
* agents perf hooks per-hook p50/p99 + cache hit rates
|
|
7
|
+
* agents perf commands slowest CLI command paths (from command.end)
|
|
8
|
+
* agents perf run agent.run / perf.timing labels
|
|
9
|
+
*
|
|
10
|
+
* Soft-joins sessions.db via shared string keys (session_id, agent, machine) —
|
|
11
|
+
* no foreign keys. Warehouse lives at ~/.agents/.cache/perf/perf.db (safe to wipe).
|
|
12
|
+
*/
|
|
13
|
+
import type { Command } from 'commander';
|
|
14
|
+
export declare function registerPerfCommand(program: Command): void;
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents perf` — latency rollups over the disposable perf SQLite warehouse.
|
|
3
|
+
*
|
|
4
|
+
* Subcommands:
|
|
5
|
+
* agents perf multi-section summary (commands + hooks + runs)
|
|
6
|
+
* agents perf hooks per-hook p50/p99 + cache hit rates
|
|
7
|
+
* agents perf commands slowest CLI command paths (from command.end)
|
|
8
|
+
* agents perf run agent.run / perf.timing labels
|
|
9
|
+
*
|
|
10
|
+
* Soft-joins sessions.db via shared string keys (session_id, agent, machine) —
|
|
11
|
+
* no foreign keys. Warehouse lives at ~/.agents/.cache/perf/perf.db (safe to wipe).
|
|
12
|
+
*/
|
|
13
|
+
import chalk from 'chalk';
|
|
14
|
+
import { aggregateSamples, perfDbPath, } from '../lib/perf/db.js';
|
|
15
|
+
import { formatMs, formatCacheColumn, DEFAULT_SLOW_HOOK_WARN_MS, loadHookFireEvents, aggregateHookProfile, } from '../lib/hooks/profile.js';
|
|
16
|
+
function parseDays(raw) {
|
|
17
|
+
const n = parseInt(raw ?? '7', 10);
|
|
18
|
+
return Number.isFinite(n) && n > 0 ? n : 7;
|
|
19
|
+
}
|
|
20
|
+
function parseWarnMs(raw, fallback) {
|
|
21
|
+
const n = parseInt(raw ?? String(fallback), 10);
|
|
22
|
+
return Number.isFinite(n) && n >= 0 ? n : fallback;
|
|
23
|
+
}
|
|
24
|
+
function parseLimit(raw, fallback) {
|
|
25
|
+
const n = parseInt(raw ?? String(fallback), 10);
|
|
26
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
27
|
+
}
|
|
28
|
+
/** Map warehouse rows shaped like hook.fire into the existing HookProfileRow UI. */
|
|
29
|
+
function asHookRows(rows) {
|
|
30
|
+
return rows.map((r) => ({
|
|
31
|
+
hook: r.label,
|
|
32
|
+
n: r.n,
|
|
33
|
+
p50Ms: r.p50Ms,
|
|
34
|
+
p99Ms: r.p99Ms,
|
|
35
|
+
meanMs: r.meanMs,
|
|
36
|
+
maxMs: r.maxMs,
|
|
37
|
+
cacheHitPct: r.cacheHitPct ?? 0,
|
|
38
|
+
cacheStalePct: r.cacheStalePct ?? 0,
|
|
39
|
+
cacheMissPct: r.cacheMissPct ?? 0,
|
|
40
|
+
errorCount: r.errorCount ?? 0,
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
function printTable(headers, widths, lines, slowFlags) {
|
|
44
|
+
const pad = (s, w) => (s.length >= w ? s.slice(0, w) : s + ' '.repeat(w - s.length));
|
|
45
|
+
const header = headers.map((h, i) => pad(h, widths[i])).join(' ');
|
|
46
|
+
console.log(chalk.bold(header));
|
|
47
|
+
console.log(chalk.gray('─'.repeat(header.length)));
|
|
48
|
+
for (let i = 0; i < lines.length; i++) {
|
|
49
|
+
const line = lines[i].map((c, j) => pad(c, widths[j])).join(' ');
|
|
50
|
+
console.log(slowFlags[i] ? chalk.yellow(line) : line);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function renderHookTable(rows, warnMs) {
|
|
54
|
+
if (rows.length === 0) {
|
|
55
|
+
console.log(chalk.gray('No hook timing samples yet.'));
|
|
56
|
+
console.log(chalk.gray(`Warehouse: ${perfDbPath()}`));
|
|
57
|
+
console.log(chalk.gray('Hooks write via cache/matches shims into the spool; run a session or resync hooks.'));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const widths = { hook: 36, n: 5, p50: 7, p99: 7, mean: 7, max: 7, cache: 28 };
|
|
61
|
+
const pad = (s, w) => (s.length >= w ? s.slice(0, w) : s + ' '.repeat(w - s.length));
|
|
62
|
+
const header = [
|
|
63
|
+
pad('HOOK', widths.hook),
|
|
64
|
+
pad('N', widths.n),
|
|
65
|
+
pad('P50', widths.p50),
|
|
66
|
+
pad('P99', widths.p99),
|
|
67
|
+
pad('MEAN', widths.mean),
|
|
68
|
+
pad('MAX', widths.max),
|
|
69
|
+
pad('CACHE', widths.cache),
|
|
70
|
+
].join(' ');
|
|
71
|
+
console.log(chalk.bold(header));
|
|
72
|
+
console.log(chalk.gray('─'.repeat(header.length)));
|
|
73
|
+
for (const r of rows) {
|
|
74
|
+
const slow = r.p99Ms > warnMs;
|
|
75
|
+
const cacheCol = formatCacheColumn(r);
|
|
76
|
+
const warning = slow && r.cacheHitPct + r.cacheStalePct === 0 ? ' ← add cache: 5m' : '';
|
|
77
|
+
const line = [
|
|
78
|
+
pad(r.hook, widths.hook),
|
|
79
|
+
pad(String(r.n), widths.n),
|
|
80
|
+
pad(formatMs(r.p50Ms), widths.p50),
|
|
81
|
+
pad(formatMs(r.p99Ms), widths.p99),
|
|
82
|
+
pad(formatMs(r.meanMs), widths.mean),
|
|
83
|
+
pad(formatMs(r.maxMs), widths.max),
|
|
84
|
+
pad(cacheCol, widths.cache),
|
|
85
|
+
].join(' ') + warning;
|
|
86
|
+
console.log(slow ? chalk.yellow(line) : line);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function renderLabelTable(title, rows, warnMs, limit) {
|
|
90
|
+
const sliced = rows.slice(0, limit);
|
|
91
|
+
if (sliced.length === 0) {
|
|
92
|
+
console.log(chalk.gray(`No ${title} samples yet.`));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const widths = [40, 5, 7, 7, 7, 7];
|
|
96
|
+
printTable(['LABEL', 'N', 'P50', 'P99', 'MEAN', 'MAX'], widths, sliced.map((r) => [
|
|
97
|
+
r.label,
|
|
98
|
+
String(r.n),
|
|
99
|
+
formatMs(r.p50Ms),
|
|
100
|
+
formatMs(r.p99Ms),
|
|
101
|
+
formatMs(r.meanMs),
|
|
102
|
+
formatMs(r.maxMs),
|
|
103
|
+
]), sliced.map((r) => r.p99Ms > warnMs));
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Prefer SQLite samples; fall back to the legacy daily JSONL so existing
|
|
107
|
+
* instrumentation still surfaces until shims are resynced.
|
|
108
|
+
*/
|
|
109
|
+
function loadHookProfile(days) {
|
|
110
|
+
const fromDb = asHookRows(aggregateSamples({ days, kinds: ['hook.fire'] }));
|
|
111
|
+
if (fromDb.length > 0)
|
|
112
|
+
return fromDb;
|
|
113
|
+
return aggregateHookProfile(loadHookFireEvents(days));
|
|
114
|
+
}
|
|
115
|
+
function hooksAction(opts) {
|
|
116
|
+
const days = parseDays(opts.days);
|
|
117
|
+
const warnMs = parseWarnMs(opts.warnMs, DEFAULT_SLOW_HOOK_WARN_MS);
|
|
118
|
+
const rows = loadHookProfile(days);
|
|
119
|
+
if (opts.json) {
|
|
120
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
renderHookTable(rows, warnMs);
|
|
124
|
+
}
|
|
125
|
+
function commandsAction(opts) {
|
|
126
|
+
const days = parseDays(opts.days);
|
|
127
|
+
const warnMs = parseWarnMs(opts.warnMs, 500);
|
|
128
|
+
const limit = parseLimit(opts.limit, 40);
|
|
129
|
+
const rows = aggregateSamples({ days, kinds: ['command.end'] });
|
|
130
|
+
if (opts.json) {
|
|
131
|
+
console.log(JSON.stringify(rows.slice(0, limit), null, 2));
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
renderLabelTable('command', rows, warnMs, limit);
|
|
135
|
+
}
|
|
136
|
+
function runAction(opts) {
|
|
137
|
+
const days = parseDays(opts.days);
|
|
138
|
+
const warnMs = parseWarnMs(opts.warnMs, 60_000);
|
|
139
|
+
const limit = parseLimit(opts.limit, 40);
|
|
140
|
+
const rows = aggregateSamples({ days, kinds: ['perf.timing'] });
|
|
141
|
+
if (opts.json) {
|
|
142
|
+
console.log(JSON.stringify(rows.slice(0, limit), null, 2));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
renderLabelTable('run/timing', rows, warnMs, limit);
|
|
146
|
+
}
|
|
147
|
+
function summaryAction(opts) {
|
|
148
|
+
const days = parseDays(opts.days);
|
|
149
|
+
if (opts.json) {
|
|
150
|
+
console.log(JSON.stringify({
|
|
151
|
+
days,
|
|
152
|
+
warehouse: perfDbPath(),
|
|
153
|
+
hooks: loadHookProfile(days),
|
|
154
|
+
commands: aggregateSamples({ days, kinds: ['command.end'] }).slice(0, 20),
|
|
155
|
+
run: aggregateSamples({ days, kinds: ['perf.timing'] }).slice(0, 20),
|
|
156
|
+
}, null, 2));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
console.log(chalk.bold(`agents perf — last ${days} day${days === 1 ? '' : 's'}`));
|
|
160
|
+
console.log(chalk.gray(`warehouse: ${perfDbPath()} (disposable; soft-join sessions via session_id/agent/machine)`));
|
|
161
|
+
console.log('');
|
|
162
|
+
console.log(chalk.bold('Commands (slowest by p99)'));
|
|
163
|
+
renderLabelTable('command', aggregateSamples({ days, kinds: ['command.end'] }), parseWarnMs(opts.warnMs, 500), 12);
|
|
164
|
+
console.log('');
|
|
165
|
+
console.log(chalk.bold('Hooks'));
|
|
166
|
+
renderHookTable(loadHookProfile(days), parseWarnMs(opts.warnMs, DEFAULT_SLOW_HOOK_WARN_MS));
|
|
167
|
+
console.log('');
|
|
168
|
+
console.log(chalk.bold('Runs (perf.timing)'));
|
|
169
|
+
renderLabelTable('run/timing', aggregateSamples({ days, kinds: ['perf.timing'] }), parseWarnMs(opts.warnMs, 60_000), 12);
|
|
170
|
+
}
|
|
171
|
+
function attachSharedOptions(cmd) {
|
|
172
|
+
return cmd
|
|
173
|
+
.option('--days <n>', 'Days of samples to include', '7')
|
|
174
|
+
.option('--warn-ms <n>', 'p99 above this is highlighted')
|
|
175
|
+
.option('--limit <n>', 'Max rows in the table', '40')
|
|
176
|
+
.option('--json', 'Emit JSON instead of a table');
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Commander binds a flag declared on both parent and child to the *parent*.
|
|
180
|
+
* Merge so `agents perf commands --json` still sees json:true on the leaf.
|
|
181
|
+
*/
|
|
182
|
+
function leafOpts(cmd) {
|
|
183
|
+
const parent = cmd.parent && typeof cmd.parent.opts === 'function'
|
|
184
|
+
? cmd.parent.opts()
|
|
185
|
+
: {};
|
|
186
|
+
return { ...parent, ...cmd.opts() };
|
|
187
|
+
}
|
|
188
|
+
export function registerPerfCommand(program) {
|
|
189
|
+
const perf = program
|
|
190
|
+
.command('perf')
|
|
191
|
+
.description('Latency rollups from the disposable perf warehouse (hooks, commands, runs)')
|
|
192
|
+
.addHelpText('after', `
|
|
193
|
+
The warehouse is SQLite at ~/.agents/.cache/perf/perf.db — safe to delete.
|
|
194
|
+
Identity columns reuse sessions/events string shapes (session_id, agent, machine)
|
|
195
|
+
for soft cross-reference; there are no foreign keys.
|
|
196
|
+
|
|
197
|
+
Examples:
|
|
198
|
+
agents perf # summary: commands + hooks + runs
|
|
199
|
+
agents perf hooks # per-hook p50/p99 + cache hit rate
|
|
200
|
+
agents perf commands --days 30 # slowest CLI entrypoints
|
|
201
|
+
agents perf run --json # agent.run timings as JSON
|
|
202
|
+
agents perf hooks --warn-ms 500
|
|
203
|
+
`);
|
|
204
|
+
// Options live on the parent so `agents perf --json` and
|
|
205
|
+
// `agents perf commands --json` both work (see leafOpts).
|
|
206
|
+
attachSharedOptions(perf).action(function summary() {
|
|
207
|
+
summaryAction(this.opts());
|
|
208
|
+
});
|
|
209
|
+
perf.command('hooks').description('Per-hook timing + cache stats')
|
|
210
|
+
.action(function hooks() {
|
|
211
|
+
hooksAction(leafOpts(this));
|
|
212
|
+
});
|
|
213
|
+
perf.command('commands').description('Slowest CLI command paths (command.end samples)')
|
|
214
|
+
.action(function commands() {
|
|
215
|
+
commandsAction(leafOpts(this));
|
|
216
|
+
});
|
|
217
|
+
perf.command('run').description('agent.run / perf.timing label rollups')
|
|
218
|
+
.action(function run() {
|
|
219
|
+
runAction(leafOpts(this));
|
|
220
|
+
});
|
|
221
|
+
}
|