@phnx-labs/agents-cli 1.22.25 → 1.22.27
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 +427 -0
- package/README.md +35 -3
- package/dist/bin/agents +0 -0
- package/dist/browser.js +14 -4
- package/dist/commands/apply.js +52 -8
- package/dist/commands/bench.d.ts +2 -0
- package/dist/commands/bench.js +101 -0
- package/dist/commands/browser.js +35 -0
- package/dist/commands/doctor.js +32 -10
- package/dist/commands/exec.js +17 -16
- package/dist/commands/insights.d.ts +25 -19
- package/dist/commands/insights.js +146 -38
- package/dist/commands/output.js +100 -25
- package/dist/commands/reconnect.d.ts +46 -0
- package/dist/commands/reconnect.js +109 -0
- package/dist/commands/resume.d.ts +11 -0
- package/dist/commands/resume.js +51 -0
- package/dist/commands/routines.js +2 -2
- package/dist/commands/secrets.d.ts +2 -8
- package/dist/commands/secrets.js +29 -105
- package/dist/commands/sessions-picker.js +12 -7
- package/dist/commands/sessions.d.ts +47 -1
- package/dist/commands/sessions.js +349 -37
- package/dist/commands/setup-secrets.d.ts +1 -0
- package/dist/commands/setup-secrets.js +1 -1
- package/dist/commands/setup.d.ts +26 -3
- package/dist/commands/setup.js +105 -46
- package/dist/commands/ssh.js +184 -11
- package/dist/commands/teams.d.ts +6 -0
- package/dist/commands/teams.js +46 -3
- package/dist/commands/trends.d.ts +8 -0
- package/dist/commands/trends.js +10 -156
- package/dist/index.js +17 -21
- package/dist/lib/agents.d.ts +11 -0
- package/dist/lib/agents.js +29 -2
- package/dist/lib/analytics/dashboard.d.ts +10 -6
- package/dist/lib/analytics/dashboard.js +6 -4
- package/dist/lib/analytics/mix-commands.d.ts +53 -0
- package/dist/lib/analytics/mix-commands.js +229 -0
- package/dist/lib/analytics/recipes.d.ts +19 -14
- package/dist/lib/analytics/recipes.js +4 -2
- package/dist/lib/auth-health.d.ts +47 -3
- package/dist/lib/auth-health.js +74 -15
- package/dist/lib/bench/index.d.ts +4 -0
- package/dist/lib/bench/index.js +4 -0
- package/dist/lib/bench/runner.d.ts +16 -0
- package/dist/lib/bench/runner.js +111 -0
- package/dist/lib/bench/schema.d.ts +5 -0
- package/dist/lib/bench/schema.js +91 -0
- package/dist/lib/bench/storage.d.ts +5 -0
- package/dist/lib/bench/storage.js +32 -0
- package/dist/lib/bench/types.d.ts +40 -0
- package/dist/lib/bench/types.js +1 -0
- package/dist/lib/browser/ipc.d.ts +26 -0
- package/dist/lib/browser/ipc.js +139 -24
- package/dist/lib/browser/profiles.d.ts +11 -0
- package/dist/lib/browser/profiles.js +1 -1
- package/dist/lib/browser/stream.d.ts +14 -0
- package/dist/lib/browser/stream.js +71 -0
- package/dist/lib/channels/owner-sink.d.ts +27 -0
- package/dist/lib/channels/owner-sink.js +93 -0
- package/dist/lib/claude-account-token.d.ts +2 -0
- package/dist/lib/claude-account-token.js +27 -1
- package/dist/lib/crabbox/cli.d.ts +2 -0
- package/dist/lib/crabbox/cli.js +2 -0
- package/dist/lib/crabbox/lease.js +7 -1
- package/dist/lib/daemon.js +20 -0
- package/dist/lib/devices/connect.d.ts +2 -0
- package/dist/lib/devices/connect.js +7 -0
- package/dist/lib/devices/doctor-findings.d.ts +10 -1
- package/dist/lib/devices/doctor-findings.js +47 -1
- package/dist/lib/devices/harness-inventory.d.ts +97 -0
- package/dist/lib/devices/harness-inventory.js +0 -0
- package/dist/lib/devices/registry.d.ts +2 -0
- package/dist/lib/devices/ssh-config.js +3 -0
- package/dist/lib/devices/windows-ssh-enrollment.d.ts +20 -0
- package/dist/lib/devices/windows-ssh-enrollment.js +98 -0
- package/dist/lib/exec.bench.d.ts +1 -0
- package/dist/lib/exec.bench.js +186 -0
- package/dist/lib/exec.js +18 -8
- package/dist/lib/fleet/apply.d.ts +59 -3
- package/dist/lib/fleet/apply.js +191 -12
- package/dist/lib/fleet/remote-login.d.ts +4 -3
- package/dist/lib/fleet/remote-login.js +11 -9
- package/dist/lib/fleet/types.d.ts +21 -2
- package/dist/lib/gemini-settings.d.ts +0 -1
- package/dist/lib/gemini-settings.js +12 -7
- package/dist/lib/hooks/cache.js +15 -0
- package/dist/lib/hooks.d.ts +8 -5
- package/dist/lib/hooks.js +10 -6
- package/dist/lib/hosts/dispatch.js +7 -3
- package/dist/lib/hosts/passthrough.d.ts +45 -0
- package/dist/lib/hosts/passthrough.js +59 -8
- package/dist/lib/hosts/progress.d.ts +2 -1
- package/dist/lib/hosts/progress.js +4 -3
- package/dist/lib/hosts/providers/devices.js +1 -0
- package/dist/lib/hosts/ready.d.ts +6 -2
- package/dist/lib/hosts/ready.js +33 -8
- package/dist/lib/hosts/reconcile.d.ts +1 -1
- package/dist/lib/hosts/reconcile.js +16 -7
- package/dist/lib/hosts/reconnect.d.ts +56 -21
- package/dist/lib/hosts/reconnect.js +128 -71
- package/dist/lib/hosts/registry.d.ts +2 -2
- package/dist/lib/hosts/registry.js +3 -5
- package/dist/lib/hosts/remote-cmd.d.ts +17 -0
- package/dist/lib/hosts/remote-cmd.js +29 -0
- package/dist/lib/hosts/tasks.d.ts +2 -0
- package/dist/lib/hosts/types.d.ts +1 -0
- package/dist/lib/hosts/types.js +3 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +9 -6
- package/dist/lib/menubar/install-menubar.js +20 -9
- package/dist/lib/models.d.ts +30 -1
- package/dist/lib/models.js +42 -144
- package/dist/lib/pricing/cost.d.ts +9 -0
- package/dist/lib/pricing/cost.js +24 -0
- package/dist/lib/pricing/index.d.ts +1 -1
- package/dist/lib/pricing/index.js +1 -1
- package/dist/lib/redact.js +8 -3
- package/dist/lib/remote-agents-json.d.ts +32 -0
- package/dist/lib/remote-agents-json.js +47 -16
- package/dist/lib/resource-profiles.js +1 -2
- package/dist/lib/routine-notify-owner.d.ts +102 -0
- package/dist/lib/routine-notify-owner.js +232 -0
- package/dist/lib/routines.d.ts +10 -0
- package/dist/lib/routines.js +14 -2
- package/dist/lib/runner.d.ts +9 -4
- package/dist/lib/runner.js +182 -30
- package/dist/lib/sandbox.d.ts +0 -2
- package/dist/lib/sandbox.js +2 -19
- 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/agent.js +3 -2
- package/dist/lib/secrets/lease.d.ts +25 -0
- package/dist/lib/secrets/lease.js +44 -0
- package/dist/lib/secrets/push.d.ts +94 -0
- package/dist/lib/secrets/push.js +145 -0
- package/dist/lib/secrets/reaper.d.ts +15 -1
- package/dist/lib/secrets/reaper.js +30 -3
- package/dist/lib/self-update.d.ts +20 -5
- package/dist/lib/self-update.js +93 -16
- package/dist/lib/session/db.d.ts +38 -3
- package/dist/lib/session/db.js +273 -16
- package/dist/lib/session/discover.d.ts +22 -1
- package/dist/lib/session/discover.js +239 -51
- package/dist/lib/session/insights.d.ts +37 -0
- package/dist/lib/session/insights.js +219 -9
- package/dist/lib/session/recovery.d.ts +17 -1
- package/dist/lib/session/recovery.js +111 -4
- package/dist/lib/session/remote-list.d.ts +10 -9
- package/dist/lib/session/remote-list.js +25 -23
- package/dist/lib/session/resume-owner.d.ts +55 -0
- package/dist/lib/session/resume-owner.js +69 -0
- package/dist/lib/session/team-filter.d.ts +65 -0
- package/dist/lib/session/team-filter.js +98 -3
- package/dist/lib/session/tool-index.js +133 -22
- package/dist/lib/session/tool-store.d.ts +26 -2
- package/dist/lib/session/tool-store.js +36 -17
- package/dist/lib/session/types.d.ts +33 -0
- package/dist/lib/smart-launch.d.ts +31 -5
- package/dist/lib/smart-launch.js +43 -7
- package/dist/lib/ssh-exec.d.ts +27 -0
- package/dist/lib/ssh-exec.js +42 -3
- package/dist/lib/ssh-tunnel.d.ts +3 -2
- package/dist/lib/ssh-tunnel.js +25 -16
- package/dist/lib/startup/command-registry.d.ts +2 -0
- package/dist/lib/startup/command-registry.js +6 -0
- package/dist/lib/teams/agents.d.ts +13 -0
- package/dist/lib/teams/agents.js +75 -7
- package/dist/lib/teams/placement-probe.d.ts +21 -0
- package/dist/lib/teams/placement-probe.js +135 -0
- package/dist/lib/teams/scheduler.d.ts +74 -1
- package/dist/lib/teams/scheduler.js +187 -10
- package/dist/lib/tmux/session.d.ts +8 -0
- package/dist/lib/tmux/session.js +22 -0
- package/dist/lib/types.d.ts +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Owner phone notification on routine FAILURE (RUSH-2288).
|
|
3
|
+
*
|
|
4
|
+
* The desktop lifecycle notifications (routine-notify.ts) never leave the
|
|
5
|
+
* machine, so a failed scheduled routine on a headless fleet box was invisible
|
|
6
|
+
* until someone happened to look. The per-routine prompt pattern (a prompt that
|
|
7
|
+
* ends by shelling `agents notify`) does not close the gap either: when the
|
|
8
|
+
* routine's OWN agent fails to spawn (`auth_failed`) that prompt never runs, so
|
|
9
|
+
* the one failure most worth surfacing is the one that goes unheard.
|
|
10
|
+
*
|
|
11
|
+
* So the DAEMON delivers on failure, from the SAME owner channel stack `agents
|
|
12
|
+
* notify` uses (humans.yaml owner channels, or the legacy `notify.owner`),
|
|
13
|
+
* calling the channel providers IN-PROCESS — no shelling out to `ssh mac-mini
|
|
14
|
+
* agents notify`. When the primary owner channel cannot deliver from this box it
|
|
15
|
+
* walks the remaining configured owner channels as fallbacks (OpenClaw, a second
|
|
16
|
+
* channel, …). Telegram is never used.
|
|
17
|
+
*
|
|
18
|
+
* Only failures notify. A green routine of any kind stays silent — this is an
|
|
19
|
+
* ADDITIONAL failures-only lane, and the desktop thresholds (which still ping a
|
|
20
|
+
* green agent/workflow finish on the local screen) are unchanged. Delivery is
|
|
21
|
+
* deduped per job+runId so a run is announced to the owner at most once.
|
|
22
|
+
*
|
|
23
|
+
* The message BUILDERS (`routineFinishOwnerText` / `routineStartFailedOwnerText`)
|
|
24
|
+
* are pure and unit-tested; `notifyOwnerRoutineFinish` /
|
|
25
|
+
* `notifyOwnerRoutineStartFailed` are the daemon glue that reads config and
|
|
26
|
+
* delivers.
|
|
27
|
+
*/
|
|
28
|
+
import * as os from 'os';
|
|
29
|
+
import { routineKind } from './routine-notify.js';
|
|
30
|
+
import { readMeta } from './state.js';
|
|
31
|
+
import { getOwnerFromHumans } from './humans.js';
|
|
32
|
+
import { readOwnerDest } from './channels/send.js';
|
|
33
|
+
import { registerBuiltinProviders } from './channels/providers/index.js';
|
|
34
|
+
import { lookupTransport } from './channels/resolve.js';
|
|
35
|
+
/** Transports that are Telegram in any form — never delivered to (owner rule). */
|
|
36
|
+
const TELEGRAM_TRANSPORTS = new Set(['telegram', 'openclaw-telegram']);
|
|
37
|
+
/** Human label for the routine body ("agent claude", "workflow deploy", "command"). */
|
|
38
|
+
function routineLabel(r) {
|
|
39
|
+
const kind = routineKind(r);
|
|
40
|
+
if (kind === 'command')
|
|
41
|
+
return 'command';
|
|
42
|
+
if (kind === 'workflow')
|
|
43
|
+
return `workflow ${r.workflow}`;
|
|
44
|
+
return `agent ${r.agent ?? 'unknown'}`;
|
|
45
|
+
}
|
|
46
|
+
/** The failure reason line for a terminal run: timeout / error message / exit code. */
|
|
47
|
+
function failureReason(meta) {
|
|
48
|
+
if (meta.status === 'timeout')
|
|
49
|
+
return 'Timed out';
|
|
50
|
+
if (meta.errorMessage)
|
|
51
|
+
return meta.errorMessage;
|
|
52
|
+
return `Exited with code ${meta.exitCode ?? '?'}`;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Owner-phone text for a routine FINISH, or null when the run did NOT fail. Only
|
|
56
|
+
* `failed` and `timeout` are failures worth an owner ping — a `completed` run
|
|
57
|
+
* (any kind) is silent, and `running`/`missed` never reach this path from the
|
|
58
|
+
* daemon's finish hook. The text is a short, phone-sized pointer: what failed,
|
|
59
|
+
* why, and on which box (so the owner knows where to look), well under the
|
|
60
|
+
* `user-message-guard` length ceiling.
|
|
61
|
+
*/
|
|
62
|
+
export function routineFinishOwnerText(meta, host) {
|
|
63
|
+
if (meta.status !== 'failed' && meta.status !== 'timeout')
|
|
64
|
+
return null;
|
|
65
|
+
return `Routine failed: ${meta.jobName}\n${failureReason(meta)}\n${routineLabel(meta)} · ${host}`;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Owner-phone text for a routine that failed to even START (the daemon's
|
|
69
|
+
* pre-spawn catch — `executeJobDetached` threw before a child existed, e.g.
|
|
70
|
+
* `auth_failed`). Never null: a broken start is always a failure worth the ping,
|
|
71
|
+
* for every routine kind, and it is exactly the case the per-routine `agents
|
|
72
|
+
* notify` prompt can never cover because that prompt never ran.
|
|
73
|
+
*/
|
|
74
|
+
export function routineStartFailedOwnerText(config, error, host) {
|
|
75
|
+
return `Routine failed to start: ${config.name}\n${error}\n${routineLabel(config)} · ${host}`;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* True when a channel would deliver over Telegram. Checks, in order:
|
|
79
|
+
* 1. the channel id itself (`telegram`);
|
|
80
|
+
* 2. the humans.yaml `transport` field on the channel entry (when provided);
|
|
81
|
+
* 3. `notify.transports[id]` remapping (or the id as the default transport name).
|
|
82
|
+
* Any of those landing on a Telegram-delivering provider is excluded.
|
|
83
|
+
*/
|
|
84
|
+
function isTelegramChannel(channelId, meta, humansTransport) {
|
|
85
|
+
if (channelId.trim().toLowerCase() === 'telegram')
|
|
86
|
+
return true;
|
|
87
|
+
if (humansTransport && TELEGRAM_TRANSPORTS.has(humansTransport.trim().toLowerCase())) {
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
const transport = meta.notify?.transports?.[channelId] ?? channelId;
|
|
91
|
+
return TELEGRAM_TRANSPORTS.has(transport.trim().toLowerCase());
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Ordered owner delivery plan for a failure ping: the primary owner destination
|
|
95
|
+
* (the same one `agents notify` resolves) first, then every other configured
|
|
96
|
+
* owner channel as a fallback. Deduped by (channel, to). Telegram channels and
|
|
97
|
+
* intrusive channels (a voice call is too much for a routine failure) are
|
|
98
|
+
* excluded entirely — so an owner whose ONLY channel is Telegram gets an empty
|
|
99
|
+
* plan and no ping, which is the intended "silence beats Telegram" outcome.
|
|
100
|
+
*
|
|
101
|
+
* The same filters apply to the primary and the fallback list — a policy that
|
|
102
|
+
* points normal severity at a voice channel must not auto-call the owner on
|
|
103
|
+
* every routine failure.
|
|
104
|
+
*/
|
|
105
|
+
export function ownerFailureDeliveryPlan(meta) {
|
|
106
|
+
const plan = [];
|
|
107
|
+
const seen = new Set();
|
|
108
|
+
const ownerChannels = getOwnerFromHumans()?.channels ?? [];
|
|
109
|
+
const byId = new Map(ownerChannels.map((ch) => [ch.id, ch]));
|
|
110
|
+
const push = (channel, to) => {
|
|
111
|
+
const c = channel?.trim();
|
|
112
|
+
const t = to?.trim();
|
|
113
|
+
if (!c || !t)
|
|
114
|
+
return;
|
|
115
|
+
const entry = byId.get(c);
|
|
116
|
+
if (entry?.intrusive)
|
|
117
|
+
return;
|
|
118
|
+
if (isTelegramChannel(c, meta, entry?.transport))
|
|
119
|
+
return;
|
|
120
|
+
const key = `${c} ${t}`;
|
|
121
|
+
if (seen.has(key))
|
|
122
|
+
return;
|
|
123
|
+
seen.add(key);
|
|
124
|
+
plan.push({ channel: c, to: t });
|
|
125
|
+
};
|
|
126
|
+
// Primary = the destination `agents notify` would resolve (humans policy →
|
|
127
|
+
// first addressable → legacy notify.owner). Same filters as the fallbacks.
|
|
128
|
+
const primary = readOwnerDest(meta);
|
|
129
|
+
if (primary)
|
|
130
|
+
push(primary.channel, primary.to);
|
|
131
|
+
// Fallbacks = the remaining configured owner channels, in declared order.
|
|
132
|
+
for (const ch of ownerChannels) {
|
|
133
|
+
push(ch.id, ch.to);
|
|
134
|
+
}
|
|
135
|
+
return plan;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Deliver `text` to the owner over the failure plan, in-process, trying each
|
|
139
|
+
* channel until one accepts. Uses the non-dying `lookupTransport` (never
|
|
140
|
+
* `resolveTransport`, which `die()`s the process) so a daemon survives a bad
|
|
141
|
+
* channel name. Best-effort by contract: the daemon wraps this in try/catch, but
|
|
142
|
+
* it also never throws for a delivery failure — it returns the attempt log.
|
|
143
|
+
*/
|
|
144
|
+
export async function deliverOwnerFailure(text, meta) {
|
|
145
|
+
const plan = ownerFailureDeliveryPlan(meta);
|
|
146
|
+
const attempts = [];
|
|
147
|
+
if (plan.length === 0)
|
|
148
|
+
return { delivered: false, attempts };
|
|
149
|
+
registerBuiltinProviders();
|
|
150
|
+
for (const dest of plan) {
|
|
151
|
+
const { provider, error } = lookupTransport(dest.channel, meta);
|
|
152
|
+
if (!provider) {
|
|
153
|
+
attempts.push({ channel: dest.channel, ok: false, error });
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
const result = await provider.send(text, {
|
|
158
|
+
target: dest.to,
|
|
159
|
+
from: 'routines',
|
|
160
|
+
ownerScoped: true,
|
|
161
|
+
});
|
|
162
|
+
attempts.push({ channel: dest.channel, ok: result.ok, error: result.error });
|
|
163
|
+
if (result.ok)
|
|
164
|
+
return { delivered: true, channel: dest.channel, attempts };
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
attempts.push({ channel: dest.channel, ok: false, error: err.message });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return { delivered: false, attempts };
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Per-process dedup of failure pings, keyed by job+runId (finishes) or the
|
|
174
|
+
* synthetic start-failed key. A run is announced to the owner at most once even
|
|
175
|
+
* if the finish hook fires twice or a later sweep re-finalizes the same record.
|
|
176
|
+
* Bounded so a long-lived daemon never grows it without limit.
|
|
177
|
+
*/
|
|
178
|
+
const notifiedFailures = new Set();
|
|
179
|
+
const MAX_DEDUP_KEYS = 1000;
|
|
180
|
+
/**
|
|
181
|
+
* Claim a dedup key for an in-flight delivery. Returns false when this
|
|
182
|
+
* job+runId was already delivered (or is already in flight). Callers MUST
|
|
183
|
+
* {@link releaseFailureKey} when delivery fails so a later tick can retry —
|
|
184
|
+
* claiming before send without a release would suppress permanent failures
|
|
185
|
+
* forever (RUSH-2288 review).
|
|
186
|
+
*/
|
|
187
|
+
function claimFailureKey(key) {
|
|
188
|
+
if (notifiedFailures.has(key))
|
|
189
|
+
return false;
|
|
190
|
+
if (notifiedFailures.size >= MAX_DEDUP_KEYS)
|
|
191
|
+
notifiedFailures.clear();
|
|
192
|
+
notifiedFailures.add(key);
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
/** Drop a key so a failed delivery can be retried on the next finish hook. */
|
|
196
|
+
function releaseFailureKey(key) {
|
|
197
|
+
notifiedFailures.delete(key);
|
|
198
|
+
}
|
|
199
|
+
/** Test-only: reset the dedup set so cases don't leak state into each other. */
|
|
200
|
+
export function __resetOwnerFailureDedup() {
|
|
201
|
+
notifiedFailures.clear();
|
|
202
|
+
}
|
|
203
|
+
/** A green run / deduped skip attempted no channel. */
|
|
204
|
+
const NO_DELIVERY = { delivered: false, attempts: [] };
|
|
205
|
+
/**
|
|
206
|
+
* Daemon glue: on a routine FINISH, ping the owner IFF the run failed. Green
|
|
207
|
+
* runs return early (no ping). Deduped per job+runId — claimed for the
|
|
208
|
+
* in-flight window so two finish hooks don't double-text, released when no
|
|
209
|
+
* channel accepted so a later sweep can retry. Best-effort.
|
|
210
|
+
*/
|
|
211
|
+
export async function notifyOwnerRoutineFinish(meta) {
|
|
212
|
+
const text = routineFinishOwnerText(meta, os.hostname());
|
|
213
|
+
if (!text)
|
|
214
|
+
return NO_DELIVERY; // green / non-terminal → silent
|
|
215
|
+
const key = `${meta.jobName} ${meta.runId}`;
|
|
216
|
+
if (!claimFailureKey(key))
|
|
217
|
+
return NO_DELIVERY;
|
|
218
|
+
const result = await deliverOwnerFailure(text, readMeta());
|
|
219
|
+
if (!result.delivered)
|
|
220
|
+
releaseFailureKey(key);
|
|
221
|
+
return result;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Daemon glue: on a pre-spawn failure (no run record, so no finish hook and no
|
|
225
|
+
* runId), ping the owner. Not deduped — a pre-spawn failure is one scheduled
|
|
226
|
+
* fire on the cron cadence (never a per-second storm), and each such fire is a
|
|
227
|
+
* distinct failure the owner should hear about. Best-effort.
|
|
228
|
+
*/
|
|
229
|
+
export async function notifyOwnerRoutineStartFailed(config, error) {
|
|
230
|
+
const text = routineStartFailedOwnerText(config, error, os.hostname());
|
|
231
|
+
return deliverOwnerFailure(text, readMeta());
|
|
232
|
+
}
|
package/dist/lib/routines.d.ts
CHANGED
|
@@ -313,6 +313,16 @@ export interface RunMeta {
|
|
|
313
313
|
jobName: string;
|
|
314
314
|
runId: string;
|
|
315
315
|
agent?: AgentId;
|
|
316
|
+
/**
|
|
317
|
+
* Resolved agent version this run launched under. Re-pointed to each failover
|
|
318
|
+
* attempt's version as the single-shot chain advances (runner.ts), so it names
|
|
319
|
+
* the version that actually ran and wrote the transcript. Recorded so
|
|
320
|
+
* `archiveRoutineTranscripts` can find the transcript in the per-version home a
|
|
321
|
+
* config-dir-relocating agent (claude/codex) writes to, and so account
|
|
322
|
+
* attribution can name the home that ran (RUSH-2271). Unset for
|
|
323
|
+
* command/self-updating runs.
|
|
324
|
+
*/
|
|
325
|
+
version?: string;
|
|
316
326
|
workflow?: string;
|
|
317
327
|
/** The shell command that ran, for command-mode routines (no agent). */
|
|
318
328
|
command?: string;
|
package/dist/lib/routines.js
CHANGED
|
@@ -14,7 +14,7 @@ import { getRoutinesDir, getSystemRoutinesDir, getRunsDir, ensureAgentsDir, getP
|
|
|
14
14
|
import { safeJoin, isSafeSegmentName } from './paths.js';
|
|
15
15
|
import { isSafeProjectName } from './projects.js';
|
|
16
16
|
import { atomicWriteFileSync } from './fs-atomic.js';
|
|
17
|
-
import { ALL_AGENT_IDS } from './agents.js';
|
|
17
|
+
import { ALL_AGENT_IDS, ROUTINE_AGENT_IDS } from './agents.js';
|
|
18
18
|
import { machineId, normalizeHost } from './machine-id.js';
|
|
19
19
|
import { resolveActor } from './actor.js';
|
|
20
20
|
import { percentile } from './percentile.js';
|
|
@@ -600,6 +600,7 @@ export function validateJob(config) {
|
|
|
600
600
|
const hasAgent = Boolean(config.agent && typeof config.agent === 'string');
|
|
601
601
|
const hasWorkflow = Boolean(config.workflow && typeof config.workflow === 'string');
|
|
602
602
|
const hasCommand = Boolean(config.command && typeof config.command === 'string');
|
|
603
|
+
const strategy = resolveHostStrategy(config);
|
|
603
604
|
const set = [hasAgent, hasWorkflow, hasCommand].filter(Boolean).length;
|
|
604
605
|
if (set === 0) {
|
|
605
606
|
errors.push('exactly one of agent, workflow, or command is required');
|
|
@@ -613,6 +614,18 @@ export function validateJob(config) {
|
|
|
613
614
|
if (hasAgent && config.agent && !ALL_AGENT_IDS.includes(config.agent)) {
|
|
614
615
|
errors.push(`agent must be one of: ${ALL_AGENT_IDS.join(', ')}`);
|
|
615
616
|
}
|
|
617
|
+
else if (hasAgent && config.agent && strategy === 'local' &&
|
|
618
|
+
!ROUTINE_AGENT_IDS.includes(config.agent)) {
|
|
619
|
+
// The local daemon only knows how to build a command for the agents in
|
|
620
|
+
// ROUTINE_AGENT_IDS (runner.ts's AGENT_COMMANDS table) — anything else is a
|
|
621
|
+
// real, installable agent (it passed the ALL_AGENT_IDS check above) but one
|
|
622
|
+
// the daemon can't fire itself, so reject it now instead of accepting the
|
|
623
|
+
// routine and failing at fire time (runner.ts buildJobCommand: "Unsupported
|
|
624
|
+
// agent for daemon jobs"). host/fleet/cloud placement dispatches through
|
|
625
|
+
// `agents run`/a cloud provider instead of this table, so they're exempt.
|
|
626
|
+
errors.push(`agent '${config.agent}' is not supported by the local routine daemon; use one of: ` +
|
|
627
|
+
`${ROUTINE_AGENT_IDS.join(', ')} (or set hostStrategy: host/fleet/cloud to run it elsewhere)`);
|
|
628
|
+
}
|
|
616
629
|
if (hasWorkflow && config.workflow) {
|
|
617
630
|
if (!/^[a-z0-9][a-z0-9_-]*$/.test(config.workflow)) {
|
|
618
631
|
errors.push('workflow must be a lowercase alphanumeric name (hyphens and underscores allowed, e.g. autodev)');
|
|
@@ -662,7 +675,6 @@ export function validateJob(config) {
|
|
|
662
675
|
errors.push(`hostStrategy must be one of: ${HOST_STRATEGIES.join(', ')}`);
|
|
663
676
|
}
|
|
664
677
|
}
|
|
665
|
-
const strategy = resolveHostStrategy(config);
|
|
666
678
|
if (config.host !== undefined) {
|
|
667
679
|
if (typeof config.host !== 'string' || config.host.trim() === '') {
|
|
668
680
|
errors.push('host must be a non-empty machine name (a registered host, device, capability tag, or user@host)');
|
package/dist/lib/runner.d.ts
CHANGED
|
@@ -26,14 +26,19 @@ export interface RunResult {
|
|
|
26
26
|
export declare class RoutineAlreadyRunningError extends Error {
|
|
27
27
|
constructor(jobName: string, runId: string);
|
|
28
28
|
}
|
|
29
|
-
/** Agents the daemon can actually run, derived from the command table above
|
|
30
|
-
* so the `--agent` help and any validation can never drift from it. */
|
|
31
|
-
export declare const ROUTINE_AGENT_IDS: readonly string[];
|
|
32
29
|
/** Stable working directory for routine children, independent of the daemon's launch cwd. */
|
|
33
30
|
export declare function routineSpawnCwd(config: Pick<JobConfig, 'repo'>, configuredRoot?: string | undefined): string;
|
|
34
31
|
/** Build the full CLI argv for executing a job, applying mode, model, and permission flags. */
|
|
35
32
|
export declare function buildJobCommand(config: JobConfig, resolvedPrompt: string): string[];
|
|
36
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Record every transcript file already present in the child's transcript dirs BEFORE
|
|
35
|
+
* the run spawns. When the transcript lands in a shared per-version home, this baseline
|
|
36
|
+
* is what lets the archiver copy only the file THIS run produced instead of sweeping in
|
|
37
|
+
* every sibling session that home holds (which would mis-tag them all `origin='routine'`
|
|
38
|
+
* under this run's name). Called once per run, before spawn.
|
|
39
|
+
*/
|
|
40
|
+
export declare function snapshotRoutineTranscriptBase(meta: Pick<RunMeta, 'jobName' | 'agent' | 'version'>, runDir: string, overlayHome?: string): void;
|
|
41
|
+
export declare function archiveRoutineTranscripts(meta: Pick<RunMeta, 'jobName' | 'runId' | 'agent' | 'version'>, runDir: string, overlayHome?: string): void;
|
|
37
42
|
/** Pre-flight version/account selection for a routine job. */
|
|
38
43
|
export interface RoutineLaunchPlan {
|
|
39
44
|
/** Ordered attempts: primary first, then same-agent failover accounts. */
|
package/dist/lib/runner.js
CHANGED
|
@@ -18,7 +18,8 @@ import * as fs from 'fs';
|
|
|
18
18
|
import * as path from 'path';
|
|
19
19
|
import * as os from 'os';
|
|
20
20
|
import { resolveJobPrompt, parseTimeout, writeRunMeta, listRuns, getJobRunsDir, getRunDir, checkJobDeviceEligibility, finalizeRunMeta, } from './routines.js';
|
|
21
|
-
import { getRunsDir } from './state.js';
|
|
21
|
+
import { getRunsDir, getUserAgentsDir } from './state.js';
|
|
22
|
+
import { shortCodexHome } from './codex-home.js';
|
|
22
23
|
import { prepareJobHome, buildSpawnEnv, getJobHomePath } from './sandbox.js';
|
|
23
24
|
import { resolveModel, buildReasoningFlags } from './models.js';
|
|
24
25
|
import { createTimer, redactPrompt } from './events.js';
|
|
@@ -36,7 +37,7 @@ import { resolveClaudeSetupToken } from './claude-account-token.js';
|
|
|
36
37
|
import { getConfiguredRunStrategy, resolveRunVersion, resolveAccountVersion, rotationFailoverChain, readinessFromCandidate, formatNoHealthyAccountError, } from './rotate.js';
|
|
37
38
|
import { readAuthHealth, isDeadVerdict } from './auth-health.js';
|
|
38
39
|
import { machineId } from './machine-id.js';
|
|
39
|
-
import { isSelfUpdatingAgent } from './agents.js';
|
|
40
|
+
import { isSelfUpdatingAgent, ROUTINE_AGENT_COMMANDS as AGENT_COMMANDS, ROUTINE_AGENT_IDS, isAgentHardDeprecated, hardDeprecationError } from './agents.js';
|
|
40
41
|
import { expandLocalHome, getProjectRoot } from './project-root.js';
|
|
41
42
|
export class RoutineAlreadyRunningError extends Error {
|
|
42
43
|
constructor(jobName, runId) {
|
|
@@ -98,19 +99,6 @@ function terminateRoutineTree(pid) {
|
|
|
98
99
|
}
|
|
99
100
|
catch { /* already exited */ }
|
|
100
101
|
}
|
|
101
|
-
/** CLI command templates per agent, with {prompt} as a placeholder. */
|
|
102
|
-
const AGENT_COMMANDS = {
|
|
103
|
-
claude: ['claude', '-p', '--verbose', '{prompt}', '--output-format', 'stream-json', '--permission-mode', 'plan'],
|
|
104
|
-
codex: ['codex', 'exec', '{prompt}', '--json'],
|
|
105
|
-
gemini: ['gemini', '{prompt}', '--output-format', 'stream-json'],
|
|
106
|
-
cursor: ['cursor-agent', '-p', '{prompt}', '--output-format', 'stream-json'],
|
|
107
|
-
kimi: ['kimi', '--prompt', '{prompt}', '--output-format', 'stream-json'],
|
|
108
|
-
droid: ['droid', 'exec', '{prompt}', '-o', 'stream-json'],
|
|
109
|
-
muse: ['muse', 'exec', '{prompt}', '--json'],
|
|
110
|
-
};
|
|
111
|
-
/** Agents the daemon can actually run, derived from the command table above
|
|
112
|
-
* so the `--agent` help and any validation can never drift from it. */
|
|
113
|
-
export const ROUTINE_AGENT_IDS = Object.freeze(Object.keys(AGENT_COMMANDS));
|
|
114
102
|
/**
|
|
115
103
|
* Where each agent's transcript files live under an overlay HOME, mirroring
|
|
116
104
|
* `SESSION_ROOT_SPECS` (session/discover.ts) — the CLI's own source of truth
|
|
@@ -322,6 +310,94 @@ function appendModelAndReasoning(cmd, config) {
|
|
|
322
310
|
function generateRunId() {
|
|
323
311
|
return new Date().toISOString().replace(/[:.]/g, '-');
|
|
324
312
|
}
|
|
313
|
+
/**
|
|
314
|
+
* Agents whose config dir `buildExecEnv` relocates OUT of the sandbox overlay HOME
|
|
315
|
+
* into their per-version home (exec.ts: CLAUDE_CONFIG_DIR / CODEX_HOME). A routine
|
|
316
|
+
* spawned for one of these writes its transcript under the version home, NOT the overlay
|
|
317
|
+
* the sandbox generated — so the archiver has to read it there. Both are version-pinned
|
|
318
|
+
* (never self-updating), so `RunMeta.version` names a real home.
|
|
319
|
+
*
|
|
320
|
+
* Scoped to the agents whose archived transcript the DISCOVERY side can already index as
|
|
321
|
+
* origin='routine' — `readRoutineArchiveMeta` (session/discover.ts) has a branch for
|
|
322
|
+
* claude and codex but not kimi. Kimi also relocates (KIMI_CODE_HOME) and hits the same
|
|
323
|
+
* bug, but archiving it here without a discovery branch would only copy files that are
|
|
324
|
+
* never indexed; adding a kimi reader (its session spans state.json + wire.jsonl under a
|
|
325
|
+
* `session_<uuid>` dir) is the separate follow-up that lets kimi join this set. Muse
|
|
326
|
+
* (XDG, self-updating) and copilot (no transcript spec) are likewise out of scope.
|
|
327
|
+
*/
|
|
328
|
+
const CONFIG_DIR_RELOCATED_AGENTS = new Set(['claude', 'codex']);
|
|
329
|
+
/** Whether this run's transcript lands in a SHARED per-version home (accumulating every
|
|
330
|
+
* session that version+account ever ran) rather than the run's own disposable overlay. */
|
|
331
|
+
function usesSharedTranscriptHome(agent, version) {
|
|
332
|
+
return Boolean(version) && CONFIG_DIR_RELOCATED_AGENTS.has(agent);
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* The directories the child actually writes a transcript to for one spec — resolved to
|
|
336
|
+
* match `buildExecEnv` (exec.ts), the single decision-point for where the transcript
|
|
337
|
+
* lands, so the archiver can never drift from it. For a config-dir-relocated agent that
|
|
338
|
+
* is the per-version home (`.claude`, `.codex`, …); for everyone else it is the sandbox
|
|
339
|
+
* overlay HOME. Codex may run from a SUN_LEN-safe short home on macOS (codex-home.ts),
|
|
340
|
+
* so both candidates are returned and the caller skips whichever does not exist.
|
|
341
|
+
*/
|
|
342
|
+
function routineTranscriptSourceRoots(agent, version, overlayHome, spec) {
|
|
343
|
+
if (usesSharedTranscriptHome(agent, version)) {
|
|
344
|
+
const roots = [path.join(getVersionHomePath(agent, version), ...spec.root)];
|
|
345
|
+
if (agent === 'codex') {
|
|
346
|
+
roots.push(path.join(shortCodexHome(getUserAgentsDir(), version), ...spec.root.slice(1)));
|
|
347
|
+
}
|
|
348
|
+
return roots;
|
|
349
|
+
}
|
|
350
|
+
return [path.join(overlayHome, ...spec.root)];
|
|
351
|
+
}
|
|
352
|
+
/** Path of the per-run baseline recorded before spawn (see {@link snapshotRoutineTranscriptBase}). */
|
|
353
|
+
function transcriptBasePath(runDir) {
|
|
354
|
+
return path.join(runDir, '.transcript-base.json');
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Record every transcript file already present in the child's transcript dirs BEFORE
|
|
358
|
+
* the run spawns. When the transcript lands in a shared per-version home, this baseline
|
|
359
|
+
* is what lets the archiver copy only the file THIS run produced instead of sweeping in
|
|
360
|
+
* every sibling session that home holds (which would mis-tag them all `origin='routine'`
|
|
361
|
+
* under this run's name). Called once per run, before spawn.
|
|
362
|
+
*/
|
|
363
|
+
export function snapshotRoutineTranscriptBase(meta, runDir, overlayHome) {
|
|
364
|
+
if (!meta.agent)
|
|
365
|
+
return;
|
|
366
|
+
const specs = ROUTINE_TRANSCRIPT_SPECS[meta.agent];
|
|
367
|
+
if (!specs)
|
|
368
|
+
return;
|
|
369
|
+
const home = overlayHome ?? getJobHomePath(meta.jobName);
|
|
370
|
+
const preexisting = new Set();
|
|
371
|
+
for (const spec of specs) {
|
|
372
|
+
for (const root of routineTranscriptSourceRoots(meta.agent, meta.version, home, spec)) {
|
|
373
|
+
if (!fs.existsSync(root))
|
|
374
|
+
continue;
|
|
375
|
+
for (const f of walkForFiles(root, spec.ext, 100_000))
|
|
376
|
+
preexisting.add(f);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
try {
|
|
380
|
+
fs.mkdirSync(runDir, { recursive: true });
|
|
381
|
+
fs.writeFileSync(transcriptBasePath(runDir), JSON.stringify([...preexisting]), { mode: 0o600 });
|
|
382
|
+
}
|
|
383
|
+
catch {
|
|
384
|
+
// Best-effort: a missing baseline is handled conservatively at archive time
|
|
385
|
+
// (a shared home is skipped rather than bulk-copied).
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
/** Read the pre-spawn baseline. `null` means none was recorded (older run or a failed
|
|
389
|
+
* snapshot) — the archiver then refuses to bulk-copy a shared home. */
|
|
390
|
+
function readTranscriptBase(runDir) {
|
|
391
|
+
try {
|
|
392
|
+
const arr = JSON.parse(fs.readFileSync(transcriptBasePath(runDir), 'utf-8'));
|
|
393
|
+
if (Array.isArray(arr))
|
|
394
|
+
return new Set(arr.filter((x) => typeof x === 'string'));
|
|
395
|
+
}
|
|
396
|
+
catch {
|
|
397
|
+
// No baseline file, or it was unreadable/corrupt.
|
|
398
|
+
}
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
325
401
|
export function archiveRoutineTranscripts(meta, runDir, overlayHome) {
|
|
326
402
|
if (!meta.agent)
|
|
327
403
|
return;
|
|
@@ -329,24 +405,35 @@ export function archiveRoutineTranscripts(meta, runDir, overlayHome) {
|
|
|
329
405
|
if (!specs)
|
|
330
406
|
return;
|
|
331
407
|
const home = overlayHome ?? getJobHomePath(meta.jobName);
|
|
408
|
+
const shared = usesSharedTranscriptHome(meta.agent, meta.version);
|
|
409
|
+
const base = readTranscriptBase(runDir);
|
|
410
|
+
// A shared per-version home holds every session that version+account ran, so without a
|
|
411
|
+
// per-run baseline we cannot tell this run's transcript from a sibling's — copy nothing
|
|
412
|
+
// rather than mis-tag them all. A disposable overlay is this run's alone, so copy all.
|
|
413
|
+
if (shared && base === null)
|
|
414
|
+
return;
|
|
332
415
|
for (const spec of specs) {
|
|
333
|
-
const sourceRoot = path.join(home, ...spec.root);
|
|
334
|
-
if (!fs.existsSync(sourceRoot))
|
|
335
|
-
continue;
|
|
336
416
|
const destRoot = path.join(runDir, 'sessions', meta.agent, spec.root[spec.root.length - 1]);
|
|
337
|
-
for (const
|
|
338
|
-
|
|
339
|
-
if (rel.startsWith('..') || path.isAbsolute(rel))
|
|
417
|
+
for (const sourceRoot of routineTranscriptSourceRoots(meta.agent, meta.version, home, spec)) {
|
|
418
|
+
if (!fs.existsSync(sourceRoot))
|
|
340
419
|
continue;
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
420
|
+
for (const sourcePath of walkForFiles(sourceRoot, spec.ext, 100_000)) {
|
|
421
|
+
// Skip files that predate this run (a sibling session in the shared home).
|
|
422
|
+
if (base?.has(sourcePath))
|
|
423
|
+
continue;
|
|
424
|
+
const rel = path.relative(sourceRoot, sourcePath);
|
|
425
|
+
if (rel.startsWith('..') || path.isAbsolute(rel))
|
|
426
|
+
continue;
|
|
427
|
+
const destPath = path.join(destRoot, rel);
|
|
428
|
+
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
429
|
+
try {
|
|
430
|
+
fs.copyFileSync(sourcePath, destPath);
|
|
431
|
+
fs.chmodSync(destPath, 0o600);
|
|
432
|
+
}
|
|
433
|
+
catch {
|
|
434
|
+
// The process already reached a terminal state; a concurrently removed
|
|
435
|
+
// transcript should not rewrite that outcome.
|
|
436
|
+
}
|
|
350
437
|
}
|
|
351
438
|
}
|
|
352
439
|
}
|
|
@@ -680,11 +767,53 @@ function injectRoutineActor(env, config) {
|
|
|
680
767
|
env.AGENTS_ACTOR = config.actor;
|
|
681
768
|
return env;
|
|
682
769
|
}
|
|
770
|
+
/**
|
|
771
|
+
* `routines add`/`routines edit` refuse to create a routine pinned to a
|
|
772
|
+
* hard-deprecated harness (commands/routines.ts), but that check runs only at
|
|
773
|
+
* CLI-write time. A routine YAML written before the harness was deprecated, or
|
|
774
|
+
* edited directly on disk / synced from another device, has no equivalent gate
|
|
775
|
+
* at fire time — the daemon would happily build and spawn `gemini …` against a
|
|
776
|
+
* backend Google retired. Fail loud here, before any version/account
|
|
777
|
+
* resolution or sandbox prep, so a legacy routine skips with the same message
|
|
778
|
+
* every other entry point (`sync`, `exec`, `import`, `run-cloud`, `versions`)
|
|
779
|
+
* already shows instead of a doomed, confusing spawn failure.
|
|
780
|
+
*/
|
|
781
|
+
function hardDeprecationRunFailure(config, agent) {
|
|
782
|
+
const runId = generateRunId();
|
|
783
|
+
const runDir = getRunDir(config.name, runId);
|
|
784
|
+
fs.mkdirSync(runDir, { recursive: true });
|
|
785
|
+
const reason = hardDeprecationError(agent);
|
|
786
|
+
const meta = {
|
|
787
|
+
jobName: config.name,
|
|
788
|
+
runId,
|
|
789
|
+
...runProvenance(config),
|
|
790
|
+
agent,
|
|
791
|
+
pid: null,
|
|
792
|
+
spawnedAt: Date.now(),
|
|
793
|
+
status: 'running',
|
|
794
|
+
startedAt: new Date().toISOString(),
|
|
795
|
+
completedAt: null,
|
|
796
|
+
exitCode: null,
|
|
797
|
+
};
|
|
798
|
+
writeRunMeta(meta);
|
|
799
|
+
process.stderr.write(`[agents] routine ${config.name}: ${reason}\n`);
|
|
800
|
+
finalizeRunMeta(meta, 'failed', 1, { errorMessage: reason });
|
|
801
|
+
writeRunMeta(meta);
|
|
802
|
+
return meta;
|
|
803
|
+
}
|
|
683
804
|
export async function executeJob(config, deps) {
|
|
684
805
|
const eligibility = checkJobDeviceEligibility(config);
|
|
685
806
|
if (eligibility) {
|
|
686
807
|
throw new Error(eligibility.message);
|
|
687
808
|
}
|
|
809
|
+
// Hard-deprecated harness (e.g. gemini): fail loud before placement, version/
|
|
810
|
+
// account resolution, or sandbox prep — local, host, AND cloud placement all
|
|
811
|
+
// share this one gate (a hard-deprecated agent has no `cloudProvider` entry,
|
|
812
|
+
// so cloud placement would otherwise silently fall back to the default
|
|
813
|
+
// provider instead of refusing). See hardDeprecationRunFailure.
|
|
814
|
+
if (!config.workflow && config.agent && ROUTINE_AGENT_IDS.includes(config.agent) && isAgentHardDeprecated(config.agent)) {
|
|
815
|
+
return { meta: hardDeprecationRunFailure(config, config.agent), reportPath: null };
|
|
816
|
+
}
|
|
688
817
|
// Placement (hostStrategy / bare host:) — body may run on another machine
|
|
689
818
|
// over SSH or in the cloud; local version selection / sandbox / spawn then
|
|
690
819
|
// do not apply. Sync callers (manual `routines run`, catchup) follow the
|
|
@@ -737,6 +866,7 @@ export async function executeJob(config, deps) {
|
|
|
737
866
|
runId,
|
|
738
867
|
...runProvenance(config),
|
|
739
868
|
agent: effectiveAgent,
|
|
869
|
+
version: primaryVersion,
|
|
740
870
|
...(config.workflow ? { workflow: config.workflow } : {}),
|
|
741
871
|
pid: null,
|
|
742
872
|
spawnedAt: Date.now(),
|
|
@@ -746,6 +876,9 @@ export async function executeJob(config, deps) {
|
|
|
746
876
|
exitCode: null,
|
|
747
877
|
};
|
|
748
878
|
writeRunMeta(meta);
|
|
879
|
+
// Baseline the child's transcript dirs BEFORE spawning so archiveRoutineTranscripts
|
|
880
|
+
// copies only THIS run's transcript out of a shared per-version home (RUSH-2271).
|
|
881
|
+
snapshotRoutineTranscriptBase(meta, runDir, overlayHome);
|
|
749
882
|
// Auth preflight: if the last live probe rejected this (agent, version)'s
|
|
750
883
|
// token (verdict `revoked`), the run is guaranteed to fail auth — fail fast
|
|
751
884
|
// before spawning instead of producing a doomed run + poisoned report. Cache-
|
|
@@ -821,6 +954,13 @@ export async function executeJob(config, deps) {
|
|
|
821
954
|
if (i === 0) {
|
|
822
955
|
process.stderr.write(`[agents] routine ${config.name}: running ${label}\n`);
|
|
823
956
|
}
|
|
957
|
+
// A rate-limit failover spawns the NEXT chain entry, whose version/account has
|
|
958
|
+
// its own per-version transcript home. Re-point meta.version at the attempt that
|
|
959
|
+
// is about to run and re-baseline that home, so the archiver reads the transcript
|
|
960
|
+
// where THIS attempt writes it — not chain[0]'s home (RUSH-2271). The pre-loop
|
|
961
|
+
// snapshot already covered chain[0]; this makes every later attempt correct too.
|
|
962
|
+
meta.version = attemptVersion;
|
|
963
|
+
snapshotRoutineTranscriptBase(meta, runDir, overlayHome);
|
|
824
964
|
const viaAgentsRun = dispatchesViaAgentsRun(config);
|
|
825
965
|
const cmd = viaAgentsRun
|
|
826
966
|
? baseCmd
|
|
@@ -1164,6 +1304,14 @@ export async function executeJobDetached(config, hooks) {
|
|
|
1164
1304
|
return withRoutineLaunchClaim(config, () => executeJobDetachedClaimed(config, hooks));
|
|
1165
1305
|
}
|
|
1166
1306
|
async function executeJobDetachedClaimed(config, hooks) {
|
|
1307
|
+
// Hard-deprecated harness (e.g. gemini): fail loud before placement, version/
|
|
1308
|
+
// account resolution, or sandbox prep — local, host, AND cloud placement all
|
|
1309
|
+
// share this one gate (a hard-deprecated agent has no `cloudProvider` entry,
|
|
1310
|
+
// so cloud placement would otherwise silently fall back to the default
|
|
1311
|
+
// provider instead of refusing). See hardDeprecationRunFailure.
|
|
1312
|
+
if (!config.workflow && config.agent && ROUTINE_AGENT_IDS.includes(config.agent) && isAgentHardDeprecated(config.agent)) {
|
|
1313
|
+
return hardDeprecationRunFailure(config, config.agent);
|
|
1314
|
+
}
|
|
1167
1315
|
// Placement (hostStrategy / bare host:) — dispatch off-box and return; the
|
|
1168
1316
|
// monitor finalizes host: runs, cloud runs stay terminal when dispatch ends.
|
|
1169
1317
|
// Either way the in-process onFinish hook does not fire for off-box routines
|
|
@@ -1236,6 +1384,7 @@ async function executeJobDetachedClaimed(config, hooks) {
|
|
|
1236
1384
|
runId,
|
|
1237
1385
|
...runProvenance(config),
|
|
1238
1386
|
agent: effectiveAgent,
|
|
1387
|
+
version,
|
|
1239
1388
|
...(config.workflow ? { workflow: config.workflow } : {}),
|
|
1240
1389
|
pid: null,
|
|
1241
1390
|
spawnedAt: Date.now(),
|
|
@@ -1245,6 +1394,9 @@ async function executeJobDetachedClaimed(config, hooks) {
|
|
|
1245
1394
|
completedAt: null,
|
|
1246
1395
|
exitCode: null,
|
|
1247
1396
|
};
|
|
1397
|
+
// Baseline the child's transcript dirs BEFORE spawning so archiveRoutineTranscripts
|
|
1398
|
+
// copies only THIS run's transcript out of a shared per-version home (RUSH-2271).
|
|
1399
|
+
snapshotRoutineTranscriptBase(meta, runDir, overlayHome);
|
|
1248
1400
|
// Auth preflight (mirrors executeJob): with no injected token, a daemon-fired
|
|
1249
1401
|
// Claude routine authenticates via the pinned account's own CLAUDE_CONFIG_DIR
|
|
1250
1402
|
// login. If the last live probe rejected that (agent, version)'s token, the run
|
package/dist/lib/sandbox.d.ts
CHANGED
|
@@ -31,5 +31,3 @@ export declare function symlinkAllowedDirs(overlayHome: string, dirs: string[]):
|
|
|
31
31
|
export declare function generateClaudeConfig(overlayHome: string, config: JobConfig): void;
|
|
32
32
|
/** Generate a Codex config.toml in the overlay with model and approval-mode settings. */
|
|
33
33
|
export declare function generateCodexConfig(overlayHome: string, config: JobConfig): void;
|
|
34
|
-
/** Generate a Gemini settings.json in the overlay from the job's config block. */
|
|
35
|
-
export declare function generateGeminiConfig(overlayHome: string, config: JobConfig): void;
|