feat(session-cursor): session-namespaced cursor store for /activity adapters (v1.4.0) #30
Reference in New Issue
Block a user
Delete Branch "feat/session-cursor-module"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Part 1 of 2 for the concurrent-session hardening (RFC: internal#726; resolves the secondary item of molecule-mcp-claude-channel#26). Adaptor PR follows once this is released as v1.4.0.
What
New subpath export
@molecule-ai/mcp-server/session-cursor: a session-namespaced, durablesince_idcursor store for/activity-polling adapters — the same roleinbox-uploads/targetsplay as shared cross-adapter contract surfaces.Surface:
CursorStore—load()(tolerant of missing/corrupt; optionalonLoadError),get/has/set/delete/entries/size,save()(atomic temp+rename, mode 0600, PID-suffixed temp),unlink().cursorFileName(sessionKey?)—cursor.json(primary, no key) /cursor.<key>.json(secondary).parseSessionKey(fileName)— inverse; null for primary/unrelated.pruneOrphanCursors(stateDir, isAlive)— removes dead per-session files; never touches the primary or files whose liveness probe throws.Why
A host can run >1 adapter session (two
claudeinvocations both loading the plugin). The platform is fully concurrent —register/heartbeatare workspace-keyed last-writer-wins andGET /activityis read-only with a client-drivensince_id(verified inmolecule-coreregistry.go:393-407,569-595/activity.go:302-619). The only thing that races between sessions is a shared cursor file. Keying the cursor file by session removes that race. Extracting it here (vs. inline in the channel) means hermes-ts / codex-ts inherit the fix instead of re-implementing it.Tradeoff
The store is intentionally logging-agnostic:
load()swallows corruption andsave()throws, so each adapter keeps its own stderr/pino phrasing and decides whether a failed tick is fatal. (The channel will wrap both to preserve its existing messages.)Test plan / verification
src/__tests__/session-cursor.test.ts: 16 tests — filename mapping +cursorFileName↔parseSessionKeyround-trip, tolerant load (missing/corrupt→onLoadError), non-string/empty value filtering, set/save/reload + delete round-trip, atomic-write (no.tmp.lingers),unlinkidempotence, andpruneOrphanCursors(removes dead only; keeps primary + live + unrelated; never deletes on a throwing probe; tolerates missing dir).npm run build(tsc) clean;npm test= 199 passed, 1 skipped, 0 failed (8 suites).Rollback
git revert. Additive only — new subpath export; existing 1.3.x consumers unaffected.Release
After merge: tag
v1.4.0→publish.ymlpublishes to the Gitea npm registry, then the adaptor PR bumps its dep.🤖 Generated with Claude Code
Adversarial review (2 dispatched reviewers — correctness/edge-cases + API/SSOT/security)
No blocking findings; both recommend merge. Verified to hold under active probing: filename round-trip (
parseSessionKey(cursorFileName(k))===k), primary never misclassified,.tmp.<pid>files never matched by prune, same-process primary+secondary temp paths can't collide,load()corruption-tolerance + non-string filtering,isAlive-throws-fails-safe, path-traversal blocked by the[A-Za-z0-9_-]+whitelist, anddist/ships innpm pack(publish builds via tsc).Optional/Nit (non-blocking, not addressed here — tracked for follow-up):
set("")is accepted in-memory but dropped byload()'slength>0filter (latent; activity ids are never empty).save()overwrites a good file with{}(correct-by-design for "corrupt→re-seed"; only matters if a caller saves before load — none do).trySave(onError?)convenience would save each future adapter re-implementing the channel's try/catch wrapper — nice SSOT win, deferred.Reviewed as core owner of the base MCP. session-cursor matches the inbox-uploads/targets cross-adapter pattern; filename round-trip, prune fail-safe (never deletes primary/live/.tmp), atomic temp+rename, and path-traversal whitelist all verified. Additive v1.4.0, publish builds dist via tsc. Optionals (empty-string set asymmetry, trySave convenience) are non-blocking follow-ups. Approve.
Reviewed as owner of the TS adapters that will consume this. API is sufficient for the channel and future hermes-ts/codex-ts; the since_secs cold-start boundary correctly stays in the consumer. SSOT placement is right. Approve.