@phnx-labs/agents-cli 1.22.66 → 1.22.69
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 +56 -0
- package/README.md +21 -8
- package/dist/bootstrap.js +6 -3
- package/dist/commands/browser.js +46 -21
- package/dist/commands/daemon-test-harness.d.ts +1 -1
- package/dist/commands/daemon-test-harness.js +2 -2
- package/dist/commands/daemon.js +21 -22
- package/dist/commands/exec.js +50 -0
- package/dist/commands/feed.js +20 -7
- package/dist/commands/monitors.js +5 -2
- package/dist/commands/projects.d.ts +26 -6
- package/dist/commands/projects.js +55 -22
- package/dist/commands/repo.js +57 -19
- package/dist/commands/resume.d.ts +16 -0
- package/dist/commands/resume.js +41 -8
- package/dist/commands/routines.js +42 -21
- package/dist/commands/send.js +29 -2
- package/dist/commands/sessions-inject.d.ts +58 -0
- package/dist/commands/sessions-inject.js +143 -7
- package/dist/commands/sessions-optimize.js +1 -1
- package/dist/commands/sessions-picker.js +1 -0
- package/dist/commands/sessions.js +4 -11
- package/dist/commands/share.d.ts +5 -3
- package/dist/commands/share.js +73 -20
- package/dist/commands/ssh.js +205 -2
- package/dist/lib/accounting/account-pool-collect.d.ts +6 -4
- package/dist/lib/accounting/account-pool-collect.js +6 -4
- package/dist/lib/accounting/usage-ingest.js +4 -2
- package/dist/lib/accounting/usage-sync.d.ts +38 -93
- package/dist/lib/accounting/usage-sync.js +66 -210
- package/dist/lib/accounting/usage.d.ts +18 -5
- package/dist/lib/accounting/usage.js +165 -20
- package/dist/lib/auth-health.d.ts +8 -0
- package/dist/lib/auth-health.js +4 -4
- package/dist/lib/boot-profile.d.ts +14 -0
- package/dist/lib/boot-profile.js +66 -0
- package/dist/lib/browser/caller-identity.d.ts +12 -0
- package/dist/lib/browser/caller-identity.js +19 -0
- package/dist/lib/browser/ipc.d.ts +37 -32
- package/dist/lib/browser/ipc.js +146 -94
- package/dist/lib/browser/task-index.d.ts +10 -2
- package/dist/lib/browser/task-index.js +22 -3
- package/dist/lib/channels/providers/desktop.d.ts +5 -4
- package/dist/lib/channels/providers/desktop.js +5 -4
- package/dist/lib/claude-account-token.js +108 -4
- package/dist/lib/daemon/account-state-daemon-service.d.ts +49 -9
- package/dist/lib/daemon/account-state-daemon-service.js +81 -18
- package/dist/lib/daemon/auth-sync-service.d.ts +4 -4
- package/dist/lib/daemon/auth-sync-service.js +17 -6
- package/dist/lib/daemon/catchup-service.d.ts +51 -0
- package/dist/lib/daemon/catchup-service.js +51 -0
- package/dist/lib/daemon/daemon.d.ts +12 -22
- package/dist/lib/daemon/daemon.js +463 -176
- package/dist/lib/daemon/runner.js +2 -0
- package/dist/lib/daemon/service.d.ts +22 -4
- package/dist/lib/daemon/service.js +2 -2
- package/dist/lib/daemon/supervisor.d.ts +55 -15
- package/dist/lib/daemon/supervisor.js +119 -29
- package/dist/lib/daemon/usage-sync-service.d.ts +4 -6
- package/dist/lib/daemon/usage-sync-service.js +22 -18
- package/dist/lib/daemon-health.js +36 -31
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +12 -2
- package/dist/lib/daemon-ticks.d.ts +9 -6
- package/dist/lib/daemon-ticks.js +14 -8
- package/dist/lib/devices/health.d.ts +38 -2
- package/dist/lib/devices/health.js +43 -5
- package/dist/lib/devices/registry.js +2 -0
- package/dist/lib/devices/worker-pick.d.ts +1 -1
- package/dist/lib/devices/worker-pick.js +4 -1
- package/dist/lib/exec.js +15 -0
- package/dist/lib/feed/watch.d.ts +3 -0
- package/dist/lib/feed/watch.js +13 -3
- package/dist/lib/feed-broadcast.d.ts +64 -5
- package/dist/lib/feed-broadcast.js +124 -22
- package/dist/lib/fleet-shared-repo-sync.d.ts +36 -0
- package/dist/lib/fleet-shared-repo-sync.js +333 -0
- package/dist/lib/fleet-shared-state.d.ts +38 -0
- package/dist/lib/fleet-shared-state.js +105 -0
- package/dist/lib/hosts/remote-cmd.d.ts +2 -0
- package/dist/lib/hosts/remote-cmd.js +12 -3
- package/dist/lib/lock-compromise.d.ts +8 -0
- package/dist/lib/lock-compromise.js +12 -0
- package/dist/lib/monitors/engine.d.ts +2 -1
- package/dist/lib/monitors/engine.js +27 -2
- package/dist/lib/monitors/sources/command.js +13 -3
- package/dist/lib/monitors/sources/failure.d.ts +32 -0
- package/dist/lib/monitors/sources/failure.js +52 -0
- package/dist/lib/monitors/sources/types.d.ts +9 -0
- package/dist/lib/owner-message.d.ts +12 -0
- package/dist/lib/owner-message.js +44 -0
- package/dist/lib/refresh-coordinator.js +2 -0
- package/dist/lib/run-trace-sync.d.ts +28 -0
- package/dist/lib/run-trace-sync.js +99 -0
- package/dist/lib/secrets/filestore.d.ts +4 -0
- package/dist/lib/secrets/filestore.js +164 -3
- package/dist/lib/secrets/push.d.ts +10 -0
- package/dist/lib/secrets/push.js +86 -7
- package/dist/lib/secrets/remote.d.ts +18 -6
- package/dist/lib/secrets/remote.js +29 -4
- package/dist/lib/secrets/reserved-sync.d.ts +28 -27
- package/dist/lib/secrets/reserved-sync.js +119 -101
- package/dist/lib/session/active.d.ts +13 -1
- package/dist/lib/session/active.js +5 -0
- package/dist/lib/session/actor-sidecar.d.ts +12 -0
- package/dist/lib/session/actor-sidecar.js +2 -0
- package/dist/lib/session/db.d.ts +16 -2
- package/dist/lib/session/db.js +73 -22
- package/dist/lib/session/discover.d.ts +12 -3
- package/dist/lib/session/discover.js +187 -26
- package/dist/lib/session/linear.d.ts +13 -0
- package/dist/lib/session/linear.js +44 -0
- package/dist/lib/session/live-metadata.js +1 -0
- package/dist/lib/session/parse.js +2 -3
- package/dist/lib/session/prompt.d.ts +17 -0
- package/dist/lib/session/prompt.js +35 -0
- package/dist/lib/session/recovery.d.ts +43 -6
- package/dist/lib/session/recovery.js +80 -10
- package/dist/lib/session/remote/remote-list.d.ts +17 -1
- package/dist/lib/session/remote/remote-list.js +29 -4
- package/dist/lib/session/remote/watch.d.ts +25 -2
- package/dist/lib/session/remote/watch.js +188 -11
- package/dist/lib/session/session-cache.d.ts +2 -1
- package/dist/lib/session/session-cache.js +1 -0
- package/dist/lib/session/state.js +11 -13
- package/dist/lib/session/types.d.ts +2 -0
- package/dist/lib/share/backend.d.ts +2 -2
- package/dist/lib/share/backend.js +20 -9
- package/dist/lib/share/delete.d.ts +5 -1
- package/dist/lib/share/delete.js +7 -2
- package/dist/lib/share/http-error.d.ts +52 -0
- package/dist/lib/share/http-error.js +65 -0
- package/dist/lib/share/publish.d.ts +67 -11
- package/dist/lib/share/publish.js +98 -16
- package/dist/lib/share/worker-template.js +105 -9
- package/dist/lib/smart-launch.js +27 -4
- package/dist/lib/ssh-exec.d.ts +2 -0
- package/dist/lib/ssh-exec.js +20 -4
- package/dist/lib/storage/index.d.ts +14 -0
- package/dist/lib/storage/index.js +14 -0
- package/dist/lib/storage/selection.d.ts +48 -0
- package/dist/lib/storage/selection.js +39 -0
- package/dist/lib/storage/visibility.d.ts +82 -0
- package/dist/lib/storage/visibility.js +99 -0
- package/dist/lib/teams/agents.js +3 -1
- package/dist/lib/teams/placement-probe.js +1 -0
- package/dist/lib/teams/registry.js +2 -0
- package/dist/lib/teams/scheduler.d.ts +8 -1
- package/dist/lib/teams/scheduler.js +4 -1
- package/dist/lib/testdata/daemon-health-writer.d.ts +1 -0
- package/dist/lib/testdata/daemon-health-writer.js +8 -0
- package/dist/lib/traces/backend.js +13 -2
- package/dist/lib/traces/sync.d.ts +7 -0
- package/dist/lib/traces/sync.js +9 -0
- package/dist/lib/usage-refresh.d.ts +8 -2
- package/dist/lib/usage-refresh.js +3 -3
- package/dist/lib/worktree/held.d.ts +166 -0
- package/dist/lib/worktree/held.js +368 -0
- package/package.json +2 -2
- package/dist/lib/account-state-service.d.ts +0 -21
- package/dist/lib/account-state-service.js +0 -60
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
import * as fs from 'fs';
|
|
24
24
|
import * as path from 'path';
|
|
25
25
|
import { getDaemonDir } from './state.js';
|
|
26
|
+
import { atomicWriteFileSync, ensureLockTarget, withFileLock } from './fs-atomic.js';
|
|
26
27
|
const HEALTH_FILE = 'health.json';
|
|
27
28
|
/** Stable subsystem identifiers shared by the daemon (writer) and `agents daemon` (reader). */
|
|
28
29
|
export const SUBSYSTEM_SECRETS_BROKER = 'secrets-broker';
|
|
@@ -61,15 +62,19 @@ function readAll() {
|
|
|
61
62
|
* dropped health update, never escape as an unhandled rejection that would
|
|
62
63
|
* hit the process-wide handler and take down every OTHER service too.
|
|
63
64
|
*/
|
|
64
|
-
function
|
|
65
|
+
function updateAll(update) {
|
|
65
66
|
try {
|
|
66
67
|
const healthPath = getHealthPath();
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
68
|
+
ensureLockTarget(healthPath, '{}');
|
|
69
|
+
withFileLock(healthPath, () => {
|
|
70
|
+
const records = readAll();
|
|
71
|
+
update(records);
|
|
72
|
+
atomicWriteFileSync(healthPath, JSON.stringify(records), { encoding: 'utf-8', mode: 0o600 });
|
|
73
|
+
try {
|
|
74
|
+
fs.chmodSync(healthPath, 0o600);
|
|
75
|
+
}
|
|
76
|
+
catch { /* best effort */ }
|
|
77
|
+
});
|
|
73
78
|
}
|
|
74
79
|
catch { /* see docblock above — health recording must never crash a caller */ }
|
|
75
80
|
}
|
|
@@ -78,23 +83,23 @@ function blankRecord(subsystem) {
|
|
|
78
83
|
}
|
|
79
84
|
/** Record a successful subsystem check-in — clears the failure streak. */
|
|
80
85
|
export function recordSubsystemOk(subsystem, at = new Date().toISOString()) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
86
|
+
updateAll((all) => {
|
|
87
|
+
const existing = all[subsystem] ?? blankRecord(subsystem);
|
|
88
|
+
all[subsystem] = { ...existing, subsystem, consecutiveFailures: 0, lastOkAt: at };
|
|
89
|
+
});
|
|
85
90
|
}
|
|
86
91
|
/** Record a subsystem failure — bumps the consecutive-failure streak. */
|
|
87
92
|
export function recordSubsystemError(subsystem, error, at = new Date().toISOString()) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
93
|
+
updateAll((all) => {
|
|
94
|
+
const existing = all[subsystem] ?? blankRecord(subsystem);
|
|
95
|
+
all[subsystem] = {
|
|
96
|
+
...existing,
|
|
97
|
+
subsystem,
|
|
98
|
+
lastError: error,
|
|
99
|
+
lastErrorAt: at,
|
|
100
|
+
consecutiveFailures: existing.consecutiveFailures + 1,
|
|
101
|
+
};
|
|
102
|
+
});
|
|
98
103
|
}
|
|
99
104
|
/**
|
|
100
105
|
* Refine the reason on an already-counted failure, without bumping the streak.
|
|
@@ -109,12 +114,12 @@ export function recordSubsystemError(subsystem, error, at = new Date().toISOStri
|
|
|
109
114
|
* subsystem is left alone rather than given a blank record to decorate.
|
|
110
115
|
*/
|
|
111
116
|
export function recordSubsystemErrorReason(subsystem, error, at = new Date().toISOString()) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
117
|
+
updateAll((all) => {
|
|
118
|
+
const existing = all[subsystem];
|
|
119
|
+
if (!existing)
|
|
120
|
+
return;
|
|
121
|
+
all[subsystem] = { ...existing, lastError: error, lastErrorAt: at };
|
|
122
|
+
});
|
|
118
123
|
}
|
|
119
124
|
/**
|
|
120
125
|
* Record a `ServiceSupervisor` lifecycle-state transition, without touching
|
|
@@ -123,10 +128,10 @@ export function recordSubsystemErrorReason(subsystem, error, at = new Date().toI
|
|
|
123
128
|
* status` runs as a separate process from the daemon (see module docblock).
|
|
124
129
|
*/
|
|
125
130
|
export function recordSubsystemState(subsystem, state) {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
131
|
+
updateAll((all) => {
|
|
132
|
+
const existing = all[subsystem] ?? blankRecord(subsystem);
|
|
133
|
+
all[subsystem] = { ...existing, subsystem, state };
|
|
134
|
+
});
|
|
130
135
|
}
|
|
131
136
|
/** Read one subsystem's health record, or null if it has never reported in. */
|
|
132
137
|
export function readSubsystemHealth(subsystem) {
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* enabled without pulling in the whole daemon lifecycle.
|
|
8
8
|
*/
|
|
9
9
|
/** Every service the daemon can host. IDs are kebab-case and stable. */
|
|
10
|
-
export type DaemonServiceId = 'secrets-broker' | 'scheduler' | 'monitors' | 'browser-ipc' | 'webhook-receiver' | 'self-heal' | 'keychain-reap' | 'account-state' | 'watchdog' | 'device-probe' | 'state-dir-check' | 'session-index' | 'auth-sync' | 'usage-sync' | 'daemon-heartbeat' | 'tmux-reap' | 'browser-task-reap' | 'session-state';
|
|
10
|
+
export type DaemonServiceId = 'secrets-broker' | 'scheduler' | 'catchup' | 'monitors' | 'browser-ipc' | 'webhook-receiver' | 'self-heal' | 'keychain-reap' | 'account-state' | 'account-auth' | 'watchdog' | 'device-probe' | 'state-dir-check' | 'session-index' | 'auth-sync' | 'usage-sync' | 'daemon-heartbeat' | 'tmux-reap' | 'browser-task-reap' | 'session-state';
|
|
11
11
|
/** Human-readable metadata for each service. */
|
|
12
12
|
export interface DaemonServiceDef {
|
|
13
13
|
id: DaemonServiceId;
|
|
@@ -22,6 +22,11 @@ export const DAEMON_SERVICES = [
|
|
|
22
22
|
title: 'Routine scheduler',
|
|
23
23
|
description: 'Fires cron-scheduled routines and catches up missed fires.',
|
|
24
24
|
},
|
|
25
|
+
{
|
|
26
|
+
id: 'catchup',
|
|
27
|
+
title: 'Catch-up recovery',
|
|
28
|
+
description: 'Supervised pass that re-runs routines whose scheduled fire this device missed (sleep, wedge, or suspend). No-ops while the scheduler gate is off.',
|
|
29
|
+
},
|
|
25
30
|
{
|
|
26
31
|
id: 'monitors',
|
|
27
32
|
title: 'Monitor engine',
|
|
@@ -49,8 +54,13 @@ export const DAEMON_SERVICES = [
|
|
|
49
54
|
},
|
|
50
55
|
{
|
|
51
56
|
id: 'account-state',
|
|
52
|
-
title: 'Account
|
|
53
|
-
description: 'Refreshes account quota/usage
|
|
57
|
+
title: 'Account usage refresh',
|
|
58
|
+
description: 'Refreshes account quota/usage on its own tick (PHNX-3608: independent circuit breaker from account-auth).',
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: 'account-auth',
|
|
62
|
+
title: 'Account auth refresh',
|
|
63
|
+
description: 'Publishes this host\'s fleet-status row and refreshes auth health on its own slower tick (PHNX-3608: independent circuit breaker from account-state).',
|
|
54
64
|
},
|
|
55
65
|
{
|
|
56
66
|
id: 'watchdog',
|
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
* Daemon account-state tick bodies.
|
|
3
3
|
*
|
|
4
4
|
* These two bodies are the `refreshUsage` / `refreshAuth` implementations the
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* routines and are never fired through the scheduler — the
|
|
8
|
-
* authentication health as first-party device state
|
|
5
|
+
* supervised `AccountStateDaemonService` (daemon/account-state-daemon-service.ts)
|
|
6
|
+
* runs on its tick, in-process (usage every tick, auth on the slower ~3 min
|
|
7
|
+
* cadence). They are NOT routines and are never fired through the scheduler — the
|
|
8
|
+
* daemon owns usage and authentication health as first-party device state
|
|
9
|
+
* (RUSH-2451); the supervisor bounds each tick with a deadline + AbortSignal so a
|
|
10
|
+
* hung refresh is abandoned and restarted instead of latching (PHNX-3608).
|
|
9
11
|
*
|
|
10
12
|
* `refreshLocalFleetAuthState` is also called by `agents fleet`/`ssh` surfaces
|
|
11
13
|
* that need a fresh local auth snapshot on demand; provider-level work is guarded
|
|
@@ -59,11 +61,12 @@ export declare function shouldReuseCachedAuthProbe(force: boolean, cached: AuthP
|
|
|
59
61
|
*/
|
|
60
62
|
export declare function refreshLocalFleetAuthState(opts?: {
|
|
61
63
|
force?: boolean;
|
|
64
|
+
signal?: AbortSignal;
|
|
62
65
|
}): Promise<{
|
|
63
66
|
row: FleetStatusRow;
|
|
64
67
|
authRows: import('./auth-health.js').AuthProbeRow[];
|
|
65
68
|
}>;
|
|
66
|
-
export declare function runFleetCacheWarmTick(): Promise<void>;
|
|
69
|
+
export declare function runFleetCacheWarmTick(signal?: AbortSignal): Promise<void>;
|
|
67
70
|
/**
|
|
68
71
|
* Usage refresh: keep the usage cache the `agents run` router reads
|
|
69
72
|
* (RUSH-2061, readOnly hot path) fresh, WITHOUT the hot path ever fetching.
|
|
@@ -71,7 +74,7 @@ export declare function runFleetCacheWarmTick(): Promise<void>;
|
|
|
71
74
|
* accounts it holds credentials for, straight from the provider APIs
|
|
72
75
|
* (RUSH-3193 #15; no cross-host broadcast).
|
|
73
76
|
*/
|
|
74
|
-
export declare function runUsageRefreshTick(): Promise<void>;
|
|
77
|
+
export declare function runUsageRefreshTick(signal?: AbortSignal): Promise<void>;
|
|
75
78
|
/**
|
|
76
79
|
* Active-sessions warm (RUSH-2062 / RUSH-2484): publish THIS host's live session
|
|
77
80
|
* rows so `agents sessions watch` (and the extension that tails it) receive
|
package/dist/lib/daemon-ticks.js
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
* Daemon account-state tick bodies.
|
|
3
3
|
*
|
|
4
4
|
* These two bodies are the `refreshUsage` / `refreshAuth` implementations the
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* routines and are never fired through the scheduler — the
|
|
8
|
-
* authentication health as first-party device state
|
|
5
|
+
* supervised `AccountStateDaemonService` (daemon/account-state-daemon-service.ts)
|
|
6
|
+
* runs on its tick, in-process (usage every tick, auth on the slower ~3 min
|
|
7
|
+
* cadence). They are NOT routines and are never fired through the scheduler — the
|
|
8
|
+
* daemon owns usage and authentication health as first-party device state
|
|
9
|
+
* (RUSH-2451); the supervisor bounds each tick with a deadline + AbortSignal so a
|
|
10
|
+
* hung refresh is abandoned and restarted instead of latching (PHNX-3608).
|
|
9
11
|
*
|
|
10
12
|
* `refreshLocalFleetAuthState` is also called by `agents fleet`/`ssh` surfaces
|
|
11
13
|
* that need a fresh local auth snapshot on demand; provider-level work is guarded
|
|
@@ -62,6 +64,7 @@ export function shouldReuseCachedAuthProbe(force, cached, now, maxAgeMs = AUTH_P
|
|
|
62
64
|
*/
|
|
63
65
|
export async function refreshLocalFleetAuthState(opts) {
|
|
64
66
|
const force = opts?.force === true;
|
|
67
|
+
const signal = opts?.signal;
|
|
65
68
|
const { machineId } = await import('./machine-id.js');
|
|
66
69
|
const { probeLocalFleetAuth, readFleetAuthRows, writeFleetAuthRows } = await import('./auth-health.js');
|
|
67
70
|
const { getCliVersion } = await import('./version.js');
|
|
@@ -89,7 +92,7 @@ export async function refreshLocalFleetAuthState(opts) {
|
|
|
89
92
|
// Fleet status publishes every tick regardless — it does not ride that endpoint.
|
|
90
93
|
const cached = readFleetAuthRows(self);
|
|
91
94
|
const reuse = shouldReuseCachedAuthProbe(force, cached, requestedAt);
|
|
92
|
-
const authRows = reuse ? cached : await probeLocalFleetAuth({ cliVersion: getCliVersion(), forceLive: force });
|
|
95
|
+
const authRows = reuse ? cached : await probeLocalFleetAuth({ cliVersion: getCliVersion(), forceLive: force, signal });
|
|
93
96
|
if (!reuse)
|
|
94
97
|
writeFleetAuthRows(self, authRows);
|
|
95
98
|
const row = await publishLocalFleetStatus(self);
|
|
@@ -97,8 +100,8 @@ export async function refreshLocalFleetAuthState(opts) {
|
|
|
97
100
|
},
|
|
98
101
|
});
|
|
99
102
|
}
|
|
100
|
-
export async function runFleetCacheWarmTick() {
|
|
101
|
-
const result = await refreshLocalFleetAuthState();
|
|
103
|
+
export async function runFleetCacheWarmTick(signal) {
|
|
104
|
+
const result = await refreshLocalFleetAuthState({ signal });
|
|
102
105
|
// A waiter receives the already-published fleet row. The auth-row count is
|
|
103
106
|
// available only to the process that performed the provider probes.
|
|
104
107
|
const row = result.row;
|
|
@@ -112,7 +115,7 @@ export async function runFleetCacheWarmTick() {
|
|
|
112
115
|
* accounts it holds credentials for, straight from the provider APIs
|
|
113
116
|
* (RUSH-3193 #15; no cross-host broadcast).
|
|
114
117
|
*/
|
|
115
|
-
export async function runUsageRefreshTick() {
|
|
118
|
+
export async function runUsageRefreshTick(signal) {
|
|
116
119
|
const { runUsageRefresh, buildLocalUsageAccounts } = await import('./usage-refresh.js');
|
|
117
120
|
const { writeClaudeUsageCache, readClaudeUsageCache } = await import('./accounting/usage.js');
|
|
118
121
|
const { usageRateLimitedUntil } = await import('./usage-backoff.js');
|
|
@@ -124,6 +127,9 @@ export async function runUsageRefreshTick() {
|
|
|
124
127
|
// so a recent capture means the account is already fresh at zero API cost —
|
|
125
128
|
// the refresher re-derives headroom from it and skips the API fetch.
|
|
126
129
|
readCachedSnapshot: (usageKey) => readClaudeUsageCache(usageKey),
|
|
130
|
+
// Thread the supervisor deadline into each provider fetch so the tick's I/O
|
|
131
|
+
// is bounded by deadlineMs, not just each fetch's own 5s timeout (PHNX-3608).
|
|
132
|
+
signal,
|
|
127
133
|
});
|
|
128
134
|
const { listProfiles } = await import('./profiles.js');
|
|
129
135
|
const { refreshDueByokUsage } = await import('./byok-usage.js');
|
|
@@ -12,9 +12,37 @@
|
|
|
12
12
|
* separate copy on purpose: the CLI does not import across packages.
|
|
13
13
|
*/
|
|
14
14
|
import type { DeviceProfile } from './registry.js';
|
|
15
|
-
/** Default per-device probe budget
|
|
16
|
-
*
|
|
15
|
+
/** Default per-device probe budget, for a device reachable over a DIRECT
|
|
16
|
+
* Tailscale path. Short enough that the list never hangs on a wedged box. */
|
|
17
17
|
export declare const PROBE_TIMEOUT_MS = 2500;
|
|
18
|
+
/**
|
|
19
|
+
* Probe budget for a device whose last handshake was DERP-relayed
|
|
20
|
+
* ({@link DeviceTailscale.direct} === false).
|
|
21
|
+
*
|
|
22
|
+
* This constant used to not exist: 2.5s was applied to every device and its
|
|
23
|
+
* docstring claimed to be "long enough for a cold relayed SSH handshake". That
|
|
24
|
+
* was false, and on a fleet with no direct paths it broke `--device auto`
|
|
25
|
+
* outright (PHNX-3682). Measured on a 9-box relayed fleet, probed in parallel
|
|
26
|
+
* with cold paths: 1686/1805/1914/2688/2706/2749/3082/5586/6588 ms — six of nine
|
|
27
|
+
* over budget, every one of them healthy (rc=0 within 10s). Warm, the same
|
|
28
|
+
* probes take 512-872ms, which is what made the failure intermittent.
|
|
29
|
+
*
|
|
30
|
+
* A relayed hop pays DERP path setup on top of the TCP+SSH handshake, so it
|
|
31
|
+
* gets the same budget the readiness probe already allows
|
|
32
|
+
* (`READY_PROBE_TIMEOUT_MS`) rather than the direct-path one.
|
|
33
|
+
*/
|
|
34
|
+
export declare const RELAYED_PROBE_TIMEOUT_MS = 8000;
|
|
35
|
+
/**
|
|
36
|
+
* The probe budget for one device. A relayed peer gets
|
|
37
|
+
* {@link RELAYED_PROBE_TIMEOUT_MS}; a direct (or unknown-path) peer keeps the
|
|
38
|
+
* tight {@link PROBE_TIMEOUT_MS}. Windows keeps its own larger budget, which
|
|
39
|
+
* already exceeds both.
|
|
40
|
+
*
|
|
41
|
+
* `direct` is only meaningful when a tailscale snapshot exists — a
|
|
42
|
+
* `via:"manual"` device never gets a peer entry, so absence is "unknown path",
|
|
43
|
+
* not "relayed", and must not silently widen every manual device's budget.
|
|
44
|
+
*/
|
|
45
|
+
export declare function probeBudgetMs(device: DeviceProfile): number;
|
|
18
46
|
/** Windows probe budget. The first CIM query of a PowerShell session pays a
|
|
19
47
|
* "Preparing modules for first use" cost on top of PowerShell startup, which
|
|
20
48
|
* routinely blows the 2.5s POSIX budget on a relayed connection. */
|
|
@@ -35,6 +63,14 @@ export declare function localProbeInvocation(platform: NodeJS.Platform): {
|
|
|
35
63
|
export interface DeviceStats {
|
|
36
64
|
host: string;
|
|
37
65
|
reachable: boolean;
|
|
66
|
+
/**
|
|
67
|
+
* The probe exceeded its budget rather than being refused or unresolvable.
|
|
68
|
+
* Only meaningful when `reachable` is false — it separates "this box did not
|
|
69
|
+
* answer in time" from "this box actively could not be reached", so callers
|
|
70
|
+
* report a slow link honestly instead of calling a healthy device offline
|
|
71
|
+
* (PHNX-3682).
|
|
72
|
+
*/
|
|
73
|
+
timedOut?: boolean;
|
|
38
74
|
loadAvg1?: number;
|
|
39
75
|
ncpu?: number;
|
|
40
76
|
/** Load normalized to core count (the "has room" number): loadAvg1 / ncpu *
|
|
@@ -13,9 +13,43 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { execFile } from 'child_process';
|
|
15
15
|
import { buildSshInvocation, writeAskpassShim } from './connect.js';
|
|
16
|
-
/** Default per-device probe budget
|
|
17
|
-
*
|
|
16
|
+
/** Default per-device probe budget, for a device reachable over a DIRECT
|
|
17
|
+
* Tailscale path. Short enough that the list never hangs on a wedged box. */
|
|
18
18
|
export const PROBE_TIMEOUT_MS = 2_500;
|
|
19
|
+
/**
|
|
20
|
+
* Probe budget for a device whose last handshake was DERP-relayed
|
|
21
|
+
* ({@link DeviceTailscale.direct} === false).
|
|
22
|
+
*
|
|
23
|
+
* This constant used to not exist: 2.5s was applied to every device and its
|
|
24
|
+
* docstring claimed to be "long enough for a cold relayed SSH handshake". That
|
|
25
|
+
* was false, and on a fleet with no direct paths it broke `--device auto`
|
|
26
|
+
* outright (PHNX-3682). Measured on a 9-box relayed fleet, probed in parallel
|
|
27
|
+
* with cold paths: 1686/1805/1914/2688/2706/2749/3082/5586/6588 ms — six of nine
|
|
28
|
+
* over budget, every one of them healthy (rc=0 within 10s). Warm, the same
|
|
29
|
+
* probes take 512-872ms, which is what made the failure intermittent.
|
|
30
|
+
*
|
|
31
|
+
* A relayed hop pays DERP path setup on top of the TCP+SSH handshake, so it
|
|
32
|
+
* gets the same budget the readiness probe already allows
|
|
33
|
+
* (`READY_PROBE_TIMEOUT_MS`) rather than the direct-path one.
|
|
34
|
+
*/
|
|
35
|
+
export const RELAYED_PROBE_TIMEOUT_MS = 8_000;
|
|
36
|
+
/**
|
|
37
|
+
* The probe budget for one device. A relayed peer gets
|
|
38
|
+
* {@link RELAYED_PROBE_TIMEOUT_MS}; a direct (or unknown-path) peer keeps the
|
|
39
|
+
* tight {@link PROBE_TIMEOUT_MS}. Windows keeps its own larger budget, which
|
|
40
|
+
* already exceeds both.
|
|
41
|
+
*
|
|
42
|
+
* `direct` is only meaningful when a tailscale snapshot exists — a
|
|
43
|
+
* `via:"manual"` device never gets a peer entry, so absence is "unknown path",
|
|
44
|
+
* not "relayed", and must not silently widen every manual device's budget.
|
|
45
|
+
*/
|
|
46
|
+
export function probeBudgetMs(device) {
|
|
47
|
+
if (device.shell === 'powershell')
|
|
48
|
+
return WIN_PROBE_TIMEOUT_MS;
|
|
49
|
+
return device.tailscale && device.tailscale.direct === false
|
|
50
|
+
? RELAYED_PROBE_TIMEOUT_MS
|
|
51
|
+
: PROBE_TIMEOUT_MS;
|
|
52
|
+
}
|
|
19
53
|
/** Windows probe budget. The first CIM query of a PowerShell session pays a
|
|
20
54
|
* "Preparing modules for first use" cost on top of PowerShell startup, which
|
|
21
55
|
* routinely blows the 2.5s POSIX budget on a relayed connection. */
|
|
@@ -227,10 +261,14 @@ export function probeDeviceStats(device, opts = {}) {
|
|
|
227
261
|
execFile('ssh', args, {
|
|
228
262
|
encoding: 'utf-8',
|
|
229
263
|
env: { ...process.env, ...env },
|
|
230
|
-
timeout: opts.timeoutMs ?? (
|
|
264
|
+
timeout: opts.timeoutMs ?? probeBudgetMs(device),
|
|
231
265
|
}, (err, stdout) => {
|
|
232
|
-
|
|
233
|
-
|
|
266
|
+
// execFile kills an over-budget child with a signal; that is a slow link,
|
|
267
|
+
// not an unreachable box, and the two must not read the same downstream.
|
|
268
|
+
if (err || !stdout) {
|
|
269
|
+
const timedOut = Boolean(err && err.killed);
|
|
270
|
+
return resolve(timedOut ? { host, reachable: false, timedOut, fetchedAt } : { host, reachable: false, fetchedAt });
|
|
271
|
+
}
|
|
234
272
|
resolve(isWin ? parseWinProbeOutput(host, stdout, fetchedAt) : parseProbeOutput(host, stdout, fetchedAt));
|
|
235
273
|
});
|
|
236
274
|
});
|
|
@@ -21,6 +21,7 @@ import { getDevicesRegistryPath, readMeta, updateMeta } from '../state.js';
|
|
|
21
21
|
import { atomicWriteJsonSync } from '../fs-atomic.js';
|
|
22
22
|
import { machineId } from '../machine-id.js';
|
|
23
23
|
import { addIgnoredEntry, unionDeviceIgnored } from './device-docs.js';
|
|
24
|
+
import { logAndContinueOnLockCompromised } from '../lock-compromise.js';
|
|
24
25
|
/**
|
|
25
26
|
* Whether a fan-out should dial this device, honouring the preference stated on
|
|
26
27
|
* {@link DeviceProfile.reachability}: the live SSH probe wins over the cached
|
|
@@ -141,6 +142,7 @@ async function withRegistryLock(p, fn) {
|
|
|
141
142
|
const release = await lockfile.lock(p, {
|
|
142
143
|
retries: { retries: 60, minTimeout: 25, maxTimeout: 250, factor: 1.5 },
|
|
143
144
|
stale: 10_000,
|
|
145
|
+
onCompromised: logAndContinueOnLockCompromised('devices registry'),
|
|
144
146
|
});
|
|
145
147
|
try {
|
|
146
148
|
return await fn();
|
|
@@ -2,7 +2,7 @@ import { type DevicePlacementSignal } from '../teams/scheduler.js';
|
|
|
2
2
|
/** Why a candidate was dropped, for the fail-loud error message. */
|
|
3
3
|
export interface WorkerExclusion {
|
|
4
4
|
device: string;
|
|
5
|
-
reason: 'unreachable' | 'overloaded' | 'wrong-platform' | 'interactive';
|
|
5
|
+
reason: 'unreachable' | 'probe timed out' | 'overloaded' | 'wrong-platform' | 'interactive';
|
|
6
6
|
}
|
|
7
7
|
export interface WorkerPickPlan {
|
|
8
8
|
/** The chosen device name. Never the local box unless it is an auto-pool member. */
|
|
@@ -79,7 +79,10 @@ export async function resolveWorkerDevice(opts = {}) {
|
|
|
79
79
|
const eligible = onPlatform.filter((device) => {
|
|
80
80
|
const signal = signals.get(device);
|
|
81
81
|
if (signal?.reachable !== true) {
|
|
82
|
-
|
|
82
|
+
// A probe killed for exceeding its budget says nothing about whether the
|
|
83
|
+
// box is up — on a relayed fleet it usually is. Report the two apart so
|
|
84
|
+
// the message points at the link, not at a fleet outage (PHNX-3682).
|
|
85
|
+
excluded.push({ device, reason: signal?.timedOut ? 'probe timed out' : 'unreachable' });
|
|
83
86
|
return false;
|
|
84
87
|
}
|
|
85
88
|
if (signal.headroom === 'loaded') {
|
package/dist/lib/exec.js
CHANGED
|
@@ -40,6 +40,7 @@ import { resolveHarnessAdapter, stripForeignConfigDir } from './harness/index.js
|
|
|
40
40
|
import { resolveConfigVersion } from './harness/exec-config-version.js';
|
|
41
41
|
import { getAccountInfo } from './agents.js';
|
|
42
42
|
import { getUsageLookupKey, noteClaudeSessionLimit, noteClaudeOutOfCredits, clearClaudeAccountRefusal, parseClaudeSessionLimitReset } from './accounting/usage.js';
|
|
43
|
+
import { bootMark, flushBootProfile } from './boot-profile.js';
|
|
43
44
|
/**
|
|
44
45
|
* Map a raw mode string (CLI flag, YAML field, env var) to the canonical Mode.
|
|
45
46
|
*
|
|
@@ -409,6 +410,17 @@ export function buildExecEnv(options) {
|
|
|
409
410
|
// re-parsing harness-specific transcripts.
|
|
410
411
|
result.AGENTS_RUN_MODE = resolveHeadlessMode(options.agent, normalizeMode(options.mode), resolveInteractive(options), options.modeWarningContext, options.modeWarningState);
|
|
411
412
|
result.AGENTS_HISTORY_DIR = getHistoryDir();
|
|
413
|
+
// Durable origin version for a later native resume. The SessionStart hook joins
|
|
414
|
+
// this to the harness's real session id (like AGENTS_RUN_MODE), so recovery can
|
|
415
|
+
// pin the exact origin version even when the transcript carries no derivable one
|
|
416
|
+
// (codex's `.codex-homes/<version>/` home) — the "version not recorded" fallback
|
|
417
|
+
// to `/continue` this closes (PHNX-3626). Resolve the concrete version-home id
|
|
418
|
+
// the same way the spawn path does; only stamp a real one.
|
|
419
|
+
if (options.agent) {
|
|
420
|
+
const runVersion = options.version ?? resolveVersion(options.agent, options.cwd || process.cwd());
|
|
421
|
+
if (runVersion)
|
|
422
|
+
result.AGENTS_RUN_VERSION = runVersion;
|
|
423
|
+
}
|
|
412
424
|
// So activity / feed posts stamp the right harness without re-detecting.
|
|
413
425
|
// A custom-harness run (`agents run deepseek`) must stamp the PROFILE name,
|
|
414
426
|
// not the host CLI (`claude`) — otherwise sessions and feed posts cannot
|
|
@@ -1722,6 +1734,7 @@ async function emitRunLaunch(ctx) {
|
|
|
1722
1734
|
}
|
|
1723
1735
|
}
|
|
1724
1736
|
async function spawnAgent(options) {
|
|
1737
|
+
bootMark('spawn-agent:enter');
|
|
1725
1738
|
// Assign a known session id up front for agents that accept one, so the
|
|
1726
1739
|
// launcher can record an EXACT pid -> session mapping (see pid-registry) —
|
|
1727
1740
|
// otherwise the headless `ag sessions --active` path can only guess
|
|
@@ -1856,6 +1869,7 @@ async function spawnAgent(options) {
|
|
|
1856
1869
|
});
|
|
1857
1870
|
if (tmuxWrap.kind === 'wrap') {
|
|
1858
1871
|
timer.mark('startup');
|
|
1872
|
+
flushBootProfile('spawn');
|
|
1859
1873
|
try {
|
|
1860
1874
|
const result = await runInTmux(options, executable, args);
|
|
1861
1875
|
timer.end({ exitCode: result.exitCode, status: result.exitCode === 0 ? 'success' : 'failed' });
|
|
@@ -1887,6 +1901,7 @@ async function spawnAgent(options) {
|
|
|
1887
1901
|
const useShell = process.platform === 'win32' && (!path.isAbsolute(executable) || executable.endsWith('.cmd'));
|
|
1888
1902
|
const spawnCommand = useShell ? composeWin32CommandLine(executable, args) : executable;
|
|
1889
1903
|
const spawnArgs = useShell ? [] : args;
|
|
1904
|
+
flushBootProfile('spawn');
|
|
1890
1905
|
const child = spawn(spawnCommand, spawnArgs, {
|
|
1891
1906
|
cwd: options.cwd || process.cwd(),
|
|
1892
1907
|
stdio,
|
package/dist/lib/feed/watch.d.ts
CHANGED
package/dist/lib/feed/watch.js
CHANGED
|
@@ -21,13 +21,20 @@ export class FeedWatchState {
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
async function attentionFor(agent) {
|
|
24
|
-
|
|
24
|
+
// Durable Previous rows share the operator stream for Sessions history, but
|
|
25
|
+
// they are not live work and must never synthesize Needs-you attention.
|
|
26
|
+
if (!agent.sessionId || agent.previous || agent.context === 'recent')
|
|
25
27
|
return undefined;
|
|
26
28
|
const blockId = blockIdForSession(agent.sessionId);
|
|
27
29
|
// ActiveSession.host names the terminal app; the feed contract's host is the
|
|
28
30
|
// device scope. Normalize only the reconciler input so lifecycle/PR keys are
|
|
29
31
|
// routable across the fleet while the projected agent row stays compatible.
|
|
30
|
-
const session = {
|
|
32
|
+
const session = {
|
|
33
|
+
...agent,
|
|
34
|
+
context: agent.context,
|
|
35
|
+
host: agent.sourceDevice,
|
|
36
|
+
viewingIn: undefined,
|
|
37
|
+
};
|
|
31
38
|
return reconcileAttention({
|
|
32
39
|
block: readBlock(blockId), session,
|
|
33
40
|
resolution: readResolution(blockId),
|
|
@@ -49,7 +56,10 @@ export async function projectSessionEnvelope(event, state) {
|
|
|
49
56
|
];
|
|
50
57
|
}
|
|
51
58
|
if (event.type === 'remove')
|
|
52
|
-
return [
|
|
59
|
+
return [
|
|
60
|
+
state.emit({ type: 'agent.remove', scope: event.scope, rowKey: event.rowKey }),
|
|
61
|
+
state.emit({ type: 'attention.remove', scope: event.scope, rowKey: event.rowKey }),
|
|
62
|
+
];
|
|
53
63
|
if (event.type === 'scope')
|
|
54
64
|
return [state.emit({ type: 'scope', capturedAt: event.capturedAt, scope: event.scope, status: event.status, ...(event.reason ? { reason: event.reason } : {}) })];
|
|
55
65
|
return [state.emit({ type: 'heartbeat', capturedAt: event.capturedAt, scope: event.scope })];
|
|
@@ -136,11 +136,56 @@ export interface SinkOutcome {
|
|
|
136
136
|
export declare function shortHost(host: string | undefined): string | undefined;
|
|
137
137
|
/** First 8 hex chars of a session id for the footer (readable, not a full uuid). */
|
|
138
138
|
export declare function shortSessionChunk(session: string | undefined): string | undefined;
|
|
139
|
+
/**
|
|
140
|
+
* Tap-to-view link for the session behind a post: the addressable console page
|
|
141
|
+
* ({@link https://prix.dev/console/sessions/<id>}, prix/web). The footer already
|
|
142
|
+
* carries a short session crumb for disambiguation; this rides the link trail so
|
|
143
|
+
* the owner can open the full transcript straight from an iMessage broadcast
|
|
144
|
+
* instead of hunting for it in the console.
|
|
145
|
+
*
|
|
146
|
+
* Accepts any real, path-safe session id — a Claude/Codex UUID *and* a native
|
|
147
|
+
* `ses_…` id from OpenCode or another harness. The console shard uploader
|
|
148
|
+
* (`traces/sync.ts`) syncs sessions with no harness filter, so all of them are
|
|
149
|
+
* addressable; a UUID-only gate would silently drop the link for every non-Claude
|
|
150
|
+
* harness (the whole point of the link). Reject only an id that could not resolve:
|
|
151
|
+
* one with a path separator (URL-unsafe, via {@link isValidMailboxId}) or the bare
|
|
152
|
+
* 8-char footer crumb (a truncated id that would 404).
|
|
153
|
+
*/
|
|
154
|
+
export declare function sessionConsoleUrl(session: string | undefined): string | undefined;
|
|
139
155
|
/**
|
|
140
156
|
* Scrub em/en dashes from outbound phone copy (house rule + iMessage readability).
|
|
141
157
|
* Collapses whitespace; does not invent meaning.
|
|
142
158
|
*/
|
|
143
159
|
export declare function scrubOutboundDashes(text: string): string;
|
|
160
|
+
/**
|
|
161
|
+
* The rendering vocabulary a sink can display, which decides how the shared
|
|
162
|
+
* `{message}` surfaces its links (PHNX-3698):
|
|
163
|
+
*
|
|
164
|
+
* - `mrkdwn` — Slack, which renders `<url|label>` as blue tappable text. The
|
|
165
|
+
* session crumb and every ticket key the prose NAMES become inline labeled
|
|
166
|
+
* links, so nothing rides a trailing naked-URL line.
|
|
167
|
+
* - `plain` — iMessage, the owner-scoped rush message, a spawned `command:`
|
|
168
|
+
* sink, desktop banners: none can render a labeled link and a dumped naked
|
|
169
|
+
* URL reads as noise, so the message stays the human sentence with no URLs.
|
|
170
|
+
*
|
|
171
|
+
* The default is `plain`; only a Slack `channel:` sink opts into `mrkdwn`.
|
|
172
|
+
*/
|
|
173
|
+
export type SinkMessageFormat = 'plain' | 'mrkdwn';
|
|
174
|
+
/**
|
|
175
|
+
* Only Slack renders `<url|label>`, so it is the one format that gets labeled
|
|
176
|
+
* links. iMessage / owner-scoped rush / command / desktop sinks stay `plain`
|
|
177
|
+
* (they can't turn `claude/6fc1db18` blue, and dumping the raw URL is worse than
|
|
178
|
+
* leaving the crumb unlinked — PHNX-3698).
|
|
179
|
+
*
|
|
180
|
+
* The argument is the **resolved provider name**, not the sink's declared
|
|
181
|
+
* channel: an operator can point an arbitrary channel name at the Slack provider
|
|
182
|
+
* through `notify.transports` (e.g. `eng-alerts -> slack`), and delivery keys off
|
|
183
|
+
* that resolved provider (`lookupTransport`), so the format decision must too —
|
|
184
|
+
* otherwise an aliased Slack sink would compose plain while delivering to Slack,
|
|
185
|
+
* or a name remapped AWAY from Slack would emit `<url|label>` markup a non-Slack
|
|
186
|
+
* transport shows literally. {@link resolveSinkProvider} does the mapping.
|
|
187
|
+
*/
|
|
188
|
+
export declare function sinkMessageFormat(provider: string | undefined): SinkMessageFormat;
|
|
144
189
|
/**
|
|
145
190
|
* Footer like "Sent from my iPhone" — who posted, a session crumb, which box.
|
|
146
191
|
*
|
|
@@ -148,10 +193,15 @@ export declare function scrubOutboundDashes(text: string): string;
|
|
|
148
193
|
*
|
|
149
194
|
* Agent name first; session chunk for disambiguation when many groks run;
|
|
150
195
|
* host last. Skip the uninformative default label `agent`.
|
|
196
|
+
*
|
|
197
|
+
* In `mrkdwn` the crumb (`agent/short`) becomes a Slack labeled link to the
|
|
198
|
+
* session's console page, so the human sentence reads identically while the
|
|
199
|
+
* crumb turns blue and taps through (PHNX-3698). `plain` keeps the bare sentence
|
|
200
|
+
* — it can't render a labeled link and must not dump the URL.
|
|
151
201
|
*/
|
|
152
|
-
export declare function composeBroadcastFooter(ctx: FeedBroadcastContext): string | undefined;
|
|
202
|
+
export declare function composeBroadcastFooter(ctx: FeedBroadcastContext, format?: SinkMessageFormat): string | undefined;
|
|
153
203
|
export declare function truncateBroadcastBody(body: string): string;
|
|
154
|
-
export declare function composeBroadcastMessage(ctx: FeedBroadcastContext): string;
|
|
204
|
+
export declare function composeBroadcastMessage(ctx: FeedBroadcastContext, format?: SinkMessageFormat): string;
|
|
155
205
|
/**
|
|
156
206
|
* Substitute `{placeholder}` tokens in an argv template. Returns undefined when
|
|
157
207
|
* the template needs a value this post does not have — the sink is then skipped
|
|
@@ -159,8 +209,13 @@ export declare function composeBroadcastMessage(ctx: FeedBroadcastContext): stri
|
|
|
159
209
|
* would otherwise comment on nothing.
|
|
160
210
|
*/
|
|
161
211
|
export declare function renderSinkArgv(template: string[], ctx: FeedBroadcastContext): string[] | undefined;
|
|
162
|
-
/**
|
|
163
|
-
|
|
212
|
+
/**
|
|
213
|
+
* Render one channel-message template with the same fail-closed placeholder
|
|
214
|
+
* contract as argv. `format` (Slack `mrkdwn` vs `plain`) flows into the shared
|
|
215
|
+
* `{message}` var so a Slack sink gets labeled links and an iMessage/owner sink
|
|
216
|
+
* gets the plain sentence.
|
|
217
|
+
*/
|
|
218
|
+
export declare function renderSinkMessage(template: string, ctx: FeedBroadcastContext, format?: SinkMessageFormat): string | undefined;
|
|
164
219
|
/**
|
|
165
220
|
* Which sinks this post reaches, in config order. Pure — the dry-run listing and
|
|
166
221
|
* the real fan-out plan through here, so what `--dry-run` shows is what runs.
|
|
@@ -168,8 +223,12 @@ export declare function renderSinkMessage(template: string, ctx: FeedBroadcastCo
|
|
|
168
223
|
* A `channel:` sink is gated by the same `minLevel` rule as a `command:` sink —
|
|
169
224
|
* one level check for both shapes, so a dry-run plan is truthful regardless of
|
|
170
225
|
* which shape an operator's sink uses.
|
|
226
|
+
*
|
|
227
|
+
* `meta` is used only to resolve a channel name to its real provider for the
|
|
228
|
+
* mrkdwn/plain format decision (`notify.transports`), the same map delivery uses;
|
|
229
|
+
* it is optional so a test can plan without a config snapshot (identity mapping).
|
|
171
230
|
*/
|
|
172
|
-
export declare function planFeedBroadcast(config: FeedBroadcastConfig | undefined, ctx: FeedBroadcastContext): PlannedSink[];
|
|
231
|
+
export declare function planFeedBroadcast(config: FeedBroadcastConfig | undefined, ctx: FeedBroadcastContext, meta?: Meta): PlannedSink[];
|
|
173
232
|
/**
|
|
174
233
|
* The effective sink config for a post: the operator's `feed.broadcast`, or —
|
|
175
234
|
* when that is unset or empty — an implicit fallback straight to
|