diff --git a/sdk/typescript/openapi-console.json b/sdk/typescript/openapi-console.json index 1043bf2c..4731bfd2 100644 --- a/sdk/typescript/openapi-console.json +++ b/sdk/typescript/openapi-console.json @@ -11201,6 +11201,7 @@ } ], "default": null, + "description": "Inline BASE override \u2014 required. Every persona must name a prompt source; built-in file-backed personas are seeded by migration, not created here, so an operator-created persona must supply base_prompt.", "title": "Base Prompt" }, "tool_allowlist": { @@ -11259,7 +11260,7 @@ "type": "object" }, "UpdatePersonaRequest": { - "description": "PATCH body \u2014 absent fields are left unchanged.\n\nExplicit ``null`` is meaningful only on the two resettable fields:\n``base_prompt: null`` clears the override back to the kind's stock\nBASE, and ``tool_allowlist: null`` resets to unrestricted. ``null``\non the boolean flags or ``applies_to_kinds`` is ignored (treated as\nabsent), so a client serializing unset optionals as null cannot\narchive a persona or flip levers by accident.\n\nArchive = ``{\"enabled\": false}``; default flip = ``{\"is_default\": true}``\non the successor (storage demotes the incumbent atomically). ``name``\nis immutable; existing workstreams are never affected by edits.", + "description": "PATCH body \u2014 absent fields are left unchanged.\n\nExplicit ``null`` resets ``tool_allowlist`` to unrestricted, and \u2014 on a\nBUILT-IN persona only \u2014 clears ``base_prompt`` (the operator override),\nreverting to that persona's file-backed prompt. An OPERATOR persona has no\nfallback source, so ``base_prompt: null`` on one is rejected: every persona\nmust name a prompt source. ``null`` on the boolean flags or\n``applies_to_kinds`` is ignored (treated as absent), so a client serializing\nunset optionals as null cannot archive a persona or flip levers by accident.\n\nArchive = ``{\"enabled\": false}``; default flip = ``{\"is_default\": true}``\non the successor (storage demotes the incumbent atomically). ``name``\nis immutable; existing workstreams are never affected by edits.", "properties": { "display_name": { "anyOf": [ @@ -13305,21 +13306,17 @@ }, "pending_approval": { "default": false, - "description": "True when the workstream is parked on ``_approval_event`` awaiting an operator approve/deny. Mirrors the same field on ``DashboardWorkstream`` / cluster live projections so a freshly-loaded chat tab can render the inline approval gate from the detail snapshot before SSE replay arrives.", + "description": "True when at least one approval cycle is live (a gate thread parked awaiting an operator approve/deny). Mirrors the same field on ``DashboardWorkstream`` / cluster live projections so a freshly-loaded chat tab can render the inline approval gate from the detail snapshot before SSE replay arrives.", "title": "Pending Approval", "type": "boolean" }, - "pending_approval_detail": { - "anyOf": [ - { - "$ref": "#/components/schemas/PendingApprovalDetail" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Inline approval payload \u2014 same shape as ``DashboardWorkstream.pending_approval_detail``. ``None`` when no approval is pending. Lets a reload paint the action row + judge verdicts immediately instead of relying on the SSE approve_request replay timing window." + "pending_approval_details": { + "description": "Inline approval payloads, one per live cycle, oldest first \u2014 same shape as ``DashboardWorkstream.pending_approval_details``. Empty when no approval is pending. Lets a reload paint every action row + judge verdicts immediately instead of relying on the SSE approve_request replay timing window. Replaces 1.6's ``pending_approval_detail`` single-object field (breaking, 1.7).", + "items": { + "$ref": "#/components/schemas/PendingApprovalDetail" + }, + "title": "Pending Approval Details", + "type": "array" } }, "required": [ @@ -13332,8 +13329,14 @@ "type": "object" }, "PendingApprovalDetail": { - "description": "Inline approval payload merged into ``DashboardWorkstream``.\n\nSet when a workstream's ``approve_tools`` is parked on\n``_approval_event``; ``None`` (omitted) otherwise. Cross-tenant\nexposure here follows the same trusted-team posture as\n``activity`` / ``tokens`` \u2014 see ``server.py``'s ``dashboard``\nhandler comment.", + "description": "Inline approval payload merged into ``DashboardWorkstream``.\n\nOne entry per live approval CYCLE \u2014 a gate thread parked in\n``approve_tools`` awaiting the operator. Parallel task agents run\nconcurrent gates, so a workstream can have several of these at\nonce (``pending_approval_details``, oldest first). Cross-tenant\nexposure here follows the same trusted-team posture as\n``activity`` / ``tokens`` \u2014 see ``server.py``'s ``dashboard``\nhandler comment.", "properties": { + "cycle_id": { + "default": "", + "description": "Identity of this approval cycle. Echo it back on ``POST /v1/api/workstreams/{ws_id}/approve`` to resolve exactly this round \u2014 required for correctness when several cycles are live (parallel task agents).", + "title": "Cycle Id", + "type": "string" + }, "call_id": { "default": "", "description": "Primary call_id \u2014 first non-empty call_id in items list order. Matches the 409 ``current_call_id`` response from ``POST /v1/api/workstreams/{ws_id}/approve`` so the UI can render the same identifier the server reports as current.", diff --git a/sdk/typescript/openapi-server.json b/sdk/typescript/openapi-server.json index 5d672184..2cb9eb63 100644 --- a/sdk/typescript/openapi-server.json +++ b/sdk/typescript/openapi-server.json @@ -2692,21 +2692,17 @@ }, "pending_approval": { "default": false, - "description": "True when the workstream is parked on ``_approval_event`` awaiting an operator approve/deny. Mirrors the same field on ``DashboardWorkstream`` / cluster live projections so a freshly-loaded chat tab can render the inline approval gate from the detail snapshot before SSE replay arrives.", + "description": "True when at least one approval cycle is live (a gate thread parked awaiting an operator approve/deny). Mirrors the same field on ``DashboardWorkstream`` / cluster live projections so a freshly-loaded chat tab can render the inline approval gate from the detail snapshot before SSE replay arrives.", "title": "Pending Approval", "type": "boolean" }, - "pending_approval_detail": { - "anyOf": [ - { - "$ref": "#/components/schemas/PendingApprovalDetail" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Inline approval payload \u2014 same shape as ``DashboardWorkstream.pending_approval_detail``. ``None`` when no approval is pending. Lets a reload paint the action row + judge verdicts immediately instead of relying on the SSE approve_request replay timing window." + "pending_approval_details": { + "description": "Inline approval payloads, one per live cycle, oldest first \u2014 same shape as ``DashboardWorkstream.pending_approval_details``. Empty when no approval is pending. Lets a reload paint every action row + judge verdicts immediately instead of relying on the SSE approve_request replay timing window. Replaces 1.6's ``pending_approval_detail`` single-object field (breaking, 1.7).", + "items": { + "$ref": "#/components/schemas/PendingApprovalDetail" + }, + "title": "Pending Approval Details", + "type": "array" } }, "required": [ @@ -2719,8 +2715,14 @@ "type": "object" }, "PendingApprovalDetail": { - "description": "Inline approval payload merged into ``DashboardWorkstream``.\n\nSet when a workstream's ``approve_tools`` is parked on\n``_approval_event``; ``None`` (omitted) otherwise. Cross-tenant\nexposure here follows the same trusted-team posture as\n``activity`` / ``tokens`` \u2014 see ``server.py``'s ``dashboard``\nhandler comment.", + "description": "Inline approval payload merged into ``DashboardWorkstream``.\n\nOne entry per live approval CYCLE \u2014 a gate thread parked in\n``approve_tools`` awaiting the operator. Parallel task agents run\nconcurrent gates, so a workstream can have several of these at\nonce (``pending_approval_details``, oldest first). Cross-tenant\nexposure here follows the same trusted-team posture as\n``activity`` / ``tokens`` \u2014 see ``server.py``'s ``dashboard``\nhandler comment.", "properties": { + "cycle_id": { + "default": "", + "description": "Identity of this approval cycle. Echo it back on ``POST /v1/api/workstreams/{ws_id}/approve`` to resolve exactly this round \u2014 required for correctness when several cycles are live (parallel task agents).", + "title": "Cycle Id", + "type": "string" + }, "call_id": { "default": "", "description": "Primary call_id \u2014 first non-empty call_id in items list order. Matches the 409 ``current_call_id`` response from ``POST /v1/api/workstreams/{ws_id}/approve`` so the UI can render the same identifier the server reports as current.", @@ -3004,17 +3006,13 @@ "default": null, "title": "Project Id" }, - "pending_approval_detail": { - "anyOf": [ - { - "$ref": "#/components/schemas/PendingApprovalDetail" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Inline approval payload for the coordinator children-tree UI. Carries the merged ``_pending_approval`` items list + per-call_id LLM verdict cache so a coord can render approve/deny buttons + judge pill without a separate per-child round-trip. ``None`` when no approval is pending. Also surfaced (verbatim) on ``GET /v1/api/cluster/ws/live`` via the ``_CLUSTER_WS_LIVE_KEYS`` projection." + "pending_approval_details": { + "description": "Inline approval payload for the coordinator children-tree UI: EVERY live approval cycle, oldest first \u2014 parallel task agents gate concurrently, so a workstream can hold several prompts at once. Each entry carries the cycle's items + per-call_id LLM verdict cache so a coord can render approve/deny buttons + judge pill without a separate per-child round-trip; resolve each with its ``cycle_id``. Empty when no approval is pending. Also surfaced (verbatim) on ``GET /v1/api/cluster/ws/live`` via the ``_CLUSTER_WS_LIVE_KEYS`` projection. Replaces 1.6's ``pending_approval_detail`` single-object field (breaking, 1.7).", + "items": { + "$ref": "#/components/schemas/PendingApprovalDetail" + }, + "title": "Pending Approval Details", + "type": "array" }, "recent_auto_approvals": { "description": "Per-ws ring buffer (cap 10) of recent tool calls that bypassed the operator approval gate. Surfaces ``WebUI._recent_auto_approvals`` so the coord-tree row can render an 'auto-approved by ...' pill when the child's skill / blanket / admin-policy rules silently let a tool through. Also projected onto ``GET /v1/api/cluster/ws/live`` via ``_CLUSTER_WS_LIVE_KEYS``.", diff --git a/turnstone/api/server_schemas.py b/turnstone/api/server_schemas.py index f9053698..6cec7960 100644 --- a/turnstone/api/server_schemas.py +++ b/turnstone/api/server_schemas.py @@ -287,8 +287,8 @@ class ListWorkstreamsResponse(BaseModel): class PendingApprovalItem(BaseModel): """One pending tool-call inside a ``PendingApprovalDetail`` envelope. - Mirrors the dict ``SessionUIBase.serialize_pending_approval_detail`` - emits per item. ``heuristic_verdict`` / ``judge_verdict`` are kept + Mirrors the dict ``SessionUIBase.serialize_pending_approval_details`` + emits per item inside each cycle entry. ``heuristic_verdict`` / ``judge_verdict`` are kept loosely-typed because the underlying verdict shape varies by tier; consumers that want the full structure can decode against :class:`turnstone.sdk.events.IntentVerdictEvent`. diff --git a/turnstone/console/static/coordinator/coordinator.js b/turnstone/console/static/coordinator/coordinator.js index 5e21da41..ccba8c66 100644 --- a/turnstone/console/static/coordinator/coordinator.js +++ b/turnstone/console/static/coordinator/coordinator.js @@ -4130,8 +4130,8 @@ function createCoordinatorPane(root, wsId, opts) { const existing = childrenState.get(childId); if (!existing) return; existing.state = ev.reason === "deleted" ? "deleted" : "closed"; - // Clearing the live cache eagerly on close prevents a stale - // pending_approval_detail from continuing to render approve/deny + // Clearing the live cache eagerly on close prevents stale + // pending_approval_details from continuing to render approve/deny // buttons on a closed row (its TTL would otherwise survive into // the closed/deleted lifecycle until natural expiry). invalidateLiveBadge(childId); diff --git a/turnstone/core/session_ui_base.py b/turnstone/core/session_ui_base.py index bc77594a..3f529f61 100644 --- a/turnstone/core/session_ui_base.py +++ b/turnstone/core/session_ui_base.py @@ -2223,7 +2223,7 @@ class SessionUIBase: see which path silently approved each call. Heuristic verdict surfaces under ``heuristic_verdict`` for - consistency with :meth:`serialize_pending_approval_detail` + consistency with :meth:`serialize_pending_approval_details` and :class:`api.server_schemas.PendingApprovalItem` — pre- fix this method emitted ``verdict`` while the dashboard payload used ``heuristic_verdict``, leaving JS consumers