@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.7",
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
- try {
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
- const apiKey = conn.apiKey || process.env.COHORT_API_KEY || "";
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 = conn.agentId || process.env.COHORT_AGENT_ID || "";
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: conn.base, apiKey, orgId, agentId });
1127
- const mid = res && res.ok && res.member && res.member.memberId ? String(res.member.memberId) : "";
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
+ });