383 KiB
AI Runtime Contract
Contract Metadata
{
"subsystem_id": "ai-runtime",
"lane": "L6",
"contract_file": "docs/release-control/v6/internal/subsystems/ai-runtime.md",
"status_file": "docs/release-control/v6/internal/status.json",
"registry_file": "docs/release-control/v6/internal/subsystems/registry.json",
"dependency_subsystem_ids": [
"api-contracts",
"cloud-paid",
"frontend-primitives"
]
}
Purpose
Own the Pulse Intelligence Core: canonical context, governed actions, safety
gates, approval state, action audit, and verification. That core backs Pulse
Patrol as the primary built-in operator that checks infrastructure, investigates
issues, follows the chosen Patrol mode before acting, verifies outcomes, and
records what happened. Pulse Assistant is the contextual
explanation, approval, and handoff access path over that same work, while the
Pulse MCP adapter is the external-agent access path over canonical API
contracts. The subsystem also owns AI orchestration, runtime cost control,
shared AI transport surfaces, and browser-visible Assistant transcript actions
that define what visible operator/model text can leave the transcript without
exposing hidden provider/tool metadata.
App-shell Patrol chrome may expose only content-free current-work pressure, such
as an open-work count on the stable Patrol tab, from Patrol-owned findings and
approval read models. It must not rename the destination, create a second
findings queue, expose finding or approval identity in shell chrome, or turn
platform pages into Patrol summaries.
When backend Patrol summaries or Assistant handoffs expose run coverage to the
browser, they translate internal full-run, scoped-run, and verification
precision into plain operator check language rather than activation-loop,
proof-strip, or scoped/full-run jargon.
Coverage warnings must stay action-oriented: they may tell the operator to run
Patrol to check everything, but must not ask for a "full issue list" or otherwise
expose backend full-run vocabulary as the product instruction.
Patrol model-facing system prompts must derive the active control-mode block
from the effective patrol_autonomy_level, after license and full-mode-lock
clamping, so Watch only, Ask first, Safe auto-fix, and Autopilot give the model
the same investigate, approval, execution, and verification boundaries enforced
by the orchestrator. Prompt copy must not fall back to the legacy
patrol_auto_fix boolean or the retired "observe only" / "auto-fix mode"
framing.
Patrol finding storage must consolidate equivalent active storage-capacity
siblings before they reach browser surfaces: a broader storage risk and a
generic usage/capacity finding for the same normalized storage identity are one
operator issue, while distinct backup, health, and pool-vs-device findings
remain separate. This consolidation belongs in the finding store, not the
frontend row renderer.
Capacity forecasts (days-to-full, current utilization, daily change rate) are
computed deterministically by linear regression over a resource's utilization
history and must persist as structured data on the Finding
(capacity_forecast), stamped by the finding store after a successful Patrol
analysis via StampCapacityForecasts. That structured field is the canonical
operator urgency source for capacity-relevant findings; the model-authored
description must never be treated as the urgency signal when a deterministic
forecast is present. A forecast requires the resource's utilization to be
ingested as a time-series in metrics history; resources without recorded
history cannot produce a forecast and must report its absence honestly rather
than fabricate one, and stable-high (>=80%) pools with no clear fill trend must
carry the deterministic "no fill trend" reading so the model's speculation
cannot override the verified trend. The forecast must query utilization
history under the resource's metrics-target ID (the same key the monitoring
layer records under, exposed as StoragePoolView.SourceID()), not the
canonical resource ID; the two diverge for most storage sources, so querying
the canonical ID silently leaves the feature dormant. The forecast's own
resourceID stays canonical so it can still be stamped onto findings keyed by
canonical resource ID.
The public Pulse Intelligence overview is a projection of those runtime
contracts, not a second Assistant tool inventory. It must point readers at the
registry-owned Assistant tool governance and the manifest-owned external-agent
capabilities instead of carrying hand-maintained tool tables.
It is also the public product-language contract for paid Patrol capabilities:
Pro positioning must describe hands-on Patrol modes, issue investigation,
governed fixes, verified outcomes, and history. Public docs and generated
commercial copy must not reintroduce proof-first activation-loop framing or the
retired Patrol Control Levels, Patrol control, and Alert investigation
labels as the visible paid story. API fields and route markers may retain
compatibility names such as autonomy_level only when the surrounding copy
plainly calls the operator-facing choice Patrol mode.
Its Core/Patrol/Assistant/MCP relationship block must be rendered from
PulseIntelligenceOverviewMarkdown(Manifest.SurfaceContract) rather than
maintained as prose separate from the agent capabilities manifest.
Reusable workflow starters are manifest-owned workflowPrompts metadata
derived from the shared Pulse Intelligence workflow-prompt core. Prompt names,
Assistant display labels, presentation kind hints, descriptions, arguments,
availability, and rendered text belong to that shared catalogue. Pulse
Assistant-compatible starters, frontend manifest clients, MCP prompts/list,
and generated docs consume that catalogue instead of maintaining per-surface
prompt lists or browser-local starter copy.
The shared catalogue includes the global pulse_operations_loop workflow
starter for the Patrol mode operations loop: it is projected only when the
manifest has fleet context, resource context, findings, governed
plan/decide/execute action, and finding resolution capabilities. Its rendered
prompt must keep the operator in an approve-or-reject loop with execution only
after policy allows it and verification before finding resolution, while the
status schema treats Patrol mode starter evidence only as entry-point
orientation, Patrol mode completed-loop evidence as aggregate terminal
approve/reject proof, and Patrol mode resolved-loop evidence only as
aggregate approved-and-verified outcome proof, not Assistant transcript, Patrol
finding identity, approval payload, action command, or MCP setup detail.
pulse_patrol, the current paid patrol_control marker, the legacy
patrol_autonomy marker, and the legacy pulse_pro_activation alias all
contribute to first-party Patrol mode starter evidence;
proActivationOperationsLoopStarterCount is a compatibility field retained
for older external-agent clients, while primary clients must read
patrolControlOperationsLoopStarterCount. The other proActivation* status
fields are compatibility aliases for the same first-party Patrol mode
journey rather than a separate AI-runtime loop, and manifest descriptions must
not describe them as a Pro activation journey.
The same status schema must expose the aggregate Patrol mode value state as
a content-safe enum so Assistant-compatible and MCP-facing agents can branch
without reverse-engineering counts. governed_decision_recorded is a safe
terminal decision state, not proven operations value; verified is the only
completed first-party value state and requires the approved, verified outcome
plus recorded action history. MCP/external-agent readiness remains optional
external-agent setup context.
Native Assistant workflow starters in frontend-modern/src/components/AI/Chat/
are a browser projection of that same catalogue, not a second prompt registry:
they may decide which manifest-owned prompt is contextually usable, but display
labels and rendered prompt text must come from shared manifest metadata and the
shared backend renderer on
POST /api/ai/workflow-prompts/render.
AI provider integration is registry-owned. Provider identity, display names,
protocol family, default model, default base URL, credential fields,
configured-state fields, clear-key fields, env-var hints, docs links, and
fallback catalog models belong in the config-layer provider registry, not in
factory branches, settings-card literals, or per-provider model-list code. New
direct chat-compatible providers extend that registry and the shared
chat-compatible provider client; native transports remain explicit only where
the protocol is not chat-compatible.
Provider definitions that declare a BaseURLField (today OpenAI, Ollama, and
Z.ai) expose a user-overridable endpoint via the AI settings payload; the
registry default base URL applies when no override is stored, so one provider
can serve both standard and alternate (e.g. Z.ai coding) endpoint tiers.
Canonical Files
internal/ai/1a.cmd/pulse-mcp/main.go1b.cmd/pulse-mcp/README.md1c.internal/agentcapabilities/1d.scripts/generate-pulse-intelligence-docs.gointernal/config/ai.go2a.internal/config/ai_providers.gointernal/api/ai_handler.gointernal/api/ai_handlers.gointernal/api/ai_hosted_runtime.gointernal/api/ai_intelligence_handlers.gofrontend-modern/src/api/agentCapabilities.ts7a.frontend-modern/src/api/generated/agentCapabilities.tsfrontend-modern/src/api/ai.ts8a.frontend-modern/src/api/aiChat.tsfrontend-modern/src/api/patrol.tsfrontend-modern/src/components/AI/AICostDashboard.tsxfrontend-modern/src/components/Settings/AgentIntegrationsPanel.tsxfrontend-modern/src/components/AI/Chat/frontend-modern/src/utils/aiChatPresentation.tsfrontend-modern/src/utils/assistantPageContext.tsfrontend-modern/src/utils/aiControlLevelPresentation.tsfrontend-modern/src/utils/aiCostPresentation.tsfrontend-modern/src/utils/aiProviderHealthPresentation.tsfrontend-modern/src/utils/aiProviderPresentation.tsfrontend-modern/src/utils/textPresentation.tsfrontend-modern/src/stores/aiRuntimeState.tsfrontend-modern/src/stores/aiChat.tsdocs/AI.mdpkg/aicontracts/investigation.gopkg/aicontracts/orchestrator_deps.gopkg/aicontracts/fix_execution.gopkg/extensions/ai_autofix.go
Shared Boundaries
cmd/pulse-mcp/main.goshared withapi-contracts: the Pulse MCP adapter runtime is both an AI runtime surface for external-agent access to Pulse Intelligence and a canonical API contract projection over the agent capabilities manifest and Pulse MCP surface tool contract.cmd/pulse-mcp/README.mdshared withapi-contracts: the Pulse MCP adapter guide is both an AI runtime surface for external-agent access to Pulse Intelligence and a canonical API contract projection over the agent capabilities manifest.frontend-modern/src/api/agentCapabilities.tsshared withapi-contracts: the agent capabilities frontend client is both the Pulse Intelligence external-agent manifest consumer and a canonical API payload contract boundary. Its presentation sanitizer may translate legacy Pro activation manifest descriptions into Patrol mode outcome/status language, but it must not reintroduce proof-first or activation-loop copy into Assistant, Patrol, or external-agent onboarding surfaces.frontend-modern/src/api/ai.tsshared withapi-contracts: the AI frontend client is both an AI runtime control surface and a canonical API payload contract boundary.frontend-modern/src/api/aiChat.tsshared withapi-contracts: the Assistant chat frontend client is both the first-party Assistant transport surface and a canonical API payload contract boundary.frontend-modern/src/api/generated/agentCapabilities.tsshared withapi-contracts: the generated agent capabilities frontend types are both the Pulse Intelligence manifest TypeScript projection and a canonical API payload contract boundary.frontend-modern/src/api/patrol.tsshared withapi-contracts: the Patrol frontend client is both an AI runtime control surface and a canonical API payload contract boundary.frontend-modern/src/components/Settings/AgentIntegrationsPanel.tsxshared withapi-contracts,frontend-primitives: the External agents settings panel is the optional settings-shell projection of Pulse MCP onboarding, the AI runtime connected-agent onboarding surface, and a presentation consumer of the shared agent capabilities frontend client. Its default copy must frame connected clients as optional connector access to Pulse context and Patrol work, with Patrol as the operator that watches, acts within Patrol mode, asks for approval when required, verifies outcomes, and records history. The normal Assistant settings view must keep setup mechanics behind aShow connector setupdisclosure, with direct setup links opening that disclosure automatically. Default copy must make clear that connected tools do not get separate powers. Tool-contract posture and manifest facts are client-builder diagnostics and must stay behind a Developer details disclosure rather than beside the normal external-agent setup steps; posture and policy context belongs under Patrol access model, while prompt, scope, failure-code, and tool inventories must sit one level deeper behind Live manifest details using user-facing labels such as Agent starting points and Agent capabilities. Direct links to/settings/pulse-intelligence/assistant#external-agent-setupmust focus this External agents panel after the Assistant settings layout settles, because connected agents are optional access to Patrol work and should not strand users at the generic token inventory. Legacy/settings/security/api#external-agent-setupand/settings/security/api#pulse-mcp-setuplinks must remain accepted and redirect to the canonical Pulse Intelligence Assistant route. TheChoose Patrol modehandoff must target the Patrol operator surface at/patrol#patrol-control, where the mode is configured, while API Access remains the token and external-client setup surface. The expanded setup checklist owns that handoff as Step 1; the panel header must not repeat a second visibleChoose Patrol modeaction while setup is open. The visible full-surface token preset for that setup isPatrol external agent;pulse_intelligence_agentremains only the compatibility preset id used by the route and token model.frontend-modern/src/stores/aiChat.tsshared withfrontend-primitives: the assistant drawer and session store is both an AI runtime control surface and a canonical app-shell presentation boundary.internal/agentcapabilities/action_target.goshared withapi-contracts: the Pulse Intelligence governed action target type and resource-to-action-target mapping vocabulary are both the Assistant approval/runtime routing contract and the canonical API/agent target contract for governed actions.internal/agentcapabilities/control_level.goshared withapi-contracts: the Pulse Intelligence control-level vocabulary and control-tool availability predicate are both the Assistant runtime gating contract and the canonical API/agent permission posture for governed action tools.internal/agentcapabilities/errors.goshared withapi-contracts: the Pulse Intelligence agent error envelope is both the canonical API failure payload contract and the AI runtime adapter error-parsing contract for Assistant and external-agent surfaces.internal/agentcapabilities/events.goshared withapi-contracts: the Pulse Intelligence event vocabulary is both the canonical API SSE event contract and the AI runtime adapter notification contract for Assistant and external-agent surfaces.internal/agentcapabilities/governance_prompt.goshared withapi-contracts: the Pulse Intelligence surface-affordance-resolved model-facing operating-instruction, tool-governance prompt, reusable provider-tool governance description, Assistant-native offered-tool filtering, and Assistant-native interactive question-tool governance projections are both the Assistant system-prompt governance section and the shared API/agent vocabulary for action mode, approval posture, MCP affordance advertisement, and non-registry interaction-tool boundaries.internal/agentcapabilities/http.goshared withapi-contracts: the Pulse Intelligence agent HTTP substrate is both the API capabilities invocation contract and the shared AI runtime adapter execution primitive for MCP and reference agent clients.internal/agentcapabilities/manifest.goshared withapi-contracts: the canonical Pulse Intelligence agent capabilities manifest declaration, including capability display titles, manifest-owned finding lifecycle schemas, manifest-owned governed action schemas and routes, manifest-owned external-adapter surface tool contracts, and manifest-owned structured output schemas, is both the API discovery payload source and the AI runtime projection contract for Pulse Assistant and MCP-facing agent tools.internal/agentcapabilities/markdown.goshared withapi-contracts: the Pulse Intelligence manifest Markdown projection, including manifest-owned capability titles, surface-filtered Pulse MCP tool/error inventories, and prompt labels, is both the canonical API/agent documentation projection and the AI runtime onboarding projection for Assistant-compatible external-agent surfaces.internal/agentcapabilities/mcp.goshared withapi-contracts: the Pulse Intelligence MCP protocol version, JSON-RPC, method dispatch, method payload, surface-tool-contract-gated initialize operating-instruction and capability advertisement payload, manifest surface-filtered tools/list and tools/call execution bridge, manifest surface-gated resources/list and resources/read bridge, manifest-owned and surface-affordance-gated workflow prompt projection, protocol wire aliases, resource and prompt handler gates, and notification projection collectively define the external-agent adapter wire contract over the shared Pulse Intelligence tool core; MCP initialize, tools/call execution, resource list/read projection, and prompt list/get projection must enter through manifest-owned surface and workflow-prompt contracts so raw capability slices cannot bypass the published external-adapter contract.internal/agentcapabilities/mcp_adapter.goshared withapi-contracts: the Pulse MCP adapter setup contract defaults and normalization are both the canonical API manifest setup projection and the AI runtime onboarding contract for Assistant-compatible external-agent surfaces.internal/agentcapabilities/projection.goshared withapi-contracts: the agent capability external-tool projection helper, normalized manifest-owned surface tool contract resolution and tools-affordance gating, manifest-owned resource-context route and argument vocabulary, operator-state capability and route vocabulary, finding workflow capability and lifecycle argument vocabulary including resolution and dismissal notes, governed action capability, route, and argument vocabulary, manifest-owned tool title and outputSchema projection, structured Pulse capability _meta, and shared tool behavior hints are both the canonical API manifest projection contract and the AI runtime adapter projection for Pulse Assistant and MCP-facing agent tools, with MCP annotation and metadata wire names confined to adapter-edge aliases.internal/agentcapabilities/provider_tool_artifacts.goshared withapi-contracts: the provider tool-call artifact detector and streaming tool-name prefix splitter are both the Assistant stream-sanitization boundary and the shared external-adapter leak guard for provider-native tool-call markup that escaped the structured channel.internal/agentcapabilities/schema.goshared withapi-contracts: the agent capability input schema contract is both the canonical API manifest schema envelope and the AI runtime structured tool-schema, governance-aware provider-projection with neutral behavior hints and Pulse governance metadata, offered-tool governance extraction for Assistant prompt policy, manifest-affordance-gated Assistant provider-surface composition, manifest raw-schema to Assistant provider-schema projection for capability tools, legacy native Assistant utility provider aliases and schemas, provider-call normalization, provider-result context projection, Assistant-native interaction provider-tool declaration, and live Assistant execution-normalization contract for Pulse Assistant and MCP-facing agent tools.internal/agentcapabilities/scopes.goshared withapi-contracts: the manifest-derived required-scope summary is both the canonical API/agent token guidance contract and the AI runtime adapter startup/onboarding contract for Assistant-compatible external-agent surfaces.internal/agentcapabilities/sse.goshared withapi-contracts: the Pulse Intelligence SSE subscription transport and record parser are both the canonical API event-stream consumption contract and the AI runtime adapter push bridge contract for MCP and reference agent clients.internal/agentcapabilities/surface_contract.goshared withapi-contracts: the Pulse Intelligence operator-surface affordance contract, shared surface-affordance, surface-tool identity, Assistant surface tool filtering, normalized external surface tool resolver, surface lookup, affordance labels, and manifest-published external-adapter surface tool allowlist projection are both the canonical API manifest surface model and the AI runtime prompt and onboarding guardrail for Assistant and MCP-facing surfaces.internal/agentcapabilities/text_tool_invocation.goshared withapi-contracts: the Pulse Intelligence text tool invocation parser, internal approval argument, and current_resource handle vocabulary are both the Assistant approved-action execution projection and the shared tool-call params bridge for governed Pulse Intelligence tool calls, with MCP tools/call compatibility staying at the adapter edge.internal/agentcapabilities/tool_call.goshared withapi-contracts: the Pulse Intelligence shared tool-call params, normalization, validation, direct registry preparation, registry-entrypoint failure result helpers, and provider/registry tool-call safety classification are both the native Assistant execution/FSM contract and the canonical API/agent tools/call compatibility contract for governed Pulse Intelligence tool calls.internal/agentcapabilities/tool_execution.goshared withapi-contracts: the Pulse Intelligence neutral capability tool HTTP execution helper and direct tool execution output/error mapper are both the Assistant-native direct execution contract and the canonical API/agent request/response execution contract, with MCP adapters consuming the neutral helpers only after the shared MCP manifest-surface execution bridge has applied the published surface tool contract.internal/agentcapabilities/tool_marker.goshared withapi-contracts: the Pulse Intelligence Assistant tool marker vocabulary and approval/policy marker parser are both the Assistant structured tool-result compatibility contract and the canonical API/agent branching contract for governed tool outcomes.internal/agentcapabilities/tool_names.goshared withapi-contracts: the Pulse Intelligence registry tool-name vocabulary is both the native Assistant execution/display contract and the canonical API/agent tool identity contract for MCP-facing external-agent adapters.internal/agentcapabilities/tool_response.goshared withapi-contracts: the shared tool response envelope, tool error-code vocabulary, and tool-result error-code and verification evidence parsers are both the Assistant structured tool-result contract and the canonical API/agent branching contract for Pulse Intelligence tool failures, recovery tracking, and write self-verification.internal/agentcapabilities/tool_result.goshared withapi-contracts: the Pulse Intelligence shared tool-result content/result envelope, structuredContent projection, result constructors, HTTP response-to-result mapping, text projection, and result interpretation helpers are both the Assistant registry result contract and the canonical API/agent result projection contract for governed tool outcomes.internal/agentcapabilities/types.goshared withapi-contracts: the agent capabilities manifest wire type, manifest-owned external-adapter surface tool contract field, capability display title and structured output schema fields, approval-policy vocabulary, capability governance normalization, and tool-governance descriptor shape are both the canonical API payload contract and the AI runtime projection contract for Pulse Assistant and MCP-facing agent tools.internal/agentcapabilities/workflow_prompt.goshared withapi-contracts: the Pulse Intelligence workflow prompt catalogue, manifest-ownedworkflowPromptsprojection, MCP prompt title projection, presentation kind hints, shared resource-context and finding argument vocabulary, Patrol issue-handling capability gating, argument validation, and manifest-gated shared prompt rendering rules are both the AI runtime starter contract for Assistant-compatible surfaces and the canonical API/agent prompt projection contract for MCP-facing clients.internal/api/ai_handler.goshared withapi-contracts: Pulse Assistant handlers are both an AI runtime control surface and a canonical API payload contract boundary.internal/api/ai_handlers.goshared withapi-contracts: AI settings and remediation handlers are both an AI runtime control surface and a canonical API payload contract boundary.internal/api/ai_intelligence_handlers.goshared withapi-contracts: AI intelligence handlers are both an AI runtime control surface and a canonical API payload contract boundary.pkg/aicontracts/fix_execution.goshared withapi-contracts: the public approved-fix execution contract is both an AI runtime approved-action boundary and a canonical API dependency contract for Patrol and enterprise auto-fix binders.pkg/aicontracts/investigation.goshared withapi-contracts: the public Patrol investigation record and finding contract is both an AI runtime handoff boundary and a canonical API payload contract for Patrol, Assistant, unified findings, persistence, and audit surfaces.pkg/aicontracts/orchestrator_deps.goshared withapi-contracts: the public investigation orchestrator dependency contract is both an AI runtime handoff boundary and a canonical API payload contract for Assistant and Patrol tool-call history.pkg/extensions/ai_autofix.goshared withapi-contracts: the enterprise auto-fix extension dependency seam is both an AI runtime approved-action boundary and a canonical API extension contract over Assistant and Patrol execution dependencies.scripts/generate-pulse-intelligence-docs.goshared withapi-contracts: the Pulse Intelligence manifest docs generator is both an AI runtime docs/onboarding projection and a canonical API contract projection over the agent capabilities manifest and Pulse MCP surface tool contract.
The shared agent-capabilities manifest also owns the runtime surface contract:
the manifest wire type and generated frontend projection must name Pulse
Intelligence Core as the shared core, Pulse Patrol as the primary built-in
operator, and Pulse Assistant plus Pulse MCP as contextual and external-agent
access paths over the same governed capabilities. Frontend copy, MCP docs, and
Assistant onboarding must consume that relationship from the manifest rather
than carrying parallel surface tables.
The same manifest owns Pulse MCP adapter setup facts: server name, command,
base URL flag/default, token environment variable, and supported client config
families. Settings, README generation, and adapter onboarding may present those
facts for operators, but they must not maintain separate MCP setup constants.
The in-app setup surface for those facts belongs under Pulse Intelligence
settings because Pulse MCP is an external-agent access path over governed
Patrol actions. API Access may be linked for scoped-token creation, but it must
not host the external-agent setup walkthrough.
Pulse Intelligence telemetry readiness for the external-agent surface follows
that same manifest surface boundary: a non-expired API token covering any
Pulse MCP-published capability scope is enough to mark MCP/external-agent
readiness, while route-level usage still comes only from authenticated
capability activity markers. Readiness must not be tied to the full manifest
scope set because least-privilege external-agent setups are supported.
The pulse-mcp adapter marks its own capability requests with the content-free
X-Pulse-Agent-Surface: pulse_mcp header so telemetry can distinguish adapter
use from direct external-agent API use without recording prompts, payloads,
route parameters, token identity, or resource identifiers.
Successful workflow starter rendering follows the same content-free boundary:
native Assistant render calls, first-party paid Patrol autonomy handoffs, and
Pulse MCP prompts/get calls may persist only the manifest prompt name, coarse
surface (pulse_assistant, pulse_patrol, patrol_autonomy, legacy
pulse_pro_activation, or pulse_mcp), and timestamp. The first-party
activation marker is POST /api/ai/workflow-prompts/activity; it is an
authenticated ai:chat route for app-owned surfaces such as the paid Patrol
autonomy handoff into Patrol, validates prompt names against the manifest
workflow prompt catalogue, accepts only first-party surfaces, and must remain a
starter marker rather than proof that Assistant context, governed action
approval, execution, verification, or finding resolution occurred.
pulse_pro_activation exists only as a legacy alias for older Pro
success/current-plan entry points; current paid handoffs use patrol_autonomy
and neither surface adds a new prompt, tool, model surface, or operations-loop
completion signal. Prompt arguments, rendered text, resource
IDs, finding IDs, session IDs, token identity, request bodies, and model output
must not enter workflow starter activity history or outbound telemetry.
Runtime model-facing instructions must consume that same contract as data.
BuildPulseAssistantOperatingInstructions identifies Pulse Assistant as the
native in-app surface over Pulse Intelligence Core, while manifest-backed MCP
initialize responses identify Pulse MCP as the external-agent adapter over the
same core and Pulse Patrol as the primary built-in operator. That copy belongs in
internal/agentcapabilities; Assistant prompts, MCP initialize payloads, and
future agent surfaces must not re-create local Assistant/MCP/Patrol relationship
wording outside the manifest-owned surface contract.
Surface tool summaries follow the same shared-core rule. The native Assistant
runtime exposes provider tools through ProjectPulseAssistantProviderTools,
which applies the manifest-owned Pulse Assistant surface affordances before
registry or native interaction tools reach model requests, and exposes
PulseToolExecutor.AssistantSurfaceToolContract, which projects the same
runtime-available provider tools through ProjectPulseAssistantSurfaceToolContract;
external-agent adapters project request/response manifest capabilities through
ProjectManifestSurfaceToolContract and publish the static external-adapter
inventory on Manifest.SurfaceToolContracts. Missing external-adapter
surfaceToolContracts entries fail closed rather than inferring tools from raw
manifest capabilities; static surface-tool affordance metadata may narrow an
external operator surface, but must not re-enable a disabled surface affordance
or keep tool/capability names when the effective tools affordance is false.
Disabled Assistant affordances likewise fail closed rather than allowing runtime
registry availability to re-enable provider tools.
Assistant prompt fallback governance follows that same contract: when no live
executor-owned offered-tool manifest is available, registry-owned fallback
governance must enter through
CanonicalToolGovernanceForManifestSurface(ControlLevelControlled, CanonicalManifest(), SurfaceIDPulseAssistant),
and the chat prompt fallback must derive explicit offered tool names from
ManifestSurfaceAffordances so registry tools and the native pulse_question
interaction tool cannot be reintroduced by nil offered-tool semantics.
Assistant registry tools, Assistant-native interaction tools, and MCP manifest
capabilities must stay explicit buckets in that contract so Pulse can support
both surfaces without duplicating tool logic or
implying MCP replaces the in-app Assistant.
Frontend consumers must import the generated SurfaceToolContract shape from
the shared agent-capabilities client rather than maintaining a local copy.
Patrol work contract availability for browser surfaces is also a shared
agent-capabilities-client verdict: consumers must derive the Pulse MCP contract
from the normalized MCP adapter setup, the manifest-owned
pulse_operations_loop workflow prompt, and the Pulse MCP surface tool contract
through frontend-modern/src/api/agentCapabilities.ts; feature shells must not
hard-code MCP contract availability or infer it from raw manifest capabilities.
Runtime external-agent readiness is stricter:
GET /api/agent/patrol-control/status may mark externalAgentReady only when
that contract is available and a single non-expired API token covers every
scope required by the published Pulse MCP Patrol work capability set.
GET /api/agent/operations-loop/status remains only a compatibility alias for
older clients. That shared Patrol work tool set starts with
get_patrol_control_status, the count-only
GET /api/agent/patrol-control/status orientation read, then uses the same
fleet-context, resource-context, finding, governed action, and finding
resolution tools as the rest of the governed issue-handling flow.
get_operations_loop_status and /api/agent/operations-loop/status are
compatibility aliases, not primary capability names. AI runtime prompts, MCP
readiness helpers, and frontend presentation helpers must not carry separate
Patrol work inventories. The native Patrol work surface may display that same
status projection, but it must fetch it through the shared agent-capabilities
frontend client so Assistant, MCP, and native UI stay on the same Patrol work
contract. The manifest-owned status schema must describe governed counts as
decision-backed, split approved and rejected decision counts explicitly, and
describe verified counts as approved-action-backed. The same schema now exposes
content-free Patrol work starter counts for the total flow and the
Assistant, Patrol mode, older-client compatibility field, and Pulse MCP
source surfaces, an Assistant step count for contextual Assistant or
external-agent collaboration, plus a content-free Patrol mode
completed-loop count derived when Patrol mode starter evidence, Patrol
issue evidence, contextual collaboration, and either a rejected governed
decision or an approved governed decision with verified outcome proof coexist
in the same status window. It also exposes a stricter Patrol mode
resolved-loop count only when that status window has an approved governed
decision and verified outcome proof. The proActivationOperationsLoopStarterCount
field is retained only for older clients; primary clients must use
patrolControlOperationsLoopStarterCount. The legacy patrolAutonomy* and
proActivation* completed-loop, resolved-loop, and value-state fields mirror
the Patrol mode counts and value state for compatibility. AI runtime
surfaces may use those counts to show that a starter
was rendered or launched, that contextual collaboration occurred, or that the
first-party Patrol mode
loop reached terminal or resolved proof, but must not treat them as Assistant
transcript, approval payload, action command, resource identity, or
finding-resolution detail. External-agent prompts and MCP adapters may treat a
rejected-only decision as a terminal no-execution outcome, but approved
decisions must still continue through execution and verification before the
verified-outcome stage is complete.
Runtime Assistant availability remains an authenticated Assistant concern,
served by GET /api/ai/assistant/surface-tools, not a static fact in the
public agent-capabilities manifest. When the Pulse Assistant shell displays
live capability availability, it must load that authenticated endpoint through
AIChatAPI.getAssistantSurfaceTools() and format the posture through
frontend-modern/src/api/agentCapabilities.ts; the shell must not keep a local
registry-tool catalogue or reuse MCP manifest capability names as native
Assistant inventory. The External agents settings panel may show the
external Pulse MCP tool posture, but it must get the normalized static
SurfaceToolContract from manifest.surfaceToolContracts through
getAgentManifestSurfaceToolContract(manifest, AGENT_SURFACE_ID_PULSE_MCP)
and format it with getAgentSurfaceToolPosturePresentation;
external-adapter request/response filtering, including the subscribe_events
streaming exception and any capability omitted from the published Pulse MCP
surface allowlist, must be owned by the backend projection in
ProjectManifestSurfaceToolContracts; shared capability reads must use
ResolveManifestSurfaceToolContract and ManifestSurfaceToolCapabilities so
the normalized surface contract and the resolved tools affordance gate are
applied once. MCP initialize must also advertise the tools capability and
name "offered tools" in operating instructions only when that same published
surface tool contract resolves. MCP prompt support must similarly enter through
MCPManifestSurfacePromptProjectionSupported, which combines the target
surface's prompts affordance with the manifest-owned workflow prompt
catalogue before initialize or prompts/list advertises prompts, and through
GetMCPPromptFromManifestSurface before prompts/get renders any prompt. The
global pulse_operations_loop prompt is part of that same shared catalogue,
not a native Assistant-only starter: it must appear in Assistant and MCP only
when the manifest can support the full Patrol-to-Assistant-to-governed-action
flow, and it must guide contextual explanation, plan-first governed action,
approval or rejection only when the returned policy requires a decision,
policy-allowed execution, post-action verification, and finding resolution from
the same rendered prompt text. The rendered prompt must orient
first through get_patrol_control_status and re-read that status, resource
context, and findings before treating an outcome as verified. Successful render activity for
that starter is activation evidence for the Patrol issue-handling journey, not
proof of contextual collaboration, governed action execution, verification, or
finding resolution.
Native Assistant callers may request a preferred workflow prompt through safe
browser context, but the drawer must resolve that request against the
manifest-owned workflow prompt catalogue and render it through
/api/ai/workflow-prompts/render; it must not inline prompt templates or
silently append to an existing composer draft. Preferred workflow prompt
requests are one-shot browser seeds and must be cleared with the scoped handoff
payload after the first successful send.
The MCP tools/call execution
helper must
accept the manifest and surface id, apply the same affordance and surface-tool
allowlist used by tools/list, and only then delegate to neutral capability
HTTP execution; it must not accept a caller-supplied raw capability slice as the
MCP execution boundary. Frontend consumers may normalize a published
surfaceToolContracts entry for presentation, but must not infer MCP tools
from raw manifest capabilities when that entry is missing, and must not add a
Pulse-MCP-specific frontend helper alias around the generic surface resolver.
The generated Pulse MCP README follows the same rule: its surface-contract
block must be rendered by MCPSurfaceContractMarkdown from
Manifest.SurfaceContract, alongside the manifest-derived scope, tool, prompt,
and error-code inventories.
The public Pulse Intelligence overview follows the public-docs version of that
rule: the marked overview block in docs/AI.md must be rendered by
PulseIntelligenceOverviewMarkdown from the same manifest-owned
SurfaceContract, while surrounding product detail may remain human-authored.
The External agents settings panel follows the browser-facing version of
that same rule: its Pulse Intelligence Core, Patrol, Assistant, and MCP surface
summary must be projected from /api/agent/capabilities.surfaceContract through
frontend-modern/src/api/agentCapabilities.ts, not maintained as panel-local
copy; surface affordance badges are part of that same manifest projection. Its
capability-publication wording must describe published manifest-owned surface
contracts, not raw backend capability rows, so the panel does not imply that
adding a raw capability automatically exposes it to Pulse MCP.
Its external-agent recovery summary must also be manifest-backed: stable
failure-code chips or summaries may be rendered for setup ergonomics, but they
must derive from capability errorCodes through the shared frontend manifest
client rather than a panel-local recovery-code list.
External-agent token setup copy in that panel must route operators back to the
API Access token creation surface and the manifest-derived Pulse Intelligence agent preset instead of carrying MCP-local token minting logic or a
component-local required-scope list.
Extension Points
The manual Patrol route is an extension boundary for scoped work. POST /api/ai/patrol/run (HandleForcePatrol) accepts an optional scope body
(resource_ids and/or resource_types, plus optional alert_identifier,
alert_type, and context) to run a manual Targeted check through the same
TriggerScopedPatrol engine and scoped run record as automatic alert-triggered
work; with no body it keeps the fleet-wide Patrol check. The scoped path must
reuse the existing scoped engine rather than adding a parallel trigger route,
honour the same Patrol readiness gate as a full run, bypass the full-run
cadence gate (targeted checks never consume a manual full-run allowance), and
carry resource identity only — no command, prompt, or remediation payload —
while the route keeps requiring admin plus ai:execute scope for both shapes.
Pulse Intelligence presents Patrol as the primary first-party operations
surface. Pulse Assistant is the in-app contextual explanation, approval-card,
governed-action, verification, and handoff access path over Patrol work.
cmd/pulse-mcp/ is the external-agent adapter for MCP-speaking clients. The
adapter must stay thin: it fetches /api/agent/capabilities, projects that
canonical manifest as MCP tools, and forwards calls to the declared API routes.
The shared MCPManifestToolServer must therefore consume the manifest as a
unit, not only a detached capability slice, so initialize instructions, tool
projection, resources, prompts, notifications, and future manifest-backed
surface metadata remain coupled to the same discovery document. It must resolve
the target surface's affordance contract before initialize advertisement and
before tool, resource, and prompt handlers run; raw manifest capabilities,
workflow prompts, or context routes are availability inputs only after the
surface contract allows that affordance.
When an adapter renders a manifest prompt locally, it may notify Pulse through
the authenticated POST /api/agent/workflow-prompt-activity marker endpoint
only after the prompt render succeeds. That marker route accepts manifest-owned
prompt names only, requires the token to cover monitoring:read, normalizes
X-Pulse-Agent-Surface: pulse_mcp into the Pulse MCP surface, and records no
prompt arguments, rendered text, resources, findings, token identity, or
request payload.
scripts/generate-pulse-intelligence-docs.go is part of that coupling: it
must update the MCP README's Pulse Intelligence surface contract and the public
docs/AI.md overview from the manifest instead of leaving hand-written
Core/Patrol/Assistant/MCP relationship prose beside generated inventories.
Public AI docs may describe outbound usage telemetry only as counts,
feature flags, and coarse Patrol control and governed Pulse Intelligence
operations adoption flags and counters.
They must keep prompts, chat messages, command text, action output, token
values, resource identifiers, and hostnames explicitly outside that telemetry
scope.
Governed-operation workflow starter counts may appear in that outbound telemetry
only as 30-day aggregate counts for the canonical pulse_operations_loop
prompt, split by total, native Assistant, first-party Patrol, and Pulse MCP surfaces. Those counts
may make pulse_intelligence_loop_active_30d true because the operator entered
the guided journey, but they must not satisfy contextual-collaboration,
complete-loop, approved-execution, or resolved-loop telemetry by themselves.
Assistant cost-ledger events may carry only the coarse context_scope
classifier for product-originated finding, resource, handoff, action, or
structured-mention turns; telemetry may aggregate that classifier into
governed-context Assistant counts, but it must not export prompts, session IDs,
resource IDs, finding IDs, command text, or action payloads.
Assistant cost-ledger events may also carry only a count-only
tool_call_count for accepted, model-selected governed tool calls within the
turn. Telemetry may aggregate that number into Assistant governed-tool
collaboration counts, but it must not export tool names, tool arguments, tool
results, provider call IDs, transcript content, resource IDs, finding IDs,
command text, action payloads, or session IDs.
The Pulse MCP README and the in-app Agent integrations panel must present that
adapter as one reusable runtime contract with client-native config wrappers:
OpenCode uses a top-level mcp object in opencode.json / opencode.jsonc,
while Claude-style clients use mcpServers. The shared command, base URL, and
token environment variable remain the Pulse Intelligence contract; client file
formats are wrappers around that contract, not separate Pulse Intelligence
surfaces. In the in-app Pulse Intelligence settings panel, those copied wrapper
snippets are on-demand setup detail; they must not occupy the default
external-agent read above the Patrol-autonomy and scoped-token hierarchy. The
Security API Access page may mint the scoped token, but it must not become the
external-agent setup page.
Full-surface token scope guidance must come from the canonical manifest's
manifest-owned requiredScopes set through shared
agentcapabilities.ManifestRequiredScopeList /
ManifestRequiredScopeMarkdownList helpers rather than Assistant-, UI-, or
adapter-local hardcoded or recomputed scope copy.
The native Assistant runtime prompt, lifecycle logs, and readiness messages
must identify that first-party surface as Pulse Assistant rather than the
legacy generic Pulse AI persona; the shared engine/core vocabulary remains
Pulse Intelligence.
The canonical manifest declaration, manifest wire shape, action-mode enum,
registry tool-name vocabulary, structured registry tool schema, provider projection helpers, JSON Schema
object-envelope helpers, provider tool/call/result shapes, provider-call
projection helper, provider tool-name catalog exact/prefix matching,
provider tool-call artifact detection and streaming tool-name prefix holding,
provider tool-call input normalization that clones caller
maps and nested JSON-like argument values before initializing empty input
objects, provider-emitted streamed tool-input parsing, final provider
tool-input raw fallback handling, and native Assistant question input parsing,
control-level vocabulary, control-tool availability
predicate, tool-governance defaulting, disabled-control guidance, capability
lookup and typed lookup-error helpers, external-tool
projection helpers, and shared agent HTTP substrate, including named capability
execution, live in
internal/agentcapabilities; MCP JSON-RPC envelopes, method constants,
request decoding, line-delimited stdio request serving, notification response
policy, stable JSON-RPC encoding, manifest-backed tool-server semantics,
tool-server method dispatch, initialize instruction/tool-call/resource/prompt payloads,
tool-server initialize result construction, notification methods, MCP
tools/call raw params decode, MCP resource URI projection, context-backed
resources/list / resources/read projection, and manifest-backed
prompts/list / prompts/get projection live at the MCP adapter edge.
Native Assistant registry declarations are part of that same identity contract:
every registry Tool.Definition.Name for Pulse Intelligence tools, including
the read-only pulse_summarize reporting synthesis tool and Patrol runtime
tools, must consume internal/agentcapabilities/tool_names.go constants rather
than owning string literals in internal/ai/tools.
Shared Pulse Intelligence workflow prompt definitions, manifest-owned
workflowPrompts catalogue selection, prompt display labels, MCP prompt title
projection, presentation kind hints, prompt argument validation, and prompt
rendering rules live in the neutral workflow-prompt core so native Assistant
starters, frontend manifest consumers, and external MCP prompts cannot drift.
When workflowPrompts is present on a manifest it is authoritative; capability
projection is compatibility plumbing for older manifests, not a second prompt
catalogue. The native Assistant starter row must therefore derive starter
availability from workflowPrompts and the attached Assistant context, then call
the AI/API-owned render route before inserting any prompt text into the composer;
it must not copy prompt labels, prompt bodies, or MCP prompts/get rendering
logic into frontend state. MCP prompt list/get helpers must likewise accept the
manifest-owned workflow prompt catalogue through ManifestPulseWorkflowPrompts,
ProjectMCPWorkflowPrompts, GetMCPPromptFromManifestSurface,
GetMCPPromptFromManifest, and
BuildMCPPromptFromManifest; they must not expose raw-capability ProjectMCPPrompts,
GetMCPPrompt, or BuildMCPPrompt entrypoints that can re-create a second MCP
prompt catalogue. Shared tool-call
parameter normalization and validation live in the neutral tool-call core: tool
names are trimmed and required, argument maps
are cloned deeply enough to detach nested JSON-like body maps/slices and
initialized, and malformed or semantically invalid tool-call params must fail
before capability lookup so native Assistant execution, the legacy text
projection, and external MCP adapters cannot drift on empty-name, nil-argument,
or caller-owned nested argument handling. Shared capability tool HTTP execution
and direct execution output/error mapping live in the neutral tool-execution
core, while result wrapping, result-text extraction, result interpretation, and
the canonical HTTPCallResponse to shared tool-result mapping live in the
neutral tool-result core. JSON object result bodies must also project into the
same shared structuredContent envelope while preserving the serialized text
content block for compatibility, so MCP clients and native Assistant paths see
one canonical result contract. Native Assistant direct execution paths must use that
shared text/marker interpretation and output/error projection instead of
flattening tool-result content blocks or branching on isError, approval, or
policy outcomes locally, so in-app execution and external MCP adapters
interpret the shared result envelope the same way. MCP tools/call decoding is
adapter-edge protocol work, but execution authorization must still enter the
neutral tool-execution core only after the shared manifest surface contract has
selected the allowed request/response capabilities. Patrol-only Assistant
registry tools that return JSON must build those responses through the shared
agentcapabilities JSON result constructors instead of manually marshaling JSON
into text-only results, so structuredContent remains available to both native
Assistant and external-agent result consumers. Patrol verification
tool-call records that decide success, failure,
approval-required, or policy-blocked outcomes must also branch through the
shared tool result interpretation rather than treating isError alone as the
success boundary. Reference clients that need a successful raw response body must also
use the shared request/response capability body helper so named capability
lookup, path/body projection, API-token headers, and stable non-2xx error
formatting stay in one place. The
agent SSE subscription transport, record parser, actionable-record filter, and
SSE-to-MCP notification bridge also live there so MCP notification bridges and
reference probes consume the same Accept: text/event-stream, status handling,
event: / data: framing rules, transport-event filtering, and JSON-RPC
notification projection.
The legacy
approved-action text projection for Pulse tool calls is also parsed there into
the shared ToolCallParams shape, including the internal approved-action
argument key/helper that carries a granted approval into replayed tool execution,
the current_resource handle plus governed aliases, and the recursive
tool-argument detector that blocks unresolved attached-resource placeholders
before visible execution; text invocations are then normalized and validated
through the same shared parameter contract, so
Assistant approval execution and MCP-compatible adapters cannot drift on tool
name, default_api: prefix, quoted argument handling, placeholder target
semantics, empty names, nil
arguments, or pre-approved action replay semantics. Structured tool
response envelopes, tool error-code vocabulary, the structured tool-result
error-code parser, and the verification.ok evidence parser also live there
so Assistant blocked/failed tool outcomes, provider call params, agent-facing
tool failure contracts, FSM block/recovery codes, recovery tracking, and
write-tool self-verification semantics cannot drift. The legacy-compatible Assistant tool markers
for approval-required and policy-blocked outcomes also live there, including
the stable APPROVAL_REQUIRED: / POLICY_BLOCKED: prefixes, payload type
values, formatter helpers, parser helpers, and the typed approval-required
payload contract for plan, context-confidence, preflight, target, risk, and
description data. Tool families may own the resource-specific payload fields
they add to those markers, but they must not redeclare the marker envelope,
rebuild anonymous approval payload readers, branch on local prefix strings, or
hide shared marker parsing behind Assistant-local parser wrappers. Legacy
service approval-marker handling may keep compatibility fallback behavior for
old marker text, but the canonical decision path must call the shared marker
parser directly at the branch site. Tool execution must not drop the returned
argument map when carrying a granted approval into replayed tool execution.
Native Assistant tool protocol aliases must expose only the shared neutral
registry shapes the Assistant actually executes (Tool, ToolCallParams,
ToolResult, ToolContent, and structured tool-response envelopes). JSON-RPC,
MCP initialize/list/resource/prompt payloads, and other MCP wire aliases remain
at the external adapter edge in internal/agentcapabilities/mcp.go, not in
internal/ai/tools/protocol.go. Chat transcript and API-facing tool-result
shapes must alias the shared provider-call contract while the richer in-app
Assistant transcript projects to that shared shape through an explicit helper.
Provider-result context projection also belongs to internal/agentcapabilities:
native Assistant, the legacy AI service loop, external adapters, and future
Pulse Intelligence surfaces must build full transcript results and model-context
results through the shared projection so transcript preservation, error flags,
and model-result truncation cannot drift into surface-local pairs.
API handlers, Assistant tool governance, cmd/pulse-mcp, and the in-repo probe
consume that shared contract so agent projections cannot drift into local
structs, local structured schema types, local provider schema projection, local
schema wrappers, local request/response filters, local capability maps, local
path placeholder parsing, local argument-to-body shaping, local manifest
fetchers, local API-token header spelling, local stable error-envelope
formatting, local named capability execution, local tool-governance defaulting,
local disabled-control guidance, local MCP manifest-backed
tool-server handlers, local MCP method dispatch, local MCP request decoding or
line-framing loops, local JSON-RPC encoders, local notification response
checks, local MCP initialize builders, local
HTTP-to-MCP result wrapping, local MCP result interpretation,
local SSE record scanners, local SSE-to-MCP notification bridges, local
tool-result maps, or local tool-description builders. Path placeholders from the manifest and top-level
tool arguments must be projected through the shared
agentcapabilities.ProjectCapabilityCall helper before dispatch, so path
values are percent-encoded single segments and non-placeholder write arguments
become the JSON request body when no nested body object is provided.
Internal tool-call metadata such as approved-action grants must stay on the
shared agentcapabilities argument helper path and be stripped by the shared
projection before any public manifest-backed HTTP body is built.
Canonical IDs containing :, /, spaces, or other reserved characters cannot
alter the route shape.
The Assistant and MCP surfaces must also rely on the router-owned manifest
route/scope proof: every canonical capability is projected through the shared
call helper into a concrete request, then exercised against the API router with
a token that lacks the advertised scope so drift between manifest metadata,
agent-facing placeholders, and real authorization fails before either surface
ships a mismatched tool.
Native Assistant provider seams are part of the Pulse Assistant surface, not
the external MCP adapter. Chat service aliases, API handler setter signatures,
router dependency wiring, direct in-app tool execution, and findings-store
adapters plus the broader native tool adapter family must use Assistant/Pulse
Intelligence terminology; MCP names are reserved for the protocol adapter,
MCP-compatible action-contract interface, wire envelopes, and result
interpretation contracts.
The Pulse Intelligence event vocabulary is part of that same shared
internal/agentcapabilities boundary. API SSE producers, the
subscribe_events manifest description, MCP notification advertising, MCP
transport-event filtering, MCP notification method projection, shared SSE
record parsing, shared SSE-to-MCP notification bridging, and reference probes
must consume the shared event kind constants/helpers instead of maintaining local copies of
finding.created, approval.pending, action.completed, stream.connected,
or heartbeat.
MCP tools/list must use the shared
agentcapabilities.ProjectManifestSurfaceTools projection for the Pulse MCP
surface. The manifest-owned surfaceToolContracts field is the allowlist for
which request/response tools Pulse MCP may advertise or execute; raw
Manifest.Capabilities remains the metadata and execution source, not the
surface availability decision. MCP initialize capability advertisement,
operating-instruction affordance copy, tools/list, tools/call,
resources/list, resources/read, prompts/list, and prompts/get must also
pass through the manifest-owned surface affordance contract: the surface must
allow tools, resources, prompts, and capability metadata before raw manifest
capability or prompt presence can expose them to an external client. The shared
agentcapabilities.ProjectTools
helper still owns the full request/response tool shape, including
manifest-owned capability metadata (category, method/path, scope, action mode,
approval policy, request/response shape, stable error codes, input schema, and
structured output schema). That metadata must be available both in model-facing
description prose and in the standard MCP _meta["pulse.capability"] object
for clients that need structured route/scope/governance facts, so external-agent
clients see the same governed API contract without a second MCP-specific tool
registry, projection loop, allowlist, or prose table. Action mode (read,
mixed, write) and approval policy (scope_only, action_plan) are
manifest-owned governance fields; MCP must not infer or override them from HTTP
method or local tool names. Manifest-backed capability lookup and tools/list
projection must also detach raw inputSchema, raw outputSchema, structured
metadata maps, and error-code slices through agentcapabilities.CloneCapability,
CloneCapabilities, and CloneRawMessage, so external-agent adapters cannot
mutate the manifest-backed contract while serializing or post-processing one
projected tool list.
The manifest-owned get_patrol_control_status structured output schema is
part of the same AI runtime contract. Its four-step operator rollup is a
content-free stage projection for Assistant and MCP callers: governance step
counts represent pending approvals until an approved or rejected decision
exists, then represent decision evidence, while verification step counts
represent verified outcomes or terminal rejected decisions. Optional MCP
readiness remains a separate externalAgentReady capability signal, not an
operator step. AI runtime surfaces must consume that manifest schema and backend
projection instead of inventing chat-local stage-count rules. The schema's
Patrol control starter count includes native Patrol, current paid Patrol
control, and legacy Pro activation handoff starts; its
completed-loop and resolved-loop counts are aggregate orientation fields for
native Assistant and MCP-facing orchestration only. Legacy
patrolAutonomy* and proActivation* completed-loop, resolved-loop, and
value-state fields are aliases for the Patrol control values; the legacy Pro
activation starter field remains an entry-point-specific count. All of
those fields must remain free of prompt names, finding IDs, action IDs,
resource names, actors, request bodies, commands, or model output.
Resolved-loop semantics remain approved-and-verified only.
cmd/pulse-mcp startup guidance and the generated MCP README token-scope
block must consume the manifest-owned requiredScopes summary through
agentcapabilities.ManifestRequiredScopeList /
agentcapabilities.ManifestRequiredScopeMarkdownList. MCP README tool and
capability-specific error-code inventories are generated through
scripts/generate-pulse-intelligence-docs.go from the manifest-owned Pulse MCP
surface contract via agentcapabilities.MCPToolCapabilityInventoryMarkdown and
agentcapabilities.MCPErrorCodeInventoryMarkdown. Workflow-prompt inventory is
generated through agentcapabilities.MCPPromptInventoryMarkdown from the same
MCPManifestSurfacePromptProjectionSupported gate and
ManifestPulseWorkflowPrompts / ProjectMCPWorkflowPrompts projection used by
prompts/list, including manifest labels as prompt titles. README tool lists,
prompt lists, scope lists, and capability-specific error-code lists must not be
hand-maintained snapshots. MCP
tools/call must use the same shared
agentcapabilities.ManifestSurfaceToolCapabilities resolver as tools/list;
streaming capabilities such as subscribe_events, and raw manifest
capabilities omitted from the Pulse MCP surface contract, remain unavailable to
request/response tool calls even if a client sends their name manually. Adding
a raw capability to Manifest.Capabilities is not enough to publish an MCP
tool; the Pulse MCP surfaceToolContracts allowlist must opt it into that
external-agent surface. Publishing the Patrol work prompt also requires the
surface contract to include get_patrol_control_status, so MCP clients can
read the same content-safe Patrol work stage before requesting fleet, finding,
resource, approval, action, or resolution detail. The legacy
get_operations_loop_status name may remain accepted only at compatibility
edges.
MCP resources/list and resources/read are also shared manifest-backed
projections: resources/list must call the canonical get_fleet_context
capability, resources/read must call get_resource_context, and the
pulse://resource/<resource-id> URI shape, resource content MIME type, and
resource read parameter validation live in internal/agentcapabilities rather
than in cmd/pulse-mcp or an MCP-only inventory registry. They must enter
through ManifestSurfaceResourceCapabilities,
ListMCPManifestSurfaceResourcesHTTP, and
ReadMCPManifestSurfaceResourceHTTP, not raw-capability
ListMCPResourcesHTTP or ReadMCPResourceHTTP helpers. The MCP resource
methods remain disabled when the target surface affordance contract disables
resources, even if those context capabilities exist in the manifest.
Operator-state writes, governed finding lifecycle tools, and action
plan/decision/execute tools must also carry manifest-owned inputSchema
definitions for their exact arguments so external-agent clients receive typed
fields and enums from the canonical manifest instead of an MCP-local free-form
body convention. When the manifest has no authored schema, MCP may derive a
permissive fallback through the shared agentcapabilities.ToolInputSchema
helper, but it must not hand-roll a second JSON Schema envelope.
Governed finding lifecycle tools must return the agent-stable error envelope
from internal/agentcapabilities/errors.go for branchable failures
(invalid_finding_request, finding_not_found,
finding_action_not_allowed, patrol_unavailable) and declare those codes in
the canonical manifest; UI callers may still use the same HTTP routes, but the
wire contract is owned by the Pulse Intelligence agent surface once the route
is advertised there. The exported agentcapabilities.AgentErrCode* constants
are the source of those values for both manifest declarations and
internal/api/ handler emissions; local string literals in either layer are
contract drift. Manifest scope values are auth-owned vocabulary from
pkg/auth and must be declared through manifest-local aliases to those
constants, not repeated as string literals, so MCP tools and direct HTTP agents
advertise the same scopes the API authorization layer enforces. Patrol finding
capabilities (list_findings, acknowledge_finding, snooze_finding,
dismiss_finding, and resolve_finding) are part of the AI runtime route
surface and therefore advertise ai:execute; they must not be downgraded to
monitoring scopes in MCP docs or manifest projections while the router gates
those HTTP routes through AI execution authorization.
Do not add MCP-only business logic or a second tool registry here. If a new
operation should be available to external agents, add it to the canonical API
capability manifest and make Assistant consume the same governed backend
contract when the in-app UX needs it.
Assistant prompt governance follows the same rule: live turns derive tool
mode, typed approval policy (scope_only or action_plan), approval summary,
and tool summary text from the shared agentcapabilities.ToolGovernanceDescriptor
shape returned by PulseToolExecutor.ListToolGovernance; descriptor defaults
must be constructed through agentcapabilities.NewToolGovernanceDescriptor,
and fallback prompt
text must call the registry-owned
tools.CanonicalToolGovernanceForSurface helper with the explicit requesting
Pulse Intelligence surface rather than maintaining a chat-local copy of tool names, governance
descriptor fields, policy vocabulary, or governance prose.
Assistant-only structured interaction tools such as pulse_question must also
be projected through the shared
agentcapabilities.BuildAssistantToolGovernancePromptSection and
agentcapabilities.AssistantGovernanceOfferedToolNames helpers rather than by
hand-building prompt-policy lines or offered-tool filters in chat-local code;
those tools remain native Assistant interaction boundaries, not MCP manifest
capabilities.
Direct Assistant registry execution is also part of the shared Pulse
Intelligence tool-call contract: ToolRegistry.Execute must normalize and
validate tool names and arguments through agentcapabilities.ToolCallParams
and use the shared invalid-params, unknown-tool, and control-disabled tool
result helpers before lookup or handler dispatch, so in-app direct execution
cannot drift from external MCP tools/call on trimmed names, blank-name
rejection, nil-argument initialization, deep caller-argument copying, or
standard entrypoint failure envelopes.
Chat-service direct execution paths such as autonomous command execution and
approved-fix replay must route through one Assistant registry execution helper
before calling agentcapabilities.InterpretDirectToolExecution, so
surface-specific failure wording does not fork executor lookup, registry
dispatch, marker interpretation, or shared result handling. MCP-named
direct-execution symbols are not part of the shared adapter contract; native
Assistant code and MCP adapters must use the neutral direct tool execution
helper.
Cross-repo auto-fix and Patrol-approved action binders must resolve approved
tool execution through AIAutoFixHandlerDeps.ResolveApprovedAssistantToolExecutor;
that resolver returns the native ApprovedAssistantToolExecutor only, so
enterprise code cannot recreate MCP-named Assistant execution selection. The
current public runtime must populate AssistantToolExecutor only; MCP remains
an external adapter boundary and is not an approved-fix execution dependency.
Assistant telemetry that records recoverable policy-block attempts must also
use the shared agentcapabilities tool-response error-code constants rather
than local string literals, so metrics labels, structured tool responses,
Assistant recovery tracking, and MCP-facing agent error vocabulary stay
aligned.
Assistant provider tool schemas are also registry-owned: chat must obtain the
runtime provider tool surface from PulseToolExecutor.AssistantProviderTools
so availability filtering, registry-owned governance descriptors, and
Assistant-native interaction tools are composed at one executor-owned boundary
instead of being paired manually in chat paths. That executor projection must
project registered tools.Tool.InputSchema values through the shared
agentcapabilities.ProjectProviderTools provider-schema helper and the
manifest-affordance-gated
agentcapabilities.ProjectPulseAssistantProviderTools provider-surface helper,
and model-selected tool calls must flow through
agentcapabilities.ProjectProviderToolCallToToolCall and the shared
agentcapabilities.NormalizeProviderToolCallForExecution /
NormalizeProviderToolCallsForExecution execution normalizers, not local schema
maps, compatibility aliases, raw provider call fields, chat-local provider
projection wrappers, or MCP-shaped provider-projection wrappers.
MCP-shaped provider projection names remain protocol compatibility aliases in
internal/agentcapabilities/mcp.go; they must not be implemented in the neutral
provider schema boundary or imported by native Assistant runtime paths.
Shared projected-tool behavior hints are the same class of boundary: the
neutral projection owns ToolBehaviorHints, while any MCPToolAnnotations
name is an alias in the MCP wire edge only.
Assistant-only provider tools are still shared Pulse Intelligence declarations:
the native pulse_question clarification tool must be included in provider
tool lists through agentcapabilities.ProjectPulseAssistantProviderTools and
agentcapabilities.AssistantNativeProviderTools, its declaration must come
from agentcapabilities.NewPulseQuestionProviderTool, its JSON Schema must
remain owned by agentcapabilities.PulseQuestionProviderInputSchema, its
question type/defaulting vocabulary must use
agentcapabilities.PulseQuestionToolTypeValues and
agentcapabilities.NormalizePulseQuestionToolType, its emitted provider input
must parse through agentcapabilities.ParsePulseQuestionToolInput, and
allowlists that recognize leaked provider tool calls must consume
agentcapabilities.NewAssistantProviderToolNameCatalog instead of hard-coding
Assistant-native names or rebuilding provider-tool name sets locally. Provider
tool-call artifact detection and streaming partial-tool-name holding must use
agentcapabilities.ProviderToolCallArtifactIndex and
agentcapabilities.SplitTrailingProviderToolNamePrefix with that shared catalog,
so Assistant and future adapter boundaries do not fork DSML/XML/pipe/JSON/function
leak semantics. Chat may own waiting for user answers, UI event
emission, and FSM user-input behavior, but it must not carry a chat-local
provider schema, description, tool-list append rule, prompt-filter rule, parser,
tool identity, or provider artifact detector for that interaction.
That schema boundary is also a copy boundary: registry tool definitions must
normalize through agentcapabilities.Tool.NormalizeCollections, and
ToolRegistry.Register / ToolRegistry.ListTools must not expose caller-owned
or registry-owned InputSchema maps, required slices, enum slices, provider
input-schema maps, or JSON-schema default containers for later mutation by an
Assistant, provider, or external-agent surface. Direct provider schema
projection helpers must normalize through that same copy boundary before
emitting provider JSON so JSON-schema default containers cannot alias registry
definitions.
Provider tool-call normalization must also stay shared: provider responses,
Assistant transcript storage, live Assistant execution, and API adapter
projections must get independent input maps, nested JSON-like argument values,
trimmed executable tool names, initialized empty argument maps, and raw provider
thought-signature payload bytes from
agentcapabilities.ProviderToolCall.NormalizeCollections plus the shared
execution normalizers so a later mutation in one surface cannot rewrite another
surface's tool arguments or provider reasoning-continuity metadata.
Assistant transcript normalization is part of that copy boundary:
chat.Message.NormalizeCollections and chat.ToolCall.NormalizeCollections
must detach tool-call slices, nested tool-call input maps/slices, provider
thought signatures, success pointers, and tool-result pointers before stored
messages are reused across session storage, provider-continuation, browser/API,
or orchestrator adapter surfaces.
Provider-facing tool results follow the same shared boundary: Assistant
tool execution must project shared tool-call results through
agentcapabilities.NewProviderToolResultFromToolResult and build transcript or
model-context result messages through agentcapabilities.NewProviderToolResult
so result text flattening and is_error semantics stay aligned with
external-agent adapters. MCP-named provider-result projection helpers are not
part of the adapter contract; runtime paths that need the plain text from a
shared tool result must call agentcapabilities.ToolResultText at the execution
site instead of keeping Assistant-, Patrol-, or MCP-local result-text wrappers.
Runtime paths that convert chat transcripts, repair orphan tool calls, or run
legacy service tool loops must not assemble providers.ToolResult or
chat.ToolResult structs locally, and must not hide shared provider-result
construction behind Assistant-local pointer wrappers.
Native Assistant registry execution and external MCP method dispatch must also
normalize shared ToolResult values through
agentcapabilities.ToolResult.NormalizeCollections before returning them, so
handler-owned content slices and nil content collections cannot leak across
Assistant, provider, or MCP response boundaries. MCP may retain only actual
MCPToolResult / MCPContent protocol wire aliases; result constructors,
structured tool-response projection, JSON object and array-wrapper
structuredContent projection, text extraction, and interpretation stay neutral
shared helpers rather than MCP-named wrappers.
Assistant runtime settings and tool exposure must derive
read-only/controlled/autonomous semantics from
agentcapabilities.ControlLevel and
agentcapabilities.ControlLevelAllowsControlTools; future MCP adapters may
alias the same values, but must not carry separate control-tool availability
rules, disabled-control guidance, or tool-governance defaulting.
Guest-family AI runtime code paths (VMs and LXC system containers) share
generic helpers instead of per-family copies. Patrol guest intelligence
gathers both families through gatherGuestIntelligenceFromViews in
internal/ai/patrol_intelligence.go; the Assistant query tool resolves
single-guest gets through canonicalGuestGetResult / guestViewGetResult
and search matches through addCanonicalGuestSearchMatches /
addGuestViewSearchMatches in internal/ai/tools/tools_query.go; per-guest
disk summaries flow through appendGuestDiskSummaries in
internal/ai/tools/tools_storage.go. A new guest family extends those
helpers (their view-constraint interfaces name the read-state methods they
need) rather than re-rolling a parallel loop. The same shape applies to
mutating tool pipelines: file append/write share executeFileMutation
driven by fileMutationSpec (internal/ai/tools/tools_file.go), and
namespaced kubectl actions share executeKubernetesResourceAction driven by
kubernetesResourceAction (internal/ai/tools/tools_kubernetes.go) — new
file or kubectl actions add a spec, keeping approval-command text stable,
instead of duplicating the approval/audit pipeline. OpenAI-compatible message
conversion for both non-streaming and streaming turns is shared through
convertMessagesToOpenAI (internal/ai/providers/openai.go), Anthropic message
conversion for both non-streaming and streaming turns is shared by the API-key
and OAuth clients through convertMessagesToAnthropic
(internal/ai/providers/anthropic.go), Gemini message conversion for both
non-streaming and streaming turns is shared through convertMessagesToGemini
(internal/ai/providers/gemini.go), and AI memory history files load through
the generic loadMemoryHistory
(internal/ai/memory/paths.go). Finding/findingJSON and
UnifiedFinding/unifiedFindingJSON are deliberate marshal-mirror twins —
do not merge them; the mirror invariants are enforced by
TestFindingJSONMirrorStaysInSync and
TestUnifiedFindingJSONMirrorStaysInSync. The same mirror posture applies on
the transport side: orchestratorChatAdapter.GetMessages
(internal/api/ai_handlers.go) and chatServiceAdapter.GetMessages
(internal/api/chat_service_adapter.go) convert the same chat-service
messages onto deliberately separate output contracts; do not merge them, and
keep their field mapping aligned. Orchestrator-facing tool-call and
tool-result payloads must project through
aicontracts.OrchestratorToolCallInfoFromProvider,
OrchestratorToolCallInfo.ProviderToolCall,
aicontracts.OrchestratorToolResultInfoFromProvider, and
OrchestratorToolResultInfo.ProviderToolResult so the public investigation
handoff contract inherits the same agentcapabilities input-map,
thought-signature, and provider-result normalization as Assistant and MCP —
TestOrchestratorAndChatAdaptersMapTheSameMessageFields enforces it.
Assistant frontend presentation changes under
frontend-modern/src/components/AI/Chat/ must keep live tool activity aligned
with OpenCode's mutable tool-part pattern without losing Pulse's operator audit
trail. The current OpenCode reference at fetched origin/dev commit
233427f08eb8b2a0627123816e807123fbbe5b26 keeps tool input/progress/result as
a single mutable part in packages/tui/src/context/sync-v2.tsx lines 276-345,
updates running tool metadata from the execution context in
packages/opencode/src/session/tools.ts lines 53-66, and suppresses completed
successful tool detail when showDetails is off in
packages/tui/src/routes/session/index.tsx lines 1704-1779. Pulse adapts that
by keeping successful completed tool rows command/status visible, compacting
them while the same Assistant turn is still streaming and newer concrete
activity has arrived, and keeping successful raw output behind the details
disclosure by default once the turn completes. Pending tools, skipped/canceled
tools, failed tools, and completed-turn tool details must remain
visible/inspectable; failed output may render inline because it is actionable.
AI Chat icon-only actions own Assistant/session semantics, labels, and
handlers, but their visible button chrome must compose the shared
ActionIconButton primitive from frontend-modern/src/components/shared/Button.tsx.
Header utilities, session row controls, transcript fallback close/download
controls, activity-dock queued follow-up actions, composer send, footer
help/route actions, and compact dismiss controls must not recreate local
h-5/h-6/h-7/h-8/h-9 icon-button class shells. Message and tool transcript copy
controls own Assistant copy targets and copied-state timing, but their visible
copied-state icon chrome must compose CopyValueButton from the same shared
button primitive boundary rather than message- or tool-local copy/check shells.
Assistant live workflow status follows OpenCode's current-state timeline
model. CANONICAL (supersedes the per-row transcript-status rules elsewhere in
this contract): live workflow status is FOOTER-OWNED and must never be narrated
into the scrolling transcript. OpenCode keeps "the model is doing X" in a single
pinned footer line and puts only durable artifacts (user text, reasoning, tool
calls, the answer) in the timeline; Pulse mirrors that by rendering the latest
replacing workflow status ONLY in the activity dock above the composer
(currentStatus / assistant-activity-dock in
frontend-modern/src/components/AI/Chat/index.tsx) and never as a transcript
row or message-header chip — shouldRenderWorkflowStatusEvent and
shouldShowHeaderWorkflowStatus in
frontend-modern/src/components/AI/Chat/MessageItem.tsx are hard-false. The
dock must stay up for the whole turn: it is gated on the streaming assistant
message (assistantTurnActive), not chat.isLoading(), because loading flips
false at visible-turn-complete and would make the footer flash and vanish.
History may persist for audit/session state, but the dock renders the latest
replacing status immediately. Pulse must not replay stale status history through
artificial timers when the stream already has a newer workflow/tool state.
Frontend display code may refresh elapsed-time suffixes while a turn is active,
but status selection must read the current workflow status directly rather than
deriving an older display status from workflowStatusHistory.
- Add or change chat runtime, Patrol orchestration, findings generation, or remediation behavior through
internal/ai/Patrol deterministic disk-health evidence changes ininternal/ai/patrol_signals.goandinternal/ai/patrol_ai.gomust read SMART health, normalized SMART counters, and canonical physical-disk identity from storage and metrics tool payloads without treating missing or unknown SMART health asPASSED. Non-zero pending-sector, offline-uncorrectable, media-error, and related SMART counters must remain model-facing reliability evidence even when the headline health field isPASSED, so Patrol does not summarize a disk as healthy while deterministic counters indicate disk risk. - Add or change canonical AI provider config, provider registry metadata, provider-scoped model selection, or runtime auth/base-URL defaults through
internal/config/ai.goandinternal/config/ai_providers.go. Direct providers that speak the shared chat-compatible API must use the registry-backedAIProviderProtocolOpenAICompatiblepath andNewOpenAICompatibleClientrather than adding new provider-specific factory branches. Per-provider auth fields, configured booleans, clear-key fields, default base URLs, display names, docs links, env-var hints, and fallback model rows must be declared once ininternal/config/ai_providers.goand projected to/api/settings/ai; frontend settings may arrange those provider cards, but must not become the source of truth for provider capability or credential metadata. Provider model-cache keys must include provider identity, auth/base-URL inputs, and credential identity changes without exposing raw secret values. Ollama request keep-alive is a provider runtime option owned by this path:internal/config/ai.gostores the value,/api/settings/aiexposes it, andinternal/ai/providers/ollama.gois the only layer that turns it into the Ollamakeep_aliverequest field. An empty configured value means Pulse omitskeep_aliveso the Ollama server default applies. Cloud context privacy is a FIXED posture, not a user setting. Pulse is a self-hosted homelab/SMB tool: when the operator points the Assistant at a cloud model they have accepted that their (non-secret) infrastructure detail reaches that provider, so the cloud model receives real resource context (names, IPs, config) and the Assistant is actually useful. There is intentionally NOcloud_context_privacydial and NOshare_operational_context_with_cloudtoggle — the privacy control users understand is the choice of model (a cloud provider vs. a local Ollama model). Granular per-resource governance is an enterprise concern, not the homelab default. Two invariants always hold at the model boundary and are NOT configurable:- Prompt-secret sanitation strips credentials (API keys, passwords, tokens) — credentials never reach a cloud model.
- The local-only floor: resources the policy engine routes
ResourceRoutingScopeLocalOnly(Restricted sensitivity — tagged secret/pii, PMG, k8s-secrets, ...) have their identifiers redacted and never leave the local trust boundary. Everything else (Internal and Sensitive) flows. Both are enforced by the model-boundary sanitizermodelboundary.RequestSanitizerForModel(model, provider)(internal/ai/modelboundary), which returns nil for local (Ollama) models — local always receives full context. UNIVERSAL backstop rule: EVERY code path that sends infrastructure-derived content to an external model MUST installRequestSanitizerForModelbefore the provider request — not only the interactive agentic loop. This explicitly includes session compaction (internal/ai/chat/session_compaction.goSummarizeSession), which sends the PERSISTED transcript (original user prompts and tool outputs carry raw identifiers), and the shared service helper(*Service).requestSanitizerForModel(internal/ai/service.go) used by discovery analysis, the report and fleet narrators, quick analysis, and the ExecuteAgentic paths. A new model-bound request path that skips the sanitizer is a leak and is not permitted. (Static capability probes with no resource content, e.g. the Patrol preflight self-test, are exempt only because their payload is fixed and carries no identifiers.) Redaction-placeholder hygiene: Pulse-authored model-bound directives (the resource-context handoff instructions ininternal/ai/chat/service.goandinternal/ai/chat/plain_text_resource_context.go) must NOT inject the literal redaction placeholder (unifiedresources.ResourcePolicyRedactedLabel, "redacted by policy") into the prompt — naming it makes the model echo it back as if it were a resource name. Directives reference withheld labels neutrally ("a withheld or placeholder label") and must instruct the model not to repeat a withheld placeholder back to the user as the resource identity; thecurrent_resourcehandle remains the authoritative target.
- Add or change Pulse Assistant request flow through
internal/api/ai_handler.go,frontend-modern/src/api/ai.ts, andfrontend-modern/src/api/aiChat.tsAssistant session compaction is a runtime-backed session workflow, not a local waiting message, transcript-only UI action, or stubbed summarize endpoint. The referenced OpenCode source at fetchedorigin/devcommite82542b8023a8374f29c23b70ec019c8f256354eregisters/compactwith/summarizeas an alias inpackages/tui/src/routes/session/index.tsxlines 548-570 and calls the runtime session summary action with the current provider/model route. Pulse adapts that behavior throughPOST /api/ai/sessions/{id}/summarize,chat.Service.SummarizeSession, andSessionStore.CompactWithSummary: the backend must reject active-running sessions, build a bounded and redacted transcript for the active chat model, record provider token usage asassistant_session_compaction, persist one visible assistant summary row plus the latest bounded turns, clear redo state, and never stream or persist raw provider tool-call artifacts, hidden reasoning, secrets, or raw provider responses as the compacted transcript. The drawer header action and/compactslash command must call the same endpoint, surface compacting progress, reload the compacted session and session list, keep focus in the composer after completion, and treatnot_neededresults as quiet operator feedback instead of an error. OpenRouter-routed Assistant chat requests must set a bounded default completion budget when the runtime request does not specifyMaxTokens. OpenRouter preflights affordability against the requested maximum completion budget, so leaving the field unset can make tiny chat turns reserve a model-scale output default and fail against per-key total limits despite the account having credit. That bounded default must still be high enough for normal detailed Assistant answers such as inventory breakdowns; provider request shaping must not make an ordinary answer finish mid-sentence. The provider boundary owns this request-shape fix; UI code must not work around it with route-specific retry copy. If the provider still rejects the request because the requested completion budget exceeds the key limit, the Assistant error message must name that key-limit condition without exposing raw provider JSON, URLs, or key identifiers. Assistant stream provider failures emitted through chat must be classified ininternal/ai/chatbefore callback events reach the frontend. Endpoint configuration/URL errors, provider reachability/transport failures, auth, quota/billing, rate-limit, timeout, and cancellation states must render as operator-facing Settings or retry guidance. Raw Go transport strings, provider JSON bodies, request URLs, request methods, dashboard links, and key-management links must not be streamed or persisted as chat-visible assistant output. Assistant provider readiness is part of that same request flow: when the drawer opens or the selected chat model changes, the frontend must start a background verification of the selected provider/model through/api/ai/test/{provider}. That check must use neutral provider diagnostic copy owned byinternal/ai/, not Patrol runtime-finding wording, and the drawer may surface the result as actionable retry/settings status plus same-model configured-provider alternatives without converting it into assistant-authored output. Provider checking is a background diagnostic and must not delay the user's first useful chat turn; the drawer should keep routine selected-route health visible in compact composer chrome while reserving the larger provider-status banner for requested checks and actionable failures. A confirmed selected-route provider error must keep typed text and focus without blocking normal chat dispatch, but the send path must still use the currently selected route until the operator explicitly chooses a different route. Same-model configured-provider alternatives remain visible one-click route changes and failed-turn retry choices, not automatic send-time recovery or hidden provider fallback. The OpenCode reference behavior for this slice is retries on the selected provider/model, not silent fallback to another provider or route; Pulse must adapt that as explicit route choice plus visible route search/recovery. Frontend stream reducers must ignore retiredprovider_fallbackworkflow phases entirely instead of rendering compatibility "fallback rejected" rows, because the selected-route retry/error contract is the only live recovery path until an operator explicitly chooses another route. The Assistant/modelcommand therefore has two owned paths: an exactprovider:model-idargument selects that route directly, and an OpenCode-styleprovider/model-idargument is accepted only when the provider prefix is a known Pulse provider before being normalized back to canonicalprovider:model-idstorage. Partial or unknown slash text opens the shared model picker with the search text prefilled instead of becoming a hidden route guess. Assistant model-selection defaults are settings-owned: the drawer may persist explicit model selections only for concrete session IDs, while blank-session chat defaults must flow from/api/settings/aichat_modelormodel. Browser storage must not keep a hidden__default__model route that overrides the configured chat model after the operator changes provider settings; stale blank-session defaults must be ignored and cleaned on mount so routes such as OpenRouter become visible and effective immediately. Assistant session model continuity is transcript-owned, not ambient UI state.internal/ai/chatmust persist the effective selected model route on the user turn as well as final assistant turns, and the browser chat runtime must restore the latest explicitprovider:modelroute from loaded session messages before publishing the loaded session ID to drawer/session storage. During an active Assistant turn, the drawer must keep the effective model route visible next to the live turn status using the route recorded on the streaming assistant message first and the selected route only as a fallback; live progress text remains the activity description, while route identity is separate compact chrome so provider/model state stays inspectable without polluting status copy. Provider-start copy must stay route-neutral ("Waiting for assistant.") because selected-route identity belongs in the route chrome and stream metadata, not in transient activity text; retry copy must say Pulse is retrying the selected route, not imply automatic provider or model fallback. Legacy bare model names, provider response IDs without a Pulse route prefix, URLs, and malformed route strings must be ignored so loading older sessions cannot corrupt the active selector. The referenced OpenCode source at fetchedorigin/devcommit2006259a02a87edf9e37f253cbddf3188309026brestores local session model state from the latest user message inpackages/app/src/pages/session.tsxlines 361-368 throughpackages/app/src/pages/session/session-model-helpers.ts; Pulse adapts that pattern by using its persisted explicit route contract rather than terminal provider/model objects. Follow-up sends during an active Assistant response are chat-runtime queue state by default. The drawer must accept and echo the user's follow-up as a queued user turn without aborting or replacing the active model stream, must show an itemized composer-adjacent queue with per-follow-up edit/remove controls plus clear-all, and must drain queued turns in order only after the active stream becomes idle. Queued follow-ups must snapshot the effective model route at enqueue time so a later model/provider switch cannot silently reroute an already-queued user turn, and both the transcript queued-user row and composer-adjacent queue row must surface that snapshotted route label when it exists; explicit failed-turn recovery actions labelled from a configured provider route must pass that selected route as an override instead of relying on ambient selector state. The referenced OpenCode source at fetchedorigin/devcommit1399323b78a04229d9bfe00c7436d7f41770fda8keeps current, recent, and selected models as structured{ providerID, modelID }values inpackages/opencode/src/cli/cmd/tui/component/dialog-model.tsxandpackages/opencode/src/provider/provider.ts, so Pulse must preserve the equivalent route identity for queued and retried turns. Queue-drain UX verification must have a deterministic local fixture path so agents can test queued follow-up echo, hold, drain, and tool-row replacement without opening a real provider stream. The referenced OpenCode source at fetchedorigin/devcommit31c099be435d59bd6749ace7a9f2bb2245e6d3faemits local demo tool events throughpackages/opencode/src/cli/cmd/run/demo.ts(startToolanddoneTool, lines 451-525; demo command routing, lines 1039-1076); Pulse adapts that by keeping/fixture queue-holdand/fixture queued-follow-upfully local browser fixtures for queue interaction proof. The referenced OpenCode source at fetchedorigin/devcommite82542b8023a8374f29c23b70ec019c8f256354eexposes queued prompts throughRunQueuedPromptSelectBodyinpackages/opencode/src/cli/cmd/run/footer.command.tsxlines 588-650: queued rows are searchable and keyboard-operable, with Enter/Ctrl+E editing the selected prompt and Delete/Ctrl+D removing it. Pulse adapts that pattern to the browser drawer by making each composer-adjacent queued follow-up row focusable and directly operable with Enter to edit and Delete/Backspace to remove, while retaining the visible icon buttons and avoiding terminal-only shortcuts that collide with browser chrome. OpenCode'sDialogModelfeeds current, recent, favorite, and provider model rows intoDialogSelect, whileDialogSelectmaintains a selected row and handles up/down/page/home/end/return navigation. The fetched OpenCodeorigin/devcommit31c099be435d59bd6749ace7a9f2bb2245e6d3fakeeps those bindings inpackages/tui/src/ui/dialog-select.tsxlines 288-378. Pulse's browser model picker must expose the same route identity as a labelled search/listbox surface, focus search on open, move keyboard focus from search into the current or filtered model row, and keep every actionable list row, including catalog-disclosure rows, navigable without requiring mouse clicks. The 2026-06-07 model-provider slice rechecked OpenCodeorigin/devcommit914a643:packages/app/src/components/dialog-select-model.tsxexposes provider connection and model management actions directly inside the model picker, whilepackages/tui/src/component/dialog-model.tsxexposes a provider action in the same dialog. Pulse adapts that by wiring the Assistant model chooser's provider action to the governed Assistant & Patrol provider settings route instead of hiding provider repair behind a separate slash command or silently switching model routes. The same 2026-06-07 activity-visibility pass rechecked OpenCodepackages/app/src/pages/session/message-timeline.tsxandmessage-timeline.data.ts: active turns are represented as session timeline rows, with busy/retry state kept visible while assistant parts continue to arrive. Pulse adapts that by keeping fast command/tool completions in a transient running state long enough for the compact transcript row to paint, while still collapsing large command output behind the existing tool-details affordance. Stop is the explicit interruption path: it must abort the active stream, clear queued follow-ups and pending tool/approval/question affordances, preserve any partial model text, return focus to the composer, and render a neutral transcript marker rather than persisting synthetic assistant answer text or surfacing the interruption as a retryable provider failure. The referenced OpenCode source at commit9ed17da55ab1f7360cc0e01075f763e27fa899e9routes prompt interruption through the prompt command inpackages/opencode/src/cli/cmd/tui/component/prompt/index.tsx(session.interrupt, lines 379-399) and handles stream interruption inpackages/opencode/src/session/processor.ts(Effect.onInterrupt, lines 977-983; aborted tool state finalization, lines 888-915). Pulse adapts that terminal active-part behavior by clearing unresolved pending tool, approval, and question transcript rows on explicit Stop while preserving already-streamed answer text and completed tool evidence before showing the neutral stopped marker. The same terminal cleanup applies to non-interrupt terminal paths. When a stream reachesdone, emits a structurederror, or rejects before an SSE terminal event, the drawer must not leave pending tool, approval, or question controls live in the transcript. The referenced OpenCode source at commit9ed17da55ab1f7360cc0e01075f763e27fa899e9mutates active tool parts into terminalsuccessorfailedrows inpackages/opencode/src/cli/cmd/tui/context/sync-v2.tsx(session.next.tool.success, lines 328-348;session.next.tool.failed, lines 350-371), and the processor marks interrupted active tools terminal before completing the assistant message inpackages/opencode/src/session/processor.ts(lines 888-917). Pulse adapts that by retaining completed tool rows and assistant text while removing unresolved interactive rows from terminal browser transcript state. Completed tool rows must preserve timing once a pending row resolves. The referenced OpenCode source at fetchedorigin/devcommiteffd27b23900720a53e965396ff1a105c1f7e9c8carries tool identity and state throughtoolCommit/startTool/doneToolinpackages/opencode/src/cli/cmd/run/session-data.ts(lines 622-733) and formats elapsed tool state withspaninpackages/opencode/src/cli/cmd/run/tool.ts(lines 191-200). Pulse adapts that by carrying the stream event start/end timestamps into the completed browser tool row, so the visible activity timeline does not collapse when a running command is replaced by its completed result. When a live browser stream receives atool_startand successfultool_endin the same buffered burst, the completed row may briefly present as running before it settles to completed, matching OpenCode's perceptible pending-tool motion at fetchedorigin/devcommit1025540fcc2a69609a0131a7168300205656d728inpackages/ui/src/components/basic-tool.tsx(pending, lines 90 and 195-205) andpackages/ui/src/components/tool-status-title.tsxwhile keeping Pulse's typed completed tool row, persisted status, answer stream, and failure state authoritative. That settle interval must be bounded, live-only, success-only, and must not delay assistant content, terminal cleanup, or failed-tool visibility. Completed browser tool rows must also preserve output trust when a raw preview is intentionally suppressed: the referenced OpenCode source at fetchedorigin/devcommit1025540fcc2a69609a0131a7168300205656d728keeps output visibility as an explicit tool-view policy inpackages/opencode/src/cli/cmd/run/tool.ts(ToolView.output, lines 40-44; tool display hooks, lines 117-123), and commits allowed tool output into scrollback inpackages/opencode/src/cli/cmd/run/session-data.ts(lines 960-964). Pulse completed rows must expose safe bounded plain-text output previews when they can be rendered without turning the transcript into a raw terminal dump, while keeping the full raw output behind the existing details disclosure. Structured, binary, unavailable, or otherwise noisy output may collapse to compact output metadata. Raw input/output details must remain copyable from the disclosure without expanding the default transcript. Completed Assistant context summaries must use the same operator-facing tool-input presentation as the visible tool rows, not raw internal tool identifiers. A completedpulse_readdevice inspection should therefore summarize as the inspected resource/action, while raw tool names such aspulse_reador bare fallback labels such asreadremain fallback-only debug/detail vocabulary. Live workflow activity is also active turn state, not disposable waiting copy. The referenced OpenCode source at fetchedorigin/devcommit1025540fcc2a69609a0131a7168300205656d728defines model, shell, step, and tool lifecycle events inpackages/core/src/session/event.ts(model switched lines 62-70; shell started/ended lines 151-174; tool lifecycle lines 340-392), projects them into durable message/part rows inpackages/core/src/session/message-updater.ts(step started lines 189-210; tool called/progress/success/failed lines 269-335), and renders model-switch plus tool rows inpackages/opencode/src/cli/cmd/tui/feature-plugins/system/session-v2.tsx(model switch lines 120-122 and 262-274; tool rows lines 455-500). Pulse adapts that by keeping the latest browserworkflow_statusactivity visible in the live stream event sequence across content, tool, approval, and question boundaries instead of dropping it the instant a richer row arrives. Burst workflow statuses may still replace one another until a durable stream boundary appears, and terminal cleanup on done/error/Stop may remove transient workflow-status rows while preserving typed model-switch and tool evidence. When the selected model route is already known at turn creation, Pulse must seed a selected-model stream event before backend/provider activity arrives so the transcript and active-turn footer show concrete route motion instead of a generic request-start wait. If backend/provider startup immediately confirms a different selected route before any durable assistant content, thinking, tool, approval, or question evidence arrives, that confirmed route replaces the provisional local selected-model row rather than stacking multiple "Using ..." rows. Once a durable boundary exists, later route events are preserved chronologically. Rendered workflow-status rows, active-turn footer text, and transcript export status lines must normalize internal tool identifiers such aspulse_query,pulse_read, andpulse_execinto operator-facing activity labels, while preserving the raw typed event payload for Details/debug paths. Provider retry states are a distinct live activity kind: aprovider_retryworkflow status must render as retrying in the active-turn footer and live workflow row so the operator sees selected-route recovery in progress rather than another generic waiting/thinking state. Assistant-authored visible answer prose is part of the same boundary: model text may describe the action in operator vocabulary such as "read command" or "inventory lookup", but raw internal identifiers such aspulse_readmust not appear in the normal transcript, copied answer text, or transcript export unless the operator explicitly opens raw details/debug content. Selected-model route rows are typed route evidence, not live activity. They may render immediately while the backend/provider stream is still quiet, but they must not hide or outrank workflow progress once arequest_start, provider, tool, thinking, approval, question, or content activity is available. This follows the referenced OpenCode separation between session status and model/tool transcript events at fetchedorigin/devcommit31c099be435d59bd6749ace7a9f2bb2245e6d3fa: Pulse keeps the selected route visible as durable evidence while the active-turn footer and live workflow rows answer what the assistant is doing now. The generated frontend SSE contract must include workflowproviderandmodelfields so selected-route activity is typed end to end. The transcript viewport is part of the same live activity contract. The referenced OpenCode source at fetchedorigin/devcommit1025540fcc2a69609a0131a7168300205656d728forwards reducer commits into terminal scrollback inpackages/opencode/src/cli/cmd/run/stream.ts(lines 140-145) and renders bottom-sticky activity inpackages/opencode/src/cli/cmd/run/footer.subagent.tsx(lines 148-153) withstickyScroll={true}andstickyStart="bottom". Pulse adapts that by tracking whether the browser transcript is pinned to the live bottom before streamed content or tool rows grow, continuing to follow active output while pinned, and preserving the operator's scroll position after they intentionally scroll away from live activity. When the transcript is unpinned while messages exist, the drawer must expose an accessible jump-to-latest command that returns the operator to the live bottom without requiring a manual scroll gesture. Composer prompt history is also drawer-local chat-runtime state: the drawer may persist a bounded local history of submitted prompt text and structured mentions for ArrowUp/ArrowDown recall, but that history must not persist or replay one-shot finding handoff, approval, autonomous-mode, or other scoped send options. The referenced OpenCode source at fetchedorigin/devcommitba57718b0516c7a8670d1e820b1a24146a8b8262binds prompt history navigation inpackages/opencode/src/cli/cmd/tui/component/prompt/index.tsxto the input cursor boundary: previous history is available at the start of the current draft, and next history restores the saved draft at the end. Pulse's Assistant drawer follows that draft-safe recall model rather than limiting history recall to an empty composer. Once a history entry is loaded, the same boundary ownership remains direction-specific: ArrowUp may move to an older prompt only from the start edge, and ArrowDown may move newer or restore the saved draft only from the end edge, so normal textarea movement is not stolen at the opposite boundary. Scoped context replay remains owned by explicit session handoff metadata or queued follow-up edit state. Unsent composer drafts are recoverable remount state, not prompt history: closing or remounting the Assistant drawer must restore the current prompt, structured mentions, cursor position, and any queued-follow-up edit metadata without writing the draft into submitted prompt history. The referenced OpenCode source at commit9ed17da55ab1f7360cc0e01075f763e27fa899e9keeps a module-localstashedprompt inpackages/opencode/src/cli/cmd/tui/component/prompt/index.tsx, restores it inPromptonMount(lines 595-604), and captures it inonCleanup(lines 607-610). Pulse adapts that source behavior with a drawer-local composer stash so normal close/reopen does not drop an unsent user thought or downgrade a scoped queued follow-up into a plain prompt. Composer submission must read the live input buffer at submit time and suppress only overlapping dispatches from the same browser interaction. The referenced OpenCode source at commit9ed17da55ab1f7360cc0e01075f763e27fa899e9packages/opencode/src/cli/cmd/tui/component/prompt/index.tsxguardsPrompt.submit()with localsubmittingstate (lines 910-924) and syncs the live inputplainTextinto prompt state before downstream reads (lines 930-936) so double-dispatch and IME/composition races do not send a stale or phantom prompt. Pulse adapts that by reading the live textarea value before trimming and by using a one-dispatch composer guard that still releases immediately for intentional queued follow-ups during an active turn. Assistant drawer controls are part of that same prompt command surface. The referenced OpenCode prompt source exposes command-named actions such as prompt submit, session interrupt, prompt history movement, and prompt stash operations through explicit command titles before those actions reach the terminal UI. Pulse's browser drawer must therefore expose command-specific accessible names and selected/expanded state for always-visible chat actions such as New session, session history, collapse/close, autonomous-warning recovery, and the control-mode selector instead of depending on title-only icon controls or ambiguous short labels. The slash command autocomplete is a command-discovery surface, not just a text shortcut hint. The referenced OpenCode source at fetchedorigin/devcommit914a643advertises session commands such as/models,/new,/sessions,/compact,/export, and/connectinpackages/tui/src/feature-plugins/home/tips-view.tsxlines 179-192, while each command registration carries a command title, category, and slash metadata inpackages/tui/src/routes/session/index.tsx(for/compact, lines 548-570). Pulse adapts that by showing the full local Assistant command set in the scrollable slash list instead of silently capping discovery to the first few commands, and every listed command row must carry a matching icon so/compact,/undo,/redo, and other lower-frequency actions do not appear as unfinished blank rows. OpenCode's current app prompt popover keeps unmatched slash searches inside the prompt surface and renders an empty-command state inpackages/app/src/components/prompt-input/slash-popover.tsx(fetchedorigin/devcommit914a643, lines 96-100); Pulse adapts that by keeping genuinely unmatched/...drafts open with a clear empty state while still hiding disabled-only command matches so manual Enter can explain why the local command is unavailable. The slash list must avoid persistent visible keyboard shortcut instruction footers; owned key handling is proven by behavior and accessible command state instead of extra instructional chrome. Slash autocomplete close semantics are owned by that same prompt command surface. The referenced OpenCode source at fetchedorigin/devcommit4519a1da329c1a4fc384054e7203ba7d06928205clears a transient slash command query whenhide()closes prompt autocomplete inpackages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx(lines 668-678), so Pulse's browser slash command popup must close Escape and outside-click locally, clear only slash-only command drafts, return focus to the composer, and leave ordinary prompts or already-submitted local command actions untouched. Closing the popup must not leave/mo,/new, or another executable command token behind for the next Enter press. Prompt-owned popups must consume their local keyboard commands before global drawer handlers can reinterpret them. The same OpenCode autocomplete source wraps previous/next selection at list boundaries inpackages/tui/src/component/prompt/autocomplete.tsxlines 552-559 at fetchedorigin/devcommit31c099be435d59bd6749ace7a9f2bb2245e6d3fa, and registers prompt-scoped previous/next/hide/select/complete commands inpackages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx(lines 600-658), and OpenCode's help dialog binds Escape as a dialog-local close command inpackages/opencode/src/cli/cmd/tui/ui/dialog-help.tsx(lines 11-16). Pulse adapts that by having slash autocomplete, resource mention autocomplete, and Assistant command help prevent default handling and stop later document-level keyboard handlers for their owned navigation, selection, and close keys. Slash and resource mention autocomplete must wrap up/down movement across the visible options, and resource mentions must expose the same selected-row state as a labelled listbox rather than only a visual hover fill. Escape inside those local surfaces must close the local popup/dialog only; it must not also close the Assistant drawer. Failed-turn retry is part of that same local chat-runtime boundary: a retryable in-memory assistant error may replay the original user turn's structured mentions, finding id, approval override, handoff resources, handoff actions, and handoff metadata, but must not reconstruct scoped context from prompt history or saved transcript prose. Provider/model route recovery is explicit user-visible recovery, not hidden chat execution. A failed turn may expose route-and-retry actions through the existing drawer model selector so operators can move from a blocked direct provider route to a configured gateway or alternate model without losing the draft or creating a parallel picker, but/api/ai/chatmust not automatically switch provider or model routes inside a single Assistant turn after the selected route fails. That prohibition includes same-model gateway equivalents and configured provider defaults: those routes may be offered as explicit recovery actions, but the selected turn owns its selected route until it completes or fails. The referenced OpenCode source at fetcheddevcommite82542b8023a8374f29c23b70ec019c8f256354emodels this boundary by keeping same-route retry visible inpackages/opencode/src/session/retry.ts, publishing retry status frompackages/opencode/src/session/processor.ts, rendering retry state inpackages/ui/src/components/session-retry.tsx, and treating provider/model changes as explicit session events inpackages/core/src/session/event.tsandpackages/opencode/src/session/prompt.ts. Pulse adapts that behavior for the drawer instead of implementing automatic cross-provider route switching. Primary interactive chat model resolution must use the explicit configured chat route or, when no explicit chat route exists, a stable provider default without calling provider model catalogs before the selected stream starts. If an explicit chat route is unconfigured, retired, or recognized as a specialized non-chat endpoint,/api/ai/chatmust fail that selected route visibly instead of substituting a same-provider default. Catalog-backed recommendation belongs to settings/model-list and explicit route-recovery flows, not/api/ai/chatfirst-response startup. Once the selected provider route starts, transient pre-output transport failures may retry the same route and must surface aprovider_retryworkflow state withattempt,max_attempts, andretry_after_msbefore sleeping. If same-route retry is exhausted before visible output, Pulse must emit a normal provider error and leave recovery to the failed-turn actions; it must not emitprovider_fallback,failed_provider,failed_model,next_provider, ornext_modelworkflow metadata. Once visible output has streamed, Pulse must also keep the failure on that visible attempt rather than silently changing route mid-turn. Frontend reducers must discard stale or nonconformingprovider_fallbackworkflow metadata without rendering a rejected-route status, switching routes, or inferring a model switch fromnext_model. Provider retry workflow states that includeretry_after_msmust render as live countdowns derived from the workflowstarted_attimestamp while the turn is active, matching OpenCode's visible retry wait behavior instead of freezing the first retry label. Assistant completion events must carry the effective model route that actually completed the selected turn. The drawer must render the initialprovider_startworkflow state as a selected-model row (Using ...) before visible content or tool activity when the event arrives. Later model-route changes may still be rendered as typedmodel_switchtranscript rows when they come from explicit route recovery or restored historical data, but the UI must label them neutrally as selected or switched routes rather than as automatic provider fallback, and completing a streamed route-switch row must not promote that route into the active model selection without an explicit user action. Interactive Assistant streams must establish the session ID and emit thesessionevent once as soon as the HTTP SSE writer is ready, before finding handoff recovery, model resolution, selected-provider startup, handoff/context prefetch, recent-session injection, inventory summary reads, tool scoping, or provider startup. The chat service then persists/ensures that same session ID while suppressing duplicate session events. This keeps the drawer's visible turn anchored while backend preparation continues and prevents simple prompts from appearing stuck in an opaque thinking state. Stored finding or handoff recovery is eligible only for a session ID supplied by the browser as an existing conversation. A session ID generated at the HTTP stream boundary for a new turn must not trigger persisted handoff lookups or legacy session discovery before the service creates the session, because that turns a new prompt into a large-history storage scan before the runtime can even report progress. After the stream is anchored, pre-model work must keep emitting neutralworkflow_stateprogress for the long phases that can otherwise look dead: context preparation, bounded canonical inventory reads for count/overview turns, and selected-provider startup. These events are runtime progress, not Assistant-authored analysis, and they must not become keyword routers, explore pre-passes, or instructions that choose the model's next action. Backend-emittedworkflow_staterows that explain real provider, context, or tool progress must remain visible while the turn is active, but they are transient runtime state rather than completed Assistant evidence. Terminaldone, terminalerror, and explicit Stop must clear workflow-status rows together with local prompt placeholders and unresolved interactive rows such as approvals, questions, and pending tools, while preserving durable model-route, content, reasoning, completed-tool, canceled-tool, and error evidence. The referenced OpenCode source at fetchedorigin/devcommit914a643ab26053df463ef78a1deadcdb223e8783writes step, tool, and text parts as soon as they arrive inpackages/opencode/src/session/processor.tslines 151-162, 322-339, 443-466, and 787-806, then renders completed text parts from stored part state inpackages/ui/src/components/message-part.tsxlines 1545-1551; Pulse adapts that part-style visibility by keeping live workflow activity visible without storing stale provider-start or waiting labels as completed transcript evidence. Selected-provider startup copy must describe the provider route as actively starting the response (for example,<Provider> is starting the response.) rather than as a passive wait label, while preserving the sameprovider_startphase and concrete model-route payload. The HTTP chat stream handler also owns visible idle progress for silent intervals after the stream has opened: if no client-visible Assistant event reaches the browser for the governed idle interval while execution is still request-bound, the handler must emit a neutralworkflow_statewith phasestream_idlerather than relying only on hidden SSE comment heartbeats. Legacy Assistant SSE routes that still use the older execute event envelope, including/api/ai/execute/streamand/api/ai/investigate-alert, share the same transport-liveness obligation while preserving their existing response payload shapes. Comment heartbeats and visible events must share a serialized writer so a progress tick cannot interleave bytes with a provider/tool event. This is a transport liveness signal only; it must replace the active workflow status, stop before terminaldone/error, and must not become persisted assistant prose or raw provider reasoning. The drawer transcript owns detailed in-flight progress for the active turn: a new assistant row must start with a neutral local preparation status until stream progress arrives, must show the current effective model route while the turn is still streaming, and must keep late workflow progress visible through the composer footer while transcript rows continue to render typed Assistant evidence. Workflow status is live progress, not answer content or a delayed walkthrough; each newworkflow_statereplaces the canonical active status immediately. The browser may retain a bounded in-flight history of those replaced labels for state continuity. When several statuses arrive in one backend/network burst, the live transcript row and active-turn composer footer must use the same paced presentation helper so the operator sees the row move through preparation, context, provider, and tool states instead of jumping to the final burst label. Single fresh status updates still render immediately, and separated transcript workflow rows stay tied to their own streamed event when a durable content/tool/approval/question boundary exists. Older neutral statuses may disappear when replaced by newer status, tool, reasoning, or answer evidence; that replacement is the intended motion signal. That history must stay live-only: once visible assistant text, tool progress, approvals, questions, terminaldone, terminalerror, or explicit Stop take over, stale workflow text and the presentation history must clear so the row does not keep saying it is waiting on a phase that has already been superseded. Live workflow and pending-tool activity must retain a per-state start timestamp so the drawer can show elapsed wait/run time for long provider starts and tool calls instead of repeating a timeless waiting label. Completed plain-text tool output must keep a bounded transcript preview visible by default, with full input/output available through expansion, so the operator sees evidence of what completed without receiving a raw output dump. Structured, binary, unavailable, or otherwise noisy output may stay collapsed behind details, but a successful text command must not degrade to an opaqueoutput availablebadge when a safe preview can be rendered. If the UI enters a loading turn before the assistant shell or firstworkflow_stateexists, the active-turn footer must show the active startup statusSending prompt.and derive elapsed time from the submitted, non-queued user turn or assistant shell timestamp. Queued follow-up messages must not reset that active-turn startup clock. The same active-turn footer owns the Stop affordance while a response is running, so interruption remains attached to the live status row instead of hiding only in the composer action cluster. The referenced OpenCode source at fetchedorigin/devcommit0875203a6c726d7a37b5ffbb770cc433c98e7cd6mutates typed message parts as events arrive inpackages/opencode/src/cli/cmd/tui/context/sync.tsx(message.part.updatedandmessage.part.delta) and represents session activity as separately updated status inpackages/opencode/src/cli/cmd/tui/context/sync-v2.tsx(session.next.step.started,session.next.text.delta, and tool state events). Pulse adapts that event-by-event activity model with browser-safeworkflow_stateand typed tool rows, including replacingstream_idlestatus while no provider event has arrived, rather than exposing terminal UI internals or chain-of-thought. The referenced OpenCode source at fetchedorigin/devcommit1399323b78a04229d9bfe00c7436d7f41770fda8applies each typed event to the active assistant message inpackages/opencode/src/cli/cmd/tui/context/sync-v2.tsx; Pulse adapts that precedence by letting typed content, model-switch, tool, approval, and question evidence own the visible row once it exists, while later neutral workflow states such as provider reasoning still replace the live footer heartbeat instead of being hidden behind generic "generating" copy. The referenced OpenCode source at commit9ed17da55ab1f7360cc0e01075f763e27fa899e9renders active assistant work as session-owned parts inpackages/opencode/src/cli/cmd/tui/routes/session/index.tsx(Session, line 185;ToolPartmapping, lines 1788-1796;InlineToolRow, line 1926) and mutates the matching active tool/text state inpackages/opencode/src/cli/cmd/tui/context/sync-v2.tsx(apply(event), line 120;session.next.tool.*, lines 276-372); Pulse's browser drawer adapts that model by keeping the transcript rows typed and stable while the active-turn footer remains a replacing live status slot for provider and workflow progress between visible parts. The same OpenCode commit applies each streamed session event as its own state mutation inpackages/opencode/src/cli/cmd/tui/context/sync-v2.tsx(apply(event), line 120;session.next.tool.input.started, line 276;session.next.tool.progress, line 317;session.next.tool.success, line 328;session.next.tool.failed, line 350), so Pulse's shared browser SSE consumer must treat opted-in Assistant progress events as paint checkpoints. Pulse's active-turn status ranking must also treat terminaltool_endevents as fresh visible activity, adapting OpenCode's in-place completed/error tool-part mutation by replacing stale provider-start footer copy with completed or failed tool activity while still preferring any separate unresolved pending tool over a terminal sibling tool. Read-only shell-backed Assistant tools must expose the redacted$ ...command preview in the active-turn status while the tool is running and when its terminal row replaces stale workflow copy; friendly intent labels may remain inside tool cards, but the live dock must not hide a known read-only command behind generic "running command" text. Write/control commands remain governed by approval and action surfaces rather than gaining a free-form live shell shortcut. The referenced OpenCode source at fetchedorigin/devcommite82542b8023a8374f29c23b70ec019c8f256354erecords shell commands as visible commits inpackages/opencode/src/cli/cmd/run/session-data.ts(startShell, lines 690-702;session.next.shell.started, lines 783-795;session.next.shell.ended, lines 803-821) and keeps non-shell tool status visible throughtoolStatuson running tool parts (lines 932-942). Pulse adapts that shell visibility by reusing its redacted read-only command preview in the browser-safe active-turn status instead of exposing raw outputs, secrets, or unrestricted control commands.frontend-modern/src/api/streaming.tsmust yield through an animation-frame-backed browser paint checkpoint, with a bounded timer fallback for inactive tabs, before draining the next opted-in event; a plain synchronous loop or microtask-only pause is insufficient because it can still render workflow/tool steps only after a batch has already finished. Chat text and reasoning deltas must paint immediately for the first delta in a run and then periodically so coalesced provider chunks do not arrive as one burst, while avoiding a frame wait for every token. Pulse's reducer must also keep the same keyed-part behavior: repeatedtool_startortool_progressevents for one backend tool ID, or the same normalized tool name when an older server omits IDs, upsert one pending tool row and collapse stale duplicate pending rows instead of replaying several near-identical steps in the transcript.frontend-modern/src/api/aiChat.tsowns the Assistant predicate: token content and hidden reasoning may continue to opt out of those checkpoints so answer streaming remains fast, while session, workflow, model-switch, tool, approval, and question events remain user-visible progress checkpoints. That same frontend stream boundary owns the deterministic dev/test fixture used for fast Assistant UX iteration:frontend-modern/src/api/aiChat.tsmay short-circuit an explicit local fixture prompt only whenimport.meta.env.DEVor test mode is active, and the fixture must emit the same typedStreamEventobjects (session,workflow_state,thinking,tool_start,tool_progress,tool_cancel,tool_end,content,done) through the normaluseChatreducer rather than rendering a separate mock transcript. The localprovider-retryfixture must exercise the realprovider_retryworkflow state, including attempt metadata andretry_after_ms, so browser proof of retry countdown behavior does not depend on external provider availability or API spend. The localtool-chainfixture must exercise consecutive tool activity with pacedtool_start->tool_progress->tool_endtransitions for one tool followed by a second live tool, so browser proof can verify completed rows compact while the next tool row replaces the active motion. Dev/test fixture prompts are also part of the Assistant command-discovery surface: slash autocomplete and Assistant command help must expose an insertable/fixturecommand in development/test builds, search it by canonical fixture names such asprovider-retry, and submit completed/fixture <name>prompts through the normal chat send path somaybeRunAIChatDevStreamFixtureremains the single local execution boundary. Production command surfaces must omit that dev command instead of advertising a fixture mode that the runtime will not intercept. The localstream-idlefixture must exercise the realstream_idleworkflow state after selected-provider startup so browser proof of visible idle liveness does not depend on making a real provider pause on demand. The localsend-holdfixture must exercise the local prompt-send boundary by emitting the session event and then holding before the first backendworkflow_state, so browser proof can captureSending prompt.without depending on real provider latency. This fixture is a local development primitive, not a product mode: it must not bypass backend governance in production, persist as a server session, or introduce any UI-only event shape that real providers cannot emit. Live answer text must be paced in the presentation layer, not throttled or mutated in the canonical stream state: appended content is revealed in short readable slices while the turn is streaming, and completed/restored messages render immediately from the full stored content. This adapts OpenCode'screatePacedValuelive markdown pattern inpackages/ui/src/components/message-part.tsxso Pulse feels active without delaying copy/export, tool boundaries, approvals, or audit evidence. Live workflow labels follow that same presentation-layer constraint: the canonical SSE/store state remains the latest typedworkflow_state, and the browser must render single fresh status updates immediately while using bounded live-only history only to pace backend/network burst replacements. This follows the current OpenCode TUI event mutation model at fetchedorigin/devcommit06d7840d1d42c9815d2d2e45e7fa4090ca4e3577, especiallypackages/opencode/src/cli/cmd/tui/context/sync-v2.tsx, wheresession.next.step.started, text deltas, and tool progress mutate the current assistant state event-by-event. Pulse uses the bounded status history only to preserve reducer continuity, not to delay audit evidence, approvals, tool boundaries, transcript export, or completed/restored messages. Completed Assistant tool rows follow the same source-anchored display policy: the referenced OpenCode commit9ed17da55ab1f7360cc0e01075f763e27fa899e9keeps ordinary tool activity terse throughGenericTool/InlineToolRowinpackages/opencode/src/cli/cmd/tui/routes/session/index.tsx, hides generic output unlessshowGenericToolOutputis enabled, and bounds output-heavy blocks withpackages/opencode/src/cli/cmd/tui/util/collapse-tool-output.ts. Pulse adapts that by keeping the completed tool action visible in the transcript, rendering only short safe plain-text output previews inline, and keeping structured JSON, unavailable output, noisy output, and the full raw payload behind Details. This preserves evidence for inspection without letting command output compete with the Assistant answer flow. The row itself owns the details trigger, not a tiny adjacent text action: completed tool rows with inspectable input or output must exposearia-expanded, keyboard activation, and a chevron affordance on the row header so opening evidence feels like expanding the tool part. That adapts OpenCode'sBasicToolcollapsible trigger inpackages/ui/src/components/basic-tool.tsx, where the trigger row and arrow own expansion while pending/running tools remain locked. Tool-row summaries are part of that same source-anchored contract, not assistant prose cleanup: when provider or backend transport supplies a function-style call such aspulse_read(...)or a friendly string backed by structured raw input, Pulse must derive the visible row from the parsed tool arguments and keep the raw transport form behind Details. This adapts OpenCode'sToolPart/InlineToolRowrendering inpackages/opencode/src/cli/cmd/tui/routes/session/index.tsxand thesession.next.tool.input.*mutations inpackages/opencode/src/cli/cmd/tui/context/sync-v2.tsx: typed tool state owns the action row, while raw invocation syntax is inspection detail. The live active-turn footer is part of the same contract: pending tools must reuse the tool-row action/command summary instead of falling back to generic tool names such asreadorquery. This keeps the always-visible progress surface aligned with OpenCode's visibleToolPartstate rather than hiding the concrete command until the transcript row catches up. Streaming thinking rows follow the same source-anchored reasoning-display contract: OpenCode commit9ed17da55ab1f7360cc0e01075f763e27fa899e9renders reasoning throughReasoningPart/ReasoningHeaderinpackages/opencode/src/cli/cmd/tui/routes/session/index.tsxand extracts only provider summary metadata withreasoningSummaryinpackages/opencode/src/cli/cmd/tui/context/thinking.ts. Pulse adapts that by showing a liveThinking:/completedThought:row with duration and optional provider summary title while keeping the raw reasoning body out of the transcript. Provider reasoning must not become fallback assistant answer text when an OpenAI-compatible non-stream response containsreasoningorreasoning_contentwithout visiblecontent. The referenced OpenCode source at fetchedorigin/devcommit4519a1da329c1a4fc384054e7203ba7d06928205maps AI SDKreasoning-start/reasoning-delta/reasoning-endevents to dedicated reasoning events inpackages/opencode/src/session/llm/ai-sdk.ts(lines 158-188), preserves assistant reasoning as atype: "reasoning"message part inpackages/opencode/src/session/message-v2.ts(lines 374-387), and keeps text and reasoning renderability distinct inpackages/ui/src/components/message-part.tsx(lines 607-615). Pulse adapts that boundary by keepingChatResponse.Contentlimited to providermessage.contentand storing OpenAI-compatible reasoning only inReasoningContent, so hidden provider reasoning cannot be rendered as the final answer by any Pulse surface that consumes non-stream chat responses. The active-turn status strip follows the same source-backed part freshness rule: OpenCode commit9ed17da55ab1f7360cc0e01075f763e27fa899e9updates live assistant parts in place throughpackages/opencode/src/cli/cmd/tui/context/sync-v2.tsx(latestTool,latestText,latestReasoning, andapply(event)), renders reasoning headers withReasoningPart/ReasoningHeaderinpackages/opencode/src/cli/cmd/tui/routes/session/index.tsx, and keeps the prompt footer as the replacing live status surface inpackages/opencode/src/cli/cmd/tui/component/prompt/index.tsx. Pulse adapts that by ranking workflow, content, hidden reasoning, and pending-tool footer copy by the freshest activity timestamp: a later answer token can replace an older tool status, and a later in-place tool progress patch can replace the answer status again without moving the transcript's chronological row. The active OpenCode-parity Assistant goal requires future parity slices to reference OpenCode's actual source implementation before changing Pulse behavior. Each slice must record the inspected OpenCode commit SHA plus source file, symbol/function, and line anchor for the behavior being adapted, including message parts, tool-state mutation, progress rendering, model/session selection, and prompt/footer interaction. Parity means adapting the proven interaction model, not guessing from screenshots or observed behavior alone. The referenced OpenCode source at fetchedorigin/devcommit54f4974546104bb72f7a0be2b75b92f9defc009bbuilds the model dialog from provider metadata, favorites, recent models, and provider sections inpackages/opencode/src/cli/cmd/tui/component/dialog-model.tsx, keeps structured{ providerID, modelID }current/recent/favorite state with provider-catalog validation inpackages/opencode/src/cli/cmd/tui/context/local.tsx, and formats the effective model label from provider/model metadata inpackages/opencode/src/cli/cmd/run/variant.shared.ts. Pulse's Assistant drawer adapts that selector workflow by keeping explicit recent model routes above the provider catalog, routing chat-specific default/override options through the shared model picker, preserving selected older models in the visible list, and accepting custom model entries only when they are explicitprovider:modelroutes that the backend chat stream can execute. Unknown recent or custom Pulse routes may survive catalog hydration only when they still have a valid provider/model shape; malformed route strings must not become selectable chat routes. The 2026-06-07 fast-model-route slice rechecked OpenCodeorigin/devcommit31c099be435d59bd6749ace7a9f2bb2245e6d3fa: the TUI command registry exposesmodel.listas/modelsplus/moand hiddenmodel.cycle_recent/model.cycle_recent_reversecommands inpackages/tui/src/app.tsxlines 725-751, the app command registry exposesmodel.chooseas/modelwith keybindmod+'inpackages/app/src/pages/session/use-session-commands.tsxlines 514-522, andDialogModelpromotes favorites/recents before provider catalog rows inpackages/tui/src/component/dialog-model.tsxlines 23-54. Pulse adapts that command model by letting the composer consume/model <provider:model>as a local route switch,/model defaultas an inherited-default reset, and/model next//model previousas recent-route cycling without sending those command strings to the provider. Malformed/model ...arguments must remain editable in the composer with a local correction message rather than being silently cleared or treated as assistant prompts. The referenced OpenCode source at commit9ed17da55ab1f7360cc0e01075f763e27fa899e9packages/opencode/src/cli/cmd/tui/component/dialog-model.tsxpasses the current structured model route intoDialogSelectat lines 146-171, andpackages/opencode/src/cli/cmd/run/footer.command.tsxmarks the current direct-mode model row with acurrentfooter at lines 796-804. Pulse adapts that by making the shared model picker mark the selected catalog, recent, override, custom, or inherited-default route as a visibleCurrentrole="option"row witharia-selected, instead of relying on background color alone. The referenced OpenCode source at fetchedorigin/devcommit147169e9b78bdd8430800f883af6b6485e5156e4runs ordinary follow-up prompts through a serial local queue inpackages/opencode/src/cli/cmd/run/runtime.queue.ts: prompts submitted behind an active ordinary turn remain visible as queued prompts, expose removal throughFooterApi.onQueuedRemove, and are removed from the visible queue before their own turn begins. Pulse's Assistant drawer adapts that behavior by keeping queued follow-ups in the transcript, showing queue position, snapshotted model-route identity, and edit/remove controls on each queued user row, and draining those rows through the existing chat-runtime queue without aborting the active model stream. The referenced OpenCode source at fetchedorigin/devcommit09d9cf01f93798939c1284fbe974b6e1f4d2759dregisters thesession.interruptcommand while a turn is non-idle inpackages/opencode/src/cli/cmd/tui/component/prompt/index.tsx, and its direct-run footer implements the same two-press interrupt guard inpackages/opencode/src/cli/cmd/run/footer.tswhile rendering the armed state inpackages/opencode/src/cli/cmd/run/footer.view.tsx. Pulse's Assistant drawer adapts that ergonomics model by letting Escape from the focused composer arm the visible Stop control first and letting the next Escape confirm the same governedchat.stop()path as the Stop button, including aborting the active stream, clearing queued follow-ups, preserving partial text, and returning focus to the composer. The App-level Assistant drawer Escape guard must not close the drawer when Escape originates inside the shared model picker; model-picker Escape is a local search/listbox close path that returns focus to the picker trigger while the drawer remains open. The referenced OpenCode source at fetchedorigin/devcommitfa2b63f850fc0a23bec2bdff9e660450d3fe7913keeps prompt/footer status visible only while the session is non-idle inpackages/opencode/src/cli/cmd/tui/component/prompt/index.tsx, and mapsturn.send,turn.wait, and tool status patches into a live footer inpackages/opencode/src/cli/cmd/run/footer.tspluspackages/opencode/src/cli/cmd/run/session-data.ts. Pulse's drawer adapts that as an active-turn status strip: no idle diagnostics, but while a turn is loading it must show waiting, current tool/workflow progress, or generating status even when the transcript already contains an in-flight assistant row. The referenced OpenCode source at fetchedorigin/devcommit09d9cf01f93798939c1284fbe974b6e1f4d2759dresolves the direct-run wait whenpackages/opencode/src/cli/cmd/run/stream.transport.tsverifies the session is idle, mapsturn.idleto an idle footer phase inpackages/opencode/src/cli/cmd/run/footer.ts, and then flushes scrollback separately from later state sync. Pulse's Assistant drawer adapts that completion boundary by clearing the visible active turn as soon as the chat stream has processed its terminaldoneorerrorevent; async conversation/session-list refresh may continue afterward, but it must not keep the composer footer or transcript row saying the assistant is still generating. When multiple governed tools are pending, completed tools must not blank the status while another tool is still running, and the status heartbeat should follow the latest progressed pending tool without reordering the transcript's chronological tool rows. Pulse adapts this with a mutable footer status and inline pending rows that show the current activity plus elapsed time while keeping large command output collapsed. The referenced OpenCode source at fetchedorigin/devcommit1399323b78a04229d9bfe00c7436d7f41770fda8updates the existing typed tool part throughupdateToolCallinpackages/opencode/src/session/processor.tsand renders ordered assistant content parts throughAssistantMessage/AssistantToolinpackages/opencode/src/cli/cmd/tui/feature-plugins/system/session-v2.tsx; Pulse pending tool progress must therefore mutate the matching row in place and carry separate activity freshness metadata for the footer instead of moving an earlier active tool row below later tools when progress arrives. Assistant experience parity claims against OpenCode must cite the inspected OpenCode commit, source file, source symbol/function, and line anchor that defines the behavior being adapted; observed UI behavior alone is not sufficient governance evidence for future parity slices. The referenced OpenCode source at commitf750deaa3e95098fdde5fb00305b273e43c5b2cdmutates a single tool part frompendinginput throughrunningprogress to completed/error inpackages/opencode/src/cli/cmd/tui/context/sync-v2.tsx, with tool metadata updates flowing throughpackages/opencode/src/session/tools.ts. Pulse's stream contract adapts that model withtool_start,tool_progress, andtool_endevents that update the same visible pending tool row in place. The referenced OpenCode source at fetchedorigin/devcommit09d9cf01f93798939c1284fbe974b6e1f4d2759dcreates and mutates tool parts as input arrives inpackages/opencode/src/session/processor.ts(ensureToolCall,tool-input-start,tool-input-delta,tool-input-end), applies those input/running/completion updates inpackages/opencode/src/cli/cmd/tui/context/sync-v2.tsx(session.next.tool.input.started,.delta,.ended,.called,.progress,.success,.failed), and renders the same live tool part inpackages/opencode/src/cli/cmd/tui/routes/session/index.tsx. Pulse's Assistant stream must mirror that timing: model-selected tools become visible when the tool name is known, streamed function-argument deltas, including Anthropicinput_json_deltachunks, surface astool_progress, later argument/progress updates mutate the pending row, and completion replaces that row rather than appending a delayed batch of steps. Waiting until[DONE],message_stop, or execution completion recreates the delayed batch feeling this contract is meant to prevent. Policy-hidden placeholder attempts remain governed by the runtime and may resolve the pending row into a durable skippedtool_cancelrow instead of either persisting a failed tool card or hiding the activity. If a terminaltool_endortool_cancelreaches the browser without a matching pending row, the frontend must still append terminal tool evidence instead of dropping the only visible activity. The 2026-06-07 Pulse slice rechecked OpenCodeorigin/devcommit1025540fcc2a69609a0131a7168300205656d728, wherepackages/core/src/session/event.tsandpackages/core/src/session/message-updater.tskeep tool lifecycle events as typed message parts through terminal success/failed states; Pulse adapts that lifecycle shape by making skipped policy/runtime tool calls visible as terminal transcript rows. The 2026-06-07 live-tool-evidence slice rechecked OpenCodeorigin/devcommit31c099be435d59bd6749ace7a9f2bb2245e6d3fa:packages/core/src/session/event.tslines 307-366 define live tool input and bounded progress events,packages/core/src/session/message-updater.tslines 247-293 mutates the same assistant tool part from pending input to running progress, andpackages/ui/src/components/basic-tool.tsxlines 90 and 246-253 keep pending/running tools as active compact rows rather than expanding details by default. Pulse adapts that source model for infrastructure operations by keeping the compact live row primary while allowing an operator to expand pending/running tool rows for copyable raw input and current progress evidence; raw provider envelopes stay behind Details and are used there when structured arguments are still placeholder{}/[]. The local dev/fixture pending-toolstream is the canonical no-provider browser proof fixture for this live pending-tool Details window and must keep a paced running state long enough to exercise the row before it resolves. The 2026-06-07 skipped-evidence slice rechecked OpenCodeorigin/devcommit31c099be435d59bd6749ace7a9f2bb2245e6d3fa:packages/core/src/session/event.tslines 307-336 define replayable tool input start/delta/end events and lines 386-399 define terminal failed tool events,packages/core/src/session/message-updater.tslines 247-267 and 318-339 preserve and update the same assistant tool part through input and failure, andpackages/opencode/src/session/tools.tslines 48-65 and 180-200 keep terminal tool input/output evidence. Pulse skipped/canceled tool rows must therefore keep raw tool input and skip reason behind the same expandable, copyable Details panel used for completed terminal tool rows, while the compact row remains the operator-readable action summary. While streamed arguments are still invalid, incomplete JSON, or incomplete provider-style function-call input, the frontend must use theraw_inputfragment to show a safe partial command/path/query summary instead of a blank{}request row, then replace it with the structured summary once parsing succeeds. The 2026-06-07 Pulse slice rechecked OpenCodeorigin/devcommiteffd27b23900720a53e965396ff1a105c1f7e9c8:packages/opencode/src/session/processor.tslines 431-451 appendtool-input-deltatext to the same call, andpackages/opencode/src/cli/cmd/tui/context/sync-v2.tsxlines 276-294 render and mutate that pending tool part as input arrives. The referenced OpenCode source at fetchedorigin/devcommit1399323b78a04229d9bfe00c7436d7f41770fda8keeps tool invocations as durable message parts inpackages/opencode/src/session/processor.ts(ensureToolCall,updateToolCall,completeToolCall) and renders the typed part immediately inpackages/opencode/src/cli/cmd/tui/feature-plugins/system/session-v2.tsx(AssistantTool,pendingInput,toolComplete). Pulse's Assistant chat stream must also preserve that event-by-event feel when HTTP or browser buffering delivers several SSE messages in one network chunk: the frontend stream consumer must let the browser paint between buffered Assistant progress/tool/status events for chat streams instead of synchronously draining a coalescedtool_start/tool_progress/tool_endbatch in one JavaScript task. Ordinary content-token streaming must also get an immediate first-delta paint checkpoint and periodic checkpoints for long coalesced chunks, but must not be frame-throttled on every token by that progress-batch safeguard. Mock-mode Assistant streaming is a canonical fixture for that same runtime contract, not a frontend-only demo. Whenmockmode.IsEnabled()is true,chat.Service.createProvidermust return the internal mock Assistant provider before invoking injected provider factories or configured external providers, andchat.Service.ExecuteStreammust short-circuit the user turn only after the durable session is ensured and the user message is persisted. The fixture must emit the same browser-facing event family used by real turns: anchoredsession/prepare events from the normal entry path followed by mockworkflow_state,tool_start,tool_progress,tool_end, responseworkflow_state, content chunks, and terminaldone. It must also persist a real assistant message with model routepulse:mock-assistant, a successfulpulse_querytool call, and a successfulpulse_readtool call whose progress event includes provider-style raw input such aspulse_read(...). That fixture keeps session restore, transcript tooling, stream-contract tests, and live browser checks exercising the same backend-to-browser shape as a real provider/tool turn, including the raw function-call syntax Pulse must summarize instead of rendering. Pulse-owned local Assistant routes such aspulse:local-inventoryandpulse:mock-assistantmust render through named labels (Pulse inventory,Pulse mock Assistant) in shared model picker and transcript chrome; the raw route IDs are implementation details, unlike external provider route IDs that may remain visible for disambiguation. Mock mode is also an effective runtime-config overlay:internal/api/ai_handler.gostartup, restart, config sync, and tenant service initialization must treat Assistant as enabled in memory when mock mode is active, without saving or mutating the persistedAIConfig, so/api/ai/chatand Assistant sessions are available with zero configured external providers or models. The live fixture must pace its status, tool, and content events enough for the browser to paint each state in sequence; unit tests may disable that pace, but the dev fixture must not collapse into a single final completed row. This fixture must not emitprovider_fallbackin non-mock mode and must not become Pulse-authored remediation or routing behavior. The referenced OpenCode source at fetchedorigin/devcommit4519a1da329c1a4fc384054e7203ba7d06928205publishes tool-call and tool-result events incrementally inpackages/core/src/session/runner/publish-llm-event.ts, mutates assistant tool parts through pending/running/completed states inpackages/core/src/session/message-updater.ts, and renders the current tool part state inpackages/ui/src/components/message-part.tsxthroughpackages/ui/src/components/basic-tool.tsx. Pulse adapts that source model by making the mock fixture prove the same visible state sequence without copying OpenCode's editor-specific tool catalog. The referenced OpenCode source at fetchedorigin/devcommit09d9cf01f93798939c1284fbe974b6e1f4d2759drenders tool-specific inline labels such asRead <path>,Grep "<pattern>",WebFetch <url>, and bash commands inpackages/opencode/src/cli/cmd/tui/feature-plugins/system/session-v2.tsx. Pulse's compact tool rows must follow that operator-language model: the row should summarize the actual governed action (search "prowlarr",list active alerts,topology summary,Inspect devices on current resource, or exact governed write commands where the command itself is the operator decision) instead of exposing only internal action names such asQUERY search,exec, provider function calls, or raw JSON; raw input and full output stay available behind Details. The referenced OpenCode source at fetchedorigin/devcommit06d7840d1d42c9815d2d2e45e7fa4090ca4e3577renders Bash tool state throughBashinpackages/opencode/src/cli/cmd/tui/feature-plugins/system/session-v2.tsx(lines 701-735), where the pending row is replaced by the concrete command and completed block output keeps$ <command>visible above bounded output. Pulse adapts that for read-style infrastructure tools by keeping the operator action label as the primary row and adding a sanitized$ ...command preview below it when the tool input carries a shell command; raw provider envelopes such aspulse_read(...), raw JSON, and full command output remain Details-only. Any visible command-copy affordance must copy that same sanitized command preview rather than the raw provider/tool input. The referenced OpenCode source at commit9ed17da55ab1f7360cc0e01075f763e27fa899e9also renders pending tool rows throughInlineToolpending text inpackages/opencode/src/cli/cmd/tui/feature-plugins/system/session-v2.tsx(InlineTool, lines 563-651;Bash, lines 701-735;Read, lines 758-770;Grep, lines 788-790). Pulse adapts that by using action-specific pending copy such asWriting command...,Preparing query..., andReading storage...before streamed tool arguments are parseable, then replacing that copy with the governed command/query/resource summary as soon as input arrives. The referenced OpenCode source at fetchedorigin/devcommit4519a1da329c1a4fc384054e7203ba7d06928205renders completed bash and generic tool output inside the tool block and usescollapseToolOutputinpackages/opencode/src/cli/cmd/tui/feature-plugins/system/session-v2.tsx(GenericTool, lines 526-559;Bash, lines 701-731) to keep long output bounded. Pulse's completed tool rows must adapt that by previewing short actionable plain-text output for successful and failed tools while keeping long output bounded behind Details. Structured JSON, unavailable output, truncated successful output, and the full raw payload stay behind Details. The browser implementation owner isfrontend-modern/src/components/AI/Chat/ToolExecutionBlock.tsx; completedtool_endevents preserve streamedraw_inputfor the same readable command summary used while the tool is pending. The 2026-06-07 completed-output badge slice rechecked OpenCodeorigin/devcommite82542b8023a8374f29c23b70ec019c8f256354ein the samesession-v2.tsxGenericTool/Bashrenderers (lines 527-559 and 702-735): overflow is communicated through the expandable tool block, not through transport-sized copy in the compact row. Pulse adapts that by keeping exact output type/size in the accessible label and Details panel while the visible compact badge says onlyoutput available. The 2026-06-07 transcript command-evidence slice rechecked OpenCodeorigin/devcommite82542b8023a8374f29c23b70ec019c8f256354einpackages/opencode/src/cli/cmd/run/session-data.ts: OpenCode keeps tool activity readable in session history while stripping shell echo noise from assistant text. Pulse adapts that by including the same sanitized$ commandpreview shown in live read-tool rows in copied/exported Assistant transcripts. Transcripts must still exclude raw provider call syntax such aspulse_read(...), raw JSON tool payloads, obvious command secrets, and unbounded raw tool output by default. The 2026-06-07 active-dock pacing slice rechecked current OpenCodeorigin/devcommite82542b8023a8374f29c23b70ec019c8f256354einpackages/tui/src/feature-plugins/system/session-v2.tsx:InlineTool(lines 564-651) keeps one visible activity row that moves from pending copy to completed copy, whileBash(lines 702-740) keeps shell activity visible through the same inline/block transition. Pulse adapts that for the Assistant dock by pacing burst-arriving workflow status history locally soPreparing context,Reading inventory, and provider wait states appear in sequence; statuses that arrive one at a time still replace immediately. The referenced OpenCode source at fetchedorigin/devcommitfa2b63f850fc0a23bec2bdff9e660450d3fe7913also keeps assistant text, reasoning, and tool invocation as typed message parts inpackages/opencode/src/session/message.ts, whilepackages/opencode/src/session/processor.tsupdates text and reasoning parts through*-deltaevents instead of rendering raw provider tool-call syntax as assistant prose. The referenced OpenCode source at fetchedorigin/devcommit1399323b78a04229d9bfe00c7436d7f41770fda8renders reasoning withAssistantReasoningandReasoningHeaderinpackages/opencode/src/cli/cmd/tui/feature-plugins/system/session-v2.tsx, separate fromAssistantTextandAssistantTool. Pulse's frontend stream reducer must preserve the same user-facing invariant: visible transcript content is typed assistant text, a neutral typed thinking-progress row, or a typed Pulse tool/approval/question row. The thinking-progress row may expose live activity state such asThinking.../Thinking complete, but it must not render raw provider reasoning text. Suspicious compacted provider prelude text that looks like tool-call narration must be buffered until it is proven to be normal answer text or stripped when a raw tool-call marker arrives; it must not flash as run-on prose such asI'llcheckthedevicenodes...while the actual governed tool row is still being assembled, and it must not be promoted to final assistant answer text when the stream reachesdonewithout proving a normal answer. The referenced OpenCode source at fetchedorigin/devcommit1025540fcc2a69609a0131a7168300205656d728keeps rendered answer text as typed text parts inpackages/ui/src/components/message-part.tsxand usescreatePacedValueonly as a presentation-layer reveal; Pulse adapts that by preserving typed content exactly while suppressing malformed compacted tool-prelude residue rather than inventing spaces or storing it as prose. A pre-tool assistant preamble such as "I'll check that" does not satisfy the final-answer contract after Pulse executes tools. The final-response guard must require non-empty assistant text after the latest user/tool-result anchor; otherwise it must make the bounded no-tools summary call or emit the deterministic fallback summary. This adapts the referenced OpenCode source at fetchedorigin/devcommit06d7840d1d42c9815d2d2e45e7fa4090ca4e3577:packages/opencode/src/session/llm/ai-sdk.tsmaps tool input/call/result stream events into typed tool events (lines 190-246),packages/opencode/src/session/message-v2.tsserializes completed tool results and reasoning as ordered provider message parts (lines 340-390), andpackages/ui/src/components/message-part.tsxonly renders non-empty typed text/reasoning/tool parts throughrenderableandAssistantParts(lines 607-728). Pulse adapts that ordered-part invariant insideinternal/ai/chat/agentic_final.gobecause Pulse's transcript is persisted as messages plus tool-result anchors rather than OpenCode's per-part store. Streamed provider startup and mid-stream progress must be bounded by the configured Assistant request timeout, the OpenAI-compatible SSE response-header guard, and the per-chunk SSE body-read guard adapted from OpenCode's providerwrapSSEsource. The referenced OpenCode source at fetchedorigin/devcommitba57718b0516c7a8670d1e820b1a24146a8b8262wrapstext/event-streamresponses inpackages/opencode/src/provider/provider.tsso each stream read either yields a chunk or aborts within the configuredchunkTimeout, distinct from the response-header timeout. Pulse's OpenAI-compatible Assistant transport follows that split: transient startup failures may retry once before surfacing failed-turn recovery, but a stalled route must not leave the user in an opaque first-token or mid-answer wait for the full provider timeout. Assistant turn tool exposure is backend-owned runtime behavior, not a provider whim or frontend polish concern, and it is model-owned at the manifest boundary: every interactive turn that reaches the selected model carries the full governed tool manifest fromtoolsForExecutionMode, and the model decides whether to answer directly, ask a clarification, or call tools inside Pulse policy. Pulse must not classify prompt wording into text-only or query-only tool scopes — greetings, exact-reply diagnostics, "no tools"/"before using tools" phrasing, inventory/count/list/breakdown wording, alert/finding wording, and prompt length are all model-visible text, not Pulse routing signals. The formerassistantToolScopeForPromptkeyword router, its query-only manifest filter, its conversational/ direct-text text-only scopes, and its query-only topology prefetch (inject compact inventory into the user message, then withhold tools) are removed and must not be reintroduced; that family withheld tools from real lookups ("hows esphome") and zeroed the manifest on false-positive phrases ("before using any tools, tell me your plan"). The only manifest-boundary exception is the contract-sanctioned context-only resource handoff turn (see Completion Obligations).TestService_ExecuteStream_ToolManifestIsModelOwned(internal/ai/chat/service_tooling_test.go) andTestService_ExecuteStream_InventoryBreakdownIsModelOwned(internal/ai/chat/service_execute_additional_test.go) are the regression proofs that previously-scoped prompt families all reach the model with the same governed manifest and an unmodified user message. The system prompt's missing-target policy is resolve-before-asking, not ask-first: when a command or diagnostic request names no target, the Assistant must first use read-only query/topology tools to identify plausible targets, proceed against a sole plausible match for read-only diagnostics while naming the target in the answer, and ask the operator only when several plausible targets remain or the action changes state. The earlier ask-first framing ("Missing target information is not a safe default... ask for the missing target") made the Assistant deflect every "run X" request back to the operator — including on single-host deployments where no real ambiguity exists — instead of investigating like a competent operator (the OpenCode-parity gap this supersedes). Placeholder targets remain forbidden in all modes: the model must never guess an unresolved target or substitutecurrent_resourceoutside an attached-resource turn, and write actions still require an explicit or operator-confirmed target.TestBuildSystemPrompt_CurrentResourceRequiresResourceHandoff(internal/ai/chat/service_tooling_test.go) pins this boundary. Deterministic count-only inventory prompts remain the single Pulse-owned local answer shortcut, and it is an answer path, not tool selection: when canonical topology state already carries the complete aggregate counts,internal/ai/chatshould answer locally by streaming normal typed assistant content anddonebefore provider attempt creation. This adapts the referenced OpenCodemessage.tsandprocessor.tstyped part/delta model at commitfa2b63f850fc0a23bec2bdff9e660450d3fe7913: locally owned text is still transcript text, but it must not wait on a remote provider or pretend that shell/tool inspection produced the answer. The completion metadata for that path should identify the effective route aspulse:local-inventoryso the transcript label reflects Pulse-owned local runtime output instead of the operator's selected remote model. Explicit or qualified operator intent is the escape hatch from this shortcut, not a Pulse-authored tool choice: prompts that ask to use tools, request a read-only attempt, provide a shell/read command or path such asls /dev | wc -l, or qualify the count beyond pure inventory ("how many vms have errors") must reach the selected model/provider with the full governed Assistant tool manifest instead of answering from local inventory. Prompt classification (assistantPromptQualifiesForLocalInventoryCountininternal/ai/chat/service.go) may only suppress the shortcut and send the turn to the model — its false positives fail safe, to the model — and the selected model still decides whether to callpulse_read, answer directly, or ask a clarification. This adapts the referenced OpenCode source at fetchedorigin/devcommit4519a1da329c1a4fc384054e7203ba7d06928205, wherepackages/core/src/session/message-updater.tsmutates tool input, called, progress, success, and failed states as typed session events (lines 247-318), by preserving the operator's explicit tool intent until Pulse can stream a visibletool_start/tool_progress/tool_endpath instead of collapsing the turn into unrelated inventory prose. The Assistant transcript should show compact activity as work happens: local context reads, governed tool names, provider routing, fallback, and first-token waiting must appear as the latest replacing live status/header status rather than a dead-looking wait state, a dumped list of transient phases, or a delayed replay of stale status history. This adapts the referenced OpenCode source at fetchedorigin/devcommit914a643, where the app session timeline derives visible activity from the current session status and streamed message/tool parts instead of pacing old status labels back through the UI. Hide or collapse unsafe details and bulky outputs, but do not hide the fact that Pulse is running a local read, invoking a governed tool, or waiting on a provider. Stream-idle heartbeats must inherit the selected provider/model route when Pulse has that route, so the visible row reads as route-specific liveness instead of falling back to generic Assistant waiting copy. Pulse must not rewrite the inputs of a model-chosen tool call (the former summary-onlypulse_querytopology input default is removed with the query-only scope);pulse_querydefaults are owned by the tool definition the model sees, not by post-hoc input mutation. Assistant answer style should stay operational and must not use emoji, warning icons, or decorative symbols unless the user explicitly asks for that tone. The system prompt for a turn must describe only the tools actually offered to the provider for that turn; generic tool-governance prose must not name tools hidden from the current manifest. Deterministic provider-request capture is the primary proof harness for this fast path; live OpenRouter/browser checks are final smoke tests, not the iteration loop. Restored Assistant sessions must hydrate saved assistant content and persisted tool calls into the same transcript event shape used by live streams so switching sessions does not hide prior tool evidence or collapse the resumed conversation into a text-only transcript. Saved Assistant message history is part of that same output contract: service reads andGET /api/ai/sessions/{id}/messagesmust return a client-safe projection that normalizes collection fields, stripsreasoning_content, and sanitizes stored assistant prose before the browser restores a transcript. Provider reasoning may remain in runtime-only history for model continuity, but it must not serialize through public session history or resumed drawer state. The referenced OpenCode source at fetchedorigin/devcommit09d9cf01f93798939c1284fbe974b6e1f4d2759dstores message parts as typed text, reasoning, andtool-invocationentries whose tool invocation state iscall,partial-call, orresultinpackages/opencode/src/session/message.ts. Pulse adapts that restored transcript contract by folding persisted internal tool-result transport messages back into the browser-safe assistanttool_callsprojection as completedoutputplussuccessstate, preserving the assistant message's effective model route, and omitting the internal tool-result messages from the restored browser transcript. Pending tool progress is part of the same transcript contract: pending tool stream events must render inline as compact rows in the assistant turn, transition torunningorwaitingthroughtool_progress, and resolve in place ontool_endinstead of disappearing until completion or relying only on the drawer-level status bar. The referenced OpenCode source at fetchedorigin/devcommitba57718b0516c7a8670d1e820b1a24146a8b8262emits an ACPtool_callwhen a tool part is known and then sendstool_call_updatepatches for pending, running, completed, and failed states throughpackages/opencode/src/acp/event.tsandpackages/opencode/src/acp/tool.ts; Pulse adapts that by keeping the live pending row's current progress text visible inside the transcript at drawer width, not only in the footer/status strip. The drawer transcript must treat those in-place progress patches as fresh visible activity even when the message count and stream-event count do not change. The referenced OpenCode source at commit9ed17da55ab1f7360cc0e01075f763e27fa899e9appliessession.next.tool.progressby mutating the latest matching tool part inpackages/opencode/src/cli/cmd/tui/context/sync-v2.tsx(apply(event), line 120;session.next.tool.progress, lines 317-326) and renders the same session-owned tool part throughpackages/opencode/src/cli/cmd/tui/routes/session/index.tsx(ToolPartmapping, lines 1788-1796;InlineToolRow, line 1926). Pulse adapts that by deriving transcript scroll/freshness from the latest message's activity fingerprint, including pending-tool progress andupdatedAt, instead of using appended-event count as the only streamed activity signal. Assistant session listing is a history-browsing path, not a send-path lock. The session store must not hold its shared mutex while scanning and parsing every persisted session file for/api/ai/sessions; writes must remain atomically readable through temp-file plus rename so a large recent-session list cannot blockEnsureSession,AddMessage, or stored handoff reads on a new prompt. Session listing cost must not scale with stored transcript bytes.SessionStore.Listserves summaries from a per-file cache validated by each file's (modTime, size), re-reading only files that changed since the last list, and a cache-miss parse must not deep-decode message or redo payloads (only their counts feed the summary). The cache persists as.sessions_index.jsonbeside the session files so the first list after a restart stays index-speed; the index is best-effort and self-healing (missing, corrupt, or stale entries only cost a re-parse, never a wrong summary, and dotfiles are never listed as sessions).writeSessionandDeletekeep the cache and index coherent on every mutation. Assistant session navigation must provide a searchable history path in the drawer-owned picker, using the canonical/api/ai/sessionscontract rather than a separate recent-chat store. Search is applied before result limiting so the picker can narrow older sessions without hiding matches behind a recency cap, the picker must open immediately with cached sessions while any refresh/search is still running, and loading/error states must stay inside the picker instead of making the main send path look busy. The referenced OpenCode source at fetchedorigin/devcommit4519a1da329c1a4fc384054e7203ba7d06928205implementsSessionSwitcherDialoginpackages/opencode/src/cli/cmd/tui/feature-plugins/session/dialog.tsxwithcreateDebouncedSignal("", 150),sdk.client.session.list({ search: input.query, limit: 30, ...input.filter }), recency ordering throughorderByRecency, and pinned/current session options before navigation. Pulse adapts that source workflow with a debounced Assistant session search field, bounded recent-session refreshes withGET /api/ai/sessions?limit=30, searched refreshes withGET /api/ai/sessions?search=...&limit=30, and searched result rows that resume through the same session-loading path as the normal recent list. Assistant session forking is a runtime-backed session workflow, not a header-only UI affordance. The referenced OpenCode source inpackages/opencode/src/cli/cmd/tui/routes/session/index.tsxregisterssession.forkas a first-class session command and opensDialogForkFromTimelinefrom the current session route. Pulse adapts that source behavior throughPOST /api/ai/sessions/{id}/fork: the localchat.Servicemust route toSessionStore.Fork, and the store must create a fresh durable session ID with fresh create/update timestamps while cloning the saved transcript and browser-safe model handoff summary. Copied messages keep their per-session message/tool IDs so assistant tool-call and tool-result relationships remain intact inside the fork. Forking must not mutate the source session, must continue through normal session list/load contracts, and must not returnnot_implementedfrom the local runtime. OpenCode-style session file diff/revert workflows do not apply to Pulse Assistant sessions: Pulse does not own local code-file edits, and infrastructure mutations are governed through approval/action history. The Settings maintenance surface must not advertise session file changes or session revert actions, and legacy direct calls to/api/ai/sessions/{id}/diff,/revert, and/unrevertmust fail explicitly with501 Not Implementedrather than returning a success-shaped placeholder. The picker trigger and result rows are part of that source-backed workflow: the trigger must expose its accessible name and expanded state, opening the picker must focus search, and each result must be a keyboard-addressable option with up/down, page, home/end, and Escape navigation from both the focused search field and focused result rows. Session-picker Escape is a local close path that returns focus to the session trigger and must consume the keypress so the App-level Assistant drawer Escape guard does not also close the whole drawer. Delete remains a named row action instead of a mouse-only affordance. Message-level copy is part of the same OpenCode-aligned message action surface, but Pulse must keep it browser-safe and role-neutral rather than implying unsupported message-level fork/revert semantics. The referenced OpenCode source inpackages/tui/src/routes/session/dialog-message.tsxexposesmessage.copyby collecting non-synthetic text parts for the selected message. Pulse adapts that by allowing visible user and completed assistant transcript rows to copy their own visible text through the shared clipboard fallback helper; hidden reasoning, raw tool input/output, provider envelopes, and scoped handoff metadata remain excluded unless the operator explicitly opens a raw Details surface. Assistant session rename is part of that same source-backed session workflow. The referenced OpenCode source inpackages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsximportsDialogSessionRenameand invokes the runtime session rename action from the session list, so Pulse must not leave rename as local picker state. Pulse owns the equivalent throughPATCH /api/ai/sessions/{id},chat.Service.RenameSession, andSessionStore.Rename; the drawer picker may render the edit inline, but saving must persist the normalized title, return the updatedChatSession, update the visible row without closing the picker, and leave messages, handoff context, approvals, and tool evidence unchanged. Session titles follow OpenCode's generated-title model: after a session's first exchange completes, the chat runtime upgrades the auto-truncated first-prompt placeholder to a short model-generated title (chat.Service.generateSessionTitle,internal/ai/chat/session_title.go). The upgrade runs after the done event reaches the client (no turn latency), only on the first exchange, and never overwrites a title the user set: any title that differs from the placeholder is left alone, with a re-check before persisting to lose races against a concurrent rename. Title requests pass the same model-boundary sanitizer as normal turns, record usage asassistant_session_title, and a failed or timed-out title call leaves the placeholder in place rather than failing anything user-visible. Assistant turn undo/redo is also a source-backed session repair workflow, not a transcript-local delete button. The referenced OpenCode source inpackages/opencode/src/cli/cmd/tui/routes/session/index.tsxregisterssession.undoandsession.redoas first-class session commands and uses runtime revert/unrevert responses to restore the user's prompt into the composer. Pulse adapts that behavior throughPOST /api/ai/sessions/{id}/undoandPOST /api/ai/sessions/{id}/redo: the backend session store must remove the latest user-authored turn plus following assistant/tool messages as one durable turn, persist a bounded redo stack, exposecan_redoon the browser-safe session summary, clear redo state when a new message or trim changes the transcript, and keep forked sessions independent from the source redo stack. The drawer must restore the removed prompt, structured mentions, finding id, handoff metadata, approval/autonomous override, and selected model route when available so the operator can edit and resend without reconstructing hidden context from transcript prose. The empty Assistant drawer may surface recent non-empty sessions as direct resume actions using the backend session list already owned by the drawer; it must not create a parallel recent-chat store or product-authored prompt shortcut path. Assistant output hygiene is part of the same boundary: provider reasoning and raw serialized tool-call artifacts must never render as assistant transcript prose or serialize into client-facing chat streams. Browser-bound Assistant stream events must pass throughchat.StreamEvent.ClientSafe(); providerthinkingchunks are runtime-only and may be retained internally for model continuity, but they are dropped before the browser/API boundary. The referenced OpenCode source at fetchedorigin/devcommit1399323b78a04229d9bfe00c7436d7f41770fda8stores assistant output as typed text, reasoning, andtool-invocationparts inpackages/opencode/src/session/message.tsand mutates those typed parts through explicit stream events inpackages/opencode/src/cli/cmd/tui/context/sync-v2.tsx; Pulse adapts that invariant by stripping operational decorative status glyphs, warning icons, and check/cross badges from browser-safe assistant prose while preserving ordinary Unicode answer text such as units. The agentic stream may translate the first private provider reasoning delta before visible output into a neutralmodel_thinkingworkflow status so the drawer shows live activity without exposing chain-of-thought. Neutral progress comes from the active stream state, workflow status, or governed tool/question/approval events, not from a reasoning body. Pulse tool invocations must surface only through governedtool_start/tool_end, approval, or question blocks; if a provider emitspulse_*/patrol_*calls, DSML, XML/function-call envelopes, or JSON tool-call shapes as text content, the chat runtime must strip them before streaming, persistence, and frontend rendering. Compacted no-whitespace internal prelude text attached to a leaked tool invocation is part of that same artifact and must be suppressed or retracted from the current stream segment instead of rendered as assistant prose; if that compacted prelude is still pending when the stream closes, terminal flush must drop it rather than turning unreadable provider residue into the final answer. Drawer-level loading/progress status must stay scoped to the active turn and must collapse back to quiet when the turn is idle; it may mirror the latest active tool/workflow state as a compact heartbeat, but raw outputs, restored evidence, errors, and completed details stay in the transcript row that owns the turn. Completed tool rows in the drawer may show compact tool name, action summary, status, and an explicit details affordance, but raw tool input/output JSON must not render in the default transcript. Token accounting and other provider metadata remain runtime/accounting data, not normal transcript prose. Background provider-readiness checks are diagnostics and must stay quiet while idle unless they produce an actionable issue; checking status may surface during an active send, but an open empty drawer must not look busy just because Pulse is verifying the selected route. - Add or change Patrol, alert-analysis, or remediation transport through
internal/api/ai_handlers.go,internal/api/ai_intelligence_handlers.go, andfrontend-modern/src/api/patrol.tsProvider preflight diagnostics returned frominternal/api/ai_handlers.gomust reuse the Patrol runtime failure classifier ininternal/ai/and expose only safe operator-facing cause, summary, recommendation, model, and action fields. Raw provider response bodies and transport errors may be logged server-side or attached as redacted internal Patrol evidence where governed, but they must not be returned through the browser provider-test contract. Anthropic runtime provider execution must be API-key backed. Legacy Anthropic OAuth tokens may remain in encrypted settings only as cleanup state; they must not make Anthropic configured, instantiate an OAuth-backed provider, refresh tokens, or send model requests. OAuth start/exchange and callback handling must fail closed, while disconnect may clear stored legacy tokens. Patrol findings history transport must stay bounded when resolved findings are included:/api/ai/patrol/findings?include_resolved=1defaults to a 200-finding limit and caps explicit limits at 500, and the frontend Patrol client/store must send the same bounded history request once the Resolved or All view has made expanded history sticky. Per-finding suppression creation is similarly narrow by default: the browser helper must require a concrete resource ID and category, while backend broad/wildcard suppression scopes require an explicitallow_broad_scoperequest from a dedicated rule management surface. - Add or change AI usage/cost dashboard presentation through
frontend-modern/src/components/AI/AICostDashboard.tsxandfrontend-modern/src/utils/aiCostPresentation.ts - Add or change AI provider, control-level, or chat/session presentation through
frontend-modern/src/components/AI/Chat/,frontend-modern/src/utils/aiProviderPresentation.ts,frontend-modern/src/utils/aiProviderHealthPresentation.ts,frontend-modern/src/utils/aiControlLevelPresentation.ts,frontend-modern/src/utils/assistantPageContext.ts, andfrontend-modern/src/utils/aiChatPresentation.tsAI provider/model presentation must preserve the model transport route when the selected provider is a gateway. OpenRouter-routed model IDs such asopenrouter:deepseek/...must render with an explicitvia OpenRouterlabel in the shared picker, System AI settings status, and inherited default descriptions unless the server-supplied model name already carries that route. Assistant transcript rows must carry and render the effective model route that produced each live or restored response so route recovery and mixed-provider sessions remain auditable. Direct provider models must not gain a gateway label, but explicit direct route strings such asdeepseek:deepseek-v4-promust still render with provider identity rather than collapsing to a bare-looking model payload. Gateway route strings must still render as readable provider/model labels before catalog hydration; this mirrors OpenCode's source pattern of retaining structuredproviderID/modelIDmetadata and presenting catalog names with ID fallbacks rather than leaking raw route storage IDs into primary chat chrome. Assistant model selector actions must remain route-distinct: if the configured chat override resolves to the same route as the effective default or the already selected session model, the drawer must not render a duplicate override action. Assistant provider-readiness actions must send operators to the canonical Pulse Intelligence > Provider & Models route/settings/pulse-intelligence/provider;/settings/system-airemains a compatibility alias for old deep links rather than the href emitted by new Assistant repair actions. Assistant discovery-context hints must use the same product language as Pulse Intelligence settings: the visible hint isDiscovery is off, while the body explains that enabling it gives Assistant real service, version, and command context instead of generic guidance. The hint must not reintroduce the oldWorkload Discoverylabel as a separate product concept. Authenticated shell Assistant launches must usefrontend-modern/src/utils/assistantPageContext.tsto attach factual current-route context before the drawer opens. That helper may identify the current platform, Patrol, Alerts, Settings, or unknown Pulse view and provide neutral briefing metadata, but it must not synthesize a prompt, require a tool choice, or turn Assistant back into the primary operations front door. Assistant route and control chrome belongs with the prompt surface, not the drawer title row. The referenced OpenCode source at commit9ed17da55ab1f7360cc0e01075f763e27fa899e9packages/opencode/src/cli/cmd/tui/component/prompt/index.tsxderives the current provider label inPromptlines 196-197 and renders agent, model, provider, and variant metadata in the prompt footer at lines 1452-1479; the model metadata row uses an explicit·separator before the model at line 1462 and before variants at line 1471, so adjacent metadata must not collapse into fused labels. Pulse's shared model picker adapts that by separating selected model labels from badges such asdefaultin both visible text and the button accessible name, so an OpenRouter default route renders asQwen: Qwen3.7 Plus via OpenRouter · defaultrather thanOpenRouterdefault. The session route injects that prompt throughpackages/opencode/src/cli/cmd/tui/routes/session/index.tsxsession_promptlines 1313-1333. Pulse adapts that by keeping the active model route selector, control-mode selector, and last-turn summary in composer chrome while the drawer header stays limited to drawer/session commands. Empty Assistant sessions are prompt-first transcript surfaces, not product marketing or instruction panels. The referenced OpenCode source at commit9ed17da55ab1f7360cc0e01075f763e27fa899e9packages/opencode/src/cli/cmd/tui/routes/session/index.tsxderives session messages inSessionlines 185-203, renders only the message stream inside the scrollbox at lines 1188-1295, and rendersPromptin thesession_promptslot at lines 1313-1333 without inserting a centered no-message title or subtitle. Pulse adapts that by leaving a blank transcript blank, keeping scoped handoff context in the drawer context strip, and limiting no-message transcript affordances to real recent-session resume actions. The referenced OpenCode source at fetchedorigin/devcommit1399323b78a04229d9bfe00c7436d7f41770fda8renders the completed assistant footer inpackages/opencode/src/cli/cmd/tui/feature-plugins/system/session-v2.tsx(AssistantMessage) with agent, provider/model, and turn duration rather than token counts. Pulse Assistant rows adapt that by keeping visible token accounting out of the transcript while showing a compact completed-turn duration beside the effective model label once a turn reachesdone,error, or user interruption. Runtime token usage may surface in low-priority composer chrome instead, but the completed-turn surface is a route summary rather than a token counter: the referenced OpenCode source at fetcheddevcommit3867fa2bad0e644166e360e2e99cfe426fe71105importsturnSummaryCommitinpackages/opencode/src/cli/cmd/run/scrollback.surface.tslines 19-20, andpackages/opencode/src/cli/cmd/run/turn-summary.tslines 5-46 renders a final system summary from agent, model, and duration. Pulse adapts that by deriving the latest non-streaming assistantChatMessage, rendering the effective provider/model route, completed-turn duration, and backend-proven total token count in the composer footer. The input/output split remains in the footer title/accessibility text, not the visible transcript or composer copy, so shorthand such as4358 in · 943 outcannot read like assistant output. If provider usage is missing, the route and duration remain visible; active streaming turns never become the completed-turn summary. Cost and context-limit percentages stay absent until the runtime exposes those values through a governed contract. Assistant tool activity is visible transcript activity, not a hidden context footer. The referenced OpenCode source at fetchedorigin/devcommite82542b8023a8374f29c23b70ec019c8f256354epackages/opencode/src/cli/cmd/run/types.tsdefines append-onlyStreamCommitrows for assistant, reasoning, tool, and system sources at lines 284-312, whilepackages/opencode/src/cli/cmd/run/footer.tsqueues commits and only coalesces consecutive progress chunks for the same part/tool at lines 512-545. Pulse adapts that contract by rendering each Assistantpending_tool, completedtool, andtool_cancelevent as its own chronological transcript row. Tool inputs, command previews, progress text, and outputs may remain collapsed inside the row, but context/read/query tools must not be replaced by a generic grouped footer that makes several operations appear all at once. Queued follow-up turns are active session pressure, not hidden backlog. The referenced OpenCode source at fetchedorigin/devcommite82542b8023a8374f29c23b70ec019c8f256354epackages/opencode/src/tool/todowrite.txtdescribes status tracking as surfacing progress to the user at line 1 and requires status updates in real time rather than batched completion at line 25, whilepackages/opencode/src/session/tools.tsupdates running tool metadata with title, status, input, and start time at lines 54-63. Pulse adapts that session-activity pattern by keeping queued follow-ups visible in the Assistant activity dock and appending the queued count to the active-turn headline while the current response is streaming. The queued row may retain edit, remove, promote, resume, and clear controls, but the primary headline must not imply the user's just-submitted follow-up disappeared behind a genericGenerating responsestate. Restored Patrol mode save-failure handoffs may preserve compatible summary kinds such aspatrol_configuration_failure, but Assistant drawer titles, subjects, action labels, and safety notes must describe Patrol mode rather than reviving generic configuration wording. Stable route markers, telemetry fields, and API wire names may retainpatrol_control; user-facing Assistant, external-agent, generated MCP, and frontend copy must call the operator's choicePatrol mode. - Keep AI chat presentation helpers aligned through
frontend-modern/src/components/AI/Chat/and the sharedfrontend-modern/src/utils/textPresentation.ts - Keep assistant drawer context, session, and org-switch reset state aligned through the shared
frontend-modern/src/stores/aiChat.tsboundary instead of lettingfrontend-modern/src/App.tsx,frontend-modern/src/AppLayout.tsx, or feature callers fork their own assistant shell state That shared drawer ownership also covers passive resource reads while the shell is mounted but closed.frontend-modern/src/components/AI/Chat/may consume the live websocket snapshot or the existing unified-resource cache for assistant context and suggestions, but it must not reopenuseResources()or trigger a second unfilteredall-resourcesREST fetch just because the drawer component is present in the app shell. The same app-shell boundary keeps Patrol/Assistant utility navigation accessible-name safe: labelled icon SVGs may remain meaningful when rendered standalone, butfrontend-modern/src/AppLayout.tsxmust treat them as decorative inside tabs so the announced tab name comes from product chrome and meaningful badge text rather than icon title duplication. Scoped approval handoffs sourced from Patrol, active alerts, or alert incident timelines must render as source-named investigation handoffs in the drawer instead of generic dashboard briefs. Source-owned handoff helpers may attach bounded model-only context, resources, action references, and metadata, but they must not synthesize, prefill, or auto-submit a user prompt. The drawer presentation must stay compact: source, status, one primary subject, and an optional safe route link. It must not render Patrol-authored remediation steps, evidence chips, command summaries, recommendations, or suggested-prompt chips as the answer. Patrol assessment handoffs must use the samepatrol-assessmenttarget identity for live opens and restored sessions rather than inheriting retired dashboard context. While such a handoff is attached, the Assistant empty message state must also remain source-named and must not fall back to generic cluster/system starter prompts that compete with the attached briefing. Structured Patrol run and Patrol mode save-failure handoffs may render bounded, redacted diagnostic lines in the drawer when they are opened directly from Patrol runtime/control surfaces, but the attached headline must remain source-owned (Patrol run attached,Patrol mode ... attached) and the drawer must still exclude raw provider payloads, commands, and Patrol-authored remediation steps. Reloaded Assistant sessions may consume the backend-ownedhandoff_summaryonly as safe presentation state and a Patrol finding pointer; hidden model context, command payloads, preflight data, and action results stay backend-owned and must not be reconstructed in the browser. - Add or change public Pulse Intelligence overview wording through
docs/AI.md; it may describe Assistant and Patrol capabilities, but it must not revive legacy commercial shorthand such asincident memoryas a current product promise. The public overview must preserve the product architecture: Pulse Intelligence Core owns canonical context, governed actions, safety gates, approval state, action audit, and verification; Pulse Patrol is the primary built-in operator on that core; its public summary must stay concise as watching, investigating, acting within the chosen Patrol mode, verifying outcomes, and recording what happened; and Pulse Assistant plus Pulse MCP are contextual and external-agent access paths over the same governed capabilities. Public overview, safety, and onboarding copy must use the same visible Patrol mode labels as the product (Watch only,Ask first,Safe auto-fix,Autopilot) and must not reintroduce the retiredOnly watch,Ask before changes,Fix safe issues,Auto-fix safe issues,Full control, orPolicy autopilotlabels as customer-facing names. The public overview must also preserve the model-owned AI boundary: Pulse Assistant and Patrol provide governed context, tools, safety gates, approval state, and audit trails, while the configured LLM owns diagnosis, prioritization, fix reasoning, and tool choice. Public copy must not present Pulse code as the intelligence engine, a prompt-keyword router, a deterministic auto-remediation oracle, a provider replacement, a learned operational-meaning engine, or a deterministic finding author. The public overview must not carry a hand-maintained Assistant tool table. Assistant tool inventory, action modes, approval policies, provider declarations, and Patrol-only filtering are registry-owned ininternal/ai/tools/and projected throughinternal/agentcapabilities/; external-agent inventory belongs to/api/agent/capabilitiesandpulse-mcptools/list. External-agent setup metadata follows the same rule: Pulse MCP server name, command, base URL flag/default, token environment variable, and supported client config families are manifest-owned in/api/agent/capabilities.mcpAdapter. Assistant, Pulse Intelligence settings, README, and MCP surfaces may present that setup, but must not maintain separate MCP setup constants. Security API settings may only host the scoped-token creation link used by that setup. - Platform/runtime top-level pages registered in
frontend-modern/src/App.tsxand the primary tab list infrontend-modern/src/AppLayout.tsxmust keep the AI launcher chrome intact: they extendaiChatStore-aware route shells rather than mounting bespoke layouts that suppress the Assistant launcher, Patrol entry, or the AI keyboard-shortcut handlers. New per-platform sub-routes inherit the shared AI-aware chrome by virtue of routing throughAppLayout, and must not introduce a parallel chat surface, launcher button, or model picker on the platform page itself; cross-platform AI guidance stays routed through Assistant and Patrol. The primary-nav demotion of Infrastructure / Workloads / Storage / Recovery does not change Patrol or Assistant addressability: both surfaces remain reachable through the utility tab strip inAppLayout.tsxand via the canonical Patrol path, and platform pages must not replicate Patrol findings, Assistant prompts, or AI launcher affordances inside their own chrome. The vSphere Networks sub-route follows the same AI runtime boundary as the vSphere overview, datastore, health, and activity routes. Network rows may seed Assistant or Patrol context only as sharednetworkunified-resource references read through/api/resources*and the common handoff payloads; the VMware page must not introduce VMware-local AI prompts, a provider model picker, or a vSphere-specific chat/runtime route just because networks are now rendered as a first-class API-native table. The frontend-primitives-owned Machines surface follows the same AI runtime boundary while retaining the internalstandaloneroute/id contract: route registration and primary-tab chrome may expose the support-manifestagentplatform plus agentless availability endpoints, but the page must not add an agent-specific Assistant prompt surface, AI launcher, model picker, or browser-owned model context. Machine rows may seed Assistant or Patrol only through the shared unified resource handoff contracts. Its IA, primary shell position, and landing eligibility belong tofrontend-primitives; those changes must not alter Patrol or Assistant utility-tab ordering, launcher visibility, or shared keyboard handling infrontend-modern/src/AppLayout.tsx.
Forbidden Paths
- Leaving new
internal/ai/runtime entry points unowned under broad architecture or generic API ownership - Duplicating AI orchestration, Patrol runtime, or cost-tracking logic outside
internal/ai/ - Treating AI transport files as payload-only boundaries when they also define live runtime control behavior
Completion Obligations
- Update this contract when canonical AI runtime or transport entry points move, including transport-level provider request-shape changes such as OpenAI-compatible
tool_choicehandling, runtime-failure classification splits (for example separating tool-choice request rejection, no tool-capable endpoint, and generic model-level lack of tool support into distinct causes), Patrol-specific verification surfaces such asPOST /api/ai/patrol/preflightthat exercise the full chat-completions path with a minimal tool definition rather than only listing models, Patrol-preflight cache observability where the AI Service caches the most recent preflight outcome (success, soft warning, or classified failure) and the AI settings response surfaces it aspatrol_preflightso the UI can hydrate a "last verified" indicator without forcing operators to re-run preflight on every page load, the auto-trigger contract onHandleUpdateAISettingswhere the save handler runsTriggerPatrolPreflightAsynconly when the change actually moved Patrol transport (model swap, provider key for that model changed, or assistant just enabled with a Patrol model) so routine settings saves do not burn provider tokens, the startup-seed contract where the AI Service handler dispatches the same async preflight on Pulse boot when assistant is enabled and a Patrol model is configured so the cache is populated for the first/api/settings/aipoll after a restart instead of blanking back to "never verified", the readiness-integration contract where thetoolscheck in the Patrol readiness payload consults the cached preflight and surfaces the classified evidence (success, soft warning, or failure with classified summary plus "last preflight ") for the configured provider+model when available (falling back to the staticPatrolToolReadinessForModelclassifier only when the cache is empty or holds a result for a different model), the preflight-runtime-recovery contract where a successful Patrol preflight with an observed tool call resolves the synthetic Patrol runtime failure finding while failed or no-tool-call preflights leave it active, the stateless-Patrol-input contract whereExecutePatrolStreammust pass only the current run's user prompt into the agentic loop rather than reloading the persistedpatrol-mainsession history (so a prior run that ended with orphantool_callscannot poison every subsequent run with malformed conversation structure), and the deterministic-resolve-gate contract where thepatrol_resolve_findingtool adapter rejects LLM-driven resolves of event/persistent category findings (backup,reliability,security,general) when a deterministic verifier exists for the finding's key and that verifier either still detects the failure signal or returns an inconclusive result — preventing the LLM from optimistically resolving a finding its current investigation simply didn't re-surface, which was the source of the "Backup failed" flap (detected → auto-resolved → re-detected ten times in a day before this gate). The fail-closed-on-inconclusive policy treats verifier errors (timeouts, executor unavailability, transport faults) as "we don't know" rather than "go ahead": resolution of an event/persistent finding is effectively permanent (next detection registers as a regression and inflates counters), so the safe default is to refuse and require either a successful re-verification or operator action. The gate has a symmetric counterpart, the verified stale-resolve contract:reconcileStaleFindings(internal/ai/patrol_ai.go) may auto-resolve an event/persistent finding that was seeded but neither re-reported nor resolved ONLY when the finding's key has a deterministic verifier and that verifier affirmatively confirms the failure signal is gone — absence of a re-report remains insufficient evidence for these categories, "still present" or inconclusive verification leaves the finding active (the same fail-closed default), and verifications are capped per reconcile pass (maxVerifiedStaleResolvesPerRun) with deferred candidates logged and retried on the next successful full patrol. Without this counterpart the lifecycle was asymmetric: a genuinely fixed backup or recovered service stayed an active finding indefinitely unless the LLM happened to callpatrol_resolve_finding.hasDeterministicVerifierForKey(internal/ai/patrol_findings.go) is the single source of truth for which keys have verifiers, consulted by both the gate and the reconcile pass, and must stay aligned with the dispatch switch inverifyFixDeterministically(it previously listed two of the seven dispatch keys, silently skipping verification that existed). Finding keys normalize onto the canonical verifier vocabulary innormalizeFindingKeyvia an alias map of unambiguous directional synonyms (high-cpu→cpu-high,high-memory→memory-high,high-disk→disk-high) so deduplication and deterministic verification meet on one key, and thepatrol_report_findingkey guidance (internal/ai/tools/tools_patrol.go) teaches the canonical vocabulary; semantically distinct keys (pbs-job-failed,node-offline) must not be aliased onto verifier keys whose resource model they do not match, the assessment-recovery contract where the overall-health "Recent Patrol errors" coverage factor insummarizeRecentPatrolCoveragesuppresses the score penalty once three consecutive trailing successful full Patrol runs exist at the most-recent end of the recent-runs window — so the grade reflects current reality after a Patrol-affecting bug is fixed rather than dragging stale failures forward for the ~9 hours it takes scheduled runs to age them out of the trailing-10 ratio, the orphan-tool-call-repair contract whereconvertToProviderMessagesinjects synthetic is_error tool result messages for anytool_call_idin an assistant message that has no matching downstream tool result, so a chat session that ended mid-tool-call (network drop, ctx timeout, browser crash) cannot poison its next message with the structural-violation error the provider rejects — the synthetic content is marked is_error=true and explains the interruption so the model can retry the call or proceed without the data, and the patrol-session-bound contract whereExecutePatrolStreamcallsSessionStore.TrimMessagesafter persisting each run's messages to cap the patrol-main session at 200 messages (roughly two recent runs' worth) — without the bound the file grew unbounded at every scheduled run, reaching 16 MB and 3,593 messages within a month and making everyAddMessagerewrite linearly more expensive; the canonical Patrol forensic log is thePatrolRunRecordhistory surfaced at/api/ai/patrol/runs, not the chat-session-shaped file - Keep AI runtime and shared API proof routing aligned in
registry.json - Preserve explicit coverage for chat, Patrol, remediation, and cost-control behavior when AI runtime changes. Interactive Assistant and Patrol tool selection must remain model-owned: Pulse may provide governed context, tools, approval state, resource-resolution facts, safety policy, and neutral resource-scoped action history, but it must not add prompt-keyword routers, expected-tool retries, auto-recovery tool calls, keyword-matched prior-fix suggestions, or Pulse-authored remediation/finding fallbacks that choose the next investigative or corrective action for the model.
Assistant FSM gates remain safety boundaries after the model chooses a tool:
repeated model attempts must not waive post-write verification or allow a
new state-changing tool before the model has supplied current verification
evidence through an allowed read/resolve path.
Assistant restored-session and recent-session context is also model-bound
context, not an identity-policy bypass: when the referenced unified resource
is governed or redacted, backend context builders must use the resource
policy label or redacted value, suppress exact location/routing metadata
such as
target_host, and avoid exposing raw provider IDs, aliases, IPs, or hostnames to the model unless policy allows them. Resource drawer Assistant handoffs use theresource_contextmetadata kind and must attach product-originated resources as model-only context, not as saved user text. The backend must preserve that handoff kind through session persistence/restore, prepend an explicit selected-resource, discovery, tool-target-handle, data, raw-context, and action boundary before the resource context pack, and sanitize streamed assistant content, saved assistant messages, and tool results through the same unified-resource policy redaction path used for the context pack. The only model-facing tool target for an attached redacted resource iscurrent_resource; read, query-get, and discovery tools resolve that handle server-side against the session-selected resource and must not treatredacted by policyas a raw infrastructure identifier. The liveresource-contexteval is the required regression proof for this path: the model must not ask which resource the user means, must not call discovery just to identify the attached resource, must report attached discovery readiness from context without a discovery tool call, must use the safe handle for scoped reads, may use the attached PII-free operational context (service access pattern, config/data/log paths, ports) to answer and guide the operator, must still refuse to expand environment variables, credentials, or other secret-bearing content, must not reveal raw hostnames, IPs, or aliases, and must not leak configured forbidden resource details in content or tool inputs. Plain-text resource references in live read, log, verification, or command-intent Assistant prompts may use the same selected-resource handle only after backend-owned canonical inventory resolution proves exactly one resource match, including explicit target-kind wording such as host, node, VM, container, or storage when inherited node labels would otherwise collide. That path must register the resource in the session resolved context, mark it as explicit current-turn access, and prepend a safe resource-context directive that exposescurrent_resourcebut not raw aliases, hostnames, platform IDs, paths, or other policy-redacted labels to external providers. Because that provider-safe rewrite replaces the user message, plain-text resolution runs only as a fallback when the prefetcher did not already resolve a structured mention or handoff resource; otherwise it would discard the injected cloud-safe operational context. Ambiguous or non-live prompts must fail closed to normal model clarification/query behavior; this is not a prompt-keyword router and must not choose, retry, or execute the model's next investigative action. Context-only resource handoff turns must be enforced at the tool-manifest boundary, not only by prompt wording: unless the operator explicitly asks for live runtime verification, a read attempt, or discovery execution, the Assistant loop must receive no tools and must answer from the attached context or state that Pulse does not currently have that fact. Resource-context model packs and drawer handoff briefings must carry the canonical discovery readiness state (fresh,stale,missing,running,failed,unavailable, orunsupported) with provenance and freshness metadata so Assistant can explain whether it is grounded in current discovery data before choosing any tool. Drawer handoffs route their attached resources through the same context-prefetch path as explicit@-mentions, so on a cloud turn the resource's cloud-safe operational context (access pattern, config/data/log paths, ports, and discovery freshness/age) reaches the model exactly as it does for an@-mention; identifying fields (hostname, IP, alias) stay redacted at the model boundary. The cloud-safe context must carry the discovery's age (a non-identifying timestamp) when known so the model can caveat staleness rather than presenting cached operational detail as current. The handoff path must not withhold operational context that the@-mention path delivers. The base Assistant system prompt must instruct the model to use this provenance: briefly attribute facts drawn from Pulse context to their source (a discovered fact's source, a metric/event timestamp), caveat stale or cached context instead of presenting it as current, and keep attribution concise and inline rather than citing every sentence. Patrol deterministic triage signals are prioritized evidence seeds for the configured model; they must not be described as a Pulse-authored final diagnosis, proof that unflagged resources are healthy, or a reason to prohibit the model from choosing governed tools for adjacent evidence relevant to its own findings. Patrol runtime failures are part of that runtime contract: provider, model, tool-calling, auth, quota, rate-limit, context-window, and connectivity failures must be classified ininternal/ai/before they reach operators, surfaced as the synthetic Patrol runtime finding, and preserved on patrol run records as structured error summary/detail instead of collapsing to generic analysis-failed copy. Demo-mode Patrol run records must carry explicit source provenance and must not persist as live runtime evidence; outside demo mode, run-history reads, run lookup, and Patrol coverage scoring must filter both source-marked and legacydemo-run-*records so live assessment state cannot be contaminated by public demo fixtures. Unavailable-provider blocked states must direct operators to Assistant & Patrol provider settings and tool-capable Patrol model selection, not legacySettings > Pulse Assistantcopy. Patrol status must also carry server-authored readiness for provider, model, settings-persistence, and tool-calling prerequisites so the UI can block known-bad manual Patrol runs before they become generic runtime failures. The sameinternal/aireadiness evaluation must gate Patrol runtime admission directly: settings saves that are needed to recover a bad provider/model selection must persist and return structured readiness cause metadata, while manual run requests, scheduled ticks, and scoped alert/anomaly runs must fail or skip before LLM execution when the selected Patrol model/provider is known not-ready. Readiness checks may retain stable machine IDs such asconfiguration, but human-facing labels in the settings payload must frame the operator boundary as Patrol mode rather than Patrol configuration. Monitor-only Patrol autonomy saves are part of the same runtime gate: when the safe-remediation extension or entitlement is unavailable, both the browser state owner andinternal/api/ai_handlers.gomust clear stale full-mode unlock state while clamping autonomy back tomonitor, so paid remediation permission cannot survive through a free runtime save. Event-triggered Patrol runtime policy is also part of this gate: when local development background automation or another runtime policy pauses automatic alert/anomaly-triggered checks,internal/aimust expose an explicit event-trigger block reason/message on trigger status while preserving the operator's configured alert/anomaly trigger preferences. Patrol update-safety observation is part of the same read-state runtime boundary: Docker host/container snapshots used to detect image-digest divergence must be sourced from canonicalReadState.DockerHosts()views, with model-shaped data limited to the watcher adapter input rather than directStateSnapshot.DockerHostsreads in the Patrol run loop. - Keep discovery scheduling authoritative through
internal/config/ai.go:discovery_enabledanddiscovery_interval_hoursmust govern both lightweight infrastructure discovery and deep service-discovery background loops.internal/api/ai_handlers.gomust preserve an explicitly supplieddiscovery_interval_hours: 0as the manual-only setting and may only apply the 24-hour default when discovery is enabled without an explicit interval payload. Discovery analysis remains a Pulse tool-led model workflow: Pulse supplies agent/API/metrics evidence and cache orchestration, while the selected model provides the intelligence. Background, settings-triggered, or drawer-triggered discovery progress must describe that discovery analysis directly and must not imply a live Pulse Assistant chat transcript unless the run is actually executing inside the chat surface. Settings-triggered manual discovery is an explicit operator refresh and must not open, append to, or masquerade as a Pulse Assistant session. Assistant and Patrol access to discovery must stay behind the governedpulse_discoverytool, including an explicit forced refresh action for known resources, while settings-level manual runs must use the canonical/api/discovery/runnew/changed/stale/repairable sweep rather than a frontend-only shortcut. Discovery analysis must use the selected discovery route only: provider-owned default resolution may choose a model before the request when no explicit route exists, butAnalyzeForDiscoverymust not fall back to a stale default provider or another configured provider after selected-route provider construction or execution fails. Fresh-but-unidentified service-discovery records are not complete when canonical resource metadata, stored facts, or safe command evidence can deterministically identify a known workload and endpoint; cached reads and the canonical sweep must repair those records instead of presentingUnknown Serviceas fresh. - Preserve auditability for outbound model-bound context exports and keep the export record aligned with the prompt boundary that actually reaches the provider
External provider-bound unified-resource context must enforce the same
data-handling policy the export audit records:
local-onlyresources are represented only as aggregate posture and omitted from detailed prompt sections, while sensitive alert text is scrubbed through the shared unified-resource redaction helper before it reaches a non-local model. The final provider-bound chat, Patrol, investigation, tool-result, and any retained legacy managed-model compatibility requests must also pass through that same resource-policy sanitizer immediately before transport, so later agentic turns cannot reintroduce local-only identifiers after the original context export. - Keep AI resource and incident context aligned with the canonical unified-resource timeline before falling back to patrol-local change detectors
- Keep platform assistant read/control claims aligned with
docs/release-control/v6/internal/PLATFORM_SUPPORT_MODEL.md. New platform-native reads or writes must extend the shared Assistant tool contracts, and read-only or augmentation-only platforms must stay explicit there instead of drifting into provider-local tools. - Keep Pulse Assistant action governance canonical in the shared tool
registry. Tool prompts and approval surfaces must derive read, mixed, write,
and approval-policy claims from
internal/ai/tools/registry.goandinternal/ai/tools/executor.goinstead of maintaining hand-written prompt-only tool lists. Fallback Assistant governance text and Patrol system-prompt tool summaries must mirror the same registry-owned capability shape, includingpulse_discoveryread-or-refresh behavior, instead of presenting stale read-only summaries. Frontend approval cards must surface backend approval risk/description without hiding a pending approval when skip or deny fails. Action-producing tools must also persist the unifiedActionPlan.Preflightdry-run boundary throughinternal/ai/tools/action_audit.gorather than leaving dry-run availability as chat-only text. Thepulse_discoverytool response must carry each discovered fact's provenance — thesourcethat produced it and itsconfidence— alongside the fact's category/key/value, so the model can attribute and weight what it reports instead of stating untraceable facts. Both are omitted when empty. When the shared registry blocks a control tool in read-only mode, its operator guidance must point to Pulse Intelligence > Provider & Models settings and the Pulse Assistant Permissions Control mode, not legacy Pulse Assistant settings paths. Runtime status, preflight, settings-persistence, profile-suggestion, and remediation-impact messages must preserve the same product boundary: use Pulse Assistant only for the first-party in-app Assistant runtime itself, Provider & Models for the shared provider/model settings surface, and Pulse Intelligence for the shared Assistant/Patrol/agent substrate rather than reviving the legacy genericPulse AIlabel orPulse Assistant settingscopy. Pulse Intelligence external-agent setup copy may point to Patrol mode, but it must frame the first step as setting how autonomous Patrol should be before connected agents request work; it must not present connected-agent setup as a separate operations-loop, MCP-readiness, or feature-unlock model. - Keep self-hosted Patrol messaging aligned with the v6 GA product contract:
ordinary self-hosted installs use BYOK or local providers, and the runtime
must not surface retired managed-model credits, trial prompts, account-backed
AI activation, or general hosted chat entitlement in the normal app.
The shared app shell must also keep
/cloudand/cloud/signupout of ordinary self-hosted public routes so Cloud acquisition cannot reappear as a proxy for retired hosted-model or AI quickstart activation. The public Pulse Intelligence overview must likewise use productized context language such as alert history, Patrol runs, and resource timelines instead of presentingincident memoryas a standalone feature. It must also describe Patrol baselines, trends, correlations, forecasts, and deterministic signals as model-bound evidence/context rather than Pulse-authored intelligence or fallback finding creation. - Keep discovery-analysis prompt bounds and response budgets aligned across
internal/ai/service.goand the shared service-discovery prompt builders: the runtime must reserve enough output tokens for structured discovery JSON, and discovery prompts must cap fact/path/port fan-out explicitly instead of relying on providers to truncate oversized infrastructure inventories. That same runtime-owned command-target boundary must resolve hostnames throughinternal/unifiedresources/hostname_equivalence.go.internal/ai/tools/internal_routing.go,internal/ai/tools/tools_control.go, and adjacent AI command helpers may match a short host against its FQDN, but they must not broaden that fallback into a generic short-name collapse that would make two distinct FQDNs with the same short host look interchangeable. - Keep AI runtime transport compatibility separate from operator-facing
product copy. Existing Patrol payload fields such as
fixed_count,auto_fix_model, andpatrol_auto_fixmay remain stable wire/API names, but frontend comments, API denial messages, runtime logs, status labels, CLI help, and commercial prompts that describe the capability must use safe remediation or remediation wording. - Keep AI control-level presentation runtime-owned rather than tier-owned.
frontend-modern/src/utils/aiControlLevelPresentation.tsandfrontend-modern/src/components/Settings/AIRuntimeControlsSection.tsxmay describe approval posture, but must not add Pro-badge suffixes or local commercial tracking around those runtime controls. - Keep Assistant control and Patrol paid runtime settings entitlement-effective at every runtime boundary. Stored config may preserve autonomous, Patrol auto-remediation, and alert-triggered analysis preferences so they come back if entitlement returns, but API responses, chat executor startup, restart, settings-update, request-clone paths, and Patrol execution must clamp those values through runtime entitlements before exposing or enforcing them.
- Keep agent-backed Patrol reachability checks aligned with the agent command
policy.
internal/ai/patrol_prober.gomay use connected agents for read-only guest ping probes, but it must validate each target as an IP address and issue only the single-targetping -c 1 -W 1 <ip>command shape covered by the agent-exec auto-approval policy. It must not compose shell loops, accept hostnames, interpolate unvalidated targets, or bypass approval requirements for compound commands. - Keep OpenAI-compatible streaming finalization fail-closed.
ChatStreammay flush buffered final SSE lines when EOF arrives with unread bytes and may accept providers that omit[DONE]only after a terminalfinish_reason, but it must not emitdoneor executable tool calls from partial tool-call builders when the stream closes before that terminal provider state. - Keep Patrol investigations product-facing through the shared
aicontracts.InvestigationRecordcontract. Patrol may keepInvestigationSessionas execution detail, but Assistant handoff, unified findings, persistence, and approval/remediation context must use the durable investigation record when they need operator-facing investigation context. The durable record carries top-levelimpactandrollbackstrings alongside the existingverificationarray, so Assistant/api/ai/chatenrichment surfaces consequence-if-ignored and undo intent when Patrol has populated them and remains silent when the fields are empty rather than fabricating placeholder analysis through the model. The TS API client must keep itsInvestigationRecordandInvestigationRecordTriggermirrors aligned with the Go struct, including thetrigger.causestring, so frontend handoff context does not lose backend-attributed failure cause. Detection-time finding creation may authorFinding.Impactdirectly so the consequence-if-ignored statement is set at finding birth rather than waiting for an AI investigation to derive it;BuildFindingInvestigationRecordthen propagatesFinding.ImpactintoInvestigationRecord.Impactwithout transformation, and the AI engine must not synthesize impact text from severity or category when the finding source has not authored one. The Patrol runtime-failure classifier (internal/ai/patrol_runtime_failure.go) is the canonical example: it stamps a constant impact string on every runtime-failure cause because the operational consequence of a non-running Patrol is invariant across causes, and only the recommendation varies. That detection-time impact text propagates throughFindingsStore.Add(which must overwriteexisting.ImpactalongsideDescriptionandRecommendationso re-detected findings adopt freshly-classified impact rather than preserving stale empty values left by older binaries) and through the Finding to UnifiedFinding conversion ininternal/api/router.goso the operator-facingunified.UnifiedFindingsurface receives the same impact string the durable investigation record carries. Threshold-alert findings author impact through a parallel hand-curated catalog ininternal/ai/unified/alerts.go(generateImpact), keyed on alert type rather than severity or category, so threshold findings carry consequence-if-ignored copy at detection time without depending on an AI investigation. The unified-store update paths (UnifiedStore.AddFromAlertandUnifiedStore.AddFromAI) must propagate Impact on re-detected findings the same way they propagate Description: AddFromAlert backfills empty Impact on existing findings; AddFromAI overwrites existing Impact when the incoming finding carries one. Threshold-alert conversion must not synthesize remediation recommendations; unknown alert types must return an empty impact rather than synthesizing generic copy. Investigation-recordRollbackis sourced from the canonicalRemediationPlanwhen one exists for the finding:AggregatePlanRollbackStepsininternal/ai/investigation_records.goflattensRemediationStep.Rollbackstrings into a deduplicated record-level slice, and the patrol findings build site (internal/ai/patrol_findings.go) populatesrecord.RollbackfromremediationEngine.GetPlanForFindingwhen the engine and an active plan are present. Rollback must remain absent rather than fabricated when no plan exists, mirroring the impact rule. LLM-generated AI patrol findings author impact through thepatrol_report_findingtool schema ininternal/ai/tools/tools_patrol.go: the tool exposes an optionalimpactparameter,PatrolFindingInput.Impactcarries it through, andpatrolFindingCreatorAdapter.CreateFindingwrites it ontoFinding.Impactso the LLM's authored consequence-if-ignored copy flows through the same propagation path used by curated catalogs. The patrol system prompt instructs the LLM to author concrete operational consequences (named workloads, jobs, recovery windows) and to leaveimpactempty rather than fabricate one when the consequence is genuinely unknown; the runtime must not synthesize a default in that case. The action broker enforces a plan-hash drift check at the execute boundary: when an approval ID resolves to a stored plan with a PlanHash, the freshly-recomputed approval-equivalent hash from the actual payload (usingapprovalPlanHash, the same function used at approval-creation time) must match. Mismatch returnsErrActionPlanDriftand refuses dispatch; the contract is "the operator approved exactly this (command, target, reason) combination" and a different one cannot run under the stale approval. WhenapprovedHashis empty (older approval records, or contract paths that did not author one), validation is skipped to preserve existing behavior. The check is currently wired inexecuteCommandWithAuditfor shell-command actions; the native- action path uses a different hash function (actionPlanHashForParams) so a coherent canonical-hash refactor must precede adding the same check there. The broker runs a class-derived read-after-write verification check immediately after a successful dispatch.VerificationCommandForCommandininternal/ai/tools/tools_control.goreturns the executable check keyed on the same command class as the preflight authoring (e.g.systemctl is-active <unit>after a service-restart). The check runs through the same agent path as the dispatch and the outcome is persisted onExecutionResult.Verificationso the audit history shows not only what the action did but whether the read-back confirmed the intended state. Container-class verification is deferred to pulse_docker's existing tool-level runtime inspect check (docker inspectorpodman inspect, according to the resolved container runtime); classes without a derivable verification command leaveVerificationnil rather than fabricating a verified=true entry. The approval preflight presented to operators authors per-command-class safety and verification context on top of the default broker-level posture.classifyApprovalCommandandapprovalCommandClassPreflightAdditionsininternal/ai/tools/tools_control.gobucket common Pulse remediation actions (service-restart, service-stop, service-start, service-reload, container-restart, container-stop, k8s-rollout-restart, plus the Proxmox VM lifecycle classes proxmox-vm-reboot, proxmox-vm-stop, proxmox-vm-start, proxmox-vm-shutdown and the matching pct-driven proxmox-ct-* container lifecycle classes) and return hand-authored operational copy: what the command actually touches, how Pulse will read back success. The additions append onto the default safety/verification arrays rather than replacing them, so the broker's structural posture (org scope, hash match, single-use approval) remains visible alongside the class-specific copy. Unknown command classes must return empty additions rather than fabricated padding — operators see only the default content, not invented assertions about what an unrecognized command will do. The Proxmox classes intentionally do not derive a broker-levelVerificationCommandForCommandcheck because pulse_control'sverifyGuestActionalready runsqm status/pct statusat the tool layer; adding a parallel broker dispatch would double-run the same read-after-write check. Drift refusal must also persist a Failed audit record with the Request, Plan, and Approvals snapshots intact and a Result whose ErrorMessage is prefixedplan_drift:so the audit trail shows every drift attempt that was caught. Operators reviewing the action audit history must be able to see drift refusals as first-class audit rows, not only in WARN-level logs; theplan_drift:prefix is a stable token for audit-UI filters and alert rules to distinguish drift from generic execution failures.FindingsStore.GetTrustSummaryreturns a snapshot of how currently tracked findings have resolved (tracked, currently-active, resolved, auto-resolved, fix-verified, fix-failed, dismissed-as-noise, dismissed-as-expected, dismissed-as-later, suppressed, regressed-at-least-once). It is the data layer for trust metrics on operator surfaces.PatrolService.GetFindingsTrustSummaryexposes the same snapshot through the service boundary, and the patrol-status API response carries it underPatrolStatusResponse.Trustso the Patrol page can render a Trust strip without callers reaching past the service boundary. The summary is intentionally a snapshot of the in-memory store, not lifetime totals; once findings are cleaned up they no longer contribute. Downstream surfaces must frame the counts as current-state distribution rather than historical aggregates, and the AutoResolved bucket includes both theResolve(auto=true)path and theUpdateInvestigationOutcome(fix_verified)path. Findings carry aprevious_resolved_fix_summaryfield as operational memory across regressions: when a finding that had a resolved investigation with a proposed fix is re-detected,FindingsStore.Addcapturesexisting.InvestigationRecord.ProposedFix.Descriptioninto the new field BEFORE clearing the investigation record, and the chat-context builder surfaces it as a "Previous Resolved Fix" line so Assistant sees what worked last time rather than treating each regression as a blank-slate diagnosis. The summary mirrors ontounified.UnifiedFindingand propagates through the Finding to UnifiedFinding conversion ininternal/api/router.goand throughUnifiedStore.AddFromAI's update branch (non-empty overwrite). The TS API client mirrors the field onUnifiedFindingRecordandFinding, the aiIntelligence store normalizer copies it through aspreviousResolvedFixSummary, andFindingsPanel.tsxrenders it on the expanded finding card so the operator sees the memory cue inline rather than only inside Assistant chat context. When/api/ai/chatreceivesfinding_id, the runtime must enrich the provider turn from that durable record while preserving the user's authored prompt as the persisted conversation message; the model-only handoff may persist as session metadata so same-session follow-up turns keep the Patrol finding context without mutating saved user messages. Patrol run-history handoffs follow the same backend-owned context rule: the browser may seed only safepatrol_runmetadata such as run ID/type/status/runtime-failure posture, while/api/ai/chatmust rehydrate model-only run context, scoped resources, and safe failure detail from the current Patrol run record before model execution and again on same-session follow-up turns. If the Patrol run no longer resolves, browser-authored run context, resources, and actions must be dropped rather than used as fallback provider context. When the handoff identifies a resource, the runtime may also seed the session's resolved-resource scope, but only through canonical unified-resource tool registration so allowed actions, executors, and explicit-access checks stay governed. Structured handoff resource references may persist as session model-context metadata for follow-up turns, but they remain references only; each turn must rehydrate them from the current canonical unified-resource model before action validation can use them. Structured finding references from Patrol/Assistant handoffs may also persist as session model-context metadata so follow-up turns can refresh the current unified finding and investigation record before model execution; those references remain model-only context selectors, not saved user text or lifecycle authority. When the current finding identifies root-cause or correlated finding IDs, Assistant may resolve those related findings through the current unified finding store and include compact related-finding summaries plus their structured handoff resources as model-only explanation context. Those summaries must carry current recency facts and latest lifecycle state from the related record rather than only title/resource prose. Those related findings must be deduplicated, bounded, refreshed from current store state, and treated only as context for the same operator conversation; they do not grant approval, lifecycle, disclosure, or execution authority. If the referenced finding no longer resolves through the current unified finding store, Assistant must invalidate the stored model-only handoff and unpinned handoff-seeded resource scope instead of falling back to stale investigation context. The refreshed finding context must include unified finding lifecycle and recency facts such as active, resolved, snoozed, dismissed or suppressed state, detection/last-seen/ resolved timestamps, recurrence, regression, and recent lifecycle events so Assistant explains the current Patrol record rather than only the original investigation narrative. The saved-session handoff envelope must also preserve first-class Patrol source identity when product callers provide safe metadata. Patrol assessment handoffs remainpatrol_assessmentwhole-surface review sessions even when their bounded action references name individual findings; the session list must not infer apatrol_findingidentity from those action references once metadata is present. Patrol mode save-failure handoffs remainpatrol_configuration_failuresessions for compatibility and may expose only the safe runtime-failure boolean needed for browser presentation. Run-specific fields stay reserved forpatrol_runhandoffs, while hidden model context, command payloads, preflight output, and action results remain backend-owned. The finding briefing may surface the primary finding's current recency facts, bounded evidence snapshot, verification summary, latest lifecycle event, and governed action artifact metadata, but it must not generate Pulse-authored attention reasons, operator-decision framing, or remediation next-step guidance for the model. Detailed lifecycle events must stay in a bounded[Finding Lifecycle Context]block with an explicit model-only boundary. Assistant runtime may also hydrate canonical resource-policy context for those handoff resources, using the same unified-resource resolution and policy presentation helpers that govern mention prefetch and provider-bound redaction; that context remains model-only handling guidance, not saved user text or disclosure authority. Before injecting any product-originated handoff context into the model prompt, the runtime must also apply canonical resource-policy redaction to the assembled handoff text itself, including finding briefings and lower-level finding/action context, so local-model prompts and non-local provider transport share the same governed identity boundary. Assistant runtime may also hydrate current canonical resource-state context for those handoff resources, including compact status, freshness, source-health, metric, incident, and governed-capability summaries from the unified-resource model. That state snapshot remains model-only read-only infrastructure context, must honor the same policy/redaction boundary before provider transport, and must not grant approval or execution authority. Assistant runtime may also hydrate canonical relationship context for those handoff resources throughFormatResourceRelationshipContext(...)and canonical parent-edge synthesis, but those topology facts remain read-only explanation context and do not grant action authority. Assistant runtime may also hydrate recent changes for those handoff resources from the canonical unified-resource timeline as model-only context on each turn; it must resolve product-originated handoff references through the canonical unified-resource provider before querying timeline changes, with raw handoff IDs used only as a compatibility fallback. Those timeline facts remain read-only explanation context and do not grant action authority. Assistant runtime must also treat the sharedinternal/agentcontextresource context pack as the canonical rich resource-grounding substrate for product-originated resource handoffs: runtime/discovery, topology, safety, policy, and recent-change facts are hydrated as model-only context from canonical unified resources and the resource store, carry provenance/freshness/redaction metadata, and must not include raw command output, provider config, environment values, mount paths, label maps, or secret-bearing metadata. The runtime may also persist structured pending-action and approval references from the same investigation record as model-context metadata, and the API handoff builder may recover the current live Patrol investigation-fix approval by finding ID when the durable record does not yet carry the latest approval ID. Those references are review context only: they must not include raw command text, must not grant approval or execution authority, and must route any requested action back through the governed approval and fix flow. Patrol provides the configured LLM with observed finding context, evidence, policy posture, and governed action state; the LLM owns diagnosis and fix reasoning. Operator-visible handoffs must not describe Patrol as having already authored the correct fix. The finding briefing may carry only factual governed action artifact metadata from those same structured references after live-approval recovery, including safe current status, request/expiry timestamps, approval policy, action plan identity, plan expiry, and dry-run posture, so Assistant sees current approval/action state without Pulse choosing the model's next investigative or corrective step. When those references include an approval ID, Assistant runtime may refresh a current status snapshot from the canonical approval store on each turn, but it must enforce org scoping and still omit the approval command payload. When those references resolve to a governed action plan or action audit, Assistant runtime must hydrate the canonical action ID, lifecycle state, requester, capability, approval policy, plan expiry, preflight/dry-run summary, and terminal success/failure state from the action-audit store rather than treating the original approval as the current action truth. That action snapshot remains model-only review context and must expose lifecycle status rather than raw execution output or command text. The public chat session contract may expose only a boundedhandoff_summaryfor this private model-context metadata so reloaded Assistant sessions can still be identified as scoped Patrol/product handoffs. That summary may include the handoff kind, finding ID, resource and Patrol run ID, safe run type/status/runtime-failure flags, resource and action counts, a primary resource label, last-known approval/action status, risk level, and timestamp, but it must not expose Patrol-authored recommended next-step titles, recommendation details, route-owned action labels, model-only handoff text, runtime failure detail, action preflight/result bodies, remediation descriptions, raw commands, or approval command payloads. Itsrequires_approvalfield is a current operator-decision flag only: pending approval states may set it, but approved, denied, rejected, executing, completed, failed, expired, or otherwise historical action references must remain action context without being relabeled as requiring approval. When the Assistant drawer restores any session from thathandoff_summary, it must restore the scoped request-local approval boundary as well as the safe visible briefing: the next chat turn must carryautonomous_mode:falseeven when the summary is context-only and has no queued action, while the visible badge/action copy must still reflect the actual last-known action state or Patrol assessment context instead of inventing a pending approval or restoring a Patrol recommendation. That restoration is success-bound: if the underlying session message load fails, the drawer must leave the current context untouched instead of applying summary-derived Patrol or approval state for a session the operator is not actually viewing. Before/api/ai/sessionsreturns summaries with stored handoff action references, the chat runtime must refresh their safe approval/action status from the canonical approval store and action-audit store. Session listing is an operator decision surface, so it must not leave stale pending/approval labels in the drawer after the governed action moved on, and it must still omit raw commands, preflight bodies, and execution output. That refresh is bounded, not exhaustive: the list is newest-first and only themaxSessionHandoffRefreshPerList(20) most recent action-carrying sessions are re-checked per list call, which covers every summary the recent-sessions surface and picker top present; deeper history self-heals because the send path refreshes the active session's handoff actions on load before the turn executes. The Assistant drawer must also fetch that current session list before opening the session picker instead of presenting mount-time cached summaries as the operator's decision surface. For restored Patrol assessment or finding sessions, that picker must present only the safe handoff kind, source, resource/action counts, and approval/action status fromhandoff_summary; it must not restore Patrol recommended next-step title/detail/action labels, route-owned hrefs, or disabled-action reasons as visible or hidden context. Browser-originatedhandoff_context,handoff_resources, andhandoff_actionsplus safehandoff_metadataare one-shot request seeds for the first successful chat turn. Patrol next-step titles, details, labels, and route-owned hrefs do not belong inhandoff_metadata, and model-context text parsing must not resurrect them as a legacy fallback. After that send succeeds, the drawer must clear those request payloads while preserving the safe visible briefing and request-local approval-required posture; later turns must rely on backend-owned session model-context hydration and current canonical stores instead of resending stale browser handoff payloads. Patrol approval-row Assistant entries are still Patrol finding handoffs, not local prompt-only shortcuts: live approval rows, expired action-artifact rows, and missing-detail queued-fix recovery rows must route through the shared Patrol finding handoff builder so the backend receives the same bounded model-only finding context, resource reference, and safe action reference posture that the main finding handoff uses. Proposed-fix command text must stay out of both the persisted chat message and the model-only handoff context, and command payloads remain approval-context data, not conversational copy./api/ai/chatmust also clamp Patrol finding handoffs to approval-required mode when a request carries a non-emptyfinding_idor resolves to model-only briefing, resource, or action context, by forcing the request-local autonomous-mode override to false, even when a caller suppliedautonomous_mode:true. That clamp belongs to the backend/API execution boundary, does not mutate the user's persistent AI control setting, and prevents product-originated Patrol action context from becoming silent command authority. The chat runtime must apply any request-local autonomous-mode override to both the per-requestAgenticLoopand the clonedPulseToolExecutor; persistent autonomous settings must not leak into scoped approval-required handoffs through executor state. When such an override forces approval mode and the saved control level is autonomous, the executor clone must clamp its effective control level to controlled for that request only, so even policy-allowed diagnostic commands require operator approval in scoped handoffs without mutating the user's saved setting. The Assistant drawer may also render an attached context briefing for that handoff, but the briefing is runtime context visibility only: it must not mutate chat control settings, execute tools, or reveal raw command payloads. Resource-drawer Assistant entries use that same briefing path withhandoff_metadata.kind=resource_context, a structuredhandoff_resourcesreference, andautonomous_mode:false; they must not prefill or submit a browser-authored prompt, and any rich resource facts must be hydrated by the backend context-pack path rather than reconstructed in the browser. Safe route-owned briefing actions may render as app links when the handoff includes anactionHref, but those links are navigation guidance only and do not grant tool execution or approval authority. Request-local approval-required scoped handoffs must present that boundary through compact source-named drawer state and the effective control label, so Patrol approval/finding handoffs and alert-investigation handoffs are named by their source rather than as generic dashboard briefs.
Current State
The canonical findings store must present one active Patrol issue per real
problem. When the model reports an equivalent active sibling under a different
finding ID, key, or severity for the same resource, internal/ai/findings.go
must merge it into the existing active finding before it reaches
/api/ai/patrol/findings, keep the highest observed severity for the merged
issue, increment recurrence, and record a bounded duplicate-merge lifecycle
event instead of creating a second operator-facing row. Distinct symptoms on
the same resource, such as CPU pressure and memory pressure, must remain
separate findings. Verification is
go test ./internal/ai -run 'TestFindingsStore' plus
go test ./internal/ai -run 'TestPatrolService_GetAllFindings|TestPatrolService_GetFindingsForResource|TestPatrolService_GetAllFindingsIncludingResolved'.
Public Pulse Intelligence overview, safety, and onboarding copy now use the
same visible Patrol mode labels as the product: Watch only,
Ask first, Safe auto-fix, and Autopilot. The legacy Only watch,
Ask before changes, Fix safe issues, Auto-fix safe issues,
Full control, and Policy autopilot names may remain only as historical
context or compatibility implementation details, not as customer-facing Patrol
mode labels.
Assistant provider-readiness repair actions now use the canonical Pulse
Intelligence > Provider & Models route
/settings/pulse-intelligence/provider. The legacy /settings/system-ai
route remains a compatibility alias for old deep links, not a href emitted by
new Assistant provider-repair actions.
Primary platform route memory in frontend-modern/src/AppLayout.tsx may
preserve platform-local query state such as workload filters, but that memory
remains navigation chrome only. It must not fork Assistant drawer state, Patrol
utility route state, prompt context, resource reads, commercial posture, or
cross-platform query parameters.
Rejected Patrol investigation-fix approvals are terminal governed-action
decisions in the AI runtime. /api/ai/approvals/{id}/deny must persist the
approval-store denial, record a rejected unified action-audit decision when the
approval carries a governed action plan, and move the owning finding to
fix_rejected so Assistant handoffs, Patrol summaries, and external agent
adapters see the declined fix as explicit loop state rather than a disappeared
pending approval.
Legacy Assistant provider-tool declarations for manifest-backed Patrol finding
lifecycle tools now consume the manifest-owned raw input schemas through
agentcapabilities.ProviderInputSchemaFromRaw. The native Assistant runtime
may keep first-party model-facing descriptions, but it must not re-declare
required arguments such as resolution_note or dismissal note when the
canonical Pulse Intelligence manifest and API contract mark those fields
optional.
Legacy native Assistant utility provider aliases for run_command, fetch_url,
and set_resource_url, plus their provider JSON schemas, are owned by
agentcapabilities.LegacyAssistantUtilityProviderTools. The older native
Assistant service may continue to expose those compatibility aliases while the
execution migration proceeds, but it must consume the shared provider projection
and shared argument constants rather than carrying inline schema maps or local
tool-input string keys.
Native Assistant registry tools that operate on Patrol finding lifecycle state
must also consume the same agentcapabilities argument vocabulary for
finding_id, resolution_note, reason, and note instead of repeating
local field-name strings in their provider schema or execution maps.
Anthropic runtime provider execution is API-key backed only. Legacy Anthropic OAuth tokens may remain in encrypted settings solely for disconnect cleanup; they do not make Anthropic configured, do not instantiate a provider, do not refresh tokens, and cannot send model requests.
The done event carries context_limit_tokens (the active model's context
window from providers.ContextWindowTokens) alongside token usage, and the
drawer's last-turn summary renders input tokens as a percentage of that
window ("8,500 tokens (6% of context)") so operators can see compaction
coming; when the limit is absent the summary renders exactly as before.
Post-tool model turns mark the status handoff: every agentic turn after the
first emits a model_processing workflow state ("Working on the response
with the gathered results.") before the provider call, so a provider that
reasons server-side without streaming events can never leave the previous
tool's status stale on screen; model_thinking still upgrades the status
when reasoning deltas actually arrive. Pinned by the interaction-quality
corpus scenario "post-tool model turn marks the status handoff".
Session titles are model-generated after the first exchange
(internal/ai/chat/session_title.go): the chat runtime upgrades the
auto-truncated first-prompt placeholder once the done event has reached the
client, never overwrites a user-set title (placeholder comparison plus a
pre-persist re-check), passes the model-boundary sanitizer, records usage as
assistant_session_title, and leaves the placeholder on failure or timeout.
The long-form contract lives with the session rename clause in Extension
Points.
Finding identity is the LLM-assigned key (resource+category+key hash → ID),
and key collisions are surfaced, not forked: when a same-ID re-detection's
title shares essentially no keywords with the existing finding's title
(keywordOverlap at or below findingIdentityShiftMaxTitleOverlap,
internal/ai/findings.go), the merge proceeds — the latest report still owns
the text, because key forking would split LLM rephrasings of one real issue
into duplicate findings — but FindingsStore.Add appends a
content_replaced lifecycle event preserving the previous and new titles in
metadata and logs the collision. This keeps the operator timeline honest
when a distinct issue reuses an existing finding's identity (including the
resolved case, where the reactivation otherwise reads as a regression of the
old issue: both content_replaced and regressed events appear).
Rephrasings and identical re-detections stay event-free per the
heartbeat-not-transition rule. The shared lifecycle presentation
(frontend-modern/src/utils/aiFindingPresentation.ts,
patrol-intelligence subsystem) labels the event "Re-detected with different
details" — see that contract's Current State entry for the UI-side rule.
TestFindingsStore_KeyCollisionRecordsContentReplacedEvent and its
companion tests (internal/ai/findings_lifecycle_test.go) pin the behavior.
The interaction-quality scenario corpus
(internal/ai/chat/interaction_scenario_corpus_test.go) is the canonical
regression home for chat-feel promises, mirroring the Discovery corpus
(internal/servicediscovery/scenario_corpus_test.go). Each scenario drives a
full ExecuteStream turn against a scripted provider and pins the
browser-facing event stream — ordered event types, forbidden event types,
answer-text teeth, and payload teeth — for the user-visible promises shipped
in the OpenCode-restraint arc: clean plain/greeting turns with no tool noise,
compact tool events with real tool names, the clean no-narrative fallback
sentence (no raw JSON or provider call ids), invisible pre-event provider
retries, and exactly one clear error event on terminal provider failure.
Interaction-quality fixes must land with a corpus scenario stating the
promise that would fail without the fix, so loop refactors (including
Patrol-phase work on the shared agentic loop) cannot silently regress the
chat feel.
The Assistant system prompt's missing-target policy is resolve-before-asking
(buildSystemPromptWithToolGovernance, internal/ai/chat/service.go): a
command or diagnostic request that names no target sends the model to
read-only query/topology tools first; a sole plausible match is used directly
for read-only diagnostics and named in the answer; the operator is asked only
when several plausible targets remain or the action changes state. This
supersedes the ask-first framing ("Missing target information is not a safe
default") that deflected every untargeted "run X" request back to the
operator even on single-host deployments. Placeholder targets
(current_resource outside an attached-resource turn) remain forbidden in
all modes, and write actions still require an explicit operator-confirmed
target. TestBuildSystemPrompt_CurrentResourceRequiresResourceHandoff pins
the boundary strings; the full Extension-Points entry sits beside the
model-owned tool-manifest rule.
The per-turn Assistant system prompt carries the current wall-clock time (the
Pulse server clock) so the Assistant answers "what time/date is it" directly
instead of deflecting ("I don't have access to a real-time clock") or demanding
a target_host just to run date. The timestamp is appended in
AgenticLoop.getSystemPrompt (internal/ai/chat/agentic_prompt.go) rather than
baked into baseSystemPrompt: the base prompt is frozen when the loop is
constructed at service start, so anything that must stay fresh per turn
(execution mode, current time) is appended on each turn. The current time
carries no PII and is therefore safe on cloud-routed turns regardless of the
operational-context sharing opt-in.
The streaming transcript list reconciles messages by id rather than rendering
the raw message array by reference. useChat rebuilds its message array
immutably on every stream event (each content chunk, workflow-status change, and
tool update spreads a brand-new message object), so a reference-keyed <For>
would tear down and recreate the whole MessageItem on every event — the
visible flashing, rows popping in and out, and transcript jumping up and down
during a turn. ChatMessages (frontend-modern/src/components/AI/Chat/ChatMessages.tsx)
reconciles the incoming array into a keyed solid-js/store mirror so each
message keeps a stable identity across updates; MessageItem already reads every
field through accessors, so only the genuinely changed text/rows update in place.
This keeps the live transcript stable the way OpenCode's timeline is.
The same stability rule applies one level down, to the per-message
stream-event/tool-row list inside MessageItem. groupStreamEventsForDisplay
remaps blocks to fresh objects on every tick and StreamDisplayEvent carries no
id, but the grouped list is strictly append-ordered (the grouper only pushes new
blocks or mutates the open content/thinking block in place — it never inserts
mid-list or reorders). It therefore renders through <Index> (positional
keying), not a reference-keyed <For>: each row keeps its DOM node across
event-object rebuilds at a stable position, so the streaming answer block and
completed tool rows update in place instead of re-mounting (and re-parsing
markdown) on every content delta. Positional keying is correct here precisely
because the list is append-only; a list that could reorder or key by identity
must not use <Index>.
The streaming answer block itself renders by morphing the DOM, not by replacing
innerHTML on every tick. renderMarkdown re-parses the full (DOMPurify-
sanitized) answer on each paced reveal; assigning that to innerHTML rebuilds
the entire prose subtree, so a multi-paragraph or list/table answer flickers and
reflows every earlier line as it streams. AssistantMarkdownBlock instead feeds
the sanitized HTML to morphMarkdownInto
(frontend-modern/src/components/AI/Chat/markdownMorph.ts), which reconciles old
and new trees in place: structurally-identical nodes are left untouched, same-tag
nodes are morphed (recursing into children so a growing <ol>/<table> keeps
its earlier <li>/<tr> and only the last one updates), and the growing tail
block updates its text node rather than being rebuilt. The morph is a rendering
optimization only — renderMarkdown remains the sole sanitization gate and the
morph must never be fed unsanitized HTML.
Live workflow status is footer-owned, not a transcript artifact (the OpenCode
restraint model — see the canonical statement in Extension Points, which
supersedes the older per-row transcript-status rules in this contract). Earlier
Pulse rendered "Preparing context / Reading inventory / Counting / Waiting for
assistant / retrying" both as transcript rows AND in the activity dock, so the
chat narrated every internal step into the timeline. Now MessageItem never
renders workflow status — neither the per-event role="status" row
(shouldRenderWorkflowStatusEvent is false) nor the early-phase header chip
(shouldShowHeaderWorkflowStatus is false). The transcript carries only
durable artifacts (reasoning, tool calls, model-route rows, the answer). The
single live "assistant is working" indicator is the activity dock above the
composer, which shows a spinner + the current status + the model route + Stop.
The dock is gated on assistantTurnActive (loading OR a streaming assistant
message) rather than chat.isLoading() so it persists for the whole turn:
isLoading flips false at visible-turn-complete, which previously made the dock
flash its status for a frame and vanish.
Prompt wording must not gate tool availability at all. The greeting/meta
text-only classifier (assistantPromptLooksConversational) is removed along
with the rest of the assistantToolScopeForPrompt keyword router: even "hi"
or "thanks" turns carry the full governed manifest, because tools offered is
not tools used — the model answers a greeting without calling anything, while
short prompts that look conversational to a keyword list are usually resource
lookups ("hows esphome", "check frigate", "grafana cpu") that need the
query/read/metrics tools to answer from Pulse data. Withholding tools by word
count or greeting keywords made the Assistant tell the user it had "no
infrastructure context or diagnostic tools" and ask them to run docker ps
themselves, for a resource Pulse already inventories. Tool selection stays
model-owned: offer the tools and let the model decide whether to use them,
rather than pre-deciding from prompt keywords/length that no tools are
needed. TestService_ExecuteStream_ToolManifestIsModelOwned is the
regression proof.
The pulse_query get action (executeGetResource in
internal/ai/tools/tools_query.go) must accept a canonical resource handle
(<type>-<hash>, e.g. system-container-599a2e3…) as resource_id without a
separate resource_type and infer the type from the handle, because that is how
a model naturally references a resource it read from context. Failing such a call
with "resource_type is required" burns visibly-failed tool calls in the
transcript before the model recovers with the numeric id. The type is recovered
from the trailing hex hash segment (no type word is all-hex); a bare numeric
VMID still requires an explicit resource_type.
When the model runs tools but returns no final narrative, the deterministic
fallback summary (buildAutomaticFallbackSummary in
internal/ai/chat/agentic_final.go) must read as a clean operator message, not a
dump of internals. It names the tools it ran by their real tool name resolved
from the assistant tool calls (pulse_ prefix stripped) — never the opaque
provider call id (call_…/toolu_…/fc_…), which must not leak into
chat-visible text — and it must not append raw tool output / result snippets.
The earlier form ("I completed N successful check(s) using call_27f0f389…, …
automatic summary. Latest successful result snippet: {…raw JSON…}") is forbidden:
provider call ids and raw tool JSON are not operator-facing answer content.
Assistant slash-command availability is part of the command runtime contract,
not only visual polish. The OpenCode reference at fetched origin/dev commit
c495635 filters prompt slash commands through the registered command catalog
and omits disabled builtin commands from the prompt popover in
packages/app/src/components/prompt-input.tsx (slash command list creation,
lines 677-699), while command metadata owns disabled and slash in
packages/app/src/context/command.tsx. Pulse adapts that by deriving command
availability from the same predicates as the drawer toolbar: prompt slash
autocomplete hides unavailable local commands, command help may show them
disabled with a reason, and manual slash submissions respect the same
availability before running local session actions.
Assistant provider-route recovery must read as explicit operator action, not
automatic fallback. The OpenCode reference at fetched origin/dev commit
914a643 retries the selected provider/model route through
packages/opencode/src/session/retry.ts and
packages/opencode/src/session/processor.ts by passing
input.model.providerID into SessionRetry.policy, renders retry state in
packages/ui/src/components/session-retry.tsx, and records provider/model
changes as explicit ModelSwitched session events in
packages/core/src/session/event.ts plus packages/opencode/src/session/prompt.ts.
Pulse adapts that by keeping same-route retry visible, ignoring obsolete
provider route-switch metadata without changing the selected route, and labeling
failed-turn or readiness recovery buttons as explicit route/model-route choices
instead of implying automatic route adoption.
Assistant model-catalog failure is selector-local state, not a drawer
initialization failure. The OpenCode reference at fetched dev commit
3867fa2bad0e644166e360e2e99cfe426fe71105
packages/opencode/src/cli/error.ts lines 58-69 formats missing model/catalog
state as an operator-facing model-selection problem with a list-models hint,
while packages/opencode/src/session/llm.ts lines 96-104 resolves the
selected provider/model route independently for the stream. Pulse adapts that
by letting Assistant sessions, settings, route health, and the composer finish
opening when /api/ai/models fails; the catalog error stays attached to the
model selector and may be refreshed explicitly, but startup must not log or
render a broad Assistant initialization failure, and it must not replace the
selected model route.
Assistant completed-turn chrome is route-owned summary, not raw usage output.
The OpenCode reference at fetched dev commit
3867fa2bad0e644166e360e2e99cfe426fe71105 imports turnSummaryCommit in
packages/opencode/src/cli/cmd/run/scrollback.surface.ts lines 19-20, while
packages/opencode/src/cli/cmd/run/turn-summary.ts lines 5-46 emits a final
system summary with agent, model, and duration. Pulse adapts that by deriving
the latest completed, non-streaming assistant turn and surfacing its effective
provider/model route, duration, and backend-proven token total in low-priority
composer chrome. Input/output usage remains title/accessibility detail only,
missing provider usage does not hide route and duration, and active streaming
turns never appear as the completed-turn summary.
Assistant tool activity now follows an OpenCode-referenced chronological row
model where appropriate for Pulse. Consecutive context/read/query tools render
as visible transcript rows in arrival order instead of being replaced by a
grouped context footer, while command previews, inputs, progress, and large
outputs remain contained inside each tool row. This keeps the user-facing stream
feeling active without dumping large command output into the default answer.
Fast tool completions must also stay visibly live long enough to be perceived:
the frontend stream reducer stamps sub-420ms successful tool completions with a
transient settle deadline, and the row renders that deadline as a running state
even if the turn's done event has already arrived. The referenced OpenCode
source at fetched dev commit e82542b8023a8374f29c23b70ec019c8f256354e
implements the same user-visible principle in
packages/opencode/src/cli/cmd/run/session-data.ts by emitting a start commit
for running tools and a later completed/error commit instead of only surfacing a
batched terminal transcript. Pulse adapts that as an in-memory UI settle window
because Pulse transcripts persist completed tool facts, not OpenCode scrollback
commit phases.
The 2026-06-08 live-tool-start slice rechecked OpenCode source at fetched dev
commit 3867fa2bad0e644166e360e2e99cfe426fe71105:
packages/opencode/src/session/processor.ts creates the visible tool part in
ensureToolCall (lines 299-349) and moves that same part to running on
tool-call (lines 507-521). Pulse adapts that by making ToolStartData
self-describing live activity: tool_start must carry a running phase when
execution is visible, and the frontend must render a new start row as running
immediately even if tool_progress arrives in a later paint.
Assistant provider retries are a first-class visible workflow state, not a
hidden server log. The referenced OpenCode source at fetched dev commit
7ae856a9e97130f664f6f11fa5871a2795de9902 defines retry session status in
packages/opencode/src/session/status.ts (SessionStatus.Info /
SessionStatus.set), wires retry updates from
packages/opencode/src/session/processor.ts through
SessionRetry.policy({ set }), and renders retry attempt/backoff state from
status().type === "retry" in
packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx. Pulse adapts
that contract at the owning stream boundary: WorkflowStateData carries
attempt, max_attempts, and retry_after_ms, and AgenticLoop emits
provider_retry before sleeping between transient pre-output provider
attempts. The frontend active-turn footer and live transcript workflow row
render that same typed workflow state immediately as compact attempt/backoff
progress, counting down retry_after_ms from started_at while the turn
remains active, so the user sees Pulse moving through a retry instead of
staring at an obsolete provider-wait message. Retry workflow states are not
held behind workflow-history pacing.
Assistant workflow progress is also a live typed activity row while a turn is
in flight, not only hidden footer state. The referenced OpenCode source at
fetched dev commit 7ae856a9e97130f664f6f11fa5871a2795de9902 stores
session status separately from message parts in
packages/opencode/src/cli/cmd/tui/context/sync.tsx (session_status,
session.status event handling) and renders a session from live messages,
permissions, questions, and running tool parts in
packages/opencode/src/cli/cmd/tui/routes/session/index.tsx (Session,
messages, permissions, questions, foreground ToolPart selection, and
session.status handling). Pulse adapts that source pattern with a transient
frontend workflow_status display event: each incoming backend
workflow_state replaces the prior workflow row, the active footer reads the
same typed status, and visible assistant content, reasoning, tool,
approval/question, terminal done, and terminal error events clear that row.
The frontend now also keeps a bounded live-only workflowStatusHistory for the
active assistant message so the reducer preserves state continuity while
backend preparation, context, and provider-start labels are being replaced. The
shared workflow-status presentation helper owns both the transcript row and the
active-turn composer footer so the two live surfaces show the latest canonical
workflow state immediately. Retry/backoff and stream-idle liveness states cut
through pacing because they are current route health, not neutral setup motion.
The transcript and footer therefore show current motion while the provider is
starting, retrying, or reasoning, but completed answers do not retain stale
internal-progress prose.
Stream-idle heartbeats are part of that same visible workflow state. When the
frontend knows the selected provider/model route from the backend workflow event
or the streaming assistant message, the idle heartbeat must render as
route-specific liveness such as OpenRouter is still working; waiting for more response data. instead of reverting to generic Assistant waiting copy.
Prompt dispatch itself is also a visible live state: before any backend
workflow event returns, the frontend seeds the active assistant turn with a
local request_send / Sending prompt. workflow row after any selected-model
row. That row is explicitly transient: it is the current live state but does
not enter the paced backend workflow history, the first backend workflow state
may replace it, and content, thinking, tool, approval, question, model-route,
done, or error evidence removes it from the durable transcript. This adapts the
referenced OpenCode source at fetched dev commit
e82542b8023a8374f29c23b70ec019c8f256354e, where
packages/opencode/src/cli/cmd/run/runtime.queue.ts emits turn.send with
sending prompt at lines 174-184, packages/opencode/src/cli/cmd/run/footer.ts
maps turn.send and turn.wait to live footer status at lines 133-145, and
packages/opencode/src/cli/cmd/run/stream.transport.ts promotes quiet
post-send periods to waiting for assistant at lines 1350-1357. Pulse keeps
that send/wait distinction in the live chat turn by promoting an unreplaced
local request_send status to local request_wait / Waiting for assistant
when the browser API client receives an open SSE response but before any parsed
backend-visible SSE activity arrives, while preserving the Pulse transcript
rule that only backend workflow, route, content, and tool evidence remain once
real activity exists.
Assistant drawer shell status follows the same OpenCode-referenced separation
between prompt-adjacent operational status and transcript content. The
referenced OpenCode source at fetched dev commit
e82542b8023a8374f29c23b70ec019c8f256354e renders agent/model metadata beneath
the prompt and active/retry/interrupt state in the prompt footer in
packages/tui/src/component/prompt/index.tsx, with broader system status kept
in packages/tui/src/routes/session/footer.tsx. Pulse adapts that pattern for
the web drawer by keeping model route, recent-route cycling, control mode,
last-turn usage, active workflow progress, queued follow-ups, route-recovery
notices, and the autonomous control warning in the input-adjacent
composer/status rail.
Those items stay visible and actionable, but they do not compete with the
transcript as separate top-of-drawer banners unless they are provider readiness
or scoped handoff context surfaces with their own governed content.
Queued follow-up pressure is part of that active-turn state: the frontend
derives the count from queued user transcript turns, appends it to the primary
active Assistant headline while a turn is running, and keeps the separate
queued-follow-up row actionable for reorder/cancel operations.
That suffix must compose with live workflow pacing rather than disabling it:
when backend workflow states arrive in a burst, the active headline still walks
through the paced status sequence while retaining the queued follow-up count.
This preserves the OpenCode-referenced live-part feel from
packages/tui/src/routes/session/index.tsx lines 1492-1503 and the running
tool metadata updates in packages/opencode/src/session/tools.ts lines 54-63,
while adapting the interaction to Pulse's single active-turn plus queued
follow-up safety model.
Assistant chat actions follow the same OpenCode-referenced footer principle:
actions are reachable from prompt-adjacent chrome, not only from a hidden slash
draft. The referenced OpenCode source at fetched dev commit
e82542b8023a8374f29c23b70ec019c8f256354e builds the direct command surface in
packages/opencode/src/cli/cmd/run/footer.command.tsx (RunCommandMenuBody
with suggested actions, slash commands, and search) and keeps the footer command
entry discoverable from packages/opencode/src/cli/cmd/run/footer.view.tsx.
Pulse adapts that pattern for the web drawer by exposing the existing Assistant
commands dialog from the composer chrome and making that dialog searchable and
keyboard-selectable. The button, slash-triggered help, dialog search, and
keyboard selection must use the same ASSISTANT_SLASH_COMMANDS registry,
filterAssistantSlashCommands, AssistantCommandHelpDialog, and
executeSlashCommand path as /help and /commands; they must not introduce
a second command registry or a separate provider-bound prompt path. The command
dialog search box must compose the frontend-primitives SearchField so the
shared search-control registry owns clear-button behavior, input chrome,
keyboard forwarding, and native-search-input drift prevention while AI runtime
keeps ownership of command filtering and execution semantics.
Primary nav moved to governed platform/runtime destinations on 2026-05-16 and
was clarified on 2026-05-25 and 2026-06-25 through frontend-modern/src/App.tsx and
frontend-modern/src/AppLayout.tsx: the top of the app may expose canonical
platform pages (Proxmox, Kubernetes, TrueNAS, vSphere) plus the
Docker / Podman container-runtime lens (shown as Docker in the shell), aggregate
platform-owned Workloads / Storage / Recovery sub-surfaces, Alerts, Patrol, and
Settings. Patrol is the visible destination for Patrol-owned operator work; it
must route through the canonical /patrol surface and must not create a second
Assistant or Intelligence route namespace.
Provider/runtime destinations must pass the shared support-and-resource-
evidence gate before they appear in navigation, command palette entries,
keyboard shortcuts, or landing fallbacks. Aggregate workspace tabs are retired
top-level routes, not compatibility placeholders. Admitted-only or absent
platform/runtime surfaces must stay hidden from the AI-adjacent shell and must
not be kept as disabled placeholders. The legacy /infrastructure route shell
was retired alongside its page wrapper, and /workloads, /storage,
/recovery, /ceph, /ai, and /operations/* remain unregistered
top-level routes. Primary platform tab settingsRoute handoffs must also point
to the canonical /settings/infrastructure workspace rather than retired
settings aliases such as /settings/workloads/docker or nested
/settings/infrastructure/platforms/* paths. The AI Chat launcher, Patrol surfaces, and
AssistantHandoffPayload deep links must use canonical platform or runtime
routes (/proxmox/overview, /proxmox/storage, /kubernetes/workloads, etc.)
rather than reviving retired Infrastructure or aggregate workspace paths;
adding a platform tab through the same shell files must not fork Assistant or
Patrol shell state or smuggle in AI-owned platform reads.
Post-auth / and /login resolution follows the frontend-primitives-owned
provider-first platform landing contract, so the assistant-capable shell never
overrides Machines-surface eligibility or revives legacy Infrastructure as the
default estate surface. The user-facing Machines label is an app-shell
presentation label for the existing standalone route/id and must not create a
separate AI handoff or prompt namespace.
Alert-triggered scoped patrols now investigate the specific breach rather than
running a broad health check. The alert bridge (internal/ai/unified/bridge.go,
internal/ai/unified/setup.go) carries the firing alert's real payload — type,
level, value, threshold, resource identifier, and message — into
PatrolScope.AlertContext, and internal/ai/patrol_ai.go /
internal/ai/patrol_triggers.go frame the alert_fired run around that breach
instead of suppressing threshold context. Whether an alert triggers a patrol at
all is the operator's per-rule policy: AIConfig.AlertTriggersInvestigation
(internal/config/ai.go) enforces the master enable, a minimum-severity floor
(patrol_alert_trigger_min_severity, default critical-only), and an optional
alert-type allowlist (patrol_alert_trigger_types, empty = all types). The
router-side bridge wiring consults that policy and skips queuing the scoped
patrol when the alert does not qualify; an unknown alert level is treated as
critical so it is never silently dropped.
The route-backed Proxmox platform tab is app-shell navigation only. Adding the
tab through frontend-modern/src/App.tsx and
frontend-modern/src/AppLayout.tsx must not fork Assistant or Patrol shell
state, synthesize platform-specific handoffs, or add AI-owned platform reads.
Future Proxmox-native Assistant or Patrol read/write claims must extend the
shared AI tool, handoff, and platform-support contracts instead of hiding
behind route registration or tab chrome.
Patrol deterministic signal extraction (internal/ai/patrol_signals.go)
does not mirror the Alerts surface. The pulse_alerts tool output is
intentionally absent from the signal switch in DetectSignals — alerts
already have their own canonical surface, lifecycle, and operator
acknowledgement model, and the SignalActiveAlert mirror path has
been removed. Mirroring previously double-counted (every alert was
also a Patrol "Active alert detected" finding), dragged the health
score down for issues the operator already knew about, and produced
bogus auto_resolved → re-detected → regressed cycles when the LLM
explicitly resolved the mirrored finding while the underlying alert
kept firing. Patrol's job, per its own system prompt, is to surface
issues alerts cannot — trends, capacity risks, misconfigurations,
reliability gaps, cross-resource correlations. The Alerts page is
the canonical surface for currently-firing alerts. To retire the
alert-mirror findings already persisted from an earlier build,
FindingsStore.SetPersistence runs a one-shot pass on load that
auto-resolves any active finding matching the legacy signature
(title "Active alert detected", source ai-analysis, category
general) with a clear retirement reason; the pass is idempotent
and self-cleaning. The same load pass also resets the
RegressionCount and clears LastRegressionAt on any active
finding whose lifecycle contains an auto_resolved event when
the finding's category is not eligible for stale-auto-resolve
(i.e. anything other than performance or capacity). For
event/persistent categories there is no legitimate absence-driven
resolution path: an auto_resolved event there came either from
one of the now-gated absence paths (legacy reason strings still
recognized) or from the LLM patrol_resolve_finding tool
(empty-message via Resolve(_, true)) which has repeatedly
misjudged findings backed by still-active conditions and reverted
through a regression on the next run. The reset appends a
regression_counter_reset lifecycle event so the migration is
idempotent; genuine recurrences from then on accrue cleanly.
Performance/capacity findings retain their counter because the
metric-cleared resolution model is sound there.
The overall health score (calculateOverallHealth in
internal/ai/intelligence.go) tiers the "recent Patrol errors" coverage
factor by the ratio of errored runs to relevant runs in the scoring
window. Above 50% of recent runs erroring is a -30 impact and is
described as "Most recent Patrol runs encountered errors"; above 25%
is -20; otherwise the original -10 light-tier description applies.
This prevents the score chip from showing grade A while the same
assessment surface warns the operator that coverage is incomplete or
recent runs failed, which previously happened whenever one successful
manual run sat among many failed startup runs.
Downstream Assistant handoffs must treat that coverage factor as a
secondary caveat when Patrol also carries active findings, pending
approvals, or governed action references. The coverage-gap explanation
and scoped-activity prompt are primary Assistant framing only for
coverage-only assessments; active Patrol findings keep the prompt,
briefing action label, and safety note focused on finding priority,
affected resources, evidence, and the governed next step.
Absence-based auto-resolve paths in internal/ai/patrol_ai.go are
all gated on the category whitelist exposed by
CategorySupportsStaleAutoResolve in internal/ai/findings.go. Two
paths use the gate: reconcileStaleFindings (auto-resolves findings
the LLM didn't re-mention in a successful run) and the
resource-absent branch inside the seed-prompt builder (auto-resolves
findings whose resource isn't in the current global inventory
snapshot). Only performance and capacity findings — continuous
current-state metric thresholds where the most recent successful
scan's observation is authoritative — may be auto-resolved from
absence. reliability, backup, security, and general findings
represent discrete events or persistent states; the LLM not
re-mentioning a failed backup task or a single inventory snapshot
missing a container (transient agent reconnect, container churn,
refresh gap) is not evidence that the underlying issue has cleared.
Those categories must stay active until explicitly resolved either
by the LLM calling patrol_resolve_finding with evidence or by
operator action through the governed findings store. Lifecycle events recorded by findings.go must
not introduce duplicate generic transition rows: the canonical
syncLoopStateLocked records loop_transition_violation only when a
transition is rejected, and otherwise leaves the semantic event
(auto_resolved, regressed, dismissed, acknowledged, snoozed,
reminded, suppression_lifted, etc.) as the single audit row for the
transition. Re-detections of an already-active finding update
TimesRaised and LastSeenAt only — they must not append a detected
lifecycle event, because a heartbeat is not a transition.
The findings store now consults a ResourceOperatorStateProvider
during the new-finding path. The interface lives in internal/ai to
avoid an import cycle with internal/unifiedresources; the API layer
wires the adapter at startup, projecting
unified.ResourceOperatorState into the
ResourceOperatorStateProjection shape the findings runtime
consumes. The projection carries every operator-set signal in one
call (active maintenance window, IntentionallyOffline,
NeverAutoRemediate, and Criticality) so adding new signals later does
not multiply round-trips per finding. FindingsStore.Add stamps
Finding.ResourceCriticality from that projection on new and re-detected
findings. The field persists as resource_criticality and may affect
same-severity Patrol attention ordering only; it must not mutate
Finding.Severity or bypass severity escalation/resolution rules.
The approval store exposes SetOnApprovalCreated(cb) so the API
layer can install a fire-and-forget callback that runs after every
successful CreateApproval (the approval is already persisted at
that point). The callback is invoked on its own goroutine against
a snapshot of the request, keeping the approval hot path off any
consumer's slowness and avoiding any chance of the consumer
reentering the store under the held write lock. This is the seam
the agent SSE stream uses to publish approval.pending events
without coupling the approval store to the api package.
ApprovalRequest.CanonicalResourceID() is the helper the bridge
uses to stamp resourceId on those events, derived from
(TargetType, TargetID, TargetName) via the same rule the store
uses internally for preflight context normalization — Plan-less
approvals (the common shape on the approval hot path) still carry
a canonical resource id agents can match against the rest of
Pulse.
PulseToolExecutor exposes SetOnActionCompleted(cb) as the
parallel seam for action-audit terminal states. The action-audit
hot path in internal/ai/tools/action_audit.go routes every
terminal-state record (Completed, runtime-Failed, plan-drift
refusal, operator-lock refusal, recovery-branch fail) through a
single helper publishActionCompleted(record) which guards on
nil callback, defensively filters non-terminal states, and fires
the callback on its own goroutine after the audit record has
been persisted. Refused-before-dispatch failures preserve the
canonical plan_drift: and resource_remediation_locked:
error-token prefixes on record.Result.ErrorMessage so the agent
SSE stream's action.completed payload carries them verbatim —
agents branch on the prefix rather than parsing human text.
The investigation runtime now hands the orchestrator a Finding
pre-enriched with operator-set state and operational memory.
MaybeInvestigateFinding (in internal/ai/patrol_findings.go)
calls f.ToCoreFinding() then attaches a
FindingOperatorContext from the in-memory operator-state
projection (intentionally offline, never auto-remediate, active
maintenance window) and a FindingOperationalMemory projection
(regression count, previous resolved fix summary, times raised)
populated from fields the internal Finding already carries. The
orchestrator (in pulse-pro) consumes these fields when reasoning
about the next move — it does not need a separate read to get
the situated picture, and it can avoid proposing fixes the
operator has locked the resource against.
ResourceOperatorStateProjection carries NeverAutoRemediate
and Criticality alongside IntentionallyOffline and MaintenanceWindow so the
investigation read path and the suppression read path share a
single projection. The findings store exposes the projection via
OperatorStateProjectionFor; the suppression hot path keeps its
existing internal access. Both paths see the same operator-state
facts so investigation reasoning, suppression behavior, and Patrol
priority stamping cannot drift against each other.
A cross-slice consequence worth pinning: operator-state-suppressed
findings (auto-dismissed with DismissedReason="expected_behavior"
and operator_state_cause metadata) are also ineligible for
autonomous investigation, because Finding.ShouldInvestigate
already gates on f.DismissedReason != "". Investigation budget is
not spent on findings the operator has told Pulse to stay quiet
about, regardless of autonomy level. This is delivered by the
existing chain (Add → auto-dismiss → ShouldInvestigate-false)
rather than a separate runtime check; the contract test in
findings_test.go pins the relationship so a future refactor of
either branch cannot silently waste investigation budget on
operator-suppressed findings.
The operator-state suppression is also reversible. When a finding
auto-dismissed under operator_state_cause re-detects after the
underlying suppression has lifted (maintenance window passed,
IntentionallyOffline cleared), FindingsStore.Add wakes it with
a suppression_lifted lifecycle event. The wake gates on the most
recent lifecycle dismiss event carrying operator_state_cause
metadata via findOperatorStateDismissCause, so a manual operator
dismissal that supersedes an earlier auto-dismiss is not
falsely re-awakened — the helper stops at the first dismissed
event when scanning from newest backwards, treating that as the
authoritative state.
When the projection carries an active maintenance window, the
new-finding path auto-dismisses with reason expected_behavior,
attributes the suppression on the lifecycle timeline
(operator_state_cause: maintenance_window, with
maintenance_end_at metadata), and persists the finding for audit
history. The action broker (executeCommandWithAudit in
internal/ai/tools/action_audit.go) consults the same
resource_operator_state table on every dispatch and refuses with
unifiedresources.ErrResourceRemediationLocked when the operator
has set NeverAutoRemediate=true on the target resource. Refusal
persists a Failed audit record whose ErrorMessage is prefixed
resource_remediation_locked: so the audit timeline shows every
refused dispatch, paralleling the plan_drift: shape from the drift
guard. Operator state outranks per-action approval — the broker
refuses even when the approval ID resolves and the plan hash matches. The IntentionallyOffline branch is the indefinite
counterpart — same auto-dismiss but with
operator_state_cause: intentionally_offline and no
maintenance_end_at field because the suppression has no scheduled
end. Maintenance windows take priority over intentionally-offline
when both are active, because the time-bounded suppression is the
more honest one to surface to the operator. Deployments without a
provider keep the original new-finding behavior — suppression is
opt-in.
This subsystem now makes Pulse Assistant and Patrol backend runtime ownership
explicit inside the current architecture lane instead of leaving those
surfaces implicit inside broad architecture or generic API ownership. A later
lane split can still promote this area into its own product lane once the
governed floor is ready.
That backend/runtime ownership does not require the Patrol product surface to
inherit AI as its canonical browser route: the customer-facing shell may use
/patrol while shared AI transport, provider settings, and payload contracts
remain the governed technical boundary behind it.
Operator-configured provider base URLs remain part of that backend transport
boundary. Ollama keeps supporting remote or local instances, but
internal/ai/providers/ollama.go must normalize the configured base URL and
route requests through the shared restricted outbound HTTP transport so
metadata, link-local, and redirect-escape paths do not bypass the runtime's
egress guardrails.
That same operator-facing vocabulary rule applies to the runtime usage surface:
frontend-modern/src/components/AI/AICostDashboard.tsx must present provider
usage and spend backing Pulse Assistant and Patrol rather than generic AI
history, and frontend-modern/src/utils/aiCostPresentation.ts must own the
title, empty/loading states, budget note, and reset/export history messaging so
settings shells and runtime widgets do not fork their own usage wording.
Usage grouping rows must present product concepts such as Assistant sessions,
Patrol runs, Discovery runs, or concrete resource labels. Raw target storage
keys, opaque session identifiers, UUIDs, and values such as
assistant_session_title:<id> are accounting implementation detail and must
stay out of the operator-facing table.
That same runtime-facing table ownership applies to the cost dashboard shell:
frontend-modern/src/components/AI/AICostDashboard.tsx owns provider usage,
budget, and history semantics, but its tabular presentation must compose the
shared frontend-modern/src/components/shared/Table.tsx primitive instead of
carrying AI-local scroll wrappers or raw table shell markup. Any future AI
usage table styling change must extend the shared primitive or its governed
wrapper affordances first, then consume that contract from the dashboard.
internal/ai/ is the live backend AI engine. It owns chat execution, Patrol
orchestration, findings generation, investigation support, provider selection,
remediation flow, and cost persistence.
That Patrol runtime ownership includes seed-context admission control.
internal/ai/patrol_ai.go must build Patrol and triage prompts from
canonical seed sections, size them against the runtime budget model, and when
a provider reports a smaller real context window than the static model map,
reassemble the same canonical sections under tighter provider-derived budgets
instead of hard-failing or truncating ad hoc prompt strings.
That same backend runtime ownership also includes bounded Patrol and
investigation read models. internal/ai/patrol_history_persistence.go and
internal/ai/proxmox/events.go must cap persisted-history loads and
caller-requested read limits at the canonical runtime maxima instead of
allocating directly from raw on-disk counts or transport-supplied limits.
Callers may request fewer records, but AI runtime storage and correlation
surfaces remain responsible for enforcing the governed ceilings that protect
memory and keep Patrol/history behavior stable under malformed or oversized
inputs.
That same backend runtime ownership includes internal/config/ai.go, because
provider auth, base URLs, provider-scoped model defaults, and other persisted
runtime AI selection rules must stay canonical in the shared AI config model
instead of drifting into handler-local fallbacks or frontend-only assumptions.
That same provider-model ownership now explicitly forbids Pulse from baking
vendor model IDs into BYOK default selection. internal/config/ai.go may
persist an explicit operator-chosen model, but when a BYOK provider is
configured without a concrete model selection,
internal/ai/model_resolution.go must resolve the effective model from the
provider's live catalog at runtime using the shared provider metadata policy
instead of reviving static vendor constants in config defaults, service
fallbacks, or frontend setup flows.
Explicit saved model routes fail closed when their provider is not configured
or cannot be initialized. Pulse may not replace an operator-selected route such
as deepseek:*, openrouter:*, or openai:* with another configured provider
default during settings load, Assistant chat startup, or service initialization.
Provider-owned defaults are allowed only when no explicit route exists, or when
the same configured provider needs its governed chat-suitable/default model
because live catalog lookup failed or the explicit model is unsuitable for chat.
That same provider-model ownership also governs live-catalog failure fallback:
when runtime client construction fails, test credentials intentionally block a
provider catalog, or a provider returns no usable models, the effective BYOK
selection may fall back only to the provider-owned default declared in
internal/config/ai.go. Runtime startup, connection-test, and load-config
paths may not return an empty effective model or borrow another provider's
selection just because live model discovery was unavailable. DeepSeek's
provider-owned fallback must track the current V4 API contract and use
deepseek-v4-flash rather than retired compatibility aliases such as
deepseek-chat or deepseek-reasoner; AI runtime context-window and cost
budgeting must likewise know the V4 Flash/Pro 1M context and distinct pricing
classes before Patrol treats those models as ready.
The shared /api/ai/models catalog must preserve that same direct-provider
fallback posture for configured DeepSeek paths: when DeepSeek live catalog
listing fails or omits current V4 entries, the backend catalog must still
surface direct deepseek-v4-flash and deepseek-v4-pro options plus clearly
labelled legacy aliases so saved Patrol or Assistant selections do not render
as unrelated default models in the browser.
Retired quickstart ownership is now an inert compatibility boundary, not a
self-hosted GA runtime path. The old quickstart provider, bootstrap manager,
and local token-cache persistence API are removed from the Pulse runtime;
ordinary self-hosted Assistant, Patrol, and AI Settings flows must use the
operator's configured provider or local model and must not bootstrap managed
credits, hosted-model tokens, or quickstart-backed provider clients from the
frontend.
Public-facing copy that reflects old quickstart fields must normalize back to
provider or local-model setup. It must not promise managed credits, account
activation support, trial CTAs, anonymous Community bootstrap, or full hosted
chat access in ordinary self-hosted v6 GA flows.
That same runtime-backed contract now governs AI settings enablement too:
unconfigured installs open provider setup, while stale managed-credit or
activation-required states are treated as compatibility metadata rather than a
direct-enable path.
That same AI/runtime boundary now also owns the server-side assistant
availability fact used by the app shell. internal/api/ai_handlers.go,
internal/api/security_status_capabilities.go, and
internal/api/router_routes_auth_security.go must expose one canonical
/api/security/status.sessionCapabilities.assistantEnabled signal for the
closed assistant affordance, so unrelated shells do not probe
/api/settings/ai or /api/ai/sessions during ordinary route bootstrap just
to decide whether the assistant drawer may be opened.
That same frontend runtime boundary now also owns the shared AI read model for
AI-owned surfaces. frontend-modern/src/stores/aiRuntimeState.ts is the
canonical frontend owner for shared /api/settings/ai and /api/ai/models
reads used by chat, Patrol, and AI usage surfaces, while
frontend-modern/src/components/Settings/useAISettingsState.ts remains the
write-side settings owner. AI-owned surfaces must not fork their own mount-time
settings/model fetch loops once this store exists.
The assistant drawer/session shell is a separate shared boundary:
frontend-modern/src/stores/aiChat.ts owns open state, focused-input handoff,
context accumulation, and org-switch clearing for the assistant drawer, while
frontend-modern/src/stores/aiRuntimeState.ts owns the shared backend-backed
settings and model catalog reads. AI runtime consumers must not move drawer
shell state into page-local signals or teach aiChat.ts to bootstrap its own
/api/settings/ai or /api/ai/models reads.
That same drawer boundary owns responsive presentation too. The canonical
assistant drawer may dock and push the authenticated shell only when the
viewport is wide enough to preserve a usable primary operating surface; once
the available viewport drops below that shell threshold, the drawer must
become an overlay owned by frontend-modern/src/components/AI/Chat/index.tsx
instead of compressing Infrastructure, Workloads, Storage, or other primary
runtime pages into an unusable narrow column or forking page-local layout
exceptions.
The closed assistant launcher follows the same shared-shell rule. While the
mobile navigation shell is active, frontend-modern/src/AppLayout.tsx must
present the launcher as a bottom floating affordance that clears the mobile
nav instead of restoring the desktop right-edge rail at an earlier breakpoint.
The edge-mounted launcher is only valid at the desktop shell breakpoint where
the primary navigation and page chrome are also desktop-mode.
Non-AI shell notices may coexist in frontend-modern/src/AppLayout.tsx, but
they must remain presentation-only. Prerelease banners, billing callouts, or
other header-adjacent notices must not fork assistant open state, gate on AI
runtime fetches, or move assistant availability logic out of
frontend-modern/src/stores/aiChat.ts and frontend-modern/src/useAppRuntimeState.ts
just because they share the same authenticated shell. The remaining
prerelease-shell treatment is the compact Preview badge on rc-channel
builds; frontend-modern/src/AppLayout.tsx must not revive a standalone
release-candidate banner, release-notes CTA, or feedback CTA that starts
participating in assistant-shell state or modal ownership.
The retired monitored-system capacity banner follows the same shell rule:
frontend-modern/src/App.tsx must not reintroduce app-shell commercial
volume warnings just because settings or support surfaces still expose
monitored-system grouping data. Assistant state and shell notices stay
independent from retired infrastructure-volume commerce.
That same shared shell boundary must respect blocking modal ownership.
frontend-modern/src/App.tsx and frontend-modern/src/AppLayout.tsx may use
the shared dialog runtime to hide the closed assistant launcher and close the
drawer while a blocking shared dialog is open, but they must not leave Pulse
Assistant interactive behind a modal or fork a second assistant-open state
model to do it.
That same shared shell rule applies when presentation policy suppresses hosted
organization chrome: frontend-modern/src/App.tsx and
frontend-modern/src/AppLayout.tsx may hide org switchers or demo-only org
labels, but they must not couple assistant visibility, session reset, or
drawer-open behavior to that organization presentation state.
That same shell boundary also owns demo-only support-surface suppression:
Pulse no longer exposes Operations as a top-level route. Demo-only support
surfaces now hide inside the shared Settings navigation instead, and assistant
availability plus reset behavior must stay independent of that settings-nav
presentation choice.
Authenticated /login recovery belongs to that same route shell boundary:
once login succeeds, frontend-modern/src/App.tsx must resolve /login
through the canonical provider-first post-auth landing route instead of leaving
the assistant-capable authenticated shell stranded on a route that only exists
for logged-out presentation.
App-shell route preloading may include the Patrol route module, but it must
remain module-only. It must not prefetch AI settings, model state, findings,
chat sessions, or assistant context while the drawer is closed.
docs/release-control/v6/internal/subsystems/registry.json must therefore keep
frontend-modern/src/stores/aiRuntimeState.ts and
frontend-modern/src/components/AI/Chat/ on the explicit AI runtime proof
route, and keep frontend-modern/src/stores/aiChat.ts on the shared
AI-runtime/frontend-primitives proof boundary instead of leaving the chat shell
or assistant drawer state unowned.
That same settings/runtime boundary now also governs BYOK first-run setup:
frontend-modern/src/components/Settings/useAISettingsState.ts may send only
provider credentials or base URLs when the operator connects a provider, and
internal/api/ai_handlers.go plus internal/ai/service.go must persist the
resolved provider model returned by the canonical runtime selection path. The
setup surface must not reintroduce vendor-default model IDs in modal payloads
just to make the backend accept the request.
Interactive Assistant chat must not put a Pulse-authored intent router, scout
model, or explore pre-pass in front of the operator's selected model. The
runtime may assemble governed context and expose the approved tool list, but
the selected model owns the decision to answer directly, ask a question, read
context, or request an action. Pulse must not use prompt heuristics to force
tool_choice=any, force a named tool, retry because an expected tool was not
used, or hide tools from the model based on keyword detection. Pulse
enforcement starts after that model choice: approval mode, FSM gates, strict
resource resolution, and tool policy remain the safety boundary.
Session continuity context follows the same boundary: Pulse may provide
neutral recent-resource facts and explicit resource addressing facts, but it
must not use prompt-keyword or pronoun heuristics to rewrite a user message as
targeted, inject log-routing instructions, or tell the model which context is
the answer. Pre-model context prefetch may only use structured resource
mentions explicitly selected by the operator; it must not scan plain chat text
for resource names, infer unresolved @name references, or inject lookup
results before the selected model chooses whether to use tools.
Legacy remediation memory follows the same boundary. Pulse may provide
resource-scoped prior action history as neutral context, but it must not
keyword-match the current problem against old fixes, label those fixes as
successful matches for the current issue, or use remediation memory to
recommend a command before the selected model has reasoned over current
evidence.
The same runtime ownership now includes the customer-facing AI usage and cost
surface. frontend-modern/src/components/AI/AICostDashboard.tsx is the
canonical AI usage dashboard shell, while
frontend-modern/src/utils/aiCostPresentation.ts owns its shared loading,
empty-state, and range-button presentation contract. Future cost-surface work
must extend those owners instead of reintroducing inline AI usage copy or
dashboard-local segmented-button styling.
The same runtime boundary also owns the shared AI semantic presentation
helpers used across chat, settings, and usage surfaces.
frontend-modern/src/utils/aiProviderPresentation.ts,
frontend-modern/src/utils/aiProviderHealthPresentation.ts,
frontend-modern/src/utils/aiControlLevelPresentation.ts,
frontend-modern/src/utils/aiChatPresentation.ts are the canonical owners
for provider naming, provider health labels, control-level semantics,
chat drawer title/subtitle, launcher title/aria copy, session-menu labeling,
discovery hint framing, chat/session empty states, assistant message and
question-card labels.
Discovery hint framing must follow the Pulse Intelligence settings IA: use the
simple Discovery label, and explain the concrete service context it unlocks,
without promoting workload discovery as another first-class product surface.
Settings and chat surfaces must consume those helpers instead of keeping local
AI wording or model/provider inference branches.
Assistant chat must not render Pulse-authored explore pre-pass cards or
internal workflow-state cards as assistant output. The user-facing stream is
model text, model thinking where supported, model-selected tool calls, governed
approval requests, and model questions; internal runtime telemetry stays out of
the chat transcript. The browser runtime may keep the latest workflow_state
message on the in-flight assistant turn only as drawer status text while waiting
for model content, so provider/session progress is visible without turning
runtime telemetry into transcript content.
Cold-start Assistant chat session creation is also stream-owned. Ordinary first
messages may call /api/ai/chat without a session_id; chat.Service.ExecuteStream
must create or resolve the durable session before provider execution and emit a
first-class session SSE event carrying SessionData{ID: ...}. The browser
chat runtime binds its active session from that stream event, with done and
question session identifiers retained only as compatible terminal/interactive
payloads, rather than issuing a separate /api/ai/sessions preflight before
the first user message. Explicit session-management actions may still create a
session through the session endpoint.
The Assistant drawer's New action is a local blank-conversation reset, not a
backend session creation. It must clear the active transcript, scoped handoff
context, and browser session ID immediately, leave the model selection intact,
and avoid adding empty conversations to the session list; the next submitted
message materializes the durable session through the stream-owned cold-start
contract above.
The drawer must also stay composer-first: when Assistant opens, starts a blank
conversation, or loads an existing session, the textarea is registered with the
shared aiChatStore focus owner and focused without requiring an extra click.
Global shell shortcuts may use that store focus boundary, but drawer-local code
must not fork a second input-focus registry.
The AI transport files are shared with api-contracts, not delegated away to
it. frontend-modern/src/api/ai.ts,
frontend-modern/src/api/patrol.ts,
internal/api/ai_handler.go,
internal/api/ai_handlers.go, and
internal/api/ai_hosted_runtime.go, and
internal/api/ai_intelligence_handlers.go are runtime control surfaces for
the AI product while also remaining canonical payload contract boundaries.
That same AI transport boundary now also defines the narrow Pulse Mobile
runtime compatibility rule: mobile relay credentials are minted with the
dedicated backend-owned relay:mobile:access scope, and only the explicit
route inventory in internal/api/relay_mobile_capability.go may accept that
scope as a compatibility alias alongside legacy ai:chat or ai:execute
mobile tokens. Broader AI runtime surfaces must stay on their canonical AI
scopes instead of treating the mobile relay capability as a general-purpose
AI permission, and any new mobile-compatible AI route must land by extending
that governed backend inventory and proof set in the same slice.
That same shared AI transport boundary now also owns hosted AI bootstrap
retirement. When Pulse Cloud runs in hosted mode and no explicit ai.enc
exists yet, internal/api/ai_hosted_runtime.go, internal/api/ai_handler.go,
and internal/api/ai_handlers.go must return the same unconfigured
BYOK/local-provider default as self-hosted settings instead of deriving a
quickstart-backed managed-model config from hosted billing state. Any
explicitly written AI config remains authoritative, and hosted billing state
must not be converted into quickstart credits or a managed-model runtime.
That same hosted and self-hosted settings boundary must also retire legacy
hosted quickstart model aliases on read and write. Persisted values such as
quickstart:minimax-2.5m are historical implementation detail, not governed
runtime truth, so internal/config/ai.go,
internal/config/persistence.go, and internal/api/ai_handlers.go must clear
them before the runtime, API payloads, or structured logs consume those fields.
That same runtime boundary also owns approval-store lifecycle in
internal/api/ai_handler.go. Settings-driven enablement and restart must be
able to cold-start the direct AI runtime, initialize approval persistence, and
leave /api/ai/approvals ready for mobile and remediation flows even when AI
was disabled at process boot. The approval cleanup loop must follow owned AI
runtime lifetime rather than an HTTP request context, and approval persistence
may fail closed only when AI is actually disabled instead of because runtime
enablement happened after startup.
Pending approval reads from that store must be deterministic across web, mobile
relay, and API consumers: live pending approvals are ordered by soonest expiry,
then highest operational risk, then oldest request time, with approval ID as
the final tie-break so map iteration cannot decide which governed action looks
most urgent.
That same approval boundary also owns approved command execution. When
internal/api/ai_handlers.go, internal/ai/service.go, or
internal/ai/tools/action_audit.go consume a governed approval record, the
runtime must carry that approval identifier into the final
agentexec.ExecuteCommandPayload so the host agent can re-check the shared
command policy locally and fail closed on blocked or still-unapproved commands
instead of treating control-plane approval as an implicit bypass.
Discovery deep scans are the one runtime that does not flow through the
approval boundary. internal/ai/discovery_adapter.go is the only call site
allowed to mark an agentexec.ExecuteCommandPayload as Trusted. The
catalog of probes lives in internal/servicediscovery/commands.go, is
read-only by construction (cat, ps, ss, find under known config
roots) and is wrapped in docker exec, pct exec, or qm guest exec
without ever interpolating caller-supplied strings. Both the server-side
agentexec authorize path and the agent-side hostagent authorize path
must honor that Trusted flag by bypassing the approval requirement,
while still enforcing PolicyBlock. AI tool calls, Patrol fixes, and
Assistant remediation must continue to flow through the governed approval
record path and must never set Trusted on their payloads.
Discovery command-backed scans are additionally gated by the operator's
Discovery setting, not by the mere presence of a service-discovery store,
connected agent, or command-capable agent token. discovery_enabled=false
must fail closed for background sweeps, /api/discovery/run, forced
single-resource discovery, and pulse_discovery refreshes before any
DeepScanner command dispatch. discovery_enabled=true with
discovery_interval_hours: 0 is the only manual-command-scan mode: recurring
scans stay stopped, but explicit admin-triggered refreshes may use the
hardcoded trusted catalog.
The value boundary for keeping Discovery is observed workload context:
Assistant and Patrol may consume normalized service name, version, endpoint,
port, config path, data path, log path, bind-mount, confidence, and user-note
fields through pulse_discovery or scoped prefetch. Raw command output remains
debug/admin material and must not become the primary Assistant context. When a
Discovery record includes a suggested web URL, the tool response and prefetch
summary may include that URL as observed context, but it must be treated as a
candidate rather than an operator-approved management URL.
The same action-audit boundary now also requires persisted action records to
carry a normalized plan and preflight: action id, request id, capability,
approval policy, dry-run availability, safety checks, verification steps, and
timestamps are normalized before persistence by the unified-resource store, so
runtime callers cannot publish an execution audit that skipped the canonical
planning contract.
Patrol investigation-fix approvals must use that same action-audit boundary:
when the orchestrator queues a fix approval, internal/api/ai_handlers.go must
attach a governed action plan, seed the shared action-audit store as planned
and pending with pulse_patrol as the requester/actor, and leave later
execution or approval decisions to the governed action/approval paths instead
of creating Patrol-only execution context or collapsing Patrol proposals into
generic Assistant-origin actions. The approval record itself must also persist
and expose that requester identity so /api/ai/approvals and Assistant
handoffs preserve Patrol provenance before later action-audit hydration refreshes
the current action state. Backend chat refresh of a Patrol finding handoff must
hydrate the same requester identity directly from the live approval record, so
Assistant does not depend on browser-authored metadata to distinguish
Patrol-origin proposals from generic Assistant actions. Rejected Patrol
investigation-fix approvals must also enter that shared decision lifecycle:
/api/ai/approvals/{id}/deny records a rejected unified action-audit decision
when the approval has a governed action plan and moves the owning Patrol
finding to fix_rejected, so a declined fix remains a visible governed loop
outcome rather than disappearing when the pending approval leaves the queue.
The same ownership includes the Pulse query tool schema under
internal/ai/tools/: topology-query input names must stay canonical inside
the AI runtime itself, so new tool arguments such as max_proxmox_nodes
cannot reintroduce parallel legacy aliases once the backend query contract is
renamed.
That same AI tool ownership also governs pulse_read action="exec" safety.
internal/ai/tools/tools_query.go and internal/ai/tools/tools_read.go must
fail closed on unknown commands: the shared read path may execute only commands
that are known read-only by construction or proven read-only by an explicit
content inspector. The runtime must not preserve a model-trusted fallback for
unknown binaries, custom scripts, downloads, shells, or dual-use interpreters
such as python, node, ruby, perl, bash, or sh, because those
surfaces can mutate state even when invoked in non-interactive forms. Wrapper
and inspection-shaped commands must inherit the same fail-closed boundary:
timeout may only bound an inner command that independently classifies as
read-only, env with a utility is executable and therefore blocked, find
must reject write or exec actions, awk and sed must not regain read-only
status through direct invocation or pipes, wget is read-only only for spider
checks, and curl must reject request-body, mutation-method, config, cookie
jar, upload, and file-output forms while preserving ordinary HTTP(S) probes.
That same AI tool ownership now also includes canonical resource-native
control. internal/ai/tools/executor.go,
internal/ai/tools/tools_control.go, and internal/api/router.go must keep
API-backed control actions such as TrueNAS app start/stop/restart on the
shared pulse_control tool with type="resource" and native audited
execution, instead of adding provider-local control tools or bypassing the
shared approval and policy model.
That same AI tool ownership now also includes canonical resource-native
diagnostics. internal/ai/tools/tools_read.go,
internal/ai/tools/executor.go, and internal/api/router.go must keep
API-backed app log reads such as TrueNAS app-container logs on the shared
pulse_read tool with action="logs" and resource_id=<canonical app>
instead of requiring target_host for non-agent platforms or adding a
provider-local log-read tool.
That same AI tool ownership now also includes canonical resource-native
configuration reads. internal/ai/tools/tools_query.go,
internal/ai/tools/executor.go, and internal/api/router.go must keep
API-backed app configuration reads such as TrueNAS app-container runtime
shape on the shared pulse_query tool with action="config" and
resource_id=<canonical app> instead of forcing those resources through the
guest-config shim or adding a provider-local config tool.
That bounded tool set is the current Assistant floor for TrueNAS. Supported
now means read-side app logs/config and native app start/stop/restart on
canonical app-container resources through the shared pulse_read,
pulse_query, and pulse_control tools. Pulse does not promise a blanket
TrueNAS admin plane, host command execution on API-backed systems without the
unified agent, or provider-local AI tools outside the shared action-governed
runtime contract.
That same platform-claim boundary now also covers the admitted VMware vSphere
direction. The phase-1 Assistant floor is
read-only access to canonical VMware-backed agent, vm, and storage
resources through the shared read and query paths only. The AI runtime must
not add VMware-local tools or action verbs for VM power, snapshot lifecycle,
guest operations, host maintenance, or cluster administration before the
governed action surface expands.
That same VMware AI rule now also includes capability exposure. Even if
runtime code can identify VMware-backed actions through upstream APIs,
canonical resource capabilities and tool routing must stay read-only in phase
1: shared pulse_read and pulse_query may expose VMware-backed context, but
pulse_control must not grow VMware verbs and VMware-backed resources must not
advertise action metadata that implies a supported VMware admin plane.
That same capability boundary also governs resolved-context enforcement inside
internal/ai/chat/context_prefetch.go, internal/ai/tools/tools_query.go, and
internal/ai/tools/tools_control.go. Once the shared runtime has resolved a
canonical VMware-backed agent, vm, or storage, Assistant summaries may
not emit pulse_control instructions for it. Phase-1 VMware host and
datastore summaries without discovery must direct pulse_query or
pulse_read only, VMware guest summaries must stay explicitly read-only, shared
resource registrations must stay limited to read-side actions, and any
attempted pulse_control restart/stop/shutdown path must fail as a read-only
denial instead of falling through to legacy guest resolution or provider-local
control assumptions.
That same boundary also governs shared Assistant wording in
internal/ai/chat/service.go and internal/ai/tools/tools_control.go: the
base system prompt and pulse_control schema/description must not claim that a
generic vm or system-container is controllable. Shared AI text must describe
control as capability-gated and explicitly allow read-only platform variants
such as VMware phase-1 guests.
That same VMware AI rule also includes the investigation path. Alarm, health,
event, task, metrics-history, and snapshot-tree context for VMware-backed
resources must stay reachable through those same shared read/query surfaces
and canonical resource links rather than through a VMware-only AI tool or
provider-local incident adapter.
That same shared read/query rule also governs AI prompt hints and prefetch
summaries in internal/ai/chat/service.go and
internal/ai/chat/context_prefetch.go: API-backed read-only resources such as
VMware-backed agent / vm / storage and TrueNAS-backed host/storage
resources must not inherit synthetic target_host log-routing hints from
agent-routed platforms. Shared AI context should carry canonical
resource_id guidance for those resources, and pulse_read action=logs may
only be suggested when the runtime has an explicit native resource read path
such as supported TrueNAS app-container logs.
If a caller still targets pulse_read action=logs with resource_id for a
resource that lacks that native log path, the shared tool boundary must fail
as a structured blocked response with a governed recovery hint toward the
correct shared path, such as pulse_query action=get for API-backed read-only
resources or target_host plus container for agent-routed app logs.
When that recovery path is safe to execute deterministically, the blocked
response should also carry a structured recovery tool call so the shared
agentic loop can retry through the correct shared tool and arguments instead
of assuming every recovery is a command rewrite on the original tool.
That same VMware AI rule also now includes mention resolution. Frontend
Assistant mention payloads for VMware-backed agent, vm, storage, and
canonical app-container resources must preserve the shared unified resource
ID coming from /api/resources, and backend prefetch/runtime code must
resolve those mentions through canonical read-state lookups rather than
reconstructing provider-local IDs in the UI or adding VMware-only read routes
under /api/vmware/* for Assistant context.
That same AI tool ownership also applies to recovery-backed storage reads.
When internal/ai/tools/adapters.go returns recovery points with malformed
persisted metadata omitted at the shared recovery-store boundary, the storage
tool runtime in internal/ai/tools/tools_storage.go must still keep snapshot
and backup-task results visible by preferring canonical point fields such as
display.clusterLabel, display.nodeHostLabel, display.entityIdLabel,
display.itemType, and point outcome before falling back to raw details.
That availability contract also applies when recovery points are the only storage data source.
internal/ai/tools/executor.go must keep pulse_storage exposed whenever a
RecoveryPointsProvider is configured, so tenant and self-hosted Chat surfaces do not lose
recovery-backed snapshot and backup-task reads just because backup/read-state adapters are absent.
Tenant-scoped AI services must now also follow canonical runtime ownership:
Patrol may initialize and operate from tenant ReadState and unified-resource
providers without requiring a tenant snapshot-provider bridge, and
internal/api/ai_handlers.go must not mint tenant-local StateSnapshot
adapters purely to satisfy Patrol when canonical tenant read-state is already
available.
That same AI ownership also extends to persisted runtime state under
internal/config/persistence.go: AI findings, usage history, patrol run
history, and chat sessions must not keep legacy plaintext files on the runtime
primary path once the process can read them. Plaintext AI persistence files may
only serve as migration input and must be rewritten immediately into
encrypted-at-rest storage on load.
That same Patrol runtime ownership also governs Patrol run-summary taxonomy.
internal/ai/ must keep API-backed TrueNAS systems distinct from unified-agent
hosts in runtime counts, triage summaries, and persisted Patrol run history
instead of collapsing both surfaces back into hosts_checked or generic
agent resource wording.
That same config-persistence boundary also owns fixed runtime file paths: the
resolved data directory must be normalized once and fixed AI/runtime filenames
must rejoin through the shared storage-path helper instead of raw
filepath.Join(dataDir, "...") construction.
That same persistence boundary also governs AI memory package storage roots:
fixed store files such as change history, incident memory, and remediation
history must resolve through normalized owned data directories and fixed
storage-leaf joins instead of raw filepath.Join(dataDir, ...) paths.
The same migration-only rule applies to guest knowledge under
internal/ai/knowledge/: legacy .json knowledge files and plaintext .enc
knowledge files may only serve as migration input, and the knowledge store
must rewrite canonical encrypted-at-rest storage immediately on load instead
of leaving guest knowledge plaintext on disk until a future note update.
That same knowledge-store boundary also governs directory scans: when the store
rejoins discovered knowledge files for reads, it must route those already-owned
leaves back through the shared storage-path helper instead of rebuilding raw
filepath.Join(dataDir, entry.Name()) paths.
Chat-session and guest-knowledge persistence now also keep canonical on-disk
names opaque and machine-owned. Legacy identifier-derived filenames may be
discovered only by inspecting already-owned files for embedded record IDs, and
the next successful write must rewrite them to hashed canonical paths instead
of preserving user-controlled identifiers as filesystem path segments.
That trust boundary also applies when the store is constructed: if the
knowledge store cannot initialize encryption, construction must fail closed
instead of silently creating a plaintext-at-rest runtime store.
Unified-resource-backed AI context now also consumes the canonical
policy-aware metadata contract. The AI runtime may summarize governed resource
policy counts for context, and it must switch to aiSafeSummary when a
resource is marked local-only instead of leaking raw resource names or local
identifiers for restricted resources through ad hoc context formatting.
That governed context should also surface the canonical routing posture and
redaction hints that were derived from the shared policy model, so prompts
reflect the same sensitivity, routing, and scrub decisions that the runtime
uses for export boundaries instead of rebuilding privacy posture locally.
That governed posture block and its export-routing inputs now also flow through
the dedicated internal/ai/resource_context_policy_model.go owner, so
resource_context.go stays on AI context composition instead of duplicating
policy redaction sections or recomputing export metadata inline.
That same ownership now includes the canonical policy-posture summary object
itself: resource_context.go must compute the shared
unifiedresources.SummarizePolicyPosture(...) result exactly once per unified
context build and pass that summary into
buildUnifiedResourcePolicyContext(...), instead of letting downstream AI
context helpers silently rebuild posture counts from the raw resource slice.
The same shared policy presenter also owns the routing-scope labels used in
the AI-facing policy surfaces, so the policy wording stays canonical instead
of being rendered inline by the consumer.
That same policy boundary now applies to chat structured-mention prefetch and
resource-summary formatting: mention resolution must consume canonical
unified-resource policy metadata, skip discovery fan-out when governed
redaction already blocks cloud-safe raw context, and withhold routing
coordinates, bind-mount paths, hostnames, and discovery file paths whenever
resource policy marks those identifiers as redacted.
The governed mention formatter must also render the policy line and redaction
list through the shared unified-resource policy presentation helper so the
chat prefetch path stays aligned with the same canonical sensitivity, routing,
and redaction labels used by the AI summary and resource drawer.
The decision to show that governed mention block now comes from the shared
unified-resource policy helper as well, so the local gate stays aligned with
the same routing and redaction rules as the rendered summary itself.
The governed mention preamble and footer text now also come from the shared
policy presenter, so the warning copy around the block does not drift from the
canonical policy wording.
The complete governed mention block is also assembled by the shared policy
presenter, so chat prefetch only decides when to render it and never rebuilds
the summary layout locally.
The chat prefetch path now also calls the shared governed-summary predicate
directly at each mention site, so it no longer carries a local wrapper around
the canonical policy decision or a separate mention-summary trim helper.
Structured mention resolution also uses the shared AI tools discovery
canonicalization helpers now, so chat prefetch and discovery responses agree
on resource-type and target-ID formatting instead of maintaining chat-local
copies.
The chat mention picker now also carries the canonical preferred resource
label as label through the structured mention payload, and the insertion
path uses that same label for prompt text and cursor placement, so mention
search, selection, and submission do not depend on a raw displayName field
fork.
Structured app-container mentions must now use canonical unified-resource
identity (app-container:<host>:<provider_uid>) instead of a Docker-transport
ID. Frontend mention pickers should emit that canonical ID for every
app-container, including API-backed platforms such as TrueNAS, while backend
structured-mention resolution may continue to accept legacy docker:...
mentions only as a compatibility path.
Compatibility-only top-level TrueNAS mention types must also collapse to the
canonical agent host type at that same handler boundary, so the AI runtime
does not carry a parallel raw truenas mention contract once transport input
has been normalized.
That same compatibility-collapse rule also applies to alert, finding, and
Patrol scope payloads. API-backed TrueNAS systems may still keep truenas
platform metadata and separate run-history coverage counts, but AI resource
type fields must normalize to canonical agent once they cross the governed
runtime boundary.
The same governed-context rule also applies to the main unified AI resource
overview: infrastructure, workload, alert-label, and top-consumer summaries
must not leak raw resource names, cluster labels, IP addresses, or unresolved
topology identifiers once canonical resource policy marks aliases, hostnames,
platform IDs, or addresses as redacted. Sensitive resources should remain
useful through aiSafeSummary and explicit redaction markers rather than
falling back to raw local identifiers in list or summary sections.
That same governed policy boundary now extends through AI tool payloads and
chat-memory extraction. Resource-bearing pulse_query results must carry the
canonical policy and ai_safe_summary fields derived from unified resources,
and deterministic knowledge extraction must prefer those governed summaries
when policy redaction covers aliases, hostnames, or platform IDs instead of
persisting raw resource labels into cached AI facts.
That same pulse_query boundary now also owns canonical resource coverage for
API-backed platforms such as TrueNAS. The runtime must expose canonical
agent, app-container, storage, and physical-disk resource views
through the shared unified-resource model instead of falling back to Proxmox-
or Docker-local enumerations when a platform projects onto canonical host,
storage, disk, or workload contracts. Compatibility aliases such as
system and storage-pool may still be accepted at the pulse_query
boundary, but the governed runtime contract is the canonical agent /
storage read path and the resolved-context registration emitted from it.
That same runtime contract applies to resource-native diagnostics. When
resolved context points at an API-backed canonical app-container such as a
TrueNAS app, chat/runtime prompt hints and tool execution must route log reads
through resource_id on pulse_read rather than inventing agent-host hints
for platforms that are not reached through the unified agent.
Unified AI context should follow the same rule: storage summaries may mention
canonical storage pools and physical disks that need attention, but must not
mislabel lower-topology storage resources such as TrueNAS datasets as
top-level pools.
That same requirement includes pulse_query action=config: guest-config
payloads must carry canonical resource policy metadata, and config-fact
extraction must not persist raw guest hostnames when governed redaction covers
hostname or platform identity fields. The same action=config contract now
also applies to API-backed canonical app-container resources such as
TrueNAS apps: runtime routing must resolve the shared resource identity first
and then read native config through the owned provider path rather than
falling back to guest semantics.
Outbound model-bound context exports now also belong to this runtime
boundary. When the AI service assembles unified-resource context for a model
request, it must record a durable export audit with the active destination
model and governed redaction decision instead of treating the prompt boundary
as a transient formatting step.
That export decision must come from the shared unified-resource privacy
helpers, so sensitivity floors and redaction-triggered routing stay aligned
with the canonical policy contract instead of being recomputed in AI-local
code.
The export audit should also record canonical human-readable redaction labels
from the shared policy presentation helper, so the audit trail and the
resource-context surfaces speak the same governed redaction language instead
of reformatting hint names locally.
The canonical AI-safe summary builder also owns the sensitive and
restricted suffix phrases, so downstream AI consumers should treat those
ending fragments as shared policy output instead of inventing their own
wording.
The same AI runtime boundary now also consumes the canonical unified-resource
timeline when it assembles rich resource or incident context. Recent-change
context should come from the shared resource store first so AI prompts reflect
the same change record that powers the resource API, with patrol-local change
detectors only serving as fallback coverage when the canonical store is not
available. When that patrol-local fallback is used, it must render through the
shared memory change presentation helper so the same heading, scope prefix, and
change-type labels are reused instead of being rebuilt ad hoc in AI-local code.
internal/ai/memory/incidents.go is therefore an alert-scoped investigation
projection only: it may retain notes, analysis, command executions, runbooks,
and alert lifecycle breadcrumbs for one incident, but it must not become a
parallel source of truth for durable backend history that already belongs to
internal/unifiedresources/.
When canonical resource history is available, the incident read path must also
project alert lifecycle and remediation entries back out of the unified-resource
timeline instead of reading those durable facts only from AI memory. AI memory
may retain annotation-only entries such as notes and analysis, but the live
incident timeline shown to handlers, prompts, and operators should read as one
projection over canonical resource history plus investigation-local annotations.
That read-side projection must also discard incident-local derived lifecycle
state when canonical history is present: acknowledgement, resolution, and
command or runbook entries in internal/ai/memory/incidents.go may still
exist as compatibility-era shell state for segmentation and fallback, but the
projected incident returned to runtime consumers must rebuild those fields from
canonical resource changes and preserve only annotation-local entries such as
analysis and notes.
The remaining shell should stay as narrow as possible: alert occurrence
boundaries and annotation anchors may remain private implementation state, but
public incident status, acknowledgement, and remediation entries should be
treated as read-model output rebuilt from canonical history whenever that
history exists.
That boundary should also be visible in code shape: the persisted incident
shell used by internal/ai/memory/incidents.go should stay a private storage
model for occurrence segmentation and annotations, while the exported
Incident type remains the public/projected read model returned to handlers
and operators.
The AI correlation root-cause engine also consumes the canonical unified-
resource relationship model directly, so cross-resource reasoning stays aligned
with the same relationship edges that back the resource API instead of
maintaining a parallel relationship vocabulary inside AI correlation.
The canonical relationship-summary helper also feeds resource change records,
so AI timeline prompts read the same relationship wording and edge labels that
the unified-resource contract emits instead of building another summary shape
in AI-local code.
The same shared change presenter also owns the resource state, restart,
incident, and config summary fragments used by change emission, so the AI
timeline prompt can reuse the canonical from/to wording before it formats the
markdown section itself.
The Patrol-backed correlation endpoint, resource-intelligence payload, and
seed prompt correlations now flow through the shared AI intelligence facade
first, so the detector remains an implementation detail behind one canonical
correlation access path instead of being routed directly by handlers or prompt
builders.
AI-facing policy metadata must also be cloned through the shared unified-
resource policy helper so chat and tools consumers do not maintain their own
policy copy logic. Chat mention prefetch now calls that shared helper directly
at each resolved mention site rather than going through an AI-local wrapper.
AI resource and intelligence consumers now also refresh canonical identity and
policy through the shared unified-resource metadata helper, so the AI runtime
no longer keeps its own slice-level normalization shim for the same
composition.
Chat knowledge extraction and resource-context rendering now also consume the
shared unified-resource label helpers directly, so governed labels and
redacted values stay consistent without AI-local presentation shims.
Those same paths also use the shared resource display-name helper, so the
name-or-ID fallback stays aligned across chat extraction, resource context,
and unified adapter presentation.
The unified resource context's IP summaries now also route through the shared
policy redaction helper, so the local "IPs" line follows the same governed
redaction decision and label vocabulary as the rest of the policy-aware
resource presentation layer. Cluster labels for AI resource context now also
come from the shared unified-resource presentation helper, so the same policy
rules govern cluster names and IP summaries instead of leaving the fallback
logic in the AI package.
The policy-posture aggregate itself now also comes from
internal/unifiedresources/policy_posture.go, so AI summaries and resource
context reuse the same canonical sensitivity, routing, and redaction counts
instead of collecting governance posture in an AI-local helper.
That shared presentation layer also owns the elapsed-time and "ago" wording
utilities, so the same "time ago" phrasing stays consistent across resource,
incident, and fallback memory summaries instead of being reformatted
independently.
The canonical resource-change kind, source type, and source adapter labels
now also come from the shared change presentation helper, so the resource
summary card and drawer history use the same badge vocabulary instead of
hardcoding their own labels.
Action-plan stale-plan protection now keys the durable audit payload on the
canonical resourceVersion, policyVersion, and planHash fields only,
so the audit record stays on the minimal deterministic contract instead of
carrying extra versioning for relationship topology.
Resource-only incident context should follow the same rule: if an alert
timeline is absent, the incident prompt path should fall back to the canonical
unified-resource timeline rather than depending only on patrol-local change
memory.
When both an alert identifier and a canonical resource ID are known, the prompt
path should include both surfaces in source-precedence order: alert-scoped
incident memory first, canonical resource timeline second.
The same runtime boundary now also owns durable action execution auditing.
internal/ai/chat/service.go initializes the unified-resource audit store on
startup. Governed API action execution must enter through
POST /api/actions/{id}/execute, which records executing before invoking the
registered executor and records the terminal completed or failed result
afterward; missing executors must fail closed without mutating the approved
audit record. Existing write-action tool paths under internal/ai/tools/
must keep their persisted lifecycle and result records aligned with that same
unified-resource action state machine: approval decisions must use the
canonical action decision transition, execution starts must use
BeginActionExecution plus RecordActionExecutionStart, and terminal tool
results must use CompleteActionExecution plus
RecordActionExecutionResult rather than inventing AI-local execution states.
AI incident handling must now also write durable resource-history facts
through the canonical unified-resource change store when a concrete resource
target is known. Command executions and runbook executions triggered during an
alert investigation may remain visible inside internal/ai/memory/incidents.go
as operator-facing incident projection entries, but the durable backend truth
for those events now belongs to canonical ResourceChange kinds such as
command_executed and runbook_executed, keyed by canonical resource ID and
linked back to the alert through metadata instead of being stored only in AI
memory.
The patrol-local memory.ChangeDetector.GetChangesSummary path now also
delegates to the shared memory recent-change presentation helper, so any
future fallback summary entry point inherits the same heading, resource
prefixing, and change-type labels without re-implementing the markdown shape.
Those unified-resource action and export audit records are now also exposed
through the enterprise audit read surface so operators can inspect the
execution trail without reaching into storage internals.
AI resource and incident context now also surfaces a canonical relationship
section from unified-resource relationships, so relationship wording and edge
provenance stay aligned with the same shared resource model instead of being
reconstructed from the drawer or prompt helpers.
That relationship section is now rendered by the shared
internal/unifiedresources.FormatResourceRelationshipContext helper, so the
service layer only resolves the canonical resource and does not rebuild the
section format locally.
The canonical recent-change sentence formatting also lives in
internal/unifiedresources.FormatResourceChangeSummary, so AI runtime prompt
sections and Patrol seed context reuse the same change wording instead of
keeping another lane-local formatter.
The confidence percentage wording used by the drawer's change timeline rows
also flows through a shared frontend formatter, so the same 50%-style
labels stay consistent across timeline surfaces instead of being re-derived
in the component.
The remaining fallback token humanization used by those same timeline and
drawer surfaces also flows through one shared frontend helper, so the
title-casing and underscore cleanup used for change and drawer labels stay
centralized instead of being reimplemented locally.
The canonical recent-change section wrapper also lives in
internal/unifiedresources.FormatResourceRecentChangesContext, so the AI
summary and resource-specific context share the same heading and prefix rules
instead of rebuilding that section layout locally.
The canonical memory conversion helpers also live in
internal/ai/memory/presentation.go, so the Patrol fallback feed and the
AI summary path translate between unified-resource changes and memory.Change
through one shared adapter boundary instead of keeping local shims.
The related-resource correlation section now also comes from the shared
correlation formatter in internal/ai/correlation, so resource chat and
incident prompts reuse the same learned-edge wording instead of rebuilding a
second patrol-local bullet format.
The Patrol intelligence page now also fetches the learned correlation list
from the canonical AI correlations endpoint, so the global AI surface and the
resource drawer both expose the same learned edge evidence instead of only
showing a correlation count. The same page and drawer now render that list
through the shared frontend-modern/src/components/Infrastructure/ResourceCorrelationSummary.tsx
card, so the learned-correlation layout and edge wording stay aligned across
both surfaces. That shared card also owns the correlation ordering and
truncation rule, so callers pass raw learned edges instead of page-specific
top-N slices.
Assistant finding handoffs now also receive a model-only finding briefing
derived from the current unified finding and structured Patrol investigation
record before the lower-level finding context. That briefing must summarize the
finding, resource, priority, current recency facts, bounded evidence and
verification summaries, investigation confidence, latest lifecycle event, and
governed action artifact metadata as factual model context without generating
Pulse-authored attention, operator-decision, or remediation guidance, while
leaving detailed lifecycle history, current
resource-state, timeline, related-finding, and action-audit
hydration in the existing canonical AI runtime handoff builders. Related
root-cause and
correlated finding records may be summarized from current unified finding state,
including their recency and latest lifecycle facts, and may seed their own
handoff resources for canonical policy, state, topology, and timeline
hydration. That related context is explanation and review context only, not
approval or execution authority. Detailed lifecycle events are
likewise current Patrol review context only. The assembled briefing, lifecycle,
and related context are policy-sanitized by the chat handoff runtime before
prompt injection, so governed resource names, IDs, aliases, nodes, paths, and
addresses are redacted or represented through the canonical AI-safe summary
instead of leaking through product prose.
The same page and drawer now also render their recent-change timeline through
the shared frontend-modern/src/components/Infrastructure/ResourceChangeSummary.tsx
card, so the canonical recent-change layout and relative-time wording stay
aligned across both surfaces instead of being rebuilt as page-local feeds.
The Patrol intelligence seed context now also prefers the canonical
unified-resource timeline before falling back to the patrol-local change
detector, so deterministic patrol context and resource detail context share
the same change source of truth.
The unified intelligence summary should follow the same rule when it counts
recent activity, so the shared AI summary and the Patrol seed context stay
aligned with the canonical timeline.
The same unified intelligence summary now also surfaces a canonical policy
posture snapshot derived from unified resources, so sensitivity, routing, and
redaction counts stay aligned with the governed resource model that the
runtime uses for prompt export and context rendering.
That posture snapshot must render redaction labels through the canonical
unified-resource hint order, not alphabetically, so the AI summary, drawer,
and any future policy surfaces all present the same redaction precedence.
Its sensitivity and routing counts must also follow the canonical
unified-resource order and shared human-readable count summaries, so both the
backend summary and the frontend policy card stay aligned on the same
presentation sequence.
The unified AI resource data-governance block must also use the shared
unified-resource redaction-label helper directly, so the same canonical
policy labels back both the posture summary and the governed prompt context
without an AI-local wrapper.
The governed query-fact and resource-context paths must also use the shared
unified-resource policy helpers for the aiSafeSummary decision and
redaction predicates, so the same local-only and redaction rules are applied
consistently instead of being reimplemented in chat-local helpers.
The frontend unified-resource hook now trusts backend canonical policy and
aiSafeSummary values directly, so the canonical summary and policy posture
stay aligned with the same resource-policy boundary that governs
policy-aware routing and redaction without any frontend-local re-normalization.
The resource detail drawer now also resolves the visible AI-safe summary
through the same shared policy helper, so governed resources still show the
canonical redacted label if the backend summary is missing instead of
silently dropping the summary block.
The per-resource intelligence payload returned from
/api/ai/intelligence?resource_id=... now carries recent changes,
dependencies, dependents, correlations, and knowledge only; policy posture
stays on the system-wide intelligence summary and the Patrol governance card
instead of riding the resource-detail payload.
That same resource-intelligence payload also carries dependency and
dependent correlation context from unified-resource correlations, so the drawer
can show canonical correlation relationships without reconstructing them from the
relationship timeline alone.
The shared AI resource and infrastructure prompt contexts should also surface
the same canonical recent changes section before any patrol-local fallback so
the model sees the same timeline entries that power the resource API and
intelligence summary counts.
The /api/ai/intelligence/changes endpoint should also route through the
canonical unified-intelligence recent-change accessor before any
patrol-local detector fallback, so the API surface reads the same unified
timeline source that powers the summary payload.
Retired dashboard Pulse Brief context follows the same monitoring-first AI
boundary in negative space: frontend-modern/src/features/dashboardOverview/
and the Dashboard route must not be restored just to create an Assistant-ready
operator paragraph. Future overview or brief surfaces need a governed product
owner first, must pass fact-bound structured context from owning Infrastructure,
Workloads, Patrol, storage, recovery, and alert summaries, and must not let an
unbounded prompt become a route's source of truth.
Future route-to-Assistant handoffs must also keep their execution mode scoped
to the request. When an overview brief opens Assistant, the drawer may prefill
only governed prompt/context data, but the submitted chat request must set
autonomous_mode:false, preserve the operator's persistent Assistant
control-level setting, and disclose the temporary approval-required mode in
the drawer instead of showing the generic Autonomous warning.
Scoped Assistant handoffs that originate in owned product surfaces may also
send bounded handoff_context text, structured handoff_resources, and safe
structured handoff_actions through frontend-modern/src/api/aiChat.ts and
/api/ai/chat. That context is model-only session metadata, not saved
user-authored message text, and the backend must clamp the exchange to
approval-required mode whenever such scoped handoff context, resources, or
action references are present. Patrol finding IDs remain stricter: when
finding_id resolves, backend-refreshed durable Patrol context remains the
canonical authority; the handler may merge only a recognized same-finding
Patrol product handoff section as secondary model-only briefing, and it must
drop mismatched resource/action references plus raw command payload lines.
Direct alert-investigation runtime handoffs follow the same rule even when
they bypass the chat drawer. /api/ai/investigate-alert must set
ai.ExecuteRequest.AutonomousMode to false plus
ai.ExecuteRequest.RequireCommandApproval to true, and
internal/ai/alert_provider.go must frame diagnostics as approval-bound
operator actions rather than instructing the model to execute commands because
they appear safe.
Those backend AI and Patrol change summaries should derive their canonical
labels and provenance fragments from
internal/unifiedresources/change_presentation.go, so the resource-model
semantics are shared before any surface-specific markdown styling is applied.
The patrol-local recent-change fallback itself should derive its section layout
and change labels from internal/ai/memory/presentation.go, so detector-based
fallbacks stay consistent across AI runtime entry points when the canonical
resource timeline is unavailable.
The per-resource intelligence payload returned from
/api/ai/intelligence?resource_id=... should also include the canonical
recent_changes history so UI and API consumers can read the same timeline
slice that the prompt context uses.
The system-wide /api/ai/intelligence summary should also surface the same
canonical recent-change slice, alongside the count, so the aggregate payload
and the prompt context stay aligned on the same shared timeline source.
The frontend Patrol intelligence page now also consumes that canonical
summary payload directly through the shared AI client and store, so the
visible summary card stays aligned with the same recent-change slice that the
runtime and API contracts expose.
The Patrol runtime now also exports a canonical runtime_state alongside
blocked_reason in the Patrol status payload, so provider-availability and any
legacy managed-credit block conditions remain part of the governed runtime
contract instead of being inferred later from the last successful patrol
summary.
When missing provider configuration blocks Patrol, blocked_reason must point
to Pulse Intelligence > Provider & Models settings and tool-capable Patrol model
selection.
That runtime-state contract must be derived from live Patrol runtime inputs,
not only from the last failed run attempt, and the backend must clear any stale
managed-credit block once a provider or local model configuration returns.
The same runtime contract now also governs when the system-wide Patrol health
summary is allowed to read as healthy. internal/ai/intelligence.go must not
derive Health A or 100/100 from "no active findings" alone when recent
Patrol evidence is limited to alert-scoped runs or includes recent Patrol run
errors; the summary must degrade and explain that overall infrastructure health
is not fully verified until a recent successful full Patrol run exists.
The backend may keep precise full-run, scoped-run, and verification terms in
wire fields and internal decisions, but browser-facing summary and handoff copy
emitted from internal/ai should translate that precision into check language:
recent broad checks, targeted checks, follow-up checks, current issues, and
verified outcomes. Runtime explanations must not surface activation-loop,
proof-strip, or scoped/full-run jargon as the ordinary operator vocabulary.
That coverage explanation must also stay faithful to the actual recent run
shape. When the most recent verification evidence includes a full Patrol run
that ended with errors, the health summary must say that a recent full patrol
errored rather than claiming recent activity was limited to scoped runs.
The Patrol status payload must keep that same scope distinction explicit in its
own recency fields. last_patrol_at is reserved for the most recent completed
full Patrol run, while scoped runs and fix-verification checks advance
last_activity_at without pretending a full verification sweep just happened.
That same runtime contract also owns scoped trigger source policy. Alert- and
anomaly-triggered Patrol work are independent runtime gates; the canonical AI
settings model must preserve them separately, and runtime status must expose
which scoped sources are enabled plus whether queued scoped work or busy-mode
acceleration is currently active. Runtime policy blocks, including the local
development background-automation guard, must be represented as an explicit
event-trigger block on the Patrol trigger status rather than by rewriting the
operator's alert/anomaly trigger preferences to disabled.
That same runtime boundary also owns which Patrol work counts toward
full-patrol cadence gates. Community-tier or other full-run limits must key
off completed full sweeps only; recent scoped or verification activity may
advance last_activity_at, but it must not block a manual full Patrol request
as if a scheduled estate-wide sweep already happened.
The manual Patrol route carries that same scoped/full distinction. POST /api/ai/patrol/run (internal/api/ai_handlers.go HandleForcePatrol) accepts
an optional scope body — resource_ids and/or resource_types, optionally with
alert_identifier, alert_type, and context. With a scope it runs a manual
Targeted check through the same scoped engine (TriggerScopedPatrol) and run
record (Type: "scoped") as automatic alert-triggered work, not a new
investigation path; without a body it keeps the legacy fleet-wide Patrol check
behaviour. A manual scoped run must reuse the existing scoped engine rather
than adding a parallel trigger route, must honour the same Patrol readiness gate
as a full run, and must bypass the full-run cadence gate because targeted
checks never consume an operator's manual full-run allowance. The request must
carry resource identity only — no command, prompt, or remediation payload — and
the route must keep requiring admin plus ai:execute scope for both shapes.
The Patrol startup scheduler must preserve that coverage guarantee as well:
internal/ai/patrol_run.go may skip the startup full patrol only when recent
run history already includes a successful full Patrol run, not merely because
some recent scoped alert-triggered run exists.
The Patrol runtime also owns synthetic Patrol service findings canonically.
Provider-credit and provider-auth failures raised against the synthetic
ai-service Patrol resource are runtime conditions, not inventory resources,
so the full-run seed/reconcile path must not auto-resolve them as
Resource no longer exists in infrastructure just because ai-service is not
present in the infrastructure snapshot. Those findings stay active until
Patrol actually succeeds or resolves them for a Patrol-owned reason.
That success boundary includes provider-backed scoped Patrol runs and successful
Patrol tool-call preflights. A successful scoped run proves that Patrol can
currently reach the selected provider/model and complete tool-backed analysis;
a successful preflight with an observed tool call proves the configured
provider/model currently accepts Patrol's tool-call path. Either must clear the
synthetic ai-service runtime failure just as a successful full Patrol run
does, without loosening ordinary scoped finding reconciliation for
infrastructure issues. A soft-warning preflight where the provider responds but
the model does not emit a tool call is not sufficient recovery evidence.
Because those findings represent Patrol blindness rather than operator-triaged
infrastructure noise, the Patrol runtime must also reject manual acknowledge,
snooze, dismiss, resolve, and suppress actions against synthetic ai-service
runtime findings. The canonical recovery path is to correct Patrol provider
configuration in Pulse Intelligence > Provider & Models settings and let Patrol
re-evaluate the runtime condition on the next run.
The shared findings lifecycle must also treat a regressed issue as a new active
occurrence. When a resolved finding reappears, internal/ai/findings.go must
clear any stale acknowledgement timestamp from the prior occurrence instead of
carrying that acknowledgement forward onto the regressed active issue. The
same owner must normalize already-persisted active findings on load when a
stored acknowledgement predates the last recorded regression, then persist the
cleaned state back through the canonical findings store.
AI chat tool-name labels, pending-tool headers, and assistant status copy now
also route through the shared frontend identifier-label helper, so the chat
surfaces do not keep their own underscore-stripping behavior separate from
the rest of the governed presentation helpers.
AI chat stream matching and mention dedupe now route through the shared
frontend chat identifier helper, so tool-name prefix stripping and mention-key
normalization stay aligned across the chat runtime instead of being redefined
inline in the stream processor or container component.
That same provider-stream boundary also owns EOF-safe SSE finalization for
OpenAI-compatible chat streams. Provider reads that return payload bytes with
io.EOF, or close immediately after the final data: frame, must still
process the buffered frame set and route tool-call assembly plus final done
event emission through the same canonical finalizer used for [DONE] instead
of dropping the last chunk or leaving tool calls unfinalized on clean close.
That same provider-transport boundary owns OpenAI-compatible tool protocol
adaptation. Pulse must keep normal tool selection automatic/model-owned for
OpenAI-compatible providers, including direct DeepSeek paths. Text-only
turns reached through loop, budget, or verification gates should omit tools
entirely instead of sending provider-specific tool_choice=none; that transport
setting must not be used as an intent classifier.
Reasoning-backed provider turns that return tool calls with reasoning_content
must preserve that reasoning state on the following tool-result turn when the
provider requires it, so Assistant and Patrol can complete multi-turn tool use
against live BYOK providers.
Readiness classification for the same provider path must be model-aware, not
provider-only. Current official DeepSeek V4 tool-capable models may report
Patrol readiness as ready; legacy DeepSeek aliases may only warn with the
alias-retirement posture and a recommendation to select the current V4 model
IDs; unknown direct DeepSeek model IDs must be not-ready with
model_unavailable; and known reasoning-only families must continue to fail
closed before Patrol work is admitted.
That same browser-owned chat read model must keep target normalization helper-
driven. Assistant shells may still derive legacy VM identifiers or display
labels for read-only targeting, but they must do so through shared helpers and
store context precedence instead of passing component-local resource objects or
duplicating naming fallbacks inline.
That same runtime boundary also owns executor session isolation. Shared AI
runtime services may reuse one canonical executor configuration, but each chat
or Patrol run must clone that executor before attaching resolved-context,
approval-routing, or patrol-finding state so concurrent sessions cannot
overwrite one another's mutable runtime context.
That same Patrol runtime boundary owns Community monitor-mode autonomy saves.
The open-source/free PUT /api/ai/patrol/autonomy adapter may persist
findings-only monitor configuration and the governed investigation budget /
timeout clamps, but it must continue to reject approval, assisted, and
full autonomy with the canonical safe-remediation license response.
The same canonical findings store owns dismissal-reason semantics. The three
dismissed_reason values must remain behaviorally distinct, not copy-only
variants: not_an_issue flips Suppressed=true, expected_behavior
acknowledges without escalation, and will_fix_later is an operator
commitment that populates Finding.RemindAt (default
DefaultWillFixLaterRemindAfter, 7 days). On re-detection, the canonical
store wakes a will_fix_later finding once RemindAt has passed by
clearing the dismissal and emitting a reminded lifecycle event, and the
dismiss_finding LLM tool response must communicate the remind-at date so
Patrol's conversational explanations stay aligned with the persisted
behavior.
The unified-finding mirror in internal/ai/unified/alerts.go also carries
that same RemindAt field so the API surface preserves the will_fix_later
wake-up deadline across the canonical findings store and the read model.
The AddFromAI dedup-merge path must mirror RemindAt onto the existing
record (including clearing it when a remind-at wake or undismiss has
already cleared the dismissal in the canonical store), and the TS API
clients in frontend-modern/src/api/patrol.ts and
frontend-modern/src/api/ai.ts must round-trip the remind_at field
verbatim so the operator surface can preview and badge the deadline.
The same Patrol API client also exposes the operator-driven manual
resolve path. resolveFinding(findingId) in
frontend-modern/src/api/patrol.ts must POST {finding_id} to the
canonical /api/ai/patrol/resolve endpoint owned by
HandleResolveFinding in internal/api/ai_handlers.go, mirroring the
acknowledge / snooze / dismiss client surface so the same Patrol service
contract drives every operator-feedback action.
The unified.UnifiedFinding mirror also carries an explicit
AutoResolved flag alongside ResolvedAt, set by the canonical
Finding.AutoResolved field. The AddFromAI dedup-merge path must
mirror that flag (allowing flips between auto-detected closure and
operator-driven closure as the canonical store transitions), and the
Finding to UnifiedFinding conversion in internal/api/router.go must
copy f.AutoResolved on both the live wire-up callback and the
persistence-recovery resync, so the frontend can honestly attribute who
closed the loop instead of flattening every resolution into a generic
"resolved" state.
The same canonical AI runtime now also owns the report-narrative surface.
internal/ai.Service implements pkg/reporting.Narrator and
pkg/reporting.FindingsProvider directly through report_narrator.go and
report_findings.go so the reporting engine can request an AI-generated
executive summary without depending on AI-internal types. The narrator is a
single-turn, no-tools call that reuses the canonical provider abstraction
already powering Patrol and Assistant; the request sanitizer, model
selection (PatrolModel preferred, falling back to GetChatModel()), cost
budget enforcement (enforceBudget("report_narrative")), and provider
factory must be the same shared seams used by QuickAnalysis, not a
parallel report-only provider stack. The structured report payload sent to
the model is denormalised through buildReportNarratorPayload so reporting
package types do not leak into the prompt surface, and the response is
parsed through parseReportNarratorResponse which tolerates an optional
json code fence the model may emit despite the no-fences instruction.
Severity normalisation maps the model's free-form output back onto the
narrative bullet severity set the renderer understands (ok, info,
warning, critical); unknown values default to info rather than
silently rendering as muted. Both interfaces fail closed: a nil provider,
parse failure, empty narrative, or context cancellation returns an error
so the reporting engine falls back to the deterministic heuristic
narrator. The findings provider filters the patrol findings store by
resource ID and lifecycle overlap with the report window via
findingOverlapsWindow, and truncates to reportFindingsLimit (25)
sorted entries so retrospective summaries stay within a predictable
prompt budget. Reporting is therefore an additive consumer of AI
runtime, not a new ownership boundary, and the narrator/findings
surfaces inherit the same governance the rest of the canonical AI
runtime already enforces.
The same canonical AI runtime now also owns the fleet-level report
narrative through report_fleet_narrator.go. Service implements
pkg/reporting.FleetNarrator with its own use-case label
(report_narrative_fleet) so fleet vs single-resource spend is
distinguishable in the cost ledger, and so the budget gate
(enforceBudget) and dashboard taxonomy can address the two
separately. The fleet payload is denormalised through
buildReportFleetPayload into compact per-resource rows plus a
fleet-wide aggregate so prompt cost scales linearly with fleet
size without exploding token usage. The same fail-closed invariant
holds: nil provider, parse failure, empty narrative, or context
cancellation returns an error so the reporting engine falls back to
HeuristicFleetNarrator. Single-resource report narration is
deliberately not propagated through the multi-report path; a
50-resource fleet report performs exactly one AI call (the fleet
narrator) rather than 51 (one per resource plus a fleet-level
summary).
Both the single-resource and fleet narrator system prompts also encode an explicit detection-boundary invariant: Pulse Patrol is the canonical detection layer, and the report narrators must function as summarizers of Patrol's classified state rather than parallel detectors. The narrator may classify an observation at "warning" or "critical" severity only when it is backed by a Patrol finding, an alert, or a hard-threshold breach in the structured input (cpu max above 90, memory avg above 85, disk avg above 85, failed or high-wear disks, storage pools at 90 percent or more). Patterns it notices in the metric data without that backing may be mentioned at "info" severity but must not be promoted. Recommendations follow the same rule: no remediation for inferred issues that lack a finding, alert, or threshold breach. This keeps the report narrative honestly retrospective on Patrol's work and prevents silent shadow-classification competing with Patrol's detection rules.
The same reporting synthesis layer is now exposed to Pulse
Assistant as a first-class chat tool, pulse_summarize. The tool
wraps the engine's NarrativeFor and FleetNarrativeFor entry
points (single-resource and fleet modes selected by an action
parameter) so an operator can ask "what's been happening with
pve1 this week" or "where should I look across my fleet" and get
a structured retrospective answer in chat rather than having to
generate, download, and read a PDF. The tool is read-only (no
approval gate, no control-level requirement) and returns a JSON
envelope carrying the narrative source, health status, observations
or outliers, recommendations, and provenance disclaimer. v1 always
returns heuristic narrative; the AI narrator wiring through the
chat session is a focused follow-up that adds Narrator,
FleetNarrator, and FindingsProvider plumbing to the executor
configuration so the tool inherits the same per-tenant AI service
the report PDF endpoint already uses. Reporting therefore expands
from an export-shaped feature into a first-class capability
Assistant can compose with — the underlying engine surface stays
unchanged.
That follow-up has now landed. chat.Config carries three optional
fields (ReportNarrator, ReportFleetNarrator,
ReportFindingsProvider) which are threaded through to
tools.ExecutorConfig and stored on PulseToolExecutor. The
pulse_summarize tool reads them when building requests so the
engine sees a populated narrator when the tenant's AI service is
configured. The router installs a SetReportNarratorResolver
closure on the chat handler that mirrors the reporting handler's
pattern: it asks the AISettingsHandler for the per-tenant
ai.Service and, when that service has Enabled=true, returns it
as the implementation for all three roles (Service satisfies
reporting.Narrator, reporting.FleetNarrator, and
reporting.FindingsProvider already). An unconfigured tenant still
sees the heuristic fallback — the tool never errors on missing AI,
matching the report PDF's graceful-degradation posture. AI-narrated
chat synthesis therefore uses the same provider, sanitizer, model
selection, cost ledger (report_narrative / report_narrative_fleet
use-cases), and budget gate the report PDF endpoint already
enforces — there is exactly one canonical synthesis path for both
surfaces.
The same canonical AI runtime now also records user-chat token
usage to the cost ledger. chat.Service.ExecuteStream was a
long-standing gap: the agentic loop accumulated token counts via
stream callbacks and surfaced them in the SSE done envelope, but
nothing on the server side recorded a cost.UsageEvent. Chat is
the bulk of AI token spend, so the operator AI usage dashboard
was understating cost dramatically. recordChatTurnCost now runs
after every loop.ExecuteWithTools return — success or error,
since the operator was billed regardless of whether the loop
produced a clean response. It emits a cost.UsageEvent with
UseCase="chat" in the same shape the rest of the runtime uses.
When a chat turn carries a product-originated finding, resource, handoff,
action, or structured-mention context, the event may also include the coarse
context_scope classifier so privacy-safe telemetry can count governed-context
Assistant collaboration without exporting prompts, session IDs, resource IDs,
finding IDs, command text, or action payloads.
When the same turn includes accepted model-selected governed tool calls, the
agentic loop records only the total count on the resulting cost event. That
count supports Pulse Intelligence adoption telemetry for Assistant
collaboration while keeping tool names, arguments, results, provider call IDs,
transcript content, resource IDs, finding IDs, command text, action payloads,
and session IDs out of persisted/exported telemetry.
The store is threaded through chat.Config.CostStore, wired by
the router from the per-tenant AISettingsHandler.GetAIService
via Service.CostStore(). ExecutePatrolStream deliberately
does NOT record here — its caller (patrol_ai.go) records via
its own helper, so cost is never double-counted on the
patrol-via-chat path.