@byokit/openclaw 0.13.0 → 0.14.2

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,9 +1,10 @@
1
1
  import { join } from 'node:path';
2
- import { activate } from "./activation.js";
3
- import { keyAgentId, MEMBER_ID, KEY_PREFIX, untilListed } from "./members.js";
4
- import { routes } from "./routes.js";
5
- import { modelsOf } from "./accounts.js";
6
- import { LIVE_CATALOGS } from "./constants.js";
2
+ import { activate } from './activation.js';
3
+ import { keyAgentId, MEMBER_ID, KEY_PREFIX, untilListed } from './members.js';
4
+ import { providerOf } from './pick.js';
5
+ import { routes } from './routes.js';
6
+ import { modelsOf } from './accounts.js';
7
+ import { LIVE_CATALOGS } from './constants.js';
7
8
  export function createKeys(ctx) {
8
9
  // Serialize key replacement and key runs, so an in-flight run never sees a half-installed replacement.
9
10
  const tails = new Map();
@@ -73,62 +74,96 @@ 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
+ // A workspace names a live-catalog login's own workspace only; another route takes no header, and a
90
+ // refusal here leaves the member's lanes untouched (it lands before any prepare or activation).
91
+ const orgId = typeof o.orgId === 'string' ? o.orgId.trim() : '';
92
+ if (orgId) {
93
+ if (orgId.length > 256)
94
+ return failure('invalid', 'auth');
95
+ if (!LIVE_CATALOGS.includes(route.provider))
96
+ return failure('not_included', 'model');
97
+ }
98
+ if (ac.signal.aborted)
99
+ return failure('retry', 'busy');
100
+ return exclusive(member, async () => {
101
+ let sent = false;
102
+ // A key the engine may have taken but not sealed is cleared (prepare already emptied the agent); if the clear
103
+ // does not reach the engine, the key stays unsealed and unusable until the next key add or kit start, so runs
104
+ // fail closed.
105
+ const abandon = async (result) => {
106
+ await control(member, 'clear').catch(() => undefined);
107
+ return result;
108
+ };
109
+ const cancelled = () => failure('retry', 'busy');
113
110
  try {
114
- await ctx.prepareOwner(agentId);
111
+ if (ac.signal.aborted)
112
+ return cancelled();
113
+ await ctx.ensure(member);
114
+ const agentId = keyAgentId(member);
115
+ const list = await ctx.request('agents.list');
116
+ if (!list.agents.some((a) => a.id === agentId))
117
+ await ctx.request('agents.create', { name: agentId, workspace: join(ctx.root, 'key-workspaces', member) });
118
+ // Creation answers before the runtime roster/owners publish, just as for ordinary members.
119
+ await untilListed(ctx.request, agentId);
120
+ if (ac.signal.aborted)
121
+ return cancelled();
122
+ if (!(await control(member, 'prepare', route.provider)).ok)
123
+ return failure('retry', 'engine');
124
+ // The workspace rides the provider's probe and every run from its config; it is written before the
125
+ // activation, so the probe itself carries it. The slot is provider-wide (shared by every member's
126
+ // key on this engine), so a replacement with no `orgId` keeps the standing one and `removeKey` drops it.
127
+ if (orgId)
128
+ await ctx.setProviderHeaders(route.provider, { 'x-opencode-org-id': orgId });
129
+ // The public pin's typed API-key path, never a CLI-login import. Discard the engine's lines/errors.
130
+ sent = true;
131
+ if (ac.signal.aborted)
132
+ return abandon(cancelled());
133
+ const result = await activate(ctx, { kind: 'api-key', agentId, authChoice: o.authChoice, apiKey: o.apiKey }, { timeoutMs: 180_000, signal: ac.signal });
134
+ if (ac.signal.aborted)
135
+ return abandon(cancelled());
136
+ if (!result.ok && !result.late)
137
+ return abandon(probeResult(result.status));
138
+ if (result.late)
139
+ return abandon(failure('retry', 'busy'));
140
+ if (o.model !== undefined && !(await models(member, o)).some((m) => m.id === o.model && m.available))
141
+ return abandon(failure('not_included', 'model'));
142
+ // Prepare before publishing the ready marker: failed preparation remains unusable even if clear fails.
143
+ try {
144
+ await ctx.prepareOwner(agentId);
145
+ }
146
+ catch {
147
+ return abandon(failure('retry', 'engine'));
148
+ }
149
+ if (ac.signal.aborted)
150
+ return abandon(cancelled());
151
+ if (o.model !== undefined && LIVE_CATALOGS.includes(route.provider) && !(await registered(route.provider, o.model)))
152
+ return abandon(failure('retry', 'model'));
153
+ if (!(await control(member, 'seal', undefined, o.model)).ok)
154
+ return abandon(failure('retry', 'engine'));
155
+ await ctx.indexLane(member, route);
156
+ return { status: 'ok' };
115
157
  }
116
- catch {
117
- return abandon(failure('retry', 'engine'));
158
+ catch (error) {
159
+ const reason = errorReason(error, sent);
160
+ if (sent)
161
+ return abandon(failure('retry', reason));
162
+ return failure('retry', reason);
118
163
  }
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
- });
164
+ });
165
+ })();
166
+ return Object.assign(done, { cancel });
132
167
  },
133
168
  async ready(member, o = {}) {
134
169
  if (!check(member))
@@ -147,11 +182,41 @@ export function createKeys(ctx) {
147
182
  }
148
183
  }
149
184
  },
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() {
185
+ // Remove this member's saved key: the key agent's store and its ready marker, so the lane reads signed out and
186
+ // stays signed out, plus its `<provider>.key` index entry, so the accounts list stops naming the removed key.
187
+ // `provider` scopes it to the key actually saved for that provider (the member agent's own
188
+ // sign-out never reaches this lane), so signing out of one provider never drops another's key. A lane that reads
189
+ // not ready has nothing saved to remove; a refused read or clear is a typed refusal, never raw engine text.
190
+ // Serialized with adds and key runs like every other key change.
191
+ async remove(member, o = {}) {
192
+ if (!check(member))
193
+ throw new Error('Choose your own member.');
194
+ await exclusive(member, async () => {
195
+ const refused = () => { throw new Error('The key could not be removed.'); };
196
+ if (o.provider !== undefined) {
197
+ // An unreadable lane cannot name its provider, so removal never guesses and risks another provider's key.
198
+ const ready = await control(member, 'ready').catch(() => undefined);
199
+ if (!ready)
200
+ refused();
201
+ else if (ready.ok !== true || providerOf(ready.model ?? '') !== o.provider)
202
+ return;
203
+ }
204
+ // The lane's own provider, when the removal can name it: the header clear below is scoped like the removal.
205
+ const cleared = await control(member, 'clear').catch(() => undefined);
206
+ if (cleared?.ok !== true && cleared?.absent !== true)
207
+ refused();
208
+ // A workspace header the removed key named must not outlive it; the next add names its own (or none).
209
+ if (o.provider !== undefined && LIVE_CATALOGS.includes(o.provider))
210
+ await ctx.setProviderHeaders(o.provider, null).catch(() => { throw new Error('The key could not be removed.'); });
211
+ await ctx.dropLane(member, o.provider);
212
+ });
213
+ },
214
+ // Kit connect calls this once the gateway is ready: a failed add whose abandon clear never reached the engine
215
+ // leaves an unsealed key with no ready marker, so drop it (`clear`, until a sweep completes). A single ready read
216
+ // per key agent, no retries; an add in flight for that member is waited for. Only an absent marker is cleared; a
217
+ // refused read or clear (the gateway left ready) makes the sweep incomplete. A sealed lane is backfilled on every
218
+ // call. Resolves true once every key agent was read.
219
+ async sweep(clear) {
155
220
  let agents;
156
221
  try {
157
222
  agents = (await ctx.request('agents.list')).agents;
@@ -168,7 +233,11 @@ export function createKeys(ctx) {
168
233
  continue;
169
234
  await exclusive(member, async () => {
170
235
  const ready = await control(member, 'ready').catch(() => undefined);
171
- if (ready?.ok === true)
236
+ if (ready?.ok === true) {
237
+ await ctx.backfillLane(member, ready.model);
238
+ return;
239
+ }
240
+ if (!clear)
172
241
  return;
173
242
  if (ready?.absent !== true) {
174
243
  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';
@@ -153,12 +153,24 @@ export declare class OpenClawKit {
153
153
  removeAccount(member: Member, id: AccountId): Promise<void>;
154
154
  defaults(member: Member): Promise<Defaults>;
155
155
  setDefaults(member: Member, d: Defaults): Promise<void>;
156
- /** Save an explicitly supplied API key (billed per use) in this member's separate key agent. */
156
+ /** Save an explicitly supplied API key (billed per use) in this member's separate key agent.
157
+ * `orgId` names the workspace a Go plan login acts for (the pick the app captured at connect);
158
+ * it rides every probe and run as `x-opencode-org-id`, and replacing the key rewrites it. */
157
159
  addKey(agentId: Member, o: {
158
160
  authChoice: string;
159
161
  apiKey: string;
160
162
  model?: string;
161
- }): Promise<AddKeyResult>;
163
+ orgId?: string;
164
+ }): AddKeyHandle;
165
+ private indexKeyLane;
166
+ /** Remove this member's saved key (the `byokit-key-<member>` lane: its credential store and ready marker), so
167
+ * `signedIn` is false for it and a restart cannot revive it. `provider` removes only that provider's saved key;
168
+ * with no saved key the call resolves without doing anything. */
169
+ removeKey(member: Member, o?: {
170
+ provider?: string;
171
+ }): Promise<void>;
172
+ private dropKeyLane;
173
+ private backfillKeyLane;
162
174
  private prepareKeyOwner;
163
175
  private syncCatalogs;
164
176
  migrateRetainedLogin(member: Member, source: RetainedLogin): Promise<'staged' | 'nothing' | 'failed'>;
package/dist/kit.js CHANGED
@@ -3,29 +3,29 @@ import { join } from 'node:path';
3
3
  import { setTimeout as delay } from 'node:timers/promises';
4
4
  import { isDeepStrictEqual } from 'node:util';
5
5
  import { fileURLToPath } from 'node:url';
6
- import { PROTOCOL_VERSION, LIVE_CATALOGS } from "./constants.js";
7
- import { atomic } from "./engine-patches.js";
8
- import { Approvals } from "./approvals.js";
9
- import { Bridge } from "./bridge.js";
10
- import { Engine, refusedOnly } from "./engine.js";
11
- import { AuthStoreSealSizeError } from "./auth-store.js";
12
- import { gatewayTransport } from "./transport.js";
13
- import { createMembers } from "./members.js";
14
- import { createAccounts, memberOfAccount } from "./accounts.js";
15
- import { createLocks } from "./locks.js";
16
- import { keyAgentId, KEY_PREFIX, MEMBER_ID } from "./members.js";
17
- import { confirmRetainedLogin as confirmLogin, migrateRetainedLogin as migrateLogin } from "./migrate.js";
18
- import { createRuns, signedInProviders } from "./runs.js";
19
- import { authStatus, createAuthStatus } from "./auth-status.js";
20
- import { outputSchema } from "./output.js";
21
- import { createKeys } from "./keys.js";
22
- import { routes as routeTable } from "./routes.js";
23
- import { providerOf } from "./pick.js";
24
- import { signIn as startSignIn, signOut as engineSignOut } from "./signin.js";
25
- import { reconcileConfig, browserToolPolicySafe, browserProfileAcknowledged, browserSessionMayRun, memoryLimited as configMemoryLimited, configKeyPath, readConfigKey, writeConfigKey } from "./config.js";
26
- import { createBrowserHost } from "./browser/host.js";
27
- import { fileSignInStore } from "./browser/store.js";
28
- import { launchBroker } from "./browser/broker.js";
6
+ import { PROTOCOL_VERSION, LIVE_CATALOGS } from './constants.js';
7
+ import { atomic } from './engine-patches.js';
8
+ import { Approvals } from './approvals.js';
9
+ import { Bridge } from './bridge.js';
10
+ import { Engine, canonicalStateDir, refusedOnly } from './engine.js';
11
+ import { AuthStoreSealSizeError } from './auth-store.js';
12
+ import { gatewayTransport } from './transport.js';
13
+ import { createMembers } from './members.js';
14
+ import { createAccounts, memberOfAccount } from './accounts.js';
15
+ import { createLocks } from './locks.js';
16
+ import { keyAgentId, KEY_PREFIX, MEMBER_ID } from './members.js';
17
+ import { confirmRetainedLogin as confirmLogin, migrateRetainedLogin as migrateLogin } from './migrate.js';
18
+ import { createRuns, signedInProviders } from './runs.js';
19
+ import { authStatus, createAuthStatus } from './auth-status.js';
20
+ import { outputSchema } from './output.js';
21
+ import { createKeys } from './keys.js';
22
+ import { routes as routeTable } from './routes.js';
23
+ import { providerOf } from './pick.js';
24
+ import { signIn as startSignIn, signOut as engineSignOut } from './signin.js';
25
+ import { reconcileConfig, browserToolPolicySafe, browserProfileAcknowledged, browserSessionMayRun, memoryLimited as configMemoryLimited, configKeyPath, readConfigKey, writeConfigKey } from './config.js';
26
+ import { createBrowserHost } from './browser/host.js';
27
+ import { fileSignInStore } from './browser/store.js';
28
+ import { launchBroker } from './browser/broker.js';
29
29
  /** The engine's learning-mode config key (5.6 "Engine learning state"). */
30
30
  const LEARNING_KEY = 'skills.workshop.autonomous.mode';
31
31
  const RESTART_MS = 60_000;
@@ -651,7 +651,7 @@ export class OpenClawKit {
651
651
  || (o.browser && ['browser', 'request_sign_in', 'exec', 'process', 'code_execution', 'terminal', 'read', 'write', 'edit', 'apply_patch', 'gateway'].includes(t.name)))
652
652
  throw new Error(`invalid tool name: ${t.name}`);
653
653
  }
654
- this.o = o;
654
+ this.o = { ...o, stateDir: canonicalStateDir(o.stateDir) };
655
655
  this.engine = new Engine({ ...o, pluginId: o.plugin?.id ?? 'byokit', tools: [...o.tools ?? [], ...(o.browser ? [{
656
656
  name: 'request_sign_in', description: 'Ask the person to sign in to the current browser site.',
657
657
  parameters: { type: 'object', additionalProperties: false, properties: {
@@ -664,11 +664,15 @@ 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),
670
673
  authConfig: () => this.authSnapshot(),
671
- restarted: (authBefore, signal) => this.restarted(authBefore, signal) });
674
+ restarted: (authBefore, signal) => this.restarted(authBefore, signal),
675
+ setProviderHeaders: (provider, headers) => this.patchConfig({ models: { providers: { [provider]: { headers: headers ?? null } } } }) });
672
676
  const slot = {};
673
677
  this.approvalsCtl = new Approvals({
674
678
  request: (method, params, co) => this.request()(method, params, co),
@@ -771,10 +775,11 @@ export class OpenClawKit {
771
775
  this.setState({ phase: 'ready' });
772
776
  this.syncCatalogs();
773
777
  // 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);
778
+ // runs stay closed, so drop it now that the gateway is ready. Ready keys keep their marker and survive. Every
779
+ // connect backfills the key lanes' index entries; only the clear stops once a sweep completes.
780
+ const clear = !this.swept;
781
+ void this.keys.sweep(clear).then((done) => { if (clear && done)
782
+ this.swept = true; }).catch(() => undefined);
778
783
  if (this.o.browser)
779
784
  await this.checkBrowserTools();
780
785
  // Replays the engine's native approval lists over the now-live transport (N9).
@@ -951,7 +956,10 @@ export class OpenClawKit {
951
956
  // Public readiness/sign-in reads stay live; their fresh result can prepare the next run's admission.
952
957
  authStatus: (agentId, refresh, native) => this.admission.read(agentId, refresh, native, false),
953
958
  ensure: (member) => this.ensureMember(member), callbackPort: this.o.callbackPort ?? 1455,
959
+ nativeClaudeLogin: () => this.engine.nativeClaudeLogin(),
960
+ resetNativeClaudeLogin: () => this.engine.resetNativeClaudeLogin(),
954
961
  onDisconnect: (fn) => { this.signInDisconnects.add(fn); return () => { this.signInDisconnects.delete(fn); }; },
962
+ stopSignal: this.engine.stopSignal,
955
963
  authConfig: () => this.authSnapshot(),
956
964
  restarted: (authBefore, signal) => this.restarted(authBefore, signal) };
957
965
  }
@@ -966,15 +974,31 @@ export class OpenClawKit {
966
974
  return this.providerStatus(member).then((names) => names ?? []);
967
975
  }
968
976
  /** 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);
977
+ async signedIn(member, provider) {
978
+ // The key probe stands alone: a member-agent admission failure after an engine restart must not hide a sealed key.
979
+ const keyReady = async () => {
980
+ try {
981
+ return providerOf((await this.keys.ready(member, { once: true }))?.model ?? '') === provider;
982
+ }
983
+ catch {
984
+ return false;
985
+ }
986
+ };
987
+ try {
988
+ if ((await this.providers(member)).includes(provider))
989
+ return true;
990
+ }
991
+ catch { /* key probe below */ }
992
+ return keyReady();
972
993
  }
973
994
  signIn(member, o, on) {
974
995
  return startSignIn(this.signInCtx(), member, o, on);
975
996
  }
976
- signOut(member, provider) {
977
- return engineSignOut(this.signInCtx(), member, provider);
997
+ async signOut(member, provider) {
998
+ await engineSignOut(this.signInCtx(), member, provider);
999
+ // The saved key lives on its own `byokit-key-<member>` agent, which the member-agent logout never reaches: without
1000
+ // this, a key of that provider stays sealed and ready and the member reads signed in again after a restart.
1001
+ await this.keys.remove(member, { provider });
978
1002
  }
979
1003
  // The member an agent id or session key belongs to, through the account index (5.15, O14m). Reads the index file, so it
980
1004
  // needs no live gateway.
@@ -1045,10 +1069,38 @@ export class OpenClawKit {
1045
1069
  return Promise.reject(new Error('gateway not ready'));
1046
1070
  return this.accountsCtl.setDefaults(member, d);
1047
1071
  }
1048
- /** Save an explicitly supplied API key (billed per use) in this member's separate key agent. */
1072
+ /** Save an explicitly supplied API key (billed per use) in this member's separate key agent.
1073
+ * `orgId` names the workspace a Go plan login acts for (the pick the app captured at connect);
1074
+ * it rides every probe and run as `x-opencode-org-id`, and replacing the key rewrites it. */
1049
1075
  addKey(agentId, o) {
1050
1076
  return this.keys.add(agentId, o);
1051
1077
  }
1078
+ // The lane's index entry follows its sealed key. A failed write is logged and never fails the add; the start sweep repairs it.
1079
+ async indexKeyLane(member, route) {
1080
+ if (!this.accountsCtl) {
1081
+ this.o.log?.('addKey: the account index is not ready, so the key lane is indexed at the next connect');
1082
+ return;
1083
+ }
1084
+ 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)}`));
1085
+ }
1086
+ /** Remove this member's saved key (the `byokit-key-<member>` lane: its credential store and ready marker), so
1087
+ * `signedIn` is false for it and a restart cannot revive it. `provider` removes only that provider's saved key;
1088
+ * with no saved key the call resolves without doing anything. */
1089
+ removeKey(member, o) {
1090
+ return this.keys.remove(member, o);
1091
+ }
1092
+ // The lane's index entry follows its sealed key down. A failed drop is logged and never fails the removal.
1093
+ async dropKeyLane(member, provider) {
1094
+ if (!this.accountsCtl) {
1095
+ this.o.log?.('removeKey: the account index is not ready, so the key lane entry stays until the next removal');
1096
+ return;
1097
+ }
1098
+ 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)}`));
1099
+ }
1100
+ async backfillKeyLane(member, model) {
1101
+ const found = model === undefined ? [] : routeTable().filter((r) => r.keyEntry && r.readiness === 'ready' && r.provider === providerOf(model));
1102
+ await this.accountsCtl?.backfill(member, found).catch((error) => this.o.log?.(`key lane ${member}: not indexed yet: ${error instanceof Error ? error.message : String(error)}`));
1103
+ }
1052
1104
  async prepareKeyOwner(agentId) {
1053
1105
  this.syncCatalogs();
1054
1106
  await this.catalogs;
@@ -1144,6 +1196,7 @@ export class OpenClawKit {
1144
1196
  });
1145
1197
  return this.runs().run(spec, on, undefined, output);
1146
1198
  }
1199
+ // An API-key run's session is on the key lane (Runs step 2).
1147
1200
  runKey(sessionKey, o) {
1148
1201
  if (o?.auth !== 'apiKey')
1149
1202
  return sessionKey;
@@ -1152,7 +1205,7 @@ export class OpenClawKit {
1152
1205
  throw new Error('Choose your own conversation.');
1153
1206
  return `agent:${keyAgentId(member)}:${tail}`;
1154
1207
  }
1155
- steer(sessionKey, text, o) {
1208
+ async steer(sessionKey, text, o) {
1156
1209
  return this.runs().steer(this.runKey(sessionKey, o), text);
1157
1210
  }
1158
1211
  async abort(sessionKey, o) {
package/dist/link.js CHANGED
@@ -7,11 +7,11 @@ import { createServer } from 'node:http';
7
7
  import { WebSocketServer } from 'ws';
8
8
  import { PublicLinkError, } from '@byokit/pair';
9
9
  import { reach } from '@byokit/discover';
10
- import { ENGINE_VERSION } from "./constants.js";
11
- import { b64urlDecode, b64urlEncode, sealNotice } from "./notices.js";
12
- import { routeFor } from "./routes.js";
13
- import { outputSchema } from "./output.js";
14
- import { stateWords, words } from "./words.js";
10
+ import { ENGINE_VERSION } from './constants.js';
11
+ import { b64urlDecode, b64urlEncode, sealNotice } from './notices.js';
12
+ import { routeFor } from './routes.js';
13
+ import { outputSchema } from './output.js';
14
+ import { stateWords, words } from './words.js';
15
15
  const VIEW_OPS = new Set(['oc.state', 'oc.routes', 'oc.signin.view', 'oc.sessions', 'oc.approvals', 'oc.events',
16
16
  'oc.browser.state', 'oc.browser.signins', 'oc.browser.thumb']);
17
17
  // This kit's own version, read once from its package.json (beside src/ and dist/ alike).
@@ -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/migrate.js CHANGED
@@ -2,9 +2,9 @@
2
2
  // only when the Gateway itself reports every provider signed in.
3
3
  import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
4
4
  import { join } from 'node:path';
5
- import { retireArchive } from "./auth-store.js";
6
- import { MEMBER_ID } from "./members.js";
7
- import { providers } from "./signin.js";
5
+ import { retireArchive } from './auth-store.js';
6
+ import { MEMBER_ID } from './members.js';
7
+ import { providers } from './signin.js';
8
8
  const MOVED = '.moved-to-engine';
9
9
  const MARKER = '.canonicalized';
10
10
  const TRY_MS = 1_500;
package/dist/pick.js CHANGED
@@ -1,4 +1,4 @@
1
- import { words } from "./words.js";
1
+ import { words } from './words.js';
2
2
  const DAY_MS = 24 * 60 * 60 * 1000;
3
3
  const finite = (n) => typeof n === 'number' && Number.isFinite(n);
4
4
  /** The lowercased provider of a well-formed `provider/model` reference; undefined when the reference is malformed. */
@@ -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);