@qoniai/qoni 0.9.1 → 0.9.2

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/README.md CHANGED
@@ -44,6 +44,17 @@ pnpm add @qoniai/qoni
44
44
  yarn add @qoniai/qoni
45
45
  ```
46
46
 
47
+ > **Requires the matching backend:** hosted login, current-task login, ordinary
48
+ > forms/Profile, pre-run memory and Personal Agent provisioning below ship in 0.9.2
49
+ > and need the matching Qoni backend. Personal Agent provisioning is not enabled in
50
+ > production yet. See the [typed examples](./docs/delivery-examples/README.md).
51
+
52
+ > This unified local candidate combines those interfaces, the corrected memory
53
+ > error contract, original-key receipt reads and the A1 r2 memory projection.
54
+ > It remains unreleased; use its fixed archive/DTS and matching backend contract.
55
+
56
+ The current candidate's Personal Agent, formal delegation, wildcard and recovery boundaries are documented in the [Quickstart v5 contract](./docs/quickstart-v5-contract.md).
57
+
47
58
  Requirements:
48
59
 
49
60
  - Node.js 18 or later.
@@ -174,6 +185,11 @@ const result = await run.wait({
174
185
  console.log(result.output);
175
186
  ```
176
187
 
188
+ This public-page example requests only the product's `read` + `manage` pair. Before using the [browser-control handler below](#interactions-hitl), explicitly add `scopes: [QoniScopes.BROWSER_USE_TAKE_CONTROL]` when requesting its run token, allow that scope in the AccessKey policy, and obtain the user's consent. The site-login handler separately needs `QoniScopes.SITE_LOGIN_REQUEST` and `QoniScopes.SITE_LOGIN_CONFIRM`. Use interactive delegation for these permissions; the `products` shorthand does not request them.
189
+
190
+ When creating a task that may request human control, pass `taskScopes: [QoniScopes.BROWSER_USE_TAKE_CONTROL]` to `doAnything.run()` (also supported by `api.createRun()` and `runWithMemory()`). Add specific `SITE_LOGIN_*` scopes there only when the task needs them. The SDK always includes manage; omitting `taskScopes` requests only manage. These scopes must already be allowed by the delegation and current policy. Only known concrete Web Agent scopes are accepted, not wildcards. Existing product tokens are passed through unchanged and cannot gain authority through this option. Available from 0.9.2.
191
+
192
+
177
193
  `run()` returns a `RunHandle` typed to the product: `qoni.deepResearch.run()` is a `RunHandle<DeepResearchEvent>`, so its events are narrowed to what Deep Research actually emits. `capture: { screenshots: true }` opts into per-step screenshots, decoded to bytes for you. `wait()` drives the run to its terminal state and settles the result (`result.raw` carries cost, step counts, token usage).
178
194
 
179
195
  Undecodable or empty screenshot data URIs never interrupt `wait()` or invoke `onScreenshot`, regardless of capture settings. They follow the existing progress fallback when a human-readable message is available; `onWireEvent` always retains the original payload, including frames with no progress text.
@@ -320,7 +336,7 @@ if (!pending || req.query.state !== pending.businessState) throw new Error("Inva
320
336
 
321
337
  const completed = await qoni.completeDelegateToken({
322
338
  grantId: pending.grantId,
323
- code: String(req.query.code),
339
+ code: req.query.code,
324
340
  state: String(req.query.grant_state), // grant_state, not your business state
325
341
  });
326
342
 
@@ -340,7 +356,7 @@ const grant = await qoni.genauth.delegateAgent({
340
356
  agentKey: "email-assistant", // Agent audit label; alias for agent
341
357
  scopes: [
342
358
  "webagent.do_anything:read", // Read status, events, and artifacts
343
- "webagent.do_anything:manage", // Start/cancel tasks and respond to interactions
359
+ "webagent.do_anything:manage", // Start/cancel tasks and respond to ordinary interactions; excludes browser control
344
360
  ],
345
361
  expiresIn: "15m", // Converted to 900 seconds before signing
346
362
  });
@@ -362,7 +378,7 @@ prohibit email sending or deletion; a prompt is not an enforced deny policy.
362
378
 
363
379
  ## Choosing Scopes
364
380
 
365
- Each WebAgent product exposes exactly two scopes: `read` (observe — status, events, artifacts) and `manage` (anything that changes execution state — run, cancel, respond, messages, monitor CRUD). The 8 product scopes are `webagent.{do_anything, web_search, deep_research, track}:{read, manage}`. Interactive site login (`openLogin()`) additionally needs `QoniScopes.SITE_LOGIN_REQUEST` and `QoniScopes.SITE_LOGIN_CONFIRM`, which are not part of any product and must be requested explicitly in `scopes`. The simplest authorization is the `products` sugar — each product name expands to its `read` + `manage` pair:
381
+ Each WebAgent product exposes two product scopes: `read` (status, events, artifacts) and `manage` (run, cancel, ordinary interaction responses, messages, monitor CRUD). Product `manage` does not include browser connection, refresh or release. The 8 product scopes are `webagent.{do_anything, web_search, deep_research, track}:{read, manage}`. Browser control additionally requires `QoniScopes.BROWSER_USE_TAKE_CONTROL` (`webagent.browser_use:take_control`); interactive site login requires `QoniScopes.SITE_LOGIN_REQUEST` and `QoniScopes.SITE_LOGIN_CONFIRM`. Request these independent permissions explicitly in `scopes`. Each name in the `products` shorthand expands only to its product's `read` + `manage` pair:
366
382
 
367
383
  ```ts
368
384
  await qoni.delegateToken({
@@ -374,7 +390,7 @@ await qoni.delegateToken({
374
390
 
375
391
  Scope strings are validated locally before any request: a scope missing its service prefix (e.g. `do_anything:manage` instead of `webagent.do_anything:manage`) throws `QoniValidationError` immediately with the correct form in the message — every candidate when the short form is ambiguous (`profile:read` → `gumem.profile:read` or `user.profile:read`).
376
392
 
377
- The site-login, user Profile, payment and Agent mailbox scopes (`QoniScopes.SITE_LOGIN_*`, `USER_PROFILE_READ`, `USER_PROFILE_WRITE`, `USER_PAYMENT_USE`, `AGENT_MAIL_READ`, `AGENT_MAIL_SEND`) belong to no product: request them explicitly in `scopes`, and allow them in the AccessKey policy. The user Profile, payment and Agent mailbox constants only name the scopes; requesting them does not by itself enable Profile, payment or mailbox access in a run.
393
+ The browser-control, site-login, user Profile, payment and Agent mailbox scopes (`QoniScopes.BROWSER_USE_TAKE_CONTROL`, `SITE_LOGIN_*`, `USER_PROFILE_READ`, `USER_PROFILE_WRITE`, `USER_PAYMENT_USE`, `AGENT_MAIL_READ`, `AGENT_MAIL_SEND`) belong to no product bundle: request them explicitly in `scopes`, and allow them in the AccessKey policy. The corresponding Console changes add separate browser-control and site-login policies; confirm that GenAuth and Console support them before requesting a new grant. The user Profile, payment and Agent mailbox constants only name the scopes; requesting them does not by itself enable Profile, payment or mailbox access in a run.
378
394
 
379
395
  Use explicit scope strings when the requested permission boundary should be visible in code.
380
396
 
@@ -384,17 +400,57 @@ Use explicit scope strings when the requested permission boundary should be visi
384
400
  | Create a GUMem session and recall context | `gumem.memory:read`, `gumem.memory:write`, `gumem.message:write` | Agent can create a memory session, write messages, and recall context for the current user. |
385
401
  | Write task results | `gumem.message:write`, `gumem.action:write` | Agent can write this task's confirmed result back to GUMem. |
386
402
  | Search public web | `webagent.web_search:read`, `webagent.web_search:manage` | Agent can search public pages and read search results. |
387
- | Run a bounded web task | `webagent.do_anything:read`, `webagent.do_anything:manage` | Agent can run a visible web task, read progress, stop it, and intervene when needed. |
403
+ | Run a bounded web task | `webagent.do_anything:read`, `webagent.do_anything:manage` | Agent can run a visible web task, read progress, stop it, and respond to ordinary interactions. Browser control requires its separate scope. |
388
404
  | Create a monitor | `webagent.track:read`, `webagent.track:manage` | Agent can configure monitors, read events, run checks, and stop monitors. |
389
405
  | Sign in to sites during a run | `webagent.site_login:request`, `webagent.site_login:confirm` | The user signs in through a live browser; the login is saved for later tasks. |
390
- | Use the user's Profile | `user.profile:read`, `user.profile:write` | Agent can read the user's saved details (name, phone, shipping address, card brand and last 4) and save new ones the user provides. |
406
+ | Human browser control | `webagent.browser_use:take_control` | The user connects, refreshes, and releases the current browser. Request this explicitly; product manage does not include it. |
407
+ | Use the user's Profile | `user.profile:read`, `user.profile:write` | The ordinary Task adapter reads/saves permitted name/address and supported nonencrypted primitive custom fields. Phone/email verification, payment/card data and other sensitive fields are excluded. |
391
408
  | Pay with the user's saved card | `user.payment:use` | After the user approves a payment, the saved card is filled in for them. |
392
409
  | Use the Agent's own mailbox | `agent.mail:read`, `agent.mail:send` | Agent can read mail sent to its own address (such as verification codes) and send mail from it. |
393
410
 
394
411
  Silent mode is usually appropriate for low-risk actions such as reading current-user memory, writing a confirmed summary, or searching public web pages.
395
412
 
413
+ Browser control uses `QoniScopes.BROWSER_USE_TAKE_CONTROL`, outside the `products` shorthand. When upgrading to the corresponding backend, both the AccessKey policy and user delegation must explicitly include it. Legacy `webagent.do_anything:control`, `webagent.deep_research:control`, and `webagent.browser:control` scopes are not automatically converted. Refresh and release require this permission too. This scope adaptation does not establish stale-card protection or durable login persistence.
414
+
415
+ ### Migrating browser-control permissions
416
+
417
+ This is an unreleased breaking change. It requires a coordinated cutover: the old Web Agent release endpoint requires product `manage`, whereas the new SDK requests only `webagent.browser_use:take_control` for release; the new Web Agent rejects `manage` as a substitute. Upgrading Web Agent and the SDK one at a time does not guarantee uninterrupted control sessions.
418
+
419
+ 1. Prepare GenAuth's scope registration and Console's **WebAgent Browser Control** and **WebAgent Site Login** policies first. Enable the permissions actually needed on the AccessKey; the site-login policy contains `webagent.site_login:request` and `webagent.site_login:confirm`.
420
+ 2. Request `QoniScopes.BROWSER_USE_TAKE_CONTROL` explicitly and obtain new user consent through interactive delegation. Add the site-login scopes only when needed. Updating the AccessKey policy or renewing an existing delegation must not silently add permissions to that user's grant.
421
+ 3. During a maintenance window, stop admitting new browser-control tasks and finish or drain existing control sessions with the old versions. Then coordinate the Web Agent and SDK cutover; do not add a `manage` compatibility bypass.
422
+ 4. Verify connect, refresh and release with the newly consented scope, and verify that delegations lacking it or carrying only legacy control scopes are rejected. The new Console lists permissions not covered by the selected policies separately, including legacy, unknown and partial policy entries. It preserves them by default and removes them only when the user selects “Remove these permissions when saving”. Inspect the saved AccessKey allowlist. Preserving an old control scope does not grant `webagent.browser_use:take_control`; migrate callers to the independent scope.
423
+
424
+ Deployment and publication still require a separate Owner instruction. This migration guide does not authorize them.
425
+
426
+ ### Raw browser-control calls
427
+
428
+ After the policy and consent migration, `doAnything.api.intervene()` accepts `interactionId` (wire alias `interaction_id`). Refresh and release must send the ID captured for that control round. A first user-initiated `take_control` may omit it only when no control marker is active; reconnecting to an active round must send its exact ID. The new Web Agent take/refresh response includes `interaction_id`; keep the raw response field, rather than deriving an ID from a URL or fetching a newer card.
429
+
430
+ ```ts
431
+ import type { DoAnythingControlResponse } from "@qoniai/qoni";
432
+
433
+ // Only for a first user-initiated take when no control round is active.
434
+ const first = await qoni.doAnything.api.intervene<DoAnythingControlResponse>({
435
+ token, runId: run.id, kind: "take_control",
436
+ });
437
+ const interactionId = first.data.interaction_id;
438
+ if (!interactionId) throw new Error("This backend did not return the control-round ID; stop and complete the migration");
439
+
440
+ // When this round's URL needs refreshing:
441
+ await qoni.doAnything.api.intervene({ token, runId: run.id, kind: "refresh_control_url", interactionId });
442
+ // When the user hands this round back:
443
+ await qoni.doAnything.api.intervene({ token, runId: run.id, kind: "release_control", interactionId });
444
+ ```
445
+
446
+ Missing, ended, expired or mismatched IDs are rejected by Web Agent with HTTP 409 before side effects. The SDK preserves that rejection and does not fetch a current ID or retry with another one. Invalidate the old UI and let the user act on a newly received interaction separately; keep old `ControlSession` objects and handles bound to their original card. Card methods continue to use the ID in the declared action path. `requestId` identifies an input question, not a browser-control round. Supplying conflicting `interactionId` and `interaction_id`, or an explicit non-string/empty ID, throws `QoniValidationError` before HTTP; omission is forwarded so the server can enforce its state-dependent rule.
447
+
396
448
  Interactive mode is recommended for higher-risk actions such as browser takeover, external site login, long-running monitoring, form submission, or access to sensitive artifacts and recordings.
397
449
 
450
+ Raw `answer_input_request` and `skip_input_request` calls must identify the current pending input with `requestId` (wire `input_request_id`). Web Agent checks the persisted request, waiting state, expiry and declared action before accepting it. A stale or undeclared action returns 409; the SDK does not replace the ID or retry. Use the declared confirmation and sign-in methods for those interactions instead of treating a raw answer or skip as approval or successful login.
451
+
452
+ In this backend change, card `connect_control` returns a pending control round. OBO/API-key callers cannot make it an active Console connection by sending `surface: "console_inline"`. Releasing such a pending round leaves a pending login/credential question unresolved. The unreleased task-login policy below uses declared `open_login` and `confirm_signed_in` for reliable save/release/reply; a connect-only card cannot substitute for that flow. The agent still verifies actual sign-in; deployment requires a separate Owner instruction.
453
+
398
454
  The SDK also exports a GUMem-oriented bundle for the common session/write/recall path:
399
455
 
400
456
  ```ts
@@ -403,6 +459,58 @@ QoniScopeBundles.GUMEM_SESSION_RECALL
403
459
 
404
460
  ## Capability Examples
405
461
 
462
+ ### Internal Pi host memory entry (Unreleased)
463
+
464
+ The local build adds `createTaskClient` for the existing Pi host. Application
465
+ Quickstart code still uses DoAnything and its interactions; it does not call
466
+ memory or Profile directly. Select the new task facade archive explicitly;
467
+ the frozen current-task integration package remains a separate input.
468
+
469
+ ```ts
470
+ import { createTaskClient } from "@qoniai/qoni";
471
+
472
+ const task = createTaskClient({
473
+ productHosts: { gumem: trustedHosts.gumem },
474
+ runId: trustedRunId,
475
+ tokenProvider: getCurrentRunPermission,
476
+ });
477
+ await task.memory.createSession();
478
+ await task.memory.addMessages({
479
+ messages: [{ id: `${piSessionId}:${entry.id}`, role: entry.role, content: entry.content }],
480
+ });
481
+ ```
482
+
483
+ The host supplies trusted service configuration, a stable run ID and
484
+ `getCurrentRunPermission(product, requiredScopes, runId, signal)`. The provider
485
+ returns `{ accessToken, expiresAt }` with expiry in Unix milliseconds, and must
486
+ check current permission on every request. This entry needs no AK/SK and only
487
+ exposes `memory.createSession` and `memory.addMessages` for `qoni-run-<runId>`.
488
+ Run IDs keep their complete value, allow at most 55 Unicode characters and
489
+ reject path separators; run and message IDs reject C0/C1 controls and DEL.
490
+ Messages require a stable nonempty `id`, a nonempty GUM string `role` and string
491
+ `content`. Role and content must be well-formed Unicode; valid pairs, whitespace,
492
+ newlines and empty content are preserved. No identity/token/header/path overrides
493
+ are accepted, including at runtime. Once a whole batch passes synchronous validation,
494
+ its IDs are submitted and bound on that client, including batches not yet sent.
495
+ Provider rejection or timeout before HTTP does not release the binding: changed
496
+ content or role fails locally; the original content can be explicitly retried.
497
+ An invalid batch binds no IDs.
498
+
499
+ `timeoutMs` (default 30 seconds) covers provider, HTTP and response body;
500
+ optional per-call `signal` cancels all stages. HTTPS is required except for
501
+ loopback HTTP fixtures; service base paths are preserved and redirects are
502
+ refused. Every call obtains current permission. No POST is automatically
503
+ retried. Ordered immutable batches receive a stable internal `X-Request-ID`
504
+ for existing usage-outbox deduplication; changed or overlapping batches do not
505
+ prove exactly-once processing. The facade returns `QoniResponse` with only
506
+ Session fields or `{ request_id, message_count }`, and typed Qoni errors without
507
+ raw private provider/HTTP payloads.
508
+
509
+ This method boundary is not resource-server authorization. Trusted G6/WA token
510
+ issuance and GUM run/session enforcement, durable Pi cursors, recovery and real
511
+ Pi/E2B/offline acceptance remain dependencies; provider/loopback tests do not
512
+ prove reliable background memory.
513
+
406
514
  ### GUMem
407
515
 
408
516
  ```ts
@@ -443,6 +551,87 @@ const context = await qoni.gumem.recall({
443
551
  // The SDK example stops here because application orchestration is product-specific.
444
552
  ```
445
553
 
554
+ **Unreleased recall mapping:** `metadataFilters` is sent inside `recall_config.metadata_filters`. Top-level and nested camel/snake aliases merge only when duplicate values agree; conflicting values throw `QoniValidationError` before HTTP. Filter values must be strings, finite numbers or booleans; null, object and array values are rejected. System fields (`project_id`, `group_id`, `user_id`, `thread_id`, `session_id`, `scope`, `expired_at`, `validity`, including normalized aliases) are reserved. Other recall configuration keys pass through unchanged. This validation protects SDK calls; it does not fix server-side isolation or verify cross-task recall.
555
+
556
+ ### Recall before starting a task (Unreleased)
557
+
558
+ Use `runWithMemory` to opt into bounded GUM context before a Do Anything task.
559
+ Keep the original delegation token with both `gumem.memory:read` and
560
+ `webagent.do_anything:manage` permissions. Each resource performs its existing
561
+ exchange; a WA-only product token cannot grant memory access.
562
+
563
+ ```ts
564
+ const { run, memory } = await qoni.doAnything.runWithMemory({
565
+ token: delegationToken,
566
+ prompt: "Plan my next trip using my confirmed preferences.",
567
+ memory: {
568
+ sessionId: "daily-assistant", // existing GUM session, not a WA session ID
569
+ query: "confirmed travel preferences",
570
+ metadataFilters: { namespace: "travel" },
571
+ maxItems: 12,
572
+ maxContextBytes: 8192,
573
+ },
574
+ timeoutMs: 30_000,
575
+ // signal: abortController.signal,
576
+ });
577
+ console.log(run.id, memory.items, memory.truncated);
578
+
579
+ // For an application that composes its own input:
580
+ const { data: context } = await qoni.gumem.recallContext({
581
+ token: delegationToken,
582
+ sessionId: "daily-assistant",
583
+ query: "confirmed travel preferences",
584
+ });
585
+ ```
586
+
587
+ Ordinary `run()` stays unchanged. An empty recall keeps the original prompt.
588
+ A failed recall, invalid context, timeout or cancellation before submission
589
+ prevents the Task POST. The combined deadline defaults to 30 seconds and accepts
590
+ 1..60000 ms; writes are never automatically retried. A failure after the Task
591
+ POST can have an unknown commit outcome. `runWithMemory` errors expose `phase`
592
+ (`recall`, `task_permission`, or `task_submit`) and `submissionUnknown`. Once the
593
+ Task POST is dispatched, a timeout, disconnect, cancellation or unusable success
594
+ response raises `QoniTaskSubmissionUnknownError` with `code: "task.submit_unknown"`,
595
+ `phase: "task_submit"`, `submissionUnknown: true`, and `retryable: false`. The error
596
+ contains no original cause or request body. Reconcile the original request with
597
+ the server; do not blindly submit again. Before dispatch, `submissionUnknown` is
598
+ `false`; caller cancellation uses the safe `request.aborted` code instead of
599
+ returning the arbitrary signal reason. HTTP 4xx responses and explicit business rejections inside a successful HTTP response retain their original typed error, code, status and retryable flag; actual HTTP 5xx responses after dispatch are submission unknown and nonretryable.
600
+ Once fetch is invoked, even connection refusal is conservatively unknown; this
601
+ does not prove that the server received or created a Task and never permits an automatic retry. Cancellation here does not cancel an
602
+ already created Task.
603
+
604
+ `recallContext()` returns `QoniResponse<MemoryContext>`; read `.data` for
605
+ `origin`, typed `items`, bounded
606
+ `referenceText`, and `truncated`. Defaults are 12 items / 8192 UTF8 bytes;
607
+ allowed limits are 1..32 items / 512..16384 bytes. Only facts, summary and user or
608
+ assistant history text become quoted reference data. Working memory,
609
+ `formatted_context`, tool channels and arbitrary metadata are excluded.
610
+ Authorization-shaped values are omitted; the references confer no permission or
611
+ approval and quoting is not a model prompt-injection guarantee.
612
+
613
+ History sources contain the returned message `id` and GUM `sessionId`, with an
614
+ optional exact stored `metadata.source_url` if it is a bounded safe HTTP(S) URL.
615
+ It is caller-stored message evidence, not verified fact provenance. No URL is
616
+ inferred from content, and `resource_urls` remain media-resource references.
617
+ With the matching A1 r2 backend projection, facts/summary items can carry
618
+ `recordId`, `engine` and `raw_input` sources (`id`, `sessionId`, `messageId`,
619
+ `createdAt`, optional stored `url`), with `sourceStatus: "record_id"`.
620
+ `memory_records_coverage` must confirm complete coverage before typed records
621
+ replace a nonempty legacy section. Missing coverage keeps the complete legacy
622
+ section with `sources: []` and `sourceStatus: "unavailable"`; text equality never
623
+ attaches a source. Content has priority within the item/byte budget: omitted
624
+ source sets, identities or content set `truncated`; sources are kept as whole
625
+ sets or omitted. Older backends still return source-less facts/summary.
626
+ User-level semantic recall retains its project/user/scope=user path. Missing
627
+ stored URLs cannot be repaired by a response field. Local GA/HTTP/PG/Qdrant evidence does not imply hosted-vector,
628
+ extraction-worker, Pi/cloud or production acceptance; existing GUM expiration
629
+ filter limitations remain separate.
630
+
631
+ The corrected-candidate markers above apply only to `runWithMemory`; ordinary
632
+ `run()` and older packages may lack them. Absence of these fields never makes a
633
+ write safe to replay. See the [corrected memory-error example](./docs/delivery-examples/m2-r2/README.md).
634
+
446
635
  ### Do Anything
447
636
 
448
637
  ```ts
@@ -599,6 +788,20 @@ pagination raises `QoniValidationError` before HTTP. `MonitorSchedule` retains
599
788
  the deprecated cron branch; optional undefined fields retain omission behavior, while specified unsupported fields fail; historical event and option types remain exported
600
789
  for source compatibility. Unsupported methods are not successful capability tests.
601
790
 
791
+ ### The original Task user's ordinary Profile (Unreleased)
792
+
793
+ `doAnything.profile.read({ runId, token, fields })` and
794
+ `doAnything.profile.update({ runId, token, values })` return `QoniResponse<OrdinaryTaskProfile>`.
795
+ Read `.data.values`, `.data.missing`, `.data.writable_fields` and `.data.field_types`.
796
+ Request `user.profile:read` and, only for explicit saves, `user.profile:write` in
797
+ the original delegation together with the required Web Agent scopes. A newer
798
+ token cannot broaden the original Task authorization. This is the original
799
+ Task owner's ordinary Profile, not admin user management or browser login state.
800
+
801
+ Use the [field allowlist and write limits](./docs/ordinary-task-profile.md).
802
+ Unknown writes require readback of the original run; do not replay the update.
803
+ Ordinary form submission alone does not save Profile fields.
804
+
602
805
  ### GenAuth User Context and Management
603
806
 
604
807
  ```ts
@@ -624,6 +827,41 @@ const created = await client.genauth.users.create({
624
827
 
625
828
  `currentUser` and `genauth.userInfo` use a GenAuth access token because they read the logged-in user's identity. `genauth.users.*` is a management-plane capability, but application code still authenticates the SDK with Qoni AK/SK: the SDK signs `POST /api/v3/eak/genauth/admin-token` with an empty JSON body `{}`, receives a standard GenAuth management token plus `userPoolId`, then calls GenAuth v3 user management APIs with `Authorization: Bearer ...` and `x-authing-userpool-id`. It does not send `resource`, `actions`, `expiresIn`, or `QONI_USER_ID`. Runtime capability calls such as GUMem/WebAgent use `delegateToken` output instead.
626
829
 
830
+ ### GenAuth User Sign-Up and Sign-In (Unreleased)
831
+
832
+ Send users to the Qoni-hosted GenAuth sign-up/sign-in page and exchange the callback code on your server. The AccessKey must be bound to a registered business app in Qoni Console; the business app, its client secret, PKCE verifier and nonce stay on the server and never appear in the URL.
833
+
834
+ ```ts
835
+ // 1. Start: store `state` in your own session, then redirect the browser.
836
+ const state = crypto.randomUUID();
837
+ session.loginState = state;
838
+ const { url, expiresAt } = await qoni.genauth.auth.createLoginUrl({
839
+ redirectUri: "https://app.example.com/auth/callback", // registered on the business app (exact match)
840
+ state,
841
+ screen: "signup", // optional; default "login"
842
+ });
843
+ res.redirect(url);
844
+
845
+ // 2. Callback: verify `state` yourself first — the SDK does not check it for you.
846
+ if (typeof req.query.state !== "string" || !session.loginState ||
847
+ req.query.state !== session.loginState || req.query.error !== undefined ||
848
+ typeof req.query.code !== "string" || !req.query.code) return res.status(400).end();
849
+ // Use session-scoped atomic match-and-consume storage in a multi-replica app;
850
+ // see the typed hosted-login example. Never exchange this state/code twice.
851
+ delete session.loginState;
852
+ const login = await qoni.genauth.auth.exchangeLoginCode({
853
+ code: req.query.code,
854
+ redirectUri: "https://app.example.com/auth/callback",
855
+ });
856
+ // login.user: { id, email?, phone?, name?, isNewUser }
857
+ // login.accessToken / login.idToken / login.expiresIn
858
+ ```
859
+
860
+ - `expiresAt` is the server-side deadline fixed when the URL is created (600 seconds). Reopening or refreshing the page, multi-factor steps and new browser sessions do not extend it; after it passes, the page shows an error or the callback carries `error=access_denied`.
861
+ - A URL completes at most once, and a code is exchanged at most once. If an exchange fails, times out or its response is lost, the code is not reusable: start again with `createLoginUrl`. The SDK never retries these calls.
862
+ - `isNewUser` is decided by GenAuth from the actual registration in this sign-in: it is `true` only when the account was registered in the same hosted-page window that completes the sign-in (same browser session, business app, user pool and final user), including after a page refresh, multi-factor step or signing in again in that window. Signing in to the same account in another window, a reused session or an existing account is `false`. If the server cannot match a registration in this browser session to a window, the callback carries `error=access_denied` instead of guessing.
863
+ - Errors arrive as typed `QoniError`s: GA business failures are HTTP 200 with an error envelope (for example `eak.hosted_login.code_invalid`, `eak.hosted_login.binding_mismatch`, `eak.credential.binding_mismatch`, `eak.hosted_login.business_app_unbound`, or `eak.hosted_login.unavailable` when the server is not configured for hosted login), and the SDK raises them rather than returning data. Use `login.accessToken` with `genauth.userInfo` to read the same user again.
864
+
627
865
  ## API Surface
628
866
 
629
867
  ### Client
@@ -659,9 +897,9 @@ const qoni = new Qoni({
659
897
  | Namespace | Methods |
660
898
  | --- | --- |
661
899
  | Delegation | `delegateToken`, `completeDelegateToken`, deprecated aliases `delegateAgent`, `completeDelegateAgent` |
662
- | GenAuth | `userInfo`, `jwks`, `discovery`, `introspectDelegationToken`, `users.list`, `users.get`, `users.getBatch`, `users.create`, `users.createBatch`, `users.update`, `users.deleteBatch` |
663
- | GUMem | `createSession`, `addMessages`, `recall`, `uploadResource`, `actions.record`, `actions.recall`, `actions.stream` |
664
- | Do Anything | `run`, `attach` → `RunHandle` |
900
+ | GenAuth | `userInfo`, `jwks`, `discovery`, `introspectDelegationToken`, `auth.createLoginUrl`, `auth.exchangeLoginCode`, `agents.create/get/bind` (Unreleased), `users.list`, `users.get`, `users.getBatch`, `users.create`, `users.createBatch`, `users.update`, `users.deleteBatch` |
901
+ | GUMem | `createSession`, `addMessages`, `recall`, `recallContext`, `uploadResource`, `actions.record`, `actions.recall`, `actions.stream` |
902
+ | Do Anything | `run`, `attach` → `RunHandle`; opt-in `runWithMemory`; `profile.read/update` (Unreleased) |
665
903
  | Deep Research | `run`, `attach` → `RunHandle` |
666
904
  | Web Search | `run`, `attach` → `RunHandle` |
667
905
  | Track | `create`, `attach` → `MonitorHandle` |
@@ -730,14 +968,23 @@ has to do, never by business, so one handler covers every task:
730
968
  | --- | --- | --- |
731
969
  | `site_login` | signs in to a site in the live browser | `openLogin()`, `confirmSignedIn()` |
732
970
  | `take_control` | operates the browser by hand (captcha, 2FA, …) | `connectControl()`, `refreshControl()`, `releaseControl()` |
971
+ | `fill_form` (Unreleased) | completes ordinary fields for this task | `submit(values)`, `skip()` |
733
972
  | `ask_user` | answers a question shaped by `answerType` / `options` | `answer()`, `skip()` |
734
973
  | `confirmation` | allows or rejects an action | `confirm()`, `reject()` |
735
974
  | `wait` | nothing — the system is waiting (rate limit, external event) | `retry()` |
736
975
 
976
+ A confirmation records the decision for that card; it does not establish
977
+ forced approval of all final actions through open browser/bash/harness paths.
978
+ Ordinary forms are not a channel for payment secrets or isolated sensitive entry.
979
+
737
980
  `Interaction` is a union distributed over `type`, so `switch (request.type)`
738
981
  narrows `request.payload` in each branch. Each method posts to the endpoint the
739
982
  backend declared in the interaction's `actions[]`, so you never hard-code a route:
740
983
 
984
+ Create the token for this handler with `products: ["doAnything"]` plus explicit `scopes: [QoniScopes.BROWSER_USE_TAKE_CONTROL, QoniScopes.SITE_LOGIN_REQUEST, QoniScopes.SITE_LOGIN_CONFIRM]` after AccessKey policy preparation and user consent. The product permissions supply DoAnything manage; omit the site-login scopes when the task does not need login. `products: ["doAnything"]` alone cannot authorize `connectControl()`, `refreshControl()` or `releaseControl()`; follow the migration steps above before switching versions.
985
+
986
+ The task must have been created with `taskScopes: [QoniScopes.BROWSER_USE_TAKE_CONTROL]` to initiate human control; delegation scope alone is insufficient. An existing task's original authority cannot be expanded by refreshing a control URL.
987
+
741
988
  ```ts
742
989
  import type { InteractionHandle } from "@qoniai/qoni";
743
990
 
@@ -776,8 +1023,14 @@ async function handleInteraction(handle: InteractionHandle) {
776
1023
  const result = await run.wait({ onInteraction: handleInteraction });
777
1024
  ```
778
1025
 
779
- Interactions arrive one at a time: the run waits until the current one is
780
- handled. The same interaction calls back again on every status change and on
1026
+ Follow each card's declared actions and current state. A pending input question
1027
+ may coexist with a control round. Releasing a control round — SDK/OBO pending or
1028
+ Console active — never answers a login/credential ask; in current_task both are
1029
+ `site_login` cards that end only through `confirmSignedIn()` after reliable save,
1030
+ a declared explicit `skip()`, or expiry. Only legacy `secure_input` asks receive
1031
+ a recheck reply on Console active release, which proves neither sign-in nor
1032
+ persistence. Clarification and confirmation are never answered automatically.
1033
+ The same interaction calls back again on every status change and on
781
1034
  reconnect replays, always with the same `id` and `type` — key your UI cards by
782
1035
  `request.id`. An update with content of its own replaces the card (a take-over
783
1036
  that became `active` carries its new `liveUrl`); when the backend settles an
@@ -787,11 +1040,27 @@ example from Console; show it if your UI offers control there. Methods: `answer(
787
1040
  `reject()`, `openLogin({ siteId?, profileId? })`, `confirmSignedIn()`,
788
1041
  `connectControl()`, `refreshControl()`, `releaseControl()`, `retry()`,
789
1042
  `switchProfile()`. `connectControl()` and `refreshControl()` resolve to
790
- `{ liveUrl, expiresAt?, status?, raw }`: the signed URL of the browser the user
791
- now controls. `handle.can(kind)` reports whether the interaction currently
1043
+ `{ liveUrl, expiresAt?, status?, raw }`, where `liveUrl` is the browser-control URL
1044
+ to show the user. `handle.can(kind)` reports whether the interaction currently
792
1045
  offers that action; calling a method the backend did NOT declare throws (so a
793
1046
  "recovery" button only exists when it actually works).
794
1047
 
1048
+ ### Ordinary forms (Unreleased)
1049
+
1050
+ `fill_form` collects ordinary details for the current task. Render `payload.fields` using the exported `FormField` type, then call `handle.submit(values)` when the card declares `submit`. Keys are the exact `field.name`; select answers use option `value`, numbers stay numbers, and an address is `{ line1, line2?, city?, region?, postalCode?, country? }`. Required text/address values must be nonempty; optional text may be empty and `0` is valid. Unknown fields, wrong types/options, NUL and nonfinite numbers are rejected before transport and again by the backend on the current durable card.
1051
+
1052
+ ```ts
1053
+ if (handle.interaction.type === "fill_form" && handle.can("submit")) {
1054
+ await handle.submit({
1055
+ count: 0,
1056
+ method: "ship",
1057
+ address: { line1: "Synthetic road", city: "Singapore" },
1058
+ });
1059
+ }
1060
+ ```
1061
+
1062
+ This is task-only collection. Sensitive fields, payment cards and `profileField` writes are unavailable here; passwords use browser sign-in/take-control. It needs the matching W4 backend. Final W6 synthesis and provider acceptance are separate.
1063
+
795
1064
  **`ask_user` answers are structured.** `answer()` takes the value matching
796
1065
  `payload.answerType` — `text` → string, `number` → number, `boolean` → boolean,
797
1066
  `single_choice` → one `options[].value`, `multiple_choice` → an array of option
@@ -799,21 +1068,39 @@ values — and throws `QoniValidationError` before sending anything that does no
799
1068
  fit. The wire still calls this type `clarification`; the SDK presents it as
800
1069
  `ask_user` (choices arrive as `{ value, label }` options).
801
1070
 
802
- **Sign-in takes two calls.** `openLogin()` opens a live login browser for the
803
- `site_login` interaction (pass `siteId` from `payload.sites[].siteId`, or omit it
804
- when exactly one site is listed). Once the user is signed in, `confirmSignedIn()`
805
- saves the login so later tasks reuse it, then tells the agent to recheck the site
806
- and continue; if the user turns out not to be signed in, the agent raises a new
807
- `site_login`. It saves once and rechecks once per login session, however many
808
- times it is called. If the backend could not save the login yet, it throws a
809
- retryable `QoniError` coded `site_login.save_failed` without telling the agent to
810
- go on; call it again. The same holds for `site_login.session_replaced`, thrown
811
- when the login was reopened while it was being saved. Handles from the same `RunHandle` (or `MonitorHandle`)
812
- share the login session, so `confirmSignedIn()` also works on the handle of a
813
- later callback; a RunHandle from a new `attach()` does not. Delegate
814
- `QoniScopes.SITE_LOGIN_REQUEST` and `QoniScopes.SITE_LOGIN_CONFIRM`, and allow
815
- both in the AccessKey policy. `openLogin()`'s result still offers `confirm()` for
816
- callers that save the login themselves; it is deprecated.
1071
+ **Sign-in takes two SDK calls.** Pass a `siteId` from `payload.sites[].siteId` to
1072
+ `openLogin()`, or omit it when one site is listed; show its `liveUrl` only to the
1073
+ current user. After the user signs in, call `confirmSignedIn()`.
1074
+
1075
+ For the **unreleased `current_task` policy**, the backend declares open/confirm
1076
+ endpoints on the same DoAnything run and original login question L. Opening uses
1077
+ the existing task browser, without navigating or opening a separate browser;
1078
+ `login.sessionId` is the fixed control round C, also returned in
1079
+ `raw.control_interaction_id`, while `raw.browser_session_id` identifies the browser.
1080
+ Opening requests SITE_LOGIN_REQUEST + BROWSER_USE_TAKE_CONTROL; confirmation
1081
+ requests SITE_LOGIN_CONFIRM + BROWSER_USE_TAKE_CONTROL + DO_ANYTHING_MANAGE.
1082
+ Allow these policies on the AccessKey and obtain explicit user consent before
1083
+ the coordinated migration above; `manage` cannot replace login/control permission.
1084
+
1085
+ One confirm HTTP `{login_session_id:C}` on the original declared endpoint saves
1086
+ reliably, releases C and replies to L. The SDK sends neither Profile confirm nor
1087
+ a second recheck. The deprecated `login.confirm()` shares this same request and
1088
+ returns `state: "saved", active: false, probeResult: "agent_pending"` with the
1089
+ unmodified response in `raw`. Saved does not mean verified: the agent must check
1090
+ the page before continuing. Unknown/missing mode or mismatched L/C fails closed;
1091
+ confirmation without a successful open fails locally. A 409 (including
1092
+ `no_pending_login`) remains an error, with no ID replacement or automatic reopen.
1093
+ 503 save errors, timeout/lost responses and `site_login.invalid_response` leave
1094
+ the original L/C available for an explicit retry; a lost success is acknowledged
1095
+ only by the backend's exact saved history.
1096
+
1097
+ The legacy `/profiles/login` policy retains its single request/confirm scopes,
1098
+ Profile save followed by the agent recheck, and existing `active/probeResult`
1099
+ semantics, including the legacy `no_pending_login` recheck behavior. Shared slots,
1100
+ concurrent calls and replacement guards apply to both policies: an old result
1101
+ cannot confirm a newer round. Handles from the same RunHandle/MonitorHandle share
1102
+ the session; a RunHandle from another `attach()` does not. Use
1103
+ `openLogin()` → `confirmSignedIn()`; calling `login.confirm()` first is unnecessary.
817
1104
 
818
1105
  The discriminant strings are also exported as constants so you can avoid bare
819
1106
  literals: `InteractionTypes` (`SiteLogin` / `TakeControl` / `AskUser` /
@@ -838,7 +1125,7 @@ Supported low-level methods live under `qoni.<product>.api.*` (e.g. `qoni.doAnyt
838
1125
 
839
1126
  `event.raw.event` is the **raw wire event type** — an internal transport name (`run.action.started`, `run.section_completed`, …) whose naming is historical and inconsistent, exposed as a plain string. This is an implementation detail, not the product API: developers match on the curated `event.type` above. The full wire-type catalog is intentionally NOT exported — reach for `event.raw.event` (a string) only when you need an exact wire match the curated layer folds into `progress` (e.g. telling a specific browser action apart).
840
1127
 
841
- Browser Use scopes are reserved until their product runtime SDK methods are exported. Site Login scopes (`QoniScopes.SITE_LOGIN_REQUEST`, `QoniScopes.SITE_LOGIN_CONFIRM`) back `InteractionHandle.openLogin()` and `confirmSignedIn()` on run interactions.
1128
+ `QoniScopes.BROWSER_USE_TAKE_CONTROL` authorizes the browser-control interaction methods and raw take/refresh/release calls described above. Site Login scopes (`QoniScopes.SITE_LOGIN_REQUEST`, `QoniScopes.SITE_LOGIN_CONFIRM`) back `InteractionHandle.openLogin()` and `confirmSignedIn()` on run interactions.
842
1129
 
843
1130
  ### Response Shape
844
1131
 
@@ -923,15 +1210,27 @@ All error classes are exported and can be matched with `instanceof`:
923
1210
  | `QoniValidationError` | Local input check failed before any request (bad scope string, silent delegation missing a user). | Fix the call — the message states the correct form. |
924
1211
  | `QoniAuthError` | AK/SK signing rejected by Qoni (bad/rotated credentials). | Check `accessKey` / `secretKey`. |
925
1212
  | `QoniPermissionDeniedError` | The AccessKey or delegation token lacks a required scope (403). | Check the AccessKey permission policy and request the missing scope with `delegateToken`. |
926
- | `QoniTokenExpiredError` | The delegation / product token expired. | Mint a fresh token and retry. |
1213
+ | `QoniTokenExpiredError` | The delegation / product token expired. | Re-establish only currently permitted access; do not expand the old grant or replay an uncertain write. |
927
1214
  | `QoniDelegationRequiredError` | A product call was made without `token`, or with a raw token the gateway won't accept directly. | Pass `delegation.data.token` to the product call. |
928
- | `QoniRateLimitError` | Rate limited (429). | Back off and retry (`retryable === true`). |
929
- | `QoniTimeoutError` | A request (or `wait({ timeoutMs })`) timed out. | Retry; the run keeps executing server-side — `attach()` to it. |
930
- | `QoniUpstreamError` | A downstream service failed (5xx / upstream exchange failure). | Back off and retry when `retryable`. |
1215
+ | `QoniRateLimitError` | Rate limited (429). | Back off for a safe read; reconcile any uncertain write before an explicit retry. |
1216
+ | `QoniTimeoutError` | A request (or `wait({ timeoutMs })`) timed out. | For an existing run, `attach()`/read status. A timed-out creation may have committed; do not submit a replacement automatically. |
1217
+ | `QoniTaskSubmissionUnknownError` (corrected candidate) | `runWithMemory` dispatched a Task POST without a definitive outcome. | `task.submit_unknown`, `submissionUnknown: true`, `retryable: false`; reconcile the original request, never automatically resubmit. |
1218
+ | `QoniUpstreamError` | A downstream service failed (5xx / upstream exchange failure). | Reconcile writes first; `retryable` does not prove that a side effect was rolled back. |
931
1219
  | `QoniError` | Base class / any other code. | Inspect `err.code`. |
932
1220
 
933
- The SDK does **not** auto-retry. `err.retryable` tells you whether a retry could
934
- succeed; you own the backoff.
1221
+ JSON requests are **not** automatically retried. `err.retryable` describes an
1222
+ error category, not permission to replay a write; SSE reconnect is separate.
1223
+ A 202/`ready: false` response means accepted or pending, not completed. Keep the
1224
+ original run/provisioning/operation ID. Read it back when supported; a missing
1225
+ receipt, 404 or elapsed time does not prove the first write had no effect.
1226
+
1227
+ When the user narrows or revokes consent in the supported user permission UI,
1228
+ stop scheduling calls that need the removed scopes and respect backend denial.
1229
+ Do not silently recreate a wider grant, switch user/App or refresh into extra
1230
+ permissions. This candidate exposes no `genauth.consents.revoke()` SDK method.
1231
+ Stopping a local wait does not itself stop a server Task or prove an external
1232
+ operation has stopped. [Failure handling examples](./docs/delivery-examples/README.md)
1233
+ keep these outcomes separate.
935
1234
 
936
1235
  ```ts
937
1236
  import {
@@ -951,7 +1250,8 @@ try {
951
1250
  error instanceof QoniTokenExpiredError ||
952
1251
  error instanceof QoniDelegationRequiredError
953
1252
  ) {
954
- // Re-mint a delegation token (add the missing scope / refresh expiry) and retry.
1253
+ // Stop this operation. Obtain explicit consent only if the user wants
1254
+ // additional permissions; never broaden the existing Task/grant silently.
955
1255
  }
956
1256
 
957
1257
  if (
@@ -959,7 +1259,8 @@ try {
959
1259
  error instanceof QoniTimeoutError ||
960
1260
  error instanceof QoniUpstreamError
961
1261
  ) {
962
- // Transient — back off and retry when `error.retryable` is true.
1262
+ // Do not repeat run creation or another write here. Read the original
1263
+ // operation when its ID is known; otherwise preserve the unknown outcome.
963
1264
  }
964
1265
 
965
1266
  throw error;
@@ -1030,3 +1331,80 @@ The Personal Quickstart email creation flow is separate from the unified Agent
1030
1331
  management model, where identity, email and phone are optional capabilities and an
1031
1332
  Agent may have multiple email addresses and phone numbers. Personal binding does
1032
1333
  not issue credentials or enable IAM-token access to the native Mail/Phone APIs.
1334
+
1335
+ Structured Pi questions and ordinary forms in this unreleased phase use the main agent entry; subagents explicitly reject parameters they cannot forward.
1336
+
1337
+ Configuring a longer run duration does not establish uninterrupted one-hour
1338
+ waiting across credentials, browser and provider lifetimes. Confirmation cards
1339
+ and ordinary forms also do not enforce approval or secret isolation across
1340
+ open browser/bash/harness channels. Those full-business guarantees remain unavailable.
1341
+
1342
+ ### Personal Agent provisioning and optional mailbox (Unreleased)
1343
+
1344
+ This candidate interface is available for local integration. Backend business validation is still incomplete; this example does not establish
1345
+ external mailbox-provider support.
1346
+
1347
+ `genauth.agents.create/get/bind` use the existing signed transport. Persist the first `idempotencyKey` and complete request before creation; reuse both for retries. A `ready: false` receipt describes accepted or recoverable work, not completed creation. Never replace an uncertain request with a new key. With a requested mailbox, `ready: true` requires the original Mail operation to succeed, its resource to be active, and IAM to confirm readiness.
1348
+
1349
+ ```ts
1350
+ const request = { displayName: "My assistant", mailboxLocalPart: "my-assistant", idempotencyKey: savedRequestKey };
1351
+ let result = await qoni.genauth.agents.create(request);
1352
+ // Persist provisioningId, then resume this same operation later.
1353
+ result = await qoni.genauth.agents.get({ provisioningId: result.provisioningId });
1354
+ if (result.state === "ready" && result.ready) {
1355
+ result = await qoni.genauth.agents.bind({ provisioningId: result.provisioningId, accessToken: currentUserAccessToken });
1356
+ }
1357
+ ```
1358
+
1359
+ Binding requires the current person's valid GenAuth user token and matching pool/App authority; a browser-provided owner ID is not accepted. Never-bound Agents are reclaimed seven days after IAM readiness; binding clears that deadline. Definitive failures are compensated promptly; uncertain provider results retain their original operation for reconciliation. Inspect `state/errorCode`; the SDK does not automatically replay uncertain writes. Keep tokens and management credentials on a trusted application server and out of logs. This feature is off by default and requires provisioning, the existing Mail worker, and IAM service authentication.
1360
+
1361
+ ### Recover a Do Anything acceptance receipt
1362
+
1363
+ This additive W11 interface requires a Web Agent deployment with the matching
1364
+ request acceptance API. Keep one stable `requestId` and the original payload for
1365
+ each submission. The SDK does not retry writes automatically.
1366
+
1367
+ ```ts
1368
+ const requestId = "checkout-task-20261009-001";
1369
+ const run = await qoni.doAnything.run({ token, prompt, requestId });
1370
+ // If the create response was lost, read the original key. Do not create another
1371
+ // task and guess which one is the original.
1372
+ const receipt = await qoni.doAnything.api.getRequest<{ id: string }>({ token, requestId });
1373
+ const trace = await qoni.doAnything.api.getTrace({ token, runId: receipt.data.id });
1374
+ ```
1375
+
1376
+ `getRequest` returns the usual run envelope (use a response generic when reading
1377
+ its fields in TypeScript). Include the original `sessionId` for a follow-up.
1378
+ Keys are isolated by the authenticated project, user, App/credential, Agent and
1379
+ initial/follow-up session. A different canonical payload for the same key returns
1380
+ `409 request_payload_conflict`. `409 request_in_progress` and a missing receipt
1381
+ are not proof that submission failed: read the original key first. If the
1382
+ original request never reached the server, that lookup can keep returning 404.
1383
+ You may explicitly resubmit task acceptance with the **same requestId and
1384
+ identical payload**, including the original sessionId for a follow-up, under
1385
+ the same authenticated identity namespace. An accepted request returns its
1386
+ original Task; no new key should be generated to guess the outcome. The SDK
1387
+ does not automatically retry POST requests. This does not authorize repeating
1388
+ an external action whose result is unknown. Canceling or expiring a run does
1389
+ not free its key.
1390
+
1391
+ Calling `runWithMemory` again recalls memory again and may change the actual `instructions`, so after an unknown outcome prefer `api.getRequest`/`api.getTrace` readback using the original request key and returned run ID, and never treat newly recalled content as the original payload to resubmit.
1392
+
1393
+ Runs with acceptance keys retain their Task/Event receipts. Hard-deleting their
1394
+ session returns `409 request_receipt_retained`; cancellation remains available.
1395
+ Supporting erasure while retaining deduplication needs a separate retention
1396
+ model. Operational database erasure is outside this receipt guarantee.
1397
+
1398
+ Trace contains only original identity references and safe states. Missing grant,
1399
+ consent or operation evidence stays missing. A completed run is distinct from an
1400
+ external operation result: `external_result: "unknown"` requires checking the
1401
+ original website/service. Acceptance deduplication does not guarantee that a
1402
+ website order or email never repeats, and trace is never an approval credential.
1403
+
1404
+ ### Protected form input (local candidate)
1405
+
1406
+ A trusted application UI can use `run.interactionHandle(card).submitSensitive(values)` for a current `fill_form` whose protected text field is named `qoni_sensitive_<customData key>`. Use the original Web Agent product access token that created the Task; a delegation token or a fresh token exchange cannot replace that source. The SDK sends each protected value directly to the configured GenAuth endpoint and sends only its opaque reference in the original Web Agent reply. Ordinary `submit()` rejects these fields. Do not call this API from a model or an open browser tool.
1407
+
1408
+ For explicit recovery, use `qoni.doAnything.forms.prepare({ token, runId, interactionId, fieldName })`, then `intake({ token, ref, value })`, `status({ token, ref })`, or `cancel({ token, ref })`. Prepare preserves the original reference and deadline; it does not authorize another intake. Persistence additionally requires the server allowlisted non-security-code definition `cardNumber`, explicit `persist: true` (or `persistFields` on `submitSensitive`), current UDF write permission and existing AES encryption. Every other or ambiguous custom key is transient-only; caller labels/types cannot authorize storage. CVC/CVV is always transient, cannot be persisted, and has no digest. Missing or unsupported UDF configuration fails closed.
1409
+
1410
+ `intake`, cancellation and reference-reply uncertainty use `submissionUnknown: true` and `retryable: false`; no POST is automatically retried and no raw response body is retained in these errors. Read the original reference/Task after a missing response. Explicit cancel removes the temporary value only when acknowledged. GA revocation denies access; next authenticated access attempts cleanup, while the fixed Redis TTL independently reclaims the value. This is not a claim of instantaneous erase at revocation or deployed backup isolation. The original card deadline (`card_deadline`) remains unchanged, including the default hour. GenAuth records `value_deadline` once as the earliest of that deadline, the original run/source deadline and issue time plus five minutes; `expires_at` is its compatibility alias. All three fields use epoch milliseconds. The value expires independently while the card may still be waiting. Only an unexpired `awaiting` reference accepts one intake; `writing`, `unknown`, `consumed` and `expired` cannot be resubmitted or replaced on the same card. Prepare/status never slide the deadline or create a replacement reference. A read of `ready` resolves the original intake only after its SQL commit was confirmed and its Redis ready transition completed; it does not authorize another POST. Production sensitive release remains disabled until the target platform's isolation is demonstrated; this candidate does not claim automatic fill, payment or external delivery.