@happyvertical/smrt-ingestion 0.57.19 → 0.57.21
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/FEEDBACK.md +33 -0
- package/PROPOSALS.md +127 -5
- package/README.md +42 -2
- package/REVIEW.md +339 -3
- package/agents/execution.md +15 -0
- package/dist/chunks/{execution-NoDh3pjs.js → execution-9CfjxX8I.js} +110 -22
- package/dist/chunks/execution-9CfjxX8I.js.map +1 -0
- package/dist/chunks/{proposal-errors-VxcTU6Db.js → extraction-CJ-zrbg0.js} +4 -10
- package/dist/chunks/extraction-CJ-zrbg0.js.map +1 -0
- package/dist/chunks/{feedback-CJB900iG.js → feedback-CuUzWKzP.js} +59 -18
- package/dist/chunks/feedback-CuUzWKzP.js.map +1 -0
- package/dist/chunks/reviewer-instruction-Db3jDN9a.js +33 -0
- package/dist/chunks/reviewer-instruction-Db3jDN9a.js.map +1 -0
- package/dist/dto.d.ts +6 -0
- package/dist/dto.d.ts.map +1 -1
- package/dist/execution-internal.d.ts +2 -0
- package/dist/execution-internal.d.ts.map +1 -1
- package/dist/execution.d.ts +43 -1
- package/dist/execution.d.ts.map +1 -1
- package/dist/execution.js +1 -1
- package/dist/feedback-dto.d.ts +6 -0
- package/dist/feedback-dto.d.ts.map +1 -1
- package/dist/feedback.d.ts +7 -0
- package/dist/feedback.d.ts.map +1 -1
- package/dist/manifest.json +19 -1
- package/dist/models.d.ts.map +1 -1
- package/dist/models.js +10 -1
- package/dist/models.js.map +1 -1
- package/dist/presented-filename.d.ts +11 -0
- package/dist/presented-filename.d.ts.map +1 -0
- package/dist/proposal-contracts.d.ts +31 -6
- package/dist/proposal-contracts.d.ts.map +1 -1
- package/dist/proposal-dto.d.ts +17 -0
- package/dist/proposal-dto.d.ts.map +1 -1
- package/dist/proposal-errors.d.ts +16 -0
- package/dist/proposal-errors.d.ts.map +1 -1
- package/dist/proposal-validation.d.ts +26 -0
- package/dist/proposal-validation.d.ts.map +1 -1
- package/dist/proposals.d.ts +5 -1
- package/dist/proposals.d.ts.map +1 -1
- package/dist/proposals.js +261 -69
- package/dist/proposals.js.map +1 -1
- package/dist/review-dto.d.ts +95 -2
- package/dist/review-dto.d.ts.map +1 -1
- package/dist/review-host.d.ts +52 -3
- package/dist/review-host.d.ts.map +1 -1
- package/dist/review-instruction-view.d.ts +12 -0
- package/dist/review-instruction-view.d.ts.map +1 -0
- package/dist/review-summary.d.ts.map +1 -1
- package/dist/reviewer-instruction.d.ts +20 -0
- package/dist/reviewer-instruction.d.ts.map +1 -0
- package/dist/server.d.ts +171 -4
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +835 -49
- package/dist/server.js.map +1 -1
- package/dist/smrt-knowledge.json +29 -21
- package/dist/sources/common.d.ts +11 -0
- package/dist/sources/common.d.ts.map +1 -1
- package/dist/sources/email.d.ts.map +1 -1
- package/dist/sources/http.d.ts.map +1 -1
- package/dist/sources/watch-folder.d.ts.map +1 -1
- package/dist/svelte/components/ActionReview.svelte +15 -46
- package/dist/svelte/components/ActionReview.svelte.d.ts.map +1 -1
- package/dist/svelte/components/CandidatePicker.svelte +72 -0
- package/dist/svelte/components/CandidatePicker.svelte.d.ts +15 -0
- package/dist/svelte/components/CandidatePicker.svelte.d.ts.map +1 -0
- package/dist/svelte/components/InboxView.svelte +14 -3
- package/dist/svelte/components/InboxView.svelte.d.ts +8 -1
- package/dist/svelte/components/InboxView.svelte.d.ts.map +1 -1
- package/dist/svelte/components/IntakeInbox.svelte +6 -3
- package/dist/svelte/components/IntakeInbox.svelte.d.ts +4 -1
- package/dist/svelte/components/IntakeInbox.svelte.d.ts.map +1 -1
- package/dist/svelte/components/IntakeReview.svelte +4 -2
- package/dist/svelte/components/IntakeReview.svelte.d.ts +2 -0
- package/dist/svelte/components/IntakeReview.svelte.d.ts.map +1 -1
- package/dist/svelte/components/ReviewView.svelte +403 -62
- package/dist/svelte/components/ReviewView.svelte.d.ts +2 -0
- package/dist/svelte/components/ReviewView.svelte.d.ts.map +1 -1
- package/dist/svelte/i18n.d.ts +17 -0
- package/dist/svelte/i18n.d.ts.map +1 -1
- package/dist/svelte/i18n.js +17 -0
- package/dist/svelte/index.d.ts +1 -1
- package/dist/svelte/index.d.ts.map +1 -1
- package/dist/svelte/view-utils.d.ts +2 -0
- package/dist/svelte/view-utils.d.ts.map +1 -1
- package/dist/svelte/view-utils.js +12 -0
- package/package.json +13 -13
- package/dist/chunks/execution-NoDh3pjs.js.map +0 -1
- package/dist/chunks/execution-internal-DWteDHM2.js +0 -7
- package/dist/chunks/execution-internal-DWteDHM2.js.map +0 -1
- package/dist/chunks/feedback-CJB900iG.js.map +0 -1
- package/dist/chunks/proposal-errors-VxcTU6Db.js.map +0 -1
package/FEEDBACK.md
CHANGED
|
@@ -51,6 +51,39 @@ examples. A receiving item's provider grant cannot substitute for a source grant
|
|
|
51
51
|
Superseded, revoked, deleted or incompatible sources are excluded. Current explicit human corrections
|
|
52
52
|
take precedence: a generation carrying one does not retrieve learned examples.
|
|
53
53
|
|
|
54
|
+
A judgment recorded on an action whose generation ran under a reviewer
|
|
55
|
+
instruction (REVIEW.md) stores only a reference, `instruction: {id, digest}`,
|
|
56
|
+
after verifying the generation's provenance against the authenticated
|
|
57
|
+
extraction record. **Write side:** the reference is ~100 bytes, so instruction
|
|
58
|
+
text never counts against the judgment's write budget (`feedback.maxBytes` /
|
|
59
|
+
`policy.maxBytes`) and cannot make `recordFeedback` fail with `Feedback
|
|
60
|
+
limit`. `retrieve` re-verifies the source record and joins the text, returning
|
|
61
|
+
`{id, digest, text}`; a stored reference that no longer matches the source's
|
|
62
|
+
record excludes the example. Because one long instruction joined at read time
|
|
63
|
+
could otherwise push every selection that ranks it over budget and deny
|
|
64
|
+
generation scope-wide, `recordFeedback` also builds that single joined example
|
|
65
|
+
exactly as `retrieve` would and refuses with `Feedback instruction exceeds
|
|
66
|
+
retrieval budget` (nothing is written) when it alone exceeds
|
|
67
|
+
`min(feedback.maxBytes, handler policy.maxBytes)`. An example that fits exactly
|
|
68
|
+
records and retrieves. **This guard covers only the single-example case** on the
|
|
69
|
+
judged handler's own budget: retrieval still sums up to `maxExamples` examples
|
|
70
|
+
and takes the minimum across *every* offered handler of the receiving item, so a
|
|
71
|
+
multi-example selection, or a receiving item with a narrower handler budget, can
|
|
72
|
+
still exceed the budget and fails closed as below. It is returned as part of the example so comparable later items see the guidance
|
|
73
|
+
that produced the judged suggestion. It is example content only: it does not
|
|
74
|
+
transfer to the receiving generation's configuration or prompt section, and it
|
|
75
|
+
expands no catalog, candidate, permission or automation eligibility. **Read-side budget
|
|
76
|
+
impact:** joined instruction text (up to 4,000 characters, more bytes for
|
|
77
|
+
non-ASCII) counts toward the exact serialized selection budget — the narrowest
|
|
78
|
+
of `feedback.maxBytes` and each offered handler's `policy.maxBytes` — like any
|
|
79
|
+
other example field. The existing fail-closed rule is unchanged: an over-budget
|
|
80
|
+
selection rejects with `Feedback limit` and generation records
|
|
81
|
+
`provider_error` without provider I/O; examples are not silently trimmed. Hosts
|
|
82
|
+
that enable reviewer instructions together with feedback should size
|
|
83
|
+
`feedback.maxBytes` (and the policy ceilings) for at least
|
|
84
|
+
`maxExamples × (4,000 characters of instruction + the example's own fields)`,
|
|
85
|
+
or lower `maxExamples`.
|
|
86
|
+
|
|
54
87
|
Full examples exist only in the authorized provider request. The SDK proposal
|
|
55
88
|
adapter includes that typed selection in its serialized user message when
|
|
56
89
|
present; disabled feedback leaves the original request shape unchanged. Saved generation
|
package/PROPOSALS.md
CHANGED
|
@@ -32,7 +32,8 @@ the authorized transaction executor. Query by tenant and confidential scope
|
|
|
32
32
|
`limit + 1`) and report `hasMore`. The service projects candidate identity, model,
|
|
33
33
|
revision and label, then checks every target with the owning execution callback.
|
|
34
34
|
There are no hidden counts or pagination tokens. Truncated discovery forces
|
|
35
|
-
`needs_review
|
|
35
|
+
`needs_review`; a person may still preview it explicitly, and automation never
|
|
36
|
+
becomes eligible.
|
|
36
37
|
|
|
37
38
|
Completed generation reload checks process authority, the current attempt, and
|
|
38
39
|
owner/candidate visibility in the same final transaction. Database-backed grant
|
|
@@ -64,6 +65,12 @@ that declaration and its target authorization semantics.
|
|
|
64
65
|
or pass an explicit attempt ID to additionally reject stale client state.
|
|
65
66
|
Queued/running revisions never return an older completed attempt.
|
|
66
67
|
|
|
68
|
+
`analyze` refuses an interpretation configuration whose pinned source is not
|
|
69
|
+
the item's latest extraction, checked under the item lock before anything is
|
|
70
|
+
superseded (`GenerationSourceStaleError`, nothing appended). A preparation
|
|
71
|
+
superseded by a later extraction therefore fails at append time rather than
|
|
72
|
+
after a revision was created.
|
|
73
|
+
|
|
67
74
|
A generation revision does not rewrite terminal extraction. Reprocessing starts
|
|
68
75
|
with a new extraction revision, followed by interpretation, while the host keeps
|
|
69
76
|
the same business intention keys. An interrupted still-current generation lease
|
|
@@ -99,6 +106,33 @@ for human preview; automatic eligibility always remains false. The service
|
|
|
99
106
|
deadline aborts requests and prevents late publication; it does not claim to terminate computation at a remote
|
|
100
107
|
provider or isolate native allocations. Extractor process limits remain separate.
|
|
101
108
|
|
|
109
|
+
When the pinned extraction carries an authenticated reviewer instruction
|
|
110
|
+
(REVIEW.md), `prepareGeneration` copies it into the interpretation configuration
|
|
111
|
+
and `generateProposals` verifies that copy against the extraction record and its
|
|
112
|
+
own digest before provider disclosure (`integrity` otherwise). The prompt then
|
|
113
|
+
carries it in its own **trusted reviewer guidance** section after the fixed
|
|
114
|
+
system instruction and handler contract: the guidance is named as authenticated
|
|
115
|
+
human input, limited to the offered catalog, and the evidence remains untrusted
|
|
116
|
+
data. `GenerativeProposalInput.reviewerInstruction` exposes `{id, text}` to custom
|
|
117
|
+
generators; `provenance.reviewerInstruction` records `{id, digest}`. The
|
|
118
|
+
instruction never changes `offered`, `catalogDigest`, argument schemas,
|
|
119
|
+
permissions or `automaticActionEligible`; a reply that follows guidance outside
|
|
120
|
+
the catalog fails validation like any other hostile output. **Budget cost:** the instruction counts once against the input budget
|
|
121
|
+
(`maxInputBytes`, each offered handler's `maxBytes` and the live policy
|
|
122
|
+
ceiling), as its JSON-escaped text inside the prompt's guidance section — up
|
|
123
|
+
to 4,000 characters plus escaping and the fixed guidance sentence. The
|
|
124
|
+
`reviewerInstruction` field duplicated for custom generators is not counted
|
|
125
|
+
again. The decision stage also carries it once: its request state holds the
|
|
126
|
+
evidence-side input with the un-guided base instructions under `evidence`, and
|
|
127
|
+
the guidance only under a separate `reviewerGuidance` key, never under
|
|
128
|
+
`evidence`; the decision budget measures that serialized request as sent. Size `maxInputBytes` with that headroom. Learned examples are
|
|
129
|
+
still retrieved for an instructed generation; only a logical split correction
|
|
130
|
+
suppresses them. Every instruction costs a fresh extraction revision before the
|
|
131
|
+
new interpretation: the predecessor-pin contract above (source = immediately
|
|
132
|
+
preceding revision, current-revision reads) is deliberately left intact rather
|
|
133
|
+
than reused for a same-source second interpretation, so OCR/speech hosts budget
|
|
134
|
+
one extraction per instruction, as for split corrections.
|
|
135
|
+
|
|
102
136
|
Model output can contain only offered handler versions, schema-valid arguments,
|
|
103
137
|
retained evidence locations and offered alternative handler/candidate keys.
|
|
104
138
|
Candidate IDs must occur in the offered set, and evidence IDs in retained source
|
|
@@ -108,6 +142,26 @@ as `missingFields`, with `needs_review`; values are never invented to satisfy a
|
|
|
108
142
|
schema. Observed page/time/source granularity is preserved. Logical scan splits
|
|
109
143
|
use `proposeDocumentSplits`; originals and hashes remain intact.
|
|
110
144
|
|
|
145
|
+
A rejected reply ends the attempt as `provider_error` with the `malformed_output`
|
|
146
|
+
warning and a bounded, value-free `provenance.failure` `{category, reason}` (#4207).
|
|
147
|
+
The reason lists schema-owned paths, keywords and schema constants, for example
|
|
148
|
+
`/outcome enum (allowedValues=proposals|unknown|...)` and
|
|
149
|
+
`/suggestions/0 required (missingProperty=evidence)`, or the fixed internal check
|
|
150
|
+
and its path, such as `Unoffered handler: /suggestions/0`. Path segments are
|
|
151
|
+
kept only when they are keys declared under the schema's `properties` or
|
|
152
|
+
indexes into a value that actually is an array in the rejected reply; any other
|
|
153
|
+
segment, numeric or not (for example a key under an open
|
|
154
|
+
`additionalProperties` argument sub-schema, or a numeric key of an object),
|
|
155
|
+
becomes `?`, and an extra property's name is never echoed.
|
|
156
|
+
It never contains document text, argument values, evidence content or
|
|
157
|
+
model-chosen keys, is cut at 1000 characters,
|
|
158
|
+
and is dropped before the output-limit fallback. Hosts read it from the
|
|
159
|
+
completed/partial analysis at `analysis.result.output.proposals.provenance.failure`.
|
|
160
|
+
The SDK adapter constrains replies with a `json_schema` response format built
|
|
161
|
+
from the validator's schema and the fixed instructions describe the shape; a
|
|
162
|
+
custom `ProposalGenerator` receives no schema, so its raw reply meets the
|
|
163
|
+
validator directly and the same reason is recorded.
|
|
164
|
+
|
|
111
165
|
`createSDKProposalDecisionClient(client)` adapts an existing SDK client’s optional
|
|
112
166
|
capability without changing its errors or protocol. Optional `decision` uses public core `executeDecision` and the existing SDK
|
|
113
167
|
predicate, choice, score and batch contracts. Supported-effect predicates,
|
|
@@ -117,15 +171,76 @@ separately from generative provenance and unknown OCR certainty. The complete
|
|
|
117
171
|
serialized decision request, including suggestions and questions, must fit the
|
|
118
172
|
minimum configured/current policy byte ceiling before capability probing and
|
|
119
173
|
again before deciding. A policy change between these boundaries fails closed.
|
|
120
|
-
|
|
121
|
-
`minimumDecisionProbability`, and actually incomplete extraction require review.
|
|
122
|
-
Unknown provider
|
|
174
|
+
Actually incomplete extraction always requires review. Unknown provider
|
|
123
175
|
completion metadata is preserved as `source_completion_unknown`; it does not
|
|
124
176
|
block otherwise schema-valid **human** preview or imply completeness. Automatic
|
|
125
177
|
action eligibility remains false. An unconfigured decision path
|
|
126
178
|
is recorded explicitly. Configured errors produce a durable safe failure outcome;
|
|
127
179
|
they never silently switch to a generative decision fallback.
|
|
128
180
|
|
|
181
|
+
### Decision readiness gate (#4209)
|
|
182
|
+
|
|
183
|
+
For every suggestion `i` the request asks `supported_i` (predicate),
|
|
184
|
+
`route_i` (choice over the offered `handlerId@version` keys plus `none`),
|
|
185
|
+
`risk_i` (ordered score over `clear`, `uncertain`, `ambiguous`) and one
|
|
186
|
+
`entity_i_<field>` choice per candidate-reference argument. The validated
|
|
187
|
+
answers are always persisted under `provenance.decision.answers`. With the
|
|
188
|
+
default `decisionReadiness: 'gate'` they are read as follows, and any failing
|
|
189
|
+
check sets the suggestion to `needs_review` (a tie also sets the outcome to
|
|
190
|
+
`ambiguous`):
|
|
191
|
+
|
|
192
|
+
- `supported.probability >= minimumDecisionProbability` (default `0.8`).
|
|
193
|
+
- `route.choice` equals the suggestion's own `handlerId@handlerVersion`,
|
|
194
|
+
`route.confidence >= minimumDecisionProbability`, and the gap between the two
|
|
195
|
+
largest route probabilities is greater than `tieMargin` (default `0.1`).
|
|
196
|
+
- `risk.score < 1`: only the `clear` level passes; `uncertain` and `ambiguous`
|
|
197
|
+
require review.
|
|
198
|
+
- Each entity match chooses the suggested candidate's key with confidence at or
|
|
199
|
+
above the threshold and a gap above `tieMargin`.
|
|
200
|
+
|
|
201
|
+
These values are provider outputs read as if they were calibrated
|
|
202
|
+
probabilities; nothing in this package calibrates them for this use. The
|
|
203
|
+
teamworks-os measurement in #4209 (self-hosted Laya `typed-decisions`) found no
|
|
204
|
+
separation between right and wrong suggestions: a correctly read vendor invoice
|
|
205
|
+
scored `supported` 0.37, garbage input 0.45–0.50, a wrongly proposed purchase
|
|
206
|
+
order 0.51, route confidence 0.003–0.15 and `risk` ≥ 0.94, so the gate sent
|
|
207
|
+
every suggestion to review. Until a labelled sample shows separation, treat the
|
|
208
|
+
answers as diagnostics and configure:
|
|
209
|
+
|
|
210
|
+
- `decisionMaxLen`: positive integer forwarded unchanged as the per-request
|
|
211
|
+
`maxLen` of the decision client (the SDK Laya provider's token window; the
|
|
212
|
+
checkpoint default is 1024 and the serialized evidence plus suggestions is
|
|
213
|
+
otherwise truncated). Other clients ignore it.
|
|
214
|
+
- `decisionReadiness: 'record'`: the decision still runs, is bounded and
|
|
215
|
+
authorized exactly as before and its answers, usage, `readiness` and `maxLen`
|
|
216
|
+
are recorded, but the gate never changes `outcome` or `disposition`; readiness
|
|
217
|
+
comes from the validator alone (`missingFields`, outcome, source completeness).
|
|
218
|
+
A client error or timeout is recorded as the `decision_unavailable`
|
|
219
|
+
warning. A reply that comes back and then fails the core `executeDecision`
|
|
220
|
+
identity or answer-shape check is recorded as `decision_malformed` with the
|
|
221
|
+
reply's usage; this is the path for injected `DecisionClient`s. SDK clients,
|
|
222
|
+
including the Laya provider behind `createSDKProposalDecisionClient`,
|
|
223
|
+
validate the reply's shape inside `client.decide` and throw, so a malformed
|
|
224
|
+
SDK reply is recorded as `decision_unavailable` with no usage. Every case
|
|
225
|
+
records the `provenance.decision` identity. The generated suggestions stay
|
|
226
|
+
as they were. The serialized request exceeding the byte budget is a
|
|
227
|
+
configuration failure, not a provider answer: it ends the attempt as `limit`
|
|
228
|
+
in both modes, and in `gate` mode every failure above still ends the attempt
|
|
229
|
+
as `provider_error`. Lease, catalog, policy and provider-allowlist revocation
|
|
230
|
+
fail closed in both modes.
|
|
231
|
+
|
|
232
|
+
Successful output also records `provenance.decision.readiness` (`'gate'` or
|
|
233
|
+
`'record'`) and `maxLen` when set; configuration digests are unchanged, but
|
|
234
|
+
persisted output is therefore not byte-identical to earlier releases.
|
|
235
|
+
|
|
236
|
+
Both settings are public configuration: they are part of
|
|
237
|
+
`ProposalReadConfiguration`, the frozen `GenerationStageConfiguration.proposals`
|
|
238
|
+
(present only when set, so existing digests are unchanged) and the completed
|
|
239
|
+
snapshot authority check. Invalid values fail at proposal service construction.
|
|
240
|
+
Neither mode affects automation: `automaticActionEligible` stays `false`, and
|
|
241
|
+
`authorizeAutomatic` reads only the host's `evaluateAutomatic` callback and
|
|
242
|
+
policy, never these answers.
|
|
243
|
+
|
|
129
244
|
Output records zero, one or multiple suggestions, alternatives, missing fields,
|
|
130
245
|
source links, proposed splits, versions, allowlisted usage, warnings and explicit
|
|
131
246
|
`unknown`, `ambiguous`, `no_action`, `needs_review` or `provider_error` outcomes.
|
|
@@ -154,7 +269,14 @@ await service.previewGeneratedProposals({
|
|
|
154
269
|
|
|
155
270
|
`intentionKey` is the host's stable business-intention identity across generation
|
|
156
271
|
revisions. Never derive a fresh key from model output, suggestion order or a retry.
|
|
157
|
-
Selections must reference current persisted validated `ready_for_review`
|
|
272
|
+
Selections must reference current persisted validated `ready_for_review` or
|
|
273
|
+
`needs_review` output (#4208); a `provider_error` attempt, an unoffered handler or
|
|
274
|
+
a non-interpretation stage is rejected. A `needs_review` suggestion whose
|
|
275
|
+
`missingFields` are still absent fails the handler schema inside the ordinary
|
|
276
|
+
preview (`Invalid handler payload`) and publishes nothing; the reviewer supplies
|
|
277
|
+
the missing values through the explicit human path (`previewProposal` /
|
|
278
|
+
`previewPlan`, as the published review host does for `preview({args})`), and the
|
|
279
|
+
handler's `validate` and `preview` run again on the completed arguments.
|
|
158
280
|
Each batch requires distinct suggestion indexes and intention keys; duplicate
|
|
159
281
|
indexes are rejected before any operation or plan preview is written.
|
|
160
282
|
The service rechecks current catalog/schema/target/source provenance, then calls
|
package/README.md
CHANGED
|
@@ -10,7 +10,25 @@ methods through their authenticated application transport.
|
|
|
10
10
|
|
|
11
11
|
Before starting a service, deploy the package manifest through the public s-m-r-t
|
|
12
12
|
migration APIs or `smrt db:migrate`. `assertReady()` only checks existing tables.
|
|
13
|
-
SQLite and PostgreSQL are supported; other engines fail construction.
|
|
13
|
+
SQLite and PostgreSQL are supported; other engines fail construction. Index
|
|
14
|
+
additions ship the same way: `intake_items_received_idx` (newest-first inbox
|
|
15
|
+
paging on `tenant_id, confidential_scope_id, created_at, id`) is declared on the model and reaches existing deployments through the
|
|
16
|
+
next manifest migration; there are no hand-written migration files in this
|
|
17
|
+
package, and `smrt doctor --db` / `db:status --parity` report the index until
|
|
18
|
+
it is applied.
|
|
19
|
+
|
|
20
|
+
`listItemsPage({ limit?, cursor?, state?, itemIds?, scanLimit? })` returns
|
|
21
|
+
`{ items: [{ item, state, cursor }], scanned, nextCursor? }`: one authorized
|
|
22
|
+
inbox page newest receipt first with each row's durable inbox state and
|
|
23
|
+
continuation cursor, and the number of rows the call examined. `listItems()`
|
|
24
|
+
remains as a deprecated array-returning method that walks the private page
|
|
25
|
+
scan to the end. Without the optional `cursorKey` service option every cursor
|
|
26
|
+
names the last row the actor can read, and a page that finds no readable row
|
|
27
|
+
within its scan limit fails with `IntakeScanLimitError`; with a server secret
|
|
28
|
+
of at least 32 bytes, cursors are sealed and such a page resumes past what it
|
|
29
|
+
skipped. `scanned` is for host
|
|
30
|
+
accounting only. See [REVIEW.md](REVIEW.md) for the paging and authorization
|
|
31
|
+
contract.
|
|
14
32
|
|
|
15
33
|
A host supplies a database, AssetRuntime, authenticated tenant/actor/confidential
|
|
16
34
|
scope, a live authorization callback, a registered jobs target, and an idempotent
|
|
@@ -20,7 +38,29 @@ transaction executor; it must intersect current grants with that ceiling. Never
|
|
|
20
38
|
construct the scope, retention policy or budgets from incoming document labels.
|
|
21
39
|
|
|
22
40
|
`receive` accepts bounded original byte buffers, with stable source part IDs,
|
|
23
|
-
parent relationships
|
|
41
|
+
parent relationships, optional upstream identity/version and an optional
|
|
42
|
+
original `filename` (kept for labels on `IntakeEvidenceDTO.filename`, never
|
|
43
|
+
part of receipt identity). The multipart upload source, email attachments and
|
|
44
|
+
the watched folder all supply it, as may a host `snapshotReference` part or a
|
|
45
|
+
vendor webhook's verified parts. Every source delivers through one place that
|
|
46
|
+
removes control, format (bidi, zero-width, tag), line-separator and
|
|
47
|
+
default-ignorable characters (Unicode properties, see
|
|
48
|
+
`src/presented-filename.ts`) and drops a name that is then empty or over 512
|
|
49
|
+
characters, so an odd name falls back to the default label instead of failing
|
|
50
|
+
the receipt (`receive` itself rejects such a name from a direct caller as
|
|
51
|
+
`invalid`, and names are cleaned again when read back). Removing every format
|
|
52
|
+
and default-ignorable character has display-only effects, since nothing uses
|
|
53
|
+
the name as identity: emoji ZWJ sequences and presentation selectors flatten,
|
|
54
|
+
ZWNJ/ZWJ are removed from Persian and Indic text (letters may join
|
|
55
|
+
differently), and ideographic variation selectors are removed (a CJK variant
|
|
56
|
+
shows its base glyph). Unicode normalization
|
|
57
|
+
(NFC/NFKC) and confusable look-alike characters are out of scope: the name is
|
|
58
|
+
still untrusted text, so render it as text, never as markup or a path. The
|
|
59
|
+
upstream `sourceReference` is stored but not exposed on the DTO: its ids are
|
|
60
|
+
upstream locators, not display text, and they are never cleaned. For email
|
|
61
|
+
they are the provider's message locator (plus the attachment part id); for the
|
|
62
|
+
watched folder the id is the raw folder entry name, whose cleaned form is what
|
|
63
|
+
`filename` exposes. Source adapters own
|
|
24
64
|
streaming and upstream snapshot retrieval. Receipt identity is tenant + source +
|
|
25
65
|
delivery key; changing bytes or authenticated scope conflicts. Content equality
|
|
26
66
|
in a different conversation does not deduplicate an arrival. The returned item
|
package/REVIEW.md
CHANGED
|
@@ -32,6 +32,41 @@ Plan membership contains the exact parent args/version so edits call
|
|
|
32
32
|
`previewPlan`, not per-step correction. An item may have more review pages; the
|
|
33
33
|
host must aggregate state across pages when projecting a production inbox state.
|
|
34
34
|
|
|
35
|
+
A `needs_review` suggestion is completable, not only rejectable (#4208). The
|
|
36
|
+
component shows its `missingFields`, an editable `Suggested arguments (JSON)`
|
|
37
|
+
draft prefilled from the generated arguments, and the candidate picker for
|
|
38
|
+
declared candidate fields; Prepare review stays disabled while required fields
|
|
39
|
+
are still missing from an unchanged draft. An unchanged draft previews the
|
|
40
|
+
generated snapshot as before; an edited draft is sent as `preview({..., args})`,
|
|
41
|
+
which the published host publishes through the ordinary `previewProposal` /
|
|
42
|
+
`previewPlan` with the same `ui:<index>` intention, never by rewriting the
|
|
43
|
+
generation. The handler validates the human arguments again before a
|
|
44
|
+
`waiting_review` action exists, approval remains a separate explicit decision,
|
|
45
|
+
`ActionReview` lists required arguments missing from the current draft while
|
|
46
|
+
editing, and no authority or automation eligibility is added: a failed attempt,
|
|
47
|
+
an unoffered handler or a foreign stage is still refused by the host.
|
|
48
|
+
`preview` throws on every failure, as before. When the handler's argument
|
|
49
|
+
schema or `validate` refused the arguments being previewed (operation preview,
|
|
50
|
+
which also carries corrections, or plan preview), the server throws
|
|
51
|
+
`HandlerArgumentsInvalidError` (`name` set, messages unchanged). Apply-time and
|
|
52
|
+
external-send revalidation and result-schema checks keep a plain `Error`. The component shows an inline message and
|
|
53
|
+
keeps the view and draft when `preview` either rejects with an error whose
|
|
54
|
+
`name` is `HandlerArgumentsInvalidError` or resolves `{ rejected: 'invalid' }`
|
|
55
|
+
(`PreviewRejection`). A transport that crosses a process boundary preserves
|
|
56
|
+
the inline UX by keeping that name (for example re-throwing an `Error` with
|
|
57
|
+
`name = 'HandlerArgumentsInvalidError'` in the browser callback) or by
|
|
58
|
+
returning `{ rejected: 'invalid' }` for that case only, as the maintained
|
|
59
|
+
fixture's `previewForReview` does. Without either, the refusal behaves like
|
|
60
|
+
any other failure: the view is cleared exactly as before this change, and the
|
|
61
|
+
reviewer's in-memory draft returns after an authorized refresh. Every other
|
|
62
|
+
thrown preview or candidate-search failure (denied, revoked, unavailable or
|
|
63
|
+
unknown) clears the view.
|
|
64
|
+
Unsent drafts live only in component memory, keyed by attempt and suggestion
|
|
65
|
+
index. They survive Refresh, a failed callback, an action on another card and a
|
|
66
|
+
transient `unavailable`/`stale` reload; a `revoked` or `expired` result
|
|
67
|
+
discards them, a successful preview clears that card's draft, and an available
|
|
68
|
+
reload drops drafts for attempts or cards no longer offered.
|
|
69
|
+
|
|
35
70
|
The host's `load` callback supplies a current `ItemReviewView`, using
|
|
36
71
|
`getCompletedAnalysis` with no historical fallback. Generated suggestions bind
|
|
37
72
|
the **current generation attempt**, not `generation.source.attemptId` (the prior
|
|
@@ -98,6 +133,151 @@ with `human_split_not_respected`. New proposals require new explicit review.
|
|
|
98
133
|
The reference host runs these stages synchronously for browser determinism;
|
|
99
134
|
production hosts dispatch the existing durable analysis jobs.
|
|
100
135
|
|
|
136
|
+
## Reviewer instructions
|
|
137
|
+
|
|
138
|
+
`recordReviewerInstruction({itemId, attemptId, expectedRevision, text,
|
|
139
|
+
requestId})` records a plain-text instruction for one item ("This is a customer
|
|
140
|
+
PO from Badger; file it, do not bill it") as the second kind of **human
|
|
141
|
+
correction**. It is authorized exactly like `reviseLogicalSplit`: the current
|
|
142
|
+
generation must pass the same-executor catalog/candidate gate and the reviewer
|
|
143
|
+
grant plus policy reviewer list, and authority is checked before mutation and
|
|
144
|
+
again before publication. Process access, `analyze`, and a client payload cannot
|
|
145
|
+
mint one: `analyze` rejects a `reviewerInstruction` key on an extraction
|
|
146
|
+
configuration, and an interpretation copy that differs from the pinned
|
|
147
|
+
extraction's record fails with `Generation evidence changed` before provider
|
|
148
|
+
disclosure. Text is required, trimmed-nonempty and at most 4,000 characters.
|
|
149
|
+
|
|
150
|
+
The record is immutable: `{id, text, actorId, recordedAt, supersedesId, digest}`
|
|
151
|
+
plus the request binding, written into a new extraction configuration beside
|
|
152
|
+
any existing split correction. Recording supersedes current suggestions and
|
|
153
|
+
approvals, queues the normal fenced re-extraction and interpretation through the
|
|
154
|
+
existing dispatch path, and returns immediately; the reviewer keeps working
|
|
155
|
+
while the item reports `processing`. `prepareGeneration` copies the instruction
|
|
156
|
+
into the interpretation configuration, the generator receives it only after its
|
|
157
|
+
digest matches the authoritative extraction record, and saved generation
|
|
158
|
+
provenance records `reviewerInstruction: {id, digest}`. A later instruction
|
|
159
|
+
records `supersedesId` and replaces the earlier one for future interpretations;
|
|
160
|
+
`listReviewerInstructions(itemId)` returns the full history, newest first, and
|
|
161
|
+
history is never deleted. The instruction steers the model only: it cannot add
|
|
162
|
+
handlers, candidates, schemas, permissions or automation eligibility, replies
|
|
163
|
+
are validated as usual, and suggestions still require explicit preview and
|
|
164
|
+
approval. A replay with the same request ID returns the same revision; a changed
|
|
165
|
+
text under the same request ID conflicts.
|
|
166
|
+
|
|
167
|
+
**Cost.** Each recorded instruction appends a new extraction revision, so the
|
|
168
|
+
item is extracted again before it is interpreted again: cheap for embedded-text
|
|
169
|
+
PDFs and plain text, but OCR/speech hosts spend provider budget per instruction,
|
|
170
|
+
exactly as they do per split correction. The pinned extraction is not reused
|
|
171
|
+
because the interpretation contract requires its source to be the immediately
|
|
172
|
+
preceding revision and `getCompletedAnalysis(itemId, attemptId)` serves only
|
|
173
|
+
the current revision (PROPOSALS.md, "Durable stages"); reusing it would mean
|
|
174
|
+
weakening that predecessor pin, which protects every generation from an
|
|
175
|
+
intervening reprocess, so this feature follows the split path instead.
|
|
176
|
+
|
|
177
|
+
**State and failure.** An in-flight stage (queued/running) reports the item as
|
|
178
|
+
`processing` even when earlier, now-stale actions exist. If the re-extraction or
|
|
179
|
+
interpretation fails, `getReinterpretationState(itemId)` returns the latest
|
|
180
|
+
attempt's safe failure category and the latest completed generation attempt;
|
|
181
|
+
the host projects them as `instruction.failure` and `instruction.binding`, and
|
|
182
|
+
`recordReviewerInstruction` accepts that historical attempt as its anchor when
|
|
183
|
+
`expectedRevision` is the item's current (failed) revision and no later
|
|
184
|
+
**interpretation** completed and nothing is queued or running, so the reviewer
|
|
185
|
+
can retry or change the instruction after either stage fails. A completed
|
|
186
|
+
extraction whose own interpretation failed belongs to the failed attempt, not a
|
|
187
|
+
newer result; a completed extraction whose interpretation has **not been
|
|
188
|
+
appended yet** is in flight (the host is between stages) and is refused with
|
|
189
|
+
`ReviewerInstructionStaleError` (`Stale reviewer instruction`), so a second
|
|
190
|
+
reviewer cannot pre-empt it. That gap expires: once the extraction attempt's
|
|
191
|
+
completion plus the item's analysis lease duration (`limits.leaseMs`, the lease
|
|
192
|
+
`claimAnalysis` grants) has passed with no interpretation appended,
|
|
193
|
+
`getReinterpretationState` reports `failure: 'not_started'` with the retry
|
|
194
|
+
binding and the guard allows the retry. `not_started` means **the
|
|
195
|
+
interpretation did not start within the lease window**: the host crashed,
|
|
196
|
+
threw between `prepareGeneration` and `analyze`, or its queue is merely slow.
|
|
197
|
+
Pre-empting a slow but healthy host is safe because `analyze` refuses an
|
|
198
|
+
interpretation whose pinned extraction is no longer the item's latest, under
|
|
199
|
+
the item lock and before anything is superseded
|
|
200
|
+
(`GenerationSourceStaleError`, nothing appended): the slow host's late append
|
|
201
|
+
cannot clobber the retry. Missing or malformed timing (no attempt, no
|
|
202
|
+
`completedAt`, non-finite `leaseMs`) counts as already expired rather than
|
|
203
|
+
holding retries forever. The "still being re-read" note shown after a stale
|
|
204
|
+
result is cleared by the next reload that shows the item settled; the draft
|
|
205
|
+
then carries a persistent "This item changed while you were writing; your
|
|
206
|
+
instruction was not recorded." note (any stale cause: another reinterpretation,
|
|
207
|
+
a split, or a host reprocess) until the reviewer edits or resubmits it. The guard and
|
|
208
|
+
`getReinterpretationState` share one predicate, and the latter's
|
|
209
|
+
`reinterpreting` flag (projected as `instruction.reinterpreting`) is true in
|
|
210
|
+
the gap too, so the component keeps polling and keeps Re-read disabled there.
|
|
211
|
+
The published host maps two typed errors to `instruct` results:
|
|
212
|
+
`ReviewerInstructionStaleError` → `{ stale: true }` (refresh-and-retry: reload
|
|
213
|
+
with a note, draft kept) and `ReviewerInstructionInvalidError` → `{ invalid:
|
|
214
|
+
true }` (empty, whitespace-only or over 4,000 characters: shown inline, draft
|
|
215
|
+
kept). Neither is a denial. Custom hosts should map them the same way; a host
|
|
216
|
+
that lets either propagate as a thrown error makes the component treat it like
|
|
217
|
+
any other failed mutation, i.e. a whole-view deny with the "view unavailable"
|
|
218
|
+
message, as before these results existed. The maintained fixture's
|
|
219
|
+
`/api/instruct` now answers an invalid text with HTTP 200 `{ invalid: true }`
|
|
220
|
+
instead of 409. A retry from an older anchor builds on the **whole configuration**
|
|
221
|
+
of the newest extraction after it (replacing only the instruction), because that
|
|
222
|
+
is the item's current configuration: corrections chain forward through it and a
|
|
223
|
+
host reprocess replaces it. So a split followed by a failed stage is never
|
|
224
|
+
silently dropped, and a failed reprocess's changed configuration is neither
|
|
225
|
+
reverted nor overlaid with a split it dropped. **Concurrency.** `recordReviewerInstruction` and `reviseLogicalSplit` each
|
|
226
|
+
run their whole decision in one transaction under the item row lock: the
|
|
227
|
+
anchor snapshot read takes the lock first, the execution authorization of
|
|
228
|
+
that snapshot takes it again (`UPDATE intake_items` row lock), the guard step
|
|
229
|
+
re-asserts it explicitly, and
|
|
230
|
+
the request-key replay check, the revision and later-work guards, the
|
|
231
|
+
carried-forward split, the history read that sets `supersedesId`, and the
|
|
232
|
+
append all run under it. On PostgreSQL a second reviewer at the same revision
|
|
233
|
+
blocks on that lock and is then refused (`ReviewerInstructionStaleError` /
|
|
234
|
+
`Stale logical split`), so the ledger cannot fork; an instruction racing a
|
|
235
|
+
split (in either order) is refused rather than built from the pre-split
|
|
236
|
+
configuration; and a double-click whose duplicate commits after the unlocked
|
|
237
|
+
replay check is answered as a replay of the committed request, not a conflict
|
|
238
|
+
or a denial.
|
|
239
|
+
|
|
240
|
+
**Host reprocess.** A host reprocess (a plain `analyze()` extraction the host
|
|
241
|
+
configures itself) cannot carry human corrections — `analyze` rejects
|
|
242
|
+
`humanCorrection`/`reviewerInstruction` on extraction configurations — so it
|
|
243
|
+
drops the split and the instruction alike, and neither is applied to its
|
|
244
|
+
interpretations. The instruction is then reported as `dropped`, not pending:
|
|
245
|
+
`getReinterpretationState().instructionInEffect` names the instruction the
|
|
246
|
+
newest extraction carries (verified by digest) and is null after such a
|
|
247
|
+
reprocess. History is unchanged; the reviewer can re-record the instruction,
|
|
248
|
+
which supersedes the dropped one.
|
|
249
|
+
|
|
250
|
+
`ItemReviewView.instruction` exposes `current` (the latest recorded instruction
|
|
251
|
+
still in effect, i.e. carried by the newest extraction), optional `dropped`
|
|
252
|
+
(the latest recorded instruction a reprocess dropped), `applied` (the
|
|
253
|
+
instruction the displayed **generation** ran under; a completed extraction alone
|
|
254
|
+
applies nothing), `stale` (the displayed suggestions predate the current
|
|
255
|
+
instruction), optional `failure` and optional `binding`. Feedback examples that carry instruction
|
|
256
|
+
text count it toward the feedback byte budget (FEEDBACK.md). The optional host callback
|
|
257
|
+
`IntakeReviewHost.instruct` is implemented by the published adapter when the
|
|
258
|
+
service supports it, with an optional `onInstruct(itemId, revision)` wakeup
|
|
259
|
+
mirroring `onSplit`. The review component renders "Instructions for the
|
|
260
|
+
assistant" with "Re-read with these instructions", shows "Reinterpreting…" while
|
|
261
|
+
the item's stage is queued/running, repeats the existing authenticated `load`
|
|
262
|
+
on `pollMs` (default 2 s, backing off to 5×) and patches the view in place —
|
|
263
|
+
drafts, split edits, focus and the evidence viewer survive and no loading
|
|
264
|
+
placeholder is shown — stops when the stage is no longer in flight, and then
|
|
265
|
+
shows the instruction that produced the current suggestions, marking a
|
|
266
|
+
superseded one as stale. An explicit host denial on a poll tick fails closed at
|
|
267
|
+
once; a thrown tick is treated as transient (view and drafts kept, a
|
|
268
|
+
"Couldn't refresh; retrying." note, backoff continues), but because hosts may
|
|
269
|
+
also throw for revoked access, three consecutive thrown ticks fail closed.
|
|
270
|
+
**Hosts must signal revocation by returning `availability: 'revoked'`** (or
|
|
271
|
+
another non-`available` state) from `load`, not by throwing: a returned denial
|
|
272
|
+
clears the view on that tick, while a host that throws on revocation leaves the
|
|
273
|
+
already-displayed content visible until the third thrown tick (about 10 s at the
|
|
274
|
+
default interval and backoff) before the view fails closed.
|
|
275
|
+
Split drafts survive a refresh only when the reviewer edited them. A failed stage shows its safe category with the form
|
|
276
|
+
still available. There is no
|
|
277
|
+
streaming channel; status is read through the existing item/analysis APIs. The
|
|
278
|
+
maintained fixture records the instruction durably and then runs the stages in
|
|
279
|
+
the background so the browser proof exercises the in-place update.
|
|
280
|
+
|
|
101
281
|
`feedback` is an optional explicit correctness callback, separate from approval
|
|
102
282
|
choice and execution success. No learning, feedback storage or rule promotion is
|
|
103
283
|
implemented by these components; #3676 owns those services. Do not pass feedback
|
|
@@ -147,14 +327,170 @@ action/plan preview CAS lookup, decisions, apply, corrections, candidates, split
|
|
|
147
327
|
and feedback. Repeated preparation of an unchanged authorized suggestion retains
|
|
148
328
|
its current review instead of creating a new revision. Changed arguments still
|
|
149
329
|
require explicit re-preview and a fresh human decision.
|
|
150
|
-
|
|
151
|
-
|
|
330
|
+
`list({ state?, assigneeId?, cursor?, limit? })` returns one page newest receipt
|
|
331
|
+
first (`intake_items.created_at`, id tiebreak) through `service.listItemsPage`.
|
|
332
|
+
`limit` defaults to 20 and is capped at 100; the cursor is an opaque
|
|
333
|
+
(received-at, id) continuation, never a bare id. Tenant, confidential scope,
|
|
334
|
+
retention and the `state` filter run inside the page query, so a page is full
|
|
335
|
+
and `nextCursor` is present only when more rows follow.
|
|
336
|
+
|
|
337
|
+
Authority is rechecked at the same two levels `load` applies, but per page
|
|
338
|
+
rather than per row: the host authorize callback once per distinct captured
|
|
339
|
+
ceiling in a batch, and the execution read gate (policy, ceiling, handler and
|
|
340
|
+
operation allowlists, principal) once per distinct (item, handler) bound by
|
|
341
|
+
the page's saved actions. The list gate is read-only: it evaluates the same
|
|
342
|
+
checks as `listReviews` without the retention write, so an item that the item
|
|
343
|
+
view would narrow past its deadline is simply not listed. The list gate also
|
|
344
|
+
resolves each bound handler and rejects version drift against the saved
|
|
345
|
+
binding and omits a cancelled item, as `listReviews` does; it does not
|
|
346
|
+
evaluate per-action target,
|
|
347
|
+
evidence or plan gates, so an item whose target became unreadable still lists
|
|
348
|
+
and its item view refuses. An item whose bound
|
|
349
|
+
handler the actor cannot currently read is not listed at all, rather than
|
|
350
|
+
listed with a state derived from actions the actor may not see; actions
|
|
351
|
+
without a proposal bind no handler and are not gated, as in `listReviews`.
|
|
352
|
+
Handler bindings (handler id and version, projected in SQL, distinct per
|
|
353
|
+
item) are read once per fetched batch for the rows that passed the item-level
|
|
354
|
+
gate. Note the host cost plainly: the execution `authorize` callback runs once
|
|
355
|
+
per distinct (item, handler) bound by a scanned row that passed the item-level
|
|
356
|
+
gate. Each item is its own authorization subject, so nothing is deduplicated
|
|
357
|
+
across items; the count is bounded by rows scanned (at most
|
|
358
|
+
`ITEM_SCAN_CEILING`) and runs in host code rather than as N+1 database reads. Either host callback, the item-level `authorize` or the execution
|
|
359
|
+
`authorize` behind the handler gate, follows the same rule: a refusal (false,
|
|
360
|
+
or an access check that fails) hides the rows it covers, and a callback that
|
|
361
|
+
throws rejects the whole page (and `listItems()`) with its error, as `load`
|
|
362
|
+
does, so a transient failure never lets paging move past readable rows.
|
|
363
|
+
Denied or unreadable rows are skipped without
|
|
364
|
+
disclosure and the page refills from the next rows; refills grow from the page
|
|
365
|
+
size to 100 rows per query, so a fully denied scope costs about a dozen page
|
|
366
|
+
reads and as many binding reads per call. One service call examines at most
|
|
367
|
+
`ITEM_SCAN_CEILING` (1000) rows, or the caller's smaller `scanLimit`, and
|
|
368
|
+
reports the count as `scanned`; when a mostly-denied scope reaches the ceiling
|
|
369
|
+
the page comes back short with a `nextCursor`. Rows are authorized first and
|
|
370
|
+
the `state` filter is then applied to the fetched batch, so it is bounded by
|
|
371
|
+
the same scan limit: a rare state yields short pages with a cursor rather than
|
|
372
|
+
an unbounded walk inside one query. A row the actor can read whose state does
|
|
373
|
+
not match discloses nothing and may anchor the cursor, so a filter with no
|
|
374
|
+
match among the next 1000 rows returns an empty page with a cursor and still
|
|
375
|
+
pages to the end. The host `list` passes its remaining budget as `scanLimit`, so the
|
|
376
|
+
rows scanned across the service calls of one host call never exceed 1000: a
|
|
377
|
+
short service page with a cursor, or an exhausted budget while filtering by
|
|
378
|
+
assignee, ends the host call with the same cursor and the caller continues.
|
|
379
|
+
Only rows the service wrote are supported: cursors carry the canonical
|
|
380
|
+
millisecond ISO receipt time the service stores. A row whose stored receipt
|
|
381
|
+
time is not that form (on SQLite anything but `YYYY-MM-DDTHH:MM:SS.mmmZ`,
|
|
382
|
+
including a parseable value without milliseconds or with an offset; on
|
|
383
|
+
PostgreSQL a value with a sub-millisecond part) is never listed, whatever the
|
|
384
|
+
actor's access, and counts toward the scan, as does a row whose id is not a
|
|
385
|
+
UUID. On SQLite a non-text stored value (an integer, say) and a receipt value
|
|
386
|
+
or id longer than 64 characters or containing a NUL character are excluded by
|
|
387
|
+
the page query itself: the first sorts after every text value so the scan
|
|
388
|
+
could never move past it, the second could not be carried by any cursor, and
|
|
389
|
+
the third is returned truncated at the NUL, so it would look canonical and
|
|
390
|
+
sort above a legitimate row at the truncated value. Within a call the scan moves
|
|
391
|
+
past a service-foreign row by its exact stored value, and an unsealed cursor
|
|
392
|
+
names the last row before it that the actor can read. A call that finds no readable row within its scan limit (at
|
|
393
|
+
most 1000) fails closed with `IntakeScanLimitError` unless a `cursorKey` is
|
|
394
|
+
configured, as below. The host
|
|
395
|
+
`list` returns what earlier service calls collected, with their cursor, when
|
|
396
|
+
a later call fails that way.
|
|
397
|
+
|
|
398
|
+
`listItems()` is deprecated and kept only for callers of the array-returning
|
|
399
|
+
API. It walks every page, so its cost grows with the scope, including one
|
|
400
|
+
execution authorization per (item, handler) across the whole scope. It follows
|
|
401
|
+
the internal scan position, which never leaves the service, so runs of rows
|
|
402
|
+
the actor cannot read never stop it and it returns `[]` where nothing is
|
|
403
|
+
readable. Items are newest-first and gated exactly as on a page.
|
|
404
|
+
|
|
405
|
+
The page's inbox state is the durable projection of the saved action journal
|
|
406
|
+
and processing state, computed once per scanned row inside the page query;
|
|
407
|
+
`load` remains the authoritative, live-gated projection for one item (it still
|
|
408
|
+
projects individual stale actions, which the list does not). Cost is
|
|
409
|
+
proportional to the page for unfiltered pages and common states, and to the
|
|
410
|
+
number of items scanned until enough matches are found for rare states: the
|
|
411
|
+
state is not persisted, because the action journal is append-only and
|
|
412
|
+
projecting it into an indexed column would need a maintained denormalisation
|
|
413
|
+
in the execution service plus a migration, which this change does not take.
|
|
414
|
+
Listing does not load original bytes.
|
|
415
|
+
|
|
416
|
+
Cursors never disclose an unreadable row. Without a `cursorKey` in the
|
|
417
|
+
service options, every cursor (each row's and `nextCursor`) names a row the
|
|
418
|
+
actor can read (the last one when a page stops early); a page that spends its scan limit before filling hands back
|
|
419
|
+
the last such row, and one that found none fails closed with
|
|
420
|
+
`IntakeScanLimitError` (`Intake scan limit`, no id or time). A run of rows the
|
|
421
|
+
actor cannot read (refused by the captured ceiling or the execution gate, or
|
|
422
|
+
stored with a non-canonical receipt time) at least as long as the scan limit
|
|
423
|
+
therefore stops an unkeyed walk there; readable rows that the `state` filter
|
|
424
|
+
excludes never count toward such a run. With a `cursorKey` (a server secret of at least 32 bytes), every
|
|
425
|
+
cursor is sealed with AES-256-GCM, bound to the tenant, confidential scope and
|
|
426
|
+
actor, and a budget-stopped page returns a sealed position past what it
|
|
427
|
+
skipped, so the walk always progresses. Sealed cursors have a fixed length
|
|
428
|
+
(the plaintext is padded to 1024 bytes) and a fresh nonce per seal, so neither
|
|
429
|
+
their length nor their bytes reveal anything about the stop row. `scanned` is for trusted host
|
|
430
|
+
accounting and must not be sent to clients; a short page with a cursor tells
|
|
431
|
+
a caller only that unlisted rows exist, never which.
|
|
432
|
+
|
|
433
|
+
Newest-first paging relies on `intake_items_received_idx`
|
|
434
|
+
(`tenant_id, confidential_scope_id, created_at, id`), the full ordering key, so
|
|
435
|
+
an unfiltered page is a backward index scan that stops at the page boundary;
|
|
436
|
+
the foundation suite asserts that plan on both engines (SQLite
|
|
437
|
+
`EXPLAIN QUERY PLAN`, PostgreSQL `EXPLAIN`), with the per-row state counters
|
|
438
|
+
served by `intake_actions_item_idx`. On SQLite the page query pins the index with `INDEXED BY` (except under an
|
|
439
|
+
id restriction, where direct id lookups are cheaper), because without planner
|
|
440
|
+
statistics SQLite otherwise picks `intake_items_expiry_idx` plus a sort; the
|
|
441
|
+
SQLite plan check runs without `ANALYZE`. A deployment must have applied the
|
|
442
|
+
manifest migration, or the page query fails with a missing-index error. This package ships no
|
|
443
|
+
migration files: indexes are declared on the model, the build emits them into
|
|
444
|
+
`dist/manifest.json`, and `smrt db:migrate` / the public migration API create
|
|
445
|
+
them, which is how `intake_items_receipt_idx` shipped. Deploy the manifest
|
|
446
|
+
migration before upgrading hosts; `db:status --parity` reports the index as
|
|
447
|
+
missing until then.
|
|
152
448
|
|
|
153
449
|
Supply four application adapters: `upload(files, requestId)` (authenticated
|
|
154
450
|
receipt plus first extraction request), `assignment(itemId)` (durable read),
|
|
155
451
|
`assign(input)` (manager authorization, eligible assignees, durable CAS and request
|
|
156
452
|
replay protection), and `evidence(part)` (safe label/text or authenticated
|
|
157
|
-
same-origin URL)
|
|
453
|
+
same-origin URL), which `load` calls for every part and `list` never calls;
|
|
454
|
+
`load` sets no row preview on its entry.
|
|
455
|
+
|
|
456
|
+
Row previews are opt-in through an optional fifth adapter,
|
|
457
|
+
`rowEvidence(rows)`. When it is supplied, `list` reads the page's durable
|
|
458
|
+
evidence metadata with one `service.listEvidence(itemIds)` query (at most 100
|
|
459
|
+
ids; items the scope cannot read, and expired or inactive items, are omitted
|
|
460
|
+
without disclosure; the host `authorize` runs once per distinct captured
|
|
461
|
+
ceiling through the same gate as `listItemsPage`, so a refusal or a
|
|
462
|
+
malformed captured ceiling hides the item and a failing `authorize` rejects
|
|
463
|
+
the page; an item whose stored data does not parse is skipped by both
|
|
464
|
+
`listItemsPage` and `listEvidence`, never fatal). It then calls `rowEvidence`
|
|
465
|
+
once with each row's primary part (`{ itemId, evidence }`) and publishes the
|
|
466
|
+
returned `{ label, viewUrl?, thumbnailUrl? }` as `InboxEntry.evidence`
|
|
467
|
+
(`{ evidence, label, viewUrl?, thumbnailUrl? }`, never text). Build those URLs
|
|
468
|
+
from metadata without reading bytes; if `rowEvidence` throws or returns a
|
|
469
|
+
malformed answer, the page still lists without previews, and a row without
|
|
470
|
+
durable parts never gets one. When it is omitted, rows show no original link.
|
|
471
|
+
Either way `list` reads the page's evidence metadata once, in that same query,
|
|
472
|
+
so the row label can use the original filename; it never reads original bytes
|
|
473
|
+
and never calls `evidence(part)`. The primary part is chosen by role, not media
|
|
474
|
+
type: the shallowest child part when any part has a parent (an upload's file
|
|
475
|
+
under its capture record, an email's first attachment under the message, even
|
|
476
|
+
when that child is JSON), else the first root part (a watched-folder file);
|
|
477
|
+
part ids sort numerically. `IntakeInbox`/`InboxView` accept an
|
|
478
|
+
optional `rowAside` snippet receiving the `InboxEntry`; without it a row renders
|
|
479
|
+
the host's "Open original" link (new tab) and thumbnail when the URLs are
|
|
480
|
+
same-origin; each link's accessible name is "Open original: <row label>", and
|
|
481
|
+
a row shows no wrapper when neither URL is usable. The slot is a plain `div`,
|
|
482
|
+
not an `aside` landmark, so a page does not add one landmark per row. Optional
|
|
483
|
+
`label(item, evidence)` changes presentation; `evidence` is the item's durable
|
|
484
|
+
parts with the primary part first, and the default label is the primary part's
|
|
485
|
+
own original `filename`, else `Intake <8 hex>`. Optional
|
|
486
|
+
`assignments(itemIds)` reads one page's assignments in one batch (otherwise
|
|
487
|
+
`assignment` runs per row), and optional `assignedItemIds(assigneeId)` lets
|
|
488
|
+
`list` push the assignee filter into the page query (junk ids are dropped, not
|
|
489
|
+
fatal); without it an assignee filter scans further pages and filters by
|
|
490
|
+
`assignment`, which stays correct but costs more rows. The page's continuation
|
|
491
|
+
is always the last row actually returned, so an assignment that changes
|
|
492
|
+
between the page read and the assignment read costs at most one extra scan and
|
|
493
|
+
never skips or repeats a row. Optional
|
|
158
494
|
`enableFeedback: true` exposes the optional feedback callback when the service
|
|
159
495
|
has feedback configured. Optional `onSplit(itemId, revision)` wakes the host worker after the durable extraction
|
|
160
496
|
request; repair dispatches if wakeup fails. The factory creates no assignment
|