@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
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2019-present Authing
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,44 @@
1
+ # Candidate SDK examples
2
+
3
+ These examples show how to use the candidate SDK without confusing pending work, saved login state or an uncertain write with business success. They target an **unreleased local package**, not the npm release that happens to share version 0.9.0. [中文](./README.zh-CN.md)
4
+
5
+ ## Before you start
6
+
7
+ Use Node.js 18+ with fetch, the exact candidate tarball supplied for your integration and a separate writable example directory. In an application, keep AK/SK and user tokens on a trusted server. The functions below accept an existing `Qoni` client; importing them sends no requests.
8
+
9
+ The local consumer uses only synthetic credentials and a temporary loopback HTTP server. It does not contact GenAuth, Web Agent, GUMem, a browser provider or a mail provider. HTTP responses stand in for those backends; this verifies SDK usage, not production authorization or backend business readiness.
10
+
11
+ ## Run the local examples
12
+
13
+ Copy the contents of this example directory to a new writable directory outside the SDK checkout. Install the **specific candidate archive**, not an unpinned npm package:
14
+
15
+ ```bash
16
+ npm install --ignore-scripts /absolute/path/to/reviewed-qoni-candidate.tgz
17
+ npm run smoke
18
+ ```
19
+
20
+ The private example package installs TypeScript and Node types along with the archive. `smoke` compiles all five `.ts` files against the installed package's public DTS, checks ESM and CommonJS imports and runs the loopback consumer. It records the installed ESM/CJS hashes in `local-results.json`; all 12 cases must be PASS and `resources.server_closed` must be true. The fixture contains no real purchase or email send.
21
+
22
+ ## Integrate one function at a time
23
+
24
+ | File | Purpose | Required application responsibility |
25
+ | --- | --- | --- |
26
+ | [hosted-login.ts](./hosted-login.ts) | Create a hosted URL and exchange one callback code after state validation | Supply browser-session-scoped storage with atomic state match-and-consume. Register the exact redirect URI. Store returned tokens server-side; never log them. |
27
+ | [interactions.ts](./interactions.ts) | Keep the original login/control handle and submit declared ordinary fields | Pass the handle from the current run's `onInteraction`; render only to the original user. Preserve its ID. Handle errors without replacing the card or automatically retrying. |
28
+ | [profile-memory.ts](./profile-memory.ts) | Read/save the original Task user's ordinary Profile; opt into memory | Obtain only the original required scopes and current user permission. Read Profile/recall results from `.data`; `runWithMemory` instead returns `{run,memory}`. |
29
+ | [agents.ts](./agents.ts) | Create, read back and bind the same Personal Agent provisioning receipt | Persist the full initial request/idempotencyKey before create and the provisioningId after receiving it. Bind only `state: ready, ready: true` with the current human accessToken. |
30
+ | [failure-handling.ts](./failure-handling.ts) | Read a known run, stop local waiting and classify errors without secret payloads | Handle consent changes through the supported user permission UI. A local abort is not a server cancel receipt; no SDK consent-revoke method is invented here. |
31
+
32
+ `signInOnOriginalCard` waits for your UI callback, then calls `confirmSignedIn()` on that same handle. For the backend-declared `current_task` strategy, this keeps the original L/C and performs one save/release/reply. The returned example state is `awaiting_agent_verification`; it is not proof of a signed-in page. Do not call deprecated `login.confirm()` first. The SDK also retains its legacy login strategy where declared by the backend.
33
+
34
+ `submitOrdinaryForm` takes values rendered from the original `payload.fields`: preserve field names and select option values, keep numbers as numbers and use an address object. An ordinary form does not write Profile fields. Profile's actual allowlist is documented in [ordinary-task-profile.md](../ordinary-task-profile.md); payment/card data, sensitive values and phone/email verification are outside that adapter.
35
+
36
+ `startWithMemory` needs the original delegation to cover both GUM read and Do Anything manage. Ordinary `run()` still performs no recall. A failed recall prevents Task creation; after a Task POST starts, a timeout can mean the write committed. The unified candidate can project per-record sources from a matching A1 r2 backend with complete coverage. Missing coverage retains source-less legacy facts/summary; empty `sources` must stay empty. Content takes priority over source metadata in the byte budget; omitted source sets are marked unavailable and `truncated`. A stored history URL is not a verified fact citation.
37
+
38
+ Agent provisioning is a candidate API whose backend business validation is incomplete. `creating`/`mail_pending` and `ready: false` are pending, not successful binding or provider acceptance. There is no public provisioning state called `unknown`; response loss and errors can still leave an unknown outcome. Keep the original key/receipt and use `get` when the original ID is known. Do not create a replacement with a new key. Optional mailbox provisioning does not establish a public mail-send SDK method or supplier support.
39
+
40
+ ## Failures and the next step
41
+
42
+ No example automatically retries a POST. `error.retryable` does not prove a write was rolled back. A hosted code with unknown exchange outcome needs a new hosted-login attempt; a known Profile/run/provisioning write requires its own original-resource readback. A 409 from an old control ID remains a 409. Extra permissions require explicit user consent; do not regrant removed scopes after narrowing or revocation.
43
+
44
+ These examples do not establish full one-hour credential/browser/provider continuity, universal action approval, or sensitive-value isolation across open browser/bash/harness channels. Read the matching backend prerequisites before integrating into your application. Keep the published version separate from this exact candidate; typed source fields require the matching backend projection.
@@ -0,0 +1,44 @@
1
+ # 候选 SDK 示例
2
+
3
+ 这些示例说明如何使用候选 SDK,并区分等待中、登录态已保存、写入结果未知与业务成功。它们针对**未发布本地候选包**;npm 发布包即使同样显示 0.9.0,也不代表包含这些能力。[English](./README.md)
4
+
5
+ ## 前置条件
6
+
7
+ 需要 Node.js 18+、fetch、此次集成指定的候选 tarball,以及独立可写的示例目录。正式应用的 AK/SK 和用户 token 留在可信服务端。示例函数接受已有 `Qoni` client,导入文件本身不会发请求。
8
+
9
+ 本地 consumer 只使用合成凭据与临时 loopback HTTP 服务,不连接真实 GenAuth、Web Agent、GUMem、浏览器或邮件供应商。HTTP 回包是后端替身;验证的是 SDK 用法,不是生产鉴权或后端业务验收。
10
+
11
+ ## 本地运行
12
+
13
+ 把本示例目录的内容复制到 SDK 工作区之外的新可写目录。安装**此次指定候选包**,不要安装未固定的 npm 包:
14
+
15
+ ```bash
16
+ npm install --ignore-scripts /absolute/path/to/reviewed-qoni-candidate.tgz
17
+ npm run smoke
18
+ ```
19
+
20
+ 示例 private package 同时安装 TypeScript 与 Node 类型。`smoke` 用实际安装包的公开 DTS 编译五个 `.ts` 文件,检查 ESM/CommonJS imports,再执行 loopback consumer。结果写入 `local-results.json`:12 项须全部 PASS,`resources.server_closed` 须为 true。夹具不发真实邮件、不购买。
21
+
22
+ ## 逐个接入函数
23
+
24
+ | 文件 | 完成什么 | 应用负责什么 |
25
+ | --- | --- | --- |
26
+ | [hosted-login.ts](./hosted-login.ts) | 生成托管登录 URL,校验 state 后仅兑换一次 code | 提供绑定原浏览器会话、可原子匹配并消费 state 的存储;登记精确 redirect URI;返回 token 留在服务端,不写日志。 |
27
+ | [interactions.ts](./interactions.ts) | 保留原登录/接管句柄,提交声明的普通表单字段 | 传当前 run 的 `onInteraction` 句柄,只展示给原用户;保留原 ID,错误后不换卡、不自动重试。 |
28
+ | [profile-memory.ts](./profile-memory.ts) | 原 Task 本人的普通资料读写,显式选择记忆召回 | 只取得原始所需 scope 与当前用户许可。Profile/recall 从 `.data` 读;`runWithMemory` 直接返回 `{run,memory}`。 |
29
+ | [agents.ts](./agents.ts) | 创建、读回并绑定同一个 Agent 编排回执 | create 前保存完整原请求/idempotencyKey,收到后保存 provisioningId;仅 `state: ready, ready: true` 时用当前本人 accessToken 绑定。 |
30
+ | [failure-handling.ts](./failure-handling.ts) | 查询已知 run、停止本地等待、仅输出不含秘密的错误分类 | 同意变更走已支持的用户权限界面;本地 abort 不是服务端取消回执,这里不发明 SDK revoke 方法。 |
31
+
32
+ `signInOnOriginalCard` 等待你的 UI callback,然后在同一个 handle 上调用 `confirmSignedIn()`。后端明确声明 `current_task` 时,SDK 保持原 L/C,只做一次保存/交还/回复。示例返回 `awaiting_agent_verification`,不代表网页已经登录;不要先调用已弃用的 `login.confirm()`。后端声明旧策略时,SDK 仍保留相应历史登录流程。
33
+
34
+ `submitOrdinaryForm` 接收按原 `payload.fields` 渲染的值:字段名和 option value 不改,数字仍为 number,地址为对象。普通表单不会写 Profile。Profile 字段以[实际白名单](../ordinary-task-profile.md)为准;支付/卡片、敏感值及手机号/邮箱验证不在普通适配器内。
35
+
36
+ `startWithMemory` 要求原委托同时覆盖 GUM read 和 Do Anything manage;普通 `run()` 不会召回。召回失败阻止创建 Task;Task POST 开始后再超时,可能已经写入。统一候选可消费配套 A1 r2 后端完整覆盖的逐记录来源;缺覆盖证明时保留无来源 legacy facts/summary。字节预算先保正文,省略的整组来源标为 unavailable 并设置 `truncated`;空 `sources` 不能补成猜测的引用;历史记录里的 URL 不等于事实来源已核验。
37
+
38
+ Agent 编排是候选 API,后端业务验证尚未完成。`creating`/`mail_pending`、`ready: false` 不代表绑定成功或供应商验收。公开 provisioning state 没有 `unknown`,但丢失回包和错误仍可使结果未知。保留原 key/回执,已知原 ID 时用 `get` 读回,不换新 key 创建替代对象。可选邮箱编排也不意味着存在公开 mail-send SDK 方法或已支持外部供应商。
39
+
40
+ ## 失败与下一步
41
+
42
+ 所有示例都不自动重试 POST;`error.retryable` 不能证明写入已回滚。hosted code 兑换未知须重新开始登录;已知 Profile/run/provisioning 写入按各自原对象查询。旧接管 ID 的 409 保持失败。新增权限须取得明确同意;用户缩窄或撤销后不能重新扩大原授权。
43
+
44
+ 这些示例不证明凭据/浏览器/供应商可连续等待一小时,也不证明开放 browser/bash/harness 的通用动作批准或敏感值隔离。接入正式应用前核对配套后端前置条件;已发布版本与本次候选应分开;typed 来源字段需要匹配的后端投影。
@@ -0,0 +1,26 @@
1
+ import type { AgentProvisioning, CreatePersonalAgentInput, Qoni } from '@qoniai/qoni';
2
+
3
+ /** The app must persist this exact request and key before calling create.
4
+ * Candidate interface only: external mailbox-provider readiness is unverified.
5
+ */
6
+ export async function createPersonalAgent(qoni: Qoni, savedRequest: CreatePersonalAgentInput) {
7
+ return qoni.genauth.agents.create(savedRequest); // One write; no automatic retry.
8
+ }
9
+
10
+ export async function readOriginalProvisioning(qoni: Qoni, provisioningId: string) {
11
+ return qoni.genauth.agents.get({ provisioningId }); // Same original receipt.
12
+ }
13
+
14
+ export async function bindReadyPersonalAgent(qoni: Qoni, receipt: AgentProvisioning, currentUserAccessToken: string) {
15
+ if (receipt.state !== 'ready' || !receipt.ready) throw new Error('Agent is not ready for binding');
16
+ return qoni.genauth.agents.bind({ provisioningId: receipt.provisioningId, accessToken: currentUserAccessToken });
17
+ }
18
+
19
+ export function provisioningProgress(receipt: AgentProvisioning) {
20
+ if (receipt.state === 'bound' && receipt.ready) return 'bound';
21
+ if (receipt.state === 'ready' && receipt.ready) return 'ready_to_bind';
22
+ if (receipt.state === 'creating' || receipt.state === 'mail_pending') return 'pending';
23
+ return 'inspect_original_operation';
24
+ // There is no public state='unknown' in this DTS. A lost/invalid response is
25
+ // still unknown; do not invent another Agent/key or call a pending receipt ready.
26
+ }
@@ -0,0 +1,21 @@
1
+ import { QoniError } from '@qoniai/qoni';
2
+ import type { Qoni } from '@qoniai/qoni';
3
+
4
+ /** Safe structured diagnostics; never serialize raw Error/body/cause/tokens. */
5
+ export function errorCategory(error: unknown) {
6
+ if (!(error instanceof QoniError)) return { category: 'unclassified' as const };
7
+ return { category: 'qoni_error' as const, status: error.status, retryable: error.retryable };
8
+ }
9
+
10
+ export async function readKnownRun(qoni: Qoni, originalToken: string, runId: string) {
11
+ // For timeout after a known Task ID, reattach/read; never call run() again.
12
+ const run = await qoni.doAnything.attach(runId, { token: originalToken });
13
+ return run.status();
14
+ }
15
+
16
+ /** Application hook, NOT an SDK revoke method and NOT a server stop receipt. */
17
+ export function stopLocalWaitingAfterConsentChange(localWait: AbortController) {
18
+ localWait.abort(new Error('Permissions changed; stop scheduling affected calls'));
19
+ // Use the supported user permission UI for narrowing/revocation. Respect
20
+ // backend denial; do not silently regrant removed scopes or switch identity.
21
+ }
@@ -0,0 +1,40 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import type { Qoni } from '@qoniai/qoni';
3
+
4
+ export interface LoginAttempt {
5
+ state: string;
6
+ redirectUri: string;
7
+ expiresAt: string;
8
+ }
9
+ /** Application-owned storage scoped to the original browser session.
10
+ * consume must atomically match/remove this session's state. Never use a global
11
+ * shared state value or let a caller choose another user's session namespace.
12
+ */
13
+ export interface LoginAttemptStore {
14
+ save(attempt: LoginAttempt): Promise<void>;
15
+ consume(state: string): Promise<LoginAttempt | undefined>;
16
+ }
17
+
18
+ export async function beginHostedLogin(qoni: Qoni, store: LoginAttemptStore, redirectUri: string) {
19
+ const state = randomUUID();
20
+ const page = await qoni.genauth.auth.createLoginUrl({ redirectUri, state, screen: 'signup' });
21
+ await store.save({ state, redirectUri, expiresAt: page.expiresAt });
22
+ return page; // Redirect only the original browser to page.url; do not log it.
23
+ }
24
+
25
+ export async function finishHostedLogin(
26
+ qoni: Qoni, store: LoginAttemptStore,
27
+ callback: { state?: unknown; code?: unknown; error?: unknown },
28
+ ) {
29
+ if (typeof callback.state !== 'string' || !callback.state) throw new Error('Invalid login state');
30
+ const attempt = await store.consume(callback.state);
31
+ if (!attempt || !Number.isFinite(Date.parse(attempt.expiresAt)) || Date.now() >= Date.parse(attempt.expiresAt)) {
32
+ throw new Error('Login attempt is missing or expired');
33
+ }
34
+ if (callback.error !== undefined || typeof callback.code !== 'string' || !callback.code) {
35
+ throw new Error('Login was not completed');
36
+ }
37
+ // State is consumed before this one write. On failure/unknown, start a new
38
+ // hosted login; do not replay this code. Save tokens in the server session.
39
+ return qoni.genauth.auth.exchangeLoginCode({ code: callback.code, redirectUri: attempt.redirectUri });
40
+ }
@@ -0,0 +1,30 @@
1
+ import type { FormValues, InteractionHandle, Qoni } from '@qoniai/qoni';
2
+
3
+ export async function signInOnOriginalCard(
4
+ handle: InteractionHandle, showToOriginalUser: (liveUrl: string) => Promise<void>,
5
+ ) {
6
+ if (handle.type !== 'site_login' || handle.status !== 'pending' ||
7
+ !handle.can('open_login') || !handle.can('confirm_signed_in')) {
8
+ throw new Error('The original card does not offer this sign-in flow');
9
+ }
10
+ const interactionId = handle.id;
11
+ const login = await handle.openLogin();
12
+ await showToOriginalUser(login.liveUrl); // Resolves only after this user's confirmation.
13
+ await handle.confirmSignedIn(); // Same handle/L/C, no lookup/reopen/Profile fallback.
14
+ return { interactionId, state: 'awaiting_agent_verification' as const };
15
+ }
16
+
17
+ export async function submitOrdinaryForm(handle: InteractionHandle, values: FormValues) {
18
+ if (handle.type !== 'fill_form' || handle.status !== 'pending' || !handle.can('submit')) {
19
+ throw new Error('The original card cannot accept this form');
20
+ }
21
+ // Render the original payload.fields; keys/options must match that card.
22
+ // Ordinary fields only. This submission does not save a Profile.
23
+ await handle.submit(values);
24
+ }
25
+
26
+ export async function releaseOriginalControl(qoni: Qoni, token: string, runId: string, interactionId: string) {
27
+ // Capture interactionId from the original control response/card. A 409 is
28
+ // propagated unchanged: never fetch a newer ID and retry this release.
29
+ return qoni.doAnything.api.intervene({ token, runId, interactionId, action: 'release_control' });
30
+ }
@@ -0,0 +1,159 @@
1
+ import assert from 'node:assert/strict';
2
+ import http from 'node:http';
3
+ import { createRequire } from 'node:module';
4
+ import { readFile, writeFile } from 'node:fs/promises';
5
+ import { createHash } from 'node:crypto';
6
+ import { pathToFileURL } from 'node:url';
7
+ import { Qoni, QoniError, QoniTimeoutError } from '@qoniai/qoni';
8
+ import * as login from './out/hosted-login.js';
9
+ import * as interactions from './out/interactions.js';
10
+ import * as profileMemory from './out/profile-memory.js';
11
+ import * as agents from './out/agents.js';
12
+ import * as failures from './out/failure-handling.js';
13
+
14
+ const installedCjs = createRequire(import.meta.url).resolve('@qoniai/qoni');
15
+ const installedEsm = new URL('./index.js', pathToFileURL(installedCjs));
16
+ const installedDist = {
17
+ esmSha256: createHash('sha256').update(await readFile(installedEsm)).digest('hex'),
18
+ cjsSha256: createHash('sha256').update(await readFile(installedCjs)).digest('hex'),
19
+ };
20
+ const calls=[],results=[];
21
+ let profileName='Synthetic', profileWrites=0, taskCreates=0, agentCreates=0, binds=0;
22
+ let dropExchange=false,dropProfile=false,dropTask=false,dropAgent=false,memoryDenied=false,writeDenied=false;
23
+ const jwt=p=>`${Buffer.from('{"alg":"none"}').toString('base64url')}.${Buffer.from(JSON.stringify(p)).toString('base64url')}.`;
24
+ const token=jwt({token_type:'eak_delegation_token',eak_tenant_id:'fixture-tenant'});
25
+ const productToken=jwt({webagent_tenant_id:'fixture-tenant',aud:'webagent'});
26
+ const receipt=state=>({provisioningId:'provision-original',state,ready:['ready','bound'].includes(state),
27
+ agentId:['ready','bound'].includes(state)?'agent-original':null,readyAt:['ready','bound'].includes(state)?1000:null,
28
+ unboundExpiresAt:state==='ready'?604801000:null,boundUserId:state==='bound'?'user-original':null,
29
+ boundAt:state==='bound'?2000:null,mailboxResourceId:null,errorCode:null});
30
+ let agentState='creating',origin;
31
+ const server=http.createServer(async(req,res)=>{
32
+ try {
33
+ const chunks=[];for await(const c of req)chunks.push(c);
34
+ const body=chunks.length?JSON.parse(Buffer.concat(chunks).toString()):undefined;
35
+ const path=new URL(req.url,origin).pathname;calls.push({path,method:req.method,body});
36
+ const send=(data,status=200)=>{res.writeHead(status,{'content-type':'application/json'});res.end(JSON.stringify({data}));};
37
+ const deny=()=>{res.writeHead(403,{'content-type':'application/json'});res.end(JSON.stringify({code:'delegation.forbidden',message:'Denied by fixture'}));};
38
+ if(path==='/api/v3/eak/token-exchange'){
39
+ if((memoryDenied&&body.resource==='gumem')||(writeDenied&&body.scopes.includes('webagent.do_anything:manage')))return deny();
40
+ return send({accessToken:productToken,expiresIn:60});
41
+ }
42
+ if(path.endsWith('/auth/login-url'))return send({url:origin+'/synthetic-login',expiresAt:new Date(Date.now()+60000).toISOString()});
43
+ if(path.endsWith('/auth/exchange-login-code')){
44
+ if(dropExchange){dropExchange=false;req.socket.destroy();return;}
45
+ return send({user:{id:'user-original',isNewUser:false},accessToken:'synthetic-user-token',idToken:'synthetic-id-token',expiresIn:60});
46
+ }
47
+ if(path.endsWith('/profile/read'))return send({values:{name:profileName},missing:['address'],writable_fields:['name'],field_types:{name:'STRING',address:'STRING'}});
48
+ if(path.endsWith('/profile/update')){
49
+ profileWrites++;profileName=body.values.name;
50
+ if(dropProfile){dropProfile=false;req.socket.destroy();return;}
51
+ return send({values:{name:profileName},missing:[],writable_fields:['name'],field_types:{name:'STRING'}});
52
+ }
53
+ if(path==='/api/sessions/memory-original/context')return send({context:{project_id:'project',user_id:'user-original',session_id:'memory-original',facts:'Synthetic preference',summary:'',history:[{id:'message-original',role:'user',content:'Synthetic history',metadata:{source_url:'https://example.invalid/source'}}]}});
54
+ if(path.endsWith('/do_anything/runs')&&req.method==='POST'){
55
+ taskCreates++;if(dropTask){dropTask=false;req.socket.destroy();return;}
56
+ return send({run_id:'run-original',session_id:'session-original',status:'running'},202);
57
+ }
58
+ if(path.endsWith('/runs/run-original')&&req.method==='GET')return send({run_id:'run-original',session_id:'session-original',status:'waiting'});
59
+ if(path.endsWith('/interactions/login-original/open_login'))return send({login_mode:'current_task',profile_id:'profile-original',site_id:'example.invalid',session_id:'control-original',interaction_id:'login-original',control_interaction_id:'control-original',browser_session_id:'browser-original',live_url:origin+'/synthetic-live',expires_at:new Date(Date.now()+60000).toISOString()});
60
+ if(path.endsWith('/interactions/login-original/confirm_signed_in'))return send({login_mode:'current_task',saved:true,interaction_id:'login-original',session_id:'control-original',state:'saved',verification:'agent_pending'});
61
+ if(path.endsWith('/interactions/form-original/submit'))return send({accepted:true});
62
+ if(path.endsWith('/intervene')){
63
+ res.writeHead(409,{'content-type':'application/json'});res.end(JSON.stringify({code:'interaction.conflict',message:'Original control is stale'}));return;
64
+ }
65
+ if(path==='/api/v3/eak/agents/create'){
66
+ agentCreates++;if(dropAgent){dropAgent=false;req.socket.destroy();return;}
67
+ return send(receipt('creating'),202);
68
+ }
69
+ if(path==='/api/v3/eak/agents/get')return send(receipt(agentState));
70
+ if(path==='/api/v3/eak/agents/bind'){binds++;return send(receipt('bound'));}
71
+ res.writeHead(404);res.end('{}');
72
+ }catch{res.writeHead(500);res.end('{}');}
73
+ });
74
+ await new Promise(resolve=>server.listen(0,'127.0.0.1',resolve));origin=`http://127.0.0.1:${server.address().port}`;
75
+ const qoni=new Qoni({accessKey:'synthetic-ak',secretKey:'synthetic-sk',host:origin,
76
+ genauthBaseUrl:origin,gumemBaseUrl:origin,webAgentBaseUrl:origin,timeoutMs:1500,sseMaxRetries:0});
77
+ const count=path=>calls.filter(c=>c.path.endsWith(path)).length;
78
+ const store=()=>{const values=new Map();return {save:async a=>{values.set(a.state,a);},consume:async state=>{const a=values.get(state);values.delete(state);return a;}};};
79
+ const attemptState=()=>calls.filter(c=>c.path.endsWith('/auth/login-url')).at(-1).body.state;
80
+ async function check(name,fn){try{const detail=await fn();results.push({name,result:'PASS',detail});}catch(error){results.push({name,result:'FAIL',error:error instanceof Error?error.message:'unclassified'});}console.log(JSON.stringify(results.at(-1)));}
81
+ try{
82
+ await check('esm_cjs_public_surface',async()=>{
83
+ const cjs=createRequire(import.meta.url)('@qoniai/qoni');const c=new cjs.Qoni({accessKey:'synthetic-ak',secretKey:'synthetic-sk',host:origin});
84
+ for(const candidate of [qoni,c])for(const f of [candidate.genauth.agents.create,candidate.genauth.agents.get,candidate.genauth.agents.bind,candidate.genauth.auth.createLoginUrl,candidate.doAnything.profile.read,candidate.doAnything.runWithMemory,candidate.gumem.recallContext])assert.equal(typeof f,'function');
85
+ return {formats:['ESM','CJS'],methods:7};
86
+ });
87
+ await check('hosted_state_and_single_exchange',async()=>{
88
+ const s=store();await login.beginHostedLogin(qoni,s,origin+'/callback');const state=attemptState();let before=count('/auth/exchange-login-code');
89
+ await assert.rejects(()=>login.finishHostedLogin(qoni,s,{state:'wrong',code:'synthetic-code'}));assert.equal(count('/auth/exchange-login-code'),before);
90
+ const session=await login.finishHostedLogin(qoni,s,{state,code:'synthetic-code'});assert.equal(session.user.id,'user-original');assert.equal(session.user.isNewUser,false);
91
+ await assert.rejects(()=>login.finishHostedLogin(qoni,s,{state,code:'synthetic-code'}));assert.equal(count('/auth/exchange-login-code'),before+1);
92
+ return {bad_state_posts:0,exchange_posts:1,replay_posts:0};
93
+ });
94
+ await check('hosted_unknown_not_replayed',async()=>{
95
+ const s=store();await login.beginHostedLogin(qoni,s,origin+'/callback');const state=attemptState();const before=count('/auth/exchange-login-code');dropExchange=true;
96
+ await assert.rejects(()=>login.finishHostedLogin(qoni,s,{state,code:'synthetic-lost-code'}));
97
+ await assert.rejects(()=>login.finishHostedLogin(qoni,s,{state,code:'synthetic-lost-code'}));assert.equal(count('/auth/exchange-login-code'),before+1);
98
+ return {unknown_exchange_posts:1};
99
+ });
100
+ await check('current_task_original_L_C',async()=>{
101
+ const run=await qoni.doAnything.attach('run-original',{token});const base='/do_anything/runs/run-original/interactions/login-original';
102
+ const handle=run.interactionHandle({id:'login-original',type:'site_login',status:'pending',createdAt:new Date().toISOString(),title:'Synthetic sign in',payload:{sites:[{siteId:'example.invalid',displayName:'Synthetic',loginUrl:'https://example.invalid'}]},actions:[{kind:'open_login',label:'Open',method:'POST',endpoint:base+'/open_login'},{kind:'confirm_signed_in',label:'Confirm',method:'POST',endpoint:base+'/confirm_signed_in'}]});
103
+ const before=count('/confirm_signed_in');const result=await interactions.signInOnOriginalCard(handle,async url=>assert.equal(url,origin+'/synthetic-live'));
104
+ assert.equal(result.interactionId,'login-original');assert.equal(result.state,'awaiting_agent_verification');assert.equal(count('/confirm_signed_in'),before+1);
105
+ const confirm=calls.findLast(c=>c.path.endsWith('/confirm_signed_in'));assert.deepEqual(confirm.body,{login_session_id:'control-original'});
106
+ assert.equal(calls.some(c=>c.path.includes('/profiles/')),false);
107
+ return {confirm_posts:1,original_L:'login-original',original_C:'control-original',verified_signin:false};
108
+ });
109
+ await check('stale_control_no_id_substitution',async()=>{
110
+ const before=count('/intervene');await assert.rejects(()=>interactions.releaseOriginalControl(qoni,token,'run-original','old-control'),e=>e instanceof QoniError&&e.status===409);
111
+ assert.equal(count('/intervene'),before+1);assert.equal(calls.findLast(c=>c.path.endsWith('/intervene')).body.interaction_id,'old-control');return {posts:1,status:409};
112
+ });
113
+ await check('ordinary_form_exact_values',async()=>{
114
+ const run=await qoni.doAnything.attach('run-original',{token});const handle=run.interactionHandle({id:'form-original',type:'fill_form',status:'pending',createdAt:new Date().toISOString(),title:'Synthetic form',payload:{title:'Synthetic',fields:[{name:'count',label:'Count',type:'number',required:true},{name:'method',label:'Method',type:'select',options:[{value:'ship',label:'Ship'}],required:true},{name:'address',label:'Address',type:'address',required:true}]},actions:[{kind:'submit',label:'Submit',method:'POST',endpoint:'/do_anything/runs/run-original/interactions/form-original/submit'}]});
115
+ const before=count('/submit');await assert.rejects(()=>interactions.submitOrdinaryForm(handle,{count:'0',method:'ship',address:{line1:'Synthetic'}}));assert.equal(count('/submit'),before);
116
+ await interactions.submitOrdinaryForm(handle,{count:0,method:'ship',address:{line1:'Synthetic',city:'Singapore'}});assert.equal(count('/submit'),before+1);assert.equal(calls.findLast(c=>c.path.endsWith('/submit')).body.values.count,0);
117
+ return {bad_input_posts:0,valid_posts:1};
118
+ });
119
+ await check('profile_unknown_readback_and_denied_consent',async()=>{
120
+ const p=await profileMemory.readOriginalUserProfile(qoni,token,'run-original');assert.equal(p.values.name,'Synthetic');assert.deepEqual(p.writable_fields,['name']);
121
+ const before=profileWrites;dropProfile=true;await assert.rejects(()=>profileMemory.saveExplicitOrdinaryName(qoni,token,'run-original','Saved synthetic'));assert.equal(profileWrites,before+1);
122
+ const reread=await profileMemory.readOriginalUserProfile(qoni,token,'run-original');assert.equal(reread.values.name,'Saved synthetic');assert.equal(profileWrites,before+1);
123
+ writeDenied=true;await assert.rejects(()=>profileMemory.saveExplicitOrdinaryName(qoni,token,'run-original','Forbidden synthetic'),e=>e instanceof QoniError&&e.status===403);writeDenied=false;
124
+ assert.equal(profileWrites,before+1);return {unknown_write_posts:1,explicit_readback:true,denied_write_posts:0};
125
+ });
126
+ await check('memory_envelope_opt_in_and_denied_recall',async()=>{
127
+ const m=await profileMemory.recallForDisplay(qoni,token,'memory-original');assert.equal(m.origin.sessionId,'memory-original');assert.deepEqual(m.items[0].sources,[]);assert.equal(m.items[0].sourceStatus,'unavailable');
128
+ const before=count('/context');await qoni.doAnything.run({token,prompt:'Ordinary synthetic run'});assert.equal(count('/context'),before);
129
+ const combined=await profileMemory.startWithMemory(qoni,token,'memory-original');assert.equal(combined.run.id,'run-original');assert.equal(combined.memory.origin.sessionId,'memory-original');assert.equal(count('/context'),before+1);
130
+ const taskBefore=taskCreates;memoryDenied=true;await assert.rejects(()=>profileMemory.startWithMemory(qoni,token,'memory-original'));memoryDenied=false;assert.equal(taskCreates,taskBefore);
131
+ return {ordinary_run_recall_posts:0,opt_in_recall_posts:1,denied_recall_task_posts:0};
132
+ });
133
+ await check('unknown_task_creation_no_replay',async()=>{
134
+ const before=taskCreates;dropTask=true;await assert.rejects(()=>profileMemory.startWithMemory(qoni,token,'memory-original'));assert.equal(taskCreates,before+1);return {unknown_task_posts:1};
135
+ });
136
+ await check('agents_pending_read_ready_bind',async()=>{
137
+ const request={displayName:'Synthetic assistant',idempotencyKey:'persisted-original-key'};
138
+ const created=await agents.createPersonalAgent(qoni,request);assert.equal(agents.provisioningProgress(created),'pending');const before=binds;
139
+ await assert.rejects(()=>agents.bindReadyPersonalAgent(qoni,created,'synthetic-user-token'));assert.equal(binds,before);
140
+ agentState='mail_pending';const pending=await agents.readOriginalProvisioning(qoni,created.provisioningId);assert.equal(agents.provisioningProgress(pending),'pending');
141
+ agentState='ready';const ready=await agents.readOriginalProvisioning(qoni,created.provisioningId);assert.equal(agents.provisioningProgress(ready),'ready_to_bind');
142
+ const bound=await agents.bindReadyPersonalAgent(qoni,ready,'synthetic-user-token');assert.equal(agents.provisioningProgress(bound),'bound');assert.equal(binds,before+1);
143
+ assert.equal(calls.findLast(c=>c.path.endsWith('/agents/get')).body.provisioningId,created.provisioningId);
144
+ return {pending_not_success:true,bind_posts:1,same_provisioning_id:true};
145
+ });
146
+ await check('agents_unknown_does_not_replace_key',async()=>{
147
+ const before=agentCreates;dropAgent=true;await assert.rejects(()=>agents.createPersonalAgent(qoni,{displayName:'Synthetic assistant',idempotencyKey:'persisted-uncertain-key'}));assert.equal(agentCreates,before+1);assert.equal(calls.findLast(c=>c.path.endsWith('/agents/create')).body.idempotencyKey,'persisted-uncertain-key');return {create_posts:1};
148
+ });
149
+ await check('failure_and_consent_handlers_do_not_write',async()=>{
150
+ const before=calls.length;const ctl=new AbortController();failures.stopLocalWaitingAfterConsentChange(ctl);assert.equal(ctl.signal.aborted,true);
151
+ const safe=failures.errorCategory(new QoniTimeoutError('synthetic private detail',{retryable:true,body:{token:'synthetic-private'}}));assert.equal(safe.retryable,true);assert.equal('body'in safe,false);assert.equal('message'in safe,false);assert.equal(calls.length,before);assert.equal('consents'in qoni.genauth,false);
152
+ const writes=calls.filter(c=>c.method==='POST').length;await failures.readKnownRun(qoni,token,'run-original');assert.equal(calls.filter(c=>c.method==='POST'&&!c.path.endsWith('/token-exchange')).length,writes-calls.slice(0,before).filter(c=>c.path.endsWith('/token-exchange')).length);
153
+ return {local_abort_is_not_server_cancel:true,automatic_posts:0};
154
+ });
155
+ }finally{
156
+ server.closeAllConnections();await new Promise(resolve=>server.close(resolve));
157
+ await writeFile(new URL('./local-results.json',import.meta.url),JSON.stringify({candidate:'installed local candidate; see npm install receipt',installedDist,node:process.version,cases:results,requests:calls.map(c=>({method:c.method,path:c.path})),resources:{server_closed:true},boundary:'Default fetch over real loopback TCP; synthetic backend responses/auth checks. No real GA/WA/GUM/PG/browser/provider.'},null,2)+'\n');
158
+ }
159
+ if(results.some(r=>r.result!=='PASS'))process.exitCode=1;
@@ -0,0 +1,20 @@
1
+ # Corrected memory-run error handling
2
+
3
+ Use [memory-outcome.ts](./memory-outcome.ts) only with the corrected candidate that exports `QoniTaskSubmissionUnknownError`, `QoniMemoryRunPhase` and `QoniError.phase/submissionUnknown`. [中文](./README.zh-CN.md)
4
+
5
+ The parent examples and this error example can use the same unified local candidate archive; older separate archives remain historical inputs. This additive error contract does not change `runWithMemory` inputs or its successful `{run,memory}` result, and does not retrofit ordinary `run()` errors.
6
+
7
+ `startMemoryRunOnce` submits at most once. `task_handle_returned` means a handle was returned, not that the Task completed. `submission_unknown` means the actual Task fetch was invoked but no definitive outcome was available: preserve the original request, do not automatically create a replacement and reconcile with the server where possible. It never claims the Task was definitely created.
8
+
9
+ `request_failed` preserves the SDK code/status, phase and retryable flag for explicit failures; that flag is not permission to repeat a write. `recall` and `task_permission` failures with `submissionUnknown: false` mean no Task POST was dispatched; explicit server rejection can also be `task_submit` with false. No body/cause/raw message is logged by this helper.
10
+
11
+ Install the corrected local archive in a separate consumer directory and compile against its real public DTS. Do not add a handwritten declaration or assume the original 0.9.0 candidate/npm package contains the new class. Backend/cloud validation remains separate.
12
+
13
+ Copy this subdirectory to its own writable consumer directory, then run:
14
+
15
+ ```bash
16
+ npm install --ignore-scripts /absolute/path/to/reviewed-m2-corrected-candidate.tgz
17
+ npm run smoke
18
+ ```
19
+
20
+ The local consumer uses synthetic loopback responses only. It checks nine cases: new ESM/CJS export, recall rejection, permission rejection, lost response, timeout, unusable success, explicit Task rejection, successful handle and omitted raw error payload. `results.json` must contain nine PASS results and a closed server. This is separate from the parent directory's 12 baseline cases.
@@ -0,0 +1,20 @@
1
+ # 修订的记忆任务错误处理
2
+
3
+ [memory-outcome.ts](./memory-outcome.ts) 仅适用于导出 `QoniTaskSubmissionUnknownError`、`QoniMemoryRunPhase` 和 `QoniError.phase/submissionUnknown` 的修订候选包。[English](./README.md)
4
+
5
+ 上级示例与本错误示例可使用同一统一本地候选包;此前分别固定的归档保持为历史输入。新增错误合同不改变 `runWithMemory` 输入与成功时的 `{run,memory}` 返回,也不改普通 `run()` 的错误。
6
+
7
+ `startMemoryRunOnce` 最多提交一次。`task_handle_returned` 只表示拿到句柄,不是 Task 已完成。`submission_unknown` 表示进入过实际 Task fetch、但没有明确结果;保留原请求,不自动创建替代任务,有条件时向服务端核对,不能宣称 Task 一定已创建。
8
+
9
+ `request_failed` 保留 SDK 的 code/status、phase 和 retryable;该标记不是写入重放许可。`recall`、`task_permission` 且 `submissionUnknown: false` 表示未派发 Task POST;明确服务端拒绝也可能是 `task_submit` 加 false。helper 不记录 body/cause/原始错误消息。
10
+
11
+ 在独立 consumer 目录安装修订本地包,用实际公开 DTS 编译。不要手写声明补齐缺失类型,也不要假定原 0.9.0 候选或 npm 包已有新类。后端与云验证另计。
12
+
13
+ 把本子目录复制到独立可写 consumer 目录,再运行:
14
+
15
+ ```bash
16
+ npm install --ignore-scripts /absolute/path/to/reviewed-m2-corrected-candidate.tgz
17
+ npm run smoke
18
+ ```
19
+
20
+ 本地 consumer 只用合成 loopback 回包,检查九项:新类 ESM/CJS 导出、召回拒绝、权限拒绝、回包丢失、超时、无可用成功结果、明确 Task 拒绝、成功返回句柄、错误不附原始数据。`results.json` 须九项全部 PASS 且 server 已关闭。这组与上级目录的 12 项原候选用例分开。
@@ -0,0 +1,53 @@
1
+ import assert from 'node:assert/strict';
2
+ import http from 'node:http';
3
+ import { writeFile } from 'node:fs/promises';
4
+ import { createRequire } from 'node:module';
5
+ import { Qoni, QoniTaskSubmissionUnknownError } from '@qoniai/qoni';
6
+ import { startMemoryRunOnce } from './out/memory-outcome.js';
7
+
8
+ const jwt=p=>`${Buffer.from('{"alg":"none"}').toString('base64url')}.${Buffer.from(JSON.stringify(p)).toString('base64url')}.`;
9
+ const original=jwt({token_type:'eak_delegation_token',eak_tenant_id:'fixture-tenant'});
10
+ const product=jwt({webagent_tenant_id:'fixture-tenant',aud:'webagent'});
11
+ let origin,mode='success',taskPosts=0;
12
+ const server=http.createServer(async(req,res)=>{
13
+ const chunks=[];for await(const c of req)chunks.push(c);const body=chunks.length?JSON.parse(Buffer.concat(chunks).toString()):undefined;
14
+ const path=new URL(req.url,origin).pathname;
15
+ const send=(data,status=200)=>{res.writeHead(status,{'content-type':'application/json'});res.end(JSON.stringify({data}));};
16
+ const deny=()=>{res.writeHead(403,{'content-type':'application/json'});res.end(JSON.stringify({code:'permission.denied',message:'Synthetic rejection'}));};
17
+ if(path==='/api/v3/eak/token-exchange'){
18
+ if(mode==='permission-denied'&&body.resource==='webagent')return deny();
19
+ if(mode==='recall-denied'&&body.resource==='gumem')return deny();
20
+ return send({accessToken:product,expiresIn:60});
21
+ }
22
+ if(path==='/api/sessions/memory-original/context')return send({context:{project_id:'project',user_id:'user-original',session_id:'memory-original',facts:'Synthetic memory',summary:'',history:[]}});
23
+ if(path.endsWith('/do_anything/runs')&&req.method==='POST'){
24
+ taskPosts++;
25
+ if(mode==='lost'){req.socket.destroy();return;}
26
+ if(mode==='timeout'){return;}
27
+ if(mode==='rejected')return deny();
28
+ if(mode==='invalid-success')return send({accepted:true});
29
+ return send({run_id:'run-original',session_id:'session-original',status:'running'},202);
30
+ }
31
+ res.writeHead(404);res.end('{}');
32
+ });
33
+ await new Promise(r=>server.listen(0,'127.0.0.1',r));origin=`http://127.0.0.1:${server.address().port}`;
34
+ const qoni=new Qoni({accessKey:'synthetic-ak',secretKey:'synthetic-sk',host:origin,genauthBaseUrl:origin,gumemBaseUrl:origin,webAgentBaseUrl:origin,timeoutMs:200,sseMaxRetries:0});
35
+ const input={token:original,prompt:'Synthetic memory run',memory:{sessionId:'memory-original'},timeoutMs:800};
36
+ const results=[];
37
+ async function check(name,fn){try{const detail=await fn();results.push({name,result:'PASS',detail});}catch(e){results.push({name,result:'FAIL',error:e instanceof Error?e.message:'unclassified'});}console.log(JSON.stringify(results.at(-1)));}
38
+ try{
39
+ await check('new_error_esm_cjs_export',async()=>{const cjs=createRequire(import.meta.url)('@qoniai/qoni');assert.equal(typeof QoniTaskSubmissionUnknownError,'function');assert.equal(typeof cjs.QoniTaskSubmissionUnknownError,'function');return {formats:2};});
40
+ for(const [failure,phase] of [['recall-denied','recall'],['permission-denied','task_permission']]){
41
+ await check(failure,async()=>{mode=failure;const before=taskPosts;const out=await startMemoryRunOnce(qoni,input);assert.equal(out.state,'request_failed');assert.equal(out.phase,phase);assert.equal(out.submissionUnknown,false);assert.equal(out.status,403);assert.equal(taskPosts,before);return {phase,task_posts:0};});
42
+ }
43
+ for(const failure of ['lost','timeout','invalid-success']){
44
+ await check(failure,async()=>{mode=failure;const before=taskPosts;const out=await startMemoryRunOnce(qoni,input);assert.equal(out.state,'submission_unknown');assert.equal(out.code,'task.submit_unknown');assert.equal(out.phase,'task_submit');assert.equal(out.submissionUnknown,true);assert.equal(out.retryable,false);assert.equal(taskPosts,before+1);return {phase:'task_submit',task_posts:1,automatic_replay:false};});
45
+ }
46
+ await check('explicit_rejection_is_not_unknown',async()=>{mode='rejected';const before=taskPosts;const out=await startMemoryRunOnce(qoni,input);assert.equal(out.state,'request_failed');assert.equal(out.phase,'task_submit');assert.equal(out.submissionUnknown,false);assert.equal(out.status,403);assert.notEqual(out.code,'task.submit_unknown');assert.equal(taskPosts,before+1);return {received_status:403,submissionUnknown:false};});
47
+ await check('successful_handle_is_not_completion',async()=>{mode='success';const out=await startMemoryRunOnce(qoni,input);assert.equal(out.state,'task_handle_returned');assert.equal(out.result.run.id,'run-original');assert.equal(out.result.memory.origin.sessionId,'memory-original');return {task_handle_returned:true};});
48
+ await check('unknown_error_omits_raw_payload',async()=>{mode='lost';await assert.rejects(()=>qoni.doAnything.runWithMemory(input),e=>{assert.ok(e instanceof QoniTaskSubmissionUnknownError);assert.equal(e.body,undefined);assert.equal(e.cause,undefined);assert.equal(e.message,'Task submission outcome is unknown. Do not resubmit automatically; reconcile the original request with the server.');return true;});return {body:false,cause:false};});
49
+ }finally{
50
+ server.closeAllConnections();await new Promise(r=>server.close(r));
51
+ await writeFile(new URL('./results.json',import.meta.url),JSON.stringify({node:process.version,results,resources:{server_closed:true},boundary:'Actual package and default fetch to local TCP; synthetic backend rejection/response faults, no real product authorization.'},null,2)+'\n');
52
+ }
53
+ if(results.some(x=>x.result!=='PASS'))process.exitCode=1;
@@ -0,0 +1,22 @@
1
+ import { QoniError, QoniTaskSubmissionUnknownError } from '@qoniai/qoni';
2
+ import type { DoAnythingRunWithMemoryOptions, Qoni } from '@qoniai/qoni';
3
+
4
+ /** Corrected candidate only. This handler never resubmits and never logs errors. */
5
+ export async function startMemoryRunOnce(qoni: Qoni, input: DoAnythingRunWithMemoryOptions) {
6
+ try {
7
+ return { state: 'task_handle_returned' as const, result: await qoni.doAnything.runWithMemory(input) };
8
+ } catch (error) {
9
+ if (error instanceof QoniTaskSubmissionUnknownError) {
10
+ return { state: 'submission_unknown' as const, phase: error.phase,
11
+ code: error.code, submissionUnknown: error.submissionUnknown, retryable: false as const };
12
+ }
13
+ if (error instanceof QoniError) {
14
+ // Keep the original class/code in the caller if needed. The display below
15
+ // uses no body, cause, raw message or token and never infers success.
16
+ return { state: 'request_failed' as const, phase: error.phase,
17
+ code: error.code, status: error.status, submissionUnknown: error.submissionUnknown,
18
+ retryable: error.retryable };
19
+ }
20
+ throw error; // No automatic retry, including unclassified failures.
21
+ }
22
+ }
@@ -0,0 +1 @@
1
+ {"name":"qoni-memory-error-candidate-example","private":true,"type":"module","scripts":{"typecheck":"tsc -p tsconfig.json","smoke":"npm run typecheck && node local-consumer.mjs"},"devDependencies":{"typescript":"5.9.3","@types/node":"20.19.41"}}
@@ -0,0 +1 @@
1
+ {"compilerOptions":{"target":"ES2022","module":"NodeNext","moduleResolution":"NodeNext","strict":true,"skipLibCheck":false,"rootDir":".","outDir":"out","types":["node"]},"include":["memory-outcome.ts"]}
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "qoni-sdk-candidate-examples", "private": true, "type": "module",
3
+ "scripts": {"typecheck": "tsc -p tsconfig.json", "smoke": "npm run typecheck && node local-consumer.mjs"},
4
+ "devDependencies": {"typescript": "5.9.3", "@types/node": "20.19.41"}
5
+ }
@@ -0,0 +1,30 @@
1
+ import type { Qoni } from '@qoniai/qoni';
2
+
3
+ export async function readOriginalUserProfile(qoni: Qoni, token: string, runId: string) {
4
+ const { data } = await qoni.doAnything.profile.read({ token, runId, fields: ['name', 'address'] });
5
+ return data; // Current user's authorized UI only; do not log private profile values.
6
+ }
7
+
8
+ export async function saveExplicitOrdinaryName(qoni: Qoni, token: string, runId: string, name: string) {
9
+ // Caller has explicitly chosen to save and obtained original user.profile:write.
10
+ // Backend checks the original Task and current field permission. One write only.
11
+ const { data } = await qoni.doAnything.profile.update({ token, runId, values: { name } });
12
+ return data;
13
+ }
14
+
15
+ export async function recallForDisplay(qoni: Qoni, token: string, sessionId: string) {
16
+ const { data: context } = await qoni.gumem.recallContext({
17
+ token, sessionId, query: 'confirmed preferences', maxItems: 8, maxContextBytes: 4096,
18
+ });
19
+ // facts/summary can have sources=[] and sourceStatus='unavailable'.
20
+ // Stored history URLs are message metadata, not verified fact citations.
21
+ return context;
22
+ }
23
+
24
+ export async function startWithMemory(qoni: Qoni, originalDelegationToken: string, sessionId: string) {
25
+ return qoni.doAnything.runWithMemory({
26
+ token: originalDelegationToken, prompt: 'Use my confirmed preferences to plan a synthetic itinerary.',
27
+ memory: { sessionId, query: 'confirmed preferences', maxItems: 8, maxContextBytes: 4096 },
28
+ timeoutMs: 30_000,
29
+ }); // { run, memory }; does not opt ordinary run() into recall.
30
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "compilerOptions": {"target":"ES2022","module":"NodeNext","moduleResolution":"NodeNext","strict":true,"skipLibCheck":false,"rootDir":".","outDir":"out","types":["node"]},
3
+ "include": ["*.ts"]
4
+ }