Skip to content

Joysafeter v2#141

Open
yuzzjj wants to merge 460 commits into
mainfrom
joysafeter-v2
Open

Joysafeter v2#141
yuzzjj wants to merge 460 commits into
mainfrom
joysafeter-v2

Conversation

@yuzzjj
Copy link
Copy Markdown
Collaborator

@yuzzjj yuzzjj commented Apr 23, 2026

No description provided.

yuzzjj and others added 30 commits April 24, 2026 19:55
…ture

Implements two new execution engines following the ExecutionEngine protocol:

- CodeEngine: executes user Python code by extracting a StateGraph via
  execute_code() sandbox, compiling, and streaming results
- CopilotEngine: wraps CopilotService streaming and persists copilot
  events as ExecutionEvents through the event bus

Adds dispatch_copilot() to ExecutionOrchestrator with engine override
mechanism, POST /v1/copilot/run endpoint, copilot event types, and
frontend execution bridge hook for copilot UI integration.

Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
…yout

CodeEditorPage is now a self-contained page (parallel to AgentBuilder)
with its own toolbar (Save + Deploy + Run/Stop), execution panel, and
run input modal — matching the graph builder UX without coupling.

- Enable code option in agent creation and version forms
- Show Builder tab for code-mode agents in layout and overview
- Add Deploy button that publishes with runtime_kind="code"
- Wire Run/Stop to executionStore.startExecution() for execution panel
- Simplify CodeEditorToolbar to save-only (run/deploy moved to page)
- deploymentAdapter.deploy() accepts optional runtimeKind parameter

Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
- Delete unused CodeEditorToolbar (dead code after page refactor)
- Remove unused imports: Input, useAgent, useRef from CodeEditorPage
- Use individual Zustand selectors instead of bare destructure to
  prevent re-renders on every execution event
- Remove WHAT comments from JSX
- Extract hasBuilderSupport() helper to deduplicate definition_kind
  checks in layout.tsx and agent-overview-tab.tsx

Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
- AgentBuilder now checks version.definition_kind instead of
  variables.graph_mode to decide CodeEditorPage vs canvas
- useVersionGraphState returns definitionKind from the version
- Slug generation auto-appends suffix on conflict instead of erroring

Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
… new execution architecture

Copilot was still using the old runService (agent_name + graph_id + message)
against a backend that only accepts the new AgentRun schema (release_id +
trigger_source + goal). All old endpoints (/snapshot, /events, /active,
/ws/runs) were 404s. This rewires the entire Copilot chain to use the
already-existing POST /v1/copilot/run → ExecutionOrchestrator.dispatch_copilot
→ execution_event_bus → /ws/executions → useCopilotExecutionBridge pipeline.

- Fix WS field inconsistency: WebSocketSubscriber "data" → "payload"
- Add agent_id/trigger_source/status filters to GET /v1/runs
- Add "copilot" to TriggerSourceLiteral
- Rewrite useCopilotActions to dispatch via copilotService.dispatchRun
- Activate useCopilotExecutionBridge in CopilotPanel (closes event loop)
- Rewrite useCopilotEffects for session restore via agentRunService
- Consolidate executionAdapter to delegate to agentRunService
- Delete runService.ts, runWsClient, test_runs_api.py and all dead code

Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
- Add use-execution-stream.ts to WS protocol task (was missing onStatus→onCompleted)
- Fix route targets: /tasks for param links, /dashboard for back nav only
- Add 'artifact' to ExecutionStepType union
- Add getWsChatUrl cleanup to dead code deletion task
- Simplify component dedup to pure deletion (no imports reference it)

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
…docstring

Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com>
…stream endpoint

Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com>
…, remove phantom execution_status

Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com>
…dlers, remove old pipeline

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
…ter, eventProcessor, getWsChatUrl

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
…n engine_kind

Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com>
…w 404

Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com>
Fixes from spec review:
1. Rename ChatMessage → ConversationMessage to avoid name collision
   with existing ChatMessage in openclaw_chat.py and code_agent/memory.py
2. Add StagedUpload table to bridge file upload (sandbox path) → chat
   attachment (UUID reference) gap
3. Specify seq generation strategy with SELECT FOR UPDATE locking
4. Add full MessageProjectionSubscriber event→message mapping table
5. Update dispatch_chat signature to accept attachment_ids

Also: remove preview_ready (compute on-the-fly), add updated_at column,
note max_length change from 4000→10000, clarify ChatResponse schema
disambiguation.

Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
Previous versions designed new tables (conversation_messages,
message_attachments, staged_uploads) — wrong direction. ExecutionEvent
is already the source of truth for all conversation content.

New design:
- Zero new tables
- Delete ThreadMessage + MessageProjectionSubscriber (redundant projection)
- One new endpoint: GET /v1/threads/{id}/events (aggregation query)
- Extend ChatRequest with attachments (metadata in USER_MESSAGE payload)
- Frontend ChatPanel is a view layer over execution events

Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
1. Pagination: after_seq → cursor-based (event_id), since sequence_no
   resets per execution
2. Attachment payload construction: in router not dispatch_chat, with
   exact code location reference (threads.py lines 185-195)
3. max_length 4000→10000 explicitly called out as intentional
4. ThreadDetailResponse added to deletion scope (embeds MessageResponse)

Also: copilot_* filtered server-side, authorization documented,
file preview raw-mode code path detailed.

Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
…, fix Date.now consistency

- Remove duplicate generateId import (genId alias)
- Remove redundant clearInterrupts() before updateGraphState that already clears
- Capture Date.now() once in onCompleted for consistent endTime/duration
- Switch if/if/if to switch in handleMessage hot path
- Add payload.delta/content fallback for assistant_text events
- Remove WHAT comments, keep only WHY comments

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
8 tasks: backend deletion → schema extension → events endpoint →
file preview → frontend types/hooks → component system → wiring → verification.

Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
- agent-overview-tab: add cn import, type ActivityItem.status as AgentRunStatus, include current_execution_id
- recent-tasks: replace .toSorted() (ES2023) with .slice().sort()
- AgentBuilder/CodeEditorPage: fix import path for deleted graph-builder/components/execution/
- en.ts/zh.ts: rename duplicate chat.untitled key to chat.untitledCode

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
…lter syntax

1. Add dependency note: Task 1 removes get_thread_with_messages,
   Task 2 must update the router handler that calls it
2. Clarify artifact download endpoint already exists (no new task)
3. Fix SQLAlchemy copilot filter: not_(like) instead of ~startswith
4. Add adapter read fallback guidance
5. Explicit ThreadDetail deletion lines in Task 5

Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
…ith attachments

Remove the redundant ThreadMessage model, MessageProjectionSubscriber,
and all dependent code (ThreadMessageRepository, list_messages endpoint,
get_thread_with_messages, MessageResponse/ThreadDetailResponse schemas).
ExecutionEvent is now the sole source of truth for chat history.

Extend ChatRequest with optional attachments (up to 10 ChatAttachment
items) and increase max message length from 4000 to 10000 characters.
Attachments are serialized into the USER_MESSAGE event payload.

Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
storage_ref is the sandbox path from /v1/files/upload response.
size_bytes is required (not optional).

Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
Add ?mode=raw query parameter to GET /v1/files/read/{filename} that
returns raw binary bytes with correct Content-Type header instead of
JSON-wrapped text. This enables inline image/PDF preview in the chat UI
via <img src="/api/v1/files/read/photo.png?mode=raw">.

Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
Aggregates execution events across all runs in a thread.
Cursor-based pagination via ?after=<event_id>.
Filters out copilot_* events server-side.

Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
- ThreadEvent type replaces ThreadMessage
- threadService.listThreadEvents and sendChat replace message methods
- useThreadEvents, useChatSend, useChatStream hooks

Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
ChatPanel, ChatHistory, ChatEventBubble, ChatInput, ChatFilePreview,
AttachmentChip, ThreadSidebar — chat UI rendering execution events
as conversation bubbles.

Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
yuzzjj and others added 30 commits April 30, 2026 18:05
Replaces confusing DefinitionKindLiteral/RuntimeKindLiteral/executor_kind
with two clean orthogonal axes: EngineKind (what kernel) and RuntimeKind
(where it runs).

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
10-task plan covering contracts, engine registry, models, schemas,
services, migration, frontend types and components.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
…_kind in models

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
…consolidate overrides

Fixes missed files (schemas/execution, services/agent_service,
agent_version_service, execution_service, frontend execution page)
and consolidates executor_kind_override into engine_kind_override
in the orchestrator.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
Replaces TriggerSourceLiteral with two orthogonal axes:
- TriggerMediumLiteral: how the run was triggered (api, scheduler, system, ui)
- RunPurposeLiteral: why the run exists (production, draft_test, debug, internal_builder)

Updates contracts, models, schemas, repositories, services, and API layer.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
…e_kind refactors

- 1a2b3c4d5e6f: unify agent_run metadata (trigger_source → trigger_medium + run_purpose)
- b2af1f3e0215: refactor agent kinds (definition_kind → engine_kind, executor_kind → engine_kind)

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
Removes deprecated assignee_type/assignee_id aliases and
TERMINAL_TASK_STATUSES export from frontend task types.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
…frontend

Renames trigger_source to trigger_medium/run_purpose in:
- execution_runner.py EventContext construction
- coordinator_tools.py sub-run creation
- execution_service.py envelope construction (5 occurrences)
- agentRunService.ts list params
- useCopilotEffects.ts copilot session restore
- executionAdapter.ts run creation

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
…ontext wiring, and fix React hook dependencies and rendering logic
…on-scoped trace listing

Foundation for Agent multi-turn full-stack tracing. Establishes Thread as the
session root across dispatch, traces, and the debug UI, without yet touching
the container pool or Trace/Collector placement (reserved for P1-P3).

Backend
- executions/debug + agent-runs/draft accept thread_id, propagated through
  DispatchService → ExecutionOrchestrator → AgentRun.thread_id
- dispatch_debug sets Trace.session_id = str(thread_id) so multi-turn debug
  runs aggregate under one session (fallback "debug-{user}-{version}-{date}"
  kept only when thread_id is absent; goes away once thread_id is NOT NULL)
- traces.list_traces: new session_id query param; the frontend was already
  sending it but the backend silently ignored it
- ExecutionOrchestrator._require_no_active_run(thread_id): centralized guard
  enforcing the "one active run per thread" invariant; hooked into
  _create_and_fire and _create_and_fire_draft
- threads/chat: inline active-run check removed; orchestrator is now the sole
  enforcement point

Frontend
- executionAdapter / executionStore / types carry threadId end-to-end
- DebugPanel creates a Thread on first turn, reuses it for follow-ups, filters
  trace history by session_id, exposes turn count and a "New Session" action
  that archives the old thread
- DebugToolbar surfaces turn count, renders trace history as "Turn N — <time>"
  when in a multi-turn session
…itional full-stack tracing

Engine-level multi-turn (decision B): same Thread reuses container + CLI
session across turns; Trace/Collector now apply to every dispatch path.

Schema
- Migration c3bf20d4e6a7 "thread_as_session": adds threads.container_id,
  cli_session_id, last_active_at (NOT NULL DEFAULT now()), and LRU index;
  agent_runs.thread_id tightened to NOT NULL (orphans deleted); tasks gains
  thread_id (nullable for this PR; PR3 tightens to NOT NULL alongside task
  creation that auto-provisions a Thread).

ContainerPool — rewritten around Thread
- key: thread_id (was release.id)
- API: acquire / release / store_session / evict (old get/put/set_session_id/
  remove removed). Old call shapes deleted, not kept as compat shims.
- DB as source of truth: cache miss lazily adopts threads.container_id if the
  container is still running via docker inspect; stale bindings cleared.
- LRU eviction only touches idle entries (active_count == 0); running
  executions are never evicted. Global max_containers hard cap (default 50).
- On shutdown containers are left running so they can be adopted after restart.

ExecutionRunner
- container_pool.acquire(run.thread_id, create_fn) replaces the release-keyed
  get+put+remove dance. Provisioning is a callback so the pool owns the
  adopt / cap / race logic.
- CLI session id persisted via store_session(thread_id, sid) after each turn;
  next turn on the same thread resumes via --resume.
- _cleanup_container removed — every container now belongs to the pool, so
  there is no "destroy on no-release" branch.

Orchestrator
- All dispatch_* thread_id parameters are now required (no Optional = None):
  dispatch_chat (already so), dispatch_debug, dispatch_draft, dispatch_direct.
  Matching tightening in DispatchService + agent_run / executions schemas.
- dispatch_task: Task lacking a thread_id lazy-provisions one via
  _ensure_task_thread (title inherited from the task). Runs bind to that
  thread so retries / follow-ups share container + CLI session.
- dispatch_copilot_draft provisions an anonymous Thread so the invariant
  "every run has a thread" holds for copilot interactions as well.
- _require_no_active_run: removed the thread_id is None fallback — invariant
  is now unconditional.

Trace / Collector unification
- _fire_engine builds the Trace row via _ensure_trace before firing, and
  constructs ObservationCollector unconditionally (was gated by debug=True).
  session_id = str(run.thread_id), no more debug-{user}-{version}-{date}
  fallback strings.
- _create_and_fire_draft.debug parameter removed; dispatch_debug no longer
  builds its own Trace.
- Every dispatch path (chat, task, direct, draft, debug, copilot_draft) now
  emits a Trace grouped by thread_id — full-stack tracing coverage.

ThreadService.archive_thread
- Refuses to archive a thread with an active run (THREAD_ACTIVE_RUN_EXISTS).
- Calls container_pool.evict(thread_id) so the container is torn down and
  the DB binding cleared atomically with the status flip.

Frontend
- executionAdapter StartRunParams / StartDraftRunParams threadId required.
- StartDraftExecutionInput.threadId required.
- CodeEditorPage "Run" path: executionStore mints a fresh Thread per click
  so the build-page Run button has a session root (same model as Debug).
- DebugPanel: Thread is provisioned on mount (replacing the per-turn "create
  if null" branch). New Session archives the thread — backend evicts the
  container — and the useEffect re-provisions a fresh one.
…nous provisioning)

Closes the last gap in the Thread-as-Session model. Tasks are no longer
allowed to exist without an agent or a thread, so the engine side can assume
every dispatch_task call has a complete session root to operate on.

Schema (migration d4c031e5f7b8)
- Deletes tasks with a NULL agent_id or thread_id (greenfield — nobody should
  have them after PR2, but the DELETE makes the upgrade idempotent).
- tasks.agent_id and tasks.thread_id are ALTERed to NOT NULL.

Model + schema
- Task.agent_id / Task.thread_id typed as non-optional Mapped[uuid.UUID].
- CreateTaskRequest.agent_id required; TaskSummary exposes thread_id as a
  required field so callers always see the session root.

Service
- TaskService.create_task provisions a Thread synchronously in the same
  transaction as the Task insert. Thread title mirrors task title; agent_id
  inherits from the task. No lazy path remains.
- ExecutionOrchestrator._ensure_task_thread removed (dead after the Task
  constructor guarantees thread_id). dispatch_task reads task.thread_id
  directly and forwards it as the run's thread_id.

Effect
- Task retries/follow-ups now always land on the same Thread → same
  container → same CLI session, preserving the agent's memory across
  attempts. Combined with PR2, every run in the system has engine-level
  multi-turn continuity.
…eplay

When the underlying CLI rejects --resume (session expired, container
restart, TTL eviction), the run no longer fails. Instead the ExecutionRunner
evicts the stale session, rebuilds the prompt from this Thread's completed
turns, and retries once without --resume — preserving the agent's apparent
memory across CLI-side session loss.

Contract
- CLIResult gains a session_invalid: bool flag (providers' responsibility to
  set when their resume id was rejected). This keeps string-matching out of
  the Runner. CLISessionInvalidError is available in base.py as a sibling
  signal path for providers that prefer exceptions; not used by Runner today
  but documents the intent.
- claude_code provider detects "session not found" / "no such session" /
  "session expired" / "session does not exist" in the combined stdout+stderr
  when the exit was non-zero with a resume id set, and flips session_invalid.
  Codex and openclaw don't use --resume, so they don't need detection.

Reader port
- ExecutionReaderPort.load_thread_history(thread_id, before_run_id) returns
  ordered (role, content) pairs drawn from completed AgentRuns on the Thread.
  User turns come from AgentRun.goal; assistant turns are concatenated
  ASSISTANT_TEXT event payloads.
- ExecutionReaderAdapter implements it via a straightforward SELECT chain.

Runner recovery loop
- The provider.execute / drain / await-result path is wrapped in a single
  retry loop. On session_invalid at attempt 1 with a prior session, the
  Runner calls container_pool.store_session(thread_id, "") to purge the bad
  id, logs a "cli_session_recovered" observation (WARNING level) through the
  collector, reloads thread history, rebuilds the prompt via _rebuild_prompt,
  and retries with resume_session_id=None.
- _rebuild_prompt concatenates prior turns verbatim with "User:" / "Assistant:"
  markers (raw-text strategy — long sessions eat more tokens on recovery but
  correctness is preserved and no summariser is needed).
…n browsing

Observation panel now shows a horizontal timeline of turns in the active
Thread, replacing the History dropdown that only surfaced a flat list.

New component
- TurnTimeline: horizontal strip of per-turn chips, ordered chronologically
  (Turn 1 leftmost). Active turn is highlighted; the trailing chip pulses
  when its execution is still live. Clicking a chip selects that turn.

DebugPanel wiring
- timelineTurns memo: reverses the DESC-sorted /traces response once so the
  timeline reads chronologically and Turn N matches the Nth execution.
- activeTraceId state tracks which turn's trace is in the observation panel;
  updated in three places:
    * handleStartDebug — set to the new execution_id so the in-flight turn
      lights up before /traces refetches.
    * handleSelectTrace — set to the clicked turn (replay mode).
    * handleNewSession — cleared alongside the other session state.
- Auto-switch behavior: starting a new turn immediately jumps the timeline
  to that turn (user's chosen behavior) while leaving past turns one click
  away.

DebugToolbar cleanup
- Removed the History <select> — TurnTimeline covers the same navigation
  more directly. Props trimmed: onSelectTrace, traces no longer needed.
…ies and switch orchestrator to commit trace records
Review agents flagged a mix of correctness, concurrency, and quality issues
across the Thread-as-Session PR chain. This commit folds in the must-fix
items plus the obvious quality wins; performance-only suggestions
(write-amplification debouncing, untracked eviction task handle) and the
deeper refactors (cascade FKs, Thread.kind column) are noted for follow-up.

## must-fix

- **load_thread_history was always returning []** — the filter used
  `status == "completed"` but the AgentRun enum is
  `pending/running/succeeded/failed/cancelled`. Session recovery would
  silently replay without any prior turns. Now uses "succeeded", collapses
  the N+1 into a single IN query over all runs' assistant-text events, and
  caps at max_turns=20 so very long threads don't blow the prompt budget.
- **Trace cross-session race** — _ensure_trace used to write the Trace row
  in the orchestrator's session, but _run_engine opens a fresh
  AsyncSessionLocal for everything else. Observations FK'd to a row that
  might not be visible yet (or at all, if the caller rolled back). Renamed
  to _insert_trace and moved the insert inside _run_engine's session.
  Dropped _version_id_for_release — callers have the version loaded already.
- **Active-run race at dispatch** — the SELECT-then-INSERT check could let
  two concurrent dispatches both pass and both create pending runs. New
  migration e5d14297a8c9 adds a partial unique index
  `uq_agent_runs_active_per_thread (thread_id) WHERE status IN
  ('pending','running')`. The service keeps the SELECT as a fast-path and
  now catches IntegrityError as the same THREAD_ACTIVE_RUN_EXISTS error.

## quality

- **ACTIVE_RUN_STATUSES constant** — extracted from the two stringly-typed
  literal tuples in the orchestrator and thread_service.
- **ExecutionRunner retry clarity** — "while True + attempt counter that
  always breaks after iteration 2" replaced with straight-line first-try /
  recover / retry, factored into _run_one_attempt. Container is acquired
  once, released once in finally; the comment makes the invariant explicit.
- **DebugPanel redundant state** — activeTraceId was set in three places
  alongside mode + executionId/replayTraceId. Derived it now
  (`mode==='live' ? executionId : mode==='replay' ? replayTraceId : null`)
  and dropped the state.
- **DebugPanel Thread hooks** — useCreateThread / useArchiveThread from
  hooks/queries/threads.ts replace the hand-rolled useEffect+cancelled
  flag and direct threadService.archive fire-and-forget. Cache
  invalidation comes for free.
- **Magic setTimeout(refetchTraces, 1000)** — replaced with
  queryClient.invalidateQueries; Trace is now inserted synchronously in
  _run_engine so the refetch sees it immediately.
- **Dead DebugToolbar props** — agentId/agentVersionId/workspaceId were
  renamed with underscore prefix but never read. Removed from interface and
  call site.
- **startExecution in-flight guard** — module-level Set<graphId> prevents
  rapid double-clicks from minting two Threads before isExecuting flips.
- **Import consistency** — `datetime.now(timezone.utc)` replaced with the
  project's utc_now() in _insert_trace.
- **timelineTurns memo dropped** — ≤20 items reversed per render is cheaper
  than the memo overhead.
Second pass on the review punch-list — the items that didn't fit in the
first cleanup commit.

- **Tracked pool eviction tasks** — ContainerPool._spawn_cleanup replaces
  the bare asyncio.create_task fire-and-forget. Tasks are retained in a
  set, exceptions surface via add_done_callback, and shutdown() awaits
  them so we don't write to a DB engine that's tearing down.
- **TurnTimeline uses Button primitive** — swapped the hand-rolled button
  for `<Button variant={isActive ? 'default' : 'outline'} size="sm">`
  with a height override; the live-pulse dot now inherits foreground
  colour so it reads as part of the chip instead of floating in primary.
- **Static threadService import** — lifted `import('@/services/threadService')`
  out of the startExecution inline dynamic import; it's not conditional
  anymore and there's nothing to code-split here.
Every acquire-hit / release / adopt used to fire a one-row UPDATE on
threads.last_active_at. For a chat session that's 2-3 DB round-trips per
turn purely for a timestamp — the reaper uses this column at 30-min
granularity, so sub-second freshness is wasted I/O.

ContainerPool now debounces those flushes to at most one per 60 s per
thread. PoolEntry gains a last_flushed wall-clock timestamp; the in-memory
last_used still updates synchronously, only the DB write is rate-limited.
Provision seeds last_flushed=now because _persist_container just wrote the
row; adopt leaves it at 0 so the stale DB timestamp gets refreshed on the
first touch after boot.

Idle-eviction window (idle_timeout, default 30 min) widened beyond the
60 s debounce, so no entry is prematurely reaped.
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