@vellumai/assistant 0.12.2-dev.202609181713.fb1ff61 → 0.12.2-dev.202609181819.66f51a7
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/docs/mode-sessions.md +183 -0
- package/openapi.yaml +255 -0
- package/package.json +1 -1
- package/src/__tests__/annotate-activity-metadata.test.ts +2 -0
- package/src/__tests__/annotate-risk-options.test.ts +2 -0
- package/src/__tests__/answered-question-persistence.test.ts +2 -0
- package/src/__tests__/approval-routes-http.test.ts +3 -0
- package/src/__tests__/chat-reveal-guard-priming.test.ts +2 -0
- package/src/__tests__/client-os-metadata-persistence.test.ts +35 -1
- package/src/__tests__/conversation-agent-loop-fatal-cleanup.test.ts +2 -0
- package/src/__tests__/conversation-agent-loop.test.ts +426 -14
- package/src/__tests__/conversation-app-control-lifecycle.test.ts +53 -1
- package/src/__tests__/conversation-evictor.test.ts +62 -37
- package/src/__tests__/conversation-history-boundary-repair-failure.test.ts +174 -0
- package/src/__tests__/conversation-loop-db-lock-resilience.test.ts +2 -0
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +2 -0
- package/src/__tests__/conversation-retry-route.test.ts +22 -0
- package/src/__tests__/conversation-routes-disk-view.test.ts +2 -0
- package/src/__tests__/conversation-routes-enabled-plugins.test.ts +2 -0
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +10 -0
- package/src/__tests__/conversation-routes-hidden-queue.test.ts +2 -0
- package/src/__tests__/conversation-routes-slash-commands.test.ts +2 -0
- package/src/__tests__/conversation-surfaces-action-delivery.test.ts +151 -0
- package/src/__tests__/conversation-surfaces-point-at-budget.test.ts +63 -1
- package/src/__tests__/conversation-undo.test.ts +61 -0
- package/src/__tests__/cu-unified-flow.test.ts +82 -1
- package/src/__tests__/db-mode-sessions-migration.test.ts +175 -0
- package/src/__tests__/evict-conversations-for-reload.test.ts +57 -8
- package/src/__tests__/guardian-action-sweep.test.ts +16 -0
- package/src/__tests__/helpers/mock-conversation.ts +21 -0
- package/src/__tests__/helpers/native-web-search-harness.ts +2 -0
- package/src/__tests__/host-cu-proxy.test.ts +46 -2
- package/src/__tests__/http-user-message-parity.test.ts +2 -0
- package/src/__tests__/mode-session-history.test.ts +245 -0
- package/src/__tests__/mode-session-recovery.test.ts +222 -0
- package/src/__tests__/mode-session-wire.test.ts +141 -0
- package/src/__tests__/outbound-slack-persistence.test.ts +2 -0
- package/src/__tests__/persistence-secret-redaction.test.ts +2 -0
- package/src/__tests__/secret-ingress-http.test.ts +2 -0
- package/src/__tests__/send-endpoint-busy.test.ts +163 -1
- package/src/__tests__/surface-completion-in-flight-snapshot.test.ts +2 -0
- package/src/__tests__/tool-preview-lifecycle.test.ts +2 -0
- package/src/__tests__/tool-result-metadata-plumbing.test.ts +2 -0
- package/src/__tests__/tool-start-timestamp.test.ts +2 -0
- package/src/api/events/assistant-turn-start.ts +3 -0
- package/src/api/events/generation-handoff.ts +2 -0
- package/src/api/events/message-complete.ts +2 -0
- package/src/api/events/tool-result.ts +2 -0
- package/src/api/events/tool-use-start.ts +3 -0
- package/src/api/events/user-message-echo.ts +3 -0
- package/src/api/index.ts +18 -0
- package/src/api/mode-session.test.ts +131 -0
- package/src/api/mode-session.ts +148 -0
- package/src/api/responses/conversation-message.ts +6 -0
- package/src/browser/__tests__/operations.test.ts +29 -0
- package/src/browser/operations.ts +29 -0
- package/src/calls/__tests__/voice-session-bridge.test.ts +132 -239
- package/src/calls/guardian-action-sweep.ts +20 -5
- package/src/calls/voice-escalation-target.ts +0 -1
- package/src/calls/voice-session-bridge.ts +52 -116
- package/src/config/feature-flag-registry.json +8 -0
- package/src/config/schemas/channels.ts +0 -18
- package/src/config/session-groups-gate.ts +21 -0
- package/src/conversations/__tests__/message-consolidation.test.ts +149 -0
- package/src/conversations/message-consolidation.ts +102 -5
- package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +10 -1
- package/src/daemon/__tests__/inference-profile-notification.test.ts +2 -0
- package/src/daemon/browser-mode-session.test.ts +402 -0
- package/src/daemon/browser-mode-session.ts +186 -0
- package/src/daemon/computer-use-mode-session.test.ts +210 -0
- package/src/daemon/computer-use-mode-session.ts +105 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +52 -4
- package/src/daemon/conversation-agent-loop.ts +170 -2
- package/src/daemon/conversation-evictor.ts +5 -24
- package/src/daemon/conversation-history.ts +10 -0
- package/src/daemon/conversation-messaging.ts +33 -0
- package/src/daemon/conversation-mode-session.test.ts +1667 -0
- package/src/daemon/conversation-mode-session.ts +1008 -0
- package/src/daemon/conversation-process.ts +5 -0
- package/src/daemon/conversation-store.ts +4 -4
- package/src/daemon/conversation-surfaces.ts +86 -4
- package/src/daemon/conversation.ts +50 -1
- package/src/daemon/host-cu-proxy.ts +21 -3
- package/src/daemon/lifecycle.ts +25 -8
- package/src/daemon/mode-session-startup-recovery.ts +23 -0
- package/src/daemon/mode-session-tracking.ts +54 -0
- package/src/ipc/__tests__/browser-ipc.test.ts +210 -0
- package/src/live-voice/__tests__/live-voice-session-groups-gate.test.ts +87 -0
- package/src/live-voice/__tests__/live-voice-sight-frame-echo.test.ts +41 -0
- package/src/live-voice/__tests__/live-voice-sight-frame.test.ts +1017 -9
- package/src/live-voice/__tests__/live-voice-vad.test.ts +89 -2
- package/src/live-voice/camera-mode-session.test.ts +265 -0
- package/src/live-voice/camera-mode-session.ts +277 -0
- package/src/live-voice/live-voice-manager.ts +26 -1
- package/src/live-voice/live-voice-photo.ts +88 -7
- package/src/live-voice/live-voice-session.ts +299 -8
- package/src/live-voice/protocol.ts +161 -2
- package/src/persistence/conversation-crud.ts +27 -0
- package/src/persistence/conversation-mode-sessions.test.ts +424 -0
- package/src/persistence/conversation-mode-sessions.ts +460 -0
- package/src/persistence/message-metadata.ts +25 -0
- package/src/persistence/migrations/381-create-conversation-mode-sessions.ts +41 -0
- package/src/persistence/schema/conversations.ts +67 -0
- package/src/persistence/steps.ts +15 -0
- package/src/runtime/pending-interactions-mode-session.test.ts +62 -0
- package/src/runtime/pending-interactions.ts +29 -0
- package/src/runtime/routes/browser-context.ts +31 -11
- package/src/runtime/routes/browser-routes.ts +43 -4
- package/src/runtime/routes/conversation-management-routes.ts +4 -1
- package/src/runtime/routes/conversation-routes.ts +112 -26
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# Transcript mode sessions
|
|
2
|
+
|
|
3
|
+
Mode sessions are compact, conversation-owned lifecycle records for grouping
|
|
4
|
+
computer-use, browser, Live vision, and reserved Ambient transcript activity.
|
|
5
|
+
They connect runtime producers to immutable message membership while keeping
|
|
6
|
+
model input and stored message content unchanged.
|
|
7
|
+
|
|
8
|
+
## API contract
|
|
9
|
+
|
|
10
|
+
`src/api/mode-session.ts` owns both shared shapes:
|
|
11
|
+
|
|
12
|
+
- `ModeSession` is immutable transcript membership: an opaque `id` and one of
|
|
13
|
+
`computer_use`, `browser`, `live_vision`, or `ambient`.
|
|
14
|
+
- `ModeSessionSummary` is the durable lifecycle record. It carries the owning
|
|
15
|
+
conversation, mode, timing bounds, boundary message IDs, terminal reason,
|
|
16
|
+
and a positive monotonic revision.
|
|
17
|
+
|
|
18
|
+
Only three statuses are stored: `active`, `completed`, and `interrupted`.
|
|
19
|
+
Waiting and draining are runtime presentation facts and do not add database
|
|
20
|
+
states. Duration is derived from recorded time bounds instead of stored as a
|
|
21
|
+
separate value. Active records have no terminal fields, completed records have
|
|
22
|
+
a confirmed end, and interrupted records may leave the end unknown. A known end
|
|
23
|
+
never precedes the last confirmed activity.
|
|
24
|
+
|
|
25
|
+
## Persistence
|
|
26
|
+
|
|
27
|
+
Migration 381 creates `conversation_mode_sessions`. Each row belongs to one
|
|
28
|
+
conversation through an `ON DELETE CASCADE` foreign key. The
|
|
29
|
+
`(conversation_id, status)` index supports conversation-scoped active reads,
|
|
30
|
+
while the primary key supports bounded ID batches for transcript pages.
|
|
31
|
+
|
|
32
|
+
`conversation-mode-sessions.ts` is the only lifecycle writer. Begin creates
|
|
33
|
+
revision 1. Activity, boundary, revision-only, and terminal updates require the
|
|
34
|
+
caller's expected revision and run in an immediate transaction. Each accepted
|
|
35
|
+
write increments the revision. A stale revision, missing record, or terminal
|
|
36
|
+
record is returned as an expected write outcome. Terminal rows cannot return
|
|
37
|
+
to active or change to a different terminal result. Repeating the same terminal
|
|
38
|
+
outcome is an accepted no-op and does not advance the revision.
|
|
39
|
+
|
|
40
|
+
The first included boundary can precede source activation when a later action
|
|
41
|
+
claims assistant content already persisted in the same logical turn. Last
|
|
42
|
+
activity never moves backward. An interruption may have an unknown end, so
|
|
43
|
+
`endedAt` remains nullable and `lastActivityAt` remains the last confirmed
|
|
44
|
+
time.
|
|
45
|
+
|
|
46
|
+
## Startup recovery
|
|
47
|
+
|
|
48
|
+
After database migrations succeed, the assistant marks every leftover active
|
|
49
|
+
record interrupted before database readiness is published. Recovery increments
|
|
50
|
+
the revision, preserves activity and message boundaries, records
|
|
51
|
+
`assistant_restarted`, and leaves `endedAt` null. It runs on every boot rather
|
|
52
|
+
than as a checkpointed migration.
|
|
53
|
+
|
|
54
|
+
If this auxiliary recovery fails, ordinary database readiness succeeds and
|
|
55
|
+
tracking stays unavailable for that boot. History omits unrecovered active
|
|
56
|
+
descriptors while preserving terminal records and message stamps. A subsequent
|
|
57
|
+
boot retries through the same startup path. Required migration failures still
|
|
58
|
+
block database readiness.
|
|
59
|
+
|
|
60
|
+
Runtime source generations and structural question associations are owned by
|
|
61
|
+
the producer and message-membership integration. Those process-local facts are
|
|
62
|
+
not reconstructed after restart. Resumed mode work uses a fresh session ID and
|
|
63
|
+
callbacks from retired source generations are rejected.
|
|
64
|
+
|
|
65
|
+
New producer admissions are controlled by the default-off `session-groups`
|
|
66
|
+
assistant feature flag. Browser and computer-use actions still execute when the
|
|
67
|
+
flag is off, but they do not activate or claim a mode session. Existing owners
|
|
68
|
+
and terminal cleanup continue to drain so disabling the flag cannot strand
|
|
69
|
+
accepted work. Live voice snapshots the flag when its socket session is
|
|
70
|
+
created: disabled sessions retain the existing camera transport without
|
|
71
|
+
advertising or preparing tracked sight-session epochs.
|
|
72
|
+
|
|
73
|
+
The same flag controls activity-label wording at the shared rendering
|
|
74
|
+
boundary. Disabled views retain their existing labels and phase grouping.
|
|
75
|
+
|
|
76
|
+
The migration, startup recovery, persisted message stamps, and history reads
|
|
77
|
+
remain available while the flag is off. This keeps records made by an enabled
|
|
78
|
+
cohort compatible with rollback and later re-enablement.
|
|
79
|
+
|
|
80
|
+
## Runtime ownership
|
|
81
|
+
|
|
82
|
+
Each live `Conversation` owns one `ConversationModeSessionCoordinator`.
|
|
83
|
+
Computer-use, browser, and camera producers activate a source with a stable
|
|
84
|
+
source identity and guarded generation. The first accepted mode action claims
|
|
85
|
+
the logical turn. The coordinator backfills rows already persisted for that
|
|
86
|
+
turn, then stamps later user, assistant, and tool-result rows as they are
|
|
87
|
+
inserted. A turn keeps its first owner when another mode acts inside it.
|
|
88
|
+
|
|
89
|
+
Source eligibility and captured turn ownership end separately. Ending or
|
|
90
|
+
resetting a source prevents future claims immediately, but a captured turn
|
|
91
|
+
keeps its owner until its final output is persisted. Ordinary computer-use and
|
|
92
|
+
browser sources finish when the turn settles. Camera sources use a source
|
|
93
|
+
lifetime plus a synthetic run turn, so accepted frame writes and captured
|
|
94
|
+
voice turns form a settlement barrier before terminal persistence. A camera
|
|
95
|
+
epoch snapshots ownership before asynchronous frame persistence; stale epochs
|
|
96
|
+
cannot join a later run.
|
|
97
|
+
|
|
98
|
+
Tracking failures are logged and isolated from authorized actions and accepted
|
|
99
|
+
content persistence. They never repeat the underlying action. A refused camera
|
|
100
|
+
start clears client negotiation and falls back to untracked frames without
|
|
101
|
+
revoking consent or ending voice. Individual stale-frame rejection does not
|
|
102
|
+
use this fallback.
|
|
103
|
+
|
|
104
|
+
Server admission is the camera boundary: an admitted frame can finish saving
|
|
105
|
+
after stop, but a device-side capture delivered after its run ends is rejected
|
|
106
|
+
and reclaimed. Reconnect pauses capture delivery and announces a fresh run
|
|
107
|
+
after replacement readiness. In-flight uploads and native samples retain their
|
|
108
|
+
captured ownership and cannot join the replacement run.
|
|
109
|
+
|
|
110
|
+
The coordinator stores only `active`, `completed`, or `interrupted`. Waiting
|
|
111
|
+
and finishing are revisioned runtime hints. Terminal dispositions remain
|
|
112
|
+
pending in memory until all captured turns release, then one idempotent
|
|
113
|
+
compare-and-swap update records the terminal outcome. A successful turn with
|
|
114
|
+
no explicit producer end also finalizes, allowing later work from the same
|
|
115
|
+
resource generation to mint a new session.
|
|
116
|
+
|
|
117
|
+
Regular reload, TTL, LRU, and memory-pressure eviction retain a conversation
|
|
118
|
+
while it has an eligible mode source, captured owner, structural association,
|
|
119
|
+
or unsettled terminal disposition. This preserves accepted camera output and
|
|
120
|
+
exact structural continuation in the coordinator that admitted them. A long
|
|
121
|
+
structural wait therefore remains resident until it is consumed, invalidated,
|
|
122
|
+
or its source retires. Explicit conversation deletion and process shutdown
|
|
123
|
+
keep their existing teardown semantics; startup recovery interrupts any
|
|
124
|
+
durable active record left by process exit.
|
|
125
|
+
|
|
126
|
+
Invalidating the final structural association retires a turn-lifetime source
|
|
127
|
+
and lets accepted output settle. Successful abandoned waits end at their last
|
|
128
|
+
confirmed activity with reason `structural_wait_abandoned`; cleanup does not
|
|
129
|
+
extend their duration. Source-lifetime camera runs remain eligible. A retired
|
|
130
|
+
disposition that cannot persist its terminal update releases residency and
|
|
131
|
+
invalidates history without claiming completion. An active record without live
|
|
132
|
+
ownership has no descriptor and renders as unavailable.
|
|
133
|
+
|
|
134
|
+
## Structural continuation
|
|
135
|
+
|
|
136
|
+
Structural waits associate an exact response identifier with the owned turn.
|
|
137
|
+
Ordinary `ask_question` awaits its resolver inside that turn, so its answer
|
|
138
|
+
resumes the existing owner. A blocking UI surface can accept a new turn: its
|
|
139
|
+
matching response consumes the association before surface cleanup and retains
|
|
140
|
+
the session ID. Rejected, stale, unrelated, or cross-conversation responses
|
|
141
|
+
cannot inherit ownership. Dismissal invalidates the association. Ordinary prose
|
|
142
|
+
and an idle reusable resource do not continue a session; later mode work starts
|
|
143
|
+
a fresh ID and duration.
|
|
144
|
+
|
|
145
|
+
Structural associations intentionally do not survive restart. Startup recovery
|
|
146
|
+
terminalizes the old record, and a late response remains part of the existing
|
|
147
|
+
interaction flow without reopening that record.
|
|
148
|
+
|
|
149
|
+
## Transcript and synchronization
|
|
150
|
+
|
|
151
|
+
`modeSession` is optional message metadata and is stripped from caller-supplied
|
|
152
|
+
metadata. Membership is added only after a row insert succeeds. Text and
|
|
153
|
+
thinking deltas carry no repeated ownership field; row-boundary events carry
|
|
154
|
+
the current owner, and the normal conversation-messages invalidation refetches
|
|
155
|
+
retrospective stamps.
|
|
156
|
+
|
|
157
|
+
History returns one batched `modeSessions` descriptor list beside messages.
|
|
158
|
+
The query resolves records for page members and accepts bounded explicit IDs
|
|
159
|
+
for groups already loaded outside the newest page. Consolidation never crosses
|
|
160
|
+
a stamped/unstamped or different-owner boundary. Same-owner folds retain row
|
|
161
|
+
aliases and `modeSessionActivity.firstAt`/`lastAt`, so donor timestamps survive
|
|
162
|
+
history compaction.
|
|
163
|
+
|
|
164
|
+
Message deletion and retry paths recompute replaceable first/last display
|
|
165
|
+
boundaries for every affected conversation session. They preserve status,
|
|
166
|
+
terminal time, reason, and confirmed activity, and increment the revision.
|
|
167
|
+
Deleting every owned row leaves the lifecycle record available for audit but
|
|
168
|
+
produces no transcript container.
|
|
169
|
+
|
|
170
|
+
All membership, lifecycle, boundary, and runtime-hint changes reuse the
|
|
171
|
+
conversation-messages `sync_changed` tag. Clients merge descriptors by
|
|
172
|
+
revision in the history query cache and refetch through the existing history
|
|
173
|
+
reconciliation path. Explicit stale revisions cannot replace newer cached
|
|
174
|
+
descriptors; an omitted descriptor remains unavailable. There is no separate
|
|
175
|
+
descriptor store, session event stream, or polling endpoint.
|
|
176
|
+
|
|
177
|
+
The transcript keeps lightweight segment identities for stable disclosure keys,
|
|
178
|
+
not duplicate message bodies. Closed children unmount, and eligible mounted
|
|
179
|
+
headers share one coarse clock that pauses while the app is hidden. No header
|
|
180
|
+
visibility observer is required. The clock reads initial desktop visibility
|
|
181
|
+
from the existing attention state on its coarse tick, without publishing a
|
|
182
|
+
shared lifecycle edge. With grouping disabled, the load-older guard uses the
|
|
183
|
+
ordinary flat-transcript behavior and skips grouped DOM scans.
|
package/openapi.yaml
CHANGED
|
@@ -20944,6 +20944,12 @@ paths:
|
|
|
20944
20944
|
schema:
|
|
20945
20945
|
type: integer
|
|
20946
20946
|
description: Maximum number of messages to return.
|
|
20947
|
+
- name: modeSessionIds
|
|
20948
|
+
in: query
|
|
20949
|
+
required: false
|
|
20950
|
+
schema:
|
|
20951
|
+
type: string
|
|
20952
|
+
description: Comma-separated mode-session IDs to include with summaries for loaded active groups outside this page.
|
|
20947
20953
|
responses:
|
|
20948
20954
|
"200":
|
|
20949
20955
|
description: Successful response
|
|
@@ -20959,6 +20965,38 @@ paths:
|
|
|
20959
20965
|
properties:
|
|
20960
20966
|
id:
|
|
20961
20967
|
type: string
|
|
20968
|
+
modeSession:
|
|
20969
|
+
type: object
|
|
20970
|
+
properties:
|
|
20971
|
+
mode:
|
|
20972
|
+
type: string
|
|
20973
|
+
enum:
|
|
20974
|
+
- computer_use
|
|
20975
|
+
- browser
|
|
20976
|
+
- live_vision
|
|
20977
|
+
- ambient
|
|
20978
|
+
id:
|
|
20979
|
+
type: string
|
|
20980
|
+
minLength: 1
|
|
20981
|
+
required:
|
|
20982
|
+
- mode
|
|
20983
|
+
- id
|
|
20984
|
+
additionalProperties: false
|
|
20985
|
+
modeSessionActivity:
|
|
20986
|
+
type: object
|
|
20987
|
+
properties:
|
|
20988
|
+
firstAt:
|
|
20989
|
+
type: integer
|
|
20990
|
+
minimum: 0
|
|
20991
|
+
maximum: 8640000000000000
|
|
20992
|
+
lastAt:
|
|
20993
|
+
type: integer
|
|
20994
|
+
minimum: 0
|
|
20995
|
+
maximum: 8640000000000000
|
|
20996
|
+
required:
|
|
20997
|
+
- firstAt
|
|
20998
|
+
- lastAt
|
|
20999
|
+
additionalProperties: false
|
|
20962
21000
|
mergedMessageIds:
|
|
20963
21001
|
type: array
|
|
20964
21002
|
items:
|
|
@@ -22277,6 +22315,223 @@ paths:
|
|
|
22277
22315
|
locally but the server reports `processing: false`, the turn has ended (or died) and the UI should
|
|
22278
22316
|
stop waiting rather than spin indefinitely. Absent on older daemons that predate this field."
|
|
22279
22317
|
type: boolean
|
|
22318
|
+
modeSessions:
|
|
22319
|
+
description: Conversation-owned mode-session summaries referenced by this page or explicitly requested by ID.
|
|
22320
|
+
type: array
|
|
22321
|
+
items:
|
|
22322
|
+
type: object
|
|
22323
|
+
properties:
|
|
22324
|
+
summary:
|
|
22325
|
+
oneOf:
|
|
22326
|
+
- type: object
|
|
22327
|
+
properties:
|
|
22328
|
+
id:
|
|
22329
|
+
type: string
|
|
22330
|
+
minLength: 1
|
|
22331
|
+
conversationId:
|
|
22332
|
+
type: string
|
|
22333
|
+
minLength: 1
|
|
22334
|
+
mode:
|
|
22335
|
+
type: string
|
|
22336
|
+
enum:
|
|
22337
|
+
- computer_use
|
|
22338
|
+
- browser
|
|
22339
|
+
- live_vision
|
|
22340
|
+
- ambient
|
|
22341
|
+
sourceStartedAt:
|
|
22342
|
+
type: integer
|
|
22343
|
+
minimum: 0
|
|
22344
|
+
maximum: 9007199254740991
|
|
22345
|
+
firstIncludedAt:
|
|
22346
|
+
anyOf:
|
|
22347
|
+
- type: integer
|
|
22348
|
+
minimum: 0
|
|
22349
|
+
maximum: 9007199254740991
|
|
22350
|
+
- type: "null"
|
|
22351
|
+
firstIncludedMessageId:
|
|
22352
|
+
anyOf:
|
|
22353
|
+
- type: string
|
|
22354
|
+
minLength: 1
|
|
22355
|
+
- type: "null"
|
|
22356
|
+
lastActivityAt:
|
|
22357
|
+
type: integer
|
|
22358
|
+
minimum: 0
|
|
22359
|
+
maximum: 9007199254740991
|
|
22360
|
+
lastOwnedMessageId:
|
|
22361
|
+
anyOf:
|
|
22362
|
+
- type: string
|
|
22363
|
+
minLength: 1
|
|
22364
|
+
- type: "null"
|
|
22365
|
+
revision:
|
|
22366
|
+
type: integer
|
|
22367
|
+
exclusiveMinimum: 0
|
|
22368
|
+
maximum: 9007199254740991
|
|
22369
|
+
status:
|
|
22370
|
+
type: string
|
|
22371
|
+
const: active
|
|
22372
|
+
endedAt:
|
|
22373
|
+
type: "null"
|
|
22374
|
+
endReason:
|
|
22375
|
+
type: "null"
|
|
22376
|
+
required:
|
|
22377
|
+
- id
|
|
22378
|
+
- conversationId
|
|
22379
|
+
- mode
|
|
22380
|
+
- sourceStartedAt
|
|
22381
|
+
- firstIncludedAt
|
|
22382
|
+
- firstIncludedMessageId
|
|
22383
|
+
- lastActivityAt
|
|
22384
|
+
- lastOwnedMessageId
|
|
22385
|
+
- revision
|
|
22386
|
+
- status
|
|
22387
|
+
- endedAt
|
|
22388
|
+
- endReason
|
|
22389
|
+
additionalProperties: false
|
|
22390
|
+
- type: object
|
|
22391
|
+
properties:
|
|
22392
|
+
id:
|
|
22393
|
+
type: string
|
|
22394
|
+
minLength: 1
|
|
22395
|
+
conversationId:
|
|
22396
|
+
type: string
|
|
22397
|
+
minLength: 1
|
|
22398
|
+
mode:
|
|
22399
|
+
type: string
|
|
22400
|
+
enum:
|
|
22401
|
+
- computer_use
|
|
22402
|
+
- browser
|
|
22403
|
+
- live_vision
|
|
22404
|
+
- ambient
|
|
22405
|
+
sourceStartedAt:
|
|
22406
|
+
type: integer
|
|
22407
|
+
minimum: 0
|
|
22408
|
+
maximum: 9007199254740991
|
|
22409
|
+
firstIncludedAt:
|
|
22410
|
+
anyOf:
|
|
22411
|
+
- type: integer
|
|
22412
|
+
minimum: 0
|
|
22413
|
+
maximum: 9007199254740991
|
|
22414
|
+
- type: "null"
|
|
22415
|
+
firstIncludedMessageId:
|
|
22416
|
+
anyOf:
|
|
22417
|
+
- type: string
|
|
22418
|
+
minLength: 1
|
|
22419
|
+
- type: "null"
|
|
22420
|
+
lastActivityAt:
|
|
22421
|
+
type: integer
|
|
22422
|
+
minimum: 0
|
|
22423
|
+
maximum: 9007199254740991
|
|
22424
|
+
lastOwnedMessageId:
|
|
22425
|
+
anyOf:
|
|
22426
|
+
- type: string
|
|
22427
|
+
minLength: 1
|
|
22428
|
+
- type: "null"
|
|
22429
|
+
revision:
|
|
22430
|
+
type: integer
|
|
22431
|
+
exclusiveMinimum: 0
|
|
22432
|
+
maximum: 9007199254740991
|
|
22433
|
+
status:
|
|
22434
|
+
type: string
|
|
22435
|
+
const: completed
|
|
22436
|
+
endedAt:
|
|
22437
|
+
type: integer
|
|
22438
|
+
minimum: 0
|
|
22439
|
+
maximum: 9007199254740991
|
|
22440
|
+
endReason:
|
|
22441
|
+
type: string
|
|
22442
|
+
minLength: 1
|
|
22443
|
+
required:
|
|
22444
|
+
- id
|
|
22445
|
+
- conversationId
|
|
22446
|
+
- mode
|
|
22447
|
+
- sourceStartedAt
|
|
22448
|
+
- firstIncludedAt
|
|
22449
|
+
- firstIncludedMessageId
|
|
22450
|
+
- lastActivityAt
|
|
22451
|
+
- lastOwnedMessageId
|
|
22452
|
+
- revision
|
|
22453
|
+
- status
|
|
22454
|
+
- endedAt
|
|
22455
|
+
- endReason
|
|
22456
|
+
additionalProperties: false
|
|
22457
|
+
- type: object
|
|
22458
|
+
properties:
|
|
22459
|
+
id:
|
|
22460
|
+
type: string
|
|
22461
|
+
minLength: 1
|
|
22462
|
+
conversationId:
|
|
22463
|
+
type: string
|
|
22464
|
+
minLength: 1
|
|
22465
|
+
mode:
|
|
22466
|
+
type: string
|
|
22467
|
+
enum:
|
|
22468
|
+
- computer_use
|
|
22469
|
+
- browser
|
|
22470
|
+
- live_vision
|
|
22471
|
+
- ambient
|
|
22472
|
+
sourceStartedAt:
|
|
22473
|
+
type: integer
|
|
22474
|
+
minimum: 0
|
|
22475
|
+
maximum: 9007199254740991
|
|
22476
|
+
firstIncludedAt:
|
|
22477
|
+
anyOf:
|
|
22478
|
+
- type: integer
|
|
22479
|
+
minimum: 0
|
|
22480
|
+
maximum: 9007199254740991
|
|
22481
|
+
- type: "null"
|
|
22482
|
+
firstIncludedMessageId:
|
|
22483
|
+
anyOf:
|
|
22484
|
+
- type: string
|
|
22485
|
+
minLength: 1
|
|
22486
|
+
- type: "null"
|
|
22487
|
+
lastActivityAt:
|
|
22488
|
+
type: integer
|
|
22489
|
+
minimum: 0
|
|
22490
|
+
maximum: 9007199254740991
|
|
22491
|
+
lastOwnedMessageId:
|
|
22492
|
+
anyOf:
|
|
22493
|
+
- type: string
|
|
22494
|
+
minLength: 1
|
|
22495
|
+
- type: "null"
|
|
22496
|
+
revision:
|
|
22497
|
+
type: integer
|
|
22498
|
+
exclusiveMinimum: 0
|
|
22499
|
+
maximum: 9007199254740991
|
|
22500
|
+
status:
|
|
22501
|
+
type: string
|
|
22502
|
+
const: interrupted
|
|
22503
|
+
endedAt:
|
|
22504
|
+
anyOf:
|
|
22505
|
+
- type: integer
|
|
22506
|
+
minimum: 0
|
|
22507
|
+
maximum: 9007199254740991
|
|
22508
|
+
- type: "null"
|
|
22509
|
+
endReason:
|
|
22510
|
+
type: string
|
|
22511
|
+
minLength: 1
|
|
22512
|
+
required:
|
|
22513
|
+
- id
|
|
22514
|
+
- conversationId
|
|
22515
|
+
- mode
|
|
22516
|
+
- sourceStartedAt
|
|
22517
|
+
- firstIncludedAt
|
|
22518
|
+
- firstIncludedMessageId
|
|
22519
|
+
- lastActivityAt
|
|
22520
|
+
- lastOwnedMessageId
|
|
22521
|
+
- revision
|
|
22522
|
+
- status
|
|
22523
|
+
- endedAt
|
|
22524
|
+
- endReason
|
|
22525
|
+
additionalProperties: false
|
|
22526
|
+
type: object
|
|
22527
|
+
runtimeState:
|
|
22528
|
+
type: string
|
|
22529
|
+
enum:
|
|
22530
|
+
- waiting
|
|
22531
|
+
- finishing
|
|
22532
|
+
required:
|
|
22533
|
+
- summary
|
|
22534
|
+
additionalProperties: false
|
|
22280
22535
|
required:
|
|
22281
22536
|
- messages
|
|
22282
22537
|
additionalProperties: false
|
package/package.json
CHANGED
|
@@ -53,6 +53,7 @@ import {
|
|
|
53
53
|
handleToolResult,
|
|
54
54
|
} from "../daemon/conversation-agent-loop-handlers.js";
|
|
55
55
|
import type { ToolActivityMetadata } from "../daemon/message-types/web-activity.js";
|
|
56
|
+
import { mockUnownedModeSessions } from "./helpers/mock-conversation.js";
|
|
56
57
|
|
|
57
58
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
58
59
|
|
|
@@ -65,6 +66,7 @@ function makeDeps(): EventHandlerDeps {
|
|
|
65
66
|
emitActivityState: () => {},
|
|
66
67
|
markWorkspaceTopLevelDirty: () => {},
|
|
67
68
|
currentTurnSurfaces: [],
|
|
69
|
+
modeSessions: mockUnownedModeSessions(),
|
|
68
70
|
} as unknown as EventHandlerDeps["ctx"],
|
|
69
71
|
onEvent: () => {},
|
|
70
72
|
reqId: "test-req",
|
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
createEventHandlerState,
|
|
48
48
|
handleToolResult,
|
|
49
49
|
} from "../daemon/conversation-agent-loop-handlers.js";
|
|
50
|
+
import { mockUnownedModeSessions } from "./helpers/mock-conversation.js";
|
|
50
51
|
|
|
51
52
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
52
53
|
|
|
@@ -59,6 +60,7 @@ function makeDeps(): EventHandlerDeps {
|
|
|
59
60
|
emitActivityState: () => {},
|
|
60
61
|
markWorkspaceTopLevelDirty: () => {},
|
|
61
62
|
currentTurnSurfaces: [],
|
|
63
|
+
modeSessions: mockUnownedModeSessions(),
|
|
62
64
|
} as unknown as EventHandlerDeps["ctx"],
|
|
63
65
|
onEvent: () => {},
|
|
64
66
|
reqId: "test-req",
|
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
createEventHandlerState,
|
|
47
47
|
handleToolResult,
|
|
48
48
|
} from "../daemon/conversation-agent-loop-handlers.js";
|
|
49
|
+
import { mockUnownedModeSessions } from "./helpers/mock-conversation.js";
|
|
49
50
|
|
|
50
51
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
51
52
|
|
|
@@ -60,6 +61,7 @@ function makeDeps(): EventHandlerDeps {
|
|
|
60
61
|
emitActivityState: () => {},
|
|
61
62
|
markWorkspaceTopLevelDirty: () => {},
|
|
62
63
|
currentTurnSurfaces: [],
|
|
64
|
+
modeSessions: mockUnownedModeSessions(),
|
|
63
65
|
} as unknown as EventHandlerDeps["ctx"],
|
|
64
66
|
onEvent: (event: unknown) => {
|
|
65
67
|
emitted.push(event as Record<string, unknown>);
|
|
@@ -9,6 +9,7 @@ import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
|
9
9
|
import type { AssistantEvent } from "../api/index.js";
|
|
10
10
|
import type { Conversation } from "../daemon/conversation.js";
|
|
11
11
|
import type { SecretPromptResult } from "../permissions/secret-prompt-types.js";
|
|
12
|
+
import { mockUnownedModeSessions } from "./helpers/mock-conversation.js";
|
|
12
13
|
|
|
13
14
|
mock.module("../config/env.js", () => ({
|
|
14
15
|
isHttpAuthDisabled: () => true,
|
|
@@ -120,6 +121,7 @@ function makeIdleSession(opts?: {
|
|
|
120
121
|
addPreactivatedSkillId: () => {},
|
|
121
122
|
enqueueMessage: () => ({ queued: false, requestId: "noop" }),
|
|
122
123
|
hasAnyPendingConfirmation: () => false,
|
|
124
|
+
modeSessions: mockUnownedModeSessions(),
|
|
123
125
|
runAgentLoop: async (
|
|
124
126
|
_content: string,
|
|
125
127
|
_messageId: string,
|
|
@@ -183,6 +185,7 @@ function makeConfirmationEmittingSession(opts?: {
|
|
|
183
185
|
addPreactivatedSkillId: () => {},
|
|
184
186
|
enqueueMessage: () => ({ queued: false, requestId: "noop" }),
|
|
185
187
|
hasAnyPendingConfirmation: () => false,
|
|
188
|
+
modeSessions: mockUnownedModeSessions(),
|
|
186
189
|
runAgentLoop: async (
|
|
187
190
|
_content: string,
|
|
188
191
|
_messageId: string,
|
|
@@ -99,6 +99,7 @@ import {
|
|
|
99
99
|
_resetRevealSuccessRegistryForTest,
|
|
100
100
|
recordRevealSuccess,
|
|
101
101
|
} from "../runtime/reveal-success-registry.js";
|
|
102
|
+
import { mockUnownedModeSessions } from "./helpers/mock-conversation.js";
|
|
102
103
|
import {
|
|
103
104
|
SYNTHETIC_OPAQUE_CREDENTIAL,
|
|
104
105
|
SYNTHETIC_OPENAI_PROJECT_KEY,
|
|
@@ -122,6 +123,7 @@ function createMockDeps(collected: AssistantEvent[]): EventHandlerDeps {
|
|
|
122
123
|
emitActivityState: () => {},
|
|
123
124
|
markWorkspaceTopLevelDirty: () => {},
|
|
124
125
|
currentTurnSurfaces: [],
|
|
126
|
+
modeSessions: mockUnownedModeSessions(),
|
|
125
127
|
} as unknown as EventHandlerDeps["ctx"],
|
|
126
128
|
onEvent: (msg: AssistantEvent) => {
|
|
127
129
|
collected.push(msg);
|
|
@@ -36,7 +36,7 @@ mock.module("../persistence/conversation-crud.js", () => ({
|
|
|
36
36
|
content,
|
|
37
37
|
metadata: options?.metadata,
|
|
38
38
|
});
|
|
39
|
-
return { id: `persisted-${addMessageCalls.length}
|
|
39
|
+
return { id: `persisted-${addMessageCalls.length}`, createdAt: 100 };
|
|
40
40
|
},
|
|
41
41
|
getConversation: () => null,
|
|
42
42
|
provenanceFromTrustContext: () => ({}),
|
|
@@ -189,6 +189,40 @@ describe("client OS surface metadata persistence", () => {
|
|
|
189
189
|
browser_family: "safari",
|
|
190
190
|
});
|
|
191
191
|
});
|
|
192
|
+
|
|
193
|
+
test("strips caller ownership and tracks only the inserted structural row", async () => {
|
|
194
|
+
const acceptTurn = mock(() => ({
|
|
195
|
+
id: "session-123",
|
|
196
|
+
mode: "browser" as const,
|
|
197
|
+
}));
|
|
198
|
+
const trackPersistedRow = mock(() => {});
|
|
199
|
+
const ctx = Object.assign(createWebTurnContext("web"), {
|
|
200
|
+
modeSessions: { acceptTurn, trackPersistedRow },
|
|
201
|
+
});
|
|
202
|
+
await persistQueuedMessageBody(ctx, {
|
|
203
|
+
content: "continue",
|
|
204
|
+
requestId: "req-structural",
|
|
205
|
+
activeSurfaceId: "surface-123",
|
|
206
|
+
metadata: {
|
|
207
|
+
modeSession: { id: "spoofed", mode: "computer_use" },
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
expect(lastUserMetadata().modeSession).toBeUndefined();
|
|
212
|
+
expect(acceptTurn).toHaveBeenCalledWith("req-structural", {
|
|
213
|
+
kind: "surface",
|
|
214
|
+
responseId: "surface-123",
|
|
215
|
+
});
|
|
216
|
+
expect(trackPersistedRow).toHaveBeenCalledWith(
|
|
217
|
+
"req-structural",
|
|
218
|
+
"persisted-1",
|
|
219
|
+
100,
|
|
220
|
+
{
|
|
221
|
+
publishMessagesChanged: true,
|
|
222
|
+
startsDisplayBoundary: false,
|
|
223
|
+
},
|
|
224
|
+
);
|
|
225
|
+
});
|
|
192
226
|
});
|
|
193
227
|
|
|
194
228
|
/**
|
|
@@ -20,6 +20,7 @@ import type { AgentLoop } from "../agent/loop.js";
|
|
|
20
20
|
import type { AssistantEvent } from "../api/index.js";
|
|
21
21
|
import type { Conversation } from "../daemon/conversation.js";
|
|
22
22
|
import type { DiskPressureStatus } from "../daemon/disk-pressure-guard.js";
|
|
23
|
+
import { mockUnownedModeSessions } from "./helpers/mock-conversation.js";
|
|
23
24
|
import { setConfig } from "./helpers/set-config.js";
|
|
24
25
|
|
|
25
26
|
// Short turn-boundary commit wait and no second-pass retitling keep the loop
|
|
@@ -109,6 +110,7 @@ function makeCtx(overrides: Partial<Context> = {}): Conversation {
|
|
|
109
110
|
pendingSurfaceActions: new Map(),
|
|
110
111
|
surfaceActionRequestIds: new Set<string>(),
|
|
111
112
|
currentTurnSurfaces: [],
|
|
113
|
+
modeSessions: mockUnownedModeSessions(),
|
|
112
114
|
workingDir: "/tmp",
|
|
113
115
|
channelCapabilities: undefined,
|
|
114
116
|
commandIntent: undefined,
|