Tab IDs in KISS Sorcar: How Workflows Route Around a Pane Identifier
A line-verified tour of every tab_id / tabId use in
src/kiss/core/, src/kiss/agents/sorcar/, and
src/kiss/server/ — branch bigrefactor, after the
mirror-clients refactor and the removal of the interactive diff/merge review.
All line numbers cite the current working tree.
1. What a tab id is — the four identifier forms
A tab id is the routing key of one pane in the frontend: a chat tab in the VS Code webview or in the kiss-web browser app. Under the mirror-clients model every connected client shows the same set of tabs, so a tab id names a globally shared pane, not a per-client one. The backend uses tab ids for exactly two things: to stamp outbound events so clients know which pane renders them, and to look up the per-tab server state (the launched task, its model pick, its viewed chat) when a command arrives carrying a tab id.
Four identifier forms appear on the wire, distinguished only by convention:
| Form | Example | Minted by | Purpose |
|---|---|---|---|
| Frontend UUID | 3f2a…c9 |
The webview's createNewTab flow; the backend first sees it on a
newChat / run command. |
An ordinary interactive chat tab. server.py:875–879 documents
that newChat always carries a fresh, never-seen id. |
| Synthetic API tab | api-{uuid.hex} |
The headless Python client kiss.server.sorcar.run(), one per call.
sorcar.py:1120 |
Makes a headless UDS client indistinguishable from a frontend tab: the daemon fans events out stamped with this id and the client filters its own stream by it (sorcar.py:1136, 1187). |
| Live sub-agent tab | task-{parent_task_id}__sub_{idx} |
The parent agent, in ChatSorcarAgent._run_tasks_parallel.
chat_sorcar_agent.py:613 |
Gives each parallel child its own child pane; injected as the child's
agent._tab_id (:614). |
| Replay sub-agent tab | {parent_tab_id}__sub_{sub_task_id} |
The server, when re-opening persisted sub-agent runs on session resume. server.py:1188 | Deterministic, so clicking the same parent task twice updates sub-tabs in place instead of stacking duplicates (server.py:1165–1171). |
The __sub_ substring in the last two forms is a cross-layer contract:
server.py's _resolve_parent_tab_id_for_sub parses it with
rsplit("__sub_", 1) as its last-resort tier when inferring which tab a
sub-agent pane should nest under (server.py:1141–1145) — the
right-split keeps nested chains (A__sub_B__sub_C → parent prefix
A__sub_B) correct. Section 6 covers the whole mechanism.
tabId on an event is a routing stamp — it tells the printer
"this copy is pre-addressed; deliver it to clients (which filter panes by it) and do not
record or persist it". Snake_case tab_id / parent_tab_id fields are
plain payload data the webview stores (e.g. the tab-tree structure in
openSubagentTab). An event can carry both at once — subagentDone ships
{"tab_id": vid, "tabId": ""}: an all-clients broadcast (empty routing stamp)
whose payload names the pane whose spinner should stop
(sorcar_agent.py:256, server.py:1222–1228).
2. The layer map: who touches tab ids, and how much
A case-insensitive grep for tab_id/tabId over the three directories
gives a strictly stratified picture: src/kiss/core/ has zero
occurrences; the three agent files hold 22 lines of narrow UI hooks; and the
nine server files own everything else — 507 lines. Commands flow
down from clients carrying tabId; task events flow up from agents
completely tab-free and are stamped per-tab only at the fan-out edge inside
WebPrinter.
cmd["tabId"] and are sanitized in
server.py:418–421; agents emit task events with no tab id at all; the per-tab
tabId stamp is spliced onto each subscriber's copy only at the fan-out edge,
WebPrinter._fanout_stamped (web_server.py:1890–1922). The single downward injection
into the agent layer is agent._tab_id = tab_id (task_runner.py:671; the only
other production assignment is the sub-agent synthesis at chat_sorcar_agent.py:614).
The stratification is deliberate. The printer base class is task-centric: "every piece
of per-stream state … is keyed by task_id rather than by the frontend tab id"
(json_printer.py:7–9); tab ids appear only at the edges —
subscription (_subscribers[task_id] → {tab_id, …}) and transient targeting. Agents
receive a tab id only where the UI structurally needs one (sub-agent panes, best-effort
toasts); everything else — steering, persistence, recording — reaches them keyed by task id.
3. Launching a task from a tab
The run command is where a tab id does the most work. Between the frontend's
click and the agent's first event, the launching tab's id is copied into four places:
the tab-keyed guards in commands.py, the new AgentState.tab_id
attribute (in a registry keyed by a uuid state key, not by tab id), the injected
agent._tab_id, and the printer's _task_ui /
_subscribers tables — wired up the moment the run's task_history
row id exists.
T. Line numbers
without a file refer to the lifeline's own file (commands.py for _cmd_run,
task_runner.py for the run lifecycle, json_printer.py for the printer boxes). The launching tab
id ends up in four places: the tab-keyed guards, AgentState.tab_id (in a
uuid-keyed registry entry), agent._tab_id (:671), and the printer's
_task_ui/_subscribers tables — the latter wired by
_on_run_task_id_allocated before any agent event is broadcast, so neither the
launcher nor pre-existing chat viewers can miss a live event.Three details worth calling out:
- The registry is not tab-keyed.
_cmd_runregisters the newAgentStateunder a freshuuid4().hexstate key (commands.py:356–378); the printer bridge later re-keys it to the real task id. The tab id is a searchable attribute, resolved byagent_state.find_by_tab(tab_id)— a two-pass linear scan that prefers server-owned states (agent_state.py:202–221). If the tab had a previous idle state (e.g. holding a pending worktree), its agent andfrontend_closedflag carry over and the old state is unregistered (commands.py:368–373). - Failures before a task id exists are tab-addressed. Setup failures,
the no-model guard, and the merge-in-progress guards broadcast
result/errorevents stamped with the launcher'stabId(task_runner.py:387–410, 682–703, 718–729) because no task-keyed fan-out is possible yet. Once atask_historyrow exists, failureresults prefer ataskIdstamp and only fall back totabId(task_runner.py:890–893, 939–942). - History resume is consumed at launch. If the user had opened a historical
task in this tab,
_tab_opened_task_ids.pop(tab_id)hands the popped id toagent.resume_from_task_id— consumed exactly once per run (task_runner.py:740–743).
4. Event routing: WebPrinter.broadcast
Every outbound event funnels through one method, and the presence of the
tabId key (even with an empty value) is the routing discriminator.
There is no per-connection tab filtering anywhere in the server: every payload
is sent to every connected WSS and UDS endpoint
(_send_to_ws_clients, web_server.py:2056–2071), and the frontend filters
by the tabId stamp. The only narrower channels are keyed by connection id, never
by tab id.
WebPrinter.broadcast
(web_server.py:1806–1889; the recording rules mirror the base class
JsonPrinter.broadcast, json_printer.py:984–1015). The tabId key is the
discriminator between pre-addressed transients (delivered verbatim, unrecorded) and task
events (recorded once tab-free, then stamped per subscriber tab by
_fanout_stamped). Every send goes to every WSS + UDS endpoint — the mirror
model — and the frontend filters panes by the stamp.
The transient primitives layered on top of this contract live in the base printer:
broadcast_transient(event, task_id=None, tab_id="") resolves all watching tabs via
_transient_targets (thread-local task id first, explicit argument as a
near-teardown fallback, _subscribers as the target source — _task_ui is
deliberately not consulted because it is dropped at task teardown) and broadcasts one
stamped copy per tab (json_printer.py:483–553). Even when no
watcher resolves, one copy stamped with the fallback tab_id — possibly
"" — still goes out: the mere key presence preserves the no-record semantics
(json_printer.py:552–553).
5. Steering: follow-up messages into a running task
Follow-ups typed while a task runs are addressed by tab but consumed by task.
The server enqueues them on the owning AgentState found via the tab id (with a
viewer-tab fallback through the printer's subscriber map); the agent drains them through a
duck-typed printer bridge that resolves the state by the calling thread's task id —
the agent never sees a tab id on the consumption side.
AgentState — directly via find_by_tab (two-pass, server-owned
first) or through the printer's subscriber map for viewer tabs — and appends to
task-owned queues. Right: the agent's self-guarding hooks drain those queues via the printer
bridge keyed by the calling thread's task id, and refuse to finish while a
follow-up is undrained. The tab id never crosses to the consumption side.6. Parallel sub-agent tabs
When a task fans out into parallel sub-agents, each child gets its own pane. Live children
are announced by the parent agent, which mints synthetic tab ids; persisted children
are re-opened by the server on session resume with a different, deterministic id form.
Both forms embed __sub_, the contract that lets the server re-derive the parent
pane when it must.
agent._tab_id (the second of the only two production assignment sites); replay ids
are minted by the server from the parent tab id and the child task id, making
re-resume idempotent. In both, the spinner-stopping subagentDone carries the pane
in the snake_case payload and an empty camelCase routing stamp.7. Post-task flows: autocommit and worktree presentation
With the interactive diff/merge review removed, what happens after a task is a short
decision tree, and every branch is tab-addressed: the tab id stamps the progress and
terminal events, keys the find_by_tab lookups that reach the tab's worktree
agent, and anchors the per-tab is_merging mutual-exclusion claim.
worktree_done. Per-tab mutual exclusion is the is_merging flag on the
tab's AgentState, always claimed and released under the daemon-wide
_state_lock, with _dispose_if_closed(tab_id) after every release so a
tab closed mid-flight is finally torn down.8. The tab lifecycle: birth, life, disconnects, death
Since the mirror-clients refactor, tabs are global server state: no connection
owns them, and a disconnect tears down nothing tab-related. The only death is an explicit
closeTab — which is also the only command whose catalog entry requires a
tabId (sorcar.py:322).
AgentState, and the
talk-muting refcount), and exactly one death path: explicit closeTab, deferred via
frontend_closed + _dispose_if_closed when the tab is busy. Disconnects
— WSS or UDS — never touch tab state; the headless client compensates by sending its own
closeTab in a finally.9. Session resume, replay, and the ready flow
9.1 resumeSession → _replay_session (server.py:907–1078)
Clicking a history entry (or restoring a session) sends resumeSession {chatId, tabId
[, taskId]}. The engine's tab-id behavior, in order:
- Empty tab id is a hard no-op (:938–940). The previous behavior of synthesizing a phantom tab keyed by chat id violated per-tab state isolation and was removed (the C2/C3 fix). The tab id — the frontend routing key — never changes during a resume; only the tab's chat association does.
- Record what the tab views:
_tab_opened_task_ids[tab_id] = task_idwhen a specific task row was clicked, popped otherwise (:941–945); consumed once by the next run on this tab (task_runner.py:740–743). - Purge, then rebind: both replay branches call
_printer_cleanup_tab(tab_id)before resubscribing (:954, :1005), so a tab re-viewing a different chat never keeps receiving the old stream. Then_reattach_running_chat(chat_id, tab_id, task_id=…, is_subagent=…)(:1267–1354) subscribes the tab to a still-live source state with two-pass matching: pass 1 requires an exact task-id match (mandatory for sub-agent multi-view); pass 2 falls back to a chat-id match but is skipped entirely whenis_subagent=Trueand always excludes sub-agent states — so a sub-agent view can never subscribe to its parent's stream, and a regular chat can never land inside a sub-agent stream. The single rebinding call isprinter.subscribe_tab(source_task_id, new_tab_id)(:1353) — duplicating the stream to the new pane, never stealing it from existing viewers. - Replayed events are stamped once, at the envelope. Persisted per-event rows
carry no tab id; they are coalesced (consecutive
thinking_delta/text_delta/system_outputmerged, :181–206) and shipped as onetask_eventsevent whose top-leveltabIdis the viewing tab (:1061–1071), withextrafiltered by_extra_for_replayso a replay cannot flip global toolbar toggles (:84–126). A still-running chat with no persisted events yet gets an empty envelope plusstatus {running:true, tabId, startTs}with the true start time from_live_task_start_ms(:962–981, 1230–1265). - Resume clears
frontend_closed(:986, :1015) and ends by re-presenting any pending worktree (_emit_pending_worktree, :1072) and re-opening persisted sub-agent tabs (:1074–1078, Fig 5B).
9.2 ready / restoredTabs (sorcar.py:821–844, web_server.py:4256–4400)
A (re)loading webview announces the tabs it restored. _sanitized_restored_tabs
caps the list, skips non-dict entries, and blanks non-string ids
(web_server.py:4256–4315); each restored id then passes through
_record_tab (talk-muting bookkeeping, sorcar.py:836–840). The web layer's
_handle_ready replays each sanitized {tabId, chatId} pair with one
backend resumeSession (:4369–4376) — the client
supplies the tab ids, the server replays into them. For remote (non-UDS) clients only, it also
pushes openRunningTasks built from _snapshot_running_task_rows() —
rows carry chatId/taskId and no tab ids at all
(:1499–1563, 4377–4399): a reconnecting browser mints fresh panes
and issues its own resumeSessions. Init fan-out (getModels,
getInputHistory, getConfig) is connId-scoped, and a
focusInput {tabId} echo lands on the sender's active pane
(:4345–4366).
10. Smaller flows
10.1 Ask-user: question out task-keyed, answer back tab-resolved
_ask_user_question broadcasts askUser {question} with no
tabId (task_runner.py:1631–1636) — a
task event that fans out to every subscribed pane, so any viewer can answer. The reply,
userAnswer {tabId, answer}, is resolved in two tiers
(commands.py:579–626): ① the state launched from the answering
tab itself, when it has a live user_answer_queue; ② otherwise any task the tab is
subscribed to, via the printer's _subscribers map — resolving through the
task id makes a cross-task answer hijack structurally impossible. The queue is
drain-then-put under _state_lock (a maxsize=1 queue cannot wedge on a
double answer), and askUserDone {tabId} is broadcast once per subscriber tab of the
answered task — exactly the panes showing that modal, not every tab the answerer ever
touched (:536–577). The waiting side resolves its queue by the
calling thread's task id, deliberately tab-free
(task_runner.py:1607–1625).
10.2 Stop
_stop_task(tab_id) refuses an empty tab id with a warning — a
missing tabId at this layer indicates a frontend bug that must not silently stop
every tab's task (task_runner.py:1390–1397). Resolution:
find_by_tab first; when the tab owns no live task, the viewer fallback
_find_viewer_task_states scans _subscribers for tasks the tab views
(:1398–1413, 1470–1506) — so a second client viewing a running
task via history-click can stop it. stop_ack {accepted, tabId} is broadcast to the
clicking pane before the stop event is set, so the click visibly lands even if the task
dies on the next bytecode (:1431–1437, 1452–1468).
10.3 Model picker
The picker is per-tab: _tab_models[tab_id] is written by
selectModel (empty tab id degrades to updating only the daemon default,
commands.py:407–426) and read when a run omits a model (task_runner.py:665, server.py:394–404).
When a running agent calls set_model, the display-only override reaches every
watching pane via broadcast_agent_model_pick, which remembers the targets in
_model_override_tabs and the model in _task_model_override[task]
(json_printer.py:587–626); a pane joining mid-task catches up in
subscribe_tab (:272–279). At task end,
restore_model_pick hands each overridden pane its user's own pick back — a no-op
for panes that never showed an override (:628–642;
task_runner.py:448–468, 523). The agent side supplies only best-effort hints:
_show_model_in_picker passes _tab_id/_last_task_id so the
fan-out still works near teardown, after the printer's thread-local task id is cleared
(sorcar_agent.py:1064–1096).
10.4 Autocomplete: the tab id as inert reply cargo
autocomplete.py holds zero tab-keyed state: staleness sequences and
request tokens are keyed by connection id, the file cache by work_dir
(:193–195). The request's tabId rides the worker queue
as inert cargo and is echoed on the three replies — ghost,
completions, files (:395–454, 627–670) —
because one window shows several chat tabs over one connection but has a single ghost overlay
and a single @-picker element: "the tab is what actually identifies the owner of
the reply" (:656–659). The request side's only semantic use is in
commands.py:_cmd_complete, which resolves the tab's chat context
(find_by_tab, then _tab_chat_views)
(commands.py:780–806). The post-task cache rescan deliberately
emits no files event at all — an unsolicited reply with blank
conn/tab/prefix would be accepted by every client showing a bare @
(autocomplete.py:576–625).
10.5 Direct tab-stamped utility replies
| Event | Trigger | Delivery | Where |
|---|---|---|---|
files | getFiles (@-mention) |
connId narrows to the window, tabId to the pane |
commands.py:463–488; autocomplete.py:627–670 |
commitMessage | generateCommitMessage |
all four exit paths (not-a-repo, nothing staged, success, failure) stamp the requesting tabId |
server.py:1454–1520 |
fileContent | openFile (remote web) |
direct _endpoint_send reply; tabId is a pure request echo |
web_server.py:3943–4020 |
pathsExist | checkPaths (linkification probe) |
direct reply, tabId echo |
web_server.py:4019–4082 |
adjacent_task_events | getAdjacentTask (history arrows) |
coalesced replay envelope stamped with the requesting pane; chat resolved from the tab's own chat only — no global-latest fallback | commands.py:812–843; server.py:1425–1452 |
error (unknown/invalid command) | dispatcher | echoes the command's tabId, sent to the issuing connection only |
server.py:427–431; sorcar.py:628–634 |
10.6 Talk muting: the one remaining per-connection tab structure
Every command's tabId is recorded in the connection's tabs_seen;
UDS (local VS Code) connections additionally refcount it in
WebPrinter._local_uds_tab_counts via register_local_uds_tab
(sorcar.py:649–666; web_server.py:1939–1952). The sole consumer is
_fanout_talk: Chromium autoplay policy keeps webviews from playing audio, so when a
talk clip targets a locally-shown tab, the daemon plays it natively and stamps those
tabs' copies muted; remote browser tabs get the playable copy
(web_server.py:1954–1990). Since the mirror-clients refactor this
bookkeeping drives nothing else — no filtering, no ownership, no lifecycle.
11. Inventory: every tab-keyed data structure
| Structure | Type | Purpose | Created / written | Cleaned |
|---|---|---|---|---|
_tab_chat_views |
dict[tab_id → chat_id] |
which chat each pane is viewing; viewer discovery at task start | server.py:313; commands.py:355; server.py:988/1017 (replay) | _teardown_tab_resources :868; _new_chat :895; sub-agent views popped :1019 |
_tab_opened_task_ids |
dict[tab_id → task_id] |
history click to resume; consumed once by the next run | server.py:314, 943 | popped at launch task_runner.py:741; teardown :869; new chat :896; no-task resume :945 |
_tab_models |
dict[tab_id → model] |
per-pane model pick | server.py:315, 894 (seed); commands.py:422 | _teardown_tab_resources :870 |
JsonPrinter._subscribers |
dict[task_id → set[tab_id]] |
the single fan-out truth: which panes receive a task's events | subscribe_tab json_printer.py:266–271; register_task_ui :306 | cleanup_tab :741–746; cleanup_task :809–814 (300 s linger via _subscriber_expiry, swept lazily :817–831) |
JsonPrinter._task_ui |
dict[task_id → (tab_id, conn_id)] |
launch-time metadata only; never consulted for fan-out or transients | register_task_ui :305 | cleanup_task :808 (immediate) |
JsonPrinter._model_override_tabs |
set[tab_id] |
panes currently showing a running agent's model instead of the user's pick | broadcast_agent_model_pick :622; subscribe_tab catch-up :277 | restore_model_pick :641; cleanup_tab :739 |
AgentState.tab_id |
attribute on task-keyed states | the launching pane; searched by find_by_tab (two-pass, server-owned first) |
commands.py:361; task_runner.py:606; json_printer.py:355 (from agent._tab_id) | dies with the state: closeTab unregister server.py:803–804; _dispose_if_closed :827; agent_task_finished for non-server-owned |
agent._tab_id |
attribute on the agent object | steering-registry bridge + sub-pane identity + toast/model-pick fallback hints | task_runner.py:671 (per run); chat_sorcar_agent.py:614 (sub-agents); initialized "" worktree_sorcar_agent.py:136 | overwritten on the tab's next run; never explicitly cleared |
WebPrinter._local_uds_tab_counts |
dict[tab_id → refcount] |
talk-playback arbitration only (mute local webview copies) | register_local_uds_tab web_server.py:1939–1946, driven by sorcar.py:662–666 | unregister_local_uds_tabs :1948–1952 from _uds_handler finally :3539–3541; key deleted at zero |
tabs_seen / conn_state["local_tabs"] |
set[tab_id] per connection |
first-seen dedupe feeding the refcount above | web_server.py:3444/3509 (empty per connection); sorcar.py:660–665 | garbage-collected with the connection |
For contrast, everything else is keyed by task id (_recordings, offsets, bash
buffers, persistence, the agent-state registry itself), by connection id
(_conn_endpoints, autocomplete staleness, _complete_seq_latest), by
endpoint object (_send_locks, _pending_sends), or by work_dir
(_file_cache).
12. The agent-side surface: all 22 occurrences
The three agent files touch tab ids on exactly 22 lines, all via duck-typed printer
attributes (getattr with safe defaults) — the agent layer never imports server
modules, and a printer-less console run degrades every site to a no-op or an empty stamp.
| File · lines | What | Why |
|---|---|---|
| chat_sorcar_agent.py:582, 590 | parent_tab_id = own _tab_id, or the first sorted
_fanout_targets viewer for nested sub-agents |
a renderable anchor for the frontend's tab tree (Fig 5A) |
| chat_sorcar_agent.py:613–614, 623 | mint task-{parent_task_id}__sub_{idx}; inject agent._tab_id;
store parent_tab_id in _subagent_info |
each parallel child streams into its own pane; the injected id is what the printer
bridge copies into the child's AgentState (json_printer.py:355) |
| chat_sorcar_agent.py:662–663 | dedupe sub_tab_id into the viewer list before subagentDone |
the synthetic pane may or may not already be a subscriber |
| chat_sorcar_agent.py:794–795, 800 | new_tab broadcast carrying parent_tab_id (payload) and
taskId: "" |
tells the frontend where to nest the child pane, at run start |
| sorcar_agent.py:234, 246, 254, 256 | _broadcast_subagent_done(printer, tab_ids, model):
{tab_id: vid, tabId: ""} + restore_model_pick per id |
stop each child pane's spinner and hand back its picker; best-effort, errors swallowed |
| sorcar_agent.py:1094 | the file's only _tab_id read: passed with
_last_task_id to broadcast_agent_model_pick |
fallback hints so the model-pick fan-out works off-thread / near teardown; the printer resolves watching panes itself |
| sorcar_agent.py:1511–1513 | non-UI parallel path synthesizes task-{parent_key}__sub_{idx} from the
parent task id alone |
closes out any child pane the frontend opened; no _tab_id read or
assigned on this path |
| worktree_sorcar_agent.py:136 | self._tab_id: str = "" field initializer |
safe default; set by task_runner.py:671 per run (or chat_sorcar_agent.py:614 for sub-agents), left empty for console runs |
| worktree_sorcar_agent.py:278, 283–284 | commit toast via broadcast_transient(event, task_id=…, tab_id=self._tab_id),
falling back to one broadcast({**event, "tabId": self._tab_id}) |
task id is the preferred routing key; the explicit stamp keeps the toast out of recording/persistence on the fallback path (Fig 6) |
Notably absent since the recent refactors: the steering hooks read no tab id
and touch no registry (they are installed unconditionally and drain via the printer bridge,
§5); the old running_agent_state.py per-tab registry module is gone (its role is
served by src/kiss/server/agent_state.py); and the old _subscribe_tab_id
run-kwarg no longer exists in any agent file — launcher-tab subscription is entirely
server-side.
13. Why core/ is tab-free, and what changed recently
src/kiss/core/ has zero tab-id occurrences (a case-insensitive
grep over the tree returns nothing). This is by design, and it is the same design that keeps
the agent surface at 22 lines: the core Printer interface and agent framework
speak only in tasks, steps, and events. The tab — a purely presentational concept, "which
pane of which frontend renders this" — is confined to the server layer, where the
task-centric printer translates at exactly two edges: subscription
(subscribe_tab) and stamping (_fanout_stamped / the transient
primitives). Anything below those edges can run headless, in tests, or embedded without a
frontend existing at all.
What changed recently (context for readers of older docs)
- UI-mirror machinery is gone. Under the mirror-clients assumption (all
clients show the same tabs), the per-viewer
mirrorOfstamping, owner/viewer resolution, andbroadcast_tab_uiwere removed; a single owner-tab-stamped event reaches every client. - Deferred close and per-connection tab ownership are gone. Disconnects no
longer arm teardown timers; a tab dies only by explicit
closeTab(Fig 7). The headless client compensates with its owncloseTabin afinally. - The interactive diff/merge review is gone.
mergeAction,autocommitAction,merge_data/merge_started/merge_navand the per-tab merge-data machinery no longer exist; dirty non-worktree trees always auto-commit, and worktrees present plain Merge/Discard buttons (Fig 6).
web_server.py:_stop_active_agent_tasks the shutdown sweep collects tuples of
(task_id, stop_event, thread) (:5628–5632) but the
unpacking variables in the log line and the join loops are named tab_id /
_tab_id (:5654, 5657, 5662, and the timeout warning near
:5677). The values logged are task ids. Behavior is correct — the values are
only formatted into log messages — but readers (and diagrams) should label them as task ids.
The signal-handler path is fine: it uses _snapshot_active_tabs(), which genuinely
prints tabId(task=task_id) pairs (:1461–1497).
Summary
Tab ids in this codebase form a disciplined, single-purpose system: commands carry them down, events carry them up, and nothing in between thinks in tabs. The 507 server lines implement the two translation edges plus per-tab UI state (chat view, model pick, opened-task resume, steering queues, worktree claims); the 22 agent lines exist only where the UI structurally needs an agent's help (sub-agent panes, best-effort toasts and picker hints); and the core layer proves the abstraction holds by never mentioning tabs at all.
Generated from six line-verified audit notes (printer, task runner, server, commands,
web server, agents/merge-flow) cross-checked against the working tree at branch
bigrefactor. Line numbers reflect that tree; counts are from a case-insensitive
grep for tab_id/tabId.