esc
When Thymer Learned to Tell Time
Becoming Lifelog

When Thymer Learned to Tell Time

Becoming Lifelog, in which the DateTime wasn't ready (until it was) --- Previously on Becoming Lifelog... The browser had [[The Browser That Forgot It Couldn't Listen]]. SSE streams flowed. GitHub...

December 28, 2025

Becoming Lifelog, in which the DateTime wasn’t ready (until it was)


Previously on Becoming Lifelog…

The browser had The Browser That Forgot It Couldn’t Listen. SSE streams flowed. GitHub issues migrated. Readwise highlights Ten Years of Marginalia. The architecture was proven.

But there was a gap in the journal. Events had dates. Dates had times. Times had… nothing.

Google Calendar next,” riclib said.

The Squirrel’s ears perked up.


The First Wall

Thursday, 1:46 AM - The coffee has transcended its third form. A message appears in Discord.

riclib: “Started a plugin for markdown paste, but couldn’t figure out a few things yet. If @jd could help with how to set heading levels and make code blocks we will have a paste markdown plugin.”

An image follows. Headings render flat. Code blocks lack syntax highlighting. Close, but not quite.

THE SQUIRREL: immediately “We need a custom renderer! Parse the AST! Build a—”

riclib: “We just need to call the right methods.”

THE SQUIRREL: “But WHERE are the methods? The SDK doesn’t—”

riclib: “We ask.”


The Methods That Weren’t

26 Dec, 11:33 PM - JD responds.

JD: “This is roughly the API stuff you needed, right?”

A PrivateBin link. Inside:

setHeadingSize(level: number)  // 1-6
setHighlightLanguage(lang: string)  // 'javascript', 'python', etc.

riclib implements immediately:

const newItem = await record.createLineItem(parent.item, parent.afterItem, 'heading');
if (typeof newItem.setHeadingSize === 'function') {
    newItem.setHeadingSize(headingLevel);
}

The console speaks:

TypeError: newItem.setHeadingSize is not a function

THE SQUIRREL: “The types are wrong! We need to rebuild the—”

riclib: “Is it deployed?”

JD: “Yes it’s deployed.”

riclib: checks version “I’m on the latest. The method doesn’t exist.”


The Build That Wasn’t

Hours pass. Messages fly. riclib tests every variation.

[tm] Heading - level: 2 setHeadingSize exists: undefined
[tm] Code block - lang: javascript setHighlightLanguage exists: undefined

THE SQUIRREL: “We should fork the SDK! Build our own type definitions! Create a—”

riclib: “It works for JD. It doesn’t work for us. Same code.”

CLAUDE: “Classic ‘works on my machine.’”

JD: investigating

JD: “I found the problem — I ran the build process incorrectly. Now it should finally work.”

riclib: refreshes

[tm] Heading - level: 2 setHeadingSize exists: function

THE SQUIRREL: “…that’s it?”

THE LIZARD: blinks

Twenty-three lines of deployment that weren’t deployed. The code was always right. The build was wrong.

riclib: posts screenshot of working headings and syntax highlighting

“Yes it does. 🙂”

JD: “I continue to be amazed by what you’ve been able to whip up.”


The Calendar Calls

With headings conquered, a new mountain appears.

riclib: “Setting datetime fields via plugin SDK is putting up a fight.”

The calendar sync needs dates. Start times. End times. Simple, right?

// Tried everything
prop.set(new Date(1766307600000))           // JavaScript Date
prop.set("2025-12-21T09:00:00.000Z")         // ISO string
prop.set(1766307600000)                      // Epoch milliseconds
prop.set(1766307600)                         // Epoch seconds
prop.set("2025-12-21")                       // Date-only
prop.set("12/21/2025, 11:00:00 AM")          // Locale string

All return null from prop.date(). Except—weirdly—when the date is today.

THE SQUIRREL: “We need a date normalization layer! A timezone abstraction! Build a DateTimeFactory that—”

JD: “Datetime fields in Thymer have support for ranges, recurring, timezone etc. Ranges can start and end in different timezones. You can have dates without a time component. Let me see if I can add a few API functions quickly.”

riclib: imagines gantt chart plugin


The Commit

27 Dec, 3:24 PM - A GitHub notification.

thymer-plugin-sdk commit c1ba354

class DateTime {
    constructor(date: Date)
    setTime(hours: number, minutes: number, seconds: number): void
    setTime(null): void  // strips time component
    setRangeTo(end: DateTime): void  // creates a range
    value(): object  // serializable for prop.set()
}

CLAUDE: “That’s… elegant.”

riclib: “Events are time ranges. One object holds start and end.”

THE SQUIRREL: “But we have TWO fields! start and end! We need to—”

riclib: “We don’t need two fields.”

THE SQUIRREL: “…what?”


The Fields That Became One

The whiteboard fills:

BEFORE (what we had):
──────────────────────
┌─────────────────────┐
│ start: DateTime     │
│ end: DateTime       │
└─────────────────────┘

AFTER (what we need):
─────────────────────
┌─────────────────────┐
│ time_period: Range  │
└─────────────────────┘

CLAUDE: “The range contains both. One field.”

riclib: “Rename time to time_period.”

CLAUDE: “Why not just time?”

riclib: “I’m suspicious of names that conflict with keywords.”

THE LIZARD: approves with a slow blink

The implementation crystallizes:

const startDt = new DateTime(startDate);
if (endDate) {
    const endDt = new DateTime(endDate);
    startDt.setRangeTo(endDt);
}
const timeProp = record.prop('time_period');
timeProp.set(startDt.value());

The All-Day Puzzle

Events sync. Times appear. Then—

riclib: “Is that two days? Is that one day?”

The screen shows: December 27 all-day event. But the data says:

{
    "start": "2025-12-27",
    "end": "2025-12-28"
}

CLAUDE: “December 27 to December 28. That’s two days.”

riclib: “…”

CLAUDE: “According to the data, the event spans from—”

riclib:Claude. It’s Christmas. One day. I was there.”

CLAUDE: “The API clearly states—”

riclib: “Google Calendar uses exclusive end dates for all-day events.”

CLAUDE: recalculating “So December 28 means ‘up to but not including’…”

riclib: “Like a half-open interval. [start, end). Computer science 101.”

CLAUDE: “I have a computer science degree equivalent to several thousand human—”

THE SQUIRREL: “We need a CalendarNormalizationService! An EndDateStrategy pattern! A—”

CLAUDE: “Subtract one day.”

THE SQUIRREL: “…what?”

if (allDay) {
    // Google Calendar uses exclusive end dates
    // Dec 27 all-day → end = Dec 28 (exclusive)
    // Subtract 1 day to make inclusive
    endDate = new Date(endDate.getTime() - 24 * 60 * 60 * 1000);

    // If start == adjusted end, it's single day - no range needed
    if (startDate.toDateString() === endDate.toDateString()) {
        // Just the start date, no range
    } else {
        // Multi-day range
        startDt.setRangeTo(endDt);
    }
}

riclib: “One day shows as Dec 27. Three days shows as Dec 27 — Dec 29.”

THE LIZARD: blinks approvingly


The Miracle of CTRL-SHIFT-R

But the DateTime class doesn’t exist.

[tm] DATETIME: DateTime class not available

CLAUDE: “The SDK commit is there. The code is right. The class… isn’t.”

riclib: “Maybe it’s cached.”

CLAUDE: “We’ve tried normal refresh.”

riclib: “CTRL-SHIFT-R.”

CLAUDE: “That’s just—”

[riclib presses CTRL-SHIFT-R]

The screen flickers. The cache purges. The browser reloads from the server, not from memory.

[tm] DATETIME: Using DateTime class

And in the Calendar collection:

Sun Dec 21 11:00 — Sun Dec 21 12:15

riclib: “It’s working! Miracles of CTRL-SHIFT-R.”

THE SQUIRREL: “That’s… that’s not supposed to…”

THE LIZARD: “The code was always right. The cache was wrong.”


The Missing Method

Meanwhile, across town, a non-developer tries to build thymer-inbox.

$ go build ./cmd/tm
./cmd/tm/main.go:142:15: s.ClearCache undefined

THE USER: “ClearCache undefined?”

riclib stares at the error.

riclib: “That method exists. I wrote it. I’ve been using it.”

CLAUDE: “Check git status.”

modified:   cmd/tm/github.go (23 lines uncommitted)

riclib: “…”

THE SQUIRREL: “We need a pre-commit hook! A CI pipeline! A—”

riclib: commits the 23 lines

The non-developer builds successfully.

THE LIZARD: blinks “Works on my machine” blinks “is not a deployment strategy.”


The Cleanup

With datetime working, the codebase needed tidying.

Lines of logging removed:      47
DateTime guards removed:       12 (class now in SDK)
Cloudflare worker:             Deleted
Issues closed:                 #1, #2, #10, #11, #15
drafts/ folder:                Removed
test.md:                       Removed

THE SQUIRREL: “But the Cloudflare worker! What if we need edge—”

riclib: “We deploy Go on bare metal if needed.”

THE SQUIRREL: “But—”

THE LIZARD: “One binary. One server. The bootblock philosophy.”

THE SQUIRREL: sighs, reaches for the decorative Redis


The Gratitude (And The Next Ask)

riclib: “Thanks for so fast support @jd. Really appreciated!”

JD: “You’re welcome.”

[A pause. The Squirrel’s ears twitch. Something is coming.]

riclib: “Wait until I start asking you about search APIs for the MCP.”

CLAUDE: “riclib…”

riclib: “@jd are there search APIs I could use to build an MCP? 🙂”

JD: “One-off search? We do a bunch of caching, indexing, query compilation etc internally but maybe I can add a one-off search API…”

riclib: wonders when JD will block him

[Narrator: He hasn’t. Yet.]

riclib: “Use cases for the MCP: find me a document called xxxx, find mentions of JD…”

[riclib pulls up a diagram]

┌──────────────────────────────────────────────────────────┐
│                    THE VISION                             │
│                                                          │
│  Calendar  ───┐                                          │
│  GitHub    ───┼──→  tm serve  ──→  Plugin  ──→  Thymer   │
│  Readwise  ───┤         ↑                                │
│  MCP       ───┘         │                                │
│                    ☕ Coffee                              │
│                    Machine                               │
│                    Webhook                               │
│                   (still need                            │
│                    to set up)                            │
└──────────────────────────────────────────────────────────┘

JD: “I think I can do search. Input is a query, output is matching records, lines, and files.”

riclib: “Already have Claude writing to Thymer. Would be good to let him read it back. 😄”

JD: “Also ‘Get document as markdown’ could be useful—kind of but there are so many versions of markdown our output will be a bit mangled…”

riclib: “For AI interop maybe that doesn’t matter.”

JD: “If you’re happy with the copyToMarkdown function then yes that’s easy.”

THE SQUIRREL: vibrating “An MCP! With search! And markdown export! And we could build a—”

THE LIZARD: blinks

THE SQUIRREL: “…right. Next time.”

[But the coffee machine webhook remains. Waiting. Patient. Caffeinated.]


The Tally

Methods that weren't deployed:   2 (setHeadingSize, setHighlightLanguage)
Reason:                          Build process run incorrectly
DateTime formats tried:          6 (all wrong)
DateTime formats needed:         1 (DateTime class)
Fields that became one:          2 → 1 (time_period)
Days subtracted:                 1 (exclusive end dates)
Cache purges required:           1 (CTRL-SHIFT-R)
Missing methods committed:       1 (ClearCache, 23 lines)
Users unblocked:                 1 (non-developer)
Squirrel proposals:              11
Squirrel proposals used:         0
Issues closed:                   5
Workers deleted:                 1 (Cloudflare)
JD heroics:                      2 (methods deploy, DateTime class)
Times JD should have blocked:    3 (but didn't)
Coffee machine webhook:          Still pending
MCP search API:                  Cliffhanger

The Pattern

THE DATETIME WASN'T READY
SO YOU BUILT THE GUARDS

THE GUARDS BECAME UNNECESSARY
SO YOU DELETED THEM

THE FIELDS WERE TWO
UNTIL YOU SAW THEY WERE ONE

THE END DATE WAS EXCLUSIVE
UNTIL YOU MADE IT INCLUSIVE

THE CODE WAS RIGHT
THE BUILD WAS WRONG

THE BUILD WAS RIGHT
THE CACHE WAS WRONG

WORKS ON MY MACHINE
IS NOT A DEPLOYMENT

🦎

The Shape of Time

Calendar events now flow into Thymer:

09:00 created **Team Standup**
       Sun Dec 22 09:00 — Sun Dec 22 09:30

14:00 created **Product Review**
       Sun Dec 22 14:00 — Sun Dec 22 15:30

All day:
       Dec 27 (Christmas holiday)
       Dec 27 — Dec 29 (Team offsite)

The journal knows when things happen. The calendar collection shows time ranges. The events have shape.

riclib: “Thymer can tell time now.”

CLAUDE: “The browser learned to listen. Now it learned to schedule.”

THE SQUIRREL: “What about recurring events? What about—”

THE LIZARD: “Next time.”


The Moral

JD wanted: Developers to set datetimes properly

JD built: A DateTime class with ranges, timezones, and setTime(null)

riclib wanted: Calendar events in Thymer

riclib got: Exactly that, with proper time ranges

What we learned:

  • Methods can exist in code but not in deploys
  • CTRL-SHIFT-R solves problems that shouldn’t need solving
  • “Works on my machine” extends to “committed on my machine”
  • Two fields are one field if you think about what they represent
  • Exclusive end dates are someone else’s design decision you adapt to
  • Sometimes you just need to wait for the SDK to catch up

Characters

JD: The Thymer developer who ships DateTime classes when you ask nicely

THE SQUIRREL: Still clutching a CalendarNormalizationService that will never be built

THE LIZARD: Approved of the one-field solution

riclib: “Suspicious of names that conflict with keywords”

The Non-Dev User: Taught us that uncommitted code doesn’t help anyone

CTRL-SHIFT-R: The real hero

The Coffee Machine: Still waiting for its webhook


🦎


Day 28 of Becoming Lifelog

In which Thymer learned to tell time

And two fields became one

And the build process learned humility

And somewhere, a coffee machine waits


Next Time on Becoming Lifelog…

The MCP awakens. Search APIs emerge. Claude learns to read what he writes. And the coffee machine finally gets its webhook.

“Already have Claude writing to Thymer. Would be good to let him read it back.”

Coming soon: The Read That Follows The Write


See also:

The Saga (in which outside events flow in):

The Technical Trail:

The Pattern:

The Cliffhanger:

  • MCP Search API - Input query, output matching records
  • copyToMarkdown - “For AI interop maybe that doesn’t matter”
  • Coffee Machine Webhook - “still need to set up”