@qoniai/qoni 0.9.0 → 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.
Files changed (33) hide show
  1. package/CHANGELOG.md +203 -0
  2. package/README.md +548 -76
  3. package/README.zh-CN.md +443 -53
  4. package/dist/index.cjs +15082 -699
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.d.cts +25826 -243
  7. package/dist/index.d.ts +25826 -243
  8. package/dist/index.js +15077 -698
  9. package/dist/index.js.map +1 -1
  10. package/docs/authing-sdk-LICENSE.txt +21 -0
  11. package/docs/delivery-examples/README.md +44 -0
  12. package/docs/delivery-examples/README.zh-CN.md +44 -0
  13. package/docs/delivery-examples/agents.ts +26 -0
  14. package/docs/delivery-examples/failure-handling.ts +21 -0
  15. package/docs/delivery-examples/hosted-login.ts +40 -0
  16. package/docs/delivery-examples/interactions.ts +30 -0
  17. package/docs/delivery-examples/local-consumer.mjs +159 -0
  18. package/docs/delivery-examples/m2-r2/README.md +20 -0
  19. package/docs/delivery-examples/m2-r2/README.zh-CN.md +20 -0
  20. package/docs/delivery-examples/m2-r2/local-consumer.mjs +53 -0
  21. package/docs/delivery-examples/m2-r2/memory-outcome.ts +22 -0
  22. package/docs/delivery-examples/m2-r2/package.json +1 -0
  23. package/docs/delivery-examples/m2-r2/tsconfig.json +1 -0
  24. package/docs/delivery-examples/package.json +5 -0
  25. package/docs/delivery-examples/profile-memory.ts +30 -0
  26. package/docs/delivery-examples/tsconfig.json +4 -0
  27. package/docs/genauth-communication.md +1410 -0
  28. package/docs/genauth-management-api.md +597 -0
  29. package/docs/genauth-management.md +285 -0
  30. package/docs/ordinary-task-profile.md +18 -0
  31. package/docs/quickstart-v5-contract.md +39 -0
  32. package/package.json +17 -7
  33. package/skills/qoni-sdk/SKILL.md +177 -7
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  [![Package manager](https://img.shields.io/badge/package%20manager-pnpm-f69220)](https://pnpm.io/)
6
6
  [![License](https://img.shields.io/badge/license-MIT-blue)](./LICENSE)
7
7
 
8
- Unified Node.js SDK for Qoni Agent delegation, GenAuth user management, GUMem memory, WebAgent automation, web search, and monitoring.
8
+ Unified Node.js SDK for Qoni Agent delegation, GenAuth management, GUMem memory, WebAgent automation, web search, and monitoring.
9
9
 
10
10
  `@qoniai/qoni` gives a trusted server one compact way to use Qoni AK/SK. For runtime product calls, your service verifies the GenAuth user, requests short-lived delegation with `delegateToken`, and then uses the returned token in GUMem, WebAgent, Do Anything, Web Search, and Track calls. For GenAuth user management, the SDK exchanges AK/SK for a standard GenAuth management token internally and calls GenAuth v3 users APIs with that token.
11
11
 
@@ -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.
@@ -125,6 +136,21 @@ await qoni.genauth.users.update({
125
136
  // await qoni.genauth.users.deleteBatch({ userIds: [profile.data.userId] });
126
137
  ```
127
138
 
139
+ ### GenAuth Management
140
+
141
+ `qoni.genauth` exposes **591 typed management methods across 35 namespaces**, aligned with [Authing's official Node SDK 4.0.2](https://github.com/Authing/authing-node-sdk). This includes all 496 official JSON management methods plus GenAuth extensions and compatibility names, including positions, public accounts, invitations, metadata, approvals and risk lists. Calls share the Qoni AK/SK management-token exchange and support cancellation. Deployment features and permissions still determine endpoint availability.
142
+
143
+ ```ts
144
+ await qoni.genauth.groups.create({ code: "engineering", name: "Engineering" });
145
+ await qoni.genauth.groups.addMembers({ code: "engineering", userIds: ["USER_ID"] });
146
+ const roles = await qoni.genauth.management.listRoles({ namespace: "project", page: 1, limit: 20 });
147
+ for await (const user of qoni.genauth.users.iterate({ limit: 50 })) {
148
+ console.log(user.userId);
149
+ }
150
+ ```
151
+
152
+ [Integration guide](./docs/genauth-management.md) · [Complete API catalog](./docs/genauth-management-api.md). Roles belong to an existing permission namespace. GenAuth employment and departure rules still apply when deleting users.
153
+
128
154
  ### Runtime Product Delegation
129
155
 
130
156
  GUMem, WebAgent, Do Anything, Web Search, and Track act for an end user. Silent delegation calls need a real GenAuth user id from the userpool bound to the Qoni credential, then a `delegateToken` result.
@@ -159,6 +185,11 @@ const result = await run.wait({
159
185
  console.log(result.output);
160
186
  ```
161
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
+
162
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).
163
194
 
164
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.
@@ -305,7 +336,7 @@ if (!pending || req.query.state !== pending.businessState) throw new Error("Inva
305
336
 
306
337
  const completed = await qoni.completeDelegateToken({
307
338
  grantId: pending.grantId,
308
- code: String(req.query.code),
339
+ code: req.query.code,
309
340
  state: String(req.query.grant_state), // grant_state, not your business state
310
341
  });
311
342
 
@@ -325,7 +356,7 @@ const grant = await qoni.genauth.delegateAgent({
325
356
  agentKey: "email-assistant", // Agent audit label; alias for agent
326
357
  scopes: [
327
358
  "webagent.do_anything:read", // Read status, events, and artifacts
328
- "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
329
360
  ],
330
361
  expiresIn: "15m", // Converted to 900 seconds before signing
331
362
  });
@@ -347,7 +378,7 @@ prohibit email sending or deletion; a prompt is not an enforced deny policy.
347
378
 
348
379
  ## Choosing Scopes
349
380
 
350
- 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:
351
382
 
352
383
  ```ts
353
384
  await qoni.delegateToken({
@@ -359,7 +390,7 @@ await qoni.delegateToken({
359
390
 
360
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`).
361
392
 
362
- 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.
363
394
 
364
395
  Use explicit scope strings when the requested permission boundary should be visible in code.
365
396
 
@@ -369,17 +400,57 @@ Use explicit scope strings when the requested permission boundary should be visi
369
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. |
370
401
  | Write task results | `gumem.message:write`, `gumem.action:write` | Agent can write this task's confirmed result back to GUMem. |
371
402
  | Search public web | `webagent.web_search:read`, `webagent.web_search:manage` | Agent can search public pages and read search results. |
372
- | 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. |
373
404
  | Create a monitor | `webagent.track:read`, `webagent.track:manage` | Agent can configure monitors, read events, run checks, and stop monitors. |
374
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. |
375
- | 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. |
376
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. |
377
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. |
378
410
 
379
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.
380
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
+
381
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.
382
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
+
383
454
  The SDK also exports a GUMem-oriented bundle for the common session/write/recall path:
384
455
 
385
456
  ```ts
@@ -388,6 +459,58 @@ QoniScopeBundles.GUMEM_SESSION_RECALL
388
459
 
389
460
  ## Capability Examples
390
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
+
391
514
  ### GUMem
392
515
 
393
516
  ```ts
@@ -428,6 +551,87 @@ const context = await qoni.gumem.recall({
428
551
  // The SDK example stops here because application orchestration is product-specific.
429
552
  ```
430
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
+
431
635
  ### Do Anything
432
636
 
433
637
  ```ts
@@ -486,46 +690,118 @@ const followUp = await qoni.deepResearch.run({
486
690
  });
487
691
  ```
488
692
 
693
+ `doAnything.api.readArtifacts()` and the exported `DoAnythingSnapshot`,
694
+ `SnapshotImage`, and `SnapshotAction` types are deprecated. There has never been
695
+ a backend endpoint/source for that structured snapshot contract. The method
696
+ raises local `QoniUnsupportedError` (`code: "unsupported.operation"`) with zero
697
+ HTTP. Use `qoni.doAnything.artifacts({ token, runId })` / `api.listArtifacts()` for files, including
698
+ `artifact.content()` or `api.artifactDownloadUrl()` for downloads. Use
699
+ `capture: { screenshots: true }` and screenshot events for images. A text file
700
+ artifact is not a snapshot; the real file APIs are unchanged.
701
+
489
702
  ### Track
490
703
 
704
+ Track uses `/track/tracks`. `prompt` maps to `instructions`; include watched URLs
705
+ in that text. An explicit schedule is required: an integer interval of
706
+ 600–2592000 seconds, or a daily time with a time zone. A daily check starts
707
+ 0–30 minutes before the target time, rather than exactly at that time.
708
+
491
709
  ```ts
710
+ import type { MonitorSchedule } from "@qoniai/qoni";
711
+
492
712
  const monitor = await qoni.track.create({
493
713
  token,
494
- prompt: "Monitor the price of Bitcoin every minute.",
495
- });
496
-
497
- // A new monitor starts in `pending_clarification` while the backend aligns
498
- // the intent into a concrete spec, then flips to `active` (usually a few
499
- // seconds; it may emit an `interaction` first if the intent is ambiguous).
500
- // runNow()/refine() require `active`, so wait for it before acting:
501
- for await (const event of monitor.events()) {
502
- if (event.type === "interaction") {
503
- const i = monitor.interactionHandle(event.data); // e.g. an intent clarification
504
- if (i.can("confirm")) await i.confirm();
505
- continue;
506
- }
507
- const { status } = await monitor.get();
508
- if (status === "active") break;
714
+ prompt: "Watch https://example.com/policy and report changes.",
715
+ title: "Policy changes",
716
+ schedule: { kind: "interval", intervalSeconds: 3600 },
717
+ }); // create immediately starts a first_look check
718
+ const initial = await monitor.get();
719
+ const firstRunId = initial.lastTaskId; // capture this original ID once
720
+ if (typeof firstRunId !== "string" || !firstRunId) throw new Error("Missing initial lastTaskId");
721
+ const sameMonitor = await qoni.track.attach(monitor.id, { token });
722
+
723
+ async function waitForCheck(runId: string) {
724
+ const deadline = Date.now() + 300_000;
725
+ do {
726
+ const check = await monitor.run(runId); // exact ID in the recent 50 checks
727
+ if (["done", "failed", "expired", "canceled"].includes(String(check.status))) {
728
+ if (check.status !== "done") throw new Error(`Check ${runId} ${String(check.status)}: ${String(check.failure ?? "")}`);
729
+ return check;
730
+ }
731
+ await new Promise(resolve => setTimeout(resolve, Math.min(1000, Math.max(0, deadline - Date.now()))));
732
+ } while (Date.now() < deadline);
733
+ throw new Error(`Check ${runId} timed out`);
509
734
  }
510
735
 
511
- await monitor.runNow(); // trigger an immediate tick
512
- const ticks = await monitor.runs({ limit: 10 }); // read-only tick run views
513
-
514
- // Change the definition with refine() (any subset of: schedule / targetUrls /
515
- // extractionSchema / triggerDsl / stopConditionDsl / notifyChannel). `schedule`
516
- // is { kind: "cron", expr } or { kind: "interval", intervalSeconds } (same for
517
- // track.create()). Extraction field names and extracted values keep their
518
- // original keys in get()/run() reads. The
519
- // natural-language intent isn't refinable — create a new monitor to change it:
520
- await monitor.refine({ schedule: { kind: "cron", expr: "0 9 * * *" } });
521
- await monitor.pause(); // stop scheduled ticks without deleting
522
- await monitor.resume();
523
- await monitor.delete();
524
-
525
- // Reconnect later from a stored id:
526
- const sameMonitor = await qoni.track.attach(monitor.id, { token });
736
+ try {
737
+ await monitor.pause(); // pause future scheduling; the first look continues
738
+ await waitForCheck(firstRunId);
739
+ await monitor.refine({ schedule: initial.schedule as MonitorSchedule }); // our known, validated input roundtrips
740
+ await monitor.refine({ schedule: { kind: "daily", at: "09:00", tz: "Asia/Singapore" } });
741
+ const task = await monitor.runNow(); // one explicit manual check while scheduling is paused
742
+ const check = await waitForCheck(task.runId); // 202 acceptance is not completion
743
+ const checks = await monitor.runs({ limit: 10, offset: 0 });
744
+ await monitor.resume();
745
+ } finally {
746
+ await monitor.delete(); // soft-delete; any in-flight check continues
747
+ }
527
748
  ```
528
749
 
750
+ Create immediately starts a `first_look` check. Capture its initial `lastTaskId`
751
+ and wait for that same check before one explicit `runNow()`. A concurrent run
752
+ can still cause 409; the SDK surfaces it and never retries. Pause stops future
753
+ scheduled checks and keeps any started check running.
754
+
755
+ Track check `status` is `running`, `done`, `failed`, `expired`, or `canceled`;
756
+ `done` is the successful terminal value. It is kept verbatim and differs from
757
+ one-shot `RunResult.status: "succeeded"`. Known wire schedule shapes are read
758
+ without revalidating server values against SDK write bounds or Intl time zones;
759
+ unknown/incomplete shapes remain verbatim. Writing a schedule stays strict, so
760
+ validate/choose supported input rather than casting every server value back.
761
+
762
+ `runs()` retrieves the backend's newest 50 checks and applies `limit`/`offset`
763
+ locally. `run(id)` searches that same bounded window; a missing ID raises
764
+ `QoniError` with local `code: "not_found"` (no HTTP status) and explains the range. A missing Track itself is the backend 404 `track_not_found`. Checks expose the
765
+ actual status, outcome, headline, answer and failure; no extraction, diffs or
766
+ snapshot IDs are synthesized. `runNow()` can return the backend's 409
767
+ `task_in_progress` while an earlier task is working. It returns a task reference
768
+ on 202; read checks to observe completion.
769
+
770
+ | Retained surface | Current behavior |
771
+ | --- | --- |
772
+ | `track.create`, `track.attach`, `MonitorHandle.get` | Create/read the real Track resource; get converts known wire schedules; unknown shapes stay verbatim. |
773
+ | `MonitorHandle.pause`, `resume` | PATCH `status: paused` / `active`; pause keeps an in-flight check running. |
774
+ | `MonitorHandle.refine` | PATCH title/schedule only; changing prompt/instructions/intent requires a new track. |
775
+ | `MonitorHandle.runNow` | POST `run_now`; returns the actual task reference. |
776
+ | `MonitorHandle.runs`, `run` | Read recent checks with Track read scope only; no DoAnything fallback. |
777
+ | `MonitorHandle.delete` | DELETE 204; soft deletion stops scheduling and does not cancel running work. |
778
+ | `track.api.createMonitor`, `getMonitor` | Historical names retained; use real `instructions`, `title`, and wire schedule `{ every_seconds: N }` or `{ daily_at: "HH:MM", tz }`. |
779
+ | `track.api.updateMonitor` | PATCH real title/instructions/schedule/status fields; wire API permits instructions, while high-level refine does not. |
780
+ | `track.api.runNow`, `deleteMonitor` | Real `run_now` / DELETE; raw task reference / empty 204. |
781
+ | `track.api.listRuns`, `getRun` | Raw checks, local limit/offset, exact lookup within recent 50. |
782
+ | `MonitorHandle.events`, `interactionHandle`, `track.api.events`, `intervene` | Deprecated; no backend endpoint. Local `QoniUnsupportedError`, zero HTTP or token exchange. |
783
+ | `targetUrls`, `extractionSchema`, `triggerDsl`, `stopConditionDsl`, `notifyChannel` (including snake_case aliases), legacy `url` | No backend equivalent; rejected locally with `QoniUnsupportedError`. Put URLs in the prompt explicitly. |
784
+ | Cron schedule | Deprecated and rejected locally with a daily alternative. Arbitrary cron is never silently converted. |
785
+
786
+ A missing schedule, malformed input, invalid interval/time/zone, or invalid
787
+ pagination raises `QoniValidationError` before HTTP. `MonitorSchedule` retains
788
+ the deprecated cron branch; optional undefined fields retain omission behavior, while specified unsupported fields fail; historical event and option types remain exported
789
+ for source compatibility. Unsupported methods are not successful capability tests.
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
+
529
805
  ### GenAuth User Context and Management
530
806
 
531
807
  ```ts
@@ -551,6 +827,41 @@ const created = await client.genauth.users.create({
551
827
 
552
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.
553
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
+
554
865
  ## API Surface
555
866
 
556
867
  ### Client
@@ -586,9 +897,9 @@ const qoni = new Qoni({
586
897
  | Namespace | Methods |
587
898
  | --- | --- |
588
899
  | Delegation | `delegateToken`, `completeDelegateToken`, deprecated aliases `delegateAgent`, `completeDelegateAgent` |
589
- | GenAuth | `userInfo`, `jwks`, `discovery`, `introspectDelegationToken`, `users.list`, `users.get`, `users.getBatch`, `users.create`, `users.createBatch`, `users.update`, `users.deleteBatch` |
590
- | GUMem | `createSession`, `addMessages`, `recall`, `uploadResource`, `actions.record`, `actions.recall`, `actions.stream` |
591
- | 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) |
592
903
  | Deep Research | `run`, `attach` → `RunHandle` |
593
904
  | Web Search | `run`, `attach` → `RunHandle` |
594
905
  | Track | `create`, `attach` → `MonitorHandle` |
@@ -632,7 +943,7 @@ interface RunResult {
632
943
  runId: string;
633
944
  status: "succeeded" | "failed" | "canceled"; // delivery status of the run
634
945
  output?: unknown; // final answer / payload (shape is task-specific)
635
- artifacts: Artifact[]; // deliverables — deepResearch reports; [] for the others
946
+ artifacts: Artifact[]; // persisted DoAnything files / deepResearch reports
636
947
  isTaskSuccessful?: boolean; // did the task achieve its goal (distinct from `status`)
637
948
  terminalReason?: string;
638
949
  raw: JsonObject; // full wire envelope — cost, step counts, token usage
@@ -645,7 +956,7 @@ is the typed payload for that call (e.g. `genauth.users.list` → a paged user
645
956
  list on `.data`). The exact `T` shapes live in the bundled `.d.ts` and follow
646
957
  the backend contract.
647
958
 
648
- `MonitorHandle` (Track) exposes `id`, `get()`, `pause()`, `resume()`, `refine(patch)`, `runNow()`, `events()`, `interactionHandle()`, `runs()`, `run(runId)`, and `delete()`. `runNow()` and `refine()` require the monitor to be `active` — a freshly created monitor is briefly `pending_clarification` first (see the Track example).
959
+ `MonitorHandle` (Track) provides `id`, `get()`, `pause()`, `resume()`, `refine(patch)`, `runNow()`, `runs()`, `run(runId)`, and `delete()`. Its deprecated `events()` and `interactionHandle()` methods fail locally; see Track above for the complete mapping.
649
960
 
650
961
  ### Interactions (HITL)
651
962
 
@@ -657,14 +968,23 @@ has to do, never by business, so one handler covers every task:
657
968
  | --- | --- | --- |
658
969
  | `site_login` | signs in to a site in the live browser | `openLogin()`, `confirmSignedIn()` |
659
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()` |
660
972
  | `ask_user` | answers a question shaped by `answerType` / `options` | `answer()`, `skip()` |
661
973
  | `confirmation` | allows or rejects an action | `confirm()`, `reject()` |
662
974
  | `wait` | nothing — the system is waiting (rate limit, external event) | `retry()` |
663
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
+
664
980
  `Interaction` is a union distributed over `type`, so `switch (request.type)`
665
981
  narrows `request.payload` in each branch. Each method posts to the endpoint the
666
982
  backend declared in the interaction's `actions[]`, so you never hard-code a route:
667
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
+
668
988
  ```ts
669
989
  import type { InteractionHandle } from "@qoniai/qoni";
670
990
 
@@ -703,8 +1023,14 @@ async function handleInteraction(handle: InteractionHandle) {
703
1023
  const result = await run.wait({ onInteraction: handleInteraction });
704
1024
  ```
705
1025
 
706
- Interactions arrive one at a time: the run waits until the current one is
707
- 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
708
1034
  reconnect replays, always with the same `id` and `type` — key your UI cards by
709
1035
  `request.id`. An update with content of its own replaces the card (a take-over
710
1036
  that became `active` carries its new `liveUrl`); when the backend settles an
@@ -714,11 +1040,27 @@ example from Console; show it if your UI offers control there. Methods: `answer(
714
1040
  `reject()`, `openLogin({ siteId?, profileId? })`, `confirmSignedIn()`,
715
1041
  `connectControl()`, `refreshControl()`, `releaseControl()`, `retry()`,
716
1042
  `switchProfile()`. `connectControl()` and `refreshControl()` resolve to
717
- `{ liveUrl, expiresAt?, status?, raw }`: the signed URL of the browser the user
718
- 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
719
1045
  offers that action; calling a method the backend did NOT declare throws (so a
720
1046
  "recovery" button only exists when it actually works).
721
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
+
722
1064
  **`ask_user` answers are structured.** `answer()` takes the value matching
723
1065
  `payload.answerType` — `text` → string, `number` → number, `boolean` → boolean,
724
1066
  `single_choice` → one `options[].value`, `multiple_choice` → an array of option
@@ -726,21 +1068,39 @@ values — and throws `QoniValidationError` before sending anything that does no
726
1068
  fit. The wire still calls this type `clarification`; the SDK presents it as
727
1069
  `ask_user` (choices arrive as `{ value, label }` options).
728
1070
 
729
- **Sign-in takes two calls.** `openLogin()` opens a live login browser for the
730
- `site_login` interaction (pass `siteId` from `payload.sites[].siteId`, or omit it
731
- when exactly one site is listed). Once the user is signed in, `confirmSignedIn()`
732
- saves the login so later tasks reuse it, then tells the agent to recheck the site
733
- and continue; if the user turns out not to be signed in, the agent raises a new
734
- `site_login`. It saves once and rechecks once per login session, however many
735
- times it is called. If the backend could not save the login yet, it throws a
736
- retryable `QoniError` coded `site_login.save_failed` without telling the agent to
737
- go on; call it again. The same holds for `site_login.session_replaced`, thrown
738
- when the login was reopened while it was being saved. Handles from the same `RunHandle` (or `MonitorHandle`)
739
- share the login session, so `confirmSignedIn()` also works on the handle of a
740
- later callback; a RunHandle from a new `attach()` does not. Delegate
741
- `QoniScopes.SITE_LOGIN_REQUEST` and `QoniScopes.SITE_LOGIN_CONFIRM`, and allow
742
- both in the AccessKey policy. `openLogin()`'s result still offers `confirm()` for
743
- 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.
744
1104
 
745
1105
  The discriminant strings are also exported as constants so you can avoid bare
746
1106
  literals: `InteractionTypes` (`SiteLogin` / `TakeControl` / `AskUser` /
@@ -761,11 +1121,11 @@ if (handle.type === InteractionTypes.AskUser && handle.can(ActionKinds.answer))
761
1121
 
762
1122
  ### Wire-Level Escape Hatch
763
1123
 
764
- The 1:1 low-level methods live under `qoni.<product>.api.*` (e.g. `qoni.doAnything.api.createSession`, `qoni.deepResearch.api.followUp`, `qoni.track.api.createMonitor`). These mirror the backend HTTP contract directly — snake_case fields, double-envelope events, single-ID `/{product}/runs/{run_id}` addressing for run operations — and their shapes may evolve with the API. They are not part of the frozen public contract.
1124
+ Supported low-level methods live under `qoni.<product>.api.*` (e.g. `qoni.doAnything.api.createSession`, `qoni.deepResearch.api.followUp`, `qoni.track.api.createMonitor`). These mirror the backend HTTP contract directly — snake_case fields, double-envelope events, single-ID `/{product}/runs/{run_id}` addressing for run operations — and their shapes may evolve with the API. They are not part of the frozen public contract.
765
1125
 
766
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).
767
1127
 
768
- 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.
769
1129
 
770
1130
  ### Response Shape
771
1131
 
@@ -794,9 +1154,9 @@ that type:
794
1154
  // "phase" string — the Deep Research phase name
795
1155
  // "sectionReady" string — the Deep Research section title
796
1156
  // "resultsReady" number — Web Search count of unique results
797
- // "monitorCreated" string — Track monitor id
798
- // "triggered" string — Track change summary
799
- // "checkCompleted" boolean — Track: did the scheduled check find a change
1157
+ // "monitorCreated" string — deprecated legacy monitor wire type
1158
+ // "triggered" string — deprecated legacy monitor wire type
1159
+ // "checkCompleted" boolean — deprecated legacy monitor wire type
800
1160
  // RICH events — event.data is a small object:
801
1161
  // "message" { text, role }
802
1162
  // "interaction" the typed Interaction object (HITL) — see Interactions
@@ -814,7 +1174,7 @@ Each product handle is typed to its own subset:
814
1174
  | Do Anything | core: `progress` / `message` / `interaction` / `screenshot` / `browserLiveUrlChanged` / `done` |
815
1175
  | Web Search | core + `resultsReady` |
816
1176
  | Deep Research | core + `phase` / `sectionReady` (the outline gate arrives as an `interaction`) |
817
- | Track (`MonitorHandle`) | core + `monitorCreated` / `triggered` / `checkCompleted` |
1177
+ | Track (`MonitorHandle`) | No SSE endpoint; historical MonitorEvent types are deprecated. Read checks with runs()/run(). |
818
1178
 
819
1179
  For a live browser preview, listen for `browserLiveUrlChanged` and show
820
1180
  `event.data.liveUrl` in your viewer. A run can switch browser tabs after its
@@ -846,18 +1206,31 @@ All error classes are exported and can be matched with `instanceof`:
846
1206
 
847
1207
  | Class | Fires when | Typical recovery |
848
1208
  | --- | --- | --- |
1209
+ | `QoniUnsupportedError` | Retained surface has no backend operation (code: `unsupported.operation`); zero HTTP. | Use the alternative named in the error. |
849
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. |
850
1211
  | `QoniAuthError` | AK/SK signing rejected by Qoni (bad/rotated credentials). | Check `accessKey` / `secretKey`. |
851
1212
  | `QoniPermissionDeniedError` | The AccessKey or delegation token lacks a required scope (403). | Check the AccessKey permission policy and request the missing scope with `delegateToken`. |
852
- | `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. |
853
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. |
854
- | `QoniRateLimitError` | Rate limited (429). | Back off and retry (`retryable === true`). |
855
- | `QoniTimeoutError` | A request (or `wait({ timeoutMs })`) timed out. | Retry; the run keeps executing server-side — `attach()` to it. |
856
- | `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. |
857
1219
  | `QoniError` | Base class / any other code. | Inspect `err.code`. |
858
1220
 
859
- The SDK does **not** auto-retry. `err.retryable` tells you whether a retry could
860
- 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.
861
1234
 
862
1235
  ```ts
863
1236
  import {
@@ -877,7 +1250,8 @@ try {
877
1250
  error instanceof QoniTokenExpiredError ||
878
1251
  error instanceof QoniDelegationRequiredError
879
1252
  ) {
880
- // 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.
881
1255
  }
882
1256
 
883
1257
  if (
@@ -885,7 +1259,8 @@ try {
885
1259
  error instanceof QoniTimeoutError ||
886
1260
  error instanceof QoniUpstreamError
887
1261
  ) {
888
- // 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.
889
1264
  }
890
1265
 
891
1266
  throw error;
@@ -936,3 +1311,100 @@ const artifacts = await client.doAnything.artifacts({ token, runId: run.id });
936
1311
  ```
937
1312
 
938
1313
  Wire methods: `client.doAnything.api.listArtifacts(...)` and `artifactDownloadUrl(...)`.
1314
+
1315
+ ## Agent Mail and Agent Phone
1316
+
1317
+ The local GenAuth communication extension provides `qoni.genauth.agentMail` and `qoni.genauth.agentPhone`, including runtime and administrator APIs. Runtime requires native GenAuth credentials via `genauthCommunicationCredentials`; administration reuses Qoni token exchange. See [the communication guide](docs/genauth-communication.md) and [method index](docs/genauth-communication-api.md).
1318
+
1319
+ ## Local Agent integration extension
1320
+
1321
+ The integration branch adds `qoni.genauth.auth.createLoginUrl` / `exchangeLoginCode` and
1322
+ `qoni.genauth.agents.create` / `get` / `bind` / `bindUser`, alongside the existing
1323
+ management, Agent Mail and Agent Phone APIs. These additions are not an npm release.
1324
+ The hosted-login callback must validate the App's stored `state` before exchanging
1325
+ the one-use code. Personal mailbox delegation requires a formal IAM `agentId`; an
1326
+ audit label does not prove mailbox ownership. A Personal creation response may still
1327
+ be pending; an unknown result must be reconciled using its original provisioning
1328
+ reference or idempotency key, without starting another creation.
1329
+
1330
+ The Personal Quickstart email creation flow is separate from the unified Agent
1331
+ management model, where identity, email and phone are optional capabilities and an
1332
+ Agent may have multiple email addresses and phone numbers. Personal binding does
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.