@cohortapp/agent-sdk 2.11.7 → 2.11.9
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cohortapp/agent-sdk",
|
|
3
|
-
"version": "2.11.
|
|
3
|
+
"version": "2.11.9",
|
|
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": {
|
|
@@ -125,6 +125,9 @@ import { policyFor } from "../../lib/execution/surface-policy.mjs";
|
|
|
125
125
|
// ---------------------------------------------------------------------------
|
|
126
126
|
|
|
127
127
|
const POLL_INTERVAL = parseInt(process.env.DAEMON_POLL_INTERVAL || "60000", 10); // 60s (up from 30s to avoid Slack rate limits)
|
|
128
|
+
// Fast reactive scan of the LOCAL cohort inbox only (no API, no rate limit) so an
|
|
129
|
+
// org-push-fetched DM is answered in seconds, not on the 60s poll. See pollCohortFast.
|
|
130
|
+
const COHORT_FAST_INTERVAL = parseInt(process.env.DAEMON_COHORT_POLL_INTERVAL || "2000", 10); // 2s
|
|
128
131
|
// Backlog sweep cadence — bumped 2min → 10min in 1.10. The dispatcher
|
|
129
132
|
// applies a per-item post-completion cooldown (4h success, 30min failure)
|
|
130
133
|
// so we no longer need a tight sweep loop. Operators with high-throughput
|
|
@@ -207,44 +210,8 @@ async function poll() {
|
|
|
207
210
|
}
|
|
208
211
|
|
|
209
212
|
let totalItems = 0;
|
|
210
|
-
|
|
211
213
|
for (const svc of services) {
|
|
212
|
-
|
|
213
|
-
const result = await svc.fn();
|
|
214
|
-
// Dedup by raw_ref first: the same Slack message arrives via
|
|
215
|
-
// multiple paths (channel scan, thread scan, events-server JSON)
|
|
216
|
-
// with different item IDs. Using raw_ref as the lock key ensures
|
|
217
|
-
// a single message is only processed once regardless of path.
|
|
218
|
-
const seenRefs = new Set();
|
|
219
|
-
const newItems = result.items.filter((item) => {
|
|
220
|
-
// Skip the agent's own messages — defensive check in case intake filters miss them
|
|
221
|
-
const sender = (item.sender || "").toLowerCase();
|
|
222
|
-
const me = loadAgent();
|
|
223
|
-
const myFirst = (me.firstName || "").toLowerCase();
|
|
224
|
-
const myFull = (me.fullName || "").toLowerCase().replace(/\s+/g, "-");
|
|
225
|
-
if (myFirst && (sender === myFirst || sender === myFull)) return false;
|
|
226
|
-
|
|
227
|
-
const lockKey = item.raw_ref || item.id || `${svc.name}-${Date.now()}`;
|
|
228
|
-
if (seenRefs.has(lockKey)) return false;
|
|
229
|
-
seenRefs.add(lockKey);
|
|
230
|
-
const lock = acquireLock(lockKey, {
|
|
231
|
-
sender: item.sender || "unknown",
|
|
232
|
-
channel: item.channel || item.channel_id || "unknown",
|
|
233
|
-
});
|
|
234
|
-
return lock.acquired;
|
|
235
|
-
});
|
|
236
|
-
totalItems += newItems.length;
|
|
237
|
-
|
|
238
|
-
for (const item of newItems) {
|
|
239
|
-
await processItem(item, svc.name);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
if (result.errors.length > 0) {
|
|
243
|
-
console.warn(`[daemon] ${svc.name} errors:`, result.errors.slice(0, 3));
|
|
244
|
-
}
|
|
245
|
-
} catch (err) {
|
|
246
|
-
console.error(`[daemon] ${svc.name} poll failed:`, err.message);
|
|
247
|
-
}
|
|
214
|
+
totalItems += await pollService(svc);
|
|
248
215
|
}
|
|
249
216
|
|
|
250
217
|
recordPoll(totalItems);
|
|
@@ -254,6 +221,74 @@ async function poll() {
|
|
|
254
221
|
|
|
255
222
|
}
|
|
256
223
|
|
|
224
|
+
/**
|
|
225
|
+
* Scan + process ONE service's inbox. Factored out of poll() so the reactive
|
|
226
|
+
* cohort path can run on its own fast interval (pollCohortFast) without waiting
|
|
227
|
+
* on the 60s full poll. Returns the count of new items processed. Never throws.
|
|
228
|
+
*/
|
|
229
|
+
async function pollService(svc) {
|
|
230
|
+
try {
|
|
231
|
+
const result = await svc.fn();
|
|
232
|
+
// Dedup by raw_ref first: the same Slack message arrives via multiple paths
|
|
233
|
+
// (channel scan, thread scan, events-server JSON) with different item IDs.
|
|
234
|
+
// Using raw_ref as the lock key ensures a single message is only processed
|
|
235
|
+
// once regardless of path.
|
|
236
|
+
const seenRefs = new Set();
|
|
237
|
+
const newItems = result.items.filter((item) => {
|
|
238
|
+
// Skip the agent's own messages — defensive check in case intake filters miss them
|
|
239
|
+
const sender = (item.sender || "").toLowerCase();
|
|
240
|
+
const me = loadAgent();
|
|
241
|
+
const myFirst = (me.firstName || "").toLowerCase();
|
|
242
|
+
const myFull = (me.fullName || "").toLowerCase().replace(/\s+/g, "-");
|
|
243
|
+
if (myFirst && (sender === myFirst || sender === myFull)) return false;
|
|
244
|
+
|
|
245
|
+
const lockKey = item.raw_ref || item.id || `${svc.name}-${Date.now()}`;
|
|
246
|
+
if (seenRefs.has(lockKey)) return false;
|
|
247
|
+
seenRefs.add(lockKey);
|
|
248
|
+
const lock = acquireLock(lockKey, {
|
|
249
|
+
sender: item.sender || "unknown",
|
|
250
|
+
channel: item.channel || item.channel_id || "unknown",
|
|
251
|
+
});
|
|
252
|
+
return lock.acquired;
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
for (const item of newItems) {
|
|
256
|
+
await processItem(item, svc.name);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (result.errors.length > 0) {
|
|
260
|
+
console.warn(`[daemon] ${svc.name} errors:`, result.errors.slice(0, 3));
|
|
261
|
+
}
|
|
262
|
+
return newItems.length;
|
|
263
|
+
} catch (err) {
|
|
264
|
+
console.error(`[daemon] ${svc.name} poll failed:`, err.message);
|
|
265
|
+
return 0;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ── Fast reactive cohort scan ────────────────────────────────────────────────
|
|
270
|
+
// The org-push wake pulls org DMs/@mentions into state/inbox/cohort/ within ~2s,
|
|
271
|
+
// but the full poll() runs only every POLL_INTERVAL (60s, kept slow because it
|
|
272
|
+
// also hits the RATE-LIMITED Slack API) — so a DM sat up to 60s BETWEEN being
|
|
273
|
+
// fetched and being answered (measured: ~24-44s reactive round-trips that were
|
|
274
|
+
// almost entirely this gap, not the model). Cohort items are local YAML with no
|
|
275
|
+
// API call, so we drain that ONE directory on a short interval. This is the
|
|
276
|
+
// single biggest reactive-latency win. Overlap-guarded; the item lock dedups
|
|
277
|
+
// against the 60s poll's own cohort scan (kept as a safety net).
|
|
278
|
+
let _cohortPoller = null;
|
|
279
|
+
let _cohortFastRunning = false;
|
|
280
|
+
async function pollCohortFast() {
|
|
281
|
+
if (_cohortFastRunning) return 0;
|
|
282
|
+
if (!_existsSync(join(AGENT_REPO_DIR, "config/org.yaml"))) return 0;
|
|
283
|
+
if (!_cohortPoller) _cohortPoller = makeInboxScanPoller("cohort", { agentRoot: AGENT_REPO_DIR });
|
|
284
|
+
_cohortFastRunning = true;
|
|
285
|
+
try {
|
|
286
|
+
return await pollService({ name: "cohort", fn: _cohortPoller });
|
|
287
|
+
} finally {
|
|
288
|
+
_cohortFastRunning = false;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
257
292
|
// ---------------------------------------------------------------------------
|
|
258
293
|
// Process a single item
|
|
259
294
|
// ---------------------------------------------------------------------------
|
|
@@ -1118,13 +1153,26 @@ export async function resolveSelfMemberId(deps = {}) {
|
|
|
1118
1153
|
try {
|
|
1119
1154
|
const cfg = daemonOrgCfg();
|
|
1120
1155
|
const conn = configFromAgent(cfg) || {};
|
|
1121
|
-
|
|
1156
|
+
// configFromAgent returns the org key as `token` (NOT `apiKey`), and the
|
|
1157
|
+
// daemon's creds come from config/org.yaml, not process.env — so read `token`
|
|
1158
|
+
// first and only fall back to env. The slug lives at cfg.org.cohort.agentId
|
|
1159
|
+
// (same source ladderMemberId used); without it fetchSelfProfile can't hit
|
|
1160
|
+
// /members/<slug> and whoami is ambiguous in a multi-agent org.
|
|
1161
|
+
const apiKey = conn.token || conn.apiKey || process.env.COHORT_API_KEY || "";
|
|
1122
1162
|
const orgId = conn.orgId || process.env.COHORT_ORG_ID || "";
|
|
1123
|
-
const agentId =
|
|
1163
|
+
const agentId =
|
|
1164
|
+
conn.agentId ||
|
|
1165
|
+
(cfg && cfg.org && cfg.org.cohort && cfg.org.cohort.agentId) ||
|
|
1166
|
+
process.env.COHORT_AGENT_ID ||
|
|
1167
|
+
"";
|
|
1168
|
+
const base = conn.base || process.env.COHORT_BASE || "";
|
|
1124
1169
|
if (!apiKey || !orgId) return _selfMemberId;
|
|
1125
1170
|
const fetchSelf = deps.fetchSelfProfileImpl || fetchSelfProfile;
|
|
1126
|
-
const res = await fetchSelf({ base
|
|
1127
|
-
|
|
1171
|
+
const res = await fetchSelf({ base, apiKey, orgId, agentId });
|
|
1172
|
+
// fetchSelfProfile returns a res frame with the member under `result` (the
|
|
1173
|
+
// authed call() shape); tolerate `member` too for the raw directory route.
|
|
1174
|
+
const m = res && res.ok ? (res.result || res.member) : null;
|
|
1175
|
+
const mid = m && m.memberId ? String(m.memberId) : "";
|
|
1128
1176
|
if (mid) {
|
|
1129
1177
|
_selfMemberId = mid;
|
|
1130
1178
|
try {
|
|
@@ -2130,6 +2178,12 @@ async function main() {
|
|
|
2130
2178
|
}
|
|
2131
2179
|
}, POLL_INTERVAL);
|
|
2132
2180
|
|
|
2181
|
+
// Fast reactive cohort scan — drains org-push-fetched DMs in ~2s instead of
|
|
2182
|
+
// waiting up to 60s for the full poll. Local YAML only; no API/rate-limit cost.
|
|
2183
|
+
setInterval(() => {
|
|
2184
|
+
pollCohortFast().catch((err) => console.error("[daemon] cohort fast-poll error:", err.message));
|
|
2185
|
+
}, COHORT_FAST_INTERVAL);
|
|
2186
|
+
|
|
2133
2187
|
// Backlog sweep
|
|
2134
2188
|
setInterval(async () => {
|
|
2135
2189
|
try {
|
|
@@ -830,3 +830,35 @@ test("NO FALLTHROUGH: a TRANSIENT quick-reply failure still falls through to a f
|
|
|
830
830
|
assert.equal(dispatched, true, "a transient failure falls through to the full session, as before");
|
|
831
831
|
assert.equal(res.path, "session");
|
|
832
832
|
});
|
|
833
|
+
|
|
834
|
+
test("resolveSelfMemberId reads the member cuid from the res frame's `result` (own-echo keystone)", async () => {
|
|
835
|
+
daemon._resetSelfMemberId();
|
|
836
|
+
const pk = process.env.COHORT_API_KEY, po = process.env.COHORT_ORG_ID, pa = process.env.COHORT_AGENT_ID;
|
|
837
|
+
process.env.COHORT_API_KEY = "nlk_test";
|
|
838
|
+
process.env.COHORT_ORG_ID = "org_test";
|
|
839
|
+
process.env.COHORT_AGENT_ID = "A016";
|
|
840
|
+
try {
|
|
841
|
+
let calls = 0;
|
|
842
|
+
// fetchSelfProfile returns the authed call() res frame: the member is under
|
|
843
|
+
// `result`, and the org key is passed as apiKey. (Both were wrong in 2.11.7.)
|
|
844
|
+
const mid = await daemon.resolveSelfMemberId({
|
|
845
|
+
fetchSelfProfileImpl: async (o) => {
|
|
846
|
+
calls++;
|
|
847
|
+
assert.equal(o.agentId, "A016", "must pass the slug so /members/<slug> resolves");
|
|
848
|
+
return { ok: true, result: { memberId: "cmSELFcuid", slug: o.agentId } };
|
|
849
|
+
},
|
|
850
|
+
});
|
|
851
|
+
assert.equal(mid, "cmSELFcuid");
|
|
852
|
+
// cached: a second call must NOT refetch
|
|
853
|
+
const again = await daemon.resolveSelfMemberId({
|
|
854
|
+
fetchSelfProfileImpl: async () => { throw new Error("must not refetch a cached self id"); },
|
|
855
|
+
});
|
|
856
|
+
assert.equal(again, "cmSELFcuid");
|
|
857
|
+
assert.equal(calls, 1);
|
|
858
|
+
} finally {
|
|
859
|
+
daemon._resetSelfMemberId();
|
|
860
|
+
if (pk === undefined) delete process.env.COHORT_API_KEY; else process.env.COHORT_API_KEY = pk;
|
|
861
|
+
if (po === undefined) delete process.env.COHORT_ORG_ID; else process.env.COHORT_ORG_ID = po;
|
|
862
|
+
if (pa === undefined) delete process.env.COHORT_AGENT_ID; else process.env.COHORT_AGENT_ID = pa;
|
|
863
|
+
}
|
|
864
|
+
});
|