@indigoai-us/hq-cli 5.225.0 → 5.227.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 +47 -1
- package/dist/command-catalog.generated.d.ts +100 -0
- package/dist/command-catalog.generated.js +128 -0
- package/dist/command-registration-plan.d.ts +18 -0
- package/dist/command-registration-plan.js +6 -0
- package/dist/commands/agent-enroll.d.ts +2 -0
- package/dist/commands/agent-enroll.js +17 -2
- package/dist/commands/cloud.d.ts +10 -0
- package/dist/commands/cloud.js +52 -47
- package/dist/commands/lanes-checks.d.ts +2 -0
- package/dist/commands/lanes-checks.js +13 -11
- package/dist/commands/lanes-import.d.ts +1 -0
- package/dist/commands/lanes-import.js +12 -11
- package/dist/commands/lanes-report.js +21 -14
- package/dist/commands/lanes-tracker.d.ts +1 -0
- package/dist/commands/lanes-tracker.js +39 -31
- package/dist/commands/lanes.d.ts +8 -0
- package/dist/commands/lanes.js +818 -314
- package/dist/commands/staff/reset-test-account.d.ts +110 -0
- package/dist/commands/staff/reset-test-account.js +268 -0
- package/dist/commands/staff/test-account.d.ts +51 -0
- package/dist/commands/staff/test-account.js +196 -0
- package/dist/lib/checkpoint-ontology/plan.d.ts +4 -6
- package/dist/lib/checkpoint-ontology/plan.js +8 -26
- package/dist/lib/doctor/client-health-flag-token.d.ts +15 -0
- package/dist/lib/doctor/client-health-flag-token.js +66 -0
- package/dist/lib/doctor/client-health-reason-codes-flag.d.ts +1 -0
- package/dist/lib/doctor/client-health-reason-codes-flag.js +16 -5
- package/dist/lib/doctor/client-health-sentry-flag.d.ts +1 -0
- package/dist/lib/doctor/client-health-sentry-flag.js +17 -6
- package/dist/lib/lanes/dropbox.d.ts +9 -0
- package/dist/lib/lanes/dropbox.js +70 -1
- package/dist/lib/lanes/reconcile.d.ts +4 -0
- package/dist/lib/lanes/reconcile.js +28 -0
- package/dist/lib/lanes/store.js +9 -1
- package/dist/lib/lanes/tracker-mesh-questions.d.ts +54 -0
- package/dist/lib/lanes/tracker-mesh-questions.js +138 -0
- package/dist/lib/lanes/tracker-mesh.d.ts +19 -0
- package/dist/lib/lanes/tracker-mesh.js +228 -0
- package/dist/lib/lanes/tracker-store.d.ts +13 -2
- package/dist/lib/lanes/tracker-store.js +1 -1
- package/dist/lib/lanes/types.d.ts +4 -0
- package/dist/lib/lanes/watcher-main.js +16 -0
- package/dist/lib/lanes/watcher.d.ts +4 -1
- package/dist/lib/lanes/watcher.js +77 -3
- package/dist/utils/cognito-session.d.ts +8 -0
- package/dist/utils/cognito-session.js +198 -3
- package/dist/utils/upload-author.d.ts +15 -0
- package/dist/utils/upload-author.js +39 -0
- package/package.json +1 -1
package/dist/commands/lanes.js
CHANGED
|
@@ -17,6 +17,8 @@ import { registerLaneChecksCommand } from "./lanes-checks.js";
|
|
|
17
17
|
import { registerLaneReportCommands } from "./lanes-report.js";
|
|
18
18
|
import { registerLaneTrackerImportCommand } from "./lanes-import.js";
|
|
19
19
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
20
|
+
import { createTrackerMeshQuestionClient, deliverTrackerMeshQuestionOnce } from "../lib/lanes/tracker-mesh-questions.js";
|
|
21
|
+
import { createWorkMeshTrackerStore } from "../lib/lanes/tracker-mesh.js";
|
|
20
22
|
import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
|
|
21
23
|
import { isSsmRunCommandProcess } from "../lib/lanes/ssm-command-context.js";
|
|
22
24
|
import { readLanesRegistryFlag } from "../lib/lanes/registry-flag.js";
|
|
@@ -44,19 +46,20 @@ import { AccountResolutionError, compareLaneQueueOrder, coveredLaneIds, DEFAULT_
|
|
|
44
46
|
import { activeLaneStories, laneStories, laneStory, storyWarning, } from "../lib/lanes/stories.js";
|
|
45
47
|
import { resolveLaneKeepAlive } from "../lib/lanes/profile.js";
|
|
46
48
|
import { keepAliveExclusion, LEAD_LANE_KEEP_ALIVE_FLAG, loadRetiredLaneIds, } from "../lib/lanes/keep-alive.js";
|
|
47
|
-
import { questionBodyAbsPath, resolveQuestionCompanyWithFallback, resolveProjectQuestionCompany, } from "../lib/lanes/questions.js";
|
|
49
|
+
import { questionBodyAbsPath, questionRecordRefusalCode, resolveQuestionCompanyWithFallback, resolveProjectQuestionCompany, } from "../lib/lanes/questions.js";
|
|
48
50
|
import { assertQuestionBodySecretSafe } from "../lib/lanes/questions.js";
|
|
49
51
|
import { compactionAfterUsage, } from "../lib/lanes/compaction.js";
|
|
50
52
|
import { assertUniqueAccountHomes, providerCompactAfterMs } from "../lib/lanes/config.js";
|
|
51
53
|
import { parseCliEffort, resolveLaneEffort, } from "../lib/lanes/effort.js";
|
|
52
54
|
import { LanesError } from "../lib/lanes/errors.js";
|
|
53
55
|
import { dispatchLaneDelivery } from "../lib/lanes/delivery-core.js";
|
|
54
|
-
import { formatMonitorEvent } from "../lib/lanes/dropbox.js";
|
|
56
|
+
import { dropboxInboxHasMessages, drainDropbox, formatDropboxMessages, formatMonitorEvent, requeueDropboxMessages, } from "../lib/lanes/dropbox.js";
|
|
57
|
+
import { isPidAlive, runWatcherLoop, WATCHER_OUT_MAX_BYTES, watcherOutLogPath, writeWatcherDecision, } from "../lib/lanes/watcher.js";
|
|
55
58
|
import { accountQuotaView, loadQuotaState, quotaBlockFor } from "../lib/lanes/quota.js";
|
|
56
59
|
import { SESSION_MOVE_REASON } from "../lib/lanes/move-account.js";
|
|
57
60
|
import { ensureSessionHome } from "../lib/lanes/session-relink.js";
|
|
58
61
|
import { defaultProbe, validateProfileLaneModel, validateLaneModel, } from "../lib/lanes/models.js";
|
|
59
|
-
import { parseCliModel, refuseOutpostSelfTarget, resolveLaneModel, FOREGROUND_ONLY_RULE, STANDING_AUTHORIZATION, spawnLaneWorker, laneRunDir, readSpawnManifest, stopLaneProcesses, } from "../lib/lanes/spawn.js";
|
|
62
|
+
import { parseCliModel, refuseOutpostSelfTarget, resolveLaneModel, FOREGROUND_ONLY_RULE, STANDING_AUTHORIZATION, spawnLaneWorker, laneRunDir, readSpawnManifest, readPidRecord, probePidRecord, readProcStarttime, stopLaneProcesses, } from "../lib/lanes/spawn.js";
|
|
60
63
|
import { handOffInstallHere } from "../lib/daemon/handoff.js";
|
|
61
64
|
export { spawnLaneWorker };
|
|
62
65
|
export { questionBodyAbsPath };
|
|
@@ -283,6 +286,8 @@ function resolveDeps(deps = {}) {
|
|
|
283
286
|
proveDetached: deps.proveDetached,
|
|
284
287
|
killProcess: deps.killProcess,
|
|
285
288
|
isAlive: deps.isAlive,
|
|
289
|
+
readStarttime: deps.readStarttime,
|
|
290
|
+
watcherLoopDeps: deps.watcherLoopDeps,
|
|
286
291
|
stopSleepMs: deps.stopSleepMs,
|
|
287
292
|
livenessDeps: deps.livenessDeps,
|
|
288
293
|
presenceConnected: deps.presenceConnected,
|
|
@@ -324,6 +329,7 @@ function resolveDeps(deps = {}) {
|
|
|
324
329
|
((company) => defaultLaneOutpostQuestionVisibilityEnabled(company, env)),
|
|
325
330
|
laneTrackerEnabled: deps.laneTrackerEnabled ?? defaultLaneTrackerEnabled,
|
|
326
331
|
laneTrackerMeshEnabled: deps.laneTrackerMeshEnabled ?? defaultLaneTrackerMeshEnabled,
|
|
332
|
+
trackerMeshRequest: deps.trackerMeshRequest,
|
|
327
333
|
laneHostScopedAccountHomesEnabled: deps.laneHostScopedAccountHomesEnabled ??
|
|
328
334
|
((company) => defaultLaneHostScopedAccountHomesEnabled(company, env)),
|
|
329
335
|
trackerActor: deps.trackerActor ?? defaultTrackerActor,
|
|
@@ -513,6 +519,167 @@ function requireQuestionBody(hqRoot, question) {
|
|
|
513
519
|
}
|
|
514
520
|
return body;
|
|
515
521
|
}
|
|
522
|
+
function trackerMeshAudience(ref) {
|
|
523
|
+
if (ref.kind === "user")
|
|
524
|
+
return "owner";
|
|
525
|
+
if (ref.kind === "session")
|
|
526
|
+
return "operator";
|
|
527
|
+
return `lane:${ref.id ?? ""}`;
|
|
528
|
+
}
|
|
529
|
+
function trackerMeshAudienceKind(value) {
|
|
530
|
+
if (value === "owner" || value.startsWith("owner:"))
|
|
531
|
+
return "owner";
|
|
532
|
+
if (value === "operator" || value.startsWith("operator:"))
|
|
533
|
+
return "operator";
|
|
534
|
+
return "lane";
|
|
535
|
+
}
|
|
536
|
+
function trackerMeshQuestionInput(lane, question, body) {
|
|
537
|
+
if (!lane.project_id.trim())
|
|
538
|
+
throw new LanesError("tracker_mesh_question_invalid", "Question lane has no project id", { lane_id: lane.lane_id });
|
|
539
|
+
const route = question.routing_history;
|
|
540
|
+
const currentAudience = route.at(-1)?.audience ?? lane.senior;
|
|
541
|
+
return {
|
|
542
|
+
laneId: lane.lane_id,
|
|
543
|
+
...(lane.thread_id ? { threadId: lane.thread_id } : {}),
|
|
544
|
+
projectId: lane.project_id,
|
|
545
|
+
audience: trackerMeshAudienceKind(trackerMeshAudience(currentAudience)),
|
|
546
|
+
audienceChain: route.length ? route.map((entry) => trackerMeshAudience(entry.audience)) : [trackerMeshAudience(currentAudience)],
|
|
547
|
+
routingHistory: route.map((entry) => ({ audience: trackerMeshAudience(entry.audience), at: entry.at, reason: entry.reason })),
|
|
548
|
+
question: body.question,
|
|
549
|
+
context: body.context,
|
|
550
|
+
options: body.options,
|
|
551
|
+
...(body.recommended !== undefined ? { recommended: body.recommended } : {}),
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
function assertMeshQuestionMatchesLocal(row, question, company) {
|
|
555
|
+
if (row.companyUid !== company.uid || row.laneId !== question.lane_id || row.projectId !== question.project_id) {
|
|
556
|
+
throw new LanesError("tracker_question_company_mismatch", "Work Mesh question does not match the local lane company and project", {
|
|
557
|
+
question_id: question.question_id,
|
|
558
|
+
mesh_question_id: row.questionId,
|
|
559
|
+
lane_id: question.lane_id,
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
async function indexTrackerQuestion(ctx, hqRoot, lane, question, body, company, client = createTrackerMeshQuestionClient({ company, request: ctx.trackerMeshRequest })) {
|
|
564
|
+
if (question.mesh_question_id)
|
|
565
|
+
return question;
|
|
566
|
+
if (question.status !== "pending")
|
|
567
|
+
return question;
|
|
568
|
+
const input = trackerMeshQuestionInput(lane, question, body);
|
|
569
|
+
const candidates = (await client.list(["pending", "answered"])).filter((row) => row.laneId === input.laneId && row.projectId === input.projectId && row.threadId === (input.threadId ?? row.threadId) &&
|
|
570
|
+
row.audience === input.audience && row.question === input.question && row.context === input.context &&
|
|
571
|
+
JSON.stringify(row.options) === JSON.stringify(input.options) &&
|
|
572
|
+
row.routingHistory.at(-1)?.at === input.routingHistory.at(-1)?.at);
|
|
573
|
+
if (candidates.length > 1) {
|
|
574
|
+
throw new LanesError("tracker_mesh_question_ambiguous", "More than one pending Work Mesh question matches this local question", { question_id: question.question_id, lane_id: lane.lane_id });
|
|
575
|
+
}
|
|
576
|
+
const row = candidates[0] ?? await client.create(input);
|
|
577
|
+
assertMeshQuestionMatchesLocal(row, question, company);
|
|
578
|
+
return casUpdateQuestion(hqRoot, question.question_id, (current) => {
|
|
579
|
+
if (current.status !== "pending")
|
|
580
|
+
return current;
|
|
581
|
+
if (current.mesh_question_id && current.mesh_question_id !== row.questionId) {
|
|
582
|
+
throw new LanesError("tracker_mesh_question_conflict", "Local question was already linked to another Work Mesh question", { question_id: current.question_id });
|
|
583
|
+
}
|
|
584
|
+
return current.mesh_question_id === row.questionId ? current : { ...current, mesh_question_id: row.questionId };
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
async function syncTrackerMeshQuestions(ctx, hqRoot) {
|
|
588
|
+
if (!ctx.laneTrackerMeshEnabled)
|
|
589
|
+
return;
|
|
590
|
+
const lanes = new Map(loadAllLanes(hqRoot).map((lane) => [lane.lane_id, lane]));
|
|
591
|
+
const byCompany = new Map();
|
|
592
|
+
for (const lane of lanes.values()) {
|
|
593
|
+
let company;
|
|
594
|
+
try {
|
|
595
|
+
company = await resolveQuestionCompanyWithFallback(hqRoot, lane, () => resolveActiveQuestionCompany(ctx, "Cannot sync lane questions without an HQ company"));
|
|
596
|
+
}
|
|
597
|
+
catch (err) {
|
|
598
|
+
if (lane.company || lane.mesh_company ||
|
|
599
|
+
!isLanesError(err) || err.error !== "question_company_unavailable" ||
|
|
600
|
+
err.details.resolution !== "active_company_required")
|
|
601
|
+
throw err;
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
if (!await ctx.laneTrackerMeshEnabled(company))
|
|
605
|
+
continue;
|
|
606
|
+
const key = `${company.uid}\0${company.slug}`;
|
|
607
|
+
if (!byCompany.has(key))
|
|
608
|
+
byCompany.set(key, { company, client: createTrackerMeshQuestionClient({ company, request: ctx.trackerMeshRequest }) });
|
|
609
|
+
}
|
|
610
|
+
const localQuestions = loadAllQuestions(hqRoot);
|
|
611
|
+
for (const question of localQuestions) {
|
|
612
|
+
if (question.status !== "pending" || question.mesh_question_id)
|
|
613
|
+
continue;
|
|
614
|
+
const lane = lanes.get(question.lane_id);
|
|
615
|
+
if (!lane)
|
|
616
|
+
continue;
|
|
617
|
+
const company = question.company ?? lane.company ?? lane.mesh_company;
|
|
618
|
+
if (!company)
|
|
619
|
+
continue;
|
|
620
|
+
const client = byCompany.get(`${company.uid}\0${company.slug}`)?.client;
|
|
621
|
+
if (!client)
|
|
622
|
+
continue;
|
|
623
|
+
const body = requireQuestionBody(hqRoot, question);
|
|
624
|
+
await indexTrackerQuestion(ctx, hqRoot, lane, question, body, company, client);
|
|
625
|
+
}
|
|
626
|
+
for (const { company, client } of byCompany.values()) {
|
|
627
|
+
const remote = await client.list(["answered"]);
|
|
628
|
+
const remoteById = new Map(remote.map((row) => [row.questionId, row]));
|
|
629
|
+
for (const question of loadAllQuestions(hqRoot)) {
|
|
630
|
+
if (!question.mesh_question_id || question.company?.uid !== company.uid)
|
|
631
|
+
continue;
|
|
632
|
+
const row = remoteById.get(question.mesh_question_id);
|
|
633
|
+
if (!row)
|
|
634
|
+
continue;
|
|
635
|
+
assertMeshQuestionMatchesLocal(row, question, company);
|
|
636
|
+
const lane = lanes.get(question.lane_id);
|
|
637
|
+
if (!lane)
|
|
638
|
+
continue;
|
|
639
|
+
if (row.status === "answered" && question.status === "pending") {
|
|
640
|
+
if (typeof row.choice !== "string" || typeof row.answeredAt !== "string" || typeof row.answeredBy !== "string") {
|
|
641
|
+
throw new LanesError("tracker_mesh_response_invalid", "Answered Work Mesh question is missing its answer fields", { mesh_question_id: row.questionId });
|
|
642
|
+
}
|
|
643
|
+
const currentBody = requireQuestionBody(hqRoot, question);
|
|
644
|
+
let notesRef;
|
|
645
|
+
let nextBody = question.body;
|
|
646
|
+
if (row.notes !== undefined) {
|
|
647
|
+
const bodyQuestion = question.company || question.body.store === "personal" ? question : { ...question, company };
|
|
648
|
+
const stored = writeQuestionBody(hqRoot, bodyQuestion, { ...currentBody, notes: row.notes });
|
|
649
|
+
notesRef = stored.ref;
|
|
650
|
+
nextBody = stored;
|
|
651
|
+
}
|
|
652
|
+
casUpdateQuestion(hqRoot, question.question_id, (current) => {
|
|
653
|
+
if (current.status !== "pending")
|
|
654
|
+
return current;
|
|
655
|
+
if (current.mesh_question_id !== row.questionId)
|
|
656
|
+
throw new LanesError("tracker_mesh_question_conflict", "Local question link changed during answer sync", { question_id: current.question_id });
|
|
657
|
+
return {
|
|
658
|
+
...current,
|
|
659
|
+
company: current.company ?? company,
|
|
660
|
+
body: nextBody,
|
|
661
|
+
status: "answered",
|
|
662
|
+
answer: {
|
|
663
|
+
choice: row.choice,
|
|
664
|
+
answer_kind: currentBody.options.includes(row.choice) ? "option" : "freeform",
|
|
665
|
+
notes_ref: notesRef,
|
|
666
|
+
answered_by: row.answeredBy,
|
|
667
|
+
answered_at: row.answeredAt,
|
|
668
|
+
answered_level: current.hop,
|
|
669
|
+
},
|
|
670
|
+
};
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
for (const question of loadAllQuestions(hqRoot)) {
|
|
675
|
+
if (question.company?.uid !== company.uid || !question.mesh_question_id)
|
|
676
|
+
continue;
|
|
677
|
+
await deliverTrackerMeshQuestionOnce(question, company, client, (deliveredAt) => casUpdateQuestion(hqRoot, question.question_id, (current) => current.mesh_delivered_at
|
|
678
|
+
? current
|
|
679
|
+
: { ...current, mesh_delivered_at: deliveredAt }));
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
516
683
|
function loadAllLanes(hqRoot) {
|
|
517
684
|
return listLaneIds(hqRoot).map((id) => readLane(hqRoot, id));
|
|
518
685
|
}
|
|
@@ -5552,7 +5719,13 @@ function assertCurrentResumeAccountAvailable(ctx, lane) {
|
|
|
5552
5719
|
}
|
|
5553
5720
|
async function runMessage(ctx, laneId, opts) {
|
|
5554
5721
|
const hqRoot = ctx.hqRoot();
|
|
5555
|
-
|
|
5722
|
+
let accepted = false;
|
|
5723
|
+
let claimedMessages = [];
|
|
5724
|
+
const markAccepted = () => {
|
|
5725
|
+
accepted = true;
|
|
5726
|
+
opts.onAccepted?.();
|
|
5727
|
+
};
|
|
5728
|
+
let text = opts.text;
|
|
5556
5729
|
if (!text) {
|
|
5557
5730
|
throw new LanesError("missing_text", "--text is required", {});
|
|
5558
5731
|
}
|
|
@@ -5562,330 +5735,498 @@ async function runMessage(ctx, laneId, opts) {
|
|
|
5562
5735
|
return;
|
|
5563
5736
|
}
|
|
5564
5737
|
assertLaneRunsHereForOperatorMutation(ctx, lane, "message");
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
|
|
5579
|
-
|
|
5580
|
-
|
|
5581
|
-
|
|
5582
|
-
|
|
5583
|
-
|
|
5584
|
-
|
|
5585
|
-
|
|
5586
|
-
|
|
5587
|
-
|
|
5588
|
-
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
|
|
5599
|
-
|
|
5600
|
-
|
|
5601
|
-
throw new LanesError("model_requires_resume", "--model with --account requires a message that resumes the lane", { lane_id: laneId, state: lane.state });
|
|
5602
|
-
}
|
|
5603
|
-
if (opts.worker !== undefined && mechanism === "dropbox") {
|
|
5604
|
-
throw new LanesError("worker_profile_requires_resume", `Lane ${laneId} is still running; --worker can only be applied when message starts a resumed provider turn`, { lane_id: laneId, state: lane.state });
|
|
5605
|
-
}
|
|
5606
|
-
if (cliEffort !== undefined && mechanism === "dropbox") {
|
|
5607
|
-
throw new LanesError("lane_effort_requires_resume", `Lane ${laneId} is still running; --effort can only be applied when message starts a resumed provider turn`, { lane_id: laneId, state: lane.state });
|
|
5608
|
-
}
|
|
5609
|
-
const resumeProfile = opts.worker !== undefined
|
|
5610
|
-
? resolveResumeProfile(ctx, lane, opts.worker)
|
|
5611
|
-
: undefined;
|
|
5612
|
-
if (opts.model !== undefined && opts.account === undefined) {
|
|
5613
|
-
throw new LanesError("model_requires_account", "--model on hq lanes message is only accepted together with --account", {});
|
|
5614
|
-
}
|
|
5615
|
-
const automaticAccountSelection = opts.account === undefined && mechanism === "resume"
|
|
5616
|
-
? await selectAutomaticClaudeResumeAccount(ctx, lane, resumeProfile)
|
|
5617
|
-
: undefined;
|
|
5618
|
-
const resumeEffort = resolveResumeEffort(ctx, lane, {
|
|
5619
|
-
cliEffort,
|
|
5620
|
-
profile: resumeProfile,
|
|
5621
|
-
account: keepCurrentAccount ? lane.account : opts.account ?? automaticAccountSelection?.selection.account,
|
|
5622
|
-
model: opts.model,
|
|
5623
|
-
});
|
|
5624
|
-
const messageId = mintMessageId();
|
|
5625
|
-
const at = ctx.now().toISOString();
|
|
5626
|
-
const projectId = opts.project?.trim() || lane.project_id;
|
|
5627
|
-
const storyId = opts.story?.trim() || lane.story_id;
|
|
5628
|
-
if (opts.story !== undefined && !opts.story.trim()) {
|
|
5629
|
-
throw new LanesError("missing_story", "--story must be non-empty", {});
|
|
5630
|
-
}
|
|
5631
|
-
if ((opts.project !== undefined || opts.story !== undefined) && ctx.storyLedgerEnabled()) {
|
|
5632
|
-
validateProjectStories(hqRoot, projectId, [storyId], lane.company?.slug);
|
|
5738
|
+
if (lane.state === "running") {
|
|
5739
|
+
const workerPidPath = path.join(laneRunDir(hqRoot, laneId), "worker.pid");
|
|
5740
|
+
const workerRecordExists = fs.existsSync(workerPidPath);
|
|
5741
|
+
const worker = readPidRecord(workerPidPath);
|
|
5742
|
+
if (workerRecordExists && worker === null) {
|
|
5743
|
+
throw new LanesError("worker_pid_invalid", `Lane ${laneId} has a malformed worker PID record; message delivery cannot be classified safely`, { lane_id: laneId, worker_pid_path: workerPidPath });
|
|
5744
|
+
}
|
|
5745
|
+
// A fresh spawn may have committed running before publishing worker.pid;
|
|
5746
|
+
// absence is not proof of exit. Reclassify only a recorded process that
|
|
5747
|
+
// fails the PID plus start-time identity probe.
|
|
5748
|
+
const processAlive = worker === null
|
|
5749
|
+
? true
|
|
5750
|
+
: probePidRecord(worker, ctx.isAlive ?? isPidAlive, ctx.readStarttime ?? readProcStarttime).alive;
|
|
5751
|
+
if (worker !== null && !processAlive) {
|
|
5752
|
+
// The watcher may not have reached its next poll yet. Run the same
|
|
5753
|
+
// classifier synchronously, then make the queue/resume choice again.
|
|
5754
|
+
await runWatcherLoop(hqRoot, laneId, {
|
|
5755
|
+
...ctx.watcherLoopDeps,
|
|
5756
|
+
now: () => ctx.now().getTime(),
|
|
5757
|
+
isAlive: ctx.isAlive ?? isPidAlive,
|
|
5758
|
+
readStarttime: ctx.readStarttime ?? readProcStarttime,
|
|
5759
|
+
mesh: null,
|
|
5760
|
+
resolveQuestionActiveCompany: () => resolveActiveQuestionCompany(ctx),
|
|
5761
|
+
});
|
|
5762
|
+
lane = readLane(hqRoot, laneId);
|
|
5763
|
+
// This invocation won the race to classify the dead worker, so the
|
|
5764
|
+
// watcher callback did not own recovery. Fold any earlier queued
|
|
5765
|
+
// messages into this resume to keep one provider turn and one delivery.
|
|
5766
|
+
if (laneDeliveryMechanism(lane) === "resume" && dropboxInboxHasMessages(hqRoot, laneId)) {
|
|
5767
|
+
const queuedMessages = drainDropbox(hqRoot, laneId);
|
|
5768
|
+
if (queuedMessages.length > 0) {
|
|
5769
|
+
claimedMessages = queuedMessages;
|
|
5770
|
+
text = `${formatDropboxMessages(queuedMessages)}\n\n${text}`;
|
|
5771
|
+
}
|
|
5772
|
+
}
|
|
5773
|
+
}
|
|
5633
5774
|
}
|
|
5634
|
-
|
|
5635
|
-
|
|
5636
|
-
|
|
5637
|
-
|
|
5638
|
-
|
|
5639
|
-
|
|
5640
|
-
text,
|
|
5641
|
-
});
|
|
5642
|
-
const deliveryText = framedMonitorEvent ?? text;
|
|
5643
|
-
const dropboxMessage = {
|
|
5644
|
-
at,
|
|
5645
|
-
message_id: messageId,
|
|
5646
|
-
text,
|
|
5647
|
-
project_id: projectId,
|
|
5648
|
-
story_id: storyId,
|
|
5649
|
-
...(opts.monitorEvent === undefined ? {} : {
|
|
5650
|
-
kind: "monitor_event",
|
|
5651
|
-
monitor_id: opts.monitorEvent.monitor_id,
|
|
5652
|
-
description: opts.monitorEvent.description,
|
|
5653
|
-
sequence: opts.monitorEvent.sequence,
|
|
5654
|
-
}),
|
|
5655
|
-
};
|
|
5656
|
-
let resumed = null;
|
|
5657
|
-
let move = null;
|
|
5658
|
-
let queuedOnAdmission = false;
|
|
5659
|
-
if (opts.account !== undefined && !keepCurrentAccount) {
|
|
5660
|
-
// A moved message is always a resume on the target account: a running
|
|
5661
|
-
// lane (the dropbox mechanism) is refused inside prepareAccountMove.
|
|
5662
|
-
move = await prepareAccountMove(ctx, lane, {
|
|
5663
|
-
account: opts.account,
|
|
5664
|
-
cliModel: opts.model?.trim() || undefined,
|
|
5665
|
-
...(resumeProfile !== undefined ? { workerProfile: resumeProfile } : {}),
|
|
5666
|
-
...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
|
|
5667
|
-
pending: {
|
|
5668
|
-
trigger: "message",
|
|
5669
|
-
text: deliveryText,
|
|
5670
|
-
...(opts.monitorEvent === undefined ? {} : { monitor_event: true }),
|
|
5671
|
-
message_id: messageId,
|
|
5672
|
-
compact: opts.compact !== false,
|
|
5673
|
-
},
|
|
5674
|
-
});
|
|
5675
|
-
lane = move.lane;
|
|
5676
|
-
if (move.queued) {
|
|
5677
|
-
archiveResumeMessage(hqRoot, laneId, dropboxMessage);
|
|
5678
|
-
}
|
|
5679
|
-
}
|
|
5680
|
-
else if (mechanism === "resume") {
|
|
5681
|
-
const automatic = automaticAccountSelection;
|
|
5682
|
-
if (automatic !== undefined) {
|
|
5683
|
-
const selection = automatic.selection;
|
|
5684
|
-
if (selection.account !== lane.account) {
|
|
5685
|
-
move = await prepareAccountMove(ctx, lane, {
|
|
5686
|
-
account: selection.account,
|
|
5687
|
-
cliModel: undefined,
|
|
5688
|
-
...(automatic.historyReason !== undefined ? { accountHistoryReason: automatic.historyReason } : {}),
|
|
5689
|
-
...(resumeProfile !== undefined ? { workerProfile: resumeProfile } : {}),
|
|
5690
|
-
...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
|
|
5691
|
-
pending: {
|
|
5692
|
-
trigger: "message",
|
|
5693
|
-
text: deliveryText,
|
|
5694
|
-
...(opts.monitorEvent === undefined ? {} : { monitor_event: true }),
|
|
5695
|
-
message_id: messageId,
|
|
5696
|
-
compact: opts.compact !== false,
|
|
5697
|
-
},
|
|
5698
|
-
accountSelection: selection,
|
|
5699
|
-
});
|
|
5700
|
-
lane = move.lane;
|
|
5701
|
-
if (move.queued)
|
|
5702
|
-
archiveResumeMessage(hqRoot, laneId, dropboxMessage);
|
|
5775
|
+
try {
|
|
5776
|
+
const exclusion = messageLaneExclusion(ctx, lane);
|
|
5777
|
+
if (exclusion !== undefined) {
|
|
5778
|
+
let messageCount;
|
|
5779
|
+
if (dropboxInboxHasMessages(hqRoot, laneId)) {
|
|
5780
|
+
messageCount = drainDropbox(hqRoot, laneId, { archiveDirectory: "undeliverable" }).length;
|
|
5703
5781
|
}
|
|
5704
|
-
|
|
5705
|
-
|
|
5782
|
+
recordMessageUndeliverable(ctx, lane, exclusion, messageCount);
|
|
5783
|
+
const refusalMessage = deliveryRefusalReason(lane);
|
|
5784
|
+
throw new LanesError("lane_not_accepting_messages", refusalMessage || `Lane ${laneId} cannot receive messages: ${exclusion}`, { lane_id: laneId, state: lane.state, reason: exclusion });
|
|
5785
|
+
}
|
|
5786
|
+
if (lane.state === "done" && dropboxInboxHasMessages(hqRoot, laneId)) {
|
|
5787
|
+
const staleMessages = drainDropbox(hqRoot, laneId, { archiveDirectory: "undeliverable" });
|
|
5788
|
+
if (staleMessages.length > 0) {
|
|
5789
|
+
recordMessageUndeliverable(ctx, lane, "done_before_queued_message", staleMessages.length);
|
|
5706
5790
|
}
|
|
5707
5791
|
}
|
|
5708
|
-
|
|
5709
|
-
|
|
5710
|
-
|
|
5711
|
-
|
|
5712
|
-
|
|
5713
|
-
|
|
5714
|
-
|
|
5715
|
-
|
|
5716
|
-
|
|
5792
|
+
const cliEffort = parseCliEffort(lane.provider, opts.effort);
|
|
5793
|
+
if (cliEffort !== undefined && !providerSupportsSessionResume(lane.provider)) {
|
|
5794
|
+
throw new LanesError("effort_resume_unsupported", `Provider ${lane.provider} cannot change effort on a resumed session`, { lane_id: laneId, provider: lane.provider });
|
|
5795
|
+
}
|
|
5796
|
+
// Check before transcript recovery, account probing, or message writes.
|
|
5797
|
+
// The final state transition repeats this check under the senior lock.
|
|
5798
|
+
await withResumeHireAdmission(ctx, lane, () => undefined);
|
|
5799
|
+
lane = laneWithRecoveredProviderSession(hqRoot, lane);
|
|
5800
|
+
const keepCurrentAccount = opts.account !== undefined && accountRequestKeepsCurrentLane(lane, opts.account);
|
|
5801
|
+
if (keepCurrentAccount)
|
|
5802
|
+
assertCurrentResumeAccountAvailable(ctx, lane);
|
|
5803
|
+
// A real account move is refused on its own terms first: a running lane and
|
|
5804
|
+
// a provider without session resume get the move's named errors, not the
|
|
5805
|
+
// generic delivery refusal below. Naming the current account is a keep.
|
|
5806
|
+
if (opts.account !== undefined && !keepCurrentAccount && lane.state === "running") {
|
|
5807
|
+
throw new LanesError("lane_running", `Lane ${laneId} is running; stop it or wait for it to exit before moving its session`, {
|
|
5808
|
+
lane_id: laneId,
|
|
5809
|
+
state: lane.state,
|
|
5810
|
+
});
|
|
5811
|
+
}
|
|
5812
|
+
if (opts.account !== undefined && !keepCurrentAccount && !providerSessionPortable(lane.provider)) {
|
|
5813
|
+
throw new LanesError("provider_session_not_portable", sessionNotPortableMessage(lane.provider), { lane_id: laneId, provider: lane.provider });
|
|
5814
|
+
}
|
|
5815
|
+
// One decision point for how a follow-up reaches this lane (delivery.ts).
|
|
5816
|
+
// `none` refuses before anything is written: no inbox append, no spawn.
|
|
5817
|
+
const mechanism = laneDeliveryMechanism(lane);
|
|
5818
|
+
if (mechanism === "none") {
|
|
5819
|
+
const message = deliveryRefusalReason(lane);
|
|
5820
|
+
throw new LanesError("lane_not_accepting_messages", message, {
|
|
5821
|
+
lane_id: laneId,
|
|
5822
|
+
state: lane.state,
|
|
5823
|
+
provider: lane.provider,
|
|
5824
|
+
message,
|
|
5825
|
+
});
|
|
5826
|
+
}
|
|
5827
|
+
if (keepCurrentAccount && opts.model !== undefined && mechanism !== "resume") {
|
|
5828
|
+
throw new LanesError("model_requires_resume", "--model with --account requires a message that resumes the lane", { lane_id: laneId, state: lane.state });
|
|
5829
|
+
}
|
|
5830
|
+
if (opts.worker !== undefined && mechanism === "dropbox") {
|
|
5831
|
+
throw new LanesError("worker_profile_requires_resume", `Lane ${laneId} is still running; --worker can only be applied when message starts a resumed provider turn`, { lane_id: laneId, state: lane.state });
|
|
5832
|
+
}
|
|
5833
|
+
if (cliEffort !== undefined && mechanism === "dropbox") {
|
|
5834
|
+
throw new LanesError("lane_effort_requires_resume", `Lane ${laneId} is still running; --effort can only be applied when message starts a resumed provider turn`, { lane_id: laneId, state: lane.state });
|
|
5835
|
+
}
|
|
5836
|
+
const resumeProfile = opts.worker !== undefined
|
|
5837
|
+
? resolveResumeProfile(ctx, lane, opts.worker)
|
|
5838
|
+
: undefined;
|
|
5839
|
+
if (opts.model !== undefined && opts.account === undefined) {
|
|
5840
|
+
throw new LanesError("model_requires_account", "--model on hq lanes message is only accepted together with --account", {});
|
|
5841
|
+
}
|
|
5842
|
+
const automaticAccountSelection = opts.account === undefined && mechanism === "resume"
|
|
5843
|
+
? await selectAutomaticClaudeResumeAccount(ctx, lane, resumeProfile)
|
|
5844
|
+
: undefined;
|
|
5845
|
+
const resumeEffort = resolveResumeEffort(ctx, lane, {
|
|
5846
|
+
cliEffort,
|
|
5847
|
+
profile: resumeProfile,
|
|
5848
|
+
account: keepCurrentAccount ? lane.account : opts.account ?? automaticAccountSelection?.selection.account,
|
|
5849
|
+
model: opts.model,
|
|
5717
5850
|
});
|
|
5718
|
-
|
|
5719
|
-
|
|
5720
|
-
|
|
5721
|
-
|
|
5722
|
-
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
|
|
5851
|
+
const messageId = mintMessageId();
|
|
5852
|
+
const at = ctx.now().toISOString();
|
|
5853
|
+
const projectId = opts.project?.trim() || lane.project_id;
|
|
5854
|
+
const storyId = opts.story?.trim() || lane.story_id;
|
|
5855
|
+
if (opts.story !== undefined && !opts.story.trim()) {
|
|
5856
|
+
throw new LanesError("missing_story", "--story must be non-empty", {});
|
|
5857
|
+
}
|
|
5858
|
+
if ((opts.project !== undefined || opts.story !== undefined) && ctx.storyLedgerEnabled()) {
|
|
5859
|
+
validateProjectStories(hqRoot, projectId, [storyId], lane.company?.slug);
|
|
5860
|
+
}
|
|
5861
|
+
const framedMonitorEvent = opts.monitorEvent === undefined
|
|
5862
|
+
? undefined
|
|
5863
|
+
: formatMonitorEvent({
|
|
5864
|
+
monitorId: opts.monitorEvent.monitor_id,
|
|
5865
|
+
description: opts.monitorEvent.description,
|
|
5866
|
+
sequence: opts.monitorEvent.sequence,
|
|
5867
|
+
text,
|
|
5868
|
+
});
|
|
5869
|
+
const deliveryText = framedMonitorEvent ?? text;
|
|
5870
|
+
const dropboxMessage = {
|
|
5871
|
+
at,
|
|
5872
|
+
message_id: messageId,
|
|
5873
|
+
text,
|
|
5874
|
+
project_id: projectId,
|
|
5875
|
+
story_id: storyId,
|
|
5876
|
+
...(opts.monitorEvent === undefined ? {} : {
|
|
5877
|
+
kind: "monitor_event",
|
|
5878
|
+
monitor_id: opts.monitorEvent.monitor_id,
|
|
5879
|
+
description: opts.monitorEvent.description,
|
|
5880
|
+
sequence: opts.monitorEvent.sequence,
|
|
5881
|
+
}),
|
|
5882
|
+
};
|
|
5883
|
+
let resumed = null;
|
|
5884
|
+
let move = null;
|
|
5885
|
+
let queuedOnAdmission = false;
|
|
5886
|
+
if (opts.account !== undefined && !keepCurrentAccount) {
|
|
5887
|
+
// A moved message is always a resume on the target account: a running
|
|
5888
|
+
// lane (the dropbox mechanism) is refused inside prepareAccountMove.
|
|
5889
|
+
move = await prepareAccountMove(ctx, lane, {
|
|
5890
|
+
account: opts.account,
|
|
5891
|
+
cliModel: opts.model?.trim() || undefined,
|
|
5892
|
+
...(resumeProfile !== undefined ? { workerProfile: resumeProfile } : {}),
|
|
5893
|
+
...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
|
|
5894
|
+
pending: {
|
|
5895
|
+
trigger: "message",
|
|
5896
|
+
text: deliveryText,
|
|
5897
|
+
...(opts.monitorEvent === undefined ? {} : { monitor_event: true }),
|
|
5898
|
+
message_id: messageId,
|
|
5899
|
+
compact: opts.compact !== false,
|
|
5900
|
+
},
|
|
5901
|
+
});
|
|
5902
|
+
lane = move.lane;
|
|
5903
|
+
if (move.queued) {
|
|
5904
|
+
archiveResumeMessage(hqRoot, laneId, dropboxMessage);
|
|
5905
|
+
}
|
|
5906
|
+
}
|
|
5907
|
+
else if (mechanism === "resume") {
|
|
5908
|
+
const automatic = automaticAccountSelection;
|
|
5909
|
+
if (automatic !== undefined) {
|
|
5910
|
+
const selection = automatic.selection;
|
|
5911
|
+
if (selection.account !== lane.account) {
|
|
5912
|
+
move = await prepareAccountMove(ctx, lane, {
|
|
5913
|
+
account: selection.account,
|
|
5914
|
+
cliModel: undefined,
|
|
5915
|
+
...(automatic.historyReason !== undefined ? { accountHistoryReason: automatic.historyReason } : {}),
|
|
5916
|
+
...(resumeProfile !== undefined ? { workerProfile: resumeProfile } : {}),
|
|
5917
|
+
...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
|
|
5918
|
+
pending: {
|
|
5919
|
+
trigger: "message",
|
|
5920
|
+
text: deliveryText,
|
|
5921
|
+
...(opts.monitorEvent === undefined ? {} : { monitor_event: true }),
|
|
5922
|
+
message_id: messageId,
|
|
5923
|
+
compact: opts.compact !== false,
|
|
5924
|
+
},
|
|
5925
|
+
accountSelection: selection,
|
|
5926
|
+
});
|
|
5927
|
+
lane = move.lane;
|
|
5928
|
+
if (move.queued)
|
|
5929
|
+
archiveResumeMessage(hqRoot, laneId, dropboxMessage);
|
|
5728
5930
|
}
|
|
5729
|
-
|
|
5730
|
-
|
|
5731
|
-
|
|
5732
|
-
|
|
5733
|
-
|
|
5734
|
-
|
|
5735
|
-
|
|
5736
|
-
|
|
5737
|
-
|
|
5738
|
-
|
|
5739
|
-
|
|
5740
|
-
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
|
|
5746
|
-
|
|
5747
|
-
|
|
5748
|
-
|
|
5749
|
-
|
|
5750
|
-
|
|
5751
|
-
|
|
5752
|
-
|
|
5753
|
-
|
|
5931
|
+
else {
|
|
5932
|
+
lane = persistClaudeAccountSelection(hqRoot, lane, selection);
|
|
5933
|
+
}
|
|
5934
|
+
}
|
|
5935
|
+
}
|
|
5936
|
+
if (resumeProfile !== undefined && (opts.account === undefined || keepCurrentAccount)) {
|
|
5937
|
+
validateResumeProfileModel(ctx, lane, resumeProfile.profile);
|
|
5938
|
+
}
|
|
5939
|
+
if (move === null && (resumeProfile !== undefined || resumeEffort !== undefined || (keepCurrentAccount && opts.model !== undefined))) {
|
|
5940
|
+
lane = recordResumeSetup(ctx, lane, {
|
|
5941
|
+
...(resumeProfile !== undefined ? { profile: resumeProfile } : {}),
|
|
5942
|
+
...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
|
|
5943
|
+
...(keepCurrentAccount && opts.model !== undefined ? { cliModel: opts.model } : {}),
|
|
5944
|
+
});
|
|
5945
|
+
}
|
|
5946
|
+
if (move?.queued) {
|
|
5947
|
+
// Parked at the target's cap: reconcile resumes the session there.
|
|
5948
|
+
markAccepted();
|
|
5949
|
+
}
|
|
5950
|
+
else {
|
|
5951
|
+
const dispatched = await dispatchLaneDelivery(lane, {
|
|
5952
|
+
dropbox: () => {
|
|
5953
|
+
// Live (or about-to-be-live) process: the PreToolUse drain delivers it.
|
|
5954
|
+
if (!laneAcceptsOperatorMessage(lane)) {
|
|
5955
|
+
throw new LanesError("lane_not_accepting_messages", operatorMessageRefusalReason(lane.state), { lane_id: laneId, state: lane.state });
|
|
5956
|
+
}
|
|
5957
|
+
appendDropboxMessage(hqRoot, laneId, dropboxMessage);
|
|
5958
|
+
return null;
|
|
5959
|
+
},
|
|
5960
|
+
resume: async () => {
|
|
5961
|
+
// Exited worker with a recorded session: queue the message when capacity
|
|
5962
|
+
// is occupied, otherwise start its next turn under the shared admission lock.
|
|
5963
|
+
assertLaneRunsHereForAdmission(ctx, lane);
|
|
5964
|
+
return withLaneAdmissionLock(hqRoot, lane.provider, async () => {
|
|
5965
|
+
const admission = await assessLaneAdmission(ctx, lane, { queueAt: at });
|
|
5966
|
+
if (admission.action === "refuse" && admission.error)
|
|
5967
|
+
throw admission.error;
|
|
5968
|
+
return withResumeHireAdmission(ctx, lane, () => {
|
|
5969
|
+
if (admission.action === "queue") {
|
|
5970
|
+
const sessionId = lane.provider_session_id;
|
|
5971
|
+
if (!sessionId) {
|
|
5972
|
+
throw new LanesError("no_provider_session", `Lane ${laneId} has no provider session id to queue this message resume`, {
|
|
5754
5973
|
lane_id: laneId,
|
|
5755
|
-
|
|
5756
|
-
version: current.version,
|
|
5757
|
-
state: current.state,
|
|
5974
|
+
provider: lane.provider,
|
|
5758
5975
|
});
|
|
5759
5976
|
}
|
|
5760
|
-
|
|
5761
|
-
|
|
5762
|
-
|
|
5763
|
-
|
|
5764
|
-
|
|
5765
|
-
|
|
5766
|
-
|
|
5767
|
-
|
|
5768
|
-
|
|
5769
|
-
|
|
5770
|
-
|
|
5771
|
-
|
|
5772
|
-
|
|
5773
|
-
|
|
5774
|
-
|
|
5775
|
-
|
|
5776
|
-
|
|
5777
|
-
|
|
5778
|
-
|
|
5779
|
-
|
|
5780
|
-
|
|
5781
|
-
|
|
5977
|
+
archiveResumeMessage(hqRoot, laneId, dropboxMessage);
|
|
5978
|
+
queuedOnAdmission = true;
|
|
5979
|
+
return casUpdateLane(hqRoot, laneId, (current) => {
|
|
5980
|
+
if (current.version !== lane.version || current.state !== lane.state) {
|
|
5981
|
+
throw new LanesError("lane_admission_conflict", `Lane ${laneId} changed while message admission was checked`, {
|
|
5982
|
+
lane_id: laneId,
|
|
5983
|
+
prepared_version: lane.version,
|
|
5984
|
+
version: current.version,
|
|
5985
|
+
state: current.state,
|
|
5986
|
+
});
|
|
5987
|
+
}
|
|
5988
|
+
return {
|
|
5989
|
+
...current,
|
|
5990
|
+
state: "queued",
|
|
5991
|
+
queued_reason: admission.reason ?? "throttle",
|
|
5992
|
+
queued_at: at,
|
|
5993
|
+
pending_resume: {
|
|
5994
|
+
session_id: sessionId,
|
|
5995
|
+
trigger: "message",
|
|
5996
|
+
text: deliveryText,
|
|
5997
|
+
...(opts.monitorEvent === undefined ? {} : { monitor_event: true }),
|
|
5998
|
+
message_id: messageId,
|
|
5999
|
+
compact: opts.compact !== false,
|
|
6000
|
+
at,
|
|
6001
|
+
},
|
|
6002
|
+
};
|
|
6003
|
+
});
|
|
6004
|
+
}
|
|
6005
|
+
const started = spawnLaneWorker(hqRoot, lane, {
|
|
5782
6006
|
env: ctx.env,
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
|
|
5786
|
-
|
|
5787
|
-
|
|
5788
|
-
|
|
5789
|
-
|
|
5790
|
-
|
|
5791
|
-
|
|
5792
|
-
|
|
5793
|
-
|
|
5794
|
-
|
|
5795
|
-
|
|
5796
|
-
|
|
5797
|
-
|
|
5798
|
-
|
|
5799
|
-
|
|
5800
|
-
|
|
6007
|
+
modelDeps: {
|
|
6008
|
+
readFileSync: ctx.readFileSync,
|
|
6009
|
+
probe: ctx.probeProvider,
|
|
6010
|
+
env: ctx.env,
|
|
6011
|
+
},
|
|
6012
|
+
existsSync: ctx.existsSync,
|
|
6013
|
+
spawnProcess: ctx.spawnProcess,
|
|
6014
|
+
proveDetached: ctx.proveDetached,
|
|
6015
|
+
killProcess: ctx.killProcess,
|
|
6016
|
+
now: ctx.now,
|
|
6017
|
+
resolveMachineIdentity: ctx.resolveMachineIdentity,
|
|
6018
|
+
runCompaction: ctx.runCompaction,
|
|
6019
|
+
locateTranscript: ctx.locateTranscript,
|
|
6020
|
+
}, {
|
|
6021
|
+
trigger: {
|
|
6022
|
+
kind: "message",
|
|
6023
|
+
text: deliveryText,
|
|
6024
|
+
message_id: messageId,
|
|
6025
|
+
...(opts.monitorEvent === undefined ? {} : { monitor_event: true }),
|
|
6026
|
+
},
|
|
6027
|
+
compact: opts.compact !== false,
|
|
6028
|
+
effort: resumeEffort ?? recordedResumeEffort(lane),
|
|
6029
|
+
});
|
|
6030
|
+
archiveResumeMessage(hqRoot, laneId, dropboxMessage);
|
|
6031
|
+
return started;
|
|
5801
6032
|
});
|
|
5802
|
-
|
|
5803
|
-
|
|
5804
|
-
|
|
5805
|
-
|
|
5806
|
-
|
|
5807
|
-
|
|
5808
|
-
|
|
5809
|
-
|
|
5810
|
-
|
|
5811
|
-
|
|
5812
|
-
|
|
5813
|
-
|
|
5814
|
-
|
|
5815
|
-
|
|
5816
|
-
|
|
5817
|
-
|
|
5818
|
-
|
|
5819
|
-
|
|
5820
|
-
|
|
5821
|
-
return {
|
|
5822
|
-
...current,
|
|
5823
|
-
project_id: projectId,
|
|
5824
|
-
story_id: storyId,
|
|
5825
|
-
no_story_reason: storyId ? undefined : current.no_story_reason,
|
|
5826
|
-
stories: laneStory(current, projectId, storyId)
|
|
5827
|
-
? laneStories(current)
|
|
5828
|
-
: [
|
|
5829
|
-
...laneStories(current),
|
|
5830
|
-
{
|
|
5831
|
-
project_id: projectId,
|
|
5832
|
-
story_id: storyId,
|
|
5833
|
-
status: "in_progress",
|
|
5834
|
-
added_at: at,
|
|
5835
|
-
updated_at: at,
|
|
5836
|
-
},
|
|
5837
|
-
],
|
|
5838
|
-
attribution_history: appendCappedHistory(current.attribution_history, {
|
|
6033
|
+
}, ctx.sleep);
|
|
6034
|
+
},
|
|
6035
|
+
}, mechanism);
|
|
6036
|
+
if (dispatched.mechanism === "resume")
|
|
6037
|
+
resumed = dispatched.value;
|
|
6038
|
+
markAccepted();
|
|
6039
|
+
}
|
|
6040
|
+
// Every accepted message moves the compaction reference forward.
|
|
6041
|
+
let next = casUpdateLane(hqRoot, laneId, (current) => ({
|
|
6042
|
+
...resetKeepAliveProgress(current),
|
|
6043
|
+
last_message_at: at,
|
|
6044
|
+
}));
|
|
6045
|
+
if (projectId !== lane.project_id || storyId !== lane.story_id) {
|
|
6046
|
+
next = casUpdateLane(hqRoot, laneId, (current) => {
|
|
6047
|
+
const already = current.attribution_history.some((entry) => entry.message_id === messageId);
|
|
6048
|
+
if (already)
|
|
6049
|
+
return current;
|
|
6050
|
+
return {
|
|
6051
|
+
...current,
|
|
5839
6052
|
project_id: projectId,
|
|
5840
6053
|
story_id: storyId,
|
|
5841
|
-
|
|
5842
|
-
|
|
5843
|
-
|
|
5844
|
-
|
|
6054
|
+
no_story_reason: storyId ? undefined : current.no_story_reason,
|
|
6055
|
+
stories: laneStory(current, projectId, storyId)
|
|
6056
|
+
? laneStories(current)
|
|
6057
|
+
: [
|
|
6058
|
+
...laneStories(current),
|
|
6059
|
+
{
|
|
6060
|
+
project_id: projectId,
|
|
6061
|
+
story_id: storyId,
|
|
6062
|
+
status: "in_progress",
|
|
6063
|
+
added_at: at,
|
|
6064
|
+
updated_at: at,
|
|
6065
|
+
},
|
|
6066
|
+
],
|
|
6067
|
+
attribution_history: appendCappedHistory(current.attribution_history, {
|
|
6068
|
+
project_id: projectId,
|
|
6069
|
+
story_id: storyId,
|
|
6070
|
+
message_id: messageId,
|
|
6071
|
+
at,
|
|
6072
|
+
}),
|
|
6073
|
+
};
|
|
6074
|
+
});
|
|
6075
|
+
}
|
|
6076
|
+
// What this command can claim when it returns: the message is durably
|
|
6077
|
+
// written (accepted). Delivery to the model is asynchronous in both
|
|
6078
|
+
// mechanisms. `dropbox` delivers on the worker's next tool call, and only
|
|
6079
|
+
// if the drain hook is registered in the account home the worker was
|
|
6080
|
+
// started from; `resume` has started a new worker with the message as its
|
|
6081
|
+
// prompt. Neither is "delivered", so the response never says so.
|
|
6082
|
+
const drainHook = drainHookStatusFor(ctx, next);
|
|
6083
|
+
const deliveryStatus = move?.queued
|
|
6084
|
+
? "queued_on_target_account"
|
|
6085
|
+
: queuedOnAdmission
|
|
6086
|
+
? "queued_on_admission"
|
|
6087
|
+
: mechanism === "dropbox"
|
|
6088
|
+
? "awaiting_next_tool_call"
|
|
6089
|
+
: "worker_spawned";
|
|
6090
|
+
const warning = mechanism === "dropbox" && drainHook !== null && !drainHook.registered
|
|
6091
|
+
? `The drain hook is not registered in ${drainHook.path}; the running worker will not receive this message. It stays in the inbox and is delivered on the lane's next start.`
|
|
6092
|
+
: mechanism === "dropbox" && next.stalled === true
|
|
6093
|
+
? `The worker has made no tool call since ${next.stalled_at ?? "an unknown time"} (${next.stalled_reason ?? "stalled"}); delivery waits on its next tool call.`
|
|
6094
|
+
: null;
|
|
6095
|
+
if (!opts.silent)
|
|
6096
|
+
printJson({
|
|
6097
|
+
ok: true,
|
|
6098
|
+
action: "message",
|
|
6099
|
+
lane_id: laneId,
|
|
6100
|
+
message_id: messageId,
|
|
6101
|
+
accepted: true,
|
|
6102
|
+
delivered: false,
|
|
6103
|
+
delivery: mechanism,
|
|
6104
|
+
delivery_status: deliveryStatus,
|
|
6105
|
+
inbox_path: mechanism === "dropbox" ? dropboxInboxPath(hqRoot, laneId) : null,
|
|
6106
|
+
drain_hook: drainHook,
|
|
6107
|
+
warning,
|
|
6108
|
+
resumed_session_id: resumed?.last_resume?.session_id ?? null,
|
|
6109
|
+
compaction: resumed?.last_compaction && resumed.last_compaction.resume_at === resumed.last_resume?.at
|
|
6110
|
+
? resumed.last_compaction
|
|
6111
|
+
: null,
|
|
6112
|
+
account_move: move ? accountMoveSummary(move) : null,
|
|
6113
|
+
project_id: next.project_id,
|
|
6114
|
+
story_id: next.story_id,
|
|
6115
|
+
stories: laneStories(next),
|
|
6116
|
+
story_warning: storyWarning(next) ?? null,
|
|
6117
|
+
lane: next,
|
|
6118
|
+
});
|
|
6119
|
+
}
|
|
6120
|
+
catch (err) {
|
|
6121
|
+
if (claimedMessages.length > 0 && !accepted) {
|
|
6122
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
6123
|
+
writeWatcherDecision(watcherOutLogPath(hqRoot, laneId), {
|
|
6124
|
+
event: "message_recovery_failed",
|
|
6125
|
+
at: ctx.now().toISOString(),
|
|
6126
|
+
state: lane.state,
|
|
6127
|
+
error,
|
|
6128
|
+
message_count: claimedMessages.length,
|
|
6129
|
+
}, WATCHER_OUT_MAX_BYTES);
|
|
6130
|
+
try {
|
|
6131
|
+
requeueDropboxMessages(hqRoot, laneId, claimedMessages);
|
|
6132
|
+
}
|
|
6133
|
+
catch (requeueErr) {
|
|
6134
|
+
const requeueMessage = requeueErr instanceof Error ? requeueErr.message : String(requeueErr);
|
|
6135
|
+
writeWatcherDecision(watcherOutLogPath(hqRoot, laneId), {
|
|
6136
|
+
event: "message_requeue_failed",
|
|
6137
|
+
at: ctx.now().toISOString(),
|
|
6138
|
+
state: lane.state,
|
|
6139
|
+
recovery_error: error,
|
|
6140
|
+
requeue_error: requeueMessage,
|
|
6141
|
+
message_count: claimedMessages.length,
|
|
6142
|
+
}, WATCHER_OUT_MAX_BYTES);
|
|
6143
|
+
throw new LanesError("message_requeue_failed", `Could not return ${claimedMessages.length} queued lane message(s) to the inbox after recovery failed: ${requeueMessage}`, { lane_id: laneId, message_count: claimedMessages.length, recovery_error: error });
|
|
6144
|
+
}
|
|
6145
|
+
}
|
|
6146
|
+
throw err;
|
|
6147
|
+
}
|
|
6148
|
+
}
|
|
6149
|
+
function messageLaneExclusion(ctx, lane) {
|
|
6150
|
+
if (lane.state === "killed" || lane.state === "cancelled")
|
|
6151
|
+
return lane.state;
|
|
6152
|
+
return keepAliveExclusion(lane, loadAllLanes(ctx.hqRoot()), loadRetiredLaneIds(ctx.hqRoot()));
|
|
6153
|
+
}
|
|
6154
|
+
function recordMessageUndeliverable(ctx, lane, reason, messageCount) {
|
|
6155
|
+
writeWatcherDecision(watcherOutLogPath(ctx.hqRoot(), lane.lane_id), {
|
|
6156
|
+
event: "message_undeliverable",
|
|
6157
|
+
at: ctx.now().toISOString(),
|
|
6158
|
+
state: lane.state,
|
|
6159
|
+
reason,
|
|
6160
|
+
...(messageCount === undefined ? {} : { message_count: messageCount }),
|
|
6161
|
+
}, WATCHER_OUT_MAX_BYTES);
|
|
6162
|
+
}
|
|
6163
|
+
/**
|
|
6164
|
+
* Watcher and reconcile call the hidden command below after they win the
|
|
6165
|
+
* terminal transition. The inbox is claimed once with the normal atomic
|
|
6166
|
+
* dropbox drain, and an accepted message is routed through runMessage so the
|
|
6167
|
+
* existing provider, admission, account, worker-profile and senior checks
|
|
6168
|
+
* remain authoritative.
|
|
6169
|
+
*/
|
|
6170
|
+
async function recoverQueuedMessages(ctx, laneId) {
|
|
6171
|
+
const hqRoot = ctx.hqRoot();
|
|
6172
|
+
const lane = readLane(hqRoot, laneId);
|
|
6173
|
+
if (!dropboxInboxHasMessages(hqRoot, laneId))
|
|
6174
|
+
return { action: "none", message_count: 0 };
|
|
6175
|
+
const exclusion = messageLaneExclusion(ctx, lane);
|
|
6176
|
+
const refusal = exclusion ?? (lane.state === "done"
|
|
6177
|
+
? "done_before_envelope"
|
|
6178
|
+
: lane.state === "awaiting_input"
|
|
6179
|
+
? "awaiting_input_requires_question_answer"
|
|
6180
|
+
: laneDeliveryMechanism(lane) !== "resume"
|
|
6181
|
+
? `no_resume_path_for_${lane.state}`
|
|
6182
|
+
: undefined);
|
|
6183
|
+
if (refusal !== undefined) {
|
|
6184
|
+
const messages = drainDropbox(hqRoot, laneId, { archiveDirectory: "undeliverable" });
|
|
6185
|
+
if (messages.length > 0)
|
|
6186
|
+
recordMessageUndeliverable(ctx, lane, refusal, messages.length);
|
|
6187
|
+
return { action: messages.length > 0 ? "undeliverable" : "none", message_count: messages.length };
|
|
6188
|
+
}
|
|
6189
|
+
const messages = drainDropbox(hqRoot, laneId);
|
|
6190
|
+
if (messages.length === 0)
|
|
6191
|
+
return { action: "none", message_count: 0 };
|
|
6192
|
+
let accepted = false;
|
|
6193
|
+
try {
|
|
6194
|
+
await runMessage(ctx, laneId, {
|
|
6195
|
+
text: formatDropboxMessages(messages),
|
|
6196
|
+
compact: false,
|
|
6197
|
+
silent: true,
|
|
6198
|
+
onAccepted: () => { accepted = true; },
|
|
5845
6199
|
});
|
|
6200
|
+
return { action: "resumed", message_count: messages.length };
|
|
6201
|
+
}
|
|
6202
|
+
catch (err) {
|
|
6203
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
6204
|
+
writeWatcherDecision(watcherOutLogPath(hqRoot, laneId), {
|
|
6205
|
+
event: "message_recovery_failed",
|
|
6206
|
+
at: ctx.now().toISOString(),
|
|
6207
|
+
state: lane.state,
|
|
6208
|
+
error,
|
|
6209
|
+
message_count: messages.length,
|
|
6210
|
+
}, WATCHER_OUT_MAX_BYTES);
|
|
6211
|
+
if (!accepted) {
|
|
6212
|
+
try {
|
|
6213
|
+
requeueDropboxMessages(hqRoot, laneId, messages);
|
|
6214
|
+
}
|
|
6215
|
+
catch (requeueErr) {
|
|
6216
|
+
const requeueMessage = requeueErr instanceof Error ? requeueErr.message : String(requeueErr);
|
|
6217
|
+
writeWatcherDecision(watcherOutLogPath(hqRoot, laneId), {
|
|
6218
|
+
event: "message_requeue_failed",
|
|
6219
|
+
at: ctx.now().toISOString(),
|
|
6220
|
+
state: lane.state,
|
|
6221
|
+
recovery_error: error,
|
|
6222
|
+
requeue_error: requeueMessage,
|
|
6223
|
+
message_count: messages.length,
|
|
6224
|
+
}, WATCHER_OUT_MAX_BYTES);
|
|
6225
|
+
throw new LanesError("message_requeue_failed", `Could not return ${messages.length} queued lane message(s) to the inbox after recovery failed: ${requeueMessage}`, { lane_id: laneId, message_count: messages.length, recovery_error: error });
|
|
6226
|
+
}
|
|
6227
|
+
}
|
|
6228
|
+
throw err;
|
|
5846
6229
|
}
|
|
5847
|
-
// What this command can claim when it returns: the message is durably
|
|
5848
|
-
// written (accepted). Delivery to the model is asynchronous in both
|
|
5849
|
-
// mechanisms. `dropbox` delivers on the worker's next tool call, and only
|
|
5850
|
-
// if the drain hook is registered in the account home the worker was
|
|
5851
|
-
// started from; `resume` has started a new worker with the message as its
|
|
5852
|
-
// prompt. Neither is "delivered", so the response never says so.
|
|
5853
|
-
const drainHook = drainHookStatusFor(ctx, next);
|
|
5854
|
-
const deliveryStatus = move?.queued
|
|
5855
|
-
? "queued_on_target_account"
|
|
5856
|
-
: queuedOnAdmission
|
|
5857
|
-
? "queued_on_admission"
|
|
5858
|
-
: mechanism === "dropbox"
|
|
5859
|
-
? "awaiting_next_tool_call"
|
|
5860
|
-
: "worker_spawned";
|
|
5861
|
-
const warning = mechanism === "dropbox" && drainHook !== null && !drainHook.registered
|
|
5862
|
-
? `The drain hook is not registered in ${drainHook.path}; the running worker will not receive this message. It stays in the inbox and is delivered on the lane's next start.`
|
|
5863
|
-
: mechanism === "dropbox" && next.stalled === true
|
|
5864
|
-
? `The worker has made no tool call since ${next.stalled_at ?? "an unknown time"} (${next.stalled_reason ?? "stalled"}); delivery waits on its next tool call.`
|
|
5865
|
-
: null;
|
|
5866
|
-
printJson({
|
|
5867
|
-
ok: true,
|
|
5868
|
-
action: "message",
|
|
5869
|
-
lane_id: laneId,
|
|
5870
|
-
message_id: messageId,
|
|
5871
|
-
accepted: true,
|
|
5872
|
-
delivered: false,
|
|
5873
|
-
delivery: mechanism,
|
|
5874
|
-
delivery_status: deliveryStatus,
|
|
5875
|
-
inbox_path: mechanism === "dropbox" ? dropboxInboxPath(hqRoot, laneId) : null,
|
|
5876
|
-
drain_hook: drainHook,
|
|
5877
|
-
warning,
|
|
5878
|
-
resumed_session_id: resumed?.last_resume?.session_id ?? null,
|
|
5879
|
-
compaction: resumed?.last_compaction && resumed.last_compaction.resume_at === resumed.last_resume?.at
|
|
5880
|
-
? resumed.last_compaction
|
|
5881
|
-
: null,
|
|
5882
|
-
account_move: move ? accountMoveSummary(move) : null,
|
|
5883
|
-
project_id: next.project_id,
|
|
5884
|
-
story_id: next.story_id,
|
|
5885
|
-
stories: laneStories(next),
|
|
5886
|
-
story_warning: storyWarning(next) ?? null,
|
|
5887
|
-
lane: next,
|
|
5888
|
-
});
|
|
5889
6230
|
}
|
|
5890
6231
|
function accountMoveSummary(move) {
|
|
5891
6232
|
return {
|
|
@@ -7219,7 +7560,7 @@ async function runAsk(ctx, opts) {
|
|
|
7219
7560
|
const attempts = ASK_ATTEMPTS;
|
|
7220
7561
|
for (let i = 0; i < attempts; i += 1) {
|
|
7221
7562
|
try {
|
|
7222
|
-
|
|
7563
|
+
let withBody = raiseLaneQuestion({
|
|
7223
7564
|
hqRoot,
|
|
7224
7565
|
lane,
|
|
7225
7566
|
body,
|
|
@@ -7229,6 +7570,9 @@ async function runAsk(ctx, opts) {
|
|
|
7229
7570
|
questionId,
|
|
7230
7571
|
});
|
|
7231
7572
|
questionId = withBody.question_id;
|
|
7573
|
+
if (await ctx.laneTrackerMeshEnabled(questionCompany)) {
|
|
7574
|
+
withBody = await indexTrackerQuestion(ctx, hqRoot, lane, withBody, body, questionCompany);
|
|
7575
|
+
}
|
|
7232
7576
|
await emitLaneMeshEvent(ctx, lane.mesh_company ?? lane.company, lane.thread_id, "lane_question", {
|
|
7233
7577
|
lane_id: lane.lane_id,
|
|
7234
7578
|
question_id: withBody.question_id,
|
|
@@ -7615,18 +7959,28 @@ async function runQuestionsList(ctx, opts) {
|
|
|
7615
7959
|
const outpostLoad = opts.allHosts
|
|
7616
7960
|
? await loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, company)
|
|
7617
7961
|
: { questions: [], errors: [] };
|
|
7962
|
+
const meshRows = await ctx.laneTrackerMeshEnabled(company) && status !== "expired"
|
|
7963
|
+
? await createTrackerMeshQuestionClient({ company, request: ctx.trackerMeshRequest }).list(status ? [status] : undefined)
|
|
7964
|
+
: [];
|
|
7618
7965
|
for (const owner of outpostLoad.questions) {
|
|
7619
7966
|
assertQuestionCompanyAccess(owner.question, owner.lane, company, hqRoot);
|
|
7620
7967
|
syncPrimaryQuestionDelivery(hqRoot, owner);
|
|
7621
7968
|
}
|
|
7622
7969
|
const remoteQuestionIds = new Set(outpostLoad.questions.map(({ question }) => question.question_id));
|
|
7970
|
+
const localMeshQuestionIds = new Set(localQuestions.map((question) => question.mesh_question_id).filter((id) => Boolean(id)));
|
|
7623
7971
|
const questions = [
|
|
7624
7972
|
...localQuestions
|
|
7625
7973
|
.filter((q) => !remoteQuestionIds.has(q.question_id))
|
|
7626
7974
|
.map((q) => {
|
|
7975
|
+
const meshRow = q.mesh_question_id ? meshRows.find((row) => row.questionId === q.mesh_question_id) : undefined;
|
|
7627
7976
|
const row = {
|
|
7628
7977
|
...q,
|
|
7629
7978
|
audience: resolveAudience(hqRoot, q, ctx),
|
|
7979
|
+
...(meshRow ? {
|
|
7980
|
+
mesh_question_id: meshRow.questionId,
|
|
7981
|
+
body_preview: { question: meshRow.question, context: meshRow.context ?? "", options: meshRow.options, ...(meshRow.recommended ? { recommended: meshRow.recommended } : {}) },
|
|
7982
|
+
host: `${meshRow.host.kind}:${meshRow.host.id}`,
|
|
7983
|
+
} : {}),
|
|
7630
7984
|
...(q.question_kind === "continue" ? { flag: "continue-loop" } : {}),
|
|
7631
7985
|
};
|
|
7632
7986
|
if (!opts.allHosts)
|
|
@@ -7658,6 +8012,50 @@ async function runQuestionsList(ctx, opts) {
|
|
|
7658
8012
|
audience,
|
|
7659
8013
|
...(q.question_kind === "continue" ? { flag: "continue-loop" } : {}),
|
|
7660
8014
|
})),
|
|
8015
|
+
...meshRows
|
|
8016
|
+
.filter((row) => {
|
|
8017
|
+
if (localMeshQuestionIds.has(row.questionId))
|
|
8018
|
+
return false;
|
|
8019
|
+
if (status && row.status !== status)
|
|
8020
|
+
return false;
|
|
8021
|
+
if (opts.from && row.laneId !== opts.from)
|
|
8022
|
+
return false;
|
|
8023
|
+
if (opts.project && row.projectId !== opts.project)
|
|
8024
|
+
return false;
|
|
8025
|
+
if (opts.story) {
|
|
8026
|
+
const lane = localLanes.get(row.laneId);
|
|
8027
|
+
if (!lane || lane.story_id !== opts.story)
|
|
8028
|
+
return false;
|
|
8029
|
+
}
|
|
8030
|
+
const audience = row.audience === "owner"
|
|
8031
|
+
? { kind: "user" }
|
|
8032
|
+
: row.audience === "operator"
|
|
8033
|
+
? { kind: "session", id: "mesh" }
|
|
8034
|
+
: { kind: "lane", id: row.laneId };
|
|
8035
|
+
if (forFilter && !audienceMatches(audience, forFilter))
|
|
8036
|
+
return false;
|
|
8037
|
+
return true;
|
|
8038
|
+
})
|
|
8039
|
+
.map((row) => ({
|
|
8040
|
+
question_id: row.questionId,
|
|
8041
|
+
mesh_question_id: row.questionId,
|
|
8042
|
+
lane_id: row.laneId,
|
|
8043
|
+
company,
|
|
8044
|
+
project_id: row.projectId,
|
|
8045
|
+
story_id: localLanes.get(row.laneId)?.story_id ?? "",
|
|
8046
|
+
...(row.threadId ? { thread_id: row.threadId } : {}),
|
|
8047
|
+
asked_at: row.askedAt,
|
|
8048
|
+
origin: { kind: "lane", id: row.laneId },
|
|
8049
|
+
hop: row.routingHistory.length || 1,
|
|
8050
|
+
status: row.status,
|
|
8051
|
+
audience: row.audience,
|
|
8052
|
+
host: `${row.host.kind}:${row.host.id}`,
|
|
8053
|
+
question: row.question,
|
|
8054
|
+
context: row.context ?? "",
|
|
8055
|
+
options: row.options,
|
|
8056
|
+
...(row.recommended ? { recommended: row.recommended } : {}),
|
|
8057
|
+
...(row.choice !== undefined ? { answer: { choice: row.choice, answered_by: row.answeredBy, answered_at: row.answeredAt, notes: row.notes } } : {}),
|
|
8058
|
+
})),
|
|
7661
8059
|
];
|
|
7662
8060
|
printJson({
|
|
7663
8061
|
ok: true,
|
|
@@ -7675,6 +8073,43 @@ async function runQuestionsShow(ctx, questionId) {
|
|
|
7675
8073
|
catch (err) {
|
|
7676
8074
|
if (!isLanesError(err) || err.error !== "question_not_found")
|
|
7677
8075
|
throw err;
|
|
8076
|
+
let meshCompany = null;
|
|
8077
|
+
try {
|
|
8078
|
+
meshCompany = await resolveActiveQuestionCompany(ctx, "Cannot look up Work Mesh question without an active company");
|
|
8079
|
+
}
|
|
8080
|
+
catch (companyError) {
|
|
8081
|
+
if (questionRecordRefusalCode(companyError) !== "question_company_unavailable")
|
|
8082
|
+
throw companyError;
|
|
8083
|
+
// The Outpost lookup below remains available to unscoped primary hosts.
|
|
8084
|
+
}
|
|
8085
|
+
if (meshCompany && await ctx.laneTrackerMeshEnabled(meshCompany)) {
|
|
8086
|
+
const rows = await createTrackerMeshQuestionClient({ company: meshCompany, request: ctx.trackerMeshRequest }).list();
|
|
8087
|
+
const matches = rows.filter((row) => row.questionId === questionId);
|
|
8088
|
+
if (matches.length > 1)
|
|
8089
|
+
throw new LanesError("tracker_mesh_question_ambiguous", `Work Mesh question ${questionId} is duplicated in the company inbox`, { question_id: questionId });
|
|
8090
|
+
const row = matches[0];
|
|
8091
|
+
if (row) {
|
|
8092
|
+
printJson({
|
|
8093
|
+
ok: true,
|
|
8094
|
+
action: "questions.show",
|
|
8095
|
+
question: {
|
|
8096
|
+
question_id: row.questionId,
|
|
8097
|
+
mesh_question_id: row.questionId,
|
|
8098
|
+
lane_id: row.laneId,
|
|
8099
|
+
company: meshCompany,
|
|
8100
|
+
project_id: row.projectId,
|
|
8101
|
+
thread_id: row.threadId,
|
|
8102
|
+
asked_at: row.askedAt,
|
|
8103
|
+
status: row.status,
|
|
8104
|
+
audience: row.audience,
|
|
8105
|
+
host: `${row.host.kind}:${row.host.id}`,
|
|
8106
|
+
},
|
|
8107
|
+
audience: row.audience,
|
|
8108
|
+
body: { question: row.question, context: row.context ?? "", options: row.options, ...(row.recommended ? { recommended: row.recommended } : {}), ...(row.notes ? { notes: row.notes } : {}) },
|
|
8109
|
+
});
|
|
8110
|
+
return;
|
|
8111
|
+
}
|
|
8112
|
+
}
|
|
7678
8113
|
const lookup = await loadPrimaryOwnedOutpostQuestions(ctx, hqRoot);
|
|
7679
8114
|
if (lookup.errors.length > 0) {
|
|
7680
8115
|
throw new LanesError("outpost_question_lookup_unavailable", `Could not verify whether Outposts own question ${questionId}`, {
|
|
@@ -7730,6 +8165,42 @@ async function runQuestionsAnswer(ctx, questionId, opts) {
|
|
|
7730
8165
|
if (!isLanesError(err) || err.error !== "question_not_found")
|
|
7731
8166
|
throw err;
|
|
7732
8167
|
const company = explicitCompany ?? await resolveActiveQuestionCompany(ctx, "Cannot look up this lane question because the active HQ company could not be resolved; re-run with --company <slug>");
|
|
8168
|
+
if (await ctx.laneTrackerMeshEnabled(company)) {
|
|
8169
|
+
const client = createTrackerMeshQuestionClient({ company, request: ctx.trackerMeshRequest });
|
|
8170
|
+
const matches = (await client.list(["pending", "answered"])).filter((row) => row.questionId === questionId);
|
|
8171
|
+
if (matches.length > 1)
|
|
8172
|
+
throw new LanesError("tracker_mesh_question_ambiguous", `Work Mesh question ${questionId} is duplicated in the company inbox`, { question_id: questionId });
|
|
8173
|
+
const row = matches[0];
|
|
8174
|
+
if (row) {
|
|
8175
|
+
if (row.status !== "pending")
|
|
8176
|
+
throw new LanesError("question_not_pending", `Question ${questionId} is ${row.status}`, { question_id: questionId, status: row.status });
|
|
8177
|
+
const freeform = opts.freeform?.trim() || undefined;
|
|
8178
|
+
if (freeform !== undefined && row.options.length === 0)
|
|
8179
|
+
throw new LanesError("freeform_not_allowed", "--freeform is only allowed for a question with options", { question_id: questionId });
|
|
8180
|
+
if (!freeform && !opts.choice)
|
|
8181
|
+
throw new LanesError("missing_choice", "--choice or --freeform is required", {});
|
|
8182
|
+
const choice = freeform ?? resolveAnswerChoice(opts.choice, row.options, false);
|
|
8183
|
+
const answer = await client.answer(questionId, choice, opts.notes);
|
|
8184
|
+
printJson({
|
|
8185
|
+
ok: true,
|
|
8186
|
+
action: "questions.answer",
|
|
8187
|
+
lane_id: answer.laneId,
|
|
8188
|
+
question: {
|
|
8189
|
+
question_id: answer.questionId,
|
|
8190
|
+
mesh_question_id: answer.questionId,
|
|
8191
|
+
lane_id: answer.laneId,
|
|
8192
|
+
project_id: answer.projectId,
|
|
8193
|
+
status: answer.status,
|
|
8194
|
+
choice: answer.choice,
|
|
8195
|
+
answered_by: answer.answeredBy,
|
|
8196
|
+
answered_at: answer.answeredAt,
|
|
8197
|
+
delivered_at: answer.deliveredAt ?? null,
|
|
8198
|
+
},
|
|
8199
|
+
delivery: "awaiting_host_poll",
|
|
8200
|
+
});
|
|
8201
|
+
return;
|
|
8202
|
+
}
|
|
8203
|
+
}
|
|
7733
8204
|
const lookup = await loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, company);
|
|
7734
8205
|
if (lookup.errors.length > 0) {
|
|
7735
8206
|
throw new LanesError("outpost_question_lookup_unavailable", `Could not verify whether Outposts own question ${questionId}`, {
|
|
@@ -7805,6 +8276,22 @@ async function runQuestionsAnswer(ctx, questionId, opts) {
|
|
|
7805
8276
|
throw new LanesError("missing_choice", "--choice or --freeform is required", {});
|
|
7806
8277
|
}
|
|
7807
8278
|
const choice = freeform ?? resolveAnswerChoice(opts.choice, body.options, false);
|
|
8279
|
+
let remoteAnswer;
|
|
8280
|
+
let meshQuestionId = question.mesh_question_id;
|
|
8281
|
+
if (await ctx.laneTrackerMeshEnabled(questionCompany)) {
|
|
8282
|
+
const client = createTrackerMeshQuestionClient({ company: questionCompany, request: ctx.trackerMeshRequest });
|
|
8283
|
+
if (!meshQuestionId) {
|
|
8284
|
+
const indexed = await indexTrackerQuestion(ctx, hqRoot, askingLane, question, body, questionCompany, client);
|
|
8285
|
+
meshQuestionId = indexed.mesh_question_id;
|
|
8286
|
+
}
|
|
8287
|
+
if (!meshQuestionId)
|
|
8288
|
+
throw new LanesError("tracker_mesh_question_link_missing", "Work Mesh question could not be linked before answering", { question_id: question.question_id });
|
|
8289
|
+
remoteAnswer = await client.answer(meshQuestionId, choice, opts.notes);
|
|
8290
|
+
assertMeshQuestionMatchesLocal(remoteAnswer, question, questionCompany);
|
|
8291
|
+
if (remoteAnswer.status !== "answered" || remoteAnswer.choice !== choice) {
|
|
8292
|
+
throw new LanesError("tracker_mesh_response_invalid", "Work Mesh did not record the requested question answer", { mesh_question_id: meshQuestionId });
|
|
8293
|
+
}
|
|
8294
|
+
}
|
|
7808
8295
|
const at = ctx.now().toISOString();
|
|
7809
8296
|
const answeredBy = sessionIdFromEnv(ctx.env)
|
|
7810
8297
|
? `session:${sessionIdFromEnv(ctx.env)}`
|
|
@@ -7839,6 +8326,7 @@ async function runQuestionsAnswer(ctx, questionId, opts) {
|
|
|
7839
8326
|
answered_at: at,
|
|
7840
8327
|
answered_level: current.hop,
|
|
7841
8328
|
},
|
|
8329
|
+
...(meshQuestionId ? { mesh_question_id: meshQuestionId } : {}),
|
|
7842
8330
|
};
|
|
7843
8331
|
});
|
|
7844
8332
|
const latestAskingLane = readLane(hqRoot, askingLane.lane_id);
|
|
@@ -7873,6 +8361,8 @@ async function runQuestionsAnswer(ctx, questionId, opts) {
|
|
|
7873
8361
|
if (wake.status === "failed") {
|
|
7874
8362
|
throw new LanesError("question_wake_failed", `Question ${questionId} was recorded, but lane ${askingLane.lane_id} could not be woken: ${wake.error}`, { lane_id: askingLane.lane_id, question_id: questionId, wake_error: wake.error });
|
|
7875
8363
|
}
|
|
8364
|
+
if (remoteAnswer)
|
|
8365
|
+
await syncTrackerMeshQuestions(ctx, hqRoot);
|
|
7876
8366
|
printJson({
|
|
7877
8367
|
ok: true,
|
|
7878
8368
|
action: "questions.answer",
|
|
@@ -8532,6 +9022,10 @@ async function runReconcile(ctx, opts) {
|
|
|
8532
9022
|
livenessDeps: livenessDepsFromCtx(ctx),
|
|
8533
9023
|
presenceConnected: ctx.presenceConnected,
|
|
8534
9024
|
resolveQuestionActiveCompany: () => resolveActiveQuestionCompany(ctx),
|
|
9025
|
+
syncTrackerQuestions: (root) => syncTrackerMeshQuestions(ctx, root),
|
|
9026
|
+
recoverQueuedMessages: async (lane) => {
|
|
9027
|
+
await recoverQueuedMessages(ctx, lane.lane_id);
|
|
9028
|
+
},
|
|
8535
9029
|
mesh: ctx.mesh,
|
|
8536
9030
|
env: ctx.env,
|
|
8537
9031
|
});
|
|
@@ -8609,6 +9103,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8609
9103
|
resolveCompany: ctx.resolveCompany,
|
|
8610
9104
|
trackerEnabled: ctx.laneTrackerEnabled,
|
|
8611
9105
|
trackerMeshEnabled: ctx.laneTrackerMeshEnabled,
|
|
9106
|
+
meshStore: (company, projectId) => createWorkMeshTrackerStore({ company, projectId, request: ctx.trackerMeshRequest, now: ctx.now }),
|
|
8612
9107
|
now: ctx.now,
|
|
8613
9108
|
actor: ctx.trackerActor,
|
|
8614
9109
|
}, wrap);
|
|
@@ -8617,6 +9112,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8617
9112
|
resolveCompany: ctx.resolveCompany,
|
|
8618
9113
|
trackerEnabled: ctx.laneTrackerEnabled,
|
|
8619
9114
|
trackerMeshEnabled: ctx.laneTrackerMeshEnabled,
|
|
9115
|
+
meshStore: (company, projectId) => createWorkMeshTrackerStore({ company, projectId, request: ctx.trackerMeshRequest, now: ctx.now }),
|
|
8620
9116
|
now: ctx.now,
|
|
8621
9117
|
}, wrap);
|
|
8622
9118
|
registerLaneChecksCommand(lanes, {
|
|
@@ -8624,6 +9120,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8624
9120
|
resolveCompany: ctx.resolveCompany,
|
|
8625
9121
|
trackerEnabled: ctx.laneTrackerEnabled,
|
|
8626
9122
|
trackerMeshEnabled: ctx.laneTrackerMeshEnabled,
|
|
9123
|
+
meshStore: (company, projectId) => createWorkMeshTrackerStore({ company, projectId, request: ctx.trackerMeshRequest, now: ctx.now }),
|
|
8627
9124
|
now: ctx.now,
|
|
8628
9125
|
}, wrap);
|
|
8629
9126
|
registerLaneReportCommands(lanes, {
|
|
@@ -8631,6 +9128,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8631
9128
|
resolveCompany: ctx.resolveCompany,
|
|
8632
9129
|
trackerEnabled: ctx.laneTrackerEnabled,
|
|
8633
9130
|
trackerMeshEnabled: ctx.laneTrackerMeshEnabled,
|
|
9131
|
+
meshStore: (company, projectId) => createWorkMeshTrackerStore({ company, projectId, request: ctx.trackerMeshRequest, now: ctx.now }),
|
|
8634
9132
|
now: ctx.now,
|
|
8635
9133
|
readQuestionBody: (root, question) => {
|
|
8636
9134
|
const body = requireQuestionBody(root, question);
|
|
@@ -8863,6 +9361,12 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8863
9361
|
.action(wrap(async (laneId, opts) => {
|
|
8864
9362
|
await runMessage(ctx, laneId, opts);
|
|
8865
9363
|
}));
|
|
9364
|
+
lanes
|
|
9365
|
+
.command("_recover-queued-messages", { hidden: true })
|
|
9366
|
+
.argument("<lane-id>", "Lane id")
|
|
9367
|
+
.action(wrap(async (laneId) => {
|
|
9368
|
+
printJson(await recoverQueuedMessages(ctx, laneId));
|
|
9369
|
+
}));
|
|
8866
9370
|
lanes
|
|
8867
9371
|
.command("_monitor-message", { hidden: true })
|
|
8868
9372
|
.argument("<lane-id>", "Lane id")
|