@cohortapp/agent-sdk 2.11.8 → 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.8",
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
  // ---------------------------------------------------------------------------
@@ -2143,6 +2178,12 @@ async function main() {
2143
2178
  }
2144
2179
  }, POLL_INTERVAL);
2145
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
+
2146
2187
  // Backlog sweep
2147
2188
  setInterval(async () => {
2148
2189
  try {