@coda-console/claude 1.6.0 → 1.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.
@@ -0,0 +1,11 @@
1
+ import { RequestError } from '@agentclientprotocol/sdk';
2
+
3
+ // Only failed turns come here. Claude Code's synthetic assistant error and the subscription's rejected window are
4
+ // evidence, ordinary assistant text and a bare HTTP 429 are not. Keep the provider's reset time/recovery advice intact.
5
+ const exhausted = message => /^(?:You've (?:hit|reached) your (?:session|weekly|5-hour|7-day|usage) limit\b|(?:Your )?[Cc]redit balance is too low\b|You're out of (?:usage credits|extra usage)\b|Your org is out of usage\b)/.test(message);
6
+
7
+ export function claudeFailure(message, { error, limits } = {}) {
8
+ const quota = (!error || ['rate_limit', 'billing_error'].includes(error)) &&
9
+ (exhausted(message) || (error === 'rate_limit' && limits?.status === 'rejected'));
10
+ return new RequestError(-32603, message, quota ? { _meta: { coda: { failure: { kind: 'quota_exhausted' } } } } : undefined);
11
+ }
package/agent/session.mjs CHANGED
@@ -10,6 +10,7 @@ import { realpath, stat } from 'node:fs/promises';
10
10
  import { join } from 'node:path';
11
11
  import { agent, PROTOCOL_VERSION, RequestError } from '@agentclientprotocol/sdk';
12
12
  import { ClaudeSessionLimits, accountHash } from './limits.mjs';
13
+ import { claudeFailure } from './errors.mjs';
13
14
  import { sdkEnvironment } from './auth.mjs';
14
15
  import { chosenServices, listedModels, serviceSettings } from './providers.mjs';
15
16
  import { SessionTitle } from './titles.mjs';
@@ -286,7 +287,7 @@ export class ClaudeSession {
286
287
  if (!prompt) return;
287
288
  // What ended before this turn ended was taken in by it.
288
289
  this.waits.settled();
289
- if (error) prompt.reject(failure(error)); else prompt.resolve({ stopReason });
290
+ if (error) prompt.reject(error); else prompt.resolve({ stopReason });
290
291
  }
291
292
  // What is said to the session goes into what its title is made of.
292
293
  heard(content) {
@@ -358,7 +359,9 @@ export class ClaudeSession {
358
359
  case 'assistant': return this.assistant(m, parent);
359
360
  case 'user': return this.toolResults(m, parent);
360
361
  case 'result': return this.result(m);
361
- case 'rate_limit_event': if (this.env.CODA_PLUGIN_STATE) new ClaudeSessionLimits({ path: join(this.env.CODA_PLUGIN_STATE, 'limits.json') }).observe(m.rate_limit_info, this.account ?? null);
362
+ case 'rate_limit_event':
363
+ this.turnLimits = m.rate_limit_info;
364
+ if (this.env.CODA_PLUGIN_STATE) new ClaudeSessionLimits({ path: join(this.env.CODA_PLUGIN_STATE, 'limits.json') }).observe(m.rate_limit_info, this.account ?? null);
362
365
  }
363
366
  }
364
367
  system(m) {
@@ -366,6 +369,7 @@ export class ClaudeSession {
366
369
  case 'init': {
367
370
  if (m.session_id !== this.sessionId) return;
368
371
  this.turning = true;
372
+ this.turnError = null; this.turnLimits = null;
369
373
  this.terminal = m.terminal_slash_commands ?? [];
370
374
  // Its effort is the one applied, the model's own included: what was chosen stays what it was. A query on the
371
375
  // service's model reports it by that service's name for it.
@@ -462,6 +466,13 @@ export class ClaudeSession {
462
466
  assistant(m, parent) {
463
467
  const sessionId = parent ?? this.sessionId, apiId = m.message?.id, blocks = this.streamed.get(apiId);
464
468
  if (parent) this.waits.model(parent, m.message?.model);
469
+ // API failures are synthetic assistant messages, followed by a result (even subtype "success" can be is_error).
470
+ // The result ends the turn and reports its failure; sending the synthetic message as prose would look like an answer.
471
+ // A child's error belongs to that child, never to the parent's prompt.
472
+ if (!parent) {
473
+ this.turnError = m.error ? { error: m.error, message: text(m.message?.content) } : null;
474
+ if (m.error) return;
475
+ }
465
476
  for (const block of m.message?.content ?? []) {
466
477
  if (block.type === 'tool_use') {
467
478
  this.tools.add(block.id);
@@ -499,17 +510,20 @@ export class ClaudeSession {
499
510
  const steered = ids.filter(id => this.steering.delete(id)).length > 0;
500
511
  const id = ids.find(id => this.prompts.has(id)) ?? (steered ? this.held : undefined);
501
512
  const cancelled = interrupted(m), failed = !cancelled && (m.is_error || m.subtype !== 'success');
513
+ const error = failed ? claudeFailure(m.errors?.length || m.result ? errorText(m) : this.turnError?.message || errorText(m),
514
+ { error: this.turnError?.error, limits: this.turnLimits }) : null;
515
+ this.turnError = null; this.turnLimits = null;
502
516
  if (id) {
503
- const outcome = this.ending ?? (failed ? { failure: errorText(m) } : { stopReason: cancelled ? 'cancelled' : 'end_turn' });
517
+ const outcome = this.ending ?? (failed ? { failure: error } : { stopReason: cancelled ? 'cancelled' : 'end_turn' });
504
518
  if (this.ending || !this.steering.size) this.settle(id, outcome);
505
519
  else { this.held = id; if (cancelled || failed) this.dropRest(outcome); }
506
520
  } else if (this.waits.handling) {
507
- // A turn of Claude's own has no reply to fail: what went wrong is said in it.
508
- if (failed) this.chunk(this.sessionId, 'text', m.uuid, errorText(m));
509
- this.waits.finished();
521
+ // A turn of Claude's own has no RPC reply to fail. End its own activity as failed with the reason, rather than
522
+ // saying the error as an answer and marking the work complete. The task that woke it retains its own outcome.
523
+ this.waits.finished(error ? { error: error.message, title: error.data?._meta?.coda?.failure?.kind === 'quota_exhausted' ? '额度不足' : undefined } : {});
510
524
  } else if (failed && this.prompts.size) {
511
525
  // A failure of the session itself names no input: none of those given can be said to have run.
512
- for (const prompt of this.prompts.values()) prompt.reject(failure(errorText(m)));
526
+ for (const prompt of this.prompts.values()) prompt.reject(error);
513
527
  this.prompts.clear();
514
528
  }
515
529
  void this.title?.refresh(!failed && !cancelled);
package/agent/waits.mjs CHANGED
@@ -161,7 +161,10 @@ export class ClaudeWaits {
161
161
  this.report(own, 'handling', { kind: 'task', title: 'Claude 自主继续', stoppable: false });
162
162
  }
163
163
  // That turn's end: what it handled ended with it, a recurring wakeup waits for its next time.
164
- finished() {
164
+ finished({ error, title } = {}) {
165
+ // Processing a successful task's result can fail. Give that failure its own activity instead of changing the task's
166
+ // outcome, and leave recurring wakeups scheduled. There may already be a turn-owned activity (hold).
167
+ if (error && !this.own.size) this.hold();
165
168
  for (const task of this.tasks.values()) if (task.state === 'handling') { this.report(task, task.final ?? 'done'); task.final = undefined; }
166
169
  for (const cron of this.crons.values()) {
167
170
  if (cron.state !== 'handling') continue;
@@ -169,7 +172,7 @@ export class ClaudeWaits {
169
172
  cron.at = nextRun(cron.schedule, this.now());
170
173
  this.report(cron, 'waiting', { at: cron.at === null ? undefined : new Date(cron.at).toISOString() });
171
174
  }
172
- for (const own of this.own) this.report(own, 'done');
175
+ for (const own of this.own) this.report(own, error ? 'failed' : 'done', error ? { title, detail: cut(error, 4000) } : {});
173
176
  this.own.clear();
174
177
  }
175
178
  // A stop Coda asks of a wait: the task to stop when it still goes (null: none). One that ended and is yet to be taken in is
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "id": "claude",
4
4
  "displayName": "Claude",
5
5
  "description": "Claude SDK Agent:Coda 基于 Claude Agent SDK 写的 ACP 入口,一个进程一个 SDK 会话;资源页的「Claude」面板显示会话报告的额度,可从 Coda 登录。",
6
- "version": "1.6.0",
6
+ "version": "1.6.1",
7
7
  "categories": ["Agent"],
8
8
  "dependencies": {
9
9
  "@agentclientprotocol/sdk": "1.4.0",