Skip to content

agent-stream: start with what every session is doing now - #67

Open
asubbarao wants to merge 6 commits into
mainfrom
agent-stream-now-first
Open

asubbarao wants to merge 6 commits into
mainfrom
agent-stream-now-first

Conversation

@asubbarao

@asubbarao asubbarao commented Oct 18, 2026 •

Copy link
Copy Markdown
Owner

Adds a first query to the agent-stream skill for "what is every session doing now" (other terminals, Claude Desktop): one row per Claude session active in the last hour, with its titles, branch, the PRs it linked, minutes idle, and the other live sessions that share its branch or one of its PRs, which is where two sessions are about to collide.

Tested by running the block from the file on dev: 4 live sessions, 2 of them on one inf-1656 branch. An earlier run showed 14 sessions, 3 sharing that branch.

🤖 Generated with Claude Code

@asubbarao

Copy link
Copy Markdown
Owner Author

duckstack/rules: success

  • warn sql_json_extract in skills/agent-stream/SKILL.md#2.sql: Read JSON with read_json and its typed columns (dot access on structs); no json_extract on nested fields.
  • warn sql_json_extract in skills/agent-stream/SKILL.md#4.sql: Read JSON with read_json and its typed columns (dot access on structs); no json_extract on nested fields.
  • warn sql_json_extract in skills/agent-stream/SKILL.md#5.sql: Read JSON with read_json and its typed columns (dot access on structs); no json_extract on nested fields.
  • warn sql_json_extract in skills/agent-stream/SKILL.md#6.sql: Read JSON with read_json and its typed columns (dot access on structs); no json_extract on nested fields.

duckstack review queue, rule gate (hooks/hook_checks.sql)

@asubbarao
asubbarao enabled auto-merge October 18, 2026 23:50
@asubbarao

Copy link
Copy Markdown
Owner Author

duckstack/review: error

The reviewer gave no verdict. Its answer, cut at 3000 characters:

Opus 5.5, duckstack review queue (duckstack-review skill)

@asubbarao

Copy link
Copy Markdown
Owner Author

duckstack/rules: success

  • warn sql_json_extract in skills/agent-stream/SKILL.md#2.sql: Read JSON with read_json and its typed columns (dot access on structs); no json_extract on nested fields.
  • warn sql_json_extract in skills/agent-stream/SKILL.md#4.sql: Read JSON with read_json and its typed columns (dot access on structs); no json_extract on nested fields.
  • warn sql_json_extract in skills/agent-stream/SKILL.md#5.sql: Read JSON with read_json and its typed columns (dot access on structs); no json_extract on nested fields.
  • warn sql_json_extract in skills/agent-stream/SKILL.md#6.sql: Read JSON with read_json and its typed columns (dot access on structs); no json_extract on nested fields.

duckstack review queue, rule gate (hooks/hook_checks.sql)

@asubbarao

Copy link
Copy Markdown
Owner Author

duckstack/review: failure

  • skills/agent-stream/SKILL.md:84 Check 1 / RULES.md 3 (a column passed into a reader); the skill's own rule at line 10 (do not re-parse the raw JSONL): read_ndjson(file_path) puts a row's column into a table function, which hits the binder error that self-dispatch exists to avoid, and it re-parses raw JSONL that agent.lines already stores with its message column.
    Rewrite: head is cut. For a message's full text, join the store instead of re-reading the file:
SELECT d.session_id, d.ts, l.message -> 'content' AS content
FROM day d
JOIN agent.lines l ON l.transcript = d.file_path AND (l.timestamp ->> '$')::TIMESTAMPTZ = d.ts;
  • skills/agent-stream/SKILL.md:78 RULES.md 10 (lossless: keep columns, SELECT * EXCLUDE): The query keeps only six columns and drops file_path, tail, block_type and is_prompt, even though line 84 tells the reader to use file_path at that row's timestamp to get the full text, so the follow-up step has no key to start from.
    Rewrite: WITH day AS (
    SELECT *, timestamp AS ts,
    ts > now() - INTERVAL 1 HOUR AS live,
    first_value(timestamp) OVER w AS last_ts,
    row_number() OVER w AS back
    FROM agent.messages
    WHERE timestamp > now() - INTERVAL 24 HOUR AND block_type = 'text' AND NOT is_agent
    AND CASE message_role WHEN 'user' THEN is_prompt ELSE true END
    WINDOW w AS (PARTITION BY session_id ORDER BY timestamp DESC)
    )
    SELECT live, back, * EXCLUDE (live, back)
    FROM day
    ORDER BY live DESC, CASE WHEN live THEN back END, last_ts DESC, ts DESC
    LIMIT 20;

Opus 5.5, duckstack review queue (duckstack-review skill)

@asubbarao

Copy link
Copy Markdown
Owner Author

Closed: the search moves to #68's BM25 index instead of a long skill section. — Opus 5.5

@asubbarao asubbarao closed this Oct 19, 2026
auto-merge was automatically disabled October 19, 2026 00:25

Pull request was closed

@asubbarao
asubbarao deleted the agent-stream-now-first branch October 19, 2026 00:25
@asubbarao asubbarao reopened this Oct 19, 2026
@asubbarao

Copy link
Copy Markdown
Owner Author

duckstack/rules: success

  • warn sql_json_extract in skills/agent-stream/SKILL.md#2.sql: Read JSON with read_json and its typed columns (dot access on structs); no json_extract on nested fields.
  • warn sql_json_extract in skills/agent-stream/SKILL.md#4.sql: Read JSON with read_json and its typed columns (dot access on structs); no json_extract on nested fields.
  • warn sql_json_extract in skills/agent-stream/SKILL.md#5.sql: Read JSON with read_json and its typed columns (dot access on structs); no json_extract on nested fields.
  • warn sql_json_extract in skills/agent-stream/SKILL.md#6.sql: Read JSON with read_json and its typed columns (dot access on structs); no json_extract on nested fields.

duckstack review queue, rule gate (hooks/hook_checks.sql)

@asubbarao
asubbarao enabled auto-merge October 19, 2026 01:07
@asubbarao

Copy link
Copy Markdown
Owner Author

duckstack/review: failure

  • skills/agent-stream/SKILL.md:61 RULES.md 25 (delete wrong advice): The new prose says never to use ORDER BY session, ts and that origin.kind = 'human' replaces the hand-written prefix filters. But the Turns query in the same file still ends with ORDER BY session, ts and still filters prompts by the <, '# AGENTS.md' and 'This session is being continued' prefixes, so the file now contradicts itself.
    Rewrite: In the Turns prompts CTE: WHERE role = 'user' AND NOT is_subagent AND origin_kind = 'human' AND NOT starts_with(ltrim(text), '') -- with origin ->> 'kind' AS origin_kind projected in recent; and end it with ORDER BY ts DESC
  • skills/agent-stream/SKILL.md:81 RULES.md 10 (lossless): LIMIT 20 silently drops sessions in a committed query that is meant to show what every session is doing. The description says 37 sessions over 24 hours, so the cap already cuts real sessions. The 24-hour window already bounds the result.
    Rewrite: FROM texts GROUP BY session_id ORDER BY last_ts DESC;
  • skills/agent-stream/SKILL.md:76 RULES.md 10 / RULES.md 13 (lossless; OR in disguise): CASE role WHEN 'user' THEN … ELSE true END in a WHERE is an OR chain written as a CASE, and it drops rows silently instead of deriving a flag and filtering in the next step.
    Rewrite: texts AS (
    SELECT session_id, ts, role, coalesce(block ->> 'text', block ->> '$') AS text,
    CASE WHEN role = 'assistant' THEN 'reply'
    WHEN origin_kind = 'human' AND NOT starts_with(ltrim(text), '') THEN 'prompt' END AS kind
    FROM blocks WHERE coalesce(block ->> 'type', 'text') = 'text'
    )
    SELECT session_id, max(ts) AS last_ts, arg_max(kind, ts) AS last_kind, arg_max(text, ts) AS last_text, strlen(last_text) AS last_text_bytes
    FROM texts WHERE kind IS NOT NULL GROUP BY session_id ORDER BY last_ts DESC;
  • skills/agent-stream/SKILL.md:68 RULES.md 13 / check 7 (selector-taking extractors): CASE json_type(message -> 'content') … message -> '$.content[*]', together with type ->> '$', timestamp ->> '$' and origin ->> 'kind', is the arrow ladder the rubric shows as its BAD example, and the rule gate flagged it as json_extract.
    Rewrite: Read the typed columns of the shared messages relation (session, ts, role, text, is_prompt) rather than digging into agent.lines' JSON columns in each query: FROM agent.messages WHERE ts > now() - INTERVAL 24 HOUR AND NOT is_agent
  • skills/agent-stream/SKILL.md:65 check 5 / check 9 (missing shared relation, bloat): The new query is the fifth copy of the blocks/messages unnest in this file, and it duplicates the existing 'One row per session' query a few lines below it. The author's reason, not depending on agent.messages and agent.docs: the stream and the memories as plain rows #58, doesn't hold: the description says the result was checked against agent.messages on dev, so that shared relation already exists and is what this query should read.
    Rewrite: SELECT session, max(ts) AS last_ts, arg_max(role, ts) AS last_role, arg_max(text, ts) AS last_text,
    strlen(last_text) AS last_text_bytes
    FROM agent.messages
    WHERE ts > now() - INTERVAL 24 HOUR AND NOT is_agent
    GROUP BY session ORDER BY last_ts DESC;
    -- and delete the duplicate 'One row per session' query, or make it this one

Opus 5.5, duckstack review queue (duckstack-review skill)

@asubbarao

Copy link
Copy Markdown
Owner Author

duckstack/rules: success

  • warn sql_json_extract in skills/agent-stream/SKILL.md#2.sql: Read JSON with read_json and its typed columns (dot access on structs); no json_extract on nested fields.
  • warn sql_json_extract in skills/agent-stream/SKILL.md#4.sql: Read JSON with read_json and its typed columns (dot access on structs); no json_extract on nested fields.
  • warn sql_json_extract in skills/agent-stream/SKILL.md#5.sql: Read JSON with read_json and its typed columns (dot access on structs); no json_extract on nested fields.
  • warn sql_json_extract in skills/agent-stream/SKILL.md#6.sql: Read JSON with read_json and its typed columns (dot access on structs); no json_extract on nested fields.

duckstack review queue, rule gate (hooks/hook_checks.sql)

@asubbarao

Copy link
Copy Markdown
Owner Author

duckstack/review: failure

  • skills/agent-stream/SKILL.md:77 RULES.md 12 / 10 / 5 (results come back as columns): The kind of overlap is encoded by prefixing the string 'branch ', and line 81 then builds shares_with by concatenating session_id, ' ' and that string. Structure turns into text that the reader has to parse by position.
    Rewrite: ), touches AS (
    SELECT session_id, 'branch' AS kind, branch AS value FROM per_session WHERE branch <> 'staging'
    UNION ALL BY NAME
    SELECT session_id, 'pr' AS kind, unnest(prs)::VARCHAR AS value FROM per_session
    ), shared AS (
    SELECT a.session_id, list_distinct(list({'session_id': b.session_id, 'kind': a.kind, 'value': a.value})) AS shares_with
    FROM touches a JOIN touches b ON a.kind = b.kind AND a.value = b.value AND a.session_id <> b.session_id
    GROUP BY a.session_id
    )
  • skills/agent-stream/SKILL.md:71 skill: shorter version / RULES.md 9: live_sessions computes max(ts) per session, and per_session then joins it back and regroups on the same key. A single GROUP BY with HAVING does both, which removes a CTE and a join (6 lines → 4).
    Rewrite: ), per_session AS (
    SELECT session_id, max(ts) AS last_ts, list_distinct(list(title)) AS titles, arg_max(branch, ts) AS branch,
    list_distinct(list(CASE WHEN pr_number IS NOT NULL THEN {'repo': pr_repo, 'number': pr_number} END)) AS prs
    FROM session_lines GROUP BY session_id HAVING max(ts) > now() - INTERVAL 1 HOUR
  • skills/agent-stream/SKILL.md:67 RULES.md 12 (no || chains) / 10 (lossless): The PR key is glued together with a || chain mixed with ->>. Both operators share one precedence level in DuckDB's parser, so this can parse as ((… || prNumber) ->> '$'). It also folds repo and number into one string that a later reader would have to split apart.
    Rewrite: -- session_lines: keep the parts as columns
    prRepository ->> '$' AS pr_repo, prNumber ->> '$' AS pr_number
    -- per_session: one struct per linked PR; list_distinct drops the NULLs
    list_distinct(list(CASE WHEN s.pr_number IS NOT NULL THEN {'repo': s.pr_repo, 'number': s.pr_number} END)) AS prs
  • skills/agent-stream/SKILL.md:68 skill: keep the read bounded / manifesto §9 / skill rule 'One statement covers every session. Filter by a time window, transcript IN (…)': session_lines parses every top-level JSON field of every line of every Claude session in the store, and only drops old sessions later in a HAVING. That is the full transcript scan the rubric says once took dev down. The PR's note (title lines carry no timestamp) explains why there is no filter on line time, but it does not justify skipping a file-level filter: filtering on the transcript file's modified time keeps every title line of a live session.
    Rewrite: WITH live_files AS (
    SELECT path FROM agent.entries
    WHERE modified > now() - INTERVAL 1 HOUR
    AND NOT contains(path, '/subagents/') AND NOT contains(path, '/.codex/')
    ), session_lines AS (
    SELECT sessionId ->> '$' AS session_id, (timestamp ->> '$')::TIMESTAMPTZ AS ts,
    gitBranch ->> '$' AS branch, coalesce(customTitle ->> '$', aiTitle ->> '$') AS title,
    prRepository ->> '$' AS pr_repo, prNumber ->> '$' AS pr_number
    FROM agent.lines l SEMI JOIN live_files f ON f.path = l.transcript
    )

Opus 5.5, duckstack review queue (duckstack-review skill)

asubbarao and others added 6 commits October 19, 2026 09:41
Rows from the last hour come first, round-robin across live sessions, so
concurrent terminal and Desktop sessions show up in the first rows. Older rows
then follow session by session through the day. Tested on dev: the first 10
rows were one per live session.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCnrCZ6kWmagfJjoK8jVGQ
One WINDOW w gives both the session's newest time (first_value) and each row's place back
from it (row_number), replacing max() OVER plus a second window on the same partition.
A boolean `live` replaces the `ordered` CTE's 1/2 tier. On dev the old and new queries
return the same 1,985 rows over 24 hours (EXCEPT ALL both ways is empty); the first 20
rows are the 7 live sessions round-robin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…session

agent.messages comes only from PR #58, so the section now reads agent.lines, which main
defines, and no longer depends on #58 merging first. It returns each session's newest
message with a GROUP BY (max, arg_max), so neither the round-robin row number nor the
per-session newest-time window is needed. On dev the newest message per session matches
agent.messages exactly: 37 sessions over 24 hours, EXCEPT ALL empty both ways.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The five hand-written text prefixes are replaced by the field Claude Code records on each
user line. On 24 hours of dev data every prefix maps to a field: compaction summaries are
isCompactSummary, skill loads isMeta, peer messages origin.kind = 'peer', interruptions
have no origin, and typed, queued and SDK prompts are origin.kind = 'human'. Of the human
blocks starting with '<', 25 are the <system-reminder> attached to a prompt (dropped) and
26 are slash commands, which the old '<' rule wrongly dropped and which now count.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The section now answers what each live session is working on instead of showing its newest
message: one row per Claude session active in the last hour with its titles, branch, linked
PRs, minutes idle, and the other live sessions that share its branch or a PR. It reads only
top-level fields of agent.lines (sessionId, timestamp, gitBranch, aiTitle/customTitle,
prRepository/prNumber), no message text. Run from the file on dev: 4 live sessions, 2 of
them on one inf-1656 branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One row per session active in the last hour, from agent.sessions (#68, #76)
instead of unpacking agent.lines JSON: every column but prompts and
block_keys, minutes idle, and shares_branch_with as a list of session ids.
Codex sessions are included. Titles and linked PRs are dropped: stream.blocks
does not carry those lines. About 4 s against 20 s for the old read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@asubbarao
asubbarao force-pushed the agent-stream-now-first branch from 4d04483 to 4b96eff Compare October 19, 2026 16:43
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