Skip to content

CCCC Daemon API/IPC Contract v1 ​

Status: Draft (for CCCC v0.5.x ecosystem)

This document defines the daemon-facing client contract for CCCC: how a client (CLI/Web/MCP bridge/SDK) discovers the daemon endpoint, frames requests, and calls daemon operations.

It is intentionally narrow:

  • CCCS v1 (docs/standards/CCCS_V1.md) defines the semantic collaboration substrate (event envelope + kinds + delivery/read/reply facts).
  • This document defines the transport + RPC layer used by CCCC today (newline-delimited JSON over a local socket/TCP).

0. Conformance Language ​

The key words MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY in this document are to be interpreted as described in RFC 2119.

1. Goals and Non‑Goals ​

1.1 Goals ​

Daemon IPC v1 MUST provide:

  • A stable request/response envelope with a normative error shape suitable for SDKs.
  • A cross-platform local transport (Unix socket where available; TCP fallback).
  • A single-writer control plane for group state, actors, messaging, inbox, and context.

1.2 Non‑Goals ​

Daemon IPC v1 does NOT standardize:

  • Remote authentication/authorization or multi-tenant security.
  • Any specific workflow engine or prompting strategy.
  • A browser-friendly HTTP API surface (this document is socket/TCP oriented).

2. Terminology ​

  • CCCC_HOME: The single global runtime home directory (default ~/.cccc/).
  • Daemon: The single-writer process that owns group state and appends to ledgers.
  • Client: Any process calling daemon operations (CLI/Web/MCP/SDK).
  • Group / Actor / Scope / Ledger: As defined in CCCS v1.
  • Principal (by): A string identity such as "user", "system", an actor_id, or a service principal.

3. Endpoint Discovery (Normative) ​

Clients MUST discover the daemon endpoint via a daemon-written descriptor file:

  • Path: ${CCCC_HOME}/daemon/ccccd.addr.json

CCCC_HOME resolution:

  • If the CCCC_HOME environment variable is set, clients MUST use it as the base directory.
  • Otherwise, clients MUST use the default ~/.cccc/.

If the descriptor file is missing or invalid, a client MAY fall back to:

  • Unix socket default: ${CCCC_HOME}/daemon/ccccd.sock (only if AF_UNIX is supported)

3.1 ccccd.addr.json Schema ​

The daemon writes a JSON object with the following fields:

json
{
  "v": 1,
  "transport": "unix",
  "path": "/home/alice/.cccc/daemon/ccccd.sock",
  "host": "",
  "port": 0,
  "pid": 12345,
  "version": "0.4.x",
  "ts": "2026-01-13T12:34:56Z"
}

Rules:

  • v MUST be 1.
  • transport MUST be "unix" or "tcp".
  • If transport == "unix", path MUST be a non-empty filesystem path.
  • If transport == "tcp", host MUST be a connectable host (typically 127.0.0.1) and port MUST be a positive integer.
  • Clients MUST treat unknown fields as ignorable metadata (but SHOULD preserve them if re-writing).

3.2 Daemon Runtime Files (Non-normative) ​

CCCC uses these files under ${CCCC_HOME}/daemon/:

  • ccccd.addr.json: endpoint descriptor (this spec)
  • ccccd.sock: Unix socket path (POSIX default)
  • ccccd.pid: daemon process id (best-effort)
  • ccccd.log: daemon log file (best-effort)

3.3 Endpoint Configuration (Non-normative) ​

Daemon endpoint selection is controlled by environment variables:

  • CCCC_DAEMON_TRANSPORT: "unix" or "tcp" (default: "unix" on POSIX, "tcp" on Windows)
  • CCCC_DAEMON_HOST: bind host for TCP (default: 127.0.0.1)
  • CCCC_DAEMON_PORT: bind port for TCP (default: 0 meaning “choose a free port”)

The native daemon rejects every non-loopback TCP host before binding. Daemon IPC has no authentication and cannot be exposed with 0.0.0.0, a LAN address, or a public address. Use the authenticated Web API for remote access.

4. Transport and Framing (Normative) ​

4.1 Transport ​

Daemon IPC v1 uses a stream transport:

  • Unix domain socket (transport="unix") where available.
  • TCP (transport="tcp") for cross-platform fallback.

Security note: there is no authentication at this layer. TCP bindings MUST remain loopback-only.

4.2 Framing: NDJSON ​

For all non-streaming operations, requests and responses are framed as:

  • One JSON object per line, delimited by a single \n (newline).
  • Encoding MUST be UTF‑8.

Baseline behavior:

  • A daemon MAY accept multiple request lines on one connection or close the connection after any response.
  • When multiple requests are accepted, they are processed strictly serially and produce one response line each.
  • Clients MUST NOT pipeline requests (there is no request id / multiplexing in v1).
  • Clients MUST tolerate the daemon closing a connection after any response and reconnect through endpoint discovery before the next request. The reference non-streaming client opens a fresh local connection for each call because v1 has no request id with which to make a close-versus-write race safely replayable.

4.3 Size Limits ​

Implementations MUST respect practical line limits to avoid truncation:

  • Request line limit (daemon receive): the native daemon stops reading after 16 MiB without a newline; clients MUST keep request lines below this bound.
  • Response line limit (typical clients): the reference client reader MAY cap a response line at ~4,000,000 bytes; daemons SHOULD keep single-response payloads below this bound.

Clients SHOULD treat truncated/invalid JSON as a transport failure. Once any request bytes have been written, clients MUST NOT automatically replay the request after a send, read, or decode failure unless the operation carries a daemon-enforced idempotency key. Retrying a failure that occurred while establishing the connection is safe because no request was sent.

4.4 Streaming Upgrade: term_attach ​

term_attach is a special operation that upgrades the connection:

  1. Client sends a normal request line with op="term_attach".
  2. Daemon sends a normal response line.
  3. If the response is ok=true, the connection becomes a terminal stream until closed.

After upgrade, the stream is not NDJSON.

The stream semantics are implementation-defined but, in CCCC today:

  • By default, the client receives raw PTY output bytes.
  • A client that requests bootstrap="snapshot_v1" can receive one negotiated ANSI screen snapshot first, followed by raw PTY bytes after the snapshot's raw cursor fence.
  • The client MAY write raw bytes as input.
  • The daemon MAY allow only one writer at a time (others become read-only).
  • A daemon MAY close an attachment that falls behind its bounded retained-output window. A reconnecting client SHOULD resume from its last fully consumed byte cursor using since; the handshake clamps an expired cursor to retained history.

Input is serialized per runtime session, without holding lifecycle synchronization across PTY I/O. An input transaction remains bound to that session: stopping and restarting an Actor MUST NOT route an old payload or submit-key suffix into its replacement. Writer ownership MUST be checked before starting an attachment input transaction. Partial or cancelled writes MUST NOT be reported as fully submitted. The Linux implementation uses nonblocking PTY readiness waits so backpressure remains responsive to cancellation, writer revocation and hangup; bytes already accepted by the PTY cannot be recalled. Other platforms retain their native PTY I/O. The reference daemon closes a terminal attachment if one input batch cannot be written within 30 seconds, releasing its writer ownership. This bounds a stalled input stream even when pending writes prevent observing client EOF. It does not limit idle viewing time or stop the Actor.

Out-of-band control:

  • Control operations (e.g., term_resize) MUST be performed over a separate concurrent daemon connection.

4.5 Streaming Upgrade: events_stream (Optional) ​

events_stream is an optional operation that upgrades the connection into a push event stream for reactive clients (Web/IDE/bots).

  1. Client sends a normal request line with op="events_stream".
  2. Daemon sends a normal response line.
  3. If the response is ok=true, the connection remains open and the daemon pushes NDJSON items indefinitely.

After upgrade, the stream is NDJSON, but it is no longer request/response: the daemon becomes the writer.

Stream item (recommended envelope):

ts
type EventStreamItem =
  | { t: "event"; event: CCCSEventV1 }
  | { t: "heartbeat"; ts: string }
  | { t: string; [k: string]: unknown } // forward-compatible extension

Rules:

  • Clients MUST ignore unknown t values.
  • heartbeat items MUST NOT be appended to the group ledger; they are transport-level keepalives.
  • Streams are best-effort: clients MUST tolerate disconnects, duplicates, and gaps (use inbox_peek or a ledger read to reconcile).

4.6 Streaming Upgrade: presentation_browser_attach / VNC attach (Optional) ​

presentation_browser_attach upgrades the connection into a daemon-local browser-surface control stream for a slot-scoped Presentation browser session. The same stream envelope is reused by other projected browser attach operations, including provider-auth and Web Model browser surfaces.

  1. Client sends a normal request line with op="presentation_browser_attach".
  2. Daemon sends a normal response line.
  3. If the response is ok=true, the connection remains open and becomes a bidirectional NDJSON stream.

After upgrade:

  • The daemon pushes state and frame items for the active browser surface session.
  • The client MAY send browser-control commands such as navigation, click, scroll, key, text, resize, and close.
  • Only one active controller MAY be attached at a time for a given slot browser surface session.
  • If a matching *_vnc_attach operation succeeds, the connection upgrades into a raw RFB/VNC byte stream instead of NDJSON. VNC attach is an optional viewer transport; browser control and delivery semantics remain owned by the daemon runtime.

Recommended daemon-to-client items (CCCC v0.4.x behavior):

ts
type PresentationBrowserStreamItem =
  | {
      t: "state"
      active: boolean
      state: "starting" | "ready" | "failed" | "closed" | "idle"
      message: string
      error?: Record<string, unknown>
      strategy?: string
      url?: string
      width?: number
      height?: number
      started_at?: string
      updated_at?: string
      last_frame_seq?: number
      last_frame_at?: string
      controller_attached?: boolean
    }
  | {
      t: "frame"
      seq: number
      captured_at: string
      mime: "image/jpeg"
      data_base64: string
      width: number
      height: number
      url: string
    }
  | {
      t: "error"
      code: string
      message: string
    }

Recommended client-to-daemon commands (CCCC v0.4.x behavior):

ts
type PresentationBrowserCommand =
  | { t: "ping" }
  | { t: "navigate"; url: string }
  | { t: "back" }
  | { t: "refresh" }
  | { t: "click"; x: number; y: number; button?: "left" | "middle" | "right" }
  | { t: "scroll"; dx?: number; dy?: number }
  | { t: "key"; key: string }
  | { t: "text"; text: string }
  | { t: "resize"; width: number; height: number }
  | { t: "close" | "disconnect" }

Rules:

  • Clients MUST treat unknown t values as ignorable forward-compatible extensions.
  • The browser-surface stream is best-effort and ephemeral; clients MUST be able to reconnect and recover via presentation_browser_info / presentation_browser_open.
  • The stream is daemon-local runtime state and MUST NOT be treated as persisted Presentation card state.

5. Request/Response Envelope (Normative) ​

Daemon IPC v1 uses the envelope defined in crates/cccc-contracts/src/ipc.rs.

5.1 Request ​

ts
interface DaemonRequestV1 {
  v: 1
  op: string
  args?: Record<string, unknown> // default {}
}

Rules:

  • v MUST be 1.
  • op MUST be a non-empty string (snake_case in CCCC v0.4.x).
  • Clients MUST NOT send unknown top-level fields (the daemon is strict at the envelope level).

5.2 Response ​

ts
interface DaemonResponseV1 {
  v: 1
  ok: boolean
  result: Record<string, unknown> // default {}
  error?: DaemonErrorV1 | null
}

interface DaemonErrorV1 {
  code: string
  message: string
  details: Record<string, unknown> // default {}
}

Rules:

  • v MUST be 1.
  • If ok == true, error MUST be omitted or null.
  • If ok == false, error MUST be present.
  • Clients MUST NOT expect a stable schema for result beyond what each op specifies.

6. Error Model (Normative) ​

The error envelope shape in §5.2 is normative: daemons MUST return errors using this shape for all application-level failures.

6.1 Error Code Conventions ​

  • error.code MUST be a stable, machine-readable token.
  • error.message MUST be human-readable.
  • error.details MUST be a JSON object (may be empty).
  • The set of error.code values is an open set; clients MUST handle unknown codes gracefully.

Common codes used by CCCC v0.4.x include (non-exhaustive):

  • invalid_request, unknown_op
  • missing_group_id, group_not_found
  • missing_actor_id, actor_not_found, actor_not_running, not_pty_actor
  • permission_denied
  • invalid_patch, invalid_template, confirmation_required

7. Operation Conventions ​

7.1 Identity and Permission Parameters ​

Many operations accept:

  • group_id: target group identifier (string)
  • actor_id: target actor identifier (string)
  • by: principal string indicating who is acting (default varies by op)

Authorization is enforced by the daemon (see crates/cccc-core/src/permissions.rs and the operation-level checks in crates/cccc-daemon/src/ops/). Daemon IPC v1 has no authentication. The practical trust boundary is OS-level access control to the local socket / localhost port.

Local-trust model (CCCC v0.4.x behavior):

  • If an operation accepts args.by, the daemon treats it as a caller-provided principal hint and uses it for attribution (ledger event.by) and permission checks.
  • If by is omitted or blank, the daemon uses an operation-specific default (often "user").

Security note:

  • In a local-trust deployment, any process that can connect to the daemon can spoof by. Do not treat by as a security boundary.
  • Remote/multi-tenant authentication is out of scope for v1.

7.2 Event Objects ​

Many operations return or include ledger events. Event envelopes follow the CCCC/CCCS v1 shape (see crates/cccc-contracts/src/event.rs and docs/standards/CCCS_V1.md).

8. Operation Catalog (Normative for v1) ​

Unless otherwise stated:

  • All operations use the request/response envelope in §5.
  • All args live under request.args.
  • All returned values live under response.result.

8.1 Core ​

ping ​

Args: none

Result:

ts
{
  version: string;
  implementation: "rust";
  pid: number;
  ts: string;
  ipc_v: 1;
  capabilities: Record<string, unknown>;
  compatibility?: string;
  build?: { source_id: string };
  executable?: string | null;
}

Notes:

  • SDK-compatible daemons MUST return ipc_v: 1; omitting it is interpreted as IPC version 0.
  • The bundled daemon MUST identify its implementation as rust.
  • compatibility, when present, is an implementation-specific compatibility identity; clients MUST NOT infer compatibility from the implementation name alone.
  • SDK-compatible daemons MUST return a capabilities feature map. The native daemon advertises supported events_stream, remote_access, optional browser-attach operations, and optional terminal-attach extensions here.
  • Each optional browser stream is advertised under its exact operation name (presentation_browser_attach, presentation_browser_vnc_attach, space_provider_auth_browser_attach, space_provider_auth_browser_vnc_attach, web_model_browser_attach, or web_model_browser_vnc_attach). true means the daemon recognizes that streaming upgrade; false means callers MUST use another product surface or treat the operation as unavailable.
  • A product implementation MAY serve an equivalent ephemeral browser surface directly through its local Web port. That does not make the daemon IPC upgrade supported: the exact daemon capability MUST remain false unless that daemon recognizes and serves the operation itself.
  • term_attachment_status=true means term_attach returns a positive attachment_id and the daemon implements writer-ownership status checks for that ID. term_attach_snapshot_v1=true means callers may request bootstrap="snapshot_v1" and receive initial_output metadata. Clients MUST retain the baseline replay-stream behavior when either extension is false.
  • Clients SHOULD probe operation support independently; a recognized operation may reject empty probe arguments, but MUST NOT return unknown_op.
  • Clients MUST NOT probe an unadvertised browser attach operation merely to discover support: a successful probe upgrades the connection and may acquire the only controller. They SHOULD consult the exact capability first.
  • Clients MUST use protocol, compatibility, and capability fields instead of exact product-version equality.
  • Optional build.source_id is a SHA-256 fingerprint of the Rust workspace source, manifests/lockfile and bundled resources (root and crate-local resources/ trees), compiled into the running process. It distinguishes same-version source changes, including uncommitted changes, without requiring Git at build/run time. It is diagnostic metadata, not a binary checksum or compatibility gate. Web assets are identified separately by authenticated Web ping (web.assets_id and web.entry_script), using the same asset source as HTTP delivery. Release builds inspect their immutable embedded bundle; source-run debug builds inspect the current disk bundle, including frontend-only rebuilds. Unavailable assets report null identities rather than old build-time values. Unauthenticated health responses remain minimal.
  • cccc doctor includes its own build and the daemon's reported build. MCP initialize includes the server's build under _meta["cccc/build"]; this reports the MCP process actually answering, not the executable currently on disk.
  • executable is the effective daemon executable path for local diagnosis. Web exposes local paths only to administrators requesting include_home; ordinary authenticated ping and all health projections omit the executable path.
  • Ordinary business commands MUST NOT stop, signal, or replace a reachable daemon. Implementation replacement is restricted to explicit daemon lifecycle commands.

shutdown ​

Args:

ts
{ expected_pid?: number }

When expected_pid is present, it MUST be a positive integer matching the receiving daemon's current process ID. A mismatch MUST return daemon_owner_mismatch and MUST NOT begin shutdown. This optional fence lets a lifecycle owner avoid stopping a replacement daemon after an IPC descriptor handoff. Omitting expected_pid preserves the normal administrator shutdown behavior.

Result:

ts
{ message: string } // "shutting down"

8.2 Observability (Global) ​

observability_get ​

Args: none

Result:

ts
{ observability: Record<string, unknown> }

observability_update ​

Args:

ts
{ by?: "user"; patch: Record<string, unknown> }

Result:

ts
{ observability: Record<string, unknown> }

branding_get ​

Args: none

Result:

ts
{
  branding: {
    product_name: string
    logo_icon_asset_path?: string
    favicon_asset_path?: string
    updated_at?: string
  }
}

branding_update ​

Args:

ts
{ by?: "user"; patch: Record<string, unknown> }

Result:

ts
{
  branding: {
    product_name: string
    logo_icon_asset_path?: string
    favicon_asset_path?: string
    updated_at?: string
  }
}

debug_snapshot ​

Developer-mode diagnostic snapshot (global + optional group context).

Args:

ts
{ group_id?: string; by?: string }

Result:

ts
{
  developer_mode: true
  observability: Record<string, unknown>
  daemon: { pid: number; version: string; ts: string }
  group?: { group_id: string; state: string; active_scope_key: string; title: string }
  actors?: Array<{ id: string; role: string; runtime: string; runner: string; runner_effective: string; enabled: boolean; running: boolean; unread_count: number }>
  delivery?: Record<string, unknown>
}

Notes:

  • Requires developer mode.
  • Permission is user, or foreman when group_id is provided.

8.3 Groups and Scopes ​

attach ​

Attach a directory scope to a group (or auto-create/select a group for this scope).

Args:

ts
{ path: string; group_id?: string; by?: string }

Result:

ts
{ group_id: string; scope_key: string; title?: string }

groups ​

List known groups (registry summaries).

Args: none

Result:

ts
{ groups: Array<Record<string, unknown>> } // includes at least group_id/title/created_at/updated_at + running/state

registry_reconcile ​

Scan registry entries for missing/corrupt groups, and optionally remove missing entries.

Args:

ts
{ remove_missing?: boolean }

Result:

ts
{
  dry_run: boolean
  scanned_groups: number
  missing_group_ids: string[]
  corrupt_group_ids: string[]
  removed_group_ids: string[]
  removed_default_scope_keys: string[]
}

capability_overview ​

Return a global capability library snapshot for Settings/Policy views (no actor scope required).

Args:

ts
{
  query?: string
  limit?: number          // default 400, max 2000
  include_indexed?: boolean // default true
}

Result:

ts
{
  items: Array<{
    capability_id: string
    kind: "mcp_toolpack" | "skill" | ""
    name: string
    description_short?: string
    source_id?: string
    source_uri?: string
    source_tier?: string
    trust_tier?: string
    license?: string
    sync_state?: string
    policy_level: "indexed" | "mounted" | "enabled" | "pinned"
    policy_visible: boolean
    blocked_global: boolean
    blocked_reason?: string
    enable_supported: boolean
    qualification_status: "qualified" | "unavailable" | "blocked"
    qualification_reasons?: string[] // currently exposed for agent_self_proposed skill management
    capsule_text?: string            // currently exposed for agent_self_proposed skill management
    install_mode?: string
    autoload_candidate: boolean
    tags?: string[]
    tool_count?: number
    tool_names?: string[]
    cached_install_state?: string
    cached_install_error_code?: string
    cached_install_error?: string
    recent_success?: {
      success_count: number
      last_success_at?: string
      last_group_id?: string
      last_actor_id?: string
      last_action?: string
    }
  }>
  count: number
  query: string
  sources: Record<string, {
    source_id: string
    enabled: boolean
    source_level: "indexed" | "mounted" | "enabled" | "pinned"
    rationale?: string
    sync_state: string
    last_synced_at?: string
    staleness_seconds: number
    record_count: number
    error?: string
  }>
  blocked_capabilities: Array<{
    capability_id: string
    scope: "global"
    reason?: string
    by?: string
    blocked_at?: string
    expires_at?: string
  }>
  allowlist_revision: string
}

Search capability registry records (built-in packs + local curated catalog + cached remote records).

Args:

ts
{
  group_id: string
  actor_id?: string
  by?: string
  query?: string
  kind?: "mcp_toolpack" | "skill" | ""
  source_id?: string
  trust_tier?: string
  qualification_status?: "qualified" | "unavailable" | "blocked" | ""
  include_external?: boolean
  limit?: number
}

Result:

ts
{
  group_id: string
  actor_id?: string
  default_profile: "core"
  items: Array<{
    capability_id: string
    kind: "mcp_toolpack" | "skill"
    name: string
    description_short: string
    source_id: string
    source_tier: string
    source_uri?: string
    trust_tier: string
    license?: string
    qualification_status: "qualified" | "unavailable" | "blocked"
    sync_state?: string
    enabled: boolean
    enable_supported: boolean
    install_mode?: string
    policy_level?: "indexed" | "mounted" | "enabled" | "pinned"
    enable_hint?: "enable_now" | "blocked" | "unsupported" | "active"
    blocked_reason?: string
    readiness_preview?: {
      preview_status: "blocked" | "enableable" | "active" | "needs_inspect"
      next_step: string
      already_active?: boolean
    }
    tags?: string[]
    tool_count?: number
    tool_names?: string[]
  }>
  count: number
  sources: Record<string, unknown>
  applied_filters: {
    kind: string
    source_id: string
    trust_tier: string
    qualification_status: string
  }
  search_diagnostics?: {
    remote_augmented: boolean
    remote_added: number
    remote_error?: string
    policy_hidden_count?: number
  }
}

capability_enable ​

Enable or disable a capability by scope.

Notes:

  1. Built-in capability packs (pack:*) are directly enable-able and can change MCP exposure.
  2. Skills (kind=skill) use the same capability_enable op for activate/deactivate and can auto-apply declared dependencies.
  3. External MCP execution path supports remote_only, package, and command.
  4. package mode supports npm (npx), pypi (uvx/pipx), OCI (docker/podman) and can fall back to command candidates when package metadata is incomplete.
  5. External enable runs preflight first (required env, runtime binary availability, remote URL sanity) and returns reason=preflight_failed:<code> on deterministic blockers.
  6. activation_pending means relist/reconnect is still required; runnable means binding is live enough to try; verified is reserved for post-call proof, not plain enable.

Args:

ts
{
  group_id: string
  capability_id: string
  scope?: "group" | "actor" | "session"   // default: session
  enabled?: boolean                         // default: true
  cleanup?: boolean                         // default: false; disable path can also clean runtime cache
  reason?: string                           // optional short audit reason
  ttl_seconds?: number                      // session scope only
  by?: string
  actor_id?: string
}

Result:

ts
{
  action_id: string
  group_id: string
  actor_id: string
  capability_id: string
  scope: "group" | "actor" | "session"
  enabled: boolean
  state: "activation_pending" | "runnable" | "blocked" | "disabled"
  refresh_required: boolean
  refresh_mode?: "relist_or_reconnect"
  wait?: "relist_or_reconnect"
  reason?: string
  error?: string
  retryable?: boolean
  install_error_code?: string
  required_env?: string[]
  missing_binaries?: string[]
  policy_level?: "indexed" | "mounted" | "enabled" | "pinned"
  install_state?: "installed" | "installed_degraded" | "install_failed"
  degraded?: boolean
  degraded_reason?: string
  degraded_call_hint?: string
  fallback_from?: "package"
  fallback_reason?: string
  preflight?: {
    ok: boolean
    code: string
    message: string
    required_env?: string[]
    missing_binaries?: string[]
  }
  diagnostics?: Array<{
    code: string
    message: string
    retryable?: boolean
    required_env?: string[]
    action_hints?: string[]
  }>
  removed_binding_count?: number
  removed_installation?: boolean
  cleanup_skipped_reason?: string
  skill?: {
    capability_id: string
    name: string
    description_short?: string
    capsule?: string
    requires_capabilities?: string[]
    applied_dependencies?: string[]
    skipped_dependencies?: Array<{ capability_id: string; reason: string }>
    source_id?: string
    source_uri?: string
  }
}

Quota notes:

  1. CCCC_CAPABILITY_MAX_ENABLED_PER_ACTOR (default 20) limits actor/session enabled non-skill capability count.
  2. CCCC_CAPABILITY_MAX_ENABLED_PER_GROUP (default 24) limits group-scope enabled capability count.
  3. CCCC_CAPABILITY_MAX_INSTALLATIONS_TOTAL (default 128) limits total cached external artifacts.
  4. Quota failures return ok=true with state="failed" and deterministic reason code.

capability_block ​

Block/unblock capabilities at runtime.

Notes:

  1. scope=group: foreman or user can block/unblock.
  2. scope=global: only user can block/unblock.
  3. Blocking revokes enabled bindings and runtime dynamic tool exposure immediately.

Args:

ts
{
  group_id: string
  capability_id: string
  scope?: "group" | "global" // default: group
  blocked?: boolean           // default: true
  ttl_seconds?: number        // 0 means no expiry
  reason?: string
  by?: string
  actor_id?: string
}

Result:

ts
{
  action_id: string
  group_id: string
  actor_id: string
  capability_id: string
  scope: "group" | "global"
  blocked: boolean
  state: "blocked" | "unblocked"
  removed_bindings: number
  removed_runtime_bindings: number
  refresh_required: boolean
  refresh_mode?: "relist_or_reconnect"
  wait?: "relist_or_reconnect"
  block?: {
    reason?: string
    by?: string
    blocked_at?: string
    expires_at?: string
  }
}

capability_state ​

Read effective capability exposure and visible MCP tool names for caller scope.

Args:

ts
{
  group_id: string
  actor_id?: string
  by?: string
  capability_id?: string // optional; returns capability_usage for this id
  view?: "mcp_catalog" // reserved for actor-scoped MCP tool discovery
}

The mcp_catalog view is read from one atomic Group snapshot without acquiring either outer lifecycle lock (Group or global). Managed runtimes can initialize their actor-scoped CCCC MCP server while actor_start is still materializing the provider, so serializing this internal catalog read behind that same lifecycle lock would deadlock provider startup. A queued global writer can also block a subsequent global read while waiting for startup to release its permit; therefore a global read permit is not a safe substitute. Catalog assembly reuses its captured Group snapshot for role and tool visibility decisions; authorization checks and capability-store synchronization still apply. All other capability_state reads retain normal Group read serialization; capability-store updates keep their own locking and are not relaxed by this view.

Ordinary Actor base tool exposure MUST share one definition with the native MCP fallback catalog. It includes cccc_connect, cccc_message_deliver and cccc_reply_request_cancel. Web Model additions and the Voice Secretary's restricted profile MUST also agree when daemon IPC is temporarily unavailable; fallback discovery does not grant daemon permissions or activate capability packs. User control tools and enabled packs retain their existing scope checks.

Native MCP admission applies a tool's published optional action default before permission, message classification and routing. JSON Schema defaults are annotations; clients are not required to insert them. Explicit actions remain unchanged. cccc_im_bind has no action selector and maps directly to im_bind_chat.

Result:

ts
{
  group_id: string
  actor_id: string
  default_profile: "core"
  core_tool_count: number
  visible_tool_count: number
  visible_tools: string[]
  dynamic_tools?: Array<{
    name: string
    description?: string
    inputSchema: Record<string, unknown>
    capability_id: string
    real_tool_name: string
  }>
  dynamic_tool_limit: number
  dynamic_tool_dropped: number
  enabled_capabilities: string[]
  active_capsule_skills?: Array<{
    capability_id: string
    name: string
    description_short?: string
    capsule_preview?: string
    capsule_text?: string
    source_id?: string
    source_uri?: string
    policy_level?: "indexed" | "mounted" | "enabled" | "pinned"
    activation_sources?: Array<{
      scope: "group" | "actor" | "session"
      actor_id?: string
      expires_at?: string
      ttl_seconds?: number
    }>
  }>
  autoload_skills?: Array<{
    capability_id: string
    name: string
    description_short?: string
    capsule_preview?: string
    capsule_text?: string
    source_id?: string
    policy_level?: "indexed" | "mounted" | "enabled" | "pinned"
  }>
  autoload_capabilities?: string[]
  actor_autoload_capabilities?: string[]
  profile_autoload_capabilities?: string[]
  actor_hidden_capabilities?: string[] // actor-level UI/menu hide preferences, including Web user slash menu; does not disable the capability
  hidden_capabilities: Array<{
    capability_id: string
    reason: string
    name?: string
    description_short?: string
    kind?: "mcp_toolpack" | "skill"
    source_id?: string
    policy_level?: "indexed" | "mounted" | "enabled" | "pinned" | "blocked"
    state?: string
    install_error_code?: string
    install_error?: string
  }>
  external_binding_states?: Record<string, {
    mode: "mcp" | "skill"
    state: string
    install_state?: string
    artifact_id?: string
    last_error?: string
    last_error_code?: string
  }>
  precedence_chain: ["session", "actor", "group"]
  session_bindings: Array<{
    capability_id: string
    expires_at: string
    ttl_seconds: number
  }>
  source_states: Record<string, unknown>
  blocked_capabilities?: Array<{
    capability_id: string
    scope: "group" | "global"
    reason?: string
    by?: string
    blocked_at?: string
    expires_at?: string
  }>
  capability_usage?: {
    capability_id: string
    used: boolean
    group_enabled: boolean
    group_actor_count: number
    actor_enabled: Array<{ actor_id: string; actor_title?: string; label?: string }>
    session_enabled: Array<{ actor_id: string; actor_title?: string; label?: string; expires_at: string; ttl_seconds: number }>
    actor_autoload: Array<{ actor_id: string; actor_title?: string; label?: string }>
    profile_autoload: Array<{ actor_id: string; actor_title?: string; label?: string; profile_id?: string; profile_name?: string }>
    blocked: boolean
    blocked_scope?: "group" | "global"
    blocked_reason?: string
  }
  is_foreman: boolean
}

Operational notes:

  1. Capability catalog is daemon-owned local state seeded from allowlist and runtime discoveries.
  2. Search uses local curated catalog + cached remote results; no periodic capability sync loop.
  3. Source gates:
    • CCCC_CAPABILITY_SOURCE_MCP_REGISTRY_ENABLED (default 1)
    • CCCC_CAPABILITY_SOURCE_ANTHROPIC_SKILLS_ENABLED (default 1)
    • github_skills_curated is allowlist-curated (no periodic source crawler).
    • agent_self_proposed is for agent-generated procedural skill candidates; default policy keeps MCP toolpacks indexed while allowing capsule skills to be validated and enabled at narrow scope.
    • skillsmp_remote is on-demand SkillsMP remote search (API key mode + proxy fallback).
    • clawhub_remote is on-demand ClawHub remote search (official API).
    • openclaw_skills_remote is on-demand OpenClaw GitHub corpus search.
    • clawskills_remote is on-demand clawskills.co index search.
  4. Dynamic tool exposure is capped by CCCC_CAPABILITY_MAX_DYNAMIC_TOOLS_VISIBLE (default 32).
  5. Catalog snapshot size is capped by CCCC_CAPABILITY_CATALOG_MAX_RECORDS (default 20000); prune is applied during explicit sync operations.
  6. Search may perform remote augmentation (MCP + skill) when local hits are insufficient:
    • CCCC_CAPABILITY_SEARCH_REMOTE_FALLBACK (default 1)
    • CCCC_CAPABILITY_SEARCH_REMOTE_FALLBACK_LIMIT (default 40, max 100)
    • CCCC_CAPABILITY_SOURCE_SKILLSMP_REMOTE_ENABLED (default 1)
    • CCCC_CAPABILITY_SOURCE_CLAWHUB_REMOTE_ENABLED (default 1)
    • CCCC_CAPABILITY_SOURCE_OPENCLAW_SKILLS_REMOTE_ENABLED (default 1)
    • CCCC_CAPABILITY_SOURCE_CLAWSKILLS_REMOTE_ENABLED (default 1)
    • CCCC_CAPABILITY_SEARCH_REMOTE_SKILL_LIMIT (default follows remote fallback limit)
    • CCCC_CAPABILITY_SEARCH_REMOTE_SKILLSMP_LIMIT (default follows remote fallback limit)
    • CCCC_CAPABILITY_SEARCH_REMOTE_CLAWHUB_LIMIT (default follows remote fallback limit)
    • CCCC_CAPABILITY_SEARCH_REMOTE_OPENCLAW_LIMIT (default follows remote fallback limit)
    • CCCC_CAPABILITY_SEARCH_REMOTE_CLAWSKILLS_LIMIT (default follows remote fallback limit)
    • CCCC_CAPABILITY_SKILLSMP_PROXY_BASE (default https://r.jina.ai/http://skillsmp.com/search)
    • CCCC_CAPABILITY_SKILLSMP_API_BASE (default https://skillsmp.com/api/v1/skills/search)
    • CCCC_CAPABILITY_SKILLSMP_API_KEY (optional; enables direct SkillsMP API)
    • CCCC_CAPABILITY_CLAWHUB_API_BASE (default https://clawhub.ai/api/v1/skills)
    • CCCC_CAPABILITY_CLAWSKILLS_DATA_URL (default https://clawskills.co/skills-data.js)
  7. Allowlist override env/path compatibility (CCCC_CAPABILITY_ALLOWLIST_PATH and CCCC_HOME/config/capability-allowlist.yaml) is removed. Policy now always uses:
    • packaged default: crates/cccc-daemon/resources/capability-allowlist.default.yaml
    • user overlay: CCCC_HOME/config/capability-allowlist.user.yaml
    • effective policy: deterministic merge (default <- overlay).
  8. The authenticated local user control plane (actor_id="user", by="user") includes cccc_group and cccc_actor in its baseline visible tools. Actor sessions do not inherit this user-only baseline and continue to require pack:group-runtime or their existing role/profile defaults.

capability_visibility ​

Hide or show a capability for one actor's UI/menu surfaces without changing enabled bindings. The Web UI uses actor_id="user" to control whether an enabled capsule skill appears in the / command menu.

Args:

ts
{
  group_id: string
  by?: string
  actor_id?: string // default: by or "user"
  capability_id: string
  hidden: boolean
  reason?: string
}

Result:

ts
{
  action_id: string
  group_id: string
  actor_id: string
  capability_id: string
  hidden: boolean
  actor_hidden_capabilities: string[]
  state: "hidden" | "visible"
}

The Web slash-command adapter MUST keep slash_skill_dispatch.task_text non-empty. For a bare capsule command such as /cccc-self-evolution, it sends the canonical task Run the skill's default workflow.; explicit text after the command is forwarded unchanged. This keeps the existing daemon validation contract compatible across independently restarted Web and daemon processes.

capability_import ​

Import one normalized capability record prepared by the caller (agent-driven parsing), then optionally enable it.

Notes:

  1. This op does not parse arbitrary web/forum text; caller must provide structured record.
  2. kind=mcp_toolpack requires install_mode + install_spec.
  3. kind=skill requires capsule_text.
  4. dry_run=true validates/probes only (no catalog persistence).
  5. command* and fallback_command* may be provided as top-level shortcuts; daemon copies them into install_spec when missing.
  6. record.source_id is optional; empty or unknown source ids are normalized to manual_import.
  7. record.source_id=agent_self_proposed preserves autonomous skill-proposal provenance. Default policy treats kind=skill capsule records from this source as mounted, while non-skill toolpacks remain indexed unless policy explicitly promotes them.
  8. agent_self_proposed skill capsule text must include required proposal sections: When to use, Avoid when, Procedure, Pitfalls, and Verification; non-dry-run imports missing sections are rejected before catalog persistence so the last valid active record is preserved.
  9. agent_self_proposed skill capability ids must use skill:agent_self_proposed:<stable-slug> to avoid colliding with curated namespaces such as skill:anthropic:* or skill:github:*.
  10. For low-risk, syntax-valid agent_self_proposed capsule skills, direct import is allowed. Use dry_run=true first when enabling immediately, scope/risk is unclear, or probe diagnostics are useful; high-risk candidates should be recorded as qualification_status=blocked with explicit qualification_reasons.
  11. Re-importing the same capability_id updates the catalog record. Agents should use that path for stale, incomplete, wrong, or duplicative agent_self_proposed skills instead of creating near-duplicates or silently deleting records.
  12. Import results use import_action, record_changed, already_active, and active_after_import to distinguish create/update/no-op and whether the target actor had an effective binding before and after import. Local sync timestamps do not count as semantic changes, while an explicitly supplied updated_at_source still participates in the comparison. import_action is the primary create/update/unchanged signal; record_changed only compares existing records. already_active is pre-import state; active_after_import is the post-import runnable binding.
  13. If readiness_preview.preview_status=active or active_after_import=true, agents must not re-enable the same skill just to refresh its capsule text. Use capability_state.active_capsule_skills[].capsule_text for full post-import verification; capsule_preview is only a compact display summary.

Args:

ts
{
  group_id: string
  by?: string
  actor_id?: string
  record: {
    capability_id: string                 // mcp:* or skill:*
    kind: "mcp_toolpack" | "skill"
    name?: string
    description_short?: string
    source_id?: string // optional; unknown/empty -> manual_import; agent_self_proposed preserves skill-proposal provenance
    source_uri?: string
    source_record_id?: string
    source_record_version?: string
    updated_at_source?: string
    source_tier?: string
    trust_tier?: string
    qualification_status?: "qualified" | "unavailable" | "blocked"
    qualification_reasons?: string[]
    tags?: string[]
    license?: string
    install_mode?: "remote_only" | "package" | "command" // mcp_toolpack only
    install_spec?: Record<string, unknown>    // mcp_toolpack only
    command?: string | string[]               // command mode shortcut
    command_candidates?: Array<string | string[]> // command mode/fallback candidates
    fallback_command?: string | string[]      // optional package->command fallback
    fallback_command_candidates?: Array<string | string[]> // optional package->command fallback candidates
    capsule_text?: string                     // skill only
    requires_capabilities?: string[]          // skill only
  }
  dry_run?: boolean                // default false
  probe?: boolean                  // default true
  enable_after_import?: boolean    // default false
  scope?: "group" | "actor" | "session"
  ttl_seconds?: number
  reason?: string
}

Result:

ts
{
  action_id: string
  group_id: string
  actor_id: string
  capability_id: string
  kind: "mcp_toolpack" | "skill"
  dry_run: boolean
  imported: boolean
  scope: "group" | "actor" | "session"
  import_action?: "created" | "updated" | "unchanged"
  record_changed?: boolean
  already_active?: boolean           // target actor had an effective binding before optional enable_after_import
  active_after_import?: boolean      // target actor has a runnable binding after import/optional enablement
  record: Record<string, unknown>
  probe: {
    state: "runnable" | "failed" | "skipped"
    kind?: "mcp_toolpack" | "skill"
    reason?: string
    tool_count?: number
    tool_names?: string[]
    install_error_code?: string
    install_error?: string
  }
  diagnostics: Array<{
    code: string
    message: string
    retryable?: boolean
    required_env?: string[]
    action_hints?: string[]
  }>
  effective_policy_level: "indexed" | "mounted" | "enabled" | "pinned"
  enableable_now: boolean
  enable_block_reason?: "policy_level_indexed" | "qualification_blocked" | "capability_unavailable"
  readiness_preview?: {
    preview_status: "blocked" | "enableable" | "active" | "needs_inspect"
    next_step: string
    already_active?: boolean
    preview_basis?: string[]
    required_env?: string[]
    missing_env?: string[]
    cached_install_state?: string
    install_error_code?: string
    enable_block_reason?: "policy_level_indexed" | "qualification_blocked" | "capability_unavailable" | "missing_required_env"
    policy_source?: "external_capability_safety_mode"
    policy_mode?: "conservative"
  }
  enable_after_import: boolean
  enable_result?: Record<string, unknown> // same shape family as capability_enable
  refresh_required: boolean
  state: "blocked" | "enableable" | "needs_inspect" | "activation_pending" | "runnable" | "verified"
  reason?: string
}

capability_allowlist_get ​

Read allowlist default/overlay/effective snapshots and revision hash.

Args:

ts
{ by?: string } // write ops still enforce by=user; read is open

Result:

ts
{
  default: Record<string, unknown>
  overlay: Record<string, unknown>
  effective: Record<string, unknown>
  revision: string
  default_source: string
  overlay_source: string
  overlay_error: string
  policy_source: string
  policy_error: string
  external_capability_safety_mode: "normal" | "conservative"
}

capability_allowlist_validate ​

Dry-run allowlist overlay validation (no persistence).

Args:

ts
{
  mode?: "patch" | "replace" // default: patch
  patch?: Record<string, unknown>   // required when mode=patch
  overlay?: Record<string, unknown> // required when mode=replace
}

Result:

ts
{
  valid: boolean
  reason: string
  default: Record<string, unknown>
  overlay: Record<string, unknown>
  effective: Record<string, unknown>
  revision: string
  external_capability_safety_mode: "normal" | "conservative"
}

capability_allowlist_update ​

Persist allowlist overlay with optimistic concurrency.

Args:

ts
{
  by?: string // must be "user"
  mode?: "patch" | "replace" // default: patch
  expected_revision?: string
  patch?: Record<string, unknown>   // required when mode=patch
  overlay?: Record<string, unknown> // required when mode=replace
}

Result:

ts
{
  updated: true
  revision: string
  default: Record<string, unknown>
  overlay: Record<string, unknown>
  effective: Record<string, unknown>
  policy_source: string
  policy_error: string
  external_capability_safety_mode: "normal" | "conservative"
}

Errors:

  • allowlist_revision_mismatch
  • allowlist_validation_failed

capability_allowlist_reset ​

Reset overlay to empty (removes CCCC_HOME/config/capability-allowlist.user.yaml when present).

Args:

ts
{ by?: string } // must be "user"

Result:

ts
{
  reset: true
  removed_overlay_file: boolean
  revision: string
  default: Record<string, unknown>
  overlay: Record<string, unknown>
  effective: Record<string, unknown>
  default_source: string
  overlay_source: string
  overlay_error: string
  policy_source: string
  policy_error: string
  external_capability_safety_mode: "normal" | "conservative"
}

capability_install_target ​

Install and enable either an existing capability id or one or more SKILL.md records from a local path, a direct HTTP(S) URL, or a GitHub repository. GitHub repositories import a root SKILL.md and files matching skills/*/SKILL.md (up to 64 records). Imported records retain their source, qualification, capsule, and installation metadata.

The native daemon commits catalog, binding, and actor slash-visibility state before appending one capability.changed event to the target Group ledger for the complete install batch. Semantically unchanged reinstalls do not append a duplicate event. Failed or rolled-back installs do not append one either. Event publication is a recoverable notification boundary: if the state commit succeeds but the ledger append fails, the operation remains successful and reports event_publish_error; Web clients catch up the authoritative slash-command capability view when their global event stream opens or reconnects.

Args:

ts
{
  group_id: string
  target: string
  actor_id?: string
  by?: string
  scope?: "actor" | "group" | "session"
  ttl_seconds?: number
}

Result:

ts
{
  action_id: string
  group_id: string
  actor_id: string
  target: string
  target_kind: "capability_id" | "local_path" | "url" | "github"
  scope: "actor" | "group" | "session"
  installed_capability_ids: string[]
  enabled_capability_ids: string[]
  use_ready_capability_ids: string[]
  requires_setup: boolean
  refresh_required: boolean // true only when the effective runtime/slash catalog changed
  state: "ready" | "needs_setup"
  event_publish_error?: string
}

capability_uninstall ​

Revoke capability bindings for the target group, mark the capability removed from that group's catalog view, remove current-group actor autoload references, and remove runtime cache when no other group/actor bindings remain. The catalog record, block policy, other groups, and profile defaults are preserved. Use capability_source_delete for an explicit global deletion of records owned by a removable import source.

Args:

ts
{
  group_id: string
  capability_id: string
  reason?: string
  by?: string
  actor_id?: string
}

Result:

ts
{
  action_id: string
  group_id: string
  actor_id: string
  capability_id: string
  state: "ready"
  removed_record: boolean
  removed_bindings: number
  removed_blocked?: number
  removed_group_marker: boolean
  removed_installation: boolean
  removed_runtime_bindings?: number
  removed_recent_success?: boolean
  removed_actor_autoload: number
  removed_profile_autoload: number
  cleanup_skipped_reason?: "cleanup_skipped_capability_still_bound"
  refresh_required: boolean
  refresh_mode?: "relist_or_reconnect"
  wait?: "relist_or_reconnect"
}

capability_source_delete ​

Explicitly delete every catalog record owned by a removable import source and clean its bindings, runtime state, actor autoload references, and profile defaults across all groups. Built-in and curated sources are protected. Only the user or a group foreman may perform this global operation.

Args:

ts
{
  group_id: string
  source_id: "manual_import" | "agent_self_proposed" | "github_import" | "url_import" | "local_import"
  reason?: string
  by?: string
  actor_id?: string
}

Result:

ts
{
  group_id: string
  actor_id: string
  source_id: string
  removed_records: number
  removed_capability_ids: string[]
  removed_runtime_bindings: number
  removed_installations: number
  removed_actor_autoload: number
  removed_profile_autoload: number
}

capability_tool_call ​

Invoke an enabled dynamic external capability tool by synthetic tool name.

Args:

ts
{
  group_id: string
  actor_id?: string
  by?: string
  tool_name: string
  arguments?: Record<string, unknown>
}

Result:

ts
{
  tool_name: string
  capability_id: string
  result: Record<string, unknown>
}

group_show ​

Args:

ts
{ group_id: string; detail?: "summary" | "full" }

Result:

ts
{ group: Record<string, unknown> } // group.yaml content, redacted

group_preamble_get ​

Read the effective group startup preamble. A non-empty group override replaces the built-in preamble body on the next preamble delivery; the fixed CCCC identity and protocol frame remains in place.

Args:

ts
{ group_id: string }

Result:

ts
{
  group_id: string
  source: "builtin" | "home"
  filename: "CCCC_PREAMBLE.md"
  overridden: boolean
  content: string
}

group_preamble_set ​

Create or replace the non-empty group preamble override. The UTF-8 encoded content must not exceed 512 KiB. Existing sessions that have already received their preamble are not reinjected; start a fresh session when the new guidance must apply immediately. group_reset creates a new group id and does not carry this override forward, so provisioners must set the desired preamble on the replacement group before starting its actors. This operation manages prompt content only; consumers requiring a distinct standby turn must observe the actor return to waiting or idle before sending the authoritative mission.

Args:

ts
{ group_id: string; content: string; by?: string }

Result: the group_preamble_get result plus changed: boolean. When changed is false, the stored override is not rewritten.

group_preamble_reset ​

Delete the group override and restore the built-in preamble body. The explicit confirmation avoids accidental removal.

Args:

ts
{ group_id: string; confirm: "preamble"; by?: string }

Result: the group_preamble_get result plus changed: boolean.

group_help_get ​

Read the effective group collaboration reference. The built-in ## Canonical Message Delivery section is always authoritative and is composed with the group's CCCC_HELP.md as an additive overlay; an overlay section with the same heading is ignored. When actor_id is supplied, the daemon MUST apply the document's ## @role:, ## @actor:, and ## @voice_secretary visibility rules before returning markdown. Runtime-only MCP addenda are outside this operation and MAY be appended by the MCP adapter. user and the foreman may request any actor's effective help; a peer may request only its own actor view and MUST NOT use this operation to read another actor's scoped note.

Args:

ts
{ group_id: string; actor_id?: string; by?: string }

Result:

ts
{
  group_id: string
  actor_id: string | null
  source: "builtin" | "home"
  source_path: string
  filename: "CCCC_HELP.md"
  overridden: boolean
  markdown: string
}

actor_notes_get ​

Read actor-scoped notes from ## @actor: <actor_id> blocks in the canonical group CCCC_HELP.md. user and the foreman may read any actor or omit target_actor_id to list all notes. A peer MUST provide its own actor id and MUST NOT read another actor's note.

Args:

ts
{ group_id: string; target_actor_id?: string; by?: string }

Result when a target is supplied:

ts
{
  target_actor_id: string
  content: string
  source: "builtin" | "home"
  path: string
}

Result when listing:

ts
{
  actor_notes: Array<{ actor_id: string; content: string }>
  source: "builtin" | "home"
  path: string
}

actor_notes_set ​

Create or replace one existing actor's scoped note in the canonical group help document. Only user or the foreman may mutate actor notes. The daemon MUST preserve common, role, other-actor, Voice Secretary, and unknown tagged blocks, write atomically, and MUST NOT create a Context or actor-record copy.

Args:

ts
{ group_id: string; target_actor_id: string; content: string; by?: string }

Result: the targeted actor_notes_get result plus changed: boolean.

actor_notes_clear ​

Remove one existing actor's scoped note without changing other help content. Permission and preservation rules are identical to actor_notes_set.

Args:

ts
{ group_id: string; target_actor_id: string; by?: string }

Result: the targeted actor_notes_get result plus changed: boolean.

group_create ​

Args:

ts
{ title?: string; topic?: string; by?: string }

Result:

ts
{ group_id: string; title?: string; event?: CCCSEventV1 }

group_update ​

Args:

ts
{ group_id: string; by?: string; patch: { title?: string; topic?: string } }

Result:

ts
{ group_id: string; group: Record<string, unknown>; event: CCCSEventV1 }

group_delete ​

Args:

ts
{ group_id: string; by?: string }

Result:

ts
{ group_id: string }

Notes:

  • Successful deletion MUST retire all Direct records and disposable catalogs owned by the deleted Group, preserve retired IDs, and release their relation quota. Reset uses the same deletion cleanup; it MUST NOT transfer those grants to the replacement Group.
  • Successful deletion MUST retire every Web Model conversation binding and pending pairing belonging to the deleted Group, without revoking the shared connector or other Groups’ authority. A failure that leaves the group registered and available MUST preserve its pre-delete connector authority.
  • Successful deletion MUST retire every local external-space binding, queued job, and referenced job payload owned by the deleted group. It MUST NOT delete the user's remote notebook or other provider space. A failure that leaves the group registered and available MUST restore the pre-delete local binding and queue state.

group_use ​

Set the active scope for a group using path (must already be attached).

Args:

ts
{ group_id: string; path: string; by?: string }

Result:

ts
{ group_id: string; active_scope_key: string; event: CCCSEventV1 }

Workspace Web clients must treat group.set_active_scope, group.attach, and group.detach_scope as invalidating their active workspace view and reconcile the current Group document, rather than applying historical event scope fields. The Web workspace path/list/read/write/content requests bind scope_key and scope_url to that document; JSON file reads return both values and saves echo the opened identity. Missing identity is rejected with HTTP 400, and a changed key or attached URL with HTTP 409 (workspace_scope_changed), before resolving the relative path. The checked Group snapshot owns the entire filesystem operation; a subsequent scope switch cannot retarget an in-flight write. The digest detects content changes within that workspace and does not establish workspace identity.

GET /api/v1/groups/{group_id}/workspace/path resolves an existing path under the same scope and access checks, returning {scope_key, scope_url, path, is_dir}. The returned path is canonical and workspace-relative; an empty path names the root. The endpoint reads metadata only. Web opens files in the viewer and reveals folders in the tree without replacing the current file or draft. Missing paths, wrong path types, and escaped paths remain distinct errors; only a boundary violation is outside_scope. This lookup creates no daemon work or ledger event.

Workspace listings mark symbolic links with is_symlink: true. An inaccessible entry carries unavailable: missing, outside_scope, unreadable, or unsupported (not a regular file or directory). External link targets and their metadata are not disclosed. This is a listing-time observation; reads and downloads still validate the scope and path independently. Web keeps path-copy actions available but disables opening, downloading, attaching and pinning an unavailable entry. Refreshing the directory reevaluates its availability.

GET /api/v1/groups/{group_id}/workspace/content reads original file bytes with path, scope_key, and scope_url, under the same Group/exhibit and Connect-frame authorization as workspace text reads. download=true forces an attachment. It supports HEAD and single byte ranges (206/416), without the JSON text limit or whole-file buffering. Requests containing If-Range receive the current full representation because this mutable-file endpoint exposes no strong validator. Every subsequent range request rechecks scope and access; an already admitted response retains its opened file. Responses use no-store and nosniff; inline raw responses are limited to images, audio, video and PDF. SVG remains sandboxed; PDF uses its exact MIME without CSP sandbox, allowing the native browser PDF viewer. Content-Disposition supplies the original safe filename for inline viewing and explicit downloads. Other raw content is downloaded, never rendered as active same-origin HTML. Text-sized Markdown, tables and static HTML are rendered by Web from the existing bounded text read; HTML remains scriptless and scoped resource URLs retain authorization and workspace identity. Media support does not create a daemon operation, ledger event, cloud copy or transcoder.

Workspace mime_type is a filename-derived hint, not proof of media content. JSON reads detect binary content independently; above the 1 MiB text limit they sample at most 8 KiB, accepting a UTF-8 character split at the sample boundary, and retain truncated: true without inlining content or a save digest. Web uses this text/binary result before choosing an audio or video player, so TypeScript .ts/.mts and text playlists remain text while binary transport streams retain media preview. Oversized text keeps the existing size-limit notice and download. SVG and PDF retain their dedicated previews. A rename updates the MIME hint while retaining the opened content classification and any unsaved draft.

Workspace management uses the same Group, exhibit, scope and Connect-frame checks. Paths must be exactly representable as UTF-8. Listings with non-UTF-8 entry names and resolutions to non-UTF-8 canonical paths fail explicitly; lossy conversion must not publish another entry's identity. Removing a UTF-8-named link remains an entry operation and does not require a readable target. POST /api/v1/groups/{group_id}/workspace/entries accepts scope_key, scope_url and one typed operation: {operation: "create", path, directory: boolean}, {operation: "move", path, destination}, or {operation: "delete", path}. Paths are workspace-relative. Creation and move never overwrite an existing entry; collisions return HTTP 409 (workspace_entry_exists). Moves require an existing parent and native exclusive-rename support; there is no copy/delete or overwrite fallback. Successful responses return the normalized entry path, plus destination and its path-derived mime_type for a move. Web applies the returned MIME to the moved file in both the visible editor and cached drafts without reloading unsaved contents; moving a folder preserves its descendants' file types. Entry operations resolve the parent under the scope, retaining the final symlink itself: moving or removing a link does not move or remove its target. Workspace-root and Git-metadata mutations are rejected. Directory removal is recursive and permanent, without following contained links. A failed recursive deletion may have removed some entries; Web retains drafts and refreshes the tree rather than claiming rollback. Text saves and these operations share process-local serialization; this does not lock out external programs.

POST /api/v1/groups/{group_id}/workspace/upload takes scope_key, scope_url, path and bytes in its query and raw file bytes in its body. The declared and received lengths must match and must not exceed 100 MiB. An owned temporary file in the destination directory is published without replacement only after receipt, flush, and a fresh scope/token/Connect-frame check. Failed or canceled requests clean up their temporary file relative to the originally opened parent directory, even if another request or external program has moved that directory. Publication checks the staged file's identity as well as its destination; a recreated old path cannot redirect the upload or its cleanup. Multi-file/folder uploads are sequential browser batches, limited to 1,000 entries and 100 MiB total; completed entries remain when later entries fail or the user stops. Existing directories are not implicitly merged. Web keeps uploads separate from composer attachments. Tree moves use the same entry operation as the menu, and successful moves transfer affected drafts to the new paths. An internal drag carries a one-use random token issued by the current Files panel; its entry identity remains in that panel's memory. Drop/end, scope changes and unmount retire it. Self-reported origin or workspace metadata from a foreign page does not authorize a move.

GET /api/v1/groups/{group_id}/workspace/changes takes the scope identity and returns {repository, branch, entries, limited}. Entries contain workspace-relative path, index/worktree status characters, optional in-scope previous_path, and untracked, conflicted, directory flags. Native Git status is bounded to the active workspace, including when it is a repository subdirectory. A non-repository is distinct from a failed or timed-out query. At most 2,000 entries are returned; limited explicitly marks a shortened list. Queries are user-driven, without background scans or daemon work.

GET /api/v1/groups/{group_id}/workspace/diff additionally takes path and side: "worktree" | "staged", returning {patch, limited}. Only a currently listed change can be selected. Worktree compares saved bytes with the index; staged compares the index with HEAD, including an unborn branch. Untracked/conflicted entries open in Files. Git uses literal pathspecs, disables external diff/text conversion and rename expansion, and never changes the index. A renamed file can appear as an addition/deletion in its patch to avoid pulling an out-of-scope source into the view. Output is limited to 1 MiB/10,000 lines per diff, with an explicit limit result rather than a partial patch. Binary or metadata-only changes remain visible as Git text. No stage, unstage, discard, commit, branch-switch, pull or push operation is exposed.

group_detach_scope ​

Args:

ts
{ group_id: string; scope_key: string; by?: string }

Result:

ts
{ group_id: string; event: CCCSEventV1 }

group_set_state ​

Args:

ts
{ group_id: string; state: "active" | "idle" | "paused"; by?: string }

Notes:

  • stopped is not a valid group_set_state value in daemon IPC v1.
  • Higher-level surfaces (CLI/MCP) MAY expose stopped as a convenience alias that maps to group_stop.
  • While a group remains paused, the daemon MUST NOT submit queued chat.message or system.notify work to actor runtimes. A user-authored Send or Request Reply is an explicit use action: it MUST first resume the group to active, enable its addressed actors, and then deliver through the normal runtime path. Mail does not resume the group. Canonical unread work remains in the ledger and MAY be surfaced through one bounded recovery notice after the group returns to active or idle.

Result:

ts
{ group_id: string; state: string; event: CCCSEventV1 }

group_settings_update ​

Update group-scoped messaging/automation/delivery/transcript settings.

Args:

ts
{ group_id: string; by?: string; patch: Record<string, unknown> }

Patch keys used by CCCC include:

  • Messaging: default_send_to
  • Delivery: mail_notice_after_seconds (default 1800, zero disables), reply_notice_after_seconds (default 900, zero disables)
  • Automation: actor_idle_timeout_seconds, keepalive_delay_seconds, keepalive_max_per_actor, silence_timeout_seconds, help_nudge_interval_seconds, help_nudge_min_messages
  • Terminal transcript: terminal_transcript_visibility, terminal_transcript_notify_tail, terminal_transcript_notify_lines

Result:

ts
{ group_id: string; settings: Record<string, unknown>; event: CCCSEventV1 }

assistant_state ​

Read the group-scoped state for first-party built-in assistants. Voice Secretary service-local ASR runs in-process through the Rust sherpa-onnx binding. The native runtime is linked into the CCCC binary; model weights remain explicit, checksummed downloads under CCCC_HOME/cache/voice-models.

Voice Secretary configuration (enabled and config) remains in group.yaml:assistants.voice_secretary. Durable workflow records live in groups/<group_id>/state/assistants.json: lifecycle, durable health, sessions, prompt drafts/requests, and ask requests. Process observations such as PID, port, live service/socket state, and actor handles MUST NOT be persisted there. Implementations MUST preserve the reserved rust_state object when updating the common records. A legacy Rust workflow embedded in group.yaml:assistants is imported canonical-first; after the canonical file commits, only assistant configuration remains in group.yaml.

Args:

ts
{
  group_id: string
  assistant_id?: "voice_secretary"
  view?: "voice_session" | string
  session_id?: string
  document_path?: string
  suppress_retry_notify?: boolean
}

view="voice_session" is the canonical session projection used by the Web meeting view. With session_id, it returns that document-capture session. Without session_id, document_path first resolves the durable cross-session transcript at $CCCC_HOME/voice-secretary/<group_id>/documents/<document_id>/transcript.jsonl; when no document transcript exists, it falls back to the latest matching session in state/assistants.json. Prompt-refinement, composer, and instruction semantic inputs are never projected as meeting transcript. The document transcript projection has source="document_transcript" and may aggregate rows from several recording sessions.

Specialized result for view="voice_session":

ts
{
  group_id: string
  session: {
    session_id: string
    capture_mode: "document"
    document_path?: string
    status?: string
    segments: Array<Record<string, unknown>>
    transcript?: string
    diarization?: Record<string, unknown>
    source?: "document_transcript"
  } | Record<string, never>
}

Result:

ts
{
  group_id: string
  assistants?: Array<Record<string, unknown>>
  assistants_by_id?: Record<string, unknown>
  assistant?: Record<string, unknown>
  proposals?: Array<Record<string, unknown>>
  proposals_by_id?: Record<string, unknown>
  documents?: Array<Record<string, unknown>>
  documents_by_path?: Record<string, unknown>
  active_document_path?: string
  capture_target_document_path?: string
  documents_by_id?: Record<string, unknown>      // daemon sidecar/internal compatibility only
  active_document_id?: string                    // daemon sidecar/internal compatibility only
  capture_target_document_id?: string            // daemon sidecar/internal compatibility only
  new_input_available?: boolean
  service_runtime?: Record<string, unknown>
  service_models?: Array<Record<string, unknown>>
  service_models_by_id?: Record<string, unknown>
}

service_runtime is read-only engine metadata with the stable runtime ID sherpa_onnx_streaming, readiness, and the linked sherpa-onnx version. The engine ships inside the CCCC executable and has no independent install/remove lifecycle; only voice models are downloaded or removed. Voice model records may include installed_manifest_sha256, update_available, last_update_error, and artifact source fields (url, sha256, archive) so model updates remain explicit and inspectable.

assistant_settings_update ​

Update group-scoped built-in assistant settings.

When voice_secretary.enabled=true, the daemon also materializes a hidden internal actor with internal_kind="voice_secretary" and actor_id="voice-secretary". That actor is a distinct assistant identity, not the foreman and not a normal peer. Its startup runtime config (Runtime-derived execution surface, command, env/secrets, scope, submit behavior) is copied from the current stable foreman actor so the user does not configure a second runtime profile. The foreman's enabled/running state does not affect assistant config inheritance. If no foreman actor exists, enabling Voice Secretary fails. An explicit enabled=true request also enables the internal actor and, if the group is already running, starts it as needed. Saving configuration alone MUST NOT start a stopped actor. Startup confirmation and health.actor.running MUST use the runtime owner's live state; a managed session does not return a traditional PTY launch status, and a retained error record does not prove it is running. Structured external runtimes retain their enabled/Group-running semantics. A failed start rolls back assistant settings, actor configuration, and private env. Disabling Voice Secretary stops/removes the actor and its private env.

Args:

ts
{
  group_id: string
  by?: string
  assistant_id: "voice_secretary"
  patch: {
    enabled?: boolean
    config?: {
      capture_mode?: "browser" | "service"
      recognition_backend?: "mock" | "assistant_service_local_asr" | "browser_asr" | "external_provider_asr"
      recognition_language?: "auto" | string
      retention_ttl_seconds?: number
      auto_document_enabled?: boolean
      document_default_dir?: string
      auto_document_quiet_ms?: number
      auto_document_min_chars?: number
      auto_document_max_window_seconds?: number
      service_model_id?: string
      tts_enabled?: boolean
    }
  }
}

browser_asr means browser-managed speech recognition and does not guarantee browser-device-local model execution. assistant_service_local_asr means ASR runs on the daemon host through native Rust and uses an installed local ASR model. The returned assistant health may include health.service with status, alive, ready, selected_model_id, model, runtime, and streaming_backend so Web can show whether service-local ASR is actually usable. service_model_id is optional and selects a daemon-managed local ASR model for on-demand install/use. recognition_language="auto" means the browser/client chooses the best language hint; otherwise callers should pass a BCP-47-like tag such as zh-CN, en-US, or ja-JP. auto_document_enabled=true is the default path: stable transcript segments are compacted into the Voice Secretary input stream, then the voice-secretary runtime actor pulls unread input and edits the working markdown document directly in the repository. auto_document_quiet_ms is the client silence window before flushing speech into that semantic lane; auto_document_min_chars and auto_document_max_window_seconds are daemon-side guardrails that keep long continuous speech from waiting forever for a pause. The runtime actor should treat transcript as source material for evidence-bounded reconstruction: it may use transcript, group context, existing documents, common knowledge, and verified lightweight research to produce a coherent artifact, but must not fabricate facts and should compactly mark low-confidence entities, numbers, quotations, or dates. The document loop should be incremental and non-lossy: each unread input batch should be organized into the best current document structure while preserving useful concrete details, and idle review should refine/reorganize/enrich rather than replace detail-rich material with a short executive summary. The daemon does not track per-job completion: it stores an input cursor, nudges the actor when unread input exists, and sends idle-review nudges only on recording stop or after enough new transcript input plus the group cooldown (default: stop flush immediately, otherwise 8 new transcript input flushes and at least 5 minutes since the previous idle review). If the group has an active workspace scope, document_default_dir (default docs/voice-secretary) is resolved under that workspace; otherwise the daemon falls back to CCCC_HOME. Raw transcript/source/input sidecars stay in CCCC_HOME. external_provider_asr must remain explicit opt-in.

The semantic input authority is $CCCC_HOME/voice-secretary/<group_id>/input_events.jsonl; its daemon-owned read/delivery cursor and retry timing live in the sibling input_state.json. Implementations MUST NOT maintain an engine-private sequence or cursor. The former Rust inputs.jsonl and groups/<group_id>/state/assistants.json:rust_state.input_* shape is a one-way migration source: canonical input/state commit first, then the legacy log and cursor fields are retired. If independently written streams must be merged, migration may conservatively replay an already-read item but MUST NOT advance across or skip an unread item.

Result:

ts
{ group_id: string; assistant: Record<string, unknown>; event: CCCSEventV1 }

assistant_voice_model_install ​

Download and verify a daemon-managed local Voice Secretary ASR model into CCCC-owned cache storage. Built-in releases include a default model manifest; tests and local development may add a local overlay at CCCC_HOME/config/voice-models.json. Each artifact entry must include a fixed URL and sha256. Reinstalling/updating a model downloads into staging storage and replaces the active model only after all artifacts verify successfully.

Args:

ts
{
  group_id: string
  by?: string
  model_id: string
}

Result:

ts
{
  group_id: string
  assistant: Record<string, unknown>
  model: {
    model_id: string
    status: "not_installed" | "downloading" | "ready" | "failed" | "unknown"
    install_dir?: string
    installed_at?: string
    updated_at?: string
    error?: Record<string, unknown>
    update_available?: boolean
    installed_manifest_sha256?: string
  }
}

HTTP Voice Secretary transcription ​

Transcribe a push-to-talk audio payload through the daemon-managed first-party Voice Secretary runtime. This endpoint only returns transcript text and service health; it does not create a chat message, proposal, or working document by itself. Call assistant_voice_transcript_append after transcription so the daemon can append stable transcript source material and update the current working document.

Request:

ts
POST /api/v1/groups/{group_id}/assistants/voice_secretary/transcriptions
  ?language={language}&by={actor_id}
Content-Type: audio/pcm | audio/wav | application/octet-stream

<streamed binary audio body>

Preconditions:

  • voice_secretary is enabled for the group.
  • recognition_backend is assistant_service_local_asr.
  • The selected offline service_model_id is installed and its manifest exposes a supported sherpa-onnx model configuration. HTTP transcription accepts mono PCM16 or WAV up to 100 MiB. The HTTP body and WebSocket PCM16 frames are streamed to auto-deleted temporary files; browser service capture sends binary PCM16 WebSocket frames.

Result:

ts
{
  group_id: string
  assistant: Record<string, unknown>
  transcript: string
  mime_type: string
  language?: string
  bytes?: number
  backend: "assistant_service_local_asr"
  service: Record<string, unknown>
  asr?: Record<string, unknown>
}

WebSocket Voice Secretary transcription ​

The service-local ASR browser transport keeps one recording lease and one microphone capture active while raw PCM is rolled into bounded server-side files:

ts
GET /api/v1/groups/{group_id}/assistants/voice_secretary/transcriptions/ws
  ?owner_id={owner_id}&lease_id={lease_id}

After upgrade, the client sends a JSON start command, then 16 kHz mono PCM16 binary frames, and finally a JSON stop command. The server returns a ready event whose recording_segment_duration_ms is currently 1500000. Whenever a full segment has been flushed and data-synced, the server emits:

ts
{
  type: "recording_segment_saved"
  ok: true
  seq: number
  segment_index: number
  start_ms: number
  end_ms: number
  duration_ms: number
  bytes: number
}

Segment rollover MUST NOT stop live recognition or require a new microphone capture. Rust stores 48,000,000 PCM bytes per segment (25 minutes) and caps one WebSocket session at 800 MiB (about 7 hours 17 minutes). On stop or an unexpected disconnect, persistent recordings longer than 30 seconds MUST defer final transcription when speaker analysis is available. Short persistent recordings MAY run immediate final ASR. Final ASR paths that cannot defer MUST process segment files sequentially and reuse one offline recognizer per recording segment across inference ranges no longer than 30 seconds. When final ASR is deferred because the recording is long or the native inference worker is occupied, WebSocket stop MUST complete promptly with final_asr_status.status set to deferred_to_speaker_analysis, retain the durable live transcript, and queue speaker analysis; temporary worker occupancy MUST NOT permanently skip speaker analysis or retain the recording lease. A final ASR path that cannot defer but finds the native inference worker occupied MUST bound its wait well below the recording lease TTL and complete stop with an asr_busy final_asr_text error if the worker stays occupied, so a queued stop never outlives its lease. HTTP upload transcription MAY retain a fail-fast busy response. The final_asr_text event keeps the combined text in timeline order and includes a segments array with each inference range's status and its owning recording_segment_index. If at least one range succeeds and another fails, the event keeps ok=true so the available text is retained, and MUST also report partial=true plus failed_segment_count; clients MUST surface that incompleteness rather than presenting the text as a complete transcript. Speaker analysis is likewise sequential so native diarization holds at most one segment waveform at a time. Multi-segment speaker results MUST NOT imply cross-segment identity matching; Rust marks them with speaker_identity_scope="recording_segment".

assistant_voice_recording_lease ​

Acquire, refresh, release, or inspect the daemon-owned Voice Secretary recording lease. Web clients may keep a local browser lock for fast UX debouncing, but the daemon lease is the final cross-tab / cross-browser / cross-device guard that prevents two Voice Secretary recording streams from running at the same time. The lease is TTL-based so a crashed tab or disconnected browser eventually expires without manual cleanup.

The service-local ASR WebSocket requires the active owner_id and lease_id as query parameters and revalidates them while audio is streaming. Opening the transcription WebSocket directly cannot bypass the daemon lease. Lease mutations match group_id, owner_id, and lease_id; public status and conflict payloads redact lease_id. The stable browser owner identifies the lease holder, while every recording uses a fresh session_id.

Args:

ts
{
  group_id: string
  by?: string
  action: "acquire" | "heartbeat" | "release" | "status"
  owner_id?: string        // required for acquire/heartbeat/release
  lease_id?: string        // returned by acquire; required to refresh/release that acquisition
  ttl_seconds?: number     // default 30; bounded by the daemon
  capture_mode?: string
  recognition_backend?: string
  dispatch_target?: string
}

Result:

ts
{
  group_id: string
  action: string
  acquired: boolean
  released: boolean
  lost: boolean
  lease_id?: string        // only returned to the acquiring/refreshing owner
  lease?: {
    owner_id: string
    group_id: string
    group_title?: string
    capture_mode?: string
    recognition_backend?: string
    dispatch_target?: string
    by?: string
    created_at?: string
    updated_at?: string
    expires_at?: string
  }
}

If another live lease exists, acquire / heartbeat returns assistant_voice_recording_busy with details.active_lease. Every successful acquire creates a fresh lease_id, including when the owner_id matches the active lease, so cleanup from an older connection cannot release its replacement. heartbeat only refreshes the matching active owner_id + lease_id; it never creates a new lease. Stale heartbeat / release requests return lost or released=false without modifying a newer lease. An omitted heartbeat metadata field preserves the value from the active lease. The transcription WebSocket binds its start frame to the lease's capture_mode, recognition_backend, and dispatch_target; changing capture scope requires a new lease.

The daemon MUST serialize lease mutations and every read that may expire and clear the lease through $CCCC_HOME/state/voice_secretary_recording_lease.json.lock. A process-local lock alone is insufficient because daemon and Web processes may use the same home. acquire and heartbeat MAY operate while Voice Secretary is disabled only when the effective dispatch_target is composer; an omitted heartbeat target inherits the active lease target. This direct-dictation path MUST NOT create Voice Secretary input, session, document, or diarization state.

assistant_voice_transcript_append ​

Append a stable transcript segment for Voice Secretary. Web/browser ASR and service-local ASR converge here. The daemon writes stable segments to $CCCC_HOME/voice-secretary/<group_id>/<session_id>/transcripts/segments.jsonl, updates the bounded shared session projection in groups/<group_id>/state/assistants.json, appends final document-capture rows to $CCCC_HOME/voice-secretary/<group_id>/documents/<document_id>/transcript.jsonl, and by default appends a semantic input event for the current Voice Secretary markdown working document. The working document is a user-facing repo artifact; raw transcript/source/revision sidecars remain in CCCC_HOME. When new input is available, the daemon emits a targeted system.notify to voice-secretary with context.kind="voice_secretary_input" and a daemon-owned input_envelope. The envelope is the canonical work item delivered to every actor runtime; assistant_voice_document_input_read / cccc_voice_secretary_document(action="read_new_input") remains a legacy, recovery, and debugging entrypoint. Input append is durable before runtime actor wake-up; if wake-up fails, the input remains readable and the API reports the best-effort wake error separately. If wake-up succeeds after the notify was created while the actor was stopped, the daemon re-dispatches that same notify through the actor's normal runtime input so lazy startup instructions are included.

The group operation validates or creates the Markdown target before committing transcript/session/input state. Retrying the same session_id and segment_id is idempotent for the stable session, document transcript, and semantic input records. Document paths must be repository-relative .md paths and must not traverse symbolic links.

Idempotency is checked against the complete semantic input log, not the bounded session display window. If the input log was committed but its ledger input or notify event was interrupted, retrying the same segment reuses the canonical input record and completes only the missing delivery work.

Transcript producers MAY attach the following revision metadata:

  • transcript_stage: live or final. When omitted, the daemon infers final only from trigger.recognition_backend=assistant_service_local_asr_final; other legacy segments are treated as live.
  • supersedes_segment_ids: segment IDs in the same session that the new record replaces in the current projection.
  • supersede_stage: "live": daemon shorthand for a stable final segment (is_final=true) that resolves all current live segment IDs in the session into supersedes_segment_ids before committing and keeps race-late live checkpoints in that session raw-only.
  • source_model_id: the producer model identity retained with the raw record.
  • revision_only: true: valid only for a stable final segment (is_final=true). The daemon retains this marker with the raw segment and updates transcript projection. Under the input-state lock it MUST reuse an existing ASR input for that session; when none exists and automatic document input is enabled, it MUST create and deliver the final segment as the session's first semantic input.

Revision records are append-only. The daemon MUST retain superseded live rows in session and document transcript sidecars, while the bounded session transcript projection and Web meeting view omit them. A normal document-mode WebSocket stop and a successfully finalized disconnect MUST persist the final SenseVoice result as a revision-only record superseding the session's live checkpoints. A partial final result MUST NOT supersede live checkpoints. A document-mode final_asr_text reports transcript_persistence as persisted, skipped_partial, or failed, with transcript_persisted carrying the boolean commit result. Callers MUST retain or retry their fallback text until persistence is confirmed. If final ASR fails, the existing live projection remains valid.

The public document identity for Voice Secretary APIs is document_path, a repository-relative markdown path. document_id may exist in daemon sidecar state as an implementation detail, but runtime actors and Web clients should route by document_path.

Repository markdown is the document-content authority. The canonical document registry and active selection live at $CCCC_HOME/voice-secretary/<group_id>/documents/index.json; implementations serialize mutations with the sibling index.json.lock. The former Rust groups/<group_id>/state/assistants.json:rust_state.documents/active_document_* shape is a one-way migration source. Canonical index entries win path conflicts, unique legacy entries are retained, and the legacy fields are removed only after the canonical index commits. Implementations MUST NOT keep an engine-private active-document selection.

assistant_index, assistant_voice_document_list, and assistant_voice_document_select reconcile repository Markdown edits into the daemon document index before returning. Reconciliation also discovers previously unindexed .md files under the effective document_default_dir, as runtime actors may create working documents directly in the repository; an archived or deleted indexed path MUST NOT be rediscovered as active. Reconciliation updates content, hash, character count, and revision only when file content changed. Missing files do not clear indexed content, and path/symbolic-link validation is applied before reading. The emitted assistant.voice.document reconciliation event is an auxiliary signal; index persistence and ledger append are not one atomic transaction.

Args:

ts
{
  group_id: string
  by?: string
  session_id: string
  segment_id?: string
  text?: string
  language?: string
  document_path?: string
  is_final?: boolean
  flush?: boolean
  trigger?: {
    trigger_kind?: "push_to_talk_stop" | "service_transcript" | "meeting_window"
    mode?: "dictation" | "meeting"
    capture_mode?: "browser" | "service" | string
    recognition_backend?: string
    client_session_id?: string
    input_device_label?: string
    language?: string
  }
}

Result:

ts
{
  group_id: string
  assistant: Record<string, unknown>
  session_id: string
  segment?: Record<string, unknown>
  segment_path?: string
  document?: Record<string, unknown>
  document_updated: boolean
  input_event?: Record<string, unknown>
  input_event_created: boolean
  input_notify_emitted: boolean
  input_notify_error?: string
  actor_woken?: boolean
  actor_wake_error?: string
  actor_notify_delivered?: boolean
  actor_notify_delivery_error?: string
}

assistant_voice_session_update ​

Persist a Web-owned completion projection (currently speaker diarization) into the canonical session authority. With completion_event, the daemon then appends the matching assistant.voice.session event under the same Group write permit. Web MUST NOT append the completion directly to the ledger. This is an internal daemon boundary used by browser capture; callers do not replace transcript segments through this operation.

Voice-session mutation is limited to the user, the assistant:voice_secretary principal, or a foreman allowed to update group settings. A session_id used for filesystem-backed state MUST be canonicalized to one safe path component or rejected before any state or filesystem mutation; caller-controlled absolute paths and . / .. components MUST never be joined into the Voice Secretary storage root.

Args:

ts
{
  group_id: string
  session_id: string
  by?: "assistant:voice_secretary" | string
  completion_event?: "diarization_ready" | "diarization_failed"
  patch: {
    status?: string
    document_path?: string
    audio_duration_ms?: number
    diarization_ready?: boolean
    diarization_artifact_path?: string
    diarization?: Record<string, unknown>
    diarization_error?: Record<string, unknown>
    error?: Record<string, unknown> | null
    latest_partial?: string
  }
}

Result:

ts
{ group_id: string; session: Record<string, unknown>; completion_event_id?: string }

When completion_event is supplied, patch.status MUST be closed and patch.diarization_ready MUST match the outcome. Invalid combinations MUST fail before state mutation. Omitting it preserves projection-only updates. An append failure MUST be reported even when session state was already saved; repeating the same completion is safe and emits at most one event per Group/session/outcome. A successful completion response MUST include its completion_event_id; a projection-only response from an older daemon is not completion confirmation. The Web completion caller retries transient I/O or unknown transport outcomes at most four times, then reports failure. This does not promise an atomic transaction across the state file and ledger, nor recovery after the Web host exits.

assistant_voice_session_transcript_clear ​

Clear the selected session display transcript and the matching durable document transcript. Document Markdown content is not deleted.

Args:

ts
{ group_id: string; session_id?: string; document_path?: string; by?: string }

Result:

ts
{ group_id: string; session_id: string; cleared: boolean }

assistant_voice_document_list ​

List active Voice Secretary working documents for the group. Archived documents are excluded unless include_archived=true.

Args:

ts
{ group_id: string; include_archived?: boolean }

Result:

ts
{
  group_id: string
  documents: Array<Record<string, unknown>>
  documents_by_id: Record<string, unknown>
  documents_by_path: Record<string, unknown>
  active_document_id?: string
  capture_target_document_id?: string
  active_document_path?: string
  capture_target_document_path?: string
}

assistant_voice_document_input_read ​

Read all unread Voice Secretary input events since the actor's last successful read. Reading advances the daemon-managed cursor immediately; the actor does not see or manage cursor/sequence values. This intentionally avoids a separate job-completion protocol. If the actor crashes after reading, the raw input log remains in CCCC_HOME for debugging/replay, but the normal live cursor has moved.

Args:

ts
{ group_id: string; by?: "voice-secretary" | "assistant:voice_secretary" }

Result:

ts
{
  group_id: string
  item_count: number
  document_count: number
  input_text: string
  input_batches: Array<{
    document_path: string
    filename?: string
    title?: string
    item_count: number
    kinds?: string[]
    intent_hints?: string[]
    languages?: string[]
    sources?: string[]
  }>
  documents: Array<Record<string, unknown>>
  has_new_input: boolean
}

assistant_voice_document_save ​

Save or create a Voice Secretary working markdown document. This is the daemon path used by Web when the user edits the document surface. The voice-secretary actor should normally edit repository-backed markdown directly at document_path; the MCP document tool intentionally has no save action. When content is omitted for an unindexed path, an implementation MUST NOT rewrite an existing repository file: it MAY read the file into the document index or reject the request. An empty file MAY be created only when the target does not already exist.

Args:

ts
{
  group_id: string
  by?: string
  document_path?: string
  workspace_path?: string
  title?: string
  content?: string
  status?: "active" | "archived"
  create_new?: boolean
}

Result:

ts
{ group_id: string; document: Record<string, unknown>; event: CCCSEventV1 }

assistant_voice_document_instruction ​

Append a user instruction for one active working document into the same Voice Secretary input stream used for ASR transcript. The daemon emits a targeted voice_secretary_input notify and the runtime actor works from the inline input_envelope. The daemon does not directly append the instruction to a document. Cross-peer handoff is intentionally handled only by assistant_voice_request, and only when the Voice Secretary decides the work belongs to foreman or one concrete peer.

Args:

ts
{
  group_id: string
  by?: string
  document_path: string
  request_id?: string
  input_append_id?: string
  instruction?: string
  source_text?: string
  trigger?: Record<string, unknown>
}

Result:

ts
{
  group_id: string
  assistant?: Record<string, unknown>
  document: Record<string, unknown>
  request_id: string
  input_append_id?: string
  ask_request?: Record<string, unknown>
  input_event?: Record<string, unknown>
  input_event_created?: boolean
  input_notify_emitted?: boolean
  input_notify_error?: string
  actor_woken?: boolean
  actor_wake_error?: string
  actor_notify_delivered?: boolean
  actor_notify_delivery_error?: string
  event?: CCCSEventV1
}

request_id identifies the logical Ask request. input_append_id identifies one durable append attempt. A caller retrying an accepted append MUST reuse both values. The daemon MUST then return the existing input with input_event_created=false and MUST NOT append a second semantic input, request event, or notification. When either value is omitted, the daemon may generate it and no retry guarantee exists until the caller retains the returned values.

assistant_voice_input_append ​

Append a general Voice Secretary Ask or create/update a composer refinement request. The daemon persists the request before emitting one targeted voice_secretary_input notification. This operation creates work for Voice Secretary; it does not create a prompt draft.

Args:

ts
{
  group_id: string
  by?: string
  kind: "voice_instruction" | "prompt_refine"
  request_id?: string
  input_append_id?: string
  instruction?: string
  text?: string
  source_text?: string
  voice_transcript?: string
  composer_text?: string
  operation?: "append_to_composer_end" | "replace_with_refined_prompt" | string
  composer_context?: Record<string, unknown>
  composer_snapshot_hash?: string
}

For voice_instruction, at least one of instruction/text or source_text must be non-empty. For prompt_refine, at least one of voice_transcript or composer_text must be non-empty. request_id groups a composer refinement and may be reused for intentional follow-up input. Each distinct follow-up MUST use a new input_append_id; an exact retry MUST reuse the prior one and follows the same no-duplicate rule as assistant_voice_document_instruction.

Result:

ts
{
  group_id: string
  assistant?: Record<string, unknown>
  request_id: string
  input_append_id?: string
  prompt_request?: Record<string, unknown>
  ask_request?: Record<string, unknown>
  input_event?: Record<string, unknown>
  input_event_created: boolean
  input_notify_emitted: boolean
  input_notify_error?: string
  actor_woken?: boolean
  actor_wake_error?: string
  actor_notify_delivered?: boolean
  actor_notify_delivery_error?: string
  event?: CCCSEventV1
}

assistant_voice_instruction_feedback ​

Report progress or the terminal result for an existing Voice Secretary Ask. Only the voice-secretary actor/principal may submit feedback.

Args:

ts
{
  group_id: string
  by?: "voice-secretary" | "assistant:voice_secretary"
  request_id: string
  status: "working" | "done" | "needs_user" | "failed"
  reply_text?: string
  result_text?: string
  message?: string
  document_path?: string
  artifact_paths?: string[]
  source_summary?: string
  checked_at?: string
  source_urls?: string[]
}

Result:

ts
{
  group_id: string
  assistant: Record<string, unknown>
  ask_request: Record<string, unknown>
  event: CCCSEventV1
}

assistant_voice_ask_requests_clear ​

Hide Ask history from the current projection. keep_active=true preserves pending and working requests in the visible result. Clearing is a display operation, not cancellation: the daemon MUST retain enough bounded state to accept later feedback for a cleared in-flight request. User-visible feedback may make that request visible again.

Args:

ts
{ group_id: string; keep_active?: boolean; by?: string }

Result:

ts
{
  group_id: string
  assistant: Record<string, unknown>
  ask_requests: Array<Record<string, unknown>>
  latest_ask_request?: Record<string, unknown>
  cleared_count: number
  removed_count: number
  kept_count: number
}

assistant_voice_prompt_draft_submit ​

Submit the Voice Secretary result for an existing prompt refinement request. Only voice-secretary / assistant:voice_secretary may call this operation. The daemon inherits a missing operation and composer snapshot hash from the request, stores the result as pending, and emits assistant.voice.prompt_draft. no_op=true stores no_change with empty draft text. Submission MUST NOT append another semantic input or emit another voice_secretary_input notification.

Args:

ts
{
  group_id: string
  by?: "voice-secretary" | "assistant:voice_secretary"
  request_id: string
  draft_text?: string
  no_op?: boolean
  summary?: string
  operation?: string
  composer_snapshot_hash?: string
}

draft_text is required unless no_op=true.

assistant_voice_prompt_draft_ack ​

Mark a submitted draft as applied, dismissed, or stale. Acknowledgement removes it from the active prompt_draft projection while retaining bounded request history.

Args:

ts
{
  group_id: string
  request_id: string
  status: "applied" | "dismissed" | "stale"
}

assistant_voice_request ​

Send a structured Voice Secretary action request to @foreman or one concrete actor without exposing normal chat.message send tools to the voice-secretary runtime actor. The daemon records an assistant.voice.request event and delivers a targeted system.notify with context.kind="voice_secretary_action_request". This is the default path for spoken "please do X / ask Y to do X" content; ordinary memo/document updates MUST stay in the Voice Secretary document surface.

Args:

ts
{
  group_id: string
  by?: "voice-secretary" | "assistant:voice_secretary"
  target?: "@foreman" | string   // one concrete actor id; no @all/user broadcast
  request_text: string           // concise actionable handoff, not raw transcript
  summary?: string
  document_path?: string
  artifact_paths?: string[]      // repo-relative produced docs/artifacts for user-visible links
  source_event_id?: string
  priority?: "low" | "normal" | "high" | "urgent"
}

Result:

ts
{
  group_id: string
  assistant: Record<string, unknown>
  request: Record<string, unknown>
  notify_event: CCCSEventV1
  event: CCCSEventV1
}

assistant_voice_document_archive ​

Archive a Voice Secretary working document. The markdown file is left in place; the assistant index hides it from the active document list, and later transcript ingress without an explicit document_path creates or selects another active document instead of appending to the archived one.

Args:

ts
{ group_id: string; by?: string; document_path: string }

Result:

ts
{ group_id: string; document: Record<string, unknown>; event: CCCSEventV1 }

assistant_voice_document_library ​

Read the group's stored Voice Secretary library, including archived documents. Deleted entries are excluded. Folders organize documents without changing their Markdown paths. This reads the index; workspace discovery and external-content reconciliation remain the responsibility of assistant_voice_document_list.

Args:

ts
{ group_id: string }

Result:

ts
{
  folders: Array<{ folder_id: string; name: string }>
  root_order: string[] // mixed folder:<folder_id> / document:<document_path> keys; initially []
  documents: Array<Record<string, unknown>> // includes content and optional folder_id
}

assistant_voice_document_library_update ​

Update library metadata or restore an archived document. Writers must be the user, a Foreman, or assistant:voice_secretary; omitted by means user. Operations serialize with other writes in the same Group and persist in the existing document index. These metadata updates do not append ledger events.

Args:

ts
{
  group_id: string
  by?: string
  action: "create_folder" | "rename_folder" | "remove_folder" | "reorder_root" | "rename" | "move" | "restore"
  name?: string          // create/rename: trimmed, 1–80 characters
  folder_id?: string     // folder rename/removal or move destination; empty move means root
  document_path?: string // rename/move/restore: registered, non-deleted document
  root_order?: string[]  // reorder_root: required array of mixed root item keys
}

Folder names must be unique within the Group. Removing a folder returns its documents to the root without deleting files. Renaming a document changes its display title only. Restore accepts archived documents and preserves their folder assignment; deleted documents cannot be restored. Result has the same shape as assistant_voice_document_library.

reorder_root replaces the saved display order. It requires an array of strings, removes duplicate keys (keeping the first), and drops keys for unknown folders or unregistered/deleted documents. An empty array clears the order. Ordering does not move documents or change their paths or archive status; clients apply it to visible root items. The Web tree places items absent from the saved order before ordered items, using its default order within that group.

assistant_voice_document_delete ​

Permanently remove a registered Voice Secretary Markdown file and mark its index entry deleted. Uses the same writer permission as library updates. Rejects traversal and symlink paths, and returns voice_recording_active while the Group holds a recording lease. Historical ledger events and transcript logs remain.

Args:

ts
{ group_id: string; by?: string; document_path: string }

Result:

ts
{ group_id: string; document: Record<string, unknown>; event: CCCSEventV1 }

The event is assistant.voice.document with action deleted. The file is moved to a temporary transaction location before the index and event are saved; either write failure attempts to restore the file and previous index. Failed rollback reports the retained recovery path. Successful writes remove that temporary copy; cleanup failures log a warning and retain it. Deleted paths reject subsequent saves, transcript appends and archive requests and are not rediscovered as active documents. The Web consumer clears quoted references and capture targets only after a successful response.

assistant_status_update ​

Update lifecycle/health for a built-in assistant service. The assistant principal (assistant:<assistant_id>) may update its own status; users/foremen may also update it for control-plane repair.

Args:

ts
{ group_id: string; by?: string; assistant_id: "voice_secretary"; lifecycle: "disabled" | "idle" | "running" | "working" | "waiting" | "failed"; health?: Record<string, unknown> }

Result:

ts
{ group_id: string; assistant: Record<string, unknown>; event: CCCSEventV1 }

group_automation_update ​

Replace group automation rules + snippets (scheduled system.notify).

Args:

ts
{
  group_id: string
  by?: string
  expected_version?: number
  ruleset: {
    rules: Array<{
      id: string
      enabled?: boolean
      scope?: "group" | "personal"
      owner_actor_id?: string | null
      to?: string[]
      trigger:
        | { kind: "interval"; every_seconds: number }
        | { kind: "cron"; cron: string; timezone?: string }
        | { kind: "at"; at: string } // RFC3339
      action?:
        | {
            kind?: "notify"
            title?: string
            snippet_ref?: string | null
            message?: string
            priority?: "low" | "normal" | "high" | "urgent"
          }
        | { kind: "group_state"; state?: "active" | "idle" | "paused" | "stopped" }
        | { kind: "actor_control"; operation?: "start" | "stop" | "restart"; targets?: string[] }
    }>
    snippets: Record<string, string>
  }
}

Result:

ts
{ group_id: string; ruleset: Record<string, unknown>; version: number; event: CCCSEventV1 }

group_automation_state ​

Get effective automation state for a caller.

Args:

ts
{ group_id: string; by?: string }

Result:

ts
{
  group_id: string
  ruleset: {
    rules: Array<Record<string, unknown>>
    snippets: Record<string, string>
  }
  status: Record<string, {
    last_fired_at: string
    last_error_at: string
    last_error: string
    next_fire_at: string
    completed: boolean
    completed_at: string
  }>
  supported_vars: string[] // exactly: interval_minutes, group_title, actor_names, scheduled_at
  version: number
  server_now: string
  config_path: string
}

Notes:

  • by as a peer receives a filtered view: group rules + own personal rules.
  • Rule IDs are non-empty and unique within a ruleset. Unknown fields and invalid trigger/action combinations are rejected instead of being persisted for one engine to ignore later.
  • group_state and actor_control actions require an at trigger. Actor callers may manage only notify rules; a peer may mutate only its own personal notification rule targeting itself.
  • The first tick of a newly enabled interval rule establishes its clock and does not fire immediately. A paused or stopped group runs no automation. An idle group runs user rules but suppresses the built-in standup rule.
  • Resume never catches up missed work: interval and cron clocks are rebased, missed one-time rules are completed without execution, and future one-time rules remain eligible.
  • A notification firing completes only after at least one system.notify has been appended durably for an enabled matching recipient. Recipient delivery does not require a currently running actor process. A successfully completed one-time rule is disabled.
  • group.yaml:automation and state/automation.json are the shared config and runtime authorities. The single-daemon process lock owns scheduling; engine handoff consumes these files and does not introduce a second scheduler lease or retry journal.

group_automation_manage ​

Incremental automation management with action list.

Args:

ts
{
  group_id: string
  by?: string
  expected_version?: number
  actions: Array<
    | { type: "create_rule"; rule: Record<string, unknown> }
    | { type: "update_rule"; rule: Record<string, unknown> }
    | { type: "set_rule_enabled"; rule_id: string; enabled: boolean }
    | { type: "delete_rule"; rule_id: string }
    | { type: "replace_all_rules"; ruleset: { rules: Array<Record<string, unknown>>; snippets: Record<string, string> } }
  >
}

Result:

ts
{
  group_id: string
  ruleset: Record<string, unknown>
  status: Record<string, Record<string, string>>
  supported_vars: string[]
  version: number
  server_now: string
  applied_actions: Array<Record<string, unknown>>
  changed: boolean
  event?: CCCSEventV1 | null
}

group_automation_reset_baseline ​

Reset automation ruleset to built-in baseline defaults.

Args:

ts
{ group_id: string; by?: string; expected_version?: number }

Result:

ts
{
  group_id: string
  ruleset: { rules: Array<Record<string, unknown>>; snippets: Record<string, string> }
  status: Record<string, Record<string, unknown>>
  supported_vars: string[]
  version: number
  server_now: string
  config_path: string
  event: CCCSEventV1
}

group_start ​

Resume the Group and run every actor whose desired enabled state is true. Actors explicitly disabled through actor_stop remain disabled.

Args:

ts
{ group_id: string; by?: string }

Result:

ts
{ group_id: string; started: string[]; event: CCCSEventV1 }

group_stop ​

Stop the Group's actor runtimes without changing any actor's desired enabled state. This is a group-level suspension; use actor_stop to disable one actor.

Args:

ts
{ group_id: string; by?: string }

Result:

ts
{ group_id: string; stopped: string[]; event: CCCSEventV1 }

8.4 Actors ​

actor_list ​

Args:

ts
{ group_id: string; include_unread?: boolean }

Result:

ts
{ actors: Array<Record<string, unknown>> } // includes at least id/title/runtime/enabled + role/running

actor_add ​

Args:

ts
{
  group_id: string
  actor_id?: string
  title?: string
  runtime?: string
  runtime_mode?: "default" | "acp" // optional headless ACP for Antigravity, Copilot, Devin and Cursor
  command?: string[]
  env?: Record<string, string>
  capability_autoload?: string[] // actor startup autoload capability ids
  capability_hidden?: string[] // actor-level skill menu hide preferences; does not disable capabilities
  env_private?: Record<string, string> // write-only secrets (stored under CCCC_HOME/state; never persisted into ledger)
  profile_id?: string            // optional Actor Profile link (runtime/command/submit/env + secrets)
  default_scope_key?: string
  submit?: "enter" | "newline" | "none"
  by?: string
}

Notes:

  • env_private is restricted to by="user" and values are never returned.
  • If env_private is provided (even empty), it is treated as authoritative for this create: it clears any existing private keys for that actor_id, then sets the provided keys.
  • profile_id links the actor to a global Actor Profile and applies profile-controlled runtime fields + profile secrets.
  • When profile_id is used, env_private is rejected (linked actor private env is profile-controlled).
  • The appended actor.add event starts that actor id's current generation. The daemon MUST initialize the new generation's read boundary at that append position, so events from before the add are not delivered as unread. Removing and later re-adding the same actor id starts a new generation at the later actor.add position.
  • Actor records also expose an opaque generation UUID for delayed remote recipient binding. Add assigns a fresh value, including when a supplied record came from an earlier Actor; update cannot overwrite it. Restart and ordinary edits preserve it. Existing records without this field use their original created_at identity (legacy:<created_at>) until they are recreated; browser ownership, pairing and tool routing MUST use that same identity and pairing MUST NOT assign a new generation after the browser opens. This does not replace the ledger-based inbox boundary above.
  • Multiple ChatGPT (web_model) and Grok Bot (grok_web_model) Actors MAY coexist, including linked Profiles and Group imports/resets. Runtime configuration does not grant connector authority. Conversation bindings MUST identify the exact Group, Actor and generation; copied/imported Actors MUST be unpaired.
  • A new actor generation MUST NOT inherit Web Model delivery preferences or persisted runner/turn status left by an earlier generation with the same actor id.
  • For a Web Model actor, successful add MUST establish the current generation's missing browser target as canonical empty state. A legacy actor-scoped browser shadow MUST NOT populate the new generation merely because it uses the same actor id.
  • Adding an enabled actor to an active or idle group MAY start it immediately and transition the group's runtime to running. Adding one to a paused or stopped group MUST only persist the actor and MUST NOT change the group lifecycle state.
  • When immediate startup is attempted, startup capability baselines follow the same rules as actor_start below.

Result:

ts
{
  actor: Record<string, unknown>
  event: CCCSEventV1
  running?: boolean
  start_event?: CCCSEventV1
  start_error?: string
}

actor_update ​

Args:

ts
{
  group_id: string
  actor_id: string
  by?: string
  patch: Record<string, unknown>
  profile_id?: string                      // attach/replace profile link
  profile_action?: "convert_to_custom"     // snapshot profile config + secrets, then unlink
}

Patch keys used by CCCC v0.4.x include:

  • Identity/UI: title
  • Runtime: runtime, runtime_mode, command, submit
  • Scope: default_scope_key
  • Enable/disable: enabled
  • Environment (use with care): env
  • Capability startup baseline: capability_autoload

A linked Profile owns runtime, runtime_mode, command, submit and its explicit environment. Actor-local title, notes, Web Model conversation bindings and capability_autoload remain editable; the Actor's capability baseline is additive to Profile defaults. A linked Actor MUST NOT merge dormant custom environment values over the Profile. Converting to custom snapshots the effective Profile configuration and secrets, replacing dormant custom secrets rather than reviving them. Private values remain outside events. Web editors MUST distinguish persisted Actor/Profile snapshots from unsaved private-env drafts. A successful Profile conversion MUST NOT discard pending secret edits; those edits are cleared only after their private-env write succeeds or the user leaves the editing session. Clients editing a command MUST preserve argument boundaries (including quotes, spaces and empty arguments) and SHOULD omit unchanged runtime fields.

Result:

ts
{ actor: Record<string, unknown>; event: CCCSEventV1 }

actor_remove ​

Args:

ts
{ group_id: string; actor_id: string; by?: string }

Notes:

  • Removing an actor ends that actor id's current generation. Actor-generation-scoped browser target, bootstrap, delivery receipt, delivery preference, and persisted runner/turn state MUST be retired before the operation reports success. A shared provider login profile MAY remain.
  • Every Web Model binding and pending pairing belonging to the removed Actor generation MUST be retired before success. Shared connector credentials and other Actors remain intact. Re-adding the same Actor ID MUST NOT restore an earlier generation’s authority.

Result:

ts
{ actor_id: string; event: CCCSEventV1 }

actor_start / actor_stop / actor_restart ​

Daemon-owned runtime resources MUST be retired after owner exit, including abrupt exit. On Unix, a daemon MAY use an EOF watchdog and a durable owned-process-group ledger to recover groups missed by graceful shutdown. Recovery MUST start only after acquiring the exclusive daemon-home lock. Watch lists MUST be isolated per daemon instance; cleanup MUST check process identity before signalling, and MUST leave unverified groups untouched. Ownership observations MUST record actual observation times, not future heartbeat allowances, and relinquishing a child MUST remove it from both active and durable cleanup lists. This cleanup MUST NOT invalidate durable provider conversation receipts or signal another daemon instance's runtime groups. Recovery MUST accept existing alive_until records by subtracting their original 20-second allowance before using the observation as ownership evidence. Unresolved groups after TERM, unavailable identity snapshots, and failed escalation MUST remain recorded for later recovery. An unreadable cleanup ledger MUST fail startup before replacing that ledger or the previous watchdog lists.

Kilo uses the same managed-session ownership as OpenCode: a private authenticated loopback ACP backend and a writable native TUI attached to the exact session. Actor and Voice Analyst share this adapter. Kilo configuration, MCP, model/variant synchronization, input readiness, cancellation and result settlement MUST follow the same boundaries; Kilo storage identity MUST include KILO_DB and its effective home/config roots. Starting or resuming an empty session MUST NOT submit a prompt. Only matching Kilo managed receipts may resume; legacy terminal receipts are not adopted.

On Windows, Kilo's official npm kilo.cmd entrypoint MUST be supported for both global and project-local installations. Actor and Analyst MUST use the same resolved launch prefix for the ACP backend and native TUI. The installed npm JavaScript launcher retains ownership of platform/binary selection and resource setup; CCCC launches it with Node without shell reinterpretation of arguments, preserving the configured environment and owned process-tree containment.

Args:

ts
{ group_id: string; actor_id: string; by?: string }

Result:

ts
{ actor: Record<string, unknown>; event: CCCSEventV1 }

Notes:

  • Antigravity, Copilot, Devin and Cursor runtime_mode defaults to default (native TUI); acp selects official pure headless ACP with derived runner=headless and runtime_state_source=managed_session. Other Runtime/mode combinations MUST be rejected. An update repeating the same Runtime with no runtime_mode MUST preserve the existing mode; changing Runtime without an explicit mode resets it to default. Mode is Profile-controlled and settings changes apply on explicit restart or next start; listing MUST report the actual running surface as runner_effective / effective_runtime_mode. Antigravity ACP MUST use its separate CCCC-owned provider home. Copilot, Devin and Cursor ACP use native CLI login/configuration; CCCC MUST NOT rewrite their global MCP configuration. Every ACP process/session MUST receive its own explicit CCCC Group/Actor (or global Analyst) identity and keep its resume receipt separate from the native TUI receipt. Copilot injects local MCP through process-scoped --additional-mcp-config; Devin and Cursor use session MCP injection. New and resumed sessions MUST reapply explicitly configured model and permission policy. Failed load MUST NOT silently start a fresh session. Copilot, Devin and Cursor do not durably store empty sessions: CCCC MUST distinguish never-attempted empty sessions from attempted prompts. The attempt state MUST be durable before sending the first prompt, including an unconfirmed delivery. Never-attempted empty sessions MAY start fresh on restart; attempted sessions MUST load the same identity or expose an error requiring explicit New session/reset. Global Voice persistence uses materialized for the same boundary; status polling cannot materialize a session.

  • Antigravity ACP Actors have no native TUI. The daemon retains ordinary ledger delivery in FIFO order while a prompt is active, admits it on live same-session provider activity or the prompt response, and projects normalized progress/completion through the existing headless journal. Replay or foreign-session events MUST NOT admit work. Permission requests are interactive unless the effective command selects unrestricted policy; user controls MUST bind the exact session generation and pending request, and grant at most one operation. Cancel/stop MUST retire pending permissions. Provider end_turn means a completed protocol turn, not proof that the task succeeded. An ACP prompt whose admission cannot be confirmed MUST settle its existing runtime.delivery claim as ambiguous and leave the automatic retry lane, including after session or daemon replacement. This MUST NOT be reported as accepted or downgraded to a retryable failure during worker shutdown. An admitted failed turn MUST project headless.turn.failed with its normalized provider error, so existing journal/SSE consumers can distinguish incomplete work.

  • For linked actors (profile_id set), actor_start and actor_restart first resolve profile runtime config and profile secrets.

  • Saving Runtime configuration does not itself replace a running session. actor_start remains idempotent while a registered session is running. A surviving attached terminal MUST NOT make a disconnected managed registration count as running; Start MUST retry its cleanup and report any failure before launching a replacement. Stop/restart MUST retire registered ownership by Group/Actor identity independently of the saved Runtime; restart MUST NOT start a second backend after a reported cleanup failure. Lifecycle status MUST follow registered sessions until explicit restart applies the saved configuration.

  • Message delivery and its transport record MUST also follow the registered running surface. Saving ACP mode on a live native PTY MUST preserve native delivery until explicit restart or the next start; a live managed session continues managed delivery after saving native mode. A registered Claude workspace-trust prompt MUST remain pending recovery rather than becoming a native task-input surface, including after a saved Runtime edit.

  • A managed Actor's native terminal is an attachment, not its provider lifetime. Its exit MUST NOT mark a healthy provider stopped, even before terminal-exit reconciliation. Reconciliation MUST follow the registered owner rather than saved next-launch Runtime settings and MUST NOT detach a replacement terminal. Explicit actor_start, writable term_attach and message delivery MAY reopen the terminal against the same healthy managed session; status reads and passive viewer attachments MUST NOT start it. A stopped or disconnected provider MUST NOT be restarted by terminal attachment. Confirmed provider process absence alone MUST NOT invalidate a durable Claude conversation receipt; existing identity and transcript validation still govern resume.

  • A daemon-launched actor whose executable is directly identified as codex MUST use one daemon-owned Codex app-server thread and MUST attach Codex's writable native TUI to that exact thread. Unsupported subcommands, wrappers, or prompt tails fail explicitly instead of silently selecting another transport. The app-server and TUI MUST receive the same executable, supported Codex global arguments, profile/model/provider configuration, and private environment. CCCC-owned listener, MCP identity, approval, and sandbox settings remain host-controlled. For both Actors and Voice Analyst, execution-policy overrides MUST be applied to the app-server; the remote TUI MUST attach without approval, sandbox, or shell-environment policy overrides. Stop/start MUST validate and resume the same version-2 managed receipt only when Runtime, workspace, command, model, and effective Codex storage identity still match. Legacy Codex receipts MUST NOT be resumed.

  • A daemon-launched claude actor MUST use one CCCC-owned Claude Agent View background session and MUST start claude attach against that exact session. The resolved executable and each observed live worker MUST independently report Claude Code 2.1.259 or newer; their versions need not match. Agent View can retain older workers after upgrading the supervisor and migrate an idle session to a newer worker, so a supported version change alone MUST NOT invalidate the same managed session. CCCC MUST continue validating exact session identity, the protocol-v1 control response shape, and the credential-file boundary; unsupported or unverifiable versions, invalid protocol responses, and credential-boundary violations MUST fail closed. CCCC observes turn ownership and terminal settlement from the append-only provider transcript. A single retryable control-query failure MUST NOT invalidate a still-live session; sustained inability to verify liveness or confirmed job absence MUST disconnect it. CCCC owns background/session/attach, name, MCP identity, autonomy, and resume arguments. Runtime Profile environment values MUST be merged into one stable, owner-scoped, CCCC-protected settings file because Agent View deliberately strips arbitrary process environment from persisted jobs and stores that file path in its durable respawn metadata; raw values MUST NOT appear in the job record, terminal command, receipt, or logs. An ordinary process stop MUST retain this file while the durable session receipt remains resumable. The copy MUST be atomically replaced when that owner's effective settings change and removed when the managed session identity, Actor, or Group is retired. Stop MUST report success only after the Agent View job is confirmed absent. Start MUST validate and resume the same version-2 managed receipt only when Runtime, workspace, command, and the complete effective Claude launch identity, including content of file-backed settings and prompt inputs, still match. A live idle matching session MAY be re-adopted; an active, ambiguous, copied, or identity-mismatched session MUST fail or start fresh according to the existing receipt boundary and MUST NOT be guessed. Legacy Claude Hook and print-mode receipts MUST NOT be resumed.

  • A daemon-launched grok actor MUST use one CCCC-owned managed session. CCCC starts a dedicated private Grok leader, connects its ACP observer, and attaches the native writable Grok TUI to the same provider session. Actor startup, Voice Analyst startup, and CLI setup MUST share a verified native cccc MCP registration so ACP session creation, resume, and native TUI reload use the same configuration. The shared command MUST resolve the launching CCCC executable dynamically and inherit Actor/instance/profile/origin identity from its process, not persist that identity in global settings. CCCC MUST preserve unrelated MCP entries and native Claude/Cursor imports, reject malformed configuration and conflicting project overrides without replacing them, and serialize its user-level updates across instances. Readiness MUST check the effective executable, arguments, enabled state, and inherited CCCC identity after native configuration overrides, and MUST reject a native policy denial. A valid base table or discovery entry alone is insufficient. Conflicting version/project overrides and policy documents MUST NOT be rewritten to force readiness. This native registration also takes precedence for standalone Grok sessions. Structured lifecycle events remain the working/completion authority. Stop/start MUST validate and load the same version-2 managed receipt when its Runtime, workspace, command, model, and effective provider-home identity still match. Legacy raw-terminal Grok receipts MUST NOT be resumed.

  • An explicit Grok --trust runtime argument grants folder trust to the native TUI for the selected workspace. CCCC MUST forward it only to the TUI, not the agent subcommand, and MUST NOT add it by default. Without an explicit or previously saved trust decision, native folder confirmation can block business input; PTY delivery acceptance is not provider admission.

  • The Grok ACP observer MUST associate live _meta.promptId activity with its local turn and consume matching durable turn_completed updates, including _x.ai/session/update, for both controlled and native turns. A send_now cancellation MUST settle the old turn before the next native input is associated; a delayed prompt RPC response or duplicate terminal MUST NOT settle the new turn or consume its sources. A turn ending before prompt-bearing activity MAY use the persisted session-scoped event sequence following its user record as its completion boundary, never wall-clock timing. Replay records MUST NOT admit controlled or native input. Uncorrelated prompt_complete notifications remain non-authoritative. OpenCode and Kilo use their ordered backend event stream as described below.

  • Grok ACP initialization MUST advertise clientCapabilities._meta["x.ai/userMessageEcho"]=true, and new/load session requests MUST bind _meta.clientUserMessageEcho=true because the leader shares initialization across clients. Current Grok persists user input even when its live echo is disabled; persistence alone does not admit the controlled prompt. Both new and resumed managed sessions need this opt-in so correlated progress can flow before the prompt completion RPC, without increasing or bypassing the bounded admission buffer.

  • A daemon-launched opencode actor MUST use one CCCC-owned managed session. CCCC starts opencode acp with a generation-scoped authenticated loopback backend, observes the ACP session over stdio, attaches opencode attach to that exact session, and injects the actor-scoped CCCC MCP server at session creation. The resolved executable MUST report OpenCode 1.18.14 or newer. ACP remains the control and permission port. The observer MUST use the authenticated workdir-scoped /event endpoint, whose listener is registered before the HTTP response; the lazily subscribed /global/event endpoint cannot guarantee delivery of the first input. Text parts marked metadata["kilocode.lifecycle"]="transient" are temporary Kilo UI progress and MUST NOT contribute to streamed or completed Actor/Analyst answer text; later deltas for these parts MUST also be excluded. The synthetic flag alone MUST NOT exclude ordinary answer text. This backend event stream MUST order user admission, assistant output, and terminal session status for both controlled and native turns; a prompt RPC response or a duplicate ACP output update MUST NOT finish or contribute text to another turn. Persisted native input is not evidence that the current turn consumed it: correlation MUST follow the assistant message's parent user identity, retaining queued inputs across the preceding turn's idle status. A lost or malformed non-replayable stream invalidates the session. A model selection made in the native TUI becomes authoritative for later CCCC-managed prompts when the user submits the next TUI message; CCCC MUST mirror that message's exact provider/model and variant into the same ACP session. An explicit runtime-command --model remains the launch-time override. Stop/start MUST validate and load the same version-2 managed receipt when its Runtime, workspace, command, model, and effective OpenCode storage identity still match. Legacy raw-terminal OpenCode state MUST NOT be resumed.

  • Completion of a temporary Actor restore worker or retirement of an IPC request worker MUST NOT terminate an otherwise healthy managed provider session. Providers that bind their lifetime to the spawning OS thread MUST be launched by daemon-lifetime workers. Explicit stop, failed-start rollback, and daemon shutdown retain ownership of process cleanup.

  • If managed Claude startup reports an untrusted workspace, the Actor terminal MAY present the configured Claude command for the operator to approve trust. CCCC MUST NOT write the trust decision. Configuration observation MUST begin before opening that prompt; approval recorded before the watcher starts MUST remain observable. Task delivery stays pending until the managed session is attached. Recovery MUST acquire the runtime-start permit before the Actor start guard and revalidate cancellation and the same live terminal before attachment. Actor and Group stop MUST cancel pending recovery, including prompts without a managed session, and clean up a cancelled launch instead of attaching it.

  • Claude trust recovery MUST continue waiting only for a verified workspace-trust refusal. Other launch errors MUST end that watcher and retire its own temporary trust terminal; an unrelated provider configuration rewrite MUST NOT trigger another recovery launch. The trust terminal is not a managed task-delivery surface.

  • A failed Claude resume MUST retain the original provider session ID, record a bounded safe diagnostic and failure count, and fence that outcome to the exact resume attempt. Provider stderr and credential values MUST NOT be persisted in this record. Known temporary startup/control failures MAY remain retryable. Copied session IDs, unavailable durable history after the bounded discovery wait, invalid identity/protocol/credential evidence and other unverifiable resume failures MUST pause automatic startup with runtime_session_status="resume_failed", runtime_session_resume_eligible=false and runtime_session_last_resume_error. Paused startup returns claude_resume_failed; it MUST NOT silently start a fresh session. Undelivered messages remain pending and their handoff claims become retryable runtime.delivery=failed, leaving the automatic worker retry lane. An authorized explicit actor_start or actor_restart MAY retry the same saved conversation and redispatch those messages. Only explicit actor_new_session (or configured resume disablement / changed launch identity) selects a fresh conversation; provider history remains intact. Successful resume clears the failure diagnostic and count. Process absence alone is not a resume failure.

  • Managed runtime startup MAY synchronously enumerate the injected actor-scoped CCCC MCP tools before its provider session becomes ready. That catalog discovery MUST use capability_state with view="mcp_catalog" so it cannot wait on the same Group lifecycle lock held by actor_start; ordinary capability reads remain serialized normally.

  • For Codex, Claude, Grok, OpenCode, and Kilo Actors, CCCC MUST hand an incoming Actor delivery to the writable native TUI as soon as that terminal is ready. CCCC MUST NOT inspect provider busy state to choose steer versus queue, and MUST NOT hold the delivery until the current turn settles. The receiving Runtime owns that policy according to its own configuration. runtime.delivery=accepted means the canonical input and submit sequence were written successfully to the Runtime terminal; it does not claim that the provider completed or semantically accepted the work. Structured protocols remain authoritative for session identity, lifecycle, progress, completion, cancellation, and Voice Analyst delegation.

  • In assistant mode, Realtime Voice owns the intent decision to create a Voice Analyst delegation; it does not own provider scheduling. For a Runtime with a native terminal or verified steering surface, once delegation.created exists, CCCC MUST immediately hand the exact correlated input to the managed Runtime and MUST NOT hide it in a server-side wait-for-idle queue. An active Runtime with a verified exact-turn steer operation MAY receive the input through that operation; otherwise CCCC MUST write the exact payload and submit sequence to the same verified native terminal session, after which the Runtime owns the steer-versus-queue decision. CCCC MUST register correlation before the write, project whichever authoritative turn consumes it, and report success only after the Runtime control operation or complete terminal submit sequence was accepted. A missing, closed, or rejecting Runtime input path MUST return an explicit delivery error; busy state alone MUST NOT drop, delay, merge, or reject the delegation.

  • The Web Voice start contract MAY carry optional immutable application_context with a validated 1–128-byte ASCII identifier and nonempty UTF-8 instructions up to 24 KiB (24,576 bytes) in both assistant and persona modes. This is a local input size limit, not a provider token budget. CCCC MUST reject oversized instructions without truncating them and MUST preserve accepted text exactly; provider limits still apply. In assistant mode, CCCC MUST include it in Realtime startup instructions and each Voice delegation delivered to the Analyst, without changing the provider delegation ID or at-most-once admission. Start idempotence MUST include the entire validated context, including mode; changed context MUST NOT reuse an active call. Context MUST NOT be interpreted as authentication, a selected Group, or a tool grant, and MUST NOT implicitly reset the persistent Analyst. Embedded assistant expression (language, length, tone, and user-facing attribution labels) MUST follow the host instructions without appending global expression preferences. Assistant calls, including embedded calls, retain provider automatic acknowledgement filler and client delegation. A brief receipt acknowledges hearing the request without waiting for Analyst completion; it MUST NOT claim execution succeeded. Host expression policy governs wording and avoids repeated receipt narration. Omission preserves global Voice behavior. Arbitrary context text MUST NOT appear in startup diagnostics.

  • Antigravity ACP Voice Analyst shares the Actor adapter, has no terminal, and MAY hold up to 32 inputs in an explicit FIFO because official ACP does not accept concurrent session/prompt calls. Its empty persisted command MUST resolve to the Actor default agy --dangerously-skip-permissions, setting provider YOLO mode and automatically selecting only a one-time allow option for any remaining permission request. This default MUST be reapplied after both new and resumed sessions; explicit custom commands and linked Profile commands MUST retain their configured permission policy. Web MUST display the same default without materializing it into saved settings. Admission MUST distinguish host queue acceptance (Queued, event analyst_queued) from provider turn acceptance (Started). Each input keeps its correlation and completion association; no merge, eviction, auto-replay after an uncertain outcome, or terminal fallback is permitted. Cancellation MUST remain available before the first provider admission receipt, correlate with that exact pending input in the protocol command loop, and cancel both current and queued inputs. Confirmed cancellation retains the warm session; failure to settle uses the existing bounded invalidation/stop path. A terminal outcome without a provider receipt MUST use a distinct host-generated turn_id per input, shared by both Web consumers for idempotent notification persistence. Stop/reset retires the queue. Actor stop/restart and daemon shutdown MUST also interrupt pending ACP admission waits without automatically replaying the unconfirmed batch. Settings replacement and rollback MUST resume only a genuinely resumable session; execution surface alone does not imply materialization. Disconnect or admission failure MUST expose an unconfirmed/failed outcome. Read-only status reports structured, queued_inputs, permissions, bounded progress and last_error; it MUST NOT start work. Terminal failure MUST retain a bounded diagnostic message (at most 4096 UTF-8 bytes, no raw RPC envelope), including pre-admission rejection, in the warm Analyst snapshot outside a call. The structured console MUST label partial output as incomplete. The next task or successful/cancelled terminal outcome clears this error. The generation-bound interactive control endpoint accepts manual input, a one-time permission answer or an explicit Cursor user-interaction response. Copilot, Devin and Cursor ACP use this same FIFO, cancellation and unconfirmed-delivery boundary, without a native terminal. Existing native Runtime admission remains unchanged. The structured Web console MUST publish same-generation authoritative read/control snapshots to its call controller. Cancellation availability MUST cover executing, queued and pending-admission manual inputs, including the interval after a previous turn completes but before the next provider receipt. A terminal event for the previous turn MUST NOT hide cancellation for remaining work.

  • Pure ACP Analyst manual investigations use the existing generation-bound control endpoint with action="input", a stable input_id, complete text and optional nullable call_generation. Omitted/null call identity is standalone; CCCC MUST NOT infer it from an active microphone session. An explicit identity MUST match the attached assistant call and that exact Analyst generation, otherwise admission fails without changing the draft or starting work. Retries preserve both input ID and original call scope; reusing an ID with changed content/scope MUST fail. Admission remains the existing FIFO and at-most-once lifecycle, distinct from provider acceptance. The Web snapshot includes manual_tasks (id, text, nullable call_generation, status, result, error) and nullable manual_task_id identifying its current/last manual output; these are read-only, generation-local console projections, not a persistent chat or another scheduler. Keep all unsettled tasks and final results awaiting the original call's output; retain at most eight settled disposable rows, with a 64-record total bound that rejects new input rather than discarding unsettled or awaiting-output work. Eviction MUST NOT erase lifecycle deduplication or permit replay. An explicit call-bound investigation receives the call's immutable host context. Only its settled outcome returns through session.context.append, with a bounded task label, authoritative final and non-execution instructions; it MUST NOT invent a provider delegation identity. Failed/cancelled work MUST NOT return partial output as a successful answer. Completed rows and failures remain visible outside a call. Call retirement prevents queued/late outcomes from entering a replacement call, while the warm Analyst may finish independent work. Independent native-TUI tasks remain Terminal-originated and are not newly opted into automatic voice projection. Persona calls neither admit call-bound investigations nor consume their results. Context submission does not prove speech; no automatic retry of uncertain task execution or context delivery is added.

  • The browser MUST publish the same-generation call attachment state from the Voice control socket ready message before completing startup. The HTTP start response describes a not-yet-attached call; retaining that snapshot after socket attachment MUST NOT misclassify manual investigations as standalone. Socket readiness does not itself prove provider context acceptance or speech.

  • Embedded Voice application_context.mode MUST be assistant (default) or persona, inside the same strict, bounded context object. Omitted and explicit assistant modes are equivalent; unknown modes/fields MUST be rejected. Mode is immutable and participates in start-request identity. Persona MUST use only host instructions as the CCCC-supplied Realtime instructions, without assistant role/routing or user expression preferences. Persona MUST NOT resolve, launch, reuse, reset or subscribe to a Voice Analyst, execute provider delegations, consume or reserve notification sources/results, prepare notification output, or apply output receipts. Delegation input MUST be ignored before content parsing; diagnostics MUST NOT contain its payload. Existing Analyst work and notification state remain independent and available for later assistant calls. Call authorization/revocation, generation fencing, microphone lease, heartbeat and cleanup remain enforced. The call-start cue MUST defer opening behavior to host instructions. Persona requests omit quicksilver client-delegation configuration; upstream suppression is experimental and MUST NOT be claimed solely from omission or a successful SDP response. A provider rejection MUST NOT silently fall back to assistant mode. readiness.supported_modes advertises supported call modes; persona requires Realtime credentials, not Analyst availability. Call payloads include mode and nullable analyst_generation; the persona start payload has analyst: null, while the active endpoint MAY still report the independently managed global Analyst. These fields confer no additional caller permissions.

  • Codex Voice startup failures MUST distinguish configuration, Analyst startup, recording ownership and Realtime connection failures. The Web start response MUST retain a safe error category and diagnostic details (stage, total attempt elapsed_ms, and http_status/os_error when available). Packaged startup MUST emit these safe fields to stderr even without a tracing subscriber. Credentials, private paths, SDP, provider response bodies and arbitrary error chains MUST NOT appear in these diagnostics. Diagnostic classification MUST NOT add automatic provider retries, extend timeouts or discard a successfully started Analyst after Realtime failure.

  • calls/active readiness MUST distinguish a missing Antigravity ACP distribution from a missing native Runtime executable. analyst_runtime_setup_required=true identifies a resolved Antigravity ACP configuration whose managed distribution is not installed; analyst_runtime_available remains false until installation. Web MUST give the explicit cccc setup --runtime antigravity --runtime-mode acp --login installation/separate-login instruction rather than native agy/PATH restart advice. This read-only discovery MUST NOT install, authenticate or launch an Analyst; installed status does not prove valid provider credentials.

  • The read-only Web catalog GET /api/v1/runtimes MAY add mode_availability: {default: boolean, acp: boolean} to the Antigravity, Copilot, Devin and Cursor entries. Existing available and the top-level available list retain native-executable semantics. default detects native agy; acp uses the same installed-distribution check as Voice readiness. For Copilot, Devin and Cursor both modes detect the native CLI executable; detection is not proof of login or provider task readiness. Detection MUST NOT install, authenticate, start sessions or write provider state. Web MUST distinguish loading/failed/unknown detection from a negative result and provide an explicit read-only recheck; Profile editing remains independent of host installation.

  • Voice Analyst Runtime settings MUST NOT change while a Realtime Voice call is active. After the call stops, active or queued Analyst work MUST block an ordinary settings update rather than being discarded implicitly. An interactive administrator MAY explicitly confirm discarding that work as part of the same settings transaction; CCCC MUST then stop the old managed session before applying the replacement and MUST report whether unfinished work was discarded. Candidate-launch failure MUST restore the prior settings and Runtime, but MUST NOT claim that explicitly discarded work was recovered.

  • Claude transcript entries MUST use the provider promptId as the durable provider-turn identity and MUST NOT infer identity from the Agent View summary headline. A human prompt observed before control acceptance is an external turn. Because the authenticated reply response does not expose that promptId, CCCC MAY return a stable local turn receipt as soon as the control request is accepted, but Voice ownership, progress, and results become authoritative only when the next transcript user record exactly matches the one pending controlled prompt and supplies its provider identifier. A competing prompt plus successful control acceptance is ambiguous and MUST invalidate the managed session rather than replaying the delivery. A controlled request that never starts, or settles without exposing the matching transcript, MUST fail within bounded post-acceptance or post-settlement intervals; active provider work MUST NOT expire solely because its turn is long. turn_duration, the provider interruption marker, and an explicit failure record are terminal authority. The state file and selected transcript file identity MUST be revalidated while following the session. An active transcript MAY relocate inside the configured Claude project store only after the old path disappears, a unique same-session file is found, and its entire consumed byte prefix matches the observer’s retained SHA-256 digest. The reader MUST preserve its byte offset and partial record without replaying history. A missing or incomplete relocation destination MUST settle within a bounded 10-second grace period; sustained loss, consumed-history mismatch, ambiguous candidates, same-path replacement, truncation of the active file, or malformed tail records MUST invalidate the session. Relocation alone MUST NOT stop or recreate the provider session.

  • A managed-runtime native-terminal delivery MUST wait for the TUI's advertised input mode before writing any payload; a PTY handle alone does not prove input readiness. Readiness waiting MUST be bounded and cancellable and MUST NOT inject a probe prompt, fall through on timeout, or wait for provider work to finish. Actor deliveries remain unaccepted on readiness failure; Voice native-input deliveries report an explicit error without writing the payload.

  • A Claude receipt MAY name an empty session that has never created a transcript. Before respawning it, CCCC MUST inspect the existing durable job metadata: only positive empty-input evidence with no transcript path, consumed transcript bytes, output, or token usage permits transcript-free recovery. An absent/null or numeric zero output-token counter does not indicate usage; nonzero or malformed counters MUST retain strict transcript validation. Unknown or materialized history MUST retain strict transcript validation, including a transcript written before Agent View publishes its path. This exception MUST preserve the exact provider session ID and MUST NOT replay old input or create a replacement conversation silently.

  • Daemon shutdown MAY stop managed Actors concurrently, but each ordinary stop MUST still confirm provider termination and retain retryable ownership on failure. A launcher forced exit MAY send best-effort Claude control stop requests for Actor sessions registered in that same process, within a single bounded deadline and without acquiring their graceful-stop locks. This does not guarantee stop confirmation or cover a detached daemon's registry or the separate Voice Analyst. An unavailable or unverifiable Agent View control endpoint MUST NOT by itself authorize signaling processes found by PID or command-line matching.

  • Observer failure MUST NOT be treated as proof of provider process exit. Actor and Analyst teardown MUST use confirmed provider stop, and a failed stop MUST retain retryable ownership rather than mark the job stopped. Normal managed-client shutdown MUST explicitly terminate event readers even when the session still retains the event sender; observers MUST distinguish expected closure from a failure and MUST NOT emit duplicate stop events.

  • Unexpected managed Codex disconnects MUST retain structural first-cause diagnostics before teardown: transport category, numeric close/OS code when available, elapsed time, and observed owned-process state/exit status. Voice control-stream closure MUST distinguish browser closure, authorization loss, server shutdown and Analyst lifecycle failure, correlating call and Analyst generations. Packaged launches MUST expose these bounded records without requiring a tracing subscriber. Diagnostic records MUST NOT contain peer close text, protocol payloads, credentials, conversation text or arbitrary error chains. A generic control failure MUST NOT assert Analyst availability. These diagnostics MUST NOT change reconnect, replay or teardown policy.

  • Managed protocol output received before a protocol-originated request is admitted MUST be buffered within fixed byte and event-count bounds. This rule applies to Voice Analyst and internal control requests; Actor message delivery uses the native-TUI rule above. Once the provider authoritatively accepts a protocol request, CCCC MUST publish buffered lifecycle updates in order. A bounded post-response drain MAY be enabled only as an explicit provider-specific normalization policy.

  • Voice result projection MUST reconcile the authoritative final with the exact already-projected prefix. A different final MUST NOT be discarded merely because progress was streamed. Result accumulation is bounded to 32 KiB; overflow without a bounded authoritative final MUST settle as result_too_large, not as successful truncated output. The Voice port MUST report that limitation without terminating the warm Analyst or the audio call. Context sends, provider context receipts, and completed speech turns MUST remain distinct observations; receipt absence MUST NOT trigger blind replay, and receipt presence MUST NOT be treated as proof that every fact was spoken.

  • Managed-runtime preview restoration and live activity MUST remain distinct for every supported Runtime. The Web /api/v1/groups/{group_id}/headless/stream (and codex/stream alias), with default replay=true, first emits one headless.snapshot SSE frame containing {events:[...]} from the bounded retained journal. Subsequent headless frames carry only increments after that same captured file boundary; replay=false omits the initial snapshot. The standalone snapshot GET remains a read-only inspection API. An unfinished journal line at attachment MUST be retained until complete, not skipped between snapshot and tail. Consumers MUST apply each stable journal event ID at most once before delta accumulation or lifecycle effects. If received IDs survive a disconnect or Group switch, consumers MUST preserve their pending projections as well; the Web consumer flushes buffered text and activities to their original Group before cleanup. Restored history MUST NOT trigger fresh progress bubbles, change message-read state, or start Runtime work. Dock bubbles show bounded recent live progress per Actor; full transcripts remain available in the inspector/TUI. Reconnecting, projecting the same completion, or evicting a UI cache MUST NOT revive stale progress as a new notification.

  • Context delivery is separate from Runtime input admission and Provider speech scheduling. The browser MUST deliver checked context in order without waiting for user or assistant speech to finish; the Provider owns speaking and yielding to interruptions. Adjacent unsent fragments with the same context/delegation envelope MAY be joined without truncation, subject to the final append-size limit below. Missing speech transitions or context receipts MUST NOT block later context, trigger blind replay, or discard positively unsent results. A completed speech turn does not prove that all source facts were spoken. This delivery policy MUST NOT delay Analyst inputs or intercept provider delegations.

  • The browser Voice control socket MAY report provider_error with an error object containing bounded provider code, type, event_id, and param identifiers. This is transport diagnostics only: it MUST NOT start, cancel, or replay Analyst work. The Web port MUST validate these identifiers and correlate its diagnostic with the active call generation; it MUST NOT log arbitrary browser payloads, provider error messages, or credentials. The browser MUST distinguish a provider error from an Analyst failure and retain the provider code in its visible error when available. Error reporting alone MUST NOT change provider recovery policy.

  • Actor start, restart, new-session, and daemon restoration MUST NOT submit a model turn solely to initialize an Actor or materialize a provider session. They create or resume the daemon-owned control session and attach its native terminal while the model remains idle. Only real input—a pending CCCC delivery or human terminal input—may start model work. The CCCC startup prompt MUST be deferred to and combined with the first successfully accepted CCCC delivery, MUST NOT be sent as a standalone turn, and MUST remain pending if that delivery is not accepted. Recovery of an actual pending Send is a valid work trigger; lifecycle operations alone are not.

  • A newly created Codex thread MUST be durably resumable before CCCC records a usable receipt or attaches its native TUI. thread/start returning an ID and planned rollout path is insufficient. CCCC MUST materialize the empty thread through native metadata operations and verify full-history readability for that exact thread, without submitting a model turn or adding synthetic conversation items. Resumed conversations MUST retain their existing names and history.

  • A provider process exit MUST record actor.stop with by="system" and data.reason="process_exit", but MUST NOT disable the actor or stop the Group. A user-authored Send or Request Reply explicitly targeting an actor ID, or a Send targeting @foreman, is also a wake action: it MUST enable that actor, move a paused or stopped Group to active, and start delivery through the normal runtime path whether the prior stop was automatic or user initiated. Broadcast selectors (@all, @peers, including the materialized default) MUST NOT enable disabled actors; they MAY resume the Group and wake already-enabled recipients. An actor explicitly named alongside a broadcast retains its explicit wake behavior. A broadcast with no enabled recipients MUST NOT resume the Group. This wake policy MUST NOT narrow the message's logical audience or remove it from history. Mail and Actor-authored messages MUST NOT enable disabled actors; Mail and previously queued work MUST NOT independently wake a runtime while a Group remains paused.

  • If the linked profile includes capability_defaults, daemon applies baseline capability enables through capability control plane before launch.

  • Daemon also applies role defaults and the actor's capability_autoload before launch. These are durable desired capability bindings, so they remain applied when the subsequent runtime launch fails.

  • A daemon-launched runtime process MUST resolve an explicit existing attached scope from the actor default or group active scope. It MUST return missing_project_root, scope_not_attached, or invalid_project_root as applicable and MUST NOT fall back to the daemon working directory. An explicitly external structured executor may omit a local process only when its product capability and documentation say so.

  • Unmanaged native PTY Actors, including Antigravity in its default TUI mode, retain the shared best-effort input-mode wait and automatic submission path. Startup context MUST accompany the first task in one submission; starting an idle Actor MUST NOT submit a model prompt. Antigravity delivery does not depend on terminal display text or a per-process Web confirmation. Native login/trust setup must be completed before delivery; paste mode alone does not prove that setup is complete. PTY handoff evidence is not a provider receipt. Custom terminal programs retain their separate preamble submission contract.

  • Antigravity TUI Actor startup and native CLI setup MUST use native agy mcp add and verify the effective CCCC MCP configuration before launch. A conflicting project .agents/mcp_config.json entry or malformed configuration MUST fail without being overwritten. Global setup MUST preserve unrelated servers and serialize updates across instances. Its cccc mcp entry MUST resolve via the owning Actor launcher on PATH and inherit instance/Actor identity rather than pinning it in shared configuration. The complete startup prompt remains attached to the first task; a bounded, cancellable settling interval before the first payload addresses the observed early paste-mode initialization window. Later deliveries MAY include a conditional bootstrap reminder. Neither pacing nor that reminder changes PTY handoff evidence into a provider receipt.

  • Antigravity TUI preparation also disables the native showFeedbackSurvey user preference, because its rating overlay can consume terminal input. Other preferences and user-owned symlinks MUST be preserved; invalid settings MUST fail explicitly without being replaced. This user-wide change also applies to standalone AGY sessions and MUST be documented as such. It does not add a UI-text readiness detector, a survey-dismissal keystroke, or automatic replay of accepted input.

  • The kimi runtime targets the current Kimi Code native TUI. Actor startup and CLI setup MUST share MCP configuration at KIMI_CODE_HOME/mcp.json (defaulting to the user's .kimi-code/mcp.json), preserve unrelated servers, and reject malformed documents without overwriting them. A project .kimi-code/mcp.json entry for cccc takes precedence; a conflicting project entry MUST fail setup without modification, unless it is the user-level configuration file itself. CCCC MUST NOT infer the active client from legacy directories or invoke the removed kimi mcp add command. MCP setup MUST NOT write provider trust records. Native input-mode signals do not establish first-use initialization readiness; users must finish startup dialogs before PTY delivery. Kimi's explicit session arguments are preserved, but CCCC does not claim managed session-ID capture or automatic resume for this runtime.

  • A deepseek actor has no native terminal surface and MUST use CCCC's structured ACP surface. The daemon MUST install and resolve CCCC's pinned ACP composition from CCCC_HOME/runtimes/deepseek/<release> and MUST NOT modify the user's DSH_HOME, home-level npm project, or attached project.

  • The managed DeepSeek root manifest and lockfile MUST declare exactly dsh-acp, dsh-mcp-client, dsh-acp-demo, and dsh-llm-deepseek as direct dependencies. Every installed @deepseek-ai/dsh* package MUST remain on the release declared by crates/cccc-contracts/src/deepseek.rs; checking only direct package manifests is insufficient.

  • Each DeepSeek actor MUST set CCCC_DEEPSEEK_SESSION_ROOT to groups/<group_id>/state/deepseek/<actor_id>/sessions under the active CCCC_HOME. A provider turn MUST reach a successful terminal response within the shared bounded timeout before its source cursor advances; timeout cancellation MUST be durably projected as a failed turn, or the unconfirmed supervisor MUST be stopped. Output and failed-terminal idempotency keys MUST include the provider-attempt identity so a retry cannot be hidden by partial output from an earlier failed attempt; the successful terminal remains idempotent by source event. Crash recovery MUST query that durable per-source completion marker directly (or through its persistent index) and MUST NOT stop recognizing completed turns merely because the append-only headless event log crossed a size or line-count threshold. A permanent credential or context-window failure MUST persist a manual-restart gate before automatic delivery can run again. The gate MUST be bound to both the actor creation identity and the failed provider launch generation, MUST survive daemon restart, and MUST be cleared only after a lifecycle start/restart operation successfully initializes a replacement provider process; daemon restore and message-triggered auto-wake MUST NOT clear it. A late failure from a replaced generation MUST NOT close the replacement actor's gate.

  • The managed dsh-llm-deepseek profile MUST set maxTokens to the shared DEEPSEEK_MAX_OUTPUT_TOKENS contract value (currently 65,536), preserving input/tool headroom instead of inheriting the upstream 256k output reservation. Credential absence and provider context-window overflow are permanent for the current runtime session: both MUST be normalized to stable, secret-free failed-turn errors and MUST stop automatic retries until a lifecycle start/restart successfully initializes the actor again.

Voice notification state (trusted local user) ​

Voice notification preferences and derived delivery observations are private instance state under CCCC_HOME/state/codex_voice/notifications.json, atomically replaced under one exclusive lock. The Group ledger remains the source of message bodies. These operations MUST NOT write mail.read, complete reply obligations, wake Actors, start a microphone, or imply user approval of Actor text. Web access MUST require the same administrator principal as the other global Voice routes. Long-lived calls, notification handoffs and Analyst terminal sockets MUST revalidate a remote administrator token. Revocation or administrator downgrade MUST stop input/output; terminal connections MUST also check while idle (one-second polling for both viewer and control attachments). Existing trusted-local identity is unchanged.

  • voice_preferences_get: read VoicePreferences only. This and voice_notifications_get MUST be classified as read-only dispatcher operations so panel polling does not queue a global writer ahead of MCP catalog discovery during Actor startup.
  • voice_preferences_set: {preferences: {revision, groups: Record<group_id, "off"|"to_user"|"all_chat">, suppress_viewed: boolean, verbosity: "concise"|"standard"|"detailed", style: "natural"|"direct"|"patient"}}. The supplied revision MUST match; successful updates increment it. Newly enabled message categories begin at the ledger boundary at save time, not historical chat. Expression changes apply to the next call and MUST NOT restart the Analyst.
  • Source-message handoff MUST include canonical Group/event and sender IDs, a readable Group name, and the canonical sender-title snapshot (current Actor title or ID when unavailable). Names and message contents remain data, never instructions. Analyst notification prompts MUST use the active call's expression preference. Both Analyst and Realtime instructions MUST require each new Actor notification to identify its Group and sender without a follow-up question. Detailed applies to notifications as well as user answers and preserves material findings, numbers/units, conditions, evidence/uncertainty and next steps; it does not require reading raw tool traces. Output preflight MUST supply source identities independently of the Analyst's free-text summary, using only eligible sources for background output. A partly suppressed summary remains generic; adding attribution MUST NOT reintroduce its excluded source names or contents. An explicit user answer retains its existing suppression exception and original source attribution.
  • voice_messages_viewed: {messages: Array<{group_id, event_id}>} (at most 128). Exact formal-chat references only; idempotent, private, and independent of Actor unread state. The Web observer MUST require an unobscured, foreground, fully visible expanded message with at least 1.5 seconds of stable viewport exposure, not GET/SSE receipt or virtual-list mounting.
  • voice_notifications_get: read at most 128 recent source references plus pending_count and unconfirmed_count, plus suppressed_count for skipped results among the visible references. Each reference includes its associated result's output_status (processing, ready, unconfirmed, submitted, suppressed) and optional suppression_reason (viewed, policy, source_unavailable). Submitted is a browser submission observation, never playback completion or confirmation that the user heard it. The GET MUST NOT scan, mark, consume or start work. Full message text, origin tokens and stored Analyst result text MUST NOT appear in this UI projection.

A notification consumer failure MUST retain durable references and publish {type:"notification_status", paused:true} to the connected call owner over the existing Voice control WebSocket. This state is independent of Analyst availability and audio state. The consumer stops for that call; a new call starts a fresh consumer. It MUST NOT silently reset corrupt state.

Before submitting source-bearing output on the browser data channel, the host MUST recheck current policy and exact viewed references. /api/v1/codex_voice/calls/{generation}/notification-output accepts {result_id} only for the active connected call and its reserved result. A fully suppressed background result returns {message:null}. That decision and its reason MUST be stored atomically with the final policy/viewed check and remain idempotent even after later preference changes. A partly suppressed unstructured summary MUST NOT be sliced or spoken as if its sources were separable; an explicit user answer remains deliverable. The browser reports notification_output_submitted only after data-channel submission and reports notification_output_not_submitted for positively unsent result IDs at orderly stop. Queue or receipt-capacity overflow MUST retain the rejected result ID for this report before initiating teardown. Reports MUST precede the stop frame and fit both the 1,024-ID observation limit and the 128 KiB WebSocket frame limit; the browser batches at most 64 managed result IDs per report. An ID already submitted to the provider MUST NOT be included merely because its receipt is missing. Disconnection, missing receipts, or timeout alone MUST NOT release a reserved result for automatic replay. The private Realtime context-append protocol limits each append to 500 tokens. The browser MUST apply a conservative 500-UTF-8-byte text budget at the final data-channel boundary, after local coalescing and notification preflight have produced the actual text. Session and delegation context appends MUST preserve the complete text and Unicode code points across ordered fragments. One output's fragments MUST be sent consecutively, without waiting for speech between fragments. Receipt capacity MUST be checked for all fragments before sending the first one. Report a result submitted only after every fragment was written; if some fragments were written and a later write fails, retain the result as unknown and MUST NOT report it positively unsent or replay the complete result. This limit does not require shortening the Analyst summary or changing the selected response detail. Context delivery MUST NOT depend on user/assistant turn.created / turn.done, a prior speech response, or context receipts. These events do not authorize or release a delivery slot; the Provider owns speech timing and interruption. The browser MUST drain queued context in order after coalescing and policy checks, even if a previous turn never ends or a received context produces no response. A delegated result MUST NOT wait for the turn that needs that result to end. The browser's conversation transcript MUST allow input/output transcript fragments to precede turn.created. A later provider turn ID, including one first received in turn.done, MUST bind the same role's unbound draft in place; the authoritative final transcript replaces that entry. An event for an already-known turn MUST update that turn without consuming another draft. Distinct provider turns MUST remain distinct even when their text is identical. Transcript reconciliation is display state only and MUST NOT trigger delegation or context delivery. The browser MUST bound transport waits: while connecting or while the data-channel buffer exceeds 256 KiB, queued output waits at most 15 seconds for usable capacity. New output MUST NOT extend that deadline. Capacity recovery resumes delivery and rechecks notification policy. Failure MUST be explicit and stop the call through the existing orderly-stop path, retaining positively unsent results; already submitted or partially submitted results remain uncertain and are never replayed on that basis. Closing the channel MUST remove its callbacks and cancel all pending waits. Notification-output preflight MUST have a finite request deadline, including when the underlying request ignores cancellation or never settles. Late responses MUST NOT submit obsolete output. Only transient preflight failures may be retried, with a bounded attempt count and backoff; each attempt MUST recheck policy. Suppression of the same call/result reservation MUST return {message:null} on subsequent checks, including when the original response was lost. Persistent or terminal preflight failure MUST stop the call explicitly and retain positively unsent results through the existing orderly-stop path. Browser delivery diagnostics MAY report queue counts, transport wait reasons, buffered bytes, receipt/speech observation counts and preflight failures. They MUST NOT log message bodies or credentials, change source-processing state, or turn any speech observation into per-source heard confirmation. Each retained result MUST have a durable completion sequence, allocated exactly once from the instance's monotonic notification sequence under the same state lock. Output snapshots MUST sort results by this sequence, not opaque Runtime turn IDs, Analyst generations, or source arrival. Duplicate completion observations MUST preserve the original sequence and result; sequence exhaustion MUST fail atomically without marking sources processed. This order survives call and Analyst restarts and does not alter Runtime input admission or imply speech completion.

The shared local client carries a host-owned Analyst origin on message-write operations. The daemon MUST validate that origin and persist an exact source-event/expected-recipient association before appending its canonical message. Expected recipients MUST snapshot the same inbox routing semantics as delivery, including @foreman, @all, @peers and internal-actor exclusions. Later role changes MUST NOT retarget this persisted reply association. Internal origin metadata MUST NOT enter public chat data. Ordinary Send, tracked Send, and nested code-mode calls use this same boundary. Failed appends may leave an inert source intent, but only an existing canonical ledger event can activate it.

An active call consumes ledger increments in bounded pages and atomically registers candidate references before advancing cursors. Lost broadcasts MUST be recoverable from these cursors. The first acknowledgement does not terminate reply association. Only the expected Actor's exact reply association is a request result; subscription matching is independent and deduplicated by the canonical group/event pair. Attachment-only messages remain eligible; their contents MUST NOT be read automatically. Source copies of cross-group messages MUST NOT duplicate the destination.

Before Runtime handoff, persist the source references and receiving Analyst generation. On an unproven post-handoff failure, retain unknown delivery rather than automatically executing again. An accepted but unfinished source belonging to a replaced Analyst generation MUST also remain visible as unconfirmed, rather than implying that the replacement is still working on it. All inputs consumed by one Runtime turn MUST survive in its completion association set. Active user answers MUST NOT be silenced by a concurrent non-speaking background update. Only the explicitly reported Antigravity ACP FIFO described above may wait for Runtime completion; no hidden Runtime-busy wait queue or oldest-item eviction is permitted. Viewing or narrowing subscriptions suppresses unsubmitted background speech, not actual Group work or explicitly requested answers.

The private version-1 state has a bounded 10,000-reference working set, 16 MiB encoded size, and a 256-event scan page per Group. Reads are size-bounded and incompatible state MUST NOT be silently reset. At most 128 completed notification references are retained for recent source links. Capacity exhaustion MUST stop scanning before losing references or advancing past unregistered events. Unknown handoffs MUST NOT be evicted to free capacity. Completed notification attempts may be compacted after their cursor has passed; Group deletion retires its derived references, never another Group with the same name. Source associations remain until their Group is retired; they are not synthetic permanently-running tasks. Voice-off MUST NOT cause notification model turns. Cancelling an unhanded background candidate because it was viewed or excluded by preferences MUST retire its viewed reference in the same transaction: scan has already passed that source. Viewed references ahead of the scan cursor, explicit request context, and handed-off observations MUST remain available for their existing consumption and output checks.

actor_new_session ​

Args:

ts
{ group_id: string; actor_id: string; by?: string }

Result:

ts
{ actor: Record<string, unknown>; event: CCCSEventV1; new_session: true }

Notes:

  • Supported for Antigravity, claude, codex, Grok, OpenCode, and Kilo actors.
  • Antigravity in its default TUI mode uses its ordinary process lifecycle: stop the existing process if present, then start the actor with the same runtime settings and a fresh deferred bootstrap. CCCC does not issue a native /clear command or claim automatic Antigravity session resume.
  • Other supported runtimes stop the current actor process if present, clear CCCC's saved runtime session metadata for that actor, then start the actor with the same runtime settings. Receipt selection, snapshot and rollback MUST resolve the current linked Runtime Profile before selecting ACP versus native metadata; the other surface's receipt remains intact.
  • Does not delete provider-side conversation/session history.

runtime_hermes_status ​

Return Hermes runtime setup diagnostics for the selected user Hermes profile.

Args:

ts
{}

Result:

ts
{
  runtime: "hermes"
  setup_ready: boolean
  auth_ready: boolean
  launch_ready: boolean
  hermes_cli: { available: boolean; path?: string; version?: string }
  hermes_home: string
  profile: { name: "default"; dir: string; exists: boolean; config_path: string; config_exists: boolean }
  mcp: Record<string, unknown>
  auth: Record<string, unknown>
  phase0_gates: Array<Record<string, unknown>>
  issues: string[]
}

Notes:

  • CCCC does not create or select a separate Hermes profile.
  • HERMES_HOME, when supplied by the user, is treated as ordinary runtime environment. Explicit Actor/Profile environment takes precedence over the host default.
  • mcp.env must persist ${CCCC_HOME}, ${CCCC_GROUP_ID}, and ${CCCC_ACTOR_ID} placeholders so each actor process resolves its own CCCC identity.

runtime_hermes_prepare ​

Configure the cccc MCP server in the selected Hermes profile through Hermes' official MCP setup flow.

Args:

ts
{
  cwd?: string
  auto_enable_tools?: boolean // alias: yes
  force_mcp?: boolean         // alias: force
}

Result:

ts
{
  ok: boolean
  commands_run?: Array<Record<string, unknown>>
  status: Record<string, unknown>
  error?: { code: string; message: string }
}

Notes:

  • Setup MAY invoke hermes mcp add cccc ... and answer Hermes' discovery prompt only when auto_enable_tools/yes is true.
  • Discovery uses concrete CCCC env values, then CCCC normalizes saved Hermes MCP env back to actor-time placeholders.
  • Hermes and other runtime MCP configuration/check helpers are finite commands: input, both output streams, and exit share a deadline. Their owned process group / Windows Job is released on completion or timeout. Captured output is bounded to 2,000,000 bytes per stream; oversized output is an explicit command error, never partial setup evidence.

runtime_hermes_mcp_test ​

Run Hermes' MCP test command for the configured cccc server with probe CCCC actor env.

Args:

ts
{
  cwd?: string
  group_id?: string
  actor_id?: string
}

Result:

ts
{
  ok: boolean
  argv: string[]
  result?: { returncode: number; stdout: string; stderr: string }
  error?: { code: string; message: string }
}

actor_env_private_keys ​

List configured private env keys for an actor (keys only; never returns values).

Notes:

  • Private env is runtime-only and MUST NOT be persisted into the append-only group ledger.
  • Intended for secrets like API keys/tokens that may vary per actor.
  • Effective env at process start is: daemon_env (inherited) → actor.env → private_env → injected CCCC_GROUP_ID/CCCC_ACTOR_ID.
  • This operation is restricted to by="user" (agents should not be able to read/inspect secrets metadata).

Args:

ts
{ group_id: string; actor_id: string; by?: string }

Result:

ts
{ group_id: string; actor_id: string; keys: string[] }

actor_env_private_update ​

Update an actor's private env map (set/unset/clear). Values are never returned.

Args:

ts
{
  group_id: string
  actor_id: string
  by?: string
  set?: Record<string, string>  // set/overwrite keys
  unset?: string[]              // remove keys
  clear?: boolean               // remove all keys (wins)
}

Result:

ts
{ group_id: string; actor_id: string; keys: string[] }

8.5 Actor Profiles (Global) ​

Actor Profiles are global reusable runtime profiles stored under CCCC_HOME/state/actor_profiles/. They are not group-local settings.

actor_profile_list ​

Args:

ts
{ by?: string }

Result:

ts
{ profiles: Array<Record<string, unknown>> } // each profile includes usage_count

actor_profile_get ​

Args:

ts
{ profile_id: string; by?: string }

Result:

ts
{
  profile: Record<string, unknown>
  usage: Array<{
    group_id: string
    group_title?: string
    actor_id: string
    actor_title?: string
  }>
}

actor_profile_upsert ​

Create/update a profile with optimistic concurrency.

Args:

ts
{
  by?: string
  profile: {
    id?: string
    name: string
    runtime: string
    runtime_mode?: "default" | "acp"
    command?: string[] | string
    submit?: "enter" | "newline" | "none"
    env?: Record<string, string> // deprecated legacy input; values are migrated into profile secrets
    capability_defaults?: {
      autoload_capabilities?: string[]
      default_scope?: "actor" | "session" // default actor
      session_ttl_seconds?: number         // clamped to 60..86400
    } | null
  }
  expected_revision?: number
}

Notes:

  • Runtime variables are unified as profile secrets (actor_profile_secret_*).
  • profile.env is accepted only as a legacy bridge and migrated into profile secrets; stored profile env is kept empty.
  • command accepts the historical shell-command string for compatibility, but successful writes normalize it to a string[]. Readers must continue to accept an existing string until that profile is saved again.

Result:

ts
{ profile: Record<string, unknown> }

actor_profile_delete ​

Args:

ts
{ profile_id: string; by?: string; force_detach?: boolean }

Notes:

  • Default behavior rejects delete when the profile is still used by linked actors (profile_in_use).
  • With force_detach: true, linked actors are converted to custom first, then the profile is deleted.

Result:

ts
{
  deleted: true
  profile_id: string
  detached_count: number
  detached: Array<{ group_id: string; actor_id: string }>
}

actor_profile_secret_keys ​

List profile secret keys (masked previews only).

Args:

ts
{ profile_id: string; by?: string }

Result:

ts
{ profile_id: string; keys: string[]; masked_values: Record<string, string> }

actor_profile_secret_update ​

Update profile-level secrets (write-only values).

Args:

ts
{
  profile_id: string
  by?: string
  set?: Record<string, string>
  unset?: string[]
  clear?: boolean
}

Result:

ts
{ profile_id: string; keys: string[] }

actor_profile_secret_copy_from_actor ​

Copy an actor's effective explicit environment into a profile's secrets (server-side copy, values are never returned). Linked Actors use their referenced Profile environment, requiring read access to that source Profile; custom Actors use their private environment. Web draft secret edits are applied after copying, before reporting the Profile save as complete.

Args:

ts
{
  profile_id: string
  group_id: string
  actor_id: string
  by?: string
}

Result:

ts
{ profile_id: string; group_id: string; actor_id: string; keys: string[] }

actor_profile_secret_copy_from_profile ​

Copy one profile's current secret map into another profile (server-side copy, values are never returned).

Args:

ts
{
  profile_id: string
  source_profile_id: string
  by?: string
}

Result:

ts
{ profile_id: string; source_profile_id: string; keys: string[] }

actor_profile_copy_voice_analyst_secrets ​

Copy the Voice Analyst custom private environment into a Runtime Profile without returning values. This operation is administrator-only and is used when the Voice settings surface saves its Custom configuration as a reusable Profile.

Args:

ts
{
  profile_id: string
  profile_scope?: "global" | "user"
  profile_owner?: string
  by?: string
}

Result:

ts
{ profile_id: string; keys: string[] }

8.6 Chat Messaging ​

send ​

Append a chat.message event. message_mode is required and is the only chat-delivery selector.

Args (core):

ts
{
  group_id: string
  text: string
  by?: string
  to?: string[]                 // empty/omitted materializes group default_send_to
  message_mode: "send" | "request_reply" | "mail"
  path?: string                 // optional filesystem path to attribute scope_key
  attachments?: unknown[]       // attachment refs (implementation-defined)
  refs?: ReferenceV1[]          // structured message refs, e.g. presentation_ref/task_ref
  insight?: string              // optional provisional sender perspective; max 1200 characters
  require_peer_insight?: boolean // profile gate; default false
  src_group_id?: string         // relay provenance (both required if either is set)
  src_event_id?: string
  dst_group_id?: string         // optional "send record" metadata (source messages)
  dst_to?: string[]
  dst_message_mode?: "send" | "request_reply" | "mail"
}

Result:

ts
{
  event: CCCSEventV1 // kind="chat.message"
  message_mode: "send" | "request_reply" | "mail"
}

send and request_reply preflight the concrete runtime audience, append the message, and then attempt prompt delivery. mail appends only; it MUST NOT wake, steer, queue, open a browser, or write to a runtime input. request_reply MUST resolve to explicit concrete recipients and rejects broadcast selectors. In the Rust implementation, omitted recipients and @foreman resolve to the current enabled Foreman actor ID before this validation. New daemon callers MUST send message_mode; missing or old priority, reply_required, or requires_ack fields fail validation. After aliases and selectors are normalized, one message MUST address either the human user or one or more agents, never both. mail is valid only for agent recipients. Mixed audiences fail with mixed_recipient_kinds; Mail addressed to the user fails with mail_requires_actor_recipient. Validation occurs before the message, blobs, delivery claims, or other side effects are written. The immediate response confirms ledger acceptance and echoes the canonical mode; it is not transport evidence. Per-recipient delivery truth is reported only by daemon-authored runtime.delivery events and the corresponding status queries.

Every daemon-rendered chat.message handed to an actor runtime MUST expose the current ledger identity before the message body:

text
[cccc] <sender> → <recipients> [event_id=<current_event_id>]: <body>
[cccc] <sender> → <recipients> (reply:<short_parent_id>) [event_id=<current_event_id>]: <body>
[cccc] <sender> → <recipients> [event_id=<current_event_id> reply_required]: <body>

event_id is the target an actor passes to cccc_message_reply when answering the delivered message. A reply MAY expose its parent as the existing short (reply:<id>) correlation hint; it MUST NOT repeat the full parent reply_to in the runtime envelope. The ledger remains authoritative for the full parent identity and canonical message_mode. Ordinary send and delivered mail messages need no mode label because the label does not change the receiver's next action. A request_reply message MUST instead add the actionable reply_required marker in the same metadata block. Implementations MUST use the same envelope in native-terminal, structured, and Web Model delivery. The metadata MUST remain in the existing first header line so adding it does not turn a one-line message into multiple runtime input lines. It does not add or mutate ledger fields, and it is not required for system.notify. Every delivered batch containing one or more chat.message events MUST append one concise instruction telling the actor to call cccc_message_reply with its event_id argument set to the target message's current event_id. The instruction MUST reserve cccc_message_send for a new message and MUST appear only once per batch.

Native MCP adapters MUST preserve a failed daemon operation's error.code, error.message, and non-empty error.details in the tool result's structuredContent.error; flattening the daemon error into text is not conforming. MCP results MUST preserve the operation's factual receipt and error fields. They MAY add the passive mail_pending summary, but MUST NOT append instructions to take over, reframe, or review unrelated work. Actor collaboration guidance belongs in the configured runtime instructions, not operation receipts. A peer-insight validation error MUST retain delivery_state="not_sent" and new_side_effects=false, with a concise explanation of the missing field.

The MCP file surface separates read permission from message delivery: cccc_file(action="read"|"info"|"blob_path", rel_path=...) is read-only and MUST reject action="send" without writing blobs, messages or delivery work. cccc_file_send(path=..., ...) sends through the existing send_files or connect_send_files operation and retains its audience, sender, peer-insight, path-scope and idempotency rules. Tool annotations describe the complete exposed action set; a mixed read/write tool MUST NOT be labelled read-only.

message_upload_preflight ​

Validate a Web-owned staged upload before its temporary files are committed to the group blob store. This operation is side-effect free and exists so both Web implementations use the selected daemon's canonical send and reply rules rather than reimplementing message policy in the HTTP port.

Args:

ts
{
  operation: "send" | "reply" | "send_cross_group"
  group_id: string
  dst_group_id?: string         // required when operation="send_cross_group"
  text?: string
  by?: string
  to?: string[]
  message_mode: "send" | "request_reply" | "mail"
  reply_to?: string             // required when operation="reply"
  path?: string
  client_id?: string
  refs?: ReferenceV1[]
  insight?: string
  require_peer_insight?: boolean
  has_attachments: boolean      // temporary upload parts exist; no blob refs yet
}

Result:

ts
{ ready: true }
// or, for send/reply when client_id already identifies an accepted message:
{
  ready: false
  duplicate: true
  result: { event: CCCSEventV1; message_mode: "send" | "request_reply" | "mail" }
}

The operation MUST perform the deterministic validation used by the eventual send, reply, or send_cross_group, including mode, audience, target, scope, Insight, and content, without waking actors, changing group state, writing the ledger, storing blobs, or starting delivery. send and reply additionally perform successful-idempotency lookup and MUST return a duplicate result before an upload is committed. Local cross-group uploads are unsupported and rejected. Connect reply uploads use the canonical reply authorization. The HTTP port MUST discard its staged files on rejection or duplicate replay. The eventual operation MUST validate again at the commit boundary; preflight is not a reservation.

send_files ​

Store one or more files from the group's active scope in the group blob store, then append one chat.message carrying those files as attachments. This is the daemon-owned upload boundary for SDK clients; callers MUST NOT write directly to state/blobs/ or manufacture attachment records.

Args:

ts
{
  group_id: string
  paths: string[]               // absolute, or relative to the active scope root
  text?: string                 // defaults to a compact file notice
  by?: string
  to?: string[]
  message_mode: "send" | "request_reply" | "mail"
  insight?: string
  client_id?: string
}

Every resolved path MUST be a regular file beneath the group's active scope. All paths are validated and read before any message is appended. The resulting event uses the selected message_mode recipient, permission, and delivery rules. If client_id already identifies an accepted message, the daemon MUST return that message before reading or storing new source content. After source paths have been validated and read, deterministic normal-send validation (including message mode, recipients, and required peer insight) MUST succeed before any new blob is stored. A request rejected by that preflight MUST NOT add a blob or ledger event.

Result:

ts
{
  event: CCCSEventV1 // kind="chat.message", data.attachments contains stored blobs
  message_mode: "send" | "request_reply" | "mail"
}

reply ​

Append a chat.message with reply_to and quote_text. Connect replies derive the qualified destination and participant from the original local event, as specified in CCCC_CONNECT_V1.md. Historical manual Bridge routes are rejected; their provenance sender is never a local recipient token.

Args:

ts
{
  group_id: string
  reply_to: string
  text: string
  by?: string
  to?: string[]                 // local: original sender; Connect: original remote participant
  message_mode?: "send" | "mail" // default: "send"
  attachments?: unknown[]
  refs?: ReferenceV1[]
  insight?: string
  require_peer_insight?: boolean // profile gate; default false
}

Replies default to message_mode="send". Callers MAY choose message_mode="mail" when fulfilling the original reply obligation does not justify immediately prompting the recipient. Both modes fulfill the original reply obligation; message_mode="request_reply" is invalid for a reply and a reply cannot create another generic reply obligation. Mail replies are valid only when every reply recipient is an agent. A reply to the human user MUST use Send. Replies also reject a recipient list that mixes the human user with agents.

Result:

ts
{
  event: CCCSEventV1 // kind="chat.message"
  message_mode: "send" | "mail"
}

tracked_send ​

Create a durable task and send one linked visible delegation message. This is an explicit composite write; the daemon MUST NOT infer it from arbitrary chat text.

Args:

ts
{
  group_id: string
  title: string
  text: string
  by?: string
  to?: string[]
  outcome?: string
  checklist?: { text: string; status?: "pending" | "in_progress" | "done" | string }[]
  task_priority?: string        // task-domain priority; does not affect message delivery
  assignee?: string             // defaults from one concrete to actor when possible
  waiting_on?: "none" | "user" | "actor" | "external"
  handoff_to?: string
  notes?: string
  idempotency_key?: string
  refs?: ReferenceV1[]
  insight?: string
  require_peer_insight?: boolean // profile gate; default false
}

Result:

ts
{
  task_id: string
  task_ref: ReferenceV1          // kind="task_ref"
  event?: CCCSEventV1            // present when message_sent=true
  event_id?: string
  message_mode?: "send"          // present when message_sent=true
  task_created: boolean
  message_sent: boolean
  partial_failure: boolean
  replayed?: boolean
}

Notes:

  • task_ref in the emitted chat.message.data.refs is the canonical message-task link.
  • The linked visible message always uses message_mode="send"; task lifecycle is the only completion authority for tracked work.
  • priority, message_priority, and reply_required are rejected; callers use task_priority only for the task-domain field.
  • If task creation fails, no message is sent.
  • If message delivery fails after task creation, the response MUST report partial_failure=true.
  • Successful retries SHOULD use idempotency_key / client_request_id to avoid duplicate task/message pairs.

send_cross_group ​

Cross-group send implemented as:

  1. Write a source chat.message in the origin group as a local Send to user, with the actual remote dst_group_id, dst_to, and dst_message_mode metadata.
  2. Write a forwarded chat.message in the destination group with src_group_id / src_event_id provenance.

Args:

ts
{ group_id: string; dst_group_id: string; text: string; by?: string; to?: string[]; message_mode: "send" | "request_reply" | "mail"; insight?: string; require_peer_insight?: boolean }

Result:

ts
{ src_event: CCCSEventV1; dst_event: CCCSEventV1 }

Notes:

  • Local user / system principals and registered source actors, including peers, may send cross-group messages; unknown source actors are rejected. The foreman-only group administration permission does not apply to message delivery.
  • Local cross-group forwarding rejects attachments. Connect sends use their qualified instance/Group destination and bounded attachment contract.

Agent Insight Profile marker ​

require_peer_insight is an internal request-profile marker, not a global message-validity rule. It defaults to false. When true, the daemon resolves the operation's real audience and rejects a new peer-facing message whose normalized insight is empty. User-only sends remain valid without Insight.

The check MUST occur after routing and successful-idempotency lookup, but before this request creates a new message, task, actor wake, or remote outbox entry. The recommended error code is peer_insight_required, with details.delivery_state="not_sent" and details.new_side_effects=false. Invalid Insight type or length SHOULD use invalid_insight instead. Existing accepted idempotent operations MUST replay their original result without being reinterpreted by a newer profile requirement.

Connect peers MUST satisfy the current account and peer protocol version gates before messages are exchanged. There is no fallback to manual Bridge.

reply_request_cancel ​

Cancel every still-open recipient obligation for an existing message_mode="request_reply" message.

Args:

ts
{ group_id: string; source_event_id: string; by?: string }

Result:

ts
{ event: CCCSEventV1 } // kind="chat.reply_request.cancelled"

Only the source sender or user may cancel. The operation is idempotent for an already-cancelled source event.

message_deliver ​

Explicitly attempt prompt delivery for one existing message without appending a second chat.message. This promotes Mail or retries a blocked/failed Send.

Args:

ts
{
  group_id: string
  source_event_id: string
  actor_ids: string[]
  by?: string
  force_ambiguous?: boolean // default false; explicit warning/confirmation required
}

Only the source sender or user may request delivery. Recipients MUST be explicit concrete recipients of the source event. Existing accepted evidence is never retried. Existing claimed evidence reports delivery_in_progress. Existing ambiguous evidence is rejected unless force_ambiguous=true. Disabled recipients return delivery_blocked without creating a delivery claim. A request that reserves new claims while the Group is paused or stopped explicitly resumes the Group before handoff. A conflicting, already accepted, or otherwise no-op request MUST NOT change lifecycle state. A successful request records all claims before returning:

ts
{
  event: CCCSEventV1
  actor_ids: string[]
  delivery_state: "claimed"
}

Terminal per-recipient states remain authoritative in runtime.delivery and the normal message-status projection; they may settle before or after this operation returns.

Delivery evidence, Mail notice, and reply notice ​

For each concrete recipient handoff, the daemon appends runtime.delivery using the CCCS contract. claimed precedes external I/O; accepted, failed, or ambiguous records the outcome. A transport queue accepting a payload is an accepted handoff; it is not a claim that the model understood it. Automatic retry is forbidden after accepted or ambiguous. Concurrent claimants treat claimed as in-progress. Daemon startup settles claims stranded by the prior daemon process to ambiguous before attempting runtime recovery. Recovery MUST interpret a same-generation legacy chat.read.event_id as an inclusive ledger watermark for that actor, not as a per-event receipt. It MUST use the furthest valid referenced ledger position and exclude each legacy system.notify at or before that position. A later notification MUST also be excluded when data.event_id, data.related_event_id, or data.context.event_id references an event at or before the watermark. Targets outside the current actor generation or after their chat.read record are invalid compatibility boundaries and MUST be ignored. This rule prevents already-consumed pre-runtime.delivery nudges from being replayed after an upgrade.

Mail is eligible for an active notice for one recipient only while all of these hold: the source event belongs to the recipient's current actor generation, is unread, has message_mode="mail", has no reply from that recipient, and has no accepted or ambiguous manual-delivery record. Reply and manual delivery suppress only the active notice; neither advances the Mail cursor nor removes the message from inbox_peek / inbox_read. Concrete-recipient Mail batches retain the earliest eligible deadline. New Mail does not reset it. Broadcast-like Mail remains visible in the Inbox but does not start an active runtime notice timer.

After mail_notice_after_seconds, an active/idle enabled actor may receive one content-free system.notify(kind="mail_notice") for the concrete batch. The notice states only that Mail is waiting and directs the actor to inbox_read; it does not copy message bodies, repeat, escalate, or create another Inbox obligation. Bootstrap, the next explicit Push, and low-frequency coordination responses MAY carry a passive mail_pending count without writing a notice.

A request_reply obligation starts its timer only after an accepted runtime delivery. If no matching reply_to message or cancellation has closed it by reply_notice_after_seconds, an active/idle enabled actor may receive one content-free system.notify(kind="reply_notice"). It never repeats or escalates. Failed, blocked, or ambiguous delivery does not nudge the recipient.

Paused, stopped, or disabled actors are never woken for either notice. Pending Mail is surfaced at the next start/resume bootstrap and begins a fresh notice window. Explicit start/resume may recover only blocked Push work from the current actor generation that has no accepted/ambiguous delivery evidence; Mail is never automatically promoted. No implementation may depend on a universal runtime "idle" detector.

8.7 Inbox (Mail Cursor) ​

inbox_peek ​

Return unread chat.message events whose message_mode="mail" without changing the Mail cursor. This is for UI polling, bootstrap previews, and diagnostics; agent tooling SHOULD use inbox_read. Send, Send + Reply, and system.notify events are never members of this projection.

The current actor generation begins at the latest actor.add for that actor id in ledger append order. Inbox membership and Mail read status MUST exclude events before that boundary, including after an actor is removed and re-added with the same id. When a cursor contains a resolvable event_id, cursor advancement and unread membership MUST use ledger append order; timestamp is informational only.

Passive mail_pending summaries MUST preserve these same cursor, recipient and Actor-generation boundaries without materializing a persistent full-ledger index merely to count unread Mail. A reverse scan MAY stop at the cursor or generation boundary; segment discovery and reads MUST share the ledger reader boundary so concurrent rotation cannot hide events. These reads MUST NOT advance the cursor.

Args:

ts
{ group_id: string; actor_id: string; by?: string; limit?: number }

Result:

ts
{ messages: CCCSEventV1[]; cursor: { event_id: string; ts: string } }

inbox_read ​

Atomically return and consume the next unread Mail prefix for an actor.

Args:

ts
{ group_id: string; actor_id: string; by?: string; limit?: number }

Result:

ts
{
  messages: CCCSEventV1[]
  cursor: { event_id: string; ts: string; updated_at: string }
  event: CCCSEventV1 | null
}

Selection, mail.read append, Mail cursor persistence, and returned messages MUST be one consuming operation under the canonical ledger/cursor transaction boundary. Ordering is the append order of the Mail projection: non-Mail events between two Mail events are intentionally skipped and do not acquire read state. If the event or cursor write fails, the operation returns no message bodies. An empty Inbox returns messages=[] and event=null without moving the cursor.

The ledger mail.read event is the authoritative commit record; the cursor file is a rebuildable projection. Implementations MUST persist state/read_cursors.pending.json before appending mail.read, append the event before advancing the cursor projection, and clear the marker after both writes commit. The shared recovery marker is:

json
{
  "schema": 1,
  "group_id": "...",
  "actor_id": "peer1",
  "expected": {"event_id": "...", "ts": "..."},
  "target": {"event_id": "...", "ts": "...", "updated_at": "..."}
}

After interruption, a matching mail.read fact completes the target cursor; without that fact the marker is discarded and the old cursor remains. Recovery MUST never move an already-later cursor backward. This makes a process exit between the two durable writes recoverable without replaying or silently skipping Mail.

The canonical cursor file is state/read_cursors.json with {"schema":1,"cursors":{...}}. Documents without this Mail-specific schema are not delivery boundaries and MUST be ignored; in particular, a cursor from the former all-message read model cannot suppress Mail.

message_history ​

Return an actor-visible, non-consuming history of chat.message events. This is the explicit path for inspecting past Send or Send + Reply traffic; it does not change the Mail cursor.

Args:

ts
{
  group_id: string
  actor_id: string
  by?: string
  mode?: "all" | "send" | "request_reply" | "mail"
  query?: string
  before_event_id?: string
  limit?: number
}

Result:

ts
{ messages: CCCSEventV1[]; has_more: boolean }

Results are newest-first, limited to the actor's current generation, and MUST contain only messages sent by or addressed to that actor. The operation is read-only and MUST NOT append mail.read or mutate any delivery state.

8.8 Context and Tasks ​

context_get ​

Args:

ts
{ group_id: string; detail?: "overview" | "summary" | "full" }

Result:

ts
{
  version: string
  tasks_version: string
  coordination: {
    brief: {
      objective: string
      current_focus: string
      constraints: string[]
      project_brief: string
      project_brief_stale: boolean
      updated_by: string
      updated_at: string
    }
    tasks?: Array<Record<string, unknown>>
    recent_decisions?: Array<{ at: string; by: string; summary: string; task_id?: string | null }>
    recent_handoffs?: Array<{ at: string; by: string; summary: string; task_id?: string | null }>
  }
  agent_states: Array<{
    id: string
    hot: {
      active_task_id?: string | null
      focus?: string | null
      blockers?: string[]
      next_action?: string | null
    }
    warm: {
      what_changed?: string | null
      open_loops?: string[]
      commitments?: string[]
      environment_summary?: string | null
      user_model?: string | null
      persona_notes?: string | null
    }
    updated_at?: string | null
  }>
  actors_runtime?: Array<Record<string, unknown>>
  tasks_summary?: {
    total: number
    done: number
    active: number
    planned: number
    archived: number
    root_count?: number
  }
  attention?: {
    blocked?: number | Array<Record<string, unknown>>
    waiting_user?: number | Array<Record<string, unknown>>
    pending_handoffs?: number | Array<Record<string, unknown>>
  }
  board?: {
    planned?: Array<Record<string, unknown>>
    active?: Array<Record<string, unknown>>
    done?: Array<Record<string, unknown>>
    archived?: Array<Record<string, unknown>>
  }
  meta?: Record<string, unknown>
}

Notes:

  • Task objects returned in coordination.tasks, board, or task_list include task_type.
  • Daemon IPC defaults detail to full; the Web HTTP route defaults it to summary for routine refreshes.
  • detail="overview" does not read task files and omits coordination.tasks, tasks_summary, attention, and board. It retains the coordination brief, recent decisions/handoffs, agent states, version, tasks_version, and metadata. Web startup and the context modal use this projection before loading task pages separately.
  • detail="summary" omits board, recent coordination notes, and live runtime probing. Its attention fields are counts, but each task in coordination.tasks MUST retain every task-editor field, including outcome, notes, and checklist, so a summary refresh cannot erase a client's editable draft.
  • detail="full" returns the complete task objects and board projections.
  • MCP convenience tools MUST preserve their focused response contracts instead of exposing this complete snapshot: cccc_coordination(action="get") returns only version, coordination, attention, board, and tasks_summary, while cccc_agent_state(action="get") returns only version plus the selected agent_state (or agent_states when no actor is selected). include_warm=false keeps only id, hot, and updated_at; archived tasks remain hidden from coordination unless include_archived=true.

context_sync ​

Args:

ts
{ group_id: string; by?: string; ops: Array<Record<string, unknown>>; dry_run?: boolean; if_version?: string }

Operation item shape (normative minimum):

ts
type ContextOpV1 = { op: string } & Record<string, unknown>

Notes:

  • Unknown op names SHOULD be rejected.
  • See docs/standards/CCCC_CONTEXT_OPS_V1.md for the v3 operation list and permission/storage failure semantics.
  • if_version is compared with the current locked snapshot; a mismatch returns version_conflict.
  • Invalid or unauthorized batches do not persist. Unreadable canonical state and persistence failures return io_error; after such a failure, reload before retrying because per-file atomic writes do not imply multi-file rollback.

Result:

ts
{
  success: true
  dry_run: boolean
  changes: Array<Record<string, unknown>>
  version: string
  space_sync?: {
    queued: boolean
    reason?: "not_bound" | "binding_inactive" | "missing_remote_space_id" | "provider_disabled" | "enqueue_failed"
    deduped?: boolean
    job_id?: string
    provider?: "notebooklm"
    kind?: "context_sync"
    idempotency_key?: string
    error?: string
  }
}

memory_reme_layout_get ​

Args:

ts
{ group_id: string }

Result:

ts
{
  group_label: string
  memory_root: string
  memory_file: string
  daily_dir: string
  today_daily_file: string
  backend: { name: "local"; vector_enabled: false; fts_enabled: true }
}

memory_reme_index_sync ​

Args:

ts
{
  group_id: string
  mode?: "scan" | "rebuild"   // default "scan"
}

Result:

ts
{
  indexed_files: number
  indexed_chunks: number
  watched_paths: string[]
  last_sync_at: string
}

Args:

ts
{
  group_id: string
  query: string
  max_results?: number           // 1..50, default 5
  min_score?: number             // 0..1, default 0.1
  sources?: string[]             // default ["memory"]
  vector_weight?: number         // 0..1 (optional)
  candidate_multiplier?: number  // 1..20 (optional)
}

Result:

ts
{
  hits: Array<{
    path: string
    start_line: number
    end_line: number
    score: number
    snippet: string
    source: string
    raw_metric?: number
    metadata: Record<string, unknown>
  }>
  count: number
  took_ms: number
}

memory_reme_get ​

Args:

ts
{
  group_id: string
  path: string
  offset?: number   // 1-indexed, default 1
  limit?: number    // default 200
}

Result:

ts
{
  path: string
  offset: number
  limit: number
  total_lines: number
  content: string
}

memory_reme_context_check ​

Args:

ts
{
  group_id: string
  messages: Array<{ role: string; name?: string; content: string }>
  context_window_tokens?: number
  reserve_tokens?: number
  keep_recent_tokens?: number
}

Result:

ts
{
  needs_compaction: boolean
  token_count: number
  threshold: number
  messages_to_summarize: Array<Record<string, unknown>>
  turn_prefix_messages: Array<Record<string, unknown>>
  left_messages: Array<Record<string, unknown>>
  is_split_turn: boolean
  cut_index: number
}

memory_reme_compact ​

Args:

ts
{
  group_id: string
  messages_to_summarize: Array<{ role: string; name?: string; content: string }>
  turn_prefix_messages?: Array<{ role: string; name?: string; content: string }>
  previous_summary?: string
  language?: string
  return_prompt?: boolean
}

Result:

ts
{ summary: string } | { prompt: Record<string, string> }

memory_reme_daily_flush ​

Args:

ts
{
  group_id: string
  messages: Array<{ role: string; name?: string; content: string }>
  date?: string               // YYYY-MM-DD
  version?: string            // default "default"
  language?: string           // default "en"
  return_prompt?: boolean
  signal_pack?: Record<string, unknown>
  signal_pack_token_budget?: number // default 320
  dedup_intent?: "new" | "update" | "supersede" | "silent" // default "new"
  dedup_query?: string
}

Result:

ts
{
  status: "written" | "silent"
  reason?: "empty_summary" | "precheck_silent" | "persistence_idempotency_key" | "persistence_content_hash"
  target_file: string
  content_hash: string
  bytes_written: number
  signal_pack?: {
    schema: string
    token_budget: number
    token_estimate: number
    truncated: boolean
  }
  dedup?: {
    intent: "new" | "update" | "supersede" | "silent"
    query: string
    candidate_count: number
    top_score: number
    precheck_decision: "new" | "update" | "supersede" | "silent"
    final_decision: "new" | "update" | "supersede" | "silent"
    final_reason: "accepted" | "empty_summary" | "precheck_silent" | "persistence_idempotency_key" | "persistence_content_hash"
    decision: "new" | "update" | "supersede" | "silent" // alias of final_decision
    hits: Array<{ path: string; start_line: number; score: number }>
    error?: string
  }
}

memory_reme_write ​

Args:

ts
{
  group_id: string
  target: "memory" | "daily"
  content: string
  date?: string               // required when target="daily"
  mode?: "append" | "replace" // default "append"
  idempotency_key?: string
  actor_id?: string
  source_refs?: string[]
  tags?: string[]
  supersedes?: string[]
  dedup_intent?: "new" | "update" | "supersede" | "silent" // default "new"
  dedup_query?: string
}

Result:

ts
{
  file_path: string
  line_count: number
  content_hash: string
  status: "written" | "silent"
  reason?: "precheck_silent" | "persistence_idempotency_key" | "persistence_content_hash"
  dedup?: {
    intent: "new" | "update" | "supersede" | "silent"
    query: string
    candidate_count: number
    top_score: number
    precheck_decision: "new" | "update" | "supersede" | "silent"
    final_decision: "new" | "update" | "supersede" | "silent"
    final_reason: "accepted" | "precheck_silent" | "persistence_idempotency_key" | "persistence_content_hash"
    decision: "new" | "update" | "supersede" | "silent" // alias of final_decision
    hits: Array<{ path: string; start_line: number; score: number }>
    error?: string
  }
}

task_list ​

Args:

ts
{
  group_id: string
  task_id?: string
  task_ids?: string // comma-separated exact ids, at most 100
  status?: "planned" | "active" | "done" | "archived"
  statuses?: string // comma-separated statuses for an atomic multi-column page
  query?: string
  assignee?: string // use "__unassigned__" for tasks without an assignee
  attention?: "blocked" | "waiting_user" | "handoff" | "unassigned"
  offset?: number
  limit?: number // 1..100
  include_index?: boolean
}

Result:

ts
// Exact lookup when task_id is present:
{
  task: Record<string, unknown> & { children: Array<Record<string, unknown>> }
  tasks_version: string
  delete_info: { allowed: boolean; total: number; reason: string }
}

// Batch exact lookup when task_ids is present:
{
  tasks: Array<Record<string, unknown>> // requested order; missing ids omitted
  tasks_version: string
}

// Paged listing when limit is present:
{
  tasks: Array<Record<string, unknown>>
  count: number
  total_count: number
  offset: number
  limit: number
  has_more: boolean
  tasks_version: string
  facets: {
    status_counts: Record<string, number>
    blocked: number
    waiting_user: number
    pending_handoffs: number
    unassigned: number
    assignees: string[]
  }
}

// Atomic multi-column listing when statuses is present:
{
  pages: Partial<Record<"planned" | "active" | "done" | "archived", {
    tasks: Array<Record<string, unknown>>
    count: number
    total_count: number
    offset: number
    limit: number
    has_more: boolean
  }>>
  tasks_version: string
  facets: {
    status_counts: Record<string, number>
    blocked: number
    waiting_user: number
    pending_handoffs: number
    unassigned: number
    assignees: string[]
  }
  task_index?: Array<{
    id: string
    title: string
    status: string
    assignee?: string | null
    parent_id?: string | null
  }>
}

// Compatibility listing when task_id, task_ids, statuses, and limit are absent:
{ tasks: Array<Record<string, unknown>> }

Notes:

  • Returned task objects include task_type.
  • task_id takes precedence over other arguments, followed by task_ids. status and statuses MUST NOT be combined. A statuses request reads one task snapshot and returns every requested column at the same tasks_version.
  • include_index=true adds an unfiltered, non-archived, lightweight task index. It is intended for relationship selectors; it is not a substitute for exact task detail.
  • Filters are applied before pagination. Planned tasks sort newest-created first; other columns sort most-recently-updated first, with numeric task id as a stable tie-breaker.
  • offset requires limit. tasks_version is the task-specific revision, not the broader context revision. Clients MUST discard a continuation response and restart all loaded pages when it differs from the initial page revision.

presence_get has been removed. Agent state is returned in context_get.result.agent_states.

8.9 Structured Runtime State ​

headless_status ​

Args:

ts
{ group_id: string; actor_id: string }

Result:

ts
{ state: Record<string, unknown> }

headless_control_state / headless_control ​

Read returns { state: null | { generation, runtime, runtime_mode, runner, running, working, permissions } } for the actual managed session. The Web port exposes GET/POST /api/v1/groups/{group_id}/actors/{actor_id}/headless/control. Read does not create a session. Write requires by="user", exact generation and { action:"permission", request_id, allow:boolean }, { action:"interaction", request_id, response } or { action:"cancel" }. Permission metadata contains the pending operation for user review, never private environment/configuration. Cursor blocking cursor/ask_question and cursor/create_plan requests MUST remain explicit user decisions even under tool YOLO. Metadata uses kind=question with questions or kind=plan with overview / plan; responses contain the documented Cursor outcome (answered/skipped/cancelled or accepted/rejected/cancelled). Answers MUST match all pending question IDs and allowed option IDs/cardinality. CCCC MUST bind pending interactions to the active session generation and turn, retire them on cancellation/completion/disconnect, and reject arbitrary RPC replies. Voice structured controls use the same interaction action and validator. request_id MUST be an opaque CCCC-generated token unique to each pending interaction and bound to its exact turn; clients MUST echo it unchanged. Provider JSON-RPC ids remain private to the adapter and MUST NOT serve as these tokens, because providers may reuse them across turns. Stale tokens, generations and settled requests fail without modifying another session. Cancellation stops the current turn; ordinary queued Group messages remain owned by the ledger.

headless_set_status ​

Args:

ts
{ group_id: string; actor_id: string; status: "idle" | "working" | "waiting" | "stopped"; task_id?: string | null }

Result:

ts
{ state: Record<string, unknown> | null }

8.10 System Notifications (Not Chat) ​

system_notify ​

Args:

ts
{
  group_id: string
  by?: string
  kind?: string
  priority?: "low" | "normal" | "high" | "urgent"
  title?: string
  message?: string
  target_actor_id?: string | null
  im_visibility?: "internal" | "public" // default: "internal"
  context?: Record<string, unknown>
}

system.notify is internal by default. An IM bridge may forward it only when im_visibility="public"; actor-targeted notifications are never eligible for external IM delivery. Producers must opt in explicitly instead of relying on to, actor_id, or target_actor_id inference.

Result:

ts
{ event: CCCSEventV1 } // kind="system.notify"

There is no generic notification acknowledgement operation. Domain workflows must expose domain lifecycle operations; chat reply obligations use request_reply and reply_request_cancel.

8.11 Terminal Diagnostics and Attach ​

terminal_tail ​

Args:

ts
{ group_id: string; actor_id: string; by?: string; max_chars?: number; strip_ansi?: boolean; compact?: boolean }

max_chars limits the final returned Unicode text. Implementations MUST render the complete retained PTY backlog before applying this limit; truncating the raw ANSI/VT byte stream first can start replay inside an escape sequence or incremental screen update and produce corrupt snapshots.

Result:

ts
{ group_id: string; actor_id: string; warning: string; hint: string; text: string; end_cursor: number }

end_cursor is the exclusive raw PTY byte cursor captured with the backlog used to produce text. A terminal client MAY display the rendered snapshot and then attach its live stream with since=end_cursor; the stream must replay output produced after the snapshot so the transition is gap-free.

terminal_snapshot ​

Return a bounded rendered screen snapshot and the exact raw cursor boundary used to render it. This operation is intended for diagnostics; interactive clients use terminal_replay so they can rebuild scrollback from the original ANSI stream.

Args:

ts
{ group_id: string; actor_id: string; by?: string; limit_bytes?: number }

Result:

ts
{ data: string; start_cursor: number; end_cursor: number }

The implementation MUST apply the same group transcript visibility policy as terminal_tail and terminal_history.

terminal_replay ​

Return one bounded page of raw ANSI output from the active PTY session's in-memory ring. This operation MUST NOT read the durable archive or a completed session. The first request atomically captures a UTF-8-complete replay_end_cursor. Callers pass that value back as end_cursor on every following page, so output produced during replay cannot extend the initial replay loop.

Args:

ts
{ group_id: string; actor_id: string; by?: string; after?: number; end_cursor?: number; limit_bytes?: number }

Result:

ts
{
  replay_end_cursor: number
  history: {
    data: string
    start_cursor: number
    end_cursor: number
    has_more: boolean
    cursor_expired: boolean
  }
}

The default page limit is 512 KiB. history.has_more is relative to the fixed replay_end_cursor, not the moving live tail. The implementation MUST apply the same group transcript visibility policy as terminal_tail and must leave an incomplete UTF-8 suffix for a later live page.

terminal_history ​

Args:

ts
{ group_id: string; actor_id: string; by?: string; before?: number; render_before?: number; limit_bytes?: number; strip_ansi?: boolean; compact?: boolean }

Result:

ts
{
  group_id: string
  actor_id: string
  warning: string
  hint: string
  text: string
  start_cursor: number
  end_cursor: number
  has_more: boolean
  cursor_expired: boolean
}

For backward paging of rendered text, pin the first response's end_cursor as render_before on subsequent requests. before and limit_bytes select the next older page's start, while the returned range extends through render_before. The server renders that contiguous range once; clients replace their previous rendering rather than concatenate independently rendered pages. The cumulative range is limited to 50 MB and excludes output written after the pinned end. If retention or terminal clear overtakes the pinned range, return an empty page with cursor_expired=true, has_more=false, and both cursors at the pinned end, not invalid_args. Clients retain already displayed text when the older cursor does not advance and show the existing expired-history warning. Omitting render_before preserves the existing per-page contract. With strip_ansi=true, history text preserves inferred pre-redraw frames in chronological order (blank-line separated), including overwritten and erased screens. Identical consecutive frames are collapsed; these are inferred terminal states, not timestamped captures. Rendered frames are bounded to 50 MB; exceeding that display budget inserts an explicit omission marker before the retained newest frames. Scrolled-off history lines are retained separately from the 4,096-row screen, so expanding a cumulative range MUST NOT silently discard previously returned newer lines. This scrollback uses the same display budget and omission marker. This does not modify raw history.data, cursor semantics, terminal_tail, or live snapshots.

terminal_since ​

Args:

ts
{ group_id: string; actor_id: string; by?: string; after: number; limit_bytes?: number }

Result:

ts
{
  history: {
    data: string
    start_cursor: number
    end_cursor: number
    has_more: boolean
    cursor_expired: boolean
  }
}

The cursors count raw PTY bytes. Because data is transported as UTF-8 JSON text, an implementation MUST NOT advance end_cursor through an incomplete UTF-8 code point. It MAY return up to three bytes beyond limit_bytes to finish a code point. If the retained stream currently ends inside a code point, it returns the complete prefix and leaves the incomplete suffix for a later call.

terminal_clear ​

Args:

ts
{ group_id: string; actor_id: string; by?: string }

Result:

ts
{ group_id: string; actor_id: string; cleared: true }

debug_tail_logs ​

Tail daemon/web/im-bridge log files (developer mode).

Args:

ts
{ component: "daemon" | "ccccd" | "web" | "im" | "im_bridge"; group_id?: string; by?: string; lines?: number }

Result:

ts
{ component: string; group_id: string; path: string; lines: string[] }

debug_clear_logs ​

Truncate daemon/web/im-bridge log files (developer mode).

Args:

ts
{ component: "daemon" | "ccccd" | "web" | "im" | "im_bridge"; group_id?: string; by?: string }

Result:

ts
{ component: string; group_id: string; path: string; cleared: true }

term_resize ​

Args:

ts
{ group_id: string; actor_id: string; cols: number; rows: number; attachment_id?: number }

cols MUST be in 10..=65535 and rows MUST be in 2..=65535; invalid or missing dimensions return invalid_size without resizing the PTY. When term_attachment_status=true, WebSocket attachment bridges MUST include the positive attachment_id returned by term_attach. The daemon atomically verifies that the attachment is still the current writer before resizing; a stale or viewer attachment returns terminal_not_writer. Legacy clients and daemons that advertise the capability as false omit the field.

Result:

ts
{ group_id: string; actor_id: string; cols: number; rows: number }

term_attachment_status ​

Optional extension, advertised by ping.capabilities.term_attachment_status. Clients MUST NOT assume this operation is available when that capability is false.

Args:

ts
{ group_id: string; actor_id: string; attachment_id: number }

Result:

ts
{ terminal_writable: boolean }

term_attach (streaming upgrade) ​

Args:

ts
{
  group_id: string
  actor_id: string
  since?: number
  mode?: "control" | "viewer"
  takeover?: boolean
  bootstrap?: "snapshot_v1"
  cols?: number
  rows?: number
}

Result (handshake):

ts
{
  group_id: string
  actor_id: string
  attachment_id?: number
  terminal_mode: "control" | "viewer"
  terminal_writable: boolean
  writer_replaced: boolean
  replay_cursor: number
  replay_end_cursor: number
  initial_output?: {
    kind: "replay" | "snapshot"
    bytes: number
    cursor: number
    cols?: number
    rows?: number
  }
}

After a successful handshake, the connection becomes a terminal stream (see §4.4).

Notes:

  • term_resize MUST be sent over a separate daemon connection (the PTY stream is not NDJSON).
  • term_attach returns not_pty_actor when the actor is not effectively running on the PTY runner.
  • Control mode MAY recreate a missing native terminal for an already-running managed Actor without replacing its provider. Viewer mode only attaches to an existing terminal; it MUST NOT launch one. Neither mode starts a stopped provider.
  • attachment_id and initial_output are optional extensions. Callers MUST consult ping.capabilities.term_attachment_status and ping.capabilities.term_attach_snapshot_v1 before depending on them. The baseline handshake always provides replay cursors and streams retained output from replay_cursor through replay_end_cursor before live PTY bytes.
  • A successful term_attach owns a dedicated connection and MUST NOT be returned to an NDJSON request pool. Rust daemon and Web implementations use this raw stream directly; terminal output is not transported through polling RPCs.
  • replay_cursor and replay_end_cursor MUST come from the same backlog snapshot that is queued for this attachment; sampling either cursor before the actual attach is not sufficient.
  • For initial_output.kind="replay", bytes in [replay_cursor, replay_end_cursor) are retained history. Clients MUST NOT send terminal-generated query replies while rendering that historical range; only live output after replay_end_cursor may generate PTY input.
  • For initial_output.kind="snapshot", replay_cursor, replay_end_cursor, and initial_output.cursor MUST be equal. Exactly initial_output.bytes bytes at the start of the upgraded daemon stream encode the ANSI snapshot and do not consume raw cursor space. All bytes after that payload are raw PTY output beginning at initial_output.cursor.
  • cols and rows are optional snapshot-size hints (10..=4096 and 2..=4096). The Rust daemon applies them only to a control attach with takeover=true; viewer and non-takeover attaches do not resize the shared PTY. Writer registration, this initial resize, and initial-output capture MUST be serialized as one runtime operation so concurrent takeovers cannot return a snapshot at another controller's dimensions.
  • The WebSocket bridge maps a negotiated snapshot to opcode 7 in one binary frame. The browser MUST resize its local xterm parser to the advertised snapshot cols/rows when present, reset xterm, parse that frame, and only then commit/ack initial_output.cursor. It MAY refit the local viewport after parsing, but a viewer MUST NOT resize the shared PTY. Opcode 1 remains raw replay/live output, and its payload length advances the raw cursor.
  • A WebSocket bridge MAY negotiate output_flow=ack_v1. The attach frame then advertises output_flow_control.protocol="ack_v1" and a bounded window_bytes. After xterm has parsed an output frame, the browser sends opcode 5 with {cursor}. The bridge MUST bound unacknowledged output and MUST continue accepting input while replay is waiting for acknowledgements. Clients and bridges that do not negotiate this extension retain the legacy stream behavior.
  • The WebSocket bridge sends opcode 6 with {terminal_writable} whenever takeover or disconnect changes the attachment's writer ownership. Clients MUST update their writable state from this frame instead of retaining the handshake value for the lifetime of the connection.

8.12 Ledger Maintenance ​

ledger_snapshot ​

Args:

ts
{ group_id: string; by?: string; reason?: string }

Result:

ts
{ snapshot: Record<string, unknown> }

Snapshot metadata counts and hashes the canonical Event array in ledger append order, including sealed plain or gzip segments. Snapshot and compaction MUST reject malformed JSON and records that cannot be decoded as Event objects before publishing snapshot metadata or rotating the active file. They MUST NOT silently omit unreadable records from a successful integrity report. Maintenance scans do not require a full-history query index. Stored records MUST contain their nonempty id and ts; maintenance MUST NOT invent those fields using construction defaults. Deterministic defaults for omitted optional fields and the snapshot hash format remain unchanged for canonical records.

ledger_compact ​

Args:

ts
{ group_id: string; by?: string; reason?: string; force?: boolean }

Result: implementation-defined compaction report.

For implementations that share a CCCC home, sealed ledger segment bytes remain part of the append-only source of truth even when a crash occurs before their manifest entry is published. Before rotating another active ledger, compaction MUST reconcile every unambiguous canonical segment present on disk and allocate a sequence greater than every physical or manifested segment sequence. It MUST NOT reuse an unpublished physical sequence. If distinct canonical segment files already claim the same sequence, compaction MUST fail before another rotation rather than silently selecting, overwriting, or hiding either file. Compaction MUST hold the same canonical ledger writer lock used by appenders from its source snapshot through active-ledger replacement and manifest publication. It MUST NOT rotate or report success while an earlier writer still owns that lock; snapshot and segment metadata MUST include every write committed before the lock is released to compaction.

8.13 Presentation State ​

The canonical group Presentation snapshot is groups/<group_id>/state/presentation.json. Ports MUST use the daemon operations below for snapshot mutations; a browser-surface session is ephemeral and is not part of this durable state.

presentation_get ​

Args:

ts
{ group_id: string }

Result:

ts
{
  group_id: string
  presentation: {
    v: 1
    updated_at: string
    highlight_slot_id: "" | "slot-1" | "slot-2" | "slot-3" | "slot-4"
    slots: Array<{
      slot_id: "slot-1" | "slot-2" | "slot-3" | "slot-4"
      index: 1 | 2 | 3 | 4
      card?: Record<string, unknown>
    }>
  }
}

presentation_publish ​

Args:

ts
{
  group_id: string
  by?: string
  slot?: "auto" | "slot-1" | "slot-2" | "slot-3" | "slot-4"
  card_type?: "markdown" | "table" | "image" | "pdf" | "file" | "web_preview"
  title?: string
  summary?: string
  source_label?: string
  source_ref?: string
  content?: string
  table?: Record<string, unknown> | Array<Record<string, unknown>>
  path?: string
  url?: string
  blob_rel_path?: string
}

by MUST identify user, system, or an actor in the group. Workspace paths MUST resolve below the active scope. A stored remote url MUST be an absolute HTTP(S) URL with a host; local or generated content uses path, content, or blob_rel_path instead. auto selects the first empty slot, then the oldest published slot.

Result:

ts
{
  group_id: string
  slot_id: string
  card: Record<string, unknown>
  presentation: Record<string, unknown>
  replaced: boolean
  event: CCCSEventV1
  event_id: string // compatibility alias of event.id
}

The event kind is presentation.publish; its data is {slot_id,title,card_type,source_label,source_ref,summary}.

presentation_clear ​

Args:

ts
{
  group_id: string
  by?: string
  slot?: "slot-1" | "slot-2" | "slot-3" | "slot-4"
  all?: boolean
}

by has the same validation as presentation_publish. all=true clears all slots regardless of slot; an omitted/empty slot also means all slots.

Result:

ts
{
  group_id: string
  slot_id: string // populated only when exactly one occupied slot was cleared
  cleared_slots: string[]
  presentation: Record<string, unknown>
  event: CCCSEventV1
  event_id: string // compatibility alias of event.id
}

The event kind is presentation.clear; its data is {slot_id,cleared_all,cleared_slots}. For both mutation operations, the snapshot update and ledger event form one acknowledged transition: if event append fails, the prior snapshot MUST be restored before failure is returned.

8.14 Presentation Browser Surface (Optional) ​

presentation_browser_attach ​

Attach to the currently active slot browser-surface session over a dedicated bidirectional NDJSON stream.

Args:

ts
{
  group_id: string
  slot: "slot-1" | "slot-2" | "slot-3" | "slot-4"
  by?: string
  viewer_mode?: "auto" | "screencast" | "vnc"
}

Handshake result:

ts
{ group_id: string; slot_id: string }

Streaming mode:

  • After a successful handshake, the connection upgrades into the browser-surface stream described in §4.6.
  • The daemon emits state items when runtime/session status changes and frame items for captured browser frames.
  • The client MAY send browser-control commands (navigate, back, refresh, click, scroll, key, text, resize, close, disconnect).
  • At most one active controller MAY be attached at a time; a second attach attempt SHOULD fail with a busy-style error.
  • If no active browser-surface session exists for the slot, attach SHOULD fail with browser_surface_not_found.
  • If the underlying browser runtime is no longer active, attach SHOULD fail with browser_surface_not_active.

presentation_browser_vnc_attach ​

Attach to the currently active slot browser-surface session over a raw RFB/VNC stream.

Args:

ts
{
  group_id: string
  slot: "slot-1" | "slot-2" | "slot-3" | "slot-4"
  by?: string
}

Handshake result:

ts
{ group_id: string; slot_id: string }

Streaming mode:

  • After a successful handshake, the connection upgrades into a raw VNC/RFB byte stream.
  • The operation SHOULD fail with browser_vnc_unavailable when the browser surface is not backed by a local VNC projection.

8.15 Event Streaming (Optional) ​

events_stream ​

Subscribe to new ledger events for a group.

Args:

ts
{
  group_id: string
  by?: string
  since_event_id?: string | null  // resume strictly after this event (preferred)
  since_ts?: string | null        // best-effort resume using timestamps
  kinds?: string[] | null         // optional kind allowlist (exact match)
}

Handshake result:

ts
{ group_id: string }

Streaming mode:

  • The daemon pushes NDJSON EventStreamItem lines (see §4.5).
  • A daemon may initially emit only a subset of event kinds. CCCC streams these kinds:
    • chat.message, mail.read, chat.reply_request.cancelled, runtime.delivery, system.notify
  • When kinds is provided, only matching event kinds SHOULD be emitted.
  • If by identifies an actor_id, a daemon MAY apply the same recipient-routing visibility rules used by messaging (e.g., only emit chat.message/system.notify addressed to that actor and exclude the actor’s own chat.message events). This stream filter is independent of the Mail-only Inbox projection.
  • Resume (since_event_id / since_ts) is best-effort in v1; clients MUST be able to reconcile using inbox_peek.
  • Ledger compaction preserves event IDs. An established follower MUST retain unseen events across rotation and refill, and MUST NOT advance its cursor when a busy writer defers polling. A missing previously observed cursor is an explicit recovery error, not permission to skip events.
  • The stream ends when the client closes the connection or the daemon exits.
  • To protect daemon responsiveness, a daemon MAY drop slow subscribers (clients SHOULD reconnect and reconcile).

8.16 IM Authentication ​

The durable group-local IM authority is group.yaml:im for provider configuration and the sibling state files im_pending_keys.json, im_authorized_chats.json, and im_subscribers.json for delivery targets. Implementations MUST serialize reads that can cause a write and every read-modify-write across those classes with groups/<group_id>/state/im_state.lock; a long-running worker MUST refresh authorization and subscription truth after acquiring that lock rather than continue from a process-private startup snapshot. Binding and revocation MUST update their coupled authorization/subscription records under one such transaction.

The former Rust group.yaml:im_bridge durable fields (config, enabled, authorized, pending, and subscribers) are a one-way migration source. Canonical classes win when present, and imported fields MUST be retired after canonical commit. An explicit IM unset MUST clear the canonical target files and consume those legacy durable fields so a later native load cannot restore configuration or delivery authority. Non-durable runtime diagnostics in im_bridge MAY remain.

Across IM authentication and subscriber state, thread_id is a platform-owned opaque identifier. Implementations MUST preserve it as either a legacy JSON number or a non-empty JSON string and MUST NOT coerce string identifiers (for example, Slack timestamps such as 1710000000.100) through an integer or floating-point representation. Omitted, null, empty, integer 0, and string "0" mean no thread; unsupported JSON value types MAY normalize to numeric 0.

im_bind_chat ​

Bind a pending one-time key to authorize an IM chat. On success the chat is also auto-subscribed for outbound message delivery.

Args:

ts
{ group_id: string; key: string }

Result:

ts
{ chat_id: string; thread_id: number | string; platform: string }

Errors:

  • missing_key – key is empty.
  • missing_group_id – group_id is empty.
  • group_not_found – group does not exist.
  • invalid_key – key not found or expired.

im_list_authorized ​

List all authorized IM chats for a group.

Args:

ts
{ group_id: string }

Result:

ts
{ authorized: Array<Record<string, unknown>> }

Errors:

  • missing_group_id – group_id is empty.
  • group_not_found – group does not exist.

im_list_pending ​

List pending one-time bind requests for a group (expired keys are omitted).

Args:

ts
{ group_id: string }

Result:

ts
{
  pending: Array<{
    key: string
    chat_id: string
    thread_id: number | string
    platform: string
    created_at: number
    expires_at: number
    expires_in_seconds: number
  }>
}

Errors:

  • missing_group_id – group_id is empty.
  • group_not_found – group does not exist.

im_reject_pending ​

Reject a pending one-time bind key.

Args:

ts
{ group_id: string; key: string }

Result:

ts
{ rejected: boolean } // idempotent: false when key is already absent/expired

Errors:

  • missing_key – key is empty.
  • missing_group_id – group_id is empty.
  • group_not_found – group does not exist.

im_revoke_chat ​

Revoke authorization for an IM chat.

Args:

ts
{ group_id: string; chat_id: string; thread_id?: number | string }

Result:

ts
{ revoked: boolean; unsubscribed?: boolean }

Notes:

  • thread_id defaults to 0 when omitted or represented by an unsupported JSON value type.

Errors:

  • missing_chat_id – chat_id is empty.
  • missing_group_id – group_id is empty.
  • group_not_found – group does not exist.

8.17 Remote Access (Contract-Gated) ​

These operations are optional extensions for productized remote-access control. Deployments without this feature MAY return unknown_op.

remote_access_state ​

Read global remote-access state.

Args:

ts
{ by?: string }

Result:

ts
{
  remote_access: {
    provider: "off" | "manual" | "tailscale" | "reach"
    mode: string
    require_access_token: boolean
    enabled: boolean
    status: "stopped" | "running" | "not_installed" | "not_authenticated" | "misconfigured" | "error"
    endpoint?: string | null
    updated_at?: string | null
    diagnostics?: {
      access_token_present?: boolean
      access_token_source?: "store" | "none" | string
      access_token_count?: number
      admin_access_token_present?: boolean
      admin_access_token_count?: number
      remote_listener_auth_required?: boolean
      remote_listener_auth_requirement_satisfied?: boolean
      allow_unauthenticated_listener_override?: boolean
      web_host?: string
      web_host_source?: "settings" | "env" | "default" | string
      web_port?: number
      web_port_source?: "settings" | "env" | "default" | string
      web_public_url?: string | null
      web_public_url_source?: "settings" | "env" | "none" | string
      web_bind_loopback?: boolean
      web_bind_reachable?: boolean
      mode_supported?: boolean
      tailscale_installed?: boolean | null
      tailscale_backend_state?: string | null
      [k: string]: unknown
    }
    config?: {
      web_host?: string
      web_port?: number
      web_public_url?: string | null
      access_token_configured?: boolean
      access_token_count?: number
      admin_access_token_configured?: boolean
      admin_access_token_count?: number
      access_token_source?: "store" | "none" | string
      [k: string]: unknown
    }
    next_steps?: string[]
  }
}

remote_access_configure ​

Update global remote-access configuration.

Args:

ts
{
  by?: string
  provider?: "off" | "manual" | "tailscale"
  mode?: string
  require_access_token?: boolean
  web_host?: string
  web_port?: number
  web_public_url?: string
}

Result:

ts
{ remote_access: Record<string, unknown> }

A non-local Web binding or a configured public URL requires at least one administrator Access Token before it can be started or applied. Group-scoped tokens do not satisfy this recovery/control-plane requirement. Localhost-only configuration remains available without a token. Implementations MAY expose CCCC_WEB_ALLOW_UNAUTHENTICATED=1 as an explicit unsafe listener override for deployments that already enforce a trusted network boundary.

remote_access_start ​

Start remote access according to configured provider/mode.

Tailscale start/stop commands have a 30-second deadline and bounded captured output. Missing executables report remote_access_not_installed; command failure or timeout reports remote_access_start_failed / remote_access_stop_failed. An error does not mark the desired enabled state as successfully changed. A timeout does not roll back external network changes; inspect Tailscale before retrying. Other providers retain their own lifecycle semantics.

Args:

ts
{ by?: string }

Result:

ts
{ remote_access: Record<string, unknown> }

Errors:

  • remote_access_admin_token_required – remote exposure has no administrator Access Token and the explicit unsafe listener override is not enabled.

remote_access_stop ​

Stop remote access service.

Args:

ts
{ by?: string }

Result:

ts
{ remote_access: Record<string, unknown> }

provider=reach is not set through remote_access_configure. It is owned by the membership reach verbs. Settings may persist reach after a successful membership_reach_on. While Reach is enabled or its tracked helper is still running, remote_access_configure MUST reject every configuration mutation; callers must complete membership_reach_off before changing provider, binding, or public URL.

8.17.1 Membership reach ​

The device-authenticated Connect directory, read-only connect_status and connect_catalog, user-only connect_group_status / connect_group_select operations, durable connect_send / connect_send_files acceptance, ordinary reply / reply_request_cancel and upload-preflight integration, connect_delivery / connect_cancellation status projections, failure notification recovery, and signed internal connect_peer_receive catalog/message/receipt/cancel port are specified in CCCC_CONNECT_V1.md. Connect is an in-progress extension; it does not give device credentials Web administrator authority. connect_group_status distinguishes not_linked, syncing, ready and unavailable, with sharing-check time and errors. Only an unexpired confirmed empty grant means no connections; GET does not perform synchronization.

Optional extension for third-party deployments. The bundled native implementation implements the complete operation set below. Deployments without membership MAY return unknown_op.

Stable error classes:

  • membership_not_logged_in – no local device binding exists for an operation that requires one
  • membership_gate – missing Admin Token, unauthenticated-listener override, or another remote provider is already on
  • membership_disabled
  • membership_network
  • membership_subprocess
  • membership_unsupported_version
  • membership_unavailable – account plane origin is not configured
  • membership_not_in_reach

membership_status ​

ts
{ by?: string }
ts
{
  membership: {
    logged_in: boolean
    device_id?: string | null
    account_label?: string | null
    hostname?: string | null
    web_url?: string | null
    online: boolean
    cut: boolean
    disabled: boolean
    in_reach: boolean
    reach_enabled: boolean
    reach_status: "off" | "connecting" | "online" | "offline" | "unknown"
    checked_at?: string
    reach_supported: boolean
    account_reachable?: boolean | null
    account_origin?: string | null
    last_error?: string | null
    warning?: string
    pending?: {
      user_code: string
      verification_uri: string
      verification_uri_complete?: string | null
      interval: number
      expires_at: string
    } | null
  }
}

membership_status is user-only. Implementations MUST reject non-user callers before assembling it. hostname is the reserved, tokenless device origin; its presence does not prove that DNS or a tunnel has been provisioned. web_url is the tokenless Web sign-in address, assembled locally and null while logged out. It MUST NOT contain a bearer credential. The Web port can separately issue a short-lived, one-time Web login grant for the current authorized administrator. Website account sign-in does not authenticate a browser to the local CCCC Web. Private Web Model connector URLs remain part of the administrator-only instance connector API and MUST NOT be selected or exposed through global membership status.

account_label is optional display-only identity (currently the verified account email). The authenticated device status and Connect directory refresh synchronize it to the issuer-bound local membership state. New device grants clear the prior label; cut/unlinked devices never expose it. Connect refresh MUST clear it on confirmed device or credential rejection, independently of membership_status. Transient transport failures retain it, and updates remain bound to the device and issuer that initiated the request. Older issuers may omit it. It does not identify the browser's Web Access Token principal or grant any Web rights.

reach_enabled is the saved Reach intent; in_reach only identifies the selected provider. Neither proves connectivity. reach_status is an ephemeral projection: off when stopped or unlinked/cut, connecting immediately after an accepted start, offline when enabled but the helper is absent or the account reports a disconnected/unprovisioned tunnel, and unknown when the running helper has no current connection confirmation. online=true and reach_status=online require enabled Reach, a live tracked helper, and current account-side connection evidence. A successful process start or an unavailable status service MUST NOT produce online=true. Mutating operations do not claim unperformed connection checks. checked_at is the UTC time of a completed status refresh, including a failed account check; it is not a cached last-success timestamp.

The account device API adds connection: not_started | online | offline | unknown alongside its existing fields. Native clients preserve unknown, including unrecognized future states; when the field is absent on an older issuer, they use the existing online boolean. This is an additive v1 change.

Tunnel connection evidence is not proof of origin application availability or browser authorization. Clients label it accordingly and offer the Web sign-in flow for an actual access check. After an explicit start, clients MAY perform bounded status confirmation while the panel is visible; they MUST stop on a deadline and MUST NOT reissue Reach start or change configuration as a retry. Disconnected Reach still owns its provider/binding settings until explicitly stopped; configuration controls must not infer ownership from online.

account_reachable is ephemeral evidence from the current status refresh; it is omitted when no linked-device probe applies, true after a valid account-plane response, and false after a transient account-plane failure. It MUST NOT be persisted as a second freshness state machine.

reach_supported reports whether this CCCC build provides a pinned managed Reach helper for the current platform. It is independent of account linkage and helper installation: an unsupported platform can still link and manage its CCCC Account, but MUST NOT present Reach as startable.

Status refresh may observe an account-side Cut or learn that the bearer for an already-linked device is definitively absent. Both are terminal revocations: the daemon MUST stop the tracked helper, persist the disabled state, and clear Reach-owned enabled / web_public_url state before returning. A timeout, DNS failure, 5xx response, or malformed response is transient and MUST preserve the binding and helper state. Daemons therefore MUST serialize membership_status with membership mutations rather than treating it as a side-effect-free read. Membership and remote-access operations share exclusive ownership, but their network waits MUST NOT hold the global Group read/write permit. Global mutations and Reach restore commits also acquire this ownership before their global permit; restore still fetches outside both permits and verifies its captured intent before committing. Ordinary Group reads and writes remain available while the account service is slow.

membership_login / membership_login_poll / membership_logout ​

ts
{ by?: string }

membership_login starts RFC 8628 device-code login against CCCC_ACCOUNT_ORIGIN and returns membership.pending (verification_uri, optional verification_uri_complete, user_code, interval). While an unexpired, still-pollable grant exists, another membership_login MUST replay it rather than issue a second device code or retarget it to a different origin. Both verification URLs MUST be absolute HTTP(S) URLs on the configured account origin; clients reject an off-origin authorization URL before storing or opening it. The selected account origin is persisted with the pending login and resulting device grant. Polling and every later authenticated device or Reach request MUST use that issuer-bound origin; a changed daemon environment or per-request override MUST NOT retarget an existing bearer token. The CLI or Web client opens verification_uri_complete when present and otherwise presents verification_uri plus user_code. The advertised interval is a minimum and MUST NOT be capped downward. On slow_down, subsequent polling waits MUST increase by at least five seconds. authorization_pending, slow_down, transport failures, and 5xx responses preserve the pending grant. access_denied and expired_token are terminal for that grant: the daemon MUST clear the matching pending state before returning the error so the next membership_login can issue a fresh code. Once a grant has been committed, an exact or late membership_login_poll MUST replay the logged-in status instead of failing because the pending code was consumed.

After a successful user-requested login grant (or its replay), the local daemon initializes one administrator Access Token if none exists, under the existing Token store lock. Existing administrator and scoped Tokens are preserved. CLI login and Web login use the same initialization. Status reads and background reconnection/directory refresh never create Tokens. The Web port binds a login cookie only for an already verified direct-loopback local administrator; account membership alone does not grant an anonymous remote browser administrator access. Explicit local Web reach on can also initialize a previously linked installation before the daemon's normal remote-access gate. Tokens never enter account requests or public URLs. Initialization failure is returned and can be retried without reissuing the already committed account grant.

membership_logout first stops any tracked Reach helper and persists disabled Reach intent, then retires the issuer-bound account device through the account plane, and only then clears local membership secrets and retired Reach URLs. Network or account errors preserve the local credential so the user can retry, but MUST NOT re-enable local Reach through background recovery. An unrelated remote-access provider is preserved. An already absent or disabled remote device is treated as retired. The result includes a warning that the next login is a new device and hostname.

Requests send CCCC-Membership-Version: 1. An account plane that no longer supports the client returns membership_unsupported_version. CCCC_ACCOUNT_ORIGIN MUST be an HTTP(S) origin without user information, a non-root path, query, or fragment. It MUST use HTTPS, except that loopback HTTP is allowed for local development. Clients MUST NOT follow account-plane redirects because authenticated requests carry a device bearer token.

For authenticated device endpoints, absence of an Authorization bearer remains 401 unauthorized. When a bearer is present but no active device exists for it (including after device or account deletion), the account plane MUST return 403 device_disabled. Clients talking to an older or third-party issuer MAY also receive 401 or 404; if the request used a locally stored device bearer, those responses are the same terminal revocation, not evidence of a transient network failure. Relinking first retires the invalid local binding and then starts a fresh device-code authorization.

membership_reach_install ​

ts
{ upgrade?: boolean, by?: string }

Installs the pinned cloudflared binary under CCCC_HOME after verifying its platform, version, and SHA-256 digest. With upgrade=true, an existing unpinned or mismatched managed binary is replaced. Installation does not enable remote access or start a tunnel.

membership_reach_on / membership_reach_off ​

ts
{ by?: string }

reach on requires an administrator Access Token, a logged-in device that is not disabled, and an account origin. It MUST refuse if CCCC_WEB_ALLOW_UNAUTHENTICATED is set or if tailscale is already enabled. An enabled manual public URL remains active while Reach is prepared and is replaced only after the Reach helper starts successfully; any pre-commit failure MUST preserve the manual provider and URL. It installs the pinned cloudflared if missing, and refuses a version/hash mismatch unless membership_reach_install (cccc reach install) was used. The account-plane request includes the port of the currently live, identity-verified Web listener as origin_port (1–65535), not merely the desired setting or environment default. The runtime descriptor MUST contain an unguessable Web-instance identifier and an owner-only proof key. Reach MUST send a fresh random challenge, and the loopback /api/v1/ready response MUST return the recorded identifier plus an HMAC-SHA256 proof bound to that challenge before Reach may start. The verifier MUST NOT send the expected identifier or proof key to the listener; a live PID, accepting TCP port, or reflected request value alone is not proof that the listener belongs to CCCC. The account plane MUST route the named tunnel to 127.0.0.1:<origin_port> and MUST NOT accept an arbitrary origin host. A returned Reach hostname MUST normalize to one HTTPS origin without user information, a non-root path, query, or fragment before it can be stored or used to assemble public URLs and origin-bound Web login links. On success it sets remote_access.provider=reach and writes web_public_url.

The tunnel token MUST NOT appear in process arguments; supported helpers use a permission-restricted token file. Before signaling a persisted helper PID, an implementation MUST verify the live executable against the exact managed executable recorded when the helper started (or use an in-process child handle it still owns); process names and argument substrings are insufficient. A mismatch preserves tracking and returns an error instead of killing an unrelated process. reach off keeps provider=reach, but reports success only after the tracked helper has exited and its tracking files are retired. A persisted enabled flag alone is not proof that Reach is online: status requires enabled Reach, a live tracked helper, and confirmation of a connected named tunnel at the account plane. If any authenticated device-status or Reach-issuance response reports the device disabled or definitively missing, the helper is stopped, Reach-owned public state is cleared, and status is cut before the operation returns.

The daemon MUST reconcile saved provider=reach, enabled=true intent after restart without a CLI action, open browser, or status poll. Only a linked, non-disabled device with an administrator Access Token may restore. A running, identity-checked tracked helper is left running; cloudflared owns its transport reconnection. When the helper has exited, restoration waits for the signed live Web binding, verifies the already-installed pinned helper, and requests fresh credentials from the bound account issuer using that Web port. Automatic restoration MUST NOT download or upgrade helpers. Missing/mismatched helpers remain offline with an actionable error and require explicit installation.

Restoration is single-flight. Account requests have a fifteen-second deadline and run outside dispatcher permits; failed attempts back off from five seconds to at most sixty seconds. Local reconciliation checks run every five seconds when idle, without cloud calls for a running helper or disabled Reach. Background work MUST NOT queue a global writer behind Actor startup. Before applying a result it MUST serialize with membership/settings mutations and recheck the saved intent, issuer/device binding, administrator prerequisite, live Web port, and shutdown state. A result superseded by off/logout/relink/configuration changes is discarded; it MUST NOT resurrect access or overwrite the newer state. Definitive device rejection clears the matching Reach intent as above; transient failures preserve it for retry. Status polling observes this work and MUST NOT trigger restoration. Helper startup alone is not online evidence.

Membership state lives in CCCC_HOME/secrets/membership.json. Every read-modify-write mutation MUST hold CCCC_HOME/secrets/membership.json.lock and preserve the full v1 shape, including issuer-bound account_origin, device_token, tunnel_token, and pending_login.

8.17.2 CCCC Connect and manual Bridge retirement ​

CCCC_CONNECT_V1.md defines current same-account discovery, qualified messaging, delivery receipts, browser authorization, and revocation. Manual pairing/session operations, remote_send, remote_delivery_status, send_cross_group_remote_record, and remote arbitrary-tool endpoints are removed. They MUST NOT be used as fallback routes for Connect or local cross-group sends.

Before serving requests, the daemon retires old pairing, registration, credential, and delivery state under the Home lock. It preserves the stable instance private key and original Group ledgers. Pending operations are never replayed or converted into account authority. Confirmed outcomes remain confirmed; unattempted queued work ends failed; possibly transmitted work ends unconfirmed. Final chat.cross_group_receipt events preserve source scope and operation IDs, carry group_bridge_retired=true, and precede removal of source state. Cleanup is idempotent across interruption and ledger rotation. Deleted Groups are not recreated. Unreadable or unprojectable records remain for inspection and retry at next startup, with a warning; they neither revive old workers nor prevent ordinary local startup. A deduplicated internal system.notify informs the user in each affected Group. Secrets and original payloads MUST NOT appear in it.

ledger_statuses exposes retired_bridge=true for affected historical messages; queries with with_obligation_status decorate those messages with _retired_bridge=true. Consumers preserve that marker across raw replay and hide the reply action. reply and its upload preflight independently reject retired provenance or archived retirement receipts with group_bridge_retired, before writes, even when a local Group happens to share the old destination ID. Historical sender names and reference text remain readable. Ordinary local cross-group send and cancellation are preserved.

8.18 Group Space (Provider-Backed Shared Memory, dual-lane NotebookLM) ​

These operations provide a thin control-plane for optional external memory providers. Provider failures MUST NOT block core collaboration flows (chat/context/actors).

NotebookLM is modeled as two fixed daemon-owned lanes:

  • lane="work": project/shared external knowledge, repo space/ sync, artifacts, general ingest/query.
  • lane="memory": finalized daily memory recall only; daemon syncs state/memory/daily/*.md asynchronously.

Normative lane rules:

  • Agent-facing surfaces SHOULD pass lane explicitly for mutating or lane-targeted actions.
  • group_space_status MAY omit lane; it returns both lanes.
  • group_space_bind|query|sources|jobs|sync are lane-targeted.
  • group_space_ingest|artifact are supported only on lane="work".
  • MEMORY.md MUST remain local-only and MUST NOT be uploaded to NotebookLM.

group_space_status ​

Read provider mode, both lane bindings, queue summaries, work-lane repo space/ sync state, and memory-lane daily sync summary.

Args:

ts
{ group_id: string; provider?: "notebooklm" }

Result:

ts
{
  group_id: string
  provider: {
    provider: "notebooklm"
    enabled: boolean
    mode: "disabled" | "active" | "degraded"
    real_adapter_enabled?: boolean
    stub_adapter_enabled?: boolean
    auth_configured?: boolean
    write_ready?: boolean
    readiness_reason?: string
    last_health_at?: string | null
    last_error?: string | null
  }
  bindings: {
    work: {
      group_id: string
      provider: "notebooklm"
      lane: "work"
      remote_space_id: string
      bound_by: string
      bound_at: string
      status: "bound" | "unbound" | "error"
    }
    memory: {
      group_id: string
      provider: "notebooklm"
      lane: "memory"
      remote_space_id: string
      bound_by: string
      bound_at: string
      status: "bound" | "unbound" | "error"
    }
  }
  queue_summary: {
    work: { pending: number; running: number; failed: number }
    memory: { pending: number; running: number; failed: number }
  }
  sync?: {
    available?: boolean
    reason?: string
    space_root?: string
    remote_space_id?: string
    last_run_at?: string
    converged?: boolean
    unsynced_count?: number
    last_error?: string
  }
  memory_sync?: {
    lane: "memory"
    manifest_path: string
    last_scan_at?: string | null
    last_success_at?: string | null
    pending_files: number
    running_files: number
    failed_files: number
    blocked_files: number
    eligible_daily_files: number
    synced_daily_files: number
    empty_daily_skipped: number
    last_eligible_daily_date?: string | null
    last_synced_daily_date?: string | null
  }
}

group_space_spaces ​

List available remote notebooks/spaces for provider selection UI.

Args:

ts
{ group_id: string; provider?: "notebooklm" }

Result:

ts
{
  group_id: string
  provider: "notebooklm"
  provider_state: Record<string, unknown>
  bindings: Record<"work" | "memory", Record<string, unknown>>
  spaces: Array<{
    remote_space_id: string
    title?: string
    created_at?: string
    is_owner?: boolean
  }>
}

group_space_capabilities ​

Return Group Space capability matrix for current group/provider.

Args:

ts
{
  group_id: string
  provider?: "notebooklm"
}

Result:

ts
{
  group_id: string
  provider: "notebooklm"
  local_scope_attached: boolean
  space_root: string
  local_file_policy: {
    allowed_extensions: string[]
    max_file_size_bytes: number
    unsupported_error_code: string
    oversize_error_code: string
  }
  ingest: {
    kinds: Array<"context_sync" | "resource_ingest" | "memory_daily_sync">
    resource_ingest: {
      source_types: string[]
      required_fields: Record<string, string[]>
      optional_fields: Record<string, string[]>
      aliases: Record<string, string>
      examples: Record<string, Record<string, unknown>>
    }
  }
  query: {
    options: {
      source_ids: string
    }
    unsupported_options: Record<string, string>
    examples: Record<string, Record<string, unknown>>
  }
  artifacts: {
    actions: string[]
    kinds: string[]
    options: Record<string, string>
    aliases: Record<string, string>
    examples: Record<string, Record<string, unknown>>
  }
  notes: string[]
  capabilities: string[]
  unavailable_capabilities: string[]
}

Capability matrices are implementation-specific runtime truth. Callers MUST NOT assume an ingest source type or asynchronous behavior that is absent from the returned matrix. An implementation MUST fail an unavailable operation with capability_unavailable; it MUST NOT silently coerce a file, URL, YouTube, or Drive source into pasted text.

group_space_bind ​

Bind/unbind a group lane to a provider remote notebook. When action=bind and remote_space_id is empty, daemon may auto-create an appropriate notebook and bind it.

Args:

ts
{
  group_id: string
  provider?: "notebooklm"
  lane: "work" | "memory"
  action?: "bind" | "unbind"
  remote_space_id?: string
  by?: string
}

Result:

ts
{
  group_id: string
  lane: "work" | "memory"
  provider: Record<string, unknown>
  bindings: Record<"work" | "memory", Record<string, unknown>>
  queue_summary: {
    work: { pending: number; running: number; failed: number }
    memory: { pending: number; running: number; failed: number }
  }
  sync?: Record<string, unknown>         // work-lane repo sync view
  memory_sync?: Record<string, unknown>  // memory-lane manifest summary
  sync_result?: Record<string, unknown>
}

group_space_ingest ​

Create (or dedupe) a durable work-lane ingest job and execute one provider attempt. lane="memory" MUST be rejected. The job MUST be persisted before the provider mutation, then settled to succeeded or failed. A process exit or a provider response whose remote commit cannot be determined MUST leave the job running to represent an uncertain outcome. Reissuing the same idempotency key MUST return that job instead of creating another source. 0.4.36 has no background ingest retry worker: retry after a terminal failure is an explicit group_space_jobs action=retry operation.

Args:

ts
{
  group_id: string
  provider?: "notebooklm"
  lane: "work" | "memory"
  kind?: "context_sync" | "resource_ingest"
  payload?: Record<string, unknown>
  idempotency_key?: string
  by?: string
}

Result:

ts
{
  group_id: string
  lane: "work"
  job_id: string
  accepted: boolean
  completed: boolean
  deduped: boolean
  job: Record<string, unknown>
  ingest_result?: Record<string, unknown>
  source_id?: string
  source_ids?: string[]
  queue_summary: { pending: number; running: number; failed: number }
  provider_mode: "disabled" | "active" | "degraded"
}

accepted=true, completed=false means durable work remains in progress. Terminal succeeded, failed, or canceled jobs report accepted=false, completed=true.

group_space_query ​

Query provider-backed knowledge for one lane. If provider is degraded, result MAY return ok=true with degraded=true and an empty answer.

Args:

ts
{
  group_id: string
  provider?: "notebooklm"
  lane: "work" | "memory"
  query: string
  options?: {
    source_ids?: string[] // optional remote source_id filter
  }
}

Validation notes:

  • options only supports source_ids.
  • options.language / options.lang are invalid for group_space_query because NotebookLM query API does not provide a language parameter.
  • Recommended recall order is local memory first, then lane="memory" for deep recall.

Result:

ts
{
  group_id: string
  provider: "notebooklm"
  lane: "work" | "memory"
  provider_mode: "disabled" | "active" | "degraded"
  degraded: boolean
  answer: string
  references: unknown[]
  reference_count: number
  binding_status: "bound" | "unbound" | "error"
  source_basis_hint: "requested_sources_hit" | "requested_sources_mixed" | "requested_sources_only" | "referenced_sources_present" | "context_sync_only" | "materialized_sources_present" | "mixed" | "memory_manifest_only" | "unknown"
  requested_source_ids?: string[]
  referenced_source_ids?: string[]
  references_match_requested?: boolean
  latest_context_sync_at?: string
  remote_sources?: number
  materialized_sources?: number
  memory_last_success_at?: string
  memory_pending_files?: number
  memory_failed_files?: number
  error?: { code: string; message: string } | null
}

Notes:

  • The extra query fields above are lightweight diagnostics/provenance hints, not retrieval guarantees.
  • When options.source_ids is provided, source_basis_hint SHOULD prefer explicit source scope / actual cited sources over inferred local sync state.
  • Work-lane answers may come from synced coordination/context even when repo materialized sources are sparse.
  • Memory-lane diagnostics describe sync-manifest health only; they do not promise a semantic hit for every query.

group_space_sources ​

List/refresh/rename/delete provider sources in the currently bound lane notebook.

Args:

ts
{
  group_id: string
  provider?: "notebooklm"
  lane: "work" | "memory"
  action?: "list" | "refresh" | "rename" | "delete"
  source_id?: string // required for refresh/rename/delete
  new_title?: string // required for rename
  by?: string
}

Result (action=list):

ts
{
  group_id: string
  provider: "notebooklm"
  lane: "work" | "memory"
  provider_mode: "disabled" | "active" | "degraded"
  binding: Record<string, unknown>
  action: "list"
  sources: Record<string, unknown>[]
  list_result: Record<string, unknown>
}

Result (action=refresh | rename | delete):

ts
{
  group_id: string
  provider: "notebooklm"
  lane: "work" | "memory"
  provider_mode: "disabled" | "active" | "degraded"
  binding: Record<string, unknown>
  action: "refresh" | "rename" | "delete"
  source_id: string
  refresh_result?: Record<string, unknown>
  rename_result?: Record<string, unknown>
  delete_result?: Record<string, unknown>
}

action=refresh MUST invoke the provider refresh mutation. Re-listing the source without refreshing it is not a successful refresh result. A successful NotebookLM refresh reports refresh_result.refreshed=true.

group_space_artifact ​

List/generate/download provider artifacts (NotebookLM studio outputs) on lane="work". lane="memory" MUST be rejected. For action=generate, daemon can optionally wait for completion and auto-save the artifact into local repo/space/artifacts/....

Args:

ts
{
  group_id: string
  provider?: "notebooklm"
  lane: "work" | "memory"
  action?: "list" | "generate" | "download"
  kind?: "audio" | "video" | "report" | "study_guide" | "quiz" | "flashcards" | "infographic" | "slide_deck" | "data_table" | "mind_map"
  options?: Record<string, unknown> // for action=generate
  wait?: boolean // action=generate only; default false
  save_to_space?: boolean // generate/download local-save behavior; default false
  output_path?: string // optional local path override
  output_format?: "json" | "markdown" | "html" | "pdf" | "pptx" | "csv"
  artifact_id?: string // optional explicit download target
  timeout_seconds?: number // generate+wait only
  initial_interval?: number // generate+wait only
  max_interval?: number // generate+wait only
  by?: string
}

Result (action=list|generate|download) mirrors the lane-targeted binding and includes lane: "work".

Generation defaults to wait=false and save_to_space=false, so a normal request does not hold a group mutation lane while polling a remote provider and does not perform an implicit local write. When wait=false, an implementation without a background artifact worker MAY return after remote generation starts with saved_to_space=false; the caller can later list or download the artifact. It MUST NOT claim that a local file was saved. wait=true plus save_to_space=true performs the wait and local save before returning or reports a provider timeout/failure.

When save_to_space=true, the implementation MUST validate the local destination and kind/format download capability before creating the remote artifact. Unsupported kind/format combinations fail with capability_unavailable without provider-side generation. Authenticated media downloads MUST require HTTPS and validate an explicit provider host allowlist for both the initial URL and every redirect hop.

Common provider error semantics:

  • space_provider_not_configured: required provider credentials or binding configuration is absent; non-transient.
  • space_provider_auth_invalid: credentials are malformed, expired, or rejected; non-transient until re-authentication.
  • space_provider_not_found: requested remote resource is absent; non-transient and does not degrade the whole provider.
  • space_provider_compat_mismatch: provider response schema cannot be decoded; non-transient and degrades the provider until compatibility is restored.
  • space_provider_timeout: request or provider-side wait timed out; transient and does not by itself degrade the provider.
  • space_provider_rate_limited: provider refused work because of quota/rate limits; transient and does not by itself degrade the provider.
  • space_provider_outcome_unresolved: the provider may have committed the mutation, but CCCC cannot prove its result; the durable job remains running and MUST NOT be retried until a user inspects/reconciles or cancels it.
  • space_provider_upstream_error: another provider transport/RPC failure occurred; retryability depends on the operation and provider response.

group_space_jobs ​

List/retry/cancel Group Space jobs for one lane.

retry accepts only terminal failed/canceled jobs (and legacy pending jobs). It MUST reject an uncertain running job. Before a retry performs any provider mutation, the job's saved remote_space_id MUST equal the lane's current binding; otherwise it fails with binding_changed and leaves the job unchanged. This prevents an old job from writing into a notebook the Group has since left.

Args:

ts
{
  group_id: string
  provider?: "notebooklm"
  lane: "work" | "memory"
  action?: "list" | "retry" | "cancel"
  job_id?: string
  state?: "pending" | "running" | "succeeded" | "failed" | "canceled"
  limit?: number
  by?: string
}

group_space_sync ​

Read legacy Python 0.4.35 synchronization state for one lane during a native upgrade. This compatibility operation is not advertised by the 0.4.36 CLI, Web API, or MCP surface. Automatic repo/memory mirroring is retired; callers use explicit group_space_ingest and source operations instead.

Args:

ts
{
  group_id: string
  provider?: "notebooklm"
  lane: "work" | "memory"
  action?: "status"
  by?: string
}

Result returns the targeted lane state in sync. Implementations that list sync.work or sync.memory in unavailable_capabilities MUST still expose canonical read-only status after the upgrade. A legacy client that sends action=run MUST receive capability_unavailable before any provider-side mutation.

group_space_provider_credential_status ​

Read provider credential status (masked metadata only, no secret values).

Args:

ts
{
  provider?: "notebooklm"
  by?: string // user-only
}

Result:

ts
{
  provider: "notebooklm"
  credential: {
    provider: "notebooklm"
    key: string
    configured: boolean
    source: "none" | "store" | "env"
    env_configured: boolean
    store_configured: boolean
    updated_at?: string | null
    masked_value?: string | null
  }
}

group_space_provider_credential_update ​

Update or clear provider credentials in the daemon secret store.

Args:

ts
{
  provider?: "notebooklm"
  by?: string // user-only
  auth_json?: string
  clear?: boolean
}

Notes:

  • clear=true removes stored credentials for this provider.
  • auth_json is write-only and never returned in response payloads.
  • Environment credential (CCCC_NOTEBOOKLM_AUTH_JSON) has higher precedence than stored credentials.
  • Updating or clearing the effective stored credential invalidates its prior verified-ready state; callers must run a successful health check before the provider is reported write_ready=true.

Result:

ts
{
  provider: "notebooklm"
  credential: {
    provider: "notebooklm"
    key: string
    configured: boolean
    source: "none" | "store" | "env"
    env_configured: boolean
    store_configured: boolean
    updated_at?: string | null
    masked_value?: string | null
  }
}

group_space_provider_health_check ​

Run provider health check and update provider state (active/degraded/disabled) accordingly.

Args:

ts
{
  provider?: "notebooklm"
  by?: string // user-only
  auth_json?: string // optional candidate storage-state JSON
}

When auth_json is present it is treated as write-only credential material: it is never returned, persisted, or used to update the current provider state. This candidate-validation form lets browser-auth controllers verify a captured session before committing it.

Result:

ts
{
  provider: "notebooklm"
  healthy: boolean
  health?: Record<string, unknown>
  error?: { code: string; message: string }
  provider_state: Record<string, unknown>
  credential: {
    provider: "notebooklm"
    key: string
    configured: boolean
    source: "none" | "store" | "env"
    env_configured: boolean
    store_configured: boolean
    updated_at?: string | null
    masked_value?: string | null
  }
}

group_space_provider_auth ​

Control provider auth flow (status/start/cancel/disconnect) for backend-managed NotebookLM sign-in.

Args:

ts
{
  provider?: "notebooklm"
  action?: "status" | "start" | "cancel" | "disconnect"
  timeout_seconds?: number
  projected?: boolean // when true, expose sign-in through the projected browser surface instead of a daemon-host browser window
  by?: string // user-only
}

Result:

ts
{
  provider: "notebooklm"
  provider_state: Record<string, unknown>
  credential: {
    provider: "notebooklm"
    key: string
    configured: boolean
    source: "none" | "store" | "env"
    env_configured: boolean
    store_configured: boolean
    updated_at?: string | null
    masked_value?: string | null
  }
  auth: {
    provider: "notebooklm"
    state: "idle" | "running" | "succeeded" | "failed" | "canceled"
    phase?: string
    delivery?: "local_browser" | "projected_browser" | ""
    session_id?: string
    started_at?: string
    updated_at?: string
    finished_at?: string
    message?: string
    error?: { code: string; message: string } | Record<string, unknown>
    projected_browser?: {
      active: boolean
      state: string
      message?: string
      error?: { code?: string; message?: string } | Record<string, unknown>
      strategy?: string
      url?: string
      width?: number
      height?: number
      started_at?: string
      updated_at?: string
      last_frame_seq?: number
      last_frame_at?: string
      controller_attached?: boolean
    }
  }
}

Notes:

  • start may open a browser on the daemon host for Google sign-in when projected is false.
  • start SHOULD expose the sign-in flow through a projected browser surface when projected=true.
  • When the daemon advertises both provider-auth browser attach capabilities as false and a product Web process owns the browser lifecycle, daemon-level start/cancel/disconnect MUST return capability_unavailable; status remains a valid durable credential/provider-state projection.
  • Provider write readiness remains gated by auth_configured and runtime mode.

space_provider_auth_browser_attach ​

Attach to the currently active projected provider-auth browser surface over a dedicated bidirectional NDJSON stream.

Args:

ts
{
  provider: "notebooklm"
  by?: string // user-only
  viewer_mode?: "auto" | "screencast" | "vnc"
}

Handshake result:

ts
{ provider: "notebooklm" }

Streaming mode:

  • After a successful handshake, the connection upgrades into the browser-surface stream described in §4.6.
  • The daemon emits state items when runtime/session status changes and frame items for captured browser frames.
  • The client MAY send browser-control commands (navigate, back, refresh, click, scroll, key, text, resize, close, disconnect).
  • At most one active controller MAY be attached at a time; a second attach attempt SHOULD fail with a busy-style error.
  • If no active projected auth browser exists, attach SHOULD fail with browser_surface_not_found.
  • If the underlying browser runtime is no longer active, attach SHOULD fail with browser_surface_not_active.

space_provider_auth_browser_vnc_attach ​

Attach to the currently active projected provider-auth browser surface over a raw RFB/VNC stream.

Args:

ts
{
  provider: "notebooklm"
  by?: string // user-only
}

Handshake result:

ts
{ provider: "notebooklm" }

Streaming mode:

  • After a successful handshake, the connection upgrades into a raw VNC/RFB byte stream.
  • The operation SHOULD fail with browser_vnc_unavailable when the browser surface is not backed by a local VNC projection.

8.19 Web Model Browser Surface (Optional) ​

web_model_delivery_preferences_get ​

Read the durable browser-delivery preference for one Web Model actor.

Args:

ts
{ group_id: string; actor_id: string }

Result:

ts
{
  group_id: string
  actor_id: string
  preference: {
    mode: "standard" | "image_compat"
    updated_at: string
    updated_by: string
  }
}

Missing or invalid stored state MUST resolve to standard without mutating the group. The preference is scoped to (group_id, actor_id) and MUST survive browser-target changes and daemon restarts.

web_model_delivery_preferences_update ​

Update the durable browser-delivery preference for one Web Model actor.

Args:

ts
{
  group_id: string
  actor_id: string
  mode: "standard" | "image_compat"
  by: "user"
}

Result has the same shape as web_model_delivery_preferences_get. The operation is user-only and MUST reject other modes. A runtime turn snapshots the effective mode in turn.delivery.web_model_mode; a preference change therefore applies to the next accepted delivery, not a delivery already in flight.

image_compat is an experimental ChatGPT transport workaround. The browser adapter MUST attach exactly one CCCC-owned blank PNG before invoking Send and MUST NOT use the OS clipboard. An attachment failure before a submit action MUST settle the delivery attempt as failed; it MUST NOT affect the Mail cursor. The mode does not select or change the ChatGPT model.

runtime_wait_next_turn ​

Accept one pending structured-runtime delivery turn. This operation is mutating: it claims work, records pull delivery as accepted, and sets the actor's active turn. It MUST NOT advance the Mail cursor.

Args:

ts
{
  group_id: string
  actor_id: string
  by: string                 // MUST equal actor_id
  limit?: number             // 1..20, default 20
  transport?: "web_model_pull" | "web_model_browser" // internal browser owner only
}

kind_filter is not supported. Runtime delivery selects only pending direct delivery work; ordinary message_mode="mail" messages remain in Inbox until an explicit promotion or a mailbox notice creates direct work.

Result is one of:

ts
{ status: "stopped"; turn: null }
{ status: "turn_in_progress"; turn: null; active_turn_id: string; event_ids: string[] }
{ status: "idle"; turn: null; suggested_retry_after_ms: number }
{
  status: "work_available"
  turn: {
    turn_id: string
    group_id: string
    actor_id: string
    event_ids: string[]      // exact canonical delivery batch
    latest_event_id: string
    latest_ts: string
    messages: Event[]
    coalesced_text: string
    system_prompt: string
    delivery: {
      mode: "runtime_delivery"
      transport: "web_model_pull" | "web_model_browser"
      max_events: number
      web_model_mode: "standard" | "image_compat"
    }
  }
}

For web_model_pull, every returned source event MUST already have a durable runtime.delivery state of accepted. For web_model_browser, this operation only establishes the browser claim; web_model_browser_delivery_record settles the claim after the submit boundary. A second wait while the actor owns an active turn MUST return turn_in_progress and MUST NOT replace that turn. coalesced_text MUST preserve the complete daemon-rendered batch without a secondary character-count truncation. Browser-originated oversized text is converted to a .txt attachment before daemon delivery; the canonical messages array remains the source of truth for every structured turn.

The browser adapter MUST wait for a signed-in conversation composer before claiming new work. A guest composer or provider security-verification page MUST NOT count as ready. While waiting for sign-in or verification, background delivery MUST NOT navigate to the saved conversation; existing ambiguous submissions still follow their normal reconciliation contract. A browser adapter MUST NOT overwrite an unrelated non-empty composer draft. Legacy draft recovery MAY replace only the exact expected old draft, revalidated on the same target page immediately before replacement, with no user messages, submission echo, or response generation. This MUST NOT grant general overwrite permission or replay an already-submitted message. After an unverified submission, it MUST fence later deliveries even for an existing conversation, until direct evidence verifies that submission or the user explicitly resolves it. User acknowledgement MUST NOT replay the unverified turn or fabricate an accepted runtime handoff. Ordinary occupied composers detected before claiming work MUST leave pending daemon work unclaimed. The adapter also MUST wait while the provider is visibly generating a response.

Immediate submission checks and later recovery MUST use the same receipt: the current batch, source-event and Actor marker in a user message. Message-node counts can change when history is loaded or virtualized and MUST NOT acknowledge a send. Assistant quotes, an emptied composer, generation indicators and a successful click dispatch are not sufficient receipts. Recovery MUST inspect the currently owned bound page rather than promote stored weak observations. Before clicking Send, the adapter MUST recheck the staged draft and the button's stable, unobstructed viewport position. Background-window dispatch MUST NOT rely on a rendering callback or activate another Actor's window.

Only one worker and one submission may own an Actor's delivery at a time; rejected acquisition MUST NOT release the current owner's registration. While a submission remains unverified, the adapter MAY observe the same owned, bound conversation for the exact batch, source-event and Actor marker in a user message. That later receipt (including a manual send) MUST append the accepted handoff before releasing subsequent work. An empty composer, an unrelated message, an assistant echo or a different conversation MUST NOT resolve the uncertainty. Reconciliation MUST NOT click Send, navigate, or modify a draft.

runtime_complete_turn ​

Close the actor's exact active structured-runtime turn after processing.

Args:

ts
{
  group_id: string
  actor_id: string
  by: string                 // MUST equal actor_id
  turn_id: string
  event_ids: string[]        // MUST exactly equal the active turn event_ids
  delivery_id?: string       // default `runtime:<turn_id>`; part of the replay fingerprint
  status?: "done" | "partial" | "failed" | "cancelled" // default done
  summary?: string
}

latest_event_id is not supported. Every supplied event MUST already have a terminal handoff fact (runtime.delivery=accepted|ambiguous) for this actor. Completion records runtime progress and releases the active turn; every status, including done, MUST leave the Mail cursor unchanged. An actor consumes Inbox contents only through inbox_read / cccc_inbox_read. The daemon MUST persist a deterministic runtime.turn.completed receipt before acknowledging completion. An exact retry with the same actor, turn, event IDs, status, and delivery ID MUST replay that receipt even after active-turn state was cleared. Reusing the turn identity with a different fingerprint MUST fail with completion_conflict.

Common result fields:

ts
{
  status: "done" | "partial" | "failed" | "cancelled"
  turn_id: string
  delivery_id: string
  completion_event: CCCSEventV1 // kind="runtime.turn.completed"
  processed_event_ids: string[]
  followup_delivery_scheduled: boolean
  summary: string
}

web_model_runtime_recover_turn ​

Rebuild a previously handed-off Web Model turn without changing runtime state, delivery state, or the actor Mail cursor. This is used only to reconcile a persisted browser-delivery attempt after process interruption.

Args:

ts
{ group_id: string; actor_id: string; event_ids: string[] }

Result:

ts
{
  status: "recovered"
  turn: {
    turn_id: string
    group_id: string
    actor_id: string
    event_ids: string[] // canonical ledger order
    latest_event_id: string
    latest_ts: string
    messages: Event[]
    coalesced_text: string
    system_prompt: string
    delivery: {
      mode: "recovery_no_delivery_mutation"
      web_model_mode: "standard" | "image_compat"
    }
  }
}

Every event MUST exist, be addressed to the actor, have a supported turn kind, and already have a terminal handoff fact (runtime.delivery=accepted|ambiguous). The operation MUST NOT change delivery state, read state, active runtime state, or completion state.

web_model_browser_delivery_record (internal) ​

Append a browser-delivery observation for a claimed Web Model turn. The browser owner uses this operation to expose the canonical message status in the Web surface and to settle the runtime handoff; it does not complete the turn or advance the actor cursor.

Args:

ts
{
  group_id: string
  actor_id: string
  by: string                 // MUST equal actor_id
  turn_id: string
  event_ids: string[]        // 1..20 addressed chat.message/system.notify IDs
  delivery_id: string
  browser_delivery: {
    state: "submitting" | "submitted" | "bound" | "pending" | "ambiguous" | "failed"
    detail?: string
    provider?: string
    target_url?: string
    bound_conversation_url?: string
    pending_conversation_url?: string
    auto_bind_new_chat?: boolean
    resolved_pending_new_chat?: boolean
  }
}

Result:

ts
{ event: CCCSEventV1 }

Each call appends an ordinary web_model.browser_delivery.<state> event. submitted and bound MUST settle every referenced runtime claim as runtime.delivery=accepted; ambiguous and failed settle it with the matching terminal outcome. submitting and pending remain observations only. The browser owner MUST record the terminal handoff before runtime_complete_turn so completion cannot outrun delivery evidence. If the record call itself fails, a verified submission remains completion-pending and reconciliation retries this operation; it MUST NOT resubmit the browser prompt.

web_model_browser_attach ​

Attach to the currently active managed Web Model browser surface over a dedicated bidirectional NDJSON stream.

Args:

ts
{
  group_id?: string
  actor_id?: string
  by?: string
  viewer_mode?: "auto" | "screencast" | "vnc"
}

Handshake result:

ts
{ group_id: string; actor_id: string }

Streaming mode:

  • After a successful handshake, the connection upgrades into the browser-surface stream described in §4.6.
  • The daemon emits state items when runtime/session status changes and frame items for captured browser frames.
  • The client MAY send browser-control commands (navigate, back, refresh, click, scroll, key, text, resize, close, disconnect).
  • The daemon owns the browser runtime; Web clients are surface proxies and MUST NOT create a separate browser runtime for the same Actor.
  • When group_id or actor_id is supplied, the Actor MUST exist and use runtime=web_model|grok_web_model.
  • If no active Web Model browser surface exists, attach SHOULD fail with browser_surface_not_found.
  • If the underlying browser runtime is no longer active, attach SHOULD fail with browser_surface_not_active.

web_model_browser_vnc_attach ​

Attach to the currently active managed Web Model browser surface over a raw RFB/VNC stream.

Args:

ts
{
  group_id?: string
  actor_id?: string
  by?: string
}

Handshake result:

ts
{ group_id: string; actor_id: string }

Streaming mode:

  • After a successful handshake, the connection upgrades into a raw VNC/RFB byte stream.
  • The operation SHOULD fail with browser_vnc_unavailable when the browser surface is not backed by a local VNC projection.

8.20 Copy Groups ​

Copy Groups operations export/import durable CCCC group state as a zip package. Copy packages contain CCCC group state only; workspace repository files are not included.

group_copy_export ​

Export one group as a base64-encoded zip package.

Args:

ts
{
  group_id: string
  by?: string
}

Result:

ts
{
  package_b64: string
  filename: string
  manifest: {
    kind: "cccc.group_copy"
    version: number
    source_group_id: string
    source_title?: string
    exported_at: string
    cccc_version?: string
    source_platform?: string
    export_mode: "group_state_only"
    workspace_included: false
    contains_secrets: false
    content_digest?: string
    content?: Record<string, unknown>
  }
}

Notes:

  • Export MUST exclude live runtime state, browser profiles, credentials, connector secrets, lock files, and rebuildable caches.
  • Export and import MUST exclude persisted IM reply credentials, including state/im_weixin_context_tokens.json; import MUST apply exclusions even when an older package declares contains_secrets: false.
  • Export MUST scrub actor environment secrets from packaged group.yaml.
  • contains_secrets: false means CCCC-managed live credentials and auth sessions are excluded. The package can still contain user-provided sensitive content such as ledger history, memory, blobs, and attachments.
  • This compatibility operation is intended for small packages. Large packages SHOULD use group_copy_export_file and pass the returned package_path to preview/import.

group_copy_export_file ​

Export one group as a zip package stored on the daemon host filesystem.

Args:

ts
{
  group_id: string
  by?: string
}

Result:

ts
{
  package_path: string
  package_size_bytes: number
  filename: string
  manifest: {
    kind: "cccc.group_copy"
    version: number
    source_group_id: string
    source_title?: string
    exported_at: string
    cccc_version?: string
    source_platform?: string
    export_mode: "group_state_only"
    workspace_included: false
    contains_secrets: false
    content_digest?: string
    content?: Record<string, unknown>
  }
}

Notes:

  • The package path is a temporary daemon-local file path intended for local download flows.
  • This operation uses the large package limit. Secret-scrubbing requirements match group_copy_export.

group_copy_preview_import ​

Validate a copy package and return an import preview without writing group state.

Args:

ts
{
  package_b64?: string
  package_path?: string
  by?: string
}

Exactly one of package_b64 or package_path is required. package_b64 is a small-package compatibility path; large local flows SHOULD use package_path.

Result:

ts
{
  preview: {
    manifest: Record<string, unknown>
    source_group_id: string
    source_title: string
    actor_count: number
    actors: Array<Record<string, unknown>>
    source_workspace_root: string
    workspace_root_exists: boolean
    group_id_conflict: boolean
    target_default_scope_conflict?: boolean
    requires_reconnect?: Record<string, boolean>
    workspace_included: false
    contains_secrets: false
    runtime_reset?: Record<string, unknown>
  }
}

Errors:

  • invalid_group_copy when the payload is not a valid supported CCCC group copy.
  • contains_secrets: false in the preview has the same meaning as export: system credentials are excluded, but user content in ledger history, memory, blobs, and attachments can still be sensitive.

group_copy_import ​

Import a group copy into the current CCCC_HOME.

Args:

ts
{
  package_b64?: string
  package_path?: string
  workspace_root?: string
  title?: string
  by?: string
}

Exactly one of package_b64 or package_path is required. package_b64 is a small-package compatibility path; large local flows SHOULD use package_path.

Result:

ts
{
  group_id: string
  source_group_id: string
  group_id_conflict: boolean
  workspace_root: string
  active_scope_key: string
}

Notes:

  • Import MUST stage and validate copy package contents before moving them into groups/<group_id>.
  • If the source group_id conflicts in the target home, import MUST allocate a new group id.
  • Imported groups MUST start stopped: running=false, state="idle".
  • workspace_root, when supplied, remaps the active workspace root during import.
  • Import MUST reject unsupported copy package schema versions, workspace-including copy packages, secret-containing copy packages, path traversal, symlinks, duplicate entries, and unsafe package paths.

9. Appendix: Example Lines ​

9.1 Ping ​

Request line:

json
{"v":1,"op":"ping","args":{}}

Response line:

json
{"v":1,"ok":true,"result":{"version":"0.4.x","implementation":"rust","pid":12345,"ts":"2026-01-13T12:34:56Z","ipc_v":1,"capabilities":{"events_stream":true,"remote_access":true}},"error":null}

9.2 Error ​

json
{"v":1,"ok":false,"result":{},"error":{"code":"missing_group_id","message":"missing group_id","details":{}}}

External ASR Web boundary ​

External provider credentials are Web-owned configuration, not daemon IPC or Group settings. Administrator-only Web routes are:

  • GET /api/v1/voice/asr/providers: redacted provider configuration and credential-presence flags.
  • PUT /api/v1/voice/asr/providers/{provider}: update Bailian/Volcengine configuration; omitted/blank secrets preserve existing values; clear_credentials=true explicitly clears credentials.
  • POST /api/v1/voice/asr/providers/{provider}/probe: test the saved connection without audio.

Group assistant settings select recognition_backend=external_provider_asr and external_asr_provider=bailian|volcengine; they never carry provider secrets. The existing leased /transcriptions/ws route accepts this backend and preserves ready, partial, final, final_asr_text, error, and closed semantics. External document checkpoints use external_provider_asr_streaming and explicit auto_document_max_window_seconds scheduling (10–300 seconds; absent means 300). An explicit null disables periodic submission until stop/recovery. Stable sentences MUST be buffered and flushed in audio-time order; periodic ticks MUST flush due text even when the provider sends no further sentences. Stop/recovery MUST flush pending text regardless of this setting. Provider completion MUST NOT bypass checkpoint recovery: retries retain segment IDs, and a connected browser MUST receive final_asr_text with persistence status even after the final packet's checkpoint failed. These checkpoints have transcript_stage=live; complete final revisions use external_provider_asr_final, transcript_stage=final, and supersede_stage=live. Late live input MUST NOT create another semantic input once that session has a final cloud revision. Provider errors are sanitized; recovered_text on an error contains only recognized non-document speech for recovery into the originating composer, not an automatic Agent request.

A complete final revision MUST NOT be submitted while any nonempty cloud checkpoint lacks acknowledgement. Final revision persistence is not proof of checkpoint or semantic-input delivery. In this case final_asr_text MUST report transcript_persistence=failed, transcript_persisted=false, and transcript_pending_segments, in audio-time order. Each pending record contains the original daemon segment_id, text, start_ms, and end_ms; an uncertain write acknowledgement MUST retain its original idempotency key too.

The browser retries these records sequentially through the existing transcript append operation using the recording's Group, session, document, language, and model. Each retry is stable live input (is_final=true, flush=true, transcript_stage=live, backend external_provider_asr_streaming). Only after all acknowledgements may a complete final result be retried as final-asr. Pending checkpoints take precedence over partial=true: partial results MUST also recover their pending records, but MUST NOT submit a superseding final revision. A failed browser checkpoint retry stops the sequence and retains the remaining IDs and text in its error details. The Web UI reports failure and returns this unconfirmed text to the original Group's composer for user recovery; it MUST NOT dispatch it automatically, mark it committed, or retry indefinitely.

Standalone Direct Group administration ​

The operations below are user-only (by must be user, default user). They do not require membership and do not grant Web administration to the remote peer. New Direct grants and approvals require an existing local group_id. Administrator status/revoke/remove also accept an original missing Group ID, permitting cleanup of records left by an interrupted deletion; another Group ID cannot remove them.

OperationAdditional argumentsResult
connect_direct_statusgroup_idListener configuration, local addresses: [{interface, bind, address}] suggestions, fresh runtime diagnostics, redacted relations for this Group
connect_direct_configurelistener: {bind, address} | null, display_name?, expected_listener?{configured:true}; actual listener state is checked separately
connect_direct_invitegroup_id, expected_listener?: {bind, address}{invitation}; explicit secret-bearing response, never polled
connect_direct_joingroup_id, invitation{id}; persists the request, not approval
connect_direct_approvegroup_id, id{updated:true}; receiver approves the exact pending pair
connect_direct_revokegroup_id, id{updated:true}; authorization is revoked before return
connect_direct_removegroup_id, id{removed:true}; only revoked or expired unapproved records

connect_direct_status is read-only; configuration uses its own store lock without a global dispatcher permit, while invitation/join/approval/revocation/removal use Group write permits and the shared local store lock. Peer network waits do not hold these permits. Status never returns the invitation or secret digest. Local addresses are read from operational interfaces on the daemon machine, excluding loopback, wildcard, multicast and link-local addresses; suggestions do not promise reachability or change saved settings. Enumeration failure yields an empty list, allowing manual configuration. Each suggestion carries a matching IPv4/IPv6 wildcard bind and the configured local port (default 8847). No discovery packet is sent. When supplied, expected_listener compares the full saved listener under the store lock: configure accepts null to mean previously disabled; invite requires an object. A mismatch rejects the mutation instead of overwriting another setup or issuing an invitation for a different address. Omitted expectations preserve explicit CLI use. The Web create action can sequence configuration, bounded readiness reads and one invitation mutation. No peer wait holds a dispatcher permit; closing the page or a failed/uncertain step must not schedule a delayed invitation or retry a POST. Relation expires_at reports the invitation deadline; it does not expire an already active grant. For a joining pending record, expired:true means the local deadline passed, not that approval was refused. state:expired is the receiver's terminal refusal; unconfirmed pending records remain reconcilable and require cancellation before removal. Local deletion/reset retires the old Group's Direct records and catalogs, retaining their IDs as retirement markers. initiated distinguishes the joining side from the receiving administrator who must approve. A saved pending request alone does not prove peer contact. connect_catalog includes approved Direct pairs in external_groups without an account; it never grants instance-wide discovery. connect_status.group_connections includes active Direct relations. connect_group_status.direct_routes identifies account link IDs whose exact pair has an explicit Direct route preference; this is a display projection, not a replacement grant or confirmation of delivery. The Connect standard defines identity, transport, routing and durable delivery semantics.

Web realtime transport (not daemon IPC) ​

GET /api/v1/events/ws upgrades an authenticated browser connection. The optional connect_frame query parameter carries the existing frame capability. On one socket the client may maintain one subscription for each global, ledger, and headless channel. These use the same event sources as their SSE counterparts.

  • Subscribe: {"type":"subscribe","channel":"ledger","id":2,"group_id":"g_example","cursor":"last-event-id"}.
  • Headless subscribe additionally accepts replay (default true).
  • Unsubscribe: {"type":"unsubscribe","channel":"ledger","id":2}.
  • Ready: {"type":"ready","channel":"ledger","id":2}.
  • Event: {"type":"event","channel":"ledger","id":2,"message":{"event":"ledger","id":"event-id","data":{}}}.
  • Producer termination or rejection: {"type":"closed","channel":"ledger","id":2,"code":"permission_denied"}; code may be absent for EOF.
  • Connection-level rejection: {"type":"fatal","code":"auth_required"}.
  • Heartbeat: {"type":"heartbeat"}; WebSocket Ping/Pong also verifies peer liveness.

Subscription IDs must identify the logical subscription, change when a Group is replaced, and be echoed on all its packets. Subscribing again replaces that channel's producer; unsubscribe only affects the matching ID. Scope and live authority checks apply to subscription messages because the socket URL itself contains no Group ID.

Web Model instance connector and conversation binding ​

The private Web port owns one shared ChatGPT browser/profile with a persistent independent window/Page target per Actor. Shared login affects every Actor; global settings own login and connector credentials, Actor settings own conversation pairing. No active-tab switching or model-declared Actor identity is allowed. A manually closed Actor window stays paused for that Actor generation only; Actor/Group removal and generation replacement MUST retire its closed-window marker even after its browser session is gone. Old Actor-specific connector stores require explicit reconfiguration, preserve user history and unresolved delivery evidence, and MUST NOT authorize tools or automatic redelivery.

Shared login and Actor windows MUST become viewable and closable after navigation starts, without waiting for the provider's DOMContentLoaded or load event. Window availability is distinct from delivery readiness: the existing target, composer, draft and running-state checks still apply before claiming or submitting work. The global shared-browser close endpoint MUST close only its provider's owned login Page; it MUST preserve Actor windows, bindings and authenticated profile, without requiring Actors to stop. Web shutdown remains responsible for closing the shared process and display resources.

These IPC operations use the daemon global write permit and require by=user; none is an Actor MCP catalog operation:

OperationRequired input / effect
web_model_connector_configureOptional provider (chatgpt_web, default, or grok_web); create or rotate only that provider’s instance credential and return its secret once. Rotation retains bindings and routing keys.
web_model_connector_revokeconnector_id; disable all tool routes and pending pairings.
web_model_pairing_beginconnector_id, group_id, actor_id; optional automatic=true requires an enabled, unbound Web Model Actor in a running, non-paused Group; otherwise requires a stopped Actor. Issue one 10-minute code, persisting only its hash and automatic intent.
web_model_pairing_acceptconnector_id, code, session_key; private Web port forwards the authenticated host session hash. Return a unique, idempotent receipt for this candidate/session; receipt alone grants no business authority.
web_model_pairing_confirmconnector_id, group_id, actor_id, pairing_id, url; current generation, accepted unexpired pairing and verified stable ChatGPT URL. Recheck the persisted automatic lifecycle condition (or stopped Actor for manual replacement). Bind atomically; reject session/URL collisions.
web_model_pairing_cancelconnector_id, group_id, actor_id, pairing_id; cancel only the named incomplete candidate without changing an existing binding; retain the cancellation to prevent automatic retries.
web_model_pairing_failconnector_id, group_id, actor_id, pairing_id, error_code; retire the named incomplete attempt, reject later acceptance/confirmation, preserve existing bindings and display the failure. A late failure MUST NOT alter a replacement or committed attempt.
web_model_grok_bindconnector_id, group_id, actor_id, url; user-only, stopped Grok Actor and matching Grok connector. Canonical HTTPS grok.com/bot/<UUID> URL, unique within the connector. Saving the same current-generation URL is idempotent. A changed URL requires no unresolved delivery.
web_model_binding_removeconnector_id, group_id, actor_id; stopped Actor with no unresolved delivery; remove only its binding/pairing.

Normal Actor startup authorizes one initial automatic setup exchange for an enabled, unbound Actor in a running Group. The Web supervisor prepares its owned window; no business delivery is claimed until a binding is verified. An existing current-generation attempt, including expired, failed, cancelled or orphaned attempts, MUST prevent another automatic send. Unrelated Actor setup MUST NOT prune these attempt fences.

The private Web POST /api/v1/web-model/pairing exposes connect (explicit retry or stopped-Actor replacement), cancel, and remove. It does not activate the Actor. A bounded Web-owned task sends a one-time code to the captured Page without navigation or overwriting text/attachments. Before web_model_pairing_confirm, it MUST verify the returned receipt in an assistant reply after the exact setup message on that same Page, and a stable conversation URL (unchanged for an existing conversation). MCP receipt alone MUST NOT trigger confirmation. The receipt is generated after acceptance and is absent from the setup prompt and status API. On automatic success, release the pairing control permit and resume the existing business delivery worker.

Pairing submission deduplication MUST compare the complete setup message, including its unique code. A shared explanatory prefix from an earlier attempt is not evidence that the current setup message has been sent.

An initial handshake started on the new-chat page MUST wait through ChatGPT's provisional /c/WEB:<client-id> URL (including its encoded-colon form) on that same Page. A provisional URL MUST NOT be bound, even with a visible receipt; confirmation still requires the stable conversation URL and receipt proof. This transition allowance MUST NOT apply to a handshake started in an existing conversation. Browser inspection failure is an interruption, not evidence that the user changed conversations.

Pairing/navigation/delivery share the Actor control permit. Cancellation invalidates the exact candidate even while sending. Actor stop/restart/new-session and Group pause/stop invalidate incomplete automatic pairings before the lifecycle transition, so a rapid restart cannot accept a late receipt. Failure, cancellation, expiry and Web shutdown MUST NOT automatically resend a prompt or resume a handshake. Status GETs are observational; an unfinished attempt without its owning Web task is reported as interrupted. Earlier bindings and uncertain delivery evidence remain until an explicitly authorized replacement is verified or removed. An unpaired Actor's setup_url is only a persisted browser startup destination; it grants no tool or delivery authority.

Unresolved delivery forbids changing conversations or removing the binding, including persisted completion ambiguous and completion_conflict states after the daemon has stopped working. Re-pairing the same saved stable URL MAY restore access while retaining all pending evidence. The Web UI MUST keep this manual recovery reachable for a stopped unpaired Actor with a saved conversation, even without an earlier pairing attempt. The browser port observes the owned Page URL and serializes pairing/navigation against delivery; URL opening alone never changes authority. Both preview navigation and saved-conversation alignment MUST preserve unsent text and attachment-only drafts.

The ChatGPT remote MCP entrance advertises one fixed tool schema. Connector-level cccc_pair and read-only cccc_connector_status work before Actor activation; they are not nested tools in CCCC's cccc_code_exec, and do not constrain the host's own connector dispatch mechanism. Pairing MUST be declared state-changing: it records a pending link but does not itself read workspace files, execute tasks or grant business-tool access. Setup instructions MUST respect host approval requirements and report rejected calls; browser receipt verification activates the Actor route before subsequent calls can use its configured permissions. All business calls require a current enabled Web Model Actor binding. Each call uses host params._meta["openai/session"], scoped by optional openai/subject and openai/organization; values must be nonempty strings, at most 1,024 bytes, without control characters. Persist only salted hashes, never raw host identifiers. Missing metadata or invalid/stale bindings MUST fail closed; tool arguments, HTTP session IDs, titles and default Actor selection MUST NOT substitute for host metadata.

Code-mode nested calls MUST recheck captured Actor generation and binding revision before executing. Request context fixes the caller (by) and Group; cccc_actor.actor_id remains the explicit management target and MUST NOT be replaced with the caller. Other identity-bearing tools continue to use the bound Actor. Command stdin/output and code cells MUST remain scoped to their original Actor/binding; re-pairing MUST NOT transfer access to retained work. Credential rotation invalidates the old credential without reassigning conversations; revocation and Actor/Group removal invalidate affected routes. These checks cannot undo an already executed command.

Grok Bot routing and provider isolation ​

grok_web_model uses a headless Actor and the same browser queue, exact user-message receipts, review/recovery controls and draft/attachment protection as web_model. It has no CLI command and no ChatGPT pairing exchange. The private Web port owns a separate shared Grok profile, with one persistent Page/window per Actor. Provider settings MUST NOT rotate another provider's connector or close another provider's browser. The version-3 connector store preserves existing version-2 ChatGPT credentials, bindings and correlation salt.

Opening a Grok Actor window requires its current binding's canonical Bot URL. The Web port MUST reject an unconfigured Actor with grok_bot_url_required before creating a browser surface; it MUST NOT fall back to the Grok homepage or a new conversation. The shared login window is separate and MAY open the provider homepage. An Actor viewer MUST complete the owned-window open operation before attaching, rather than treating a registered surface during initialization as a completed open.

The Bot URL belongs to the Actor, including when its runtime comes from a linked Profile. A private Web binding save MUST align an already-open, idle Actor page with the saved Bot URL before reporting success. It MUST verify the page's provider and Actor generation before reuse. A stale page is retired only after checking for drafts and in-progress responses, then replaced through the current provider's shared profile; the new window follows the navigation-start availability rule above. Navigation failure leaves the Actor stopped and the same URL can be retried. This alignment belongs to the save operation, not status GETs or viewer opening.

Grok Bot URL saving is routing configuration authorized in the private CCCC UI. It proves no host-provided Bot identity. Grok business-tool requests MUST include actor_token, an opaque credential selecting an existing Group/Actor/generation/binding revision within the authenticated Grok connector. Possession grants that Actor's configured authority; intentional sharing lends that same authority. Missing, invalid, revoked, stopped or stale routes MUST be rejected. Model-supplied Group/Actor names, progress tokens, transport sessions and connector names MUST NOT substitute for this credential. cccc_connector_status MAY report connector connectivity without a token but MUST NOT select a default Actor. Grok MUST NOT advertise cccc_pair.

The Web MCP boundary removes actor_token before dispatch. Nested code-mode calls inherit and revalidate the captured binding, preserving explicit management targets. Browser delivery reconstructs a stable credential using the private provider signing key and binding identity; it adds the credential only to the rendered task prompt. Canonical ledger events, runtime turns, public settings, binding snapshots, tool output and activity diagnostics MUST NOT expose it. Stop/start and unchanged URL saves retain the binding; unbind, URL replacement, provider/runtime changes and Actor/Group removal retire affected authority. Copy/import/reset MUST NOT copy it.

Grok delivery uses standard mode only. image_compat is ChatGPT-specific and MUST be rejected for Grok. An enabled Submit control alone is not evidence that Grok is idle: Bot working indicators fence later delivery. Only a batch marker inside a Grok user-message element can settle an uncertain handoff; assistant echoes do not count. Host tool approval or safety rejection is reported rather than bypassed or retried blindly.

Released under the Apache-2.0 License.