@byokit/openclaw 0.12.0 → 0.14.0

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/dist/keys.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { join } from 'node:path';
2
2
  import { activate } from "./activation.js";
3
3
  import { keyAgentId, MEMBER_ID, KEY_PREFIX, untilListed } from "./members.js";
4
+ import { providerOf } from "./pick.js";
4
5
  import { routes } from "./routes.js";
5
6
  import { modelsOf } from "./accounts.js";
6
7
  import { LIVE_CATALOGS } from "./constants.js";
@@ -73,62 +74,82 @@ export function createKeys(ctx) {
73
74
  return {
74
75
  models,
75
76
  exclusive,
76
- async add(member, o) {
77
- if (!check(member))
78
- throw new Error('Choose your own member.');
79
- if (!o.apiKey.trim())
80
- return failure('invalid', 'empty');
81
- const route = routes().find((r) => r.choice === o.authChoice && r.keyEntry && r.readiness === 'ready');
82
- if (!route || (o.model !== undefined && !LIVE_CATALOGS.includes(route.provider)))
83
- return failure('not_included', 'model');
84
- return exclusive(member, async () => {
85
- let sent = false;
86
- // A key the engine may have taken but not sealed is cleared (prepare already emptied the agent); if the clear
87
- // does not reach the engine, the key stays unsealed and unusable until the next key add or kit start, so runs
88
- // fail closed.
89
- const abandon = async (result) => {
90
- await control(member, 'clear').catch(() => undefined);
91
- return result;
92
- };
93
- try {
94
- await ctx.ensure(member);
95
- const agentId = keyAgentId(member);
96
- const list = await ctx.request('agents.list');
97
- if (!list.agents.some((a) => a.id === agentId))
98
- await ctx.request('agents.create', { name: agentId, workspace: join(ctx.root, 'key-workspaces', member) });
99
- // Creation answers before the runtime roster/owners publish, just as for ordinary members.
100
- await untilListed(ctx.request, agentId);
101
- if (!(await control(member, 'prepare', route.provider)).ok)
102
- return failure('retry', 'engine');
103
- // The public pin's typed API-key path, never a CLI-login import. Discard the engine's lines/errors.
104
- sent = true;
105
- const result = await activate(ctx, { kind: 'api-key', agentId, authChoice: o.authChoice, apiKey: o.apiKey }, { timeoutMs: 180_000 });
106
- if (!result.ok && !result.late)
107
- return abandon(probeResult(result.status));
108
- if (result.late)
109
- return abandon(failure('retry', 'busy'));
110
- if (o.model !== undefined && !(await models(member, o)).some((m) => m.id === o.model && m.available))
111
- return abandon(failure('not_included', 'model'));
112
- // Prepare before publishing the ready marker: failed preparation remains unusable even if clear fails.
77
+ add(member, o) {
78
+ const ac = new AbortController();
79
+ const cancel = () => { if (!ac.signal.aborted)
80
+ ac.abort(); };
81
+ const done = (async () => {
82
+ if (!check(member))
83
+ throw new Error('Choose your own member.');
84
+ if (!o.apiKey.trim())
85
+ return failure('invalid', 'empty');
86
+ const route = routes().find((r) => r.choice === o.authChoice && r.keyEntry && r.readiness === 'ready');
87
+ if (!route || (o.model !== undefined && !LIVE_CATALOGS.includes(route.provider)))
88
+ return failure('not_included', 'model');
89
+ if (ac.signal.aborted)
90
+ return failure('retry', 'busy');
91
+ return exclusive(member, async () => {
92
+ let sent = false;
93
+ // A key the engine may have taken but not sealed is cleared (prepare already emptied the agent); if the clear
94
+ // does not reach the engine, the key stays unsealed and unusable until the next key add or kit start, so runs
95
+ // fail closed.
96
+ const abandon = async (result) => {
97
+ await control(member, 'clear').catch(() => undefined);
98
+ return result;
99
+ };
100
+ const cancelled = () => failure('retry', 'busy');
113
101
  try {
114
- await ctx.prepareOwner(agentId);
102
+ if (ac.signal.aborted)
103
+ return cancelled();
104
+ await ctx.ensure(member);
105
+ const agentId = keyAgentId(member);
106
+ const list = await ctx.request('agents.list');
107
+ if (!list.agents.some((a) => a.id === agentId))
108
+ await ctx.request('agents.create', { name: agentId, workspace: join(ctx.root, 'key-workspaces', member) });
109
+ // Creation answers before the runtime roster/owners publish, just as for ordinary members.
110
+ await untilListed(ctx.request, agentId);
111
+ if (ac.signal.aborted)
112
+ return cancelled();
113
+ if (!(await control(member, 'prepare', route.provider)).ok)
114
+ return failure('retry', 'engine');
115
+ // The public pin's typed API-key path, never a CLI-login import. Discard the engine's lines/errors.
116
+ sent = true;
117
+ if (ac.signal.aborted)
118
+ return abandon(cancelled());
119
+ const result = await activate(ctx, { kind: 'api-key', agentId, authChoice: o.authChoice, apiKey: o.apiKey }, { timeoutMs: 180_000, signal: ac.signal });
120
+ if (ac.signal.aborted)
121
+ return abandon(cancelled());
122
+ if (!result.ok && !result.late)
123
+ return abandon(probeResult(result.status));
124
+ if (result.late)
125
+ return abandon(failure('retry', 'busy'));
126
+ if (o.model !== undefined && !(await models(member, o)).some((m) => m.id === o.model && m.available))
127
+ return abandon(failure('not_included', 'model'));
128
+ // Prepare before publishing the ready marker: failed preparation remains unusable even if clear fails.
129
+ try {
130
+ await ctx.prepareOwner(agentId);
131
+ }
132
+ catch {
133
+ return abandon(failure('retry', 'engine'));
134
+ }
135
+ if (ac.signal.aborted)
136
+ return abandon(cancelled());
137
+ if (o.model !== undefined && LIVE_CATALOGS.includes(route.provider) && !(await registered(route.provider, o.model)))
138
+ return abandon(failure('retry', 'model'));
139
+ if (!(await control(member, 'seal', undefined, o.model)).ok)
140
+ return abandon(failure('retry', 'engine'));
141
+ await ctx.indexLane(member, route);
142
+ return { status: 'ok' };
115
143
  }
116
- catch {
117
- return abandon(failure('retry', 'engine'));
144
+ catch (error) {
145
+ const reason = errorReason(error, sent);
146
+ if (sent)
147
+ return abandon(failure('retry', reason));
148
+ return failure('retry', reason);
118
149
  }
119
- if (o.model !== undefined && LIVE_CATALOGS.includes(route.provider) && !(await registered(route.provider, o.model)))
120
- return abandon(failure('retry', 'model'));
121
- if ((await control(member, 'seal', undefined, o.model)).ok)
122
- return { status: 'ok' };
123
- return abandon(failure('retry', 'engine'));
124
- }
125
- catch (error) {
126
- const reason = errorReason(error, sent);
127
- if (sent)
128
- return abandon(failure('retry', reason));
129
- return failure('retry', reason);
130
- }
131
- });
150
+ });
151
+ })();
152
+ return Object.assign(done, { cancel });
132
153
  },
133
154
  async ready(member, o = {}) {
134
155
  if (!check(member))
@@ -147,11 +168,37 @@ export function createKeys(ctx) {
147
168
  }
148
169
  }
149
170
  },
150
- // Kit start calls this once the gateway is ready: a failed add whose abandon clear never reached the engine
151
- // leaves an unsealed key with no ready marker, so drop it. A single ready read per key agent, no retries; an add
152
- // in flight for that member is waited for. Only an absent marker is cleared; a refused read or clear (the gateway
153
- // left ready) makes the sweep incomplete. Resolves true once every key agent was read.
154
- async sweep() {
171
+ // Remove this member's saved key: the key agent's store and its ready marker, so the lane reads signed out and
172
+ // stays signed out, plus its `<provider>.key` index entry, so the accounts list stops naming the removed key.
173
+ // `provider` scopes it to the key actually saved for that provider (the member agent's own
174
+ // sign-out never reaches this lane), so signing out of one provider never drops another's key. A lane that reads
175
+ // not ready has nothing saved to remove; a refused read or clear is a typed refusal, never raw engine text.
176
+ // Serialized with adds and key runs like every other key change.
177
+ async remove(member, o = {}) {
178
+ if (!check(member))
179
+ throw new Error('Choose your own member.');
180
+ await exclusive(member, async () => {
181
+ const refused = () => { throw new Error('The key could not be removed.'); };
182
+ if (o.provider !== undefined) {
183
+ // An unreadable lane cannot name its provider, so removal never guesses and risks another provider's key.
184
+ const ready = await control(member, 'ready').catch(() => undefined);
185
+ if (!ready)
186
+ refused();
187
+ else if (ready.ok !== true || providerOf(ready.model ?? '') !== o.provider)
188
+ return;
189
+ }
190
+ const cleared = await control(member, 'clear').catch(() => undefined);
191
+ if (cleared?.ok !== true && cleared?.absent !== true)
192
+ refused();
193
+ await ctx.dropLane(member, o.provider);
194
+ });
195
+ },
196
+ // Kit connect calls this once the gateway is ready: a failed add whose abandon clear never reached the engine
197
+ // leaves an unsealed key with no ready marker, so drop it (`clear`, until a sweep completes). A single ready read
198
+ // per key agent, no retries; an add in flight for that member is waited for. Only an absent marker is cleared; a
199
+ // refused read or clear (the gateway left ready) makes the sweep incomplete. A sealed lane is backfilled on every
200
+ // call. Resolves true once every key agent was read.
201
+ async sweep(clear) {
155
202
  let agents;
156
203
  try {
157
204
  agents = (await ctx.request('agents.list')).agents;
@@ -168,7 +215,11 @@ export function createKeys(ctx) {
168
215
  continue;
169
216
  await exclusive(member, async () => {
170
217
  const ready = await control(member, 'ready').catch(() => undefined);
171
- if (ready?.ok === true)
218
+ if (ready?.ok === true) {
219
+ await ctx.backfillLane(member, ready.model);
220
+ return;
221
+ }
222
+ if (!clear)
172
223
  return;
173
224
  if (ready?.absent !== true) {
174
225
  complete = false;
package/dist/kit.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { SealingAdapter } from '@byokit/secrets';
2
- import { type AddKeyResult } from './keys.ts';
2
+ import { type AddKeyHandle } from './keys.ts';
3
3
  import { type RouteView } from './routes.ts';
4
4
  import type { BrowserHost, BrowserOptions } from './browser.ts';
5
5
  import type { Account, AccountId, AccountPick, Approval, CallOptions, Decision, Defaults, GatewayMethod, GatewayParams, GatewayResult, GatewayTransport, Hello, KitState, KitEventName, KitEventPayload, LearningCapture, LearningMode, Member, ModelInfo, MoveResult, Room, RunEnd, RunEvent, RunRef, RunSelection, RunSpec, OutputSchema, SchemaOutput, SignInOptions, SignInView, ToolHost, ToolSpec } from './types.ts';
@@ -148,6 +148,9 @@ export declare class OpenClawKit {
148
148
  }>;
149
149
  }>;
150
150
  renameAccount(member: Member, id: AccountId, name: string): Promise<Account>;
151
+ /** Remove one account: a member-agent account through `authLogout` with its profileIds, an account agent through
152
+ * `clear`, an empty `list`, `agents.delete` with `deleteFiles: false`, removal of its state dir, then the entry. */
153
+ removeAccount(member: Member, id: AccountId): Promise<void>;
151
154
  defaults(member: Member): Promise<Defaults>;
152
155
  setDefaults(member: Member, d: Defaults): Promise<void>;
153
156
  /** Save an explicitly supplied API key (billed per use) in this member's separate key agent. */
@@ -155,7 +158,16 @@ export declare class OpenClawKit {
155
158
  authChoice: string;
156
159
  apiKey: string;
157
160
  model?: string;
158
- }): Promise<AddKeyResult>;
161
+ }): AddKeyHandle;
162
+ private indexKeyLane;
163
+ /** Remove this member's saved key (the `byokit-key-<member>` lane: its credential store and ready marker), so
164
+ * `signedIn` is false for it and a restart cannot revive it. `provider` removes only that provider's saved key;
165
+ * with no saved key the call resolves without doing anything. */
166
+ removeKey(member: Member, o?: {
167
+ provider?: string;
168
+ }): Promise<void>;
169
+ private dropKeyLane;
170
+ private backfillKeyLane;
159
171
  private prepareKeyOwner;
160
172
  private syncCatalogs;
161
173
  migrateRetainedLogin(member: Member, source: RetainedLogin): Promise<'staged' | 'nothing' | 'failed'>;
package/dist/kit.js CHANGED
@@ -664,6 +664,9 @@ export class OpenClawKit {
664
664
  this.admission = createAuthStatus(this.engine.root, (method, params, options) => this.request()(method, params, options));
665
665
  this.keys = createKeys({ root: this.engine.root,
666
666
  prepareOwner: (agentId) => this.prepareKeyOwner(agentId),
667
+ indexLane: (member, route) => this.indexKeyLane(member, route),
668
+ dropLane: (member, provider) => this.dropKeyLane(member, provider),
669
+ backfillLane: (member, model) => this.backfillKeyLane(member, model),
667
670
  restarting: () => this.engineRestarting(),
668
671
  request: (method, params, options) => this.request()(method, params, options),
669
672
  ensure: (member) => this.ensureMember(member),
@@ -763,7 +766,7 @@ export class OpenClawKit {
763
766
  this.accountsCtl = createAccounts({ request: (method, params, o) => this.request()(method, params, o), root: this.engine.root,
764
767
  ensure: (member) => this.ensureMember(member), locks: this.locks,
765
768
  signIn: (agentId, o, on, fresh) => startSignIn({ ...this.signInCtx(), ensure: async () => ({ agentId }), fresh }, agentId, o, on),
766
- routes: this.routeMap, log: this.o.log });
769
+ routes: this.routeMap, log: this.o.log, reseal: () => this.engine.authStore.reseal() });
767
770
  // The fail-closed tool bridge feeds the kit's single Approval surface (5.9, O5).
768
771
  await this.bridge.start();
769
772
  this.off.push(transport.onEvent((e) => this.approvalsCtl.handleEvent(e)));
@@ -771,10 +774,11 @@ export class OpenClawKit {
771
774
  this.setState({ phase: 'ready' });
772
775
  this.syncCatalogs();
773
776
  // A failed key add whose abandon clear never reached the engine leaves an unsealed key with no ready marker;
774
- // runs stay closed, so drop it now that the gateway is ready. Ready keys keep their marker and survive.
775
- if (!this.swept)
776
- void this.keys.sweep().then((done) => { if (done)
777
- this.swept = true; }).catch(() => undefined);
777
+ // runs stay closed, so drop it now that the gateway is ready. Ready keys keep their marker and survive. Every
778
+ // connect backfills the key lanes' index entries; only the clear stops once a sweep completes.
779
+ const clear = !this.swept;
780
+ void this.keys.sweep(clear).then((done) => { if (clear && done)
781
+ this.swept = true; }).catch(() => undefined);
778
782
  if (this.o.browser)
779
783
  await this.checkBrowserTools();
780
784
  // Replays the engine's native approval lists over the now-live transport (N9).
@@ -951,7 +955,10 @@ export class OpenClawKit {
951
955
  // Public readiness/sign-in reads stay live; their fresh result can prepare the next run's admission.
952
956
  authStatus: (agentId, refresh, native) => this.admission.read(agentId, refresh, native, false),
953
957
  ensure: (member) => this.ensureMember(member), callbackPort: this.o.callbackPort ?? 1455,
958
+ nativeClaudeLogin: () => this.engine.nativeClaudeLogin(),
959
+ resetNativeClaudeLogin: () => this.engine.resetNativeClaudeLogin(),
954
960
  onDisconnect: (fn) => { this.signInDisconnects.add(fn); return () => { this.signInDisconnects.delete(fn); }; },
961
+ stopSignal: this.engine.stopSignal,
955
962
  authConfig: () => this.authSnapshot(),
956
963
  restarted: (authBefore, signal) => this.restarted(authBefore, signal) };
957
964
  }
@@ -966,24 +973,40 @@ export class OpenClawKit {
966
973
  return this.providerStatus(member).then((names) => names ?? []);
967
974
  }
968
975
  /** A member sign-in to `provider`, or a ready key (5.8) whose saved model is that provider's. */
969
- signedIn(member, provider) {
970
- return this.providers(member).then(async (names) => names.includes(provider)
971
- || providerOf((await this.keys.ready(member, { once: true }))?.model ?? '') === provider).catch(() => false);
976
+ async signedIn(member, provider) {
977
+ // The key probe stands alone: a member-agent admission failure after an engine restart must not hide a sealed key.
978
+ const keyReady = async () => {
979
+ try {
980
+ return providerOf((await this.keys.ready(member, { once: true }))?.model ?? '') === provider;
981
+ }
982
+ catch {
983
+ return false;
984
+ }
985
+ };
986
+ try {
987
+ if ((await this.providers(member)).includes(provider))
988
+ return true;
989
+ }
990
+ catch { /* key probe below */ }
991
+ return keyReady();
972
992
  }
973
993
  signIn(member, o, on) {
974
994
  return startSignIn(this.signInCtx(), member, o, on);
975
995
  }
976
- signOut(member, provider) {
977
- return engineSignOut(this.signInCtx(), member, provider);
996
+ async signOut(member, provider) {
997
+ await engineSignOut(this.signInCtx(), member, provider);
998
+ // The saved key lives on its own `byokit-key-<member>` agent, which the member-agent logout never reaches: without
999
+ // this, a key of that provider stays sealed and ready and the member reads signed in again after a restart.
1000
+ await this.keys.remove(member, { provider });
978
1001
  }
979
1002
  // The member an agent id or session key belongs to, through the account index (5.15, O14m). Reads the index file, so it
980
1003
  // needs no live gateway.
981
1004
  memberOf(ref) {
982
1005
  return memberOfAccount(this.engine.root, ref);
983
1006
  }
984
- // accounts (5.15, O14 steps S5, S6 and S9-S12): the per-member index with listing, adding, defaults, rename, models
985
- // and room; `run({ account })` and `pick` choose among them, held to the account a session is bound to, and `move`
986
- // carries a conversation to another account. Removing arrives in a later step.
1007
+ // accounts (5.15, O14 steps S5, S6 and S8-S12): the per-member index with listing, adding, defaults, rename, models
1008
+ // and room; `run({ account })` and `pick` choose among them, held to the account a session is bound to, `move`
1009
+ // carries a conversation to another account, and `removeAccount` deletes one.
987
1010
  accounts(member) {
988
1011
  if (!this.accountsCtl)
989
1012
  return Promise.reject(new Error('gateway not ready'));
@@ -1028,6 +1051,13 @@ export class OpenClawKit {
1028
1051
  return Promise.reject(new Error('gateway not ready'));
1029
1052
  return this.accountsCtl.rename(member, id, name);
1030
1053
  }
1054
+ /** Remove one account: a member-agent account through `authLogout` with its profileIds, an account agent through
1055
+ * `clear`, an empty `list`, `agents.delete` with `deleteFiles: false`, removal of its state dir, then the entry. */
1056
+ removeAccount(member, id) {
1057
+ if (!this.accountsCtl)
1058
+ return Promise.reject(new Error('gateway not ready'));
1059
+ return this.accountsCtl.remove(member, id);
1060
+ }
1031
1061
  defaults(member) {
1032
1062
  if (!this.accountsCtl)
1033
1063
  return Promise.reject(new Error('gateway not ready'));
@@ -1042,6 +1072,32 @@ export class OpenClawKit {
1042
1072
  addKey(agentId, o) {
1043
1073
  return this.keys.add(agentId, o);
1044
1074
  }
1075
+ // The lane's index entry follows its sealed key. A failed write is logged and never fails the add; the start sweep repairs it.
1076
+ async indexKeyLane(member, route) {
1077
+ if (!this.accountsCtl) {
1078
+ this.o.log?.('addKey: the account index is not ready, so the key lane is indexed at the next connect');
1079
+ return;
1080
+ }
1081
+ await this.accountsCtl.key(member, route).catch((error) => this.o.log?.(`addKey: the key lane is not indexed yet: ${error instanceof Error ? error.message : String(error)}`));
1082
+ }
1083
+ /** Remove this member's saved key (the `byokit-key-<member>` lane: its credential store and ready marker), so
1084
+ * `signedIn` is false for it and a restart cannot revive it. `provider` removes only that provider's saved key;
1085
+ * with no saved key the call resolves without doing anything. */
1086
+ removeKey(member, o) {
1087
+ return this.keys.remove(member, o);
1088
+ }
1089
+ // The lane's index entry follows its sealed key down. A failed drop is logged and never fails the removal.
1090
+ async dropKeyLane(member, provider) {
1091
+ if (!this.accountsCtl) {
1092
+ this.o.log?.('removeKey: the account index is not ready, so the key lane entry stays until the next removal');
1093
+ return;
1094
+ }
1095
+ await this.accountsCtl.unkey(member, provider).catch((error) => this.o.log?.(`removeKey: the key lane entry is not dropped yet: ${error instanceof Error ? error.message : String(error)}`));
1096
+ }
1097
+ async backfillKeyLane(member, model) {
1098
+ const found = model === undefined ? [] : routeTable().filter((r) => r.keyEntry && r.readiness === 'ready' && r.provider === providerOf(model));
1099
+ await this.accountsCtl?.backfill(member, found).catch((error) => this.o.log?.(`key lane ${member}: not indexed yet: ${error instanceof Error ? error.message : String(error)}`));
1100
+ }
1045
1101
  async prepareKeyOwner(agentId) {
1046
1102
  this.syncCatalogs();
1047
1103
  await this.catalogs;
@@ -1137,6 +1193,7 @@ export class OpenClawKit {
1137
1193
  });
1138
1194
  return this.runs().run(spec, on, undefined, output);
1139
1195
  }
1196
+ // An API-key run's session is on the key lane (Runs step 2).
1140
1197
  runKey(sessionKey, o) {
1141
1198
  if (o?.auth !== 'apiKey')
1142
1199
  return sessionKey;
@@ -1145,7 +1202,7 @@ export class OpenClawKit {
1145
1202
  throw new Error('Choose your own conversation.');
1146
1203
  return `agent:${keyAgentId(member)}:${tail}`;
1147
1204
  }
1148
- steer(sessionKey, text, o) {
1205
+ async steer(sessionKey, text, o) {
1149
1206
  return this.runs().steer(this.runKey(sessionKey, o), text);
1150
1207
  }
1151
1208
  async abort(sessionKey, o) {
package/dist/link.js CHANGED
@@ -322,12 +322,16 @@ export function openclawLink(kit, o) {
322
322
  drives.get(key)?.cancel();
323
323
  const view = { state: 'waiting', via: args.via };
324
324
  latest.set(key, view);
325
- const drive = kit.signIn(member, { authChoice: route.choice, via: args.via }, (next) => {
326
- latest.set(key, next);
327
- });
325
+ // A superseded attempt (still activating past its commit point) never writes over the newer one's view.
326
+ let drive;
327
+ const current = () => drive !== undefined && drives.get(key) === drive;
328
+ drive = kit.signIn(member, { authChoice: route.choice, via: args.via }, (next) => { if (current())
329
+ latest.set(key, next); });
328
330
  drives.set(key, drive);
329
- void drive.done.then((done) => { latest.set(key, done); if (drives.get(key) === drive)
330
- drives.delete(key); }, () => { if (drives.get(key) === drive)
331
+ void drive.done.then((done) => { if (current()) {
332
+ latest.set(key, done);
333
+ drives.delete(key);
334
+ } }, () => { if (current())
331
335
  drives.delete(key); });
332
336
  return latest.get(key);
333
337
  }
@@ -336,7 +340,9 @@ export function openclawLink(kit, o) {
336
340
  throw new Error('oc.signin.view needs { provider }');
337
341
  // `ready` is the member's account (signed in to this provider), not the engine: `phaseOf` reads it as done.
338
342
  const key = `${member}:${args.provider}`;
339
- const ready = kit.state.phase === 'ready' && await kit.signedIn(member, args.provider);
343
+ // Native Claude is ready only once this attempt's live activation settles.
344
+ const native = args.provider === 'claude-cli';
345
+ const ready = kit.state.phase === 'ready' && await kit.signedIn(member, args.provider) && (!native || (!drives.has(key) && latest.get(key)?.state !== 'failed'));
340
346
  // A finished sign-in whose account is gone since (signed out anywhere, or dropped by the engine) is no
341
347
  // sign-in to show: without this the view says done while `ready` says no, which phaseOf reads as opening.
342
348
  if (!ready && !drives.has(key) && latest.get(key)?.state === 'done')
package/dist/pick.js CHANGED
@@ -154,6 +154,10 @@ export function resolveSelection(accounts, defaults, sel, room, nowMs, models, b
154
154
  const rests = eligible.filter((c) => c.row.out === 'resting');
155
155
  if (rests.length)
156
156
  return restingPick(rests.reduce((a, b) => ((b.row.until ?? Infinity) < (a.row.until ?? Infinity) ? b : a)));
157
+ // An empty `bound` (a member-agent conversation holding no indexed account) names no provider: every account is
158
+ // signed in somewhere else, so the provider-specific sentence would mislead.
159
+ if (bound !== undefined && !bound.length)
160
+ return { ok: false, code: 'none', reason: words('auto.noneBound'), considered };
157
161
  return { ok: false, code: 'none', reason: words('auto.none', { provider: provider ?? '' }), considered };
158
162
  function finish(account, row, how, why) {
159
163
  const listed = models?.(account);
package/dist/runs.js CHANGED
@@ -6,6 +6,7 @@ import { outputSchema } from "./output.js";
6
6
  import { words } from "./words.js";
7
7
  import { classify } from "./classify.js";
8
8
  import { providerOf } from "./pick.js";
9
+ import { catalogModelOf, clampThinking, levelsOf } from "./thinking.js";
9
10
  // The gateway gives up on a silent run after an hour; we wait ten minutes longer for the reply frame itself.
10
11
  const WAIT_MS = 3_600_000;
11
12
  const WAIT_CLIENT_MS = 3_610_000;
@@ -109,7 +110,7 @@ export function createRuns(ctx) {
109
110
  const tail = spec.sessionKey.slice(spec.sessionKey.indexOf(':', 'agent:'.length) + 1);
110
111
  // Runs step 1 (5.15): every run first finds the account its session is bound to, the API-key lane included.
111
112
  const binding = ctx.accounts ? await ctx.accounts.bind(spec.member, spec.sessionKey) : undefined;
112
- // Step 2 on the API-key lane: only an API-billed conversation continues on its key; any other binding is refused.
113
+ // Step 2 on the API-key lane: its own lane binding continues; any other binding is refused.
113
114
  if (keyAgent && ctx.accounts && binding) {
114
115
  const message = await ctx.accounts.keyRefusal(spec.member, binding);
115
116
  if (message !== undefined)
@@ -119,8 +120,8 @@ export function createRuns(ctx) {
119
120
  // session went to another account, so it ends before any event and the caller's retry lands there.
120
121
  const moved = async () => !!binding && (await ctx.accounts.bind(spec.member, spec.sessionKey))?.agentId !== binding.agentId;
121
122
  const movedEnd = { ok: false, kind: 'other', message: words('run.moved') };
122
- // Step 3: with `account` and `needs` both absent, on a key no account agent holds (or an API-key run, whose
123
- // binding is API-billed), exactly the run on the agent it always was (a bound key under its shared lock).
123
+ // Step 3: with `account` and `needs` both absent, on a key no account agent holds (or an API-key run, which binds only
124
+ // to its lane), exactly the run on the agent it always was (a bound key under its shared lock).
124
125
  if ((keyAgent || binding?.id === undefined) && spec.account === undefined && !spec.needs?.length) {
125
126
  if (keyAgent)
126
127
  return start(spec, on, { agentId: keyAgent, sessionKey: `agent:${keyAgent}:${tail}`, idempotencyKey, system, picked, output });
@@ -197,6 +198,19 @@ export function createRuns(ctx) {
197
198
  return { ...end, until };
198
199
  }
199
200
  };
201
+ const thinkingOf = async (picked, asked) => {
202
+ if (asked === undefined)
203
+ return undefined;
204
+ if (!picked)
205
+ return { asked, used: asked };
206
+ try {
207
+ const snap = await ctx.request('config.get', {});
208
+ return clampThinking(asked, levelsOf(catalogModelOf(snap.config?.models?.providers, picked.provider, picked.model)));
209
+ }
210
+ catch {
211
+ return { asked, used: asked };
212
+ }
213
+ };
200
214
  const start = async (spec, on, t) => {
201
215
  const { agentId, sessionKey, idempotencyKey, system, picked, output, chosen } = t;
202
216
  const release = spec.register !== false ? ctx.bridge.register({ sessionKey, member: spec.member }, spec.tools, idempotencyKey) : undefined;
@@ -253,7 +267,9 @@ export function createRuns(ctx) {
253
267
  return { ok: false, kind: 'signed-out', message: `${picked.provider} is not signed in for ${spec.member}` };
254
268
  }
255
269
  // One `agent` request: its interim `accepted` frame names the run, its final frame (after the run) carries the
256
- // run's usage. The wait below still decides how the run ended.
270
+ // run's usage. The wait below still decides how the run ended. Thinking is clamped to the catalog before the
271
+ // request so an unsupported level (e.g. Muse's off/high/max) never fails the engine.
272
+ const thinking = await thinkingOf(picked, spec.thinking);
257
273
  let accepted;
258
274
  const ack = new Promise((resolve) => { accepted = resolve; });
259
275
  // The run id is taken as the accepted frame lands, so no event read after it is missed.
@@ -274,7 +290,7 @@ export function createRuns(ctx) {
274
290
  ...(picked ?? {}),
275
291
  ...(system ? { extraSystemPrompt: system } : {}),
276
292
  ...(spec.images ? { attachments: spec.images.map((image) => ({ mimeType: image.mimeType, content: image.data })) } : {}),
277
- ...(spec.thinking ? { thinking: spec.thinking } : {}),
293
+ ...(thinking !== undefined ? { thinking: thinking.used } : {}),
278
294
  }, { expectFinal: true, timeoutMs: WAIT_CLIENT_MS, onAccepted });
279
295
  final.catch(() => { }); // a late failure is the wait's to report
280
296
  const started = await Promise.race([ack, final]);
@@ -307,7 +323,8 @@ export function createRuns(ctx) {
307
323
  const reported = typeof agentMeta.provider === 'string' && typeof agentMeta.model === 'string'
308
324
  ? `${agentMeta.provider}/${agentMeta.model}`.toLowerCase() : '';
309
325
  return { ok: true, text, ...(parsed ? { data: parsed.data } : {}), ...(usage ? { usage } : {}), ...(planWindow ? { planWindow } : {}),
310
- ...(chosen ? { account: chosen.account.id, model: chosen.model || reported } : {}) };
326
+ ...(chosen ? { account: chosen.account.id, model: chosen.model || reported } : {}),
327
+ ...(thinking ? { thinking } : {}) };
311
328
  }
312
329
  if (result.stopReason === 'aborted' || (abortedByEngine && result.status !== 'ok'))
313
330
  return { ok: false, aborted: true };
package/dist/signin.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { remoteCliHost } from '@byokit/accounts/cli';
1
2
  import { authStatus } from './auth-status.ts';
2
3
  import type { GatewayTransport, Member, SignInOptions, SignInView } from './types.ts';
3
4
  export type SignInCtx = {
@@ -7,9 +8,12 @@ export type SignInCtx = {
7
8
  agentId: string;
8
9
  }>;
9
10
  callbackPort: number;
11
+ nativeClaudeLogin?(): ReturnType<typeof remoteCliHost>;
12
+ resetNativeClaudeLogin?(): Promise<void>;
10
13
  onDisconnect?(fn: () => void): () => void;
11
14
  authConfig(): string;
12
15
  restarted(authBefore: string | undefined, signal?: AbortSignal): Promise<boolean>;
16
+ stopSignal?: AbortSignal;
13
17
  fresh?: boolean;
14
18
  };
15
19
  /**