Skip to content

fix: match session webhook PID to parent when wrapper scripts are used#101

Open
Spirotot wants to merge 1 commit intohappier-dev:devfrom
Spirotot:fix/daemon-pid-parent-matching
Open

fix: match session webhook PID to parent when wrapper scripts are used#101
Spirotot wants to merge 1 commit intohappier-dev:devfrom
Spirotot:fix/daemon-pid-parent-matching

Conversation

@Spirotot
Copy link

Summary

  • When the daemon spawns a session through a wrapper script (e.g. a Node.js entrypoint that spawns the actual binary as a child process), the daemon tracks the wrapper's PID while the session reports the binary's PID via webhook
  • This PID mismatch causes the session to be registered as "externally-started," a ~90-second webhook timeout, and delayed spawn response to the client
  • When an unknown PID reports via webhook, now checks if its parent PID (PPID) matches any daemon-tracked PID
  • If a match is found, re-keys the tracked session to the actual PID and resolves pending awaiters immediately

Implementation

  • Adds getParentPid() helper using ps -o ppid= -p <pid> (Unix only, <10ms, 1s timeout guard)
  • On Windows the PPID check is skipped and existing behavior is preserved
  • Only runs when a PID is NOT in the tracked map (infrequent path)
  • Failures are caught gracefully — worst case falls back to existing "register as externally-started" behavior

Context

Discovered with compiled binary deployments where a Node.js entrypoint wrapper spawns the actual Happier binary. The daemon tracks the wrapper's PID, but the session webhook reports the binary's PID. This causes a PID mismatch that results in a 90-second timeout before the spawn response reaches the client, even though the session itself works fine.

Test plan

  • Verify daemon-spawned sessions are correctly tracked when spawned directly (no wrapper)
  • Verify daemon-spawned sessions via wrapper scripts are re-keyed to the correct PID
  • Verify the ~90-second webhook timeout no longer occurs in the wrapper scenario
  • Verify externally-started sessions (not spawned by daemon) are still registered correctly
  • Verify Windows behavior is unaffected (PPID check skipped)

When the daemon spawns a session through a wrapper script (e.g. a
Node.js entrypoint that spawns the actual binary as a child process),
the daemon tracks the wrapper's PID while the session reports the
binary's PID via webhook. This mismatch causes:

1. The session to be registered as "externally-started" instead of
   correlating with the daemon-spawned entry
2. A ~90-second webhook timeout on the wrapper PID
3. Delayed spawn response to the client

When an unknown PID reports via webhook, check if its parent PID (PPID)
matches any daemon-tracked PID. If found, re-key the tracked session to
the actual PID and resolve pending awaiters immediately.

Uses a lightweight ps call (Unix only, <10ms, 1s timeout guard). On
Windows the check is skipped and existing behavior is preserved.
@coderabbitai
Copy link

coderabbitai bot commented Feb 28, 2026

Warning

Rate limit exceeded

@Spirotot has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 29 minutes and 20 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 1037536 and a6aec5c.

📒 Files selected for processing (1)
  • apps/cli/src/daemon/sessions/onHappySessionWebhook.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 28, 2026

Greptile Summary

Fixes daemon session tracking when wrapper scripts spawn the actual binary. When a session webhook reports an unknown PID, checks if its parent PID (PPID) matches a daemon-tracked session, then re-keys the tracking to the actual process PID and resolves pending awaiters immediately.

  • Added getParentPid() helper using ps -o ppid= (Unix only, Windows returns null)
  • Re-keys tracked session from wrapper PID to child PID when PPID match found
  • Resolves pending awaiters immediately instead of waiting 90s
  • Gracefully falls back to existing "externally-started" behavior on failures

Important: Missing test coverage for the new PPID matching logic. Should add tests to verify re-keying behavior, awaiter resolution, and error handling.

Confidence Score: 4/5

  • safe to merge with recommendation to add test coverage
  • implementation is sound with proper error handling and graceful fallbacks, but lacks test coverage for the new PPID matching logic which is important for this critical daemon functionality
  • no files require special attention - implementation is straightforward and well-documented

Important Files Changed

Filename Overview
apps/cli/src/daemon/sessions/onHappySessionWebhook.ts adds PPID matching to fix wrapper script session tracking, logic is sound but needs test coverage and input validation

Sequence Diagram

sequenceDiagram
    participant Daemon
    participant Wrapper as Wrapper Script<br/>(PID 1000)
    participant Binary as Session Binary<br/>(PID 1001)
    
    Note over Daemon: Tracks session by PID 1000
    Daemon->>Wrapper: spawn wrapper script
    activate Wrapper
    Wrapper->>Binary: spawn actual binary
    activate Binary
    Wrapper-->>Daemon: returns (tracked as PID 1000)
    
    Note over Daemon: Awaiting webhook from PID 1000
    Binary->>Daemon: webhook with PID 1001
    Note over Daemon: PID 1001 not in tracked map!
    
    Daemon->>Daemon: getParentPid(1001) → 1000
    Daemon->>Daemon: find session for PID 1000
    Note over Daemon: Match found! Re-key session
    
    Daemon->>Daemon: delete session[1000]
    Daemon->>Daemon: set session[1001]
    Daemon->>Daemon: resolve awaiter immediately
    deactivate Binary
    deactivate Wrapper
Loading

Last reviewed commit: a6aec5c

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

1 file reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +28 to +31
function getParentPid(pid: number): number | null {
if (process.platform === 'win32') return null;
try {
const stdout = execSync(`ps -o ppid= -p ${pid}`, { encoding: 'utf-8', timeout: 1000 });
Copy link
Contributor

Choose a reason for hiding this comment

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

add input validation to ensure pid is a number before using in shell command

Suggested change
function getParentPid(pid: number): number | null {
if (process.platform === 'win32') return null;
try {
const stdout = execSync(`ps -o ppid= -p ${pid}`, { encoding: 'utf-8', timeout: 1000 });
function getParentPid(pid: number): number | null {
if (process.platform === 'win32') return null;
// Runtime validation for shell command safety
if (!Number.isInteger(pid) || pid <= 0) return null;
try {

Comment on lines +108 to +130
// PID not in tracked map. Check if this is a child of a tracked PID —
// this happens when a wrapper script (e.g. Node.js entrypoint) spawns
// the actual session binary as a child process, causing a PID mismatch
// between what the daemon spawned and what the session reports.
const ppid = getParentPid(pid);
const parentSession = ppid ? pidToTrackedSession.get(ppid) : null;

if (parentSession && parentSession.startedBy === 'daemon') {
// Re-key the tracked session from wrapper PID to actual session PID
pidToTrackedSession.delete(ppid);
parentSession.pid = pid;
parentSession.happySessionId = sessionId;
parentSession.happySessionMetadataFromLocalWebhook = normalizedMetadata;
pidToTrackedSession.set(pid, parentSession);
logger.debug(`[DAEMON RUN] Re-keyed daemon session from wrapper PID ${ppid} to actual PID ${pid}`);

// Resolve any awaiter that was waiting on the wrapper PID
const awaiter = pidToAwaiter.get(ppid);
if (awaiter) {
pidToAwaiter.delete(ppid);
awaiter(parentSession);
logger.debug(`[DAEMON RUN] Resolved session awaiter via parent PID ${ppid}`);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

add test coverage for the parent PID matching logic - should verify:

  • child process correctly re-keyed when PPID matches daemon-spawned session
  • awaiter resolved when parent PID match found
  • falls back to externally-started when PPID doesn't match
  • handles getParentPid returning null gracefully

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant