@indigoai-us/hq-cli 5.247.0 → 5.248.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 +15 -0
- package/assets/scaffold/core/scripts/hq-session.sh +2 -2
- package/assets/scaffold/core/scripts/lib/session-scope-capability.sh +1 -1
- package/dist/commands/bot.js +10 -4
- package/dist/commands/lanes-message.d.ts +9 -0
- package/dist/commands/lanes-message.js +558 -0
- package/dist/commands/lanes-resume.d.ts +6 -0
- package/dist/commands/lanes-resume.js +455 -0
- package/dist/commands/lanes-shared.d.ts +153 -1
- package/dist/commands/lanes-shared.js +94 -1
- package/dist/commands/lanes.d.ts +1 -1
- package/dist/commands/lanes.js +59 -1086
- package/package.json +1 -1
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
import { BALANCE_MOVABLE_STATES } from "../lib/lanes/claude-balance.js";
|
|
2
|
+
import { LanesError } from "../lib/lanes/errors.js";
|
|
3
|
+
import { appendCappedHistory, providerSessionPortable, providerSupportsSessionResume, readLane, resumeCommandRefusal, sessionNotPortableMessage, validateProjectStories, withLaneAdmissionLock } from "../lib/lanes/index.js";
|
|
4
|
+
import { parseCliEffort } from "../lib/lanes/effort.js";
|
|
5
|
+
import { parseOutpostHireGrantContext } from "../lib/lanes/hire-grant-dispatch.js";
|
|
6
|
+
import { assertNoSecretText } from "../lib/lanes/text-scan.js";
|
|
7
|
+
import { laneStories, laneStory, storyWarning } from "../lib/lanes/stories.js";
|
|
8
|
+
import { spawnLaneWorker } from "../lib/lanes/spawn.js";
|
|
9
|
+
import { accountMoveSummary, accountRequestKeepsCurrentLane, assertCurrentResumeAccountAvailable, hasKeepAliveProgress, recordedResumeEffort, resetKeepAliveProgress, resolveResumeEffort } from "./lanes-shared.js";
|
|
10
|
+
export function createLaneResumeHandlers(deps) {
|
|
11
|
+
const { archiveResumeMessage, assertLaneRunsHereForAdmission, assertLaneRunsHereForOperatorMutation, assessLaneAdmission, automaticBalanceMoveHistoryReason, authorizeFailedResume, authorizeResumeProfileRepin, casUpdateLane, dispatchOutpostLaneCommand, laneIsHostedHere, laneWithRecoveredProviderSession, mintMessageId, persistClaudeAccountSelection, prepareAccountMove, printJson, recordResumeSetup, resolveResumeProfile, selectAutomaticClaudeResumeAccount, validateResumeProfileModel, withResumeHireAdmission } = deps;
|
|
12
|
+
async function runClaudeBalanceMoveTarget(ctx, laneId, opts) {
|
|
13
|
+
const lane = laneWithRecoveredProviderSession(ctx.hqRoot(), readLane(ctx.hqRoot(), laneId));
|
|
14
|
+
assertLaneRunsHereForOperatorMutation(ctx, lane, "balance-move-target");
|
|
15
|
+
if (lane.provider !== "claude") {
|
|
16
|
+
throw new LanesError("claude_account_balance_provider_mismatch", `Lane ${laneId} does not use Claude`, {
|
|
17
|
+
lane_id: laneId,
|
|
18
|
+
provider: lane.provider,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
const expectedFrom = opts.fromAccount?.trim();
|
|
22
|
+
if (!expectedFrom) {
|
|
23
|
+
throw new LanesError("claude_account_balance_source_invalid", "Balance move requires --from-account <id>", {
|
|
24
|
+
lane_id: laneId,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
if (lane.account !== expectedFrom) {
|
|
28
|
+
throw new LanesError("claude_account_balance_source_changed", `Lane ${laneId} no longer uses the planned source account`, {
|
|
29
|
+
lane_id: laneId,
|
|
30
|
+
expected_account: expectedFrom,
|
|
31
|
+
actual_account: lane.account ?? null,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
const expectedState = opts.expectedState?.trim();
|
|
35
|
+
if (!expectedState) {
|
|
36
|
+
throw new LanesError("claude_account_balance_state_invalid", "Balance move requires --expected-state <state>", {
|
|
37
|
+
lane_id: laneId,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
if (lane.state !== expectedState) {
|
|
41
|
+
throw new LanesError("claude_account_balance_state_changed", `Lane ${laneId} changed state after balance was calculated`, {
|
|
42
|
+
lane_id: laneId,
|
|
43
|
+
expected_state: expectedState,
|
|
44
|
+
actual_state: lane.state,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
if (!BALANCE_MOVABLE_STATES.includes(lane.state) || lane.pending_resume !== undefined) {
|
|
48
|
+
throw new LanesError("claude_account_balance_state_not_movable", `Lane ${laneId} is not between turns`, {
|
|
49
|
+
lane_id: laneId,
|
|
50
|
+
state: lane.state,
|
|
51
|
+
pending_resume: lane.pending_resume !== undefined,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
const account = opts.account?.trim();
|
|
55
|
+
if (!account) {
|
|
56
|
+
throw new LanesError("claude_account_balance_account_invalid", "Balance move requires --account <id>", {
|
|
57
|
+
lane_id: laneId,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
const moved = await prepareAccountMove(ctx, lane, {
|
|
61
|
+
account,
|
|
62
|
+
cliModel: undefined,
|
|
63
|
+
accountHistoryReason: automaticBalanceMoveHistoryReason(ctx, lane),
|
|
64
|
+
deferTurnStart: true,
|
|
65
|
+
});
|
|
66
|
+
printJson({
|
|
67
|
+
ok: true,
|
|
68
|
+
action: "balance-move-target",
|
|
69
|
+
lane_id: laneId,
|
|
70
|
+
from: moved.from,
|
|
71
|
+
state: moved.lane.state,
|
|
72
|
+
account: moved.lane.account,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* `hq lanes resume <lane-id> [--text <msg>]`: the explicit opt-in that
|
|
77
|
+
* continues a lane in `interrupted`, `done` or `blocked`, or a failed lane
|
|
78
|
+
* whose only terminal reason is an invalid envelope after its repair turn,
|
|
79
|
+
* on its recorded
|
|
80
|
+
* provider session. `hq lanes message` keeps refusing `interrupted`; this
|
|
81
|
+
* command is the path its refusal names. The launch is the same
|
|
82
|
+
* `spawnLaneWorker` a message resume uses, so the resume-time compaction
|
|
83
|
+
* rule and the watcher's mesh events are the same. The record gains
|
|
84
|
+
* `last_resume` with `trigger: "resume"`, `resumed_from` and the text.
|
|
85
|
+
*/
|
|
86
|
+
async function runResume(ctx, laneId, opts) {
|
|
87
|
+
const hqRoot = ctx.hqRoot();
|
|
88
|
+
let lane = readLane(hqRoot, laneId);
|
|
89
|
+
const failedResumeReason = opts.reason?.trim();
|
|
90
|
+
if (opts.reason !== undefined && opts.fromFailed !== true) {
|
|
91
|
+
throw new LanesError("failed_resume_reason_requires_flag", "--reason on hq lanes resume requires --from-failed", { lane_id: laneId });
|
|
92
|
+
}
|
|
93
|
+
if (opts.fromFailed === true && !failedResumeReason) {
|
|
94
|
+
throw new LanesError("failed_resume_reason_required", "--from-failed requires a non-empty --reason", { lane_id: laneId });
|
|
95
|
+
}
|
|
96
|
+
if (opts.reason !== undefined)
|
|
97
|
+
assertNoSecretText("resume reason", opts.reason);
|
|
98
|
+
if (opts.failedResumeContext !== undefined && opts.fromFailed !== true) {
|
|
99
|
+
throw new LanesError("failed_resume_authorization_invalid", "Failed-resume authorization context requires --from-failed", { lane_id: laneId });
|
|
100
|
+
}
|
|
101
|
+
const failedResumeActor = opts.fromFailed === true
|
|
102
|
+
? authorizeFailedResume(ctx, lane, opts.failedResumeContext)
|
|
103
|
+
: undefined;
|
|
104
|
+
if (opts.fromFailed === true && lane.state !== "failed") {
|
|
105
|
+
throw new LanesError("lane_not_failed", `Lane ${laneId} is ${lane.state}; --from-failed only applies to a failed lane`, { lane_id: laneId, state: lane.state });
|
|
106
|
+
}
|
|
107
|
+
const failedResumeExpectedSenior = opts.fromFailed === true
|
|
108
|
+
? { ...lane.senior }
|
|
109
|
+
: undefined;
|
|
110
|
+
const recoveredFailure = opts.fromFailed === true
|
|
111
|
+
? lane.terminal_reason ?? "failure_reason_unavailable"
|
|
112
|
+
: undefined;
|
|
113
|
+
const hireGrantContext = opts.grantContext === undefined
|
|
114
|
+
? undefined
|
|
115
|
+
: (() => {
|
|
116
|
+
if (!ctx.isSsmOwnerDispatch()) {
|
|
117
|
+
throw new LanesError("hire_grant_dispatch_unauthorized", "Resume grant context is only valid on an Outpost owner dispatch", { lane_id: laneId });
|
|
118
|
+
}
|
|
119
|
+
return parseOutpostHireGrantContext(opts.grantContext, laneId);
|
|
120
|
+
})();
|
|
121
|
+
if (opts.profileRepinContext !== undefined && !ctx.isSsmOwnerDispatch()) {
|
|
122
|
+
throw new LanesError("worker_profile_hash_mismatch", "Profile re-pin context is only valid on an Outpost owner dispatch", { lane_id: laneId });
|
|
123
|
+
}
|
|
124
|
+
if (lane.host.kind === "outpost" && !laneIsHostedHere(ctx, lane)) {
|
|
125
|
+
await withResumeHireAdmission(ctx, lane, () => undefined);
|
|
126
|
+
await dispatchOutpostLaneCommand(ctx, lane, "resume", opts);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
// Fail before provider/session work if a terminal child would consume a
|
|
130
|
+
// slot that its senior has already assigned elsewhere.
|
|
131
|
+
await withResumeHireAdmission(ctx, lane, () => undefined, hireGrantContext);
|
|
132
|
+
assertLaneRunsHereForOperatorMutation(ctx, lane, "resume");
|
|
133
|
+
const cliEffort = parseCliEffort(lane.provider, opts.effort);
|
|
134
|
+
if (cliEffort !== undefined && !providerSupportsSessionResume(lane.provider)) {
|
|
135
|
+
throw new LanesError("effort_resume_unsupported", `Provider ${lane.provider} cannot change effort on a resumed session`, { lane_id: laneId, provider: lane.provider });
|
|
136
|
+
}
|
|
137
|
+
lane = laneWithRecoveredProviderSession(hqRoot, lane);
|
|
138
|
+
if (opts.model !== undefined && opts.account === undefined) {
|
|
139
|
+
throw new LanesError("model_requires_account", "--model on hq lanes resume is only accepted together with --account", {});
|
|
140
|
+
}
|
|
141
|
+
if (opts.account !== undefined && lane.state === "running") {
|
|
142
|
+
throw new LanesError("lane_running", `Lane ${laneId} is running; stop it or wait for it to exit before moving its session`, {
|
|
143
|
+
lane_id: laneId,
|
|
144
|
+
state: lane.state,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
if (opts.account !== undefined && !providerSessionPortable(lane.provider)) {
|
|
148
|
+
throw new LanesError("provider_session_not_portable", sessionNotPortableMessage(lane.provider), { lane_id: laneId, provider: lane.provider });
|
|
149
|
+
}
|
|
150
|
+
if (lane.state === "queued") {
|
|
151
|
+
throw new LanesError("lane_not_resumable", `Lane ${laneId} is queued; use hq lanes admit ${laneId} to request admission`, { lane_id: laneId, state: lane.state, admission_command: `hq lanes admit ${laneId}` });
|
|
152
|
+
}
|
|
153
|
+
const refusal = resumeCommandRefusal(lane, { allowFailed: opts.fromFailed === true });
|
|
154
|
+
if (refusal !== null) {
|
|
155
|
+
throw new LanesError(refusal.error, refusal.message, {
|
|
156
|
+
lane_id: laneId,
|
|
157
|
+
state: lane.state,
|
|
158
|
+
provider: lane.provider,
|
|
159
|
+
message: refusal.message,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
assertLaneRunsHereForAdmission(ctx, lane);
|
|
163
|
+
const resumedFrom = lane.state;
|
|
164
|
+
const text = opts.text?.trim() ? opts.text : undefined;
|
|
165
|
+
const at = ctx.now().toISOString();
|
|
166
|
+
const projectId = opts.project?.trim() || lane.project_id;
|
|
167
|
+
const storyId = opts.story?.trim() || lane.story_id;
|
|
168
|
+
if (opts.story !== undefined && !opts.story.trim()) {
|
|
169
|
+
throw new LanesError("missing_story", "--story must be non-empty", {});
|
|
170
|
+
}
|
|
171
|
+
if ((opts.project !== undefined || opts.story !== undefined) && ctx.storyLedgerEnabled()) {
|
|
172
|
+
validateProjectStories(hqRoot, projectId, [storyId], lane.company?.slug);
|
|
173
|
+
}
|
|
174
|
+
const attributionChanged = projectId !== lane.project_id || storyId !== lane.story_id;
|
|
175
|
+
const messageId = text !== undefined || attributionChanged ? mintMessageId() : undefined;
|
|
176
|
+
let requestedMeshCompany;
|
|
177
|
+
let meshThreadId;
|
|
178
|
+
if (opts.meshCompany !== undefined) {
|
|
179
|
+
const meshCompanySlug = opts.meshCompany.trim();
|
|
180
|
+
if (!meshCompanySlug) {
|
|
181
|
+
throw new LanesError("missing_mesh_company", "--mesh-company must be a non-empty company slug", {});
|
|
182
|
+
}
|
|
183
|
+
requestedMeshCompany = await ctx.resolveCompany(meshCompanySlug);
|
|
184
|
+
if (lane.company !== null &&
|
|
185
|
+
(lane.company.uid !== requestedMeshCompany.uid || lane.company.slug !== requestedMeshCompany.slug)) {
|
|
186
|
+
throw new LanesError("mesh_company_mismatch", "--mesh-company must match the lane's company when the lane is company-owned", { company: lane.company.slug, mesh_company: requestedMeshCompany.slug });
|
|
187
|
+
}
|
|
188
|
+
meshThreadId = await ctx.mesh.ensureProjectThread(requestedMeshCompany, projectId);
|
|
189
|
+
}
|
|
190
|
+
const resumeProfile = opts.worker !== undefined
|
|
191
|
+
? resolveResumeProfile(ctx, lane, opts.worker)
|
|
192
|
+
: undefined;
|
|
193
|
+
const profileRepin = authorizeResumeProfileRepin(ctx, lane, resumeProfile, opts.profileRepinContext);
|
|
194
|
+
const keepAlive = opts.keepAlive === true
|
|
195
|
+
? true
|
|
196
|
+
: resumeProfile?.profile.meta.keep_alive;
|
|
197
|
+
const keepCurrentAccount = opts.account !== undefined && accountRequestKeepsCurrentLane(lane, opts.account);
|
|
198
|
+
if (keepCurrentAccount)
|
|
199
|
+
assertCurrentResumeAccountAvailable(ctx, lane);
|
|
200
|
+
if (resumeProfile !== undefined && (opts.account === undefined || keepCurrentAccount)) {
|
|
201
|
+
validateResumeProfileModel(ctx, lane, resumeProfile.profile);
|
|
202
|
+
}
|
|
203
|
+
const automaticAccountSelection = opts.account === undefined
|
|
204
|
+
? await selectAutomaticClaudeResumeAccount(ctx, lane, resumeProfile, { repairUnavailableAccount: true })
|
|
205
|
+
: undefined;
|
|
206
|
+
const resumeEffort = resolveResumeEffort(ctx, lane, {
|
|
207
|
+
cliEffort,
|
|
208
|
+
profile: resumeProfile,
|
|
209
|
+
account: keepCurrentAccount ? lane.account : opts.account ?? automaticAccountSelection?.selection.account,
|
|
210
|
+
model: opts.model,
|
|
211
|
+
});
|
|
212
|
+
let move = null;
|
|
213
|
+
if (opts.account !== undefined && !keepCurrentAccount) {
|
|
214
|
+
move = await prepareAccountMove(ctx, lane, {
|
|
215
|
+
account: opts.account,
|
|
216
|
+
cliModel: opts.model?.trim() || undefined,
|
|
217
|
+
...(resumeProfile !== undefined ? { workerProfile: resumeProfile } : {}),
|
|
218
|
+
...(profileRepin !== undefined ? { profileRepin } : {}),
|
|
219
|
+
...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
|
|
220
|
+
pending: {
|
|
221
|
+
trigger: "resume",
|
|
222
|
+
resumed_from: resumedFrom,
|
|
223
|
+
...(opts.fromFailed === true ? { from_failed: true } : {}),
|
|
224
|
+
...(failedResumeExpectedSenior !== undefined
|
|
225
|
+
? { expected_senior: failedResumeExpectedSenior }
|
|
226
|
+
: {}),
|
|
227
|
+
...(text !== undefined ? { text } : {}),
|
|
228
|
+
...(messageId !== undefined ? { message_id: messageId } : {}),
|
|
229
|
+
compact: opts.compact !== false,
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
lane = move.lane;
|
|
233
|
+
}
|
|
234
|
+
else if (automaticAccountSelection !== undefined) {
|
|
235
|
+
const selection = automaticAccountSelection.selection;
|
|
236
|
+
if (selection.account !== lane.account) {
|
|
237
|
+
move = await prepareAccountMove(ctx, lane, {
|
|
238
|
+
account: selection.account,
|
|
239
|
+
cliModel: undefined,
|
|
240
|
+
...(automaticAccountSelection.historyReason !== undefined
|
|
241
|
+
? { accountHistoryReason: automaticAccountSelection.historyReason }
|
|
242
|
+
: {}),
|
|
243
|
+
...(automaticAccountSelection.preservedModel !== undefined
|
|
244
|
+
? { preservedModel: automaticAccountSelection.preservedModel }
|
|
245
|
+
: {}),
|
|
246
|
+
...(resumeProfile !== undefined ? { workerProfile: resumeProfile } : {}),
|
|
247
|
+
...(profileRepin !== undefined ? { profileRepin } : {}),
|
|
248
|
+
...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
|
|
249
|
+
pending: {
|
|
250
|
+
trigger: "resume",
|
|
251
|
+
resumed_from: resumedFrom,
|
|
252
|
+
...(opts.fromFailed === true ? { from_failed: true } : {}),
|
|
253
|
+
...(failedResumeExpectedSenior !== undefined
|
|
254
|
+
? { expected_senior: failedResumeExpectedSenior }
|
|
255
|
+
: {}),
|
|
256
|
+
...(text !== undefined ? { text } : {}),
|
|
257
|
+
...(messageId !== undefined ? { message_id: messageId } : {}),
|
|
258
|
+
compact: opts.compact !== false,
|
|
259
|
+
},
|
|
260
|
+
accountSelection: selection,
|
|
261
|
+
});
|
|
262
|
+
lane = move.lane;
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
lane = persistClaudeAccountSelection(hqRoot, lane, selection);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
lane = recordResumeSetup(ctx, lane, {
|
|
269
|
+
...(move === null && resumeProfile !== undefined ? { profile: resumeProfile } : {}),
|
|
270
|
+
...(move === null && profileRepin !== undefined ? { profileRepin } : {}),
|
|
271
|
+
...(move === null && resumeEffort !== undefined ? { effort: resumeEffort } : {}),
|
|
272
|
+
...(keepAlive !== undefined ? { keepAlive } : {}),
|
|
273
|
+
...(move === null && keepCurrentAccount && opts.model !== undefined ? { cliModel: opts.model } : {}),
|
|
274
|
+
...(requestedMeshCompany !== undefined
|
|
275
|
+
? { meshCompany: requestedMeshCompany, threadId: meshThreadId }
|
|
276
|
+
: {}),
|
|
277
|
+
});
|
|
278
|
+
let queuedOnAdmission = false;
|
|
279
|
+
let resumed;
|
|
280
|
+
if (move?.queued) {
|
|
281
|
+
resumed = lane;
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
resumed = await withLaneAdmissionLock(hqRoot, lane.provider, async () => {
|
|
285
|
+
const admission = await assessLaneAdmission(ctx, lane, { queueAt: at });
|
|
286
|
+
if (admission.action === "refuse" && admission.error)
|
|
287
|
+
throw admission.error;
|
|
288
|
+
return withResumeHireAdmission(ctx, lane, () => {
|
|
289
|
+
if (admission.action === "queue") {
|
|
290
|
+
const sessionId = lane.provider_session_id;
|
|
291
|
+
if (!sessionId) {
|
|
292
|
+
throw new LanesError("no_provider_session", `Lane ${laneId} has no provider session id to queue this resume`, {
|
|
293
|
+
lane_id: laneId,
|
|
294
|
+
provider: lane.provider,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
queuedOnAdmission = true;
|
|
298
|
+
return casUpdateLane(hqRoot, laneId, (current) => {
|
|
299
|
+
if (current.version !== lane.version || current.state !== lane.state) {
|
|
300
|
+
throw new LanesError("lane_admission_conflict", `Lane ${laneId} changed while resume admission was checked`, {
|
|
301
|
+
lane_id: laneId,
|
|
302
|
+
prepared_version: lane.version,
|
|
303
|
+
version: current.version,
|
|
304
|
+
state: current.state,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
return {
|
|
308
|
+
...current,
|
|
309
|
+
state: "queued",
|
|
310
|
+
queued_reason: admission.reason ?? "throttle",
|
|
311
|
+
queued_at: at,
|
|
312
|
+
pending_resume: {
|
|
313
|
+
session_id: sessionId,
|
|
314
|
+
trigger: "resume",
|
|
315
|
+
resumed_from: resumedFrom,
|
|
316
|
+
...(opts.fromFailed === true ? { from_failed: true } : {}),
|
|
317
|
+
...(failedResumeExpectedSenior !== undefined
|
|
318
|
+
? { expected_senior: failedResumeExpectedSenior }
|
|
319
|
+
: {}),
|
|
320
|
+
...(text !== undefined ? { text } : {}),
|
|
321
|
+
...(messageId !== undefined ? { message_id: messageId } : {}),
|
|
322
|
+
compact: opts.compact !== false,
|
|
323
|
+
at,
|
|
324
|
+
},
|
|
325
|
+
};
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
return spawnLaneWorker(hqRoot, lane, {
|
|
329
|
+
env: ctx.env,
|
|
330
|
+
modelDeps: {
|
|
331
|
+
readFileSync: ctx.readFileSync,
|
|
332
|
+
probe: ctx.probeProvider,
|
|
333
|
+
env: ctx.env,
|
|
334
|
+
},
|
|
335
|
+
existsSync: ctx.existsSync,
|
|
336
|
+
spawnProcess: ctx.spawnProcess,
|
|
337
|
+
proveDetached: ctx.proveDetached,
|
|
338
|
+
killProcess: ctx.killProcess,
|
|
339
|
+
now: ctx.now,
|
|
340
|
+
resolveMachineIdentity: ctx.resolveMachineIdentity,
|
|
341
|
+
runCompaction: ctx.runCompaction,
|
|
342
|
+
locateTranscript: ctx.locateTranscript,
|
|
343
|
+
}, {
|
|
344
|
+
trigger: {
|
|
345
|
+
kind: "resume",
|
|
346
|
+
from: resumedFrom,
|
|
347
|
+
...(opts.fromFailed === true ? { fromFailed: true } : {}),
|
|
348
|
+
...(failedResumeExpectedSenior !== undefined
|
|
349
|
+
? { expectedSenior: failedResumeExpectedSenior }
|
|
350
|
+
: {}),
|
|
351
|
+
...(text !== undefined ? { text } : {}),
|
|
352
|
+
...(messageId !== undefined ? { message_id: messageId } : {}),
|
|
353
|
+
},
|
|
354
|
+
compact: opts.compact !== false,
|
|
355
|
+
effort: resumeEffort ?? recordedResumeEffort(lane),
|
|
356
|
+
});
|
|
357
|
+
}, hireGrantContext);
|
|
358
|
+
}, ctx.sleep);
|
|
359
|
+
}
|
|
360
|
+
if (text !== undefined && messageId !== undefined) {
|
|
361
|
+
// Same audit copy a message resume leaves; nothing drains it. Archive only
|
|
362
|
+
// after a successful spawn so a concurrent resume cannot leave a phantom.
|
|
363
|
+
archiveResumeMessage(hqRoot, laneId, {
|
|
364
|
+
at,
|
|
365
|
+
message_id: messageId,
|
|
366
|
+
text,
|
|
367
|
+
project_id: projectId,
|
|
368
|
+
story_id: storyId,
|
|
369
|
+
resumed_from: resumedFrom,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
let next = resumed;
|
|
373
|
+
if (failedResumeActor !== undefined &&
|
|
374
|
+
failedResumeReason !== undefined &&
|
|
375
|
+
recoveredFailure !== undefined) {
|
|
376
|
+
next = casUpdateLane(hqRoot, laneId, (current) => ({
|
|
377
|
+
...current,
|
|
378
|
+
failed_resume_history: appendCappedHistory(current.failed_resume_history ?? [], {
|
|
379
|
+
at,
|
|
380
|
+
actor: failedResumeActor,
|
|
381
|
+
reason: failedResumeReason,
|
|
382
|
+
failure: recoveredFailure,
|
|
383
|
+
}),
|
|
384
|
+
}));
|
|
385
|
+
}
|
|
386
|
+
if (text !== undefined) {
|
|
387
|
+
// An operator text moves the compaction reference forward, as a message does.
|
|
388
|
+
next = casUpdateLane(hqRoot, laneId, (current) => ({
|
|
389
|
+
...resetKeepAliveProgress(current),
|
|
390
|
+
last_message_at: at,
|
|
391
|
+
}));
|
|
392
|
+
}
|
|
393
|
+
if (messageId !== undefined && attributionChanged) {
|
|
394
|
+
next = casUpdateLane(hqRoot, laneId, (current) => {
|
|
395
|
+
const already = current.attribution_history.some((entry) => entry.message_id === messageId);
|
|
396
|
+
if (already)
|
|
397
|
+
return current;
|
|
398
|
+
return {
|
|
399
|
+
...current,
|
|
400
|
+
project_id: projectId,
|
|
401
|
+
story_id: storyId,
|
|
402
|
+
no_story_reason: storyId ? undefined : current.no_story_reason,
|
|
403
|
+
stories: laneStory(current, projectId, storyId)
|
|
404
|
+
? laneStories(current)
|
|
405
|
+
: [
|
|
406
|
+
...laneStories(current),
|
|
407
|
+
{
|
|
408
|
+
project_id: projectId,
|
|
409
|
+
story_id: storyId,
|
|
410
|
+
status: "in_progress",
|
|
411
|
+
added_at: at,
|
|
412
|
+
updated_at: at,
|
|
413
|
+
},
|
|
414
|
+
],
|
|
415
|
+
attribution_history: appendCappedHistory(current.attribution_history, {
|
|
416
|
+
project_id: projectId,
|
|
417
|
+
story_id: storyId,
|
|
418
|
+
message_id: messageId,
|
|
419
|
+
at,
|
|
420
|
+
}),
|
|
421
|
+
};
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
if (text === undefined && hasKeepAliveProgress(next)) {
|
|
425
|
+
next = casUpdateLane(hqRoot, laneId, (current) => resetKeepAliveProgress(current));
|
|
426
|
+
}
|
|
427
|
+
printJson({
|
|
428
|
+
ok: true,
|
|
429
|
+
action: "resume",
|
|
430
|
+
lane_id: laneId,
|
|
431
|
+
resumed_from: resumedFrom,
|
|
432
|
+
message_id: messageId ?? null,
|
|
433
|
+
text: text ?? null,
|
|
434
|
+
delivery: "resume",
|
|
435
|
+
delivery_status: move?.queued
|
|
436
|
+
? "queued_on_target_account"
|
|
437
|
+
: queuedOnAdmission
|
|
438
|
+
? "queued_on_admission"
|
|
439
|
+
: "worker_spawned",
|
|
440
|
+
account_move: move ? accountMoveSummary(move) : null,
|
|
441
|
+
resumed_session_id: resumed.last_resume?.session_id ?? null,
|
|
442
|
+
last_resume: next.last_resume ?? null,
|
|
443
|
+
compaction: resumed.last_compaction && resumed.last_compaction.resume_at === resumed.last_resume?.at
|
|
444
|
+
? resumed.last_compaction
|
|
445
|
+
: null,
|
|
446
|
+
project_id: next.project_id,
|
|
447
|
+
story_id: next.story_id,
|
|
448
|
+
stories: laneStories(next),
|
|
449
|
+
story_warning: storyWarning(next) ?? null,
|
|
450
|
+
lane: next,
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
return { runClaudeBalanceMoveTarget, runResume };
|
|
454
|
+
}
|
|
455
|
+
//# sourceMappingURL=lanes-resume.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { LaneCompanyRef, LaneRecord, LanesConfig, QuestionAnswer, QuestionBody, QuestionRecord, SeniorRef, AccountHistoryEntry, LivenessDeps, MachineIdentity, MonitorEvidenceReader, ReconcileHost, RunCommand, ThrottleDeps, withLaneHireLocks } from "../lib/lanes/index.js";
|
|
1
|
+
import type { LaneCompanyRef, LaneRecord, LanesConfig, QuestionAnswer, QuestionBody, QuestionRecord, SeniorRef, AccountHistoryEntry, LaneAccountSelection, PendingResume, QueuedReason, ResolvedLaneWorker, LivenessDeps, MachineIdentity, MonitorEvidenceReader, ReconcileHost, RunCommand, ThrottleDeps, withLaneHireLocks } from "../lib/lanes/index.js";
|
|
2
2
|
import type { OutpostIdentity } from "../lib/lanes/admission-host.js";
|
|
3
3
|
import type { OutpostDispatchResult } from "../lib/lanes/outpost-dispatch.js";
|
|
4
4
|
import type { ProbeCommand } from "../lib/lanes/models.js";
|
|
@@ -7,6 +7,158 @@ import type { WatcherLoopDeps } from "../lib/lanes/watcher.js";
|
|
|
7
7
|
import type { RunCompactionFn } from "../lib/lanes/compaction.js";
|
|
8
8
|
import type { LaneMesh, LaneMeshEvent } from "../lib/lanes/mesh-events.js";
|
|
9
9
|
import type { BalanceHostGap, BalanceHostSnapshot } from "../lib/lanes/claude-balance.js";
|
|
10
|
+
import { type ResolvedEffort } from "../lib/lanes/effort.js";
|
|
11
|
+
import type { SessionCopyResult } from "../lib/lanes/move-account.js";
|
|
12
|
+
import type { WorkerProfile } from "../lib/lanes/profile.js";
|
|
13
|
+
import type { OutpostHireGrantContext } from "../lib/lanes/hire-grant-dispatch.js";
|
|
14
|
+
import { LanesError } from "../lib/lanes/errors.js";
|
|
15
|
+
export type { LaneCompanyRef, LaneRecord } from "../lib/lanes/index.js";
|
|
16
|
+
export interface LaneAccountMove {
|
|
17
|
+
lane: LaneRecord;
|
|
18
|
+
from: string | null;
|
|
19
|
+
to: string;
|
|
20
|
+
copy: SessionCopyResult;
|
|
21
|
+
queued: boolean;
|
|
22
|
+
}
|
|
23
|
+
export type ResolvedResumeProfile = ResolvedLaneWorker & {
|
|
24
|
+
profile: NonNullable<ResolvedLaneWorker["profile"]>;
|
|
25
|
+
};
|
|
26
|
+
export interface ResumeProfileRepinAuthorization {
|
|
27
|
+
kind: "senior" | "ssm-owner-dispatch";
|
|
28
|
+
expectedSenior: SeniorRef;
|
|
29
|
+
actor: string;
|
|
30
|
+
reason: "resume --worker";
|
|
31
|
+
newHash: string;
|
|
32
|
+
}
|
|
33
|
+
export interface LaneMessageOptions {
|
|
34
|
+
text?: string;
|
|
35
|
+
project?: string;
|
|
36
|
+
story?: string;
|
|
37
|
+
compact?: boolean;
|
|
38
|
+
account?: string;
|
|
39
|
+
model?: string;
|
|
40
|
+
effort?: string;
|
|
41
|
+
worker?: string;
|
|
42
|
+
grantContext?: string;
|
|
43
|
+
monitorEvent?: {
|
|
44
|
+
monitor_id: string;
|
|
45
|
+
description: string;
|
|
46
|
+
sequence: number;
|
|
47
|
+
};
|
|
48
|
+
/** Internal recovery messages do not emit a second command response. */
|
|
49
|
+
silent?: boolean;
|
|
50
|
+
/** Internal recovery claim is safe to archive once this callback runs. */
|
|
51
|
+
onAccepted?: () => void;
|
|
52
|
+
}
|
|
53
|
+
export interface LaneResumeOptions {
|
|
54
|
+
text?: string;
|
|
55
|
+
fromFailed?: boolean;
|
|
56
|
+
reason?: string;
|
|
57
|
+
failedResumeContext?: string;
|
|
58
|
+
project?: string;
|
|
59
|
+
story?: string;
|
|
60
|
+
compact?: boolean;
|
|
61
|
+
account?: string;
|
|
62
|
+
model?: string;
|
|
63
|
+
effort?: string;
|
|
64
|
+
worker?: string;
|
|
65
|
+
keepAlive?: boolean;
|
|
66
|
+
meshCompany?: string;
|
|
67
|
+
profileRepinContext?: string;
|
|
68
|
+
grantContext?: string;
|
|
69
|
+
}
|
|
70
|
+
export interface LaneBalanceMoveTargetOptions {
|
|
71
|
+
account?: string;
|
|
72
|
+
fromAccount?: string;
|
|
73
|
+
expectedState?: string;
|
|
74
|
+
}
|
|
75
|
+
export interface AdmissionResult {
|
|
76
|
+
action: "start" | "queue" | "refuse";
|
|
77
|
+
reason?: QueuedReason;
|
|
78
|
+
error?: LanesError;
|
|
79
|
+
}
|
|
80
|
+
export interface PrepareAccountMoveInput {
|
|
81
|
+
account: string;
|
|
82
|
+
cliModel: string | undefined;
|
|
83
|
+
preservedModel?: string;
|
|
84
|
+
accountHistoryReason?: AccountHistoryEntry["reason"];
|
|
85
|
+
workerProfile?: ResolvedResumeProfile;
|
|
86
|
+
profileRepin?: ResumeProfileRepinAuthorization;
|
|
87
|
+
effort?: ResolvedEffort;
|
|
88
|
+
grantContext?: OutpostHireGrantContext;
|
|
89
|
+
pending?: Omit<PendingResume, "session_id" | "at"> & {
|
|
90
|
+
trigger: PendingResume["trigger"];
|
|
91
|
+
};
|
|
92
|
+
/** Copy the session and change account while preserving the between-turn state. */
|
|
93
|
+
deferTurnStart?: boolean;
|
|
94
|
+
accountSelection?: LaneAccountSelection;
|
|
95
|
+
}
|
|
96
|
+
export interface RecordResumeSetupInput {
|
|
97
|
+
profile?: ResolvedResumeProfile;
|
|
98
|
+
effort?: ResolvedEffort;
|
|
99
|
+
keepAlive?: boolean;
|
|
100
|
+
cliModel?: string;
|
|
101
|
+
meshCompany?: LaneCompanyRef;
|
|
102
|
+
threadId?: string;
|
|
103
|
+
profileRepin?: ResumeProfileRepinAuthorization;
|
|
104
|
+
}
|
|
105
|
+
export declare function accountRequestKeepsCurrentLane(lane: LaneRecord, rawAccount: string): boolean;
|
|
106
|
+
export declare function assertCurrentResumeAccountAvailable(ctx: Ctx, lane: LaneRecord): void;
|
|
107
|
+
export declare function accountMoveSummary(move: LaneAccountMove): Record<string, unknown>;
|
|
108
|
+
export declare function hasKeepAliveProgress(lane: LaneRecord): boolean;
|
|
109
|
+
export declare function resetKeepAliveProgress(lane: LaneRecord): LaneRecord;
|
|
110
|
+
export declare function resolveResumeEffort(ctx: Ctx, lane: LaneRecord, input: {
|
|
111
|
+
cliEffort?: string;
|
|
112
|
+
profile?: ResolvedResumeProfile;
|
|
113
|
+
account?: string;
|
|
114
|
+
model?: string;
|
|
115
|
+
}): ResolvedEffort | undefined;
|
|
116
|
+
export declare function recordedResumeEffort(lane: LaneRecord): ResolvedEffort;
|
|
117
|
+
export interface LaneMessageHandlerDeps {
|
|
118
|
+
archiveResumeMessage(hqRoot: string, laneId: string, message: {
|
|
119
|
+
message_id: string;
|
|
120
|
+
[key: string]: unknown;
|
|
121
|
+
}): void;
|
|
122
|
+
assertLaneRunsHereForAdmission(ctx: Ctx, lane: LaneRecord): void;
|
|
123
|
+
assertLaneRunsHereForOperatorMutation(ctx: Ctx, lane: LaneRecord, action: string): void;
|
|
124
|
+
assessLaneAdmission(ctx: Ctx, lane: LaneRecord, options: {
|
|
125
|
+
account?: string;
|
|
126
|
+
queueAt: string;
|
|
127
|
+
alreadyQueued?: boolean;
|
|
128
|
+
}): Promise<AdmissionResult>;
|
|
129
|
+
casUpdateLane(hqRoot: string, laneId: string, updater: (current: LaneRecord) => LaneRecord): LaneRecord;
|
|
130
|
+
dispatchOutpostLaneCommand(ctx: Ctx, lane: LaneRecord, action: "message" | "resume", options: LaneMessageOptions | LaneResumeOptions): Promise<void>;
|
|
131
|
+
drainHookStatusFor(ctx: Ctx, lane: LaneRecord): {
|
|
132
|
+
registered: boolean;
|
|
133
|
+
kind: string;
|
|
134
|
+
path: string;
|
|
135
|
+
} | null;
|
|
136
|
+
laneIsHostedHere(ctx: Ctx, lane: LaneRecord): boolean;
|
|
137
|
+
laneWithRecoveredProviderSession(hqRoot: string, lane: LaneRecord): LaneRecord;
|
|
138
|
+
loadAllLanes(hqRoot: string): LaneRecord[];
|
|
139
|
+
mintMessageId(): string;
|
|
140
|
+
prepareAccountMove(ctx: Ctx, lane: LaneRecord, input: PrepareAccountMoveInput): Promise<LaneAccountMove>;
|
|
141
|
+
persistClaudeAccountSelection(hqRoot: string, lane: LaneRecord, selection: LaneAccountSelection): LaneRecord;
|
|
142
|
+
printJson(value: unknown): void;
|
|
143
|
+
recordResumeSetup(ctx: Ctx, lane: LaneRecord, input: RecordResumeSetupInput): LaneRecord;
|
|
144
|
+
resolveActiveQuestionCompany(ctx: Ctx, message?: string): Promise<LaneCompanyRef>;
|
|
145
|
+
resolveResumeProfile(ctx: Ctx, lane: LaneRecord, workerId: string): ResolvedResumeProfile;
|
|
146
|
+
selectAutomaticClaudeResumeAccount(ctx: Ctx, lane: LaneRecord, workerProfile?: ResolvedResumeProfile, options?: {
|
|
147
|
+
repairUnavailableAccount?: boolean;
|
|
148
|
+
}): Promise<{
|
|
149
|
+
selection: LaneAccountSelection;
|
|
150
|
+
historyReason?: AccountHistoryEntry["reason"];
|
|
151
|
+
preservedModel?: string;
|
|
152
|
+
} | undefined>;
|
|
153
|
+
validateResumeProfileModel(ctx: Ctx, lane: LaneRecord, profile: WorkerProfile): void;
|
|
154
|
+
withResumeHireAdmission<T>(ctx: Ctx, lane: LaneRecord, fn: () => T, grantContext?: OutpostHireGrantContext): Promise<T>;
|
|
155
|
+
}
|
|
156
|
+
export interface LaneResumeHandlerDeps extends Pick<LaneMessageHandlerDeps, "archiveResumeMessage" | "assertLaneRunsHereForAdmission" | "assertLaneRunsHereForOperatorMutation" | "assessLaneAdmission" | "casUpdateLane" | "laneIsHostedHere" | "laneWithRecoveredProviderSession" | "mintMessageId" | "prepareAccountMove" | "persistClaudeAccountSelection" | "printJson" | "recordResumeSetup" | "resolveResumeProfile" | "selectAutomaticClaudeResumeAccount" | "validateResumeProfileModel" | "withResumeHireAdmission"> {
|
|
157
|
+
dispatchOutpostLaneCommand(ctx: Ctx, lane: LaneRecord, action: "resume", options: LaneResumeOptions): Promise<void>;
|
|
158
|
+
authorizeFailedResume(ctx: Ctx, lane: LaneRecord, encodedContext?: string): string;
|
|
159
|
+
authorizeResumeProfileRepin(ctx: Ctx, lane: LaneRecord, profile: ResolvedResumeProfile | undefined, encodedContext: string | undefined): ResumeProfileRepinAuthorization | undefined;
|
|
160
|
+
automaticBalanceMoveHistoryReason(ctx: Ctx, lane: LaneRecord): AccountHistoryEntry["reason"];
|
|
161
|
+
}
|
|
10
162
|
export interface SpawnLoginResult {
|
|
11
163
|
status: number;
|
|
12
164
|
}
|