URL: https://github.com/can1357/oh-my-pi/issues/3073
Content-Type: text/markdown
Method: github-issue
Notes: Fetched via GitHub API

---

# feat(extensions): give SessionStartEvent a start-source discriminator (startup | resume | clear | compact)

**#3073** · open · opened by @ken-jo
Created: 2026-06-19T19:29:29Z · Updated: 2026-06-19T22:53:04Z
Labels: enhancement, agent, sdk, triaged

---

### Description

The extension event `session_start` carries no information about *how* the session began. In `packages/coding-agent/src/extensibility/shared-events.ts:27-30` it is defined as:

```ts
/** Fired on initial session load */
export interface SessionStartEvent {
	type: "session_start";
}
```

Its sibling session events in the same file already expose a discriminator. `SessionBeforeSwitchEvent` and `SessionSwitchEvent` (same file, lines 33-46) both carry `reason: "new" | "resume" | "fork"`, so the asymmetry is visible right in the event definitions: a fresh launch, a `--session` restore, a `/clear`, and a post-compact continuation all surface to extensions as the same opaque `{ type: "session_start" }`.

The docs reflect the same gap — `docs/extensions.md:198` and `docs/hooks.md:84` list `session_start` with no source/reason, unlike the neighboring cancelable/switch events whose payloads are documented.

This was already noted incidentally by @cexll in #2834 (in a Claude/Codex hook-mapping table): *"OMP can start/inject, but `session_start` has no start source like `startup` / `resume` / `clear` / `compact`."* That issue was about main-agent Stop-hook parity (now shipped as `session_stop`/`subagent_stop`), so the start-source observation was never tracked on its own — filing it separately here.

### Use Case

Extensions that need to gate one-time, per-session-start behavior on *how* the session began have no signal to do so today. Concrete examples:

- Inject orientation/priming context only on a brand-new (`startup`) session, and skip it on a `resume` so a restored session is not re-primed.
- Re-seed state after a `compact` continuation differently than on a fresh start.
- Avoid double-running setup when a `/clear` recycles the session.

`before_agent_start` is **per-turn** (it fires on every turn), so it is not a substitute for a once-per-session-start signal with a source. The switch events expose `reason`, but they only fire on explicit switch/fork, not on the common startup/resume/clear/compact paths that surface as `session_start`.

Why this matters from a cross-platform integrator's perspective: when building an adapter that maps OMP's extension events onto a common hook model shared with other agents (Claude Code surfaces exactly `startup | resume | clear | compact` on its SessionStart), OMP's `session_start` is the only one we cannot map faithfully — we are forced to hardcode `"startup"` for every session start, which is wrong for resume/clear/compact. A first-class source field would let extensions and adapters branch correctly instead of fabricating it.

### Area

Extensions / Plugins

### Proposed Solution

Add a `source` field to `SessionStartEvent`, mirroring the existing switch-`reason` vocabulary and Claude Code's session-start sources:

```ts
/** Fired on initial session load */
export interface SessionStartEvent {
	type: "session_start";
	/** How this session began. */
	source: "startup" | "resume" | "clear" | "compact";
}
```

The path that emits `session_start` already knows whether it is a fresh launch, a `--session`/restore, a `/clear`, or a post-compact continuation, so `source` can be populated there. To preserve backward compatibility for existing extension factories you could make it optional (`source?:`) and/or default to `"startup"`.

Scope is small: one event payload + the single emit site; no change to the loader/runner contract. The docs touch-ups would be `docs/extensions.md:198` and `docs/hooks.md:84`.

I'm happy to send a PR if the field name/vocabulary is agreed — I can match whatever you prefer (reuse the switch `reason` union, or a dedicated `source` union; optional vs required). (Noting the repo's vouch policy for PRs — glad to open a Discussion to get vouched first if this is something you'd take.)

### Alternatives Considered

- **Infer from `before_agent_start`** — rejected: it is per-turn, not per-session-start, and carries no start-source either.
- **Inspect `ctx.sessionManager` state at `session_start`** — fragile; there is no exposed, stable "how did this session begin" signal, and #2834's investigation noted the context object intentionally exposes a limited surface.
- **Reuse the switch `reason` union (`"new" | "resume" | "fork"`)** — viable and would keep vocabulary consistent, but it does not cover `clear`/`compact` starts; a dedicated source union (or a superset) maps better to the real start paths. Open to either.

---

## Comments (3)

### @roboomp · 2026-06-19T19:32:05Z

Triaged as `enhancement`. The gap is real but narrower than the body implies, and the vocabulary picks aren't load-bearing — flagging the shape decisions a maintainer needs to make before this lands.

## What actually fires today

Of the four sources you list, only two currently route through `session_start`:

- `startup` (fresh launch) and `--session` resume — both emit `{ type: "session_start" }` from the three init sites (`modes/runtime-init.ts:141`, `modes/controllers/extension-ui-controller.ts:249`, `modes/acp/acp-agent.ts:2209`, plus `task/executor.ts:2206`).
- `/clear` runs through `AgentSession.newSession` (`session/agent-session.ts:6698`), which emits `session_before_switch` / `session_switch` with `reason: "new"` — **not** `session_start`.
- `/compact` emits `session_before_compact` / `session_compact`, not `session_start`.

So an adapter mapping OMP → Claude Code's `startup | resume | clear | compact` doesn't actually have to fabricate the last two — they have dedicated events already. The one thing it genuinely can't tell apart is `startup` vs `resume`, both of which arrive as the same opaque `{ type: "session_start" }`.

## Design choices for a maintainer

1. **Source vocabulary.**
   - **(a) Minimal — `source: "startup" | "resume"`** on `SessionStartEvent`. Matches the actual emit paths; extensions wanting `clear`/`compact` keep listening to `session_switch` / `session_compact` (which they already have to, for cancellation).
   - **(b) Maximal — `source: "startup" | "resume" | "clear" | "compact"`** with `session_start` *also* re-emitted after `newSession({reason:"new"})` and `session_compact`. Single subscribe point, but creates two events per `/clear` and `/compact` and forces an ordering decision against `session_switch`/`session_compact`.

   (a) is the smaller, more honest contract; (b) is the friendlier adapter target. I'd lean (a) unless the cross-agent adapter story is the explicit driver.

2. **Field name.** Sibling switch events use `reason: "new" | "resume" | "fork"`. Reusing `reason` keeps the schema consistent, but `"resume"` would mean two slightly different things across the two events (session-load vs switch-target). A separate `source` avoids that overload at the cost of one more synonym in the API. Mild preference for `source`.

3. **Optional vs required.** Existing in-tree consumers (autoresearch in `autoresearch/index.ts:248`, `sdk.ts:919`) ignore the payload, so a required field is source-compatible internally — but third-party extension factories instantiated with structural type-checking off would silently see `undefined`. Optional with documented default `"startup"` is the safer path; flip to required if a major bump is on the table.

4. **Resume detection.** Knowable at the emit site: `sessionManager.buildSessionContext()` already discriminates an existing-data restore from a fresh start (`main.ts:1199-1202`). All four emit sites have access to the session manager, so populating `source` is a one-line lookup, no plumbing.

Happy to take this once a maintainer signs off on (1) and (2); the diff is small but the vocabulary call is theirs. Per the vouch policy, will wait for an explicit go-ahead (or `accepted` label) before sending a PR.

---

### @ken-jo · 2026-06-19T22:52:41Z

You're right — thanks for the detailed triage. Agreed the real gap is just `startup` vs `resume`: both emit `session_start` with nothing to tell them apart, whereas `/clear` and `/compact` already have their own dedicated events, so an adapter doesn't need those folded in. So the narrowed ask is simply a way to distinguish startup from resume on `session_start` — whatever shape you'd prefer. Happy to send a small PR for just that if it's welcome.

---

### @roboomp · 2026-06-19T22:53:04Z

Confirming the narrowed read: yes, the only ambiguity on `session_start` is startup vs resume, and the emit sites have everything they need (`sessionManager.buildSessionContext()` already distinguishes) to populate a discriminator without new plumbing.

I can't authorize the PR myself though — that call is a maintainer's. The two open shape questions (`source` vs reuse `reason`; optional w/ default `"startup"` vs required) still need a sign-off before code goes in, since they're contract decisions. Flagging for a maintainer to confirm scope + pick the shape, then you're clear to send.

---