@indigoai-us/hq-cli 5.229.0 → 5.229.2
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 +19 -0
- package/dist/command-catalog.generated.d.ts +3 -0
- package/dist/command-catalog.generated.js +4 -0
- package/dist/commands/lanes.js +157 -9
- package/dist/lib/lanes/index.d.ts +1 -1
- package/dist/lib/lanes/keep-alive.d.ts +16 -1
- package/dist/lib/lanes/keep-alive.js +51 -0
- package/dist/lib/lanes/outpost-dispatch.d.ts +1 -0
- package/dist/lib/lanes/outpost-dispatch.js +1 -0
- package/dist/lib/lanes/outpost-profile-repin.d.ts +17 -0
- package/dist/lib/lanes/outpost-profile-repin.js +84 -0
- package/dist/lib/lanes/reconcile.js +25 -11
- package/dist/lib/lanes/store.js +35 -0
- package/dist/lib/lanes/types.d.ts +12 -0
- package/dist/lib/lanes/types.js +2 -2
- package/dist/lib/lanes/watch.js +4 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.229.2] — 2026-09-27
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Standing keep-alive leads now wait after done turns for drop-box messages,
|
|
10
|
+
undelivered answers, or pending resumes; queued answers are included in the
|
|
11
|
+
next turn.
|
|
12
|
+
- An unreadable question record no longer aborts reconciliation of other lanes.
|
|
13
|
+
|
|
14
|
+
## [5.229.1] — 2026-09-27
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- An authorized `hq lanes resume --worker` on an Outpost re-pins a changed
|
|
19
|
+
worker profile before spawn, preserves the provider session, records the
|
|
20
|
+
hash change in lane history, and syncs the resulting lane record to primary.
|
|
21
|
+
|
|
5
22
|
## [5.229.0] — 2026-09-27
|
|
6
23
|
|
|
7
24
|
### Added
|
|
@@ -13,6 +30,8 @@
|
|
|
13
30
|
new entity, and to skip glossary or translation table rows, paths and names
|
|
14
31
|
built from tags or slugs.
|
|
15
32
|
|
|
33
|
+
## [5.228.4] — 2026-09-27
|
|
34
|
+
|
|
16
35
|
### Fixed
|
|
17
36
|
|
|
18
37
|
- Account history now distinguishes automatic moves caused by a quota hold, an
|
|
@@ -7541,6 +7541,9 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
7541
7541
|
}, {
|
|
7542
7542
|
readonly flags: "--off";
|
|
7543
7543
|
readonly description: "Disable keep-alive for this lane";
|
|
7544
|
+
}, {
|
|
7545
|
+
readonly flags: "--standing";
|
|
7546
|
+
readonly description: "Wait for queued work after every done turn; requires --on";
|
|
7544
7547
|
}, {
|
|
7545
7548
|
readonly flags: "--json";
|
|
7546
7549
|
readonly description: "Emit JSON on stdout (default)";
|
|
@@ -9750,6 +9750,10 @@ export const COMMAND_CATALOG = [
|
|
|
9750
9750
|
"flags": "--off",
|
|
9751
9751
|
"description": "Disable keep-alive for this lane"
|
|
9752
9752
|
},
|
|
9753
|
+
{
|
|
9754
|
+
"flags": "--standing",
|
|
9755
|
+
"description": "Wait for queued work after every done turn; requires --on"
|
|
9756
|
+
},
|
|
9753
9757
|
{
|
|
9754
9758
|
"flags": "--json",
|
|
9755
9759
|
"description": "Emit JSON on stdout (default)"
|
package/dist/commands/lanes.js
CHANGED
|
@@ -25,6 +25,7 @@ import { readLanesRegistryFlag } from "../lib/lanes/registry-flag.js";
|
|
|
25
25
|
import { resolveProcessFlagGate } from "../lib/flag-registry.js";
|
|
26
26
|
import { defaultTrackerActor, LANES_TRACKER_FLAG, LANES_TRACKER_MESH_FLAG, registerLaneTrackerCommands, } from "./lanes-tracker.js";
|
|
27
27
|
import { createOutpostHireGrantContext, parseOutpostHireGrantContext, sameHireGrantSenior, } from "../lib/lanes/hire-grant-dispatch.js";
|
|
28
|
+
import { createOutpostProfileRepinContext, parseOutpostProfileRepinContext, } from "../lib/lanes/outpost-profile-repin.js";
|
|
28
29
|
import { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity, resolveAdmissionHost, } from "../lib/lanes/admission-host.js";
|
|
29
30
|
export { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity } from "../lib/lanes/admission-host.js";
|
|
30
31
|
import { buildOutpostCapacityCommand, buildOutpostBalanceMoveCommand, buildOutpostIdentityBackfillCommand, buildOutpostLaneCommand, buildOutpostMoveTargetCommand, buildOutpostQuestionAnswerCommand, buildOutpostQuestionListCommand, buildOutpostQuestionShowCommand, buildOutpostPickupCommand, createOutpostsClient, dispatchErrorMessage, dispatchOutpostPickup, buildOutpostLaneListCommand, outpostRejectedEffortOption, parseOutpostPickupPayload, stageOutpostPayload, } from "../lib/lanes/outpost-dispatch.js";
|
|
@@ -3564,6 +3565,10 @@ function parseRemoteJson(stdout, surface) {
|
|
|
3564
3565
|
}
|
|
3565
3566
|
return null;
|
|
3566
3567
|
}
|
|
3568
|
+
function outpostRejectedProfileRepinOption(result) {
|
|
3569
|
+
return result.exitCode !== 0 &&
|
|
3570
|
+
/unknown option\s+(?:['"])?--profile-repin-context(?:['"])?(?:\s|$)/i.test(`${result.stderr}\n${result.stdout}`);
|
|
3571
|
+
}
|
|
3567
3572
|
function verifyOutpostProfileContent(hqRoot, lane) {
|
|
3568
3573
|
if (lane.host.kind !== "outpost" || lane.worker_profile === undefined)
|
|
3569
3574
|
return;
|
|
@@ -3579,6 +3584,89 @@ function verifyOutpostProfileContent(hqRoot, lane) {
|
|
|
3579
3584
|
});
|
|
3580
3585
|
}
|
|
3581
3586
|
}
|
|
3587
|
+
function resumeProfileRepinActor(ctx) {
|
|
3588
|
+
const laneId = ctx.env.HQ_LANE_ID?.trim();
|
|
3589
|
+
if (laneId)
|
|
3590
|
+
return `lane:${laneId}`;
|
|
3591
|
+
const sessionId = sessionIdFromEnv(ctx.env);
|
|
3592
|
+
return sessionId ? `session:${sessionId}` : "ssm-owner-dispatch";
|
|
3593
|
+
}
|
|
3594
|
+
function workerProfileHashMismatch(lane, profile) {
|
|
3595
|
+
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`, {
|
|
3596
|
+
lane_id: lane.lane_id,
|
|
3597
|
+
worker: profile.id,
|
|
3598
|
+
outpost: lane.host.kind === "outpost" ? lane.host.id : null,
|
|
3599
|
+
expected_hash: lane.worker_profile_hash ?? null,
|
|
3600
|
+
actual_hash: profile.profile.contentHash,
|
|
3601
|
+
});
|
|
3602
|
+
}
|
|
3603
|
+
function authorizeResumeProfileRepin(ctx, lane, profile, encodedContext) {
|
|
3604
|
+
const ssmOwnerDispatch = ctx.isSsmOwnerDispatch();
|
|
3605
|
+
if (encodedContext !== undefined && !ssmOwnerDispatch) {
|
|
3606
|
+
throw profile === undefined
|
|
3607
|
+
? new LanesError("worker_profile_hash_mismatch", "Profile re-pin context is only valid on an Outpost owner dispatch", { lane_id: lane.lane_id })
|
|
3608
|
+
: workerProfileHashMismatch(lane, profile);
|
|
3609
|
+
}
|
|
3610
|
+
let context;
|
|
3611
|
+
if (encodedContext !== undefined) {
|
|
3612
|
+
try {
|
|
3613
|
+
context = parseOutpostProfileRepinContext(encodedContext, lane.lane_id);
|
|
3614
|
+
}
|
|
3615
|
+
catch {
|
|
3616
|
+
throw profile === undefined
|
|
3617
|
+
? new LanesError("worker_profile_hash_mismatch", "Profile re-pin context is invalid", { lane_id: lane.lane_id })
|
|
3618
|
+
: workerProfileHashMismatch(lane, profile);
|
|
3619
|
+
}
|
|
3620
|
+
if (lane.host.kind !== "outpost" ||
|
|
3621
|
+
!sameHireGrantSenior(lane.senior, context.expected_senior)) {
|
|
3622
|
+
throw profile === undefined
|
|
3623
|
+
? new LanesError("worker_profile_hash_mismatch", "Lane senior changed after profile re-pin authorization", { lane_id: lane.lane_id })
|
|
3624
|
+
: workerProfileHashMismatch(lane, profile);
|
|
3625
|
+
}
|
|
3626
|
+
}
|
|
3627
|
+
if (profile === undefined ||
|
|
3628
|
+
lane.host.kind !== "outpost" ||
|
|
3629
|
+
lane.worker_profile_hash === profile.profile.contentHash) {
|
|
3630
|
+
return undefined;
|
|
3631
|
+
}
|
|
3632
|
+
if (ssmOwnerDispatch) {
|
|
3633
|
+
if (context === undefined)
|
|
3634
|
+
throw workerProfileHashMismatch(lane, profile);
|
|
3635
|
+
return {
|
|
3636
|
+
kind: "ssm-owner-dispatch",
|
|
3637
|
+
expectedSenior: context.expected_senior,
|
|
3638
|
+
actor: context.actor,
|
|
3639
|
+
reason: context.reason,
|
|
3640
|
+
newHash: profile.profile.contentHash,
|
|
3641
|
+
};
|
|
3642
|
+
}
|
|
3643
|
+
if (context !== undefined || !callerMatchesSenior(ctx, lane.senior)) {
|
|
3644
|
+
throw workerProfileHashMismatch(lane, profile);
|
|
3645
|
+
}
|
|
3646
|
+
return {
|
|
3647
|
+
kind: "senior",
|
|
3648
|
+
expectedSenior: lane.senior,
|
|
3649
|
+
actor: resumeProfileRepinActor(ctx),
|
|
3650
|
+
reason: "resume --worker",
|
|
3651
|
+
newHash: profile.profile.contentHash,
|
|
3652
|
+
};
|
|
3653
|
+
}
|
|
3654
|
+
function assertResumeProfileRepinStillAuthorized(ctx, current, authorization) {
|
|
3655
|
+
const authorized = current.host.kind === "outpost" &&
|
|
3656
|
+
sameHireGrantSenior(current.senior, authorization.expectedSenior) &&
|
|
3657
|
+
(authorization.kind === "ssm-owner-dispatch"
|
|
3658
|
+
? ctx.isSsmOwnerDispatch()
|
|
3659
|
+
: callerMatchesSenior(ctx, current.senior));
|
|
3660
|
+
if (!authorized) {
|
|
3661
|
+
throw new LanesError("worker_profile_hash_mismatch", `Lane ${current.lane_id}'s senior changed before its Outpost profile could be re-pinned`, {
|
|
3662
|
+
lane_id: current.lane_id,
|
|
3663
|
+
expected_senior: authorization.expectedSenior,
|
|
3664
|
+
current_senior: current.senior,
|
|
3665
|
+
expected_hash: current.worker_profile_hash ?? null,
|
|
3666
|
+
actual_hash: authorization.newHash,
|
|
3667
|
+
});
|
|
3668
|
+
}
|
|
3669
|
+
}
|
|
3582
3670
|
function laneIsHostedHere(ctx, lane) {
|
|
3583
3671
|
if (lane.host.kind !== "outpost")
|
|
3584
3672
|
return true;
|
|
@@ -3603,6 +3691,15 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
|
|
|
3603
3691
|
throw new LanesError("outpost_dispatch_failed", `Lane ${lane.lane_id} has no Outpost host to dispatch to`, { lane_id: lane.lane_id });
|
|
3604
3692
|
}
|
|
3605
3693
|
const outpostId = lane.host.id;
|
|
3694
|
+
const profileRepinContext = action === "resume" &&
|
|
3695
|
+
options.worker !== undefined &&
|
|
3696
|
+
callerMatchesSenior(ctx, lane.senior)
|
|
3697
|
+
? createOutpostProfileRepinContext({
|
|
3698
|
+
laneId: lane.lane_id,
|
|
3699
|
+
expectedSenior: lane.senior,
|
|
3700
|
+
actor: resumeProfileRepinActor(ctx),
|
|
3701
|
+
})
|
|
3702
|
+
: undefined;
|
|
3606
3703
|
let grantContext;
|
|
3607
3704
|
let grantHierarchy;
|
|
3608
3705
|
if (action === "grant") {
|
|
@@ -3618,7 +3715,7 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
|
|
|
3618
3715
|
}
|
|
3619
3716
|
let dispatched;
|
|
3620
3717
|
try {
|
|
3621
|
-
|
|
3718
|
+
const dispatchCommand = (repinContext) => ctx.dispatchOutpostExec({
|
|
3622
3719
|
outpostId,
|
|
3623
3720
|
command: buildOutpostLaneCommand({
|
|
3624
3721
|
action: action === "grant"
|
|
@@ -3642,9 +3739,15 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
|
|
|
3642
3739
|
senior: options.senior,
|
|
3643
3740
|
reason: options.reason,
|
|
3644
3741
|
grantContext,
|
|
3742
|
+
...(repinContext !== undefined ? { profileRepinContext: repinContext } : {}),
|
|
3645
3743
|
compact: options.compact,
|
|
3646
3744
|
}),
|
|
3647
3745
|
});
|
|
3746
|
+
dispatched = await dispatchCommand(profileRepinContext);
|
|
3747
|
+
if (profileRepinContext !== undefined &&
|
|
3748
|
+
outpostRejectedProfileRepinOption(dispatched)) {
|
|
3749
|
+
dispatched = await dispatchCommand();
|
|
3750
|
+
}
|
|
3648
3751
|
}
|
|
3649
3752
|
catch (err) {
|
|
3650
3753
|
throw new LanesError("outpost_dispatch_failed", `Could not dispatch ${action} for lane ${lane.lane_id} to Outpost ${outpostId}: ${dispatchErrorMessage(err)}`, { lane_id: lane.lane_id, outpost_id: outpostId });
|
|
@@ -3663,6 +3766,14 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
|
|
|
3663
3766
|
});
|
|
3664
3767
|
}
|
|
3665
3768
|
if (ownerError) {
|
|
3769
|
+
if (action === "resume" && ownerError.error === "worker_profile_hash_mismatch") {
|
|
3770
|
+
throw new LanesError(ownerError.error, ownerError.message, {
|
|
3771
|
+
...ownerError.details,
|
|
3772
|
+
lane_id: lane.lane_id,
|
|
3773
|
+
outpost_id: outpostId,
|
|
3774
|
+
command_id: dispatched.commandId,
|
|
3775
|
+
});
|
|
3776
|
+
}
|
|
3666
3777
|
throw new LanesError("outpost_dispatch_failed", `Outpost ${outpostId} rejected the ${action} command: ${ownerError.message}`, {
|
|
3667
3778
|
lane_id: lane.lane_id,
|
|
3668
3779
|
outpost_id: outpostId,
|
|
@@ -3708,8 +3819,9 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
|
|
|
3708
3819
|
: action === "reparent"
|
|
3709
3820
|
? remote.lane !== null && typeof remote.lane === "object" &&
|
|
3710
3821
|
Array.isArray(remote.changed_descendant_ids)
|
|
3711
|
-
: remote.delivery_status === "worker_spawned" ||
|
|
3712
|
-
remote.delivery_status === "queued_on_target_account")
|
|
3822
|
+
: (remote.delivery_status === "worker_spawned" ||
|
|
3823
|
+
remote.delivery_status === "queued_on_target_account") &&
|
|
3824
|
+
(options.worker === undefined || remote.lane !== undefined));
|
|
3713
3825
|
if (!resultConfirmed) {
|
|
3714
3826
|
throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} completed exec without a confirmed ${action} result for lane ${lane.lane_id}`, {
|
|
3715
3827
|
lane_id: lane.lane_id,
|
|
@@ -3730,19 +3842,22 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
|
|
|
3730
3842
|
});
|
|
3731
3843
|
return;
|
|
3732
3844
|
}
|
|
3733
|
-
if (action === "reparent") {
|
|
3845
|
+
if (action === "reparent" || (action === "resume" && options.worker !== undefined)) {
|
|
3734
3846
|
const ownerLane = syncLaneRecordFromOwner(ctx.hqRoot(), lane.lane_id, outpostId, remote.lane, undefined, { allowNonTerminal: true });
|
|
3735
3847
|
const descendants = [];
|
|
3736
|
-
|
|
3737
|
-
const
|
|
3738
|
-
|
|
3848
|
+
if (action === "reparent") {
|
|
3849
|
+
for (const descendantId of changedDescendantIds) {
|
|
3850
|
+
const ownerDescendant = await readOutpostLaneSnapshot(ctx, outpostId, descendantId);
|
|
3851
|
+
descendants.push(syncLaneRecordFromOwner(ctx.hqRoot(), descendantId, outpostId, ownerDescendant, undefined, { allowNonTerminal: true }));
|
|
3852
|
+
}
|
|
3739
3853
|
}
|
|
3740
3854
|
printJson({
|
|
3741
3855
|
...remote,
|
|
3742
3856
|
lane_id: ownerLane.lane_id,
|
|
3743
3857
|
lane: ownerLane,
|
|
3744
|
-
|
|
3745
|
-
|
|
3858
|
+
...(action === "reparent"
|
|
3859
|
+
? { changed_descendant_ids: changedDescendantIds, descendants }
|
|
3860
|
+
: {}),
|
|
3746
3861
|
outpost_id: outpostId,
|
|
3747
3862
|
outpost_command_id: dispatched.commandId,
|
|
3748
3863
|
outpost_transport: "outposts_exec",
|
|
@@ -4860,6 +4975,7 @@ async function runShow(ctx, laneId) {
|
|
|
4860
4975
|
keep_alive_state: {
|
|
4861
4976
|
enabled: keepAliveState.enabled,
|
|
4862
4977
|
source: keepAliveState.source,
|
|
4978
|
+
standing: storedLane.keep_alive_standing === true,
|
|
4863
4979
|
opted_in_at: storedLane.keep_alive_opted_in_at ?? null,
|
|
4864
4980
|
blocked_reason: storedLane.keep_alive_blocked_reason ??
|
|
4865
4981
|
(storedLane.keep_alive === true ? null : "not_opted_in"),
|
|
@@ -4989,6 +5105,9 @@ function printGrantResult(hqRoot, updated, hierarchy) {
|
|
|
4989
5105
|
printJson({ ok: true, action: "grant", lane_id: updated.lane_id, lane: updated, ...fields });
|
|
4990
5106
|
}
|
|
4991
5107
|
async function runKeepAlive(ctx, laneId, opts) {
|
|
5108
|
+
if (opts.standing && !opts.on) {
|
|
5109
|
+
throw new LanesError("keep_alive_standing_requires_on", "Pass --standing together with --on", { lane_id: laneId });
|
|
5110
|
+
}
|
|
4992
5111
|
if (Boolean(opts.on) === Boolean(opts.off)) {
|
|
4993
5112
|
throw new LanesError("keep_alive_mode_invalid", "Pass exactly one of --on or --off", { lane_id: laneId });
|
|
4994
5113
|
}
|
|
@@ -5016,6 +5135,7 @@ async function runKeepAlive(ctx, laneId, opts) {
|
|
|
5016
5135
|
return updateLane(hqRoot, laneId, lane.version, "command", (current) => ({
|
|
5017
5136
|
...current,
|
|
5018
5137
|
keep_alive: Boolean(opts.on),
|
|
5138
|
+
keep_alive_standing: Boolean(opts.on && opts.standing),
|
|
5019
5139
|
keep_alive_opted_in_at: optedInAt,
|
|
5020
5140
|
keep_alive_blocked_reason: undefined,
|
|
5021
5141
|
}));
|
|
@@ -5248,6 +5368,18 @@ function applyResumeProfileAndEffort(current, next, input) {
|
|
|
5248
5368
|
next.worker_pack = input.profile.pack;
|
|
5249
5369
|
next.worker_profile = profile.id;
|
|
5250
5370
|
next.worker_profile_hash = profile.contentHash;
|
|
5371
|
+
if (input.profileRepin !== undefined &&
|
|
5372
|
+
current.host.kind === "outpost" &&
|
|
5373
|
+
current.worker_profile_hash !== profile.contentHash) {
|
|
5374
|
+
const entry = {
|
|
5375
|
+
old_hash: current.worker_profile_hash ?? null,
|
|
5376
|
+
new_hash: profile.contentHash,
|
|
5377
|
+
actor: input.profileRepin.actor,
|
|
5378
|
+
at: input.at ?? new Date().toISOString(),
|
|
5379
|
+
reason: input.profileRepin.reason,
|
|
5380
|
+
};
|
|
5381
|
+
next.worker_profile_repin_history = appendCappedHistory(current.worker_profile_repin_history ?? [], entry);
|
|
5382
|
+
}
|
|
5251
5383
|
next.worker_profile_ignored = ignoredWorkerProfileFields(current.provider);
|
|
5252
5384
|
next.worker_policy_delivery = undefined;
|
|
5253
5385
|
if (profile.meta.keep_alive !== undefined) {
|
|
@@ -5308,6 +5440,9 @@ function recordResumeSetup(ctx, lane, input) {
|
|
|
5308
5440
|
if (current.version !== lane.version) {
|
|
5309
5441
|
throw new LanesError("lane_resume_setup_conflict", `Lane ${lane.lane_id} changed while resume settings were prepared; run the resume again`, { lane_id: lane.lane_id, prepared_version: lane.version, version: current.version });
|
|
5310
5442
|
}
|
|
5443
|
+
if (input.profileRepin !== undefined) {
|
|
5444
|
+
assertResumeProfileRepinStillAuthorized(ctx, current, input.profileRepin);
|
|
5445
|
+
}
|
|
5311
5446
|
const next = { ...current };
|
|
5312
5447
|
applyResumeProfileAndEffort(current, next, { ...input, at: ctx.now().toISOString() });
|
|
5313
5448
|
if (input.cliModel !== undefined) {
|
|
@@ -5525,6 +5660,9 @@ async function prepareAccountMove(ctx, lane, input) {
|
|
|
5525
5660
|
if (current.version !== lane.version) {
|
|
5526
5661
|
throw new LanesError("account_move_conflict", `Lane ${laneId} changed while the move was prepared (version ${lane.version} → ${current.version}, state ${current.state}, account ${current.account ?? "(none)"}); run the move again`, { lane_id: laneId, prepared_version: lane.version, version: current.version, state: current.state, account: current.account ?? null });
|
|
5527
5662
|
}
|
|
5663
|
+
if (input.profileRepin !== undefined) {
|
|
5664
|
+
assertResumeProfileRepinStillAuthorized(ctx, current, input.profileRepin);
|
|
5665
|
+
}
|
|
5528
5666
|
const next = {
|
|
5529
5667
|
...current,
|
|
5530
5668
|
account: target,
|
|
@@ -5549,6 +5687,7 @@ async function prepareAccountMove(ctx, lane, input) {
|
|
|
5549
5687
|
};
|
|
5550
5688
|
applyResumeProfileAndEffort(current, next, {
|
|
5551
5689
|
...(input.workerProfile !== undefined ? { profile: input.workerProfile } : {}),
|
|
5690
|
+
...(input.profileRepin !== undefined ? { profileRepin: input.profileRepin } : {}),
|
|
5552
5691
|
...(input.effort !== undefined ? { effort: input.effort } : {}),
|
|
5553
5692
|
at,
|
|
5554
5693
|
});
|
|
@@ -6410,6 +6549,9 @@ function laneWithRecoveredProviderSession(hqRoot, lane) {
|
|
|
6410
6549
|
async function runResume(ctx, laneId, opts) {
|
|
6411
6550
|
const hqRoot = ctx.hqRoot();
|
|
6412
6551
|
let lane = readLane(hqRoot, laneId);
|
|
6552
|
+
if (opts.profileRepinContext !== undefined && !ctx.isSsmOwnerDispatch()) {
|
|
6553
|
+
throw new LanesError("worker_profile_hash_mismatch", "Profile re-pin context is only valid on an Outpost owner dispatch", { lane_id: laneId });
|
|
6554
|
+
}
|
|
6413
6555
|
if (lane.host.kind === "outpost" && !laneIsHostedHere(ctx, lane)) {
|
|
6414
6556
|
await dispatchOutpostLaneCommand(ctx, lane, "resume", opts);
|
|
6415
6557
|
return;
|
|
@@ -6478,6 +6620,7 @@ async function runResume(ctx, laneId, opts) {
|
|
|
6478
6620
|
const resumeProfile = opts.worker !== undefined
|
|
6479
6621
|
? resolveResumeProfile(ctx, lane, opts.worker)
|
|
6480
6622
|
: undefined;
|
|
6623
|
+
const profileRepin = authorizeResumeProfileRepin(ctx, lane, resumeProfile, opts.profileRepinContext);
|
|
6481
6624
|
const keepAlive = opts.keepAlive === true
|
|
6482
6625
|
? true
|
|
6483
6626
|
: resumeProfile?.profile.meta.keep_alive;
|
|
@@ -6502,6 +6645,7 @@ async function runResume(ctx, laneId, opts) {
|
|
|
6502
6645
|
account: opts.account,
|
|
6503
6646
|
cliModel: opts.model?.trim() || undefined,
|
|
6504
6647
|
...(resumeProfile !== undefined ? { workerProfile: resumeProfile } : {}),
|
|
6648
|
+
...(profileRepin !== undefined ? { profileRepin } : {}),
|
|
6505
6649
|
...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
|
|
6506
6650
|
pending: {
|
|
6507
6651
|
trigger: "resume",
|
|
@@ -6526,6 +6670,7 @@ async function runResume(ctx, laneId, opts) {
|
|
|
6526
6670
|
? { preservedModel: automaticAccountSelection.preservedModel }
|
|
6527
6671
|
: {}),
|
|
6528
6672
|
...(resumeProfile !== undefined ? { workerProfile: resumeProfile } : {}),
|
|
6673
|
+
...(profileRepin !== undefined ? { profileRepin } : {}),
|
|
6529
6674
|
...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
|
|
6530
6675
|
pending: {
|
|
6531
6676
|
trigger: "resume",
|
|
@@ -6544,6 +6689,7 @@ async function runResume(ctx, laneId, opts) {
|
|
|
6544
6689
|
}
|
|
6545
6690
|
lane = recordResumeSetup(ctx, lane, {
|
|
6546
6691
|
...(move === null && resumeProfile !== undefined ? { profile: resumeProfile } : {}),
|
|
6692
|
+
...(move === null && profileRepin !== undefined ? { profileRepin } : {}),
|
|
6547
6693
|
...(move === null && resumeEffort !== undefined ? { effort: resumeEffort } : {}),
|
|
6548
6694
|
...(keepAlive !== undefined ? { keepAlive } : {}),
|
|
6549
6695
|
...(move === null && keepCurrentAccount && opts.model !== undefined ? { cliModel: opts.model } : {}),
|
|
@@ -9188,6 +9334,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
9188
9334
|
.argument("<lane-id>", "Lane id")
|
|
9189
9335
|
.option("--on", "Enable explicit keep-alive for this lane")
|
|
9190
9336
|
.option("--off", "Disable keep-alive for this lane")
|
|
9337
|
+
.option("--standing", "Wait for queued work after every done turn; requires --on")
|
|
9191
9338
|
.option("--json", "Emit JSON on stdout (default)")
|
|
9192
9339
|
.action(wrap(async (laneId, opts) => {
|
|
9193
9340
|
await runKeepAlive(ctx, laneId, opts);
|
|
@@ -9419,6 +9566,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
9419
9566
|
.option("--model <id>", "Model for the moved lane (with --account only)")
|
|
9420
9567
|
.option("--effort <level>", "Reasoning effort for the resumed provider turn")
|
|
9421
9568
|
.option("--worker <profile>", "Attach a personal worker profile for the resumed turn")
|
|
9569
|
+
.addOption(new Option("--profile-repin-context <base64url>").hideHelp())
|
|
9422
9570
|
.option("--keep-alive", "Automatically resume done lead lanes while their story remains open")
|
|
9423
9571
|
.option("--mesh-company <slug>", "Attach Work Mesh attribution after verifying company membership")
|
|
9424
9572
|
.option("--no-compact", "Skip the resume-time compaction check for this resume")
|
|
@@ -10,7 +10,7 @@ export type { AdmissionCapReason, AdmissionHost } from "./admission.js";
|
|
|
10
10
|
export { buildHostCapacitySnapshot, chooseHostForLane, parseHostCapacitySnapshot, } from "./host-capacity.js";
|
|
11
11
|
export type { HostCapacityAccount, HostCapacityProvider, HostCapacitySnapshot, HostPlacementChoice, } from "./host-capacity.js";
|
|
12
12
|
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";
|
|
13
|
-
export type { AccountConfig, AttributionHistoryEntry, EnvelopeDecision, EnvelopeQuestion, HostKind, LaneCompanyRef, LaneAccountSelection, LaneAccountSelectionConsidered, LaneAccountSelectionHostCount, 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";
|
|
13
|
+
export type { AccountConfig, AttributionHistoryEntry, EnvelopeDecision, EnvelopeQuestion, HostKind, LaneCompanyRef, LaneAccountSelection, LaneAccountSelectionConsidered, LaneAccountSelectionHostCount, 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, WorkerProfileRepinHistoryEntry, StoryStatus, TerminalLaneState, ThrottleConfig, } from "./types.js";
|
|
14
14
|
export { ACTIVE_LANE_STORY_STATUSES, activeLaneStories, laneStories, laneStory, storyKey, storyStatusForEnvelope, storyWarning, withCurrentStory, withLaneStories, } from "./stories.js";
|
|
15
15
|
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";
|
|
16
16
|
export type { ConfigLoadDeps, ConfigWriteDeps } from "./config.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { LaneRecord } from "./types.js";
|
|
1
|
+
import type { LaneRecord, QuestionRecord } from "./types.js";
|
|
2
2
|
export declare const LEAD_LANE_KEEP_ALIVE_FLAG = "lanes.lead-keep-alive";
|
|
3
3
|
export declare const LEAD_LANE_KEEP_ALIVE_DEFAULT_ENABLED = false;
|
|
4
4
|
export declare const LEAD_LANE_KEEP_ALIVE_MAX_RESUMES = 24;
|
|
@@ -7,6 +7,21 @@ export declare const LEAD_LANE_KEEP_ALIVE_MAX_BACKOFF_MS: number;
|
|
|
7
7
|
export declare const LEAD_LANE_KEEP_ALIVE_RESUME_TEXT = "Automatic keep-alive resume: your last turn ended with decision done and story_complete false. Continue the open work from where you stopped. End with done only when story_complete is true; if you are waiting, use hq lanes ask or stay in the loop.";
|
|
8
8
|
export declare const LEAD_LANE_KEEP_ALIVE_INTERRUPTED_RESUME_TEXT = "Automatic keep-alive resume: your previous turn was interrupted by a provider process exit. Check your last state before continuing. Continue the open work from where you stopped. End with done only when story_complete is true; if you are waiting, use hq lanes ask or stay in the loop.";
|
|
9
9
|
export type KeepAliveBlockedReason = NonNullable<LaneRecord["keep_alive_blocked_reason"]>;
|
|
10
|
+
/** The latest terminal envelope fields used by both watch and reconcile. */
|
|
11
|
+
export interface KeepAliveDoneEnvelope {
|
|
12
|
+
decision?: string;
|
|
13
|
+
story_complete?: boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Shared done-state policy: ordinary opted-in lanes resume open stories;
|
|
17
|
+
* standing leads wait for queued work, even when each turn marks the story
|
|
18
|
+
* complete. Watch and reconcile both use this decision.
|
|
19
|
+
*/
|
|
20
|
+
export declare function canResumeDoneKeepAliveLane(lane: Pick<LaneRecord, "keep_alive_standing">, envelope: KeepAliveDoneEnvelope, hasQueuedWork: boolean): boolean;
|
|
21
|
+
/** Work signals already persisted for a lane that ended its turn. */
|
|
22
|
+
export declare function hasPendingKeepAliveWork(hqRoot: string, lane: Pick<LaneRecord, "lane_id" | "company" | "pending_resume">, questionRecords?: readonly QuestionRecord[]): boolean;
|
|
23
|
+
/** Undelivered answers that belong to this lane and company. */
|
|
24
|
+
export declare function undeliveredAnsweredQuestions(lane: Pick<LaneRecord, "lane_id" | "company">, questions: readonly QuestionRecord[]): QuestionRecord[];
|
|
10
25
|
export declare function retiredLaneRegistryPath(hqRoot: string): string;
|
|
11
26
|
/** Missing registry means no retired lanes; every present invalid file fails closed. */
|
|
12
27
|
export declare function loadRetiredLaneIds(hqRoot: string): ReadonlySet<string>;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { LanesError } from "./errors.js";
|
|
4
|
+
import { dropboxInboxHasMessages } from "./dropbox.js";
|
|
5
|
+
import { listQuestionIds, readQuestion } from "./store.js";
|
|
4
6
|
export const LEAD_LANE_KEEP_ALIVE_FLAG = "lanes.lead-keep-alive";
|
|
5
7
|
export const LEAD_LANE_KEEP_ALIVE_DEFAULT_ENABLED = false;
|
|
6
8
|
export const LEAD_LANE_KEEP_ALIVE_MAX_RESUMES = 24;
|
|
@@ -8,6 +10,55 @@ export const LEAD_LANE_KEEP_ALIVE_BASE_BACKOFF_MS = 5 * 60 * 1000;
|
|
|
8
10
|
export const LEAD_LANE_KEEP_ALIVE_MAX_BACKOFF_MS = 60 * 60 * 1000;
|
|
9
11
|
export const LEAD_LANE_KEEP_ALIVE_RESUME_TEXT = "Automatic keep-alive resume: your last turn ended with decision done and story_complete false. Continue the open work from where you stopped. End with done only when story_complete is true; if you are waiting, use hq lanes ask or stay in the loop.";
|
|
10
12
|
export const LEAD_LANE_KEEP_ALIVE_INTERRUPTED_RESUME_TEXT = "Automatic keep-alive resume: your previous turn was interrupted by a provider process exit. Check your last state before continuing. Continue the open work from where you stopped. End with done only when story_complete is true; if you are waiting, use hq lanes ask or stay in the loop.";
|
|
13
|
+
/**
|
|
14
|
+
* Shared done-state policy: ordinary opted-in lanes resume open stories;
|
|
15
|
+
* standing leads wait for queued work, even when each turn marks the story
|
|
16
|
+
* complete. Watch and reconcile both use this decision.
|
|
17
|
+
*/
|
|
18
|
+
export function canResumeDoneKeepAliveLane(lane, envelope, hasQueuedWork) {
|
|
19
|
+
if (envelope.decision !== "done")
|
|
20
|
+
return false;
|
|
21
|
+
return lane.keep_alive_standing === true
|
|
22
|
+
? hasQueuedWork
|
|
23
|
+
: envelope.story_complete !== true;
|
|
24
|
+
}
|
|
25
|
+
/** Work signals already persisted for a lane that ended its turn. */
|
|
26
|
+
export function hasPendingKeepAliveWork(hqRoot, lane, questionRecords) {
|
|
27
|
+
if (lane.pending_resume !== undefined)
|
|
28
|
+
return true;
|
|
29
|
+
if (dropboxInboxHasMessages(hqRoot, lane.lane_id))
|
|
30
|
+
return true;
|
|
31
|
+
const questions = questionRecords ?? readKeepAliveQuestionRecords(hqRoot);
|
|
32
|
+
return undeliveredAnsweredQuestions(lane, questions).length > 0;
|
|
33
|
+
}
|
|
34
|
+
/** Undelivered answers that belong to this lane and company. */
|
|
35
|
+
export function undeliveredAnsweredQuestions(lane, questions) {
|
|
36
|
+
return questions.filter((question) => {
|
|
37
|
+
const sameCompany = question.company === null
|
|
38
|
+
? lane.company === null
|
|
39
|
+
: lane.company !== null &&
|
|
40
|
+
question.company.uid === lane.company.uid &&
|
|
41
|
+
question.company.slug === lane.company.slug;
|
|
42
|
+
return (question.lane_id === lane.lane_id &&
|
|
43
|
+
question.origin.id === lane.lane_id &&
|
|
44
|
+
sameCompany &&
|
|
45
|
+
question.status === "answered" &&
|
|
46
|
+
question.answer !== undefined &&
|
|
47
|
+
(question.delivered_at === null || question.delivered_at === undefined));
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
function readKeepAliveQuestionRecords(hqRoot) {
|
|
51
|
+
const questions = [];
|
|
52
|
+
for (const questionId of listQuestionIds(hqRoot)) {
|
|
53
|
+
try {
|
|
54
|
+
questions.push(readQuestion(hqRoot, questionId));
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
process.stderr.write(`hq lanes keep-alive: skipped unreadable question ${questionId}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return questions;
|
|
61
|
+
}
|
|
11
62
|
export function retiredLaneRegistryPath(hqRoot) {
|
|
12
63
|
return path.join(hqRoot, "workspace", "lane-briefs", "juniors.json");
|
|
13
64
|
}
|
|
@@ -150,6 +150,7 @@ export function buildOutpostLaneCommand(input) {
|
|
|
150
150
|
["--worker", input.worker],
|
|
151
151
|
["--hire", input.hire],
|
|
152
152
|
["--grant-context", input.grantContext],
|
|
153
|
+
["--profile-repin-context", input.profileRepinContext],
|
|
153
154
|
["--senior", input.senior],
|
|
154
155
|
["--reason", input.reason],
|
|
155
156
|
]) {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type SeniorRef } from "./types.js";
|
|
2
|
+
declare const PROFILE_REPIN_CONTEXT_SCHEMA: "hq-lane-profile-repin-context.v1";
|
|
3
|
+
export interface OutpostProfileRepinContext {
|
|
4
|
+
schema_version: typeof PROFILE_REPIN_CONTEXT_SCHEMA;
|
|
5
|
+
lane_id: string;
|
|
6
|
+
expected_senior: SeniorRef;
|
|
7
|
+
actor: string;
|
|
8
|
+
reason: "resume --worker";
|
|
9
|
+
}
|
|
10
|
+
export declare function createOutpostProfileRepinContext(input: {
|
|
11
|
+
laneId: string;
|
|
12
|
+
expectedSenior: SeniorRef;
|
|
13
|
+
actor: string;
|
|
14
|
+
}): string;
|
|
15
|
+
export declare function parseOutpostProfileRepinContext(encoded: string, expectedLaneId: string): OutpostProfileRepinContext;
|
|
16
|
+
export {};
|
|
17
|
+
//# sourceMappingURL=outpost-profile-repin.d.ts.map
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { LanesError } from "./errors.js";
|
|
2
|
+
import { isSeniorKind } from "./types.js";
|
|
3
|
+
const PROFILE_REPIN_CONTEXT_SCHEMA = "hq-lane-profile-repin-context.v1";
|
|
4
|
+
const MAX_CONTEXT_BYTES = 4 * 1024;
|
|
5
|
+
function contextError(message) {
|
|
6
|
+
return new LanesError("worker_profile_repin_context_invalid", message, {});
|
|
7
|
+
}
|
|
8
|
+
function parseSenior(value) {
|
|
9
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
10
|
+
return null;
|
|
11
|
+
const record = value;
|
|
12
|
+
if (Object.keys(record).some((key) => key !== "kind" && key !== "id"))
|
|
13
|
+
return null;
|
|
14
|
+
if (!isSeniorKind(record.kind))
|
|
15
|
+
return null;
|
|
16
|
+
if (record.id !== undefined &&
|
|
17
|
+
(typeof record.id !== "string" || !record.id.trim() || record.id.trim() !== record.id))
|
|
18
|
+
return null;
|
|
19
|
+
if ((record.kind === "session" || record.kind === "lane") && record.id === undefined)
|
|
20
|
+
return null;
|
|
21
|
+
if (record.kind === "user" && record.id !== undefined)
|
|
22
|
+
return null;
|
|
23
|
+
return record.id === undefined
|
|
24
|
+
? { kind: record.kind }
|
|
25
|
+
: { kind: record.kind, id: record.id };
|
|
26
|
+
}
|
|
27
|
+
export function createOutpostProfileRepinContext(input) {
|
|
28
|
+
const context = {
|
|
29
|
+
schema_version: PROFILE_REPIN_CONTEXT_SCHEMA,
|
|
30
|
+
lane_id: input.laneId,
|
|
31
|
+
expected_senior: input.expectedSenior.id === undefined
|
|
32
|
+
? { kind: input.expectedSenior.kind }
|
|
33
|
+
: { kind: input.expectedSenior.kind, id: input.expectedSenior.id },
|
|
34
|
+
actor: input.actor,
|
|
35
|
+
reason: "resume --worker",
|
|
36
|
+
};
|
|
37
|
+
const encoded = Buffer.from(JSON.stringify(context), "utf8").toString("base64url");
|
|
38
|
+
if (Buffer.byteLength(encoded, "utf8") > MAX_CONTEXT_BYTES) {
|
|
39
|
+
throw contextError(`Profile re-pin context for lane ${input.laneId} is too large`);
|
|
40
|
+
}
|
|
41
|
+
return encoded;
|
|
42
|
+
}
|
|
43
|
+
export function parseOutpostProfileRepinContext(encoded, expectedLaneId) {
|
|
44
|
+
if (encoded.length === 0 ||
|
|
45
|
+
encoded.length > MAX_CONTEXT_BYTES ||
|
|
46
|
+
!/^[A-Za-z0-9_-]+$/.test(encoded)) {
|
|
47
|
+
throw contextError("Profile re-pin context must be bounded base64url text");
|
|
48
|
+
}
|
|
49
|
+
let raw;
|
|
50
|
+
try {
|
|
51
|
+
const decoded = Buffer.from(encoded, "base64url");
|
|
52
|
+
if (decoded.toString("base64url") !== encoded) {
|
|
53
|
+
throw new Error("non-canonical base64url");
|
|
54
|
+
}
|
|
55
|
+
raw = JSON.parse(decoded.toString("utf8"));
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
throw contextError("Profile re-pin context is not valid base64url JSON");
|
|
59
|
+
}
|
|
60
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
61
|
+
throw contextError("Profile re-pin context must be a JSON object");
|
|
62
|
+
}
|
|
63
|
+
const record = raw;
|
|
64
|
+
if (Object.keys(record).some((key) => !["schema_version", "lane_id", "expected_senior", "actor", "reason"].includes(key))) {
|
|
65
|
+
throw contextError("Profile re-pin context contains unknown fields");
|
|
66
|
+
}
|
|
67
|
+
const expectedSenior = parseSenior(record.expected_senior);
|
|
68
|
+
if (record.schema_version !== PROFILE_REPIN_CONTEXT_SCHEMA ||
|
|
69
|
+
record.lane_id !== expectedLaneId ||
|
|
70
|
+
expectedSenior === null ||
|
|
71
|
+
typeof record.actor !== "string" ||
|
|
72
|
+
!record.actor.trim() ||
|
|
73
|
+
record.reason !== "resume --worker") {
|
|
74
|
+
throw contextError(`Profile re-pin context is invalid for lane ${expectedLaneId}`);
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
schema_version: PROFILE_REPIN_CONTEXT_SCHEMA,
|
|
78
|
+
lane_id: expectedLaneId,
|
|
79
|
+
expected_senior: expectedSenior,
|
|
80
|
+
actor: record.actor,
|
|
81
|
+
reason: "resume --worker",
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=outpost-profile-repin.js.map
|
|
@@ -21,7 +21,7 @@ import { ANSWERED_QUESTIONS_FILE, armLaneWatcher, envelopeRepairMode, isPidRecor
|
|
|
21
21
|
import { appendCappedHistory, lanesStoreRoot, parseLaneRecord, parseQuestionRecord, readLane, readQuestion, updateLane, updateQuestion, } from "./store.js";
|
|
22
22
|
import { applyThrottleDecision, decideAdmission, defaultThrottleDeps, emptyThrottleState, evaluateThrottle, isCpuCheckEnabled, readAvailableMemMb, readThrottleState, resolveCpuBusyFraction, throttleStatePath, writeThrottleState, } from "./throttle.js";
|
|
23
23
|
import { readLanesRegistryFlag } from "./registry-flag.js";
|
|
24
|
-
import { LEAD_LANE_KEEP_ALIVE_FLAG, LEAD_LANE_KEEP_ALIVE_INTERRUPTED_RESUME_TEXT, LEAD_LANE_KEEP_ALIVE_MAX_RESUMES, LEAD_LANE_KEEP_ALIVE_RESUME_TEXT, keepAliveExclusion, loadRetiredLaneIds, leadLaneKeepAliveBackoffMs, } from "./keep-alive.js";
|
|
24
|
+
import { LEAD_LANE_KEEP_ALIVE_FLAG, LEAD_LANE_KEEP_ALIVE_INTERRUPTED_RESUME_TEXT, LEAD_LANE_KEEP_ALIVE_MAX_RESUMES, LEAD_LANE_KEEP_ALIVE_RESUME_TEXT, canResumeDoneKeepAliveLane, hasPendingKeepAliveWork, keepAliveExclusion, loadRetiredLaneIds, leadLaneKeepAliveBackoffMs, undeliveredAnsweredQuestions, } from "./keep-alive.js";
|
|
25
25
|
import { providerSupportsSessionResume } from "./delivery.js";
|
|
26
26
|
import { applyDefaultQuotaHold, readQuotaSignal, recordAccountQuota } from "./quota.js";
|
|
27
27
|
import { answeredQuestionEntries, questionRecordRefusalCode, raiseEnvelopeQuestions, resolveQuestionCompanyWithFallback, } from "./questions.js";
|
|
@@ -818,7 +818,10 @@ export async function runPassBody(hqRoot, deps) {
|
|
|
818
818
|
continue;
|
|
819
819
|
if (lane.state === "done") {
|
|
820
820
|
const envelope = readLatestEnvelopeRecord(hqRoot, lane.lane_id)?.envelope;
|
|
821
|
-
if (!envelope || envelope.decision !== "done"
|
|
821
|
+
if (!envelope || envelope.decision !== "done")
|
|
822
|
+
continue;
|
|
823
|
+
const hasQueuedWork = lane.keep_alive_standing === true && hasPendingKeepAliveWork(hqRoot, lane, questions);
|
|
824
|
+
if (!canResumeDoneKeepAliveLane(lane, envelope, hasQueuedWork))
|
|
822
825
|
continue;
|
|
823
826
|
}
|
|
824
827
|
const key = keepAliveCompanyKey(lane.company);
|
|
@@ -834,9 +837,9 @@ export async function runPassBody(hqRoot, deps) {
|
|
|
834
837
|
}));
|
|
835
838
|
await withAllLaneAdmissionLocks(hqRoot, async () => {
|
|
836
839
|
const freshLanes = loadLaneRecords(hqRoot, summary);
|
|
837
|
-
resumeKeepAlive(hqRoot, freshLanes, summary, deps, now, keepAliveFlagByCompany);
|
|
838
|
-
const drainLanes = loadLaneRecords(hqRoot, summary);
|
|
839
840
|
const freshQuestions = loadQuestionRecords(hqRoot, summary);
|
|
841
|
+
resumeKeepAlive(hqRoot, freshLanes, freshQuestions, summary, deps, now, keepAliveFlagByCompany);
|
|
842
|
+
const drainLanes = loadLaneRecords(hqRoot, summary);
|
|
840
843
|
await admitQueued(hqRoot, drainLanes, summary, deps, now);
|
|
841
844
|
await resumeAwaiting(hqRoot, drainLanes, freshQuestions, summary, deps);
|
|
842
845
|
});
|
|
@@ -1532,7 +1535,7 @@ function latestEnvelopeMtimeMs(hqRoot, lane, ref) {
|
|
|
1532
1535
|
throw new LanesError("keep_alive_envelope_timestamp_unavailable", `Cannot stat latest envelope for lane ${lane.lane_id}: ${err instanceof Error ? err.message : String(err)}`, { lane_id: lane.lane_id, path: filePath });
|
|
1533
1536
|
}
|
|
1534
1537
|
}
|
|
1535
|
-
function resumeKeepAlive(hqRoot, lanes, summary, deps, now, flagByCompany) {
|
|
1538
|
+
function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, flagByCompany) {
|
|
1536
1539
|
const nowMs = now.getTime();
|
|
1537
1540
|
const nowIso = now.toISOString();
|
|
1538
1541
|
const config = loadLanesConfig(hqRoot);
|
|
@@ -1566,8 +1569,9 @@ function resumeKeepAlive(hqRoot, lanes, summary, deps, now, flagByCompany) {
|
|
|
1566
1569
|
const envelope = readLatestEnvelopeRecord(hqRoot, lane.lane_id)?.envelope;
|
|
1567
1570
|
if (!envelope || envelope.decision !== "done")
|
|
1568
1571
|
continue;
|
|
1569
|
-
|
|
1570
|
-
|
|
1572
|
+
const hasQueuedWork = lane.keep_alive_standing === true && hasPendingKeepAliveWork(hqRoot, lane, questions);
|
|
1573
|
+
if (!canResumeDoneKeepAliveLane(lane, envelope, hasQueuedWork)) {
|
|
1574
|
+
if (envelope.story_complete === true && lane.keep_alive_standing !== true && hasKeepAliveProgress(lane)) {
|
|
1571
1575
|
casLaneIf(hqRoot, lane.lane_id, (current) => current.version === lane.version &&
|
|
1572
1576
|
current.state === "done" &&
|
|
1573
1577
|
current.keep_alive === lane.keep_alive, (current) => ({ ...clearedKeepAliveProgress(current), keep_alive_blocked_reason: undefined }), deps.dryRun);
|
|
@@ -1652,7 +1656,10 @@ function resumeKeepAlive(hqRoot, lanes, summary, deps, now, flagByCompany) {
|
|
|
1652
1656
|
let eligibilityAtMs;
|
|
1653
1657
|
if (initialState === "done") {
|
|
1654
1658
|
const latest = readLatestEnvelopeRecord(hqRoot, lane.lane_id);
|
|
1655
|
-
if (!latest || latest.envelope.decision !== "done"
|
|
1659
|
+
if (!latest || latest.envelope.decision !== "done")
|
|
1660
|
+
continue;
|
|
1661
|
+
const hasQueuedWork = lane.keep_alive_standing === true && hasPendingKeepAliveWork(hqRoot, lane, questions);
|
|
1662
|
+
if (!canResumeDoneKeepAliveLane(lane, latest.envelope, hasQueuedWork))
|
|
1656
1663
|
continue;
|
|
1657
1664
|
try {
|
|
1658
1665
|
eligibilityAtMs = latestEnvelopeMtimeMs(hqRoot, lane, latest.ref);
|
|
@@ -1726,8 +1733,9 @@ function resumeKeepAlive(hqRoot, lanes, summary, deps, now, flagByCompany) {
|
|
|
1726
1733
|
const currentEnvelope = readLatestEnvelopeRecord(hqRoot, current.lane_id);
|
|
1727
1734
|
if (!currentEnvelope || currentEnvelope.envelope.decision !== "done")
|
|
1728
1735
|
continue;
|
|
1729
|
-
|
|
1730
|
-
|
|
1736
|
+
const hasQueuedWork = current.keep_alive_standing === true && hasPendingKeepAliveWork(hqRoot, current, questions);
|
|
1737
|
+
if (!canResumeDoneKeepAliveLane(current, currentEnvelope.envelope, hasQueuedWork)) {
|
|
1738
|
+
if (currentEnvelope.envelope.story_complete === true && current.keep_alive_standing !== true && hasKeepAliveProgress(current)) {
|
|
1731
1739
|
casLaneIf(hqRoot, current.lane_id, (latestLane) => latestLane.version === current.version &&
|
|
1732
1740
|
latestLane.state === "done" &&
|
|
1733
1741
|
latestLane.keep_alive === true &&
|
|
@@ -1840,6 +1848,12 @@ function resumeKeepAlive(hqRoot, lanes, summary, deps, now, flagByCompany) {
|
|
|
1840
1848
|
const resumeText = resumedFrom === "interrupted"
|
|
1841
1849
|
? LEAD_LANE_KEEP_ALIVE_INTERRUPTED_RESUME_TEXT
|
|
1842
1850
|
: LEAD_LANE_KEEP_ALIVE_RESUME_TEXT;
|
|
1851
|
+
const carriesAnsweredQuestions = resumedFrom === "done" &&
|
|
1852
|
+
current.keep_alive_standing === true &&
|
|
1853
|
+
undeliveredAnsweredQuestions(current, questions).length > 0;
|
|
1854
|
+
if (carriesAnsweredQuestions && !deps.dryRun) {
|
|
1855
|
+
writeAnsweredQuestions(hqRoot, current, questions);
|
|
1856
|
+
}
|
|
1843
1857
|
const queue = (expectedSeniorId) => casLaneIf(hqRoot, current.lane_id, (latestLane) => latestLane.version === current.version &&
|
|
1844
1858
|
latestLane.state === current.state &&
|
|
1845
1859
|
latestLane.keep_alive === true &&
|
|
@@ -1855,7 +1869,7 @@ function resumeKeepAlive(hqRoot, lanes, summary, deps, now, flagByCompany) {
|
|
|
1855
1869
|
terminal_details: undefined,
|
|
1856
1870
|
pending_resume: {
|
|
1857
1871
|
session_id: currentSessionId,
|
|
1858
|
-
trigger: "resume",
|
|
1872
|
+
trigger: carriesAnsweredQuestions ? "answered_questions" : "resume",
|
|
1859
1873
|
resumed_from: resumedFrom,
|
|
1860
1874
|
text: resumeText,
|
|
1861
1875
|
compact: true,
|
package/dist/lib/lanes/store.js
CHANGED
|
@@ -409,6 +409,13 @@ export function parseLaneRecord(raw, filePath) {
|
|
|
409
409
|
value: raw.keep_alive,
|
|
410
410
|
});
|
|
411
411
|
}
|
|
412
|
+
if (raw.keep_alive_standing !== undefined && typeof raw.keep_alive_standing !== "boolean") {
|
|
413
|
+
throw new LaneSchemaError("keep_alive_standing must be a boolean", {
|
|
414
|
+
path: filePath,
|
|
415
|
+
field: "keep_alive_standing",
|
|
416
|
+
value: raw.keep_alive_standing,
|
|
417
|
+
});
|
|
418
|
+
}
|
|
412
419
|
const keepAliveBlockedReason = raw.keep_alive_blocked_reason;
|
|
413
420
|
const validKeepAliveBlockedReasons = [
|
|
414
421
|
"not_opted_in",
|
|
@@ -562,6 +569,29 @@ export function parseLaneRecord(raw, filePath) {
|
|
|
562
569
|
path: filePath,
|
|
563
570
|
});
|
|
564
571
|
}
|
|
572
|
+
if (raw.worker_profile_repin_history !== undefined) {
|
|
573
|
+
if (!Array.isArray(raw.worker_profile_repin_history)) {
|
|
574
|
+
throw new LaneSchemaError("worker_profile_repin_history must be an array when present", {
|
|
575
|
+
path: filePath,
|
|
576
|
+
field: "worker_profile_repin_history",
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
for (const [index, entry] of raw.worker_profile_repin_history.entries()) {
|
|
580
|
+
const field = `worker_profile_repin_history[${index}]`;
|
|
581
|
+
if (!isPlainObject(entry) || Object.keys(entry).some((key) => !["old_hash", "new_hash", "actor", "at", "reason"].includes(key))) {
|
|
582
|
+
throw new LaneSchemaError(`${field} must be an audit entry`, { path: filePath, field });
|
|
583
|
+
}
|
|
584
|
+
if (entry.old_hash !== null && (typeof entry.old_hash !== "string" || !entry.old_hash.trim())) {
|
|
585
|
+
throw new LaneSchemaError(`${field}.old_hash must be a non-empty string or null`, { path: filePath, field });
|
|
586
|
+
}
|
|
587
|
+
for (const key of ["new_hash", "actor", "reason"]) {
|
|
588
|
+
if (typeof entry[key] !== "string" || !entry[key].trim()) {
|
|
589
|
+
throw new LaneSchemaError(`${field}.${key} must be a non-empty string`, { path: filePath, field });
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
requireTimestamp(entry.at, `${field}.at`, filePath);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
565
595
|
if (raw.account_selection !== undefined) {
|
|
566
596
|
parseLaneAccountSelection(raw.account_selection, filePath);
|
|
567
597
|
}
|
|
@@ -853,6 +883,11 @@ export function mergeLaneRecords(live, sidecars) {
|
|
|
853
883
|
account_history: unionHistory(all.map((record) => record.account_history ?? []), entryIdentity, base.account_history ?? []),
|
|
854
884
|
}
|
|
855
885
|
: {}),
|
|
886
|
+
...(all.some((record) => record.worker_profile_repin_history !== undefined)
|
|
887
|
+
? {
|
|
888
|
+
worker_profile_repin_history: unionHistory(all.map((record) => record.worker_profile_repin_history ?? []), entryIdentity, base.worker_profile_repin_history ?? []),
|
|
889
|
+
}
|
|
890
|
+
: {}),
|
|
856
891
|
stories: mergeLaneStories(all, base),
|
|
857
892
|
};
|
|
858
893
|
}
|
|
@@ -283,6 +283,14 @@ export interface SeniorHistoryEntry {
|
|
|
283
283
|
by: string;
|
|
284
284
|
reason: string;
|
|
285
285
|
}
|
|
286
|
+
/** Evidence for a senior-authorized Outpost worker-profile content re-pin. */
|
|
287
|
+
export interface WorkerProfileRepinHistoryEntry {
|
|
288
|
+
old_hash: string | null;
|
|
289
|
+
new_hash: string;
|
|
290
|
+
actor: string;
|
|
291
|
+
at: string;
|
|
292
|
+
reason: string;
|
|
293
|
+
}
|
|
286
294
|
export interface AttributionHistoryEntry {
|
|
287
295
|
project_id: string;
|
|
288
296
|
story_id: string;
|
|
@@ -356,6 +364,8 @@ export interface LaneRecord {
|
|
|
356
364
|
worker_profile?: string;
|
|
357
365
|
/** Whether this lead lane should resume completed turns while its story is open. */
|
|
358
366
|
keep_alive?: boolean;
|
|
367
|
+
/** Whether a keep-alive lead waits for queued work after every done turn. */
|
|
368
|
+
keep_alive_standing?: boolean;
|
|
359
369
|
/** Time this lane most recently explicitly opted into keep-alive. */
|
|
360
370
|
keep_alive_opted_in_at?: string;
|
|
361
371
|
/** Why reconcile declined to resume this lane most recently. */
|
|
@@ -372,6 +382,8 @@ export interface LaneRecord {
|
|
|
372
382
|
hire_grant?: number;
|
|
373
383
|
/** Content hash of the profile last applied to this lane. */
|
|
374
384
|
worker_profile_hash?: string;
|
|
385
|
+
/** Bounded audit history of authorized Outpost profile hash re-pins. */
|
|
386
|
+
worker_profile_repin_history?: WorkerProfileRepinHistoryEntry[];
|
|
375
387
|
/** Profile fields intentionally unsupported by the selected provider. */
|
|
376
388
|
worker_profile_ignored?: string[];
|
|
377
389
|
/** How policy files were delivered on the most recent profile spawn. */
|
package/dist/lib/lanes/types.js
CHANGED
|
@@ -223,8 +223,8 @@ export const LANE_FIELD_WRITERS = {
|
|
|
223
223
|
state: ["creator", "command", "watcher", "reconcile", "spawn"], queued_reason: ["creator", "command", "spawn", "watcher", "reconcile"], queued_at: ["creator", "command", "spawn", "watcher", "reconcile"], admission_reserved_at: ["creator", "command", "reconcile", "spawn", "watcher"],
|
|
224
224
|
pickup_validation_pending: ["creator", "command", "spawn", "reconcile"],
|
|
225
225
|
pickup_validation_pending_since: ["creator", "command", "spawn", "reconcile"],
|
|
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_ignored: ["creator", "command", "spawn"], worker_policy_delivery: ["command", "spawn"],
|
|
227
|
-
keep_alive: ["creator", "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"],
|
|
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
|
+
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
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"],
|
|
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"],
|
package/dist/lib/lanes/watch.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { listQuestionIds, readLane, readQuestion, } from "./store.js";
|
|
2
2
|
import { LaneNotFoundError, LanesError } from "./errors.js";
|
|
3
3
|
import { providerSupportsSessionResume } from "./delivery.js";
|
|
4
|
-
import { LEAD_LANE_KEEP_ALIVE_MAX_RESUMES } from "./keep-alive.js";
|
|
4
|
+
import { canResumeDoneKeepAliveLane, hasPendingKeepAliveWork, LEAD_LANE_KEEP_ALIVE_MAX_RESUMES, } from "./keep-alive.js";
|
|
5
5
|
import { isTerminalLaneState, } from "./types.js";
|
|
6
6
|
export const DEFAULT_LANE_WATCH_INTERVAL_MS = 5_000;
|
|
7
7
|
export const MAX_LANE_WATCH_INTERVAL_MS = 30_000;
|
|
@@ -32,7 +32,7 @@ function validateLaneIds(laneIds) {
|
|
|
32
32
|
}
|
|
33
33
|
return unique;
|
|
34
34
|
}
|
|
35
|
-
function canResumeKeepAliveLane(lane) {
|
|
35
|
+
function canResumeKeepAliveLane(hqRoot, lane) {
|
|
36
36
|
if (lane.keep_alive !== true ||
|
|
37
37
|
lane.keep_alive_opted_in_at === undefined ||
|
|
38
38
|
!Number.isFinite(Date.parse(lane.keep_alive_opted_in_at)) ||
|
|
@@ -49,8 +49,7 @@ function canResumeKeepAliveLane(lane) {
|
|
|
49
49
|
return lane.terminal_reason === "exited_with_partial_output";
|
|
50
50
|
}
|
|
51
51
|
if (lane.state === "done") {
|
|
52
|
-
return lane.last_envelope?.decision ===
|
|
53
|
-
lane.story_complete !== true &&
|
|
52
|
+
return canResumeDoneKeepAliveLane(lane, { decision: lane.last_envelope?.decision, story_complete: lane.story_complete }, lane.keep_alive_standing === true && hasPendingKeepAliveWork(hqRoot, lane)) &&
|
|
54
53
|
lane.terminal_reason !== "provider_rejected" &&
|
|
55
54
|
lane.terminal_reason !== "account_not_logged_in";
|
|
56
55
|
}
|
|
@@ -133,7 +132,7 @@ export async function runLaneWatch(hqRoot, laneIds, deps = {}) {
|
|
|
133
132
|
if (deps.keepAliveEnabled !== undefined) {
|
|
134
133
|
for (const record of records) {
|
|
135
134
|
if (isTerminalLaneState(record.state) &&
|
|
136
|
-
canResumeKeepAliveLane(record) &&
|
|
135
|
+
canResumeKeepAliveLane(hqRoot, record) &&
|
|
137
136
|
await deps.keepAliveEnabled(record))
|
|
138
137
|
keepAliveWaiting.add(record.lane_id);
|
|
139
138
|
}
|