@indigoai-us/hq-cli 5.237.0 → 5.239.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/dist/command-catalog.generated.d.ts +30 -0
- package/dist/command-catalog.generated.js +39 -0
- package/dist/commands/lanes-checks.d.ts +2 -0
- package/dist/commands/lanes-checks.js +129 -11
- package/dist/commands/lanes.js +63 -27
- package/dist/lib/checkpoint-ontology/apply.js +26 -1
- package/dist/lib/checkpoint-ontology/plan.d.ts +12 -0
- package/dist/lib/checkpoint-ontology/plan.js +12 -0
- package/dist/lib/lanes/orphans.d.ts +18 -0
- package/dist/lib/lanes/orphans.js +67 -0
- package/dist/lib/lanes/store.js +23 -0
- package/dist/lib/lanes/story-ledger.js +4 -0
- package/dist/lib/lanes/tracker-checks.d.ts +1 -1
- package/dist/lib/lanes/types.d.ts +6 -0
- package/dist/lib/lanes/types.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.239.0] — 2026-09-27
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `hq core checkpoint` can skip a knowledge link to a concept entity whose
|
|
10
|
+
name is also the name of an entity of another type, such as a concept
|
|
11
|
+
`hq-secrets` beside a project `HQ Secrets`. The report lists the skipped
|
|
12
|
+
link as rejected with reason `ambiguous_type`. Signal and session inputs,
|
|
13
|
+
other links and new entities are unchanged. Behind the hq-flags flag
|
|
14
|
+
`checkpoint.ontology.skip-ambiguous-concept-links`, off by default.
|
|
15
|
+
|
|
16
|
+
## [5.238.0] — 2026-09-27
|
|
17
|
+
|
|
18
|
+
### Added: Lane orphan reporting
|
|
19
|
+
|
|
20
|
+
- `hq lanes checks` includes an orphaned-lane check. The company-scoped
|
|
21
|
+
`hq lanes orphans` report lists candidates without changing their state. Add
|
|
22
|
+
`--stop --reason <text>` to stop candidates that remain orphaned on recheck
|
|
23
|
+
and record the reason in lane history.
|
|
24
|
+
|
|
5
25
|
## [5.237.0] — 2026-09-27
|
|
6
26
|
|
|
7
27
|
### Added
|
|
@@ -7300,6 +7300,33 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
7300
7300
|
}, {
|
|
7301
7301
|
readonly flags: "--project <slug>";
|
|
7302
7302
|
readonly description: "Limit checks to one company project";
|
|
7303
|
+
}, {
|
|
7304
|
+
readonly flags: "--min-age <minutes>";
|
|
7305
|
+
readonly description: "Minimum orphan lane age in minutes (default: 30)";
|
|
7306
|
+
}, {
|
|
7307
|
+
readonly flags: "--json";
|
|
7308
|
+
readonly description: "JSON on stdout (always)";
|
|
7309
|
+
}];
|
|
7310
|
+
readonly subcommands: readonly [];
|
|
7311
|
+
}, {
|
|
7312
|
+
readonly name: "orphans";
|
|
7313
|
+
readonly description: "Report lanes whose lane or session senior has ended";
|
|
7314
|
+
readonly aliases: readonly [];
|
|
7315
|
+
readonly hidden: false;
|
|
7316
|
+
readonly usage: "[options]";
|
|
7317
|
+
readonly arguments: readonly [];
|
|
7318
|
+
readonly options: readonly [{
|
|
7319
|
+
readonly flags: "--company <slug>";
|
|
7320
|
+
readonly description: "Select the company by slug";
|
|
7321
|
+
}, {
|
|
7322
|
+
readonly flags: "--min-age <minutes>";
|
|
7323
|
+
readonly description: "Minimum orphan lane age in minutes (default: 30)";
|
|
7324
|
+
}, {
|
|
7325
|
+
readonly flags: "--stop";
|
|
7326
|
+
readonly description: "Stop the reported lanes through their normal owner path";
|
|
7327
|
+
}, {
|
|
7328
|
+
readonly flags: "--reason <text>";
|
|
7329
|
+
readonly description: "Required audit reason when --stop is set";
|
|
7303
7330
|
}, {
|
|
7304
7331
|
readonly flags: "--json";
|
|
7305
7332
|
readonly description: "JSON on stdout (always)";
|
|
@@ -8159,6 +8186,9 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
8159
8186
|
readonly variadic: false;
|
|
8160
8187
|
}];
|
|
8161
8188
|
readonly options: readonly [{
|
|
8189
|
+
readonly flags: "--reason <text>";
|
|
8190
|
+
readonly description: "Record the reason in the lane's terminal history";
|
|
8191
|
+
}, {
|
|
8162
8192
|
readonly flags: "--json";
|
|
8163
8193
|
readonly description: "JSON on stdout (always)";
|
|
8164
8194
|
}];
|
|
@@ -9436,6 +9436,41 @@ export const COMMAND_CATALOG = [
|
|
|
9436
9436
|
"flags": "--project <slug>",
|
|
9437
9437
|
"description": "Limit checks to one company project"
|
|
9438
9438
|
},
|
|
9439
|
+
{
|
|
9440
|
+
"flags": "--min-age <minutes>",
|
|
9441
|
+
"description": "Minimum orphan lane age in minutes (default: 30)"
|
|
9442
|
+
},
|
|
9443
|
+
{
|
|
9444
|
+
"flags": "--json",
|
|
9445
|
+
"description": "JSON on stdout (always)"
|
|
9446
|
+
}
|
|
9447
|
+
],
|
|
9448
|
+
"subcommands": []
|
|
9449
|
+
},
|
|
9450
|
+
{
|
|
9451
|
+
"name": "orphans",
|
|
9452
|
+
"description": "Report lanes whose lane or session senior has ended",
|
|
9453
|
+
"aliases": [],
|
|
9454
|
+
"hidden": false,
|
|
9455
|
+
"usage": "[options]",
|
|
9456
|
+
"arguments": [],
|
|
9457
|
+
"options": [
|
|
9458
|
+
{
|
|
9459
|
+
"flags": "--company <slug>",
|
|
9460
|
+
"description": "Select the company by slug"
|
|
9461
|
+
},
|
|
9462
|
+
{
|
|
9463
|
+
"flags": "--min-age <minutes>",
|
|
9464
|
+
"description": "Minimum orphan lane age in minutes (default: 30)"
|
|
9465
|
+
},
|
|
9466
|
+
{
|
|
9467
|
+
"flags": "--stop",
|
|
9468
|
+
"description": "Stop the reported lanes through their normal owner path"
|
|
9469
|
+
},
|
|
9470
|
+
{
|
|
9471
|
+
"flags": "--reason <text>",
|
|
9472
|
+
"description": "Required audit reason when --stop is set"
|
|
9473
|
+
},
|
|
9439
9474
|
{
|
|
9440
9475
|
"flags": "--json",
|
|
9441
9476
|
"description": "JSON on stdout (always)"
|
|
@@ -10555,6 +10590,10 @@ export const COMMAND_CATALOG = [
|
|
|
10555
10590
|
}
|
|
10556
10591
|
],
|
|
10557
10592
|
"options": [
|
|
10593
|
+
{
|
|
10594
|
+
"flags": "--reason <text>",
|
|
10595
|
+
"description": "Record the reason in the lane's terminal history"
|
|
10596
|
+
},
|
|
10558
10597
|
{
|
|
10559
10598
|
"flags": "--json",
|
|
10560
10599
|
"description": "JSON on stdout (always)"
|
|
@@ -6,6 +6,8 @@ export interface LaneChecksDeps {
|
|
|
6
6
|
resolveCompany: (company?: string) => Promise<LaneCompanyRef>;
|
|
7
7
|
trackerEnabled: (company: LaneCompanyRef) => boolean | Promise<boolean>;
|
|
8
8
|
now: () => Date;
|
|
9
|
+
stopLane?: (laneId: string, reason: string) => Promise<void>;
|
|
10
|
+
sessionState?: (root: string, sessionId: string, now: Date) => string | null;
|
|
9
11
|
}
|
|
10
12
|
type WrapHandler = <Args extends unknown[]>(action: (...args: Args) => Promise<void>) => (...args: Args) => Promise<void>;
|
|
11
13
|
export interface ProjectMapping {
|
|
@@ -6,6 +6,9 @@ import { LanesError, isLanesError } from "../lib/lanes/errors.js";
|
|
|
6
6
|
import { evaluateTrackerChecks } from "../lib/lanes/tracker-checks.js";
|
|
7
7
|
import { LANE_STORE_UPGRADE_COMMAND, isLaneStoreVersionNewer, laneRecordPath, listLaneIds, listQuestionIds, questionRecordPath, readLane, readQuestion } from "../lib/lanes/store.js";
|
|
8
8
|
import { createLocalTrackerStore } from "../lib/lanes/tracker-store.js";
|
|
9
|
+
import { sessionLiveness } from "../lib/lanes/liveness.js";
|
|
10
|
+
import { findOrphanLanes, parseOrphanMinAge } from "../lib/lanes/orphans.js";
|
|
11
|
+
import { assertNoSecretText } from "../lib/lanes/text-scan.js";
|
|
9
12
|
import { LANES_TRACKER_FLAG } from "./lanes-tracker.js";
|
|
10
13
|
function fail(error, message, details = {}) {
|
|
11
14
|
throw new LanesError(error, message, details);
|
|
@@ -320,6 +323,47 @@ function readScopedLanes(root, company, projects) {
|
|
|
320
323
|
return [readLane(root, id)];
|
|
321
324
|
});
|
|
322
325
|
}
|
|
326
|
+
function readCompanyLanes(root, company, projectId) {
|
|
327
|
+
return listLaneIds(root).flatMap((id) => {
|
|
328
|
+
const file = laneRecordPath(root, id);
|
|
329
|
+
const metadata = readJsonObject(file);
|
|
330
|
+
if (metadata === null)
|
|
331
|
+
return fail("tracker_checks_input_unreadable", "Lane record could not be read while checking its scope", { path: file });
|
|
332
|
+
const laneCompany = isObject(metadata.company) ? metadata.company : null;
|
|
333
|
+
if (laneCompany?.slug !== company.slug || laneCompany.uid !== company.uid)
|
|
334
|
+
return [];
|
|
335
|
+
if (projectId !== undefined && !laneMetadataMatchesScope(metadata, company, new Set([projectId])))
|
|
336
|
+
return [];
|
|
337
|
+
return [readLane(root, id)];
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
function orphanSessionState(root, sessionId, now, deps) {
|
|
341
|
+
if (deps.sessionState)
|
|
342
|
+
return deps.sessionState(root, sessionId, now);
|
|
343
|
+
return sessionLiveness(root, sessionId, { nowMs: now.getTime() });
|
|
344
|
+
}
|
|
345
|
+
function orphanFindings(root, company, now, minAgeMinutes, deps, projectId) {
|
|
346
|
+
const candidates = readCompanyLanes(root, company, projectId);
|
|
347
|
+
const lanes = [...candidates];
|
|
348
|
+
const known = new Set(lanes.map((lane) => lane.lane_id));
|
|
349
|
+
for (const candidate of candidates) {
|
|
350
|
+
if (candidate.senior.kind !== "lane" || candidate.senior.id === undefined || known.has(candidate.senior.id))
|
|
351
|
+
continue;
|
|
352
|
+
const file = laneRecordPath(root, candidate.senior.id);
|
|
353
|
+
const metadata = readJsonObject(file, "empty");
|
|
354
|
+
const laneCompany = metadata && isObject(metadata.company) ? metadata.company : null;
|
|
355
|
+
if (laneCompany?.slug !== company.slug || laneCompany.uid !== company.uid)
|
|
356
|
+
continue;
|
|
357
|
+
lanes.push(readLane(root, candidate.senior.id));
|
|
358
|
+
known.add(candidate.senior.id);
|
|
359
|
+
}
|
|
360
|
+
return findOrphanLanes({
|
|
361
|
+
lanes,
|
|
362
|
+
now,
|
|
363
|
+
minAgeMinutes,
|
|
364
|
+
sessionState: (sessionId, at) => orphanSessionState(root, sessionId, at, deps),
|
|
365
|
+
}).filter((finding) => candidates.some((lane) => lane.lane_id === finding.lane_id));
|
|
366
|
+
}
|
|
323
367
|
function sameCompanyLane(lane, company) {
|
|
324
368
|
return lane.company?.slug === company.slug && lane.company.uid === company.uid;
|
|
325
369
|
}
|
|
@@ -573,7 +617,7 @@ export async function resolveTrackerCompany(deps, requested) {
|
|
|
573
617
|
}
|
|
574
618
|
return company;
|
|
575
619
|
}
|
|
576
|
-
async function checkReport(root, company, projects, directiveProjects, projectFilter, now) {
|
|
620
|
+
async function checkReport(root, company, projects, directiveProjects, projectFilter, minAgeMinutes, now, deps) {
|
|
577
621
|
const selectedProjects = [...projects.values()];
|
|
578
622
|
const laneRecords = readScopedLanes(root, company, selectedProjects);
|
|
579
623
|
const warnings = laneRecords.flatMap((lane) => {
|
|
@@ -602,39 +646,113 @@ async function checkReport(root, company, projects, directiveProjects, projectFi
|
|
|
602
646
|
team: project.team,
|
|
603
647
|
...(project.leadLaneId === undefined ? {} : { lead_lane_id: project.leadLaneId }),
|
|
604
648
|
}));
|
|
649
|
+
const checks = evaluateTrackerChecks({
|
|
650
|
+
now,
|
|
651
|
+
thresholds: loadLanesConfig(root).tracker.checks,
|
|
652
|
+
questions: questionRecords,
|
|
653
|
+
teams,
|
|
654
|
+
...trackerRecords,
|
|
655
|
+
directives: trackerRecords.directives,
|
|
656
|
+
directive_teams: directiveTeams,
|
|
657
|
+
});
|
|
658
|
+
const orphanedLanes = orphanFindings(root, company, now, minAgeMinutes, deps, projectFilter);
|
|
659
|
+
checks.push({
|
|
660
|
+
id: "orphaned_lane",
|
|
661
|
+
result: orphanedLanes.length === 0 ? "ok" : "failing",
|
|
662
|
+
items: orphanedLanes.map((finding) => ({
|
|
663
|
+
id: finding.lane_id,
|
|
664
|
+
reason: finding.senior_state === "missing" ? "missing_senior" :
|
|
665
|
+
finding.senior_state === "ended" ? "ended_session_senior" : "terminal_senior",
|
|
666
|
+
senior: finding.senior,
|
|
667
|
+
senior_state: finding.senior_state,
|
|
668
|
+
age_minutes: finding.age_minutes,
|
|
669
|
+
account: finding.account,
|
|
670
|
+
host: finding.host,
|
|
671
|
+
})),
|
|
672
|
+
});
|
|
605
673
|
return {
|
|
606
674
|
ok: true,
|
|
607
675
|
company: company.slug,
|
|
608
676
|
generated_at: now.toISOString(),
|
|
609
677
|
...(warnings.length === 0 ? {} : { warnings }),
|
|
610
|
-
checks
|
|
611
|
-
now,
|
|
612
|
-
thresholds: loadLanesConfig(root).tracker.checks,
|
|
613
|
-
questions: questionRecords,
|
|
614
|
-
teams,
|
|
615
|
-
...trackerRecords,
|
|
616
|
-
directives: trackerRecords.directives,
|
|
617
|
-
directive_teams: directiveTeams,
|
|
618
|
-
}),
|
|
678
|
+
checks,
|
|
619
679
|
};
|
|
620
680
|
}
|
|
681
|
+
async function orphanReport(root, company, now, minAgeMinutes, deps, options) {
|
|
682
|
+
const reason = options.reason?.trim();
|
|
683
|
+
if (options.stop && !reason) {
|
|
684
|
+
fail("orphan_stop_reason_required", "--stop requires a non-empty --reason", {});
|
|
685
|
+
}
|
|
686
|
+
if (!options.stop && options.reason !== undefined) {
|
|
687
|
+
fail("orphan_reason_requires_stop", "--reason can only be used with --stop", {});
|
|
688
|
+
}
|
|
689
|
+
if (reason !== undefined)
|
|
690
|
+
assertNoSecretText("stop reason", reason);
|
|
691
|
+
const orphans = orphanFindings(root, company, now, minAgeMinutes, deps);
|
|
692
|
+
if (!options.stop) {
|
|
693
|
+
console.log(JSON.stringify({
|
|
694
|
+
ok: true,
|
|
695
|
+
action: "orphans",
|
|
696
|
+
company: company.slug,
|
|
697
|
+
generated_at: now.toISOString(),
|
|
698
|
+
min_age_minutes: minAgeMinutes,
|
|
699
|
+
orphans,
|
|
700
|
+
}, null, 2));
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
if (!deps.stopLane)
|
|
704
|
+
fail("orphan_stop_unavailable", "The normal lane stop path is unavailable", {});
|
|
705
|
+
const stopped = [];
|
|
706
|
+
for (const finding of orphans) {
|
|
707
|
+
const fresh = orphanFindings(root, company, deps.now(), minAgeMinutes, deps)
|
|
708
|
+
.find((candidate) => candidate.lane_id === finding.lane_id);
|
|
709
|
+
if (!fresh)
|
|
710
|
+
continue;
|
|
711
|
+
await deps.stopLane(finding.lane_id, reason);
|
|
712
|
+
stopped.push({ lane_id: finding.lane_id, state: readLane(root, finding.lane_id).state });
|
|
713
|
+
}
|
|
714
|
+
console.log(JSON.stringify({
|
|
715
|
+
ok: true,
|
|
716
|
+
action: "orphans.stop",
|
|
717
|
+
company: company.slug,
|
|
718
|
+
generated_at: now.toISOString(),
|
|
719
|
+
min_age_minutes: minAgeMinutes,
|
|
720
|
+
reason,
|
|
721
|
+
orphans,
|
|
722
|
+
stopped,
|
|
723
|
+
}, null, 2));
|
|
724
|
+
}
|
|
621
725
|
export function registerLaneChecksCommand(parent, deps, wrap) {
|
|
622
726
|
parent.command("checks")
|
|
623
727
|
.description("Evaluate tracker and lane health checks")
|
|
624
728
|
.option("--company <slug>", "Select the company by slug")
|
|
625
729
|
.option("--project <slug>", "Limit checks to one company project")
|
|
730
|
+
.option("--min-age <minutes>", "Minimum orphan lane age in minutes (default: 30)")
|
|
626
731
|
.action(wrap(async (options) => {
|
|
627
732
|
const company = await resolveTrackerCompany(deps, options.company);
|
|
628
733
|
const root = deps.hqRoot();
|
|
629
734
|
const projectFilter = options.project === undefined ? undefined : validSegment(options.project, "project");
|
|
735
|
+
const minAgeMinutes = parseOrphanMinAge(options.minAge);
|
|
630
736
|
const projects = loadProjectMappings(root, company.slug, projectFilter);
|
|
631
737
|
const directiveProjects = projectFilter === undefined
|
|
632
738
|
? projects
|
|
633
739
|
: loadProjectMappings(root, company.slug);
|
|
634
|
-
const report = await checkReport(root, company, projects, directiveProjects, projectFilter, deps.now());
|
|
740
|
+
const report = await checkReport(root, company, projects, directiveProjects, projectFilter, minAgeMinutes, deps.now(), deps);
|
|
635
741
|
console.log(JSON.stringify(report, null, 2));
|
|
636
742
|
if (report.checks.some((item) => item.result === "failing"))
|
|
637
743
|
process.exitCode = 2;
|
|
638
744
|
}));
|
|
745
|
+
parent.command("orphans")
|
|
746
|
+
.description("Report lanes whose lane or session senior has ended")
|
|
747
|
+
.option("--company <slug>", "Select the company by slug")
|
|
748
|
+
.option("--min-age <minutes>", "Minimum orphan lane age in minutes (default: 30)")
|
|
749
|
+
.option("--stop", "Stop the reported lanes through their normal owner path")
|
|
750
|
+
.option("--reason <text>", "Required audit reason when --stop is set")
|
|
751
|
+
.action(wrap(async (options) => {
|
|
752
|
+
const company = await deps.resolveCompany(options.company);
|
|
753
|
+
const root = deps.hqRoot();
|
|
754
|
+
const minAgeMinutes = parseOrphanMinAge(options.minAge);
|
|
755
|
+
await orphanReport(root, company, deps.now(), minAgeMinutes, deps, options);
|
|
756
|
+
}));
|
|
639
757
|
}
|
|
640
758
|
//# sourceMappingURL=lanes-checks.js.map
|
package/dist/commands/lanes.js
CHANGED
|
@@ -3462,6 +3462,10 @@ function outpostRejectedFailedResumeOption(result) {
|
|
|
3462
3462
|
return result.exitCode !== 0 &&
|
|
3463
3463
|
/unknown option\s+(?:['"])?--(?:from-failed|failed-resume-context)(?:['"])?(?:\s|$)/i.test(`${result.stderr}\n${result.stdout}`);
|
|
3464
3464
|
}
|
|
3465
|
+
function outpostRejectedStopReasonOption(result) {
|
|
3466
|
+
return result.exitCode !== 0 &&
|
|
3467
|
+
/unknown option\s+(?:['"])?--reason(?:['"])?(?:\s|$)/i.test(`${result.stderr}\n${result.stdout}`);
|
|
3468
|
+
}
|
|
3465
3469
|
function verifyOutpostProfileContent(hqRoot, lane) {
|
|
3466
3470
|
if (lane.host.kind !== "outpost" || lane.worker_profile === undefined)
|
|
3467
3471
|
return;
|
|
@@ -7635,7 +7639,7 @@ async function readOutpostTerminalLane(ctx, laneId, outpostId, stopCommandId) {
|
|
|
7635
7639
|
commandId,
|
|
7636
7640
|
};
|
|
7637
7641
|
}
|
|
7638
|
-
async function dispatchOutpostLaneStop(ctx, lane) {
|
|
7642
|
+
async function dispatchOutpostLaneStop(ctx, lane, reason, quiet = false) {
|
|
7639
7643
|
if (lane.host.kind !== "outpost") {
|
|
7640
7644
|
throw new LanesError("outpost_dispatch_failed", `Lane ${lane.lane_id} has no Outpost host to dispatch stop to`, { lane_id: lane.lane_id });
|
|
7641
7645
|
}
|
|
@@ -7644,7 +7648,7 @@ async function dispatchOutpostLaneStop(ctx, lane) {
|
|
|
7644
7648
|
try {
|
|
7645
7649
|
dispatched = await ctx.dispatchOutpostExec({
|
|
7646
7650
|
outpostId,
|
|
7647
|
-
command: buildOutpostLaneCommand({ action: "stop", laneId: lane.lane_id }),
|
|
7651
|
+
command: buildOutpostLaneCommand({ action: "stop", laneId: lane.lane_id, reason }),
|
|
7648
7652
|
timeoutSeconds: 120,
|
|
7649
7653
|
waitMs: 120_000,
|
|
7650
7654
|
});
|
|
@@ -7652,6 +7656,13 @@ async function dispatchOutpostLaneStop(ctx, lane) {
|
|
|
7652
7656
|
catch (err) {
|
|
7653
7657
|
throw new LanesError("outpost_dispatch_failed", `Could not dispatch stop for lane ${lane.lane_id} to Outpost ${outpostId}: ${dispatchErrorMessage(err)}`, { lane_id: lane.lane_id, outpost_id: outpostId });
|
|
7654
7658
|
}
|
|
7659
|
+
if (reason !== undefined && outpostRejectedStopReasonOption(dispatched)) {
|
|
7660
|
+
throw new LanesError("outpost_cli_upgrade_required", `Outpost ${outpostId} needs an hq-cli release containing stop reason support before it can stop lane ${lane.lane_id}`, {
|
|
7661
|
+
lane_id: lane.lane_id,
|
|
7662
|
+
outpost_id: outpostId,
|
|
7663
|
+
required_option: "--reason",
|
|
7664
|
+
});
|
|
7665
|
+
}
|
|
7655
7666
|
const ownerError = ownerErrorFromOutput(dispatched.stdout);
|
|
7656
7667
|
if (ownerError) {
|
|
7657
7668
|
throw new LanesError("outpost_dispatch_failed", `Outpost ${outpostId} rejected the stop command: ${ownerError.message}`, {
|
|
@@ -7742,27 +7753,34 @@ async function dispatchOutpostLaneStop(ctx, lane) {
|
|
|
7742
7753
|
cancelledQuestions = parseRemoteCancelledQuestionIds(remote.cancelled_questions, lane.lane_id, outpostId, commandId);
|
|
7743
7754
|
}
|
|
7744
7755
|
const updated = syncLaneRecordFromOwner(ctx.hqRoot(), lane.lane_id, outpostId, ownerLane);
|
|
7745
|
-
|
|
7746
|
-
|
|
7747
|
-
|
|
7748
|
-
|
|
7749
|
-
|
|
7750
|
-
|
|
7751
|
-
|
|
7752
|
-
|
|
7753
|
-
|
|
7754
|
-
|
|
7755
|
-
|
|
7756
|
-
|
|
7757
|
-
|
|
7758
|
-
|
|
7759
|
-
|
|
7756
|
+
if (!quiet)
|
|
7757
|
+
printJson({
|
|
7758
|
+
ok: true,
|
|
7759
|
+
action: "stop",
|
|
7760
|
+
lane: updated,
|
|
7761
|
+
cancelled_questions: cancelledQuestions,
|
|
7762
|
+
outpost_id: outpostId,
|
|
7763
|
+
outpost_command_id: commandId,
|
|
7764
|
+
...(syncCommandId ? { outpost_sync_command_id: syncCommandId } : {}),
|
|
7765
|
+
outpost_result: {
|
|
7766
|
+
state,
|
|
7767
|
+
already_terminal: alreadyTerminal,
|
|
7768
|
+
status: dispatched.status,
|
|
7769
|
+
exit_code: dispatched.exitCode,
|
|
7770
|
+
},
|
|
7771
|
+
});
|
|
7760
7772
|
}
|
|
7761
|
-
async function runStop(ctx, laneId) {
|
|
7773
|
+
async function runStop(ctx, laneId, reason, quiet = false) {
|
|
7774
|
+
const operatorReason = reason?.trim();
|
|
7775
|
+
if (reason !== undefined && !operatorReason) {
|
|
7776
|
+
throw new LanesError("lane_stop_reason_invalid", "Stop reason must not be empty", {});
|
|
7777
|
+
}
|
|
7778
|
+
if (operatorReason !== undefined)
|
|
7779
|
+
assertNoSecretText("stop reason", operatorReason);
|
|
7762
7780
|
const hqRoot = ctx.hqRoot();
|
|
7763
7781
|
const lane = readLane(hqRoot, laneId);
|
|
7764
7782
|
if (lane.host.kind === "outpost" && !laneRunsHereForOperatorMutation(ctx, lane)) {
|
|
7765
|
-
await dispatchOutpostLaneStop(ctx, lane);
|
|
7783
|
+
await dispatchOutpostLaneStop(ctx, lane, operatorReason, quiet);
|
|
7766
7784
|
return;
|
|
7767
7785
|
}
|
|
7768
7786
|
assertLaneRunsHereForOperatorMutation(ctx, lane, "stop");
|
|
@@ -7779,6 +7797,16 @@ async function runStop(ctx, laneId) {
|
|
|
7779
7797
|
...current,
|
|
7780
7798
|
state: stopCommittedState(current.state),
|
|
7781
7799
|
queued_reason: undefined,
|
|
7800
|
+
...(operatorReason === undefined ? {} : {
|
|
7801
|
+
stop_history: [
|
|
7802
|
+
...(current.stop_history ?? []),
|
|
7803
|
+
{ at, reason: operatorReason },
|
|
7804
|
+
],
|
|
7805
|
+
terminal_details: {
|
|
7806
|
+
...(current.terminal_details ?? {}),
|
|
7807
|
+
operator_reason: operatorReason,
|
|
7808
|
+
},
|
|
7809
|
+
}),
|
|
7782
7810
|
};
|
|
7783
7811
|
});
|
|
7784
7812
|
if (updated.state === "killed") {
|
|
@@ -7790,12 +7818,13 @@ async function runStop(ctx, laneId) {
|
|
|
7790
7818
|
}
|
|
7791
7819
|
await emitLaneTerminalStateSoft(ctx, updated);
|
|
7792
7820
|
const cancelled = cancelPendingQuestionsForLane(hqRoot, laneId, at);
|
|
7793
|
-
|
|
7794
|
-
|
|
7795
|
-
|
|
7796
|
-
|
|
7797
|
-
|
|
7798
|
-
|
|
7821
|
+
if (!quiet)
|
|
7822
|
+
printJson({
|
|
7823
|
+
ok: true,
|
|
7824
|
+
action: "stop",
|
|
7825
|
+
lane: updated,
|
|
7826
|
+
cancelled_questions: cancelled.map((q) => q.question_id),
|
|
7827
|
+
});
|
|
7799
7828
|
}
|
|
7800
7829
|
async function runAsk(ctx, opts) {
|
|
7801
7830
|
const hqRoot = ctx.hqRoot();
|
|
@@ -9206,6 +9235,12 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
9206
9235
|
resolveCompany: ctx.resolveCompany,
|
|
9207
9236
|
trackerEnabled: ctx.laneTrackerEnabled,
|
|
9208
9237
|
now: ctx.now,
|
|
9238
|
+
stopLane: (laneId, reason) => runStop(ctx, laneId, reason, true),
|
|
9239
|
+
sessionState: (root, sessionId, now) => sessionLiveness(root, sessionId, {
|
|
9240
|
+
nowMs: now.getTime(),
|
|
9241
|
+
presenceConnected: ctx.presenceConnected ?? false,
|
|
9242
|
+
deps: ctx.livenessDeps,
|
|
9243
|
+
}),
|
|
9209
9244
|
}, wrap);
|
|
9210
9245
|
registerLaneReportCommands(lanes, {
|
|
9211
9246
|
hqRoot: ctx.hqRoot,
|
|
@@ -9542,8 +9577,9 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
9542
9577
|
.command("stop")
|
|
9543
9578
|
.description("Cancel a queued lane or kill a running one")
|
|
9544
9579
|
.argument("<lane-id>", "Lane id")
|
|
9545
|
-
.
|
|
9546
|
-
|
|
9580
|
+
.option("--reason <text>", "Record the reason in the lane's terminal history")
|
|
9581
|
+
.action(wrap(async (laneId, options) => {
|
|
9582
|
+
await runStop(ctx, laneId, options.reason);
|
|
9547
9583
|
}));
|
|
9548
9584
|
lanes
|
|
9549
9585
|
.command("ask")
|
|
@@ -262,6 +262,25 @@ export function applyOntologyRun(options) {
|
|
|
262
262
|
}
|
|
263
263
|
report.candidates = rawCandidates.length;
|
|
264
264
|
const index = buildEntityIndex(company.dir);
|
|
265
|
+
// checkpoint.ontology.skip-ambiguous-concept-links: canonical names held by entities of more than one type.
|
|
266
|
+
// Names compare in the non-organization form for every type (lowercase, keeps + and #, no squashing),
|
|
267
|
+
// so "C++" and "C", or "AB" and "A B", stay distinct.
|
|
268
|
+
const crossTypeKey = (name) => normalizeEntityName(name, "concept");
|
|
269
|
+
const crossTypeNames = new Set();
|
|
270
|
+
if (plan.skip_ambiguous_concept_links === true) {
|
|
271
|
+
const typesByName = new Map();
|
|
272
|
+
for (const entity of index.byRel.values()) {
|
|
273
|
+
const name = crossTypeKey(entity.canonical_name);
|
|
274
|
+
if (name === "")
|
|
275
|
+
continue;
|
|
276
|
+
const types = typesByName.get(name) ?? new Set();
|
|
277
|
+
types.add(entity.type);
|
|
278
|
+
typesByName.set(name, types);
|
|
279
|
+
}
|
|
280
|
+
for (const [name, types] of typesByName)
|
|
281
|
+
if (types.size > 1)
|
|
282
|
+
crossTypeNames.add(name);
|
|
283
|
+
}
|
|
265
284
|
const inputs = liveInputs(options.hqRoot, company, plan);
|
|
266
285
|
const blocked = new Set();
|
|
267
286
|
const updates = new Map();
|
|
@@ -313,8 +332,14 @@ export function applyOntologyRun(options) {
|
|
|
313
332
|
continue;
|
|
314
333
|
}
|
|
315
334
|
if (resolved.tier === 1 || resolved.tier === 2) {
|
|
316
|
-
report.matched += 1;
|
|
317
335
|
const linked = index.byRel.get(resolved.target);
|
|
336
|
+
if (input.kind === "knowledge" &&
|
|
337
|
+
linked.type === "concept" &&
|
|
338
|
+
crossTypeNames.has(crossTypeKey(linked.canonical_name))) {
|
|
339
|
+
note(report.rejected, "ambiguous_type");
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
report.matched += 1;
|
|
318
343
|
report.links.push({ input: candidate.input, action: "link", type: linked.type, canonical_name: linked.canonical_name, path: resolved.target });
|
|
319
344
|
if (resolved.tier === 1 && resolved.duplicates.length > 0) {
|
|
320
345
|
report.suspect_duplicates.push({ name: canonical, type: candidate.type, paths: [resolved.target, ...resolved.duplicates] });
|
|
@@ -68,6 +68,16 @@ export declare const NO_LINK_ALIASES_FLAG = "checkpoint.ontology.no-link-aliases
|
|
|
68
68
|
* the model cannot see that `John Smith` already exists.
|
|
69
69
|
*/
|
|
70
70
|
export declare const FIRST_NAME_PEOPLE_FLAG = "checkpoint.ontology.first-name-people";
|
|
71
|
+
/**
|
|
72
|
+
* A knowledge input does not link to a concept entity whose canonical name is
|
|
73
|
+
* also the canonical name of an entity of another type (`concept/hq-secrets`
|
|
74
|
+
* beside `project/hq-secrets`). The plan carries
|
|
75
|
+
* `skip_ambiguous_concept_links: true`, and apply rejects the link as
|
|
76
|
+
* `ambiguous_type`. CO-46: 34 of the 73 false positives on the knowledge
|
|
77
|
+
* fixtures were such concept links; skipping them raised precision from 0.733
|
|
78
|
+
* to 0.803 and cost recall 0.023 on a replay of six runs.
|
|
79
|
+
*/
|
|
80
|
+
export declare const SKIP_AMBIGUOUS_CONCEPT_LINKS_FLAG = "checkpoint.ontology.skip-ambiguous-concept-links";
|
|
71
81
|
export declare const PLAN_FILE = "ontology-plan.json";
|
|
72
82
|
export declare const CANDIDATES_FILE = "ontology-candidates.json";
|
|
73
83
|
export declare const REPORT_FILE = "ontology-report.json";
|
|
@@ -138,6 +148,8 @@ export interface OntologyPlan {
|
|
|
138
148
|
link_aliases?: false;
|
|
139
149
|
/** Set by checkpoint.ontology.first-name-people: apply defers a one-word person who may be an existing fuller name. */
|
|
140
150
|
first_name_people?: true;
|
|
151
|
+
/** Set by checkpoint.ontology.skip-ambiguous-concept-links: apply rejects a knowledge link to a concept whose name another type also holds. */
|
|
152
|
+
skip_ambiguous_concept_links?: true;
|
|
141
153
|
inputs: PlanInput[];
|
|
142
154
|
/** The session's summary, decisions and learnings, for the `session` input. */
|
|
143
155
|
session_text: string | null;
|
|
@@ -72,6 +72,16 @@ export const NO_LINK_ALIASES_FLAG = "checkpoint.ontology.no-link-aliases";
|
|
|
72
72
|
* the model cannot see that `John Smith` already exists.
|
|
73
73
|
*/
|
|
74
74
|
export const FIRST_NAME_PEOPLE_FLAG = "checkpoint.ontology.first-name-people";
|
|
75
|
+
/**
|
|
76
|
+
* A knowledge input does not link to a concept entity whose canonical name is
|
|
77
|
+
* also the canonical name of an entity of another type (`concept/hq-secrets`
|
|
78
|
+
* beside `project/hq-secrets`). The plan carries
|
|
79
|
+
* `skip_ambiguous_concept_links: true`, and apply rejects the link as
|
|
80
|
+
* `ambiguous_type`. CO-46: 34 of the 73 false positives on the knowledge
|
|
81
|
+
* fixtures were such concept links; skipping them raised precision from 0.733
|
|
82
|
+
* to 0.803 and cost recall 0.023 on a replay of six runs.
|
|
83
|
+
*/
|
|
84
|
+
export const SKIP_AMBIGUOUS_CONCEPT_LINKS_FLAG = "checkpoint.ontology.skip-ambiguous-concept-links";
|
|
75
85
|
export const PLAN_FILE = "ontology-plan.json";
|
|
76
86
|
export const CANDIDATES_FILE = "ontology-candidates.json";
|
|
77
87
|
export const REPORT_FILE = "ontology-report.json";
|
|
@@ -254,6 +264,7 @@ export async function planOntologyRun(options) {
|
|
|
254
264
|
const hintsOn = await options.readFlag(ENTITY_HINTS_FLAG, company);
|
|
255
265
|
const noLinkAliases = await options.readFlag(NO_LINK_ALIASES_FLAG, company);
|
|
256
266
|
const firstNamePeople = await options.readFlag(FIRST_NAME_PEOPLE_FLAG, company);
|
|
267
|
+
const skipAmbiguousConceptLinks = await options.readFlag(SKIP_AMBIGUOUS_CONCEPT_LINKS_FLAG, company);
|
|
257
268
|
const touched = sessionTouched(hqRoot, company, payload.files);
|
|
258
269
|
// A first run takes its boundary from the entity tree, so the ledger is
|
|
259
270
|
// refreshed before merge-back moves any `last_updated` forward.
|
|
@@ -368,6 +379,7 @@ export async function planOntologyRun(options) {
|
|
|
368
379
|
...(multiWriter ? { multi_writer: multiWriter } : {}),
|
|
369
380
|
...(noLinkAliases ? { link_aliases: false } : {}),
|
|
370
381
|
...(firstNamePeople ? { first_name_people: true } : {}),
|
|
382
|
+
...(skipAmbiguousConceptLinks ? { skip_ambiguous_concept_links: true } : {}),
|
|
371
383
|
inputs,
|
|
372
384
|
session_text: sessionText,
|
|
373
385
|
pending_after_run: remaining,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type LaneRecord, type SeniorRef } from "./types.js";
|
|
2
|
+
export declare const DEFAULT_ORPHAN_MIN_AGE_MINUTES = 30;
|
|
3
|
+
export interface OrphanLaneFinding {
|
|
4
|
+
lane_id: string;
|
|
5
|
+
senior: SeniorRef;
|
|
6
|
+
senior_state: string;
|
|
7
|
+
age_minutes: number;
|
|
8
|
+
account: string | null;
|
|
9
|
+
host: LaneRecord["host"];
|
|
10
|
+
}
|
|
11
|
+
export declare function parseOrphanMinAge(value: string | undefined): number;
|
|
12
|
+
export declare function findOrphanLanes(input: {
|
|
13
|
+
lanes: readonly LaneRecord[];
|
|
14
|
+
now: Date;
|
|
15
|
+
minAgeMinutes: number;
|
|
16
|
+
sessionState: (sessionId: string, now: Date) => string | null;
|
|
17
|
+
}): OrphanLaneFinding[];
|
|
18
|
+
//# sourceMappingURL=orphans.d.ts.map
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { LanesError } from "./errors.js";
|
|
2
|
+
import { isTerminalLaneState } from "./types.js";
|
|
3
|
+
export const DEFAULT_ORPHAN_MIN_AGE_MINUTES = 30;
|
|
4
|
+
const LIVE_LANE_STATES = new Set(["running", "queued", "blocked", "awaiting_input"]);
|
|
5
|
+
export function parseOrphanMinAge(value) {
|
|
6
|
+
if (value === undefined)
|
|
7
|
+
return DEFAULT_ORPHAN_MIN_AGE_MINUTES;
|
|
8
|
+
if (!/^(0|[1-9]\d*)$/.test(value)) {
|
|
9
|
+
throw new LanesError("orphan_min_age_invalid", "Minimum orphan age must be a non-negative whole number of minutes", { value });
|
|
10
|
+
}
|
|
11
|
+
const minutes = Number(value);
|
|
12
|
+
if (!Number.isSafeInteger(minutes) || minutes > Math.floor(Number.MAX_SAFE_INTEGER / 60_000)) {
|
|
13
|
+
throw new LanesError("orphan_min_age_invalid", "Minimum orphan age is outside the supported range", { value });
|
|
14
|
+
}
|
|
15
|
+
return minutes;
|
|
16
|
+
}
|
|
17
|
+
export function findOrphanLanes(input) {
|
|
18
|
+
const nowMs = input.now.getTime();
|
|
19
|
+
if (!Number.isFinite(nowMs)) {
|
|
20
|
+
throw new LanesError("orphan_clock_invalid", "Current time for orphan detection is invalid", {});
|
|
21
|
+
}
|
|
22
|
+
const byId = new Map(input.lanes.map((lane) => [lane.lane_id, lane]));
|
|
23
|
+
const minAgeMs = input.minAgeMinutes * 60_000;
|
|
24
|
+
const findings = [];
|
|
25
|
+
for (const lane of input.lanes) {
|
|
26
|
+
if (!LIVE_LANE_STATES.has(lane.state))
|
|
27
|
+
continue;
|
|
28
|
+
if (lane.keep_alive_standing === true || lane.senior.kind === "user")
|
|
29
|
+
continue;
|
|
30
|
+
const createdAt = Date.parse(lane.created_at);
|
|
31
|
+
if (!Number.isFinite(createdAt)) {
|
|
32
|
+
throw new LanesError("orphan_lane_timestamp_invalid", `Lane ${lane.lane_id} has an invalid created_at timestamp`, { lane_id: lane.lane_id, field: "created_at" });
|
|
33
|
+
}
|
|
34
|
+
const ageMs = nowMs - createdAt;
|
|
35
|
+
if (ageMs < 0) {
|
|
36
|
+
throw new LanesError("orphan_lane_timestamp_future", `Lane ${lane.lane_id} was created after the current time`, { lane_id: lane.lane_id, created_at: lane.created_at });
|
|
37
|
+
}
|
|
38
|
+
if (ageMs < minAgeMs)
|
|
39
|
+
continue;
|
|
40
|
+
let seniorState;
|
|
41
|
+
if (lane.senior.kind === "lane") {
|
|
42
|
+
const senior = byId.get(lane.senior.id ?? "");
|
|
43
|
+
if (!senior) {
|
|
44
|
+
seniorState = "missing";
|
|
45
|
+
}
|
|
46
|
+
else if (isTerminalLaneState(senior.state)) {
|
|
47
|
+
seniorState = senior.state;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
else if (lane.senior.kind === "session" && lane.senior.id !== undefined) {
|
|
51
|
+
if (input.sessionState(lane.senior.id, input.now) === "ended")
|
|
52
|
+
seniorState = "ended";
|
|
53
|
+
}
|
|
54
|
+
if (seniorState === undefined)
|
|
55
|
+
continue;
|
|
56
|
+
findings.push({
|
|
57
|
+
lane_id: lane.lane_id,
|
|
58
|
+
senior: { ...lane.senior },
|
|
59
|
+
senior_state: seniorState,
|
|
60
|
+
age_minutes: Math.floor(ageMs / 60_000),
|
|
61
|
+
account: lane.account ?? null,
|
|
62
|
+
host: { ...lane.host },
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return findings.sort((a, b) => b.age_minutes - a.age_minutes || a.lane_id.localeCompare(b.lane_id));
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=orphans.js.map
|
package/dist/lib/lanes/store.js
CHANGED
|
@@ -591,6 +591,24 @@ export function parseLaneRecord(raw, filePath) {
|
|
|
591
591
|
path: filePath,
|
|
592
592
|
});
|
|
593
593
|
}
|
|
594
|
+
if (raw.stop_history !== undefined) {
|
|
595
|
+
if (!Array.isArray(raw.stop_history)) {
|
|
596
|
+
throw new LaneSchemaError("stop_history must be an array when present", {
|
|
597
|
+
path: filePath,
|
|
598
|
+
field: "stop_history",
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
for (const [index, entry] of raw.stop_history.entries()) {
|
|
602
|
+
const field = `stop_history[${index}]`;
|
|
603
|
+
if (!isPlainObject(entry) || Object.keys(entry).some((key) => !["at", "reason"].includes(key))) {
|
|
604
|
+
throw new LaneSchemaError(`${field} must be a stop history entry`, { path: filePath, field });
|
|
605
|
+
}
|
|
606
|
+
if (typeof entry.reason !== "string" || entry.reason.trim() === "") {
|
|
607
|
+
throw new LaneSchemaError(`${field}.reason must be a non-empty string`, { path: filePath, field });
|
|
608
|
+
}
|
|
609
|
+
requireTimestamp(entry.at, `${field}.at`, filePath);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
594
612
|
if (raw.worker_profile_repin_history !== undefined) {
|
|
595
613
|
if (!Array.isArray(raw.worker_profile_repin_history)) {
|
|
596
614
|
throw new LaneSchemaError("worker_profile_repin_history must be an array when present", {
|
|
@@ -907,6 +925,11 @@ export function mergeLaneRecords(live, sidecars) {
|
|
|
907
925
|
failed_resume_history: unionHistory(all.map((record) => record.failed_resume_history ?? []), entryIdentity, base.failed_resume_history ?? []),
|
|
908
926
|
}
|
|
909
927
|
: {}),
|
|
928
|
+
...(all.some((record) => record.stop_history !== undefined)
|
|
929
|
+
? {
|
|
930
|
+
stop_history: unionHistory(all.map((record) => record.stop_history ?? []), entryIdentity, base.stop_history ?? []),
|
|
931
|
+
}
|
|
932
|
+
: {}),
|
|
910
933
|
stories: mergeLaneStories(all, base),
|
|
911
934
|
};
|
|
912
935
|
}
|
|
@@ -473,12 +473,16 @@ function setLaneEntry(story, lane, laneState, laneStoryStatus, prs, pendingQuest
|
|
|
473
473
|
item.terminal === true);
|
|
474
474
|
const stateChanged = oldState !== laneState;
|
|
475
475
|
if (stateChanged || (terminal && !alreadyRecorded)) {
|
|
476
|
+
const operatorReason = lane.terminal_details?.operator_reason;
|
|
476
477
|
history.push({
|
|
477
478
|
lane_id: lane.lane_id,
|
|
478
479
|
state: laneState,
|
|
479
480
|
at,
|
|
480
481
|
prs,
|
|
481
482
|
...(terminal ? { terminal: true } : {}),
|
|
483
|
+
...(terminal && typeof operatorReason === "string" && operatorReason.trim() !== ""
|
|
484
|
+
? { reason: operatorReason }
|
|
485
|
+
: {}),
|
|
482
486
|
});
|
|
483
487
|
}
|
|
484
488
|
const entry = {
|
|
@@ -69,7 +69,7 @@ export interface TrackerCheckFinding {
|
|
|
69
69
|
[key: string]: unknown;
|
|
70
70
|
}
|
|
71
71
|
export interface TrackerCheckResult {
|
|
72
|
-
id: "questions" | "lead_lanes" | "freshness" | "blockers" | "kpis" | "directives";
|
|
72
|
+
id: "questions" | "lead_lanes" | "freshness" | "blockers" | "kpis" | "directives" | "orphaned_lane";
|
|
73
73
|
result: "ok" | "failing";
|
|
74
74
|
items: TrackerCheckFinding[];
|
|
75
75
|
}
|
|
@@ -283,6 +283,10 @@ export interface SeniorHistoryEntry {
|
|
|
283
283
|
by: string;
|
|
284
284
|
reason: string;
|
|
285
285
|
}
|
|
286
|
+
export interface LaneStopHistoryEntry {
|
|
287
|
+
at: string;
|
|
288
|
+
reason: string;
|
|
289
|
+
}
|
|
286
290
|
/** Evidence for a senior-authorized Outpost worker-profile content re-pin. */
|
|
287
291
|
export interface WorkerProfileRepinHistoryEntry {
|
|
288
292
|
old_hash: string | null;
|
|
@@ -469,6 +473,8 @@ export interface LaneRecord {
|
|
|
469
473
|
/** Set when the reconcile pass force-admits under the starvation bound. */
|
|
470
474
|
admitted_reason?: string;
|
|
471
475
|
terminal_reason?: string;
|
|
476
|
+
/** Reasons supplied to explicit stop commands, retained independently of optional project ledgers. */
|
|
477
|
+
stop_history?: LaneStopHistoryEntry[];
|
|
472
478
|
/** Classification time when this lane entered interrupted; stable across later metadata writes. */
|
|
473
479
|
interrupted_at?: string;
|
|
474
480
|
/**
|
package/dist/lib/lanes/types.js
CHANGED
|
@@ -230,7 +230,7 @@ export const LANE_FIELD_WRITERS = {
|
|
|
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"],
|
|
232
232
|
overdue: ["reconcile"], overdue_at: ["reconcile"], last_host_report_at: ["watcher", "spawn", "reconcile"], stalled: ["watcher"], stalled_at: ["watcher"], stalled_reason: ["watcher"],
|
|
233
|
-
admitted_reason: ["command", "reconcile"], terminal_reason: ["command", "watcher", "spawn", "reconcile"], interrupted_at: ["watcher", "reconcile"], terminal_details: ["command", "spawn", "watcher", "reconcile"],
|
|
233
|
+
admitted_reason: ["command", "reconcile"], terminal_reason: ["command", "watcher", "spawn", "reconcile"], stop_history: ["command"], interrupted_at: ["watcher", "reconcile"], terminal_details: ["command", "spawn", "watcher", "reconcile"],
|
|
234
234
|
quota_reset_at: ["watcher", "reconcile"], model_quota_marker: ["watcher", "reconcile"], outpost_command_id: ["command"],
|
|
235
235
|
provider_session_id: ["watcher", "command", "spawn"], provider_session_id_absent: ["watcher", "command"], provider_home_at_spawn: ["spawn"], last_resume: ["spawn"], last_run_usage: ["watcher"], envelope_repair: ["watcher", "reconcile"], envelope_validation_errors: ["watcher"], last_progress_event_at: ["watcher"],
|
|
236
236
|
};
|