pushary 1.9.126 → 1.9.127

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/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.127
4
+
5
+ - The background daemon reads the record of a settled phone message to Codex or Hermes once, instead of every few seconds, and deletes its own records once they are more than 7 days old. Before, every record was reread on each scan and kept forever, so the work grew with every message.
6
+
3
7
  ## 1.9.126
4
8
 
5
9
  - With two or more sessions taking turns, a tool call no longer fetches your policy again just because another session fetched it last, and each session can ride out an outage on its own copy. Before, nearly every call refetched, and an outage often left a session with no copy. When a new session starts, copies older than 12 hours are deleted.
@@ -179,7 +179,8 @@ import {
179
179
  openSync,
180
180
  writeSync,
181
181
  fsyncSync,
182
- closeSync
182
+ closeSync,
183
+ unlinkSync
183
184
  } from "fs";
184
185
  import { dirname, join, resolve } from "path";
185
186
  var privateNode = (path, kind) => {
@@ -311,13 +312,21 @@ var controlRequest = (path, request, timeout) => new Promise((resolve3, reject)
311
312
  s.destroy();
312
313
  });
313
314
  });
315
+ var JOURNAL_FILE = /^[0-9a-f]{64}\.json$/;
316
+ var outlivedTheServersCommand = (age, nowMs, monotonicNowMs) => nowMs - age.submittedAtMs >= COMMAND_RECOVERY.receiptRetentionMs && monotonicNowMs - age.firstSeenMonotonicMs >= COMMAND_RECOVERY.retrySendWindowMs;
317
+ var isMissingFile = (error) => error instanceof Error && "code" in error && error.code === "ENOENT";
314
318
  var NativeHostClient = class {
315
319
  constructor(options) {
316
320
  this.options = options;
317
321
  this.timeout = options.timeoutMs ?? NATIVE_INPUT.rpcTimeoutMs;
322
+ this.monotonicNow = options.monotonicNow ?? (() => performance.now());
318
323
  }
319
324
  timeout;
320
325
  lastDiscoveryFailure = null;
326
+ journalsOfOthers = /* @__PURE__ */ new Set();
327
+ settledJournals = /* @__PURE__ */ new Map();
328
+ firstSeenMonotonicMs = /* @__PURE__ */ new Map();
329
+ monotonicNow;
321
330
  async connect() {
322
331
  if ((this.options.platform ?? process.platform) !== "darwin")
323
332
  throw new Error("native host platform unqualified");
@@ -557,21 +566,74 @@ var NativeHostClient = class {
557
566
  ...hermesCursor ? { hermesCursor } : {}
558
567
  };
559
568
  } catch (e) {
560
- if (e instanceof Error && "code" in e && e.code === "ENOENT") return null;
569
+ if (isMissingFile(e)) return null;
561
570
  throw e;
562
571
  }
563
572
  }
573
+ ownsJournal(raw) {
574
+ return raw.agent === this.options.agent && raw.settlementScope === this.options.settlementScope;
575
+ }
576
+ mayRemoveOwnJournals() {
577
+ return this.options.settlementScope !== void 0;
578
+ }
579
+ removeJournal(file) {
580
+ this.settledJournals.delete(file);
581
+ this.firstSeenMonotonicMs.delete(file);
582
+ try {
583
+ unlinkSync(join(this.options.journalDir, file));
584
+ } catch (error) {
585
+ if (!isMissingFile(error)) this.options.report?.(`could not remove a native-input journal: ${error instanceof Error ? error.message : String(error)}`);
586
+ }
587
+ }
588
+ observeJournalAge(file, submittedAtMs, monotonicNowMs) {
589
+ const firstSeen = this.firstSeenMonotonicMs.get(file) ?? monotonicNowMs;
590
+ this.firstSeenMonotonicMs.set(file, firstSeen);
591
+ return { submittedAtMs, firstSeenMonotonicMs: firstSeen };
592
+ }
593
+ removeSettledJournalsPastRetention(nowMs, monotonicNowMs) {
594
+ if (!this.mayRemoveOwnJournals()) return;
595
+ for (const [file, age] of [...this.settledJournals]) {
596
+ if (outlivedTheServersCommand(age, nowMs, monotonicNowMs)) this.removeJournal(file);
597
+ }
598
+ }
599
+ forgetJournalsNoLongerListed(listed) {
600
+ for (const file of [...this.journalsOfOthers]) if (!listed.has(file)) this.journalsOfOthers.delete(file);
601
+ for (const file of [...this.settledJournals.keys()]) if (!listed.has(file)) this.settledJournals.delete(file);
602
+ for (const file of [...this.firstSeenMonotonicMs.keys()]) if (!listed.has(file)) this.firstSeenMonotonicMs.delete(file);
603
+ }
604
+ needsReading(file) {
605
+ return JOURNAL_FILE.test(file) && !this.journalsOfOthers.has(file) && !this.settledJournals.has(file);
606
+ }
564
607
  pendingSettlements() {
608
+ const nowMs = Date.now();
609
+ const monotonicNowMs = this.monotonicNow();
610
+ this.removeSettledJournalsPastRetention(nowMs, monotonicNowMs);
565
611
  try {
566
- return readdirSync(this.options.journalDir).filter((file) => /^[0-9a-f]{64}\.json$/.test(file)).flatMap((file) => {
612
+ const listed = readdirSync(this.options.journalDir);
613
+ this.forgetJournalsNoLongerListed(new Set(listed));
614
+ return listed.filter((file) => this.needsReading(file)).flatMap((file) => {
567
615
  try {
568
616
  const raw = JSON.parse(
569
617
  readFileSync(join(this.options.journalDir, file), "utf8")
570
618
  );
571
619
  if (!nativeRecord(raw) || !nativeIdentifier(raw.sessionId) || !nativeIdentifier(raw.commandId))
572
620
  return [];
621
+ if (!this.ownsJournal(raw)) {
622
+ this.journalsOfOthers.add(file);
623
+ return [];
624
+ }
573
625
  const intent = this.readIntent(raw.sessionId, raw.commandId);
574
- return intent?.accepted && !intent.settled && intent.settlementScope === this.options.settlementScope ? [{ sessionId: intent.sessionId, commandId: intent.commandId }] : [];
626
+ if (!intent) return [];
627
+ const age = this.observeJournalAge(file, intent.submittedAtMs, monotonicNowMs);
628
+ if (this.mayRemoveOwnJournals() && outlivedTheServersCommand(age, nowMs, monotonicNowMs)) {
629
+ this.removeJournal(file);
630
+ return [];
631
+ }
632
+ if (intent.settled) {
633
+ this.settledJournals.set(file, age);
634
+ return [];
635
+ }
636
+ return intent.accepted ? [{ sessionId: intent.sessionId, commandId: intent.commandId }] : [];
575
637
  } catch {
576
638
  return [];
577
639
  }
@@ -971,7 +1033,7 @@ import {
971
1033
  readSync,
972
1034
  statSync,
973
1035
  writeFileSync,
974
- unlinkSync
1036
+ unlinkSync as unlinkSync2
975
1037
  } from "fs";
976
1038
  import { dirname as dirname2, isAbsolute, join as join3, resolve as resolve2 } from "path";
977
1039
  import { Socket } from "net";
@@ -1185,7 +1247,7 @@ var ClaudeInboxClient = class {
1185
1247
  return { file, attempt };
1186
1248
  });
1187
1249
  for (const name of files.slice(CLAUDE_WAKE.retainedAttempts))
1188
- unlinkSync(join3(directory, name));
1250
+ unlinkSync2(join3(directory, name));
1189
1251
  return attempts;
1190
1252
  }
1191
1253
  async wake(sessionId, signal) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pushary",
3
- "version": "1.9.126",
3
+ "version": "1.9.127",
4
4
  "description": "Human-in-the-loop (HITL) for AI agents: ask a person for approvals or answers by push notification. One command connects Claude Code, Codex, Cursor, Gemini CLI and more.",
5
5
  "keywords": [
6
6
  "pushary",