Skip to content

Save-nudge never fires (or fires too eagerly) outside UTC+0, and never fires for a project's first observation #668

Description

@Alexter2003

📋 Pre-flight Checks

  • I have searched existing issues and this is not a duplicate
  • I understand this issue needs status:approved before a PR can be opened

📝 Bug Description

The save-nudge mechanism (the MEMORY REMINDER system message injected by the UserPromptSubmit hook to prompt the agent to call mem_save) never fires in practice for non-UTC hosts, and never fires at all for a project's very first observation — regardless of timezone. Across 4 real projects and 15 sessions on my machine, this produced 0 observations ever saved, despite substantial work happening in every session.

Two independent root causes, both in parse_epoch() / the nudge-gating logic, shared verbatim between plugin/claude-code/scripts/user-prompt-submit.sh and plugin/codex/scripts/user-prompt-submit.sh (the plugin/opencode/engram.ts implementation only has the second bug — its toEpochSecs already normalizes to UTC correctly):

1. parse_epoch() parses naive sqlite timestamps as local time instead of UTC.

sessions.started_at and observations.created_at are written by sqlite as datetime('now'), which returns YYYY-MM-DD HH:MM:SS in UTC with no timezone marker. The final fallback branch of parse_epoch() parses this string without -u:

date -j -f "%Y-%m-%dT%H:%M:%S" "$TS" "+%s" 2>/dev/null \
  || date -j -f "%Y-%m-%d %H:%M:%S" "$TS" "+%s" 2>/dev/null \
  || date -d "$TS" "+%s" 2>/dev/null

date interprets the naive string in the host's local timezone, so the computed epoch is skewed by the host's UTC offset:

  • Negative offsets (e.g. UTC-6, Latin America): the parsed epoch is pushed into the future relative to the real UTC epoch, so SESSION_AGE_SECS / ELAPSED come out negative or near-zero. The session/last-save always looks "just started," and the nudge condition (elapsed > 900) can never become true. The nudge never fires, for any project, ever.
  • Positive offsets (e.g. UTC+1/+2, Spain/Europe): the parsed epoch is pushed into the past, so elapsed time is inflated. The nudge fires far too eagerly — potentially right after the 5-minute session-age floor, and repeating every cooldown window even right after a save.
  • UTC+0 hosts happen not to notice, since local time equals UTC.

I confirmed both directions empirically:

Timestamp stored in sqlite (UTC): 2026-07-26 06:00:00

Real epoch (parsed with -u):                     1785045600
Buggy epoch, host TZ=UTC-6 (my machine):          <parses into the future — negative/near-zero elapsed>
Buggy epoch, host TZ=Europe/Madrid (UTC+1/+2):    1785038400   (2h in the past vs. real epoch)
Fixed epoch (forced -u), any TZ:                  1785045600   (correct in both cases)

2. All three hook implementations treat "no observations yet" as "session just started," so a project's first save can never be nudged.

if [ -z "$LAST_SAVE_AT" ]; then
  # No observations yet — no nudge (session might just be starting)
  echo "$OUTPUT"
  exit 0
fi

and the opencode equivalent:

// No observations yet — nothing to nudge about
if (lastObsEpoch === 0) return

If a project has never had an observation saved, this branch always short-circuits before the elapsed-time check, regardless of how long the session has been running. Since the nudge is the only automated mechanism nudging the agent to call mem_save in the first place, a project can never accumulate its first observation through this path — it's a cycle with no way out. Independent of bug #1, this alone guarantees 0 observations for any brand-new project.

🔄 Steps to Reproduce

  1. Start a fresh project with the Claude Code (or codex) plugin enabled, on a host whose local timezone is not UTC+0 (e.g. TZ=America/Mexico_City or TZ=Europe/Madrid).
  2. Start a session, send a first prompt (loads tools), then keep the session open and active past 5 minutes without ever calling mem_save.
  3. Send further prompts past the 15-minute staleness threshold.
  4. Observe the UserPromptSubmit hook output for each prompt (or check engram stats / engram projects list after a while).

✅ Expected Behavior

Once a session is older than 5 minutes and the last save (or project creation, if there's never been one) is older than 15 minutes, the hook should inject the MEMORY REMINDER system message — regardless of the host's timezone and regardless of whether the project has ever had an observation before.

❌ Actual Behavior

🖥️ Environment

📋 Relevant Logs

$ engram stats
Engram Memory Stats
  Sessions:     15
  Observations: 0
  Prompts:      43
  Projects:     none yet
  Database:     /Users/alexvas/.engram/engram.db

$ engram projects list
Projects (4):
  dotfiles       0 obs   1 session    1 prompt
  acatesystem    0 obs   8 sessions  29 prompts
  alexvas        0 obs   2 sessions   6 prompts
  docuai         0 obs   4 sessions   7 prompts

💡 Additional Context

I have a fix ready and verified for both root causes, in both the claude-code and codex bash hooks (forcing -u on every naive-timestamp date parse, and treating "never saved" as maximally-stale instead of "just started"), plus the equivalent fix for the opencode TypeScript implementation's bug #2. Happy to open the PR once this issue is approved, per CONTRIBUTING.md.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions