@indigoai-us/hq-cli 5.233.0 → 5.234.1
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 +24 -0
- package/dist/command-catalog.generated.d.ts +13 -1
- package/dist/command-catalog.generated.js +17 -1
- package/dist/commands/agents.js +10 -2
- package/dist/commands/lanes.js +183 -9
- package/dist/lib/lanes/delivery.d.ts +9 -3
- package/dist/lib/lanes/delivery.js +11 -6
- package/dist/lib/lanes/outpost-dispatch.d.ts +2 -0
- package/dist/lib/lanes/outpost-dispatch.js +3 -0
- package/dist/lib/lanes/spawn.js +20 -0
- package/dist/lib/lanes/store.js +5 -0
- package/dist/lib/lanes/types.d.ts +12 -0
- package/dist/lib/lanes/types.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.234.1] — 2026-09-27
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- A primary resume now confirms and syncs an Outpost lane queued during
|
|
10
|
+
owner-side admission. (#926)
|
|
11
|
+
- Outpost messages and account moves for children of primary-hosted lanes now
|
|
12
|
+
carry the bounded hire-grant context required by the owner. (#926)
|
|
13
|
+
- Outpost message resumes, moves, and admission queues now sync the returned
|
|
14
|
+
owner lane record back to the primary. (#926)
|
|
15
|
+
|
|
16
|
+
## [5.234.0] — 2026-09-27
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- A lane's senior can explicitly resume a failed lane's recorded provider
|
|
21
|
+
session with `hq lanes resume --from-failed --reason`; the secret-scanned
|
|
22
|
+
reason and recovered failure are recorded in lane history. Failed lanes
|
|
23
|
+
continue to refuse ordinary messages. (#927)
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- `hq agents stop` says the stop was accepted and shows the status the API returns, instead of saying the agent has already stopped. (#922)
|
|
28
|
+
|
|
5
29
|
## [5.233.0] — 2026-09-27
|
|
6
30
|
|
|
7
31
|
### Added
|
|
@@ -8005,6 +8005,9 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
8005
8005
|
}, {
|
|
8006
8006
|
readonly flags: "--worker <profile>";
|
|
8007
8007
|
readonly description: "Attach a personal worker profile when this message resumes the lane";
|
|
8008
|
+
}, {
|
|
8009
|
+
readonly flags: "--grant-context <base64url>";
|
|
8010
|
+
readonly description: "";
|
|
8008
8011
|
}, {
|
|
8009
8012
|
readonly flags: "--no-compact";
|
|
8010
8013
|
readonly description: "Skip the resume-time compaction check for this message";
|
|
@@ -8047,7 +8050,7 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
8047
8050
|
readonly subcommands: readonly [];
|
|
8048
8051
|
}, {
|
|
8049
8052
|
readonly name: "resume";
|
|
8050
|
-
readonly description: "Resume an interrupted, done or blocked lane on its recorded provider session";
|
|
8053
|
+
readonly description: "Resume an interrupted, done or blocked lane on its recorded provider session; recover failed lanes with the senior-only --from-failed option";
|
|
8051
8054
|
readonly aliases: readonly [];
|
|
8052
8055
|
readonly hidden: false;
|
|
8053
8056
|
readonly usage: "[options] <lane-id>";
|
|
@@ -8059,6 +8062,12 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
8059
8062
|
readonly options: readonly [{
|
|
8060
8063
|
readonly flags: "--text <msg>";
|
|
8061
8064
|
readonly description: "Operator message for the resumed turn";
|
|
8065
|
+
}, {
|
|
8066
|
+
readonly flags: "--from-failed";
|
|
8067
|
+
readonly description: "Explicitly resume a failed lane as its senior";
|
|
8068
|
+
}, {
|
|
8069
|
+
readonly flags: "--reason <text>";
|
|
8070
|
+
readonly description: "Required explanation for resuming from failed";
|
|
8062
8071
|
}, {
|
|
8063
8072
|
readonly flags: "--project <slug>";
|
|
8064
8073
|
readonly description: "Re-point project attribution from this resume";
|
|
@@ -8080,6 +8089,9 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
8080
8089
|
}, {
|
|
8081
8090
|
readonly flags: "--grant-context <base64url>";
|
|
8082
8091
|
readonly description: "";
|
|
8092
|
+
}, {
|
|
8093
|
+
readonly flags: "--failed-resume-context <base64url>";
|
|
8094
|
+
readonly description: "";
|
|
8083
8095
|
}, {
|
|
8084
8096
|
readonly flags: "--keep-alive";
|
|
8085
8097
|
readonly description: "Automatically resume done lead lanes while their story remains open";
|
|
@@ -10353,6 +10353,10 @@ export const COMMAND_CATALOG = [
|
|
|
10353
10353
|
"flags": "--worker <profile>",
|
|
10354
10354
|
"description": "Attach a personal worker profile when this message resumes the lane"
|
|
10355
10355
|
},
|
|
10356
|
+
{
|
|
10357
|
+
"flags": "--grant-context <base64url>",
|
|
10358
|
+
"description": ""
|
|
10359
|
+
},
|
|
10356
10360
|
{
|
|
10357
10361
|
"flags": "--no-compact",
|
|
10358
10362
|
"description": "Skip the resume-time compaction check for this message"
|
|
@@ -10408,7 +10412,7 @@ export const COMMAND_CATALOG = [
|
|
|
10408
10412
|
},
|
|
10409
10413
|
{
|
|
10410
10414
|
"name": "resume",
|
|
10411
|
-
"description": "Resume an interrupted, done or blocked lane on its recorded provider session",
|
|
10415
|
+
"description": "Resume an interrupted, done or blocked lane on its recorded provider session; recover failed lanes with the senior-only --from-failed option",
|
|
10412
10416
|
"aliases": [],
|
|
10413
10417
|
"hidden": false,
|
|
10414
10418
|
"usage": "[options] <lane-id>",
|
|
@@ -10424,6 +10428,14 @@ export const COMMAND_CATALOG = [
|
|
|
10424
10428
|
"flags": "--text <msg>",
|
|
10425
10429
|
"description": "Operator message for the resumed turn"
|
|
10426
10430
|
},
|
|
10431
|
+
{
|
|
10432
|
+
"flags": "--from-failed",
|
|
10433
|
+
"description": "Explicitly resume a failed lane as its senior"
|
|
10434
|
+
},
|
|
10435
|
+
{
|
|
10436
|
+
"flags": "--reason <text>",
|
|
10437
|
+
"description": "Required explanation for resuming from failed"
|
|
10438
|
+
},
|
|
10427
10439
|
{
|
|
10428
10440
|
"flags": "--project <slug>",
|
|
10429
10441
|
"description": "Re-point project attribution from this resume"
|
|
@@ -10452,6 +10464,10 @@ export const COMMAND_CATALOG = [
|
|
|
10452
10464
|
"flags": "--grant-context <base64url>",
|
|
10453
10465
|
"description": ""
|
|
10454
10466
|
},
|
|
10467
|
+
{
|
|
10468
|
+
"flags": "--failed-resume-context <base64url>",
|
|
10469
|
+
"description": ""
|
|
10470
|
+
},
|
|
10455
10471
|
{
|
|
10456
10472
|
"flags": "--keep-alive",
|
|
10457
10473
|
"description": "Automatically resume done lead lanes while their story remains open"
|
package/dist/commands/agents.js
CHANGED
|
@@ -1819,8 +1819,16 @@ export function registerAgentsCommand(program) {
|
|
|
1819
1819
|
.action(async function (agentUid) {
|
|
1820
1820
|
try {
|
|
1821
1821
|
const token = (await resolveVaultCredential()).token;
|
|
1822
|
-
await startStopAgent(token, agentUid, action);
|
|
1823
|
-
|
|
1822
|
+
const result = await startStopAgent(token, agentUid, action);
|
|
1823
|
+
const runtimeStatus = result.runtime?.status;
|
|
1824
|
+
const successMessage = action === "start"
|
|
1825
|
+
? `Started agent ${agentUid}.`
|
|
1826
|
+
: runtimeStatus === "terminated"
|
|
1827
|
+
? `Stopped agent ${agentUid}.`
|
|
1828
|
+
: typeof runtimeStatus === "string"
|
|
1829
|
+
? `Stop accepted for agent ${agentUid} (status: ${runtimeStatus}).`
|
|
1830
|
+
: `Stop accepted for agent ${agentUid}.`;
|
|
1831
|
+
console.log(chalk.green(successMessage));
|
|
1824
1832
|
}
|
|
1825
1833
|
catch (err) {
|
|
1826
1834
|
fail(err);
|
package/dist/commands/lanes.js
CHANGED
|
@@ -3626,6 +3626,10 @@ function outpostRejectedGrantContextOption(result) {
|
|
|
3626
3626
|
return result.exitCode !== 0 &&
|
|
3627
3627
|
/unknown option\s+(?:['"])?--grant-context(?:['"])?(?:\s|$)/i.test(`${result.stderr}\n${result.stdout}`);
|
|
3628
3628
|
}
|
|
3629
|
+
function outpostRejectedFailedResumeOption(result) {
|
|
3630
|
+
return result.exitCode !== 0 &&
|
|
3631
|
+
/unknown option\s+(?:['"])?--(?:from-failed|failed-resume-context)(?:['"])?(?:\s|$)/i.test(`${result.stderr}\n${result.stdout}`);
|
|
3632
|
+
}
|
|
3629
3633
|
function verifyOutpostProfileContent(hqRoot, lane) {
|
|
3630
3634
|
if (lane.host.kind !== "outpost" || lane.worker_profile === undefined)
|
|
3631
3635
|
return;
|
|
@@ -3648,6 +3652,70 @@ function resumeProfileRepinActor(ctx) {
|
|
|
3648
3652
|
const sessionId = sessionIdFromEnv(ctx.env);
|
|
3649
3653
|
return sessionId ? `session:${sessionId}` : "ssm-owner-dispatch";
|
|
3650
3654
|
}
|
|
3655
|
+
function createOutpostFailedResumeContext(ctx, lane) {
|
|
3656
|
+
return Buffer.from(JSON.stringify({
|
|
3657
|
+
version: 1,
|
|
3658
|
+
lane_id: lane.lane_id,
|
|
3659
|
+
expected_senior: lane.senior,
|
|
3660
|
+
actor: resumeProfileRepinActor(ctx),
|
|
3661
|
+
})).toString("base64url");
|
|
3662
|
+
}
|
|
3663
|
+
function parseOutpostFailedResumeContext(encoded, laneId) {
|
|
3664
|
+
try {
|
|
3665
|
+
if (!/^[A-Za-z0-9_-]{1,4096}$/.test(encoded))
|
|
3666
|
+
throw new Error("invalid encoding");
|
|
3667
|
+
const value = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
|
|
3668
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
3669
|
+
throw new Error("invalid payload");
|
|
3670
|
+
const payload = value;
|
|
3671
|
+
const seniorValue = payload.expected_senior;
|
|
3672
|
+
if (seniorValue === null || typeof seniorValue !== "object" || Array.isArray(seniorValue))
|
|
3673
|
+
throw new Error("invalid senior");
|
|
3674
|
+
const senior = seniorValue;
|
|
3675
|
+
let expectedSenior;
|
|
3676
|
+
if (senior.kind === "user") {
|
|
3677
|
+
expectedSenior = { kind: "user" };
|
|
3678
|
+
}
|
|
3679
|
+
else if ((senior.kind === "lane" || senior.kind === "session") &&
|
|
3680
|
+
typeof senior.id === "string" && senior.id.trim().length > 0) {
|
|
3681
|
+
expectedSenior = { kind: senior.kind, id: senior.id };
|
|
3682
|
+
}
|
|
3683
|
+
else {
|
|
3684
|
+
throw new Error("invalid senior");
|
|
3685
|
+
}
|
|
3686
|
+
if (payload.version !== 1 ||
|
|
3687
|
+
payload.lane_id !== laneId ||
|
|
3688
|
+
typeof payload.actor !== "string" ||
|
|
3689
|
+
payload.actor.trim().length === 0) {
|
|
3690
|
+
throw new Error("invalid payload");
|
|
3691
|
+
}
|
|
3692
|
+
return { expectedSenior, actor: payload.actor };
|
|
3693
|
+
}
|
|
3694
|
+
catch {
|
|
3695
|
+
throw new LanesError("failed_resume_authorization_invalid", "Failed-resume authorization context is invalid", { lane_id: laneId });
|
|
3696
|
+
}
|
|
3697
|
+
}
|
|
3698
|
+
function authorizeFailedResume(ctx, lane, encodedContext) {
|
|
3699
|
+
if (encodedContext !== undefined) {
|
|
3700
|
+
if (!ctx.isSsmOwnerDispatch()) {
|
|
3701
|
+
throw new LanesError("failed_resume_authorization_invalid", "Failed-resume authorization context is only valid on an Outpost owner dispatch", { lane_id: lane.lane_id });
|
|
3702
|
+
}
|
|
3703
|
+
const context = parseOutpostFailedResumeContext(encodedContext, lane.lane_id);
|
|
3704
|
+
if (lane.host.kind !== "outpost" ||
|
|
3705
|
+
!sameHireGrantSenior(lane.senior, context.expectedSenior)) {
|
|
3706
|
+
throw new LanesError("failed_resume_senior_changed", `Lane ${lane.lane_id}'s senior changed after its failed-resume request was authorized`, {
|
|
3707
|
+
lane_id: lane.lane_id,
|
|
3708
|
+
expected_senior: context.expectedSenior,
|
|
3709
|
+
current_senior: lane.senior,
|
|
3710
|
+
});
|
|
3711
|
+
}
|
|
3712
|
+
return context.actor;
|
|
3713
|
+
}
|
|
3714
|
+
if (!callerMatchesSenior(ctx, lane.senior)) {
|
|
3715
|
+
throw new LanesError("failed_resume_unauthorized", "Only the lane's senior may resume a failed lane with --from-failed", { lane_id: lane.lane_id });
|
|
3716
|
+
}
|
|
3717
|
+
return resumeProfileRepinActor(ctx);
|
|
3718
|
+
}
|
|
3651
3719
|
function workerProfileHashMismatch(lane, profile) {
|
|
3652
3720
|
return new LanesError("worker_profile_hash_mismatch", `Worker profile ${JSON.stringify(profile.id)} on Outpost ${lane.host.id} does not match the hash recorded by the lane`, {
|
|
3653
3721
|
lane_id: lane.lane_id,
|
|
@@ -3757,6 +3825,9 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
|
|
|
3757
3825
|
actor: resumeProfileRepinActor(ctx),
|
|
3758
3826
|
})
|
|
3759
3827
|
: undefined;
|
|
3828
|
+
const failedResumeContext = action === "resume" && options.fromFailed === true
|
|
3829
|
+
? createOutpostFailedResumeContext(ctx, lane)
|
|
3830
|
+
: undefined;
|
|
3760
3831
|
let grantContext;
|
|
3761
3832
|
let grantHierarchy;
|
|
3762
3833
|
if (action === "grant") {
|
|
@@ -3770,12 +3841,13 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
|
|
|
3770
3841
|
grantHierarchy = hierarchy;
|
|
3771
3842
|
grantContext = createOutpostHireGrantContext(lane, hierarchy);
|
|
3772
3843
|
}
|
|
3773
|
-
else if (action === "resume" &&
|
|
3844
|
+
else if ((action === "resume" || action === "message") &&
|
|
3774
3845
|
lane.senior.kind === "lane") {
|
|
3775
3846
|
grantContext = createOutpostHireGrantContext(lane, loadAllLanes(ctx.hqRoot()));
|
|
3776
3847
|
}
|
|
3777
3848
|
let dispatched;
|
|
3778
3849
|
let grantContextRejected = false;
|
|
3850
|
+
let failedResumeOptionRejected = false;
|
|
3779
3851
|
try {
|
|
3780
3852
|
const dispatchCommand = (contexts) => ctx.dispatchOutpostExec({
|
|
3781
3853
|
outpostId,
|
|
@@ -3797,15 +3869,17 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
|
|
|
3797
3869
|
effort: options.effort,
|
|
3798
3870
|
worker: options.worker,
|
|
3799
3871
|
keepAlive: options.keepAlive,
|
|
3872
|
+
fromFailed: options.fromFailed,
|
|
3800
3873
|
hire: options.hire,
|
|
3801
3874
|
senior: options.senior,
|
|
3802
3875
|
reason: options.reason,
|
|
3803
3876
|
grantContext: contexts.grantContext,
|
|
3804
3877
|
profileRepinContext: contexts.profileRepinContext,
|
|
3878
|
+
failedResumeContext: contexts.failedResumeContext,
|
|
3805
3879
|
compact: options.compact,
|
|
3806
3880
|
}),
|
|
3807
3881
|
});
|
|
3808
|
-
let contexts = { grantContext, profileRepinContext };
|
|
3882
|
+
let contexts = { grantContext, profileRepinContext, failedResumeContext };
|
|
3809
3883
|
dispatched = await dispatchCommand(contexts);
|
|
3810
3884
|
for (let retry = 0; action === "resume" && retry < 2; retry += 1) {
|
|
3811
3885
|
if (contexts.grantContext !== undefined && outpostRejectedGrantContextOption(dispatched)) {
|
|
@@ -3816,6 +3890,10 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
|
|
|
3816
3890
|
outpostRejectedProfileRepinOption(dispatched)) {
|
|
3817
3891
|
contexts = { ...contexts, profileRepinContext: undefined };
|
|
3818
3892
|
}
|
|
3893
|
+
else if (options.fromFailed === true && outpostRejectedFailedResumeOption(dispatched)) {
|
|
3894
|
+
failedResumeOptionRejected = true;
|
|
3895
|
+
break;
|
|
3896
|
+
}
|
|
3819
3897
|
else {
|
|
3820
3898
|
break;
|
|
3821
3899
|
}
|
|
@@ -3832,6 +3910,13 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
|
|
|
3832
3910
|
required_option: "--grant-context",
|
|
3833
3911
|
});
|
|
3834
3912
|
}
|
|
3913
|
+
if (failedResumeOptionRejected) {
|
|
3914
|
+
throw new LanesError("outpost_cli_upgrade_required", `Outpost ${outpostId} needs an hq-cli release containing failed-lane resume support before it can resume lane ${lane.lane_id}`, {
|
|
3915
|
+
lane_id: lane.lane_id,
|
|
3916
|
+
outpost_id: outpostId,
|
|
3917
|
+
required_option: "--from-failed",
|
|
3918
|
+
});
|
|
3919
|
+
}
|
|
3835
3920
|
const ownerError = ownerErrorFromOutput(dispatched.stdout);
|
|
3836
3921
|
if (options.effort !== undefined && outpostRejectedEffortOption(dispatched)) {
|
|
3837
3922
|
throw new LanesError("outpost_effort_unsupported", `Outpost ${outpostId} rejected --effort; update hq-cli on that Outpost before changing a lane's effort remotely`, {
|
|
@@ -3848,6 +3933,7 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
|
|
|
3848
3933
|
if (ownerError) {
|
|
3849
3934
|
if (action === "resume" &&
|
|
3850
3935
|
(ownerError.error === "worker_profile_hash_mismatch" ||
|
|
3936
|
+
ownerError.error.startsWith("failed_resume_") ||
|
|
3851
3937
|
ownerError.error.startsWith("hire_grant_"))) {
|
|
3852
3938
|
throw new LanesError(ownerError.error, ownerError.message, {
|
|
3853
3939
|
...ownerError.details,
|
|
@@ -3901,9 +3987,14 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
|
|
|
3901
3987
|
: action === "reparent"
|
|
3902
3988
|
? remote.lane !== null && typeof remote.lane === "object" &&
|
|
3903
3989
|
Array.isArray(remote.changed_descendant_ids)
|
|
3904
|
-
: (remote.delivery_status === "worker_spawned" ||
|
|
3990
|
+
: (((remote.delivery_status === "worker_spawned" ||
|
|
3905
3991
|
remote.delivery_status === "queued_on_target_account") &&
|
|
3906
|
-
remote.lane !== undefined)
|
|
3992
|
+
remote.lane !== undefined) ||
|
|
3993
|
+
(remote.delivery_status === "queued_on_admission" &&
|
|
3994
|
+
remote.lane !== null &&
|
|
3995
|
+
typeof remote.lane === "object" &&
|
|
3996
|
+
!Array.isArray(remote.lane) &&
|
|
3997
|
+
remote.lane.state === "queued")));
|
|
3907
3998
|
if (!resultConfirmed) {
|
|
3908
3999
|
throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} completed exec without a confirmed ${action} result for lane ${lane.lane_id}`, {
|
|
3909
4000
|
lane_id: lane.lane_id,
|
|
@@ -3924,6 +4015,21 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
|
|
|
3924
4015
|
});
|
|
3925
4016
|
return;
|
|
3926
4017
|
}
|
|
4018
|
+
if (action === "message" &&
|
|
4019
|
+
(remote.delivery_status === "worker_spawned" ||
|
|
4020
|
+
remote.delivery_status === "queued_on_target_account" ||
|
|
4021
|
+
remote.delivery_status === "queued_on_admission")) {
|
|
4022
|
+
const ownerLane = syncLaneRecordFromOwner(ctx.hqRoot(), lane.lane_id, outpostId, remote.lane, undefined, { allowNonTerminal: true });
|
|
4023
|
+
printJson({
|
|
4024
|
+
...remote,
|
|
4025
|
+
lane_id: ownerLane.lane_id,
|
|
4026
|
+
lane: ownerLane,
|
|
4027
|
+
outpost_id: outpostId,
|
|
4028
|
+
outpost_command_id: dispatched.commandId,
|
|
4029
|
+
outpost_transport: "outposts_exec",
|
|
4030
|
+
});
|
|
4031
|
+
return;
|
|
4032
|
+
}
|
|
3927
4033
|
if (action === "reparent" || action === "resume") {
|
|
3928
4034
|
const ownerLane = syncLaneRecordFromOwner(ctx.hqRoot(), lane.lane_id, outpostId, remote.lane, undefined, { allowNonTerminal: true });
|
|
3929
4035
|
const descendants = [];
|
|
@@ -5047,6 +5153,7 @@ async function runShow(ctx, laneId) {
|
|
|
5047
5153
|
last_resume: lane.last_resume ?? null,
|
|
5048
5154
|
account: lane.account ?? null,
|
|
5049
5155
|
account_history: lane.account_history ?? [],
|
|
5156
|
+
failed_resume_history: lane.failed_resume_history ?? [],
|
|
5050
5157
|
pending_resume: lane.pending_resume ?? null,
|
|
5051
5158
|
last_run_usage: lane.last_run_usage ?? null,
|
|
5052
5159
|
model: lane.model ?? null,
|
|
@@ -5785,7 +5892,7 @@ async function prepareAccountMove(ctx, lane, input) {
|
|
|
5785
5892
|
next.queued_at = undefined;
|
|
5786
5893
|
}
|
|
5787
5894
|
return next;
|
|
5788
|
-
}));
|
|
5895
|
+
}), input.grantContext);
|
|
5789
5896
|
return { lane: moved, from, to: target, copy, queued };
|
|
5790
5897
|
}, ctx.sleep);
|
|
5791
5898
|
}
|
|
@@ -5903,6 +6010,14 @@ function assertCurrentResumeAccountAvailable(ctx, lane) {
|
|
|
5903
6010
|
}
|
|
5904
6011
|
async function runMessage(ctx, laneId, opts) {
|
|
5905
6012
|
const hqRoot = ctx.hqRoot();
|
|
6013
|
+
const hireGrantContext = opts.grantContext === undefined
|
|
6014
|
+
? undefined
|
|
6015
|
+
: (() => {
|
|
6016
|
+
if (!ctx.isSsmOwnerDispatch()) {
|
|
6017
|
+
throw new LanesError("hire_grant_dispatch_unauthorized", "Message grant context is only valid on an Outpost owner dispatch", { lane_id: laneId });
|
|
6018
|
+
}
|
|
6019
|
+
return parseOutpostHireGrantContext(opts.grantContext, laneId);
|
|
6020
|
+
})();
|
|
5906
6021
|
let accepted = false;
|
|
5907
6022
|
let claimedMessages = [];
|
|
5908
6023
|
const markAccepted = () => {
|
|
@@ -5979,7 +6094,7 @@ async function runMessage(ctx, laneId, opts) {
|
|
|
5979
6094
|
}
|
|
5980
6095
|
// Check before transcript recovery, account probing, or message writes.
|
|
5981
6096
|
// The final state transition repeats this check under the senior lock.
|
|
5982
|
-
await withResumeHireAdmission(ctx, lane, () => undefined);
|
|
6097
|
+
await withResumeHireAdmission(ctx, lane, () => undefined, hireGrantContext);
|
|
5983
6098
|
lane = laneWithRecoveredProviderSession(hqRoot, lane);
|
|
5984
6099
|
const keepCurrentAccount = opts.account !== undefined && accountRequestKeepsCurrentLane(lane, opts.account);
|
|
5985
6100
|
if (keepCurrentAccount)
|
|
@@ -6075,6 +6190,7 @@ async function runMessage(ctx, laneId, opts) {
|
|
|
6075
6190
|
cliModel: opts.model?.trim() || undefined,
|
|
6076
6191
|
...(resumeProfile !== undefined ? { workerProfile: resumeProfile } : {}),
|
|
6077
6192
|
...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
|
|
6193
|
+
...(hireGrantContext !== undefined ? { grantContext: hireGrantContext } : {}),
|
|
6078
6194
|
pending: {
|
|
6079
6195
|
trigger: "message",
|
|
6080
6196
|
text: deliveryText,
|
|
@@ -6099,6 +6215,7 @@ async function runMessage(ctx, laneId, opts) {
|
|
|
6099
6215
|
...(automatic.historyReason !== undefined ? { accountHistoryReason: automatic.historyReason } : {}),
|
|
6100
6216
|
...(resumeProfile !== undefined ? { workerProfile: resumeProfile } : {}),
|
|
6101
6217
|
...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
|
|
6218
|
+
...(hireGrantContext !== undefined ? { grantContext: hireGrantContext } : {}),
|
|
6102
6219
|
pending: {
|
|
6103
6220
|
trigger: "message",
|
|
6104
6221
|
text: deliveryText,
|
|
@@ -6213,7 +6330,7 @@ async function runMessage(ctx, laneId, opts) {
|
|
|
6213
6330
|
});
|
|
6214
6331
|
archiveResumeMessage(hqRoot, laneId, dropboxMessage);
|
|
6215
6332
|
return started;
|
|
6216
|
-
});
|
|
6333
|
+
}, hireGrantContext);
|
|
6217
6334
|
}, ctx.sleep);
|
|
6218
6335
|
},
|
|
6219
6336
|
}, mechanism);
|
|
@@ -6631,6 +6748,30 @@ function laneWithRecoveredProviderSession(hqRoot, lane) {
|
|
|
6631
6748
|
async function runResume(ctx, laneId, opts) {
|
|
6632
6749
|
const hqRoot = ctx.hqRoot();
|
|
6633
6750
|
let lane = readLane(hqRoot, laneId);
|
|
6751
|
+
const failedResumeReason = opts.reason?.trim();
|
|
6752
|
+
if (opts.reason !== undefined && opts.fromFailed !== true) {
|
|
6753
|
+
throw new LanesError("failed_resume_reason_requires_flag", "--reason on hq lanes resume requires --from-failed", { lane_id: laneId });
|
|
6754
|
+
}
|
|
6755
|
+
if (opts.fromFailed === true && !failedResumeReason) {
|
|
6756
|
+
throw new LanesError("failed_resume_reason_required", "--from-failed requires a non-empty --reason", { lane_id: laneId });
|
|
6757
|
+
}
|
|
6758
|
+
if (opts.reason !== undefined)
|
|
6759
|
+
assertNoSecretText("resume reason", opts.reason);
|
|
6760
|
+
if (opts.failedResumeContext !== undefined && opts.fromFailed !== true) {
|
|
6761
|
+
throw new LanesError("failed_resume_authorization_invalid", "Failed-resume authorization context requires --from-failed", { lane_id: laneId });
|
|
6762
|
+
}
|
|
6763
|
+
const failedResumeActor = opts.fromFailed === true
|
|
6764
|
+
? authorizeFailedResume(ctx, lane, opts.failedResumeContext)
|
|
6765
|
+
: undefined;
|
|
6766
|
+
if (opts.fromFailed === true && lane.state !== "failed") {
|
|
6767
|
+
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 });
|
|
6768
|
+
}
|
|
6769
|
+
const failedResumeExpectedSenior = opts.fromFailed === true
|
|
6770
|
+
? { ...lane.senior }
|
|
6771
|
+
: undefined;
|
|
6772
|
+
const recoveredFailure = opts.fromFailed === true
|
|
6773
|
+
? lane.terminal_reason ?? "failure_reason_unavailable"
|
|
6774
|
+
: undefined;
|
|
6634
6775
|
const hireGrantContext = opts.grantContext === undefined
|
|
6635
6776
|
? undefined
|
|
6636
6777
|
: (() => {
|
|
@@ -6671,7 +6812,7 @@ async function runResume(ctx, laneId, opts) {
|
|
|
6671
6812
|
if (lane.state === "queued") {
|
|
6672
6813
|
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}` });
|
|
6673
6814
|
}
|
|
6674
|
-
const refusal = resumeCommandRefusal(lane);
|
|
6815
|
+
const refusal = resumeCommandRefusal(lane, { allowFailed: opts.fromFailed === true });
|
|
6675
6816
|
if (refusal !== null) {
|
|
6676
6817
|
throw new LanesError(refusal.error, refusal.message, {
|
|
6677
6818
|
lane_id: laneId,
|
|
@@ -6741,6 +6882,10 @@ async function runResume(ctx, laneId, opts) {
|
|
|
6741
6882
|
pending: {
|
|
6742
6883
|
trigger: "resume",
|
|
6743
6884
|
resumed_from: resumedFrom,
|
|
6885
|
+
...(opts.fromFailed === true ? { from_failed: true } : {}),
|
|
6886
|
+
...(failedResumeExpectedSenior !== undefined
|
|
6887
|
+
? { expected_senior: failedResumeExpectedSenior }
|
|
6888
|
+
: {}),
|
|
6744
6889
|
...(text !== undefined ? { text } : {}),
|
|
6745
6890
|
...(messageId !== undefined ? { message_id: messageId } : {}),
|
|
6746
6891
|
compact: opts.compact !== false,
|
|
@@ -6766,6 +6911,10 @@ async function runResume(ctx, laneId, opts) {
|
|
|
6766
6911
|
pending: {
|
|
6767
6912
|
trigger: "resume",
|
|
6768
6913
|
resumed_from: resumedFrom,
|
|
6914
|
+
...(opts.fromFailed === true ? { from_failed: true } : {}),
|
|
6915
|
+
...(failedResumeExpectedSenior !== undefined
|
|
6916
|
+
? { expected_senior: failedResumeExpectedSenior }
|
|
6917
|
+
: {}),
|
|
6769
6918
|
...(text !== undefined ? { text } : {}),
|
|
6770
6919
|
...(messageId !== undefined ? { message_id: messageId } : {}),
|
|
6771
6920
|
compact: opts.compact !== false,
|
|
@@ -6826,6 +6975,10 @@ async function runResume(ctx, laneId, opts) {
|
|
|
6826
6975
|
session_id: sessionId,
|
|
6827
6976
|
trigger: "resume",
|
|
6828
6977
|
resumed_from: resumedFrom,
|
|
6978
|
+
...(opts.fromFailed === true ? { from_failed: true } : {}),
|
|
6979
|
+
...(failedResumeExpectedSenior !== undefined
|
|
6980
|
+
? { expected_senior: failedResumeExpectedSenior }
|
|
6981
|
+
: {}),
|
|
6829
6982
|
...(text !== undefined ? { text } : {}),
|
|
6830
6983
|
...(messageId !== undefined ? { message_id: messageId } : {}),
|
|
6831
6984
|
compact: opts.compact !== false,
|
|
@@ -6853,6 +7006,10 @@ async function runResume(ctx, laneId, opts) {
|
|
|
6853
7006
|
trigger: {
|
|
6854
7007
|
kind: "resume",
|
|
6855
7008
|
from: resumedFrom,
|
|
7009
|
+
...(opts.fromFailed === true ? { fromFailed: true } : {}),
|
|
7010
|
+
...(failedResumeExpectedSenior !== undefined
|
|
7011
|
+
? { expectedSenior: failedResumeExpectedSenior }
|
|
7012
|
+
: {}),
|
|
6856
7013
|
...(text !== undefined ? { text } : {}),
|
|
6857
7014
|
...(messageId !== undefined ? { message_id: messageId } : {}),
|
|
6858
7015
|
},
|
|
@@ -6875,6 +7032,19 @@ async function runResume(ctx, laneId, opts) {
|
|
|
6875
7032
|
});
|
|
6876
7033
|
}
|
|
6877
7034
|
let next = resumed;
|
|
7035
|
+
if (failedResumeActor !== undefined &&
|
|
7036
|
+
failedResumeReason !== undefined &&
|
|
7037
|
+
recoveredFailure !== undefined) {
|
|
7038
|
+
next = casUpdateLane(hqRoot, laneId, (current) => ({
|
|
7039
|
+
...current,
|
|
7040
|
+
failed_resume_history: appendCappedHistory(current.failed_resume_history ?? [], {
|
|
7041
|
+
at,
|
|
7042
|
+
actor: failedResumeActor,
|
|
7043
|
+
reason: failedResumeReason,
|
|
7044
|
+
failure: recoveredFailure,
|
|
7045
|
+
}),
|
|
7046
|
+
}));
|
|
7047
|
+
}
|
|
6878
7048
|
if (text !== undefined) {
|
|
6879
7049
|
// An operator text moves the compaction reference forward, as a message does.
|
|
6880
7050
|
next = casUpdateLane(hqRoot, laneId, (current) => ({
|
|
@@ -9612,6 +9782,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
9612
9782
|
.option("--model <id>", "Model for the moved lane (with --account only)")
|
|
9613
9783
|
.option("--effort <level>", "Reasoning effort for the resumed provider turn")
|
|
9614
9784
|
.option("--worker <profile>", "Attach a personal worker profile when this message resumes the lane")
|
|
9785
|
+
.addOption(new Option("--grant-context <base64url>").hideHelp())
|
|
9615
9786
|
.option("--no-compact", "Skip the resume-time compaction check for this message")
|
|
9616
9787
|
.action(wrap(async (laneId, opts) => {
|
|
9617
9788
|
await runMessage(ctx, laneId, opts);
|
|
@@ -9656,9 +9827,11 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
9656
9827
|
}));
|
|
9657
9828
|
lanes
|
|
9658
9829
|
.command("resume")
|
|
9659
|
-
.description("Resume an interrupted, done or blocked lane on its recorded provider session")
|
|
9830
|
+
.description("Resume an interrupted, done or blocked lane on its recorded provider session; recover failed lanes with the senior-only --from-failed option")
|
|
9660
9831
|
.argument("<lane-id>", "Lane id")
|
|
9661
9832
|
.option("--text <msg>", "Operator message for the resumed turn")
|
|
9833
|
+
.option("--from-failed", "Explicitly resume a failed lane as its senior")
|
|
9834
|
+
.option("--reason <text>", "Required explanation for resuming from failed")
|
|
9662
9835
|
.option("--project <slug>", "Re-point project attribution from this resume")
|
|
9663
9836
|
.option("--story <id>", "Re-point story attribution from this resume")
|
|
9664
9837
|
.option("--account <id>", "Move the lane's session to this account of its provider before resuming")
|
|
@@ -9667,6 +9840,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
9667
9840
|
.option("--worker <profile>", "Attach a personal worker profile for the resumed turn")
|
|
9668
9841
|
.addOption(new Option("--profile-repin-context <base64url>").hideHelp())
|
|
9669
9842
|
.addOption(new Option("--grant-context <base64url>").hideHelp())
|
|
9843
|
+
.addOption(new Option("--failed-resume-context <base64url>").hideHelp())
|
|
9670
9844
|
.option("--keep-alive", "Automatically resume done lead lanes while their story remains open")
|
|
9671
9845
|
.option("--mesh-company <slug>", "Attach Work Mesh attribution after verifying company membership")
|
|
9672
9846
|
.option("--no-compact", "Skip the resume-time compaction check for this resume")
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* module rather than testing provider or state inline. A guard bound at one
|
|
18
18
|
* site and not another is the defect class this branch has already shipped.
|
|
19
19
|
*/
|
|
20
|
-
import { type LaneProvider, type LaneRecord, type LaneState } from "./types.js";
|
|
20
|
+
import { type LaneProvider, type LaneRecord, type LaneState, type SeniorRef } from "./types.js";
|
|
21
21
|
/** Which providers can continue an exited worker from a recorded session id. */
|
|
22
22
|
export declare const PROVIDER_SESSION_RESUME: Record<LaneProvider, boolean>;
|
|
23
23
|
export declare function providerSupportsSessionResume(provider: LaneProvider): boolean;
|
|
@@ -45,7 +45,8 @@ export declare function sessionNotPortableMessage(provider: LaneProvider): strin
|
|
|
45
45
|
* reconcile pass resumes it once every question is answered. `killed` and
|
|
46
46
|
* `cancelled` were stopped on purpose by the operator; a message must not
|
|
47
47
|
* silently restart them. `failed` means the run ended without an envelope or
|
|
48
|
-
* never started; the
|
|
48
|
+
* never started; only the lane's senior can explicitly recover it with
|
|
49
|
+
* `hq lanes resume <lane-id> --from-failed --reason <text>`. `interrupted` exited with
|
|
49
50
|
* partial output and no envelope, so the lane's state was never closed
|
|
50
51
|
* cleanly; only the explicit `hq lanes resume` opt-in continues it
|
|
51
52
|
* (RESUME_COMMAND_STATES). `queued` and `running` take the drop box, not a
|
|
@@ -139,6 +140,9 @@ export type LaunchTrigger = {
|
|
|
139
140
|
kind: "resume";
|
|
140
141
|
/** Lane state at the moment of the command; recorded as `last_resume.resumed_from`. */
|
|
141
142
|
from: ResumeCommandState | "failed";
|
|
143
|
+
fromFailed?: true;
|
|
144
|
+
/** Senior that authorized a failed-lane resume, rechecked at the claim CAS. */
|
|
145
|
+
expectedSenior?: SeniorRef;
|
|
142
146
|
text?: string;
|
|
143
147
|
message_id?: string;
|
|
144
148
|
};
|
|
@@ -152,7 +156,9 @@ export type LaunchPlan = {
|
|
|
152
156
|
* Refusal for `hq lanes resume`, in precedence order: wrong state, provider
|
|
153
157
|
* without a resume path, no recorded session. Null when the resume can go.
|
|
154
158
|
*/
|
|
155
|
-
export declare function resumeCommandRefusal(lane: DeliveryLane
|
|
159
|
+
export declare function resumeCommandRefusal(lane: DeliveryLane, options?: {
|
|
160
|
+
allowFailed?: boolean;
|
|
161
|
+
}): {
|
|
156
162
|
error: "lane_not_resumable" | "resume_unsupported" | "no_provider_session";
|
|
157
163
|
message: string;
|
|
158
164
|
} | null;
|
|
@@ -65,7 +65,8 @@ export function sessionNotPortableMessage(provider) {
|
|
|
65
65
|
* reconcile pass resumes it once every question is answered. `killed` and
|
|
66
66
|
* `cancelled` were stopped on purpose by the operator; a message must not
|
|
67
67
|
* silently restart them. `failed` means the run ended without an envelope or
|
|
68
|
-
* never started; the
|
|
68
|
+
* never started; only the lane's senior can explicitly recover it with
|
|
69
|
+
* `hq lanes resume <lane-id> --from-failed --reason <text>`. `interrupted` exited with
|
|
69
70
|
* partial output and no envelope, so the lane's state was never closed
|
|
70
71
|
* cleanly; only the explicit `hq lanes resume` opt-in continues it
|
|
71
72
|
* (RESUME_COMMAND_STATES). `queued` and `running` take the drop box, not a
|
|
@@ -137,7 +138,7 @@ export function deliveryRefusalReason(lane) {
|
|
|
137
138
|
case "interrupted":
|
|
138
139
|
return `Lane is ${state}. The run did not close cleanly and the operator has not decided what to do with it. Resuming from ${state} needs an explicit opt-in: hq lanes resume <lane-id> [--text <message>].`;
|
|
139
140
|
case "failed":
|
|
140
|
-
return `Lane is ${state}.
|
|
141
|
+
return `Lane is ${state}. A plain message does not restart it. Its senior can resume the recorded session with: hq lanes resume <lane-id> --from-failed --reason <text>.`;
|
|
141
142
|
default: {
|
|
142
143
|
const _exhaustive = state;
|
|
143
144
|
return _exhaustive;
|
|
@@ -345,8 +346,10 @@ export const LAUNCH_TRIGGERS = ["admit", "answered_questions", "message", "resum
|
|
|
345
346
|
* Refusal for `hq lanes resume`, in precedence order: wrong state, provider
|
|
346
347
|
* without a resume path, no recorded session. Null when the resume can go.
|
|
347
348
|
*/
|
|
348
|
-
export function resumeCommandRefusal(lane) {
|
|
349
|
-
if (!isResumeCommandState(lane.state) &&
|
|
349
|
+
export function resumeCommandRefusal(lane, options = {}) {
|
|
350
|
+
if (!isResumeCommandState(lane.state) &&
|
|
351
|
+
!isFailedEnvelopeRepairLane(lane) &&
|
|
352
|
+
!(options.allowFailed === true && lane.state === "failed")) {
|
|
350
353
|
return {
|
|
351
354
|
error: "lane_not_resumable",
|
|
352
355
|
message: `Lane is ${lane.state}. hq lanes resume continues a lane in ${RESUME_COMMAND_STATES.join(", ")}; ${lane.state === "awaiting_input"
|
|
@@ -395,7 +398,7 @@ export function planLaneLaunch(lane, trigger) {
|
|
|
395
398
|
return { mode: "resume", session_id: lane.provider_session_id };
|
|
396
399
|
}
|
|
397
400
|
case "resume": {
|
|
398
|
-
const refusal = resumeCommandRefusal(lane);
|
|
401
|
+
const refusal = resumeCommandRefusal(lane, { allowFailed: trigger.fromFailed === true });
|
|
399
402
|
if (refusal !== null || !hasRecordedSessionId(lane)) {
|
|
400
403
|
throw new LanesError(refusal?.error ?? "no_provider_session", refusal?.message ?? `Lane is ${lane.state} and has no recorded provider session`, { state: lane.state, provider: lane.provider });
|
|
401
404
|
}
|
|
@@ -418,7 +421,9 @@ export function claimableStatesFor(trigger, lane) {
|
|
|
418
421
|
case "message":
|
|
419
422
|
return envelopeRepairFailed ? [...RESUMABLE_LANE_STATES, "failed"] : RESUMABLE_LANE_STATES;
|
|
420
423
|
case "resume":
|
|
421
|
-
return
|
|
424
|
+
return trigger.fromFailed === true || envelopeRepairFailed
|
|
425
|
+
? [...RESUME_COMMAND_STATES, "failed"]
|
|
426
|
+
: RESUME_COMMAND_STATES;
|
|
422
427
|
default: {
|
|
423
428
|
const _exhaustive = trigger;
|
|
424
429
|
return _exhaustive;
|
|
@@ -151,12 +151,15 @@ export function buildOutpostLaneCommand(input) {
|
|
|
151
151
|
["--hire", input.hire],
|
|
152
152
|
["--grant-context", input.grantContext],
|
|
153
153
|
["--profile-repin-context", input.profileRepinContext],
|
|
154
|
+
["--failed-resume-context", input.failedResumeContext],
|
|
154
155
|
["--senior", input.senior],
|
|
155
156
|
["--reason", input.reason],
|
|
156
157
|
]) {
|
|
157
158
|
if (value !== undefined)
|
|
158
159
|
args.push(flag, value);
|
|
159
160
|
}
|
|
161
|
+
if (input.action === "resume" && input.fromFailed === true)
|
|
162
|
+
args.push("--from-failed");
|
|
160
163
|
if (input.action === "resume" && input.keepAlive === true)
|
|
161
164
|
args.push("--keep-alive");
|
|
162
165
|
if (input.compact === false)
|
package/dist/lib/lanes/spawn.js
CHANGED
|
@@ -1576,6 +1576,9 @@ function laneLaunchResult(lane) {
|
|
|
1576
1576
|
const { provider_home_at_spawn: _providerHomeAtSpawn, ...safe } = lane;
|
|
1577
1577
|
return safe;
|
|
1578
1578
|
}
|
|
1579
|
+
function sameSeniorRef(a, b) {
|
|
1580
|
+
return a.kind === b.kind && a.id === b.id;
|
|
1581
|
+
}
|
|
1579
1582
|
function inferTrigger(lane) {
|
|
1580
1583
|
if (lane.state === "awaiting_input")
|
|
1581
1584
|
return { kind: "answered_questions" };
|
|
@@ -1631,6 +1634,12 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
|
|
|
1631
1634
|
// starting fresh from the brief.
|
|
1632
1635
|
const pendingResume = launch.trigger === undefined && existing.state === "queued" ? existing.pending_resume : undefined;
|
|
1633
1636
|
const trigger = launch.trigger ?? inferTrigger(existing);
|
|
1637
|
+
const failedResumeClaim = (trigger.kind === "resume" && trigger.fromFailed === true) || pendingResume?.from_failed === true;
|
|
1638
|
+
const failedResumeExpectedSenior = trigger.kind === "resume" && trigger.fromFailed === true
|
|
1639
|
+
? trigger.expectedSenior
|
|
1640
|
+
: pendingResume?.from_failed === true
|
|
1641
|
+
? pendingResume.expected_senior
|
|
1642
|
+
: undefined;
|
|
1634
1643
|
// A message into a finished lane must not turn that lane `failed` when the
|
|
1635
1644
|
// account cannot be bound: nothing has started yet, so the record keeps its
|
|
1636
1645
|
// state and the caller sees the error. Admission and question resume keep
|
|
@@ -1971,6 +1980,15 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
|
|
|
1971
1980
|
if (!claimableStatesFor(trigger, current).includes(current.state)) {
|
|
1972
1981
|
throw new LanesError("spawn_state_mismatch", `Cannot mark ${current.lane_id} running from ${current.state}`, { lane_id: current.lane_id, state: current.state, trigger: trigger.kind });
|
|
1973
1982
|
}
|
|
1983
|
+
if (failedResumeClaim &&
|
|
1984
|
+
(failedResumeExpectedSenior === undefined ||
|
|
1985
|
+
!sameSeniorRef(current.senior, failedResumeExpectedSenior))) {
|
|
1986
|
+
throw new LanesError("failed_resume_senior_changed", `Lane ${current.lane_id}'s senior changed after its failed-resume request was authorized`, {
|
|
1987
|
+
lane_id: current.lane_id,
|
|
1988
|
+
expected_senior: failedResumeExpectedSenior ?? null,
|
|
1989
|
+
current_senior: current.senior,
|
|
1990
|
+
});
|
|
1991
|
+
}
|
|
1974
1992
|
claimed = true;
|
|
1975
1993
|
const next = {
|
|
1976
1994
|
...current,
|
|
@@ -2044,6 +2062,7 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
|
|
|
2044
2062
|
...(pendingResume.message_id !== undefined ? { message_id: pendingResume.message_id } : {}),
|
|
2045
2063
|
...(pendingResume.monitor_event === true ? { monitor_event: true } : {}),
|
|
2046
2064
|
...(pendingResume.resumed_from !== undefined ? { resumed_from: pendingResume.resumed_from } : {}),
|
|
2065
|
+
...(pendingResume.from_failed === true ? { from_failed: true } : {}),
|
|
2047
2066
|
...(pendingResume.text !== undefined ? { text: pendingResume.text } : {}),
|
|
2048
2067
|
...(pendingResume.compact !== undefined ? { compact: pendingResume.compact } : {}),
|
|
2049
2068
|
};
|
|
@@ -2065,6 +2084,7 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
|
|
|
2065
2084
|
...(trigger.kind === "resume"
|
|
2066
2085
|
? {
|
|
2067
2086
|
resumed_from: trigger.from,
|
|
2087
|
+
...(trigger.fromFailed === true ? { from_failed: true } : {}),
|
|
2068
2088
|
...(trigger.text !== undefined ? { text: trigger.text } : {}),
|
|
2069
2089
|
...(trigger.message_id !== undefined ? { message_id: trigger.message_id } : {}),
|
|
2070
2090
|
}
|
package/dist/lib/lanes/store.js
CHANGED
|
@@ -888,6 +888,11 @@ export function mergeLaneRecords(live, sidecars) {
|
|
|
888
888
|
worker_profile_repin_history: unionHistory(all.map((record) => record.worker_profile_repin_history ?? []), entryIdentity, base.worker_profile_repin_history ?? []),
|
|
889
889
|
}
|
|
890
890
|
: {}),
|
|
891
|
+
...(all.some((record) => record.failed_resume_history !== undefined)
|
|
892
|
+
? {
|
|
893
|
+
failed_resume_history: unionHistory(all.map((record) => record.failed_resume_history ?? []), entryIdentity, base.failed_resume_history ?? []),
|
|
894
|
+
}
|
|
895
|
+
: {}),
|
|
891
896
|
stories: mergeLaneStories(all, base),
|
|
892
897
|
};
|
|
893
898
|
}
|
|
@@ -400,6 +400,8 @@ export interface LaneRecord {
|
|
|
400
400
|
* like the other histories.
|
|
401
401
|
*/
|
|
402
402
|
account_history?: AccountHistoryEntry[];
|
|
403
|
+
/** Bounded audit history for explicitly authorized resumes from failed. */
|
|
404
|
+
failed_resume_history?: FailedResumeHistoryEntry[];
|
|
403
405
|
/**
|
|
404
406
|
* Set by an account move (`--account`) until the spawn claims the lane:
|
|
405
407
|
* the turn the move was asked to run. While set it reserves a slot on the
|
|
@@ -537,11 +539,20 @@ export interface AccountHistoryEntry {
|
|
|
537
539
|
at: string;
|
|
538
540
|
reason: "first_assignment" | "model_change" | "usage_limit" | "account_unavailable" | "recorded_account_logged_out" | "load_balance" | "operator_move";
|
|
539
541
|
}
|
|
542
|
+
export interface FailedResumeHistoryEntry {
|
|
543
|
+
at: string;
|
|
544
|
+
actor: string;
|
|
545
|
+
reason: string;
|
|
546
|
+
failure: string;
|
|
547
|
+
}
|
|
540
548
|
export interface PendingResume {
|
|
541
549
|
/** Fresh answered-question launches need no previous session id. */
|
|
542
550
|
session_id?: string;
|
|
543
551
|
trigger: "answered_questions" | "message" | "resume";
|
|
544
552
|
resumed_from?: "interrupted" | "done" | "blocked" | "failed";
|
|
553
|
+
from_failed?: true;
|
|
554
|
+
/** Senior that authorized a failed-lane resume, rechecked at the claim CAS. */
|
|
555
|
+
expected_senior?: SeniorRef;
|
|
545
556
|
text?: string;
|
|
546
557
|
message_id?: string;
|
|
547
558
|
/** The text is already framed as untrusted monitor data, not an operator instruction. */
|
|
@@ -572,6 +583,7 @@ export interface LaneResumeRecord {
|
|
|
572
583
|
* resumed it (`interrupted`, `done` or `blocked`) and the text they sent.
|
|
573
584
|
*/
|
|
574
585
|
resumed_from?: "interrupted" | "done" | "blocked" | "failed";
|
|
586
|
+
from_failed?: true;
|
|
575
587
|
text?: string;
|
|
576
588
|
/** False when the continued turn explicitly skipped compaction. */
|
|
577
589
|
compact?: boolean;
|
package/dist/lib/lanes/types.js
CHANGED
|
@@ -225,7 +225,7 @@ export const LANE_FIELD_WRITERS = {
|
|
|
225
225
|
pickup_validation_pending_since: ["creator", "command", "spawn", "reconcile"],
|
|
226
226
|
priority: ["command"], worker: ["creator", "command"], worker_path: ["creator", "command"], worker_pack: ["creator", "command"], worker_profile: ["creator", "command"], worker_profile_hash: ["creator", "command", "spawn"], worker_profile_repin_history: ["command"], worker_profile_ignored: ["creator", "command", "spawn"], worker_policy_delivery: ["command", "spawn"],
|
|
227
227
|
keep_alive: ["creator", "command"], keep_alive_standing: ["command"], keep_alive_opted_in_at: ["creator", "command"], keep_alive_blocked_reason: ["command", "reconcile"], keep_alive_resume_count: ["command", "reconcile"], keep_alive_last_resume_at: ["command", "reconcile"], keep_alive_next_eligible_at: ["command", "reconcile"], keep_alive_exhausted_at: ["command", "reconcile"],
|
|
228
|
-
provider: ["creator"], account: ["creator", "spawn", "command", "watcher", "reconcile"], account_selection: ["creator", "command"], 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"],
|
|
228
|
+
provider: ["creator"], account: ["creator", "spawn", "command", "watcher", "reconcile"], account_selection: ["creator", "command"], admission_accounts: ["creator", "command"], account_history: ["command", "spawn", "watcher", "reconcile"], failed_resume_history: ["command"], pending_resume: ["command", "spawn", "watcher", "reconcile"], placement_default: ["creator"], host: ["creator", "command"], origin_host: ["creator"], origin_host_source: ["creator"],
|
|
229
229
|
version: ["creator"], created_at: ["creator"], updated_at: ["creator"], attribution_history: ["command"],
|
|
230
230
|
last_envelope: ["watcher", "reconcile"], model: ["creator", "spawn", "command"], model_source: ["creator", "spawn", "command"], effort: ["creator", "spawn", "command"], effort_source: ["creator", "spawn", "command"], fast_mode: ["spawn"],
|
|
231
231
|
last_message_at: ["command"], last_compaction: ["spawn"], story_complete: ["watcher", "reconcile"], story_complete_reason: ["watcher", "reconcile"], story_status: ["watcher", "reconcile"],
|