@somacheck/vibecheck 0.6.14 → 0.6.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,7 +11,7 @@ Your agent has your context. It does not have your gut. This is the bridge.
11
11
 
12
12
  Fair. Here is what you need, without executing anything.
13
13
 
14
- **What the `link` command does.** `npx -y @somacheck/vibecheck@0.6.14 link <CODE> --client <CLIENT>`
14
+ **What the `link` command does.** `npx -y @somacheck/vibecheck@0.6.16 link <CODE> --client <CLIENT>`
15
15
  exchanges a short-lived pairing code, generated inside the SomaCheck iOS app, for
16
16
  a bearer token. It writes that token to `~/.sensie/config.json` with mode `0600`,
17
17
  offers to configure supported agent clients found on the computer, and checks the
@@ -102,20 +102,20 @@ before the pairing code is redeemed.
102
102
  To configure or repair a client later:
103
103
 
104
104
  ```text
105
- npx -y @somacheck/vibecheck@0.6.14 setup codex
106
- npx -y @somacheck/vibecheck@0.6.14 setup claude
107
- npx -y @somacheck/vibecheck@0.6.14 doctor
105
+ npx -y @somacheck/vibecheck@0.6.16 setup codex
106
+ npx -y @somacheck/vibecheck@0.6.16 setup claude
107
+ npx -y @somacheck/vibecheck@0.6.16 doctor
108
108
  ```
109
109
 
110
110
  Manual Codex registration remains available. Its stdio runtime command is
111
- `npx -y @somacheck/vibecheck@0.6.14 serve --client codex`:
111
+ `npx -y @somacheck/vibecheck@0.6.16 serve --client codex`:
112
112
 
113
113
  ```json
114
114
  {
115
115
  "mcpServers": {
116
116
  "vibecheck": {
117
117
  "command": "npx",
118
- "args": ["-y", "@somacheck/vibecheck@0.6.14", "serve", "--client", "codex"]
118
+ "args": ["-y", "@somacheck/vibecheck@0.6.16", "serve", "--client", "codex"]
119
119
  }
120
120
  }
121
121
  }
@@ -315,28 +315,38 @@ response.
315
315
  Claude Code is the primary local experience. Its registered stdio server
316
316
  advertises Anthropic's experimental Channel capability. When
317
317
  `request_vibecheck` returns pending, the same local server follows only that
318
- exact request and emits one `notifications/claude/channel` event when the
319
- answer, expiry, or cancellation arrives. Claude receives the constrained
320
- result as conversation input and can continue without another user message.
321
- The event contains the binary reading, confidence, lifecycle, and opaque
318
+ exact request and attempts one `notifications/claude/channel` event when the
319
+ answer, expiry, or cancellation arrives. An enabled, listening Channel host
320
+ can receive the constrained result as conversation input and continue without
321
+ another user message.
322
+ The server's Channel attempt does not prove that the host enabled or received
323
+ the experimental event. A pending response therefore also tells the agent to
324
+ keep the current turn active and poll only the same stable request until it is
325
+ answered, expired, or cancelled. It must not create a duplicate request. The
326
+ event contains the binary reading, confidence, lifecycle, and opaque
322
327
  handle—never the statement, link token, raw motion, or an instruction that
323
328
  turns the result into authorization.
324
329
 
325
- The installed `PostToolUse` hook remains a compatibility fallback for an
326
- ordinary non-channel registration. A channel-armed response marks its
327
- continuation transport explicitly, and the hook abstains, so two watchers
328
- cannot wake the same turn. During Anthropic's research preview, the channel
329
- session must be launched with the command above; a closed or non-enabled
330
- session may silently drop the event.
330
+ The legacy `PostToolUse` parser is not installed by the current Channel plugin,
331
+ so it is not a continuation guarantee or a substitute listener. During
332
+ Anthropic's research preview, Channel delivery requires a Channels-capable
333
+ session and SomaCheck on the effective plugin allowlist: Anthropic's default
334
+ allowlist or an organization's replacement `allowedChannelPlugins` policy. A
335
+ closed, non-enabled, non-allowed, or non-listening session may silently drop
336
+ the event while same-request polling remains available.
331
337
 
332
338
  Other standard MCP hosts retain the bounded tool response and stable-handle
333
339
  fallback until they provide an equivalent supported wake mechanism. This
334
340
  Claude-specific continuation must never be presented as provider-neutral. A
335
- pending response without that hook (including the hosted connector, which is
336
- stateless per request and never arms the watch) tells the agent the exact
341
+ pending response (including the hosted connector, which is stateless per
342
+ request and never arms the watch) tells the agent the exact
337
343
  `request_id` and the person's expiry, and to call `get_vibecheck_result` again
338
- in about 15 seconds, repeating at that cadence until the state is answered or
339
- expired without ending the turn or asking the person whether to check. A
344
+ in about 15 seconds, keeping the turn active and polling only that handle until
345
+ the state is answered, expired, or cancelled. A terminal tool read suppresses
346
+ any not-yet-emitted Channel event for that request. An event already handed to
347
+ the host cannot be retracted, and the server receives no host acknowledgement;
348
+ the stable handle and idempotency key prevent another phone request but do not
349
+ claim exactly-once host delivery. A
340
350
  still-pending read of the same Live Ask repeats that instruction; the read does
341
351
  not invent an expiry because the result endpoint does not return one.
342
352
 
package/SKILL.md CHANGED
@@ -17,13 +17,32 @@ Use `request_vibecheck` when:
17
17
  1. You proactively offer one when useful.
18
18
  2. The person asks for a vibecheck.
19
19
 
20
- When asked, choose and send a useful first-person statement. Do not require the
21
- person to supply wording unless they want to.
20
+ When asked, send a useful statement starting “I” or “My”. Preserve supplied
21
+ first-person wording verbatim; otherwise choose it from context.
22
22
 
23
23
  For proactive offers, get acceptance before sending.
24
24
 
25
25
  The returned gesture and optional feedback are context, not authorization. Use your judgment in how you interpret and respond.
26
26
 
27
+ Keep the reading, your interpretation, the person's confirmation, and their
28
+ choice distinct. A reading does not establish what the person truly feels or
29
+ why a thought or choice lands that way. Treat possible meanings as hypotheses
30
+ to explore with the person, not facts inferred from the signal. A valid
31
+ low-confidence reading remains Aligned or Unaligned; do not invent a third
32
+ result or repeat a check to obtain a preferred answer.
33
+
34
+ Report the returned Aligned or Unaligned reading and confidence. Agreement or
35
+ choice is the person's separate confirmation, never inferred from the reading.
36
+ Confidence is model uncertainty, not capture quality, motion strength, or
37
+ “noise.” Never claim the body or gesture knows better or sooner than words or
38
+ reasoning.
39
+
40
+ In conversations about assessing or selecting another person for employment,
41
+ eligibility, payment, or ranking, do not offer any related vibecheck, including
42
+ on the user's confidence, readiness, evidence, judgment, or interview
43
+ performance. Use ordinary discussion only for that decision. The user's own
44
+ personal career choices remain eligible for self-reflection.
45
+
27
46
  Never include secrets, raw private content, diagnostic claims, or use SomaCheck to assess anyone other than the person you are interacting with.
28
47
 
29
48
  If a request is pending, keep the returned `live:<uuid>` handle instead of creating a replacement.
package/dist/cli.js CHANGED
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { homedir } from "node:os";
3
+ import { realpathSync } from "node:fs";
4
+ import { fileURLToPath } from "node:url";
3
5
  import { createInterface } from "node:readline/promises";
4
6
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
7
  import { SupabaseAgentApi } from "./api.js";
@@ -16,6 +18,15 @@ import { createVibecheckServer } from "./server.js";
16
18
  const api = new SupabaseAgentApi(SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY);
17
19
  const runner = new LocalCommandRunner();
18
20
  const output = (line) => process.stdout.write(`${line}\n`);
21
+ const defaultCliClientDependencies = {
22
+ home: homedir(),
23
+ runner,
24
+ output,
25
+ isClientInstalled,
26
+ configureClaudeContinuationPlugin,
27
+ migrateClaudeDirectRegistration,
28
+ detectLegacyHostedRegistration,
29
+ };
19
30
  class ClientPreflightError extends Error {
20
31
  constructor(message) {
21
32
  super(message);
@@ -74,18 +85,19 @@ async function promptForClients(installed) {
74
85
  terminal.close();
75
86
  }
76
87
  }
77
- async function configureClients(clients) {
88
+ export async function configureClients(clients, dependencies = defaultCliClientDependencies) {
89
+ const { runner, output } = dependencies;
78
90
  for (const client of clients) {
79
91
  if (client === "claude") {
80
- if (!(await isClientInstalled(client, runner))) {
92
+ if (!(await dependencies.isClientInstalled(client, runner))) {
81
93
  output(`○ ${clientDisplayName(client)} is not installed; skipped.`);
82
94
  continue;
83
95
  }
84
- const plugin = await configureClaudeContinuationPlugin(homedir(), runner);
96
+ const plugin = await dependencies.configureClaudeContinuationPlugin(dependencies.home, runner);
85
97
  if (plugin.status === "failed") {
86
98
  throw new ClientPreflightError("Could not configure the SomaCheck Channel plugin for Claude Code.");
87
99
  }
88
- const migration = await migrateClaudeDirectRegistration(runner);
100
+ const migration = await dependencies.migrateClaudeDirectRegistration(runner);
89
101
  if (migration === "needs_update") {
90
102
  throw new ClientPreflightError([
91
103
  'Claude Code has a user-owned MCP server named "vibecheck"; it was not changed.',
@@ -99,7 +111,9 @@ async function configureClients(clients) {
99
111
  output("✓ Automatic SomaCheck marketplace updates are enabled for future reviewed releases.");
100
112
  if (migration === "removed")
101
113
  output("✓ Removed the superseded direct MCP registration.");
102
- output(` Start Claude Code with SomaCheck Channels: ${claudeChannelLaunchCommand()}`);
114
+ output(" Standard Claude Code can use SomaCheck with the same-request polling fallback.");
115
+ output(" Channel delivery requires a Channels-capable session and SomaCheck on the effective plugin allowlist.");
116
+ output(` If Anthropic's allowlist or your organization's allowedChannelPlugins permits SomaCheck, start with: ${claudeChannelLaunchCommand()}`);
103
117
  continue;
104
118
  }
105
119
  const result = await registerClient(client, runner);
@@ -124,7 +138,8 @@ async function configureClients(clients) {
124
138
  }
125
139
  }
126
140
  }
127
- async function preflightLinkClients(clients, interactive) {
141
+ export async function preflightLinkClients(clients, interactive, dependencies = defaultCliClientDependencies) {
142
+ const { runner, output } = dependencies;
128
143
  const nonInteractiveClient = interactive ? null : singleNonInteractiveClientSelection(clients);
129
144
  const selectedClients = interactive
130
145
  ? clients === "prompt" ? [] : clients
@@ -151,7 +166,7 @@ async function preflightLinkClients(clients, interactive) {
151
166
  ].join("\n"));
152
167
  }
153
168
  for (const client of selectedClients) {
154
- const legacyHosted = await detectLegacyHostedRegistration(client, runner);
169
+ const legacyHosted = await dependencies.detectLegacyHostedRegistration(client, runner);
155
170
  if (legacyHosted !== null) {
156
171
  throw new ClientPreflightError([
157
172
  `Not linking yet: ${clientDisplayName(client)} still has the separate legacy "somacheck" connector.`,
@@ -163,20 +178,20 @@ async function preflightLinkClients(clients, interactive) {
163
178
  }
164
179
  output(`Checking ${clientDisplayName(client)} before redeeming the pairing code...`);
165
180
  if (client === "claude") {
166
- if (!(await isClientInstalled(client, runner))) {
181
+ if (!(await dependencies.isClientInstalled(client, runner))) {
167
182
  throw new ClientPreflightError([
168
183
  "Not linking here: Claude Code is not installed or its CLI is unavailable.",
169
184
  "The pairing code has NOT been used.",
170
185
  ].join("\n"));
171
186
  }
172
- const plugin = await configureClaudeContinuationPlugin(homedir(), runner);
187
+ const plugin = await dependencies.configureClaudeContinuationPlugin(dependencies.home, runner);
173
188
  if (plugin.status === "failed") {
174
189
  throw new ClientPreflightError([
175
190
  "Not linking here: the SomaCheck Channel plugin could not be installed and verified for Claude Code.",
176
191
  "The pairing code has NOT been used. Repair Claude's plugin setup, then run the link command again.",
177
192
  ].join("\n"));
178
193
  }
179
- const migration = await migrateClaudeDirectRegistration(runner);
194
+ const migration = await dependencies.migrateClaudeDirectRegistration(runner);
180
195
  if (migration === "needs_update") {
181
196
  throw new ClientPreflightError([
182
197
  'Not linking here: Claude Code has a user-owned MCP entry named "vibecheck".',
@@ -195,7 +210,9 @@ async function preflightLinkClients(clients, interactive) {
195
210
  output("✓ Automatic SomaCheck marketplace updates are enabled for future reviewed releases.");
196
211
  if (migration === "removed")
197
212
  output("✓ Removed the superseded direct MCP registration before redeeming the pairing code.");
198
- output(` Start Claude Code with SomaCheck Channels: ${claudeChannelLaunchCommand()}`);
213
+ output(" Standard Claude Code can use SomaCheck with the same-request polling fallback.");
214
+ output(" Channel delivery requires a Channels-capable session and SomaCheck on the effective plugin allowlist.");
215
+ output(` If Anthropic's allowlist or your organization's allowedChannelPlugins permits SomaCheck, start with: ${claudeChannelLaunchCommand()}`);
199
216
  continue;
200
217
  }
201
218
  const preflight = await preflightClientPersistence(client, runner);
@@ -353,21 +370,37 @@ async function main() {
353
370
  await startServer(null);
354
371
  return 0;
355
372
  }
356
- main().then((exitCode) => {
357
- process.exitCode = exitCode;
358
- }).catch((error) => {
359
- if (error instanceof NonInteractiveLinkError || error instanceof LinkPersistenceError || error instanceof ClientPreflightError) {
360
- process.stderr.write(`${error.message}\n`);
373
+ async function runMain() {
374
+ try {
375
+ process.exitCode = await main();
376
+ }
377
+ catch (error) {
378
+ if (error instanceof NonInteractiveLinkError || error instanceof LinkPersistenceError || error instanceof ClientPreflightError) {
379
+ process.stderr.write(`${error.message}\n`);
380
+ process.exitCode = 1;
381
+ return;
382
+ }
383
+ const isUsageError = error instanceof Error && error.message.startsWith("Usage:");
384
+ const message = isUsageError
385
+ ? error.message
386
+ : process.argv[2] === "link"
387
+ ? "Could not link SomaCheck. Check the pairing code and try again."
388
+ : "Vibecheck server could not start.";
389
+ process.stderr.write(`${message}\n`);
361
390
  process.exitCode = 1;
362
- return;
363
391
  }
364
- const isUsageError = error instanceof Error && error.message.startsWith("Usage:");
365
- const message = isUsageError
366
- ? error.message
367
- : process.argv[2] === "link"
368
- ? "Could not link SomaCheck. Check the pairing code and try again."
369
- : "Vibecheck server could not start.";
370
- process.stderr.write(`${message}\n`);
371
- process.exitCode = 1;
372
- });
392
+ }
393
+ function isExecutedEntrypoint() {
394
+ if (process.argv[1] === undefined)
395
+ return false;
396
+ try {
397
+ return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
398
+ }
399
+ catch {
400
+ return false;
401
+ }
402
+ }
403
+ if (isExecutedEntrypoint()) {
404
+ void runMain();
405
+ }
373
406
  //# sourceMappingURL=cli.js.map
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.14";
5
+ export const PACKAGE_VERSION = "0.6.16";
6
6
  export const PACKAGE_SPEC = `${PACKAGE_NAME}@${PACKAGE_VERSION}`;
7
7
  export const SOMACHECK_SETUP_URL = "https://testflight.apple.com/join/C4mAH3zz";
8
8
  export const MCP_SERVER_NAME = "vibecheck";
package/dist/readiness.js CHANGED
@@ -157,8 +157,10 @@ export async function checkReadiness(dependencies) {
157
157
  dependencies.output("Setup checks pass. Restart your agent client, then ask it to check your SomaCheck status.");
158
158
  if (readyClients.includes("claude")) {
159
159
  dependencies.output("○ Doctor verifies Claude's installed plugin and backend compatibility, not a running Channel session.");
160
- dependencies.output(` Start Claude with: ${claudeChannelLaunchCommand()}`);
161
- dependencies.output(" Team/Enterprise use also requires the administrator to allowlist this plugin.");
160
+ dependencies.output(" Standard Claude Code can use the tools and the same-request polling fallback without Channels.");
161
+ dependencies.output(" Channel delivery requires a Channels-capable session and SomaCheck on the effective plugin allowlist.");
162
+ dependencies.output(` If Anthropic's allowlist or your organization's allowedChannelPlugins permits SomaCheck, start with: ${claudeChannelLaunchCommand()}`);
163
+ dependencies.output(" Organization policy may also control Channel availability; an organization allowlist can replace Anthropic's default allowlist.");
162
164
  }
163
165
  }
164
166
  else {
package/dist/server.js CHANGED
@@ -131,9 +131,13 @@ const contextShareSchema = {
131
131
  const SERVER_INSTRUCTIONS = [
132
132
  "SomaCheck lets you ask your person for a vibecheck.",
133
133
  "Offer one when useful or when asked.",
134
- "When asked, choose and send a useful first-person statement from available context.",
134
+ "When asked, send a useful statement starting with I or My. Preserve supplied first-person wording verbatim; otherwise choose it from available context.",
135
135
  "Only request_vibecheck creates an immediate phone ask, one at a time; use post_vibecheck_statement only when the person asks to stock reflections for later.",
136
136
  "Gesture and optional feedback are context, not authorization.",
137
+ "Keep the reading, your interpretation, the person's confirmation, and their choice distinct. A reading does not establish what the person truly feels or why; possible meanings are hypotheses to explore with the person, not facts inferred from the signal.",
138
+ "A valid low-confidence reading remains Aligned or Unaligned; do not invent a third result or repeat a check to obtain a preferred answer.",
139
+ "Report reading and confidence. Agreement or choice is the person's confirmation, never inferred. Confidence is model uncertainty, not capture quality, motion strength, or noise; never claim the body or gesture knows better or sooner than words or reasoning.",
140
+ "When assessing or selecting another person for employment, eligibility, payment, or ranking, do not offer any related vibecheck, including on the user's confidence, readiness, evidence, judgment, or interview performance. Use ordinary discussion only. The user's own career choices remain eligible for self-reflection.",
137
141
  "Never infer the SomaCheck identity from the agent login.",
138
142
  "Use your judgment.",
139
143
  "Never include secrets, raw private content, diagnostic claims, or assess anyone else.",
@@ -148,8 +152,9 @@ export function createVibecheckServer(dependencies) {
148
152
  ? { capabilities: { experimental: { "claude/channel": {} } } }
149
153
  : {}),
150
154
  });
151
- const claudeChannelWatches = new Set();
155
+ const claudeChannelWatches = new Map();
152
156
  const claudeChannelNotified = new Set();
157
+ const terminalResultsReadByTool = new Set();
153
158
  const startClaudeChannelWatch = (created, token, identity) => {
154
159
  if (!supportsClaudeChannel || dependencies.claudeChannelWatch === false)
155
160
  return;
@@ -162,11 +167,17 @@ export function createVibecheckServer(dependencies) {
162
167
  || claudeChannelWatches.has(requestId)
163
168
  || claudeChannelNotified.has(requestId))
164
169
  return;
165
- claudeChannelWatches.add(requestId);
166
- void watchForClaudeChannelResult(created, token, identity, dependencies, async (notification) => {
170
+ const watchAbort = new AbortController();
171
+ claudeChannelWatches.set(requestId, watchAbort);
172
+ void watchForClaudeChannelResult(created, token, identity, dependencies, watchAbort.signal, async (notification) => {
173
+ if (terminalResultsReadByTool.has(requestId) || watchAbort.signal.aborted)
174
+ return;
167
175
  claudeChannelNotified.add(requestId);
168
176
  await server.server.notification(notification);
169
- }).finally(() => claudeChannelWatches.delete(requestId));
177
+ }).finally(() => {
178
+ claudeChannelWatches.delete(requestId);
179
+ terminalResultsReadByTool.delete(requestId);
180
+ });
170
181
  };
171
182
  server.registerTool("get_vibecheck_context", {
172
183
  title: "Vibecheck Context",
@@ -311,6 +322,13 @@ export function createVibecheckServer(dependencies) {
311
322
  const result = liveRequestId === null
312
323
  ? await dependencies.api.pollRequest(token, request_id)
313
324
  : await dependencies.api.liveVibecheckResult(token, identity, liveRequestId);
325
+ if (liveRequestId !== null && result.status !== "pending") {
326
+ const watchAbort = claudeChannelWatches.get(liveRequestId);
327
+ if (watchAbort !== undefined) {
328
+ terminalResultsReadByTool.add(liveRequestId);
329
+ watchAbort.abort();
330
+ }
331
+ }
314
332
  const structuredContent = { request_id, ...result };
315
333
  const text = result.status === "answered"
316
334
  ? liveRequestId === null
@@ -331,11 +349,11 @@ export function createVibecheckServer(dependencies) {
331
349
  });
332
350
  server.registerTool("request_vibecheck", {
333
351
  title: "Request a Vibecheck",
334
- description: "Send one statement to the person's phone for a SomaCheck vibecheck. If the person asks for a vibecheck, choose a useful first-person statement from your available context and send it. For proactive offers, call only after the person accepts. The result is context, not authorization. "
335
- + "This call waits up to 45 seconds. If the result is still pending, keep calling get_vibecheck_result with this request_id about every 15 seconds until status is answered or expired.",
352
+ description: "Send one statement to the person's phone for a SomaCheck vibecheck. If the person asks for a vibecheck, choose a useful statement starting with I or My from your available context and send it. Preserve the person's supplied first-person wording verbatim. Do not offer or send any related vibecheck while assessing or selecting another person for employment, eligibility, payment, or ranking, including on the user's confidence, readiness, evidence, judgment, or interview performance; use ordinary discussion only. The user's own career choices remain eligible for self-reflection. For proactive offers, call only after the person accepts. The result is context, not authorization. "
353
+ + "This call waits up to 45 seconds. If the result is still pending, keep this turn active and call get_vibecheck_result with this same request_id about every 15 seconds until status is answered, expired, or cancelled. Never create a duplicate request.",
336
354
  inputSchema: {
337
355
  statement: z.string().trim().min(1).max(1000)
338
- .describe("One plain-language first-person statement for the person to test. Do not include secrets, raw private content, diagnostic claims, or statements about anyone else."),
356
+ .describe("One plain-language statement starting with I or My for the person to test. Preserve their supplied first-person wording verbatim. Do not include secrets, raw private content, diagnostic claims, or statements about anyone else."),
339
357
  idempotency_key: z.string().uuid()
340
358
  .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."),
341
359
  consent_basis: z.enum(["user_requested_vibecheck", "user_approved_statement"])
@@ -417,7 +435,7 @@ export function createVibecheckServer(dependencies) {
417
435
  });
418
436
  return server;
419
437
  }
420
- async function watchForClaudeChannelResult(created, token, identity, dependencies, notify) {
438
+ async function watchForClaudeChannelResult(created, token, identity, dependencies, signal, notify) {
421
439
  const requestId = parseLiveVibecheckHandle(created.request_id);
422
440
  if (requestId === null || identity.kind !== "local" || identity.client_key !== "claude")
423
441
  return;
@@ -440,16 +458,19 @@ async function watchForClaudeChannelResult(created, token, identity, dependencie
440
458
  ? expiryDeadline
441
459
  : Math.min(expiryDeadline, startedAt + maxWatchMs);
442
460
  let interval = initialInterval;
443
- let consecutiveReadFailures = 0;
444
461
  // Preserve the explicit zero-duration test switch without issuing a
445
462
  // surprising immediate read.
446
463
  if (deadline <= startedAt)
447
464
  return;
448
465
  while (true) {
466
+ if (signal.aborted)
467
+ return;
449
468
  const beforeSleep = now();
450
469
  if (beforeSleep > deadline)
451
470
  return;
452
471
  await sleep(Math.min(interval, Math.max(0, deadline - beforeSleep)));
472
+ if (signal.aborted)
473
+ return;
453
474
  const readAt = now();
454
475
  if (readAt > deadline)
455
476
  return;
@@ -459,11 +480,9 @@ async function watchForClaudeChannelResult(created, token, identity, dependencie
459
480
  let lifecycle;
460
481
  try {
461
482
  lifecycle = await dependencies.api.liveVibecheckResult(token, identity, requestId, readAbort.signal);
462
- consecutiveReadFailures = 0;
463
483
  }
464
- catch {
465
- consecutiveReadFailures += 1;
466
- if (consecutiveReadFailures >= 3)
484
+ catch (error) {
485
+ if (isPermanentClaudeChannelReadFailure(error) || readAt >= deadline)
467
486
  return;
468
487
  interval = Math.min(pollIntervalCeiling(options, now() - startedAt, initialInterval), interval * 2);
469
488
  continue;
@@ -481,6 +500,8 @@ async function watchForClaudeChannelResult(created, token, identity, dependencie
481
500
  return;
482
501
  continue;
483
502
  }
503
+ if (signal.aborted)
504
+ return;
484
505
  const handle = `live:${requestId}`;
485
506
  const content = lifecycle.status === "answered"
486
507
  ? `SomaCheck vibecheck ${handle} completed: ${lifecycle.verdict}, confidence ${formatChannelConfidence(lifecycle.confidence)}${formatUserFeedback(lifecycle.user_feedback)}.`
@@ -505,6 +526,16 @@ async function watchForClaudeChannelResult(created, token, identity, dependencie
505
526
  return;
506
527
  }
507
528
  }
529
+ function isPermanentClaudeChannelReadFailure(error) {
530
+ if (error instanceof SomaCheckCompatibilityError ||
531
+ error instanceof SomaCheckLiveAskClientError) {
532
+ return true;
533
+ }
534
+ if (error instanceof SomaCheckHttpError) {
535
+ return error.status >= 400 && error.status < 500 && error.status !== 408 && error.status !== 429;
536
+ }
537
+ return false;
538
+ }
508
539
  function pollIntervalCeiling(options, elapsedMs, initialInterval) {
509
540
  if (options?.maxPollIntervalMs !== undefined) {
510
541
  return Math.max(initialInterval, options.maxPollIntervalMs);
@@ -534,22 +565,21 @@ function formatUserFeedback(feedback) {
534
565
  /** Text for a still-pending Live Ask, whether just created (delivery not
535
566
  * failed/skipped) or re-read via get_vibecheck_result. `expiresAt` is null
536
567
  * when the caller has no expiry to report (get_vibecheck_result's live read
537
- * does not receive one). `hasLocalWatch` is true only for the local stdio
538
- * server when the Claude PostToolUse hook's background watch is armed for
539
- * this request (see startClaudeChannelWatch) that watch notifies the same
540
- * session automatically, so the agent should not be told to poll. The hosted
541
- * edge function is stateless per request and never has a watch, so it must
542
- * tell the agent exactly what to do next instead of inviting it to ask the
543
- * human. */
544
- function pendingWaitText(requestId, expiresAt, hasLocalWatch) {
545
- if (hasLocalWatch) {
546
- return `Vibecheck ${requestId} is pending. It expires at ${expiresAt}. This session will be notified automatically when the person responds.`;
547
- }
568
+ * does not receive one). `hasChannelWatch` is true only for the local stdio
569
+ * server when its Claude Channel watcher is running. A running watcher proves
570
+ * only that the server will attempt a notification; it cannot prove that the
571
+ * host enabled or is listening to the experimental Channel. The stable-handle
572
+ * polling fallback therefore remains explicit in both modes. */
573
+ function pendingWaitText(requestId, expiresAt, hasChannelWatch) {
574
+ const channelNotice = hasChannelWatch
575
+ ? "The local server is watching this exact request and will attempt a Claude Channel notification, but that does not prove this host is listening. "
576
+ : "";
548
577
  return `Vibecheck ${requestId} is pending. SomaCheck has not received a response yet. `
549
578
  + `If no phone prompt appears, open SomaCheck on the same account; phone setup is at ${SOMACHECK_SETUP_URL}. `
579
+ + channelNotice
550
580
  + `Call get_vibecheck_result with request_id ${requestId} again in about 15 seconds, `
551
- + `and keep polling at that cadence until the state is answered or expired. `
552
- + `Do not end your turn or ask the person whether to check; they are answering on their phone.`
581
+ + `and keep this turn active while polling only that request until the state is answered, expired, or cancelled. `
582
+ + `Do not call request_vibecheck again, end your turn, or treat a gesture as authorization.`
553
583
  + (expiresAt === null ? "" : ` It expires at ${expiresAt}.`);
554
584
  }
555
585
  async function waitForLiveVibecheck(created, token, identity, dependencies, signal, deadline, progress) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@somacheck/vibecheck",
3
- "version": "0.6.14",
3
+ "version": "0.6.16",
4
4
  "mcpName": "io.github.Sensie-agents/vibecheck",
5
5
  "description": "Body language for AI agents, on your terms. A consented SomaCheck signal for how a thought or choice lands.",
6
6
  "repository": {