Update writes every mutable column of agent_runs except checkpoint.
That exclusion is intentional: PGCheckpointer.Save and ClearCheckpoint
are the only paths that touch the column, so a status commit cannot
overwrite an in-flight checkpoint saved between Load and Update.
Surface the rule on the Update method so future readers do not patch
in a checkpoint write thinking it was an oversight.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Result.LastAgent is *Agent, which holds only unexported fields. The
default json.Marshal renders it as an empty object, which is misleading
when persisted alongside the agent run.
Tag LastAgent as json:"-" and add explicit lowercase JSON tags to the
remaining fields so the serialized shape is stable for callers that
read the persisted result column.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Run, RunStreamed, and Resume each shipped both a no-options form and a
mirror *WithOpts form taking variadic RunOption. Variadic parameters
are backward-compatible additions, so the wrappers were dead surface.
Make Run, RunStreamed, and Resume directly variadic and update the two
internal callers.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Documents why the supervisor suite stays sequential (claim is
cross-tenant via LoadNextPendingForUpdateSkipLocked; parallel
supervisors would steal each other's runs) and switches the SIGTERM
subtest to errors.AsType[*exec.ExitError] to match the project
convention enforced elsewhere in this file.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Adds AgentRunSupervisor.ShutdownBroadcast() returning the handler's
shutdown channel so the StopAndResume integration test can wait for
graceful-shutdown propagation deterministically instead of sleeping
for a fixed duration. The method is explicitly documented as
test-only and not part of the operational contract.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Raw tool or LLM errors can embed URLs with credentials, PII, or
partial DB records. We now log the full runErr for operator context
and persist a sanitized summary into agent_runs.error_message,
truncated at 512 bytes with a trailing ellipsis. The cut rewinds to
the nearest utf8.RuneStart so we never store a split rune. Also
wraps the pending-run loader error in Claim for parity with the
other coredata failures.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Each subtest now inserts its own PENDING run and runs under
t.Parallel(); shared state across subtests was the only reason they
had to stay sequential. Also adds a round-trip test that exercises
the approval-state fields (PendingToolCalls, PendingApprovals,
ApprovalInput, AllToolCalls, InnerCheckpoints, CompletedCalls) to
catch regressions where Save/Load drops nested or approval payloads.
The nonexistent-run case now uses a valid GID in the same tenant so
it reaches the row-not-found branch instead of short-circuiting on
the tenant-scope guard.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Each Save and Load now derives tenant_id from the run GID and pins it
in the WHERE clause. A caller that supplies an ID from another tenant
fails closed instead of silently reading or overwriting cross-tenant
checkpoint data. Also rejects oversize checkpoints on load as a
read-side guard against a tampered or migrated row exceeding
MaxCheckpointBytes.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
The transactional variant is no longer reachable: supervisor paths
persist checkpoints through PGCheckpointer (WithConn, not WithTx),
and Update deliberately excludes the checkpoint column to avoid
racing a concurrent checkpoint save. Nothing else calls it.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Prefix the unknown-tool error with the "cannot" convention and drop
the duplicate wrap around executeSingleTool: that helper already wraps
its generic error path, so the outer wrap produced messages shaped like
"cannot execute tool X: cannot execute tool X: ...".
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
MaxTurns is the only agent bound compared against a counter that is
serialised in the checkpoint (Turns). When config drifts between save
and restore -- typically because a deploy changed WithMaxTurns or a
different build of the agent is registered by name -- cp.Turns can
exceed agent.maxTurns on the resumed run, which previously surfaced
as a warning log and then a MaxTurnsExceededError on the first
iteration of the resumed coreLoop.
Capture MaxTurns in the new AgentConfig on every save, and on
restore clone the registry-resolved agent with WithMaxTurns applied
from the snapshot. The override flows through the outer Restore path
and through both inner-agent resolution sites in
restoreNestedSuspended and restoreAwaitingApproval, so nested
runs get the same treatment. Other loop bounds
(maxEmptyOutputRetries, maxToolDepth) reset per turn / per tool
depth and stay intentionally live so deploys can tune them without
invalidating in-flight checkpoints. Live references (tools, hooks,
LLM client, approval callbacks, guardrails) are not snapshotted for
the same reason.
With the snapshot in place, the "restored agent run has already
reached max turns" warning at the top of continueFromMessages is
structurally unreachable -- the live agent's bound is now the same
value cp.Turns was bounded by at save time -- and is removed.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
The supervisor was a hand-rolled polling, semaphore, and wait-group
loop predating the project's adoption of the shared worker kit. Two
sibling workers in pkg/probo already use the kit, and go-worker.md
documents it as the project convention.
This commit introduces agentRunHandler, which implements
worker.Handler[coredata.AgentRun] and worker.StaleRecoverer, and
reduces AgentRunSupervisor to a thin wrapper that owns the handler
plus a worker.Worker and bridges ctx cancellation into a handler-
level shutdown broadcast via context.AfterFunc. The agent stop
channel is now closed by a per-Process forwarder goroutine when the
broadcast fires, so in-flight runs checkpoint at the next turn
boundary and drain through wg.Wait before Run returns.
The stop_requested column, struct field, supporting SQL, and the
LoadRunningStopRequestedIDs function are removed end-to-end. None
of it was ever wired to an external surface; it existed purely to
let the supervisor find runs the operator wanted to halt. With the
kit handling the polling cadence and the AfterFunc bridging
shutdown, per-row flagging is dead weight.
The supervisor's public API (NewAgentRunSupervisor, Run, the With*
option helpers, and the error sentinels) stays intact so probod.go
needs no change. The integration test now triggers stop by
cancelling the supervisor context, which is the actual production
path through SIGTERM rather than a synthetic DB flag. Prometheus
counters and OTel spans labelled worker="agent-run-supervisor"
come for free.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
executeParallel ignored SuspendedError when checkpoint was nil,
treating it as a normal tool error. Nested suspension propagation
also dropped the in-memory checkpoint when persistence failed,
making runs non-resumable.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
The test does not belong in the probo package. Move it
alongside its shared helpers in pkg/agentruntest.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Tests the checkpoint persistence and supervisor lifecycle against a
real Postgres database:
- PGCheckpointStore Save/Load/Delete round-trip
- Supervisor claims PENDING run and completes it
- Cooperative stop/resume via stop_requested flag
- SIGTERM battle test: 3 kill/resume cycles across 10 tool-call
turns with parallel calls, long-running tools, thinking text,
and progressive checkpoint accumulation
Tests skip gracefully when Postgres is unavailable.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Poll-based supervisor that claims PENDING agent runs with FOR UPDATE
SKIP LOCKED, runs them with lease-based heartbeat, and handles
graceful shutdown. On infrastructure stop the row stays RUNNING so
stale recovery resets it to PENDING on restart; Restore picks up
from the last checkpoint. Heartbeat loss cancels execution without
committing a terminal status.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Create agent_runs table with lease-based concurrency control.
AgentRun entity follows standard coredata patterns with Scoper,
StrictNamedArgs, and cursor pagination. PGCheckpointStore implements
agent.CheckpointStore backed by the checkpoint JSONB column with
version validation and 10 MiB size guard. Register AgentRunEntityType
as entity type 75.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Restore loads a checkpoint from the store, resolves the agent from
a registry, and re-enters coreLoop. Handles suspended, nested
suspended (concurrent inner restore), and awaiting-approval states.
Partial progress is saved when some inner agents complete while
others remain suspended.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
coreLoop now saves incremental checkpoints after each tool-call turn
and checks a cooperative stop signal at turn boundaries. SuspendedError
is handled in finishRun, executeParallel, and executeSingleTool.
Approval-interrupted checkpoints are persisted for both flat and
nested interruptions.
Introduce RunOption, WithCheckpointStore, RunWithOpts, ResumeWithOpts,
and RunStreamedWithOpts so callers can provide checkpoint storage.
Add StreamEventSuspended and OnRunRestore hook.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Introduce Checkpoint, CheckpointStore, SuspendedError, AgentRegistry,
and CompletedCall types. Add cooperative stop signal via context.
Export CompletedCall (was unexported completedCall) so checkpoints
can reference completed tool results. Add JSON tags to ToolResult
and ApprovalResult for checkpoint serialization.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
Message, Part (Text/Image/File), ToolCall, FunctionCall, and Usage
now round-trip through JSON. Message uses a type-discriminated
envelope for the Part interface. Required for checkpoint persistence.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
The consent cookie is automatically created as a database record in
the necessary category when a new cookie banner is set up, so it
appears alongside other necessary cookies in the banner UI.
Signed-off-by: Émile Ré <emile@getprobo.com>
Include a `bid` field in the consent cookie so it explicitly
identifies which cookie banner it belongs to, making validation
direct instead of relying on the visitor ID as an implicit
discriminator. Existing cookies without `bid` self-heal on the
next load by falling through to the API fetch.
Signed-off-by: Émile Ré <emile@getprobo.com>
Introduce a required cookie_policy_url alongside the existing
privacy_policy_url (now optional) so banners can link directly to a
dedicated cookie policy — a compliance best practice recommended by
CNIL, ICO, and the EDPB. Existing rows are seeded from their current
privacy_policy_url value.
Both {{cookie_policy_link}} and {{privacy_policy_link}} placeholders
are supported independently in banner description translations.
Signed-off-by: Émile Ré <emile@getprobo.com>
Origin is a fundamental identity property of a banner tied to consent
records for a specific site. Changing it would break the audit trail
and violate GDPR consent specificity requirements.
Signed-off-by: Émile Ré <emile@getprobo.com>
The SCIM bridge for Google Workspace already exists but the
bootstrap builder did not register it, preventing deployment
via environment variables.
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
When DNS and CAA checks pass, ProvisioningError is set to nil but
was only persisted later alongside the challenge data. If
GetHTTPChallenge then failed, the update was never reached,
leaving stale DNS/CAA error messages visible to the user.
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
When DNS verification, CAA checks, or HTTP challenge completion
fail during certificate provisioning, the error is now stored on
the custom domain record and exposed via GraphQL. The console
displays it in both the domain card and domain detail dialog so
users can diagnose configuration issues without checking logs.
Previously these failures returned an error that was only logged
server-side, leaving users with no visibility into why their
domain was stuck in a pending state.
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
Add a partial unique index ensuring only one category per banner can
have posthog_consent enabled. Default it to the analytics category on
banner creation, clear the previous mapping before setting a new one,
and restrict the toggle to NORMAL categories in both the service layer
and the console UI.
Signed-off-by: Émile Ré <emile@getprobo.com>
Add PostHog opt-in/opt-out consent support mirroring the existing Google
Consent Mode integration: database column, GraphQL field, console UI
toggle, and client-side posthog-js calls.
Extract both GCM and PostHog logic from CookieBannerClient into a
ConsentIntegration plugin interface so future integrations can be added
without modifying the client core.
Signed-off-by: Émile Ré <emile@getprobo.com>
Expose gcmConsentTypes on CookieCategory type and accept
it in UpdateCookieCategoryInput so the console can read
and write GCM consent type mappings per category.
Signed-off-by: Émile Ré <emile@getprobo.com>
Include gcm_consent_types in the published banner config
so the cookie banner SDK can read category-to-GCM mappings
from the config endpoint.
Signed-off-by: Émile Ré <emile@getprobo.com>
Stores the Google Consent Mode v2 consent type mapping
per cookie category so each category can declare which
GCM signals it controls (e.g. analytics_storage,
ad_storage).
Signed-off-by: Émile Ré <emile@getprobo.com>
Address locale normalization for region-tagged values, guard
language detection for non-DOM runtimes, validate DefaultLanguage
on update, pass translated texts through the deactivation flow,
handle slug collisions in migration, add organizations FK, fix
consent migration from name-keyed to slug-keyed data, render all
template placeholders in previews, and wrap helper text for i18n.
Signed-off-by: Émile Ré <emile@getprobo.com>
label_description and label_duration are not user-customizable, so they
don't belong in the backend translation defaults. Hardcode them in the
cookie-banner SDK's i18n module instead.
Signed-off-by: Émile Ré <emile@getprobo.com>
The mutation was wired up in the GraphQL schema and resolver
but never called from the console frontend. This removes the
service method, request type, resolver, schema entries, and
two unused error sentinels (ErrTranslationNotFound,
ErrTranslationAlreadyExists).
Signed-off-by: Émile Ré <emile@getprobo.com>
Validate translation string values server-side with NoHTML() and
MaxLen(2000) to reject HTML in the translations JSON blob. On the
client side, escape user-provided template text before innerHTML
injection in banner_description and placeholder_text paths.
Signed-off-by: Émile Ré <emile@getprobo.com>
Introduce category-level name/description translations in the cookie
banner i18n flow. Seed default translations for fr/de/es on banner
creation, parse them from the stored JSON, and manage them via
react-hook-form Controllers instead of a manual ref/callback pattern.
Enhance the panel preview with category descriptions and all three
action buttons.
Signed-off-by: Émile Ré <emile@getprobo.com>
The category slug provides a stable, URL-safe key used as the
data-cookie-consent attribute value and consent data key, replacing
the fragile category name. This prevents breakage when categories
are renamed.
- Add slug column with unique-per-banner constraint and backfill migration
- Add Slug validator (lowercase alphanumeric + hyphens)
- Propagate slug through GraphQL schema, service layer, and snapshot
- Update console UI with slug field in create/edit forms
- Switch cookie-banner widget to use slug as consent data keys
Signed-off-by: Émile Ré <emile@getprobo.com>
Duration strings are static and not customizable through the console,
so they belong in the frontend cookie-banner package instead.
Signed-off-by: Émile Ré <emile@getprobo.com>
The field was populated by the backend and declared in the TS interface
but never consumed by any client-side code. The banner resolves its
language via detectLanguage() and passes it as a query param.
Signed-off-by: Émile Ré <emile@getprobo.com>
New banners now get French, German, and Spanish UI string
translations seeded alongside English during creation.
Signed-off-by: Émile Ré <emile@getprobo.com>