@somacheck/vibecheck 0.6.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -13
- package/dist/api.js +7 -6
- package/dist/client-setup.js +10 -4
- package/dist/constants.js +1 -1
- package/dist/server.js +122 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ Your agent has your context. It does not have your gut. This is the bridge.
|
|
|
9
9
|
|
|
10
10
|
Fair. Here is what you need, without executing anything.
|
|
11
11
|
|
|
12
|
-
**What the `link` command does.** `npx -y @somacheck/vibecheck@0.6.
|
|
12
|
+
**What the `link` command does.** `npx -y @somacheck/vibecheck@0.6.1 link <CODE> --client <CLIENT>`
|
|
13
13
|
exchanges a short-lived pairing code, generated inside the SomaCheck iOS app, for
|
|
14
14
|
a bearer token. It writes that token to `~/.sensie/config.json` with mode `0600`,
|
|
15
15
|
offers to configure supported agent clients found on the computer, and checks the
|
|
@@ -78,9 +78,9 @@ before the pairing code is redeemed.
|
|
|
78
78
|
To configure or repair a client later:
|
|
79
79
|
|
|
80
80
|
```text
|
|
81
|
-
npx -y @somacheck/vibecheck@0.6.
|
|
82
|
-
npx -y @somacheck/vibecheck@0.6.
|
|
83
|
-
npx -y @somacheck/vibecheck@0.6.
|
|
81
|
+
npx -y @somacheck/vibecheck@0.6.1 setup codex
|
|
82
|
+
npx -y @somacheck/vibecheck@0.6.1 setup claude
|
|
83
|
+
npx -y @somacheck/vibecheck@0.6.1 doctor
|
|
84
84
|
```
|
|
85
85
|
|
|
86
86
|
Manual registration remains available:
|
|
@@ -90,13 +90,13 @@ Manual registration remains available:
|
|
|
90
90
|
"mcpServers": {
|
|
91
91
|
"vibecheck": {
|
|
92
92
|
"command": "npx",
|
|
93
|
-
"args": ["-y", "@somacheck/vibecheck@0.6.
|
|
93
|
+
"args": ["-y", "@somacheck/vibecheck@0.6.1", "serve", "--client", "codex"]
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
```
|
|
98
98
|
|
|
99
|
-
For Claude Code: `claude mcp add --scope user vibecheck -- npx -y @somacheck/vibecheck@0.6.
|
|
99
|
+
For Claude Code: `claude mcp add --scope user vibecheck -- npx -y @somacheck/vibecheck@0.6.1 serve --client claude`
|
|
100
100
|
|
|
101
101
|
The link step writes only the bearer token in `~/.sensie/config.json`. The client
|
|
102
102
|
setup step asks Codex or Claude to add the pinned MCP command to that client's own
|
|
@@ -116,7 +116,7 @@ will explain it and leave it alone. The canonical local MCP key is `vibecheck`.
|
|
|
116
116
|
| `share_somacheck_context` | Share 1–20 bounded, derived, user-authorized observations; raw conversations and secrets are rejected. |
|
|
117
117
|
| `post_vibecheck_statement` | Add one to three personalized statements without blocking; returns a stable `request_id` for each. |
|
|
118
118
|
| `get_vibecheck_result` | Read that exact proposition once by `request_id`; returns queued, pending, or its terminal result. |
|
|
119
|
-
| `request_vibecheck` |
|
|
119
|
+
| `request_vibecheck` | With per-ask consent, send one immediate phone vibecheck, wait boundedly for its exact answer, and retain an opaque `live:<uuid>` fallback handle. |
|
|
120
120
|
|
|
121
121
|
## How to use it well
|
|
122
122
|
|
|
@@ -126,17 +126,29 @@ prompt is **a statement for the person to test**, not a question about facts:
|
|
|
126
126
|
something to test, never as a claim about them. If it is wrong, their body says
|
|
127
127
|
`unaligned`, and that is a useful answer rather than a failure.
|
|
128
128
|
|
|
129
|
-
|
|
129
|
+
Setup and linking are not permission for future asks. If the person's current
|
|
130
|
+
message explicitly asks you to use SomaCheck and includes the exact statement,
|
|
131
|
+
that is consent for this one vibecheck; do not ask them to confirm twice.
|
|
132
|
+
Otherwise, first show the exact statement and invite the person plainly:
|
|
130
133
|
|
|
131
134
|
> Want a quick vibecheck before I continue? I'll send this statement to your
|
|
132
135
|
> phone; you answer with your wrist gesture, and I'll use the result as a
|
|
133
136
|
> signal—not a verdict.
|
|
134
137
|
|
|
135
|
-
Call `request_vibecheck`
|
|
136
|
-
|
|
137
|
-
same statement. The
|
|
138
|
-
|
|
139
|
-
|
|
138
|
+
Call `request_vibecheck` with `human_confirmed: true` and a new UUID in
|
|
139
|
+
`idempotency_key` only after one of those per-ask consent paths. Reuse that UUID
|
|
140
|
+
only when retrying the same statement. The tool keeps the one created request
|
|
141
|
+
open for a bounded wait and returns `completed` when the exact answer arrives.
|
|
142
|
+
On timeout or a result-read failure it returns the same pending `live:<uuid>`
|
|
143
|
+
handle; pass that handle to `get_vibecheck_result` once later. Caller
|
|
144
|
+
cancellation stops result reads and preserves the backend request and
|
|
145
|
+
idempotency key, but the cancelled MCP transport may suppress that pending
|
|
146
|
+
response. Do not continuously poll or re-ask the same decision to obtain a
|
|
147
|
+
preferred result.
|
|
148
|
+
|
|
149
|
+
The 45-second whole-call deadline also covers the one create request. If that
|
|
150
|
+
network request is aborted after an ambiguous commit, retry the exact same
|
|
151
|
+
statement with the same `idempotency_key`; never create a replacement ask.
|
|
140
152
|
|
|
141
153
|
After a completed result, explain what the signal changes in your next action.
|
|
142
154
|
Never treat it as authorization or say that the person's body made the
|
package/dist/api.js
CHANGED
|
@@ -334,7 +334,7 @@ export class SupabaseAgentApi {
|
|
|
334
334
|
const row = await this.#rpc("agent_link_redeem", { code });
|
|
335
335
|
return requiredString(row, "token");
|
|
336
336
|
}
|
|
337
|
-
async requestVibecheck(token, identity, statement, idempotencyKey) {
|
|
337
|
+
async requestVibecheck(token, identity, statement, idempotencyKey, signal) {
|
|
338
338
|
if (identity.kind !== "local")
|
|
339
339
|
throw new SomaCheckLiveAskClientError();
|
|
340
340
|
const clientKey = identity.client_key;
|
|
@@ -343,9 +343,9 @@ export class SupabaseAgentApi {
|
|
|
343
343
|
statement,
|
|
344
344
|
idempotency_key: idempotencyKey,
|
|
345
345
|
client_key: clientKey,
|
|
346
|
-
}));
|
|
346
|
+
}, signal));
|
|
347
347
|
const lifecycle = created.status === "answered"
|
|
348
|
-
? await this.liveVibecheckResult(token, identity, created.request_id)
|
|
348
|
+
? await this.liveVibecheckResult(token, identity, created.request_id, signal)
|
|
349
349
|
: { status: created.status, verdict: null, confidence: null, latency_s: null };
|
|
350
350
|
return {
|
|
351
351
|
request_id: `live:${created.request_id}`,
|
|
@@ -364,14 +364,14 @@ export class SupabaseAgentApi {
|
|
|
364
364
|
async pollRequest(token, requestId) {
|
|
365
365
|
return decodePollResponse(await this.#rpc("agent_proposition_result", { token, proposition_id: requestId }));
|
|
366
366
|
}
|
|
367
|
-
async liveVibecheckResult(token, identity, requestId) {
|
|
367
|
+
async liveVibecheckResult(token, identity, requestId, signal) {
|
|
368
368
|
if (identity.kind !== "local")
|
|
369
369
|
throw new SomaCheckLiveAskClientError();
|
|
370
370
|
return decodeLiveVibecheckResult(await this.#rpcJson("agent_vibecheck_result", {
|
|
371
371
|
token,
|
|
372
372
|
live_ask_id: requestId,
|
|
373
373
|
client_key: identity.client_key,
|
|
374
|
-
}), requestId);
|
|
374
|
+
}, signal), requestId);
|
|
375
375
|
}
|
|
376
376
|
async statusRequest(token) {
|
|
377
377
|
return decodeVibecheckStatus(await this.#rpc("agent_proposition_cache_status", { token }));
|
|
@@ -418,7 +418,7 @@ export class SupabaseAgentApi {
|
|
|
418
418
|
async #rpc(name, body) {
|
|
419
419
|
return firstRow(await this.#rpcJson(name, body));
|
|
420
420
|
}
|
|
421
|
-
async #rpcJson(name, body) {
|
|
421
|
+
async #rpcJson(name, body, signal) {
|
|
422
422
|
const response = await this.#fetch(`${this.#endpoint}/rest/v1/rpc/${name}`, {
|
|
423
423
|
method: "POST",
|
|
424
424
|
headers: {
|
|
@@ -429,6 +429,7 @@ export class SupabaseAgentApi {
|
|
|
429
429
|
"accept-profile": "somacheck_engine",
|
|
430
430
|
},
|
|
431
431
|
body: JSON.stringify(body),
|
|
432
|
+
...(signal === undefined ? {} : { signal }),
|
|
432
433
|
});
|
|
433
434
|
if (!response.ok) {
|
|
434
435
|
const responseBody = await response.text().catch(() => "");
|
package/dist/client-setup.js
CHANGED
|
@@ -91,6 +91,8 @@ function registrationMatches(client, stdout) {
|
|
|
91
91
|
return false;
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
|
+
if (/^\s*(?:Enabled|Status):\s*(?:false|disabled|off)\s*$/im.test(stdout))
|
|
95
|
+
return false;
|
|
94
96
|
const command = /^\s*Command:\s*(\S+)\s*$/im.exec(stdout)?.[1];
|
|
95
97
|
const rawArgs = /^\s*Args:\s*(.*?)\s*$/im.exec(stdout)?.[1] ?? "";
|
|
96
98
|
const parsedArgs = rawArgs.split(/\s+/).filter(Boolean);
|
|
@@ -117,6 +119,8 @@ function isManagedSomaCheckRegistration(client, stdout) {
|
|
|
117
119
|
}
|
|
118
120
|
}
|
|
119
121
|
else {
|
|
122
|
+
if (/^\s*(?:Enabled|Status):\s*(?:false|disabled|off)\s*$/im.test(stdout))
|
|
123
|
+
return false;
|
|
120
124
|
command = /^\s*Command:\s*(\S+)\s*$/im.exec(stdout)?.[1];
|
|
121
125
|
args = (/^\s*Args:\s*(.*?)\s*$/im.exec(stdout)?.[1] ?? "").split(/\s+/).filter(Boolean);
|
|
122
126
|
}
|
|
@@ -131,10 +135,12 @@ function isManagedSomaCheckRegistration(client, stdout) {
|
|
|
131
135
|
if (legacyWithoutClient)
|
|
132
136
|
return true;
|
|
133
137
|
// 0.5 introduced the exact local runtime identity required by Live Ask.
|
|
134
|
-
// It is safe to replace only the canonical command generated by
|
|
135
|
-
// release. Custom wrappers, extra arguments,
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
+
// It is safe to replace only the canonical command generated by 0.5 or the
|
|
139
|
+
// exact 0.6.0 release being patched. Custom wrappers, extra arguments,
|
|
140
|
+
// disabled entries, later versions, and registrations naming the other
|
|
141
|
+
// client remain user-owned.
|
|
142
|
+
const previousExactClient = (/^@somacheck\/vibecheck@0\.5\.\d+$/.test(packageArg)
|
|
143
|
+
|| packageArg === "@somacheck/vibecheck@0.6.0")
|
|
138
144
|
&& args.length === 5
|
|
139
145
|
&& args[0] === "-y"
|
|
140
146
|
&& args[1] === packageArg
|
package/dist/constants.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
export const SUPABASE_URL = "https://pbldcmniommltbdwuykk.supabase.co";
|
|
3
3
|
export const SUPABASE_PUBLISHABLE_KEY = "sb_publishable_af-lUNI2FqEcb-oGy-4uxQ_cnm6kY85";
|
|
4
4
|
export const PACKAGE_NAME = "@somacheck/vibecheck";
|
|
5
|
-
export const PACKAGE_VERSION = "0.6.
|
|
5
|
+
export const PACKAGE_VERSION = "0.6.1";
|
|
6
6
|
export const PACKAGE_SPEC = `${PACKAGE_NAME}@${PACKAGE_VERSION}`;
|
|
7
7
|
export const MCP_SERVER_NAME = "vibecheck";
|
|
8
8
|
export const LEGACY_HOSTED_MCP_SERVER_NAME = "somacheck";
|
package/dist/server.js
CHANGED
|
@@ -3,6 +3,26 @@ import { z } from "zod";
|
|
|
3
3
|
import { StatementPendingError, } from "./vibecheck.js";
|
|
4
4
|
import { SomaCheckCompatibilityError, SomaCheckHttpError, SomaCheckLiveAskClientError, SomaCheckLiveAskConflictError, SomaCheckLiveAskPendingError, } from "./api.js";
|
|
5
5
|
import { PACKAGE_NAME, PACKAGE_SPEC, PACKAGE_VERSION } from "./constants.js";
|
|
6
|
+
const LIVE_ASK_WAIT_BUDGET_MS = 45_000;
|
|
7
|
+
const LIVE_ASK_POLL_DELAYS_MS = [
|
|
8
|
+
1_000, 1_000, 2_000, 2_000, 3_000, 3_000,
|
|
9
|
+
4_000, 4_000, 5_000, 5_000, 5_000, 5_000,
|
|
10
|
+
];
|
|
11
|
+
function abortAwareSleep(milliseconds, signal) {
|
|
12
|
+
if (signal.aborted || milliseconds <= 0)
|
|
13
|
+
return Promise.resolve();
|
|
14
|
+
return new Promise((resolve) => {
|
|
15
|
+
let timer;
|
|
16
|
+
const finish = () => {
|
|
17
|
+
if (timer !== undefined)
|
|
18
|
+
clearTimeout(timer);
|
|
19
|
+
signal.removeEventListener("abort", finish);
|
|
20
|
+
resolve();
|
|
21
|
+
};
|
|
22
|
+
timer = setTimeout(finish, milliseconds);
|
|
23
|
+
signal.addEventListener("abort", finish, { once: true });
|
|
24
|
+
});
|
|
25
|
+
}
|
|
6
26
|
const cadenceSchema = z.object({
|
|
7
27
|
reason: z.string(),
|
|
8
28
|
create_due_at: z.string().nullable(),
|
|
@@ -92,10 +112,13 @@ const SERVER_INSTRUCTIONS = [
|
|
|
92
112
|
"Call get_vibecheck_context and get_vibecheck_status at the start of a session or background run, and call status again after a completed check-in.",
|
|
93
113
|
"Treat gesture outcomes as contextual signals, never fixed facts or blanket authorization.",
|
|
94
114
|
"Use request_vibecheck when a current choice would benefit from the person's own felt response; never use it to evaluate someone else.",
|
|
95
|
-
"
|
|
115
|
+
"A setup or link is not blanket consent for future vibechecks.",
|
|
116
|
+
"If the person's current message explicitly tells you to request a SomaCheck vibecheck and includes the exact statement, that message is consent for that one ask; set human_confirmed to true and do not ask them to confirm twice.",
|
|
117
|
+
"Otherwise, first show the exact statement and ask: Want a quick vibecheck before I continue? I'll send this statement to your phone; you answer with your wrist gesture, and I'll use the result as a signal—not a verdict. Call only after the person accepts.",
|
|
96
118
|
"Use at most one vibecheck per decision. Never re-ask to obtain a preferred answer.",
|
|
97
119
|
"An immediate vibecheck is separate from the asynchronous reflection feed. Do not use it to replenish cached propositions.",
|
|
98
120
|
"Use a fresh idempotency_key for each new vibecheck, and reuse that exact key only when retrying the same statement.",
|
|
121
|
+
"request_vibecheck waits boundedly for the exact answer. If it returns completed, continue in the same turn and explain what changes. If it returns pending, retain that exact handle and read it once later; never create a replacement ask.",
|
|
99
122
|
"After a completed result, say what the signal changes in your next action. Never say 'your body decided'; a typed human response always overrides the signal.",
|
|
100
123
|
"When you have meaningful user-authorized context, call share_somacheck_context with concise derived observations before creating propositions.",
|
|
101
124
|
"Never share raw conversation text, photos, credentials, private identifiers, or diagnostic claims.",
|
|
@@ -257,14 +280,14 @@ export function createVibecheckServer(dependencies) {
|
|
|
257
280
|
});
|
|
258
281
|
server.registerTool("request_vibecheck", {
|
|
259
282
|
title: "Request a Vibecheck",
|
|
260
|
-
description: "Send one plain-language statement to the person's phone for a vibecheck answered with a wrist gesture.
|
|
283
|
+
description: "Send one plain-language statement to the person's phone for a vibecheck answered with a wrist gesture. A current user command that explicitly requests SomaCheck and includes the exact statement counts as confirmation for that one ask; do not ask twice. Otherwise, first show the exact statement and call only after the person accepts. Setup or linking is not blanket consent. Use this only when their own felt response would help with the current choice, not for routine feed content or to judge another person. The call waits boundedly for the exact result and returns completed when available. A timeout returns the same stable pending request_id for one later read. Caller cancellation stops result reads without creating another request; the cancelled transport may suppress the pending response.",
|
|
261
284
|
inputSchema: {
|
|
262
285
|
statement: z.string().trim().min(1).max(1000)
|
|
263
286
|
.describe("One plain-language statement for the person to test; offer a signal, not a fact, diagnosis, or instruction."),
|
|
264
287
|
idempotency_key: z.string().uuid()
|
|
265
288
|
.describe("A new UUID for this logical ask. Reuse the same UUID only to retry the exact same statement; retries will not create another phone request."),
|
|
266
289
|
human_confirmed: z.literal(true)
|
|
267
|
-
.describe("Confirms the
|
|
290
|
+
.describe("Confirms the person accepted this exact vibecheck, either by explicitly requesting SomaCheck with this statement in their current message or by accepting after the agent showed it."),
|
|
268
291
|
},
|
|
269
292
|
outputSchema: liveAskSchema,
|
|
270
293
|
annotations: {
|
|
@@ -273,30 +296,123 @@ export function createVibecheckServer(dependencies) {
|
|
|
273
296
|
idempotentHint: true,
|
|
274
297
|
openWorldHint: false,
|
|
275
298
|
},
|
|
276
|
-
}, async ({ statement, idempotency_key }) => {
|
|
299
|
+
}, async ({ statement, idempotency_key }, extra) => {
|
|
300
|
+
const now = dependencies.liveAskWait?.now ?? (() => performance.now());
|
|
301
|
+
const budgetMs = dependencies.liveAskWait?.budgetMs ?? LIVE_ASK_WAIT_BUDGET_MS;
|
|
302
|
+
const deadline = now() + budgetMs;
|
|
303
|
+
const callAbort = new AbortController();
|
|
304
|
+
const forwardCallerAbort = () => callAbort.abort(extra.signal.reason);
|
|
305
|
+
if (extra.signal.aborted) {
|
|
306
|
+
forwardCallerAbort();
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
extra.signal.addEventListener("abort", forwardCallerAbort, { once: true });
|
|
310
|
+
}
|
|
311
|
+
const callDeadline = setTimeout(() => callAbort.abort(new Error("Live Ask tool deadline reached.")), budgetMs);
|
|
277
312
|
try {
|
|
278
313
|
const identity = dependencies.identity ?? { kind: "legacy" };
|
|
279
314
|
if (identity.kind === "legacy") {
|
|
280
315
|
return liveAskFailure(new SomaCheckLiveAskClientError());
|
|
281
316
|
}
|
|
282
317
|
const token = await dependencies.loadToken();
|
|
283
|
-
const
|
|
318
|
+
const created = await dependencies.api.requestVibecheck(token, identity, statement.trim(), idempotency_key, callAbort.signal);
|
|
319
|
+
const result = await waitForLiveVibecheck(created, token, identity, dependencies, callAbort.signal, deadline, (attempt) => {
|
|
320
|
+
const progressToken = extra._meta?.progressToken;
|
|
321
|
+
if (progressToken === undefined)
|
|
322
|
+
return;
|
|
323
|
+
void extra.sendNotification({
|
|
324
|
+
method: "notifications/progress",
|
|
325
|
+
params: {
|
|
326
|
+
progressToken,
|
|
327
|
+
progress: attempt,
|
|
328
|
+
total: LIVE_ASK_POLL_DELAYS_MS.length,
|
|
329
|
+
message: "Waiting for the person's vibecheck signal.",
|
|
330
|
+
},
|
|
331
|
+
}).catch(() => {
|
|
332
|
+
// Progress is optional. A host that does not accept it must not
|
|
333
|
+
// prevent the exact result from completing this tool call.
|
|
334
|
+
});
|
|
335
|
+
});
|
|
284
336
|
const structuredContent = { ...result, error_code: null };
|
|
285
337
|
const text = result.state === "completed"
|
|
286
338
|
? `Vibecheck completed: ${Math.round((result.confidence ?? 0) * 100)}% ${result.verdict}. Treat this as a signal, say what it changes in your next action, and defer to any typed response from the person.`
|
|
287
339
|
: result.state === "pending"
|
|
288
340
|
? result.delivery_state === "failed" || result.delivery_state === "skipped"
|
|
289
341
|
? `Vibecheck ${result.request_id} was stored, but phone delivery is ${result.delivery_state}. It expires at ${result.expires_at}. Do not repost it or change the idempotency_key; check the phone setup before a later decision.`
|
|
290
|
-
: `Vibecheck
|
|
342
|
+
: `Vibecheck ${result.request_id} is still pending after a bounded wait. Keep this exact handle and check it once later with get_vibecheck_result. It expires at ${result.expires_at}; do not poll continuously or re-ask for this decision.`
|
|
291
343
|
: `This vibecheck is ${result.state}. Do not create another ask for the same decision.`;
|
|
292
344
|
return { content: [{ type: "text", text }], structuredContent };
|
|
293
345
|
}
|
|
294
346
|
catch (error) {
|
|
295
347
|
return liveAskFailure(error);
|
|
296
348
|
}
|
|
349
|
+
finally {
|
|
350
|
+
clearTimeout(callDeadline);
|
|
351
|
+
extra.signal.removeEventListener("abort", forwardCallerAbort);
|
|
352
|
+
}
|
|
297
353
|
});
|
|
298
354
|
return server;
|
|
299
355
|
}
|
|
356
|
+
async function waitForLiveVibecheck(created, token, identity, dependencies, signal, deadline, progress) {
|
|
357
|
+
if (created.state !== "pending"
|
|
358
|
+
|| created.delivery_state === "failed"
|
|
359
|
+
|| created.delivery_state === "skipped"
|
|
360
|
+
|| signal.aborted) {
|
|
361
|
+
return created;
|
|
362
|
+
}
|
|
363
|
+
const requestId = parseLiveVibecheckHandle(created.request_id);
|
|
364
|
+
if (requestId === null)
|
|
365
|
+
return created;
|
|
366
|
+
const now = dependencies.liveAskWait?.now ?? (() => performance.now());
|
|
367
|
+
const sleep = dependencies.liveAskWait?.sleep ?? abortAwareSleep;
|
|
368
|
+
for (const [index, delay] of LIVE_ASK_POLL_DELAYS_MS.entries()) {
|
|
369
|
+
const remaining = deadline - now();
|
|
370
|
+
if (signal.aborted || remaining <= 0)
|
|
371
|
+
return created;
|
|
372
|
+
await sleep(Math.min(delay, remaining), signal);
|
|
373
|
+
if (signal.aborted || now() >= deadline)
|
|
374
|
+
return created;
|
|
375
|
+
progress(index + 1);
|
|
376
|
+
if (signal.aborted)
|
|
377
|
+
return created;
|
|
378
|
+
let lifecycle;
|
|
379
|
+
const resultAbort = new AbortController();
|
|
380
|
+
const forwardCallerAbort = () => resultAbort.abort(signal.reason);
|
|
381
|
+
signal.addEventListener("abort", forwardCallerAbort, { once: true });
|
|
382
|
+
const resultTimeout = setTimeout(() => resultAbort.abort(new Error("Live Ask result deadline reached.")), Math.max(0, deadline - now()));
|
|
383
|
+
try {
|
|
384
|
+
lifecycle = await dependencies.api.liveVibecheckResult(token, identity, requestId, resultAbort.signal);
|
|
385
|
+
}
|
|
386
|
+
catch {
|
|
387
|
+
// Creation already succeeded. Keep the exact handle so a transient,
|
|
388
|
+
// cancelled, revoked, or deadline-aborted read cannot prompt a duplicate.
|
|
389
|
+
return created;
|
|
390
|
+
}
|
|
391
|
+
finally {
|
|
392
|
+
clearTimeout(resultTimeout);
|
|
393
|
+
signal.removeEventListener("abort", forwardCallerAbort);
|
|
394
|
+
}
|
|
395
|
+
if (signal.aborted)
|
|
396
|
+
return created;
|
|
397
|
+
if (lifecycle.status === "answered") {
|
|
398
|
+
return {
|
|
399
|
+
...created,
|
|
400
|
+
state: "completed",
|
|
401
|
+
verdict: lifecycle.verdict,
|
|
402
|
+
confidence: lifecycle.confidence,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
if (lifecycle.status === "expired" || lifecycle.status === "cancelled") {
|
|
406
|
+
return {
|
|
407
|
+
...created,
|
|
408
|
+
state: lifecycle.status,
|
|
409
|
+
verdict: null,
|
|
410
|
+
confidence: null,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return created;
|
|
415
|
+
}
|
|
300
416
|
function summarise(status) {
|
|
301
417
|
const cadence = status.cadence;
|
|
302
418
|
if (status.first_run_intro.should_offer_now) {
|