also looking at this
fix: answer a text that arrived while the Mac was asleep
#54
2 files
+115
-2
| @@ -66,6 +66,12 @@ interface ChatState { | ||
| 66 | 66 | /** Two copies of one self-text are written within a few seconds of each other. */ |
| 67 | 67 | const DUPLICATE_WINDOW_MS = 15_000 |
| 68 | 68 | const SENT_MEMORY_MS = 30 * 60_000 |
| 69 | /** | |
| 70 | * A skipped text older than this gets no mention. Waking a Mac that slept a | |
| 71 | * week should not text about Tuesday, and by then whoever sent it has long | |
| 72 | * since given up on it. | |
| 73 | */ | |
| 74 | const STALE_REPLY_CEILING_MS = 24 * 60 * 60_000 | |
| 69 | 75 | const OWN_HANDLES_REFRESH_MS = 10 * 60_000 |
| 70 | 76 | |
| 71 | 77 | const consoleLogger: Logger = { |
| @@ -81,6 +87,8 @@ export class Uplink { | ||
| 81 | 87 | private readonly sent: RecentTexts |
| 82 | 88 | private readonly chats = new Map<string, ChatState>() |
| 83 | 89 | private readonly lastCommand = new Map<string, number>() |
| 90 | /** Texts this tick found too old to run, by chat, reported once at the end. */ | |
| 91 | private readonly skipped = new Map<string, { target: ReplyTarget, count: number, newestAt: number }>() | |
| 84 | 92 | private own = new Set<string>() |
| 85 | 93 | private allowed = new Set<string>() |
| 86 | 94 | private ownRefreshedAt = 0 |
| @@ -212,6 +220,7 @@ export class Uplink { | ||
| 212 | 220 | this.cursor = message.rowid |
| 213 | 221 | await this.deps.store.setCursor(this.cursor) |
| 214 | 222 | } |
| 223 | await this.reportSkipped() | |
| 215 | 224 | this.lastPollAt = this.now() |
| 216 | 225 | this.lastError = null |
| 217 | 226 | } |
| @@ -254,8 +263,14 @@ export class Uplink { | ||
| 254 | 263 | return |
| 255 | 264 | } |
| 256 | 265 | |
| 257 | if (this.now() - message.sentAt > this.config.catchUpMs) { | |
| 258 | this.log.info(`Skipping stale text from ${formatDuration(this.now() - message.sentAt)} ago: ${truncate(verdict.text, 60)}`) | |
| 266 | const age = this.now() - message.sentAt | |
| 267 | if (age > this.config.catchUpMs) { | |
| 268 | this.log.info(`Skipping stale text from ${formatDuration(age)} ago: ${truncate(verdict.text, 60)}`) | |
| 269 | // `status` and `stop` are answered live or not at all: reporting that | |
| 270 | // the status someone asked for at midnight went unanswered is noise. | |
| 271 | // Past the ceiling nothing is a live question either. | |
| 272 | if (!parseControl(verdict.text) && age <= STALE_REPLY_CEILING_MS) | |
| 273 | this.noteSkipped(message) | |
| 259 | 274 | return |
| 260 | 275 | } |
| 261 | 276 | |
| @@ -327,6 +342,45 @@ export class Uplink { | ||
| 327 | 342 | await this.begin(state, job) |
| 328 | 343 | } |
| 329 | 344 | |
| 345 | /** | |
| 346 | * Remember a text that was too old to run. Silence was the previous answer, | |
| 347 | * and over a satellite link, where the send itself may have taken minutes, | |
| 348 | * silence is the worst one available. | |
| 349 | */ | |
| 350 | private noteSkipped(message: IncomingMessage): void { | |
| 351 | const existing = this.skipped.get(message.chatGuid) | |
| 352 | if (existing) { | |
| 353 | existing.count += 1 | |
| 354 | existing.newestAt = Math.max(existing.newestAt, message.sentAt) | |
| 355 | return | |
| 356 | } | |
| 357 | this.skipped.set(message.chatGuid, { | |
| 358 | target: { chatGuid: message.chatGuid, handle: message.chatIdentifier, service: message.service }, | |
| 359 | count: 1, | |
| 360 | newestAt: message.sentAt, | |
| 361 | }) | |
| 362 | } | |
| 363 | ||
| 364 | /** | |
| 365 | * One reply per chat per wake, whatever the backlog. A Mac that slept | |
| 366 | * through eight texts owes an explanation, not eight of them: the inbound | |
| 367 | * link is thin and a wall of texts is worse than none. | |
| 368 | * | |
| 369 | * It never runs the task. That is the whole reason the cutoff exists, and a | |
| 370 | * replayed day of texts could do real work nobody wants now. Sending it | |
| 371 | * again is one text, and the reply says so. | |
| 372 | */ | |
| 373 | private async reportSkipped(): Promise<void> { | |
| 374 | const batches = [...this.skipped.values()] | |
| 375 | this.skipped.clear() | |
| 376 | for (const batch of batches) { | |
| 377 | const ago = formatDuration(this.now() - batch.newestAt) | |
| 378 | await this.reply(batch.target, batch.count === 1 | |
| 379 | ? `That arrived ${ago} ago, while this Mac was asleep, so I did not run it. Send it again to run it now.` | |
| 380 | : `${batch.count} texts arrived while this Mac was asleep, the most recent ${ago} ago. I ran none of them. Send one again to run it now.`) | |
| 381 | } | |
| 382 | } | |
| 383 | ||
| 330 | 384 | private threadIsFresh(conversation: ConversationRecord): boolean { |
| 331 | 385 | return conversation.lastActiveAt !== null && this.now() - conversation.lastActiveAt < this.config.sessionIdleMs |
| 332 | 386 | } |