@vellumai/assistant 0.12.3 → 0.12.4-dev.202609231620.a15f321
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dockerfile +7 -0
- package/README.md +12 -12
- package/docker/mousepad/90-vellum.gschema.override +4 -0
- package/node_modules/@vellumai/avatar-manifest/src/__tests__/notification-avatar.test.ts +48 -4
- package/node_modules/@vellumai/avatar-manifest/src/notification-avatar.ts +12 -8
- package/openapi.yaml +97 -4
- package/package.json +1 -1
- package/src/__tests__/approval-routes-http.test.ts +4 -2
- package/src/__tests__/call-routes-http.test.ts +4 -2
- package/src/__tests__/channel-readiness-discord.test.ts +8 -0
- package/src/__tests__/channel-readiness-slack-remote.test.ts +8 -0
- package/src/__tests__/config-loader-backfill.test.ts +1 -0
- package/src/__tests__/conversation-pairing.test.ts +10 -0
- package/src/__tests__/conversation-resurface-done.test.ts +289 -0
- package/src/__tests__/default-profile-catalog-fallback.test.ts +1 -0
- package/src/__tests__/gemini-provider.test.ts +31 -0
- package/src/__tests__/inference-profile-validation-jev.test.ts +17 -0
- package/src/__tests__/job-handler-registration-guard.test.ts +1 -0
- package/src/__tests__/llm-catalog-parity.test.ts +4 -0
- package/src/__tests__/llm-resolver-override-or-default.test.ts +43 -0
- package/src/__tests__/openai-responses-prompt-cache.test.ts +3 -1
- package/src/__tests__/platform-callback-registration.test.ts +46 -1
- package/src/__tests__/plugin-api-model-profiles.test.ts +7 -0
- package/src/__tests__/plugin-import-boundary-guard.test.ts +3 -0
- package/src/__tests__/pricing.test.ts +33 -0
- package/src/__tests__/provider-error-scenarios.test.ts +46 -0
- package/src/__tests__/provider-platform-proxy-integration.test.ts +45 -15
- package/src/__tests__/retry-fallback-escalation.test.ts +22 -0
- package/src/__tests__/runtime-attachment-metadata.test.ts +4 -2
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +116 -66
- package/src/__tests__/secret-routes-platform-proxy.test.ts +1 -0
- package/src/__tests__/send-endpoint-busy.test.ts +4 -2
- package/src/__tests__/skill-load-inline-command.test.ts +35 -0
- package/src/__tests__/skill-load-tool.test.ts +42 -11
- package/src/__tests__/skill-tool-factory.test.ts +50 -2
- package/src/__tests__/steer-on-enqueue-question.test.ts +265 -34
- package/src/__tests__/validate-input.test.ts +73 -0
- package/src/__tests__/workspace-migration-159-rename-colliding-jev-profile-name.test.ts +197 -0
- package/src/api/ask-question-tool.ts +50 -0
- package/src/api/events/tool-result.ts +11 -0
- package/src/api/index.ts +7 -0
- package/src/api/requests/dictation.ts +2 -2
- package/src/api/responses/home.ts +27 -0
- package/src/avatar/notification-avatar.test.ts +42 -34
- package/src/avatar/notification-avatar.ts +1 -0
- package/src/calls/__tests__/voice-session-bridge.test.ts +63 -1
- package/src/calls/voice-escalation-judge.ts +6 -5
- package/src/calls/voice-screen-annotation.ts +44 -0
- package/src/calls/voice-session-bridge.ts +22 -0
- package/src/cli/commands/__tests__/plugins.test.ts +29 -1
- package/src/cli/commands/channel-verification-sessions.help.ts +12 -2
- package/src/cli/commands/platform/index.help.ts +2 -1
- package/src/cli/commands/platform/index.ts +8 -2
- package/src/cli/commands/plugins.ts +16 -0
- package/src/cli/lib/__tests__/inspect-plugin.test.ts +29 -0
- package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +92 -2
- package/src/cli/lib/__tests__/plugin-catalog-local.test.ts +15 -0
- package/src/cli/lib/__tests__/plugin-catalog-platform.test.ts +66 -2
- package/src/cli/lib/__tests__/plugin-catalog-resolve.test.ts +21 -0
- package/src/cli/lib/__tests__/plugin-catalog-visibility.test.ts +65 -0
- package/src/cli/lib/__tests__/plugin-details.test.ts +43 -0
- package/src/cli/lib/__tests__/plugins-install-offline.test.ts +26 -0
- package/src/cli/lib/bundled-marketplace.json +4 -4
- package/src/cli/lib/inspect-plugin.ts +5 -0
- package/src/cli/lib/plugin-catalog-cache.ts +41 -4
- package/src/cli/lib/plugin-catalog-local.ts +9 -0
- package/src/cli/lib/plugin-catalog-platform.ts +48 -9
- package/src/cli/lib/plugin-catalog-visibility.ts +41 -0
- package/src/cli/lib/plugin-details.ts +10 -7
- package/src/cli/lib/search-plugins.ts +2 -0
- package/src/config/__tests__/default-profile-catalog.test.ts +61 -1
- package/src/config/__tests__/jev-managed-profile.test.ts +82 -0
- package/src/config/__tests__/sync-gated-profiles.test.ts +71 -0
- package/src/config/bundled-skills/screen-annotation/SKILL.md +6 -3
- package/src/config/call-site-defaults.ts +17 -11
- package/src/config/default-profile-catalog.ts +118 -19
- package/src/config/default-profile-names.ts +69 -0
- package/src/config/feature-flag-registry.json +25 -9
- package/src/config/inference-profile-validation.ts +7 -6
- package/src/config/llm-resolver.ts +16 -0
- package/src/config/profile-text-generation.test.ts +9 -0
- package/src/config/schemas/call-site-catalog.ts +8 -1
- package/src/config/schemas/llm.ts +70 -9
- package/src/config/sidebar-done-gate.ts +27 -0
- package/src/config/sync-gated-profiles.ts +96 -44
- package/src/daemon/__tests__/auto-profile-router.test.ts +228 -0
- package/src/daemon/auto-profile-router.ts +220 -0
- package/src/daemon/conversation-agent-loop.ts +77 -4
- package/src/daemon/conversation-slash.ts +2 -2
- package/src/daemon/conversation-turn-finalize.ts +12 -0
- package/src/daemon/dictation-text-processing.ts +4 -4
- package/src/daemon/handlers/__tests__/conversation-history-search.test.ts +61 -0
- package/src/daemon/handlers/config-channels.ts +3 -1
- package/src/daemon/handlers/conversation-history.ts +11 -1
- package/src/daemon/handlers/conversations.ts +118 -10
- package/src/daemon/pkb-reminder-builder.test.ts +10 -3
- package/src/daemon/pkb-reminder-builder.ts +1 -1
- package/src/desktop/desktop-browser-endpoint.ts +1 -0
- package/src/home/feed-types.ts +2 -0
- package/src/inbound/platform-callback-registration.ts +19 -6
- package/src/ipc/routes/__tests__/conversation-sync-ipc-routes.test.ts +49 -1
- package/src/ipc/routes/conversation-sync-ipc-routes.ts +38 -2
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +27 -0
- package/src/live-voice/__tests__/protocol.test.ts +22 -0
- package/src/live-voice/continuation-judge.ts +1 -1
- package/src/live-voice/live-voice-session.ts +7 -6
- package/src/live-voice/protocol.ts +16 -5
- package/src/messaging/providers/gmail/client.ts +4 -0
- package/src/notifications/README.md +11 -3
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +87 -3
- package/src/notifications/conversation-pairing.ts +23 -8
- package/src/notifications/home-feed-mirror.ts +39 -0
- package/src/notifications/home-feed-side-effect.ts +46 -19
- package/src/oauth/AGENTS.md +7 -1
- package/src/permissions/question-prompter.ts +14 -4
- package/src/persistence/conversation-crud.ts +114 -3
- package/src/persistence/conversation-queries.ts +48 -3
- package/src/persistence/embeddings/__tests__/embedding-local-lifecycle.test.ts +228 -32
- package/src/persistence/embeddings/embedding-backend-shutdown.test.ts +47 -0
- package/src/persistence/embeddings/embedding-backend.ts +23 -41
- package/src/persistence/embeddings/embedding-gemini.test.ts +17 -0
- package/src/persistence/embeddings/embedding-gemini.ts +3 -1
- package/src/persistence/embeddings/embedding-local.ts +72 -17
- package/src/persistence/embeddings/embedding-types.ts +0 -19
- package/src/persistence/jobs-store.ts +229 -0
- package/src/persistence/migrations/383-normalize-opencode-host-connections.ts +44 -0
- package/src/persistence/migrations/__tests__/383-normalize-opencode-host-connections.test.ts +102 -0
- package/src/persistence/steps.ts +12 -0
- package/src/platform/sync-avatar.test.ts +29 -0
- package/src/plugin-api/model-profiles.ts +5 -3
- package/src/plugins/defaults/memory/AGENTS.md +5 -3
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-skill-card.test.ts +2 -0
- package/src/plugins/defaults/memory/__tests__/skill-update-receipt-boundary-guard.test.ts +45 -0
- package/src/plugins/defaults/memory/__tests__/skill-update-receipt-job.test.ts +580 -0
- package/src/plugins/defaults/memory/graph/tools.ts +9 -5
- package/src/plugins/defaults/memory/hooks/conversation-deleted.ts +4 -0
- package/src/plugins/defaults/memory/job-handlers.ts +8 -2
- package/src/plugins/defaults/memory/memory-retrospective-skill-card.ts +5 -0
- package/src/plugins/defaults/memory/skill-update-receipt-job.ts +334 -0
- package/src/plugins/defaults/memory/substrate/skill-content.ts +1 -1
- package/src/plugins/defaults/memory/tools.test.ts +17 -0
- package/src/plugins/defaults/memory/v3/__tests__/render-injection.test.ts +3 -0
- package/src/providers/__tests__/connection-model-compat.test.ts +2 -0
- package/src/providers/__tests__/search-provider-catalog.test.ts +8 -0
- package/src/providers/anthropic/client.ts +2 -2
- package/src/providers/gemini/client.ts +17 -1
- package/src/providers/inference/auth.ts +25 -0
- package/src/providers/jev/ask.test.ts +66 -0
- package/src/{calls/typesafe-noul.ts → providers/jev/ask.ts} +75 -36
- package/src/providers/malformed-tool-call.ts +55 -0
- package/src/providers/model-catalog.ts +257 -4
- package/src/providers/openai/__tests__/chat-completions-malformed-tool-call.test.ts +105 -0
- package/src/providers/openai/chat-completions-provider.ts +30 -0
- package/src/providers/openai/codex-models.ts +2 -0
- package/src/providers/platform-proxy/constants.ts +5 -0
- package/src/providers/retry.ts +68 -36
- package/src/providers/search-provider-catalog.ts +10 -0
- package/src/providers/vellum-model-routing.test.ts +1 -0
- package/src/runtime/__tests__/agent-wake-archived-gate.test.ts +90 -0
- package/src/runtime/__tests__/question-resolution.test.ts +32 -0
- package/src/runtime/agent-wake.ts +9 -2
- package/src/runtime/auth/__tests__/middleware.test.ts +138 -0
- package/src/runtime/auth/middleware.ts +15 -11
- package/src/runtime/middleware/auth.ts +7 -1
- package/src/runtime/pending-interactions.ts +8 -8
- package/src/runtime/question-resolution.ts +54 -8
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +146 -0
- package/src/runtime/routes/__tests__/dictation-processing.test.ts +174 -0
- package/src/runtime/routes/__tests__/global-search-routes.test.ts +74 -1
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +60 -0
- package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +16 -1
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +8 -0
- package/src/runtime/routes/conversation-list-routes.ts +16 -14
- package/src/runtime/routes/conversation-query-routes.ts +4 -0
- package/src/runtime/routes/conversation-routes.ts +4 -2
- package/src/runtime/routes/diagnostics-routes.ts +66 -229
- package/src/runtime/routes/global-search-routes.ts +22 -7
- package/src/runtime/routes/inference-provider-connection-routes.ts +25 -8
- package/src/runtime/routes/llm-call-sites-routes.ts +19 -10
- package/src/runtime/routes/platform-routes.ts +6 -1
- package/src/runtime/routes/plugins-routes.ts +5 -2
- package/src/runtime/sync/documents-sidecar-publish.test.ts +50 -4
- package/src/runtime/sync/resource-sync-events.ts +11 -0
- package/src/runtime/sync/worker-daemon-notify.test.ts +30 -0
- package/src/runtime/sync/worker-daemon-notify.ts +44 -0
- package/src/runtime/verification-outbound-actions.ts +2 -2
- package/src/skills/validate-input.ts +75 -11
- package/src/telegram/__tests__/webhook-health.test.ts +8 -0
- package/src/tools/ask-question/ask-question-tool.test.ts +38 -0
- package/src/tools/ask-question/ask-question-tool.ts +12 -1
- package/src/tools/network/__tests__/web-fetch-firecrawl.test.ts +28 -1
- package/src/tools/network/__tests__/web-fetch-metadata.test.ts +34 -0
- package/src/tools/network/__tests__/web-fetch-tinyfish.test.ts +35 -1
- package/src/tools/network/__tests__/web-search.test.ts +96 -0
- package/src/tools/network/web-fetch.ts +30 -9
- package/src/tools/network/web-search.ts +235 -4
- package/src/tools/skills/execute.ts +1 -1
- package/src/tools/skills/format-input-schema.ts +15 -0
- package/src/tools/skills/load.ts +15 -43
- package/src/tools/skills/scaffold-managed.ts +86 -71
- package/src/tools/skills/skill-tool-factory.ts +2 -1
- package/src/tools/workflows/manage-workflows.ts +4 -2
- package/src/util/worker-pipe.test.ts +146 -0
- package/src/util/worker-pipe.ts +49 -0
- package/src/workspace/migrations/158-repoint-opencode-host-provider-fragments.ts +163 -0
- package/src/workspace/migrations/159-rename-colliding-jev-profile-name.ts +344 -0
- package/src/workspace/migrations/__tests__/158-repoint-opencode-host-provider-fragments.test.ts +188 -0
- package/src/workspace/migrations/registry.ts +4 -0
- package/src/runtime/routes/__tests__/dictation-cleanup-guard.test.ts +0 -107
|
@@ -33,6 +33,10 @@ export const GMAIL_API_BASE_URL =
|
|
|
33
33
|
* requests Calendar-only). Resolving against Gmail read access turns a
|
|
34
34
|
* downstream 403 into an actionable "reconnect Google and grant Gmail" error
|
|
35
35
|
* at resolution time when the selected connection cannot read Gmail.
|
|
36
|
+
*
|
|
37
|
+
* Managed connections never carry this scope: Vellum's app grants Gmail as
|
|
38
|
+
* `https://mail.google.com/`, which `scopeDifference` in `oauth/scope-utils.ts`
|
|
39
|
+
* accepts as covering it.
|
|
36
40
|
*/
|
|
37
41
|
export const GMAIL_REQUIRED_SCOPES = [
|
|
38
42
|
"https://www.googleapis.com/auth/gmail.readonly",
|
|
@@ -13,7 +13,7 @@ Producer → NotificationSignal → Source-Active Gate → Candidate Generation
|
|
|
13
13
|
|
|
14
14
|
### 1. Signal
|
|
15
15
|
|
|
16
|
-
A producer calls `emitNotificationSignal()` with a free-form event name, attention hints (urgency, requiresAction, deadlineAt), and a context payload. The signal is persisted as a `notification_events` row.
|
|
16
|
+
A producer calls `emitNotificationSignal()` with a free-form event name, attention hints (urgency, requiresAction, deadlineAt), and a context payload. The signal is persisted as a `notification_events` row. A `dedupeKey` the producer passes is claimed by that row on insert, with no time window: a later signal carrying a key an existing row holds gets no row of its own and returns `deduplicated: true` before anything below runs. See [Choosing `dedupeKey`](#choosing-dedupekey).
|
|
17
17
|
|
|
18
18
|
Immediately after persistence, a **source-active pre-gate** runs (`checkSourceActiveSuppression`): when `visibleInSourceNow` is set, a hard signal-only invariant the decision engine cannot override, the signal is suppressed and short-circuits here, before candidate generation and the LLM decision. This keeps an always-suppressed signal (e.g. trusted-contact `verification_sent`) from spending an LLM inference whose result would be discarded. The `notification_events` row is still written for the audit trail.
|
|
19
19
|
|
|
@@ -65,7 +65,7 @@ Hard invariants that the LLM cannot override:
|
|
|
65
65
|
|
|
66
66
|
- **Schema validity** -- fail-closed if the decision is malformed
|
|
67
67
|
- **Channel availability** -- at least one selected channel must be connected
|
|
68
|
-
- **Deduplication** --
|
|
68
|
+
- **Deduplication** -- the decision's `dedupeKey` matches another event row younger than the dedupe window (1 hour default). This is the engine's own key; the producer's key was already checked at event insert in step 1, with no window.
|
|
69
69
|
- **Rendered copy quality** -- fail-closed on empty copy or a fallback leak (body equal to the raw event name)
|
|
70
70
|
|
|
71
71
|
Source-active suppression depends only on the signal, so it runs earlier — as the pre-decision gate in `emit-signal.ts` (see step 1), not as a pre-send check here.
|
|
@@ -470,7 +470,15 @@ Step 1 suppresses on this hint before anything else runs, and nothing downstream
|
|
|
470
470
|
- **Opt in only when the notification duplicates something that conversation has already rendered, and only behind a flag that names your producer.** `resolveVisibleInSourceNow()` reads `activity-presence-suppression`, which gates the background-activity failure producer alone, so a new producer needs its own flag or an extension of that one before it calls the resolver. Otherwise the flag silently governs a producer its description does not cover. Having a conversation id in scope is not sufficient; the producer must know that _this_ arrival put the announced thing on screen there. `runtime/background-job-runner.ts` is the worked example: one catch block is reached from several directions, and it passes `presenceConversationId` only when the failure carries a `turnFailure`, the one arrival whose conversation is guaranteed to have emitted a `conversation_error` the user can read. A runner timeout leaves the turn still rendering as in progress and a bootstrap throw never reaches the agent loop, so both keep notifying.
|
|
471
471
|
- **Pass a literal `false` for global and infra signals.** Heartbeat, credential health, webhook health, and worker liveness have no source surface to watch, so there is nothing to duplicate.
|
|
472
472
|
- **Never opt in a `guardian.question` producer.** The card _is_ the prompt, so a `true` suppresses the question's only rendering and the tool hangs until the prompt timeout. `runtime/question-request-guardian-bridge.ts` carries the rationale at its hint block, and three regression pins hold the line: `runtime/__tests__/question-request-guardian-bridge.test.ts`, `__tests__/confirmation-request-guardian-bridge.test.ts`, and `__tests__/notification-guardian-path.test.ts`.
|
|
473
|
-
- **A producer running
|
|
473
|
+
- **A producer running outside the main assistant process cannot read presence at all.** The schedule worker (`schedule/worker.ts`) and the memory jobs worker (`plugins/defaults/memory/worker.ts`) are standalone OS processes, while the resolver reads `assistantEventHub` through `isWebConversationFocused()` and that hub's SSE clients exist only in the assistant process. Every presence read from a worker resolves `false`, whatever the user is watching. The trap is in testing: a test that drives the producer and mocks presence in one process passes while the shipped worker suppresses nothing. Tracked as JARVIS-1711. The boundary runs the other way too: a hub publish from a worker is forwarded to the assistant process best-effort (`runtime/assistant-event-hub.ts`, logged rather than thrown on failure). A worker-side producer is therefore guaranteed its event row and nothing past it: the home-feed write is best-effort in every process (`writeHomeFeedItemForSignal` returns `null` for an ineligible signal, a missing summary, or a failed write, and `emit-signal.ts` only logs a rejection), and the live push to open clients depends on the forward as well.
|
|
474
|
+
|
|
475
|
+
### Choosing `dedupeKey`
|
|
476
|
+
|
|
477
|
+
There are two keys and they do different jobs. Choose the producer's by what it names, not by how often it fires.
|
|
478
|
+
|
|
479
|
+
- **The producer's key is a permanent claim, not a rate limit.** `createEvent` refuses a second row for a key any row already holds (`events-store.ts`), however old that row is. Key on a durable identity (a job row, a request id) to attempt a thing at most once ever; key on that identity plus a time bucket to attempt it at most once per bucket. A key with no identity in it silences every later instance for the whole bucket, whether or not it was the same thing. At most once, not exactly once: the claim lands at insert, before the decision and dispatch, so a process that dies between the two leaves the key claimed with nothing announced, and the retry short-circuits `deduplicated: true` and reports success. A producer that must not lose an announcement across a mid-pipeline shutdown (routine during an app update) needs its own record of delivery, not the key.
|
|
480
|
+
- **A failed pipeline releases the claim; a retry re-emits.** When the pipeline fails after the row landed and no channel ran (`hasChannelSideEffect` is false), `emit-signal.ts` writes the key back to `null`, so the same producer can retry the same key. When any channel already ran, the claim stays: releasing it would double-send to those channels. This is what makes a crash retry idempotent against a double send for a producer keyed on a durable row id: `plugins/defaults/memory/skill-update-receipt-job.ts` keys on its job row, so a retry past the event row meets the claim, is reported `deduplicated: true, pipelineFailed: false`, and completes.
|
|
481
|
+
- **The decision's key is windowed.** The engine picks its own key per decision; step 4 checks it against event rows inside the 1-hour window and writes it onto the row only when the producer passed none. A producer never passes the engine's key and never appeals its verdict.
|
|
474
482
|
|
|
475
483
|
## Audit Trail
|
|
476
484
|
|
|
@@ -20,7 +20,7 @@ const messageAppends: Array<{
|
|
|
20
20
|
conversationId: string;
|
|
21
21
|
role: string;
|
|
22
22
|
content: string;
|
|
23
|
-
options?: { skipIndexing?: boolean };
|
|
23
|
+
options?: { skipIndexing?: boolean; skipResurface?: boolean };
|
|
24
24
|
}> = [];
|
|
25
25
|
const messageRewrites: Array<{ messageId: string; content: string }> = [];
|
|
26
26
|
/** messageId -> the conversation it belongs to, for the scoped lookup. */
|
|
@@ -86,7 +86,7 @@ mock.module("../../persistence/conversation-crud.js", () => ({
|
|
|
86
86
|
conversationId: string,
|
|
87
87
|
role: string,
|
|
88
88
|
content: string,
|
|
89
|
-
options?: { skipIndexing?: boolean },
|
|
89
|
+
options?: { skipIndexing?: boolean; skipResurface?: boolean },
|
|
90
90
|
) => {
|
|
91
91
|
if (messageAppendShouldThrow) {
|
|
92
92
|
throw new Error("simulated message write failure");
|
|
@@ -1039,6 +1039,88 @@ describe("writeHomeFeedItemForSignal", () => {
|
|
|
1039
1039
|
expect(appendCalls).toHaveLength(1);
|
|
1040
1040
|
});
|
|
1041
1041
|
|
|
1042
|
+
// ── skill-update receipts ───────────────────────────────────────────
|
|
1043
|
+
|
|
1044
|
+
const RECEIPT_UPDATES = [
|
|
1045
|
+
{
|
|
1046
|
+
skillId: "skill-a",
|
|
1047
|
+
name: "Skill A",
|
|
1048
|
+
summary: "Changed a step.",
|
|
1049
|
+
conversationId: "conv-source-1",
|
|
1050
|
+
},
|
|
1051
|
+
{ skillId: "skill-b", name: "Skill B", summary: "Changed another." },
|
|
1052
|
+
];
|
|
1053
|
+
|
|
1054
|
+
test("a receipt's entries land as the typed updates, out of metadata, under the list panel", async () => {
|
|
1055
|
+
conversationRow = { conversationType: "background" };
|
|
1056
|
+
const signal = makeSignal({
|
|
1057
|
+
sourceChannel: "assistant_tool",
|
|
1058
|
+
sourceEventName: "activity.complete",
|
|
1059
|
+
contextPayload: {
|
|
1060
|
+
title: "2 skills updated",
|
|
1061
|
+
body: "- Skill A: Changed a step.\n- Skill B: Changed another.",
|
|
1062
|
+
updates: RECEIPT_UPDATES,
|
|
1063
|
+
skillId: "skill-a",
|
|
1064
|
+
},
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
const item = await writeHomeFeedItemForSignal(signal, makeDecision());
|
|
1068
|
+
|
|
1069
|
+
expect(item?.updates).toEqual(RECEIPT_UPDATES);
|
|
1070
|
+
expect(item?.detailPanel).toEqual({ kind: "updatesList" });
|
|
1071
|
+
expect(item?.metadata).not.toHaveProperty("updates");
|
|
1072
|
+
expect(item?.metadata?.skillId).toBe("skill-a");
|
|
1073
|
+
});
|
|
1074
|
+
|
|
1075
|
+
test("a receipt's summary is the payload body, never model copy", async () => {
|
|
1076
|
+
// The pipeline's seed message and body win for every other item; a
|
|
1077
|
+
// paraphrase of a receipt can drop an entry, so the list is kept.
|
|
1078
|
+
conversationRow = { conversationType: "background" };
|
|
1079
|
+
const signal = makeSignal({
|
|
1080
|
+
sourceChannel: "assistant_tool",
|
|
1081
|
+
sourceEventName: "activity.complete",
|
|
1082
|
+
contextPayload: {
|
|
1083
|
+
title: "2 skills updated",
|
|
1084
|
+
body: "- Skill A: Changed a step.\n- Skill B: Changed another.",
|
|
1085
|
+
updates: RECEIPT_UPDATES,
|
|
1086
|
+
},
|
|
1087
|
+
});
|
|
1088
|
+
const decision = makeDecision({
|
|
1089
|
+
selectedChannels: ["vellum"],
|
|
1090
|
+
renderedCopy: {
|
|
1091
|
+
vellum: {
|
|
1092
|
+
title: "Skills refreshed",
|
|
1093
|
+
body: "Two skills were refreshed.",
|
|
1094
|
+
conversationSeedMessage:
|
|
1095
|
+
"The assistant refreshed two of your skills after reviewing recent work.",
|
|
1096
|
+
},
|
|
1097
|
+
},
|
|
1098
|
+
});
|
|
1099
|
+
|
|
1100
|
+
const item = await writeHomeFeedItemForSignal(signal, decision);
|
|
1101
|
+
|
|
1102
|
+
expect(item?.summary).toBe(
|
|
1103
|
+
"- Skill A: Changed a step.\n- Skill B: Changed another.",
|
|
1104
|
+
);
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1107
|
+
test("a malformed or empty updates list leaves the item an ordinary notification", async () => {
|
|
1108
|
+
conversationRow = { conversationType: "background" };
|
|
1109
|
+
for (const updates of [[], [{ name: "no id" }], "not a list"]) {
|
|
1110
|
+
appendCalls.length = 0;
|
|
1111
|
+
const signal = makeSignal({
|
|
1112
|
+
sourceChannel: "assistant_tool",
|
|
1113
|
+
sourceEventName: "activity.complete",
|
|
1114
|
+
contextPayload: { title: "Skill updated", body: "Body", updates },
|
|
1115
|
+
});
|
|
1116
|
+
|
|
1117
|
+
const item = await writeHomeFeedItemForSignal(signal, makeDecision());
|
|
1118
|
+
|
|
1119
|
+
expect(item?.updates).toBeUndefined();
|
|
1120
|
+
expect(item?.detailPanel).toBeUndefined();
|
|
1121
|
+
}
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1042
1124
|
// ── noteworthy derivation ────────────────────────────────────────────
|
|
1043
1125
|
|
|
1044
1126
|
test("assistant_tool source marks the feed item noteworthy", async () => {
|
|
@@ -1264,7 +1346,9 @@ describe("writeHomeFeedItemForSignal", () => {
|
|
|
1264
1346
|
conversationId: "conv-source-1",
|
|
1265
1347
|
role: "assistant",
|
|
1266
1348
|
content: "Three things today.",
|
|
1267
|
-
|
|
1349
|
+
// Bookkeeping about the conversation, so it never resurfaces a
|
|
1350
|
+
// chat the user marked Done.
|
|
1351
|
+
options: { skipIndexing: true, skipResurface: true },
|
|
1268
1352
|
});
|
|
1269
1353
|
expect(item?.metadata?.notificationConversationMessageId).toBe("msg-1");
|
|
1270
1354
|
// A client with the conversation open refetches only on the messages
|
|
@@ -61,6 +61,7 @@ import {
|
|
|
61
61
|
isConversationSeedSane,
|
|
62
62
|
} from "./conversation-seed-composer.js";
|
|
63
63
|
import { isGuardianRequestSignalEvent } from "./guardian-feed-projection.js";
|
|
64
|
+
import { signalMirrorsToHomeFeed } from "./home-feed-mirror.js";
|
|
64
65
|
import type { NotificationSignal } from "./signal.js";
|
|
65
66
|
import type {
|
|
66
67
|
ConversationAction,
|
|
@@ -274,7 +275,7 @@ export async function pairDeliveryWithConversation(
|
|
|
274
275
|
signal.sourceEventName === ASSISTANT_REPLY_EVENT
|
|
275
276
|
) {
|
|
276
277
|
return {
|
|
277
|
-
conversationId:
|
|
278
|
+
conversationId: resolveSourceConversation(signal)?.id ?? null,
|
|
278
279
|
messageId: null,
|
|
279
280
|
strategy,
|
|
280
281
|
createdNewConversation: false,
|
|
@@ -767,12 +768,15 @@ async function resolveChannelDeliveryHome(params: {
|
|
|
767
768
|
* Indexing is skipped for parity with the other notification write paths:
|
|
768
769
|
* notification copy is delivery audit, not conversational memory.
|
|
769
770
|
*/
|
|
770
|
-
function
|
|
771
|
+
function resolveSourceConversation(
|
|
772
|
+
signal: NotificationSignal,
|
|
773
|
+
): { id: string; conversationType?: string } | null {
|
|
771
774
|
if (!signal.sourceContextId) {
|
|
772
775
|
return null;
|
|
773
776
|
}
|
|
774
777
|
try {
|
|
775
|
-
|
|
778
|
+
const row = getConversation(signal.sourceContextId);
|
|
779
|
+
return row ? { id: row.id, conversationType: row.conversationType } : null;
|
|
776
780
|
} catch {
|
|
777
781
|
return null;
|
|
778
782
|
}
|
|
@@ -783,14 +787,25 @@ async function appendBodyToSourceConversation(
|
|
|
783
787
|
channel: NotificationChannel,
|
|
784
788
|
messageContent: string,
|
|
785
789
|
): Promise<{ conversationId: string; messageId: string } | null> {
|
|
786
|
-
const
|
|
787
|
-
if (!
|
|
790
|
+
const source = resolveSourceConversation(signal);
|
|
791
|
+
if (!source) {
|
|
788
792
|
return null;
|
|
789
793
|
}
|
|
794
|
+
const conversationId = source.id;
|
|
790
795
|
|
|
791
|
-
const message = await addMessage(
|
|
792
|
-
|
|
793
|
-
|
|
796
|
+
const message = await addMessage(
|
|
797
|
+
conversationId,
|
|
798
|
+
"assistant",
|
|
799
|
+
messageContent,
|
|
800
|
+
{
|
|
801
|
+
skipIndexing: true,
|
|
802
|
+
// A notification the bell carries is bookkeeping about the
|
|
803
|
+
// conversation, not activity in it, so it must not bounce a chat the
|
|
804
|
+
// user just marked Done back into the sidebar. One the bell does not
|
|
805
|
+
// carry has this transcript as its only home, and the chat resurfaces.
|
|
806
|
+
skipResurface: signalMirrorsToHomeFeed(signal, source.conversationType),
|
|
807
|
+
},
|
|
808
|
+
);
|
|
794
809
|
// `addMessage` projects attention metadata alone, so a client with this
|
|
795
810
|
// conversation open needs the messages tag to refetch the transcript. A
|
|
796
811
|
// notification the user taps through to has every chance of landing on an
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { isBackgroundConversationType } from "../persistence/conversation-types.js";
|
|
2
|
+
import { readChannelAllowlist } from "./channel-allowlist.js";
|
|
3
|
+
import type { NotificationSignal } from "./signal.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Whether the home feed carries a card for `signal`, judged from the signal
|
|
7
|
+
* and the conversation it names. The feed side effect's mirror decision is
|
|
8
|
+
* this plus two checks only it can make (a guardian request always mirrors;
|
|
9
|
+
* a delivery that materialized an assistant-initiated thread never does).
|
|
10
|
+
*
|
|
11
|
+
* The one other reader is the passive vellum append: a notification the
|
|
12
|
+
* bell carries is bookkeeping about the conversation it is appended to, so
|
|
13
|
+
* that append must not resurface a Done chat, while a notification the bell
|
|
14
|
+
* does not carry has the transcript as its only home and must.
|
|
15
|
+
*
|
|
16
|
+
* `assistant_tool` mirrors unconditionally because the documented
|
|
17
|
+
* `notifications send` skill (and background-job failure emits) deliberately
|
|
18
|
+
* target the home feed, unless an exclusive channel allowlist leaves vellum
|
|
19
|
+
* out; `chat.assistant_reply` follows the same rule. An async-background
|
|
20
|
+
* hint or a background-typed source conversation mirrors as well.
|
|
21
|
+
*/
|
|
22
|
+
export function signalMirrorsToHomeFeed(
|
|
23
|
+
signal: NotificationSignal,
|
|
24
|
+
sourceConversationType: string | undefined,
|
|
25
|
+
): boolean {
|
|
26
|
+
if (
|
|
27
|
+
signal.sourceChannel === "assistant_tool" ||
|
|
28
|
+
signal.sourceEventName === "chat.assistant_reply"
|
|
29
|
+
) {
|
|
30
|
+
const allowlist = readChannelAllowlist(signal.contextPayload);
|
|
31
|
+
if (!allowlist || allowlist.includes("vellum")) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (signal.attentionHints.isAsyncBackground) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
return isBackgroundConversationType(sourceConversationType);
|
|
39
|
+
}
|
|
@@ -15,6 +15,8 @@ import {
|
|
|
15
15
|
type FeedItemCategory,
|
|
16
16
|
type FeedItemDetailPanelKind,
|
|
17
17
|
feedItemSchema,
|
|
18
|
+
type FeedItemUpdate,
|
|
19
|
+
feedItemUpdateSchema,
|
|
18
20
|
} from "../home/feed-types.js";
|
|
19
21
|
import { appendFeedItem } from "../home/feed-writer.js";
|
|
20
22
|
import {
|
|
@@ -23,14 +25,11 @@ import {
|
|
|
23
25
|
getMessageById,
|
|
24
26
|
updateMessageContent,
|
|
25
27
|
} from "../persistence/conversation-crud.js";
|
|
26
|
-
import {
|
|
27
|
-
ASSISTANT_INITIATED_SOURCE,
|
|
28
|
-
isBackgroundConversationType,
|
|
29
|
-
} from "../persistence/conversation-types.js";
|
|
28
|
+
import { ASSISTANT_INITIATED_SOURCE } from "../persistence/conversation-types.js";
|
|
30
29
|
import { publishConversationMessagesChanged } from "../runtime/sync/resource-sync-events.js";
|
|
31
30
|
import { getLogger } from "../util/logger.js";
|
|
31
|
+
import { isPlainObject } from "../util/object.js";
|
|
32
32
|
import { normalizeTitle, stripMarkdown } from "../util/short-title.js";
|
|
33
|
-
import { readChannelAllowlist } from "./channel-allowlist.js";
|
|
34
33
|
import { isConversationSeedSane } from "./conversation-seed-composer.js";
|
|
35
34
|
import { deriveTitle } from "./copy-composer.js";
|
|
36
35
|
import {
|
|
@@ -39,6 +38,7 @@ import {
|
|
|
39
38
|
isGuardianRequestSignalEvent,
|
|
40
39
|
receiptGuardianFeedItemIfRequestTerminal,
|
|
41
40
|
} from "./guardian-feed-projection.js";
|
|
41
|
+
import { signalMirrorsToHomeFeed } from "./home-feed-mirror.js";
|
|
42
42
|
import { readPayloadString } from "./notification-utils.js";
|
|
43
43
|
import type { NotificationSignal } from "./signal.js";
|
|
44
44
|
import type {
|
|
@@ -65,6 +65,30 @@ const log = getLogger("home-feed-side-effect");
|
|
|
65
65
|
*/
|
|
66
66
|
const CONVERSATION_MESSAGE_ID_KEY = "notificationConversationMessageId";
|
|
67
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Payload key a skill-update receipt carries its entries under. Read here
|
|
70
|
+
* through the wire schema and mapped onto the item's typed `updates`, so the
|
|
71
|
+
* panel the client opens is decided by the shape, never by copy.
|
|
72
|
+
*/
|
|
73
|
+
const UPDATES_KEY = "updates";
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The receipt entries in `contextPayload`, when it carries a non-empty list
|
|
77
|
+
* that parses as the wire shape; otherwise undefined, and the item renders
|
|
78
|
+
* as an ordinary notification. A malformed list is dropped rather than
|
|
79
|
+
* mirrored, since a client would render whatever survived.
|
|
80
|
+
*/
|
|
81
|
+
function readUpdates(contextPayload: unknown): FeedItemUpdate[] | undefined {
|
|
82
|
+
if (!isPlainObject(contextPayload)) {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
const parsed = feedItemUpdateSchema
|
|
86
|
+
.array()
|
|
87
|
+
.min(1)
|
|
88
|
+
.safeParse(contextPayload[UPDATES_KEY]);
|
|
89
|
+
return parsed.success ? parsed.data : undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
68
92
|
/**
|
|
69
93
|
* Append a `FeedItem` for the given notification signal when the
|
|
70
94
|
* filter criteria pass.
|
|
@@ -106,12 +130,19 @@ export async function writeHomeFeedItemForSignal(
|
|
|
106
130
|
readPayloadString(signal.contextPayload, "body") ??
|
|
107
131
|
readPayloadString(signal.contextPayload, "requestedMessage");
|
|
108
132
|
|
|
133
|
+
// A receipt's summary is the producer's own list of what changed, which
|
|
134
|
+
// the panel renders from the typed entries and a client without the panel
|
|
135
|
+
// reads as-is. Model copy is not allowed to rewrite it: a paraphrase can
|
|
136
|
+
// drop an entry, and the list is the receipt's whole point.
|
|
137
|
+
const updates = readUpdates(signal.contextPayload);
|
|
138
|
+
|
|
109
139
|
// Prefer conversationSeedMessage over body for the home feed: the seed
|
|
110
140
|
// message is richer and may contain structured markdown (lists, headers,
|
|
111
141
|
// bold) that the detail panel renders. The popup-oriented `body` is
|
|
112
142
|
// intentionally short (≤ 2 sentences) and loses formatting.
|
|
113
143
|
const seedCandidate = renderedCopy?.conversationSeedMessage;
|
|
114
144
|
const resolvedSummary =
|
|
145
|
+
(updates ? payloadBody?.trim() : undefined) ||
|
|
115
146
|
(isConversationSeedSane(seedCandidate)
|
|
116
147
|
? seedCandidate.trim()
|
|
117
148
|
: undefined) ||
|
|
@@ -146,7 +177,7 @@ export async function writeHomeFeedItemForSignal(
|
|
|
146
177
|
const now = new Date().toISOString();
|
|
147
178
|
|
|
148
179
|
const category = deriveCategory(signal);
|
|
149
|
-
const panelKind = deriveDetailPanelKind(signal);
|
|
180
|
+
const panelKind = updates ? "updatesList" : deriveDetailPanelKind(signal);
|
|
150
181
|
|
|
151
182
|
const baseMetadata =
|
|
152
183
|
signal.contextPayload &&
|
|
@@ -157,6 +188,9 @@ export async function writeHomeFeedItemForSignal(
|
|
|
157
188
|
// Producer payloads reach the card verbatim, and this key addresses a
|
|
158
189
|
// message row for rewriting, so only this module may set it.
|
|
159
190
|
delete baseMetadata?.[CONVERSATION_MESSAGE_ID_KEY];
|
|
191
|
+
// The entries ride on the item as its typed `updates`, not in the
|
|
192
|
+
// free-form bag, so a client reads them from one place.
|
|
193
|
+
delete baseMetadata?.[UPDATES_KEY];
|
|
160
194
|
|
|
161
195
|
// Link scheduled-run notifications back to their schedule. `notify`-mode
|
|
162
196
|
// jobs put `scheduleId` directly in the context payload; `execute`-mode (and
|
|
@@ -205,6 +239,7 @@ export async function writeHomeFeedItemForSignal(
|
|
|
205
239
|
...(urgency ? { urgency } : {}),
|
|
206
240
|
...(sourceConversationId ? { conversationId: sourceConversationId } : {}),
|
|
207
241
|
...(panelKind ? { detailPanel: { kind: panelKind } } : {}),
|
|
242
|
+
...(updates ? { updates } : {}),
|
|
208
243
|
...(metadata ? { metadata } : {}),
|
|
209
244
|
};
|
|
210
245
|
|
|
@@ -322,6 +357,10 @@ async function appendSummaryToFeedTarget(
|
|
|
322
357
|
try {
|
|
323
358
|
const message = await addMessage(conversationId, "assistant", summary, {
|
|
324
359
|
skipIndexing: true,
|
|
360
|
+
// The body is bookkeeping about the conversation, not activity in it:
|
|
361
|
+
// a receipt for work the conversation already saw must not bounce a
|
|
362
|
+
// chat the user just marked Done back into the sidebar.
|
|
363
|
+
skipResurface: true,
|
|
325
364
|
});
|
|
326
365
|
publishConversationMessagesChanged(conversationId);
|
|
327
366
|
log.info(
|
|
@@ -553,19 +592,7 @@ function resolveHomeFeedMirror(
|
|
|
553
592
|
if (isAssistantInitiatedThreadDelivery(fallbackConversationId)) {
|
|
554
593
|
return { mirror: false };
|
|
555
594
|
}
|
|
556
|
-
if (
|
|
557
|
-
signal.sourceChannel === "assistant_tool" ||
|
|
558
|
-
signal.sourceEventName === "chat.assistant_reply"
|
|
559
|
-
) {
|
|
560
|
-
const allowlist = readChannelAllowlist(signal.contextPayload);
|
|
561
|
-
if (!allowlist || allowlist.includes("vellum")) {
|
|
562
|
-
return { mirror: true, sourceConversationId, sourceScheduleJobId };
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
if (signal.attentionHints.isAsyncBackground) {
|
|
566
|
-
return { mirror: true, sourceConversationId, sourceScheduleJobId };
|
|
567
|
-
}
|
|
568
|
-
if (isBackgroundConversationType(sourceRow?.conversationType)) {
|
|
595
|
+
if (signalMirrorsToHomeFeed(signal, sourceRow?.conversationType)) {
|
|
569
596
|
return { mirror: true, sourceConversationId, sourceScheduleJobId };
|
|
570
597
|
}
|
|
571
598
|
return { mirror: false };
|
package/src/oauth/AGENTS.md
CHANGED
|
@@ -14,6 +14,12 @@ Add an entry to `PROVIDER_SEED_DATA`. Required fields: `provider`, `authorizeUrl
|
|
|
14
14
|
|
|
15
15
|
If the provider will support managed mode, set `managedServiceConfigKey` to a slug matching the key you will add to `ServicesSchema` (e.g. `"acme-oauth"`).
|
|
16
16
|
|
|
17
|
+
#### Scopes: your-own vs. managed
|
|
18
|
+
|
|
19
|
+
`defaultScopes` here is requested only by the user's own OAuth client. A managed connection authorizes against Vellum's OAuth app, whose defaults live in the platform's `provider_registry.json`. Google shows users warnings or errors for scopes not approved for the app, so the managed start endpoint rejects anything outside the platform registry's `extra_config.allowed_scopes`. Managed Google, for example, grants Gmail only as `https://mail.google.com/`, never `gmail.readonly` / `gmail.modify` / `gmail.send`.
|
|
20
|
+
|
|
21
|
+
So anything that names scopes for a managed connection (a hardcoded constant, a client hook, `requestedScopes` on an `oauth_connect` surface, a `--scopes` example in a skill) must use scopes from that approved list, not the `defaultScopes` names. A required-scope check that runs against managed grants must accept the approved equivalent: `grantedScopeCoversRequiredScope` in `scope-utils.ts` treats `mail.google.com` as covering `gmail.readonly`. Adding a scope to a managed flow means getting it approved in the provider console and adding it to `allowed_scopes` first.
|
|
22
|
+
|
|
17
23
|
### 2. _(managed only)_ Add a service schema — `../config/schemas/services.ts`
|
|
18
24
|
|
|
19
25
|
Create a schema and export its type:
|
|
@@ -98,7 +104,7 @@ A grant is pinned when the caller passed `--account` or the resolved connection
|
|
|
98
104
|
|
|
99
105
|
The grant carries one scope for one route, and four checks hold it there:
|
|
100
106
|
|
|
101
|
-
- **Gateway edge auth** (`gateway/src/http/middleware/auth.ts`) refuses it on every gateway-native route, with no loopback fallback. `isSingleRouteGrant` calls `isNarrowScopeProfile` from `gateway/src/auth/scopes.ts`, and also catches a proxy subject carrying some other profile. The passthrough itself never passes through edge auth: it falls to the runtime-proxy catch-all, which re-mints the grant's own subject and profile for the daemon. That holds whether or not the gateway requires client-facing auth (`gateway.runtimeProxyRequireAuth
|
|
107
|
+
- **Gateway edge auth** (`gateway/src/http/middleware/auth.ts`) refuses it on every gateway-native route, with no loopback fallback. `isSingleRouteGrant` calls `isNarrowScopeProfile` from `gateway/src/auth/scopes.ts`, and also catches a proxy subject carrying some other profile. The passthrough itself never passes through edge auth: it falls to the runtime-proxy catch-all, which re-mints the grant's own subject and profile for the daemon. That holds whether or not the gateway requires client-facing auth (`gateway.runtimeProxyRequireAuth`): a presented grant is validated the same way in both modes and only a caller presenting no valid grant is answered with the gateway's own service token, so the minted URL works in either and neither mode hands the grant more than `oauth.proxy`.
|
|
102
108
|
- **The gateway's IPC fast path** (`gateway/src/http/routes/ipc-runtime-proxy.ts`) refuses it against any daemon route naming no scope. The daemon's IPC server runs no policy check of its own, so this is the only enforcement an IPC-served request gets.
|
|
103
109
|
- **`enforcePolicy`** (`../runtime/auth/route-policy.ts`) refuses the same on the HTTP path, then applies the route's own scopes. `oauth.proxy` reaches the passthrough and nothing else. It runs wherever `routeRequest` dispatches through the router, which is every `/v1` route and the shareable-pages path bar three groups that earlier branches answer first, each carrying a gate of its own: `GET /v1/audio/:audioId`, deliberately public and served before `authenticateRequest` runs at all because Twilio fetches it directly; the Twilio webhook paths (`TWILIO_WEBHOOK_RE`), also pre-auth and gated on Twilio's request signature; and the WebSocket upgrades in the next bullet, which never reach the route table. A route added anywhere else is covered.
|
|
104
110
|
- **WebSocket upgrades** (`/v1/calls/media-stream`, `/v1/stt/stream`, `/v1/live-voice`, `/v1/watch/stream`) are handled before the route table on both hops, so neither edge auth nor `enforcePolicy` sees them, and each carries its own identity gate. The daemon admits only a private-network peer presenting a gateway service token; at the edge it is an actor principal on `runtime-audio-stream` and `live-voice`, the `speech.relay` scope on the relay, and the relay-token identity on `twilio-media-websocket`.
|
|
@@ -58,6 +58,14 @@ export interface QuestionPromptEntryResult {
|
|
|
58
58
|
export interface QuestionPromptResult {
|
|
59
59
|
entries: QuestionPromptEntryResult[];
|
|
60
60
|
overall: "completed" | "closed" | "timed_out" | "aborted";
|
|
61
|
+
/**
|
|
62
|
+
* True when the answer arrived as a message typed into the chat while the
|
|
63
|
+
* card was still open, rather than through the card itself. Only the first
|
|
64
|
+
* question carries the typed text; the rest stay `skipped`. The
|
|
65
|
+
* `ask_question` tool says so in the model-facing result, so the model can
|
|
66
|
+
* map the reply onto the questions itself.
|
|
67
|
+
*/
|
|
68
|
+
answeredInChat?: boolean;
|
|
61
69
|
}
|
|
62
70
|
|
|
63
71
|
/**
|
|
@@ -178,10 +186,12 @@ export interface QuestionBatchMetadata {
|
|
|
178
186
|
*
|
|
179
187
|
* The idle timeout is a backstop (`getConfig().timeouts.questionResponseTimeoutSec`,
|
|
180
188
|
* default 30 min), not the primary way a prompt is dismissed. An interactive
|
|
181
|
-
* user who
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
189
|
+
* user who types into the chat instead of using the card settles the prompt at
|
|
190
|
+
* the enqueue handler: the typed text answers it, or, when it cannot stand in
|
|
191
|
+
* for an answer, supersedes it (see
|
|
192
|
+
* `daemon/handlers/conversations.ts`). A non-interactive turn resolves
|
|
193
|
+
* immediately at the tool. The backstop only fires when a prompt is left open
|
|
194
|
+
* with no response and no follow-up message.
|
|
185
195
|
*/
|
|
186
196
|
export class QuestionPrompter {
|
|
187
197
|
async prompt(params: QuestionPromptParams): Promise<QuestionPromptOutcome> {
|