@indigoai-us/hq-cli 5.155.0 → 5.157.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 +39 -0
- package/dist/command-catalog.generated.d.ts +4 -1
- package/dist/command-catalog.generated.js +5 -1
- package/dist/commands/agent-mcp.d.ts +1 -3
- package/dist/commands/agent-mcp.js +5 -26
- package/dist/commands/bot.js +13 -2
- package/dist/commands/lanes.js +94 -18
- package/dist/commands/mesh.js +35 -8
- package/dist/lib/agent-kit/attachment-download.d.ts +24 -0
- package/dist/lib/agent-kit/attachment-download.js +47 -0
- package/dist/lib/agent-kit/inbox-state.d.ts +6 -2
- package/dist/lib/agent-kit/inbox-state.js +6 -2
- package/dist/lib/agent-kit/mcp/attachments.d.ts +28 -1
- package/dist/lib/agent-kit/mcp/attachments.js +48 -2
- package/dist/lib/agent-kit/mcp/tools.js +13 -19
- package/dist/lib/bot/api.d.ts +9 -0
- package/dist/lib/bot/attachments.d.ts +45 -0
- package/dist/lib/bot/attachments.js +89 -0
- package/dist/lib/bot/paths.d.ts +5 -0
- package/dist/lib/bot/paths.js +7 -0
- package/dist/lib/bot/run.d.ts +7 -0
- package/dist/lib/bot/run.js +34 -4
- package/dist/lib/bot/runtime/claude.d.ts +1 -0
- package/dist/lib/bot/runtime/claude.js +6 -0
- package/dist/lib/bot/runtime/types.d.ts +8 -0
- package/dist/lib/lanes/mesh-events.d.ts +7 -3
- package/dist/lib/lanes/mesh-events.js +116 -20
- package/dist/lib/lanes/reconcile.js +55 -1
- package/dist/lib/lanes/watcher.js +27 -14
- package/dist/lib/mesh/api.d.ts +27 -1
- package/dist/lib/mesh/api.js +41 -12
- package/dist/lib/mesh/project-board.d.ts +1 -0
- package/dist/lib/mesh/project-board.js +1 -1
- package/package.json +1 -1
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
* of opening a browser nobody is watching, and the watcher records
|
|
43
43
|
* `mesh_event_failed` and moves on. No credential file is read here.
|
|
44
44
|
*/
|
|
45
|
-
import { appendThreadEvent, clamp, ensureProjectThread, getProjectView, patchStoryStatus, putProjectView, requireToken, registerMeshProject, resolveMeshCompany, } from "../mesh/api.js";
|
|
45
|
+
import { appendThreadEvent, clamp, ensureLaneMeshThread, ensureProjectThread, getProjectView, patchStoryStatus, putProjectView, requireToken, registerMeshProject, resolveMeshCompany, } from "../mesh/api.js";
|
|
46
46
|
import { LanesError } from "./errors.js";
|
|
47
47
|
export const LANE_MESH_EVENTS = [
|
|
48
48
|
"lane_started",
|
|
@@ -52,12 +52,17 @@ export const LANE_MESH_EVENTS = [
|
|
|
52
52
|
"lane_ask",
|
|
53
53
|
"lane_blocked",
|
|
54
54
|
"lane_done",
|
|
55
|
+
"lane_failed",
|
|
56
|
+
"lane_interrupted",
|
|
57
|
+
"lane_cancelled",
|
|
58
|
+
"lane_killed",
|
|
55
59
|
];
|
|
56
60
|
/** Roster kinds this module emits. Subset of the server's `validKinds`. */
|
|
57
61
|
export const WIRE_EVENT_KINDS = [
|
|
58
62
|
"run_started",
|
|
59
63
|
"run_progress",
|
|
60
64
|
"run_complete",
|
|
65
|
+
"blocked",
|
|
61
66
|
"question",
|
|
62
67
|
"note",
|
|
63
68
|
];
|
|
@@ -94,6 +99,30 @@ function nonEmpty(text, fallback) {
|
|
|
94
99
|
export function toWireThreadEvent(event, payload) {
|
|
95
100
|
const kind = parseLaneMeshEvent(event);
|
|
96
101
|
const id = identity(payload);
|
|
102
|
+
const laneId = str(payload.lane_id ?? payload.laneId).trim();
|
|
103
|
+
const senior = payload.senior && typeof payload.senior === "object" && !Array.isArray(payload.senior)
|
|
104
|
+
? payload.senior
|
|
105
|
+
: undefined;
|
|
106
|
+
const lane = laneId
|
|
107
|
+
? {
|
|
108
|
+
laneId,
|
|
109
|
+
...(str(payload.story_id ?? payload.storyId) ? { storyId: str(payload.story_id ?? payload.storyId) } : {}),
|
|
110
|
+
...(str(payload.worker_profile ?? payload.workerProfile) ? { workerProfile: str(payload.worker_profile ?? payload.workerProfile) } : {}),
|
|
111
|
+
...(str(payload.model) ? { model: str(payload.model) } : {}),
|
|
112
|
+
...(str(payload.account) ? { account: str(payload.account) } : {}),
|
|
113
|
+
...(str(payload.parent_lane_id ?? payload.parentLaneId) ? { parentLaneId: str(payload.parent_lane_id ?? payload.parentLaneId) } : {}),
|
|
114
|
+
...(senior && ["user", "session", "lane"].includes(String(senior.kind))
|
|
115
|
+
? { senior: { kind: senior.kind, ...(typeof senior.id === "string" ? { id: senior.id } : {}) } }
|
|
116
|
+
: {}),
|
|
117
|
+
laneVersion: Number.isSafeInteger(Number(payload.lane_version ?? payload.laneVersion)) && Number(payload.lane_version ?? payload.laneVersion) > 0
|
|
118
|
+
? Number(payload.lane_version ?? payload.laneVersion)
|
|
119
|
+
: 1,
|
|
120
|
+
}
|
|
121
|
+
: undefined;
|
|
122
|
+
const laneFields = (laneStatus) => ({
|
|
123
|
+
...(lane ? { lane } : {}),
|
|
124
|
+
...(laneStatus ? { laneStatus } : {}),
|
|
125
|
+
});
|
|
97
126
|
switch (kind) {
|
|
98
127
|
case "lane_started":
|
|
99
128
|
return {
|
|
@@ -101,6 +130,7 @@ export function toWireThreadEvent(event, payload) {
|
|
|
101
130
|
payload: {
|
|
102
131
|
title: clamp(`lane_started ${str(payload.lane_id) || "unknown-lane"}`, WIRE_ACTIVITY_TEXT_MAX),
|
|
103
132
|
summary: clamp(`Lane ${id} started`, WIRE_ACTIVITY_TEXT_MAX),
|
|
133
|
+
...laneFields("running"),
|
|
104
134
|
},
|
|
105
135
|
};
|
|
106
136
|
case "lane_progress": {
|
|
@@ -109,29 +139,47 @@ export function toWireThreadEvent(event, payload) {
|
|
|
109
139
|
eventKind: "run_progress",
|
|
110
140
|
payload: {
|
|
111
141
|
summary: clamp(nonEmpty(`lane_progress ${id}${detail ? `: ${detail}` : ""}`, "lane_progress"), WIRE_ACTIVITY_TEXT_MAX),
|
|
142
|
+
...laneFields("running"),
|
|
112
143
|
},
|
|
113
144
|
};
|
|
114
145
|
}
|
|
115
146
|
case "lane_done": {
|
|
116
147
|
const summary = str(payload.summary);
|
|
148
|
+
const suppliedStatus = str(payload.lane_status ?? payload.laneStatus);
|
|
149
|
+
const laneStatus = isMeshLaneStatus(suppliedStatus) ? suppliedStatus : "done";
|
|
117
150
|
return {
|
|
118
151
|
eventKind: "run_complete",
|
|
119
152
|
payload: {
|
|
120
153
|
title: clamp(`lane_done ${str(payload.lane_id) || "unknown-lane"}`, WIRE_ACTIVITY_TEXT_MAX),
|
|
121
154
|
summary: clamp(nonEmpty(`${id}: ${summary}`.replace(/:\s*$/, ""), `lane_done ${id}`), WIRE_ACTIVITY_TEXT_MAX),
|
|
155
|
+
...laneFields(laneStatus),
|
|
122
156
|
},
|
|
123
157
|
};
|
|
124
158
|
}
|
|
125
159
|
case "lane_blocked": {
|
|
126
|
-
const reason = str(payload.blocked_reason);
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
// audit note kind to preserve a filterable blocked marker.
|
|
160
|
+
const reason = str(payload.blocked_reason ?? payload.reason);
|
|
161
|
+
const suppliedStatus = str(payload.lane_status ?? payload.laneStatus);
|
|
162
|
+
const laneStatus = isMeshLaneStatus(suppliedStatus) ? suppliedStatus : "blocked";
|
|
130
163
|
return {
|
|
131
|
-
eventKind: "
|
|
164
|
+
eventKind: "blocked",
|
|
132
165
|
payload: {
|
|
133
|
-
|
|
134
|
-
|
|
166
|
+
reason: clamp(`lane_blocked ${id}${reason ? `: ${reason}` : ""}`, WIRE_NOTE_TEXT_MAX),
|
|
167
|
+
...laneFields(laneStatus),
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
case "lane_failed":
|
|
172
|
+
case "lane_interrupted":
|
|
173
|
+
case "lane_cancelled":
|
|
174
|
+
case "lane_killed": {
|
|
175
|
+
const laneStatus = kind.slice("lane_".length);
|
|
176
|
+
const summary = str(payload.summary ?? payload.reason ?? payload.blocked_reason);
|
|
177
|
+
return {
|
|
178
|
+
eventKind: "run_complete",
|
|
179
|
+
payload: {
|
|
180
|
+
title: clamp(`${kind} ${str(payload.lane_id) || "unknown-lane"}`, WIRE_ACTIVITY_TEXT_MAX),
|
|
181
|
+
summary: clamp(nonEmpty(`${id}: ${summary}`.replace(/:\s*$/, ""), `${kind} ${id}`), WIRE_ACTIVITY_TEXT_MAX),
|
|
182
|
+
...laneFields(laneStatus),
|
|
135
183
|
},
|
|
136
184
|
};
|
|
137
185
|
}
|
|
@@ -144,6 +192,7 @@ export function toWireThreadEvent(event, payload) {
|
|
|
144
192
|
};
|
|
145
193
|
if (questionId)
|
|
146
194
|
out.questionId = questionId;
|
|
195
|
+
Object.assign(out, laneFields());
|
|
147
196
|
return { eventKind: "question", payload: out };
|
|
148
197
|
}
|
|
149
198
|
case "lane_ask": {
|
|
@@ -155,6 +204,7 @@ export function toWireThreadEvent(event, payload) {
|
|
|
155
204
|
};
|
|
156
205
|
if (ids[0])
|
|
157
206
|
out.questionId = ids[0];
|
|
207
|
+
Object.assign(out, laneFields("awaiting_input"));
|
|
158
208
|
return { eventKind: "question", payload: out };
|
|
159
209
|
}
|
|
160
210
|
default: {
|
|
@@ -224,12 +274,28 @@ export function createLaneMesh(opts = {}) {
|
|
|
224
274
|
return {
|
|
225
275
|
async ensureProjectThread(company, projectId) {
|
|
226
276
|
const tok = await token();
|
|
227
|
-
const resolved = await resolveMeshCompany(tok, company.
|
|
277
|
+
const resolved = await resolveMeshCompany(tok, company.uid);
|
|
228
278
|
const ensured = await ensureProjectThread(tok, resolved, projectId, {
|
|
229
279
|
summary: `HQ lane ${projectId}`,
|
|
230
280
|
});
|
|
231
281
|
return ensured.threadId;
|
|
232
282
|
},
|
|
283
|
+
async ensureLaneThread(company, lane, laneStatus = lane.state, signal) {
|
|
284
|
+
const tok = await token();
|
|
285
|
+
const resolved = await resolveMeshCompany(tok, company.uid);
|
|
286
|
+
const descriptor = {
|
|
287
|
+
laneId: lane.lane_id,
|
|
288
|
+
...(lane.story_id ? { storyId: lane.story_id } : {}),
|
|
289
|
+
...(lane.worker_profile ? { workerProfile: lane.worker_profile } : {}),
|
|
290
|
+
...(lane.model ? { model: lane.model } : {}),
|
|
291
|
+
...(lane.account ? { account: lane.account } : {}),
|
|
292
|
+
...(lane.senior.kind === "lane" ? { parentLaneId: lane.senior.id } : {}),
|
|
293
|
+
senior: { kind: lane.senior.kind, id: lane.senior.id },
|
|
294
|
+
laneVersion: lane.version,
|
|
295
|
+
};
|
|
296
|
+
const ensured = await ensureLaneMeshThread(tok, resolved, lane.project_id, descriptor, isMeshLaneStatus(laneStatus) ? laneStatus : "running", signal);
|
|
297
|
+
return ensured.threadId;
|
|
298
|
+
},
|
|
233
299
|
async appendThreadEvent(company, threadId, event, payload, signal) {
|
|
234
300
|
const wire = toWireThreadEvent(event, payload);
|
|
235
301
|
const tok = await token();
|
|
@@ -263,34 +329,51 @@ export function laneMeshCompany(lane) {
|
|
|
263
329
|
export function laneCanEmit(lane) {
|
|
264
330
|
return laneMeshCompany(lane) !== undefined && !!lane.thread_id;
|
|
265
331
|
}
|
|
266
|
-
function
|
|
332
|
+
export function laneMeshEventIdentity(lane) {
|
|
267
333
|
const lastMove = lane.account_history?.[lane.account_history.length - 1];
|
|
268
334
|
return {
|
|
269
335
|
lane_id: lane.lane_id,
|
|
270
336
|
project_id: lane.project_id,
|
|
271
337
|
story_id: lane.story_id,
|
|
338
|
+
worker_profile: lane.worker_profile,
|
|
339
|
+
model: lane.model,
|
|
340
|
+
lane_version: lane.version,
|
|
341
|
+
senior: { kind: lane.senior.kind, id: lane.senior.id },
|
|
342
|
+
...(lane.senior.kind === "lane" ? { parent_lane_id: lane.senior.id } : {}),
|
|
272
343
|
...(lane.account !== undefined ? { account: lane.account } : {}),
|
|
273
344
|
// A moved lane names the move on every event its run emits (US-023 §8).
|
|
274
345
|
...(lastMove !== undefined ? { account_move: { from: lastMove.from, to: lastMove.to, at: lastMove.at } } : {}),
|
|
275
346
|
};
|
|
276
347
|
}
|
|
348
|
+
function isMeshLaneStatus(value) {
|
|
349
|
+
return ["queued", "running", "awaiting_input", "blocked", "done", "failed", "interrupted", "cancelled", "killed"].includes(value);
|
|
350
|
+
}
|
|
351
|
+
// hq-pro derives lane thread ids from company + lane identity; opaque ids may
|
|
352
|
+
// be legacy shared project threads and must be migrated before a lane closes.
|
|
353
|
+
const STABLE_LANE_THREAD_ID = /^lane-[0-9a-f]{32}$/;
|
|
354
|
+
async function ensureLaneEventThread(mesh, company, lane, laneStatus, signal) {
|
|
355
|
+
if (mesh.ensureLaneThread && !STABLE_LANE_THREAD_ID.test(lane.thread_id ?? "")) {
|
|
356
|
+
return mesh.ensureLaneThread(company, lane, laneStatus, signal);
|
|
357
|
+
}
|
|
358
|
+
return lane.thread_id;
|
|
359
|
+
}
|
|
277
360
|
/**
|
|
278
361
|
* Spec §8 terminal-envelope events. `done` → lane_done and story → review;
|
|
279
362
|
* `blocked` → lane_blocked; `ask` → lane_ask with the queue ids. The story is
|
|
280
363
|
* never set to `done` here: promotion belongs to the senior.
|
|
281
364
|
*/
|
|
282
365
|
export async function emitLaneDecisionEvents(mesh, lane, envelope, opts = {}) {
|
|
283
|
-
if (!laneCanEmit(lane))
|
|
284
|
-
return;
|
|
285
366
|
const company = laneMeshCompany(lane);
|
|
286
|
-
|
|
367
|
+
if (!company)
|
|
368
|
+
return;
|
|
369
|
+
const threadId = await ensureLaneEventThread(mesh, company, lane, lane.state, opts.signal);
|
|
287
370
|
if (!company || !threadId)
|
|
288
371
|
return;
|
|
289
372
|
switch (envelope.decision) {
|
|
290
373
|
case "done": {
|
|
291
374
|
const outcomes = await Promise.allSettled([
|
|
292
375
|
mesh.appendThreadEvent(company, threadId, "lane_done", {
|
|
293
|
-
...
|
|
376
|
+
...laneMeshEventIdentity(lane),
|
|
294
377
|
summary: envelope.summary,
|
|
295
378
|
}, opts.signal),
|
|
296
379
|
mesh.patchStoryStatus(company, lane.project_id, lane.story_id, "review"),
|
|
@@ -304,7 +387,7 @@ export async function emitLaneDecisionEvents(mesh, lane, envelope, opts = {}) {
|
|
|
304
387
|
{
|
|
305
388
|
const outcomes = await Promise.allSettled([
|
|
306
389
|
mesh.appendThreadEvent(company, threadId, "lane_blocked", {
|
|
307
|
-
...
|
|
390
|
+
...laneMeshEventIdentity(lane),
|
|
308
391
|
blocked_reason: envelope.blocked_reason ?? "",
|
|
309
392
|
}),
|
|
310
393
|
mesh.patchStoryStatus(company, lane.project_id, lane.story_id, "in_progress"),
|
|
@@ -322,7 +405,7 @@ export async function emitLaneDecisionEvents(mesh, lane, envelope, opts = {}) {
|
|
|
322
405
|
for (const questionId of opts.questionIds ?? []) {
|
|
323
406
|
try {
|
|
324
407
|
await mesh.appendThreadEvent(company, threadId, "lane_ask", {
|
|
325
|
-
...
|
|
408
|
+
...laneMeshEventIdentity(lane),
|
|
326
409
|
question_ids: [questionId],
|
|
327
410
|
}, opts.signal);
|
|
328
411
|
}
|
|
@@ -340,15 +423,28 @@ export async function emitLaneDecisionEvents(mesh, lane, envelope, opts = {}) {
|
|
|
340
423
|
}
|
|
341
424
|
}
|
|
342
425
|
}
|
|
343
|
-
|
|
344
|
-
|
|
426
|
+
/** Emit an abnormal terminal state even when the worker produced no envelope. */
|
|
427
|
+
export async function emitLaneTerminalStateEvent(mesh, lane, state, summary, signal) {
|
|
428
|
+
const company = laneMeshCompany(lane);
|
|
429
|
+
if (!company)
|
|
345
430
|
return;
|
|
431
|
+
const threadId = await ensureLaneEventThread(mesh, company, lane, state, signal);
|
|
432
|
+
if (!company || !threadId)
|
|
433
|
+
return;
|
|
434
|
+
await mesh.appendThreadEvent(company, threadId, `lane_${state}`, {
|
|
435
|
+
...laneMeshEventIdentity(lane),
|
|
436
|
+
...(summary ? { summary } : {}),
|
|
437
|
+
}, signal);
|
|
438
|
+
}
|
|
439
|
+
export async function emitLaneProgressEvent(mesh, lane, detail, signal) {
|
|
346
440
|
const company = laneMeshCompany(lane);
|
|
347
|
-
|
|
441
|
+
if (!company)
|
|
442
|
+
return;
|
|
443
|
+
const threadId = await ensureLaneEventThread(mesh, company, lane, lane.state, signal);
|
|
348
444
|
if (!company || !threadId)
|
|
349
445
|
return;
|
|
350
446
|
await mesh.appendThreadEvent(company, threadId, "lane_progress", {
|
|
351
|
-
...
|
|
447
|
+
...laneMeshEventIdentity(lane),
|
|
352
448
|
detail,
|
|
353
449
|
}, signal);
|
|
354
450
|
}
|
|
@@ -21,7 +21,7 @@ import { applyThrottleDecision, decideAdmission, defaultThrottleDeps, emptyThrot
|
|
|
21
21
|
import { applyDefaultQuotaHold, readQuotaSignal, recordAccountQuota } from "./quota.js";
|
|
22
22
|
import { answeredQuestionEntries, raiseEnvelopeQuestions } from "./questions.js";
|
|
23
23
|
import { SILENCE_MS, classifyLaneExit, isPidAlive, readLatestEnvelope, readLatestEnvelopeFile, readLatestEnvelopeRecord, } from "./watcher.js";
|
|
24
|
-
import { createLaneMesh, emitLaneDecisionEvents, laneMeshCompany } from "./mesh-events.js";
|
|
24
|
+
import { createLaneMesh, emitLaneDecisionEvents, emitLaneTerminalStateEvent, laneMeshCompany, } from "./mesh-events.js";
|
|
25
25
|
import { laneStories, storyStatusForEnvelope } from "./stories.js";
|
|
26
26
|
import { decideEnvelopeRepair, envelopeQuestionIds, routeProviderFailure } from "./watcher.js";
|
|
27
27
|
import { providerRejectionDetails, readProviderRejection, } from "./provider-rejection.js";
|
|
@@ -1032,6 +1032,34 @@ async function heartbeatAndDeadHosts(hqRoot, lanes, summary, deps, now) {
|
|
|
1032
1032
|
}
|
|
1033
1033
|
}
|
|
1034
1034
|
}
|
|
1035
|
+
else if (!deps.dryRun &&
|
|
1036
|
+
(result.record.state === "failed" || result.record.state === "interrupted" ||
|
|
1037
|
+
result.record.state === "cancelled" || result.record.state === "killed")) {
|
|
1038
|
+
const mesh = deps.mesh === undefined ? createLaneMesh({ interactive: false }) : deps.mesh;
|
|
1039
|
+
if (mesh) {
|
|
1040
|
+
const meshEmitTimeoutMs = deps.meshEmitTimeoutMs ?? 10_000;
|
|
1041
|
+
let timer;
|
|
1042
|
+
try {
|
|
1043
|
+
await Promise.race([
|
|
1044
|
+
emitLaneTerminalStateEvent(mesh, result.record, result.record.state, result.record.terminal_reason),
|
|
1045
|
+
new Promise((_resolve, reject) => {
|
|
1046
|
+
timer = setTimeout(() => reject(new Error(`terminal mesh emit timed out after ${meshEmitTimeoutMs}ms`)), meshEmitTimeoutMs);
|
|
1047
|
+
}),
|
|
1048
|
+
]);
|
|
1049
|
+
}
|
|
1050
|
+
catch (err) {
|
|
1051
|
+
markDirty(summary, {
|
|
1052
|
+
kind: "lane",
|
|
1053
|
+
id: lane.lane_id,
|
|
1054
|
+
why: `terminal mesh emit failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
finally {
|
|
1058
|
+
if (timer)
|
|
1059
|
+
clearTimeout(timer);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1035
1063
|
}
|
|
1036
1064
|
}
|
|
1037
1065
|
catch (err) {
|
|
@@ -1060,6 +1088,32 @@ async function heartbeatAndDeadHosts(hqRoot, lanes, summary, deps, now) {
|
|
|
1060
1088
|
to: "interrupted",
|
|
1061
1089
|
reason: "host_lost",
|
|
1062
1090
|
});
|
|
1091
|
+
const mesh = !deps.dryRun
|
|
1092
|
+
? deps.mesh === undefined ? createLaneMesh({ interactive: false }) : deps.mesh
|
|
1093
|
+
: null;
|
|
1094
|
+
if (mesh) {
|
|
1095
|
+
const meshEmitTimeoutMs = deps.meshEmitTimeoutMs ?? 10_000;
|
|
1096
|
+
let timer;
|
|
1097
|
+
try {
|
|
1098
|
+
await Promise.race([
|
|
1099
|
+
emitLaneTerminalStateEvent(mesh, result.record, "interrupted", "host_lost"),
|
|
1100
|
+
new Promise((_resolve, reject) => {
|
|
1101
|
+
timer = setTimeout(() => reject(new Error(`terminal mesh emit timed out after ${meshEmitTimeoutMs}ms`)), meshEmitTimeoutMs);
|
|
1102
|
+
}),
|
|
1103
|
+
]);
|
|
1104
|
+
}
|
|
1105
|
+
catch (err) {
|
|
1106
|
+
markDirty(summary, {
|
|
1107
|
+
kind: "lane",
|
|
1108
|
+
id: lane.lane_id,
|
|
1109
|
+
why: `terminal mesh emit failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
finally {
|
|
1113
|
+
if (timer)
|
|
1114
|
+
clearTimeout(timer);
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1063
1117
|
}
|
|
1064
1118
|
}
|
|
1065
1119
|
catch (err) {
|
|
@@ -44,7 +44,7 @@ import { PROVIDER_REJECTED_REASON, QUOTA_STATUS, isProviderLoginFailure, provide
|
|
|
44
44
|
import { readToolActivity, toolActivityPath } from "./dropbox.js";
|
|
45
45
|
import { loadLanesConfig, providerProgressEventMinIntervalMs, providerQuotaDefaultHoldMs } from "./config.js";
|
|
46
46
|
import { resolveAdmissionHost as resolveProcessAdmissionHost } from "./admission-host.js";
|
|
47
|
-
import { createLaneMesh, emitLaneDecisionEvents, emitLaneProgressEvent, progressEventDue, } from "./mesh-events.js";
|
|
47
|
+
import { createLaneMesh, emitLaneDecisionEvents, emitLaneProgressEvent, emitLaneTerminalStateEvent, progressEventDue, } from "./mesh-events.js";
|
|
48
48
|
import { envelopeQuestionId } from "./questions.js";
|
|
49
49
|
import { laneRunDir, missingPidProbe, probePidRecord, readPidRecord, readProcStarttime, readSpawnManifest, envelopeRepairMode, spawnEnvelopeRepair, retryLaneOnAlternateAccount, } from "./spawn.js";
|
|
50
50
|
/** Spec §5.7: no signal for 20 minutes while the process is still alive. */
|
|
@@ -586,20 +586,27 @@ export async function runWatcherLoop(hqRoot, laneId, deps = {}) {
|
|
|
586
586
|
const meshRequestTimeoutMs = deps.meshRequestTimeoutMs ?? 10_000;
|
|
587
587
|
const initialLane = readLaneSoft(hqRoot, laneId);
|
|
588
588
|
if (mesh && initialLane) {
|
|
589
|
-
//
|
|
590
|
-
//
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
589
|
+
// Every armed watcher refreshes the server descriptor. This migrates old
|
|
590
|
+
// shared project threads and picks up account/model changes after resume.
|
|
591
|
+
const company = initialLane.mesh_company ?? initialLane.company ?? undefined;
|
|
592
|
+
if (company &&
|
|
593
|
+
(mesh.ensureLaneThread || !initialLane.thread_id || initialLane.thread_id.startsWith("proj-"))) {
|
|
594
594
|
let timer;
|
|
595
595
|
try {
|
|
596
596
|
const threadId = await Promise.race([
|
|
597
|
-
mesh.
|
|
597
|
+
mesh.ensureLaneThread
|
|
598
|
+
? mesh.ensureLaneThread(company, initialLane)
|
|
599
|
+
: mesh.ensureProjectThread(company, initialLane.project_id),
|
|
598
600
|
new Promise((_resolve, reject) => {
|
|
599
601
|
timer = setTimeout(() => reject(new Error(`mesh thread ensure timed out after ${meshRequestTimeoutMs}ms`)), meshRequestTimeoutMs);
|
|
600
602
|
}),
|
|
601
603
|
]);
|
|
602
|
-
|
|
604
|
+
const updated = initialLane.thread_id === threadId
|
|
605
|
+
? initialLane
|
|
606
|
+
: retryUpdateLane(hqRoot, laneId, "spawn", (current) => current.thread_id === threadId ? current : { ...current, thread_id: threadId });
|
|
607
|
+
if (mesh.ensureLaneThread && updated.version !== initialLane.version) {
|
|
608
|
+
await mesh.ensureLaneThread(company, updated);
|
|
609
|
+
}
|
|
603
610
|
}
|
|
604
611
|
catch (err) {
|
|
605
612
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -607,7 +614,7 @@ export async function runWatcherLoop(hqRoot, laneId, deps = {}) {
|
|
|
607
614
|
writeWatcherDecision(outPath, {
|
|
608
615
|
event: "mesh_event_failed",
|
|
609
616
|
at: new Date(now()).toISOString(),
|
|
610
|
-
mesh_event: "
|
|
617
|
+
mesh_event: "ensure_lane_thread",
|
|
611
618
|
error: message,
|
|
612
619
|
}, outMax);
|
|
613
620
|
}
|
|
@@ -889,11 +896,17 @@ export async function runWatcherLoop(hqRoot, laneId, deps = {}) {
|
|
|
889
896
|
// Spec §8 terminal rows: done → lane_done + story review; blocked →
|
|
890
897
|
// lane_blocked; ask → lane_ask with the queue ids. After the state
|
|
891
898
|
// write, so a mesh failure can never leave the lane unclassified.
|
|
892
|
-
if (mesh &&
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
899
|
+
if (mesh && terminalWrite.changed) {
|
|
900
|
+
if (envelope && latest) {
|
|
901
|
+
const questionIds = envelope.decision === "ask"
|
|
902
|
+
? envelopeQuestionIds(laneId, latest.ref, envelope)
|
|
903
|
+
: undefined;
|
|
904
|
+
await emitMeshSoft(`lane_${envelope.decision}`, logDecision, tick.emit.at, (signal) => emitLaneDecisionEvents(mesh, updated, envelope, { questionIds, signal }), meshRequestTimeoutMs);
|
|
905
|
+
}
|
|
906
|
+
else if (updated.state === "failed" || updated.state === "interrupted" ||
|
|
907
|
+
updated.state === "cancelled" || updated.state === "killed") {
|
|
908
|
+
await emitMeshSoft(`lane_${updated.state}`, logDecision, tick.emit.at, (signal) => emitLaneTerminalStateEvent(mesh, updated, updated.state, classification.reason, signal), meshRequestTimeoutMs);
|
|
909
|
+
}
|
|
897
910
|
}
|
|
898
911
|
}
|
|
899
912
|
}
|
package/dist/lib/mesh/api.d.ts
CHANGED
|
@@ -16,13 +16,35 @@ export interface MeshThread {
|
|
|
16
16
|
projectId?: string;
|
|
17
17
|
companyUid?: string;
|
|
18
18
|
threadStatus?: string;
|
|
19
|
+
kind?: "project" | "work-session" | "lane" | string;
|
|
20
|
+
lane?: MeshLaneDescriptor;
|
|
21
|
+
laneStatus?: MeshLaneStatus;
|
|
22
|
+
projectChannelId?: string;
|
|
19
23
|
ownerUid?: string;
|
|
24
|
+
ownerType?: "human" | "agent";
|
|
25
|
+
senderMachineUid?: string;
|
|
26
|
+
senderPrincipalType?: "agent" | "outpost";
|
|
27
|
+
senderAuthenticatedSub?: string;
|
|
20
28
|
progressSummary?: string;
|
|
21
29
|
sourceSignalSummary?: string;
|
|
22
30
|
blockedReason?: string;
|
|
23
31
|
lastActivityAt?: string;
|
|
24
32
|
createdAt?: string;
|
|
25
33
|
}
|
|
34
|
+
export type MeshLaneStatus = "queued" | "running" | "awaiting_input" | "blocked" | "done" | "failed" | "interrupted" | "cancelled" | "killed";
|
|
35
|
+
export interface MeshLaneDescriptor {
|
|
36
|
+
laneId: string;
|
|
37
|
+
storyId?: string;
|
|
38
|
+
workerProfile?: string;
|
|
39
|
+
model?: string;
|
|
40
|
+
account?: string;
|
|
41
|
+
parentLaneId?: string;
|
|
42
|
+
senior?: {
|
|
43
|
+
kind: "user" | "session" | "lane";
|
|
44
|
+
id?: string;
|
|
45
|
+
};
|
|
46
|
+
laneVersion: number;
|
|
47
|
+
}
|
|
26
48
|
export interface MeshProjectStoryInput {
|
|
27
49
|
id: string;
|
|
28
50
|
title: string;
|
|
@@ -101,6 +123,10 @@ export declare function ensureProjectThread(token: string, company: MeshCompany,
|
|
|
101
123
|
threadId: string;
|
|
102
124
|
created: boolean;
|
|
103
125
|
}>;
|
|
126
|
+
/** Ensure the server-owned, stable Work Mesh thread for one lane. */
|
|
127
|
+
export declare function ensureLaneMeshThread(token: string, company: MeshCompany, projectId: string, lane: MeshLaneDescriptor, laneStatus: MeshLaneStatus, signal?: AbortSignal): Promise<{
|
|
128
|
+
threadId: string;
|
|
129
|
+
}>;
|
|
104
130
|
export declare function appendThreadEvent(token: string, companyUid: string, threadId: string, eventKind: string, payload: Record<string, unknown>, signal?: AbortSignal): Promise<{
|
|
105
131
|
eventId?: string;
|
|
106
132
|
createdAt?: string;
|
|
@@ -138,7 +164,7 @@ export declare function putProjectView(token: string, companyUid: string, projec
|
|
|
138
164
|
expectedVersion?: number;
|
|
139
165
|
}): Promise<MeshProjectView>;
|
|
140
166
|
/** Register the project genesis thread and deterministic project channel. */
|
|
141
|
-
export declare function registerMeshProject(token: string, companyUid: string, projectId: string): Promise<MeshProjectRegistration>;
|
|
167
|
+
export declare function registerMeshProject(token: string, companyUid: string, projectId: string, channelId?: string): Promise<MeshProjectRegistration>;
|
|
142
168
|
export declare function pairDmUidsFromInbox(inbox: unknown): string[];
|
|
143
169
|
export interface WarmCacheResult {
|
|
144
170
|
directory: boolean;
|
package/dist/lib/mesh/api.js
CHANGED
|
@@ -229,21 +229,23 @@ async function listThreadsByStatus(token, companyUid) {
|
|
|
229
229
|
return collected;
|
|
230
230
|
}
|
|
231
231
|
/**
|
|
232
|
-
* Project
|
|
233
|
-
*
|
|
234
|
-
* back to `projectId` alone.
|
|
232
|
+
* Project facets contain all project-owned threads, including terminal lane
|
|
233
|
+
* threads that have been removed from the active-status index.
|
|
235
234
|
*/
|
|
236
235
|
async function listProjectThreads(token, companyUid, projectId) {
|
|
237
236
|
try {
|
|
238
237
|
return await listThreadsByQuery(token, {
|
|
239
238
|
companyUid,
|
|
240
239
|
projectId,
|
|
241
|
-
status: "open",
|
|
242
240
|
});
|
|
243
241
|
}
|
|
244
242
|
catch {
|
|
245
243
|
try {
|
|
246
|
-
return await listThreadsByQuery(token, {
|
|
244
|
+
return await listThreadsByQuery(token, {
|
|
245
|
+
companyUid,
|
|
246
|
+
projectId,
|
|
247
|
+
status: "open",
|
|
248
|
+
});
|
|
247
249
|
}
|
|
248
250
|
catch {
|
|
249
251
|
return [];
|
|
@@ -253,7 +255,10 @@ async function listProjectThreads(token, companyUid, projectId) {
|
|
|
253
255
|
function mergeProjectAndStatusThreads(projectThreads, statusThreads, projectId) {
|
|
254
256
|
const seen = new Set();
|
|
255
257
|
const merged = [];
|
|
256
|
-
for (const thread of sortThreadsByActivity(projectThreads.filter((t) => (!t.projectId || t.projectId === projectId) &&
|
|
258
|
+
for (const thread of sortThreadsByActivity(projectThreads.filter((t) => (!t.projectId || t.projectId === projectId) &&
|
|
259
|
+
(isActiveThread(t) ||
|
|
260
|
+
(t.kind === "lane" &&
|
|
261
|
+
["done", "failed", "interrupted", "cancelled", "killed"].includes(String(t.laneStatus))))))) {
|
|
257
262
|
const key = threadKey(thread);
|
|
258
263
|
if (key) {
|
|
259
264
|
if (seen.has(key))
|
|
@@ -312,7 +317,7 @@ export function eventPayload(kind, opts, claimedBy) {
|
|
|
312
317
|
export async function ensureProjectThread(token, company, projectId, opts) {
|
|
313
318
|
if (opts.threadId)
|
|
314
319
|
return { threadId: opts.threadId, created: false };
|
|
315
|
-
const existing = (await listActiveThreads(token, company.companyUid, projectId))
|
|
320
|
+
const existing = (await listActiveThreads(token, company.companyUid, projectId)).find((thread) => (thread.kind ?? "project") === "project" && !!thread.threadId);
|
|
316
321
|
if (existing?.threadId)
|
|
317
322
|
return { threadId: existing.threadId, created: false };
|
|
318
323
|
const created = asRecord(await meshJson(token, "/v1/work-mesh/threads", {
|
|
@@ -332,6 +337,27 @@ export async function ensureProjectThread(token, company, projectId, opts) {
|
|
|
332
337
|
throw new Error("Work mesh did not return a threadId");
|
|
333
338
|
return { threadId, created: true };
|
|
334
339
|
}
|
|
340
|
+
/** Ensure the server-owned, stable Work Mesh thread for one lane. */
|
|
341
|
+
export async function ensureLaneMeshThread(token, company, projectId, lane, laneStatus, signal) {
|
|
342
|
+
const result = asRecord(await meshJson(token, "/v1/work-mesh/threads", {
|
|
343
|
+
method: "POST",
|
|
344
|
+
body: {
|
|
345
|
+
companyUid: company.companyUid,
|
|
346
|
+
projectId,
|
|
347
|
+
routing: {
|
|
348
|
+
priority: "normal",
|
|
349
|
+
tags: ["hq-lane", `project:${projectId}`, `lane:${lane.laneId}`],
|
|
350
|
+
},
|
|
351
|
+
lane,
|
|
352
|
+
laneStatus,
|
|
353
|
+
},
|
|
354
|
+
signal,
|
|
355
|
+
}));
|
|
356
|
+
const threadId = typeof result?.threadId === "string" ? result.threadId.trim() : "";
|
|
357
|
+
if (!threadId)
|
|
358
|
+
throw new Error("Work mesh lane registration did not return a threadId");
|
|
359
|
+
return { threadId };
|
|
360
|
+
}
|
|
335
361
|
export async function appendThreadEvent(token, companyUid, threadId, eventKind, payload, signal) {
|
|
336
362
|
const data = asRecord(await meshJson(token, `/v1/work-mesh/threads/${encodeURIComponent(threadId)}/events`, {
|
|
337
363
|
method: "POST",
|
|
@@ -443,18 +469,21 @@ export async function putProjectView(token, companyUid, projectId, input) {
|
|
|
443
469
|
return result;
|
|
444
470
|
}
|
|
445
471
|
/** Register the project genesis thread and deterministic project channel. */
|
|
446
|
-
export async function registerMeshProject(token, companyUid, projectId) {
|
|
447
|
-
|
|
472
|
+
export async function registerMeshProject(token, companyUid, projectId, channelId) {
|
|
473
|
+
if (channelId !== undefined && !channelId.trim()) {
|
|
474
|
+
throw new Error("PROJECT_CHANNEL_INVALID: channelId must be non-empty");
|
|
475
|
+
}
|
|
476
|
+
const result = asRecord(await meshJson(token, `/v1/work-mesh/projects/${encodeURIComponent(projectId)}/register`, { method: "POST", body: { companyUid, ...(channelId ? { channelId: channelId.trim() } : {}) } }));
|
|
448
477
|
const threadId = String(result?.threadId || "").trim();
|
|
449
|
-
const
|
|
450
|
-
if (!threadId || !
|
|
478
|
+
const registeredChannelId = String(result?.channelId || "").trim();
|
|
479
|
+
if (!threadId || !registeredChannelId) {
|
|
451
480
|
throw new Error("Work mesh registration did not return a threadId and channelId");
|
|
452
481
|
}
|
|
453
482
|
return {
|
|
454
483
|
companyUid: String(result?.companyUid || companyUid).trim(),
|
|
455
484
|
projectId: String(result?.projectId || projectId).trim(),
|
|
456
485
|
threadId,
|
|
457
|
-
channelId,
|
|
486
|
+
channelId: registeredChannelId,
|
|
458
487
|
channelCreated: result?.channelCreated === true,
|
|
459
488
|
};
|
|
460
489
|
}
|
|
@@ -125,7 +125,7 @@ export async function ensureProjectBoard(input) {
|
|
|
125
125
|
stories: input.stories,
|
|
126
126
|
repos: input.repos ?? [],
|
|
127
127
|
});
|
|
128
|
-
const registration = await registerMeshProject(input.token, input.company.companyUid, input.projectId);
|
|
128
|
+
const registration = await registerMeshProject(input.token, input.company.companyUid, input.projectId, input.channelId);
|
|
129
129
|
await closeSupersededProjectThread({
|
|
130
130
|
token: input.token,
|
|
131
131
|
companyUid: input.company.companyUid,
|