@phnx-labs/agents-cli 1.22.62 → 1.22.64
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 +37 -0
- package/dist/commands/accounts.d.ts +1 -1
- package/dist/commands/accounts.js +33 -7
- package/dist/commands/exec.js +40 -0
- package/dist/commands/feed.d.ts +1 -1
- package/dist/commands/feed.js +2 -1
- package/dist/lib/accounting/rotate.d.ts +22 -0
- package/dist/lib/accounting/rotate.js +20 -0
- package/dist/lib/claude-account-token.d.ts +26 -0
- package/dist/lib/claude-account-token.js +65 -11
- package/dist/lib/event-families.js +1 -1
- package/dist/lib/exec.d.ts +66 -1
- package/dist/lib/exec.js +104 -8
- package/dist/lib/feed/events.d.ts +1 -1
- package/dist/lib/feed/events.js +5 -0
- package/dist/lib/feed-broadcast.d.ts +9 -0
- package/dist/lib/feed-broadcast.js +30 -1
- package/dist/lib/identity/client.d.ts +5 -4
- package/dist/lib/identity/client.js +6 -5
- package/dist/lib/resource-inventory.d.ts +1 -1
- package/dist/lib/resource-inventory.js +1 -1
- package/dist/lib/session/active.d.ts +10 -1
- package/dist/lib/session/active.js +8 -0
- package/dist/lib/session/digest.js +8 -2
- package/dist/lib/session/parse.d.ts +4 -0
- package/dist/lib/session/parse.js +16 -11
- package/dist/lib/session/state.d.ts +5 -0
- package/dist/lib/session/state.js +6 -0
- package/dist/lib/share/worker-template.js +268 -12
- package/dist/lib/types.d.ts +3 -1
- package/package.json +1 -1
package/dist/lib/exec.js
CHANGED
|
@@ -14,7 +14,7 @@ import { parseTimeout } from './scheduling/routines.js';
|
|
|
14
14
|
import { compareVersions, getBinaryPath, getVersionHomePath, isVersionInstalled, listInstalledVersions, resolveVersion } from './installations/versions.js';
|
|
15
15
|
import { resolveModel, buildReasoningFlags } from './models.js';
|
|
16
16
|
import { isTierToken, resolveTier } from './model-tiers.js';
|
|
17
|
-
import { createTimer, redactPrompt, redactArgs } from './feed/events.js';
|
|
17
|
+
import { emit, createTimer, redactPrompt, redactArgs } from './feed/events.js';
|
|
18
18
|
import { sanitizeProcessEnv } from './secrets/bundles.js';
|
|
19
19
|
import { resolveActor, actorEnv } from './actor.js';
|
|
20
20
|
import { expandLocalHome } from './project-root.js';
|
|
@@ -1151,6 +1151,12 @@ export async function execShimPassthrough(agent, rawArgs, cwd, pinnedVersion) {
|
|
|
1151
1151
|
// buildExecEnv — `installations/shims.ts` generateShimScript).
|
|
1152
1152
|
const env = buildExecEnv({ agent, version, cwd, mode: defaultModeFor(agent), effort: 'auto', env: { AGENT_LAUNCH_ID: launchId } });
|
|
1153
1153
|
const { command, args, shell } = resolveShimSpawn(process.platform, binary, [...launchArgs, ...rawArgs]);
|
|
1154
|
+
// Pre-launch marker for the SECOND live launch path: the Windows generated
|
|
1155
|
+
// `.cmd` shim delegates here and spawns the harness directly, so without this
|
|
1156
|
+
// a pinned, logged-out version launched via the shim would be invisible —
|
|
1157
|
+
// exactly the blind spot this event closes. Best-effort; re-derives the
|
|
1158
|
+
// signed-in verdict from the pinned version home (no rotated pick here).
|
|
1159
|
+
await emitRunLaunch({ agent, version, resolvedVia: 'shim' });
|
|
1154
1160
|
return new Promise((resolve) => {
|
|
1155
1161
|
const child = spawn(command, args, { cwd, stdio: 'inherit', env, shell });
|
|
1156
1162
|
// Record the launch so `ag sessions --active` can attribute the agent
|
|
@@ -1633,15 +1639,88 @@ function emitResolvedSessionId(options, launchId, childPid) {
|
|
|
1633
1639
|
process.stdout.write(sessionIdMarkerLine(sessionId));
|
|
1634
1640
|
}
|
|
1635
1641
|
/**
|
|
1636
|
-
*
|
|
1642
|
+
* Build the `run.launch` event payload. Pure and exported so the signedIn ->
|
|
1643
|
+
* launchedLoggedOut mapping is unit-testable without spawning. Mirrors the
|
|
1644
|
+
* buildRotationDecisionEvent / emitRotationDecision split in rotate.ts.
|
|
1637
1645
|
*
|
|
1638
|
-
*
|
|
1639
|
-
*
|
|
1640
|
-
*
|
|
1641
|
-
*
|
|
1642
|
-
* run ... | ...` composes cleanly), otherwise 'inherit' so TTY output is
|
|
1643
|
-
* unbuffered.
|
|
1646
|
+
* `launchedLoggedOut` is the headline flag — true ONLY when `signedIn` was
|
|
1647
|
+
* resolved to false (a launch into a logged-out version, the yosemite-m3 2.1.219
|
|
1648
|
+
* incident). An UNKNOWN verdict (`signedIn === null`) is never treated as
|
|
1649
|
+
* logged out.
|
|
1644
1650
|
*/
|
|
1651
|
+
export function buildRunLaunchPayload(input) {
|
|
1652
|
+
return {
|
|
1653
|
+
module: 'run',
|
|
1654
|
+
agent: input.agent,
|
|
1655
|
+
...(input.harnessName ? { harnessName: input.harnessName } : {}),
|
|
1656
|
+
// Omitted only when no version could be resolved at all — the typed `version`
|
|
1657
|
+
// field can't carry null, and an absent version is honest.
|
|
1658
|
+
version: input.version,
|
|
1659
|
+
strategy: input.strategy ?? null,
|
|
1660
|
+
signedIn: input.signedIn,
|
|
1661
|
+
launchedLoggedOut: input.signedIn === false,
|
|
1662
|
+
email: input.email,
|
|
1663
|
+
...(input.resolvedVia ? { resolvedVia: input.resolvedVia } : {}),
|
|
1664
|
+
};
|
|
1665
|
+
}
|
|
1666
|
+
/**
|
|
1667
|
+
* Emit the pre-launch `run.launch` observability event just before the harness
|
|
1668
|
+
* child is spawned. Best-effort by construction — the whole point is that it can
|
|
1669
|
+
* NEVER break a launch, so every step (the signed-in probe, the emit itself) is
|
|
1670
|
+
* wrapped and a failure is swallowed. Mirrors the non-fatal pattern of
|
|
1671
|
+
* recordDispatchedRun and emitRotationDecision.
|
|
1672
|
+
*
|
|
1673
|
+
* The `signedIn` verdict is for the SPECIFIC launched version on THIS device, so
|
|
1674
|
+
* `launchedLoggedOut` catches a launch into a logged-out version that
|
|
1675
|
+
* `--device auto` readiness let through (the yosemite-m3 2.1.219 incident). It is
|
|
1676
|
+
* REUSED from `ctx.launchSignedIn` when the caller already computed it via the
|
|
1677
|
+
* identical gate (the rotated pick — no double fs read, no disagreement window),
|
|
1678
|
+
* and only otherwise re-derived via {@link isVersionLaunchableHere} (dynamically
|
|
1679
|
+
* imported so the spawn path takes on no static dependency on the accounting
|
|
1680
|
+
* layer).
|
|
1681
|
+
*/
|
|
1682
|
+
async function emitRunLaunch(ctx) {
|
|
1683
|
+
try {
|
|
1684
|
+
let signedIn;
|
|
1685
|
+
let email;
|
|
1686
|
+
if (ctx.launchSignedIn !== undefined) {
|
|
1687
|
+
// Caller already computed the verdict for this exact version via the same
|
|
1688
|
+
// gate — reuse it verbatim.
|
|
1689
|
+
signedIn = ctx.launchSignedIn;
|
|
1690
|
+
email = ctx.launchEmail ?? null;
|
|
1691
|
+
}
|
|
1692
|
+
else {
|
|
1693
|
+
// Pinned-default / shim paths: probe the version home now. Without a
|
|
1694
|
+
// concrete version we cannot probe, so leave the verdict unknown (null)
|
|
1695
|
+
// rather than guess.
|
|
1696
|
+
signedIn = null;
|
|
1697
|
+
email = null;
|
|
1698
|
+
if (ctx.version) {
|
|
1699
|
+
try {
|
|
1700
|
+
const { isVersionLaunchableHere } = await import('./accounting/rotate.js');
|
|
1701
|
+
const state = await isVersionLaunchableHere(ctx.agent, ctx.version);
|
|
1702
|
+
signedIn = state.launchable;
|
|
1703
|
+
email = state.email;
|
|
1704
|
+
}
|
|
1705
|
+
catch {
|
|
1706
|
+
/* probe unavailable — record the launch without the signed-in verdict */
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
emit('run.launch', buildRunLaunchPayload({
|
|
1711
|
+
agent: ctx.agent,
|
|
1712
|
+
harnessName: ctx.harnessName,
|
|
1713
|
+
version: ctx.version,
|
|
1714
|
+
strategy: ctx.strategy,
|
|
1715
|
+
signedIn,
|
|
1716
|
+
email,
|
|
1717
|
+
resolvedVia: ctx.resolvedVia,
|
|
1718
|
+
}));
|
|
1719
|
+
}
|
|
1720
|
+
catch {
|
|
1721
|
+
/* observability must never break a launch */
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1645
1724
|
async function spawnAgent(options) {
|
|
1646
1725
|
// Assign a known session id up front for agents that accept one, so the
|
|
1647
1726
|
// launcher can record an EXACT pid -> session mapping (see pid-registry) —
|
|
@@ -1758,6 +1837,23 @@ async function spawnAgent(options) {
|
|
|
1758
1837
|
timer.end({ exitCode: 1, status: 'failed', error: 'remote interactive run has no tmux for durability' });
|
|
1759
1838
|
return { exitCode: 1, stdout: '', stderr: msg };
|
|
1760
1839
|
}
|
|
1840
|
+
// Pre-launch marker (`run.launch`), fired on THIS device right before the
|
|
1841
|
+
// harness child is spawned — for BOTH the tmux-wrapped and bare paths below,
|
|
1842
|
+
// and only once the undurable refusal above has passed (a refused run never
|
|
1843
|
+
// spawns). Unlike `run.dispatched` (recordDispatchedRun, at FINALIZE), this
|
|
1844
|
+
// records a launch that then sits stuck at a login screen and never finalizes,
|
|
1845
|
+
// so a launch into a logged-out version is visible instead of silent. The
|
|
1846
|
+
// signed-in verdict is REUSED from the rotated pick when the command threaded
|
|
1847
|
+
// it (options.launchSignedIn), else re-derived from the version home.
|
|
1848
|
+
await emitRunLaunch({
|
|
1849
|
+
agent: options.agent,
|
|
1850
|
+
harnessName: options.harnessName,
|
|
1851
|
+
version: options.version ?? resolveVersion(options.agent, options.cwd || process.cwd()) ?? undefined,
|
|
1852
|
+
strategy: options.strategy,
|
|
1853
|
+
resolvedVia: options.resolvedVia,
|
|
1854
|
+
launchSignedIn: options.launchSignedIn,
|
|
1855
|
+
launchEmail: options.launchEmail,
|
|
1856
|
+
});
|
|
1761
1857
|
if (tmuxWrap.kind === 'wrap') {
|
|
1762
1858
|
timer.mark('startup');
|
|
1763
1859
|
try {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import type { ActorKind } from '../actor.js';
|
|
15
15
|
export type EventLevel = 'audit' | 'warn' | 'info' | 'debug';
|
|
16
|
-
export type EventType = 'agent.run.start' | 'agent.run.end' | 'agent.spawn.start' | 'agent.spawn.end' | 'run.dispatched' | 'daemon.start' | 'daemon.stop' | 'daemon.error' | 'daemon.info' | 'routine.start' | 'routine.end' | 'watchdog.action' | 'version.install' | 'version.switch' | 'version.remove' | 'skill.install' | 'skill.remove' | 'browser.launch' | 'browser.close' | 'browser.navigate' | 'browser.screenshot' | 'computer.action' | 'secrets.get' | 'secrets.unlocked' | 'secrets.create' | 'secrets.import' | 'secrets.export' | 'secrets.view' | 'secrets.lease-denied' | 'secrets.lease-expire' | 'secrets.set' | 'secrets.delete' | 'secrets.rename' | 'cloud.dispatch' | 'cloud.complete' | 'cloud.cancel' | 'cloud.message' | 'teams.create' | 'teams.add' | 'teams.start' | 'teams.complete' | 'teams.disband' | 'hook.fire' | 'hook.complete' | 'hook.error' | 'mcp.add' | 'mcp.remove' | 'mcp.register' | 'resource.sync' | 'rotation.resolved' | 'rotation.unresolved' | 'command.start' | 'command.end' | 'perf.timing' | 'session.start' | 'session.end' | 'webhook.received' | 'webhook.authorized' | 'webhook.rejected' | 'webhook.matched' | 'webhook.fired' | 'webhook.failed' | 'webhook.handler.start' | 'webhook.handler.end' | 'plan.created' | 'pr.opened' | 'pr.merged' | 'worktree.created' | 'worktree.removed' | 'commit.created' | 'pushed' | 'subagent.spawned' | 'artifact.created' | 'task.completed' | 'checklist.created' | 'status.posted' | 'file.edited' | 'factory.command' | 'factory.action' | 'factory.uri' | 'factory.launch' | 'friction' | 'error' | 'warn' | 'info' | 'debug';
|
|
16
|
+
export type EventType = 'agent.run.start' | 'agent.run.end' | 'agent.spawn.start' | 'agent.spawn.end' | 'run.dispatched' | 'run.launch' | 'daemon.start' | 'daemon.stop' | 'daemon.error' | 'daemon.info' | 'routine.start' | 'routine.end' | 'watchdog.action' | 'version.install' | 'version.switch' | 'version.remove' | 'skill.install' | 'skill.remove' | 'browser.launch' | 'browser.close' | 'browser.navigate' | 'browser.screenshot' | 'computer.action' | 'secrets.get' | 'secrets.unlocked' | 'secrets.create' | 'secrets.import' | 'secrets.export' | 'secrets.view' | 'secrets.lease-denied' | 'secrets.lease-expire' | 'secrets.set' | 'secrets.delete' | 'secrets.rename' | 'cloud.dispatch' | 'cloud.complete' | 'cloud.cancel' | 'cloud.message' | 'teams.create' | 'teams.add' | 'teams.start' | 'teams.complete' | 'teams.disband' | 'hook.fire' | 'hook.complete' | 'hook.error' | 'mcp.add' | 'mcp.remove' | 'mcp.register' | 'resource.sync' | 'rotation.resolved' | 'rotation.unresolved' | 'command.start' | 'command.end' | 'perf.timing' | 'session.start' | 'session.end' | 'webhook.received' | 'webhook.authorized' | 'webhook.rejected' | 'webhook.matched' | 'webhook.fired' | 'webhook.failed' | 'webhook.handler.start' | 'webhook.handler.end' | 'plan.created' | 'pr.opened' | 'pr.merged' | 'worktree.created' | 'worktree.removed' | 'commit.created' | 'pushed' | 'subagent.spawned' | 'artifact.created' | 'task.completed' | 'checklist.created' | 'status.posted' | 'file.edited' | 'factory.command' | 'factory.action' | 'factory.uri' | 'factory.launch' | 'friction' | 'error' | 'warn' | 'info' | 'debug';
|
|
17
17
|
/** Every known event kind. Derived from {@link EVENT_TYPE_TABLE}, never hand-listed. */
|
|
18
18
|
export declare const EVENT_TYPES: readonly EventType[];
|
|
19
19
|
/** Runtime guard for an event kind arriving from outside this process. */
|
package/dist/lib/feed/events.js
CHANGED
|
@@ -121,6 +121,7 @@ const FILE_MODE = 0o600;
|
|
|
121
121
|
const EVENT_TYPE_TABLE = {
|
|
122
122
|
'agent.run.start': true, 'agent.run.end': true, 'agent.spawn.start': true, 'agent.spawn.end': true,
|
|
123
123
|
'run.dispatched': true,
|
|
124
|
+
'run.launch': true,
|
|
124
125
|
'daemon.start': true, 'daemon.stop': true, 'daemon.error': true, 'daemon.info': true,
|
|
125
126
|
'routine.start': true, 'routine.end': true,
|
|
126
127
|
'watchdog.action': true,
|
|
@@ -158,6 +159,10 @@ export function isEventType(value) {
|
|
|
158
159
|
const AUDIT_EVENTS = new Set([
|
|
159
160
|
'command.start', 'command.end',
|
|
160
161
|
'run.dispatched',
|
|
162
|
+
// Sibling of run.dispatched, and the MORE reliable signal for a stuck /
|
|
163
|
+
// logged-out launch (it fires pre-spawn, before an agent can hang at a login
|
|
164
|
+
// screen and never finalize). Must surface under --level audit --include runs.
|
|
165
|
+
'run.launch',
|
|
161
166
|
'secrets.get', 'secrets.unlocked', 'secrets.create', 'secrets.import', 'secrets.export', 'secrets.view', 'secrets.lease-denied', 'secrets.lease-expire',
|
|
162
167
|
'secrets.set', 'secrets.delete', 'secrets.rename',
|
|
163
168
|
'teams.create', 'teams.add', 'teams.start', 'teams.complete', 'teams.disband',
|
|
@@ -21,6 +21,13 @@ export interface FeedSinkConfig {
|
|
|
21
21
|
channel?: string;
|
|
22
22
|
/** Recipient for a `channel` sink. Required unless `channel` is the `owner` alias. */
|
|
23
23
|
to?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Optional channel body template. Uses the same placeholders as `command`
|
|
26
|
+
* argv (`{message}`, `{ticket}`, `{project}`, ...). Defaults to `{message}`.
|
|
27
|
+
* A missing placeholder skips the sink, which lets a `{ticket}` template
|
|
28
|
+
* declare that only ticket-backed posts belong in that destination.
|
|
29
|
+
*/
|
|
30
|
+
message?: string;
|
|
24
31
|
/** Lowest post level that reaches this sink. Defaults to `milestone` (all posts). */
|
|
25
32
|
minLevel?: FeedPostLevel;
|
|
26
33
|
}
|
|
@@ -150,6 +157,8 @@ export declare function composeBroadcastMessage(ctx: FeedBroadcastContext): stri
|
|
|
150
157
|
* would otherwise comment on nothing.
|
|
151
158
|
*/
|
|
152
159
|
export declare function renderSinkArgv(template: string[], ctx: FeedBroadcastContext): string[] | undefined;
|
|
160
|
+
/** Render one channel-message template with the same fail-closed placeholder contract as argv. */
|
|
161
|
+
export declare function renderSinkMessage(template: string, ctx: FeedBroadcastContext): string | undefined;
|
|
153
162
|
/**
|
|
154
163
|
* Which sinks this post reaches, in config order. Pure — the dry-run listing and
|
|
155
164
|
* the real fan-out plan through here, so what `--dry-run` shows is what runs.
|
|
@@ -38,6 +38,7 @@ import { isOwnerAlias, readOwnerDest, resolveSendEnvelope, deliverEnvelope } fro
|
|
|
38
38
|
import { lookupTransport } from './channels/resolve.js';
|
|
39
39
|
import { registerBuiltinProviders } from './channels/providers/index.js';
|
|
40
40
|
import { sendToOwner } from './notify.js';
|
|
41
|
+
import { forwardOwnerNotifyToPeer } from './channels/owner-forward.js';
|
|
41
42
|
const LEVEL_RANK = { milestone: 0, important: 1 };
|
|
42
43
|
/** Parse a `--level` value; anything unrecognized is a usage error, not a default. */
|
|
43
44
|
export function parseFeedPostLevel(raw) {
|
|
@@ -324,6 +325,23 @@ export function renderSinkArgv(template, ctx) {
|
|
|
324
325
|
}
|
|
325
326
|
return argv.length > 0 ? argv : undefined;
|
|
326
327
|
}
|
|
328
|
+
/** Render one channel-message template with the same fail-closed placeholder contract as argv. */
|
|
329
|
+
export function renderSinkMessage(template, ctx) {
|
|
330
|
+
const vars = templateVars(ctx);
|
|
331
|
+
let missing = false;
|
|
332
|
+
const rendered = template.replace(PLACEHOLDER, (whole, key) => {
|
|
333
|
+
const value = vars[key];
|
|
334
|
+
if (value === undefined || value === '') {
|
|
335
|
+
missing = true;
|
|
336
|
+
return whole;
|
|
337
|
+
}
|
|
338
|
+
return value;
|
|
339
|
+
});
|
|
340
|
+
if (missing)
|
|
341
|
+
return undefined;
|
|
342
|
+
const text = rendered.trim();
|
|
343
|
+
return text || undefined;
|
|
344
|
+
}
|
|
327
345
|
/**
|
|
328
346
|
* Which sinks this post reaches, in config order. Pure — the dry-run listing and
|
|
329
347
|
* the real fan-out plan through here, so what `--dry-run` shows is what runs.
|
|
@@ -350,11 +368,14 @@ export function planFeedBroadcast(config, ctx) {
|
|
|
350
368
|
// placeholder below).
|
|
351
369
|
if (!isOwnerAlias(channel) && !sink.to?.trim())
|
|
352
370
|
continue;
|
|
371
|
+
const text = renderSinkMessage(sink.message ?? '{message}', ctx);
|
|
372
|
+
if (!text)
|
|
373
|
+
continue;
|
|
353
374
|
planned.push({
|
|
354
375
|
name,
|
|
355
376
|
channel,
|
|
356
377
|
to: isOwnerAlias(channel) ? undefined : sink.to.trim(),
|
|
357
|
-
text
|
|
378
|
+
text,
|
|
358
379
|
});
|
|
359
380
|
continue;
|
|
360
381
|
}
|
|
@@ -480,6 +501,14 @@ async function runChannelSink(sink, meta) {
|
|
|
480
501
|
const result = await deliverEnvelope(resolved.envelope, meta);
|
|
481
502
|
if (result.ok)
|
|
482
503
|
return { name, ok: true };
|
|
504
|
+
// Explicit rush-backed channel sinks need the same cross-device handoff as
|
|
505
|
+
// the owner alias. Linux workers do not carry the macOS Keychain-bound Rush
|
|
506
|
+
// transport, but feed posts originate on those workers routinely. Keep the
|
|
507
|
+
// destination explicit so the peer delivers exactly this sink once instead
|
|
508
|
+
// of expanding the owner's multi-channel policy.
|
|
509
|
+
const forwarded = await forwardOwnerNotifyToPeer(resolved.envelope.text, resolved.envelope.channel, resolved.envelope.to, meta);
|
|
510
|
+
if (forwarded?.ok)
|
|
511
|
+
return { name, ok: true };
|
|
483
512
|
return { name, ok: false, error: result.error };
|
|
484
513
|
}
|
|
485
514
|
/**
|
|
@@ -17,11 +17,12 @@
|
|
|
17
17
|
/**
|
|
18
18
|
* Where the account backend lives. Config, never a literal at a call site.
|
|
19
19
|
*
|
|
20
|
-
* The default is
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
20
|
+
* The default is Phoenix ID's branded custom domain. The legacy workers.dev
|
|
21
|
+
* hostname remains live for already-released clients, but new clients and
|
|
22
|
+
* managed Workers must share this canonical base so token verification cannot
|
|
23
|
+
* drift across the release boundary.
|
|
24
24
|
*/
|
|
25
|
+
export declare const DEFAULT_PHOENIX_ID_BASE = "https://id.byphoenix.com";
|
|
25
26
|
export declare const PHOENIX_ID_BASE: string;
|
|
26
27
|
/** Our own session file. agents-cli never reads another product's credentials. */
|
|
27
28
|
export declare function sessionFilePath(): string;
|
|
@@ -20,12 +20,13 @@ import { getRuntimeStateDir } from '../state.js';
|
|
|
20
20
|
/**
|
|
21
21
|
* Where the account backend lives. Config, never a literal at a call site.
|
|
22
22
|
*
|
|
23
|
-
* The default is
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
23
|
+
* The default is Phoenix ID's branded custom domain. The legacy workers.dev
|
|
24
|
+
* hostname remains live for already-released clients, but new clients and
|
|
25
|
+
* managed Workers must share this canonical base so token verification cannot
|
|
26
|
+
* drift across the release boundary.
|
|
27
27
|
*/
|
|
28
|
-
export const
|
|
28
|
+
export const DEFAULT_PHOENIX_ID_BASE = 'https://id.byphoenix.com';
|
|
29
|
+
export const PHOENIX_ID_BASE = process.env.PHOENIX_ID_BASE ?? DEFAULT_PHOENIX_ID_BASE;
|
|
29
30
|
/** Our own session file. agents-cli never reads another product's credentials. */
|
|
30
31
|
export function sessionFilePath() {
|
|
31
32
|
return path.join(getRuntimeStateDir(), 'phoenix-session.json');
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Resource inventory — the single chokepoint for "what does this agent@version
|
|
3
|
-
* have" (RUSH-2238, parent RUSH-2236
|
|
3
|
+
* have" (RUSH-2238, parent RUSH-2236). Contract: cli/docs/specifications.md.
|
|
4
4
|
*
|
|
5
5
|
* One API answers four orthogonal questions per (agent, version, kind):
|
|
6
6
|
*
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Resource inventory — the single chokepoint for "what does this agent@version
|
|
3
|
-
* have" (RUSH-2238, parent RUSH-2236
|
|
3
|
+
* have" (RUSH-2238, parent RUSH-2236). Contract: cli/docs/specifications.md.
|
|
4
4
|
*
|
|
5
5
|
* One API answers four orthogonal questions per (agent, version, kind):
|
|
6
6
|
*
|
|
@@ -55,7 +55,7 @@ export declare function attributedSetLostPids(prev: Set<number>, next: Set<numbe
|
|
|
55
55
|
export declare function filterCachedUnattributed(sessions: ActiveSession[], attributed: Set<number>, alive: (pid: number, startedAtMs?: number) => boolean): ActiveSession[];
|
|
56
56
|
type ActiveContext = 'terminal' | 'teams' | 'cloud' | 'headless';
|
|
57
57
|
/** The SessionMeta fields the live-row backfill reads — the enrichment a running process cannot report. */
|
|
58
|
-
export type BackfillMeta = Pick<SessionMeta, 'version' | 'account' | 'timestamp' | 'label' | 'ticketId' | 'prUrl' | 'prNumber' | 'origin' | 'routineName' | 'harness'>;
|
|
58
|
+
export type BackfillMeta = Pick<SessionMeta, 'version' | 'account' | 'timestamp' | 'label' | 'ticketId' | 'prUrl' | 'prNumber' | 'origin' | 'routineName' | 'harness' | 'tokenCount' | 'durationMs' | 'subAgentCount'>;
|
|
59
59
|
export declare function backfillActiveRowsFromMeta(sessions: ActiveSession[], metaById: Map<string, BackfillMeta>): void;
|
|
60
60
|
export declare function backfillActiveRowsFromIndex(sessions: ActiveSession[]): void;
|
|
61
61
|
export declare function isRunningLiveSession(s: ActiveSession): boolean;
|
|
@@ -205,6 +205,15 @@ export interface ActiveSession {
|
|
|
205
205
|
spawnedTeam?: string;
|
|
206
206
|
/** Files/screenshots attached to the session prompt. */
|
|
207
207
|
attachments?: SessionAttachment[];
|
|
208
|
+
/** Total tokens and elapsed duration from the indexed transcript. */
|
|
209
|
+
tokenCount?: number;
|
|
210
|
+
durationMs?: number;
|
|
211
|
+
/** Number of subagents launched, persisted by the index scanner. */
|
|
212
|
+
subAgentCount?: number;
|
|
213
|
+
/** Durable documents created in the live transcript tail. */
|
|
214
|
+
artifacts?: import('./highlights.js').ProducedArtifact[];
|
|
215
|
+
/** Created plan document singled out for plan-first consumers. */
|
|
216
|
+
planFile?: string;
|
|
208
217
|
sessionFile?: string;
|
|
209
218
|
startedAtMs?: number;
|
|
210
219
|
/**
|
|
@@ -164,6 +164,12 @@ export function backfillActiveRowsFromMeta(sessions, metaById) {
|
|
|
164
164
|
s.routineName = m.routineName;
|
|
165
165
|
if (!s.harness && m.harness)
|
|
166
166
|
s.harness = m.harness;
|
|
167
|
+
if (s.tokenCount == null && m.tokenCount != null)
|
|
168
|
+
s.tokenCount = m.tokenCount;
|
|
169
|
+
if (s.durationMs == null && m.durationMs != null)
|
|
170
|
+
s.durationMs = m.durationMs;
|
|
171
|
+
if (s.subAgentCount == null && m.subAgentCount != null)
|
|
172
|
+
s.subAgentCount = m.subAgentCount;
|
|
167
173
|
}
|
|
168
174
|
}
|
|
169
175
|
function loadBackfillMetaFor(sessions) {
|
|
@@ -851,6 +857,8 @@ function applyState(base, state, fallbackFile, pidAlive) {
|
|
|
851
857
|
createdTickets: state.createdTickets,
|
|
852
858
|
spawnedTeam: state.spawnedTeam,
|
|
853
859
|
attachments: state.attachments,
|
|
860
|
+
artifacts: state.artifacts,
|
|
861
|
+
planFile: state.planFile,
|
|
854
862
|
rateLimited: state.rateLimited,
|
|
855
863
|
};
|
|
856
864
|
}
|
|
@@ -98,9 +98,15 @@ export function classifyFileChanges(events) {
|
|
|
98
98
|
deleted.delete(p); // a write after a delete recreates the file
|
|
99
99
|
}
|
|
100
100
|
else if (EDIT_TOOLS.has(tool)) {
|
|
101
|
-
|
|
101
|
+
if (args.patch_op === 'Add')
|
|
102
|
+
created.add(p);
|
|
103
|
+
else if (args.patch_op === 'Delete')
|
|
104
|
+
deleted.add(p);
|
|
105
|
+
else
|
|
106
|
+
modified.add(p);
|
|
102
107
|
seen.add(p);
|
|
103
|
-
|
|
108
|
+
if (args.patch_op !== 'Delete')
|
|
109
|
+
deleted.delete(p);
|
|
104
110
|
}
|
|
105
111
|
}
|
|
106
112
|
const out = [];
|
|
@@ -70,6 +70,10 @@ export declare function parseCodex(filePath: string): SessionEvent[];
|
|
|
70
70
|
* Returns every path in order (a multi-file patch emits multiple paths so
|
|
71
71
|
* artifact discovery sees each file — RUSH-1410). Empty when unparseable.
|
|
72
72
|
*/
|
|
73
|
+
export declare function applyPatchTargets(input: string): Array<{
|
|
74
|
+
path: string;
|
|
75
|
+
op: 'Add' | 'Update' | 'Delete';
|
|
76
|
+
}>;
|
|
73
77
|
export declare function applyPatchTargetPaths(input: string): string[];
|
|
74
78
|
/**
|
|
75
79
|
* Parse Codex JSONL *content* (already read into a string) into normalized
|
|
@@ -555,15 +555,18 @@ export function parseCodex(filePath) {
|
|
|
555
555
|
* Returns every path in order (a multi-file patch emits multiple paths so
|
|
556
556
|
* artifact discovery sees each file — RUSH-1410). Empty when unparseable.
|
|
557
557
|
*/
|
|
558
|
-
export function
|
|
559
|
-
const
|
|
560
|
-
const re = /^\*\*\* (
|
|
558
|
+
export function applyPatchTargets(input) {
|
|
559
|
+
const targets = [];
|
|
560
|
+
const re = /^\*\*\* (Update|Add|Delete) File: (.+)$/gm;
|
|
561
561
|
for (const m of input.matchAll(re)) {
|
|
562
|
-
const p = m[
|
|
562
|
+
const p = m[2].trim();
|
|
563
563
|
if (p)
|
|
564
|
-
|
|
564
|
+
targets.push({ path: p, op: m[1] });
|
|
565
565
|
}
|
|
566
|
-
return
|
|
566
|
+
return targets;
|
|
567
|
+
}
|
|
568
|
+
export function applyPatchTargetPaths(input) {
|
|
569
|
+
return applyPatchTargets(input).map((target) => target.path);
|
|
567
570
|
}
|
|
568
571
|
/** @deprecated Prefer applyPatchTargetPaths — kept for single-file call sites. */
|
|
569
572
|
function applyPatchTargetPath(input) {
|
|
@@ -724,13 +727,15 @@ export function parseCodexContent(content) {
|
|
|
724
727
|
const execCommand = rawName === 'exec' ? codexExecCommand(input) : undefined;
|
|
725
728
|
// Multi-file patches: one tool_use per file so artifact discovery sees
|
|
726
729
|
// every path (RUSH-1410). Single-file / non-patch keep one event.
|
|
727
|
-
const
|
|
730
|
+
const patchTargets = isApplyPatch ? applyPatchTargets(input) : [];
|
|
728
731
|
const tool = isApplyPatch ? 'Edit' : rawName;
|
|
729
732
|
const truncatedInput = input.length > 500 ? input.slice(0, 497) + '...' : input;
|
|
730
|
-
const emitOne = (patchPath) => {
|
|
733
|
+
const emitOne = (patchPath, patchOp) => {
|
|
731
734
|
const args = { input: truncatedInput };
|
|
732
735
|
if (patchPath)
|
|
733
736
|
args.file_path = patchPath;
|
|
737
|
+
if (patchOp)
|
|
738
|
+
args.patch_op = patchOp;
|
|
734
739
|
if (execCommand)
|
|
735
740
|
args.command = execCommand;
|
|
736
741
|
const callId = payload.call_id || payload.id;
|
|
@@ -747,9 +752,9 @@ export function parseCodexContent(content) {
|
|
|
747
752
|
path: patchPath,
|
|
748
753
|
});
|
|
749
754
|
};
|
|
750
|
-
if (isApplyPatch &&
|
|
751
|
-
for (const
|
|
752
|
-
emitOne(
|
|
755
|
+
if (isApplyPatch && patchTargets.length > 0) {
|
|
756
|
+
for (const target of patchTargets)
|
|
757
|
+
emitOne(target.path, target.op);
|
|
753
758
|
}
|
|
754
759
|
else {
|
|
755
760
|
emitOne(undefined);
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* shape + mtime — same function, driven off the normalized events.
|
|
16
16
|
*/
|
|
17
17
|
import type { SessionAttachment, SessionEvent, TodoItem, TodoProgress } from './types.js';
|
|
18
|
+
import { type ProducedArtifact } from './highlights.js';
|
|
18
19
|
export type { TodoItem, TodoProgress };
|
|
19
20
|
export type SessionActivity = 'working' | 'waiting_input' | 'idle';
|
|
20
21
|
export type AwaitingReason = 'question' | 'plan_review' | 'permission';
|
|
@@ -95,6 +96,10 @@ export interface SessionState {
|
|
|
95
96
|
* checklist, notably for remote/device-dispatched agents with no local stream.
|
|
96
97
|
*/
|
|
97
98
|
todos?: TodoProgress;
|
|
99
|
+
/** Durable documents created in the bounded transcript slice. */
|
|
100
|
+
artifacts?: ProducedArtifact[];
|
|
101
|
+
/** First created plan document, for consumers that give plans special treatment. */
|
|
102
|
+
planFile?: string;
|
|
98
103
|
/** Last few assistant turns (most-recent last), one line each — panel context. */
|
|
99
104
|
tail?: string[];
|
|
100
105
|
lastActivityMs?: number;
|
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
import * as path from 'path';
|
|
18
18
|
import { isCompletedTodoStatus, SNAPSHOT_TODO_TOOLS, summarizeToolUse } from './parse.js';
|
|
19
19
|
import { isShellExecTool } from './shell-programs.js';
|
|
20
|
+
import { classifyFileChanges } from './digest.js';
|
|
21
|
+
import { extractArtifacts } from './highlights.js';
|
|
20
22
|
/**
|
|
21
23
|
* Detect per-session rate-limit / usage-limit signals in assistant or error
|
|
22
24
|
* text (RUSH-1523). Matches the same shapes the ext's prewarm detectBlockingPrompt
|
|
@@ -626,6 +628,8 @@ export function inferSessionState(events, ctx = {}) {
|
|
|
626
628
|
const state = inferActivity(events, ctx);
|
|
627
629
|
const { pr, ticket, createdTickets, spawnedTeam, attachments } = detectDurableSignals(events);
|
|
628
630
|
const worktree = detectWorktree(ctx.cwd, ctx.gitBranch);
|
|
631
|
+
const artifacts = extractArtifacts(classifyFileChanges(events));
|
|
632
|
+
const planFile = artifacts.find((artifact) => artifact.bucket === 'plans')?.path;
|
|
629
633
|
// Rate-limit: scan the most recent assistant messages + tool errors (tail-first).
|
|
630
634
|
let rateLimited = false;
|
|
631
635
|
for (let i = events.length - 1; i >= 0 && i >= events.length - 12; i--) {
|
|
@@ -651,6 +655,8 @@ export function inferSessionState(events, ctx = {}) {
|
|
|
651
655
|
createdTickets,
|
|
652
656
|
spawnedTeam,
|
|
653
657
|
attachments,
|
|
658
|
+
artifacts: artifacts.length > 0 ? artifacts : undefined,
|
|
659
|
+
planFile,
|
|
654
660
|
rateLimited: rateLimited || undefined,
|
|
655
661
|
};
|
|
656
662
|
}
|