@cohortapp/agent-sdk 2.18.14 → 2.18.15
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/docs/runbooks/fleet-rollout.md +45 -1
- package/lib/assurance/batch.mjs +353 -0
- package/lib/assurance/first-reply.mjs +423 -0
- package/lib/assurance/notice-voice.mjs +357 -0
- package/lib/assurance/plan-note.mjs +43 -0
- package/lib/assurance/room-budget.mjs +55 -6
- package/lib/comms/send-gate.mjs +59 -0
- package/lib/identity/persona.mjs +31 -2
- package/lib/telemetry/alerts.mjs +94 -0
- package/lib/telemetry/collect.mjs +26 -2
- package/package.json +1 -1
- package/scripts/daemon/agent-daemon.mjs +75 -5
- package/scripts/daemon/assurance.mjs +709 -44
- package/scripts/daemon/deliver.mjs +109 -0
- package/scripts/daemon/dispatcher.mjs +21 -3
- package/scripts/daemon/inbox-deferral.mjs +102 -9
- package/scripts/daemon/session-lock.mjs +41 -1
- package/scripts/fleet/rollout.mjs +246 -7
- package/scripts/local-triggers/autoupdate.sh +144 -11
package/lib/telemetry/alerts.mjs
CHANGED
|
@@ -19,6 +19,10 @@
|
|
|
19
19
|
* - subAgentsRunning over the cap → INFO (more children than expected).
|
|
20
20
|
* - machine.sessionNote present → WARNING/CRITICAL (frontdoor) — the
|
|
21
21
|
* seat's front door is not answering.
|
|
22
|
+
* - machine.upgrade.failStreak >= N → WARNING/CRITICAL (rollout) — N
|
|
23
|
+
* consecutive attempts to reach
|
|
24
|
+
* @latest have failed; the seat is
|
|
25
|
+
* stranded on old code.
|
|
22
26
|
*
|
|
23
27
|
* Output: a deterministic array `[{ id, severity, kind, detail }, ...]` (the
|
|
24
28
|
* snapshot's alert shape) sorted critical→warning→info then by id, so identical
|
|
@@ -90,6 +94,30 @@ export const DEFAULT_THRESHOLDS = Object.freeze({
|
|
|
90
94
|
warnMs: 15 * 60 * 1000,
|
|
91
95
|
critMs: 2 * 60 * 60 * 1000,
|
|
92
96
|
}),
|
|
97
|
+
/**
|
|
98
|
+
* How many CONSECUTIVE failed attempts to reach @latest make a seat STUCK.
|
|
99
|
+
*
|
|
100
|
+
* `warnStreak` 3, not 1: one failure is a bad release or a slow mirror and
|
|
101
|
+
* the hourly job is entitled to try again; the failed-target hold already
|
|
102
|
+
* spaces those out. Three separate, completed attempts have failed, which
|
|
103
|
+
* means every fix the seat can apply to itself has been applied three times
|
|
104
|
+
* and the seat is still where it was.
|
|
105
|
+
*
|
|
106
|
+
* `critStreak` 6: past this the seat has been refusing @latest for the best
|
|
107
|
+
* part of a week under the default 24 h hold — which is exactly how long
|
|
108
|
+
* three seats sat on 2.17.0 while every hourly log line was individually
|
|
109
|
+
* correct. A release that cannot reach a colleague is not a machine problem
|
|
110
|
+
* that resolves itself; it waits for a person, like a scope fault.
|
|
111
|
+
*/
|
|
112
|
+
upgradeStuck: Object.freeze({
|
|
113
|
+
// THE ONE DEFINITION of the stuck threshold. scripts/fleet/rollout.mjs
|
|
114
|
+
// imports `warnStreak` as its STUCK_STREAK rather than restating it, and
|
|
115
|
+
// the shell copy (autoupdate.sh) is pinned against this value by a
|
|
116
|
+
// source-reading test — because three literals agreeing by comment is the
|
|
117
|
+
// prose-promise-instead-of-a-check pattern this repo bans.
|
|
118
|
+
warnStreak: 3,
|
|
119
|
+
critStreak: 6,
|
|
120
|
+
}),
|
|
93
121
|
replyDebt: Object.freeze({
|
|
94
122
|
// ONE withheld reply is already worth saying. This is not a resource gauge
|
|
95
123
|
// where a low reading is normal noise — it counts people who asked this
|
|
@@ -175,6 +203,7 @@ export function deriveAlerts(status, thresholds, now = Date.now()) {
|
|
|
175
203
|
push(alerts, frontDoorRule(machine, t.frontDoor, now));
|
|
176
204
|
push(alerts, scopeFaultRule(machine));
|
|
177
205
|
push(alerts, repliesWithheldRule(machine, t.replyDebt));
|
|
206
|
+
push(alerts, upgradeStuckRule(machine, t.upgradeStuck, now));
|
|
178
207
|
|
|
179
208
|
alerts.sort((a, b) => {
|
|
180
209
|
const r = (SEVERITY_RANK[a.severity] ?? 9) - (SEVERITY_RANK[b.severity] ?? 9);
|
|
@@ -282,6 +311,71 @@ function repliesWithheldRule(machine, cfg = {}) {
|
|
|
282
311
|
};
|
|
283
312
|
}
|
|
284
313
|
|
|
314
|
+
/**
|
|
315
|
+
* THIS SEAT CANNOT GET TO @latest, AND HAS PROVED IT N TIMES.
|
|
316
|
+
*
|
|
317
|
+
* Every other rule in this file reads a gauge. This one reads a COUNT of
|
|
318
|
+
* completed failures, because the failure it names has no gauge: on 2026-09-25
|
|
319
|
+
* three seats were found sitting on 2.17.0 while the fleet ran 2.18.13, and
|
|
320
|
+
* nothing anywhere was in an error state. The launchd job fired hourly, npm
|
|
321
|
+
* answered, the install ran, the health gate did its job, the rollback worked,
|
|
322
|
+
* `last.json` recorded the outcome honestly and the beat carried it. Each hour
|
|
323
|
+
* was a correct, self-contained failure — and the org has no way to see the
|
|
324
|
+
* difference between the first one and the fortieth, which is the only thing
|
|
325
|
+
* that distinguishes "a release is settling" from "a colleague is stranded on
|
|
326
|
+
* code from three weeks ago and will never leave it unaided".
|
|
327
|
+
*
|
|
328
|
+
* WHY THIS RIDES THE EXISTING BEAT AND NOTHING ELSE. The seats this rule is
|
|
329
|
+
* about are, by construction, running the OLDEST code in the fleet — so any
|
|
330
|
+
* new channel it might use is the one channel they do not have. `machine.upgrade`
|
|
331
|
+
* is a field their beat already carries; the count is added to it, and this
|
|
332
|
+
* rule turns it into an alert on the seats new enough to run this file.
|
|
333
|
+
*
|
|
334
|
+
* FOR THE ONES THAT ARE NOT, THIS RULE IS STRUCTURALLY BLIND AND SAYING SO IS
|
|
335
|
+
* PART OF THE DESIGN. A seat too stale to install the SDK runs a collector that
|
|
336
|
+
* never writes `failStreak` and an alerts file that has never heard of this
|
|
337
|
+
* rule — so the alert cannot reach exactly the seats it was written for. The
|
|
338
|
+
* fleet-side derivation covers them from outside: `seatStuck()` in
|
|
339
|
+
* scripts/fleet/rollout.mjs reads the same fact out of a 2.17-era beat, and
|
|
340
|
+
* `propagationOutcome()` there returns reason `"stuck"` so that stage 3 of
|
|
341
|
+
* every publish exits non-zero and names the machine. That path is automatic
|
|
342
|
+
* because publishing is; nobody has to decide to go looking.
|
|
343
|
+
*
|
|
344
|
+
* Both answer (d) the same way: a streak counts ATTEMPTS, and a seat that has
|
|
345
|
+
* not been asked has made none.
|
|
346
|
+
*
|
|
347
|
+
* DISTINCT FROM DRIFT. A `stranded` seat (framework paths pinned in
|
|
348
|
+
* `.maestroignore`, `machine.stranded`) cannot RECEIVE parts of a release it
|
|
349
|
+
* did install; a STUCK seat never installs the release at all. Different
|
|
350
|
+
* cause, different fix, different alert id — they can be true at once.
|
|
351
|
+
*/
|
|
352
|
+
function upgradeStuckRule(machine, cfg = {}, now = Date.now()) {
|
|
353
|
+
const u = machine && machine.upgrade;
|
|
354
|
+
if (!u || typeof u !== "object") return null;
|
|
355
|
+
// Strict: the producer (autoupdate.sh via collect.upgradeSummary) writes an
|
|
356
|
+
// integer, and both validate it. A string here means something else wrote
|
|
357
|
+
// this record, and a rule that coerces would be reporting on a shape it does
|
|
358
|
+
// not understand.
|
|
359
|
+
const n = Number.isInteger(u.failStreak) ? u.failStreak : 0;
|
|
360
|
+
if (!(n > 0)) return null;
|
|
361
|
+
const sev = severityFor(n, num(cfg.warnStreak, Infinity), num(cfg.critStreak, Infinity));
|
|
362
|
+
if (!sev) return null;
|
|
363
|
+
const target = typeof u.streakTarget === "string" && u.streakTarget ? u.streakTarget : (typeof u.to === "string" ? u.to : "@latest");
|
|
364
|
+
// A DURATION, not only a tally: four attempts since Monday and four attempts
|
|
365
|
+
// in the last hour are different seats with different urgency, and the count
|
|
366
|
+
// alone cannot tell them apart.
|
|
367
|
+
const sinceMs = typeof u.stuckSince === "string" ? Date.parse(u.stuckSince) : NaN;
|
|
368
|
+
const days = Number.isFinite(sinceMs) ? Math.floor((now - sinceMs) / 86400000) : null;
|
|
369
|
+
const forHow = days === null ? "" : days >= 1 ? ` over ${days} ${days === 1 ? "day" : "days"}` : " today";
|
|
370
|
+
const why = typeof u.reason === "string" && u.reason ? ` Last failure: ${u.reason.split(":")[0]}.` : "";
|
|
371
|
+
return {
|
|
372
|
+
id: "upgrade_stuck",
|
|
373
|
+
severity: sev,
|
|
374
|
+
kind: "rollout",
|
|
375
|
+
detail: `${n} consecutive upgrade attempts to ${target} have failed${forHow} — this seat is running ${typeof u.from === "string" && u.from ? u.from : "older code"} and will not arrive on its own.${why} It needs a person at the machine.`,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
285
379
|
function subAgentsRule(status, cfg = {}) {
|
|
286
380
|
const v = num(status.subAgentsRunning);
|
|
287
381
|
const cap = num(cfg.infoCap, Infinity);
|
|
@@ -84,8 +84,14 @@
|
|
|
84
84
|
* // Also on `machine` (open record), so hq's fleet view can show it next
|
|
85
85
|
* // to sdkVersion without a schema change. `ok` is the attempt's verdict:
|
|
86
86
|
* // true = healthy on `to`; false = install failed or rolled back to `from`.
|
|
87
|
+
* // `failStreak` is the one field here that describes a PATTERN: N
|
|
88
|
+
* // consecutive failed attempts to reach @latest, since `stuckSince`,
|
|
89
|
+
* // against `streakTarget`. Absent below 1. A seat that has simply not
|
|
90
|
+
* // been asked (nothing newer published, or held after its last failure)
|
|
91
|
+
* // never accrues one — see autoupdate.sh's write_last.
|
|
87
92
|
* upgrade?: { at: ISO8601, from: string, to: string, ok: boolean,
|
|
88
|
-
* reason?: string, healthy?: boolean
|
|
93
|
+
* reason?: string, healthy?: boolean, // reason: WHY a failure failed
|
|
94
|
+
* failStreak?: number, stuckSince?: ISO8601, streakTarget?: string },
|
|
89
95
|
* // THE BEATING DAEMON — which process is emitting this beat, what code
|
|
90
96
|
* // it is actually running, and the last health-gate verdict on it.
|
|
91
97
|
* // Also on `machine` (open record). `sdkVersion` ABOVE is what is
|
|
@@ -1395,7 +1401,9 @@ export function sessionNote(a = {}) {
|
|
|
1395
1401
|
* fields the fleet view reads; anything malformed → null, and the field drops
|
|
1396
1402
|
* out rather than lying.
|
|
1397
1403
|
* @param {object|null} last
|
|
1398
|
-
* @returns {{at:string, from:string, to:string, ok:boolean
|
|
1404
|
+
* @returns {{at:string, from:string, to:string, ok:boolean, reason?:string,
|
|
1405
|
+
* healthy?:boolean, failStreak?:number, stuckSince?:string,
|
|
1406
|
+
* streakTarget?:string}|null}
|
|
1399
1407
|
*/
|
|
1400
1408
|
export function upgradeSummary(last) {
|
|
1401
1409
|
if (!last || typeof last !== "object") return null;
|
|
@@ -1413,6 +1421,19 @@ export function upgradeSummary(last) {
|
|
|
1413
1421
|
// Bounded on the way out: a reason is a short token plus at most a trimmed
|
|
1414
1422
|
// error line, and the beat is not a log shipper.
|
|
1415
1423
|
const reason = typeof last.reason === "string" ? last.reason.trim().slice(0, 300) : "";
|
|
1424
|
+
// `failStreak` — how many CONSECUTIVE upgrade attempts have failed, with the
|
|
1425
|
+
// instant the run of failures began and the version it is failing against.
|
|
1426
|
+
// Written by autoupdate.sh's write_last; see the long block above it for what
|
|
1427
|
+
// does and does not count as an attempt. This is the ONLY field here that is
|
|
1428
|
+
// about a pattern rather than an event, and it is the reason three seats sat
|
|
1429
|
+
// on 2.17.0 for days without anything saying so: every individual record was
|
|
1430
|
+
// a truthful `ok:false` that told a reader nothing about the fortieth one.
|
|
1431
|
+
//
|
|
1432
|
+
// Absent when zero, so a healthy seat's beat carries nothing extra and a
|
|
1433
|
+
// reader can treat presence as the finding.
|
|
1434
|
+
const failStreak = Number.isInteger(last.failStreak) && last.failStreak > 0 ? last.failStreak : 0;
|
|
1435
|
+
const stuckSince = typeof last.stuckSince === "string" && !Number.isNaN(Date.parse(last.stuckSince)) ? last.stuckSince : "";
|
|
1436
|
+
const streakTarget = typeof last.streakTarget === "string" ? last.streakTarget.trim().slice(0, 40) : "";
|
|
1416
1437
|
return {
|
|
1417
1438
|
at,
|
|
1418
1439
|
from: typeof last.from === "string" ? last.from : "",
|
|
@@ -1420,6 +1441,9 @@ export function upgradeSummary(last) {
|
|
|
1420
1441
|
ok: last.ok === true,
|
|
1421
1442
|
...(reason ? { reason } : {}),
|
|
1422
1443
|
...(typeof last.healthy === "boolean" ? { healthy: last.healthy } : {}),
|
|
1444
|
+
...(failStreak ? { failStreak } : {}),
|
|
1445
|
+
...(failStreak && stuckSince ? { stuckSince } : {}),
|
|
1446
|
+
...(failStreak && streakTarget ? { streakTarget } : {}),
|
|
1423
1447
|
};
|
|
1424
1448
|
}
|
|
1425
1449
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cohortapp/agent-sdk",
|
|
3
|
-
"version": "2.18.
|
|
3
|
+
"version": "2.18.15",
|
|
4
4
|
"description": "Cohort Agent SDK — autonomous AI colleague runtime. Deploy senior AI colleagues on dedicated Mac minis, wired to the Cohort operating surface.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -67,6 +67,7 @@ import { classifyItem, isDirectedAtAgent } from "./classifier.mjs";
|
|
|
67
67
|
// gets a deterministic classification (no LLM triage), no holding ack, and a
|
|
68
68
|
// per-seat jitter so thirteen answers do not land in the same second.
|
|
69
69
|
import { isRollCallItem, rollCallJitterMs } from "../../lib/org/inbound/broadcast.mjs";
|
|
70
|
+
import { stampFirstReplyPlan } from "../../lib/assurance/first-reply.mjs";
|
|
70
71
|
import { isRoomSurface } from "../../lib/org/inbound/surfaces.mjs";
|
|
71
72
|
import { isMembershipReason } from "../../lib/org/inbound/directedness.mjs";
|
|
72
73
|
import { cohortSurfaceOf, cohortSurfaceLabel, cohortSurfaceIsDeclared } from "./deliver.mjs";
|
|
@@ -96,7 +97,7 @@ import {
|
|
|
96
97
|
isPidAlive,
|
|
97
98
|
} from "./assurance.mjs";
|
|
98
99
|
import { recordPoll, recordClassification, recordSession, writeHealthDashboard } from "./health.mjs";
|
|
99
|
-
import { acquireLock, releaseLock, updateLock, scanStaleLocks, acquireThreadLock, claimRequest, hasActiveClaim, sweepStaleItemClaims, sanitiseItemId } from "./session-lock.mjs";
|
|
100
|
+
import { acquireLock, releaseLock, updateLock, scanStaleLocks, acquireThreadLock, threadLockKey, claimRequest, hasActiveClaim, sweepStaleItemClaims, sanitiseItemId } from "./session-lock.mjs";
|
|
100
101
|
import { markDeferred } from "./inbox-deferral.mjs";
|
|
101
102
|
import { parseQueueItems, rankBacklog, resolveBacklogWeights } from "../../lib/backlog.mjs";
|
|
102
103
|
import { readLatestGaps } from "../../lib/goals/gaps.mjs";
|
|
@@ -322,10 +323,23 @@ async function reviveFrontDoorIfShut(state, deps = {}) {
|
|
|
322
323
|
return verdict;
|
|
323
324
|
}
|
|
324
325
|
|
|
325
|
-
|
|
326
|
+
/**
|
|
327
|
+
* Drain one service's inbox: poll, dedup, stamp the flurry plan, process.
|
|
328
|
+
*
|
|
329
|
+
* @param {{name:string, fn:Function}} svc
|
|
330
|
+
* @param {object} [deps] TEST-ONLY seams; PRODUCTION PASSES NOTHING, so the
|
|
331
|
+
* defaults are the real collaborators and behaviour is byte-identical. The
|
|
332
|
+
* same idiom as `processItem`/`sweepBacklog` above. Exported (and seamed) so
|
|
333
|
+
* the ONE-FIRST-REPLY-PER-FLURRY stamping has a test that fails when the call
|
|
334
|
+
* is removed — see `notice-emission.test.mjs`.
|
|
335
|
+
* @param {object} [deps.frontDoorGate] override the front-door gate
|
|
336
|
+
* @param {Function} [deps.processItem] override processItem
|
|
337
|
+
*/
|
|
338
|
+
export async function pollService(svc, deps = {}) {
|
|
339
|
+
const processItemImpl = deps.processItem || processItem;
|
|
326
340
|
try {
|
|
327
341
|
// Front door: a live main session owns this service's inbox — leave it.
|
|
328
|
-
const fd = _frontDoorGate.check(svc.name);
|
|
342
|
+
const fd = (deps.frontDoorGate || _frontDoorGate).check(svc.name);
|
|
329
343
|
if (fd.changed) console.log(`[daemon] front door for ${svc.name}: ${fd.dispatch ? "daemon dispatches" : "main session owns intake"} (${fd.reason})`);
|
|
330
344
|
if (!fd.dispatch) return 0;
|
|
331
345
|
const result = await svc.fn();
|
|
@@ -358,8 +372,33 @@ async function pollService(svc) {
|
|
|
358
372
|
return lock.acquired;
|
|
359
373
|
});
|
|
360
374
|
|
|
375
|
+
// ── ONE FIRST REPLY PER FLURRY ───────────────────────────────────────
|
|
376
|
+
// "If there's a flurry of 4 messages in one go by others, instead of having
|
|
377
|
+
// a generic reply per message, it would be responding to the batch of
|
|
378
|
+
// messages." (owner, 2026-09-25.) The batch is computed HERE, over the whole
|
|
379
|
+
// poll, because this is the only place that sees more than one item at a
|
|
380
|
+
// time — `processItem` is called per item and structurally cannot know it is
|
|
381
|
+
// the third of four. Every non-leader carries `covered_by_batch`, which
|
|
382
|
+
// `assurance.shouldAcknowledge` turns into `ack:false` and the daemon makes
|
|
383
|
+
// durable as `interimForbidden`, so no later sweep in any process speaks for
|
|
384
|
+
// it either. The leader is told how many it stands for, so the one line it
|
|
385
|
+
// writes can answer all of them.
|
|
386
|
+
//
|
|
387
|
+
// WORK IS UNAFFECTED. Every item still gets its own `processItem`, its own
|
|
388
|
+
// obligation and its own answer; what is batched is only the FIRST thing
|
|
389
|
+
// said. Silence about timing is not silence about outcome.
|
|
390
|
+
//
|
|
391
|
+
// The stamping itself is `assurance/first-reply.stampFirstReplyPlan`, not
|
|
392
|
+
// four lines here, because four lines here had NO TEST THAT COULD SEE THEM:
|
|
393
|
+
// the only call site was inside this module-internal function, so deleting
|
|
394
|
+
// them left the whole suite green — including a test named "wired, not just
|
|
395
|
+
// available". `first-reply.test.mjs` now exercises the stamp and
|
|
396
|
+
// `notice-emission.test.mjs` drives THIS function through `pollService`'s
|
|
397
|
+
// injected seams, so removing the call is red in both.
|
|
398
|
+
stampFirstReplyPlan(newItems);
|
|
399
|
+
|
|
361
400
|
for (const item of newItems) {
|
|
362
|
-
await
|
|
401
|
+
await processItemImpl(item, svc.name);
|
|
363
402
|
}
|
|
364
403
|
|
|
365
404
|
if (result.errors.length > 0) {
|
|
@@ -1107,7 +1146,38 @@ export async function answerItem(item, service, itemId, trace_id, deps = {}, rou
|
|
|
1107
1146
|
// This check runs BEFORE quick reply to prevent ALL duplicate responses.
|
|
1108
1147
|
{
|
|
1109
1148
|
const channel = item.channel_id || (item.raw_ref ? item.raw_ref.match(/slack:([^:]+):/)?.[1] : null) || item.channel;
|
|
1110
|
-
|
|
1149
|
+
// THE UNTREADED CHANNEL POST USED TO TAKE NO LOCK AT ALL.
|
|
1150
|
+
//
|
|
1151
|
+
// `item.thread_id || (isDm ? "dm-channel" : null)` is null for exactly
|
|
1152
|
+
// one shape: a message posted to the main surface of a channel or group,
|
|
1153
|
+
// not in a thread, not a DM. That is the shape the owner screenshotted —
|
|
1154
|
+
// a flurry of messages into a room — and it was the one shape with no
|
|
1155
|
+
// dedup: each message cleared this block, spawned its own session, and
|
|
1156
|
+
// opened its own obligation, so the room got N sessions and N chances at
|
|
1157
|
+
// a holding line for one conversation.
|
|
1158
|
+
//
|
|
1159
|
+
// `channel-main` is the DM lock's counterpart for that surface: one
|
|
1160
|
+
// session at a time on a room's main feed, the rest deferred and
|
|
1161
|
+
// promoted at session close exactly like a DM burst.
|
|
1162
|
+
//
|
|
1163
|
+
// THIS IS ONLY SAFE BECAUSE OF THE BUNDLING FIX BESIDE IT. Deferral ends
|
|
1164
|
+
// in `promoteDeferred`, which collapses a group to its latest member and
|
|
1165
|
+
// marks the rest `.processed-bundled`. On a shared channel the deferred
|
|
1166
|
+
// set is several PEOPLE, not one person's flurry, so an unconditional
|
|
1167
|
+
// collapse here would silently bin a colleague's question — which is why
|
|
1168
|
+
// inbox-deferral.mjs now splits a group by sender unless the winner
|
|
1169
|
+
// actually carries the room's history. Widening this lock without that
|
|
1170
|
+
// change would trade a message storm for lost messages.
|
|
1171
|
+
//
|
|
1172
|
+
// The off-switch is real and deliberate: MAESTRO_CHANNEL_MAIN_LOCK=0
|
|
1173
|
+
// restores the previous behaviour on a seat without a code change, in
|
|
1174
|
+
// the same style as every other threshold in the assurance stack.
|
|
1175
|
+
const threadTs = threadLockKey({
|
|
1176
|
+
channel,
|
|
1177
|
+
threadId: item.thread_id,
|
|
1178
|
+
isDm,
|
|
1179
|
+
channelMainLock: String(process.env.MAESTRO_CHANNEL_MAIN_LOCK ?? "1") !== "0",
|
|
1180
|
+
});
|
|
1111
1181
|
if (threadTs && channel) {
|
|
1112
1182
|
const threadCheck = acquireThreadLock(channel, threadTs);
|
|
1113
1183
|
if (!threadCheck.allowed) {
|