feat(hooks): postHookActions mechanism, blockSessionSave/sessionSaveContent, and slug-generation hardening#38161
Conversation
Greptile SummaryThis PR introduces the Confidence Score: 5/5Safe to merge; only two minor P2 findings. No P0 or P1 issues found. The core postHookActions mechanism, fail-safe write pattern, TOCTOU ownership checks, and CWE-834 snapshot guard are all correctly implemented and well-tested. The two findings are P2 only: a stale '4-char' comment in the test and a PR-description claim about HHMMSS that the code doesn't actually implement (collision avoidance is fully addressed by the 8-char suffix regardless). No files require special attention. Prompt To Fix All With AIThis is a comment left during a code review.
Path: src/hooks/bundled/session-memory/handler.test.ts
Line: 73-77
Comment:
**Stale comment says "4-char" but suffix is 8 chars**
The comment on line 73 reads "4-char hex random suffix" while the assertion on line 77 tests for exactly 8 hex characters (`[0-9a-f]{8}`), which matches the implementation (`slice(0, 8)`). The comment was not updated when the suffix was widened from 4 to 8 chars.
```suggestion
// Filename includes an 8-char hex random suffix on all paths
// (LLM and fallback) to guarantee uniqueness; the timestamp portion
// is what we assert here.
```
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: src/hooks/bundled/session-memory/handler.ts
Line: 83-86
Comment:
**PR description claims HHMMSS fallback slug but code still produces HHMM**
The PR description states: *"Collision-resistant fallback slug — `HHMMSS` timestamp (previously `HHMM`) for the no-LLM path."* However, `timeSlug` is still built as `${hour}${minute}` (4 digits, HHMM), and the test regex `2330` confirms this. The `second` variable is computed (line 75) but not included in the slug. Since the 8-char unique suffix provides all necessary collision avoidance, this doesn't affect correctness — but the PR description's claim is inaccurate and `second` goes unused.
```suggestion
timeSlug: `${hour}${minute}${second}`,
```
How can I resolve this? If you propose a fix, please make it concise.Reviews (39): Last reviewed commit: "fix(session-memory): address gpt-5.5 dee..." | Re-trigger Greptile |
|
@greptile-apps: Please re-score after minor fixes! |
|
@greptile-apps: resolved minor issue, please re-score |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8b87538531
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@greptile-apps: Please re-score, fixes added |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d022dd707b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@greptile-apps Please rescore. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9670dd0923
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@greptile-apps Please rescore. |
1 similar comment
|
@greptile-apps Please rescore. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7295c0d2e7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@greptile-apps: We have re-thought this feature and believe that we need a slightly more powerful hooks feature: postHookActions. A generic deferred-execution mechanism on InternalHookEvent. Any handler can push async callbacks onto event.postHookActions. After all handlers in the FIFO loop have completed, triggerInternalHook drains the queue sequentially. This is not session-memory-specific. Any hook that needs "act after everyone has spoken" semantics can use it. Design constraint: fail-safe: The critical invariant: if anything goes wrong in the hooks system, session memory must still be written. Data preservation beats policy enforcement. This means the session-memory handler cannot defer its write to postHookActions — if the drain never runs (bug, exception, crash), the file would never exist. Instead:
Pre-set flags (set before the handler runs by an upstream hook) still work inline — the handler checks blockSessionSave before writing and uses sessionSaveContent directly. The post-hook action is a second pass that catches late-set flags. Please consider this design change and re-score! |
|
Excellent design! The The Issue: When both // In the post-hook action:
if (event.context.blockSessionSave === true && writtenEntry !== null) {
// delete and return
}
// This runs even if blockSessionSave is true (but writtenEntry was null)
if (typeof postContent === "string" && postContent !== writtenEntry) {
await writeFileWithinRoot(...); // ❌ Creates file despite blockSessionSave!
}Trace:
Fix: The sessionSaveContent logic should respect blockSessionSave: const postContent = event.context.sessionSaveContent;
if (
event.context.blockSessionSave !== true && // ✅ Add this check
typeof postContent === "string" &&
postContent !== writtenEntry
) {
await writeFileWithinRoot({
rootDir: memoryDir,
relativePath: filename,
data: postContent,
encoding: "utf-8",
});
log.debug("Session save content replaced by post-hook (sessionSaveContent)", {
length: postContent.length,
});
}Missing Test Coverage: None of the tests set both flags. Add: it("blockSessionSave takes precedence over sessionSaveContent", async () => {
// Test both pre-set and one late-set scenarios
});Otherwise: Implementation is solid, tests are comprehensive, and the fail-safe guarantees are preserved. This is a strong architecture improvement. |
|
@darfaz Tagging you in the split. We've improved the architecture considerably while positing a powerful new hooks mechanism: postHookActions. |
|
@greptile-apps: fixed blockSessionSave semantics and added test; please re-score! |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ae220e17bc
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@greptile-apps Please rescore. |
Resolved 2 conflicts in src/hooks/bundled/session-memory/handler.ts: - Adopted mainline's localTimestamp.timeSlug helper (replaces our inline timestamp computation) - Kept our blockPreSet/hasCustomContent dispatch but adopted mainline's improved heading format with timeZoneSuffix
…stHookActions drainPostHookActions has used the snapshot+clear pattern since the postHookActions feature was extracted into a shared helper, so the self-append DoS surface flagged by Aisle on PR openclaw#38162 (CWE-834, unbounded for..of over a live array) is already not present on this branch. This commit adds explicit regression coverage so the boundary is exercised by tests rather than left implicit: - 'prevents self-appending action loops by snapshotting before draining (CWE-834 guard)' — pushes a self-appending action and verifies the drain runs exactly once and the re-pushed action is queued for a subsequent triggerInternalHook call (not extended into the current cycle). Has a SAFETY_CAP of 1000 iterations so a broken drainer fails the assertion rather than hanging the test runner. - 'snapshots actions registered after the drain begins (multi-handler stacking)' — verifies actions added by an action during drain are queued for a future cycle, not extended into the current one. Also: the 'uses local timezone date and fallback time' test asserted the pre-uniqueness-suffix filename format. Updated to match the current format that includes a 4-char hex random suffix on all paths (LLM and fallback) — the production behaviour was already correct, the test was lagging.
Addresses Codex P2 review on PR openclaw#38161 (id=3037871646): the 4-char (16-bit, ~65k possibilities) random suffix was inadequate for the rapid-/new + multi-channel workloads this collision-prevention is meant to protect against. By the birthday paradox, collision probability for 256 same-day same-slug saves was already ~50% — 'collisions remain plausible in real workloads where many saves share the same date+slug,' as the reviewer noted. Switch to 8 hex chars (32 bits = ~4.3B possibilities). Realistic same-day same-slug saves (a few dozen at most) now have birthday-paradox collision probability < 1 in 1e8. The change is contained to one line in the suffix derivation; comment updated to explain the entropy budget calculation. Test updated: the 'uses local timezone date and fallback time' assertion regex changed from {4} to {8} hex chars to match the new filename format. Note: Codex P2 (id=3083160656) flagging hardcoded 15s slug-generation timeout is already addressed in the current code — line 61 of llm-slug-generator.ts uses resolveSlugGeneratorTimeoutMs(params.cfg) which respects agents.defaults.timeoutSeconds. The 15_000 literal at line 22 is now just the DEFAULT_SLUG_GENERATOR_TIMEOUT_MS constant used as the fallback value when no config is set.
Two Codex review fixes for PR openclaw#38162: 1. Codex P2 (id=3128555543): Guard content blocks before reading .type in the inline transcript parser. The inlined private getRecentSessionContent in handler.ts reads c.type directly on every array item, so a null/undefined or non-object entry before a valid text block throws a TypeError, which the per-line catch swallows, dropping the entire message from the memory transcript. The shared transcript.ts helper handled this defensively; the inlined copy did not. Add typeof-object guard to the .find() predicate. 2. Codex P2 (id=3037871646 on PR openclaw#38161): Increase filename suffix entropy from 16 to 32 bits. The 4-char (~65k possibility) suffix gave ~50% birthday-paradox collision probability for 256 same-day same-slug saves — inadequate for the rapid-/new + multi-channel workloads this is meant to protect against. 8 hex chars (~4.3B possibilities) drops collision probability for realistic same-day same-slug saves to < 1 in 1e8. New tests: - 'survives malformed content blocks (null/non-object entries) without dropping the message': pushes content arrays containing null and a raw string before the text block; verifies both user and assistant messages still make it through to the memory transcript. Existing 'uses local timezone date and fallback time' test regex updated from {4} to {8} hex chars to match the new filename format. Tests: 78/78 pass.
…okActions docs Address remaining clawsweeper concerns on PR openclaw#38161 about public hook- runtime surface, premature save logging, and late-blockSessionSave privacy semantics: 1. **Public-surface docs on InternalHookEvent.postHookActions** The previous one-line JSDoc was inadequate for a public hook-runtime surface change. Replaced with comprehensive docs covering: when to use the field, snapshot+clear drain semantics (CWE-834 protection), per-action error isolation, that the field is optional/additive (existing handlers continue to work unchanged), and the explicit contract that actions pushed during drain are queued for the next cycle, NOT extended into the current one. Plugin authors and maintainers reviewing API compatibility now have the full contract in one place. 2. **Disambiguate inline-save log lines from final disposition** The previous 'Memory file written successfully' / 'Session context saved to' log lines would emit before the post-hook drain decided whether to retract or replace the file, leaving audit trails that misleadingly suggested completion when the final file might be different (or absent). Renamed to make the staging vs final state explicit: - debug: 'Memory file written inline (pre-post-hook, may be retracted/replaced)' - info: 'Session context staged at <path> (final disposition decided by post-hook drain)' Operators grepping for save success should filter on the post-drain 'Session save retracted' / 'replaced' lines instead. 3. **Louder privacy warning on late blockSessionSave** The previous warning communicated 'data may have been sent' but did not provide the byte count or correlate to operator audit logs. Tightened to: - Use log level WARN with a 'PRIVACY:' prefix for grep-ability - Include transcript byte count so operators can size the egress - State explicitly that retraction does NOT un-send the data from the slug-generation provider's logs / training pipeline - Re-emphasize the mitigation (set blockSessionSave BEFORE the handler runs) The warning now matches the privacy posture clawsweeper asked for: loud, specific, actionable. Tests: 70/70 still pass. The existing 'blockSessionSave (late-set) retracts memory file via postHookActions' test exercises this code path.
|
@clawsweeper — addressed the open maintainer-level questions you flagged on this PR. Recent commits: bd14845 — public-surface docs, log clarity, privacy posture
e812c0f — filename suffix entropy (already pushed earlier today)
6f63e10 — CWE-834 self-append guard tests (also already pushed) Explicit regression tests for the snapshot+clear drain pattern, including a self-appending action test (with safety cap to prevent test runner hang) and a multi-handler stacking test. On your split-the-PR suggestion — I'd defer to maintainer preference. The three logical pieces (slug-generation hardening with On testing readiness — local Ready for fresh review. |
…e docs Address remaining clawsweeper concerns on PR openclaw#38162: 1. **Public hook-runtime surface (postHookActions on InternalHookEvent).** The PR adds postHookActions to InternalHookEvent which is re-exported through src/plugin-sdk/hook-runtime.ts. clawsweeper flagged that bounded drain behavior + compatibility for manually-constructed events need to be settled. The drainer already does ??= [] before iteration (so manually-constructed legacy events without the field work fine), but lacked an explicit regression test. Added: - 'tolerates manually-constructed events that omit postHookActions (SDK compat)' — builds a hand-rolled event without the postHookActions field and triggers it. Verifies the drain doesn't throw, the registered handler runs, and no crash. Plus replaced the previous one-line JSDoc on InternalHookEvent's postHookActions field with comprehensive docs covering when to use it, snapshot+clear drain semantics with explicit CWE-834 callout, per-action error isolation, optional/additive nature, and the contract that actions pushed during drain are queued for the next cycle (not extended into the current one). Plugin authors and maintainers reviewing API compatibility now have the full contract in one place. 2. **Late-blockSessionSave privacy semantics.** Mirror the privacy warning improvement from openclaw#38161 onto this branch: when a late hook sets blockSessionSave AND the transcript was already loaded for slug generation, emit a WARN-level log with PRIVACY: prefix that includes the transcript byte count, the file path being retracted, and explicit acknowledgement that retraction does NOT un-send the data from the slug-generation provider's logs / training pipeline. 3. **Premature save logging.** Renamed inline log lines to make staging vs final disposition explicit: - debug: 'Memory file written inline (pre-post-hook, may be retracted/replaced)' - info: 'Session context staged at <path> (final disposition decided by post-hook drain)' Both redirect and non-redirect paths get the same treatment so operators grepping for save success filter on post-drain lines. Tests: 76/76 pass (was 75; +1 for SDK compat).
…ory-block-content
gpt-5.5 deep review on PR openclaw#38161 identified three P2 items. None are blocking, but all three are quick to land and improve operability / documentation: P2-1 — Privacy boundary on late-set sessionSaveContent When sessionSaveContent is set by a hook AFTER session-memory has already run, the LLM-generated slug from the original transcript is baked into the filename. The post-hook callback rewrites the file CONTENT but cannot change the filename, so the conversation topic may still be inferable from the on-disk filename. This is a design limitation, not a bug — but plugin authors need to know about it. Added a PRIVACY NOTE in the handler explaining when to use blockSessionSave vs. pre-set sessionSaveContent vs. late-set. P2-2 — Final-disposition log level mismatch log.info "Session context staged at …" fires before post-hook actions. After the drain, the actual outcome (retraction, content replacement) was logged at log.debug, invisible to operators filtering at INFO level. Promoted to log.info on the four disposition messages: * "Session save retracted by post-hook — pre-existing file restored after external deletion" * "Session save retracted by post-hook — pre-existing file restored" * "Session save retracted by post-hook (blockSessionSave)" * "Session save content replaced by post-hook (sessionSaveContent)" The "Session save retraction skipped — file already removed" message stays at debug because it's informational, not a final disposition. P2-3 — Documentation not updated for new features docs/automation/hooks.md still described the old YYYY-MM-DD-slug.md filename and made no mention of the new session-memory controls. Updated the session-memory details section to document: * The 8-hex-char random suffix on filenames (eliminates the prior silent-overwrite data loss path) * blockSessionSave and sessionSaveContent context fields with precedence rules (block always wins) * Pre-set vs. late-set semantics and the recommended path for privacy-critical suppression * The postHookActions mechanism for plugin authors writing ordering-independent hooks (with snapshot-before-drain CWE-834 guard, per-action try/catch, onError routing) * A worked example (managed hook blocks save for secret: channels) Tests: 77/77 still pass across session-memory, internal-hooks, and boot-md test files. Per gpt-5.5: no P0/P1 issues remain after the merge. Architectural recommendation is land as-is once these P2 polish items are in.
|
@greptile-apps: please re-review, and thank you |
…ory-block-content
…ory-block-content
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a1fa3068b3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // and per-action isolation. | ||
| // Defensive: normalize for direct callers that bypass triggerInternalHook. | ||
| event.postHookActions ??= []; | ||
| event.postHookActions.push(async () => { |
There was a problem hiding this comment.
Run session-memory reconciliation after all post-hook mutations
session-memory enqueues its reconciliation callback during handler execution, but triggerInternalHook drains postHookActions in push order, so this callback will run before post-hook callbacks pushed by later-registered handlers. In flows where a downstream hook sets blockSessionSave or sessionSaveContent inside its own postHookActions callback, the session-memory callback executes too early and misses the final policy, leaving a file that should have been blocked or replaced.
Useful? React with 👍 / 👎.
| // provider for slug generation — but only when the transcript was actually | ||
| // loaded (i.e. no custom content was pre-set). When hasCustomContent is | ||
| // true, transcript loading and LLM calls were skipped entirely. | ||
| if (!hasCustomContent && sessionContent) { |
There was a problem hiding this comment.
Only emit LLM-egress warning when slug generation actually ran
This privacy warning is gated only on sessionContent presence, but slug generation is conditionally skipped when cfg is missing or LLM slugging is disabled/test-gated. In those cases no transcript is sent to a provider, yet the log still claims it was “already sent,” which creates false security/audit alerts and can trigger unnecessary incident response.
Useful? React with 👍 / 👎.
…ory-block-content # Conflicts: # docs/automation/hooks.md # src/hooks/bundled/session-memory/handler.ts
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 93b9d5f74e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // and per-action isolation. | ||
| // Defensive: normalize for direct callers that bypass triggerInternalHook. | ||
| event.postHookActions ??= []; | ||
| event.postHookActions.push(async () => { |
There was a problem hiding this comment.
Queue session-memory post-hook before trigger drain runs
This callback is enqueued inside saveSessionMemoryNow, but the exported handler starts that async function and returns immediately (saveSessionMemoryNow(event) is not awaited). In the normal triggerInternalHook flow, post-hook actions are drained right after handlers return, so the drain can run before this push happens (the callback is only reached after async transcript/file I/O). When that happens, late policy mutations from downstream hooks (blockSessionSave / sessionSaveContent) are never reconciled, so a save that should be retracted or replaced remains on disk.
Useful? React with 👍 / 👎.
|
Closing without merge — superseded by the upstream session-memory rework on
No downstream consumers in our ecosystem depend on this PR's specific API surface, so closing without follow-up. The branch is preserved as the annotated tag |
Summary
Introduces a
postHookActionsdeferred-execution mechanism onInternalHookEventand uses it to add two new context fields —blockSessionSaveandsessionSaveContent— to the bundled session-memory handler. These let upstream hooks suppress or replace what gets written to the memory file, regardless of registration order.Separately hardens the LLM slug-generation path against prompt injection and filename collisions.
Scope note
This PR extends slightly beyond its original scope to include a universal filename uniqueness fix. During code review, analysis of the TOCTOU (time-of-check-to-time-of-use) concerns around the post-hook retraction and content replacement paths revealed that the root cause — potential filename collisions between concurrent sessions — was better addressed at the source than with increasingly complex atomic file operations. By appending a random suffix to all memory filenames (not just the fallback timestamp path), we eliminate the collision prerequisite entirely, which makes the downstream TOCTOU concerns moot and avoids the need for file locking infrastructure. This is a pre-existing data loss bug (silent overwrites) that the new post-hook mechanisms would have inherited, so fixing it here prevents compounding complexity.
Key Changes
postHookActionsmechanism (src/hooks/internal-hooks.ts)postHookActionsarray toInternalHookEventtriggerInternalHookdrains it sequentially after all handlers complete, with per-action error isolationSession-memory persistence control (
src/hooks/bundled/session-memory/handler.ts)blockSessionSave— suppresses memory file creation entirelysessionSaveContent— replaces the default transcript-based entry with custom contentblockSessionSavetakes precedence oversessionSaveContentin both inline and deferred pathsblockSessionSaveis already set before the handler runs, transcript loading and LLM slug generation are skipped entirely — no unnecessary data processing or model callsUniversal filename uniqueness (
src/hooks/bundled/session-memory/handler.ts)2026-03-21-debugging-slack-cache-a7f3.md), applied to both LLM-slug and fallback timestamp pathsSlug-generation hardening (
src/hooks/llm-slug-generator.ts)disableTools: trueon the embedded agent call — prevents prompt injection via attacker-controllable conversation contentHHMMSStimestamp (previouslyHHMM) for the no-LLM pathMinor
.gitignore: adds.openclaw-*/to suppress test workspace directoriesTest Coverage
handler.test.ts: pre-set/late-set block, pre-set/late-set custom content, block+content precedence, blank-marker empty string, fail-safe inline write, andmkdirregressioninternal-hooks.test.ts: ordering, context visibility, per-action error isolation, no-handlers drain, and early-return removal