📋 Pre-flight Checks
📝 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
- 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).
- Start a session, send a first prompt (loads tools), then keep the session open and active past 5 minutes without ever calling
mem_save.
- Send further prompts past the 15-minute staleness threshold.
- 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.
📋 Pre-flight Checks
status:approvedbefore a PR can be opened📝 Bug Description
The save-nudge mechanism (the
MEMORY REMINDERsystem message injected by theUserPromptSubmithook to prompt the agent to callmem_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 betweenplugin/claude-code/scripts/user-prompt-submit.shandplugin/codex/scripts/user-prompt-submit.sh(theplugin/opencode/engram.tsimplementation only has the second bug — itstoEpochSecsalready normalizes to UTC correctly):1.
parse_epoch()parses naive sqlite timestamps as local time instead of UTC.sessions.started_atandobservations.created_atare written by sqlite asdatetime('now'), which returnsYYYY-MM-DD HH:MM:SSin UTC with no timezone marker. The final fallback branch ofparse_epoch()parses this string without-u:dateinterprets the naive string in the host's local timezone, so the computed epoch is skewed by the host's UTC offset:SESSION_AGE_SECS/ELAPSEDcome 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.I confirmed both directions empirically:
2. All three hook implementations treat "no observations yet" as "session just started," so a project's first save can never be nudged.
and the opencode equivalent:
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_savein 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
TZ=America/Mexico_CityorTZ=Europe/Madrid).mem_save.UserPromptSubmithook output for each prompt (or checkengram stats/engram projects listafter 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 REMINDERsystem message — regardless of the host's timezone and regardless of whether the project has ever had an observation before.❌ Actual Behavior
engram stats— 15 sessions, 43 prompts, 0 observations, across 4 different projects, over multiple days of real work.🖥️ Environment
📋 Relevant Logs
💡 Additional Context
I have a fix ready and verified for both root causes, in both the claude-code and codex bash hooks (forcing
-uon every naive-timestampdateparse, 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.