The probe target this week was the session/resume layer — the small file that persists a Claude session ID across tool restarts so that an attorney's multi-turn conversation continues from where they left off. The file lives at ~/.aaicase/session.json and contains three fields: the session ID, the timestamp of when it was saved, and the PID of the process that owned it.

On startup, the tool reads this file and decides whether to resume the saved session or start a fresh one. The rules:

The function was straightforward. The bugs were in two of the three checks.

Clock skew on the timestamp check

The timestamp check was if (Date.now() - data.ts < 2 * 60 * 60 * 1000). The intent: if the saved timestamp is less than two hours in the past, the session is still valid.

For a timestamp from the past, this works. For a timestamp from the future, the subtraction produces a negative number, and negative is always less than the positive two-hour bound. A session file with a future timestamp would be loaded indefinitely — every boot would pick it up, every boot would attempt to resume a stale session ID against the Claude API, and every boot would fail (probably) when the API didn't recognize it.

The case isn't impossible. Clock skew happens on Macs that wake from sleep at the wrong wall-clock time. NTP corrections can roll the system clock backward. Copying ~/.aaicase between machines preserves the file but not the original time context. Manual edits during development (which is exactly how I probed this) produce the same state.

This is the third instance of the same bug family we've shipped a fix for. The first was 4.5.32 — file lock detection treated locks as stale based on mtime alone, which a forward clock jump could fool. The second was 4.5.57 — cache TTL checks did Date.now() - mtimeMs < ttl, same subtraction trap, future-mtime cache would never expire. The third is now this one.

The fix is identical in pattern across all three. If age < 0, the file is in the future. Reject explicitly, optionally log a diagnostic, optionally clean up the stale file. Add three lines, run the probe again, watch the malformed case get rejected.

The recurring shape is small enough that I can now spot it on sight: anywhere we subtract a saved timestamp from Date.now() and compare to a positive bound, the negative-age case needs an explicit check. It's worth grepping the codebase for that pattern periodically. I don't think there are any other instances in this tool, but the next system I work on will probably have one.

Loose substring matching on error recovery

The second finding was in the session-error recovery branch. When a query fails, the tool decides whether the failure is a session-state problem (in which case clear the session and retry from scratch) or a transient problem (in which case preserve the session and bubble up the error). The decision rule was substring matching on the error message:

const isSessionError = errMsg.includes("session") || errMsg.includes("resume") || errMsg.includes("conversation not found");

The recovery is correct for the specific case the matching was designed for. The problem is that "session" is a common word in error messages. Transient errors that happen to contain the word — "session-info response timed out", "could not establish session keep-alive", "session-related metric collection failed" — would all trigger the recovery path. The recovery path destroys the session state and retries from scratch, which loses the attorney's multi-turn context.

The fix was to find out what the SDK actually emits for the real session-not-found case. Grepped the bundled SDK, found tengu_teleport_error_session_not_found_404. That's the specific signature. Tightened the matching to that string and a few other specific ones ("conversation not found", "session expired", "resume failed", "invalid session"). The loose "session" substring is gone.

The auth-error branch got the same treatment: was errMsg.includes("401") which matches any error string containing "401" anywhere; now matches " 401 " as a word boundary or "HTTP 401" or "authentication failed" specifically.

This is the fifth or sixth instance of the loose-substring-matching family. The pattern: code that needs to detect a specific error condition reaches for .includes() because writing the full regex is annoying. The substring catches the intended case AND many unintended ones. The fix is always to find the actual error string the upstream emits and match it precisely.

The first was around the HTTP 200 with errors-body pattern (4.5.39 and subsequent) — a Merus quirk where the API returns 200 status with an errors array in the body, and our code had to learn to inspect the body, not just the status. The next handful of releases caught the same pattern at other call sites (4.5.45, 4.5.46). Each one was a sibling instance of the same shape: the obvious check (status code, substring) wasn't sufficient; the precise check (specific error string, specific structure) was needed.

The value of recognizing shapes

The two bugs in this release are small in absolute terms. The clock skew on the session file probably never bit anyone — the Claude API would 404 on the stale session and the recovery path would kick in and start fresh, more or less invisibly to the attorney. The substring-matching false positives were probably rare — most error messages don't contain "session" as an incidental word.

What makes them worth shipping is recognizing the shape. Each shape, once named, generalizes. Clock skew anywhere wall-time subtraction is compared to a positive bound. Loose substring matching anywhere a specific error condition needs detection. After enough instances of each family, the diagnosis becomes "this is one of those, fix it the same way."

This is the value of the probe series being deliberately repetitive. We're not hunting for novel bug families. We're applying the same kind of probe — feed the validation/parsing/error-handling layer a deliberately-off input — and noting which fixes look like fixes we've already shipped elsewhere. The recurring shapes become a small mental library: a list of patterns that, once you have a name for them, you can recognize across codebases.

The version on the install URL is 4.5.86. Two small fixes. Two old shapes. The work continues at the pace of one verified release at a time.