@happyvertical/smrt-ingestion 0.57.21 → 0.57.22

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.
Files changed (83) hide show
  1. package/FEEDBACK.md +104 -0
  2. package/PROPOSALS.md +19 -0
  3. package/README.md +126 -0
  4. package/REVIEW.md +272 -4
  5. package/agents/execution.md +72 -0
  6. package/dist/chunks/{execution-9CfjxX8I.js → execution-BzVQax0n.js} +421 -23
  7. package/dist/chunks/execution-BzVQax0n.js.map +1 -0
  8. package/dist/chunks/{feedback-CuUzWKzP.js → feedback-D3UTF8fm.js} +119 -21
  9. package/dist/chunks/feedback-D3UTF8fm.js.map +1 -0
  10. package/dist/chunks/{extraction-CJ-zrbg0.js → proposal-contracts-77Uo1B1j.js} +39 -39
  11. package/dist/chunks/proposal-contracts-77Uo1B1j.js.map +1 -0
  12. package/dist/chunks/{reviewer-instruction-Db3jDN9a.js → reviewer-instruction-CEc3AOuU.js} +2 -2
  13. package/dist/chunks/{reviewer-instruction-Db3jDN9a.js.map → reviewer-instruction-CEc3AOuU.js.map} +1 -1
  14. package/dist/dto.d.ts +69 -0
  15. package/dist/dto.d.ts.map +1 -1
  16. package/dist/execution-contracts.d.ts +4 -1
  17. package/dist/execution-contracts.d.ts.map +1 -1
  18. package/dist/execution-dto.d.ts +16 -0
  19. package/dist/execution-dto.d.ts.map +1 -1
  20. package/dist/execution.d.ts +58 -3
  21. package/dist/execution.d.ts.map +1 -1
  22. package/dist/execution.js +2 -2
  23. package/dist/feedback-dto.d.ts +12 -4
  24. package/dist/feedback-dto.d.ts.map +1 -1
  25. package/dist/feedback-internal.d.ts +9 -0
  26. package/dist/feedback-internal.d.ts.map +1 -1
  27. package/dist/feedback.d.ts +3 -0
  28. package/dist/feedback.d.ts.map +1 -1
  29. package/dist/index.js +2 -2
  30. package/dist/manifest.json +321 -3
  31. package/dist/models.d.ts +15 -0
  32. package/dist/models.d.ts.map +1 -1
  33. package/dist/models.js +73 -2
  34. package/dist/models.js.map +1 -1
  35. package/dist/policy.d.ts +3 -0
  36. package/dist/policy.d.ts.map +1 -1
  37. package/dist/proposal-errors.d.ts +4 -0
  38. package/dist/proposal-errors.d.ts.map +1 -1
  39. package/dist/proposals.d.ts +1 -0
  40. package/dist/proposals.d.ts.map +1 -1
  41. package/dist/proposals.js +11 -6
  42. package/dist/proposals.js.map +1 -1
  43. package/dist/redaction.d.ts +9 -0
  44. package/dist/redaction.d.ts.map +1 -0
  45. package/dist/review-dto.d.ts +14 -4
  46. package/dist/review-dto.d.ts.map +1 -1
  47. package/dist/review-host.d.ts.map +1 -1
  48. package/dist/server.d.ts +82 -10
  49. package/dist/server.d.ts.map +1 -1
  50. package/dist/server.js +366 -47
  51. package/dist/server.js.map +1 -1
  52. package/dist/smrt-knowledge.json +124 -18
  53. package/dist/svelte/components/ActionReview.svelte +39 -9
  54. package/dist/svelte/components/ActionReview.svelte.d.ts +8 -1
  55. package/dist/svelte/components/ActionReview.svelte.d.ts.map +1 -1
  56. package/dist/svelte/components/EvidenceViewer.svelte +60 -4
  57. package/dist/svelte/components/EvidenceViewer.svelte.d.ts +12 -1
  58. package/dist/svelte/components/EvidenceViewer.svelte.d.ts.map +1 -1
  59. package/dist/svelte/components/InboxView.svelte +160 -18
  60. package/dist/svelte/components/InboxView.svelte.d.ts +28 -1
  61. package/dist/svelte/components/InboxView.svelte.d.ts.map +1 -1
  62. package/dist/svelte/components/IntakeWorkspace.svelte +27 -0
  63. package/dist/svelte/components/IntakeWorkspace.svelte.d.ts +19 -0
  64. package/dist/svelte/components/IntakeWorkspace.svelte.d.ts.map +1 -0
  65. package/dist/svelte/components/ReviewView.svelte +264 -23
  66. package/dist/svelte/components/ReviewView.svelte.d.ts +18 -1
  67. package/dist/svelte/components/ReviewView.svelte.d.ts.map +1 -1
  68. package/dist/svelte/components/TriageView.svelte +669 -0
  69. package/dist/svelte/components/TriageView.svelte.d.ts +16 -0
  70. package/dist/svelte/components/TriageView.svelte.d.ts.map +1 -0
  71. package/dist/svelte/i18n.d.ts +39 -0
  72. package/dist/svelte/i18n.d.ts.map +1 -1
  73. package/dist/svelte/i18n.js +39 -0
  74. package/dist/svelte/index.d.ts +1 -0
  75. package/dist/svelte/index.d.ts.map +1 -1
  76. package/dist/svelte/index.js +1 -0
  77. package/dist/svelte/view-utils.d.ts +65 -0
  78. package/dist/svelte/view-utils.d.ts.map +1 -1
  79. package/dist/svelte/view-utils.js +145 -0
  80. package/package.json +13 -13
  81. package/dist/chunks/execution-9CfjxX8I.js.map +0 -1
  82. package/dist/chunks/extraction-CJ-zrbg0.js.map +0 -1
  83. package/dist/chunks/feedback-CuUzWKzP.js.map +0 -1
package/FEEDBACK.md CHANGED
@@ -30,6 +30,110 @@ an authorized existing event; it cannot supply a correctness label or fabricate
30
30
  an execution result. An initial denial does not mutate retention. Once authorized,
31
31
  a narrowed privacy ceiling survives business rollback and later revocation.
32
32
 
33
+ Observed `action_decision` events carry the decision's `decisionMode`
34
+ (`review` or `review_own`) and `selfDecision` flag from the durable decision
35
+ record; `selfDecision` is true whenever the decider submitted the item, in
36
+ either mode. A submitter confirming their own proposal (#4085) is observed as a
37
+ decision by that actor, never as reviewer correctness evidence; consumers that
38
+ derive reviewer agreement must exclude `selfDecision: true` events, and nothing
39
+ in this package turns them into automation eligibility.
40
+
41
+ ## Reviewer routing choices
42
+
43
+ A `reviewer_routing` record is appended inside the owning transaction when
44
+ feedback is configured and `capture` is authorized, at the moment the choice is
45
+ final: `resolveItem` (REVIEW.md) records `choice: 'none'` keyed
46
+ `routing:<requestId>`, and `submitDecision` records `choice: 'handler'` keyed
47
+ `routing:<decision requestId>` when a reviewer **approves** a reviewer-selected
48
+ action. Handler-choice capture requires `mode: 'review'`: a `review_own`
49
+ submitter approval authorizes normally but appends no reviewer-routing example or
50
+ rule support. A reviewer using the reviewer path may still record an explicit
51
+ routing choice for their own receipt; it does not judge any model suggestion.
52
+ Starting an action records nothing; rejecting or deferring it never produces an
53
+ example. The record needs no model suggestion: it binds the current
54
+ completed/partial attempt (`attemptId`, output digest, analysis revision, frozen
55
+ evidence hashes and the extracted text as `query`), the acting reviewer
56
+ (`reviewer`, also `approvedBy` for a handler choice), the actor who started the
57
+ action (`startedBy`), the start reason, and for a handler choice the
58
+ `handlerId@version`, exact approved arguments and the approved action binding
59
+ (revision and binding hash). Denied or oversized capture, an unavailable source
60
+ pin or malformed evidence skip the projection and leave the authoritative
61
+ decision or disposition intact; database write failures still roll everything
62
+ back. There is no
63
+ supersession: a correction after approval (`previewProposal` on the current
64
+ revision) changes the revision, which retires the old record from retrieval and
65
+ support until the new revision is approved and captured again; a correction
66
+ before approval records nothing until that revision is approved.
67
+
68
+ Settledness is a selection-time condition. Once a generation used an example,
69
+ or a rule suggestion stored its support, the lineage re-checks (reading the
70
+ generation, reviewing or deciding its actions, reading or adopting the rule,
71
+ and replaying `suggestRoutingRule`, which returns the stored suggestion) verify:
72
+
73
+ - every record: its digest, that it is not superseded, retention (no
74
+ redaction or deletion), the unchanged source attempt digest and frozen
75
+ evidence hashes, and live `retrieve` authority in the same tenant and
76
+ confidential scope with every generation recipient allowed by the source's
77
+ provider policy;
78
+ - an `interpretation` record (unchanged from #3676): its action's current
79
+ revision, binding hash, attempt, handler@version and arguments; the action's
80
+ target preconditions at their recorded revisions unless the action
81
+ `succeeded`; the prompt, configuration and model versions; and that the action
82
+ is not reviewer-selected;
83
+ - a `reviewer_routing` record with `choice: 'handler'`: its action's current
84
+ revision, binding hash, attempt, handler@version and arguments, that the
85
+ action is still reviewer-selected, and that its targets are still accessible
86
+ (not their revisions: the record means the handler choice, not target state);
87
+ - a `reviewer_routing` record with `choice: 'none'`: it has no action, so the
88
+ source item is re-checked under the first currently offered receiving handler
89
+ that still authorizes it.
90
+
91
+ Rule support (`suggestRoutingRule`, and on read, adoption and replay of a stored
92
+ suggestion) additionally requires: at least two records from distinct source
93
+ items; `judgment: 'correct'` with matching prompt/configuration/model versions
94
+ for an interpretation record; `choice: 'handler'` for a routing record; each
95
+ record's handler@version equal to the receiving binding's; and each source's
96
+ live routing projection (or full arguments when no projection is configured)
97
+ equal to the receiving action's. Settledness of a routing source is checked only
98
+ when the support is first selected.
99
+
100
+ A later disposition, approval lapse, outcome change or target write of a routing
101
+ source never revokes it.
102
+
103
+ Retrieval scans `interpretation` and `reviewer_routing` records together under
104
+ the same bounds and lexical matcher. A routing example is returned only when its
105
+ digest verifies, its attempt and evidence hashes are unchanged, the source item
106
+ is still readable in the same tenant and confidential scope, every generation
107
+ recipient is allowed by the source's live provider policy, and (for a chosen
108
+ handler) the action is still reviewer-selected and settled — applied
109
+ (`executing`, `succeeded`) or `authorized` with an unexpired approval, on an item
110
+ without a disposition — its binding is still the current revision, and the handler with its candidate
111
+ references is currently offered to the receiver. A `none` choice is
112
+ revalidated under the first currently offered receiving handler the source item
113
+ authorizes. Routing examples carry `kind: 'reviewer_routing'`,
114
+ `judgment: 'correct'`, `routing: { choice, reason }`, `handlerId: 'none'` with an
115
+ empty action for closed items, and no model/prompt/configuration versions, since
116
+ no suggestion was judged. They are included in provider input and selection
117
+ provenance exactly like interpretation examples and are rechecked by the same
118
+ lineage gates.
119
+
120
+ `suggestRoutingRule` accepts `reviewer_routing` support only for `choice: 'handler'`
121
+ records whose action is settled in the same sense when the support is chosen
122
+ (a lapsed, unapplied approval or a disposition excludes it) and whose handler, version and
123
+ full arguments match the receiving binding and each other, from distinct source
124
+ items, under the live routing projection; `none`, rejected, deferred and
125
+ re-corrected records never support a rule. Suggestions and adoptions keep every existing #3676
126
+ rule: no catalog, candidate, permission or automation expansion, and
127
+ `automaticActionEligible: false`. Reviewer-selected actions themselves are refused
128
+ by `authorizeAutomatic`. They are routing choices, never labels for a model
129
+ suggestion: `recordFeedback` refuses an interpretation judgment on one (even
130
+ when the item also carries a generation, and whatever its decision), retrieval
131
+ and rule support ignore interpretation records bound to one, and `ActionReview`
132
+ hides the correctness buttons for them. A disposition says nothing about
133
+ whether a suggestion was right, so interpretation judgments on a resolved item
134
+ still count. Only
135
+ `startReviewerAction` mints that origin; `createAction` refuses it.
136
+
33
137
  ## Retrieval and provider boundaries
34
138
 
35
139
  `retrieveFeedback({ itemId, query })` derives the current offered catalog,
package/PROPOSALS.md CHANGED
@@ -85,6 +85,25 @@ Expired, cancelled, revoked, retried or superseded workers cannot read or publis
85
85
  current lease and each offered handler/candidate are rechecked immediately before
86
86
  each outbound generative/decision boundary and before publication.
87
87
 
88
+ ### Inline interpretation
89
+
90
+ Receipts created with `dispatch: 'inline'` (text/structured parts only) run the
91
+ same stages in-process through `interpretInline(itemId, { requestId, extraction
92
+ })`: an immutable extraction revision claimed under a fenced lease and extracted
93
+ with the built-in UTF-8 extractor, then `prepareGeneration` and a fenced
94
+ interpretation revision generated by `generateProposals`. The analysis lease,
95
+ catalog digest and live catalog/candidate checks, source pin, provenance and
96
+ `StageFailure` categories are identical to the job path; no dispatch intent is
97
+ ever deliverable and nothing is enqueued. A retry with the same `requestId`
98
+ replays the recorded revisions rather than re-preparing against a superseded
99
+ source, also under concurrency. The interpretation append rechecks, under the
100
+ item lock, that its pinned extraction is still the latest revision and refuses
101
+ a superseded pin: first through the general latest-extraction check that
102
+ every interpretation append runs (`GenerationSourceStaleError`), then by the
103
+ inline check that the pinned attempt is still current (`Generation source changed`). Generated suggestions still require explicit preview and a human
104
+ decision, either through the reviewer surface or the submitter-scoped surface
105
+ described in [REVIEW.md](REVIEW.md).
106
+
88
107
  ## Providers and validation
89
108
 
90
109
  `createSDKProposalGenerator(client, identity, {maxTokens,timeoutMs})` uses an
package/README.md CHANGED
@@ -197,11 +197,22 @@ Mount the `/svelte` inbox and review components through an authenticated host us
197
197
  the browser-safe `IntakeReviewHost` DTO contract. See [REVIEW.md](REVIEW.md) for
198
198
  exact revision-bound callbacks, live evidence access, logical split corrections,
199
199
  assignment ownership, and the maintained upload-to-domain-result browser fixture.
200
+ `IntakeWorkspace` puts the inbox, the selected item with its original inline,
201
+ the message box and approve-and-advance on one page with keyboard next/previous;
202
+ it uses the same callbacks and adds no authority (REVIEW.md, "Single-page triage
203
+ workspace").
200
204
 
201
205
  Optional explicit feedback and scoped example retrieval are documented in
202
206
  [FEEDBACK.md](FEEDBACK.md). Approval and execution outcomes remain separate from
203
207
  interpretation correctness; routing policy adoption is explicit and versioned.
204
208
 
209
+ Reviewers can start any offered handler for an item (`startReviewerAction`) or
210
+ close it as handled without action (`resolveItem`); the published host adapter
211
+ wires both as `startAction`/`resolve` and exposes the offered `catalog`. Hosts
212
+ with their own transport add the two POST operations behind the same session,
213
+ CSRF and reviewer checks as `decide`. The `intake_dispositions` table requires
214
+ a manifest migration before upgrading.
215
+
205
216
  ## Recipe preview
206
217
 
207
218
  `ingestion.inbox` and `ingestion.review` declare the user-facing inbox and
@@ -248,6 +259,121 @@ and `maxSuggestions` are each capped at 20; `timeoutMs` is capped at 2147483647.
248
259
  Invalid limits are rejected when constructing `IngestionService`, before receipt
249
260
  or interpretation. See [REVIEW.md](REVIEW.md) for the published server review host.
250
261
 
262
+ ## Trusted internal sources
263
+
264
+ A host with a trusted internal source, such as chat messages recorded in
265
+ smrt-messages, can receive **by reference**, interpret **inline**, and let the
266
+ **submitter** confirm their own proposal (#4085). None of these modes changes
267
+ copied receipts: their identity, validation, queueing and retention are
268
+ untouched, and a replay that switches mode conflicts.
269
+
270
+ **Receive by reference.** A part may be `{ partId, reference: { owner, id,
271
+ version? }, snapshot: { text } | { structured } }` instead of bytes. The
272
+ service derives UTF-8 bytes once (`text/plain` or canonical `application/json`;
273
+ text, reference fields and owner identity containing NUL or unpaired
274
+ surrogates are rejected `invalid` on every engine, and `expireOwned` refuses
275
+ them, so distinct owners can never collapse onto one stored key),
276
+ hashes them into the receipt identity, and stores only that snapshot in the
277
+ durable evidence row; no AssetStore object or asset record is created, and
278
+ `readEvidence` serves the snapshot after the same hash/length check. The
279
+ reference is identity and provenance, never authority. Such parts must be
280
+ text or structured; files still arrive as copied bytes.
281
+
282
+ **Owner-bound retention.** `retention: { kind: 'owner', owner: { kind, id },
283
+ version, expiresAt, replayUntil, acceptAfter }` binds the receipt to its
284
+ owner's lifecycle. The time deadlines remain required and validated exactly as
285
+ before: they are the hard privacy ceiling, and `sweepRetention` and execution's
286
+ policy ceiling still apply. When the owner is deleted the host calls
287
+ `expireOwned({ kind, id })` from its deletion path; every active receipt bound
288
+ to that owner is revoked and removed through the existing `expire` /
289
+ `repairDeletions` protocol, which redacts the snapshot together with every
290
+ other payload, and the deleted delivery is never re-admitted. The owner key is
291
+ recorded in the indexed `owner_key` column (`<kind>:<id>`, so `kind` cannot
292
+ contain `:`). Deletion redaction blanks `owner_key` and `submitter_id` together
293
+ with every payload: only the receipt identity (`delivery_key`) and the captured
294
+ ceiling survive, and the ceiling loses `execution.principalId` unless an
295
+ external action is still `executing` or `outcome_unknown`. So once nothing is
296
+ left to reconcile, a redacted row no longer links an owner to an actor, and a
297
+ repeated `expireOwned` finds nothing. Redaction also overwrites every
298
+ identity- or content-derived digest (`payload_digest`, `scope_digest`, analysis
299
+ `input_digest`, attempt `output_digest`, plan `digest` and proposal, decision
300
+ and execution `binding_hash`) with the fixed marker `redacted`; successful
301
+ result digests remain as the replay tombstone. The principal is retained only
302
+ for an external action that was still in flight when the item was redacted,
303
+ because reconciling it authorizes under that principal; it is dropped when that
304
+ reconciliation reaches a final state, or by the next `repairDeletions` /
305
+ `sweepRetention` pass. For a self-approvable receipt the principal is the
306
+ submitter, so until then it identifies the sender. Later deletion sweeps pass
307
+ this redacted ceiling to the host's `authorize({ operation: 'delete' })`, so
308
+ delete authorization must not depend on `execution.principalId`: a host that
309
+ requires it would refuse, and the sweep would stop with later deletion intents
310
+ still pending. A successful result digest
311
+ is a plain SHA-256 of the canonical result, so it can confirm a guessed result:
312
+ keep handler results to record ids or opaque references, never content.
313
+
314
+ A replay of a deleted delivery is rejected and never becomes fresh work. While
315
+ deletion is still pending it returns `rejected/expired`; once redaction has
316
+ completed the identity digests are gone, so it returns `rejected/conflict`.
317
+ Sources treat both as terminal and must not retry. `delivery_key` stays in plaintext because it is
318
+ the unique receipt identity and the package holds no secret to key it: hosts
319
+ must use an opaque value (for example a random UUID recorded with the message)
320
+ and must not encode owner ids, sender ids or content in `deliveryKey`.
321
+
322
+ Host ordering for owner deletion: first stop issuing receipts for the owner,
323
+ then call `expireOwned`, and call it again once in-flight receipts settle (or
324
+ from a periodic sweep). A receipt still in flight when its owner is expired
325
+ returns `{ kind: 'rejected', category: 'expired' }` rather than `accepted`, but
326
+ one that reserved its row after the first `expireOwned` is only found by the
327
+ later call; until then the time ceiling bounds it. Services are confidential-scope bound, so
328
+ a host whose owner has receipts in several confidential scopes calls
329
+ `expireOwned` from each scope's service; a foreign tenant or scope sees zero.
330
+
331
+ **Inline mode.** `receive({ ..., dispatch: 'inline' })` requires text/structured
332
+ parts only, records the route provenance but **no deliverable dispatch intent**
333
+ for any revision (every intent is retained as `completed`, so no worker or
334
+ `repairDispatches` ever picks it up), and does not enqueue. The host then calls
335
+ `interpretInline(itemId, { requestId, extraction: { configurationRevision,
336
+ limits } })`, which runs in-process exactly what the job path runs: an
337
+ immutable `stage: 'extract'` revision claimed under a fenced lease and
338
+ extracted with the built-in UTF-8 extractor, then `prepareGeneration` and the
339
+ `stage: 'interpret'` revision with the same analysis lease, catalog-live
340
+ checks, provenance and failure categories as `generateProposals`. A retry with
341
+ the same `requestId` replays the immutable revisions, including when two
342
+ identical calls race. The interpretation is appended under the item lock only
343
+ while its pinned extraction is still the latest revision; if another request
344
+ reprocessed the item meanwhile it fails with `GenerationSourceStaleError` (the
345
+ general latest-extraction check every interpretation append runs, #4228) or,
346
+ for a pin whose extraction attempt was replaced, `Generation source changed`. The result reports each
347
+ stage's attempt and safe error and, when generation completed, the authorized
348
+ `getCompletedAnalysis` snapshot. Inline is for send-triggered suggestions that
349
+ must not depend on the scheduler; it is not a bypass of review or execution.
350
+
351
+ **Submitter-scoped approval.** The service records `submitter_id` as the
352
+ authenticated `scope.actorId` of the receiving service, server-side; payload
353
+ and source metadata never supply it. `listOwnProposals({ cursor?, limit? })`
354
+ lists only this actor's own active submissions with their saved review pages,
355
+ reloaded under current item/handler/target authority, without the reviewer
356
+ inbox. A submission the actor can no longer read is skipped before projection
357
+ and never named by the cursor; a readable one whose reviews are denied by
358
+ ingestion authority is returned as `{ item, reviews: null, unavailable: true }`
359
+ with no reason, so one revoked grant never hides the others. `submitOwnDecision(input)` accepts `approve`, `reject` or `defer` (never
360
+ `correct`) under the `review_own` authority: the host's execution `authorize`
361
+ must return `submitter: true` for that actor, the application policy must opt
362
+ in with `submitterReview: true` (narrower layers may only disable it), and the
363
+ item's recorded submitter must equal the current actor. Every other check is
364
+ the ordinary one, and `applyAction` remains the single once-only effect path
365
+ under the approver's live grants. Because the effect runs as the receipt's
366
+ captured `capturedCeiling.execution.principalId`, `review_own` additionally
367
+ requires that principal to be the submitter itself (`submitter_id ===
368
+ scope.actorId === principalId`), checked under the item lock at decision time
369
+ and again at apply; a receipt captured for a bot or service principal can only
370
+ be approved by a reviewer. The decision is recorded with `mode` (`review` or
371
+ `review_own`) and `selfDecision`, which is true whenever the deciding actor is
372
+ the item's submitter, in either mode (mirrored as `decisionMode` /
373
+ `selfDecision` on feedback events), so a self-confirmation is never mistaken
374
+ for independent review and never contributes to automation eligibility. See
375
+ [REVIEW.md](REVIEW.md) and [execution integration](agents/execution.md).
376
+
251
377
  ## Dispatch route ownership and legacy recovery
252
378
 
253
379
  A receipt retains a versioned, server-owned `jobTarget` snapshot, and every
package/REVIEW.md CHANGED
@@ -1,9 +1,12 @@
1
1
  # Authenticated review surfaces
2
2
 
3
- `@happyvertical/smrt-ingestion/svelte` exports `IntakeInbox`, `IntakeReview` and
4
- `EvidenceViewer`. The browser-safe DTO entry exports their `IntakeReviewHost`
3
+ `@happyvertical/smrt-ingestion/svelte` exports `IntakeInbox`, `IntakeReview`,
4
+ `IntakeWorkspace` and `EvidenceViewer`. The browser-safe DTO entry exports their `IntakeReviewHost`
5
5
  callback contract. Import styling/theme configuration from smrt-ui in the host.
6
6
  Components use smrt-ui primitives and the `ingestion.*` i18n catalog.
7
+ Existing `IntakeInbox` hosts now keep the previous rows visible, with their
8
+ controls disabled, while a refresh or filter change loads; any failed read
9
+ (Load more included) still clears them.
7
10
 
8
11
  The host authenticates every request, resolves tenant/actor/confidential scope
9
12
  from its session, enforces CSRF on mutations, and constructs `IngestionService`.
@@ -103,6 +106,49 @@ Denied refresh clears prior originals, candidates, arguments and results.
103
106
  Missing confidence is unavailable; page/time links exist only for observed
104
107
  locations. Provider errors and unresolved/partial results remain review states.
105
108
 
109
+ ## Submitter-scoped decisions
110
+
111
+ For trusted internal sources the person who submitted an item may confirm or
112
+ decline their own proposal without the reviewer inbox. `listOwnProposals`
113
+ returns only items whose server-recorded `submitter_id` equals the current
114
+ authenticated actor, each with its bounded saved review page reloaded under
115
+ current item, handler and target authority. An item the actor can no longer
116
+ read (the host `authorize` returns false or throws `IntakeAccessDeniedError`)
117
+ is skipped before projection: the page is filled from readable items only, in
118
+ 50-row batches, and `nextCursor` names the last emitted item and is present
119
+ only when a further readable item was actually found. Neither the entries, the
120
+ page size nor the cursor reveal a hidden item. If one call has to skip 1000
121
+ hidden submissions it fails with `Own proposal scan limit` rather than return a
122
+ short page or a cursor that stops inside the hidden run; this error is the only
123
+ signal, and it says only that such a run exists. Any other `authorize` error
124
+ propagates instead of shrinking the list. An item that is still readable but
125
+ whose reviews are denied is returned as `{ item, reviews: null, unavailable:
126
+ true }`: that covers ingestion's own denials and any throw from the host
127
+ `assertTarget` (deleted or revoked targets), whose text is never returned.
128
+ Integrity, validation and database errors from ingestion itself propagate. The
129
+ reviewer `listReviews` surface keeps propagating host target errors unchanged.
130
+ `submitOwnDecision` takes the same `ReviewInput` CAS fields and request ID as
131
+ `submitDecision` and accepts `approve`, `reject` and `defer` only; `correct`
132
+ belongs to the reviewer surface. It runs under the `review_own` execution
133
+ operation: the host's `authorize` must return `submitter: true`, the resolved
134
+ policy must have `submitterReview` enabled, and the recorded submitter must be
135
+ the current actor and also the receipt's captured execution principal (the
136
+ identity the effect runs as), checked under the item lock and again at apply.
137
+ A receipt captured for a bot or service principal is never self-approvable. Policy enablement, handler/operation allowlists, captured
138
+ ceiling, confidential access, retention and the first-valid-decision lock are
139
+ unchanged. The decision is a human decision by that actor, recorded with
140
+ `mode: 'review_own'`; reviewer decisions carry `mode: 'review'`. In both modes
141
+ `selfDecision` is true exactly when the deciding actor is the item's
142
+ submitter, so a reviewer approving their own submission is never independent
143
+ review. A self-decision is not a reviewer judgment: it never feeds
144
+ automation eligibility (machine authorization remains the separate evaluated
145
+ `kind: 'machine'` path) and its feedback events carry the same marker. The
146
+ `unavailable` entry discloses nothing about the denied handler or target.
147
+ `applyAction` then runs once under the approver's live grants exactly as
148
+ for reviewer decisions. Hosts render this surface from their own authenticated
149
+ session; presentation state confers no authority, and a source payload cannot
150
+ nominate a submitter.
151
+
106
152
  ## Assignment and logical splits
107
153
 
108
154
  Assignment is explicitly host-owned, durable and versioned. `assign` receives
@@ -283,6 +329,215 @@ choice and execution success. No learning, feedback storage or rule promotion is
283
329
  implemented by these components; #3676 owns those services. Do not pass feedback
284
330
  or a feedback callback to readers without the relevant host grant.
285
331
 
332
+ ## Reviewer-chosen actions and "handled, no action"
333
+
334
+ A reviewer is not limited to model suggestions. `ItemReviewView.catalog` carries
335
+ the handlers the item's catalog currently offers (`listHandlers`, discovery media
336
+ types and live preview authority; it is presentation, never a grant). The optional
337
+ `startAction` callback forwards `StartActionInput` (`itemId`, `handlerId@version`,
338
+ explicit `args`, `requestId`, optional `reason`) to `startReviewerAction`, which
339
+ requires the current reviewer grant under that handler, a completed or partial
340
+ attempt on the current analysis revision, and an operation handler the item's
341
+ evidence media types offer. Plan handlers go through `previewPlan`. The server
342
+ creates an action keyed `reviewer:<requestId>` with origin `reviewer-selected`
343
+ (`createAction` refuses that key prefix and origin, so process authority cannot
344
+ mint reviewer provenance; re-previewing such an action with `previewProposal`
345
+ needs the reviewer grant and keeps its handler fixed, so only its arguments can
346
+ be corrected),
347
+ runs the handler's normal `validate`/`preview`, publishes a `waiting_review`
348
+ revision and requires the same explicit approval and apply as a generated
349
+ proposal. Replays with the same request return the same review; changed arguments
350
+ or reason conflict. Such actions are never automation-eligible: `authorizeAutomatic`
351
+ refuses them regardless of policy opt-ins. The UI pre-fills arguments only from a
352
+ same-handler suggestion (for example a `needs_review` one) and a single original's
353
+ evidence references, lists the required fields still missing, and submits exactly
354
+ what the reviewer typed. An item whose current attempt failed outright must be
355
+ reprocessed first; an interpretation that completed with `provider_error`,
356
+ `unknown` or no suggestions can be acted on directly. `ReviewAction.origin`
357
+ exposes the provenance so `ActionReview` can label human-started actions.
358
+
359
+ The optional `resolve` callback forwards `ResolveItemInput`
360
+ (`disposition: 'resolve_no_action'`, `requestId`, optional `reason`) to
361
+ `resolveItem`. It needs reviewer authority under any handler the item's captured
362
+ ceiling offers and refuses while an action is `authorized` with a live approval,
363
+ `executing`, `outcome_unknown` or `succeeded`; waiting, deferred, rejected and
364
+ failed actions, and authorized ones whose approval expired, may remain (neither
365
+ an exhausted failed action nor a lapsed approval can be rejected, so they must
366
+ not keep the queue open). Afterwards only reject remains: those actions can no
367
+ longer be approved, corrected, deferred, re-previewed, retried, applied or
368
+ automated. `submitDecision` refuses every decision but `reject`, no new revision
369
+ is published for the item, `applyAction` rechecks the disposition before any
370
+ effect, and `analyze`, `reviseLogicalSplit` and `createAction` refuse the item
371
+ under its lock before any write, so it is never reanalysed, re-split or given a
372
+ new action. `resolveItem` supersedes any queued or running analysis under the
373
+ same lock and `claimAnalysis` refuses a resolved item, so no provider is called
374
+ after a human closed it; the item's `processingState` keeps its last value (it
375
+ describes analysis, not review), and the inbox state `resolved` is authoritative.
376
+ `publishProposal` (process authority) refuses a resolved item and any
377
+ reviewer-selected action. The UI hides Approve, Defer, Apply, every edit control, suggestion
378
+ preview and split controls on a resolved item.
379
+ Resolved items hide the instruction editor and stop reinterpretation polling, even if an older processing projection is still queued. Recording an instruction cannot append a new revision after resolution.
380
+
381
+ Paged lists project the resolved state and disposition time in the same bounded, scope-filtered query, so state filters and row controls agree with a full item reload.
382
+
383
+ The append-only `intake_dispositions` record is terminal: replaying the same
384
+ request returns the same disposition, any other request conflicts, and
385
+ `startReviewerAction` refuses the item. `IntakeItemDTO.disposition` projects only
386
+ `{ state: 'resolved', at }` to any item reader (`listItems` reads it in the same
387
+ statement as the page); the reviewer and reason are returned only by
388
+ `getItemDisposition`, which requires reviewer authority, and the review hosts put
389
+ them on `ItemReviewView.disposition` (omitted when that read is denied, so the
390
+ view shows only "Handled, no action"). `loadReviewSummary` reports inbox state `resolved` unless an action
391
+ actually succeeded. Privacy deletion and retention expiry redact the reason and
392
+ reviewer (`intake_dispositions.data`) and keep only the kind. The disposition is
393
+ distinct from `reject`, which still means the proposal was wrong. Closing an item
394
+ records `reviewer_routing` feedback immediately; a started action records it only
395
+ when the reviewer approves it (see FEEDBACK.md). Capture is an optional projection
396
+ that never vetoes the decision or disposition.
397
+
398
+ `startReviewerAction` applies the handler's discovery media types and the live
399
+ reviewer grant/policy for that handler. It intentionally does not apply the
400
+ proposal catalog's provider-policy filter or `maxHandlers` slice: those bound what
401
+ a model may be offered, and a human choosing a handler sends nothing to a
402
+ provider. The UI picker shows the offered catalog, so a handler hidden from it is
403
+ only reachable by a reviewer who already holds `review` authority for it. The maintained
404
+ browser fixture serves both operations behind CSRF and the same session scope.
405
+
406
+ ## Single-page triage workspace
407
+
408
+ `IntakeWorkspace` (#4215) composes the existing pieces so a reviewer works the
409
+ whole queue on one page: the paged inbox as a list pane, the review view of the
410
+ selected item beside it (the original above its action form), and the
411
+ reviewer-instruction message box of the item pane. Props: `host`, `contextKey`
412
+ (replace on session change, as for the other components), optional initial
413
+ `itemId`, `pollMs`, `rowAside` and `onselect(itemId | undefined)`, which only
414
+ reports the selection (to keep a URL in step) and never navigates. It adds **no
415
+ host callback and no server authority**: every read is the host's `list` and
416
+ `load`, every action still goes through the handler preview and an explicit
417
+ approval and apply under the approver's live grants, and the message steers the
418
+ model only, exactly as in `IntakeReview`.
419
+
420
+ - **Inline original.** In the item pane `EvidenceViewer` shows one part at a
421
+ time (`layout="pager"`), opening on the first part that renders inline (the
422
+ uploaded file rather than its capture record), with previous/next original.
423
+ A PDF with more than one *observed* page (extraction locations or proposed
424
+ page groups; `pages` prop) gets previous/next page, which reloads the frame
425
+ at the same-origin URL plus `#page=n`; only `evidenceUrl`-approved URLs ever
426
+ reach a frame. `IntakeReview` gets the same page navigation over its
427
+ unchanged stacked list of parts. The chosen part and page belong to the item
428
+ view (bindable `chosen`/`page`), so they survive in-place reloads, Prepare
429
+ review, Approve and Apply.
430
+ - **Keyboard.** J/K and Alt+Down/Alt+Up move to the next/previous row (loading
431
+ the next page at the end), ignored while focus is in a text field, select or
432
+ editable region; "Previous item"/"Next item" buttons carry
433
+ `aria-keyshortcuts`. The selected row is `aria-current`, and every selection
434
+ moves focus to the item pane region, named by the row's label. A navigation
435
+ that waited for the next page is dropped if the reviewer chose another item
436
+ meanwhile.
437
+ - **Unsent input.** The pane holds unsent input when it has instruction text,
438
+ an edited suggestion draft or split draft, a chosen start handler or start
439
+ reason, a no-action reason, or an action editor with edited arguments, a
440
+ reason or a feedback comment. It is read from the drafts themselves, so it
441
+ holds while a reload has cleared the view; while the view is denied nothing
442
+ is shown and nothing counts (the drafts stay in memory and return with an
443
+ authorized refresh, as before). A reviewer-initiated move (J/K, Alt+arrows,
444
+ Next/Previous item, a row's Review button) then asks inline, in a group
445
+ labelled by its note: "Stay on this item" takes focus and returns it to the
446
+ field the reviewer last used; "Discard and open the other item" proceeds.
447
+ When an item is closed without action (here or by another session), input a
448
+ closed item cannot take
449
+ (instruction, suggestion and split drafts, a started action, a no-action
450
+ reason) is discarded.
451
+ - **Live state.** A row's status follows the item: "Interpreting…", "Ready to
452
+ review", "Needs review" with missing argument names or the generation
453
+ outcome, "Awaiting approval", "Approved, not yet applied", "Failed" with a
454
+ safe category, "Handled, no action", or the listed inbox state. Rows the
455
+ reviewer has not opened show the listed state only; reasons come from a
456
+ loaded view, never from server exception text. The open item's own polling
457
+ (`ReviewView`, unchanged backoff and three-failure fail-closed) reports each
458
+ displayed view and the workspace patches that row in place
459
+ (`InboxView.patch`, keeping the listed label and preview, and the row's
460
+ assignment unless the patch carries a newer assignment version, so a view
461
+ read before an assignment never rolls the row or the next expected version
462
+ back), so the row follows each newer read of the form and the list is
463
+ **not** re-listed per tick. An item the
464
+ reviewer leaves while its stage is in flight, or before the pane showed the
465
+ result of its last operation (an instruction just recorded), keeps
466
+ following: the workspace re-reads followed items with the same `load` (first
467
+ review page) on the pane's backoff, at most **5** reads per tick, one after
468
+ another, least recently read first (so every followed row is reached), with
469
+ only one tick running at a time. At most **20** items are followed; beyond
470
+ that the longest-followed one (the earliest started, whatever the read
471
+ rotation) stops. An item left while its pane was mid-operation (an Apply not
472
+ yet answered) keeps following while a read still shows an action authorized
473
+ or executing: exactly **3** more reads when it never answers, the fourth
474
+ ending the follow. That debt belongs to the operation, so it survives
475
+ reopening and leaving the item again until a view shows the operation
476
+ answered or the reads run out; leaving mid-operation again starts a full
477
+ debt for the new operation. Each follow has a
478
+ generation: an answer from an earlier follow of the same item (one asked
479
+ before the reviewer reopened or left it again) is dropped, and its failure
480
+ does not count toward the new follow's three. An item stops being followed
481
+ when it settles, its view is denied, three reads in a row throw, its row
482
+ leaves the displayed list (a filter or a refreshed page removed it), the
483
+ list fails closed, or the cap drops it; a denial, the failure stop or the
484
+ cap drops its cached status and restores its entry exactly as its page read
485
+ returned it (never a patched entry). Opening an item drops its cached
486
+ status until the pane shows a view; a denial in the open pane (a failed
487
+ action, the pane's own fail-closed poll, or a first load that failed) does
488
+ the same as a follow denial, clears the announcement, and the item is not
489
+ followed when left. A settled stage that was seen running is announced in a
490
+ polite status region ("badger-po.pdf: Ready to review"), only while its row
491
+ is displayed; a refreshed page or a failed list clears it.
492
+ Other rows update on Refresh, a filter change or when opened; there is no
493
+ streaming. While a refresh or "Load more" is pending, the displayed rows stay
494
+ (controls disabled) and keep taking patches. Every read (a page, a pane
495
+ view, including an unchanged poll, and a follow read) is numbered when its
496
+ request starts, and each row shows the entry of its newest read, whatever
497
+ the source: a page read before a pane or follow read never regresses the
498
+ row, and a page read after them replaces what they showed. The same rule
499
+ governs a follow: a follow read older than the row's newest read changes
500
+ no status, does not stop the follow and announces nothing (a newer read
501
+ settles it), so the announcement never contradicts the row. Cached status reasons follow the same ordering, even when entries have identical
502
+ fingerprints. "Load more" continues the
503
+ filter the rows were listed under, not filter inputs edited since, and does
504
+ not repeat a row already shown. A failed "Load more" fails closed like any
505
+ failed read (the host cannot tell a transient failure from a revoked
506
+ grant): the rows clear and the list shows its error.
507
+ - **Approve and advance.** After an approve, apply or "Handled, no action" whose
508
+ reload left nothing for the reviewer (closed without action, or an action
509
+ succeeded and none is waiting for approval, waiting to be applied, executing,
510
+ unknown or failed; stale, rejected and deferred actions do not hold it), the
511
+ item's row is patched and leaves the list when it no longer matches the
512
+ listed state/assignee filter, and the next row that is not closed or
513
+ completed opens and takes focus, with no navigation and no re-listing. An
514
+ approval that leaves an apply step keeps the item open with Apply in place.
515
+ Unloaded saved-review pages and an active background stage also prevent
516
+ settlement. Advancing skips completed inbox pages until a reviewable row
517
+ appears; a page that makes no progress stops the walk.
518
+ It never advances over unsent input on the same item: the item stays open,
519
+ the input stays, and a note says it is finished but has unsent input; the
520
+ reviewer moves on explicitly, through the confirmation; input typed while
521
+ the next page loads counts too. When no open row is displayed, the advance
522
+ appends pages of finished rows until one has an open row or no page
523
+ remains, at most **5** appends. After each append, the advance (like
524
+ J/K at the end of the list) finds its place again by id: just after the
525
+ item it left, or, if that row has gone, at the first row that followed it
526
+ and is still listed, or at the first new row, so a row that left the list
527
+ meanwhile never shifts it onto the wrong item. Its notes (and "nothing else
528
+ needs review") are written into one persistent polite status region. The queue is never reported empty when
529
+ that is unknown: past the bound, or when an append cannot start (a refresh
530
+ is pending) or is superseded, the item stays open with a note to try Next
531
+ item again, which still works; when an append fails, the list has failed
532
+ closed (no rows, Next disabled), so the item stays open with a note to
533
+ refresh the list to continue. The open item stays listed while the
534
+ reviewer works on it even if its state stops matching the filter; it
535
+ leaves once they move on.
536
+ When nothing is left the pane says so. The per-row "Handled, no action"
537
+ control is hidden in the workspace (`resolveInRow={false}`) so closing always
538
+ goes through the item pane and advances.
539
+
540
+
286
541
  ## Maintained browser proof
287
542
 
288
543
  `pnpm test:components` runs real Svelte component tests; `pnpm test:e2e` runs the
@@ -323,8 +578,13 @@ Import `createIntakeReviewHost` and `IntakeReviewHostOptions` from
323
578
  options)` with a service constructed from the current authenticated session.
324
579
  The returned `IntakeReviewHost` implements inbox state projection across saved
325
580
  review pages, current generation loading, stable `ui:<index>` intention keys,
326
- action/plan preview CAS lookup, decisions, apply, corrections, candidates, split
327
- and feedback. Repeated preparation of an unchanged authorized suggestion retains
581
+ action/plan preview CAS lookup, decisions, apply, corrections, candidates, split,
582
+ feedback, the offered `catalog`, `startAction` and `resolve`. Any catalog
583
+ failure (not configured, bound exceeded, denied) omits `catalog`, so the picker
584
+ disappears while existing reviews stay reviewable; item, review and evidence
585
+ denials still propagate as before; the reference host (`reference/review-host.ts`)
586
+ degrades the same way. Repeated
587
+ preparation of an unchanged authorized suggestion retains
328
588
  its current review instead of creating a new revision. Changed arguments still
329
589
  require explicit re-preview and a fresh human decision.
330
590
  `list({ state?, assigneeId?, cursor?, limit? })` returns one page newest receipt
@@ -518,3 +778,11 @@ choosing the same local file explicitly starts a new upload identity. Opening
518
778
  then dismissing the chooser preserves the separately retained originals and
519
779
  request identity for retry; it does not clear the shared FilePicker's native-form
520
780
  behavior. Successful upload and host/context disposal still discard retry state.
781
+
782
+ Workspace ordering also protects an open item during an awaited automatic page walk:
783
+ the selected row remains until navigation succeeds, except that a failed list read
784
+ clears the inbox. Applying an action is disabled
785
+ while that action card holds unsent arguments, reason, or feedback comment. A list
786
+ refresh retires earlier follow snapshots; only follow reads started after that
787
+ page request may override its rows, so a newer refresh cannot be regressed by an
788
+ already settled or older in-flight follow read.