@indigoai-us/hq-cli 5.199.0 → 5.200.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 +29 -9
- package/dist/commands/lanes.js +223 -22
- package/dist/lib/lanes/outpost-dispatch.d.ts +4 -2
- package/dist/lib/lanes/outpost-dispatch.js +8 -3
- package/dist/lib/lanes/outpost-hire-cache.d.ts +20 -0
- package/dist/lib/lanes/outpost-hire-cache.js +146 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.200.1] — 2026-09-25
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- `hq lanes questions show` and `answer` preserve `question_not_found` when
|
|
10
|
+
Outpost question visibility is disabled, even if an owned Outpost lane has
|
|
11
|
+
no company. `questions list --all-hosts` also omits the company-unavailable
|
|
12
|
+
error while the gate is off.
|
|
13
|
+
|
|
14
|
+
## [5.200.0] — 2026-09-25
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- `hq lanes list` now fetches Outpost hire summaries once per host and reuses
|
|
19
|
+
them for 60 seconds. Missing or invalid owner rows keep the local summary and
|
|
20
|
+
include a warning.
|
|
21
|
+
|
|
5
22
|
## [5.199.0] — 2026-09-25
|
|
6
23
|
|
|
7
24
|
### Added
|
|
@@ -15,8 +32,6 @@
|
|
|
15
32
|
company-scoped `lanes.outpost-question-visibility` hq-flags gate. Use
|
|
16
33
|
`--company` to limit a list or answer to one company.
|
|
17
34
|
|
|
18
|
-
## [5.198.1] — 2026-09-25
|
|
19
|
-
|
|
20
35
|
### Fixed
|
|
21
36
|
|
|
22
37
|
- Registry-backed hq-flags readers now use the central registry endpoint when
|
|
@@ -24,6 +39,18 @@
|
|
|
24
39
|
keep precedence, and process-wide gates retain their legacy fallback until a
|
|
25
40
|
registry snapshot is ready.
|
|
26
41
|
|
|
42
|
+
## [5.198.1] — 2026-09-25
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
|
|
46
|
+
- Session questions stay with live or unknown sessions until their configured
|
|
47
|
+
unanswered timeout; only a positive `session_end` advances immediately. When
|
|
48
|
+
there is no Work Mesh entry, hq-cli checks Claude transcript metadata in
|
|
49
|
+
configured account homes without opening transcript contents, using the
|
|
50
|
+
newest matching mtime. Metadata errors remain visible and do not block the
|
|
51
|
+
independent timeout. The timeout defaults to 600 seconds. A running-lane
|
|
52
|
+
answer reports `awaiting_next_tool_call` after its durable dropbox append.
|
|
53
|
+
|
|
27
54
|
## [5.198.0] — 2026-09-25
|
|
28
55
|
|
|
29
56
|
### Added
|
|
@@ -47,13 +74,6 @@
|
|
|
47
74
|
`result` / `error.message` / `subtype` — instead of the last characters of
|
|
48
75
|
the JSON tail (`"costUsdTicks" | } | "numTurns" | }`). Log only; never
|
|
49
76
|
posted to a person.
|
|
50
|
-
- Session questions stay with live or unknown sessions until their configured
|
|
51
|
-
unanswered timeout; only a positive `session_end` advances immediately. When
|
|
52
|
-
there is no Work Mesh entry, hq-cli checks Claude transcript metadata in
|
|
53
|
-
configured account homes without opening transcript contents, using the
|
|
54
|
-
newest matching mtime. Metadata errors remain visible and do not block the
|
|
55
|
-
independent timeout. The timeout defaults to 600 seconds. A running-lane
|
|
56
|
-
answer reports `awaiting_next_tool_call` after its durable dropbox append.
|
|
57
77
|
|
|
58
78
|
## [5.197.0] — 2026-09-25
|
|
59
79
|
|
package/dist/commands/lanes.js
CHANGED
|
@@ -23,6 +23,7 @@ import { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity, resolveAdmissionHost
|
|
|
23
23
|
export { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity } from "../lib/lanes/admission-host.js";
|
|
24
24
|
export { LANE_HIRE_GRANTS_FLAG };
|
|
25
25
|
import { buildOutpostCapacityCommand, buildOutpostBalanceMoveCommand, buildOutpostIdentityBackfillCommand, buildOutpostLaneCommand, buildOutpostMoveTargetCommand, buildOutpostQuestionAnswerCommand, buildOutpostQuestionListCommand, buildOutpostQuestionShowCommand, buildOutpostPickupCommand, createOutpostsClient, dispatchErrorMessage, dispatchOutpostPickup, buildOutpostLaneListCommand, outpostRejectedEffortOption, parseOutpostPickupPayload, stageOutpostPayload, } from "../lib/lanes/outpost-dispatch.js";
|
|
26
|
+
import { outpostHireSummaryCacheKey, readOutpostHireSummaryCache, writeOutpostHireSummaryCache, } from "../lib/lanes/outpost-hire-cache.js";
|
|
26
27
|
import { buildHostCapacitySnapshot, chooseHostForLane, parseHostCapacitySnapshot, } from "../lib/lanes/host-capacity.js";
|
|
27
28
|
import { BALANCE_MOVABLE_STATES, CLAUDE_ACCOUNT_BALANCE_FLAG, accountLoadsForReport, assertBalanceApplyEnabled, planClaudeBalanceMoves, selectClaudeAccount, } from "../lib/lanes/claude-account-balance.js";
|
|
28
29
|
import { parseUserYaml } from "../utils/user-yaml-error.js";
|
|
@@ -578,6 +579,32 @@ function normalizeLaneRecordView(raw) {
|
|
|
578
579
|
return normalized;
|
|
579
580
|
}
|
|
580
581
|
async function ownerLaneHireFields(ctx, lane) {
|
|
582
|
+
if (lane.host.kind !== "outpost") {
|
|
583
|
+
throw new LanesError("outpost_lane_hire_summary_invalid", "Owner summary requires an Outpost-hosted lane", {
|
|
584
|
+
lane_id: lane.lane_id,
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
const nowMs = ctx.now().getTime();
|
|
588
|
+
const cachedEntries = readOutpostHireSummaryCache(ctx.hqRoot(), nowMs);
|
|
589
|
+
const cached = cachedEntries.get(outpostHireSummaryCacheKey(lane.host.id, lane.lane_id));
|
|
590
|
+
if (cached) {
|
|
591
|
+
return {
|
|
592
|
+
hire_grant: cached.hire_grant,
|
|
593
|
+
hire_alive_children: cached.hire_alive_children,
|
|
594
|
+
hire: cached.hire,
|
|
595
|
+
hire_summary: cached.hire_summary,
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
const fields = await fetchOwnerLaneHireFields(ctx, lane);
|
|
599
|
+
const cachedAtMs = ctx.now().getTime();
|
|
600
|
+
writeOutpostHireSummaryCache(ctx.hqRoot(), [{
|
|
601
|
+
outpost_id: lane.host.id,
|
|
602
|
+
lane_id: lane.lane_id,
|
|
603
|
+
...fields,
|
|
604
|
+
}], cachedAtMs, cachedEntries);
|
|
605
|
+
return fields;
|
|
606
|
+
}
|
|
607
|
+
async function fetchOwnerLaneHireFields(ctx, lane) {
|
|
581
608
|
if (lane.host.kind !== "outpost") {
|
|
582
609
|
throw new LanesError("outpost_lane_hire_summary_invalid", "Owner summary requires an Outpost-hosted lane", {
|
|
583
610
|
lane_id: lane.lane_id,
|
|
@@ -608,25 +635,42 @@ async function ownerLaneHireFields(ctx, lane) {
|
|
|
608
635
|
if (!remote || remote.ok !== true || remote.action !== "show") {
|
|
609
636
|
throw new LanesError("outpost_lane_hire_summary_invalid", `Outpost ${outpostId} returned an invalid hire summary for lane ${lane.lane_id}`, { lane_id: lane.lane_id, outpost_id: outpostId, command_id: dispatched.commandId });
|
|
610
637
|
}
|
|
638
|
+
return validateOwnerLaneHireSummary({
|
|
639
|
+
outpostId,
|
|
640
|
+
laneId: lane.lane_id,
|
|
641
|
+
rawLane: remote.lane,
|
|
642
|
+
hireGrant: remote.hire_grant,
|
|
643
|
+
hireAliveChildren: remote.hire_alive_children,
|
|
644
|
+
hire: remote.hire,
|
|
645
|
+
hireSummary: remote.hire_summary,
|
|
646
|
+
commandId: dispatched.commandId,
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
function validateOwnerLaneHireSummary(input) {
|
|
611
650
|
let ownerLane;
|
|
612
651
|
try {
|
|
613
|
-
ownerLane = parseLaneRecord(normalizeLaneRecordView(
|
|
652
|
+
ownerLane = parseLaneRecord(normalizeLaneRecordView(input.rawLane));
|
|
614
653
|
}
|
|
615
654
|
catch (err) {
|
|
616
|
-
throw new LanesError("outpost_lane_hire_summary_invalid", `Outpost ${outpostId} returned an invalid lane snapshot for ${
|
|
655
|
+
throw new LanesError("outpost_lane_hire_summary_invalid", `Outpost ${input.outpostId} returned an invalid lane snapshot for ${input.laneId}`, {
|
|
656
|
+
lane_id: input.laneId,
|
|
657
|
+
outpost_id: input.outpostId,
|
|
658
|
+
command_id: input.commandId,
|
|
659
|
+
cause: dispatchErrorMessage(err),
|
|
660
|
+
});
|
|
617
661
|
}
|
|
618
|
-
const grant =
|
|
619
|
-
const alive =
|
|
620
|
-
if (ownerLane.lane_id !==
|
|
662
|
+
const grant = input.hireGrant;
|
|
663
|
+
const alive = input.hireAliveChildren;
|
|
664
|
+
if (ownerLane.lane_id !== input.laneId ||
|
|
621
665
|
ownerLane.host.kind !== "outpost" ||
|
|
622
|
-
ownerLane.host.id !== outpostId ||
|
|
666
|
+
ownerLane.host.id !== input.outpostId ||
|
|
623
667
|
!Number.isSafeInteger(grant) ||
|
|
624
668
|
grant < 0 ||
|
|
625
669
|
!Number.isSafeInteger(alive) ||
|
|
626
670
|
alive < 0 ||
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
throw new LanesError("outpost_lane_hire_summary_invalid", `Outpost ${outpostId} returned an inconsistent hire summary for lane ${
|
|
671
|
+
input.hire !== `${String(alive)}/${String(grant)}` ||
|
|
672
|
+
input.hireSummary !== `hire ${String(alive)}/${String(grant)}`) {
|
|
673
|
+
throw new LanesError("outpost_lane_hire_summary_invalid", `Outpost ${input.outpostId} returned an inconsistent hire summary for lane ${input.laneId}`, { lane_id: input.laneId, outpost_id: input.outpostId, command_id: input.commandId });
|
|
630
674
|
}
|
|
631
675
|
return {
|
|
632
676
|
hire_grant: grant,
|
|
@@ -635,6 +679,116 @@ async function ownerLaneHireFields(ctx, lane) {
|
|
|
635
679
|
hire_summary: `hire ${alive}/${grant}`,
|
|
636
680
|
};
|
|
637
681
|
}
|
|
682
|
+
async function fetchOutpostListHireSummaries(ctx, outpostId, lanes) {
|
|
683
|
+
const fallback = (error) => new Map(lanes.map((lane) => [lane.lane_id, {
|
|
684
|
+
error: new LanesError(error.error, error.message, {
|
|
685
|
+
...error.details,
|
|
686
|
+
outpost_id: outpostId,
|
|
687
|
+
lane_id: lane.lane_id,
|
|
688
|
+
}),
|
|
689
|
+
}]));
|
|
690
|
+
let hqBinary;
|
|
691
|
+
try {
|
|
692
|
+
hqBinary = outpostCliOverride(ctx, outpostId);
|
|
693
|
+
}
|
|
694
|
+
catch (err) {
|
|
695
|
+
if (isLanesError(err))
|
|
696
|
+
return fallback(err);
|
|
697
|
+
return fallback(new LanesError("outpost_lane_hire_summary_unavailable", `Could not read hire summaries from Outpost ${outpostId}: ${dispatchErrorMessage(err)}`, { outpost_id: outpostId }));
|
|
698
|
+
}
|
|
699
|
+
let dispatched;
|
|
700
|
+
try {
|
|
701
|
+
// No filters: the owner list must return every lane requested by this host group.
|
|
702
|
+
dispatched = await ctx.dispatchOutpostExec({
|
|
703
|
+
outpostId,
|
|
704
|
+
command: buildOutpostLaneListCommand({ hqBinary }),
|
|
705
|
+
timeoutSeconds: OUTPOST_HIRE_SUMMARY_TIMEOUT_SECONDS,
|
|
706
|
+
waitMs: OUTPOST_HIRE_SUMMARY_WAIT_MS,
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
catch (err) {
|
|
710
|
+
return fallback(new LanesError("outpost_lane_hire_summary_unavailable", `Could not read hire summaries from Outpost ${outpostId}: ${dispatchErrorMessage(err)}`, { outpost_id: outpostId }));
|
|
711
|
+
}
|
|
712
|
+
if (dispatched.done !== true || dispatched.status !== "Success" || dispatched.exitCode !== 0) {
|
|
713
|
+
return fallback(new LanesError("outpost_lane_hire_summary_unavailable", `Outpost ${outpostId} did not confirm its lane list for hire summaries`, { outpost_id: outpostId, command_id: dispatched.commandId }));
|
|
714
|
+
}
|
|
715
|
+
const remote = parseRemoteJson(dispatched.stdout, "Outpost lane hire summary list");
|
|
716
|
+
if (!remote || remote.ok !== true || remote.action !== "list" || !Array.isArray(remote.lanes)) {
|
|
717
|
+
return fallback(new LanesError("outpost_lane_hire_summary_invalid", `Outpost ${outpostId} returned an invalid lane list for hire summaries`, { outpost_id: outpostId, command_id: dispatched.commandId }));
|
|
718
|
+
}
|
|
719
|
+
const outcomes = new Map();
|
|
720
|
+
for (const lane of lanes) {
|
|
721
|
+
const matchingRows = remote.lanes.filter((row) => row !== null && typeof row === "object" && !Array.isArray(row) &&
|
|
722
|
+
row.lane_id === lane.lane_id);
|
|
723
|
+
if (matchingRows.length === 0) {
|
|
724
|
+
outcomes.set(lane.lane_id, { error: new LanesError("outpost_lane_hire_summary_missing", `Outpost ${outpostId} did not return lane ${lane.lane_id} in its lane list`, { outpost_id: outpostId, lane_id: lane.lane_id, command_id: dispatched.commandId }) });
|
|
725
|
+
continue;
|
|
726
|
+
}
|
|
727
|
+
if (matchingRows.length !== 1) {
|
|
728
|
+
outcomes.set(lane.lane_id, { error: new LanesError("outpost_lane_hire_summary_invalid", `Outpost ${outpostId} returned duplicate lane rows for ${lane.lane_id}`, { outpost_id: outpostId, lane_id: lane.lane_id, command_id: dispatched.commandId }) });
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
731
|
+
const row = matchingRows[0];
|
|
732
|
+
try {
|
|
733
|
+
outcomes.set(lane.lane_id, { fields: validateOwnerLaneHireSummary({
|
|
734
|
+
outpostId,
|
|
735
|
+
laneId: lane.lane_id,
|
|
736
|
+
rawLane: row,
|
|
737
|
+
hireGrant: row.hire_grant,
|
|
738
|
+
hireAliveChildren: row.hire_alive_children,
|
|
739
|
+
hire: row.hire,
|
|
740
|
+
hireSummary: row.hire_summary,
|
|
741
|
+
commandId: dispatched.commandId,
|
|
742
|
+
}) });
|
|
743
|
+
}
|
|
744
|
+
catch (err) {
|
|
745
|
+
outcomes.set(lane.lane_id, { error: isLanesError(err)
|
|
746
|
+
? err
|
|
747
|
+
: new LanesError("outpost_lane_hire_summary_invalid", `Outpost ${outpostId} returned an invalid hire summary for lane ${lane.lane_id}`, { outpost_id: outpostId, lane_id: lane.lane_id, command_id: dispatched.commandId }) });
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
return outcomes;
|
|
751
|
+
}
|
|
752
|
+
async function batchOwnerLaneHireFields(ctx, lanes) {
|
|
753
|
+
const outcomes = new Map();
|
|
754
|
+
const groups = new Map();
|
|
755
|
+
const nowMs = ctx.now().getTime();
|
|
756
|
+
const cached = readOutpostHireSummaryCache(ctx.hqRoot(), nowMs);
|
|
757
|
+
for (const lane of lanes) {
|
|
758
|
+
if (lane.host.kind !== "outpost")
|
|
759
|
+
continue;
|
|
760
|
+
const cachedSummary = cached.get(outpostHireSummaryCacheKey(lane.host.id, lane.lane_id));
|
|
761
|
+
if (cachedSummary) {
|
|
762
|
+
outcomes.set(lane.lane_id, {
|
|
763
|
+
fields: {
|
|
764
|
+
hire_grant: cachedSummary.hire_grant,
|
|
765
|
+
hire_alive_children: cachedSummary.hire_alive_children,
|
|
766
|
+
hire: cachedSummary.hire,
|
|
767
|
+
hire_summary: cachedSummary.hire_summary,
|
|
768
|
+
},
|
|
769
|
+
});
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
const group = groups.get(lane.host.id) ?? [];
|
|
773
|
+
group.push(lane);
|
|
774
|
+
groups.set(lane.host.id, group);
|
|
775
|
+
}
|
|
776
|
+
const fetchedGroups = await mapWithConcurrency([...groups.entries()], OUTPOST_HIRE_SUMMARY_CONCURRENCY, ([outpostId, group]) => fetchOutpostListHireSummaries(ctx, outpostId, group));
|
|
777
|
+
const cacheUpdates = [];
|
|
778
|
+
for (const groupOutcomes of fetchedGroups) {
|
|
779
|
+
for (const [laneId, outcome] of groupOutcomes) {
|
|
780
|
+
outcomes.set(laneId, outcome);
|
|
781
|
+
if ("fields" in outcome) {
|
|
782
|
+
const lane = lanes.find((candidate) => candidate.lane_id === laneId);
|
|
783
|
+
if (lane?.host.kind === "outpost") {
|
|
784
|
+
cacheUpdates.push({ outpost_id: lane.host.id, lane_id: laneId, ...outcome.fields });
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
writeOutpostHireSummaryCache(ctx.hqRoot(), cacheUpdates, ctx.now().getTime(), cached);
|
|
790
|
+
return outcomes;
|
|
791
|
+
}
|
|
638
792
|
const OUTPOST_HIRE_SUMMARY_TIMEOUT_SECONDS = 20;
|
|
639
793
|
const OUTPOST_HIRE_SUMMARY_WAIT_MS = 15_000;
|
|
640
794
|
const OUTPOST_HIRE_SUMMARY_CONCURRENCY = 4;
|
|
@@ -3681,6 +3835,7 @@ async function runList(ctx, opts) {
|
|
|
3681
3835
|
return true;
|
|
3682
3836
|
})
|
|
3683
3837
|
.sort(compareLaneQueueOrder);
|
|
3838
|
+
const ownerEligibleLaneIds = new Set();
|
|
3684
3839
|
const companyContexts = new Map();
|
|
3685
3840
|
for (const lane of selectedLanes) {
|
|
3686
3841
|
if (lane.host.kind !== "outpost" ||
|
|
@@ -3688,14 +3843,58 @@ async function runList(ctx, opts) {
|
|
|
3688
3843
|
laneRunsHere(lane, host)) {
|
|
3689
3844
|
continue;
|
|
3690
3845
|
}
|
|
3691
|
-
const
|
|
3692
|
-
|
|
3846
|
+
const companyKey = JSON.stringify([
|
|
3847
|
+
lane.company?.uid ?? null,
|
|
3848
|
+
lane.company?.slug ?? null,
|
|
3849
|
+
]);
|
|
3850
|
+
ownerEligibleLaneIds.add(lane.lane_id);
|
|
3851
|
+
companyContexts.set(companyKey, lane.company);
|
|
3852
|
+
}
|
|
3853
|
+
const enabledByCompany = new Map(await Promise.all([...companyContexts.entries()].map(async ([companyKey, company]) => [
|
|
3854
|
+
companyKey,
|
|
3855
|
+
await ctx.laneHireGrantsEnabled(company),
|
|
3856
|
+
])));
|
|
3857
|
+
const ownerLanes = [];
|
|
3858
|
+
const hireByLane = new Map();
|
|
3859
|
+
for (const lane of selectedLanes) {
|
|
3860
|
+
if (!ownerEligibleLaneIds.has(lane.lane_id)) {
|
|
3861
|
+
hireByLane.set(lane.lane_id, {
|
|
3862
|
+
...laneHireFields(lane, allLanes),
|
|
3863
|
+
hire_source: "local",
|
|
3864
|
+
});
|
|
3865
|
+
continue;
|
|
3866
|
+
}
|
|
3867
|
+
const companyKey = JSON.stringify([
|
|
3868
|
+
lane.company?.uid ?? null,
|
|
3869
|
+
lane.company?.slug ?? null,
|
|
3870
|
+
]);
|
|
3871
|
+
if (enabledByCompany.get(companyKey))
|
|
3872
|
+
ownerLanes.push(lane);
|
|
3873
|
+
else {
|
|
3874
|
+
hireByLane.set(lane.lane_id, {
|
|
3875
|
+
...laneHireFields(lane, allLanes),
|
|
3876
|
+
hire_source: "local",
|
|
3877
|
+
});
|
|
3878
|
+
}
|
|
3879
|
+
}
|
|
3880
|
+
if (ownerLanes.length > 0) {
|
|
3881
|
+
const ownerSummaries = await batchOwnerLaneHireFields(ctx, ownerLanes);
|
|
3882
|
+
for (const lane of ownerLanes) {
|
|
3883
|
+
const outcome = ownerSummaries.get(lane.lane_id);
|
|
3884
|
+
if (outcome && "fields" in outcome) {
|
|
3885
|
+
hireByLane.set(lane.lane_id, { ...outcome.fields, hire_source: "owner" });
|
|
3886
|
+
continue;
|
|
3887
|
+
}
|
|
3888
|
+
const error = outcome && "error" in outcome
|
|
3889
|
+
? outcome.error
|
|
3890
|
+
: new LanesError("outpost_lane_hire_summary_missing", `No owner hire summary was returned for lane ${lane.lane_id}`, { lane_id: lane.lane_id, outpost_id: lane.host.id });
|
|
3891
|
+
hireByLane.set(lane.lane_id, {
|
|
3892
|
+
...laneHireFields(lane, allLanes),
|
|
3893
|
+
hire_source: "local",
|
|
3894
|
+
hire_warning: `${error.error}: ${error.message}`,
|
|
3895
|
+
});
|
|
3896
|
+
}
|
|
3693
3897
|
}
|
|
3694
|
-
await Promise.all([...companyContexts.values()].map((company) => ctx.laneHireGrantsEnabled(company)));
|
|
3695
|
-
const hireByLane = new Map(await mapWithConcurrency(selectedLanes, OUTPOST_HIRE_SUMMARY_CONCURRENCY, async (lane) => [
|
|
3696
|
-
lane.lane_id,
|
|
3697
|
-
await laneHirePresentation(ctx, lane, allLanes),
|
|
3698
|
-
]));
|
|
3699
3898
|
const lanes = selectedLanes.map((lane) => {
|
|
3700
3899
|
const remoteQueued = lane.state === "queued" &&
|
|
3701
3900
|
lane.host.kind === "outpost" &&
|
|
@@ -6809,15 +7008,17 @@ async function loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, companyFilter) {
|
|
|
6809
7008
|
const eligibleGroups = [];
|
|
6810
7009
|
for (const [outpostId, lanes] of [...lanesByOutpost.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
6811
7010
|
for (const group of groupQuestionLanesByCompany(lanes)) {
|
|
6812
|
-
if (companyFilter && !questionCompanyRefMatches(group.company, companyFilter))
|
|
6813
|
-
continue;
|
|
6814
7011
|
if (group.company === null) {
|
|
6815
|
-
|
|
6816
|
-
|
|
6817
|
-
|
|
6818
|
-
|
|
7012
|
+
if (await ctx.laneOutpostQuestionVisibilityEnabled(null)) {
|
|
7013
|
+
errors.push({
|
|
7014
|
+
host: `outpost:${outpostId}`,
|
|
7015
|
+
error: "outpost_question_company_unavailable",
|
|
7016
|
+
});
|
|
7017
|
+
}
|
|
6819
7018
|
continue;
|
|
6820
7019
|
}
|
|
7020
|
+
if (companyFilter && !questionCompanyRefMatches(group.company, companyFilter))
|
|
7021
|
+
continue;
|
|
6821
7022
|
if (await ctx.laneOutpostQuestionVisibilityEnabled(group.company)) {
|
|
6822
7023
|
eligibleGroups.push({ outpostId, group });
|
|
6823
7024
|
}
|
|
@@ -72,8 +72,10 @@ export declare function parseOutpostPickupPayload(raw: unknown): OutpostPickupPa
|
|
|
72
72
|
export declare function buildOutpostPickupCommand(payloadUrl: string): string;
|
|
73
73
|
/** Bounded, read-only capacity query executed by the target Outpost CLI. */
|
|
74
74
|
export declare function buildOutpostCapacityCommand(): string;
|
|
75
|
-
/** Read
|
|
76
|
-
export declare function buildOutpostLaneListCommand(
|
|
75
|
+
/** Read every lane on an Outpost, optionally using its configured HQ binary. */
|
|
76
|
+
export declare function buildOutpostLaneListCommand(input?: {
|
|
77
|
+
hqBinary?: string;
|
|
78
|
+
}): string;
|
|
77
79
|
/** Read questions on one Outpost, scoped to a company when supplied. */
|
|
78
80
|
export declare function buildOutpostQuestionListCommand(input?: {
|
|
79
81
|
company?: string;
|
|
@@ -65,9 +65,14 @@ export function buildOutpostPickupCommand(payloadUrl) {
|
|
|
65
65
|
export function buildOutpostCapacityCommand() {
|
|
66
66
|
return withRemoteHqDir(joinCommandParts(["hq", "lanes", "capacity", "--json"]));
|
|
67
67
|
}
|
|
68
|
-
/** Read
|
|
69
|
-
export function buildOutpostLaneListCommand() {
|
|
70
|
-
return withRemoteHqDir(joinCommandParts([
|
|
68
|
+
/** Read every lane on an Outpost, optionally using its configured HQ binary. */
|
|
69
|
+
export function buildOutpostLaneListCommand(input = {}) {
|
|
70
|
+
return withRemoteHqDir(joinCommandParts([
|
|
71
|
+
input.hqBinary?.trim() || "hq",
|
|
72
|
+
"lanes",
|
|
73
|
+
"list",
|
|
74
|
+
"--json",
|
|
75
|
+
]));
|
|
71
76
|
}
|
|
72
77
|
/** Read questions on one Outpost, scoped to a company when supplied. */
|
|
73
78
|
export function buildOutpostQuestionListCommand(input = {}) {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare const OUTPOST_HIRE_SUMMARY_CACHE_TTL_MS = 60000;
|
|
2
|
+
export interface OutpostHireSummary {
|
|
3
|
+
hire_grant: number;
|
|
4
|
+
hire_alive_children: number;
|
|
5
|
+
hire: string;
|
|
6
|
+
hire_summary: string;
|
|
7
|
+
}
|
|
8
|
+
export interface OutpostHireSummaryCacheEntry extends OutpostHireSummary {
|
|
9
|
+
outpost_id: string;
|
|
10
|
+
lane_id: string;
|
|
11
|
+
cached_at: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function outpostHireSummaryCachePath(hqRoot: string): string;
|
|
14
|
+
export declare function outpostHireSummaryCacheKey(outpostId: string, laneId: string): string;
|
|
15
|
+
export declare function readOutpostHireSummaryCache(hqRoot: string, nowMs: number): Map<string, OutpostHireSummaryCacheEntry>;
|
|
16
|
+
export declare function writeOutpostHireSummaryCache(hqRoot: string, updates: readonly (OutpostHireSummary & {
|
|
17
|
+
outpost_id: string;
|
|
18
|
+
lane_id: string;
|
|
19
|
+
})[], nowMs: number, existingEntries?: ReadonlyMap<string, OutpostHireSummaryCacheEntry>): void;
|
|
20
|
+
//# sourceMappingURL=outpost-hire-cache.d.ts.map
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
export const OUTPOST_HIRE_SUMMARY_CACHE_TTL_MS = 60_000;
|
|
5
|
+
const CACHE_RELATIVE_PATH = path.join("workspace", "lanes-runs", ".cache", "outpost-hire-summaries.json");
|
|
6
|
+
export function outpostHireSummaryCachePath(hqRoot) {
|
|
7
|
+
return path.join(hqRoot, CACHE_RELATIVE_PATH);
|
|
8
|
+
}
|
|
9
|
+
export function outpostHireSummaryCacheKey(outpostId, laneId) {
|
|
10
|
+
return JSON.stringify([outpostId, laneId]);
|
|
11
|
+
}
|
|
12
|
+
export function readOutpostHireSummaryCache(hqRoot, nowMs) {
|
|
13
|
+
const filePath = outpostHireSummaryCachePath(hqRoot);
|
|
14
|
+
let contents;
|
|
15
|
+
try {
|
|
16
|
+
contents = fs.readFileSync(filePath, "utf8");
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
if (isMissingFile(err))
|
|
20
|
+
return new Map();
|
|
21
|
+
console.warn(`Ignoring unreadable Outpost hire summary cache at ${filePath}: ${errorText(err)}`);
|
|
22
|
+
return new Map();
|
|
23
|
+
}
|
|
24
|
+
let parsed;
|
|
25
|
+
try {
|
|
26
|
+
parsed = JSON.parse(contents);
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
console.warn(`Ignoring malformed Outpost hire summary cache at ${filePath}: ${errorText(err)}`);
|
|
30
|
+
return new Map();
|
|
31
|
+
}
|
|
32
|
+
if (!Array.isArray(parsed)) {
|
|
33
|
+
console.warn(`Ignoring malformed Outpost hire summary cache at ${filePath}: expected an array`);
|
|
34
|
+
return new Map();
|
|
35
|
+
}
|
|
36
|
+
const entries = new Map();
|
|
37
|
+
const duplicates = new Set();
|
|
38
|
+
for (const value of parsed) {
|
|
39
|
+
const entry = parseCacheEntry(value, nowMs);
|
|
40
|
+
if (!entry)
|
|
41
|
+
continue;
|
|
42
|
+
const key = outpostHireSummaryCacheKey(entry.outpost_id, entry.lane_id);
|
|
43
|
+
if (entries.has(key)) {
|
|
44
|
+
entries.delete(key);
|
|
45
|
+
duplicates.add(key);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (!duplicates.has(key))
|
|
49
|
+
entries.set(key, entry);
|
|
50
|
+
}
|
|
51
|
+
return entries;
|
|
52
|
+
}
|
|
53
|
+
export function writeOutpostHireSummaryCache(hqRoot, updates, nowMs, existingEntries) {
|
|
54
|
+
if (updates.length === 0)
|
|
55
|
+
return;
|
|
56
|
+
const filePath = outpostHireSummaryCachePath(hqRoot);
|
|
57
|
+
const cached = new Map(existingEntries ?? readOutpostHireSummaryCache(hqRoot, nowMs));
|
|
58
|
+
for (const update of updates) {
|
|
59
|
+
const entry = {
|
|
60
|
+
outpost_id: update.outpost_id,
|
|
61
|
+
lane_id: update.lane_id,
|
|
62
|
+
hire_grant: update.hire_grant,
|
|
63
|
+
hire_alive_children: update.hire_alive_children,
|
|
64
|
+
hire: update.hire,
|
|
65
|
+
hire_summary: update.hire_summary,
|
|
66
|
+
cached_at: new Date(nowMs).toISOString(),
|
|
67
|
+
};
|
|
68
|
+
if (!parseCacheEntry(entry, nowMs))
|
|
69
|
+
continue;
|
|
70
|
+
cached.set(outpostHireSummaryCacheKey(entry.outpost_id, entry.lane_id), entry);
|
|
71
|
+
}
|
|
72
|
+
const directory = path.dirname(filePath);
|
|
73
|
+
let temporaryPath;
|
|
74
|
+
try {
|
|
75
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
76
|
+
temporaryPath = path.join(directory, `.outpost-hire-summaries.${process.pid}.${randomBytes(6).toString("hex")}.tmp`);
|
|
77
|
+
fs.writeFileSync(temporaryPath, `${JSON.stringify([...cached.values()])}\n`, {
|
|
78
|
+
encoding: "utf8",
|
|
79
|
+
mode: 0o600,
|
|
80
|
+
});
|
|
81
|
+
fs.renameSync(temporaryPath, filePath);
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
if (temporaryPath) {
|
|
85
|
+
try {
|
|
86
|
+
fs.unlinkSync(temporaryPath);
|
|
87
|
+
}
|
|
88
|
+
catch (cleanupErr) {
|
|
89
|
+
if (!isMissingFile(cleanupErr)) {
|
|
90
|
+
console.warn(`Could not remove temporary Outpost hire summary cache ${temporaryPath}: ${errorText(cleanupErr)}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
console.warn(`Could not write Outpost hire summary cache at ${filePath}: ${errorText(err)}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function parseCacheEntry(value, nowMs) {
|
|
98
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
99
|
+
return undefined;
|
|
100
|
+
const entry = value;
|
|
101
|
+
const allowedKeys = new Set([
|
|
102
|
+
"outpost_id",
|
|
103
|
+
"lane_id",
|
|
104
|
+
"hire_grant",
|
|
105
|
+
"hire_alive_children",
|
|
106
|
+
"hire",
|
|
107
|
+
"hire_summary",
|
|
108
|
+
"cached_at",
|
|
109
|
+
]);
|
|
110
|
+
if (Object.keys(entry).length !== allowedKeys.size || Object.keys(entry).some((key) => !allowedKeys.has(key))) {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
if (typeof entry.outpost_id !== "string" || !entry.outpost_id.trim() ||
|
|
114
|
+
typeof entry.lane_id !== "string" || !entry.lane_id.trim() ||
|
|
115
|
+
!Number.isSafeInteger(entry.hire_grant) || entry.hire_grant < 0 ||
|
|
116
|
+
!Number.isSafeInteger(entry.hire_alive_children) || entry.hire_alive_children < 0 ||
|
|
117
|
+
typeof entry.cached_at !== "string") {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
const cachedAt = Date.parse(entry.cached_at);
|
|
121
|
+
if (!Number.isFinite(cachedAt) ||
|
|
122
|
+
cachedAt > nowMs ||
|
|
123
|
+
nowMs - cachedAt > OUTPOST_HIRE_SUMMARY_CACHE_TTL_MS) {
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
const hire = `${entry.hire_alive_children}/${entry.hire_grant}`;
|
|
127
|
+
const hireSummary = `hire ${hire}`;
|
|
128
|
+
if (entry.hire !== hire || entry.hire_summary !== hireSummary)
|
|
129
|
+
return undefined;
|
|
130
|
+
return {
|
|
131
|
+
outpost_id: entry.outpost_id,
|
|
132
|
+
lane_id: entry.lane_id,
|
|
133
|
+
hire_grant: entry.hire_grant,
|
|
134
|
+
hire_alive_children: entry.hire_alive_children,
|
|
135
|
+
hire,
|
|
136
|
+
hire_summary: hireSummary,
|
|
137
|
+
cached_at: entry.cached_at,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function isMissingFile(err) {
|
|
141
|
+
return err !== null && typeof err === "object" && "code" in err && err.code === "ENOENT";
|
|
142
|
+
}
|
|
143
|
+
function errorText(err) {
|
|
144
|
+
return err instanceof Error ? err.message : String(err);
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=outpost-hire-cache.js.map
|