commonswarm 0.1.58 → 0.1.60

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.
Files changed (2) hide show
  1. package/cswarm.cjs +488 -438
  2. package/package.json +1 -1
package/cswarm.cjs CHANGED
@@ -30841,6 +30841,336 @@ function idlePollHelpSentence(defaultMs = IDLE_POLL_DEFAULT_MS) {
30841
30841
  return `listen start --poll-interval sets how long the listener waits after an empty claim (default ${formatIdlePollDuration(defaultMs)}). A whole number plus s or m (for example ${idlePollDurationHint()}), ${idlePollBoundSentence()}. Empty polls double that wait up to ${IDLE_POLL_MAX_LABEL}; any delivery resets it to the configured interval.`;
30842
30842
  }
30843
30843
 
30844
+ // src/listener/wake.ts
30845
+ var LISTENER_RECONCILE_POLL_MS = 3e5;
30846
+ var WAKE_COALESCE_MS = 1e3;
30847
+ var WAKE_CLAIMS_PER_MINUTE_BUDGET = 50;
30848
+ var WAKE_RATE_LIMIT_POLL_MS = 6e4;
30849
+ var REALTIME_SUBSCRIBE_STATUS = {
30850
+ SUBSCRIBED: "SUBSCRIBED",
30851
+ CHANNEL_ERROR: "CHANNEL_ERROR",
30852
+ CLOSED: "CLOSED",
30853
+ TIMED_OUT: "TIMED_OUT"
30854
+ };
30855
+ var LISTENER_WAKE_MODES = ["push", "poll"];
30856
+ var LISTENER_WAKE_MODE_PUSH = LISTENER_WAKE_MODES[0];
30857
+ var LISTENER_WAKE_MODE_POLL = LISTENER_WAKE_MODES[1];
30858
+ var LISTENER_WAKE_MODE_SET = new Set(
30859
+ LISTENER_WAKE_MODES
30860
+ );
30861
+ var WAKE_ERROR_CODES = [
30862
+ "channel_error",
30863
+ "closed",
30864
+ "timed_out",
30865
+ "rate_limited",
30866
+ "wake_budget"
30867
+ ];
30868
+ var WAKE_ERROR_CODE_SET = new Set(WAKE_ERROR_CODES);
30869
+ var WAKE_ERROR_CODE_WAKE_BUDGET = WAKE_ERROR_CODES.find(
30870
+ (code) => code === "wake_budget"
30871
+ );
30872
+ var LISTENER_WAKE_STATUS_KEYS = [
30873
+ "mode",
30874
+ "subscribedAt",
30875
+ "reconnects",
30876
+ "lastWakeAt",
30877
+ "lastReconcileAt",
30878
+ "errorCode",
30879
+ "topicRotatedAt",
30880
+ "rateLimited"
30881
+ ];
30882
+ var LISTENER_WAKE_SENSITIVE_KEYS = [
30883
+ "topic",
30884
+ "wakeTopic",
30885
+ "wake_topic"
30886
+ ];
30887
+ function defaultRealtime(target2) {
30888
+ const client = createClient(target2.url, target2.anonKey, {
30889
+ auth: { persistSession: false, autoRefreshToken: false }
30890
+ });
30891
+ return client.realtime;
30892
+ }
30893
+ function isSubscribeStatus(value) {
30894
+ return value === REALTIME_SUBSCRIBE_STATUS.SUBSCRIBED || value === REALTIME_SUBSCRIBE_STATUS.CHANNEL_ERROR || value === REALTIME_SUBSCRIBE_STATUS.CLOSED || value === REALTIME_SUBSCRIBE_STATUS.TIMED_OUT;
30895
+ }
30896
+ function wakeErrorCodeFromSubscribeStatus(status) {
30897
+ switch (status) {
30898
+ case REALTIME_SUBSCRIBE_STATUS.CHANNEL_ERROR:
30899
+ return "channel_error";
30900
+ case REALTIME_SUBSCRIBE_STATUS.CLOSED:
30901
+ return "closed";
30902
+ case REALTIME_SUBSCRIBE_STATUS.TIMED_OUT:
30903
+ return "timed_out";
30904
+ default:
30905
+ return null;
30906
+ }
30907
+ }
30908
+ function emptyListenerWakeStatus() {
30909
+ return {
30910
+ mode: LISTENER_WAKE_MODE_POLL,
30911
+ subscribedAt: null,
30912
+ reconnects: 0,
30913
+ lastWakeAt: null,
30914
+ lastReconcileAt: null,
30915
+ errorCode: null,
30916
+ topicRotatedAt: null,
30917
+ rateLimited: false
30918
+ };
30919
+ }
30920
+ function listenerWakePersistWorthy(previous, next, lastPersistMs, nowMs) {
30921
+ if (previous === void 0) return true;
30922
+ if (previous.mode !== next.mode || previous.rateLimited !== next.rateLimited || previous.errorCode !== next.errorCode || previous.reconnects !== next.reconnects || previous.subscribedAt !== next.subscribedAt || previous.topicRotatedAt !== next.topicRotatedAt || previous.lastReconcileAt !== next.lastReconcileAt) {
30923
+ return true;
30924
+ }
30925
+ if (previous.lastWakeAt !== next.lastWakeAt) {
30926
+ return nowMs - lastPersistMs >= WAKE_COALESCE_MS;
30927
+ }
30928
+ return false;
30929
+ }
30930
+ function listenerWakeStatusSentence(wake, pollIntervalMs, lastWakeLabel) {
30931
+ if (wake.mode === LISTENER_WAKE_MODE_PUSH) {
30932
+ const last = lastWakeLabel === null ? "no wake yet" : `last wake ${lastWakeLabel}`;
30933
+ return `${LISTENER_WAKE_MODE_PUSH} (Realtime), ${last}, reconcile every ${formatIdlePollDuration(LISTENER_RECONCILE_POLL_MS)}.`;
30934
+ }
30935
+ if (wake.errorCode === WAKE_ERROR_CODE_WAKE_BUDGET) {
30936
+ return `Subscribed; claims paused until the minute clears (${WAKE_ERROR_CODE_WAKE_BUDGET}); polling every ${formatIdlePollDuration(pollIntervalMs)} meanwhile.`;
30937
+ }
30938
+ const code = wake.errorCode ?? "disconnected";
30939
+ return `${LISTENER_WAKE_MODE_POLL} every ${formatIdlePollDuration(pollIntervalMs)}. Realtime not connected (${code}).`;
30940
+ }
30941
+ var WakeSubscriber = class {
30942
+ now;
30943
+ target;
30944
+ createRealtime;
30945
+ realtime = null;
30946
+ channel = null;
30947
+ topic = null;
30948
+ waiter = null;
30949
+ pending = null;
30950
+ connectionState = "disconnected";
30951
+ subscribedAt = null;
30952
+ reconnects = 0;
30953
+ lastWakeAt = null;
30954
+ lastReconcileAt = null;
30955
+ lastErrorCode = null;
30956
+ topicRotatedAt = null;
30957
+ rateLimitedUntil = 0;
30958
+ lastClaimAt = 0;
30959
+ wakeClaimTimes = [];
30960
+ closed = false;
30961
+ everSubscribed = false;
30962
+ constructor(options) {
30963
+ this.target = options.target;
30964
+ this.now = options.now ?? Date.now;
30965
+ this.createRealtime = options.createRealtime ?? defaultRealtime;
30966
+ }
30967
+ get state() {
30968
+ return this.connectionState;
30969
+ }
30970
+ get hasTopic() {
30971
+ return this.topic !== null;
30972
+ }
30973
+ snapshot(nowMs = this.now()) {
30974
+ const serverLimited = nowMs < this.rateLimitedUntil;
30975
+ const overBudget = this.overWakeBudget(nowMs);
30976
+ const rateLimited = serverLimited || overBudget;
30977
+ const mode3 = this.connectionState === "subscribed" && !rateLimited ? LISTENER_WAKE_MODE_PUSH : LISTENER_WAKE_MODE_POLL;
30978
+ const errorCode = serverLimited ? "rate_limited" : overBudget ? WAKE_ERROR_CODE_WAKE_BUDGET : this.lastErrorCode;
30979
+ return {
30980
+ mode: mode3,
30981
+ subscribedAt: this.subscribedAt,
30982
+ reconnects: this.reconnects,
30983
+ lastWakeAt: this.lastWakeAt,
30984
+ lastReconcileAt: this.lastReconcileAt,
30985
+ errorCode,
30986
+ topicRotatedAt: this.topicRotatedAt,
30987
+ rateLimited
30988
+ };
30989
+ }
30990
+ noteReconcile(nowMs = this.now()) {
30991
+ this.lastReconcileAt = new Date(nowMs).toISOString();
30992
+ }
30993
+ noteClaim(nowMs = this.now()) {
30994
+ this.lastClaimAt = nowMs;
30995
+ }
30996
+ noteWakeClaim(nowMs = this.now()) {
30997
+ this.noteClaim(nowMs);
30998
+ this.wakeClaimTimes.push(nowMs);
30999
+ this.trimWakeClaims(nowMs);
31000
+ }
31001
+ coalescingRemainingMs(nowMs = this.now()) {
31002
+ if (this.lastClaimAt <= 0) return 0;
31003
+ return Math.max(0, this.lastClaimAt + WAKE_COALESCE_MS - nowMs);
31004
+ }
31005
+ overWakeBudget(nowMs = this.now()) {
31006
+ this.trimWakeClaims(nowMs);
31007
+ return this.wakeClaimTimes.length >= WAKE_CLAIMS_PER_MINUTE_BUDGET;
31008
+ }
31009
+ canClaimOnWake(nowMs = this.now()) {
31010
+ if (nowMs < this.rateLimitedUntil) return false;
31011
+ if (this.coalescingRemainingMs(nowMs) > 0) return false;
31012
+ return !this.overWakeBudget(nowMs);
31013
+ }
31014
+ markRateLimited(nowMs = this.now()) {
31015
+ this.rateLimitedUntil = nowMs + WAKE_RATE_LIMIT_POLL_MS;
31016
+ this.lastErrorCode = "rate_limited";
31017
+ this.emitPending("state");
31018
+ }
31019
+ setTopic(topic) {
31020
+ if (this.closed) return;
31021
+ if (!isWakeTopic(topic)) {
31022
+ throw new Error("wake topic is malformed");
31023
+ }
31024
+ if (this.topic === topic) return;
31025
+ const rotated = this.topic !== null;
31026
+ void this.detachChannel();
31027
+ this.topic = topic;
31028
+ if (rotated) {
31029
+ this.topicRotatedAt = new Date(this.now()).toISOString();
31030
+ }
31031
+ this.connect();
31032
+ }
31033
+ next(options) {
31034
+ if (this.waiter !== null) {
31035
+ throw new Error("wake next() already has a waiter");
31036
+ }
31037
+ if (options.signal?.aborted) {
31038
+ return Promise.resolve("deadline");
31039
+ }
31040
+ const nowMs = this.now();
31041
+ if (this.pending !== null) {
31042
+ if (!(this.pending === "wake" && this.wakeClaimPaused(nowMs))) {
31043
+ const reason = this.pending;
31044
+ this.pending = null;
31045
+ return Promise.resolve(reason);
31046
+ }
31047
+ }
31048
+ if (nowMs >= options.until) {
31049
+ return Promise.resolve("deadline");
31050
+ }
31051
+ return new Promise((resolve3) => {
31052
+ const delay2 = Math.max(0, options.until - this.now());
31053
+ const timer2 = setTimeout(() => this.finishWait("deadline"), delay2);
31054
+ const onAbort = () => this.finishWait("deadline");
31055
+ options.signal?.addEventListener("abort", onAbort, { once: true });
31056
+ this.waiter = {
31057
+ resolve: resolve3,
31058
+ timer: timer2,
31059
+ onAbort,
31060
+ signal: options.signal
31061
+ };
31062
+ });
31063
+ }
31064
+ async close() {
31065
+ this.closed = true;
31066
+ this.topic = null;
31067
+ this.finishWait("deadline");
31068
+ await this.detachChannel();
31069
+ try {
31070
+ this.realtime?.disconnect?.();
31071
+ } catch {
31072
+ }
31073
+ this.realtime = null;
31074
+ this.connectionState = "disconnected";
31075
+ }
31076
+ trimWakeClaims(nowMs) {
31077
+ const minuteStart = Math.floor(nowMs / 6e4) * 6e4;
31078
+ this.wakeClaimTimes = this.wakeClaimTimes.filter((ts) => ts >= minuteStart);
31079
+ }
31080
+ /** Client budget or a server 429: do not claim on wake; poll covers the window. */
31081
+ wakeClaimPaused(nowMs) {
31082
+ return nowMs < this.rateLimitedUntil || this.overWakeBudget(nowMs);
31083
+ }
31084
+ emitPending(reason) {
31085
+ if (this.waiter !== null) {
31086
+ if (reason === "wake" && this.wakeClaimPaused(this.now())) {
31087
+ this.pending = "wake";
31088
+ return;
31089
+ }
31090
+ this.finishWait(reason);
31091
+ return;
31092
+ }
31093
+ if (reason === "wake" || this.pending !== "wake") {
31094
+ this.pending = reason;
31095
+ }
31096
+ }
31097
+ finishWait(reason) {
31098
+ const waiter = this.waiter;
31099
+ if (waiter === null) return;
31100
+ this.waiter = null;
31101
+ if (waiter.timer !== null) clearTimeout(waiter.timer);
31102
+ if (waiter.signal && waiter.onAbort) {
31103
+ waiter.signal.removeEventListener("abort", waiter.onAbort);
31104
+ }
31105
+ waiter.resolve(reason);
31106
+ }
31107
+ connect() {
31108
+ if (this.closed || this.topic === null) return;
31109
+ if (this.realtime === null) {
31110
+ this.realtime = this.createRealtime(this.target);
31111
+ void this.realtime.setAuth(this.target.anonKey);
31112
+ }
31113
+ const topic = this.topic;
31114
+ this.connectionState = "connecting";
31115
+ this.lastErrorCode = null;
31116
+ const channel = this.realtime.channel(topic, {
31117
+ config: { private: true }
31118
+ });
31119
+ this.channel = channel;
31120
+ channel.on("broadcast", { event: WAKE_EVENT }, () => {
31121
+ this.lastWakeAt = new Date(this.now()).toISOString();
31122
+ this.emitPending("wake");
31123
+ });
31124
+ channel.subscribe((status) => {
31125
+ this.onSubscribeStatus(status);
31126
+ });
31127
+ }
31128
+ onSubscribeStatus(status) {
31129
+ if (this.closed) return;
31130
+ if (!isSubscribeStatus(status)) return;
31131
+ const wasSubscribed = this.connectionState === "subscribed";
31132
+ if (status === REALTIME_SUBSCRIBE_STATUS.SUBSCRIBED) {
31133
+ this.connectionState = "subscribed";
31134
+ this.subscribedAt = new Date(this.now()).toISOString();
31135
+ this.lastErrorCode = null;
31136
+ if (this.everSubscribed && !wasSubscribed) this.reconnects += 1;
31137
+ this.everSubscribed = true;
31138
+ if (!wasSubscribed) this.emitPending("state");
31139
+ return;
31140
+ }
31141
+ const code = wakeErrorCodeFromSubscribeStatus(status);
31142
+ if (status === REALTIME_SUBSCRIBE_STATUS.CLOSED) {
31143
+ this.connectionState = "disconnected";
31144
+ } else {
31145
+ this.connectionState = "errored";
31146
+ }
31147
+ this.lastErrorCode = code;
31148
+ this.subscribedAt = null;
31149
+ if (wasSubscribed) this.emitPending("state");
31150
+ }
31151
+ async detachChannel() {
31152
+ const channel = this.channel;
31153
+ this.channel = null;
31154
+ if (channel === null) return;
31155
+ try {
31156
+ await channel.unsubscribe();
31157
+ } catch {
31158
+ }
31159
+ try {
31160
+ await this.realtime?.removeChannel?.(channel);
31161
+ } catch {
31162
+ }
31163
+ if (this.channel !== null) return;
31164
+ if (this.connectionState === "subscribed") {
31165
+ this.connectionState = "disconnected";
31166
+ this.subscribedAt = null;
31167
+ }
31168
+ }
31169
+ };
31170
+ function createWakeSubscriber(options) {
31171
+ return new WakeSubscriber(options);
31172
+ }
31173
+
30844
31174
  // src/cloud/arrival-watch.ts
30845
31175
  var UUID_RE11 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
30846
31176
  var CURSOR_MAX_BYTES = 4 * 1024;
@@ -31102,10 +31432,15 @@ function assertCursorPage(page) {
31102
31432
  async function runArrivalWatch(options) {
31103
31433
  const pollMs = options.pollMs ?? ARRIVAL_WATCH_POLL_MS2;
31104
31434
  const random = options.random ?? Math.random;
31435
+ const now = options.now ?? Date.now;
31436
+ const reconcileMs = options.reconcileMs ?? LISTENER_RECONCILE_POLL_MS;
31437
+ const wake = options.wake;
31105
31438
  let emptyIdleStreak = 0;
31106
31439
  let cursor = await options.store.read();
31107
31440
  let baseline = cursor === void 0;
31108
31441
  let attempt = 0;
31442
+ let reconcileDueAt = now();
31443
+ let pendingKind = "other";
31109
31444
  const cancelled = () => options.signal?.aborted === true;
31110
31445
  const wait = async (ms) => {
31111
31446
  if (options.sleep) {
@@ -31133,6 +31468,52 @@ async function runArrivalWatch(options) {
31133
31468
  if (!hadDelivery) emptyIdleStreak += 1;
31134
31469
  await wait(intervalMs);
31135
31470
  };
31471
+ const pushMode = () => wake !== void 0 && wake.hasTopic && wake.snapshot(now()).mode === LISTENER_WAKE_MODE_PUSH;
31472
+ const waitCapMs = () => {
31473
+ if (pushMode()) return reconcileMs;
31474
+ return nextIdlePollMs(pollMs, emptyIdleStreak, IDLE_POLL_MAX_MS);
31475
+ };
31476
+ const applyWakeHint = (page) => {
31477
+ const topic = page.wake?.topic;
31478
+ if (topic === void 0 || wake === void 0) return;
31479
+ try {
31480
+ wake.setTopic(topic);
31481
+ } catch {
31482
+ }
31483
+ };
31484
+ const waitForTrigger = async (hadDelivery) => {
31485
+ if (hadDelivery) emptyIdleStreak = 0;
31486
+ if (wake === void 0 || !wake.hasTopic) {
31487
+ pendingKind = "other";
31488
+ await idleWait(hadDelivery);
31489
+ return;
31490
+ }
31491
+ const cap = waitCapMs();
31492
+ const until = Math.min(reconcileDueAt, now() + cap);
31493
+ const reason = await wake.next({
31494
+ until,
31495
+ ...options.signal ? { signal: options.signal } : {}
31496
+ });
31497
+ if (cancelled()) return;
31498
+ if (reason === "wake" && pushMode()) {
31499
+ const coalesceMs = wake.coalescingRemainingMs(now());
31500
+ if (coalesceMs > 0) await wait(coalesceMs);
31501
+ pendingKind = pushMode() ? "wake" : "other";
31502
+ return;
31503
+ }
31504
+ pendingKind = "other";
31505
+ if (!hadDelivery && reason === "deadline" && !pushMode()) {
31506
+ emptyIdleStreak += 1;
31507
+ }
31508
+ };
31509
+ const noteCycle = () => {
31510
+ if (pendingKind === "wake") {
31511
+ wake?.noteClaim(now());
31512
+ return;
31513
+ }
31514
+ reconcileDueAt = now() + reconcileMs;
31515
+ wake?.noteReconcile(now());
31516
+ };
31136
31517
  while (!cancelled()) {
31137
31518
  try {
31138
31519
  const page = await options.readPage({
@@ -31141,6 +31522,7 @@ async function runArrivalWatch(options) {
31141
31522
  limit: baseline ? 1 : SIGNAL_FOLLOW_PAGE_LIMIT
31142
31523
  });
31143
31524
  assertCursorPage(page);
31525
+ applyWakeHint(page);
31144
31526
  if (page.signals.some(
31145
31527
  (row) => row.workspace_id !== options.workspaceId || !(signalAddressesAgent(row, options.principalId) || row.to === null && row.to_agent === null)
31146
31528
  )) {
@@ -31158,7 +31540,8 @@ async function runArrivalWatch(options) {
31158
31540
  await options.store.write(cursor);
31159
31541
  baseline = false;
31160
31542
  if (cancelled()) break;
31161
- await idleWait(false);
31543
+ noteCycle();
31544
+ await waitForTrigger(false);
31162
31545
  continue;
31163
31546
  }
31164
31547
  const emittedSignals = [];
@@ -31172,10 +31555,14 @@ async function runArrivalWatch(options) {
31172
31555
  if (emittedSignals.length > 0) {
31173
31556
  await options.afterEmitBatch?.(emittedSignals);
31174
31557
  }
31175
- if (cancelled()) break;
31176
- const fullPage = page.rawCount >= SIGNAL_FOLLOW_PAGE_LIMIT;
31177
- if (fullPage) await wait(0);
31178
- else await idleWait(emittedSignals.length > 0);
31558
+ if (cancelled()) break;
31559
+ const fullPage = page.rawCount >= SIGNAL_FOLLOW_PAGE_LIMIT;
31560
+ if (fullPage) {
31561
+ await wait(0);
31562
+ continue;
31563
+ }
31564
+ noteCycle();
31565
+ await waitForTrigger(emittedSignals.length > 0);
31179
31566
  } catch (error) {
31180
31567
  if (cancelled()) break;
31181
31568
  const http = followHttpDetails(error);
@@ -37357,453 +37744,113 @@ function parseEntry(value, rejectUnknownKeys) {
37357
37744
  }
37358
37745
  function parseFile(raw, rejectUnknownKeys = false) {
37359
37746
  let value;
37360
- try {
37361
- value = JSON.parse(raw);
37362
- } catch {
37363
- throw new Error("stored pending-for-main queue is malformed");
37364
- }
37365
- if (!value || typeof value !== "object" || Array.isArray(value)) {
37366
- throw new Error("stored pending-for-main queue is malformed");
37367
- }
37368
- const row = value;
37369
- if (rejectUnknownKeys && Object.keys(row).some((key2) => !QUEUE_KEYS.has(key2)) || row.version !== 1 || !Array.isArray(row.entries) || row.entries.length > LISTENER_MAIN_QUEUE_MAX || !(row.droppedCount === void 0 || typeof row.droppedCount === "number" && Number.isSafeInteger(row.droppedCount) && row.droppedCount >= 0)) {
37370
- throw new Error("stored pending-for-main queue is malformed");
37371
- }
37372
- const entries = row.entries.map((entry) => parseEntry(entry, rejectUnknownKeys));
37373
- if (new Set(entries.map((entry) => entry.signalId)).size !== entries.length) {
37374
- throw new Error("stored pending-for-main queue repeats a signal");
37375
- }
37376
- return { version: 1, entries, droppedCount: row.droppedCount ?? 0 };
37377
- }
37378
- var FilePendingMainQueue = class {
37379
- path;
37380
- directory;
37381
- constructor(instanceDirectory) {
37382
- if (!(0, import_node_path15.isAbsolute)(instanceDirectory)) {
37383
- throw new Error("pending-for-main directory must be absolute");
37384
- }
37385
- this.directory = instanceDirectory;
37386
- this.path = (0, import_node_path15.join)(instanceDirectory, QUEUE_FILE);
37387
- }
37388
- async readUnlocked() {
37389
- const raw = await readSecureJsonFile(this.path, MAX_QUEUE_BYTES);
37390
- return raw === null ? { version: 1, entries: [], droppedCount: 0 } : parseFile(raw);
37391
- }
37392
- async writeUnlocked(file) {
37393
- const canonical = parseFile(JSON.stringify(file), true);
37394
- await writeSecureJsonFile(this.path, JSON.stringify(canonical));
37395
- }
37396
- async read() {
37397
- return [...(await this.readUnlocked()).entries];
37398
- }
37399
- async count() {
37400
- return (await this.readUnlocked()).entries.length;
37401
- }
37402
- async stats() {
37403
- const file = await this.readUnlocked();
37404
- return { count: file.entries.length, droppedCount: file.droppedCount };
37405
- }
37406
- async enqueue(entry) {
37407
- const checked = parseEntry(entry, true);
37408
- return await withFileLock(this.directory, QUEUE_LOCK, async () => {
37409
- const file = await this.readUnlocked();
37410
- if (file.entries.some((item) => item.signalId === checked.signalId)) {
37411
- return {
37412
- count: file.entries.length,
37413
- added: false,
37414
- droppedOldest: false,
37415
- droppedCount: file.droppedCount
37416
- };
37417
- }
37418
- file.entries.push(checked);
37419
- const droppedOldest = file.entries.length > LISTENER_MAIN_QUEUE_MAX;
37420
- if (droppedOldest) {
37421
- file.entries.shift();
37422
- file.droppedCount += 1;
37423
- }
37424
- await this.writeUnlocked(file);
37425
- return {
37426
- count: file.entries.length,
37427
- added: true,
37428
- droppedOldest,
37429
- droppedCount: file.droppedCount
37430
- };
37431
- });
37432
- }
37433
- async remove(signalIds, lockTimeoutMs) {
37434
- if (signalIds.size === 0) return await this.count();
37435
- return await withFileLock(this.directory, QUEUE_LOCK, async () => {
37436
- const file = await this.readUnlocked();
37437
- const entries = file.entries.filter((entry) => !signalIds.has(entry.signalId));
37438
- if (entries.length !== file.entries.length) {
37439
- await this.writeUnlocked({
37440
- version: 1,
37441
- entries,
37442
- droppedCount: file.droppedCount
37443
- });
37444
- }
37445
- return entries.length;
37446
- }, lockTimeoutMs === void 0 ? {} : { timeoutMs: lockTimeoutMs });
37447
- }
37448
- };
37449
- function pendingMainEntry(signal, principalId, provenance, now, options = {}) {
37450
- if (signal.kind !== "ask" && signal.kind !== "note") {
37451
- throw new Error("only directed asks and notes can enter the pending-for-main queue");
37452
- }
37453
- return parseEntry({
37454
- signalId: signal.id,
37455
- workspaceId: signal.workspace_id,
37456
- principalId,
37457
- fromId: signal.from,
37458
- fromKind: signal.from_kind,
37459
- kind: signal.kind,
37460
- senderName: provenance.senderName,
37461
- body: signal.body,
37462
- ...(signal.attachments?.length ?? 0) > 0 ? { attachmentCount: signal.attachments.length } : {},
37463
- createdAt: signal.created_at,
37464
- queuedAt: new Date(now).toISOString(),
37465
- ...options.observationPending ? { observationPending: true } : {}
37466
- }, true);
37467
- }
37468
-
37469
- // src/listener/wake.ts
37470
- var LISTENER_RECONCILE_POLL_MS = 3e5;
37471
- var WAKE_COALESCE_MS = 1e3;
37472
- var WAKE_CLAIMS_PER_MINUTE_BUDGET = 50;
37473
- var WAKE_RATE_LIMIT_POLL_MS = 6e4;
37474
- var REALTIME_SUBSCRIBE_STATUS = {
37475
- SUBSCRIBED: "SUBSCRIBED",
37476
- CHANNEL_ERROR: "CHANNEL_ERROR",
37477
- CLOSED: "CLOSED",
37478
- TIMED_OUT: "TIMED_OUT"
37479
- };
37480
- var LISTENER_WAKE_MODES = ["push", "poll"];
37481
- var LISTENER_WAKE_MODE_PUSH = LISTENER_WAKE_MODES[0];
37482
- var LISTENER_WAKE_MODE_POLL = LISTENER_WAKE_MODES[1];
37483
- var LISTENER_WAKE_MODE_SET = new Set(
37484
- LISTENER_WAKE_MODES
37485
- );
37486
- var WAKE_ERROR_CODES = [
37487
- "channel_error",
37488
- "closed",
37489
- "timed_out",
37490
- "rate_limited",
37491
- "wake_budget"
37492
- ];
37493
- var WAKE_ERROR_CODE_SET = new Set(WAKE_ERROR_CODES);
37494
- var WAKE_ERROR_CODE_WAKE_BUDGET = WAKE_ERROR_CODES.find(
37495
- (code) => code === "wake_budget"
37496
- );
37497
- var LISTENER_WAKE_STATUS_KEYS = [
37498
- "mode",
37499
- "subscribedAt",
37500
- "reconnects",
37501
- "lastWakeAt",
37502
- "lastReconcileAt",
37503
- "errorCode",
37504
- "topicRotatedAt",
37505
- "rateLimited"
37506
- ];
37507
- var LISTENER_WAKE_SENSITIVE_KEYS = [
37508
- "topic",
37509
- "wakeTopic",
37510
- "wake_topic"
37511
- ];
37512
- function defaultRealtime(target2) {
37513
- const client = createClient(target2.url, target2.anonKey, {
37514
- auth: { persistSession: false, autoRefreshToken: false }
37515
- });
37516
- return client.realtime;
37517
- }
37518
- function isSubscribeStatus(value) {
37519
- return value === REALTIME_SUBSCRIBE_STATUS.SUBSCRIBED || value === REALTIME_SUBSCRIBE_STATUS.CHANNEL_ERROR || value === REALTIME_SUBSCRIBE_STATUS.CLOSED || value === REALTIME_SUBSCRIBE_STATUS.TIMED_OUT;
37520
- }
37521
- function wakeErrorCodeFromSubscribeStatus(status) {
37522
- switch (status) {
37523
- case REALTIME_SUBSCRIBE_STATUS.CHANNEL_ERROR:
37524
- return "channel_error";
37525
- case REALTIME_SUBSCRIBE_STATUS.CLOSED:
37526
- return "closed";
37527
- case REALTIME_SUBSCRIBE_STATUS.TIMED_OUT:
37528
- return "timed_out";
37529
- default:
37530
- return null;
37531
- }
37532
- }
37533
- function emptyListenerWakeStatus() {
37534
- return {
37535
- mode: LISTENER_WAKE_MODE_POLL,
37536
- subscribedAt: null,
37537
- reconnects: 0,
37538
- lastWakeAt: null,
37539
- lastReconcileAt: null,
37540
- errorCode: null,
37541
- topicRotatedAt: null,
37542
- rateLimited: false
37543
- };
37544
- }
37545
- function listenerWakePersistWorthy(previous, next, lastPersistMs, nowMs) {
37546
- if (previous === void 0) return true;
37547
- if (previous.mode !== next.mode || previous.rateLimited !== next.rateLimited || previous.errorCode !== next.errorCode || previous.reconnects !== next.reconnects || previous.subscribedAt !== next.subscribedAt || previous.topicRotatedAt !== next.topicRotatedAt || previous.lastReconcileAt !== next.lastReconcileAt) {
37548
- return true;
37549
- }
37550
- if (previous.lastWakeAt !== next.lastWakeAt) {
37551
- return nowMs - lastPersistMs >= WAKE_COALESCE_MS;
37552
- }
37553
- return false;
37554
- }
37555
- function listenerWakeStatusSentence(wake, pollIntervalMs, lastWakeLabel) {
37556
- if (wake.mode === LISTENER_WAKE_MODE_PUSH) {
37557
- const last = lastWakeLabel === null ? "no wake yet" : `last wake ${lastWakeLabel}`;
37558
- return `${LISTENER_WAKE_MODE_PUSH} (Realtime), ${last}, reconcile every ${formatIdlePollDuration(LISTENER_RECONCILE_POLL_MS)}.`;
37559
- }
37560
- if (wake.errorCode === WAKE_ERROR_CODE_WAKE_BUDGET) {
37561
- return `Subscribed; claims paused until the minute clears (${WAKE_ERROR_CODE_WAKE_BUDGET}); polling every ${formatIdlePollDuration(pollIntervalMs)} meanwhile.`;
37562
- }
37563
- const code = wake.errorCode ?? "disconnected";
37564
- return `${LISTENER_WAKE_MODE_POLL} every ${formatIdlePollDuration(pollIntervalMs)}. Realtime not connected (${code}).`;
37565
- }
37566
- var WakeSubscriber = class {
37567
- now;
37568
- target;
37569
- createRealtime;
37570
- realtime = null;
37571
- channel = null;
37572
- topic = null;
37573
- waiter = null;
37574
- pending = null;
37575
- connectionState = "disconnected";
37576
- subscribedAt = null;
37577
- reconnects = 0;
37578
- lastWakeAt = null;
37579
- lastReconcileAt = null;
37580
- lastErrorCode = null;
37581
- topicRotatedAt = null;
37582
- rateLimitedUntil = 0;
37583
- lastClaimAt = 0;
37584
- wakeClaimTimes = [];
37585
- closed = false;
37586
- everSubscribed = false;
37587
- constructor(options) {
37588
- this.target = options.target;
37589
- this.now = options.now ?? Date.now;
37590
- this.createRealtime = options.createRealtime ?? defaultRealtime;
37591
- }
37592
- get state() {
37593
- return this.connectionState;
37594
- }
37595
- get hasTopic() {
37596
- return this.topic !== null;
37597
- }
37598
- snapshot(nowMs = this.now()) {
37599
- const serverLimited = nowMs < this.rateLimitedUntil;
37600
- const overBudget = this.overWakeBudget(nowMs);
37601
- const rateLimited = serverLimited || overBudget;
37602
- const mode3 = this.connectionState === "subscribed" && !rateLimited ? LISTENER_WAKE_MODE_PUSH : LISTENER_WAKE_MODE_POLL;
37603
- const errorCode = serverLimited ? "rate_limited" : overBudget ? WAKE_ERROR_CODE_WAKE_BUDGET : this.lastErrorCode;
37604
- return {
37605
- mode: mode3,
37606
- subscribedAt: this.subscribedAt,
37607
- reconnects: this.reconnects,
37608
- lastWakeAt: this.lastWakeAt,
37609
- lastReconcileAt: this.lastReconcileAt,
37610
- errorCode,
37611
- topicRotatedAt: this.topicRotatedAt,
37612
- rateLimited
37613
- };
37747
+ try {
37748
+ value = JSON.parse(raw);
37749
+ } catch {
37750
+ throw new Error("stored pending-for-main queue is malformed");
37614
37751
  }
37615
- noteReconcile(nowMs = this.now()) {
37616
- this.lastReconcileAt = new Date(nowMs).toISOString();
37752
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
37753
+ throw new Error("stored pending-for-main queue is malformed");
37617
37754
  }
37618
- noteClaim(nowMs = this.now()) {
37619
- this.lastClaimAt = nowMs;
37755
+ const row = value;
37756
+ if (rejectUnknownKeys && Object.keys(row).some((key2) => !QUEUE_KEYS.has(key2)) || row.version !== 1 || !Array.isArray(row.entries) || row.entries.length > LISTENER_MAIN_QUEUE_MAX || !(row.droppedCount === void 0 || typeof row.droppedCount === "number" && Number.isSafeInteger(row.droppedCount) && row.droppedCount >= 0)) {
37757
+ throw new Error("stored pending-for-main queue is malformed");
37620
37758
  }
37621
- noteWakeClaim(nowMs = this.now()) {
37622
- this.noteClaim(nowMs);
37623
- this.wakeClaimTimes.push(nowMs);
37624
- this.trimWakeClaims(nowMs);
37759
+ const entries = row.entries.map((entry) => parseEntry(entry, rejectUnknownKeys));
37760
+ if (new Set(entries.map((entry) => entry.signalId)).size !== entries.length) {
37761
+ throw new Error("stored pending-for-main queue repeats a signal");
37625
37762
  }
37626
- coalescingRemainingMs(nowMs = this.now()) {
37627
- if (this.lastClaimAt <= 0) return 0;
37628
- return Math.max(0, this.lastClaimAt + WAKE_COALESCE_MS - nowMs);
37763
+ return { version: 1, entries, droppedCount: row.droppedCount ?? 0 };
37764
+ }
37765
+ var FilePendingMainQueue = class {
37766
+ path;
37767
+ directory;
37768
+ constructor(instanceDirectory) {
37769
+ if (!(0, import_node_path15.isAbsolute)(instanceDirectory)) {
37770
+ throw new Error("pending-for-main directory must be absolute");
37771
+ }
37772
+ this.directory = instanceDirectory;
37773
+ this.path = (0, import_node_path15.join)(instanceDirectory, QUEUE_FILE);
37629
37774
  }
37630
- overWakeBudget(nowMs = this.now()) {
37631
- this.trimWakeClaims(nowMs);
37632
- return this.wakeClaimTimes.length >= WAKE_CLAIMS_PER_MINUTE_BUDGET;
37775
+ async readUnlocked() {
37776
+ const raw = await readSecureJsonFile(this.path, MAX_QUEUE_BYTES);
37777
+ return raw === null ? { version: 1, entries: [], droppedCount: 0 } : parseFile(raw);
37633
37778
  }
37634
- canClaimOnWake(nowMs = this.now()) {
37635
- if (nowMs < this.rateLimitedUntil) return false;
37636
- if (this.coalescingRemainingMs(nowMs) > 0) return false;
37637
- return !this.overWakeBudget(nowMs);
37779
+ async writeUnlocked(file) {
37780
+ const canonical = parseFile(JSON.stringify(file), true);
37781
+ await writeSecureJsonFile(this.path, JSON.stringify(canonical));
37638
37782
  }
37639
- markRateLimited(nowMs = this.now()) {
37640
- this.rateLimitedUntil = nowMs + WAKE_RATE_LIMIT_POLL_MS;
37641
- this.lastErrorCode = "rate_limited";
37642
- this.emitPending("state");
37783
+ async read() {
37784
+ return [...(await this.readUnlocked()).entries];
37643
37785
  }
37644
- setTopic(topic) {
37645
- if (this.closed) return;
37646
- if (!isWakeTopic(topic)) {
37647
- throw new Error("wake topic is malformed");
37648
- }
37649
- if (this.topic === topic) return;
37650
- const rotated = this.topic !== null;
37651
- void this.detachChannel();
37652
- this.topic = topic;
37653
- if (rotated) {
37654
- this.topicRotatedAt = new Date(this.now()).toISOString();
37655
- }
37656
- this.connect();
37786
+ async count() {
37787
+ return (await this.readUnlocked()).entries.length;
37657
37788
  }
37658
- next(options) {
37659
- if (this.waiter !== null) {
37660
- throw new Error("wake next() already has a waiter");
37661
- }
37662
- if (options.signal?.aborted) {
37663
- return Promise.resolve("deadline");
37664
- }
37665
- const nowMs = this.now();
37666
- if (this.pending !== null) {
37667
- if (!(this.pending === "wake" && this.wakeClaimPaused(nowMs))) {
37668
- const reason = this.pending;
37669
- this.pending = null;
37670
- return Promise.resolve(reason);
37789
+ async stats() {
37790
+ const file = await this.readUnlocked();
37791
+ return { count: file.entries.length, droppedCount: file.droppedCount };
37792
+ }
37793
+ async enqueue(entry) {
37794
+ const checked = parseEntry(entry, true);
37795
+ return await withFileLock(this.directory, QUEUE_LOCK, async () => {
37796
+ const file = await this.readUnlocked();
37797
+ if (file.entries.some((item) => item.signalId === checked.signalId)) {
37798
+ return {
37799
+ count: file.entries.length,
37800
+ added: false,
37801
+ droppedOldest: false,
37802
+ droppedCount: file.droppedCount
37803
+ };
37671
37804
  }
37672
- }
37673
- if (nowMs >= options.until) {
37674
- return Promise.resolve("deadline");
37675
- }
37676
- return new Promise((resolve3) => {
37677
- const finish = (reason) => {
37678
- if (this.waiter === null) return;
37679
- const current = this.waiter;
37680
- this.waiter = null;
37681
- if (current.timer !== null) clearTimeout(current.timer);
37682
- if (current.signal && current.onAbort) {
37683
- current.signal.removeEventListener("abort", current.onAbort);
37684
- }
37685
- resolve3(reason);
37686
- };
37687
- const delay2 = Math.max(0, options.until - this.now());
37688
- const timer2 = setTimeout(() => finish("deadline"), delay2);
37689
- const onAbort = () => finish("deadline");
37690
- options.signal?.addEventListener("abort", onAbort, { once: true });
37691
- this.waiter = {
37692
- resolve: finish,
37693
- timer: timer2,
37694
- onAbort,
37695
- signal: options.signal
37805
+ file.entries.push(checked);
37806
+ const droppedOldest = file.entries.length > LISTENER_MAIN_QUEUE_MAX;
37807
+ if (droppedOldest) {
37808
+ file.entries.shift();
37809
+ file.droppedCount += 1;
37810
+ }
37811
+ await this.writeUnlocked(file);
37812
+ return {
37813
+ count: file.entries.length,
37814
+ added: true,
37815
+ droppedOldest,
37816
+ droppedCount: file.droppedCount
37696
37817
  };
37697
37818
  });
37698
37819
  }
37699
- async close() {
37700
- this.closed = true;
37701
- this.topic = null;
37702
- this.finishWait("deadline");
37703
- await this.detachChannel();
37704
- try {
37705
- this.realtime?.disconnect?.();
37706
- } catch {
37707
- }
37708
- this.realtime = null;
37709
- this.connectionState = "disconnected";
37710
- }
37711
- trimWakeClaims(nowMs) {
37712
- const minuteStart = Math.floor(nowMs / 6e4) * 6e4;
37713
- this.wakeClaimTimes = this.wakeClaimTimes.filter((ts) => ts >= minuteStart);
37714
- }
37715
- /** Client budget or a server 429: do not claim on wake; poll covers the window. */
37716
- wakeClaimPaused(nowMs) {
37717
- return nowMs < this.rateLimitedUntil || this.overWakeBudget(nowMs);
37718
- }
37719
- emitPending(reason) {
37720
- if (this.waiter !== null) {
37721
- if (reason === "wake" && this.wakeClaimPaused(this.now())) {
37722
- this.pending = "wake";
37723
- return;
37820
+ async remove(signalIds, lockTimeoutMs) {
37821
+ if (signalIds.size === 0) return await this.count();
37822
+ return await withFileLock(this.directory, QUEUE_LOCK, async () => {
37823
+ const file = await this.readUnlocked();
37824
+ const entries = file.entries.filter((entry) => !signalIds.has(entry.signalId));
37825
+ if (entries.length !== file.entries.length) {
37826
+ await this.writeUnlocked({
37827
+ version: 1,
37828
+ entries,
37829
+ droppedCount: file.droppedCount
37830
+ });
37724
37831
  }
37725
- this.finishWait(reason);
37726
- return;
37727
- }
37728
- if (reason === "wake" || this.pending !== "wake") {
37729
- this.pending = reason;
37730
- }
37731
- }
37732
- finishWait(reason) {
37733
- const waiter = this.waiter;
37734
- if (waiter === null) return;
37735
- this.waiter = null;
37736
- if (waiter.timer !== null) clearTimeout(waiter.timer);
37737
- if (waiter.signal && waiter.onAbort) {
37738
- waiter.signal.removeEventListener("abort", waiter.onAbort);
37739
- }
37740
- waiter.resolve(reason);
37741
- }
37742
- connect() {
37743
- if (this.closed || this.topic === null) return;
37744
- if (this.realtime === null) {
37745
- this.realtime = this.createRealtime(this.target);
37746
- void this.realtime.setAuth(this.target.anonKey);
37747
- }
37748
- const topic = this.topic;
37749
- this.connectionState = "connecting";
37750
- this.lastErrorCode = null;
37751
- const channel = this.realtime.channel(topic, {
37752
- config: { private: true }
37753
- });
37754
- this.channel = channel;
37755
- channel.on("broadcast", { event: WAKE_EVENT }, () => {
37756
- this.lastWakeAt = new Date(this.now()).toISOString();
37757
- this.emitPending("wake");
37758
- });
37759
- channel.subscribe((status) => {
37760
- this.onSubscribeStatus(status);
37761
- });
37762
- }
37763
- onSubscribeStatus(status) {
37764
- if (this.closed) return;
37765
- if (!isSubscribeStatus(status)) return;
37766
- const wasSubscribed = this.connectionState === "subscribed";
37767
- if (status === REALTIME_SUBSCRIBE_STATUS.SUBSCRIBED) {
37768
- this.connectionState = "subscribed";
37769
- this.subscribedAt = new Date(this.now()).toISOString();
37770
- this.lastErrorCode = null;
37771
- if (this.everSubscribed && !wasSubscribed) this.reconnects += 1;
37772
- this.everSubscribed = true;
37773
- if (!wasSubscribed) this.emitPending("state");
37774
- return;
37775
- }
37776
- const code = wakeErrorCodeFromSubscribeStatus(status);
37777
- if (status === REALTIME_SUBSCRIBE_STATUS.CLOSED) {
37778
- this.connectionState = "disconnected";
37779
- } else {
37780
- this.connectionState = "errored";
37781
- }
37782
- this.lastErrorCode = code;
37783
- this.subscribedAt = null;
37784
- if (wasSubscribed) this.emitPending("state");
37785
- }
37786
- async detachChannel() {
37787
- const channel = this.channel;
37788
- this.channel = null;
37789
- if (channel === null) return;
37790
- try {
37791
- await channel.unsubscribe();
37792
- } catch {
37793
- }
37794
- try {
37795
- await this.realtime?.removeChannel?.(channel);
37796
- } catch {
37797
- }
37798
- if (this.channel !== null) return;
37799
- if (this.connectionState === "subscribed") {
37800
- this.connectionState = "disconnected";
37801
- this.subscribedAt = null;
37802
- }
37832
+ return entries.length;
37833
+ }, lockTimeoutMs === void 0 ? {} : { timeoutMs: lockTimeoutMs });
37803
37834
  }
37804
37835
  };
37805
- function createWakeSubscriber(options) {
37806
- return new WakeSubscriber(options);
37836
+ function pendingMainEntry(signal, principalId, provenance, now, options = {}) {
37837
+ if (signal.kind !== "ask" && signal.kind !== "note") {
37838
+ throw new Error("only directed asks and notes can enter the pending-for-main queue");
37839
+ }
37840
+ return parseEntry({
37841
+ signalId: signal.id,
37842
+ workspaceId: signal.workspace_id,
37843
+ principalId,
37844
+ fromId: signal.from,
37845
+ fromKind: signal.from_kind,
37846
+ kind: signal.kind,
37847
+ senderName: provenance.senderName,
37848
+ body: signal.body,
37849
+ ...(signal.attachments?.length ?? 0) > 0 ? { attachmentCount: signal.attachments.length } : {},
37850
+ createdAt: signal.created_at,
37851
+ queuedAt: new Date(now).toISOString(),
37852
+ ...options.observationPending ? { observationPending: true } : {}
37853
+ }, true);
37807
37854
  }
37808
37855
 
37809
37856
  // src/listener/runtime.ts
@@ -43662,8 +43709,8 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
43662
43709
  ]);
43663
43710
  var UUID_RE23 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
43664
43711
  function packageVersion() {
43665
- if ("0.1.58".length > 0) {
43666
- return "0.1.58";
43712
+ if ("0.1.60".length > 0) {
43713
+ return "0.1.60";
43667
43714
  }
43668
43715
  try {
43669
43716
  const value = JSON.parse(
@@ -46427,6 +46474,7 @@ async function runInboxNotifyCommand(args) {
46427
46474
  principalId
46428
46475
  );
46429
46476
  await acquireArrivalWatchLock(lockPath);
46477
+ const wake = createWakeSubscriber({ target: cloud });
46430
46478
  try {
46431
46479
  const retryNotices = createArrivalRetryNoticePolicy();
46432
46480
  let renderedBearer = selected.bearer;
@@ -46440,6 +46488,7 @@ async function runInboxNotifyCommand(args) {
46440
46488
  principalId,
46441
46489
  store: cursorStore,
46442
46490
  signal: controller.signal,
46491
+ wake,
46443
46492
  readPage: async ({ after, baseline, limit }) => {
46444
46493
  const token = selected.session ? await selected.session.bearer() : selected.bearer;
46445
46494
  renderedBearer = token;
@@ -46500,6 +46549,7 @@ async function runInboxNotifyCommand(args) {
46500
46549
  process.off("SIGINT", stop);
46501
46550
  process.off("SIGTERM", stop);
46502
46551
  httpClient.close();
46552
+ await wake.close();
46503
46553
  await releaseArrivalWatchLock(lockPath);
46504
46554
  }
46505
46555
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "commonswarm",
3
- "version": "0.1.58",
3
+ "version": "0.1.60",
4
4
  "description": "CommonSwarm CLI — coordination for teams where people and AI agents work side by side.",
5
5
  "bin": {
6
6
  "cswarm": "cswarm.cjs"