The skill detail drawer could only show and edit SKILL.md; the bundle
files under scripts/ and references/ had no console surface, and
templates/ was readable by agents but absent from the canonical
store's bucket set.
- admin endpoints on /api/v1/skills/{id}/files: list (self-heals an
empty canonical store from on-disk files), read, upsert, delete.
Writes update the canonical row, materialize the workspace cache,
and re-resolve the skill so agents pick changes up immediately.
Path envelope enforces the three buckets and blocks traversal;
builtin skill files stay read-only; virtual skills own no files.
- templates/ becomes a first-class DB-persisted bucket shared across
the syncer, the workspace write/delete envelope, and prune guards.
- the agent-facing write_file action now mirrors into the canonical
store and re-resolves instead of writing only the local filesystem.
- SkillMarket detail drawer gains a Files tab: grouped list, viewer,
inline editor, create and delete, refetched on every entry.
The runtime resolved SKILL.md from the workspace directory while the
admin console read the skill_content column, so out-of-band file edits
(agent shell tools in a chat session) changed runtime behavior but never
showed up in the console, and a failed workspace export left agents
executing stale content the console claimed was current.
- SkillContentReconciler: three-way sync between the canonical DB column
and the convention-workspace file, anchored on a sidecar hash marker.
File-side edits ingest into the DB, DB-side edits materialize to the
file, two-sided conflicts resolve DB-wins with a backup.
- Skill detail GET performs a read-time reconcile and triggers a
single-skill rescan when the file side changed, so a console query is
always current without waiting for the runtime cache TTL.
- SkillMarket detail drawer refetches the row and runtime status on open
instead of rendering the page-load list snapshot.
The chat page is kept alive by the router so navigating away and back
only fires an activation hook, not a fresh mount. An agent created,
edited, or deleted elsewhere (e.g. the employee management page) never
reached the chat page's own agent list, and stayed invisible or
unselectable in the agent picker until a full page reload forced a
fresh mount. The agent list is now refetched on every reactivation.
Entity extraction previously constrained entity types but let the
model freely invent any relation between entities, producing noise
that diluted the entities a knowledge base actually cares about.
Adds an optional per-KB relation schema (subjectType/predicate/
objectType triples): when set, the extraction prompt is scoped to
only those relations, and a hard filter drops anything that slips
through before it is persisted. Empty/unset keeps the existing
open-vocabulary behaviour.
In the "Edit Agent → Preferred Providers" tab the same (provider, model)
combination could be selected repeatedly — e.g. two rows of the same
provider both pointing at the same model, or two "provider default" rows.
This is unintended: a provider may repeat across the fallback chain, but
each (provider, model) should stay unique.
Root cause: addProviderEntry() pushed unconditionally (the code comment
even said "we never dedup here") and the model <option>s had no disabled
state, so already-chosen models remained selectable.
Fix (Agents.vue):
- isProviderChoiceTaken(): detect whether a (provider, model) slot — or the
provider-default slot (modelId === null) — is already used in another row.
- Model <option> + the default-model option are :disabled when already taken;
the current row's own value stays selectable (exceptIdx).
- addProviderEntry(): take the default slot if free, else the first unused
model; do nothing if every option is taken.
- The "+ Provider" pool button is disabled once the provider has no free
(provider, model) slot left, so the click is never a silent no-op.
The existing unique index uk_agent_provider_model(agent_id, provider_id,
model_id) already guards non-null duplicates at the DB level, but it cannot
catch model_id IS NULL rows (SQL treats NULLs as distinct); the UI is now
the single source of truth for that.
Fixes#530
A tool/MCP call could render 2+ times in the timeline (issue #521). The
tool is invoked once — this is a display artifact. The segment de-dup in
MessageBubble keyed on `toolName::toolArgs`, which fails two ways:
- The same logical call rendered on both the live SSE stream and the
reloaded/persisted path can carry differing toolArgs strings
(whitespace / key-order from re-serialization), so the two are NOT
de-duplicated and both survive → the reported duplicate.
- Genuine repeated calls of the same tool with identical args (e.g. shell
/ python retries) share the key and get wrongly collapsed to one.
Prefer the LLM-provided toolCallId (carried end-to-end on both live and
persisted segments, stable across serialization) and fall back to
toolName::toolArgs only for legacy segments without an id. This fixes
both the visible duplication and the over-collapse.
Adds pure-function tests for the de-dup logic.
The web ChatConsole seeded a fresh conversation's model from the global
default, never the selected agent's model override, and handleSendMessage
then pinned that default onto the conversation row. Since a conversation
pin outranks the agent override in the backend runtime resolver
(AgentGraphBuilder.resolveRuntimeBaseModel), the model chosen on the
agent edit page was silently clobbered. IM channels and external webchat
were unaffected — they leave the conversation unpinned.
- applyConversationModel now follows the backend precedence:
conversation pin > agent model override > global default.
- The agent tier resolves via /agents/{id}/capabilities (authoritative)
with a synchronous fallback (currentAgent.modelName against the
enabled-model list) so an agent switch, a capability-fetch failure, or
a not-yet-hydrated deep-link still honour the override instead of
dropping to the global default.
- userPickedModel guards the async re-seed from clobbering an explicit
pick; reset on new/switch/delete conversation.
- Unit tests cover the precedence tiers, the empty-string capabilities
wire shape, and the synchronous fallback.