Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Terminal in Editor profile** ([#1683](https://github.com/rust-works/omni-dev/issues/1683)): the VS Code terminal **+ ▾** menu gains an entry that creates a fresh, focused shell terminal as an editor tab using the normal shell and working-directory configuration.

- **Jev route top-two margin close calls** ([#2050](https://github.com/rust-works/omni-dev/issues/2050)): `ai jev route` flags model-class stages when confidence is below `--close-call` or their top-two usable tier probabilities differ by less than `--close-call-margin` (default `0.2`). JSON, YAML, and text share the same flags. Set the margin to `0` to retain confidence-only behavior; effort advice keeps its existing confidence rule. Partial maps without two usable offered probabilities fall back to confidence.
- **Session diagnostics** ([#1447](https://github.com/rust-works/omni-dev/issues/1447)): configurable `daemon.log_level`, registry/feed and subscription diagnostics, VS Code rejection/frame reporting, and opt-in metadata-only `OMNI_DEV_CLAUDE_WRAP_LOG` files make stale or missing session cues diagnosable without persisting conversation content. See [session troubleshooting](docs/sessions-service.md#troubleshooting).
- **`ai jev route` reports which stage supplies the class** ([#2051](https://github.com/rust-works/omni-dev/issues/2051), #1823 step 3): each provider's result gains `class_from` (`design` or `implement`), and text output annotates the class, as `opus (from design)` or `class: <tier> (from implementation)`, for every ladder. `class` is unchanged. A tie goes to `implement`, so `design` means design alone raised the class above what implementation chose. The source is derived from the existing stage answers, so there is no new Jev question or call. See [docs/jev.md](docs/jev.md#route).
- **`ai jev` rejects oversized `choice` and `score` questions locally** ([#1774](https://github.com/rust-works/omni-dev/issues/1774)): `ai jev choice`, `score` and every spec in an `ask` questions file now fail before any request when a `choice` has more than 255 options or a `score` more than 10 levels, with an error naming the question, instead of waiting for an API rejection whose body does not say which question was too big. The caps belong to a model version, so they apply only to `jev-latest` and `jev-1.13`/`jev-1.13.*`; any other `--jev-model` skips them and the minimums still apply to every model. See [docs/jev.md](docs/jev.md#keep-the-state-small-and-relevant).
- **`ai claude skills {status,sync,clean}` accept `-C/--repo`** ([#1781](https://github.com/rust-works/omni-dev/issues/1781), follow-up to [#1778](https://github.com/rust-works/omni-dev/issues/1778)): `-C/--repo <PATH>` now replaces the current directory as the default `sync` source and `status`/`clean` target, and a relative `--source`/`--target` resolves against it, as with `git -C`. Before #1778 the root-global flag parsed but was silently ignored here; since #1778 these commands rejected it. See [docs/user-guide.md](docs/user-guide.md#ai-claude-skills--distribute-skills-across-repositories).
Expand Down
13 changes: 13 additions & 0 deletions docs/adrs/adr-0057.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,3 +250,16 @@ coverage for anything this wrapper does not launch. See [ADR-0039](adr-0039.md)
for the daemon framework, and
[docs/sessions-service.md](../sessions-service.md) for the operator-facing guide
and the wrapper's setup.


## Amendment: opt-in diagnostic metadata (#1447)

Feed 4 keeps conversation content unpersisted. `OMNI_DEV_CLAUDE_WRAP_LOG`
explicitly opts into a user-controlled metadata-only diagnostic file containing
process/session identity, state/report outcomes, and aggregate parser/tee drop
counts. Raw stream lines, messages, tool inputs/results, and daemon error text
are excluded. File I/O runs on an independent bounded writer queue, never on
the byte-forwarding path; failures remain fail-open and shutdown flushing is
bounded. The default remains no diagnostic persistence. See the
[troubleshooting guide](../sessions-service.md#troubleshooting) for enabling,
sharing, and deleting these logs.
29 changes: 29 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -1187,3 +1187,32 @@ Configure one token mode only. Both token keys accept `_FILE` and `_COMMAND`.
Process sources override the selected settings profile; a selected profile does
not inherit the base `env`. See [Atlassian PAT setup and compatibility](user-guide.md#serverdata-center-personal-access-tokens)
for login, verification, mode switching, context-path examples and limitations.

## Daemon and MCP tracing settings

The per-user `$HOME/.omni-dev/settings.json` file accepts tracing defaults
independently of project configuration:

```json
{
"daemon": { "log_level": "info,omni_dev::sessions=debug,omni_dev::daemon=debug" },
"mcp": { "log_level": "warn" }
}
```

For `omni-dev daemon run`, the first valid filter wins: process `RUST_LOG`,
`daemon.log_level`, then `info`. Directives can be plain levels (`debug`) or
comma-separated module filters. Invalid directives fall through to the next
source with a startup warning; malformed/unreadable settings warn and use
built-in defaults. A missing file or missing `daemon` block is normal.
An empty directive is a valid filter that enables no events.

The daemon reads this file at startup, including launchd/socket-activated and
systemd launches that do not inherit a shell's environment. Restart the daemon
after changing it (`omni-dev daemon restart`). Other CLI commands retain
`RUST_LOG` → `warn` and do not use `daemon.log_level`.

`mcp.log_level` configures the separate MCP server (valid `RUST_LOG` takes
precedence there too); it does not control the daemon or hook subprocesses.
See [session troubleshooting](sessions-service.md#troubleshooting) for targeted
filters, the wrapper's opt-in metadata file, and where to read the logs.
139 changes: 139 additions & 0 deletions docs/sessions-service.md
Original file line number Diff line number Diff line change
Expand Up @@ -912,3 +912,142 @@ always includes it.
- **Windows** support waits on the broader daemon Windows work (#1363); the hook
sink and transcript scheme are already portable, only the socket transport is
Unix-only.


## Troubleshooting

Session diagnostics let you follow a sighting through ingestion, the registry,
the daemon subscription, and the VS Code companion. They preserve existing
state and attribution rules; an attribution mismatch remains a separate bug
to investigate using the evidence.

### Enable and read daemon diagnostics

Set `daemon.log_level` in `$HOME/.omni-dev/settings.json`:

```json
{
"daemon": {
"log_level": "info,omni_dev::sessions=debug,omni_dev::daemon=debug"
}
}
```

Restart the daemon, then follow its log:

```bash
omni-dev daemon restart
omni-dev daemon logs --follow
```

The log reader supports foreground/background file-log launches and launchd;
a foreground `daemon run` emits tracing to stderr. For a systemd user unit,
read the journal with `journalctl --user -u omni-dev.service -f` instead.
See [configuration](configuration.md#daemon-and-mcp-tracing-settings) for
precedence and fallback behavior. `RUST_LOG` in the daemon process overrides
the settings filter; shell exports are not inherited by socket activation.

Use `omni_dev::daemon::server=trace` to see each subscription sample's
`change_notification` or `periodic_tick` trigger and `pushed` versus
`suppressed_identical` result. Initial snapshots and terminal counts are
accounted for too. A subscription ends with a distinct `ClientCancel`,
`ClientEof`, `ReadDecodeError`, `DaemonShutdown`, or `WriteFailure` reason.
Cancellation and EOF are normal debug events; failed writes and service
rejections are warnings visible at the default `info` level.

Registry `session_observed` records include identity, agent/PID/event,
old/new states, an outcome, reap count, and the actual `bumped` decision.
`heartbeat_only` refreshes liveness without triggering a change notification;
`replaced_pid_ignored` explains a late event from the process an in-place resume
replaced. `ended_passive_ignored` prevents passive sightings reviving ended
sessions. `session_end` distinguishes unknown, already ended, ignored, and
newly ended sessions. Window reports and unregisters expose their bump decision.

Positive TTL reap counts and capacity evictions explain disappearing entries.
At trace level, individual reaps identify `session_ttl`, `ended_ttl`, or
`window_ttl`; `session_process_ended` identifies PID-based cleanup. Busy
stream-wrapper sessions keep alive, while idle ones can age out. A
`session_attribution_miss` reports cwd and the live-window count; trace-level
candidate folders help reveal lexical mismatches such as `/tmp` versus
`/private/tmp`. Matching still uses the existing folder specificity,
registration time, and key ordering, without canonicalization.

Feed 2 emits one scan summary containing candidate/sighting and skip/error
counts. A blocking scan failure warns that tracking state was reset. Repository
enrichment task failures warn separately from a normal nonrepository cwd.

### Hook diagnostics

Hook sinks are short-lived CLI processes; daemon settings do not set their
filter. To investigate hooks, run the configured hook command with
`RUST_LOG=omni_dev::cli::sessions=debug` in its own environment. Diagnostics go
to stderr, never stdout, and do not change exit 0 or the report timeout.
`session_hook_skipped` identifies read/JSON/identity/event/socket gates;
`session_hook_report` distinguishes delivery, timeout, transport failure, and
daemon rejection. Notification diagnostics contain the classification and
presence of type/message fields, never the raw notification text. Claude and
Codex sightings are tagged separately by the shared hook sink.

### Opt-in wrapper metadata file

For the Claude stream wrapper, set `OMNI_DEV_CLAUDE_WRAP_LOG` to an absolute
file path in the environment of the process launching the wrapper. For example,
when launching a fresh VS Code process from a shell:

```bash
OMNI_DEV_CLAUDE_WRAP_LOG="$HOME/claude-wrap-diagnostics.jsonl" code .
```

An already running VS Code process may retain its earlier environment; fully
quit/relaunch it and start a new wrapped Claude process to apply the change.
The variable does not enable or install the wrapper by itself: use the Feed 4
installation instructions above first.

The file contains newline-delimited metadata: process start/exit, learned
session identity/cwd/model, state reports, report outcome codes, and periodic
plus final cumulative diagnostic summaries. `tee_full` identifies dropped
observer lines when a slow daemon backs up the bounded tee; `tee_closed`,
`tee_oversize`, and `tee_non_utf8` distinguish other drops. Tracker counters
identify parse failure, unknown control shapes, missing permission IDs, and
permission-cap drops. Known unrelated control requests are ignored normally.
The summary also counts drops from the independent diagnostic queue.

When unset or empty, no file, writer thread, tee-counter allocation, or
diagnostic formatting is created. The pure tracker still counts protocol drift.
When enabled, a separate thread appends records through a bounded nonblocking
queue; byte pumps only update counters. New files use `0600`; symlinks and
nonregular targets are refused, and existing file permissions are left intact.
Open/write failures silently disable writing without preventing Claude from
launching. Shutdown waits at most 200 ms for diagnostics; a killed process,
stuck disk, or full queue can lose records. Files append across processes and
include timestamps, wrapper PID, and session/child PID metadata where known;
there is no automatic rotation.

Conversation messages, tool inputs/results, raw stdio/hook payloads, and daemon
error text are never written to this wrapper file. Paths and identifiers are
still personal metadata: inspect logs before sharing them. Remove the variable
and launch new wrapper processes to disable logging; delete the file when the
investigation is finished. This is an explicit metadata-only persistence
exception to Feed 4's normal no-persistence rule.

### VS Code output and tracing a stale cue

Open **View → Output → omni-dev**. One-shot requests now report daemon
rejections as well as transport failures. A rejected/unreachable session-window
report includes the window key; invalid subscription frames report a reason
without dumping the frame, preserving the last valid snapshot. A rejected
subscription continues to use the existing unsupported/fallback behavior.

Compare evidence in this order:

1. Did the hook/watcher/wrapper deliver a sighting, or record a gate/drop?
2. Did `session_observed` accept it, change state/metadata, and bump?
3. Did the subscription wake and push, or suppress an identical snapshot?
4. Was the window report accepted and cwd matched to a live window?
5. Did the companion reject the frame or report a connection problem?
6. Did TTL/PID cleanup or capacity eviction remove the session afterward?

Sessions outside tracked worktrees legitimately contribute no row count.
The pure tally function remains unchanged and does not log every unmatched
session. Gather logs for the narrow reproduction window; use trace only when
debug cannot identify the failing hop.
15 changes: 5 additions & 10 deletions editors/vscode/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
treeEnvelope,
unregisterEnvelope,
} from "./socket";
import { sendWithDiagnostics } from "./replyDiagnostics";
import { runGh } from "./gh";
import { PullRequest, parsePrList, prFallbackBadge, prListArgsForRepo } from "./github";
import { countClaudeTabs, countClaudeTerminals } from "./claudeEmbeddings";
Expand Down Expand Up @@ -257,15 +258,9 @@ function registerPayload(): RegisterPayload {
* when the daemon was unreachable. `timeoutMs` overrides the default for a
* long-running op (the `close` execute waits on windows closing).
*/
async function send(envelope: Envelope, timeoutMs?: number): Promise<Reply | undefined> {
try {
return await sendEnvelope(socketPath(), envelope, timeoutMs);
} catch (err) {
output?.appendLine(
`${envelope.op} skipped: ${err instanceof Error ? err.message : String(err)}`,
);
return undefined;
}
async function send(envelope: Envelope, timeoutMs?: number, context?: string): Promise<Reply | undefined> {
return sendWithDiagnostics(envelope, () => sendEnvelope(socketPath(), envelope, timeoutMs),
(message) => output?.appendLine(message), context);
}

/**
Expand Down Expand Up @@ -486,7 +481,7 @@ function claudeEmbeddings(): { tabs: number; terminals: number } {
async function reportSessionWindow(): Promise<void> {
const folders = (vscode.workspace.workspaceFolders ?? []).map((f) => f.uri.fsPath);
const { tabs, terminals } = claudeEmbeddings();
await send(sessionWindowEnvelope({ key: windowKey, folders, tabs, terminals }));
await send(sessionWindowEnvelope({ key: windowKey, folders, tabs, terminals }), undefined, `window=${windowKey}`);
}

async function heartbeat(): Promise<void> {
Expand Down
25 changes: 25 additions & 0 deletions editors/vscode/src/replyDiagnostics.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { sendWithDiagnostics } from "./replyDiagnostics";

test("request diagnostics preserve rejection and include window context once", async () => {
const lines: string[] = [];
const reply = { ok: false, error: "invalid window" };
const received = await sendWithDiagnostics(
{ service: "sessions", op: "window", payload: {} },
async () => reply, (line) => lines.push(line), "window=abc",
);
assert.equal(received, reply);
assert.deepEqual(lines, ["sessions/window (window=abc) rejected: invalid window"]);
});

test("request diagnostics swallow transport throws and leave success silent", async () => {
const lines: string[] = [];
const env = { service: "sessions", op: "list", payload: {} };
assert.equal(await sendWithDiagnostics(env, async () => { throw new Error("offline"); },
(line) => lines.push(line)), undefined);
assert.equal(lines.length, 1);
const reply = { ok: true, payload: {} };
assert.equal(await sendWithDiagnostics(env, async () => reply, (line) => lines.push(line)), reply);
assert.equal(lines.length, 1);
});
21 changes: 21 additions & 0 deletions editors/vscode/src/replyDiagnostics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Envelope, Reply } from "./socket";

/** Preserve fail-open replies while making every rejection visible in output. */
export async function sendWithDiagnostics(
envelope: Envelope,
request: () => Promise<Reply>,
log: (message: string) => void,
context?: string,
): Promise<Reply | undefined> {
const label = `${envelope.service ?? "daemon"}/${envelope.op}${context ? ` (${context})` : ""}`;
try {
const reply = await request();
if (!reply.ok) {
log(`${label} rejected: ${reply.error ?? "daemon refused the request"}`);
}
return reply;
} catch (err) {
log(`${label} skipped: ${err instanceof Error ? err.message : String(err)}`);
return undefined;
}
}
31 changes: 31 additions & 0 deletions editors/vscode/src/subscription.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,3 +330,34 @@ test("sessions subscribe: without onUnsupported an error reply is ignored, as be
// behaviour `TreeSubscription` still relies on.
assert.equal(received[0].sessions[0].session_id, "s1");
});


test("sessions subscribe: invalid frames report errors and retain the last valid snapshot", async (t: TestContext) => {
const socketPath = tempSocketPath();
const srv = trackingServer((conn) => {
conn.on("data", () => {
for (const frame of ["null", "{}", '{"ok":true,"payload":{}}',
sessionsLine([]), '{"ok":true,"payload":{"wrong":[]}}',
'{"ok":false,"error":"refused"}', "not json"]) {
conn.write(frame.endsWith("\n") ? frame : frame + "\n");
}
});
});
await srv.listen(socketPath);
const received: SessionsSnapshot[] = [];
const errors: string[] = [];
const statuses: boolean[] = [];
const sub = new SessionsSubscription(socketPath, {
onSnapshot: (snapshot) => received.push(snapshot),
onError: (message) => errors.push(message),
onStatus: (status) => statuses.push(status),
});
t.after(() => { sub.close(); srv.close(); });
sub.start();
await waitFor(() => errors.length === 6);
assert.equal(received.length, 1);
assert.deepEqual(statuses, [true]);
assert.match(errors[0], /invalid snapshot envelope/);
assert.match(errors[2], /invalid snapshot payload/);
assert.equal(errors[4], "refused");
});
12 changes: 12 additions & 0 deletions editors/vscode/src/subscription.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,10 @@ export class DaemonSubscription<T> {
this.onError?.(`malformed snapshot: ${err instanceof Error ? err.message : String(err)}`);
return;
}
if (!reply || typeof reply !== "object" || typeof reply.ok !== "boolean") {
this.onError?.("invalid snapshot envelope");
return;
}
// An explicit error reply means the daemon is up but will not serve this op
// — almost always a daemon too old to know it. Hand that to the caller so it
// can degrade, and stop: reconnecting would only re-earn the same refusal.
Expand All @@ -200,6 +204,14 @@ export class DaemonSubscription<T> {
this.onUnsupported(message);
return;
}
if (!reply.ok) {
this.onError?.(reply.error ?? "daemon refused the subscription");
return;
}
if (!reply.payload || typeof reply.payload !== "object" || !this.isSnapshot(reply.payload)) {
this.onError?.("invalid snapshot payload for this subscription");
return;
}
// Ignore anything that is not a well-formed snapshot for this stream; a
// fresh snapshot is the only frame the stream should carry.
if (reply.ok && reply.payload && this.isSnapshot(reply.payload)) {
Expand Down
Loading
Loading