@indigoai-us/hq-cli 5.155.0 → 5.156.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 +23 -0
- package/dist/command-catalog.generated.d.ts +4 -1
- package/dist/command-catalog.generated.js +5 -1
- package/dist/commands/lanes.js +94 -18
- package/dist/commands/mesh.js +35 -8
- 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
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.156.0] — 2026-09-24
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Project-thread lookup treats an omitted server `kind` as the project thread,
|
|
10
|
+
preventing duplicate project threads when a newer lane thread is present.
|
|
11
|
+
- Lane admission reservations are refreshed after Work Mesh lane-thread
|
|
12
|
+
registration so slow registration cannot age the reservation before spawn.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- `hq mesh project register --channel` and `project set --channel` can bind an
|
|
17
|
+
existing channel to project events. `hq mesh check --project` shows lane
|
|
18
|
+
identity and status, including terminal lane threads.
|
|
19
|
+
- Lane mesh threads keep a stable lane id across resume and account moves.
|
|
20
|
+
Events include the worker profile, model, account, parent lane, and senior
|
|
21
|
+
owner. Stop and dead-host recovery events close terminal threads.
|
|
22
|
+
|
|
23
|
+
### Tests
|
|
24
|
+
|
|
25
|
+
- Cover server-shaped project-thread results and reservation timing after
|
|
26
|
+
lane-thread registration.
|
|
27
|
+
|
|
5
28
|
## [5.155.0] — 2026-09-24
|
|
6
29
|
|
|
7
30
|
### Fixed
|
|
@@ -5708,6 +5708,9 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
5708
5708
|
}, {
|
|
5709
5709
|
readonly flags: "--repo <path[@branch]>";
|
|
5710
5710
|
readonly description: "Repository binding (repeatable)";
|
|
5711
|
+
}, {
|
|
5712
|
+
readonly flags: "--channel <channelId>";
|
|
5713
|
+
readonly description: "Bind the project to an existing company or project channel";
|
|
5711
5714
|
}, {
|
|
5712
5715
|
readonly flags: "--create";
|
|
5713
5716
|
readonly description: "Create the Board view when the GET is a 404";
|
|
@@ -5754,7 +5757,7 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
5754
5757
|
readonly description: "Company slug or cloud uid";
|
|
5755
5758
|
}, {
|
|
5756
5759
|
readonly flags: "--channel <channelId>";
|
|
5757
|
-
readonly description: "
|
|
5760
|
+
readonly description: "Bind the project to an existing company or project channel";
|
|
5758
5761
|
}, {
|
|
5759
5762
|
readonly flags: "--json";
|
|
5760
5763
|
readonly description: "Print machine-readable JSON";
|
|
@@ -7367,6 +7367,10 @@ export const COMMAND_CATALOG = [
|
|
|
7367
7367
|
"flags": "--repo <path[@branch]>",
|
|
7368
7368
|
"description": "Repository binding (repeatable)"
|
|
7369
7369
|
},
|
|
7370
|
+
{
|
|
7371
|
+
"flags": "--channel <channelId>",
|
|
7372
|
+
"description": "Bind the project to an existing company or project channel"
|
|
7373
|
+
},
|
|
7370
7374
|
{
|
|
7371
7375
|
"flags": "--create",
|
|
7372
7376
|
"description": "Create the Board view when the GET is a 404"
|
|
@@ -7427,7 +7431,7 @@ export const COMMAND_CATALOG = [
|
|
|
7427
7431
|
},
|
|
7428
7432
|
{
|
|
7429
7433
|
"flags": "--channel <channelId>",
|
|
7430
|
-
"description": "
|
|
7434
|
+
"description": "Bind the project to an existing company or project channel"
|
|
7431
7435
|
},
|
|
7432
7436
|
{
|
|
7433
7437
|
"flags": "--json",
|
package/dist/commands/lanes.js
CHANGED
|
@@ -18,7 +18,7 @@ import { buildOutpostLaneCommand, buildOutpostPickupCommand, createOutpostsClien
|
|
|
18
18
|
import { parseUserYaml } from "../utils/user-yaml-error.js";
|
|
19
19
|
import { vaultApiFetch } from "../utils/vault-api.js";
|
|
20
20
|
import { openMeshTransport } from "../lib/mesh/client.js";
|
|
21
|
-
import { createLaneMesh, emitLaneDecisionEvents, } from "../lib/lanes/mesh-events.js";
|
|
21
|
+
import { createLaneMesh, emitLaneDecisionEvents, emitLaneTerminalStateEvent, laneMeshEventIdentity, } from "../lib/lanes/mesh-events.js";
|
|
22
22
|
import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
|
|
23
23
|
import { AccountResolutionError, compareLaneQueueOrder, coveredLaneIds, DEFAULT_ACCOUNT_HOMES, LANE_PROVIDERS, LANE_STATES, PROVIDER_HOME_ENV, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installDrainHook, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, listAccountStatuses, listLaneIds, listQuestionIds, listSessionLiveness, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, MONITOR_ENGINES, MONITOR_MAX_TIMEOUT_MS, MONITOR_UNCOVERED_EXIT_CODE, MAX_WATCHED_LANES, monitorReminderText, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneState, parseLivenessState, parseQuestionStatus, parseSeniorKind, readDaemonPresenceConnected, readLane, retryUpdateLane, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, runReconcilePass, runLaneWatch, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, writeLanesConfig, appendDropboxMessage, answerDropboxMessage, deliveryRefusalReason, resumeCommandRefusal, dropboxDir, dropboxInboxPath, laneAcceptsDropboxDelivery, laneAcceptsOperatorMessage, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, withLaneAdmissionLock, laneIdForAnswerDelivery, operatorMessageRefusalReason, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "../lib/lanes/index.js";
|
|
24
24
|
import { activeLaneStories, laneStories, laneStory, storyWarning, } from "../lib/lanes/stories.js";
|
|
@@ -1150,18 +1150,45 @@ function askDeliveryFailedPath(hqRoot, laneId) {
|
|
|
1150
1150
|
return path.join(runDir(hqRoot, laneId), "ask_delivery_failed.json");
|
|
1151
1151
|
}
|
|
1152
1152
|
async function emitLaneMeshEvent(ctx, company, threadId, event, payload) {
|
|
1153
|
-
if (!
|
|
1153
|
+
if (!company)
|
|
1154
|
+
return;
|
|
1155
|
+
let targetThreadId = threadId;
|
|
1156
|
+
let eventPayload = payload;
|
|
1157
|
+
const laneId = typeof payload.lane_id === "string" ? payload.lane_id.trim() : "";
|
|
1158
|
+
if (laneId && ctx.mesh.ensureLaneThread) {
|
|
1159
|
+
const lane = readLane(ctx.hqRoot(), laneId);
|
|
1160
|
+
const ready = await ensureLaneThreadRecord(ctx, lane, lane.project_id, lane.story_id, "running");
|
|
1161
|
+
targetThreadId = ready.thread_id;
|
|
1162
|
+
eventPayload = { ...laneMeshEventIdentity(ready), ...payload };
|
|
1163
|
+
}
|
|
1164
|
+
if (!targetThreadId)
|
|
1154
1165
|
return;
|
|
1155
|
-
await ctx.mesh.appendThreadEvent(company,
|
|
1166
|
+
await ctx.mesh.appendThreadEvent(company, targetThreadId, event, eventPayload);
|
|
1167
|
+
}
|
|
1168
|
+
async function emitLaneTerminalStateSoft(ctx, lane) {
|
|
1169
|
+
if (lane.state !== "failed" && lane.state !== "interrupted" &&
|
|
1170
|
+
lane.state !== "cancelled" && lane.state !== "killed")
|
|
1171
|
+
return;
|
|
1172
|
+
try {
|
|
1173
|
+
await emitLaneTerminalStateEvent(ctx.mesh, lane, lane.state, lane.terminal_reason, AbortSignal.timeout(10_000));
|
|
1174
|
+
}
|
|
1175
|
+
catch (err) {
|
|
1176
|
+
process.stderr.write(`hq lanes: terminal mesh update failed for ${lane.lane_id}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
1177
|
+
}
|
|
1156
1178
|
}
|
|
1157
1179
|
async function emitLaneStartedForStories(ctx, company, lane, threadId) {
|
|
1158
|
-
|
|
1180
|
+
const ready = ctx.mesh.ensureLaneThread
|
|
1181
|
+
? await ensureLaneThreadRecord(ctx, lane, lane.project_id, lane.story_id, "running")
|
|
1182
|
+
: lane;
|
|
1183
|
+
const stableThreadId = ready.thread_id ?? threadId;
|
|
1184
|
+
if (!stableThreadId)
|
|
1159
1185
|
return;
|
|
1160
1186
|
const failures = [];
|
|
1161
|
-
for (const story of laneStories(
|
|
1187
|
+
for (const story of laneStories(ready)) {
|
|
1162
1188
|
try {
|
|
1163
|
-
await emitLaneMeshEvent(ctx, company,
|
|
1164
|
-
|
|
1189
|
+
await emitLaneMeshEvent(ctx, company, stableThreadId, "lane_started", {
|
|
1190
|
+
...laneMeshEventIdentity(ready),
|
|
1191
|
+
lane_id: ready.lane_id,
|
|
1165
1192
|
project_id: story.project_id,
|
|
1166
1193
|
story_id: story.story_id,
|
|
1167
1194
|
});
|
|
@@ -1630,9 +1657,8 @@ async function runCreate(ctx, opts) {
|
|
|
1630
1657
|
(profileAccounts === undefined || profileAccounts.includes(id)))
|
|
1631
1658
|
.map(([id]) => id))]
|
|
1632
1659
|
: undefined;
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
: undefined;
|
|
1660
|
+
// The lane id is created in `persist`; its Work Mesh thread is ensured
|
|
1661
|
+
// immediately afterward from that stable identity.
|
|
1636
1662
|
const persist = async () => {
|
|
1637
1663
|
let admission;
|
|
1638
1664
|
let validation;
|
|
@@ -1712,7 +1738,6 @@ async function runCreate(ctx, opts) {
|
|
|
1712
1738
|
added_at: createdAt,
|
|
1713
1739
|
updated_at: createdAt,
|
|
1714
1740
|
})),
|
|
1715
|
-
thread_id: threadId,
|
|
1716
1741
|
depth: laneDepth,
|
|
1717
1742
|
senior,
|
|
1718
1743
|
senior_history: [],
|
|
@@ -1765,7 +1790,35 @@ async function runCreate(ctx, opts) {
|
|
|
1765
1790
|
else {
|
|
1766
1791
|
persisted = await withLaneAdmissionLock(hqRoot, provider, persist, ctx.sleep);
|
|
1767
1792
|
}
|
|
1768
|
-
|
|
1793
|
+
let lane = persisted.lane;
|
|
1794
|
+
const { admission, validation } = persisted;
|
|
1795
|
+
if (meshCompany) {
|
|
1796
|
+
try {
|
|
1797
|
+
lane = await ensureLaneThreadRecord(ctx, lane);
|
|
1798
|
+
}
|
|
1799
|
+
catch {
|
|
1800
|
+
try {
|
|
1801
|
+
lane = casUpdateLane(ctx.hqRoot(), lane.lane_id, (current) => ({
|
|
1802
|
+
...current,
|
|
1803
|
+
state: "failed",
|
|
1804
|
+
queued_reason: undefined,
|
|
1805
|
+
terminal_reason: "mesh_thread_registration_failed",
|
|
1806
|
+
}));
|
|
1807
|
+
}
|
|
1808
|
+
catch {
|
|
1809
|
+
throw new LanesError("mesh_thread_registration_failed", `Work Mesh lane registration failed for ${lane.lane_id}, and its local record could not be marked terminal.`, { lane_id: lane.lane_id });
|
|
1810
|
+
}
|
|
1811
|
+
throw new LanesError("mesh_thread_registration_failed", `Work Mesh lane registration failed; lane ${lane.lane_id} was marked failed.`, { lane_id: lane.lane_id, state: lane.state });
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
// Thread registration runs after persistence; anchor the reservation after
|
|
1815
|
+
// its network work so a slow response cannot age the slot before spawn.
|
|
1816
|
+
if (meshCompany && admission.action === "start" && lane.admission_reserved_at) {
|
|
1817
|
+
lane = casUpdateLane(ctx.hqRoot(), lane.lane_id, (current) => ({
|
|
1818
|
+
...current,
|
|
1819
|
+
admission_reserved_at: ctx.now().toISOString(),
|
|
1820
|
+
}));
|
|
1821
|
+
}
|
|
1769
1822
|
const dest = runDir(hqRoot, lane.lane_id);
|
|
1770
1823
|
ensureDir(path.join(dest, "logs"));
|
|
1771
1824
|
ensureDir(path.join(dest, "envelopes"));
|
|
@@ -1802,7 +1855,7 @@ async function runCreate(ctx, opts) {
|
|
|
1802
1855
|
locateTranscript: ctx.locateTranscript,
|
|
1803
1856
|
});
|
|
1804
1857
|
if (meshCompany)
|
|
1805
|
-
await emitLaneStartedForStories(ctx, meshCompany, started,
|
|
1858
|
+
await emitLaneStartedForStories(ctx, meshCompany, started, started.thread_id);
|
|
1806
1859
|
}
|
|
1807
1860
|
else if (outpostId) {
|
|
1808
1861
|
started = await dispatchCreatedOutpostLane(ctx, {
|
|
@@ -1864,13 +1917,15 @@ async function dispatchCreatedOutpostLane(ctx, input) {
|
|
|
1864
1917
|
const dispatched = await ctx.dispatchOutpostExec({ outpostId, command });
|
|
1865
1918
|
const remote = parseRemotePickupJson(dispatched.stdout);
|
|
1866
1919
|
if (remote?.error === "outpost_self_target") {
|
|
1867
|
-
|
|
1920
|
+
const cancelled = casUpdateLane(hqRoot, lane.lane_id, (current) => ({
|
|
1868
1921
|
...current,
|
|
1869
1922
|
state: "cancelled",
|
|
1870
1923
|
queued_reason: undefined,
|
|
1871
1924
|
terminal_reason: "outpost_self_target",
|
|
1872
1925
|
outpost_command_id: dispatched.commandId,
|
|
1873
1926
|
}));
|
|
1927
|
+
await emitLaneTerminalStateSoft(ctx, cancelled);
|
|
1928
|
+
return cancelled;
|
|
1874
1929
|
}
|
|
1875
1930
|
const stdoutTrimmed = dispatched.stdout.trim();
|
|
1876
1931
|
if (remote === null && stdoutTrimmed !== "" && dispatched.status !== "Success") {
|
|
@@ -1882,6 +1937,7 @@ async function dispatchCreatedOutpostLane(ctx, input) {
|
|
|
1882
1937
|
stderr: dispatched.stderr.slice(0, 2000),
|
|
1883
1938
|
parse_error: "unparseable_pickup_stdout",
|
|
1884
1939
|
});
|
|
1940
|
+
await emitLaneTerminalStateSoft(ctx, failed);
|
|
1885
1941
|
throw new LanesError(DISPATCH_TERMINAL_REASON, "Outpost pickup command failed", {
|
|
1886
1942
|
outpost_id: outpostId,
|
|
1887
1943
|
lane_id: failed.lane_id,
|
|
@@ -1899,6 +1955,7 @@ async function dispatchCreatedOutpostLane(ctx, input) {
|
|
|
1899
1955
|
status: dispatched.status,
|
|
1900
1956
|
stderr: dispatched.stderr.slice(0, 2000),
|
|
1901
1957
|
});
|
|
1958
|
+
await emitLaneTerminalStateSoft(ctx, failed);
|
|
1902
1959
|
throw new LanesError(DISPATCH_TERMINAL_REASON, "Outpost pickup command failed", {
|
|
1903
1960
|
outpost_id: outpostId,
|
|
1904
1961
|
lane_id: failed.lane_id,
|
|
@@ -1923,6 +1980,7 @@ async function dispatchCreatedOutpostLane(ctx, input) {
|
|
|
1923
1980
|
outpost_id: outpostId,
|
|
1924
1981
|
message,
|
|
1925
1982
|
});
|
|
1983
|
+
await emitLaneTerminalStateSoft(ctx, failed);
|
|
1926
1984
|
throw new LanesError(DISPATCH_TERMINAL_REASON, `Failed to dispatch lane to Outpost ${outpostId}: ${message}`, {
|
|
1927
1985
|
outpost_id: outpostId,
|
|
1928
1986
|
lane_id: failed.lane_id,
|
|
@@ -2126,6 +2184,7 @@ async function runPickup(ctx, opts) {
|
|
|
2126
2184
|
env: ctx.env,
|
|
2127
2185
|
});
|
|
2128
2186
|
if (isTerminalLaneState(afterAccount.state)) {
|
|
2187
|
+
await emitLaneTerminalStateSoft(ctx, afterAccount);
|
|
2129
2188
|
printJson({
|
|
2130
2189
|
ok: true,
|
|
2131
2190
|
action: "pickup",
|
|
@@ -3150,17 +3209,32 @@ function callerMayVerifyStory(ctx, lane) {
|
|
|
3150
3209
|
return false;
|
|
3151
3210
|
return sessionIdFromEnv(ctx.env) === lane.senior.id;
|
|
3152
3211
|
}
|
|
3153
|
-
async function ensureStoryThread(ctx, lane, projectId) {
|
|
3212
|
+
async function ensureStoryThread(ctx, lane, projectId, storyId = lane.story_id) {
|
|
3213
|
+
const company = lane.mesh_company ?? lane.company;
|
|
3214
|
+
if (!company)
|
|
3215
|
+
return lane;
|
|
3216
|
+
return ensureLaneThreadRecord(ctx, lane, projectId, storyId);
|
|
3217
|
+
}
|
|
3218
|
+
async function ensureLaneThreadRecord(ctx, lane, projectId = lane.project_id, storyId = lane.story_id, laneStatus = lane.state) {
|
|
3154
3219
|
const company = lane.mesh_company ?? lane.company;
|
|
3155
3220
|
if (!company)
|
|
3156
3221
|
return lane;
|
|
3157
|
-
const
|
|
3158
|
-
|
|
3222
|
+
const descriptorLane = { ...lane, project_id: projectId, story_id: storyId };
|
|
3223
|
+
const threadId = ctx.mesh.ensureLaneThread
|
|
3224
|
+
? await ctx.mesh.ensureLaneThread(company, descriptorLane, laneStatus)
|
|
3225
|
+
: await ctx.mesh.ensureProjectThread(company, projectId);
|
|
3226
|
+
if (threadId === lane.thread_id && !lane.thread_id.startsWith("proj-"))
|
|
3159
3227
|
return lane;
|
|
3160
|
-
|
|
3228
|
+
const updated = casUpdateLane(ctx.hqRoot(), lane.lane_id, (current) => ({
|
|
3161
3229
|
...current,
|
|
3162
3230
|
thread_id: threadId,
|
|
3163
3231
|
}));
|
|
3232
|
+
// The CAS changes the lane version. Refresh once so the server descriptor
|
|
3233
|
+
// and the durable local record converge before the worker starts.
|
|
3234
|
+
if (ctx.mesh.ensureLaneThread) {
|
|
3235
|
+
await ctx.mesh.ensureLaneThread(company, { ...updated, project_id: projectId, story_id: storyId }, laneStatus);
|
|
3236
|
+
}
|
|
3237
|
+
return updated;
|
|
3164
3238
|
}
|
|
3165
3239
|
async function emitStoryTransition(ctx, lane, projectId, storyId, event, status, payload = {}) {
|
|
3166
3240
|
const company = lane.mesh_company ?? lane.company;
|
|
@@ -3174,6 +3248,7 @@ async function emitStoryTransition(ctx, lane, projectId, storyId, event, status,
|
|
|
3174
3248
|
project_id: projectId,
|
|
3175
3249
|
story_id: storyId,
|
|
3176
3250
|
...payload,
|
|
3251
|
+
lane_status: lane.state,
|
|
3177
3252
|
});
|
|
3178
3253
|
await ctx.mesh.patchStoryStatus(company, projectId, storyId, status === "blocked" ? "in_progress" : status);
|
|
3179
3254
|
}
|
|
@@ -3514,6 +3589,7 @@ async function runStop(ctx, laneId) {
|
|
|
3514
3589
|
sleepMs: ctx.stopSleepMs,
|
|
3515
3590
|
});
|
|
3516
3591
|
}
|
|
3592
|
+
await emitLaneTerminalStateSoft(ctx, updated);
|
|
3517
3593
|
const cancelled = cancelPendingQuestionsForLane(hqRoot, laneId, at);
|
|
3518
3594
|
printJson({
|
|
3519
3595
|
ok: true,
|
package/dist/commands/mesh.js
CHANGED
|
@@ -59,14 +59,22 @@ export function formatCheckLines(threads, company, projectId) {
|
|
|
59
59
|
const scope = projectId
|
|
60
60
|
? `${company.companySlug || company.companyUid}/${projectId}`
|
|
61
61
|
: company.companySlug || company.companyUid;
|
|
62
|
-
const lines = [`Work mesh: ${threads.length}
|
|
62
|
+
const lines = [`Work mesh: ${threads.length} thread(s) for ${scope}`];
|
|
63
63
|
for (const thread of threads.slice(0, 8)) {
|
|
64
64
|
const owner = thread.ownerUid ? ` owner=${thread.ownerUid}` : "";
|
|
65
65
|
const summary = thread.progressSummary ||
|
|
66
66
|
thread.sourceSignalSummary ||
|
|
67
67
|
thread.blockedReason ||
|
|
68
68
|
"";
|
|
69
|
-
|
|
69
|
+
const lane = thread.kind === "lane" ? thread.lane : undefined;
|
|
70
|
+
const laneDetails = lane
|
|
71
|
+
? ` lane=${lane.laneId} laneStatus=${thread.laneStatus ?? "unknown"}` +
|
|
72
|
+
` story=${lane.storyId ?? "unknown"} profile=${lane.workerProfile ?? "unknown"}` +
|
|
73
|
+
` model=${lane.model ?? "unknown"} account=${lane.account ?? "unknown"}` +
|
|
74
|
+
`${lane.parentLaneId ? ` parent=${lane.parentLaneId}` : ""}`
|
|
75
|
+
: "";
|
|
76
|
+
const machine = thread.senderMachineUid ? ` machine=${thread.senderMachineUid}` : "";
|
|
77
|
+
lines.push(`- ${thread.threadStatus ?? "unknown"} ${thread.threadId ?? "?"}${owner}${machine}${laneDetails}${summary ? `: ${summary}` : ""}`);
|
|
70
78
|
}
|
|
71
79
|
return lines;
|
|
72
80
|
}
|
|
@@ -320,6 +328,9 @@ function mergeProjectRepos(current, planned) {
|
|
|
320
328
|
return [...planned, ...current.filter((repo) => !plannedPaths.has(repo.path))];
|
|
321
329
|
}
|
|
322
330
|
async function runProjectSet(projectId, opts) {
|
|
331
|
+
if (opts.channel !== undefined && !opts.channel.trim()) {
|
|
332
|
+
fail("PROJECT_CHANNEL_INVALID: --channel must be a non-empty channel id.");
|
|
333
|
+
}
|
|
323
334
|
if (opts.storiesFile && opts.storiesJson) {
|
|
324
335
|
fail("Pass only one of --stories-file or --stories-json.");
|
|
325
336
|
}
|
|
@@ -358,6 +369,7 @@ async function runProjectSet(projectId, opts) {
|
|
|
358
369
|
const principalUid = await resolveMeshPrincipalUid(token, loadCachedTokens());
|
|
359
370
|
const PROJECT_CHANGED = "The Board changed while you were editing. Re-run to pick up the latest.";
|
|
360
371
|
let view;
|
|
372
|
+
let registration;
|
|
361
373
|
if (!current) {
|
|
362
374
|
const ensured = await ensureProjectBoard({
|
|
363
375
|
token,
|
|
@@ -367,10 +379,12 @@ async function runProjectSet(projectId, opts) {
|
|
|
367
379
|
description: opts.description !== undefined ? opts.description : source?.description ?? "",
|
|
368
380
|
stories: source?.stories ?? [],
|
|
369
381
|
repos: explicitRepos ?? source?.repos ?? [],
|
|
382
|
+
channelId: opts.channel,
|
|
370
383
|
});
|
|
371
384
|
view = ensured.view;
|
|
385
|
+
registration = ensured.registration;
|
|
372
386
|
}
|
|
373
|
-
else {
|
|
387
|
+
else if (opts.name !== undefined || opts.description !== undefined || source !== undefined || explicitRepos !== undefined) {
|
|
374
388
|
try {
|
|
375
389
|
view = await putProjectView(token, company.companyUid, projectId, {
|
|
376
390
|
name: opts.name?.trim() || source?.name || current.name,
|
|
@@ -395,18 +409,30 @@ async function runProjectSet(projectId, opts) {
|
|
|
395
409
|
throw err;
|
|
396
410
|
}
|
|
397
411
|
}
|
|
412
|
+
else {
|
|
413
|
+
view = current;
|
|
414
|
+
}
|
|
415
|
+
if (current && opts.channel !== undefined) {
|
|
416
|
+
registration = await registerMeshProject(token, company.companyUid, projectId, opts.channel);
|
|
417
|
+
}
|
|
398
418
|
if (!current && opts.create) {
|
|
399
419
|
noteCallingSessionBound(view.projectId, company);
|
|
400
420
|
}
|
|
401
421
|
if (opts.json) {
|
|
402
|
-
console.log(JSON.stringify({
|
|
422
|
+
console.log(JSON.stringify({
|
|
423
|
+
ok: true,
|
|
424
|
+
action: "project.set",
|
|
425
|
+
principalUid,
|
|
426
|
+
...view,
|
|
427
|
+
...(registration ? { registration } : {}),
|
|
428
|
+
}, null, 2));
|
|
403
429
|
return;
|
|
404
430
|
}
|
|
405
431
|
console.log(`Work mesh: project ${view.projectId} set (${view.name}; ${view.stories.length} stories)`);
|
|
406
432
|
}
|
|
407
433
|
async function runProjectRegister(projectId, opts) {
|
|
408
|
-
if (opts.channel
|
|
409
|
-
fail("--channel
|
|
434
|
+
if (opts.channel !== undefined && !opts.channel.trim()) {
|
|
435
|
+
fail("PROJECT_CHANNEL_INVALID: --channel must be a non-empty channel id.");
|
|
410
436
|
}
|
|
411
437
|
const { token, company } = await withCompany(opts);
|
|
412
438
|
const principalUid = await resolveMeshPrincipalUid(token, loadCachedTokens());
|
|
@@ -425,7 +451,7 @@ async function runProjectRegister(projectId, opts) {
|
|
|
425
451
|
}
|
|
426
452
|
let registration;
|
|
427
453
|
try {
|
|
428
|
-
registration = await registerMeshProject(token, company.companyUid, projectId);
|
|
454
|
+
registration = await registerMeshProject(token, company.companyUid, projectId, opts.channel);
|
|
429
455
|
}
|
|
430
456
|
catch (err) {
|
|
431
457
|
const failure = err;
|
|
@@ -1787,6 +1813,7 @@ export function registerMeshCommand(program) {
|
|
|
1787
1813
|
.option("--stories-json <json>", "Story array or JSON object with userStories")
|
|
1788
1814
|
.option("--repos-json <json>", "Repository array, such as [{path,branch}]")
|
|
1789
1815
|
.option("--repo <path[@branch]>", "Repository binding (repeatable)", collectRepeatable)
|
|
1816
|
+
.option("--channel <channelId>", "Bind the project to an existing company or project channel")
|
|
1790
1817
|
.option("--create", "Create the Board view when the GET is a 404")
|
|
1791
1818
|
.option("--json", "Print machine-readable JSON")
|
|
1792
1819
|
.action((projectId, opts) => wrap(() => runProjectSet(projectId, opts))());
|
|
@@ -1801,7 +1828,7 @@ export function registerMeshCommand(program) {
|
|
|
1801
1828
|
.command("register <projectId>")
|
|
1802
1829
|
.description("Register an existing Board and bind its project channel; use project ensure to create or repair a Board")
|
|
1803
1830
|
.requiredOption("--company <slug|uid>", "Company slug or cloud uid")
|
|
1804
|
-
.option("--channel <channelId>", "
|
|
1831
|
+
.option("--channel <channelId>", "Bind the project to an existing company or project channel")
|
|
1805
1832
|
.option("--json", "Print machine-readable JSON")
|
|
1806
1833
|
.action((projectId, opts) => wrap(() => runProjectRegister(projectId, opts))());
|
|
1807
1834
|
mesh
|
|
@@ -42,11 +42,11 @@
|
|
|
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 type { LaneCompanyRef, LaneEnvelope, LaneRecord } from "./types.js";
|
|
46
|
-
export declare const LANE_MESH_EVENTS: readonly ["lane_started", "lane_progress", "lane_question", "lane_question_at_user", "lane_ask", "lane_blocked", "lane_done"];
|
|
45
|
+
import type { LaneCompanyRef, LaneEnvelope, LaneRecord, TerminalLaneState } from "./types.js";
|
|
46
|
+
export declare const LANE_MESH_EVENTS: readonly ["lane_started", "lane_progress", "lane_question", "lane_question_at_user", "lane_ask", "lane_blocked", "lane_done", "lane_failed", "lane_interrupted", "lane_cancelled", "lane_killed"];
|
|
47
47
|
export type LaneMeshEvent = (typeof LANE_MESH_EVENTS)[number];
|
|
48
48
|
/** Roster kinds this module emits. Subset of the server's `validKinds`. */
|
|
49
|
-
export declare const WIRE_EVENT_KINDS: readonly ["run_started", "run_progress", "run_complete", "question", "note"];
|
|
49
|
+
export declare const WIRE_EVENT_KINDS: readonly ["run_started", "run_progress", "run_complete", "blocked", "question", "note"];
|
|
50
50
|
export type WireEventKind = (typeof WIRE_EVENT_KINDS)[number];
|
|
51
51
|
/** Server caps: activity title/summary 2000, question text 500, note text 500. */
|
|
52
52
|
export declare const WIRE_ACTIVITY_TEXT_MAX = 2000;
|
|
@@ -70,6 +70,7 @@ export declare function toWireThreadEvent(event: LaneMeshEvent, payload: Record<
|
|
|
70
70
|
*/
|
|
71
71
|
export interface LaneMesh {
|
|
72
72
|
ensureProjectThread: (company: LaneCompanyRef, projectId: string) => Promise<string>;
|
|
73
|
+
ensureLaneThread?: (company: LaneCompanyRef, lane: LaneRecord, laneStatus?: string, signal?: AbortSignal) => Promise<string>;
|
|
73
74
|
appendThreadEvent: (company: LaneCompanyRef, threadId: string, event: LaneMeshEvent, payload: Record<string, unknown>, signal?: AbortSignal) => Promise<void>;
|
|
74
75
|
patchStoryStatus: (company: LaneCompanyRef, projectId: string, storyId: string, status: string) => Promise<void>;
|
|
75
76
|
}
|
|
@@ -92,6 +93,7 @@ export declare function createLaneMesh(opts?: CreateLaneMeshOptions): LaneMesh;
|
|
|
92
93
|
export declare function laneMeshCompany(lane: Pick<LaneRecord, "company" | "mesh_company">): LaneCompanyRef | undefined;
|
|
93
94
|
/** Company attribution + thread present. Unscoped personal lanes emit nothing. */
|
|
94
95
|
export declare function laneCanEmit(lane: Pick<LaneRecord, "company" | "mesh_company" | "thread_id">): boolean;
|
|
96
|
+
export declare function laneMeshEventIdentity(lane: LaneRecord): Record<string, unknown>;
|
|
95
97
|
/**
|
|
96
98
|
* Spec §8 terminal-envelope events. `done` → lane_done and story → review;
|
|
97
99
|
* `blocked` → lane_blocked; `ask` → lane_ask with the queue ids. The story is
|
|
@@ -101,6 +103,8 @@ export declare function emitLaneDecisionEvents(mesh: LaneMesh, lane: LaneRecord,
|
|
|
101
103
|
questionIds?: string[];
|
|
102
104
|
signal?: AbortSignal;
|
|
103
105
|
}): Promise<void>;
|
|
106
|
+
/** Emit an abnormal terminal state even when the worker produced no envelope. */
|
|
107
|
+
export declare function emitLaneTerminalStateEvent(mesh: LaneMesh, lane: LaneRecord, state: Exclude<TerminalLaneState, "done">, summary?: string, signal?: AbortSignal): Promise<void>;
|
|
104
108
|
export declare function emitLaneProgressEvent(mesh: LaneMesh, lane: LaneRecord, detail: string, signal?: AbortSignal): Promise<void>;
|
|
105
109
|
/**
|
|
106
110
|
* Throttle for lane_progress (spec §8 progress row). True when no progress
|
|
@@ -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,
|