@fitchmultz/pi-subagents 0.47.0 → 0.49.0

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 (73) hide show
  1. package/CHANGELOG.md +29 -3
  2. package/dist/extension/build-info.js +1 -1
  3. package/dist/pi-intercom/bridge-broadcast.js +60 -0
  4. package/dist/pi-intercom/bridge-catalog.js +372 -0
  5. package/dist/pi-intercom/bridge-config.js +15 -3
  6. package/dist/pi-intercom/bridge-control.js +104 -0
  7. package/dist/pi-intercom/bridge-groups.js +135 -0
  8. package/dist/pi-intercom/bridge-http.js +8 -1
  9. package/dist/pi-intercom/bridge-launch-journal.js +84 -0
  10. package/dist/pi-intercom/bridge-launch-policy.js +71 -5
  11. package/dist/pi-intercom/bridge-launch.js +186 -76
  12. package/dist/pi-intercom/bridge-mailbox.js +3 -55
  13. package/dist/pi-intercom/bridge-operations.js +127 -0
  14. package/dist/pi-intercom/bridge-presence.js +5 -1
  15. package/dist/pi-intercom/bridge-protocol.js +7 -0
  16. package/dist/pi-intercom/bridge-routes.js +195 -27
  17. package/dist/pi-intercom/bridge-rpc-process.js +43 -15
  18. package/dist/pi-intercom/bridge-session.js +83 -66
  19. package/dist/pi-intercom/bridge-storage.js +73 -0
  20. package/dist/pi-intercom/bridge-target.js +22 -0
  21. package/dist/pi-intercom/bridge-wake-owner.js +57 -0
  22. package/dist/pi-intercom/bridge-wake-state.js +293 -0
  23. package/dist/pi-intercom/bridge-wake.js +190 -99
  24. package/dist/pi-intercom/bridge.js +39 -39
  25. package/dist/pi-intercom/broker/client.js +1 -0
  26. package/dist/pi-intercom/connection.js +2 -3
  27. package/dist/pi-intercom/inbound.js +2 -4
  28. package/dist/pi-intercom/index.js +7 -1
  29. package/dist/pi-intercom/native-control-auth.js +80 -0
  30. package/dist/pi-intercom/native-control-protocol.js +111 -0
  31. package/dist/pi-intercom/native-control.js +130 -0
  32. package/dist/pi-intercom/saved-transcript.js +135 -0
  33. package/dist/pi-intercom/session-targets.js +3 -0
  34. package/dist/pi-intercom/transcript.js +175 -0
  35. package/docs/intercom-bridge.md +315 -174
  36. package/docs/intercom.md +1 -1
  37. package/docs/reference.md +1 -1
  38. package/package.json +1 -1
  39. package/scripts/intercom-bridge-wake-preflight.mjs +112 -0
  40. package/scripts/intercom-remote.mjs +213 -38
  41. package/src/pi-intercom/bridge-broadcast.ts +70 -0
  42. package/src/pi-intercom/bridge-catalog.ts +489 -0
  43. package/src/pi-intercom/bridge-config.ts +20 -3
  44. package/src/pi-intercom/bridge-control.ts +179 -0
  45. package/src/pi-intercom/bridge-groups.ts +169 -0
  46. package/src/pi-intercom/bridge-http.ts +15 -1
  47. package/src/pi-intercom/bridge-launch-journal.ts +122 -0
  48. package/src/pi-intercom/bridge-launch-policy.ts +88 -7
  49. package/src/pi-intercom/bridge-launch.ts +296 -107
  50. package/src/pi-intercom/bridge-mailbox.ts +3 -59
  51. package/src/pi-intercom/bridge-operations.ts +169 -0
  52. package/src/pi-intercom/bridge-presence.ts +7 -1
  53. package/src/pi-intercom/bridge-protocol.ts +8 -0
  54. package/src/pi-intercom/bridge-routes.ts +291 -49
  55. package/src/pi-intercom/bridge-rpc-process.ts +53 -16
  56. package/src/pi-intercom/bridge-session.ts +98 -96
  57. package/src/pi-intercom/bridge-storage.ts +83 -0
  58. package/src/pi-intercom/bridge-target.ts +35 -0
  59. package/src/pi-intercom/bridge-wake-owner.ts +58 -0
  60. package/src/pi-intercom/bridge-wake-state.ts +321 -0
  61. package/src/pi-intercom/bridge-wake.ts +224 -130
  62. package/src/pi-intercom/bridge.ts +41 -49
  63. package/src/pi-intercom/broker/client.ts +3 -0
  64. package/src/pi-intercom/connection.ts +2 -3
  65. package/src/pi-intercom/inbound.ts +2 -4
  66. package/src/pi-intercom/index.ts +7 -1
  67. package/src/pi-intercom/native-control-auth.ts +89 -0
  68. package/src/pi-intercom/native-control-protocol.ts +180 -0
  69. package/src/pi-intercom/native-control.ts +160 -0
  70. package/src/pi-intercom/saved-transcript.ts +175 -0
  71. package/src/pi-intercom/session-targets.ts +4 -0
  72. package/src/pi-intercom/transcript.ts +226 -0
  73. package/src/pi-intercom/types.ts +2 -0
@@ -0,0 +1,73 @@
1
+ import { closeSync, constants, fstatSync, lstatSync, openSync, realpathSync } from "node:fs";
2
+ import { isAbsolute, join } from "node:path";
3
+ import { DatabaseSync } from "node:sqlite";
4
+ import { isRecord } from "../shared/unknown.js";
5
+ import { label } from "./bridge-protocol.js";
6
+ export function stateDirectory(value) {
7
+ if (value === undefined) {
8
+ return;
9
+ }
10
+ const path = label(value, "stateDir", 4096);
11
+ const stat = lstatSync(path);
12
+ if (!isAbsolute(path) ||
13
+ !stat.isDirectory() ||
14
+ stat.uid !== process.getuid?.() ||
15
+ (stat.mode & 0o777) !== 0o700) {
16
+ throw new Error("stateDir must be an absolute owned directory with mode 0700, not a symlink.");
17
+ }
18
+ return realpathSync(path);
19
+ }
20
+ export function privateFile(path) {
21
+ const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
22
+ try {
23
+ const stat = fstatSync(fd);
24
+ if (!stat.isFile() ||
25
+ stat.uid !== process.getuid?.() ||
26
+ (stat.mode & 0o777) !== 0o600 ||
27
+ stat.nlink !== 1) {
28
+ throw new Error("Storage files must be owned regular files with mode 0600 and one link.");
29
+ }
30
+ }
31
+ finally {
32
+ closeSync(fd);
33
+ }
34
+ }
35
+ /** Native SQLite FULL commits own durability; never replace an unreadable existing database. */
36
+ export function privateDatabase(stateDir, name) {
37
+ stateDirectory(stateDir);
38
+ const path = join(stateDir, name);
39
+ let created = false;
40
+ try {
41
+ const fd = openSync(path, constants.O_RDWR | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
42
+ closeSync(fd);
43
+ created = true;
44
+ }
45
+ catch (error) {
46
+ if (!isRecord(error) || error.code !== "EEXIST") {
47
+ throw error;
48
+ }
49
+ }
50
+ privateFile(path);
51
+ for (const suffix of ["-journal", "-wal", "-shm"]) {
52
+ try {
53
+ lstatSync(`${path}${suffix}`);
54
+ }
55
+ catch (error) {
56
+ if (isRecord(error) && error.code === "ENOENT") {
57
+ continue;
58
+ }
59
+ throw error;
60
+ }
61
+ privateFile(`${path}${suffix}`);
62
+ }
63
+ const db = new DatabaseSync(path, { enableDoubleQuotedStringLiterals: false });
64
+ try {
65
+ db.exec(`PRAGMA journal_mode=DELETE; PRAGMA synchronous=FULL; PRAGMA secure_delete=ON;
66
+ PRAGMA cache_size=-1024; PRAGMA busy_timeout=1000;`);
67
+ return { db, created };
68
+ }
69
+ catch (error) {
70
+ db.close();
71
+ throw error;
72
+ }
73
+ }
@@ -0,0 +1,22 @@
1
+ import { resolveSessionTarget, formatSessionTarget, nativePeerId } from "./session-targets.js";
2
+ import { Fault } from "./bridge-protocol.js";
3
+ export function bridgeSessionList(peers) {
4
+ return peers.map(({ topics: _topics, subscriptions: _subscriptions, ...entry }) => ({
5
+ ...entry,
6
+ target: formatSessionTarget(entry, peers),
7
+ }));
8
+ }
9
+ export function resolveBridgeTarget(peers, to, self) {
10
+ const exactNative = peers.find((peer) => peer.id === nativePeerId(to));
11
+ const resolution = resolveSessionTarget(peers, exactNative?.id ?? to);
12
+ if (resolution.status === "none") {
13
+ throw new Fault(404, "peer_offline", "Peer is not connected.");
14
+ }
15
+ if (resolution.status !== "found" || !resolution.target) {
16
+ throw new Fault(409, "ambiguous_target", "Use an unambiguous peer name or session ID (prefixes need at least 8 characters).");
17
+ }
18
+ if (resolution.target.id === self) {
19
+ throw new Fault(400, "self_target", "Choose another peer.");
20
+ }
21
+ return resolution.target;
22
+ }
@@ -0,0 +1,57 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { rmSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { DatabaseSync } from "node:sqlite";
5
+ import { privateDatabase, privateFile } from "./bridge-storage.js";
6
+ import { isRecord } from "../shared/unknown.js";
7
+ export const WAKE_OWNER_FILE = /^wake-owner-[0-9a-f-]{36}\.lock$/;
8
+ /** SQLite's OS lock survives pauses and is released on exit, without PID reuse or stale leases. */
9
+ export class WakeOwner {
10
+ name = `wake-owner-${randomUUID()}.lock`;
11
+ db;
12
+ path;
13
+ constructor(stateDir) {
14
+ this.path = join(stateDir, this.name);
15
+ this.db = privateDatabase(stateDir, this.name).db;
16
+ try {
17
+ this.db.exec("BEGIN EXCLUSIVE");
18
+ }
19
+ catch (error) {
20
+ this.db.close();
21
+ throw error;
22
+ }
23
+ }
24
+ close() {
25
+ this.db.close();
26
+ rmSync(this.path, { force: true });
27
+ }
28
+ }
29
+ export function stoppedWakeOwner(stateDir, name) {
30
+ if (!WAKE_OWNER_FILE.test(name)) {
31
+ throw new Error("Invalid wake owner.");
32
+ }
33
+ const path = join(stateDir, name);
34
+ try {
35
+ privateFile(path);
36
+ }
37
+ catch (error) {
38
+ if (isRecord(error) && error.code === "ENOENT") {
39
+ return true;
40
+ }
41
+ throw error;
42
+ }
43
+ const db = new DatabaseSync(path);
44
+ try {
45
+ db.exec("PRAGMA busy_timeout=0; BEGIN EXCLUSIVE");
46
+ return true;
47
+ }
48
+ catch (error) {
49
+ if (isRecord(error) && error.errcode === 5) {
50
+ return false;
51
+ }
52
+ throw error;
53
+ }
54
+ finally {
55
+ db.close();
56
+ }
57
+ }
@@ -0,0 +1,293 @@
1
+ import { lstatSync, readdirSync, rmSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { privateDatabase, privateFile, stateDirectory } from "./bridge-storage.js";
4
+ import { readBridgeFile } from "./bridge-files.js";
5
+ import { isRecord, isUnknownArray } from "../shared/unknown.js";
6
+ import { object } from "./bridge-protocol.js";
7
+ import { WakeOwner, WAKE_OWNER_FILE, stoppedWakeOwner } from "./bridge-wake-owner.js";
8
+ const HOUR_MS = 3600000;
9
+ const LIMIT = 20;
10
+ const RETRY_MS = 30000;
11
+ const MAX_BACKOFF_MS = 300000;
12
+ const FILE = "wake-quota.sqlite";
13
+ const ATTEMPTS = `CREATE TABLE wake_attempts(
14
+ sequence INTEGER PRIMARY KEY, attemptedAt INTEGER NOT NULL,
15
+ settledAt INTEGER, owner TEXT,
16
+ CHECK(settledAt IS NOT NULL OR owner IS NOT NULL))`;
17
+ function integer(value) {
18
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
19
+ throw new Error("Invalid durable wake state.");
20
+ }
21
+ return value;
22
+ }
23
+ /** One helper ledger, independent of identity, certificate, URL and credential rotation. */
24
+ export class WakeState {
25
+ db;
26
+ path;
27
+ stateDir;
28
+ inode;
29
+ device;
30
+ owner;
31
+ finishing = false;
32
+ constructor(stateDir) {
33
+ this.stateDir = stateDir;
34
+ this.path = join(stateDir, FILE);
35
+ const { db, created } = privateDatabase(stateDir, FILE);
36
+ this.db = db;
37
+ const stat = lstatSync(this.path);
38
+ this.inode = stat.ino;
39
+ this.device = stat.dev;
40
+ try {
41
+ if (created) {
42
+ this.initialize();
43
+ }
44
+ this.verify();
45
+ this.migrate();
46
+ this.owner = this.createOwner();
47
+ }
48
+ catch (error) {
49
+ db.close();
50
+ throw error;
51
+ }
52
+ }
53
+ verify() {
54
+ const db = this.db;
55
+ if (db.prepare("PRAGMA quick_check").get()?.quick_check !== "ok") {
56
+ throw new Error("Wake ledger is corrupt.");
57
+ }
58
+ const meta = db.prepare("SELECT version, clock FROM wake_meta WHERE singleton=1").get();
59
+ if (!meta || (meta.version !== 1 && meta.version !== 2)) {
60
+ throw new Error("Unsupported wake ledger.");
61
+ }
62
+ integer(meta.clock);
63
+ this.verifyAttempts(meta.version);
64
+ for (const row of db.prepare("SELECT due, backoff FROM wake_batches").all()) {
65
+ integer(row.due);
66
+ const backoff = integer(row.backoff);
67
+ if ((backoff !== 0 && backoff < RETRY_MS) || backoff > MAX_BACKOFF_MS) {
68
+ throw new Error("Invalid wake retry state.");
69
+ }
70
+ }
71
+ }
72
+ createOwner() {
73
+ let owner;
74
+ try {
75
+ // Recovery holds this same ledger lock: it cannot reap a file before its owner locks it.
76
+ return this.transaction(() => {
77
+ owner = new WakeOwner(this.stateDir);
78
+ return owner;
79
+ });
80
+ }
81
+ catch (error) {
82
+ owner?.close();
83
+ throw error;
84
+ }
85
+ }
86
+ verifyAttempts(version) {
87
+ for (const row of this.db.prepare("SELECT attemptedAt FROM wake_attempts").all()) {
88
+ integer(row.attemptedAt);
89
+ }
90
+ if (version !== 2) {
91
+ return;
92
+ }
93
+ for (const row of this.db
94
+ .prepare("SELECT attemptedAt, settledAt, owner FROM wake_attempts")
95
+ .all()) {
96
+ if (row.settledAt !== null) {
97
+ if (integer(row.settledAt) < integer(row.attemptedAt)) {
98
+ throw new Error("Invalid wake completion time.");
99
+ }
100
+ }
101
+ else if (typeof row.owner !== "string" || !WAKE_OWNER_FILE.test(row.owner)) {
102
+ throw new Error("Invalid open wake attempt.");
103
+ }
104
+ }
105
+ }
106
+ initialize() {
107
+ // Timestamped legacy outcomes omit crash-before-log attempts. Verified preflight may provide
108
+ // complete history plus a bounded unknown reservation; absent proof reserves the full hour.
109
+ const legacy = readdirSync(this.stateDir).some((name) => name !== FILE && (name.endsWith(".sqlite") || name.startsWith("presence-")));
110
+ const now = Date.now();
111
+ const attempts = this.history(now) ?? (legacy ? Array(LIMIT).fill(now) : []);
112
+ this.db.exec(`BEGIN IMMEDIATE;
113
+ CREATE TABLE wake_meta(singleton INTEGER PRIMARY KEY CHECK(singleton=1), version INTEGER NOT NULL, clock INTEGER NOT NULL);
114
+ ${ATTEMPTS};
115
+ CREATE TABLE wake_batches(bucket TEXT PRIMARY KEY, due INTEGER NOT NULL, backoff INTEGER NOT NULL);`);
116
+ this.db.prepare("INSERT INTO wake_meta VALUES(1,2,?)").run(now);
117
+ const insert = this.db.prepare("INSERT INTO wake_attempts(attemptedAt,settledAt) VALUES(?,?)");
118
+ for (const timestamp of attempts) {
119
+ insert.run(timestamp, timestamp);
120
+ }
121
+ this.db.exec("COMMIT");
122
+ }
123
+ migrate() {
124
+ this.transaction(() => {
125
+ if (this.db.prepare("SELECT version FROM wake_meta").get()?.version !== 1) {
126
+ return;
127
+ }
128
+ // V1 timestamps precede COMMIT/connect/TLS, so none prove when a POST finished.
129
+ // Stop all old writers before upgrading; the new CHECK also denies their old INSERT shape.
130
+ const now = Math.max(Date.now(), integer(this.db.prepare("SELECT clock FROM wake_meta").get()?.clock));
131
+ this.db.exec(`ALTER TABLE wake_attempts RENAME TO wake_attempts_v1; ${ATTEMPTS};`);
132
+ this.db
133
+ .prepare(`INSERT INTO wake_attempts(sequence,attemptedAt,settledAt)
134
+ SELECT sequence,attemptedAt,MAX(attemptedAt,?) FROM wake_attempts_v1`)
135
+ .run(now);
136
+ this.db.exec("DROP TABLE wake_attempts_v1");
137
+ this.db.prepare("UPDATE wake_meta SET version=2,clock=?").run(now);
138
+ });
139
+ }
140
+ history(now) {
141
+ const path = join(this.stateDir, "wake-history.json");
142
+ let input;
143
+ try {
144
+ privateFile(path);
145
+ input = JSON.parse(readBridgeFile(path, true, 1024 * 1024).toString("utf8"));
146
+ }
147
+ catch (error) {
148
+ if (isRecord(error) && error.code === "ENOENT") {
149
+ return;
150
+ }
151
+ throw error;
152
+ }
153
+ const raw = object(input, ["coveredSince", "stoppedAt", "attempts", "unknownAttempts"]);
154
+ const coveredSince = integer(raw.coveredSince);
155
+ const stoppedAt = integer(raw.stoppedAt);
156
+ const unknownAttempts = integer(raw.unknownAttempts);
157
+ if (coveredSince > stoppedAt - HOUR_MS ||
158
+ stoppedAt > now ||
159
+ unknownAttempts > LIMIT ||
160
+ !isUnknownArray(raw.attempts) ||
161
+ raw.attempts.length > 10000) {
162
+ throw new Error("Invalid legacy wake history; never reset prior usage.");
163
+ }
164
+ const attempts = raw.attempts.map(integer);
165
+ if (attempts.some((timestamp) => timestamp < coveredSince || timestamp > stoppedAt)) {
166
+ throw new Error("Attempt outside attested history coverage.");
167
+ }
168
+ return attempts
169
+ .filter((timestamp) => timestamp > now - HOUR_MS)
170
+ .concat(Array(unknownAttempts).fill(now));
171
+ }
172
+ validate() {
173
+ stateDirectory(this.stateDir);
174
+ privateFile(this.path);
175
+ const stat = lstatSync(this.path);
176
+ if (stat.ino !== this.inode || stat.dev !== this.device) {
177
+ throw new Error("Wake ledger was replaced.");
178
+ }
179
+ }
180
+ deadline(bucket) {
181
+ this.validate();
182
+ const existing = this.db.prepare("SELECT due FROM wake_batches WHERE bucket=?").get(bucket);
183
+ if (existing) {
184
+ return integer(existing.due);
185
+ }
186
+ // First observation occurs only after mailbox FULL commit, never at the pre-write receivedAt.
187
+ // Keep this grace durable too: new arrivals/reload/restart cannot restart an existing deadline.
188
+ this.db
189
+ .prepare("INSERT OR IGNORE INTO wake_batches VALUES(?,?,0)")
190
+ .run(bucket, Date.now() + 2000);
191
+ return integer(this.db.prepare("SELECT due FROM wake_batches WHERE bucket=?").get(bucket)?.due);
192
+ }
193
+ retire(bucket) {
194
+ this.validate();
195
+ this.db.prepare("DELETE FROM wake_batches WHERE bucket=?").run(bucket);
196
+ }
197
+ /** Persist quota and uncertainty BEFORE dispatch. Returning a deadline means no attempt admitted. */
198
+ reserve(bucket) {
199
+ this.retrySettlement();
200
+ return this.transaction(() => this.reservation(bucket));
201
+ }
202
+ retrySettlement() {
203
+ if (this.finishing) {
204
+ this.settle();
205
+ }
206
+ }
207
+ transaction(action) {
208
+ this.validate();
209
+ this.db.exec("BEGIN IMMEDIATE");
210
+ try {
211
+ const result = action();
212
+ this.db.exec("COMMIT");
213
+ return result;
214
+ }
215
+ catch (error) {
216
+ this.db.exec("ROLLBACK");
217
+ throw error;
218
+ }
219
+ }
220
+ /** A failed settlement stays open and must be retried before this owner can dispatch again. */
221
+ settle() {
222
+ this.finishing = true;
223
+ this.transaction(() => this.finishOwner(this.owner.name));
224
+ this.finishing = false;
225
+ }
226
+ finishOwner(owner) {
227
+ const now = Math.max(Date.now(), integer(this.db.prepare("SELECT clock FROM wake_meta").get()?.clock));
228
+ this.db
229
+ .prepare(`UPDATE wake_attempts SET settledAt=MAX(attemptedAt,?)
230
+ WHERE owner=? AND settledAt IS NULL`)
231
+ .run(now, owner);
232
+ this.db.prepare("UPDATE wake_meta SET clock=?").run(now);
233
+ }
234
+ recover() {
235
+ const names = new Set(readdirSync(this.stateDir).filter((name) => WAKE_OWNER_FILE.test(name)));
236
+ for (const row of this.db
237
+ .prepare("SELECT DISTINCT owner FROM wake_attempts WHERE settledAt IS NULL")
238
+ .all()) {
239
+ if (typeof row.owner !== "string") {
240
+ throw new Error("Invalid open wake owner.");
241
+ }
242
+ names.add(row.owner);
243
+ }
244
+ for (const name of names) {
245
+ if (name !== this.owner.name && stoppedWakeOwner(this.stateDir, name)) {
246
+ // The owner cannot send again. Unknown outcomes retain a full hour from this observation.
247
+ this.finishOwner(name);
248
+ rmSync(join(this.stateDir, name), { force: true });
249
+ }
250
+ }
251
+ }
252
+ reservation(bucket) {
253
+ this.recover();
254
+ const clock = integer(this.db.prepare("SELECT clock FROM wake_meta WHERE singleton=1").get()?.clock);
255
+ const now = Date.now();
256
+ if (now < clock) {
257
+ return clock;
258
+ }
259
+ const row = this.db.prepare("SELECT due, backoff FROM wake_batches WHERE bucket=?").get(bucket);
260
+ if (row && integer(row.due) > now) {
261
+ return integer(row.due);
262
+ }
263
+ this.db.prepare("DELETE FROM wake_attempts WHERE settledAt<=?").run(now - HOUR_MS);
264
+ const attempts = this.db
265
+ .prepare("SELECT settledAt FROM wake_attempts ORDER BY settledAt")
266
+ .all();
267
+ if (attempts.length >= LIMIT) {
268
+ const oldest = attempts.find((attempt) => attempt.settledAt !== null);
269
+ // Only open/unknown attempts have no expiry yet; revisit on the existing recovery backoff.
270
+ return oldest ? integer(oldest.settledAt) + HOUR_MS : now + RETRY_MS;
271
+ }
272
+ const backoff = row
273
+ ? Math.max(RETRY_MS, Math.min(integer(row.backoff) * 2, MAX_BACKOFF_MS))
274
+ : RETRY_MS;
275
+ this.db
276
+ .prepare("INSERT INTO wake_attempts(attemptedAt,owner) VALUES(?,?)")
277
+ .run(now, this.owner.name);
278
+ this.db.prepare("UPDATE wake_meta SET clock=? WHERE singleton=1").run(now);
279
+ this.db
280
+ .prepare("INSERT OR REPLACE INTO wake_batches VALUES(?,?,?)")
281
+ .run(bucket, now + backoff, backoff);
282
+ return;
283
+ }
284
+ close() {
285
+ try {
286
+ this.settle();
287
+ }
288
+ finally {
289
+ this.db.close();
290
+ this.owner.close();
291
+ }
292
+ }
293
+ }