arisa 5.2.21 → 5.2.22

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arisa",
3
- "version": "5.2.21",
3
+ "version": "5.2.22",
4
4
  "description": "Telegram + Pi Agent modular assistant",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -33,6 +33,15 @@ export function getPiAuthIssue(error) {
33
33
  return null;
34
34
  }
35
35
 
36
+ export function buildPiRequestFailureMessage(error) {
37
+ const message = getErrorMessage(error);
38
+ const limited = /usage limit|quota|rate[ _-]?limit|too many requests|insufficient[_ ]credits|credits? (?:exhausted|depleted)/i.test(message);
39
+ const title = limited
40
+ ? "The provider's usage quota or request limit was reached. Retry when capacity is available; signing in again does not restore quota."
41
+ : "Pi request could not be completed. This does not establish an authentication failure.";
42
+ return `${title}\nDetails: ${message}`;
43
+ }
44
+
36
45
  export async function getPiAuthStatus(config, chatId = null) {
37
46
  const runtime = await createPiRuntime({
38
47
  provider: config.pi.provider,
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  buildPiAuthRecoveryBlockedMessage,
3
3
  buildPiAuthTelegramMessage,
4
+ buildPiRequestFailureMessage,
4
5
  getErrorMessage,
5
6
  getPiAuthIssue,
6
7
  getPiAuthStatus
@@ -37,12 +38,16 @@ export function createTelegramAuthController({
37
38
  }
38
39
 
39
40
  function rememberValidationFailure(error) {
40
- const detected = rememberIssue(error) || {
41
- kind: "validation-failed",
42
- message: getErrorMessage(error)
43
- };
44
- issue = detected;
45
- return detected;
41
+ // Quota, network and server failures must never latch the Telegram auth gate.
42
+ issue = getPiAuthIssue(error);
43
+ return issue;
44
+ }
45
+
46
+ async function buildValidationFailureMessage(chatId, error) {
47
+ const detected = rememberValidationFailure(error);
48
+ return detected
49
+ ? buildPiAuthTelegramMessage({ config, chatId, issue: detected })
50
+ : buildPiRequestFailureMessage(error);
46
51
  }
47
52
 
48
53
  async function notifyIssueIfNeeded(chatId, error) {
@@ -62,15 +67,14 @@ export function createTelegramAuthController({
62
67
  async function finishRenewal(chatId, renewal) {
63
68
  try {
64
69
  await renewal.promise;
65
- await agentManager.validateAgent();
66
- agentManager.clearSessionCache(chatId);
67
70
  issue = null;
71
+ agentManager.clearSessionCache(chatId);
72
+ await agentManager.validateAgent();
68
73
  logger?.log("telegram", `Pi auth renewal completed for chat ${chatId}`);
69
74
  await api.sendMessage(chatId, await buildPiAuthTelegramMessage({ config, chatId, verified: true }));
70
75
  } catch (error) {
71
- const detected = rememberValidationFailure(error);
72
- logger?.error("telegram", `Pi auth renewal failed for chat ${chatId}: ${getErrorMessage(error)}`);
73
- await api.sendMessage(chatId, await buildPiAuthTelegramMessage({ config, chatId, issue: detected })).catch((notifyError) => {
76
+ logger?.error("telegram", `Pi auth renewal or validation failed for chat ${chatId}: ${getErrorMessage(error)}`);
77
+ await api.sendMessage(chatId, await buildValidationFailureMessage(chatId, error)).catch((notifyError) => {
74
78
  logger?.error("telegram", `auth renewal failure notice failed for chat ${chatId}: ${getErrorMessage(notifyError)}`);
75
79
  });
76
80
  } finally {
@@ -141,7 +145,9 @@ export function createTelegramAuthController({
141
145
  if (!authorization.ok) return;
142
146
 
143
147
  const status = await getPiAuthStatus(config, ctx.chat.id);
144
- if (status.hasApiKey || !status.supportsOAuth) {
148
+ const canValidateStoredAuth = status.hasStoredAuth && !issue && !renewals.has(chatKey(ctx.chat.id));
149
+ // A stored OAuth login may still be valid even when the provider has no quota.
150
+ if (status.hasApiKey || !status.supportsOAuth || canValidateStoredAuth) {
145
151
  await withTyping(ctx, async () => {
146
152
  try {
147
153
  await agentManager.validateAgent();
@@ -149,8 +155,7 @@ export function createTelegramAuthController({
149
155
  issue = null;
150
156
  await ctx.reply(await buildPiAuthTelegramMessage({ config, chatId: ctx.chat.id, verified: true }));
151
157
  } catch (error) {
152
- const detected = rememberValidationFailure(error);
153
- await ctx.reply(await buildPiAuthTelegramMessage({ config, chatId: ctx.chat.id, issue: detected }));
158
+ await ctx.reply(await buildValidationFailureMessage(ctx.chat.id, error));
154
159
  }
155
160
  });
156
161
  return;
@@ -162,8 +167,7 @@ export function createTelegramAuthController({
162
167
  ? "Starting Pi login from Telegram..."
163
168
  : "Pi login is already in progress. Paste the redirect URL or code here when you have it.");
164
169
  } catch (error) {
165
- const detected = rememberValidationFailure(error);
166
- await ctx.reply(await buildPiAuthTelegramMessage({ config, chatId: ctx.chat.id, issue: detected }));
170
+ await ctx.reply(await buildValidationFailureMessage(ctx.chat.id, error));
167
171
  }
168
172
  }
169
173
 
@@ -0,0 +1,124 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { ModelRuntime } from "@earendil-works/pi-coding-agent";
4
+ import { createTelegramAuthController } from "../src/transport/telegram/telegram-auth-controller.js";
5
+
6
+ function setup(t, { stored = true, apiKey = "", failure = null, loginFailure = null } = {}) {
7
+ const messages = [];
8
+ const calls = { login: 0, validate: 0, clear: 0 };
9
+ t.mock.method(ModelRuntime, "create", async () => ({
10
+ setRuntimeApiKey: async () => {},
11
+ getProviderAuthStatus: () => ({ configured: stored, source: "stored" }),
12
+ getProvider: () => ({ auth: { oauth: {} } }),
13
+ async login() {
14
+ calls.login++;
15
+ if (loginFailure) throw loginFailure;
16
+ return { type: "oauth", access: "test-only" };
17
+ }
18
+ }));
19
+ const controller = createTelegramAuthController({
20
+ config: { pi: { provider: "openai-codex", model: "test-model", apiKey } },
21
+ api: { sendMessage: async (_chatId, text) => messages.push(text) },
22
+ agentManager: {
23
+ async validateAgent() {
24
+ calls.validate++;
25
+ if (failure) throw failure;
26
+ },
27
+ clearSessionCache() { calls.clear++; }
28
+ }
29
+ });
30
+ const command = () => controller.handleCommand({
31
+ chat: { id: 123 }, reply: async (text) => messages.push(text)
32
+ }, { authorize: async () => ({ ok: true }), withTyping: async (_ctx, fn) => fn() });
33
+ return { controller, messages, calls, command };
34
+ }
35
+
36
+ for (const apiKey of ["", "test-key"]) {
37
+ for (const message of [
38
+ "Codex error: The usage limit has been reached",
39
+ "429 Too many requests",
40
+ "insufficient_quota",
41
+ "request timed out",
42
+ "503 service unavailable"
43
+ ]) {
44
+ test(`validation preserves usable auth after ${message} (${apiKey ? "API key" : "OAuth"})`, async (t) => {
45
+ const { controller, calls, command, messages } = setup(t, { apiKey, failure: new Error(message) });
46
+ await command();
47
+ assert.equal(controller.getIssue(), null);
48
+ assert.equal(calls.login, 0);
49
+ assert.equal(calls.clear, 0);
50
+ assert.match(messages[0], new RegExp(message));
51
+ assert.doesNotMatch(messages[0], /Send \/auth|Run \/auth|Update the key|not ready/);
52
+ await command();
53
+ assert.equal(calls.validate, 2);
54
+ assert.equal(calls.login, 0);
55
+ });
56
+ }
57
+ }
58
+
59
+ test("/auth validates stored OAuth credentials instead of starting a device login", async (t) => {
60
+ const { controller, calls, command, messages } = setup(t);
61
+ await command();
62
+ assert.equal(calls.validate, 1);
63
+ assert.equal(calls.login, 0);
64
+ assert.equal(controller.getIssue(), null);
65
+ assert.match(messages[0], /authentication is working/);
66
+ });
67
+
68
+ test("real token invalidation remains blocking and permits OAuth recovery", async (t) => {
69
+ const { controller, command, calls, messages } = setup(t, { failure: new Error("auth token revoked") });
70
+ await command();
71
+ assert.equal(controller.getIssue()?.kind, "invalidated-token");
72
+ assert.match(messages[0], /Run \/auth/);
73
+ await command();
74
+ await new Promise((resolve) => setImmediate(resolve));
75
+ assert.equal(calls.login, 1);
76
+ assert.equal(controller.getIssue()?.kind, "invalidated-token");
77
+ });
78
+
79
+ test("successful OAuth renewal followed by quota failure clears a stale auth block", async (t) => {
80
+ const { controller, command, calls, messages } = setup(t, {
81
+ failure: new Error("Codex error: The usage limit has been reached")
82
+ });
83
+ controller.rememberIssue(new Error("auth token expired"));
84
+ await command();
85
+ await new Promise((resolve) => setImmediate(resolve));
86
+ assert.equal(calls.login, 1);
87
+ assert.equal(calls.clear, 1);
88
+ assert.equal(controller.getIssue(), null);
89
+ assert.equal(controller.hasActiveRenewal(123), false);
90
+ assert.match(messages.join("\n"), /signing in again does not restore quota/);
91
+ await command();
92
+ assert.equal(calls.login, 1);
93
+ assert.equal(calls.validate, 2);
94
+ });
95
+
96
+ test("missing credentials still initiate login", async (t) => {
97
+ const { controller, command, calls } = setup(t, { stored: false });
98
+ await command();
99
+ await new Promise((resolve) => setImmediate(resolve));
100
+ assert.equal(calls.login, 1);
101
+ assert.equal(calls.validate, 1);
102
+ assert.equal(controller.getIssue(), null);
103
+ });
104
+
105
+ test("transient OAuth login failure does not create an authentication block", async (t) => {
106
+ const { controller, command, calls, messages } = setup(t, {
107
+ stored: false, loginFailure: new Error("request timed out")
108
+ });
109
+ await command();
110
+ await new Promise((resolve) => setImmediate(resolve));
111
+ assert.equal(calls.validate, 0);
112
+ assert.equal(controller.getIssue(), null);
113
+ assert.equal(controller.hasActiveRenewal(123), false);
114
+ assert.match(messages.join("\n"), /does not establish an authentication failure/);
115
+ });
116
+
117
+ test("ordinary prompt quota errors never latch the auth issue", async (t) => {
118
+ const { controller, messages } = setup(t);
119
+ assert.equal(await controller.notifyIssueIfNeeded(123, new Error("Codex error: The usage limit has been reached")), false);
120
+ assert.equal(controller.getIssue(), null);
121
+ assert.equal(messages.length, 0);
122
+ assert.equal(await controller.notifyIssueIfNeeded(123, new Error("No auth found")), true);
123
+ assert.equal(controller.getIssue()?.kind, "missing-auth");
124
+ });