@indigoai-us/hq-cli 5.156.0 → 5.158.0
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 +23 -0
- package/dist/command-catalog.generated.d.ts +101 -0
- package/dist/command-catalog.generated.js +130 -0
- package/dist/commands/agent-mcp.d.ts +1 -3
- package/dist/commands/agent-mcp.js +5 -26
- package/dist/commands/bot.js +13 -2
- package/dist/commands/lanes.d.ts +7 -0
- package/dist/commands/lanes.js +892 -46
- package/dist/lib/agent-kit/attachment-download.d.ts +24 -0
- package/dist/lib/agent-kit/attachment-download.js +47 -0
- package/dist/lib/agent-kit/inbox-state.d.ts +6 -2
- package/dist/lib/agent-kit/inbox-state.js +6 -2
- package/dist/lib/agent-kit/mcp/attachments.d.ts +28 -1
- package/dist/lib/agent-kit/mcp/attachments.js +48 -2
- package/dist/lib/agent-kit/mcp/tools.js +13 -19
- package/dist/lib/bot/api.d.ts +9 -0
- package/dist/lib/bot/attachments.d.ts +45 -0
- package/dist/lib/bot/attachments.js +89 -0
- package/dist/lib/bot/paths.d.ts +5 -0
- package/dist/lib/bot/paths.js +7 -0
- package/dist/lib/bot/run.d.ts +7 -0
- package/dist/lib/bot/run.js +34 -4
- package/dist/lib/bot/runtime/claude.d.ts +1 -0
- package/dist/lib/bot/runtime/claude.js +6 -0
- package/dist/lib/bot/runtime/types.d.ts +8 -0
- package/dist/lib/lanes/admission.d.ts +2 -0
- package/dist/lib/lanes/admission.js +7 -3
- package/dist/lib/lanes/host-capacity.d.ts +46 -0
- package/dist/lib/lanes/host-capacity.js +173 -0
- package/dist/lib/lanes/index.d.ts +4 -2
- package/dist/lib/lanes/index.js +2 -1
- package/dist/lib/lanes/outpost-dispatch.d.ts +11 -1
- package/dist/lib/lanes/outpost-dispatch.js +38 -2
- package/dist/lib/lanes/outpost-identity.d.ts +33 -0
- package/dist/lib/lanes/outpost-identity.js +59 -0
- package/dist/lib/lanes/spawn.js +11 -0
- package/dist/lib/lanes/store.js +12 -0
- package/dist/lib/lanes/types.d.ts +10 -0
- package/dist/lib/lanes/types.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { LaneHost, LaneProvider, LaneRecord, LanesConfig } from "./types.js";
|
|
2
|
+
export interface HostCapacityAccount {
|
|
3
|
+
enabled: boolean;
|
|
4
|
+
logged_in: boolean | null;
|
|
5
|
+
max_running: number;
|
|
6
|
+
running: number;
|
|
7
|
+
queued: number;
|
|
8
|
+
}
|
|
9
|
+
export interface HostCapacityProvider {
|
|
10
|
+
max_running: number;
|
|
11
|
+
max_queued: number;
|
|
12
|
+
running: number;
|
|
13
|
+
queued: number;
|
|
14
|
+
accounts: Record<string, HostCapacityAccount>;
|
|
15
|
+
}
|
|
16
|
+
export interface HostCapacitySnapshot {
|
|
17
|
+
schema_version: "hq-lanes-host-capacity.v1";
|
|
18
|
+
host: LaneHost;
|
|
19
|
+
sampled_at: string;
|
|
20
|
+
providers: Record<LaneProvider, HostCapacityProvider>;
|
|
21
|
+
}
|
|
22
|
+
export interface HostPlacementChoice {
|
|
23
|
+
host: LaneHost;
|
|
24
|
+
account: string;
|
|
25
|
+
queued: boolean;
|
|
26
|
+
}
|
|
27
|
+
/** Snapshot the same per-host running-slot predicate used by lane admission. */
|
|
28
|
+
export declare function buildHostCapacitySnapshot(input: {
|
|
29
|
+
host: LaneHost;
|
|
30
|
+
lanes: readonly LaneRecord[];
|
|
31
|
+
config: LanesConfig;
|
|
32
|
+
existsSync: (path: string) => boolean;
|
|
33
|
+
now: Date;
|
|
34
|
+
}): HostCapacitySnapshot;
|
|
35
|
+
/** Reject malformed remote snapshots before they can influence host selection. */
|
|
36
|
+
export declare function parseHostCapacitySnapshot(raw: unknown): HostCapacitySnapshot;
|
|
37
|
+
/**
|
|
38
|
+
* Choose the least-loaded eligible host/account from advisory snapshots. The
|
|
39
|
+
* selected host still runs the shared admission check under its own lock.
|
|
40
|
+
*/
|
|
41
|
+
export declare function chooseHostForLane(snapshots: readonly HostCapacitySnapshot[], request: {
|
|
42
|
+
provider: LaneProvider;
|
|
43
|
+
account?: string;
|
|
44
|
+
allowedAccounts?: readonly string[];
|
|
45
|
+
}): HostPlacementChoice;
|
|
46
|
+
//# sourceMappingURL=host-capacity.d.ts.map
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { accountStatus } from "./accounts.js";
|
|
2
|
+
import { accountMaxRunning } from "./config.js";
|
|
3
|
+
import { laneOccupiesRunningSlot, laneRunsOnHost, } from "./admission.js";
|
|
4
|
+
import { LanesError } from "./errors.js";
|
|
5
|
+
function admissionHost(host) {
|
|
6
|
+
return host.kind === "outpost"
|
|
7
|
+
? { hostname: "", outpostId: host.id }
|
|
8
|
+
: { hostname: host.id, outpostId: null };
|
|
9
|
+
}
|
|
10
|
+
/** Snapshot the same per-host running-slot predicate used by lane admission. */
|
|
11
|
+
export function buildHostCapacitySnapshot(input) {
|
|
12
|
+
const at = input.now.getTime();
|
|
13
|
+
const onHost = input.lanes.filter((lane) => laneRunsOnHost(lane, admissionHost(input.host)));
|
|
14
|
+
const providers = {};
|
|
15
|
+
for (const provider of ["claude", "codex", "grok"]) {
|
|
16
|
+
const providerConfig = input.config.providers[provider];
|
|
17
|
+
const providerLanes = onHost.filter((lane) => lane.provider === provider);
|
|
18
|
+
const running = providerLanes.filter((lane) => laneOccupiesRunningSlot(lane, at));
|
|
19
|
+
const queued = providerLanes.filter((lane) => lane.state === "queued");
|
|
20
|
+
const accounts = {};
|
|
21
|
+
for (const [id, account] of Object.entries(providerConfig.accounts)) {
|
|
22
|
+
const status = accountStatus(provider, id, input.config, {
|
|
23
|
+
existsSync: input.existsSync,
|
|
24
|
+
});
|
|
25
|
+
accounts[id] = {
|
|
26
|
+
enabled: account.enabled,
|
|
27
|
+
logged_in: status.logged_in,
|
|
28
|
+
max_running: accountMaxRunning(providerConfig, id),
|
|
29
|
+
running: running.filter((lane) => lane.account === id).length,
|
|
30
|
+
queued: queued.filter((lane) => lane.account === id).length,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
providers[provider] = {
|
|
34
|
+
max_running: providerConfig.max_running,
|
|
35
|
+
max_queued: providerConfig.max_queued,
|
|
36
|
+
running: running.length,
|
|
37
|
+
queued: queued.length,
|
|
38
|
+
accounts,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
schema_version: "hq-lanes-host-capacity.v1",
|
|
43
|
+
host: input.host,
|
|
44
|
+
sampled_at: input.now.toISOString(),
|
|
45
|
+
providers,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function objectValue(value) {
|
|
49
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
50
|
+
? value
|
|
51
|
+
: undefined;
|
|
52
|
+
}
|
|
53
|
+
function requireCount(value, field, positive = false) {
|
|
54
|
+
if (typeof value !== "number" ||
|
|
55
|
+
!Number.isInteger(value) ||
|
|
56
|
+
value < (positive ? 1 : 0)) {
|
|
57
|
+
throw new LanesError("host_capacity_invalid", `Invalid host capacity field ${field}`, {
|
|
58
|
+
field,
|
|
59
|
+
value: value ?? null,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
/** Reject malformed remote snapshots before they can influence host selection. */
|
|
65
|
+
export function parseHostCapacitySnapshot(raw) {
|
|
66
|
+
const root = objectValue(raw);
|
|
67
|
+
const hostRaw = objectValue(root?.host);
|
|
68
|
+
const providersRaw = objectValue(root?.providers);
|
|
69
|
+
if (root?.schema_version !== "hq-lanes-host-capacity.v1" ||
|
|
70
|
+
typeof root.sampled_at !== "string" ||
|
|
71
|
+
!Number.isFinite(Date.parse(root.sampled_at)) ||
|
|
72
|
+
(hostRaw?.kind !== "local" && hostRaw?.kind !== "outpost") ||
|
|
73
|
+
typeof hostRaw.id !== "string" ||
|
|
74
|
+
hostRaw.id.trim() === "" ||
|
|
75
|
+
providersRaw === undefined) {
|
|
76
|
+
throw new LanesError("host_capacity_invalid", "Malformed host capacity snapshot", {});
|
|
77
|
+
}
|
|
78
|
+
const providers = {};
|
|
79
|
+
for (const provider of ["claude", "codex", "grok"]) {
|
|
80
|
+
const row = objectValue(providersRaw[provider]);
|
|
81
|
+
const accountsRaw = objectValue(row?.accounts);
|
|
82
|
+
if (row === undefined || accountsRaw === undefined) {
|
|
83
|
+
throw new LanesError("host_capacity_invalid", `Missing capacity for ${provider}`, {
|
|
84
|
+
provider,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
const accounts = {};
|
|
88
|
+
for (const [id, accountValue] of Object.entries(accountsRaw)) {
|
|
89
|
+
const account = objectValue(accountValue);
|
|
90
|
+
if (id.trim() === "" || account === undefined ||
|
|
91
|
+
typeof account.enabled !== "boolean" ||
|
|
92
|
+
!(account.logged_in === null || typeof account.logged_in === "boolean")) {
|
|
93
|
+
throw new LanesError("host_capacity_invalid", `Malformed ${provider} account capacity`, {
|
|
94
|
+
provider,
|
|
95
|
+
account: id,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
accounts[id] = {
|
|
99
|
+
enabled: account.enabled,
|
|
100
|
+
logged_in: account.logged_in,
|
|
101
|
+
max_running: requireCount(account.max_running, `${provider}.${id}.max_running`, true),
|
|
102
|
+
running: requireCount(account.running, `${provider}.${id}.running`),
|
|
103
|
+
queued: requireCount(account.queued, `${provider}.${id}.queued`),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
providers[provider] = {
|
|
107
|
+
max_running: requireCount(row.max_running, `${provider}.max_running`, true),
|
|
108
|
+
max_queued: requireCount(row.max_queued, `${provider}.max_queued`, true),
|
|
109
|
+
running: requireCount(row.running, `${provider}.running`),
|
|
110
|
+
queued: requireCount(row.queued, `${provider}.queued`),
|
|
111
|
+
accounts,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
schema_version: "hq-lanes-host-capacity.v1",
|
|
116
|
+
host: { kind: hostRaw.kind, id: hostRaw.id },
|
|
117
|
+
sampled_at: root.sampled_at,
|
|
118
|
+
providers,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Choose the least-loaded eligible host/account from advisory snapshots. The
|
|
123
|
+
* selected host still runs the shared admission check under its own lock.
|
|
124
|
+
*/
|
|
125
|
+
export function chooseHostForLane(snapshots, request) {
|
|
126
|
+
const candidates = [];
|
|
127
|
+
const allowed = request.account === undefined
|
|
128
|
+
? request.allowedAccounts
|
|
129
|
+
: request.allowedAccounts === undefined
|
|
130
|
+
? [request.account]
|
|
131
|
+
: request.allowedAccounts.filter((account) => account === request.account);
|
|
132
|
+
for (const snapshot of snapshots) {
|
|
133
|
+
const provider = snapshot.providers[request.provider];
|
|
134
|
+
if (!provider)
|
|
135
|
+
continue;
|
|
136
|
+
const accounts = Object.entries(provider.accounts)
|
|
137
|
+
.filter(([id, account]) => (allowed === undefined || allowed.includes(id)) &&
|
|
138
|
+
account.enabled &&
|
|
139
|
+
account.logged_in === true);
|
|
140
|
+
for (const [accountId, account] of accounts) {
|
|
141
|
+
const hasRunningSlot = provider.running < provider.max_running &&
|
|
142
|
+
account.running < account.max_running;
|
|
143
|
+
const hasQueueSlot = provider.queued < provider.max_queued;
|
|
144
|
+
if (!hasRunningSlot && !hasQueueSlot)
|
|
145
|
+
continue;
|
|
146
|
+
candidates.push({
|
|
147
|
+
host: snapshot.host,
|
|
148
|
+
account: accountId,
|
|
149
|
+
queued: !hasRunningSlot,
|
|
150
|
+
score: provider.running / provider.max_running +
|
|
151
|
+
provider.queued / provider.max_queued +
|
|
152
|
+
account.running / account.max_running,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
candidates.sort((a, b) => Number(a.queued) - Number(b.queued) ||
|
|
157
|
+
a.score - b.score ||
|
|
158
|
+
`${a.host.kind}:${a.host.id}`.localeCompare(`${b.host.kind}:${b.host.id}`) ||
|
|
159
|
+
a.account.localeCompare(b.account));
|
|
160
|
+
const selected = candidates[0];
|
|
161
|
+
if (!selected) {
|
|
162
|
+
throw new LanesError("lane_placement_no_capacity", `No host has logged-in account capacity for ${request.provider}${request.account ? ` account ${request.account}` : ""}`, {
|
|
163
|
+
provider: request.provider,
|
|
164
|
+
...(request.account !== undefined ? { account: request.account } : {}),
|
|
165
|
+
hosts: snapshots.map((snapshot) => ({
|
|
166
|
+
kind: snapshot.host.kind,
|
|
167
|
+
id: snapshot.host.id,
|
|
168
|
+
})),
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
return { host: selected.host, account: selected.account, queued: selected.queued };
|
|
172
|
+
}
|
|
173
|
+
//# sourceMappingURL=host-capacity.js.map
|
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export { AccountResolutionError, LaneNotFoundError, LaneSchemaError, LaneVersionConflict, LanesConfigError, LanesError, WorkerResolutionError, isLaneVersionConflict, isLanesError, } from "./errors.js";
|
|
5
5
|
export { MAX_LANE_DEPTH } from "./constants.js";
|
|
6
|
-
export { compareLaneQueueOrder, hasActiveAdmissionReservation, hasActivePendingResume, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, laneRunsOnHost, withAllLaneAdmissionLocks, withLaneAdmissionLock, } from "./admission.js";
|
|
6
|
+
export { compareLaneQueueOrder, hasActiveAdmissionReservation, hasActivePendingResume, hasEarlierEligibleQueuedLane, laneOccupiesRunningSlot, laneAdmissionCapReason, laneRunsOnHost, withAllLaneAdmissionLocks, withLaneAdmissionLock, } from "./admission.js";
|
|
7
7
|
export type { AdmissionCapReason, AdmissionHost } from "./admission.js";
|
|
8
|
+
export { buildHostCapacitySnapshot, chooseHostForLane, parseHostCapacitySnapshot, } from "./host-capacity.js";
|
|
9
|
+
export type { HostCapacityAccount, HostCapacityProvider, HostCapacitySnapshot, HostPlacementChoice, } from "./host-capacity.js";
|
|
8
10
|
export { CONFIG_SCHEMA_VERSION, ENVELOPE_DECISIONS, ENVELOPE_SCHEMA_VERSION, HISTORY_CAP, HOST_KINDS, LANE_PROVIDERS, LANE_SCHEMA_VERSION, LANE_STORY_STATUSES, LANE_STATES, LIVENESS_STATES, QUESTION_BODY_STORES, QUESTION_SCHEMA_VERSION, QUESTION_STATUSES, QUEUED_REASONS, RAISED_VIA, ROUTING_REASONS, SENIOR_KINDS, STORY_STATUSES, TERMINAL_LANE_STATES, isEnvelopeDecision, isHostKind, isLaneProvider, isLaneState, isLivenessState, isQuestionStatus, isSeniorKind, isTerminalLaneState, parseLaneEnvelope, parseLaneProvider, parseLaneState, parseLivenessState, parseMajorSchema, parseQuestionStatus, parseSeniorKind, } from "./types.js";
|
|
9
|
-
export type { AccountConfig, AttributionHistoryEntry, EnvelopeDecision, EnvelopeQuestion, HostKind, LaneCompanyRef, LaneEnvelope, LaneHost, LaneProvider, LaneRecord, LaneStory, LaneStoryStatus, PendingResume, AccountHistoryEntry, LaneState, LanesConfig, LastEnvelopeRef, LivenessState, LivenessWindows, ProviderConfig, QuestionAnswer, QuestionBodyRef, QuestionRecord, QuestionStatus, QueuedReason, RaisedVia, RoutingHistoryEntry, RoutingReason, SeniorHistoryEntry, SeniorKind, SeniorRef, StoryStatus, TerminalLaneState, ThrottleConfig, } from "./types.js";
|
|
11
|
+
export type { AccountConfig, AttributionHistoryEntry, EnvelopeDecision, EnvelopeQuestion, HostKind, LaneCompanyRef, LaneEnvelope, LaneHost, LaneProvider, LanePlacementPolicy, LaneRecord, LaneStory, LaneStoryStatus, PendingResume, AccountHistoryEntry, LaneState, LanesConfig, LastEnvelopeRef, LivenessState, LivenessWindows, ProviderConfig, QuestionAnswer, QuestionBodyRef, QuestionRecord, QuestionStatus, QueuedReason, RaisedVia, RoutingHistoryEntry, RoutingReason, SeniorHistoryEntry, SeniorKind, SeniorRef, StoryStatus, TerminalLaneState, ThrottleConfig, } from "./types.js";
|
|
10
12
|
export { ACTIVE_LANE_STORY_STATUSES, activeLaneStories, laneStories, laneStory, storyKey, storyStatusForEnvelope, storyWarning, withCurrentStory, withLaneStories, } from "./stories.js";
|
|
11
13
|
export { DEFAULT_ACCOUNT_HOMES, DEFAULT_AUTO_COMPACT_TOKENS, DEFAULT_COMPACT_AFTER_MS, DEFAULT_CPU_BUSY_PAUSE, DEFAULT_CPU_BUSY_RESUME, DEFAULT_LIVE_WITHIN_MS, DEFAULT_LIVENESS_WINDOWS, DEFAULT_MAX_QUEUED, DEFAULT_MIN_AVAILABLE_MEM_MB, DEFAULT_PROVIDER_MAX_RUNNING, DEFAULT_SILENT_LOST_MS, DEFAULT_TURN_START_LOST_MS, LANES_CONFIG_REL, accountMaxRunning, canonicalizeHome, documentedDefaultConfig, expandHome, lanesConfigPath, loadLanesConfig, parseLanesConfig, providerCompactAfterMs, providerAutoCompactTokens, providerQuotaDefaultHoldMs, DEFAULT_QUOTA_HOLD_MS, writeLanesConfig, } from "./config.js";
|
|
12
14
|
export type { ConfigLoadDeps, ConfigWriteDeps } from "./config.js";
|
package/dist/lib/lanes/index.js
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export { AccountResolutionError, LaneNotFoundError, LaneSchemaError, LaneVersionConflict, LanesConfigError, LanesError, WorkerResolutionError, isLaneVersionConflict, isLanesError, } from "./errors.js";
|
|
5
5
|
export { MAX_LANE_DEPTH } from "./constants.js";
|
|
6
|
-
export { compareLaneQueueOrder, hasActiveAdmissionReservation, hasActivePendingResume, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, laneRunsOnHost, withAllLaneAdmissionLocks, withLaneAdmissionLock, } from "./admission.js";
|
|
6
|
+
export { compareLaneQueueOrder, hasActiveAdmissionReservation, hasActivePendingResume, hasEarlierEligibleQueuedLane, laneOccupiesRunningSlot, laneAdmissionCapReason, laneRunsOnHost, withAllLaneAdmissionLocks, withLaneAdmissionLock, } from "./admission.js";
|
|
7
|
+
export { buildHostCapacitySnapshot, chooseHostForLane, parseHostCapacitySnapshot, } from "./host-capacity.js";
|
|
7
8
|
export { CONFIG_SCHEMA_VERSION, ENVELOPE_DECISIONS, ENVELOPE_SCHEMA_VERSION, HISTORY_CAP, HOST_KINDS, LANE_PROVIDERS, LANE_SCHEMA_VERSION, LANE_STORY_STATUSES, LANE_STATES, LIVENESS_STATES, QUESTION_BODY_STORES, QUESTION_SCHEMA_VERSION, QUESTION_STATUSES, QUEUED_REASONS, RAISED_VIA, ROUTING_REASONS, SENIOR_KINDS, STORY_STATUSES, TERMINAL_LANE_STATES, isEnvelopeDecision, isHostKind, isLaneProvider, isLaneState, isLivenessState, isQuestionStatus, isSeniorKind, isTerminalLaneState, parseLaneEnvelope, parseLaneProvider, parseLaneState, parseLivenessState, parseMajorSchema, parseQuestionStatus, parseSeniorKind, } from "./types.js";
|
|
8
9
|
export { ACTIVE_LANE_STORY_STATUSES, activeLaneStories, laneStories, laneStory, storyKey, storyStatusForEnvelope, storyWarning, withCurrentStory, withLaneStories, } from "./stories.js";
|
|
9
10
|
export { DEFAULT_ACCOUNT_HOMES, DEFAULT_AUTO_COMPACT_TOKENS, DEFAULT_COMPACT_AFTER_MS, DEFAULT_CPU_BUSY_PAUSE, DEFAULT_CPU_BUSY_RESUME, DEFAULT_LIVE_WITHIN_MS, DEFAULT_LIVENESS_WINDOWS, DEFAULT_MAX_QUEUED, DEFAULT_MIN_AVAILABLE_MEM_MB, DEFAULT_PROVIDER_MAX_RUNNING, DEFAULT_SILENT_LOST_MS, DEFAULT_TURN_START_LOST_MS, LANES_CONFIG_REL, accountMaxRunning, canonicalizeHome, documentedDefaultConfig, expandHome, lanesConfigPath, loadLanesConfig, parseLanesConfig, providerCompactAfterMs, providerAutoCompactTokens, providerQuotaDefaultHoldMs, DEFAULT_QUOTA_HOLD_MS, writeLanesConfig, } from "./config.js";
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* `outpost_dispatch_failed`, never a queued lane nobody will pick up.
|
|
8
8
|
*/
|
|
9
9
|
import { OutpostsClient } from "@indigoai-us/hq-cloud/outposts";
|
|
10
|
-
import type { LaneRecord } from "./types.js";
|
|
10
|
+
import type { LaneHost, LaneRecord } from "./types.js";
|
|
11
11
|
export declare const PICKUP_EXEC_TIMEOUT_SECONDS = 120;
|
|
12
12
|
export declare const PICKUP_WAIT_MS: number;
|
|
13
13
|
export interface OutpostPickupPayload {
|
|
@@ -57,6 +57,16 @@ export interface OutpostExecPort {
|
|
|
57
57
|
export declare function parseOutpostPickupPayload(raw: unknown): OutpostPickupPayload;
|
|
58
58
|
/** Same wrapping `hq outposts exec` applies: run from the box's HQ folder. */
|
|
59
59
|
export declare function buildOutpostPickupCommand(payloadUrl: string): string;
|
|
60
|
+
/** Bounded, read-only capacity query executed by the target Outpost CLI. */
|
|
61
|
+
export declare function buildOutpostCapacityCommand(): string;
|
|
62
|
+
/** Commit a queued lane move on the host that currently owns its state. */
|
|
63
|
+
export declare function buildOutpostMoveTargetCommand(input: {
|
|
64
|
+
laneId: string;
|
|
65
|
+
host: LaneHost;
|
|
66
|
+
account: string;
|
|
67
|
+
}): string;
|
|
68
|
+
/** Idempotently backfill the marker on the selected Outpost. */
|
|
69
|
+
export declare function buildOutpostIdentityBackfillCommand(expectedId: string): string;
|
|
60
70
|
/** Build the same `hq outposts exec` remote command used by lane pickup. */
|
|
61
71
|
export declare function buildOutpostLaneCommand(input: OutpostLaneCommand): string;
|
|
62
72
|
export declare function stageOutpostPayload(port: Pick<OutpostExecPort, "stageExecInput">, outpostId: string, bytes: Uint8Array, put?: typeof fetch): Promise<{
|
|
@@ -42,6 +42,35 @@ export function parseOutpostPickupPayload(raw) {
|
|
|
42
42
|
export function buildOutpostPickupCommand(payloadUrl) {
|
|
43
43
|
return withRemoteHqDir(joinCommandParts(["hq", "lanes", "pickup", "--payload-url", payloadUrl]));
|
|
44
44
|
}
|
|
45
|
+
/** Bounded, read-only capacity query executed by the target Outpost CLI. */
|
|
46
|
+
export function buildOutpostCapacityCommand() {
|
|
47
|
+
return withRemoteHqDir(joinCommandParts(["hq", "lanes", "capacity", "--json"]));
|
|
48
|
+
}
|
|
49
|
+
/** Commit a queued lane move on the host that currently owns its state. */
|
|
50
|
+
export function buildOutpostMoveTargetCommand(input) {
|
|
51
|
+
return withRemoteHqDir(joinCommandParts([
|
|
52
|
+
"hq",
|
|
53
|
+
"lanes",
|
|
54
|
+
"move-target",
|
|
55
|
+
input.laneId,
|
|
56
|
+
"--host-kind",
|
|
57
|
+
input.host.kind,
|
|
58
|
+
"--host-id",
|
|
59
|
+
input.host.id,
|
|
60
|
+
"--account",
|
|
61
|
+
input.account,
|
|
62
|
+
]));
|
|
63
|
+
}
|
|
64
|
+
/** Idempotently backfill the marker on the selected Outpost. */
|
|
65
|
+
export function buildOutpostIdentityBackfillCommand(expectedId) {
|
|
66
|
+
return withRemoteHqDir(joinCommandParts([
|
|
67
|
+
"hq",
|
|
68
|
+
"lanes",
|
|
69
|
+
"outpost-identity-backfill-target",
|
|
70
|
+
"--expected-id",
|
|
71
|
+
expectedId,
|
|
72
|
+
]));
|
|
73
|
+
}
|
|
45
74
|
/** Build the same `hq outposts exec` remote command used by lane pickup. */
|
|
46
75
|
export function buildOutpostLaneCommand(input) {
|
|
47
76
|
const args = ["hq", "lanes", input.action, input.laneId];
|
|
@@ -88,10 +117,17 @@ export async function dispatchOutpostPickup(port, input) {
|
|
|
88
117
|
};
|
|
89
118
|
}
|
|
90
119
|
catch (err) {
|
|
120
|
+
const outcomeDetails = {
|
|
121
|
+
outpost_id: input.outpostId,
|
|
122
|
+
command_id: submitted.commandId,
|
|
123
|
+
done: false,
|
|
124
|
+
outcome_unknown: true,
|
|
125
|
+
};
|
|
91
126
|
if (ac.signal.aborted) {
|
|
92
|
-
throw new LanesError("outpost_dispatch_failed", `Timed out waiting for Outpost pickup (${waitMs}ms)`,
|
|
127
|
+
throw new LanesError("outpost_dispatch_failed", `Timed out waiting for Outpost pickup (${waitMs}ms)`, outcomeDetails);
|
|
93
128
|
}
|
|
94
|
-
|
|
129
|
+
const message = dispatchErrorMessage(err);
|
|
130
|
+
throw new LanesError("outpost_dispatch_failed", `Failed waiting for Outpost pickup: ${message}`, { ...outcomeDetails, message });
|
|
95
131
|
}
|
|
96
132
|
finally {
|
|
97
133
|
clearTimeout(timer);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export declare const OUTPOST_ID_MARKER_PATH = "/etc/outpost/outpost-id";
|
|
2
|
+
export declare const OUTPOST_ID_MARKER_DIR = "/etc/outpost";
|
|
3
|
+
export type OutpostIdentityValue = {
|
|
4
|
+
kind: "id";
|
|
5
|
+
id: string;
|
|
6
|
+
} | {
|
|
7
|
+
kind: "unknown";
|
|
8
|
+
} | {
|
|
9
|
+
kind: "not_outpost";
|
|
10
|
+
};
|
|
11
|
+
export interface OutpostIdentityMarkerStore {
|
|
12
|
+
read(): OutpostIdentityValue;
|
|
13
|
+
/** Return false if another writer already created the marker. */
|
|
14
|
+
createIfMissing(id: string): boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface OutpostIdentityMarkerWriterOptions {
|
|
17
|
+
markerDir?: string;
|
|
18
|
+
markerPath?: string;
|
|
19
|
+
run?: (input: {
|
|
20
|
+
id: string;
|
|
21
|
+
markerDir: string;
|
|
22
|
+
markerPath: string;
|
|
23
|
+
script: string;
|
|
24
|
+
}) => {
|
|
25
|
+
status: number | null;
|
|
26
|
+
error?: Error;
|
|
27
|
+
stderr?: string;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export declare function validateOutpostIdentityId(id: string): void;
|
|
31
|
+
export declare function backfillOutpostIdentityMarker(expectedId: string, store: OutpostIdentityMarkerStore): "created" | "unchanged";
|
|
32
|
+
export declare function createOutpostIdentityMarkerIfMissing(id: string, options?: OutpostIdentityMarkerWriterOptions): boolean;
|
|
33
|
+
//# sourceMappingURL=outpost-identity.d.ts.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { LanesError } from "./errors.js";
|
|
3
|
+
export const OUTPOST_ID_MARKER_PATH = "/etc/outpost/outpost-id";
|
|
4
|
+
export const OUTPOST_ID_MARKER_DIR = "/etc/outpost";
|
|
5
|
+
const SAFE_OUTPOST_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
|
|
6
|
+
export function validateOutpostIdentityId(id) {
|
|
7
|
+
if (id !== id.trim() || !SAFE_OUTPOST_ID.test(id)) {
|
|
8
|
+
throw new LanesError("outpost_identity_id_invalid", "Outpost identity must be a single-line id containing only letters, numbers, hyphens, or underscores", { value: id });
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function backfillOutpostIdentityMarker(expectedId, store) {
|
|
12
|
+
validateOutpostIdentityId(expectedId);
|
|
13
|
+
const current = store.read();
|
|
14
|
+
if (current.kind === "id") {
|
|
15
|
+
if (current.id !== expectedId) {
|
|
16
|
+
throw outpostIdentityMismatch(expectedId, current.id);
|
|
17
|
+
}
|
|
18
|
+
return "unchanged";
|
|
19
|
+
}
|
|
20
|
+
if (current.kind === "not_outpost") {
|
|
21
|
+
throw new LanesError("outpost_identity_not_host", `This host has no ${OUTPOST_ID_MARKER_DIR} directory; identity backfill only runs on an Outpost`, { expected_id: expectedId, marker_path: OUTPOST_ID_MARKER_PATH });
|
|
22
|
+
}
|
|
23
|
+
let created;
|
|
24
|
+
try {
|
|
25
|
+
created = store.createIfMissing(expectedId);
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
const after = store.read();
|
|
29
|
+
if (after.kind === "id" && after.id === expectedId)
|
|
30
|
+
return "unchanged";
|
|
31
|
+
if (after.kind === "id")
|
|
32
|
+
throw outpostIdentityMismatch(expectedId, after.id);
|
|
33
|
+
throw new LanesError("outpost_identity_backfill_failed", `Could not write ${OUTPOST_ID_MARKER_PATH}: ${err instanceof Error ? err.message : String(err)}`, { expected_id: expectedId, marker_path: OUTPOST_ID_MARKER_PATH });
|
|
34
|
+
}
|
|
35
|
+
const after = store.read();
|
|
36
|
+
if (after.kind === "id") {
|
|
37
|
+
if (after.id !== expectedId)
|
|
38
|
+
throw outpostIdentityMismatch(expectedId, after.id);
|
|
39
|
+
return created ? "created" : "unchanged";
|
|
40
|
+
}
|
|
41
|
+
throw new LanesError("outpost_identity_backfill_failed", `Backfill did not create ${OUTPOST_ID_MARKER_PATH} with expected value ${expectedId}`, { expected_id: expectedId, marker_path: OUTPOST_ID_MARKER_PATH });
|
|
42
|
+
}
|
|
43
|
+
export function createOutpostIdentityMarkerIfMissing(id, options = {}) {
|
|
44
|
+
validateOutpostIdentityId(id);
|
|
45
|
+
const markerDir = options.markerDir ?? OUTPOST_ID_MARKER_DIR;
|
|
46
|
+
const markerPath = options.markerPath ?? OUTPOST_ID_MARKER_PATH;
|
|
47
|
+
const script = `umask 022; install -d -m 0755 "$2" && set -C && printf '%s\\n' "$1" > "$3"`;
|
|
48
|
+
const result = options.run?.({ id, markerDir, markerPath, script }) ?? spawnSync("sudo", ["-n", "sh", "-c", script, "hq-outpost-identity", id, markerDir, markerPath], { encoding: "utf8" });
|
|
49
|
+
if (result.error) {
|
|
50
|
+
throw new LanesError("outpost_identity_backfill_failed", `Could not run sudo to write ${OUTPOST_ID_MARKER_PATH}: ${result.error.message}`, { expected_id: id, marker_path: OUTPOST_ID_MARKER_PATH });
|
|
51
|
+
}
|
|
52
|
+
if (result.status === 0)
|
|
53
|
+
return true;
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
function outpostIdentityMismatch(expectedId, actualId) {
|
|
57
|
+
return new LanesError("outpost_identity_mismatch", `Refusing to replace ${OUTPOST_ID_MARKER_PATH}: it contains ${actualId}, expected ${expectedId}`, { expected_id: expectedId, actual_id: actualId, marker_path: OUTPOST_ID_MARKER_PATH });
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=outpost-identity.js.map
|
package/dist/lib/lanes/spawn.js
CHANGED
|
@@ -1496,6 +1496,17 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
|
|
|
1496
1496
|
const profile = existing.worker_profile
|
|
1497
1497
|
? loadWorkerProfile(hqRoot, existing.worker_profile)
|
|
1498
1498
|
: undefined;
|
|
1499
|
+
if (existing.host.kind === "outpost" &&
|
|
1500
|
+
profile !== undefined &&
|
|
1501
|
+
existing.worker_profile_hash !== profile.contentHash) {
|
|
1502
|
+
throw new LanesError("worker_profile_hash_mismatch", `Worker profile ${JSON.stringify(existing.worker_profile)} on Outpost ${existing.host.id} changed after pickup validation`, {
|
|
1503
|
+
lane_id: existing.lane_id,
|
|
1504
|
+
worker: existing.worker_profile,
|
|
1505
|
+
outpost: existing.host.id,
|
|
1506
|
+
expected_hash: existing.worker_profile_hash ?? null,
|
|
1507
|
+
actual_hash: profile.contentHash,
|
|
1508
|
+
});
|
|
1509
|
+
}
|
|
1499
1510
|
const profileDefaultsApply = profile !== undefined && profile.meta.provider === existing.provider;
|
|
1500
1511
|
const profileModel = profileDefaultsApply ? profile.meta.model : undefined;
|
|
1501
1512
|
const profileEffort = profileDefaultsApply ? profile.meta.effort : undefined;
|
package/dist/lib/lanes/store.js
CHANGED
|
@@ -460,6 +460,18 @@ export function parseLaneRecord(raw, filePath) {
|
|
|
460
460
|
path: filePath,
|
|
461
461
|
});
|
|
462
462
|
}
|
|
463
|
+
if (raw.placement_default !== undefined) {
|
|
464
|
+
const placement = raw.placement_default;
|
|
465
|
+
if (!isPlainObject(placement) ||
|
|
466
|
+
(placement.kind !== "local" && placement.kind !== "auto" && placement.kind !== "outpost") ||
|
|
467
|
+
(placement.kind === "outpost" && (typeof placement.id !== "string" || placement.id.trim() === "")) ||
|
|
468
|
+
(placement.kind !== "outpost" && "id" in placement)) {
|
|
469
|
+
throw new LaneSchemaError("placement_default must be local, auto, or a pinned Outpost", {
|
|
470
|
+
path: filePath,
|
|
471
|
+
field: "placement_default",
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
}
|
|
463
475
|
if (!Array.isArray(raw.attribution_history)) {
|
|
464
476
|
throw new LaneSchemaError("attribution_history must be an array", {
|
|
465
477
|
path: filePath,
|
|
@@ -314,6 +314,7 @@ export interface LaneRecord {
|
|
|
314
314
|
* recorded session with this turn instead of starting fresh from the brief.
|
|
315
315
|
*/
|
|
316
316
|
pending_resume?: PendingResume;
|
|
317
|
+
placement_default?: LanePlacementPolicy;
|
|
317
318
|
host: LaneHost;
|
|
318
319
|
/**
|
|
319
320
|
* Creating machine's identity when `host.kind` is `outpost` (spec §5.2).
|
|
@@ -448,6 +449,15 @@ export interface PendingResume {
|
|
|
448
449
|
compact?: boolean;
|
|
449
450
|
at: string;
|
|
450
451
|
}
|
|
452
|
+
/** Where child lanes created by this manager should be placed by default. */
|
|
453
|
+
export type LanePlacementPolicy = {
|
|
454
|
+
kind: "local";
|
|
455
|
+
} | {
|
|
456
|
+
kind: "auto";
|
|
457
|
+
} | {
|
|
458
|
+
kind: "outpost";
|
|
459
|
+
id: string;
|
|
460
|
+
};
|
|
451
461
|
export interface LaneResumeRecord {
|
|
452
462
|
session_id: string;
|
|
453
463
|
at: string;
|
package/dist/lib/lanes/types.js
CHANGED
|
@@ -220,7 +220,7 @@ export const LANE_FIELD_WRITERS = {
|
|
|
220
220
|
pickup_validation_pending: ["creator", "command", "spawn", "reconcile"],
|
|
221
221
|
pickup_validation_pending_since: ["creator", "command", "spawn", "reconcile"],
|
|
222
222
|
priority: ["command"], worker: ["creator"], worker_path: ["creator"], worker_pack: ["creator"], worker_profile: ["creator"], worker_profile_hash: ["creator", "spawn"], worker_profile_ignored: ["creator", "spawn"],
|
|
223
|
-
provider: ["creator"], account: ["creator", "spawn", "command", "watcher", "reconcile"], admission_accounts: ["creator", "command"], account_history: ["command", "spawn", "watcher", "reconcile"], pending_resume: ["command", "spawn", "watcher", "reconcile"], host: ["creator"], origin_host: ["creator"], origin_host_source: ["creator"],
|
|
223
|
+
provider: ["creator"], account: ["creator", "spawn", "command", "watcher", "reconcile"], admission_accounts: ["creator", "command"], account_history: ["command", "spawn", "watcher", "reconcile"], pending_resume: ["command", "spawn", "watcher", "reconcile"], placement_default: ["creator"], host: ["creator", "command"], origin_host: ["creator"], origin_host_source: ["creator"],
|
|
224
224
|
version: ["creator"], created_at: ["creator"], updated_at: ["creator"], attribution_history: ["command"],
|
|
225
225
|
last_envelope: ["watcher", "reconcile"], model: ["creator", "spawn", "command"], model_source: ["creator", "spawn", "command"], effort: ["creator", "spawn"], effort_source: ["creator", "spawn"], fast_mode: ["spawn"],
|
|
226
226
|
last_message_at: ["command"], last_compaction: ["spawn"], story_complete: ["watcher", "reconcile"], story_complete_reason: ["watcher", "reconcile"], story_status: ["watcher", "reconcile"],
|