Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 24 additions & 15 deletions plugin/claude-code/scripts/user-prompt-submit.sh
Original file line number Diff line number Diff line change
Expand Up @@ -148,9 +148,14 @@ parse_epoch() {
date -j -u -f "%Y-%m-%dT%H:%M:%S" "$Z_TS" "+%s" 2>/dev/null && return 0
fi

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
# No timezone marker at all — these are naive timestamps from sqlite's
# datetime('now'), which is always UTC. Must parse with -u; parsing as
# local time skews the result by the host's UTC offset (e.g. on a UTC-6
# host, session age comes out ~6h in the future, so it's always seen as
# "just started" and the save-nudge can never fire).
date -j -u -f "%Y-%m-%dT%H:%M:%S" "$TS" "+%s" 2>/dev/null \
|| date -j -u -f "%Y-%m-%d %H:%M:%S" "$TS" "+%s" 2>/dev/null \
|| date -u -d "$TS" "+%s" 2>/dev/null
}

# Default: no injection
Expand Down Expand Up @@ -238,20 +243,24 @@ fi

LAST_SAVE_AT=$(echo "$LAST_SAVE_JSON" | jq -r '.[0].created_at // empty' 2>/dev/null)

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

# Parse last save timestamp and compare to now
LAST_EPOCH=$(parse_epoch "$LAST_SAVE_AT")
if [ -z "$LAST_EPOCH" ]; then
echo "$OUTPUT"
exit 0
if [ -z "$LAST_SAVE_AT" ]; then
# No observations exist yet for this project. This is the "never saved"
# case, not "session just started" (session age was already gated to
# >= 5 minutes above) — treat it as maximally stale so the nudge can fire
# and break the cycle where a project with zero observations can never
# reach its first one.
ELAPSED=901
else
# Parse last save timestamp and compare to now
LAST_EPOCH=$(parse_epoch "$LAST_SAVE_AT")
if [ -z "$LAST_EPOCH" ]; then
echo "$OUTPUT"
exit 0
fi
ELAPSED=$(( NOW_EPOCH - LAST_EPOCH ))
Comment on lines +246 to +262

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Move save-nudge eligibility into the core Go API/tool.

The adapters independently decide first-save staleness and reminder eligibility. Put this policy in the core so all integrations consume one result and cannot drift.

  • plugin/claude-code/scripts/user-prompt-submit.sh#L246-L262: replace the local sentinel/elapsed decision with a core eligibility call.
  • plugin/codex/scripts/user-prompt-submit.sh#L177-L193: replace the duplicate local eligibility decision with the same core call.
  • plugin/opencode/engram.ts#L481-L488: consume the core eligibility result instead of evaluating observation age locally.

As per path instructions, adapters stay thin: parse input, call the core Go API/tool, return.

📍 Affects 3 files
  • plugin/claude-code/scripts/user-prompt-submit.sh#L246-L262 (this comment)
  • plugin/codex/scripts/user-prompt-submit.sh#L177-L193
  • plugin/opencode/engram.ts#L481-L488
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugin/claude-code/scripts/user-prompt-submit.sh` around lines 246 - 262,
Move save-nudge eligibility into the core Go API/tool so adapters no longer
calculate first-save staleness or observation age locally. In
plugin/claude-code/scripts/user-prompt-submit.sh lines 246-262 and
plugin/codex/scripts/user-prompt-submit.sh lines 177-193, replace the local
sentinel and elapsed-time decisions with the shared core eligibility call; in
plugin/opencode/engram.ts lines 481-488, consume that same core result instead
of evaluating age locally. Keep each adapter limited to parsing input, invoking
the core API/tool, and returning the result.

Source: Path instructions

fi
NOW_EPOCH=$(date "+%s")
ELAPSED=$(( NOW_EPOCH - LAST_EPOCH ))

# Nudge if last save was > 15 minutes ago (900 seconds), but debounce so we do
# not repeat the reminder on every message while the agent has nothing to save.
Expand Down
39 changes: 24 additions & 15 deletions plugin/codex/scripts/user-prompt-submit.sh
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,14 @@ parse_epoch() {
date -j -u -f "%Y-%m-%dT%H:%M:%S" "$Z_TS" "+%s" 2>/dev/null && return 0
fi

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
# No timezone marker at all — these are naive timestamps from sqlite's
# datetime('now'), which is always UTC. Must parse with -u; parsing as
# local time skews the result by the host's UTC offset (e.g. on a UTC-6
# host, session age comes out ~6h in the future, so it's always seen as
# "just started" and the save-nudge can never fire).
date -j -u -f "%Y-%m-%dT%H:%M:%S" "$TS" "+%s" 2>/dev/null \
|| date -j -u -f "%Y-%m-%d %H:%M:%S" "$TS" "+%s" 2>/dev/null \
|| date -u -d "$TS" "+%s" 2>/dev/null
}

# Default: no injection
Expand Down Expand Up @@ -169,20 +174,24 @@ fi

LAST_SAVE_AT=$(echo "$LAST_SAVE_JSON" | jq -r '.[0].created_at // empty' 2>/dev/null)

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

# Parse last save timestamp and compare to now
LAST_EPOCH=$(parse_epoch "$LAST_SAVE_AT")
if [ -z "$LAST_EPOCH" ]; then
echo "$OUTPUT"
exit 0
if [ -z "$LAST_SAVE_AT" ]; then
# No observations exist yet for this project. This is the "never saved"
# case, not "session just started" (session age was already gated to
# >= 5 minutes above) — treat it as maximally stale so the nudge can fire
# and break the cycle where a project with zero observations can never
# reach its first one.
ELAPSED=901
else
# Parse last save timestamp and compare to now
LAST_EPOCH=$(parse_epoch "$LAST_SAVE_AT")
if [ -z "$LAST_EPOCH" ]; then
echo "$OUTPUT"
exit 0
fi
ELAPSED=$(( NOW_EPOCH - LAST_EPOCH ))
fi
NOW_EPOCH=$(date "+%s")
ELAPSED=$(( NOW_EPOCH - LAST_EPOCH ))

# Nudge if last save was > 15 minutes ago (900 seconds), but debounce so we do
# not repeat the reminder on every message while the agent has nothing to save.
Expand Down
13 changes: 8 additions & 5 deletions plugin/opencode/engram.ts
Original file line number Diff line number Diff line change
Expand Up @@ -478,11 +478,14 @@ export const Engram: Plugin = async (ctx) => {
return
}

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

// Only nudge if last save was more than 15 minutes ago
if (nowSecs - lastObsEpoch < 900) return
// No observations exist yet for this project. This is the "never
// saved" case, not "session just started" (session age was already
// gated to >= 5 minutes above) — treat it as maximally stale so the
// nudge can fire and break the cycle where a project with zero
// observations can never reach its first one.
//
// Only nudge if last save was more than 15 minutes ago (or never).
if (lastObsEpoch !== 0 && nowSecs - lastObsEpoch < 900) return
Comment on lines +481 to +488

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Distinguish an invalid observation timestamp from no observations.

lastObsEpoch === 0 also means created_at failed to parse. This change treats that as maximally stale and can nudge immediately after a recent but malformed save. Track whether an observation row exists, and skip the nudge when its timestamp is invalid.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugin/opencode/engram.ts` around lines 481 - 488, Update the
observation-staleness logic around lastObsEpoch to distinguish “no observation
row” from an existing row whose created_at failed to parse. Track whether an
observation exists, and return without nudging when a row exists but its
timestamp is invalid; only treat lastObsEpoch === 0 as maximally stale when no
observation row exists.


// Append the nudge to the last system message
const nudge =
Expand Down