mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
Merge pull request #75583 from NousResearch/bb/hide-kanban-worker-sessions
Some checks failed
CI / Detect affected areas (push) Has been cancelled
CI / OSV scan (push) Has been cancelled
Deploy Site / deploy-vercel (push) Has been cancelled
Deploy Site / deploy-docs (push) Has been cancelled
Docker Build, Test, and Publish / build (amd64, type=gha,scope=docker-amd64, type=gha,mode=max,scope=docker-amd64, linux/amd64, ubuntu-latest) (push) Has been cancelled
Docker Build, Test, and Publish / build (arm64, type=gha,scope=docker-arm64, type=gha,mode=max,scope=docker-arm64, linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
auto-fix lint issues & formatting / Generate eslint --fix patch (push) Has been cancelled
CI / Python tests (push) Has been cancelled
CI / Python lints (push) Has been cancelled
CI / JS & TS checks (push) Has been cancelled
CI / Desktop E2E (push) Has been cancelled
CI / Docs Site (push) Has been cancelled
CI / Deny unrelated histories (push) Has been cancelled
CI / Check contributors (push) Has been cancelled
CI / Check uv.lock (push) Has been cancelled
CI / Check no committed infographics (push) Has been cancelled
CI / package-lock.json diff (push) Has been cancelled
CI / Lint Docker scripts (push) Has been cancelled
CI / Build&Test Docker image (push) Has been cancelled
CI / Supply-chain scan (push) Has been cancelled
CI / Review label gate (push) Has been cancelled
CI / CI review comment (live) (push) Has been cancelled
CI / All required checks pass (push) Has been cancelled
CI / CI timing report (push) Has been cancelled
Docker Build, Test, and Publish / publish (amd64, type=gha,scope=docker-amd64, type=gha,mode=max,scope=docker-amd64, linux/amd64, ubuntu-latest) (push) Has been cancelled
Docker Build, Test, and Publish / publish (arm64, type=gha,scope=docker-arm64, type=gha,mode=max,scope=docker-arm64, linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Docker Build, Test, and Publish / merge (push) Has been cancelled
auto-fix lint issues & formatting / Apply patch (push) Has been cancelled
Some checks failed
CI / Detect affected areas (push) Has been cancelled
CI / OSV scan (push) Has been cancelled
Deploy Site / deploy-vercel (push) Has been cancelled
Deploy Site / deploy-docs (push) Has been cancelled
Docker Build, Test, and Publish / build (amd64, type=gha,scope=docker-amd64, type=gha,mode=max,scope=docker-amd64, linux/amd64, ubuntu-latest) (push) Has been cancelled
Docker Build, Test, and Publish / build (arm64, type=gha,scope=docker-arm64, type=gha,mode=max,scope=docker-arm64, linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
auto-fix lint issues & formatting / Generate eslint --fix patch (push) Has been cancelled
CI / Python tests (push) Has been cancelled
CI / Python lints (push) Has been cancelled
CI / JS & TS checks (push) Has been cancelled
CI / Desktop E2E (push) Has been cancelled
CI / Docs Site (push) Has been cancelled
CI / Deny unrelated histories (push) Has been cancelled
CI / Check contributors (push) Has been cancelled
CI / Check uv.lock (push) Has been cancelled
CI / Check no committed infographics (push) Has been cancelled
CI / package-lock.json diff (push) Has been cancelled
CI / Lint Docker scripts (push) Has been cancelled
CI / Build&Test Docker image (push) Has been cancelled
CI / Supply-chain scan (push) Has been cancelled
CI / Review label gate (push) Has been cancelled
CI / CI review comment (live) (push) Has been cancelled
CI / All required checks pass (push) Has been cancelled
CI / CI timing report (push) Has been cancelled
Docker Build, Test, and Publish / publish (amd64, type=gha,scope=docker-amd64, type=gha,mode=max,scope=docker-amd64, linux/amd64, ubuntu-latest) (push) Has been cancelled
Docker Build, Test, and Publish / publish (arm64, type=gha,scope=docker-arm64, type=gha,mode=max,scope=docker-arm64, linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Docker Build, Test, and Publish / merge (push) Has been cancelled
auto-fix lint issues & formatting / Apply patch (push) Has been cancelled
Kanban worker runs stop appearing as chats in the sidebar
This commit is contained in:
commit
4b60979dc1
12 changed files with 227 additions and 23 deletions
|
|
@ -29,12 +29,13 @@ import {
|
|||
} from '@/store/session'
|
||||
import { $workingSessionIds, getRecentlySettledSessionIds } from '@/store/session-states'
|
||||
|
||||
// The recents list is local-only: cron rows have their own section, and each
|
||||
// messaging platform (telegram, discord, …) is fetched separately into its own
|
||||
// self-managed sidebar section (refreshMessagingSessions). Excluding both here
|
||||
// keeps "Load more" paging through interactive local chats instead of
|
||||
// The recents list is local-only: cron rows have their own section, kanban
|
||||
// dispatcher workers are read on the board, and each messaging platform
|
||||
// (telegram, discord, …) is fetched separately into its own self-managed
|
||||
// sidebar section (refreshMessagingSessions). Excluding them here keeps
|
||||
// "Load more" paging through interactive local chats instead of
|
||||
// interleaving gateway threads that bury them.
|
||||
const SIDEBAR_EXCLUDED_SOURCES = ['cron', 'subagent', 'tool', ...MESSAGING_SESSION_SOURCE_IDS]
|
||||
const SIDEBAR_EXCLUDED_SOURCES = ['cron', 'kanban', 'subagent', 'tool', ...MESSAGING_SESSION_SOURCE_IDS]
|
||||
// The messaging slice is the inverse: drop cron + every local source so only
|
||||
// external-platform conversations remain, then split per platform in the UI.
|
||||
const MESSAGING_EXCLUDED_SOURCES = ['cron', ...LOCAL_SESSION_SOURCE_IDS]
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ const SOURCE_LABELS: Record<string, string> = {
|
|||
discord: 'Discord',
|
||||
email: 'Email',
|
||||
gateway: 'Gateway',
|
||||
kanban: 'Kanban',
|
||||
local: 'Local',
|
||||
matrix: 'Matrix',
|
||||
mattermost: 'Mattermost',
|
||||
|
|
@ -42,7 +43,7 @@ const SOURCE_ALIASES: Record<string, string[]> = {
|
|||
// platform. A handoff *from* one of these isn't a platform origin worth a badge.
|
||||
// Exported so the recents fetch can keep these in the main list while the
|
||||
// messaging fetch excludes them.
|
||||
export const LOCAL_SESSION_SOURCE_IDS = ['cli', 'codex', 'desktop', 'gateway', 'local', 'tui']
|
||||
export const LOCAL_SESSION_SOURCE_IDS = ['cli', 'codex', 'desktop', 'gateway', 'kanban', 'local', 'tui']
|
||||
const LOCAL_SOURCE_IDS = new Set(LOCAL_SESSION_SOURCE_IDS)
|
||||
|
||||
// External messaging platforms that each get their own self-managed sidebar
|
||||
|
|
|
|||
2
cli.py
2
cli.py
|
|
@ -7712,7 +7712,7 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
|
|||
include_all_sources=False,
|
||||
include_unnamed=True,
|
||||
limit=limit,
|
||||
exclude_sources=["tool"],
|
||||
exclude_sources=["kanban", "tool"],
|
||||
)
|
||||
except Exception:
|
||||
return []
|
||||
|
|
|
|||
|
|
@ -350,6 +350,7 @@ NON_MESSAGING_SESSION_SURFACES = frozenset(
|
|||
"codex",
|
||||
"desktop",
|
||||
"gateway",
|
||||
"kanban",
|
||||
"local",
|
||||
"msgraph_webhook",
|
||||
"tool",
|
||||
|
|
|
|||
|
|
@ -1324,7 +1324,7 @@ def _sessions_list(_engine: HermesConsoleEngine, args: list[str]) -> str:
|
|||
db = SessionDB()
|
||||
try:
|
||||
sessions = db.list_sessions_rich(
|
||||
exclude_sources=["tool"],
|
||||
exclude_sources=["kanban", "tool"],
|
||||
limit=ns.limit,
|
||||
order_by_last_active=True,
|
||||
)
|
||||
|
|
@ -1340,7 +1340,7 @@ def _sessions_stats(_engine: HermesConsoleEngine, args: list[str]) -> str:
|
|||
db = SessionDB()
|
||||
try:
|
||||
total = db.session_count()
|
||||
listable = db.session_count(exclude_children=True, exclude_sources=["tool"])
|
||||
listable = db.session_count(exclude_children=True, exclude_sources=["kanban", "tool"])
|
||||
messages = db.message_count()
|
||||
lines = [
|
||||
f"Total sessions: {total}",
|
||||
|
|
|
|||
|
|
@ -8841,6 +8841,32 @@ def _resolve_worker_cli_toolsets(hermes_home: Optional[str]) -> Optional[list[st
|
|||
return None
|
||||
|
||||
|
||||
_retagged_workspace_roots: set[str] = set()
|
||||
|
||||
|
||||
def _retag_legacy_worker_sessions(workspaces_root_path: str) -> None:
|
||||
"""Reclaim pre-tag worker rows in state.db so they leave the session lists.
|
||||
|
||||
Best-effort and gated — the durable ``state_meta`` gate lives in
|
||||
``retag_kanban_worker_sessions``; the in-process set keeps a busy
|
||||
dispatcher from reopening state.db on every spawn just to read it. A
|
||||
dispatcher tick must never fail because a session DB was busy or missing.
|
||||
"""
|
||||
if workspaces_root_path in _retagged_workspace_roots:
|
||||
return
|
||||
try:
|
||||
from hermes_state import SessionDB
|
||||
|
||||
db = SessionDB()
|
||||
try:
|
||||
db.retag_kanban_worker_sessions(workspaces_root_path)
|
||||
finally:
|
||||
db.close()
|
||||
_retagged_workspace_roots.add(workspaces_root_path)
|
||||
except Exception as exc:
|
||||
_log.debug("kanban worker: legacy session retag skipped (%s)", exc)
|
||||
|
||||
|
||||
def _default_spawn(
|
||||
task: Task,
|
||||
workspace: str,
|
||||
|
|
@ -8898,6 +8924,14 @@ def _default_spawn(
|
|||
env["HERMES_TENANT"] = task.tenant
|
||||
env["HERMES_KANBAN_TASK"] = task.id
|
||||
env["HERMES_KANBAN_WORKSPACE"] = workspace
|
||||
# Tag the worker's session so it lands in state.db as `kanban`, not as an
|
||||
# untitled `cli` row. A worker is a dispatcher-owned run whose transcript is
|
||||
# read on the board and in `hermes kanban log` — it is not a conversation
|
||||
# the user started, so every session-browsing surface (desktop sidebar, TUI
|
||||
# resume picker, session_search) filters it out by source. Without this the
|
||||
# sidebar renders one row per attempt, labeled with the worker's own prompt
|
||||
# ("work kanban task t_…").
|
||||
env["HERMES_SESSION_SOURCE"] = "kanban"
|
||||
# Pin TERMINAL_CWD to the task's workspace so the worker's file tools and
|
||||
# context-file loader anchor on the workspace, not whatever cwd the
|
||||
# dispatching gateway happened to export. The worker subprocess is already
|
||||
|
|
@ -8945,6 +8979,7 @@ def _default_spawn(
|
|||
# but unusual symlink / Docker layouts are caught here too.
|
||||
env["HERMES_KANBAN_DB"] = str(kanban_db_path(board=board))
|
||||
env["HERMES_KANBAN_WORKSPACES_ROOT"] = str(workspaces_root(board=board))
|
||||
_retag_legacy_worker_sessions(env["HERMES_KANBAN_WORKSPACES_ROOT"])
|
||||
# Board slug — the final defense-in-depth pin. If the worker ever
|
||||
# resolves kanban paths without the DB / workspaces env vars, the
|
||||
# board slug still forces it to the right directory.
|
||||
|
|
|
|||
|
|
@ -7748,6 +7748,41 @@ class SessionDB(SessionSearchMixin, SessionSchemaMixin, SessionPortabilityMixin)
|
|||
)
|
||||
self._execute_write(_do)
|
||||
|
||||
def retag_kanban_worker_sessions(self, workspaces_root: str) -> int:
|
||||
"""Retag legacy kanban worker rows from ``cli`` to ``kanban``.
|
||||
|
||||
Workers used to spawn without ``HERMES_SESSION_SOURCE``, so their runs
|
||||
landed as untitled ``cli`` rows and the sidebar rendered one per attempt
|
||||
labeled with the worker's own prompt. New workers tag themselves; this
|
||||
reclaims the rows already on disk so they drop out of the session lists
|
||||
too. Identified by cwd under the board's workspaces root — a path only
|
||||
the dispatcher ever runs a session in.
|
||||
|
||||
Gated per workspaces root (``state_meta``) so each board reclaims its
|
||||
own rows exactly once. Returns the number of rows retagged.
|
||||
"""
|
||||
prefix = str(workspaces_root).rstrip("/\\")
|
||||
if not prefix:
|
||||
return 0
|
||||
|
||||
gate = f"kanban_worker_source_retagged:{prefix}"
|
||||
if self.get_meta(gate) == "1":
|
||||
return 0
|
||||
|
||||
def _do(conn):
|
||||
cursor = conn.execute(
|
||||
"UPDATE sessions SET source = 'kanban' "
|
||||
"WHERE source = 'cli' AND (cwd = ? OR cwd LIKE ? ESCAPE '\\')",
|
||||
(prefix, prefix.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_") + "/%"),
|
||||
)
|
||||
# Read rowcount before set_meta reuses this cursor for its INSERT,
|
||||
# which would otherwise overwrite it with the meta write's count.
|
||||
retagged = cursor.rowcount or 0
|
||||
self.set_meta(gate, "1", cursor=cursor)
|
||||
return retagged
|
||||
|
||||
return self._execute_write(_do)
|
||||
|
||||
def apply_telegram_topic_migration(self) -> None:
|
||||
"""Create Telegram DM topic-mode tables on explicit /topic opt-in.
|
||||
|
||||
|
|
|
|||
|
|
@ -792,6 +792,9 @@ class TestSharedBoardPaths:
|
|||
):
|
||||
# The dispatcher must pin board paths while stripping any unrelated
|
||||
# HERMES_SESSION_* identity inherited from the long-lived gateway.
|
||||
# The one exception is HERMES_SESSION_SOURCE, which the dispatcher
|
||||
# re-sets to its own `kanban` tag AFTER the strip — a value it owns,
|
||||
# never one inherited from whatever the gateway last routed.
|
||||
default_home = tmp_path / ".hermes"
|
||||
default_home.mkdir()
|
||||
self._set_home(monkeypatch, tmp_path, default_home)
|
||||
|
|
@ -842,6 +845,11 @@ class TestSharedBoardPaths:
|
|||
assert env["HERMES_KANBAN_TASK"] == "t_dispatch_env"
|
||||
assert env["HERMES_KANBAN_BRANCH"] == "wt/t_dispatch_env"
|
||||
for key in sc._VAR_MAP:
|
||||
if key == "HERMES_SESSION_SOURCE":
|
||||
# Re-set by the dispatcher, so what matters is that it carries
|
||||
# the worker's own tag rather than the inherited routing value.
|
||||
assert env[key] == "kanban"
|
||||
continue
|
||||
assert key not in env
|
||||
|
||||
|
||||
|
|
|
|||
121
tests/hermes_cli/test_kanban_worker_session_source.py
Normal file
121
tests/hermes_cli/test_kanban_worker_session_source.py
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
"""Kanban worker runs must not surface as user conversations.
|
||||
|
||||
Workers spawn as `hermes chat -q "work kanban task <id>"`, which used to land in
|
||||
state.db as an untitled `cli` row — the desktop sidebar then rendered one entry
|
||||
per attempt, labeled with the worker's own prompt.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from hermes_state import SessionDB
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def db(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
database = SessionDB(db_path=tmp_path / "state.db")
|
||||
yield database
|
||||
database.close()
|
||||
|
||||
|
||||
def test_worker_spawn_tags_session_source_kanban(monkeypatch, tmp_path):
|
||||
"""The dispatcher tags the worker's env so its session is a `kanban` row."""
|
||||
from hermes_cli import kanban_db as kb
|
||||
|
||||
captured = {}
|
||||
|
||||
class _Proc:
|
||||
pid = 4321
|
||||
|
||||
def _fake_popen(cmd, **kwargs):
|
||||
captured["env"] = kwargs["env"]
|
||||
return _Proc()
|
||||
|
||||
monkeypatch.setattr("subprocess.Popen", _fake_popen)
|
||||
monkeypatch.setattr(kb, "_retag_legacy_worker_sessions", lambda _root: None)
|
||||
monkeypatch.setattr(kb, "worker_logs_dir", lambda board=None: tmp_path / "logs")
|
||||
|
||||
task = kb.Task(
|
||||
id="t_b21733fb",
|
||||
title="ship it",
|
||||
body=None,
|
||||
assignee="default",
|
||||
status="in_progress",
|
||||
priority=0,
|
||||
created_by=None,
|
||||
created_at=0,
|
||||
started_at=None,
|
||||
completed_at=None,
|
||||
workspace_kind="scratch",
|
||||
workspace_path=None,
|
||||
claim_lock=None,
|
||||
claim_expires=None,
|
||||
tenant=None,
|
||||
)
|
||||
workspace = str(tmp_path / "ws")
|
||||
os.makedirs(workspace, exist_ok=True)
|
||||
|
||||
kb._default_spawn(task, workspace)
|
||||
|
||||
assert captured["env"]["HERMES_SESSION_SOURCE"] == "kanban"
|
||||
|
||||
|
||||
def test_kanban_rows_stay_out_of_the_session_list(db):
|
||||
"""A `kanban` row is filtered by the same exclude the sidebar sends."""
|
||||
db.create_session(session_id="chat", source="desktop")
|
||||
db.append_message(session_id="chat", role="user", content="hey")
|
||||
db.create_session(session_id="worker", source="kanban")
|
||||
db.append_message(session_id="worker", role="user", content="work kanban task t_b21733fb")
|
||||
|
||||
listed = db.list_sessions_rich(exclude_sources=["cron", "kanban", "subagent", "tool"])
|
||||
|
||||
assert [row["id"] for row in listed] == ["chat"]
|
||||
|
||||
|
||||
def test_retag_reclaims_legacy_worker_rows(db, tmp_path):
|
||||
"""Rows written before the tag existed are identified by workspace cwd.
|
||||
|
||||
Two rows, not one: the count has to survive ``set_meta`` reusing the same
|
||||
cursor, which would otherwise report the meta write's rowcount instead.
|
||||
"""
|
||||
workspaces = tmp_path / "kanban" / "workspaces"
|
||||
db.create_session(session_id="legacy", source="cli", cwd=str(workspaces / "t_b21733fb"))
|
||||
db.create_session(session_id="legacy2", source="cli", cwd=str(workspaces / "t_c0ffee"))
|
||||
db.create_session(session_id="mine", source="cli", cwd=str(tmp_path / "www" / "repo"))
|
||||
|
||||
assert db.retag_kanban_worker_sessions(str(workspaces)) == 2
|
||||
|
||||
sources = {row[0]: row[1] for row in db._conn.execute("SELECT id, source FROM sessions")}
|
||||
assert sources == {"legacy": "kanban", "legacy2": "kanban", "mine": "cli"}
|
||||
|
||||
|
||||
def test_retag_runs_once_per_workspaces_root(db, tmp_path):
|
||||
"""The state_meta gate keeps the retag off every subsequent spawn."""
|
||||
workspaces = tmp_path / "kanban" / "workspaces"
|
||||
db.create_session(session_id="legacy", source="cli", cwd=str(workspaces / "t_a"))
|
||||
db.retag_kanban_worker_sessions(str(workspaces))
|
||||
|
||||
# A row that a *new* worker would never write as `cli`; if the gate leaked,
|
||||
# a later sweep would grab it too.
|
||||
db.create_session(session_id="later", source="cli", cwd=str(workspaces / "t_b"))
|
||||
|
||||
assert db.retag_kanban_worker_sessions(str(workspaces)) == 0
|
||||
row = db._conn.execute("SELECT source FROM sessions WHERE id = 'later'").fetchone()
|
||||
assert row[0] == "cli"
|
||||
|
||||
|
||||
def test_retag_gate_is_per_board(db, tmp_path):
|
||||
"""A second board's workspaces root still gets its own sweep.
|
||||
|
||||
The gate is keyed on the root, so reclaiming board A must not convince the
|
||||
dispatcher that board B's legacy rows were already handled.
|
||||
"""
|
||||
board_a = tmp_path / "kanban" / "boards" / "a" / "workspaces"
|
||||
board_b = tmp_path / "kanban" / "boards" / "b" / "workspaces"
|
||||
db.create_session(session_id="a1", source="cli", cwd=str(board_a / "t_a"))
|
||||
db.create_session(session_id="b1", source="cli", cwd=str(board_b / "t_b"))
|
||||
|
||||
assert db.retag_kanban_worker_sessions(str(board_a)) == 1
|
||||
assert db.retag_kanban_worker_sessions(str(board_b)) == 1
|
||||
|
|
@ -35,9 +35,9 @@ from typing import Any, Dict, List, Optional, Union
|
|||
|
||||
# Sources that are excluded from session browsing/searching by default.
|
||||
# Third-party integrations tag their sessions with HERMES_SESSION_SOURCE=tool;
|
||||
# delegate subagent runs are tagged "subagent" — neither belongs in the
|
||||
# user's session history.
|
||||
_HIDDEN_SESSION_SOURCES = ("subagent", "tool")
|
||||
# delegate subagent runs are tagged "subagent"; kanban dispatcher workers are
|
||||
# tagged "kanban" — none belongs in the user's session history.
|
||||
_HIDDEN_SESSION_SOURCES = ("kanban", "subagent", "tool")
|
||||
|
||||
# Automation sources that are kept searchable but DEMOTED below interactive
|
||||
# sessions in discover ranking. Cron jobs run on a schedule and accumulate
|
||||
|
|
|
|||
|
|
@ -170,10 +170,11 @@ def _(rid, params: dict) -> dict:
|
|||
# ones not enumerated here), ACP adapter clients, webhook sessions,
|
||||
# custom `HERMES_SESSION_SOURCE` values, and older installs with
|
||||
# different source labels. We deny-list only the noisy internal
|
||||
# sources (``tool`` sub-agent runs) rather than allow-listing a
|
||||
# fixed set of platform names that goes stale whenever a new
|
||||
# platform is added or a user names their own source.
|
||||
deny = frozenset({"tool"})
|
||||
# sources (``tool`` sub-agent runs and ``kanban`` dispatcher
|
||||
# workers) rather than allow-listing a fixed set of platform names
|
||||
# that goes stale whenever a new platform is added or a user names
|
||||
# their own source.
|
||||
deny = frozenset({"kanban", "tool"})
|
||||
|
||||
limit = int(params.get("limit", 200) or 200)
|
||||
# Over-fetch modestly so per-source filtering doesn't leave us
|
||||
|
|
@ -215,7 +216,7 @@ def _(rid, params: dict) -> dict:
|
|||
"""Return the most recent human-facing session id, or ``None``.
|
||||
|
||||
Mirrors ``session.list``'s deny-list behaviour (drops ``tool``
|
||||
sub-agent rows). Used by TUI auto-resume when
|
||||
sub-agent rows and ``kanban`` worker rows). Used by TUI auto-resume when
|
||||
``display.tui_auto_resume_recent`` is on; the field is also handy
|
||||
for any CLI tooling that wants "latest session" without paginating
|
||||
the full list.
|
||||
|
|
@ -232,7 +233,7 @@ def _(rid, params: dict) -> dict:
|
|||
if db is None:
|
||||
return _ok(rid, {"session_id": None})
|
||||
try:
|
||||
deny = frozenset({"tool"})
|
||||
deny = frozenset({"kanban", "tool"})
|
||||
# Over-fetch by a generous bounded amount so heavy sub-agent
|
||||
# users (lots of recent ``tool`` rows) don't get a false
|
||||
# "no eligible session" answer. ``session.list`` uses a
|
||||
|
|
|
|||
|
|
@ -618,7 +618,7 @@ def _transfer_active_session_slot(
|
|||
# TUI backend itself creates ("tui", plus whatever a client passes as its
|
||||
# own ``source``) and the CLI's own sessions are NOT gateway-owned.
|
||||
_NON_GATEWAY_SOURCES = frozenset({
|
||||
"", "tui", "cli", "webui", "desktop", "cron", "subagent", "test",
|
||||
"", "tui", "cli", "webui", "desktop", "cron", "kanban", "subagent", "test",
|
||||
"local", "acp", "webhook", "api_server", "msgraph_webhook",
|
||||
})
|
||||
|
||||
|
|
@ -11033,10 +11033,11 @@ def _discover_repos_payload(
|
|||
return out
|
||||
|
||||
|
||||
# Sources excluded from the project tree: cron runs and tool/subagent children
|
||||
# are not user conversations. Subagent/compression children are already dropped
|
||||
# by list_sessions_rich(include_children=False); cron has its own section.
|
||||
_PROJECT_TREE_EXCLUDED_SOURCES = ["cron"]
|
||||
# Sources excluded from the project tree: cron runs, and kanban dispatcher
|
||||
# workers, are not user conversations. Subagent/compression children are
|
||||
# already dropped by list_sessions_rich(include_children=False); cron has its
|
||||
# own section, and kanban runs are read on the board.
|
||||
_PROJECT_TREE_EXCLUDED_SOURCES = ["cron", "kanban"]
|
||||
|
||||
|
||||
def _project_tree_row(r: dict) -> dict:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue