@byokit/openclaw 0.24.0 → 0.26.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/CHANGELOG.md CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.26.0 (2026-10-20)
6
+
7
+ - Dependency update: pins @byokit/accounts 0.30.0.
8
+
9
+ - FEAT: every key route now lists its own models: `kit.models(member, { authChoice })` answers the provider plugin's catalog joined by the vendor's own `/models` (not only OpenCode Go, OpenRouter and MiniMax), and `addKey({ ..., model })` takes any full `provider/model` id on any key route. An id the catalog lags is taken with the route's own capabilities instead of being refused; only a route with no catalog at all refuses a model.
10
+ - The bridge's Codex CLI route now names its two model rows itself instead of reading them from `@byokit/accounts`' catalogue, which no longer keeps model tiers; the rows the engine lists are unchanged.
11
+
12
+ ## 0.25.0 (2026-10-20)
13
+
14
+ - Dependency update: pins @byokit/accounts 0.29.0.
15
+
16
+ - FEAT: a MiniMax API key now lists and runs any model MiniMax's own `/models` lists, with no BYOKit model list: `kit.models({ authChoice: 'minimax-global-api' })` answers the vendor's models, and `addKey({ ..., model })` takes an id the list lags (such as `MiniMax-M3.1-Flash-Preview`) with the route's own capabilities (image input included) instead of refusing it or falling back to `MiniMax-M3`.
17
+ - FIX: When a child a run sent work to (`sessions_send`, `timeoutSeconds: 0`) finished, the engine's own reply run bringing its report back to the sender's session was refused `unknown run` at every app tool. That return run is now admitted as the sender: same member and helper, same app-tool subset and delegates, and the sender's system text as its persona; `RunRef.parent` names the finished child. Any other run on that session is still refused.
18
+
5
19
  ## 0.24.0 (2026-10-20)
6
20
 
7
21
  - FEAT: a helper's delegated run now speaks as its helper. A native `sessions_send` carries no system text, so the kit's plugin adds the helper's recorded persona (from `ensureHelper`) as system context to the admitted helper child's run, and only to it. No app change: Chief's `helperDelegates` send to `agent:<agent>:h-<helper>:<rest>` is enough. The kit plugin's config entry now sets `hooks.allowConversationAccess: true` for this hook. `sessions_spawn` children get no persona.
package/README.md CHANGED
@@ -159,10 +159,10 @@ other choice that takes no key (a subscription such as `openai`, a provider with
159
159
  throws its cause before anything is sent.
160
160
  `reason` is `auth` (provider refused the key) or `billing` (the provider's billing refused it; the kit words it and never passes
161
161
  provider text): both are `invalid`, and an unchanged key must not be retried. `empty` is a blank key, rejected locally before any
162
- provider call, also `invalid`. `model` is an unsupported route or unknown model (`not_included`), or a live model the engine did not
162
+ provider call, also `invalid`. `model` is an unsupported route or a model on a route with no catalog (`not_included`), or a model the engine did not
163
163
  register (`retry`). `network` is a timeout or transport failure; `inference` is a provider reply in an unexpected format, an unknown
164
164
  probe failure, or a typed request-validation refusal; `engine` is a control or preparation failure; `busy` is a rate limit or a
165
- restarting engine. `not_included` covers an unsupported route, an unknown chosen model and a probe reporting the route unavailable;
165
+ restarting engine. `not_included` covers an unsupported route, a chosen model on a route with no catalog and a probe reporting the route unavailable;
166
166
  every other failure is `retry`. Migrate from the old string result with `result.status === 'ok'`, not `result === 'ok'`.
167
167
  An OpenCode Go plan login can act for several workspaces, and the Go gateway refuses its every call until one is named
168
168
  (`403 Workspace selection is required`). Pass the workspace as `orgId` when you have it; it rides the probe and every run as
@@ -185,16 +185,19 @@ live catalog. Before a key exists it returns `[]` (a failed catalog probe with n
185
185
  empty; with one it is an error) and runs no engine discovery. Only available key-lane
186
186
  models can be saved; the ordinary member's sign-ins never supply the list. Pass a full catalog id, e.g.
187
187
  `model: 'opencode-go/muse-spark-1.3-contributor'`, to `addKey` to save it as the lane's default after activation.
188
- Model listing and selection belong to the live-catalog routes, OpenCode Go and OpenRouter. An OpenRouter key is
188
+ Every key route lists and takes models: its catalog is the provider plugin's own, joined by the vendor's own `/models`
189
+ list, and the kit keeps no provider or model list. An OpenRouter key is
189
190
  pay-as-you-go with no plan, so it takes any model its own live catalog lists, named `openrouter/<id>`, e.g.
190
191
  `model: 'openrouter/deepseek/deepseek-v4.1-flash-1017'`; an id already beginning with its own provider keeps one
191
192
  prefix (`openrouter/auto`). A Google preview id is named and saved as the canonical id the engine keeps
192
193
  (`openrouter/google/gemini-3.1-flash-lite-preview` becomes `openrouter/google/gemini-3.1-flash-lite`); the add
193
- registers that one model with the engine. Other key routes list no models, and a `model` on them returns
194
- `not_included`/`model` before activation.
195
- An unknown model returns `{ status: 'not_included', reason: 'model' }` and clears the replacement.
194
+ registers that one model with the engine (every provider but OpenCode Go and MiniMax, whose whole lists are registered).
195
+ A vendor's list can lag a model it already runs, so an id the catalog lacks is taken, not refused: it rides the route's
196
+ own models (the richest row's api, base URL and context), unpriced, and the app sees it missing from `models()` to warn.
197
+ A route with no catalog rows (its plugin has none) lists nothing, and a `model` there returns
198
+ `{ status: 'not_included', reason: 'model' }` and clears the replacement.
196
199
  Before success, `addKey` waits for catalog registration and prepares the new key agent's full-catalog owner once.
197
- For a live-catalog provider, a chosen model must also be in the engine's registered `models.providers[provider].models`
200
+ A chosen model must also be in the engine's registered `models.providers[provider].models`
198
201
  list before anything is sealed; otherwise the add clears the attempt and returns `{ status: 'retry', reason: 'model' }`.
199
202
  Preparation failure clears the attempted key and returns `{ status: 'retry', reason: 'engine' }`; ordinary runs stay unchanged.
200
203
  With no choice, today's starter stays.
@@ -452,6 +455,13 @@ and terminal observations with exact `runId`, `agentId`, `member`, `sessionKey`,
452
455
  The same identity reaches `ToolHost`; each delegated action has its own gate/approval. Tool arguments/results
453
456
  are omitted from this event surface. An accepted native dispatch is not finished work or proof of delivery.
454
457
 
458
+ When a sent child finishes, the engine starts its own reply run on the sender's session to bring the report back.
459
+ The kit admits that exact run as the sender: same member and helper, same app-tool subset and delegates, the sender's
460
+ `system` as its persona, `RunRef.parent` naming the finished child. Only a run whose prompt the engine headed with that
461
+ child's session, on the sender's exact key, within a minute of the child's (or the previous return turn's) end, binds;
462
+ any other run there stays unknown. Return runs are not on `delegatedRuns()` or `byokit.delegated`; read them from the
463
+ engine's `agent` events on the sender's key. The pinned engine starts no reply run for a child that took over 30 s.
464
+
455
465
  Admitted exact run IDs survive parent settlement and are reconciled on reconnect/start; a child the engine no longer
456
466
  knows (as after an engine restart) ends `lost`, and a dispatch that finds the journal full reconciles first. `abort(rootSessionKey)`
457
467
  durably revokes its recorded subtree, closes parked gates/active calls, then asks the engine to abort; a child key
package/dist/bridge.js CHANGED
@@ -3,7 +3,6 @@ import { randomBytes } from 'node:crypto';
3
3
  import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
4
4
  import { createServer } from 'node:net';
5
5
  import { rm } from 'node:fs/promises';
6
- import { provider } from '@byokit/accounts';
7
6
  import { words } from './words.js';
8
7
  import { HelperError } from './helpers.js';
9
8
  /** Newline-framed JSON, one request per connection; anything larger is not a gate request. */
@@ -40,8 +39,9 @@ export function writePlugin(dir, o) {
40
39
  name: 'BYOKit bridge',
41
40
  activation: { onStartup: true },
42
41
  cliBackends: ['openai-codex'],
43
- // The Codex CLI route lists the ChatGPT catalogue's models; the engine lists a plugin route's rows only from here.
44
- modelCatalog: { providers: { 'openai-codex': { models: [provider('chatgpt').models.strong, provider('chatgpt').models.fast].map((id) => ({ id, name: id })) } }, discovery: { 'openai-codex': 'static' } },
42
+ // The engine lists a plugin route's rows only from here. Named exception: the Codex CLI's own list (its app-server
43
+ // `model/list`) is not read yet, so the route names these two ids; a recorded real `model/list` reply lifts it.
44
+ modelCatalog: { providers: { 'openai-codex': { models: ['gpt-6-sol', 'gpt-6-luna'].map((id) => ({ id, name: id })) } }, discovery: { 'openai-codex': 'static' } },
45
45
  contracts: { tools: o.tools.map((t) => t.name), ...(o.browser ? { agentToolResultMiddleware: ['openclaw', 'codex'] } : {}) },
46
46
  configSchema: { type: 'object', additionalProperties: false },
47
47
  };
@@ -332,11 +332,13 @@ export class Bridge {
332
332
  delegations.event(message.event);
333
333
  this.reply(socket, { ok: true });
334
334
  }
335
- // The plugin's prompt hook on a helper key: an admitted helper child speaks as its helper (5.8.3).
335
+ // The plugin's prompt hook on a helper key or an inter-session prompt: an admitted helper child speaks as its
336
+ // helper (5.8.3), a return run as the kit run that sent the work (5.8.4).
336
337
  else if (message.kind === 'delegation-persona' && typeof message.key === 'string') {
337
338
  const runId = typeof message.runId === 'string' ? message.runId : undefined;
338
339
  // A kit run carries its own persona; only an engine-created child is looked up (it may wait for its receipt).
339
- const persona = runId && this.isRegisteredRun(message.key, runId) ? undefined : await delegations.persona(message.key, runId);
340
+ const persona = runId && this.isRegisteredRun(message.key, runId) ? undefined
341
+ : await delegations.persona(message.key, runId, typeof message.header === 'string' ? message.header : undefined);
340
342
  this.reply(socket, persona === undefined ? {} : { persona });
341
343
  }
342
344
  else
@@ -1,5 +1,4 @@
1
1
  export declare const ENGINE_VERSION = "2026.8.35";
2
2
  export declare const PROTOCOL_VERSION = 4;
3
- export declare const LIVE_CATALOGS: string[];
4
3
  export declare const REGISTERED_CATALOGS: string[];
5
4
  export declare const OPERATOR_SCOPES: readonly ['operator.read', 'operator.write', 'operator.admin', 'operator.approvals', 'operator.questions', 'operator.pairing', 'operator.talk'];
package/dist/constants.js CHANGED
@@ -2,12 +2,9 @@
2
2
  // `needs-update`, never silently run.
3
3
  export const ENGINE_VERSION = '2026.8.35';
4
4
  export const PROTOCOL_VERSION = 4;
5
- // Providers whose live catalog lists and checks a key's chosen model (5.6), through the catalog hook. OpenRouter is
6
- // pay-as-you-go: its key takes any model its own catalog lists, with no plan to include it.
7
- export const LIVE_CATALOGS = ['opencode-go', 'openrouter'];
8
- // Live catalogs the kit registers whole. The engine canonicalizes Google preview ids on config write, so OpenRouter's
5
+ // Live catalogs (5.6) the kit registers whole; every other key route registers its chosen model alone. The engine canonicalizes Google preview ids on config write, so OpenRouter's
9
6
  // list collides with itself (`google/x-preview` and `google/x`); an OpenRouter add registers only the chosen model.
10
- export const REGISTERED_CATALOGS = ['opencode-go'];
7
+ export const REGISTERED_CATALOGS = ['opencode-go', 'minimax'];
11
8
  export const OPERATOR_SCOPES = [
12
9
  'operator.read',
13
10
  'operator.write',
@@ -1,16 +1,26 @@
1
1
  import type { DelegatedEvent, DelegatedRun, GatewayTransport, RunRef } from './types.ts';
2
+ type Back = {
3
+ member: string;
4
+ helper?: string;
5
+ tools: string[];
6
+ helpers?: Record<string, string[]>;
7
+ };
2
8
  type Grant = {
3
9
  ref: DelegatedRun;
4
10
  tools: string[];
5
11
  delegates: Record<string, string[]>;
6
12
  helpers?: Record<string, string[]>;
7
13
  sourceKey: string;
14
+ back?: Back;
8
15
  };
9
16
  export declare class Delegations {
10
17
  private loaded;
11
18
  private roots;
12
19
  private children;
13
20
  private pending;
21
+ private personas;
22
+ private armed;
23
+ private returns;
14
24
  private changed;
15
25
  private early;
16
26
  constructor(privateOptions: {
@@ -31,15 +41,19 @@ export declare class Delegations {
31
41
  private load;
32
42
  private save;
33
43
  validate(delegates: Record<string, string[]>, member?: string, helpers?: Record<string, string[]>): void;
34
- register(ref: RunRef, runId: string, tools: readonly string[] | undefined, delegates: Record<string, string[]>, sourceKey?: string, helpers?: Record<string, string[]>): () => void;
44
+ register(ref: RunRef, runId: string, tools: readonly string[] | undefined, delegates: Record<string, string[]>, sourceKey?: string, helpers?: Record<string, string[]>, system?: string): () => void;
35
45
  list(): DelegatedRun[];
36
46
  has(key: string, runId: string): boolean;
37
47
  private full;
38
48
  prepare(m: Record<string, any>): Promise<boolean>;
39
49
  receipt(m: Record<string, any>): void;
40
50
  resolve(key: string, runId?: string): Promise<Grant | undefined>;
41
- /** The persona of the admitted helper child on this exact key and run, as its own run would carry it (5.8.3). */
42
- persona(key: string, runId?: string): Promise<string | undefined>;
51
+ /** The persona of the admitted helper child on this exact key and run, as its own run would carry it (5.8.3), or of
52
+ * the return run on its sender's key (5.8.4): `header` is the engine's inter-session header on that run's prompt. */
53
+ persona(key: string, runId?: string, header?: string): Promise<string | undefined>;
54
+ private bind;
55
+ private returned;
56
+ private arm;
43
57
  private bound;
44
58
  event(event: Record<string, any>): void;
45
59
  private ended;
@@ -5,6 +5,11 @@ import { classify } from './classify.js';
5
5
  import { HELPER_ID, HelperError, helperOf } from './helpers.js';
6
6
  // A resting end whose message names no time rests the account this long, as a kit run's does (runs.ts).
7
7
  const REST_MS = 300_000;
8
+ // A return binds within this long of its child's end (or of its previous turn's end): the engine's own reply step waits
9
+ // at most 60 s for the child before it starts.
10
+ const RETURN_MS = 60_000;
11
+ // The engine's own header on an inter-session prompt (pin `annotateInterSessionPromptText`), naming the source session.
12
+ const INTER_SESSION = /^\[Inter-session message\] sourceSession=(\S+)(?: sourceChannel=\S+)? sourceTool=sessions_send isUser=false$/;
8
13
  const object = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
9
14
  const agentOf = (key) => /^agent:([^:]+):.+/.exec(key)?.[1];
10
15
  const actionKey = (m) => JSON.stringify([m.key, m.runId, m.callId]);
@@ -13,6 +18,10 @@ export class Delegations {
13
18
  roots = new Map();
14
19
  children = new Map();
15
20
  pending = new Map();
21
+ // In memory only (no prompt is persisted): a kit run's system text by its run id, its return runs' persona.
22
+ personas = new Map();
23
+ armed = [];
24
+ returns = new Map();
16
25
  changed = new Set();
17
26
  early = new Map();
18
27
  constructor(privateOptions) { this.o = privateOptions; }
@@ -56,9 +65,13 @@ export class Delegations {
56
65
  this.o.helper(member, helper); // never ensured, or its account gone: unknown_helper
57
66
  }
58
67
  }
59
- register(ref, runId, tools, delegates, sourceKey = ref.sessionKey, helpers) {
68
+ register(ref, runId, tools, delegates, sourceKey = ref.sessionKey, helpers, system = '') {
60
69
  this.load();
61
70
  this.validate(delegates, ref.member, helpers);
71
+ // ponytail: the oldest 256 kit runs' system texts are kept; a child of an older one gets no return run (fail closed).
72
+ this.personas.set(runId, system);
73
+ while (this.personas.size > 256)
74
+ this.personas.delete(this.personas.keys().next().value);
62
75
  const root = { ref: { ...ref, runId, root: { sessionKey: ref.sessionKey, runId } },
63
76
  tools: [...tools ?? this.o.tools], delegates: structuredClone(delegates), ...(helpers ? { helpers: structuredClone(helpers) } : {}), sourceKey };
64
77
  this.roots.set(runId, root);
@@ -68,7 +81,7 @@ export class Delegations {
68
81
  list() { this.load(); return [...this.children.values()].map(g => structuredClone(g.ref)); }
69
82
  has(key, runId) {
70
83
  this.load();
71
- return (this.roots.get(runId) ?? this.children.get(runId))?.ref.sessionKey === key;
84
+ return (this.roots.get(runId) ?? this.children.get(runId) ?? this.returns.get(runId))?.ref.sessionKey === key;
72
85
  }
73
86
  full() {
74
87
  for (const [id, p] of this.pending)
@@ -83,7 +96,7 @@ export class Delegations {
83
96
  // ponytail: a full journal of live children re-reads all 1024 per dispatch; index ends by age if that shows up.
84
97
  if (this.full())
85
98
  await this.reconcile();
86
- const parent = this.roots.get(m.runId) ?? this.children.get(m.runId);
99
+ const parent = this.roots.get(m.runId) ?? this.children.get(m.runId) ?? this.returns.get(m.runId);
87
100
  if (!parent || parent.ref.sessionKey !== m.key)
88
101
  return false;
89
102
  if (typeof m.callId !== 'string' || !m.callId || !object(m.input))
@@ -111,8 +124,11 @@ export class Delegations {
111
124
  }
112
125
  if (this.full())
113
126
  throw new Error('delegated run journal is full');
114
- this.pending.set(actionKey(m), { parent: structuredClone(parent), agent: agent,
115
- ...(send ? { key: m.input.sessionKey } : {}), ...(helper !== undefined ? { helper } : {}), expires: Date.now() + 60_000 });
127
+ const { persona: _, ...grant } = parent; // a return's persona is never persisted
128
+ this.pending.set(actionKey(m), { parent: structuredClone(grant), agent: agent,
129
+ ...(send ? { key: m.input.sessionKey } : {}), ...(helper !== undefined ? { helper } : {}),
130
+ // The engine brings a sent child's report back to the sender's key, as a return run (5.8.4); a spawn's is not one.
131
+ ...(send && !this.children.has(m.runId) ? { back: true } : {}), expires: Date.now() + 60_000 });
116
132
  this.save();
117
133
  return true;
118
134
  }
@@ -143,7 +159,9 @@ export class Delegations {
143
159
  parent: { sessionKey: p.parent.ref.sessionKey, runId: p.parent.ref.runId }, root: p.parent.ref.root, tools };
144
160
  const grant = { ref, delegates: p.parent.delegates,
145
161
  ...(p.parent.helpers && ref.member === p.parent.ref.member ? { helpers: p.parent.helpers } : {}),
146
- sourceKey: p.parent.sourceKey, tools };
162
+ sourceKey: p.parent.sourceKey, tools,
163
+ ...(p.back ? { back: { member: p.parent.ref.member, ...(p.parent.ref.helper !== undefined ? { helper: p.parent.ref.helper } : {}),
164
+ tools: p.parent.tools, ...(p.parent.helpers ? { helpers: p.parent.helpers } : {}) } } : {}) };
147
165
  // A steered/already-admitted run keeps its original parent and cannot acquire a broader grant.
148
166
  if (this.children.has(ref.runId)) {
149
167
  this.save();
@@ -160,6 +178,13 @@ export class Delegations {
160
178
  this.load();
161
179
  if (!runId)
162
180
  return undefined;
181
+ const back = this.returns.get(runId);
182
+ if (back?.ref.sessionKey === key) {
183
+ if (back.ref.helper === undefined || this.bound(back.ref.member, back.ref.helper, back.ref.agentId))
184
+ return back;
185
+ this.returned(runId);
186
+ return undefined;
187
+ }
163
188
  const get = () => { const g = this.children.get(runId); return g?.ref.sessionKey === key ? g : undefined; };
164
189
  if (!get() && [...this.pending.values()].some(p => p.expires > Date.now())) {
165
190
  await new Promise(resolve => {
@@ -179,11 +204,68 @@ export class Delegations {
179
204
  }
180
205
  return grant;
181
206
  }
182
- /** The persona of the admitted helper child on this exact key and run, as its own run would carry it (5.8.3). */
183
- async persona(key, runId) {
207
+ /** The persona of the admitted helper child on this exact key and run, as its own run would carry it (5.8.3), or of
208
+ * the return run on its sender's key (5.8.4): `header` is the engine's inter-session header on that run's prompt. */
209
+ async persona(key, runId, header) {
210
+ this.load();
211
+ const source = header === undefined ? undefined : INTER_SESSION.exec(header)?.[1];
212
+ if (runId && source !== undefined && !this.returns.has(runId) && !this.children.has(runId))
213
+ await this.bind(key, runId, source);
214
+ const back = runId ? this.returns.get(runId) : undefined;
215
+ if (back?.ref.sessionKey === key)
216
+ return back.persona || undefined;
217
+ if (!/^agent:[^:]+:h-/.test(key))
218
+ return undefined;
184
219
  const helper = (await this.resolve(key, runId))?.ref;
185
220
  return helper?.helper === undefined ? undefined : this.o.helper(helper.member, helper.helper).persona;
186
221
  }
222
+ // The engine's reply step for a sent child starts a fresh run on the sender's exact key, its prompt headed by the
223
+ // child's exact session (pin `runSessionsSendA2AFlow`): bound only to a return armed by that child's own terminal end,
224
+ // one run at a time. A child still running when its report arrives is waited for briefly.
225
+ async bind(key, runId, source) {
226
+ const find = () => {
227
+ const now = Date.now();
228
+ this.armed = this.armed.filter(a => a.expires > now);
229
+ return this.armed.find(a => a.key === key && a.child.sessionKey === source && a.live === undefined);
230
+ };
231
+ if (!find() && [...this.children.values()].some(g => g.back && g.ref.sessionKey === source && g.ref.parent.sessionKey === key)) {
232
+ await new Promise(resolve => {
233
+ const done = () => { clearTimeout(timer); this.changed.delete(check); resolve(); };
234
+ const check = () => { if (find())
235
+ done(); };
236
+ const timer = setTimeout(done, 5_000);
237
+ this.changed.add(check);
238
+ });
239
+ }
240
+ const armed = find();
241
+ if (!armed)
242
+ return;
243
+ armed.live = runId;
244
+ this.returns.set(runId, { ...structuredClone(armed.grant), ref: { ...structuredClone(armed.grant.ref), runId }, persona: armed.persona });
245
+ }
246
+ // A return run ended: its report's next turn on the same key may bind within the window (the engine's ping-pong).
247
+ returned(runId) {
248
+ const back = this.returns.get(runId);
249
+ if (!back)
250
+ return;
251
+ this.returns.delete(runId);
252
+ this.o.revoke(back.ref.sessionKey, runId);
253
+ const armed = this.armed.find(a => a.live === runId);
254
+ if (armed) {
255
+ armed.live = undefined;
256
+ armed.expires = Date.now() + RETURN_MS;
257
+ }
258
+ }
259
+ arm(child) {
260
+ const { back, ref } = child, persona = this.personas.get(ref.root.runId);
261
+ if (!back || persona === undefined || this.armed.length >= 64)
262
+ return; // the root's persona unknown (a restart): none
263
+ const key = ref.parent.sessionKey;
264
+ this.armed.push({ key, child: { sessionKey: ref.sessionKey, runId: ref.runId }, persona, expires: Date.now() + RETURN_MS,
265
+ grant: { ref: { sessionKey: key, member: back.member, ...(back.helper !== undefined ? { helper: back.helper } : {}),
266
+ agentId: agentOf(key), parent: { sessionKey: ref.sessionKey, runId: ref.runId }, root: ref.root, tools: back.tools },
267
+ tools: back.tools, delegates: child.delegates, ...(back.helpers ? { helpers: back.helpers } : {}), sourceKey: child.sourceKey } });
268
+ }
187
269
  bound(member, helper, agent) {
188
270
  try {
189
271
  return this.o.helper(member, helper).agentId === agent;
@@ -194,6 +276,12 @@ export class Delegations {
194
276
  }
195
277
  event(event) {
196
278
  this.load();
279
+ const back = this.returns.get(event.runId);
280
+ if (back) {
281
+ if (event.sessionKey === back.ref.sessionKey && event.stream === 'lifecycle' && ['end', 'error'].includes(event.data?.phase))
282
+ this.returned(event.runId);
283
+ return;
284
+ }
197
285
  const grant = this.children.get(event.runId);
198
286
  if (!grant) {
199
287
  if (this.pending.size && typeof event.runId === 'string' && this.early.size < 64) {
@@ -232,6 +320,8 @@ export class Delegations {
232
320
  }
233
321
  end(grant, end) {
234
322
  this.children.delete(grant.ref.runId);
323
+ if (end.reason === 'terminal')
324
+ this.arm(grant); // before save: a return run waiting on this end sees it armed
235
325
  this.save();
236
326
  this.o.revoke(grant.ref.sessionKey, grant.ref.runId);
237
327
  this.o.emit({ type: 'ended', run: structuredClone(grant.ref), ...end });
@@ -260,7 +350,7 @@ export class Delegations {
260
350
  async abort(key) {
261
351
  this.load();
262
352
  const ids = new Set();
263
- for (const g of [...this.roots.values(), ...this.children.values()])
353
+ for (const g of [...this.roots.values(), ...this.children.values(), ...this.returns.values()])
264
354
  if (g.ref.sessionKey === key || g.ref.parent?.sessionKey === key || g.ref.root.sessionKey === key || g.sourceKey === key)
265
355
  ids.add(g.ref.runId);
266
356
  for (;;) {
@@ -272,6 +362,10 @@ export class Delegations {
272
362
  break;
273
363
  }
274
364
  let removed = false;
365
+ const armed = this.armed.length;
366
+ this.armed = this.armed.filter(a => a.key !== key && a.grant.ref.root.sessionKey !== key && !ids.has(a.child.runId) && !ids.has(a.grant.ref.root.runId));
367
+ if (this.armed.length !== armed)
368
+ removed = true;
275
369
  for (const [id, p] of this.pending)
276
370
  if (ids.has(p.parent.ref.runId) || p.parent.ref.sessionKey === key || p.parent.ref.root.sessionKey === key || p.parent.sourceKey === key) {
277
371
  this.pending.delete(id);
@@ -280,9 +374,11 @@ export class Delegations {
280
374
  if (!ids.size && !removed)
281
375
  return;
282
376
  const cancelled = [...this.children.values()].filter(g => ids.has(g.ref.runId));
283
- const revoked = [...this.roots.values(), ...cancelled].filter(g => ids.has(g.ref.runId));
284
- for (const id of ids)
377
+ const revoked = [...this.roots.values(), ...this.returns.values(), ...cancelled].filter(g => ids.has(g.ref.runId));
378
+ for (const id of ids) {
285
379
  this.roots.delete(id);
380
+ this.returns.delete(id);
381
+ }
286
382
  for (const g of cancelled)
287
383
  this.children.delete(g.ref.runId);
288
384
  this.save(); // revoke durably before any awaited transport call
package/dist/keys.js CHANGED
@@ -5,7 +5,7 @@ import { activate } from './activation.js';
5
5
  import { keyAgentId, MEMBER_ID, KEY_PREFIX, untilListed } from './members.js';
6
6
  import { providerOf } from './pick.js';
7
7
  import { kitId, modelsOf } from './accounts.js';
8
- import { LIVE_CATALOGS, REGISTERED_CATALOGS } from './constants.js';
8
+ import { REGISTERED_CATALOGS } from './constants.js';
9
9
  export function createKeys(ctx) {
10
10
  // Serialize key replacement and key runs, so an in-flight run never sees a half-installed replacement.
11
11
  const tails = new Map();
@@ -93,12 +93,22 @@ export function createKeys(ctx) {
93
93
  throw new Error(keyed.length ? `${authChoice} takes no API key: add the key with ${keyed.map((r) => r.choice).join(' or ')}.`
94
94
  : `${authChoice} takes no API key.`);
95
95
  };
96
+ // The vendor's list can lag a model that runs: an id it lacks rides the route's own models (the richest row's api,
97
+ // base URL and context, every input modality any row takes), unpriced. Nothing is added to an empty list.
98
+ const unlisted = (provider, rows, model) => {
99
+ if (!rows.length || !model.startsWith(`${provider}/`) || rows.some((m) => kitId(provider, String(m.id)) === model))
100
+ return [];
101
+ const base = rows.reduce((a, b) => (Number(b.contextWindow) || 0) > (Number(a.contextWindow) || 0) ? b : a);
102
+ const input = ['text', 'image'].filter((i) => rows.some((m) => Array.isArray(m.input) && m.input.includes(i)));
103
+ const id = model.slice(provider.length + 1);
104
+ return [{ ...base, id, name: id, input, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } }];
105
+ };
96
106
  const listed = (provider, rows) => modelsOf(provider, { models: rows.map((m) => ({ ...m, provider })) });
97
107
  const models = async (member, o) => {
98
108
  if (!check(member))
99
109
  throw new Error('Choose your own member.');
100
110
  const route = keyRoute(o.authChoice);
101
- if (route.readiness !== 'ready' || !LIVE_CATALOGS.includes(route.provider))
111
+ if (route.readiness !== 'ready')
102
112
  return [];
103
113
  try {
104
114
  return listed(route.provider, await catalog(member, route.provider));
@@ -128,7 +138,7 @@ export function createKeys(ctx) {
128
138
  if (!o.apiKey.trim())
129
139
  return failure('invalid', 'empty');
130
140
  const route = keyRoute(o.authChoice);
131
- if (route.readiness !== 'ready' || (o.model !== undefined && !LIVE_CATALOGS.includes(route.provider)))
141
+ if (route.readiness !== 'ready')
132
142
  return failure('not_included', 'model');
133
143
  const model = o.model === undefined ? undefined : canonicalModel(o.model);
134
144
  // A workspace names a Go login's own workspace only; another route takes no header, and a
@@ -217,6 +227,8 @@ export function createKeys(ctx) {
217
227
  if (result.late)
218
228
  return abandon(failure('retry', 'busy'));
219
229
  const rows = model === undefined ? [] : await catalog(member, route.provider);
230
+ const lagging = model !== undefined ? unlisted(route.provider, rows, model) : [];
231
+ rows.push(...lagging);
220
232
  if (model !== undefined && !listed(route.provider, rows).some((m) => m.id === model && m.available))
221
233
  return abandon(failure('not_included', 'model'));
222
234
  // Prepare before publishing the ready marker: failed preparation remains unusable even if clear fails.
@@ -228,10 +240,10 @@ export function createKeys(ctx) {
228
240
  }
229
241
  if (ac.signal.aborted)
230
242
  return abandon(cancelled());
231
- // A live catalog the kit does not register whole (OpenRouter) registers the chosen model alone.
232
- if (model !== undefined && !REGISTERED_CATALOGS.includes(route.provider))
243
+ // A catalog the kit does not register whole (OpenRouter's, and any other provider's) registers the chosen model alone.
244
+ if (model !== undefined && (!REGISTERED_CATALOGS.includes(route.provider) || lagging.length))
233
245
  await ctx.registerModels(route.provider, rows.filter((m) => kitId(route.provider, String(m.id)) === model)).catch(() => { });
234
- if (model !== undefined && LIVE_CATALOGS.includes(route.provider) && !(await registered(route.provider, model)))
246
+ if (model !== undefined && !(await registered(route.provider, model)))
235
247
  return abandon(failure('retry', 'model'));
236
248
  if (!(await control(member, 'seal', undefined, model)).ok)
237
249
  return abandon(failure('retry', 'engine'));
package/dist/runs.js CHANGED
@@ -347,7 +347,7 @@ export function createRuns(ctx) {
347
347
  try {
348
348
  const ref = { sessionKey, member: spec.member, ...(spec.helper !== undefined ? { helper: spec.helper } : {}) };
349
349
  releaseDelegation = spec.delegates || spec.helperDelegates
350
- ? ctx.delegations.register(ref, idempotencyKey, spec.tools, spec.delegates ?? {}, spec.sessionKey, spec.helperDelegates) : undefined;
350
+ ? ctx.delegations.register(ref, idempotencyKey, spec.tools, spec.delegates ?? {}, spec.sessionKey, spec.helperDelegates, system) : undefined;
351
351
  release = spec.register !== false ? ctx.bridge.register(ref, spec.tools, idempotencyKey) : undefined;
352
352
  if (picked && !chosen) {
353
353
  // The picked account must be the one called: check it is signed in for this member before the run, since
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@byokit/openclaw",
3
- "version": "0.24.0",
3
+ "version": "0.26.0",
4
4
  "description": "The OpenClaw runtime kit: the pinned engine's full operator surface with plain-words helpers for members, sign-in, runs and approvals.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -60,7 +60,7 @@
60
60
  "prepack": "tsc -b && node ../../scripts/fix-words-dts.cjs"
61
61
  },
62
62
  "dependencies": {
63
- "@byokit/accounts": "0.28.2",
63
+ "@byokit/accounts": "0.30.0",
64
64
  "@byokit/pair": "0.10.4",
65
65
  "@byokit/relay": "0.8.2",
66
66
  "@byokit/discover": "0.8.4",
package/plugin/catalog.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // byokit.catalog (5.6 live provider catalogs): the provider plugin's own live model list, with each model's api and
2
- // baseUrl from its catalog data, so the kit can register models embedded runs cannot resolve otherwise. The engine's
3
- // catalog loader stops at a registered list; the provider's catalog hook does not.
2
+ // baseUrl from its catalog data, joined by the vendor's own model list, so the kit can register models embedded runs
3
+ // cannot resolve otherwise. The engine's catalog loader stops at a registered list; the provider's catalog hook does not.
4
4
  export function registerCatalog(api) {
5
5
  api.registerGatewayMethod('byokit.catalog', async ({ params, respond }) => {
6
6
  try {
@@ -11,7 +11,8 @@ export function registerCatalog(api) {
11
11
  const { resolveApiKeyForProvider } = await import('openclaw/plugin-sdk/provider-auth-runtime');
12
12
  const config = getRuntimeConfig();
13
13
  const hook = resolvePluginProviders({ config, env: process.env }).find((p) => p.id === provider)?.catalog;
14
- if (!provider || typeof hook?.run !== 'function') return respond(true, { ok: false, reason: 'failed' });
14
+ if (!provider) return respond(true, { ok: false, reason: 'failed' });
15
+ if (typeof hook?.run !== 'function') return respond(true, { ok: true, models: [] }); // a provider with no catalog lists nothing
15
16
  // The first agent holding the provider's key; every account of one provider sees the same catalog.
16
17
  let unreadable = false;
17
18
  for (const agentId of new Set([...Object.keys(config.agents?.entries ?? {}), 'main'])) {
@@ -38,6 +39,22 @@ export function registerCatalog(api) {
38
39
  const models = [...live.models, ...(pinned?.models ?? []).filter((m) => !liveIds.has(m.id))]
39
40
  .map(({ provider: _p, apiKey: _k, headers: _h, ...model }) =>
40
41
  (endpoint.has(model.api) ? { ...model, baseUrl: endpoint.get(model.api) } : model));
42
+ // A plugin catalog can be a fixed list (MiniMax's): the vendor's own `/models` lists newer ids, which join it with
43
+ // the richest row's api, base URL and capabilities (a vendor list carries ids only). Same rule as
44
+ // @byokit/accounts' live lists; another api, or a failed fetch, keeps the plugin's rows.
45
+ const base = models.reduce((a, b) => (b.contextWindow ?? 0) > (a.contextWindow ?? 0) ? b : a);
46
+ const openai = String(base.api).startsWith('openai-');
47
+ if (base.baseUrl && (openai || base.api === 'anthropic-messages')) {
48
+ const input = ['text', 'image'].filter((i) => models.some((m) => m.input?.includes(i)));
49
+ const bearer = openai || /^minimax/.test(provider);
50
+ const listed = await fetch(`${String(base.baseUrl).replace(/\/$/, '')}${openai ? '/models' : '/v1/models'}`, { signal: AbortSignal.timeout(10_000),
51
+ headers: bearer ? { authorization: `Bearer ${apiKey}`, 'anthropic-version': '2023-06-01' } : { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01' } })
52
+ .then((r) => r.ok ? r.json() : null, () => null);
53
+ const have = new Set(models.map((m) => m.id));
54
+ for (const m of Array.isArray(listed?.data) ? listed.data : [])
55
+ if (typeof m?.id === 'string' && m.id && !have.has(m.id))
56
+ models.push({ ...base, id: m.id, name: m.display_name || m.id, input, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } });
57
+ }
41
58
  return respond(true, { ok: true, models });
42
59
  }
43
60
  respond(true, { ok: false, reason: unreadable ? 'failed' : 'no-key' });
package/plugin/index.js CHANGED
@@ -160,12 +160,16 @@ export default {
160
160
  result: { details: d && { status: d.status, runId: d.runId, sessionKey: d.sessionKey, childSessionKey: d.childSessionKey } } });
161
161
  });
162
162
  // A native send carries no system text: an admitted helper child gets its helper's persona from the kit (5.8.3).
163
- // Any other run, or no answer, adds nothing; the persona is the helper's voice, never a grant.
164
- api.on('before_prompt_build', async (_event, ctx) => {
163
+ // The engine's reply step bringing a sent child's report back (an inter-session prompt) asks too: the kit binds that
164
+ // exact run as the sender's return run (5.8.4) and answers the sender's persona. Any other run, or no answer, adds
165
+ // nothing; the persona is a voice, never a grant.
166
+ api.on('before_prompt_build', async (event, ctx) => {
165
167
  const key = ctx?.sessionKey;
166
- if (typeof key !== 'string' || !/^agent:[^:]+:h-[a-z][a-z0-9-]{0,15}:/.test(key)) return undefined;
168
+ const header = typeof event?.prompt === 'string' && event.prompt.startsWith('[Inter-session message] ')
169
+ ? event.prompt.slice(0, 1024).split('\n', 1)[0] : undefined;
170
+ if (typeof key !== 'string' || (!header && !/^agent:[^:]+:h-[a-z][a-z0-9-]{0,15}:/.test(key))) return undefined;
167
171
  try {
168
- const reply = await bridgeRequest({ kind: 'delegation-persona', key, runId: ctx?.runId }, { timeoutMs: 10_000 });
172
+ const reply = await bridgeRequest({ kind: 'delegation-persona', key, runId: ctx?.runId, ...(header ? { header } : {}) }, { timeoutMs: 10_000 });
169
173
  if (typeof reply?.persona === 'string' && reply.persona) return { appendSystemContext: reply.persona };
170
174
  } catch { /* no kit: nothing added */ }
171
175
  return undefined;